182 lines
7.8 KiB
Python
182 lines
7.8 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 _single_member_dressup_members(
|
||
node: FeaturePlanNode,
|
||
session: "ExecutionSession",
|
||
body: Any,
|
||
selected_edges: list[Any],
|
||
) -> dict[str, Any] | None:
|
||
"""Preserve unchanged body members after one exact-member dress-up.
|
||
|
||
The adapter only reports Compound history when every selected edge maps to
|
||
one source solid. Keep the same proof at the body-graph layer: all other
|
||
members must appear unchanged in the result and exactly one result solid
|
||
must remain for the changed member. Otherwise aggregate replay remains
|
||
valid, but no member-lifecycle transfer is asserted.
|
||
"""
|
||
if not selected_edges or len(session.body_members) < 2:
|
||
return None
|
||
source_members: dict[str, Any] = {}
|
||
for member_id, member in session.body_members.items():
|
||
solids = session.adapter.body_solids(member)
|
||
if len(solids) != 1:
|
||
return None
|
||
source_members[member_id] = solids[0]
|
||
selected_members = {
|
||
member_id
|
||
for edge in selected_edges
|
||
for member_id, member in source_members.items()
|
||
if any(edge.is_same(candidate) for candidate in member.edges())
|
||
}
|
||
if len(selected_members) != 1:
|
||
return None
|
||
changed_member_id = next(iter(selected_members))
|
||
result_solids = session.adapter.body_solids(body)
|
||
unchanged: dict[str, Any] = {}
|
||
matched_result_indexes: set[int] = set()
|
||
for member_id, member in source_members.items():
|
||
if member_id == changed_member_id:
|
||
continue
|
||
matches = [
|
||
index for index, result in enumerate(result_solids)
|
||
if session.topology._same_topology_value(member, result)
|
||
]
|
||
if len(matches) != 1 or matches[0] in matched_result_indexes:
|
||
return None
|
||
matched_result_indexes.add(matches[0])
|
||
unchanged[member_id] = result_solids[matches[0]]
|
||
changed = [
|
||
result for index, result in enumerate(result_solids)
|
||
if index not in matched_result_indexes
|
||
]
|
||
if len(changed) != 1:
|
||
return None
|
||
return {**unchanged, node.feature_id: changed[0]}
|
||
|
||
|
||
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 相切传播),并执行圆角。
|
||
edges = _selector_edges(node, session, tangent_propagation=bool(node.params.get("tangent_propagation")))
|
||
body, topology_delta = session.adapter.fillet_with_topology_delta(
|
||
session.body, radius, edges,
|
||
)
|
||
# 4. 登记新主体并返回结果。
|
||
session.register_body(
|
||
node.feature_id, body, replay_node=node, topology_delta=topology_delta,
|
||
body_members=_single_member_dressup_members(node, session, body, edges),
|
||
)
|
||
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,
|
||
body_members=_single_member_dressup_members(node, session, body, edges),
|
||
)
|
||
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)
|