feat(cadfs): 补齐核心建模能力并建立代表性回归
- 新增 loft、双向切除、through-all/up-to-next 等 CADFS lowering 与 engine 支持 - 支持多种 reference plane、B-spline profile 和 circular pattern replay - 保留 transform 历史,并烘焙安全的单源平移/旋转变换 - 改进 selector 绑定、拓扑快照和 pattern 变换处理 - 建立 17 个代表样本的转换、重建与比较回归工具链 - 补充 schema、author guidance、运行时和几何回归测试
This commit is contained in:
@@ -73,7 +73,7 @@ class AuthorGuidanceTests(unittest.TestCase):
|
||||
self.assertLessEqual(len(selection.content), 1_200)
|
||||
self.assertIn("世界坐标", selection.content)
|
||||
covered.update(section_id for section_id in selection.section_ids if section_id.startswith("op-"))
|
||||
self.assertEqual(covered, {"op-extrude-add", "op-extrude-cut", "op-revolve", "op-hole", "op-reference", "op-pattern", "op-finish", "op-sphere"})
|
||||
self.assertEqual(covered, {"op-extrude-add", "op-extrude-cut", "op-loft", "op-revolve", "op-hole", "op-reference", "op-pattern", "op-finish", "op-sphere", "op-primitives", "op-thread"})
|
||||
|
||||
def test_phase_repair_and_budget_selection_are_stable(self) -> None:
|
||||
guidance = FileAuthorGuidance(GUIDANCE_ROOT, max_chars=3_600)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from engine.cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
|
||||
def _spline_profile(sketch_id: str, z: float) -> dict:
|
||||
return {
|
||||
"id": sketch_id,
|
||||
"workplane": {"origin_mm": [0, 0, z], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
||||
"profile": {
|
||||
"type": "analytic_contours",
|
||||
"contours": [{
|
||||
"role": "outer",
|
||||
"closed": True,
|
||||
"segments": [{
|
||||
"type": "bspline",
|
||||
"start": [0, 0],
|
||||
"end": [0, 0],
|
||||
"points": [[0, 0], [20, 0], [25, 10], [15, 18], [0, 10], [0, 0]],
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class LoftGeometryTests(unittest.TestCase):
|
||||
def test_open_bspline_segment_can_close_with_other_contour_edges(self):
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"schema_version": "1.1.0",
|
||||
"kind": "part",
|
||||
"part_id": "open-bspline-contour",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [{
|
||||
"id": "profile",
|
||||
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
||||
"profile": {"type": "analytic_contours", "contours": [{
|
||||
"role": "outer", "closed": True,
|
||||
"segments": [
|
||||
{"type": "line", "start": [0, 0], "end": [0, 10]},
|
||||
{
|
||||
"type": "bspline", "start": [0, 10], "end": [0, 0],
|
||||
"points": [[0, 10], [4, 10], [4, 0], [0, 0]],
|
||||
},
|
||||
],
|
||||
}]},
|
||||
}]},
|
||||
"features": [{
|
||||
"id": "add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
||||
"sketch_id": "profile", "params": {"distance_mm": 5},
|
||||
}],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = rebuild_cdsl(cdsl, Path(tmp) / "open-bspline-contour.step")
|
||||
self.assertGreater(rebuilt["volume_mm3"], 1.0)
|
||||
|
||||
def test_bspline_loft_and_mirror_replay_produce_step(self):
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"schema_version": "1.1.0",
|
||||
"kind": "part",
|
||||
"part_id": "bspline-loft-mirror",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [_spline_profile("lower", 0), _spline_profile("upper", 20)]},
|
||||
"features": [
|
||||
{
|
||||
"id": "mirror_plane",
|
||||
"atomic_id": "reference_plane",
|
||||
"depends_on": [],
|
||||
"params": {"plane": {"origin_mm": [35, 0, 0], "x_dir": [0, 1, 0], "normal": [1, 0, 0]}},
|
||||
"execution_status": "supported",
|
||||
},
|
||||
{
|
||||
"id": "loft",
|
||||
"atomic_id": "loft_add",
|
||||
"depends_on": [],
|
||||
"params": {"profile_sketch_ids": ["lower", "upper"]},
|
||||
"execution_status": "supported",
|
||||
},
|
||||
{
|
||||
"id": "mirror",
|
||||
"atomic_id": "pattern_mirror",
|
||||
"depends_on": ["mirror_plane", "loft"],
|
||||
"params": {
|
||||
"source_feature_ids": ["loft"],
|
||||
"mirror_plane": {
|
||||
"kind": "plane",
|
||||
"owner_feature_id": "mirror_plane",
|
||||
"stable_id": "mirror-plane",
|
||||
"source": "runtime_snapshot",
|
||||
"confidence": 1.0,
|
||||
},
|
||||
},
|
||||
"selectors": [{
|
||||
"kind": "plane",
|
||||
"owner_feature_id": "mirror_plane",
|
||||
"stable_id": "mirror-plane",
|
||||
"source": "runtime_snapshot",
|
||||
"confidence": 1.0,
|
||||
}],
|
||||
"execution_status": "supported",
|
||||
},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = rebuild_cdsl(cdsl, Path(tmp) / "loft.step")
|
||||
self.assertGreater(rebuilt["volume_mm3"], 1.0)
|
||||
self.assertEqual(rebuilt["solid_count"], 2)
|
||||
self.assertEqual([item["feature_id"] for item in rebuilt["feature_results"]], ["mirror_plane", "loft", "mirror.m.loft", "mirror"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -316,6 +316,55 @@ class PatternTransformContractTests(unittest.TestCase):
|
||||
|
||||
self.assertAlmostEqual(rebuilt["volume_mm3"], 1000 + 72 + 24, places=5)
|
||||
|
||||
def test_circular_pattern_rotates_analytic_arc_normals_with_the_sketch(self) -> None:
|
||||
"""倾斜圆草图的环形阵列必须保持每条圆弧处于旋转后的工作平面。
|
||||
|
||||
``analytic_contours`` 在运行时会预先展开为世界坐标的圆弧边。环形阵列
|
||||
不能只旋转边端点和圆心:Build123d 依据 edge.normal 计算三点圆弧中点,
|
||||
法向遗留在源草图平面会让中点偏离 wire 平面,最终报
|
||||
``Cannot build face(s): wires not planar``。该 fixture 使用绕 Z 轴的
|
||||
垂直圆草图,保证每个实例都实际变换该法向。
|
||||
"""
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
||||
"part_id": "circular-arc-normal", "meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [
|
||||
{
|
||||
"id": "base", "workplane": _workplane(origin=[0, 0, 0], normal=[0, 0, 1]),
|
||||
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 10},
|
||||
},
|
||||
{
|
||||
"id": "boss", "workplane": _workplane(origin=[0, -10, 0], normal=[0, 1, 0]),
|
||||
"profile": {"type": "analytic_contours", "contours": [{
|
||||
"role": "outer", "closed": True,
|
||||
"segments": [{"type": "circle", "center": [0, -5], "radius_mm": 1}],
|
||||
}]},
|
||||
},
|
||||
]},
|
||||
"features": [
|
||||
{
|
||||
"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
||||
"sketch_id": "base", "params": {"distance_mm": 10},
|
||||
},
|
||||
{
|
||||
"id": "boss_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
||||
"sketch_id": "boss", "params": {"distance_mm": 2},
|
||||
},
|
||||
{
|
||||
"id": "repeat", "atomic_id": "pattern_circular", "depends_on": ["boss_add"],
|
||||
"params": {
|
||||
"source_feature_ids": ["boss_add"],
|
||||
"axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
||||
"pattern_count": 4, "sweep_angle_deg": 360,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = rebuild_cdsl(cdsl, Path(directory) / "circular-arc-normal.step")
|
||||
self.assertGreater(rebuilt["volume_mm3"], math.pi * 1000)
|
||||
self.assertEqual(rebuilt["solid_count"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -918,6 +918,25 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
|
||||
analysis = analyze_cdsl(cdsl)
|
||||
self.assertIn("missing_parameter", [item.code for item in analysis.feature_results[0].blockers])
|
||||
|
||||
def test_two_sided_cut_uses_independent_forward_and_reverse_distances(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = self._base_block()
|
||||
cdsl["geometry"]["sketches"].append({
|
||||
"id": "cut", "workplane": {**_workplane(), "origin_mm": [0, 0, 5]},
|
||||
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
||||
})
|
||||
cdsl["features"].append({
|
||||
"id": "cut_1", "atomic_id": "extrude_cut_two_sided", "depends_on": ["base_add"],
|
||||
"params": {
|
||||
"distance_mm": 3, "reverse_distance_mm": 5,
|
||||
"end_condition": {"type": "blind"}, "reverse_end_condition": {"type": "blind"},
|
||||
}, "sketch_id": "cut",
|
||||
})
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "two-sided-cut.step")
|
||||
self.assertAlmostEqual(result["volume_mm3"], 1000.0 - 8.0 * 3.141592653589793, places=5)
|
||||
|
||||
def test_revolve_can_resolve_an_owner_qualified_reference_axis(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
@@ -1038,6 +1057,22 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
|
||||
result = rebuild_cdsl(cdsl, root / f"{name}.step")
|
||||
self.assertAlmostEqual(result["volume_mm3"], 1000 - expected_depth * 3.141592653589793, places=5)
|
||||
|
||||
def test_through_next_trims_a_partially_overlapping_profile_to_the_next_body_face(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
base = self._base_block()
|
||||
base["geometry"]["sketches"].append({
|
||||
"id": "partial", "workplane": {"origin_mm": [0, 0, 12], "x_dir": [1, 0, 0], "normal": [0, 0, -1]},
|
||||
"profile": {"type": "polygon", "vertices": [[3, -3], [9, -3], [9, 3], [3, 3]]},
|
||||
})
|
||||
base["features"].append({
|
||||
"id": "partial_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
||||
"sketch_id": "partial", "params": {"distance_mm": 0, "end_condition": {"type": "through_next"}},
|
||||
})
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(base, Path(directory) / "partial-through-next.step")
|
||||
self.assertAlmostEqual(result["volume_mm3"], 1000 + 24, places=5)
|
||||
|
||||
def test_up_to_body_extent_uses_a_uniquely_resolved_body_record(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
|
||||
Reference in New Issue
Block a user