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

182 lines
7.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.
"""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)