feat(cadfs): 扩展重建引擎能力并固化代表性模型回归
- 扩展 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 特征链及运行时兼容性。
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -129,6 +130,133 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
|
||||
self.assertEqual(len(sketch["contour_regions_mm"]), 2)
|
||||
self.assertTrue(all(not region["holes"] for region in sketch["contour_regions_mm"]))
|
||||
|
||||
def test_analytic_circle_contours_preserve_single_circular_wire_edges(self) -> None:
|
||||
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"geometry": {"sketches": [{
|
||||
"id": "sketch", "workplane": _workplane(),
|
||||
"profile": {"type": "analytic_contours", "contours": [
|
||||
{"role": "outer", "closed": True, "segments": [
|
||||
{"type": "circle", "center": [-10, 0], "radius_mm": 2},
|
||||
]},
|
||||
{"role": "outer", "closed": True, "segments": [
|
||||
{"type": "circle", "center": [10, 0], "radius_mm": 2},
|
||||
]},
|
||||
]},
|
||||
}]},
|
||||
"features": [],
|
||||
}
|
||||
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
||||
faces = Build123dGeometryAdapter().faces_for_sketch(sketch)
|
||||
|
||||
self.assertEqual(len(faces), 2)
|
||||
self.assertTrue(all(len(face.outer_wire().edges()) == 1 for face in faces))
|
||||
|
||||
def test_analytic_ellipse_preserves_its_workplane_orientation_and_volume(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = self._base_block()
|
||||
cdsl["geometry"]["sketches"][0] = {
|
||||
"id": "ellipse",
|
||||
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [0, 1, 0], "normal": [0, 0, 1]},
|
||||
"profile": {"type": "analytic_contours", "contours": [{
|
||||
"role": "outer", "closed": True, "segments": [{
|
||||
"type": "ellipse", "center": [0, 0], "major_radius_mm": 5,
|
||||
"minor_radius_mm": 2, "major_axis": [3 / 5, 4 / 5],
|
||||
}],
|
||||
}]},
|
||||
}
|
||||
cdsl["features"][0]["sketch_id"] = "ellipse"
|
||||
cdsl["features"][0]["params"] = {"distance_mm": 4}
|
||||
|
||||
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
||||
edge = sketch["contour_regions_mm"][0]["outer"][0]
|
||||
self.assertEqual(edge["major_axis_mm"], [-0.8, 0.6, 0.0])
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "ellipse.step")
|
||||
|
||||
self.assertEqual(result["solid_count"], 1)
|
||||
self.assertAlmostEqual(result["volume_mm3"], math.pi * 5 * 2 * 4, places=5)
|
||||
|
||||
def test_bspline_profile_preserves_endpoint_tangents(self) -> None:
|
||||
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"geometry": {"sketches": [{
|
||||
"id": "spline", "workplane": _workplane(),
|
||||
"profile": {"type": "analytic_contours", "contours": [{
|
||||
"role": "outer", "closed": True, "segments": [
|
||||
{"type": "line", "start": [0, 0], "end": [4, 0]},
|
||||
{"type": "bspline", "start": [4, 0], "end": [0, 0],
|
||||
"points": [[4, 0], [4, 3], [0, 0]],
|
||||
"start_tangent": [0, 5], "end_tangent": [-5, 0]},
|
||||
],
|
||||
}]},
|
||||
}]},
|
||||
"features": [],
|
||||
}
|
||||
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
||||
spline = next(edge for edge in sketch["contour_regions_mm"][0]["outer"] if edge["type"] == "bspline")
|
||||
self.assertEqual(spline["start_tangent_mm"], [0.0, 5.0, 0.0])
|
||||
self.assertEqual(spline["end_tangent_mm"], [-5.0, 0.0, 0.0])
|
||||
edge = Build123dGeometryAdapter()._wire([spline]).edges()[0]
|
||||
self.assertAlmostEqual(edge.tangent_at(0).Y, 1.0, places=6)
|
||||
self.assertAlmostEqual(edge.tangent_at(1).X, -1.0, places=6)
|
||||
|
||||
def test_drafted_ellipse_exports_as_a_solid(self) -> None:
|
||||
from build123d import import_step
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = self._base_block()
|
||||
cdsl["geometry"]["sketches"].append({
|
||||
"id": "ellipse", "workplane": {"origin_mm": [0, 0, 10], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
||||
"profile": {"type": "analytic_contours", "contours": [{
|
||||
"role": "outer", "closed": True, "segments": [{
|
||||
"type": "ellipse", "center": [0, 0], "major_radius_mm": 3,
|
||||
"minor_radius_mm": 2, "major_axis": [1, 0],
|
||||
}],
|
||||
}]},
|
||||
})
|
||||
cdsl["features"].append({
|
||||
"id": "drafted_ellipse", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
||||
"params": {"distance_mm": 4, "draft": {"angle_deg": 1, "pull_direction": False}}, "sketch_id": "ellipse",
|
||||
})
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "drafted-ellipse.step"
|
||||
result = rebuild_cdsl(cdsl, output)
|
||||
exported = import_step(str(output))
|
||||
|
||||
self.assertEqual(result["solid_count"], 1)
|
||||
self.assertEqual(len(exported.solids()), 1)
|
||||
self.assertGreater(float(exported.volume), 1000.0)
|
||||
|
||||
def test_boolean_bodies_subtracts_explicit_new_body_sources(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "boolean-bodies",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [
|
||||
{"id": "outer", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 10}},
|
||||
{"id": "inner", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 4}},
|
||||
]},
|
||||
"features": [
|
||||
{"id": "outer_body", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "outer", "params": {"distance_mm": 10, "result_mode": "new_body"}},
|
||||
{"id": "inner_body", "atomic_id": "extrude_add_blind", "depends_on": ["outer_body"], "sketch_id": "inner", "params": {"distance_mm": 10, "result_mode": "new_body"}},
|
||||
{"id": "cut", "atomic_id": "boolean_bodies", "depends_on": ["outer_body", "inner_body"], "params": {"operation": "subtract", "target_feature_ids": ["outer_body"], "tool_feature_ids": ["inner_body"], "keep_tools": False}},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = rebuild_cdsl(cdsl, Path(tmp) / "boolean.step")
|
||||
|
||||
self.assertEqual(rebuilt["solid_count"], 1)
|
||||
self.assertAlmostEqual(rebuilt["volume_mm3"], math.pi * (10 ** 2 - 4 ** 2) * 10)
|
||||
self.assertEqual([item["feature_id"] for item in rebuilt["feature_results"]], ["outer_body", "inner_body", "cut"])
|
||||
|
||||
def test_deferred_reference_is_currently_executable_without_a_sketch(self) -> None:
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
@@ -598,6 +726,81 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(result["volume_mm3"], 200 - 4 * 3.141592653589793, places=5)
|
||||
self.assertIn("mirror", [item["feature_id"] for item in result["feature_results"]])
|
||||
|
||||
def test_circular_pattern_fuses_face_sharing_new_body_instances(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "circular-fuse",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [{
|
||||
"id": "wedge", "workplane": _workplane(),
|
||||
"profile": {"type": "polygon", "vertices": [[0, 0], [10, 0], [0, 10]]},
|
||||
}]},
|
||||
"features": [
|
||||
{"id": "wedge_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
||||
"params": {"distance_mm": 10, "result_mode": "new_body"}, "sketch_id": "wedge"},
|
||||
{"id": "wedge_pattern", "atomic_id": "pattern_circular", "depends_on": ["wedge_add"], "params": {
|
||||
"source_feature_ids": ["wedge_add"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
||||
"pattern_count": 4, "sweep_angle_deg": 360,
|
||||
}},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "circular-fuse.step")
|
||||
self.assertEqual(result["solid_count"], 1)
|
||||
self.assertAlmostEqual(result["volume_mm3"], 2000.0, places=5)
|
||||
|
||||
def test_circular_pattern_skips_explicitly_deleted_copy_instances(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "circular-delete-copy",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [{
|
||||
"id": "wedge", "workplane": _workplane(),
|
||||
"profile": {"type": "polygon", "vertices": [[0, 0], [10, 0], [0, 10]]},
|
||||
}]},
|
||||
"features": [
|
||||
{"id": "wedge_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
||||
"params": {"distance_mm": 10, "result_mode": "new_body"}, "sketch_id": "wedge"},
|
||||
{"id": "wedge_pattern", "atomic_id": "pattern_circular", "depends_on": ["wedge_add"], "params": {
|
||||
"source_feature_ids": ["wedge_add"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
||||
"pattern_count": 4, "sweep_angle_deg": 360, "excluded_instance_indices": [2],
|
||||
}},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "circular-delete-copy.step")
|
||||
self.assertAlmostEqual(result["volume_mm3"], 1500.0, places=5)
|
||||
|
||||
def test_mirror_pattern_can_union_the_active_body(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "mirror-active-body",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [
|
||||
{"id": "base", "workplane": _workplane(), "profile": _rectangle([-5, -5], [5, 5])},
|
||||
{"id": "half_cut", "workplane": _workplane(), "profile": _rectangle([-5, 0], [5, 5])},
|
||||
{"id": "hole", "workplane": _workplane(), "profile": {"type": "circle", "center": [0, -2], "radius_mm": 1}},
|
||||
]},
|
||||
"features": [
|
||||
{"id": "mirror_plane", "atomic_id": "reference_plane", "depends_on": [], "params": {"plane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 1, 0]}}, "execution_status": "supported"},
|
||||
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": ["mirror_plane"], "params": {"distance_mm": 2}, "sketch_id": "base", "execution_status": "supported"},
|
||||
{"id": "half_remove", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "params": {"distance_mm": 2}, "sketch_id": "half_cut", "execution_status": "supported"},
|
||||
{"id": "hole_remove", "atomic_id": "extrude_cut_blind", "depends_on": ["half_remove"], "params": {"distance_mm": 2}, "sketch_id": "hole", "execution_status": "supported"},
|
||||
{
|
||||
"id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["hole_remove", "mirror_plane"],
|
||||
"params": {"source_feature_ids": ["base_add"], "mirror_current_body": True, "mirror_plane": {"kind": "plane", "owner_feature_id": "mirror_plane"}},
|
||||
"selectors": [{"kind": "plane", "owner_feature_id": "mirror_plane"}], "execution_status": "supported",
|
||||
},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "mirror-active-body.step")
|
||||
self.assertAlmostEqual(result["volume_mm3"], 200 - 4 * 3.141592653589793, places=5)
|
||||
self.assertEqual(result["solid_count"], 1)
|
||||
|
||||
def test_mirrored_local_circle_preserves_its_reflected_world_position(self) -> None:
|
||||
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
||||
from cdsl_engine.runtime import _mirrored_sketch
|
||||
@@ -622,6 +825,120 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(center.Y, 3.0)
|
||||
self.assertAlmostEqual(center.Z, 0.0)
|
||||
|
||||
def test_circle_profile_retains_one_topological_circle_edge(self) -> None:
|
||||
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"geometry": {"sketches": [{
|
||||
"id": "circle", "workplane": _workplane(),
|
||||
"profile": {"type": "circle", "center": [2, 3], "radius_mm": 1},
|
||||
}]},
|
||||
"features": [],
|
||||
}
|
||||
sketch = resolve_all_sketches(cdsl)["geometry"]["sketches"][0]
|
||||
self.assertNotIn("contour_edges_mm", sketch)
|
||||
face = Build123dGeometryAdapter().faces_for_sketch(sketch)[0]
|
||||
self.assertEqual(len(face.outer_wire().edges()), 1)
|
||||
|
||||
def test_new_body_mode_preserves_an_overlapping_result_body(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = self._base_block()
|
||||
cdsl["geometry"]["sketches"].append({
|
||||
"id": "overlap", "workplane": _workplane(),
|
||||
"profile": _rectangle([-2, -5], [8, 5]),
|
||||
})
|
||||
cdsl["features"].append({
|
||||
"id": "second_body", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
||||
"params": {"distance_mm": 10, "result_mode": "new_body"}, "sketch_id": "overlap",
|
||||
})
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "two-bodies.step")
|
||||
self.assertEqual(result["solid_count"], 2)
|
||||
|
||||
def test_sweep_add_builds_a_solid_from_a_closed_profile_and_bspline_path(self) -> None:
|
||||
from cdsl_engine.runtime import 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": "sweep-add",
|
||||
"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": [0, 20],
|
||||
"points": [[0, 0], [0, 10], [0, 20]],
|
||||
"start_tangent": [0, 10], "end_tangent": [0, 10],
|
||||
},
|
||||
}},
|
||||
}],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "sweep.step")
|
||||
self.assertEqual(result["solid_count"], 1)
|
||||
self.assertAlmostEqual(result["volume_mm3"], 80 * math.pi, delta=1e-4)
|
||||
|
||||
def test_circular_pattern_rotates_a_sweep_profile_and_path(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "sweep-pattern",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [{
|
||||
"id": "profile", "workplane": {"origin_mm": [10, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, -1]},
|
||||
"profile": {"type": "circle", "radius_mm": 1},
|
||||
}]},
|
||||
"features": [
|
||||
{"id": "sweep", "atomic_id": "sweep_add", "depends_on": [], "sketch_id": "profile", "params": {"path": {
|
||||
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 1, 0]},
|
||||
"segment": {"type": "line", "start": [10, 0], "end": [10, 10]},
|
||||
}}},
|
||||
{"id": "pattern", "atomic_id": "pattern_circular", "depends_on": ["sweep"], "params": {
|
||||
"source_feature_ids": ["sweep"], "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
|
||||
"pattern_count": 3, "sweep_angle_deg": 360,
|
||||
}},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "sweep-pattern.step")
|
||||
self.assertEqual(result["solid_count"], 3)
|
||||
self.assertAlmostEqual(result["volume_mm3"], 30 * math.pi, delta=1e-4)
|
||||
|
||||
def test_shell_removes_a_selected_cap_and_offsets_inward(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
base = self._base_block()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
baseline = rebuild_cdsl(base, root / "baseline.step")
|
||||
top = next(
|
||||
item for item in baseline["topology_records"]
|
||||
if item["kind"] == "face"
|
||||
and item["feature_id"] == "base_add"
|
||||
and item["geometry"].get("surface_type") == "plane"
|
||||
and item["geometry"].get("normal", [0, 0, 0])[2] > 0.9
|
||||
)
|
||||
shelled = deepcopy(base)
|
||||
shelled["features"].append({
|
||||
"id": "shell", "atomic_id": "shell", "depends_on": ["base_add"],
|
||||
"params": {"thickness_mm": 1, "inward": True},
|
||||
"selectors": [{
|
||||
"kind": "face", "stable_id": top["record_id"], "snapshot_id": top["record_id"],
|
||||
"source": "runtime_snapshot", "confidence": 1, "owner_feature_id": "base_add",
|
||||
"geometry": top["geometry"],
|
||||
}],
|
||||
})
|
||||
result = rebuild_cdsl(shelled, root / "shell.step")
|
||||
self.assertEqual(result["solid_count"], 1)
|
||||
self.assertAlmostEqual(result["volume_mm3"], 424.0, places=5)
|
||||
|
||||
def test_fillet_and_hole_wizard_use_resolved_face_edge_selectors(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
@@ -983,6 +1300,131 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
|
||||
self.assertLess(result["volume_mm3"], 1000.0)
|
||||
self.assertIn("revolve_cut", [item["feature_id"] for item in result["feature_results"]])
|
||||
|
||||
def test_revolve_surface_preserves_the_active_solid_and_registers_a_shell(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = self._base_block()
|
||||
cdsl["geometry"]["sketches"].append({
|
||||
"id": "surface_profile", "workplane": _workplane(),
|
||||
"profile": _rectangle([2, 1], [4, 2]),
|
||||
})
|
||||
cdsl["features"].append({
|
||||
"id": "surface_revolve", "atomic_id": "revolve_surface", "depends_on": ["base_add"],
|
||||
"sketch_id": "surface_profile",
|
||||
"params": {"angle_deg": 360, "axis": {"origin_mm": [0, 0, 0], "direction": [1, 0, 0]}},
|
||||
})
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "surface-revolve.step")
|
||||
surface = next(item for item in result["feature_results"] if item["feature_id"] == "surface_revolve")
|
||||
self.assertNotIn("body_id", surface)
|
||||
self.assertEqual(surface["surface_id"], "surface:surface_revolve")
|
||||
self.assertEqual(result["solid_count"], 1)
|
||||
self.assertAlmostEqual(result["volume_mm3"], 1000.0)
|
||||
faces = [
|
||||
item for item in result["topology_records"]
|
||||
if item["feature_id"] == "surface_revolve" and item["kind"] == "face"
|
||||
]
|
||||
self.assertEqual(len(faces), 4)
|
||||
|
||||
def test_extrude_surface_preserves_the_active_solid_and_registers_a_shell(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = self._base_block()
|
||||
cdsl["geometry"]["sketches"].append({
|
||||
"id": "surface_profile", "workplane": _workplane(),
|
||||
"profile": {
|
||||
"type": "analytic_contours",
|
||||
"contours": [{
|
||||
"role": "unknown", "closed": True,
|
||||
"segments": [{"type": "circle", "center": [5, 5], "radius_mm": 2}],
|
||||
}],
|
||||
},
|
||||
})
|
||||
cdsl["features"].append({
|
||||
"id": "surface_extrude", "atomic_id": "extrude_surface", "depends_on": ["base_add"],
|
||||
"sketch_id": "surface_profile", "params": {"distance_mm": 10},
|
||||
})
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "surface-extrude.step")
|
||||
surface = next(item for item in result["feature_results"] if item["feature_id"] == "surface_extrude")
|
||||
self.assertNotIn("body_id", surface)
|
||||
self.assertEqual(surface["surface_id"], "surface:surface_extrude")
|
||||
self.assertEqual(result["solid_count"], 1)
|
||||
self.assertAlmostEqual(result["volume_mm3"], 1000.0)
|
||||
faces = [
|
||||
item for item in result["topology_records"]
|
||||
if item["feature_id"] == "surface_extrude" and item["kind"] == "face"
|
||||
]
|
||||
self.assertEqual(len(faces), 1)
|
||||
|
||||
def test_extrude_surface_exports_a_surface_only_step(self) -> None:
|
||||
from build123d import import_step
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "surface-only",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [{
|
||||
"id": "surface_profile", "workplane": _workplane(),
|
||||
"profile": {
|
||||
"type": "analytic_contours",
|
||||
"contours": [{
|
||||
"role": "unknown", "closed": True,
|
||||
"segments": [{"type": "circle", "center": [5, 5], "radius_mm": 2}],
|
||||
}],
|
||||
},
|
||||
}]},
|
||||
"features": [{
|
||||
"id": "surface_extrude", "atomic_id": "extrude_surface", "depends_on": [],
|
||||
"sketch_id": "surface_profile", "params": {"distance_mm": 10},
|
||||
}],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
out_step = Path(directory) / "surface-only.step"
|
||||
result = rebuild_cdsl(cdsl, out_step)
|
||||
rebuilt = import_step(str(out_step))
|
||||
self.assertEqual(result["solid_count"], 0)
|
||||
self.assertEqual(result["surface_count"], 1)
|
||||
self.assertEqual(result["surface_face_count"], 1)
|
||||
self.assertAlmostEqual(result["volume_mm3"], 0.0)
|
||||
self.assertAlmostEqual(result["surface_area_mm2"], 40 * math.pi)
|
||||
self.assertEqual(len(rebuilt.solids()), 0)
|
||||
self.assertEqual(len(rebuilt.faces()), 1)
|
||||
|
||||
def test_surface_limited_chamfer_requires_an_explicit_shell_boundary(self) -> None:
|
||||
from build123d import Plane, Solid
|
||||
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter
|
||||
|
||||
adapter = Build123dGeometryAdapter()
|
||||
base = Solid.make_cylinder(19, 20, Plane(origin=(0, 0, -10))).cut(
|
||||
Solid.make_cylinder(16, 20, Plane(origin=(0, 0, -10))),
|
||||
)
|
||||
lower = Solid.make_cylinder(28.5, 20, Plane(origin=(0, 0, -20))).cut(
|
||||
Solid.make_cylinder(16, 20, Plane(origin=(0, 0, -20))),
|
||||
)
|
||||
upper = Solid.make_cylinder(25.5, 25, Plane(origin=(0, 0, 10))).cut(
|
||||
Solid.make_cylinder(17.5, 25, Plane(origin=(0, 0, 10))),
|
||||
)
|
||||
body = adapter.fuse(adapter.fuse(base, lower), upper)
|
||||
selected = [
|
||||
edge for edge in body.edges()
|
||||
if str(edge.geom_type).split(".")[-1].lower() == "circle"
|
||||
and abs(float(edge.radius) - 28.5) <= 1e-6
|
||||
and abs(edge.arc_center.Z) <= 1e-6
|
||||
]
|
||||
self.assertEqual(len(selected), 1)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "explicit surface support"):
|
||||
adapter.surface_limited_chamfer(body, 10, selected, [])
|
||||
|
||||
wire = adapter._circle_wire([0, 0], 19, PlaneSpec.from_mapping(_workplane()))
|
||||
support = adapter.extrude_surface([wire], [0, 0, 10])
|
||||
rebuilt = adapter.surface_limited_chamfer(body, 10, selected, [support])
|
||||
|
||||
self.assertLess(rebuilt.volume, body.volume)
|
||||
cones = [face for face in rebuilt.faces() if str(face.geom_type).split(".")[-1].lower() == "cone"]
|
||||
self.assertEqual(len(cones), 1)
|
||||
|
||||
def test_unowned_revolve_feature_selector_is_preflight_blocked(self) -> None:
|
||||
from cdsl_engine.runtime import analyze_cdsl
|
||||
|
||||
@@ -1057,6 +1499,53 @@ 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_up_to_vertex_intersection_requires_one_shared_current_body_vertex(self) -> None:
|
||||
from cdsl_engine.runtime import RuntimeExecutionError, rebuild_cdsl
|
||||
|
||||
base = self._base_block()
|
||||
base["geometry"]["sketches"].append({
|
||||
"id": "cut", "workplane": _workplane(),
|
||||
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 1},
|
||||
})
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
baseline = rebuild_cdsl(base, root / "intersection-baseline.step")
|
||||
faces = [item for item in baseline["topology_records"] if item["kind"] == "face"]
|
||||
top = next(item for item in faces if item["geometry"]["normal"][2] > 0.9)
|
||||
right = next(item for item in faces if item["geometry"]["normal"][0] > 0.9)
|
||||
back = next(item for item in faces if item["geometry"]["normal"][1] > 0.9)
|
||||
|
||||
def face_selector(face: dict) -> dict:
|
||||
return {
|
||||
"kind": "face", "owner_feature_id": "base_add",
|
||||
"stable_id": f"intersection-{face['record_id']}",
|
||||
"source": "runtime_snapshot", "confidence": 1.0,
|
||||
"geometry": face["geometry"],
|
||||
}
|
||||
|
||||
cdsl = deepcopy(base)
|
||||
cdsl["features"].append({
|
||||
"id": "cut_to_intersection", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"],
|
||||
"params": {"distance_mm": 0, "end_condition": {
|
||||
"type": "up_to_vertex", "reference": {
|
||||
"kind": "vertex", "owner_feature_id": "base_add",
|
||||
"stable_id": "top-right-back", "source": "runtime_snapshot", "confidence": 1.0,
|
||||
"intersection_of": [face_selector(top), face_selector(right), face_selector(back)],
|
||||
},
|
||||
}},
|
||||
"sketch_id": "cut",
|
||||
})
|
||||
result = rebuild_cdsl(cdsl, root / "intersection.step")
|
||||
self.assertAlmostEqual(result["volume_mm3"], 1000 - 10 * 3.141592653589793, places=5)
|
||||
|
||||
ambiguous = deepcopy(cdsl)
|
||||
ambiguous["features"][-1]["params"]["end_condition"]["reference"]["intersection_of"] = [
|
||||
face_selector(right), face_selector(back),
|
||||
]
|
||||
with self.assertRaises(RuntimeExecutionError) as error:
|
||||
rebuild_cdsl(ambiguous, root / "ambiguous-intersection.step")
|
||||
self.assertEqual(error.exception.diagnostic.code, "intersection_vertex_unresolved")
|
||||
|
||||
def test_through_next_trims_a_partially_overlapping_profile_to_the_next_body_face(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
@@ -1073,6 +1562,71 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
|
||||
result = rebuild_cdsl(base, Path(directory) / "partial-through-next.step")
|
||||
self.assertAlmostEqual(result["volume_mm3"], 1000 + 24, places=5)
|
||||
|
||||
def test_through_next_keeps_leading_material_before_a_cylindrical_body(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "leading-through-next",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [
|
||||
{"id": "base", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 10}},
|
||||
{"id": "lead", "workplane": {
|
||||
"origin_mm": [0, -50, 25], "x_dir": [1, 0, 0], "normal": [0, 1, 0],
|
||||
}, "profile": {"type": "circle", "radius_mm": 2}},
|
||||
]},
|
||||
"features": [
|
||||
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
||||
"params": {"distance_mm": 50}, "sketch_id": "base"},
|
||||
{"id": "lead_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
||||
"params": {"distance_mm": 0, "end_condition": {"type": "through_next"}, "result_mode": "new_body"},
|
||||
"sketch_id": "lead"},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = rebuild_cdsl(cdsl, Path(directory) / "leading-through-next.step")
|
||||
self.assertEqual(result["solid_count"], 2)
|
||||
self.assertAlmostEqual(result["bbox_mm"]["min"][1], -50.0, places=5)
|
||||
|
||||
def test_up_to_surface_from_a_cylindrical_wall_uses_the_next_body_face(self) -> None:
|
||||
from cdsl_engine.runtime import rebuild_cdsl
|
||||
|
||||
base = {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "wall-hole",
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": [
|
||||
{"id": "tube", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 10}},
|
||||
{"id": "tube_bore", "workplane": _workplane(), "profile": {"type": "circle", "radius_mm": 7}},
|
||||
{"id": "wall_hole", "workplane": {
|
||||
"origin_mm": [10, 0, 5], "x_dir": [0, 1, 0], "normal": [-1, 0, 0],
|
||||
}, "profile": {"type": "circle", "radius_mm": 1}},
|
||||
]},
|
||||
"features": [
|
||||
{"id": "tube_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
||||
"params": {"distance_mm": 10}, "sketch_id": "tube"},
|
||||
{"id": "tube_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["tube_add"],
|
||||
"params": {"distance_mm": 10}, "sketch_id": "tube_bore"},
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
baseline = rebuild_cdsl(base, root / "tube.step")
|
||||
outer_face = next(
|
||||
item for item in baseline["topology_records"]
|
||||
if item["kind"] == "face" and item["geometry"]["surface_type"] == "cylinder"
|
||||
and abs(item["geometry"]["radius_mm"] - 10) <= 1e-6
|
||||
)
|
||||
cdsl = deepcopy(base)
|
||||
cdsl["features"].append({
|
||||
"id": "wall_hole_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["tube_cut"],
|
||||
"params": {"distance_mm": 0, "end_condition": {"type": "up_to_surface", "reference": {
|
||||
"kind": "face", "owner_feature_id": "tube_add", "geometry": outer_face["geometry"],
|
||||
}}},
|
||||
"sketch_id": "wall_hole",
|
||||
})
|
||||
result = rebuild_cdsl(cdsl, root / "wall-hole.step")
|
||||
self.assertLess(result["volume_mm3"], baseline["volume_mm3"] - 8)
|
||||
self.assertGreater(result["volume_mm3"], baseline["volume_mm3"] - 12)
|
||||
|
||||
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