feat(cadfs): expand selector provenance coverage
This commit is contained in:
@@ -14,6 +14,7 @@ from typing import Any, Protocol
|
||||
|
||||
from .build123d_adapter import Build123dGeometryAdapter
|
||||
from .runtime_base import FeatureExecutionError
|
||||
from .sketch_solver import resolve_profile
|
||||
from .specs import AxisSpec, BendSpec, GearSpec, HoleSpec, PlaneSpec, RackSpec, ThreadSpec, Vector3
|
||||
from .topology import (
|
||||
FeaturePlanNode,
|
||||
@@ -38,18 +39,25 @@ class GeometryAdapter(Protocol):
|
||||
def body_solids(self, body: Any) -> list[Any]: ...
|
||||
def body_geometry(self, body: Any) -> dict[str, Any]: ...
|
||||
def surface_geometry(self, surface: Any) -> dict[str, Any]: ...
|
||||
def faces_for_sketch(self, sketch: dict[str, Any]) -> list[Any]: ...
|
||||
def faces_for_sketch_with_source_anchors(self, sketch: dict[str, Any]) -> tuple[list[Any], list[dict[str, Any]]]: ...
|
||||
def faces_for_sketch(self, sketch: dict[str, Any], *, support_face: Any | None = None,
|
||||
external_anchor_edges: dict[str, Any] | None = None) -> list[Any]: ...
|
||||
def faces_for_sketch_with_source_anchors(self, sketch: dict[str, Any], *, support_face: Any | None = None,
|
||||
external_anchor_edges: dict[str, Any] | None = None) -> tuple[list[Any], list[dict[str, Any]]]: ...
|
||||
def face_with_holes(self, outer: Any, holes: list[Any]) -> Any: ...
|
||||
def loft(self, sketches: list[dict[str, Any]]) -> Any: ...
|
||||
def loft_with_topology_delta(self, sketches: list[dict[str, Any]]) -> tuple[Any, TopologyDelta | None]: ...
|
||||
def loft_surface(self, sketches: list[dict[str, Any]]) -> Any: ...
|
||||
def loft_with_cap_face(self, cap_face: Any, sketches: list[dict[str, Any]]) -> Any: ...
|
||||
def sweep(self, section: Any, spine: Any, *, inner_wires: list[Any] | None = None, make_solid: bool = True, is_frenet: bool = False, transition: Any = None) -> Any: ...
|
||||
def sweep_with_topology_delta(self, section: Any, spine: Any, *, inner_wires: list[Any] | None = None, make_solid: bool = True, is_frenet: bool = False, transition: Any = None) -> tuple[Any, TopologyDelta | None]: ...
|
||||
def sweep_path(self, points: list[Vector3], *, start_tangent: Vector3 | None = None, end_tangent: Vector3 | None = None, parameters: list[float] | None = None) -> Any: ...
|
||||
def sweep_arc_path(self, start: Vector3, end: Vector3, center: Vector3, normal: Vector3, *, radius_mm: float, clockwise: bool) -> Any: ...
|
||||
def sweep_circle_path(self, center: Vector3, x_dir: Vector3, normal: Vector3, *, radius_mm: float) -> Any: ...
|
||||
def face_normal(self, face: Any) -> Vector3: ...
|
||||
def planar_face_workplane(self, face: Any) -> PlaneSpec: ...
|
||||
def extrude(self, face: Any, direction: Vector3) -> Any: ...
|
||||
def extrude_with_topology_delta(self, face: Any, direction: Vector3) -> tuple[Any, TopologyDelta]: ...
|
||||
def extrude_faces_with_composed_topology_delta(self, faces: list[Any], direction: Vector3) -> tuple[Any, TopologyDelta] | None: ...
|
||||
def extrude_taper_with_topology_delta(self, face: Any, direction: Vector3, taper_deg: float) -> tuple[Any, TopologyDelta | None]: ...
|
||||
def extrude_taper(self, face: Any, direction: Vector3, taper_deg: float) -> Any: ...
|
||||
def extrude_trimmed(self, face: Any, target: Any, direction: Vector3) -> Any: ...
|
||||
@@ -57,6 +65,7 @@ class GeometryAdapter(Protocol):
|
||||
def extrude_surface(self, wires: list[Any], direction: Vector3) -> Any: ...
|
||||
def combine_surfaces(self, *surfaces: Any) -> Any: ...
|
||||
def revolve(self, face: Any, angle_deg: float, axis: AxisSpec) -> Any: ...
|
||||
def revolve_with_topology_delta(self, face: Any, angle_deg: float, axis: AxisSpec) -> tuple[Any, TopologyDelta]: ...
|
||||
def revolve_surface(self, wire: Any, angle_deg: float, axis: AxisSpec) -> Any: ...
|
||||
def intersect(self, left: Any, right: Any) -> Any: ...
|
||||
def intersect_with_topology_delta(self, left: Any, right: Any) -> tuple[Any, TopologyDelta | None]: ...
|
||||
@@ -82,6 +91,8 @@ class GeometryAdapter(Protocol):
|
||||
def profile_touches_target(self, target: Any, faces: list[Any]) -> bool: ...
|
||||
def next_body_face_after(self, body: Any, faces: list[Any], direction: Vector3, *, excluded_face: Any) -> Any: ...
|
||||
def uniform_intersection_distance(self, target: Any, faces: list[Any], direction: Vector3) -> float: ...
|
||||
def target_has_forward_intersection(self, target: Any, faces: list[Any], direction: Vector3) -> bool: ...
|
||||
def uniform_planar_supporting_surface_distance(self, target: Any, faces: list[Any], direction: Vector3) -> float: ...
|
||||
def fillet(self, body: Any, radius_mm: float, edges: list[Any]) -> Any: ...
|
||||
def fillet_with_topology_delta(self, body: Any, radius_mm: float, edges: list[Any]) -> tuple[Any, TopologyDelta | None]: ...
|
||||
def tangent_edges(self, body: Any, seeds: list[Any]) -> list[Any]: ...
|
||||
@@ -104,10 +115,85 @@ class ExecutionSession:
|
||||
results: dict[str, FeatureResult] = field(default_factory=dict)
|
||||
replay_definitions: dict[str, FeaturePlanNode] = field(default_factory=dict)
|
||||
body_members: dict[str, Any] = field(default_factory=dict)
|
||||
body_member_snapshot_ids: dict[str, str] = field(default_factory=dict)
|
||||
surface_members: dict[str, Any] = field(default_factory=dict)
|
||||
sketch_attachment_faces: dict[str, Any] = field(default_factory=dict)
|
||||
sketch_imprint_external_edges: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
selector_resolutions: list[dict[str, Any]] = field(default_factory=list)
|
||||
active_feature_id: str = ""
|
||||
|
||||
def resolve_sketch_attachment(self, sketch_id: str, *, feature_id: str | None = None) -> None:
|
||||
"""Resolve one runtime-attached sketch immediately before consumption."""
|
||||
sketch = self.sketches.get(sketch_id)
|
||||
if sketch is None or not isinstance(sketch.get("attachment"), dict):
|
||||
return
|
||||
previous_feature_id = self.active_feature_id
|
||||
if feature_id is not None:
|
||||
self.active_feature_id = feature_id
|
||||
try:
|
||||
resolution = self.resolve(sketch["attachment"])
|
||||
if resolution.status != "resolved" or resolution.record is None or resolution.record.kind != "face":
|
||||
detail = resolution.diagnostic.message if resolution.diagnostic else "attached face was not resolved"
|
||||
raise FeatureExecutionError("sketch_attachment_unresolved", detail)
|
||||
try:
|
||||
plane = self.adapter.planar_face_workplane(resolution.record.value)
|
||||
except ValueError as error:
|
||||
raise FeatureExecutionError("sketch_attachment_nonplanar", str(error)) from error
|
||||
external_edges: dict[str, Any] = {}
|
||||
profile = sketch.get("profile") or {}
|
||||
for entry in profile.get("external_anchors") or ():
|
||||
anchor_id = entry.get("id") if isinstance(entry, dict) else None
|
||||
selector = entry.get("selector") if isinstance(entry, dict) else None
|
||||
if not isinstance(anchor_id, str) or not anchor_id or not isinstance(selector, dict):
|
||||
raise FeatureExecutionError("imprint_external_anchor_invalid", "external anchor contract is invalid")
|
||||
edge_resolution = self.resolve(selector)
|
||||
if (
|
||||
edge_resolution.status != "resolved"
|
||||
or edge_resolution.record is None
|
||||
or edge_resolution.record.kind != "edge"
|
||||
):
|
||||
detail = edge_resolution.diagnostic.message if edge_resolution.diagnostic else "external anchor edge was not resolved"
|
||||
raise FeatureExecutionError("imprint_external_anchor_unresolved", detail)
|
||||
if not any(edge.wrapped.IsSame(edge_resolution.record.value.wrapped) for edge in resolution.record.value.edges()):
|
||||
raise FeatureExecutionError(
|
||||
"imprint_external_anchor_not_support_boundary",
|
||||
"external anchor edge is not an exact boundary of the attached support face",
|
||||
)
|
||||
external_edges[anchor_id] = edge_resolution.record.value
|
||||
materialized = deepcopy(sketch)
|
||||
materialized["workplane"] = plane.as_dict()
|
||||
# IMPRINT face-side disambiguation is expressed in the source
|
||||
# sketch's oriented plane. A native attached face may have the
|
||||
# opposite normal, which reverses the physical side of the same
|
||||
# local curve after its coordinates are mapped to that face.
|
||||
# This is a session-local frame conversion, not selector or
|
||||
# geometry fallback data.
|
||||
source_profile = materialized.get("profile") or {}
|
||||
if source_profile.get("type") == "planar_imprint":
|
||||
try:
|
||||
source_plane = PlaneSpec.from_mapping(sketch.get("workplane") or {})
|
||||
except ValueError as error:
|
||||
raise FeatureExecutionError("sketch_attachment_frame_invalid", str(error)) from error
|
||||
orientation = sum(
|
||||
source_plane.normal[index] * plane.normal[index]
|
||||
for index in range(3)
|
||||
)
|
||||
if orientation < 0.0:
|
||||
for selection in source_profile.get("selections") or ():
|
||||
if isinstance(selection, dict) and selection.get("face_side") in {-1, 1}:
|
||||
selection["face_side"] = -selection["face_side"]
|
||||
# Preflight intentionally leaves attached sketches local. Resolve now
|
||||
# so every world-space contour is rebuilt from the proven face frame.
|
||||
materialized = resolve_profile(materialized)
|
||||
self.sketches[sketch_id] = materialized
|
||||
# Keep exact supports only in the live session. They are never CDSL
|
||||
# geometry, serialized record IDs, or selector fallback evidence.
|
||||
self.sketch_attachment_faces[sketch_id] = resolution.record.value
|
||||
if external_edges:
|
||||
self.sketch_imprint_external_edges[sketch_id] = external_edges
|
||||
finally:
|
||||
self.active_feature_id = previous_feature_id
|
||||
|
||||
def register_body(
|
||||
self,
|
||||
feature_id: str,
|
||||
@@ -123,6 +209,8 @@ class ExecutionSession:
|
||||
# 拉伸)。body_id 现在反映真实实体结构而不是"最后一个特征的 id":
|
||||
# 每个独立 Solid 一个 body:{feature}:{index},供 selector 精确匹配目标
|
||||
# 实体;单体保持 body:{feature}(与历史行为完全一致)。
|
||||
previous_members = dict(self.body_members)
|
||||
previous_snapshot_ids = dict(self.body_member_snapshot_ids)
|
||||
self.body = body
|
||||
self.body_id = f"body:{feature_id}"
|
||||
self.body_members = dict(body_members) if body_members is not None else {feature_id: body}
|
||||
@@ -135,11 +223,27 @@ class ExecutionSession:
|
||||
self.topology.register(anchor)
|
||||
predecessors = [*(topology_predecessors or ()), *anchors]
|
||||
solids = self.adapter.body_solids(body)
|
||||
member_snapshot_ids: dict[str, str] = {}
|
||||
for member_key, member in self.body_members.items():
|
||||
matches = [
|
||||
index for index, solid in enumerate(solids)
|
||||
if TopologyRegistry._same_topology_value(member, solid)
|
||||
]
|
||||
if len(matches) == 1:
|
||||
member_snapshot_ids[member_key] = (
|
||||
self.body_id if len(solids) == 1 else f"{self.body_id}:{matches[0]}"
|
||||
)
|
||||
member_preservations = [
|
||||
(previous_snapshot_ids[key], member_snapshot_ids[key])
|
||||
for key, member in previous_members.items()
|
||||
if key in previous_snapshot_ids and key in member_snapshot_ids
|
||||
and TopologyRegistry._same_topology_value(member, self.body_members[key])
|
||||
]
|
||||
if len(solids) <= 1:
|
||||
self.topology.replace_body_topology(
|
||||
feature_id, self.body_id, self.adapter.topology_records(body, feature_id, self.body_id),
|
||||
topology_delta=topology_delta,
|
||||
additional_predecessors=predecessors,
|
||||
additional_predecessors=predecessors, member_preservations=member_preservations,
|
||||
)
|
||||
else:
|
||||
# 一个 Compound 的全部成员共享同一个前置 body snapshot。逐个登记会让
|
||||
@@ -152,7 +256,7 @@ class ExecutionSession:
|
||||
]
|
||||
self.topology.replace_body_topologies(
|
||||
feature_id, members, active_body_id=self.body_id, topology_delta=topology_delta,
|
||||
additional_predecessors=predecessors,
|
||||
additional_predecessors=predecessors, member_preservations=member_preservations,
|
||||
)
|
||||
self.topology.register(TopologyRecord(
|
||||
record_id=self.body_id, kind="body", feature_id=feature_id, body_id=self.body_id,
|
||||
@@ -160,6 +264,7 @@ class ExecutionSession:
|
||||
))
|
||||
if replay_node is not None:
|
||||
self.replay_definitions[feature_id] = replay_node
|
||||
self.body_member_snapshot_ids = member_snapshot_ids
|
||||
|
||||
def register_transient_prism_tool(
|
||||
self,
|
||||
@@ -198,6 +303,7 @@ class ExecutionSession:
|
||||
self.body = None
|
||||
self.body_id = None
|
||||
self.body_members = {}
|
||||
self.body_member_snapshot_ids = {}
|
||||
|
||||
def _record_selector_resolution(self, resolution: SelectorResolution) -> SelectorResolution:
|
||||
evidence = resolution.as_dict()
|
||||
|
||||
Reference in New Issue
Block a user