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:
@@ -65,9 +65,15 @@ class Build123dGeometryAdapter:
|
||||
|
||||
@staticmethod
|
||||
def _wire(edges: list[dict[str, Any]]) -> Wire:
|
||||
# 将边字典列表(直线/圆弧)组装成 build123d 的 Wire 线框。
|
||||
# 将边字典列表(直线/圆弧/插值 B 样条)组装成 build123d 的 Wire 线框。
|
||||
built: list[Edge] = []
|
||||
for edge in edges:
|
||||
if edge.get("type") == "bspline":
|
||||
points = [_vector(point) for point in edge.get("points_mm") or []]
|
||||
if len(points) < 3:
|
||||
raise ValueError("bspline contour edge needs at least 3 points")
|
||||
built.append(Edge.make_spline(points, periodic=False))
|
||||
continue
|
||||
start = _vector(edge["start_mm"])
|
||||
end = _vector(edge["end_mm"])
|
||||
if edge.get("type") == "arc" and edge.get("center_mm") is not None:
|
||||
@@ -133,20 +139,38 @@ class Build123dGeometryAdapter:
|
||||
result: list[Face] = []
|
||||
for region in regions:
|
||||
outer = region.get("outer") or []
|
||||
if len(outer) < 2:
|
||||
# 闭合插值样条仅有一条边;直线/圆弧轮廓通常由多条边组成。
|
||||
if len(outer) < 1:
|
||||
continue
|
||||
face = Face(self._wire(outer))
|
||||
holes = [self._wire(hole) for hole in region.get("holes") or [] if len(hole) >= 2]
|
||||
holes = [self._wire(hole) for hole in region.get("holes") or [] if len(hole) >= 1]
|
||||
result.append(face.make_holes(holes) if holes else face)
|
||||
return result
|
||||
# 2. 退化:仅有单组轮廓边时,直接作为外轮廓建面。
|
||||
edges = sketch.get("contour_edges_mm") or []
|
||||
if len(edges) >= 2:
|
||||
if len(edges) >= 1:
|
||||
return [Face(self._wire(edges))]
|
||||
# 3. 最终回退:由工作平面与实体圆生成面(圆环/孔洞处理见 _faces_from_circles)。
|
||||
plane = PlaneSpec.from_mapping(sketch.get("workplane") or {})
|
||||
return self._faces_from_circles(sketch.get("entities") or [], plane)
|
||||
|
||||
def loft(self, sketches: list[dict[str, Any]]) -> Solid:
|
||||
"""由多条简单闭合草图轮廓生成实体放样。"""
|
||||
wires: list[Wire] = []
|
||||
for index, sketch in enumerate(sketches):
|
||||
# Solid.make_loft 接收 Wire;复用 faces_for_sketch 保持放样、
|
||||
# 拉伸和回转的 profile resolver 一致。多区域/内环的截面对应关系
|
||||
# 尚未由 CDSL 表达,必须显式拒绝而非猜测。
|
||||
faces = self.faces_for_sketch(sketch)
|
||||
if len(faces) != 1:
|
||||
raise ValueError(f"loft profile {index} must resolve to exactly one closed region")
|
||||
if faces[0].inner_wires():
|
||||
raise ValueError(f"loft profile {index} must not contain inner loops")
|
||||
wires.append(faces[0].outer_wire())
|
||||
if len(wires) < 2:
|
||||
raise ValueError("loft requires at least two profile sketches")
|
||||
return Solid.make_loft(wires)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_single_or_compound(result: Any, *, empty_error: str | None = None) -> Any:
|
||||
"""规整一次布尔结果:None/空视为失败(可选报错),多成员合并为 Compound。"""
|
||||
@@ -188,14 +212,22 @@ class Build123dGeometryAdapter:
|
||||
"""
|
||||
# 1. 采样点到目标的最远命中距离决定穿透余量;没有任何采样点命中
|
||||
# 说明 profile 与目标面无交叠,无法裁剪(保留 extent_target_not_reached)。
|
||||
# through_next 的 target 是当前主体,必须先从其面中选出实际命中的
|
||||
# 下一终止面,不能把整个 body 当作待拉伸的 Face。
|
||||
unit = _vector(direction).normalized()
|
||||
hits = [
|
||||
Build123dGeometryAdapter._forward_intersection_distance(target, point, unit)
|
||||
for point in Build123dGeometryAdapter.profile_sample_points(face)
|
||||
]
|
||||
distances = [value for value in hits if value is not None]
|
||||
if not distances:
|
||||
points = Build123dGeometryAdapter.profile_sample_points(face)
|
||||
targets = [target] if isinstance(target, Face) else list(target.faces())
|
||||
candidates = []
|
||||
for candidate in targets:
|
||||
hits = [Build123dGeometryAdapter._forward_intersection_distance(candidate, point, unit) for point in points]
|
||||
distances = [value for value in hits if value is not None]
|
||||
if distances:
|
||||
candidates.append((len(distances), min(distances), candidate, distances))
|
||||
if not candidates:
|
||||
raise ValueError("extent target is not reached by the profile")
|
||||
# 覆盖最多 profile 采样点的面就是本次实体的下一终止面;覆盖数相同
|
||||
# 时取最近的正向交点,确保相邻面交界处的选择稳定。
|
||||
_, _, target, distances = max(candidates, key=lambda item: (item[0], -item[1]))
|
||||
margin = max(distances) + 2.0
|
||||
# 2. 穿透拉伸 profile,同时把目标面向回推生成体层,二者求交即裁剪体。
|
||||
# build123d 的布尔交方法名是 intersect(不是 OCC 的 common),
|
||||
|
||||
Reference in New Issue
Block a user