1137 lines
68 KiB
Python
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 [])}
|