feat: integrate SimpleCADAPI 2.0.2 CAD workflows
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
"""Regenerate immutable cross-language Scene 1.0 contract vectors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
||||
from simplecadapi.scene import (
|
||||
BASE_LIMITS,
|
||||
canonical_json_bytes,
|
||||
validate_connector_binding,
|
||||
validate_entity_asset,
|
||||
validate_normalized_product,
|
||||
validate_presentation,
|
||||
validate_scene_manifest,
|
||||
validate_scene_package,
|
||||
with_scene_revision,
|
||||
)
|
||||
from scene_contract_fixtures.authored_cases import (
|
||||
build_connector_binding_matrix_cases,
|
||||
build_normalized_product_matrix_cases,
|
||||
build_presentation_matrix_cases,
|
||||
)
|
||||
from scene_contract_fixtures.base_scene import build_scene_package
|
||||
from scene_contract_fixtures.binary_cases import build_binary_cases
|
||||
from scene_contract_fixtures.common import b64, content_hash, first_issue, report_case
|
||||
from scene_contract_fixtures.entity_cases import build_entity_matrix_cases
|
||||
from scene_contract_fixtures.numeric_cases import build_numeric_vectors
|
||||
from scene_contract_fixtures.package_cases import build_package_matrix_cases
|
||||
from scene_contract_fixtures.resource_cases import build_resource_cases
|
||||
from scene_contract_fixtures.schema_field_matrix import build_schema_field_matrices
|
||||
from scene_contract_fixtures.scene_corpus import (
|
||||
build_revision_vectors,
|
||||
build_scene_field_cases,
|
||||
build_scene_shape_cases,
|
||||
)
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / "test" / "fixtures" / "scene-contract" / "corpus.json"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true", help="fail if output is stale")
|
||||
args = parser.parse_args()
|
||||
|
||||
scene, scene_bytes, entity, entity_bytes, blobs = build_scene_package()
|
||||
blob_pool = {uri: b64(payload) for uri, payload in blobs.items()}
|
||||
scene_shape_cases, scene_packages = build_scene_shape_cases(
|
||||
scene, entity, blobs, blob_pool
|
||||
)
|
||||
scene_field_cases = build_scene_field_cases(scene)
|
||||
geometry_uri = next(uri for uri in blobs if uri.startswith("geometry/"))
|
||||
edge_uri = next(uri for uri in blobs if uri.startswith("edges/"))
|
||||
entity_uri = next(uri for uri in blobs if uri.startswith("entities/"))
|
||||
triangle_glb = blobs[geometry_uri]
|
||||
line_glb = blobs[edge_uri]
|
||||
canonical_zip, deflate_zip, glb_cases, zip_cases = build_binary_cases(
|
||||
triangle_glb,
|
||||
line_glb,
|
||||
{"scene.json": scene_bytes, **blobs},
|
||||
)
|
||||
|
||||
missing_nodes = deepcopy(scene)
|
||||
del missing_nodes["nodes"]
|
||||
missing_nodes_bytes = canonical_json_bytes(missing_nodes)
|
||||
empty_nodes = with_scene_revision({**scene, "nodes": []})
|
||||
empty_nodes_bytes = canonical_json_bytes(empty_nodes)
|
||||
revision_mismatch = deepcopy(scene)
|
||||
revision_mismatch["revision"] = "sha256:" + "0" * 64
|
||||
revision_mismatch_bytes = canonical_json_bytes(revision_mismatch)
|
||||
transform_invalid = deepcopy(scene)
|
||||
transform_invalid["nodes"][0]["transform"]["x_axis"] = [2, 0, 0]
|
||||
transform_invalid = with_scene_revision(transform_invalid)
|
||||
transform_invalid_bytes = canonical_json_bytes(transform_invalid)
|
||||
|
||||
nonunit_entity = deepcopy(entity)
|
||||
next(item for item in nonunit_entity["entities"] if item["kind"] == "edge")[
|
||||
"geometry"
|
||||
]["direction"] = [2, 0, 0]
|
||||
nonunit_entity_bytes = canonical_json_bytes(nonunit_entity)
|
||||
unordered_entity = deepcopy(entity)
|
||||
shared_edge = next(
|
||||
item
|
||||
for item in unordered_entity["entities"]
|
||||
if item["kind"] == "edge" and len(item["parent_entity_ids"]) == 2
|
||||
)
|
||||
shared_edge["parent_entity_ids"].reverse()
|
||||
unordered_entity_bytes = canonical_json_bytes(unordered_entity)
|
||||
|
||||
presentation = {
|
||||
"appearances": [
|
||||
{
|
||||
"alpha_mode": "opaque",
|
||||
"base_color": [0.2, 0.3, 0.4, 1],
|
||||
"double_sided": False,
|
||||
"edge_color": [0.05, 0.05, 0.05, 1],
|
||||
"metallic": 0.1,
|
||||
"name": "fixture",
|
||||
"roughness": 0.7,
|
||||
}
|
||||
],
|
||||
"cameras": [
|
||||
{
|
||||
"far": 10000,
|
||||
"name": "overview",
|
||||
"near": 1,
|
||||
"parent_node_id": None,
|
||||
"projection": "perspective",
|
||||
"transform": {
|
||||
"origin": [2000, -2000, 2000],
|
||||
"x_axis": [1, 0, 0],
|
||||
"y_axis": [0, 1, 0],
|
||||
"z_axis": [0, 0, 1],
|
||||
},
|
||||
"vertical_fov_degrees": 45,
|
||||
}
|
||||
],
|
||||
"node_overrides": [
|
||||
{
|
||||
"appearance_name": "fixture",
|
||||
"node_id": "instance/root",
|
||||
"visible": True,
|
||||
}
|
||||
],
|
||||
"presentation_id": "fixture",
|
||||
"schema_version": "1.0",
|
||||
"source_scene_id": "fixture",
|
||||
}
|
||||
presentation_bytes = canonical_json_bytes(presentation)
|
||||
missing_appearance = deepcopy(presentation)
|
||||
missing_appearance["node_overrides"][0]["appearance_name"] = "missing"
|
||||
missing_appearance_bytes = canonical_json_bytes(missing_appearance)
|
||||
bad_camera = deepcopy(presentation)
|
||||
bad_camera["cameras"][0]["far"] = 1
|
||||
bad_camera_bytes = canonical_json_bytes(bad_camera)
|
||||
|
||||
connector_binding = {
|
||||
"binding_id": "fixture",
|
||||
"connector_id": "fixture_connector",
|
||||
"name": None,
|
||||
"owner_definition_id": "definition/root/part/fixture",
|
||||
"schema_version": "1.0",
|
||||
"selected_occurrence_node_id": "instance/root",
|
||||
"source_model": {
|
||||
"artifact_hash": "sha256:" + "1" * 64,
|
||||
"graph_id": "fixture_graph",
|
||||
"model_schema_version": "2.0",
|
||||
},
|
||||
"source_scene": {"revision": scene["revision"], "scene_id": "fixture"},
|
||||
"target": {
|
||||
"entity_asset_id": entity["geometry_asset_id"],
|
||||
"entity_id": "entity/face/0",
|
||||
"expected_source": {
|
||||
"graph_id": "fixture_graph",
|
||||
"kind": "model_output",
|
||||
"node_id": "fixture_body",
|
||||
"output_slot": 0,
|
||||
},
|
||||
"flip": True,
|
||||
"kind": "topology_entity",
|
||||
},
|
||||
}
|
||||
connector_binding_bytes = canonical_json_bytes(connector_binding)
|
||||
wrong_binding_graph = deepcopy(connector_binding)
|
||||
wrong_binding_graph["target"]["expected_source"]["graph_id"] = "other_graph"
|
||||
wrong_binding_graph_bytes = canonical_json_bytes(wrong_binding_graph)
|
||||
vertex_flip = deepcopy(connector_binding)
|
||||
vertex_flip["target"]["entity_id"] = "entity/vertex/0"
|
||||
vertex_flip_bytes = canonical_json_bytes(vertex_flip)
|
||||
|
||||
normalized_part = {
|
||||
"body_source": {"kind": "manual", "source_id": "fixture_body"},
|
||||
"connectors": [],
|
||||
"kind": "part",
|
||||
"material": None,
|
||||
"metadata": {},
|
||||
"name": None,
|
||||
"part_id": "fixture_part",
|
||||
}
|
||||
normalized_part_bytes = canonical_json_bytes(normalized_part)
|
||||
normalized_assembly = {
|
||||
"assembly_id": "fixture_assembly",
|
||||
"components": [
|
||||
{
|
||||
"component_id": "component_a",
|
||||
"definition_ref": "definition/root/part/fixture",
|
||||
"local_placement": {
|
||||
"origin": [0, 0, 0],
|
||||
"x_axis": [1, 0, 0],
|
||||
"y_axis": [0, 1, 0],
|
||||
"z_axis": [0, 0, 1],
|
||||
},
|
||||
"name": None,
|
||||
}
|
||||
],
|
||||
"connectors": [],
|
||||
"constraints": [],
|
||||
"grounded_component_ids": ["component_a"],
|
||||
"kind": "assembly",
|
||||
"metadata": {},
|
||||
"name": None,
|
||||
}
|
||||
normalized_assembly_bytes = canonical_json_bytes(normalized_assembly)
|
||||
missing_grounded_component = deepcopy(normalized_assembly)
|
||||
missing_grounded_component["grounded_component_ids"] = ["missing"]
|
||||
missing_grounded_component_bytes = canonical_json_bytes(missing_grounded_component)
|
||||
duplicate_components = deepcopy(normalized_assembly)
|
||||
duplicate_components["components"].append(
|
||||
deepcopy(duplicate_components["components"][0])
|
||||
)
|
||||
duplicate_components_bytes = canonical_json_bytes(duplicate_components)
|
||||
|
||||
bad_bounds = deepcopy(scene)
|
||||
bad_bounds["geometry_assets"][0]["scene_local_bounds"]["max"][0] = 999
|
||||
bad_bounds = with_scene_revision(bad_bounds)
|
||||
bad_bounds_bytes = canonical_json_bytes(bad_bounds)
|
||||
missing_blob = dict(blobs)
|
||||
del missing_blob[edge_uri]
|
||||
mutated_blobs = dict(blobs)
|
||||
mutated_blobs[geometry_uri] = bytes([triangle_glb[0] ^ 1]) + triangle_glb[1:]
|
||||
|
||||
resource_cases = build_resource_cases(
|
||||
scene,
|
||||
normalized_part,
|
||||
)
|
||||
manifest_cases = [
|
||||
report_case("valid_scene", scene_bytes, validate_scene_manifest(scene_bytes)),
|
||||
report_case(
|
||||
"missing_nodes",
|
||||
missing_nodes_bytes,
|
||||
validate_scene_manifest(missing_nodes_bytes),
|
||||
),
|
||||
report_case(
|
||||
"empty_nodes",
|
||||
empty_nodes_bytes,
|
||||
validate_scene_manifest(empty_nodes_bytes),
|
||||
),
|
||||
report_case(
|
||||
"revision_mismatch",
|
||||
revision_mismatch_bytes,
|
||||
validate_scene_manifest(revision_mismatch_bytes),
|
||||
),
|
||||
report_case(
|
||||
"transform_invalid",
|
||||
transform_invalid_bytes,
|
||||
validate_scene_manifest(transform_invalid_bytes),
|
||||
),
|
||||
report_case(
|
||||
"noncanonical_json",
|
||||
scene_bytes + b"\n",
|
||||
validate_scene_manifest(scene_bytes + b"\n"),
|
||||
),
|
||||
report_case(
|
||||
"duplicate_json_key",
|
||||
b'{"schema_version":"1.0","schema_version":"1.0"}',
|
||||
validate_scene_manifest(
|
||||
b'{"schema_version":"1.0","schema_version":"1.0"}'
|
||||
),
|
||||
),
|
||||
report_case(
|
||||
"bom",
|
||||
b"\xef\xbb\xbf" + scene_bytes,
|
||||
validate_scene_manifest(b"\xef\xbb\xbf" + scene_bytes),
|
||||
),
|
||||
]
|
||||
entity_cases = [
|
||||
report_case(
|
||||
"valid_entities", entity_bytes, validate_entity_asset(entity_bytes)
|
||||
),
|
||||
report_case(
|
||||
"nonunit_analytic_direction",
|
||||
nonunit_entity_bytes,
|
||||
validate_entity_asset(nonunit_entity_bytes),
|
||||
),
|
||||
report_case(
|
||||
"unordered_adjacency",
|
||||
unordered_entity_bytes,
|
||||
validate_entity_asset(unordered_entity_bytes),
|
||||
),
|
||||
]
|
||||
entity_cases.extend(build_entity_matrix_cases(entity))
|
||||
presentation_cases = [
|
||||
report_case(
|
||||
"valid_presentation",
|
||||
presentation_bytes,
|
||||
validate_presentation(presentation_bytes),
|
||||
),
|
||||
report_case(
|
||||
"missing_presentation_appearance",
|
||||
missing_appearance_bytes,
|
||||
validate_presentation(missing_appearance_bytes),
|
||||
),
|
||||
report_case(
|
||||
"invalid_presentation_camera",
|
||||
bad_camera_bytes,
|
||||
validate_presentation(bad_camera_bytes),
|
||||
),
|
||||
]
|
||||
presentation_cases.extend(build_presentation_matrix_cases(presentation))
|
||||
connector_binding_cases = [
|
||||
report_case(
|
||||
"valid_connector_binding",
|
||||
connector_binding_bytes,
|
||||
validate_connector_binding(connector_binding_bytes),
|
||||
),
|
||||
report_case(
|
||||
"connector_binding_graph_mismatch",
|
||||
wrong_binding_graph_bytes,
|
||||
validate_connector_binding(wrong_binding_graph_bytes),
|
||||
),
|
||||
report_case(
|
||||
"connector_binding_vertex_flip",
|
||||
vertex_flip_bytes,
|
||||
validate_connector_binding(vertex_flip_bytes),
|
||||
),
|
||||
]
|
||||
connector_binding_cases.extend(
|
||||
build_connector_binding_matrix_cases(connector_binding)
|
||||
)
|
||||
normalized_product_cases = [
|
||||
report_case(
|
||||
"valid_normalized_part",
|
||||
normalized_part_bytes,
|
||||
validate_normalized_product(normalized_part_bytes),
|
||||
),
|
||||
report_case(
|
||||
"valid_normalized_assembly",
|
||||
normalized_assembly_bytes,
|
||||
validate_normalized_product(normalized_assembly_bytes),
|
||||
),
|
||||
report_case(
|
||||
"missing_grounded_component",
|
||||
missing_grounded_component_bytes,
|
||||
validate_normalized_product(missing_grounded_component_bytes),
|
||||
),
|
||||
report_case(
|
||||
"duplicate_components",
|
||||
duplicate_components_bytes,
|
||||
validate_normalized_product(duplicate_components_bytes),
|
||||
),
|
||||
]
|
||||
normalized_product_cases.extend(
|
||||
build_normalized_product_matrix_cases(normalized_part, normalized_assembly)
|
||||
)
|
||||
package_cases = [
|
||||
{
|
||||
"blob_uris": sorted(blobs),
|
||||
"expected": first_issue(validate_scene_package(scene_bytes, blobs)),
|
||||
"manifest_base64": b64(scene_bytes),
|
||||
"name": "valid_package",
|
||||
"valid": True,
|
||||
},
|
||||
{
|
||||
"blob_uris": sorted(blobs),
|
||||
"expected": first_issue(validate_scene_package(bad_bounds_bytes, blobs)),
|
||||
"manifest_base64": b64(bad_bounds_bytes),
|
||||
"name": "glb_bounds_mismatch",
|
||||
"valid": False,
|
||||
},
|
||||
{
|
||||
"blob_uris": sorted(missing_blob),
|
||||
"expected": first_issue(validate_scene_package(scene_bytes, missing_blob)),
|
||||
"manifest_base64": b64(scene_bytes),
|
||||
"name": "missing_blob",
|
||||
"valid": False,
|
||||
},
|
||||
{
|
||||
"blob_mutations": {geometry_uri: b64(mutated_blobs[geometry_uri])},
|
||||
"blob_uris": sorted(mutated_blobs),
|
||||
"expected": first_issue(validate_scene_package(scene_bytes, mutated_blobs)),
|
||||
"manifest_base64": b64(scene_bytes),
|
||||
"name": "blob_hash_mismatch",
|
||||
"valid": False,
|
||||
},
|
||||
]
|
||||
package_cases.extend(
|
||||
build_package_matrix_cases(
|
||||
scene, entity, blobs, scene_packages, blob_pool
|
||||
)
|
||||
)
|
||||
|
||||
jcs_input = {
|
||||
"literals": [None, True, False],
|
||||
"numbers": [333333333.33333329, 1e30, 4.50, 2e-3, 1e-27],
|
||||
"string": "€$\u000f\nA'B\"\\\\\"/",
|
||||
}
|
||||
jcs_bytes = canonical_json_bytes(jcs_input)
|
||||
corpus = {
|
||||
"artifacts": {
|
||||
"canonical_zip": {
|
||||
"base64": b64(canonical_zip),
|
||||
"sha256": content_hash(canonical_zip),
|
||||
},
|
||||
"deflate_zip": {
|
||||
"base64": b64(deflate_zip),
|
||||
"sha256": content_hash(deflate_zip),
|
||||
},
|
||||
"edge_glb": {
|
||||
"base64": b64(line_glb),
|
||||
"sha256": content_hash(line_glb),
|
||||
"uri": edge_uri,
|
||||
},
|
||||
"entities": {
|
||||
"base64": b64(entity_bytes),
|
||||
"sha256": content_hash(entity_bytes),
|
||||
"uri": entity_uri,
|
||||
},
|
||||
"geometry_glb": {
|
||||
"base64": b64(triangle_glb),
|
||||
"sha256": content_hash(triangle_glb),
|
||||
"uri": geometry_uri,
|
||||
},
|
||||
"scene": {
|
||||
"base64": b64(scene_bytes),
|
||||
"revision": scene["revision"],
|
||||
"sha256": content_hash(scene_bytes),
|
||||
},
|
||||
},
|
||||
"blobs": dict(sorted(blob_pool.items())),
|
||||
"entity_cases": entity_cases,
|
||||
"connector_binding_cases": connector_binding_cases,
|
||||
"format_version": "1.0",
|
||||
"glb_cases": glb_cases,
|
||||
"jcs_vectors": [
|
||||
{
|
||||
"canonical_base64": b64(jcs_bytes),
|
||||
"input": jcs_input,
|
||||
"name": "rfc8785_number_and_string_sample",
|
||||
"sha256": content_hash(jcs_bytes),
|
||||
}
|
||||
],
|
||||
"manifest_cases": manifest_cases,
|
||||
"numeric_vectors": build_numeric_vectors(),
|
||||
"normalized_product_cases": normalized_product_cases,
|
||||
"package_cases": package_cases,
|
||||
"presentation_cases": presentation_cases,
|
||||
"resource_cases": resource_cases,
|
||||
"resource_limits": asdict(BASE_LIMITS),
|
||||
"revision_vectors": build_revision_vectors(scene_packages),
|
||||
"scene_field_cases": scene_field_cases,
|
||||
"scene_shape_cases": scene_shape_cases,
|
||||
"schema_field_matrices": build_schema_field_matrices(
|
||||
ROOT / "src/simplecadapi/scene/contracts/schemas"
|
||||
),
|
||||
"zip_cases": zip_cases,
|
||||
}
|
||||
for collection in (
|
||||
manifest_cases,
|
||||
entity_cases,
|
||||
presentation_cases,
|
||||
connector_binding_cases,
|
||||
normalized_product_cases,
|
||||
package_cases,
|
||||
):
|
||||
for case in collection:
|
||||
assert case["valid"] == (case["expected"] is None), case["name"]
|
||||
payload = (
|
||||
json.dumps(corpus, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
||||
).encode("utf-8")
|
||||
relative_output = OUTPUT.relative_to(ROOT)
|
||||
if args.check:
|
||||
if not OUTPUT.exists() or OUTPUT.read_bytes() != payload:
|
||||
print(f"stale generated fixture: {relative_output}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"{relative_output} is current ({len(payload)} bytes)")
|
||||
return 0
|
||||
|
||||
OUTPUT.write_bytes(payload)
|
||||
print(f"wrote {relative_output} ({len(payload)} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user