from __future__ import annotations from pathlib import Path import unittest from cadfs_to_cdsl.featurescript_parser import parse_featurescript from cadfs_to_cdsl.lowering import lower_model from cadfs_to_cdsl.selector_binding import _score, bind_candidate_selectors class SelectorBindingTests(unittest.TestCase): def test_face_normal_match_is_orientation_independent(self) -> None: score = _score( {"normal": [0.0, 0.0, 1.0], "plane_offset_mm": 12.0}, {"normal": [0.0, 0.0, -1.0], "plane_offset_mm": 12.0}, ) self.assertEqual(score, 1.0) def test_rotational_face_axis_direction_is_orientation_independent(self) -> None: score = _score( {"axis_direction": [0.0, 0.0, 1.0]}, {"axis_direction": [0.0, 0.0, -1.0]}, ) self.assertEqual(score, 1.0) def test_axis_origin_distinguishes_parallel_cylinders(self) -> None: score = _score( {"axis_origin_mm": [0.0, 25.4, 33.37]}, {"axis_origin_mm": [33.38, 25.4, 0.0]}, ) self.assertEqual(score, 0.0) def test_empty_snapshot_score_is_not_treated_as_a_match(self) -> None: self.assertEqual(_score({}, {"normal": [0.0, 0.0, 1.0]}), 0.0) def test_swept_face_area_lower_bound_rejects_coplanar_fragment(self) -> None: expected = {"normal": [0.0, 1.0, 0.0], "plane_offset_mm": 54.69, "minimum_area_mm2": 285.0} self.assertIsNone(_score(expected, {"normal": [0.0, 1.0, 0.0], "plane_offset_mm": 54.69, "area_mm2": 0.64})) self.assertEqual(_score(expected, {"normal": [0.0, 1.0, 0.0], "plane_offset_mm": 54.69, "area_mm2": 463.7}), 1.0) def test_reversed_cap_normal_reverses_its_plane_offset(self) -> None: score = _score( {"normal": [0.0, 0.0, 1.0], "plane_offset_mm": 10.0}, { "normal": [0.0, 0.0, 1.0], "plane_normal": [0.0, 0.0, -1.0], "plane_offset_mm": -10.0, }, ) self.assertEqual(score, 1.0) def test_intersection_vertex_binds_pattern_and_current_body_prefixes(self) -> None: 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") candidate = lower_model(parse_featurescript(feature.read_text(), "00423838"), {}) bound, evidence = bind_candidate_selectors(candidate.cdsl) f7 = next(item for item in bound["features"] if item["id"] == "f_F7") reference = f7["params"]["end_condition"]["reference"] components = reference["intersection_of"] self.assertEqual(len(components), 3) self.assertEqual([item["binding_feature_id"] for item in components], ["f_F4", "f_F4", "f_F6"]) # 绑定发生在 F4 完整 pattern 前缀;owner 保留 instance 4 的语义来源, # snapshot 则指向该前缀的 active Compound B-rep。 self.assertEqual([item["owner_feature_id"] for item in components[:2]], ["f_F4.c4.f_F1", "f_F4.c4.f_F1"]) self.assertTrue(all(item["snapshot_id"].startswith("body:f_F4:") for item in components[:2])) self.assertEqual(components[2]["match_mode"], "all") # 同一圆柱面可能被前缀 boolean 切成不同数量的 B-rep face;关键是 # match_mode=all 保留每个可匹配片段,而不是把它收缩为任意一个面。 matched = components[2]["matched_selectors"] self.assertTrue(matched) self.assertTrue(all(item["owner_feature_id"] == "f_F6" for item in matched)) self.assertTrue(all(item["snapshot_id"].startswith("body:f_F6:") for item in matched)) f7_evidence = next(item for item in evidence if item["feature_id"] == "f_F7") self.assertEqual(len(f7_evidence["resolved"]), len(matched) + 2)