738934416e
- 扩展 CDSL engine 的 shell、sweep、loft、reference plane、pattern 等运行时能力, 支持新的实体结果模式、双向拉伸、曲线扫掠、镜像/圆周阵列及相关 selector 解析。 - 完善 Build123d 适配层的拓扑快照、Compound/ShapeList 兼容处理和旋转曲面识别, 兼容 Python 3.12 / 当前 Build123d 缺少 axis_of_rotation 的合法曲面场景。 - 扩展 CDSL schema、profile schema、capability analysis、semantic validation 和 sketch solver,使新增建模操作能够被校验、执行并保留可诊断的部分结果。 - 完善 CADFS FeatureScript lowering: 支持 shell、sweep、surface/实体 loft、圆周阵列副本、镜像副本、删除阵列实例、 新 body 操作、更多拉伸终止条件和 reference plane 变体。 - 补齐椭圆、B-spline、环形区域、imprint、SWEPT_FACE、CAP_FACE、OFFSET_FACE 等 草图和拓扑引用的转换逻辑,改善后续特征的工作平面、轴线和 profile 定位精度。 - 改进 selector binding:支持 pattern 前缀复合 B-rep 快照、交集顶点引用、 多面 match_mode=all、圆柱轴线/半径和面积下限等稳定匹配条件。 - 修复 MID_PLANE 法向统一后交线方向未同步的问题,恢复 00287955 基准面的正确位置; 修复 00542223 sweep 路径反转后的切线契约和 00423838 的拓扑面数不稳定测试假设。 - 修正 CADFS 比较模块 import 路径,补充重建报告、批量重建脚本、目标文档和 README。 - 新增并扩展 engine、lowering、parser、selector binding、reports、integration 和 Onshape pipeline 回归测试,覆盖代表性 CADFS 特征链及运行时兼容性。
197 lines
11 KiB
Python
197 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile, unittest
|
|
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.lowering import lower_model
|
|
from cadfs_to_cdsl.rebuild import rebuild_candidate
|
|
|
|
|
|
class IntegrationTests(unittest.TestCase):
|
|
def test_known_rp_roundtrip_00000173(self):
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0000/00000173.txt"; gold = root / "step_abc/0000/00000173.step"
|
|
if not feature.exists() or not gold.exists(): self.skipTest("CADFS sample is not installed")
|
|
cdsl = lower_model(parse_featurescript(feature.read_text(), "00000173"), {}).cdsl
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
rebuilt = Path(tmp) / "rebuild.step"
|
|
result = rebuild_candidate(cdsl, rebuilt)
|
|
self.assertEqual(result["status"], "rebuilt")
|
|
comparison = compare_steps(gold, rebuilt)
|
|
self.assertFalse(comparison["strict"]["passed"])
|
|
self.assertTrue(comparison["rp"]["passed"])
|
|
self.assertEqual(comparison["decision"], "approximate_pass")
|
|
|
|
def test_standard_tapped_through_counterbore_00002243(self):
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0000/00002243.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
result = lower_model(parse_featurescript(feature.read_text(), "00002243"), {})
|
|
first = next(item for item in result.cdsl["features"] if item["id"] == "f_F1")
|
|
self.assertEqual(first["atomic_id"], "extrude_add_two_sided")
|
|
self.assertEqual(first["params"]["distance_mm"], 125.0)
|
|
self.assertEqual(first["params"]["reverse_distance_mm"], 125.0)
|
|
cut = next(item for item in result.cdsl["features"] if item["id"] == "f_F3")
|
|
self.assertEqual(cut["atomic_id"], "extrude_cut_two_sided")
|
|
hole = next(item for item in result.cdsl["features"] if item["atomic_id"] == "hole_wizard")
|
|
self.assertEqual(hole["params"]["hole_type"], "c_bore")
|
|
self.assertEqual(hole["params"]["diameter_mm"], 15.0)
|
|
self.assertEqual(hole["params"]["end_condition"]["type"], "through_all")
|
|
self.assertNotIn("counterbore", hole["params"])
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
rebuilt = Path(tmp) / "rebuild.step"
|
|
self.assertEqual(rebuild_candidate(result.cdsl, rebuilt)["status"], "rebuilt")
|
|
comparison = compare_steps(root / "step_abc/0000/00002243.step", rebuilt)
|
|
self.assertTrue(comparison["strict"]["passed"])
|
|
|
|
def test_rectilinear_fillet_arcs_00129362_preserve_recovered_sketch_geometry(self):
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0012/00129362.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
result = lower_model(parse_featurescript(feature.read_text(), "00129362"), {})
|
|
profile = next(item for item in result.cdsl["geometry"]["sketches"] if item["id"] == "sketch_F0")["profile"]
|
|
arcs = [segment for segment in profile["contours"][0]["segments"] if segment["type"] == "arc"]
|
|
self.assertEqual(sorted(arc["center"] for arc in arcs), [[25.4, 12.7], [25.4, 63.5]])
|
|
self.assertTrue(all(abs(arc["radius_mm"] - 12.7) <= 1e-12 for arc in arcs))
|
|
triangle = next(item for item in result.cdsl["geometry"]["sketches"] if item["id"] == "sketch_F5")["profile"]
|
|
self.assertEqual(
|
|
sorted({coordinate for contour in triangle["contours"] for segment in contour["segments"] for point in (segment.get("start"), segment.get("end")) if point for coordinate in point}),
|
|
[-12.7, -6.35, 0.0, 6.35, 12.7, 25.4, 34.925, 41.275],
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
rebuilt = Path(tmp) / "rebuild.step"
|
|
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
|
self.assertEqual(outcome["status"], "rebuilt")
|
|
|
|
def test_cylindrical_swept_face_selector_00111611(self):
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0011/00111611.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
result = lower_model(parse_featurescript(feature.read_text(), "00111611"), {})
|
|
fillet = next(item for item in result.cdsl["features"] if item["id"] == "f_F3")
|
|
self.assertEqual(fillet["atomic_id"], "fillet")
|
|
self.assertEqual(fillet["selectors"][0]["kind"], "face")
|
|
self.assertEqual(
|
|
fillet["selectors"][0]["geometry"],
|
|
{"axis_origin_mm": [0.0, 0.0, 0.0], "axis_direction": [0.0, -1.0, 0.0], "radius_mm": 45.66},
|
|
)
|
|
|
|
chamfer = next(item for item in result.cdsl["features"] if item["id"] == "f_F5")
|
|
self.assertEqual(
|
|
sorted(selector["geometry"]["axis_origin_mm"] for selector in chamfer["selectors"]),
|
|
sorted([[0.0, 25.4, 33.37], [33.38, 25.4, 0.0], [0.0, 25.4, -33.38], [-33.38, 25.4, 0.0]]),
|
|
)
|
|
|
|
sketches = {item["id"]: item for item in result.cdsl["geometry"]["sketches"]}
|
|
f4 = next(item for item in result.cdsl["features"] if item["id"] == "f_F4")
|
|
self.assertEqual(
|
|
sorted(segment["center"] for contour in sketches[f4["sketch_id"]]["profile"]["contours"] for segment in contour["segments"]),
|
|
sorted([[0.0, 33.37], [33.38, 0.0], [-33.38, 0.0], [0.0, -33.38]]),
|
|
)
|
|
f7 = next(item for item in result.cdsl["features"] if item["id"] == "f_F7")
|
|
self.assertEqual(
|
|
[segment["radius_mm"] for contour in sketches[f7["sketch_id"]]["profile"]["contours"] for segment in contour["segments"]],
|
|
[4.48, 2.36],
|
|
)
|
|
|
|
self.assertNotIn("result_mode", next(item for item in result.cdsl["features"] if item["id"] == "f_F7")["params"])
|
|
self.assertEqual(next(item for item in result.cdsl["features"] if item["id"] == "f_F9")["params"]["result_mode"], "new_body")
|
|
|
|
from build123d import import_step
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
rebuilt = Path(tmp) / "rebuild.step"
|
|
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
|
self.assertEqual(outcome["status"], "rebuilt")
|
|
runtime_box = outcome["result"]["bbox_mm"]
|
|
imported_box = import_step(str(rebuilt)).bounding_box()
|
|
actual = [imported_box.min.X, imported_box.min.Y, imported_box.min.Z, imported_box.max.X, imported_box.max.Y, imported_box.max.Z]
|
|
expected = runtime_box["min"] + runtime_box["max"]
|
|
for value, target in zip(actual, expected): self.assertAlmostEqual(value, target, places=5)
|
|
|
|
def test_inward_shells_00789939_lower_and_rebuild(self):
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0078/00789939.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
result = lower_model(parse_featurescript(feature.read_text(), "00789939"), {})
|
|
features = {item["id"]: item for item in result.cdsl["features"]}
|
|
self.assertEqual(features["f_F2"]["atomic_id"], "shell")
|
|
self.assertEqual(features["f_F5"]["atomic_id"], "shell")
|
|
self.assertEqual(features["f_F2"]["params"], {"thickness_mm": 2.5, "inward": True})
|
|
self.assertEqual(features["f_F5"]["params"], {"thickness_mm": 2.5, "inward": True})
|
|
self.assertNotIn("F2", [item.get("feature_id") for item in result.diagnostics])
|
|
self.assertNotIn("F5", [item.get("feature_id") for item in result.diagnostics])
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
rebuilt = Path(tmp) / "rebuild.step"
|
|
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
|
self.assertEqual(outcome["status"], "rebuilt")
|
|
self.assertEqual(outcome["result"]["solid_count"], 2)
|
|
|
|
def test_sweep_00542223_preserves_its_open_bspline_path(self):
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0054/00542223.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
result = lower_model(parse_featurescript(feature.read_text(), "00542223"), {})
|
|
sweep = next(item for item in result.cdsl["features"] if item["id"] == "f_F5")
|
|
segment = sweep["params"]["path"]["segment"]
|
|
self.assertEqual(sweep["atomic_id"], "sweep_add")
|
|
self.assertEqual(sweep["sketch_id"], "sketch_F4")
|
|
self.assertEqual(segment["points"], [[-40.0, -50.0], [-20.0, -14.96], [0.0, 5.0]])
|
|
self.assertEqual(segment["start_tangent"], [27.94, 92.49])
|
|
self.assertEqual(segment["end_tangent"], [52.88, 49.52])
|
|
self.assertNotIn("F5", [item.get("feature_id") for item in result.diagnostics])
|
|
|
|
def test_face_chamfer_ignores_periodic_seams_00111611(self):
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0011/00111611.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
|
|
from copy import deepcopy
|
|
from engine.cdsl_engine.capabilities import CapabilityAnalyzer, sketch_ids_required_by_contract
|
|
from engine.cdsl_engine.runtime import EXECUTORS, ExecutionSession, _execute_node, _selector_edges
|
|
from engine.cdsl_engine.sketch_solver import CORE_SHAPE_GENERATORS, resolve_required_sketches
|
|
from cadfs_to_cdsl.selector_binding import bind_candidate_selectors
|
|
|
|
candidate = lower_model(parse_featurescript(feature.read_text(), "00111611"), {}).cdsl
|
|
bound, _ = bind_candidate_selectors(candidate)
|
|
resolved = resolve_required_sketches(
|
|
deepcopy(bound), sketch_ids_required_by_contract(bound), errors={},
|
|
)
|
|
analysis = CapabilityAnalyzer(
|
|
atomic_ids=EXECUTORS, profile_types=CORE_SHAPE_GENERATORS,
|
|
).analyze(resolved)
|
|
session = ExecutionSession(
|
|
sketches={str(item["id"]): item for item in resolved["geometry"]["sketches"]},
|
|
nodes={node.feature_id: node for node in analysis.plan},
|
|
)
|
|
|
|
for node in analysis.plan:
|
|
if node.feature_id == "f_F5":
|
|
edges = _selector_edges(node, session, tangent_propagation=True)
|
|
break
|
|
_execute_node(node, session)
|
|
else:
|
|
self.fail("F5 chamfer was not planned")
|
|
|
|
self.assertEqual(len(edges), 8)
|
|
self.assertTrue(all(str(edge.geom_type).endswith("CIRCLE") for edge in edges))
|
|
|
|
def test_circular_remove_pattern_replays_cut_sources_00159804(self):
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0015/00159804.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
|
|
result = lower_model(parse_featurescript(feature.read_text(), "00159804"), {})
|
|
source = next(item for item in result.cdsl["features"] if item["id"] == "f_F8")
|
|
pattern = next(item for item in result.cdsl["features"] if item["id"] == "f_F9")
|
|
|
|
self.assertEqual(source["atomic_id"], "extrude_cut_blind")
|
|
self.assertNotIn("result_mode", source["params"])
|
|
self.assertEqual(pattern["params"]["operation_mode"], "remove")
|
|
|
|
|
|
if __name__ == "__main__": unittest.main()
|