"""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())