4678 lines
241 KiB
Python
4678 lines
241 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "backend" / "engine"))
|
|
|
|
from cdsl_engine.batch_rebuild import _failure_category, _verification_classification, batch_analyze # noqa: E402
|
|
from cdsl_engine.capabilities import CapabilityAnalyzer # noqa: E402
|
|
from cdsl_engine.runtime_types import ( # noqa: E402
|
|
BendSpec, HoleSpec, PlaneSpec, TopologyDelta, TopologyDeltaRelation,
|
|
TopologyRecord, TopologyRegistry,
|
|
)
|
|
from cdsl_engine.sketch_solver import SHAPE_GENERATORS, resolve_all_sketches # noqa: E402
|
|
|
|
|
|
def _workplane() -> dict:
|
|
return {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]}
|
|
|
|
|
|
def _rectangle(minimum: list[float], maximum: list[float]) -> dict:
|
|
return {"type": "polygon", "vertices": [
|
|
[minimum[0], minimum[1]], [maximum[0], minimum[1]],
|
|
[maximum[0], maximum[1]], [minimum[0], maximum[1]],
|
|
]}
|
|
|
|
|
|
def _two_body_boolean_cdsl(operation: str, left: dict, right: dict) -> dict:
|
|
return {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
|
"part_id": f"boolean-{operation}", "meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "left", "workplane": _workplane(), "profile": left},
|
|
{"id": "right", "workplane": _workplane(), "profile": right},
|
|
]},
|
|
"features": [
|
|
{
|
|
"id": "left_body", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"sketch_id": "left", "params": {"distance_mm": 4, "result_mode": "new_body"},
|
|
},
|
|
{
|
|
"id": "right_body", "atomic_id": "extrude_add_blind", "depends_on": ["left_body"],
|
|
"sketch_id": "right", "params": {"distance_mm": 4, "result_mode": "new_body"},
|
|
},
|
|
{
|
|
"id": "boolean", "atomic_id": "boolean_bodies",
|
|
"depends_on": ["left_body", "right_body"],
|
|
"params": {
|
|
"operation": operation, "target_feature_ids": ["left_body"],
|
|
"tool_feature_ids": ["right_body"], "keep_tools": False,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _selector_digest(selectors: list[str]) -> str:
|
|
return hashlib.sha256("\n".join(selectors).encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _corpus_manifest(source: Path) -> dict:
|
|
return json.loads((source / "corpus-manifest.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
class EngineRuntimeFoundationTests(unittest.TestCase):
|
|
def _base_block(self) -> dict:
|
|
return {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "runtime-block",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "base", "workplane": _workplane(),
|
|
"profile": _rectangle([-5, -5], [5, 5]),
|
|
}]},
|
|
"features": [{
|
|
"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"params": {"distance_mm": 10}, "sketch_id": "base",
|
|
}],
|
|
}
|
|
|
|
def test_planar_face_workplane_projects_the_global_origin_to_the_support_plane(self) -> None:
|
|
from build123d import Edge, Face, Plane, Wire
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
plane = Plane((10, 20, 30), (1, 0, 0), (0, 0, 1))
|
|
corners = [
|
|
plane.origin + plane.x_dir * x + plane.y_dir * y
|
|
for x, y in ((-5, -10), (5, -10), (5, 10), (-5, 10))
|
|
]
|
|
face = Face(Wire([
|
|
Edge.make_line(corners[index], corners[(index + 1) % len(corners)])
|
|
for index in range(len(corners))
|
|
]))
|
|
workplane = Build123dGeometryAdapter.planar_face_workplane(face)
|
|
|
|
self.assertAlmostEqual(workplane.origin_mm[0], 0.0, places=7)
|
|
self.assertAlmostEqual(workplane.origin_mm[1], 0.0, places=7)
|
|
self.assertAlmostEqual(workplane.origin_mm[2], 30.0, places=7)
|
|
self.assertEqual(workplane.normal, (0.0, 0.0, 1.0))
|
|
|
|
def test_runtime_module_has_no_build123d_import(self) -> None:
|
|
runtime_source = (ROOT / "backend" / "engine" / "cdsl_engine" / "runtime.py").read_text(encoding="utf-8")
|
|
self.assertNotIn("from build123d", runtime_source)
|
|
self.assertNotIn("import build123d", runtime_source)
|
|
|
|
def test_execution_session_declares_a_kernel_neutral_adapter_protocol(self) -> None:
|
|
from cdsl_engine.runtime import ExecutionSession, GeometryAdapter
|
|
|
|
self.assertIn("adapter", ExecutionSession.__dataclass_fields__)
|
|
self.assertTrue(getattr(GeometryAdapter, "_is_protocol", False))
|
|
self.assertIn("export", GeometryAdapter.__dict__)
|
|
|
|
def test_missing_bend_generator_does_not_block_engine_import(self) -> None:
|
|
if importlib.util.find_spec("cdsl_engine.parametric_bend") is not None:
|
|
self.skipTest("bend generator is installed")
|
|
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
self.assertTrue(callable(rebuild_cdsl))
|
|
spec = BendSpec.from_feature({
|
|
"thickness_mm": 1,
|
|
"width_mm": 10,
|
|
"chain": [{"leg_mm": 10}],
|
|
})
|
|
with self.assertRaisesRegex(RuntimeError, "bend_add requires cdsl_engine.parametric_bend.build_bend_solid"):
|
|
Build123dGeometryAdapter.bend_solid(spec)
|
|
|
|
def test_hole_spec_normalizes_wizard_subtypes_without_occ_dependencies(self) -> None:
|
|
spec = HoleSpec.from_feature("hole_wizard", {
|
|
"diameter_mm": 2, "depth_mm": 6, "end_condition": {"type": "blind", "solidworks_code": 0},
|
|
"positions": [{"mm": [1, 2, 3]}],
|
|
"countersink": {"diameter_mm": 4, "angle_rad": 1.5707963267948966},
|
|
}, wizard=True)
|
|
self.assertEqual(spec.positions_mm, ((1.0, 2.0, 3.0),))
|
|
self.assertEqual(spec.countersink, (4.0, 1.5707963267948966))
|
|
with self.assertRaisesRegex(ValueError, "larger than the main hole"):
|
|
HoleSpec.from_feature("hole_wizard", {
|
|
"diameter_mm": 2, "depth_mm": 6, "end_condition": {"type": "blind", "solidworks_code": 0}, "positions": [{"mm": [0, 0, 0]}],
|
|
"counterbore": {"diameter_mm": 2, "depth_mm": 1},
|
|
}, wizard=True)
|
|
|
|
def test_analytic_contours_create_a_region_with_hole(self) -> None:
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
|
"part_id": "two-point-spline", "meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "sketch", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [
|
|
{"role": "outer", "closed": True, "segments": [
|
|
{"type": "line", "start": [0, 0], "end": [10, 0]},
|
|
{"type": "line", "start": [10, 0], "end": [10, 10]},
|
|
{"type": "line", "start": [10, 10], "end": [0, 10]},
|
|
{"type": "line", "start": [0, 10], "end": [0, 0]},
|
|
]},
|
|
{"role": "inner", "closed": True, "segments": [
|
|
{"type": "circle", "center": [5, 5], "radius_mm": 2},
|
|
]},
|
|
]},
|
|
}]},
|
|
"features": [{
|
|
"id": "two_point_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"sketch_id": "two-point-spline", "params": {"distance_mm": 1},
|
|
}],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
region = sketch["contour_regions_mm"][0]
|
|
self.assertEqual(len(region["outer"]), 4)
|
|
self.assertEqual(len(region["holes"]), 1)
|
|
self.assertEqual(len(region["holes"][0]), 4)
|
|
|
|
def test_analytic_contours_normalize_disjoint_inner_roles(self) -> None:
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"geometry": {"sketches": [{
|
|
"id": "sketch", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [
|
|
{"role": "outer", "closed": True, "segments": [
|
|
{"type": "line", "start": [0, 0], "end": [2, 0]},
|
|
{"type": "line", "start": [2, 0], "end": [2, 2]},
|
|
{"type": "line", "start": [2, 2], "end": [0, 2]},
|
|
{"type": "line", "start": [0, 2], "end": [0, 0]},
|
|
]},
|
|
{"role": "inner", "closed": True, "segments": [
|
|
{"type": "line", "start": [4, 0], "end": [6, 0]},
|
|
{"type": "line", "start": [6, 0], "end": [6, 2]},
|
|
{"type": "line", "start": [6, 2], "end": [4, 2]},
|
|
{"type": "line", "start": [4, 2], "end": [4, 0]},
|
|
]},
|
|
]},
|
|
}]},
|
|
"features": [],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
self.assertEqual(len(sketch["contour_regions_mm"]), 2)
|
|
self.assertTrue(all(not region["holes"] for region in sketch["contour_regions_mm"]))
|
|
|
|
def test_analytic_contours_do_not_turn_shared_boundaries_into_holes(self) -> None:
|
|
"""A touching contour is an independent region, never a hole loop."""
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"geometry": {"sketches": [{
|
|
"id": "sketch", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [
|
|
{"role": "unknown", "closed": True, "segments": [
|
|
{"type": "line", "start": [0, 0], "end": [2, 0]},
|
|
{"type": "line", "start": [2, 0], "end": [1, 1]},
|
|
{"type": "line", "start": [1, 1], "end": [0, 0]},
|
|
]},
|
|
{"role": "unknown", "closed": True, "segments": [
|
|
{"type": "line", "start": [0, 0], "end": [1, 1]},
|
|
{"type": "line", "start": [1, 1], "end": [-1, 1]},
|
|
{"type": "line", "start": [-1, 1], "end": [0, 0]},
|
|
]},
|
|
]},
|
|
}]},
|
|
"features": [],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
self.assertEqual(len(sketch["contour_regions_mm"]), 2)
|
|
self.assertTrue(all(not region["holes"] for region in sketch["contour_regions_mm"]))
|
|
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
faces = Build123dGeometryAdapter().faces_for_sketch(sketch)
|
|
self.assertEqual(len(faces), 2)
|
|
self.assertTrue(all(len(face.faces()) == 1 for face in faces))
|
|
|
|
def test_analytic_circle_contours_preserve_single_circular_wire_edges(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"geometry": {"sketches": [{
|
|
"id": "sketch", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [
|
|
{"role": "outer", "closed": True, "segments": [
|
|
{"type": "circle", "center": [-10, 0], "radius_mm": 2},
|
|
]},
|
|
{"role": "outer", "closed": True, "segments": [
|
|
{"type": "circle", "center": [10, 0], "radius_mm": 2},
|
|
]},
|
|
]},
|
|
}]},
|
|
"features": [],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
faces = Build123dGeometryAdapter().faces_for_sketch(sketch)
|
|
|
|
self.assertEqual(len(faces), 2)
|
|
self.assertTrue(all(len(face.outer_wire().edges()) == 1 for face in faces))
|
|
|
|
def test_multi_source_regions_keep_nested_sources_as_independent_union_regions(self) -> None:
|
|
"""A nested qSketchRegion source is not a hole in its sibling source."""
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
profile = {
|
|
"type": "multi_source_regions",
|
|
"source_sketch_ids": ["outer_source", "inner_source"],
|
|
"profiles": [
|
|
{"type": "circle", "center": [0, 0], "radius_mm": 5},
|
|
{"type": "circle", "center": [0, 0], "radius_mm": 2},
|
|
],
|
|
}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
|
"part_id": "multi-source-regions", "meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{"id": "regions", "workplane": _workplane(), "profile": profile}]},
|
|
"features": [{
|
|
"id": "regions_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"sketch_id": "regions", "params": {"distance_mm": 2}, "execution_status": "supported",
|
|
}],
|
|
}
|
|
validate_semantic_cdsl(cdsl)
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
self.assertEqual(len(sketch["contour_regions_mm"]), 2)
|
|
self.assertTrue(all(not region["holes"] for region in sketch["contour_regions_mm"]))
|
|
self.assertEqual(len(Build123dGeometryAdapter().faces_for_sketch(sketch)), 2)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
rebuilt = rebuild_cdsl(cdsl, Path(directory) / "multi-source-regions.step")
|
|
self.assertEqual(rebuilt["solid_count"], 1)
|
|
self.assertAlmostEqual(rebuilt["volume_mm3"], math.pi * 5 ** 2 * 2, places=5)
|
|
|
|
malformed = json.loads(json.dumps(cdsl))
|
|
malformed["geometry"]["sketches"][0]["profile"]["source_sketch_ids"].append("third_source")
|
|
with self.assertRaisesRegex(ValueError, "one unique source id per profile"):
|
|
validate_semantic_cdsl(malformed)
|
|
|
|
def test_planar_imprint_selects_an_exact_bounded_split_region(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
def line(identifier: str, start: list[float], end: list[float]) -> dict:
|
|
return {"id": identifier, "curve": {"type": "line", "start": start, "end": end}}
|
|
|
|
profile = {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
line("bottom", [-5, -5], [5, -5]), line("right", [5, -5], [5, 5]),
|
|
line("top", [5, 5], [-5, 5]), line("left", [-5, 5], [-5, -5]),
|
|
line("cut", [-5, 0], [5, 0]), line("divider", [0, -5], [0, 5]),
|
|
],
|
|
"selections": [{
|
|
"source_entity_id": "cut", "face_side": 1,
|
|
"fragment": {"anchor_entity_id": "divider", "side": -1, "intersection_index": 0},
|
|
}],
|
|
}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
|
"part_id": "planar-imprint", "meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{"id": "imprint", "workplane": _workplane(), "profile": profile}]},
|
|
"features": [{
|
|
"id": "imprint_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"sketch_id": "imprint", "params": {"distance_mm": 2}, "execution_status": "supported",
|
|
}],
|
|
}
|
|
validate_semantic_cdsl(cdsl)
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
faces = Build123dGeometryAdapter().faces_for_sketch(sketch)
|
|
self.assertEqual(len(faces), 1)
|
|
self.assertAlmostEqual(faces[0].area, 25.0, places=6)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "imprint.step")
|
|
self.assertAlmostEqual(float(result["volume_mm3"]), 50.0, places=6)
|
|
|
|
def test_planar_imprint_bare_source_collects_its_bounded_split_faces(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
profile = {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "bottom", "curve": {"type": "line", "start": [-5, -5], "end": [5, -5]}},
|
|
{"id": "right", "curve": {"type": "line", "start": [5, -5], "end": [5, 5]}},
|
|
{"id": "top", "curve": {"type": "line", "start": [5, 5], "end": [-5, 5]}},
|
|
{"id": "left", "curve": {"type": "line", "start": [-5, 5], "end": [-5, -5]}},
|
|
{"id": "divider", "curve": {"type": "line", "start": [0, -5], "end": [0, 5]}},
|
|
],
|
|
"selections": [{"source_entity_id": "top", "face_side": 1}],
|
|
}
|
|
sketch = resolve_all_sketches({"geometry": {"sketches": [{
|
|
"id": "imprint", "workplane": _workplane(), "profile": profile,
|
|
}]}})["geometry"]["sketches"][0]
|
|
faces = Build123dGeometryAdapter().faces_for_sketch(sketch)
|
|
self.assertEqual(len(faces), 2)
|
|
self.assertAlmostEqual(sum(face.area for face in faces), 100.0, places=6)
|
|
|
|
def test_planar_imprint_multiregion_extrude_registers_final_fuse_history(self) -> None:
|
|
from cdsl_engine.runtime import prepare_cdsl_execution
|
|
|
|
profile = {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "bottom", "curve": {"type": "line", "start": [-5, -5], "end": [5, -5]}},
|
|
{"id": "right", "curve": {"type": "line", "start": [5, -5], "end": [5, 5]}},
|
|
{"id": "top", "curve": {"type": "line", "start": [5, 5], "end": [-5, 5]}},
|
|
{"id": "left", "curve": {"type": "line", "start": [-5, 5], "end": [-5, -5]}},
|
|
{"id": "divider", "curve": {"type": "line", "start": [0, -5], "end": [0, 5]}},
|
|
],
|
|
"selections": [{"source_entity_id": "top", "face_side": 1}],
|
|
}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
|
"part_id": "planar-imprint-fuse-history", "meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "imprint", "source_sketch_id": "F0", "workplane": _workplane(), "profile": profile,
|
|
}]},
|
|
"features": [{
|
|
"id": "f1", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "imprint",
|
|
"params": {"distance_mm": 2, "result_mode": "new_body"},
|
|
}],
|
|
}
|
|
|
|
execution = prepare_cdsl_execution(cdsl)
|
|
self.assertTrue(execution.analysis.runtime_eligible)
|
|
execution.execute_all()
|
|
self.assertTrue(execution.session.body.is_valid)
|
|
self.assertAlmostEqual(float(execution.session.body.volume), 200.0, places=6)
|
|
self.assertEqual(
|
|
[(item["history_status"], item.get("history_reason")) for item in execution.session.topology.topology_deltas()],
|
|
[("proven", "exact_prism_fuse_history")],
|
|
)
|
|
|
|
def test_planar_imprint_preserves_a_logical_circle_source_through_a_split(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
profile = {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "circle", "curve": {"type": "circle", "center": [0, 0], "radius_mm": 10}},
|
|
{"id": "divider", "curve": {"type": "line", "start": [-10, 0], "end": [10, 0]}},
|
|
],
|
|
"selections": [{
|
|
"source_entity_id": "circle", "face_side": 1,
|
|
"fragment": {"anchor_entity_id": "divider", "side": -1, "intersection_index": 0},
|
|
}],
|
|
}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
|
"part_id": "circle-imprint", "meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{"id": "imprint", "workplane": _workplane(), "profile": profile}]},
|
|
"features": [{
|
|
"id": "imprint_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"sketch_id": "imprint", "params": {"distance_mm": 2}, "execution_status": "supported",
|
|
}],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
circle = next(item for item in sketch["imprint_entities_mm"] if item["id"] == "circle")
|
|
self.assertEqual([edge["type"] for edge in circle["edges"]], ["circle"])
|
|
faces = Build123dGeometryAdapter().faces_for_sketch(sketch)
|
|
self.assertEqual(len(faces), 1)
|
|
self.assertAlmostEqual(faces[0].area, math.pi * 50.0, places=6)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "circle-imprint.step")
|
|
self.assertAlmostEqual(float(result["volume_mm3"]), math.pi * 100.0, places=6)
|
|
|
|
def test_planar_imprint_rejects_a_near_miss_fragment_intersection(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
sketch = {
|
|
"id": "near-miss", "workplane": _workplane(),
|
|
"profile": {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "circle", "curve": {"type": "circle", "center": [0, 0], "radius_mm": 10}},
|
|
{"id": "nearby", "curve": {"type": "line", "start": [10.001, -5], "end": [10.001, 5]}},
|
|
],
|
|
"selections": [{
|
|
"source_entity_id": "circle", "face_side": 1,
|
|
"fragment": {"anchor_entity_id": "nearby", "side": -1, "intersection_index": 0},
|
|
}],
|
|
},
|
|
}
|
|
resolved = resolve_all_sketches({"geometry": {"sketches": [sketch]}})["geometry"]["sketches"][0]
|
|
with self.assertRaisesRegex(ValueError, "source and anchor do not intersect"):
|
|
Build123dGeometryAdapter().faces_for_sketch(resolved)
|
|
|
|
def test_planar_imprint_rejects_an_unbounded_arrangement_region(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
sketch = {
|
|
"id": "unbounded", "workplane": _workplane(),
|
|
"profile": {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "cut", "curve": {"type": "line", "start": [-5, 0], "end": [5, 0]}},
|
|
{"id": "divider", "curve": {"type": "line", "start": [0, -5], "end": [0, 5]}},
|
|
],
|
|
"selections": [{
|
|
"source_entity_id": "cut", "face_side": 1,
|
|
"fragment": {"anchor_entity_id": "divider", "side": -1, "intersection_index": 0},
|
|
}],
|
|
},
|
|
}
|
|
resolved = resolve_all_sketches({"geometry": {"sketches": [sketch]}})["geometry"]["sketches"][0]
|
|
with self.assertRaisesRegex(ValueError, "selected region is unbounded"):
|
|
Build123dGeometryAdapter().faces_for_sketch(resolved)
|
|
|
|
def test_planar_imprint_uses_an_exact_runtime_support_face(self) -> None:
|
|
"""An attached support is topology, not the artificial IMPRINT box."""
|
|
from build123d import Face, Plane
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
sketch = {
|
|
"id": "attached-imprint", "workplane": _workplane(),
|
|
"profile": {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "cut", "curve": {"type": "line", "start": [-5, 0], "end": [5, 0]}},
|
|
{"id": "divider", "curve": {"type": "line", "start": [0, -5], "end": [0, 5]}},
|
|
],
|
|
"selections": [{
|
|
"source_entity_id": "cut", "face_side": 1,
|
|
"fragment": {"anchor_entity_id": "divider", "side": -1, "intersection_index": 0},
|
|
}],
|
|
},
|
|
}
|
|
resolved = resolve_all_sketches({"geometry": {"sketches": [sketch]}})["geometry"]["sketches"][0]
|
|
support = Face.make_rect(10, 10, Plane.XY)
|
|
faces = Build123dGeometryAdapter().faces_for_sketch(resolved, support_face=support)
|
|
self.assertEqual(len(faces), 1)
|
|
self.assertAlmostEqual(faces[0].area, 25.0, places=6)
|
|
|
|
def test_planar_imprint_can_use_a_proven_external_boundary_edge_as_fragment_anchor(self) -> None:
|
|
"""An attached CAP edge can be a splitter witness without becoming a source curve."""
|
|
from build123d import Face, Plane
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
sketch = {
|
|
"id": "attached-external-anchor", "source_sketch_id": "F-attached", "workplane": _workplane(),
|
|
"profile": {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "cut", "curve": {"type": "line", "start": [0, -5], "end": [0, 5]}},
|
|
{"id": "top", "curve": {"type": "line", "start": [-5, 5], "end": [5, 5]}},
|
|
],
|
|
"selections": [{
|
|
"source_entity_id": "cut", "face_side": 1,
|
|
"fragment": {"external_anchor_id": "cap-boundary", "side": -1},
|
|
}],
|
|
},
|
|
}
|
|
resolved = resolve_all_sketches({"geometry": {"sketches": [sketch]}})["geometry"]["sketches"][0]
|
|
support = Face.make_rect(10, 10, Plane.XY)
|
|
cap_boundary = next(
|
|
edge for edge in support.edges()
|
|
if abs(edge.bounding_box().min.Y + 5.0) < 1e-9
|
|
and abs(edge.bounding_box().max.Y + 5.0) < 1e-9
|
|
)
|
|
faces, anchors = Build123dGeometryAdapter().faces_for_sketch_with_source_anchors(
|
|
resolved,
|
|
support_face=support,
|
|
external_anchor_edges={"cap-boundary": cap_boundary},
|
|
)
|
|
self.assertEqual(len(faces), 1)
|
|
self.assertAlmostEqual(faces[0].area, 50.0, places=6)
|
|
self.assertTrue(anchors)
|
|
self.assertTrue(all(item.get("source_entity", (None,))[0] == "F-attached" for item in anchors if item["kind"] == "edge"))
|
|
self.assertFalse(any(item.get("source_entity", (None, None))[1] == "cap-boundary" for item in anchors))
|
|
|
|
def test_planar_imprint_rejects_missing_or_ambiguous_external_fragment_anchor(self) -> None:
|
|
"""The adapter never substitutes a same-sketch curve for an external witness."""
|
|
from build123d import Face, Plane
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
support = Face.make_rect(10, 10, Plane.XY)
|
|
base = {
|
|
"id": "external-anchor-reject", "workplane": _workplane(),
|
|
"profile": {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "cut", "curve": {"type": "line", "start": [0, -5], "end": [0, 5]}},
|
|
{"id": "other", "curve": {"type": "line", "start": [-5, 0], "end": [0, 0]}},
|
|
],
|
|
"selections": [{
|
|
"source_entity_id": "cut", "face_side": 1,
|
|
"fragment": {"external_anchor_id": "missing", "side": -1},
|
|
}],
|
|
},
|
|
}
|
|
resolved = resolve_all_sketches({"geometry": {"sketches": [base]}})["geometry"]["sketches"][0]
|
|
with self.assertRaisesRegex(ValueError, "fragment anchor is unavailable"):
|
|
Build123dGeometryAdapter().faces_for_sketch(resolved, support_face=support)
|
|
|
|
ambiguous = json.loads(json.dumps(base))
|
|
ambiguous["profile"]["selections"][0]["fragment"]["anchor_entity_id"] = "other"
|
|
resolved_ambiguous = resolve_all_sketches({"geometry": {"sketches": [ambiguous]}})["geometry"]["sketches"][0]
|
|
with self.assertRaisesRegex(ValueError, "exactly one anchor"):
|
|
Build123dGeometryAdapter().faces_for_sketch(
|
|
resolved_ambiguous,
|
|
support_face=support,
|
|
external_anchor_edges={"missing": support.edges()[0]},
|
|
)
|
|
|
|
def test_runtime_attachment_face_stays_session_local(self) -> None:
|
|
"""Attachment topology may support a splitter but cannot leak into CDSL."""
|
|
from build123d import Face, Plane
|
|
from cdsl_engine.session import ExecutionSession
|
|
from cdsl_engine.topology import SelectorResolution
|
|
|
|
support = Face.make_rect(10, 10, Plane.XY)
|
|
selector = {"kind": "face", "owner_feature_id": "base"}
|
|
session = ExecutionSession(
|
|
sketches={"attached": {
|
|
"id": "attached", "workplane": _workplane(),
|
|
"attachment": selector,
|
|
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
|
}},
|
|
nodes={},
|
|
)
|
|
record = TopologyRecord("face:base", "face", "base", "body:base", value=support)
|
|
session.resolve = lambda _selector: SelectorResolution( # type: ignore[method-assign]
|
|
selector=selector, status="resolved", record=record,
|
|
)
|
|
session.resolve_sketch_attachment("attached", feature_id="consumer")
|
|
self.assertTrue(session.sketch_attachment_faces["attached"].wrapped.IsSame(support.wrapped))
|
|
self.assertNotIn("support_face", session.sketches["attached"])
|
|
self.assertNotIn("runtime_record", session.sketches["attached"])
|
|
|
|
def test_runtime_attachment_resolves_external_imprint_anchor_only_on_support_boundary(self) -> None:
|
|
"""An external fragment anchor must be a proven edge of its support face."""
|
|
from build123d import Face, Plane
|
|
from cdsl_engine.session import ExecutionSession
|
|
from cdsl_engine.topology import SelectorResolution
|
|
|
|
support = Face.make_rect(10, 10, Plane.XY)
|
|
cap_edge = next(
|
|
edge for edge in support.edges()
|
|
if abs(edge.bounding_box().min.Y + 5.0) < 1e-9
|
|
and abs(edge.bounding_box().max.Y + 5.0) < 1e-9
|
|
)
|
|
sketch = {
|
|
"id": "attached-imprint", "workplane": _workplane(),
|
|
"attachment": {"kind": "face", "tag": "support"},
|
|
"profile": {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "cut", "curve": {"type": "line", "start": [0, -5], "end": [0, 5]}},
|
|
{"id": "top", "curve": {"type": "line", "start": [-5, 5], "end": [5, 5]}},
|
|
],
|
|
"external_anchors": [{"id": "cap", "selector": {"kind": "edge", "tag": "cap"}}],
|
|
"selections": [{
|
|
"source_entity_id": "cut", "face_side": 1,
|
|
"fragment": {"external_anchor_id": "cap", "side": -1},
|
|
}],
|
|
},
|
|
}
|
|
session = ExecutionSession(sketches={"attached-imprint": sketch}, nodes={})
|
|
support_record = TopologyRecord("face:base", "face", "base", "body:base", value=support)
|
|
edge_record = TopologyRecord("edge:base", "edge", "base", "body:base", value=cap_edge)
|
|
session.resolve = lambda selector: SelectorResolution( # type: ignore[method-assign]
|
|
selector=selector, status="resolved",
|
|
record=support_record if selector.get("tag") == "support" else edge_record,
|
|
)
|
|
session.resolve_sketch_attachment("attached-imprint", feature_id="consumer")
|
|
self.assertTrue(session.sketch_attachment_faces["attached-imprint"].wrapped.IsSame(support.wrapped))
|
|
self.assertTrue(session.sketch_imprint_external_edges["attached-imprint"]["cap"].wrapped.IsSame(cap_edge.wrapped))
|
|
self.assertNotIn("runtime_record", session.sketches["attached-imprint"])
|
|
|
|
outside = Face.make_rect(10, 10, Plane((0, 0, 2), (1, 0, 0), (0, 0, 1))).edges()[0]
|
|
bad = ExecutionSession(sketches={"attached-imprint": sketch}, nodes={})
|
|
bad_edge_record = TopologyRecord("edge:outside", "edge", "base", "body:base", value=outside)
|
|
bad.resolve = lambda selector: SelectorResolution( # type: ignore[method-assign]
|
|
selector=selector, status="resolved",
|
|
record=support_record if selector.get("tag") == "support" else bad_edge_record,
|
|
)
|
|
with self.assertRaisesRegex(Exception, "not an exact boundary") as rejected:
|
|
bad.resolve_sketch_attachment("attached-imprint", feature_id="consumer")
|
|
self.assertEqual(rejected.exception.code, "imprint_external_anchor_not_support_boundary")
|
|
|
|
def test_planar_imprint_external_anchor_requires_an_attached_support(self) -> None:
|
|
"""A profile cannot introduce external topology without an attachment selector."""
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"][0]["profile"] = {
|
|
"type": "planar_imprint",
|
|
"source_entities": [
|
|
{"id": "cut", "curve": {"type": "line", "start": [-5, 0], "end": [5, 0]}},
|
|
{"id": "other", "curve": {"type": "line", "start": [0, -5], "end": [0, 5]}},
|
|
],
|
|
"external_anchors": [{"id": "cap", "selector": {
|
|
"kind": "edge", "stable_id": "never-bind", "source": "runtime_snapshot", "confidence": 1.0,
|
|
}}],
|
|
"selections": [{
|
|
"source_entity_id": "cut", "face_side": 1,
|
|
"fragment": {"external_anchor_id": "cap", "side": -1},
|
|
}],
|
|
}
|
|
with self.assertRaisesRegex(ValueError, "external anchors require a runtime face attachment"):
|
|
validate_semantic_cdsl(cdsl)
|
|
|
|
def test_analytic_ellipse_preserves_its_workplane_orientation_and_volume(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"][0] = {
|
|
"id": "ellipse",
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True, "segments": [{
|
|
"type": "ellipse", "center": [0, 0], "major_radius_mm": 5,
|
|
"minor_radius_mm": 2, "major_axis": [3 / 5, 4 / 5],
|
|
}],
|
|
}]},
|
|
}
|
|
cdsl["features"][0]["sketch_id"] = "ellipse"
|
|
cdsl["features"][0]["params"] = {"distance_mm": 4}
|
|
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
edge = sketch["contour_regions_mm"][0]["outer"][0]
|
|
self.assertEqual(edge["major_axis_mm"], [-0.8, 0.6, 0.0])
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "ellipse.step")
|
|
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], math.pi * 5 * 2 * 4, places=5)
|
|
|
|
def test_bspline_profile_preserves_endpoint_tangents(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"geometry": {"sketches": [{
|
|
"id": "spline", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True, "segments": [
|
|
{"type": "line", "start": [0, 0], "end": [4, 0]},
|
|
{"type": "bspline", "start": [4, 0], "end": [0, 0],
|
|
"points": [[4, 0], [4, 3], [0, 0]],
|
|
"start_tangent": [0, 5], "end_tangent": [-5, 0]},
|
|
],
|
|
}]},
|
|
}]},
|
|
"features": [],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
spline = next(edge for edge in sketch["contour_regions_mm"][0]["outer"] if edge["type"] == "bspline")
|
|
self.assertEqual(spline["start_tangent_mm"], [0.0, 5.0, 0.0])
|
|
self.assertEqual(spline["end_tangent_mm"], [-5.0, 0.0, 0.0])
|
|
edge = Build123dGeometryAdapter()._wire([spline]).edges()[0]
|
|
self.assertAlmostEqual(edge.tangent_at(0).Y, 1.0, places=6)
|
|
self.assertAlmostEqual(edge.tangent_at(1).X, -1.0, places=6)
|
|
|
|
def test_bspline_interpolation_tangent_uses_the_runtime_parameter_domain(self) -> None:
|
|
from cdsl_engine.build123d_adapter import interpolated_bspline_point_and_tangent
|
|
|
|
points = [(0.0, -23.11, 0.0), (0.0, -5.33, 0.0), (26.7, 9.39, 0.0)]
|
|
parameters = [0.0, 0.43299474427180723, 1.0]
|
|
point, tangent = interpolated_bspline_point_and_tangent(
|
|
points,
|
|
start_tangent=(0.0, 64.52, 0.0),
|
|
end_tangent=(141.23, -13.99, 0.0),
|
|
parameters=parameters,
|
|
interpolation_index=1,
|
|
)
|
|
for actual, expected in zip(point, points[1]):
|
|
self.assertAlmostEqual(actual, expected, places=12)
|
|
self.assertAlmostEqual(tangent[0], 0.008342033594358346, places=10)
|
|
self.assertAlmostEqual(tangent[1], 36.52292778091852, places=10)
|
|
|
|
def test_two_point_bspline_profile_requires_and_preserves_endpoint_tangents(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"geometry": {"sketches": [{
|
|
"id": "two-point-spline", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True, "segments": [
|
|
{"type": "line", "start": [0, 0], "end": [4, 0]},
|
|
{"type": "bspline", "start": [4, 0], "end": [0, 0],
|
|
"points": [[4, 0], [0, 0]], "parameters": [0, 1],
|
|
"start_tangent": [0, 5], "end_tangent": [-5, 0]},
|
|
],
|
|
}]},
|
|
}]},
|
|
"features": [],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
spline = next(edge for edge in sketch["contour_regions_mm"][0]["outer"] if edge["type"] == "bspline")
|
|
edge = Build123dGeometryAdapter()._wire([spline]).edges()[0]
|
|
self.assertAlmostEqual(edge.tangent_at(0).Y, 1.0, places=6)
|
|
self.assertAlmostEqual(edge.tangent_at(1).X, -1.0, places=6)
|
|
|
|
with self.assertRaisesRegex(ValueError, "parameters must be finite and strictly increasing"):
|
|
Build123dGeometryAdapter()._wire([{**spline, "parameters": [0, 0]}])
|
|
|
|
coincident = deepcopy(cdsl)
|
|
coincident_spline = coincident["geometry"]["sketches"][0]["profile"]["contours"][0]["segments"][1]
|
|
coincident_spline["end"] = [4, 0]
|
|
coincident_spline["points"][-1] = [4, 0]
|
|
with self.assertRaisesRegex(ValueError, "two-point bspline endpoints must be distinct"):
|
|
resolve_all_sketches(coincident)
|
|
|
|
incomplete = {**cdsl, "geometry": {"sketches": [{
|
|
**cdsl["geometry"]["sketches"][0],
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True, "segments": [
|
|
{"type": "line", "start": [0, 0], "end": [4, 0]},
|
|
{"type": "bspline", "start": [4, 0], "end": [0, 0], "points": [[4, 0], [0, 0]]},
|
|
],
|
|
}]},
|
|
}]}}
|
|
with self.assertRaisesRegex(ValueError, "two-point bspline requires both endpoint tangents"):
|
|
resolve_all_sketches(incomplete)
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
with self.assertRaisesRegex(ValueError, "CDSL schema violation"):
|
|
validate_semantic_cdsl(incomplete)
|
|
|
|
def test_drafted_ellipse_exports_as_a_solid(self) -> None:
|
|
from build123d import import_step
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "ellipse", "workplane": {"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True, "segments": [{
|
|
"type": "ellipse", "center": [0, 0], "major_radius_mm": 3,
|
|
"minor_radius_mm": 2, "major_axis": [1, 0],
|
|
}],
|
|
}]},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "drafted_ellipse", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 4, "draft": {"angle_deg": 1, "pull_direction": False}}, "sketch_id": "ellipse",
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
output = Path(directory) / "drafted-ellipse.step"
|
|
result = rebuild_cdsl(cdsl, output)
|
|
exported = import_step(str(output))
|
|
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertEqual(len(exported.solids()), 1)
|
|
self.assertGreater(float(exported.volume), 1000.0)
|
|
|
|
def test_boolean_bodies_subtracts_explicit_new_body_sources(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "boolean-bodies",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "outer", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 10}},
|
|
{"id": "inner", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 4}},
|
|
]},
|
|
"features": [
|
|
{"id": "outer_body", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "outer", "params": {"distance_mm": 10, "result_mode": "new_body"}},
|
|
{"id": "inner_body", "atomic_id": "extrude_add_blind", "depends_on": ["outer_body"], "sketch_id": "inner", "params": {"distance_mm": 10, "result_mode": "new_body"}},
|
|
{"id": "cut", "atomic_id": "boolean_bodies", "depends_on": ["outer_body", "inner_body"], "params": {"operation": "subtract", "target_feature_ids": ["outer_body"], "tool_feature_ids": ["inner_body"], "keep_tools": False}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
rebuilt = rebuild_cdsl(cdsl, Path(tmp) / "boolean.step")
|
|
|
|
self.assertEqual(rebuilt["solid_count"], 1)
|
|
self.assertAlmostEqual(rebuilt["volume_mm3"], math.pi * (10 ** 2 - 4 ** 2) * 10)
|
|
self.assertEqual([item["feature_id"] for item in rebuilt["feature_results"]], ["outer_body", "inner_body", "cut"])
|
|
modified_cap = next(
|
|
item for item in rebuilt["topology_records"]
|
|
if item["feature_id"] == "cut" and item["kind"] == "face"
|
|
and item["geometry"].get("surface_type") == "plane"
|
|
and abs(item["geometry"].get("area_mm2", 0) - math.pi * (10 ** 2 - 4 ** 2)) < 1e-5
|
|
)
|
|
self.assertEqual(modified_cap["owner_feature_ids"], ["outer_body"])
|
|
delta = next(item for item in rebuilt["topology_deltas"] if item["operation"] == "subtract")
|
|
self.assertTrue(any(
|
|
item["event"] == "modified" and item["status"] == "unique_exact_continuation"
|
|
for item in delta["relations"]
|
|
))
|
|
|
|
def test_boolean_union_and_intersect_capture_exact_kernel_history(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cases = (
|
|
(
|
|
"union", _rectangle([-4, -2], [0, 2]), _rectangle([4, -2], [8, 2]),
|
|
128.0, 2,
|
|
),
|
|
(
|
|
"intersect", _rectangle([-4, -2], [2, 2]), _rectangle([-1, -2], [5, 2]),
|
|
48.0, 1,
|
|
),
|
|
)
|
|
for operation, left, right, volume, solid_count in cases:
|
|
with self.subTest(operation=operation):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
rebuilt = rebuild_cdsl(
|
|
_two_body_boolean_cdsl(operation, left, right), Path(directory) / f"{operation}.step",
|
|
)
|
|
|
|
self.assertAlmostEqual(rebuilt["volume_mm3"], volume)
|
|
self.assertEqual(rebuilt["solid_count"], solid_count)
|
|
delta = next(item for item in rebuilt["topology_deltas"] if item["operation"] == operation)
|
|
self.assertTrue(any(
|
|
item["status"] == "unique_exact_continuation"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item["owner_feature_ids"] == ["left_body"]
|
|
for item in rebuilt["topology_records"]
|
|
if item["feature_id"] == "boolean" and item["kind"] in {"face", "edge", "vertex"}
|
|
))
|
|
|
|
def test_targetless_body_set_keep_tools_retains_the_complete_input_set(self) -> None:
|
|
"""The lowered left operand is still an original tool for keepTools."""
|
|
from cdsl_engine.runtime import prepare_cdsl_execution
|
|
|
|
cdsl = _two_body_boolean_cdsl(
|
|
"intersect", _rectangle([-4, -2], [2, 2]), _rectangle([-1, -2], [5, 2]),
|
|
)
|
|
boolean = cdsl["features"][-1]
|
|
boolean["params"]["keep_tools"] = True
|
|
boolean["params"]["targetless_body_set"] = True
|
|
execution = prepare_cdsl_execution(cdsl)
|
|
self.assertTrue(all(item.executable for item in execution.analysis.feature_results))
|
|
execution.execute_all()
|
|
self.assertEqual(set(execution.session.body_members), {"left_body", "right_body", "boolean"})
|
|
self.assertAlmostEqual(float(execution.session.body_members["left_body"].volume), 96.0)
|
|
self.assertAlmostEqual(float(execution.session.body_members["right_body"].volume), 96.0)
|
|
self.assertAlmostEqual(float(execution.session.body_members["boolean"].volume), 48.0)
|
|
|
|
def test_boolean_intersection_owner_selector_executes_downstream_fillet(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = _two_body_boolean_cdsl(
|
|
"intersect", _rectangle([-4, -2], [2, 2]), _rectangle([-1, -2], [5, 2]),
|
|
)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
intersected = rebuild_cdsl(cdsl, root / "intersected.step")
|
|
edge = next(
|
|
item for item in intersected["topology_records"]
|
|
if item["feature_id"] == "boolean" and item["kind"] == "edge"
|
|
and item["owner_feature_ids"] == ["left_body"]
|
|
)
|
|
filleted = deepcopy(cdsl)
|
|
filleted["features"].append({
|
|
"id": "fillet", "atomic_id": "fillet", "depends_on": ["boolean"], "params": {"radius_mm": 0.25},
|
|
"selectors": [{
|
|
"kind": "edge", "stable_id": "intersection-left-edge", "source": "runtime_snapshot",
|
|
"confidence": 1, "owner_feature_id": "left_body", "geometry": edge["geometry"],
|
|
}],
|
|
})
|
|
rebuilt = rebuild_cdsl(filleted, root / "filleted.step")
|
|
|
|
self.assertEqual([item["feature_id"] for item in rebuilt["feature_results"]], [
|
|
"left_body", "right_body", "boolean", "fillet",
|
|
])
|
|
self.assertLess(rebuilt["volume_mm3"], intersected["volume_mm3"])
|
|
|
|
def test_single_body_dressups_capture_kernel_history_only_for_direct_builder_paths(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "base.step")
|
|
edge = next(item for item in baseline["topology_records"] if item["kind"] == "edge")
|
|
for atomic_id, params in (("fillet", {"radius_mm": 1}), ("chamfer", {"distance_mm": 1})):
|
|
with self.subTest(atomic_id=atomic_id):
|
|
dressed = deepcopy(base)
|
|
dressed["features"].append({
|
|
"id": atomic_id, "atomic_id": atomic_id, "depends_on": ["base_add"], "params": params,
|
|
"selectors": [{
|
|
"kind": "edge", "stable_id": "base-edge", "source": "runtime_snapshot", "confidence": 1,
|
|
"owner_feature_id": "base_add", "geometry": edge["geometry"],
|
|
}],
|
|
})
|
|
result = rebuild_cdsl(dressed, root / f"{atomic_id}.step")
|
|
delta = next(item for item in result["topology_deltas"] if item["operation"] == atomic_id)
|
|
self.assertTrue(any(
|
|
item["status"] == "unique_exact_continuation"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item["event"] == "generated" and item["status"] == "recorded_without_owner_transfer"
|
|
for item in delta["relations"]
|
|
))
|
|
# Dress-up builders create the patch as a FACE from the
|
|
# selected EDGE. Keep this cross-kind OCC fact explicit;
|
|
# it is diagnostic lineage only, not a BLEND_EDGE selector.
|
|
self.assertTrue(any(
|
|
item["event"] == "generated"
|
|
and item["source_kind"] == "edge"
|
|
and item["result_kind"] == "face"
|
|
and item["coverage"] == "complete"
|
|
and item["lineage_status"] == "proven"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item.get("blend_transition") is True
|
|
and item["status"] == "exact_blend_boundary"
|
|
and item["source_kind"] == "edge"
|
|
and item["result_kind"] == "edge"
|
|
and len(item["source_record_ids"]) == 1
|
|
and len(item["blend_into_source_record_ids"]) == 1
|
|
and len(item["blend_into_result_record_ids"]) == 1
|
|
and len(item["patch_face_record_ids"]) == 1
|
|
and len(item["result_record_ids"]) == 1
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item["owner_feature_ids"] == ["base_add"]
|
|
for item in result["topology_records"]
|
|
if item["feature_id"] == atomic_id and item["kind"] in {"face", "edge"}
|
|
))
|
|
|
|
angled = deepcopy(base)
|
|
angled["features"].append({
|
|
"id": "angled", "atomic_id": "chamfer", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 1, "distance_2_mm": 0.5},
|
|
"selectors": [{
|
|
"kind": "edge", "stable_id": "base-edge", "source": "runtime_snapshot", "confidence": 1,
|
|
"owner_feature_id": "base_add", "geometry": edge["geometry"],
|
|
}],
|
|
})
|
|
angled_result = rebuild_cdsl(angled, root / "angled.step")
|
|
|
|
self.assertFalse(any(item["operation"] == "chamfer" for item in angled_result["topology_deltas"]))
|
|
|
|
def test_single_member_multibody_dressup_preserves_other_members_and_history(self) -> None:
|
|
"""A dress-up on one Compound member must not discard its OCC delta."""
|
|
from cdsl_engine.runtime import prepare_cdsl_execution
|
|
|
|
base = _two_body_boolean_cdsl(
|
|
"union", _rectangle([-12, -4], [-4, 4]), _rectangle([4, -4], [12, 4]),
|
|
)
|
|
base["features"] = base["features"][:2]
|
|
execution = prepare_cdsl_execution(base)
|
|
execution.execute_all()
|
|
edge = next(
|
|
record for record in execution.session.topology.records()
|
|
if record.feature_id == "right_body" and record.kind == "edge" and record.owners == ("right_body",)
|
|
)
|
|
for atomic_id, params in (("fillet", {"radius_mm": 0.5}), ("chamfer", {"distance_mm": 0.5})):
|
|
with self.subTest(atomic_id=atomic_id):
|
|
dressed = deepcopy(base)
|
|
dressed["features"].append({
|
|
"id": atomic_id, "atomic_id": atomic_id, "depends_on": ["left_body", "right_body"],
|
|
"params": params, "selectors": [{
|
|
"kind": "edge", "stable_id": edge.record_id, "snapshot_id": edge.record_id,
|
|
"source": "runtime_snapshot", "confidence": 1, "owner_feature_id": "right_body",
|
|
"geometry": edge.geometry,
|
|
}],
|
|
})
|
|
execution = prepare_cdsl_execution(dressed)
|
|
execution.execute_all()
|
|
|
|
self.assertEqual(set(execution.session.body_members), {"left_body", atomic_id})
|
|
delta = next(
|
|
item for item in execution.session.topology.topology_deltas()
|
|
if item["feature_id"] == atomic_id
|
|
)
|
|
self.assertTrue(delta["relations"])
|
|
self.assertTrue(any(
|
|
item["event"] == "generated"
|
|
and item["source_kind"] == "edge"
|
|
and item["result_kind"] == "face"
|
|
and item["coverage"] == "complete"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
lineage.operation == "body_member_preserve"
|
|
and lineage.feature_id == atomic_id
|
|
and lineage.source_kind == "edge"
|
|
for lineage in execution.session.topology.lineage()
|
|
))
|
|
|
|
def test_shell_captures_exact_kernel_history_for_downstream_selector(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "base.step")
|
|
removed_face = next(
|
|
item for item in baseline["topology_records"]
|
|
if item["kind"] == "face"
|
|
and item["feature_id"] == "base_add"
|
|
and item["geometry"].get("surface_type") == "plane"
|
|
and item["geometry"].get("normal", [0, 0, 0])[2] > 0.9
|
|
)
|
|
shelled = deepcopy(base)
|
|
shelled["features"].append({
|
|
"id": "shell", "atomic_id": "shell", "depends_on": ["base_add"],
|
|
"params": {"thickness_mm": 1, "inward": True},
|
|
"selectors": [{
|
|
"kind": "face", "stable_id": removed_face["record_id"],
|
|
"snapshot_id": removed_face["record_id"], "source": "runtime_snapshot",
|
|
"confidence": 1, "owner_feature_id": "base_add", "geometry": removed_face["geometry"],
|
|
}],
|
|
})
|
|
shell_result = rebuild_cdsl(shelled, root / "shell.step")
|
|
delta = next(item for item in shell_result["topology_deltas"] if item["operation"] == "shell")
|
|
continuation = next(
|
|
item for item in delta["relations"]
|
|
if item["kind"] == "edge" and item["status"] == "unique_exact_continuation"
|
|
)
|
|
source_id = continuation["source_record_ids"][0]
|
|
result_id = continuation["result_record_ids"][0]
|
|
successor = next(item for item in shell_result["topology_records"] if item["record_id"] == result_id)
|
|
|
|
downstream = deepcopy(shelled)
|
|
downstream["features"].append({
|
|
"id": "fillet", "atomic_id": "fillet", "depends_on": ["shell"],
|
|
"params": {"radius_mm": 0.25},
|
|
"selectors": [{
|
|
"kind": "edge", "stable_id": source_id, "snapshot_id": result_id,
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
"owner_feature_id": "base_add", "geometry": successor["geometry"],
|
|
}],
|
|
})
|
|
downstream_result = rebuild_cdsl(downstream, root / "shell-fillet.step")
|
|
|
|
self.assertTrue(any(
|
|
item["event"] == "generated" and item["status"] == "recorded_without_owner_transfer"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item["event"] == "generated" and item.get("output_role") == "shell.offset_face"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item.get("output_role") == "shell.closing_descendant"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item.get("output_role") == "shell.wall"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item["status"] == "unique_exact_continuation" for item in delta["relations"]
|
|
))
|
|
self.assertEqual(successor["owner_feature_ids"], ["base_add"])
|
|
self.assertEqual([item["feature_id"] for item in downstream_result["feature_results"]], [
|
|
"base_add", "shell", "fillet",
|
|
])
|
|
self.assertEqual(downstream_result["runtime_diagnostics"], [])
|
|
|
|
def test_body_transform_copy_and_explicit_delete_preserve_unselected_members(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "body-transform-delete",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "source", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 2},
|
|
}]},
|
|
"features": [
|
|
{"id": "source_body", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "source", "params": {"distance_mm": 5, "result_mode": "new_body"}},
|
|
{"id": "copied_body", "atomic_id": "transform_bodies", "depends_on": ["source_body"], "params": {
|
|
"source_feature_ids": ["source_body"],
|
|
"transform": {"type": "translation", "translation_mm": [10, 0, 0]},
|
|
"make_copy": True,
|
|
}},
|
|
{"id": "remove_copy", "atomic_id": "delete_bodies", "depends_on": ["copied_body"], "params": {"target_feature_ids": ["copied_body"]}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "body-transform-delete.step")
|
|
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], math.pi * 2 ** 2 * 5)
|
|
self.assertEqual(result["bbox_mm"], {"min": [-2.0, -2.0, 0.0], "max": [2.0, 2.0, 5.0]})
|
|
self.assertEqual([item["feature_id"] for item in result["feature_results"]], ["source_body", "copied_body", "remove_copy"])
|
|
|
|
def test_multi_source_transform_copy_preserves_source_qualified_members(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "multi-source-copy",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "left", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 1}},
|
|
{"id": "right", "workplane": _workplane(), "profile": {"type": "circle", "center": [10, 0], "radius_mm": 1}},
|
|
]},
|
|
"features": [
|
|
{"id": "left_body", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "left", "params": {"distance_mm": 2, "result_mode": "new_body"}},
|
|
{"id": "right_body", "atomic_id": "extrude_add_blind", "depends_on": ["left_body"], "sketch_id": "right", "params": {"distance_mm": 2, "result_mode": "new_body"}},
|
|
{"id": "copy_pair", "atomic_id": "transform_bodies", "depends_on": ["left_body", "right_body"], "params": {
|
|
"source_feature_ids": ["left_body", "right_body"],
|
|
"transform": {"type": "translation", "translation_mm": [0, 10, 0]}, "make_copy": True,
|
|
}},
|
|
{"id": "move_left_copy", "atomic_id": "transform_bodies", "depends_on": ["copy_pair"], "params": {
|
|
"transform_copy_refs": [{"transform_feature_id": "copy_pair", "source_feature_id": "left_body"}],
|
|
"transform": {"type": "translation", "translation_mm": [0, 0, 10]}, "make_copy": False,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "multi-source-copy.step")
|
|
|
|
self.assertEqual(result["solid_count"], 4)
|
|
self.assertAlmostEqual(result["volume_mm3"], 4 * math.pi * 2, places=5)
|
|
self.assertEqual(result["bbox_mm"], {"min": [-1.0, -1.0, 0.0], "max": [11.0, 11.0, 12.0]})
|
|
self.assertEqual([item["feature_id"] for item in result["feature_results"]], [
|
|
"left_body", "right_body", "copy_pair", "move_left_copy",
|
|
])
|
|
|
|
def test_boolean_consumes_only_source_qualified_multi_source_copy_members(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "multi-source-copy-boolean",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "left", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 1}},
|
|
{"id": "right", "workplane": _workplane(), "profile": {"type": "circle", "center": [10, 0], "radius_mm": 1}},
|
|
]},
|
|
"features": [
|
|
{"id": "left_body", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "left", "params": {"distance_mm": 2, "result_mode": "new_body"}},
|
|
{"id": "right_body", "atomic_id": "extrude_add_blind", "depends_on": ["left_body"], "sketch_id": "right", "params": {"distance_mm": 2, "result_mode": "new_body"}},
|
|
{"id": "copy_pair", "atomic_id": "transform_bodies", "depends_on": ["left_body", "right_body"], "params": {
|
|
"source_feature_ids": ["left_body", "right_body"],
|
|
"transform": {"type": "translation", "translation_mm": [0, 10, 0]}, "make_copy": True,
|
|
}},
|
|
{"id": "join_copies", "atomic_id": "boolean_bodies", "depends_on": ["copy_pair"], "params": {
|
|
"operation": "union",
|
|
"target_transform_copy_refs": [{"transform_feature_id": "copy_pair", "source_feature_id": "left_body"}],
|
|
"tool_transform_copy_refs": [{"transform_feature_id": "copy_pair", "source_feature_id": "right_body"}],
|
|
"keep_tools": False,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "multi-source-copy-boolean.step")
|
|
|
|
self.assertEqual(result["solid_count"], 4)
|
|
self.assertAlmostEqual(result["volume_mm3"], 4 * math.pi * 2, places=5)
|
|
self.assertEqual(result["bbox_mm"], {"min": [-1.0, -1.0, 0.0], "max": [11.0, 11.0, 2.0]})
|
|
self.assertEqual([item["feature_id"] for item in result["feature_results"]], [
|
|
"left_body", "right_body", "copy_pair", "join_copies",
|
|
])
|
|
|
|
def test_boolean_transform_copy_reference_rejects_unselected_source(self) -> None:
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["params"]["result_mode"] = "new_body"
|
|
cdsl["features"].extend([
|
|
{"id": "second_body", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"], "sketch_id": "base", "params": {"distance_mm": 5, "result_mode": "new_body"}},
|
|
{"id": "copy_pair", "atomic_id": "transform_bodies", "depends_on": ["base_add", "second_body"], "params": {
|
|
"source_feature_ids": ["base_add", "second_body"],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]}, "make_copy": True,
|
|
}},
|
|
{"id": "join", "atomic_id": "boolean_bodies", "depends_on": ["copy_pair"], "params": {
|
|
"operation": "union",
|
|
"target_transform_copy_refs": [{"transform_feature_id": "copy_pair", "source_feature_id": "missing_body"}],
|
|
"tool_feature_ids": ["base_add"], "keep_tools": False,
|
|
}},
|
|
])
|
|
for feature in cdsl["features"]:
|
|
feature["execution_status"] = "supported"
|
|
|
|
with self.assertRaisesRegex(ValueError, "names a source outside its transform"):
|
|
validate_semantic_cdsl(cdsl)
|
|
|
|
def test_transform_copy_reference_rejects_unselected_source(self) -> None:
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["params"]["result_mode"] = "new_body"
|
|
cdsl["features"].extend([
|
|
{"id": "second_body", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"], "sketch_id": "base", "params": {"distance_mm": 5, "result_mode": "new_body"}},
|
|
{"id": "copy_pair", "atomic_id": "transform_bodies", "depends_on": ["base_add", "second_body"], "params": {
|
|
"source_feature_ids": ["base_add", "second_body"],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]}, "make_copy": True,
|
|
}},
|
|
{"id": "move", "atomic_id": "transform_bodies", "depends_on": ["copy_pair"], "params": {
|
|
"transform_copy_refs": [{"transform_feature_id": "copy_pair", "source_feature_id": "missing_body"}],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]}, "make_copy": False,
|
|
}},
|
|
])
|
|
for feature in cdsl["features"]:
|
|
feature["execution_status"] = "supported"
|
|
|
|
with self.assertRaisesRegex(ValueError, "names a source outside its transform"):
|
|
validate_semantic_cdsl(cdsl)
|
|
|
|
def test_body_uniform_scale_uses_its_explicit_center_and_preserves_topology_provenance(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "body-uniform-scale",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "source", "workplane": _workplane(), "profile": {"type": "circle", "center": [10, 0], "radius_mm": 2},
|
|
}]},
|
|
"features": [
|
|
{"id": "source_body", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "source", "params": {"distance_mm": 5, "result_mode": "new_body"}},
|
|
{"id": "scaled_body", "atomic_id": "transform_bodies", "depends_on": ["source_body"], "params": {
|
|
"source_feature_ids": ["source_body"],
|
|
"transform": {"type": "uniform_scale", "center_mm": [10, 0, 0], "scale_factor": 0.5},
|
|
"make_copy": False,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "body-uniform-scale.step")
|
|
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], math.pi * 2 ** 2 * 5 / 8)
|
|
self.assertEqual(result["bbox_mm"], {"min": [9.0, -1.0, 0.0], "max": [11.0, 1.0, 2.5]})
|
|
self.assertTrue(any(item["operation"] == "uniform_scale" for item in result["topology_deltas"]))
|
|
|
|
def test_kernel_transform_delta_preserves_owner_and_rejects_stale_geometry(self) -> None:
|
|
from build123d import Solid
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
adapter = Build123dGeometryAdapter()
|
|
source = Solid.make_box(2, 3, 4)
|
|
cases = (
|
|
("translation", {"type": "translation", "translation_mm": [7, -3, 2]}),
|
|
("rotation", {
|
|
"type": "rotation",
|
|
"axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
|
"angle_deg": 90,
|
|
}),
|
|
("uniform_scale", {"type": "uniform_scale", "center_mm": [1, 1.5, 2], "scale_factor": 0.5}),
|
|
)
|
|
for name, transform in cases:
|
|
with self.subTest(transform=name):
|
|
moved, delta = adapter.transform_with_topology_delta(source, transform)
|
|
registry = TopologyRegistry()
|
|
source_records = adapter.topology_records(source, "base", "body:base")
|
|
registry.replace_body_topology("base", "body:base", source_records)
|
|
registry.replace_body_topology(
|
|
"move", "body:move", adapter.topology_records(moved, "move", "body:move"),
|
|
topology_delta=delta,
|
|
)
|
|
|
|
moved_face = next(record for record in registry.records_for_feature("move") if record.kind == "face")
|
|
resolved = registry.resolve({
|
|
"kind": "face", "owner_feature_id": "base", "geometry": moved_face.geometry,
|
|
}, active_body_id="body:move")
|
|
stale = registry.resolve({
|
|
"kind": "face", "owner_feature_id": "base", "geometry": source_records[0].geometry,
|
|
}, active_body_id="body:move")
|
|
|
|
self.assertEqual(moved_face.owner_feature_ids, ("base",))
|
|
self.assertEqual(resolved.status, "resolved")
|
|
self.assertEqual(stale.status, "not_found")
|
|
evidence = registry.topology_deltas()[0]
|
|
self.assertEqual(evidence["operation"], name)
|
|
self.assertTrue(any(
|
|
item["status"] == "unique_exact_continuation"
|
|
for item in evidence["relations"]
|
|
))
|
|
|
|
def test_non_unique_or_incomplete_kernel_history_does_not_transfer_owner(self) -> None:
|
|
first_source = object()
|
|
second_source = object()
|
|
result = object()
|
|
registry = TopologyRegistry()
|
|
registry.replace_body_topology("base", "body:base", [
|
|
TopologyRecord("base:first", "face", "base", "body:base", {"center_mm": [0, 0, 0]}, first_source),
|
|
TopologyRecord("base:second", "face", "other", "body:base", {"center_mm": [0, 0, 0]}, second_source),
|
|
])
|
|
registry.replace_body_topology("move", "body:move", [
|
|
TopologyRecord("move:face", "face", "move", "body:move", {"center_mm": [7, 0, 0]}, result),
|
|
], topology_delta=TopologyDelta("translation", (
|
|
TopologyDeltaRelation("modified", "face", first_source, (result,)),
|
|
TopologyDeltaRelation("modified", "face", second_source, (result,)),
|
|
)))
|
|
|
|
moved = registry.records_for_feature("move")[0]
|
|
statuses = [item["status"] for item in registry.topology_deltas()[0]["relations"]]
|
|
self.assertEqual(moved.owner_feature_ids, ("move",))
|
|
self.assertEqual(statuses, ["ambiguous_exact_continuation", "ambiguous_exact_continuation"])
|
|
self.assertEqual(registry.resolve({
|
|
"kind": "face", "stable_id": "base:first", "owner_feature_id": "base",
|
|
}, active_body_id="body:move").status, "not_found")
|
|
|
|
def test_kernel_deleted_history_blocks_geometry_drift_successor_guessing(self) -> None:
|
|
registry = TopologyRegistry()
|
|
source = object()
|
|
registry.replace_body_topology("base", "body:base", [
|
|
TopologyRecord("base:edge", "edge", "base", "body:base", {
|
|
"curve_type": "line", "start_mm": [0, 0, 0], "end_mm": [0, 0, 10],
|
|
}, source),
|
|
])
|
|
registry.replace_body_topology("dress_up", "body:dress_up", [
|
|
TopologyRecord("dress_up:edge", "edge", "dress_up", "body:dress_up", {
|
|
"curve_type": "line", "start_mm": [0, 0, 1], "end_mm": [0, 0, 9],
|
|
}, object()),
|
|
], topology_delta=TopologyDelta("fillet", (
|
|
TopologyDeltaRelation("deleted", "edge", source),
|
|
)))
|
|
|
|
self.assertNotIn("base:edge", registry._successors)
|
|
self.assertEqual(registry.topology_deltas()[0]["relations"][0]["status"], "recorded_without_owner_transfer")
|
|
|
|
def test_inactive_stable_id_without_geometry_does_not_bind_a_unique_active_candidate(self) -> None:
|
|
registry = TopologyRegistry()
|
|
registry.replace_body_topology("base", "body:base", [
|
|
TopologyRecord("base:edge", "edge", "base", "body:base", {"center_mm": [0, 0, 0]}),
|
|
])
|
|
registry.replace_body_topology("later", "body:later", [
|
|
TopologyRecord("later:edge", "edge", "later", "body:later", {"center_mm": [5, 0, 0]}),
|
|
])
|
|
|
|
resolution = registry.resolve({"kind": "edge", "stable_id": "base:edge"}, active_body_id="body:later")
|
|
self.assertEqual(resolution.status, "not_found")
|
|
self.assertEqual(resolution.diagnostic.code, "selector_stable_id_inactive")
|
|
|
|
def test_output_role_selector_binds_only_unique_active_kernel_evidence(self) -> None:
|
|
registry = TopologyRegistry()
|
|
source_face = object()
|
|
sweep_end = object()
|
|
registry.replace_body_topology("base", "body:base", [
|
|
TopologyRecord("base:face", "face", "base", "body:base", {"center_mm": [0, 0, 0]}, source_face),
|
|
])
|
|
registry.replace_body_topology("sweep", "body:sweep", [
|
|
TopologyRecord("sweep:end", "face", "sweep", "body:sweep", {"center_mm": [0, 0, 10]}, sweep_end),
|
|
], topology_delta=TopologyDelta("sweep", (
|
|
TopologyDeltaRelation("generated", "face", object(), (sweep_end,), output_role="sweep.end"),
|
|
)))
|
|
|
|
resolved = registry.resolve({
|
|
"kind": "face", "owner_feature_id": "sweep", "output_role": "sweep.end",
|
|
}, active_body_id="body:sweep")
|
|
evidence = registry.topology_deltas()[0]["relations"][0]
|
|
|
|
self.assertEqual(resolved.status, "resolved")
|
|
self.assertEqual(resolved.record.record_id, "sweep:end")
|
|
self.assertEqual(resolved.record.output_roles, ("sweep.end",))
|
|
self.assertEqual(evidence["output_role_status"], "unique_result_snapshot")
|
|
registry.replace_body_topology("later", "body:later", [
|
|
TopologyRecord("later:end", "face", "later", "body:later", {"center_mm": [0, 0, 10]}, object()),
|
|
])
|
|
stale = registry.resolve({
|
|
"kind": "face", "owner_feature_id": "sweep", "output_role": "sweep.end",
|
|
}, active_body_id="body:later")
|
|
self.assertEqual(stale.status, "not_found")
|
|
self.assertEqual(stale.diagnostic.code, "selector_output_role_not_found")
|
|
|
|
def test_provenance_selector_uses_exact_lineage_not_geometry_successors(self) -> None:
|
|
registry = TopologyRegistry()
|
|
source = object()
|
|
exact_result = object()
|
|
geometrically_similar = object()
|
|
source_anchor = TopologyRecord(
|
|
"anchor:base:circle", "edge", "base", geometry={}, value=source,
|
|
source_entity=("sketch_base", "circle"),
|
|
)
|
|
registry.register(source_anchor)
|
|
registry.replace_body_topology("later", "body:later", [
|
|
TopologyRecord("later:exact", "face", "later", "body:later", {"center_mm": [10, 0, 0]}, exact_result),
|
|
TopologyRecord("later:similar", "face", "later", "body:later", {"center_mm": [0, 0, 0]}, geometrically_similar),
|
|
], topology_delta=TopologyDelta("extrude", (
|
|
TopologyDeltaRelation(
|
|
"generated", "edge", source, (exact_result,),
|
|
source_kind="edge", result_kind="face", derivation="boundary",
|
|
),
|
|
)), additional_predecessors=[source_anchor])
|
|
selector = {
|
|
"kind": "face", "owner_feature_id": "base",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
"geometry": {"center_mm": [0, 0, 0]},
|
|
"selector_intent": {
|
|
"version": "1.0", "kind": "face", "query_family": "SWEPT_FACE",
|
|
"source_query": {
|
|
"ast": {}, "featurescript_version": "1511",
|
|
"standard_library": "onshape/std/geometry.fs",
|
|
"standard_library_version": "1511.0",
|
|
},
|
|
"source_entity": {"sketch_id": "sketch_base", "entity_id": "circle"},
|
|
"derivation_policy": {"allowed": ["boundary"], "multiplicity": "one"},
|
|
"evidence": "kernel_history",
|
|
},
|
|
}
|
|
resolution = registry.resolve(selector, active_body_id="body:later")
|
|
self.assertEqual(resolution.status, "resolved")
|
|
self.assertEqual(resolution.record.record_id, "later:exact")
|
|
self.assertEqual(registry.lineage()[0].derivation, "boundary")
|
|
|
|
def test_complete_continuation_ignores_nonfinal_builder_handles(self) -> None:
|
|
"""A boolean's intermediate Generated handles are not face fragments.
|
|
|
|
OCC can return both one final ``Modified(face)`` and Generated faces
|
|
which are absent from the result snapshot for the same source face.
|
|
The latter are useful diagnostics but cannot invalidate the exact
|
|
one-to-one target continuation.
|
|
"""
|
|
registry = TopologyRegistry()
|
|
source_edge = object()
|
|
initial_face = object()
|
|
final_face = object()
|
|
intermediate_face = object()
|
|
anchor = TopologyRecord(
|
|
"anchor:base:edge", "edge", "base", geometry={}, value=source_edge,
|
|
source_entity=("sketch_base", "edge"),
|
|
)
|
|
registry.register(anchor)
|
|
registry.replace_body_topology("base", "body:base", [
|
|
TopologyRecord("base:wall", "face", "base", "body:base", {}, initial_face),
|
|
], topology_delta=TopologyDelta("extrude", (
|
|
TopologyDeltaRelation(
|
|
"generated", "edge", source_edge, (initial_face,),
|
|
source_kind="edge", result_kind="face", derivation="boundary",
|
|
),
|
|
)), additional_predecessors=[anchor])
|
|
registry.replace_body_topology("cut", "body:cut", [
|
|
TopologyRecord("cut:wall", "face", "cut", "body:cut", {}, final_face),
|
|
], topology_delta=TopologyDelta("subtract", (
|
|
TopologyDeltaRelation("modified", "face", initial_face, (final_face,)),
|
|
TopologyDeltaRelation(
|
|
"generated", "face", initial_face, (intermediate_face,),
|
|
derivation="boundary",
|
|
),
|
|
)))
|
|
|
|
resolution = registry.resolve({
|
|
"kind": "face", "owner_feature_id": "base",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
"selector_intent": {
|
|
"version": "1.0", "kind": "face", "query_family": "SWEPT_FACE",
|
|
"source_query": {
|
|
"ast": {}, "featurescript_version": "1511",
|
|
"standard_library": "onshape/std/geometry.fs",
|
|
"standard_library_version": "1511.0",
|
|
},
|
|
"source_entity": {"sketch_id": "sketch_base", "entity_id": "edge"},
|
|
"derivation_policy": {"allowed": ["boundary", "continuation"], "multiplicity": "one"},
|
|
"evidence": "kernel_history",
|
|
},
|
|
}, active_body_id="body:cut")
|
|
|
|
self.assertEqual(resolution.status, "resolved")
|
|
self.assertEqual(resolution.record.record_id, "cut:wall")
|
|
cut_relations = registry.topology_deltas()[-1]["relations"]
|
|
self.assertTrue(any(item["coverage"] == "partial" for item in cut_relations))
|
|
|
|
def test_provenance_selector_rejects_non_unique_fragment(self) -> None:
|
|
registry = TopologyRegistry()
|
|
source = object()
|
|
first = object()
|
|
second = object()
|
|
source_anchor = TopologyRecord(
|
|
"anchor:base:vertex", "vertex", "base", geometry={}, value=source,
|
|
source_entities=(("sketch_base", "left"), ("sketch_base", "right")),
|
|
)
|
|
registry.register(source_anchor)
|
|
registry.replace_body_topology("fillet", "body:fillet", [
|
|
TopologyRecord("fillet:first", "edge", "fillet", "body:fillet", {"center_mm": [0, 0, 0]}, first),
|
|
TopologyRecord("fillet:second", "edge", "fillet", "body:fillet", {"center_mm": [1, 0, 0]}, second),
|
|
], topology_delta=TopologyDelta("fillet", (
|
|
TopologyDeltaRelation(
|
|
"modified", "vertex", source, (first, second),
|
|
source_kind="vertex", result_kind="edge", derivation="fragment",
|
|
),
|
|
)), additional_predecessors=[source_anchor])
|
|
resolution = registry.resolve({
|
|
"kind": "edge", "owner_feature_id": "base",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
"selector_intent": {
|
|
"version": "1.0", "kind": "edge", "query_family": "SWEPT_EDGE",
|
|
"source_query": {
|
|
"ast": {}, "featurescript_version": "1511",
|
|
"standard_library": "onshape/std/geometry.fs",
|
|
"standard_library_version": "1511.0",
|
|
},
|
|
"source_entities": [
|
|
{"sketch_id": "sketch_base", "entity_id": "left"},
|
|
{"sketch_id": "sketch_base", "entity_id": "right"},
|
|
],
|
|
"derivation_policy": {"allowed": ["fragment"], "multiplicity": "one"},
|
|
"evidence": "kernel_history",
|
|
},
|
|
}, active_body_id="body:fillet")
|
|
self.assertEqual(resolution.status, "ambiguous")
|
|
self.assertEqual(resolution.diagnostic.code, "selector_relation_non_unique")
|
|
|
|
def test_provenance_selector_returns_all_proven_fragments(self) -> None:
|
|
registry = TopologyRegistry()
|
|
source = object()
|
|
first = object()
|
|
second = object()
|
|
source_anchor = TopologyRecord(
|
|
"anchor:base:vertex", "vertex", "base", geometry={}, value=source,
|
|
source_entities=(("sketch_base", "left"), ("sketch_base", "right")),
|
|
)
|
|
registry.register(source_anchor)
|
|
registry.replace_body_topology("fillet", "body:fillet", [
|
|
TopologyRecord("fillet:first", "edge", "fillet", "body:fillet", {}, first),
|
|
TopologyRecord("fillet:second", "edge", "fillet", "body:fillet", {}, second),
|
|
], topology_delta=TopologyDelta("fillet", (
|
|
TopologyDeltaRelation(
|
|
"modified", "vertex", source, (first, second),
|
|
source_kind="vertex", result_kind="edge", derivation="fragment",
|
|
),
|
|
)), additional_predecessors=[source_anchor])
|
|
resolution = registry.resolve({
|
|
"kind": "edge", "owner_feature_id": "base",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
"selector_intent": {
|
|
"version": "1.0", "kind": "edge", "query_family": "SWEPT_EDGE",
|
|
"source_query": {
|
|
"ast": {}, "featurescript_version": "1511",
|
|
"standard_library": "onshape/std/geometry.fs",
|
|
"standard_library_version": "1511.0",
|
|
},
|
|
"source_entities": [
|
|
{"sketch_id": "sketch_base", "entity_id": "left"},
|
|
{"sketch_id": "sketch_base", "entity_id": "right"},
|
|
],
|
|
"derivation_policy": {"allowed": ["fragment"], "multiplicity": "all_fragments"},
|
|
"evidence": "kernel_history",
|
|
},
|
|
}, active_body_id="body:fillet")
|
|
self.assertEqual(resolution.status, "resolved")
|
|
self.assertIsNone(resolution.record)
|
|
self.assertEqual([record.record_id for record in resolution.records], ["fillet:first", "fillet:second"])
|
|
|
|
def test_fillet_consumes_all_proven_fragment_records(self) -> None:
|
|
"""A dress-up must consume every record allowed by all_fragments."""
|
|
from build123d import Box
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
from cdsl_engine.runtime import ExecutionSession, FeaturePlanNode, execute_node
|
|
|
|
class RecordingAdapter(Build123dGeometryAdapter):
|
|
def __init__(self) -> None:
|
|
self.selected_edges: list[object] = []
|
|
|
|
def fillet_with_topology_delta(self, body, radius_mm, edges):
|
|
self.selected_edges = list(edges)
|
|
# Selection delivery is the behavior under test. Returning the
|
|
# same valid B-rep keeps this focused on the executor contract.
|
|
return body, None
|
|
|
|
adapter = RecordingAdapter()
|
|
body = Box(10, 10, 10)
|
|
first, second = body.edges()[:2]
|
|
source = object()
|
|
session = ExecutionSession(sketches={}, nodes={}, adapter=adapter)
|
|
session.body = body
|
|
session.body_id = "body:fragments"
|
|
session.body_members = {"fragments": body}
|
|
source_anchor = TopologyRecord(
|
|
"anchor:source:vertex", "vertex", "source", geometry={}, value=source,
|
|
source_entities=(("sketch_source", "left"), ("sketch_source", "right")),
|
|
)
|
|
session.topology.register(source_anchor)
|
|
session.topology.replace_body_topology("fragments", "body:fragments", [
|
|
TopologyRecord("fragments:first", "edge", "fragments", "body:fragments", value=first),
|
|
TopologyRecord("fragments:second", "edge", "fragments", "body:fragments", value=second),
|
|
], topology_delta=TopologyDelta("split", (
|
|
TopologyDeltaRelation(
|
|
"modified", "vertex", source, (first, second),
|
|
source_kind="vertex", result_kind="edge", derivation="fragment",
|
|
),
|
|
)), additional_predecessors=[source_anchor])
|
|
node = FeaturePlanNode(
|
|
"fillet", "fillet", None, ("fragments",), {"radius_mm": 0.5}, ({
|
|
"kind": "edge", "owner_feature_id": "source",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
"selector_intent": {
|
|
"version": "1.0", "kind": "edge", "query_family": "SWEPT_EDGE",
|
|
"source_query": {
|
|
"ast": {}, "featurescript_version": "1511",
|
|
"standard_library": "onshape/std/geometry.fs",
|
|
"standard_library_version": "1511.0",
|
|
},
|
|
"source_entities": [
|
|
{"sketch_id": "sketch_source", "entity_id": "left"},
|
|
{"sketch_id": "sketch_source", "entity_id": "right"},
|
|
],
|
|
"derivation_policy": {"allowed": ["fragment"], "multiplicity": "all_fragments"},
|
|
},
|
|
},), None, "supported", {"id": "fillet"},
|
|
)
|
|
|
|
result = execute_node(node, session)
|
|
|
|
self.assertEqual(result.status, "executed")
|
|
self.assertEqual(len(adapter.selected_edges), 2)
|
|
self.assertTrue(adapter.selected_edges[0].is_same(first))
|
|
self.assertTrue(adapter.selected_edges[1].is_same(second))
|
|
self.assertEqual(
|
|
[record["record_id"] for record in session.selector_resolutions[-1]["records"]],
|
|
["fragments:first", "fragments:second"],
|
|
)
|
|
|
|
def test_boolean_section_edges_are_recorded_as_intersection_lineage(self) -> None:
|
|
registry = TopologyRegistry()
|
|
section_edge = object()
|
|
registry.replace_body_topology("boolean", "body:boolean", [
|
|
TopologyRecord("boolean:section", "edge", "boolean", "body:boolean", {}, section_edge),
|
|
], topology_delta=TopologyDelta("intersect", section_values=(section_edge,)))
|
|
delta = registry.topology_deltas()[0]
|
|
lineage = delta["lineage"][0]
|
|
self.assertEqual(lineage["derivation"], "intersection")
|
|
self.assertEqual(lineage["result_record_ids"], ["boolean:section"])
|
|
self.assertTrue(delta["relations"][0]["section_edge"])
|
|
|
|
def test_boolean_builder_qualifies_section_edges_with_exact_input_faces(self) -> None:
|
|
from build123d import Location, Solid
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
adapter = Build123dGeometryAdapter()
|
|
target = Solid.make_box(10, 10, 10)
|
|
tool = Solid.make_box(10, 10, 10).moved(Location((5, 5, 5)))
|
|
result, delta = adapter.cut_with_topology_delta(target, tool)
|
|
|
|
self.assertTrue(result.is_valid)
|
|
self.assertIsNotNone(delta)
|
|
self.assertTrue(delta.section_relations)
|
|
final_edges = [edge.wrapped for edge in result.edges()]
|
|
target_faces = [face.wrapped for face in target.faces()]
|
|
tool_faces = [face.wrapped for face in tool.faces()]
|
|
for relation in delta.section_relations:
|
|
self.assertTrue(any(relation.result_value.IsSame(edge) for edge in final_edges))
|
|
self.assertTrue(any(relation.source_values[0].IsSame(face) for face in target_faces))
|
|
self.assertTrue(any(relation.source_values[1].IsSame(face) for face in tool_faces))
|
|
# Qualified results must not also be exposed through the unqualified
|
|
# SectionEdges-only diagnostic channel.
|
|
self.assertFalse(delta.section_values)
|
|
|
|
def test_boolean_section_selector_executes_through_source_qualified_lineage(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
def source_rectangle(points: list[list[float]]) -> list[dict]:
|
|
return [
|
|
{"type": "line", "start": start, "end": end, "source_entity_id": f"E{index}"}
|
|
for index, (start, end) in enumerate(zip(points, [*points[1:], points[0]]))
|
|
]
|
|
|
|
workplane = _workplane()
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "section-selector",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{
|
|
"id": "left_sketch", "source_sketch_id": "L", "workplane": workplane,
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True,
|
|
"segments": source_rectangle([[0, 0], [10, 0], [10, 10], [0, 10]]),
|
|
}]},
|
|
},
|
|
{
|
|
"id": "right_sketch", "source_sketch_id": "R", "workplane": workplane,
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True,
|
|
"segments": source_rectangle([[5, -5], [15, -5], [15, 5], [5, 5]]),
|
|
}]},
|
|
},
|
|
]},
|
|
"features": [
|
|
{
|
|
"id": "left", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "left_sketch",
|
|
"params": {"distance_mm": 4, "result_mode": "new_body"},
|
|
},
|
|
{
|
|
"id": "right", "atomic_id": "extrude_add_blind", "depends_on": ["left"], "sketch_id": "right_sketch",
|
|
"params": {"distance_mm": 4, "result_mode": "new_body"},
|
|
},
|
|
{
|
|
"id": "boolean", "atomic_id": "boolean_bodies", "depends_on": ["left", "right"],
|
|
"params": {
|
|
"operation": "subtract", "target_feature_ids": ["left"],
|
|
"tool_feature_ids": ["right"], "keep_tools": False,
|
|
},
|
|
},
|
|
{
|
|
"id": "fillet", "atomic_id": "fillet", "depends_on": ["boolean"], "params": {"radius_mm": 0.1},
|
|
"selectors": [{
|
|
"kind": "edge", "owner_feature_id": "boolean", "source": "runtime_snapshot", "confidence": 1.0,
|
|
"selector_intent": {
|
|
"version": "1.0", "kind": "edge", "query_family": "INTERSECT",
|
|
"source_query": {
|
|
"ast": {"call": "makeQuery"}, "featurescript_version": "1511",
|
|
"standard_library": "onshape/std/geometry.fs",
|
|
"standard_library_version": "1511.0",
|
|
},
|
|
"derivation_policy": {"allowed": ["intersection"], "multiplicity": "one"},
|
|
"evidence": "kernel_history",
|
|
"intersection_sources": [
|
|
{
|
|
"query_family": "SWEPT_FACE", "owner_feature_id": "left",
|
|
"source_entity": {"sketch_id": "L", "entity_id": "E0"},
|
|
},
|
|
{
|
|
"query_family": "SWEPT_FACE", "owner_feature_id": "right",
|
|
"source_entity": {"sketch_id": "R", "entity_id": "E3"},
|
|
},
|
|
],
|
|
},
|
|
}],
|
|
},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "section-selector.step")
|
|
|
|
self.assertEqual(result["feature_results"][-1]["feature_id"], "fillet")
|
|
section_relations = [
|
|
relation for delta in result["topology_deltas"] if delta["feature_id"] == "boolean"
|
|
for relation in delta["relations"] if relation.get("source_qualified")
|
|
]
|
|
self.assertTrue(section_relations)
|
|
|
|
# A primary REMOVE owns no active tool member. It can still retain the
|
|
# same source-qualified section evidence only through its transient
|
|
# direct-prism tool snapshot.
|
|
primary = deepcopy(cdsl)
|
|
primary["part_id"] = "primary-section-selector"
|
|
primary["features"][1].update({
|
|
"id": "cut", "atomic_id": "extrude_cut_blind", "depends_on": ["left"],
|
|
"params": {"distance_mm": 4},
|
|
})
|
|
primary["features"] = [primary["features"][0], primary["features"][1], primary["features"][3]]
|
|
primary["features"][2]["depends_on"] = ["cut"]
|
|
selector = primary["features"][2]["selectors"][0]
|
|
selector["owner_feature_id"] = "cut"
|
|
selector["selector_intent"]["intersection_sources"][1]["owner_feature_id"] = "cut"
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
primary_result = rebuild_cdsl(primary, Path(directory) / "primary-section-selector.step")
|
|
|
|
self.assertEqual(primary_result["feature_results"][-1]["feature_id"], "fillet")
|
|
primary_delta = next(
|
|
delta for delta in primary_result["topology_deltas"]
|
|
if delta["feature_id"] == "cut" and delta["operation"] == "subtract"
|
|
)
|
|
self.assertEqual(set(primary_delta["input_snapshot_ids"]), {"body:left", "transient:cut"})
|
|
self.assertTrue(any(relation.get("source_qualified") for relation in primary_delta["relations"]))
|
|
self.assertTrue(any(record.get("transient") for record in primary_result["topology_records"]))
|
|
|
|
def test_shell_offset_role_source_selects_one_exact_builder_relation(self) -> None:
|
|
registry = TopologyRegistry()
|
|
extrude_start = object()
|
|
extrude_end = object()
|
|
offset_start = object()
|
|
offset_end = object()
|
|
registry.replace_body_topology("extrude", "body:extrude", [
|
|
TopologyRecord(
|
|
"extrude:start", "face", "extrude", "body:extrude", {"center_mm": [0, 0, 0]}, extrude_start,
|
|
owner_feature_ids=("extrude",), output_roles=("extrude.start",),
|
|
),
|
|
TopologyRecord(
|
|
"extrude:end", "face", "extrude", "body:extrude", {"center_mm": [0, 0, 10]}, extrude_end,
|
|
owner_feature_ids=("extrude",), output_roles=("extrude.end",),
|
|
),
|
|
])
|
|
registry.replace_body_topology("shell", "body:shell", [
|
|
TopologyRecord("shell:offset-start", "face", "shell", "body:shell", {"center_mm": [0, 0, 1]}, offset_start),
|
|
TopologyRecord("shell:offset-end", "face", "shell", "body:shell", {"center_mm": [0, 0, 9]}, offset_end),
|
|
], topology_delta=TopologyDelta("shell", (
|
|
TopologyDeltaRelation("generated", "face", extrude_start, (offset_start,), output_role="shell.offset_face"),
|
|
TopologyDeltaRelation("generated", "face", extrude_end, (offset_end,), output_role="shell.offset_face"),
|
|
)))
|
|
|
|
ambiguous = registry.resolve({
|
|
"kind": "face", "owner_feature_id": "shell", "output_role": "shell.offset_face",
|
|
}, active_body_id="body:shell")
|
|
selected = registry.resolve({
|
|
"kind": "face", "owner_feature_id": "shell", "output_role": "shell.offset_face",
|
|
"output_role_source": {"owner_feature_id": "extrude", "output_role": "extrude.start"},
|
|
}, active_body_id="body:shell")
|
|
|
|
self.assertEqual(ambiguous.status, "ambiguous")
|
|
self.assertEqual(selected.status, "resolved")
|
|
self.assertEqual(selected.record.record_id, "shell:offset-start")
|
|
self.assertEqual(
|
|
selected.record.output_role_sources,
|
|
(("shell.offset_face", "extrude", "extrude.start"),),
|
|
)
|
|
|
|
def test_tapered_prism_exposes_builder_proven_single_cap_roles(self) -> None:
|
|
from build123d import Face, Plane, Wire
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
face = Face(Wire.make_circle(10, Plane.XY))
|
|
result, delta = Build123dGeometryAdapter().extrude_taper_with_topology_delta(
|
|
face, (0.0, 0.0, 20.0), 10.0,
|
|
)
|
|
|
|
self.assertTrue(result.is_valid)
|
|
self.assertGreater(result.volume, 0.0)
|
|
self.assertIsNotNone(delta)
|
|
self.assertEqual(
|
|
[(relation.event, relation.kind, relation.output_role) for relation in delta.relations],
|
|
[("generated", "face", "extrude.start"), ("generated", "face", "extrude.end")],
|
|
)
|
|
|
|
def test_exact_kernel_history_crosses_a_boolean_member_index_shift(self) -> None:
|
|
registry = TopologyRegistry()
|
|
independent = object()
|
|
tool = object()
|
|
draft_cap = object()
|
|
boolean_result_cap = object()
|
|
registry.replace_body_topologies("draft", [
|
|
("body:draft:0", [TopologyRecord(
|
|
"draft:independent", "face", "independent", "body:draft:0", {"center_mm": [-5, 0, 0]}, independent,
|
|
)]),
|
|
("body:draft:1", [TopologyRecord(
|
|
"draft:tool", "face", "tool", "body:draft:1", {"center_mm": [0, 0, 0]}, tool,
|
|
)]),
|
|
("body:draft:2", [TopologyRecord(
|
|
"draft:cap", "face", "draft", "body:draft:2", {"center_mm": [5, 0, 0]}, draft_cap,
|
|
owner_feature_ids=("draft",), output_roles=("extrude.start",),
|
|
)]),
|
|
], active_body_id="body:draft")
|
|
registry.replace_body_topologies("boolean", [
|
|
("body:boolean:0", [TopologyRecord(
|
|
"boolean:independent", "face", "boolean", "body:boolean:0", {"center_mm": [-5, 0, 0]}, independent,
|
|
)]),
|
|
("body:boolean:1", [TopologyRecord(
|
|
"boolean:cap", "face", "boolean", "body:boolean:1", {"center_mm": [5, 0, 0]}, boolean_result_cap,
|
|
)]),
|
|
], active_body_id="body:boolean", topology_delta=TopologyDelta("subtract", (
|
|
TopologyDeltaRelation("modified", "face", draft_cap, (boolean_result_cap,)),
|
|
)))
|
|
|
|
resolution = registry.resolve({
|
|
"kind": "face", "owner_feature_id": "draft", "output_role": "extrude.start",
|
|
}, active_body_id="body:boolean")
|
|
|
|
self.assertEqual(resolution.status, "resolved")
|
|
self.assertEqual(resolution.record.record_id, "boolean:cap")
|
|
self.assertEqual(resolution.record.owner_feature_ids, ("draft",))
|
|
self.assertEqual(resolution.record.output_roles, ("extrude.start",))
|
|
|
|
def test_output_role_semantic_contract_requires_face_runtime_evidence(self) -> None:
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["execution_status"] = "supported"
|
|
cdsl["features"].append({
|
|
"id": "shell", "atomic_id": "shell", "depends_on": ["base_add"],
|
|
"params": {"thickness_mm": 1}, "execution_status": "supported",
|
|
"selectors": [{
|
|
"kind": "face", "owner_feature_id": "base_add", "output_role": "sweep.end",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
}],
|
|
})
|
|
self.assertTrue(validate_semantic_cdsl(cdsl)["future_rebuild_ready"])
|
|
|
|
invalid = deepcopy(cdsl)
|
|
invalid["features"][-1]["selectors"][0]["stable_id"] = "legacy-face"
|
|
with self.assertRaisesRegex(ValueError, "cannot mix stable or geometry evidence"):
|
|
validate_semantic_cdsl(invalid)
|
|
|
|
nested = deepcopy(cdsl)
|
|
nested["features"][-1]["selectors"][0]["matched_selectors"] = [{
|
|
"kind": "face", "owner_feature_id": "base_add", "output_role": "sweep.start",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
}]
|
|
with self.assertRaisesRegex(ValueError, "only supported in feature.selectors"):
|
|
validate_semantic_cdsl(nested)
|
|
|
|
bypassed_semantic_validation = deepcopy(cdsl)
|
|
bypassed_semantic_validation["features"][-1]["params"]["role_reference"] = {
|
|
"kind": "face", "owner_feature_id": "base_add", "output_role": "sweep.start",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
}
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids={"extrude_add_blind", "shell"}, profile_types=SHAPE_GENERATORS,
|
|
).analyze(bypassed_semantic_validation)
|
|
self.assertIn(
|
|
"unsupported_output_role_selector_context",
|
|
[blocker.code for blocker in analysis.feature_results[-1].blockers],
|
|
)
|
|
|
|
malformed_source = deepcopy(cdsl)
|
|
malformed_source["features"][-1]["selectors"][0]["output_role"] = "shell.offset_face"
|
|
malformed_source["features"][-1]["selectors"][0]["output_role_source"] = {"owner_feature_id": "base_add"}
|
|
with self.assertRaisesRegex(ValueError, "CDSL schema violation"):
|
|
validate_semantic_cdsl(malformed_source)
|
|
|
|
unsupported_source = deepcopy(cdsl)
|
|
unsupported_source["features"][-1]["selectors"][0]["output_role"] = "shell.offset_face"
|
|
unsupported_source["features"][-1]["selectors"][0]["output_role_source"] = {
|
|
"owner_feature_id": "base_add", "output_role": "sweep.end",
|
|
}
|
|
with self.assertRaisesRegex(ValueError, "must be an extrude cap role"):
|
|
validate_semantic_cdsl(unsupported_source)
|
|
|
|
invalid_shell_target = deepcopy(cdsl)
|
|
invalid_shell_target["features"][-1]["params"]["target_feature_id"] = "shell"
|
|
with self.assertRaisesRegex(ValueError, "requires a preceding body feature"):
|
|
validate_semantic_cdsl(invalid_shell_target)
|
|
|
|
def test_up_to_surface_consumes_only_an_immediate_cap_output_role(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl, rebuild_cdsl
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cap_reference = {
|
|
"kind": "face", "owner_feature_id": "base_add", "output_role": "extrude.end",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
"selector_intent": {
|
|
"version": "1.0", "kind": "face", "query_family": "CAP_FACE",
|
|
"source_query": {
|
|
"ast": {"call": "makeQuery"}, "featurescript_version": "1511",
|
|
"standard_library": "onshape/std/geometry.fs",
|
|
"standard_library_version": "1511.0",
|
|
},
|
|
"derivation_policy": {"allowed": ["boundary", "continuation"], "multiplicity": "one"},
|
|
"evidence": "operation_role", "output_role": "extrude.end",
|
|
},
|
|
}
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["execution_status"] = "supported"
|
|
cdsl["features"][0]["params"].update({
|
|
"result_mode": "new_body", "end_condition": {"type": "blind", "solidworks_code": 0},
|
|
})
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "cut", "workplane": _workplane(),
|
|
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "cut", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {
|
|
"distance_mm": 1,
|
|
"end_condition": {"type": "up_to_surface", "solidworks_code": 2, "reference": cap_reference},
|
|
},
|
|
"sketch_id": "cut", "execution_status": "supported",
|
|
})
|
|
|
|
self.assertTrue(validate_semantic_cdsl(cdsl)["future_rebuild_ready"])
|
|
self.assertTrue(analyze_cdsl(cdsl).feature_results[-1].executable)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
rebuilt = rebuild_cdsl(cdsl, Path(directory) / "up-to-surface-cap-role.step")
|
|
resolution = next(item for item in rebuilt["selector_resolution"] if item["feature_id"] == "cut")
|
|
self.assertEqual(resolution["status"], "resolved")
|
|
self.assertEqual(resolution["resolution_mode"], "operation_role")
|
|
self.assertEqual(resolution["selected"]["output_roles"], ["extrude.end"])
|
|
|
|
non_new_body = deepcopy(cdsl)
|
|
non_new_body["features"][0]["params"]["result_mode"] = "fuse"
|
|
with self.assertRaisesRegex(ValueError, "direct new_body or primary ADD blind extrusion cap"):
|
|
validate_semantic_cdsl(non_new_body)
|
|
self.assertIn(
|
|
"unsupported_extent_output_role_selector",
|
|
[blocker.code for blocker in analyze_cdsl(non_new_body).feature_results[-1].blockers],
|
|
)
|
|
|
|
non_immediate = deepcopy(cdsl)
|
|
non_immediate["features"].insert(1, {
|
|
"id": "gap", "atomic_id": "reference_plane", "depends_on": ["base_add"],
|
|
"params": {"plane": _workplane()}, "execution_status": "supported",
|
|
})
|
|
non_immediate["features"][-1]["depends_on"] = ["gap"]
|
|
with self.assertRaisesRegex(ValueError, "immediately preceding direct new_body or primary ADD blind extrusion cap"):
|
|
validate_semantic_cdsl(non_immediate)
|
|
self.assertIn(
|
|
"unsupported_extent_output_role_selector",
|
|
[blocker.code for blocker in analyze_cdsl(non_immediate).feature_results[-1].blockers],
|
|
)
|
|
|
|
def test_shell_consumes_only_an_immediate_direct_cap_output_role(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl, rebuild_cdsl
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cap_selector = {
|
|
"kind": "face", "owner_feature_id": "base_add", "output_role": "extrude.end",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
"selector_intent": {
|
|
"version": "1.0", "kind": "face", "query_family": "CAP_FACE",
|
|
"source_query": {
|
|
"ast": {"call": "makeQuery"}, "featurescript_version": "1511",
|
|
"standard_library": "onshape/std/geometry.fs",
|
|
"standard_library_version": "1511.0",
|
|
},
|
|
"derivation_policy": {"allowed": ["boundary", "continuation"], "multiplicity": "one"},
|
|
"evidence": "operation_role", "output_role": "extrude.end",
|
|
},
|
|
}
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["execution_status"] = "supported"
|
|
cdsl["features"][0]["params"].update({
|
|
"result_mode": "new_body", "end_condition": {"type": "blind", "solidworks_code": 0},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "shell", "atomic_id": "shell", "depends_on": ["base_add"],
|
|
"params": {"thickness_mm": 1, "inward": True}, "selectors": [cap_selector],
|
|
"execution_status": "supported",
|
|
})
|
|
|
|
self.assertTrue(validate_semantic_cdsl(cdsl)["future_rebuild_ready"])
|
|
self.assertTrue(analyze_cdsl(cdsl).feature_results[-1].executable)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
rebuilt = rebuild_cdsl(cdsl, Path(directory) / "cap-role-shell.step")
|
|
resolution = next(item for item in rebuilt["selector_resolution"] if item["feature_id"] == "shell")
|
|
self.assertEqual(resolution["status"], "resolved")
|
|
self.assertEqual(resolution["resolution_mode"], "operation_role")
|
|
self.assertEqual(resolution["selected"]["output_roles"], ["extrude.end"])
|
|
|
|
non_immediate = deepcopy(cdsl)
|
|
non_immediate["features"].insert(1, {
|
|
"id": "gap", "atomic_id": "reference_plane", "depends_on": ["base_add"],
|
|
"params": {"plane": _workplane()}, "execution_status": "supported",
|
|
})
|
|
non_immediate["features"][-1]["depends_on"] = ["gap"]
|
|
with self.assertRaisesRegex(ValueError, "CAP_FACE output role requires the immediately preceding direct new_body blind extrusion cap"):
|
|
validate_semantic_cdsl(non_immediate)
|
|
self.assertIn(
|
|
"unsupported_cap_face_output_role_selector",
|
|
[blocker.code for blocker in analyze_cdsl(non_immediate).feature_results[-1].blockers],
|
|
)
|
|
|
|
def test_transformed_owner_selector_executes_a_downstream_fillet(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].append({
|
|
"id": "move", "atomic_id": "transform_bodies", "depends_on": ["base_add"],
|
|
"params": {
|
|
"source_feature_ids": ["base_add"],
|
|
"transform": {"type": "translation", "translation_mm": [20, 0, 0]},
|
|
"make_copy": False,
|
|
},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
moved = rebuild_cdsl(cdsl, root / "moved.step")
|
|
edge = next(
|
|
item for item in moved["topology_records"]
|
|
if item["kind"] == "edge" and item.get("body_id") == "body:move"
|
|
)
|
|
filleted = deepcopy(cdsl)
|
|
filleted["features"].append({
|
|
"id": "fillet", "atomic_id": "fillet", "depends_on": ["move"], "params": {"radius_mm": 1},
|
|
"selectors": [{
|
|
"kind": "edge", "stable_id": "moved-edge", "source": "runtime_snapshot", "confidence": 1,
|
|
"owner_feature_id": "base_add", "geometry": edge["geometry"],
|
|
}],
|
|
})
|
|
result = rebuild_cdsl(filleted, root / "filleted.step")
|
|
|
|
self.assertEqual([item["feature_id"] for item in result["feature_results"]], ["base_add", "move", "fillet"])
|
|
self.assertLess(result["volume_mm3"], moved["volume_mm3"])
|
|
self.assertTrue(any(
|
|
item["operation"] == "translation" for item in result["topology_deltas"]
|
|
))
|
|
|
|
def test_body_transform_unavailable_source_is_a_preflight_diagnostic(self) -> None:
|
|
cdsl = self._base_block()
|
|
cdsl["features"].append({
|
|
"id": "move", "atomic_id": "transform_bodies", "depends_on": ["base_add"],
|
|
"params": {
|
|
"source_feature_ids": ["missing_body"],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]},
|
|
"make_copy": True,
|
|
},
|
|
})
|
|
analysis = CapabilityAnalyzer(atomic_ids={"extrude_add_blind", "transform_bodies"}, profile_types=SHAPE_GENERATORS).analyze(cdsl)
|
|
blocker = next(item for item in analysis.feature_results[-1].blockers if item.code == "body_source_unavailable")
|
|
self.assertEqual(blocker.code, "body_source_unavailable")
|
|
self.assertEqual(blocker.detail, {"source_feature_id": "missing_body"})
|
|
|
|
def test_absorbed_body_source_is_not_selectable_after_a_fused_feature(self) -> None:
|
|
cdsl = self._base_block()
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "fused_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 5}, "sketch_id": "base",
|
|
},
|
|
{
|
|
"id": "move", "atomic_id": "transform_bodies", "depends_on": ["fused_add"],
|
|
"params": {
|
|
"source_feature_ids": ["base_add"],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]},
|
|
"make_copy": True,
|
|
},
|
|
},
|
|
])
|
|
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids={"extrude_add_blind", "transform_bodies"}, profile_types=SHAPE_GENERATORS,
|
|
).analyze(cdsl)
|
|
|
|
blocker = next(item for item in analysis.feature_results[-1].blockers if item.code == "body_source_unavailable")
|
|
self.assertEqual(blocker.detail, {"source_feature_id": "base_add"})
|
|
|
|
def test_boolean_consumed_body_source_is_not_selectable_afterward(self) -> None:
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["params"]["result_mode"] = "new_body"
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "tool_body", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 5, "result_mode": "new_body"}, "sketch_id": "base",
|
|
},
|
|
{
|
|
"id": "join", "atomic_id": "boolean_bodies", "depends_on": ["base_add", "tool_body"],
|
|
"params": {
|
|
"operation": "union", "target_feature_ids": ["base_add"],
|
|
"tool_feature_ids": ["tool_body"], "keep_tools": False,
|
|
},
|
|
},
|
|
{
|
|
"id": "move", "atomic_id": "transform_bodies", "depends_on": ["join"],
|
|
"params": {
|
|
"source_feature_ids": ["base_add"],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]},
|
|
"make_copy": True,
|
|
},
|
|
},
|
|
])
|
|
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids={"extrude_add_blind", "boolean_bodies", "transform_bodies"}, profile_types=SHAPE_GENERATORS,
|
|
).analyze(cdsl)
|
|
|
|
blocker = next(item for item in analysis.feature_results[-1].blockers if item.code == "body_source_unavailable")
|
|
self.assertEqual(blocker.detail, {"source_feature_id": "base_add"})
|
|
|
|
def test_boolean_rejects_a_body_source_absorbed_by_a_fused_feature(self) -> None:
|
|
cdsl = self._base_block()
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "fused_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 5}, "sketch_id": "base",
|
|
},
|
|
{
|
|
"id": "selected_tool", "atomic_id": "extrude_add_blind", "depends_on": ["fused_add"],
|
|
"params": {"distance_mm": 3, "result_mode": "new_body"}, "sketch_id": "base",
|
|
},
|
|
{
|
|
"id": "boolean", "atomic_id": "boolean_bodies", "depends_on": ["selected_tool"],
|
|
"params": {
|
|
"operation": "subtract", "target_feature_ids": ["base_add"],
|
|
"tool_feature_ids": ["selected_tool"], "keep_tools": False,
|
|
},
|
|
},
|
|
])
|
|
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids={"extrude_add_blind", "boolean_bodies"}, profile_types=SHAPE_GENERATORS,
|
|
).analyze(cdsl)
|
|
|
|
blocker = next(item for item in analysis.feature_results[-1].blockers if item.code == "boolean_body_unavailable")
|
|
self.assertEqual(blocker.detail, {"source_feature_id": "base_add"})
|
|
|
|
def test_deleted_last_body_clears_the_active_body_precondition(self) -> None:
|
|
cdsl = self._base_block()
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "delete_base", "atomic_id": "delete_bodies", "depends_on": ["base_add"],
|
|
"params": {"target_feature_ids": ["base_add"]},
|
|
},
|
|
{
|
|
"id": "cut_after_delete", "atomic_id": "extrude_cut_blind", "depends_on": ["delete_base"],
|
|
"params": {"distance_mm": 1}, "sketch_id": "base",
|
|
},
|
|
])
|
|
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids={"extrude_add_blind", "extrude_cut_blind", "delete_bodies"}, profile_types=SHAPE_GENERATORS,
|
|
).analyze(cdsl)
|
|
|
|
blockers = {item.code for item in analysis.feature_results[-1].blockers}
|
|
self.assertIn("missing_active_body", blockers)
|
|
|
|
def test_pattern_replay_does_not_make_its_source_a_body_member(self) -> None:
|
|
cdsl = self._base_block()
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "array", "atomic_id": "pattern_linear", "depends_on": ["base_add"],
|
|
"params": {
|
|
"source_feature_ids": ["base_add"], "direction_1": [1, 0, 0],
|
|
"spacing_1_mm": 20, "pattern_count_1": 2,
|
|
},
|
|
},
|
|
{
|
|
"id": "move", "atomic_id": "transform_bodies", "depends_on": ["array"],
|
|
"params": {
|
|
"source_feature_ids": ["base_add"],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]},
|
|
"make_copy": True,
|
|
},
|
|
},
|
|
])
|
|
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids={"extrude_add_blind", "pattern_linear", "transform_bodies"}, profile_types=SHAPE_GENERATORS,
|
|
).analyze(cdsl)
|
|
|
|
blocker = next(item for item in analysis.feature_results[-1].blockers if item.code == "body_source_unavailable")
|
|
self.assertEqual(blocker.detail, {"source_feature_id": "base_add"})
|
|
|
|
def test_deferred_reference_is_currently_executable_without_a_sketch(self) -> None:
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"geometry": {"sketches": [{"id": "s", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 1}}]},
|
|
"features": [{
|
|
"id": "plane", "atomic_id": "reference_plane", "depends_on": [],
|
|
"execution_status": "deferred", "params": {"plane": _workplane()},
|
|
}],
|
|
}
|
|
analysis = CapabilityAnalyzer(atomic_ids={"reference_plane"}, profile_types=SHAPE_GENERATORS).analyze(cdsl)
|
|
self.assertFalse(analysis.runtime_eligible)
|
|
self.assertEqual(analysis.feature_results[0].resolved_status, "executable")
|
|
self.assertEqual(analysis.document_blockers[0].code, "no_solid_feature")
|
|
|
|
def test_reference_point_is_non_mutating_and_requires_finite_coordinates(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl, prepare_cdsl_execution
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].append({
|
|
"id": "datum", "atomic_id": "reference_point", "depends_on": ["base_add"],
|
|
"params": {"point_mm": [1.0, 2.0, 3.0]},
|
|
})
|
|
execution = prepare_cdsl_execution(cdsl)
|
|
base = execution.execute_next()
|
|
point = execution.execute_next()
|
|
self.assertEqual(base.status, "executed")
|
|
self.assertEqual(point.status, "executed")
|
|
self.assertIsNone(point.body_id)
|
|
self.assertEqual(execution.session.body_id, "body:base_add")
|
|
|
|
invalid = deepcopy(cdsl)
|
|
invalid["features"][-1]["params"]["point_mm"] = [0.0, math.nan, 0.0]
|
|
analysis = analyze_cdsl(invalid)
|
|
datum = next(item for item in analysis.feature_results if item.feature_id == "datum")
|
|
self.assertIn("invalid_reference_point", [item.code for item in datum.blockers])
|
|
|
|
def test_assign_variable_is_non_mutating_and_rejects_nonfinite_values(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl, prepare_cdsl_execution
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
for feature in cdsl["features"]:
|
|
feature["execution_status"] = "supported"
|
|
cdsl["features"].append({
|
|
"id": "thickness", "atomic_id": "assign_variable", "depends_on": ["base_add"],
|
|
"params": {"name": "thickness", "value": 4.0, "value_kind": "any"},
|
|
"execution_status": "supported",
|
|
})
|
|
validate_semantic_cdsl(cdsl)
|
|
execution = prepare_cdsl_execution(cdsl)
|
|
base = execution.execute_next()
|
|
variable = execution.execute_next()
|
|
self.assertEqual(base.status, "executed")
|
|
self.assertEqual(variable.status, "executed")
|
|
self.assertIsNone(variable.body_id)
|
|
self.assertEqual(execution.session.body_id, "body:base_add")
|
|
self.assertFalse(any(record.feature_id == "thickness" for record in execution.session.topology.records()))
|
|
|
|
invalid = deepcopy(cdsl)
|
|
invalid["features"][-1]["params"]["value"] = math.nan
|
|
analysis = analyze_cdsl(invalid)
|
|
assignment = next(item for item in analysis.feature_results if item.feature_id == "thickness")
|
|
self.assertIn("invalid_assign_variable", [item.code for item in assignment.blockers])
|
|
with self.assertRaisesRegex(ValueError, "finite scalar value"):
|
|
validate_semantic_cdsl(invalid)
|
|
|
|
duplicate = deepcopy(cdsl)
|
|
duplicate["features"].append({
|
|
"id": "thickness_again", "atomic_id": "assign_variable", "depends_on": ["thickness"],
|
|
"params": {"name": "thickness", "value": 5.0, "value_kind": "any"},
|
|
"execution_status": "supported",
|
|
})
|
|
with self.assertRaisesRegex(ValueError, "redeclares source variable thickness"):
|
|
validate_semantic_cdsl(duplicate)
|
|
|
|
def test_unused_invalid_profile_does_not_block_runtime_preflight(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "abandoned", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True,
|
|
"segments": [{"type": "line", "start": [0, 0], "end": [1, 0]}],
|
|
}]},
|
|
})
|
|
analysis = analyze_cdsl(cdsl)
|
|
self.assertTrue(analysis.runtime_eligible)
|
|
|
|
def test_used_invalid_profile_is_a_feature_level_blocker(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "broken", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "outer", "closed": True,
|
|
"segments": [{"type": "line", "start": [0, 0], "end": [1, 0]}],
|
|
}]},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "broken_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 1}, "sketch_id": "broken",
|
|
})
|
|
analysis = analyze_cdsl(cdsl)
|
|
broken = next(item for item in analysis.feature_results if item.feature_id == "broken_cut")
|
|
self.assertEqual(broken.resolved_status, "blocked")
|
|
self.assertIn("profile_resolution_failed", [item.code for item in broken.blockers])
|
|
|
|
def test_used_empty_analytic_profile_is_a_feature_level_blocker(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "construction_only", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": []},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "empty_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 1}, "sketch_id": "construction_only",
|
|
})
|
|
analysis = analyze_cdsl(cdsl)
|
|
empty = next(item for item in analysis.feature_results if item.feature_id == "empty_cut")
|
|
self.assertIn("profile_no_closed_region", [item.code for item in empty.blockers])
|
|
|
|
def test_selector_resolver_refuses_equal_candidates(self) -> None:
|
|
registry = TopologyRegistry()
|
|
plane = PlaneSpec.from_mapping(_workplane())
|
|
registry.register(TopologyRecord("first", "plane", "f1", geometry=plane.as_dict(), value=plane))
|
|
registry.register(TopologyRecord("second", "plane", "f2", geometry=plane.as_dict(), value=plane))
|
|
resolution = registry.resolve({"kind": "plane", "geometry": plane.as_dict()})
|
|
self.assertEqual(resolution.status, "ambiguous")
|
|
self.assertEqual(resolution.diagnostic.code, "selector_ambiguous")
|
|
|
|
def test_selector_resolver_prefers_exact_runtime_stable_id(self) -> None:
|
|
registry = TopologyRegistry()
|
|
geometry = {"curve_type": "circle", "center_mm": [0, 0, 0]}
|
|
registry.register(TopologyRecord("body:b:edge:18", "edge", "boss", "body:b", geometry))
|
|
registry.register(TopologyRecord("body:b:edge:20", "edge", "boss", "body:b", geometry))
|
|
|
|
resolution = registry.resolve({
|
|
"kind": "edge",
|
|
"stable_id": "body:b:edge:18",
|
|
"owner_feature_id": "boss",
|
|
"snapshot_id": "cad_test/rev_001",
|
|
"geometry": geometry,
|
|
}, active_body_id="body:b")
|
|
|
|
self.assertEqual(resolution.status, "resolved")
|
|
self.assertEqual(resolution.record.record_id, "body:b:edge:18")
|
|
|
|
def test_snapshot_stable_id_rejects_geometry_that_changed(self) -> None:
|
|
registry = TopologyRegistry()
|
|
registry.register(TopologyRecord(
|
|
"body:b:edge:18", "edge", "boss", "body:b",
|
|
{"curve_type": "circle", "center_mm": [0, 0, 0]},
|
|
))
|
|
resolution = registry.resolve({
|
|
"kind": "edge", "stable_id": "body:b:edge:18", "owner_feature_id": "boss",
|
|
"snapshot_id": "cad_test/rev_001",
|
|
"geometry": {"curve_type": "circle", "center_mm": [1, 0, 0]},
|
|
}, active_body_id="body:b")
|
|
self.assertEqual(resolution.status, "not_found")
|
|
self.assertEqual(resolution.diagnostic.code, "selector_geometry_mismatch")
|
|
|
|
def test_selector_resolver_normalizes_legacy_solidworks_plane_evidence(self) -> None:
|
|
registry = TopologyRegistry()
|
|
registry.register(TopologyRecord(
|
|
"face", "face", "f1", "body:f1",
|
|
geometry={
|
|
"surface_type": "plane", "plane_normal": [1, 0, 0],
|
|
"plane_offset_mm": 12.0, "bbox_mm": [12, -2, -3, 12, 2, 3], "area_mm2": 24,
|
|
},
|
|
))
|
|
resolution = registry.resolve({
|
|
"kind": "face",
|
|
"geometry": {
|
|
"surface": {"type": "plane", "parameters": [-1, 0, 0, -0.012, 0, 0]},
|
|
"box": [0.012, -0.002, -0.003, 0.012, 0.002, 0.003], "area": 0.000024,
|
|
},
|
|
})
|
|
self.assertEqual(resolution.status, "resolved")
|
|
self.assertEqual(resolution.record.record_id, "face")
|
|
|
|
def test_owner_selector_survives_a_later_body_snapshot_when_geometry_is_unchanged(self) -> None:
|
|
registry = TopologyRegistry()
|
|
geometry = {
|
|
"bbox_mm": [0, 0, 0, 0, 0, 10],
|
|
"center_mm": [0, 0, 5],
|
|
"length_mm": 10.0,
|
|
"curve_type": "line",
|
|
"start_mm": [0, 0, 0],
|
|
"end_mm": [0, 0, 10],
|
|
}
|
|
registry.replace_body_topology("base_add", "body:base_add", [
|
|
TopologyRecord("body:base_add:edge:0", "edge", "base_add", "body:base_add", geometry, "old-edge"),
|
|
])
|
|
registry.replace_body_topology("later_cut", "body:later_cut", [
|
|
TopologyRecord("body:later_cut:edge:3", "edge", "later_cut", "body:later_cut", geometry, "current-edge"),
|
|
])
|
|
|
|
resolution = registry.resolve(
|
|
{"kind": "edge", "owner_feature_id": "base_add", "geometry": geometry},
|
|
active_body_id="body:later_cut",
|
|
)
|
|
self.assertEqual(resolution.status, "resolved")
|
|
self.assertEqual(resolution.record.value, "current-edge")
|
|
self.assertEqual(resolution.record.feature_id, "later_cut")
|
|
self.assertEqual(resolution.record.owner_feature_ids, ("base_add",))
|
|
|
|
def test_ambiguous_predecessors_do_not_invent_topology_ownership(self) -> None:
|
|
registry = TopologyRegistry()
|
|
geometry = {"center_mm": [1, 2, 3]}
|
|
registry.replace_body_topology("base_add", "body:base_add", [
|
|
TopologyRecord("body:base_add:vertex:0", "vertex", "base_add", "body:base_add", geometry),
|
|
TopologyRecord("body:base_add:vertex:1", "vertex", "base_add", "body:base_add", geometry),
|
|
])
|
|
registry.replace_body_topology("later_cut", "body:later_cut", [
|
|
TopologyRecord("body:later_cut:vertex:0", "vertex", "later_cut", "body:later_cut", geometry),
|
|
])
|
|
|
|
resolution = registry.resolve(
|
|
{"kind": "vertex", "owner_feature_id": "base_add", "geometry": geometry},
|
|
active_body_id="body:later_cut",
|
|
)
|
|
self.assertEqual(resolution.status, "not_found")
|
|
|
|
def test_changed_adjacency_prevents_owner_provenance_transfer(self) -> None:
|
|
registry = TopologyRegistry()
|
|
unchanged_geometry = {
|
|
"bbox_mm": [0, 0, 0, 1, 1, 0], "center_mm": [0.5, 0.5, 0],
|
|
"normal": [0, 0, 1], "area_mm2": 1, "surface_type": "plane",
|
|
}
|
|
registry.replace_body_topology("base_add", "body:base_add", [
|
|
TopologyRecord(
|
|
"body:base_add:face:0", "face", "base_add", "body:base_add",
|
|
{**unchanged_geometry, "adjacency_signature": ["line:1.000000:2"]},
|
|
),
|
|
])
|
|
registry.replace_body_topology("later_cut", "body:later_cut", [
|
|
TopologyRecord(
|
|
"body:later_cut:face:0", "face", "later_cut", "body:later_cut",
|
|
{**unchanged_geometry, "adjacency_signature": ["line:1.000000:2", "circle:1.000000:1"]},
|
|
),
|
|
])
|
|
|
|
resolution = registry.resolve(
|
|
{"kind": "face", "owner_feature_id": "base_add", "geometry": unchanged_geometry},
|
|
active_body_id="body:later_cut",
|
|
)
|
|
self.assertEqual(resolution.status, "not_found")
|
|
|
|
def test_batch_analysis_writes_one_report_per_input(self) -> None:
|
|
document = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "batch-part",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{"id": "s", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 1}}]},
|
|
"features": [{
|
|
"id": "f", "atomic_id": "reference_plane", "depends_on": [], "execution_status": "deferred",
|
|
"params": {"plane": _workplane()},
|
|
}],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
input_path = root / "input"
|
|
input_path.mkdir()
|
|
(input_path / "batch-part.cdsl.json").write_text(json.dumps(document), encoding="utf-8")
|
|
manifest = batch_analyze(input_path, root / "out", atomic_ids=frozenset({"reference_plane"}))
|
|
report = json.loads((root / "out" / "parts" / "batch-part.report.json").read_text(encoding="utf-8"))
|
|
self.assertEqual(manifest["part_count"], 1)
|
|
self.assertTrue(report["semantic_valid"])
|
|
self.assertFalse(report["runtime_eligible"])
|
|
self.assertEqual(report["first_blocker"]["code"], "no_solid_feature")
|
|
|
|
def test_batch_analysis_part_filter_is_exact_and_rejects_unknown_ids(self) -> None:
|
|
document = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
|
"meta": {"unit": "mm"}, "geometry": {"sketches": []}, "features": [],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
input_path = root / "input"
|
|
input_path.mkdir()
|
|
for part_id in ("first", "second"):
|
|
(input_path / f"{part_id}.cdsl.json").write_text(
|
|
json.dumps({**document, "part_id": part_id}), encoding="utf-8",
|
|
)
|
|
manifest = batch_analyze(
|
|
input_path, root / "out", atomic_ids=frozenset({"reference_plane"}), part_ids=["second"],
|
|
)
|
|
self.assertEqual(manifest["part_count"], 1)
|
|
self.assertEqual(manifest["results"], [{"part_id": "second", "report": "parts/second.report.json"}])
|
|
with self.assertRaisesRegex(ValueError, "do not exist"):
|
|
batch_analyze(input_path, root / "bad", part_ids=["missing"])
|
|
|
|
def test_batch_resume_migrates_derived_failure_category_without_rebuilding(self) -> None:
|
|
document = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "cached",
|
|
"meta": {"unit": "mm"}, "geometry": {"sketches": []}, "features": [],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
source = root / "input"
|
|
source.mkdir()
|
|
(source / "cached.cdsl.json").write_text(json.dumps(document), encoding="utf-8")
|
|
report_path = root / "out" / "parts" / "cached.report.json"
|
|
report_path.parent.mkdir(parents=True)
|
|
report_path.write_text(json.dumps({
|
|
"part_id": "cached", "semantic_valid": True, "runtime_eligible": False, "compiled": False,
|
|
"build_attempted": False, "built": False, "geometry_verified": False, "feature_results": [],
|
|
"first_blocker": {"code": "missing_host_face"},
|
|
}), encoding="utf-8")
|
|
manifest = batch_analyze(source, root / "out")
|
|
report = json.loads(report_path.read_text(encoding="utf-8"))
|
|
self.assertEqual(manifest["failure_category_counts"], {"input_incomplete": 1})
|
|
self.assertEqual(report["failure_category"], "input_incomplete")
|
|
|
|
def test_full_export_batch_has_a_machine_readable_report_for_every_input(self) -> None:
|
|
"""Keep the shipped CDSL corpus on the capability-report path.
|
|
|
|
STEP construction is intentionally left to the resumable build job;
|
|
this CI-sized pass proves that every current export gets a semantic
|
|
result and a first blocker rather than being silently skipped.
|
|
"""
|
|
source = ROOT / "json_to_cdsl" / "output"
|
|
selectors = [path.name.removesuffix(".cdsl.json") for path in sorted(source.glob("*.cdsl.json"))]
|
|
corpus = _corpus_manifest(source)
|
|
input_count = len(selectors)
|
|
self.assertGreater(input_count, 0)
|
|
self.assertEqual(input_count, corpus["document_count"])
|
|
self.assertEqual(_selector_digest(selectors), corpus["document_stems_sha256"])
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
out = Path(directory) / "baseline"
|
|
manifest = batch_analyze(source, out)
|
|
reports = [json.loads(path.read_text(encoding="utf-8")) for path in (out / "parts").glob("*.report.json")]
|
|
self.assertEqual(manifest["part_count"], input_count)
|
|
self.assertEqual(manifest["completed_count"], input_count)
|
|
self.assertTrue(manifest["complete"])
|
|
self.assertEqual(manifest["semantic_valid_count"], input_count)
|
|
self.assertEqual(len(reports), input_count)
|
|
self.assertTrue(all(report["runtime_eligible"] or report.get("first_blocker") for report in reports))
|
|
|
|
def test_p3_static_pool_is_explicit_and_reports_missing_capture_separately(self) -> None:
|
|
from cdsl_engine.phase_pools import select_p3_static_pool
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
source = ROOT / "json_to_cdsl" / "output"
|
|
part_ids = select_p3_static_pool(source)
|
|
expected = _corpus_manifest(source)["phase_pools"]["p3"]
|
|
self.assertEqual(len(part_ids), expected["selector_count"])
|
|
self.assertEqual(_selector_digest(part_ids), expected["selectors_sha256"])
|
|
|
|
# Static pool membership is input-based. Runtime preflight remains a
|
|
# separate signal and must retain captured blockers for its members.
|
|
expected_blockers = {
|
|
"027784": {"missing_revolve_axis"},
|
|
"104237": {"missing_extent_reference"},
|
|
"239358": {"missing_extent_reference", "dependency_unavailable"},
|
|
"241720": {"missing_extent_reference"},
|
|
}
|
|
for part_id, blockers in expected_blockers.items():
|
|
self.assertIn(part_id, part_ids)
|
|
cdsl = json.loads((source / f"{part_id}.cdsl.json").read_text(encoding="utf-8"))
|
|
result = analyze_cdsl(cdsl)
|
|
actual = {
|
|
blocker.code
|
|
for feature in result.feature_results
|
|
for blocker in feature.blockers
|
|
}
|
|
self.assertTrue(blockers <= actual)
|
|
|
|
def test_p3_phase_pool_can_be_passed_to_batch_rebuild(self) -> None:
|
|
from cdsl_engine.phase_pools import select_p3_static_pool
|
|
|
|
source = ROOT / "json_to_cdsl" / "output"
|
|
pool = select_p3_static_pool(source)
|
|
self.assertIn("Bearing-Spacer-1", pool)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
manifest = batch_analyze(source, Path(directory) / "p3", part_ids=pool, max_parts=1)
|
|
self.assertEqual(manifest["part_count"], len(pool))
|
|
self.assertEqual(manifest["completed_count"], 1)
|
|
self.assertFalse(manifest["complete"])
|
|
|
|
def test_phase_pool_membership_is_regressed_for_p4_and_p6(self) -> None:
|
|
from cdsl_engine.phase_pools import select_static_phase_pool
|
|
|
|
source = ROOT / "json_to_cdsl" / "output"
|
|
expected_pools = _corpus_manifest(source)["phase_pools"]
|
|
for phase in ("p4", "p6"):
|
|
selectors = select_static_phase_pool(source, phase)
|
|
expected = expected_pools[phase]
|
|
self.assertEqual(len(selectors), expected["selector_count"])
|
|
self.assertEqual(_selector_digest(selectors), expected["selectors_sha256"])
|
|
with self.assertRaisesRegex(ValueError, "Unknown CDSL runtime phase"):
|
|
select_static_phase_pool(source, "p5")
|
|
|
|
def test_construction_bspline_is_auditable_but_not_an_executable_contour(self) -> None:
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["execution_status"] = "supported"
|
|
profile = {
|
|
"type": "analytic_contours",
|
|
"contours": [{
|
|
"role": "outer", "closed": True,
|
|
"segments": [
|
|
{"type": "line", "start": [-5, -5], "end": [5, -5]},
|
|
{"type": "line", "start": [5, -5], "end": [5, 5]},
|
|
{"type": "line", "start": [5, 5], "end": [-5, 5]},
|
|
{"type": "line", "start": [-5, 5], "end": [-5, -5]},
|
|
],
|
|
}],
|
|
"construction": [{
|
|
"type": "bspline", "degree": 2,
|
|
"control_points": [[-5, 0], [0, 2], [5, 0]],
|
|
"knots": [0, 0, 0, 1, 1, 1], "periodic": False,
|
|
}],
|
|
}
|
|
cdsl["geometry"]["sketches"][0]["profile"] = profile
|
|
validate_semantic_cdsl(cdsl)
|
|
resolved = resolve_all_sketches(cdsl)
|
|
entities = resolved["geometry"]["sketches"][0]["entities"]
|
|
self.assertNotIn("bspline", {entity["type"] for entity in entities})
|
|
|
|
profile["contours"][0]["segments"][0] = profile["construction"][0]
|
|
with self.assertRaisesRegex(ValueError, "CDSL schema violation"):
|
|
validate_semantic_cdsl(cdsl)
|
|
|
|
def test_nested_pattern_source_is_replayable_after_its_first_execution(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "first_pattern", "atomic_id": "pattern_linear", "depends_on": ["base_add"],
|
|
"params": {"source_feature_ids": ["base_add"], "direction_1": [1, 0, 0],
|
|
"spacing_1_mm": 20, "pattern_count_1": 2},
|
|
},
|
|
{
|
|
"id": "nested_pattern", "atomic_id": "pattern_linear", "depends_on": ["first_pattern"],
|
|
"params": {"source_feature_ids": ["first_pattern"], "direction_1": [0, 1, 0],
|
|
"spacing_1_mm": 20, "pattern_count_1": 2},
|
|
},
|
|
])
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "nested.step")
|
|
self.assertEqual(result["engine"], "cdsl_session_runtime")
|
|
|
|
def test_pattern_source_without_body_definition_is_blocked_in_preflight(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].insert(0, {
|
|
"id": "context", "atomic_id": "reference_plane", "depends_on": [],
|
|
"params": {"plane": _workplane()},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "pattern", "atomic_id": "pattern_linear", "depends_on": ["base_add", "context"],
|
|
"params": {"source_feature_ids": ["context"], "direction_1": [1, 0, 0],
|
|
"spacing_1_mm": 20, "pattern_count_1": 2},
|
|
})
|
|
analysis = analyze_cdsl(cdsl)
|
|
pattern = next(result for result in analysis.feature_results if result.feature_id == "pattern")
|
|
self.assertEqual(pattern.resolved_status, "unsupported")
|
|
self.assertIn("unsupported_pattern_source", [blocker.code for blocker in pattern.blockers])
|
|
|
|
def test_truth_comparison_keeps_frame_mismatch_distinct_from_verified_geometry(self) -> None:
|
|
truth = {"bounding_box_mm": [-10, -200, -10, 10, 200, 10], "solid_count": 1}
|
|
common = {
|
|
"truth": truth,
|
|
"volume_relative_error": 1e-8,
|
|
"area_relative_error": 1e-8,
|
|
"solid_count_matches": True,
|
|
}
|
|
self.assertEqual(
|
|
_verification_classification(
|
|
actual_box=[-10, 990, 0, 10, 1010, 400], box_delta=1190, **common,
|
|
),
|
|
"coordinate_frame_mismatch_candidate",
|
|
)
|
|
self.assertEqual(
|
|
_verification_classification(
|
|
actual_box=[-10, -200, -10, 10, 200, 10], box_delta=0, **common,
|
|
),
|
|
"verified",
|
|
)
|
|
self.assertEqual(
|
|
_verification_classification(
|
|
actual_box=[-10, 990, 0, 10, 1010, 400], box_delta=1190,
|
|
truth=truth, volume_relative_error=0.1, area_relative_error=1e-8, solid_count_matches=True,
|
|
),
|
|
"geometry_mismatch",
|
|
)
|
|
|
|
def test_batch_failure_categories_preserve_input_selector_capability_and_occ_boundaries(self) -> None:
|
|
self.assertEqual(_failure_category({"geometry_verified": True}), "geometry_verified")
|
|
self.assertEqual(_failure_category({"runtime_eligible": True, "build_attempted": False}), "runtime_eligible_not_built")
|
|
self.assertEqual(_failure_category({"built": True, "numeric_comparison": {"classification": "coordinate_frame_mismatch_candidate"}}), "coordinate_frame_mismatch_candidate")
|
|
self.assertEqual(_failure_category({"built": False, "first_blocker": {"code": "missing_host_face"}}), "input_incomplete")
|
|
self.assertEqual(_failure_category({"built": False, "first_blocker": {"code": "extent_target_not_reached"}}), "input_incomplete")
|
|
self.assertEqual(_failure_category({"built": False, "first_blocker": {"code": "selector_ambiguous"}}), "selector_resolution")
|
|
self.assertEqual(_failure_category({"built": False, "first_blocker": {"code": "unsupported_hole_subtype"}}), "unsupported_capability")
|
|
self.assertEqual(_failure_category({"built": False, "first_blocker": {"code": "build_timeout"}}), "occ_execution_failure")
|
|
|
|
def test_mirror_pattern_replays_a_selector_free_cut(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "mirror-test",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "base", "workplane": _workplane(), "profile": _rectangle([-5, -5], [5, 5])},
|
|
{"id": "cut", "workplane": _workplane(), "profile": {"type": "circle", "center": [2, 0], "radius_mm": 1}},
|
|
]},
|
|
"features": [
|
|
{"id": "ref", "atomic_id": "reference_plane", "depends_on": [], "params": {"plane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]}}},
|
|
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": ["ref"], "params": {"distance_mm": 2}, "sketch_id": "base"},
|
|
{"id": "cut_1", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "params": {"distance_mm": 2}, "sketch_id": "cut"},
|
|
{
|
|
"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["cut_1", "ref"],
|
|
"params": {"source_feature_ids": ["cut_1"], "mirror_plane": {"kind": "plane", "stable_id": "source", "source": "solidworks", "confidence": 1, "owner_feature_id": "ref"}},
|
|
"selectors": [{"kind": "plane", "stable_id": "source", "source": "solidworks", "confidence": 1, "owner_feature_id": "ref"}],
|
|
},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "mirror.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 200 - 4 * 3.141592653589793, places=5)
|
|
self.assertIn("mirror", [item["feature_id"] for item in result["feature_results"]])
|
|
|
|
def test_circular_pattern_fuses_face_sharing_new_body_instances(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "circular-fuse",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "wedge", "workplane": _workplane(),
|
|
"profile": {"type": "polygon", "vertices": [[0, 0], [10, 0], [0, 10]]},
|
|
}]},
|
|
"features": [
|
|
{"id": "wedge_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"params": {"distance_mm": 10, "result_mode": "new_body"}, "sketch_id": "wedge"},
|
|
{"id": "wedge_pattern", "atomic_id": "pattern_circular", "depends_on": ["wedge_add"], "params": {
|
|
"source_feature_ids": ["wedge_add"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
|
"pattern_count": 4, "sweep_angle_deg": 360,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "circular-fuse.step")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], 2000.0, places=5)
|
|
copy_owners = {
|
|
owner
|
|
for record in result["topology_records"]
|
|
for owner in record.get("owner_feature_ids") or ()
|
|
if owner.startswith("wedge_pattern.c")
|
|
}
|
|
self.assertEqual(
|
|
copy_owners,
|
|
{
|
|
"wedge_pattern.c1.wedge_add",
|
|
"wedge_pattern.c2.wedge_add",
|
|
"wedge_pattern.c3.wedge_add",
|
|
},
|
|
)
|
|
|
|
def test_circular_pattern_skips_explicitly_deleted_copy_instances(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "circular-delete-copy",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "wedge", "workplane": _workplane(),
|
|
"profile": {"type": "polygon", "vertices": [[0, 0], [10, 0], [0, 10]]},
|
|
}]},
|
|
"features": [
|
|
{"id": "wedge_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"params": {"distance_mm": 10, "result_mode": "new_body"}, "sketch_id": "wedge"},
|
|
{"id": "wedge_pattern", "atomic_id": "pattern_circular", "depends_on": ["wedge_add"], "params": {
|
|
"source_feature_ids": ["wedge_add"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
|
"pattern_count": 4, "sweep_angle_deg": 360, "excluded_instance_indices": [2],
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "circular-delete-copy.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1500.0, places=5)
|
|
|
|
def test_transform_can_move_one_proven_circular_pattern_copy(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "circular-copy-transform",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "boss", "workplane": _workplane(), "profile": {"type": "circle", "center": [10, 0], "radius_mm": 1},
|
|
}]},
|
|
"features": [
|
|
{"id": "boss_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "boss", "params": {"distance_mm": 2, "result_mode": "new_body"}},
|
|
{"id": "boss_pattern", "atomic_id": "pattern_circular", "depends_on": ["boss_add"], "params": {
|
|
"source_feature_ids": ["boss_add"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
|
"pattern_count": 4, "sweep_angle_deg": 360,
|
|
}},
|
|
{"id": "move_copy", "atomic_id": "transform_bodies", "depends_on": ["boss_pattern"], "params": {
|
|
"pattern_instance_refs": [{"pattern_feature_id": "boss_pattern", "source_feature_id": "boss_add", "instance_index": 1}],
|
|
"transform": {"type": "translation", "translation_mm": [40, 0, 0]}, "make_copy": False,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "circular-copy-transform.step")
|
|
|
|
self.assertEqual(result["solid_count"], 4)
|
|
self.assertAlmostEqual(result["volume_mm3"], 4 * math.pi * 2, places=5)
|
|
self.assertEqual(result["bbox_mm"]["max"], [41.0, 11.0, 2.0])
|
|
self.assertEqual([item["feature_id"] for item in result["feature_results"]], ["boss_add", "boss_pattern", "move_copy"])
|
|
|
|
def test_boolean_can_union_copies_of_a_proven_fused_body_successor(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "fused-body-pattern-boolean",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "base", "workplane": _workplane(), "profile": {"type": "circle", "center": [10, 0], "radius_mm": 2}},
|
|
{"id": "boss", "workplane": _workplane(), "profile": {"type": "circle", "center": [12, 0], "radius_mm": 1.5}},
|
|
]},
|
|
"features": [
|
|
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "base", "params": {"distance_mm": 2, "result_mode": "new_body"}},
|
|
{"id": "boss_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"], "sketch_id": "boss", "params": {"distance_mm": 2}},
|
|
{"id": "boss_pattern", "atomic_id": "pattern_circular", "depends_on": ["boss_add"], "params": {
|
|
"source_feature_ids": ["boss_add"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
|
"pattern_count": 3, "sweep_angle_deg": 360,
|
|
}},
|
|
{"id": "join", "atomic_id": "boolean_bodies", "depends_on": ["boss_pattern"], "params": {
|
|
"operation": "union", "target_feature_ids": ["boss_add"], "tool_pattern_instance_refs": [
|
|
{"pattern_feature_id": "boss_pattern", "source_feature_id": "boss_add", "instance_index": 1},
|
|
{"pattern_feature_id": "boss_pattern", "source_feature_id": "boss_add", "instance_index": 2},
|
|
], "keep_tools": False,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "fused-body-pattern-boolean.step")
|
|
|
|
self.assertEqual(result["solid_count"], 3)
|
|
self.assertEqual([item["feature_id"] for item in result["feature_results"]], [
|
|
"base_add", "boss_add", "boss_pattern", "join",
|
|
])
|
|
|
|
def test_transform_can_move_one_proven_mirror_pattern_copy(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "mirror-copy-transform",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "boss", "workplane": _workplane(), "profile": {"type": "circle", "center": [10, 0], "radius_mm": 1},
|
|
}]},
|
|
"features": [
|
|
{"id": "mirror_plane", "atomic_id": "reference_plane", "depends_on": [], "params": {
|
|
"plane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]},
|
|
}},
|
|
{"id": "boss_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "boss", "params": {
|
|
"distance_mm": 2, "result_mode": "new_body",
|
|
}},
|
|
{"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["boss_add", "mirror_plane"], "params": {
|
|
"source_feature_ids": ["boss_add"], "mirror_current_body": True,
|
|
"mirror_plane": {"kind": "plane", "owner_feature_id": "mirror_plane"},
|
|
}, "selectors": [{"kind": "plane", "owner_feature_id": "mirror_plane"}]},
|
|
{"id": "move_copy", "atomic_id": "transform_bodies", "depends_on": ["mirror"], "params": {
|
|
"pattern_instance_refs": [{"pattern_feature_id": "mirror", "source_feature_id": "boss_add", "instance_index": 1}],
|
|
"transform": {"type": "translation", "translation_mm": [0, 20, 0]}, "make_copy": False,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "mirror-copy-transform.step")
|
|
|
|
self.assertEqual(result["solid_count"], 2)
|
|
self.assertAlmostEqual(result["volume_mm3"], 4 * math.pi, places=5)
|
|
self.assertEqual(result["bbox_mm"], {"min": [-11.0, -1.0, 0.0], "max": [11.0, 21.0, 2.0]})
|
|
self.assertEqual([item["feature_id"] for item in result["feature_results"]], [
|
|
"mirror_plane", "boss_add", "mirror", "move_copy",
|
|
])
|
|
|
|
def test_boolean_can_union_one_proven_mirror_pattern_copy(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "mirror-copy-boolean",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "boss", "workplane": _workplane(), "profile": {"type": "circle", "center": [10, 0], "radius_mm": 1},
|
|
}]},
|
|
"features": [
|
|
{"id": "mirror_plane", "atomic_id": "reference_plane", "depends_on": [], "params": {
|
|
"plane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]},
|
|
}},
|
|
{"id": "boss_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "boss", "params": {
|
|
"distance_mm": 2, "result_mode": "new_body",
|
|
}},
|
|
{"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["boss_add", "mirror_plane"], "params": {
|
|
"source_feature_ids": ["boss_add"], "mirror_current_body": True,
|
|
"mirror_plane": {"kind": "plane", "owner_feature_id": "mirror_plane"},
|
|
}, "selectors": [{"kind": "plane", "owner_feature_id": "mirror_plane"}]},
|
|
{"id": "join", "atomic_id": "boolean_bodies", "depends_on": ["mirror"], "params": {
|
|
"operation": "union", "target_feature_ids": ["boss_add"],
|
|
"tool_pattern_instance_refs": [{
|
|
"pattern_feature_id": "mirror", "source_feature_id": "boss_add", "instance_index": 1,
|
|
}], "keep_tools": False,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "mirror-copy-boolean.step")
|
|
|
|
self.assertEqual(result["solid_count"], 2)
|
|
self.assertAlmostEqual(result["volume_mm3"], 4 * math.pi, places=5)
|
|
self.assertEqual(result["bbox_mm"], {"min": [-11.0, -1.0, 0.0], "max": [11.0, 1.0, 2.0]})
|
|
self.assertEqual([item["feature_id"] for item in result["feature_results"]], [
|
|
"mirror_plane", "boss_add", "mirror", "join",
|
|
])
|
|
|
|
def test_boolean_pattern_instance_requires_a_surviving_member(self) -> None:
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["params"]["result_mode"] = "new_body"
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "mirror_plane", "atomic_id": "reference_plane", "depends_on": [], "params": {
|
|
"plane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]},
|
|
},
|
|
},
|
|
{
|
|
"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["base_add", "mirror_plane"], "params": {
|
|
"source_feature_ids": ["base_add"], "mirror_plane": {
|
|
"kind": "plane", "owner_feature_id": "mirror_plane", "stable_id": "mirror-plane",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
},
|
|
}, "selectors": [{
|
|
"kind": "plane", "owner_feature_id": "mirror_plane", "stable_id": "mirror-plane",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
}],
|
|
},
|
|
{
|
|
"id": "join", "atomic_id": "boolean_bodies", "depends_on": ["mirror"], "params": {
|
|
"operation": "union", "target_feature_ids": ["base_add"],
|
|
"tool_pattern_instance_refs": [{
|
|
"pattern_feature_id": "mirror", "source_feature_id": "base_add", "instance_index": 2,
|
|
}], "keep_tools": False,
|
|
},
|
|
},
|
|
])
|
|
for feature in cdsl["features"]:
|
|
feature["execution_status"] = "supported"
|
|
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
with self.assertRaisesRegex(ValueError, "not a surviving copy"):
|
|
validate_semantic_cdsl(cdsl)
|
|
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids={"extrude_add_blind", "reference_plane", "pattern_mirror", "boolean_bodies"},
|
|
profile_types=SHAPE_GENERATORS,
|
|
).analyze(cdsl)
|
|
blocker = next(item for item in analysis.feature_results[-1].blockers if item.code == "pattern_instance_unavailable")
|
|
self.assertEqual(blocker.detail, {
|
|
"pattern_feature_id": "mirror", "source_feature_id": "base_add", "instance_index": 2,
|
|
})
|
|
|
|
def test_mirror_pattern_instance_reference_rejects_any_instance_but_one(self) -> None:
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["params"]["result_mode"] = "new_body"
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "mirror_plane", "atomic_id": "reference_plane", "depends_on": [], "params": {
|
|
"plane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]},
|
|
},
|
|
},
|
|
{
|
|
"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["base_add", "mirror_plane"], "params": {
|
|
"source_feature_ids": ["base_add"], "mirror_plane": {
|
|
"kind": "plane", "owner_feature_id": "mirror_plane", "stable_id": "mirror-plane",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
},
|
|
}, "selectors": [{
|
|
"kind": "plane", "owner_feature_id": "mirror_plane", "stable_id": "mirror-plane",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
}],
|
|
},
|
|
{
|
|
"id": "move", "atomic_id": "transform_bodies", "depends_on": ["mirror"], "params": {
|
|
"pattern_instance_refs": [{"pattern_feature_id": "mirror", "source_feature_id": "base_add", "instance_index": 2}],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]}, "make_copy": False,
|
|
},
|
|
},
|
|
])
|
|
for feature in cdsl["features"]:
|
|
feature["execution_status"] = "supported"
|
|
with self.assertRaisesRegex(ValueError, "not a surviving copy"):
|
|
validate_semantic_cdsl(cdsl)
|
|
|
|
def test_mirror_pattern_instance_reference_requires_a_new_body_source(self) -> None:
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "mirror_plane", "atomic_id": "reference_plane", "depends_on": [], "params": {
|
|
"plane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]},
|
|
},
|
|
},
|
|
{
|
|
"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["base_add", "mirror_plane"], "params": {
|
|
"source_feature_ids": ["base_add"], "mirror_plane": {
|
|
"kind": "plane", "owner_feature_id": "mirror_plane", "stable_id": "mirror-plane",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
},
|
|
}, "selectors": [{
|
|
"kind": "plane", "owner_feature_id": "mirror_plane", "stable_id": "mirror-plane",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
}],
|
|
},
|
|
{
|
|
"id": "move", "atomic_id": "transform_bodies", "depends_on": ["mirror"], "params": {
|
|
"pattern_instance_refs": [{"pattern_feature_id": "mirror", "source_feature_id": "base_add", "instance_index": 1}],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]}, "make_copy": False,
|
|
},
|
|
},
|
|
])
|
|
for feature in cdsl["features"]:
|
|
feature["execution_status"] = "supported"
|
|
with self.assertRaisesRegex(ValueError, "requires a preceding new_body mirror source"):
|
|
validate_semantic_cdsl(cdsl)
|
|
|
|
def test_pattern_instance_reference_rejects_an_excluded_copy(self) -> None:
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["params"]["result_mode"] = "new_body"
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "pattern", "atomic_id": "pattern_circular", "depends_on": ["base_add"],
|
|
"params": {
|
|
"source_feature_ids": ["base_add"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
|
"pattern_count": 3, "sweep_angle_deg": 360, "excluded_instance_indices": [1],
|
|
}, "execution_status": "supported",
|
|
},
|
|
{
|
|
"id": "move", "atomic_id": "transform_bodies", "depends_on": ["pattern"],
|
|
"params": {
|
|
"pattern_instance_refs": [{"pattern_feature_id": "pattern", "source_feature_id": "base_add", "instance_index": 1}],
|
|
"transform": {"type": "translation", "translation_mm": [1, 0, 0]}, "make_copy": False,
|
|
}, "execution_status": "supported",
|
|
},
|
|
])
|
|
cdsl["features"][0]["execution_status"] = "supported"
|
|
from cdsl_engine.semantic_validation import validate_semantic_cdsl
|
|
|
|
with self.assertRaisesRegex(ValueError, "not a surviving copy"):
|
|
validate_semantic_cdsl(cdsl)
|
|
|
|
def test_mirror_pattern_can_union_the_active_body(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "mirror-active-body",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "base", "workplane": _workplane(), "profile": _rectangle([-5, -5], [5, 5])},
|
|
{"id": "half_cut", "workplane": _workplane(), "profile": _rectangle([-5, 0], [5, 5])},
|
|
{"id": "hole", "workplane": _workplane(), "profile": {"type": "circle", "center": [0, -2], "radius_mm": 1}},
|
|
]},
|
|
"features": [
|
|
{"id": "mirror_plane", "atomic_id": "reference_plane", "depends_on": [], "params": {"plane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 1, 0]}}, "execution_status": "supported"},
|
|
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": ["mirror_plane"], "params": {"distance_mm": 2}, "sketch_id": "base", "execution_status": "supported"},
|
|
{"id": "half_remove", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "params": {"distance_mm": 2}, "sketch_id": "half_cut", "execution_status": "supported"},
|
|
{"id": "hole_remove", "atomic_id": "extrude_cut_blind", "depends_on": ["half_remove"], "params": {"distance_mm": 2}, "sketch_id": "hole", "execution_status": "supported"},
|
|
{
|
|
"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["hole_remove", "mirror_plane"],
|
|
"params": {"source_feature_ids": ["base_add"], "mirror_current_body": True, "mirror_plane": {"kind": "plane", "owner_feature_id": "mirror_plane"}},
|
|
"selectors": [{"kind": "plane", "owner_feature_id": "mirror_plane"}], "execution_status": "supported",
|
|
},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "mirror-active-body.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 200 - 4 * 3.141592653589793, places=5)
|
|
self.assertEqual(result["solid_count"], 1)
|
|
|
|
def test_mirrored_local_circle_preserves_its_reflected_world_position(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
from cdsl_engine.runtime import _mirrored_sketch
|
|
from cdsl_engine.sketch_solver import resolve_all_sketches
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"geometry": {"sketches": [{
|
|
"id": "circles", "workplane": _workplane(),
|
|
"profile": {"type": "circle", "center": [2, 3], "radius_mm": 1},
|
|
}]},
|
|
"features": [],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
mirror_plane = PlaneSpec.from_mapping({
|
|
"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0],
|
|
})
|
|
mirrored = _mirrored_sketch(sketch, mirror_plane)
|
|
face = Build123dGeometryAdapter().faces_for_sketch(mirrored)[0]
|
|
center = face.center()
|
|
self.assertAlmostEqual(center.X, -2.0)
|
|
self.assertAlmostEqual(center.Y, 3.0)
|
|
self.assertAlmostEqual(center.Z, 0.0)
|
|
|
|
def test_circle_profile_retains_one_topological_circle_edge(self) -> None:
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"geometry": {"sketches": [{
|
|
"id": "circle", "workplane": _workplane(),
|
|
"profile": {"type": "circle", "center": [2, 3], "radius_mm": 1},
|
|
}]},
|
|
"features": [],
|
|
}
|
|
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
|
self.assertNotIn("contour_edges_mm", sketch)
|
|
face = Build123dGeometryAdapter().faces_for_sketch(sketch)[0]
|
|
self.assertEqual(len(face.outer_wire().edges()), 1)
|
|
|
|
def test_new_body_mode_preserves_an_overlapping_result_body(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "overlap", "workplane": _workplane(),
|
|
"profile": _rectangle([-2, -5], [8, 5]),
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "second_body", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 10, "result_mode": "new_body"}, "sketch_id": "overlap",
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "two-bodies.step")
|
|
self.assertEqual(result["solid_count"], 2)
|
|
|
|
def test_sweep_add_builds_a_solid_from_a_closed_profile_and_bspline_path(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
profile_plane = {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 1, 0]}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "sweep-add",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "profile", "workplane": profile_plane,
|
|
"profile": {"type": "circle", "radius_mm": 2},
|
|
}]},
|
|
"features": [{
|
|
"id": "sweep", "atomic_id": "sweep_add", "depends_on": [], "sketch_id": "profile",
|
|
"params": {"path": {
|
|
"workplane": _workplane(),
|
|
"segment": {
|
|
"type": "bspline", "start": [0, 0], "end": [0, 20],
|
|
"points": [[0, 0], [0, 10], [0, 20]],
|
|
"start_tangent": [0, 10], "end_tangent": [0, 10],
|
|
},
|
|
}},
|
|
}],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
result = rebuild_cdsl(cdsl, root / "sweep.step")
|
|
shelled = deepcopy(cdsl)
|
|
shelled["features"].append({
|
|
"id": "shell", "atomic_id": "shell", "depends_on": ["sweep"],
|
|
"params": {"thickness_mm": 0.25, "inward": True},
|
|
"selectors": [{
|
|
"kind": "face", "owner_feature_id": "sweep", "output_role": "sweep.end",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
}],
|
|
})
|
|
downstream = rebuild_cdsl(shelled, root / "sweep-shell.step")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], 80 * math.pi, delta=1e-4)
|
|
delta = next(item for item in result["topology_deltas"] if item["operation"] == "sweep")
|
|
self.assertEqual(
|
|
{item.get("output_role") for item in delta["relations"] if item.get("output_role") is not None},
|
|
{"sweep.start", "sweep.end"},
|
|
)
|
|
self.assertTrue(any(
|
|
item.get("source_kind") == "edge"
|
|
and item.get("result_kind") == "face"
|
|
and item.get("derivation") == "boundary"
|
|
and item.get("coverage") == "complete"
|
|
and item.get("status") == "recorded_without_owner_transfer"
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(all(
|
|
item["event"] == "generated" and item["status"] == "recorded_without_owner_transfer"
|
|
and item["result_record_ids"]
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item["feature_id"] == "sweep" and item.get("output_roles") == ["sweep.end"]
|
|
for item in result["topology_records"]
|
|
))
|
|
self.assertEqual(downstream["runtime_diagnostics"], [])
|
|
self.assertTrue(any(
|
|
item["feature_id"] == "shell" and item["status"] == "resolved"
|
|
and item["selected"].get("output_roles") == ["sweep.end"]
|
|
for item in downstream["selector_resolution"]
|
|
))
|
|
|
|
def test_sweep_add_builds_a_solid_from_a_directed_arc_path(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl, rebuild_cdsl
|
|
|
|
profile_plane = {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "arc-sweep",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "profile", "workplane": profile_plane,
|
|
"profile": {"type": "circle", "radius_mm": 2},
|
|
}]},
|
|
"features": [{
|
|
"id": "sweep", "atomic_id": "sweep_add", "depends_on": [], "sketch_id": "profile",
|
|
"params": {"path": {"workplane": _workplane(), "segment": {
|
|
"type": "arc", "start": [0, 0], "end": [10, 10], "center": [0, 10],
|
|
"radius_mm": 10, "clockwise": True,
|
|
}}},
|
|
}],
|
|
}
|
|
self.assertTrue(analyze_cdsl(cdsl).runtime_eligible)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "arc-sweep.step")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertGreater(result["volume_mm3"], 0)
|
|
|
|
invalid = deepcopy(cdsl)
|
|
invalid["features"][0]["params"]["path"]["segment"].pop("clockwise")
|
|
blocker = analyze_cdsl(invalid).feature_results[0].blockers[0]
|
|
self.assertEqual(blocker.code, "invalid_sweep_path")
|
|
|
|
def test_sweep_cut_uses_a_transient_tool_and_preserves_target_cut_history(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl, rebuild_cdsl
|
|
|
|
profile_plane = {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 1, 0]}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "sweep-cut",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "base", "workplane": _workplane(), "profile": _rectangle([-5, -5], [5, 5])},
|
|
{"id": "tool", "workplane": profile_plane, "profile": {"type": "circle", "radius_mm": 1}},
|
|
]},
|
|
"features": [
|
|
{"id": "base", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "base",
|
|
"params": {"distance_mm": 10}},
|
|
{"id": "cut", "atomic_id": "sweep_cut", "depends_on": ["base"], "sketch_id": "tool",
|
|
"params": {"path": {"workplane": _workplane(), "segment": {"type": "line", "start": [0, 0], "end": [0, 20]}}}},
|
|
],
|
|
}
|
|
self.assertTrue(analyze_cdsl(cdsl).runtime_eligible)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "sweep-cut.step")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertLess(result["volume_mm3"], 1000)
|
|
delta = next(item for item in result["topology_deltas"] if item["feature_id"] == "cut")
|
|
self.assertEqual(delta["operation"], "subtract")
|
|
self.assertTrue(delta["relations"])
|
|
self.assertFalse(any(item["record_id"].startswith("transient:cut") for item in result["topology_records"]))
|
|
|
|
def test_sweep_add_builds_a_solid_from_two_point_bspline_with_endpoint_tangents(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl, rebuild_cdsl
|
|
|
|
profile_plane = {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 1, 0]}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "two-point-sweep",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "profile", "workplane": profile_plane,
|
|
"profile": {"type": "circle", "radius_mm": 2},
|
|
}]},
|
|
"features": [{
|
|
"id": "sweep", "atomic_id": "sweep_add", "depends_on": [], "sketch_id": "profile",
|
|
"params": {"path": {
|
|
"workplane": _workplane(),
|
|
"segment": {
|
|
"type": "bspline", "start": [0, 0], "end": [6, 10],
|
|
"points": [[0, 0], [6, 10]], "parameters": [0, 1],
|
|
"start_tangent": [0, 10], "end_tangent": [10, 0],
|
|
},
|
|
}},
|
|
}],
|
|
}
|
|
self.assertTrue(analyze_cdsl(cdsl).runtime_eligible)
|
|
missing_tangent = deepcopy(cdsl)
|
|
missing_tangent["features"][0]["params"]["path"]["segment"].pop("end_tangent")
|
|
blocker = analyze_cdsl(missing_tangent).feature_results[0].blockers[0]
|
|
self.assertEqual(blocker.code, "invalid_sweep_path")
|
|
self.assertEqual(blocker.message, "A two-point B-spline sweep path requires both endpoint tangents")
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "two-point-sweep.step")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertGreater(result["volume_mm3"], 0)
|
|
|
|
def test_sweep_add_builds_a_solid_from_spatial_segmented_path(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl, rebuild_cdsl
|
|
|
|
profile_plane = {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]}
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "spatial-sweep",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "profile", "workplane": profile_plane,
|
|
"profile": {"type": "circle", "radius_mm": 2},
|
|
}]},
|
|
"features": [{
|
|
"id": "sweep", "atomic_id": "sweep_add", "depends_on": [], "sketch_id": "profile",
|
|
"params": {"path": {"segments": [
|
|
{
|
|
"type": "line", "start_mm": [0, 0, 0], "end_mm": [0, 0, 10],
|
|
"source_sketch_id": "F1", "source_entity_id": "E0",
|
|
},
|
|
{
|
|
"type": "line", "start_mm": [0, 0, 10], "end_mm": [10, 0, 10],
|
|
"source_sketch_id": "F2", "source_entity_id": "E1",
|
|
},
|
|
]}},
|
|
}],
|
|
}
|
|
self.assertTrue(analyze_cdsl(cdsl).runtime_eligible)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "spatial-sweep.step")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertGreater(result["volume_mm3"], 0)
|
|
|
|
duplicate_source = deepcopy(cdsl)
|
|
duplicate_source["features"][0]["params"]["path"]["segments"][1]["source_sketch_id"] = "F1"
|
|
duplicate_source["features"][0]["params"]["path"]["segments"][1]["source_entity_id"] = "E0"
|
|
blocker = analyze_cdsl(duplicate_source).feature_results[0].blockers[0]
|
|
self.assertEqual(blocker.code, "invalid_sweep_path")
|
|
self.assertEqual(blocker.message, "Sweep spatial path must not repeat one source sketch entity")
|
|
|
|
def test_sweep_history_falls_back_for_hollow_profiles(self) -> None:
|
|
from build123d import Edge, Face, Plane, Vector, Wire
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
adapter = Build123dGeometryAdapter()
|
|
outer = Face(Wire.make_circle(4, Plane.XZ))
|
|
inner = Face(Wire.make_circle(2, Plane.XZ))
|
|
profile = adapter.face_with_holes(outer, [inner])
|
|
result, delta = adapter.sweep_with_topology_delta(
|
|
profile, Edge.make_line(Vector(0, 0, 0), Vector(0, 10, 0)),
|
|
)
|
|
|
|
self.assertIsNone(delta)
|
|
self.assertAlmostEqual(float(result.volume), 120 * math.pi, places=5)
|
|
|
|
def test_sweep_native_fallback_translates_empty_kernel_assertion(self) -> None:
|
|
from build123d import Face, Plane, Wire
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
adapter = Build123dGeometryAdapter()
|
|
outer = Face(Wire.make_circle(28.03, Plane.XY))
|
|
inner = Face(Wire.make_circle(24.77, Plane.XY))
|
|
profile = adapter.face_with_holes(outer, [inner])
|
|
path = adapter.sweep_path_segments([
|
|
{"type": "line", "start_mm": [0, 0, 0], "end_mm": [0, 0, 55.85]},
|
|
{"type": "line", "start_mm": [0, 0, 55.85], "end_mm": [63.5, 0, 55.85]},
|
|
])
|
|
|
|
with self.assertRaisesRegex(ValueError, "OCC sweep operation raised while building the native sweep"):
|
|
adapter.sweep_with_topology_delta(profile, path)
|
|
|
|
def test_sweep_history_matches_native_builder_for_a_curved_path(self) -> None:
|
|
from build123d import Edge, Face, Plane, Vector, Wire
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
adapter = Build123dGeometryAdapter()
|
|
profile = Face(Wire.make_circle(2, Plane.XZ))
|
|
path = Edge.make_spline([
|
|
Vector(0, 0, 0), Vector(0, 10, 0), Vector(5, 20, 0),
|
|
], scale=False)
|
|
direct, delta = adapter.sweep_with_topology_delta(profile, path)
|
|
native = adapter._sweep_without_topology_delta(profile, path)
|
|
|
|
self.assertIsNotNone(delta)
|
|
self.assertTrue(direct.is_valid)
|
|
self.assertAlmostEqual(float(direct.volume), float(native.volume), places=5)
|
|
self.assertEqual(len(direct.faces()), len(native.faces()))
|
|
self.assertEqual(
|
|
[relation.output_role for relation in delta.relations if relation.kind == "face"],
|
|
["sweep.start", "sweep.end"],
|
|
)
|
|
side_relations = [
|
|
relation for relation in delta.relations
|
|
if relation.source_kind == "edge" and relation.result_kind == "face"
|
|
]
|
|
self.assertEqual(len(side_relations), 1)
|
|
self.assertEqual(side_relations[0].coverage, "complete")
|
|
self.assertEqual(side_relations[0].status, "proven")
|
|
|
|
def test_sweep_history_captures_each_direct_profile_vertex_edge(self) -> None:
|
|
from build123d import Edge, Face, Plane, Vector
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
adapter = Build123dGeometryAdapter()
|
|
profile = Face.make_rect(8, 6, Plane.XZ)
|
|
_solid, delta = adapter.sweep_with_topology_delta(
|
|
profile, Edge.make_line(Vector(0, 0, 0), Vector(0, 20, 0)),
|
|
)
|
|
|
|
self.assertIsNotNone(delta)
|
|
swept_vertex_relations = [
|
|
relation for relation in delta.relations
|
|
if relation.source_kind == "vertex" and relation.result_kind == "edge"
|
|
]
|
|
self.assertEqual(len(swept_vertex_relations), 4)
|
|
self.assertTrue(all(relation.result_values for relation in swept_vertex_relations))
|
|
self.assertTrue(all(relation.coverage == "complete" for relation in swept_vertex_relations))
|
|
self.assertTrue(all(relation.status == "proven" for relation in swept_vertex_relations))
|
|
|
|
def test_initial_loft_captures_builder_proven_cap_evidence(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "loft-history",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "lower", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 2}},
|
|
{
|
|
"id": "upper",
|
|
"workplane": {"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 4},
|
|
},
|
|
{
|
|
"id": "extension",
|
|
"workplane": {"origin_mm": [0, 0, 20], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 3},
|
|
},
|
|
]},
|
|
"features": [{
|
|
"id": "loft", "atomic_id": "loft_add", "depends_on": [],
|
|
"params": {"profile_sketch_ids": ["lower", "upper"]},
|
|
}],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
result = rebuild_cdsl(cdsl, root / "loft.step")
|
|
extended = deepcopy(cdsl)
|
|
extended["features"].append({
|
|
"id": "loft_extension", "atomic_id": "loft_add_with_cap_face", "depends_on": ["loft"],
|
|
"params": {"profile_sketch_ids": ["extension"]},
|
|
"selectors": [{
|
|
"kind": "face", "owner_feature_id": "loft", "output_role": "loft.end",
|
|
"source": "runtime_snapshot", "confidence": 1,
|
|
}],
|
|
})
|
|
downstream = rebuild_cdsl(extended, root / "loft-extension.step")
|
|
|
|
self.assertAlmostEqual(result["volume_mm3"], 280 * math.pi / 3, places=5)
|
|
delta = next(item for item in result["topology_deltas"] if item["operation"] == "loft")
|
|
self.assertEqual(
|
|
{item.get("output_role") for item in delta["relations"]},
|
|
{"loft.start", "loft.end"},
|
|
)
|
|
self.assertTrue(all(
|
|
item["event"] == "generated" and item["status"] == "recorded_without_owner_transfer"
|
|
and item["result_record_ids"]
|
|
for item in delta["relations"]
|
|
))
|
|
self.assertTrue(any(
|
|
item["feature_id"] == "loft" and item.get("output_roles") == ["loft.end"]
|
|
for item in result["topology_records"]
|
|
))
|
|
self.assertEqual(downstream["runtime_diagnostics"], [])
|
|
self.assertTrue(any(
|
|
item["feature_id"] == "loft_extension" and item["status"] == "resolved"
|
|
and item["selected"].get("output_roles") == ["loft.end"]
|
|
for item in downstream["selector_resolution"]
|
|
))
|
|
|
|
def test_circular_pattern_rotates_a_sweep_profile_and_path(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "sweep-pattern",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "profile", "workplane": {"origin_mm": [10, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, -1]},
|
|
"profile": {"type": "circle", "radius_mm": 1},
|
|
}]},
|
|
"features": [
|
|
{"id": "sweep", "atomic_id": "sweep_add", "depends_on": [], "sketch_id": "profile", "params": {"path": {
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 1, 0]},
|
|
"segment": {"type": "line", "start": [10, 0], "end": [10, 10]},
|
|
}}},
|
|
{"id": "pattern", "atomic_id": "pattern_circular", "depends_on": ["sweep"], "params": {
|
|
"source_feature_ids": ["sweep"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
|
"pattern_count": 3, "sweep_angle_deg": 360,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "sweep-pattern.step")
|
|
self.assertEqual(result["solid_count"], 3)
|
|
self.assertAlmostEqual(result["volume_mm3"], 30 * math.pi, delta=1e-4)
|
|
|
|
def test_sweep_accepts_a_closed_circle_path_as_a_wire(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "closed-circle-sweep",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "profile",
|
|
"workplane": {"origin_mm": [20, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 1, 0]},
|
|
"profile": {"type": "circle", "radius_mm": 2},
|
|
}]},
|
|
"features": [{
|
|
"id": "sweep", "atomic_id": "sweep_add", "depends_on": [], "sketch_id": "profile",
|
|
"params": {"path": {
|
|
"workplane": _workplane(),
|
|
"segment": {"type": "circle", "center": [0, 0], "radius_mm": 20},
|
|
}},
|
|
}],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "closed-circle-sweep.step")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertEqual(result["runtime_diagnostics"], [])
|
|
self.assertAlmostEqual(result["volume_mm3"], 160 * math.pi**2, delta=1e-4)
|
|
|
|
def test_circular_pattern_rotates_a_spatial_sweep_path(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "spatial-sweep-pattern",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "profile", "workplane": {"origin_mm": [10, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 1},
|
|
}]},
|
|
"features": [
|
|
{"id": "sweep", "atomic_id": "sweep_add", "depends_on": [], "sketch_id": "profile", "params": {"path": {"segments": [
|
|
{
|
|
"type": "line", "start_mm": [10, 0, 0], "end_mm": [10, 0, 10],
|
|
"source_sketch_id": "F1", "source_entity_id": "E0",
|
|
},
|
|
{
|
|
"type": "line", "start_mm": [10, 0, 10], "end_mm": [20, 0, 10],
|
|
"source_sketch_id": "F2", "source_entity_id": "E1",
|
|
},
|
|
]}}},
|
|
{"id": "pattern", "atomic_id": "pattern_circular", "depends_on": ["sweep"], "params": {
|
|
"source_feature_ids": ["sweep"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
|
"pattern_count": 3, "sweep_angle_deg": 360,
|
|
}},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "spatial-sweep-pattern.step")
|
|
self.assertEqual(result["solid_count"], 3)
|
|
self.assertGreater(result["volume_mm3"], 0)
|
|
|
|
def test_shell_removes_a_selected_cap_and_offsets_inward(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "baseline.step")
|
|
top = next(
|
|
item for item in baseline["topology_records"]
|
|
if item["kind"] == "face"
|
|
and item["feature_id"] == "base_add"
|
|
and item["geometry"].get("surface_type") == "plane"
|
|
and item["geometry"].get("normal", [0, 0, 0])[2] > 0.9
|
|
)
|
|
shelled = deepcopy(base)
|
|
shelled["features"].append({
|
|
"id": "shell", "atomic_id": "shell", "depends_on": ["base_add"],
|
|
"params": {"thickness_mm": 1, "inward": True},
|
|
"selectors": [{
|
|
"kind": "face", "stable_id": top["record_id"], "snapshot_id": top["record_id"],
|
|
"source": "runtime_snapshot", "confidence": 1, "owner_feature_id": "base_add",
|
|
"geometry": top["geometry"],
|
|
}],
|
|
})
|
|
result = rebuild_cdsl(shelled, root / "shell.step")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], 424.0, places=5)
|
|
|
|
def test_shell_removes_a_selected_cap_and_offsets_outward(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "baseline.step")
|
|
top = next(
|
|
item for item in baseline["topology_records"]
|
|
if item["kind"] == "face"
|
|
and item["feature_id"] == "base_add"
|
|
and item["geometry"].get("surface_type") == "plane"
|
|
and item["geometry"].get("normal", [0, 0, 0])[2] > 0.9
|
|
)
|
|
shelled = deepcopy(base)
|
|
shelled["features"].append({
|
|
"id": "shell", "atomic_id": "shell", "depends_on": ["base_add"],
|
|
"params": {"thickness_mm": 1, "inward": False},
|
|
"selectors": [{
|
|
"kind": "face", "stable_id": top["record_id"], "snapshot_id": top["record_id"],
|
|
"source": "runtime_snapshot", "confidence": 1, "owner_feature_id": "base_add",
|
|
"geometry": top["geometry"],
|
|
}],
|
|
})
|
|
result = rebuild_cdsl(shelled, root / "shell-outward.step")
|
|
|
|
self.assertEqual(result["solid_count"], 1)
|
|
# A shell is hollow on either side of the source skin. This confirms
|
|
# the exterior offset reached a distinct OCC result rather than
|
|
# silently using the inward default (424 mm^3 for this fixture).
|
|
self.assertGreater(result["volume_mm3"], 424.0)
|
|
|
|
def test_shell_explicit_target_requires_the_live_face_member(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "baseline.step")
|
|
top = next(
|
|
item for item in baseline["topology_records"]
|
|
if item["kind"] == "face"
|
|
and item["feature_id"] == "base_add"
|
|
and item["geometry"].get("surface_type") == "plane"
|
|
and item["geometry"].get("normal", [0, 0, 0])[2] > 0.9
|
|
)
|
|
shelled = deepcopy(base)
|
|
shelled["features"].append({
|
|
"id": "shell", "atomic_id": "shell", "depends_on": ["base_add"],
|
|
"params": {"thickness_mm": 1, "inward": True, "target_feature_id": "base_add"},
|
|
"selectors": [{
|
|
"kind": "face", "stable_id": top["record_id"], "snapshot_id": top["record_id"],
|
|
"source": "runtime_snapshot", "confidence": 1, "owner_feature_id": "base_add",
|
|
"geometry": top["geometry"],
|
|
}],
|
|
})
|
|
result = rebuild_cdsl(shelled, root / "shell.step")
|
|
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], 424.0, places=5)
|
|
|
|
invalid = deepcopy(shelled)
|
|
invalid["features"][-1]["params"]["target_feature_id"] = "missing_body"
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids={"extrude_add_blind", "shell"}, profile_types=SHAPE_GENERATORS,
|
|
).analyze(invalid)
|
|
self.assertIn(
|
|
"shell_target_body_unavailable",
|
|
[blocker.code for blocker in analysis.feature_results[-1].blockers],
|
|
)
|
|
|
|
def test_fillet_and_hole_wizard_use_resolved_face_edge_selectors(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "baseline.step")
|
|
edge = next(item for item in baseline["topology_records"] if item["kind"] == "edge")
|
|
top_face = next(
|
|
item for item in baseline["topology_records"]
|
|
if item["kind"] == "face" and item["geometry"]["surface_type"] == "plane" and item["geometry"]["normal"][2] > 0.9
|
|
)
|
|
with_fillet = deepcopy(base)
|
|
with_fillet["features"].append({
|
|
"id": "fillet", "atomic_id": "fillet", "depends_on": ["base_add"], "params": {"radius_mm": 1},
|
|
"selectors": [{"kind": "edge", "stable_id": "edge", "source": "solidworks", "confidence": 1, "owner_feature_id": "base_add", "geometry": edge["geometry"]}],
|
|
})
|
|
filleted = rebuild_cdsl(with_fillet, root / "fillet.step")
|
|
with_hole = deepcopy(base)
|
|
with_hole["features"].append({
|
|
"id": "hole", "atomic_id": "hole_wizard", "depends_on": ["base_add"],
|
|
"params": {
|
|
"hole_type": "plain", "diameter_mm": 2, "depth_mm": 5,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0}, "positions": [{"mm": [0, 0, 10]}],
|
|
"host_face": {"kind": "face", "stable_id": "top", "source": "inferred_from_step", "confidence": 1, "geometry": top_face["geometry"]},
|
|
},
|
|
"selectors": [{"kind": "face", "stable_id": "top", "source": "inferred_from_step", "confidence": 1, "geometry": top_face["geometry"]}],
|
|
})
|
|
holed = rebuild_cdsl(with_hole, root / "hole.step")
|
|
self.assertLess(filleted["volume_mm3"], baseline["volume_mm3"])
|
|
self.assertAlmostEqual(holed["volume_mm3"], 1000 - 5 * 3.141592653589793, places=5)
|
|
|
|
def test_hole_frame_uses_local_coordinates(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
base["features"].append({
|
|
"id": "hole", "atomic_id": "hole_blind", "depends_on": ["base_add"],
|
|
"sketch_id": "base",
|
|
"params": {
|
|
"diameter_mm": 2, "depth_mm": 5, "positions": [{"mm": [0, 0, 0]}],
|
|
"host_face": {"frame": {"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0], "normal": [0, 0, 1]}},
|
|
},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(base, Path(directory) / "local-hole.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000 - 5 * 3.141592653589793, places=5)
|
|
|
|
def test_scoped_holes_preserve_the_original_body_member(self) -> None:
|
|
from cdsl_engine.runtime import prepare_cdsl_execution
|
|
|
|
cdsl = self._base_block()
|
|
host_frame = {
|
|
"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0],
|
|
"y_dir": [0, 1, 0], "normal": [0, 0, 1],
|
|
}
|
|
for feature_id, position in (("first_hole", [-2, 0, 0]), ("second_hole", [2, 0, 0])):
|
|
cdsl["features"].append({
|
|
"id": feature_id, "atomic_id": "hole_wizard",
|
|
"depends_on": ["base_add" if feature_id == "first_hole" else "first_hole"],
|
|
"params": {
|
|
"hole_type": "plain", "diameter_mm": 2, "depth_mm": 5,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0},
|
|
"positions": [{"mm": position}], "host_face": {"frame": host_frame},
|
|
"scope_feature_id": "base_add",
|
|
},
|
|
})
|
|
execution = prepare_cdsl_execution(cdsl)
|
|
self.assertTrue(all(result.executable for result in execution.analysis.feature_results))
|
|
execution.execute_all()
|
|
self.assertEqual(set(execution.session.body_members), {"base_add"})
|
|
self.assertLess(float(execution.session.body.volume), 1000.0)
|
|
|
|
def test_unscoped_hole_keeps_the_legacy_feature_owned_member(self) -> None:
|
|
from cdsl_engine.runtime import prepare_cdsl_execution
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].append({
|
|
"id": "hole", "atomic_id": "hole_wizard", "depends_on": ["base_add"],
|
|
"params": {
|
|
"hole_type": "plain", "diameter_mm": 2, "depth_mm": 5,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0},
|
|
"positions": [{"mm": [0, 0, 0]}],
|
|
"host_face": {"frame": {"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0], "normal": [0, 0, 1]}},
|
|
},
|
|
})
|
|
execution = prepare_cdsl_execution(cdsl)
|
|
self.assertTrue(all(result.executable for result in execution.analysis.feature_results))
|
|
execution.execute_all()
|
|
self.assertEqual(set(execution.session.body_members), {"hole"})
|
|
|
|
def test_scoped_hole_cuts_only_its_explicit_live_member(self) -> None:
|
|
from cdsl_engine.runtime import prepare_cdsl_execution
|
|
|
|
cdsl = _two_body_boolean_cdsl(
|
|
"union", _rectangle([-5, -5], [5, 5]), _rectangle([20, -5], [30, 5]),
|
|
)
|
|
cdsl["features"] = cdsl["features"][:2]
|
|
cdsl["features"].append({
|
|
"id": "scoped_hole", "atomic_id": "hole_wizard", "depends_on": ["left_body", "right_body"],
|
|
"params": {
|
|
"hole_type": "plain", "diameter_mm": 2, "depth_mm": 2,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0},
|
|
"positions": [{"mm": [0, 0, 0]}],
|
|
"host_face": {"frame": {"origin_mm": [0, 0, 4], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0], "normal": [0, 0, 1]}},
|
|
"scope_feature_id": "left_body",
|
|
},
|
|
})
|
|
execution = prepare_cdsl_execution(cdsl)
|
|
self.assertTrue(execution.analysis.feature_results[-1].executable)
|
|
execution.execute_all()
|
|
self.assertEqual(set(execution.session.body_members), {"left_body", "right_body"})
|
|
self.assertAlmostEqual(float(execution.session.body.volume), 800.0 - 2.0 * math.pi, places=5)
|
|
self.assertAlmostEqual(float(execution.session.body_members["right_body"].volume), 400.0, places=5)
|
|
|
|
def test_counterbore_and_countersink_holes_execute_with_explicit_host_frames(self) -> None:
|
|
"""Keep both legacy hole contracts covered by an actual kernel rebuild."""
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
host_frame = {
|
|
"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0],
|
|
"y_dir": [0, 1, 0], "normal": [0, 0, 1],
|
|
}
|
|
for atomic_id, details in (
|
|
("hole_counterbore", {"counterbore_diameter_mm": 4, "counterbore_depth_mm": 2}),
|
|
("hole_countersink", {"countersink_diameter_mm": 4, "countersink_angle_rad": 1.5707963267948966}),
|
|
):
|
|
with self.subTest(atomic_id=atomic_id):
|
|
cdsl = self._base_block()
|
|
cdsl["features"].append({
|
|
"id": atomic_id, "atomic_id": atomic_id, "depends_on": ["base_add"],
|
|
"params": {
|
|
"diameter_mm": 2, "depth_mm": 5, "positions": [{"mm": [0, 0, 0]}],
|
|
"host_face": {"frame": host_frame}, **details,
|
|
},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / f"{atomic_id}.step")
|
|
self.assertLess(result["volume_mm3"], 1000.0)
|
|
self.assertIn(atomic_id, [item["feature_id"] for item in result["feature_results"]])
|
|
|
|
def test_sphere_add_executes_without_a_sketch(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "sphere",
|
|
"meta": {"unit": "mm"}, "geometry": {"sketches": []},
|
|
"features": [{
|
|
"id": "sphere", "atomic_id": "sphere_add", "depends_on": [],
|
|
"params": {"radius_mm": 2, "center_mm": [0, 0, 0]},
|
|
}],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "sphere.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 32 * 3.141592653589793 / 3, places=5)
|
|
self.assertEqual(result["feature_results"][-1]["feature_id"], "sphere")
|
|
|
|
def test_pattern_with_selector_source_is_blocked_before_execution(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].extend([
|
|
{"id": "fillet", "atomic_id": "fillet", "depends_on": ["base_add"], "params": {"radius_mm": 1}, "selectors": [{"kind": "edge", "stable_id": "edge", "source": "solidworks", "confidence": 1}]},
|
|
{"id": "repeat", "atomic_id": "pattern_linear", "depends_on": ["fillet"], "params": {"source_feature_ids": ["fillet"], "direction_1": [1, 0, 0], "spacing_1_mm": 10, "pattern_count_1": 2}},
|
|
])
|
|
analysis = analyze_cdsl(cdsl)
|
|
pattern = next(item for item in analysis.feature_results if item.feature_id == "repeat")
|
|
self.assertFalse(pattern.executable)
|
|
self.assertIn("unsupported_pattern_selector_transform", [item.code for item in pattern.blockers])
|
|
|
|
def test_chamfer_and_linear_pattern_execute_without_selector_guessing(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
base["geometry"]["sketches"].append({
|
|
"id": "cut", "workplane": _workplane(), "profile": {"type": "circle", "center": [-2, 0], "radius_mm": 1},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "baseline.step")
|
|
edge = next(item for item in baseline["topology_records"] if item["kind"] == "edge")
|
|
chamfered = deepcopy(base)
|
|
chamfered["features"].append({
|
|
"id": "chamfer", "atomic_id": "chamfer", "depends_on": ["base_add"], "params": {"distance_mm": 1},
|
|
"selectors": [{"kind": "edge", "stable_id": "edge", "source": "solidworks", "confidence": 1, "owner_feature_id": "base_add", "geometry": edge["geometry"]}],
|
|
})
|
|
chamfer_result = rebuild_cdsl(chamfered, root / "chamfer.step")
|
|
patterned = deepcopy(base)
|
|
patterned["features"].extend([
|
|
{"id": "cut_1", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "params": {"distance_mm": 10}, "sketch_id": "cut"},
|
|
{"id": "repeat", "atomic_id": "pattern_linear", "depends_on": ["cut_1"], "params": {"source_feature_ids": ["cut_1"], "direction_1": [1, 0, 0], "spacing_1_mm": 2, "pattern_count_1": 3}},
|
|
])
|
|
pattern_result = rebuild_cdsl(patterned, root / "pattern.step")
|
|
self.assertLess(chamfer_result["volume_mm3"], baseline["volume_mm3"])
|
|
self.assertAlmostEqual(pattern_result["volume_mm3"], 1000 - 3 * 10 * 3.141592653589793, places=5)
|
|
|
|
def test_chamfer_consumes_angle_rad_instead_of_silent_45_degree_fallback(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
import math
|
|
|
|
base = self._base_block()
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "baseline.step")
|
|
edge = next(item for item in baseline["topology_records"] if item["kind"] == "edge")
|
|
selector = {
|
|
"kind": "edge", "stable_id": "edge", "source": "solidworks", "confidence": 1,
|
|
"owner_feature_id": "base_add", "geometry": edge["geometry"],
|
|
}
|
|
|
|
equal = deepcopy(base)
|
|
equal["features"].append({
|
|
"id": "chamfer_45", "atomic_id": "chamfer", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 1, "angle_rad": math.pi / 4}, "selectors": [deepcopy(selector)],
|
|
})
|
|
equal_result = rebuild_cdsl(equal, root / "chamfer-45.step")
|
|
# 45° Distance-Angle 等价于等距倒角:tan(45°)=1,切掉 0.5*1*1*10=5 mm³。
|
|
self.assertAlmostEqual(equal_result["volume_mm3"], 1000 - 5, places=5)
|
|
|
|
slanted = deepcopy(base)
|
|
slanted["features"].append({
|
|
"id": "chamfer_30", "atomic_id": "chamfer", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 1, "angle_rad": math.pi / 6}, "selectors": [deepcopy(selector)],
|
|
})
|
|
slanted_result = rebuild_cdsl(slanted, root / "chamfer-30.step")
|
|
# 30°:第二距离 = 1*tan(30°)≈0.577,切掉 0.5*1*0.577*10≈2.887 mm³,
|
|
# 体积明显大于 45° 等距倒角(995),验证 angle_rad 被消费而非静默 45°。
|
|
self.assertAlmostEqual(slanted_result["volume_mm3"], 1000 - 0.5 * math.tan(math.pi / 6) * 10, places=5)
|
|
|
|
def test_linear_pattern_replays_hole_with_explicit_host_frame(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "hole_1", "atomic_id": "hole_blind", "depends_on": ["base_add"], "sketch_id": "base",
|
|
"params": {
|
|
"diameter_mm": 2, "depth_mm": 5, "positions": [{"mm": [0, 0, 0]}],
|
|
"host_face": {"frame": {
|
|
"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0],
|
|
"y_dir": [0, 1, 0], "normal": [0, 0, 1],
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
"id": "repeat", "atomic_id": "pattern_linear", "depends_on": ["hole_1"],
|
|
"params": {
|
|
"source_feature_ids": ["hole_1"], "direction_1": [1, 0, 0],
|
|
"spacing_1_mm": 4, "pattern_count_1": 2,
|
|
},
|
|
},
|
|
])
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "patterned-holes.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000 - 2 * 5 * 3.141592653589793, places=5)
|
|
|
|
def test_mirror_pattern_replays_local_hole_coordinates_in_the_correct_quadrant(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].insert(0, {
|
|
"id": "mirror_plane", "atomic_id": "reference_plane", "depends_on": [],
|
|
"params": {"plane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]}},
|
|
})
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "hole_1", "atomic_id": "hole_blind", "depends_on": ["base_add"], "sketch_id": "base",
|
|
"params": {
|
|
"diameter_mm": 2, "depth_mm": 5, "positions": [{"mm": [2, 3, 0]}],
|
|
"host_face": {"frame": {
|
|
"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0],
|
|
"y_dir": [0, 1, 0], "normal": [0, 0, 1],
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["hole_1", "mirror_plane"],
|
|
"params": {
|
|
"source_feature_ids": ["hole_1"],
|
|
"mirror_plane": {"kind": "plane", "owner_feature_id": "mirror_plane"},
|
|
},
|
|
"selectors": [{"kind": "plane", "owner_feature_id": "mirror_plane"}],
|
|
},
|
|
])
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "mirrored-holes.step")
|
|
mirrored_cylinder = next(
|
|
item for item in result["topology_records"]
|
|
if item["record_id"].startswith("body:mirror.m.hole_1:face")
|
|
and item["geometry"].get("surface_type") == "cylinder"
|
|
and item["geometry"]["bbox_mm"][0] < -2.9
|
|
)
|
|
self.assertEqual(mirrored_cylinder["geometry"]["bbox_mm"][:2], [-3.0, 2.0])
|
|
self.assertEqual(mirrored_cylinder["geometry"]["bbox_mm"][3:5], [-1.0, 4.0])
|
|
|
|
def test_pattern_replays_selected_sources_in_history_order(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "cut", "workplane": _workplane(),
|
|
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
|
})
|
|
cdsl["features"].extend([
|
|
{
|
|
"id": "cut_1", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 10}, "sketch_id": "cut",
|
|
},
|
|
{
|
|
"id": "repeat", "atomic_id": "pattern_linear", "depends_on": ["cut_1"],
|
|
# SolidWorks selection exports are not guaranteed to match
|
|
# history order; the executor must replay the boss before its cut.
|
|
"params": {
|
|
"source_feature_ids": ["cut_1", "base_add"],
|
|
"direction_1": [1, 0, 0], "spacing_1_mm": 20, "pattern_count_1": 2,
|
|
},
|
|
},
|
|
])
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "ordered-pattern.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 2 * (1000 - 10 * 3.141592653589793), places=5)
|
|
|
|
def test_through_all_extent_uses_current_body(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "cut", "workplane": _workplane(), "profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "cut_all", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 0, "end_condition": {"type": "through_all", "solidworks_code": 1}}, "sketch_id": "cut",
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "through.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000 - 10 * 3.141592653589793, places=5)
|
|
|
|
def test_two_sided_extrude_uses_independent_forward_and_reverse_distances(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
feature = cdsl["features"][0]
|
|
feature["atomic_id"] = "extrude_add_two_sided"
|
|
feature["params"] = {
|
|
"distance_mm": 2, "reverse_distance_mm": 3,
|
|
"end_condition": {"type": "blind"}, "reverse_end_condition": {"type": "blind"},
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "two-sided.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 500.0)
|
|
self.assertEqual(result["bbox_mm"]["min"][2], -3.0)
|
|
self.assertEqual(result["bbox_mm"]["max"][2], 2.0)
|
|
|
|
def test_two_sided_prism_fuse_does_not_preserve_half_prism_swept_edge_handles(self) -> None:
|
|
"""A symmetric prism needs a dedicated final relation for SWEPT_EDGE.
|
|
|
|
Each half prism has exact ``Generated(vertex)`` evidence, but the
|
|
fuse replaces those handles with the complete final edge. A resolver
|
|
must not treat geometric continuity as a provenance continuation.
|
|
"""
|
|
from build123d import Face, Vector, Wire
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
face = Face(Wire.make_polygon([
|
|
Vector(-5, -5, 0), Vector(5, -5, 0), Vector(5, 5, 0),
|
|
Vector(-5, 5, 0), Vector(-5, -5, 0),
|
|
]))
|
|
forward, forward_delta = Build123dGeometryAdapter.extrude_with_topology_delta(face, (0, 0, 2))
|
|
reverse, reverse_delta = Build123dGeometryAdapter.extrude_with_topology_delta(face, (0, 0, -3))
|
|
fused = Build123dGeometryAdapter.fuse(forward, reverse)
|
|
final_edges = [edge.wrapped for edge in fused.edges()]
|
|
|
|
for delta in (forward_delta, reverse_delta):
|
|
generated = [
|
|
value
|
|
for relation in delta.relations
|
|
if relation.source_kind == "vertex" and relation.result_kind == "edge"
|
|
for value in relation.result_values
|
|
]
|
|
self.assertTrue(generated)
|
|
self.assertFalse(any(
|
|
generated_edge.IsSame(final_edge)
|
|
for generated_edge in generated
|
|
for final_edge in final_edges
|
|
))
|
|
|
|
def test_two_sided_extrude_requires_reverse_distance_contract(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
feature = cdsl["features"][0]
|
|
feature["atomic_id"] = "extrude_add_two_sided"
|
|
analysis = analyze_cdsl(cdsl)
|
|
self.assertIn("missing_parameter", [item.code for item in analysis.feature_results[0].blockers])
|
|
|
|
def test_two_sided_cut_uses_independent_forward_and_reverse_distances(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "cut", "workplane": {**_workplane(), "origin_mm": [0, 0, 5]},
|
|
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "cut_1", "atomic_id": "extrude_cut_two_sided", "depends_on": ["base_add"],
|
|
"params": {
|
|
"distance_mm": 3, "reverse_distance_mm": 5,
|
|
"end_condition": {"type": "blind"}, "reverse_end_condition": {"type": "blind"},
|
|
}, "sketch_id": "cut",
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "two-sided-cut.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000.0 - 8.0 * 3.141592653589793, places=5)
|
|
|
|
def test_revolve_can_resolve_an_owner_qualified_reference_axis(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "axis-revolve",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "profile", "workplane": _workplane(),
|
|
"profile": _rectangle([2, 1], [4, 2]),
|
|
}]},
|
|
"features": [
|
|
{
|
|
"id": "axis", "atomic_id": "reference_axis", "depends_on": [],
|
|
"params": {"axis": {"origin_mm": [0, 0, 0], "direction": [1, 0, 0]}},
|
|
},
|
|
{
|
|
"id": "revolve", "atomic_id": "revolve_add", "depends_on": ["axis"], "sketch_id": "profile",
|
|
"params": {
|
|
"angle_deg": 360, "axis": {"selector": {"kind": "axis", "owner_feature_id": "axis"}},
|
|
},
|
|
"selectors": [{"kind": "axis", "owner_feature_id": "axis"}],
|
|
},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "axis-revolve.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 6 * 3.141592653589793, places=5)
|
|
|
|
def test_revolve_cut_executes_against_an_existing_body(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "revolve_cut_profile", "workplane": _workplane(),
|
|
"profile": _rectangle([-4, 1], [4, 2]),
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "revolve_cut", "atomic_id": "revolve_cut", "depends_on": ["base_add"],
|
|
"sketch_id": "revolve_cut_profile",
|
|
"params": {"angle_deg": 360, "axis": {"origin_mm": [0, 0, 0], "direction": [1, 0, 0]}},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "revolve-cut.step")
|
|
self.assertLess(result["volume_mm3"], 1000.0)
|
|
self.assertIn("revolve_cut", [item["feature_id"] for item in result["feature_results"]])
|
|
|
|
def test_revolve_surface_preserves_the_active_solid_and_registers_a_shell(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "surface_profile", "workplane": _workplane(),
|
|
"profile": _rectangle([2, 1], [4, 2]),
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "surface_revolve", "atomic_id": "revolve_surface", "depends_on": ["base_add"],
|
|
"sketch_id": "surface_profile",
|
|
"params": {"angle_deg": 360, "axis": {"origin_mm": [0, 0, 0], "direction": [1, 0, 0]}},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "surface-revolve.step")
|
|
surface = next(item for item in result["feature_results"] if item["feature_id"] == "surface_revolve")
|
|
self.assertNotIn("body_id", surface)
|
|
self.assertEqual(surface["surface_id"], "surface:surface_revolve")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000.0)
|
|
faces = [
|
|
item for item in result["topology_records"]
|
|
if item["feature_id"] == "surface_revolve" and item["kind"] == "face"
|
|
]
|
|
self.assertEqual(len(faces), 4)
|
|
|
|
def test_extrude_surface_preserves_the_active_solid_and_registers_a_shell(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["geometry"]["sketches"].append({
|
|
"id": "surface_profile", "workplane": _workplane(),
|
|
"profile": {
|
|
"type": "analytic_contours",
|
|
"contours": [{
|
|
"role": "unknown", "closed": True,
|
|
"segments": [{"type": "circle", "center": [5, 5], "radius_mm": 2}],
|
|
}],
|
|
},
|
|
})
|
|
cdsl["features"].append({
|
|
"id": "surface_extrude", "atomic_id": "extrude_surface", "depends_on": ["base_add"],
|
|
"sketch_id": "surface_profile", "params": {"distance_mm": 10},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "surface-extrude.step")
|
|
surface = next(item for item in result["feature_results"] if item["feature_id"] == "surface_extrude")
|
|
self.assertNotIn("body_id", surface)
|
|
self.assertEqual(surface["surface_id"], "surface:surface_extrude")
|
|
self.assertEqual(result["solid_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000.0)
|
|
faces = [
|
|
item for item in result["topology_records"]
|
|
if item["feature_id"] == "surface_extrude" and item["kind"] == "face"
|
|
]
|
|
self.assertEqual(len(faces), 1)
|
|
|
|
def test_extrude_surface_exports_a_surface_only_step(self) -> None:
|
|
from build123d import import_step
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "surface-only",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "surface_profile", "workplane": _workplane(),
|
|
"profile": {
|
|
"type": "analytic_contours",
|
|
"contours": [{
|
|
"role": "unknown", "closed": True,
|
|
"segments": [{"type": "circle", "center": [5, 5], "radius_mm": 2}],
|
|
}],
|
|
},
|
|
}]},
|
|
"features": [{
|
|
"id": "surface_extrude", "atomic_id": "extrude_surface", "depends_on": [],
|
|
"sketch_id": "surface_profile", "params": {"distance_mm": 10},
|
|
}],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
out_step = Path(directory) / "surface-only.step"
|
|
result = rebuild_cdsl(cdsl, out_step)
|
|
rebuilt = import_step(str(out_step))
|
|
self.assertEqual(result["solid_count"], 0)
|
|
self.assertEqual(result["surface_count"], 1)
|
|
self.assertEqual(result["surface_face_count"], 1)
|
|
self.assertAlmostEqual(result["volume_mm3"], 0.0)
|
|
self.assertAlmostEqual(result["surface_area_mm2"], 40 * math.pi)
|
|
self.assertEqual(len(rebuilt.solids()), 0)
|
|
self.assertEqual(len(rebuilt.faces()), 1)
|
|
|
|
def test_extrude_surface_preserves_an_explicit_open_source_wire(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "surface-wire",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "surface_wire", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [{
|
|
"role": "open", "closed": False, "surface_wire": True,
|
|
"segments": [{"type": "line", "start": [0, 0], "end": [10, 0]}],
|
|
}]},
|
|
}]},
|
|
"features": [{
|
|
"id": "surface_extrude", "atomic_id": "extrude_surface", "depends_on": [],
|
|
"sketch_id": "surface_wire", "params": {"distance_mm": 5},
|
|
}],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "surface-wire.step")
|
|
self.assertEqual(result["solid_count"], 0)
|
|
self.assertEqual(result["surface_count"], 1)
|
|
self.assertEqual(result["surface_face_count"], 1)
|
|
self.assertAlmostEqual(result["surface_area_mm2"], 50.0)
|
|
|
|
def test_extrude_surface_preserves_connected_and_disconnected_open_source_wires(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "surface-wires",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "surface_wires", "workplane": _workplane(),
|
|
"profile": {"type": "analytic_contours", "contours": [
|
|
{
|
|
"role": "open", "closed": False, "surface_wire": True,
|
|
"segments": [
|
|
{"type": "line", "start": [0, 0], "end": [10, 0]},
|
|
{"type": "line", "start": [10, 0], "end": [10, 10]},
|
|
],
|
|
},
|
|
{
|
|
"role": "open", "closed": False, "surface_wire": True,
|
|
"segments": [{"type": "line", "start": [20, 0], "end": [26, 0]}],
|
|
},
|
|
]},
|
|
}]},
|
|
"features": [{
|
|
"id": "surface_extrude", "atomic_id": "extrude_surface", "depends_on": [],
|
|
"sketch_id": "surface_wires", "params": {"distance_mm": 5},
|
|
}],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "surface-wires.step")
|
|
self.assertEqual(result["solid_count"], 0)
|
|
self.assertEqual(result["surface_count"], 1)
|
|
self.assertEqual(result["surface_face_count"], 3)
|
|
self.assertAlmostEqual(result["surface_area_mm2"], 130.0)
|
|
|
|
def test_surface_limited_chamfer_requires_an_explicit_shell_boundary(self) -> None:
|
|
from build123d import Plane, Solid
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
adapter = Build123dGeometryAdapter()
|
|
base = Solid.make_cylinder(19, 20, Plane(origin=(0, 0, -10))).cut(
|
|
Solid.make_cylinder(16, 20, Plane(origin=(0, 0, -10))),
|
|
)
|
|
lower = Solid.make_cylinder(28.5, 20, Plane(origin=(0, 0, -20))).cut(
|
|
Solid.make_cylinder(16, 20, Plane(origin=(0, 0, -20))),
|
|
)
|
|
upper = Solid.make_cylinder(25.5, 25, Plane(origin=(0, 0, 10))).cut(
|
|
Solid.make_cylinder(17.5, 25, Plane(origin=(0, 0, 10))),
|
|
)
|
|
body = adapter.fuse(adapter.fuse(base, lower), upper)
|
|
selected = [
|
|
edge for edge in body.edges()
|
|
if str(edge.geom_type).split(".")[-1].lower() == "circle"
|
|
and abs(float(edge.radius) - 28.5) <= 1e-6
|
|
and abs(edge.arc_center.Z) <= 1e-6
|
|
]
|
|
self.assertEqual(len(selected), 1)
|
|
|
|
with self.assertRaisesRegex(ValueError, "explicit surface support"):
|
|
adapter.surface_limited_chamfer(body, 10, selected, [])
|
|
|
|
wire = adapter._circle_wire([0, 0], 19, PlaneSpec.from_mapping(_workplane()))
|
|
support = adapter.extrude_surface([wire], [0, 0, 10])
|
|
rebuilt = adapter.surface_limited_chamfer(body, 10, selected, [support])
|
|
|
|
self.assertLess(rebuilt.volume, body.volume)
|
|
cones = [face for face in rebuilt.faces() if str(face.geom_type).split(".")[-1].lower() == "cone"]
|
|
self.assertEqual(len(cones), 1)
|
|
|
|
def test_unowned_revolve_feature_selector_is_preflight_blocked(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = json.loads((ROOT / "json_to_cdsl" / "output" / "027784.cdsl.json").read_text(encoding="utf-8"))
|
|
analysis = analyze_cdsl(cdsl)
|
|
revolve = next(item for item in analysis.feature_results if item.feature_id == "f_007")
|
|
self.assertIn("missing_revolve_axis", [item.code for item in revolve.blockers])
|
|
|
|
def test_tangent_propagation_does_not_expand_to_unrelated_box_edges(self) -> None:
|
|
from build123d import Box
|
|
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
|
|
|
body = Box(10, 10, 10)
|
|
selected = body.edges()[0]
|
|
expanded = Build123dGeometryAdapter().tangent_edges(body, [selected])
|
|
self.assertEqual(len(expanded), 1)
|
|
self.assertTrue(expanded[0].is_same(selected))
|
|
|
|
def test_inconsistent_quarter_arc_flags_are_normalized_from_loop_orientation(self) -> None:
|
|
from cdsl_engine.batch_rebuild import analyze_document
|
|
|
|
fixture = ROOT / "json_to_cdsl" / "output" / "053393.cdsl.json"
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
report = analyze_document(fixture, out_step=Path(directory) / "053393.step")
|
|
self.assertTrue(report["runtime_eligible"])
|
|
self.assertTrue(report["built"])
|
|
self.assertTrue(report["geometry_verified"])
|
|
|
|
def test_exported_two_sided_fixture_is_geometry_verified(self) -> None:
|
|
from cdsl_engine.batch_rebuild import analyze_document
|
|
|
|
fixture = ROOT / "json_to_cdsl" / "output" / "046112.cdsl.json"
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
report = analyze_document(fixture, out_step=Path(directory) / "046112.step")
|
|
self.assertTrue(report["runtime_eligible"])
|
|
self.assertTrue(report["built"])
|
|
self.assertTrue(report["geometry_verified"])
|
|
|
|
|
|
def test_selector_driven_extrude_extents_require_unique_rebuilt_topology(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
base["geometry"]["sketches"].append({
|
|
"id": "cut", "workplane": _workplane(),
|
|
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "baseline.step")
|
|
top_face = next(
|
|
item for item in baseline["topology_records"]
|
|
if item["kind"] == "face" and item["geometry"]["surface_type"] == "plane"
|
|
and item["geometry"]["normal"][2] > 0.9
|
|
)
|
|
top_vertex = next(
|
|
item for item in baseline["topology_records"]
|
|
if item["kind"] == "vertex" and item["geometry"]["center_mm"][2] > 9.9
|
|
)
|
|
for name, end_condition, expected_depth in (
|
|
("surface", {"type": "up_to_surface", "reference": {"kind": "face", "owner_feature_id": "base_add", "geometry": top_face["geometry"]}}, 10.0),
|
|
("vertex", {"type": "up_to_vertex", "reference": {"kind": "vertex", "owner_feature_id": "base_add", "geometry": top_vertex["geometry"]}}, 10.0),
|
|
("offset", {"type": "offset_from_surface", "reference": {"kind": "face", "owner_feature_id": "base_add", "geometry": top_face["geometry"]}}, 8.0),
|
|
("next", {"type": "through_next"}, 10.0),
|
|
):
|
|
cdsl = deepcopy(base)
|
|
cdsl["features"].append({
|
|
"id": f"cut_{name}", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 2 if name == "offset" else 0, "end_condition": end_condition},
|
|
"sketch_id": "cut",
|
|
})
|
|
result = rebuild_cdsl(cdsl, root / f"{name}.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000 - expected_depth * 3.141592653589793, places=5)
|
|
|
|
def test_up_to_vertex_source_datum_does_not_resolve_runtime_topology(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
base["geometry"]["sketches"].append({
|
|
"id": "cut", "source_sketch_id": "source_cut", "workplane": _workplane(),
|
|
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
|
})
|
|
base["features"].append({
|
|
"id": "cut_to_source_vertex", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 0, "end_condition": {
|
|
"type": "up_to_vertex", "solidworks_code": 5,
|
|
"reference": {
|
|
"kind": "source_vertex", "source_sketch_id": "source_cut",
|
|
"source_entity_id": "E0.end", "point_mm": [0, 0, 10],
|
|
},
|
|
}},
|
|
"sketch_id": "cut",
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(base, Path(directory) / "source-vertex.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000 - 10 * math.pi, places=5)
|
|
self.assertEqual(result["selector_resolution"], [])
|
|
|
|
def test_up_to_vertex_intersection_requires_one_shared_current_body_vertex(self) -> None:
|
|
from cdsl_engine.runtime import RuntimeExecutionError, rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
base["geometry"]["sketches"].append({
|
|
"id": "cut", "workplane": _workplane(),
|
|
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "intersection-baseline.step")
|
|
faces = [item for item in baseline["topology_records"] if item["kind"] == "face"]
|
|
top = next(item for item in faces if item["geometry"]["normal"][2] > 0.9)
|
|
right = next(item for item in faces if item["geometry"]["normal"][0] > 0.9)
|
|
back = next(item for item in faces if item["geometry"]["normal"][1] > 0.9)
|
|
|
|
def face_selector(face: dict) -> dict:
|
|
return {
|
|
"kind": "face", "owner_feature_id": "base_add",
|
|
"stable_id": f"intersection-{face['record_id']}",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
"geometry": face["geometry"],
|
|
}
|
|
|
|
cdsl = deepcopy(base)
|
|
cdsl["features"].append({
|
|
"id": "cut_to_intersection", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 0, "end_condition": {
|
|
"type": "up_to_vertex", "reference": {
|
|
"kind": "vertex", "owner_feature_id": "base_add",
|
|
"stable_id": "top-right-back", "source": "runtime_snapshot", "confidence": 1.0,
|
|
"intersection_of": [face_selector(top), face_selector(right), face_selector(back)],
|
|
},
|
|
}},
|
|
"sketch_id": "cut",
|
|
})
|
|
result = rebuild_cdsl(cdsl, root / "intersection.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000 - 10 * 3.141592653589793, places=5)
|
|
|
|
ambiguous = deepcopy(cdsl)
|
|
ambiguous["features"][-1]["params"]["end_condition"]["reference"]["intersection_of"] = [
|
|
face_selector(right), face_selector(back),
|
|
]
|
|
with self.assertRaises(RuntimeExecutionError) as error:
|
|
rebuild_cdsl(ambiguous, root / "ambiguous-intersection.step")
|
|
self.assertEqual(error.exception.diagnostic.code, "intersection_vertex_unresolved")
|
|
|
|
def test_through_next_trims_a_partially_overlapping_profile_to_the_next_body_face(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
base["geometry"]["sketches"].append({
|
|
"id": "partial", "workplane": {"origin_mm": [0, 0, 12], "x_dir": [1, 0, 0], "normal": [0, 0, -1]},
|
|
"profile": {"type": "polygon", "vertices": [[3, -3], [9, -3], [9, 3], [3, 3]]},
|
|
})
|
|
base["features"].append({
|
|
"id": "partial_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
|
"sketch_id": "partial", "params": {"distance_mm": 0, "end_condition": {"type": "through_next"}},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(base, Path(directory) / "partial-through-next.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000 + 24, places=5)
|
|
|
|
def test_through_next_keeps_leading_material_before_a_cylindrical_body(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "leading-through-next",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "base", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 10}},
|
|
{"id": "lead", "workplane": {
|
|
"origin_mm": [0, -50, 25], "x_dir": [1, 0, 0], "normal": [0, 1, 0],
|
|
}, "profile": {"type": "circle", "radius_mm": 2}},
|
|
]},
|
|
"features": [
|
|
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"params": {"distance_mm": 50}, "sketch_id": "base"},
|
|
{"id": "lead_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 0, "end_condition": {"type": "through_next"}, "result_mode": "new_body"},
|
|
"sketch_id": "lead"},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result = rebuild_cdsl(cdsl, Path(directory) / "leading-through-next.step")
|
|
self.assertEqual(result["solid_count"], 2)
|
|
self.assertAlmostEqual(result["bbox_mm"]["min"][1], -50.0, places=5)
|
|
|
|
def test_up_to_surface_from_a_cylindrical_wall_uses_the_next_body_face(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "wall-hole",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{"id": "tube", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 10}},
|
|
{"id": "tube_bore", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 7}},
|
|
{"id": "wall_hole", "workplane": {
|
|
"origin_mm": [10, 0, 5], "x_dir": [0, 1, 0], "normal": [-1, 0, 0],
|
|
}, "profile": {"type": "circle", "radius_mm": 1}},
|
|
]},
|
|
"features": [
|
|
{"id": "tube_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"params": {"distance_mm": 10}, "sketch_id": "tube"},
|
|
{"id": "tube_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["tube_add"],
|
|
"params": {"distance_mm": 10}, "sketch_id": "tube_bore"},
|
|
],
|
|
}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "tube.step")
|
|
outer_face = next(
|
|
item for item in baseline["topology_records"]
|
|
if item["kind"] == "face" and item["geometry"]["surface_type"] == "cylinder"
|
|
and abs(item["geometry"]["radius_mm"] - 10) <= 1e-6
|
|
)
|
|
cdsl = deepcopy(base)
|
|
cdsl["features"].append({
|
|
"id": "wall_hole_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["tube_cut"],
|
|
"params": {"distance_mm": 0, "end_condition": {"type": "up_to_surface", "reference": {
|
|
"kind": "face", "owner_feature_id": "tube_add", "geometry": outer_face["geometry"],
|
|
}}},
|
|
"sketch_id": "wall_hole",
|
|
})
|
|
result = rebuild_cdsl(cdsl, root / "wall-hole.step")
|
|
self.assertLess(result["volume_mm3"], baseline["volume_mm3"] - 8)
|
|
self.assertGreater(result["volume_mm3"], baseline["volume_mm3"] - 12)
|
|
|
|
def test_up_to_body_extent_uses_a_uniquely_resolved_body_record(self) -> None:
|
|
from cdsl_engine.runtime import rebuild_cdsl
|
|
|
|
base = self._base_block()
|
|
base["geometry"]["sketches"].append({
|
|
"id": "cut", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 1},
|
|
})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
baseline = rebuild_cdsl(base, root / "baseline.step")
|
|
body = next(item for item in baseline["topology_records"] if item["kind"] == "body")
|
|
cdsl = deepcopy(base)
|
|
cdsl["features"].append({
|
|
"id": "cut_to_body", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
|
"params": {"distance_mm": 0, "end_condition": {
|
|
"type": "up_to_body", "reference": {
|
|
"kind": "body", "owner_feature_id": "base_add", "geometry": body["geometry"],
|
|
},
|
|
}},
|
|
"sketch_id": "cut",
|
|
})
|
|
result = rebuild_cdsl(cdsl, root / "up-to-body.step")
|
|
self.assertAlmostEqual(result["volume_mm3"], 1000 - 10 * 3.141592653589793, places=5)
|
|
|
|
def test_selector_dependent_extent_without_reference_is_preflight_blocked(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"][0]["params"]["end_condition"] = {"type": "up_to_surface"}
|
|
analysis = analyze_cdsl(cdsl)
|
|
result = analysis.feature_results[0]
|
|
self.assertIn("missing_extent_reference", [item.code for item in result.blockers])
|
|
|
|
def test_hole_wizard_unsupported_extent_is_preflight_blocked(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].append({
|
|
"id": "hole", "atomic_id": "hole_wizard", "depends_on": ["base_add"],
|
|
"params": {
|
|
"hole_type": "plain", "diameter_mm": 2, "depth_mm": 2,
|
|
"positions": [{"mm": [0, 0, 0]}], "host_face": {"kind": "face"},
|
|
"end_condition": {"type": "up_to_surface", "solidworks_code": 0},
|
|
},
|
|
})
|
|
analysis = analyze_cdsl(cdsl)
|
|
result = next(item for item in analysis.feature_results if item.feature_id == "hole")
|
|
self.assertIn("unsupported_hole_extent", [item.code for item in result.blockers])
|
|
|
|
def test_legacy_hole_atomics_have_the_same_host_and_shape_preflight_contract(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = self._base_block()
|
|
cdsl["features"].append({
|
|
"id": "hole", "atomic_id": "hole_counterbore", "depends_on": ["base_add"], "sketch_id": "base",
|
|
"params": {
|
|
"diameter_mm": 2, "depth_mm": 3, "positions": [{"mm": [0, 0, 0]}],
|
|
"counterbore_diameter_mm": 2, "counterbore_depth_mm": 1,
|
|
},
|
|
})
|
|
analysis = analyze_cdsl(cdsl)
|
|
result = next(item for item in analysis.feature_results if item.feature_id == "hole")
|
|
codes = {item.code for item in result.blockers}
|
|
self.assertIn("missing_host_face", codes)
|
|
self.assertIn("invalid_hole_spec", codes)
|
|
|
|
def test_body_mutation_without_a_preceding_solid_is_preflight_blocked(self) -> None:
|
|
from cdsl_engine.runtime import analyze_cdsl
|
|
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "no-body",
|
|
"meta": {"unit": "mm"}, "geometry": {"sketches": []},
|
|
"features": [{
|
|
"id": "hole", "atomic_id": "hole_wizard", "depends_on": [],
|
|
"params": {
|
|
"hole_type": "plain", "diameter_mm": 2, "depth_mm": 3,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0}, "positions": [{"mm": [0, 0, 0]}],
|
|
"host_face": {"frame": _workplane()},
|
|
},
|
|
}],
|
|
}
|
|
result = analyze_cdsl(cdsl).feature_results[0]
|
|
self.assertIn("missing_active_body", [item.code for item in result.blockers])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|