"""SolidWorks-exact fallback rebuilds and project-specific compensations. ``_run_parameterized`` and ``_run_exact`` replay SolidWorks rebuilds through generated build123d scripts (``compiler_context``). They are the frozen exact/parameterized legs of ``rebuild.run_rebuild``; the production path is ``run_cdsl_only`` in the session runtime. ``_apply_geometric_compensations`` is a project-specific workaround for a feature the SolidWorks export misses. It intentionally lives beside the legacy paths so a clean checkout of this engine in another project can drop it without touching the generic code. """ from __future__ import annotations import sys import time from pathlib import Path from typing import Any from ..sketch_solver import resolve_all_sketches from ..translator import generate_build123d_code 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), } # ═══════════════════════════════════════════════════════════════ # 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