更新功能
This commit is contained in:
@@ -15,6 +15,7 @@ from jsonschema.exceptions import SchemaError
|
||||
from vendor.cdsl_preview_runtime import step_to_glb
|
||||
from app.services.quality import evaluate_quality, validate_verification
|
||||
from app.services.storage import WorkspaceStore, now_iso, write_json
|
||||
from app.services.cdsl_fragment import selector_bindings
|
||||
from app.settings import Settings
|
||||
|
||||
|
||||
@@ -299,7 +300,140 @@ def parameter_contract(cdsl: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"schema_version": "1.0", "parameters": _derived_parameters(cdsl), "source": "derived"}
|
||||
|
||||
|
||||
def topology_sidecars(engine_result: dict[str, Any], preview: dict[str, Any] | None = None) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
def topology_snapshot(
|
||||
engine_result: dict[str, Any],
|
||||
*,
|
||||
task_id: str = "",
|
||||
revision_id: str = "",
|
||||
preview: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
raw_records = [
|
||||
raw for raw in engine_result.get("topology_records") or ()
|
||||
if isinstance(raw, dict) and raw.get("record_id") and raw.get("kind")
|
||||
]
|
||||
active_body_id = next(
|
||||
(
|
||||
str(result.get("body_id"))
|
||||
for result in reversed(engine_result.get("feature_results") or ())
|
||||
if isinstance(result, dict) and result.get("body_id")
|
||||
),
|
||||
"",
|
||||
)
|
||||
if not active_body_id:
|
||||
active_body_id = next(
|
||||
(
|
||||
str(raw.get("body_id"))
|
||||
for raw in reversed(raw_records)
|
||||
if raw.get("kind") == "body" and raw.get("body_id")
|
||||
),
|
||||
"",
|
||||
)
|
||||
|
||||
# The runtime retains historical B-rep records for provenance, but only
|
||||
# the final body can resolve face, edge, vertex, and body selectors.
|
||||
active_records = [
|
||||
raw for raw in raw_records
|
||||
if not active_body_id
|
||||
or raw.get("kind") in {"plane", "axis"}
|
||||
or str(raw.get("body_id") or "") == active_body_id
|
||||
]
|
||||
records: list[dict[str, Any]] = []
|
||||
for raw in active_records:
|
||||
kind = str(raw.get("kind"))
|
||||
records.append({
|
||||
"record_id": str(raw["record_id"]),
|
||||
"kind": kind,
|
||||
"feature_id": str(raw.get("feature_id") or ""),
|
||||
"body_id": str(raw.get("body_id") or "") or None,
|
||||
"owner_feature_ids": [str(item) for item in raw.get("owner_feature_ids") or () if str(item)],
|
||||
"geometry": copy.deepcopy(raw.get("geometry") or {}),
|
||||
"executable": kind in {"body", "face", "edge", "vertex", "plane", "axis"},
|
||||
"synthetic": False,
|
||||
})
|
||||
# Preview/B-rep fallback faces are useful for visual explanation only.
|
||||
# Keep them in the unified audit snapshot, but never expose them as
|
||||
# executable selector candidates.
|
||||
if not any(item.get("kind") == "face" for item in records):
|
||||
for index, raw in enumerate((preview or {}).get("topology_faces") or ()):
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
record_id = str(raw.get("id") or f"synthetic:face:{index}")
|
||||
center = raw.get("center")
|
||||
normal = raw.get("normal")
|
||||
raw_bbox = raw.get("bbox")
|
||||
if isinstance(raw_bbox, dict) and isinstance(raw_bbox.get("min"), list) and isinstance(raw_bbox.get("max"), list):
|
||||
raw_bbox = [*raw_bbox["min"], *raw_bbox["max"]]
|
||||
geometry = {
|
||||
"surface_type": str(raw.get("surface_type") or "unknown"),
|
||||
"center_mm": copy.deepcopy(center) if isinstance(center, list) else None,
|
||||
"normal": copy.deepcopy(normal) if isinstance(normal, list) else None,
|
||||
"bbox_mm": copy.deepcopy(raw_bbox or {}),
|
||||
}
|
||||
records.append({
|
||||
"record_id": record_id,
|
||||
"kind": "face",
|
||||
"feature_id": "",
|
||||
"body_id": None,
|
||||
"owner_feature_ids": [],
|
||||
"geometry": geometry,
|
||||
"executable": False,
|
||||
"synthetic": True,
|
||||
})
|
||||
return {
|
||||
"schema_version": "cad.topology.v1",
|
||||
"task_id": task_id,
|
||||
"revision_id": revision_id,
|
||||
"snapshot_id": f"{task_id}/{revision_id}" if task_id and revision_id else "",
|
||||
"body_id": active_body_id,
|
||||
"records": records,
|
||||
}
|
||||
|
||||
|
||||
def topology_sidecars(
|
||||
engine_result: dict[str, Any],
|
||||
preview: dict[str, Any] | None = None,
|
||||
*,
|
||||
snapshot: dict[str, Any] | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
runtime_records = (snapshot or topology_snapshot(engine_result)).get("records") or []
|
||||
runtime_faces = [item for item in runtime_records if item.get("kind") == "face" and item.get("executable", True)]
|
||||
runtime_edges = [item for item in runtime_records if item.get("kind") == "edge" and item.get("executable", True)]
|
||||
if runtime_faces or runtime_edges:
|
||||
references = []
|
||||
for record in runtime_faces:
|
||||
geometry = record.get("geometry") or {}
|
||||
references.append({
|
||||
"id": str(record.get("record_id")),
|
||||
"selectorType": "face",
|
||||
"label": str(geometry.get("surface_type") or "face"),
|
||||
"center": geometry.get("center_mm"),
|
||||
"normal": geometry.get("normal"),
|
||||
"frame": {
|
||||
"origin_mm": geometry.get("center_mm"),
|
||||
"normal": geometry.get("normal"),
|
||||
"x_dir": [1, 0, 0],
|
||||
"y_dir": [0, 1, 0],
|
||||
},
|
||||
"bbox": geometry.get("bbox_mm") or {},
|
||||
"surface_type": str(geometry.get("surface_type") or "unknown"),
|
||||
"owner_feature_ids": record.get("owner_feature_ids") or [],
|
||||
"source": "runtime_snapshot",
|
||||
"snapshot_id": (snapshot or {}).get("snapshot_id") or "",
|
||||
"executable": True,
|
||||
})
|
||||
edge_records = [
|
||||
{
|
||||
**record,
|
||||
"selectorType": "edge",
|
||||
"source": "runtime_snapshot",
|
||||
"snapshot_id": (snapshot or {}).get("snapshot_id") or "",
|
||||
}
|
||||
for record in runtime_edges
|
||||
]
|
||||
return (
|
||||
{"schema_version": "cad.topology.v1", "references": references, "edges": edge_records},
|
||||
{"schema_version": "cad.topology.v1", "edges": edge_records},
|
||||
)
|
||||
topology_faces = (preview or {}).get("topology_faces")
|
||||
if isinstance(topology_faces, list) and topology_faces:
|
||||
references = []
|
||||
@@ -322,6 +456,9 @@ def topology_sidecars(engine_result: dict[str, Any], preview: dict[str, Any] | N
|
||||
"surface_type": str(face.get("surface_type") or "unknown"),
|
||||
"triangle_start": int(face.get("triangle_start") or 0),
|
||||
"triangle_count": int(face.get("triangle_count") or 0),
|
||||
"source": "preview",
|
||||
"synthetic": True,
|
||||
"executable": False,
|
||||
})
|
||||
if references:
|
||||
return ({"schema_version": "1.1", "references": references}, {"schema_version": "1.0", "edges": []})
|
||||
@@ -346,6 +483,9 @@ def topology_sidecars(engine_result: dict[str, Any], preview: dict[str, Any] | N
|
||||
"center": point, "normal": normal,
|
||||
"frame": {"origin_mm": point, "normal": normal, "x_dir": x_dir, "y_dir": y_dir},
|
||||
"bbox": {"min": minimum, "max": maximum},
|
||||
"source": "bbox_fallback",
|
||||
"synthetic": True,
|
||||
"executable": False,
|
||||
}
|
||||
for name, point, normal, x_dir, y_dir in definitions
|
||||
]
|
||||
@@ -463,6 +603,11 @@ def build_revision(
|
||||
repair_attempts: int = 0,
|
||||
verification: dict[str, Any] | None = None,
|
||||
reference_records: list[dict[str, Any]] | None = None,
|
||||
feature_plan: dict[str, Any] | None = None,
|
||||
node_id: str = "",
|
||||
fragment: dict[str, Any] | None = None,
|
||||
branch_id: str = "main",
|
||||
visibility: str = "final",
|
||||
) -> dict[str, Any]:
|
||||
engine = load_engine(settings)
|
||||
task = store.ensure_task(task_id, request)
|
||||
@@ -476,12 +621,17 @@ def build_revision(
|
||||
parameters_path = revision_dir / "parameters.json"
|
||||
selector_path = revision_dir / "model.selector.json"
|
||||
edges_path = revision_dir / "model.edges.json"
|
||||
topology_path = revision_dir / "model.topology.json"
|
||||
part_skills_path = revision_dir / "part-skills.json"
|
||||
quality_path = revision_dir / "quality-report.json"
|
||||
snapshot_manifest_path = revision_dir / "snapshot-manifest.json"
|
||||
fragment_path = revision_dir / "fragment.json"
|
||||
selector_bindings_path = revision_dir / "selector-bindings.json"
|
||||
part_skill_audit = _part_skill_audit(part_skills, request, generation_assumptions)
|
||||
generation_context = _generation_context(reference_ids, part_skill_audit)
|
||||
write_json(request_path, {"request": request, "created_at": now_iso()})
|
||||
if isinstance(feature_plan, dict):
|
||||
store.write_feature_plan(task["task_id"], feature_plan)
|
||||
write_json(references_path, {"reference_ids": reference_ids, "records": [item for item in reference_records or [] if isinstance(item, dict)]})
|
||||
write_json(part_skills_path, part_skill_audit)
|
||||
write_json(snapshot_manifest_path, {
|
||||
@@ -499,6 +649,7 @@ def build_revision(
|
||||
"cdsl_path": cdsl_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"report_path": report_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"parameters_path": parameters_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"topology_path": topology_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"part_skills_path": part_skills_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"quality_path": quality_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"snapshot_manifest_path": snapshot_manifest_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
@@ -512,6 +663,11 @@ def build_revision(
|
||||
"operation": operation or {},
|
||||
"input_attachments": input_attachments or [],
|
||||
"repair_attempts": max(0, int(repair_attempts)),
|
||||
"branch_id": branch_id or "main",
|
||||
"visibility": visibility if visibility in {"checkpoint", "final", "superseded"} else "checkpoint",
|
||||
"node_id": node_id,
|
||||
"fragment_path": fragment_path.relative_to(store.task_dir(task["task_id"])).as_posix() if fragment else "",
|
||||
"selector_bindings_path": selector_bindings_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
}
|
||||
if status == "success":
|
||||
record.update({
|
||||
@@ -519,6 +675,7 @@ def build_revision(
|
||||
"glb_path": glb_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"selector_path": selector_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"edges_path": edges_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"topology_path": topology_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"engine": engine_name,
|
||||
})
|
||||
else:
|
||||
@@ -538,6 +695,8 @@ def build_revision(
|
||||
if not isinstance(meta.get("editable_parameters"), list) or not meta["editable_parameters"]:
|
||||
meta["editable_parameters"] = _derived_parameters(cdsl_copy)
|
||||
write_json(cdsl_path, cdsl_copy)
|
||||
if fragment is not None:
|
||||
write_json(fragment_path, fragment)
|
||||
write_json(parameters_path, parameter_contract(cdsl_copy))
|
||||
validate_cdsl(cdsl_copy, engine)
|
||||
# Product revisions are semantic CDSL artifacts. Do not route them
|
||||
@@ -547,7 +706,14 @@ def build_revision(
|
||||
if engine_result.get("engine") != "cdsl_only" or not step_path.is_file() or step_path.stat().st_size == 0:
|
||||
raise RuntimeError("Engine did not produce a CDSL-only STEP artifact")
|
||||
preview = step_to_glb(step_path, glb_path)
|
||||
selector, edges = topology_sidecars(engine_result, preview)
|
||||
snapshot = topology_snapshot(engine_result, task_id=task["task_id"], revision_id=revision_id, preview=preview)
|
||||
write_json(topology_path, snapshot)
|
||||
write_json(selector_bindings_path, selector_bindings(
|
||||
engine_result,
|
||||
node_id=node_id,
|
||||
snapshot_id=str(snapshot.get("snapshot_id") or ""),
|
||||
))
|
||||
selector, edges = topology_sidecars(engine_result, preview, snapshot=snapshot)
|
||||
write_json(selector_path, selector)
|
||||
write_json(edges_path, edges)
|
||||
rules = validate_verification(verification, cdsl_copy)
|
||||
|
||||
Reference in New Issue
Block a user