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:
@@ -14,6 +14,12 @@ def _score(expected: dict[str, Any], actual: dict[str, Any]) -> float | None:
|
||||
left, right = expected[key], actual.get(key)
|
||||
if not isinstance(right, (list, tuple)) or len(left) != len(right): return None
|
||||
delta = math.sqrt(sum((float(a) - float(b)) ** 2 for a, b in zip(left, right)))
|
||||
if key == "normal":
|
||||
# A source CAP_FACE identifies a geometric plane, not the OCC
|
||||
# orientation of the resulting face. The two kernels may
|
||||
# report the same cap with inverse normals, especially for a
|
||||
# start cap. Keep axis direction orientation-sensitive.
|
||||
delta = min(delta, math.sqrt(sum((float(a) + float(b)) ** 2 for a, b in zip(left, right))))
|
||||
scores.append(max(0.0, 1.0 - delta / 0.05))
|
||||
for key in ("radius_mm", "plane_offset_mm"):
|
||||
if key in expected:
|
||||
@@ -75,7 +81,16 @@ def bind_candidate_selectors(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list
|
||||
if not prefix["features"]: raise ValueError(f"{feature['id']}: selector has no executable prefix")
|
||||
report = rebuild_cdsl(prefix, Path(temporary) / f"prefix-{index}.step", strict=True)
|
||||
body_id = next((item.get("body_id") for item in reversed(report.get("feature_results") or []) if item.get("body_id")), None)
|
||||
records = [item for item in report.get("topology_records") or [] if not body_id or item.get("body_id") == body_id or str(item.get("body_id") or "").startswith(f"{body_id}:")]
|
||||
records = [
|
||||
item for item in report.get("topology_records") or []
|
||||
# Reference planes and axes are session context, not body
|
||||
# topology. They must remain available while binding a mirror
|
||||
# or extent selector against a body-bearing prefix.
|
||||
if item.get("kind") in {"plane", "axis"}
|
||||
or not body_id
|
||||
or item.get("body_id") == body_id
|
||||
or str(item.get("body_id") or "").startswith(f"{body_id}:")
|
||||
]
|
||||
resolved = []
|
||||
for placeholder in placeholders:
|
||||
geometry = placeholder.get("geometry") or {}
|
||||
@@ -85,14 +100,21 @@ def bind_candidate_selectors(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list
|
||||
owner = placeholder.get("owner_feature_id")
|
||||
owner_matches = [record for record in same_kind if owner in (record.get("owner_feature_ids") or [record.get("feature_id")])]
|
||||
pool = owner_matches or same_kind
|
||||
if not geometry and len(pool) != 1:
|
||||
raise ValueError(f"{feature['id']}: selector_ambiguous after prefix rebuild")
|
||||
scored = [(score, record) for record in pool if (score := _score(geometry, record.get("geometry") or {})) is not None and score >= 0.8]
|
||||
scored.sort(key=lambda value: (-value[0], str(value[1].get("record_id"))))
|
||||
if scored:
|
||||
if len(scored) > 1 and abs(scored[0][0] - scored[1][0]) <= 1e-9:
|
||||
if not geometry:
|
||||
# Context selectors (notably a generated mirror plane)
|
||||
# may have no geometric snapshot. Their owner-qualified
|
||||
# singleton identity is sufficient and must not be scored
|
||||
# as a zero-information geometric match.
|
||||
if len(pool) != 1:
|
||||
raise ValueError(f"{feature['id']}: selector_ambiguous after prefix rebuild")
|
||||
candidates = [scored[0][1]]
|
||||
candidates = [pool[0]]
|
||||
else:
|
||||
scored = [(score, record) for record in pool if (score := _score(geometry, record.get("geometry") or {})) is not None and score >= 0.8]
|
||||
scored.sort(key=lambda value: (-value[0], str(value[1].get("record_id"))))
|
||||
if scored:
|
||||
if len(scored) > 1 and abs(scored[0][0] - scored[1][0]) <= 1e-9:
|
||||
raise ValueError(f"{feature['id']}: selector_ambiguous after prefix rebuild")
|
||||
candidates = [scored[0][1]]
|
||||
if not candidates: raise ValueError(f"{feature['id']}: selector_not_found after prefix rebuild")
|
||||
for record in candidates:
|
||||
owners = record.get("owner_feature_ids") or [record.get("feature_id")]
|
||||
|
||||
Reference in New Issue
Block a user