@forgent3d/cad-build
v0.4.1
Published
Portable Forgent3D CAD build capability for coding agents: build-check a build123d-dialect model.py — or an OpenSCAD .scad, evaluated by the bundled OpenSCAD and rebuilt as exact B-rep — and measure it with the same wasm engine (and the same report text)
Readme
@forgent3d/cad-build
The portable CAD build capability a coding agent (Claude Code / Codex) drives to
author a Forgent3D model.py — or an OpenSCAD .scad, which the bundled OpenSCAD
(the Forgent3D fork, wasm) renders. It bundles the same engine the product runs —
@forgent3d/build-service's buildOne / probeOne over @forgent3d/cad-engine's
wasm — and prints through the same formatter (formatRebuildSummary /
formatProbeResult), so rebuild and check return byte-for-byte what the
in-product CAD agent sees. No monorepo, no python, no OCCT install: the wasm +
cadkit assets ship inside this package.
This is the engine behind the forgent3d skill
(forgent3d/forgent3d-skills), which picks
its build123d or OpenSCAD route per request; its bin/cad-build shells out to
npx @forgent3d/cad-build@latest … (with a local-path fallback via
FORGENT3D_CAD_BUILD_BIN).
Commands
cad-build rebuild [file.py|file.scad] # build-check + dimension report
cad-build check [file.py] --code '…' # one-off probe in the built model's namespace (.py only)
cad-build check [file.py] --code-stdin < probe.py # large snippet via stdin
cad-build knobs [file.py|file.scad] [--params a,b] # build every published #@ knob at its extremes
cad-build snapshot [file.py|file.scad] [--view front] [--line N] [--line-art] [--xray] # orthographic PNG; --line zooms onto one statement's faces
cad-build export [file.py|file.scad] --format step # step | stl | glb | obj | 3mf | brep
cad-build recipe <outline|motion|pocket>
cad-build preview [file.py|file.scad] # → shareable 3D link
cad-build feedback --message '…' [file.py] # report an engine/tool bugThe source file may be named anything; it defaults to ./model.py, else the sole
*.py, else the sole *.scad in the directory. Its directory is the project root (so
relative refs resolve exactly like the product's per-project workspace).
cad-build help <command> has the details of each.
OpenSCAD
Every command but check takes a .scad in place of the .py, and the real OpenSCAD
(shipped in dist/assets/openscad/, pointed at by OPENSCAD_WASM_DIR; Manifold backend)
renders it — the same render the desktop's F5 shows and the browser workbench (/scad)
previews, through @forgent3d/scad-transpile's render path. No model.py is derived and
the B-rep kernel is not involved: every report reads OpenSCAD's own triangles.
rebuildreports the render's bbox, volume (the facets'), triangle count, what the fileecho()ed and OpenSCAD's warnings at their.scadlines.checkis refused: there is no namespace behind a render —echo()is the probe.The file's folder is the project (
--root <dir>mounts a larger one): every.scadand every data file OpenSCAD reads (import(),surface(), fonts) is mounted at its relative path, under the workbench's limits. MCAD, BOSL2 and Write.scad are built in.knobswalks the Customizer parameters (// [min:max],// [a, b], true/false), writing each literal back into the source and re-rendering — never-D.exportwritesstl(byte for byte the workbench's STL),3mf,glborobjfrom OpenSCAD's triangles.stepis refused with the way out: the page behindpreviewconverts the file to a STEP with exact surfaces.previewhands outhttps://app.forgent3d.com/scad#code=<base64url>: the link carries the code, nothing is uploaded, and the page runs OpenSCAD on it with the parameters live and STEP/STL export. One link holds one file; a project that includes others is refused one and told to load the folder there.rebuild — the product's
rebuild_model:ok+ bbox, volume, the cylinder census (each radius and where its axis runs), the planar faces (where every flat face sits), disjoint solids, per-body world bboxes, model.py's ownprint()output and the build diagnostics — or the first actionable error, with a non-zero exit.check — the product's
check_model: runs a--codesnippet in model.py's namespace after it builds and prints what itprint()s. Top-leveldef test_*functions run as isolated named checks (assertis allowed in the probe dialect only). Nothing is read from or written to the project; any failure exits non-zero.knobs — does every published
#@knob survive its own range? Each is moved alone to its extremes — number range ends, the other switch state, every dropdown option — and rebuilt (broken/warns/inert/ok;warns= builds there but with a warning the default does not raise). Nothing else ever builds a non-default value.snapshot — the 2×2 third-angle sheet by default;
--viewrenders one view alone (left/back/bottom only exist that way);--line-art --xrayis the product'scomparedress — a line drawing with hidden edges dashed, the one to hold beside a drawing.--line Nis the feature-level shot: the faces built by model.py line N (the same row the product's 说明书 shows) tinted orange, their edges dashed through material, the picture zoomed onto them — a 1×2 ALONG/ACROSS strip without--view, one view zoomed with it; the text names the faces, their box and per-view visibility. model.py only.recipe — a focused reference bundled from the production
*.recipe.ts(single source of truth), with the product's tool names translated to these commands.export — writes the built model next to the source (
--outto place it elsewhere). Entirely local; talks to no server.preview — builds locally, then uploads
model.py, the BREP, that build'sscene.json(colours, materials, assembly poses) and theimports/files model.py names (an STL aMeshRefshows, a STEPimport_cadread — the page draws the mesh from the file, a remix rebuilds from it) to the anonymous preview endpoint and prints a share link. Anyone can open it, signed in or not: a 3D view of the part (in its colours — that is what the scene upload is for), and a browser editor where visitors can turn the#@knobs, rebuilt in their own browser, but nothing is saved; remixing makes an editable copy in their own account. Nothing is built server-side. The secret that lets a laterpreviewupdate the same link lives in.forgent3d-preview.json. Override the endpoint withFORGENT3D_PREVIEW_ENDPOINT.
Build
pnpm --filter @forgent3d/cad-build build # regen cadkit snapshot + esbuild bundle + copy assets + gen recipes.json
pnpm --filter @forgent3d/cad-build smoke # offline smoke of every command in an os-temp dir (preview against a mock server)scripts/build.mjs regenerates cad-engine/src/cadkit-snapshot.json from
cadkit/*.py (it is a gitignored generated file — copying whatever is on disk would
ship a stale cadkit), bundles src/cli.ts → dist/cli.mjs with code splitting (BOSL2
and the Write.scad fonts land in dist/chunks/, loaded only by a file that uses them;
web-tree-sitter and @forgent3d/openscad-wasm stay external), copies the runtime
assets into dist/assets/ (tree-sitter*.wasm, occt-import-js.cjs + .wasm,
cadkit-snapshot.json, and openscad/: the OpenSCAD full build — the render path never
loads the slim one, so it is not shipped — its fonts and the GPL text from
packages/openscad-wasm/dist), and generates recipes.json.
pnpm typecheck runs tsc over src/.
bin/cad-build.mjs points the engine at those assets via CAD_ENGINE_ASSETS_DIR /
OCCT_WASM_JS / CAD_BUILD_ASSETS_DIR before loading the bundle.
Release
build+smoke(above).- Bump
version, thennpm publishfrom this directory (prepackrebuilds). - Only then push skill text that uses new commands or flags: the skill's launcher runs
@latest, so the CLI has to be on npm first.
Keep in sync
- The report text is build-service's
summary-format.ts, shared with cf-sandbox'srebuild_model/check_model— change it there and both surfaces move together. recipes.jsonregenerates frompackages/cloud/lib/aicad-agent/recipes/*.recipe.ts; a recipe that names a product-only tool fails the build (map it inTOOL_NAMES).
Relationship to the forgent3d skill
The skill (github.com/forgent3d/forgent3d-skills, mounted here as the
forgent3d-skills submodule) is maintained independently — its SKILL.md is
hand-written and evolves on its own. The only contract between the two lines is this
CLI: the skill shells out to cad-build <cmd>, so the subcommand names, flags and
output shape above are what must stay stable. Changing them is a breaking change for
the skill.
