Files
cadSet/text-to-cad/tests/python/skills/cad/cadpy/test_step_scene.py
T
2026-07-22 19:38:36 +08:00

297 lines
12 KiB
Python

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