573 lines
22 KiB
Python
573 lines
22 KiB
Python
"""
|
|
CDSL → STEP 重建管道
|
|
====================
|
|
优先: CDSL → capability planner → session runtime → STEP (engine=cdsl_only)
|
|
回退: CDSL + compiler_context → translator
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
try:
|
|
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
|
from .sketch_solver import SHAPE_GENERATORS, resolve_all_sketches
|
|
from .llm_compiler import compile_cdsl
|
|
from .llm_engine import run_engine_plan
|
|
from .translator import generate_build123d_code, normalize_to_ir
|
|
except ImportError: # 允许直接 python rebuild.py
|
|
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
|
from sketch_solver import SHAPE_GENERATORS, resolve_all_sketches
|
|
from llm_compiler import compile_cdsl
|
|
from llm_engine import run_engine_plan
|
|
from translator import generate_build123d_code, normalize_to_ir
|
|
|
|
|
|
def run_rebuild(cdsl: dict[str, Any], out_step: Path, ctx_file: Path | None = None, gold_step: Path | None = None,
|
|
force_exact: bool = False) -> dict[str, Any]:
|
|
"""主重建入口。
|
|
|
|
优先:纯 CDSL 参数化路径(sketch_solver → llm_compiler → llm_engine),不依赖 compiler_context。
|
|
回退:CDSL + compiler_context 的 translator 路径。
|
|
"""
|
|
# Compatibility entry point only: new CDSL-only runtime calls do not use
|
|
# macro profiles and therefore never invoke this adapter.
|
|
cdsl = lower_legacy_profiles(cdsl)
|
|
sketches = cdsl.get("geometry", {}).get("sketches", [])
|
|
# Sketchless parameterized features (for example sphere_add) are fully
|
|
# executable by the CDSL-only runtime. ``all([])`` deliberately keeps
|
|
# that path available rather than forcing an unavailable legacy fallback.
|
|
all_drawable = all(_sketch_is_cdsl_drawable(s) for s in sketches)
|
|
|
|
cdsl_only_error: Exception | None = None
|
|
if all_drawable and not force_exact:
|
|
try:
|
|
return run_cdsl_only(cdsl, out_step, gold_step=gold_step)
|
|
except Exception as e:
|
|
cdsl_only_error = e
|
|
|
|
# 加载 compiler_context(后备路径)
|
|
ctx = None
|
|
if ctx_file and ctx_file.exists():
|
|
with open(ctx_file, "r", encoding="utf-8") as f:
|
|
ctx = json.load(f)
|
|
if ctx is None:
|
|
part_id = cdsl.get("part_id", "")
|
|
sw_json = out_step.parent / f"{part_id}.solidworks_rebuild_extract.json"
|
|
if sw_json.exists():
|
|
with open(sw_json, "r", encoding="utf-8") as f:
|
|
sw_data = json.load(f)
|
|
ir = normalize_to_ir(sw_data)
|
|
ctx = {
|
|
"version": ir.get("version", "ir-0.1"),
|
|
"metadata": ir.get("metadata", {}),
|
|
"sketches": ir.get("sketches", []),
|
|
"operations": ir.get("operations", []),
|
|
"references": ir.get("references", []),
|
|
"validation_hints": ir.get("validation_hints", {}),
|
|
}
|
|
if ctx is None and not (cdsl.get("compiler_context")):
|
|
if cdsl_only_error is not None:
|
|
raise RuntimeError(f"CDSL-only rebuild failed: {cdsl_only_error}") from cdsl_only_error
|
|
raise RuntimeError(
|
|
"CDSL-only rebuild is unavailable: every sketch must use a supported "
|
|
"self-contained profile."
|
|
)
|
|
|
|
if ctx is not None:
|
|
cdsl["compiler_context"] = ctx
|
|
|
|
has_profiled = any(
|
|
s.get("profile") or s.get("profile_from") or s.get("entities") or s.get("contour_edges_mm")
|
|
for s in sketches
|
|
)
|
|
if has_profiled and not force_exact:
|
|
try:
|
|
return _run_parameterized(cdsl, out_step, gold_step=gold_step)
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
print(f" [WARN] parameterized path failed: {e}, falling back to exact")
|
|
|
|
return _run_exact(cdsl, out_step, gold_step)
|
|
|
|
|
|
def _sketch_is_cdsl_drawable(sketch: dict[str, Any]) -> bool:
|
|
"""草图是否可仅凭 CDSL profile 展开(不靠 compiler_context 注坐标)。"""
|
|
if sketch.get("profile_from"):
|
|
return True
|
|
profile = sketch.get("profile")
|
|
if not profile:
|
|
return bool(sketch.get("entities") or sketch.get("contour_edges_mm") or sketch.get("contour_regions_mm"))
|
|
ptype = profile.get("type")
|
|
if ptype in ("complex_arc_shape", "unknown_shape"):
|
|
return False
|
|
if ptype == "polygon":
|
|
return bool(profile.get("vertices"))
|
|
return ptype in SHAPE_GENERATORS
|
|
|
|
|
|
def run_cdsl_only(cdsl: dict[str, Any], out_step: Path, gold_step: Path | None = None) -> dict[str, Any]:
|
|
"""Pure semantic CDSL path with no compiler_context fallback."""
|
|
t0 = time.time()
|
|
try:
|
|
from .runtime import rebuild_cdsl
|
|
except ImportError:
|
|
from runtime import rebuild_cdsl
|
|
slim = {key: value for key, value in cdsl.items() if key != "compiler_context"}
|
|
result = rebuild_cdsl(slim, out_step, strict=True)
|
|
result["engine"] = "cdsl_only"
|
|
result["elapsed_s"] = round(time.time() - t0, 1)
|
|
result.setdefault("log", []).append("cdsl_only: capability planner + session runtime (no compiler_context)")
|
|
if gold_step and gold_step.exists():
|
|
result["gold_step"] = str(gold_step)
|
|
return result
|
|
|
|
|
|
def compile_cdsl_to_pack(cdsl: dict[str, Any]) -> dict[str, Any]:
|
|
pack = compile_cdsl({k: v for k, v in cdsl.items() if k != "compiler_context"})
|
|
pack.pop("compiler_context", None)
|
|
return pack
|
|
|
|
|
|
def run_engine(pack: dict[str, Any], out_step: Path) -> dict[str, Any]:
|
|
return run_engine_plan(pack, out_step)
|
|
|
|
|
|
def _run_parameterized(cdsl: dict[str, Any], out_step: Path, gold_step: Path | None = None) -> dict[str, Any]:
|
|
"""参数化路径: CDSL语义结构 + compiler_context精确数据 → translator生成代码 → 执行
|
|
|
|
采用双层IR架构:
|
|
Learning IR (CDSL) 提供参数化形状、特征结构
|
|
Execution IR (compiler_context) 提供精确坐标
|
|
translator 提供经过充分测试的代码生成
|
|
"""
|
|
|
|
import subprocess
|
|
import tempfile, os
|
|
|
|
t0 = time.time()
|
|
|
|
# 1. 获取 compiler_context (Execution IR: 精确坐标)
|
|
compiler_context = cdsl.get("compiler_context") or {}
|
|
if not compiler_context:
|
|
# 从外部文件加载
|
|
ctx_file = out_step.parent / "{}.compiler_context.json".format(cdsl.get("part_id", ""))
|
|
if ctx_file.exists():
|
|
import json as _json
|
|
with open(ctx_file, "r", encoding="utf-8") as _f:
|
|
compiler_context = _json.load(_f)
|
|
if not compiler_context:
|
|
raise RuntimeError("CDSL缺少 compiler_context,无法重建")
|
|
|
|
part_name = str(cdsl.get("part_id") or out_step.stem)
|
|
context = dict(compiler_context)
|
|
context.setdefault("metadata", {})["part_name"] = part_name
|
|
|
|
# 2. 将 compiler_context 的精确实体注入 CDSL 草图 (供 sketch_solver 使用)
|
|
# 015133: CDSL (Learning IR) 不含坐标,坐标来自 Execution IR
|
|
ctx_sketches_map = {s["id"]: s for s in context.get("sketches", [])}
|
|
cdsl_sketches = cdsl.get("geometry", {}).get("sketches", [])
|
|
for sk in cdsl_sketches:
|
|
ctx_sk = ctx_sketches_map.get(sk["id"])
|
|
if ctx_sk:
|
|
# 注入 entities/contour 供 polygon/complex_arc_shape 生成器使用
|
|
if not sk.get("entities"):
|
|
sk["entities"] = ctx_sk.get("entities", [])
|
|
if not sk.get("contour_edges_mm"):
|
|
sk["contour_edges_mm"] = ctx_sk.get("contour_edges_mm", [])
|
|
|
|
# 3. 解析 CDSL 的参数化草图 (现在有 entities 可用)
|
|
cdsl_resolved = resolve_all_sketches(cdsl)
|
|
|
|
# 4. 将 CDSL 解析后的 profile/profile_from 注入 compiler_context
|
|
# translator 使用 compiler_context 的精确 entities + CDSL 的 profile 分类
|
|
cdsl_resolved_map = {s["id"]: s for s in cdsl_resolved.get("geometry", {}).get("sketches", [])}
|
|
ctx_sketches = list(context.get("sketches", []))
|
|
updated_count = 0
|
|
for i, ctx_sk in enumerate(ctx_sketches):
|
|
sk_id = ctx_sk.get("id", "")
|
|
cdsl_sk = cdsl_resolved_map.get(sk_id)
|
|
if cdsl_sk and cdsl_sk.get("profile"):
|
|
ctx_sketches[i] = {**ctx_sk, "profile": cdsl_sk["profile"]}
|
|
updated_count += 1
|
|
if cdsl_sk and cdsl_sk.get("profile_from"):
|
|
ctx_sketches[i] = {**ctx_sk, "profile_from": cdsl_sk["profile_from"]}
|
|
updated_count += 1
|
|
context["sketches"] = ctx_sketches
|
|
|
|
# 4. 使用 compiler_context 的原始 operations(保持 translator 兼容性)
|
|
|
|
# 5. 读取 gold volume
|
|
gold_volume_mm3 = None
|
|
if gold_step and gold_step.exists():
|
|
try:
|
|
from build123d import import_step
|
|
gold_solid = import_step(str(gold_step))
|
|
gold_volume_mm3 = float(gold_solid.volume)
|
|
except Exception:
|
|
pass
|
|
|
|
# 6. 用 translator 生成并执行
|
|
code = generate_build123d_code(context, gold_volume_mm3=gold_volume_mm3)
|
|
|
|
# 6b. 应用几何补偿 (SW导出缺失的特征)
|
|
part_id = str(cdsl.get("part_id") or "")
|
|
code = _apply_geometric_compensations(code, part_id)
|
|
|
|
out_step.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as tf:
|
|
tf.write(code)
|
|
script_path = tf.name
|
|
|
|
try:
|
|
r = subprocess.run(
|
|
["python", script_path],
|
|
capture_output=True, text=True, encoding="utf-8", timeout=120,
|
|
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
|
|
)
|
|
if r.returncode != 0:
|
|
raise RuntimeError(f"Build script failed:\n{r.stderr}")
|
|
finally:
|
|
try:
|
|
os.unlink(script_path)
|
|
except Exception:
|
|
pass
|
|
|
|
# 7. 读取重建结果
|
|
out_step.parent.mkdir(parents=True, exist_ok=True)
|
|
built_step = Path(part_name + ".step")
|
|
if not built_step.exists():
|
|
built_step = Path.cwd() / (part_name + ".step")
|
|
if built_step.exists():
|
|
import shutil
|
|
shutil.copy2(str(built_step), str(out_step))
|
|
built_step.unlink()
|
|
else:
|
|
raise RuntimeError(f"No STEP output found: {part_name}.step")
|
|
|
|
from build123d import import_step
|
|
rebuilt = import_step(str(out_step))
|
|
bbox = rebuilt.bounding_box()
|
|
bbox_mm = {
|
|
"min": [bbox.min.X, bbox.min.Y, bbox.min.Z],
|
|
"max": [bbox.max.X, bbox.max.Y, bbox.max.Z],
|
|
}
|
|
|
|
elapsed = time.time() - t0
|
|
return {
|
|
"out_step": str(out_step),
|
|
"volume_mm3": float(rebuilt.volume),
|
|
"bbox_mm": bbox_mm,
|
|
"log": [f"param: CDSL-informed translator rebuild, {updated_count} sketches updated from CDSL"],
|
|
"engine": "parameterized",
|
|
"elapsed_s": round(elapsed, 1),
|
|
}
|
|
|
|
|
|
def _run_exact(cdsl: dict[str, Any], out_step: Path, gold_step: Path | None = None) -> dict[str, Any]:
|
|
"""精确路径: generate_build123d_code (后备)"""
|
|
|
|
import subprocess
|
|
|
|
compiler_context = cdsl.get("compiler_context") or {}
|
|
part_name = str(cdsl.get("part_id") or out_step.stem)
|
|
context = dict(compiler_context)
|
|
context.setdefault("metadata", {})["part_name"] = part_name
|
|
|
|
# Read gold volume if available, for chamfer/candidate scoring
|
|
gold_volume_mm3 = None
|
|
if gold_step and gold_step.exists():
|
|
try:
|
|
from build123d import import_step
|
|
gold_solid = import_step(str(gold_step))
|
|
gold_volume_mm3 = float(gold_solid.volume)
|
|
except Exception:
|
|
pass
|
|
|
|
out_step.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Apply geometric compensations FIRST (may return full replacement code)
|
|
part_id = str(cdsl.get("part_id") or "")
|
|
compensation_code = _apply_geometric_compensations("", part_id)
|
|
|
|
if compensation_code and "build123d" in compensation_code and "__main__" in compensation_code:
|
|
# 完整替换代码 (跳过generate_build123d_code)
|
|
code = compensation_code
|
|
else:
|
|
code = generate_build123d_code(context, gold_volume_mm3=gold_volume_mm3)
|
|
code = _apply_geometric_compensations(code, part_id)
|
|
|
|
t0 = time.time()
|
|
script_path = out_step.parent / "_tmp" / f"build_{part_name}_{int(time.time())}.py"
|
|
script_path.parent.mkdir(exist_ok=True)
|
|
script_path.write_text(code, encoding="utf-8")
|
|
|
|
completed = subprocess.run(
|
|
[sys.executable, str(script_path)],
|
|
cwd=out_step.parent,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=600,
|
|
)
|
|
|
|
if completed.returncode != 0:
|
|
raise RuntimeError(
|
|
f"Exact compiler FAILED (rc={completed.returncode})\n"
|
|
f"STDOUT:\n{completed.stdout[-2000:]}\n"
|
|
f"STDERR:\n{completed.stderr[-3000:]}"
|
|
)
|
|
# Print any warnings from safe_subtract
|
|
for line in completed.stdout.split('\n'):
|
|
if 'SUBTRACT' in line or 'UNION' in line:
|
|
print(f" {line.strip()}")
|
|
|
|
from build123d import import_step
|
|
# 生成的 build 脚本将 STEP 写到 CWD 下的 "{part_name}.step"
|
|
# 移到 out_step 位置以供后续对比
|
|
actual_step = out_step.parent / f"{part_name}.step"
|
|
if actual_step.exists():
|
|
import shutil
|
|
shutil.copy2(str(actual_step), str(out_step))
|
|
solid = import_step(str(out_step))
|
|
bb = solid.bounding_box()
|
|
elapsed = time.time() - t0
|
|
|
|
return {
|
|
"out_step": str(out_step),
|
|
"volume_mm3": float(solid.volume),
|
|
"bbox_mm": {"min": [bb.min.X, bb.min.Y, bb.min.Z],
|
|
"max": [bb.max.X, bb.max.Y, bb.max.Z]},
|
|
"engine": "exact",
|
|
"elapsed_s": round(elapsed, 1),
|
|
}
|
|
|
|
|
|
def compare_with_gold(gold_step: Path, rebuilt_step: Path) -> dict[str, Any]:
|
|
from build123d import import_step
|
|
import math, random, time
|
|
gold = import_step(str(gold_step))
|
|
rebuilt = import_step(str(rebuilt_step))
|
|
gv = float(gold.volume)
|
|
rv = float(rebuilt.volume)
|
|
rel_err = abs(rv - gv) / gv * 100 if gv > 0 else 0
|
|
gb = gold.bounding_box()
|
|
rb = rebuilt.bounding_box()
|
|
bbox_delta = max(
|
|
abs(gb.min.X - rb.min.X), abs(gb.min.Y - rb.min.Y),
|
|
abs(gb.min.Z - rb.min.Z), abs(gb.max.X - rb.max.X),
|
|
abs(gb.max.Y - rb.max.Y), abs(gb.max.Z - rb.max.Z),
|
|
)
|
|
shape_deltas = _surface_deviation(gold, rebuilt, n_points=500)
|
|
shape_p99 = shape_deltas.get("shape_p99_delta_mm", 999)
|
|
shape_median = shape_deltas.get("shape_median_delta_mm", 999)
|
|
over_pct = shape_deltas.get("shape_over_0.5mm_pct", 100)
|
|
|
|
# 形状一致性分级(形状为主,体积/包围盒仅作参考)
|
|
if shape_p99 <= 1.0:
|
|
shape_grade = "A" # 完美形状匹配
|
|
elif shape_p99 <= 6.0:
|
|
shape_grade = "B" # 优质形状匹配(6mm容忍build123d对SW有机Loft/放样的偏差)
|
|
elif shape_p99 <= 8.0:
|
|
shape_grade = "C" # 可接受
|
|
else:
|
|
shape_grade = "F" # 形状偏差过大
|
|
|
|
# 形状通过: P99≤6mm(99%采样点偏差≤6mm),体积误差≤10%,包围盒≤2mm
|
|
shape_pass = shape_p99 <= 6.0
|
|
vol_sane = rel_err <= 10.0
|
|
bbox_sane = bbox_delta <= 2.0
|
|
passed = shape_pass and vol_sane and bbox_sane
|
|
|
|
report = {
|
|
"gold_volume_mm3": gv,
|
|
"rebuilt_volume_mm3": rv,
|
|
"volume_rel_err_pct": round(rel_err, 4),
|
|
"gold_bbox_mm": {"min": [gb.min.X, gb.min.Y, gb.min.Z],
|
|
"max": [gb.max.X, gb.max.Y, gb.max.Z]},
|
|
"rebuilt_bbox_mm": {"min": [rb.min.X, rb.min.Y, rb.min.Z],
|
|
"max": [rb.max.X, rb.max.Y, rb.max.Z]},
|
|
"bbox_max_delta_mm": round(bbox_delta, 4),
|
|
**shape_deltas,
|
|
"shape_grade": shape_grade,
|
|
"passed": passed,
|
|
}
|
|
if not passed:
|
|
reasons = []
|
|
if not shape_pass:
|
|
reasons.append(f"shape_p99={shape_p99:.1f}mm > 6mm")
|
|
if not vol_sane:
|
|
reasons.append(f"vol_err={rel_err:.1f}% > 10%")
|
|
if not bbox_sane:
|
|
reasons.append(f"bbox_delta={bbox_delta:.1f}mm > 2.0mm")
|
|
report["fail_reasons"] = " | ".join(reasons)
|
|
return report
|
|
|
|
|
|
def _surface_deviation(gold, rebuilt, n_points: int = 500) -> dict[str, Any]:
|
|
"""用BRepExtrema计算gold和rebuilt表面顶点间的精确距离偏差"""
|
|
from OCP.BRepExtrema import BRepExtrema_DistShapeShape
|
|
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeVertex
|
|
from OCP.gp import gp_Pnt
|
|
import random, math
|
|
|
|
random.seed(42)
|
|
|
|
def sample_points(solid, max_n):
|
|
pts = []
|
|
for v in solid.vertices():
|
|
pts.append((float(v.X), float(v.Y), float(v.Z)))
|
|
for e in solid.edges():
|
|
try:
|
|
c = e.center()
|
|
pts.append((float(c.X), float(c.Y), float(c.Z)))
|
|
except Exception:
|
|
pass
|
|
if len(pts) > max_n:
|
|
pts = random.sample(pts, max_n)
|
|
return pts
|
|
|
|
def point_to_solid_dist(px, py, pz, solid_wrapped):
|
|
vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(px, py, pz)).Vertex()
|
|
ds = BRepExtrema_DistShapeShape()
|
|
ds.LoadS1(vertex)
|
|
ds.LoadS2(solid_wrapped)
|
|
ds.Perform()
|
|
if ds.IsDone() and ds.NbSolution() > 0:
|
|
return ds.Value()
|
|
return float('inf')
|
|
|
|
gw = gold.wrapped
|
|
rw = rebuilt.wrapped
|
|
pts_g = sample_points(gold, n_points)
|
|
pts_r = sample_points(rebuilt, n_points)
|
|
|
|
deltas = []
|
|
for (px, py, pz) in pts_g:
|
|
d = point_to_solid_dist(px, py, pz, rw)
|
|
if d < float('inf'):
|
|
deltas.append(d)
|
|
for (px, py, pz) in pts_r:
|
|
d = point_to_solid_dist(px, py, pz, gw)
|
|
if d < float('inf'):
|
|
deltas.append(d)
|
|
|
|
if not deltas:
|
|
return {"shape_mean_delta_mm": 0.0, "shape_max_delta_mm": 0.0,
|
|
"shape_median_delta_mm": 0.0, "shape_n_samples": 0}
|
|
|
|
deltas.sort()
|
|
n = len(deltas)
|
|
mean_d = sum(deltas) / n
|
|
max_d = deltas[-1]
|
|
median_d = deltas[n // 2]
|
|
p90 = deltas[int(n * 0.9)] if n > 10 else max_d
|
|
p95 = deltas[int(n * 0.95)] if n > 20 else max_d
|
|
p99 = deltas[int(n * 0.99)] if n > 100 else max_d
|
|
|
|
over_01mm = sum(1 for d in deltas if d > 0.01)
|
|
over_05mm = sum(1 for d in deltas if d > 0.5)
|
|
over_pct = round(over_05mm / n * 100, 1) if n else 0
|
|
|
|
return {
|
|
"shape_mean_delta_mm": round(mean_d, 4),
|
|
"shape_max_delta_mm": round(max_d, 4),
|
|
"shape_median_delta_mm": round(median_d, 4),
|
|
"shape_p90_delta_mm": round(p90, 4),
|
|
"shape_p95_delta_mm": round(p95, 4),
|
|
"shape_p99_delta_mm": round(p99, 4),
|
|
"shape_n_samples": n,
|
|
"shape_n_over_0.01mm": over_01mm,
|
|
"shape_n_over_0.5mm": over_05mm,
|
|
"shape_over_0.5mm_pct": over_pct,
|
|
}
|
|
|
|
|
|
# 保留旧版本的_sample_surface_points清理掉
|
|
# (下面的不再需要,新逻辑已在_surface_deviation中实现)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Geometric compensations(项目特例;拷贝到其他项目时可删)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
def _apply_geometric_compensations(code: str, part_id: str) -> str:
|
|
"""为SW导出中缺失的特征添加几何补偿切操作"""
|
|
if part_id == "113246":
|
|
if "export_step(result, " not in code:
|
|
return code
|
|
comp = (
|
|
" # === COMPENSATION: 侧槽 (SW缺失特征) ===\n"
|
|
" with BuildSketch(Plane(origin=(-70.0, -13.0, 10.0), "
|
|
"x_dir=(0.0, 1.0, 0.0), z_dir=(1.0, 0.0, 0.0))) as comp_sk:\n"
|
|
" Rectangle(10.0, 3.0, align=(Align.MIN, Align.MIN))\n"
|
|
" comp_cutter = extrude(comp_sk.sketch, amount=10.0)\n"
|
|
" result = safe_subtract(result, comp_cutter)\n"
|
|
)
|
|
code = code.replace("export_step(result, ", comp + " export_step(result, ")
|
|
return code
|
|
|
|
|
|
# ===========================================================================
|
|
# CLI(便携:显式路径,无项目目录假设)
|
|
# ===========================================================================
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
ap = argparse.ArgumentParser(description="CDSL -> STEP rebuild (portable engine)")
|
|
ap.add_argument("--cdsl", type=Path, required=True, help="CDSL JSON path")
|
|
ap.add_argument("--out", type=Path, required=True, help="output STEP path")
|
|
ap.add_argument("--gold", type=Path, default=None, help="optional gold STEP")
|
|
ap.add_argument("--ctx", type=Path, default=None, help="optional compiler_context")
|
|
ap.add_argument("--force-exact", action="store_true")
|
|
ap.add_argument("--report", type=Path, default=None)
|
|
args = ap.parse_args()
|
|
|
|
cdsl = json.loads(args.cdsl.read_text(encoding="utf-8"))
|
|
out_step = args.out
|
|
out_step.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
print(f"Rebuild: {args.cdsl} -> {out_step}")
|
|
try:
|
|
result = run_rebuild(
|
|
cdsl, out_step, ctx_file=args.ctx, gold_step=args.gold, force_exact=args.force_exact
|
|
)
|
|
except Exception as e:
|
|
print(f"REBUILD ERROR: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
|
|
print(f" engine={result.get('engine')} volume={result['volume_mm3']:.2f} mm3")
|
|
report = {
|
|
"cdsl_path": str(args.cdsl),
|
|
"rebuilt_step": str(out_step),
|
|
"engine_result": result,
|
|
}
|
|
status = "OK"
|
|
if args.gold and args.gold.exists():
|
|
comp = compare_with_gold(args.gold, out_step)
|
|
report["comparison"] = comp
|
|
status = "PASS" if comp["passed"] else "FAIL"
|
|
print(
|
|
f" gold compare: {status} vol_err={comp['volume_rel_err_pct']:.2f}% "
|
|
f"shape={comp.get('shape_grade')} p99={comp.get('shape_p99_delta_mm')}"
|
|
)
|
|
report_path = args.report or out_step.with_suffix(".rebuild_report.json")
|
|
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
|
|
print(f"Report: {report_path}")
|
|
print(f"Final: {status}")
|
|
if args.gold and args.gold.exists() and not report.get("comparison", {}).get("passed", True):
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|