import os import unittest from pathlib import Path from unittest import mock import build123d from OCP.Bnd import Bnd_Box from OCP.BRepBndLib import BRepBndLib from OCP.TopAbs import TopAbs_FACE from OCP.TopExp import TopExp_Explorer from OCP.TopoDS import TopoDS from cadpy import step_scene from cadpy.step_scene import ( LoadedStepScene, OccurrenceNode, SelectorOptions, SelectorProfile, adaptive_mesh_resolution_for_scene, extract_selectors_from_scene, load_step_scene, scene_occurrence_shape, ) from cadpy.glb_topology import STEP_TOPOLOGY_SCHEMA_VERSION from cadpy.metadata import DEFAULT_MESH_ANGULAR_TOLERANCE, DEFAULT_MESH_TOLERANCE from tests.python.support.tmp_root import temporary_directory class StepSceneSelectorArtifactTests(unittest.TestCase): def test_load_step_scene_cached_reuses_brep_scene_cache(self) -> None: with temporary_directory(prefix="cad-step-scene-cache-") as temp_dir: temp_root = Path(temp_dir) step_path = temp_root / "box.step" cache_dir = temp_root / "cache" build123d.export_step(build123d.Box(1, 1, 1), step_path) with mock.patch.dict(os.environ, {"TEXT_TO_CAD_STEP_SCENE_CACHE_DIR": str(cache_dir)}): first = step_scene.load_step_scene_cached(step_path) self.assertEqual(1, len(first.prototype_shapes)) with mock.patch.object(step_scene, "load_step_scene", side_effect=AssertionError("cache miss")): cached = step_scene.load_step_scene_cached(step_path) self.assertEqual(first.step_hash, cached.step_hash) self.assertEqual(1, len(cached.roots)) self.assertEqual(1, len(cached.prototype_shapes)) self.assertFalse(scene_occurrence_shape(cached, cached.roots[0]).IsNull()) def test_step_scene_cache_restores_locations_and_face_color_hashes(self) -> None: with temporary_directory(prefix="cad-step-scene-cache-private-") as temp_dir: temp_root = Path(temp_dir) shape = build123d.Box(1, 1, 1).wrapped explorer = TopExp_Explorer(shape, TopAbs_FACE) face_hash = step_scene._shape_hash(TopoDS.Face_s(explorer.Current())) transform = ( 1.0, 0.0, 0.0, 5.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ) step_path = temp_root / "synthetic.step" step_path.write_text("synthetic cache key only", encoding="utf-8") scene = LoadedStepScene( step_path=step_path, roots=[ OccurrenceNode( path=(1,), name="moved_box", source_name="box", transform=transform, local_transform=transform, prototype_key=7, ) ], prototype_shapes={7: shape}, prototype_face_colors={7: {face_hash: (1.0, 0.0, 0.0, 1.0)}}, ) cache_root = temp_root / "cache" step_hash = "a" * 64 step_scene._write_step_scene_cache(scene, step_hash=step_hash, root=cache_root) cached = step_scene._read_step_scene_cache(step_path, step_hash=step_hash, root=cache_root) self.assertIsNotNone(cached) assert cached is not None self.assertEqual(1, sum(len(colors) for colors in cached.prototype_face_colors.values())) located = scene_occurrence_shape(cached, cached.roots[0]) bounds = Bnd_Box() BRepBndLib.Add_s(located, bounds) x_min, _y_min, _z_min, x_max, _y_max, _z_max = bounds.Get() self.assertGreater(x_min, 4.0) self.assertGreater(x_max, 5.0) def test_artifact_topology_uses_glb_face_runs_without_duplicate_face_buffers(self) -> None: with temporary_directory(prefix="cad-topology-v2-") as temp_dir: step_path = Path(temp_dir) / "box.step" build123d.export_step(build123d.Box(1, 1, 1), step_path) scene = load_step_scene(step_path) bundle = extract_selectors_from_scene( scene, cad_ref="fixtures/box", profile=SelectorProfile.ARTIFACT, options=SelectorOptions(linear_deflection=0.1, angular_deflection=0.1), ) self.assertEqual(STEP_TOPOLOGY_SCHEMA_VERSION, bundle.manifest["schemaVersion"]) self.assertTrue(bundle.manifest["capabilities"]["surfaceEdgeRendering"]) self.assertEqual(".box.step.glb", bundle.manifest["faceProxy"]["source"]) self.assertIn("faceRuns", bundle.buffers) self.assertIn("surfaceHalfEdges", bundle.buffers) self.assertTrue(scene.glb_mesh_payloads) self.assertNotIn("facePositions", bundle.buffers) self.assertNotIn("faceIndices", bundle.buffers) self.assertNotIn("faceIds", bundle.buffers) face_columns = bundle.manifest["tables"]["faceColumns"] triangle_count_column = face_columns.index("triangleCount") row_triangle_count = sum(int(row[triangle_count_column]) for row in bundle.manifest["faces"]) run_triangle_count = sum(int(bundle.buffers["faceRuns"][index + 3]) for index in range(0, len(bundle.buffers["faceRuns"]), 5)) self.assertEqual(row_triangle_count, run_triangle_count) def test_shape_rows_include_occurrence_and_prototype_names(self) -> None: transform = ( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ) scene = LoadedStepScene( step_path=Path("labeled.step"), roots=[ OccurrenceNode( path=(1,), name="base:front_left", source_name="base", transform=transform, prototype_key=7, ) ], prototype_shapes={7: build123d.Box(1, 1, 1).wrapped}, prototype_names={7: "base"}, ) bundle = extract_selectors_from_scene( scene, cad_ref="fixtures/labeled", profile=SelectorProfile.REFS, options=SelectorOptions(linear_deflection=0.1, angular_deflection=0.1), ) shape_columns = bundle.manifest["tables"]["shapeColumns"] shape = dict(zip(shape_columns, bundle.manifest["shapes"][0])) self.assertEqual("base:front_left", shape["name"]) self.assertEqual("base", shape["sourceName"]) def test_adaptive_mesh_resolution_prefers_finer_defaults_for_small_simple_parts(self) -> None: with temporary_directory(prefix="cad-adaptive-mesh-") as temp_dir: step_path = Path(temp_dir) / "box.step" build123d.export_step(build123d.Box(10, 8, 4), step_path) scene = load_step_scene(step_path) resolution = adaptive_mesh_resolution_for_scene(scene) self.assertEqual("extra-fine", resolution.profile) self.assertLess(resolution.settings.tolerance, DEFAULT_MESH_TOLERANCE) self.assertLess(resolution.settings.angular_tolerance, DEFAULT_MESH_ANGULAR_TOLERANCE) self.assertEqual(1, resolution.hints["leafOccurrenceCount"]) def test_adaptive_mesh_resolution_does_not_coarsen_simple_repeated_assemblies_by_leaf_count_alone(self) -> None: box_shape = build123d.Box(10, 8, 4).wrapped identity = ( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ) scene = LoadedStepScene( step_path=Path("synthetic-repeated-box.step"), roots=[ OccurrenceNode( path=(index + 1,), name=f"box_{index}", source_name=f"box_{index}", transform=identity, prototype_key=1, ) for index in range(100) ], prototype_shapes={1: box_shape}, ) resolution = adaptive_mesh_resolution_for_scene(scene) self.assertEqual("medium", resolution.profile) self.assertLess(resolution.settings.tolerance, DEFAULT_MESH_TOLERANCE) self.assertEqual(100, resolution.hints["leafOccurrenceCount"]) def test_adaptive_mesh_resolution_keeps_many_low_curvature_occurrences_balanced(self) -> None: with mock.patch.object( step_scene, "_scene_mesh_resolution_hints", return_value={ "bboxDiag": 190.0, "prototypeFaceCount": 420, "prototypeEdgeCount": 860, "prototypeCurvedFaceCount": 30, "prototypeCurvedEdgeCount": 70, "occurrenceFaceCount": 2957, "occurrenceEdgeCount": 6012, "occurrenceCurvedFaceCount": 80, "occurrenceCurvedEdgeCount": 120, "leafOccurrenceCount": 481, "complexityScore": 18083.7, "effectiveComplexityScore": 18083.7, "curvaturePressureScore": 280.0, }, ): resolution = adaptive_mesh_resolution_for_scene( LoadedStepScene(step_path=Path("repeated-low-curvature.step"), roots=[], prototype_shapes={}) ) self.assertEqual("balanced-assembly", resolution.profile) self.assertEqual(0.016, resolution.settings.tolerance) self.assertEqual(0.5, resolution.settings.angular_tolerance) def test_adaptive_mesh_resolution_uses_large_topology_profile_for_extreme_imports(self) -> None: with mock.patch.object( step_scene, "_scene_mesh_resolution_hints", return_value={ "bboxDiag": None, "prototypeFaceCount": 12000, "prototypeEdgeCount": 30000, "prototypeCurvedFaceCount": 4000, "prototypeCurvedEdgeCount": 12000, "occurrenceFaceCount": 23000, "occurrenceEdgeCount": 59000, "occurrenceCurvedFaceCount": 8000, "occurrenceCurvedEdgeCount": 24000, "leafOccurrenceCount": 120, "complexityScore": 60000.0, "effectiveComplexityScore": 60000.0, "curvaturePressureScore": 38000.0, }, ): resolution = adaptive_mesh_resolution_for_scene( LoadedStepScene(step_path=Path("huge.step"), roots=[], prototype_shapes={}) ) self.assertEqual("large-topology", resolution.profile) self.assertGreater(resolution.settings.tolerance, DEFAULT_MESH_TOLERANCE) self.assertGreater(resolution.settings.angular_tolerance, DEFAULT_MESH_ANGULAR_TOLERANCE) def test_adaptive_mesh_resolution_uses_curvature_pressure_before_raw_counts_explode(self) -> None: with mock.patch.object( step_scene, "_scene_mesh_resolution_hints", return_value={ "bboxDiag": 120.0, "prototypeFaceCount": 700, "prototypeEdgeCount": 1600, "prototypeCurvedFaceCount": 550, "prototypeCurvedEdgeCount": 1500, "occurrenceFaceCount": 700, "occurrenceEdgeCount": 1600, "occurrenceCurvedFaceCount": 550, "occurrenceCurvedEdgeCount": 1500, "leafOccurrenceCount": 8, "complexityScore": 2100.0, "effectiveComplexityScore": 2100.0, "curvaturePressureScore": 3600.0, }, ): resolution = adaptive_mesh_resolution_for_scene( LoadedStepScene(step_path=Path("curvy.step"), roots=[], prototype_shapes={}) ) self.assertEqual("medium", resolution.profile) self.assertEqual(0.014, resolution.settings.tolerance) self.assertEqual(0.45, resolution.settings.angular_tolerance) if __name__ == "__main__": unittest.main()