994d06aaea
- 新增 selector_candidate_demo,移除 provenance intent 后枚举候选 selector - 对候选分支执行有界重建与严格 STEP 比较 - 仅在候选遍历完整且唯一 strict 通过时生成 selector 映射记录 - 增加 selector 候选搜索、预算限制和记录生成的测试 - 保持生产 selector resolver 不受 Demo 逻辑影响 - 更新 CADFS 能力台账,记录 IMPRINT 派生 profile 的 lineage selector 缺口
100 lines
5.1 KiB
Python
100 lines
5.1 KiB
Python
"""Hole executors (hole_blind / hole_countersink / hole_counterbore / hole_wizard)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import TYPE_CHECKING, Any
|
||
|
||
from ..registry import atomic_executor
|
||
from ..specs import HoleSpec, PlaneSpec, vector_scale
|
||
from ..topology import FeaturePlanNode, FeatureResult, RuntimeDiagnostic
|
||
from .common import _hole_starts, _host_plane
|
||
|
||
if TYPE_CHECKING: # pragma: no cover - import for type checkers only
|
||
from ..session import ExecutionSession
|
||
|
||
|
||
@atomic_executor("hole_blind", "hole_countersink", "hole_counterbore")
|
||
def _hole_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult:
|
||
del sketch
|
||
return _execute_hole(node, session, wizard=False)
|
||
|
||
|
||
@atomic_executor("hole_wizard")
|
||
def _hole_wizard_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult:
|
||
del sketch
|
||
return _execute_hole(node, session, wizard=True)
|
||
|
||
|
||
def _execute_hole(node: FeaturePlanNode, session: "ExecutionSession", *, wizard: bool = False) -> FeatureResult:
|
||
# 孔特征(hole)执行入口:在指定宿主面上按孔规格生成切除工具,并从主体上减去。
|
||
|
||
# 1. 校验:孔是切除操作,必须先有主体。
|
||
if session.body is None:
|
||
raise ValueError("hole feature has no body")
|
||
scope_feature_id = node.params.get("scope_feature_id")
|
||
if scope_feature_id is not None:
|
||
if not isinstance(scope_feature_id, str) or not scope_feature_id:
|
||
raise ValueError("hole scope_feature_id is invalid")
|
||
if len(session.body_members) != 1:
|
||
raise ValueError("hole scope body is no longer the sole active member")
|
||
scoped_body = session.body_members.get(scope_feature_id)
|
||
if scoped_body is None:
|
||
raise ValueError("hole scope body is no longer an independently selectable member")
|
||
scoped_solids = session.adapter.body_solids(scoped_body)
|
||
active_solids = session.adapter.body_solids(session.body)
|
||
if (
|
||
len(scoped_solids) != 1
|
||
or len(active_solids) != 1
|
||
or not scoped_solids[0].is_same(active_solids[0])
|
||
):
|
||
raise ValueError("hole scope body does not match the active body")
|
||
# 2. 确定宿主面 host_face:
|
||
host_selector = node.params.get("host_face")
|
||
if isinstance(host_selector, dict) and isinstance(host_selector.get("frame"), dict):
|
||
# 若直接带 frame(平面定义),则以该平面为宿主,孔位按局部坐标解释。
|
||
host = PlaneSpec.from_mapping(host_selector["frame"])
|
||
positions_are_local = True
|
||
else:
|
||
# 否则从特征选择器中取 face,解析出宿主平面,孔位按世界坐标解释。
|
||
selectors = list(node.selectors)
|
||
if isinstance(host_selector, dict):
|
||
selectors.append(host_selector)
|
||
selector = next((item for item in selectors if item.get("kind") == "face"), None)
|
||
if selector is None:
|
||
raise ValueError("hole requires host_face selector or frame")
|
||
host = _host_plane(session.resolve(selector))
|
||
positions_are_local = False
|
||
# 3. 解析孔规格 HoleSpec(直径、深度、类型等,wizard 模式提供额外默认值)。
|
||
spec = HoleSpec.from_feature(node.atomic_id, node.params, wizard=wizard)
|
||
# 4. A host-face normal is an outward B-rep orientation, so its inverse
|
||
# always enters the material. Inferring direction from the global body
|
||
# centre fails for concave or multi-leg parts: for example, the top face
|
||
# of an L bracket can sit below the whole body's centre and the old rule
|
||
# drilled outward, producing a no-op feature reported as successful.
|
||
# The selected topology face is the local, authoritative orientation.
|
||
inward = vector_scale(host.normal, -1)
|
||
# 5. 生成孔切除工具:按孔规格、起始位置、内方向及“贯穿到主体底面”的深度构造工具实体。
|
||
tool = session.adapter.hole_tool(
|
||
spec,
|
||
_hole_starts(spec, host_plane=host, positions_are_local=positions_are_local),
|
||
inward,
|
||
session.adapter.body_span(session.body, inward) + 2.0,
|
||
)
|
||
# 6. 从主体上减去工具实体,登记新主体并返回结果。
|
||
# thread 是装饰螺纹(无螺距、不进实体几何,SolidWorks/STEP 的螺纹孔
|
||
# 即光滑孔):孔按光滑圆柱孔执行,同时记录 info 级诊断便于批量报告
|
||
# 追溯降级数量(issue #9,capabilities 已不再拒绝 thread)。
|
||
diagnostics: list[RuntimeDiagnostic] = []
|
||
if wizard and node.params.get("thread"):
|
||
diagnostics.append(RuntimeDiagnostic(
|
||
code="thread_decoration_ignored",
|
||
message="Thread decoration is not modeled; the hole falls back to a plain cylindrical bore",
|
||
feature_id=node.feature_id,
|
||
))
|
||
result_body = session.adapter.cut(session.body, tool)
|
||
members = {scope_feature_id: result_body} if scope_feature_id is not None else None
|
||
session.register_body(
|
||
node.feature_id, result_body, replay_node=node, body_members=members,
|
||
)
|
||
return session.result(node, diagnostics=diagnostics)
|