Files
cdsl-cad/backend/engine/cdsl_engine/executors/bodies.py
T
ganjihong 5ffb106f36 refactor(cdsl_engine): executor registry + per-family executor package
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.
2026-09-09 13:33:06 +08:00

125 lines
5.7 KiB
Python

"""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",
)
tool_ids = _member_sources(
node, session, "tool_feature_ids", pattern_instance_parameter="tool_pattern_instance_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")):
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)