feat(cadfs): expand reconstruction capability coverage

This commit is contained in:
2026-09-09 10:11:46 +08:00
parent 5bd1011335
commit 2f89d7930c
21 changed files with 8373 additions and 418 deletions
+73 -4
View File
@@ -10,7 +10,7 @@ from typing import Any
def _score(expected: dict[str, Any], actual: dict[str, Any]) -> float | None:
scores: list[float] = []
reversed_plane_normal = False
for key in ("center_mm", "start_mm", "end_mm", "normal", "axis_origin_mm", "axis_direction"):
for key in ("center_mm", "circle_center_mm", "start_mm", "end_mm", "normal", "axis_origin_mm", "axis_direction"):
if key in expected:
# plane_offset_mm 与平面方程绑定,必须使用记录平面方程时采用的
# plane_normal。face 的局部采样 normal 在 OCC 中可能与其相反。
@@ -101,6 +101,7 @@ def _bound_selector(placeholder: dict[str, Any], records: list[dict[str, Any]])
owners = record.get("owner_feature_ids") or [record.get("feature_id")]
selector = {"kind": placeholder["kind"], "owner_feature_id": str(owners[0]), "stable_id": record["record_id"], "snapshot_id": record["record_id"], "source": "runtime_snapshot", "confidence": 1.0, "geometry": record.get("geometry") or {}}
if placeholder.get("binding_feature_id") is not None: selector["binding_feature_id"] = placeholder["binding_feature_id"]
if placeholder.get("owner_match_required"): selector["owner_match_required"] = True
bound.append(selector)
if placeholder.get("match_mode") == "all":
selector = dict(placeholder); selector["matched_selectors"] = bound
@@ -150,23 +151,76 @@ def bind_candidate_selectors(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list
resolved = []
for placeholder in targets:
records = prefix_records(placeholder.get("binding_feature_id"), placeholder.get("owner_feature_id"))
output_role = str(placeholder.get("output_role") or "").strip()
if output_role:
# Builder output roles are not geometry placeholders. They
# remain in the bound CDSL so runtime can resolve the
# current active B-rep face through exact kernel history.
# Replacing one with stable_id/geometry would mix evidence
# and make a stale snapshot appear durable.
owner = placeholder.get("owner_feature_id")
role_source = placeholder.get("output_role_source")
source_owner = role_source.get("owner_feature_id") if isinstance(role_source, dict) else None
source_role = role_source.get("output_role") if isinstance(role_source, dict) else None
candidates = [
record for record in records
if record.get("kind") == placeholder.get("kind")
and owner in (record.get("owner_feature_ids") or [record.get("feature_id")])
and output_role in (record.get("output_roles") or [])
and (
role_source is None
or any(
item.get("output_role") == output_role
and item.get("owner_feature_id") == source_owner
and item.get("source_output_role") == source_role
for item in record.get("output_role_sources") or []
)
)
]
if len(candidates) != 1:
status = "not_found" if not candidates else "ambiguous"
raise ValueError(
f"{feature['id']}: selector_output_role_{status} after prefix rebuild"
)
resolved.append(candidates[0])
continue
geometry = placeholder.get("geometry") or {}
candidates = _circle_records(geometry, records) if geometry.get("source_circle_radius_mm") else []
if not candidates:
same_kind = [record for record in records if record.get("kind") == placeholder.get("kind")]
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
# Pattern instance provenance is stronger than an ordinary
# feature owner: an unresolved instance cannot fall back to
# an aggregate face with matching geometry. Ordinary source
# selectors retain their established geometry-binding path
# across topology-changing dress-up operations.
pool = owner_matches if placeholder.get("owner_match_required") else (owner_matches or same_kind)
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")
status = "not_found" if not pool else "ambiguous"
raise ValueError(f"{feature['id']}: selector_{status} after prefix rebuild")
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]
# A source owner is preferred for ordinary geometry
# selectors, but can be retained by an unrelated exact
# continuation after a dress-up. If none of that
# owner's active records satisfies the full geometry
# signature, bind against the current active body and
# still require a unique threshold-qualified match.
# Instance-qualified selectors never take this path.
if not scored and not placeholder.get("owner_match_required"):
scored = [
(score, record)
for record in same_kind
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 placeholder.get("match_mode") != "all" and len(scored) > 1 and abs(scored[0][0] - scored[1][0]) <= 1e-9:
@@ -177,7 +231,22 @@ def bind_candidate_selectors(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list
placeholder.clear(); placeholder.update(selector)
resolved.extend(bound_selectors)
feature_selectors = feature.get("selectors") or []
unique = {selector["stable_id"]: selector for selector in feature_selectors}; feature["selectors"] = list(unique.values())
def selector_key(selector: dict[str, Any]) -> tuple[Any, ...]:
stable_id = selector.get("stable_id")
if stable_id is not None:
return ("stable_id", str(stable_id))
source = selector.get("output_role_source")
return (
"output_role",
selector.get("owner_feature_id"),
selector.get("kind"),
selector.get("output_role"),
source.get("owner_feature_id") if isinstance(source, dict) else None,
source.get("output_role") if isinstance(source, dict) else None,
)
unique = {selector_key(selector): selector for selector in feature_selectors}
feature["selectors"] = list(unique.values())
if feature.get("atomic_id") == "pattern_mirror":
planes = [selector for selector in feature["selectors"] if selector.get("kind") == "plane"]
if len(planes) != 1: