feat(selector): 增加离线候选遍历与严格回放验证 Demo

- 新增 selector_candidate_demo,移除 provenance intent 后枚举候选 selector
- 对候选分支执行有界重建与严格 STEP 比较
- 仅在候选遍历完整且唯一 strict 通过时生成 selector 映射记录
- 增加 selector 候选搜索、预算限制和记录生成的测试
- 保持生产 selector resolver 不受 Demo 逻辑影响
- 更新 CADFS 能力台账,记录 IMPRINT 派生 profile 的 lineage selector 缺口
This commit is contained in:
2026-09-10 15:12:57 +08:00
parent d8fc2a9207
commit 994d06aaea
36 changed files with 9268 additions and 753 deletions
+552 -24
View File
@@ -395,6 +395,23 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
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
@@ -930,51 +947,131 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
source = object()
exact_result = object()
geometrically_similar = object()
registry.replace_body_topology("base", "body:base", [
TopologyRecord("base:face", "face", "base", "body:base", {"center_mm": [0, 0, 0]}, source),
])
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("transform", (
TopologyDeltaRelation("modified", "face", source, (exact_result,)),
)))
], 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", "stable_id": "base:face",
"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"},
"derivation_policy": {"allowed": ["continuation"], "multiplicity": "one"},
"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, "continuation")
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()
registry.replace_body_topology("base", "body:base", [
TopologyRecord("base:edge", "edge", "base", "body:base", {"center_mm": [0, 0, 0]}, source),
])
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", "edge", source, (first, second)),
)))
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", "stable_id": "base:edge",
"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"},
"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",
},
@@ -987,21 +1084,34 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
source = object()
first = object()
second = object()
registry.replace_body_topology("base", "body:base", [
TopologyRecord("base:edge", "edge", "base", "body:base", {}, source),
])
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", "edge", source, (first, second)),
)))
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", "stable_id": "base:edge",
"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"},
"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",
},
@@ -1010,6 +1120,75 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
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()
@@ -1022,6 +1201,138 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
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()
@@ -1177,6 +1488,124 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "requires a preceding body feature"):
validate_semantic_cdsl(invalid_shell_target)
def test_up_to_surface_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_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 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 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
@@ -2303,6 +2732,40 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
for item in downstream["selector_resolution"]
))
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_history_falls_back_for_hollow_profiles(self) -> None:
from build123d import Edge, Face, Plane, Vector, Wire
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
@@ -2572,6 +3035,71 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
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_requires_a_sole_live_member_before_execution(self) -> None:
from cdsl_engine.runtime import analyze_cdsl
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",
},
})
analysis = analyze_cdsl(cdsl)
result = analysis.feature_results[-1]
self.assertFalse(result.executable)
self.assertIn("hole_scope_body_ambiguous", [blocker.code for blocker in result.blockers])
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