"""Reference-geometry executors (reference_plane / reference_axis). Context features produce no solid; they register durable topology contexts that later features resolve through owner-qualified selectors. """ from __future__ import annotations import math from typing import TYPE_CHECKING, Any from ..registry import atomic_executor from ..specs import AxisSpec, PlaneSpec, vector_add, vector_cross, vector_dot, vector_scale, vector_unit from ..topology import FeaturePlanNode, FeatureResult if TYPE_CHECKING: # pragma: no cover - import for type checkers only from ..session import ExecutionSession @atomic_executor("reference_plane") def _reference_plane_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch # 基准面特征(reference_plane)执行入口:从参数解析平面并登记为拓扑上下文。 # 1. 从特征参数 plane 中解析出平面定义 PlaneSpec(原点到法向)。 plane = PlaneSpec.from_mapping(node.params.get("plane") or {}) # 2. 将该平面注册到拓扑上下文,供后续特征(如草图基准、参考轴)引用。 session.topology.register_context(node.feature_id, plane) # 3. 返回结果对象,并将该平面作为上下文一并携带。 return session.result(node, context=plane) @atomic_executor("reference_axis") def _reference_axis_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch # 基准轴特征(reference_axis)执行入口:由参数直接定义轴,或由两个基准平面求交线得到轴。 # 1. 尝试直接取参数:若同时给出原点 origin_mm 与方向 direction,则直接构造轴。 params = node.params.get("axis") or {} if params.get("origin_mm") and params.get("direction"): axis = AxisSpec.from_mapping(params) else: # 2. 否则从特征选择器中筛选出已解析的基准平面。 planes = [session.resolve(selector) for selector in node.selectors if selector.get("kind") == "plane"] resolved = [item.record.value for item in planes if item.status == "resolved" and isinstance(item.record.value, PlaneSpec)] # 3. 校验:轴需要两个非平行的平面,不足两个则报错。 if len(resolved) < 2: raise ValueError("reference axis requires two uniquely resolved planes") # 4. 用两平面法线叉积求交线方向;若方向长度接近 0 说明两平面平行,无法成轴。 first, second = resolved[0], resolved[1] n1, n2 = first.normal, second.normal direction = vector_cross(n1, n2) squared_length = vector_dot(direction, direction) if squared_length <= 1e-18: raise ValueError("reference planes are parallel and cannot define an axis") # 5. 求交线上的一点:两平面到各自原点的垂距参与线性组合,得到交线上的最近点。 d1 = vector_dot(n1, first.origin_mm) d2 = vector_dot(n2, second.origin_mm) point = vector_scale(vector_add(vector_scale(vector_cross(n2, direction), d1), vector_scale(vector_cross(direction, n1), d2)), 1 / squared_length) # 6. 由该点与归一化的交线方向组合成基准轴 AxisSpec。 axis = AxisSpec(origin_mm=point, direction=vector_unit(direction, field_name="reference axis")) # 7. 注册为拓扑上下文,并返回结果对象(携带该轴)。 session.topology.register_context(node.feature_id, axis) return session.result(node, context=axis) @atomic_executor("reference_point") def _reference_point_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: """Execute an explicit source-datum point without changing model topology.""" del sketch point = node.params.get("point_mm") if ( not isinstance(point, list) or len(point) != 3 or not all(isinstance(value, (int, float)) and math.isfinite(float(value)) for value in point) ): raise ValueError("reference point requires one finite three-dimensional point") return session.result(node, include_body=False) @atomic_executor("assign_variable") def _assign_variable_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: """Replay a source variable declaration after lowering has consumed it.""" del sketch name = node.params.get("name") value = node.params.get("value") if not isinstance(name, str) or not name or not isinstance(value, (int, float)) or not math.isfinite(float(value)): raise ValueError("assign_variable requires one finite named value") return session.result(node, include_body=False)