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:
@@ -16,30 +16,30 @@ from .runtime_types import CapabilityResult, FeaturePlanNode, HoleSpec, RuntimeD
|
||||
from .operation_contracts import materialized_feature_contracts
|
||||
|
||||
|
||||
_SELECTOR_REQUIRED = frozenset({"fillet", "chamfer"})
|
||||
_SKETCH_ATOM_PREFIXES = ("extrude_", "revolve_")
|
||||
_SELECTOR_REQUIRED = frozenset({"extrude_add_blind_with_hole", "loft_add_with_cap_face", "fillet", "chamfer", "shell"})
|
||||
_SKETCH_ATOM_PREFIXES = ("extrude_", "revolve_", "sweep_")
|
||||
# 开放轮廓(closed=false / role=open)只有"刀具截面补槽口边闭合后作切除"的
|
||||
# 物理意义:仅 extrude 直切类原子支持;add/回转对开放轮廓会造出无意义的封块。
|
||||
_OPEN_PROFILE_ATOMICS = frozenset({"extrude_cut_blind", "extrude_cut_through"})
|
||||
_PRIMARY_ATOMICS = frozenset({
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_surface",
|
||||
"extrude_cut_through",
|
||||
"revolve_add", "revolve_cut", "hole_blind", "hole_countersink",
|
||||
"revolve_add", "revolve_cut", "revolve_surface", "hole_blind", "hole_countersink",
|
||||
"hole_counterbore", "sphere_add", "box_add", "cylinder_add",
|
||||
})
|
||||
_HOLE_ATOMICS = frozenset({"hole_blind", "hole_countersink", "hole_counterbore", "hole_wizard"})
|
||||
_ACTIVE_BODY_REQUIRED = frozenset({
|
||||
"extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through",
|
||||
"revolve_cut", *_HOLE_ATOMICS, "fillet", "chamfer",
|
||||
"loft_add_with_cap_face", "revolve_cut", *_HOLE_ATOMICS, "fillet", "chamfer", "shell",
|
||||
# thread_cut 是 cut 型特征:必须在已有主体(宿主)上做布尔差,不能凭空
|
||||
# 造实体;无宿主时按 active_body 前置阻止而非让 executor 在 None 上崩溃。
|
||||
"thread_cut",
|
||||
})
|
||||
_BODY_MUTATING_ATOMICS = frozenset({
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through", "loft_add",
|
||||
"extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through", "loft_add", "loft_add_with_cap_face", "sweep_add",
|
||||
"revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add",
|
||||
"thread_add", "thread_cut", *_HOLE_ATOMICS, "fillet", "chamfer",
|
||||
"thread_add", "thread_cut", *_HOLE_ATOMICS, "fillet", "chamfer", "shell", "boolean_bodies",
|
||||
})
|
||||
# A pattern may replay a previous pattern as well as a direct body mutation.
|
||||
# Context-only features have no geometry definition to instance. thread_add
|
||||
@@ -47,7 +47,7 @@ _BODY_MUTATING_ATOMICS = frozenset({
|
||||
# parametric axis, so a replayed thread would silently re-run at the original
|
||||
# location.
|
||||
_REPLAYABLE_ATOMICS = (
|
||||
_BODY_MUTATING_ATOMICS - frozenset({"thread_add", "thread_cut"})
|
||||
_BODY_MUTATING_ATOMICS - frozenset({"thread_add", "thread_cut", "shell"})
|
||||
) | frozenset({"pattern_linear", "pattern_mirror", "pattern_circular"})
|
||||
_SUPPORTED_EXTENTS = frozenset({
|
||||
"blind", "mid_plane", "through_all", "through_all_both", "through_all_and_blind",
|
||||
@@ -163,7 +163,7 @@ def sketch_ids_required_by_contract(cdsl: dict[str, Any]) -> frozenset[str]:
|
||||
atomic_id = str(feature.get("atomic_id") or "")
|
||||
if feature.get("sketch_id") is not None and (contracts.get(atomic_id) or {}).get("requires_sketch"):
|
||||
required.add(str(feature["sketch_id"]))
|
||||
if atomic_id == "loft_add":
|
||||
if atomic_id in {"loft_add", "loft_add_with_cap_face"}:
|
||||
for sketch_id in (feature.get("params") or {}).get("profile_sketch_ids") or ():
|
||||
required.add(str(sketch_id))
|
||||
return frozenset(required)
|
||||
@@ -313,12 +313,13 @@ class CapabilityAnalyzer:
|
||||
sketch_id=node.sketch_id,
|
||||
atomic_id=node.atomic_id,
|
||||
))
|
||||
if node.atomic_id == "loft_add":
|
||||
if node.atomic_id in {"loft_add", "loft_add_with_cap_face"}:
|
||||
profile_ids = params.get("profile_sketch_ids")
|
||||
if not isinstance(profile_ids, list) or len(profile_ids) < 2:
|
||||
minimum_profiles = 1 if node.atomic_id == "loft_add_with_cap_face" else 2
|
||||
if not isinstance(profile_ids, list) or len(profile_ids) < minimum_profiles:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "invalid_loft_profiles",
|
||||
"Loft requires at least two profile sketch ids",
|
||||
f"Loft requires at least {minimum_profiles} profile sketch ids",
|
||||
))
|
||||
elif len({str(sketch_id) for sketch_id in profile_ids}) != len(profile_ids):
|
||||
blockers.append(self._blocker(
|
||||
@@ -361,20 +362,90 @@ class CapabilityAnalyzer:
|
||||
"Loft currently requires exactly one outer profile without holes",
|
||||
sketch_id=sketch_id,
|
||||
))
|
||||
if node.atomic_id.startswith(_SKETCH_ATOM_PREFIXES):
|
||||
# #2 draft:extrudeParams.draft 在 cdsl_schema.json 中被允许,
|
||||
# 但 runtime 的拉伸执行器(build123d Solid.extrude)没有锥形
|
||||
# 拉伸能力,人读契约 profile_schema.json 也未声明该参数。
|
||||
# 若 importer 把 SolidWorks 的 draft_angle_rad 写进 CDSL,
|
||||
# 当前 runtime 会静默产出无拔模角的直壁实体。这里把它从
|
||||
# "静默忽略"改为"显式拒绝"(与 unsupported_extent 同模式)。
|
||||
if params.get("draft"):
|
||||
if node.atomic_id == "sweep_add":
|
||||
path = params.get("path")
|
||||
segment = path.get("segment") if isinstance(path, dict) else None
|
||||
kind = segment.get("type") if isinstance(segment, dict) else None
|
||||
if kind not in {"line", "bspline"}:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "unsupported_draft",
|
||||
"Extrude draft/taper is not implemented; the runtime would silently ignore it",
|
||||
node.feature_id, "unsupported_sweep_path",
|
||||
"Sweep requires one captured line or B-spline path",
|
||||
))
|
||||
elif kind == "line" and not all(
|
||||
isinstance(segment.get(key), list) and len(segment[key]) == 2
|
||||
for key in ("start", "end")
|
||||
):
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "invalid_sweep_path",
|
||||
"Sweep line path requires two-dimensional start and end points",
|
||||
))
|
||||
elif kind == "bspline" and (
|
||||
not isinstance(segment.get("points"), list)
|
||||
or len(segment.get("points") or []) < 3
|
||||
):
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "invalid_sweep_path",
|
||||
"Sweep B-spline path requires at least three interpolation points",
|
||||
))
|
||||
if node.atomic_id == "boolean_bodies":
|
||||
target_ids = params.get("target_feature_ids")
|
||||
tool_ids = params.get("tool_feature_ids")
|
||||
operation = params.get("operation")
|
||||
if operation not in {"union", "subtract", "intersect"}:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "unsupported_boolean_operation",
|
||||
"booleanBodies requires union, subtract or intersect",
|
||||
))
|
||||
for parameter, feature_ids in (("target_feature_ids", target_ids), ("tool_feature_ids", tool_ids)):
|
||||
if not isinstance(feature_ids, list) or not feature_ids:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "missing_boolean_bodies",
|
||||
"booleanBodies requires explicit target and tool feature ids", parameter=parameter,
|
||||
))
|
||||
continue
|
||||
for feature_id in feature_ids:
|
||||
source = nodes_by_id.get(str(feature_id))
|
||||
if source is None:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "boolean_body_unavailable",
|
||||
"booleanBodies source feature does not exist", feature_id=feature_id,
|
||||
))
|
||||
elif source.feature_id not in completed:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "boolean_body_unavailable",
|
||||
"booleanBodies source feature did not become executable", feature_id=feature_id,
|
||||
))
|
||||
if isinstance(target_ids, list) and isinstance(tool_ids, list) and set(target_ids) & set(tool_ids):
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "boolean_body_overlap",
|
||||
"booleanBodies targets and tools must be disjoint",
|
||||
))
|
||||
if node.atomic_id.startswith(_SKETCH_ATOM_PREFIXES):
|
||||
end_condition = params.get("end_condition") or {"type": "blind"}
|
||||
end_type = end_condition.get("type")
|
||||
draft = params.get("draft")
|
||||
if draft is not None:
|
||||
# 目前只将 CADFS 单侧盲向拔模映射到 build123d
|
||||
# Solid.extrude_taper。双向、到面和非实体 profile 的中性面
|
||||
# 语义尚无 CDSL 表达,必须保留为明确能力缺口。
|
||||
if (
|
||||
node.atomic_id not in {"extrude_add_blind", "extrude_cut_blind"}
|
||||
or end_type != "blind"
|
||||
):
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "unsupported_draft_extent",
|
||||
"Draft currently supports only one-sided blind extrusions",
|
||||
))
|
||||
elif not (
|
||||
isinstance(draft, dict)
|
||||
and isinstance(draft.get("angle_deg"), (int, float))
|
||||
and 0 < float(draft["angle_deg"]) < 90
|
||||
and isinstance(draft.get("pull_direction"), bool)
|
||||
):
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "invalid_draft",
|
||||
"Draft requires angle_deg in (0, 90) and boolean pull_direction",
|
||||
))
|
||||
required.append(f"extent:{end_type}")
|
||||
if end_type not in _SUPPORTED_EXTENTS:
|
||||
blockers.append(self._blocker(node.feature_id, "unsupported_extent", "The extent needs a resolved topology selector or is not implemented", extent=end_type))
|
||||
@@ -430,6 +501,13 @@ class CapabilityAnalyzer:
|
||||
))
|
||||
if node.atomic_id in _SELECTOR_REQUIRED and not node.selectors:
|
||||
blockers.append(self._blocker(node.feature_id, "missing_selector", "Dress-up features require an explicit selector"))
|
||||
if node.atomic_id in {"extrude_add_blind_with_hole", "loft_add_with_cap_face"}:
|
||||
face_selectors = [selector for selector in node.selectors if selector.get("kind") == "face"]
|
||||
if len(face_selectors) != 1 or len(node.selectors) != 1:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "invalid_profile_hole_selector",
|
||||
"Derived profile features require exactly one cap-face selector",
|
||||
))
|
||||
if node.atomic_id in _HOLE_ATOMICS:
|
||||
required.append("selector:host_face")
|
||||
if not params.get("host_face"):
|
||||
@@ -529,16 +607,17 @@ class CapabilityAnalyzer:
|
||||
if node.atomic_id in _BODY_MUTATING_ATOMICS:
|
||||
body_available = True
|
||||
body_producers = {
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through", "loft_add",
|
||||
"extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through", "loft_add", "loft_add_with_cap_face", "sweep_add", "boolean_bodies",
|
||||
"revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add",
|
||||
"thread_add",
|
||||
# thread_cut 与 extrude_cut_blind/revolve_cut 一致:无宿主时由
|
||||
# active_body 前置阻止,文档含该类特征即视为携带可执行几何。
|
||||
"thread_cut",
|
||||
}
|
||||
surface_producers = {"extrude_surface", "revolve_surface"}
|
||||
document_blockers: list[RuntimeDiagnostic] = []
|
||||
if not any(node.atomic_id in body_producers for node in plan):
|
||||
if not any(node.atomic_id in body_producers | surface_producers for node in plan):
|
||||
document_blockers.append(RuntimeDiagnostic(
|
||||
"no_solid_feature", "CDSL contains no feature capable of creating a solid body",
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user