"""Body-graph executors (boolean_bodies / transform_bodies / delete_bodies). These operate on explicitly named body members instead of the aggregate session body, so adjacent independent solids never accidentally become tools or targets of one another. """ from __future__ import annotations from typing import TYPE_CHECKING, Any from ..registry import atomic_executor from ..specs import transform_copy_member_id from ..topology import FeaturePlanNode, FeatureResult, TopologyDelta from .common import _combine_members, _member_sources if TYPE_CHECKING: # pragma: no cover - import for type checkers only from ..session import ExecutionSession def _execute_boolean_bodies(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult: # booleanBodies 总是作用于 source feature 的明确 body 输出,不能回退为 # 当前聚合 body。这样相邻独立实体不会意外成为工具或目标。 params = node.params target_ids = _member_sources( node, session, "target_feature_ids", pattern_instance_parameter="target_pattern_instance_refs", allow_transform_copies=True, transform_copy_parameter="target_transform_copy_refs", ) tool_ids = _member_sources( node, session, "tool_feature_ids", pattern_instance_parameter="tool_pattern_instance_refs", allow_transform_copies=True, transform_copy_parameter="tool_transform_copy_refs", ) targets = {feature_id: session.body_members[feature_id] for feature_id in target_ids} tools = {feature_id: session.body_members[feature_id] for feature_id in tool_ids} target = _combine_members(session, targets) tool = _combine_members(session, tools) operation = str(params.get("operation") or "") topology_delta: TopologyDelta | None = None if operation == "union": result, topology_delta = session.adapter.fuse_with_topology_delta(target, tool) elif operation == "subtract": result, topology_delta = session.adapter.cut_with_topology_delta(target, tool) elif operation == "intersect": result, topology_delta = session.adapter.intersect_with_topology_delta(target, tool) else: raise ValueError(f"unsupported booleanBodies operation {operation!r}") members = { feature_id: body for feature_id, body in session.body_members.items() if feature_id not in set(target_ids + tool_ids) } members[node.feature_id] = result if bool(params.get("keep_tools")): # In a targetless FeatureScript body-set operation every selected # member is semantically a tool. Lowering chooses the first source # member only to satisfy CDSL's binary executor shape, so retain that # left operand too when the explicit targetless contract requests it. if bool(params.get("targetless_body_set")): members.update(targets) members.update(tools) session.register_body( node.feature_id, _combine_members(session, members), body_members=members, topology_delta=topology_delta, ) return session.result(node) @atomic_executor("boolean_bodies") def _boolean_bodies_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch return _execute_boolean_bodies(node, session) def _execute_transform_bodies(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult: # FeatureScript transform targets explicit bodies. Do not move the # aggregate session body, because it may include unrelated members. source_ids = _member_sources( node, session, "source_feature_ids", pattern_instance_parameter="pattern_instance_refs", allow_transform_copies=True, ) make_copy = bool(node.params.get("make_copy")) direct_sources = node.params.get("source_feature_ids") or [] if make_copy and isinstance(direct_sources, list) and len(direct_sources) > 1: # The aggregate is only an export compound. Each source transform has # its own B-rep builder and is the only output a later COPY query may # select. Do not attach an aggregate topology delta to source members. members = dict(session.body_members) members.update({ transform_copy_member_id(node.feature_id, source_id): session.adapter.transform( session.body_members[source_id], dict(node.params.get("transform") or {}), ) for source_id in source_ids }) session.register_body( node.feature_id, _combine_members(session, members), body_members=members, ) return session.result(node) source = _combine_members(session, {feature_id: session.body_members[feature_id] for feature_id in source_ids}) transformed, topology_delta = session.adapter.transform_with_topology_delta( source, dict(node.params.get("transform") or {}), ) members = dict(session.body_members) if not make_copy: for feature_id in source_ids: members.pop(feature_id) members[node.feature_id] = transformed session.register_body( node.feature_id, _combine_members(session, members), body_members=members, topology_delta=topology_delta, ) return session.result(node) @atomic_executor("transform_bodies") def _transform_bodies_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch return _execute_transform_bodies(node, session) def _execute_delete_bodies(node: FeaturePlanNode, session: "ExecutionSession") -> FeatureResult: # Deletion is a body-graph operation, never a Boolean subtraction. A # selected member can be disjoint or overlap another independent body. source_ids = _member_sources(node, session, "target_feature_ids") members = {feature_id: body for feature_id, body in session.body_members.items() if feature_id not in set(source_ids)} if members: session.register_body(node.feature_id, _combine_members(session, members), body_members=members) else: session.clear_body() return session.result(node) @atomic_executor("delete_bodies") def _delete_bodies_executor(node: FeaturePlanNode, session: "ExecutionSession", sketch: dict[str, Any] | None) -> FeatureResult: del sketch return _execute_delete_bodies(node, session)