Files
cdsl-cad/backend/engine/cdsl_engine/runtime.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

185 lines
8.0 KiB
Python

"""Session-based CDSL execution entry points.
The runtime was split into focused modules (behavior-preserving move):
- ``registry``: atomic executor registry, ``atomic_executor`` decorator, and
the ``execute_node`` dispatcher.
- ``executors/``: one module per executor family; importing the package
performs the registration and verifies registry completeness.
- ``session``: ``ExecutionSession`` and the ``GeometryAdapter`` protocol.
- ``runtime_base``: shared error types and the ``ExtentVector`` value.
- ``extents``: end-condition planning.
- ``pattern_transform``: translate/mirror/rotate parameter algebra for replay.
This module keeps the ``analyze_cdsl`` / ``rebuild_cdsl`` entry points and
re-exports the historical ``cdsl_engine.runtime`` names, including the
private helpers referenced by the test suite, so every existing import keeps
working.
"""
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
from typing import Any
from . import executors # noqa: F401 (importing performs executor registration)
from .capabilities import CapabilityAnalyzer, pattern_transform_blocker, sketch_ids_required_by_contract
# Historical private name still imported by the test suite.
from .executors.primitives import _execute_box # noqa: F401
from .extents import (
_extent_reference,
_extent_vectors,
_extent_vectors_from_normal,
_normal_from_sketch,
_side_extent_vectors,
_targeted_extent_vector,
)
from .pattern_transform import (
_box_circular_is_exact,
_coordinate_axis_direction,
_mirrored_node,
_mirrored_sketch,
_normal_is_coordinate_axis,
_owner_plane_frame,
_pattern_operation_node,
_reflect_point,
_rotated_node,
_rotated_point,
_rotated_sketch,
_rotated_vector,
_transformed_loft_profiles,
_translated_node,
_translated_sketch,
)
from .registry import (
ALL_ATOMIC_IDS,
EXECUTORS,
AtomicExecutor,
ExecutorFunction,
atomic_executor,
execute_node,
)
from .runtime_base import ExtentVector, FeatureExecutionError, RuntimeExecutionError
from .session import ExecutionSession, GeometryAdapter
from .sketch_solver import CORE_SHAPE_GENERATORS, resolve_required_sketches
from .specs import (
AxisSpec, BendSpec, GearSpec, HoleSpec, PlaneSpec, RackSpec, ThreadSpec, Vector3,
pattern_instance_member_id, transform_copy_member_id,
vector_add, vector_cross, vector_dot, vector_scale, vector_subtract, vector_unit,
)
from .topology import (
CapabilityResult, FeaturePlanNode, FeatureResult, RuntimeDiagnostic,
SelectorResolution, TopologyDelta, TopologyDeltaRelation, TopologyRecord, TopologyRegistry,
)
__all__ = [
"ALL_ATOMIC_IDS",
"EXECUTORS",
"ExecutionSession",
"ExecutorFunction",
"ExtentVector",
"FeatureExecutionError",
"FeaturePlanNode",
"FeatureResult",
"GeometryAdapter",
"RuntimeDiagnostic",
"RuntimeExecutionError",
"analyze_cdsl",
"execute_node",
"rebuild_cdsl",
]
def analyze_cdsl(cdsl: dict[str, Any]):
"""Resolve profiles and return the current runtime capability analysis."""
sketch_errors: dict[str, str] = {}
resolved = resolve_required_sketches(
deepcopy(cdsl), sketch_ids_required_by_contract(cdsl), errors=sketch_errors,
)
analyzer = CapabilityAnalyzer(atomic_ids=EXECUTORS, profile_types=CORE_SHAPE_GENERATORS)
return analyzer.analyze(resolved, sketch_errors=sketch_errors)
def rebuild_cdsl(cdsl: dict[str, Any], out_step: Path, *, strict: bool = True) -> dict[str, Any]:
"""Rebuild CDSL through session-scoped atomic executors only."""
sketch_errors: dict[str, str] = {}
resolved = resolve_required_sketches(
deepcopy(cdsl), sketch_ids_required_by_contract(cdsl), errors=sketch_errors,
)
analysis = CapabilityAnalyzer(atomic_ids=EXECUTORS, profile_types=CORE_SHAPE_GENERATORS).analyze(
resolved, sketch_errors=sketch_errors,
)
if strict and not analysis.runtime_eligible:
first = next((result for result in analysis.feature_results if not result.executable), None)
if first is None:
raise ValueError(analysis.document_blockers[0].code)
if any(blocker.code == "unknown_atomic" for blocker in first.blockers):
raise ValueError(f"unsupported atomic_id: {first.atomic_id}")
detail = "; ".join(blocker.code for blocker in first.blockers)
raise ValueError(f"Feature {first.feature_id} is not runtime eligible: {detail}")
session = ExecutionSession(
sketches={str(sketch.get("id")): sketch for sketch in (resolved.get("geometry") or {}).get("sketches") or []},
nodes={node.feature_id: node for node in analysis.plan},
)
diagnostics: list[RuntimeDiagnostic] = []
for node, preflight in zip(analysis.plan, analysis.feature_results):
if not preflight.executable:
diagnostics.extend(preflight.blockers)
if strict:
break
continue
try:
execute_node(node, session)
except Exception as error:
failed_resolution = next(
(item for item in reversed(session.selector_resolutions) if item["status"] != "resolved"), None,
)
diagnostic = (
RuntimeDiagnostic(error.code, str(error), feature_id=node.feature_id, detail=error.detail)
if isinstance(error, FeatureExecutionError)
else
RuntimeDiagnostic(
failed_resolution["diagnostic"]["code"], failed_resolution["diagnostic"]["message"],
feature_id=node.feature_id, detail=failed_resolution["diagnostic"].get("detail") or {},
)
if failed_resolution and failed_resolution.get("diagnostic")
else RuntimeDiagnostic("execution_failed", str(error), feature_id=node.feature_id)
)
diagnostics.append(diagnostic)
if strict:
raise RuntimeExecutionError(diagnostic, list(session.selector_resolutions)) from error
output = session.body
surface_geometry: dict[str, Any] | None = None
if output is None:
if not session.surface_members:
raise ValueError("CDSL execution produced no body")
# 纯曲面文档没有 active solid,但依然是可执行的 CAD 结果。只有在
# 没有实体时才将 surface members 作为 STEP 输出,混合模型继续只导出
# 实体,避免曲面意外改变既有实体比较和下游消费语义。
output = session.adapter.combine_surfaces(*session.surface_members.values())
surface_geometry = session.adapter.surface_geometry(output)
out_step.parent.mkdir(parents=True, exist_ok=True)
session.adapter.export(output, str(out_step))
geometry = session.adapter.body_geometry(session.body) if session.body is not None else surface_geometry
if geometry is None:
raise ValueError("CDSL execution produced no exportable geometry")
bbox = geometry["bbox_mm"]
return {
"engine": "cdsl_session_runtime",
"out_step": str(out_step),
"volume_mm3": float(geometry.get("volume_mm3") or 0.0),
"bbox_mm": {"min": bbox[:3], "max": bbox[3:]},
# #7 multi-body:重建结果里的独立实体数(Compound 成员数),
# 与 batch 验证的 document_truth.geometry.solid_body_count 对齐。
"solid_count": len(session.adapter.body_solids(session.body)) if session.body is not None else 0,
"surface_count": len(session.surface_members),
"surface_face_count": int(surface_geometry["face_count"]) if surface_geometry is not None else 0,
"surface_area_mm2": float(surface_geometry["area_mm2"]) if surface_geometry is not None else 0.0,
"feature_results": [result.as_dict() for result in session.results.values()],
"runtime_diagnostics": [diagnostic.as_dict() for diagnostic in diagnostics],
"topology_records": [record.public_dict() for record in session.topology.records()],
"topology_deltas": list(session.topology.topology_deltas()),
"selector_resolution": session.selector_resolutions,
}