Files
cdsl-cad/backend/engine/cdsl_engine/legacy/llm_compiler.py
T
ganjihong 95cae203f4 refactor(cdsl_engine): freeze legacy engine paths under legacy/
Phase 7 of the decoupling refactor (behavior-preserving move):
- legacy/llm_compiler.py, legacy/llm_engine.py: frozen build_pack path
  (unused by run_cdsl_only), moved with git mv for history
- legacy/exact_rebuild.py: _run_parameterized / _run_exact /
  _apply_geometric_compensations moved out of rebuild.py, including
  the part-specific compensation table
- rebuild.py keeps run_rebuild / run_cdsl_only / compare_with_gold and
  imports the moved functions
- llm_compiler.py / llm_engine.py become compatibility shims

Frozen zone: new capability belongs in executors/ + schema contracts.
The project-specific compensation no longer sits in the main pipeline.
2026-09-09 14:02:53 +08:00

288 lines
12 KiB
Python

"""通用编译器:瘦 CDSL → build_pack;线性阵列在此展开为重复步骤。"""
from __future__ import annotations
import json
from copy import deepcopy
from pathlib import Path
from typing import Any
try:
from ..sketch_solver import resolve_all_sketches
except ImportError:
from sketch_solver import resolve_all_sketches
REQUIRED = {
"revolve_add": ["angle_deg", "axis"],
"revolve_cut": ["angle_deg", "axis"],
"extrude_add_blind": ["distance_mm"],
"extrude_add_two_sided": ["distance_mm"],
"extrude_cut_blind": ["distance_mm"],
"hole_blind": ["diameter_mm", "depth_mm"],
"hole_countersink": ["diameter_mm", "depth_mm"],
"hole_counterbore": ["diameter_mm", "depth_mm"],
"sphere_add": ["radius_mm", "center_mm"],
}
def _load(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def _offset_sketch(sketch: dict[str, Any] | None, dx: float, dy: float, dz: float) -> dict[str, Any] | None:
if sketch is None:
return None
s = deepcopy(sketch)
wp = s.get("workplane") or {}
o = list(wp.get("origin_mm") or [0, 0, 0])
wp["origin_mm"] = [o[0] + dx, o[1] + dy, o[2] + dz]
s["workplane"] = wp
edges = []
for e in s.get("contour_edges_mm") or []:
ne = deepcopy(e)
for key in ("start_mm", "end_mm", "center_mm"):
if key in ne:
p = ne[key]
ne[key] = [p[0] + dx, p[1] + dy, p[2] + dz]
edges.append(ne)
if edges:
s["contour_edges_mm"] = edges
# 2D entities: shift in plane if offset has in-plane components only — skip for world offset patterns
return s
def _offset_params_positions(params: dict[str, Any], dx: float, dy: float, dz: float) -> dict[str, Any]:
p = deepcopy(params)
if "positions" in p:
for pos in p["positions"]:
mm = pos.get("mm")
if mm:
pos["mm"] = [mm[0] + dx, mm[1] + dy, mm[2] + dz]
if "axis" in p and isinstance(p["axis"], dict):
o = list(p["axis"].get("origin_mm") or [0, 0, 0])
p["axis"]["origin_mm"] = [o[0] + dx, o[1] + dy, o[2] + dz]
return p
def compile_cdsl(
cdsl: dict[str, Any],
atoms_catalog: dict[str, Any] | None = None,
techniques_catalog: dict[str, Any] | None = None,
) -> dict[str, Any]:
allowed = set()
if atoms_catalog:
allowed = {a["atomic_id"] for a in atoms_catalog.get("atoms") or []}
techniques = {
item["technique_id"]: item
for item in (techniques_catalog or {}).get("techniques") or []
}
sketches = {s["id"]: s for s in (cdsl.get("geometry") or {}).get("sketches") or []}
# 参数化轮廓求解:将 profile 字段展开为精确的 entities + contour_edges_mm
cdsl = resolve_all_sketches(cdsl)
sketches = {s["id"]: s for s in (cdsl.get("geometry") or {}).get("sketches") or []}
steps: list[dict[str, Any]] = []
seen_ids: set[str] = set()
# feature_id -> list of emitted step dicts (for pattern source)
emitted: dict[str, list[dict[str, Any]]] = {}
def emit(feature: dict[str, Any], params: dict[str, Any], sketch: dict[str, Any] | None, step_id: str) -> dict[str, Any]:
atomic = feature["atomic_id"]
if allowed and atomic not in allowed:
raise ValueError(f"{step_id}: atomic_id {atomic!r} is not admitted by catalog")
for dep in feature.get("depends_on") or []:
if dep not in seen_ids and not any(dep in emitted):
# dependency may be ok if earlier
if dep not in seen_ids:
raise ValueError(f"{step_id}: depends_on {dep} not yet defined")
step = {
"step_id": step_id,
"atomic_id": atomic,
"depends_on": list(feature.get("depends_on") or []),
"params": params,
"sketch": sketch,
"source_name": feature.get("name"),
}
steps.append(step)
seen_ids.add(step_id)
return step
for feat in cdsl.get("features") or []:
fid = feat["id"]
atomic = feat.get("atomic_id")
technique_id = feat.get("technique_id")
if technique_id:
technique = techniques.get(technique_id)
if technique is None:
raise ValueError(f"{fid}: technique_id {technique_id!r} is not admitted by catalog")
groups = feat.get("params") or {}
expanded: list[dict[str, Any]] = []
previous_step_id: str | None = None
for index, internal in enumerate(technique.get("internal_steps") or [], start=1):
group_name = internal.get("params_from")
group = deepcopy(groups.get(group_name) or {})
if not isinstance(group, dict):
raise ValueError(f"{fid}: parameter group {group_name!r} must be an object")
params = deepcopy(group.get("params") if isinstance(group.get("params"), dict) else group)
sketch_id = group.get("sketch_id") or params.pop("sketch_id", None)
sketch = deepcopy(sketches[sketch_id]) if sketch_id and sketch_id in sketches else None
internal_atomic = internal.get("atomic_id")
if not internal_atomic:
raise ValueError(f"{fid}: technique {technique_id!r} has an invalid internal step")
for key in REQUIRED.get(internal_atomic, []):
if params.get(key) is None:
raise ValueError(
f"{fid}: technique {technique_id!r} group {group_name!r} missing {key}"
)
internal_feature = {
"atomic_id": internal_atomic,
"depends_on": [previous_step_id] if previous_step_id else list(feat.get("depends_on") or []),
"name": f"{feat.get('name') or technique_id}:{group_name or index}",
}
step_id = f"{fid}.t{index}"
expanded.append(emit(internal_feature, params, sketch, step_id))
previous_step_id = step_id
if len(expanded) < 2:
raise ValueError(f"{fid}: technique {technique_id!r} must expand to at least two steps")
emitted[fid] = expanded
seen_ids.add(fid)
continue
if not atomic:
raise ValueError(f"{fid}: missing atomic_id")
if atomic == "pattern_linear":
params = feat.get("params") or {}
src_ids = params.get("source_feature_ids") or []
c1 = int(params.get("pattern_count_1") or 1)
c2 = int(params.get("pattern_count_2") or 1)
s1 = float(params.get("spacing_1_mm") or 0)
s2 = float(params.get("spacing_2_mm") or 0)
d1 = params.get("direction_1") or [1, 0, 0]
d2 = params.get("direction_2") or [0, 1, 0]
if params.get("direction_1_reverse"):
d1 = [-d1[0], -d1[1], -d1[2]]
if params.get("direction_2_reverse"):
d2 = [-d2[0], -d2[1], -d2[2]]
src_steps: list[dict[str, Any]] = []
for sid in src_ids:
src_steps.extend(emitted.get(sid) or [])
if not src_steps:
# 无源则跳过并记录
steps.append(
{
"step_id": fid,
"atomic_id": "noop_pattern",
"depends_on": list(feat.get("depends_on") or []),
"params": params,
"sketch": None,
"note": "pattern source steps missing",
}
)
seen_ids.add(fid)
continue
clone_steps = []
k = 0
for i in range(c1):
for j in range(c2):
if i == 0 and j == 0:
continue
dx = d1[0] * s1 * i + d2[0] * s2 * j
dy = d1[1] * s1 * i + d2[1] * s2 * j
dz = d1[2] * s1 * i + d2[2] * s2 * j
for src in src_steps:
k += 1
clone_id = f"{fid}.p{k}"
fake_feat = {
"atomic_id": src["atomic_id"],
"depends_on": [steps[-1]["step_id"]] if steps else [],
"name": f"{src.get('source_name')}_pattern",
}
st = emit(
fake_feat,
_offset_params_positions(src["params"], dx, dy, dz),
_offset_sketch(src.get("sketch"), dx, dy, dz),
clone_id,
)
clone_steps.append(st)
emitted[fid] = clone_steps
seen_ids.add(fid)
continue
params = deepcopy(feat.get("params") or {})
sketch_id = feat.get("sketch_id") or params.get("sketch_id")
sketch = deepcopy(sketches[sketch_id]) if sketch_id and sketch_id in sketches else None
if sketch_id:
params["sketch_id"] = sketch_id
# Auto-derive revolve axis origin
if "revolve" in atomic and sketch and "axis" in params:
ax = params.get("axis") or {}
# 优先级: from_workplane_origin > from_contour_vertex > origin_mm 裸坐标
wp = sketch.get("workplane") or {}
wp_origin = wp.get("origin_mm") or [0.0, 0.0, 0.0]
if ax.get("from_workplane_origin") and "origin_mm" not in ax:
params["axis"] = deepcopy(params["axis"])
params["axis"]["origin_mm"] = list(wp_origin)
elif "origin_mm" not in ax:
ce = sketch.get("contour_edges_mm") or []
if ce:
idx = int(ax.get("from_contour_vertex", 0))
vertex = ce[idx % len(ce)]["start_mm"]
params["axis"] = deepcopy(params["axis"])
params["axis"]["origin_mm"] = list(vertex)
for key in REQUIRED.get(atomic, []):
if key == "axis" and "axis" not in params:
raise ValueError(f"{fid}: missing axis")
if key not in ("axis",) and params.get(key) is None and key != "sketch_id":
# positions can be empty temporarily
if key in params:
continue
if key in ("diameter_mm", "depth_mm", "distance_mm", "angle_deg") and params.get(key) is None:
raise ValueError(f"{fid}: missing {key}")
st = emit(feat, params, sketch, fid)
emitted[fid] = [st]
# filter noop
steps = [s for s in steps if s.get("atomic_id") != "noop_pattern"]
return {
"schema": "cad.engine_plan.v1",
"part_id": cdsl.get("part_id"),
"unit": "mm",
"steps": steps,
"compiler_context": deepcopy(cdsl.get("compiler_context")),
"meta": {
"from_cdsl_schema": cdsl.get("schema"),
"compiler": "cad-heard.llm_compiler.v1",
"n_steps": len(steps),
},
}
def main() -> None:
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--cdsl", type=Path, required=True)
ap.add_argument("--catalog", type=Path, default=None)
ap.add_argument("--techniques", type=Path, default=None)
ap.add_argument("--out", type=Path, required=True)
args = ap.parse_args()
catalog = _load(args.catalog) if args.catalog else None
techniques = _load(args.techniques) if args.techniques else None
pack = compile_cdsl(_load(args.cdsl), catalog, techniques)
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(pack, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"wrote {args.out} steps={len(pack['steps'])}")
if __name__ == "__main__":
main()