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:
@@ -1,7 +1,7 @@
|
||||
"""Core CDSL sketch resolver.
|
||||
|
||||
The runtime accepts only direct geometric descriptions: circles, straight-edge
|
||||
polygons, and closed analytic line/arc/circle/B-spline contours. Semantic
|
||||
polygons, and closed analytic line/arc/circle/ellipse/B-spline contours. Semantic
|
||||
shapes and historical profile macros belong to the importer compatibility
|
||||
layer and must be lowered before this module is invoked.
|
||||
"""
|
||||
@@ -58,6 +58,12 @@ def _to_3d(workplane: _Ctx, u: float, v: float) -> list[float]:
|
||||
return [origin[0] + u * x_dir[0] + v * y_dir[0], origin[1] + u * x_dir[1] + v * y_dir[1], origin[2] + u * x_dir[2] + v * y_dir[2]]
|
||||
|
||||
|
||||
def _to_3d_vector(workplane: _Ctx, u: float, v: float) -> list[float]:
|
||||
origin = _to_3d(workplane, 0.0, 0.0)
|
||||
target = _to_3d(workplane, u, v)
|
||||
return [target[index] - origin[index] for index in range(3)]
|
||||
|
||||
|
||||
def _dot(left: Iterable[float], right: Iterable[float]) -> float:
|
||||
return sum(a * b for a, b in zip(left, right))
|
||||
|
||||
@@ -81,11 +87,19 @@ def _transform_contours(contours: list[_Ctx], workplane: _Ctx) -> list[_Ctx]:
|
||||
if edge["type"] == "arc":
|
||||
output["center_mm"] = _to_3d(workplane, edge["center_mm"][0], edge["center_mm"][1])
|
||||
output["normal"] = list(normal)
|
||||
elif edge["type"] == "ellipse":
|
||||
output["center_mm"] = _to_3d(workplane, edge["center_mm"][0], edge["center_mm"][1])
|
||||
output["major_axis_mm"] = _to_3d_vector(workplane, edge["major_axis_mm"][0], edge["major_axis_mm"][1])
|
||||
output["normal"] = list(normal)
|
||||
elif edge["type"] == "bspline":
|
||||
output["points_mm"] = [
|
||||
_to_3d(workplane, point[0], point[1])
|
||||
for point in edge["points_mm"]
|
||||
]
|
||||
for key in ("start_tangent_mm", "end_tangent_mm"):
|
||||
if key in edge:
|
||||
tangent = edge[key]
|
||||
output[key] = _to_3d_vector(workplane, tangent[0], tangent[1])
|
||||
transformed.append(output)
|
||||
return transformed
|
||||
|
||||
@@ -96,8 +110,9 @@ def _gen_circle(profile: _Ctx, _: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]:
|
||||
if radius <= 0:
|
||||
raise ValueError("circle radius must be > 0")
|
||||
cx, cy = float(center[0]), float(center[1])
|
||||
points = [[cx + radius, cy, 0.0], [cx, cy + radius, 0.0], [cx - radius, cy, 0.0], [cx, cy - radius, 0.0], [cx + radius, cy, 0.0]]
|
||||
return [_circle([cx, cy], radius)], [_contour_arc(points[index], points[index + 1], [cx, cy, 0.0], radius) for index in range(4)]
|
||||
# 直接圆 profile 必须保留为一条完整的圆边。若拆成四条圆弧,后续按边
|
||||
# 选择的圆角/倒角会把同一拓扑圆误解为四个独立目标。
|
||||
return [_circle([cx, cy], radius)], []
|
||||
|
||||
|
||||
def _gen_polygon(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]:
|
||||
@@ -118,6 +133,19 @@ def _distance(left: list[float], right: list[float]) -> float:
|
||||
return math.hypot(float(left[0]) - float(right[0]), float(left[1]) - float(right[1]))
|
||||
|
||||
|
||||
def _centripetal_parameters(points: list[list[float]], periodic: bool) -> list[float]:
|
||||
pairs = list(zip(points, points[1:]))
|
||||
if periodic:
|
||||
pairs.append((points[-1], points[0]))
|
||||
parameters = [0.0]
|
||||
for start, end in pairs:
|
||||
distance = math.dist(start, end)
|
||||
if distance <= _TOLERANCE_MM:
|
||||
raise ValueError("analytic_contours: centripetal bspline has coincident interpolation points")
|
||||
parameters.append(parameters[-1] + math.sqrt(distance))
|
||||
return parameters
|
||||
|
||||
|
||||
def _reverse(edge: _Ctx) -> _Ctx:
|
||||
output = deepcopy(edge)
|
||||
output["start_mm"], output["end_mm"] = output["end_mm"], output["start_mm"]
|
||||
@@ -125,6 +153,16 @@ def _reverse(edge: _Ctx) -> _Ctx:
|
||||
output["clockwise"] = not bool(output["clockwise"])
|
||||
if output.get("type") == "bspline":
|
||||
output["points_mm"] = list(reversed(output["points_mm"]))
|
||||
parameters = output.get("parameters")
|
||||
if parameters is not None:
|
||||
final_parameter = float(parameters[-1])
|
||||
output["parameters"] = [final_parameter - float(value) for value in reversed(parameters)]
|
||||
start_tangent = output.pop("start_tangent_mm", None)
|
||||
end_tangent = output.pop("end_tangent_mm", None)
|
||||
if end_tangent is not None:
|
||||
output["start_tangent_mm"] = [-float(value) for value in end_tangent]
|
||||
if start_tangent is not None:
|
||||
output["end_tangent_mm"] = [-float(value) for value in start_tangent]
|
||||
return output
|
||||
|
||||
|
||||
@@ -174,6 +212,27 @@ def _circle_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
return [_contour_arc(points[index], points[index + 1], [cx, cy, 0.0], radius, clockwise) for index in range(4)]
|
||||
|
||||
|
||||
def _ellipse_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
center = segment.get("center") or [0.0, 0.0]
|
||||
major_radius = float(segment.get("major_radius_mm") or 0.0)
|
||||
minor_radius = float(segment.get("minor_radius_mm") or 0.0)
|
||||
major_axis = segment.get("major_axis") or []
|
||||
if major_radius <= 0 or minor_radius <= 0:
|
||||
raise ValueError("analytic_contours: ellipse radii must be > 0")
|
||||
if len(major_axis) < 2:
|
||||
raise ValueError("analytic_contours: ellipse major_axis must have two components")
|
||||
axis_length = math.hypot(float(major_axis[0]), float(major_axis[1]))
|
||||
if axis_length <= _TOLERANCE_MM:
|
||||
raise ValueError("analytic_contours: ellipse major_axis is degenerate")
|
||||
cx, cy = float(center[0]), float(center[1])
|
||||
ux, uy = float(major_axis[0]) / axis_length, float(major_axis[1]) / axis_length
|
||||
start = [cx + major_radius * ux, cy + major_radius * uy, 0.0]
|
||||
return [{
|
||||
"type": "ellipse", "start_mm": start, "end_mm": list(start), "center_mm": [cx, cy, 0.0],
|
||||
"major_axis_mm": [ux, uy, 0.0], "major_radius_mm": major_radius, "minor_radius_mm": minor_radius,
|
||||
}]
|
||||
|
||||
|
||||
def _segment_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
kind = segment.get("type")
|
||||
if kind == "line":
|
||||
@@ -182,17 +241,55 @@ def _segment_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
return [_contour_arc(segment["start"], segment["end"], segment["center"], segment.get("radius_mm"), segment.get("clockwise"))]
|
||||
if kind == "circle":
|
||||
return _circle_edges(segment)
|
||||
if kind == "ellipse":
|
||||
return _ellipse_edges(segment)
|
||||
if kind == "bspline":
|
||||
points = segment.get("points") or []
|
||||
if len(points) < 3:
|
||||
raise ValueError("analytic_contours: bspline needs at least 3 interpolation points")
|
||||
converted = [_point(point) for point in points]
|
||||
return [{
|
||||
periodic = bool(segment.get("periodic"))
|
||||
if periodic:
|
||||
if _distance(converted[0], converted[-1]) > _TOLERANCE_MM:
|
||||
raise ValueError("analytic_contours: periodic bspline endpoints do not meet")
|
||||
# The duplicated closing interpolation point describes topology,
|
||||
# not an additional periodic interpolation constraint. OCC's
|
||||
# periodic interpolator receives each unique point exactly once.
|
||||
interpolation_points = converted[:-1]
|
||||
else:
|
||||
interpolation_points = converted
|
||||
parameterization = segment.get("parameterization")
|
||||
if parameterization not in {None, "chord", "centripetal"}:
|
||||
raise ValueError(f"analytic_contours: unsupported bspline parameterization {parameterization!r}")
|
||||
parameters = segment.get("parameters")
|
||||
if parameters is not None:
|
||||
expected_count = len(interpolation_points) + int(periodic)
|
||||
if len(parameters) != expected_count:
|
||||
raise ValueError("analytic_contours: bspline parameter count does not match interpolation points")
|
||||
parameters = [float(value) for value in parameters]
|
||||
if not all(math.isfinite(value) for value in parameters):
|
||||
raise ValueError("analytic_contours: bspline parameters must be finite")
|
||||
if any(right - left <= _TOLERANCE_MM for left, right in zip(parameters, parameters[1:])):
|
||||
raise ValueError("analytic_contours: bspline parameters must be strictly increasing")
|
||||
output: _Ctx = {
|
||||
"type": "bspline",
|
||||
"start_mm": converted[0],
|
||||
"end_mm": converted[-1],
|
||||
"points_mm": converted,
|
||||
}]
|
||||
"points_mm": interpolation_points,
|
||||
"periodic": periodic,
|
||||
**({"parameters": parameters} if parameters is not None else {}),
|
||||
**({"parameters": _centripetal_parameters(interpolation_points, periodic)} if parameters is None and parameterization == "centripetal" else {}),
|
||||
}
|
||||
start_tangent = segment.get("start_tangent")
|
||||
end_tangent = segment.get("end_tangent")
|
||||
if (start_tangent is None) != (end_tangent is None):
|
||||
raise ValueError("analytic_contours: bspline requires both endpoint tangents")
|
||||
if start_tangent is not None:
|
||||
if periodic:
|
||||
raise ValueError("analytic_contours: periodic bspline does not accept endpoint tangents")
|
||||
output["start_tangent_mm"] = _point(start_tangent)
|
||||
output["end_tangent_mm"] = _point(end_tangent)
|
||||
return [output]
|
||||
raise ValueError(f"analytic_contours: unsupported segment type {kind!r}")
|
||||
|
||||
|
||||
@@ -204,6 +301,25 @@ def _sample_loop(edges: list[_Ctx]) -> list[tuple[float, float]]:
|
||||
if edge.get("type") == "bspline":
|
||||
points.extend((float(point[0]), float(point[1])) for point in edge["points_mm"][1:-1])
|
||||
continue
|
||||
if edge.get("type") == "ellipse":
|
||||
center, axis = edge["center_mm"], edge["major_axis_mm"]
|
||||
major_radius = float(edge["major_radius_mm"])
|
||||
minor_radius = float(edge["minor_radius_mm"])
|
||||
normal = edge.get("normal") or [0.0, 0.0, 1.0]
|
||||
axis_length = math.sqrt(sum(float(value) * float(value) for value in axis))
|
||||
normal_length = math.sqrt(sum(float(value) * float(value) for value in normal))
|
||||
if axis_length <= _TOLERANCE_MM or normal_length <= _TOLERANCE_MM:
|
||||
raise ValueError("analytic_contours: ellipse axis is degenerate")
|
||||
x_axis = [float(value) / axis_length for value in axis]
|
||||
z_axis = [float(value) / normal_length for value in normal]
|
||||
y_axis = [z_axis[1] * x_axis[2] - z_axis[2] * x_axis[1], z_axis[2] * x_axis[0] - z_axis[0] * x_axis[2], z_axis[0] * x_axis[1] - z_axis[1] * x_axis[0]]
|
||||
for step in range(1, 8):
|
||||
angle = math.tau * step / 8
|
||||
points.append((
|
||||
float(center[0]) + major_radius * math.cos(angle) * x_axis[0] + minor_radius * math.sin(angle) * y_axis[0],
|
||||
float(center[1]) + major_radius * math.cos(angle) * x_axis[1] + minor_radius * math.sin(angle) * y_axis[1],
|
||||
))
|
||||
continue
|
||||
if edge.get("type") != "arc":
|
||||
continue
|
||||
center, end = edge["center_mm"], edge["end_mm"]
|
||||
@@ -328,7 +444,7 @@ SHAPE_GENERATORS = CORE_SHAPE_GENERATORS
|
||||
SHAPE_CAPABILITIES: dict[str, _Ctx] = {
|
||||
"circle": {"detectable": True, "arity": "circle", "description": "single circular contour"},
|
||||
"polygon": {"detectable": True, "arity": "polygon", "description": "closed straight-edge contour"},
|
||||
"analytic_contours": {"detectable": True, "arity": "analytic", "description": "closed line, arc, and circle contours"},
|
||||
"analytic_contours": {"detectable": True, "arity": "analytic", "description": "closed line, arc, circle, ellipse and B-spline contours"},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user