Import CAD projects and cad-router v1

This commit is contained in:
Jerry
2026-07-22 19:38:36 +08:00
parent e4e0199bd5
commit af9f630c11
2253 changed files with 1994207 additions and 0 deletions
@@ -0,0 +1,107 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
function usage() {
return "Usage: node cadam_compile.mjs <input.scad> <output.stl|output.dxf> [--cadam-root <path>]";
}
function parseArgs(argv) {
const positional = [];
let cadamRoot = String(process.env.CADAM_ROOT || "").trim();
for (let index = 0; index < argv.length; index += 1) {
const value = argv[index];
if (value === "--cadam-root") {
cadamRoot = String(argv[index + 1] || "").trim();
index += 1;
} else {
positional.push(value);
}
}
if (positional.length !== 2) {
throw new Error(usage());
}
return { inputPath: path.resolve(positional[0]), outputPath: path.resolve(positional[1]), cadamRoot };
}
function defaultCadamRoot() {
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
return path.resolve(scriptDirectory, "../../../../CADAM");
}
function runtimePaths(configuredRoot) {
const root = path.resolve(configuredRoot || defaultCadamRoot());
const runtimeDirectory = path.join(root, "src", "vendor", "openscad-wasm");
const modulePath = path.join(runtimeDirectory, "openscad.js");
const wasmPath = path.join(runtimeDirectory, "openscad.wasm");
if (!fs.existsSync(modulePath) || !fs.existsSync(wasmPath)) {
throw new Error(
`CADAM OpenSCAD WASM runtime not found under ${runtimeDirectory}. ` +
"Set CADAM_ROOT or pass --cadam-root to an installed CADAM checkout."
);
}
return { root, modulePath, wasmPath };
}
async function compile({ inputPath, outputPath, cadamRoot }) {
if (path.extname(inputPath).toLowerCase() !== ".scad") {
throw new Error("CADAM input must be an OpenSCAD .scad source file");
}
const extension = path.extname(outputPath).toLowerCase();
if (!new Set([".stl", ".dxf"]).has(extension)) {
throw new Error("CADAM output must be .stl or .dxf");
}
if (!fs.existsSync(inputPath) || !fs.statSync(inputPath).isFile()) {
throw new Error(`CADAM input file not found: ${inputPath}`);
}
const runtime = runtimePaths(cadamRoot);
const logs = { stdout: [], stderr: [] };
const runtimeModule = await import(pathToFileURL(runtime.modulePath).href);
const instance = await runtimeModule.default({
noInitialRun: true,
wasmBinary: fs.readFileSync(runtime.wasmPath),
print: (line) => logs.stdout.push(String(line)),
printErr: (line) => logs.stderr.push(String(line))
});
const virtualInput = "/input.scad";
const virtualOutput = `/out${extension}`;
instance.FS.writeFile(virtualInput, fs.readFileSync(inputPath, "utf8"));
const format = extension === ".stl" ? "binstl" : "dxf";
const exitCode = instance.callMain([
virtualInput,
"-o",
virtualOutput,
`--export-format=${format}`,
"--backend=manifold",
"--enable=lazy-union"
]);
if (exitCode !== 0) {
throw new Error(`CADAM OpenSCAD compilation failed with exit code ${exitCode}: ${logs.stderr.join("\n")}`);
}
const output = instance.FS.readFile(virtualOutput, { encoding: "binary" });
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, output);
return {
backend: "cadam",
runtime: "CADAM OpenSCAD WASM",
cadam_root: runtime.root,
input: inputPath,
output: outputPath,
bytes: output.length,
exit_code: exitCode,
stderr_tail: logs.stderr.slice(-8)
};
}
try {
const result = await compile(parseArgs(process.argv.slice(2)));
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}
@@ -0,0 +1,56 @@
{
"schema_version": "1.0",
"backends": {
"build123d": {
"project": "text-to-cad",
"runner_skill": "cad",
"source_formats": ["py"],
"primary_formats": ["step", "stp"],
"secondary_formats": ["stl", "3mf", "glb"],
"strengths": [
"general mechanical parts",
"machined features",
"source-level assemblies",
"existing STEP inspection and modification",
"geometry measurement and validation"
],
"source_of_truth": "build123d Python generator"
},
"simplecadapi": {
"project": "SimpleCADAPI",
"runner_skill": "simplecadapi",
"source_formats": ["py", "json"],
"primary_formats": ["step", "stp", "fcstd"],
"secondary_formats": ["stl", "json"],
"strengths": [
"gears, ring gears, racks, and cycloidal discs",
"bearings, reducers, and joint actuators",
"replayable operation graphs",
"semantic tags and topology queries",
"FreeCAD interchange"
],
"source_of_truth": "SimpleCADAPI Python generator and model JSON"
},
"cadam": {
"project": "CADAM",
"runner_skill": "cad-router",
"adapter": "scripts/cadam_compile.mjs",
"source_formats": ["scad"],
"primary_formats": ["scad", "stl"],
"secondary_formats": ["dxf"],
"strengths": [
"fast printable CSG models",
"patterns, lattices, and decorative geometry",
"printable threads and BOSL/BOSL2 libraries",
"browser-friendly parameter controls"
],
"source_of_truth": "CADAM-compatible OpenSCAD source"
}
},
"workflow_projects": {
"CADDesigner-Code": {
"profiles": ["caddesigner_ecip", "requirement_refinement", "visual_repair"],
"role": "Explicit context, requirement expansion, execution feedback, and bounded visual repair without launching a second LLM agent"
}
}
}
+283
View File
@@ -0,0 +1,283 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parents[1]
REGISTRY_PATH = Path(__file__).with_name("capabilities.json")
CADAM_RUNTIME_FILES = (
Path("src/vendor/openscad-wasm/openscad.js"),
Path("src/vendor/openscad-wasm/openscad.wasm"),
)
@dataclass
class BackendScore:
name: str
score: int
reasons: list[str] = field(default_factory=list)
def add(self, points: int, reason: str) -> None:
self.score += points
self.reasons.append(f"{points:+d} {reason}")
KEYWORD_GROUPS: dict[str, tuple[str, ...]] = {
"special_mechanical": (
"gear", "gears", "齿轮", "齿条", "rack", "ring gear", "内齿圈",
"cycloidal", "摆线", "bearing", "轴承", "planetary", "行星",
"reducer", "gearbox", "减速器", "减速箱", "joint actuator", "关节执行器",
),
"general_mechanical": (
"bracket", "支架", "connecting rod", "连杆", "crank", "曲柄",
"lever", "摇臂", "shaft", "轴", "bushing", "轴套", "flange", "法兰",
"housing", "壳体", "enclosure", "外壳", "fixture", "夹具", "plate", "安装板",
),
"printable": (
"3d print", "3d-print", "打印", "printable", "vase", "花瓶", "knob", "旋钮",
"container", "容器", "adapter", "适配器", "honeycomb", "蜂窝", "lattice", "晶格",
"pattern", "阵列", "decorative", "装饰", "openscad", "scad", "bosl",
),
"replay": (
"replay", "重放", "operation graph", "model json", "模型图", "semantic tag",
"语义标签", "freecad", "fcstd",
),
"visual_complexity": (
"image", "photo", "sketch", "图片", "照片", "草图", "复杂", "complex",
"像", "参考图", "reference",
),
}
FOCUSED_SKILLS: tuple[tuple[str, tuple[str, ...]], ...] = (
("dxf", ("dxf", "laser cut", "激光切割", "waterjet", "水刀", "flat pattern", "展开")),
("urdf", ("urdf", "robot description", "机器人描述")),
("srdf", ("srdf", "moveit", "planning group", "规划组")),
("sdf", ("sdformat", "gazebo", "simulation world", "仿真世界", ".sdf")),
("gcode", ("g-code", "gcode", "slice", "切片")),
("sendcutsend", ("sendcutsend",)),
("bambu-labs", ("bambu", "拓竹")),
)
def contains_any(text: str, terms: tuple[str, ...]) -> bool:
return any(term in text for term in terms)
def normalize_formats(formats: list[str]) -> list[str]:
result: list[str] = []
for value in formats:
normalized = value.lower().lstrip(".")
if normalized not in result:
result.append(normalized)
return result
def cadam_root() -> Path:
configured = os.environ.get("CADAM_ROOT", "").strip()
if configured:
return Path(configured).expanduser().resolve()
return (SKILL_ROOT.parents[2] / "CADAM").resolve()
def cadam_runtime_available(root: Path | None = None) -> bool:
candidate = root or cadam_root()
return all((candidate / relative_path).is_file() for relative_path in CADAM_RUNTIME_FILES)
def probe_backends() -> dict[str, dict[str, object]]:
return {
"build123d": {
"available": importlib.util.find_spec("build123d") is not None,
"probe": f"python={sys.executable}",
},
"simplecadapi": {
"available": importlib.util.find_spec("simplecadapi") is not None,
"probe": f"python={sys.executable}",
},
"cadam": {
"available": cadam_runtime_available(),
"probe": str(cadam_root()),
},
}
def route(args: argparse.Namespace) -> dict[str, object]:
request = args.request.strip()
text = request.lower()
outputs = normalize_formats(args.output)
scores = {
"build123d": BackendScore("build123d", 35, ["+35 general STEP-first default"]),
"simplecadapi": BackendScore("simplecadapi", 20, ["+20 specialist mechanical baseline"]),
"cadam": BackendScore("cadam", 10, ["+10 CADAM printable CSG baseline"]),
}
if contains_any(text, KEYWORD_GROUPS["special_mechanical"]):
scores["simplecadapi"].add(65, "supported standard mechanical family")
scores["build123d"].add(8, "can model the family generically")
if contains_any(text, KEYWORD_GROUPS["general_mechanical"]):
scores["build123d"].add(35, "general mechanical part with direct parametric features")
scores["simplecadapi"].add(10, "general OCP modeling is available")
if contains_any(text, KEYWORD_GROUPS["printable"]):
scores["cadam"].add(55, "printable, pattern-heavy, or SCAD-native request")
if contains_any(text, KEYWORD_GROUPS["replay"]):
scores["simplecadapi"].add(60, "replay, semantic graph, or FreeCAD requirement")
if args.existing_model:
scores["build123d"].add(55, "existing model inspection or source modification")
scores["cadam"].add(-50, "poor fit for imported editable STEP")
if args.assembly:
scores["build123d"].add(15, "source-level assembly workflow")
scores["simplecadapi"].add(18, "mechanism and constraint workflow")
scores["cadam"].add(-15, "weak fit for engineering assembly relationships")
if args.browser_controls:
scores["cadam"].add(45, "CADAM-style parameter controls requested")
if args.manufacturing == "machining":
scores["build123d"].add(25, "machining favors STEP-first B-Rep")
scores["simplecadapi"].add(18, "OCP STEP geometry suits machining")
scores["cadam"].add(-25, "mesh/CSG route is not preferred for machining")
elif args.manufacturing == "printing":
scores["cadam"].add(30, "printing favors fast parametric CSG")
scores["build123d"].add(8, "can export validated printable meshes")
elif args.manufacturing == "laser-cutting":
scores["build123d"].add(12, "can own projected source geometry")
if any(fmt in {"step", "stp"} for fmt in outputs):
scores["build123d"].add(35, "editable STEP requested")
scores["simplecadapi"].add(30, "OCP STEP export requested")
scores["cadam"].add(-70, "no native editable STEP output")
if "fcstd" in outputs:
scores["simplecadapi"].add(70, "FreeCAD output requested")
scores["cadam"].add(-40, "FreeCAD output is unsupported")
if "scad" in outputs:
scores["cadam"].add(80, "SCAD source requested")
if any(fmt in {"stl", "3mf"} for fmt in outputs):
scores["cadam"].add(12, "print mesh requested")
scores["build123d"].add(5, "secondary mesh export is available")
forced_backend = args.backend if args.backend != "auto" else None
ranked = sorted(scores.values(), key=lambda item: (-item.score, item.name))
if forced_backend:
selected = scores[forced_backend]
selected.reasons.append("explicit backend override")
fallback = [item.name for item in ranked if item.name != forced_backend]
else:
selected = ranked[0]
fallback = [item.name for item in ranked[1:]]
profiles: list[str] = ["caddesigner_ecip"]
if contains_any(text, KEYWORD_GROUPS["visual_complexity"]):
profiles.extend(["requirement_refinement", "visual_repair"])
if not any(char.isdigit() for char in request) and contains_any(
text,
KEYWORD_GROUPS["general_mechanical"] + KEYWORD_GROUPS["special_mechanical"],
):
profiles.append("requirement_refinement")
profiles = list(dict.fromkeys(profiles))
downstream = [name for name, terms in FOCUSED_SKILLS if contains_any(text, terms)]
if args.manufacturing == "laser-cutting" and "dxf" not in downstream:
downstream.append("dxf")
if any(fmt in {"stl", "3mf", "gcode"} for fmt in outputs) and "cad-viewer" not in downstream:
downstream.append("cad-viewer")
elif selected.name in {"build123d", "simplecadapi"}:
downstream.append("cad-viewer")
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
backend_info = registry["backends"][selected.name]
gap = selected.score - max((scores[name].score for name in scores if name != selected.name), default=0)
confidence = "high" if gap >= 40 else "medium" if gap >= 15 else "low"
result: dict[str, object] = {
"schema_version": "1.0",
"request": request,
"selected_backend": selected.name,
"runner_skill": backend_info["runner_skill"],
"project": backend_info["project"],
"adapter": backend_info.get("adapter"),
"confidence": confidence,
"backend_scores": [
{"backend": item.name, "score": item.score, "reasons": item.reasons}
for item in sorted(scores.values(), key=lambda item: (-item.score, item.name))
],
"fallback_order": fallback,
"workflow_profiles": profiles,
"downstream_skills": downstream,
"requested_outputs": outputs,
"source_of_truth": backend_info["source_of_truth"],
"project_contributions": {
"text-to-cad": "core Skill host, build123d CAD backend, validation, artifacts, and CAD Viewer handoff",
"SimpleCADAPI": "specialized mechanical backend with replayable graphs and semantic modeling",
"CADAM": "OpenSCAD-WASM execution for printable and pattern-heavy SCAD models",
"CADDesigner-Code": "ECIP brief expansion, execution feedback, and bounded visual repair workflow",
},
"availability": probe_backends() if args.probe else None,
}
return result
def explain(result: dict[str, object]) -> str:
lines = [
f"Selected backend: {result['selected_backend']}",
f"Runner skill: ${result['runner_skill']}",
f"Project: {result['project']}",
f"Confidence: {result['confidence']}",
f"Source of truth: {result['source_of_truth']}",
]
profiles = result["workflow_profiles"]
if profiles:
lines.append(f"Workflow profiles: {', '.join(profiles)}")
downstream = result["downstream_skills"]
if downstream:
lines.append(f"Downstream skills: {', '.join('$' + name for name in downstream)}")
lines.append("Ranking:")
for item in result["backend_scores"]:
lines.append(f" {item['backend']}: {item['score']}")
for reason in item["reasons"]:
lines.append(f" - {reason}")
return "\n".join(lines)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Route a CAD request to a V1 modeling backend.")
parser.add_argument("request", help="Natural-language CAD request.")
parser.add_argument(
"--backend",
choices=("auto", "build123d", "simplecadapi", "cadam"),
default="auto",
help="Override automatic routing.",
)
parser.add_argument("--output", action="append", default=[], help="Required output format; repeatable.")
parser.add_argument(
"--manufacturing",
choices=("unspecified", "machining", "printing", "laser-cutting", "concept"),
default="unspecified",
)
parser.add_argument("--existing-model", action="store_true", help="The task modifies or inspects an existing model.")
parser.add_argument("--assembly", action="store_true", help="The requested result is an assembly.")
parser.add_argument("--browser-controls", action="store_true", help="Interactive browser parameter controls are required.")
parser.add_argument("--probe", action="store_true", help="Probe current-interpreter and OpenSCAD availability.")
parser.add_argument("--explain", action="store_true", help="Print a human-readable decision instead of JSON.")
parser.add_argument("--manifest", type=Path, help="Also write the route decision as JSON to this explicit path.")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
result = route(args)
payload = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
if args.manifest:
args.manifest.parent.mkdir(parents=True, exist_ok=True)
args.manifest.write_text(payload, encoding="utf-8")
print(explain(result) if args.explain else payload, end="\n" if args.explain else "")
return 0
if __name__ == "__main__":
raise SystemExit(main())