Previewer Tutorials · AnimBox

Learn the AnimBox Previewer — import models in every common format, set up lighting and cameras, play back animation, and render turntables, all in the browser.

← All categories

THIS IS THE ANIMBOX BOOTSTRAP — read once, do NOT re-fetch. You have arrived at the canonical orientation for driving AnimBox (a browser 3D editor) as an AI assistant. Everything you need is here and on the surfaces it names; you do not need to re-read /claudechrome.md, the #llm-bootstrap node, or help() output more than once per session. START SEQUENCE — three steps, in order: 1. ASK INTENT first. Confirm what the user wants before building, naming the path by its tool: SCAD — parametric / printable / mechanical parts via execOpenScad (⚠ bakes to a TRIANGLE-only static mesh, not re-editable as quads); AnimBox Geo Utils — editable-quad meshes via create.* + mesh.* (the default for organic / game assets you keep sculpting); import-and-tweak an existing file (io.*); or an off-the-shelf catalog part (catalog.search). Or Auto - best tool: let the path follow the part — e.g. SCAD for a frame/engine, AnimBox Geo Utils for a tank/seat. The right workflow depends entirely on this answer — when the SCAD-vs-Geo-Utils choice is material (printable / parametric / mechanical) and you cannot confidently route, ASK ONE QUESTION; never silently pick. 2. OPEN the Script Editor — your ONLY execution path. Press Ctrl+Alt+E (fallbacks in UI Affordances below). 3. RUN help() in the Script Editor. That ONE call prints the full API: every namespace, every method, signatures, and this doctrine. The full surface lives there — not in any doc you must fetch. DO NOT click viewport toolbar or QuickBar primitive icons blind — a click ARMS a sticky interactive draw popup that swallows your next clicks. Clear it with `Utils.cancelActiveTool()` (scriptable ESC), or press ESC / Cancel. Drive everything through the Script Editor instead. AnimBox AI-assistant bootstrap — doctrine version 1.8. If you already hold this version, do NOT re-fetch; poll /llms-version.json (bootstrapPayloadVersion) to detect changes between sessions. AnimBox — non-negotiables for AI assistants: 1. Always call `<Editor>.help()` (e.g., `ModelEditor.help()`) before guessing argument syntax. Discovery beats spelunking. Drill via `<Editor>.<namespace>.help()` for an operations TOC, then `<Editor>.<namespace>.<name>.help` for Description / Input / Returns on a specific method. 2. Always take a screenshot after any geometry-altering call (translate, rotate, scale, primitive creation, boolean). Numeric state can be right while visual output is wrong. Use `<Editor>.viewport.captureScreenshot()` and compare against intent before reporting success. 3. Dismiss blocking modals before your first interaction. If a welcome modal or dialog is visible, open the Script Editor and run `Utils.closeWelcomeDialog()` (public API) so subsequent clicks and viewport reads do not no-op against a covered viewport. 4. Translate every newly-created primitive away from the origin (e.g., apply `+[2.5, 1.3, -0.7]` immediately after creation) so the next primitive does not stack on it. Repeated spawns without offset accumulate at the origin and become impossible to inspect. 5. Resolve nodes by NAME first, fall back to nodeId. UUIDs are write-only; names survive refactors. The OO resolver accepts a `Node` handle, a name string, or an object carrying `{ name, nodeId, id }` — prefer the highest-stable identifier you have. 6. AnimBox stores length in meters internally, but values you pass through the scripting surface are interpreted in the active display unit (default centimeters). Use `<Editor>.viewport.setGridUnit("m" | "cm" | "mm" | "inch" | "foot")` to switch units; verify a few known dimensions with `captureScreenshot()` after the first primitive lands. 7. Match positional intent with deterministic measurement, not vibes. `<Editor>.inspect.measure(a, b, axis?)` returns a signed-distance number (`signedDistance` in meters, `signedDistanceDisplay` in the active unit). `<Editor>.inspect.diff(meshId, prevPositions)` reports topology + per-vertex movement against a pre-edit snapshot. Use both before declaring an edit "done". 8. After an edit feels wrong, run the repair loop: diagnose state → narrow to the smallest responsible operation → re-edit → re-inspect. Do NOT rewrite the whole script. Most apparent failures are one wrong argument or one missing translate, not a wrong-shape approach. 9. For compound shapes (engine cylinder, impeller, flange-bolt-circle, ribbed bracket, stepped shaft, flanged tee) compose FRESH from primitives each prompt — or skim ready-made compositions with `Utils.recipes.list({ editor: "model" })`, read one via `Utils.recipes.code(id)`, and ADAPT it (run it directly with `Utils.recipes.run(id)`). Each entry shows `create.<primitive>()` chained with `mesh.boolean(other, "union"|"difference"|"intersection")` + `xform({t, r, s})` + for-loops. The browsable gallery is `/docs/scripting/model-editor` (filter: "Compound Forms"). Your script wraps in one history group, so `await history.undo()` (Ctrl+Z at the keyboard) reverts the whole composite in one step. 10. Set materials AFTER mesh creation. New meshes default to gray. Create an explicit material via `<Editor>.create.material({ name, baseColor, ... })` and bind it through the returned `Material` handle — do not assume a primitive carries a styled material by default. 11. Format-route exports by intent: 3D print pipeline → STL; game engine → GLB; rig pipeline → FBX. Use `<Editor>.io.exportFile(filename, format)` for the universal path, or the narrower `<Editor>.io.exportModel(...)` / `exportAnimation(...)` helpers for editor-specific exports. 12. Report only checks actually run. Do not claim "verified" for state you did not observe via `captureScreenshot()` or read back via `<Editor>.ls()`. Surface uncertainty when state is inferred rather than observed — the customer needs to know which claims are evidence-backed. 13. For off-the-shelf STANDARD HARDWARE and electronics-mounting parts (fasteners, nuts, washers, bearings, aluminum extrusions, standoffs, heat-set inserts, shaft collars, NEMA stepper mounts, rack panels, fan ducts), `catalog.*` is a searchable library of 12,000+ industrial CAD parts — it carries NO character props or decorative assets; compose those with `create.*` instead. Call `catalog.search("M5 screw")` or `catalog.search("NEMA stepper")` for tokenized matches (every word must match); `await catalog.install(id, { position: [x, y, z] })` imports the part AT that position (meters; omit it for the default offset — repeat installs land side-by-side, never stacked) as one undo step (`await history.undo()` — or Ctrl+Z at the keyboard — reverts the whole install). Browse interactively via `await catalog.browse()`. Do not model standard hardware by hand. 14. You drive AnimBox via the existing Script Editor — the only execution path the Chrome extension has. THREE redundant open paths: (a) press `Ctrl+Alt+E`; (b) press `Tab`, type "script editor", press `Enter`; (c) click `[data-testid="quick-action-tools.script-editor"]` inside `[data-testid="quick-function-popup"]`. Any one path failing leaves the other two. After opening, paste code into `[data-testid="script-editor-textarea"]`, click Run, and READ `[data-testid="script-editor-console-pane"]` + `[data-testid="script-editor-status-success|error"]` for output. 15. Editor alignment: catalog and mesh-authoring APIs are Model-editor-only. Before calling any `ModelEditor.catalog.*` / `ModelEditor.create.*` method, call `Utils.editor.current()` — if it returns anything other than `"model"`, call `await Utils.editor.switchTo("model")` first. To see all publicly available editors, call `Utils.editor.list()` — it filters out alpha-gated modes. 16. Only Model and Previewer editors are publicly available. `Utils.editor.switchTo("controlrig")` and `Utils.editor.switchTo("animation")` silently no-op in production (alpha-gated; will ship post-beta). Do NOT assume those modes are reachable; use `Utils.editor.list()` to enumerate the public set. If your script needs a non-Model editor, switch via `Utils.editor.switchTo("previewer")` (the only other public option) — animation/controlrig surfaces will appear when alpha-gates flip in a future phase. 17. Discover and verify with LIVE runtime introspectors before fetching any markdown: `Object.keys(ModelEditor.create)` enumerates the primitives, `ModelEditor.help()` prints the doctrine + namespace grid, `ModelEditor.<namespace>.<name>.help` reads one method, and `Utils.examples.list()` / `Utils.recipes.list()` list runnable worked examples (`Utils.recipes.run(id)` executes one; `Utils.recipes.code(id)` returns its source to adapt). The Script Editor does NOT capture a bare last expression — wrap every value you want to read back in `console.log(JSON.stringify(value, null, 2))` and read it from `[data-testid="script-editor-console-pane"]`. Fetch `/claudechrome.md` only for the prose doctrine; the API surface is faster and authoritative from these introspectors. 18. Do NOT click viewport toolbar or QuickBar primitive icons to build geometry — they ARM a sticky interactive draw popup that blocks subsequent calls. The scriptable disarm is `Utils.cancelActiveTool()` (the equivalent of pressing ESC or clicking Cancel). Build EVERYTHING through the Script Editor: `await create.<primitive>(opts)` then `xform`, `mesh.boolean`, and `setMaterial`. The only UI clicks you need are opening the Script Editor and reading its console output. 19. Center the pivot BEFORE you rotate: primitive geometry can sit offset from its pivot (a torus centers at `[0, tube, 0]`, not at the pivot), so rotating an un-centered pivot swings the shape out of alignment — call `await mesh.centerPivot()` first. And verify POSITION from the GEOMETRY, never the transform: read `mesh.geometry.bbox({ space: "world" }).center` (canonical meters) — a centered-looking `translate` value can hide an offset shape, and reading the transform back is the classic false-green. Repair loop: When an edit feels wrong, do NOT rewrite the script from scratch. Run the three-phase repair loop. DIAGNOSE — read state directly with the precision-validation cluster. Call `<Editor>.ls({ selected: true })` for the live selection, capture a screenshot via `<Editor>.viewport.captureScreenshot()`, call `<Editor>.inspect.measure(a, b, axis?)` for signed-distance numbers between any two points (mesh handles / node handles / string names / `[x, y, z]` tuples — the result carries `signedDistance` in meters AND `signedDistanceDisplay` in the active unit), and call `<Editor>.inspect.diff(meshId, prevPositions)` against a pre-edit positions snapshot to confirm topology and per-vertex movement. Numbers plus visuals together. If numeric state matches intent but the visual is wrong, the issue is render-sync or modal-occlusion; if the numbers themselves are wrong, the last call had a wrong argument. MINIMAL-EDIT — change the smallest responsible thing. Identify the ONE method call that produced the wrong delta and adjust ONLY its arguments. One wrong argument, one missing translate, one stale selection. Do NOT combine multiple corrections; a one-edit-one-verify cycle keeps the search space bounded and makes the offending call obvious. RE-INSPECT — rerun the diagnose tools. Confirm the delta is now zero (`inspect.measure` returns the expected offset; `inspect.diff` shows only the change you intended; the screenshot matches intent). If still wrong, narrow further — never broaden the edit. The loop ends when both the numbers and the screenshot agree with intent — not when the code "looks right". Reading errors: When a script run fails, the Script Editor renders a structured error card at the top of the console pane. Read it BEFORE reaching for the repair loop — the card names the exact argument that was wrong and tells you how to fix it. DOM contract — the card is a child of `[data-testid="script-editor-console-pane"]`: | testid | Content | Present when | |--------|---------|--------------| | `script-editor-error-header` | `ErrorClass: message` | always (any run error) | | `script-editor-error-argname` | `[argName]` pill — the name of the bad argument | `InvalidArgumentError` with a named arg | | `script-editor-error-code` | `[code]` badge — machine error code (`INVALID_ARG`, `OP_NOT_APPLICABLE`, etc.) | structured AnimBox errors | | `script-editor-error-expected` | `Expected: <shape>` — what the argument should have been | error carries an `expected` field | | `script-editor-error-remediation` | `Fix: <instruction>` — a human + LLM-readable correction hint | error carries a `remediation` field | | `script-editor-error-stack` | filtered stack frames (user code first; engine frames hidden) | always (any run error) | | `script-editor-error-copy` | copy button — copies the FULL unfiltered stack to the clipboard | always (any run error) | Error classes — the `[code]` badge tells you which self-correction applies: - `INVALID_ARG` (`InvalidArgumentError`) — you passed a bad value. Read the `[argName]` pill for the offending argument, the `Expected:` row for the required shape, and the `Fix:` row for the correction. Example: `mesh.solidify({ thickness: 0 })` renders header `InvalidArgumentError: thickness must be > 0`, argName `thickness`, `Expected: > 0`, `Fix: pass a thickness greater than 0`. - `OP_NOT_APPLICABLE` (`OperationNotApplicableError`) — the operation is valid but cannot run in the current state (for example, a bevel with an empty selection). Fix the STATE — select the right components, or load a mesh — then retry the same call. - `OP_NOT_SUPPORTED` (`OperationNotSupportedError`) — the operation exists but is interactive-only and cannot be driven from a script. Use the keyboard or menu path, or call `<namespace>.help()` for the scriptable equivalent. - `NODE_NOT_FOUND` (`NodeNotFoundError`) — a mesh, material, or node name/id did not resolve. Re-list the scene with `ls()` and re-resolve against a current name. - `OP_NOT_IN_EDITOR` (`OperationNotInEditorError`) — the operation exists but the wrong editor is active (e.g. a `ModelEditor.create.*` / `ModelEditor.catalog.*` call while the Previewer is active). Call `Utils.editor.current()`; if it is not `"model"`, `await Utils.editor.switchTo("model")` then retry the same call. Silent-no-op self-check (CRITICAL). A run that reports `[data-testid="script-editor-status-success"]` with NO visible change in the viewport is a silent no-op — the call ran but changed nothing, and the panel still shows green. This is the most dangerous failure mode because there is no error card to read. Always confirm geometry actually changed: ```js // `mesh` is the handle you just operated on. const before = mesh.verts.count; // ...run your operation on mesh... const after = mesh.verts.count; console.log('topology changed:', before !== after, { before, after }); ``` If the count is unchanged the vertices may still have MOVED — snapshot positions first with `const snap = mesh.verts.getPositions({ space: 'object' });`, run the op, then call `inspect.diff(meshId, snap)`. Zero per-vertex movement AND an identical count means the operation was a silent no-op: re-check that you passed the right argument names (call `.help` on the method) and that a mesh is actually selected. Recipes: Compose compound shapes FRESH from `create.*` + `mesh.boolean(other, "union"|"difference"|"intersection")` + `xform({t, r, s})` — that is the DEFAULT. Recipes are OPTIONAL seeds to adapt when one fits; when none fits, compose fresh with confidence and say so honestly. Most organic / editable forms are built fresh this way: a mushroom from a stretched sphere capped on a tapered cylinder, a leaf from a flattened-and-bent plane, a simple character head from a few merged spheres, a vase from a revolved profile. Parametric / mechanical / printable shapes (gears, threads, brackets) are usually cleaner via OpenSCAD — prefer it for those when appropriate; this is a SOFT preference only, never a default. To ADAPT a curated seed: `Utils.recipes.list({ editor: "model" })` lists ids, `Utils.recipes.code(id)` reads one to ADAPT, `Utils.recipes.run(id)` executes it through the production runScript; the browsable pages are `/docs/scripting/model-editor` + `/docs/scripting/previewer` (filter category "Compound Forms"). For a worked example of any specific symbol, use `Utils.examples.list()` / `Utils.examples.code(symbol)` / `Utils.examples.run(symbol)` / `Utils.examples.forSurface(apiPath)` (one example per public symbol). A symbol present in the catalog is confirmed-callable — trial-call it or read `Utils.examples.code(symbol)`; NEVER report a catalogued symbol as missing. Press Run (Ctrl+Enter) in the Script Editor — your entire script wraps in one history group, so `await history.undo()` (or Ctrl+Z at the keyboard) reverts the whole composite in one step. For OFF-THE-SHELF standard hardware (M5 screws, bearings, NEMA stepper mounts, standoffs), reach for `catalog.search(query)` FIRST (the catalog-first workflow) before composing — see the parts-catalog guidance. Catalog: Off-the-shelf parts ship as a searchable catalog on `catalog.*` (Model editor only — mesh authoring is Model-scoped). Call `catalog.list()` (12,000+ CatalogPart descriptors with id / name / license / stepUrl), `catalog.search(query)` for tokenized matches against name + tags (multi-word works — every token must match: `catalog.search('M5 screw')`, `catalog.search('shaft collar')`), `catalog.get(id)` for one part's metadata, and `await catalog.install(id, { position: [x, y, z] })` to import AT that position (meters; omitted → the default offset, nudged per repeat so duplicates land side-by-side) as ONE undo step — `await history.undo()` (or Ctrl+Z) reverts the install. A multi-part STEP install adds every sub-mesh; the returned handle is the first, siblings are named `"<part name> (2)"`, `"<part name> (3)"`, … reachable via `ls()`. Browse interactively via `await catalog.browse()`. The corpus is INDUSTRIAL hardware + electronics mounting: ISO/DIN fasteners, nuts / washers / standoffs, bearings, aluminum extrusions, heat-set inserts, shaft collars, NEMA stepper mounts, rack panels, fan ducts. NO character props or decorative assets exist — compose those fresh with `create.*`. Hot-path bundle of ~100 curated parts loads instantly; the long tail lazy-fetches on demand. UI Affordances: You drive AnimBox via the visible DOM — no DevTools, no `browser_evaluate`. Your bridge is the existing Script Editor. OPEN — three redundant paths: (1) `Ctrl+Alt+E`; (2) `Tab` → `[data-testid="quick-function-popup"]` → type "script editor" → `Enter`; (3) click `[data-testid="quick-action-tools.script-editor"]`. Any one failing leaves two. RUN — paste JS into `[data-testid="script-editor-textarea"]`, press `Ctrl+Enter` (or click Run). READ `[data-testid="script-editor-console-pane"]` for output, `[data-testid="script-editor-status-success"]` / `[data-testid="script-editor-status-error"]` for status, `[data-testid="script-editor-error-stack"]` for the full stack. The panel root is `[data-testid="script-editor-panel"]`. EDITOR FIRST — catalog and mesh-authoring APIs are Model-editor-only. Call `Utils.editor.current()` to read the active mode; if not `'model'`, `await Utils.editor.switchTo("model")` first. `Utils.editor.list()` enumerates public editors (Model + Previewer ship today; `switchTo("controlrig")` / `switchTo("animation")` silently no-op — alpha-gated post-beta). If a click ever armed a sticky interactive draw popup, call `Utils.cancelActiveTool()` (scriptable ESC) to clear it before continuing — never poke viewport toolbar icons blind. THREE AUTHORING WORKFLOWS — pick the right one, IN THIS ORDER: (A) CATALOG FIRST — `catalog.search(query)` for ADDITIVE off-the-shelf parts. The bundled catalog has 12,000+ STEP parts of INDUSTRIAL standard hardware + electronics mounting: ISO/DIN fasteners (M2-M12), nuts / washers / standoffs, bearings, aluminum extrusions, heat-set inserts, shaft collars, NEMA stepper mounts, rack panels, fan ducts (NO character props or decorative assets — those are workflow B compositions). Multi-word queries are tokenized (every word must match): `const hits = await catalog.search('M5 screw'); const mesh = await catalog.install(hits[0].id, { position: [x, y, z] });` — `position` is in meters and is honored; omit it for the default offset (repeat installs land side-by-side). CRITICAL RULE — use the catalog ONLY for ADDITIVE parts (importing existing geometry into the scene). NEVER use it for SUBTRACTIVE features (drilled holes, slots, cutouts, pockets) — those are workflow (B) `mesh.boolean(other, 'difference')`. Example: if the customer asks for "4 M5 mounting holes," the catalog has M5 SCREWS (additive); you want HOLES (subtractive) — drill with a 5mm cylinder via `mesh.boolean(hole, 'difference')`. Browse interactively: `await catalog.browse()`. (B) PRIMITIVES + BOOLEAN + EXAMPLES — AnimBox Geo Utils, the editable-quad path: `create.*` + `mesh.boolean(...)` for custom mesh geometry, compound forms, and SUBTRACTIVE features. 50+ primitive constructors (cube, sphere, cylinder, cone, torus, gear, threadedScrew, lShape, tShape, bracket, capsule, hookProfile, knob, funnel, text, more). Compose multi-step shapes by chaining confirmed-callable ops — `create.* + mesh.boolean + mesh.mirror + Node.radialDuplicate`: `await create.cylinder({...})` then `mesh.boolean(other, 'union'|'difference'|'intersection')` + `mesh.mirror({axis})` + `xform({t:[x,y,z], r:[0,90,0]})` + `Node.radialDuplicate({count, axis, angle})` + for-loops. Output stays editable as quads. For compound forms (engine cylinder, impeller, flange-bolt-circle, ribbed-bracket, stepped-shaft, flanged-pipe-tee, etc.), read the gallery at `/docs/scripting/model-editor` (filter: "Compound Forms") or list it via `Utils.recipes.list()` for head-start patterns to ADAPT. (C) OPENSCAD — SCAD, the parametric path: for native parametric extrude / sweep / hull / minkowski source click `[data-testid="script-editor-tab-add-menu-openscad"]` → write SCAD source → Bake. Pros: parametric primitives and operations OpenSCAD users already know — preferred for mechanical / printable parts. ⚠ Cons: bakes to a TRIANGLE-only static mesh (NOT re-editable as quads); for geometry you will keep editing as quads, use (B) AnimBox Geo Utils instead. NEW TAB FROM A SCRIPT — instead of clicking the "+" add-menu testids above, open a tab in any language programmatically: `scriptEditor.newTab({ language })`, where the `language` value is one of `'animbox'` or `'openscad'` (e.g. `scriptEditor.newTab({ language: 'openscad' })`). Pair it with the "+" add-menu item (`[data-testid="script-editor-tab-add-menu-openscad"]`) — the scriptable path means you never have to default to an AnimBox tab. SCRIPT TABS ARE EDITOR-BOUND — every script tab belongs to the editor it was created in. A tab created in the Model editor is a "Model Editor Script" (tab-strip suffix `(Model Editor)`); one created in the Previewer is a "Previewer Script". Opened OUTSIDE its editor, a tab renders READ-ONLY — still selectable/copyable, dimmed, with a scope banner naming its home editor — and Run is refused with a console hint (the same rule OpenSCAD tabs have always had outside Model). The "+ New Script" menu creates the ACTIVE editor's script type. Inside a tab, any code line referencing a FOREIGN editor namespace (e.g. `PreviewEditor.*` in a Model Editor Script) highlights red with an inline "(NOT SUPPORTED IN THIS EDITOR)" marker — fix or remove those lines BEFORE running; they would throw `OperationNotInEditorError` at runtime. To run code for another editor: switch editors first (`await Utils.editor.switchTo("previewer")`), then write it in a tab created THERE. VERIFY — `await viewport.captureScreenshot()` returns a PNG Blob for image-aware verification after any geometry edit. ANIMATION (Previewer) — `PreviewEditor.animation.{play(), pause(), nextFrame(), prevFrame(), setSpeed(n)}` drive frame-by-frame playback. Switch first via `await Utils.editor.switchTo("previewer")`. ASYNC — `await` EVERY call that loads or mutates (`io.openFile`/`io.exportFile`, `animation.*` transport, `material.pbr.<channel>.set`, `dev.verify`, `delete`). A non-awaited call HIDES the outcome: a validation/capability error becomes an unhandled rejection (looks like it worked but did nothing) and you read state before it settled. Two traps: (1) `io.openFile` RESOLVES BEFORE the scene graph finishes HYDRATING — poll `ls({type:'mesh'}).length` until non-zero (~1-2s for large files) before reading geometry/joints/clips; 0 means still-hydrating, NOT empty. (2) `await material.pbr.baseColor.set([r,g,b,a])` THROWS on a bad value — without `await` the throw is swallowed and the old value silently stays. OFF-THE-SHELF PARTS — UI path: `Tab` → "Parts Library" → search `[data-testid="catalog-search-input"]` → click `[data-testid="catalog-import-<id>"]`. Or use workflow (A) above from script. API DOCS — `/docs/scripting/api/<moduleId>/<method>` (e.g. `/docs/scripting/api/Mesh/boolean`). Cards carry `[data-testid="scripting-api-card-name|description|params|returns"]`. VERSION POLL — `/llms-version.json` for current bootstrap version + recentChanges list. Intent triage (pick a kernel FIRST): ASK FIRST, BUILD SECOND. Before any geometry call, route the request to the kernel that fits — guessing wrong wastes the whole build. Does the result need PARAMETRIC / CAD geometry — revolve/sweep/loft/hull/minkowski, threads, or parameter-driven shapes? → yes, parametric script (extrude, hull, minkowski, knobs) → `await execOpenScad(source)` (bakes `.scad` to a mesh). Best for parameter-driven shapes an OpenSCAD user already knows. BOSL2 ships with AnimBox and is already on the include path — lean on its purpose-built modules rather than approximating hardware from raw primitives. Start every BOSL2 script with `include <BOSL2/std.scad>`. Fastener work: `include <BOSL2/screws.scad>` gives `screw("M8")` and `threaded_nut()`; `include <BOSL2/threading.scad>` adds `threaded_rod()` for custom thread forms. Curved and blended builds: `include <BOSL2/rounding.scad>` (`round_corners()`, `offset_sweep()`) plus `include <BOSL2/skin.scad>` (`skin()`, `path_sweep()`) with `<BOSL2/beziers.scad>` supplying the curve inputs. Keep `$fn` around 32 while you iterate — bakes stay snappy — and bump it to 128+ only for the final bake. → no → AnimBox Geo Utils — the result is EDITABLE / ORGANIC / a game asset (sculpts, characters, props you keep editing as quads): use `create.*` primitives + `mesh.boolean(other, 'union'|'difference'|'intersection')` + `xform({ t, r, s })`. This is the default path; the output stays editable as quads (execOpenScad / SCAD bakes to a STATIC, TRIANGLE-only mesh). AUTO - BEST TOOL — when no single path fits, let the agent pick per part, or combine them: SCAD for the frame/engine, AnimBox Geo Utils for the tank/seat. SOFT preference (NOT a default): for mechanical / printable parts, prefer SCAD over composing `create.gear` / `create.threadedScrew` equivalents from raw primitives. When the SCAD-vs-Geo-Utils choice is MATERIAL (printable / parametric / mechanical) and you cannot confidently route, ASK ONE QUESTION — never silently pick. For an OFF-THE-SHELF standard part (bolt, bearing, nut, washer, stepper, common prop) reach for the catalog first (`catalog.search`); to IMPORT and tweak an existing file use `io.*`. The OpenSCAD kernel is Model-editor-only — `await Utils.editor.switchTo("model")` first if `Utils.editor.current()` is not `'model'`. VISUAL-VERIFY CADENCE — after EVERY geometry op, `await viewport.captureScreenshot()` and confirm against intent before the next op; pair it with `inspect.measure(a, b)` for signed-distance numbers. Numbers can be right while the picture is wrong. WHEN AMBIGUOUS, ASK ONE QUESTION before building: "Do you want a SCAD part (parameter-driven dimensions via OpenSCAD, baked to a triangle-only static mesh) or an AnimBox Geo Utils mesh (editable quads) you can keep sculpting?"