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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user