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:
@@ -19,21 +19,22 @@ from .operation_contracts import materialized_feature_contracts
|
||||
_SELECTOR_REQUIRED = frozenset({"fillet", "chamfer"})
|
||||
_SKETCH_ATOM_PREFIXES = ("extrude_", "revolve_")
|
||||
_PRIMARY_ATOMICS = frozenset({
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind",
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through",
|
||||
"revolve_add", "revolve_cut", "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_through", "revolve_cut", *_HOLE_ATOMICS, "fillet", "chamfer",
|
||||
"extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through",
|
||||
"revolve_cut", *_HOLE_ATOMICS, "fillet", "chamfer",
|
||||
# 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_through",
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through", "loft_add",
|
||||
"revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add",
|
||||
"thread_add", "thread_cut", *_HOLE_ATOMICS, "fillet", "chamfer",
|
||||
})
|
||||
@@ -145,27 +146,32 @@ def _schema_contract() -> dict[str, dict[str, Any]]:
|
||||
|
||||
|
||||
def sketch_ids_required_by_contract(cdsl: dict[str, Any]) -> frozenset[str]:
|
||||
"""Return only sketches consumed by a feature with a sketch contract.
|
||||
"""Return only sketches consumed by executable feature contracts.
|
||||
|
||||
CAD documents commonly preserve construction or abandoned sketches whose
|
||||
contours are incomplete. They are semantic data, but must not make an
|
||||
otherwise independent feature history ineligible for execution.
|
||||
otherwise independent feature history ineligible for execution. Most
|
||||
operations declare ``sketch_id``; loft declares its ordered section set
|
||||
in ``params.profile_sketch_ids``.
|
||||
"""
|
||||
contracts = _schema_contract()
|
||||
return frozenset(
|
||||
str(feature["sketch_id"])
|
||||
for feature in cdsl.get("features") or ()
|
||||
if feature.get("sketch_id") is not None
|
||||
and (contracts.get(str(feature.get("atomic_id") or "")) or {}).get("requires_sketch")
|
||||
)
|
||||
required: set[str] = set()
|
||||
for feature in cdsl.get("features") or ():
|
||||
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":
|
||||
for sketch_id in (feature.get("params") or {}).get("profile_sketch_ids") or ():
|
||||
required.add(str(sketch_id))
|
||||
return frozenset(required)
|
||||
|
||||
|
||||
def _has_closed_region(sketch: dict[str, Any]) -> bool:
|
||||
"""Mirror the adapter's input contract without importing the geometry kernel."""
|
||||
regions = sketch.get("contour_regions_mm") or []
|
||||
if any(len(region.get("outer") or []) >= 2 for region in regions if isinstance(region, dict)):
|
||||
if any(len(region.get("outer") or []) >= 1 for region in regions if isinstance(region, dict)):
|
||||
return True
|
||||
if len(sketch.get("contour_edges_mm") or []) >= 2:
|
||||
if len(sketch.get("contour_edges_mm") or []) >= 1:
|
||||
return True
|
||||
return any(
|
||||
entity.get("type") == "circle" and not entity.get("construction")
|
||||
@@ -175,6 +181,18 @@ def _has_closed_region(sketch: dict[str, Any]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _has_single_loft_region(sketch: dict[str, Any]) -> bool:
|
||||
"""Whether a resolved profile maps exactly to one solid loft section."""
|
||||
regions = sketch.get("contour_regions_mm") or []
|
||||
if regions:
|
||||
return len(regions) == 1 and not (regions[0].get("holes") or [])
|
||||
circles = [
|
||||
entity for entity in sketch.get("entities") or []
|
||||
if isinstance(entity, dict) and entity.get("type") == "circle" and not entity.get("construction")
|
||||
]
|
||||
return len(circles) == 1
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CapabilityAnalysis:
|
||||
plan: tuple[FeaturePlanNode, ...]
|
||||
@@ -281,6 +299,54 @@ class CapabilityAnalyzer:
|
||||
"The resolved sketch contains no closed profile region",
|
||||
sketch_id=node.sketch_id,
|
||||
))
|
||||
if node.atomic_id == "loft_add":
|
||||
profile_ids = params.get("profile_sketch_ids")
|
||||
if not isinstance(profile_ids, list) or len(profile_ids) < 2:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "invalid_loft_profiles",
|
||||
"Loft requires at least two profile sketch ids",
|
||||
))
|
||||
elif len({str(sketch_id) for sketch_id in profile_ids}) != len(profile_ids):
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "invalid_loft_profiles",
|
||||
"Loft profile sketch ids must be distinct",
|
||||
))
|
||||
else:
|
||||
for sketch_id in profile_ids:
|
||||
sketch_id = str(sketch_id)
|
||||
profile = sketches.get(sketch_id)
|
||||
if profile is None:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "missing_loft_profile",
|
||||
"Loft profile sketch does not exist", sketch_id=sketch_id,
|
||||
))
|
||||
continue
|
||||
profile_type = str((profile.get("profile") or {}).get("type") or "")
|
||||
required.append(f"loft_profile:{profile_type}")
|
||||
resolution_error = sketch_errors.get(sketch_id)
|
||||
if resolution_error:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "profile_resolution_failed",
|
||||
"A loft profile sketch could not be resolved into executable regions",
|
||||
sketch_id=sketch_id, reason=resolution_error,
|
||||
))
|
||||
elif profile_type not in self.profile_types:
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "unsupported_profile",
|
||||
"The current runtime cannot resolve a loft profile",
|
||||
sketch_id=sketch_id, profile_type=profile_type,
|
||||
))
|
||||
elif not _has_closed_region(profile):
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "profile_no_closed_region",
|
||||
"A loft profile contains no closed region", sketch_id=sketch_id,
|
||||
))
|
||||
elif not _has_single_loft_region(profile):
|
||||
blockers.append(self._blocker(
|
||||
node.feature_id, "unsupported_loft_profile_regions",
|
||||
"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)没有锥形
|
||||
@@ -318,7 +384,7 @@ class CapabilityAnalyzer:
|
||||
node.feature_id, "missing_offset_distance",
|
||||
"Offset-from-surface requires a non-zero captured offset distance",
|
||||
))
|
||||
if node.atomic_id == "extrude_add_two_sided":
|
||||
if node.atomic_id in {"extrude_add_two_sided", "extrude_cut_two_sided"}:
|
||||
reverse_condition = params.get("reverse_end_condition") or {"type": "blind"}
|
||||
reverse_type = reverse_condition.get("type")
|
||||
required.append(f"extent:reverse:{reverse_type}")
|
||||
@@ -449,7 +515,8 @@ 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_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
|
||||
"extrude_cut_through", "loft_add",
|
||||
"revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add",
|
||||
"thread_add",
|
||||
# thread_cut 与 extrude_cut_blind/revolve_cut 一致:无宿主时由
|
||||
|
||||
Reference in New Issue
Block a user