Files
cdsl-cad/backend/engine/cdsl_engine/executors/holes.py
T

114 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")
scoped_body = None
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")
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)
if not scoped_solids:
raise ValueError("hole scope body has no active solid")
# 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), session.adapter)
intent = selector.get("selector_intent") if isinstance(selector, dict) else None
# Existing selector-hosted holes store world positions. Only the new
# runtime-attached COPY(CAP_FACE) sketch preserves local coordinates
# until its exact host relation has materialized.
positions_are_local = (
isinstance(intent, dict)
and intent.get("copy_contract") == "primary_cut_cap_face_workplane"
)
# 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(scoped_body or session.body, inward) + 2.0,
)
# 6. 从主体上减去工具实体,登记新主体并返回结果。
# thread 是装饰螺纹(无螺距、不进实体几何,SolidWorks/STEP 的螺纹孔
# 即光滑孔):孔按光滑圆柱孔执行,同时记录 info 级诊断便于批量报告
# 追溯降级数量(issue #9capabilities 已不再拒绝 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,
))
if scope_feature_id is None:
result_body = session.adapter.cut(session.body, tool)
members = None
else:
# CADFS ``scope`` identifies the target body member. Never apply the
# cutter to the aggregate merely because unrelated live members share
# the exported part; unchanged members remain exact body-graph nodes.
result_body = session.adapter.cut(scoped_body, tool)
members = {**session.body_members, scope_feature_id: result_body}
body = None
for member in members.values():
body = session.adapter.combine(body, member)
if body is None:
raise ValueError("hole scope cut produced no active body")
result_body = body
session.register_body(
node.feature_id, result_body, replay_node=node, body_members=members,
)
return session.result(node, diagnostics=diagnostics)