Files
cdsl-cad/backend/app/cad_agent/adapters/runtime.py
T

1137 lines
68 KiB
Python

"""CDSL runtime adapter driven exclusively by profile operation contracts."""
from __future__ import annotations
from copy import deepcopy
from hashlib import sha256
import json
import math
from pathlib import Path
from typing import Any
from app.cad_agent.domain.operation_contract import (
SEMANTIC_PREFLIGHT_NAMES,
OperationContractError,
canonical_hash,
validate_fragment,
validate_operation_contract,
)
from app.cad_agent.domain.verifier_registry import default_registry
from app.services.engine_service import load_engine, topology_snapshot, validate_cdsl
from app.services.review_renderer import ReviewRenderError, render_checkpoint
from app.settings import Settings
from vendor.cdsl_preview_runtime import step_to_glb
class RuntimeAdapterError(RuntimeError):
pass
class ProfileCadRuntime:
"""Adapter that owns engine imports; application code sees only its port."""
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.engine = load_engine(settings)
self._semantic_preflight_handlers = {
"sketch_workplane": self._preflight_sketch_workplane,
"profile_non_self_intersecting": self._preflight_profile_non_self_intersecting,
"host_face_exists": self._preflight_host_face_exists,
"hole_positions_on_host_plane": self._preflight_hole_positions_on_host_plane,
"cut_exit_distance": self._preflight_cut_exit_distance,
"requires_active_solid": self._preflight_requires_active_solid,
"revolve_axis_on_sketch": self._preflight_revolve_axis_on_sketch,
"reference_plane_nonzero_normal": self._preflight_reference_plane_nonzero_normal,
"reference_axis_nonzero_direction": self._preflight_reference_axis_nonzero_direction,
"selected_edges_exist": self._preflight_selected_edges_exist,
"source_features_exist": self._preflight_source_features_exist,
"mirror_plane_exists": self._preflight_mirror_plane_exists,
"loft_profiles_exist": self._preflight_loft_profiles_exist,
"loft_profiles_closed": self._preflight_loft_profiles_closed,
"loft_profiles_single_region": self._preflight_loft_profiles_single_region,
}
schema_path = Path(str(self.engine.__file__)).with_name("profile_schema.json")
try:
self._profile = json.loads(schema_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: profile schema is unavailable") from error
self._contracts = self._profile.get("operation_contracts")
if not isinstance(self._contracts, dict):
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: profile has no v3 operation contracts")
declared = {str(item) for item in self._contracts}
registered = {str(item) for item in getattr(self.engine, "SUPPORTED_ATOMIC_IDS", ())}
if declared != registered:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: runtime and profile operation registries disagree")
verifier_kinds = set(default_registry().claim_kinds)
for contract in self._contracts.values():
try:
validate_operation_contract(contract)
except OperationContractError as error:
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: {error}") from error
unknown_preflights = set(contract["semantic_preflight"]) - set(self._semantic_preflight_handlers)
unknown_verifiers = set(contract["candidate_verifiers"]) - verifier_kinds
if unknown_preflights or unknown_verifiers:
raise RuntimeAdapterError(
"RUNTIME_CONTRACT_INVALID: operation contract references an unavailable "
f"{'preflight' if unknown_preflights else 'candidate verifier'}"
)
if set(self._semantic_preflight_handlers) != SEMANTIC_PREFLIGHT_NAMES:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: runtime preflight registry is incomplete")
def supported_atomic_ids(self) -> tuple[str, ...]:
return tuple(sorted(self._contracts))
def operation_contract(self, atomic_id: str) -> dict[str, Any]:
contract = self._contracts.get(atomic_id)
if not isinstance(contract, dict):
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: unknown operation {atomic_id}")
result = deepcopy(contract)
result["contract_hash"] = canonical_hash(contract)
result["registry_revision"] = str(self._profile.get("schema_version") or "")
return result
def selector_tokens(self, topology: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
if not isinstance(topology, dict):
return {}
snapshot_id = str(topology.get("snapshot_id") or "")
if not snapshot_id:
return {}
result: dict[str, dict[str, Any]] = {}
for record in topology.get("records") or ():
if not isinstance(record, dict) or not record.get("executable"):
continue
record_id = str(record.get("record_id") or "")
kind = str(record.get("kind") or "")
if not record_id or kind not in {"face", "edge", "plane", "axis", "body", "vertex"}:
continue
token = "sel_" + sha256(f"{snapshot_id}|{record_id}".encode("utf-8")).hexdigest()[:16]
geometry = deepcopy(record.get("geometry") or {})
owners = record.get("owner_feature_ids") or [record.get("feature_id") or ""]
result[token] = {
"token": token,
"kind": kind,
"snapshot_id": snapshot_id,
"selector": {"kind": kind, "stable_id": record_id, "owner_feature_id": str(owners[0] or ""), "geometry": geometry, "source": "runtime_snapshot", "snapshot_id": snapshot_id, "confidence": 1.0},
"geometry": geometry,
}
return result
def reference_tokens(self, cdsl: dict[str, Any] | None) -> dict[str, str]:
"""Return opaque, head-scoped feature references for pattern contracts."""
result: dict[str, str] = {}
document_hash = canonical_hash(cdsl) if isinstance(cdsl, dict) else "root"
for feature in (cdsl or {}).get("features") or ():
if not isinstance(feature, dict) or not isinstance(feature.get("id"), str) or not feature["id"]:
continue
feature_id = feature["id"]
result["ref_" + sha256(f"{document_hash}|{feature_id}".encode("utf-8")).hexdigest()[:16]] = feature_id
return result
def materialize_fragment(self, base_cdsl: dict[str, Any] | None, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], reference_tokens: dict[str, str], *, require_through: bool = False, depends_on_feature_ids: tuple[str, ...] | list[str] = ()) -> tuple[dict[str, Any], dict[str, Any]]:
# ActionCommandHandler validates the exposed schema first, but this
# adapter is also used during crash recovery. Keep the runtime boundary
# self-contained so a corrupted/replayed staged payload cannot produce
# a candidate merely by bypassing that handler-level validation.
try:
validate_operation_contract(contract)
except OperationContractError as error:
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: {error}") from error
current = self.operation_contract(str(contract.get("atomic_id") or ""))
if (
contract.get("contract_hash") != current["contract_hash"]
or contract.get("registry_revision") != current["registry_revision"]
):
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: operation contract is not the current verified registry entry")
selector_shape = str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden")
selector_kind = str((contract.get("selector_policy") or {}).get("token_kind") or "")
allowed_selectors = [
token
for token, value in selector_tokens.items()
if selector_shape == "required"
and isinstance(value, dict)
and value.get("kind") == selector_kind
]
errors = validate_fragment(
contract,
fragment,
selector_tokens=allowed_selectors,
reference_tokens=list(reference_tokens),
root_xy_datum=not bool((base_cdsl or {}).get("features")),
)
if errors:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: fragment no longer matches the active operation schema: "
+ errors[0]["message"]
)
materialized = deepcopy(fragment)
reference = contract["reference_policy"]
if reference["mode"] == "snapshot_bound":
slot = str(reference["slot"]).removeprefix("params.")
supplied = materialized.get("feature", {}).get("params", {}).get(slot, [])
if not isinstance(supplied, list) or not all(token in reference_tokens for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference token is absent or stale")
materialized["feature"]["params"][slot] = [reference_tokens[token] for token in supplied]
through_normalizations = self._normalize_required_through_cut_depth(
materialized,
contract,
selector_tokens,
require_through=require_through,
)
cut_support_normal = self._semantic_preflight(
materialized,
contract,
selector_tokens,
base_cdsl,
require_through=require_through,
)
direction_normalizations = self._normalize_extrude_cut_direction(
materialized,
contract,
cut_support_normal,
)
document = deepcopy(base_cdsl) if isinstance(base_cdsl, dict) else {
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "agent_preflight", "geometry": {"sketches": []}, "features": [],
}
geometry = document.setdefault("geometry", {})
sketches = geometry.setdefault("sketches", []) if isinstance(geometry, dict) else None
features = document.setdefault("features", [])
if not isinstance(sketches, list) or not isinstance(features, list):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: base CDSL collections are invalid")
existing_feature_ids = {
str(item.get("id") or "")
for item in features
if isinstance(item, dict) and str(item.get("id") or "")
}
direct_dependencies = tuple(str(value) for value in depends_on_feature_ids)
if len(direct_dependencies) != len(set(direct_dependencies)) or any(value not in existing_feature_ids for value in direct_dependencies):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: plan dependency feature is absent from the active checkpoint")
index = len(features) + 1
feature = materialized["feature"]
output = {"id": f"feature_{index:03d}", "atomic_id": contract["atomic_id"], "params": deepcopy(feature["params"]), "depends_on": list(direct_dependencies)}
if contract["fragment_shape"]["sketch"] == "required":
sketch_id = f"sketch_{len(sketches) + 1:03d}"
sketch = materialized["sketch"]
sketches.append({"id": sketch_id, "workplane": deepcopy(sketch["workplane"]), "profile": deepcopy(sketch["profile"])})
output["sketch_id"] = sketch_id
selected = [selector_tokens[token]["selector"] for token in feature.get("selector_tokens", [])]
slot = contract["selector_policy"]["slot"]
if slot == "params.host_face":
# A topology selector is authoritative while accepting the
# action, but a later pattern replays this feature after its own
# cut may have split the selected B-rep face. Lower the accepted
# planar selector to a concrete, world-aligned host frame and
# transform the author-supplied world coordinates into that
# frame. The token remains in the fragment audit for provenance;
# the materialized CDSL is stable under replay.
output["params"]["host_face"], output["params"]["positions"] = self._materialized_hole_host_frame(
selector_tokens[str(feature["selector_tokens"][0])],
output["params"].get("positions"),
)
elif slot == "params.mirror_plane":
output["params"]["mirror_plane"] = selected[0]
elif slot == "feature.selectors":
output["selectors"] = selected
features.append(output)
try:
self._validate_finite_tree(document)
self._validate_materialized_runtime_types(output)
validate_cdsl(document, self.engine)
except Exception as error:
message = str(error)
# A JSON-schema failure after server materialization is a registry
# drift: the author could not have supplied the missing server
# field. Engine capability analysis, however, can reject a
# schema-valid author sketch (for example an unresolvable analytic
# contour). That is a recoverable action precondition failure,
# not a deployment defect.
if "CDSL engine runtime preflight failed:" in message:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: materialized fragment is not executable by the engine: "
+ message
) from error
raise RuntimeAdapterError(
"RUNTIME_CONTRACT_INVALID: materialized fragment violates the engine CDSL schema: "
+ message
) from error
return document, {"schema_version": "cad.v3.2.fragment-audit.v1", "atomic_id": contract["atomic_id"], "fragment_hash": canonical_hash(fragment), "contract_hash": contract["contract_hash"], "assigned_feature_ids": [output["id"]], "depends_on_feature_ids": list(direct_dependencies), "assigned_sketch_ids": [output["sketch_id"]] if output.get("sketch_id") else [], "selector_snapshot_id": next(iter(selector_tokens.values()), {}).get("snapshot_id", ""), "selector_tokens": list(feature.get("selector_tokens", [])), "reference_snapshot_id": canonical_hash(reference_tokens), "reference_tokens": list(fragment.get("feature", {}).get("params", {}).get(str(reference.get("slot") or "").removeprefix("params."), [])) if reference["mode"] == "snapshot_bound" else [], "server_normalizations": [*through_normalizations, *direction_normalizations]}
def build_checkpoint(self, cdsl: dict[str, Any], output_dir: str, task_id: str, revision_id: str) -> dict[str, Any]:
"""Build the exact geometry checkpoint required by every DAG node.
Rendering is deliberately absent here: a renderer outage must never
make a geometrically valid atomic feature fail its local acceptance.
"""
root = Path(output_dir)
root.mkdir(parents=True, exist_ok=True)
cdsl_copy = deepcopy(cdsl)
cdsl_copy["part_id"] = task_id
step_path = root / "model.step"
self._write_json(root / "model.cdsl.json", cdsl_copy)
try:
self._validate_finite_tree(cdsl_copy)
validate_cdsl(cdsl_copy, self.engine)
engine_result = self.engine.run_cdsl_only(cdsl_copy, step_path)
if str(engine_result.get("engine") or "") != "cdsl_only":
raise RuntimeAdapterError("engine did not execute CDSL-only rebuild")
health = self._health(engine_result, step_path)
topology = topology_snapshot(engine_result, task_id=task_id, revision_id=revision_id)
self._write_json(root / "model.topology.json", topology)
report = {"engine_result": engine_result, "preview": {}, "health": health, "render_manifest": {}}
self._write_json(root / "rebuild-report.json", report)
return {"health": health, "topology": topology, "report": report, "render_manifest": {}, "paths": {"cdsl": "model.cdsl.json", "step": "model.step", "topology": "model.topology.json", "report": "rebuild-report.json"}}
except OSError:
raise
except RuntimeAdapterError:
raise
except Exception as error:
raise RuntimeAdapterError(f"RUNTIME_EXECUTION_FAILURE: {error}") from error
def create_preview(self, output_dir: str) -> dict[str, Any]:
"""Create an optional GLB preview for a completed checkpoint."""
root = Path(output_dir)
step_path = root / "model.step"
glb_path = root / "model.glb"
preview = step_to_glb(step_path, glb_path)
report_path = root / "rebuild-report.json"
report = json.loads(report_path.read_text(encoding="utf-8")) if report_path.is_file() else {}
report["preview"] = preview
self._write_json(report_path, report)
return {"preview": preview, "path": "model.glb"}
def render_review_bundle(self, output_dir: str) -> dict[str, Any]:
root = Path(output_dir)
manifest = render_checkpoint(self.settings, step_path=root / "model.step", output_dir=root / "renders")
report_path = root / "rebuild-report.json"
report = json.loads(report_path.read_text(encoding="utf-8")) if report_path.is_file() else {}
report["render_manifest"] = manifest
self._write_json(report_path, report)
return manifest
def rebuild(self, cdsl: dict[str, Any], output_dir: str, task_id: str, revision_id: str) -> dict[str, Any]:
# Legacy v3.1 compatibility path. New DAG nodes use build_checkpoint.
built = self.build_checkpoint(cdsl, output_dir, task_id, revision_id)
root = Path(output_dir)
try:
preview = self.create_preview(output_dir)
manifest = self.render_review_bundle(output_dir)
report = json.loads((root / "rebuild-report.json").read_text(encoding="utf-8"))
return {**built, "report": report, "preview": preview["preview"], "render_manifest": manifest, "paths": {**built["paths"], "glb": "model.glb", "render_manifest": "renders/render-manifest.json"}}
except OSError:
# Artifact writes are a recoverable infrastructure outage. Let the
# application handler park the same candidate stage for replay.
raise
except ReviewRenderError as error:
raise RuntimeAdapterError(f"RENDER_SERVICE_UNAVAILABLE: {error}") from error
except Exception as error:
raise RuntimeAdapterError(f"RUNTIME_EXECUTION_FAILURE: {error}") from error
def rebuild_best_effort(
self,
cdsl: dict[str, Any],
output_dir: str,
task_id: str,
revision_id: str,
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
"""Build independent feature prefixes without discarding prior geometry.
A full CDSL document can contain several features even though the
action protocol normally appends one at a time. Engine validation is
all-or-nothing, so replay features in source order and retain each
executable feature. A failed feature is reported as a diagnostic;
it does not erase already executable geometry or prevent later,
independent features from being tried.
"""
features = cdsl.get("features") if isinstance(cdsl.get("features"), list) else []
accepted: list[dict[str, Any]] = []
accepted_ids: set[str] = set()
failures: list[dict[str, Any]] = []
latest: dict[str, Any] | None = None
for index, feature in enumerate(features):
if not isinstance(feature, dict):
failures.append({"feature_index": index, "feature_id": "", "message": "Feature must be an object."})
continue
feature_id = str(feature.get("id") or f"feature_{index + 1:03d}")
dependencies = [str(value) for value in feature.get("depends_on") or () if isinstance(value, str)]
unavailable = [value for value in dependencies if value not in accepted_ids]
if unavailable:
failures.append({
"feature_index": index,
"feature_id": feature_id,
"message": "Feature was skipped because an earlier dependency did not execute.",
"dependencies": unavailable,
})
continue
candidate = self._feature_subset(cdsl, [*accepted, feature])
try:
latest = self.rebuild(candidate, output_dir, task_id, revision_id)
except Exception as error:
failures.append({"feature_index": index, "feature_id": feature_id, "message": str(error)[:1000]})
continue
accepted.append(deepcopy(feature))
accepted_ids.add(feature_id)
if not accepted:
if failures:
raise RuntimeAdapterError(str(failures[0].get("message") or "RUNTIME_EXECUTION_FAILURE: no feature could be rebuilt"))
raise RuntimeAdapterError("RUNTIME_EXECUTION_FAILURE: CDSL document has no executable features")
# A failed later attempt may have left partial files in the stage.
# Rebuild the retained feature set once so all published artifacts are
# guaranteed to describe the same successful checkpoint.
latest = self.rebuild(self._feature_subset(cdsl, accepted), output_dir, task_id, revision_id)
return {**latest, "executed_feature_ids": sorted(accepted_ids)}, failures
@staticmethod
def _feature_subset(cdsl: dict[str, Any], features: list[dict[str, Any]]) -> dict[str, Any]:
document = deepcopy(cdsl)
document["features"] = deepcopy(features)
geometry = document.get("geometry") if isinstance(document.get("geometry"), dict) else {}
sketches = geometry.get("sketches") if isinstance(geometry.get("sketches"), list) else []
sketch_ids = {str(feature.get("sketch_id") or "") for feature in features}
document["geometry"] = {
**geometry,
"sketches": [deepcopy(sketch) for sketch in sketches if isinstance(sketch, dict) and str(sketch.get("id") or "") in sketch_ids],
}
return document
def _semantic_preflight(self, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], base_cdsl: dict[str, Any] | None, *, require_through: bool) -> list[float] | None:
if fragment.get("feature", {}).get("atomic_id") != contract.get("atomic_id"):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: atomic_id does not match active contract")
policy = contract["selector_policy"]
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if contract["fragment_shape"]["selector_tokens"] == "required":
if not all(token in selector_tokens and selector_tokens[token]["kind"] == policy["token_kind"] for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selector token is absent, stale, or has the wrong kind")
for name in contract["semantic_preflight"]:
self._semantic_preflight_handlers[name](fragment, selector_tokens, base_cdsl, require_through)
if contract.get("atomic_id") == "extrude_cut_blind":
return self._preflight_extrude_cut_contacts_material(fragment, selector_tokens)
return None
def _normalize_extrude_cut_direction(
self,
fragment: dict[str, Any],
contract: dict[str, Any],
support_normal: list[float] | None,
) -> list[dict[str, Any]]:
"""Aim a surface-attached cut into the measured material half-space.
A sketch extrusion has no host selector. Once preflight proves that its
profile lies on an oriented boundary face, the only executable blind
cut direction is the material side of that face. This is a coordinate
normalization, not a planning decision, and is recorded in the audit.
"""
if contract.get("atomic_id") != "extrude_cut_blind" or not self._valid_vector3(support_normal, require_nonzero=True):
return []
sketch = fragment.get("sketch") if isinstance(fragment, dict) else None
workplane = sketch.get("workplane") if isinstance(sketch, dict) else None
normal = workplane.get("normal") if isinstance(workplane, dict) else None
params = fragment.get("feature", {}).get("params") if isinstance(fragment.get("feature"), dict) else None
if not self._valid_vector3(normal, require_nonzero=True) or not isinstance(params, dict):
return []
unit_normal = [float(component) / self._norm(normal) for component in normal]
unit_support = [float(component) / self._norm(support_normal) for component in support_normal]
alignment = self._dot(unit_normal, unit_support)
if abs(abs(alignment) - 1.0) > 1e-6:
return []
materialized_reverse = alignment > 0
submitted_reverse = bool(params.get("reverse", False))
params["reverse"] = materialized_reverse
return [{
"path": "feature.params.reverse",
"submitted": submitted_reverse,
"materialized": materialized_reverse,
"reason": "surface-attached cut must travel into the measured material half-space",
"support_normal": unit_support,
}]
def _normalize_required_through_cut_depth(
self,
fragment: dict[str, Any],
contract: dict[str, Any],
selector_tokens: dict[str, dict[str, Any]],
*,
require_through: bool,
) -> list[dict[str, Any]]:
"""Add the minimum deterministic exit allowance for a through cut.
The author owns nominal feature geometry. For a through requirement,
however, the runtime owns the executable end condition: this engine
needs a strictly greater cut distance than the measured host span.
Recording the adjustment makes the operational allowance visible
without treating it as a user-specified blind-cut depth.
"""
if not require_through or contract.get("atomic_id") != "extrude_cut_blind":
return []
params = fragment.get("feature", {}).get("params", {})
sketch = fragment.get("sketch") if isinstance(fragment.get("sketch"), dict) else {}
workplane = sketch.get("workplane") if isinstance(sketch, dict) else {}
normal = workplane.get("normal") if isinstance(workplane, dict) else None
thickness = self._span_from_bbox(self._active_body_bbox(selector_tokens), normal)
distance = params.get("distance_mm") if isinstance(params, dict) else None
if not isinstance(distance, (int, float)) or thickness is None:
return []
required_distance = float(thickness) + 0.01
if float(distance) > float(thickness) + 1e-6:
return []
params["distance_mm"] = required_distance
return [{
"path": "/feature/params/distance_mm",
"submitted_mm": float(distance),
"materialized_mm": required_distance,
"reason": "required through-cut exit allowance",
}]
def _preflight_sketch_workplane(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
sketch = fragment.get("sketch")
workplane = sketch.get("workplane") if isinstance(sketch, dict) else None
if not isinstance(workplane, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch workplane is unavailable")
normal, x_dir = workplane.get("normal"), workplane.get("x_dir")
if not isinstance(normal, list) or not isinstance(x_dir, list) or self._norm(normal) <= 1e-9 or self._norm(x_dir) <= 1e-9 or self._norm(self._cross(normal, x_dir)) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch workplane vectors are degenerate")
def _preflight_profile_non_self_intersecting(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
sketch = fragment.get("sketch")
profile = sketch.get("profile") if isinstance(sketch, dict) else None
if not isinstance(profile, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch profile is unavailable")
if profile.get("type") == "polygon":
vertices = profile.get("vertices")
if not isinstance(vertices, list) or len(vertices) < 3 or self._polygon_self_intersects(vertices):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: polygon profile self-intersects")
elif profile.get("type") == "analytic_contours":
self._preflight_analytic_contours(profile)
def _preflight_extrude_cut_contacts_material(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]]) -> list[float] | None:
"""Reject an extrude cut whose start profile floats above the solid.
Sketch cuts do not carry a host-face selector, so a model can place a
correct 2-D profile on the top of an unrelated boss. The engine then
rebuilds successfully but leaves the body unchanged. Detect the common
no-contact form using the current planar topology and return a useful
coordinate diagnosis before allocating a stage or running the kernel.
"""
sketch = fragment.get("sketch") if isinstance(fragment, dict) else None
workplane = sketch.get("workplane") if isinstance(sketch, dict) else None
profile = sketch.get("profile") if isinstance(sketch, dict) else None
origin = workplane.get("origin_mm") if isinstance(workplane, dict) else None
normal = workplane.get("normal") if isinstance(workplane, dict) else None
x_dir = workplane.get("x_dir") if isinstance(workplane, dict) else None
if not self._valid_vector3(origin) or not self._valid_vector3(normal, require_nonzero=True) or not self._valid_vector3(x_dir, require_nonzero=True) or not isinstance(profile, dict):
return None
unit_normal = [float(component) / self._norm(normal) for component in normal]
x_projection = self._dot(x_dir, unit_normal)
raw_x = [float(x_dir[index]) - x_projection * unit_normal[index] for index in range(3)]
if self._norm(raw_x) <= 1e-9:
return None
unit_x = [value / self._norm(raw_x) for value in raw_x]
unit_y = self._cross(unit_normal, unit_x)
local_points = self._profile_probe_points(profile)
if not local_points:
return None
world_points = [
[
float(origin[index]) + local[0] * unit_x[index] + local[1] * unit_y[index]
for index in range(3)
]
for local in local_points
]
tolerance = 1e-5
matching_faces: list[dict[str, Any]] = []
available_heights: list[float] = []
for value in selectors.values():
geometry = value.get("geometry") if isinstance(value, dict) else None
face_normal = geometry.get("normal") if isinstance(geometry, dict) else None
center = geometry.get("center_mm") if isinstance(geometry, dict) else None
loops = geometry.get("boundary_loops_mm") if isinstance(geometry, dict) else None
if (
not isinstance(geometry, dict)
or geometry.get("surface_type") != "plane"
or not self._valid_vector3(face_normal, require_nonzero=True)
or not self._valid_vector3(center)
or not isinstance(loops, list)
or not loops
):
continue
unit_face_normal = [float(component) / self._norm(face_normal) for component in face_normal]
if abs(abs(self._dot(unit_normal, unit_face_normal)) - 1.0) > 1e-6:
continue
available_heights.append(self._dot([float(center[index]) for index in range(3)], unit_normal))
if abs(self._dot([float(origin[index]) - float(center[index]) for index in range(3)], unit_normal)) <= tolerance:
matching_faces.append(geometry)
if not matching_faces:
return None
for face in matching_faces:
if any(
self._point_in_planar_face(point, face["boundary_loops_mm"], unit_normal, tolerance)
for point in world_points
):
face_normal = face.get("normal")
return [float(component) for component in face_normal] if self._valid_vector3(face_normal, require_nonzero=True) else None
plane_coordinate = self._dot([float(value) for value in origin], unit_normal)
heights = ", ".join(f"{value:g}" for value in sorted(set(round(value, 6) for value in available_heights))[:8])
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: extrude-cut profile does not contact material on its start plane "
f"(plane coordinate {plane_coordinate:g}; available parallel planar faces: [{heights}]); "
"place the sketch on the material face containing the intended cut profile"
)
@staticmethod
def _profile_probe_points(profile: dict[str, Any]) -> list[tuple[float, float]]:
"""Return inexpensive local points sufficient for contact preflight."""
points: list[tuple[float, float]] = []
def append(value: Any) -> None:
point = ProfileCadRuntime._point2(value)
if point is not None:
points.append(point)
profile_type = profile.get("type")
if profile_type == "circle":
append(profile.get("center"))
elif profile_type == "rectangle":
append(profile.get("center"))
elif profile_type == "polygon":
vertices = profile.get("vertices")
if isinstance(vertices, list):
for vertex in vertices:
append(vertex)
elif profile_type == "analytic_contours":
contours = profile.get("contours")
if isinstance(contours, list):
for contour in contours:
segments = contour.get("segments") if isinstance(contour, dict) else None
if not isinstance(segments, list):
continue
contour_points: list[tuple[float, float]] = []
for segment in segments:
if not isinstance(segment, dict):
continue
for key in ("start", "end", "center"):
point = ProfileCadRuntime._point2(segment.get(key))
if point is not None:
points.append(point)
contour_points.append(point)
if contour_points:
points.append((
sum(point[0] for point in contour_points) / len(contour_points),
sum(point[1] for point in contour_points) / len(contour_points),
))
return points
def _preflight_host_face_exists(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if len(supplied) != 1 or not isinstance(selectors.get(supplied[0]), dict) or selectors[supplied[0]].get("kind") != "face":
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: host face is absent or stale")
def _materialized_hole_host_frame(
self,
selected: dict[str, Any],
positions: Any,
) -> tuple[dict[str, Any], list[dict[str, list[float]]]]:
"""Lower a verified planar host selector into replay-stable CDSL data.
Hole position inputs are world coordinates at the author boundary.
The engine's frame form uses local coordinates, so both values must
be converted together. Keeping only the selector makes a later
pattern replay depend on a face that the source cut has already
subdivided, which is neither stable nor geometrically meaningful.
"""
geometry = selected.get("geometry") if isinstance(selected, dict) else None
center = geometry.get("center_mm") if isinstance(geometry, dict) else None
normal = geometry.get("normal") if isinstance(geometry, dict) else None
if not self._valid_vector3(center) or not self._valid_vector3(normal, require_nonzero=True):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected host face has no usable plane frame")
if not isinstance(positions, list) or not positions:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole positions are unavailable for host-frame materialization")
origin = [float(value) for value in center]
unit_normal = [float(value) / self._norm(normal) for value in normal]
seed = [1.0, 0.0, 0.0] if abs(unit_normal[0]) < 0.9 else [0.0, 1.0, 0.0]
x_raw = [seed[index] - self._dot(seed, unit_normal) * unit_normal[index] for index in range(3)]
x_length = self._norm(x_raw)
if x_length <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected host face cannot define a stable x direction")
x_dir = [value / x_length for value in x_raw]
y_dir = self._cross(unit_normal, x_dir)
local_positions: list[dict[str, list[float]]] = []
for position in positions:
point = position.get("mm") if isinstance(position, dict) else None
if not self._valid_vector3(point):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not a finite 3D point")
offset = [float(point[index]) - origin[index] for index in range(3)]
local_positions.append({"mm": [
self._dot(offset, x_dir),
self._dot(offset, y_dir),
self._dot(offset, unit_normal),
]})
return (
{"frame": {"origin_mm": origin, "x_dir": x_dir, "y_dir": y_dir, "normal": unit_normal}},
local_positions,
)
def _preflight_hole_positions_on_host_plane(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
positions = fragment.get("feature", {}).get("params", {}).get("positions")
if not isinstance(positions, list) or not positions or not all(isinstance(item, dict) and isinstance(item.get("mm"), list) and len(item["mm"]) == 3 for item in positions):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole positions are invalid for the host plane")
supplied = fragment.get("feature", {}).get("selector_tokens", [])
host = selectors.get(supplied[0]) if len(supplied) == 1 else None
geometry = host.get("geometry") if isinstance(host, dict) else None
normal = geometry.get("normal") if isinstance(geometry, dict) else None
center = geometry.get("center_mm") if isinstance(geometry, dict) else None
bbox = geometry.get("bbox_mm") if isinstance(geometry, dict) else None
if not isinstance(geometry, dict) or geometry.get("surface_type") != "plane" or not self._valid_vector3(normal, require_nonzero=True) or not self._valid_vector3(center):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole host must be an observable planar face")
unit_normal = [float(component) / self._norm(normal) for component in normal]
tolerance = 1e-5
if not self._valid_bbox(bbox):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: host face has no usable topology bounds")
for position in positions:
point = position["mm"]
if not self._valid_vector3(point):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not a finite 3D point")
offset = [float(point[index]) - float(center[index]) for index in range(3)]
if abs(self._dot(offset, unit_normal)) > tolerance:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not on the selected host plane")
if any(float(point[index]) < float(bbox[index]) - tolerance or float(point[index]) > float(bbox[index + 3]) + tolerance for index in range(3)):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is outside the selected host-face bounds")
boundary_loops = geometry.get("boundary_loops_mm")
if isinstance(boundary_loops, list) and boundary_loops and not self._point_in_planar_face(point, boundary_loops, unit_normal, tolerance):
if not self._counterbore_reuses_existing_pilot(fragment, selectors, point, unit_normal, tolerance):
host_z = float(center[2]) if isinstance(center, list) and len(center) == 3 else float("nan")
coordinate = ", ".join(f"{float(value):g}" for value in point)
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: hole position "
f"[{coordinate}] lies outside the selected host face material boundary (host center z={host_z:g}); "
"select a planar host face that contains every requested hole center"
)
def _counterbore_reuses_existing_pilot(
self,
fragment: dict[str, Any],
selectors: dict[str, dict[str, Any]],
point: list[float],
host_normal: list[float],
tolerance: float,
) -> bool:
"""Allow a counterbore to start from an existing coaxial pilot bore.
A top face becomes annular after a through bore, so the pilot centre
is deliberately outside its material boundary. Counterboring that
pilot is nevertheless a standard valid operation. The exception is
deliberately narrow: it only applies to a matching inner cylindrical
bore whose axis is normal to the selected host plane and whose open
end contains the requested start point.
"""
feature = fragment.get("feature") if isinstance(fragment, dict) else None
params = feature.get("params") if isinstance(feature, dict) and isinstance(feature.get("params"), dict) else {}
pilot_diameter = params.get("diameter_mm")
counterbore_diameter = params.get("counterbore_diameter_mm")
if (
not isinstance(feature, dict)
or feature.get("atomic_id") != "hole_counterbore"
or not isinstance(pilot_diameter, (int, float))
or not isinstance(counterbore_diameter, (int, float))
or float(pilot_diameter) <= 0
or float(counterbore_diameter) <= float(pilot_diameter)
):
return False
diameter_tolerance = max(tolerance, abs(float(pilot_diameter)) * 1e-6)
for value in selectors.values():
geometry = value.get("geometry") if isinstance(value, dict) else None
if (
not isinstance(geometry, dict)
or geometry.get("surface_type") != "cylinder"
or geometry.get("cylinder_role") != "inner"
or not bool(geometry.get("through"))
):
continue
radius = geometry.get("radius_mm")
axis_origin = geometry.get("axis_origin_mm")
axis_direction = geometry.get("axis_direction")
bbox = geometry.get("bbox_mm")
if (
not isinstance(radius, (int, float))
or abs(2 * float(radius) - float(pilot_diameter)) > diameter_tolerance
or not self._valid_vector3(axis_origin)
or not self._valid_vector3(axis_direction, require_nonzero=True)
or not self._valid_bbox(bbox)
):
continue
unit_axis = [float(component) / self._norm(axis_direction) for component in axis_direction]
if abs(abs(self._dot(unit_axis, host_normal)) - 1.0) > 1e-6:
continue
offset = [float(point[index]) - float(axis_origin[index]) for index in range(3)]
axial = self._dot(offset, unit_axis)
radial = [offset[index] - axial * unit_axis[index] for index in range(3)]
if self._norm(radial) > tolerance:
continue
if any(float(point[index]) < float(bbox[index]) - tolerance or float(point[index]) > float(bbox[index + 3]) + tolerance for index in range(3)):
continue
return True
return False
def _preflight_analytic_contours(self, profile: dict[str, Any]) -> None:
contours = profile.get("contours")
if not isinstance(contours, list) or not contours:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: analytic profile requires at least one contour")
tolerance = 1e-7
for contour_index, contour in enumerate(contours):
segments = contour.get("segments") if isinstance(contour, dict) else None
if not isinstance(segments, list) or not segments:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is empty")
if any(segment.get("type") == "circle" for segment in segments if isinstance(segment, dict)):
if len(segments) != 1 or segments[0].get("type") != "circle":
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} cannot mix a full circle with other segments")
continue
endpoints: list[tuple[tuple[float, float], tuple[float, float]]] = []
for segment_index, segment in enumerate(segments):
if not isinstance(segment, dict) or segment.get("type") not in {"line", "arc"}:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} has an unsupported segment")
start = self._point2(segment.get("start"))
end = self._point2(segment.get("end"))
if start is None or end is None:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} segment {segment_index} has non-finite endpoints")
if self._distance2(start, end) <= tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} segment {segment_index} is degenerate")
if segment.get("type") == "arc":
center = self._point2(segment.get("center"))
radius = segment.get("radius_mm")
if center is None or not isinstance(radius, (int, float)) or isinstance(radius, bool) or not math.isfinite(float(radius)) or float(radius) <= 0:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} arc {segment_index} has an invalid circle")
arc_tolerance = max(tolerance, float(radius) * 1e-7)
if abs(self._distance2(start, center) - float(radius)) > arc_tolerance or abs(self._distance2(end, center) - float(radius)) > arc_tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} arc {segment_index} endpoints are not on the declared circle")
endpoints.append((start, end))
for segment_index in range(1, len(endpoints)):
if self._distance2(endpoints[segment_index - 1][1], endpoints[segment_index][0]) > tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is discontinuous before segment {segment_index}")
if contour.get("closed") is True and self._distance2(endpoints[-1][1], endpoints[0][0]) > tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is not closed")
line_segments = [segment for segment, raw in zip(endpoints, segments) if raw.get("type") == "line"]
for first, (a, b) in enumerate(line_segments):
for second, (c, d) in enumerate(line_segments):
if second <= first + 1 or (first == 0 and second == len(line_segments) - 1 and contour.get("closed") is True):
continue
if self._segments_intersect(a, b, c, d):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} has an obvious self-intersection")
@classmethod
def _validate_materialized_runtime_types(cls, feature: dict[str, Any]) -> None:
atomic_id = str(feature.get("atomic_id") or "")
if not atomic_id.startswith("hole_"):
return
params = feature.get("params") if isinstance(feature.get("params"), dict) else {}
host = params.get("host_face") if isinstance(params.get("host_face"), dict) else None
frame = host.get("frame") if isinstance(host, dict) and isinstance(host.get("frame"), dict) else None
if not isinstance(frame, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole host_face.frame is unavailable")
for field in ("origin_mm", "x_dir", "y_dir", "normal"):
if not cls._valid_vector3(frame.get(field), require_nonzero=field != "origin_mm"):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: materialized hole host_face.frame.{field} is invalid")
x_dir, y_dir, normal = frame["x_dir"], frame["y_dir"], frame["normal"]
if abs(cls._dot(x_dir, y_dir)) > 1e-6 or abs(cls._dot(x_dir, normal)) > 1e-6 or abs(cls._dot(y_dir, normal)) > 1e-6:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole host frame is not orthogonal")
positions = params.get("positions")
if not isinstance(positions, list) or not positions:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole positions are unavailable")
for index, position in enumerate(positions):
point = position.get("mm") if isinstance(position, dict) else None
if not cls._valid_vector3(point) or abs(float(point[2])) > 1e-5:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: materialized hole position {index} is invalid in the host frame")
@staticmethod
def _validate_finite_tree(value: Any, path: str = "$") -> None:
if value is None:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: {path} must not be null")
if isinstance(value, float) and not math.isfinite(value):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: {path} must be finite")
if isinstance(value, dict):
for key, child in value.items():
ProfileCadRuntime._validate_finite_tree(child, f"{path}.{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
ProfileCadRuntime._validate_finite_tree(child, f"{path}[{index}]")
@staticmethod
def _point2(value: Any) -> tuple[float, float] | None:
if not isinstance(value, list) or len(value) != 2:
return None
if not all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value):
return None
return float(value[0]), float(value[1])
@staticmethod
def _distance2(left: tuple[float, float], right: tuple[float, float]) -> float:
return math.hypot(left[0] - right[0], left[1] - right[1])
@classmethod
def _point_in_planar_face(cls, point: list[float], loops: list[Any], normal: list[float], tolerance: float) -> bool:
drop_axis = max(range(3), key=lambda index: abs(float(normal[index])))
def project(value: Any) -> tuple[float, float] | None:
if not cls._valid_vector3(value):
return None
coordinates = [float(value[index]) for index in range(3) if index != drop_axis]
return coordinates[0], coordinates[1]
projected_point = project(point)
if projected_point is None:
return False
polygons: list[list[tuple[float, float]]] = []
for loop in loops:
polygon = [project(vertex) for vertex in loop] if isinstance(loop, list) else []
if len(polygon) >= 3 and all(vertex is not None for vertex in polygon):
polygons.append([vertex for vertex in polygon if vertex is not None])
if not polygons:
return False
def area(polygon: list[tuple[float, float]]) -> float:
return abs(sum(
polygon[index][0] * polygon[(index + 1) % len(polygon)][1]
- polygon[(index + 1) % len(polygon)][0] * polygon[index][1]
for index in range(len(polygon))
)) / 2
def contains(polygon: list[tuple[float, float]]) -> bool:
x, y = projected_point
inside = False
for index, first in enumerate(polygon):
second = polygon[(index + 1) % len(polygon)]
if cls._distance_to_segment_2d(projected_point, first, second) <= tolerance:
return True
if (first[1] > y) != (second[1] > y):
crossing_x = (second[0] - first[0]) * (y - first[1]) / (second[1] - first[1]) + first[0]
if x < crossing_x:
inside = not inside
return inside
outer = max(polygons, key=area)
return contains(outer) and not any(contains(inner) for inner in polygons if inner is not outer)
@staticmethod
def _distance_to_segment_2d(point: tuple[float, float], start: tuple[float, float], end: tuple[float, float]) -> float:
dx, dy = end[0] - start[0], end[1] - start[1]
length_squared = dx * dx + dy * dy
if length_squared <= 1e-18:
return math.hypot(point[0] - start[0], point[1] - start[1])
fraction = max(0.0, min(1.0, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dy) / length_squared))
return math.hypot(point[0] - (start[0] + fraction * dx), point[1] - (start[1] + fraction * dy))
def _preflight_cut_exit_distance(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, require_through: bool) -> None:
if not require_through:
return
params = fragment.get("feature", {}).get("params", {})
depth = params.get("depth_mm", params.get("distance_mm"))
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if not isinstance(depth, (int, float)):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: cut depth is unavailable")
if supplied:
host = selectors[supplied[0]]["geometry"]
bbox = host.get("bbox_mm") if isinstance(host, dict) else None
normal = host.get("normal") or host.get("plane_normal") if isinstance(host, dict) else None
thickness = self._span_from_bbox(bbox, normal)
else:
bbox = self._active_body_bbox(selectors)
normal = (fragment.get("sketch") or {}).get("workplane", {}).get("normal")
thickness = self._span_from_bbox(bbox, normal)
if thickness is None:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: cannot prove through-cut exit distance from the active topology")
if float(depth) <= thickness + 1e-6:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: "
f"cut depth {float(depth):g} mm must exceed measured host-body thickness {thickness:g} mm"
)
def _preflight_requires_active_solid(self, _fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
# A committed active CDSL document exists only after a candidate with
# a solid has been accepted. Reference and subtraction operations
# therefore cannot be the first feature of a part.
if not isinstance(base, dict) or not base.get("features"):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: operation requires an active solid checkpoint")
def _preflight_revolve_axis_on_sketch(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
axis = fragment.get("feature", {}).get("params", {}).get("axis")
direction = axis.get("direction") if isinstance(axis, dict) else None
origin = axis.get("origin_mm") if isinstance(axis, dict) else None
workplane = fragment.get("sketch", {}).get("workplane") if isinstance(fragment.get("sketch"), dict) else None
plane_origin = workplane.get("origin_mm") if isinstance(workplane, dict) else None
plane_normal = workplane.get("normal") if isinstance(workplane, dict) else None
if not self._valid_vector3(direction, require_nonzero=True) or not self._valid_vector3(origin) or not self._valid_vector3(plane_origin) or not self._valid_vector3(plane_normal, require_nonzero=True):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis or sketch plane is invalid")
unit_direction = [float(component) / self._norm(direction) for component in direction]
unit_normal = [float(component) / self._norm(plane_normal) for component in plane_normal]
if abs(self._dot(unit_direction, unit_normal)) > 1e-6:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis is not parallel to the sketch plane")
axis_offset = [float(origin[index]) - float(plane_origin[index]) for index in range(3)]
if abs(self._dot(axis_offset, unit_normal)) > 1e-5:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis is not on the sketch plane")
def _preflight_reference_plane_nonzero_normal(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
plane = fragment.get("feature", {}).get("params", {}).get("plane")
normal = plane.get("normal") if isinstance(plane, dict) else None
if not isinstance(normal, list) or self._norm(normal) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference plane normal is degenerate")
def _preflight_reference_axis_nonzero_direction(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
axis = fragment.get("feature", {}).get("params", {}).get("axis")
direction = axis.get("direction") if isinstance(axis, dict) else None
if not isinstance(direction, list) or self._norm(direction) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference axis direction is degenerate")
def _preflight_selected_edges_exist(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if not supplied or not all(isinstance(selectors.get(token), dict) and selectors[token].get("kind") == "edge" for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected edge is absent or stale")
def _preflight_source_features_exist(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
params = fragment.get("feature", {}).get("params", {})
known = {str(feature.get("id") or "") for feature in ((base or {}).get("features") or ()) if isinstance(feature, dict)}
if not set(params.get("source_feature_ids") or ()).issubset(known):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: source feature is not part of current head")
def _preflight_mirror_plane_exists(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if len(supplied) != 1 or not isinstance(selectors.get(supplied[0]), dict) or selectors[supplied[0]].get("kind") != "plane":
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: mirror plane is absent or stale")
def _loft_profile_sketches(self, fragment: dict[str, Any], base: dict[str, Any] | None) -> list[dict[str, Any]]:
"""Resolve ``profile_sketch_ids`` against the base document's sketches."""
ids = [str(value) for value in fragment.get("feature", {}).get("params", {}).get("profile_sketch_ids") or ()]
sketches = (base or {}).get("geometry", {}).get("sketches") if isinstance((base or {}).get("geometry"), dict) else None
by_id = {
str(sketch.get("id") or ""): sketch
for sketch in (sketches or ())
if isinstance(sketch, dict)
}
resolved: list[dict[str, Any]] = []
for sketch_id in ids:
sketch = by_id.get(sketch_id)
if not isinstance(sketch, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: loft profile sketch is not part of current head")
resolved.append(sketch)
if not resolved:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: loft has no profile sketches")
return resolved
def _preflight_loft_profiles_exist(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
self._loft_profile_sketches(fragment, base)
def _preflight_loft_profiles_closed(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
# circle/polygon 草图类型闭合性由 schema 保证;analytic_contours 需要
# 每条参与轮廓显式 closed。
for sketch in self._loft_profile_sketches(fragment, base):
profile = sketch.get("profile") if isinstance(sketch.get("profile"), dict) else None
if profile is None:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: loft profile sketch has no profile")
if profile.get("type") == "analytic_contours":
contours = profile.get("contours")
if not isinstance(contours, list) or not contours:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: loft profile has no contours")
for contour in contours:
if not isinstance(contour, dict) or not contour.get("closed"):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: loft profile contour is not closed")
def _preflight_loft_profiles_single_region(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
# 每个放样截面必须是单连通区域:analytic_contours 只允许恰好一条
# outer 闭合轮廓,不得携带 inner 环或 open 段。
for sketch in self._loft_profile_sketches(fragment, base):
profile = sketch.get("profile") if isinstance(sketch.get("profile"), dict) else None
if profile is None:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: loft profile sketch has no profile")
if profile.get("type") == "analytic_contours":
contours = [contour for contour in (profile.get("contours") or []) if isinstance(contour, dict)]
outer = [contour for contour in contours if contour.get("role") == "outer" and contour.get("closed")]
if len(outer) != 1 or len(outer) != len(contours):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: loft profile must be a single closed region")
@staticmethod
def _polygon_self_intersects(vertices: list[Any]) -> bool:
points = [(float(point[0]), float(point[1])) for point in vertices if isinstance(point, list) and len(point) == 2]
if len(points) != len(vertices):
return True
segments = list(zip(points, [*points[1:], points[0]]))
for first, (a, b) in enumerate(segments):
for second, (c, d) in enumerate(segments):
if second <= first + 1 or (first == 0 and second == len(segments) - 1):
continue
if ProfileCadRuntime._segments_intersect(a, b, c, d):
return True
return False
@staticmethod
def _segments_intersect(a: tuple[float, float], b: tuple[float, float], c: tuple[float, float], d: tuple[float, float]) -> bool:
def orientation(p: tuple[float, float], q: tuple[float, float], r: tuple[float, float]) -> float:
return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])
left = orientation(a, b, c)
right = orientation(a, b, d)
low = orientation(c, d, a)
high = orientation(c, d, b)
return (left > 0) != (right > 0) and (low > 0) != (high > 0)
@staticmethod
def _active_body_bbox(selector_tokens: dict[str, dict[str, Any]]) -> list[float] | None:
for token in selector_tokens.values():
if token.get("kind") != "body":
continue
geometry = token.get("geometry")
bbox = geometry.get("bbox_mm") if isinstance(geometry, dict) else None
if isinstance(bbox, list) and len(bbox) == 6 and all(isinstance(value, (int, float)) for value in bbox):
return [float(value) for value in bbox]
return None
@staticmethod
def _span_from_bbox(bbox: Any, normal: Any) -> float | None:
if not isinstance(bbox, list) or len(bbox) != 6 or not isinstance(normal, list) or len(normal) != 3:
return None
if not all(isinstance(value, (int, float)) for value in [*bbox, *normal]):
return None
length = math.sqrt(sum(float(value) ** 2 for value in normal))
if length <= 1e-9:
return None
return sum(
abs(float(normal[index]) / length) * abs(float(bbox[index + 3]) - float(bbox[index]))
for index in range(3)
)
@staticmethod
def _norm(vector: list[float]) -> float:
return math.sqrt(sum(float(item) ** 2 for item in vector))
@staticmethod
def _dot(left: list[float], right: list[float]) -> float:
return sum(float(left[index]) * float(right[index]) for index in range(3))
@staticmethod
def _valid_vector3(value: Any, *, require_nonzero: bool = False) -> bool:
valid = (
isinstance(value, list)
and len(value) == 3
and all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value)
)
return valid and (not require_nonzero or ProfileCadRuntime._norm(value) > 1e-9)
@staticmethod
def _valid_bbox(value: Any) -> bool:
return (
isinstance(value, list)
and len(value) == 6
and all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value)
and all(float(value[index]) <= float(value[index + 3]) for index in range(3))
)
@staticmethod
def _cross(a: list[float], b: list[float]) -> list[float]:
return [float(a[1]) * float(b[2]) - float(a[2]) * float(b[1]), float(a[2]) * float(b[0]) - float(a[0]) * float(b[2]), float(a[0]) * float(b[1]) - float(a[1]) * float(b[0])]
@staticmethod
def _write_json(path: Path, payload: dict[str, Any]) -> None:
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
@staticmethod
def _health(engine_result: dict[str, Any], step_path: Path, glb_path: Path | None = None) -> dict[str, Any]:
bbox = engine_result.get("bbox_mm") if isinstance(engine_result.get("bbox_mm"), dict) else {}
minimum, maximum = bbox.get("min"), bbox.get("max")
if not isinstance(minimum, list) or not isinstance(maximum, list) or len(minimum) != 3 or len(maximum) != 3 or not step_path.is_file() or (glb_path is not None and not glb_path.is_file()):
raise RuntimeAdapterError("rebuild did not produce complete deterministic artifacts")
return {"bbox_mm": {"min": [float(item) for item in minimum], "max": [float(item) for item in maximum], "dimensions": [float(maximum[index]) - float(minimum[index]) for index in range(3)]}, "volume_mm3": float(engine_result["volume_mm3"]), "solid_count": int(engine_result["solid_count"]), "feature_count": len(engine_result.get("feature_results") or [])}