"""Loft and sweep executors (loft_add / loft_add_with_cap_face / sweep_add).""" from __future__ import annotations from typing import TYPE_CHECKING, Any from ..registry import atomic_executor from ..topology import FeaturePlanNode, FeatureResult from .common import _sweep_path if TYPE_CHECKING: # pragma: no cover - import for type checkers only from ..session import ExecutionSession def _execute_loft_add(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult: # 放样截面不占用 feature.sketch_id;按有序 profile_sketch_ids 取已解析 # 草图,并由 adapter 统一校验单闭环、无内环等内核输入约束。 profile_ids = node.params.get("profile_sketch_ids") or [] profiles: list[dict[str, Any]] = [] for sketch_id in profile_ids: sketch = session.sketches.get(str(sketch_id)) if sketch is None: raise ValueError(f"loft profile sketch {sketch_id!r} is not resolved") profiles.append(sketch) solid, topology_delta = session.adapter.loft_with_topology_delta(profiles) body = session.adapter.fuse(session.body, solid) # Fusing a loft into an existing body replaces its subshapes through a # different builder. Only an initial direct loft can expose this builder's # cap evidence for the final B-rep snapshot. if session.body is not None: topology_delta = None session.register_body(node.feature_id, body, replay_node=node, topology_delta=topology_delta) return session.result(node) @atomic_executor("loft_add") def _loft_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch return _execute_loft_add(node, session) def _execute_loft_add_with_cap_face(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult: resolved = [session.resolve(selector) for selector in node.selectors] failed = next((item for item in resolved if item.status != "resolved"), None) if failed or len(resolved) != 1 or resolved[0].record is None or resolved[0].record.kind != "face": raise ValueError(failed.diagnostic.message if failed and failed.diagnostic else "cap-face loft selector is unresolved") profile_ids = node.params.get("profile_sketch_ids") or [] profiles: list[dict[str, Any]] = [] for sketch_id in profile_ids: sketch = session.sketches.get(str(sketch_id)) if sketch is None: raise ValueError(f"loft profile sketch {sketch_id!r} is not resolved") profiles.append(sketch) solid = session.adapter.loft_with_cap_face(resolved[0].record.value, profiles) session.register_body(node.feature_id, session.adapter.fuse(session.body, solid), replay_node=node) return session.result(node) @atomic_executor("loft_add_with_cap_face") def _loft_cap_face_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch return _execute_loft_add_with_cap_face(node, session) def _execute_sweep_add(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None = None) -> FeatureResult: profile = sketch or session.sketches.get(str(node.sketch_id)) if profile is None: raise ValueError("sweep has no resolved profile sketch") faces = session.adapter.faces_for_sketch(profile) if len(faces) != 1: raise ValueError("sweep requires exactly one closed profile region") solid, topology_delta = session.adapter.sweep_with_topology_delta( faces[0], _sweep_path(node, session), is_frenet=bool(node.params.get("is_frenet", False)), ) is_new_body = node.params.get("result_mode") == "new_body" body = session.adapter.combine(session.body, solid) if is_new_body else session.adapter.fuse(session.body, solid) # A union rebuilds topology, so the pipe-shell builder cannot prove the # final aggregate's relations. The independent-body path retains its exact # subshape identity and may expose evidence for the new member. if session.body is not None and not is_new_body: topology_delta = None session.register_body(node.feature_id, body, replay_node=node, topology_delta=topology_delta) return session.result(node) @atomic_executor("sweep_add") def _sweep_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: return _execute_sweep_add(node, session, sketch)