添加cadfs_to_cdsl代码,优化cad生成

This commit is contained in:
2026-09-03 10:40:51 +08:00
parent f3eb5dff54
commit c25b73c71e
34 changed files with 3586 additions and 147 deletions
+346 -21
View File
@@ -124,7 +124,7 @@ class ProfileCadRuntime:
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) -> tuple[dict[str, Any], dict[str, Any]]:
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
@@ -168,7 +168,24 @@ class ProfileCadRuntime:
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]
self._semantic_preflight(materialized, contract, selector_tokens, base_cdsl, require_through=require_through)
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": [],
}
@@ -177,9 +194,17 @@ class ProfileCadRuntime:
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": [str(features[-1].get("id"))] if features else []}
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"]
@@ -225,35 +250,69 @@ class ProfileCadRuntime:
"RUNTIME_CONTRACT_INVALID: materialized fragment violates the engine CDSL schema: "
+ message
) from error
return document, {"schema_version": "cad.v3.fragment-audit.v1", "atomic_id": contract["atomic_id"], "fragment_hash": canonical_hash(fragment), "contract_hash": contract["contract_hash"], "assigned_feature_ids": [output["id"]], "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 []}
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 rebuild(self, cdsl: dict[str, Any], output_dir: str, task_id: str, revision_id: str) -> dict[str, Any]:
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"
glb_path = root / "model.glb"
self._write_json(root / "model.cdsl.json", cdsl_copy)
try:
self._validate_finite_tree(cdsl_copy)
validate_cdsl(cdsl_copy, self.engine)
except RuntimeAdapterError:
raise
except Exception as error:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: persisted CDSL failed rebuild preflight: {error}") from error
try:
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")
preview = step_to_glb(step_path, glb_path)
health = self._health(engine_result, step_path, glb_path)
topology = topology_snapshot(engine_result, task_id=task_id, revision_id=revision_id, preview=preview)
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)
manifest = render_checkpoint(self.settings, step_path=step_path, output_dir=root / "renders")
report = {"engine_result": engine_result, "preview": preview, "health": health, "render_manifest": manifest}
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": manifest, "paths": {"cdsl": "model.cdsl.json", "step": "model.step", "glb": "model.glb", "topology": "model.topology.json", "report": "rebuild-report.json", "render_manifest": "renders/render-manifest.json"}}
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.
@@ -330,7 +389,7 @@ class ProfileCadRuntime:
}
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) -> None:
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"]
@@ -340,6 +399,83 @@ class ProfileCadRuntime:
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")
@@ -362,6 +498,123 @@ class ProfileCadRuntime:
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":
@@ -439,7 +692,79 @@ class ProfileCadRuntime:
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):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is outside the selected host face's effective boundary")
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")
@@ -750,9 +1075,9 @@ class ProfileCadRuntime:
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) -> dict[str, Any]:
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 not glb_path.is_file():
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 [])}