feat(cadfs): expand reconstruction capability coverage
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user