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:
@@ -6,7 +6,12 @@ from pathlib import Path
|
||||
from cadfs_to_cdsl.compare import compare_steps
|
||||
from cadfs_to_cdsl.featurescript_parser import parse_featurescript
|
||||
from cadfs_to_cdsl.ir import Call
|
||||
from cadfs_to_cdsl.lowering import _arc, _contours, _global, _number, _record_single_body_successor, lower_model
|
||||
from cadfs_to_cdsl.lowering import (
|
||||
UnsupportedCapability, _arc, _contours, _curve_endpoint_tangent,
|
||||
_direct_hole_location, _direct_line_angle_axis, _direct_sketch_wire_path,
|
||||
_direct_boolean_intersection_selector, _direct_primary_cut_intersection_selector, _global, _number,
|
||||
_record_single_body_successor, lower_model,
|
||||
)
|
||||
from cadfs_to_cdsl.pipeline import compare_one, convert_one, rebuild_one
|
||||
from cadfs_to_cdsl.rebuild import rebuild_candidate
|
||||
from cadfs_to_cdsl.dataset import Sample
|
||||
@@ -14,7 +19,92 @@ from cadfs_to_cdsl.dataset import scan_dataset
|
||||
from cadfs_to_cdsl.tests.test_parser import SOURCE, TRANSFORM_SOURCE
|
||||
|
||||
|
||||
LINE_ANGLE_TWO_ENTITY_SOURCE = '''FeatureScript 1511;
|
||||
import(path : "onshape/std/geometry.fs", version : "1511.0");
|
||||
const mm = millimeter;
|
||||
const VERTEX = EntityType.VERTEX;
|
||||
const EDGE = EntityType.EDGE;
|
||||
const FACE = EntityType.FACE;
|
||||
function v(x, y){return vector(x, y);}
|
||||
annotation { "Feature Type Name" : "Feature" }
|
||||
export const myFeature = defineFeature(function(context is Context, id is Id, definition is map)
|
||||
precondition{}
|
||||
{
|
||||
{ var Q0; Q0=qCreatedBy(makeId("Top.planeOp"),FACE); var sketch=newSketch(context,id+"F0",{"sketchPlane":qUnion([Q0])}); skLineSegment(sketch,"E0",{"start":v(0,0)*mm,"end":v(10,0)*mm}); skLineSegment(sketch,"E1",{"start":v(0,0)*mm,"end":v(0,10)*mm}); skLineSegment(sketch,"E2",{"start":v(0,5)*mm,"end":v(10,5)*mm}); skPoint(sketch,"P0",{"position":v(0,10)*mm}); skPoint(sketch,"P1",{"position":v(10,0)*mm}); skSolve(sketch); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=sQuery(id+"F0.wireOp",EDGE,"E1"); cPlane(context,id+"F1",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=sQuery(id+"F0.wireOp",EDGE,"E2"); cPlane(context,id+"F2",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=sQuery(id+"F0.wireOp",VERTEX,"P0"); cPlane(context,id+"F3",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",VERTEX,"P0"); var Q1; Q1=sQuery(id+"F0.wireOp",EDGE,"E0"); cPlane(context,id+"F4",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":90*degree,"oppositeDirection":true}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=sQuery(id+"F0.wireOp",VERTEX,"P1"); cPlane(context,id+"F5",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=qCreatedBy(id+"F1.planeOp",FACE); cPlane(context,id+"F6",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
}'''
|
||||
|
||||
|
||||
class LoweringTests(unittest.TestCase):
|
||||
def test_direct_sketch_wire_path_accepts_only_one_nonconstruction_line_or_bspline(self):
|
||||
plane = {"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0]}
|
||||
line = {"type": "line", "start": [0.0, 0.0], "end": [10.0, 0.0], "source_entity_id": "E0"}
|
||||
query = Call("qUnion", [[
|
||||
Call("qConstructionFilter", [
|
||||
Call("qBodyType", [
|
||||
Call("qCreatedBy", [Call("__binary__", ["id", "+", "F0"]), "EDGE"]),
|
||||
"BodyType.WIRE",
|
||||
]),
|
||||
"ConstructionObject.NO",
|
||||
]),
|
||||
]])
|
||||
sketches = {"F0": {"workplane": plane}}
|
||||
entities = {"F0": {"E0": line, "axis": {"type": "line", "start": [0.0, 0.0], "end": [0.0, 5.0], "construction": True}}}
|
||||
|
||||
self.assertEqual(
|
||||
_direct_sketch_wire_path(
|
||||
query, sketches, entities, featurescript_version="1511",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
),
|
||||
("F0", "E0", line, sketches["F0"]),
|
||||
)
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
query, sketches, entities, featurescript_version="1512",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
))
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
query, sketches, entities, featurescript_version="1511",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1512.0",
|
||||
))
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
query, sketches, {"F0": {"E0": line, "E1": {**line, "source_entity_id": "E1"}}},
|
||||
featurescript_version="1511", standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
))
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
Call("qBodyType", [Call("qCreatedBy", ["F0", "EDGE"]), "BodyType.WIRE"]),
|
||||
sketches, entities, featurescript_version="1511",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
))
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
query.args[0][0], sketches, entities, featurescript_version="1511",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
))
|
||||
|
||||
def test_direct_hole_location_accepts_only_original_sketch_vertices(self):
|
||||
plane = {"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0]}
|
||||
sketches = {"F2": {"workplane": plane}}
|
||||
entities = {"F2": {
|
||||
"P0": {"type": "point", "point": [1.0, 2.0]},
|
||||
"C0": {"type": "circle", "center": [3.0, 4.0], "radius_mm": 1.0},
|
||||
"L0": {"type": "line", "start": [5.0, 6.0], "end": [7.0, 8.0]},
|
||||
"A0": {"type": "arc", "start": [9.0, 10.0], "end": [11.0, 12.0]},
|
||||
}}
|
||||
|
||||
def vertex(token: str) -> Call:
|
||||
return Call("sQuery", ["F2.wireOp", "VERTEX", token])
|
||||
|
||||
self.assertEqual(_direct_hole_location(vertex("P0"), sketches, entities), ([1.0, 2.0, 0.0], plane))
|
||||
self.assertEqual(_direct_hole_location(vertex("C0.center"), sketches, entities), ([3.0, 4.0, 0.0], plane))
|
||||
self.assertEqual(_direct_hole_location(vertex("L0.end"), sketches, entities), ([7.0, 8.0, 0.0], plane))
|
||||
self.assertEqual(_direct_hole_location(vertex("A0.start"), sketches, entities), ([9.0, 10.0, 0.0], plane))
|
||||
self.assertIsNone(_direct_hole_location(Call("qAdjacent", [vertex("C0.center")]), sketches, entities))
|
||||
self.assertIsNone(_direct_hole_location(vertex("C0.center.copy"), sketches, entities))
|
||||
|
||||
def test_strict_comparison_status_is_not_labeled_approximate(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp); directory = output / "samples" / "strict-status"; directory.mkdir(parents=True)
|
||||
@@ -53,7 +143,7 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(arc["center"][1], 0.5)
|
||||
self.assertAlmostEqual(arc["radius_mm"], math.hypot(2.5, 0.5))
|
||||
|
||||
def test_direct_extrude_and_two_section_loft_swept_edges_bind_for_fillet(self):
|
||||
def test_direct_extrude_lineage_succeeds_and_unregistered_loft_query_preserves_prefix(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
cases = {
|
||||
"00000715": ("f_F2", 2, True),
|
||||
@@ -67,14 +157,37 @@ class LoweringTests(unittest.TestCase):
|
||||
fillet = next(item for item in result.cdsl["features"] if item["id"] == fillet_id)
|
||||
self.assertEqual(len(fillet["selectors"]), selector_count)
|
||||
self.assertTrue(all(item["owner_feature_id"] == fillet["depends_on"][0] for item in fillet["selectors"]))
|
||||
self.assertTrue(all("bbox_mm" in item["geometry"] for item in fillet["selectors"]))
|
||||
self.assertEqual(["curve_type" in item["geometry"] for item in fillet["selectors"]], [line_type] * selector_count)
|
||||
if sample_id == "00000715":
|
||||
self.assertTrue(all("geometry" not in item for item in fillet["selectors"]))
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
and item["selector_intent"]["derivation_policy"] == {"allowed": ["boundary"], "multiplicity": "one"}
|
||||
and len(item["selector_intent"]["source_entities"]) == 2
|
||||
for item in fillet["selectors"]
|
||||
))
|
||||
else:
|
||||
self.assertTrue(all("bbox_mm" in item["geometry"] for item in fillet["selectors"]))
|
||||
self.assertEqual(["curve_type" in item["geometry"] for item in fillet["selectors"]], [line_type] * selector_count)
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
and item["selector_intent"]["evidence"] == "feature_script_query"
|
||||
and item["selector_intent"]["derivation_policy"] == {"allowed": ["continuation"], "multiplicity": "none"}
|
||||
and item["selector_intent"]["source_query"]["featurescript_version"] == "1793"
|
||||
for item in fillet["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = Path(directory) / "rebuild.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
comparison = compare_steps(root / "step_abc" / sample_id[:4] / f"{sample_id}.step", rebuilt)
|
||||
self.assertTrue(comparison["strict"]["passed"])
|
||||
if sample_id == "00005267":
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F4: selector_query_unsupported during incremental replay")
|
||||
self.assertTrue(rebuilt.exists())
|
||||
self.assertEqual(outcome["last_executable_prefix"]["failed_feature_id"], "f_F4")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F3")
|
||||
else:
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
comparison = compare_steps(root / "step_abc" / sample_id[:4] / f"{sample_id}.step", rebuilt)
|
||||
self.assertTrue(comparison["strict"]["passed"])
|
||||
|
||||
def test_swept_edge_requires_one_direct_shared_source_endpoint(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -93,7 +206,7 @@ class LoweringTests(unittest.TestCase):
|
||||
"message": "swept edge source endpoint provenance is unsupported",
|
||||
}])
|
||||
|
||||
def test_direct_full_revolve_swept_circle_edges_bind_for_dressups(self):
|
||||
def test_full_revolve_swept_circle_edges_remain_deferred_with_prefixes(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
for sample_id, dressup_id, selector_count in (
|
||||
("00048326", "f_F3", 2),
|
||||
@@ -109,14 +222,17 @@ class LoweringTests(unittest.TestCase):
|
||||
selector["geometry"].get("curve_type") == "circle"
|
||||
and len(selector["geometry"].get("circle_center_mm") or []) == 3
|
||||
and selector["geometry"].get("radius_mm", 0) > 0
|
||||
and selector["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
and selector["selector_intent"]["evidence"] == "feature_script_query"
|
||||
and selector["selector_intent"]["derivation_policy"]["multiplicity"] == "none"
|
||||
for selector in dressup["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = Path(directory) / "rebuild.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
comparison = compare_steps(root / "step_abc" / sample_id[:4] / f"{sample_id}.step", rebuilt)
|
||||
self.assertTrue(comparison["strict"]["passed"])
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], f"{dressup_id}: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["failed_feature_id"], dressup_id)
|
||||
|
||||
def test_full_revolve_swept_circle_requires_one_direct_shared_source_endpoint(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -135,7 +251,7 @@ class LoweringTests(unittest.TestCase):
|
||||
"message": "swept edge source endpoint provenance is unsupported",
|
||||
}, result.diagnostics)
|
||||
|
||||
def test_equivalent_imprint_profile_retains_direct_full_revolve_swept_edges(self):
|
||||
def test_equivalent_imprint_profile_preserves_but_does_not_bind_full_revolve_queries(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
source = root / "featurescript_rp/0011/00112257.txt"
|
||||
result = lower_model(parse_featurescript(source.read_text(), "00112257"), {})
|
||||
@@ -144,17 +260,16 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertEqual(len(fillet["selectors"]), 5)
|
||||
self.assertTrue(all(
|
||||
selector["geometry"].get("curve_type") == "circle"
|
||||
and selector["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
and selector["selector_intent"]["derivation_policy"]["multiplicity"] == "none"
|
||||
for selector in fillet["selectors"]
|
||||
))
|
||||
|
||||
from cadfs_to_cdsl.selector_binding import bind_candidate_selectors
|
||||
prefix = dict(result.cdsl)
|
||||
prefix["features"] = result.cdsl["features"][:2]
|
||||
bound, evidence = bind_candidate_selectors(prefix)
|
||||
selectors = next(item for item in bound["features"] if item["id"] == "f_F2")["selectors"]
|
||||
self.assertEqual(len(evidence), 1)
|
||||
self.assertEqual(len(selectors), 5)
|
||||
self.assertTrue(all(selector.get("snapshot_id") for selector in selectors))
|
||||
with self.assertRaisesRegex(ValueError, "f_F2: selector_query_unsupported after prefix rebuild"):
|
||||
bind_candidate_selectors(prefix)
|
||||
|
||||
def test_changed_imprint_profile_does_not_inherit_full_revolve_source_edges(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -689,6 +804,30 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertEqual(end["offset_mm"], 3.0)
|
||||
self.assertEqual(end["reference"]["kind"], "body")
|
||||
self.assertEqual(end["reference"]["owner_feature_id"], "f_F1")
|
||||
self.assertEqual(end["reference"]["source"], "runtime_snapshot")
|
||||
self.assertNotIn("stable_id", end["reference"])
|
||||
self.assertEqual(end["reference"]["selector_intent"]["query_family"], "SWEPT_BODY")
|
||||
self.assertEqual(end["reference"]["selector_intent"]["evidence"], "active_body_member")
|
||||
self.assertEqual(
|
||||
end["reference"]["selector_intent"]["derivation_policy"],
|
||||
{"allowed": ["boundary"], "multiplicity": "one"},
|
||||
)
|
||||
fillet = next(item for item in result.cdsl["features"] if item["id"] == "f_F4")
|
||||
self.assertEqual(len(fillet["selectors"]), 1)
|
||||
self.assertEqual(fillet["selectors"][0]["selector_intent"]["query_family"], "CAP_EDGE")
|
||||
self.assertEqual(
|
||||
fillet["selectors"][0]["selector_intent"]["derivation_policy"],
|
||||
{"allowed": ["boundary", "continuation"], "multiplicity": "one"},
|
||||
)
|
||||
from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl
|
||||
|
||||
mixed_evidence = deepcopy(result.cdsl)
|
||||
mixed_reference = next(
|
||||
item for item in mixed_evidence["features"] if item["id"] == "f_F3"
|
||||
)["params"]["end_condition"]["reference"]
|
||||
mixed_reference["stable_id"] = "body:f_F1"
|
||||
with self.assertRaisesRegex(ValueError, "invalid SWEPT_BODY member contract"):
|
||||
validate_semantic_cdsl(mixed_evidence)
|
||||
|
||||
def test_up_to_surface_extrude_captures_the_cap_face(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -700,9 +839,24 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertEqual(end["type"], "up_to_surface")
|
||||
self.assertEqual(end["reference"]["kind"], "face")
|
||||
self.assertEqual(end["reference"]["owner_feature_id"], "f_F1")
|
||||
self.assertEqual(end["reference"]["geometry"]["plane_offset_mm"], 0.0)
|
||||
self.assertEqual(end["reference"]["output_role"], "extrude.start")
|
||||
self.assertEqual(end["reference"]["source"], "runtime_snapshot")
|
||||
self.assertNotIn("geometry", end["reference"])
|
||||
self.assertEqual(end["reference"]["selector_intent"]["query_family"], "CAP_FACE")
|
||||
self.assertEqual(end["reference"]["selector_intent"]["evidence"], "operation_role")
|
||||
swept = next(item for item in result.cdsl["features"] if item["id"] == "f_F6")
|
||||
self.assertEqual(swept["params"]["end_condition"]["reference"]["geometry"]["radius_mm"], 24.3)
|
||||
swept_reference = swept["params"]["end_condition"]["reference"]
|
||||
self.assertEqual(swept_reference["selector_intent"]["query_family"], "SWEPT_FACE")
|
||||
self.assertEqual(
|
||||
swept_reference["selector_intent"]["derivation_policy"],
|
||||
{"allowed": ["boundary", "continuation"], "multiplicity": "one"},
|
||||
)
|
||||
self.assertEqual(swept_reference["selector_intent"]["evidence"], "kernel_history")
|
||||
self.assertEqual(swept_reference["selector_intent"]["source_entity"], {"sketch_id": "F0", "entity_id": "E0"})
|
||||
self.assertEqual(swept_reference["source"], "runtime_snapshot")
|
||||
self.assertNotIn("stable_id", swept_reference)
|
||||
self.assertNotIn("binding_feature_id", swept_reference)
|
||||
self.assertNotIn("geometry", swept_reference)
|
||||
mirror = next(item for item in result.cdsl["features"] if item["id"] == "f_F18")
|
||||
self.assertTrue(mirror["params"]["mirror_current_body"])
|
||||
|
||||
@@ -756,6 +910,160 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(plane["origin_mm"][0], -12.15)
|
||||
self.assertAlmostEqual(plane["origin_mm"][1], -24.3 * math.sqrt(3) / 2)
|
||||
|
||||
def test_line_angle_direct_circle_uses_the_featurescript_circle_axis(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0003/00030209.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00030209"), {})
|
||||
self.assertEqual(result.status, "converted_partial")
|
||||
planes = {
|
||||
item["id"]: item["params"]["plane"]
|
||||
for item in result.cdsl["features"]
|
||||
if item["atomic_id"] == "reference_plane"
|
||||
}
|
||||
expected_centers = {
|
||||
"f_F1": [45.78, 0.0, 26.2],
|
||||
"f_F2": [13.9, 0.0, 26.5],
|
||||
"f_F4": [-17.08, 0.0, 23.46],
|
||||
"f_F5": [-44.4, 0.0, 19.8],
|
||||
}
|
||||
self.assertEqual(set(planes), set(expected_centers))
|
||||
for feature_id, center in expected_centers.items():
|
||||
with self.subTest(feature_id=feature_id):
|
||||
plane = planes[feature_id]
|
||||
self.assertEqual(plane["origin_mm"], center)
|
||||
self.assertEqual(plane["x_dir"], [0.0, -1.0, 0.0])
|
||||
self.assertAlmostEqual(plane["normal"][0], 1.0)
|
||||
self.assertAlmostEqual(plane["normal"][1], 0.0)
|
||||
self.assertAlmostEqual(plane["normal"][2], 0.0)
|
||||
self.assertFalse(any(
|
||||
diagnostic.get("operation") == "cPlane"
|
||||
for diagnostic in result.diagnostics
|
||||
))
|
||||
|
||||
opposite_source = feature.read_text().replace(
|
||||
'"angle" : 90 * degree, "width"',
|
||||
'"angle" : 90 * degree, "oppositeDirection" : true, "width"',
|
||||
1,
|
||||
)
|
||||
opposite = lower_model(parse_featurescript(opposite_source, "line-angle-circle-opposite"), {})
|
||||
opposite_plane = next(
|
||||
item for item in opposite.cdsl["features"] if item["id"] == "f_F1"
|
||||
)["params"]["plane"]
|
||||
self.assertAlmostEqual(opposite_plane["normal"][0], -1.0)
|
||||
self.assertAlmostEqual(opposite_plane["normal"][1], 0.0)
|
||||
self.assertAlmostEqual(opposite_plane["normal"][2], 0.0)
|
||||
|
||||
def test_line_angle_two_entities_follow_axis_plane_order_and_swap_rules(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
cases = {
|
||||
# F8 selects Right plane first, then the direct F7 line. The
|
||||
# standard library must swap them before calculating the frame.
|
||||
"00298026": ("f_F8", [0.0, -1.0, 0.0], [0.43837114678907757, 0.0, -0.898794046299167]),
|
||||
# F1 supplies the direct line first and the default plane second.
|
||||
"00586156": ("f_F1", [0.0, 0.0, 1.0], [-math.sqrt(0.5), -math.sqrt(0.5), 0.0]),
|
||||
}
|
||||
for sample_id, (feature_id, expected_x, expected_normal) in cases.items():
|
||||
with self.subTest(sample_id=sample_id):
|
||||
source = root / "featurescript_rp" / sample_id[:4] / f"{sample_id}.txt"
|
||||
result = lower_model(parse_featurescript(source.read_text(), sample_id), {})
|
||||
plane = next(item for item in result.cdsl["features"] if item["id"] == feature_id)["params"]["plane"]
|
||||
self.assertEqual(plane["x_dir"], expected_x)
|
||||
for actual, expected in zip(plane["normal"], expected_normal):
|
||||
self.assertAlmostEqual(actual, expected)
|
||||
self.assertFalse(any(
|
||||
diagnostic.get("operation") == "cPlane"
|
||||
and diagnostic.get("feature_id") == feature_id.removeprefix("f_")
|
||||
for diagnostic in result.diagnostics
|
||||
))
|
||||
|
||||
def test_line_angle_two_direct_entities_cover_axis_point_and_degenerate_contracts(self):
|
||||
result = lower_model(parse_featurescript(LINE_ANGLE_TWO_ENTITY_SOURCE, "line-angle-two-entities"), {})
|
||||
self.assertEqual(result.status, "converted_partial")
|
||||
planes = {
|
||||
item["id"]: item["params"]["plane"]
|
||||
for item in result.cdsl["features"]
|
||||
if item["atomic_id"] == "reference_plane"
|
||||
}
|
||||
# Nonparallel axes, parallel offset axes, and an axis plus a direct
|
||||
# point each use the exact source direction, rather than a sketch
|
||||
# workplane frame.
|
||||
for feature_id in ("f_F1", "f_F2", "f_F3"):
|
||||
self.assertEqual(planes[feature_id]["x_dir"], [1.0, 0.0, 0.0])
|
||||
self.assertEqual(planes[feature_id]["normal"], [0.0, 0.0, 1.0])
|
||||
# A point selected before its axis triggers FeatureScript's swap. The
|
||||
# signed angle also follows oppositeDirection.
|
||||
self.assertEqual(planes["f_F4"]["x_dir"], [1.0, 0.0, 0.0])
|
||||
self.assertAlmostEqual(planes["f_F4"]["normal"][0], 0.0)
|
||||
self.assertAlmostEqual(planes["f_F4"]["normal"][1], 1.0)
|
||||
self.assertAlmostEqual(planes["f_F4"]["normal"][2], 0.0)
|
||||
# A cPlane output is represented as qCreatedBy(...planeOp, FACE),
|
||||
# not a sketch source. It is accepted only because F1 is an existing
|
||||
# reference-plane frame, and it supplies the true zero-angle side.
|
||||
self.assertEqual(planes["f_F6"]["x_dir"], [1.0, 0.0, 0.0])
|
||||
self.assertEqual(planes["f_F6"]["normal"], [0.0, 0.0, -1.0])
|
||||
self.assertIn({
|
||||
"code": "feature_deferred",
|
||||
"feature_id": "F5",
|
||||
"operation": "cPlane",
|
||||
"message": "line-angle reference selection is degenerate",
|
||||
}, result.diagnostics)
|
||||
|
||||
def test_line_angle_direct_axis_refuses_a_topology_derived_query(self):
|
||||
direct = Call("sQuery", ["F0.wireOp", "EDGE", "E0"], 1)
|
||||
derived = Call("makeQuery", [
|
||||
"F1.opExtrude", "CAP_EDGE", "EDGE",
|
||||
{"derivedFrom": direct, "isStart": False},
|
||||
], 2)
|
||||
sketch_by_source = {"F0": {"workplane": {
|
||||
"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0],
|
||||
}}}
|
||||
entity_by_sketch = {"F0": {"E0": {
|
||||
"type": "line", "start": [0.0, 0.0], "end": [10.0, 0.0], "source_entity_id": "E0",
|
||||
}}}
|
||||
self.assertIsNotNone(_direct_line_angle_axis(direct, sketch_by_source, entity_by_sketch))
|
||||
self.assertIsNone(_direct_line_angle_axis(derived, sketch_by_source, entity_by_sketch))
|
||||
|
||||
def test_line_angle_direct_axis_refuses_a_query_wrapper_and_keeps_the_prefix(self):
|
||||
direct = Call("sQuery", ["F0.wireOp", "EDGE", "E0"], 1)
|
||||
wrapped = Call("qAdjacent", [direct, "VERTEX", "EDGE"], 2)
|
||||
sketch_by_source = {"F0": {"workplane": {
|
||||
"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0],
|
||||
}}}
|
||||
entity_by_sketch = {"F0": {"E0": {
|
||||
"type": "line", "start": [0.0, 0.0], "end": [10.0, 0.0], "source_entity_id": "E0",
|
||||
}}}
|
||||
self.assertIsNone(_direct_line_angle_axis(wrapped, sketch_by_source, entity_by_sketch))
|
||||
|
||||
model = parse_featurescript(LINE_ANGLE_TWO_ENTITY_SOURCE, "line-angle-wrapped-axis")
|
||||
plane = next(item for item in model.features if item.feature_id == "F1")
|
||||
plane.params["entities"].args[0][0] = wrapped
|
||||
result = lower_model(model, {})
|
||||
self.assertIn({
|
||||
"code": "feature_deferred",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "line-angle reference selection is unsupported",
|
||||
}, result.diagnostics)
|
||||
self.assertIn("f_F2", {item["id"] for item in result.cdsl["features"]})
|
||||
|
||||
def test_line_angle_rejects_a_circle_center_vertex_as_an_axis(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0003/00030209.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
source = feature.read_text().replace(
|
||||
'Q0=sQuery(id+"F0.wireOp",EDGE,"E0");',
|
||||
'Q0=sQuery(id+"F0.wireOp",VERTEX,"E0.center");',
|
||||
1,
|
||||
)
|
||||
result = lower_model(parse_featurescript(source, "line-angle-circle-center"), {})
|
||||
self.assertIn({
|
||||
"code": "feature_deferred",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "line-angle reference selection is unsupported",
|
||||
}, result.diagnostics)
|
||||
|
||||
def test_open_nonconstruction_geometry_is_preserved_as_reference(self):
|
||||
source = SOURCE.replace('skSolve(sketch);', 'skLineSegment(sketch, "open", {"start":v(0, 0) * mm, "end":v(20, 0) * mm}); skSolve(sketch);', 1)
|
||||
result = lower_model(parse_featurescript(source, "open-profile"), {})
|
||||
@@ -931,7 +1239,7 @@ class LoweringTests(unittest.TestCase):
|
||||
[("outer", 25.5), ("inner", 19.0)],
|
||||
)
|
||||
|
||||
def test_mixed_extrude_rebuilds_surface_limited_chamfer_history(self):
|
||||
def test_mixed_extrude_preserves_prefix_when_cap_edge_chamfer_query_is_deferred(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0071/00710855.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
@@ -941,10 +1249,10 @@ class LoweringTests(unittest.TestCase):
|
||||
from cadfs_to_cdsl.rebuild import rebuild_candidate
|
||||
rebuilt = rebuild_candidate(result.cdsl, Path(tmp) / "rebuilt.step")
|
||||
|
||||
self.assertEqual(rebuilt["status"], "rebuilt")
|
||||
chamfer = next(item for item in rebuilt["result"]["feature_results"] if item["feature_id"] == "f_F10")
|
||||
self.assertIn("chamfer_surface_limited", [item["code"] for item in chamfer["diagnostics"]])
|
||||
self.assertAlmostEqual(rebuilt["result"]["volume_mm3"], 72577.33528261917, places=6)
|
||||
self.assertEqual(rebuilt["status"], "rebuild_failed")
|
||||
self.assertEqual(rebuilt["error"]["message"], "f_F10: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(rebuilt["last_executable_prefix"]["failed_feature_id"], "f_F10")
|
||||
self.assertEqual(rebuilt["last_executable_prefix"]["last_feature_id"], "f_F9")
|
||||
|
||||
def test_fit_spline_loft_lowers_to_executable_loft_add(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -1048,7 +1356,7 @@ class LoweringTests(unittest.TestCase):
|
||||
"kind": "face", "owner_feature_id": "f_F3", "output_role": "extrude.start",
|
||||
"source": "runtime_snapshot", "confidence": 1.0,
|
||||
})
|
||||
self.assertEqual(selector["selector_intent_version"], "1.0")
|
||||
self.assertNotIn("selector_intent_version", selector)
|
||||
self.assertEqual(selector["selector_intent"]["query_family"], "CAP_FACE")
|
||||
|
||||
from engine.cdsl_engine.runtime import rebuild_cdsl
|
||||
@@ -1130,21 +1438,19 @@ class LoweringTests(unittest.TestCase):
|
||||
})
|
||||
|
||||
# F9 rotates F4 only after F5/F7 have consumed it. It cannot be baked
|
||||
# into the earlier F4 sketch; the unsupported body continuation stays
|
||||
# explicit and F1--F7 remains an executable prefix.
|
||||
# into the earlier F4 sketch. The CAP_EDGE geometry is preserved for
|
||||
# diagnostics, but no source-qualified cap-edge lineage exists for
|
||||
# this two-sided profile contract.
|
||||
prefix = deepcopy(result.cdsl)
|
||||
prefix["features"] = [
|
||||
item for item in prefix["features"]
|
||||
if item["id"] in {"f_F1", "f_F2", "f_F4", "f_F5", "f_F7"}
|
||||
]
|
||||
from engine.cdsl_engine.runtime import rebuild_cdsl
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = rebuild_cdsl(prefix, Path(directory) / "two-sided-cap-prefix.step")
|
||||
self.assertFalse(rebuilt["runtime_diagnostics"])
|
||||
self.assertEqual(
|
||||
[item["feature_id"] for item in rebuilt["feature_results"]],
|
||||
["f_F1", "f_F2", "f_F4", "f_F5", "f_F7"],
|
||||
)
|
||||
rebuilt = rebuild_candidate(prefix, Path(directory) / "two-sided-cap-prefix.step")
|
||||
self.assertEqual(rebuilt["status"], "rebuild_failed")
|
||||
self.assertEqual(rebuilt["error"]["message"], "f_F7: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(rebuilt["last_executable_prefix"]["last_feature_id"], "f_F5")
|
||||
|
||||
from engine.cdsl_engine.runtime import analyze_cdsl
|
||||
f9 = next(item for item in analyze_cdsl(result.cdsl).as_dict()["feature_results"] if item["feature_id"] == "f_F9")
|
||||
@@ -1338,6 +1644,41 @@ class LoweringTests(unittest.TestCase):
|
||||
["E14", "E14"],
|
||||
)
|
||||
|
||||
def test_direct_imprint_profile_preserves_selector_intent_and_f1_checkpoint(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0035/00354246.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00354246"), {})
|
||||
self.assertEqual(result.status, "converted_complete")
|
||||
features = {item["id"]: item for item in result.cdsl["features"]}
|
||||
sketches = {item["id"]: item for item in result.cdsl["geometry"]["sketches"]}
|
||||
profile = sketches[features["f_F1"]["sketch_id"]]["profile"]
|
||||
self.assertEqual(profile["type"], "planar_imprint")
|
||||
self.assertEqual(profile["selections"], [
|
||||
{"source_entity_id": "E6.MirrorCS", "face_side": -1.0},
|
||||
{"source_entity_id": "E0.MirrorCS", "face_side": 1.0},
|
||||
])
|
||||
|
||||
selectors = features["f_F2"]["selectors"]
|
||||
self.assertEqual(len(selectors), 7)
|
||||
self.assertTrue(all(
|
||||
selector["selector_intent"]["query_family"] == "SWEPT_FACE"
|
||||
and selector["selector_intent"]["derivation_policy"] == {
|
||||
"allowed": ["continuation"], "multiplicity": "none",
|
||||
}
|
||||
for selector in selectors
|
||||
))
|
||||
self.assertTrue(any("geometry" not in selector for selector in selectors))
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = Path(directory) / "rebuild.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F2: selector_query_unsupported during incremental replay")
|
||||
self.assertTrue(rebuilt.exists())
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F1")
|
||||
|
||||
def test_direct_translation_transform_updates_the_source_revolve(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0011/00111611.txt"
|
||||
@@ -1512,6 +1853,121 @@ class LoweringTests(unittest.TestCase):
|
||||
diagnostics = [item for item in result.diagnostics if item.get("operation") == "cPlane"]
|
||||
self.assertFalse(diagnostics)
|
||||
|
||||
def test_curve_point_plane_uses_an_explicit_bspline_endpoint_tangent(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0082/00827798.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00827798"), {})
|
||||
plane = next(item for item in result.cdsl["features"] if item["id"] == "f_F1")
|
||||
self.assertEqual(plane["atomic_id"], "reference_plane")
|
||||
self.assertFalse(any(
|
||||
item.get("operation") == "cPlane" and item.get("feature_id") == "F1"
|
||||
for item in result.diagnostics
|
||||
))
|
||||
expected = [107.86, 0.0, -176.04]
|
||||
magnitude = math.sqrt(sum(value * value for value in expected))
|
||||
for actual, value in zip(plane["params"]["plane"]["normal"], expected):
|
||||
self.assertAlmostEqual(actual, value / magnitude)
|
||||
|
||||
second_feature = root / "featurescript_rp/0084/00845891.txt"
|
||||
if second_feature.exists():
|
||||
second = lower_model(parse_featurescript(second_feature.read_text(), "00845891"), {})
|
||||
self.assertEqual(
|
||||
next(item for item in second.cdsl["features"] if item["id"] == "f_F3")["atomic_id"],
|
||||
"reference_plane",
|
||||
)
|
||||
self.assertFalse(any(
|
||||
item.get("operation") == "cPlane" and item.get("feature_id") == "F3"
|
||||
for item in second.diagnostics
|
||||
))
|
||||
|
||||
without_end_derivative = feature.read_text().replace(
|
||||
', "endDerivative": vector(107.86, -176.04) * mm', "", 1,
|
||||
)
|
||||
rejected = lower_model(parse_featurescript(without_end_derivative, "00827798-no-end-derivative"), {})
|
||||
self.assertIn({
|
||||
"code": "unsupported_engine_capability",
|
||||
"capability": "reference_plane:curve_point",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "CURVE_POINT B-spline endpoint has no exported tangent",
|
||||
}, rejected.diagnostics)
|
||||
|
||||
def test_curve_endpoint_tangent_uses_arc_orientation_without_a_chord_fallback(self):
|
||||
arc = {
|
||||
"type": "arc", "start": [1.0, 0.0], "end": [0.0, 1.0],
|
||||
"center": [0.0, 0.0], "clockwise": False,
|
||||
}
|
||||
self.assertEqual(_curve_endpoint_tangent(arc, "start"), [0.0, 1.0])
|
||||
self.assertEqual(_curve_endpoint_tangent(arc, "end"), [-1.0, 0.0])
|
||||
with self.assertRaisesRegex(UnsupportedCapability, "direct source-curve endpoint"):
|
||||
_curve_endpoint_tangent(arc, "internal")
|
||||
|
||||
def test_curve_point_plane_uses_exact_bspline_interpolation_vertex_tangents(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
cases = {
|
||||
"00456498": ("f_F1", "F1", [141.23, 0.0, -13.99]),
|
||||
"00812557": ("f_F2", "F2", [0.0, 191.15, -139.74]),
|
||||
}
|
||||
for sample_id, (cdsl_feature_id, source_feature_id, expected) in cases.items():
|
||||
with self.subTest(sample_id=sample_id):
|
||||
source = root / "featurescript_rp" / sample_id[:4] / f"{sample_id}.txt"
|
||||
if not source.exists():
|
||||
self.skipTest("CADFS sample is not installed")
|
||||
result = lower_model(parse_featurescript(source.read_text(), sample_id), {})
|
||||
plane = next(item for item in result.cdsl["features"] if item["id"] == cdsl_feature_id)
|
||||
self.assertEqual(plane["atomic_id"], "reference_plane")
|
||||
magnitude = math.sqrt(sum(value * value for value in expected))
|
||||
for actual, value in zip(plane["params"]["plane"]["normal"], expected):
|
||||
self.assertAlmostEqual(actual, value / magnitude, places=8)
|
||||
self.assertFalse(any(
|
||||
item.get("operation") == "cPlane" and item.get("feature_id") == source_feature_id
|
||||
for item in result.diagnostics
|
||||
))
|
||||
|
||||
no_derivatives = (root / "featurescript_rp/0045/00456498.txt").read_text().replace(
|
||||
', "startDerivative": vector(0, 64.52) * mm, "endDerivative": vector(141.23, -13.99) * mm', "", 1,
|
||||
)
|
||||
rejected = lower_model(parse_featurescript(no_derivatives, "00456498-no-derivatives"), {})
|
||||
self.assertIn({
|
||||
"code": "unsupported_engine_capability",
|
||||
"capability": "reference_plane:curve_point",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "CURVE_POINT B-spline interpolation point has no complete source interpolation data",
|
||||
}, rejected.diagnostics)
|
||||
|
||||
derived = (root / "featurescript_rp/0045/00456498.txt").read_text().replace(
|
||||
'"E0.2.internal"', '"E0.2.endSnap0"', 1,
|
||||
)
|
||||
rejected_derived = lower_model(parse_featurescript(derived, "00456498-derived-vertex"), {})
|
||||
self.assertIn({
|
||||
"code": "unsupported_engine_capability",
|
||||
"capability": "reference_plane:curve_point",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "CURVE_POINT point must name a direct source-curve endpoint or B-spline interpolation vertex",
|
||||
}, rejected_derived.diagnostics)
|
||||
|
||||
def test_curve_point_plane_uses_a_direct_arc_endpoint_tangent(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0005/00057273.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
source = feature.read_text().replace(
|
||||
'EDGE,"E1"', 'EDGE,"E0"', 1,
|
||||
).replace(
|
||||
'VERTEX,"E1.end"', 'VERTEX,"E0.end"', 1,
|
||||
)
|
||||
result = lower_model(parse_featurescript(source, "00057273-arc-curve-point"), {})
|
||||
plane = next(item for item in result.cdsl["features"] if item["id"] == "f_F1")
|
||||
self.assertEqual(plane["atomic_id"], "reference_plane")
|
||||
self.assertEqual(plane["params"]["plane"]["origin_mm"], [-24.6, 0.0, 0.0])
|
||||
self.assertEqual(plane["params"]["plane"]["normal"], [0.0, -1.0, 0.0])
|
||||
self.assertFalse(any(
|
||||
item.get("operation") == "cPlane" and item.get("feature_id") == "F1"
|
||||
for item in result.diagnostics
|
||||
))
|
||||
|
||||
def test_offset_reference_plane_honors_opposite_direction_without_reversing_its_frame(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0077/00777619.txt"
|
||||
@@ -1571,7 +2027,7 @@ class LoweringTests(unittest.TestCase):
|
||||
))
|
||||
self.assertNotIn("F8", {item.get("feature_id") for item in result.diagnostics})
|
||||
|
||||
def test_shell_lowers_and_executes_direct_linear_extrude_swept_faces(self):
|
||||
def test_shell_resolves_direct_prism_swept_faces_through_kernel_lineage(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0059/00594348.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
@@ -1582,18 +2038,105 @@ class LoweringTests(unittest.TestCase):
|
||||
shell = result.cdsl["features"][shell_index]
|
||||
self.assertEqual(shell["atomic_id"], "shell")
|
||||
self.assertEqual([item["owner_feature_id"] for item in shell["selectors"]], ["f_F1", "f_F1"])
|
||||
self.assertTrue(all("normal" in item["geometry"] for item in shell["selectors"]))
|
||||
|
||||
prefix = deepcopy(result.cdsl)
|
||||
prefix["features"] = prefix["features"][:shell_index + 1]
|
||||
self.assertTrue(all("geometry" not in item and "stable_id" not in item for item in shell["selectors"]))
|
||||
self.assertTrue(all(item["selector_intent"]["query_family"] == "SWEPT_FACE" for item in shell["selectors"]))
|
||||
self.assertTrue(all(item["selector_intent"]["evidence"] == "kernel_history" for item in shell["selectors"]))
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["derivation_policy"] == {"allowed": ["boundary"], "multiplicity": "one"}
|
||||
for item in shell["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(prefix, Path(directory) / "swept-face-shell.step")
|
||||
outcome = rebuild_candidate(result.cdsl, Path(directory) / "swept-face-shell.step")
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
self.assertEqual(
|
||||
[item["status"] for item in outcome["result"]["feature_results"]],
|
||||
["executed", "executed"],
|
||||
resolutions = [
|
||||
item for item in outcome["result"]["selector_resolution"]
|
||||
if item["feature_id"] == "f_F2"
|
||||
]
|
||||
self.assertEqual(len(resolutions), 2)
|
||||
self.assertTrue(all(item["resolution_mode"] == "kernel_lineage" for item in resolutions))
|
||||
self.assertTrue(all(
|
||||
item["evidence"]["relations"][0]["source_kind"] == "edge"
|
||||
and item["evidence"]["relations"][0]["result_kind"] == "face"
|
||||
for item in resolutions
|
||||
))
|
||||
|
||||
def test_shell_resolves_immediate_direct_prism_cap_faces_through_output_roles(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
cases = (("00212904", "extrude.start"), ("00789939", "extrude.end"))
|
||||
for sample_id, output_role in cases:
|
||||
with self.subTest(sample_id=sample_id):
|
||||
feature = root / "featurescript_rp" / sample_id[:4] / f"{sample_id}.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
result = lower_model(parse_featurescript(feature.read_text(encoding="utf-8"), sample_id), {})
|
||||
self.assertEqual(result.status, "converted_complete")
|
||||
shell_index = next(index for index, item in enumerate(result.cdsl["features"]) if item["id"] == "f_F2")
|
||||
shell = result.cdsl["features"][shell_index]
|
||||
self.assertEqual(shell["atomic_id"], "shell")
|
||||
self.assertEqual(len(shell["selectors"]), 1)
|
||||
selector = shell["selectors"][0]
|
||||
self.assertEqual(selector["owner_feature_id"], "f_F1")
|
||||
self.assertEqual(selector["output_role"], output_role)
|
||||
self.assertEqual(selector["selector_intent"]["query_family"], "CAP_FACE")
|
||||
self.assertEqual(selector["selector_intent"]["evidence"], "operation_role")
|
||||
self.assertNotIn("stable_id", selector)
|
||||
self.assertNotIn("binding_feature_id", selector)
|
||||
self.assertNotIn("geometry", selector)
|
||||
|
||||
prefix = deepcopy(result.cdsl)
|
||||
prefix["features"] = prefix["features"][:shell_index + 1]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(prefix, Path(directory) / f"{sample_id}-cap-face-shell.step")
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
resolution = next(
|
||||
item for item in outcome["result"]["selector_resolution"]
|
||||
if item["feature_id"] == "f_F2"
|
||||
)
|
||||
self.assertEqual(resolution["status"], "resolved")
|
||||
self.assertEqual(resolution["resolution_mode"], "operation_role")
|
||||
self.assertEqual(resolution["selected"]["output_roles"], [output_role])
|
||||
|
||||
def test_shell_direct_prism_swept_face_rejects_unregistered_source_version(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0059/00594348.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
source = feature.read_text(encoding="utf-8").replace("FeatureScript 1511;", "FeatureScript 1793;", 1)
|
||||
result = lower_model(parse_featurescript(source, "00594348-version-1793"), {})
|
||||
shell = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["evidence"] == "feature_script_query"
|
||||
and item["selector_intent"]["derivation_policy"]["multiplicity"] == "none"
|
||||
for item in shell["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(result.cdsl, Path(directory) / "swept-face-shell-1793.step")
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F2: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F1")
|
||||
|
||||
def test_shell_direct_prism_swept_face_rejects_unregistered_standard_library_revision(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0059/00594348.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
source = feature.read_text(encoding="utf-8").replace(
|
||||
'import(path : "onshape/std/geometry.fs", version : "1511.0");',
|
||||
'import(path : "onshape/std/geometry.fs", version : "1512.0");',
|
||||
1,
|
||||
)
|
||||
self.assertFalse(outcome["result"].get("diagnostics", []))
|
||||
result = lower_model(parse_featurescript(source, "00594348-library-1512"), {})
|
||||
shell = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["source_query"]["featurescript_version"] == "1511"
|
||||
and item["selector_intent"]["source_query"]["standard_library"] == "onshape/std/geometry.fs"
|
||||
and item["selector_intent"]["source_query"]["standard_library_version"] == "1512.0"
|
||||
for item in shell["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(result.cdsl, Path(directory) / "swept-face-shell-library-1512.step")
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F2: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F1")
|
||||
|
||||
def test_shell_offset_face_requires_true_dependency_cap_evidence(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -1623,13 +2166,14 @@ class LoweringTests(unittest.TestCase):
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertTrue(rebuilt.exists())
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["type"], "RuntimeExecutionError")
|
||||
self.assertEqual(outcome["error"]["type"], "ValueError")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F5: selector_query_unsupported during incremental replay")
|
||||
self.assertIn("bound_cdsl", outcome)
|
||||
self.assertEqual(outcome["last_executable_prefix"]["failed_feature_id"], "f_F5")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F4")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["result"]["solid_count"], 1)
|
||||
|
||||
def test_pattern_copy_selector_binding_rebuilds_the_full_history(self):
|
||||
def test_pattern_copy_intersection_query_preserves_its_prefix_without_instance_geometry_binding(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0042/00423838.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
@@ -1639,14 +2183,15 @@ class LoweringTests(unittest.TestCase):
|
||||
rebuilt = Path(directory) / "selector-prefix.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertTrue(rebuilt.exists())
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
resolved = [
|
||||
item for item in outcome["result"]["selector_resolution"]
|
||||
if item["feature_id"] == "f_F7"
|
||||
and item["selector"].get("owner_feature_id") == "f_F4.c4.f_F1"
|
||||
]
|
||||
self.assertEqual(len(resolved), 2)
|
||||
self.assertTrue(all(item["status"] == "resolved" for item in resolved))
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F7: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F6")
|
||||
reference = next(item for item in result.cdsl["features"] if item["id"] == "f_F7")["params"]["end_condition"]["reference"]
|
||||
self.assertEqual(reference["selector_intent"]["query_family"], "INTERSECT")
|
||||
self.assertEqual(
|
||||
[item["selector_intent"]["query_family"] for item in reference["intersection_of"][:2]],
|
||||
["COPY", "COPY"],
|
||||
)
|
||||
|
||||
def test_shell_rejects_swept_faces_without_direct_linear_extrude_ownership(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -1698,6 +2243,10 @@ class LoweringTests(unittest.TestCase):
|
||||
|
||||
def test_cap_face_selector_retains_versioned_provenance_intent(self):
|
||||
source = SOURCE.replace(
|
||||
"FeatureScript 1511;",
|
||||
'FeatureScript 1511;\nimport(path : "onshape/std/geometry.fs", version : "1511.0");',
|
||||
1,
|
||||
).replace(
|
||||
'\n});\n',
|
||||
'\n extrude(context, id + "F2", {"entities":makeQuery(id + "F1.opExtrude", "CAP_FACE", FACE, {"isStart":false}), "depth":10 * mm});\n});\n',
|
||||
)
|
||||
@@ -1705,12 +2254,127 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertEqual(result.status, "converted_complete")
|
||||
selector = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")["selectors"][0]
|
||||
self.assertEqual(selector["selector_intent"]["query_family"], "CAP_FACE")
|
||||
self.assertEqual(selector["selector_intent_version"], "1.0")
|
||||
self.assertNotIn("selector_intent_version", selector)
|
||||
self.assertEqual(selector["selector_intent"]["source_query"]["featurescript_version"], "1511")
|
||||
self.assertEqual(selector["selector_intent"]["source_query"]["standard_library"], "onshape/std/geometry.fs")
|
||||
self.assertEqual(selector["selector_intent"]["source_query"]["standard_library_version"], "1511.0")
|
||||
self.assertEqual(selector["selector_intent"]["evidence"], "operation_role")
|
||||
from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl
|
||||
self.assertTrue(validate_semantic_cdsl(result.cdsl)["future_rebuild_ready"])
|
||||
|
||||
def test_direct_boolean_intersection_lowering_keeps_two_source_contract(self):
|
||||
base = lower_model(parse_featurescript(SOURCE, "intersection-intent"), {}).cdsl
|
||||
sketch = base["geometry"]["sketches"][0]
|
||||
direct = {**base["features"][0], "id": "f_F2", "name": "F2"}
|
||||
feature_by_id = {
|
||||
"f_F1": base["features"][0],
|
||||
"f_F2": direct,
|
||||
"f_F3": {
|
||||
"id": "f_F3", "atomic_id": "boolean_bodies",
|
||||
"params": {
|
||||
"operation": "subtract", "target_feature_ids": ["f_F1"],
|
||||
"tool_feature_ids": ["f_F2"], "keep_tools": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
cap = Call("makeQuery", ["F1.opExtrude", "CAP_FACE", "EntityType.FACE", {"isStart": False}])
|
||||
source_edge = Call("sQuery", ["F0.wireOp", "EntityType.EDGE", "E0"])
|
||||
swept = Call("makeQuery", [
|
||||
"F2.opExtrude", "SWEPT_FACE", "EntityType.FACE",
|
||||
{"disambiguationData": [Call("OSD", [[source_edge]])]},
|
||||
])
|
||||
query = Call("makeQuery", [
|
||||
"F3.boolean.opBoolean", "INTERSECT", "EntityType.EDGE", {"derivedFrom": [cap, swept]},
|
||||
])
|
||||
selector = _direct_boolean_intersection_selector(
|
||||
query,
|
||||
feature_by_id=feature_by_id,
|
||||
feature_frames={"F2": {"profile_source": "F0"}},
|
||||
sketch_by_source={"F0": sketch},
|
||||
sketches_by_id={sketch["id"]: sketch},
|
||||
entity_by_sketch={"F0": {"E0": {"type": "circle", "center": [0, 0], "radius_mm": 9.53}}},
|
||||
featurescript_version="1511",
|
||||
)
|
||||
|
||||
self.assertIsNotNone(selector)
|
||||
self.assertNotIn("stable_id", selector)
|
||||
intent = selector["selector_intent"]
|
||||
self.assertEqual(intent["derivation_policy"], {"allowed": ["intersection"], "multiplicity": "one"})
|
||||
self.assertEqual(intent["intersection_sources"], [
|
||||
{"query_family": "CAP_FACE", "owner_feature_id": "f_F1", "output_role": "extrude.end"},
|
||||
{"query_family": "SWEPT_FACE", "owner_feature_id": "f_F2", "source_entity": {"sketch_id": "F0", "entity_id": "E0"}},
|
||||
])
|
||||
|
||||
def test_direct_primary_cut_intersection_requires_no_unverified_disambiguation(self):
|
||||
base = lower_model(parse_featurescript(SOURCE, "primary-intersection-intent"), {}).cdsl
|
||||
target = base["features"][0]
|
||||
target_sketch = base["geometry"]["sketches"][0]
|
||||
tool_sketch = deepcopy(target_sketch)
|
||||
tool_sketch.update({"id": "sketch_F2", "source_sketch_id": "F2"})
|
||||
if tool_sketch["profile"].get("type") == "circle":
|
||||
tool_sketch["profile"]["source_entity_id"] = "E1"
|
||||
else:
|
||||
for contour in tool_sketch["profile"]["contours"]:
|
||||
for segment in contour["segments"]:
|
||||
segment["source_entity_id"] = "E1"
|
||||
tool = {
|
||||
"id": "f_F3", "atomic_id": "extrude_cut_blind", "sketch_id": "sketch_F2",
|
||||
"params": {"distance_mm": 10, "end_condition": {"type": "blind"}},
|
||||
}
|
||||
cap = Call("makeQuery", ["F1.opExtrude", "CAP_FACE", "EntityType.FACE", {"isStart": False}])
|
||||
tool_edge = Call("sQuery", ["F2.wireOp", "EntityType.EDGE", "E1"])
|
||||
swept = Call("makeQuery", [
|
||||
"F3.opExtrude", "SWEPT_FACE", "EntityType.FACE",
|
||||
{"disambiguationData": [Call("OSD", [[tool_edge]])]},
|
||||
])
|
||||
query = Call("makeQuery", [
|
||||
"F3.boolean.opBoolean", "INTERSECT", "EntityType.EDGE", {"derivedFrom": [cap, swept]},
|
||||
])
|
||||
kwargs = {
|
||||
"feature_by_id": {"f_F1": target, "f_F3": tool},
|
||||
"feature_frames": {"F3": {"profile_source": "F2"}},
|
||||
"sketch_by_source": {"F2": tool_sketch},
|
||||
"sketches_by_id": {tool_sketch["id"]: tool_sketch, target_sketch["id"]: target_sketch},
|
||||
"entity_by_sketch": {"F2": {"E1": {"type": "circle", "center": [0, 0], "radius_mm": 9.53}}},
|
||||
"previous": ["f_F1", "f_F3"],
|
||||
"featurescript_version": "1511",
|
||||
}
|
||||
selector = _direct_primary_cut_intersection_selector(query, **kwargs)
|
||||
|
||||
self.assertIsNotNone(selector)
|
||||
self.assertEqual(selector["owner_feature_id"], "f_F3")
|
||||
self.assertEqual(selector["selector_intent"]["intersection_sources"][1], {
|
||||
"query_family": "SWEPT_FACE", "owner_feature_id": "f_F3",
|
||||
"source_entity": {"sketch_id": "F2", "entity_id": "E1"},
|
||||
})
|
||||
deferred = Call("makeQuery", [
|
||||
"F3.boolean.opBoolean", "INTERSECT", "EntityType.EDGE",
|
||||
{"derivedFrom": [cap, swept], "disambiguationData": [Call("OD", [0.0])]},
|
||||
])
|
||||
self.assertIsNone(_direct_primary_cut_intersection_selector(deferred, **kwargs))
|
||||
|
||||
def test_deferred_primary_intersect_keeps_the_convertible_f3_checkpoint(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0002/00020311.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00020311"), {})
|
||||
f4 = next(item for item in result.cdsl["features"] if item["id"] == "f_F4")
|
||||
self.assertTrue(all(
|
||||
selector["kind"] == "edge"
|
||||
and selector["selector_intent"]["query_family"] == "INTERSECT"
|
||||
and selector["selector_intent"]["derivation_policy"]["multiplicity"] == "none"
|
||||
for selector in f4["selectors"]
|
||||
))
|
||||
from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl
|
||||
self.assertTrue(validate_semantic_cdsl(result.cdsl)["future_rebuild_ready"])
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(result.cdsl, Path(directory) / "00020311.step")
|
||||
self.assertTrue((Path(directory) / "00020311.step").exists())
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F4: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F3")
|
||||
|
||||
def test_conversion_writes_status_and_sidecars(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp); source = root / "00000173.txt"; source.write_text(SOURCE)
|
||||
|
||||
Reference in New Issue
Block a user