"""Surface-feature executors (extrude_surface / revolve_surface). Surface features register an independent shell and never touch the active solid body's fuse/cut lifecycle. """ from __future__ import annotations from typing import TYPE_CHECKING, Any from ..extents import _normal_from_sketch from ..registry import atomic_executor from ..specs import AxisSpec, PlaneSpec, vector_cross, vector_dot, vector_scale, vector_unit from ..topology import FeaturePlanNode, FeatureResult from .common import _revolve_axis, _validate_revolve_axis_in_sketch_plane if TYPE_CHECKING: # pragma: no cover - import for type checkers only from ..session import ExecutionSession def _execute_revolve_surface(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult: # Surface revolve 的 profile 是单一闭合 wire。它只生成独立 shell,不能参与 # 当前实体 body 的 fuse/cut,也不能把其结果误报为新的实体 body。 sketch = session.sketches.get(str(node.sketch_id)) if sketch is None: raise ValueError("surface revolve has no resolved sketch") faces = session.adapter.faces_for_sketch(sketch) if len(faces) != 1 or faces[0].inner_wires(): raise ValueError("surface revolve requires exactly one closed profile without holes") axis = _revolve_axis(node, session) _validate_revolve_axis_in_sketch_plane(axis, sketch) angle = float(node.params.get("angle_deg") or 0.0) if angle <= 0: raise ValueError("surface revolve requires angle_deg > 0") if bool(node.params.get("reverse")): angle = -angle surface_id = session.register_surface( node.feature_id, session.adapter.revolve_surface(faces[0].outer_wire(), angle, axis), ) return session.result(node, include_body=False, surface_id=surface_id) @atomic_executor("revolve_surface") def _revolve_surface_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch return _execute_revolve_surface(node, session) def _execute_extrude_surface(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult: # surfaceEntities 的曲面拉伸沿用实体特征已 lower 的距离,但始终独立登记为 # shell。它既不改变 active solid,也不以曲面参与实体 fuse/cut。 sketch = session.sketches.get(str(node.sketch_id)) if sketch is None: raise ValueError("surface extrude has no resolved sketch") direction = vector_unit(_normal_from_sketch(sketch), field_name="sketch normal") if bool(node.params.get("reverse")): direction = vector_scale(direction, -1) distance = float(node.params.get("distance_mm") or 0.0) if distance <= 0: raise ValueError("surface extrude requires distance_mm > 0") wires = session.adapter.surface_wires_for_sketch(sketch) surface = session.adapter.extrude_surface(wires, vector_scale(direction, distance)) reverse_distance = float(node.params.get("reverse_distance_mm") or 0.0) if reverse_distance > 0: opposite = session.adapter.extrude_surface(wires, vector_scale(direction, -reverse_distance)) surface = session.adapter.combine_surfaces(surface, opposite) surface_id = session.register_surface(node.feature_id, surface) return session.result(node, include_body=False, surface_id=surface_id) @atomic_executor("extrude_surface") def _extrude_surface_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch return _execute_extrude_surface(node, session) def _execute_loft_surface(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult: profile_ids = node.params.get("profile_sketch_ids") or [] profiles: list[dict[str, Any]] = [] for sketch_id in profile_ids: profile = session.sketches.get(str(sketch_id)) if profile is None: raise ValueError(f"surface loft profile sketch {sketch_id!r} is not resolved") profiles.append(profile) surface_id = session.register_surface(node.feature_id, session.adapter.loft_surface(profiles)) return session.result(node, include_body=False, surface_id=surface_id) @atomic_executor("loft_surface") def _loft_surface_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch return _execute_loft_surface(node, session)