feat: integrate SimpleCADAPI 2.0.2 CAD workflows
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""Shared fixture construction and package-record helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from copy import deepcopy
|
||||
from typing import Any, Callable
|
||||
|
||||
from simplecadapi.scene import (
|
||||
canonical_json_bytes,
|
||||
validate_scene_package,
|
||||
)
|
||||
|
||||
|
||||
JsonObject = dict[str, Any]
|
||||
ScenePackage = tuple[JsonObject, JsonObject, dict[str, bytes]]
|
||||
Validator = Callable[[Any], Any]
|
||||
|
||||
IDENTITY = {
|
||||
"origin": [0, 0, 0],
|
||||
"x_axis": [1, 0, 0],
|
||||
"y_axis": [0, 1, 0],
|
||||
"z_axis": [0, 0, 1],
|
||||
}
|
||||
|
||||
|
||||
def b64(value: bytes) -> str:
|
||||
return base64.b64encode(value).decode("ascii")
|
||||
|
||||
|
||||
def sha256_hex(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def content_hash(value: bytes) -> str:
|
||||
return "sha256:" + sha256_hex(value)
|
||||
|
||||
|
||||
def first_issue(report: Any) -> dict[str, str] | None:
|
||||
issue = report.first_error
|
||||
return None if issue is None else {"code": issue.code, "path": issue.path}
|
||||
|
||||
|
||||
def report_case(name: str, payload: bytes, report: Any) -> dict[str, object]:
|
||||
return {
|
||||
"expected": first_issue(report),
|
||||
"name": name,
|
||||
"payload_base64": b64(payload),
|
||||
"valid": report.valid,
|
||||
}
|
||||
|
||||
|
||||
def transform(origin: list[float] | None = None) -> JsonObject:
|
||||
value = deepcopy(IDENTITY)
|
||||
if origin is not None:
|
||||
value["origin"] = origin
|
||||
return value
|
||||
|
||||
|
||||
def validation_case(
|
||||
name: str, value: Any, validator: Validator
|
||||
) -> dict[str, object]:
|
||||
payload = value if isinstance(value, bytes) else canonical_json_bytes(value)
|
||||
return report_case(name, payload, validator(payload))
|
||||
|
||||
|
||||
def replace_entity_sidecar(
|
||||
scene: JsonObject,
|
||||
entity: JsonObject,
|
||||
blobs: dict[str, bytes],
|
||||
) -> ScenePackage:
|
||||
entity_bytes = canonical_json_bytes(entity)
|
||||
entity_hash = content_hash(entity_bytes)
|
||||
entity_uri = f"entities/sha256-{entity_hash.removeprefix('sha256:')}.json"
|
||||
entity_record = scene["entity_assets"][0]
|
||||
old_uri = entity_record["uri"]
|
||||
entity_record.update(
|
||||
{
|
||||
"byte_length": len(entity_bytes),
|
||||
"content_hash": entity_hash,
|
||||
"entity_asset_id": entity_hash,
|
||||
"uri": entity_uri,
|
||||
}
|
||||
)
|
||||
for definition in scene["definitions"]:
|
||||
if definition["kind"] in {"part", "shape"}:
|
||||
definition["entity_asset_id"] = entity_hash
|
||||
updated_blobs = {uri: payload for uri, payload in blobs.items() if uri != old_uri}
|
||||
updated_blobs[entity_uri] = entity_bytes
|
||||
return scene, entity, updated_blobs
|
||||
|
||||
|
||||
def set_product_material_appearance(
|
||||
scene: JsonObject, material_id: str = "fixture_material"
|
||||
) -> None:
|
||||
appearance = scene["appearances"][0]
|
||||
appearance["name"] = "Fixture material"
|
||||
appearance["source"] = {
|
||||
"kind": "product_material",
|
||||
"material_id": material_id,
|
||||
"root_id": "root",
|
||||
}
|
||||
draft = dict(appearance)
|
||||
draft.pop("appearance_id")
|
||||
appearance_id = "appearance/evaluated/" + sha256_hex(canonical_json_bytes(draft))
|
||||
appearance["appearance_id"] = appearance_id
|
||||
for definition in scene["definitions"]:
|
||||
if definition["kind"] in {"part", "shape"}:
|
||||
definition["appearance_id"] = appearance_id
|
||||
|
||||
|
||||
def inline_package_case(
|
||||
name: str,
|
||||
scene: JsonObject,
|
||||
blobs: dict[str, bytes],
|
||||
blob_pool: dict[str, str],
|
||||
) -> dict[str, object]:
|
||||
scene_bytes = canonical_json_bytes(scene)
|
||||
report = validate_scene_package(scene_bytes, blobs)
|
||||
for uri, payload in blobs.items():
|
||||
encoded = b64(payload)
|
||||
previous = blob_pool.setdefault(uri, encoded)
|
||||
if previous != encoded:
|
||||
raise AssertionError(f"one fixture URI resolved to different bytes: {uri}")
|
||||
return {
|
||||
"blob_uris": sorted(blobs),
|
||||
"expected": first_issue(report),
|
||||
"manifest_base64": b64(scene_bytes),
|
||||
"name": name,
|
||||
"valid": report.valid,
|
||||
}
|
||||
|
||||
|
||||
def scene_shape_case(
|
||||
name: str,
|
||||
scene: JsonObject,
|
||||
blobs: dict[str, bytes],
|
||||
expected: dict[str, object],
|
||||
blob_pool: dict[str, str],
|
||||
) -> dict[str, object]:
|
||||
package = inline_package_case(name, scene, blobs, blob_pool)
|
||||
if not package["valid"]:
|
||||
raise AssertionError(f"invalid scene shape fixture {name}: {package['expected']}")
|
||||
return {**package, "expected_shape": expected}
|
||||
Reference in New Issue
Block a user