5ffb106f36
Phase 3 of the decoupling refactor (behavior-preserving): - registry.py: atomic_executor decorator, ALL_ATOMIC_IDS with fail-fast registration validation, execute_node dispatcher - executors/: one module per family (extrude, revolve, surfaces, loft_sweep, bodies, context, primitives, parametric, holes, dressup, patterns) + shared helpers in executors/common - executors/__init__: explicit aggregation + completeness check (registry must cover every declared atomic id at import time) - runtime.py: slimmed to entry points (analyze_cdsl/rebuild_cdsl) plus full historical re-exports incl. test-referenced privates Adding an atomic operation now touches only one executor module and its schema contract; the shared registry never changes. Verified against baseline: zero new failures.
121 lines
5.6 KiB
Python
121 lines
5.6 KiB
Python
"""Dress-up executors (fillet / chamfer / shell).
|
||
|
||
These mutate an existing body through edge/face selectors resolved from the
|
||
current B-rep snapshot.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from typing import TYPE_CHECKING, Any
|
||
|
||
from ..registry import atomic_executor
|
||
from ..topology import FeaturePlanNode, FeatureResult, RuntimeDiagnostic, TopologyDelta
|
||
from .common import _replace_shell_target, _selector_edges, _shell_target
|
||
|
||
if TYPE_CHECKING: # pragma: no cover - import for type checkers only
|
||
from ..session import ExecutionSession
|
||
|
||
|
||
def _execute_fillet(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult:
|
||
# 圆角特征(fillet)执行入口:对选中边按半径做圆角,平滑尖角与棱边。
|
||
|
||
# 1. 校验:圆角作用于已有主体,必须先有主体。
|
||
if session.body is None:
|
||
raise ValueError("fillet has no body")
|
||
# 2. 解析圆角半径并校验必须大于 0。
|
||
radius = float(node.params.get("radius_mm") or 0)
|
||
if radius <= 0:
|
||
raise ValueError("fillet radius_mm must be > 0")
|
||
# 3. 解析目标边(支持 tangent_propagation 相切传播),并执行圆角。
|
||
body, topology_delta = session.adapter.fillet_with_topology_delta(
|
||
session.body, radius, _selector_edges(node, session, tangent_propagation=bool(node.params.get("tangent_propagation"))),
|
||
)
|
||
# 4. 登记新主体并返回结果。
|
||
session.register_body(node.feature_id, body, replay_node=node, topology_delta=topology_delta)
|
||
return session.result(node)
|
||
|
||
|
||
@atomic_executor("fillet")
|
||
def _fillet_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult:
|
||
del sketch
|
||
return _execute_fillet(node, session)
|
||
|
||
|
||
def _execute_chamfer(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult:
|
||
# 倒角特征(chamfer)执行入口:对选中边按距离做倒角(可带第二距离形成不对称倒角)。
|
||
|
||
# 1. 校验:倒角作用于已有主体,必须先有主体。
|
||
if session.body is None:
|
||
raise ValueError("chamfer has no body")
|
||
# 2. 解析主距离并校验必须大于 0。
|
||
distance = float(node.params.get("distance_mm") or 0)
|
||
if distance <= 0:
|
||
raise ValueError("chamfer distance_mm must be > 0")
|
||
# 3. 解析第二距离与角度(importer 对 SolidWorks Distance-Angle 倒角产出
|
||
# angle_rad,单位为弧度)。第二距离 = 主距离 * tan(angle);angle=45° 时
|
||
# tan=1,退化为等距倒角(与历史行为一致,零回归)。
|
||
# 注意:build123d 的 length/length2 侧向分配依赖面的枚举顺序,对非 45°
|
||
# 倒角仅保证量级正确,距离所在侧可能反转。
|
||
distance_2 = node.params.get("distance_2_mm")
|
||
angle_rad = node.params.get("angle_rad")
|
||
if distance_2 is None and angle_rad is not None:
|
||
distance_2 = distance * math.tan(float(angle_rad))
|
||
# 4. 解析目标边(支持相切传播),执行倒角。
|
||
edges = _selector_edges(node, session, tangent_propagation=bool(node.params.get("tangent_propagation")))
|
||
diagnostics: list[RuntimeDiagnostic] = []
|
||
topology_delta: TopologyDelta | None = None
|
||
try:
|
||
body, topology_delta = session.adapter.chamfer_with_topology_delta(session.body, distance, distance_2, edges)
|
||
except ValueError as error:
|
||
# 显式 surfaceEntities 可以在后续实体上留下曲面分区边界。若标准
|
||
# OCC 倒角因环域宽度不足而拒绝,只允许在该 shell 给出同轴边界证据
|
||
# 时按原始距离构造受限倒角;没有证明时仍保留原始内核失败。
|
||
if distance_2 is not None or not session.surface_members:
|
||
raise
|
||
try:
|
||
body = session.adapter.surface_limited_chamfer(
|
||
session.body, distance, edges, list(session.surface_members.values()),
|
||
)
|
||
except ValueError:
|
||
raise error
|
||
diagnostics.append(RuntimeDiagnostic(
|
||
"chamfer_surface_limited",
|
||
"Chamfer was limited by an explicit coaxial surface boundary",
|
||
feature_id=node.feature_id,
|
||
detail={"distance_mm": distance, "surface_count": len(session.surface_members)},
|
||
))
|
||
# 5. 登记新主体并返回结果。
|
||
session.register_body(node.feature_id, body, replay_node=node, topology_delta=topology_delta)
|
||
return session.result(node, diagnostics=diagnostics)
|
||
|
||
|
||
@atomic_executor("chamfer")
|
||
def _chamfer_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult:
|
||
del sketch
|
||
return _execute_chamfer(node, session)
|
||
|
||
|
||
def _execute_shell(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult:
|
||
# 抽壳特征:移除 selector 所指面,并按 CADFS thickness 向实体内部偏置。
|
||
if session.body is None:
|
||
raise ValueError("shell has no body")
|
||
thickness = float(node.params.get("thickness_mm") or 0)
|
||
if thickness <= 0:
|
||
raise ValueError("shell thickness_mm must be > 0")
|
||
target, faces = _shell_target(node, session)
|
||
result, topology_delta = session.adapter.shell_with_topology_delta(
|
||
target, faces, thickness, inward=bool(node.params.get("inward", True)),
|
||
)
|
||
session.register_body(
|
||
node.feature_id, _replace_shell_target(session, target, result), replay_node=node,
|
||
topology_delta=topology_delta,
|
||
)
|
||
return session.result(node)
|
||
|
||
|
||
@atomic_executor("shell")
|
||
def _shell_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult:
|
||
del sketch
|
||
return _execute_shell(node, session)
|