forgecad 0.9.7 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/dist/assets/{AdminPage-DX0mpSZT.js → AdminPage-CXaVLMiV.js} +1 -1
  3. package/dist/assets/{BlogPage-CI_P0_Pf.js → BlogPage-Crpr3JjH.js} +1 -1
  4. package/dist/assets/{DocsPage-DLhIIZyJ.js → DocsPage-CNBKuitP.js} +2 -2
  5. package/dist/assets/{EditorApp-DfFT2Dn8.css → EditorApp-D11wL4Qn.css} +51 -0
  6. package/dist/assets/{EditorApp-BujZvuwX.js → EditorApp-DVMnXOmO.js} +151 -9
  7. package/dist/assets/{EmbedViewer-0S0qXKog.js → EmbedViewer-KXFLSnpo.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-O_yMtAri.js → LandingPageProofDriven-2q2sn7aW.js} +1 -1
  9. package/dist/assets/{PricingPage-DGkX3Ahr.js → PricingPage-CVvgdv0i.js} +1 -1
  10. package/dist/assets/{SettingsPage-DBsqTB_y.js → SettingsPage-BVj1FtEv.js} +1 -1
  11. package/dist/assets/__vite-browser-external-Dhvy_jtL.js +4 -0
  12. package/dist/assets/{app-BE2nD6Yz.js → app-Dn4EwHhN.js} +707 -458
  13. package/dist/assets/cli/{render-iP9qh475.js → render-BI3gLMXz.js} +1011 -145
  14. package/dist/assets/constructionHistoryWorker-z9_LGiRd.js +42984 -0
  15. package/dist/assets/{evalWorker-Ds5U4xtN.js → evalWorker-CtO7GsJR.js} +42 -9
  16. package/dist/assets/{inspectWorker-Dll4eVyD.js → inspectWorker-BZ2CkQZr.js} +785 -111
  17. package/dist/assets/{manifold-sJ-axdXM.js → manifold-BVi4_OeB.js} +1 -1
  18. package/dist/assets/{manifold-DjYsd7A_.js → manifold-C6-sZYQN.js} +2 -2
  19. package/dist/assets/manifold-Cp_dCC7i.js +3018 -0
  20. package/dist/assets/{manifold-Bk26ViCr.js → manifold-DAzn2Fsa.js} +1 -1
  21. package/dist/assets/{renderSceneState-Bngp5MrQ.js → renderSceneState-BIvOkPK3.js} +1 -1
  22. package/dist/assets/{reportWorker-CU8RZ4O0.js → reportWorker-Bz9tGiHb.js} +42 -9
  23. package/dist/assets/{sectionPlaneMath-BdTjyVfs.js → scalar-sampling-budget-iBAeF8RM.js} +483 -71
  24. package/dist/cli/render.html +1 -1
  25. package/dist/docs/index.html +1 -1
  26. package/dist/docs-raw/CLI.md +10 -10
  27. package/dist/docs-raw/coding-best-practices.md +1 -1
  28. package/dist/docs-raw/guides/inspection-bundles.md +77 -19
  29. package/dist/docs-raw/guides/skill-maintenance.md +1 -1
  30. package/dist/docs-raw/runbook.md +2 -2
  31. package/dist/docs-raw/skills/forgecad-make-a-model.md +11 -0
  32. package/dist/docs-raw/skills/forgecad-render-inspect.md +12 -6
  33. package/dist/docs-raw/skills/index.md +1 -1
  34. package/dist/index.html +1 -1
  35. package/dist/sitemap.xml +6 -6
  36. package/dist-cli/forgecad.js +596 -354
  37. package/dist-cli/forgecad.js.map +1 -1
  38. package/dist-skill/CONTEXT.md +77 -19
  39. package/dist-skill/docs/CLI.md +10 -10
  40. package/dist-skill/docs/guides/inspection-bundles.md +77 -19
  41. package/dist-skill/docs-dev/CLI.md +10 -10
  42. package/dist-skill/docs-dev/coding-best-practices.md +1 -1
  43. package/dist-skill/docs-dev/guides/inspection-bundles.md +77 -19
  44. package/dist-skill/docs-dev/guides/skill-maintenance.md +1 -1
  45. package/dist-skill/library/forgecad-make-a-model/SKILL.md +11 -0
  46. package/dist-skill/library/forgecad-render-inspect/SKILL.md +12 -6
  47. package/package.json +6 -3
@@ -1,9 +1,9 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/app-CsHnaBWt.css"])))=>i.map(i=>d[i]);
2
2
  import { r as reactExports, j as jsxRuntimeExports, R as React, f as useNavigate, c as create$1 } from "./vendor-react-Da3A2QmU.js";
3
- import { bY as generateCuttingLayoutPdf, aB as getShapeCompilePlan, bZ as getCameraForwardVector, b_ as RENDER_STYLE_OPTIONS, _ as __vitePreload, $ as Matrix4, bs as getSketchWorldMatrix, G as Box3, A as ACESFilmicToneMapping, b3 as shapeToGeometry, D as DoubleSide, b$ as initKernelManifoldOnly, bI as initSolverWasm, bH as initKernel } from "./sectionPlaneMath-BdTjyVfs.js";
4
- import { u as useAuthStore, c as authApi, d as showToast, A as AuthApiError, e as useProjectStore, t as triggerDownload, g as useForgeStore, r as readProjectFilesFromDataTransfer, h as hasImports, i as collectDependencies, j as buildBundleShareUrl, k as buildBundleEmbedUrl, l as buildShareUrl, m as buildEmbedUrl, n as fileSystem, o as useFeatureFlagStore, p as fetchGistModel, q as fetchUrlModel, F as FLAG_DEFINITIONS, v as exportMeshFromStore, w as exportReportFromStore, x as exportOrbitVideoFromStore, y as exportSketchFromStore, z as buildGistShareUrl, C as exportExactFromStore, D as deriveExportStem, b as authFetch, E as storageQuotaUpgradeMessage, G as isImportableProjectMeshFile, H as isImportableProjectBinaryFile, I as hasExternalFiles, J as isImportableProjectExactFile, K as resolvePreviewFile, L as countParamSnapshotDiff, M as buildEmbedSnippet, a as applyTheme, N as themes, O as formatArea, P as sliderToAnimationSpeed, Q as animationSpeedToSlider, R as formatAnimationSpeed, S as resolveJointRange, T as useJointsConfig, U as useJointAnimationValues, V as INSPECT_POINT_SAMPLE_COUNT_MAX, W as INSPECT_POINT_SAMPLE_COUNT_MIN, X as expandBoundsByTransformedAabb, Y as Canvas, Z as PerspectiveCamera, _ as ControlsInteractionBridge, $ as ViewController, a0 as SceneConfigurator, a1 as LocalStudioEnvironment, a2 as RenderLabelsOverlay, a3 as Grid, a4 as OrbitControls, a5 as TOUCH_GESTURES_3D, a6 as MOUSE_BUTTONS_3D, a7 as ModelJourneyBar, a8 as FOCUS_MODE_DIM_OPACITY, a9 as useJointAnimationLoop, aa as computeJointNodeMatrices, ab as computeObjectJointMatrices, ac as readLastActiveFileForUser, ad as ToastContainer, ae as isMobile, af as useFeatureFlag, ag as decodeSharedHash, ah as decodeSharedBundle, ai as getExternalUrl, aj as getGistId, ak as Viewport, al as shouldBlockBrowserShortcut, am as useDrawStore, an as storePendingShareCopy } from "./app-BE2nD6Yz.js";
3
+ import { c8 as generateCuttingLayoutPdf, aB as getShapeCompilePlan, c9 as getCameraForwardVector, ca as RENDER_STYLE_OPTIONS, _ as __vitePreload, $ as Matrix4, bC as getSketchWorldMatrix, G as Box3, A as ACESFilmicToneMapping, b3 as shapeToGeometry, D as DoubleSide, cb as initKernelManifoldOnly, bT as initSolverWasm, bS as initKernel } from "./scalar-sampling-budget-iBAeF8RM.js";
4
+ import { u as useAuthStore, c as authApi, d as showToast, A as AuthApiError, e as useProjectStore, t as triggerDownload, g as useForgeStore, r as readProjectFilesFromDataTransfer, h as hasImports, i as collectDependencies, j as buildBundleShareUrl, k as buildBundleEmbedUrl, l as buildShareUrl, m as buildEmbedUrl, n as fileSystem, o as useFeatureFlagStore, p as fetchGistModel, q as fetchUrlModel, F as FLAG_DEFINITIONS, v as exportMeshFromStore, w as exportReportFromStore, x as exportOrbitVideoFromStore, y as exportSketchFromStore, z as buildGistShareUrl, C as exportExactFromStore, D as deriveExportStem, b as authFetch, E as storageQuotaUpgradeMessage, G as isImportableProjectMeshFile, H as isImportableProjectBinaryFile, I as hasExternalFiles, J as isImportableProjectExactFile, K as resolvePreviewFile, L as countParamSnapshotDiff, M as buildEmbedSnippet, a as applyTheme, N as themes, O as formatArea, P as sliderToAnimationSpeed, Q as animationSpeedToSlider, R as formatAnimationSpeed, S as resolveJointRange, T as useJointsConfig, U as useJointAnimationValues, V as INSPECT_POINT_SAMPLE_COUNT_MAX, W as INSPECT_POINT_SAMPLE_COUNT_MIN, X as expandBoundsByTransformedAabb, Y as Canvas, Z as PerspectiveCamera, _ as ControlsInteractionBridge, $ as ViewController, a0 as SceneConfigurator, a1 as LocalStudioEnvironment, a2 as RenderLabelsOverlay, a3 as Grid, a4 as OrbitControls, a5 as TOUCH_GESTURES_3D, a6 as MOUSE_BUTTONS_3D, a7 as ModelJourneyBar, a8 as FOCUS_MODE_DIM_OPACITY, a9 as useJointAnimationLoop, aa as computeJointNodeMatrices, ab as computeObjectJointMatrices, ac as readLastActiveFileForUser, ad as ToastContainer, ae as isMobile, af as useFeatureFlag, ag as decodeSharedHash, ah as decodeSharedBundle, ai as getExternalUrl, aj as getGistId, ak as Viewport, al as shouldBlockBrowserShortcut, am as useDrawStore, an as storePendingShareCopy } from "./app-Dn4EwHhN.js";
5
5
  import { a as PRODUCTION_EXPORT_COPY } from "./copy-CQKQppF-.js";
6
- import { f as formatRenderSceneCliSpec } from "./renderSceneState-Bngp5MrQ.js";
6
+ import { f as formatRenderSceneCliSpec } from "./renderSceneState-BIvOkPK3.js";
7
7
  import { H as HighlightJS, j as javascript } from "./javascript-70-4uGcz.js";
8
8
  const RESEND_COOLDOWN_S = 60;
9
9
  function EmailVerificationBanner() {
@@ -101,7 +101,7 @@ function logoutAndRedirectToLanding(logout, navigate) {
101
101
  navigate("/", { replace: true });
102
102
  });
103
103
  }
104
- const contextMd = "# ForgeCAD — AI Context (Chat UI)\n\n> **Usage:** Paste this file as context into your AI chat session (Claude.ai, ChatGPT, Gemini, etc.).\n> The AI will have full ForgeCAD API knowledge and will guide you through building models.\n>\n> **No CLI access in this session.** The AI cannot run commands directly. Instead, it will ask\n> you to run commands like `forgecad run <file>`\n> in your terminal and paste back the output for verification and iteration.\n\n## Workflow\n\n1. Tell the AI what you want to build and share any existing `.forge.js` files.\n2. The AI will write or edit model files for you.\n3. To validate, run `forgecad run <file>` in your terminal and paste the output.\n4. Iterate until the model looks right, then optionally `forgecad render 3d <file>` for a PNG.\n\n---\n\n## ForgeCAD API Reference\n\nAuthor or modify ForgeCAD models, sketches, assemblies, and CLI workflows.\nPrefer documented primitives, import rules, and placement strategies over inventing new APIs.\n\n### Model files\n\n- `.forge.js` — parametric part or assembly script; return a `Shape`, `Sketch`, `ShapeGroup`, `Assembly`, `SolvedAssembly`, array of renderables, or metadata object. Assemblies render directly; do not add `.toGroup()` unless you need `ShapeGroup` behavior.\n- Model the physical artifact, not an educational diagram. Do not add explanatory labels, arrows, legends, or text plaques unless the user explicitly asks for a presentation or teaching view. Product markings are allowed only when they would exist on the real object.\n- Build the real closed CAD first. Do not bake cutaways, sectioned shells, permanently exploded layouts, or hidden-parts views into the default model just to show internals. Use viewer-only cut planes, `explodeView`, object hiding, transparency, or `render inspect` section channels after the artifact exists.\n\n### Import and composition\n\n- Always include the extension in relative imports: `require(\"./file.forge.js\", { Param: value })` for model files and `require(\"./helpers.js\")` for plain helper modules. Do not write extensionless imports such as `require(\"./file\")`; ForgeCAD resolves project imports by exact path.\n- ForgeCAD APIs are injected globals in `.forge.js` files. Use `bom()`, `box()`, `scene()`, `Shape`, etc. directly; do not destructure those names from helpers with patterns like `const { bom } = require(\"./bom.js\")`. If a helper file is needed, import it under a project-specific name such as `const bomHelpers = require(\"./bom.js\")`.\n- `importSvgSketch()` for SVG files (file format loader, not a module import).\n- `.placeReference('bottom', [0,0,0])` to align any built-in anchor to a world coordinate; also works with custom `.withReferences()`.\n- Plain `.js` modules for shared helpers/constants (not model imports).\n\n### Validation commands (ask the user to run these)\n\n```\nforgecad run <file.forge.js> # geometry diagnostics\nforgecad render 3d <file.forge.js> # PNG render (shaded 3D)\nforgecad render wireframe <file.forge.js> # wireframe-only render\nforgecad render section <file.forge.js> --plane XZ # 2D cross-section (SVG/PNG)\nforgecad capture gif <file.forge.js> # animated orbit GIF\n```\n\n---\n\n<!-- skill-cli.md -->\n\n# ForgeCAD CLI for AI Workflows\n\nUse the CLI to validate, inspect, and export the model the AI is editing. Keep commands generic so they apply to the user's file, not a repo demo.\n\n## Validation Loop\n\n```bash\nforgecad run path/to/model.forge.js\nforgecad run path/to/model.forge.js --debug-imports\nforgecad run path/to/model.forge.js --backend occt\nforgecad check print path/to/model.forge.js --json\nforgecad check params path/to/model.forge.js --samples 12\n```\n\n- `forgecad run` prints geometry diagnostics, object summaries, collisions, verification results, and solver info.\n- `forgecad check print` reports collisions, mesh health, sampled walls, overhangs, and bed contact.\n- `forgecad check params` sweeps declared parameter ranges and reports crashes, degenerates, and new collisions.\n\n## Visual Checks\n\n```bash\nforgecad render 3d path/to/model.forge.js\nforgecad render 3d path/to/model.forge.js --camera front --camera iso\nforgecad render wireframe path/to/model.forge.js\nforgecad render section path/to/model.forge.js out/section.svg --plane XZ --offset 10\nforgecad capture gif path/to/model.forge.js\n```\n\n- Use `render 3d` for normal shaded verification.\n- Use `wireframe` or `section` when internal geometry or edge flow matters.\n- Use `capture gif` or `capture mp4` for motion and presentation.\n\n## Export\n\n```bash\nforgecad export stl path/to/model.forge.js\nforgecad export 3mf path/to/model.forge.js --quality high\nforgecad export step path/to/model.forge.js\nforgecad export report path/to/model.forge.js out/report.pdf\nforgecad export cutting-layout path/to/sheet-stock-model.forge.js --sheet-width 420 --sheet-height 594 --kerf 3\n```\n\nPick the export that matches the goal: mesh for printing, STEP for exact CAD interchange, report for review, cutting layout for sheet-stock workflows.\n\n\n---\n\n<!-- API/core/concepts.md -->\n\n# ForgeCAD Core Concepts\n\nForgeCAD scripts are JavaScript that returns geometry. The forge API is globally available — no imports needed.\n\n```javascript\nconst width = param(\"Width\", 50, { min: 20, max: 100, unit: \"mm\" });\nreturn box(width, 30, 10);\n```\n\n## Injected Runtime Names\n\nForgeCAD API functions and classes are injected into every `.forge.js` script. Use them directly; do not import or destructure ForgeCAD API names from helper files.\n\n```javascript\n// BAD — `bom` and `bomToCsv` are already built-in runtime names.\nconst { bom, bomToCsv } = require(\"./bom.js\");\n\n// GOOD — use the built-in directly.\nbom(4, \"M4 bolt\");\n\n// GOOD — keep project helpers under their own local name.\nconst bomHelpers = require(\"./bom.js\");\nbomHelpers.addFasteners(...);\n```\n\nTop-level declarations such as `const bom = ...`, `let scene = ...`, or `class Shape {}` collide with the injected runtime names. If you need a local helper, choose a project-specific name like `projectBom`, `sceneConfig`, or `makeShape`.\n\n## Execution Model\n\n- Scripts re-execute on every parameter change (400ms debounce)\n- Geometry operations are **immutable** — shapes, sketches, groups, imported assemblies, and wood boards return new values instead of modifying in place\n- Must return one of: `Shape`, `Sketch`, `ShapeGroup`, `Assembly`, `SolvedAssembly`, `SdfShape`, `Array` of renderables, `Array` of `{ name, tags?, shape?, sketch?, group?, color? }`, or a **metadata object** (see below)\n\nTop-level assembly scripts can return an unsolved `Assembly` directly; ForgeCAD solves it at default joint values for display. Return `assembly.solve(state)` when you want a specific pose. Do not call `.toGroup()` just to make an assembly render — use `.toGroup()` only when you specifically need `ShapeGroup` composition, group-style transforms, or named-child lookup.\n\n### Metadata Object Return\n\nA script can return a plain object whose values include renderable geometry alongside non-renderable metadata. All renderable entries (Shape, Sketch, ShapeGroup, Assembly, SolvedAssembly, SdfShape, or Array of named objects) are rendered; non-renderable entries are silently skipped. This is useful for multi-file projects where a part needs to publish interface data (bolt positions, dimensions) to other files:\n\nWhen importing project files, include the full extension in every relative path: `require('./motor-mount.forge.js')` for model files and `require('./helpers.js')` for plain helper modules. ForgeCAD resolves project imports by exact path and does not infer `.forge.js` or `.js` from `require('./motor-mount')`.\n\n```javascript\n// motor-mount.forge.js — renders standalone, exports metadata via require()\nconst holePositions = [[17, 15], [-29, 15], [17, -15], [-29, -15]];\nreturn {\n shape: mount.color('#556B2F'), // rendered\n bolts: { dia: 5.3, pos: holePositions }, // metadata — skipped in render, available via require()\n};\n\n// base-body.forge.js — imports mount, accesses .bolts\nconst mount = require('./motor-mount.forge.js');\nfor (const [x, y] of mount.bolts.pos) { ... } // use metadata\n// mount.shape is the Shape if you need it in an assembly\n```\n\nArrays inside the object are also rendered:\n\n```javascript\nreturn {\n parts: [{ name: 'Left', shape: leftShape }, { name: 'Right', shape: rightShape }],\n armWidth: 6, // metadata\n};\n```\n\nNamed return objects and named `group(...)` children can include `tags`. Tags are viewport metadata: they do not affect geometry, exports, face labels, or BOM rows, but the command palette can hide, show only, or focus every object with a selected tag.\n\n```javascript\nreturn [\n { name: 'Base Plate', tags: ['printed', 'structural'], shape: base },\n { name: 'M4 Bolt A', tags: 'fastener', shape: boltA },\n { name: 'M4 Bolt B', tags: 'fastener', shape: boltB },\n];\n```\n\n## Coordinate System\n\nZ-up right-handed: X = left/right, Y = forward/back, Z = up/down.\n\n## Colors\n\n`.color(hex)` works on `Shape` and `Sketch`. Colors survive transforms. Boolean operations return a single result shape, so only the first operand's color survives.\n\n**`union()` merges shapes into one solid mesh** — later operands do not keep separate colors or identities. Use `group(...)` or return named objects instead when you want separate parts:\n\n```javascript\nreturn [\n { name: \"Base\", shape: box(100, 100, 5), color: \"#888888\" },\n { name: \"Column\", shape: cylinder(50, 10).translate(50, 50, 5), color: \"#4488cc\" },\n];\n```\n\n## Face Operations\n\nShapes carry semantic face labels through their lifecycle. The flow is:\n\n1. **Primitives** assign canonical names — `box()` gives you `top`, `bottom`, `side-left`, etc.; `cylinder()` gives `top`, `bottom`, `side`.\n2. **Extrusions** inherit labels from the sketch and add `top`/`bottom`.\n3. **Transforms** (translate, rotate, scale, mirror) preserve all labels.\n4. **Booleans** preserve labels from the first operand where geometry survives.\n\nYou resolve labels to geometry with `.face(name)` or `.face(query)` — see the Shape class docs for the full query API. Operations like `.pocket()`, `.boss()`, `.hole()`, and `faceProfile()` all consume face references.\n\n## Text vs Viewport Labels\n\nDefault to no explanatory text inside CAD geometry. A ForgeCAD model should represent the physical artifact, not a labeled teaching diagram. Explain the design through file names, named return objects, comments, BOM entries, inspection bundles, and companion docs.\n\nUse `text2d()` only when the letters are part of the real object: raised branding, engraving, serial plates, keyboard legends, gauge ticks, connector labels, service arrows, scale markings, or exported manufacturing markings. `text2d()` builds filled sketch geometry from font outlines, so it can make exact/OCCT workflows slower.\n\nUse `Viewport.label(text, [x, y, z], options)` only for temporary review, debug, tutorial, or explicitly requested presentation views. Render labels are annotations only: they do not create meshes, do not export, do not enter the B-rep path, and do not add face labels. Do not use viewport labels to compensate for unclear geometry in the final model.\n\n## SDF Modeling\n\nFor organic shapes, smooth blending, TPMS lattices, and surface deformations. Return `SdfShape` values directly, or return a plain object/array tree of SDF leaves, for native raymarch preview. Use `.toShape()` or `toShape(...)` only when you need mesh-backed CAD/export behavior. See [sdf-primitives.md](sdf-primitives.md).\n\n---\n\n<!-- generated/core.md -->\n\n# Core API\n\n3D primitives, boolean operations, transforms, patterns, imports, and parameters.\n\n## Contents\n\n- [3D Primitives](#3d-primitives) — `box`, `cylinder`, `sphere`, `torus`\n- [Boolean Operations](#boolean-operations) — `union`, `difference`, `intersection`\n- [Edge Features](#edge-features) — `fillet`, `chamfer`, `draft`, `offsetSolid`\n- [Patterns & Layout](#patterns-layout) — `circularLayout`, `polygonVertices`, `linearPattern`, `circularPattern`, `linearPattern2d`, `circularPattern2d`, `mirrorCopy`, `selectEdges`, `selectEdge`, `coalesceEdges`\n- [Imports & Composition](#imports-composition) — `require`, `importSvgSketch`, `importMesh`, `importStep`\n- [Parameters](#parameters) — `Param.number`, `Param.string`, `Param.bool`, `Param.choice`, `Param.list`\n- [Grouping & Local Coordinates](#grouping-local-coordinates) — `group`\n- [Section & Projection](#section-projection) — `intersectWithPlane`, `faceProfile`, `projectToPlane`\n- [Transforms](#transforms) — `composeChain`\n- [Verification](#verification) — `verify.that`, `verify.equal`, `verify.notEqual`, `verify.greaterThan`, `verify.lessThan`, `verify.inRange`, `verify.centersCoincide`, `verify.connectorDistance`, `verify.physicalComponentCount`, `verify.intentionalOverlap`, `verify.notColliding`, `verify.minClearance`, `verify.clearanceBetween`, `verify.parallel`, `verify.perpendicular`, `verify.coplanar`, `verify.faceAt`, `verify.sameDirection`, `verify.isEmpty`, `verify.notEmpty`, `verify.volumeApprox`, `verify.areaApprox`, `verify.boundingBoxSize`, `verify.edgeContinuity`, `verify.noTinyEdges`, `verify.noSliverFaces`, `verify.noSelfIntersection`, `spec`\n- [Shape](#shape) — Appearance, Face Topology, Edge Topology, Transforms, Booleans & Cutting, Features, Placement, Connectors, References, Measurement\n- [Transform](#transform)\n- [ShapeGroup](#shapegroup) — Children, Transforms, Placement, Connectors, References\n- [SurfacePattern](#surfacepattern)\n- [Pattern2D](#pattern2d)\n- [Pattern2DBuilder](#pattern2dbuilder)\n- [ShapeRef](#shaperef)\n- [ANCHOR3D_NAMES](#anchor3d-names)\n- [verify](#verify)\n- [Constraint](#constraint)\n- [Points](#points)\n- [connector](#connector)\n\n## Functions\n\n### 3D Primitives\n\n#### `box()` — Create a rectangular box. Centered on XY, base at Z=0.\n\nExtents:\n\n- X: `[-width/2, width/2]`\n- Y: `[-depth/2, depth/2]`\n- Z: `[0, height]`\n\nFor named faces, build from a labeled sketch: `rect(width, depth).labelEdges('s', 'e', 'n', 'w').extrude(height, { labels: { start: 'bottom', end: 'top' } })`.\n\n```ts\nbox(width: number, depth: number, height: number): Shape\n```\n\n#### `cylinder()` — Create a cylinder or cone with named faces and edges. Centered on XY, base at Z=0.\n\nExtents:\n\n- X/Y: centered at the origin\n- Z: `[0, height]`\n\n`radiusTop` defaults to `radius`. Set `radiusTop` smaller to taper the side, or `0` for a pointy cone. Use `segments` to create regular prisms (for example `6` for a hexagonal prism).\n\nNamed faces: `top`, `bottom`, `side` Named edges: `top-rim`, `bottom-rim`\n\n```ts\ncylinder(height: number, radius: number, radiusTop?: number, segments?: number): Shape\n```\n\n#### `sphere()` — Create a sphere centered at the origin.\n\nExtents:\n\n- X: `[-radius, radius]`\n- Y: `[-radius, radius]`\n- Z: `[-radius, radius]`\n\nUse `segments` for lower-poly approximations.\n\n```ts\nsphere(radius: number, segments?: number): Shape\n```\n\n#### `torus()` — Create a torus (donut shape) lying in the XY plane. Centered on all axes.\n\nExtents:\n\n- X: `[-(majorRadius + minorRadius), +(majorRadius + minorRadius)]`\n- Y: `[-(majorRadius + minorRadius), +(majorRadius + minorRadius)]`\n- Z: `[-minorRadius, minorRadius]`\n\nThe origin is the center of the ring.\n\n```ts\ntorus(majorRadius: number, minorRadius: number, segments?: number): Shape\n```\n\n### Boolean Operations\n\n#### `union()` — Combine shapes into a single solid (additive boolean).\n\nAccepts individual shapes, or an array of shapes. `union()` returns one solid, so only the first operand's color is preserved in the result. Use `group()` when you want separate child colors or identities.\n\n```ts\nunion(...inputs: ShapeOperandInput[]): Shape\n```\n\n#### `difference()` — Subtract shapes from a base shape (subtractive boolean).\n\nThe first shape is the base; all subsequent shapes are subtracted from it. Accepts individual shapes, or an array of shapes.\n\n```ts\ndifference(...inputs: ShapeOperandInput[]): Shape\n```\n\n#### `intersection()` — Keep only the overlapping volume of the input shapes (intersection boolean).\n\nRequires at least two shapes. Accepts individual shapes, or an array.\n\n```ts\nintersection(...inputs: ShapeOperandInput[]): Shape\n```\n\n### Edge Features\n\n#### `fillet()` — Apply experimental fillets (rounded edges) to one or more edges of a shape.\n\n**Experimental**: fillets are still backend-sensitive. The Manifold backend is known to produce incorrect results for some edge-finish cases, and the OCCT backend can be very slow, especially with broad edge selections. Prefer targeted edge selectors and inspect the result before treating it as production-ready geometry.\n\nEdge selections compile into backend operations; unsupported selections fail as explicit kernel gaps instead of using TypeScript geometry fallbacks.\n\nThe `edges` parameter is flexible:\n\n- Omit to fillet **all** sharp edges\n- Pass an `EdgeQuery` for an inline filter (most common)\n- Pass an `EdgeSegment` or `EdgeSegment[]` from `selectEdges()` for pre-selected edges\n\nThrows if no edges match the selection, or if `radius` is not a positive finite number.\n\n```ts\n// Fillet all edges\nfillet(myShape, 2)\n\n// Fillet only top convex edges\nfillet(myShape, 1.5, { atZ: 20, convex: true })\n\n// Fillet vertical edges selected beforehand\nconst edges = selectEdges(myShape, { parallel: [0, 0, 1] })\nfillet(myShape, 3, edges)\n```\n\n```ts\nfillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape\n```\n\n#### `chamfer()` — Apply experimental chamfers (beveled edges) to one or more edges of a shape.\n\n**Experimental**: chamfers are still backend-sensitive. The Manifold backend is known to produce incorrect results for some edge-finish cases, and the OCCT backend can be very slow, especially with broad edge selections. Prefer targeted edge selectors and inspect the result before treating it as production-ready geometry.\n\nProduces a 45° bevel at the specified `size` (distance from edge). Edge selections compile into backend operations; unsupported selections fail as explicit kernel gaps instead of using TypeScript geometry fallbacks.\n\nThe `edges` parameter accepts the same options as `fillet()`: inline `EdgeQuery`, pre-selected `EdgeSegment`/`EdgeSegment[]`, or `undefined` (all sharp edges).\n\n```ts\n// Chamfer all edges\nchamfer(myShape, 1)\n\n// Chamfer only vertical edges\nchamfer(myShape, 2, { parallel: [0, 0, 1] })\n```\n\n```ts\nchamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape\n```\n\n#### `draft()` — Apply a draft angle (taper) to vertical faces for mold extraction.\n\nAdds a taper angle to the vertical faces of a solid so that it can be extracted from a mold. The neutral plane is the Z position where the draft angle is zero — faces above and below are tapered symmetrically. Typical values for injection molding are 1–5°.\n\nTruck supports vertical-prism solids with Z-axis pull directions. OCCT uses its native draft operation when available. Manifold throws.\n\n```ts\n// Add 3° draft to a box for injection molding\ndraft(myBox, 3)\n\n// Draft with custom pull direction and neutral plane\ndraft(myShape, 2, [0, 0, 1], 10)\n```\n\n```ts\ndraft(shape: Shape, angleDeg: number, pullDirection?: [ number, number, number ], neutralPlaneOffset?: number): Shape\n```\n\n#### `offsetSolid()` — Uniformly offset all surfaces of a solid inward or outward.\n\nUnlike `shell()`, which hollows a solid by removing one face, `offsetSolid()` produces a new solid whose every surface is shifted by `thickness`. Positive values grow the shape outward; negative values shrink it inward.\n\nRequires the OCCT backend. Throws on Manifold.\n\n```ts\n// Grow a box outward by 1mm on all sides\noffsetSolid(myBox, 1)\n\n// Shrink a shape inward by 0.5mm\noffsetSolid(myShape, -0.5)\n```\n\n```ts\noffsetSolid(shape: Shape, thickness: number): Shape\n```\n\n### Patterns & Layout\n\n#### `circularLayout()` — Compute evenly-spaced positions around a circle.\n\nEliminates the most common trig pattern in CAD scripts:\n\n```js\n// Before — manual trig\nfor (let i = 0; i < 12; i++) {\n const angle = i * 30 * Math.PI / 180;\n markers.push(marker.translate(r * Math.cos(angle), r * Math.sin(angle), 0));\n}\n\n// After — declarative\nfor (const {x, y} of circularLayout(12, r)) {\n markers.push(marker.translate(x, y, 0));\n}\n```\n\n```ts\ncircularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[]\n```\n\n**`CircularLayoutOptions`**\n- `startDeg?: number` — Angle of the first element in degrees (default: 0 = +X axis).\n- `centerX?: number` — Center X coordinate (default: 0).\n- `centerY?: number` — Center Y coordinate (default: 0).\n\n`LayoutPoint`: `{ x: number, y: number }`\n\n#### `polygonVertices()` — Compute the vertex positions of a regular polygon.\n\nDefault orientation places the first vertex at the top (90 degrees), matching the convention used by [`ngon()`](/docs/sketch#ngon).\n\nEliminates manual Math.sqrt(3) for triangles, pentagon vertex math, etc:\n\n```js\n// Before — manual equilateral triangle\nconst v1 = [center.x - r/2, center.y + r * Math.sqrt(3)/2];\nconst v2 = [center.x - r/2, center.y - r * Math.sqrt(3)/2];\nconst v3 = [center.x + r, center.y];\n\n// After — declarative\nconst [v1, v2, v3] = polygonVertices(3, r);\n```\n\n```ts\npolygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[]\n```\n\n**`PolygonVerticesOptions`**\n- `startDeg?: number` — Angle of the first vertex in degrees (default: 90 = top).\n- `centerX?: number` — Center X coordinate (default: 0).\n- `centerY?: number` — Center Y coordinate (default: 0).\n\n#### `linearPattern()` — Repeat a shape in a linear pattern along a direction vector and union the copies.\n\nCreates `count` copies of `shape`, each offset by `(dx*i, dy*i, dz*i)` from the original. All copies are unioned into a single `Shape`. Distinct compiler ownership is assigned to each copy so face identity via owner-scoped canonical queries still works post-merge.\n\n```ts\n// 5 cylinders, 20mm apart along X\nlinearPattern(cylinder(10, 3), 5, 20, 0)\n```\n\n```ts\nlinearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape\n```\n\n#### `circularPattern()` — Repeat a shape in a circular pattern around an axis and union the copies.\n\nDistributes `count` copies evenly around the rotation axis (360° / count per step). All copies are unioned into a single `Shape`. Distinct compiler ownership is assigned to each copy — post-merge face identity via owner-scoped canonical queries still works for pattern descendants.\n\nTwo calling conventions:\n\n- **Simple** (Z axis): `circularPattern(shape, 6)` or `circularPattern(shape, 6, centerX, centerY)`\n- **Advanced** (arbitrary axis): `circularPattern(shape, 6, { axis, origin })`\n\n```ts\n// 8 holes evenly spaced around origin\ncircularPattern(cylinder(12, 4).translate(30, 0, -1), 8)\n\n// Circular pattern around X axis\ncircularPattern(myFeature, 4, { axis: [1, 0, 0], origin: [0, 0, 50] })\n```\n\n```ts\ncircularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape\n```\n\n**`CircularPatternOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `centerX?` | `number` | Center X of the rotation (default: 0). Used when axis is Z (legacy mode). |\n| `centerY?` | `number` | Center Y of the rotation (default: 0). Used when axis is Z (legacy mode). |\n| `axis?` | `[ number, number, number ]` | Rotation axis direction (default: [0, 0, 1] = Z axis). |\n| `origin?` | `[ number, number, number ]` | Pivot point for the rotation (default: [0, 0, 0]). Overrides centerX/centerY when set. |\n\n#### `linearPattern2d()` — Repeat a 2D sketch in a linear pattern and union the copies.\n\n```ts\nlinearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch\n```\n\n#### `circularPattern2d()` — Repeat a 2D sketch in a circular pattern around a center point and union the copies.\n\n```ts\ncircularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | { centerX?: number; centerY?: number; startDeg?: number; }, centerY?: number): Sketch\n```\n\n#### `mirrorCopy()` — Mirror a shape across a plane and union the mirror with the original.\n\nThe mirror plane passes through the origin and is defined by its normal vector. The mirrored copy is unioned with the original to produce a single symmetric Shape.\n\n```ts\n// Mirror across the YZ plane (X=0)\nmirrorCopy(box(50, 30, 10), [1, 0, 0])\n```\n\n```ts\nmirrorCopy(shape: Shape, normal: [ number, number, number ]): Shape\n```\n\n#### `selectEdges()` — Select all edges from a shape that match the given query.\n\nUses the active kernel's native topology query when available (Truck), otherwise extracts sharp edges from the mesh (dihedral angle > 1°), applies all filters in the query, and returns the matching `EdgeSegment[]`. When `near` is specified the results are sorted closest-first.\n\nWorks on any shape — primitives, booleans, shells, and imported meshes. Use this when tracked topology is unavailable (e.g. after a difference or on imported geometry). For simpler cases, pass an `EdgeQuery` directly to `fillet()` or `chamfer()` instead of calling `selectEdges` separately.\n\n```ts\n// Fillet all top edges of a box\nconst topEdges = selectEdges(part, { atZ: 20, perpendicular: [0, 0, 1] });\nlet result = part;\nfor (const edge of coalesceEdges(topEdges)) {\n result = fillet(result, 2, edge);\n}\n```\n\n```ts\nselectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]\n```\n\n**`EdgeQuery`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `near?` | `Vec3` | Sort by proximity to this point (closest first). When used with `selectEdge`, picks the closest match. |\n| `parallel?` | `Vec3` | Filter: edge direction approximately parallel to this vector. |\n| `perpendicular?` | `Vec3` | Filter: edge direction approximately perpendicular to this vector. |\n| `convex?` | `boolean` | Filter: only convex (outside corner) edges. |\n| `concave?` | `boolean` | Filter: only concave (inside corner) edges. |\n| `minAngle?` | `number` | Filter: minimum dihedral angle in degrees. |\n| `maxAngle?` | `number` | Filter: maximum dihedral angle in degrees. |\n| `minLength?` | `number` | Filter: minimum edge length. |\n| `maxLength?` | `number` | Filter: maximum edge length. |\n| `within?` | `BoundingRegion` | Filter: edge midpoint must be within this bounding region. |\n| `atZ?` | `number` | Shorthand: edge midpoint Z ≈ this value (within `tolerance`). Equivalent to `within: { zMin: atZ - tol, zMax: atZ + tol }`. |\n| `tolerance?` | `number` | Position tolerance for approximate matches (default: `1.0`). Used by `atZ` and `near`. |\n| `angleTolerance?` | `number` | Angular tolerance in degrees for `parallel`/`perpendicular` filters (default: `10`). |\n\n`BoundingRegion`: `{ xMin?: number, xMax?: number, yMin?: number, yMax?: number, zMin?: number, zMax?: number }`\n\n**`EdgeSegment`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |\n| `direction` | `Vec3` | Normalized direction from start → end. |\n| `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |\n| `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |\n| `normalA` | `Vec3` | Normal of first adjacent face. |\n| `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |\n| `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |\n| `start`, `end`, `midpoint`, `length` | | — |\n\n#### `selectEdge()` — Select the single best-matching edge from a shape.\n\nWhen `near` is specified, returns the edge whose midpoint is closest to that point. Otherwise returns the first matching edge in mesh order. Throws if no edges match the query — useful as a guard when you expect exactly one result.\n\n```ts\n// Chamfer one specific edge near a known point\nconst bottomEdge = selectEdge(part, { near: [25, 0, 0], atZ: 0 });\nresult = chamfer(result, 1.5, bottomEdge);\n```\n\n```ts\nselectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment\n```\n\n#### `coalesceEdges()` — Merge collinear edge segments into longer logical edges.\n\nTessellation often splits one geometric edge into multiple short segments. `coalesceEdges` groups adjacent collinear segments and merges each group into a single `EdgeSegment` spanning the full extent. This is usually needed before passing edges to `fillet()` or `chamfer()` on non-primitive shapes.\n\nThe `tolerance` controls the maximum perpendicular distance from collinearity before two segments are considered non-collinear. Default: `0.01`.\n\n```ts\nconst topEdges = selectEdges(part, { atZ: 20 });\nfor (const edge of coalesceEdges(topEdges)) {\n result = fillet(result, 2, edge);\n}\n```\n\n```ts\ncoalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]\n```\n\n### Imports & Composition\n\n#### `require()` — Import a module with optional ForgeCAD parameter overrides. Returns the module's exports.\n\nWhen importing a `.forge.js` file, the return value is what the script returns. If the script returns a metadata object (e.g. `{ shape: myShape, bolts: {...} }`), the caller receives the full object — renderable values and metadata together.\n\n**Path rule:** Always include the file extension in relative imports: use `require(\"./part.forge.js\")` for model files and `require(\"./helpers.js\")` for plain helper modules. ForgeCAD does not apply Node-style extension inference, so `require(\"./part\")` will not find `part.forge.js` or `part.js`.\n\n**Parameter scoping:** Parameters declared in required files are automatically namespaced with a `\"filename#N / \"` prefix (e.g. `\"bracket.forge.js#1 / Width\"`). This prevents collisions when multiple files declare same-named params. Each file's params appear as separate sliders.\n\n**Parameter overrides:** When passing overrides, use the bare param name (not the scoped name). Overrides are type-checked — unrecognized keys throw an error with typo suggestions.\n\n**Multi-file assembly pattern** — pass cross-cutting design values from the assembly to parts:\n\n```js\n// assembly.forge.js — owns cross-cutting params, passes to parts\nconst wall = param(\"Wall\", 3);\nconst baseH = param(\"Base Height\", 20);\n\nconst mount = require('./motor-mount.forge.js', { Wall: wall });\nconst base = require('./base-body.forge.js', { Wall: wall, Height: baseH });\n```\n\n**Metadata pattern** — parts publish interface data alongside geometry:\n\n```js\n// motor-mount.forge.js\nreturn { shape: mount, bolts: { dia: 5.3, pos: holePositions } };\n\n// base-body.forge.js\nconst mount = require('./motor-mount.forge.js');\nmount.bolts.pos // access the metadata\nmount.shape // access the geometry\n```\n\n```ts\nrequire(path: string, paramOverrides?: Record<string, number | string>): any\n```\n\n#### `importSvgSketch()` — Parse an SVG file and return it as a Sketch with options for region filtering, scaling, and simplification.\n\n```ts\nimportSvgSketch(fileName: string, options?: SvgImportOptions): Sketch\n```\n\n**`SvgImportOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `include?` | `\"auto\" \\| \"fill\" \\| \"stroke\" \\| \"fill-and-stroke\"` | Which geometry channels to include: - `auto`: prefer fills; if no fill geometry exists, fall back to strokes - `fill`: import only filled regions - `stroke`: import only stroke geometry - `fill-and-stroke`: include both |\n| `regionSelection?` | `\"all\" \\| \"largest\"` | Keep all disconnected regions, or only the largest. |\n| `maxRegions?` | `number` | Keep at most this many regions (largest-first). |\n| `minRegionArea?` | `number` | Drop regions below this absolute area threshold. |\n| `minRegionAreaRatio?` | `number` | Drop regions below this ratio of largest-region area. |\n| `flattenTolerance?` | `number` | Curve flattening tolerance in SVG user units. Smaller = more segments, higher fidelity. |\n| `arcSegments?` | `number` | Minimum segment count for arc discretization. |\n| `scale?` | `number` | Global scale applied after SVG parsing. |\n| `maxWidth?` | `number` | Maximum imported sketch width. If exceeded, geometry is uniformly downscaled to fit. |\n| `maxHeight?` | `number` | Maximum imported sketch height. If exceeded, geometry is uniformly downscaled to fit. |\n| `centerOnOrigin?` | `boolean` | Recenter imported geometry so its 2D bounds center is at CAD origin. |\n| `simplify?` | `number` | Simplification tolerance for final sketch cleanup. |\n| `invertY?` | `boolean` | Flip SVG Y-down coordinates to CAD Y-up. Enabled by default. |\n\n#### `importMesh()` — Import an external mesh file (STL, OBJ, 3MF) as a Shape.\n\n```ts\nimportMesh(fileName: string, options?: { scale?: number; center?: boolean; }): Shape\n```\n\n#### `importStep()` — Import a STEP file (.step, .stp) as an exact OCCT-backed Shape. Preserves NURBS curves, B-spline surfaces, and exact topology. Requires running with the OCCT backend.\n\n```ts\nimportStep(fileName: string): Shape\n```\n\n### Parameters\n\n#### `Param.number()` — Declare a numeric parameter that renders as a slider in the UI.\n\nEach call registers a slider control. When the user moves the slider the entire script re-executes with the new value. Parameter values are also overridable from `require()` imports or the CLI `--param` flag — the `name` string is the key used in both cases.\n\nDefault range rules when options are omitted:\n\n- `min` defaults to `0`\n- `max` defaults to `defaultValue * 4`\n- `step` is auto-calculated: `1` for integer params, `0.1` for ranges ≤ 100, `1` for larger ranges\n\nThe `unit` option is cosmetic only — no conversion is performed. Use `integer: true` for counts, sides, quantities (rounds to whole numbers; step defaults to `1`).\n\n```ts\nconst width = Param.number(\"Width\", 50);\nconst angle = Param.number(\"Angle\", 45, { min: 0, max: 180, unit: \"°\" });\nconst sides = Param.number(\"Sides\", 6, { min: 3, max: 12, integer: true });\n```\n\n**Parameter overrides** — key must match `name` exactly:\n\n```ts\n// Via require()\nconst bracket = require(\"./bracket.forge.js\", { Width: 80 });\n\n// Via CLI\n// forgecad run model.forge.js --param \"Wall Thickness=3\"\n```\n\nAlso available as the shorthand alias `param()`.\n\n```ts\nParam.number(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number\n```\n\n#### `Param.string()` — Declare a string parameter that renders as a text input in the UI.\n\nString parameters let users type free-form text — labels, names, inscriptions, file paths, etc. The `name` string is the override key.\n\n```ts\nconst label = Param.string(\"Label\", \"Hello World\");\nconst name = Param.string(\"Name\", \"Part-001\", { maxLength: 20 });\n```\n\nOverride via import:\n\n```ts\nconst tag = require(\"./tag.forge.js\", { Label: \"Custom Text\" });\n```\n\nOnly available as `Param.string()` — no standalone alias.\n\n```ts\nParam.string(name: string, defaultValue: string, opts?: { maxLength?: number; }): string\n```\n\n#### `Param.bool()` — Declare a boolean parameter that renders as a checkbox in the UI.\n\nInternally stored as `0`/`1`. When overriding from CLI or `require()`, pass `1` for true and `0` for false. The `name` string is the override key.\n\n```ts\nconst showHoles = Param.bool(\"Show Holes\", true);\nif (showHoles) return difference(plate, cylinder(10, 5).translate(50, 30, 0));\nreturn plate;\n```\n\nOverride via import:\n\n```ts\nconst pan = require(\"./pan.forge.js\", { \"Show Lid\": 0 });\n```\n\nAlso available as the shorthand alias `boolParam()`.\n\n```ts\nParam.bool(name: string, defaultValue: boolean): boolean\n```\n\n#### `Param.choice()` — Declare a choice parameter that renders as a dropdown in the UI.\n\n`defaultValue` must exactly match one entry in `choices`. Returns the selected string label. Prefer `Param.choice` over `Param.number` when a slider would hide intent — named choices like `\"wok\"` are self-describing.\n\nOverrides may be passed as the choice label string (preferred) or as a numeric index. The `name` string is the override key.\n\n```ts\nconst panStyle = Param.choice(\"Pan Style\", \"frying-pan\", [\"frying-pan\", \"saute-pan\", \"wok\"]);\nif (panStyle === \"wok\") return buildWok();\n```\n\nOverride via import:\n\n```ts\nconst pan = require(\"./pan.forge.js\", { \"Pan Style\": \"wok\" });\n```\n\nOverride via CLI:\n\n```bash\nforgecad run model.forge.js --param \"Pan Style=wok\"\n```\n\nAlso available as the shorthand alias `choiceParam()`.\n\n```ts\nParam.choice(name: string, defaultValue: string, choices: string[]): string\n```\n\n#### `Param.list()` — Declare a list parameter — an array of struct items with per-field UI controls.\n\nEach item in the list is a struct whose fields each render as their own control (slider, checkbox, or dropdown). The user can add/remove rows up to `minItems`/`maxItems` bounds.\n\nField types:\n\n- Boolean fields (`boolean: true` in field defs) return as `boolean`\n- Choice fields (`choices: [...]` in field defs) return as `string`\n- All other fields return as `number`\n\n```ts\nParam.list<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: { ... }): T[]\n```\n\n`ListParamFieldDef`: `{ min?: number, max?: number, step?: number, unit?: string, integer?: boolean, boolean?: boolean, choices?: string[] }`\n\n### Grouping & Local Coordinates\n\n#### `group()` — Group multiple shapes/sketches for joint transforms without merging into a single mesh.\n\nUnlike union(), child colors and individual identities are preserved. Children can be plain shapes, named descriptors ({ name, shape/sketch/group }), or nested groups. The returned ShapeGroup supports all Shape transforms (translate, rotate, etc.).\n\nNamed descriptors can include `tags` for viewport organization. Tags do not affect geometry; they let the command palette hide, show only, or focus all objects with the same tag.\n\n**Local coordinate pattern:** Build child parts at the origin (local coordinates), then group and translate once to place the whole assembly. This eliminates the error-prone pattern of manually adding parent offsets to every sub-part.\n\n```js\nconst body = roundedBox(100, 20, 32, 4);\nconst panel = box(98, 2, 18).translate(0, -12, 4);\nconst louver = box(88, 2, 6).translate(0, -14, -11);\nconst indoorUnit = group(\n { name: 'Body', shape: body },\n { name: 'Panel', tags: 'cover', shape: panel },\n { name: 'Louver', tags: ['cover', 'moving'], shape: louver },\n).translate(0, -18, 70);\n```\n\n```ts\ngroup(...items: GroupInput[]): ShapeGroup\n```\n\n### Section & Projection\n\n#### `intersectWithPlane()` — Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch.\n\n```ts\nintersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch\n```\n\n#### `faceProfile()` — Extract the boundary profile of a named face as a 2D sketch.\n\nThe result is returned in the face's local 2D coordinate system, making it convenient for offsets, pocket profiles, or follow-up sketch operations driven by an existing face.\n\n```ts\nfaceProfile(shape: Shape, face: FaceSelector): Sketch\n```\n\n#### `projectToPlane()` — Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch.\n\n```ts\nprojectToPlane(shape: Shape, plane: PlaneSpec): Sketch\n```\n\n### Transforms\n\n#### `composeChain()` — Compose transforms in chain order. Equivalent to Transform.identity().mul(a).mul(b).mul(c)...\n\n```ts\ncomposeChain(...steps: TransformInput[]): Transform\n```\n\n### Verification\n\n#### `verify.that()` — Custom predicate check.\n\n```ts\nverify.that(label: string, check: () => boolean, message?: string): void\n```\n\n#### `verify.equal()` — Check that two numbers are approximately equal (within tolerance).\n\n```ts\nverify.equal(label: string, actual: number, expected: number, tolerance?: number, message?: string): void\n```\n\n#### `verify.notEqual()` — Check that two numbers are NOT equal (differ by more than tolerance).\n\n```ts\nverify.notEqual(label: string, actual: number, unexpected: number, tolerance?: number, message?: string): void\n```\n\n#### `verify.greaterThan()` — Check that actual > min.\n\n```ts\nverify.greaterThan(label: string, actual: number, min: number, message?: string): void\n```\n\n#### `verify.lessThan()` — Check that actual < max.\n\n```ts\nverify.lessThan(label: string, actual: number, max: number, message?: string): void\n```\n\n#### `verify.inRange()` — Check that min <= actual <= max.\n\n```ts\nverify.inRange(label: string, actual: number, min: number, max: number, message?: string): void\n```\n\n#### `verify.centersCoincide()` — Check that the bounding-box centers of two shapes coincide within tolerance (mm).\n\n```ts\nverify.centersCoincide(label: string, a: ShapeLike, b: ShapeLike, tolerance?: number): void\n```\n\n`ShapeLike`: `{ min: number[], max: number[] }`\n\n#### `verify.connectorDistance()` — Check the distance between two named connectors on a shape or group.\n\nUse this when connectors + `matchTo()` define a static assembly interface. It proves the mate at runtime, unlike a plain source-level connector declaration. The common case is `expected = 0`, meaning the two connector origins should coincide after placement.\n\n```ts\nverify.connectorDistance(\"leg is seated\", bench, \"Rail.leg_0\", \"Leg0.head\", 0, 0.01);\n```\n\n```ts\nverify.connectorDistance(label: string, target: ConnectorDistanceLike, connectorA: string, connectorB: string, expected?: number, tolerance?: number): void\n```\n\n#### `verify.physicalComponentCount()` — Declare the expected physical connectivity component count for the returned visible model.\n\nUse this for generated mechanical models that should have a clear component graph: one connected fixture, a purchased part plus a removable cartridge, a root assembly plus named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned visible objects with the same physical-connectivity analysis used in the quality gate and fails if the actual component count differs.\n\nThis catches the common generated-CAD failure where a script returns a visually plausible artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.\n\n```ts\nverify.physicalComponentCount(\"vise is one connected installed assembly\", 1);\n```\n\n```ts\nverify.physicalComponentCount(label: string, expected: number): void\n```\n\n#### `verify.intentionalOverlap()` — Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.\n\nUse this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume: welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers without pockets, or parts placed with collision as a positioning hack.\n\n`forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are returned as visible objects and the exact collision report finds that same object pair. Unused or non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.\n\n```ts\nverify.intentionalOverlap(\"rubber grip is overmolded on handle\", rubberGrip, handleCore, \"overmolded insert\");\n```\n\n```ts\nverify.intentionalOverlap(label: string, a: ShapeLike, b: ShapeLike, reason: string): void\n```\n\n#### `verify.notColliding()` — Check that two shapes do not collide (minGap > 0).\n\n```ts\nverify.notColliding(label: string, a: ShapeLike, b: ShapeLike, searchLength?: number): void\n```\n\n#### `verify.minClearance()` — Check that a minimum clearance gap exists between two shapes.\n\n```ts\nverify.minClearance(label: string, a: ShapeLike, b: ShapeLike, minGap: number, searchLength?: number): void\n```\n\n#### `verify.clearanceBetween()` — Check that the clearance gap between two shapes is inside an allowed range.\n\nUse this for seated and retained interfaces where a part must be close enough to be mechanically accountable, but must not collide beyond the allowed minimum. It catches both failure modes that make generated CAD look fake: parts floating away from their receiver, and parts intersecting their receiver because the pocket, bore, or running clearance was not modeled.\n\nFor contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny numerical noise. For a running fit, use the intended clearance band.\n\nManifold-backed shapes use exact min-gap distance. Other backends use a mesh-derived min-gap check and say so in the verification message; keep `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for positive-volume interference.\n\n```ts\nverify.clearanceBetween(\"cover is seated on gasket\", cover, gasket, -0.01, 0.05);\nverify.clearanceBetween(\"carriage runs inside rail\", carriage, rail, 0.2, 0.5);\n```\n\n```ts\nverify.clearanceBetween(label: string, a: ShapeLike, b: ShapeLike, minGap: number, maxGap: number, searchLength?: number): void\n```\n\n#### `verify.parallel()` — Check that two face normals are parallel (within toleranceDeg degrees).\n\n```ts\nverify.parallel(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void\n```\n\n`FaceRefLike`: `{ normal: [ number, number, number ], center: [ number, number, number ] }`\n\n#### `verify.perpendicular()` — Check that two face normals are perpendicular (within toleranceDeg degrees).\n\n```ts\nverify.perpendicular(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void\n```\n\n#### `verify.coplanar()` — Check that a face is coplanar with (same plane as) another face, meaning they are parallel AND their centers lie on the same plane.\n\n```ts\nverify.coplanar(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number, toleranceMm?: number): void\n```\n\n#### `verify.faceAt()` — Check that a face center lies at a specific position (within toleranceMm).\n\n```ts\nverify.faceAt(label: string, face: FaceRefLike, expectedPos: [ number, number, number ], toleranceMm?: number): void\n```\n\n#### `verify.sameDirection()` — Check that two face normals point in the same direction (not antiparallel). Stricter than parallel — both |angle| AND sign must match.\n\n```ts\nverify.sameDirection(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void\n```\n\n#### `verify.isEmpty()` — Check that a shape is empty.\n\n```ts\nverify.isEmpty(label: string, shape: ShapeLike, message?: string): void\n```\n\n#### `verify.notEmpty()` — Check that a shape is NOT empty.\n\n```ts\nverify.notEmpty(label: string, shape: ShapeLike, message?: string): void\n```\n\n#### `verify.volumeApprox()` — Check that a shape's volume is approximately equal to expected (mm³).\n\n```ts\nverify.volumeApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void\n```\n\n#### `verify.areaApprox()` — Check that a shape's surface area is approximately equal to expected (mm²).\n\n```ts\nverify.areaApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void\n```\n\n#### `verify.boundingBoxSize()` — Check that a shape's bounding box has approximately the given size.\n\n```ts\nverify.boundingBoxSize(label: string, shape: ShapeLike, expectedSize: [ number, number, number ], tolerance?: number): void\n```\n\n#### `verify.edgeContinuity()` — Check that every sampled seam on a shape meets a requested continuity threshold.\n\n```ts\nverify.edgeContinuity(label: string, shape: ShapeLike, options?: EdgeContinuityThresholds): void\n```\n\n**`EdgeContinuityThresholds`**: `continuity?: SurfaceContinuity`, `samples?: number`, `positionTolerance?: number`, `tangentToleranceDeg?: number`, `curvatureTolerance?: number`\n\n#### `verify.noTinyEdges()` — Check that a shape has no tiny edges below the requested threshold.\n\n```ts\nverify.noTinyEdges(label: string, shape: ShapeLike, threshold?: number): void\n```\n\n#### `verify.noSliverFaces()` — Check that a shape has no sliver faces below the requested score threshold.\n\n```ts\nverify.noSliverFaces(label: string, shape: ShapeLike, threshold?: number): void\n```\n\n#### `verify.noSelfIntersection()` — Best-effort exact-shape validity guard for self-intersections or broken B-Rep topology.\n\n```ts\nverify.noSelfIntersection(label: string, shape: ShapeLike): void\n```\n\n#### `spec()` — Create a named, reusable bundle of verification checks.\n\nA spec groups related `verify.*` calls under a collapsible header in the Checks panel. This makes large check suites scannable. Specs can be applied to multiple shapes and can check relationships between parts.\n\nSpecs can be defined in separate `.forge.js` files and imported via `require()` to share them across models.\n\n`spec.check()` returns a `SpecResult` — you can inspect it programmatically or ignore the return value and let the Checks panel show results.\n\n```ts\nconst printable = spec(\"Fits printer bed\", (shape) => {\n verify.notEmpty(\"Has geometry\", shape);\n const bb = shape.boundingBox();\n verify.lessThan(\"Width < 220mm\", bb.max[0] - bb.min[0], 220);\n verify.lessThan(\"Depth < 220mm\", bb.max[1] - bb.min[1], 220);\n verify.lessThan(\"Height < 250mm\", bb.max[2] - bb.min[2], 250);\n});\n\n// Reuse on multiple shapes\nprintable.check(bracket);\nprintable.check(standoff);\n\n// Check relationships between parts\nconst fitSpec = spec(\"Assembly fit\", (partA, partB) => {\n verify.notColliding(\"No interference\", partA, partB, 10);\n});\nfitSpec.check(bracket, standoff);\n```\n\n**Spec-first workflow:** Write specs before building geometry. Checks go from red to green as you build — effectively TDD for CAD.\n\n```ts\nspec(name: string, checkFn: (...args: any[]) => void): Spec\n```\n\n**`Spec`**\n- `name: string` — The display name of this spec\n\n---\n\n## Classes\n\n### `Shape`\n\nCore 3D solid shape. All operations are immutable and return new shapes.\n\nSupports transforms (translate, rotate, scale, mirror, transform, rotateAround, pointAlong), booleans (add, subtract, intersect), cutting (split, splitByPlane, trimByPlane), shelling, anchor positioning (attachTo, onFace), placement references, and queries (volume, surfaceArea, boundingBox, isEmpty, numTri, geometryInfo).\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `materialProps` | `ShapeMaterialProps | undefined` | — |\n\n**Appearance**\n\n#### `color()` — Set the color of this shape (hex string, e.g. \"#ff0000\"). Returns a new Shape with the color applied.\n\n```ts\ncolor(value: string | undefined): Shape\n```\n\n#### `material()` — Set PBR material properties for this shape's visual appearance.\n\nReturns a new Shape with the specified material properties merged on top of any previously set properties. All properties are optional — omitted keys retain their current value. Material properties survive transforms and boolean operations.\n\nUse `.color()` to set the base diffuse color; `.material()` controls how that color behaves under light (metalness, roughness, clearcoat) and can add emissive glow independent of lighting. Emissive glow pairs naturally with the `postProcessing.bloom` effect in [`scene()`](/docs/viewport#scene).\n\n```js\nbox(50, 50, 50).material({ metalness: 0.9, roughness: 0.1 }); // polished metal\nsphere(30).material({ emissive: '#ff6b35', emissiveIntensity: 2 }); // glowing\ncylinder(40, 20).material({ opacity: 0.4, clearcoat: 1.0, clearcoatRoughness: 0.02 }); // ice\n\n// Chainable with other shape methods\nbox(100, 100, 10).color('#gold').material({ metalness: 0.95, roughness: 0.05 }).translate(0, 0, 50);\n```\n\n```ts\nmaterial(props: ShapeMaterialProps): Shape\n```\n\n**Face Topology**\n\n#### `face()` — Resolve a face by user-authored label or compiler-owned name. Returns a `FaceRef` that can be passed to `.onFace()`, `projectToPlane()`, or used directly in placement.\n\n`.face(name)` is a pure label lookup — it finds faces by user-authored labels, not by geometric queries. Labels are born in sketches via `.label()` / `.labelEdges()` and grow into face names through extrude, loft, revolve, and sweep. They are stable references that travel with the geometry.\n\nLabels must be unique within a shape. Use `.prefixLabels()` before combining shapes with `union()` / `difference()` to avoid collisions. Collision detection throws a clear error with a fix suggestion.\n\nFor compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape's compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops) are resolved via coplanar triangle clustering.\n\n```ts\n// Edge labels become side face names after extrude\nconst profile = path()\n .moveTo(0, 0)\n .lineTo(100, 0).label('floor')\n .lineTo(100, 50).label('wall')\n .lineTo(0, 50).label('ceiling')\n .closeLabel('left-wall');\nconst room = profile.extrude(30, { labels: { start: 'base', end: 'top' } });\nroom.face('floor'); // side face from the labeled edge\nroom.face('base'); // base cap (user-specified)\n\n// .labelEdges() shorthand for sequential edge labeling\nconst plate = rect(100, 50).labelEdges('south', 'east', 'north', 'west');\nconst solid = plate.extrude(20, { labels: { start: 'bottom', end: 'top' } });\nsolid.face('south'); // side face\n\n// Prefix before combining to avoid collisions\nconst left = wing.prefixLabels('l/');\nconst right = wing.mirror([1, 0, 0]).prefixLabels('r/');\nconst full = union(left, right);\nfull.face('l/upper'); // left wing upper surface\n```\n\n```ts\nface(selector: FaceSelector): FaceRef\n```\n\n#### `faces()` — Return faces matching a query, or label semantic faces when passed a mapping.\n\nMapping form returns a new shape: `shape.faces({ lid: 'top', walls: ['front', 'back', 'left', 'right'] })`.\n\n```ts\nfaces(): FaceRef[]\n```\n\n#### `faceNames()` — List defined semantic face names currently available on this shape.\n\n```ts\nfaceNames(): string[]\n```\n\n#### `prefixLabels()` — Prefix all user-authored face labels, including semantic labels from `faces(mapping)`. Returns a new shape with modified labels.\n\n```ts\nprefixLabels(prefix: string): Shape\n```\n\n#### `renameLabel()` — Rename a single face label. Returns a new shape.\n\n```ts\nrenameLabel(from: string, to: string): Shape\n```\n\n#### `dropLabels()` — Remove specific face labels. Returns a new shape.\n\n```ts\ndropLabels(...names: string[]): Shape\n```\n\n#### `dropAllLabels()` — Remove all face labels. Returns a new shape.\n\n```ts\ndropAllLabels(): Shape\n```\n\n#### `faceHistory()` — Get the transformation history for a specific face.\n\n```ts\nfaceHistory(name: string): FaceTransformationHistory\n```\n\n**Edge Topology**\n\n#### `edge()` — Get a named topology edge. Only available on shapes with tracked topology (from box/cylinder/extrude).\n\n```ts\nedge(name: string): EdgeRef\n```\n\n#### `edgeNames()` — List named topology edge names. Returns empty array if shape has no tracked topology.\n\n```ts\nedgeNames(): string[]\n```\n\n#### `edgesOf()` — Return all boundary edges of a named face.\n\nFinds edges where one adjacent mesh face belongs to the target face and the other belongs to a different face. The result is coalesced (tessellation fragments merged) and can be passed directly to `fillet()` or `chamfer()`.\n\nThis is a topological query — no coordinates, no tolerances, no minimum-length hacks. It works because an edge is the boundary between two faces.\n\n```js\n// Fillet all top edges of a mounting plate\nlet plate = box(120, 80, 6).faces({ workSurface: 'top' })\nplate = fillet(plate, 3, plate.edgesOf('workSurface'))\n\n// Shelled enclosure — fillet the outer lip\nlet body = box(80, 50, 35).faces({ opening: 'top' })\nbody = body.shell(2, { openFaces: ['top'] })\nbody = fillet(body, 1.5, body.edgesOf('opening'))\n\n// Filter: only concave edges (after a boolean subtraction)\nbody.edgesOf('top', { concave: true })\n```\n\n```ts\nedgesOf(faceLabel: string, options?: EdgesOfOptions): EdgeSegment[]\n```\n\n#### `edgesBetween()` — Return edges shared between two named faces.\n\nAn edge is \"between\" faces A and B when one of its adjacent mesh triangles belongs to A and the other belongs to B. This is the most precise topological edge selection — \"fillet the edges where the top meets the wall.\"\n\nThe second argument can be a single face name or an array (edges between A and any of B1, B2, ...).\n\n```js\n// Fillet the edge where lid meets one wall\nlet body = box(100, 60, 30).faces({ lid: 'top', wall: 'side-left' })\nbody = fillet(body, 2, body.edgesBetween('lid', 'wall'))\n\n// Fillet a cylinder rim — where the flat cap meets the curved barrel\nlet tube = cylinder(30, 10).faces({ cap: 'top', barrel: 'side' })\ntube = fillet(tube, 1, tube.edgesBetween('cap', 'barrel'))\n\n// Multiple target faces at once\nbody.edgesBetween('lid', ['left-wall', 'right-wall', 'front-wall', 'back-wall'])\n```\n\n```ts\nedgesBetween(faceA: string, faceB: string | string[]): EdgeSegment[]\n```\n\n**Transforms**\n\n#### `translate()` — Move the shape relative to its current position. All transforms are immutable and return new shapes.\n\n```ts\ntranslate(x: number, y: number, z: number): Shape\n```\n\n#### `translatePolar()` — Translate using polar coordinates (radius + angle in degrees). Eliminates manual `r * Math.cos(angle * PI/180)` calculations.\n\nExample: `shape.translatePolar(50, 30)` moves 50mm at 30 degrees from +X.\n\n```ts\ntranslatePolar(radius: number, angleDeg: number, z?: number): Shape\n```\n\n#### `moveTo()` — Position the shape so its bounding box min corner is at the given global coordinate.\n\n```ts\nmoveTo(x: number, y: number, z: number): Shape\n```\n\n#### `moveToLocal()` — Position the shape relative to another shape's local coordinate system (bounding box min corner).\n\n```ts\nmoveToLocal(target: Shape | { toShape(): Shape; }, x: number, y: number, z: number): Shape\n```\n\n#### `rotate()` — Rotate around an arbitrary axis through the origin.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape\n```\n\n#### `rotateX()` — Rotate around the X axis by the given angle in degrees.\n\n```ts\nrotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape\n```\n\n#### `rotateY()` — Rotate around the Y axis by the given angle in degrees.\n\n```ts\nrotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape\n```\n\n#### `rotateZ()` — Rotate around the Z axis by the given angle in degrees.\n\n```ts\nrotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape\n```\n\n#### `rotateAroundTo()` — Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point. `movingPoint` / `targetPoint` may be raw world points or this shape's anchors/references.\n\n```ts\nrotateAroundTo(axis: [ number, number, number ], pivot: [ number, number, number ], movingPoint: RotationPointLike, targetPoint: RotationPointLike, options?: RotateAroundToOptions): Shape\n```\n\n#### `transform()` — Apply a 4x4 affine transform matrix (column-major) or a Transform object.\n\n```ts\ntransform(m: Mat4 | Transform): Shape\n```\n\n#### `scale()` — Scale the shape uniformly or per-axis from the shape's bounding box center. Accepts a single number or [x, y, z] array.\n\n```ts\nscale(v: number | [ number, number, number ]): Shape\n```\n\n#### `scaleAround()` — Scale the shape uniformly or per-axis from an explicit pivot point.\n\n```ts\nscaleAround(pivot: [ number, number, number ], v: number | [ number, number, number ]): Shape\n```\n\n#### `mirror()` — Mirror across a plane through the shape's bounding box center, defined by its normal vector.\n\n```ts\nmirror(normal: [ number, number, number ]): Shape\n```\n\n#### `mirrorThrough()` — Mirror across a plane through an explicit point, defined by its normal vector.\n\n```ts\nmirrorThrough(point: [ number, number, number ], normal: [ number, number, number ]): Shape\n```\n\n#### `pointAlong()` — Reorient a shape so its primary axis (Z) points along the given direction. Useful for laying cylinders/extrusions along X or Y without thinking about Euler angles. The shape's origin stays at [0,0,0] — translate after pointAlong to position it.\n\nExample: cylinder(40, 5).pointAlong([1, 0, 0]) — lays cylinder along X, starting at origin\n\n```ts\npointAlong(direction: [ number, number, number ]): Shape\n```\n\n**Booleans & Cutting**\n\n#### `add()` — Union this shape with others (additive boolean). Method form of union().\n\n```ts\nadd(...others: ShapeOperandInput[]): Shape\n```\n\n#### `subtract()` — Subtract other shapes from this one. Method form of difference().\n\n```ts\nsubtract(...others: ShapeOperandInput[]): Shape\n```\n\n#### `intersect()` — Keep only the overlap with other shapes. Method form of intersection().\n\n```ts\nintersect(...others: ShapeOperandInput[]): Shape\n```\n\n#### `split()` — Split into [inside, outside] by another shape.\n\n```ts\nsplit(cutter: Shape | { toShape(): Shape; }): [ Shape, Shape ]\n```\n\n#### `splitByPlane()` — Split by infinite plane. Returns [positive-side, negative-side].\n\n```ts\nsplitByPlane(normal: [ number, number, number ], originOffset?: number): [ Shape, Shape ]\n```\n\n#### `trimByPlane()` — Keep the positive side of the plane and discard the opposite side.\n\n```ts\ntrimByPlane(normal: [ number, number, number ], originOffset?: number): Shape\n```\n\n**Features**\n\n#### `shell()` — Hollow out compile-covered boxes, cylinders, and straight extrudes. `openFaces` names any subset of the base shape's labeled faces to leave open (no wall).\n\n```ts\nshell(thickness: number, opts?: { openFaces?: string[]; }): Shape\n```\n\n#### `pocket()` — Cut a pocket (cavity) into this solid through the named face.\n\n```js\nbox(100, 100, 20).pocket('top', 8)\nbox(100, 100, 20).pocket('top', 8, { inset: 5 })\nbox(100, 100, 20).pocket('top', 8, { scale: 0.8 })\n```\n\n```ts\npocket(face: FaceSelector, depth: number, opts?: PocketOptions): Shape\n```\n\n#### `boss()` — Add a boss (protrusion) from the named face.\n\n```js\nbox(100, 100, 20).boss('top', 5)\nbox(100, 100, 20).boss('top', 10, { scale: 0.6 })\n```\n\n```ts\nboss(face: FaceSelector, height: number, opts?: BossOptions): Shape\n```\n\n#### `hole()` — Drill a hole into this solid at a face.\n\n```js\nbox(50, 50, 20).hole('top', { diameter: 8, depth: 10 })\nbox(50, 50, 20).hole('top', { diameter: 6, counterbore: { diameter: 12, depth: 3 } })\n```\n\n```ts\nhole(faceOrRef: SketchFaceTarget | FaceRef, opts: ShapeHoleOptions): Shape\n```\n\n#### `cutout()` — Cut a profile-shaped pocket through a face using a placed sketch.\n\nThe sketch must be placed on a face with `Sketch.onFace(...)`. The cut follows the sketch's 2D profile.\n\n```js\nconst profile = circle2d(10).onFace(body, 'top');\nbody.cutout(profile, { depth: 5 })\n```\n\n```ts\ncutout(sketch: Sketch, opts?: ShapeCutoutOptions): Shape\n```\n\n**Placement**\n\n#### `placeReference()` — Translate the shape so the given anchor or reference lands on the target coordinate.\n\nAccepts any built-in anchor name (`'bottom'`, `'center'`, `'top-front-left'`, etc.) or a custom placement reference attached via `withReferences()`.\n\n```javascript\n// Ground a shape — put its bottom face center at Z = 0\nshape.placeReference('bottom', [0, 0, 0])\n\n// Center at the world origin\nshape.placeReference('center', [0, 0, 0])\n\n// Align left edge to X = 10\nshape.placeReference('left', [10, 0, 0])\n```\n\n```ts\nplaceReference(ref: PlacementAnchorLike, target: [ number, number, number ], offset?: [ number, number, number ]): Shape\n```\n\n#### `attachTo()` — Position this shape relative to another using named 3D anchor points.\n\nAnchors are bounding-box-relative: 'center', face centers ('top', 'front', ...), edge midpoints ('top-front', 'back-left', ...), and corners ('top-front-left', ...). Anchor word order is flexible: 'front-left' and 'left-front' are equivalent. Named placement references (from withReferences) can also be used as anchors.\n\n```ts\nattachTo(target: ShapeAnchorTarget, targetAnchor: PlacementAnchorLike, selfAnchor?: PlacementAnchorLike, offset?: [ number, number, number ]): Shape\n```\n\n#### `onFace()` — Place this shape on a face of a parent shape.\n\nThink of it like sticking a label on a box surface:\n\n- `face` picks which surface ('front', 'back', 'top', etc.)\n- `u, v` position within that face's 2D plane (from center)\n- front/back: u = left/right (X), v = up/down (Z)\n- left/right: u = forward/back (Y), v = up/down (Z)\n- top/bottom: u = left/right (X), v = forward/back (Y)\n- `protrude` = how far the child sticks out (positive = outward from face)\n\n```ts\nonFace(parent: ShapeAnchorTarget, face: \"front\" | \"back\" | \"left\" | \"right\" | \"top\" | \"bottom\", opts?: { u?: number; v?: number; protrude?: number; }): Shape\n```\n\n#### `seatInto()` — Slide this shape along an axis until a labeled face is embedded in the target body.\n\nPosition the shape roughly first (translate/rotate), then call seatInto to auto-adjust the penetration depth. No manual coordinate math needed.\n\n```js\n// Wing root embeds into fuselage — adapts to any fuselage shape\nwing.translate(0, wingY, 0).seatInto(fuselage, 'root');\n\n// Sensor pod sits flush on fuselage surface\npod.translate(0, station, radius + 20).seatInto(fuselage, 'base', { depth: 'flush' });\n\n// Antenna with 3mm gasket standoff\nmast.translate(0, station, radius + 50).seatInto(fuselage, 'mount', { depth: 'flush', gap: 3 });\n```\n\n```ts\nseatInto(target: Shape, surface: string, options?: SeatIntoOptions): Shape\n```\n\n#### `seatOver()` — Slide this shape until a target's labeled face is fully covered (inside this shape).\n\nThe inverse of `seatInto`: instead of embedding *your* face into the target, you move until the *target's* face is embedded inside you.\n\n```js\n// Nacelle moves up until pylon's bottom face is inside the nacelle\nnacelle.translate(rough).seatOver(pylon, 'bottom');\n\n// Cap slides down over a post until post's top face is covered\ncap.translate(rough).seatOver(post, 'top');\n```\n\n```ts\nseatOver(target: Shape, targetSurface: string, options?: SeatIntoOptions): Shape\n```\n\n**Connectors**\n\n#### `withConnectors()` — Attach named connectors — attachment points that survive transforms and imports. Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching).\n\n```ts\nwithConnectors(connectors: Record<string, ConnectorInput>): Shape\n```\n\n#### `connectorNames()` — List all connector names on this shape.\n\n```ts\nconnectorNames(): string[]\n```\n\n#### `connectorsByType()` — Get all connectors of a given type.\n\n```ts\nconnectorsByType(type: string): Array<{ name: string; port: ConnectorDef; }>\n```\n\n#### `connectorDistance()` — Distance between two connector origins on this shape.\n\n```ts\nconnectorDistance(nameA: string, nameB: string): number\n```\n\n#### `connectorMeasurements()` — Get measurements metadata from a connector.\n\n```ts\nconnectorMeasurements(name: string): Record<string, number | string>\n```\n\n#### `matchTo()` — Position this shape by matching connectors to a target.\n\nOverloads:\n\n- Single pair: `matchTo(target, selfConn, targetConn, options?)`\n- Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`\n- Multi-target: `matchTo([ [target1, selfConn1, targetConn1], ... ], options?)`\n\n```ts\nmatchTo(targetOrPairs: Shape | MatchTarget | Array<[ Shape | MatchTarget, string, string ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): Shape\n```\n\n**References**\n\n#### `withReferences()` — Attach named placement references that survive normal transforms and imports.\n\n```ts\nwithReferences(refs: PlacementReferenceInput): Shape\n```\n\n#### `referenceNames()` — List named placement references carried by this shape.\n\n```ts\nreferenceNames(kind?: PlacementReferenceKind): string[]\n```\n\n#### `referencePoint()` — Resolve a named placement reference or built-in anchor to a 3D point.\n\n```ts\nreferencePoint(ref: PlacementAnchorLike): [ number, number, number ]\n```\n\n**Measurement**\n\n#### `boundingBox()` — Get the axis-aligned bounding box as { min: [x,y,z], max: [x,y,z] }.\n\n```ts\nboundingBox(): ShapeRuntimeBounds\n```\n\n#### `volume()` — Volume in mm cubed.\n\n```ts\nvolume(): number\n```\n\n#### `surfaceArea()` — Surface area in mm squared.\n\n```ts\nsurfaceArea(): number\n```\n\n#### `isEmpty()` — True if the shape contains no geometry.\n\n```ts\nisEmpty(): boolean\n```\n\n#### `numBodies()` — Number of disconnected solid bodies in this shape.\n\n```ts\nnumBodies(): number\n```\n\n#### `numTri()` — Triangle count of the mesh representation.\n\n```ts\nnumTri(): number\n```\n\n**Other**\n\n#### `clone()` — Return a new Shape wrapper for explicit duplication in scripts.\n\n```ts\nclone(): Shape\n```\n\n#### `geometryInfo()` — Inspect which backend/representation produced this solid.\n\n```ts\ngeometryInfo(): GeometryInfo\n```\n\n#### `as()` — Name this shape as a reference namespace for diagnostics and future published refs.\n\n```ts\nas(name: string): Shape\n```\n\n#### `ref()` — Resolve a semantic reference path like `lid`, `lid/back`, or a midpoint selector on `lid/back`.\n\n```ts\nref(path: string): ShapeRef\n```\n\n#### `thicken()` — Offset-thicken an exact open surface or shell into a solid.\n\n```ts\nthicken(thickness: number): Shape\n```\n\n#### `getMesh()` — Extract triangle mesh for Three.js rendering\n\n```ts\ngetMesh(): ShapeRuntimeMesh\n```\n\n#### `slice()` — Slice the runtime solid by a plane normal to local Z at the given offset.\n\n```ts\nslice(offset?: number): any\n```\n\n#### `project()` — Orthographically project the runtime solid onto the local XY plane.\n\n```ts\nproject(): any\n```\n\n**Legacy Aliases**\n\n- `withPorts()` -> `withConnectors()`\n- `portNames()` -> `connectorNames()`\n\n### `Transform`\n\n#### `identity()` — Return the identity transform.\n\n```ts\nstatic identity(): Transform\n```\n\n#### `from()` — Wrap an existing `Transform` or raw 4x4 matrix as a `Transform`.\n\n```ts\nstatic from(input: TransformInput): Transform\n```\n\n#### `translation()` — Create a translation transform.\n\n```ts\nstatic translation(x: number, y: number, z: number): Transform\n```\n\n#### `scale()` — Create a uniform or per-axis scale transform.\n\n```ts\nstatic scale(v: number | Vec3): Transform\n```\n\n#### `rotationAxis()` — Create a rotation around an arbitrary axis, optionally about a pivot.\n\n```ts\nstatic rotationAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform\n```\n\n#### `rotateAroundTo()` — Solve the rotation needed to move one point onto a target line or plane.\n\n```ts\nstatic rotateAroundTo(axis: Vec3, pivot: Vec3, movingPoint: Vec3, targetPoint: Vec3, options?: RotateAroundToOptions): Transform\n```\n\n#### `mul()` — Compose transforms in chain order: `a.mul(b)` applies `a`, then `b`.\n\n```ts\nmul(other: TransformInput): Transform\n```\n\n#### `translate()` — Translate after the current transform.\n\n```ts\ntranslate(x: number, y: number, z: number): Transform\n```\n\n#### `rotateAxis()` — Rotate after the current transform.\n\n```ts\nrotateAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform\n```\n\n#### `inverse()` — Return the inverse transform.\n\n```ts\ninverse(): Transform\n```\n\n#### [`point()`](/docs/sketch#point) — Transform a point using homogeneous coordinates.\n\n```ts\npoint(p: Vec3): Vec3\n```\n\n#### `vector()` — Transform a direction vector without translation.\n\n```ts\nvector(v: Vec3): Vec3\n```\n\n#### `toArray()` — Return the transform as a raw 4x4 matrix array.\n\n```ts\ntoArray(): Mat4\n```\n\n### `ShapeGroup`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `children` | `GroupChild[]` | — |\n| `childNames` | `Array<string | undefined>` | — |\n\n**Children**\n\n#### `child()` — Return the named child by name. Throws if not found. Useful when importing a multipart group and working on components individually.\n\n```ts\nchild(name: string): GroupChild\n```\n\n#### `childName()` — Return the optional name of the child at `index`.\n\n```ts\nchildName(index: number): string | undefined\n```\n\n**Transforms**\n\n#### `translate()` — Move the entire group by (x, y, z). All children move together as a unit.\n\n```ts\ntranslate(x: number, y: number, z: number): ShapeGroup\n```\n\n#### `moveTo()` — Move the group so its bounding-box min corner lands at the given coordinate.\n\n```ts\nmoveTo(x: number, y: number, z: number): ShapeGroup\n```\n\n#### `moveToLocal()` — Move the group relative to another part's bounding-box min corner.\n\n```ts\nmoveToLocal(target: Shape | ShapeGroup, x: number, y: number, z: number): ShapeGroup\n```\n\n#### `rotate()` — Rotate the group around an arbitrary axis through the origin.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateX()` — Rotate the group around the X axis.\n\n```ts\nrotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateY()` — Rotate the group around the Y axis.\n\n```ts\nrotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateZ()` — Rotate the group around the Z axis.\n\n```ts\nrotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateAroundAxis()` — Rotate around an arbitrary axis, optionally through a pivot point.\n\n```ts\nrotateAroundAxis(axis: [ number, number, number ], angleDeg: number, pivot?: [ number, number, number ]): ShapeGroup\n```\n\n#### `rotateAroundTo()` — Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point. ShapeGroup string points use built-in anchors only.\n\n```ts\nrotateAroundTo(axis: [ number, number, number ], pivot: [ number, number, number ], movingPoint: Anchor3D | [ number, number, number ], targetPoint: Anchor3D | [ number, number, number ], options?: RotateAroundToOptions): ShapeGroup\n```\n\n#### `pointAlong()` — Reorient the group so its local Z axis points along `direction`.\n\n```ts\npointAlong(direction: [ number, number, number ]): ShapeGroup\n```\n\n#### `transform()` — Apply a 4x4 transform matrix or `Transform` to all 3D children.\n\n```ts\ntransform(m: Mat4 | Transform): ShapeGroup\n```\n\n#### `scale()` — Scale uniformly or per-axis from the group's bounding-box center.\n\n```ts\nscale(v: number | [ number, number, number ]): ShapeGroup\n```\n\n#### `scaleAround()` — Scale uniformly or per-axis from an explicit pivot point.\n\n```ts\nscaleAround(pivot: [ number, number, number ], v: number | [ number, number, number ]): ShapeGroup\n```\n\n#### `mirror()` — Mirror across a plane through the group's bounding-box center.\n\n```ts\nmirror(normal: [ number, number, number ]): ShapeGroup\n```\n\n#### `mirrorThrough()` — Mirror across a plane through an explicit point.\n\n```ts\nmirrorThrough(point: [ number, number, number ], normal: [ number, number, number ]): ShapeGroup\n```\n\n**Placement**\n\n#### `placeReference()` — Translate the group so the given anchor or reference lands on the target coordinate.\n\nAccepts any built-in anchor name (`'bottom'`, `'center'`, `'top-front-left'`, etc.) or a custom placement reference attached via `withReferences()`.\n\n```javascript\n// Ground a group — put its bottom at Z = 0\nassembly.placeReference('bottom', [0, 0, 0])\n\n// Use a custom reference from a multi-file part\nconst placed = require('./bracket-assembly.forge.js').group\n .placeReference('mountCenter', [0, 0, 50]);\n```\n\n```ts\nplaceReference(ref: PlacementAnchorLike, target: [ number, number, number ], offset?: [ number, number, number ]): ShapeGroup\n```\n\n#### `attachTo()` — Attach this group to a face or anchor on another part.\n\n`targetAnchor` can be a built-in anchor name or a custom reference name on the target. `selfAnchor` selects the anchor on this group to align.\n\n```ts\nattachTo(target: Shape | ShapeGroup, targetAnchor: Anchor3D | string, selfAnchor?: Anchor3D, offset?: [ number, number, number ]): ShapeGroup\n```\n\n#### `onFace()` — Place this group on a face of a parent shape. See Shape.onFace() for full documentation.\n\n```ts\nonFace(parent: Shape | ShapeGroup, face: \"front\" | \"back\" | \"left\" | \"right\" | \"top\" | \"bottom\", opts?: { u?: number; v?: number; protrude?: number; }): ShapeGroup\n```\n\n**Connectors**\n\n#### `withConnectors()` — Attach named connectors — attachment points that survive transforms. Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching).\n\n```ts\nwithConnectors(connectors: Record<string, ConnectorInput>): ShapeGroup\n```\n\n#### `connectorNames()` — List all connector names, including \"ChildName.connectorName\" from named children.\n\n```ts\nconnectorNames(): string[]\n```\n\n#### `connectorsByType()` — Get all connectors of a given type, including from named children.\n\n```ts\nconnectorsByType(type: string): Array<{ name: string; port: ConnectorDef; }>\n```\n\n#### `connectorDistance()` — Distance between two connector origins on this group (supports dotted child paths).\n\n```ts\nconnectorDistance(nameA: string, nameB: string): number\n```\n\n#### `connectorMeasurements()` — Get measurements metadata from a connector (supports dotted child paths).\n\n```ts\nconnectorMeasurements(name: string): Record<string, number | string>\n```\n\n#### `matchTo()` — Position this group by matching connectors to a target. Connector names support dotted paths into named children: \"ChildName.connectorName\".\n\nOverloads:\n\n- Single pair: `matchTo(target, selfConn, targetConn, options?)`\n- Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`\n- Multi-target: `matchTo([ [target1, selfConn1, targetConn1], ... ], options?)`\n\n```ts\nmatchTo(targetOrPairs: Shape | ShapeGroup | Array<[ Shape | ShapeGroup, string, string ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): ShapeGroup\n```\n\n**References**\n\n#### `withReferences()` — Attach named placement references to this group. References survive normal transforms (translate/rotate/scale/mirror/transform).\n\n```javascript\nconst bracket = group(\n { name: 'Left', shape: leftShape },\n { name: 'Right', shape: rightShape },\n).withReferences({\n points: { mountCenter: [0, 0, 0] },\n});\n```\n\n```ts\nwithReferences(refs: PlacementReferenceInput): ShapeGroup\n```\n\n#### `referenceNames()` — List named placement references carried by this group.\n\n```ts\nreferenceNames(kind?: PlacementReferenceKind): string[]\n```\n\n#### `referencePoint()` — Resolve a named placement reference or built-in Anchor3D to a 3D point. Named refs take priority over built-in anchors.\n\n```ts\nreferencePoint(ref: PlacementAnchorLike): [ number, number, number ]\n```\n\n**Other**\n\n#### `clone()` — Return a deep-cloned ShapeGroup tree (refs copied).\n\n```ts\nclone(): ShapeGroup\n```\n\n#### `boundingBox()` — Return the combined 3D bounding box of all children.\n\n```ts\nboundingBox(): { min: [ number, number, number ]; max: [ number, number, number ]; }\n```\n\n#### `color()` — Return a copy of the group with the given display color applied to each child.\n\n```ts\ncolor(hex: string): ShapeGroup\n```\n\n**Legacy Aliases**\n\n- `withPorts()` -> `withConnectors()`\n- `portNames()` -> `connectorNames()`\n\n### `SurfacePattern`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `body` | `string` | Function body: receives (u, v) in surface mm, returns height displacement. |\n| `constants` | `Record<string, number>` | Named constants injected into the function. |\n\n### `Pattern2D`\n\n#### `add()` — Add this pattern to one or more patterns or constant height offsets.\n\n```ts\nadd(...patterns: Pattern2DInput[]): Pattern2D\n```\n\n#### `subtract()` — Subtract another pattern or constant height offset from this pattern.\n\n```ts\nsubtract(pattern: Pattern2DInput): Pattern2D\n```\n\n#### `multiply()` — Multiply this pattern by one or more patterns or numeric scale factors.\n\n```ts\nmultiply(...patterns: Pattern2DInput[]): Pattern2D\n```\n\n#### `min()` — Keep the lower height between this pattern and one or more other patterns.\n\n```ts\nmin(...patterns: Pattern2DInput[]): Pattern2D\n```\n\n#### `max()` — Keep the higher height between this pattern and one or more other patterns.\n\n```ts\nmax(...patterns: Pattern2DInput[]): Pattern2D\n```\n\n#### `clamp()` — Limit pattern height to the inclusive `[min, max]` range in millimeters.\n\n```ts\nclamp(min: number, max: number): Pattern2D\n```\n\n#### `abs()` — Convert negative heights to positive heights.\n\n```ts\nabs(): Pattern2D\n```\n\n#### `negate()` — Flip the pattern height sign.\n\n```ts\nnegate(): Pattern2D\n```\n\n### `Pattern2DBuilder`\n\n#### `constant()` — Create a constant-height pattern in millimeters.\n\n```ts\nconstant(value?: number): Pattern2D\n```\n\n#### `sineWave()` — Create a sinusoidal wave pattern in UV space.\n\n```ts\nsineWave(options: Pattern2DSineWaveOptions): Pattern2D\n```\n\n#### `stripes()` — Create recessed stripe bands in UV space.\n\n```ts\nstripes(options: Pattern2DStripesOptions): Pattern2D\n```\n\n#### `overUnderWeave()` — Create an over-under woven relief pattern in UV space.\n\n```ts\noverUnderWeave(options: Pattern2DOverUnderWeaveOptions): Pattern2D\n```\n\n### `ShapeRef`\n\nA first-class reference path over a shape's semantic faces and face relationships.\n\nCreated with `shape.ref(\"lid/back\")`, then refined through methods such as `.point()` or `.edges()`. The reference stores intent as a readable path and resolves lazily against the current shape metadata.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `path` | `string` | — |\n\n**Methods:**\n\n#### `resolve()` — Resolve this reference into its current faces, edges, or points.\n\n```ts\nresolve(): ShapeReferenceResolution\n```\n\n#### `kind()` — The resolved reference kind, such as `face`, `edge-set`, or [`point`](/docs/sketch#point).\n\n```ts\nget kind(): ShapeReferenceKind\n```\n\n#### `cardinality()` — Whether the reference currently resolves to zero, one, or many matches.\n\n```ts\nget cardinality(): ShapeReferenceCardinality\n```\n\n#### `status()` — Return the reference lifecycle status for the current shape state.\n\n```ts\nstatus(): ShapeReferenceStatus\n```\n\n#### `explain()` — Return a human-readable explanation of how this reference resolved.\n\n```ts\nexplain(): string\n```\n\n#### `as()` — Name this derived reference so the same shape can resolve it by `shape.ref(name)`.\n\n```ts\nas(name: string): ShapeRef\n```\n\n#### `maybe()` — Return an optional reference that resolves to zero matches instead of throwing when missing.\n\n```ts\nmaybe(): ShapeRef\n```\n\n#### `all()` — Mark that a multi-match reference is intentionally being used as a set.\n\n```ts\nall(): ShapeRef\n```\n\n#### `one()` — Require this reference to resolve to exactly one match.\n\n```ts\none(): ShapeRef\n```\n\n#### `faces()` — Resolve this reference as one or more faces.\n\n```ts\nfaces(): FaceRef[]\n```\n\n#### `face()` — Resolve this reference as exactly one face.\n\n```ts\nface(): FaceRef\n```\n\n#### `edges()` — Resolve this reference as one or more edges. Face references return boundary edges.\n\n```ts\nedges(): EdgeSegment[]\n```\n\n#### `edge()` — Resolve this reference as exactly one edge.\n\n```ts\nedge(): EdgeSegment\n```\n\n#### `points()` — Resolve this reference as one or more points. Faces use centers and edges use midpoints.\n\n```ts\npoints(): Vec3[]\n```\n\n#### [`point()`](/docs/sketch#point) — Resolve this reference as exactly one point.\n\n```ts\npoint(): Vec3\n```\n\n#### `toJSON()` — Return the structured JSON-friendly reference resolution.\n\n```ts\ntoJSON(): ShapeReferenceResolution\n```\n\n#### `toString()` — Return a compact display form for this reference path.\n\n```ts\ntoString(): string\n```\n\n---\n\n## Constants\n\n### `ANCHOR3D_NAMES`\n\n### `verify`\n\n- `that(label: string, check: () => boolean, message?: string): void` — Custom predicate check.\n- `equal(label: string, actual: number, expected: number, tolerance?: number, message?: string): void` — Check that two numbers are approximately equal (within tolerance).\n- `notEqual(label: string, actual: number, unexpected: number, tolerance?: number, message?: string): void` — Check that two numbers are NOT equal (differ by more than tolerance).\n- `greaterThan(label: string, actual: number, min: number, message?: string): void` — Check that actual > min.\n- `lessThan(label: string, actual: number, max: number, message?: string): void` — Check that actual < max.\n- `inRange(label: string, actual: number, min: number, max: number, message?: string): void` — Check that min <= actual <= max.\n- `centersCoincide(label: string, a: ShapeLike, b: ShapeLike, tolerance?: number): void` — Check that the bounding-box centers of two shapes coincide within tolerance (mm).\n- `connectorDistance(label: string, target: ConnectorDistanceLike, connectorA: string, connectorB: string, expected?: number, tolerance?: number): void` — Check the distance between two named connectors on a shape or group. Use this when connectors + `matchTo()` define a static assembly interface. It proves the mate at runtime, unlike a plain source-level connector declaration. The common case is `expected = 0`, meaning the two connector origins should coincide after placement. **Example** ```ts verify.connectorDistance(\"leg is seated\", bench, \"Rail.leg_0\", \"Leg0.head\", 0, 0.01); ```\n- `physicalComponentCount(label: string, expected: number): void` — Declare the expected physical connectivity component count for the returned visible model. **Details** Use this for generated mechanical models that should have a clear component graph: one connected fixture, a purchased part plus a removable cartridge, a root assembly plus named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned visible objects with the same physical-connectivity analysis used in the quality gate and fails if the actual component count differs. This catches the common generated-CAD failure where a script returns a visually plausible artifact but the handle, screw, washer, cover, or terminal block is actually a separate island. **Example** ```ts verify.physicalComponentCount(\"vise is one connected installed assembly\", 1); ```\n- `intentionalOverlap(label: string, a: ShapeLike, b: ShapeLike, reason: string): void` — Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent. **Details** Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume: welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers without pockets, or parts placed with collision as a positioning hack. `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are returned as visible objects and the exact collision report finds that same object pair. Unused or non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions. **Example** ```ts verify.intentionalOverlap(\"rubber grip is overmolded on handle\", rubberGrip, handleCore, \"overmolded insert\"); ```\n- `notColliding(label: string, a: ShapeLike, b: ShapeLike, searchLength?: number): void` — Check that two shapes do not collide (minGap > 0).\n- `minClearance(label: string, a: ShapeLike, b: ShapeLike, minGap: number, searchLength?: number): void` — Check that a minimum clearance gap exists between two shapes.\n- `clearanceBetween(label: string, a: ShapeLike, b: ShapeLike, minGap: number, maxGap: number, searchLength?: number): void` — Check that the clearance gap between two shapes is inside an allowed range. **Details** Use this for seated and retained interfaces where a part must be close enough to be mechanically accountable, but must not collide beyond the allowed minimum. It catches both failure modes that make generated CAD look fake: parts floating away from their receiver, and parts intersecting their receiver because the pocket, bore, or running clearance was not modeled. For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny numerical noise. For a running fit, use the intended clearance band. Manifold-backed shapes use exact min-gap distance. Other backends use a mesh-derived min-gap check and say so in the verification message; keep `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for positive-volume interference. **Example** ```ts verify.clearanceBetween(\"cover is seated on gasket\", cover, gasket, -0.01, 0.05); verify.clearanceBetween(\"carriage runs inside rail\", carriage, rail, 0.2, 0.5); ```\n- `parallel(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals are parallel (within toleranceDeg degrees).\n- `perpendicular(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals are perpendicular (within toleranceDeg degrees).\n- `coplanar(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number, toleranceMm?: number): void` — Check that a face is coplanar with (same plane as) another face, meaning they are parallel AND their centers lie on the same plane.\n- `faceAt(label: string, face: FaceRefLike, expectedPos: [ number, number, number ], toleranceMm?: number): void` — Check that a face center lies at a specific position (within toleranceMm).\n- `sameDirection(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals point in the same direction (not antiparallel). Stricter than parallel — both |angle| AND sign must match.\n- `isEmpty(label: string, shape: ShapeLike, message?: string): void` — Check that a shape is empty.\n- `notEmpty(label: string, shape: ShapeLike, message?: string): void` — Check that a shape is NOT empty.\n- `volumeApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void` — Check that a shape's volume is approximately equal to expected (mm³).\n- `areaApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void` — Check that a shape's surface area is approximately equal to expected (mm²).\n- `boundingBoxSize(label: string, shape: ShapeLike, expectedSize: [ number, number, number ], tolerance?: number): void` — Check that a shape's bounding box has approximately the given size.\n- `edgeContinuity(label: string, shape: ShapeLike, options?: EdgeContinuityThresholds): void` — Check that every sampled seam on a shape meets a requested continuity threshold.\n- `noTinyEdges(label: string, shape: ShapeLike, threshold?: number): void` — Check that a shape has no tiny edges below the requested threshold.\n- `noSliverFaces(label: string, shape: ShapeLike, threshold?: number): void` — Check that a shape has no sliver faces below the requested score threshold.\n- `noSelfIntersection(label: string, shape: ShapeLike): void` — Best-effort exact-shape validity guard for self-intersections or broken B-Rep topology.\n\n### `Constraint`\n\n- `makeParallel(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to be parallel.\n- `enforceAngle(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg, angleDeg: number): ConstrainedSketchBuilder` — Constrain the signed angle from line `a` to line `b`.\n- `horizontal(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder` — Constrain a line to be horizontal.\n- `vertical(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder` — Constrain a line to be vertical.\n- `equalLength(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to have equal length.\n- `distance(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg, value: number): ConstrainedSketchBuilder` — Constrain the distance between two points.\n- `fix(builder: ConstrainedSketchBuilder, pt: PointArg, x: number, y: number): ConstrainedSketchBuilder` — Fix a point at a specific coordinate.\n- `coincident(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg): ConstrainedSketchBuilder` — Constrain two points to occupy the same location.\n- `perpendicular(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to be perpendicular.\n- `length(builder: ConstrainedSketchBuilder, line: LineArg, value: number): ConstrainedSketchBuilder` — Constrain the length of a line.\n\n### `Points`\n\n- `distance(a: Vec3, b: Vec3): number` — Euclidean distance between two 3D points.\n- `midpoint(a: Vec3, b: Vec3): Vec3` — Center point between two 3D points.\n- `lerp(a: Vec3, b: Vec3, t: number): Vec3` — Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b.\n- `direction(a: Vec3, b: Vec3): Vec3` — Unit direction vector from a to b. Throws if a and b are the same point.\n- `offset(point: Vec3, dir: Vec3, amount: number): Vec3` — Move a point along a direction vector by a given amount.\n- `polar(length: number, angleDeg: number, from?: [ number, number ]): [ number, number ]` — Compute a 2D point at distance and angle (degrees) from an optional origin.\n\n### `connector`\n\nConnector factory. Create attachment points: `connector({...})`, `connector.male(type, {...})`, etc.\n\n---\n\n<!-- guides/coordinate-system.md -->\n\n# Coordinate System Convention\n\nForgeCAD uses a **Z-up** right-handed coordinate system.\n\n## Axes\n\n| Axis | Direction | Positive |\n|------|-----------------|----------|\n| X | Left / Right | Right |\n| Y | Forward / Back | Forward |\n| Z | Up / Down | Up |\n\n## Standard Views\n\n| View | Camera position direction | Sees plane |\n|--------|--------------------------|------------|\n| Front | −Y | XZ |\n| Back | +Y | XZ |\n| Right | +X | YZ |\n| Left | −X | YZ |\n| Top | +Z | XY |\n| Bottom | −Z | XY |\n\n## GizmoViewcube Face Mapping\n\nThree.js BoxGeometry material indices vs ForgeCAD labels (Z-up remapping):\n\n| Index | Three.js direction | ForgeCAD label |\n|-------|--------------------|----------------|\n| 0 | +X | Right |\n| 1 | −X | Left |\n| 2 | +Y | Front |\n| 3 | −Y | Back |\n| 4 | +Z | Top |\n| 5 | −Z | Bottom |\n\nDefault drei labels are Y-up; ForgeCAD passes `faces={['Right','Left','Front','Back','Top','Bottom']}`.\n\n## Grid\n\nThe ground plane is XY (Z = 0). Extrusion goes along +Z. Manifold is Y-up internally — if a kernel-facing operation behaves as if axes are swapped, check for Manifold Y-up semantics leaking through.\n\n---\n\n<!-- guides/geometry-conventions.md -->\n\n# Geometry Conventions\n\nForgeCAD wraps Manifold (mesh kernel) and Three.js (Y-up renderer). This doc captures convention mismatches and how ForgeCAD resolves them.\n\n## Winding Order\n\nCCW = positive area, CW = empty in Manifold's `CrossSection`. ForgeCAD auto-fixes at all entry points:\n- `polygon(points)` — computes signed area (shoelace), reverses if CW\n- `path().close()` — same fix\n\n**Rule for new code:** Any function accepting user point arrays that creates a `CrossSection` MUST auto-fix winding.\n\n## Coordinate System (Z-up vs Y-up)\n\nThree.js is Y-up; ForgeCAD is Z-up. Fix applied at camera level (`camera.up = (0,0,1)`) — geometry coordinates are native Z-up. Never swap Y/Z in geometry.\n\n## Revolution Axis\n\n`CrossSection.revolve()` revolves around Y. Profile X = radial distance, Profile Y = height (becomes Z after revolution). Profile must be at X > 0.\n\n## Boolean Winding (3D)\n\nManifold requires consistent outward face normals. ForgeCAD only creates meshes through Manifold's own constructors, which guarantee correct normals.\n\n## Transform Order\n\nTransforms apply left-to-right. `Sketch.rotate()`, `scale()`, and `mirror()` operate around bounding-box center. For 3D `Shape` / `ShapeGroup`, `scale()` and `mirror()` operate around bounding-box center, while `rotate()` remains origin-based unless you pass `options.pivot` or use `rotateAroundAxis(...)`.\n\nFor explicit transform objects: `A.mul(B)` = apply A then B; `composeChain(A, B, C)` = A→B→C.\n\n## Assembly Frame Composition\n\n```ts\nchildWorld = composeChain(childBase, jointMotion, jointFrame, parentWorld)\n```\n\nPrefer `composeChain(...)` over manual `.mul(...).mul(...)` in kinematics code to avoid order mistakes.\n\n## Summary\n\n| Convention | User sees | Kernel needs | Where we fix it |\n|---|---|---|---|\n| Winding | Any point order | CCW | `polygon()`, `path().close()` |\n| Up axis | Z-up | Y-up (Three.js) | `camera.up`, gizmo labels |\n| Revolution | \"revolve this profile\" | Profile in X-Y, X>0 | Documented only |\n| Face normals | Doesn't think about it | Outward-pointing | Manifold constructors |\n| Transform order | Left-to-right chain | Post-multiply | Native match |\n\n---\n\n<!-- guides/positioning.md -->\n\n# Positioning Strategy\n\n## Rule 0: if parts should touch, use connectors first\n\nFor any fixed assembly where parts are meant to stay in contact in the final model, start with connectors + `matchTo()`. This applies to furniture, fixtures, toys, enclosures, sleds, and any other static multi-part object, not only mechanisms.\n\nUse raw `translate()` and `rotate()` when parts are intentionally free-floating or when you are doing quick exploratory layout. Use `attachTo()` for rough bounding-box placement. But if the relationship is a real interface, make it explicit with connectors.\n\n## Primitive origin convention\n\nAll 3D primitives are **centered on XY, base at Z=0**:\n\n| Primitive | X range | Y range | Z range |\n|-----------|---------|---------|---------|\n| `box(60, 40, 20)` | [-30, 30] | [-20, 20] | [0, 20] |\n| `cylinder(50, 10)` | [-10, 10] | [-10, 10] | [0, 50] |\n| `sphere(15)` | [-15, 15] | [-15, 15] | [-15, 15] |\n| `torus(20, 5)` | [-25, 25] | [-25, 25] | [-5, 5] |\n\nSphere and torus are fully centered (symmetric in Z). Box and cylinder sit on the XY ground plane — **Z goes up from zero, never negative**.\n\nThis means `box(w, d, h).translate(0, 0, -h / 2)` is the manual way to \"center on Z\" — it moves the box from `[0, h]` to `[-h / 2, h / 2]`. Prefer `box(w, d, h).placeReference('center', [0, 0, 0])` when you want full XYZ centering.\n\nDo not assume `center: true` or a positional `true` gives OpenSCAD-style full XYZ centering. Primitive placement is fixed unless the primitive docs explicitly say otherwise.\n\n---\n\nMost positioning bugs come from manual coordinate arithmetic. Use these methods in priority order.\n\n## 1. Connectors + `matchTo()` — default for mating interfaces\n\nDefine connectors on parts; `matchTo()` provides automatic 6-DOF alignment. The child translates and rotates so its connector aligns with the target's — origins coincide, axes oppose (plug-in model).\n\n```javascript\nconst shelf = box(200, 120, 10).translate(0, 0, -5).withConnectors({\n left_tab: connector.male(\"dovetail\", { origin: [-100, 0, 0], axis: [-1, 0, 0] }),\n});\nconst panel = box(12, 120, 200).translate(0, 0, -100).withConnectors({\n shelf_0: connector.female(\"dovetail\", { origin: [6, 0, -50], axis: [1, 0, 0] }),\n});\nconst placed = shelf.matchTo(panel, \"left_tab\", \"shelf_0\");\n// Dictionary form for multiple pairs on same target:\nconst placed2 = shelf.matchTo(panel, { left_tab: \"shelf_0\" });\n// Named group children bubble connectors via dotted paths:\nconst cabinet = group({ name: \"Left\", shape: panel });\nshelf.matchTo(cabinet, \"left_tab\", \"Left.shelf_0\");\n```\n\n**Why connectors first:** stable (don't shift on fillet/chamfer/boolean), semantic (carry type/gender), oriented (full frame), queryable (`shape.connectorDistance('a','b')`), explode-aware.\n\nFor a non-mechanism fixed-assembly example, see `examples/api/static-assembly-connectors.forge.js`.\n\n## 2. `group()` — local coordinates for multi-part assemblies\n\nThe most common positioning bug: manually adding a parent's global offset to every sub-part. One wrong sign or forgotten variable and parts float into space. **Use `group()` to build parts in local coordinates (at the origin), then position the group once.**\n\n```javascript\n// BAD — every sub-part repeats the parent's global position\nconst unitY = -18, unitZ = 70;\nconst body = lib.roundedBox(100, 20, 32, 4).translate(0, unitY, unitZ);\nconst panel = box(98, 2, 18).translate(0, unitY - 12, unitZ + 4);\nconst louver = box(88, 2, 6).translate(0, unitY - 14, unitZ - 11);\nconst led = sphere(1.2).translate(35, unitY - 12, unitZ + 9);\n\n// GOOD — build at local origin, group, translate once\nconst body = lib.roundedBox(100, 20, 32, 4);\nconst panel = box(98, 2, 18).translate(0, -12, 4); // relative to local origin\nconst louver = box(88, 2, 6).translate(0, -14, -11); // relative to local origin\nconst led = sphere(1.2).translate(35, -12, 9); // relative to local origin\nconst indoorUnit = group(\n { name: 'Body', shape: body },\n { name: 'Panel', shape: panel },\n { name: 'Louver', shape: louver },\n { name: 'LED', shape: led },\n).translate(0, -18, 70); // ONE translate for the whole assembly\n```\n\n**Groups nest.** Build sub-assemblies as groups, then group those into larger assemblies — each level has its own local origin.\n\n```javascript\nconst fan = group(hub, ...blades).translate(0, 25, 0); // fan assembly\nconst outdoorUnit = group(\n { name: 'Body', shape: casing },\n { name: 'Fan', shape: fan }, // already a group\n { name: 'Grille', shape: grille },\n).translate(0, 23, -42); // position the whole outdoor unit\n```\n\n**When to use something else:** `group()` preserves individual shapes — you can't boolean (subtract/intersect) a group. If a sub-part needs a boolean with the parent body, do that boolean first in local coordinates, then group the result.\n\n## 3. `pointAlong()` — orient cylinders before positioning\n\n```javascript\n// BAD\nconst pipe = cylinder(100, 5).rotateX(90).translate(x, y, z);\n// GOOD — reads as \"pipe pointing along Y\"\nconst pipe = cylinder(100, 5).pointAlong([0, 1, 0]).translate(x, y, z);\n```\n\n**Always call `pointAlong()` BEFORE `matchTo()` or `translate()`** — it reorients around the origin.\n\n## 4. `attachTo()` — quick bounding-box positioning\n\n```javascript\nconst column = cylinder(50, 8).attachTo(base, 'top', 'bottom');\n```\n\n`child.attachTo(parent, parentAnchor, selfAnchor, offset)`. Anchor points shift on fillet/chamfer/boolean — fragile for assembly interfaces, fine for quick prototyping.\n\n## 5. `rotateAroundTo()` — aim a point around a hinge/axis\n\n```javascript\nconst aimed = arm.rotateAroundTo([0, 0, 1], [0, 0, 0], \"tip\", [30, 30, 20]);\n// Exact line solve:\nconst lineHit = arm.rotateAroundTo([0, 0, 1], [0, 0, 0], \"tip\", [30, 30, 0], { mode: 'line' });\n```\n\n## 6. `moveToLocal()` — offset from another shape's min corner\n\n```javascript\nconst part = box(20, 20, 30).moveToLocal(base, 10, 10, 10);\n```\n\n## 7. `translate()` — for simple offsets or bridging computed locations\n\n```javascript\nconst pipeLen = bb2.min[1] - bb1.max[1];\nconst pipe = cylinder(pipeLen, 5).pointAlong([0, 1, 0]).translate(40, (bb1.max[1] + bb2.min[1]) / 2, bb1.min[2] + 15);\n```\n\n## 8. `placeReference()` — align any anchor to a world coordinate\n\nPlace a shape so a named anchor point lands exactly where you want it. Accepts all built-in anchors (`'bottom'`, `'center'`, `'top-front-left'`, etc.) plus custom references from `withReferences()`.\n\n```javascript\n// Ground a shape — bottom face center at Z = 0\nconst grounded = shape.placeReference('bottom', [0, 0, 0])\n\n// Center at the world origin\nconst centered = shape.placeReference('center', [0, 0, 0])\n\n// Align left edge to X = 10\nconst aligned = shape.placeReference('left', [10, 0, 0])\n```\n\nAlso works with custom placement references for cross-file parts:\n\n```javascript\n// widget.forge.js — define once\nreturn union(base, post).withReferences({ points: { mount: [0, -16, -4] } });\n\n// importer — consume\nconst widget = require(\"./widget.forge.js\").placeReference(\"mount\", [120, 40, 0]);\n```\n\nFor cross-file parts needing proper alignment, prefer connectors over placement references.\n\n---\n\n<!-- generated/sketch.md -->\n\n# Sketch API\n\n2D geometry creation, transforms, booleans, constrained sketches, and extrusion.\n\n## Contents\n\n- [2D Sketch Primitives](#2d-sketch-primitives) — `path`, `stroke`, `rect`, `circle2d`, `roundedRect`, `polygon`, `ngon`, `ellipse`, `slot`, `arcSlot`, `star`\n- [2D Sketch Booleans](#2d-sketch-booleans) — `union2d`, `difference2d`, `intersection2d`\n- [2D Sketch Features](#2d-sketch-features) — `filletCorners`\n- [Tracked Solid Edge Features](#tracked-solid-edge-features) — `filletTrackedEdge`, `chamferTrackedEdge`\n- [2D Text](#2d-text) — `loadFont`, `text2d`, `textWidth`\n- [Constrained Sketches](#constrained-sketches) — `constrainedSketch`, `addRect`, `addPolygon`, `addRegularPolygon`\n- [2D Geometry Helpers](#2d-geometry-helpers) — `point`, `line`, `circle`, `degrees`, `radians`\n- [Sketch](#sketch) — Transforms, Booleans, Features, Promotion, Placement, Labels, Measurement\n- [ConstrainedSketchBuilder](#constrainedsketchbuilder) — Drawing, Entities, Geometric Constraints, Dimensional Constraints, Coincidence & Equality, Tangent Transitions, Shape Constraints, Positioning, Solving\n- [ConstraintSketch](#constraintsketch)\n- [SketchGroupBuilder](#sketchgroupbuilder)\n- [Point2D](#point2d)\n- [Line2D](#line2d)\n- [Circle2D](#circle2d)\n- [Rectangle2D](#rectangle2d)\n\n## Functions\n\n### 2D Sketch Primitives\n\n#### `path()` — Create a new [`PathBuilder`](/docs/curves#pathbuilder) for tracing a 2D outline point by point.\n\n[`PathBuilder`](/docs/curves#pathbuilder) is a fluent API for constructing 2D profiles using a mix of line segments, arcs, bezier curves, and splines. Always start with `.moveTo(x, y)` to set the starting point. Call `.close()` to get a filled `Sketch`, or `.stroke(width)` to thicken an open polyline into a solid profile.\n\nEdge labels can be assigned with `.label('name')` after any segment — they propagate through extrusion, revolve, loft, and sweep into named faces on the resulting [`Shape`](/docs/core#shape).\n\n```ts\n// Closed triangle\nconst triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();\n\n// L-shaped bracket as a stroke\nconst bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n\n// Labeled edges for downstream face references\nconst slot = path()\n .moveTo(0, 0)\n .lineTo(30, 0).label('bottom')\n .lineTo(30, 10)\n .lineTo(0, 10).label('top')\n .close();\n```\n\n```ts\npath(): PathBuilder\n```\n\n#### `stroke()` — Create a stroked polyline sketch from an array of 2D points.\n\n```ts\nstroke(points: [ number, number ][], width: number, join?: \"Round\" | \"Square\"): Sketch\n```\n\n#### `rect()` — Create a 2D rectangle centered at the origin.\n\n```ts\nrect(40, 20).extrude(5);\n```\n\n```ts\nrect(width: number, height: number): Sketch\n```\n\n#### `circle2d()` — Create a 2D circle centered at the origin.\n\nOmit `segments` for a smooth (auto-tessellated) circle. Pass an integer to get a regular polygon approximation — e.g. `6` for a hexagon, `8` for an octagon.\n\n```ts\ncircle2d(25).extrude(10); // smooth cylinder\ncircle2d(25, 6).extrude(10); // hexagonal prism\n```\n\n```ts\ncircle2d(radius: number, segments?: number): Sketch\n```\n\n#### `roundedRect()` — Create a 2D rectangle with rounded corners, centered at the origin.\n\nThe corner radius is automatically clamped to `min(width/2, height/2)` so it can never exceed the shape dimensions.\n\n```ts\nroundedRect(60, 30, 5).extrude(3);\n```\n\n```ts\nroundedRect(width: number, height: number, radius: number): Sketch\n```\n\n#### `polygon()` — Create a 2D polygon from an array of `[x, y]` points or `Point2D` objects.\n\nWinding order is normalized automatically — clockwise (CW) input is silently reversed to CCW before being passed to the geometry kernel.\n\n```ts\npolygon([[0, 0], [50, 0], [25, 40]]).extrude(5); // triangle\n```\n\n```ts\npolygon(points: ([ number, number ] | Point2D)[]): Sketch\n```\n\n#### `ngon()` — Create a regular polygon inscribed in a circle of the given radius.\n\n`radius` is the center-to-vertex (circumradius) distance. Use `sides` of `3` for a triangle, `6` for a hexagon, etc. The first vertex is at the top (−90° from +X).\n\n```ts\nngon(6, 20).extrude(10); // hexagonal prism, circumradius 20\n```\n\n```ts\nngon(sides: number, radius: number): Sketch\n```\n\n#### `ellipse()` — Create a 2D ellipse centered at the origin.\n\n```ts\nellipse(30, 15).extrude(5);\nellipse(30, 15, 32).extrude(5); // lower-resolution approximation\n```\n\n```ts\nellipse(rx: number, ry: number, segments?: number): Sketch\n```\n\n#### `slot()` — Create a slot (oblong / stadium shape) — a rectangle with semicircular ends, centered at the origin.\n\n```ts\nslot(40, 10).extrude(3); // 40mm long, 10mm wide slot\n```\n\n```ts\nslot(length: number, width: number): Sketch\n```\n\n#### `arcSlot()` — Create an arc-shaped slot (banana / annular sector) centered at the origin.\n\nThe slot is symmetric about the +X axis. The two ends are closed with semicircular caps. `pitchRadius` is the distance from the origin to the centerline of the slot, and `thickness` is the radial width of the slot.\n\n```ts\narcSlot(135, 74, 40).extrude(5); // pitch R135, 74° sweep, 40mm wide\n```\n\n```ts\narcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch\n```\n\n#### `star()` — Create a star shape with alternating outer and inner radii.\n\n```ts\nstar(5, 30, 12).extrude(4); // five-pointed star\n```\n\n```ts\nstar(points: number, outerR: number, innerR: number): Sketch\n```\n\n### 2D Sketch Booleans\n\n#### `union2d()` — Combine 2D sketches into a single profile using an additive boolean union.\n\nAccepts individual sketches or arrays: `union2d(a, b, c)` or `union2d([a, b, c])`. Uses Manifold's batch operation — faster than chaining `.add()` one by one when combining many sketches.\n\n```ts\nconst cross = union2d(rect(60, 10), rect(10, 60));\n```\n\n```ts\nunion2d(...inputs: SketchOperandInput[]): Sketch\n```\n\n#### `difference2d()` — Subtract one or more 2D sketches from a base sketch.\n\nThe first sketch is the base; all subsequent sketches are subtracted from it. Accepts individual sketches or arrays: `difference2d(base, c1, c2)` or `difference2d([base, c1, c2])`. Uses Manifold's batch operation — faster than chaining `.subtract()` one by one.\n\n```ts\nconst donut = difference2d(circle2d(50), circle2d(30));\n```\n\n```ts\ndifference2d(...inputs: SketchOperandInput[]): Sketch\n```\n\n#### `intersection2d()` — Keep only the area where all input sketches overlap (intersection boolean).\n\nAccepts individual sketches or arrays: `intersection2d(a, b)` or `intersection2d([a, b, c])`. Uses Manifold's batch operation — faster than chaining `.intersect()` one by one.\n\n```ts\nconst lens = intersection2d(circle2d(30).translate(-10, 0), circle2d(30).translate(10, 0));\n```\n\n```ts\nintersection2d(...inputs: SketchOperandInput[]): Sketch\n```\n\n### 2D Sketch Features\n\n#### `filletCorners()` — Create a polygon from points with specific corners rounded to arc fillets.\n\nEach corner spec identifies a vertex by its index in the `points` array and the desired fillet `radius`. Both convex and concave corners are supported.\n\nConstraints:\n\n- Collinear corners cannot be filleted (throws an error)\n- Two neighboring fillets whose tangent lengths overlap the same edge will throw\n- Radius must be positive and small enough to fit within the adjacent edge lengths\n\nUse `offset(-r).offset(+r)` instead if you want to round **all** convex corners uniformly. Use `filletCorners` when you need selective or mixed sharp/rounded profiles.\n\n```ts\nconst roof = filletCorners(roofPoints, [\n { index: 3, radius: 19 },\n { index: 4, radius: 19 },\n { index: 5, radius: 19 },\n]);\n```\n\n```ts\nfilletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch\n```\n\n`FilletCornerSpec`: `{ index: number, radius: number, segments?: number }`\n\n### Tracked Solid Edge Features\n\n#### `filletTrackedEdge()` — Round a tracked vertical solid edge with a circular fillet.\n\nCompiler-owned fillet for a narrow tracked-edge subset on solids.\n\nThis is **not** a general 2D sketch-corner fillet. It currently works only on tracked vertical edges from [`box()`](/docs/core#box) or `Rectangle2D` extrusions (plus rigid transforms and supported preserved descendants of those). Generic sketch extrudes, including `rect(...).extrude(...)`, are outside the supported subset right now.\n\n**Supported edges:**\n\n- Tracked vertical edges from [`box()`](/docs/core#box) or `Rectangle2D.extrude()`\n- Rigid transforms between tracked source and target\n- Untouched sibling tracked vertical edges after earlier `filletTrackedEdge`/`chamferTrackedEdge`\n\n**Not supported:** edges after shell, hole, cut, trim, difference, intersection, generic sketch extrudes, or tapered extrudes.\n\nCanonical quadrants: `vert-bl → [1,-1]`, `vert-br → [-1,-1]`, `vert-tr → [-1,1]`, `vert-tl → [1,1]`\n\n```ts\nconst base = Rectangle2D.fromDimensions(0, 0, 50, 50).extrude(20);\nconst filleted = filletTrackedEdge(base, base.edge('vert-br'), 5, [-1, -1]);\n```\n\n```ts\nfilletTrackedEdge(shape: Shape, edge: EdgeRef, radius: number, quadrant?: [ number, number ], segments?: number): Shape\n```\n\n**`EdgeRef`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `start` | `[ number, number, number ]` | Start point |\n| `end` | `[ number, number, number ]` | End point |\n| `query?` | `EdgeQueryRef` | Compiler-owned edge query when available. |\n| `curve?` | `EdgeCurve` | Exact or parametric curve family when the backend/source can identify one. |\n| `faceName?` | `string` | Owning face name when the edge is associated with one face in a larger topology. |\n| `name` | | — |\n\n#### `chamferTrackedEdge()` — Bevel a tracked vertical solid edge with a 45° chamfer.\n\nCompiler-owned chamfer for tracked vertical edges. Requires a compile-plan-covered target. This is not a general 2D sketch-corner tool; supported subset and quadrant semantics are the same as `filletTrackedEdge()` - see that function for details.\n\n```ts\nconst base = Rectangle2D.fromDimensions(0, 0, 50, 50).extrude(20);\nconst chamfered = chamferTrackedEdge(base, base.edge('vert-br'), 3, [-1, -1]);\n```\n\n```ts\nchamferTrackedEdge(shape: Shape, edge: EdgeRef, size: number, quadrant?: [ number, number ]): Shape\n```\n\n### 2D Text\n\n#### `loadFont()` — Pre-load and cache a font for use with `text2d()`.\n\nFonts are cached by their source string (or `cacheKey` for `ArrayBuffer` sources), so repeated calls with the same path are free. Pre-loading is useful when you call `text2d()` many times with the same font — it avoids repeated disk reads.\n\nBuilt-in font names that work everywhere (browser + CLI):\n\n- `'sans-serif'` or `'inter'` — bundled Inter Regular\n\n```ts\nconst font = loadFont('/path/to/Arial Bold.ttf');\ntext2d('Title', { size: 12, font }).extrude(1.5);\ntext2d('Subtitle', { size: 8, font }).extrude(1);\n```\n\n```ts\nloadFont(source: string | ArrayBuffer, cacheKey?: string): opentype.Font\n```\n\n#### `text2d()` — Build a filled 2D Sketch from a text string.\n\nThe Sketch origin is at the left end of the text baseline by default. Use `align` and `baseline` options to adjust placement. Text is rendered using the bundled Inter font by default, or any TTF/OTF/WOFF font you provide.\n\n`text2d()` creates real geometry. For temporary viewport annotations, prefer `Viewport.label()` so the text stays off the geometry and OCCT compile paths. Do not use either form of text to make unclear production geometry readable; model the physical artifact clearly instead.\n\nAlignment reference table:\n\n| `align` | `baseline` | Origin |\n|------------|--------------|-------------------------------------|\n| `'left'` | `'baseline'` | Bottom-left of first char (default) |\n| `'center'` | `'center'` | Dead center of text block |\n| `'right'` | `'top'` | Top-right corner |\n\n```ts\n// Extruded nameplate\ntext2d('FORGE CAD', { size: 8 }).extrude(1.2);\n\n// Centered label on the XY plane\ntext2d('V 2.0', { size: 6, align: 'center', baseline: 'center' });\n\n// Engraved text cut into the top face of a box\nconst label = text2d('REV A', { size: 5, align: 'center', baseline: 'center' });\nplate.subtract(label.onFace(plate, 'top', { protrude: -0.5 }).extrude(1));\n\n// Custom TTF font\ntext2d('Hello', { size: 10, font: '/path/to/Arial.ttf' }).extrude(1);\n\n// Pre-loaded font for reuse\nconst font = loadFont('/path/to/Arial Bold.ttf');\ntext2d('Title', { size: 12, font }).extrude(1.5);\n```\n\n```ts\ntext2d(content: string, options?: TextOptions): Sketch\n```\n\n**`TextOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `size?` | `number` | Cap height of the text in model units. All other dimensions (stroke weight, spacing) scale proportionally. |\n| `letterSpacing?` | `number` | Extra space between characters in model units. Negative values tighten the tracking. |\n| `align?` | `\"left\" \\| \"center\" \\| \"right\"` | Horizontal alignment relative to x = 0. - `'left'` — left edge at x = 0 (default) - `'center'` — centred on x = 0 - `'right'` — right edge at x = 0 |\n| `baseline?` | `\"baseline\" \\| \"center\" \\| \"top\"` | Vertical alignment relative to y = 0. - `'baseline'` — y = 0 is the text baseline (bottom of capital letters) - `'center'` — y = 0 is the vertical midpoint of the cap height - `'top'` — y = 0 is the top of capital letters |\n| `font?` | `string \\| opentype.Font` | Font to use for text rendering. - `'sans-serif'` or `'inter'` — bundled Inter font (works everywhere, including browser) - **file path** — path to a TTF, OTF, or WOFF font file (CLI/Node only) - **Font object** — a previously loaded opentype.js Font (from `loadFont()`) - **omitted** — uses the bundled Inter font (same as `'sans-serif'`) |\n| `flattenTolerance?` | `number` | Bezier flattening tolerance in model units. Smaller = more polygon segments = smoother curves. |\n\n#### `textWidth()` — Measure the rendered advance width of a string without creating any geometry.\n\nUses the same font metrics as `text2d()`. Useful for computing layout dimensions before building the actual sketch — e.g. sizing a plate to fit a label.\n\n```ts\nconst w = textWidth('SERIAL: 001', { size: 6 });\nconst plate = box(w + 10, 12, 2);\n```\n\n```ts\ntextWidth(content: string, options?: Pick<TextOptions, \"size\" | \"letterSpacing\" | \"font\">): number\n```\n\n### Constrained Sketches\n\n#### `constrainedSketch()` — Create a parametric 2D sketch driven by geometric constraints and a nonlinear solver.\n\n**Workflow**\n\n1. Create a builder with `constrainedSketch()`.\n2. Add geometry — points, lines, circles, arcs — using the builder methods.\n3. Add constraints (`horizontal`, `length`, `fix`, etc.) to drive the geometry.\n4. Call `.solve()` to run the solver and get a `ConstraintSketch` (which extends `Sketch`).\n\n```ts\nconst sk = constrainedSketch();\nconst p1 = sk.point(0, 0);\nconst p2 = sk.point(50, 0);\nconst l1 = sk.line(p1, p2);\nsk.fix(p1, 0, 0);\nsk.horizontal(l1);\nsk.length(l1, 50);\nreturn sk.solve().extrude(10);\n```\n\n**Solver status**\n\n```ts\nconst result = sk.solve();\nresult.constraintMeta.status; // 'fully' | 'under' | 'over' | 'over-redundant'\nresult.constraintMeta.dof; // 0 = fully constrained\nresult.constraintMeta.maxError; // residual — should be < 1e-6\nresult.inspect(); // human-readable summary\nresult.withUpdatedConstraint('cst-5', 120); // update a dimension without rebuilding\n```\n\n```ts\nconstrainedSketch(options?: ConstrainedSketchOptions): ConstrainedSketchBuilder\n```\n\n**`ConstrainedSketchOptions`**\n- `strict?: boolean` — When true, adding a constraint that cannot be satisfied throws instead of silently discarding it.\n\n#### `addRect()` — Add an axis-aligned rectangle concept to the builder.\n\nCreates 4 vertices (CCW: bl→br→tr→tl), 4 sides, 4 structural constraints (`horizontal`/`vertical` on each side), CCW winding, a center point, a loop, and a shape. Returns a `ConstrainedRect` handle with 4 DOF (x, y, width, height).\n\nUse `sk.rect()` as the shorthand builder method.\n\n```ts\nconst sk = constrainedSketch();\nconst r = sk.rect({ x: 0, y: 0, width: 100, height: 50 });\nsk.fix(r.bottomLeft, 0, 0);\nsk.length(r.bottom, 120); // override initial width\nreturn sk.solve().extrude(10);\n```\n\n```ts\naddRect(sk: ConstrainedSketchBuilder, options?: RectOptions): ConstrainedRect\n```\n\n**`RectOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `x?` | `number` | Bottom-left x coordinate. Default: 0. |\n| `y?` | `number` | Bottom-left y coordinate. Default: 0. |\n| `width?` | `number` | Width (along x). Default: 10. |\n| `height?` | `number` | Height (along y). Default: 10. |\n| `blockRotation?` | `boolean` | Prevent 180° rotation (ensures bottom edge points rightward). Default: false. |\n\n**`ConstrainedRect`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `bottom` | `LineId` | bottom-left → bottom-right |\n| `right` | `LineId` | bottom-right → top-right |\n| `top` | `LineId` | top-right → top-left |\n| `left` | `LineId` | top-left → bottom-left |\n| `center` | `PointId` | Center point constrained to the geometric center via `midpoint` on the diagonal. Can be used in further constraints: `sk.fix(rect.center, 0, 0)`, `sk.coincident(rect.center, other)`. |\n| `shape` | `ShapeId` | ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. |\n| `vertices` | `[ PointId, PointId, PointId, PointId ]` | CCW-ordered vertex array: [bottomLeft, bottomRight, topRight, topLeft]. |\n| `sides` | `[ LineId, LineId, LineId, LineId ]` | CCW-ordered side array: [bottom, right, top, left]. |\n| `bottomLeft`, `bottomRight`, `topRight`, `topLeft` | | — |\n\n#### `addPolygon()` — Add a general polygon concept to the builder.\n\nCreates n vertices and n sides (CCW: `sides[i]` from `vertices[i]` → `vertices[(i+1) % n]`). Applies a `ccw` constraint to enforce winding. All dimensional constraints (lengths, angles, position) are left to the caller.\n\nUse `sk.addPolygon()` as the shorthand builder method.\n\n```ts\nconst sk = constrainedSketch();\nconst tri = sk.addPolygon({ points: [[0,0],[100,0],[50,80]] });\nsk.fix(tri.vertex(0), 0, 0);\nsk.length(tri.side(0), 100);\nreturn sk.solve().extrude(5);\n```\n\n```ts\naddPolygon(sk: ConstrainedSketchBuilder, options: PolygonOptions): ConstrainedPolygon\n```\n\n**`PolygonOptions`**\n- `points: ReadonlyArray<readonly [ number, number ]>` — Initial vertex coordinates. Minimum 3 points.\n- `addLoop?: boolean` — Whether to register a closed loop for sketch generation. Default: true.\n- `blockRotation?: boolean` — Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false.\n\n**`ConstrainedPolygon`**\n- `vertices: PointId[]` — CCW-ordered PointIds.\n- `sides: LineId[]` — CCW-ordered LineIds. `sides[i]` runs from `vertices[i]` → `vertices[(i+1) % n]`.\n- `shape: ShapeId` — ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`.\n\n#### `addRegularPolygon()` — Add a regular n-gon concept to the builder.\n\nVertices are placed at `(cx + r·cos(startAngle + i·2π/n), cy + r·sin(...))`. Equal-radius and equal-side constraints enforce regularity (4 DOF: center x/y, radius, rotation). The center point is tracked by the solver and exposed via the returned handle.\n\nUse `sk.regularPolygon()` as the shorthand builder method.\n\n```ts\nconst sk = constrainedSketch();\nconst hex = sk.regularPolygon({ sides: 6, radius: 25 });\nsk.fix(hex.center, 0, 0);\nsk.length(hex.side(0), 30); // all sides change (equal constraint)\nreturn sk.solve().extrude(5);\n```\n\n```ts\naddRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions): ConstrainedRegularPolygon\n```\n\n**`RegularPolygonOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `sides` | `number` | Number of sides (minimum 3). |\n| `radius?` | `number` | Circumradius — distance from center to vertex. Default: 10. |\n| `cx?` | `number` | Center x coordinate. Default: 0. |\n| `cy?` | `number` | Center y coordinate. Default: 0. |\n| `startAngle?` | `number` | Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive). Default: 0 (rightmost vertex). |\n| `blockRotation?` | `boolean` | Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. |\n\n\n**`ConstrainedRegularPolygon`** extends ConstrainedPolygon\n- `center: PointId` — Center point. Use `sk.fix(poly.center, x, y)` to pin location, or `sk.coincident(poly.center, other)` to align with other geometry.\n\n### 2D Geometry Helpers\n\n#### `point()` — Create an analytic 2D point for measurement and construction geometry.\n\n```ts\nconst p = point(10, 20);\np.distanceTo(point(30, 40)); // Euclidean distance\np.midpointTo(point(30, 40)); // midpoint\np.translate(5, 5); // new shifted point\np.toTuple(); // [10, 20]\n```\n\n```ts\npoint(x: number, y: number): Point2D\n```\n\n#### `line()` — Create an analytic 2D line segment between two points.\n\n```ts\nconst l = line(0, 0, 50, 0);\nl.length; l.midpoint; l.angle; l.direction;\nl.parallel(10); // parallel line offset 10 (positive = left)\nl.intersect(l2); // Point2D — treats lines as infinite\nl.intersectSegment(l2); // Point2D or null — segments only\n\nLine2D.fromPointAndAngle(point(0, 0), 45, 100);\nLine2D.fromPointAndDirection(point(0, 0), [1, 1], 50);\n```\n\n```ts\nline(x1: number, y1: number, x2: number, y2: number): Line2D\n```\n\n#### `circle()` — Create an analytic 2D circle for measurement, construction, and extrusion.\n\n```ts\nconst c = circle(0, 0, 25);\nc.diameter; c.circumference; c.area;\nc.pointAtAngle(90); // Point2D at top (90° CCW from +X)\n\n// Extrude to cylinder with named faces\nconst cyl = c.extrude(30);\ncyl.face('top'); // FaceRef (planar)\ncyl.face('side'); // FaceRef (curved)\n\nCircle2D.fromDiameter(point(0, 0), 50);\n```\n\n```ts\ncircle(cx: number, cy: number, radius: number): Circle2D\n```\n\n#### `degrees()` — Identity function that returns degrees unchanged.\n\nUse for clarity when the unit of an angle value would otherwise be ambiguous — e.g. `param(\"Angle\", degrees(45))`.\n\n```ts\ndegrees(deg: number): number\n```\n\n#### `radians()` — Convert radians to degrees.\n\nForgeCAD's public API uses degrees throughout. Use this when you have a radian value (e.g. from `Math.atan2`) that you want to express in degrees.\n\n```ts\nradians(rad: number): number\n```\n\n---\n\n## Classes\n\n### `Sketch`\n\nImmutable 2D profile for extrusion, revolve, and other operations.\n\n`Sketch` wraps Manifold's `CrossSection` with a chainable 2D API. Every method returns a new `Sketch` — the original is never mutated. Colors, edge labels, and placement data are preserved through all transforms and boolean operations.\n\nSupported operations:\n\n- **Transforms** — `translate`, `rotate`, `rotateAround`, `scale`, `mirror`\n- **Booleans** — `add` (union), `subtract` (difference), `intersect`\n- **Operations** — `offset`, `simplify`\n- **Queries** — `area`, `bounds`, `isEmpty`, `numVert`\n- **3D operations** — `extrude`, `revolve`, `onFace`\n- **Regions** — `regions`, `region`\n- **Placement** — `attachTo`\n\nNamed anchor positions used by `attachTo()`: `'center'` | `'top-left'` | `'top-right'` | `'bottom-left'` | `'bottom-right'` | `'top'` | `'bottom'` | `'left'` | `'right'`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `cross` | `ProfileBackend` | — |\n\n**Transforms**\n\n#### `translate()` — Move the sketch by the given X and Y offset.\n\n```ts\ntranslate(x: number, y?: number): Sketch\n```\n\n#### `rotate()` — Rotate the sketch around its bounding-box center.\n\n```ts\nrotate(degrees: number): Sketch\n```\n\n#### `rotateAround()` — Rotate the sketch around a specific pivot point.\n\n```ts\nrect(20, 20).rotateAround(45, [0, 0]);\n```\n\n```ts\nrotateAround(degrees: number, pivot: [ number, number ]): Sketch\n```\n\n#### `scale()` — Scale the sketch relative to its bounding-box center.\n\nPass a single number for uniform scaling, or `[sx, sy]` for per-axis scaling.\n\n```ts\nscale(v: number | [ number, number ]): Sketch\n```\n\n#### `scaleAround()` — Scale the sketch relative to an arbitrary pivot point.\n\n```ts\nscaleAround(pivot: [ number, number ], v: number | [ number, number ]): Sketch\n```\n\n#### `mirror()` — Mirror the sketch across a line through its bounding-box center.\n\n`normal` is the normal vector of the mirror line (not the line direction). For example, `[1, 0]` mirrors across a vertical line (Y axis direction), and `[0, 1]` mirrors across a horizontal line.\n\n```ts\nmirror(normal: [ number, number ]): Sketch\n```\n\n#### `mirrorThrough()` — Mirror the sketch across a line defined by a point and a normal direction.\n\n```ts\nmirrorThrough(point: [ number, number ], normal: [ number, number ]): Sketch\n```\n\n**Booleans**\n\n#### `add()` — Add (union) one or more sketches to this sketch.\n\nAccepts individual sketches or arrays: `sketch.add(a, b)` or `sketch.add([a, b])`. For combining many sketches at once, prefer the free function `union2d()` which uses Manifold's batch operation and is faster than chaining.\n\n```ts\ncircle2d(20).add(rect(10, 30)).extrude(5);\n```\n\n```ts\nadd(...others: SketchOperandInput[]): Sketch\n```\n\n#### `subtract()` — Subtract one or more sketches from this sketch.\n\nAccepts individual sketches or arrays: `sketch.subtract(a, b)` or `sketch.subtract([a, b])`. For subtracting many cutters at once, prefer the free function `difference2d()`.\n\n```ts\nrect(40, 40).subtract(circle2d(10)).extrude(5);\n```\n\n```ts\nsubtract(...others: SketchOperandInput[]): Sketch\n```\n\n#### `intersect()` — Intersect this sketch with one or more others (keep overlapping area only).\n\nAccepts individual sketches or arrays: `sketch.intersect(a, b)` or `sketch.intersect([a, b])`. For intersecting many sketches, prefer the free function `intersection2d()`.\n\n```ts\nintersect(...others: SketchOperandInput[]): Sketch\n```\n\n**Features**\n\n#### `offset()` — Inflate (positive delta) or deflate (negative delta) the sketch contour.\n\nUse `offset(-r).offset(+r)` to round every convex corner of a closed sketch.\n\n- `'Round'` — smooth arc at each corner (default)\n- `'Square'` — flat mitered extension\n- `'Miter'` — sharp pointed extension\n\n```ts\nrect(40, 20).offset(3); // expand by 3\nrect(40, 20).offset(-2).offset(2); // round all convex corners\n```\n\n```ts\noffset(delta: number, join?: \"Square\" | \"Round\" | \"Miter\"): Sketch\n```\n\n#### `regions()` — Decompose this sketch into its distinct filled regions, sorted largest-first by area.\n\nA single sketch can contain several disconnected filled areas (e.g., two separate rectangles, or a ring shape with a hole). This method enumerates all top-level connected regions as independent `Sketch` objects, each with its own outer boundary and associated holes.\n\n```ts\nconst pair = union2d(rect(40, 40), rect(40, 40).translate(60, 0));\nconst [left, right] = pair.regions(); // largest first\nleft.extrude(5);\n```\n\n```ts\nregions(): Sketch[]\n```\n\n#### `region()` — Select the single filled region that contains the given 2D seed point.\n\nThe seed must lie strictly inside the filled area — not on a boundary edge and not inside a hole. Throws a descriptive error if the seed is outside all regions. If unsure where regions are, use `.regions()` first — each result has `.bounds()`.\n\n```ts\nconst donut = circle2d(50).subtract(circle2d(30));\ndonut.region([40, 0]).extrude(10); // seed at radius 40, inside the ring\n```\n\n```ts\nregion(seed: [ number, number ]): Sketch\n```\n\n**Promotion**\n\n#### `extrude()` — Extrude this 2D sketch along Z to create a 3D solid. Supports twist and scale tapering.\n\n```ts\nextrude(height: number, opts?: { twist?: number; divisions?: number; scaleTop?: number | [ number, number ]; }): Shape\n```\n\n#### `revolve()` — Revolve this 2D sketch around the Y axis to create a 3D solid of revolution.\n\n```ts\nrevolve(degrees?: number, segments?: number): Shape\n```\n\n**Placement**\n\n#### `attachTo()` — Position this sketch relative to another using named anchor points.\n\nComputes the translation needed to align `selfAnchor` on this sketch with `targetAnchor` on the target sketch, then applies an optional pixel-exact offset.\n\nAnchor positions: `'center'` | `'top-left'` | `'top-right'` | `'bottom-left'` | `'bottom-right'` | `'top'` | `'bottom'` | `'left'` | `'right'`\n\n```ts\nconst arm = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left');\nconst shifted = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left', [5, 0]);\n```\n\n```ts\nattachTo(target: Sketch, targetAnchor: Anchor, selfAnchor?: Anchor, offset?: [ number, number ]): Sketch\n```\n\n#### `onFace()` — Place this sketch on a face or planar target in 3D space.\n\nUse this when a 2D profile should be oriented onto a 3D face before extrusion or other downstream operations.\n\n```ts\nonFace(parentOrFace: Shape | { toShape(): Shape; } | { _bbox(): { min: number[]; max: number[]; }; } | FaceRef, faceOrOpts?: \"front\" | \"back\" | \"left\" | \"right\" | \"top\" | \"bottom\" | string | FaceRef | { u?: number; v?: number; protrude?: number; selfAnchor?: Anchor; }, opts?: { u?: number; v?: number; protrude?: number; selfAnchor?: Anchor; }): Sketch\n```\n\n**Labels**\n\n#### `labelEdge()` — Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch.\n\n```ts\nlabelEdge(name: string): Sketch\n```\n\n#### `labelEdges()` — Label edges in winding order, or by named map for rect.\n\nPositional: `labelEdges('bottom', 'right', 'top', 'left')` — one per edge, `null` to skip. Named (rect only): `labelEdges({ bottom: 'floor', top: 'ceiling' })`. Returns a new sketch.\n\n```ts\nlabelEdges(...args: (string | null)[] | [ Record<string, string> ]): Sketch\n```\n\n#### `edgeLabels()` — List current edge label names.\n\n```ts\nedgeLabels(): string[]\n```\n\n#### `prefixLabels()` — Prefix all edge labels. Returns a new sketch with prefixed labels.\n\n```ts\nprefixLabels(prefix: string): Sketch\n```\n\n#### `renameLabel()` — Rename a single edge label. Returns a new sketch.\n\n```ts\nrenameLabel(from: string, to: string): Sketch\n```\n\n#### `dropLabels()` — Remove specific labels. Returns a new sketch.\n\n```ts\ndropLabels(...names: string[]): Sketch\n```\n\n#### `dropAllLabels()` — Remove all labels. Returns a new sketch.\n\n```ts\ndropAllLabels(): Sketch\n```\n\n**Measurement**\n\n#### `area()` — Return the total filled area of the sketch.\n\n```ts\narea(): number\n```\n\n#### `bounds()` — Return the axis-aligned bounding box of the sketch.\n\n```ts\nbounds(): ProfileBounds\n```\n\n#### `isEmpty()` — Return `true` if the sketch contains no filled area.\n\n```ts\nisEmpty(): boolean\n```\n\n#### `numVert()` — Return the number of vertices in the polygon representation of the sketch contours.\n\n```ts\nnumVert(): number\n```\n\n#### `toPolygons()` — Return the sketch as a list of polygons matching its contour topology.\n\nUseful when you need raw polygon data for inspection or custom export.\n\n```ts\ntoPolygons(): number[][][]\n```\n\n**Other**\n\n#### `color()` — Set the display color of this sketch.\n\nColor is preserved through all transforms and boolean operations. Pass `undefined` to clear the color.\n\n```ts\ncircle2d(20).color('#ff0000').extrude(5);\n```\n\n```ts\ncolor(value: string | undefined): Sketch\n```\n\n#### `clone()` — Create an explicit copy of this sketch for branching variants.\n\nBecause all Sketch operations are immutable, `clone()` is rarely needed. Use it when you want to assign the same sketch to multiple names and continue modifying each independently without confusion.\n\n```ts\nclone(): Sketch\n```\n\n### `ConstrainedSketchBuilder`\n\n**Drawing**\n\n#### `moveTo()` — Move the cursor to `(x, y)` and start a new profile loop.\n\n```ts\nmoveTo(x: number, y: number): this\n```\n\n#### `lineTo()` — Draw a line from the current cursor to `(x, y)`.\n\n```ts\nlineTo(x: number, y: number): this\n```\n\n#### `lineH()` — Draw a horizontal line of length `dx` from the current cursor.\n\n```ts\nlineH(dx: number): this\n```\n\n#### `lineV()` — Draw a vertical line of length `dy` from the current cursor.\n\n```ts\nlineV(dy: number): this\n```\n\n#### `lineAngled()` — Draw a line of the given `length` at `degrees` from +X.\n\n```ts\nlineAngled(length: number, degrees: number): this\n```\n\n#### `arcTo()` — Draw a circular arc from the current cursor to `(x, y)` with the given radius.\n\n```ts\narcTo(x: number, y: number, radius: number, clockwise?: boolean): this\n```\n\n#### `arcByCenter()` — Create an arc from an explicit center point and endpoint IDs.\n\n```ts\narcByCenter(centerId: PointId, startId: PointId, endId: PointId, clockwise?: boolean, name?: string, fixedRadius?: boolean): ArcId\n```\n\n#### `bezier()` — Create a cubic Bezier curve from four control points.\n\n```ts\nbezier(p0: any, p1: any, p2: any, p3: any, name?: string): BezierId\n```\n\n#### `bezierTo()` — Draw a cubic Bezier from the current cursor to `(x3, y3)`.\n\n```ts\nbezierTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): this\n```\n\n#### `blendTo()` — Draw a smooth Bezier tangent to the previous arc.\n\n```ts\nblendTo(x: number, y: number, weight?: number): this\n```\n\n#### `label()` — Label the current path segment.\n\n```ts\nlabel(name: string): this\n```\n\n#### `close()` — Close the current path and register the loop.\n\n```ts\nclose(): this\n```\n\n#### `addLoopCircle()` — Add a circle loop to the path.\n\n```ts\naddLoopCircle(center: PointId, radius: number, segments?: number): this\n```\n\n#### `addLoop()` — Add a closed polygon loop from point IDs.\n\n```ts\naddLoop(points: any[]): this\n```\n\n#### `addProfileLoop()` — Add a profile loop from prebuilt line/arc/bezier segments.\n\n```ts\naddProfileLoop(segments: Array<{ kind: \"line\"; line: any; } | { kind: \"arc\"; arc: any; } | { kind: \"bezier\"; bezier: any; }>): this\n```\n\n**Entities**\n\n#### `point()` — Add a free point to the sketch at `(x, y)`.\n\nIf `x` or `y` are omitted, the point is placed at the bounding-box center of existing geometry so it starts near other entities rather than at the origin. Throws if either coordinate is `NaN` or `Infinity`.\n\n```ts\npoint(x?: number, y?: number, fixed?: boolean): PointId\n```\n\n#### `pointAt()` — Return the `PointId` of the point created at the given insertion index.\n\n```ts\npointAt(index: number): PointId\n```\n\n#### `line()` — Connect two existing points with a line segment.\n\nPass `construction = true` for a helper line that participates in constraints but is excluded from the solved sketch output (not part of any profile loop).\n\n```ts\nconst axis = sk.line(sk.point(0, -50), sk.point(0, 50), true);\nsk.symmetric(p1, p2, axis);\n```\n\n```ts\nline(a: PointId, b: PointId, construction?: boolean, name?: string): LineId\n```\n\n#### `lineAt()` — Return the `LineId` of the line created at the given insertion index.\n\n```ts\nlineAt(index: number): LineId\n```\n\n#### `circle()` — Add a circle to the sketch with the given center point and initial radius.\n\nThe radius is a starting value — if you add a `radius()` or `diameter()` constraint, the solver will adjust it. Non-construction circles automatically register a loop.\n\n```ts\ncircle(center: PointId, radius: number, construction?: boolean, segments?: number, name?: string): CircleId\n```\n\n#### `circleAt()` — Return the `CircleId` of the circle created at the given insertion index.\n\n```ts\ncircleAt(index: number): CircleId\n```\n\n#### `shape()` — Register a named shape (closed polygon) from an ordered list of line IDs.\n\nThe `ShapeId` can be passed to `shapeWidth()`, `shapeHeight()`, `shapeArea()`, `shapeCentroidX()`, `shapeCentroidY()`, and `shapeEqualCentroid()` constraints. Shape registration is done automatically by concept factories like `rect()` and `addPolygon()`.\n\n```ts\nshape(lines: LineId[]): ShapeId\n```\n\n#### [`group()`](/docs/core#group) — Create a rigid-body group with a local coordinate frame.\n\nPoints and lines added to the group move together as a unit — the solver sees 3 DOF (x, y, θ) instead of 2N per point. After configuring the group, call `.done()` to register it and receive a `SketchGroupHandle`.\n\nGroup points are addressable by their `PointId` in all sketch constraints (e.g. `sk.coincident`, `sk.distance`) just like any other points.\n\n```ts\nconst g = sk.group({ x: 50, y: 30 });\nconst p0 = g.point(0, 0); // local origin → world (50, 30)\nconst p1 = g.point(100, 0); // local (100,0) → world (150, 30)\nconst l = g.line(p0, p1);\ng.fixRotation();\nconst handle = g.done();\n// p0, p1 work in constraints like any other PointId:\nsk.coincident(p0, someExternalPoint);\n```\n\n```ts\ngroup(opts?: { x?: number; y?: number; theta?: number; id?: string; }): SketchGroupBuilder\n```\n\n#### `rect()` — Add an axis-aligned rectangle concept. Returns a `ConstrainedRect` handle with named vertices, sides, and center.\n\n```ts\nrect(options?: RectOptions): ConstrainedRect\n```\n\n#### `addPolygon()` — Add a general polygon concept (CCW winding enforced). Returns a `ConstrainedPolygon` handle.\n\n```ts\naddPolygon(options: PolygonOptions): ConstrainedPolygon\n```\n\n#### `regularPolygon()` — Add a regular n-gon concept (equal sides, CCW winding). Returns a `ConstrainedRegularPolygon` handle with a center point.\n\n```ts\nregularPolygon(options: RegularPolygonOptions): ConstrainedRegularPolygon\n```\n\n#### `groupRect()` — Add a rigid rectangle as a group concept. Returns a `ConstrainedGroupRect` handle with named vertices and sides. The rectangle is fixed in shape — only position (and optionally rotation) varies.\n\n```ts\ngroupRect(options: GroupRectOptions): ConstrainedGroupRect\n```\n\n**Geometric Constraints**\n\n#### `horizontal()` — Constrain a line to be horizontal (parallel to the X axis).\n\n```ts\nhorizontal(line: any): this\n```\n\n#### `vertical()` — Constrain a line to be vertical (parallel to the Y axis).\n\n```ts\nvertical(line: any): this\n```\n\n#### `parallel()` — Constrain two lines to be parallel.\n\n```ts\nparallel(a: any, b: any): this\n```\n\n#### `sameDirection()` — Constrain two lines to point in the same direction.\n\n```ts\nsameDirection(a: any, b: any): this\n```\n\n#### `oppositeDirection()` — Constrain two lines to point in opposite directions.\n\n```ts\noppositeDirection(a: any, b: any): this\n```\n\n#### `perpendicular()` — Constrain two lines to be perpendicular.\n\n```ts\nperpendicular(a: any, b: any): this\n```\n\n#### `tangent()` — Constrain a line/circle or circle/circle tangency relationship.\n\n```ts\ntangent(a: any, b: any): this\n```\n\n#### `collinear()` — Constrain a point to lie on the infinite extension of a line.\n\n```ts\ncollinear(point: any, line: any): this\n```\n\n#### `symmetric()` — Constrain two points to be symmetric about an axis line.\n\n```ts\nsymmetric(a: any, b: any, axis: any): this\n```\n\n#### `blockRotation()` — Prevent 180° rotation of a polygon by anchoring its first edge.\n\n```ts\nblockRotation(points: any[], axis?: \"x\" | \"y\"): this\n```\n\n**Dimensional Constraints**\n\n#### `distance()` — Constrain the Euclidean distance between two points.\n\n```ts\ndistance(a: any, b: any, value: number): this\n```\n\n#### `length()` — Constrain the length of a line segment.\n\n```ts\nlength(line: any, value: number): this\n```\n\n#### `angle()` — Constrain the signed angle from line `a` to line `b`.\n\n```ts\nangle(a: any, b: any, value: number): this\n```\n\n#### `radius()` — Constrain the radius of a circle.\n\n```ts\nradius(circle: any, value: number): this\n```\n\n#### `diameter()` — Constrain the diameter of a circle.\n\n```ts\ndiameter(circle: any, value: number): this\n```\n\n#### `hDistance()` — Constrain the horizontal distance between two points.\n\n```ts\nhDistance(a: any, b: any, value: number): this\n```\n\n#### `vDistance()` — Constrain the vertical distance between two points.\n\n```ts\nvDistance(a: any, b: any, value: number): this\n```\n\n#### `pointLineDistance()` — Constrain the signed perpendicular distance from a point to a line.\n\n```ts\npointLineDistance(point: any, line: any, value: number): this\n```\n\n#### `lineDistance()` — Constrain the perpendicular offset distance between two lines.\n\n```ts\nlineDistance(a: any, b: any, value: number): this\n```\n\n#### `absoluteAngle()` — Constrain the absolute angle of a line measured from +X.\n\n```ts\nabsoluteAngle(line: any, value: number): this\n```\n\n#### `arcLength()` — Constrain the arc length of an arc.\n\n```ts\narcLength(arc: any, value: number): this\n```\n\n#### `equalRadius()` — Constrain two circles to have equal radii.\n\n```ts\nequalRadius(a: any, b: any): this\n```\n\n#### `angleBetween()` — Constrain the unsigned angle between two lines.\n\n```ts\nangleBetween(a: any, b: any, value: number): this\n```\n\n**Coincidence & Equality**\n\n#### `equal()` — Constrain two lines to have equal length.\n\n```ts\nequal(a: any, b: any): this\n```\n\n#### `coincident()` — Constrain two points to coincide.\n\n```ts\ncoincident(a: any, b: any): this\n```\n\n#### `concentric()` — Constrain two circles to share a center.\n\n```ts\nconcentric(a: any, b: any): this\n```\n\n#### `fix()` — Pin a point at a specific world location.\n\n```ts\nfix(point: any, x?: number, y?: number): this\n```\n\n#### `midpoint()` — Constrain a point to lie at the midpoint of a line.\n\n```ts\nmidpoint(point: any, line: any): this\n```\n\n#### `pointOnCircle()` — Constrain a point to lie on the perimeter of a circle.\n\n```ts\npointOnCircle(point: any, circle: any): this\n```\n\n#### `pointOnLine()` — Constrain a point to lie on the bounded segment of a line.\n\n```ts\npointOnLine(point: any, line: any): this\n```\n\n#### `ccw()` — Constrain all given points to be in counter-clockwise order.\n\n```ts\nccw(...points: any[]): this\n```\n\n**Tangent Transitions**\n\n#### `lineTangentArc()` — Constrain a line to be tangent to an arc at its start or end point.\n\n```ts\nlineTangentArc(line: any, arc: any, atStart: boolean): this\n```\n\n#### `arcTangentArc()` — Constrain two arcs to be tangent at their shared junction point.\n\n```ts\narcTangentArc(arcA: any, arcB: any, aAtStart?: boolean, bAtStart?: boolean): this\n```\n\n#### `bezierTangentArc()` — Constrain a Bezier to be tangent to an arc at one endpoint.\n\n```ts\nbezierTangentArc(bezier: any, arc: any, atBezierStart: boolean, atArcStart: boolean): this\n```\n\n#### `smoothBlend()` — Create a Bezier blend between two arcs.\n\n```ts\nsmoothBlend(arc1: any, arc2: any, options?: { weight?: number; arc1End?: \"start\" | \"end\"; arc2End?: \"start\" | \"end\"; }): BezierId\n```\n\n**Shape Constraints**\n\n#### `shapeWidth()` — Constrain a shape's width.\n\n```ts\nshapeWidth(shape: any, value: number): this\n```\n\n#### `shapeHeight()` — Constrain a shape's height.\n\n```ts\nshapeHeight(shape: any, value: number): this\n```\n\n#### `shapeCentroidX()` — Constrain a shape's centroid X position.\n\n```ts\nshapeCentroidX(shape: any, value: number): this\n```\n\n#### `shapeCentroidY()` — Constrain a shape's centroid Y position.\n\n```ts\nshapeCentroidY(shape: any, value: number): this\n```\n\n#### `shapeArea()` — Constrain a shape's area.\n\n```ts\nshapeArea(shape: any, value: number): this\n```\n\n#### `shapeEqualCentroid()` — Constrain two shapes to have the same centroid.\n\n```ts\nshapeEqualCentroid(a: any, b: any): this\n```\n\n**Positioning**\n\n#### `offsetX()` — Constrain the horizontal (X-axis) offset between two lines. Uses the start-point of each line to measure horizontal distance. `value` is the signed distance: b.startPt.x − a.startPt.x = value.\n\n```ts\noffsetX(a: any, b: any, value: number): this\n```\n\n#### `offsetY()` — Constrain the vertical (Y-axis) offset between two lines. Uses the start-point of each line to measure vertical distance. `value` is the signed distance: b.startPt.y − a.startPt.y = value.\n\n```ts\noffsetY(a: any, b: any, value: number): this\n```\n\n#### `importPoint()` — Import a `Point2D` object into the sketch.\n\n```ts\nimportPoint(pt: { x: number; y: number; }, fixed?: boolean): PointId\n```\n\n#### `importLine()` — Import a `Line2D` object into the sketch.\n\n```ts\nimportLine(l: { start: { x: number; y: number; }; end: { x: number; y: number; }; }, fixed?: boolean): LineId\n```\n\n#### `importRectangle()` — Import a `Rectangle2D` as four points and four lines.\n\n```ts\nimportRectangle(r: { vertices: [ { x: number; y: number; }, { x: number; y: number; }, { x: number; y: number; }, { x: number; y: number; } ]; }, fixed?: boolean): { ... }\n```\n\n#### `referencePoint()` — Add a fixed reference point at `(x, y)`.\n\n```ts\nreferencePoint(x: number, y: number): PointId\n```\n\n#### `referenceLine()` — Add a fixed reference line from `(x1, y1)` to `(x2, y2)`.\n\n```ts\nreferenceLine(x1: number, y1: number, x2: number, y2: number): LineId\n```\n\n#### `referenceFrom()` — Import a single named entity from a solved sketch as fixed reference geometry.\n\n```ts\nreferenceFrom(source: ConstraintSketch, entityId: string): PointId | LineId | null\n```\n\n#### `referenceAllFrom()` — Import all non-construction entities from a solved sketch as fixed references.\n\n```ts\nreferenceAllFrom(source: ConstraintSketch): { points: Map<string, PointId>; lines: Map<string, LineId>; }\n```\n\n**Solving**\n\n#### `constrain()` — Add a raw constraint object to the builder.\n\n```ts\nconstrain(constraint: Omit<SketchConstraint, \"id\">): this\n```\n\n#### `solve()` — Run the constraint solver and return a solved sketch.\n\nThe returned `ConstraintSketch` extends `Sketch` and can be used directly in all 3D operations (`extrude`, `revolve`, etc.). It also exposes `constraintMeta` with the solver status:\n\n```ts\nconst result = sk.solve();\nresult.constraintMeta.status; // 'fully' | 'under' | 'over' | 'over-redundant'\nresult.constraintMeta.dof; // 0 = fully constrained\nresult.constraintMeta.maxError; // residual — should be < 1e-6\nresult.inspect(); // human-readable summary\nresult.withUpdatedConstraint('cst-5', 120); // update a dimension without rebuilding\n```\n\n**Troubleshooting**\n\n- **Under-constrained (dof > 0)** — add `fix()`, `length()`, or other dimensional constraints.\n- **Over-constrained** — conflicting constraints are auto-rejected. Check `result.constraintMeta.constraints` and `result.inspect()`.\n- **maxError > 1e-6** — solver did not converge; check for contradictory constraints.\n\n```ts\nsolve(options?: SolveOptions): ConstraintSketch | Sketch\n```\n\n#### `solveConstraintsOnly()` — Run the solver without building a full `ConstraintSketch`.\n\nLighter than `solve()` — skips profile and DOF analysis. Useful for lightweight constraint validation or progress monitoring mid-construction.\n\n```ts\nsolveConstraintsOnly(options?: SolveOptions): { maxError: number; rejectedCount: number; definition: ConstraintDefinition; }\n```\n\n#### `route()` — Start a directional route from coordinates.\n\nReturns a [`RouteBuilder`](/docs/viewport#routebuilder) - describe the path with up/down/left/right/arcLeft/arcRight. Each method returns the entity ID (`LineId` or `ArcId`) for use in `sk.*` constraints.\n\n```js\nconst r = sk.route(0, 0);\nconst stem = r.up(18);\nr.arcLeft(8.9);\nconst neck = r.down();\nr.done();\nsk.offsetX(stem, neck, 10.8);\n```\n\n```ts\nroute(x: number, y: number): RouteBuilder\n```\n\n### `ConstraintSketch`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `constraintMeta` | `SketchConstraintMeta` | — |\n| `definition` | `ConstraintDefinition` | — |\n\n**Methods:**\n\n#### `detectArrangement()` — Enumerate all bounded regions formed by the line arrangement of this sketch. Construction lines are excluded. Regions are returned largest-first by area.\n\n```ts\ndetectArrangement(): Sketch[]\n```\n\n#### `detectArrangementRegion()` — Select the single arrangement region that contains the given seed point. Throws if no region contains the seed.\n\n```ts\ndetectArrangementRegion(_seed: [ number, number ]): Sketch\n```\n\n#### `toPolyline()` — Return the solved constrained path as a sampled 2D polyline.\n\nUse this when a construction rail was authored with `constrainedSketch()` and should feed another operation such as `Loft.pathOnXz(...)`. The sketch must contain exactly one profile path.\n\n```ts\ntoPolyline(samples?: number): [ number, number ][]\n```\n\n#### `withUpdatedConstraint()` — Re-solve the sketch after changing the value of one existing constraint.\n\nUse this for interactive dimension edits without rebuilding the whole sketch graph. It attempts a warm-started solve first, then falls back to a full solve if needed.\n\n```ts\nwithUpdatedConstraint(constraintId: string, value: number): ConstraintSketch\n```\n\n#### `inspect()` — Return a human-readable diagnostic string of the solved state.\n\n```ts\ninspect(): string\n```\n\n### `SketchGroupBuilder`\n\n#### `point()` — Add a point in local coordinates. Returns its globally-addressable PointId.\n\n```ts\npoint(lx: number, ly: number): PointId\n```\n\n#### `line()` — Connect two group points with a line. Both must be PointIds from this group.\n\n```ts\nline(a: PointId, b: PointId, name?: string): LineId\n```\n\n#### `fixRotation()` — Freeze rotation (theta). Group can still translate - 2 DOF remain.\n\n```ts\nfixRotation(): this\n```\n\n#### `fix()` — Freeze all 3 DOF - group is completely fixed.\n\n```ts\nfix(): this\n```\n\n#### `done()` — Finalize and register the group with the builder.\n\n```ts\ndone(): SketchGroupHandle\n```\n\n### `Point2D`\n\nAn immutable 2D point with measurement and construction helpers.\n\nUsed as construction geometry in sketches, constraints, and analytic measurements. All methods return new instances — `Point2D` is immutable.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `x` | `number` | — |\n| `y` | `number` | — |\n\n**Methods:**\n\n#### `distanceTo()` — Measure straight-line distance to another point.\n\n```ts\ndistanceTo(other: Point2D): number\n```\n\n#### `midpointTo()` — Compute the midpoint between this point and another point.\n\n```ts\nmidpointTo(other: Point2D): Point2D\n```\n\n#### `translate()` — Return a point shifted by the given delta.\n\n```ts\ntranslate(dx: number, dy: number): Point2D\n```\n\n#### `toTuple()` — Convert this point to a plain `[x, y]` tuple.\n\n```ts\ntoTuple(): [ number, number ]\n```\n\n### `Line2D`\n\nAn immutable 2D line segment with length, angle, intersection, and parallel helpers.\n\nProvides both segment-only (`intersectSegment`) and infinite-line (`intersect`) intersection queries. All methods return new instances.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `start` | `Point2D` | — |\n| `end` | `Point2D` | — |\n\n**Methods:**\n\n#### `length()` — Length of the line segment.\n\n```ts\nget length(): number\n```\n\n#### `midpoint()` — Midpoint of the line segment.\n\n```ts\nget midpoint(): Point2D\n```\n\n#### `angle()` — Direction angle in degrees, measured CCW from +X.\n\n```ts\nget angle(): number\n```\n\n#### `direction()` — Unit direction vector from start to end.\n\n```ts\nget direction(): [ number, number ]\n```\n\n#### `parallel()` — Create a parallel line offset by the given distance.\n\nPositive distance shifts to the left of the line direction.\n\n```ts\nparallel(distance: number): Line2D\n```\n\n#### `intersect()` — Intersect this line with another infinite line.\n\n```ts\nintersect(other: Line2D): Point2D | null\n```\n\n#### `intersectSegment()` — Intersect this line with another as bounded segments.\n\n```ts\nintersectSegment(other: Line2D): Point2D | null\n```\n\n#### `fromCoordinates()` — Create a line from raw coordinates.\n\n```ts\nstatic fromCoordinates(x1: number, y1: number, x2: number, y2: number): Line2D\n```\n\n#### `fromPointAndAngle()` — Create a line from a start point, angle, and length.\n\n```ts\nstatic fromPointAndAngle(origin: Point2D, angleDeg: number, length: number): Line2D\n```\n\n#### `fromPointAndDirection()` — Create a line from a start point, direction vector, and length.\n\n```ts\nstatic fromPointAndDirection(origin: Point2D, dir: [ number, number ], length: number): Line2D\n```\n\n### `Circle2D`\n\nAn immutable 2D circle with area, circumference, and extrusion support.\n\nExtruding a `Circle2D` produces a cylinder with named `top`, `bottom`, and `side` faces accessible via the topology API.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `center` | `Point2D` | — |\n| `radius` | `number` | — |\n\n**Methods:**\n\n#### `diameter()` — Diameter of the circle.\n\n```ts\nget diameter(): number\n```\n\n#### `circumference()` — Circumference of the circle.\n\n```ts\nget circumference(): number\n```\n\n#### `area()` — Area of the circle.\n\n```ts\nget area(): number\n```\n\n#### `pointAtAngle()` — Return a point on the circle at the given angle.\n\n```ts\npointAtAngle(angleDeg: number): Point2D\n```\n\n#### `translate()` — Return a translated circle.\n\n```ts\ntranslate(dx: number, dy: number): Circle2D\n```\n\n#### `toSketch()` — Convert this circle to a sketch profile.\n\n```ts\ntoSketch(segments?: number): Sketch\n```\n\n#### `extrude()` — Extrude the circle into a solid cylinder.\n\n```ts\nextrude(height: number, segments?: number): Shape\n```\n\n#### `fromCenterAndRadius()` — Create a circle from its center and radius.\n\n```ts\nstatic fromCenterAndRadius(center: Point2D, radius: number): Circle2D\n```\n\n#### `fromDiameter()` — Create a circle from its center and diameter.\n\n```ts\nstatic fromDiameter(center: Point2D, diameter: number): Circle2D\n```\n\n### `Rectangle2D`\n\nA rectangle with named sides, vertices, and extrusion support.\n\nSides are named based on the rectangle's local orientation at construction time. Vertices go: bottom-left, bottom-right, top-right, top-left (CCW).\n\nUse `rect()` for the normal centered sketch primitive. Use `Rectangle2D` when you need named sides/vertices, or an extrusion with tracked vertical edges such as `vert-br` for `filletTrackedEdge()` / `chamferTrackedEdge()`.\n\nExtruding a `Rectangle2D` produces a [`Shape`](/docs/core#shape) with named faces: `top`, `bottom`, `side-left`, `side-right`, `side-top`, `side-bottom`. These are accessible via the topology API (`.face()`, `.edge()`).\n\n```ts\nconst r = Rectangle2D.fromDimensions(0, 0, 100, 60);\nr.side('top'); r.side('left'); // Line2D\nr.vertex('top-left'); // Point2D\nr.width; r.height; r.center;\nconst [d1, d2] = r.diagonals(); // [bl-tr, br-tl]\n\nr.toSketch(); // Sketch (for 2D operations)\nr.extrude(20); // Shape with named faces\n\nRectangle2D.fromCenterAndDimensions(point(50, 30), 100, 60);\nRectangle2D.from2Corners(point(0, 0), point(100, 60));\nRectangle2D.from3Points(p1, p2, p3); // free-angle rectangle\n```\n\n#### `width()` — Width of the rectangle.\n\n```ts\nget width(): number\n```\n\n#### `height()` — Height of the rectangle.\n\n```ts\nget height(): number\n```\n\n#### `center()` — Geometric center of the rectangle.\n\n```ts\nget center(): Point2D\n```\n\n#### `side()` — Return a named side of the rectangle.\n\n```ts\nside(name: RectSide): Line2D\n```\n\n#### `sideAt()` — Return a side by index.\n\n```ts\nsideAt(index: number): Line2D\n```\n\n#### `vertex()` — Return a named vertex of the rectangle.\n\n```ts\nvertex(name: RectVertex): Point2D\n```\n\n#### `diagonals()` — Return the two diagonals of the rectangle.\n\n```ts\ndiagonals(): [ Line2D, Line2D ]\n```\n\n#### `toSketch()` — Convert the rectangle to a sketch profile.\n\n```ts\ntoSketch(): Sketch\n```\n\n#### `translate()` — Return a translated rectangle.\n\n```ts\ntranslate(dx: number, dy: number): Rectangle2D\n```\n\n#### `fromDimensions()` — Create an axis-aligned rectangle from origin corner plus width and height.\n\n```ts\nstatic fromDimensions(x: number, y: number, width: number, height: number): Rectangle2D\n```\n\n#### `fromCenterAndDimensions()` — Create a rectangle centered on a point.\n\n```ts\nstatic fromCenterAndDimensions(center: Point2D, width: number, height: number): Rectangle2D\n```\n\n#### `from2Corners()` — Create an axis-aligned rectangle from two opposite corners.\n\n```ts\nstatic from2Corners(p1: Point2D, p2: Point2D): Rectangle2D\n```\n\n#### `from3Points()` — Create a free-angle rectangle from three points.\n\n`p1` and `p2` define one edge, and `p3` chooses the perpendicular side.\n\n```ts\nstatic from3Points(p1: Point2D, p2: Point2D, p3: Point2D): Rectangle2D\n```\n\n#### `extrude()` — Extrude the rectangle into a solid prism with named topology.\n\n```ts\nextrude(height: number, up?: boolean): Shape\n```\n\n---\n\n<!-- generated/curves.md -->\n\n# Curves & Surfacing\n\nSmooth curves, lofted surfaces, swept solids, splines, and high-level product skins.\n\n## Contents\n\n- [Curves & Surfacing](#curves-surfacing) — `Loft.station`, `Loft.leftRail`, `Loft.rightRail`, `Loft.frontRail`, `Loft.backRail`, `Loft.centerRail`, `Loft.pathOnXz`, `Loft.pathOnYz`, `Loft.pathOnXy`, `Loft.withGuideRails`, `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`\n- [Surface Members](#surface-members) — `surfaceBand`, `SurfaceBody`\n- [Curve3D](#curve3d)\n- [NurbsCurve3D](#nurbscurve3d)\n- [NurbsSurface](#nurbssurface)\n- [PathBuilder](#pathbuilder) — Line Segments, Arcs, Curves, Closing & Output\n- [HermiteCurve3D](#hermitecurve3d)\n- [QuinticHermiteCurve3D](#quintichermitecurve3d)\n- [ProductSkin](#productskin)\n- [ProductSurfaceRef](#productsurfaceref)\n- [ProductSurfaceBuilder](#productsurfacebuilder)\n- [ProductSkinBuilder](#productskinbuilder)\n- [ProductStationBuilder](#productstationbuilder)\n- [ProductPanelBuilder](#productpanelbuilder)\n- [ProductRibbonBuilder](#productribbonbuilder)\n- [ProductSpoutBuilder](#productspoutbuilder)\n- [ProductHandleBuilder](#producthandlebuilder)\n- [ProductHandleFeature](#producthandlefeature)\n- [CylinderCarrier](#cylindercarrier)\n- [PlaneCarrier](#planecarrier)\n- [ProductSkinCarrier](#productskincarrier)\n- [SurfacePath](#surfacepath)\n- [SurfacePathBuilder](#surfacepathbuilder)\n- [SurfaceBand](#surfaceband)\n- [SurfaceBodyBuilder](#surfacebodybuilder)\n- [SurfaceMemberBuilder](#surfacememberbuilder)\n- [SurfaceJoinBuilder](#surfacejoinbuilder)\n- [CounterboreBuilder](#counterborebuilder)\n- [RoundedSlotBuilder](#roundedslotbuilder)\n- [Surface](#surface)\n- [Blend](#blend)\n- [Analysis](#analysis)\n- [Product](#product)\n- [Carrier](#carrier)\n- [SurfaceMembers](#surfacemembers)\n- [Slot](#slot)\n- [Counterbore](#counterbore)\n- [Ribs](#ribs)\n\n## Functions\n\n### Curves & Surfacing\n\n#### `Loft.station()` — Create a loft station from a 2D profile and an axis position.\n\n```ts\nLoft.station(profile: Sketch, position: number): LoftStation\n```\n\n`LoftStation`: `{ profile: Sketch, position: number }`\n\n#### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.\n\n```ts\nLoft.leftRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n`LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`\n\n#### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.\n\n```ts\nLoft.rightRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n#### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.\n\n```ts\nLoft.frontRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n#### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.\n\n```ts\nLoft.backRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n#### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.\n\n```ts\nLoft.centerRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n#### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.\n\nThe path's first coordinate becomes X and its second coordinate becomes Z. Use this for left/right silhouette rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).\n\n```ts\nLoft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]\n```\n\n#### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.\n\nThe path's first coordinate becomes Y and its second coordinate becomes Z. Use this for front/back crown rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).\n\n```ts\nLoft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]\n```\n\n#### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.\n\nThe path's first coordinate becomes X and its second coordinate becomes Y. Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.\n\n```ts\nLoft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]\n```\n\n#### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.\n\nStations define the cross-section family. Guide rails define the side or center paths the loft must pass through. With opposite side rails, the section is scaled to touch both rails. With one side rail, the section keeps its interpolated size unless a center rail is also present.\n\n```ts\nLoft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape\n```\n\n**`LoftOptions`**\n- `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.\n- `boundsPadding?: number` — Optional extra bounds padding.\n\n**`LoftWithGuideRailsOptions`** extends LoftOptions\n- `axis?: LoftAxis` — Primary station axis. Default Z.\n- `samples?: number` — Number of generated loft stations including ends. Default scales with station count.\n- `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.\n\n#### `hermiteTransitionG2()` — Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).\n\nThe curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`, and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`, with smooth G2-continuous interpolation matching position, tangent, and curvature.\n\n```ts\nhermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D\n```\n\n**`QuinticHermiteCurveEndpoint`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `point` | `Vec3` | Position |\n| `tangent` | `Vec3` | Tangent direction (will be normalized internally) |\n| `curvature?` | `Vec3` | Second derivative / curvature vector. Default [0, 0, 0]. |\n| `weight?` | `number` | Weight: scales tangent magnitude relative to chord length. Default 1.0. |\n\n#### `nurbs3d()` — Create a NURBS curve from control points.\n\nWith default options, creates a cubic non-rational B-spline with uniform clamped knots. Set `weights` for rational curves (exact circles, conics). Set `degree` for linear (1), quadratic (2), cubic (3), or higher-order curves.\n\n```js\n// Simple cubic B-spline through control points\nconst curve = nurbs3d([[0,0,0], [10,5,0], [20,-5,10], [30,0,5]]);\nconst tube = sweep(circle(2), curve);\n```\n\n```js\n// Rational quadratic — exact circular arc\nconst arc = nurbs3d(\n [[10,0,0], [10,10,0], [0,10,0]],\n { degree: 2, weights: [1, Math.SQRT1_2, 1] }\n);\n```\n\n```ts\nnurbs3d(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D\n```\n\n**`NurbsCurve3DOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `degree?` | `number` | Polynomial degree (default 3 = cubic). Must be ≥ 1. |\n| `weights?` | `number[]` | Rational weights, one per control point (default: all 1.0 = non-rational). |\n| `knots?` | `number[]` | Knot vector (default: uniform clamped). Must have length = controlPoints.length + degree + 1. |\n| `closed?` | `boolean` | Whether the curve is closed/periodic (default false). |\n\n#### `spline2d()` — Build a smooth Catmull-Rom spline sketch from 2D control points.\n\nA closed spline (default) returns a filled profile. An open spline requires a strokeWidth option to produce a solid sketch. Use tension (0..1, default 0.5) to control curve tightness.\n\n```ts\nspline2d(points: Vec2[], options?: Spline2DOptions): Sketch\n```\n\n**`Spline2DOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `closed?` | `boolean` | Closed loop (default true). |\n| `tension?` | `number` | Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. |\n| `samplesPerSegment?` | `number` | Samples per segment (minimum 3). Default 16. |\n| `strokeWidth?` | `number` | For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. |\n| `join?` | `\"Round\" \\| \"Square\"` | Stroke join for open splines. Default 'Round'. |\n\n#### `spline3d()` — Create a reusable 3D spline curve object (Catmull-Rom).\n\nThe returned Curve3D provides sample(), pointAt(t), tangentAt(t), and length() for downstream use in sweep() or manual path operations.\n\n```ts\nspline3d(points: Vec3[], options?: Spline3DOptions): Curve3D\n```\n\n**`Spline3DOptions`**\n- `closed?: boolean` — Closed loop (default false).\n- `tension?: number` — Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5.\n\n#### `loft()` — Loft between multiple sketches along Z stations.\n\nProfiles can differ in topology and vertex count: interpolation is done on signed-distance fields and meshed with level-set extraction. Heights must be strictly increasing. Compatible loft stacks can also stay on the maintained export-backend path.\n\nPerformance note: loft is significantly heavier than primitive/extrude/revolve. If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().\n\n```ts\nloft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape\n```\n\n#### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.\n\nUnlike loft() which only supports Z heights, loftAlongSpine() places each profile at a position along a 3D spine, oriented perpendicular to the spine tangent. This enables lofting along curved paths — e.g., a wing root-to-tip transition that follows a swept-back leading edge.\n\nThe tValues array specifies where each profile sits along the spine (0 = start, 1 = end). Must have the same length as profiles and be in [0, 1].\n\nInternally uses variableSweep infrastructure with SDF interpolation.\n\nPerformance note: uses level-set meshing, heavier than simple loft().\n\n```ts\nloftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3[], tValues: number[], options?: LoftAlongSpineOptions): Shape\n```\n\n**`LoftAlongSpineOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |\n| `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |\n| `boundsPadding?` | `number` | Optional extra bounds padding. |\n| `up?` | `Vec3` | Preferred \"up\" vector for local profile frame. Auto fallback is used near parallel segments. |\n\n#### `sweep()`\n\n```ts\nsweep(profile: Sketch, path: SweepPathInput, options?: SweepOptions): Shape\n```\n\n**`SweepOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `samples?` | `number` | Number of samples when path is a Curve3D. Default 48. |\n| `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |\n| `boundsPadding?` | `number` | Optional extra bounds padding. |\n| `up?` | `Vec3` | Preferred \"up\" vector for local profile frame. Auto fallback is used near parallel segments. |\n\n#### `variableSweep()` — Sweep a variable cross-section along a 3D spine curve.\n\nUnlike sweep(), which uses a single constant profile, variableSweep() interpolates between multiple profiles at different stations along the spine. This enables organic shapes like tapering tubes, bone-like structures, and sculptural forms.\n\nEach section specifies a t parameter (0 = start, 1 = end of spine) and a 2D profile sketch. The SDF-based level-set mesher smoothly blends between profiles at intermediate positions.\n\nPerformance note: like sweep(), this uses level-set meshing internally.\n\n```ts\nvariableSweep(spine: SweepPathInput, sections: VariableSweepSection[], options?: VariableSweepOptions): Shape\n```\n\n**`VariableSweepSection`**\n- `t: number` — Parameter along the spine (0 = start, 1 = end).\n- `profile: Sketch` — Cross-section profile at this station.\n\n**`VariableSweepOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |\n| `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |\n| `boundsPadding?` | `number` | Optional extra bounds padding. |\n| `up?` | `Vec3` | Preferred \"up\" vector for local profile frame. Auto fallback is used near parallel segments. |\n\n#### `nurbsSurface()` — Create a NURBS surface from a grid of control points.\n\nThe control grid is indexed as `controlGrid[u][v]` — each row is a curve in the V direction, and columns trace curves in the U direction.\n\nWith default options, creates a bicubic non-rational B-spline surface with uniform clamped knots.\n\n```js\n// Simple 4×4 control grid — a gently curved surface\nconst grid = [\n [[0,0,0], [10,0,2], [20,0,2], [30,0,0]],\n [[0,10,1], [10,10,5], [20,10,5], [30,10,1]],\n [[0,20,1], [10,20,5], [20,20,5], [30,20,1]],\n [[0,30,0], [10,30,2], [20,30,2], [30,30,0]],\n];\nconst surface = nurbsSurface(grid, { thickness: 2 });\n```\n\n```ts\nnurbsSurface(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape\n```\n\n**`NurbsSurfaceOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `degreeU?` | `number` | Degree in U direction (default 3). |\n| `degreeV?` | `number` | Degree in V direction (default 3). |\n| `weights?` | `number[][]` | Weights grid — same dimensions as controlGrid (default: all 1.0). |\n| `knotsU?` | `number[]` | Knot vector in U direction (default: uniform clamped). |\n| `knotsV?` | `number[]` | Knot vector in V direction (default: uniform clamped). |\n| `thickness?` | `number` | Sheet thickness — if > 0, thickens the surface into a solid (default 0 = surface only). |\n| `resolution?` | `number` | Tessellation resolution — points per direction (default 32). |\n| `domain?` | `SurfaceDomainOptions` | Optional rectangular parameter domain in normalized [0, 1] U/V space. |\n| `trim?` | `SurfaceTrimOptions` | Optional polygonal or NURBS-curve UV trim loops. Truck and OCCT support open trimmed surfaces; Manifold supports sampled thickened trimmed solids. |\n| `tessellation?` | `SurfaceTessellationOptions` | Optional Truck kernel tessellation controls for render mesh generation. |\n| `approximate?` | `boolean` | Explicit opt-in for sampled approximation paths on non-exact backends. |\n\n**`SurfaceDomainOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `uMin?` | `number` | Lower U parameter bound in normalized surface space (default 0). |\n| `uMax?` | `number` | Upper U parameter bound in normalized surface space (default 1). |\n| `vMin?` | `number` | Lower V parameter bound in normalized surface space (default 0). |\n| `vMax?` | `number` | Upper V parameter bound in normalized surface space (default 1). |\n\n**`SurfaceTrimOptions`**\n- `outer: SurfaceTrimLoopInput` — Outer trim loop in normalized post-domain UV space.\n- `holes?: SurfaceTrimLoopInput[]` — Optional hole loops in normalized post-domain UV space.\n\n**`SurfaceTessellationOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `mode?` | `\"uniform\" \\| \"adaptive\"` | `uniform` uses resolution directly; `adaptive` lets the Truck kernel refine open sheets from chord error. |\n| `tolerance?` | `number` | Target chord-error tolerance in model units for adaptive Truck tessellation. |\n| `minResolution?` | `number` | Minimum adaptive samples per direction. |\n| `maxResolution?` | `number` | Maximum adaptive samples per direction. Defaults to `resolution` when omitted. |\n\n#### `surfacePatch()` — Create a smooth surface patch from 4 boundary curves (Coons patch).\n\nThe four curves form the boundary of a quadrilateral patch:\n\n- bottom: u=0..1 at v=0 (from corner00 to corner10)\n- top: u=0..1 at v=1 (from corner01 to corner11)\n- left: v=0..1 at u=0 (from corner00 to corner01)\n- right: v=0..1 at u=1 (from corner10 to corner11)\n\nThe interior is filled using bilinear Coons patch interpolation: P(u,v) = Lc(u,v) + Ld(u,v) - B(u,v)\n\nThe result is a thin solid created by offsetting the surface mesh along its normals by the specified thickness.\n\nNote: curves should meet at corners. Small gaps are tolerated.\n\n```ts\nsurfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape\n```\n\n**`SurfacePatchOptions`**\n- `resolution?: number` — Number of samples along each direction. Default 24.\n- `thickness?: number` — Thickness of the generated solid. Default 0 for an open exact sheet.\n- `approximate?: boolean` — Allow explicit approximation for non-exact curve inputs such as Curve3D samples.\n\n#### `transitionCurve()` — Create a smooth transition curve between two edges.\n\nReturns a `HermiteCurve3D` that starts at `edgeA.point` tangent to `edgeA.tangent` and ends at `edgeB.point` tangent to `edgeB.tangent`.\n\nThe curve maintains G1 continuity (matching tangent direction) at both endpoints. Weight parameters control the shape of the transition.\n\n```js\n// Connect two edges with a balanced transition\nconst curve = transitionCurve(\n { point: [0, 0, 0], tangent: [1, 0, 0] },\n { point: [10, 5, 0], tangent: [1, 0, 0] },\n);\n```\n\n// Weighted: curve hugs edge A longer const weighted = transitionCurve( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 0], tangent: [1, 0, 0] }, { weightA: 2.0, weightB: 0.5 }, );\n\n```\n\n```ts\ntransitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D\n```\n\n**`TransitionEdge`**\n- `point: Vec3` — Connection point on the edge. Can be any point along the edge where the transition should connect.\n- `tangent: Vec3` — Tangent direction at the connection point. This is the direction the curve should initially follow when leaving this edge. For a straight edge, this is typically the edge direction pointing \"outward\" (away from the body of the edge, toward the other edge).\n- `normal?: Vec3` — Surface normal at the connection point (optional). Used as a hint for the sweep frame's up vector.\n\n**`TransitionCurveOptions`**\n- `weightA?: number` — Weight for the start edge. Controls tangent magnitude at the start. - 1.0 (default): balanced transition - > 1.0: curve follows start edge longer before turning - < 1.0: curve turns sooner at the start\n- `weightB?: number` — Weight for the end edge. Controls tangent magnitude at the end. - 1.0 (default): balanced transition - > 1.0: curve follows end edge longer before turning - < 1.0: curve turns sooner at the end\n- `samples?: number` — Number of sample points for the output polyline. Default 64. Higher values give smoother curves at the cost of more geometry.\n\n#### `transitionSurface()` — Create a solid transition surface between two edges by sweeping a profile along a Hermite transition curve.\n\nThis produces a watertight solid that smoothly connects the two edges. Works with both Manifold and OCCT backends.\n\n```js\n// Circular tube connecting two edges\nconst tube = transitionSurface(\n { point: [0, 0, 0], tangent: [1, 0, 0] },\n { point: [10, 5, 3], tangent: [0, 1, 0] },\n { radius: 0.5 },\n);\n```\n\n// Custom profile with weights const custom = transitionSurface( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 3], tangent: [0, 1, 0] }, { profile: mySketch, weightA: 1.5, weightB: 0.8 }, );\n\n```\n\n```ts\ntransitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape\n```\n\n\n**`TransitionSurfaceOptions`** extends TransitionCurveOptions\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `profile?` | `Sketch` | Cross-section profile to sweep along the transition curve. If omitted, a circular profile with `radius` is used. |\n| `radius?` | `number` | Radius of circular cross-section (used when `profile` is omitted). Default: 5% of chord length. |\n| `rectangleSection?` | `{ width: number; height: number; }` | Width and height for rectangular cross-section. Alternative to `radius` when `profile` is omitted. |\n| `up?` | `Vec3` | Preferred up vector for the sweep frame. Default: auto-detected. |\n| `edgeLength?` | `number` | Edge length for level-set meshing. Smaller = finer. |\n| `boundsPadding?` | `number` | Extra bounds padding for level-set meshing. |\n\n#### `connectEdges()` — Create a transition surface or solid bridge between two edge segments.\n\nTangents can be inferred from neighboring geometry or supplied explicitly through `options`. This is useful for loft-like blends where you want a direct connection between two edge spans.\n\n```ts\nconnectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape\n```\n\n**`EdgeSegment`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |\n| `direction` | `Vec3` | Normalized direction from start → end. |\n| `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |\n| `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |\n| `normalA` | `Vec3` | Normal of first adjacent face. |\n| `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |\n| `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |\n| `start`, `end`, `midpoint`, `length` | | — |\n\n\n**`ConnectEdgesOptions`** extends TransitionSurfaceOptions\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `endA?` | `EdgeEnd` | Which end of edge A to connect. Default: 'start'. |\n| `endB?` | `EdgeEnd` | Which end of edge B to connect. Default: 'start'. |\n| `tangentModeA?` | `TangentMode` | Tangent mode for edge A. Default: 'along'. |\n| `tangentModeB?` | `TangentMode` | Tangent mode for edge B. Default: 'along'. |\n| `tangentA?` | `Vec3` | Explicit tangent for edge A. |\n| `tangentB?` | `Vec3` | Explicit tangent for edge B. |\n| `flipA?` | `boolean` | Flip tangent A. |\n| `flipB?` | `boolean` | Flip tangent B. |\n\n### Surface Members\n\n#### `surfaceBand()`\n\n```ts\nsurfaceBand<C extends SurfaceCoordinate>(path: SurfacePath<C> | SurfacePathBuilder<C>, width: WidthProfile, cap?: SurfaceBandCap): SurfaceBand<C>\n```\n\n#### `SurfaceBody()` — Start a surface-member body builder for straps, inlays, guards, braces, cuffs, and similar physical members that live on a carrier surface.\n\n```js\nconst carrier = Carrier.cylinder('guard-envelope').diameter(84).height(36).clearance(2);\nconst guard = SurfaceBody('simple-guard')\n .carrier(carrier)\n .member('left-strut')\n .band()\n .path(carrier.path().from({ angle: -132, z: 6 }).to({ angle: -58, z: 18 }))\n .section({ width: 5.5, thickness: 2.8, edgeRadius: 0.6 })\n .member('right-strut')\n .mirrorOf('left-strut')\n .member('front-hoop')\n .band()\n .path(carrier.path().around({ z: 18, fromAngle: -58, toAngle: 58 }))\n .section({ width: 6.2, thickness: 3, edgeRadius: 0.7 })\n .join('left-strut', 'front-hoop').blend({ radius: 3.2 })\n .join('right-strut', 'front-hoop').blend({ radius: 3.2 })\n .build();\n```\n\n```ts\nSurfaceBody(name: string): SurfaceBodyBuilder\n```\n\n---\n\n## Classes\n\n### `Curve3D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `points` | `Vec3[]` | — |\n| `closed` | `boolean` | — |\n| `tension` | `number` | — |\n\n**Methods:**\n\n#### `sampleBySegment()` — Sample the curve with a fixed number of points per segment.\n\n```ts\nsampleBySegment(samplesPerSegment?: number): Vec3[]\n```\n\n#### `sample()` — Sample the curve to an approximate total point count.\n\n```ts\nsample(count?: number): Vec3[]\n```\n\n#### `pointAt()` — Return the position on the curve at normalized parameter `t` in `[0, 1]`. O(1), no allocations.\n\n```ts\npointAt(t: number): Vec3\n```\n\n#### `tangentAt()` — Return a unit tangent vector at normalized parameter `t` in `[0, 1]`. O(1), analytical derivative.\n\n```ts\ntangentAt(t: number): Vec3\n```\n\n#### `length()` — Approximate the curve length by polyline sampling.\n\n```ts\nlength(samples?: number): number\n```\n\n### `NurbsCurve3D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `controlPoints` | `Vec3[]` | — |\n| `weights` | `number[]` | — |\n| `knots` | `number[]` | — |\n| `degree` | `number` | — |\n| `closed` | `boolean` | — |\n\n**Methods:**\n\n#### `pointAt()` — Evaluate the curve at parameter t ∈ [0, 1]. Uses De Boor's algorithm — exact, O(degree²).\n\n```ts\npointAt(t: number): Vec3\n```\n\n#### `tangentAt()` — Evaluate the unit tangent vector at parameter t ∈ [0, 1].\n\n```ts\ntangentAt(t: number): Vec3\n```\n\n#### `sample()` — Sample the curve uniformly at `count` points.\n\n```ts\nsample(count?: number): Vec3[]\n```\n\n#### `sampleAdaptive()` — Sample with adaptive density — more points in high-curvature regions.\n\n```ts\nsampleAdaptive(minCount?: number, maxCount?: number): Vec3[]\n```\n\n#### `length()` — Approximate arc length by summing polyline segment lengths.\n\n```ts\nlength(samples?: number): number\n```\n\n#### `toPolyline()` — Convert to a format compatible with sweep() path input.\n\n```ts\ntoPolyline(samples?: number): Vec3[]\n```\n\n### `NurbsSurface`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `controlGrid` | `Vec3[][]` | — |\n| `weightsGrid` | `number[][]` | — |\n| `knotsU` | `number[]` | — |\n| `knotsV` | `number[]` | — |\n| `degreeU` | `number` | — |\n| `degreeV` | `number` | — |\n| `nU` | `number` | — |\n| `nV` | `number` | — |\n| `domain` | `SurfaceDomainCompilePlan` | — |\n\n**Methods:**\n\n#### `pointAt()` — Evaluate the surface at parameters (u, v) ∈ [0, 1]². Uses tensor product evaluation: evaluate basis functions in U and V independently.\n\n```ts\npointAt(u: number, v: number): Vec3\n```\n\n#### `normalAt()` — Evaluate the surface normal at (u, v) via cross product of partial derivatives.\n\n```ts\nnormalAt(u: number, v: number): Vec3\n```\n\n#### `tessellate()` — Tessellate the surface into a triangle mesh. Returns positions, normals, and triangle indices.\n\n```ts\ntessellate(resU?: number, resV?: number): { positions: Vec3[]; normals: Vec3[]; indices: number[]; }\n```\n\n### `PathBuilder`\n\n**Line Segments**\n\n#### `moveTo()` — Move the cursor to an absolute position without drawing a segment.\n\nWhen called after the initial [`path()`](/docs/sketch#path), this establishes the start of the outline. Calling `moveTo` again mid-path starts a new sub-path (hole in `close()`, separate segment for [`stroke()`](/docs/sketch#stroke)).\n\n```ts\nmoveTo(x: number, y: number): this\n```\n\n#### `lineTo()` — Draw a straight line from the current cursor to an absolute position.\n\n```ts\nlineTo(x: number, y: number): this\n```\n\n#### `lineH()` — Draw a horizontal line segment by `dx` units from the current cursor.\n\nPositive `dx` moves right; negative moves left.\n\n```ts\nlineH(dx: number): this\n```\n\n#### `lineV()` — Draw a vertical line segment by `dy` units from the current cursor.\n\nPositive `dy` moves up; negative moves down.\n\n```ts\nlineV(dy: number): this\n```\n\n#### `lineAngled()` — Draw a line at the given angle and length from the current cursor.\n\nAngle convention: `0°` points right (+X), `90°` points up (+Y).\n\n```ts\n// L-bracket with angled return\npath().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n```\n\n```ts\nlineAngled(length: number, degrees: number): this\n```\n\n**Arcs**\n\n#### `arc()` — Draw an arc defined by center, radius, and angle range (no trig needed). If the path has no segments yet, automatically moves to the arc start. Positive sweep (startDeg < endDeg) = CCW, negative = CW.\n\n```js\n// Arc centered at (10, 0), radius 50, from -30° to +30°\npath().arc(10, 0, 50, -30, 30).stroke(8, 'Round')\n```\n\n```ts\narc(cx: number, cy: number, radius: number, startDeg: number, endDeg: number): this\n```\n\n#### `arcTo()` — Draw a circular arc from the current position to (x, y) with the given radius. `clockwise=true` → arc curves to the right of the start→end direction. `clockwise=false` → arc curves to the left of the start→end direction.\n\n```ts\narcTo(x: number, y: number, radius: number, clockwise?: boolean): this\n```\n\n#### `tangentArcTo()` — G1-continuous arc — radius derived from current tangent + endpoint. Throws if endpoint is collinear with current direction.\n\n```ts\ntangentArcTo(x: number, y: number): this\n```\n\n**Curves**\n\n#### `bezierTo()` — Cubic bezier from current position to (x, y) via two control points.\n\n```ts\nbezierTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): this\n```\n\n**Closing & Output**\n\n#### `close()` — Close the path and return a filled [`Sketch`](/docs/sketch#sketch).\n\nThe winding of the polygon is automatically corrected to CCW (the expected orientation for ForgeCAD sketches). If the path contains multiple sub-paths (started with subsequent `moveTo` calls), the first sub-path is the outer contour and subsequent sub-paths become holes subtracted from it.\n\nEdge labels (assigned with `.label('name')`) are transferred to the resulting sketch and propagate through `extrude()`, `revolve()`, `loft()`, and `sweep()` into named faces on the resulting [`Shape`](/docs/core#shape).\n\n```ts\nconst triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();\n\n// With a hole (second sub-path)\nconst frame = path()\n .moveTo(0, 0).lineH(40).lineV(30).lineH(-40).close(); // outer\n // (hole would be added with another moveTo and line sequence before close)\n```\n\n```ts\nclose(): Sketch\n```\n\n#### `closeLabel()` — Label the closing segment and close the path. Shorthand for labeling the implicit line from the last point back to the start, then closing.\n\n```ts\ncloseLabel(name: string): Sketch\n```\n\n#### [`stroke()`](/docs/sketch#stroke) — Thicken an open polyline (centerline) into a solid filled profile with uniform width.\n\nExpands the path into a closed profile `width` units wide (half-width on each side of the centerline). Use `'Round'` for ribs, wire traces, and organic profiles — it adds semicircular endcaps and rounds joins. Use `'Square'` (default) for sharp miter joins without endcaps.\n\nNot the same as rounding corners of a closed polygon — for mixed sharp-and-rounded outlines, build the polygon first and apply [`filletCorners()`](/docs/sketch#filletcorners).\n\n```ts\n// Square-join L-bracket\nconst bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n\n// Round-join rib\nconst rib = path().moveTo(0, 0).lineH(60).stroke(6, 'Round');\n\n// Equivalent standalone form\nconst wire = stroke([[0, 0], [50, 0], [50, -70]], 4);\n```\n\nand semicircular endcaps.\n\n```ts\nstroke(width: number, join?: \"Round\" | \"Square\"): Sketch\n```\n\n#### `label()` — Label the most recently added segment. Labels are born here and grow into face names when the sketch is extruded, lofted, swept, or revolved.\n\nLabels must be unique within a path. Each segment can have at most one label.\n\n```ts\nlabel(name: string): this\n```\n\n**Other**\n\n#### `getX()` — Current cursor X position.\n\n```ts\ngetX(): number\n```\n\n#### `getY()` — Current cursor Y position.\n\n```ts\ngetY(): number\n```\n\n#### `lineBy()` — Draw a line by a relative `(dx, dy)` displacement from the current cursor.\n\n```ts\nlineBy(dx: number, dy: number): this\n```\n\n#### `arcBy()` — Draw an arc to a point offset from the current cursor.\n\n```ts\narcBy(dx: number, dy: number, radius: number, clockwise?: boolean): this\n```\n\n#### `bezierBy()` — Draw a cubic Bezier using control points relative to the current cursor.\n\n```ts\nbezierBy(dcp1x: number, dcp1y: number, dcp2x: number, dcp2y: number, dx: number, dy: number): this\n```\n\n#### `arcAround()` — Arc around a known center point, sweeping by the given angle. Radius is derived from the distance between the current position and the center. Positive sweep = CCW (math convention), negative = CW.\n\n```js\n// Arc 90° CCW around (50, 50)\npath().moveTo(70, 50).arcAround(50, 50, 90)\n// Arc 45° CW around the origin\npath().moveTo(10, 0).arcAround(0, 0, -45)\n```\n\n```ts\narcAround(cx: number, cy: number, sweepDeg: number): this\n```\n\n#### `arcAroundRelative()` — Arc around a center point given as an offset from the current position. `(dx, dy)` is the vector from the current point to the center. Positive sweep = CCW (math convention), negative = CW.\n\n```js\n// Arc 90° CCW around a center 20 units to the right\npath().moveTo(50, 50).arcAroundRelative(20, 0, 90)\n// Equivalent to: path().moveTo(50, 50).arcAround(70, 50, 90)\n```\n\n```ts\narcAroundRelative(dx: number, dy: number, sweepDeg: number): this\n```\n\n#### `smoothCapTo()` — Smooth three-arc end cap from the current position to (endX, endY). Inserts: small corner arc → large cap arc → small corner arc, all G1-continuous.\n\n```ts\nsmoothCapTo(endX: number, endY: number, cornerRadius: number, capRadius: number): this\n```\n\n#### `tangentBezierTo()` — G1-continuous cubic bezier — first control point is auto-derived from the current tangent direction. `weight` controls how far the auto-placed control point extends along the tangent (default: 1/3 of the chord).\n\nThe second control point `(cp2x, cp2y)` must be provided — it controls the arrival curvature. For a fully automatic smooth curve, see `smoothThrough`.\n\n```ts\ntangentBezierTo(cp2x: number, cp2y: number, x: number, y: number, weight?: number): this\n```\n\n#### `smoothThrough()` — Catmull-Rom spline through a list of waypoints from the current position. The current position is included as the first point. The last waypoint becomes the new cursor position.\n\n```ts\nsmoothThrough(waypoints: [ number, number ][], tension?: number): this\n```\n\n#### `nurbsTo()` — Rational B-spline edge to (x, y) with explicit control points and weights.\n\nThe control points define the B-spline shape between the current position and (x, y). The current position is NOT included in `controlPoints` — it is automatically prepended. The endpoint (x, y) is the last control point.\n\n```ts\nnurbsTo(controlPoints: [ number, number ][], opts?: { weights?: number[]; degree?: number; }): this\n```\n\n#### `exactArcTo()` — Exact circular arc to (x, y) using a rational quadratic NURBS.\n\nUnlike `arcTo()` which tessellates to a polyline, this preserves the exact arc definition. When extruded through the OCCT backend, it produces a true cylindrical face — not a faceted approximation.\n\n```ts\nexactArcTo(x: number, y: number, opts?: { radius?: number; clockwise?: boolean; }): this\n```\n\n#### [`fillet()`](/docs/core#fillet) — Round the last corner (the junction between the previous two segments) with a tangent arc of the given radius.\n\nMust be called after at least two line/arc segments that form a corner. The fillet trims back both segments and inserts a tangent arc.\n\n```js\npath().moveTo(0,0).lineTo(10,0).lineTo(10,10).fillet(2).lineTo(0,10).close()\n```\n\n```ts\nfillet(radius: number): this\n```\n\n#### [`chamfer()`](/docs/core#chamfer) — Chamfer the last corner with a straight cut of the given distance.\n\n```js\npath().moveTo(0,0).lineTo(10,0).lineTo(10,10).chamfer(2).lineTo(0,10).close()\n```\n\n```ts\nchamfer(distance: number): this\n```\n\n#### `mirror()` — Mirror all existing segments across an axis and append the mirrored copy in reverse order, creating a symmetric path. The axis passes through the current cursor position.\n\n'y' mirrors across the local Y-axis (flips X), or `[nx, ny]` for an arbitrary axis direction.\n\n```js\n// Build right half, mirror to get full symmetric profile\npath().moveTo(0,0).lineTo(10,0).lineTo(10,5).mirror('x').close()\n```\n\n```ts\nmirror(axis: \"x\" | \"y\" | [ number, number ]): this\n```\n\n#### `toPolyline()` — Return the open path as a sampled 2D polyline.\n\nThis is for construction geometry such as guide rails, measured centerlines, and curve-driven helpers where the authored path should stay open instead of becoming a filled sketch or stroked profile.\n\n```ts\nconst rail = path()\n .moveTo(24, 0)\n .bezierTo(32, 44, 28, 92, 18, 120)\n .toPolyline();\n```\n\n```ts\ntoPolyline(): [ number, number ][]\n```\n\n#### `closeOffset()` — Close the path and return an offset version of the filled Sketch. Positive delta expands outward, negative shrinks inward.\n\n```ts\ncloseOffset(delta: number, join?: \"Round\" | \"Square\" | \"Miter\"): Sketch\n```\n\n### `HermiteCurve3D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `p0` | `Vec3` | Start position |\n| `p1` | `Vec3` | End position |\n| `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |\n| `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |\n| `chordLength` | `number` | Chord length (straight-line distance between endpoints) |\n\n**Methods:**\n\n#### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]\n\n```ts\npointAt(t: number): Vec3\n```\n\n#### `tangentAt()` — Evaluate tangent (first derivative) at parameter t ∈ [0, 1]\n\n```ts\ntangentAt(t: number): Vec3\n```\n\n#### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]\n\n```ts\ncurvatureAt(t: number): Vec3\n```\n\n#### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.\n\n```ts\nsample(count?: number): Vec3[]\n```\n\n#### `length()` — Approximate arc length by sampling.\n\n```ts\nlength(samples?: number): number\n```\n\n#### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.\n\n```ts\nsampleAdaptive(minCount?: number, maxCount?: number): Vec3[]\n```\n\n#### `toPolyline()` — Convert to a format compatible with sweep() path input.\n\n```ts\ntoPolyline(samples?: number): Vec3[]\n```\n\n### `QuinticHermiteCurve3D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `p0` | `Vec3` | Start position |\n| `p1` | `Vec3` | End position |\n| `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |\n| `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |\n| `c0` | `Vec3` | Scaled second derivative at start (curvature * weight² * chordLength²) |\n| `c1` | `Vec3` | Scaled second derivative at end (curvature * weight² * chordLength²) |\n| `chordLength` | `number` | Chord length (straight-line distance between endpoints) |\n\n**Methods:**\n\n#### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]\n\n```ts\npointAt(t: number): Vec3\n```\n\n#### `tangentAt()` — Evaluate tangent (first derivative, normalized) at parameter t ∈ [0, 1]\n\n```ts\ntangentAt(t: number): Vec3\n```\n\n#### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]\n\n```ts\ncurvatureAt(t: number): Vec3\n```\n\n#### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.\n\n```ts\nsample(count?: number): Vec3[]\n```\n\n#### `length()` — Approximate arc length by sampling.\n\n```ts\nlength(samples?: number): number\n```\n\n#### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.\n\n```ts\nsampleAdaptive(minCount?: number, maxCount?: number): Vec3[]\n```\n\n#### `toPolyline()` — Convert to a format compatible with sweep() path input.\n\n```ts\ntoPolyline(samples?: number): Vec3[]\n```\n\n### `ProductSkin`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n| `shape` | `Shape` | — |\n| `axis` | `ProductSkinAxis` | — |\n| `stations` | `ProductStationSpec[]` | — |\n| `rails` | `Record<string, ProductRailSpec>` | — |\n\n**Methods:**\n\n#### [`toShape()`](/docs/sdf#toshape) — Return the renderable shape generated for this product skin.\n\n```ts\ntoShape(): Shape\n```\n\n#### `with()` — Create a group containing this skin plus named child details.\n\n```ts\nwith(...children: GroupInput[]): ShapeGroup\n```\n\n#### `integrate()` — Boolean-union structural details into the skin body.\n\n```ts\nintegrate(...details: Shape[]): Shape\n```\n\n#### `uv()` — Create a side/u/v surface-ref query on this skin.\n\n```ts\nuv(side: ProductSkinSide, u?: number, v?: number): ProductSkinRefQuery\n```\n\n**`ProductSkinSide`** — Semantic side of a ProductSkin. `back` is accepted as an alias for `rear`.\n\n`\"left\" | \"right\" | \"top\" | \"bottom\" | \"front\" | \"rear\" | \"back\"`\n\n**`ProductSkinRefQuery`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `side` | `ProductSkinSide` | Side of the product skin. `front` is the minimum axis cap, `rear`/`back` is the maximum axis cap. |\n| `u?` | `number` | Across-side parameter for side refs. Defaults to 0.5. |\n| `v?` | `number` | Along-axis parameter, 0 at the first cap and 1 at the rear/back cap. Defaults to 0.5. |\n| `offset?` | `number` | Positive distance away from the surface along the resolved normal. |\n\n#### `ref()` — Resolve a named ref published with Product.skin().refs(...).\n\n```ts\nref(name: string): ProductSurfaceRef\n```\n\n#### `curveOnSurface()` — Create a sampled curve as a sequence of surface refs on this skin.\n\n```ts\ncurveOnSurface(name: string, points: Array<Partial<ProductSkinRefQuery> & { side: ProductSkinSide; }>): ProductSurfaceRef[]\n```\n\n#### `surface()` — Create a fluent surface helper for refs and conformal features on one side of this skin.\n\nUse this when several refs or ribbons share the same skin side; side-local helpers keep path points concise and make it harder to mix sides accidentally.\n\n```ts\nsurface(side: ProductSkinSide): ProductSurfaceBuilder\n```\n\n#### `stationAt()` — Interpolate center, width, and depth at a normalized v or absolute axis value.\n\n```ts\nstationAt(vOrAxis: number): { ... }\n```\n\n**`ProductProfileKind`**\n\n`\"oval\" | \"roundedRect\" | \"circle\" | \"superEllipse\" | \"custom\"`\n\n#### `frame()` — Build a local surface frame from a side/u/v query.\n\n```ts\nframe(query: ProductSkinRefQuery): ProductSurfaceFrame\n```\n\n`ProductSurfaceFrame`: `{ point: Vec3, normal: Vec3, tangentU: Vec3, tangentV: Vec3, matrix: Mat4, skin: string }`\n\n### `ProductSurfaceRef`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string | undefined` | — |\n\n**Methods:**\n\n#### `frame()` — Resolve this semantic surface ref into a point, normal, tangents, and placement matrix.\n\n```ts\nframe(overrides?: Partial<ProductSkinRefQuery>): ProductSurfaceFrame\n```\n\n#### `with()` — Return a copy of this ref with side/u/v/offset overrides.\n\n```ts\nwith(overrides: Partial<ProductSkinRefQuery>): ProductSurfaceRef\n```\n\n#### `attach()` — Place a detail shape or group on this ref's local surface frame.\n\n```ts\nattach(detail: Shape | ShapeGroup, options?: ProductAttachOptions): Shape | ShapeGroup\n```\n\n`ProductAttachOptions`: `{ offset?: number, inset?: number }`\n\n#### `querySpec()` — Return the serializable side/u/v query behind this ref.\n\n```ts\nquerySpec(): ProductSkinRefQuery\n```\n\n### `ProductSurfaceBuilder`\n\nFluent helper bound to one ProductSkin side for refs and side-local conformal features.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `side` | `ProductSkinSide` | — |\n\n**Methods:**\n\n#### `ref()` — Create a ref on this skin side.\n\n```ts\nref(u?: number, v?: number, offset?: number): ProductSurfaceRef\n```\n\n#### `uv()` — Create a side/u/v query on this skin side.\n\n```ts\nuv(u?: number, v?: number, offset?: number): ProductSkinRefQuery\n```\n\n#### `frame()` — Resolve a point/frame on this surface using the builder's side.\n\n```ts\nframe(query?: Partial<ProductSkinRefQuery>): ProductSurfaceFrame\n```\n\n#### `ribbon()` — Start a conformal ribbon on this skin side.\n\nPath points use side-local `u`/`v` coordinates; this builder supplies the side. The returned ProductRibbonBuilder is already bound to the source skin and can be further configured before build(). Use `widthSamples` >= 3 when the ribbon must visibly wrap over curved product sections instead of behaving like a flat strip.\n\n```ts\nribbon(name: string, points: ProductSurfacePathPoint[], options?: ProductRibbonBuildOptions): ProductRibbonBuilder\n```\n\n**`ProductSurfacePathPoint`** — Side-local path point for Product.surface(side).ribbon(...); the surface helper supplies `side`.\n- `u?: number` — Across-side parameter on the bound side. Defaults to 0.5.\n- `v?: number` — Along-axis parameter, 0 at the first cap and 1 at the rear/back cap. Defaults to 0.5.\n- `offset?: number` — Positive distance away from the surface along the resolved normal.\n\n**`ProductRibbonBuildOptions`** — Options shared by Product.ribbon() builders and Product.surface(...).ribbon(...).\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `width?` | `number` | Width across the surface in millimeters. |\n| `thickness?` | `number` | Solid thickness outward from the source surface in millimeters. |\n| `offset?` | `number` | Positive clearance between the source surface and the ribbon's inner face. |\n| `samples?` | `number` | Samples along the ribbon path. Higher values bend more smoothly. |\n| `widthSamples?` | `number` | Samples across the ribbon width. Use 3+ to visibly wrap over curved cross-sections. |\n| `resolution?` | `number` | Tessellation resolution passed to the lowered NURBS surface. |\n| `material?` | `ProductMaterial` | Apply a product material preset to the ribbon. |\n| `color?` | `string` | Apply a simple color override. |\n\n`ProductMaterial`: `{ color?: string, material?: ShapeMaterialProps }`\n\n**`ShapeMaterialProps`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `metalness?` | `number` | Metalness factor (0 = dielectric, 1 = metal). Default: 0.05 |\n| `roughness?` | `number` | Roughness factor (0 = mirror, 1 = fully diffuse). Default: 0.35 |\n| `emissive?` | `string` | Emissive glow color (hex string, e.g. \"#ff6b35\"). |\n| `emissiveIntensity?` | `number` | Emissive intensity multiplier. Default: 1 |\n| `opacity?` | `number` | Opacity (0 = fully transparent, 1 = fully opaque). Default: 1 |\n| `wireframe?` | `boolean` | Render as wireframe. Default: false |\n| `clearcoat?` | `number` | Clearcoat intensity (0–1). Default: 0.1 |\n| `clearcoatRoughness?` | `number` | Clearcoat roughness (0–1). Default: 0.4 |\n| `transmission?` | `number` | Glass/translucency transmission factor (0–1). Renderer support depends on target. |\n| `ior?` | `number` | Index of refraction for transmissive materials. Typical glass is ~1.45. |\n| `thickness?` | `number` | Approximate transmissive volume thickness in model units. |\n| `specularIntensity?` | `number` | Specular highlight intensity (0–1). |\n| `specularColor?` | `string` | Specular highlight tint. |\n| `reflectivity?` | `number` | Reflection strength for supported renderers (0–1). |\n\n### `ProductSkinBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `axis()` — Choose the primary station axis for the skin loft.\n\n```ts\naxis(axis: ProductSkinAxis): this\n```\n\n**`ProductSkinAxis`** — Primary world axis used to order ProductSkin loft stations.\n\n`\"X\" | \"Y\" | \"Z\"`\n\n#### `stations()` — Set named cross-section stations for the product skin.\n\n```ts\nstations(stations: Array<ProductStationBuilder | ProductStationSpec>): this\n```\n\n`ProductStationSpec`: `{ name: string, center: Vec3, profile: ProductStationProfile, crown?: number }`\n\n`ProductStationProfile`: `{ sketch: Sketch, width: number, depth: number, kind: ProductProfileKind, radius?: number, exponent?: number }`\n\n#### `rails()` — Attach named guide rails for product-skin construction and downstream surface references.\n\n```ts\nrails(rails: Record<string, ProductRailSpec>): this\n```\n\n`ProductRailSpec`: `{ kind: ProductRailKind, points: Vec3[], degree?: number, name?: string }`\n\n**`ProductRailKind`**\n\n`\"bezier\" | \"nurbs\" | \"polyline\"`\n\n#### `ref()` — Publish a named semantic surface ref on the skin.\n\n```ts\nref(name: string, query: ProductSkinRefQuery): this\n```\n\n#### `refs()` — Publish multiple named semantic surface refs on the skin.\n\n```ts\nrefs(refs: Record<string, ProductSkinRefQuery>): this\n```\n\n#### `uv()` — Create a side/u/v surface-ref query for use in refs(...) or Product.ref(...).\n\n```ts\nuv(side: ProductSkinSide, u?: number, v?: number): ProductSkinRefQuery\n```\n\n#### `material()` — Apply a product material preset to the lowered skin.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `color()` — Apply a simple color override to the lowered skin.\n\n```ts\ncolor(color: string): this\n```\n\n#### `edgeLength()` — Set the sampled loft target edge length.\n\n```ts\nedgeLength(value: number): this\n```\n\n#### `wall()` — Record intended wall thickness for product design metadata. Use explicit shelling when the model needs real inner-wall geometry.\n\n```ts\nwall(thickness: number): this\n```\n\n#### `build()` — Lower stations and refs into a ProductSkin body.\n\n```ts\nbuild(): ProductSkin\n```\n\n### `ProductStationBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `at()` — Position this station in world coordinates.\n\n```ts\nat(point: Vec3): this\n```\n\n#### `z()` — Convenience for traditional Z-up section stacks.\n\n```ts\nz(z: number): this\n```\n\n#### `y()` — Convenience for product bodies running front-to-back along Y.\n\n```ts\ny(y: number): this\n```\n\n#### `x()` — Convenience for product bodies running left-to-right along X.\n\n```ts\nx(x: number): this\n```\n\n#### `oval()` — Use an oval cross-section with full width and depth dimensions.\n\n```ts\noval(width: number, depth: number, options?: { segments?: number; }): this\n```\n\n#### `superEllipse()` — Use a superellipse cross-section for soft-square product surfaces.\n\n```ts\nsuperEllipse(width: number, depth: number, options?: ProductStationSuperEllipseOptions): this\n```\n\n`ProductStationSuperEllipseOptions`: `{ segments?: number, exponent?: number }`\n\n#### [`roundedRect()`](/docs/sketch#roundedrect) — Use a rounded-rectangle cross-section with the given corner radius.\n\n```ts\nroundedRect(width: number, depth: number, radius: number): this\n```\n\n#### [`circle()`](/docs/sketch#circle) — Use a circular cross-section from a full diameter.\n\n```ts\ncircle(diameter: number, options?: { segments?: number; }): this\n```\n\n#### `custom()` — Use a custom 2D sketch as the station cross-section.\n\n```ts\ncustom(sketch: Sketch, width: number, depth: number): this\n```\n\n#### `crown()` — Set the station crown amount for soft product-section intent.\n\n```ts\ncrown(amount: number): this\n```\n\n#### `toSpec()` — Return the immutable station spec consumed by Product.skin().\n\n```ts\ntoSpec(): ProductStationSpec\n```\n\n### `ProductPanelBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `rounded()` — Use a rounded rectangle panel profile.\n\n```ts\nrounded(width: number, height: number, radius?: number): this\n```\n\n#### `oval()` — Use an oval panel profile.\n\n```ts\noval(width: number, height: number): this\n```\n\n#### `profile()` — Use a custom 2D panel profile.\n\n```ts\nprofile(profile: Sketch): this\n```\n\n#### `thickness()` — Set panel extrusion thickness.\n\n```ts\nthickness(thickness: number): this\n```\n\n#### `material()` — Apply a product material preset to the panel.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `color()` — Apply a simple color override to the panel.\n\n```ts\ncolor(color: string): this\n```\n\n#### `build()` — Build the panel in local coordinates.\n\n```ts\nbuild(): Shape\n```\n\n#### `attachTo()` — Build and attach this panel to a ProductSurfaceRef.\n\n```ts\nattachTo(ref: ProductRefInput, options?: ProductPanelAttachOptions): Shape\n```\n\n**`ProductRefInput`**\n\n`ProductSurfaceRef`\n\n\n`ProductPanelAttachOptions`: `{ at?: Partial<ProductSkinRefQuery>, thickness?: number, material?: ProductMaterial, color?: string }`\n\n### `ProductRibbonBuilder`\n\nBuilder for thin trim, label, grip, and split-line features that bend with a ProductSkin surface.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `on()` — Follow a ProductSkin with side/u/v path queries or refs.\n\nThis is the highest-fidelity mode because every interpolated sample is resolved through ProductSkin.frame(), so the ribbon bends along the selected side as station width/depth changes. All query path points must stay on one side; split side transitions into separate ribbons.\n\n```ts\non(skin: ProductSkin, points: ProductRibbonPathPoint[], options?: ProductRibbonBuildOptions): this\n```\n\n**`ProductRibbonPathPoint`** — Path point for Product.ribbon().on(...): either a side/u/v query or a resolved surface ref.\n\n`ProductSkinRefQuery | ProductSurfaceRef`\n\n#### `fromRefs()` — Follow explicit surface refs.\n\nUseful for named refs or paths assembled elsewhere. The builder resolves each ref frame and interpolates between those frames; use on(skin, points) when you need full skin-side sampling between sparse control points.\n\n```ts\nfromRefs(points: ProductSurfaceRef[], options?: ProductRibbonBuildOptions): this\n```\n\n#### `width()` — Set ribbon width in millimeters.\n\n```ts\nwidth(width: number): this\n```\n\n#### `thickness()` — Set solid thickness outward from the source surface in millimeters.\n\n```ts\nthickness(thickness: number): this\n```\n\n#### `offset()` — Set positive clearance between the source surface and the ribbon's inner face.\n\n```ts\noffset(offset: number): this\n```\n\n#### `samples()` — Set samples along the path.\n\n```ts\nsamples(samples: number): this\n```\n\n#### `widthSamples()` — Set samples across the width. Use 3+ to bend over curved cross-sections.\n\n```ts\nwidthSamples(samples: number): this\n```\n\n#### `resolution()` — Set NURBS tessellation resolution.\n\n```ts\nresolution(resolution: number): this\n```\n\n#### `material()` — Apply a product material preset.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `color()` — Apply a simple color override.\n\n```ts\ncolor(color: string): this\n```\n\n#### `build()` — Build a conformal ribbon as a thin NURBS surface solid.\n\n```ts\nbuild(options?: ProductRibbonBuildOptions): Shape\n```\n\n### `ProductSpoutBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `from()` — Set the skin ref this spout projects from.\n\n```ts\nfrom(ref: ProductSurfaceRef): this\n```\n\n#### `sections()` — Set local spout section profiles from root to mouth.\n\n```ts\nsections(sections: Array<Sketch | ProductStationBuilder | ProductStationSpec>): this\n```\n\n#### `projection()` — Set the projection length along the source ref normal.\n\n```ts\nprojection(length: number): this\n```\n\n#### `edgeLength()` — Set the sampled loft target edge length for the spout.\n\n```ts\nedgeLength(value: number): this\n```\n\n#### `material()` — Apply a product material preset to the spout.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `color()` — Apply a simple color override to the spout.\n\n```ts\ncolor(color: string): this\n```\n\n#### `build()` — Build the spout in local coordinates.\n\n```ts\nbuild(): Shape\n```\n\n#### `attach()` — Build and place the spout on its source ref.\n\n```ts\nattach(options?: ProductAttachOptions): Shape\n```\n\n### `ProductHandleBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `between()` — Set the upper body ref and lower world anchor for the handle.\n\n```ts\nbetween(upper: ProductSurfaceRef, lower: Vec3): this\n```\n\n#### `spine()` — Set an explicit handle centerline from points or a rail spec.\n\n```ts\nspine(points: Vec3[] | ProductRailSpec): this\n```\n\n#### `grip()` — Set the grip cross-section profile.\n\n```ts\ngrip(profile: Sketch): this\n```\n\n#### `material()` — Apply a product material preset to the grip.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `padMaterial()` — Apply a product material preset to handle landing pads.\n\n```ts\npadMaterial(material: ProductMaterial): this\n```\n\n#### `edgeLength()` — Set the sampled loft target edge length for the grip.\n\n```ts\nedgeLength(value: number): this\n```\n\n#### `build()` — Build the handle grip and landing pads.\n\n```ts\nbuild(): ProductHandleFeature\n```\n\n### `ProductHandleFeature`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `grip` | `Shape` | — |\n| `upperPad` | `Shape` | — |\n| `lowerPad` | `Shape` | — |\n\n**Methods:**\n\n#### `structural()` — Return the physical shapes that make up this handle feature.\n\n```ts\nstructural(): Shape[]\n```\n\n#### [`toShape()`](/docs/sdf#toshape) — Boolean-union the handle feature into a single shape.\n\n```ts\ntoShape(): Shape\n```\n\n#### `toGroup()` — Return the handle as a named ShapeGroup preserving child colors.\n\n```ts\ntoGroup(): ShapeGroup\n```\n\n### `CylinderCarrier`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n| `kind` | `\"cylinder\"` | — |\n\n**Methods:**\n\n#### `diameter()`\n\n```ts\ndiameter(value: number): this\n```\n\n#### `radius()`\n\n```ts\nradius(value: number): this\n```\n\n#### `height()`\n\n```ts\nheight(value: number): this\n```\n\n#### `clearance()`\n\n```ts\nclearance(value: number): this\n```\n\n#### `center()`\n\n```ts\ncenter(point: Vec3): this\n```\n\n#### [`path()`](/docs/sketch#path)\n\n```ts\npath(): SurfacePathBuilder<CylinderSurfaceCoordinate>\n```\n\n#### `anchor()`\n\n```ts\nanchor(angle: number, z?: number, options?: { offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `front()`\n\n```ts\nfront(options?: { z?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `back()`\n\n```ts\nback(options?: { z?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `left()`\n\n```ts\nleft(options?: { z?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `right()`\n\n```ts\nright(options?: { z?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `top()`\n\n```ts\ntop(options?: { angle?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `bottom()`\n\n```ts\nbottom(options?: { angle?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `pointAt()`\n\n```ts\npointAt(coordinate: CylinderSurfaceCoordinate): Vec3\n```\n\n#### `mirrorPoint()`\n\n```ts\nmirrorPoint(point: Vec3): Vec3\n```\n\n#### `normalAt()`\n\n```ts\nnormalAt(coordinate: CylinderSurfaceCoordinate): Vec3\n```\n\n#### `tangentAt()`\n\n```ts\ntangentAt(coordinate: CylinderSurfaceCoordinate, tangentHint?: Vec3): Vec3\n```\n\n#### `frameAt()`\n\n```ts\nframeAt(coordinate: CylinderSurfaceCoordinate, tangentHint?: Vec3): SurfaceFrame\n```\n\n#### `bounds()`\n\n```ts\nbounds(): SurfaceBounds\n```\n\n#### `offset()`\n\n```ts\noffset(distance: number): CylinderCarrier\n```\n\n#### `mirrorCoordinate()`\n\n```ts\nmirrorCoordinate(coordinate: CylinderSurfaceCoordinate): CylinderSurfaceCoordinate\n```\n\n#### `radiusValueWithClearance()`\n\n```ts\nradiusValueWithClearance(): number\n```\n\n### `PlaneCarrier`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n| `kind` | `\"plane\"` | — |\n\n**Methods:**\n\n#### `size()`\n\n```ts\nsize(width: number, height: number): this\n```\n\n#### `origin()`\n\n```ts\norigin(point: Vec3): this\n```\n\n#### `normal()`\n\n```ts\nnormal(normal: Vec3): this\n```\n\n#### [`path()`](/docs/sketch#path)\n\n```ts\npath(): SurfacePathBuilder<PlaneSurfaceCoordinate>\n```\n\n#### `anchor()`\n\n```ts\nanchor(x?: number, y?: number, options?: { offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `left()`\n\n```ts\nleft(options?: { y?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `right()`\n\n```ts\nright(options?: { y?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `top()`\n\n```ts\ntop(options?: { x?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `bottom()`\n\n```ts\nbottom(options?: { x?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `pointAt()`\n\n```ts\npointAt(coordinate: PlaneSurfaceCoordinate): Vec3\n```\n\n#### `mirrorPoint()`\n\n```ts\nmirrorPoint(point: Vec3): Vec3\n```\n\n#### `normalAt()`\n\n```ts\nnormalAt(): Vec3\n```\n\n#### `tangentAt()`\n\n```ts\ntangentAt(coordinate: PlaneSurfaceCoordinate, tangentHint?: Vec3): Vec3\n```\n\n#### `frameAt()`\n\n```ts\nframeAt(coordinate: PlaneSurfaceCoordinate, tangentHint?: Vec3): SurfaceFrame\n```\n\n#### `bounds()`\n\n```ts\nbounds(): SurfaceBounds\n```\n\n#### `offset()`\n\n```ts\noffset(distance: number): PlaneCarrier\n```\n\n#### `mirrorCoordinate()`\n\n```ts\nmirrorCoordinate(coordinate: PlaneSurfaceCoordinate): PlaneSurfaceCoordinate\n```\n\n### `ProductSkinCarrier`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `skin` | `ProductSkin` | — |\n| `name` | `string` | — |\n| `kind` | `\"productSkin\"` | — |\n\n**Methods:**\n\n#### `surface()`\n\n```ts\nsurface(side: ProductSkinSide): ProductSkinCarrier\n```\n\n#### [`path()`](/docs/sketch#path)\n\n```ts\npath(): SurfacePathBuilder<ProductSkinSurfaceCoordinate>\n```\n\n`ProductSkinSurfaceCoordinate`: `{ kind?: \"productSkin\", side?: ProductSkinSide, u?: number, v?: number, offset?: number }`\n\n#### `sideTransition()` — Return matching side-local coordinates for an explicit split-member transition.\n\nEach SurfacePath still stays on one ProductSkin side. Use this helper to create one member ending on `from`, another starting on `to`, then join named anchors. The helper validates normalized `v`, non-empty names, adjacency, and physical coincidence before returning anchors.\n\n```ts\nsideTransition(fromSide: ProductSkinSide, toSide: ProductSkinSide, input?: ProductSkinSideTransitionInput): ProductSkinSideTransition\n```\n\n`ProductSkinSideTransitionInput`: `{ name?: string, v?: number, offset?: number }`\n\n`ProductSkinSideTransition`: `{ name?: string, from: ProductSkinSurfaceCoordinate, to: ProductSkinSurfaceCoordinate }`\n\n#### `sideTransitionChain()` — Return a sequence of matching side-local coordinates for an explicit multi-side split-member route.\n\nEach adjacent side pair becomes one named transition. Build one member per side segment, add transition anchors at each returned pair, then join the anchors. The same validation as `sideTransition()` applies to every adjacent pair.\n\n```ts\nsideTransitionChain(sides: ProductSkinSide[], input?: ProductSkinSideTransitionInput): ProductSkinSideTransition[]\n```\n\n#### `sideRoute()` — Return side-local member segments for a generated multi-side split-member route.\n\nThe route still compiles as explicit members plus named-anchor joins. This helper only generates the per-side segment endpoints and transition names.\n\n```ts\nsideRoute(input: ProductSkinSideRouteInput): ProductSkinSideRoute\n```\n\n**`ProductSkinSideRouteInput`**: `name?: string`, `sides: ProductSkinSide[]`, `from: ProductSkinSurfaceCoordinate`, `to: ProductSkinSurfaceCoordinate`, `v?: number`, `offset?: number`\n\n`ProductSkinSideRoute`: `{ name?: string, transitions: ProductSkinSideTransition[], segments: ProductSkinSideRouteSegment[] }`\n\n**`ProductSkinSideRouteSegment`**: `name: string`, `side: ProductSkinSide`, `from: ProductSkinSurfaceCoordinate`, `to: ProductSkinSurfaceCoordinate`, `startAnchorName?: string`, `endAnchorName?: string`\n\n#### `pointAt()`\n\n```ts\npointAt(coordinate: ProductSkinSurfaceCoordinate): Vec3\n```\n\n#### `mirrorPoint()`\n\n```ts\nmirrorPoint(point: Vec3): Vec3\n```\n\n#### `normalAt()`\n\n```ts\nnormalAt(coordinate: ProductSkinSurfaceCoordinate): Vec3\n```\n\n#### `tangentAt()`\n\n```ts\ntangentAt(coordinate: ProductSkinSurfaceCoordinate, tangentHint?: Vec3): Vec3\n```\n\n#### `frameAt()`\n\n```ts\nframeAt(coordinate: ProductSkinSurfaceCoordinate, tangentHint?: Vec3): SurfaceFrame\n```\n\n**`SurfaceFrame`**: `point: Vec3`, `normal: Vec3`, `tangentAlong: Vec3`, `tangentAcross: Vec3`, `matrix: Mat4`, `carrier: string`, `representation: SurfaceCarrierKind | string`, `coordinate: SurfaceCoordinate`\n\n#### `bounds()`\n\n```ts\nbounds(): SurfaceBounds\n```\n\n**`SurfaceBounds`**: `u?: [ number, number ]`, `v?: [ number, number ]`, `angle?: [ number, number ]`, `z?: [ number, number ]`, `x?: [ number, number ]`, `y?: [ number, number ]`\n\n#### `offset()`\n\n```ts\noffset(distance: number): ProductSkinCarrier\n```\n\n#### `mirrorCoordinate()`\n\n```ts\nmirrorCoordinate(coordinate: ProductSkinSurfaceCoordinate): ProductSkinSurfaceCoordinate\n```\n\n### `SurfacePath`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `carrier` | `CarrierSurface<C>` | — |\n| `points` | `C[]` | — |\n| `closedValue` | `boolean` | — |\n\n**Methods:**\n\n#### `closed()`\n\n```ts\nclosed(): SurfacePath<C>\n```\n\n#### `mirror()`\n\n```ts\nmirror(): SurfacePath<C>\n```\n\n#### `coordinateAt()`\n\n```ts\ncoordinateAt(t: number): C\n```\n\n#### `sample()`\n\n```ts\nsample(count?: number): SurfacePathSample<C>[]\n```\n\n#### `length()`\n\n```ts\nlength(samples?: number): number\n```\n\n### `SurfacePathBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `carrier` | `CarrierSurface<C>` | — |\n\n**Methods:**\n\n#### `from()`\n\n```ts\nfrom(coordinate: C): this\n```\n\n#### `through()`\n\n```ts\nthrough(coordinate: C): this\n```\n\n#### `to()`\n\n```ts\nto(coordinate: C): this\n```\n\n#### `around()`\n\n```ts\naround(input: { z: number; fromAngle: number; toAngle: number; offset?: number; }): this\n```\n\n#### `closed()`\n\n```ts\nclosed(): this\n```\n\n#### `mirror()`\n\n```ts\nmirror(): SurfacePath<C>\n```\n\n#### `build()`\n\n```ts\nbuild(): SurfacePath<C>\n```\n\n#### `sample()`\n\n```ts\nsample(count?: number): SurfacePathSample<C>[]\n```\n\n### `SurfaceBand`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `centerPath` | `SurfacePath<C>` | — |\n| `widthProfile` | `WidthProfile` | — |\n| `capStyle` | `SurfaceBandCap` | — |\n\n**Methods:**\n\n#### `widthAt()`\n\n```ts\nwidthAt(t: number): number\n```\n\n#### `boundaries()`\n\n```ts\nboundaries(samples?: number): SurfaceBandBoundarySample[]\n```\n\n#### `withHole()` — Return a new band with a named member-local rounded-slot hole region recorded as inspectable intent.\n\n```ts\nwithHole(name: string, input: SurfaceBandHoleInput): SurfaceBand<C>\n```\n\n#### `holes()` — Resolve recorded hole regions into member-local across/along loops.\n\n```ts\nholes(): SurfaceBandHoleRegion[]\n```\n\n### `SurfaceBodyBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `carrier()`\n\n```ts\ncarrier(carrier: CarrierSurface): this\n```\n\n#### `member()`\n\n```ts\nmember(name: string): SurfaceMemberBuilder\n```\n\n#### `join()`\n\n```ts\njoin(from: string, to: string | string[]): SurfaceJoinBuilder\n```\n\n#### `autoJoinAtSharedAnchors()`\n\n```ts\nautoJoinAtSharedAnchors(): this\n```\n\n#### `build()`\n\n```ts\nbuild(): Shape | ShapeGroup\n```\n\n### `SurfaceMemberBuilder`\n\n#### `plate()`\n\n```ts\nplate(): this\n```\n\n#### `band()`\n\n```ts\nband(): this\n```\n\n#### `at()`\n\n```ts\nat(anchor: SurfaceAnchor<C>): this\n```\n\n#### `size()`\n\n```ts\nsize(width: number, height: number): this\n```\n\n#### [`path()`](/docs/sketch#path)\n\n```ts\npath(path: SurfacePath<C> | SurfacePathBuilder<C>): this\n```\n\n#### `section()`\n\n```ts\nsection(section: MemberSectionInput): this\n```\n\n#### `cap()`\n\n```ts\ncap(style: SurfaceBandCap): this\n```\n\n#### [`slot()`](/docs/sketch#slot)\n\n```ts\nslot(name: string, feature: MemberFeature | RoundedSlotBuilder): this\n```\n\n#### `cutout()`\n\n```ts\ncutout(name: string, feature: MemberFeature | RoundedSlotBuilder): this\n```\n\n#### `counterbore()`\n\n```ts\ncounterbore(name: string, feature: MemberFeature | CounterboreBuilder): this\n```\n\n#### `anchorAt()` — Add a named anchor at a carrier surface coordinate for explicit member joins.\n\n```ts\nanchorAt(name: string, coordinate: C | SurfaceAnchor<C>): this\n```\n\n#### `features()`\n\n```ts\nfeatures(features: MemberFeature | MemberFeature[]): this\n```\n\n#### `profile()`\n\n```ts\nprofile(name: string, options?: { depth?: number; height?: number; }): this\n```\n\n#### `mirrorOf()`\n\n```ts\nmirrorOf(memberName: string): SurfaceBodyBuilder\n```\n\n#### `member()`\n\n```ts\nmember(name: string): SurfaceMemberBuilder\n```\n\n#### `join()`\n\n```ts\njoin(from: string, to: string | string[]): SurfaceJoinBuilder\n```\n\n#### `autoJoinAtSharedAnchors()`\n\n```ts\nautoJoinAtSharedAnchors(): SurfaceBodyBuilder\n```\n\n#### `build()`\n\n```ts\nbuild(): Shape | ShapeGroup\n```\n\n### `SurfaceJoinBuilder`\n\n#### `betweenAnchors()` — Select named anchors on the source and target members before lowering this join.\n\n```ts\nbetweenAnchors(fromAnchor: string, toAnchor: string): this\n```\n\n#### `blend()`\n\n```ts\nblend(input?: { radius?: number; style?: string; priority?: number; continuity?: string; }): SurfaceBodyBuilder\n```\n\n### `CounterboreBuilder`\n\n#### `at()`\n\n```ts\nat(input: { along?: number; across?: number; z?: number; }): this\n```\n\n#### `named()`\n\n```ts\nnamed(name: string): MemberFeature\n```\n\n#### `toFeature()`\n\n```ts\ntoFeature(name?: string): MemberFeature\n```\n\n### `RoundedSlotBuilder`\n\n#### `verticalTravel()`\n\n```ts\nverticalTravel(value: number): this\n```\n\n#### `at()`\n\n```ts\nat(input: { along?: number; across?: number; z?: number; }): this\n```\n\n#### `named()`\n\n```ts\nnamed(name: string): MemberFeature\n```\n\n#### `toFeature()`\n\n```ts\ntoFeature(name?: string): MemberFeature\n```\n\n---\n\n## Constants\n\n### `Surface`\n\n- `Nurbs(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape`\n- `Ruled(curveA: ExactCurveInput, curveB: ExactCurveInput, options?: SurfaceCommonOptions): Shape`\n- `Patch(curves: { bottom: ExactCurveInput; top: ExactCurveInput; left: ExactCurveInput; right: ExactCurveInput; }, options?: SurfacePatchOptions): Shape`\n- `Boundary(input: SurfaceBoundaryInput): Shape`\n- `Fill(input: SurfaceFillInput): Shape`\n- `Sew(shapes: Shape[], options?: { tolerance?: number; }): Shape`\n- `Extend(shape: Shape, options: SurfaceExtendOptions): Shape`\n- `Trim(shape: Shape, tool: Shape | SurfacePlaneOp): Shape`\n- `Split(shape: Shape, tool: Shape | SurfacePlaneOp): [ Shape, Shape ]`\n- `Match(shape: Shape, options: { edge: \"u0\" | \"u1\" | \"v0\" | \"v1\"; target: EdgeRef; continuity?: SurfaceContinuity; }): Shape`\n- `MatchEdge(shape: Shape, options: { edge: \"u0\" | \"u1\" | \"v0\" | \"v1\"; target: EdgeRef; continuity?: SurfaceContinuity; }): Shape`\n\n### `Blend`\n\n- `Edge(options: BlendEdgeOptions): Shape`\n- `Surface(options: BlendSurfaceOptions): Shape`\n\n### `Analysis`\n\n- `EdgeContinuity(shape: Shape, options?: EdgeContinuityThresholds): EdgeContinuityReport`\n- `SurfaceContinuity(shape: Shape, options?: EdgeContinuityThresholds): EdgeContinuityReport`\n- `CurvatureComb(input: NurbsCurve3D | EdgeRef, options?: { samples?: number; }): CurvatureSample[]`\n- `SurfaceHealth(shape: Shape, options?: { tinyEdgeThreshold?: number; sliverThreshold?: number; }): SurfaceHealthReport`\n\n### `Product`\n\n- `skin(name: string): ProductSkinBuilder` — Start a named product skin builder.\n- `station(name: string): ProductStationBuilder` — Start a named cross-section station for Product.skin(...).stations(...).\n- `rail: { bezier(points: Vec3[], options?: { name?: string; }): ProductRailSpec; nurbs(points: Vec3[], options?: { degree?: number; name?: string; }): ProductRailSpec; polyline(points: Vec3[], options?: { name?: string; }): ProductRailSpec; }` — Namespaced rail builders for product skin guide rails and handle spines.\n- `profiles: { ... }` — Namespaced product profile helpers for stations, panels, trims, and openings.\n- `materials: { ... }` — Namespaced product material presets for molded plastic, rubber, metal, and transparent parts.\n- `applyMaterial(shape: Shape, preset: ProductMaterial | undefined): Shape` — Apply a product material preset to a Shape.\n- `scenePreset(name: ProductScenePreset): void` — Apply an opinionated scene preset for product review renders.\n- `ovalProfile(width: number, depth: number, options?: ProductProfileOptions): Sketch` — Create a centered oval profile from full width/depth dimensions.\n- `roundedRectProfile(width: number, depth: number, radius: number): Sketch` — Create a centered rounded-rectangle profile.\n- `circleProfile(diameter: number, options?: ProductProfileOptions): Sketch` — Create a centered circular profile from full diameter.\n- `superEllipseProfile(width: number, depth: number, options?: ProductSuperEllipseOptions): Sketch` — Create a centered superellipse profile for soft-square product sections.\n- `profileSize(sketch: Sketch): { width: number; depth: number; }` — Measure the width and depth of a 2D profile sketch.\n- `describeProfile(sketch: Sketch, kind?: ProductProfileKind, radius?: number): ProductProfileDescriptor` — Describe a custom sketch as a product profile.\n- `scaleProfileTo(sketch: Sketch, width: number, depth: number): Sketch` — Scale an existing profile sketch to a target width/depth.\n- `ref(skin: ProductSkin, query: ProductSkinRefQuery): ProductSurfaceRef` — Create an ad-hoc ProductSurfaceRef from a skin and side/u/v query.\n- `surface(skin: ProductSkin, side: ProductSkinSide): ProductSurfaceBuilder` — Create a fluent surface helper for refs and conformal features on one side of a skin. Equivalent to skin.surface(side), useful when writing in Product.* namespace style.\n- `panel(name: string): ProductPanelBuilder` — Start a panel feature builder.\n- `ribbon(name: string): ProductRibbonBuilder` — Start a conformal ribbon/trim builder for details that should bend with a ProductSkin. Call .on(skin, points) for side/u/v sampling or .fromRefs(points) for explicit surface refs, then configure width, thickness, offset, sampling, material, and color before build().\n- `spout(name: string): ProductSpoutBuilder` — Start a spout/nozzle feature builder.\n- `handle(name: string): ProductHandleBuilder` — Start a handle feature builder.\n- `place(detail: Shape | ShapeGroup, ref: ProductRefInput, options?: ProductAttachOptions): Shape | ShapeGroup` — Place a shape or group on a ProductSurfaceRef.\n- `landing(name: string, radius?: number, material?: ProductMaterial): Shape` — Small blended landing volume for manual structural bridges and connection proofs.\n\n### `Carrier`\n\n- `cylinder(name: string): CylinderCarrier` — Create an analytic cylinder carrier for bottles, limbs, tubes, guards, and cuffs.\n- `plane(name: string): PlaneCarrier` — Create an analytic plane carrier for plates and local flat construction surfaces.\n- `productSkin(skin: ProductSkin): ProductSkinCarrier` — Adapt an existing ProductSkin into the general surface-member carrier protocol.\n\n### `SurfaceMembers`\n\n- `Body(name: string): SurfaceBodyBuilder` — Start a surface-member body builder for straps, inlays, guards, braces, cuffs, and similar physical members that live on a carrier surface.\n- `Band: typeof SurfaceBand`\n- `band<C extends SurfaceCoordinate>(path: SurfacePath<C> | SurfacePathBuilder<C>, width: WidthProfile, cap?: SurfaceBandCap): SurfaceBand<C>`\n\n### `Slot`\n\n- `rounded(input: { length: number; width: number; }): RoundedSlotBuilder` — Create a rounded member-local slot feature.\n\n### `Counterbore`\n\n- `cylindrical(input: { diameter: number; clearanceDiameter: number; depth: number; }): CounterboreBuilder` — Create a cylindrical member-local counterbore feature.\n\n### `Ribs`\n\n- `repeated(input: { count: number; height: number; }): MemberFeature` — Create repeated ribs that belong to a surface member before lowering.\n\n---\n\n<!-- generated/assembly.md -->\n\n# Assembly API\n\nKinematic assemblies, joints, couplings, and robot export.\n\n## Contents\n\n- [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`, `joint`\n- [Assembly](#assembly) — Structure, Connectors, References, Joints, Solving\n- [ImportedAssembly](#importedassembly)\n- [SolvedAssembly](#solvedassembly)\n- [MateBuilder](#matebuilder)\n\n## Functions\n\n### Assembly & Joints\n\n#### `bomToCsv()` — Convert an array of BOM rows into a CSV string.\n\nProduces a CSV with columns: `part`, `qty`, `material`, `process`, `tolerance`, `notes`. String values are quoted and internal double-quotes are escaped. Prefer calling `solvedAssembly.bomCsv()` directly — this function is exposed for custom BOM processing.\n\n```ts\nbomToCsv(rows: BomRow[]): string\n```\n\n**`BomRow`**: `part: string`, `qty: number`, `material?: string`, `process?: string`, `tolerance?: string`, `notes?: string`, `metadata?: PartMetadata`\n\n**`PartMetadata`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `tags?` | `string \\| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |\n| `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |\n\n#### `assembly()` — Create an assembly container with named parts and joints for kinematic mechanisms.\n\n**Use this from iteration 1 for any model with moving parts.** Hinges, sliders, gears, articulated fingers, doors — all start with `assembly()`, not with manual rotation math. Don't build a static \"extended pose\" first and refactor to an assembly later: joint sliders, animations, sweeps, collision detection, and robot export all flow from the kinematic graph.\n\nAn assembly models a mechanism as a directed graph of parts connected by joints. Parts are the nodes; joints are directed edges from parent to child. The graph must be a forest (no cycles). Root parts (those with no incoming joint) are anchored to world space.\n\nThree joint types are supported: `'revolute'` (hinge), `'prismatic'` (slider), and `'fixed'` (rigid attachment). Use `addPart()` to add geometry, `addJoint()` (or the shorthands `addRevolute()`, `addPrismatic()`, `addFixed()`) to connect parts, and `solve()` to compute world-space positions at a given joint state.\n\nThe higher-level `connect()` API uses declared **connectors** to compute joint frames automatically. The `match()` API uses typed connectors (with gender and type metadata) for automatic compatibility validation and joint creation.\n\nFor multi-file assemblies, a file that returns an `Assembly` is importable via [`require()`](/docs/core#require) and yields an `ImportedAssembly`. Use `mergeInto()` to flatten a sub-assembly into a parent assembly.\n\n```ts\nconst mech = assembly(\"Arm\")\n .addPart(\"base\", box(80, 80, 20).translate(0, 0, -10), {\n metadata: { material: \"PETG\", process: \"FDM\", qty: 1 },\n })\n .addPart(\"link\", box(140, 24, 24).translate(0, -12, -12))\n .addRevolute(\"shoulder\", \"base\", \"link\", {\n axis: [0, 1, 0],\n min: -30, max: 120, default: 25,\n frame: Transform.identity().translate(0, 0, 20),\n });\n\nreturn mech; // auto-solved at defaults, renders all parts\n```\n\n```ts\nassembly(name?: string): Assembly\n```\n\n#### `joint()` — Create a revolute joint that auto-generates a parameter slider and rotates the shape.\n\nThis is a convenience wrapper for single-shape, single-joint use cases. It calls `param()` to create a named angle slider, then applies `rotateAroundAxis()` to the shape. Use the full `Assembly` API for mechanisms with multiple parts and joints.\n\n```ts\nconst arm = joint(\"Shoulder\", armShape, [0, 0, 20], {\n axis: [0, 1, 0],\n min: -30, max: 120, default: 25,\n});\nreturn arm;\n```\n\n```ts\njoint(name: string, shape: Shape, pivot: [ number, number, number ], opts?: RevoluteJointOpts): Shape\n```\n\n`RevoluteJointOpts`: `{ axis?: [ number, number, number ], min?: number, max?: number, default?: number, unit?: string, reverse?: boolean }`\n\n---\n\n## Classes\n\n### `Assembly`\n\nContainer for a kinematic mechanism made up of named parts and joints.\n\nAn assembly is a directed graph where **parts** are nodes and **joints** are directed edges from parent to child. The graph must be a forest (one or more trees with no cycles). Root parts (no incoming joint) are fixed to world space.\n\nEach joint carries a `frame` transform (from the parent part frame to the joint's zero-state frame) and a motion formula:\n\n```\nchildWorld = parentWorld × frame × motion(value) × childBase\n```\n\nThree joint types are supported:\n\n- **revolute** — rotates the child around an axis by `value` degrees\n- **prismatic** — translates the child along an axis by `value` mm\n- **fixed** — no motion; rigidly attaches the child at `frame`\n\n**Quick start**\n\n```ts\nconst mech = assembly(\"Arm\")\n .addPart(\"base\", box(80, 80, 20).translate(0, 0, -10))\n .addPart(\"link\", box(140, 24, 24).translate(0, -12, -12))\n .addJoint(\"shoulder\", \"revolute\", \"base\", \"link\", {\n axis: [0, 1, 0],\n min: -30, max: 120, default: 25,\n frame: Transform.identity().translate(0, 0, 20),\n });\n\nreturn mech; // auto-solved at defaults\n```\n\nReturning an unsolved `Assembly` auto-solves at default joint values. Return a `SolvedAssembly` directly for a specific pose:\n\n```ts\nreturn mech.solve({ shoulder: 60 });\n```\n\n**Return types**\n\n| Return value | Standalone | `require()` result type |\n|---|---|---|\n| `Assembly` (unsolved) | yes | `ImportedAssembly` |\n| `SolvedAssembly` | yes | `SolvedAssembly` |\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Connectors**\n\n#### `usedConnectorRefs()` — Connector refs (e.g. \"PartName.connectorName\") consumed by connect/match calls.\n\n```ts\nget usedConnectorRefs(): ReadonlySet<string>\n```\n\n#### `withConnectors()` — Attach named connectors to a specific part or the assembly as a whole.\n\nConnectors declared this way are in the part's local coordinate system. They are captured automatically if the incoming [`Shape`](/docs/core#shape) already has connectors via `shape.withConnectors(...)`, but you can also add or override connectors after the fact with this method.\n\nUse the single-argument overload to attach assembly-level connectors — these are exposed when this assembly is imported as a sub-assembly.\n\n```ts\nwithConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly\n```\n\n#### `getConnectors()` — Get connectors declared on a part in part-local space.\n\n```ts\ngetConnectors(partName: string): ConnectorMap\n```\n\n#### `getConnector()` — Parse a \"PartName.connectorName\" reference and return the resolved connector. Throws descriptive errors if the part or connector doesn't exist.\n\n```ts\ngetConnector(ref: string): { partName: string; connectorName: string; connector: ConnectorDef; }\n```\n\n#### `connect()` — Connect two parts by aligning their declared connectors, automatically computing frame and axis.\n\nConnector references use `\"PartName.connectorName\"` format. The system aligns connector origins (child connector lands exactly on parent connector) and derives the joint frame and axis from the connector geometry — no manual `frame` or `axis` math needed.\n\n**Face-to-face convention:** Connectors always meet face-to-face, like a USB plug meeting a socket. Each connector's axis points \"outward\" from its part. When two connectors mate, the system brings them together so their axes oppose (anti-parallel). This is the same convention used by `matchTo()`.\n\nFor a revolute joint (hinge), both connectors' axes should point outward from their respective parts along the hinge line. For a prismatic joint (slider), both axes should point along the slide direction from their part's perspective.\n\nThe joint type is inferred from the connector's `kind` field if not specified in `options`.\n\nWhen connectors are defined with `start`/`end`, you can control which point on each connector meets via `align` / `parentAlign` / `childAlign` (`'start'`, `'middle'`, `'end'`).\n\nUse `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore). For mechanisms where parts share an axis but are deliberately spaced apart, use `addRevolute()` with pre-positioned parts instead.\n\n```ts\n// Hinge: both axes point outward along the hinge line\nconst frame = box(100, 10, 80).withConnectors({\n hinge: connector(\"hinge\", { origin: [0, 0, 40], axis: [0, 0, 1] }),\n});\nconst door = box(60, 4, 80).withConnectors({\n hinge: connector(\"hinge\", { origin: [0, 0, 40], axis: [0, 0, -1] }),\n});\nassembly(\"Door\")\n .addPart(\"Frame\", frame)\n .addPart(\"Door\", door)\n .connect(\"Frame.hinge\", \"Door.hinge\", { as: \"swing\", min: 0, max: 110 });\n```\n\n```ts\nconnect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly\n```\n\n#### `match()` — Auto-create a joint by matching typed connectors between two parts.\n\nConnectors can carry a `connectorType` string and a `gender` (`'male'`, `'female'`, or `'neutral'`). `match()` validates type and gender compatibility (use `{ force: true }` to skip validation) and creates the joint automatically from the connector's `kind` metadata.\n\nThe `pairs` map is `{ childConnector: parentConnector }`. The first pair drives joint creation; additional pairs are validated but do not create additional joints (they constrain the same rigid connection).\n\nDefine connectors on shapes with `shape.withConnectors(...)`:\n\n```ts\nconst door = doorShape.withConnectors({\n hinge_top: connector.male(\"hinge\", { origin: [0, 0, 90], axis: [0, 0, 1] }),\n hinge_bottom: connector.male(\"hinge\", { origin: [0, 0, 10], axis: [0, 0, 1] }),\n});\n```\n\nThen match in the assembly:\n\n```ts\nconst mech = assembly(\"Door\")\n .addPart(\"Frame\", frame)\n .addPart(\"Door\", door)\n .match(\"Door\", \"Frame\", { hinge_top: \"hinge_top\", hinge_bottom: \"hinge_bottom\" });\n// Revolute connectors → auto-creates revolute joint. No manual addRevolute needed.\n```\n\n```ts\nmatch(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & { as?: string; }): Assembly\n```\n\n**References**\n\n#### `withReferences()` — Attach named placement reference points to this assembly. These are surfaced automatically on the ImportedAssembly when this file is imported via require(), so consumers can use placeReference() without re-declaring them. Returns a new Assembly — does not mutate.\n\n```ts\nwithReferences(refs: Pick<PlacementReferenceInput, \"points\">): Assembly\n```\n\n**Solving**\n\n#### `solve()` — Solve the assembly at the given joint state and return positioned parts.\n\nPerforms a depth-first traversal of the joint graph. Each joint's value is taken from `state`, falling back to `defaultValue`. Coupled joints compute their value from source joints. Values outside `[min, max]` are clamped (a warning is added to `SolvedAssembly.warnings()`).\n\nIf mate constraints were registered via `mate()`, the solver runs a pre-pass to derive base transforms, then the kinematic DFS applies joints on top of those positions.\n\n**Pitfall — [`jointsView`](/docs/viewport#jointsview) double-rotation:** When calling `toJointsView()`, always solve at the rest pose (all joint values = 0 or default). Solving at a non-zero angle and then animating will double-rotate parts. Use the `defaults` option on `toJointsView()` to set the initial display angle instead.\n\nThis pitfall only applies when `toJointsView()` is active. If you only want a static posed result, return the solved assembly directly and skip `toJointsView()`.\n\n**Example — static posed output (no `toJointsView()`)**\n\n```ts\nreturn mech.solve({ shoulder: 45, elbow: -20 });\n```\n\n```ts\nsolve(state?: JointState): SolvedAssembly\n```\n\n**Other**\n\n#### `mate()` — Register mate constraints between parts. Constraints are solved during `solve()` to derive part positions and explode hints. Part references use \"partName:featureName\" format.\n\n```ts\nmate(fn: (m: MateBuilder) => void): Assembly\n```\n\n#### `addFrame()` — Add a virtual reference frame (no geometry) to the assembly graph.\n\nUseful when you need a named pivot point or coordinate frame that has no visual geometry. Acts like a zero-volume part and can be connected to other parts via joints.\n\n```ts\naddFrame(name: string, options?: PartOptions): Assembly\n```\n\n#### `addPart()` — Add a named part to the assembly.\n\nConnectors declared on the part (via `withConnectors()`) are captured automatically. Parts are positioned at world origin by default unless a `transform` is provided in `options`. For root parts (no incoming joint), `transform` is their final world position.\n\nWhen a part is a [`ShapeGroup`](/docs/core#shapegroup), name the group children explicitly to get readable viewport labels (e.g. `\"Base Assembly.Body\"` instead of `\"Base Assembly.1\"`):\n\n```ts\nconst housing = group(\n { name: \"Body\", shape: body },\n { name: \"Lid\", shape: lid },\n);\nassembly.addPart(\"Base Assembly\", housing);\n```\n\n```ts\naddPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly\n```\n\n#### `addJoint()` — Add a kinematic joint between a parent and child part.\n\n`frame` is a transform from the **parent part frame** to the **joint frame at zero state**. The child's world position is computed as:\n\n```\nchildWorld = parentWorld × frame × motion(value) × childBase\n```\n\nFor revolute joints `value` is in degrees; for prismatic joints `value` is in mm. Coupled joints (see `addJointCoupling`) ignore the `state` value passed to `solve()` and compute their value from source joints.\n\n```ts\naddJoint(name: string, type: JointType, parent: string, child: string, options?: JointOptions): Assembly\n```\n\n#### `addRevolute()` — Shorthand for `addJoint(name, 'revolute', parent, child, options)`.\n\n```ts\naddRevolute(name: string, parent: string, child: string, options?: JointOptions): Assembly\n```\n\n#### `addPrismatic()` — Shorthand for `addJoint(name, 'prismatic', parent, child, options)`.\n\n```ts\naddPrismatic(name: string, parent: string, child: string, options?: JointOptions): Assembly\n```\n\n#### `addFixed()` — Shorthand for `addJoint(name, 'fixed', parent, child, options)`.\n\nFixed joints rigidly attach a child part to its parent at `frame` with no motion. Before calling `mergeInto()`, use `addFixed()` to collapse multiple root parts into a single root.\n\n```ts\naddFixed(name: string, parent: string, child: string, options?: JointOptions): Assembly\n```\n\n#### `addJointCoupling()` — Link a joint's value to a linear combination of other joint values.\n\nThe driven joint's value is computed as:\n\n```\ndriven = offset + Σ(ratio_i × source_i)\n```\n\nCoupled joints ignore any value passed in `solve(state)` — a warning is emitted if you try to override one. Coupling cycles are rejected. You cannot sweep a coupled joint directly; sweep one of its source joints instead.\n\n```ts\nassembly\n .addRevolute(\"Steering\", \"Base\", \"Turret\", { axis: [0, 0, 1] })\n .addRevolute(\"WheelDrive\", \"Turret\", \"Wheel\", { axis: [1, 0, 0] })\n .addRevolute(\"TopGear\", \"Base\", \"TopInput\", { axis: [0, 0, 1] })\n .addJointCoupling(\"TopGear\", {\n terms: [\n { joint: \"Steering\", ratio: 1 },\n { joint: \"WheelDrive\", ratio: 20 / 14 },\n ],\n });\n```\n\n```ts\naddJointCoupling(jointName: string, options: JointCouplingOptions): Assembly\n```\n\n#### `addGearCoupling()` — Link two revolute joints via a gear ratio.\n\nChoose exactly one ratio source:\n\n- `ratio` — explicit numeric ratio (driven/driver, negative for external mesh)\n- `pair` — a `GearRatioLike` from `lib.gearPair`, `lib.bevelGearPair`, etc. (uses `pair.jointRatio`)\n- `driverTeeth` + `drivenTeeth` — auto-computes ratio; use `mesh` to control sign (`'external'` = negative/opposite rotation, `'internal'` = positive, `'bevel'`/`'face'` = negative)\n\nWhen `pair` carries a `phaseDeg`, it is auto-applied as the coupling `offset` to align teeth correctly. Override with `offset: 0` if gear shapes already have the phase baked in.\n\n```ts\nconst pair = lib.gearPair({ pinion: { module: 1.25, teeth: 14 }, gear: { module: 1.25, teeth: 42 } });\nassembly\n .addRevolute(\"Pinion\", \"Base\", \"PinionPart\", { axis: [0, 0, 1] })\n .addRevolute(\"Driven\", \"Base\", \"GearPart\", { axis: [0, 0, 1] })\n .addGearCoupling(\"Driven\", \"Pinion\", { pair });\n```\n\n```ts\naddGearCoupling(drivenJointName: string, driverJointName: string, options?: GearCouplingOptions): Assembly\n```\n\n#### `sweepJoint()` — Sample a joint through its motion range, collecting collision data at each step.\n\nDivides `[from, to]` into `steps` intervals (producing `steps + 1` frames). At each sample, the assembly is solved with the sweeping joint at that value and `baseState` for all others. Returns one `JointSweepFrame` per sample with the joint value, collision findings, and any solve warnings.\n\nYou cannot sweep a coupled joint — sweep one of its source joints instead.\n\n```ts\nconst sweep = mech.sweepJoint(\"elbow\", -10, 135, 12, { shoulder: 35 });\nconst hits = sweep.filter(frame => frame.collisions.length > 0);\nconsole.log(`Collisions at ${hits.length} of ${sweep.length} poses`);\n```\n\n```ts\nsweepJoint(jointName: string, from: number, to: number, steps: number, baseState?: JointState, collisionOptions?: CollisionOptions): JointSweepFrame[]\n```\n\n#### `toJointsView()` — Derive viewport joint controls from the assembly graph and register them.\n\nSolves the assembly at rest (all joints = default), then converts each joint into a `JointViewInput` with world-space pivot and axis. Fixed joints become hidden zero-range revolute entries so attached parts follow their parent during animation. Joint couplings are forwarded to the viewport automatically.\n\nThis method is optional. Call it only when you want viewport joint sliders, coupled controls, or playback animations. If you only want geometry, return the `Assembly` or `SolvedAssembly` directly and skip `toJointsView()`.\n\n**Critical pitfall:** Always call `toJointsView()` before solving for display. Then solve at the **rest pose** (no state overrides) and return that solved assembly result directly. Do not flatten it with `.toGroup()` if you want the viewport joint animation to keep working.\n\nDo not solve at a non-zero angle when using `toJointsView()` — the viewport will apply the same rotation again, double-rotating the part.\n\n```ts\nmech.toJointsView({\n defaults: { J1: 30 },\n animations: [{\n name: \"Swing\", duration: 2, loop: true,\n keyframes: [{ values: { J1: -45 } }, { values: { J1: 45 } }, { values: { J1: -45 } }],\n }],\n});\n\n// Solve at REST — viewport handles posing\nreturn mech.solve();\n```\n\n```ts\ntoJointsView(options?: ToJointsViewOptions): void\n```\n\n#### `describe()` — Return the serializable assembly definition used by solve/inspect pipelines.\n\n```ts\ndescribe(): AssemblyDefinition\n```\n\n**Legacy Aliases**\n\n- `usedPortRefs` -> `usedConnectorRefs`\n- `withPorts()` -> `withConnectors()`\n- `getPorts()` -> `getConnectors()`\n- `getPort()` -> `getConnector()`\n\n### `ImportedAssembly`\n\nA wrapper around an imported `Assembly` that provides kinematic access and convenient transform helpers.\n\nWhen a `.forge.js` file returns an unsolved `Assembly`, [`require()`](/docs/core#require) wraps it in an `ImportedAssembly`. This preserves the kinematic structure — you can call `solve()`, `sweepJoint()`, and `mergeInto()` — while also allowing convenience transforms that auto-solve at default values.\n\n**Kinematic access**\n\n```ts\nconst arm = require(\"./arm.forge.js\");\n\nconst solved = arm.solve({ shoulder: 45 }); // full kinematic solve\nconst link = arm.part(\"Link\", { shoulder: 60 }); // single part at state\nconst group = arm.toGroup({ shoulder: 45 }); // only when ShapeGroup behavior is needed\n```\n\n**Convenience transforms** (auto-solve at defaults, return [`ShapeGroup`](/docs/core#shapegroup)):\n\n```ts\nconst positioned = arm.rotateZ(-90).translate(0, -20, 50);\n```\n\n**Merging into a parent**\n\n```ts\nrequire(\"./arm.forge.js\").mergeInto(robot, {\n prefix: \"Left Arm\",\n mountParent: \"Chassis\",\n mountJoint: \"leftMount\",\n mountOptions: { frame: Transform.identity().translate(-70, 0, 10) },\n});\n```\n\n#### `assembly()` — The underlying Assembly — use for sweepJoint, addPart into parent, etc.\n\n```ts\nget assembly(): Assembly\n```\n\n#### `solve()` — Solve the assembly at the given joint state (defaults to each joint's default value).\n\n```ts\nsolve(state?: JointState): SolvedAssembly\n```\n\n#### `part()` — Return a specific named part positioned at the given joint state, with any stored placement offset applied.\n\n```ts\npart(name: string, state?: JointState): AssemblyPart\n```\n\n#### `toGroup()` — Convert all assembly parts to a ShapeGroup with named children. Use this for composition, transforms, or child lookup — not as a required render step for assemblies. Child names match the part names used in the assembly. Any stored placement offset and placement references are forwarded to the group.\n\n```ts\ntoGroup(state?: JointState): ShapeGroup\n```\n\n#### `withReferences()` — Attach named placement reference points to this assembly. Points are simple 3D coordinates (relative to the assembly's own origin). Returns a new ImportedAssembly — does not mutate.\n\n```ts\nwithReferences(refs: Pick<PlacementReferenceInput, \"points\">): ImportedAssembly\n```\n\n#### `referenceNames()` — List all attached placement reference names.\n\n```ts\nreferenceNames(kind?: PlacementReferenceKind): string[]\n```\n\n#### `placeReference()` — Translate the assembly so the named reference point lands on `target`. Returns a new ImportedAssembly — does not mutate. All point refs are translated by the same delta.\n\n```ts\nplaceReference(ref: string, target: [ number, number, number ], offset?: [ number, number, number ]): ImportedAssembly\n```\n\n#### `translate()` — Solve at defaults and return a translated ShapeGroup.\n\n```ts\ntranslate(x: number, y: number, z: number): ShapeGroup\n```\n\n#### `rotate()` — Solve at defaults and return a rotated ShapeGroup.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateX()` — Solve at defaults and return a ShapeGroup rotated around X.\n\n```ts\nrotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateY()` — Solve at defaults and return a ShapeGroup rotated around Y.\n\n```ts\nrotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateZ()` — Solve at defaults and return a ShapeGroup rotated around Z.\n\n```ts\nrotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `scale()` — Solve at defaults and return a scaled ShapeGroup.\n\n```ts\nscale(v: number | [ number, number, number ]): ShapeGroup\n```\n\n#### `mirror()` — Solve at defaults and return a mirrored ShapeGroup.\n\n```ts\nmirror(normal: [ number, number, number ]): ShapeGroup\n```\n\n#### `color()` — Solve at defaults and return a colored ShapeGroup.\n\n```ts\ncolor(hex: string): ShapeGroup\n```\n\n#### `child()` — Solve at defaults, get a named child part from the resulting group.\n\n```ts\nchild(name: string): Shape | Sketch | ShapeGroup\n```\n\n#### `mergeInto()` — Flatten this sub-assembly's parts and joints into `parent` and wire a mount joint.\n\nAll part and joint names from the sub-assembly are prefixed with `\"${options.prefix}.\"` to avoid collisions. After the merge, sub-assembly joints are driven from the parent using the prefixed names:\n\n```ts\nparent.solve({ \"Left Arm.shoulder\": 45, \"Right Arm.shoulder\": -20 })\n```\n\nJoint couplings inside the sub-assembly are preserved and rewritten with the prefix. Ports from sub-assembly parts are forwarded with the prefix.\n\nThe sub-assembly must have exactly one root part. If it has multiple roots, use `addFixed()` first to consolidate them before merging.\n\n```ts\nconst robot = assembly(\"Robot\").addPart(\"Chassis\", chassis);\n\nrequire(\"./arm.forge.js\").mergeInto(robot, {\n prefix: \"Left Arm\",\n mountParent: \"Chassis\",\n mountJoint: \"leftMount\",\n mountOptions: { frame: Transform.identity().translate(-70, 0, 10) },\n});\n```\n\n```ts\nmergeInto(parent: Assembly, options: MergeIntoOptions): Assembly\n```\n\n### `SolvedAssembly`\n\nThe result of solving an assembly at a specific joint state.\n\n`SolvedAssembly` holds world-space transforms for every part at a given pose. Top-level scripts can return a `SolvedAssembly` directly for display. Use `toGroup()` when you specifically need a [`ShapeGroup`](/docs/core#shapegroup) for composition, group-style transforms, or named-child lookup. Do not call `toGroup()` just to make a solved assembly render. Use `getPart()` / `getTransform()` to inspect individual parts programmatically.\n\n**Validation**\n\nCall `collisionReport()` to detect overlapping parts, or `sweepJoint()` on the parent `Assembly` to check for interference across the joint's motion range.\n\n```ts\nconst solved = mech.solve({ shoulder: 45, elbow: -20 });\nconsole.log(\"Collisions\", solved.collisionReport());\nreturn solved;\n```\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `warnings()` — Return any warnings generated during solve (clamped joints, unconverged mates, etc.).\n\n```ts\nwarnings(): string[]\n```\n\n#### `getJointState()` — Return a snapshot of resolved joint values (after clamping and coupling).\n\n```ts\ngetJointState(): JointState\n```\n\n#### `mateExplodeHints()` — Explode direction hints derived from mate constraints, or null if no mates.\n\n```ts\nget mateExplodeHints(): Record<string, { direction: Vec3; }> | null\n```\n\n#### `mateDof()` — Remaining degrees of freedom after mate constraints, or null if no mates.\n\n```ts\nget mateDof(): number | null\n```\n\n#### `mateConverged()` — Whether the mate constraint solver converged, or null if no mates.\n\n```ts\nget mateConverged(): boolean | null\n```\n\n#### `getTransform()` — Return the world-space [`Transform`](/docs/core#transform) for the named part at the solved pose.\n\n```ts\ngetTransform(partName: string): Transform\n```\n\n#### `getPart()` — Return the named part already positioned at its solved world transform.\n\n```ts\ngetPart(partName: string): AssemblyPart\n```\n\n#### `toGroup()` — Convert all solved parts into a [`ShapeGroup`](/docs/core#shapegroup) with named children.\n\nEach part becomes a named child in the group, already positioned at its solved world transform. Use this only when you specifically need a [`ShapeGroup`](/docs/core#shapegroup) for composition, [`ShapeGroup`](/docs/core#shapegroup) transforms, or named-child access. Top-level scripts can return the `SolvedAssembly` directly; do not call `toGroup()` just to make a solved assembly render.\n\n```ts\nconst armGroup = mech.solve({ shoulder: 60 }).toGroup(); // only because we need rotateZ()\nreturn armGroup.rotateZ(90);\n```\n\n```ts\ntoGroup(): ShapeGroup\n```\n\n#### `toSceneObjects()` — Return an array of named scene objects for the viewport renderer.\n\nEach part becomes `{ name, shape }` or `{ name, group: [...] }` if the part is a [`ShapeGroup`](/docs/core#shapegroup). Top-level scripts should normally return the `SolvedAssembly` directly. Use `toGroup()` when you need [`ShapeGroup`](/docs/core#shapegroup) behavior; use this method only for advanced scene-graph control where you need access to the flat per-part array with metadata.\n\n```ts\ntoSceneObjects(): Array<{ name: string; shape?: Shape; group?: Array<{ name: string; shape: Shape; tags?: string[]; }>; metadata?: PartMetadata; }>\n```\n\n#### `toScene()` — Backward-compatible alias for `toSceneObjects()`.\n\n```ts\ntoScene(): Array<{ name: string; shape?: Shape; group?: Array<{ name: string; shape: Shape; tags?: string[]; }>; metadata?: PartMetadata; }>\n```\n\n#### [`bom()`](/docs/output#bom) — Generate a bill of materials for all parts in the solved assembly.\n\n```ts\nbom(): BomRow[]\n```\n\n#### `bomCsv()` — Generate a bill of materials as a CSV string.\n\n```ts\nbomCsv(): string\n```\n\n#### `collisionReport()` — Detect overlapping (colliding) part pairs in this solved pose.\n\nComputes boolean intersections between all part pairs and returns findings where the overlap volume exceeds `minOverlapVolume` (default 0.1 mm³).\n\n```ts\nconst solved = mech.solve({ shoulder: 35, elbow: 60 });\nconsole.log(\"Collisions\", solved.collisionReport());\n```\n\n```ts\ncollisionReport(options?: CollisionOptions): CollisionFinding[]\n```\n\n#### `minClearance()` — Compute the minimum gap (clearance) between two parts in this solved pose.\n\nReturns `0` if the parts are touching or overlapping. Requires the Manifold backend. `searchLength` bounds the search radius in mm — increase it for widely separated parts.\n\n```ts\nminClearance(partA: string, partB: string, searchLength?: number): number\n```\n\n### `MateBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `constraints` | `Constraint3D[]` | — |\n\n**Methods:**\n\n#### `flush()` — Constrain two faces so they stay flush.\n\n```ts\nflush(faceA: string, faceB: string): string\n```\n\n#### `align()` — Constrain two faces so their normals align.\n\n```ts\nalign(faceA: string, faceB: string): string\n```\n\n#### `parallel()` — Constrain two faces so they remain parallel.\n\n```ts\nparallel(faceA: string, faceB: string): string\n```\n\n#### `faceDistance()` — Constrain the distance between two faces.\n\n```ts\nfaceDistance(faceA: string, faceB: string, distance: number): string\n```\n\n#### `concentric()` — Constrain two axes to share the same center line.\n\n```ts\nconcentric(axisA: string, axisB: string): string\n```\n\n#### `axisParallel()` — Constrain two axes to remain parallel.\n\n```ts\naxisParallel(axisA: string, axisB: string): string\n```\n\n#### `pointCoincident()` — Constrain two points to coincide.\n\n```ts\npointCoincident(pointA: string, pointB: string): string\n```\n\n#### `pointOnFace()` — Constrain a point to lie on a face.\n\n```ts\npointOnFace(point: string, face: string): string\n```\n\n#### `pointOnAxis()` — Constrain a point to lie on an axis.\n\n```ts\npointOnAxis(point: string, axis: string): string\n```\n\n#### `angle()` — Constrain the angle between two faces.\n\n```ts\nangle(faceA: string, faceB: string, degrees: number): string\n```\n\n#### `totalEquations()` — Total constraint equations.\n\n```ts\nget totalEquations(): number\n```\n\n---\n\n<!-- generated/sheet-metal.md -->\n\n# Sheet Metal\n\nFolded sheet metal parts with flanges, bends, and flat pattern unfolding.\n\n## Contents\n\n- [Sheet Metal](#sheet-metal) — `sheetMetal`\n- [Laser Cutting](#laser-cutting) — `kerfCompensateOutline`, `kerfCompensateTabs`, `kerfCompensateSlots`, `kerfCompensatePart`, `lookupKerf`, `flatPanel`, `flatPart`, `fingerJoint`, `tabSlot`, `assemblyPreview`, `assemblyInstructions`, `formatInstructions`, `laserKit`\n- [SheetMetalPart](#sheetmetalpart)\n- [FlatPart](#flatpart)\n- [LaserKit](#laserkit)\n- [SHEET_METAL_EDGES](#sheet-metal-edges)\n- [COMMON_KERFS](#common-kerfs)\n\n## Functions\n\n### Sheet Metal\n\n#### `sheetMetal()` — Create a parametric sheet metal part with flanges, bend allowances, and flat-pattern unfolding.\n\n`sheetMetal()` keeps one semantic model and derives both a folded 3D solid and an accurate flat pattern from it. The K-factor bend allowance is applied during unfolding. This is a strict v1 subset — it does not infer sheet metal from arbitrary solids.\n\n**Recommended authoring order:**\n\n1. Define the base panel + thickness + bend parameters.\n2. Chain `.flange()` calls for each edge. Validate with `.folded()` and `.flatPattern()` before adding cutouts.\n3. Add panel cutouts, then flange cutouts one region at a time.\n4. Validate after each new cutout region.\n\n**v1 limitations:** one base panel, up to four 90° edge flanges, constant thickness, explicit K-factor, rectangular corner reliefs, planar cutouts only. No hems, jogs, lofted bends, non-90° flanges, or bend-region cutouts.\n\n```ts\nconst cover = sheetMetal({\n panel: { width: 180, height: 110 },\n thickness: 1.5,\n bendRadius: 2,\n bendAllowance: { kFactor: 0.42 },\n cornerRelief: { size: 4 },\n})\n .flange('top', { length: 18 })\n .flange('right', { length: 18 })\n .flange('bottom', { length: 18 })\n .flange('left', { length: 18 })\n .cutout('panel', rect(72, 36), { selfAnchor: 'center' })\n .cutout('flange-right', roundedRect(26, 10, 5), { selfAnchor: 'center' });\n\nconst folded = cover.folded();\nconst flat = cover.flatPattern();\n```\n\n```ts\nsheetMetal(options: SheetMetalOptions): SheetMetalPart\n```\n\n**`SheetMetalOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `panel` | `{ width: number; height: number; }` | Base panel dimensions. This is the flat blank before flanges are applied. |\n| `thickness` | `number` | Sheet thickness in mm. Applied uniformly across the panel and all flanges. |\n| `bendRadius` | `number` | Inside bend radius in mm. Must be ≥ 0. Typically 0.5–2× the sheet thickness. |\n| `bendAllowance` | `{ kFactor: number; }` | Bend allowance model used when computing the flat-pattern developed length. Currently only K-factor is supported. The K-factor (0–1) describes how far the neutral axis sits from the inner bend surface. Typical values: - Soft materials / large radius: 0.50 - General sheet steel: 0.42–0.44 - Hard materials / tight radius: 0.30–0.38 |\n| `cornerRelief?` | `{ kind?: \"rect\"; size: number; }` | Corner relief cut at each bend intersection. Prevents material overlap when two flanges meet at a corner. Defaults to a rectangular relief sized to `bendRadius + thickness` if omitted. |\n\n### Laser Cutting\n\n#### `kerfCompensateOutline()` — Apply kerf compensation to a complete part outline (outer boundary + holes).\n\nOffsets inward by half-kerf: the outer boundary shrinks and inner holes grow. This is correct because the laser beam removes material on both sides of the cut line.\n\n```ts\nkerfCompensateOutline(sketch: Sketch, kerf: number): Sketch\n```\n\n#### `kerfCompensateTabs()` — Apply kerf compensation to joint protrusions (tabs, fingers).\n\nThese grow by half-kerf so they are slightly oversized and fit tightly in their mating slots after the laser removes material.\n\n```ts\nkerfCompensateTabs(sketch: Sketch, kerf: number): Sketch\n```\n\n#### `kerfCompensateSlots()` — Apply kerf compensation to joint cutouts (slots, holes that receive tabs).\n\nThese grow by half-kerf so tabs can fit into them after the laser removes material from both sides of the slot walls.\n\n```ts\nkerfCompensateSlots(sketch: Sketch, kerf: number): Sketch\n```\n\n#### `kerfCompensatePart()` — Build a kerf-compensated part profile.\n\n1. Start with the base profile.\n2. Kerf-compensate each tab addition (grow by kerf/2), then union with base.\n3. Kerf-compensate each slot subtraction (grow by kerf/2), then subtract from base.\n4. Kerf-compensate the resulting outline (shrink by kerf/2).\n\nOrder matters: joints modify geometry BEFORE outline compensation so the final inward offset applies uniformly to the assembled profile.\n\n```ts\nkerfCompensatePart(baseProfile: Sketch, joints: PartJoints, kerf: number): Sketch\n```\n\n**`PartJoints`**\n- `additions?: Sketch[]` — Geometry to ADD to the base profile (tabs, fingers protruding from edges).\n- `subtractions?: Sketch[]` — Geometry to SUBTRACT from the base profile (slots, holes for mating tabs).\n\n#### `lookupKerf()` — Look up kerf for a material + thickness + laser combo.\n\nIf `laserType` is omitted, returns the first matching material + thickness entry. Returns `undefined` when no match is found.\n\n```ts\nlookupKerf(material: string, thickness: number, laserType?: string): number | undefined\n```\n\n#### `flatPanel()` — Create a rectangular flat panel with 4 named edges.\n\nProfile origin at bottom-left corner. Edges: bottom (y=0), right (x=width), top (y=height), left (x=0). Edge traversal follows CCW winding order.\n\n```ts\nflatPanel(name: string, width: number, height: number, thickness: number, options?: FlatPartOptions): FlatPart\n```\n\n`FlatPartOptions`: `{ material?: string, qty?: number, color?: string }`\n\n#### `flatPart()` — Create a flat part from an arbitrary profile with user-named edges.\n\nEdge normals are computed automatically (perpendicular to direction, rotated 90deg CW).\n\n```ts\nflatPart(name: string, profile: Sketch, thickness: number, edges?: Record<string, { start: [ number, number ]; end: [ number, number ]; }>, options?: FlatPartOptions): FlatPart\n```\n\n#### `fingerJoint()` — Connect two parts with finger joints along specified edges.\n\nAdds finger geometry to partA's edge, cuts matching slots from partB's edge. The joint profiles are positioned along each edge using rotation + translation.\n\n```ts\nfingerJoint(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: FingerJointOptions & { foldAngle?: number; }): void\n```\n\n**`FingerJointOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `fingers?` | `number` | Explicit finger count (must be odd, >= 3). Default: auto from length/thickness. |\n| `fingerWidth?` | `number` | Explicit finger width. Default: auto. |\n| `clearance?` | `number` | Extra clearance per side (mm). Default: 0. |\n| `kerf?` | `number` | Laser kerf (mm). Default: 0. |\n| `endStyle?` | `\"full\" \\| \"half\"` | Whether edge starts with full finger or half. Default: 'full'. |\n\n#### `tabSlot()` — Connect two parts with tab-and-slot joints along specified edges.\n\nAdds tab geometry to partA's edge, cuts matching slots from partB's edge.\n\n```ts\ntabSlot(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: TabSlotOptions & { foldAngle?: number; }): void\n```\n\n**`TabSlotOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `tabCount?` | `number` | Number of tabs. Default: auto (length / (4 * thickness)). |\n| `tabWidth?` | `number` | Tab width. Default: 2 * thickness. |\n| `clearance?` | `number` | Extra clearance per side (mm). Default: 0. |\n| `kerf?` | `number` | Laser kerf (mm). Default: 0. |\n| `inset?` | `number` | Distance from panel edges to first/last tab center. Default: thickness. |\n\n#### `assemblyPreview()` — Generate a 3D assembly preview from flat parts and their joint records.\n\nThe preview can fold joints partially or fully and optionally apply exploded spacing so part relationships are easier to inspect visually.\n\n```ts\nassemblyPreview(parts: FlatPart[], joints: JointRecord[], options?: AssemblyPreviewOptions): AssemblyPreviewResult\n```\n\n**`JointRecord`**\n- `foldAngle: number` — Fold angle in degrees. Default: 90.\n- Also: `type: \"finger\" | \"tabSlot\" | \"snapFit\", partA: string, partB: string, edgeA: string, edgeB: string`\n\n**`AssemblyPreviewOptions`**\n- `kerf?: number` — Kerf compensation passed to each part's solid(). Default: 0\n- `fold?: number` — Fold amount: 0 = flat layout, 1 = fully assembled. Default: 1\n- `explode?: number` — Explode distance: 0 = assembled, >0 = parts spread outward. Default: 0\n\n**`AssemblyPreviewResult`**\n- `shapes: ShapeGroup` — All part shapes grouped for display.\n- `partShapes: Map<string, Shape>` — Individual transformed shapes keyed by part name.\n\n#### `assemblyInstructions()` — Generate step-by-step assembly instructions from flat parts and joints.\n\nAlgorithm:\n\n1. Build adjacency graph from joints\n2. Pick root part (most connections, or user-specified)\n3. BFS from root, creating one step per part addition\n4. Each step describes: which part to add, where it connects, how to orient it\n\nHeuristics for step ordering:\n\n- Start with the part that has the most connections (the base)\n- Add parts that connect to already-assembled parts first (BFS order)\n- Among candidates at the same BFS depth, prefer parts with more connections to already-assembled parts (structurally stable)\n\n```ts\nassemblyInstructions(parts: FlatPart[], joints: JointRecord[], options?: AssemblyInstructionsOptions): AssemblyInstructionsResult\n```\n\n**`AssemblyInstructionsOptions`**\n- `rootPart?: string` — Part to start from. Default: part with most joint connections.\n\n**`AssemblyInstructionsResult`**\n- `totalParts: number` — Total number of parts in the assembly.\n- `orphanParts: string[]` — Parts not connected to the joint graph (orphans).\n- Also: `steps: AssemblyStep[]`\n\n**`AssemblyStep`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `stepNumber` | `number` | 1-based step number. |\n| `description` | `string` | Human-readable instruction. |\n| `partName` | `string` | The part being added in this step. |\n| `partNumber` | `number` | Part number (for cross-ref with cut sheets). |\n| `connectsTo` | `string` | Which existing part it connects to. |\n| `jointType` | `\"finger\" \\| \"tabSlot\" \\| \"snapFit\"` | Joint type used. |\n| `newPartEdge` | `string` | The edge on the new part. |\n| `existingPartEdge` | `string` | The edge on the existing part. |\n| `foldAngle` | `number` | Fold angle in degrees. |\n| `assembledParts` | `string[]` | Part names in the assembly so far (after this step). |\n\n#### `formatInstructions()` — Format assembly instructions as a human-readable text document.\n\nIncludes a \"Step 0\" preamble identifying the base part, followed by numbered steps, and a note about any orphan parts.\n\n```ts\nformatInstructions(result: AssemblyInstructionsResult): string\n```\n\n#### `laserKit()` — Top-level factory for creating a LaserKit container.\n\n```ts\nlaserKit(options?: LaserKitOptions): LaserKit\n```\n\n**`LaserKitOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `material?` | `string` | Default material label for parts that don't specify one. |\n| `sheetWidth?` | `number` | Stock sheet width in mm (default 600). |\n| `sheetHeight?` | `number` | Stock sheet height in mm (default 400). |\n| `kerf?` | `number` | Laser kerf in mm (default 0.2). |\n\n---\n\n## Classes\n\n### `SheetMetalPart`\n\nAn immutable sheet metal part that accumulates flanges and cutouts.\n\nEach mutating method returns a **new** `SheetMetalPart`; the original is unchanged. The part does not produce geometry until you call `.folded()` or `.flatPattern()`.\n\n#### `flange()` — Add a 90° flange along one edge of the base panel.\n\nEach of the four edges (`'top'`, `'right'`, `'bottom'`, `'left'`) may carry at most one flange. Calling `.flange()` twice for the same edge throws.\n\nCorner reliefs are automatically inserted at the intersections of adjacent flanges. Build flanges before cutouts — validate with `.folded()` and `.flatPattern()` after each addition.\n\n```ts\nconst part = sheetMetal({ panel: { width: 100, height: 60 }, thickness: 1.5, bendRadius: 2, bendAllowance: { kFactor: 0.42 } })\n .flange('top', { length: 15 })\n .flange('bottom', { length: 15 });\n```\n\n```ts\nflange(edge: SheetMetalEdge, options: SheetMetalFlangeOptions): SheetMetalPart\n```\n\n#### `cutout()` — Subtract a 2D sketch cutout from a planar region of the sheet metal part.\n\n`region` must be `'panel'` or one of `'flange-top'`, `'flange-right'`, `'flange-bottom'`, `'flange-left'` (only available once the corresponding flange has been added). Cutouts inside bend regions are **not** supported in v1.\n\n`sketch` must be an **unplaced** compile-covered 2D profile (e.g. the result of [`circle2d()`](/docs/sketch#circle2d), [`rect()`](/docs/sketch#rect), [`roundedRect()`](/docs/sketch#roundedrect)). Passing an already-placed sketch (one that has had `.onFace(...)` called on it) will throw.\n\n**Authoring order:** Add all flanges before adding cutouts. Add panel cutouts before flange cutouts. Add one region at a time and validate with `.folded()` / `.flatPattern()` after each step.\n\n```ts\nconst part = sheetMetal({ panel: { width: 180, height: 110 }, thickness: 1.5, bendRadius: 2, bendAllowance: { kFactor: 0.42 } })\n .flange('top', { length: 18 })\n .cutout('panel', rect(72, 36), { selfAnchor: 'center' })\n .cutout('flange-top', roundedRect(26, 10, 5), { selfAnchor: 'center' });\n```\n\n```ts\ncutout(region: SheetMetalPlanarRegionName, sketch: Sketch, options?: SheetMetalCutoutOptions): SheetMetalPart\n```\n\n#### `regionNames()` — Return all semantic region names currently available on this part.\n\nThe returned list always includes `'panel'`. For every flange that has been added, the list also includes the corresponding `'flange-<edge>'` and `'bend-<edge>'` entries.\n\nUse this to discover valid targets for `.cutout()` or for querying faces by region after materializing with `.folded()`.\n\nDefended region names: `panel` | `flange-top` | `flange-right` | `flange-bottom` | `flange-left` | `bend-top` | `bend-right` | `bend-bottom` | `bend-left`\n\n```ts\nregionNames(): SheetMetalRegionName[]\n```\n\n#### `folded()` — Materialize the 3D folded solid.\n\nApplies all flanges (bent up at their configured angles) and all registered cutouts, then returns the resulting [`Shape`](/docs/core#shape). The shape is compiler-owned and exact-exportable (STEP, IGES, etc.).\n\nPrefer calling `.folded()` to validate each build step before proceeding to the final model.\n\n```ts\nfolded(): Shape\n```\n\n#### `flatPattern()` — Materialize the flat-pattern (unfolded blank) for fabrication.\n\nUnfolds all flanges using the K-factor bend allowance and lays the result flat in the XY plane. Cutouts are projected into the flat geometry. The returned shape is exact-exportable and ready for laser / waterjet / CNC nesting workflows.\n\nThe developed length of each bend zone is: `BA = (bendRadius + kFactor × thickness) × angleDeg × π / 180`\n\n```ts\nflatPattern(): Shape\n```\n\n### `FlatPart`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n| `thickness` | `number` | — |\n| `options` | `FlatPartOptions` | — |\n\n**Methods:**\n\n#### `edges()` — All edges as a read-only map.\n\n```ts\nget edges(): ReadonlyMap<string, EdgeInfo>\n```\n\n#### `edge()` — Look up a named edge. Throws if the edge does not exist.\n\n```ts\nedge(name: string): EdgeInfo\n```\n\n#### `edgeNames()` — All edge names on this part.\n\n```ts\nedgeNames(): string[]\n```\n\n#### `partNumber()` — BOM part number assigned to this flat part.\n\n```ts\nget partNumber(): number\n```\n\n#### `joints()` — Joint records that attach this part to other parts in the kit.\n\n```ts\nget joints(): readonly JointRecord[]\n```\n\n#### `quantity()` — Requested quantity of this part in the kit. Defaults to `1`.\n\n```ts\nget quantity(): number\n```\n\n#### `addGeometry()` — Add geometry (e.g. protruding tabs) to the part profile.\n\n```ts\naddGeometry(sketch: Sketch): void\n```\n\n#### `subtractGeometry()` — Subtract geometry (e.g. slot cuts) from the part profile.\n\n```ts\nsubtractGeometry(sketch: Sketch): void\n```\n\n#### `addJoint()` — Record a joint connection for assembly preview.\n\n```ts\naddJoint(record: JointRecord): void\n```\n\n#### `profile()` — Final 2D profile with joints and optional kerf compensation.\n\n```ts\nprofile(kerf?: number): Sketch\n```\n\n#### `solid()` — 3D solid — extrude the profile by material thickness.\n\n```ts\nsolid(kerf?: number): Shape\n```\n\n### `LaserKit`\n\n#### `kerf()` — Laser kerf in mm.\n\n```ts\nget kerf(): number\n```\n\n#### `parts()` — All registered parts (flat, in insertion order).\n\n```ts\nget parts(): readonly FlatPart[]\n```\n\n#### `material()` — Default material label.\n\n```ts\nget material(): string\n```\n\n#### `sheetWidth()` — Stock sheet width in mm.\n\n```ts\nget sheetWidth(): number\n```\n\n#### `sheetHeight()` — Stock sheet height in mm.\n\n```ts\nget sheetHeight(): number\n```\n\n#### `addPart()` — Register a flat part with this kit. Assigns a sequential part number and records the quantity.\n\n```ts\naddPart(part: FlatPart, overrides?: { qty?: number; }): this\n```\n\n#### `cutSheets()` — Generate nested cut sheets using guillotine bin-packing.\n\n```ts\ncutSheets(): CuttingLayoutResult\n```\n\n#### [`bom()`](/docs/output#bom) — Bill of materials listing every part with dimensions.\n\n```ts\nbom(): LaserKitBomEntry[]\n```\n\n#### `partSvgs()` — Individual SVG string for each part profile, keyed by part name.\n\n```ts\npartSvgs(): Map<string, string>\n```\n\n#### `inventorySvg()` — Combined inventory SVG showing all parts in a labeled grid.\n\n```ts\ninventorySvg(): string\n```\n\n#### `assemblyPreview()` — 3D fold-up preview of the assembled kit.\n\n```ts\nassemblyPreview(options?: Omit<AssemblyPreviewOptions, \"kerf\">): AssemblyPreviewResult\n```\n\n#### `assemblyInstructions()` — Step-by-step assembly instructions.\n\n```ts\nassemblyInstructions(options?: AssemblyInstructionsOptions): AssemblyInstructionsResult\n```\n\n#### `formatInstructions()` — Human-readable assembly instructions text.\n\n```ts\nformatInstructions(options?: AssemblyInstructionsOptions): string\n```\n\n---\n\n## Constants\n\n### `SHEET_METAL_EDGES`\n\n### `COMMON_KERFS`\n\nCommon kerf values. Users should always test-cut to verify for their specific setup.\n\n---\n\n<!-- generated/output.md -->\n\n# Output & Annotations\n\nDimensions, BOM entries, verification checks, and sketch export.\n\n## Contents\n\n- [Annotations & Output](#annotations-output) — `bom`, `robotExport`, `dim`, `dimLine`\n- [Sketch Export](#sketch-export) — `sketchToDxf`, `sketchToSvg`\n\n## Functions\n\n### Annotations & Output\n\n#### `bom()` — Register a Bill of Materials entry for report export.\n\nBOM entries are accumulated during script execution and exported alongside the model in report views. Rows are grouped by normalized `description + unit`. Pass an explicit `key` to force multiple descriptions to collapse into a single line item.\n\n- `quantity` must be a finite number `>= 0`. A quantity of `0` is silently ignored (useful for conditional scripting with `param()`-driven counts).\n- `unit` defaults to `\"pieces\"` when omitted or empty.\n- The assembly `solved.bom()` / `solved.bomCsv()` API is separate and covers per-part assembly metadata; this function is for free-form purchased-item annotation.\n- `bom()` is injected into every `.forge.js` script. Call it directly; do not write `const { bom } = require(...)`, because top-level declarations named `bom` collide with the built-in runtime name.\n\n```ts\nconst tubeLen = param(\"Tube Length\", 1200, { min: 300, max: 4000, unit: \"mm\" });\nconst boltCount = param(\"Bolt Count\", 16, { min: 0, max: 200, integer: true });\n\nbom(tubeLen, \"iron tube 30 x 20\", { unit: \"mm\" });\nbom(boltCount, \"M4 bolt, 16 mm length\");\nbom(4, \"rubber foot\", { key: \"foot-rubber\" }); // explicit aggregation key\n\n// Structured metadata for richer reports:\nbom(tubeLen, \"rectangular steel tube\", {\n unit: \"mm\",\n material: \"steel\",\n section: [30, 20],\n wall: 3,\n});\n```\n\n```ts\nbom(quantity: number, description: string, opts?: BomOpts): void\n```\n\n**`BomOpts`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `unit?` | `string` | Quantity unit label, e.g. \"mm\", \"pieces\", \"kg\". Default: \"pieces\" |\n| `key?` | `string` | Optional explicit grouping key used during report aggregation. |\n| `material?` | `string` | Material name, e.g. \"steel\", \"birch plywood\", \"nylon\" |\n| `dimensions?` | `number[]` | Overall dimensions `[width, height]` or `[width, height, thickness]` in the entry's unit |\n| `section?` | `number[]` | Cross-section dimensions `[w, h]` for tubes and profiles |\n| `wall?` | `number` | Wall thickness for hollow sections (mm) |\n| `diameter?` | `number` | Diameter for round stock, bolts, dowels (mm) |\n| `length?` | `number` | Length for fasteners (mm) |\n| `process?` | `string` | Manufacturing process, e.g. \"laser cut\", \"CNC\", \"welded\" |\n| `notes?` | `string` | Free-form notes |\n| `grain?` | `string` | Wood grain direction, e.g. \"long\", \"cross\" |\n\n#### `robotExport()` — Declare that this script should export the assembly as a SDF/URDF robot package.\n\nCall `robotExport()` alongside your assembly definition. The CLI commands `forgecad export sdf` and `forgecad export urdf` pick up the declaration and produce a robot package with:\n\n- Mesh-based inertia tensors (full 6-component, not bounding-box approximations)\n- Separate collision meshes (convex hull by default — ~50–80% smaller)\n- Joint mimic elements derived from `addJointCoupling` / `addGearCoupling`\n\n**Collision mesh modes** (set per-link via `links[\"PartName\"].collision`):\n\n| Mode | Description | Default |\n|------|-------------|---------|\n| `'convex'` | Convex hull (separate `_collision.stl`) | Yes |\n| `'box'` | AABB primitive — fastest physics | |\n| `'visual'` | Same mesh as visual — exact but slow | |\n| `'none'` | No collision geometry | |\n\n**Unit conventions:**\n\n- Revolute `velocity` is in degrees/second in Forge; exporters convert to rad/s.\n- Prismatic distances are in mm in Forge; exported in meters.\n- `massKg` is preferred; `densityKgM3` is used when mass is unknown.\n- Couplings with multiple terms: only the primary term (largest ratio) maps to `<mimic>` — SDF/URDF support single-leader mimic only. Dropped terms emit a warning.\n\n```ts\nconst rover = assembly(\"Scout\")\n .addPart(\"Chassis\", box(300, 220, 50).translate(0, 0, -25))\n .addPart(\"Left Wheel\", cylinder(30, 60, undefined, 48).translate(0, 0, -15))\n .addRevolute(\"leftWheel\", \"Chassis\", \"Left Wheel\", {\n axis: [0, 1, 0],\n frame: Transform.identity().translate(90, 140, 60),\n effort: 20, velocity: 1080,\n });\n\nrobotExport({\n assembly: rover,\n modelName: \"Scout\",\n links: {\n Chassis: { massKg: 10 },\n \"Left Wheel\": { massKg: 0.8 },\n },\n plugins: {\n diffDrive: {\n leftJoints: [\"leftWheel\"], rightJoints: [\"rightWheel\"],\n wheelSeparationMm: 280, wheelRadiusMm: 60,\n },\n },\n world: { generateDemoWorld: true },\n});\n```\n\n**CLI usage**\n\n```bash\nforgecad export sdf model.forge.js # SDF package (Gazebo/Ignition)\nforgecad export urdf model.forge.js # URDF package (ROS/PyBullet/MuJoCo)\n```\n\n```ts\nrobotExport(options: RobotExportOptions): CollectedRobotExport\n```\n\n**`RobotExportOptions`**: `assembly: Assembly`, `modelName?: string`, `state?: JointState`, `static?: boolean`, `selfCollide?: boolean`, `allowAutoDisable?: boolean`, `links?: Record<string, RobotLinkExportOptions>`, `joints?: Record<string, RobotJointExportOptions>`, `plugins?: { diffDrive?: RobotDiffDrivePluginOptions; jointStatePublisher?: RobotJointStatePublisherOptions; }`, `world?: RobotWorldOptions`\n\n`RobotLinkExportOptions`: `{ massKg?: number, densityKgM3?: number, collision?: \"visual\" | \"convex\" | \"box\" | \"none\" }`\n\n`RobotJointExportOptions`: `{ effort?: number, velocity?: number, damping?: number, friction?: number }`\n\n**`RobotDiffDrivePluginOptions`**: `leftJoints: string[]`, `rightJoints: string[]`, `wheelSeparationMm: number`, `wheelRadiusMm: number`, `topic?: string`, `odomTopic?: string`, `tfTopic?: string`, `frameId?: string`, `odomFrameId?: string`, `maxLinearVelocity?: number`, `maxAngularVelocity?: number`, `linearAcceleration?: number`, `angularAcceleration?: number`\n\n`RobotJointStatePublisherOptions`: `{ enabled?: boolean, joints?: string[], topic?: string, updateRate?: number }`\n\n`RobotWorldOptions`: `{ name?: string, generateDemoWorld?: boolean, spawnPose?: RobotPose6, keyboardTeleop?: RobotWorldKeyboardTeleopOptions }`\n\n`RobotWorldKeyboardTeleopOptions`: `{ enabled?: boolean, linearStep?: number, angularStep?: number }`\n\n**`CollectedRobotExport`**: `modelName: string`, `assembly: AssemblyDefinition`, `state: JointState`, `static: boolean`, `selfCollide: boolean`, `allowAutoDisable: boolean`, `links: Record<string, RobotLinkExportOptions>`, `joints: Record<string, RobotJointExportOptions>`, `plugins: { diffDrive?: RobotDiffDrivePluginOptions; jointStatePublisher?: RobotJointStatePublisherOptions; }`, `world: RobotWorldOptions | null`\n\n`AssemblyDefinition`: `{ name: string, parts: AssemblyPartDef[], joints: AssemblyJointDef[], jointCouplings: AssemblyJointCouplingDef[] }`\n\n`AssemblyPartDef`: `{ name: string, part: AssemblyPart, base: Transform, metadata?: PartMetadata }`\n\n**`PartMetadata`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `tags?` | `string \\| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |\n| `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |\n\n**`AssemblyJointDef`**: `name: string`, `type: JointType`, `parent: string`, `child: string`, `frame: Transform`, `axis: Vec3`, `min?: number`, `max?: number`, `defaultValue: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`, `connectorRefs?: JointConnectorRefs`\n\n`JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`\n\n`AssemblyJointCouplingDef`: `{ joint: string, terms: JointCouplingTermRecord[], offset: number }`\n\n`JointCouplingTermRecord`: `{ joint: string, ratio: number }`\n\n#### `dim()` — Add a dimension annotation between two points.\n\nDimension annotations are purely visual callouts rendered in the viewport and report export. They do not affect geometry or constrain the model.\n\nPoint arguments accept 2D tuples `[x, y]`, 3D tuples `[x, y, z]`, or [`Point2D`](/docs/sketch#point2d) objects (Z is treated as 0 for 2D inputs).\n\n**Ownership Rules (Report Pages)**\n\n- `currentComponent: true` — deterministic ownership by the calling import instance. Use when authoring reusable imported parts.\n- `component: \"Part Name\"` — route dimension to another named returned object.\n- Multiple owners: dimension is shared and appears on the assembly overview page.\n- No ownership set: report export infers ownership via endpoint-in-bbox.\n\n```ts\ndim([-w / 2, 0, 0], [w / 2, 0, 0], { label: \"Width\" });\ndim([0, 0, -h / 2], [0, 0, h / 2], { label: \"Height\", offset: 14 });\ndim([0, 0, 0], [100, 0, 0], { component: \"Base\", color: \"#00AAFF\" });\n```\n\n`component` (string or string[] — report ownership), `currentComponent` (boolean)\n\n```ts\ndim(from: PointArg, to: PointArg, opts?: DimOpts): void\n```\n\n`DimOpts`: `{ offset?: number, label?: string, color?: string, component?: string | string[], currentComponent?: boolean }`\n\n#### `dimLine()` — Add a dimension annotation along a [`Line2D`](/docs/sketch#line2d).\n\nConvenience wrapper around { points from a constrained-sketch [`Line2D`](/docs/sketch#line2d) entity. All `opts` are forwarded unchanged.\n\n```ts\nconst a = point(0, 0);\nconst b = point(100, 0);\ndimLine(line(a, b), { label: \"Span\", offset: -8 });\n```\n\n```ts\ndimLine(l: Line2D, opts?: DimOpts): void\n```\n\n### Sketch Export\n\n#### `sketchToDxf()` — Export a 2D sketch as a DXF string (R12/AC1009 — maximally compatible).\n\nFor regular sketches, each polygon loop becomes a closed `LWPOLYLINE`. For constrained sketches, exports raw `LINE`, `CIRCLE`, and `ARC` entities from the constraint edge geometry, which preserves internal/shared edges that `toPolygons()` would merge away.\n\nThe R12 format is chosen for maximum compatibility with CAM tools, laser-cutter software, and older CAD readers.\n\n```ts\nconst s = rect(100, 60);\nconst dxf = sketchToDxf(s, { layer: 'cut' });\n```\n\n```ts\nsketchToDxf(sketch: Sketch, options?: SketchDxfOptions): string\n```\n\n**`SketchDxfOptions`**\n- `layer?: string` — DXF layer name. Default: \"0\"\n- `colorIndex?: number` — DXF color index (1–255, AutoCAD ACI). Default: 7 (white/black)\n\n#### `sketchToSvg()` — Export a 2D sketch as an SVG string.\n\nFor regular sketches, exports filled polygon regions. For constrained sketches, exports raw edge geometry (LINE, ARC, CIRCLE) which preserves internal/shared edges that `toPolygons()` would merge away.\n\nThe SVG uses the sketch's native coordinate system (Y-up) with a CSS transform that flips Y so the output renders correctly in SVG's Y-down space. Coordinates are in sketch units (typically mm).\n\n```ts\nconst s = rect(100, 60);\nconst svg = sketchToSvg(s, { stroke: '#333', strokeWidth: 0.8 });\n```\n\n```ts\nsketchToSvg(sketch: Sketch, options?: SketchSvgOptions): string\n```\n\n**`SketchSvgOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `stroke?` | `string` | Stroke color. Default: \"black\" |\n| `strokeWidth?` | `number` | Stroke width in sketch units. Default: 0.5 |\n| `fill?` | `string` | Fill color. Default: \"none\" |\n| `padding?` | `number` | Padding around the sketch bounding box in sketch units. Default: 2 |\n| `pixelsPerUnit?` | `number` | If set, scale so 1 sketch-unit = this many px. Otherwise auto-fit. |\n\n---\n\n<!-- generated/lib.md -->\n\n# Part Library\n\nPre-built fasteners, gears, pipes, structural profiles, and utility shapes. Access via `lib.*`.\n\n## Contents\n\n- [TangentLoop2D](#tangentloop2d)\n- [DriveWheelBuilder](#drivewheelbuilder)\n- [lib](#lib)\n\n---\n\n## Classes\n\n### `TangentLoop2D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `circles` | `TangentCircle2D[]` | — |\n| `mode` | `BeltMode` | — |\n| `segments` | `BeltPathSegment[]` | — |\n| `straightSpans` | `BeltLineSpan[]` | — |\n| `wraps` | `BeltWrapArc[]` | — |\n| `wrapByPulley` | `Record<string, BeltWrapArc>` | — |\n| `length` | `number` | — |\n\n**Methods:**\n\n#### `toSketch()` — Convert the loop centerline into a thin visual sketch.\n\n```ts\ntoSketch(width?: number): Sketch\n```\n\n#### `toProfile()` — Convert the loop into a filled profile using the pitch path itself as the boundary.\n\n```ts\ntoProfile(): Sketch\n```\n\n#### `offsetBand()` — Build a belt band sketch by offsetting the route to inner and outer pulley radii.\n\n```ts\noffsetBand(thickness: number): Sketch\n```\n\n### `DriveWheelBuilder`\n\n#### `addSpurTeethBetween()` — Add an involute spur-tooth window on part of the pitch circle.\n\n```ts\naddSpurTeethBetween(options: DriveWheelSpurTeethRegionOptions): this\n```\n\n#### `addSolidArcBetween()` — Add a constant-radius solid arc region such as a dwell, stop, or pusher.\n\n```ts\naddSolidArcBetween(options: DriveWheelSolidArcRegionOptions): this\n```\n\n#### `addShapeRegion()` — Add a fully custom region shape while preserving region metadata.\n\n```ts\naddShapeRegion(name: string, shape: Shape, options?: DriveWheelShapeRegionOptions): this\n```\n\n#### `build()` — Build the final wheel shape with a bore connector and region metadata.\n\n```ts\nbuild(): Shape\n```\n\n---\n\n## Constants\n\n### `lib`\n\nPre-built parametric parts available in user scripts as `lib.*`.\n\nEvery key in this object becomes a method or namespace on the `lib` object exposed to `.forge.js` scripts. The catalog includes:\n\n**Fasteners and hardware patterns:** `bolt`, `nut`, `washer`, `fastenerSet`, `boltedServiceCover`, `datumEnclosureAssembly`, `snapLatchCoverAssembly`, `pinnedLeverAssembly`, `retainedShaftAssembly`, `capturedLinearSlide`, `capturedCartridgeGuideAssembly`, `livingHingeCoverAssembly`, `knuckledHingeAssembly`, `clevisPinJointAssembly`, `seatedBearingAssembly`, `cableGlandAnchorAssembly`, `hoseBarbPortAssembly`, `routedTubeClipAssembly`, `pcbTerminalBlockAssembly`, `thumbScrewClampAssembly`, `fastenerHole`, `boltHole`, `counterbore`, `hexNut`, `holePattern`\n\n**Structure:** `tube`, `pipe`, `bracket`, `pipeRoute`, `elbow`, `tSlotProfile`, `tSlotExtrusion`, `profile2020BSlot6Profile`, `profile2020BSlot6`\n\n**Belt drives:** `beltDrive`, `tangentLoop2d`\n\n**Threads:** `thread`\n\n**Gears:** `spurGear`, `sectorGear`, `driveWheel`, `bevelGear`, `faceGear`, `sideGear`, `ringGear`, `rackGear`, `gearPair`, `bevelGearPair`, `faceGearPair`, `sideGearPair`\n\n**Gear bodies:** `gearBodies.disk`, `gearBodies.diskWithHub`, `gearBodies.spoked`, `gearBodies.fromProfile` plus direct aliases `gearBodyDisk`, `gearBodyDiskWithHub`, `gearBodySpoked`, `gearBodyFromProfile`\n\n**Gear ratios (pure math helpers):** `gearRatio`, `rackRatio`, `planetaryRatio`\n\n**Bolt patterns:** `boltPattern` — define hole positions once, cut them from multiple parts\n\n**Utilities:** `explode`\n\nSizes outside the supported ranges will throw at runtime with a descriptive error.\n\n- `boltHole(diameter: number, depth: number): Shape` — Simple cylindrical through-hole cutter centered on Z=0. Subtract the result from a part to produce a plain cylindrical clearance hole. For ISO metric sizes with fit classes and counterbore/countersink, use {\n- `fastenerHole(opts: FastenerHoleOptions): Shape` — ISO metric fastener hole cutter with optional counterbore or countersink. **Details** Returns a cutter shape (subtract from a solid to produce the hole). Sizes outside M2–M10 will throw. Extend `METRIC_HOLE_TABLE` in this file to add new sizes. **Example** ```ts const plate = box(60, 40, 8) .subtract(lib.fastenerHole({ size: 'M5', fit: 'normal', depth: 8 }) .translate(15, 10, 4)); ```\n- `counterbore(holeDia: number, boreDia: number, boreDepth: number, totalDepth: number): Shape` — Counterbore hole cutter — through-hole with a wider cylindrical recess at the top. Use for socket-head cap screws that must sit flush. Subtract from a solid. For ISO metric sizing and fit classes, prefer {\n- `tube(outerX: number, outerY: number, outerZ: number, wall: number): Shape` — Rectangular hollow tube (thin-wall box section). Both the outer and inner boxes are centered on the XY plane with their base at Z=0.\n- `pipe(height: number, outerRadius: number, wall: number, segments?: number): Shape` — Hollow cylindrical pipe. Centered on the XY plane, extending upward along +Z from z=0 to z=height. For complex routed pipe geometry, see `lib.pipeRoute`.\n- `explode<T extends ExplodeItem[] | ShapeGroup>(items: T, options?: ExplodeOptions): T` — Apply deterministic exploded-view offsets to an assembly tree. **Details** Traverses arrays of shapes/sketches/named items, nested `{ name, group: [...] }` structures, and [`ShapeGroup`](/docs/core#shapegroup) outputs, translating each node by a computed offset while preserving names, colors, and nesting. Returns the same structure type as the input. In `radial` mode the algorithm is branch-aware and parent-relative: each node fans out from its immediate parent's center, so nested assemblies peel apart level by level. Named items may also include an inline `explode: { stage?, direction?, axisLock? }` property to override per-item behavior. Use this function when you want to bake the explode offset into the geometry before returning (e.g. to drive the amount with a `param()` slider). For a viewport-only explode slider without rerunning the script, use [`explodeView()`](/docs/viewport#explodeview) instead. **Example** ```js const explodeAmt = param('Explode', 0, { min: 0, max: 40, unit: 'mm' }); return lib.explode(assembly, { amount: explodeAmt, stages: [0.4, 0.8], mode: 'radial', byName: { Shaft: { direction: [1, 0, 0], stage: 1.4 } }, }); ```\n- `hexNut(acrossFlats: number, height: number, holeDia: number): Shape` — Generic hex nut with a cylindrical bore. Constructed via intersection of three rotated rectangular slabs, then a bore is subtracted. Centered at origin, height along Z. For standard ISO metric nuts by thread size, use `lib.nut` instead.\n- `bracket(width: number, height: number, depth: number, thick: number, holeDia?: number): Shape` — L-shaped mounting bracket with optional through-holes. Produces a right-angle bracket: a horizontal base plate and a vertical wall. Both legs share `width`. Optional holes are drilled through the base (along Z) and the wall (along Y).\n- `holePattern(rows: number, cols: number, spacingX: number, spacingY: number, holeDia: number, depth: number): Shape` — Rectangular grid of cylindrical hole cutters. Returns the union of `rows × cols` cylinders laid out on a regular grid. Subtract from a solid to produce the full pattern. **Example** ```ts const pattern = lib.holePattern(3, 4, 20, 20, 4, 10); const panel = box(80, 70, 10).subtract(pattern.translate(-30, -20, 0)); ```\n- `thread(diameter: number, pitch: number, length: number, options?: { depth?: number; segments?: number; }): Shape` — External helical thread — clean mesh, no SDF grid artifacts. **Details** Builds a cross-section with a single trapezoidal tooth from the root radius out to the crest radius, then twist-extrudes it so the tooth traces a helix. Manifold's extrude+twist produces structured quad-based geometry that follows the thread profile cleanly. Returns a threaded cylinder along +Z from z=0 to z=length. **Example** ```ts const t = lib.thread(5, 0.8, 12); // M5 × 0.8 pitch, 12 mm long ```\n- `bolt(diameter: number, length: number, options?: { ... }): Shape` — ISO-style hex bolt with real helical threads. **Details** The hex head sits from z=0 up to z=headHeight. The shaft extends downward along −Z by `length` mm. An unthreaded shank section is included when `threadLength < length`. Default proportions follow ISO 4762 loosely: pitch ≈ 0.15×diameter, head height ≈ 0.65×diameter, across-flats ≈ 1.6×diameter. For standard M-size bolts pre-configured for a complete joint, use { **Example** ```ts const b = lib.bolt(5, 20); // M5 × 20 mm ```\n- `nut(diameter: number, options?: { pitch?: number; height?: number; acrossFlats?: number; segments?: number; }): Shape` — ISO-style hex nut with a threaded bore. **Details** Constructed from the intersection of three rotated slabs with a cylindrical bore subtracted. The nut is centered at the origin, height along Z. Default proportions follow ISO 4032 loosely: height ≈ 0.8×diameter, across-flats ≈ 1.6×diameter. The bore is a clearance bore (not modelled with helical threads) for rendering efficiency. For standard M-size nuts pre-configured for a complete joint, use { **Example** ```ts const n = lib.nut(5); // M5 nut ```\n- `washer(size: MetricSize, options?: { standard?: WasherStandard; segments?: number; }): Shape` — ISO metric flat washer (DIN 125-A). **Details** Returns a flat ring centered at the origin, thickness along Z. Dimensions are taken from { **Example** ```ts const w = lib.washer('M5'); // DIN 125-A M5 washer ```\n- `fastenerSet(size: MetricSize, boltLength: number, options?: FastenerSetOptions): FastenerSetResult` — Complete ISO metric fastener set — bolt, nut, optional washers, and matching hole cutters. **Details** Returns all geometry for one bolted joint: the bolt, nut, up to two washers, a clearance-hole cutter, and a tap-drill cutter. All shapes are returned **un-positioned** (each on the Z-axis). Place them with `.translate()`. Sizes outside M4–M10 are supported for the washer (M2–M10); unsupported combinations will throw. **Example** ```ts const hw = lib.fastenerSet('M5', 20); const topPlate = box(60, 40, 8).translate(0, 0, 12) .subtract(hw.clearanceHole.translate(15, 10, 12)); const botPlate = box(60, 40, 8) .subtract(hw.clearanceHole.translate(15, 10, 0)); return [ { name: 'Top Plate', shape: topPlate }, { name: 'Bot Plate', shape: botPlate }, { name: 'Bolt', shape: hw.bolt.translate(15, 10, 20) }, { name: 'Nut', shape: hw.nut.translate(15, 10, -4) }, ]; ```\n- `boltedServiceCover(options: BoltedServiceCoverOptions): BoltedServiceCoverResult` — Bolted service-cover interface with real seats, aligned holes, gasket, fused pull tabs, and installed screws. **Details** This is a higher-level mechanical pattern for the common \"removable service cover\" failure mode. It creates the parent ledge, cover, gasket, and screws from one shared bolt pattern so agents do not place decorative screw heads or floating pull tabs by eye. Coordinate convention: the parent frame sits from `z=0` to `parentThickness`, the gasket sits on the ledge, the cover sits above the gasket, and screw shafts run downward through the cover into the parent. All parts are centered on the XY origin. **Example** ```ts const cover = lib.boltedServiceCover({ width: 90, depth: 56, screwSize: 'M4', ledgeWidth: 10, boltInset: [6, 6], }); verify.equal('four retained cover screws', cover.screws.length, 4); return cover.parts; ```\n- `datumEnclosureAssembly(options: DatumEnclosureAssemblyOptions): DatumEnclosureAssemblyResult` — Datum-driven enclosure tray with shared wall, ledge, standoff, cover, gasket, port, and screw geometry. **Details** This pattern is for electronics boxes, thermostat backplates, service-stack housings, camera housings, and small fixtures where generated models often place panels, ribs, bosses, ports, and covers by eye. The tray, internal ledges, standoffs, ribs, service port, gasket, cover holes, and installed screws all come from one datum system. This keeps screw axes, boss locations, wall thickness, and service openings aligned instead of relying on independent magic numbers. Coordinate convention: X/Y are the enclosure footprint, Z is up. The base tray starts at `z=0` and rises to `height`; the gasket and cover sit above the top ledge with small explicit face clearances. **Example** ```ts const enclosure = lib.datumEnclosureAssembly({ width: 96, depth: 64, height: 18, }); verify.notColliding('cover clears enclosure gasket', enclosure.cover, enclosure.gasket); verify.inRange('cover stack has small seating clearance', enclosure.dims.faceClearance, 0.01, 0.08); return enclosure.parts; ```\n- `snapLatchCoverAssembly(options: SnapLatchCoverAssemblyOptions): SnapLatchCoverAssemblyResult` — Snap-retained cover with a receiver frame, latch windows, underside catch lands, and fused snap hooks. **Details** This pattern is for covers, cartridges, clasps, and small housings where agents often add decorative tabs without a catch. The receiver has a real service opening plus two clearance latch windows. The cover is one fused part with two flexible-looking snap fingers that pass through the windows and barb under the receiver underside. Nothing intersects in the final assembly; the hook geometry sits close enough to the catch lands to prove retention intent. Coordinate convention: the receiver frame sits from `z=0` to `parentThickness`; the cover is seated just above the receiver on +Z. Two snap hooks sit on the +/-Y ledges and tuck under the receiver. **Example** ```ts const snapCover = lib.snapLatchCoverAssembly({ width: 72, depth: 44, }); verify.notColliding('snap hooks clear receiver windows', snapCover.cover, snapCover.parent); verify.inRange('snap cover has small seating clearance', snapCover.dims.faceClearance, 0.01, 0.08); return snapCover.parts; ```\n- `pinnedLeverAssembly(options: PinnedLeverAssemblyOptions): PinnedLeverAssemblyResult` — Retained pinned lever stack with a fused hub/arm/grip, low stop land, pivot pin, bore cutters, and thrust washers. **Details** This pattern is for the common handle/lever failure mode where a visual arm, hub, washer, and pin are placed near each other but never form a credible mechanism. The lever body is one fused part, the pin runs through aligned bores, washers sit on both sides of the lever, and the support includes a bearing land plus an optional low stop land beside the lever path. Coordinate convention: pivot axis is +Z at the XY origin. The support starts at `z=0`, the lower washer sits on top of the support, the lever sits on the lower washer, the upper washer sits on the lever, and the retained pin spans the full stack. **Example** ```ts const lever = lib.pinnedLeverAssembly({ armLength: 54, armWidth: 10, pinDiameter: 5, }); verify.equal('lever stack has five retained parts', lever.parts.length, 5); return lever.parts; ```\n- `retainedShaftAssembly(options: RetainedShaftAssemblyOptions): RetainedShaftAssemblyResult` — Retained shaft, washer, knob, and support-cheek stack for trunnions, pivots, and adjustable clamps. **Details** This pattern replaces the common \"pin, washers, and knob are near each other\" visual shortcut with a mechanically accountable shaft stack. The two support cheeks get matching clearance bores, the through shaft spans the whole stack, washers and knobs share the same axis, and retaining heads keep the knobs from reading as loose floating cylinders. Coordinate convention: the shaft axis is +X through the world origin. Support cheeks are centered at `x = +/- supportSpacing / 2`. The supports are bored for clearance, so collision inspection should report no support/shaft overlap while the connectivity audit still sees one retained stack. **Example** ```ts const trunnion = lib.retainedShaftAssembly({ supportSpacing: 96, shaftDiameter: 8, supportHeight: 42, }); verify.equal('retained shaft stack has seven parts', trunnion.parts.length, 7); return trunnion.parts; ```\n- `capturedLinearSlide(options: CapturedLinearSlideOptions): CapturedLinearSlideResult` — Captured linear slide with a U-channel rail, return lips, end stops, and a carriage posed inside the guide. **Details** This pattern is for drawer-slide, quick-release plate, and guided-carriage models where agents often place rail details and a moving block near each other without a capture relationship. The rail is one fused part with side walls, inward lips, and end stops; the carriage is wider than the lip throat but narrower than the inner rail width, so it is mechanically captured while retaining explicit clearance. Coordinate convention: rail length is along X, width is along Y, and Z is up. The rail base starts at `z=0`; the carriage sits above the base and below the return lips. `travel=0` places the carriage at the negative-X end of travel, and `travel=maxTravel` places it at the positive-X end. **Example** ```ts const slide = lib.capturedLinearSlide({ length: 160, carriageLength: 52, travel: 42, }); verify.greaterThan('carriage is captured by return lips', slide.dims.carriageWidth, slide.dims.throatWidth); return slide.parts; ```\n- `capturedCartridgeGuideAssembly(options: CapturedCartridgeGuideAssemblyOptions): CapturedCartridgeGuideAssemblyResult` — Captured removable cartridge guide with return lips, rear stop, wide cartridge flange, and pull tab. **Details** This pattern is for pump cartridges, filter cassettes, skeg cassettes, battery cartridges, and slide-in service modules where generated models often place a tray and a loose block near each other. The guide is one fused part with side walls, inward return lips, and a rear stop. The cartridge has a wide lower flange captured under the lips and a narrower body that passes through the throat, so the model has a real retention contract without manual coordinate tuning. Coordinate convention: insertion travel is along +X. The open entry is at −X, the rear stop is at +X, the guide base starts at `z=0`, and `insertion=0` places the cartridge at the front travel limit. **Example** ```ts const cassette = lib.capturedCartridgeGuideAssembly({ length: 150, cartridgeLength: 72, }); verify.notColliding('cartridge clears guide rails', cassette.cartridge, cassette.guide); verify.greaterThan('cartridge flange is captured by lips', cassette.dims.cartridgeWidth, cassette.dims.throatWidth); return cassette.parts; ```\n- `livingHingeCoverAssembly(options: LivingHingeCoverAssemblyOptions): LivingHingeCoverAssemblyResult` — One-piece molded living-hinge cover strip with a fixed leaf, thin flexible web, cover leaf, pull lip, snap barb, and catch land. **Details** This pattern is for small polypropylene-style lids, battery doors, sample covers, blister latches, and molded service flaps where generated models often draw a decorative hinge strip between two disconnected plates. It returns one fused molded part in its as-molded flat state: fixed mounting leaf, thin hinge web, moving cover leaf, pull lip, raised snap barb, and catch land. The flexible web is intentionally much thinner than the rigid leaves and shares material with both leaves. Coordinate convention: X is hinge length/part width, Y runs from fixed leaf through hinge web to cover leaf, and Z is thickness. The hinge web is centered on `y=0`; the fixed leaf lies at −Y and the cover leaf at +Y. **Example** ```ts const livingCover = lib.livingHingeCoverAssembly({ width: 64, coverDepth: 42, }); verify.greaterThan('living hinge is much thinner than rigid leaves', livingCover.dims.flexRatio, 3); return livingCover.parts; ```\n- `knuckledHingeAssembly(options: KnuckledHingeAssemblyOptions): KnuckledHingeAssemblyResult` — Alternating knuckle hinge with two fused leaves and a retained pin. **Details** This pattern replaces hand-placed hinge barrels and pin ghosts with a mechanically accountable hinge. The fixed leaf owns every other knuckle, the moving leaf owns the alternating knuckles, all knuckles share one bore size, and the retained pin spans the full stack with heads outside the barrels. Coordinate convention: the hinge pin axis is +X through the world origin. The fixed leaf extends toward +Y. The moving leaf extends toward -Y and rotates about +X by `openAngleDeg`. **Example** ```ts const hinge = lib.knuckledHingeAssembly({ length: 70, leafLength: 28, openAngleDeg: 45, }); verify.equal('hinge has two leaves and one retained pin', hinge.parts.length, 3); return hinge.parts; ```\n- `clevisPinJointAssembly(options?: ClevisPinJointAssemblyOptions): ClevisPinJointAssemblyResult` — Clevis-style pin joint with bored yoke ears, a center link eye, and a retained pin. **Details** This pattern is for crank links, damper rod ends, pump crossheads, capo/cam pivots, and small mechanism joints where agents often place an eyelet and a pin near a bracket without modeling the captured load path. The clevis is one fused part with two bored ears and a rear bridge, the center link has a real eye and arm, and the retained pin spans the full stack with heads outside the ears. Coordinate convention: the pin axis is +Y through the world origin. The center link arm extends toward +X. The clevis bridge sits behind the eye on -X, leaving the link eye clear inside the yoke. **Example** ```ts const clevis = lib.clevisPinJointAssembly({ pinDiameter: 4, linkArmLength: 38, }); verify.equal('clevis joint has three retained parts', clevis.parts.length, 3); return clevis.parts; ```\n- `seatedBearingAssembly(options: SeatedBearingAssemblyOptions): SeatedBearingAssemblyResult` — Seated radial-bearing support with a real counterbore, shoulder, through shaft, and retaining collars. **Details** This pattern is for purchased bearings, rollers, burr-cartridge shafts, and small spindle supports where agents often place a ring and a shaft near a block without modelling the pocket that locates the bearing. The housing includes a through-bore and a larger counterbore that leaves a shoulder for the bearing outer race. The shaft is smaller than the bearing bore and carries collars outside the housing, so collision checks can distinguish intended clearance from impossible overlap. Coordinate convention: the shaft axis is +Z through the world origin. The housing block starts at `z=0`, the raised boss is on top of the block, the bearing is seated from the top counterbore, and the shaft extends above and below the housing. **Example** ```ts const bearingStack = lib.seatedBearingAssembly({ bearingOuterDiameter: 22, bearingInnerDiameter: 8, bearingWidth: 7, }); verify.greaterThan('housing has wall around bearing pocket', bearingStack.dims.bossOuterDiameter - bearingStack.dims.pocketDiameter, 4); return bearingStack.parts; ```\n- `cableGlandAnchorAssembly(options: CableGlandAnchorAssemblyOptions): CableGlandAnchorAssemblyResult` — Cable, wire, or tube gland anchor with a real panel hole, hollow gland body, compression nut, and routed cable. **Details** This pattern is for pumps, filters, electronics boxes, vents, monitors, and fixtures where generated models often leave hoses or cables terminating in space. It creates the receiving panel hole, a hollow gland body with a panel-side flange seated in a shallow pocket, a hollow compression nut, and a cable/tube that runs through the gland bore with explicit clearance. Coordinate convention: the cable axis is +X through the world origin. The panel is centered around `x=0` with thickness along X; the flange sits on the +X side of the panel and the compression nut sits on the −X side. The cable spans the full anchor. **Example** ```ts const anchor = lib.cableGlandAnchorAssembly({ cableDiameter: 6, panelThickness: 3, }); verify.notColliding('cable clears gland bore', anchor.cable, anchor.gland); verify.clearanceBetween('gland flange is seated at panel pocket', anchor.gland, anchor.panel, 0.01, 0.2); return anchor.parts; ```\n- `hoseBarbPortAssembly(options: HoseBarbPortAssemblyOptions): HoseBarbPortAssemblyResult` — Hose-barb pump/filter port with a bored receiver, shoulder, barb ridges, installed hose, and clamp band. **Details** This pattern is for pump heads, filters, vents, lab cartridges, and fluid fittings where generated models often leave tubes ending near a block. The receiver has a real through-port and raised boss, the fitting is hollow with a shoulder and multiple barb ridges, and the hose is modeled as an installed tube over the barb envelope with a clamp band. The hose bore is sized for the deformed installed hose, so collision checks distinguish the retained interface from impossible solid overlap. Coordinate convention: the fluid axis is +X through the world origin. The receiver block is centered around `x=0`; the raised boss and hose are on the +X side. **Example** ```ts const hosePort = lib.hoseBarbPortAssembly({ hoseInnerDiameter: 6, hoseOuterDiameter: 10, }); verify.notColliding('hose clears barb peaks', hosePort.hose, hosePort.fitting); verify.inRange('fitting shoulder seats near boss face', hosePort.dims.faceClearance, 0.01, 0.08); return hosePort.parts; ```\n- `routedTubeClipAssembly(options: RoutedTubeClipAssemblyOptions): RoutedTubeClipAssemblyResult` — Routed tube or cable retained by saddle clips with real bores, screw holes, and installed screws. **Details** This pattern is for hoses, wires, pump tubes, sensor leads, and appliance cable runs where generated models often draw a cylinder near a wall without clips or strain relief. The base panel has receiving screw envelopes, each saddle clip has a real through-bore around the tube and vertical screw clearances, and the installed screws share those positions. Coordinate convention: the routed tube runs along +X through the world origin. The base panel starts at `z=0`; clips sit on top of the panel, and the tube passes through their bores. **Example** ```ts const route = lib.routedTubeClipAssembly({ tubeDiameter: 6, clipCount: 3, }); verify.notColliding('tube clears clip bores', route.tube, union(...route.clips)); verify.notColliding('clip screws clear retained stack', union(...route.screws), union(route.panel, ...route.clips)); return route.parts; ```\n- `pcbTerminalBlockAssembly(options?: PcbTerminalBlockAssemblyOptions): PcbTerminalBlockAssemblyResult` — PCB terminal-block stack with a backplate, standoffs, mounting screws, pin holes, and a seated terminal block. **Details** This pattern is for thermostat backplates, appliance control panels, sensor boards, and small electronics where generated models often place a terminal block, screw heads, and holes as independent decorations. The PCB mounting holes, fused standoffs, installed screws, terminal pins, and PCB pin clearances all come from one shared datum system so the purchased block is mechanically seated and the board is actually mounted. Coordinate convention: X/Y are the board footprint, Z is up. The backplate starts at `z=0`, standoffs rise from the plate, the PCB rests on the standoffs, and the terminal block sits on top of the PCB near the front edge. **Example** ```ts const terminalStack = lib.pcbTerminalBlockAssembly({ terminalCount: 5, screwSize: 'M3', }); verify.notColliding('terminal pins clear PCB holes', terminalStack.terminalBlock, terminalStack.pcb); verify.notColliding('mounting screws clear PCB and standoff holes', union(...terminalStack.screws), union(terminalStack.pcb, terminalStack.backplate)); return terminalStack.parts; ```\n- `thumbScrewClampAssembly(options?: ThumbScrewClampAssemblyOptions): ThumbScrewClampAssemblyResult` — Thumb-screw clamp with a C-frame, threaded boss, captive pressure pad, knob, and clamped workpiece. **Details** This pattern is for bench clamps, monitor-arm desk clamps, small vise screws, capo pressure screws, fixture hold-downs, and service brackets where generated models often place a loose screw, knob, or pressure pad near a bracket. The helper creates a one-piece clamp frame with a fixed anvil pad, a bored threaded support and boss, an installed screw with a captive pressure pad and hand knob, and a representative clamped workpiece seated between the pads. Coordinate convention: the clamp screw runs along +X. The fixed anvil is on the -X side, the threaded support and knob are on the +X side, and Z is up from the base bridge. **Example** ```ts const clamp = lib.thumbScrewClampAssembly({ screwSize: 'M6', workpieceThickness: 20, }); verify.notColliding('thumb screw clears threaded boss', clamp.clampScrew, clamp.frame); verify.clearanceBetween('pressure pad is seated on workpiece', clamp.clampScrew, clamp.workpiece, -0.01, 0.05); return clamp.parts; ```\n- `pipeRoute(points: [ number, number, number ][], radius: number, options?: { bendRadius?: number; wall?: number; segments?: number; }): Shape` — Route a pipe (solid or hollow) through 3D waypoints with smooth bends. Each interior waypoint gets a torus-section bend. Straight segments connect them. Returns a single unioned Shape.\n- `elbow(pipeRadius: number, bendRadius: number, angle?: number | { ... }, options?: { ... }): Shape` — Pipe elbow — a curved pipe section (torus arc) for connecting two pipe directions. By default creates a bend in the XZ plane: incoming along +Z, outgoing rotated by `angle`. The bend starts at the origin, curving away from it.\n- `beltDrive(options: BeltDriveOptions): BeltDriveResult` — Create a flat open-belt body around two pulley pitch circles. The belt is generated as a tangent loop in the XY plane and extruded along +Z by `beltWidth`. The result includes the solid belt, the 2D belt profile, a thin pitch-path sketch for visualization, total belt length, tangent spans, and wrap metadata for each pulley. For more than two pulleys, the API intentionally asks for route intent before geometry is created. Use `route: \"outer\"` for the future outside-envelope mode, or an ordered route for future serpentine/idler layouts. ```ts const drive = lib.beltDrive({ pulleys: [ { name: \"motor\", center: [0, 0], pitchRadius: 12 }, { name: \"output\", center: [80, 0], pitchRadius: 28 }, ], beltWidth: 8, beltThickness: 2, }); return drive.belt; ```\n- `tangentLoop2d(circles: TangentCircle2D[], options?: TangentLoop2DOptions): TangentLoop2D` — Build a closed 2D route made from common tangent spans and pulley wrap arcs. Use this when you need reusable belt/chain route geometry before creating a solid body. The first implementation supports two circles. `mode: \"open\"` uses external tangents; `mode: \"crossed\"` uses internal tangents. ```ts const route = lib.tangentLoop2d([ { center: [0, 0], radius: 12 }, { center: [80, 0], radius: 28 }, ]); const belt = route.offsetBand(2).extrude(8); ```\n- `tSlotProfile(options?: TSlotProfileOptions): Sketch` — Build a 2D T-slot cross-section sketch. Default parameters describe a 20x20 B-type profile with slot 6. Use this when you want a drawing-ready profile sketch before extrusion.\n- `tSlotExtrusion(length: number, options?: TSlotExtrusionOptions): Shape` — Build a T-slot extrusion from the generated 2D profile. Extrudes along +Z by default.\n- `profile2020BSlot6Profile(options?: Profile2020BSlot6ProfileOptions): Sketch` — Accurate-ish 2D profile for 20x20 B-type slot 6. Returns a drawing-ready Sketch centered at origin.\n- `profile2020BSlot6(length: number, options?: Profile2020BSlot6Options): Shape` — 20x20 B-type slot 6 extrusion with profile-accurate defaults. Pass option overrides if your supplier's profile differs slightly.\n- `spurGear(options: SpurGearOptions): Shape` — Involute external spur gear with optional center bore. Specify module, teeth, faceWidth as required parameters. Optional tuning includes pressureAngleDeg (default 20), backlash, clearance, addendum, dedendum, boreDiameter, and segmentsPerTooth (default 10). **Connectors (for assembly-based positioning):** - `bore`: revolute connector at the bore center, axis along +Z. Carries measurements: `{ module, teeth, pitchRadius, outerRadius, faceWidth }`. Use `.connect(\"Housing.seat\", \"Gear.bore\")` to mount a gear on a shaft seat.\n- `bevelGear(options: BevelGearOptions): Shape` — Conical bevel gear generated from a tapered involute extrusion. Specify pitchAngleDeg directly or derive it from mateTeeth + shaftAngleDeg. **Connectors (for assembly-based positioning):** - `bore`: revolute connector at the large-end bore center (Z=0), axis along -Z (away from teeth). - `apex`: connector at the cone apex above the gear (the point where the pitch cone converges), axis along +Z. Useful for meshing two bevel gears — their apices should coincide. Carries measurements: `{ module, teeth, pitchRadius, pitchAngleDeg, coneDistance, faceWidth }`.\n- `faceGear(options: FaceGearOptions): Shape` — Face gear (crown style) where teeth are on one face (top or bottom) instead of the outer rim. Uses the same involute tooth sizing as spurGear, then projects the tooth band axially from one side. Alias for sideGear (which is kept for backward compatibility).\n- `sideGear(options: SideGearOptions): Shape` — Crown/face style gear where the teeth project from one side of the disk instead of the outer cylindrical rim.\n- `ringGear(options: RingGearOptions): Shape` — Internal ring gear with involute-derived tooth spaces. Specify rimWidth or outerDiameter for the annular body. **Connectors (for assembly-based positioning):** - `bore`: connector at the ring center, axis along +Z. For planetary gearboxes, this is where the ring mounts to the housing. Carries measurements: `{ module, teeth, pitchRadius, innerRadius, outerRadius, faceWidth }`.\n- `rackGear(options: RackGearOptions): Shape` — Linear rack gear with pressure-angle flanks. Use with spurGear for rack-and-pinion mechanisms. **Orientation:** teeth run along the X axis with tooth tips pointing +Y (pitch line at Y=0). The rack is extruded +Z by `faceWidth`. Rotate the rack to align with a different slide axis. **Connectors (for assembly-based positioning):** - `teeth`: prismatic connector at the pitch line center, axis along +X (slide direction). Carries measurements: `{ module, teeth, faceWidth, length }`. Connect to a housing's rack channel: ```js housing.withConnectors({ rack_channel: connector(\"rack-channel\", { origin: [pitchR, 0, channelZ], axis: [1, 0, 0], kind: \"prismatic\", }), }); assembly.connect(\"Housing.rack_channel\", \"Rack.teeth\", { as: \"slide\" }); ```\n- `gearPair(options: GearPairOptions): GearPairResult` — Build or validate a spur-gear pair and return ratio, backlash, and mesh diagnostics. Accepts either shapes from spurGear() or analytical specs for each member. When place is true (default), the gear is auto-positioned at the correct center distance.\n- `bevelGearPair(options: BevelGearPairOptions): BevelGearPairResult` — Build or validate a bevel-gear pair and return ratio diagnostics plus recommended joint placement vectors.\n- `faceGearPair(options: FaceGearPairOptions): FaceGearPairResult` — Build or validate a perpendicular pair between a face gear and a vertical spur gear.\n- `sideGearPair(options: SideGearPairOptions): SideGearPairResult` — Pair helper for side (crown/face) gear + perpendicular \"vertical\" spur gear. Auto-placement rotates the spur around +Y and positions it to mesh at the side tooth band.\n- `gearRatio(teethA: number, teethB: number, options?: { internal?: boolean; }): number` — Coupling ratio between two meshed spur gears. When gear A turns 1°, gear B turns `-teethA / teethB` degrees (negative because meshed external gears rotate in opposite directions). Pass `{ internal: true }` for internal gear pairs (ring gear + spur/planet), where the two rotate in the same direction.\n- `rackRatio(module: number, pinionTeeth: number): number` — Coupling ratio between a pinion and a rack. When the pinion rotates by `θ` degrees, the rack slides by `θ × (π × module × teeth / 360)` mm. Equivalently, 1mm of rack travel = `180 / (π × pitchRadius)` degrees of pinion rotation.\n- `planetaryRatio(sunTeeth: number, ringTeeth: number): number` — Planetary gear reduction ratio when the ring is held fixed. Input: sun. Output: carrier. Ratio: `1 + ringTeeth / sunTeeth`. One turn of the sun produces `1 / ratio` turns of the carrier.\n- `boltPattern(options: BoltPatternOptions): BoltPattern` — Define a bolt pattern once and cut it from multiple parts. const base = bolts.cut(box(60, 50, 10), 12, { from: -1 }); const cover = bolts.cut(box(60, 50, 3), 5, { from: -1 }); // Same positions in both parts — guaranteed aligned. ```\n- `driveWheel(options?: DriveWheelOptions): DriveWheelBuilder` — Start a composable exceptional gear or drive wheel.\n- `readDriveWheelMeta(shape: Shape): DriveWheelMeta | null` — Read the functional-region metadata attached by `driveWheel().build()`.\n- `sectorGear(options: SectorGearOptions): Shape` — Involute sector gear with teeth on only part of the pitch circle. Specify the full-circle pitch as `teethOnFullCircle`, then choose the active tooth window with `firstTooth` and `toothCount`. The body is separate from the tooth region: pass a `gearBody...` shape for spokes, hubs, and product styling, or omit it for a simple root-radius disk. **Example** ```ts const body = lib.gearBodies.spoked({ outerRadius: 22, rimWidth: 3, hubDiameter: 10, spokeCount: 5, spokeWidth: 2.5, faceWidth: 8, boreDiameter: 5, }); const sector = lib.sectorGear({ module: 1.25, teethOnFullCircle: 36, toothCount: 10, faceWidth: 8, body, }); ```\n- `gearBodies: { ... }` — Gear body preset namespace: disk, diskWithHub, spoked, and fromProfile.\n- `gearBodyDisk(options: GearBodyDiskOptions): Shape` — Solid disk/ring gear body, independent from any tooth geometry.\n- `gearBodyDiskWithHub(options: GearBodyDiskWithHubOptions): Shape` — Disk gear body with a raised center hub.\n- `gearBodySpoked(options: GearBodySpokedOptions): Shape` — Spoked gear body with an outer rim, center hub, and radial spokes.\n- `gearBodyFromProfile(profile: Sketch, options: GearBodyFromProfileOptions): Shape` — Extrude a custom 2D profile into a gear body.\n\n---\n\n<!-- generated/wood.md -->\n\n# Woodworking\n\nWood boards with grain/species metadata, and joinery operations: dado, rabbet, mortise & tenon. Access via `Wood.*`.\n\n## Contents\n\n- [WoodBoard](#woodboard)\n- [Wood](#wood)\n\n---\n\n## Classes\n\n### `WoodBoard`\n\nA board of wood with metadata for manufacturing: grain direction, species, and dimensions. The underlying geometry is a simple box.\n\nWoodBoard operations are immutable. Joint operations return new boards instead of carving the original in-place, and transform methods preserve all metadata.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `shape` | `Shape` | The underlying 3D shape. |\n| `width` | `number` | Board width (mm) — the longer flat dimension |\n| `height` | `number` | Board height (mm) — the shorter flat dimension |\n| `thickness` | `number` | Board thickness (mm) |\n| `grain` | `string` | Grain direction: \"long\" or \"cross\" |\n| `species` | `string` | Wood species, e.g. \"birch\", \"oak\" |\n| `material` | `string` | Material label for BOM |\n\n**Methods:**\n\n#### `cut()` — Subtract a cutter from this board, returning a new board. Used by joint functions (dado, rabbet, mortiseAndTenon).\n\n```ts\ncut(cutter: Shape): WoodBoard\n```\n\n#### `translate()` — Translate the board in 3D space.\n\n```ts\ntranslate(x: number, y: number, z: number): WoodBoard\n```\n\n#### `rotate()` — Rotate the board around an axis by a given angle in degrees.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): WoodBoard\n```\n\n#### `rotateX()` — Rotate the board around the X axis by a given angle in degrees.\n\n```ts\nrotateX(angleDeg: number): WoodBoard\n```\n\n#### `rotateY()` — Rotate the board around the Y axis by a given angle in degrees.\n\n```ts\nrotateY(angleDeg: number): WoodBoard\n```\n\n#### `rotateZ()` — Rotate the board around the Z axis by a given angle in degrees.\n\n```ts\nrotateZ(angleDeg: number): WoodBoard\n```\n\n#### `mirror()` — Mirror the board across a plane defined by its normal.\n\n```ts\nmirror(normal: [ number, number, number ]): WoodBoard\n```\n\n#### `color()` — Set the board's display color.\n\n```ts\ncolor(value: string): WoodBoard\n```\n\n#### `clone()` — Clone the board (creates an independent copy of the underlying shape).\n\n```ts\nclone(): WoodBoard\n```\n\n---\n\n## Constants\n\n### `Wood`\n\nWoodworking namespace — create boards and cut joints.\n\n**Boards:** `Wood.board()` creates a WoodBoard with grain, species, and BOM metadata.\n\n**Joints:** `Wood.dado()`, `Wood.rabbet()`, and `Wood.mortiseAndTenon()` are immutable — they return new board value(s) with the joint cut applied.\n\n- `readonly board: (width: number, height: number, thickness: number, opts?: WoodBoardOptions) => WoodBoard` — Create a wood board with metadata for manufacturing. The board is a box(width, height, thickness) centered on XY, base at Z=0. Width along X, height along Y, thickness along Z (0 to thickness).\n- `dado(host: WoodBoard, guest: WoodBoard, opts: DadoOptions): WoodBoard` — Cut a dado (channel) across the face of a host board for a guest board to sit in. Returns a new host board with the dado cut applied.\n- `rabbet(board: WoodBoard, opts: RabbetOptions): WoodBoard` — Cut a rabbet (L-shaped step) along an edge of a board. Returns a new board with the rabbet cut applied.\n- `mortiseAndTenon(mortiseBoard: WoodBoard, tenonBoard: WoodBoard, opts?: MortiseAndTenonOptions): MortiseAndTenonResult` — Cut a mortise in one board and shape a tenon on another. Returns new boards with the mortise pocket and tenon cuts applied.\n\n---\n\n<!-- generated/viewport.md -->\n\n# Viewport & Runtime\n\nCut planes, exploded views, joint animations, and scene configuration.\n\n## Contents\n\n- [Viewport & Runtime](#viewport-runtime) — `Viewport.label`, `scene`, `viewConfig`, `explodeView`, `jointsView`, `cutPlane`, `mock`, `showLabels`, `highlight`\n- [RouteBuilder](#routebuilder)\n- [route](#route)\n\n## Functions\n\n### Viewport & Runtime\n\n#### `Viewport.label()` — Add a render-only viewport label at a world-space point.\n\n`Viewport.label()` is for temporary review, debug, tutorial, or explicitly requested presentation overlays. It does not create sketches, meshes, B-rep topology, exported text, or face labels, so it stays off the OCCT path. Default production models should be understandable from physical geometry, materials, part boundaries, and named objects, not viewport annotations.\n\nUse [`text2d()`](/docs/sketch#text2d) only when the letters should become manufactured geometry, such as raised lettering, engraved serial numbers, or exported nameplates.\n\nLabels are collected during script execution and rendered by the viewport as lightweight overlay annotations. They are ignored by exports and do not appear in `objects`.\n\n```js\nViewport.label('Bearing bore', [0, 0, 18], {\n color: '#f8fafc',\n background: '#0f172acc',\n offset: [0, 0, 8],\n anchor: 'bottom',\n});\n\nreturn box(40, 30, 12);\n```\n\n```ts\nViewport.label(text: string, at: [ number, number, number ], options?: RenderLabelOptions): void\n```\n\n**`RenderLabelOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `color?` | `string` | Text color as any CSS color string. |\n| `background?` | `string` | Background color as any CSS color string. Use `'transparent'` for no pill background. |\n| `size?` | `number` | Font size in CSS pixels. Defaults to 12. |\n| `offset?` | `[ number, number, number ]` | Additional world-space offset from `at`. |\n| `anchor?` | `RenderLabelAnchor` | Which point of the label box is anchored to `at`. Defaults to `'center'`. |\n| `alwaysOnTop?` | `boolean` | When false, the label is hidden when occluded by scene geometry. Defaults to true. |\n\n#### `scene()` — Configure the scene environment for the current script execution.\n\nControls camera position, named render views, optional model journeys, lighting rig, background color or gradient, atmospheric fog, environment maps, post-processing effects, and capture parameters for the `forgecad capture` command. Multiple calls merge — later values override earlier ones on a per-key basis, so you can split configuration across multiple `scene()` calls.\n\nWhen `lights` is specified, **all** default lights are removed. You must include your own ambient light or the scene will be fully dark.\n\nSetting `camera.position` overrides auto-framing — the viewport will no longer auto-fit the geometry on script reload.\n\nNamed render views let scripts check in repeatable cameras next to the model code. The canonical shape is `{ camera: { position, target } }`, and a direct camera shorthand `{ position, target }` is also accepted. Use the canonical shape when you may add view metadata later. Use it from the CLI with `--view hero` on `forgecad render 3d`, `forgecad render hq`, or `forgecad capture`.\n\nModel journeys let scripts check in a compact guided path through named objects. Each journey has ordered `steps`; each step can name a `focus` target by object name/tree path, provide a caption, and optionally provide an explicit camera. In the viewer, journeys are opt-in: they appear as a small Explore control and do not move the camera until the user starts them. Use `forgecad run model.forge.js --journeys` or `--journeys-json` to inspect resolved targets.\n\nPost-processing effects (`bloom`, `vignette`, `grain`) work in the browser viewport only. The CLI applies camera, lights, background, fog, and `toneMappingExposure` but skips shader effects.\n\nAll numeric values accept `param()` expressions.\n\n```js\nscene({\n background: { top: '#000814', bottom: '#001d3d' },\n camera: { position: [160, -120, 100], target: [0, 0, 50], fov: 52 },\n views: {\n hero: {\n camera: { position: [180, -140, 90], target: [0, 0, 25], up: [0, 0, 1], fov: 38 },\n },\n side: { position: [240, 0, 70], target: [0, 0, 25], fov: 34 },\n },\n journeys: {\n grandTour: {\n title: 'Grand Tour',\n startsAt: 'overview',\n steps: [\n { id: 'overview', focus: 'Solar System', caption: 'Start with the whole model.' },\n { id: 'earth', focus: 'Earth', caption: 'Fit and inspect Earth.' },\n ],\n },\n },\n lights: [\n { type: 'ambient', color: '#001233', intensity: 0.08 },\n { type: 'point', position: [120, -80, 130], color: '#00f5d4', intensity: 4, distance: 400, decay: 1 },\n { type: 'point', position: [-100, 60, 20], color: '#f72585', intensity: 3, distance: 350 },\n { type: 'directional', position: [50, -30, 200], color: '#ffd60a', intensity: 1.2 },\n { type: 'hemisphere', skyColor: '#003566', groundColor: '#000814', intensity: 0.2 },\n ],\n fog: { color: '#000814', near: 100, far: 450 },\n postProcessing: {\n bloom: { intensity: param('bloom', 1.5, 0, 4), threshold: 0.5, radius: 0.7 },\n vignette: { darkness: 0.8, offset: 0.25 },\n grain: { intensity: 0.08 },\n toneMappingExposure: param('exposure', 1.5, 0.5, 4),\n },\n});\n```\n\n```ts\nscene(options: SceneOptions): void\n```\n\n**`SceneOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `capture?` | `SceneCaptureConfig` | Default capture parameters for `forgecad capture` — CLI flags override these. |\n| `background?`, `camera?`, `views?`, `journeys?`, `lights?`, `environment?`, `fog?`, `postProcessing?`, `ground?` | | — |\n\n`SceneBackgroundGradient`: `{ top: string, bottom: string }`\n\n**`SceneCameraConfig`**: `position?: [ number, number, number ]`, `target?: [ number, number, number ]`, `up?: [ number, number, number ]`, `fov?: number`, `type?: \"perspective\" | \"orthographic\"`\n\n**`SceneJourneyConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `title?` | `string` | Viewer-facing journey title. Defaults to the journey id. |\n| `startsAt?` | `string` | Optional starting step id. Defaults to the first step. |\n| `behavior?` | `\"opt-in\" \\| \"auto\"` | Whether the viewer should offer or auto-open the journey. First slice supports opt-in. |\n| `steps` | `SceneJourneyStepConfig[]` | Ordered journey spine. Branches can be added later without changing this core contract. |\n| `valid?` | `boolean` | True unless any journey or step diagnostic has level \"error\". |\n\n**`SceneJourneyStepConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `id` | `string` | Stable step id used by viewer links and Next/Back state. |\n| `title?` | `string` | Viewer-facing title. Defaults to the step id. |\n| `focus?` | `string` | Object name or slash-separated tree path to focus. |\n| `caption?` | `string` | Short optional viewer caption. |\n| `camera?` | `SceneViewCameraConfig` | Optional explicit camera for this step. When omitted, the viewer fits `focus`. |\n| `resolvedFocusId?` | `string \\| null` | Resolved object id after script execution, when `focus` matched exactly one object. |\n| `resolvedFocusPath?` | `string \\| null` | Resolved object tree path or name after script execution. |\n\n**`SceneLightConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `target?` | `[ number, number, number ]` | Target for directional/spot lights |\n| `groundColor?` | `string` | Ground color for hemisphere lights |\n| `skyColor?` | `string` | Sky color alias for hemisphere lights (same as color) |\n| `angle?` | `number` | Spot light cone angle in radians |\n| `penumbra?` | `number` | Spot light penumbra (0–1) |\n| `decay?` | `number` | Point/spot light decay |\n| `distance?` | `number` | Point/spot light distance (0 = infinite) |\n| `castShadow?` | `boolean` | Whether this light casts shadows |\n| `type`, `color?`, `intensity?`, `position?` | | — |\n\n**`SceneEnvironmentConfig`**\n- `preset?: \"studio\" | \"sunset\" | \"dawn\" | \"warehouse\" | \"forest\" | \"apartment\" | \"lobby\" | \"city\" | \"park\" | \"night\" | \"none\"` — Built-in preset name or 'none' to disable\n- `intensity?: number` — Environment map intensity\n- `background?: boolean` — Use environment map as scene background\n\n**`SceneFogConfig`**\n- `near?: number` — Linear fog near distance\n- `far?: number` — Linear fog far distance\n- `density?: number` — Exponential fog density (if set, uses FogExp2 instead of linear Fog)\n- Also: `color?: string`\n\n`ScenePostProcessingConfig`: `{ bloom?: SceneBloomConfig, vignette?: SceneVignetteConfig, grain?: SceneGrainConfig, toneMappingExposure?: number }`\n\n`SceneBloomConfig`: `{ intensity?: number, threshold?: number, radius?: number }`\n\n`SceneVignetteConfig`: `{ darkness?: number, offset?: number }`\n\n`SceneGrainConfig`: `{ intensity?: number }`\n\n**`SceneGroundConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `visible?` | `boolean` | Show a ground plane |\n| `color?` | `string` | Ground color |\n| `offset?` | `number` | Offset below the model's bounding box minimum Z. Default 0 (flush with model bottom). |\n| `receiveShadow?` | `boolean` | Receive shadows on the ground |\n\n**`SceneCaptureConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `framesPerTurn?` | `number` | Frames for one full orbit rotation (default: 72) |\n| `holdFrames?` | `number` | Frozen frames before motion starts (default: 6) |\n| `pitchDeg?` | `number` | Orbit pitch angle in degrees (default: auto from camera) |\n| `fps?` | `number` | Output frame rate (default: 24) |\n| `size?` | `number` | Output frame size in pixels (default: 960) |\n| `background?` | `string` | Canvas background color for capture (default: '#252526') |\n\n#### `viewConfig()` — Configure viewport helper visuals for the current script execution.\n\nControls renderer-only overlays that appear in the viewport but are not part of the geometry. Currently supports the joint overlay that renders axis arrows and arc indicators when `jointsView` is active. Multiple calls merge — later values override earlier ones per key.\n\nThis does **not** trigger a geometry recompute; it only affects the visual helpers drawn on top of the 3D scene.\n\n```js\nviewConfig({\n jointOverlay: {\n axisColor: '#13dfff',\n arcColor: '#ff7a1a',\n axisLineRadiusScale: 0.03,\n arcLineRadiusScale: 0.022,\n },\n});\n```\n\n```ts\nviewConfig(options?: ViewConfigOptions): void\n```\n\n#### `explodeView()` — Configure how the viewport explode slider offsets returned objects.\n\nOffsets are resolved from the returned object tree, not a flat list. In `radial` mode each node follows its parent branch direction, then fans locally from the immediate parent center — nested assemblies peel apart level by level. In fixed-axis or fixed-vector modes, the branch follows that axis/vector but nested descendants fan out perpendicular by default.\n\nMultiple calls merge — later values override earlier ones on a per-key basis. `byName` and `byPath` maps are merged entry-by-entry.\n\nFor programmatic explode applied before returning (without the slider), use `lib.explode()` instead.\n\n```js\nexplodeView({\n amountScale: 1.2,\n stages: [0.35, 0.8],\n mode: 'radial',\n byPath: { 'Drive/Shaft': { direction: [1, 0, 0], stage: 1.6 } },\n});\n```\n\n```ts\nexplodeView(options?: ExplodeViewOptions): void\n```\n\n**`ExplodeViewOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `enabled?` | `boolean` | Set false to disable viewport explode offsets for this script output. |\n| `amountScale?` | `number` | Scales the UI explode amount. Default: 1 |\n| `stages?` | `number[]` | Per-depth stage multipliers (depth 1 = first level). If depth exceeds this array, the last value is reused. Default when omitted: reciprocal depth (1, 1/2, 1/3, ...) |\n| `mode?` | `ExplodeViewDirection` | Global direction mode fallback. Default: 'radial' |\n| `axisLock?` | `ExplodeAxis` | Global axis lock fallback. |\n| `byName?` | `Record<string, ExplodeViewDirective>` | Per-object overrides by final object name. |\n| `byPath?` | `Record<string, ExplodeViewDirective>` | Per-tree-path overrides using slash-separated object tree segments. |\n\n**`ExplodeDirective`**\n- `stage?: number` — Multiplier applied to `amount` for this node\n- `direction?: ExplodeDirection` — Direction mode for this node\n- `axisLock?: ExplodeAxis` — Optional axis lock after direction is resolved\n\n#### `jointsView()` — Register viewport-only mechanism controls that animate returned objects without re-running the script.\n\nDefines joints (revolute or prismatic), optional gear/rack couplings, and named animations. The viewport resolves transforms through the joint chain at display time — the script geometry is computed only once at rest pose.\n\n**Critical:** Solve the assembly at **rest pose** (all animated joints = 0). The viewport applies `jointsView` transforms on top of the returned scene. If geometry is already solved at non-zero angles, animation will double-rotate everything.\n\n```js\n// BAD — double rotation\nconst solved = mech.solve({ shoulder: 45, elbow: 30 });\njointsView({ joints: [{ name: 'shoulder', ... }] });\nreturn solved;\n\n// GOOD — rest pose, jointsView controls all posing\nconst solved = mech.solve({ shoulder: 0, elbow: 0 });\njointsView({\n joints: [\n { name: 'shoulder', child: 'Upper Arm', default: 45, ... },\n { name: 'elbow', child: 'Forearm', parent: 'Upper Arm', default: 30, ... },\n ],\n});\nreturn solved;\n```\n\n**Pivot coordinates** are world-space positions of each joint origin at rest pose. For `addRevolute('shoulder', 'Base', 'Link', { frame: Transform.identity().translate(0, 0, 20) })` where \"Base\" is at world origin, the pivot is `[0, 0, 20]`.\n\n**Fixed attachments** that must follow a parent during animation need a zero-angle revolute joint in the chain:\n\n```js\n{ name: 'EE_Follow', child: 'End Effector', parent: 'Last Link',\n type: 'revolute', axis: [0, 0, 1], pivot: [linkLength, 0, 0],\n min: 0, max: 0, default: 0 }\n```\n\nAnimation values are interpolated linearly between keyframes. ForgeCAD does **not** auto-wrap revolute values across `-180/180`. Keep keyframe values continuous — a `-180 -> 171` jump spins the part the long way around. Use `-180 -> -189` instead. Author high-speed multi-turn joints as accumulating angles (`0, 360, 720, ...`) with `continuous: true`.\n\n**Tick-based keyframes:** Omit `at` from all keyframes to auto-distribute by tick weight:\n\n```js\nkeyframes: [\n { ticks: 3, values: { Shoulder: 20 } }, // slow segment (3x weight)\n { ticks: 1, values: { Shoulder: -10 } }, // fast segment (1x weight)\n { values: { Shoulder: 20 } }, // last keyframe; ticks ignored\n]\n// positions: 0, 0.75, 1.0\n```\n\nMixing explicit `at` and omitted `at` in the same animation is not allowed.\n\n```js\njointsView({\n joints: [{\n name: 'Shoulder', child: 'Upper Arm', parent: 'Base',\n type: 'revolute', axis: [0, -1, 0], pivot: [0, 0, 46],\n min: -30, max: 110, default: 15,\n }],\n animations: [{\n name: 'Walk Cycle', duration: 1.6, loop: true,\n keyframes: [\n { values: { Shoulder: 20 } },\n { values: { Shoulder: -10 } },\n { values: { Shoulder: 20 } },\n ],\n }],\n});\n```\n\n```ts\njointsView(options?: JointsViewOptions): void\n```\n\n**`JointsViewOptions`**: `enabled?: boolean`, `joints?: JointViewInput[]`, `couplings?: JointViewCouplingInput[]`, `animations?: JointViewAnimationInput[]`, `defaultAnimation?: string`\n\n**`JointViewInput`**: `name: string`, `child: string`, `parent?: string`, `type?: JointViewType`, `axis?: JointViewAxis`, `pivot?: [ number, number, number ]`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `hidden?: boolean`\n\n`JointViewCouplingInput`: `{ joint: string, terms: JointViewCouplingTermInput[], offset?: number }`\n\n`JointViewCouplingTermInput`: `{ joint: string, ratio?: number }`\n\n`JointViewAnimationInput`: `{ name: string, duration?: number, loop?: boolean, continuous?: boolean, keyframes: JointViewAnimationKeyframeInput[] }`\n\n**`JointViewAnimationKeyframeInput`**\n- `at?: number` — Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights.\n- `ticks?: number` — Relative weight of the segment from this keyframe to the next (default 1). Only used in tick-based mode (when `at` is omitted). Last keyframe's ticks value is ignored.\n- Also: `values: Record<string, number>`\n\n#### `cutPlane()` — Define a named section plane for inspecting internal geometry.\n\nRegisters a cut plane that appears as a toggle in the viewport View Panel. When enabled, geometry on the positive side of the plane (the side the normal points toward) is clipped away, revealing the internal cross-section. The newly exposed section faces render with a hatched overlay; pre-existing coplanar boundary faces are left unhatched.\n\nPlanes are registered once per script run. The viewport toggle state (on/off) persists across parameter changes without re-running the script. The `exclude` option only works correctly when the excluded object names are stable across parameter changes.\n\nAccepts two overloads: `cutPlane(name, normal, offset?, options?)` or `cutPlane(name, normal, options?)` where options may include `offset`.\n\n```js\nconst cutZ = param('Cut Height', 10, { min: -50, max: 50, unit: 'mm' });\ncutPlane('Inspection', [0, 0, 1], cutZ, { exclude: ['Probe', 'Fasteners'] });\n```\n\nOverloads:\n\n- `cutPlane(name: string, normal: [ number, number, number ], offset?: number, options?: CutPlaneOptions): void`\n- `cutPlane(name: string, normal: [ number, number, number ], options?: CutPlaneOptions): void`\n\n**`CutPlaneOptions`**\n- `offset?: number` — Optional offset along the plane normal (primarily for object-form overload).\n- `exclude?: CutPlaneExcludeInput` — Object names to keep uncut for this plane.\n\n#### `mock()` — Register a mock (context) object for visualization and collision checking.\n\nMock objects appear in the viewport and spatial analysis when you run a file directly, but are excluded when the file is imported via [`require()`](/docs/core#require). This lets you model the surrounding context — walls, bolts, mating parts — without polluting the module's exports.\n\nThe shape is returned unchanged, so you can reference it for alignment, dimensioning, and `verify` checks.\n\nMock objects participate in `forgecad run --spatial bounded|exact` collision detection and spatial analysis. Their names appear with a `(mock)` suffix in reports.\n\nIn the viewport, mock objects render at reduced opacity so they are visually distinct from real geometry.\n\n```ts\n// bracket.forge.js\nconst wall = mock(box(100, 200, 10).translate(0, 0, -5), \"wall\");\nconst bolt = mock(cylinder(3, 15).translate(10, 15, 0), \"bolt\");\n\nconst bracket = box(20, 30, 5);\nverify.notColliding(\"bracket vs wall\", bracket, wall);\n\nreturn bracket;\n// When imported: only bracket is exported\n// When run directly: bracket + wall + bolt all visible\n```\n\n```ts\nmock<T extends Shape>(shape: T, name?: string): T\n```\n\n#### `showLabels()` — Highlight all user-labeled faces on a shape for visual debugging.\n\nShows each user-authored label name in the viewport for visual debugging. Returns the shape unchanged for chaining: `return showLabels(myShape)`.\n\n```ts\nshowLabels(shape: Shape): Shape\n```\n\n#### `highlight()` — Highlight any geometry for visual debugging in the viewport.\n\nSupported inputs:\n\n- `string` — sketch entity ID (e.g. `'L0'`, `'P0'`, `'C0'`)\n- `[x, y, z]` — 3D point\n- `[[x1,y1,z1], [x2,y2,z2]]` — edge (line segment)\n- `{ normal: [x,y,z], offset: number }` — plane by normal + distance from origin\n- `{ normal: [x,y,z], point: [x,y,z] }` — plane by normal + point on plane\n- [`Shape`](/docs/core#shape) — highlight entire 3D shape\n- `FaceRef` (from `shape.face('top')`) — highlight as plane at face center\n- `EdgeRef` (from `shape.edge('left')`) — highlight as edge segment\n\nOverloads:\n\n- `highlight(entityId: string, opts?: HighlightOptions): void`\n- `highlight(point: [ number, number, number ], opts?: HighlightOptions): void`\n- `highlight(edge: [ [ number, number, number ], [ number, number, number ] ], opts?: HighlightOptions): void`\n- `highlight(plane: { normal: [ number, number, number ]; offset: number; }, opts?: HighlightOptions): void`\n- `highlight(plane: { normal: [ number, number, number ]; point: [ number, number, number ]; }, opts?: HighlightOptions): void`\n- `highlight(shape: Shape, opts?: HighlightOptions): void`\n- `highlight(face: FaceRef, opts?: HighlightOptions): void`\n- `highlight(edge: EdgeRef, opts?: HighlightOptions): void`\n\n**`HighlightOptions`**\n- `size?: number` — Size hint for points (radius in mm) or planes (disc radius in mm).\n- Also: `color?: string, label?: string, pulse?: boolean`\n\n**`FaceRef`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `normal` | `[ number, number, number ]` | Normal direction of the face |\n| `center` | `[ number, number, number ]` | Center point of the face |\n| `query?` | `FaceQueryRef` | Compiler-owned face query when available. |\n| `planar?` | `boolean` | True when the face can host a 2D sketch placement frame |\n| `uAxis?` | `[ number, number, number ]` | Face-local horizontal axis for planar faces |\n| `vAxis?` | `[ number, number, number ]` | Face-local vertical axis for planar faces |\n| `surface?` | `FaceSurface` | Analytic surface family when the backend can identify one. |\n| `descendant?` | `FaceDescendantMetadata` | Shared descendant-resolution metadata when this face is a semantic region/set. |\n| `name` | | — |\n\n**`FaceDescendantMetadata`**: `kind: \"single\" | \"face-set\"`, `semantic: FaceDescendantSemantic`, `memberCount: number`, `memberNames: string[]`, `coplanar: boolean`\n\n**`EdgeRef`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `start` | `[ number, number, number ]` | Start point |\n| `end` | `[ number, number, number ]` | End point |\n| `query?` | `EdgeQueryRef` | Compiler-owned edge query when available. |\n| `curve?` | `EdgeCurve` | Exact or parametric curve family when the backend/source can identify one. |\n| `faceName?` | `string` | Owning face name when the edge is associated with one face in a larger topology. |\n| `name` | | — |\n\n---\n\n## Classes\n\n### `RouteBuilder`\n\n#### `up()` — Vertical line going +Y. Length is optional (solver determines it from constraints).\n\n```ts\nup(length?: number): LineId\n```\n\n#### `down()` — Vertical line going -Y. Length is optional.\n\n```ts\ndown(length?: number): LineId\n```\n\n#### `right()` — Horizontal line going +X. Length is optional.\n\n```ts\nright(length?: number): LineId\n```\n\n#### `left()` — Horizontal line going -X. Length is optional.\n\n```ts\nleft(length?: number): LineId\n```\n\n#### `lineAt()` — Line at an arbitrary angle (degrees from +X). Length is optional.\n\n```ts\nlineAt(angleDeg: number, length?: number): LineId\n```\n\n#### [`line()`](/docs/sketch#line) — Line with solver-determined direction. Length is optional. Direction comes from tangency to previous arc or from constraints.\n\n```ts\nline(length?: number): LineId\n```\n\n#### `toward()` — Line toward a specific point. Length defaults to the distance to that point.\n\n```ts\ntoward(x: number, y: number): LineId\n```\n\n#### `arcLeft()` — Tangent arc turning left relative to travel direction.\n\nor `{ minSweep: degrees }` to seed the geometry without constraining. `minSweep` guides the solver to the correct branch for arcs that sweep more than the default 90° seed.\n\n```ts\narcLeft(radius?: number, sweepDegOrOpts?: number | { minSweep: number; }): ArcId\n```\n\n#### `arcRight()` — Tangent arc turning right relative to travel direction.\n\nor `{ minSweep: degrees }` to seed without constraining.\n\n```ts\narcRight(radius?: number, sweepDegOrOpts?: number | { minSweep: number; }): ArcId\n```\n\n#### `close()` — Close the route with a straight line back to the start point.\n\n```ts\nclose(): void\n```\n\n#### `done()` — Close the route back to its start point and register as a profile loop.\n\nNo extra line segment is added. A coincident constraint connects the last point to the start, and tangency is added for G1 smoothness when arcs are at the junction. The session's incremental solver processes these constraints, keeping seed positions accurate for the final solve.\n\n```ts\ndone(): void\n```\n\n#### `start()` — PointId of the route's start point.\n\n```ts\nget start(): PointId\n```\n\n#### `end()` — PointId of the current cursor (route's end).\n\n```ts\nget end(): PointId\n```\n\n#### `startOf()` — Get the start point of a segment.\n\n```ts\nstartOf(segId: LineId | ArcId): PointId\n```\n\n#### `endOf()` — Get the end point of a segment.\n\n```ts\nendOf(segId: LineId | ArcId): PointId\n```\n\n---\n\n## Constants\n\n### `route`\n\nRoute step factories. Access via `route.line()`, `route.fillet()`, etc.\n\n---\n\n<!-- guides/modeling-recipes.md -->\n\n# Modeling Recipes\n\n## Iteration Bias\n\n- Default to a buildable first pass instead of a long proposal.\n- Replace a broken model wholesale when that is faster than incremental patching.\n- Validate early with `forgecad run <file>`.\n\n## Common Patterns\n\n### Hollow Shell\n```javascript\nconst innerSize = outer - 2 * wall;\nconst outerBox = box(outer, outer, outer).placeReference('center', [0, 0, 0]);\nconst innerBox = box(innerSize, innerSize, innerSize).placeReference('center', [0, 0, 0]);\nreturn outerBox.subtract(innerBox);\n```\n\n### Sketch-Based Twist\n```javascript\nconst outer = ngon(sides, radius);\nconst inner = ngon(sides, radius - wall);\nreturn outer.subtract(inner).extrude(height, { twist: 45, divisions: 32 });\n```\n\n### Rounded Profiles\n```javascript\n// All convex corners — offset trick\nconst base = rect(50, 30).offset(-3, 'Round').offset(3, 'Round');\n\n// Selected corners only\nconst roof = filletCorners(roofPoints, [\n { index: 3, radius: 19 },\n { index: 4, radius: 19 },\n { index: 5, radius: 19 },\n]);\n```\n\n### Choosing the right sketch-rounding tool\n\n- `offset(-r).offset(+r)` — round every convex corner of a closed outline\n- `stroke(points, width, 'Round')` — centerline-based geometry (ribs, traces)\n- `filletCorners(points, ...)` — selective true-corner fillets on mixed profiles\n\n## Best Practices\n\n- All dimensions in millimeters; angles in degrees.\n- Primitives are centered on XY, base at Z=0. Use `placeReference('center', [0,0,0])` to center on all axes.\n- Prefer named intermediate values over deeply nested one-liners.\n- `union2d`, `difference2d`, `intersection2d` batch faster than chained `.add()` / `.subtract()`.\n\n## Debugging\n\n```javascript\nconsole.log(\"Volume:\", shape.volume());\n```\n\nFor sketch-heavy work, compare the raw profile and rounded profile side-by-side before extruding:\n\n```javascript\nreturn [\n { name: \"Raw\", sketch: polygon(roofPoints) },\n { name: \"Rounded\", sketch: filletCorners(roofPoints, [...]).translate(120, 0) },\n];\n```\n\n## Common Errors\n\n- `\"Kernel not initialized\"` — internal/runtime issue, reload the app\n- zero dimensions or self-intersecting sketches → invalid geometry\n- wrong variable name → `\"Cannot read property of undefined\"`\n\nFor deeper API coverage, load the relevant generated doc group from the skill source map instead of reaching for repo examples by default.\n\n---\n\n<!-- guides/joint-design.md -->\n\n# Joint Design Recipes\n\nHow to build mechanical joints — clevis-tongue hinges, ball-and-socket, dovetails — that actually rotate without binding and stop where they should.\n\n## The Cavity Rule\n\nEvery mechanical joint has a **cavity** in one part and a **tenon** in the other. The cavity must be a real empty volume — not a gap implied by the absence of two separate solids.\n\nIf two adjacent parts in an assembly show a collision volume larger than the expected clearance volume in `forgecad run`, one part is missing its cavity. Both parts have solid material at the same joint position. This will look fine at rest pose but will block rotation and produce confusing joint behavior.\n\n```ts\n// BAD — body has a stadium cap at both ends; the \"slot\" between two clevis tines\n// is just empty space next to a solid body cap. The next phalanx's tongue knuckle\n// has nowhere to go (it intersects the previous body's cap).\nconst body = stadiumBar(L); // cap at X=0 AND X=L\nconst tine1 = box(...).translate(L, Y_OFF, 0);\nconst tine2 = box(...).translate(L, -Y_OFF, 0);\nlet phalanx = union(body, tine1, tine2);\n\n// GOOD — body ends FLAT before the joint. Tines extend forward to the pivot.\n// The X = L-KNUCK_R..L+KNUCK_R volume between the tines is genuinely empty.\nconst body = box(L - KNUCK_R, TONG_T, H).translate((L - KNUCK_R) / 2, 0, -H / 2);\nconst tongueKnuckle = knuckleDisc(0, 0, TONG_T); // proximal cap only\nlet phalanx = union(tongueKnuckle, body, tine1, tine2, ...tineCaps);\n```\n\nAfter applying the cavity rule, `forgecad run` collision volume between adjacent parts in a clevis-tongue chain should drop to **zero** (or a few mm³ of clearance overlap). If it doesn't, there's still solid material where there should be a cavity.\n\n## Connecting Cantilevers\n\nA clevis tine arm at Y=±Y_OFF is geometrically separate from a body at Y=±TONG_T/2. With Y_OFF > TONG_T/2 + clearance, there is a **physical gap** between them. The tines float — they would snap off as soon as load is applied.\n\nAlways add a **yoke**: a short slab spanning the full clevis width, sitting between the body's flat distal end and the tines' attachment point. The yoke fills the Y gap so material is continuous from the body through to each tine.\n\n```ts\nconst yokeLen = 3; // a few mm of structural overlap\nconst yokeStart = L - KNUCK_R - yokeLen;\nconst totalY = (Y_OFF + TINE_T / 2) * 2; // full clevis width\nconst yoke = box(yokeLen, totalY, H)\n .translate(yokeStart + yokeLen / 2, 0, -H / 2);\nphalanx = union(phalanx, yoke);\n```\n\n## Hard Stops vs Slider Limits\n\n`addRevolute({ min: 0, max: 90 })` sets **slider limits** — the viewport won't let the user drag past them, but the geometry permits any rotation. There is no physical stop.\n\nFor a **geometric** hard stop (parts can't backbend past extension, or can't curl past full closure), add a small protrusion on one part that interferes with the other at the limit angle:\n\n- **Extension stop at 0°** (typical for fingers, knees, elbows): add a small \"lip\" on the dorsal side of the proximal end of the child phalanx, sized so it just touches the parent's distal dorsal corner at 0°. Negative rotation (backbending) is then blocked by part-on-part contact.\n- **Flexion stop at θmax**: add a similar lip on the palmar side, or rely on the body-to-body collision when bodies meet.\n\nVerify with `forgecad run` at the limit poses — the contact pair should show ~0 mm³ collision (just touching), and rotation past the limit should report a non-zero collision volume.\n\n## Knuckle Sizing\n\nFor a clevis-tongue joint with body height H, the tongue knuckle radius and clevis tine knuckle radius must satisfy:\n\n```\nKNUCK_R >= H / 2\n```\n\nIf the knuckle radius is smaller than the body's half-height, the body's corners protrude beyond the knuckle envelope. When the joint rotates, those corners sweep through space outside the cylindrical envelope and collide with the adjacent part.\n\nSetting `KNUCK_R = H / 2` exactly makes the body cross-section a stadium that perfectly fits the knuckle envelope.\n\n## Verification Workflow\n\n1. Build the joint at rest pose. Run `forgecad run`. Check collision volumes.\n2. If adjacent parts in the joint show > clearance-volume of overlap → missing cavity (apply the cavity rule).\n3. Render with `--focus PartName` to inspect each part in isolation. The clevis end should clearly show a gap between the tines (the cavity).\n4. Render at curl angles (set joint debug params) at 30°, 60°, 90°. No new collisions should appear from rotation.\n5. Render at -10° (backbend test). Either no rotation possible (geometric stop in place) or rotation occurs and you need to add a stop.\n\n---\n\n<!-- guides/inspection-bundles.md -->\n\n# Inspection Bundles\n\n`forgecad render inspect` writes a deterministic directory bundle for agents,\ntests, and automation. Use it when a single shaded PNG is too ambiguous and the\nconsumer needs geometry-aware signals such as depth, normals, surface roughness,\npart identity, physical connected components, collisions, local thickness, or\ncross-sections.\n\n## When To Use It\n\n- Use `forgecad render inspect` for agent repair loops, model debugging, CI\n artifacts, and structured visual comparison.\n- Use `forgecad render 3d` for a quick human viewport PNG.\n- Use `forgecad render section` when you only need one specific cut plane.\n- Use `forgecad render hq` for presentation-quality output, docs, and marketing\n renders.\n\n## Command\n\n```bash\nforgecad render inspect examples/api/static-assembly-connectors.forge.js --channels rgb,mask\nforgecad render inspect model.forge.js out/model-inspect --channels rgb,section --force\nforgecad render inspect model.forge.js --channels rgb,mask,section\nforgecad render inspect model.forge.js --channels collisions --focus \"Bench.*\"\nforgecad render inspect model.forge.js --channels rgb,mask --hide \"Bench.Slat0,Bench.Slat1\"\nforgecad render inspect model.forge.js --channels thickness --min-thickness 1.2 --warn-thickness 2.0\nforgecad render inspect channels\n```\n\nThe default output directory is `<script-name>-inspect/` next to the input file.\nPass `--force` to replace an existing bundle directory.\n\nThere are no default channels. Pass `--channels` every time as a\ncomma-separated subset. Run `forgecad render inspect channels` to list the\nsupported channels in the installed CLI. Keep bundles targeted to the current\nquestion so heavy analyses do not run unnecessarily.\n\n`--focus` and `--hide` use the same object-name filtering semantics as\n`forgecad run` and `forgecad render 3d`. A bare `--focus` hides mock objects;\n`--focus name1,name2` emits only matching objects; `--hide name1,name2` removes\nmatching objects from an otherwise visible scene. Matching is case-insensitive\nand supports `*` / `?` globs, so grouped child objects are usually best matched\nwith patterns such as `Bench.*`.\n\n## Bundle Layout\n\nA bundle that asks for `--channels rgb,depth,normals,mask,section` has this\nlayout:\n\n```text\nmodel-inspect/\n manifest.json\n channels/\n rgb/\n front.png\n right.png\n top.png\n iso.png\n depth/\n front.png\n right.png\n top.png\n iso.png\n normals/\n front.png\n right.png\n top.png\n iso.png\n mask/\n front.png\n right.png\n top.png\n iso.png\n section/\n xy/\n 000.png\n 001.png\n 002.png\n 003.png\n 004.png\n xz/\n 000.png\n ...\n yz/\n 000.png\n ...\n```\n\nUse targeted channel groups for expensive analyses instead of running every\nimplemented channel in one bundle:\n\n```bash\nforgecad render inspect model.forge.js --channels depth,normals\nforgecad render inspect model.forge.js --channels rgb,roughness\nforgecad render inspect model.forge.js --channels rgb,mask,collisions\nforgecad render inspect model.forge.js --channels rgb,section,thickness\n```\n\nSupported channels are `rgb`, `depth`, `normals`, `roughness`, `mask`,\n`connectivity`, `distance`, `collisions`, `thickness`, and `section`.\n\n## Channel Semantics\n\n`rgb` emits the standard solid viewport render with a thin edge overlay. Views\nare canonical `front`, `right`, `top`, and `iso`.\n\n`depth` emits visible ray-distance heatmaps. Each shaded pixel is colored by the\ndistance from the camera position to the visible surface point, normalized per\nview between `minDistance` and `maxDistance` from the manifest:\n\n```text\nrayDistance = distance(cameraPosition, surfacePoint)\nnormalized = (rayDistance - minDistance) / (maxDistance - minDistance)\n```\n\nThe ramp is blue near the camera, green in the middle, and red far from the\ncamera. Background pixels are black and should be treated as `null`.\n\n`normals` emits camera-view normals packed into RGB:\n\n```text\nnormal = normalize((rgb / 255) * 2 - 1)\n```\n\nBackground pixels are black and should be treated as `null`.\n\n`roughness` emits a mesh-dihedral surface-quality heatmap. Smooth and gently\ncurved triangles render as a faint translucent shadow over black, while\ntriangles adjacent to sharp, harsh, boundary, or non-manifold mesh edges render\nin orange or magenta:\n\n```text\nshadow = max adjacent angle < sharpAngleDeg\norange = sharpAngleDeg <= angle < harshAngleDeg\nmagenta = angle >= harshAngleDeg, boundary, or non-manifold\n```\n\nThe default thresholds are `smoothAngleDeg=5`, `sharpAngleDeg=30`, and\n`harshAngleDeg=90`. The manifest stores the method, thresholds, palette, object\nlist, per-object triangle and edge counts, area percentages by smooth,\nmoderate, sharp, and harsh classes, angle percentiles, maximum angle, quality\nscore, and warnings. Moderate angles are reported in the manifest but stay in\nthe shadow layer by default so intentionally curved surfaces do not light up as\ndefects. Use this channel to spot spiky tessellation, accidental faceting,\njagged boolean residue, and dense sharp-corner regions without losing the\nsilhouette of otherwise smooth surfaces.\n\nThe channel also writes `channels/roughness/point-cloud.json`. Each point sample\nstores object identity, object-local position, normal, dihedral angle, class,\nRGB color, and represented surface area. The PNG renders those samples over\nmuted source geometry so the visual evidence stays point-level instead of\npainting a whole object.\n\n`mask` emits one object-color image per view. Black is background. Non-black\npixels resolve through `manifest.channels.mask.objects`, which includes object\nindex, RGB color, object id, name, group, tree path, and mock flag. Edge pixels\nmay be antialiased blends; use solid interior colors for exact object lookup.\n\n`connectivity` emits one physical-component-color image per view. Black is\nbackground. Non-black pixels resolve through\n`manifest.channels.connectivity.components`, and every visible object also has a\n`componentIndex` in `manifest.channels.connectivity.objects`.\n\nConnectivity is computed from visible scene objects:\n\n```text\nbbox candidate = bbox interiors overlap or bbox contact gap <= 0.05 model units\noverlap edge = exact boolean intersection volume > 0.1 model units^3\ncomponent = transitive closure over exact overlap edges\n```\n\nThe manifest stores the edge list, component list, per-object body counts, and\nwarnings. Component colors group scene objects; if one scene object contains\nmultiple disconnected kernel bodies and the caller supplied a body count, the\nmanifest reports `bodyCount > 1` but the PNG cannot color those internal bodies\nseparately yet.\n\nConnectivity uses bbox only as a broadphase. Bbox contact is not enough to merge\nseparate scene objects, which keeps concave assemblies such as cages and captive\nballs from being falsely colored as one component. Use the `collisions` channel\nwhen you need positive-volume overlap evidence as a defect report rather than a\ncomponent grouping.\n\n`distance` emits one rooted physical-component-distance heatmap per view. Black\nis background. Non-black pixels resolve through\n`manifest.channels.distance.components`, and every visible object also has\n`componentIndex`, `rootDistance`, `nearestGap`, and parent-tree metadata in\n`manifest.channels.distance.objects`.\n\nDistance is computed from visible scene objects:\n\n```text\ncomponent = physical connectivity component\ngap edge = Euclidean distance between component bounding boxes\nroot = largest component by body count, object count, then bbox volume\nrootDistance = shortest accumulated gap distance from root component\n```\n\nFor large scenes the manifest does not materialize the complete component gap\ngraph, because that graph is quadratic in the number of components. The\n`gapEdgeCount` field reports the logical complete-graph edge count used by the\nanalysis. `gapEdges` stores a compact evidence subset containing nearest-gap\nand root-parent edges.\n\nThe PNG colors components from green at the root/near distances through yellow to\nred at the farthest rooted component. The manifest stores the root component,\nmaximum rooted distance, compact gap edge evidence, nearest-gap data, and\nshortest-path parent fields. The current v1 metric is bbox-based: it measures air\ngaps between component bounding boxes, not exact closest mesh-surface distance.\n\n`collisions` emits one ghosted-overlap image per view. It uses the same\n`--focus` / `--hide` visibility set as every other inspect channel: focused\nobjects are the only inspected objects. Source objects render as translucent\nghosts, while actual boolean intersection volumes render as solid per-finding\npalette colors.\n\nCollision findings are computed from visible scene objects:\n\n```text\ncollision = boolean intersection volume > 0.1mm^3\n```\n\nThe manifest stores the inspected objects, collision pair names/ids, overlap\nvolume, broadphase counters, warnings, render style, and each collision finding's\n`groupIndex`, `color`, and `hex`. Exact interior pixels can be matched against\n`manifest.channels.collisions.collisions[].color`; antialiased edges may blend\nwith the ghosted source geometry. If `--focus PartA,PartB` is used, everything\nexcept those objects is hidden, `PartA` and `PartB` are ghosted, and their\noverlap volume is highlighted if present.\n\n`thickness` emits one local wall-thickness heatmap per view. The renderer places\ndeterministic area-weighted point samples across visible mesh surfaces, casts\nthrough the object along each sample normal, and colors each point by the first\nopposite-surface distance:\n\n```text\nred = thickness <= minThickness\norange = thickness <= warnThickness\ngreen = acceptable thickness\nblue = thickness >= maxThickness\ngray = unresolved sample\n```\n\nThe default thresholds are `minThickness=1.2`, `warnThickness=2.0`, and\n`maxThickness=6.0` model units. Override them with `--min-thickness`,\n`--warn-thickness`, and `--max-thickness`. Use `--thickness-samples` to raise or\nlower the maximum thickness point samples per object.\n\nThe manifest stores the method, thresholds, palette, object list, per-object\ntriangle counts, sampled-triangle counts, minimum, p05, median, mean, maximum,\ncritical-area percentage, warning-area percentage, below-warning percentage, and\nunresolved-area percentage. This makes the PNG useful for visual debugging while\nthe manifest remains the machine-readable source of truth.\n\nThe channel also writes `channels/thickness/point-cloud.json`. Each point sample\nstores object identity, object-local position, normal, measured thickness,\nclass, RGB color, and represented surface area. The PNG renders those samples\nover muted source geometry, so local evidence survives even when neighboring\ntriangles have very different values.\n\n`roughness` uses the same area-weighted point placement. Point colors are local\nto nearby physical feature edges: smooth tessellation diagonals do not become\nvisible roughness lines. Use `--roughness-samples` to raise or lower the maximum\nroughness point samples per object.\n\n`section` emits five interior slices per principal plane. The current slicing\npolicy is:\n\n```text\noffset = bbox.min[axis] + fraction * (bbox.max[axis] - bbox.min[axis])\nfractions = [1/6, 2/6, 3/6, 4/6, 5/6]\nplanes = xy, xz, yz\n```\n\nEach section slice records its exact offset, fraction, area, path count, size,\nand contributing object count in the manifest.\n\n## Manifest\n\n`manifest.json` is the authoritative contract for consuming a bundle. It\ncontains:\n\n- `schemaVersion` and generator metadata.\n- Source entry file and project root paths.\n- Requested channels, emitted channels, filters, image size, and quality.\n- Canonical views.\n- Scene metadata: bbox, volume, params, cut planes, animations, verifications,\n and objects.\n- Channel metadata and relative file paths.\n\nA consumer should prefer paths from the manifest over hard-coding bundle layout.\nThe layout is intentionally simple, but the manifest is where encoding details,\nper-view depth ranges, and object-mask mappings live.\n\n## Current Limits\n\n- Depth is a visual heatmap, not an EXR or raw float array.\n- Normals are camera-view normals, not world-space normals.\n- Mask indices are stable within a bundle and resolved through the manifest; do\n not infer identity from object order alone.\n- Connectivity is object-level. It reports disconnected kernel bodies in the\n manifest, but the PNG does not split a single scene object into per-body colors.\n- Bbox contact is only broadphase evidence and does not merge separate scene\n objects by default. Boolean-overlap edges are exact.\n- Distance is a physical-component bbox-gap metric in v1, not exact nearest\n mesh-surface distance. Concave components and loose bounding boxes can make the\n reported gap smaller than the real closest-surface distance.\n- Collisions are only positive-volume boolean overlaps. Face-touching parts are\n not collision findings.\n- Thickness is a mesh/raycast approximation, not FEA or a manufacturability\n guarantee. Open meshes, concave geometry, very coarse tessellation, or low\n `--thickness-samples` values can leave gray/unresolved or approximate regions.\n- Section atlases use five default interior slices today.\n- Zebra/reflection-line inspection is a follow-up channel, not part of the v1\n bundle.\n\n---\n\n<!-- generated/sdf.md -->\n\n# SDF Modeling\n\nSigned Distance Field modeling for organic forms, smooth booleans, TPMS lattices, and deformations. SDFs are inherently implicit fields, not B-rep/exact geometry; use them with caution when precision or exact export matters. Return raw `SdfShape` values directly for native preview; use `toShape(...)` when materializing SDF trees for CAD/export workflows.\n\n## Contents\n\n- [SDF Materialization](#sdf-materialization) — `toShape`, `combine`\n- [SdfShape](#sdfshape)\n- [sdf](#sdf)\n- [Sculpt](#sculpt)\n\n## Functions\n\n### SDF Materialization\n\n#### `toShape()` — Materialize one SDF leaf or all SDF leaves in a renderable tree.\n\nRaw `SdfShape` values become mesh-backed [`Shape`](/docs/core#shape)s. Plain objects and arrays preserve their renderable children as a [`ShapeGroup`](/docs/core#shapegroup) when more than one leaf is found. Non-renderable metadata is ignored for materialization and remains available to callers through normal [`require()`](/docs/core#require) return values.\n\n```ts\ntoShape(value: unknown, options?: SdfToShapeOptions): ToShapeTreeResult\n```\n\n**`SdfToShapeOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `edgeLength?` | `number` | Target mesh edge length. Smaller = finer mesh. Overrides quality-derived resolution. |\n| `bounds?` | `{ min: Vec3; max: Vec3; }` | Override auto-computed bounds. Strongly recommended for infinite/repeated fields. |\n| `quality?` | `SdfMeshingQuality` | Coarse quality preset. Default: 'preview'. |\n| `tolerance?` | `number` | Preferred absolute surface tolerance in millimeters. |\n| `minFeatureSize?` | `number` | Smallest feature that should survive meshing, in millimeters. |\n| `simplify?` | `boolean \\| \"safe\"` | Simplification control. `false` disables, `true` and `'safe'` use topology-validated simplification. |\n| `maxTriangles?` | `number` | Optional post-extraction triangle budget. |\n| `maxGridPoints?` | `number` | Optional pre-extraction grid-point budget. Default is browser-safe. |\n| `minEdgeLength?` | `number` | Lower clamp for resolved edge length. Default: 0.15mm. |\n| `diagnostics?` | `boolean` | Log resolved meshing settings and backend extraction timings. |\n\n#### `combine()` — Collapse a tree of SDF leaves into one continuous SDF field.\n\nThis intentionally discards per-leaf color/material identity because the result is one scalar field. Use plain object returns for multi-material SDF preview, and use `combine(...)` only when you want one implicit body.\n\n```ts\ncombine(value: unknown, options?: CombineOptions): SdfShape\n```\n\n`CombineOptions`: `{ op?: \"union\" | \"intersection\" }`\n\n---\n\n## Classes\n\n### `SdfShape`\n\nAn immutable SDF expression. Supports SDF-specific operations (smooth booleans, domain warps, etc.), can be returned directly for native preview, and converts to a ForgeCAD Shape via `.toShape()` when materialization is needed.\n\n#### `colorHex()` — Display color carried by this implicit leaf.\n\n```ts\nget colorHex(): string | undefined\n```\n\n#### `materialProps()` — Display material carried by this implicit leaf.\n\n```ts\nget materialProps(): ShapeMaterialProps | undefined\n```\n\n#### `explicitBounds()` — Explicit bounds carried by this implicit leaf, if any.\n\n```ts\nget explicitBounds(): SdfBounds | undefined\n```\n\n#### `clone()` — Clone this SDF expression and its visual metadata.\n\n```ts\nclone(): SdfShape\n```\n\n#### `toShape()` — Mesh this SDF into a ForgeCAD Shape through ForgeCAD's Surface Nets pipeline. Once converted, the result is a regular Shape — booleans, transforms, export all work.\n\n```ts\ntoShape(options?: SdfToShapeOptions): Shape\n```\n\n#### `color()` — Set the display color for this implicit leaf.\n\n```ts\ncolor(value: string | undefined): SdfShape\n```\n\n#### `material()` — Set PBR display material properties for this implicit leaf.\n\n```ts\nmaterial(props: ShapeMaterialProps): SdfShape\n```\n\n#### `bounds()` — Set explicit preview/meshing bounds for this implicit leaf.\n\n```ts\nbounds(bounds: SdfBounds | [ Vec3, Vec3 ]): SdfShape\n```\n\n#### `at()` — Sculpt-style alias for translate().\n\n```ts\nat(x: number, y: number, z: number): SdfShape\n```\n\n#### `move()` — Sculpt-style alias for translate().\n\n```ts\nmove(x: number, y: number, z: number): SdfShape\n```\n\n#### `spin()` — Sculpt-style alias for rotateZ().\n\n```ts\nspin(angleDeg: number): SdfShape\n```\n\n#### `tilt()` — Sculpt-style tilt around X, Y, Z, or a custom axis.\n\n```ts\ntilt(angleDeg: number, axis?: \"x\" | \"y\" | \"z\" | Vec3): SdfShape\n```\n\n#### `round()` — Sculpt-style rounded-box helper. Currently applies directly to primitive SDF boxes.\n\n```ts\nround(radius: number): SdfShape\n```\n\n#### `blend()` — Sculpt-style smooth blend with another implicit shape.\n\n```ts\nblend(other: SdfShape, options?: number | { radius?: number; }): SdfShape\n```\n\n#### `goop()` — Sculpt-style alias for blend().\n\n```ts\ngoop(other: SdfShape, options?: number | { radius?: number; }): SdfShape\n```\n\n#### `carve()` — Sculpt-style smooth carve/subtract.\n\n```ts\ncarve(other: SdfShape, options?: number | { radius?: number; }): SdfShape\n```\n\n#### `keep()` — Sculpt-style smooth intersection/keep operation.\n\n```ts\nkeep(other: SdfShape, options?: number | { radius?: number; }): SdfShape\n```\n\n#### `polish()` — Apply a Sculpt material preset or direct material props.\n\n```ts\npolish(input?: SculptPolishInput): SdfShape\n```\n\n#### [`union()`](/docs/core#union) — SDF union (sharp).\n\n```ts\nunion(...others: SdfShape[]): SdfShape\n```\n\n#### `subtract()` — SDF difference (sharp) — subtracts others from this.\n\n```ts\nsubtract(...others: SdfShape[]): SdfShape\n```\n\n#### `intersect()` — SDF intersection (sharp).\n\n```ts\nintersect(...others: SdfShape[]): SdfShape\n```\n\n#### `clipBox()` — Clip this SDF to an explicit box-shaped design space.\n\n```ts\nclipBox(x: number, y: number, z: number): SdfShape\n```\n\n#### `fillWith()` — Keep only the material where this shape overlaps another SDF pattern.\n\n```ts\nfillWith(pattern: SdfShape): SdfShape\n```\n\n#### `fillWithGyroid()` — Keep only the gyroid lattice inside this shape.\n\n```ts\nfillWithGyroid(options: TpmsOptions): SdfShape\n```\n\n#### `fillWithSchwarzP()` — Keep only the Schwarz-P lattice inside this shape.\n\n```ts\nfillWithSchwarzP(options: TpmsOptions): SdfShape\n```\n\n#### `fillWithDiamond()` — Keep only the diamond TPMS lattice inside this shape.\n\n```ts\nfillWithDiamond(options: TpmsOptions): SdfShape\n```\n\n#### `fillWithLidinoid()` — Keep only the lidinoid TPMS lattice inside this shape.\n\n```ts\nfillWithLidinoid(options: TpmsOptions): SdfShape\n```\n\n#### `smoothUnion()` — Smooth union — blends shapes together with a smooth radius.\n\n```ts\nsmoothUnion(other: SdfShape, radius: number): SdfShape\n```\n\n#### `smoothSubtract()` — Smooth difference — smoothly carves other from this.\n\n```ts\nsmoothSubtract(other: SdfShape, radius: number): SdfShape\n```\n\n#### `smoothIntersect()` — Smooth intersection — smoothly intersects.\n\n```ts\nsmoothIntersect(other: SdfShape, radius: number): SdfShape\n```\n\n#### `morph()` — Morph between this shape and another. t=0 → this, t=1 → other.\n\n```ts\nmorph(other: SdfShape, t: number): SdfShape\n```\n\n#### `translate()` — Translate this SDF by the given offsets in millimeters.\n\n```ts\ntranslate(x: number, y: number, z: number): SdfShape\n```\n\n#### `rotate()` — Rotate around an arbitrary axis through the origin.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number): SdfShape\n```\n\n#### `rotateX()` — Rotate around the X axis by the given angle in degrees.\n\n```ts\nrotateX(angleDeg: number): SdfShape\n```\n\n#### `rotateY()` — Rotate around the Y axis by the given angle in degrees.\n\n```ts\nrotateY(angleDeg: number): SdfShape\n```\n\n#### `rotateZ()` — Rotate around the Z axis by the given angle in degrees.\n\n```ts\nrotateZ(angleDeg: number): SdfShape\n```\n\n#### `scale()` — Uniformly scale this SDF around the origin.\n\n```ts\nscale(factor: number): SdfShape\n```\n\n#### `twist()` — Twist around the Z axis.\n\n```ts\ntwist(degreesPerUnit: number): SdfShape\n```\n\n#### `bend()` — Bend around the Z axis with given radius.\n\n```ts\nbend(radius: number): SdfShape\n```\n\n#### `repeat()` — Repeat in space. Spacing of 0 on an axis means no repetition. Count of 0 = infinite.\n\n```ts\nrepeat(spacing: Vec3, count?: Vec3): SdfShape\n```\n\n#### `circularArray()` — Arrange this SDF in a circular array around the Z axis.\n\nThe source shape is translated by `offset` in +X before arraying. This uses angular domain folding, so evaluation stays O(1): the source SDF is sampled twice no matter how many copies are requested.\n\n```ts\ncircularArray(count: number, offset?: number): SdfShape\n```\n\n#### `shell()` — Hollow out, keeping only a shell of given thickness.\n\n```ts\nshell(thickness: number): SdfShape\n```\n\n#### `displace()` — Displace the surface by a function of position, or by a pattern SdfShape.\n\n```js\n// Function displacement\nshape.displace((x, y, z) => Math.sin(x) * 0.5)\n\n// Pattern displacement from a 3D SDF field\nshape.displace(sdf.knurl({ pitch: 2, depth: 0.3 }))\n```\n\n```ts\ndisplace(fn: ((x: number, y: number, z: number) => number) | SdfShape, constants?: Record<string, number>): SdfShape\n```\n\n#### `surfaceDisplace()` — Displace the surface using a 2D pattern in surface-local UV coordinates.\n\nAutomatically detects the shape's UV parametrization (sphere, cylinder, torus) from the SDF tree. Falls back to triplanar mapping for arbitrary shapes.\n\nUV coordinates are in **surface millimeters** — patterns defined with `spacing: 3` always produce 3mm spacing, regardless of shape size.\n\nPrefer `sdf.pattern2d()` or built-in surface patterns when the relief should stay on the native shader and meshing path. Callback functions are supported for experimentation, but they are opaque to the typed pattern optimizer.\n\n```js\n// Native typed pattern — auto-detects sphere UV\nconst p = sdf.pattern2d()\nconst ribs = p.stripes({ spacing: 3, width: 0.8, depth: 0.35 })\n .add(p.sineWave({ direction: [0, 1], wavelength: 14, amplitude: 0.08 }))\n\nsdf.sphere(27).shell(3)\n .surfaceDisplace(ribs)\n .toShape()\n\n// Custom 2D pattern via function\nshape.surfaceDisplace((u, v) => -Math.sin(u * 2) * 0.3)\n```\n\n```ts\nsurfaceDisplace(pattern: SurfacePattern | ((u: number, v: number) => number), options?: SurfaceDisplaceOptions): SdfShape\n```\n\n#### `onion()` — Create concentric onion layers.\n\n```ts\nonion(layers: number, thickness: number): SdfShape\n```\n\n---\n\n## Constants\n\n### `sdf`\n\nSDF modeling — signed distance field primitives, smooth booleans, TPMS lattices, domain warps, and surface patterns.\n\nReturn `SdfShape` values directly from a ForgeCAD script for native raymarch preview. Plain objects and arrays of SDF leaves are renderable too, so object keys become named preview parts.\n\nCall `.toShape()` or `toShape(...)` only when you need a mesh-backed ForgeCAD Shape for export, mesh booleans, or mixed SDF/manifold projects. All shapes live as a lazy expression tree until that materialization boundary.\n\nSDF is inherently implicit and sampled, not B-rep/exact geometry. Use it with caution when precision, tolerances, or exact export matter.\n\n```js\nreturn sdf.smoothUnion(sdf.sphere(10), sdf.box(15, 15, 15), { radius: 3 })\n .color('#4488cc');\n```\n\n```js\nreturn {\n shell: sdf.sphere(20).shell(2).color('#9be7ff'),\n core: sdf.gyroid({ cellSize: 6, wallThickness: 0.8 })\n .intersect(sdf.sphere(18))\n .color('#ffcf5a'),\n};\n```\n\n- `sphere(radius: number): SdfShape` — Create an SDF sphere centered at the origin.\n- `box(x: number, y: number, z: number): SdfShape` — Create an SDF box centered at the origin with given full dimensions (not half-extents).\n- `cylinder(height: number, radius: number): SdfShape` — Create an SDF cylinder centered at the origin, axis along Z.\n- `torus(majorRadius: number, minorRadius: number): SdfShape` — Create an SDF torus centered at the origin, lying in the XY plane.\n- `capsule(height: number, radius: number): SdfShape` — Create an SDF capsule centered at the origin, axis along Z.\n- `cone(height: number, radius: number): SdfShape` — Create an SDF cone with base at z=0 and tip at z=height.\n- `smoothUnion(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape` — Smooth union — blends shapes together with a smooth transition radius.\n- `smoothDifference(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape` — Smooth difference — smoothly subtracts b from a.\n- `smoothIntersection(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape` — Smooth intersection — smoothly intersects a and b.\n- `morph(a: SdfShape, b: SdfShape, t: number): SdfShape` — Morph between two SDF shapes. t=0 → a, t=1 → b.\n- `blend(a: SdfShape, b: SdfShape, fn: (x: number, y: number, z: number) => number, options?: BlendOptions): SdfShape` — Spatially blend between two SDF patterns. The blend function receives (x, y, z) and returns 0..1: 0 = fully pattern `a`, 1 = fully pattern `b`.\n- `gyroid(options: TpmsOptions): SdfShape` — Gyroid TPMS lattice — the most common lattice for additive manufacturing.\n- `schwarzP(options: TpmsOptions): SdfShape` — Schwarz-P TPMS lattice — isotropic pore structure.\n- `diamond(options: TpmsOptions): SdfShape` — Diamond TPMS lattice — stiffest TPMS structure.\n- `lidinoid(options: TpmsOptions): SdfShape` — Lidinoid TPMS lattice — visually distinct from gyroid, popular in research and art.\n- `tpmsBlock(options: TpmsBlockOptions): SdfShape` — TPMS block preset clipped to an explicit design space.\n- `withinBox(shape: SdfShape, options: { size: Vec3; }): SdfShape` — Clip an SDF shape to a box-shaped design space.\n- `noise(options?: NoiseOptions): SdfShape` — 3D Simplex noise field — produces organic, natural-looking displacements.\n- `voronoi(options?: VoronoiOptions): SdfShape` — 3D Voronoi pattern — organic cellular structures like bone, coral, or soap bubbles.\n- `honeycomb(options?: HoneycombOptions): SdfShape` — Honeycomb (hexagonal) lattice pattern. Intersect with your shape to apply.\n- `waves(options?: WavesOptions): SdfShape` — Sinusoidal wave ridges — parallel ridges along an axis.\n- `knurl(options?: KnurlOptions): SdfShape` — Knurl pattern — crossed helical grooves for grips and handles.\n- `perforated(options?: PerforatedOptions): SdfShape` — Perforated plate pattern — regular array of cylindrical holes.\n- `scales(options?: ScalesOptions): SdfShape` — Fish/dragon scale pattern — overlapping circular scales in hex-packed rows.\n- `brick(options?: BrickOptions): SdfShape` — Brick/stone wall pattern — running bond with mortar grooves.\n- `weave(options?: WeaveOptions): SdfShape` — Grid lattice pattern — two families of infinite slabs crossing at 90°.\n- `basketWeave(options?: BasketWeaveOptions): SurfacePattern` — Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`.\n- `pattern2d(): Pattern2DBuilder` — Create typed, composable 2D surface patterns for `.surfaceDisplace()`.\n- `twist(shape: SdfShape, degreesPerUnit: number): SdfShape` — Twist an SDF shape around the Z axis.\n- `bend(shape: SdfShape, radius: number): SdfShape` — Bend an SDF shape around the Z axis.\n- `repeat(shape: SdfShape, spacing: Vec3, count?: Vec3): SdfShape` — Repeat an SDF shape in space.\n- `circularArray(shape: SdfShape, count: number, offset?: number): SdfShape` — Arrange an SDF shape in a circular array around the Z axis with O(1) folded-domain evaluation.\n- `SurfacePattern: typeof SurfacePattern` — A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`.\n- `fromFunction(fn: SdfFunctionSource, options: SdfFunctionOptions): SdfShape` — Create a custom SDF from one expression; shader-safe expressions raymarch directly.\n- `Sculpt: { sphere: (radius: number) => SdfShape; box: (x: number, y: number, z: number, options?: SculptBoxOptions) => SdfShape; cylinder: (height: number, radius: number) => SdfShape; disk: (radius: number, thickness?: number) => SdfShape; circle: (radius: number, thickness?: number) => SdfShape; capsule: (height: number, radius: number) => SdfShape; torus: (majorRadius: number, minorRadius: number) => SdfShape; cone: (height: number, radius: number) => SdfShape; tube: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; curve: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; path: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; blend: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape; union: (first?: SculptBlendInput, ...rest: SculptBlendInput[]) => SdfShape; carve: (base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions) => SdfShape; keep: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape; polish: (shape: SdfShape, input?: SculptPolishInput) => SdfShape; material: (input?: SculptPolishInput) => ShapeMaterialProps & { color?: string; }; look: (preset?: SculptLookPreset) => SceneOptions; knownMaterials: typeof knownSculptMaterialPresets; }` — Sculpt-like facade: friendly liquid-modeling verbs backed by the same SDF kernel.\n\n### `Sculpt`\n\n- `sphere(radius: number): SdfShape` — Create a liquid SDF sphere centered at the origin.\n- `box(x: number, y: number, z: number, options?: SculptBoxOptions): SdfShape` — Create a liquid SDF box; pass `{ radius }` for a rounded box.\n- `cylinder(height: number, radius: number): SdfShape` — Create a liquid SDF cylinder centered at the origin, axis along Z.\n- `disk(radius: number, thickness?: number): SdfShape` — Create a thin circular disk centered at the origin, axis along Z. Useful as a circular cutter or insert.\n- `circle(radius: number, thickness?: number): SdfShape` — Alias for `Sculpt.disk()`.\n- `capsule(height: number, radius: number): SdfShape` — Create a liquid SDF capsule centered at the origin, axis along Z.\n- `torus(majorRadius: number, minorRadius: number): SdfShape` — Create a liquid SDF torus lying in the XY plane.\n- `cone(height: number, radius: number): SdfShape` — Create a liquid SDF cone.\n- `tube(points: SculptPointList, options?: SculptTubeOptions): SdfShape` — Create a smooth tube through a list of 3D points.\n- `curve(points: SculptPointList, options?: SculptTubeOptions): SdfShape` — Create a smooth variable-thickness sweep through 3D control points.\n- `path(points: SculptPointList, options?: SculptTubeOptions): SdfShape` — Alias for `Sculpt.tube()`; points may use [x, y, z, radius] for variable thickness.\n- `blend(first?: SculptBlendArg, optionsOrShape?: SculptBlendArg, ...rest: SculptBlendArg[]): SdfShape` — Smoothly blend one or more SDF shapes into a continuous body.\n- `union(first?: SculptBlendInput, ...rest: SculptBlendInput[]): SdfShape` — Sharply union one or more SDF shapes.\n- `carve(base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions): SdfShape` — Smoothly subtract one or more cutter shapes from a base shape.\n- `keep(first?: SculptBlendArg, optionsOrShape?: SculptBlendArg, ...rest: SculptBlendArg[]): SdfShape` — Smoothly intersect one or more SDF shapes.\n- `polish(shape: SdfShape, input?: SculptPolishInput): SdfShape` — Apply a Sculpt material preset or direct material properties.\n- `material(input?: SculptPolishInput): ShapeMaterialProps & { color?: string; }` — Resolve a Sculpt material preset to ForgeCAD material properties.\n- `look(preset?: SculptLookPreset): SceneOptions` — Return a polished scene preset tuned for liquid SDF preview.\n- `knownMaterials(): SculptMaterialPreset[]` — List the built-in Sculpt material preset names.\n";
104
+ const contextMd = "# ForgeCAD — AI Context (Chat UI)\n\n> **Usage:** Paste this file as context into your AI chat session (Claude.ai, ChatGPT, Gemini, etc.).\n> The AI will have full ForgeCAD API knowledge and will guide you through building models.\n>\n> **No CLI access in this session.** The AI cannot run commands directly. Instead, it will ask\n> you to run commands like `forgecad run <file>`\n> in your terminal and paste back the output for verification and iteration.\n\n## Workflow\n\n1. Tell the AI what you want to build and share any existing `.forge.js` files.\n2. The AI will write or edit model files for you.\n3. To validate, run `forgecad run <file>` in your terminal and paste the output.\n4. Iterate until the model looks right, then optionally `forgecad render 3d <file>` for a PNG.\n\n---\n\n## ForgeCAD API Reference\n\nAuthor or modify ForgeCAD models, sketches, assemblies, and CLI workflows.\nPrefer documented primitives, import rules, and placement strategies over inventing new APIs.\n\n### Model files\n\n- `.forge.js` — parametric part or assembly script; return a `Shape`, `Sketch`, `ShapeGroup`, `Assembly`, `SolvedAssembly`, array of renderables, or metadata object. Assemblies render directly; do not add `.toGroup()` unless you need `ShapeGroup` behavior.\n- Model the physical artifact, not an educational diagram. Do not add explanatory labels, arrows, legends, or text plaques unless the user explicitly asks for a presentation or teaching view. Product markings are allowed only when they would exist on the real object.\n- Build the real closed CAD first. Do not bake cutaways, sectioned shells, permanently exploded layouts, or hidden-parts views into the default model just to show internals. Use viewer-only cut planes, `explodeView`, object hiding, transparency, or `render inspect` section channels after the artifact exists.\n\n### Import and composition\n\n- Always include the extension in relative imports: `require(\"./file.forge.js\", { Param: value })` for model files and `require(\"./helpers.js\")` for plain helper modules. Do not write extensionless imports such as `require(\"./file\")`; ForgeCAD resolves project imports by exact path.\n- ForgeCAD APIs are injected globals in `.forge.js` files. Use `bom()`, `box()`, `scene()`, `Shape`, etc. directly; do not destructure those names from helpers with patterns like `const { bom } = require(\"./bom.js\")`. If a helper file is needed, import it under a project-specific name such as `const bomHelpers = require(\"./bom.js\")`.\n- `importSvgSketch()` for SVG files (file format loader, not a module import).\n- `.placeReference('bottom', [0,0,0])` to align any built-in anchor to a world coordinate; also works with custom `.withReferences()`.\n- Plain `.js` modules for shared helpers/constants (not model imports).\n\n### Validation commands (ask the user to run these)\n\n```\nforgecad run <file.forge.js> # geometry diagnostics\nforgecad render 3d <file.forge.js> # PNG render (shaded 3D)\nforgecad render wireframe <file.forge.js> # wireframe-only render\nforgecad render section <file.forge.js> --plane XZ # 2D cross-section (SVG/PNG)\nforgecad capture gif <file.forge.js> # animated orbit GIF\n```\n\n---\n\n<!-- skill-cli.md -->\n\n# ForgeCAD CLI for AI Workflows\n\nUse the CLI to validate, inspect, and export the model the AI is editing. Keep commands generic so they apply to the user's file, not a repo demo.\n\n## Validation Loop\n\n```bash\nforgecad run path/to/model.forge.js\nforgecad run path/to/model.forge.js --debug-imports\nforgecad run path/to/model.forge.js --backend occt\nforgecad check print path/to/model.forge.js --json\nforgecad check params path/to/model.forge.js --samples 12\n```\n\n- `forgecad run` prints geometry diagnostics, object summaries, collisions, verification results, and solver info.\n- `forgecad check print` reports collisions, mesh health, sampled walls, overhangs, and bed contact.\n- `forgecad check params` sweeps declared parameter ranges and reports crashes, degenerates, and new collisions.\n\n## Visual Checks\n\n```bash\nforgecad render 3d path/to/model.forge.js\nforgecad render 3d path/to/model.forge.js --camera front --camera iso\nforgecad render wireframe path/to/model.forge.js\nforgecad render section path/to/model.forge.js out/section.svg --plane XZ --offset 10\nforgecad capture gif path/to/model.forge.js\n```\n\n- Use `render 3d` for normal shaded verification.\n- Use `wireframe` or `section` when internal geometry or edge flow matters.\n- Use `capture gif` or `capture mp4` for motion and presentation.\n\n## Export\n\n```bash\nforgecad export stl path/to/model.forge.js\nforgecad export 3mf path/to/model.forge.js --quality high\nforgecad export step path/to/model.forge.js\nforgecad export report path/to/model.forge.js out/report.pdf\nforgecad export cutting-layout path/to/sheet-stock-model.forge.js --sheet-width 420 --sheet-height 594 --kerf 3\n```\n\nPick the export that matches the goal: mesh for printing, STEP for exact CAD interchange, report for review, cutting layout for sheet-stock workflows.\n\n\n---\n\n<!-- API/core/concepts.md -->\n\n# ForgeCAD Core Concepts\n\nForgeCAD scripts are JavaScript that returns geometry. The forge API is globally available — no imports needed.\n\n```javascript\nconst width = param(\"Width\", 50, { min: 20, max: 100, unit: \"mm\" });\nreturn box(width, 30, 10);\n```\n\n## Injected Runtime Names\n\nForgeCAD API functions and classes are injected into every `.forge.js` script. Use them directly; do not import or destructure ForgeCAD API names from helper files.\n\n```javascript\n// BAD — `bom` and `bomToCsv` are already built-in runtime names.\nconst { bom, bomToCsv } = require(\"./bom.js\");\n\n// GOOD — use the built-in directly.\nbom(4, \"M4 bolt\");\n\n// GOOD — keep project helpers under their own local name.\nconst bomHelpers = require(\"./bom.js\");\nbomHelpers.addFasteners(...);\n```\n\nTop-level declarations such as `const bom = ...`, `let scene = ...`, or `class Shape {}` collide with the injected runtime names. If you need a local helper, choose a project-specific name like `projectBom`, `sceneConfig`, or `makeShape`.\n\n## Execution Model\n\n- Scripts re-execute on every parameter change (400ms debounce)\n- Geometry operations are **immutable** — shapes, sketches, groups, imported assemblies, and wood boards return new values instead of modifying in place\n- Must return one of: `Shape`, `Sketch`, `ShapeGroup`, `Assembly`, `SolvedAssembly`, `SdfShape`, `Array` of renderables, `Array` of `{ name, tags?, shape?, sketch?, group?, color? }`, or a **metadata object** (see below)\n\nTop-level assembly scripts can return an unsolved `Assembly` directly; ForgeCAD solves it at default joint values for display. Return `assembly.solve(state)` when you want a specific pose. Do not call `.toGroup()` just to make an assembly render — use `.toGroup()` only when you specifically need `ShapeGroup` composition, group-style transforms, or named-child lookup.\n\n### Metadata Object Return\n\nA script can return a plain object whose values include renderable geometry alongside non-renderable metadata. All renderable entries (Shape, Sketch, ShapeGroup, Assembly, SolvedAssembly, SdfShape, or Array of named objects) are rendered; non-renderable entries are silently skipped. This is useful for multi-file projects where a part needs to publish interface data (bolt positions, dimensions) to other files:\n\nWhen importing project files, include the full extension in every relative path: `require('./motor-mount.forge.js')` for model files and `require('./helpers.js')` for plain helper modules. ForgeCAD resolves project imports by exact path and does not infer `.forge.js` or `.js` from `require('./motor-mount')`.\n\n```javascript\n// motor-mount.forge.js — renders standalone, exports metadata via require()\nconst holePositions = [[17, 15], [-29, 15], [17, -15], [-29, -15]];\nreturn {\n shape: mount.color('#556B2F'), // rendered\n bolts: { dia: 5.3, pos: holePositions }, // metadata — skipped in render, available via require()\n};\n\n// base-body.forge.js — imports mount, accesses .bolts\nconst mount = require('./motor-mount.forge.js');\nfor (const [x, y] of mount.bolts.pos) { ... } // use metadata\n// mount.shape is the Shape if you need it in an assembly\n```\n\nArrays inside the object are also rendered:\n\n```javascript\nreturn {\n parts: [{ name: 'Left', shape: leftShape }, { name: 'Right', shape: rightShape }],\n armWidth: 6, // metadata\n};\n```\n\nNamed return objects and named `group(...)` children can include `tags`. Tags are viewport metadata: they do not affect geometry, exports, face labels, or BOM rows, but the command palette can hide, show only, or focus every object with a selected tag.\n\n```javascript\nreturn [\n { name: 'Base Plate', tags: ['printed', 'structural'], shape: base },\n { name: 'M4 Bolt A', tags: 'fastener', shape: boltA },\n { name: 'M4 Bolt B', tags: 'fastener', shape: boltB },\n];\n```\n\n## Coordinate System\n\nZ-up right-handed: X = left/right, Y = forward/back, Z = up/down.\n\n## Colors\n\n`.color(hex)` works on `Shape` and `Sketch`. Colors survive transforms. Boolean operations return a single result shape, so only the first operand's color survives.\n\n**`union()` merges shapes into one solid mesh** — later operands do not keep separate colors or identities. Use `group(...)` or return named objects instead when you want separate parts:\n\n```javascript\nreturn [\n { name: \"Base\", shape: box(100, 100, 5), color: \"#888888\" },\n { name: \"Column\", shape: cylinder(50, 10).translate(50, 50, 5), color: \"#4488cc\" },\n];\n```\n\n## Face Operations\n\nShapes carry semantic face labels through their lifecycle. The flow is:\n\n1. **Primitives** assign canonical names — `box()` gives you `top`, `bottom`, `side-left`, etc.; `cylinder()` gives `top`, `bottom`, `side`.\n2. **Extrusions** inherit labels from the sketch and add `top`/`bottom`.\n3. **Transforms** (translate, rotate, scale, mirror) preserve all labels.\n4. **Booleans** preserve labels from the first operand where geometry survives.\n\nYou resolve labels to geometry with `.face(name)` or `.face(query)` — see the Shape class docs for the full query API. Operations like `.pocket()`, `.boss()`, `.hole()`, and `faceProfile()` all consume face references.\n\n## Text vs Viewport Labels\n\nDefault to no explanatory text inside CAD geometry. A ForgeCAD model should represent the physical artifact, not a labeled teaching diagram. Explain the design through file names, named return objects, comments, BOM entries, inspection bundles, and companion docs.\n\nUse `text2d()` only when the letters are part of the real object: raised branding, engraving, serial plates, keyboard legends, gauge ticks, connector labels, service arrows, scale markings, or exported manufacturing markings. `text2d()` builds filled sketch geometry from font outlines, so it can make exact/OCCT workflows slower.\n\nUse `Viewport.label(text, [x, y, z], options)` only for temporary review, debug, tutorial, or explicitly requested presentation views. Render labels are annotations only: they do not create meshes, do not export, do not enter the B-rep path, and do not add face labels. Do not use viewport labels to compensate for unclear geometry in the final model.\n\n## SDF Modeling\n\nFor organic shapes, smooth blending, TPMS lattices, and surface deformations. Return `SdfShape` values directly, or return a plain object/array tree of SDF leaves, for native raymarch preview. Use `.toShape()` or `toShape(...)` only when you need mesh-backed CAD/export behavior. See [sdf-primitives.md](sdf-primitives.md).\n\n---\n\n<!-- generated/core.md -->\n\n# Core API\n\n3D primitives, boolean operations, transforms, patterns, imports, and parameters.\n\n## Contents\n\n- [3D Primitives](#3d-primitives) — `box`, `cylinder`, `sphere`, `torus`\n- [Boolean Operations](#boolean-operations) — `union`, `difference`, `intersection`\n- [Edge Features](#edge-features) — `fillet`, `chamfer`, `draft`, `offsetSolid`\n- [Patterns & Layout](#patterns-layout) — `circularLayout`, `polygonVertices`, `linearPattern`, `circularPattern`, `linearPattern2d`, `circularPattern2d`, `mirrorCopy`, `selectEdges`, `selectEdge`, `coalesceEdges`\n- [Imports & Composition](#imports-composition) — `require`, `importSvgSketch`, `importMesh`, `importStep`\n- [Parameters](#parameters) — `Param.number`, `Param.string`, `Param.bool`, `Param.choice`, `Param.list`\n- [Grouping & Local Coordinates](#grouping-local-coordinates) — `group`\n- [Section & Projection](#section-projection) — `intersectWithPlane`, `faceProfile`, `projectToPlane`\n- [Transforms](#transforms) — `composeChain`\n- [Verification](#verification) — `verify.that`, `verify.equal`, `verify.notEqual`, `verify.greaterThan`, `verify.lessThan`, `verify.inRange`, `verify.centersCoincide`, `verify.connectorDistance`, `verify.physicalComponentCount`, `verify.intentionalOverlap`, `verify.notColliding`, `verify.minClearance`, `verify.clearanceBetween`, `verify.parallel`, `verify.perpendicular`, `verify.coplanar`, `verify.faceAt`, `verify.sameDirection`, `verify.isEmpty`, `verify.notEmpty`, `verify.volumeApprox`, `verify.areaApprox`, `verify.boundingBoxSize`, `verify.edgeContinuity`, `verify.noTinyEdges`, `verify.noSliverFaces`, `verify.noSelfIntersection`, `spec`\n- [Shape](#shape) — Appearance, Face Topology, Edge Topology, Transforms, Booleans & Cutting, Features, Placement, Connectors, References, Measurement\n- [Transform](#transform)\n- [ShapeGroup](#shapegroup) — Children, Transforms, Placement, Connectors, References\n- [SurfacePattern](#surfacepattern)\n- [Pattern2D](#pattern2d)\n- [Pattern2DBuilder](#pattern2dbuilder)\n- [ShapeRef](#shaperef)\n- [ANCHOR3D_NAMES](#anchor3d-names)\n- [verify](#verify)\n- [Constraint](#constraint)\n- [Points](#points)\n- [connector](#connector)\n\n## Functions\n\n### 3D Primitives\n\n#### `box()` — Create a rectangular box. Centered on XY, base at Z=0.\n\nExtents:\n\n- X: `[-width/2, width/2]`\n- Y: `[-depth/2, depth/2]`\n- Z: `[0, height]`\n\nFor named faces, build from a labeled sketch: `rect(width, depth).labelEdges('s', 'e', 'n', 'w').extrude(height, { labels: { start: 'bottom', end: 'top' } })`.\n\n```ts\nbox(width: number, depth: number, height: number): Shape\n```\n\n#### `cylinder()` — Create a cylinder or cone with named faces and edges. Centered on XY, base at Z=0.\n\nExtents:\n\n- X/Y: centered at the origin\n- Z: `[0, height]`\n\n`radiusTop` defaults to `radius`. Set `radiusTop` smaller to taper the side, or `0` for a pointy cone. Use `segments` to create regular prisms (for example `6` for a hexagonal prism).\n\nNamed faces: `top`, `bottom`, `side` Named edges: `top-rim`, `bottom-rim`\n\n```ts\ncylinder(height: number, radius: number, radiusTop?: number, segments?: number): Shape\n```\n\n#### `sphere()` — Create a sphere centered at the origin.\n\nExtents:\n\n- X: `[-radius, radius]`\n- Y: `[-radius, radius]`\n- Z: `[-radius, radius]`\n\nUse `segments` for lower-poly approximations.\n\n```ts\nsphere(radius: number, segments?: number): Shape\n```\n\n#### `torus()` — Create a torus (donut shape) lying in the XY plane. Centered on all axes.\n\nExtents:\n\n- X: `[-(majorRadius + minorRadius), +(majorRadius + minorRadius)]`\n- Y: `[-(majorRadius + minorRadius), +(majorRadius + minorRadius)]`\n- Z: `[-minorRadius, minorRadius]`\n\nThe origin is the center of the ring.\n\n```ts\ntorus(majorRadius: number, minorRadius: number, segments?: number): Shape\n```\n\n### Boolean Operations\n\n#### `union()` — Combine shapes into a single solid (additive boolean).\n\nAccepts individual shapes, or an array of shapes. `union()` returns one solid, so only the first operand's color is preserved in the result. Use `group()` when you want separate child colors or identities.\n\n```ts\nunion(...inputs: ShapeOperandInput[]): Shape\n```\n\n#### `difference()` — Subtract shapes from a base shape (subtractive boolean).\n\nThe first shape is the base; all subsequent shapes are subtracted from it. Accepts individual shapes, or an array of shapes.\n\n```ts\ndifference(...inputs: ShapeOperandInput[]): Shape\n```\n\n#### `intersection()` — Keep only the overlapping volume of the input shapes (intersection boolean).\n\nRequires at least two shapes. Accepts individual shapes, or an array.\n\n```ts\nintersection(...inputs: ShapeOperandInput[]): Shape\n```\n\n### Edge Features\n\n#### `fillet()` — Apply experimental fillets (rounded edges) to one or more edges of a shape.\n\n**Experimental**: fillets are still backend-sensitive. The Manifold backend is known to produce incorrect results for some edge-finish cases, and the OCCT backend can be very slow, especially with broad edge selections. Prefer targeted edge selectors and inspect the result before treating it as production-ready geometry.\n\nEdge selections compile into backend operations; unsupported selections fail as explicit kernel gaps instead of using TypeScript geometry fallbacks.\n\nThe `edges` parameter is flexible:\n\n- Omit to fillet **all** sharp edges\n- Pass an `EdgeQuery` for an inline filter (most common)\n- Pass an `EdgeSegment` or `EdgeSegment[]` from `selectEdges()` for pre-selected edges\n\nThrows if no edges match the selection, or if `radius` is not a positive finite number.\n\n```ts\n// Fillet all edges\nfillet(myShape, 2)\n\n// Fillet only top convex edges\nfillet(myShape, 1.5, { atZ: 20, convex: true })\n\n// Fillet vertical edges selected beforehand\nconst edges = selectEdges(myShape, { parallel: [0, 0, 1] })\nfillet(myShape, 3, edges)\n```\n\n```ts\nfillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape\n```\n\n#### `chamfer()` — Apply experimental chamfers (beveled edges) to one or more edges of a shape.\n\n**Experimental**: chamfers are still backend-sensitive. The Manifold backend is known to produce incorrect results for some edge-finish cases, and the OCCT backend can be very slow, especially with broad edge selections. Prefer targeted edge selectors and inspect the result before treating it as production-ready geometry.\n\nProduces a 45° bevel at the specified `size` (distance from edge). Edge selections compile into backend operations; unsupported selections fail as explicit kernel gaps instead of using TypeScript geometry fallbacks.\n\nThe `edges` parameter accepts the same options as `fillet()`: inline `EdgeQuery`, pre-selected `EdgeSegment`/`EdgeSegment[]`, or `undefined` (all sharp edges).\n\n```ts\n// Chamfer all edges\nchamfer(myShape, 1)\n\n// Chamfer only vertical edges\nchamfer(myShape, 2, { parallel: [0, 0, 1] })\n```\n\n```ts\nchamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape\n```\n\n#### `draft()` — Apply a draft angle (taper) to vertical faces for mold extraction.\n\nAdds a taper angle to the vertical faces of a solid so that it can be extracted from a mold. The neutral plane is the Z position where the draft angle is zero — faces above and below are tapered symmetrically. Typical values for injection molding are 1–5°.\n\nTruck supports vertical-prism solids with Z-axis pull directions. OCCT uses its native draft operation when available. Manifold throws.\n\n```ts\n// Add 3° draft to a box for injection molding\ndraft(myBox, 3)\n\n// Draft with custom pull direction and neutral plane\ndraft(myShape, 2, [0, 0, 1], 10)\n```\n\n```ts\ndraft(shape: Shape, angleDeg: number, pullDirection?: [ number, number, number ], neutralPlaneOffset?: number): Shape\n```\n\n#### `offsetSolid()` — Uniformly offset all surfaces of a solid inward or outward.\n\nUnlike `shell()`, which hollows a solid by removing one face, `offsetSolid()` produces a new solid whose every surface is shifted by `thickness`. Positive values grow the shape outward; negative values shrink it inward.\n\nRequires the OCCT backend. Throws on Manifold.\n\n```ts\n// Grow a box outward by 1mm on all sides\noffsetSolid(myBox, 1)\n\n// Shrink a shape inward by 0.5mm\noffsetSolid(myShape, -0.5)\n```\n\n```ts\noffsetSolid(shape: Shape, thickness: number): Shape\n```\n\n### Patterns & Layout\n\n#### `circularLayout()` — Compute evenly-spaced positions around a circle.\n\nEliminates the most common trig pattern in CAD scripts:\n\n```js\n// Before — manual trig\nfor (let i = 0; i < 12; i++) {\n const angle = i * 30 * Math.PI / 180;\n markers.push(marker.translate(r * Math.cos(angle), r * Math.sin(angle), 0));\n}\n\n// After — declarative\nfor (const {x, y} of circularLayout(12, r)) {\n markers.push(marker.translate(x, y, 0));\n}\n```\n\n```ts\ncircularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[]\n```\n\n**`CircularLayoutOptions`**\n- `startDeg?: number` — Angle of the first element in degrees (default: 0 = +X axis).\n- `centerX?: number` — Center X coordinate (default: 0).\n- `centerY?: number` — Center Y coordinate (default: 0).\n\n`LayoutPoint`: `{ x: number, y: number }`\n\n#### `polygonVertices()` — Compute the vertex positions of a regular polygon.\n\nDefault orientation places the first vertex at the top (90 degrees), matching the convention used by [`ngon()`](/docs/sketch#ngon).\n\nEliminates manual Math.sqrt(3) for triangles, pentagon vertex math, etc:\n\n```js\n// Before — manual equilateral triangle\nconst v1 = [center.x - r/2, center.y + r * Math.sqrt(3)/2];\nconst v2 = [center.x - r/2, center.y - r * Math.sqrt(3)/2];\nconst v3 = [center.x + r, center.y];\n\n// After — declarative\nconst [v1, v2, v3] = polygonVertices(3, r);\n```\n\n```ts\npolygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[]\n```\n\n**`PolygonVerticesOptions`**\n- `startDeg?: number` — Angle of the first vertex in degrees (default: 90 = top).\n- `centerX?: number` — Center X coordinate (default: 0).\n- `centerY?: number` — Center Y coordinate (default: 0).\n\n#### `linearPattern()` — Repeat a shape in a linear pattern along a direction vector and union the copies.\n\nCreates `count` copies of `shape`, each offset by `(dx*i, dy*i, dz*i)` from the original. All copies are unioned into a single `Shape`. Distinct compiler ownership is assigned to each copy so face identity via owner-scoped canonical queries still works post-merge.\n\n```ts\n// 5 cylinders, 20mm apart along X\nlinearPattern(cylinder(10, 3), 5, 20, 0)\n```\n\n```ts\nlinearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape\n```\n\n#### `circularPattern()` — Repeat a shape in a circular pattern around an axis and union the copies.\n\nDistributes `count` copies evenly around the rotation axis (360° / count per step). All copies are unioned into a single `Shape`. Distinct compiler ownership is assigned to each copy — post-merge face identity via owner-scoped canonical queries still works for pattern descendants.\n\nTwo calling conventions:\n\n- **Simple** (Z axis): `circularPattern(shape, 6)` or `circularPattern(shape, 6, centerX, centerY)`\n- **Advanced** (arbitrary axis): `circularPattern(shape, 6, { axis, origin })`\n\n```ts\n// 8 holes evenly spaced around origin\ncircularPattern(cylinder(12, 4).translate(30, 0, -1), 8)\n\n// Circular pattern around X axis\ncircularPattern(myFeature, 4, { axis: [1, 0, 0], origin: [0, 0, 50] })\n```\n\n```ts\ncircularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape\n```\n\n**`CircularPatternOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `centerX?` | `number` | Center X of the rotation (default: 0). Used when axis is Z (legacy mode). |\n| `centerY?` | `number` | Center Y of the rotation (default: 0). Used when axis is Z (legacy mode). |\n| `axis?` | `[ number, number, number ]` | Rotation axis direction (default: [0, 0, 1] = Z axis). |\n| `origin?` | `[ number, number, number ]` | Pivot point for the rotation (default: [0, 0, 0]). Overrides centerX/centerY when set. |\n\n#### `linearPattern2d()` — Repeat a 2D sketch in a linear pattern and union the copies.\n\n```ts\nlinearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch\n```\n\n#### `circularPattern2d()` — Repeat a 2D sketch in a circular pattern around a center point and union the copies.\n\n```ts\ncircularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | { centerX?: number; centerY?: number; startDeg?: number; }, centerY?: number): Sketch\n```\n\n#### `mirrorCopy()` — Mirror a shape across a plane and union the mirror with the original.\n\nThe mirror plane passes through the origin and is defined by its normal vector. The mirrored copy is unioned with the original to produce a single symmetric Shape.\n\n```ts\n// Mirror across the YZ plane (X=0)\nmirrorCopy(box(50, 30, 10), [1, 0, 0])\n```\n\n```ts\nmirrorCopy(shape: Shape, normal: [ number, number, number ]): Shape\n```\n\n#### `selectEdges()` — Select all edges from a shape that match the given query.\n\nUses the active kernel's native topology query when available (Truck), otherwise extracts sharp edges from the mesh (dihedral angle > 1°), applies all filters in the query, and returns the matching `EdgeSegment[]`. When `near` is specified the results are sorted closest-first.\n\nWorks on any shape — primitives, booleans, shells, and imported meshes. Use this when tracked topology is unavailable (e.g. after a difference or on imported geometry). For simpler cases, pass an `EdgeQuery` directly to `fillet()` or `chamfer()` instead of calling `selectEdges` separately.\n\n```ts\n// Fillet all top edges of a box\nconst topEdges = selectEdges(part, { atZ: 20, perpendicular: [0, 0, 1] });\nlet result = part;\nfor (const edge of coalesceEdges(topEdges)) {\n result = fillet(result, 2, edge);\n}\n```\n\n```ts\nselectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]\n```\n\n**`EdgeQuery`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `near?` | `Vec3` | Sort by proximity to this point (closest first). When used with `selectEdge`, picks the closest match. |\n| `parallel?` | `Vec3` | Filter: edge direction approximately parallel to this vector. |\n| `perpendicular?` | `Vec3` | Filter: edge direction approximately perpendicular to this vector. |\n| `convex?` | `boolean` | Filter: only convex (outside corner) edges. |\n| `concave?` | `boolean` | Filter: only concave (inside corner) edges. |\n| `minAngle?` | `number` | Filter: minimum dihedral angle in degrees. |\n| `maxAngle?` | `number` | Filter: maximum dihedral angle in degrees. |\n| `minLength?` | `number` | Filter: minimum edge length. |\n| `maxLength?` | `number` | Filter: maximum edge length. |\n| `within?` | `BoundingRegion` | Filter: edge midpoint must be within this bounding region. |\n| `atZ?` | `number` | Shorthand: edge midpoint Z ≈ this value (within `tolerance`). Equivalent to `within: { zMin: atZ - tol, zMax: atZ + tol }`. |\n| `tolerance?` | `number` | Position tolerance for approximate matches (default: `1.0`). Used by `atZ` and `near`. |\n| `angleTolerance?` | `number` | Angular tolerance in degrees for `parallel`/`perpendicular` filters (default: `10`). |\n\n`BoundingRegion`: `{ xMin?: number, xMax?: number, yMin?: number, yMax?: number, zMin?: number, zMax?: number }`\n\n**`EdgeSegment`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |\n| `direction` | `Vec3` | Normalized direction from start → end. |\n| `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |\n| `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |\n| `normalA` | `Vec3` | Normal of first adjacent face. |\n| `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |\n| `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |\n| `start`, `end`, `midpoint`, `length` | | — |\n\n#### `selectEdge()` — Select the single best-matching edge from a shape.\n\nWhen `near` is specified, returns the edge whose midpoint is closest to that point. Otherwise returns the first matching edge in mesh order. Throws if no edges match the query — useful as a guard when you expect exactly one result.\n\n```ts\n// Chamfer one specific edge near a known point\nconst bottomEdge = selectEdge(part, { near: [25, 0, 0], atZ: 0 });\nresult = chamfer(result, 1.5, bottomEdge);\n```\n\n```ts\nselectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment\n```\n\n#### `coalesceEdges()` — Merge collinear edge segments into longer logical edges.\n\nTessellation often splits one geometric edge into multiple short segments. `coalesceEdges` groups adjacent collinear segments and merges each group into a single `EdgeSegment` spanning the full extent. This is usually needed before passing edges to `fillet()` or `chamfer()` on non-primitive shapes.\n\nThe `tolerance` controls the maximum perpendicular distance from collinearity before two segments are considered non-collinear. Default: `0.01`.\n\n```ts\nconst topEdges = selectEdges(part, { atZ: 20 });\nfor (const edge of coalesceEdges(topEdges)) {\n result = fillet(result, 2, edge);\n}\n```\n\n```ts\ncoalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]\n```\n\n### Imports & Composition\n\n#### `require()` — Import a module with optional ForgeCAD parameter overrides. Returns the module's exports.\n\nWhen importing a `.forge.js` file, the return value is what the script returns. If the script returns a metadata object (e.g. `{ shape: myShape, bolts: {...} }`), the caller receives the full object — renderable values and metadata together.\n\n**Path rule:** Always include the file extension in relative imports: use `require(\"./part.forge.js\")` for model files and `require(\"./helpers.js\")` for plain helper modules. ForgeCAD does not apply Node-style extension inference, so `require(\"./part\")` will not find `part.forge.js` or `part.js`.\n\n**Parameter scoping:** Parameters declared in required files are automatically namespaced with a `\"filename#N / \"` prefix (e.g. `\"bracket.forge.js#1 / Width\"`). This prevents collisions when multiple files declare same-named params. Each file's params appear as separate sliders.\n\n**Parameter overrides:** When passing overrides, use the bare param name (not the scoped name). Overrides are type-checked — unrecognized keys throw an error with typo suggestions.\n\n**Multi-file assembly pattern** — pass cross-cutting design values from the assembly to parts:\n\n```js\n// assembly.forge.js — owns cross-cutting params, passes to parts\nconst wall = param(\"Wall\", 3);\nconst baseH = param(\"Base Height\", 20);\n\nconst mount = require('./motor-mount.forge.js', { Wall: wall });\nconst base = require('./base-body.forge.js', { Wall: wall, Height: baseH });\n```\n\n**Metadata pattern** — parts publish interface data alongside geometry:\n\n```js\n// motor-mount.forge.js\nreturn { shape: mount, bolts: { dia: 5.3, pos: holePositions } };\n\n// base-body.forge.js\nconst mount = require('./motor-mount.forge.js');\nmount.bolts.pos // access the metadata\nmount.shape // access the geometry\n```\n\n```ts\nrequire(path: string, paramOverrides?: Record<string, number | string>): any\n```\n\n#### `importSvgSketch()` — Parse an SVG file and return it as a Sketch with options for region filtering, scaling, and simplification.\n\n```ts\nimportSvgSketch(fileName: string, options?: SvgImportOptions): Sketch\n```\n\n**`SvgImportOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `include?` | `\"auto\" \\| \"fill\" \\| \"stroke\" \\| \"fill-and-stroke\"` | Which geometry channels to include: - `auto`: prefer fills; if no fill geometry exists, fall back to strokes - `fill`: import only filled regions - `stroke`: import only stroke geometry - `fill-and-stroke`: include both |\n| `regionSelection?` | `\"all\" \\| \"largest\"` | Keep all disconnected regions, or only the largest. |\n| `maxRegions?` | `number` | Keep at most this many regions (largest-first). |\n| `minRegionArea?` | `number` | Drop regions below this absolute area threshold. |\n| `minRegionAreaRatio?` | `number` | Drop regions below this ratio of largest-region area. |\n| `flattenTolerance?` | `number` | Curve flattening tolerance in SVG user units. Smaller = more segments, higher fidelity. |\n| `arcSegments?` | `number` | Minimum segment count for arc discretization. |\n| `scale?` | `number` | Global scale applied after SVG parsing. |\n| `maxWidth?` | `number` | Maximum imported sketch width. If exceeded, geometry is uniformly downscaled to fit. |\n| `maxHeight?` | `number` | Maximum imported sketch height. If exceeded, geometry is uniformly downscaled to fit. |\n| `centerOnOrigin?` | `boolean` | Recenter imported geometry so its 2D bounds center is at CAD origin. |\n| `simplify?` | `number` | Simplification tolerance for final sketch cleanup. |\n| `invertY?` | `boolean` | Flip SVG Y-down coordinates to CAD Y-up. Enabled by default. |\n\n#### `importMesh()` — Import an external mesh file (STL, OBJ, 3MF) as a Shape.\n\n```ts\nimportMesh(fileName: string, options?: { scale?: number; center?: boolean; }): Shape\n```\n\n#### `importStep()` — Import a STEP file (.step, .stp) as an exact OCCT-backed Shape. Preserves NURBS curves, B-spline surfaces, and exact topology. Requires running with the OCCT backend.\n\n```ts\nimportStep(fileName: string): Shape\n```\n\n### Parameters\n\n#### `Param.number()` — Declare a numeric parameter that renders as a slider in the UI.\n\nEach call registers a slider control. When the user moves the slider the entire script re-executes with the new value. Parameter values are also overridable from `require()` imports or the CLI `--param` flag — the `name` string is the key used in both cases.\n\nDefault range rules when options are omitted:\n\n- `min` defaults to `0`\n- `max` defaults to `defaultValue * 4`\n- `step` is auto-calculated: `1` for integer params, `0.1` for ranges ≤ 100, `1` for larger ranges\n\nThe `unit` option is cosmetic only — no conversion is performed. Use `integer: true` for counts, sides, quantities (rounds to whole numbers; step defaults to `1`).\n\n```ts\nconst width = Param.number(\"Width\", 50);\nconst angle = Param.number(\"Angle\", 45, { min: 0, max: 180, unit: \"°\" });\nconst sides = Param.number(\"Sides\", 6, { min: 3, max: 12, integer: true });\n```\n\n**Parameter overrides** — key must match `name` exactly:\n\n```ts\n// Via require()\nconst bracket = require(\"./bracket.forge.js\", { Width: 80 });\n\n// Via CLI\n// forgecad run model.forge.js --param \"Wall Thickness=3\"\n```\n\nAlso available as the shorthand alias `param()`.\n\n```ts\nParam.number(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number\n```\n\n#### `Param.string()` — Declare a string parameter that renders as a text input in the UI.\n\nString parameters let users type free-form text — labels, names, inscriptions, file paths, etc. The `name` string is the override key.\n\n```ts\nconst label = Param.string(\"Label\", \"Hello World\");\nconst name = Param.string(\"Name\", \"Part-001\", { maxLength: 20 });\n```\n\nOverride via import:\n\n```ts\nconst tag = require(\"./tag.forge.js\", { Label: \"Custom Text\" });\n```\n\nOnly available as `Param.string()` — no standalone alias.\n\n```ts\nParam.string(name: string, defaultValue: string, opts?: { maxLength?: number; }): string\n```\n\n#### `Param.bool()` — Declare a boolean parameter that renders as a checkbox in the UI.\n\nInternally stored as `0`/`1`. When overriding from CLI or `require()`, pass `1` for true and `0` for false. The `name` string is the override key.\n\n```ts\nconst showHoles = Param.bool(\"Show Holes\", true);\nif (showHoles) return difference(plate, cylinder(10, 5).translate(50, 30, 0));\nreturn plate;\n```\n\nOverride via import:\n\n```ts\nconst pan = require(\"./pan.forge.js\", { \"Show Lid\": 0 });\n```\n\nAlso available as the shorthand alias `boolParam()`.\n\n```ts\nParam.bool(name: string, defaultValue: boolean): boolean\n```\n\n#### `Param.choice()` — Declare a choice parameter that renders as a dropdown in the UI.\n\n`defaultValue` must exactly match one entry in `choices`. Returns the selected string label. Prefer `Param.choice` over `Param.number` when a slider would hide intent — named choices like `\"wok\"` are self-describing.\n\nOverrides may be passed as the choice label string (preferred) or as a numeric index. The `name` string is the override key.\n\n```ts\nconst panStyle = Param.choice(\"Pan Style\", \"frying-pan\", [\"frying-pan\", \"saute-pan\", \"wok\"]);\nif (panStyle === \"wok\") return buildWok();\n```\n\nOverride via import:\n\n```ts\nconst pan = require(\"./pan.forge.js\", { \"Pan Style\": \"wok\" });\n```\n\nOverride via CLI:\n\n```bash\nforgecad run model.forge.js --param \"Pan Style=wok\"\n```\n\nAlso available as the shorthand alias `choiceParam()`.\n\n```ts\nParam.choice(name: string, defaultValue: string, choices: string[]): string\n```\n\n#### `Param.list()` — Declare a list parameter — an array of struct items with per-field UI controls.\n\nEach item in the list is a struct whose fields each render as their own control (slider, checkbox, or dropdown). The user can add/remove rows up to `minItems`/`maxItems` bounds.\n\nField types:\n\n- Boolean fields (`boolean: true` in field defs) return as `boolean`\n- Choice fields (`choices: [...]` in field defs) return as `string`\n- All other fields return as `number`\n\n```ts\nParam.list<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: { ... }): T[]\n```\n\n`ListParamFieldDef`: `{ min?: number, max?: number, step?: number, unit?: string, integer?: boolean, boolean?: boolean, choices?: string[] }`\n\n### Grouping & Local Coordinates\n\n#### `group()` — Group multiple shapes/sketches for joint transforms without merging into a single mesh.\n\nUnlike union(), child colors and individual identities are preserved. Children can be plain shapes, named descriptors ({ name, shape/sketch/group }), or nested groups. The returned ShapeGroup supports all Shape transforms (translate, rotate, etc.).\n\nNamed descriptors can include `tags` for viewport organization. Tags do not affect geometry; they let the command palette hide, show only, or focus all objects with the same tag.\n\n**Local coordinate pattern:** Build child parts at the origin (local coordinates), then group and translate once to place the whole assembly. This eliminates the error-prone pattern of manually adding parent offsets to every sub-part.\n\n```js\nconst body = roundedBox(100, 20, 32, 4);\nconst panel = box(98, 2, 18).translate(0, -12, 4);\nconst louver = box(88, 2, 6).translate(0, -14, -11);\nconst indoorUnit = group(\n { name: 'Body', shape: body },\n { name: 'Panel', tags: 'cover', shape: panel },\n { name: 'Louver', tags: ['cover', 'moving'], shape: louver },\n).translate(0, -18, 70);\n```\n\n```ts\ngroup(...items: GroupInput[]): ShapeGroup\n```\n\n### Section & Projection\n\n#### `intersectWithPlane()` — Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch.\n\n```ts\nintersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch\n```\n\n#### `faceProfile()` — Extract the boundary profile of a named face as a 2D sketch.\n\nThe result is returned in the face's local 2D coordinate system, making it convenient for offsets, pocket profiles, or follow-up sketch operations driven by an existing face.\n\n```ts\nfaceProfile(shape: Shape, face: FaceSelector): Sketch\n```\n\n#### `projectToPlane()` — Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch.\n\n```ts\nprojectToPlane(shape: Shape, plane: PlaneSpec): Sketch\n```\n\n### Transforms\n\n#### `composeChain()` — Compose transforms in chain order. Equivalent to Transform.identity().mul(a).mul(b).mul(c)...\n\n```ts\ncomposeChain(...steps: TransformInput[]): Transform\n```\n\n### Verification\n\n#### `verify.that()` — Custom predicate check.\n\n```ts\nverify.that(label: string, check: () => boolean, message?: string): void\n```\n\n#### `verify.equal()` — Check that two numbers are approximately equal (within tolerance).\n\n```ts\nverify.equal(label: string, actual: number, expected: number, tolerance?: number, message?: string): void\n```\n\n#### `verify.notEqual()` — Check that two numbers are NOT equal (differ by more than tolerance).\n\n```ts\nverify.notEqual(label: string, actual: number, unexpected: number, tolerance?: number, message?: string): void\n```\n\n#### `verify.greaterThan()` — Check that actual > min.\n\n```ts\nverify.greaterThan(label: string, actual: number, min: number, message?: string): void\n```\n\n#### `verify.lessThan()` — Check that actual < max.\n\n```ts\nverify.lessThan(label: string, actual: number, max: number, message?: string): void\n```\n\n#### `verify.inRange()` — Check that min <= actual <= max.\n\n```ts\nverify.inRange(label: string, actual: number, min: number, max: number, message?: string): void\n```\n\n#### `verify.centersCoincide()` — Check that the bounding-box centers of two shapes coincide within tolerance (mm).\n\n```ts\nverify.centersCoincide(label: string, a: ShapeLike, b: ShapeLike, tolerance?: number): void\n```\n\n`ShapeLike`: `{ min: number[], max: number[] }`\n\n#### `verify.connectorDistance()` — Check the distance between two named connectors on a shape or group.\n\nUse this when connectors + `matchTo()` define a static assembly interface. It proves the mate at runtime, unlike a plain source-level connector declaration. The common case is `expected = 0`, meaning the two connector origins should coincide after placement.\n\n```ts\nverify.connectorDistance(\"leg is seated\", bench, \"Rail.leg_0\", \"Leg0.head\", 0, 0.01);\n```\n\n```ts\nverify.connectorDistance(label: string, target: ConnectorDistanceLike, connectorA: string, connectorB: string, expected?: number, tolerance?: number): void\n```\n\n#### `verify.physicalComponentCount()` — Declare the expected physical connectivity component count for the returned visible model.\n\nUse this for generated mechanical models that should have a clear component graph: one connected fixture, a purchased part plus a removable cartridge, a root assembly plus named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned visible objects with the same physical-connectivity analysis used in the quality gate and fails if the actual component count differs.\n\nThis catches the common generated-CAD failure where a script returns a visually plausible artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.\n\n```ts\nverify.physicalComponentCount(\"vise is one connected installed assembly\", 1);\n```\n\n```ts\nverify.physicalComponentCount(label: string, expected: number): void\n```\n\n#### `verify.intentionalOverlap()` — Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.\n\nUse this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume: welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers without pockets, or parts placed with collision as a positioning hack.\n\n`forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are returned as visible objects and the exact collision report finds that same object pair. Unused or non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.\n\n```ts\nverify.intentionalOverlap(\"rubber grip is overmolded on handle\", rubberGrip, handleCore, \"overmolded insert\");\n```\n\n```ts\nverify.intentionalOverlap(label: string, a: ShapeLike, b: ShapeLike, reason: string): void\n```\n\n#### `verify.notColliding()` — Check that two shapes do not collide (minGap > 0).\n\n```ts\nverify.notColliding(label: string, a: ShapeLike, b: ShapeLike, searchLength?: number): void\n```\n\n#### `verify.minClearance()` — Check that a minimum clearance gap exists between two shapes.\n\n```ts\nverify.minClearance(label: string, a: ShapeLike, b: ShapeLike, minGap: number, searchLength?: number): void\n```\n\n#### `verify.clearanceBetween()` — Check that the clearance gap between two shapes is inside an allowed range.\n\nUse this for seated and retained interfaces where a part must be close enough to be mechanically accountable, but must not collide beyond the allowed minimum. It catches both failure modes that make generated CAD look fake: parts floating away from their receiver, and parts intersecting their receiver because the pocket, bore, or running clearance was not modeled.\n\nFor contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny numerical noise. For a running fit, use the intended clearance band.\n\nManifold-backed shapes use exact min-gap distance. Other backends use a mesh-derived min-gap check and say so in the verification message; keep `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for positive-volume interference.\n\n```ts\nverify.clearanceBetween(\"cover is seated on gasket\", cover, gasket, -0.01, 0.05);\nverify.clearanceBetween(\"carriage runs inside rail\", carriage, rail, 0.2, 0.5);\n```\n\n```ts\nverify.clearanceBetween(label: string, a: ShapeLike, b: ShapeLike, minGap: number, maxGap: number, searchLength?: number): void\n```\n\n#### `verify.parallel()` — Check that two face normals are parallel (within toleranceDeg degrees).\n\n```ts\nverify.parallel(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void\n```\n\n`FaceRefLike`: `{ normal: [ number, number, number ], center: [ number, number, number ] }`\n\n#### `verify.perpendicular()` — Check that two face normals are perpendicular (within toleranceDeg degrees).\n\n```ts\nverify.perpendicular(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void\n```\n\n#### `verify.coplanar()` — Check that a face is coplanar with (same plane as) another face, meaning they are parallel AND their centers lie on the same plane.\n\n```ts\nverify.coplanar(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number, toleranceMm?: number): void\n```\n\n#### `verify.faceAt()` — Check that a face center lies at a specific position (within toleranceMm).\n\n```ts\nverify.faceAt(label: string, face: FaceRefLike, expectedPos: [ number, number, number ], toleranceMm?: number): void\n```\n\n#### `verify.sameDirection()` — Check that two face normals point in the same direction (not antiparallel). Stricter than parallel — both |angle| AND sign must match.\n\n```ts\nverify.sameDirection(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void\n```\n\n#### `verify.isEmpty()` — Check that a shape is empty.\n\n```ts\nverify.isEmpty(label: string, shape: ShapeLike, message?: string): void\n```\n\n#### `verify.notEmpty()` — Check that a shape is NOT empty.\n\n```ts\nverify.notEmpty(label: string, shape: ShapeLike, message?: string): void\n```\n\n#### `verify.volumeApprox()` — Check that a shape's volume is approximately equal to expected (mm³).\n\n```ts\nverify.volumeApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void\n```\n\n#### `verify.areaApprox()` — Check that a shape's surface area is approximately equal to expected (mm²).\n\n```ts\nverify.areaApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void\n```\n\n#### `verify.boundingBoxSize()` — Check that a shape's bounding box has approximately the given size.\n\n```ts\nverify.boundingBoxSize(label: string, shape: ShapeLike, expectedSize: [ number, number, number ], tolerance?: number): void\n```\n\n#### `verify.edgeContinuity()` — Check that every sampled seam on a shape meets a requested continuity threshold.\n\n```ts\nverify.edgeContinuity(label: string, shape: ShapeLike, options?: EdgeContinuityThresholds): void\n```\n\n**`EdgeContinuityThresholds`**: `continuity?: SurfaceContinuity`, `samples?: number`, `positionTolerance?: number`, `tangentToleranceDeg?: number`, `curvatureTolerance?: number`\n\n#### `verify.noTinyEdges()` — Check that a shape has no tiny edges below the requested threshold.\n\n```ts\nverify.noTinyEdges(label: string, shape: ShapeLike, threshold?: number): void\n```\n\n#### `verify.noSliverFaces()` — Check that a shape has no sliver faces below the requested score threshold.\n\n```ts\nverify.noSliverFaces(label: string, shape: ShapeLike, threshold?: number): void\n```\n\n#### `verify.noSelfIntersection()` — Best-effort exact-shape validity guard for self-intersections or broken B-Rep topology.\n\n```ts\nverify.noSelfIntersection(label: string, shape: ShapeLike): void\n```\n\n#### `spec()` — Create a named, reusable bundle of verification checks.\n\nA spec groups related `verify.*` calls under a collapsible header in the Checks panel. This makes large check suites scannable. Specs can be applied to multiple shapes and can check relationships between parts.\n\nSpecs can be defined in separate `.forge.js` files and imported via `require()` to share them across models.\n\n`spec.check()` returns a `SpecResult` — you can inspect it programmatically or ignore the return value and let the Checks panel show results.\n\n```ts\nconst printable = spec(\"Fits printer bed\", (shape) => {\n verify.notEmpty(\"Has geometry\", shape);\n const bb = shape.boundingBox();\n verify.lessThan(\"Width < 220mm\", bb.max[0] - bb.min[0], 220);\n verify.lessThan(\"Depth < 220mm\", bb.max[1] - bb.min[1], 220);\n verify.lessThan(\"Height < 250mm\", bb.max[2] - bb.min[2], 250);\n});\n\n// Reuse on multiple shapes\nprintable.check(bracket);\nprintable.check(standoff);\n\n// Check relationships between parts\nconst fitSpec = spec(\"Assembly fit\", (partA, partB) => {\n verify.notColliding(\"No interference\", partA, partB, 10);\n});\nfitSpec.check(bracket, standoff);\n```\n\n**Spec-first workflow:** Write specs before building geometry. Checks go from red to green as you build — effectively TDD for CAD.\n\n```ts\nspec(name: string, checkFn: (...args: any[]) => void): Spec\n```\n\n**`Spec`**\n- `name: string` — The display name of this spec\n\n---\n\n## Classes\n\n### `Shape`\n\nCore 3D solid shape. All operations are immutable and return new shapes.\n\nSupports transforms (translate, rotate, scale, mirror, transform, rotateAround, pointAlong), booleans (add, subtract, intersect), cutting (split, splitByPlane, trimByPlane), shelling, anchor positioning (attachTo, onFace), placement references, and queries (volume, surfaceArea, boundingBox, isEmpty, numTri, geometryInfo).\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `materialProps` | `ShapeMaterialProps | undefined` | — |\n\n**Appearance**\n\n#### `color()` — Set the color of this shape (hex string, e.g. \"#ff0000\"). Returns a new Shape with the color applied.\n\n```ts\ncolor(value: string | undefined): Shape\n```\n\n#### `material()` — Set PBR material properties for this shape's visual appearance.\n\nReturns a new Shape with the specified material properties merged on top of any previously set properties. All properties are optional — omitted keys retain their current value. Material properties survive transforms and boolean operations.\n\nUse `.color()` to set the base diffuse color; `.material()` controls how that color behaves under light (metalness, roughness, clearcoat) and can add emissive glow independent of lighting. Emissive glow pairs naturally with the `postProcessing.bloom` effect in [`scene()`](/docs/viewport#scene).\n\n```js\nbox(50, 50, 50).material({ metalness: 0.9, roughness: 0.1 }); // polished metal\nsphere(30).material({ emissive: '#ff6b35', emissiveIntensity: 2 }); // glowing\ncylinder(40, 20).material({ opacity: 0.4, clearcoat: 1.0, clearcoatRoughness: 0.02 }); // ice\n\n// Chainable with other shape methods\nbox(100, 100, 10).color('#gold').material({ metalness: 0.95, roughness: 0.05 }).translate(0, 0, 50);\n```\n\n```ts\nmaterial(props: ShapeMaterialProps): Shape\n```\n\n**Face Topology**\n\n#### `face()` — Resolve a face by user-authored label or compiler-owned name. Returns a `FaceRef` that can be passed to `.onFace()`, `projectToPlane()`, or used directly in placement.\n\n`.face(name)` is a pure label lookup — it finds faces by user-authored labels, not by geometric queries. Labels are born in sketches via `.label()` / `.labelEdges()` and grow into face names through extrude, loft, revolve, and sweep. They are stable references that travel with the geometry.\n\nLabels must be unique within a shape. Use `.prefixLabels()` before combining shapes with `union()` / `difference()` to avoid collisions. Collision detection throws a clear error with a fix suggestion.\n\nFor compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape's compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops) are resolved via coplanar triangle clustering.\n\n```ts\n// Edge labels become side face names after extrude\nconst profile = path()\n .moveTo(0, 0)\n .lineTo(100, 0).label('floor')\n .lineTo(100, 50).label('wall')\n .lineTo(0, 50).label('ceiling')\n .closeLabel('left-wall');\nconst room = profile.extrude(30, { labels: { start: 'base', end: 'top' } });\nroom.face('floor'); // side face from the labeled edge\nroom.face('base'); // base cap (user-specified)\n\n// .labelEdges() shorthand for sequential edge labeling\nconst plate = rect(100, 50).labelEdges('south', 'east', 'north', 'west');\nconst solid = plate.extrude(20, { labels: { start: 'bottom', end: 'top' } });\nsolid.face('south'); // side face\n\n// Prefix before combining to avoid collisions\nconst left = wing.prefixLabels('l/');\nconst right = wing.mirror([1, 0, 0]).prefixLabels('r/');\nconst full = union(left, right);\nfull.face('l/upper'); // left wing upper surface\n```\n\n```ts\nface(selector: FaceSelector): FaceRef\n```\n\n#### `faces()` — Return faces matching a query, or label semantic faces when passed a mapping.\n\nMapping form returns a new shape: `shape.faces({ lid: 'top', walls: ['front', 'back', 'left', 'right'] })`.\n\n```ts\nfaces(): FaceRef[]\n```\n\n#### `faceNames()` — List defined semantic face names currently available on this shape.\n\n```ts\nfaceNames(): string[]\n```\n\n#### `prefixLabels()` — Prefix all user-authored face labels, including semantic labels from `faces(mapping)`. Returns a new shape with modified labels.\n\n```ts\nprefixLabels(prefix: string): Shape\n```\n\n#### `renameLabel()` — Rename a single face label. Returns a new shape.\n\n```ts\nrenameLabel(from: string, to: string): Shape\n```\n\n#### `dropLabels()` — Remove specific face labels. Returns a new shape.\n\n```ts\ndropLabels(...names: string[]): Shape\n```\n\n#### `dropAllLabels()` — Remove all face labels. Returns a new shape.\n\n```ts\ndropAllLabels(): Shape\n```\n\n#### `faceHistory()` — Get the transformation history for a specific face.\n\n```ts\nfaceHistory(name: string): FaceTransformationHistory\n```\n\n**Edge Topology**\n\n#### `edge()` — Get a named topology edge. Only available on shapes with tracked topology (from box/cylinder/extrude).\n\n```ts\nedge(name: string): EdgeRef\n```\n\n#### `edgeNames()` — List named topology edge names. Returns empty array if shape has no tracked topology.\n\n```ts\nedgeNames(): string[]\n```\n\n#### `edgesOf()` — Return all boundary edges of a named face.\n\nFinds edges where one adjacent mesh face belongs to the target face and the other belongs to a different face. The result is coalesced (tessellation fragments merged) and can be passed directly to `fillet()` or `chamfer()`.\n\nThis is a topological query — no coordinates, no tolerances, no minimum-length hacks. It works because an edge is the boundary between two faces.\n\n```js\n// Fillet all top edges of a mounting plate\nlet plate = box(120, 80, 6).faces({ workSurface: 'top' })\nplate = fillet(plate, 3, plate.edgesOf('workSurface'))\n\n// Shelled enclosure — fillet the outer lip\nlet body = box(80, 50, 35).faces({ opening: 'top' })\nbody = body.shell(2, { openFaces: ['top'] })\nbody = fillet(body, 1.5, body.edgesOf('opening'))\n\n// Filter: only concave edges (after a boolean subtraction)\nbody.edgesOf('top', { concave: true })\n```\n\n```ts\nedgesOf(faceLabel: string, options?: EdgesOfOptions): EdgeSegment[]\n```\n\n#### `edgesBetween()` — Return edges shared between two named faces.\n\nAn edge is \"between\" faces A and B when one of its adjacent mesh triangles belongs to A and the other belongs to B. This is the most precise topological edge selection — \"fillet the edges where the top meets the wall.\"\n\nThe second argument can be a single face name or an array (edges between A and any of B1, B2, ...).\n\n```js\n// Fillet the edge where lid meets one wall\nlet body = box(100, 60, 30).faces({ lid: 'top', wall: 'side-left' })\nbody = fillet(body, 2, body.edgesBetween('lid', 'wall'))\n\n// Fillet a cylinder rim — where the flat cap meets the curved barrel\nlet tube = cylinder(30, 10).faces({ cap: 'top', barrel: 'side' })\ntube = fillet(tube, 1, tube.edgesBetween('cap', 'barrel'))\n\n// Multiple target faces at once\nbody.edgesBetween('lid', ['left-wall', 'right-wall', 'front-wall', 'back-wall'])\n```\n\n```ts\nedgesBetween(faceA: string, faceB: string | string[]): EdgeSegment[]\n```\n\n**Transforms**\n\n#### `translate()` — Move the shape relative to its current position. All transforms are immutable and return new shapes.\n\n```ts\ntranslate(x: number, y: number, z: number): Shape\n```\n\n#### `translatePolar()` — Translate using polar coordinates (radius + angle in degrees). Eliminates manual `r * Math.cos(angle * PI/180)` calculations.\n\nExample: `shape.translatePolar(50, 30)` moves 50mm at 30 degrees from +X.\n\n```ts\ntranslatePolar(radius: number, angleDeg: number, z?: number): Shape\n```\n\n#### `moveTo()` — Position the shape so its bounding box min corner is at the given global coordinate.\n\n```ts\nmoveTo(x: number, y: number, z: number): Shape\n```\n\n#### `moveToLocal()` — Position the shape relative to another shape's local coordinate system (bounding box min corner).\n\n```ts\nmoveToLocal(target: Shape | { toShape(): Shape; }, x: number, y: number, z: number): Shape\n```\n\n#### `rotate()` — Rotate around an arbitrary axis through the origin.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape\n```\n\n#### `rotateX()` — Rotate around the X axis by the given angle in degrees.\n\n```ts\nrotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape\n```\n\n#### `rotateY()` — Rotate around the Y axis by the given angle in degrees.\n\n```ts\nrotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape\n```\n\n#### `rotateZ()` — Rotate around the Z axis by the given angle in degrees.\n\n```ts\nrotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape\n```\n\n#### `rotateAroundTo()` — Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point. `movingPoint` / `targetPoint` may be raw world points or this shape's anchors/references.\n\n```ts\nrotateAroundTo(axis: [ number, number, number ], pivot: [ number, number, number ], movingPoint: RotationPointLike, targetPoint: RotationPointLike, options?: RotateAroundToOptions): Shape\n```\n\n#### `transform()` — Apply a 4x4 affine transform matrix (column-major) or a Transform object.\n\n```ts\ntransform(m: Mat4 | Transform): Shape\n```\n\n#### `scale()` — Scale the shape uniformly or per-axis from the shape's bounding box center. Accepts a single number or [x, y, z] array.\n\n```ts\nscale(v: number | [ number, number, number ]): Shape\n```\n\n#### `scaleAround()` — Scale the shape uniformly or per-axis from an explicit pivot point.\n\n```ts\nscaleAround(pivot: [ number, number, number ], v: number | [ number, number, number ]): Shape\n```\n\n#### `mirror()` — Mirror across a plane through the shape's bounding box center, defined by its normal vector.\n\n```ts\nmirror(normal: [ number, number, number ]): Shape\n```\n\n#### `mirrorThrough()` — Mirror across a plane through an explicit point, defined by its normal vector.\n\n```ts\nmirrorThrough(point: [ number, number, number ], normal: [ number, number, number ]): Shape\n```\n\n#### `pointAlong()` — Reorient a shape so its primary axis (Z) points along the given direction. Useful for laying cylinders/extrusions along X or Y without thinking about Euler angles. The shape's origin stays at [0,0,0] — translate after pointAlong to position it.\n\nExample: cylinder(40, 5).pointAlong([1, 0, 0]) — lays cylinder along X, starting at origin\n\n```ts\npointAlong(direction: [ number, number, number ]): Shape\n```\n\n**Booleans & Cutting**\n\n#### `add()` — Union this shape with others (additive boolean). Method form of union().\n\n```ts\nadd(...others: ShapeOperandInput[]): Shape\n```\n\n#### `subtract()` — Subtract other shapes from this one. Method form of difference().\n\n```ts\nsubtract(...others: ShapeOperandInput[]): Shape\n```\n\n#### `intersect()` — Keep only the overlap with other shapes. Method form of intersection().\n\n```ts\nintersect(...others: ShapeOperandInput[]): Shape\n```\n\n#### `split()` — Split into [inside, outside] by another shape.\n\n```ts\nsplit(cutter: Shape | { toShape(): Shape; }): [ Shape, Shape ]\n```\n\n#### `splitByPlane()` — Split by infinite plane. Returns [positive-side, negative-side].\n\n```ts\nsplitByPlane(normal: [ number, number, number ], originOffset?: number): [ Shape, Shape ]\n```\n\n#### `trimByPlane()` — Keep the positive side of the plane and discard the opposite side.\n\n```ts\ntrimByPlane(normal: [ number, number, number ], originOffset?: number): Shape\n```\n\n**Features**\n\n#### `shell()` — Hollow out compile-covered boxes, cylinders, and straight extrudes. `openFaces` names any subset of the base shape's labeled faces to leave open (no wall).\n\n```ts\nshell(thickness: number, opts?: { openFaces?: string[]; }): Shape\n```\n\n#### `pocket()` — Cut a pocket (cavity) into this solid through the named face.\n\n```js\nbox(100, 100, 20).pocket('top', 8)\nbox(100, 100, 20).pocket('top', 8, { inset: 5 })\nbox(100, 100, 20).pocket('top', 8, { scale: 0.8 })\n```\n\n```ts\npocket(face: FaceSelector, depth: number, opts?: PocketOptions): Shape\n```\n\n#### `boss()` — Add a boss (protrusion) from the named face.\n\n```js\nbox(100, 100, 20).boss('top', 5)\nbox(100, 100, 20).boss('top', 10, { scale: 0.6 })\n```\n\n```ts\nboss(face: FaceSelector, height: number, opts?: BossOptions): Shape\n```\n\n#### `hole()` — Drill a hole into this solid at a face.\n\n```js\nbox(50, 50, 20).hole('top', { diameter: 8, depth: 10 })\nbox(50, 50, 20).hole('top', { diameter: 6, counterbore: { diameter: 12, depth: 3 } })\n```\n\n```ts\nhole(faceOrRef: SketchFaceTarget | FaceRef, opts: ShapeHoleOptions): Shape\n```\n\n#### `cutout()` — Cut a profile-shaped pocket through a face using a placed sketch.\n\nThe sketch must be placed on a face with `Sketch.onFace(...)`. The cut follows the sketch's 2D profile.\n\n```js\nconst profile = circle2d(10).onFace(body, 'top');\nbody.cutout(profile, { depth: 5 })\n```\n\n```ts\ncutout(sketch: Sketch, opts?: ShapeCutoutOptions): Shape\n```\n\n**Placement**\n\n#### `placeReference()` — Translate the shape so the given anchor or reference lands on the target coordinate.\n\nAccepts any built-in anchor name (`'bottom'`, `'center'`, `'top-front-left'`, etc.) or a custom placement reference attached via `withReferences()`.\n\n```javascript\n// Ground a shape — put its bottom face center at Z = 0\nshape.placeReference('bottom', [0, 0, 0])\n\n// Center at the world origin\nshape.placeReference('center', [0, 0, 0])\n\n// Align left edge to X = 10\nshape.placeReference('left', [10, 0, 0])\n```\n\n```ts\nplaceReference(ref: PlacementAnchorLike, target: [ number, number, number ], offset?: [ number, number, number ]): Shape\n```\n\n#### `attachTo()` — Position this shape relative to another using named 3D anchor points.\n\nAnchors are bounding-box-relative: 'center', face centers ('top', 'front', ...), edge midpoints ('top-front', 'back-left', ...), and corners ('top-front-left', ...). Anchor word order is flexible: 'front-left' and 'left-front' are equivalent. Named placement references (from withReferences) can also be used as anchors.\n\n```ts\nattachTo(target: ShapeAnchorTarget, targetAnchor: PlacementAnchorLike, selfAnchor?: PlacementAnchorLike, offset?: [ number, number, number ]): Shape\n```\n\n#### `onFace()` — Place this shape on a face of a parent shape.\n\nThink of it like sticking a label on a box surface:\n\n- `face` picks which surface ('front', 'back', 'top', etc.)\n- `u, v` position within that face's 2D plane (from center)\n- front/back: u = left/right (X), v = up/down (Z)\n- left/right: u = forward/back (Y), v = up/down (Z)\n- top/bottom: u = left/right (X), v = forward/back (Y)\n- `protrude` = how far the child sticks out (positive = outward from face)\n\n```ts\nonFace(parent: ShapeAnchorTarget, face: \"front\" | \"back\" | \"left\" | \"right\" | \"top\" | \"bottom\", opts?: { u?: number; v?: number; protrude?: number; }): Shape\n```\n\n#### `seatInto()` — Slide this shape along an axis until a labeled face is embedded in the target body.\n\nPosition the shape roughly first (translate/rotate), then call seatInto to auto-adjust the penetration depth. No manual coordinate math needed.\n\n```js\n// Wing root embeds into fuselage — adapts to any fuselage shape\nwing.translate(0, wingY, 0).seatInto(fuselage, 'root');\n\n// Sensor pod sits flush on fuselage surface\npod.translate(0, station, radius + 20).seatInto(fuselage, 'base', { depth: 'flush' });\n\n// Antenna with 3mm gasket standoff\nmast.translate(0, station, radius + 50).seatInto(fuselage, 'mount', { depth: 'flush', gap: 3 });\n```\n\n```ts\nseatInto(target: Shape, surface: string, options?: SeatIntoOptions): Shape\n```\n\n#### `seatOver()` — Slide this shape until a target's labeled face is fully covered (inside this shape).\n\nThe inverse of `seatInto`: instead of embedding *your* face into the target, you move until the *target's* face is embedded inside you.\n\n```js\n// Nacelle moves up until pylon's bottom face is inside the nacelle\nnacelle.translate(rough).seatOver(pylon, 'bottom');\n\n// Cap slides down over a post until post's top face is covered\ncap.translate(rough).seatOver(post, 'top');\n```\n\n```ts\nseatOver(target: Shape, targetSurface: string, options?: SeatIntoOptions): Shape\n```\n\n**Connectors**\n\n#### `withConnectors()` — Attach named connectors — attachment points that survive transforms and imports. Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching).\n\n```ts\nwithConnectors(connectors: Record<string, ConnectorInput>): Shape\n```\n\n#### `connectorNames()` — List all connector names on this shape.\n\n```ts\nconnectorNames(): string[]\n```\n\n#### `connectorsByType()` — Get all connectors of a given type.\n\n```ts\nconnectorsByType(type: string): Array<{ name: string; port: ConnectorDef; }>\n```\n\n#### `connectorDistance()` — Distance between two connector origins on this shape.\n\n```ts\nconnectorDistance(nameA: string, nameB: string): number\n```\n\n#### `connectorMeasurements()` — Get measurements metadata from a connector.\n\n```ts\nconnectorMeasurements(name: string): Record<string, number | string>\n```\n\n#### `matchTo()` — Position this shape by matching connectors to a target.\n\nOverloads:\n\n- Single pair: `matchTo(target, selfConn, targetConn, options?)`\n- Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`\n- Multi-target: `matchTo([ [target1, selfConn1, targetConn1], ... ], options?)`\n\n```ts\nmatchTo(targetOrPairs: Shape | MatchTarget | Array<[ Shape | MatchTarget, string, string ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): Shape\n```\n\n**References**\n\n#### `withReferences()` — Attach named placement references that survive normal transforms and imports.\n\n```ts\nwithReferences(refs: PlacementReferenceInput): Shape\n```\n\n#### `referenceNames()` — List named placement references carried by this shape.\n\n```ts\nreferenceNames(kind?: PlacementReferenceKind): string[]\n```\n\n#### `referencePoint()` — Resolve a named placement reference or built-in anchor to a 3D point.\n\n```ts\nreferencePoint(ref: PlacementAnchorLike): [ number, number, number ]\n```\n\n**Measurement**\n\n#### `boundingBox()` — Get the axis-aligned bounding box as { min: [x,y,z], max: [x,y,z] }.\n\n```ts\nboundingBox(): ShapeRuntimeBounds\n```\n\n#### `volume()` — Volume in mm cubed.\n\n```ts\nvolume(): number\n```\n\n#### `surfaceArea()` — Surface area in mm squared.\n\n```ts\nsurfaceArea(): number\n```\n\n#### `isEmpty()` — True if the shape contains no geometry.\n\n```ts\nisEmpty(): boolean\n```\n\n#### `numBodies()` — Number of disconnected solid bodies in this shape.\n\n```ts\nnumBodies(): number\n```\n\n#### `numTri()` — Triangle count of the mesh representation.\n\n```ts\nnumTri(): number\n```\n\n**Other**\n\n#### `clone()` — Return a new Shape wrapper for explicit duplication in scripts.\n\n```ts\nclone(): Shape\n```\n\n#### `geometryInfo()` — Inspect which backend/representation produced this solid.\n\n```ts\ngeometryInfo(): GeometryInfo\n```\n\n#### `as()` — Name this shape as a reference namespace for diagnostics and future published refs.\n\n```ts\nas(name: string): Shape\n```\n\n#### `ref()` — Resolve a semantic reference path like `lid`, `lid/back`, or a midpoint selector on `lid/back`.\n\n```ts\nref(path: string): ShapeRef\n```\n\n#### `thicken()` — Offset-thicken an exact open surface or shell into a solid.\n\n```ts\nthicken(thickness: number): Shape\n```\n\n#### `getMesh()` — Extract triangle mesh for Three.js rendering\n\n```ts\ngetMesh(): ShapeRuntimeMesh\n```\n\n#### `slice()` — Slice the runtime solid by a plane normal to local Z at the given offset.\n\n```ts\nslice(offset?: number): any\n```\n\n#### `project()` — Orthographically project the runtime solid onto the local XY plane.\n\n```ts\nproject(): any\n```\n\n**Legacy Aliases**\n\n- `withPorts()` -> `withConnectors()`\n- `portNames()` -> `connectorNames()`\n\n### `Transform`\n\n#### `identity()` — Return the identity transform.\n\n```ts\nstatic identity(): Transform\n```\n\n#### `from()` — Wrap an existing `Transform` or raw 4x4 matrix as a `Transform`.\n\n```ts\nstatic from(input: TransformInput): Transform\n```\n\n#### `translation()` — Create a translation transform.\n\n```ts\nstatic translation(x: number, y: number, z: number): Transform\n```\n\n#### `scale()` — Create a uniform or per-axis scale transform.\n\n```ts\nstatic scale(v: number | Vec3): Transform\n```\n\n#### `rotationAxis()` — Create a rotation around an arbitrary axis, optionally about a pivot.\n\n```ts\nstatic rotationAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform\n```\n\n#### `rotateAroundTo()` — Solve the rotation needed to move one point onto a target line or plane.\n\n```ts\nstatic rotateAroundTo(axis: Vec3, pivot: Vec3, movingPoint: Vec3, targetPoint: Vec3, options?: RotateAroundToOptions): Transform\n```\n\n#### `mul()` — Compose transforms in chain order: `a.mul(b)` applies `a`, then `b`.\n\n```ts\nmul(other: TransformInput): Transform\n```\n\n#### `translate()` — Translate after the current transform.\n\n```ts\ntranslate(x: number, y: number, z: number): Transform\n```\n\n#### `rotateAxis()` — Rotate after the current transform.\n\n```ts\nrotateAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform\n```\n\n#### `inverse()` — Return the inverse transform.\n\n```ts\ninverse(): Transform\n```\n\n#### [`point()`](/docs/sketch#point) — Transform a point using homogeneous coordinates.\n\n```ts\npoint(p: Vec3): Vec3\n```\n\n#### `vector()` — Transform a direction vector without translation.\n\n```ts\nvector(v: Vec3): Vec3\n```\n\n#### `toArray()` — Return the transform as a raw 4x4 matrix array.\n\n```ts\ntoArray(): Mat4\n```\n\n### `ShapeGroup`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `children` | `GroupChild[]` | — |\n| `childNames` | `Array<string | undefined>` | — |\n\n**Children**\n\n#### `child()` — Return the named child by name. Throws if not found. Useful when importing a multipart group and working on components individually.\n\n```ts\nchild(name: string): GroupChild\n```\n\n#### `childName()` — Return the optional name of the child at `index`.\n\n```ts\nchildName(index: number): string | undefined\n```\n\n**Transforms**\n\n#### `translate()` — Move the entire group by (x, y, z). All children move together as a unit.\n\n```ts\ntranslate(x: number, y: number, z: number): ShapeGroup\n```\n\n#### `moveTo()` — Move the group so its bounding-box min corner lands at the given coordinate.\n\n```ts\nmoveTo(x: number, y: number, z: number): ShapeGroup\n```\n\n#### `moveToLocal()` — Move the group relative to another part's bounding-box min corner.\n\n```ts\nmoveToLocal(target: Shape | ShapeGroup, x: number, y: number, z: number): ShapeGroup\n```\n\n#### `rotate()` — Rotate the group around an arbitrary axis through the origin.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateX()` — Rotate the group around the X axis.\n\n```ts\nrotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateY()` — Rotate the group around the Y axis.\n\n```ts\nrotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateZ()` — Rotate the group around the Z axis.\n\n```ts\nrotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateAroundAxis()` — Rotate around an arbitrary axis, optionally through a pivot point.\n\n```ts\nrotateAroundAxis(axis: [ number, number, number ], angleDeg: number, pivot?: [ number, number, number ]): ShapeGroup\n```\n\n#### `rotateAroundTo()` — Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point. ShapeGroup string points use built-in anchors only.\n\n```ts\nrotateAroundTo(axis: [ number, number, number ], pivot: [ number, number, number ], movingPoint: Anchor3D | [ number, number, number ], targetPoint: Anchor3D | [ number, number, number ], options?: RotateAroundToOptions): ShapeGroup\n```\n\n#### `pointAlong()` — Reorient the group so its local Z axis points along `direction`.\n\n```ts\npointAlong(direction: [ number, number, number ]): ShapeGroup\n```\n\n#### `transform()` — Apply a 4x4 transform matrix or `Transform` to all 3D children.\n\n```ts\ntransform(m: Mat4 | Transform): ShapeGroup\n```\n\n#### `scale()` — Scale uniformly or per-axis from the group's bounding-box center.\n\n```ts\nscale(v: number | [ number, number, number ]): ShapeGroup\n```\n\n#### `scaleAround()` — Scale uniformly or per-axis from an explicit pivot point.\n\n```ts\nscaleAround(pivot: [ number, number, number ], v: number | [ number, number, number ]): ShapeGroup\n```\n\n#### `mirror()` — Mirror across a plane through the group's bounding-box center.\n\n```ts\nmirror(normal: [ number, number, number ]): ShapeGroup\n```\n\n#### `mirrorThrough()` — Mirror across a plane through an explicit point.\n\n```ts\nmirrorThrough(point: [ number, number, number ], normal: [ number, number, number ]): ShapeGroup\n```\n\n**Placement**\n\n#### `placeReference()` — Translate the group so the given anchor or reference lands on the target coordinate.\n\nAccepts any built-in anchor name (`'bottom'`, `'center'`, `'top-front-left'`, etc.) or a custom placement reference attached via `withReferences()`.\n\n```javascript\n// Ground a group — put its bottom at Z = 0\nassembly.placeReference('bottom', [0, 0, 0])\n\n// Use a custom reference from a multi-file part\nconst placed = require('./bracket-assembly.forge.js').group\n .placeReference('mountCenter', [0, 0, 50]);\n```\n\n```ts\nplaceReference(ref: PlacementAnchorLike, target: [ number, number, number ], offset?: [ number, number, number ]): ShapeGroup\n```\n\n#### `attachTo()` — Attach this group to a face or anchor on another part.\n\n`targetAnchor` can be a built-in anchor name or a custom reference name on the target. `selfAnchor` selects the anchor on this group to align.\n\n```ts\nattachTo(target: Shape | ShapeGroup, targetAnchor: Anchor3D | string, selfAnchor?: Anchor3D, offset?: [ number, number, number ]): ShapeGroup\n```\n\n#### `onFace()` — Place this group on a face of a parent shape. See Shape.onFace() for full documentation.\n\n```ts\nonFace(parent: Shape | ShapeGroup, face: \"front\" | \"back\" | \"left\" | \"right\" | \"top\" | \"bottom\", opts?: { u?: number; v?: number; protrude?: number; }): ShapeGroup\n```\n\n**Connectors**\n\n#### `withConnectors()` — Attach named connectors — attachment points that survive transforms. Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching).\n\n```ts\nwithConnectors(connectors: Record<string, ConnectorInput>): ShapeGroup\n```\n\n#### `connectorNames()` — List all connector names, including \"ChildName.connectorName\" from named children.\n\n```ts\nconnectorNames(): string[]\n```\n\n#### `connectorsByType()` — Get all connectors of a given type, including from named children.\n\n```ts\nconnectorsByType(type: string): Array<{ name: string; port: ConnectorDef; }>\n```\n\n#### `connectorDistance()` — Distance between two connector origins on this group (supports dotted child paths).\n\n```ts\nconnectorDistance(nameA: string, nameB: string): number\n```\n\n#### `connectorMeasurements()` — Get measurements metadata from a connector (supports dotted child paths).\n\n```ts\nconnectorMeasurements(name: string): Record<string, number | string>\n```\n\n#### `matchTo()` — Position this group by matching connectors to a target. Connector names support dotted paths into named children: \"ChildName.connectorName\".\n\nOverloads:\n\n- Single pair: `matchTo(target, selfConn, targetConn, options?)`\n- Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`\n- Multi-target: `matchTo([ [target1, selfConn1, targetConn1], ... ], options?)`\n\n```ts\nmatchTo(targetOrPairs: Shape | ShapeGroup | Array<[ Shape | ShapeGroup, string, string ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): ShapeGroup\n```\n\n**References**\n\n#### `withReferences()` — Attach named placement references to this group. References survive normal transforms (translate/rotate/scale/mirror/transform).\n\n```javascript\nconst bracket = group(\n { name: 'Left', shape: leftShape },\n { name: 'Right', shape: rightShape },\n).withReferences({\n points: { mountCenter: [0, 0, 0] },\n});\n```\n\n```ts\nwithReferences(refs: PlacementReferenceInput): ShapeGroup\n```\n\n#### `referenceNames()` — List named placement references carried by this group.\n\n```ts\nreferenceNames(kind?: PlacementReferenceKind): string[]\n```\n\n#### `referencePoint()` — Resolve a named placement reference or built-in Anchor3D to a 3D point. Named refs take priority over built-in anchors.\n\n```ts\nreferencePoint(ref: PlacementAnchorLike): [ number, number, number ]\n```\n\n**Other**\n\n#### `clone()` — Return a deep-cloned ShapeGroup tree (refs copied).\n\n```ts\nclone(): ShapeGroup\n```\n\n#### `boundingBox()` — Return the combined 3D bounding box of all children.\n\n```ts\nboundingBox(): { min: [ number, number, number ]; max: [ number, number, number ]; }\n```\n\n#### `color()` — Return a copy of the group with the given display color applied to each child.\n\n```ts\ncolor(hex: string): ShapeGroup\n```\n\n**Legacy Aliases**\n\n- `withPorts()` -> `withConnectors()`\n- `portNames()` -> `connectorNames()`\n\n### `SurfacePattern`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `body` | `string` | Function body: receives (u, v) in surface mm, returns height displacement. |\n| `constants` | `Record<string, number>` | Named constants injected into the function. |\n\n### `Pattern2D`\n\n#### `add()` — Add this pattern to one or more patterns or constant height offsets.\n\n```ts\nadd(...patterns: Pattern2DInput[]): Pattern2D\n```\n\n#### `subtract()` — Subtract another pattern or constant height offset from this pattern.\n\n```ts\nsubtract(pattern: Pattern2DInput): Pattern2D\n```\n\n#### `multiply()` — Multiply this pattern by one or more patterns or numeric scale factors.\n\n```ts\nmultiply(...patterns: Pattern2DInput[]): Pattern2D\n```\n\n#### `min()` — Keep the lower height between this pattern and one or more other patterns.\n\n```ts\nmin(...patterns: Pattern2DInput[]): Pattern2D\n```\n\n#### `max()` — Keep the higher height between this pattern and one or more other patterns.\n\n```ts\nmax(...patterns: Pattern2DInput[]): Pattern2D\n```\n\n#### `clamp()` — Limit pattern height to the inclusive `[min, max]` range in millimeters.\n\n```ts\nclamp(min: number, max: number): Pattern2D\n```\n\n#### `abs()` — Convert negative heights to positive heights.\n\n```ts\nabs(): Pattern2D\n```\n\n#### `negate()` — Flip the pattern height sign.\n\n```ts\nnegate(): Pattern2D\n```\n\n### `Pattern2DBuilder`\n\n#### `constant()` — Create a constant-height pattern in millimeters.\n\n```ts\nconstant(value?: number): Pattern2D\n```\n\n#### `sineWave()` — Create a sinusoidal wave pattern in UV space.\n\n```ts\nsineWave(options: Pattern2DSineWaveOptions): Pattern2D\n```\n\n#### `stripes()` — Create recessed stripe bands in UV space.\n\n```ts\nstripes(options: Pattern2DStripesOptions): Pattern2D\n```\n\n#### `overUnderWeave()` — Create an over-under woven relief pattern in UV space.\n\n```ts\noverUnderWeave(options: Pattern2DOverUnderWeaveOptions): Pattern2D\n```\n\n### `ShapeRef`\n\nA first-class reference path over a shape's semantic faces and face relationships.\n\nCreated with `shape.ref(\"lid/back\")`, then refined through methods such as `.point()` or `.edges()`. The reference stores intent as a readable path and resolves lazily against the current shape metadata.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `path` | `string` | — |\n\n**Methods:**\n\n#### `resolve()` — Resolve this reference into its current faces, edges, or points.\n\n```ts\nresolve(): ShapeReferenceResolution\n```\n\n#### `kind()` — The resolved reference kind, such as `face`, `edge-set`, or [`point`](/docs/sketch#point).\n\n```ts\nget kind(): ShapeReferenceKind\n```\n\n#### `cardinality()` — Whether the reference currently resolves to zero, one, or many matches.\n\n```ts\nget cardinality(): ShapeReferenceCardinality\n```\n\n#### `status()` — Return the reference lifecycle status for the current shape state.\n\n```ts\nstatus(): ShapeReferenceStatus\n```\n\n#### `explain()` — Return a human-readable explanation of how this reference resolved.\n\n```ts\nexplain(): string\n```\n\n#### `as()` — Name this derived reference so the same shape can resolve it by `shape.ref(name)`.\n\n```ts\nas(name: string): ShapeRef\n```\n\n#### `maybe()` — Return an optional reference that resolves to zero matches instead of throwing when missing.\n\n```ts\nmaybe(): ShapeRef\n```\n\n#### `all()` — Mark that a multi-match reference is intentionally being used as a set.\n\n```ts\nall(): ShapeRef\n```\n\n#### `one()` — Require this reference to resolve to exactly one match.\n\n```ts\none(): ShapeRef\n```\n\n#### `faces()` — Resolve this reference as one or more faces.\n\n```ts\nfaces(): FaceRef[]\n```\n\n#### `face()` — Resolve this reference as exactly one face.\n\n```ts\nface(): FaceRef\n```\n\n#### `edges()` — Resolve this reference as one or more edges. Face references return boundary edges.\n\n```ts\nedges(): EdgeSegment[]\n```\n\n#### `edge()` — Resolve this reference as exactly one edge.\n\n```ts\nedge(): EdgeSegment\n```\n\n#### `points()` — Resolve this reference as one or more points. Faces use centers and edges use midpoints.\n\n```ts\npoints(): Vec3[]\n```\n\n#### [`point()`](/docs/sketch#point) — Resolve this reference as exactly one point.\n\n```ts\npoint(): Vec3\n```\n\n#### `toJSON()` — Return the structured JSON-friendly reference resolution.\n\n```ts\ntoJSON(): ShapeReferenceResolution\n```\n\n#### `toString()` — Return a compact display form for this reference path.\n\n```ts\ntoString(): string\n```\n\n---\n\n## Constants\n\n### `ANCHOR3D_NAMES`\n\n### `verify`\n\n- `that(label: string, check: () => boolean, message?: string): void` — Custom predicate check.\n- `equal(label: string, actual: number, expected: number, tolerance?: number, message?: string): void` — Check that two numbers are approximately equal (within tolerance).\n- `notEqual(label: string, actual: number, unexpected: number, tolerance?: number, message?: string): void` — Check that two numbers are NOT equal (differ by more than tolerance).\n- `greaterThan(label: string, actual: number, min: number, message?: string): void` — Check that actual > min.\n- `lessThan(label: string, actual: number, max: number, message?: string): void` — Check that actual < max.\n- `inRange(label: string, actual: number, min: number, max: number, message?: string): void` — Check that min <= actual <= max.\n- `centersCoincide(label: string, a: ShapeLike, b: ShapeLike, tolerance?: number): void` — Check that the bounding-box centers of two shapes coincide within tolerance (mm).\n- `connectorDistance(label: string, target: ConnectorDistanceLike, connectorA: string, connectorB: string, expected?: number, tolerance?: number): void` — Check the distance between two named connectors on a shape or group. Use this when connectors + `matchTo()` define a static assembly interface. It proves the mate at runtime, unlike a plain source-level connector declaration. The common case is `expected = 0`, meaning the two connector origins should coincide after placement. **Example** ```ts verify.connectorDistance(\"leg is seated\", bench, \"Rail.leg_0\", \"Leg0.head\", 0, 0.01); ```\n- `physicalComponentCount(label: string, expected: number): void` — Declare the expected physical connectivity component count for the returned visible model. **Details** Use this for generated mechanical models that should have a clear component graph: one connected fixture, a purchased part plus a removable cartridge, a root assembly plus named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned visible objects with the same physical-connectivity analysis used in the quality gate and fails if the actual component count differs. This catches the common generated-CAD failure where a script returns a visually plausible artifact but the handle, screw, washer, cover, or terminal block is actually a separate island. **Example** ```ts verify.physicalComponentCount(\"vise is one connected installed assembly\", 1); ```\n- `intentionalOverlap(label: string, a: ShapeLike, b: ShapeLike, reason: string): void` — Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent. **Details** Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume: welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers without pockets, or parts placed with collision as a positioning hack. `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are returned as visible objects and the exact collision report finds that same object pair. Unused or non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions. **Example** ```ts verify.intentionalOverlap(\"rubber grip is overmolded on handle\", rubberGrip, handleCore, \"overmolded insert\"); ```\n- `notColliding(label: string, a: ShapeLike, b: ShapeLike, searchLength?: number): void` — Check that two shapes do not collide (minGap > 0).\n- `minClearance(label: string, a: ShapeLike, b: ShapeLike, minGap: number, searchLength?: number): void` — Check that a minimum clearance gap exists between two shapes.\n- `clearanceBetween(label: string, a: ShapeLike, b: ShapeLike, minGap: number, maxGap: number, searchLength?: number): void` — Check that the clearance gap between two shapes is inside an allowed range. **Details** Use this for seated and retained interfaces where a part must be close enough to be mechanically accountable, but must not collide beyond the allowed minimum. It catches both failure modes that make generated CAD look fake: parts floating away from their receiver, and parts intersecting their receiver because the pocket, bore, or running clearance was not modeled. For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny numerical noise. For a running fit, use the intended clearance band. Manifold-backed shapes use exact min-gap distance. Other backends use a mesh-derived min-gap check and say so in the verification message; keep `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for positive-volume interference. **Example** ```ts verify.clearanceBetween(\"cover is seated on gasket\", cover, gasket, -0.01, 0.05); verify.clearanceBetween(\"carriage runs inside rail\", carriage, rail, 0.2, 0.5); ```\n- `parallel(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals are parallel (within toleranceDeg degrees).\n- `perpendicular(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals are perpendicular (within toleranceDeg degrees).\n- `coplanar(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number, toleranceMm?: number): void` — Check that a face is coplanar with (same plane as) another face, meaning they are parallel AND their centers lie on the same plane.\n- `faceAt(label: string, face: FaceRefLike, expectedPos: [ number, number, number ], toleranceMm?: number): void` — Check that a face center lies at a specific position (within toleranceMm).\n- `sameDirection(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals point in the same direction (not antiparallel). Stricter than parallel — both |angle| AND sign must match.\n- `isEmpty(label: string, shape: ShapeLike, message?: string): void` — Check that a shape is empty.\n- `notEmpty(label: string, shape: ShapeLike, message?: string): void` — Check that a shape is NOT empty.\n- `volumeApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void` — Check that a shape's volume is approximately equal to expected (mm³).\n- `areaApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void` — Check that a shape's surface area is approximately equal to expected (mm²).\n- `boundingBoxSize(label: string, shape: ShapeLike, expectedSize: [ number, number, number ], tolerance?: number): void` — Check that a shape's bounding box has approximately the given size.\n- `edgeContinuity(label: string, shape: ShapeLike, options?: EdgeContinuityThresholds): void` — Check that every sampled seam on a shape meets a requested continuity threshold.\n- `noTinyEdges(label: string, shape: ShapeLike, threshold?: number): void` — Check that a shape has no tiny edges below the requested threshold.\n- `noSliverFaces(label: string, shape: ShapeLike, threshold?: number): void` — Check that a shape has no sliver faces below the requested score threshold.\n- `noSelfIntersection(label: string, shape: ShapeLike): void` — Best-effort exact-shape validity guard for self-intersections or broken B-Rep topology.\n\n### `Constraint`\n\n- `makeParallel(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to be parallel.\n- `enforceAngle(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg, angleDeg: number): ConstrainedSketchBuilder` — Constrain the signed angle from line `a` to line `b`.\n- `horizontal(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder` — Constrain a line to be horizontal.\n- `vertical(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder` — Constrain a line to be vertical.\n- `equalLength(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to have equal length.\n- `distance(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg, value: number): ConstrainedSketchBuilder` — Constrain the distance between two points.\n- `fix(builder: ConstrainedSketchBuilder, pt: PointArg, x: number, y: number): ConstrainedSketchBuilder` — Fix a point at a specific coordinate.\n- `coincident(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg): ConstrainedSketchBuilder` — Constrain two points to occupy the same location.\n- `perpendicular(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to be perpendicular.\n- `length(builder: ConstrainedSketchBuilder, line: LineArg, value: number): ConstrainedSketchBuilder` — Constrain the length of a line.\n\n### `Points`\n\n- `distance(a: Vec3, b: Vec3): number` — Euclidean distance between two 3D points.\n- `midpoint(a: Vec3, b: Vec3): Vec3` — Center point between two 3D points.\n- `lerp(a: Vec3, b: Vec3, t: number): Vec3` — Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b.\n- `direction(a: Vec3, b: Vec3): Vec3` — Unit direction vector from a to b. Throws if a and b are the same point.\n- `offset(point: Vec3, dir: Vec3, amount: number): Vec3` — Move a point along a direction vector by a given amount.\n- `polar(length: number, angleDeg: number, from?: [ number, number ]): [ number, number ]` — Compute a 2D point at distance and angle (degrees) from an optional origin.\n\n### `connector`\n\nConnector factory. Create attachment points: `connector({...})`, `connector.male(type, {...})`, etc.\n\n---\n\n<!-- guides/coordinate-system.md -->\n\n# Coordinate System Convention\n\nForgeCAD uses a **Z-up** right-handed coordinate system.\n\n## Axes\n\n| Axis | Direction | Positive |\n|------|-----------------|----------|\n| X | Left / Right | Right |\n| Y | Forward / Back | Forward |\n| Z | Up / Down | Up |\n\n## Standard Views\n\n| View | Camera position direction | Sees plane |\n|--------|--------------------------|------------|\n| Front | −Y | XZ |\n| Back | +Y | XZ |\n| Right | +X | YZ |\n| Left | −X | YZ |\n| Top | +Z | XY |\n| Bottom | −Z | XY |\n\n## GizmoViewcube Face Mapping\n\nThree.js BoxGeometry material indices vs ForgeCAD labels (Z-up remapping):\n\n| Index | Three.js direction | ForgeCAD label |\n|-------|--------------------|----------------|\n| 0 | +X | Right |\n| 1 | −X | Left |\n| 2 | +Y | Front |\n| 3 | −Y | Back |\n| 4 | +Z | Top |\n| 5 | −Z | Bottom |\n\nDefault drei labels are Y-up; ForgeCAD passes `faces={['Right','Left','Front','Back','Top','Bottom']}`.\n\n## Grid\n\nThe ground plane is XY (Z = 0). Extrusion goes along +Z. Manifold is Y-up internally — if a kernel-facing operation behaves as if axes are swapped, check for Manifold Y-up semantics leaking through.\n\n---\n\n<!-- guides/geometry-conventions.md -->\n\n# Geometry Conventions\n\nForgeCAD wraps Manifold (mesh kernel) and Three.js (Y-up renderer). This doc captures convention mismatches and how ForgeCAD resolves them.\n\n## Winding Order\n\nCCW = positive area, CW = empty in Manifold's `CrossSection`. ForgeCAD auto-fixes at all entry points:\n- `polygon(points)` — computes signed area (shoelace), reverses if CW\n- `path().close()` — same fix\n\n**Rule for new code:** Any function accepting user point arrays that creates a `CrossSection` MUST auto-fix winding.\n\n## Coordinate System (Z-up vs Y-up)\n\nThree.js is Y-up; ForgeCAD is Z-up. Fix applied at camera level (`camera.up = (0,0,1)`) — geometry coordinates are native Z-up. Never swap Y/Z in geometry.\n\n## Revolution Axis\n\n`CrossSection.revolve()` revolves around Y. Profile X = radial distance, Profile Y = height (becomes Z after revolution). Profile must be at X > 0.\n\n## Boolean Winding (3D)\n\nManifold requires consistent outward face normals. ForgeCAD only creates meshes through Manifold's own constructors, which guarantee correct normals.\n\n## Transform Order\n\nTransforms apply left-to-right. `Sketch.rotate()`, `scale()`, and `mirror()` operate around bounding-box center. For 3D `Shape` / `ShapeGroup`, `scale()` and `mirror()` operate around bounding-box center, while `rotate()` remains origin-based unless you pass `options.pivot` or use `rotateAroundAxis(...)`.\n\nFor explicit transform objects: `A.mul(B)` = apply A then B; `composeChain(A, B, C)` = A→B→C.\n\n## Assembly Frame Composition\n\n```ts\nchildWorld = composeChain(childBase, jointMotion, jointFrame, parentWorld)\n```\n\nPrefer `composeChain(...)` over manual `.mul(...).mul(...)` in kinematics code to avoid order mistakes.\n\n## Summary\n\n| Convention | User sees | Kernel needs | Where we fix it |\n|---|---|---|---|\n| Winding | Any point order | CCW | `polygon()`, `path().close()` |\n| Up axis | Z-up | Y-up (Three.js) | `camera.up`, gizmo labels |\n| Revolution | \"revolve this profile\" | Profile in X-Y, X>0 | Documented only |\n| Face normals | Doesn't think about it | Outward-pointing | Manifold constructors |\n| Transform order | Left-to-right chain | Post-multiply | Native match |\n\n---\n\n<!-- guides/positioning.md -->\n\n# Positioning Strategy\n\n## Rule 0: if parts should touch, use connectors first\n\nFor any fixed assembly where parts are meant to stay in contact in the final model, start with connectors + `matchTo()`. This applies to furniture, fixtures, toys, enclosures, sleds, and any other static multi-part object, not only mechanisms.\n\nUse raw `translate()` and `rotate()` when parts are intentionally free-floating or when you are doing quick exploratory layout. Use `attachTo()` for rough bounding-box placement. But if the relationship is a real interface, make it explicit with connectors.\n\n## Primitive origin convention\n\nAll 3D primitives are **centered on XY, base at Z=0**:\n\n| Primitive | X range | Y range | Z range |\n|-----------|---------|---------|---------|\n| `box(60, 40, 20)` | [-30, 30] | [-20, 20] | [0, 20] |\n| `cylinder(50, 10)` | [-10, 10] | [-10, 10] | [0, 50] |\n| `sphere(15)` | [-15, 15] | [-15, 15] | [-15, 15] |\n| `torus(20, 5)` | [-25, 25] | [-25, 25] | [-5, 5] |\n\nSphere and torus are fully centered (symmetric in Z). Box and cylinder sit on the XY ground plane — **Z goes up from zero, never negative**.\n\nThis means `box(w, d, h).translate(0, 0, -h / 2)` is the manual way to \"center on Z\" — it moves the box from `[0, h]` to `[-h / 2, h / 2]`. Prefer `box(w, d, h).placeReference('center', [0, 0, 0])` when you want full XYZ centering.\n\nDo not assume `center: true` or a positional `true` gives OpenSCAD-style full XYZ centering. Primitive placement is fixed unless the primitive docs explicitly say otherwise.\n\n---\n\nMost positioning bugs come from manual coordinate arithmetic. Use these methods in priority order.\n\n## 1. Connectors + `matchTo()` — default for mating interfaces\n\nDefine connectors on parts; `matchTo()` provides automatic 6-DOF alignment. The child translates and rotates so its connector aligns with the target's — origins coincide, axes oppose (plug-in model).\n\n```javascript\nconst shelf = box(200, 120, 10).translate(0, 0, -5).withConnectors({\n left_tab: connector.male(\"dovetail\", { origin: [-100, 0, 0], axis: [-1, 0, 0] }),\n});\nconst panel = box(12, 120, 200).translate(0, 0, -100).withConnectors({\n shelf_0: connector.female(\"dovetail\", { origin: [6, 0, -50], axis: [1, 0, 0] }),\n});\nconst placed = shelf.matchTo(panel, \"left_tab\", \"shelf_0\");\n// Dictionary form for multiple pairs on same target:\nconst placed2 = shelf.matchTo(panel, { left_tab: \"shelf_0\" });\n// Named group children bubble connectors via dotted paths:\nconst cabinet = group({ name: \"Left\", shape: panel });\nshelf.matchTo(cabinet, \"left_tab\", \"Left.shelf_0\");\n```\n\n**Why connectors first:** stable (don't shift on fillet/chamfer/boolean), semantic (carry type/gender), oriented (full frame), queryable (`shape.connectorDistance('a','b')`), explode-aware.\n\nFor a non-mechanism fixed-assembly example, see `examples/api/static-assembly-connectors.forge.js`.\n\n## 2. `group()` — local coordinates for multi-part assemblies\n\nThe most common positioning bug: manually adding a parent's global offset to every sub-part. One wrong sign or forgotten variable and parts float into space. **Use `group()` to build parts in local coordinates (at the origin), then position the group once.**\n\n```javascript\n// BAD — every sub-part repeats the parent's global position\nconst unitY = -18, unitZ = 70;\nconst body = lib.roundedBox(100, 20, 32, 4).translate(0, unitY, unitZ);\nconst panel = box(98, 2, 18).translate(0, unitY - 12, unitZ + 4);\nconst louver = box(88, 2, 6).translate(0, unitY - 14, unitZ - 11);\nconst led = sphere(1.2).translate(35, unitY - 12, unitZ + 9);\n\n// GOOD — build at local origin, group, translate once\nconst body = lib.roundedBox(100, 20, 32, 4);\nconst panel = box(98, 2, 18).translate(0, -12, 4); // relative to local origin\nconst louver = box(88, 2, 6).translate(0, -14, -11); // relative to local origin\nconst led = sphere(1.2).translate(35, -12, 9); // relative to local origin\nconst indoorUnit = group(\n { name: 'Body', shape: body },\n { name: 'Panel', shape: panel },\n { name: 'Louver', shape: louver },\n { name: 'LED', shape: led },\n).translate(0, -18, 70); // ONE translate for the whole assembly\n```\n\n**Groups nest.** Build sub-assemblies as groups, then group those into larger assemblies — each level has its own local origin.\n\n```javascript\nconst fan = group(hub, ...blades).translate(0, 25, 0); // fan assembly\nconst outdoorUnit = group(\n { name: 'Body', shape: casing },\n { name: 'Fan', shape: fan }, // already a group\n { name: 'Grille', shape: grille },\n).translate(0, 23, -42); // position the whole outdoor unit\n```\n\n**When to use something else:** `group()` preserves individual shapes — you can't boolean (subtract/intersect) a group. If a sub-part needs a boolean with the parent body, do that boolean first in local coordinates, then group the result.\n\n## 3. `pointAlong()` — orient cylinders before positioning\n\n```javascript\n// BAD\nconst pipe = cylinder(100, 5).rotateX(90).translate(x, y, z);\n// GOOD — reads as \"pipe pointing along Y\"\nconst pipe = cylinder(100, 5).pointAlong([0, 1, 0]).translate(x, y, z);\n```\n\n**Always call `pointAlong()` BEFORE `matchTo()` or `translate()`** — it reorients around the origin.\n\n## 4. `attachTo()` — quick bounding-box positioning\n\n```javascript\nconst column = cylinder(50, 8).attachTo(base, 'top', 'bottom');\n```\n\n`child.attachTo(parent, parentAnchor, selfAnchor, offset)`. Anchor points shift on fillet/chamfer/boolean — fragile for assembly interfaces, fine for quick prototyping.\n\n## 5. `rotateAroundTo()` — aim a point around a hinge/axis\n\n```javascript\nconst aimed = arm.rotateAroundTo([0, 0, 1], [0, 0, 0], \"tip\", [30, 30, 20]);\n// Exact line solve:\nconst lineHit = arm.rotateAroundTo([0, 0, 1], [0, 0, 0], \"tip\", [30, 30, 0], { mode: 'line' });\n```\n\n## 6. `moveToLocal()` — offset from another shape's min corner\n\n```javascript\nconst part = box(20, 20, 30).moveToLocal(base, 10, 10, 10);\n```\n\n## 7. `translate()` — for simple offsets or bridging computed locations\n\n```javascript\nconst pipeLen = bb2.min[1] - bb1.max[1];\nconst pipe = cylinder(pipeLen, 5).pointAlong([0, 1, 0]).translate(40, (bb1.max[1] + bb2.min[1]) / 2, bb1.min[2] + 15);\n```\n\n## 8. `placeReference()` — align any anchor to a world coordinate\n\nPlace a shape so a named anchor point lands exactly where you want it. Accepts all built-in anchors (`'bottom'`, `'center'`, `'top-front-left'`, etc.) plus custom references from `withReferences()`.\n\n```javascript\n// Ground a shape — bottom face center at Z = 0\nconst grounded = shape.placeReference('bottom', [0, 0, 0])\n\n// Center at the world origin\nconst centered = shape.placeReference('center', [0, 0, 0])\n\n// Align left edge to X = 10\nconst aligned = shape.placeReference('left', [10, 0, 0])\n```\n\nAlso works with custom placement references for cross-file parts:\n\n```javascript\n// widget.forge.js — define once\nreturn union(base, post).withReferences({ points: { mount: [0, -16, -4] } });\n\n// importer — consume\nconst widget = require(\"./widget.forge.js\").placeReference(\"mount\", [120, 40, 0]);\n```\n\nFor cross-file parts needing proper alignment, prefer connectors over placement references.\n\n---\n\n<!-- generated/sketch.md -->\n\n# Sketch API\n\n2D geometry creation, transforms, booleans, constrained sketches, and extrusion.\n\n## Contents\n\n- [2D Sketch Primitives](#2d-sketch-primitives) — `path`, `stroke`, `rect`, `circle2d`, `roundedRect`, `polygon`, `ngon`, `ellipse`, `slot`, `arcSlot`, `star`\n- [2D Sketch Booleans](#2d-sketch-booleans) — `union2d`, `difference2d`, `intersection2d`\n- [2D Sketch Features](#2d-sketch-features) — `filletCorners`\n- [Tracked Solid Edge Features](#tracked-solid-edge-features) — `filletTrackedEdge`, `chamferTrackedEdge`\n- [2D Text](#2d-text) — `loadFont`, `text2d`, `textWidth`\n- [Constrained Sketches](#constrained-sketches) — `constrainedSketch`, `addRect`, `addPolygon`, `addRegularPolygon`\n- [2D Geometry Helpers](#2d-geometry-helpers) — `point`, `line`, `circle`, `degrees`, `radians`\n- [Sketch](#sketch) — Transforms, Booleans, Features, Promotion, Placement, Labels, Measurement\n- [ConstrainedSketchBuilder](#constrainedsketchbuilder) — Drawing, Entities, Geometric Constraints, Dimensional Constraints, Coincidence & Equality, Tangent Transitions, Shape Constraints, Positioning, Solving\n- [ConstraintSketch](#constraintsketch)\n- [SketchGroupBuilder](#sketchgroupbuilder)\n- [Point2D](#point2d)\n- [Line2D](#line2d)\n- [Circle2D](#circle2d)\n- [Rectangle2D](#rectangle2d)\n\n## Functions\n\n### 2D Sketch Primitives\n\n#### `path()` — Create a new [`PathBuilder`](/docs/curves#pathbuilder) for tracing a 2D outline point by point.\n\n[`PathBuilder`](/docs/curves#pathbuilder) is a fluent API for constructing 2D profiles using a mix of line segments, arcs, bezier curves, and splines. Always start with `.moveTo(x, y)` to set the starting point. Call `.close()` to get a filled `Sketch`, or `.stroke(width)` to thicken an open polyline into a solid profile.\n\nEdge labels can be assigned with `.label('name')` after any segment — they propagate through extrusion, revolve, loft, and sweep into named faces on the resulting [`Shape`](/docs/core#shape).\n\n```ts\n// Closed triangle\nconst triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();\n\n// L-shaped bracket as a stroke\nconst bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n\n// Labeled edges for downstream face references\nconst slot = path()\n .moveTo(0, 0)\n .lineTo(30, 0).label('bottom')\n .lineTo(30, 10)\n .lineTo(0, 10).label('top')\n .close();\n```\n\n```ts\npath(): PathBuilder\n```\n\n#### `stroke()` — Create a stroked polyline sketch from an array of 2D points.\n\n```ts\nstroke(points: [ number, number ][], width: number, join?: \"Round\" | \"Square\"): Sketch\n```\n\n#### `rect()` — Create a 2D rectangle centered at the origin.\n\n```ts\nrect(40, 20).extrude(5);\n```\n\n```ts\nrect(width: number, height: number): Sketch\n```\n\n#### `circle2d()` — Create a 2D circle centered at the origin.\n\nOmit `segments` for a smooth (auto-tessellated) circle. Pass an integer to get a regular polygon approximation — e.g. `6` for a hexagon, `8` for an octagon.\n\n```ts\ncircle2d(25).extrude(10); // smooth cylinder\ncircle2d(25, 6).extrude(10); // hexagonal prism\n```\n\n```ts\ncircle2d(radius: number, segments?: number): Sketch\n```\n\n#### `roundedRect()` — Create a 2D rectangle with rounded corners, centered at the origin.\n\nThe corner radius is automatically clamped to `min(width/2, height/2)` so it can never exceed the shape dimensions.\n\n```ts\nroundedRect(60, 30, 5).extrude(3);\n```\n\n```ts\nroundedRect(width: number, height: number, radius: number): Sketch\n```\n\n#### `polygon()` — Create a 2D polygon from an array of `[x, y]` points or `Point2D` objects.\n\nWinding order is normalized automatically — clockwise (CW) input is silently reversed to CCW before being passed to the geometry kernel.\n\n```ts\npolygon([[0, 0], [50, 0], [25, 40]]).extrude(5); // triangle\n```\n\n```ts\npolygon(points: ([ number, number ] | Point2D)[]): Sketch\n```\n\n#### `ngon()` — Create a regular polygon inscribed in a circle of the given radius.\n\n`radius` is the center-to-vertex (circumradius) distance. Use `sides` of `3` for a triangle, `6` for a hexagon, etc. The first vertex is at the top (−90° from +X).\n\n```ts\nngon(6, 20).extrude(10); // hexagonal prism, circumradius 20\n```\n\n```ts\nngon(sides: number, radius: number): Sketch\n```\n\n#### `ellipse()` — Create a 2D ellipse centered at the origin.\n\n```ts\nellipse(30, 15).extrude(5);\nellipse(30, 15, 32).extrude(5); // lower-resolution approximation\n```\n\n```ts\nellipse(rx: number, ry: number, segments?: number): Sketch\n```\n\n#### `slot()` — Create a slot (oblong / stadium shape) — a rectangle with semicircular ends, centered at the origin.\n\n```ts\nslot(40, 10).extrude(3); // 40mm long, 10mm wide slot\n```\n\n```ts\nslot(length: number, width: number): Sketch\n```\n\n#### `arcSlot()` — Create an arc-shaped slot (banana / annular sector) centered at the origin.\n\nThe slot is symmetric about the +X axis. The two ends are closed with semicircular caps. `pitchRadius` is the distance from the origin to the centerline of the slot, and `thickness` is the radial width of the slot.\n\n```ts\narcSlot(135, 74, 40).extrude(5); // pitch R135, 74° sweep, 40mm wide\n```\n\n```ts\narcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch\n```\n\n#### `star()` — Create a star shape with alternating outer and inner radii.\n\n```ts\nstar(5, 30, 12).extrude(4); // five-pointed star\n```\n\n```ts\nstar(points: number, outerR: number, innerR: number): Sketch\n```\n\n### 2D Sketch Booleans\n\n#### `union2d()` — Combine 2D sketches into a single profile using an additive boolean union.\n\nAccepts individual sketches or arrays: `union2d(a, b, c)` or `union2d([a, b, c])`. Uses Manifold's batch operation — faster than chaining `.add()` one by one when combining many sketches.\n\n```ts\nconst cross = union2d(rect(60, 10), rect(10, 60));\n```\n\n```ts\nunion2d(...inputs: SketchOperandInput[]): Sketch\n```\n\n#### `difference2d()` — Subtract one or more 2D sketches from a base sketch.\n\nThe first sketch is the base; all subsequent sketches are subtracted from it. Accepts individual sketches or arrays: `difference2d(base, c1, c2)` or `difference2d([base, c1, c2])`. Uses Manifold's batch operation — faster than chaining `.subtract()` one by one.\n\n```ts\nconst donut = difference2d(circle2d(50), circle2d(30));\n```\n\n```ts\ndifference2d(...inputs: SketchOperandInput[]): Sketch\n```\n\n#### `intersection2d()` — Keep only the area where all input sketches overlap (intersection boolean).\n\nAccepts individual sketches or arrays: `intersection2d(a, b)` or `intersection2d([a, b, c])`. Uses Manifold's batch operation — faster than chaining `.intersect()` one by one.\n\n```ts\nconst lens = intersection2d(circle2d(30).translate(-10, 0), circle2d(30).translate(10, 0));\n```\n\n```ts\nintersection2d(...inputs: SketchOperandInput[]): Sketch\n```\n\n### 2D Sketch Features\n\n#### `filletCorners()` — Create a polygon from points with specific corners rounded to arc fillets.\n\nEach corner spec identifies a vertex by its index in the `points` array and the desired fillet `radius`. Both convex and concave corners are supported.\n\nConstraints:\n\n- Collinear corners cannot be filleted (throws an error)\n- Two neighboring fillets whose tangent lengths overlap the same edge will throw\n- Radius must be positive and small enough to fit within the adjacent edge lengths\n\nUse `offset(-r).offset(+r)` instead if you want to round **all** convex corners uniformly. Use `filletCorners` when you need selective or mixed sharp/rounded profiles.\n\n```ts\nconst roof = filletCorners(roofPoints, [\n { index: 3, radius: 19 },\n { index: 4, radius: 19 },\n { index: 5, radius: 19 },\n]);\n```\n\n```ts\nfilletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch\n```\n\n`FilletCornerSpec`: `{ index: number, radius: number, segments?: number }`\n\n### Tracked Solid Edge Features\n\n#### `filletTrackedEdge()` — Round a tracked vertical solid edge with a circular fillet.\n\nCompiler-owned fillet for a narrow tracked-edge subset on solids.\n\nThis is **not** a general 2D sketch-corner fillet. It currently works only on tracked vertical edges from [`box()`](/docs/core#box) or `Rectangle2D` extrusions (plus rigid transforms and supported preserved descendants of those). Generic sketch extrudes, including `rect(...).extrude(...)`, are outside the supported subset right now.\n\n**Supported edges:**\n\n- Tracked vertical edges from [`box()`](/docs/core#box) or `Rectangle2D.extrude()`\n- Rigid transforms between tracked source and target\n- Untouched sibling tracked vertical edges after earlier `filletTrackedEdge`/`chamferTrackedEdge`\n\n**Not supported:** edges after shell, hole, cut, trim, difference, intersection, generic sketch extrudes, or tapered extrudes.\n\nCanonical quadrants: `vert-bl → [1,-1]`, `vert-br → [-1,-1]`, `vert-tr → [-1,1]`, `vert-tl → [1,1]`\n\n```ts\nconst base = Rectangle2D.fromDimensions(0, 0, 50, 50).extrude(20);\nconst filleted = filletTrackedEdge(base, base.edge('vert-br'), 5, [-1, -1]);\n```\n\n```ts\nfilletTrackedEdge(shape: Shape, edge: EdgeRef, radius: number, quadrant?: [ number, number ], segments?: number): Shape\n```\n\n**`EdgeRef`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `start` | `[ number, number, number ]` | Start point |\n| `end` | `[ number, number, number ]` | End point |\n| `query?` | `EdgeQueryRef` | Compiler-owned edge query when available. |\n| `curve?` | `EdgeCurve` | Exact or parametric curve family when the backend/source can identify one. |\n| `faceName?` | `string` | Owning face name when the edge is associated with one face in a larger topology. |\n| `name` | | — |\n\n#### `chamferTrackedEdge()` — Bevel a tracked vertical solid edge with a 45° chamfer.\n\nCompiler-owned chamfer for tracked vertical edges. Requires a compile-plan-covered target. This is not a general 2D sketch-corner tool; supported subset and quadrant semantics are the same as `filletTrackedEdge()` - see that function for details.\n\n```ts\nconst base = Rectangle2D.fromDimensions(0, 0, 50, 50).extrude(20);\nconst chamfered = chamferTrackedEdge(base, base.edge('vert-br'), 3, [-1, -1]);\n```\n\n```ts\nchamferTrackedEdge(shape: Shape, edge: EdgeRef, size: number, quadrant?: [ number, number ]): Shape\n```\n\n### 2D Text\n\n#### `loadFont()` — Pre-load and cache a font for use with `text2d()`.\n\nFonts are cached by their source string (or `cacheKey` for `ArrayBuffer` sources), so repeated calls with the same path are free. Pre-loading is useful when you call `text2d()` many times with the same font — it avoids repeated disk reads.\n\nBuilt-in font names that work everywhere (browser + CLI):\n\n- `'sans-serif'` or `'inter'` — bundled Inter Regular\n\n```ts\nconst font = loadFont('/path/to/Arial Bold.ttf');\ntext2d('Title', { size: 12, font }).extrude(1.5);\ntext2d('Subtitle', { size: 8, font }).extrude(1);\n```\n\n```ts\nloadFont(source: string | ArrayBuffer, cacheKey?: string): opentype.Font\n```\n\n#### `text2d()` — Build a filled 2D Sketch from a text string.\n\nThe Sketch origin is at the left end of the text baseline by default. Use `align` and `baseline` options to adjust placement. Text is rendered using the bundled Inter font by default, or any TTF/OTF/WOFF font you provide.\n\n`text2d()` creates real geometry. For temporary viewport annotations, prefer `Viewport.label()` so the text stays off the geometry and OCCT compile paths. Do not use either form of text to make unclear production geometry readable; model the physical artifact clearly instead.\n\nAlignment reference table:\n\n| `align` | `baseline` | Origin |\n|------------|--------------|-------------------------------------|\n| `'left'` | `'baseline'` | Bottom-left of first char (default) |\n| `'center'` | `'center'` | Dead center of text block |\n| `'right'` | `'top'` | Top-right corner |\n\n```ts\n// Extruded nameplate\ntext2d('FORGE CAD', { size: 8 }).extrude(1.2);\n\n// Centered label on the XY plane\ntext2d('V 2.0', { size: 6, align: 'center', baseline: 'center' });\n\n// Engraved text cut into the top face of a box\nconst label = text2d('REV A', { size: 5, align: 'center', baseline: 'center' });\nplate.subtract(label.onFace(plate, 'top', { protrude: -0.5 }).extrude(1));\n\n// Custom TTF font\ntext2d('Hello', { size: 10, font: '/path/to/Arial.ttf' }).extrude(1);\n\n// Pre-loaded font for reuse\nconst font = loadFont('/path/to/Arial Bold.ttf');\ntext2d('Title', { size: 12, font }).extrude(1.5);\n```\n\n```ts\ntext2d(content: string, options?: TextOptions): Sketch\n```\n\n**`TextOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `size?` | `number` | Cap height of the text in model units. All other dimensions (stroke weight, spacing) scale proportionally. |\n| `letterSpacing?` | `number` | Extra space between characters in model units. Negative values tighten the tracking. |\n| `align?` | `\"left\" \\| \"center\" \\| \"right\"` | Horizontal alignment relative to x = 0. - `'left'` — left edge at x = 0 (default) - `'center'` — centred on x = 0 - `'right'` — right edge at x = 0 |\n| `baseline?` | `\"baseline\" \\| \"center\" \\| \"top\"` | Vertical alignment relative to y = 0. - `'baseline'` — y = 0 is the text baseline (bottom of capital letters) - `'center'` — y = 0 is the vertical midpoint of the cap height - `'top'` — y = 0 is the top of capital letters |\n| `font?` | `string \\| opentype.Font` | Font to use for text rendering. - `'sans-serif'` or `'inter'` — bundled Inter font (works everywhere, including browser) - **file path** — path to a TTF, OTF, or WOFF font file (CLI/Node only) - **Font object** — a previously loaded opentype.js Font (from `loadFont()`) - **omitted** — uses the bundled Inter font (same as `'sans-serif'`) |\n| `flattenTolerance?` | `number` | Bezier flattening tolerance in model units. Smaller = more polygon segments = smoother curves. |\n\n#### `textWidth()` — Measure the rendered advance width of a string without creating any geometry.\n\nUses the same font metrics as `text2d()`. Useful for computing layout dimensions before building the actual sketch — e.g. sizing a plate to fit a label.\n\n```ts\nconst w = textWidth('SERIAL: 001', { size: 6 });\nconst plate = box(w + 10, 12, 2);\n```\n\n```ts\ntextWidth(content: string, options?: Pick<TextOptions, \"size\" | \"letterSpacing\" | \"font\">): number\n```\n\n### Constrained Sketches\n\n#### `constrainedSketch()` — Create a parametric 2D sketch driven by geometric constraints and a nonlinear solver.\n\n**Workflow**\n\n1. Create a builder with `constrainedSketch()`.\n2. Add geometry — points, lines, circles, arcs — using the builder methods.\n3. Add constraints (`horizontal`, `length`, `fix`, etc.) to drive the geometry.\n4. Call `.solve()` to run the solver and get a `ConstraintSketch` (which extends `Sketch`).\n\n```ts\nconst sk = constrainedSketch();\nconst p1 = sk.point(0, 0);\nconst p2 = sk.point(50, 0);\nconst l1 = sk.line(p1, p2);\nsk.fix(p1, 0, 0);\nsk.horizontal(l1);\nsk.length(l1, 50);\nreturn sk.solve().extrude(10);\n```\n\n**Solver status**\n\n```ts\nconst result = sk.solve();\nresult.constraintMeta.status; // 'fully' | 'under' | 'over' | 'over-redundant'\nresult.constraintMeta.dof; // 0 = fully constrained\nresult.constraintMeta.maxError; // residual — should be < 1e-6\nresult.inspect(); // human-readable summary\nresult.withUpdatedConstraint('cst-5', 120); // update a dimension without rebuilding\n```\n\n```ts\nconstrainedSketch(options?: ConstrainedSketchOptions): ConstrainedSketchBuilder\n```\n\n**`ConstrainedSketchOptions`**\n- `strict?: boolean` — When true, adding a constraint that cannot be satisfied throws instead of silently discarding it.\n\n#### `addRect()` — Add an axis-aligned rectangle concept to the builder.\n\nCreates 4 vertices (CCW: bl→br→tr→tl), 4 sides, 4 structural constraints (`horizontal`/`vertical` on each side), CCW winding, a center point, a loop, and a shape. Returns a `ConstrainedRect` handle with 4 DOF (x, y, width, height).\n\nUse `sk.rect()` as the shorthand builder method.\n\n```ts\nconst sk = constrainedSketch();\nconst r = sk.rect({ x: 0, y: 0, width: 100, height: 50 });\nsk.fix(r.bottomLeft, 0, 0);\nsk.length(r.bottom, 120); // override initial width\nreturn sk.solve().extrude(10);\n```\n\n```ts\naddRect(sk: ConstrainedSketchBuilder, options?: RectOptions): ConstrainedRect\n```\n\n**`RectOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `x?` | `number` | Bottom-left x coordinate. Default: 0. |\n| `y?` | `number` | Bottom-left y coordinate. Default: 0. |\n| `width?` | `number` | Width (along x). Default: 10. |\n| `height?` | `number` | Height (along y). Default: 10. |\n| `blockRotation?` | `boolean` | Prevent 180° rotation (ensures bottom edge points rightward). Default: false. |\n\n**`ConstrainedRect`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `bottom` | `LineId` | bottom-left → bottom-right |\n| `right` | `LineId` | bottom-right → top-right |\n| `top` | `LineId` | top-right → top-left |\n| `left` | `LineId` | top-left → bottom-left |\n| `center` | `PointId` | Center point constrained to the geometric center via `midpoint` on the diagonal. Can be used in further constraints: `sk.fix(rect.center, 0, 0)`, `sk.coincident(rect.center, other)`. |\n| `shape` | `ShapeId` | ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. |\n| `vertices` | `[ PointId, PointId, PointId, PointId ]` | CCW-ordered vertex array: [bottomLeft, bottomRight, topRight, topLeft]. |\n| `sides` | `[ LineId, LineId, LineId, LineId ]` | CCW-ordered side array: [bottom, right, top, left]. |\n| `bottomLeft`, `bottomRight`, `topRight`, `topLeft` | | — |\n\n#### `addPolygon()` — Add a general polygon concept to the builder.\n\nCreates n vertices and n sides (CCW: `sides[i]` from `vertices[i]` → `vertices[(i+1) % n]`). Applies a `ccw` constraint to enforce winding. All dimensional constraints (lengths, angles, position) are left to the caller.\n\nUse `sk.addPolygon()` as the shorthand builder method.\n\n```ts\nconst sk = constrainedSketch();\nconst tri = sk.addPolygon({ points: [[0,0],[100,0],[50,80]] });\nsk.fix(tri.vertex(0), 0, 0);\nsk.length(tri.side(0), 100);\nreturn sk.solve().extrude(5);\n```\n\n```ts\naddPolygon(sk: ConstrainedSketchBuilder, options: PolygonOptions): ConstrainedPolygon\n```\n\n**`PolygonOptions`**\n- `points: ReadonlyArray<readonly [ number, number ]>` — Initial vertex coordinates. Minimum 3 points.\n- `addLoop?: boolean` — Whether to register a closed loop for sketch generation. Default: true.\n- `blockRotation?: boolean` — Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false.\n\n**`ConstrainedPolygon`**\n- `vertices: PointId[]` — CCW-ordered PointIds.\n- `sides: LineId[]` — CCW-ordered LineIds. `sides[i]` runs from `vertices[i]` → `vertices[(i+1) % n]`.\n- `shape: ShapeId` — ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`.\n\n#### `addRegularPolygon()` — Add a regular n-gon concept to the builder.\n\nVertices are placed at `(cx + r·cos(startAngle + i·2π/n), cy + r·sin(...))`. Equal-radius and equal-side constraints enforce regularity (4 DOF: center x/y, radius, rotation). The center point is tracked by the solver and exposed via the returned handle.\n\nUse `sk.regularPolygon()` as the shorthand builder method.\n\n```ts\nconst sk = constrainedSketch();\nconst hex = sk.regularPolygon({ sides: 6, radius: 25 });\nsk.fix(hex.center, 0, 0);\nsk.length(hex.side(0), 30); // all sides change (equal constraint)\nreturn sk.solve().extrude(5);\n```\n\n```ts\naddRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions): ConstrainedRegularPolygon\n```\n\n**`RegularPolygonOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `sides` | `number` | Number of sides (minimum 3). |\n| `radius?` | `number` | Circumradius — distance from center to vertex. Default: 10. |\n| `cx?` | `number` | Center x coordinate. Default: 0. |\n| `cy?` | `number` | Center y coordinate. Default: 0. |\n| `startAngle?` | `number` | Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive). Default: 0 (rightmost vertex). |\n| `blockRotation?` | `boolean` | Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. |\n\n\n**`ConstrainedRegularPolygon`** extends ConstrainedPolygon\n- `center: PointId` — Center point. Use `sk.fix(poly.center, x, y)` to pin location, or `sk.coincident(poly.center, other)` to align with other geometry.\n\n### 2D Geometry Helpers\n\n#### `point()` — Create an analytic 2D point for measurement and construction geometry.\n\n```ts\nconst p = point(10, 20);\np.distanceTo(point(30, 40)); // Euclidean distance\np.midpointTo(point(30, 40)); // midpoint\np.translate(5, 5); // new shifted point\np.toTuple(); // [10, 20]\n```\n\n```ts\npoint(x: number, y: number): Point2D\n```\n\n#### `line()` — Create an analytic 2D line segment between two points.\n\n```ts\nconst l = line(0, 0, 50, 0);\nl.length; l.midpoint; l.angle; l.direction;\nl.parallel(10); // parallel line offset 10 (positive = left)\nl.intersect(l2); // Point2D — treats lines as infinite\nl.intersectSegment(l2); // Point2D or null — segments only\n\nLine2D.fromPointAndAngle(point(0, 0), 45, 100);\nLine2D.fromPointAndDirection(point(0, 0), [1, 1], 50);\n```\n\n```ts\nline(x1: number, y1: number, x2: number, y2: number): Line2D\n```\n\n#### `circle()` — Create an analytic 2D circle for measurement, construction, and extrusion.\n\n```ts\nconst c = circle(0, 0, 25);\nc.diameter; c.circumference; c.area;\nc.pointAtAngle(90); // Point2D at top (90° CCW from +X)\n\n// Extrude to cylinder with named faces\nconst cyl = c.extrude(30);\ncyl.face('top'); // FaceRef (planar)\ncyl.face('side'); // FaceRef (curved)\n\nCircle2D.fromDiameter(point(0, 0), 50);\n```\n\n```ts\ncircle(cx: number, cy: number, radius: number): Circle2D\n```\n\n#### `degrees()` — Identity function that returns degrees unchanged.\n\nUse for clarity when the unit of an angle value would otherwise be ambiguous — e.g. `param(\"Angle\", degrees(45))`.\n\n```ts\ndegrees(deg: number): number\n```\n\n#### `radians()` — Convert radians to degrees.\n\nForgeCAD's public API uses degrees throughout. Use this when you have a radian value (e.g. from `Math.atan2`) that you want to express in degrees.\n\n```ts\nradians(rad: number): number\n```\n\n---\n\n## Classes\n\n### `Sketch`\n\nImmutable 2D profile for extrusion, revolve, and other operations.\n\n`Sketch` wraps Manifold's `CrossSection` with a chainable 2D API. Every method returns a new `Sketch` — the original is never mutated. Colors, edge labels, and placement data are preserved through all transforms and boolean operations.\n\nSupported operations:\n\n- **Transforms** — `translate`, `rotate`, `rotateAround`, `scale`, `mirror`\n- **Booleans** — `add` (union), `subtract` (difference), `intersect`\n- **Operations** — `offset`, `simplify`\n- **Queries** — `area`, `bounds`, `isEmpty`, `numVert`\n- **3D operations** — `extrude`, `revolve`, `onFace`\n- **Regions** — `regions`, `region`\n- **Placement** — `attachTo`\n\nNamed anchor positions used by `attachTo()`: `'center'` | `'top-left'` | `'top-right'` | `'bottom-left'` | `'bottom-right'` | `'top'` | `'bottom'` | `'left'` | `'right'`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `cross` | `ProfileBackend` | — |\n\n**Transforms**\n\n#### `translate()` — Move the sketch by the given X and Y offset.\n\n```ts\ntranslate(x: number, y?: number): Sketch\n```\n\n#### `rotate()` — Rotate the sketch around its bounding-box center.\n\n```ts\nrotate(degrees: number): Sketch\n```\n\n#### `rotateAround()` — Rotate the sketch around a specific pivot point.\n\n```ts\nrect(20, 20).rotateAround(45, [0, 0]);\n```\n\n```ts\nrotateAround(degrees: number, pivot: [ number, number ]): Sketch\n```\n\n#### `scale()` — Scale the sketch relative to its bounding-box center.\n\nPass a single number for uniform scaling, or `[sx, sy]` for per-axis scaling.\n\n```ts\nscale(v: number | [ number, number ]): Sketch\n```\n\n#### `scaleAround()` — Scale the sketch relative to an arbitrary pivot point.\n\n```ts\nscaleAround(pivot: [ number, number ], v: number | [ number, number ]): Sketch\n```\n\n#### `mirror()` — Mirror the sketch across a line through its bounding-box center.\n\n`normal` is the normal vector of the mirror line (not the line direction). For example, `[1, 0]` mirrors across a vertical line (Y axis direction), and `[0, 1]` mirrors across a horizontal line.\n\n```ts\nmirror(normal: [ number, number ]): Sketch\n```\n\n#### `mirrorThrough()` — Mirror the sketch across a line defined by a point and a normal direction.\n\n```ts\nmirrorThrough(point: [ number, number ], normal: [ number, number ]): Sketch\n```\n\n**Booleans**\n\n#### `add()` — Add (union) one or more sketches to this sketch.\n\nAccepts individual sketches or arrays: `sketch.add(a, b)` or `sketch.add([a, b])`. For combining many sketches at once, prefer the free function `union2d()` which uses Manifold's batch operation and is faster than chaining.\n\n```ts\ncircle2d(20).add(rect(10, 30)).extrude(5);\n```\n\n```ts\nadd(...others: SketchOperandInput[]): Sketch\n```\n\n#### `subtract()` — Subtract one or more sketches from this sketch.\n\nAccepts individual sketches or arrays: `sketch.subtract(a, b)` or `sketch.subtract([a, b])`. For subtracting many cutters at once, prefer the free function `difference2d()`.\n\n```ts\nrect(40, 40).subtract(circle2d(10)).extrude(5);\n```\n\n```ts\nsubtract(...others: SketchOperandInput[]): Sketch\n```\n\n#### `intersect()` — Intersect this sketch with one or more others (keep overlapping area only).\n\nAccepts individual sketches or arrays: `sketch.intersect(a, b)` or `sketch.intersect([a, b])`. For intersecting many sketches, prefer the free function `intersection2d()`.\n\n```ts\nintersect(...others: SketchOperandInput[]): Sketch\n```\n\n**Features**\n\n#### `offset()` — Inflate (positive delta) or deflate (negative delta) the sketch contour.\n\nUse `offset(-r).offset(+r)` to round every convex corner of a closed sketch.\n\n- `'Round'` — smooth arc at each corner (default)\n- `'Square'` — flat mitered extension\n- `'Miter'` — sharp pointed extension\n\n```ts\nrect(40, 20).offset(3); // expand by 3\nrect(40, 20).offset(-2).offset(2); // round all convex corners\n```\n\n```ts\noffset(delta: number, join?: \"Square\" | \"Round\" | \"Miter\"): Sketch\n```\n\n#### `regions()` — Decompose this sketch into its distinct filled regions, sorted largest-first by area.\n\nA single sketch can contain several disconnected filled areas (e.g., two separate rectangles, or a ring shape with a hole). This method enumerates all top-level connected regions as independent `Sketch` objects, each with its own outer boundary and associated holes.\n\n```ts\nconst pair = union2d(rect(40, 40), rect(40, 40).translate(60, 0));\nconst [left, right] = pair.regions(); // largest first\nleft.extrude(5);\n```\n\n```ts\nregions(): Sketch[]\n```\n\n#### `region()` — Select the single filled region that contains the given 2D seed point.\n\nThe seed must lie strictly inside the filled area — not on a boundary edge and not inside a hole. Throws a descriptive error if the seed is outside all regions. If unsure where regions are, use `.regions()` first — each result has `.bounds()`.\n\n```ts\nconst donut = circle2d(50).subtract(circle2d(30));\ndonut.region([40, 0]).extrude(10); // seed at radius 40, inside the ring\n```\n\n```ts\nregion(seed: [ number, number ]): Sketch\n```\n\n**Promotion**\n\n#### `extrude()` — Extrude this 2D sketch along Z to create a 3D solid. Supports twist and scale tapering.\n\n```ts\nextrude(height: number, opts?: { twist?: number; divisions?: number; scaleTop?: number | [ number, number ]; }): Shape\n```\n\n#### `revolve()` — Revolve this 2D sketch around the Y axis to create a 3D solid of revolution.\n\n```ts\nrevolve(degrees?: number, segments?: number): Shape\n```\n\n**Placement**\n\n#### `attachTo()` — Position this sketch relative to another using named anchor points.\n\nComputes the translation needed to align `selfAnchor` on this sketch with `targetAnchor` on the target sketch, then applies an optional pixel-exact offset.\n\nAnchor positions: `'center'` | `'top-left'` | `'top-right'` | `'bottom-left'` | `'bottom-right'` | `'top'` | `'bottom'` | `'left'` | `'right'`\n\n```ts\nconst arm = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left');\nconst shifted = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left', [5, 0]);\n```\n\n```ts\nattachTo(target: Sketch, targetAnchor: Anchor, selfAnchor?: Anchor, offset?: [ number, number ]): Sketch\n```\n\n#### `onFace()` — Place this sketch on a face or planar target in 3D space.\n\nUse this when a 2D profile should be oriented onto a 3D face before extrusion or other downstream operations.\n\n```ts\nonFace(parentOrFace: Shape | { toShape(): Shape; } | { _bbox(): { min: number[]; max: number[]; }; } | FaceRef, faceOrOpts?: \"front\" | \"back\" | \"left\" | \"right\" | \"top\" | \"bottom\" | string | FaceRef | { u?: number; v?: number; protrude?: number; selfAnchor?: Anchor; }, opts?: { u?: number; v?: number; protrude?: number; selfAnchor?: Anchor; }): Sketch\n```\n\n**Labels**\n\n#### `labelEdge()` — Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch.\n\n```ts\nlabelEdge(name: string): Sketch\n```\n\n#### `labelEdges()` — Label edges in winding order, or by named map for rect.\n\nPositional: `labelEdges('bottom', 'right', 'top', 'left')` — one per edge, `null` to skip. Named (rect only): `labelEdges({ bottom: 'floor', top: 'ceiling' })`. Returns a new sketch.\n\n```ts\nlabelEdges(...args: (string | null)[] | [ Record<string, string> ]): Sketch\n```\n\n#### `edgeLabels()` — List current edge label names.\n\n```ts\nedgeLabels(): string[]\n```\n\n#### `prefixLabels()` — Prefix all edge labels. Returns a new sketch with prefixed labels.\n\n```ts\nprefixLabels(prefix: string): Sketch\n```\n\n#### `renameLabel()` — Rename a single edge label. Returns a new sketch.\n\n```ts\nrenameLabel(from: string, to: string): Sketch\n```\n\n#### `dropLabels()` — Remove specific labels. Returns a new sketch.\n\n```ts\ndropLabels(...names: string[]): Sketch\n```\n\n#### `dropAllLabels()` — Remove all labels. Returns a new sketch.\n\n```ts\ndropAllLabels(): Sketch\n```\n\n**Measurement**\n\n#### `area()` — Return the total filled area of the sketch.\n\n```ts\narea(): number\n```\n\n#### `bounds()` — Return the axis-aligned bounding box of the sketch.\n\n```ts\nbounds(): ProfileBounds\n```\n\n#### `isEmpty()` — Return `true` if the sketch contains no filled area.\n\n```ts\nisEmpty(): boolean\n```\n\n#### `numVert()` — Return the number of vertices in the polygon representation of the sketch contours.\n\n```ts\nnumVert(): number\n```\n\n#### `toPolygons()` — Return the sketch as a list of polygons matching its contour topology.\n\nUseful when you need raw polygon data for inspection or custom export.\n\n```ts\ntoPolygons(): number[][][]\n```\n\n**Other**\n\n#### `color()` — Set the display color of this sketch.\n\nColor is preserved through all transforms and boolean operations. Pass `undefined` to clear the color.\n\n```ts\ncircle2d(20).color('#ff0000').extrude(5);\n```\n\n```ts\ncolor(value: string | undefined): Sketch\n```\n\n#### `clone()` — Create an explicit copy of this sketch for branching variants.\n\nBecause all Sketch operations are immutable, `clone()` is rarely needed. Use it when you want to assign the same sketch to multiple names and continue modifying each independently without confusion.\n\n```ts\nclone(): Sketch\n```\n\n### `ConstrainedSketchBuilder`\n\n**Drawing**\n\n#### `moveTo()` — Move the cursor to `(x, y)` and start a new profile loop.\n\n```ts\nmoveTo(x: number, y: number): this\n```\n\n#### `lineTo()` — Draw a line from the current cursor to `(x, y)`.\n\n```ts\nlineTo(x: number, y: number): this\n```\n\n#### `lineH()` — Draw a horizontal line of length `dx` from the current cursor.\n\n```ts\nlineH(dx: number): this\n```\n\n#### `lineV()` — Draw a vertical line of length `dy` from the current cursor.\n\n```ts\nlineV(dy: number): this\n```\n\n#### `lineAngled()` — Draw a line of the given `length` at `degrees` from +X.\n\n```ts\nlineAngled(length: number, degrees: number): this\n```\n\n#### `arcTo()` — Draw a circular arc from the current cursor to `(x, y)` with the given radius.\n\n```ts\narcTo(x: number, y: number, radius: number, clockwise?: boolean): this\n```\n\n#### `arcByCenter()` — Create an arc from an explicit center point and endpoint IDs.\n\n```ts\narcByCenter(centerId: PointId, startId: PointId, endId: PointId, clockwise?: boolean, name?: string, fixedRadius?: boolean): ArcId\n```\n\n#### `bezier()` — Create a cubic Bezier curve from four control points.\n\n```ts\nbezier(p0: any, p1: any, p2: any, p3: any, name?: string): BezierId\n```\n\n#### `bezierTo()` — Draw a cubic Bezier from the current cursor to `(x3, y3)`.\n\n```ts\nbezierTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): this\n```\n\n#### `blendTo()` — Draw a smooth Bezier tangent to the previous arc.\n\n```ts\nblendTo(x: number, y: number, weight?: number): this\n```\n\n#### `label()` — Label the current path segment.\n\n```ts\nlabel(name: string): this\n```\n\n#### `close()` — Close the current path and register the loop.\n\n```ts\nclose(): this\n```\n\n#### `addLoopCircle()` — Add a circle loop to the path.\n\n```ts\naddLoopCircle(center: PointId, radius: number, segments?: number): this\n```\n\n#### `addLoop()` — Add a closed polygon loop from point IDs.\n\n```ts\naddLoop(points: any[]): this\n```\n\n#### `addProfileLoop()` — Add a profile loop from prebuilt line/arc/bezier segments.\n\n```ts\naddProfileLoop(segments: Array<{ kind: \"line\"; line: any; } | { kind: \"arc\"; arc: any; } | { kind: \"bezier\"; bezier: any; }>): this\n```\n\n**Entities**\n\n#### `point()` — Add a free point to the sketch at `(x, y)`.\n\nIf `x` or `y` are omitted, the point is placed at the bounding-box center of existing geometry so it starts near other entities rather than at the origin. Throws if either coordinate is `NaN` or `Infinity`.\n\n```ts\npoint(x?: number, y?: number, fixed?: boolean): PointId\n```\n\n#### `pointAt()` — Return the `PointId` of the point created at the given insertion index.\n\n```ts\npointAt(index: number): PointId\n```\n\n#### `line()` — Connect two existing points with a line segment.\n\nPass `construction = true` for a helper line that participates in constraints but is excluded from the solved sketch output (not part of any profile loop).\n\n```ts\nconst axis = sk.line(sk.point(0, -50), sk.point(0, 50), true);\nsk.symmetric(p1, p2, axis);\n```\n\n```ts\nline(a: PointId, b: PointId, construction?: boolean, name?: string): LineId\n```\n\n#### `lineAt()` — Return the `LineId` of the line created at the given insertion index.\n\n```ts\nlineAt(index: number): LineId\n```\n\n#### `circle()` — Add a circle to the sketch with the given center point and initial radius.\n\nThe radius is a starting value — if you add a `radius()` or `diameter()` constraint, the solver will adjust it. Non-construction circles automatically register a loop.\n\n```ts\ncircle(center: PointId, radius: number, construction?: boolean, segments?: number, name?: string): CircleId\n```\n\n#### `circleAt()` — Return the `CircleId` of the circle created at the given insertion index.\n\n```ts\ncircleAt(index: number): CircleId\n```\n\n#### `shape()` — Register a named shape (closed polygon) from an ordered list of line IDs.\n\nThe `ShapeId` can be passed to `shapeWidth()`, `shapeHeight()`, `shapeArea()`, `shapeCentroidX()`, `shapeCentroidY()`, and `shapeEqualCentroid()` constraints. Shape registration is done automatically by concept factories like `rect()` and `addPolygon()`.\n\n```ts\nshape(lines: LineId[]): ShapeId\n```\n\n#### [`group()`](/docs/core#group) — Create a rigid-body group with a local coordinate frame.\n\nPoints and lines added to the group move together as a unit — the solver sees 3 DOF (x, y, θ) instead of 2N per point. After configuring the group, call `.done()` to register it and receive a `SketchGroupHandle`.\n\nGroup points are addressable by their `PointId` in all sketch constraints (e.g. `sk.coincident`, `sk.distance`) just like any other points.\n\n```ts\nconst g = sk.group({ x: 50, y: 30 });\nconst p0 = g.point(0, 0); // local origin → world (50, 30)\nconst p1 = g.point(100, 0); // local (100,0) → world (150, 30)\nconst l = g.line(p0, p1);\ng.fixRotation();\nconst handle = g.done();\n// p0, p1 work in constraints like any other PointId:\nsk.coincident(p0, someExternalPoint);\n```\n\n```ts\ngroup(opts?: { x?: number; y?: number; theta?: number; id?: string; }): SketchGroupBuilder\n```\n\n#### `rect()` — Add an axis-aligned rectangle concept. Returns a `ConstrainedRect` handle with named vertices, sides, and center.\n\n```ts\nrect(options?: RectOptions): ConstrainedRect\n```\n\n#### `addPolygon()` — Add a general polygon concept (CCW winding enforced). Returns a `ConstrainedPolygon` handle.\n\n```ts\naddPolygon(options: PolygonOptions): ConstrainedPolygon\n```\n\n#### `regularPolygon()` — Add a regular n-gon concept (equal sides, CCW winding). Returns a `ConstrainedRegularPolygon` handle with a center point.\n\n```ts\nregularPolygon(options: RegularPolygonOptions): ConstrainedRegularPolygon\n```\n\n#### `groupRect()` — Add a rigid rectangle as a group concept. Returns a `ConstrainedGroupRect` handle with named vertices and sides. The rectangle is fixed in shape — only position (and optionally rotation) varies.\n\n```ts\ngroupRect(options: GroupRectOptions): ConstrainedGroupRect\n```\n\n**Geometric Constraints**\n\n#### `horizontal()` — Constrain a line to be horizontal (parallel to the X axis).\n\n```ts\nhorizontal(line: any): this\n```\n\n#### `vertical()` — Constrain a line to be vertical (parallel to the Y axis).\n\n```ts\nvertical(line: any): this\n```\n\n#### `parallel()` — Constrain two lines to be parallel.\n\n```ts\nparallel(a: any, b: any): this\n```\n\n#### `sameDirection()` — Constrain two lines to point in the same direction.\n\n```ts\nsameDirection(a: any, b: any): this\n```\n\n#### `oppositeDirection()` — Constrain two lines to point in opposite directions.\n\n```ts\noppositeDirection(a: any, b: any): this\n```\n\n#### `perpendicular()` — Constrain two lines to be perpendicular.\n\n```ts\nperpendicular(a: any, b: any): this\n```\n\n#### `tangent()` — Constrain a line/circle or circle/circle tangency relationship.\n\n```ts\ntangent(a: any, b: any): this\n```\n\n#### `collinear()` — Constrain a point to lie on the infinite extension of a line.\n\n```ts\ncollinear(point: any, line: any): this\n```\n\n#### `symmetric()` — Constrain two points to be symmetric about an axis line.\n\n```ts\nsymmetric(a: any, b: any, axis: any): this\n```\n\n#### `blockRotation()` — Prevent 180° rotation of a polygon by anchoring its first edge.\n\n```ts\nblockRotation(points: any[], axis?: \"x\" | \"y\"): this\n```\n\n**Dimensional Constraints**\n\n#### `distance()` — Constrain the Euclidean distance between two points.\n\n```ts\ndistance(a: any, b: any, value: number): this\n```\n\n#### `length()` — Constrain the length of a line segment.\n\n```ts\nlength(line: any, value: number): this\n```\n\n#### `angle()` — Constrain the signed angle from line `a` to line `b`.\n\n```ts\nangle(a: any, b: any, value: number): this\n```\n\n#### `radius()` — Constrain the radius of a circle.\n\n```ts\nradius(circle: any, value: number): this\n```\n\n#### `diameter()` — Constrain the diameter of a circle.\n\n```ts\ndiameter(circle: any, value: number): this\n```\n\n#### `hDistance()` — Constrain the horizontal distance between two points.\n\n```ts\nhDistance(a: any, b: any, value: number): this\n```\n\n#### `vDistance()` — Constrain the vertical distance between two points.\n\n```ts\nvDistance(a: any, b: any, value: number): this\n```\n\n#### `pointLineDistance()` — Constrain the signed perpendicular distance from a point to a line.\n\n```ts\npointLineDistance(point: any, line: any, value: number): this\n```\n\n#### `lineDistance()` — Constrain the perpendicular offset distance between two lines.\n\n```ts\nlineDistance(a: any, b: any, value: number): this\n```\n\n#### `absoluteAngle()` — Constrain the absolute angle of a line measured from +X.\n\n```ts\nabsoluteAngle(line: any, value: number): this\n```\n\n#### `arcLength()` — Constrain the arc length of an arc.\n\n```ts\narcLength(arc: any, value: number): this\n```\n\n#### `equalRadius()` — Constrain two circles to have equal radii.\n\n```ts\nequalRadius(a: any, b: any): this\n```\n\n#### `angleBetween()` — Constrain the unsigned angle between two lines.\n\n```ts\nangleBetween(a: any, b: any, value: number): this\n```\n\n**Coincidence & Equality**\n\n#### `equal()` — Constrain two lines to have equal length.\n\n```ts\nequal(a: any, b: any): this\n```\n\n#### `coincident()` — Constrain two points to coincide.\n\n```ts\ncoincident(a: any, b: any): this\n```\n\n#### `concentric()` — Constrain two circles to share a center.\n\n```ts\nconcentric(a: any, b: any): this\n```\n\n#### `fix()` — Pin a point at a specific world location.\n\n```ts\nfix(point: any, x?: number, y?: number): this\n```\n\n#### `midpoint()` — Constrain a point to lie at the midpoint of a line.\n\n```ts\nmidpoint(point: any, line: any): this\n```\n\n#### `pointOnCircle()` — Constrain a point to lie on the perimeter of a circle.\n\n```ts\npointOnCircle(point: any, circle: any): this\n```\n\n#### `pointOnLine()` — Constrain a point to lie on the bounded segment of a line.\n\n```ts\npointOnLine(point: any, line: any): this\n```\n\n#### `ccw()` — Constrain all given points to be in counter-clockwise order.\n\n```ts\nccw(...points: any[]): this\n```\n\n**Tangent Transitions**\n\n#### `lineTangentArc()` — Constrain a line to be tangent to an arc at its start or end point.\n\n```ts\nlineTangentArc(line: any, arc: any, atStart: boolean): this\n```\n\n#### `arcTangentArc()` — Constrain two arcs to be tangent at their shared junction point.\n\n```ts\narcTangentArc(arcA: any, arcB: any, aAtStart?: boolean, bAtStart?: boolean): this\n```\n\n#### `bezierTangentArc()` — Constrain a Bezier to be tangent to an arc at one endpoint.\n\n```ts\nbezierTangentArc(bezier: any, arc: any, atBezierStart: boolean, atArcStart: boolean): this\n```\n\n#### `smoothBlend()` — Create a Bezier blend between two arcs.\n\n```ts\nsmoothBlend(arc1: any, arc2: any, options?: { weight?: number; arc1End?: \"start\" | \"end\"; arc2End?: \"start\" | \"end\"; }): BezierId\n```\n\n**Shape Constraints**\n\n#### `shapeWidth()` — Constrain a shape's width.\n\n```ts\nshapeWidth(shape: any, value: number): this\n```\n\n#### `shapeHeight()` — Constrain a shape's height.\n\n```ts\nshapeHeight(shape: any, value: number): this\n```\n\n#### `shapeCentroidX()` — Constrain a shape's centroid X position.\n\n```ts\nshapeCentroidX(shape: any, value: number): this\n```\n\n#### `shapeCentroidY()` — Constrain a shape's centroid Y position.\n\n```ts\nshapeCentroidY(shape: any, value: number): this\n```\n\n#### `shapeArea()` — Constrain a shape's area.\n\n```ts\nshapeArea(shape: any, value: number): this\n```\n\n#### `shapeEqualCentroid()` — Constrain two shapes to have the same centroid.\n\n```ts\nshapeEqualCentroid(a: any, b: any): this\n```\n\n**Positioning**\n\n#### `offsetX()` — Constrain the horizontal (X-axis) offset between two lines. Uses the start-point of each line to measure horizontal distance. `value` is the signed distance: b.startPt.x − a.startPt.x = value.\n\n```ts\noffsetX(a: any, b: any, value: number): this\n```\n\n#### `offsetY()` — Constrain the vertical (Y-axis) offset between two lines. Uses the start-point of each line to measure vertical distance. `value` is the signed distance: b.startPt.y − a.startPt.y = value.\n\n```ts\noffsetY(a: any, b: any, value: number): this\n```\n\n#### `importPoint()` — Import a `Point2D` object into the sketch.\n\n```ts\nimportPoint(pt: { x: number; y: number; }, fixed?: boolean): PointId\n```\n\n#### `importLine()` — Import a `Line2D` object into the sketch.\n\n```ts\nimportLine(l: { start: { x: number; y: number; }; end: { x: number; y: number; }; }, fixed?: boolean): LineId\n```\n\n#### `importRectangle()` — Import a `Rectangle2D` as four points and four lines.\n\n```ts\nimportRectangle(r: { vertices: [ { x: number; y: number; }, { x: number; y: number; }, { x: number; y: number; }, { x: number; y: number; } ]; }, fixed?: boolean): { ... }\n```\n\n#### `referencePoint()` — Add a fixed reference point at `(x, y)`.\n\n```ts\nreferencePoint(x: number, y: number): PointId\n```\n\n#### `referenceLine()` — Add a fixed reference line from `(x1, y1)` to `(x2, y2)`.\n\n```ts\nreferenceLine(x1: number, y1: number, x2: number, y2: number): LineId\n```\n\n#### `referenceFrom()` — Import a single named entity from a solved sketch as fixed reference geometry.\n\n```ts\nreferenceFrom(source: ConstraintSketch, entityId: string): PointId | LineId | null\n```\n\n#### `referenceAllFrom()` — Import all non-construction entities from a solved sketch as fixed references.\n\n```ts\nreferenceAllFrom(source: ConstraintSketch): { points: Map<string, PointId>; lines: Map<string, LineId>; }\n```\n\n**Solving**\n\n#### `constrain()` — Add a raw constraint object to the builder.\n\n```ts\nconstrain(constraint: Omit<SketchConstraint, \"id\">): this\n```\n\n#### `solve()` — Run the constraint solver and return a solved sketch.\n\nThe returned `ConstraintSketch` extends `Sketch` and can be used directly in all 3D operations (`extrude`, `revolve`, etc.). It also exposes `constraintMeta` with the solver status:\n\n```ts\nconst result = sk.solve();\nresult.constraintMeta.status; // 'fully' | 'under' | 'over' | 'over-redundant'\nresult.constraintMeta.dof; // 0 = fully constrained\nresult.constraintMeta.maxError; // residual — should be < 1e-6\nresult.inspect(); // human-readable summary\nresult.withUpdatedConstraint('cst-5', 120); // update a dimension without rebuilding\n```\n\n**Troubleshooting**\n\n- **Under-constrained (dof > 0)** — add `fix()`, `length()`, or other dimensional constraints.\n- **Over-constrained** — conflicting constraints are auto-rejected. Check `result.constraintMeta.constraints` and `result.inspect()`.\n- **maxError > 1e-6** — solver did not converge; check for contradictory constraints.\n\n```ts\nsolve(options?: SolveOptions): ConstraintSketch | Sketch\n```\n\n#### `solveConstraintsOnly()` — Run the solver without building a full `ConstraintSketch`.\n\nLighter than `solve()` — skips profile and DOF analysis. Useful for lightweight constraint validation or progress monitoring mid-construction.\n\n```ts\nsolveConstraintsOnly(options?: SolveOptions): { maxError: number; rejectedCount: number; definition: ConstraintDefinition; }\n```\n\n#### `route()` — Start a directional route from coordinates.\n\nReturns a [`RouteBuilder`](/docs/viewport#routebuilder) - describe the path with up/down/left/right/arcLeft/arcRight. Each method returns the entity ID (`LineId` or `ArcId`) for use in `sk.*` constraints.\n\n```js\nconst r = sk.route(0, 0);\nconst stem = r.up(18);\nr.arcLeft(8.9);\nconst neck = r.down();\nr.done();\nsk.offsetX(stem, neck, 10.8);\n```\n\n```ts\nroute(x: number, y: number): RouteBuilder\n```\n\n### `ConstraintSketch`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `constraintMeta` | `SketchConstraintMeta` | — |\n| `definition` | `ConstraintDefinition` | — |\n\n**Methods:**\n\n#### `detectArrangement()` — Enumerate all bounded regions formed by the line arrangement of this sketch. Construction lines are excluded. Regions are returned largest-first by area.\n\n```ts\ndetectArrangement(): Sketch[]\n```\n\n#### `detectArrangementRegion()` — Select the single arrangement region that contains the given seed point. Throws if no region contains the seed.\n\n```ts\ndetectArrangementRegion(_seed: [ number, number ]): Sketch\n```\n\n#### `toPolyline()` — Return the solved constrained path as a sampled 2D polyline.\n\nUse this when a construction rail was authored with `constrainedSketch()` and should feed another operation such as `Loft.pathOnXz(...)`. The sketch must contain exactly one profile path.\n\n```ts\ntoPolyline(samples?: number): [ number, number ][]\n```\n\n#### `withUpdatedConstraint()` — Re-solve the sketch after changing the value of one existing constraint.\n\nUse this for interactive dimension edits without rebuilding the whole sketch graph. It attempts a warm-started solve first, then falls back to a full solve if needed.\n\n```ts\nwithUpdatedConstraint(constraintId: string, value: number): ConstraintSketch\n```\n\n#### `inspect()` — Return a human-readable diagnostic string of the solved state.\n\n```ts\ninspect(): string\n```\n\n### `SketchGroupBuilder`\n\n#### `point()` — Add a point in local coordinates. Returns its globally-addressable PointId.\n\n```ts\npoint(lx: number, ly: number): PointId\n```\n\n#### `line()` — Connect two group points with a line. Both must be PointIds from this group.\n\n```ts\nline(a: PointId, b: PointId, name?: string): LineId\n```\n\n#### `fixRotation()` — Freeze rotation (theta). Group can still translate - 2 DOF remain.\n\n```ts\nfixRotation(): this\n```\n\n#### `fix()` — Freeze all 3 DOF - group is completely fixed.\n\n```ts\nfix(): this\n```\n\n#### `done()` — Finalize and register the group with the builder.\n\n```ts\ndone(): SketchGroupHandle\n```\n\n### `Point2D`\n\nAn immutable 2D point with measurement and construction helpers.\n\nUsed as construction geometry in sketches, constraints, and analytic measurements. All methods return new instances — `Point2D` is immutable.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `x` | `number` | — |\n| `y` | `number` | — |\n\n**Methods:**\n\n#### `distanceTo()` — Measure straight-line distance to another point.\n\n```ts\ndistanceTo(other: Point2D): number\n```\n\n#### `midpointTo()` — Compute the midpoint between this point and another point.\n\n```ts\nmidpointTo(other: Point2D): Point2D\n```\n\n#### `translate()` — Return a point shifted by the given delta.\n\n```ts\ntranslate(dx: number, dy: number): Point2D\n```\n\n#### `toTuple()` — Convert this point to a plain `[x, y]` tuple.\n\n```ts\ntoTuple(): [ number, number ]\n```\n\n### `Line2D`\n\nAn immutable 2D line segment with length, angle, intersection, and parallel helpers.\n\nProvides both segment-only (`intersectSegment`) and infinite-line (`intersect`) intersection queries. All methods return new instances.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `start` | `Point2D` | — |\n| `end` | `Point2D` | — |\n\n**Methods:**\n\n#### `length()` — Length of the line segment.\n\n```ts\nget length(): number\n```\n\n#### `midpoint()` — Midpoint of the line segment.\n\n```ts\nget midpoint(): Point2D\n```\n\n#### `angle()` — Direction angle in degrees, measured CCW from +X.\n\n```ts\nget angle(): number\n```\n\n#### `direction()` — Unit direction vector from start to end.\n\n```ts\nget direction(): [ number, number ]\n```\n\n#### `parallel()` — Create a parallel line offset by the given distance.\n\nPositive distance shifts to the left of the line direction.\n\n```ts\nparallel(distance: number): Line2D\n```\n\n#### `intersect()` — Intersect this line with another infinite line.\n\n```ts\nintersect(other: Line2D): Point2D | null\n```\n\n#### `intersectSegment()` — Intersect this line with another as bounded segments.\n\n```ts\nintersectSegment(other: Line2D): Point2D | null\n```\n\n#### `fromCoordinates()` — Create a line from raw coordinates.\n\n```ts\nstatic fromCoordinates(x1: number, y1: number, x2: number, y2: number): Line2D\n```\n\n#### `fromPointAndAngle()` — Create a line from a start point, angle, and length.\n\n```ts\nstatic fromPointAndAngle(origin: Point2D, angleDeg: number, length: number): Line2D\n```\n\n#### `fromPointAndDirection()` — Create a line from a start point, direction vector, and length.\n\n```ts\nstatic fromPointAndDirection(origin: Point2D, dir: [ number, number ], length: number): Line2D\n```\n\n### `Circle2D`\n\nAn immutable 2D circle with area, circumference, and extrusion support.\n\nExtruding a `Circle2D` produces a cylinder with named `top`, `bottom`, and `side` faces accessible via the topology API.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `center` | `Point2D` | — |\n| `radius` | `number` | — |\n\n**Methods:**\n\n#### `diameter()` — Diameter of the circle.\n\n```ts\nget diameter(): number\n```\n\n#### `circumference()` — Circumference of the circle.\n\n```ts\nget circumference(): number\n```\n\n#### `area()` — Area of the circle.\n\n```ts\nget area(): number\n```\n\n#### `pointAtAngle()` — Return a point on the circle at the given angle.\n\n```ts\npointAtAngle(angleDeg: number): Point2D\n```\n\n#### `translate()` — Return a translated circle.\n\n```ts\ntranslate(dx: number, dy: number): Circle2D\n```\n\n#### `toSketch()` — Convert this circle to a sketch profile.\n\n```ts\ntoSketch(segments?: number): Sketch\n```\n\n#### `extrude()` — Extrude the circle into a solid cylinder.\n\n```ts\nextrude(height: number, segments?: number): Shape\n```\n\n#### `fromCenterAndRadius()` — Create a circle from its center and radius.\n\n```ts\nstatic fromCenterAndRadius(center: Point2D, radius: number): Circle2D\n```\n\n#### `fromDiameter()` — Create a circle from its center and diameter.\n\n```ts\nstatic fromDiameter(center: Point2D, diameter: number): Circle2D\n```\n\n### `Rectangle2D`\n\nA rectangle with named sides, vertices, and extrusion support.\n\nSides are named based on the rectangle's local orientation at construction time. Vertices go: bottom-left, bottom-right, top-right, top-left (CCW).\n\nUse `rect()` for the normal centered sketch primitive. Use `Rectangle2D` when you need named sides/vertices, or an extrusion with tracked vertical edges such as `vert-br` for `filletTrackedEdge()` / `chamferTrackedEdge()`.\n\nExtruding a `Rectangle2D` produces a [`Shape`](/docs/core#shape) with named faces: `top`, `bottom`, `side-left`, `side-right`, `side-top`, `side-bottom`. These are accessible via the topology API (`.face()`, `.edge()`).\n\n```ts\nconst r = Rectangle2D.fromDimensions(0, 0, 100, 60);\nr.side('top'); r.side('left'); // Line2D\nr.vertex('top-left'); // Point2D\nr.width; r.height; r.center;\nconst [d1, d2] = r.diagonals(); // [bl-tr, br-tl]\n\nr.toSketch(); // Sketch (for 2D operations)\nr.extrude(20); // Shape with named faces\n\nRectangle2D.fromCenterAndDimensions(point(50, 30), 100, 60);\nRectangle2D.from2Corners(point(0, 0), point(100, 60));\nRectangle2D.from3Points(p1, p2, p3); // free-angle rectangle\n```\n\n#### `width()` — Width of the rectangle.\n\n```ts\nget width(): number\n```\n\n#### `height()` — Height of the rectangle.\n\n```ts\nget height(): number\n```\n\n#### `center()` — Geometric center of the rectangle.\n\n```ts\nget center(): Point2D\n```\n\n#### `side()` — Return a named side of the rectangle.\n\n```ts\nside(name: RectSide): Line2D\n```\n\n#### `sideAt()` — Return a side by index.\n\n```ts\nsideAt(index: number): Line2D\n```\n\n#### `vertex()` — Return a named vertex of the rectangle.\n\n```ts\nvertex(name: RectVertex): Point2D\n```\n\n#### `diagonals()` — Return the two diagonals of the rectangle.\n\n```ts\ndiagonals(): [ Line2D, Line2D ]\n```\n\n#### `toSketch()` — Convert the rectangle to a sketch profile.\n\n```ts\ntoSketch(): Sketch\n```\n\n#### `translate()` — Return a translated rectangle.\n\n```ts\ntranslate(dx: number, dy: number): Rectangle2D\n```\n\n#### `fromDimensions()` — Create an axis-aligned rectangle from origin corner plus width and height.\n\n```ts\nstatic fromDimensions(x: number, y: number, width: number, height: number): Rectangle2D\n```\n\n#### `fromCenterAndDimensions()` — Create a rectangle centered on a point.\n\n```ts\nstatic fromCenterAndDimensions(center: Point2D, width: number, height: number): Rectangle2D\n```\n\n#### `from2Corners()` — Create an axis-aligned rectangle from two opposite corners.\n\n```ts\nstatic from2Corners(p1: Point2D, p2: Point2D): Rectangle2D\n```\n\n#### `from3Points()` — Create a free-angle rectangle from three points.\n\n`p1` and `p2` define one edge, and `p3` chooses the perpendicular side.\n\n```ts\nstatic from3Points(p1: Point2D, p2: Point2D, p3: Point2D): Rectangle2D\n```\n\n#### `extrude()` — Extrude the rectangle into a solid prism with named topology.\n\n```ts\nextrude(height: number, up?: boolean): Shape\n```\n\n---\n\n<!-- generated/curves.md -->\n\n# Curves & Surfacing\n\nSmooth curves, lofted surfaces, swept solids, splines, and high-level product skins.\n\n## Contents\n\n- [Curves & Surfacing](#curves-surfacing) — `Loft.station`, `Loft.leftRail`, `Loft.rightRail`, `Loft.frontRail`, `Loft.backRail`, `Loft.centerRail`, `Loft.pathOnXz`, `Loft.pathOnYz`, `Loft.pathOnXy`, `Loft.withGuideRails`, `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`\n- [Surface Members](#surface-members) — `surfaceBand`, `SurfaceBody`\n- [Curve3D](#curve3d)\n- [NurbsCurve3D](#nurbscurve3d)\n- [NurbsSurface](#nurbssurface)\n- [PathBuilder](#pathbuilder) — Line Segments, Arcs, Curves, Closing & Output\n- [HermiteCurve3D](#hermitecurve3d)\n- [QuinticHermiteCurve3D](#quintichermitecurve3d)\n- [ProductSkin](#productskin)\n- [ProductSurfaceRef](#productsurfaceref)\n- [ProductSurfaceBuilder](#productsurfacebuilder)\n- [ProductSkinBuilder](#productskinbuilder)\n- [ProductStationBuilder](#productstationbuilder)\n- [ProductPanelBuilder](#productpanelbuilder)\n- [ProductRibbonBuilder](#productribbonbuilder)\n- [ProductSpoutBuilder](#productspoutbuilder)\n- [ProductHandleBuilder](#producthandlebuilder)\n- [ProductHandleFeature](#producthandlefeature)\n- [CylinderCarrier](#cylindercarrier)\n- [PlaneCarrier](#planecarrier)\n- [ProductSkinCarrier](#productskincarrier)\n- [SurfacePath](#surfacepath)\n- [SurfacePathBuilder](#surfacepathbuilder)\n- [SurfaceBand](#surfaceband)\n- [SurfaceBodyBuilder](#surfacebodybuilder)\n- [SurfaceMemberBuilder](#surfacememberbuilder)\n- [SurfaceJoinBuilder](#surfacejoinbuilder)\n- [CounterboreBuilder](#counterborebuilder)\n- [RoundedSlotBuilder](#roundedslotbuilder)\n- [Surface](#surface)\n- [Blend](#blend)\n- [Analysis](#analysis)\n- [Product](#product)\n- [Carrier](#carrier)\n- [SurfaceMembers](#surfacemembers)\n- [Slot](#slot)\n- [Counterbore](#counterbore)\n- [Ribs](#ribs)\n\n## Functions\n\n### Curves & Surfacing\n\n#### `Loft.station()` — Create a loft station from a 2D profile and an axis position.\n\n```ts\nLoft.station(profile: Sketch, position: number): LoftStation\n```\n\n`LoftStation`: `{ profile: Sketch, position: number }`\n\n#### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.\n\n```ts\nLoft.leftRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n`LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`\n\n#### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.\n\n```ts\nLoft.rightRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n#### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.\n\n```ts\nLoft.frontRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n#### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.\n\n```ts\nLoft.backRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n#### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.\n\n```ts\nLoft.centerRail(path: LoftGuideRailPath): LoftGuideRail\n```\n\n#### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.\n\nThe path's first coordinate becomes X and its second coordinate becomes Z. Use this for left/right silhouette rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).\n\n```ts\nLoft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]\n```\n\n#### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.\n\nThe path's first coordinate becomes Y and its second coordinate becomes Z. Use this for front/back crown rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).\n\n```ts\nLoft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]\n```\n\n#### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.\n\nThe path's first coordinate becomes X and its second coordinate becomes Y. Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.\n\n```ts\nLoft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]\n```\n\n#### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.\n\nStations define the cross-section family. Guide rails define the side or center paths the loft must pass through. With opposite side rails, the section is scaled to touch both rails. With one side rail, the section keeps its interpolated size unless a center rail is also present.\n\n```ts\nLoft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape\n```\n\n**`LoftOptions`**\n- `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.\n- `boundsPadding?: number` — Optional extra bounds padding.\n\n**`LoftWithGuideRailsOptions`** extends LoftOptions\n- `axis?: LoftAxis` — Primary station axis. Default Z.\n- `samples?: number` — Number of generated loft stations including ends. Default scales with station count.\n- `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.\n\n#### `hermiteTransitionG2()` — Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).\n\nThe curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`, and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`, with smooth G2-continuous interpolation matching position, tangent, and curvature.\n\n```ts\nhermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D\n```\n\n**`QuinticHermiteCurveEndpoint`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `point` | `Vec3` | Position |\n| `tangent` | `Vec3` | Tangent direction (will be normalized internally) |\n| `curvature?` | `Vec3` | Second derivative / curvature vector. Default [0, 0, 0]. |\n| `weight?` | `number` | Weight: scales tangent magnitude relative to chord length. Default 1.0. |\n\n#### `nurbs3d()` — Create a NURBS curve from control points.\n\nWith default options, creates a cubic non-rational B-spline with uniform clamped knots. Set `weights` for rational curves (exact circles, conics). Set `degree` for linear (1), quadratic (2), cubic (3), or higher-order curves.\n\n```js\n// Simple cubic B-spline through control points\nconst curve = nurbs3d([[0,0,0], [10,5,0], [20,-5,10], [30,0,5]]);\nconst tube = sweep(circle(2), curve);\n```\n\n```js\n// Rational quadratic — exact circular arc\nconst arc = nurbs3d(\n [[10,0,0], [10,10,0], [0,10,0]],\n { degree: 2, weights: [1, Math.SQRT1_2, 1] }\n);\n```\n\n```ts\nnurbs3d(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D\n```\n\n**`NurbsCurve3DOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `degree?` | `number` | Polynomial degree (default 3 = cubic). Must be ≥ 1. |\n| `weights?` | `number[]` | Rational weights, one per control point (default: all 1.0 = non-rational). |\n| `knots?` | `number[]` | Knot vector (default: uniform clamped). Must have length = controlPoints.length + degree + 1. |\n| `closed?` | `boolean` | Whether the curve is closed/periodic (default false). |\n\n#### `spline2d()` — Build a smooth Catmull-Rom spline sketch from 2D control points.\n\nA closed spline (default) returns a filled profile. An open spline requires a strokeWidth option to produce a solid sketch. Use tension (0..1, default 0.5) to control curve tightness.\n\n```ts\nspline2d(points: Vec2[], options?: Spline2DOptions): Sketch\n```\n\n**`Spline2DOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `closed?` | `boolean` | Closed loop (default true). |\n| `tension?` | `number` | Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. |\n| `samplesPerSegment?` | `number` | Samples per segment (minimum 3). Default 16. |\n| `strokeWidth?` | `number` | For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. |\n| `join?` | `\"Round\" \\| \"Square\"` | Stroke join for open splines. Default 'Round'. |\n\n#### `spline3d()` — Create a reusable 3D spline curve object (Catmull-Rom).\n\nThe returned Curve3D provides sample(), pointAt(t), tangentAt(t), and length() for downstream use in sweep() or manual path operations.\n\n```ts\nspline3d(points: Vec3[], options?: Spline3DOptions): Curve3D\n```\n\n**`Spline3DOptions`**\n- `closed?: boolean` — Closed loop (default false).\n- `tension?: number` — Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5.\n\n#### `loft()` — Loft between multiple sketches along Z stations.\n\nProfiles can differ in topology and vertex count: interpolation is done on signed-distance fields and meshed with level-set extraction. Heights must be strictly increasing. Compatible loft stacks can also stay on the maintained export-backend path.\n\nPerformance note: loft is significantly heavier than primitive/extrude/revolve. If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().\n\n```ts\nloft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape\n```\n\n#### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.\n\nUnlike loft() which only supports Z heights, loftAlongSpine() places each profile at a position along a 3D spine, oriented perpendicular to the spine tangent. This enables lofting along curved paths — e.g., a wing root-to-tip transition that follows a swept-back leading edge.\n\nThe tValues array specifies where each profile sits along the spine (0 = start, 1 = end). Must have the same length as profiles and be in [0, 1].\n\nInternally uses variableSweep infrastructure with SDF interpolation.\n\nPerformance note: uses level-set meshing, heavier than simple loft().\n\n```ts\nloftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3[], tValues: number[], options?: LoftAlongSpineOptions): Shape\n```\n\n**`LoftAlongSpineOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |\n| `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |\n| `boundsPadding?` | `number` | Optional extra bounds padding. |\n| `up?` | `Vec3` | Preferred \"up\" vector for local profile frame. Auto fallback is used near parallel segments. |\n\n#### `sweep()`\n\n```ts\nsweep(profile: Sketch, path: SweepPathInput, options?: SweepOptions): Shape\n```\n\n**`SweepOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `samples?` | `number` | Number of samples when path is a Curve3D. Default 48. |\n| `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |\n| `boundsPadding?` | `number` | Optional extra bounds padding. |\n| `up?` | `Vec3` | Preferred \"up\" vector for local profile frame. Auto fallback is used near parallel segments. |\n\n#### `variableSweep()` — Sweep a variable cross-section along a 3D spine curve.\n\nUnlike sweep(), which uses a single constant profile, variableSweep() interpolates between multiple profiles at different stations along the spine. This enables organic shapes like tapering tubes, bone-like structures, and sculptural forms.\n\nEach section specifies a t parameter (0 = start, 1 = end of spine) and a 2D profile sketch. The SDF-based level-set mesher smoothly blends between profiles at intermediate positions.\n\nPerformance note: like sweep(), this uses level-set meshing internally.\n\n```ts\nvariableSweep(spine: SweepPathInput, sections: VariableSweepSection[], options?: VariableSweepOptions): Shape\n```\n\n**`VariableSweepSection`**\n- `t: number` — Parameter along the spine (0 = start, 1 = end).\n- `profile: Sketch` — Cross-section profile at this station.\n\n**`VariableSweepOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |\n| `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |\n| `boundsPadding?` | `number` | Optional extra bounds padding. |\n| `up?` | `Vec3` | Preferred \"up\" vector for local profile frame. Auto fallback is used near parallel segments. |\n\n#### `nurbsSurface()` — Create a NURBS surface from a grid of control points.\n\nThe control grid is indexed as `controlGrid[u][v]` — each row is a curve in the V direction, and columns trace curves in the U direction.\n\nWith default options, creates a bicubic non-rational B-spline surface with uniform clamped knots.\n\n```js\n// Simple 4×4 control grid — a gently curved surface\nconst grid = [\n [[0,0,0], [10,0,2], [20,0,2], [30,0,0]],\n [[0,10,1], [10,10,5], [20,10,5], [30,10,1]],\n [[0,20,1], [10,20,5], [20,20,5], [30,20,1]],\n [[0,30,0], [10,30,2], [20,30,2], [30,30,0]],\n];\nconst surface = nurbsSurface(grid, { thickness: 2 });\n```\n\n```ts\nnurbsSurface(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape\n```\n\n**`NurbsSurfaceOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `degreeU?` | `number` | Degree in U direction (default 3). |\n| `degreeV?` | `number` | Degree in V direction (default 3). |\n| `weights?` | `number[][]` | Weights grid — same dimensions as controlGrid (default: all 1.0). |\n| `knotsU?` | `number[]` | Knot vector in U direction (default: uniform clamped). |\n| `knotsV?` | `number[]` | Knot vector in V direction (default: uniform clamped). |\n| `thickness?` | `number` | Sheet thickness — if > 0, thickens the surface into a solid (default 0 = surface only). |\n| `resolution?` | `number` | Tessellation resolution — points per direction (default 32). |\n| `domain?` | `SurfaceDomainOptions` | Optional rectangular parameter domain in normalized [0, 1] U/V space. |\n| `trim?` | `SurfaceTrimOptions` | Optional polygonal or NURBS-curve UV trim loops. Truck and OCCT support open trimmed surfaces; Manifold supports sampled thickened trimmed solids. |\n| `tessellation?` | `SurfaceTessellationOptions` | Optional Truck kernel tessellation controls for render mesh generation. |\n| `approximate?` | `boolean` | Explicit opt-in for sampled approximation paths on non-exact backends. |\n\n**`SurfaceDomainOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `uMin?` | `number` | Lower U parameter bound in normalized surface space (default 0). |\n| `uMax?` | `number` | Upper U parameter bound in normalized surface space (default 1). |\n| `vMin?` | `number` | Lower V parameter bound in normalized surface space (default 0). |\n| `vMax?` | `number` | Upper V parameter bound in normalized surface space (default 1). |\n\n**`SurfaceTrimOptions`**\n- `outer: SurfaceTrimLoopInput` — Outer trim loop in normalized post-domain UV space.\n- `holes?: SurfaceTrimLoopInput[]` — Optional hole loops in normalized post-domain UV space.\n\n**`SurfaceTessellationOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `mode?` | `\"uniform\" \\| \"adaptive\"` | `uniform` uses resolution directly; `adaptive` lets the Truck kernel refine open sheets from chord error. |\n| `tolerance?` | `number` | Target chord-error tolerance in model units for adaptive Truck tessellation. |\n| `minResolution?` | `number` | Minimum adaptive samples per direction. |\n| `maxResolution?` | `number` | Maximum adaptive samples per direction. Defaults to `resolution` when omitted. |\n\n#### `surfacePatch()` — Create a smooth surface patch from 4 boundary curves (Coons patch).\n\nThe four curves form the boundary of a quadrilateral patch:\n\n- bottom: u=0..1 at v=0 (from corner00 to corner10)\n- top: u=0..1 at v=1 (from corner01 to corner11)\n- left: v=0..1 at u=0 (from corner00 to corner01)\n- right: v=0..1 at u=1 (from corner10 to corner11)\n\nThe interior is filled using bilinear Coons patch interpolation: P(u,v) = Lc(u,v) + Ld(u,v) - B(u,v)\n\nThe result is a thin solid created by offsetting the surface mesh along its normals by the specified thickness.\n\nNote: curves should meet at corners. Small gaps are tolerated.\n\n```ts\nsurfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape\n```\n\n**`SurfacePatchOptions`**\n- `resolution?: number` — Number of samples along each direction. Default 24.\n- `thickness?: number` — Thickness of the generated solid. Default 0 for an open exact sheet.\n- `approximate?: boolean` — Allow explicit approximation for non-exact curve inputs such as Curve3D samples.\n\n#### `transitionCurve()` — Create a smooth transition curve between two edges.\n\nReturns a `HermiteCurve3D` that starts at `edgeA.point` tangent to `edgeA.tangent` and ends at `edgeB.point` tangent to `edgeB.tangent`.\n\nThe curve maintains G1 continuity (matching tangent direction) at both endpoints. Weight parameters control the shape of the transition.\n\n```js\n// Connect two edges with a balanced transition\nconst curve = transitionCurve(\n { point: [0, 0, 0], tangent: [1, 0, 0] },\n { point: [10, 5, 0], tangent: [1, 0, 0] },\n);\n```\n\n// Weighted: curve hugs edge A longer const weighted = transitionCurve( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 0], tangent: [1, 0, 0] }, { weightA: 2.0, weightB: 0.5 }, );\n\n```\n\n```ts\ntransitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D\n```\n\n**`TransitionEdge`**\n- `point: Vec3` — Connection point on the edge. Can be any point along the edge where the transition should connect.\n- `tangent: Vec3` — Tangent direction at the connection point. This is the direction the curve should initially follow when leaving this edge. For a straight edge, this is typically the edge direction pointing \"outward\" (away from the body of the edge, toward the other edge).\n- `normal?: Vec3` — Surface normal at the connection point (optional). Used as a hint for the sweep frame's up vector.\n\n**`TransitionCurveOptions`**\n- `weightA?: number` — Weight for the start edge. Controls tangent magnitude at the start. - 1.0 (default): balanced transition - > 1.0: curve follows start edge longer before turning - < 1.0: curve turns sooner at the start\n- `weightB?: number` — Weight for the end edge. Controls tangent magnitude at the end. - 1.0 (default): balanced transition - > 1.0: curve follows end edge longer before turning - < 1.0: curve turns sooner at the end\n- `samples?: number` — Number of sample points for the output polyline. Default 64. Higher values give smoother curves at the cost of more geometry.\n\n#### `transitionSurface()` — Create a solid transition surface between two edges by sweeping a profile along a Hermite transition curve.\n\nThis produces a watertight solid that smoothly connects the two edges. Works with both Manifold and OCCT backends.\n\n```js\n// Circular tube connecting two edges\nconst tube = transitionSurface(\n { point: [0, 0, 0], tangent: [1, 0, 0] },\n { point: [10, 5, 3], tangent: [0, 1, 0] },\n { radius: 0.5 },\n);\n```\n\n// Custom profile with weights const custom = transitionSurface( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 3], tangent: [0, 1, 0] }, { profile: mySketch, weightA: 1.5, weightB: 0.8 }, );\n\n```\n\n```ts\ntransitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape\n```\n\n\n**`TransitionSurfaceOptions`** extends TransitionCurveOptions\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `profile?` | `Sketch` | Cross-section profile to sweep along the transition curve. If omitted, a circular profile with `radius` is used. |\n| `radius?` | `number` | Radius of circular cross-section (used when `profile` is omitted). Default: 5% of chord length. |\n| `rectangleSection?` | `{ width: number; height: number; }` | Width and height for rectangular cross-section. Alternative to `radius` when `profile` is omitted. |\n| `up?` | `Vec3` | Preferred up vector for the sweep frame. Default: auto-detected. |\n| `edgeLength?` | `number` | Edge length for level-set meshing. Smaller = finer. |\n| `boundsPadding?` | `number` | Extra bounds padding for level-set meshing. |\n\n#### `connectEdges()` — Create a transition surface or solid bridge between two edge segments.\n\nTangents can be inferred from neighboring geometry or supplied explicitly through `options`. This is useful for loft-like blends where you want a direct connection between two edge spans.\n\n```ts\nconnectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape\n```\n\n**`EdgeSegment`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |\n| `direction` | `Vec3` | Normalized direction from start → end. |\n| `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |\n| `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |\n| `normalA` | `Vec3` | Normal of first adjacent face. |\n| `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |\n| `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |\n| `start`, `end`, `midpoint`, `length` | | — |\n\n\n**`ConnectEdgesOptions`** extends TransitionSurfaceOptions\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `endA?` | `EdgeEnd` | Which end of edge A to connect. Default: 'start'. |\n| `endB?` | `EdgeEnd` | Which end of edge B to connect. Default: 'start'. |\n| `tangentModeA?` | `TangentMode` | Tangent mode for edge A. Default: 'along'. |\n| `tangentModeB?` | `TangentMode` | Tangent mode for edge B. Default: 'along'. |\n| `tangentA?` | `Vec3` | Explicit tangent for edge A. |\n| `tangentB?` | `Vec3` | Explicit tangent for edge B. |\n| `flipA?` | `boolean` | Flip tangent A. |\n| `flipB?` | `boolean` | Flip tangent B. |\n\n### Surface Members\n\n#### `surfaceBand()`\n\n```ts\nsurfaceBand<C extends SurfaceCoordinate>(path: SurfacePath<C> | SurfacePathBuilder<C>, width: WidthProfile, cap?: SurfaceBandCap): SurfaceBand<C>\n```\n\n#### `SurfaceBody()` — Start a surface-member body builder for straps, inlays, guards, braces, cuffs, and similar physical members that live on a carrier surface.\n\n```js\nconst carrier = Carrier.cylinder('guard-envelope').diameter(84).height(36).clearance(2);\nconst guard = SurfaceBody('simple-guard')\n .carrier(carrier)\n .member('left-strut')\n .band()\n .path(carrier.path().from({ angle: -132, z: 6 }).to({ angle: -58, z: 18 }))\n .section({ width: 5.5, thickness: 2.8, edgeRadius: 0.6 })\n .member('right-strut')\n .mirrorOf('left-strut')\n .member('front-hoop')\n .band()\n .path(carrier.path().around({ z: 18, fromAngle: -58, toAngle: 58 }))\n .section({ width: 6.2, thickness: 3, edgeRadius: 0.7 })\n .join('left-strut', 'front-hoop').blend({ radius: 3.2 })\n .join('right-strut', 'front-hoop').blend({ radius: 3.2 })\n .build();\n```\n\n```ts\nSurfaceBody(name: string): SurfaceBodyBuilder\n```\n\n---\n\n## Classes\n\n### `Curve3D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `points` | `Vec3[]` | — |\n| `closed` | `boolean` | — |\n| `tension` | `number` | — |\n\n**Methods:**\n\n#### `sampleBySegment()` — Sample the curve with a fixed number of points per segment.\n\n```ts\nsampleBySegment(samplesPerSegment?: number): Vec3[]\n```\n\n#### `sample()` — Sample the curve to an approximate total point count.\n\n```ts\nsample(count?: number): Vec3[]\n```\n\n#### `pointAt()` — Return the position on the curve at normalized parameter `t` in `[0, 1]`. O(1), no allocations.\n\n```ts\npointAt(t: number): Vec3\n```\n\n#### `tangentAt()` — Return a unit tangent vector at normalized parameter `t` in `[0, 1]`. O(1), analytical derivative.\n\n```ts\ntangentAt(t: number): Vec3\n```\n\n#### `length()` — Approximate the curve length by polyline sampling.\n\n```ts\nlength(samples?: number): number\n```\n\n### `NurbsCurve3D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `controlPoints` | `Vec3[]` | — |\n| `weights` | `number[]` | — |\n| `knots` | `number[]` | — |\n| `degree` | `number` | — |\n| `closed` | `boolean` | — |\n\n**Methods:**\n\n#### `pointAt()` — Evaluate the curve at parameter t ∈ [0, 1]. Uses De Boor's algorithm — exact, O(degree²).\n\n```ts\npointAt(t: number): Vec3\n```\n\n#### `tangentAt()` — Evaluate the unit tangent vector at parameter t ∈ [0, 1].\n\n```ts\ntangentAt(t: number): Vec3\n```\n\n#### `sample()` — Sample the curve uniformly at `count` points.\n\n```ts\nsample(count?: number): Vec3[]\n```\n\n#### `sampleAdaptive()` — Sample with adaptive density — more points in high-curvature regions.\n\n```ts\nsampleAdaptive(minCount?: number, maxCount?: number): Vec3[]\n```\n\n#### `length()` — Approximate arc length by summing polyline segment lengths.\n\n```ts\nlength(samples?: number): number\n```\n\n#### `toPolyline()` — Convert to a format compatible with sweep() path input.\n\n```ts\ntoPolyline(samples?: number): Vec3[]\n```\n\n### `NurbsSurface`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `controlGrid` | `Vec3[][]` | — |\n| `weightsGrid` | `number[][]` | — |\n| `knotsU` | `number[]` | — |\n| `knotsV` | `number[]` | — |\n| `degreeU` | `number` | — |\n| `degreeV` | `number` | — |\n| `nU` | `number` | — |\n| `nV` | `number` | — |\n| `domain` | `SurfaceDomainCompilePlan` | — |\n\n**Methods:**\n\n#### `pointAt()` — Evaluate the surface at parameters (u, v) ∈ [0, 1]². Uses tensor product evaluation: evaluate basis functions in U and V independently.\n\n```ts\npointAt(u: number, v: number): Vec3\n```\n\n#### `normalAt()` — Evaluate the surface normal at (u, v) via cross product of partial derivatives.\n\n```ts\nnormalAt(u: number, v: number): Vec3\n```\n\n#### `tessellate()` — Tessellate the surface into a triangle mesh. Returns positions, normals, and triangle indices.\n\n```ts\ntessellate(resU?: number, resV?: number): { positions: Vec3[]; normals: Vec3[]; indices: number[]; }\n```\n\n### `PathBuilder`\n\n**Line Segments**\n\n#### `moveTo()` — Move the cursor to an absolute position without drawing a segment.\n\nWhen called after the initial [`path()`](/docs/sketch#path), this establishes the start of the outline. Calling `moveTo` again mid-path starts a new sub-path (hole in `close()`, separate segment for [`stroke()`](/docs/sketch#stroke)).\n\n```ts\nmoveTo(x: number, y: number): this\n```\n\n#### `lineTo()` — Draw a straight line from the current cursor to an absolute position.\n\n```ts\nlineTo(x: number, y: number): this\n```\n\n#### `lineH()` — Draw a horizontal line segment by `dx` units from the current cursor.\n\nPositive `dx` moves right; negative moves left.\n\n```ts\nlineH(dx: number): this\n```\n\n#### `lineV()` — Draw a vertical line segment by `dy` units from the current cursor.\n\nPositive `dy` moves up; negative moves down.\n\n```ts\nlineV(dy: number): this\n```\n\n#### `lineAngled()` — Draw a line at the given angle and length from the current cursor.\n\nAngle convention: `0°` points right (+X), `90°` points up (+Y).\n\n```ts\n// L-bracket with angled return\npath().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n```\n\n```ts\nlineAngled(length: number, degrees: number): this\n```\n\n**Arcs**\n\n#### `arc()` — Draw an arc defined by center, radius, and angle range (no trig needed). If the path has no segments yet, automatically moves to the arc start. Positive sweep (startDeg < endDeg) = CCW, negative = CW.\n\n```js\n// Arc centered at (10, 0), radius 50, from -30° to +30°\npath().arc(10, 0, 50, -30, 30).stroke(8, 'Round')\n```\n\n```ts\narc(cx: number, cy: number, radius: number, startDeg: number, endDeg: number): this\n```\n\n#### `arcTo()` — Draw a circular arc from the current position to (x, y) with the given radius. `clockwise=true` → arc curves to the right of the start→end direction. `clockwise=false` → arc curves to the left of the start→end direction.\n\n```ts\narcTo(x: number, y: number, radius: number, clockwise?: boolean): this\n```\n\n#### `tangentArcTo()` — G1-continuous arc — radius derived from current tangent + endpoint. Throws if endpoint is collinear with current direction.\n\n```ts\ntangentArcTo(x: number, y: number): this\n```\n\n**Curves**\n\n#### `bezierTo()` — Cubic bezier from current position to (x, y) via two control points.\n\n```ts\nbezierTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): this\n```\n\n**Closing & Output**\n\n#### `close()` — Close the path and return a filled [`Sketch`](/docs/sketch#sketch).\n\nThe winding of the polygon is automatically corrected to CCW (the expected orientation for ForgeCAD sketches). If the path contains multiple sub-paths (started with subsequent `moveTo` calls), the first sub-path is the outer contour and subsequent sub-paths become holes subtracted from it.\n\nEdge labels (assigned with `.label('name')`) are transferred to the resulting sketch and propagate through `extrude()`, `revolve()`, `loft()`, and `sweep()` into named faces on the resulting [`Shape`](/docs/core#shape).\n\n```ts\nconst triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();\n\n// With a hole (second sub-path)\nconst frame = path()\n .moveTo(0, 0).lineH(40).lineV(30).lineH(-40).close(); // outer\n // (hole would be added with another moveTo and line sequence before close)\n```\n\n```ts\nclose(): Sketch\n```\n\n#### `closeLabel()` — Label the closing segment and close the path. Shorthand for labeling the implicit line from the last point back to the start, then closing.\n\n```ts\ncloseLabel(name: string): Sketch\n```\n\n#### [`stroke()`](/docs/sketch#stroke) — Thicken an open polyline (centerline) into a solid filled profile with uniform width.\n\nExpands the path into a closed profile `width` units wide (half-width on each side of the centerline). Use `'Round'` for ribs, wire traces, and organic profiles — it adds semicircular endcaps and rounds joins. Use `'Square'` (default) for sharp miter joins without endcaps.\n\nNot the same as rounding corners of a closed polygon — for mixed sharp-and-rounded outlines, build the polygon first and apply [`filletCorners()`](/docs/sketch#filletcorners).\n\n```ts\n// Square-join L-bracket\nconst bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n\n// Round-join rib\nconst rib = path().moveTo(0, 0).lineH(60).stroke(6, 'Round');\n\n// Equivalent standalone form\nconst wire = stroke([[0, 0], [50, 0], [50, -70]], 4);\n```\n\nand semicircular endcaps.\n\n```ts\nstroke(width: number, join?: \"Round\" | \"Square\"): Sketch\n```\n\n#### `label()` — Label the most recently added segment. Labels are born here and grow into face names when the sketch is extruded, lofted, swept, or revolved.\n\nLabels must be unique within a path. Each segment can have at most one label.\n\n```ts\nlabel(name: string): this\n```\n\n**Other**\n\n#### `getX()` — Current cursor X position.\n\n```ts\ngetX(): number\n```\n\n#### `getY()` — Current cursor Y position.\n\n```ts\ngetY(): number\n```\n\n#### `lineBy()` — Draw a line by a relative `(dx, dy)` displacement from the current cursor.\n\n```ts\nlineBy(dx: number, dy: number): this\n```\n\n#### `arcBy()` — Draw an arc to a point offset from the current cursor.\n\n```ts\narcBy(dx: number, dy: number, radius: number, clockwise?: boolean): this\n```\n\n#### `bezierBy()` — Draw a cubic Bezier using control points relative to the current cursor.\n\n```ts\nbezierBy(dcp1x: number, dcp1y: number, dcp2x: number, dcp2y: number, dx: number, dy: number): this\n```\n\n#### `arcAround()` — Arc around a known center point, sweeping by the given angle. Radius is derived from the distance between the current position and the center. Positive sweep = CCW (math convention), negative = CW.\n\n```js\n// Arc 90° CCW around (50, 50)\npath().moveTo(70, 50).arcAround(50, 50, 90)\n// Arc 45° CW around the origin\npath().moveTo(10, 0).arcAround(0, 0, -45)\n```\n\n```ts\narcAround(cx: number, cy: number, sweepDeg: number): this\n```\n\n#### `arcAroundRelative()` — Arc around a center point given as an offset from the current position. `(dx, dy)` is the vector from the current point to the center. Positive sweep = CCW (math convention), negative = CW.\n\n```js\n// Arc 90° CCW around a center 20 units to the right\npath().moveTo(50, 50).arcAroundRelative(20, 0, 90)\n// Equivalent to: path().moveTo(50, 50).arcAround(70, 50, 90)\n```\n\n```ts\narcAroundRelative(dx: number, dy: number, sweepDeg: number): this\n```\n\n#### `smoothCapTo()` — Smooth three-arc end cap from the current position to (endX, endY). Inserts: small corner arc → large cap arc → small corner arc, all G1-continuous.\n\n```ts\nsmoothCapTo(endX: number, endY: number, cornerRadius: number, capRadius: number): this\n```\n\n#### `tangentBezierTo()` — G1-continuous cubic bezier — first control point is auto-derived from the current tangent direction. `weight` controls how far the auto-placed control point extends along the tangent (default: 1/3 of the chord).\n\nThe second control point `(cp2x, cp2y)` must be provided — it controls the arrival curvature. For a fully automatic smooth curve, see `smoothThrough`.\n\n```ts\ntangentBezierTo(cp2x: number, cp2y: number, x: number, y: number, weight?: number): this\n```\n\n#### `smoothThrough()` — Catmull-Rom spline through a list of waypoints from the current position. The current position is included as the first point. The last waypoint becomes the new cursor position.\n\n```ts\nsmoothThrough(waypoints: [ number, number ][], tension?: number): this\n```\n\n#### `nurbsTo()` — Rational B-spline edge to (x, y) with explicit control points and weights.\n\nThe control points define the B-spline shape between the current position and (x, y). The current position is NOT included in `controlPoints` — it is automatically prepended. The endpoint (x, y) is the last control point.\n\n```ts\nnurbsTo(controlPoints: [ number, number ][], opts?: { weights?: number[]; degree?: number; }): this\n```\n\n#### `exactArcTo()` — Exact circular arc to (x, y) using a rational quadratic NURBS.\n\nUnlike `arcTo()` which tessellates to a polyline, this preserves the exact arc definition. When extruded through the OCCT backend, it produces a true cylindrical face — not a faceted approximation.\n\n```ts\nexactArcTo(x: number, y: number, opts?: { radius?: number; clockwise?: boolean; }): this\n```\n\n#### [`fillet()`](/docs/core#fillet) — Round the last corner (the junction between the previous two segments) with a tangent arc of the given radius.\n\nMust be called after at least two line/arc segments that form a corner. The fillet trims back both segments and inserts a tangent arc.\n\n```js\npath().moveTo(0,0).lineTo(10,0).lineTo(10,10).fillet(2).lineTo(0,10).close()\n```\n\n```ts\nfillet(radius: number): this\n```\n\n#### [`chamfer()`](/docs/core#chamfer) — Chamfer the last corner with a straight cut of the given distance.\n\n```js\npath().moveTo(0,0).lineTo(10,0).lineTo(10,10).chamfer(2).lineTo(0,10).close()\n```\n\n```ts\nchamfer(distance: number): this\n```\n\n#### `mirror()` — Mirror all existing segments across an axis and append the mirrored copy in reverse order, creating a symmetric path. The axis passes through the current cursor position.\n\n'y' mirrors across the local Y-axis (flips X), or `[nx, ny]` for an arbitrary axis direction.\n\n```js\n// Build right half, mirror to get full symmetric profile\npath().moveTo(0,0).lineTo(10,0).lineTo(10,5).mirror('x').close()\n```\n\n```ts\nmirror(axis: \"x\" | \"y\" | [ number, number ]): this\n```\n\n#### `toPolyline()` — Return the open path as a sampled 2D polyline.\n\nThis is for construction geometry such as guide rails, measured centerlines, and curve-driven helpers where the authored path should stay open instead of becoming a filled sketch or stroked profile.\n\n```ts\nconst rail = path()\n .moveTo(24, 0)\n .bezierTo(32, 44, 28, 92, 18, 120)\n .toPolyline();\n```\n\n```ts\ntoPolyline(): [ number, number ][]\n```\n\n#### `closeOffset()` — Close the path and return an offset version of the filled Sketch. Positive delta expands outward, negative shrinks inward.\n\n```ts\ncloseOffset(delta: number, join?: \"Round\" | \"Square\" | \"Miter\"): Sketch\n```\n\n### `HermiteCurve3D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `p0` | `Vec3` | Start position |\n| `p1` | `Vec3` | End position |\n| `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |\n| `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |\n| `chordLength` | `number` | Chord length (straight-line distance between endpoints) |\n\n**Methods:**\n\n#### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]\n\n```ts\npointAt(t: number): Vec3\n```\n\n#### `tangentAt()` — Evaluate tangent (first derivative) at parameter t ∈ [0, 1]\n\n```ts\ntangentAt(t: number): Vec3\n```\n\n#### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]\n\n```ts\ncurvatureAt(t: number): Vec3\n```\n\n#### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.\n\n```ts\nsample(count?: number): Vec3[]\n```\n\n#### `length()` — Approximate arc length by sampling.\n\n```ts\nlength(samples?: number): number\n```\n\n#### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.\n\n```ts\nsampleAdaptive(minCount?: number, maxCount?: number): Vec3[]\n```\n\n#### `toPolyline()` — Convert to a format compatible with sweep() path input.\n\n```ts\ntoPolyline(samples?: number): Vec3[]\n```\n\n### `QuinticHermiteCurve3D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `p0` | `Vec3` | Start position |\n| `p1` | `Vec3` | End position |\n| `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |\n| `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |\n| `c0` | `Vec3` | Scaled second derivative at start (curvature * weight² * chordLength²) |\n| `c1` | `Vec3` | Scaled second derivative at end (curvature * weight² * chordLength²) |\n| `chordLength` | `number` | Chord length (straight-line distance between endpoints) |\n\n**Methods:**\n\n#### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]\n\n```ts\npointAt(t: number): Vec3\n```\n\n#### `tangentAt()` — Evaluate tangent (first derivative, normalized) at parameter t ∈ [0, 1]\n\n```ts\ntangentAt(t: number): Vec3\n```\n\n#### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]\n\n```ts\ncurvatureAt(t: number): Vec3\n```\n\n#### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.\n\n```ts\nsample(count?: number): Vec3[]\n```\n\n#### `length()` — Approximate arc length by sampling.\n\n```ts\nlength(samples?: number): number\n```\n\n#### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.\n\n```ts\nsampleAdaptive(minCount?: number, maxCount?: number): Vec3[]\n```\n\n#### `toPolyline()` — Convert to a format compatible with sweep() path input.\n\n```ts\ntoPolyline(samples?: number): Vec3[]\n```\n\n### `ProductSkin`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n| `shape` | `Shape` | — |\n| `axis` | `ProductSkinAxis` | — |\n| `stations` | `ProductStationSpec[]` | — |\n| `rails` | `Record<string, ProductRailSpec>` | — |\n\n**Methods:**\n\n#### [`toShape()`](/docs/sdf#toshape) — Return the renderable shape generated for this product skin.\n\n```ts\ntoShape(): Shape\n```\n\n#### `with()` — Create a group containing this skin plus named child details.\n\n```ts\nwith(...children: GroupInput[]): ShapeGroup\n```\n\n#### `integrate()` — Boolean-union structural details into the skin body.\n\n```ts\nintegrate(...details: Shape[]): Shape\n```\n\n#### `uv()` — Create a side/u/v surface-ref query on this skin.\n\n```ts\nuv(side: ProductSkinSide, u?: number, v?: number): ProductSkinRefQuery\n```\n\n**`ProductSkinSide`** — Semantic side of a ProductSkin. `back` is accepted as an alias for `rear`.\n\n`\"left\" | \"right\" | \"top\" | \"bottom\" | \"front\" | \"rear\" | \"back\"`\n\n**`ProductSkinRefQuery`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `side` | `ProductSkinSide` | Side of the product skin. `front` is the minimum axis cap, `rear`/`back` is the maximum axis cap. |\n| `u?` | `number` | Across-side parameter for side refs. Defaults to 0.5. |\n| `v?` | `number` | Along-axis parameter, 0 at the first cap and 1 at the rear/back cap. Defaults to 0.5. |\n| `offset?` | `number` | Positive distance away from the surface along the resolved normal. |\n\n#### `ref()` — Resolve a named ref published with Product.skin().refs(...).\n\n```ts\nref(name: string): ProductSurfaceRef\n```\n\n#### `curveOnSurface()` — Create a sampled curve as a sequence of surface refs on this skin.\n\n```ts\ncurveOnSurface(name: string, points: Array<Partial<ProductSkinRefQuery> & { side: ProductSkinSide; }>): ProductSurfaceRef[]\n```\n\n#### `surface()` — Create a fluent surface helper for refs and conformal features on one side of this skin.\n\nUse this when several refs or ribbons share the same skin side; side-local helpers keep path points concise and make it harder to mix sides accidentally.\n\n```ts\nsurface(side: ProductSkinSide): ProductSurfaceBuilder\n```\n\n#### `stationAt()` — Interpolate center, width, and depth at a normalized v or absolute axis value.\n\n```ts\nstationAt(vOrAxis: number): { ... }\n```\n\n**`ProductProfileKind`**\n\n`\"oval\" | \"roundedRect\" | \"circle\" | \"superEllipse\" | \"custom\"`\n\n#### `frame()` — Build a local surface frame from a side/u/v query.\n\n```ts\nframe(query: ProductSkinRefQuery): ProductSurfaceFrame\n```\n\n`ProductSurfaceFrame`: `{ point: Vec3, normal: Vec3, tangentU: Vec3, tangentV: Vec3, matrix: Mat4, skin: string }`\n\n### `ProductSurfaceRef`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string | undefined` | — |\n\n**Methods:**\n\n#### `frame()` — Resolve this semantic surface ref into a point, normal, tangents, and placement matrix.\n\n```ts\nframe(overrides?: Partial<ProductSkinRefQuery>): ProductSurfaceFrame\n```\n\n#### `with()` — Return a copy of this ref with side/u/v/offset overrides.\n\n```ts\nwith(overrides: Partial<ProductSkinRefQuery>): ProductSurfaceRef\n```\n\n#### `attach()` — Place a detail shape or group on this ref's local surface frame.\n\n```ts\nattach(detail: Shape | ShapeGroup, options?: ProductAttachOptions): Shape | ShapeGroup\n```\n\n`ProductAttachOptions`: `{ offset?: number, inset?: number }`\n\n#### `querySpec()` — Return the serializable side/u/v query behind this ref.\n\n```ts\nquerySpec(): ProductSkinRefQuery\n```\n\n### `ProductSurfaceBuilder`\n\nFluent helper bound to one ProductSkin side for refs and side-local conformal features.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `side` | `ProductSkinSide` | — |\n\n**Methods:**\n\n#### `ref()` — Create a ref on this skin side.\n\n```ts\nref(u?: number, v?: number, offset?: number): ProductSurfaceRef\n```\n\n#### `uv()` — Create a side/u/v query on this skin side.\n\n```ts\nuv(u?: number, v?: number, offset?: number): ProductSkinRefQuery\n```\n\n#### `frame()` — Resolve a point/frame on this surface using the builder's side.\n\n```ts\nframe(query?: Partial<ProductSkinRefQuery>): ProductSurfaceFrame\n```\n\n#### `ribbon()` — Start a conformal ribbon on this skin side.\n\nPath points use side-local `u`/`v` coordinates; this builder supplies the side. The returned ProductRibbonBuilder is already bound to the source skin and can be further configured before build(). Use `widthSamples` >= 3 when the ribbon must visibly wrap over curved product sections instead of behaving like a flat strip.\n\n```ts\nribbon(name: string, points: ProductSurfacePathPoint[], options?: ProductRibbonBuildOptions): ProductRibbonBuilder\n```\n\n**`ProductSurfacePathPoint`** — Side-local path point for Product.surface(side).ribbon(...); the surface helper supplies `side`.\n- `u?: number` — Across-side parameter on the bound side. Defaults to 0.5.\n- `v?: number` — Along-axis parameter, 0 at the first cap and 1 at the rear/back cap. Defaults to 0.5.\n- `offset?: number` — Positive distance away from the surface along the resolved normal.\n\n**`ProductRibbonBuildOptions`** — Options shared by Product.ribbon() builders and Product.surface(...).ribbon(...).\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `width?` | `number` | Width across the surface in millimeters. |\n| `thickness?` | `number` | Solid thickness outward from the source surface in millimeters. |\n| `offset?` | `number` | Positive clearance between the source surface and the ribbon's inner face. |\n| `samples?` | `number` | Samples along the ribbon path. Higher values bend more smoothly. |\n| `widthSamples?` | `number` | Samples across the ribbon width. Use 3+ to visibly wrap over curved cross-sections. |\n| `resolution?` | `number` | Tessellation resolution passed to the lowered NURBS surface. |\n| `material?` | `ProductMaterial` | Apply a product material preset to the ribbon. |\n| `color?` | `string` | Apply a simple color override. |\n\n`ProductMaterial`: `{ color?: string, material?: ShapeMaterialProps }`\n\n**`ShapeMaterialProps`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `metalness?` | `number` | Metalness factor (0 = dielectric, 1 = metal). Default: 0.05 |\n| `roughness?` | `number` | Roughness factor (0 = mirror, 1 = fully diffuse). Default: 0.35 |\n| `emissive?` | `string` | Emissive glow color (hex string, e.g. \"#ff6b35\"). |\n| `emissiveIntensity?` | `number` | Emissive intensity multiplier. Default: 1 |\n| `opacity?` | `number` | Opacity (0 = fully transparent, 1 = fully opaque). Default: 1 |\n| `wireframe?` | `boolean` | Render as wireframe. Default: false |\n| `clearcoat?` | `number` | Clearcoat intensity (0–1). Default: 0.1 |\n| `clearcoatRoughness?` | `number` | Clearcoat roughness (0–1). Default: 0.4 |\n| `transmission?` | `number` | Glass/translucency transmission factor (0–1). Renderer support depends on target. |\n| `ior?` | `number` | Index of refraction for transmissive materials. Typical glass is ~1.45. |\n| `thickness?` | `number` | Approximate transmissive volume thickness in model units. |\n| `specularIntensity?` | `number` | Specular highlight intensity (0–1). |\n| `specularColor?` | `string` | Specular highlight tint. |\n| `reflectivity?` | `number` | Reflection strength for supported renderers (0–1). |\n\n### `ProductSkinBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `axis()` — Choose the primary station axis for the skin loft.\n\n```ts\naxis(axis: ProductSkinAxis): this\n```\n\n**`ProductSkinAxis`** — Primary world axis used to order ProductSkin loft stations.\n\n`\"X\" | \"Y\" | \"Z\"`\n\n#### `stations()` — Set named cross-section stations for the product skin.\n\n```ts\nstations(stations: Array<ProductStationBuilder | ProductStationSpec>): this\n```\n\n`ProductStationSpec`: `{ name: string, center: Vec3, profile: ProductStationProfile, crown?: number }`\n\n`ProductStationProfile`: `{ sketch: Sketch, width: number, depth: number, kind: ProductProfileKind, radius?: number, exponent?: number }`\n\n#### `rails()` — Attach named guide rails for product-skin construction and downstream surface references.\n\n```ts\nrails(rails: Record<string, ProductRailSpec>): this\n```\n\n`ProductRailSpec`: `{ kind: ProductRailKind, points: Vec3[], degree?: number, name?: string }`\n\n**`ProductRailKind`**\n\n`\"bezier\" | \"nurbs\" | \"polyline\"`\n\n#### `ref()` — Publish a named semantic surface ref on the skin.\n\n```ts\nref(name: string, query: ProductSkinRefQuery): this\n```\n\n#### `refs()` — Publish multiple named semantic surface refs on the skin.\n\n```ts\nrefs(refs: Record<string, ProductSkinRefQuery>): this\n```\n\n#### `uv()` — Create a side/u/v surface-ref query for use in refs(...) or Product.ref(...).\n\n```ts\nuv(side: ProductSkinSide, u?: number, v?: number): ProductSkinRefQuery\n```\n\n#### `material()` — Apply a product material preset to the lowered skin.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `color()` — Apply a simple color override to the lowered skin.\n\n```ts\ncolor(color: string): this\n```\n\n#### `edgeLength()` — Set the sampled loft target edge length.\n\n```ts\nedgeLength(value: number): this\n```\n\n#### `wall()` — Record intended wall thickness for product design metadata. Use explicit shelling when the model needs real inner-wall geometry.\n\n```ts\nwall(thickness: number): this\n```\n\n#### `build()` — Lower stations and refs into a ProductSkin body.\n\n```ts\nbuild(): ProductSkin\n```\n\n### `ProductStationBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `at()` — Position this station in world coordinates.\n\n```ts\nat(point: Vec3): this\n```\n\n#### `z()` — Convenience for traditional Z-up section stacks.\n\n```ts\nz(z: number): this\n```\n\n#### `y()` — Convenience for product bodies running front-to-back along Y.\n\n```ts\ny(y: number): this\n```\n\n#### `x()` — Convenience for product bodies running left-to-right along X.\n\n```ts\nx(x: number): this\n```\n\n#### `oval()` — Use an oval cross-section with full width and depth dimensions.\n\n```ts\noval(width: number, depth: number, options?: { segments?: number; }): this\n```\n\n#### `superEllipse()` — Use a superellipse cross-section for soft-square product surfaces.\n\n```ts\nsuperEllipse(width: number, depth: number, options?: ProductStationSuperEllipseOptions): this\n```\n\n`ProductStationSuperEllipseOptions`: `{ segments?: number, exponent?: number }`\n\n#### [`roundedRect()`](/docs/sketch#roundedrect) — Use a rounded-rectangle cross-section with the given corner radius.\n\n```ts\nroundedRect(width: number, depth: number, radius: number): this\n```\n\n#### [`circle()`](/docs/sketch#circle) — Use a circular cross-section from a full diameter.\n\n```ts\ncircle(diameter: number, options?: { segments?: number; }): this\n```\n\n#### `custom()` — Use a custom 2D sketch as the station cross-section.\n\n```ts\ncustom(sketch: Sketch, width: number, depth: number): this\n```\n\n#### `crown()` — Set the station crown amount for soft product-section intent.\n\n```ts\ncrown(amount: number): this\n```\n\n#### `toSpec()` — Return the immutable station spec consumed by Product.skin().\n\n```ts\ntoSpec(): ProductStationSpec\n```\n\n### `ProductPanelBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `rounded()` — Use a rounded rectangle panel profile.\n\n```ts\nrounded(width: number, height: number, radius?: number): this\n```\n\n#### `oval()` — Use an oval panel profile.\n\n```ts\noval(width: number, height: number): this\n```\n\n#### `profile()` — Use a custom 2D panel profile.\n\n```ts\nprofile(profile: Sketch): this\n```\n\n#### `thickness()` — Set panel extrusion thickness.\n\n```ts\nthickness(thickness: number): this\n```\n\n#### `material()` — Apply a product material preset to the panel.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `color()` — Apply a simple color override to the panel.\n\n```ts\ncolor(color: string): this\n```\n\n#### `build()` — Build the panel in local coordinates.\n\n```ts\nbuild(): Shape\n```\n\n#### `attachTo()` — Build and attach this panel to a ProductSurfaceRef.\n\n```ts\nattachTo(ref: ProductRefInput, options?: ProductPanelAttachOptions): Shape\n```\n\n**`ProductRefInput`**\n\n`ProductSurfaceRef`\n\n\n`ProductPanelAttachOptions`: `{ at?: Partial<ProductSkinRefQuery>, thickness?: number, material?: ProductMaterial, color?: string }`\n\n### `ProductRibbonBuilder`\n\nBuilder for thin trim, label, grip, and split-line features that bend with a ProductSkin surface.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `on()` — Follow a ProductSkin with side/u/v path queries or refs.\n\nThis is the highest-fidelity mode because every interpolated sample is resolved through ProductSkin.frame(), so the ribbon bends along the selected side as station width/depth changes. All query path points must stay on one side; split side transitions into separate ribbons.\n\n```ts\non(skin: ProductSkin, points: ProductRibbonPathPoint[], options?: ProductRibbonBuildOptions): this\n```\n\n**`ProductRibbonPathPoint`** — Path point for Product.ribbon().on(...): either a side/u/v query or a resolved surface ref.\n\n`ProductSkinRefQuery | ProductSurfaceRef`\n\n#### `fromRefs()` — Follow explicit surface refs.\n\nUseful for named refs or paths assembled elsewhere. The builder resolves each ref frame and interpolates between those frames; use on(skin, points) when you need full skin-side sampling between sparse control points.\n\n```ts\nfromRefs(points: ProductSurfaceRef[], options?: ProductRibbonBuildOptions): this\n```\n\n#### `width()` — Set ribbon width in millimeters.\n\n```ts\nwidth(width: number): this\n```\n\n#### `thickness()` — Set solid thickness outward from the source surface in millimeters.\n\n```ts\nthickness(thickness: number): this\n```\n\n#### `offset()` — Set positive clearance between the source surface and the ribbon's inner face.\n\n```ts\noffset(offset: number): this\n```\n\n#### `samples()` — Set samples along the path.\n\n```ts\nsamples(samples: number): this\n```\n\n#### `widthSamples()` — Set samples across the width. Use 3+ to bend over curved cross-sections.\n\n```ts\nwidthSamples(samples: number): this\n```\n\n#### `resolution()` — Set NURBS tessellation resolution.\n\n```ts\nresolution(resolution: number): this\n```\n\n#### `material()` — Apply a product material preset.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `color()` — Apply a simple color override.\n\n```ts\ncolor(color: string): this\n```\n\n#### `build()` — Build a conformal ribbon as a thin NURBS surface solid.\n\n```ts\nbuild(options?: ProductRibbonBuildOptions): Shape\n```\n\n### `ProductSpoutBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `from()` — Set the skin ref this spout projects from.\n\n```ts\nfrom(ref: ProductSurfaceRef): this\n```\n\n#### `sections()` — Set local spout section profiles from root to mouth.\n\n```ts\nsections(sections: Array<Sketch | ProductStationBuilder | ProductStationSpec>): this\n```\n\n#### `projection()` — Set the projection length along the source ref normal.\n\n```ts\nprojection(length: number): this\n```\n\n#### `edgeLength()` — Set the sampled loft target edge length for the spout.\n\n```ts\nedgeLength(value: number): this\n```\n\n#### `material()` — Apply a product material preset to the spout.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `color()` — Apply a simple color override to the spout.\n\n```ts\ncolor(color: string): this\n```\n\n#### `build()` — Build the spout in local coordinates.\n\n```ts\nbuild(): Shape\n```\n\n#### `attach()` — Build and place the spout on its source ref.\n\n```ts\nattach(options?: ProductAttachOptions): Shape\n```\n\n### `ProductHandleBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `between()` — Set the upper body ref and lower world anchor for the handle.\n\n```ts\nbetween(upper: ProductSurfaceRef, lower: Vec3): this\n```\n\n#### `spine()` — Set an explicit handle centerline from points or a rail spec.\n\n```ts\nspine(points: Vec3[] | ProductRailSpec): this\n```\n\n#### `grip()` — Set the grip cross-section profile.\n\n```ts\ngrip(profile: Sketch): this\n```\n\n#### `material()` — Apply a product material preset to the grip.\n\n```ts\nmaterial(material: ProductMaterial): this\n```\n\n#### `padMaterial()` — Apply a product material preset to handle landing pads.\n\n```ts\npadMaterial(material: ProductMaterial): this\n```\n\n#### `edgeLength()` — Set the sampled loft target edge length for the grip.\n\n```ts\nedgeLength(value: number): this\n```\n\n#### `build()` — Build the handle grip and landing pads.\n\n```ts\nbuild(): ProductHandleFeature\n```\n\n### `ProductHandleFeature`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `grip` | `Shape` | — |\n| `upperPad` | `Shape` | — |\n| `lowerPad` | `Shape` | — |\n\n**Methods:**\n\n#### `structural()` — Return the physical shapes that make up this handle feature.\n\n```ts\nstructural(): Shape[]\n```\n\n#### [`toShape()`](/docs/sdf#toshape) — Boolean-union the handle feature into a single shape.\n\n```ts\ntoShape(): Shape\n```\n\n#### `toGroup()` — Return the handle as a named ShapeGroup preserving child colors.\n\n```ts\ntoGroup(): ShapeGroup\n```\n\n### `CylinderCarrier`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n| `kind` | `\"cylinder\"` | — |\n\n**Methods:**\n\n#### `diameter()`\n\n```ts\ndiameter(value: number): this\n```\n\n#### `radius()`\n\n```ts\nradius(value: number): this\n```\n\n#### `height()`\n\n```ts\nheight(value: number): this\n```\n\n#### `clearance()`\n\n```ts\nclearance(value: number): this\n```\n\n#### `center()`\n\n```ts\ncenter(point: Vec3): this\n```\n\n#### [`path()`](/docs/sketch#path)\n\n```ts\npath(): SurfacePathBuilder<CylinderSurfaceCoordinate>\n```\n\n#### `anchor()`\n\n```ts\nanchor(angle: number, z?: number, options?: { offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `front()`\n\n```ts\nfront(options?: { z?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `back()`\n\n```ts\nback(options?: { z?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `left()`\n\n```ts\nleft(options?: { z?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `right()`\n\n```ts\nright(options?: { z?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `top()`\n\n```ts\ntop(options?: { angle?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `bottom()`\n\n```ts\nbottom(options?: { angle?: number; offset?: number; }): SurfaceAnchor<CylinderSurfaceCoordinate>\n```\n\n#### `pointAt()`\n\n```ts\npointAt(coordinate: CylinderSurfaceCoordinate): Vec3\n```\n\n#### `mirrorPoint()`\n\n```ts\nmirrorPoint(point: Vec3): Vec3\n```\n\n#### `normalAt()`\n\n```ts\nnormalAt(coordinate: CylinderSurfaceCoordinate): Vec3\n```\n\n#### `tangentAt()`\n\n```ts\ntangentAt(coordinate: CylinderSurfaceCoordinate, tangentHint?: Vec3): Vec3\n```\n\n#### `frameAt()`\n\n```ts\nframeAt(coordinate: CylinderSurfaceCoordinate, tangentHint?: Vec3): SurfaceFrame\n```\n\n#### `bounds()`\n\n```ts\nbounds(): SurfaceBounds\n```\n\n#### `offset()`\n\n```ts\noffset(distance: number): CylinderCarrier\n```\n\n#### `mirrorCoordinate()`\n\n```ts\nmirrorCoordinate(coordinate: CylinderSurfaceCoordinate): CylinderSurfaceCoordinate\n```\n\n#### `radiusValueWithClearance()`\n\n```ts\nradiusValueWithClearance(): number\n```\n\n### `PlaneCarrier`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n| `kind` | `\"plane\"` | — |\n\n**Methods:**\n\n#### `size()`\n\n```ts\nsize(width: number, height: number): this\n```\n\n#### `origin()`\n\n```ts\norigin(point: Vec3): this\n```\n\n#### `normal()`\n\n```ts\nnormal(normal: Vec3): this\n```\n\n#### [`path()`](/docs/sketch#path)\n\n```ts\npath(): SurfacePathBuilder<PlaneSurfaceCoordinate>\n```\n\n#### `anchor()`\n\n```ts\nanchor(x?: number, y?: number, options?: { offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `left()`\n\n```ts\nleft(options?: { y?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `right()`\n\n```ts\nright(options?: { y?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `top()`\n\n```ts\ntop(options?: { x?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `bottom()`\n\n```ts\nbottom(options?: { x?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCoordinate>\n```\n\n#### `pointAt()`\n\n```ts\npointAt(coordinate: PlaneSurfaceCoordinate): Vec3\n```\n\n#### `mirrorPoint()`\n\n```ts\nmirrorPoint(point: Vec3): Vec3\n```\n\n#### `normalAt()`\n\n```ts\nnormalAt(): Vec3\n```\n\n#### `tangentAt()`\n\n```ts\ntangentAt(coordinate: PlaneSurfaceCoordinate, tangentHint?: Vec3): Vec3\n```\n\n#### `frameAt()`\n\n```ts\nframeAt(coordinate: PlaneSurfaceCoordinate, tangentHint?: Vec3): SurfaceFrame\n```\n\n#### `bounds()`\n\n```ts\nbounds(): SurfaceBounds\n```\n\n#### `offset()`\n\n```ts\noffset(distance: number): PlaneCarrier\n```\n\n#### `mirrorCoordinate()`\n\n```ts\nmirrorCoordinate(coordinate: PlaneSurfaceCoordinate): PlaneSurfaceCoordinate\n```\n\n### `ProductSkinCarrier`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `skin` | `ProductSkin` | — |\n| `name` | `string` | — |\n| `kind` | `\"productSkin\"` | — |\n\n**Methods:**\n\n#### `surface()`\n\n```ts\nsurface(side: ProductSkinSide): ProductSkinCarrier\n```\n\n#### [`path()`](/docs/sketch#path)\n\n```ts\npath(): SurfacePathBuilder<ProductSkinSurfaceCoordinate>\n```\n\n`ProductSkinSurfaceCoordinate`: `{ kind?: \"productSkin\", side?: ProductSkinSide, u?: number, v?: number, offset?: number }`\n\n#### `sideTransition()` — Return matching side-local coordinates for an explicit split-member transition.\n\nEach SurfacePath still stays on one ProductSkin side. Use this helper to create one member ending on `from`, another starting on `to`, then join named anchors. The helper validates normalized `v`, non-empty names, adjacency, and physical coincidence before returning anchors.\n\n```ts\nsideTransition(fromSide: ProductSkinSide, toSide: ProductSkinSide, input?: ProductSkinSideTransitionInput): ProductSkinSideTransition\n```\n\n`ProductSkinSideTransitionInput`: `{ name?: string, v?: number, offset?: number }`\n\n`ProductSkinSideTransition`: `{ name?: string, from: ProductSkinSurfaceCoordinate, to: ProductSkinSurfaceCoordinate }`\n\n#### `sideTransitionChain()` — Return a sequence of matching side-local coordinates for an explicit multi-side split-member route.\n\nEach adjacent side pair becomes one named transition. Build one member per side segment, add transition anchors at each returned pair, then join the anchors. The same validation as `sideTransition()` applies to every adjacent pair.\n\n```ts\nsideTransitionChain(sides: ProductSkinSide[], input?: ProductSkinSideTransitionInput): ProductSkinSideTransition[]\n```\n\n#### `sideRoute()` — Return side-local member segments for a generated multi-side split-member route.\n\nThe route still compiles as explicit members plus named-anchor joins. This helper only generates the per-side segment endpoints and transition names.\n\n```ts\nsideRoute(input: ProductSkinSideRouteInput): ProductSkinSideRoute\n```\n\n**`ProductSkinSideRouteInput`**: `name?: string`, `sides: ProductSkinSide[]`, `from: ProductSkinSurfaceCoordinate`, `to: ProductSkinSurfaceCoordinate`, `v?: number`, `offset?: number`\n\n`ProductSkinSideRoute`: `{ name?: string, transitions: ProductSkinSideTransition[], segments: ProductSkinSideRouteSegment[] }`\n\n**`ProductSkinSideRouteSegment`**: `name: string`, `side: ProductSkinSide`, `from: ProductSkinSurfaceCoordinate`, `to: ProductSkinSurfaceCoordinate`, `startAnchorName?: string`, `endAnchorName?: string`\n\n#### `pointAt()`\n\n```ts\npointAt(coordinate: ProductSkinSurfaceCoordinate): Vec3\n```\n\n#### `mirrorPoint()`\n\n```ts\nmirrorPoint(point: Vec3): Vec3\n```\n\n#### `normalAt()`\n\n```ts\nnormalAt(coordinate: ProductSkinSurfaceCoordinate): Vec3\n```\n\n#### `tangentAt()`\n\n```ts\ntangentAt(coordinate: ProductSkinSurfaceCoordinate, tangentHint?: Vec3): Vec3\n```\n\n#### `frameAt()`\n\n```ts\nframeAt(coordinate: ProductSkinSurfaceCoordinate, tangentHint?: Vec3): SurfaceFrame\n```\n\n**`SurfaceFrame`**: `point: Vec3`, `normal: Vec3`, `tangentAlong: Vec3`, `tangentAcross: Vec3`, `matrix: Mat4`, `carrier: string`, `representation: SurfaceCarrierKind | string`, `coordinate: SurfaceCoordinate`\n\n#### `bounds()`\n\n```ts\nbounds(): SurfaceBounds\n```\n\n**`SurfaceBounds`**: `u?: [ number, number ]`, `v?: [ number, number ]`, `angle?: [ number, number ]`, `z?: [ number, number ]`, `x?: [ number, number ]`, `y?: [ number, number ]`\n\n#### `offset()`\n\n```ts\noffset(distance: number): ProductSkinCarrier\n```\n\n#### `mirrorCoordinate()`\n\n```ts\nmirrorCoordinate(coordinate: ProductSkinSurfaceCoordinate): ProductSkinSurfaceCoordinate\n```\n\n### `SurfacePath`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `carrier` | `CarrierSurface<C>` | — |\n| `points` | `C[]` | — |\n| `closedValue` | `boolean` | — |\n\n**Methods:**\n\n#### `closed()`\n\n```ts\nclosed(): SurfacePath<C>\n```\n\n#### `mirror()`\n\n```ts\nmirror(): SurfacePath<C>\n```\n\n#### `coordinateAt()`\n\n```ts\ncoordinateAt(t: number): C\n```\n\n#### `sample()`\n\n```ts\nsample(count?: number): SurfacePathSample<C>[]\n```\n\n#### `length()`\n\n```ts\nlength(samples?: number): number\n```\n\n### `SurfacePathBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `carrier` | `CarrierSurface<C>` | — |\n\n**Methods:**\n\n#### `from()`\n\n```ts\nfrom(coordinate: C): this\n```\n\n#### `through()`\n\n```ts\nthrough(coordinate: C): this\n```\n\n#### `to()`\n\n```ts\nto(coordinate: C): this\n```\n\n#### `around()`\n\n```ts\naround(input: { z: number; fromAngle: number; toAngle: number; offset?: number; }): this\n```\n\n#### `closed()`\n\n```ts\nclosed(): this\n```\n\n#### `mirror()`\n\n```ts\nmirror(): SurfacePath<C>\n```\n\n#### `build()`\n\n```ts\nbuild(): SurfacePath<C>\n```\n\n#### `sample()`\n\n```ts\nsample(count?: number): SurfacePathSample<C>[]\n```\n\n### `SurfaceBand`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `centerPath` | `SurfacePath<C>` | — |\n| `widthProfile` | `WidthProfile` | — |\n| `capStyle` | `SurfaceBandCap` | — |\n\n**Methods:**\n\n#### `widthAt()`\n\n```ts\nwidthAt(t: number): number\n```\n\n#### `boundaries()`\n\n```ts\nboundaries(samples?: number): SurfaceBandBoundarySample[]\n```\n\n#### `withHole()` — Return a new band with a named member-local rounded-slot hole region recorded as inspectable intent.\n\n```ts\nwithHole(name: string, input: SurfaceBandHoleInput): SurfaceBand<C>\n```\n\n#### `holes()` — Resolve recorded hole regions into member-local across/along loops.\n\n```ts\nholes(): SurfaceBandHoleRegion[]\n```\n\n### `SurfaceBodyBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `carrier()`\n\n```ts\ncarrier(carrier: CarrierSurface): this\n```\n\n#### `member()`\n\n```ts\nmember(name: string): SurfaceMemberBuilder\n```\n\n#### `join()`\n\n```ts\njoin(from: string, to: string | string[]): SurfaceJoinBuilder\n```\n\n#### `autoJoinAtSharedAnchors()`\n\n```ts\nautoJoinAtSharedAnchors(): this\n```\n\n#### `build()`\n\n```ts\nbuild(): Shape | ShapeGroup\n```\n\n### `SurfaceMemberBuilder`\n\n#### `plate()`\n\n```ts\nplate(): this\n```\n\n#### `band()`\n\n```ts\nband(): this\n```\n\n#### `at()`\n\n```ts\nat(anchor: SurfaceAnchor<C>): this\n```\n\n#### `size()`\n\n```ts\nsize(width: number, height: number): this\n```\n\n#### [`path()`](/docs/sketch#path)\n\n```ts\npath(path: SurfacePath<C> | SurfacePathBuilder<C>): this\n```\n\n#### `section()`\n\n```ts\nsection(section: MemberSectionInput): this\n```\n\n#### `cap()`\n\n```ts\ncap(style: SurfaceBandCap): this\n```\n\n#### [`slot()`](/docs/sketch#slot)\n\n```ts\nslot(name: string, feature: MemberFeature | RoundedSlotBuilder): this\n```\n\n#### `cutout()`\n\n```ts\ncutout(name: string, feature: MemberFeature | RoundedSlotBuilder): this\n```\n\n#### `counterbore()`\n\n```ts\ncounterbore(name: string, feature: MemberFeature | CounterboreBuilder): this\n```\n\n#### `anchorAt()` — Add a named anchor at a carrier surface coordinate for explicit member joins.\n\n```ts\nanchorAt(name: string, coordinate: C | SurfaceAnchor<C>): this\n```\n\n#### `features()`\n\n```ts\nfeatures(features: MemberFeature | MemberFeature[]): this\n```\n\n#### `profile()`\n\n```ts\nprofile(name: string, options?: { depth?: number; height?: number; }): this\n```\n\n#### `mirrorOf()`\n\n```ts\nmirrorOf(memberName: string): SurfaceBodyBuilder\n```\n\n#### `member()`\n\n```ts\nmember(name: string): SurfaceMemberBuilder\n```\n\n#### `join()`\n\n```ts\njoin(from: string, to: string | string[]): SurfaceJoinBuilder\n```\n\n#### `autoJoinAtSharedAnchors()`\n\n```ts\nautoJoinAtSharedAnchors(): SurfaceBodyBuilder\n```\n\n#### `build()`\n\n```ts\nbuild(): Shape | ShapeGroup\n```\n\n### `SurfaceJoinBuilder`\n\n#### `betweenAnchors()` — Select named anchors on the source and target members before lowering this join.\n\n```ts\nbetweenAnchors(fromAnchor: string, toAnchor: string): this\n```\n\n#### `blend()`\n\n```ts\nblend(input?: { radius?: number; style?: string; priority?: number; continuity?: string; }): SurfaceBodyBuilder\n```\n\n### `CounterboreBuilder`\n\n#### `at()`\n\n```ts\nat(input: { along?: number; across?: number; z?: number; }): this\n```\n\n#### `named()`\n\n```ts\nnamed(name: string): MemberFeature\n```\n\n#### `toFeature()`\n\n```ts\ntoFeature(name?: string): MemberFeature\n```\n\n### `RoundedSlotBuilder`\n\n#### `verticalTravel()`\n\n```ts\nverticalTravel(value: number): this\n```\n\n#### `at()`\n\n```ts\nat(input: { along?: number; across?: number; z?: number; }): this\n```\n\n#### `named()`\n\n```ts\nnamed(name: string): MemberFeature\n```\n\n#### `toFeature()`\n\n```ts\ntoFeature(name?: string): MemberFeature\n```\n\n---\n\n## Constants\n\n### `Surface`\n\n- `Nurbs(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape`\n- `Ruled(curveA: ExactCurveInput, curveB: ExactCurveInput, options?: SurfaceCommonOptions): Shape`\n- `Patch(curves: { bottom: ExactCurveInput; top: ExactCurveInput; left: ExactCurveInput; right: ExactCurveInput; }, options?: SurfacePatchOptions): Shape`\n- `Boundary(input: SurfaceBoundaryInput): Shape`\n- `Fill(input: SurfaceFillInput): Shape`\n- `Sew(shapes: Shape[], options?: { tolerance?: number; }): Shape`\n- `Extend(shape: Shape, options: SurfaceExtendOptions): Shape`\n- `Trim(shape: Shape, tool: Shape | SurfacePlaneOp): Shape`\n- `Split(shape: Shape, tool: Shape | SurfacePlaneOp): [ Shape, Shape ]`\n- `Match(shape: Shape, options: { edge: \"u0\" | \"u1\" | \"v0\" | \"v1\"; target: EdgeRef; continuity?: SurfaceContinuity; }): Shape`\n- `MatchEdge(shape: Shape, options: { edge: \"u0\" | \"u1\" | \"v0\" | \"v1\"; target: EdgeRef; continuity?: SurfaceContinuity; }): Shape`\n\n### `Blend`\n\n- `Edge(options: BlendEdgeOptions): Shape`\n- `Surface(options: BlendSurfaceOptions): Shape`\n\n### `Analysis`\n\n- `EdgeContinuity(shape: Shape, options?: EdgeContinuityThresholds): EdgeContinuityReport`\n- `SurfaceContinuity(shape: Shape, options?: EdgeContinuityThresholds): EdgeContinuityReport`\n- `CurvatureComb(input: NurbsCurve3D | EdgeRef, options?: { samples?: number; }): CurvatureSample[]`\n- `SurfaceHealth(shape: Shape, options?: { tinyEdgeThreshold?: number; sliverThreshold?: number; }): SurfaceHealthReport`\n\n### `Product`\n\n- `skin(name: string): ProductSkinBuilder` — Start a named product skin builder.\n- `station(name: string): ProductStationBuilder` — Start a named cross-section station for Product.skin(...).stations(...).\n- `rail: { bezier(points: Vec3[], options?: { name?: string; }): ProductRailSpec; nurbs(points: Vec3[], options?: { degree?: number; name?: string; }): ProductRailSpec; polyline(points: Vec3[], options?: { name?: string; }): ProductRailSpec; }` — Namespaced rail builders for product skin guide rails and handle spines.\n- `profiles: { ... }` — Namespaced product profile helpers for stations, panels, trims, and openings.\n- `materials: { ... }` — Namespaced product material presets for molded plastic, rubber, metal, and transparent parts.\n- `applyMaterial(shape: Shape, preset: ProductMaterial | undefined): Shape` — Apply a product material preset to a Shape.\n- `scenePreset(name: ProductScenePreset): void` — Apply an opinionated scene preset for product review renders.\n- `ovalProfile(width: number, depth: number, options?: ProductProfileOptions): Sketch` — Create a centered oval profile from full width/depth dimensions.\n- `roundedRectProfile(width: number, depth: number, radius: number): Sketch` — Create a centered rounded-rectangle profile.\n- `circleProfile(diameter: number, options?: ProductProfileOptions): Sketch` — Create a centered circular profile from full diameter.\n- `superEllipseProfile(width: number, depth: number, options?: ProductSuperEllipseOptions): Sketch` — Create a centered superellipse profile for soft-square product sections.\n- `profileSize(sketch: Sketch): { width: number; depth: number; }` — Measure the width and depth of a 2D profile sketch.\n- `describeProfile(sketch: Sketch, kind?: ProductProfileKind, radius?: number): ProductProfileDescriptor` — Describe a custom sketch as a product profile.\n- `scaleProfileTo(sketch: Sketch, width: number, depth: number): Sketch` — Scale an existing profile sketch to a target width/depth.\n- `ref(skin: ProductSkin, query: ProductSkinRefQuery): ProductSurfaceRef` — Create an ad-hoc ProductSurfaceRef from a skin and side/u/v query.\n- `surface(skin: ProductSkin, side: ProductSkinSide): ProductSurfaceBuilder` — Create a fluent surface helper for refs and conformal features on one side of a skin. Equivalent to skin.surface(side), useful when writing in Product.* namespace style.\n- `panel(name: string): ProductPanelBuilder` — Start a panel feature builder.\n- `ribbon(name: string): ProductRibbonBuilder` — Start a conformal ribbon/trim builder for details that should bend with a ProductSkin. Call .on(skin, points) for side/u/v sampling or .fromRefs(points) for explicit surface refs, then configure width, thickness, offset, sampling, material, and color before build().\n- `spout(name: string): ProductSpoutBuilder` — Start a spout/nozzle feature builder.\n- `handle(name: string): ProductHandleBuilder` — Start a handle feature builder.\n- `place(detail: Shape | ShapeGroup, ref: ProductRefInput, options?: ProductAttachOptions): Shape | ShapeGroup` — Place a shape or group on a ProductSurfaceRef.\n- `landing(name: string, radius?: number, material?: ProductMaterial): Shape` — Small blended landing volume for manual structural bridges and connection proofs.\n\n### `Carrier`\n\n- `cylinder(name: string): CylinderCarrier` — Create an analytic cylinder carrier for bottles, limbs, tubes, guards, and cuffs.\n- `plane(name: string): PlaneCarrier` — Create an analytic plane carrier for plates and local flat construction surfaces.\n- `productSkin(skin: ProductSkin): ProductSkinCarrier` — Adapt an existing ProductSkin into the general surface-member carrier protocol.\n\n### `SurfaceMembers`\n\n- `Body(name: string): SurfaceBodyBuilder` — Start a surface-member body builder for straps, inlays, guards, braces, cuffs, and similar physical members that live on a carrier surface.\n- `Band: typeof SurfaceBand`\n- `band<C extends SurfaceCoordinate>(path: SurfacePath<C> | SurfacePathBuilder<C>, width: WidthProfile, cap?: SurfaceBandCap): SurfaceBand<C>`\n\n### `Slot`\n\n- `rounded(input: { length: number; width: number; }): RoundedSlotBuilder` — Create a rounded member-local slot feature.\n\n### `Counterbore`\n\n- `cylindrical(input: { diameter: number; clearanceDiameter: number; depth: number; }): CounterboreBuilder` — Create a cylindrical member-local counterbore feature.\n\n### `Ribs`\n\n- `repeated(input: { count: number; height: number; }): MemberFeature` — Create repeated ribs that belong to a surface member before lowering.\n\n---\n\n<!-- generated/assembly.md -->\n\n# Assembly API\n\nKinematic assemblies, joints, couplings, and robot export.\n\n## Contents\n\n- [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`, `joint`\n- [Assembly](#assembly) — Structure, Connectors, References, Joints, Solving\n- [ImportedAssembly](#importedassembly)\n- [SolvedAssembly](#solvedassembly)\n- [MateBuilder](#matebuilder)\n\n## Functions\n\n### Assembly & Joints\n\n#### `bomToCsv()` — Convert an array of BOM rows into a CSV string.\n\nProduces a CSV with columns: `part`, `qty`, `material`, `process`, `tolerance`, `notes`. String values are quoted and internal double-quotes are escaped. Prefer calling `solvedAssembly.bomCsv()` directly — this function is exposed for custom BOM processing.\n\n```ts\nbomToCsv(rows: BomRow[]): string\n```\n\n**`BomRow`**: `part: string`, `qty: number`, `material?: string`, `process?: string`, `tolerance?: string`, `notes?: string`, `metadata?: PartMetadata`\n\n**`PartMetadata`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `tags?` | `string \\| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |\n| `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |\n\n#### `assembly()` — Create an assembly container with named parts and joints for kinematic mechanisms.\n\n**Use this from iteration 1 for any model with moving parts.** Hinges, sliders, gears, articulated fingers, doors — all start with `assembly()`, not with manual rotation math. Don't build a static \"extended pose\" first and refactor to an assembly later: joint sliders, animations, sweeps, collision detection, and robot export all flow from the kinematic graph.\n\nAn assembly models a mechanism as a directed graph of parts connected by joints. Parts are the nodes; joints are directed edges from parent to child. The graph must be a forest (no cycles). Root parts (those with no incoming joint) are anchored to world space.\n\nThree joint types are supported: `'revolute'` (hinge), `'prismatic'` (slider), and `'fixed'` (rigid attachment). Use `addPart()` to add geometry, `addJoint()` (or the shorthands `addRevolute()`, `addPrismatic()`, `addFixed()`) to connect parts, and `solve()` to compute world-space positions at a given joint state.\n\nThe higher-level `connect()` API uses declared **connectors** to compute joint frames automatically. The `match()` API uses typed connectors (with gender and type metadata) for automatic compatibility validation and joint creation.\n\nFor multi-file assemblies, a file that returns an `Assembly` is importable via [`require()`](/docs/core#require) and yields an `ImportedAssembly`. Use `mergeInto()` to flatten a sub-assembly into a parent assembly.\n\n```ts\nconst mech = assembly(\"Arm\")\n .addPart(\"base\", box(80, 80, 20).translate(0, 0, -10), {\n metadata: { material: \"PETG\", process: \"FDM\", qty: 1 },\n })\n .addPart(\"link\", box(140, 24, 24).translate(0, -12, -12))\n .addRevolute(\"shoulder\", \"base\", \"link\", {\n axis: [0, 1, 0],\n min: -30, max: 120, default: 25,\n frame: Transform.identity().translate(0, 0, 20),\n });\n\nreturn mech; // auto-solved at defaults, renders all parts\n```\n\n```ts\nassembly(name?: string): Assembly\n```\n\n#### `joint()` — Create a revolute joint that auto-generates a parameter slider and rotates the shape.\n\nThis is a convenience wrapper for single-shape, single-joint use cases. It calls `param()` to create a named angle slider, then applies `rotateAroundAxis()` to the shape. Use the full `Assembly` API for mechanisms with multiple parts and joints.\n\n```ts\nconst arm = joint(\"Shoulder\", armShape, [0, 0, 20], {\n axis: [0, 1, 0],\n min: -30, max: 120, default: 25,\n});\nreturn arm;\n```\n\n```ts\njoint(name: string, shape: Shape, pivot: [ number, number, number ], opts?: RevoluteJointOpts): Shape\n```\n\n`RevoluteJointOpts`: `{ axis?: [ number, number, number ], min?: number, max?: number, default?: number, unit?: string, reverse?: boolean }`\n\n---\n\n## Classes\n\n### `Assembly`\n\nContainer for a kinematic mechanism made up of named parts and joints.\n\nAn assembly is a directed graph where **parts** are nodes and **joints** are directed edges from parent to child. The graph must be a forest (one or more trees with no cycles). Root parts (no incoming joint) are fixed to world space.\n\nEach joint carries a `frame` transform (from the parent part frame to the joint's zero-state frame) and a motion formula:\n\n```\nchildWorld = parentWorld × frame × motion(value) × childBase\n```\n\nThree joint types are supported:\n\n- **revolute** — rotates the child around an axis by `value` degrees\n- **prismatic** — translates the child along an axis by `value` mm\n- **fixed** — no motion; rigidly attaches the child at `frame`\n\n**Quick start**\n\n```ts\nconst mech = assembly(\"Arm\")\n .addPart(\"base\", box(80, 80, 20).translate(0, 0, -10))\n .addPart(\"link\", box(140, 24, 24).translate(0, -12, -12))\n .addJoint(\"shoulder\", \"revolute\", \"base\", \"link\", {\n axis: [0, 1, 0],\n min: -30, max: 120, default: 25,\n frame: Transform.identity().translate(0, 0, 20),\n });\n\nreturn mech; // auto-solved at defaults\n```\n\nReturning an unsolved `Assembly` auto-solves at default joint values. Return a `SolvedAssembly` directly for a specific pose:\n\n```ts\nreturn mech.solve({ shoulder: 60 });\n```\n\n**Return types**\n\n| Return value | Standalone | `require()` result type |\n|---|---|---|\n| `Assembly` (unsolved) | yes | `ImportedAssembly` |\n| `SolvedAssembly` | yes | `SolvedAssembly` |\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Connectors**\n\n#### `usedConnectorRefs()` — Connector refs (e.g. \"PartName.connectorName\") consumed by connect/match calls.\n\n```ts\nget usedConnectorRefs(): ReadonlySet<string>\n```\n\n#### `withConnectors()` — Attach named connectors to a specific part or the assembly as a whole.\n\nConnectors declared this way are in the part's local coordinate system. They are captured automatically if the incoming [`Shape`](/docs/core#shape) already has connectors via `shape.withConnectors(...)`, but you can also add or override connectors after the fact with this method.\n\nUse the single-argument overload to attach assembly-level connectors — these are exposed when this assembly is imported as a sub-assembly.\n\n```ts\nwithConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly\n```\n\n#### `getConnectors()` — Get connectors declared on a part in part-local space.\n\n```ts\ngetConnectors(partName: string): ConnectorMap\n```\n\n#### `getConnector()` — Parse a \"PartName.connectorName\" reference and return the resolved connector. Throws descriptive errors if the part or connector doesn't exist.\n\n```ts\ngetConnector(ref: string): { partName: string; connectorName: string; connector: ConnectorDef; }\n```\n\n#### `connect()` — Connect two parts by aligning their declared connectors, automatically computing frame and axis.\n\nConnector references use `\"PartName.connectorName\"` format. The system aligns connector origins (child connector lands exactly on parent connector) and derives the joint frame and axis from the connector geometry — no manual `frame` or `axis` math needed.\n\n**Face-to-face convention:** Connectors always meet face-to-face, like a USB plug meeting a socket. Each connector's axis points \"outward\" from its part. When two connectors mate, the system brings them together so their axes oppose (anti-parallel). This is the same convention used by `matchTo()`.\n\nFor a revolute joint (hinge), both connectors' axes should point outward from their respective parts along the hinge line. For a prismatic joint (slider), both axes should point along the slide direction from their part's perspective.\n\nThe joint type is inferred from the connector's `kind` field if not specified in `options`.\n\nWhen connectors are defined with `start`/`end`, you can control which point on each connector meets via `align` / `parentAlign` / `childAlign` (`'start'`, `'middle'`, `'end'`).\n\nUse `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore). For mechanisms where parts share an axis but are deliberately spaced apart, use `addRevolute()` with pre-positioned parts instead.\n\n```ts\n// Hinge: both axes point outward along the hinge line\nconst frame = box(100, 10, 80).withConnectors({\n hinge: connector(\"hinge\", { origin: [0, 0, 40], axis: [0, 0, 1] }),\n});\nconst door = box(60, 4, 80).withConnectors({\n hinge: connector(\"hinge\", { origin: [0, 0, 40], axis: [0, 0, -1] }),\n});\nassembly(\"Door\")\n .addPart(\"Frame\", frame)\n .addPart(\"Door\", door)\n .connect(\"Frame.hinge\", \"Door.hinge\", { as: \"swing\", min: 0, max: 110 });\n```\n\n```ts\nconnect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly\n```\n\n#### `match()` — Auto-create a joint by matching typed connectors between two parts.\n\nConnectors can carry a `connectorType` string and a `gender` (`'male'`, `'female'`, or `'neutral'`). `match()` validates type and gender compatibility (use `{ force: true }` to skip validation) and creates the joint automatically from the connector's `kind` metadata.\n\nThe `pairs` map is `{ childConnector: parentConnector }`. The first pair drives joint creation; additional pairs are validated but do not create additional joints (they constrain the same rigid connection).\n\nDefine connectors on shapes with `shape.withConnectors(...)`:\n\n```ts\nconst door = doorShape.withConnectors({\n hinge_top: connector.male(\"hinge\", { origin: [0, 0, 90], axis: [0, 0, 1] }),\n hinge_bottom: connector.male(\"hinge\", { origin: [0, 0, 10], axis: [0, 0, 1] }),\n});\n```\n\nThen match in the assembly:\n\n```ts\nconst mech = assembly(\"Door\")\n .addPart(\"Frame\", frame)\n .addPart(\"Door\", door)\n .match(\"Door\", \"Frame\", { hinge_top: \"hinge_top\", hinge_bottom: \"hinge_bottom\" });\n// Revolute connectors → auto-creates revolute joint. No manual addRevolute needed.\n```\n\n```ts\nmatch(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & { as?: string; }): Assembly\n```\n\n**References**\n\n#### `withReferences()` — Attach named placement reference points to this assembly. These are surfaced automatically on the ImportedAssembly when this file is imported via require(), so consumers can use placeReference() without re-declaring them. Returns a new Assembly — does not mutate.\n\n```ts\nwithReferences(refs: Pick<PlacementReferenceInput, \"points\">): Assembly\n```\n\n**Solving**\n\n#### `solve()` — Solve the assembly at the given joint state and return positioned parts.\n\nPerforms a depth-first traversal of the joint graph. Each joint's value is taken from `state`, falling back to `defaultValue`. Coupled joints compute their value from source joints. Values outside `[min, max]` are clamped (a warning is added to `SolvedAssembly.warnings()`).\n\nIf mate constraints were registered via `mate()`, the solver runs a pre-pass to derive base transforms, then the kinematic DFS applies joints on top of those positions.\n\n**Pitfall — [`jointsView`](/docs/viewport#jointsview) double-rotation:** When calling `toJointsView()`, always solve at the rest pose (all joint values = 0 or default). Solving at a non-zero angle and then animating will double-rotate parts. Use the `defaults` option on `toJointsView()` to set the initial display angle instead.\n\nThis pitfall only applies when `toJointsView()` is active. If you only want a static posed result, return the solved assembly directly and skip `toJointsView()`.\n\n**Example — static posed output (no `toJointsView()`)**\n\n```ts\nreturn mech.solve({ shoulder: 45, elbow: -20 });\n```\n\n```ts\nsolve(state?: JointState): SolvedAssembly\n```\n\n**Other**\n\n#### `mate()` — Register mate constraints between parts. Constraints are solved during `solve()` to derive part positions and explode hints. Part references use \"partName:featureName\" format.\n\n```ts\nmate(fn: (m: MateBuilder) => void): Assembly\n```\n\n#### `addFrame()` — Add a virtual reference frame (no geometry) to the assembly graph.\n\nUseful when you need a named pivot point or coordinate frame that has no visual geometry. Acts like a zero-volume part and can be connected to other parts via joints.\n\n```ts\naddFrame(name: string, options?: PartOptions): Assembly\n```\n\n#### `addPart()` — Add a named part to the assembly.\n\nConnectors declared on the part (via `withConnectors()`) are captured automatically. Parts are positioned at world origin by default unless a `transform` is provided in `options`. For root parts (no incoming joint), `transform` is their final world position.\n\nWhen a part is a [`ShapeGroup`](/docs/core#shapegroup), name the group children explicitly to get readable viewport labels (e.g. `\"Base Assembly.Body\"` instead of `\"Base Assembly.1\"`):\n\n```ts\nconst housing = group(\n { name: \"Body\", shape: body },\n { name: \"Lid\", shape: lid },\n);\nassembly.addPart(\"Base Assembly\", housing);\n```\n\n```ts\naddPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly\n```\n\n#### `addJoint()` — Add a kinematic joint between a parent and child part.\n\n`frame` is a transform from the **parent part frame** to the **joint frame at zero state**. The child's world position is computed as:\n\n```\nchildWorld = parentWorld × frame × motion(value) × childBase\n```\n\nFor revolute joints `value` is in degrees; for prismatic joints `value` is in mm. Coupled joints (see `addJointCoupling`) ignore the `state` value passed to `solve()` and compute their value from source joints.\n\n```ts\naddJoint(name: string, type: JointType, parent: string, child: string, options?: JointOptions): Assembly\n```\n\n#### `addRevolute()` — Shorthand for `addJoint(name, 'revolute', parent, child, options)`.\n\n```ts\naddRevolute(name: string, parent: string, child: string, options?: JointOptions): Assembly\n```\n\n#### `addPrismatic()` — Shorthand for `addJoint(name, 'prismatic', parent, child, options)`.\n\n```ts\naddPrismatic(name: string, parent: string, child: string, options?: JointOptions): Assembly\n```\n\n#### `addFixed()` — Shorthand for `addJoint(name, 'fixed', parent, child, options)`.\n\nFixed joints rigidly attach a child part to its parent at `frame` with no motion. Before calling `mergeInto()`, use `addFixed()` to collapse multiple root parts into a single root.\n\n```ts\naddFixed(name: string, parent: string, child: string, options?: JointOptions): Assembly\n```\n\n#### `addJointCoupling()` — Link a joint's value to a linear combination of other joint values.\n\nThe driven joint's value is computed as:\n\n```\ndriven = offset + Σ(ratio_i × source_i)\n```\n\nCoupled joints ignore any value passed in `solve(state)` — a warning is emitted if you try to override one. Coupling cycles are rejected. You cannot sweep a coupled joint directly; sweep one of its source joints instead.\n\n```ts\nassembly\n .addRevolute(\"Steering\", \"Base\", \"Turret\", { axis: [0, 0, 1] })\n .addRevolute(\"WheelDrive\", \"Turret\", \"Wheel\", { axis: [1, 0, 0] })\n .addRevolute(\"TopGear\", \"Base\", \"TopInput\", { axis: [0, 0, 1] })\n .addJointCoupling(\"TopGear\", {\n terms: [\n { joint: \"Steering\", ratio: 1 },\n { joint: \"WheelDrive\", ratio: 20 / 14 },\n ],\n });\n```\n\n```ts\naddJointCoupling(jointName: string, options: JointCouplingOptions): Assembly\n```\n\n#### `addGearCoupling()` — Link two revolute joints via a gear ratio.\n\nChoose exactly one ratio source:\n\n- `ratio` — explicit numeric ratio (driven/driver, negative for external mesh)\n- `pair` — a `GearRatioLike` from `lib.gearPair`, `lib.bevelGearPair`, etc. (uses `pair.jointRatio`)\n- `driverTeeth` + `drivenTeeth` — auto-computes ratio; use `mesh` to control sign (`'external'` = negative/opposite rotation, `'internal'` = positive, `'bevel'`/`'face'` = negative)\n\nWhen `pair` carries a `phaseDeg`, it is auto-applied as the coupling `offset` to align teeth correctly. Override with `offset: 0` if gear shapes already have the phase baked in.\n\n```ts\nconst pair = lib.gearPair({ pinion: { module: 1.25, teeth: 14 }, gear: { module: 1.25, teeth: 42 } });\nassembly\n .addRevolute(\"Pinion\", \"Base\", \"PinionPart\", { axis: [0, 0, 1] })\n .addRevolute(\"Driven\", \"Base\", \"GearPart\", { axis: [0, 0, 1] })\n .addGearCoupling(\"Driven\", \"Pinion\", { pair });\n```\n\n```ts\naddGearCoupling(drivenJointName: string, driverJointName: string, options?: GearCouplingOptions): Assembly\n```\n\n#### `sweepJoint()` — Sample a joint through its motion range, collecting collision data at each step.\n\nDivides `[from, to]` into `steps` intervals (producing `steps + 1` frames). At each sample, the assembly is solved with the sweeping joint at that value and `baseState` for all others. Returns one `JointSweepFrame` per sample with the joint value, collision findings, and any solve warnings.\n\nYou cannot sweep a coupled joint — sweep one of its source joints instead.\n\n```ts\nconst sweep = mech.sweepJoint(\"elbow\", -10, 135, 12, { shoulder: 35 });\nconst hits = sweep.filter(frame => frame.collisions.length > 0);\nconsole.log(`Collisions at ${hits.length} of ${sweep.length} poses`);\n```\n\n```ts\nsweepJoint(jointName: string, from: number, to: number, steps: number, baseState?: JointState, collisionOptions?: CollisionOptions): JointSweepFrame[]\n```\n\n#### `toJointsView()` — Derive viewport joint controls from the assembly graph and register them.\n\nSolves the assembly at rest (all joints = default), then converts each joint into a `JointViewInput` with world-space pivot and axis. Fixed joints become hidden zero-range revolute entries so attached parts follow their parent during animation. Joint couplings are forwarded to the viewport automatically.\n\nThis method is optional. Call it only when you want viewport joint sliders, coupled controls, or playback animations. If you only want geometry, return the `Assembly` or `SolvedAssembly` directly and skip `toJointsView()`.\n\n**Critical pitfall:** Always call `toJointsView()` before solving for display. Then solve at the **rest pose** (no state overrides) and return that solved assembly result directly. Do not flatten it with `.toGroup()` if you want the viewport joint animation to keep working.\n\nDo not solve at a non-zero angle when using `toJointsView()` — the viewport will apply the same rotation again, double-rotating the part.\n\n```ts\nmech.toJointsView({\n defaults: { J1: 30 },\n animations: [{\n name: \"Swing\", duration: 2, loop: true,\n keyframes: [{ values: { J1: -45 } }, { values: { J1: 45 } }, { values: { J1: -45 } }],\n }],\n});\n\n// Solve at REST — viewport handles posing\nreturn mech.solve();\n```\n\n```ts\ntoJointsView(options?: ToJointsViewOptions): void\n```\n\n#### `describe()` — Return the serializable assembly definition used by solve/inspect pipelines.\n\n```ts\ndescribe(): AssemblyDefinition\n```\n\n**Legacy Aliases**\n\n- `usedPortRefs` -> `usedConnectorRefs`\n- `withPorts()` -> `withConnectors()`\n- `getPorts()` -> `getConnectors()`\n- `getPort()` -> `getConnector()`\n\n### `ImportedAssembly`\n\nA wrapper around an imported `Assembly` that provides kinematic access and convenient transform helpers.\n\nWhen a `.forge.js` file returns an unsolved `Assembly`, [`require()`](/docs/core#require) wraps it in an `ImportedAssembly`. This preserves the kinematic structure — you can call `solve()`, `sweepJoint()`, and `mergeInto()` — while also allowing convenience transforms that auto-solve at default values.\n\n**Kinematic access**\n\n```ts\nconst arm = require(\"./arm.forge.js\");\n\nconst solved = arm.solve({ shoulder: 45 }); // full kinematic solve\nconst link = arm.part(\"Link\", { shoulder: 60 }); // single part at state\nconst group = arm.toGroup({ shoulder: 45 }); // only when ShapeGroup behavior is needed\n```\n\n**Convenience transforms** (auto-solve at defaults, return [`ShapeGroup`](/docs/core#shapegroup)):\n\n```ts\nconst positioned = arm.rotateZ(-90).translate(0, -20, 50);\n```\n\n**Merging into a parent**\n\n```ts\nrequire(\"./arm.forge.js\").mergeInto(robot, {\n prefix: \"Left Arm\",\n mountParent: \"Chassis\",\n mountJoint: \"leftMount\",\n mountOptions: { frame: Transform.identity().translate(-70, 0, 10) },\n});\n```\n\n#### `assembly()` — The underlying Assembly — use for sweepJoint, addPart into parent, etc.\n\n```ts\nget assembly(): Assembly\n```\n\n#### `solve()` — Solve the assembly at the given joint state (defaults to each joint's default value).\n\n```ts\nsolve(state?: JointState): SolvedAssembly\n```\n\n#### `part()` — Return a specific named part positioned at the given joint state, with any stored placement offset applied.\n\n```ts\npart(name: string, state?: JointState): AssemblyPart\n```\n\n#### `toGroup()` — Convert all assembly parts to a ShapeGroup with named children. Use this for composition, transforms, or child lookup — not as a required render step for assemblies. Child names match the part names used in the assembly. Any stored placement offset and placement references are forwarded to the group.\n\n```ts\ntoGroup(state?: JointState): ShapeGroup\n```\n\n#### `withReferences()` — Attach named placement reference points to this assembly. Points are simple 3D coordinates (relative to the assembly's own origin). Returns a new ImportedAssembly — does not mutate.\n\n```ts\nwithReferences(refs: Pick<PlacementReferenceInput, \"points\">): ImportedAssembly\n```\n\n#### `referenceNames()` — List all attached placement reference names.\n\n```ts\nreferenceNames(kind?: PlacementReferenceKind): string[]\n```\n\n#### `placeReference()` — Translate the assembly so the named reference point lands on `target`. Returns a new ImportedAssembly — does not mutate. All point refs are translated by the same delta.\n\n```ts\nplaceReference(ref: string, target: [ number, number, number ], offset?: [ number, number, number ]): ImportedAssembly\n```\n\n#### `translate()` — Solve at defaults and return a translated ShapeGroup.\n\n```ts\ntranslate(x: number, y: number, z: number): ShapeGroup\n```\n\n#### `rotate()` — Solve at defaults and return a rotated ShapeGroup.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateX()` — Solve at defaults and return a ShapeGroup rotated around X.\n\n```ts\nrotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateY()` — Solve at defaults and return a ShapeGroup rotated around Y.\n\n```ts\nrotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `rotateZ()` — Solve at defaults and return a ShapeGroup rotated around Z.\n\n```ts\nrotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup\n```\n\n#### `scale()` — Solve at defaults and return a scaled ShapeGroup.\n\n```ts\nscale(v: number | [ number, number, number ]): ShapeGroup\n```\n\n#### `mirror()` — Solve at defaults and return a mirrored ShapeGroup.\n\n```ts\nmirror(normal: [ number, number, number ]): ShapeGroup\n```\n\n#### `color()` — Solve at defaults and return a colored ShapeGroup.\n\n```ts\ncolor(hex: string): ShapeGroup\n```\n\n#### `child()` — Solve at defaults, get a named child part from the resulting group.\n\n```ts\nchild(name: string): Shape | Sketch | ShapeGroup\n```\n\n#### `mergeInto()` — Flatten this sub-assembly's parts and joints into `parent` and wire a mount joint.\n\nAll part and joint names from the sub-assembly are prefixed with `\"${options.prefix}.\"` to avoid collisions. After the merge, sub-assembly joints are driven from the parent using the prefixed names:\n\n```ts\nparent.solve({ \"Left Arm.shoulder\": 45, \"Right Arm.shoulder\": -20 })\n```\n\nJoint couplings inside the sub-assembly are preserved and rewritten with the prefix. Ports from sub-assembly parts are forwarded with the prefix.\n\nThe sub-assembly must have exactly one root part. If it has multiple roots, use `addFixed()` first to consolidate them before merging.\n\n```ts\nconst robot = assembly(\"Robot\").addPart(\"Chassis\", chassis);\n\nrequire(\"./arm.forge.js\").mergeInto(robot, {\n prefix: \"Left Arm\",\n mountParent: \"Chassis\",\n mountJoint: \"leftMount\",\n mountOptions: { frame: Transform.identity().translate(-70, 0, 10) },\n});\n```\n\n```ts\nmergeInto(parent: Assembly, options: MergeIntoOptions): Assembly\n```\n\n### `SolvedAssembly`\n\nThe result of solving an assembly at a specific joint state.\n\n`SolvedAssembly` holds world-space transforms for every part at a given pose. Top-level scripts can return a `SolvedAssembly` directly for display. Use `toGroup()` when you specifically need a [`ShapeGroup`](/docs/core#shapegroup) for composition, group-style transforms, or named-child lookup. Do not call `toGroup()` just to make a solved assembly render. Use `getPart()` / `getTransform()` to inspect individual parts programmatically.\n\n**Validation**\n\nCall `collisionReport()` to detect overlapping parts, or `sweepJoint()` on the parent `Assembly` to check for interference across the joint's motion range.\n\n```ts\nconst solved = mech.solve({ shoulder: 45, elbow: -20 });\nconsole.log(\"Collisions\", solved.collisionReport());\nreturn solved;\n```\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n\n**Methods:**\n\n#### `warnings()` — Return any warnings generated during solve (clamped joints, unconverged mates, etc.).\n\n```ts\nwarnings(): string[]\n```\n\n#### `getJointState()` — Return a snapshot of resolved joint values (after clamping and coupling).\n\n```ts\ngetJointState(): JointState\n```\n\n#### `mateExplodeHints()` — Explode direction hints derived from mate constraints, or null if no mates.\n\n```ts\nget mateExplodeHints(): Record<string, { direction: Vec3; }> | null\n```\n\n#### `mateDof()` — Remaining degrees of freedom after mate constraints, or null if no mates.\n\n```ts\nget mateDof(): number | null\n```\n\n#### `mateConverged()` — Whether the mate constraint solver converged, or null if no mates.\n\n```ts\nget mateConverged(): boolean | null\n```\n\n#### `getTransform()` — Return the world-space [`Transform`](/docs/core#transform) for the named part at the solved pose.\n\n```ts\ngetTransform(partName: string): Transform\n```\n\n#### `getPart()` — Return the named part already positioned at its solved world transform.\n\n```ts\ngetPart(partName: string): AssemblyPart\n```\n\n#### `toGroup()` — Convert all solved parts into a [`ShapeGroup`](/docs/core#shapegroup) with named children.\n\nEach part becomes a named child in the group, already positioned at its solved world transform. Use this only when you specifically need a [`ShapeGroup`](/docs/core#shapegroup) for composition, [`ShapeGroup`](/docs/core#shapegroup) transforms, or named-child access. Top-level scripts can return the `SolvedAssembly` directly; do not call `toGroup()` just to make a solved assembly render.\n\n```ts\nconst armGroup = mech.solve({ shoulder: 60 }).toGroup(); // only because we need rotateZ()\nreturn armGroup.rotateZ(90);\n```\n\n```ts\ntoGroup(): ShapeGroup\n```\n\n#### `toSceneObjects()` — Return an array of named scene objects for the viewport renderer.\n\nEach part becomes `{ name, shape }` or `{ name, group: [...] }` if the part is a [`ShapeGroup`](/docs/core#shapegroup). Top-level scripts should normally return the `SolvedAssembly` directly. Use `toGroup()` when you need [`ShapeGroup`](/docs/core#shapegroup) behavior; use this method only for advanced scene-graph control where you need access to the flat per-part array with metadata.\n\n```ts\ntoSceneObjects(): Array<{ name: string; shape?: Shape; group?: Array<{ name: string; shape: Shape; tags?: string[]; }>; metadata?: PartMetadata; }>\n```\n\n#### `toScene()` — Backward-compatible alias for `toSceneObjects()`.\n\n```ts\ntoScene(): Array<{ name: string; shape?: Shape; group?: Array<{ name: string; shape: Shape; tags?: string[]; }>; metadata?: PartMetadata; }>\n```\n\n#### [`bom()`](/docs/output#bom) — Generate a bill of materials for all parts in the solved assembly.\n\n```ts\nbom(): BomRow[]\n```\n\n#### `bomCsv()` — Generate a bill of materials as a CSV string.\n\n```ts\nbomCsv(): string\n```\n\n#### `collisionReport()` — Detect overlapping (colliding) part pairs in this solved pose.\n\nComputes boolean intersections between all part pairs and returns findings where the overlap volume exceeds `minOverlapVolume` (default 0.1 mm³).\n\n```ts\nconst solved = mech.solve({ shoulder: 35, elbow: 60 });\nconsole.log(\"Collisions\", solved.collisionReport());\n```\n\n```ts\ncollisionReport(options?: CollisionOptions): CollisionFinding[]\n```\n\n#### `minClearance()` — Compute the minimum gap (clearance) between two parts in this solved pose.\n\nReturns `0` if the parts are touching or overlapping. Requires the Manifold backend. `searchLength` bounds the search radius in mm — increase it for widely separated parts.\n\n```ts\nminClearance(partA: string, partB: string, searchLength?: number): number\n```\n\n### `MateBuilder`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `constraints` | `Constraint3D[]` | — |\n\n**Methods:**\n\n#### `flush()` — Constrain two faces so they stay flush.\n\n```ts\nflush(faceA: string, faceB: string): string\n```\n\n#### `align()` — Constrain two faces so their normals align.\n\n```ts\nalign(faceA: string, faceB: string): string\n```\n\n#### `parallel()` — Constrain two faces so they remain parallel.\n\n```ts\nparallel(faceA: string, faceB: string): string\n```\n\n#### `faceDistance()` — Constrain the distance between two faces.\n\n```ts\nfaceDistance(faceA: string, faceB: string, distance: number): string\n```\n\n#### `concentric()` — Constrain two axes to share the same center line.\n\n```ts\nconcentric(axisA: string, axisB: string): string\n```\n\n#### `axisParallel()` — Constrain two axes to remain parallel.\n\n```ts\naxisParallel(axisA: string, axisB: string): string\n```\n\n#### `pointCoincident()` — Constrain two points to coincide.\n\n```ts\npointCoincident(pointA: string, pointB: string): string\n```\n\n#### `pointOnFace()` — Constrain a point to lie on a face.\n\n```ts\npointOnFace(point: string, face: string): string\n```\n\n#### `pointOnAxis()` — Constrain a point to lie on an axis.\n\n```ts\npointOnAxis(point: string, axis: string): string\n```\n\n#### `angle()` — Constrain the angle between two faces.\n\n```ts\nangle(faceA: string, faceB: string, degrees: number): string\n```\n\n#### `totalEquations()` — Total constraint equations.\n\n```ts\nget totalEquations(): number\n```\n\n---\n\n<!-- generated/sheet-metal.md -->\n\n# Sheet Metal\n\nFolded sheet metal parts with flanges, bends, and flat pattern unfolding.\n\n## Contents\n\n- [Sheet Metal](#sheet-metal) — `sheetMetal`\n- [Laser Cutting](#laser-cutting) — `kerfCompensateOutline`, `kerfCompensateTabs`, `kerfCompensateSlots`, `kerfCompensatePart`, `lookupKerf`, `flatPanel`, `flatPart`, `fingerJoint`, `tabSlot`, `assemblyPreview`, `assemblyInstructions`, `formatInstructions`, `laserKit`\n- [SheetMetalPart](#sheetmetalpart)\n- [FlatPart](#flatpart)\n- [LaserKit](#laserkit)\n- [SHEET_METAL_EDGES](#sheet-metal-edges)\n- [COMMON_KERFS](#common-kerfs)\n\n## Functions\n\n### Sheet Metal\n\n#### `sheetMetal()` — Create a parametric sheet metal part with flanges, bend allowances, and flat-pattern unfolding.\n\n`sheetMetal()` keeps one semantic model and derives both a folded 3D solid and an accurate flat pattern from it. The K-factor bend allowance is applied during unfolding. This is a strict v1 subset — it does not infer sheet metal from arbitrary solids.\n\n**Recommended authoring order:**\n\n1. Define the base panel + thickness + bend parameters.\n2. Chain `.flange()` calls for each edge. Validate with `.folded()` and `.flatPattern()` before adding cutouts.\n3. Add panel cutouts, then flange cutouts one region at a time.\n4. Validate after each new cutout region.\n\n**v1 limitations:** one base panel, up to four 90° edge flanges, constant thickness, explicit K-factor, rectangular corner reliefs, planar cutouts only. No hems, jogs, lofted bends, non-90° flanges, or bend-region cutouts.\n\n```ts\nconst cover = sheetMetal({\n panel: { width: 180, height: 110 },\n thickness: 1.5,\n bendRadius: 2,\n bendAllowance: { kFactor: 0.42 },\n cornerRelief: { size: 4 },\n})\n .flange('top', { length: 18 })\n .flange('right', { length: 18 })\n .flange('bottom', { length: 18 })\n .flange('left', { length: 18 })\n .cutout('panel', rect(72, 36), { selfAnchor: 'center' })\n .cutout('flange-right', roundedRect(26, 10, 5), { selfAnchor: 'center' });\n\nconst folded = cover.folded();\nconst flat = cover.flatPattern();\n```\n\n```ts\nsheetMetal(options: SheetMetalOptions): SheetMetalPart\n```\n\n**`SheetMetalOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `panel` | `{ width: number; height: number; }` | Base panel dimensions. This is the flat blank before flanges are applied. |\n| `thickness` | `number` | Sheet thickness in mm. Applied uniformly across the panel and all flanges. |\n| `bendRadius` | `number` | Inside bend radius in mm. Must be ≥ 0. Typically 0.5–2× the sheet thickness. |\n| `bendAllowance` | `{ kFactor: number; }` | Bend allowance model used when computing the flat-pattern developed length. Currently only K-factor is supported. The K-factor (0–1) describes how far the neutral axis sits from the inner bend surface. Typical values: - Soft materials / large radius: 0.50 - General sheet steel: 0.42–0.44 - Hard materials / tight radius: 0.30–0.38 |\n| `cornerRelief?` | `{ kind?: \"rect\"; size: number; }` | Corner relief cut at each bend intersection. Prevents material overlap when two flanges meet at a corner. Defaults to a rectangular relief sized to `bendRadius + thickness` if omitted. |\n\n### Laser Cutting\n\n#### `kerfCompensateOutline()` — Apply kerf compensation to a complete part outline (outer boundary + holes).\n\nOffsets inward by half-kerf: the outer boundary shrinks and inner holes grow. This is correct because the laser beam removes material on both sides of the cut line.\n\n```ts\nkerfCompensateOutline(sketch: Sketch, kerf: number): Sketch\n```\n\n#### `kerfCompensateTabs()` — Apply kerf compensation to joint protrusions (tabs, fingers).\n\nThese grow by half-kerf so they are slightly oversized and fit tightly in their mating slots after the laser removes material.\n\n```ts\nkerfCompensateTabs(sketch: Sketch, kerf: number): Sketch\n```\n\n#### `kerfCompensateSlots()` — Apply kerf compensation to joint cutouts (slots, holes that receive tabs).\n\nThese grow by half-kerf so tabs can fit into them after the laser removes material from both sides of the slot walls.\n\n```ts\nkerfCompensateSlots(sketch: Sketch, kerf: number): Sketch\n```\n\n#### `kerfCompensatePart()` — Build a kerf-compensated part profile.\n\n1. Start with the base profile.\n2. Kerf-compensate each tab addition (grow by kerf/2), then union with base.\n3. Kerf-compensate each slot subtraction (grow by kerf/2), then subtract from base.\n4. Kerf-compensate the resulting outline (shrink by kerf/2).\n\nOrder matters: joints modify geometry BEFORE outline compensation so the final inward offset applies uniformly to the assembled profile.\n\n```ts\nkerfCompensatePart(baseProfile: Sketch, joints: PartJoints, kerf: number): Sketch\n```\n\n**`PartJoints`**\n- `additions?: Sketch[]` — Geometry to ADD to the base profile (tabs, fingers protruding from edges).\n- `subtractions?: Sketch[]` — Geometry to SUBTRACT from the base profile (slots, holes for mating tabs).\n\n#### `lookupKerf()` — Look up kerf for a material + thickness + laser combo.\n\nIf `laserType` is omitted, returns the first matching material + thickness entry. Returns `undefined` when no match is found.\n\n```ts\nlookupKerf(material: string, thickness: number, laserType?: string): number | undefined\n```\n\n#### `flatPanel()` — Create a rectangular flat panel with 4 named edges.\n\nProfile origin at bottom-left corner. Edges: bottom (y=0), right (x=width), top (y=height), left (x=0). Edge traversal follows CCW winding order.\n\n```ts\nflatPanel(name: string, width: number, height: number, thickness: number, options?: FlatPartOptions): FlatPart\n```\n\n`FlatPartOptions`: `{ material?: string, qty?: number, color?: string }`\n\n#### `flatPart()` — Create a flat part from an arbitrary profile with user-named edges.\n\nEdge normals are computed automatically (perpendicular to direction, rotated 90deg CW).\n\n```ts\nflatPart(name: string, profile: Sketch, thickness: number, edges?: Record<string, { start: [ number, number ]; end: [ number, number ]; }>, options?: FlatPartOptions): FlatPart\n```\n\n#### `fingerJoint()` — Connect two parts with finger joints along specified edges.\n\nAdds finger geometry to partA's edge, cuts matching slots from partB's edge. The joint profiles are positioned along each edge using rotation + translation.\n\n```ts\nfingerJoint(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: FingerJointOptions & { foldAngle?: number; }): void\n```\n\n**`FingerJointOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `fingers?` | `number` | Explicit finger count (must be odd, >= 3). Default: auto from length/thickness. |\n| `fingerWidth?` | `number` | Explicit finger width. Default: auto. |\n| `clearance?` | `number` | Extra clearance per side (mm). Default: 0. |\n| `kerf?` | `number` | Laser kerf (mm). Default: 0. |\n| `endStyle?` | `\"full\" \\| \"half\"` | Whether edge starts with full finger or half. Default: 'full'. |\n\n#### `tabSlot()` — Connect two parts with tab-and-slot joints along specified edges.\n\nAdds tab geometry to partA's edge, cuts matching slots from partB's edge.\n\n```ts\ntabSlot(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: TabSlotOptions & { foldAngle?: number; }): void\n```\n\n**`TabSlotOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `tabCount?` | `number` | Number of tabs. Default: auto (length / (4 * thickness)). |\n| `tabWidth?` | `number` | Tab width. Default: 2 * thickness. |\n| `clearance?` | `number` | Extra clearance per side (mm). Default: 0. |\n| `kerf?` | `number` | Laser kerf (mm). Default: 0. |\n| `inset?` | `number` | Distance from panel edges to first/last tab center. Default: thickness. |\n\n#### `assemblyPreview()` — Generate a 3D assembly preview from flat parts and their joint records.\n\nThe preview can fold joints partially or fully and optionally apply exploded spacing so part relationships are easier to inspect visually.\n\n```ts\nassemblyPreview(parts: FlatPart[], joints: JointRecord[], options?: AssemblyPreviewOptions): AssemblyPreviewResult\n```\n\n**`JointRecord`**\n- `foldAngle: number` — Fold angle in degrees. Default: 90.\n- Also: `type: \"finger\" | \"tabSlot\" | \"snapFit\", partA: string, partB: string, edgeA: string, edgeB: string`\n\n**`AssemblyPreviewOptions`**\n- `kerf?: number` — Kerf compensation passed to each part's solid(). Default: 0\n- `fold?: number` — Fold amount: 0 = flat layout, 1 = fully assembled. Default: 1\n- `explode?: number` — Explode distance: 0 = assembled, >0 = parts spread outward. Default: 0\n\n**`AssemblyPreviewResult`**\n- `shapes: ShapeGroup` — All part shapes grouped for display.\n- `partShapes: Map<string, Shape>` — Individual transformed shapes keyed by part name.\n\n#### `assemblyInstructions()` — Generate step-by-step assembly instructions from flat parts and joints.\n\nAlgorithm:\n\n1. Build adjacency graph from joints\n2. Pick root part (most connections, or user-specified)\n3. BFS from root, creating one step per part addition\n4. Each step describes: which part to add, where it connects, how to orient it\n\nHeuristics for step ordering:\n\n- Start with the part that has the most connections (the base)\n- Add parts that connect to already-assembled parts first (BFS order)\n- Among candidates at the same BFS depth, prefer parts with more connections to already-assembled parts (structurally stable)\n\n```ts\nassemblyInstructions(parts: FlatPart[], joints: JointRecord[], options?: AssemblyInstructionsOptions): AssemblyInstructionsResult\n```\n\n**`AssemblyInstructionsOptions`**\n- `rootPart?: string` — Part to start from. Default: part with most joint connections.\n\n**`AssemblyInstructionsResult`**\n- `totalParts: number` — Total number of parts in the assembly.\n- `orphanParts: string[]` — Parts not connected to the joint graph (orphans).\n- Also: `steps: AssemblyStep[]`\n\n**`AssemblyStep`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `stepNumber` | `number` | 1-based step number. |\n| `description` | `string` | Human-readable instruction. |\n| `partName` | `string` | The part being added in this step. |\n| `partNumber` | `number` | Part number (for cross-ref with cut sheets). |\n| `connectsTo` | `string` | Which existing part it connects to. |\n| `jointType` | `\"finger\" \\| \"tabSlot\" \\| \"snapFit\"` | Joint type used. |\n| `newPartEdge` | `string` | The edge on the new part. |\n| `existingPartEdge` | `string` | The edge on the existing part. |\n| `foldAngle` | `number` | Fold angle in degrees. |\n| `assembledParts` | `string[]` | Part names in the assembly so far (after this step). |\n\n#### `formatInstructions()` — Format assembly instructions as a human-readable text document.\n\nIncludes a \"Step 0\" preamble identifying the base part, followed by numbered steps, and a note about any orphan parts.\n\n```ts\nformatInstructions(result: AssemblyInstructionsResult): string\n```\n\n#### `laserKit()` — Top-level factory for creating a LaserKit container.\n\n```ts\nlaserKit(options?: LaserKitOptions): LaserKit\n```\n\n**`LaserKitOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `material?` | `string` | Default material label for parts that don't specify one. |\n| `sheetWidth?` | `number` | Stock sheet width in mm (default 600). |\n| `sheetHeight?` | `number` | Stock sheet height in mm (default 400). |\n| `kerf?` | `number` | Laser kerf in mm (default 0.2). |\n\n---\n\n## Classes\n\n### `SheetMetalPart`\n\nAn immutable sheet metal part that accumulates flanges and cutouts.\n\nEach mutating method returns a **new** `SheetMetalPart`; the original is unchanged. The part does not produce geometry until you call `.folded()` or `.flatPattern()`.\n\n#### `flange()` — Add a 90° flange along one edge of the base panel.\n\nEach of the four edges (`'top'`, `'right'`, `'bottom'`, `'left'`) may carry at most one flange. Calling `.flange()` twice for the same edge throws.\n\nCorner reliefs are automatically inserted at the intersections of adjacent flanges. Build flanges before cutouts — validate with `.folded()` and `.flatPattern()` after each addition.\n\n```ts\nconst part = sheetMetal({ panel: { width: 100, height: 60 }, thickness: 1.5, bendRadius: 2, bendAllowance: { kFactor: 0.42 } })\n .flange('top', { length: 15 })\n .flange('bottom', { length: 15 });\n```\n\n```ts\nflange(edge: SheetMetalEdge, options: SheetMetalFlangeOptions): SheetMetalPart\n```\n\n#### `cutout()` — Subtract a 2D sketch cutout from a planar region of the sheet metal part.\n\n`region` must be `'panel'` or one of `'flange-top'`, `'flange-right'`, `'flange-bottom'`, `'flange-left'` (only available once the corresponding flange has been added). Cutouts inside bend regions are **not** supported in v1.\n\n`sketch` must be an **unplaced** compile-covered 2D profile (e.g. the result of [`circle2d()`](/docs/sketch#circle2d), [`rect()`](/docs/sketch#rect), [`roundedRect()`](/docs/sketch#roundedrect)). Passing an already-placed sketch (one that has had `.onFace(...)` called on it) will throw.\n\n**Authoring order:** Add all flanges before adding cutouts. Add panel cutouts before flange cutouts. Add one region at a time and validate with `.folded()` / `.flatPattern()` after each step.\n\n```ts\nconst part = sheetMetal({ panel: { width: 180, height: 110 }, thickness: 1.5, bendRadius: 2, bendAllowance: { kFactor: 0.42 } })\n .flange('top', { length: 18 })\n .cutout('panel', rect(72, 36), { selfAnchor: 'center' })\n .cutout('flange-top', roundedRect(26, 10, 5), { selfAnchor: 'center' });\n```\n\n```ts\ncutout(region: SheetMetalPlanarRegionName, sketch: Sketch, options?: SheetMetalCutoutOptions): SheetMetalPart\n```\n\n#### `regionNames()` — Return all semantic region names currently available on this part.\n\nThe returned list always includes `'panel'`. For every flange that has been added, the list also includes the corresponding `'flange-<edge>'` and `'bend-<edge>'` entries.\n\nUse this to discover valid targets for `.cutout()` or for querying faces by region after materializing with `.folded()`.\n\nDefended region names: `panel` | `flange-top` | `flange-right` | `flange-bottom` | `flange-left` | `bend-top` | `bend-right` | `bend-bottom` | `bend-left`\n\n```ts\nregionNames(): SheetMetalRegionName[]\n```\n\n#### `folded()` — Materialize the 3D folded solid.\n\nApplies all flanges (bent up at their configured angles) and all registered cutouts, then returns the resulting [`Shape`](/docs/core#shape). The shape is compiler-owned and exact-exportable (STEP, IGES, etc.).\n\nPrefer calling `.folded()` to validate each build step before proceeding to the final model.\n\n```ts\nfolded(): Shape\n```\n\n#### `flatPattern()` — Materialize the flat-pattern (unfolded blank) for fabrication.\n\nUnfolds all flanges using the K-factor bend allowance and lays the result flat in the XY plane. Cutouts are projected into the flat geometry. The returned shape is exact-exportable and ready for laser / waterjet / CNC nesting workflows.\n\nThe developed length of each bend zone is: `BA = (bendRadius + kFactor × thickness) × angleDeg × π / 180`\n\n```ts\nflatPattern(): Shape\n```\n\n### `FlatPart`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | — |\n| `thickness` | `number` | — |\n| `options` | `FlatPartOptions` | — |\n\n**Methods:**\n\n#### `edges()` — All edges as a read-only map.\n\n```ts\nget edges(): ReadonlyMap<string, EdgeInfo>\n```\n\n#### `edge()` — Look up a named edge. Throws if the edge does not exist.\n\n```ts\nedge(name: string): EdgeInfo\n```\n\n#### `edgeNames()` — All edge names on this part.\n\n```ts\nedgeNames(): string[]\n```\n\n#### `partNumber()` — BOM part number assigned to this flat part.\n\n```ts\nget partNumber(): number\n```\n\n#### `joints()` — Joint records that attach this part to other parts in the kit.\n\n```ts\nget joints(): readonly JointRecord[]\n```\n\n#### `quantity()` — Requested quantity of this part in the kit. Defaults to `1`.\n\n```ts\nget quantity(): number\n```\n\n#### `addGeometry()` — Add geometry (e.g. protruding tabs) to the part profile.\n\n```ts\naddGeometry(sketch: Sketch): void\n```\n\n#### `subtractGeometry()` — Subtract geometry (e.g. slot cuts) from the part profile.\n\n```ts\nsubtractGeometry(sketch: Sketch): void\n```\n\n#### `addJoint()` — Record a joint connection for assembly preview.\n\n```ts\naddJoint(record: JointRecord): void\n```\n\n#### `profile()` — Final 2D profile with joints and optional kerf compensation.\n\n```ts\nprofile(kerf?: number): Sketch\n```\n\n#### `solid()` — 3D solid — extrude the profile by material thickness.\n\n```ts\nsolid(kerf?: number): Shape\n```\n\n### `LaserKit`\n\n#### `kerf()` — Laser kerf in mm.\n\n```ts\nget kerf(): number\n```\n\n#### `parts()` — All registered parts (flat, in insertion order).\n\n```ts\nget parts(): readonly FlatPart[]\n```\n\n#### `material()` — Default material label.\n\n```ts\nget material(): string\n```\n\n#### `sheetWidth()` — Stock sheet width in mm.\n\n```ts\nget sheetWidth(): number\n```\n\n#### `sheetHeight()` — Stock sheet height in mm.\n\n```ts\nget sheetHeight(): number\n```\n\n#### `addPart()` — Register a flat part with this kit. Assigns a sequential part number and records the quantity.\n\n```ts\naddPart(part: FlatPart, overrides?: { qty?: number; }): this\n```\n\n#### `cutSheets()` — Generate nested cut sheets using guillotine bin-packing.\n\n```ts\ncutSheets(): CuttingLayoutResult\n```\n\n#### [`bom()`](/docs/output#bom) — Bill of materials listing every part with dimensions.\n\n```ts\nbom(): LaserKitBomEntry[]\n```\n\n#### `partSvgs()` — Individual SVG string for each part profile, keyed by part name.\n\n```ts\npartSvgs(): Map<string, string>\n```\n\n#### `inventorySvg()` — Combined inventory SVG showing all parts in a labeled grid.\n\n```ts\ninventorySvg(): string\n```\n\n#### `assemblyPreview()` — 3D fold-up preview of the assembled kit.\n\n```ts\nassemblyPreview(options?: Omit<AssemblyPreviewOptions, \"kerf\">): AssemblyPreviewResult\n```\n\n#### `assemblyInstructions()` — Step-by-step assembly instructions.\n\n```ts\nassemblyInstructions(options?: AssemblyInstructionsOptions): AssemblyInstructionsResult\n```\n\n#### `formatInstructions()` — Human-readable assembly instructions text.\n\n```ts\nformatInstructions(options?: AssemblyInstructionsOptions): string\n```\n\n---\n\n## Constants\n\n### `SHEET_METAL_EDGES`\n\n### `COMMON_KERFS`\n\nCommon kerf values. Users should always test-cut to verify for their specific setup.\n\n---\n\n<!-- generated/output.md -->\n\n# Output & Annotations\n\nDimensions, BOM entries, verification checks, and sketch export.\n\n## Contents\n\n- [Annotations & Output](#annotations-output) — `bom`, `robotExport`, `dim`, `dimLine`\n- [Sketch Export](#sketch-export) — `sketchToDxf`, `sketchToSvg`\n\n## Functions\n\n### Annotations & Output\n\n#### `bom()` — Register a Bill of Materials entry for report export.\n\nBOM entries are accumulated during script execution and exported alongside the model in report views. Rows are grouped by normalized `description + unit`. Pass an explicit `key` to force multiple descriptions to collapse into a single line item.\n\n- `quantity` must be a finite number `>= 0`. A quantity of `0` is silently ignored (useful for conditional scripting with `param()`-driven counts).\n- `unit` defaults to `\"pieces\"` when omitted or empty.\n- The assembly `solved.bom()` / `solved.bomCsv()` API is separate and covers per-part assembly metadata; this function is for free-form purchased-item annotation.\n- `bom()` is injected into every `.forge.js` script. Call it directly; do not write `const { bom } = require(...)`, because top-level declarations named `bom` collide with the built-in runtime name.\n\n```ts\nconst tubeLen = param(\"Tube Length\", 1200, { min: 300, max: 4000, unit: \"mm\" });\nconst boltCount = param(\"Bolt Count\", 16, { min: 0, max: 200, integer: true });\n\nbom(tubeLen, \"iron tube 30 x 20\", { unit: \"mm\" });\nbom(boltCount, \"M4 bolt, 16 mm length\");\nbom(4, \"rubber foot\", { key: \"foot-rubber\" }); // explicit aggregation key\n\n// Structured metadata for richer reports:\nbom(tubeLen, \"rectangular steel tube\", {\n unit: \"mm\",\n material: \"steel\",\n section: [30, 20],\n wall: 3,\n});\n```\n\n```ts\nbom(quantity: number, description: string, opts?: BomOpts): void\n```\n\n**`BomOpts`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `unit?` | `string` | Quantity unit label, e.g. \"mm\", \"pieces\", \"kg\". Default: \"pieces\" |\n| `key?` | `string` | Optional explicit grouping key used during report aggregation. |\n| `material?` | `string` | Material name, e.g. \"steel\", \"birch plywood\", \"nylon\" |\n| `dimensions?` | `number[]` | Overall dimensions `[width, height]` or `[width, height, thickness]` in the entry's unit |\n| `section?` | `number[]` | Cross-section dimensions `[w, h]` for tubes and profiles |\n| `wall?` | `number` | Wall thickness for hollow sections (mm) |\n| `diameter?` | `number` | Diameter for round stock, bolts, dowels (mm) |\n| `length?` | `number` | Length for fasteners (mm) |\n| `process?` | `string` | Manufacturing process, e.g. \"laser cut\", \"CNC\", \"welded\" |\n| `notes?` | `string` | Free-form notes |\n| `grain?` | `string` | Wood grain direction, e.g. \"long\", \"cross\" |\n\n#### `robotExport()` — Declare that this script should export the assembly as a SDF/URDF robot package.\n\nCall `robotExport()` alongside your assembly definition. The CLI commands `forgecad export sdf` and `forgecad export urdf` pick up the declaration and produce a robot package with:\n\n- Mesh-based inertia tensors (full 6-component, not bounding-box approximations)\n- Separate collision meshes (convex hull by default — ~50–80% smaller)\n- Joint mimic elements derived from `addJointCoupling` / `addGearCoupling`\n\n**Collision mesh modes** (set per-link via `links[\"PartName\"].collision`):\n\n| Mode | Description | Default |\n|------|-------------|---------|\n| `'convex'` | Convex hull (separate `_collision.stl`) | Yes |\n| `'box'` | AABB primitive — fastest physics | |\n| `'visual'` | Same mesh as visual — exact but slow | |\n| `'none'` | No collision geometry | |\n\n**Unit conventions:**\n\n- Revolute `velocity` is in degrees/second in Forge; exporters convert to rad/s.\n- Prismatic distances are in mm in Forge; exported in meters.\n- `massKg` is preferred; `densityKgM3` is used when mass is unknown.\n- Couplings with multiple terms: only the primary term (largest ratio) maps to `<mimic>` — SDF/URDF support single-leader mimic only. Dropped terms emit a warning.\n\n```ts\nconst rover = assembly(\"Scout\")\n .addPart(\"Chassis\", box(300, 220, 50).translate(0, 0, -25))\n .addPart(\"Left Wheel\", cylinder(30, 60, undefined, 48).translate(0, 0, -15))\n .addRevolute(\"leftWheel\", \"Chassis\", \"Left Wheel\", {\n axis: [0, 1, 0],\n frame: Transform.identity().translate(90, 140, 60),\n effort: 20, velocity: 1080,\n });\n\nrobotExport({\n assembly: rover,\n modelName: \"Scout\",\n links: {\n Chassis: { massKg: 10 },\n \"Left Wheel\": { massKg: 0.8 },\n },\n plugins: {\n diffDrive: {\n leftJoints: [\"leftWheel\"], rightJoints: [\"rightWheel\"],\n wheelSeparationMm: 280, wheelRadiusMm: 60,\n },\n },\n world: { generateDemoWorld: true },\n});\n```\n\n**CLI usage**\n\n```bash\nforgecad export sdf model.forge.js # SDF package (Gazebo/Ignition)\nforgecad export urdf model.forge.js # URDF package (ROS/PyBullet/MuJoCo)\n```\n\n```ts\nrobotExport(options: RobotExportOptions): CollectedRobotExport\n```\n\n**`RobotExportOptions`**: `assembly: Assembly`, `modelName?: string`, `state?: JointState`, `static?: boolean`, `selfCollide?: boolean`, `allowAutoDisable?: boolean`, `links?: Record<string, RobotLinkExportOptions>`, `joints?: Record<string, RobotJointExportOptions>`, `plugins?: { diffDrive?: RobotDiffDrivePluginOptions; jointStatePublisher?: RobotJointStatePublisherOptions; }`, `world?: RobotWorldOptions`\n\n`RobotLinkExportOptions`: `{ massKg?: number, densityKgM3?: number, collision?: \"visual\" | \"convex\" | \"box\" | \"none\" }`\n\n`RobotJointExportOptions`: `{ effort?: number, velocity?: number, damping?: number, friction?: number }`\n\n**`RobotDiffDrivePluginOptions`**: `leftJoints: string[]`, `rightJoints: string[]`, `wheelSeparationMm: number`, `wheelRadiusMm: number`, `topic?: string`, `odomTopic?: string`, `tfTopic?: string`, `frameId?: string`, `odomFrameId?: string`, `maxLinearVelocity?: number`, `maxAngularVelocity?: number`, `linearAcceleration?: number`, `angularAcceleration?: number`\n\n`RobotJointStatePublisherOptions`: `{ enabled?: boolean, joints?: string[], topic?: string, updateRate?: number }`\n\n`RobotWorldOptions`: `{ name?: string, generateDemoWorld?: boolean, spawnPose?: RobotPose6, keyboardTeleop?: RobotWorldKeyboardTeleopOptions }`\n\n`RobotWorldKeyboardTeleopOptions`: `{ enabled?: boolean, linearStep?: number, angularStep?: number }`\n\n**`CollectedRobotExport`**: `modelName: string`, `assembly: AssemblyDefinition`, `state: JointState`, `static: boolean`, `selfCollide: boolean`, `allowAutoDisable: boolean`, `links: Record<string, RobotLinkExportOptions>`, `joints: Record<string, RobotJointExportOptions>`, `plugins: { diffDrive?: RobotDiffDrivePluginOptions; jointStatePublisher?: RobotJointStatePublisherOptions; }`, `world: RobotWorldOptions | null`\n\n`AssemblyDefinition`: `{ name: string, parts: AssemblyPartDef[], joints: AssemblyJointDef[], jointCouplings: AssemblyJointCouplingDef[] }`\n\n`AssemblyPartDef`: `{ name: string, part: AssemblyPart, base: Transform, metadata?: PartMetadata }`\n\n**`PartMetadata`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `tags?` | `string \\| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |\n| `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |\n\n**`AssemblyJointDef`**: `name: string`, `type: JointType`, `parent: string`, `child: string`, `frame: Transform`, `axis: Vec3`, `min?: number`, `max?: number`, `defaultValue: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`, `connectorRefs?: JointConnectorRefs`\n\n`JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`\n\n`AssemblyJointCouplingDef`: `{ joint: string, terms: JointCouplingTermRecord[], offset: number }`\n\n`JointCouplingTermRecord`: `{ joint: string, ratio: number }`\n\n#### `dim()` — Add a dimension annotation between two points.\n\nDimension annotations are purely visual callouts rendered in the viewport and report export. They do not affect geometry or constrain the model.\n\nPoint arguments accept 2D tuples `[x, y]`, 3D tuples `[x, y, z]`, or [`Point2D`](/docs/sketch#point2d) objects (Z is treated as 0 for 2D inputs).\n\n**Ownership Rules (Report Pages)**\n\n- `currentComponent: true` — deterministic ownership by the calling import instance. Use when authoring reusable imported parts.\n- `component: \"Part Name\"` — route dimension to another named returned object.\n- Multiple owners: dimension is shared and appears on the assembly overview page.\n- No ownership set: report export infers ownership via endpoint-in-bbox.\n\n```ts\ndim([-w / 2, 0, 0], [w / 2, 0, 0], { label: \"Width\" });\ndim([0, 0, -h / 2], [0, 0, h / 2], { label: \"Height\", offset: 14 });\ndim([0, 0, 0], [100, 0, 0], { component: \"Base\", color: \"#00AAFF\" });\n```\n\n`component` (string or string[] — report ownership), `currentComponent` (boolean)\n\n```ts\ndim(from: PointArg, to: PointArg, opts?: DimOpts): void\n```\n\n`DimOpts`: `{ offset?: number, label?: string, color?: string, component?: string | string[], currentComponent?: boolean }`\n\n#### `dimLine()` — Add a dimension annotation along a [`Line2D`](/docs/sketch#line2d).\n\nConvenience wrapper around { points from a constrained-sketch [`Line2D`](/docs/sketch#line2d) entity. All `opts` are forwarded unchanged.\n\n```ts\nconst a = point(0, 0);\nconst b = point(100, 0);\ndimLine(line(a, b), { label: \"Span\", offset: -8 });\n```\n\n```ts\ndimLine(l: Line2D, opts?: DimOpts): void\n```\n\n### Sketch Export\n\n#### `sketchToDxf()` — Export a 2D sketch as a DXF string (R12/AC1009 — maximally compatible).\n\nFor regular sketches, each polygon loop becomes a closed `LWPOLYLINE`. For constrained sketches, exports raw `LINE`, `CIRCLE`, and `ARC` entities from the constraint edge geometry, which preserves internal/shared edges that `toPolygons()` would merge away.\n\nThe R12 format is chosen for maximum compatibility with CAM tools, laser-cutter software, and older CAD readers.\n\n```ts\nconst s = rect(100, 60);\nconst dxf = sketchToDxf(s, { layer: 'cut' });\n```\n\n```ts\nsketchToDxf(sketch: Sketch, options?: SketchDxfOptions): string\n```\n\n**`SketchDxfOptions`**\n- `layer?: string` — DXF layer name. Default: \"0\"\n- `colorIndex?: number` — DXF color index (1–255, AutoCAD ACI). Default: 7 (white/black)\n\n#### `sketchToSvg()` — Export a 2D sketch as an SVG string.\n\nFor regular sketches, exports filled polygon regions. For constrained sketches, exports raw edge geometry (LINE, ARC, CIRCLE) which preserves internal/shared edges that `toPolygons()` would merge away.\n\nThe SVG uses the sketch's native coordinate system (Y-up) with a CSS transform that flips Y so the output renders correctly in SVG's Y-down space. Coordinates are in sketch units (typically mm).\n\n```ts\nconst s = rect(100, 60);\nconst svg = sketchToSvg(s, { stroke: '#333', strokeWidth: 0.8 });\n```\n\n```ts\nsketchToSvg(sketch: Sketch, options?: SketchSvgOptions): string\n```\n\n**`SketchSvgOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `stroke?` | `string` | Stroke color. Default: \"black\" |\n| `strokeWidth?` | `number` | Stroke width in sketch units. Default: 0.5 |\n| `fill?` | `string` | Fill color. Default: \"none\" |\n| `padding?` | `number` | Padding around the sketch bounding box in sketch units. Default: 2 |\n| `pixelsPerUnit?` | `number` | If set, scale so 1 sketch-unit = this many px. Otherwise auto-fit. |\n\n---\n\n<!-- generated/lib.md -->\n\n# Part Library\n\nPre-built fasteners, gears, pipes, structural profiles, and utility shapes. Access via `lib.*`.\n\n## Contents\n\n- [TangentLoop2D](#tangentloop2d)\n- [DriveWheelBuilder](#drivewheelbuilder)\n- [lib](#lib)\n\n---\n\n## Classes\n\n### `TangentLoop2D`\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `circles` | `TangentCircle2D[]` | — |\n| `mode` | `BeltMode` | — |\n| `segments` | `BeltPathSegment[]` | — |\n| `straightSpans` | `BeltLineSpan[]` | — |\n| `wraps` | `BeltWrapArc[]` | — |\n| `wrapByPulley` | `Record<string, BeltWrapArc>` | — |\n| `length` | `number` | — |\n\n**Methods:**\n\n#### `toSketch()` — Convert the loop centerline into a thin visual sketch.\n\n```ts\ntoSketch(width?: number): Sketch\n```\n\n#### `toProfile()` — Convert the loop into a filled profile using the pitch path itself as the boundary.\n\n```ts\ntoProfile(): Sketch\n```\n\n#### `offsetBand()` — Build a belt band sketch by offsetting the route to inner and outer pulley radii.\n\n```ts\noffsetBand(thickness: number): Sketch\n```\n\n### `DriveWheelBuilder`\n\n#### `addSpurTeethBetween()` — Add an involute spur-tooth window on part of the pitch circle.\n\n```ts\naddSpurTeethBetween(options: DriveWheelSpurTeethRegionOptions): this\n```\n\n#### `addSolidArcBetween()` — Add a constant-radius solid arc region such as a dwell, stop, or pusher.\n\n```ts\naddSolidArcBetween(options: DriveWheelSolidArcRegionOptions): this\n```\n\n#### `addShapeRegion()` — Add a fully custom region shape while preserving region metadata.\n\n```ts\naddShapeRegion(name: string, shape: Shape, options?: DriveWheelShapeRegionOptions): this\n```\n\n#### `build()` — Build the final wheel shape with a bore connector and region metadata.\n\n```ts\nbuild(): Shape\n```\n\n---\n\n## Constants\n\n### `lib`\n\nPre-built parametric parts available in user scripts as `lib.*`.\n\nEvery key in this object becomes a method or namespace on the `lib` object exposed to `.forge.js` scripts. The catalog includes:\n\n**Fasteners and hardware patterns:** `bolt`, `nut`, `washer`, `fastenerSet`, `boltedServiceCover`, `datumEnclosureAssembly`, `snapLatchCoverAssembly`, `pinnedLeverAssembly`, `retainedShaftAssembly`, `capturedLinearSlide`, `capturedCartridgeGuideAssembly`, `livingHingeCoverAssembly`, `knuckledHingeAssembly`, `clevisPinJointAssembly`, `seatedBearingAssembly`, `cableGlandAnchorAssembly`, `hoseBarbPortAssembly`, `routedTubeClipAssembly`, `pcbTerminalBlockAssembly`, `thumbScrewClampAssembly`, `fastenerHole`, `boltHole`, `counterbore`, `hexNut`, `holePattern`\n\n**Structure:** `tube`, `pipe`, `bracket`, `pipeRoute`, `elbow`, `tSlotProfile`, `tSlotExtrusion`, `profile2020BSlot6Profile`, `profile2020BSlot6`\n\n**Belt drives:** `beltDrive`, `tangentLoop2d`\n\n**Threads:** `thread`\n\n**Gears:** `spurGear`, `sectorGear`, `driveWheel`, `bevelGear`, `faceGear`, `sideGear`, `ringGear`, `rackGear`, `gearPair`, `bevelGearPair`, `faceGearPair`, `sideGearPair`\n\n**Gear bodies:** `gearBodies.disk`, `gearBodies.diskWithHub`, `gearBodies.spoked`, `gearBodies.fromProfile` plus direct aliases `gearBodyDisk`, `gearBodyDiskWithHub`, `gearBodySpoked`, `gearBodyFromProfile`\n\n**Gear ratios (pure math helpers):** `gearRatio`, `rackRatio`, `planetaryRatio`\n\n**Bolt patterns:** `boltPattern` — define hole positions once, cut them from multiple parts\n\n**Utilities:** `explode`\n\nSizes outside the supported ranges will throw at runtime with a descriptive error.\n\n- `boltHole(diameter: number, depth: number): Shape` — Simple cylindrical through-hole cutter centered on Z=0. Subtract the result from a part to produce a plain cylindrical clearance hole. For ISO metric sizes with fit classes and counterbore/countersink, use {\n- `fastenerHole(opts: FastenerHoleOptions): Shape` — ISO metric fastener hole cutter with optional counterbore or countersink. **Details** Returns a cutter shape (subtract from a solid to produce the hole). Sizes outside M2–M10 will throw. Extend `METRIC_HOLE_TABLE` in this file to add new sizes. **Example** ```ts const plate = box(60, 40, 8) .subtract(lib.fastenerHole({ size: 'M5', fit: 'normal', depth: 8 }) .translate(15, 10, 4)); ```\n- `counterbore(holeDia: number, boreDia: number, boreDepth: number, totalDepth: number): Shape` — Counterbore hole cutter — through-hole with a wider cylindrical recess at the top. Use for socket-head cap screws that must sit flush. Subtract from a solid. For ISO metric sizing and fit classes, prefer {\n- `tube(outerX: number, outerY: number, outerZ: number, wall: number): Shape` — Rectangular hollow tube (thin-wall box section). Both the outer and inner boxes are centered on the XY plane with their base at Z=0.\n- `pipe(height: number, outerRadius: number, wall: number, segments?: number): Shape` — Hollow cylindrical pipe. Centered on the XY plane, extending upward along +Z from z=0 to z=height. For complex routed pipe geometry, see `lib.pipeRoute`.\n- `explode<T extends ExplodeItem[] | ShapeGroup>(items: T, options?: ExplodeOptions): T` — Apply deterministic exploded-view offsets to an assembly tree. **Details** Traverses arrays of shapes/sketches/named items, nested `{ name, group: [...] }` structures, and [`ShapeGroup`](/docs/core#shapegroup) outputs, translating each node by a computed offset while preserving names, colors, and nesting. Returns the same structure type as the input. In `radial` mode the algorithm is branch-aware and parent-relative: each node fans out from its immediate parent's center, so nested assemblies peel apart level by level. Named items may also include an inline `explode: { stage?, direction?, axisLock? }` property to override per-item behavior. Use this function when you want to bake the explode offset into the geometry before returning (e.g. to drive the amount with a `param()` slider). For a viewport-only explode slider without rerunning the script, use [`explodeView()`](/docs/viewport#explodeview) instead. **Example** ```js const explodeAmt = param('Explode', 0, { min: 0, max: 40, unit: 'mm' }); return lib.explode(assembly, { amount: explodeAmt, stages: [0.4, 0.8], mode: 'radial', byName: { Shaft: { direction: [1, 0, 0], stage: 1.4 } }, }); ```\n- `hexNut(acrossFlats: number, height: number, holeDia: number): Shape` — Generic hex nut with a cylindrical bore. Constructed via intersection of three rotated rectangular slabs, then a bore is subtracted. Centered at origin, height along Z. For standard ISO metric nuts by thread size, use `lib.nut` instead.\n- `bracket(width: number, height: number, depth: number, thick: number, holeDia?: number): Shape` — L-shaped mounting bracket with optional through-holes. Produces a right-angle bracket: a horizontal base plate and a vertical wall. Both legs share `width`. Optional holes are drilled through the base (along Z) and the wall (along Y).\n- `holePattern(rows: number, cols: number, spacingX: number, spacingY: number, holeDia: number, depth: number): Shape` — Rectangular grid of cylindrical hole cutters. Returns the union of `rows × cols` cylinders laid out on a regular grid. Subtract from a solid to produce the full pattern. **Example** ```ts const pattern = lib.holePattern(3, 4, 20, 20, 4, 10); const panel = box(80, 70, 10).subtract(pattern.translate(-30, -20, 0)); ```\n- `thread(diameter: number, pitch: number, length: number, options?: { depth?: number; segments?: number; }): Shape` — External helical thread — clean mesh, no SDF grid artifacts. **Details** Builds a cross-section with a single trapezoidal tooth from the root radius out to the crest radius, then twist-extrudes it so the tooth traces a helix. Manifold's extrude+twist produces structured quad-based geometry that follows the thread profile cleanly. Returns a threaded cylinder along +Z from z=0 to z=length. **Example** ```ts const t = lib.thread(5, 0.8, 12); // M5 × 0.8 pitch, 12 mm long ```\n- `bolt(diameter: number, length: number, options?: { ... }): Shape` — ISO-style hex bolt with real helical threads. **Details** The hex head sits from z=0 up to z=headHeight. The shaft extends downward along −Z by `length` mm. An unthreaded shank section is included when `threadLength < length`. Default proportions follow ISO 4762 loosely: pitch ≈ 0.15×diameter, head height ≈ 0.65×diameter, across-flats ≈ 1.6×diameter. For standard M-size bolts pre-configured for a complete joint, use { **Example** ```ts const b = lib.bolt(5, 20); // M5 × 20 mm ```\n- `nut(diameter: number, options?: { pitch?: number; height?: number; acrossFlats?: number; segments?: number; }): Shape` — ISO-style hex nut with a threaded bore. **Details** Constructed from the intersection of three rotated slabs with a cylindrical bore subtracted. The nut is centered at the origin, height along Z. Default proportions follow ISO 4032 loosely: height ≈ 0.8×diameter, across-flats ≈ 1.6×diameter. The bore is a clearance bore (not modelled with helical threads) for rendering efficiency. For standard M-size nuts pre-configured for a complete joint, use { **Example** ```ts const n = lib.nut(5); // M5 nut ```\n- `washer(size: MetricSize, options?: { standard?: WasherStandard; segments?: number; }): Shape` — ISO metric flat washer (DIN 125-A). **Details** Returns a flat ring centered at the origin, thickness along Z. Dimensions are taken from { **Example** ```ts const w = lib.washer('M5'); // DIN 125-A M5 washer ```\n- `fastenerSet(size: MetricSize, boltLength: number, options?: FastenerSetOptions): FastenerSetResult` — Complete ISO metric fastener set — bolt, nut, optional washers, and matching hole cutters. **Details** Returns all geometry for one bolted joint: the bolt, nut, up to two washers, a clearance-hole cutter, and a tap-drill cutter. All shapes are returned **un-positioned** (each on the Z-axis). Place them with `.translate()`. Sizes outside M4–M10 are supported for the washer (M2–M10); unsupported combinations will throw. **Example** ```ts const hw = lib.fastenerSet('M5', 20); const topPlate = box(60, 40, 8).translate(0, 0, 12) .subtract(hw.clearanceHole.translate(15, 10, 12)); const botPlate = box(60, 40, 8) .subtract(hw.clearanceHole.translate(15, 10, 0)); return [ { name: 'Top Plate', shape: topPlate }, { name: 'Bot Plate', shape: botPlate }, { name: 'Bolt', shape: hw.bolt.translate(15, 10, 20) }, { name: 'Nut', shape: hw.nut.translate(15, 10, -4) }, ]; ```\n- `boltedServiceCover(options: BoltedServiceCoverOptions): BoltedServiceCoverResult` — Bolted service-cover interface with real seats, aligned holes, gasket, fused pull tabs, and installed screws. **Details** This is a higher-level mechanical pattern for the common \"removable service cover\" failure mode. It creates the parent ledge, cover, gasket, and screws from one shared bolt pattern so agents do not place decorative screw heads or floating pull tabs by eye. Coordinate convention: the parent frame sits from `z=0` to `parentThickness`, the gasket sits on the ledge, the cover sits above the gasket, and screw shafts run downward through the cover into the parent. All parts are centered on the XY origin. **Example** ```ts const cover = lib.boltedServiceCover({ width: 90, depth: 56, screwSize: 'M4', ledgeWidth: 10, boltInset: [6, 6], }); verify.equal('four retained cover screws', cover.screws.length, 4); return cover.parts; ```\n- `datumEnclosureAssembly(options: DatumEnclosureAssemblyOptions): DatumEnclosureAssemblyResult` — Datum-driven enclosure tray with shared wall, ledge, standoff, cover, gasket, port, and screw geometry. **Details** This pattern is for electronics boxes, thermostat backplates, service-stack housings, camera housings, and small fixtures where generated models often place panels, ribs, bosses, ports, and covers by eye. The tray, internal ledges, standoffs, ribs, service port, gasket, cover holes, and installed screws all come from one datum system. This keeps screw axes, boss locations, wall thickness, and service openings aligned instead of relying on independent magic numbers. Coordinate convention: X/Y are the enclosure footprint, Z is up. The base tray starts at `z=0` and rises to `height`; the gasket and cover sit above the top ledge with small explicit face clearances. **Example** ```ts const enclosure = lib.datumEnclosureAssembly({ width: 96, depth: 64, height: 18, }); verify.notColliding('cover clears enclosure gasket', enclosure.cover, enclosure.gasket); verify.inRange('cover stack has small seating clearance', enclosure.dims.faceClearance, 0.01, 0.08); return enclosure.parts; ```\n- `snapLatchCoverAssembly(options: SnapLatchCoverAssemblyOptions): SnapLatchCoverAssemblyResult` — Snap-retained cover with a receiver frame, latch windows, underside catch lands, and fused snap hooks. **Details** This pattern is for covers, cartridges, clasps, and small housings where agents often add decorative tabs without a catch. The receiver has a real service opening plus two clearance latch windows. The cover is one fused part with two flexible-looking snap fingers that pass through the windows and barb under the receiver underside. Nothing intersects in the final assembly; the hook geometry sits close enough to the catch lands to prove retention intent. Coordinate convention: the receiver frame sits from `z=0` to `parentThickness`; the cover is seated just above the receiver on +Z. Two snap hooks sit on the +/-Y ledges and tuck under the receiver. **Example** ```ts const snapCover = lib.snapLatchCoverAssembly({ width: 72, depth: 44, }); verify.notColliding('snap hooks clear receiver windows', snapCover.cover, snapCover.parent); verify.inRange('snap cover has small seating clearance', snapCover.dims.faceClearance, 0.01, 0.08); return snapCover.parts; ```\n- `pinnedLeverAssembly(options: PinnedLeverAssemblyOptions): PinnedLeverAssemblyResult` — Retained pinned lever stack with a fused hub/arm/grip, low stop land, pivot pin, bore cutters, and thrust washers. **Details** This pattern is for the common handle/lever failure mode where a visual arm, hub, washer, and pin are placed near each other but never form a credible mechanism. The lever body is one fused part, the pin runs through aligned bores, washers sit on both sides of the lever, and the support includes a bearing land plus an optional low stop land beside the lever path. Coordinate convention: pivot axis is +Z at the XY origin. The support starts at `z=0`, the lower washer sits on top of the support, the lever sits on the lower washer, the upper washer sits on the lever, and the retained pin spans the full stack. **Example** ```ts const lever = lib.pinnedLeverAssembly({ armLength: 54, armWidth: 10, pinDiameter: 5, }); verify.equal('lever stack has five retained parts', lever.parts.length, 5); return lever.parts; ```\n- `retainedShaftAssembly(options: RetainedShaftAssemblyOptions): RetainedShaftAssemblyResult` — Retained shaft, washer, knob, and support-cheek stack for trunnions, pivots, and adjustable clamps. **Details** This pattern replaces the common \"pin, washers, and knob are near each other\" visual shortcut with a mechanically accountable shaft stack. The two support cheeks get matching clearance bores, the through shaft spans the whole stack, washers and knobs share the same axis, and retaining heads keep the knobs from reading as loose floating cylinders. Coordinate convention: the shaft axis is +X through the world origin. Support cheeks are centered at `x = +/- supportSpacing / 2`. The supports are bored for clearance, so collision inspection should report no support/shaft overlap while the connectivity audit still sees one retained stack. **Example** ```ts const trunnion = lib.retainedShaftAssembly({ supportSpacing: 96, shaftDiameter: 8, supportHeight: 42, }); verify.equal('retained shaft stack has seven parts', trunnion.parts.length, 7); return trunnion.parts; ```\n- `capturedLinearSlide(options: CapturedLinearSlideOptions): CapturedLinearSlideResult` — Captured linear slide with a U-channel rail, return lips, end stops, and a carriage posed inside the guide. **Details** This pattern is for drawer-slide, quick-release plate, and guided-carriage models where agents often place rail details and a moving block near each other without a capture relationship. The rail is one fused part with side walls, inward lips, and end stops; the carriage is wider than the lip throat but narrower than the inner rail width, so it is mechanically captured while retaining explicit clearance. Coordinate convention: rail length is along X, width is along Y, and Z is up. The rail base starts at `z=0`; the carriage sits above the base and below the return lips. `travel=0` places the carriage at the negative-X end of travel, and `travel=maxTravel` places it at the positive-X end. **Example** ```ts const slide = lib.capturedLinearSlide({ length: 160, carriageLength: 52, travel: 42, }); verify.greaterThan('carriage is captured by return lips', slide.dims.carriageWidth, slide.dims.throatWidth); return slide.parts; ```\n- `capturedCartridgeGuideAssembly(options: CapturedCartridgeGuideAssemblyOptions): CapturedCartridgeGuideAssemblyResult` — Captured removable cartridge guide with return lips, rear stop, wide cartridge flange, and pull tab. **Details** This pattern is for pump cartridges, filter cassettes, skeg cassettes, battery cartridges, and slide-in service modules where generated models often place a tray and a loose block near each other. The guide is one fused part with side walls, inward return lips, and a rear stop. The cartridge has a wide lower flange captured under the lips and a narrower body that passes through the throat, so the model has a real retention contract without manual coordinate tuning. Coordinate convention: insertion travel is along +X. The open entry is at −X, the rear stop is at +X, the guide base starts at `z=0`, and `insertion=0` places the cartridge at the front travel limit. **Example** ```ts const cassette = lib.capturedCartridgeGuideAssembly({ length: 150, cartridgeLength: 72, }); verify.notColliding('cartridge clears guide rails', cassette.cartridge, cassette.guide); verify.greaterThan('cartridge flange is captured by lips', cassette.dims.cartridgeWidth, cassette.dims.throatWidth); return cassette.parts; ```\n- `livingHingeCoverAssembly(options: LivingHingeCoverAssemblyOptions): LivingHingeCoverAssemblyResult` — One-piece molded living-hinge cover strip with a fixed leaf, thin flexible web, cover leaf, pull lip, snap barb, and catch land. **Details** This pattern is for small polypropylene-style lids, battery doors, sample covers, blister latches, and molded service flaps where generated models often draw a decorative hinge strip between two disconnected plates. It returns one fused molded part in its as-molded flat state: fixed mounting leaf, thin hinge web, moving cover leaf, pull lip, raised snap barb, and catch land. The flexible web is intentionally much thinner than the rigid leaves and shares material with both leaves. Coordinate convention: X is hinge length/part width, Y runs from fixed leaf through hinge web to cover leaf, and Z is thickness. The hinge web is centered on `y=0`; the fixed leaf lies at −Y and the cover leaf at +Y. **Example** ```ts const livingCover = lib.livingHingeCoverAssembly({ width: 64, coverDepth: 42, }); verify.greaterThan('living hinge is much thinner than rigid leaves', livingCover.dims.flexRatio, 3); return livingCover.parts; ```\n- `knuckledHingeAssembly(options: KnuckledHingeAssemblyOptions): KnuckledHingeAssemblyResult` — Alternating knuckle hinge with two fused leaves and a retained pin. **Details** This pattern replaces hand-placed hinge barrels and pin ghosts with a mechanically accountable hinge. The fixed leaf owns every other knuckle, the moving leaf owns the alternating knuckles, all knuckles share one bore size, and the retained pin spans the full stack with heads outside the barrels. Coordinate convention: the hinge pin axis is +X through the world origin. The fixed leaf extends toward +Y. The moving leaf extends toward -Y and rotates about +X by `openAngleDeg`. **Example** ```ts const hinge = lib.knuckledHingeAssembly({ length: 70, leafLength: 28, openAngleDeg: 45, }); verify.equal('hinge has two leaves and one retained pin', hinge.parts.length, 3); return hinge.parts; ```\n- `clevisPinJointAssembly(options?: ClevisPinJointAssemblyOptions): ClevisPinJointAssemblyResult` — Clevis-style pin joint with bored yoke ears, a center link eye, and a retained pin. **Details** This pattern is for crank links, damper rod ends, pump crossheads, capo/cam pivots, and small mechanism joints where agents often place an eyelet and a pin near a bracket without modeling the captured load path. The clevis is one fused part with two bored ears and a rear bridge, the center link has a real eye and arm, and the retained pin spans the full stack with heads outside the ears. Coordinate convention: the pin axis is +Y through the world origin. The center link arm extends toward +X. The clevis bridge sits behind the eye on -X, leaving the link eye clear inside the yoke. **Example** ```ts const clevis = lib.clevisPinJointAssembly({ pinDiameter: 4, linkArmLength: 38, }); verify.equal('clevis joint has three retained parts', clevis.parts.length, 3); return clevis.parts; ```\n- `seatedBearingAssembly(options: SeatedBearingAssemblyOptions): SeatedBearingAssemblyResult` — Seated radial-bearing support with a real counterbore, shoulder, through shaft, and retaining collars. **Details** This pattern is for purchased bearings, rollers, burr-cartridge shafts, and small spindle supports where agents often place a ring and a shaft near a block without modelling the pocket that locates the bearing. The housing includes a through-bore and a larger counterbore that leaves a shoulder for the bearing outer race. The shaft is smaller than the bearing bore and carries collars outside the housing, so collision checks can distinguish intended clearance from impossible overlap. Coordinate convention: the shaft axis is +Z through the world origin. The housing block starts at `z=0`, the raised boss is on top of the block, the bearing is seated from the top counterbore, and the shaft extends above and below the housing. **Example** ```ts const bearingStack = lib.seatedBearingAssembly({ bearingOuterDiameter: 22, bearingInnerDiameter: 8, bearingWidth: 7, }); verify.greaterThan('housing has wall around bearing pocket', bearingStack.dims.bossOuterDiameter - bearingStack.dims.pocketDiameter, 4); return bearingStack.parts; ```\n- `cableGlandAnchorAssembly(options: CableGlandAnchorAssemblyOptions): CableGlandAnchorAssemblyResult` — Cable, wire, or tube gland anchor with a real panel hole, hollow gland body, compression nut, and routed cable. **Details** This pattern is for pumps, filters, electronics boxes, vents, monitors, and fixtures where generated models often leave hoses or cables terminating in space. It creates the receiving panel hole, a hollow gland body with a panel-side flange seated in a shallow pocket, a hollow compression nut, and a cable/tube that runs through the gland bore with explicit clearance. Coordinate convention: the cable axis is +X through the world origin. The panel is centered around `x=0` with thickness along X; the flange sits on the +X side of the panel and the compression nut sits on the −X side. The cable spans the full anchor. **Example** ```ts const anchor = lib.cableGlandAnchorAssembly({ cableDiameter: 6, panelThickness: 3, }); verify.notColliding('cable clears gland bore', anchor.cable, anchor.gland); verify.clearanceBetween('gland flange is seated at panel pocket', anchor.gland, anchor.panel, 0.01, 0.2); return anchor.parts; ```\n- `hoseBarbPortAssembly(options: HoseBarbPortAssemblyOptions): HoseBarbPortAssemblyResult` — Hose-barb pump/filter port with a bored receiver, shoulder, barb ridges, installed hose, and clamp band. **Details** This pattern is for pump heads, filters, vents, lab cartridges, and fluid fittings where generated models often leave tubes ending near a block. The receiver has a real through-port and raised boss, the fitting is hollow with a shoulder and multiple barb ridges, and the hose is modeled as an installed tube over the barb envelope with a clamp band. The hose bore is sized for the deformed installed hose, so collision checks distinguish the retained interface from impossible solid overlap. Coordinate convention: the fluid axis is +X through the world origin. The receiver block is centered around `x=0`; the raised boss and hose are on the +X side. **Example** ```ts const hosePort = lib.hoseBarbPortAssembly({ hoseInnerDiameter: 6, hoseOuterDiameter: 10, }); verify.notColliding('hose clears barb peaks', hosePort.hose, hosePort.fitting); verify.inRange('fitting shoulder seats near boss face', hosePort.dims.faceClearance, 0.01, 0.08); return hosePort.parts; ```\n- `routedTubeClipAssembly(options: RoutedTubeClipAssemblyOptions): RoutedTubeClipAssemblyResult` — Routed tube or cable retained by saddle clips with real bores, screw holes, and installed screws. **Details** This pattern is for hoses, wires, pump tubes, sensor leads, and appliance cable runs where generated models often draw a cylinder near a wall without clips or strain relief. The base panel has receiving screw envelopes, each saddle clip has a real through-bore around the tube and vertical screw clearances, and the installed screws share those positions. Coordinate convention: the routed tube runs along +X through the world origin. The base panel starts at `z=0`; clips sit on top of the panel, and the tube passes through their bores. **Example** ```ts const route = lib.routedTubeClipAssembly({ tubeDiameter: 6, clipCount: 3, }); verify.notColliding('tube clears clip bores', route.tube, union(...route.clips)); verify.notColliding('clip screws clear retained stack', union(...route.screws), union(route.panel, ...route.clips)); return route.parts; ```\n- `pcbTerminalBlockAssembly(options?: PcbTerminalBlockAssemblyOptions): PcbTerminalBlockAssemblyResult` — PCB terminal-block stack with a backplate, standoffs, mounting screws, pin holes, and a seated terminal block. **Details** This pattern is for thermostat backplates, appliance control panels, sensor boards, and small electronics where generated models often place a terminal block, screw heads, and holes as independent decorations. The PCB mounting holes, fused standoffs, installed screws, terminal pins, and PCB pin clearances all come from one shared datum system so the purchased block is mechanically seated and the board is actually mounted. Coordinate convention: X/Y are the board footprint, Z is up. The backplate starts at `z=0`, standoffs rise from the plate, the PCB rests on the standoffs, and the terminal block sits on top of the PCB near the front edge. **Example** ```ts const terminalStack = lib.pcbTerminalBlockAssembly({ terminalCount: 5, screwSize: 'M3', }); verify.notColliding('terminal pins clear PCB holes', terminalStack.terminalBlock, terminalStack.pcb); verify.notColliding('mounting screws clear PCB and standoff holes', union(...terminalStack.screws), union(terminalStack.pcb, terminalStack.backplate)); return terminalStack.parts; ```\n- `thumbScrewClampAssembly(options?: ThumbScrewClampAssemblyOptions): ThumbScrewClampAssemblyResult` — Thumb-screw clamp with a C-frame, threaded boss, captive pressure pad, knob, and clamped workpiece. **Details** This pattern is for bench clamps, monitor-arm desk clamps, small vise screws, capo pressure screws, fixture hold-downs, and service brackets where generated models often place a loose screw, knob, or pressure pad near a bracket. The helper creates a one-piece clamp frame with a fixed anvil pad, a bored threaded support and boss, an installed screw with a captive pressure pad and hand knob, and a representative clamped workpiece seated between the pads. Coordinate convention: the clamp screw runs along +X. The fixed anvil is on the -X side, the threaded support and knob are on the +X side, and Z is up from the base bridge. **Example** ```ts const clamp = lib.thumbScrewClampAssembly({ screwSize: 'M6', workpieceThickness: 20, }); verify.notColliding('thumb screw clears threaded boss', clamp.clampScrew, clamp.frame); verify.clearanceBetween('pressure pad is seated on workpiece', clamp.clampScrew, clamp.workpiece, -0.01, 0.05); return clamp.parts; ```\n- `pipeRoute(points: [ number, number, number ][], radius: number, options?: { bendRadius?: number; wall?: number; segments?: number; }): Shape` — Route a pipe (solid or hollow) through 3D waypoints with smooth bends. Each interior waypoint gets a torus-section bend. Straight segments connect them. Returns a single unioned Shape.\n- `elbow(pipeRadius: number, bendRadius: number, angle?: number | { ... }, options?: { ... }): Shape` — Pipe elbow — a curved pipe section (torus arc) for connecting two pipe directions. By default creates a bend in the XZ plane: incoming along +Z, outgoing rotated by `angle`. The bend starts at the origin, curving away from it.\n- `beltDrive(options: BeltDriveOptions): BeltDriveResult` — Create a flat open-belt body around two pulley pitch circles. The belt is generated as a tangent loop in the XY plane and extruded along +Z by `beltWidth`. The result includes the solid belt, the 2D belt profile, a thin pitch-path sketch for visualization, total belt length, tangent spans, and wrap metadata for each pulley. For more than two pulleys, the API intentionally asks for route intent before geometry is created. Use `route: \"outer\"` for the future outside-envelope mode, or an ordered route for future serpentine/idler layouts. ```ts const drive = lib.beltDrive({ pulleys: [ { name: \"motor\", center: [0, 0], pitchRadius: 12 }, { name: \"output\", center: [80, 0], pitchRadius: 28 }, ], beltWidth: 8, beltThickness: 2, }); return drive.belt; ```\n- `tangentLoop2d(circles: TangentCircle2D[], options?: TangentLoop2DOptions): TangentLoop2D` — Build a closed 2D route made from common tangent spans and pulley wrap arcs. Use this when you need reusable belt/chain route geometry before creating a solid body. The first implementation supports two circles. `mode: \"open\"` uses external tangents; `mode: \"crossed\"` uses internal tangents. ```ts const route = lib.tangentLoop2d([ { center: [0, 0], radius: 12 }, { center: [80, 0], radius: 28 }, ]); const belt = route.offsetBand(2).extrude(8); ```\n- `tSlotProfile(options?: TSlotProfileOptions): Sketch` — Build a 2D T-slot cross-section sketch. Default parameters describe a 20x20 B-type profile with slot 6. Use this when you want a drawing-ready profile sketch before extrusion.\n- `tSlotExtrusion(length: number, options?: TSlotExtrusionOptions): Shape` — Build a T-slot extrusion from the generated 2D profile. Extrudes along +Z by default.\n- `profile2020BSlot6Profile(options?: Profile2020BSlot6ProfileOptions): Sketch` — Accurate-ish 2D profile for 20x20 B-type slot 6. Returns a drawing-ready Sketch centered at origin.\n- `profile2020BSlot6(length: number, options?: Profile2020BSlot6Options): Shape` — 20x20 B-type slot 6 extrusion with profile-accurate defaults. Pass option overrides if your supplier's profile differs slightly.\n- `spurGear(options: SpurGearOptions): Shape` — Involute external spur gear with optional center bore. Specify module, teeth, faceWidth as required parameters. Optional tuning includes pressureAngleDeg (default 20), backlash, clearance, addendum, dedendum, boreDiameter, and segmentsPerTooth (default 10). **Connectors (for assembly-based positioning):** - `bore`: revolute connector at the bore center, axis along +Z. Carries measurements: `{ module, teeth, pitchRadius, outerRadius, faceWidth }`. Use `.connect(\"Housing.seat\", \"Gear.bore\")` to mount a gear on a shaft seat.\n- `bevelGear(options: BevelGearOptions): Shape` — Conical bevel gear generated from a tapered involute extrusion. Specify pitchAngleDeg directly or derive it from mateTeeth + shaftAngleDeg. **Connectors (for assembly-based positioning):** - `bore`: revolute connector at the large-end bore center (Z=0), axis along -Z (away from teeth). - `apex`: connector at the cone apex above the gear (the point where the pitch cone converges), axis along +Z. Useful for meshing two bevel gears — their apices should coincide. Carries measurements: `{ module, teeth, pitchRadius, pitchAngleDeg, coneDistance, faceWidth }`.\n- `faceGear(options: FaceGearOptions): Shape` — Face gear (crown style) where teeth are on one face (top or bottom) instead of the outer rim. Uses the same involute tooth sizing as spurGear, then projects the tooth band axially from one side. Alias for sideGear (which is kept for backward compatibility).\n- `sideGear(options: SideGearOptions): Shape` — Crown/face style gear where the teeth project from one side of the disk instead of the outer cylindrical rim.\n- `ringGear(options: RingGearOptions): Shape` — Internal ring gear with involute-derived tooth spaces. Specify rimWidth or outerDiameter for the annular body. **Connectors (for assembly-based positioning):** - `bore`: connector at the ring center, axis along +Z. For planetary gearboxes, this is where the ring mounts to the housing. Carries measurements: `{ module, teeth, pitchRadius, innerRadius, outerRadius, faceWidth }`.\n- `rackGear(options: RackGearOptions): Shape` — Linear rack gear with pressure-angle flanks. Use with spurGear for rack-and-pinion mechanisms. **Orientation:** teeth run along the X axis with tooth tips pointing +Y (pitch line at Y=0). The rack is extruded +Z by `faceWidth`. Rotate the rack to align with a different slide axis. **Connectors (for assembly-based positioning):** - `teeth`: prismatic connector at the pitch line center, axis along +X (slide direction). Carries measurements: `{ module, teeth, faceWidth, length }`. Connect to a housing's rack channel: ```js housing.withConnectors({ rack_channel: connector(\"rack-channel\", { origin: [pitchR, 0, channelZ], axis: [1, 0, 0], kind: \"prismatic\", }), }); assembly.connect(\"Housing.rack_channel\", \"Rack.teeth\", { as: \"slide\" }); ```\n- `gearPair(options: GearPairOptions): GearPairResult` — Build or validate a spur-gear pair and return ratio, backlash, and mesh diagnostics. Accepts either shapes from spurGear() or analytical specs for each member. When place is true (default), the gear is auto-positioned at the correct center distance.\n- `bevelGearPair(options: BevelGearPairOptions): BevelGearPairResult` — Build or validate a bevel-gear pair and return ratio diagnostics plus recommended joint placement vectors.\n- `faceGearPair(options: FaceGearPairOptions): FaceGearPairResult` — Build or validate a perpendicular pair between a face gear and a vertical spur gear.\n- `sideGearPair(options: SideGearPairOptions): SideGearPairResult` — Pair helper for side (crown/face) gear + perpendicular \"vertical\" spur gear. Auto-placement rotates the spur around +Y and positions it to mesh at the side tooth band.\n- `gearRatio(teethA: number, teethB: number, options?: { internal?: boolean; }): number` — Coupling ratio between two meshed spur gears. When gear A turns 1°, gear B turns `-teethA / teethB` degrees (negative because meshed external gears rotate in opposite directions). Pass `{ internal: true }` for internal gear pairs (ring gear + spur/planet), where the two rotate in the same direction.\n- `rackRatio(module: number, pinionTeeth: number): number` — Coupling ratio between a pinion and a rack. When the pinion rotates by `θ` degrees, the rack slides by `θ × (π × module × teeth / 360)` mm. Equivalently, 1mm of rack travel = `180 / (π × pitchRadius)` degrees of pinion rotation.\n- `planetaryRatio(sunTeeth: number, ringTeeth: number): number` — Planetary gear reduction ratio when the ring is held fixed. Input: sun. Output: carrier. Ratio: `1 + ringTeeth / sunTeeth`. One turn of the sun produces `1 / ratio` turns of the carrier.\n- `boltPattern(options: BoltPatternOptions): BoltPattern` — Define a bolt pattern once and cut it from multiple parts. const base = bolts.cut(box(60, 50, 10), 12, { from: -1 }); const cover = bolts.cut(box(60, 50, 3), 5, { from: -1 }); // Same positions in both parts — guaranteed aligned. ```\n- `driveWheel(options?: DriveWheelOptions): DriveWheelBuilder` — Start a composable exceptional gear or drive wheel.\n- `readDriveWheelMeta(shape: Shape): DriveWheelMeta | null` — Read the functional-region metadata attached by `driveWheel().build()`.\n- `sectorGear(options: SectorGearOptions): Shape` — Involute sector gear with teeth on only part of the pitch circle. Specify the full-circle pitch as `teethOnFullCircle`, then choose the active tooth window with `firstTooth` and `toothCount`. The body is separate from the tooth region: pass a `gearBody...` shape for spokes, hubs, and product styling, or omit it for a simple root-radius disk. **Example** ```ts const body = lib.gearBodies.spoked({ outerRadius: 22, rimWidth: 3, hubDiameter: 10, spokeCount: 5, spokeWidth: 2.5, faceWidth: 8, boreDiameter: 5, }); const sector = lib.sectorGear({ module: 1.25, teethOnFullCircle: 36, toothCount: 10, faceWidth: 8, body, }); ```\n- `gearBodies: { ... }` — Gear body preset namespace: disk, diskWithHub, spoked, and fromProfile.\n- `gearBodyDisk(options: GearBodyDiskOptions): Shape` — Solid disk/ring gear body, independent from any tooth geometry.\n- `gearBodyDiskWithHub(options: GearBodyDiskWithHubOptions): Shape` — Disk gear body with a raised center hub.\n- `gearBodySpoked(options: GearBodySpokedOptions): Shape` — Spoked gear body with an outer rim, center hub, and radial spokes.\n- `gearBodyFromProfile(profile: Sketch, options: GearBodyFromProfileOptions): Shape` — Extrude a custom 2D profile into a gear body.\n\n---\n\n<!-- generated/wood.md -->\n\n# Woodworking\n\nWood boards with grain/species metadata, and joinery operations: dado, rabbet, mortise & tenon. Access via `Wood.*`.\n\n## Contents\n\n- [WoodBoard](#woodboard)\n- [Wood](#wood)\n\n---\n\n## Classes\n\n### `WoodBoard`\n\nA board of wood with metadata for manufacturing: grain direction, species, and dimensions. The underlying geometry is a simple box.\n\nWoodBoard operations are immutable. Joint operations return new boards instead of carving the original in-place, and transform methods preserve all metadata.\n\n**Properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `shape` | `Shape` | The underlying 3D shape. |\n| `width` | `number` | Board width (mm) — the longer flat dimension |\n| `height` | `number` | Board height (mm) — the shorter flat dimension |\n| `thickness` | `number` | Board thickness (mm) |\n| `grain` | `string` | Grain direction: \"long\" or \"cross\" |\n| `species` | `string` | Wood species, e.g. \"birch\", \"oak\" |\n| `material` | `string` | Material label for BOM |\n\n**Methods:**\n\n#### `cut()` — Subtract a cutter from this board, returning a new board. Used by joint functions (dado, rabbet, mortiseAndTenon).\n\n```ts\ncut(cutter: Shape): WoodBoard\n```\n\n#### `translate()` — Translate the board in 3D space.\n\n```ts\ntranslate(x: number, y: number, z: number): WoodBoard\n```\n\n#### `rotate()` — Rotate the board around an axis by a given angle in degrees.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): WoodBoard\n```\n\n#### `rotateX()` — Rotate the board around the X axis by a given angle in degrees.\n\n```ts\nrotateX(angleDeg: number): WoodBoard\n```\n\n#### `rotateY()` — Rotate the board around the Y axis by a given angle in degrees.\n\n```ts\nrotateY(angleDeg: number): WoodBoard\n```\n\n#### `rotateZ()` — Rotate the board around the Z axis by a given angle in degrees.\n\n```ts\nrotateZ(angleDeg: number): WoodBoard\n```\n\n#### `mirror()` — Mirror the board across a plane defined by its normal.\n\n```ts\nmirror(normal: [ number, number, number ]): WoodBoard\n```\n\n#### `color()` — Set the board's display color.\n\n```ts\ncolor(value: string): WoodBoard\n```\n\n#### `clone()` — Clone the board (creates an independent copy of the underlying shape).\n\n```ts\nclone(): WoodBoard\n```\n\n---\n\n## Constants\n\n### `Wood`\n\nWoodworking namespace — create boards and cut joints.\n\n**Boards:** `Wood.board()` creates a WoodBoard with grain, species, and BOM metadata.\n\n**Joints:** `Wood.dado()`, `Wood.rabbet()`, and `Wood.mortiseAndTenon()` are immutable — they return new board value(s) with the joint cut applied.\n\n- `readonly board: (width: number, height: number, thickness: number, opts?: WoodBoardOptions) => WoodBoard` — Create a wood board with metadata for manufacturing. The board is a box(width, height, thickness) centered on XY, base at Z=0. Width along X, height along Y, thickness along Z (0 to thickness).\n- `dado(host: WoodBoard, guest: WoodBoard, opts: DadoOptions): WoodBoard` — Cut a dado (channel) across the face of a host board for a guest board to sit in. Returns a new host board with the dado cut applied.\n- `rabbet(board: WoodBoard, opts: RabbetOptions): WoodBoard` — Cut a rabbet (L-shaped step) along an edge of a board. Returns a new board with the rabbet cut applied.\n- `mortiseAndTenon(mortiseBoard: WoodBoard, tenonBoard: WoodBoard, opts?: MortiseAndTenonOptions): MortiseAndTenonResult` — Cut a mortise in one board and shape a tenon on another. Returns new boards with the mortise pocket and tenon cuts applied.\n\n---\n\n<!-- generated/viewport.md -->\n\n# Viewport & Runtime\n\nCut planes, exploded views, joint animations, and scene configuration.\n\n## Contents\n\n- [Viewport & Runtime](#viewport-runtime) — `Viewport.label`, `scene`, `viewConfig`, `explodeView`, `jointsView`, `cutPlane`, `mock`, `showLabels`, `highlight`\n- [RouteBuilder](#routebuilder)\n- [route](#route)\n\n## Functions\n\n### Viewport & Runtime\n\n#### `Viewport.label()` — Add a render-only viewport label at a world-space point.\n\n`Viewport.label()` is for temporary review, debug, tutorial, or explicitly requested presentation overlays. It does not create sketches, meshes, B-rep topology, exported text, or face labels, so it stays off the OCCT path. Default production models should be understandable from physical geometry, materials, part boundaries, and named objects, not viewport annotations.\n\nUse [`text2d()`](/docs/sketch#text2d) only when the letters should become manufactured geometry, such as raised lettering, engraved serial numbers, or exported nameplates.\n\nLabels are collected during script execution and rendered by the viewport as lightweight overlay annotations. They are ignored by exports and do not appear in `objects`.\n\n```js\nViewport.label('Bearing bore', [0, 0, 18], {\n color: '#f8fafc',\n background: '#0f172acc',\n offset: [0, 0, 8],\n anchor: 'bottom',\n});\n\nreturn box(40, 30, 12);\n```\n\n```ts\nViewport.label(text: string, at: [ number, number, number ], options?: RenderLabelOptions): void\n```\n\n**`RenderLabelOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `color?` | `string` | Text color as any CSS color string. |\n| `background?` | `string` | Background color as any CSS color string. Use `'transparent'` for no pill background. |\n| `size?` | `number` | Font size in CSS pixels. Defaults to 12. |\n| `offset?` | `[ number, number, number ]` | Additional world-space offset from `at`. |\n| `anchor?` | `RenderLabelAnchor` | Which point of the label box is anchored to `at`. Defaults to `'center'`. |\n| `alwaysOnTop?` | `boolean` | When false, the label is hidden when occluded by scene geometry. Defaults to true. |\n\n#### `scene()` — Configure the scene environment for the current script execution.\n\nControls camera position, named render views, optional model journeys, lighting rig, background color or gradient, atmospheric fog, environment maps, post-processing effects, and capture parameters for the `forgecad capture` command. Multiple calls merge — later values override earlier ones on a per-key basis, so you can split configuration across multiple `scene()` calls.\n\nWhen `lights` is specified, **all** default lights are removed. You must include your own ambient light or the scene will be fully dark.\n\nSetting `camera.position` overrides auto-framing — the viewport will no longer auto-fit the geometry on script reload.\n\nNamed render views let scripts check in repeatable cameras next to the model code. The canonical shape is `{ camera: { position, target } }`, and a direct camera shorthand `{ position, target }` is also accepted. Use the canonical shape when you may add view metadata later. Use it from the CLI with `--view hero` on `forgecad render 3d`, `forgecad render hq`, or `forgecad capture`.\n\nModel journeys let scripts check in a compact guided path through named objects. Each journey has ordered `steps`; each step can name a `focus` target by object name/tree path, provide a caption, and optionally provide an explicit camera. In the viewer, journeys are opt-in: they appear as a small Explore control and do not move the camera until the user starts them. Use `forgecad run model.forge.js --journeys` or `--journeys-json` to inspect resolved targets.\n\nPost-processing effects (`bloom`, `vignette`, `grain`) work in the browser viewport only. The CLI applies camera, lights, background, fog, and `toneMappingExposure` but skips shader effects.\n\nAll numeric values accept `param()` expressions.\n\n```js\nscene({\n background: { top: '#000814', bottom: '#001d3d' },\n camera: { position: [160, -120, 100], target: [0, 0, 50], fov: 52 },\n views: {\n hero: {\n camera: { position: [180, -140, 90], target: [0, 0, 25], up: [0, 0, 1], fov: 38 },\n },\n side: { position: [240, 0, 70], target: [0, 0, 25], fov: 34 },\n },\n journeys: {\n grandTour: {\n title: 'Grand Tour',\n startsAt: 'overview',\n steps: [\n { id: 'overview', focus: 'Solar System', caption: 'Start with the whole model.' },\n { id: 'earth', focus: 'Earth', caption: 'Fit and inspect Earth.' },\n ],\n },\n },\n lights: [\n { type: 'ambient', color: '#001233', intensity: 0.08 },\n { type: 'point', position: [120, -80, 130], color: '#00f5d4', intensity: 4, distance: 400, decay: 1 },\n { type: 'point', position: [-100, 60, 20], color: '#f72585', intensity: 3, distance: 350 },\n { type: 'directional', position: [50, -30, 200], color: '#ffd60a', intensity: 1.2 },\n { type: 'hemisphere', skyColor: '#003566', groundColor: '#000814', intensity: 0.2 },\n ],\n fog: { color: '#000814', near: 100, far: 450 },\n postProcessing: {\n bloom: { intensity: param('bloom', 1.5, 0, 4), threshold: 0.5, radius: 0.7 },\n vignette: { darkness: 0.8, offset: 0.25 },\n grain: { intensity: 0.08 },\n toneMappingExposure: param('exposure', 1.5, 0.5, 4),\n },\n});\n```\n\n```ts\nscene(options: SceneOptions): void\n```\n\n**`SceneOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `capture?` | `SceneCaptureConfig` | Default capture parameters for `forgecad capture` — CLI flags override these. |\n| `background?`, `camera?`, `views?`, `journeys?`, `lights?`, `environment?`, `fog?`, `postProcessing?`, `ground?` | | — |\n\n`SceneBackgroundGradient`: `{ top: string, bottom: string }`\n\n**`SceneCameraConfig`**: `position?: [ number, number, number ]`, `target?: [ number, number, number ]`, `up?: [ number, number, number ]`, `fov?: number`, `type?: \"perspective\" | \"orthographic\"`\n\n**`SceneJourneyConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `title?` | `string` | Viewer-facing journey title. Defaults to the journey id. |\n| `startsAt?` | `string` | Optional starting step id. Defaults to the first step. |\n| `behavior?` | `\"opt-in\" \\| \"auto\"` | Whether the viewer should offer or auto-open the journey. First slice supports opt-in. |\n| `steps` | `SceneJourneyStepConfig[]` | Ordered journey spine. Branches can be added later without changing this core contract. |\n| `valid?` | `boolean` | True unless any journey or step diagnostic has level \"error\". |\n\n**`SceneJourneyStepConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `id` | `string` | Stable step id used by viewer links and Next/Back state. |\n| `title?` | `string` | Viewer-facing title. Defaults to the step id. |\n| `focus?` | `string` | Object name or slash-separated tree path to focus. |\n| `caption?` | `string` | Short optional viewer caption. |\n| `camera?` | `SceneViewCameraConfig` | Optional explicit camera for this step. When omitted, the viewer fits `focus`. |\n| `resolvedFocusId?` | `string \\| null` | Resolved object id after script execution, when `focus` matched exactly one object. |\n| `resolvedFocusPath?` | `string \\| null` | Resolved object tree path or name after script execution. |\n\n**`SceneLightConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `target?` | `[ number, number, number ]` | Target for directional/spot lights |\n| `groundColor?` | `string` | Ground color for hemisphere lights |\n| `skyColor?` | `string` | Sky color alias for hemisphere lights (same as color) |\n| `angle?` | `number` | Spot light cone angle in radians |\n| `penumbra?` | `number` | Spot light penumbra (0–1) |\n| `decay?` | `number` | Point/spot light decay |\n| `distance?` | `number` | Point/spot light distance (0 = infinite) |\n| `castShadow?` | `boolean` | Whether this light casts shadows |\n| `type`, `color?`, `intensity?`, `position?` | | — |\n\n**`SceneEnvironmentConfig`**\n- `preset?: \"studio\" | \"sunset\" | \"dawn\" | \"warehouse\" | \"forest\" | \"apartment\" | \"lobby\" | \"city\" | \"park\" | \"night\" | \"none\"` — Built-in preset name or 'none' to disable\n- `intensity?: number` — Environment map intensity\n- `background?: boolean` — Use environment map as scene background\n\n**`SceneFogConfig`**\n- `near?: number` — Linear fog near distance\n- `far?: number` — Linear fog far distance\n- `density?: number` — Exponential fog density (if set, uses FogExp2 instead of linear Fog)\n- Also: `color?: string`\n\n`ScenePostProcessingConfig`: `{ bloom?: SceneBloomConfig, vignette?: SceneVignetteConfig, grain?: SceneGrainConfig, toneMappingExposure?: number }`\n\n`SceneBloomConfig`: `{ intensity?: number, threshold?: number, radius?: number }`\n\n`SceneVignetteConfig`: `{ darkness?: number, offset?: number }`\n\n`SceneGrainConfig`: `{ intensity?: number }`\n\n**`SceneGroundConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `visible?` | `boolean` | Show a ground plane |\n| `color?` | `string` | Ground color |\n| `offset?` | `number` | Offset below the model's bounding box minimum Z. Default 0 (flush with model bottom). |\n| `receiveShadow?` | `boolean` | Receive shadows on the ground |\n\n**`SceneCaptureConfig`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `framesPerTurn?` | `number` | Frames for one full orbit rotation (default: 72) |\n| `holdFrames?` | `number` | Frozen frames before motion starts (default: 6) |\n| `pitchDeg?` | `number` | Orbit pitch angle in degrees (default: auto from camera) |\n| `fps?` | `number` | Output frame rate (default: 24) |\n| `size?` | `number` | Output frame size in pixels (default: 960) |\n| `background?` | `string` | Canvas background color for capture (default: '#252526') |\n\n#### `viewConfig()` — Configure viewport helper visuals for the current script execution.\n\nControls renderer-only overlays that appear in the viewport but are not part of the geometry. Currently supports the joint overlay that renders axis arrows and arc indicators when `jointsView` is active. Multiple calls merge — later values override earlier ones per key.\n\nThis does **not** trigger a geometry recompute; it only affects the visual helpers drawn on top of the 3D scene.\n\n```js\nviewConfig({\n jointOverlay: {\n axisColor: '#13dfff',\n arcColor: '#ff7a1a',\n axisLineRadiusScale: 0.03,\n arcLineRadiusScale: 0.022,\n },\n});\n```\n\n```ts\nviewConfig(options?: ViewConfigOptions): void\n```\n\n#### `explodeView()` — Configure how the viewport explode slider offsets returned objects.\n\nOffsets are resolved from the returned object tree, not a flat list. In `radial` mode each node follows its parent branch direction, then fans locally from the immediate parent center — nested assemblies peel apart level by level. In fixed-axis or fixed-vector modes, the branch follows that axis/vector but nested descendants fan out perpendicular by default.\n\nMultiple calls merge — later values override earlier ones on a per-key basis. `byName` and `byPath` maps are merged entry-by-entry.\n\nFor programmatic explode applied before returning (without the slider), use `lib.explode()` instead.\n\n```js\nexplodeView({\n amountScale: 1.2,\n stages: [0.35, 0.8],\n mode: 'radial',\n byPath: { 'Drive/Shaft': { direction: [1, 0, 0], stage: 1.6 } },\n});\n```\n\n```ts\nexplodeView(options?: ExplodeViewOptions): void\n```\n\n**`ExplodeViewOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `enabled?` | `boolean` | Set false to disable viewport explode offsets for this script output. |\n| `amountScale?` | `number` | Scales the UI explode amount. Default: 1 |\n| `stages?` | `number[]` | Per-depth stage multipliers (depth 1 = first level). If depth exceeds this array, the last value is reused. Default when omitted: reciprocal depth (1, 1/2, 1/3, ...) |\n| `mode?` | `ExplodeViewDirection` | Global direction mode fallback. Default: 'radial' |\n| `axisLock?` | `ExplodeAxis` | Global axis lock fallback. |\n| `byName?` | `Record<string, ExplodeViewDirective>` | Per-object overrides by final object name. |\n| `byPath?` | `Record<string, ExplodeViewDirective>` | Per-tree-path overrides using slash-separated object tree segments. |\n\n**`ExplodeDirective`**\n- `stage?: number` — Multiplier applied to `amount` for this node\n- `direction?: ExplodeDirection` — Direction mode for this node\n- `axisLock?: ExplodeAxis` — Optional axis lock after direction is resolved\n\n#### `jointsView()` — Register viewport-only mechanism controls that animate returned objects without re-running the script.\n\nDefines joints (revolute or prismatic), optional gear/rack couplings, and named animations. The viewport resolves transforms through the joint chain at display time — the script geometry is computed only once at rest pose.\n\n**Critical:** Solve the assembly at **rest pose** (all animated joints = 0). The viewport applies `jointsView` transforms on top of the returned scene. If geometry is already solved at non-zero angles, animation will double-rotate everything.\n\n```js\n// BAD — double rotation\nconst solved = mech.solve({ shoulder: 45, elbow: 30 });\njointsView({ joints: [{ name: 'shoulder', ... }] });\nreturn solved;\n\n// GOOD — rest pose, jointsView controls all posing\nconst solved = mech.solve({ shoulder: 0, elbow: 0 });\njointsView({\n joints: [\n { name: 'shoulder', child: 'Upper Arm', default: 45, ... },\n { name: 'elbow', child: 'Forearm', parent: 'Upper Arm', default: 30, ... },\n ],\n});\nreturn solved;\n```\n\n**Pivot coordinates** are world-space positions of each joint origin at rest pose. For `addRevolute('shoulder', 'Base', 'Link', { frame: Transform.identity().translate(0, 0, 20) })` where \"Base\" is at world origin, the pivot is `[0, 0, 20]`.\n\n**Fixed attachments** that must follow a parent during animation need a zero-angle revolute joint in the chain:\n\n```js\n{ name: 'EE_Follow', child: 'End Effector', parent: 'Last Link',\n type: 'revolute', axis: [0, 0, 1], pivot: [linkLength, 0, 0],\n min: 0, max: 0, default: 0 }\n```\n\nAnimation values are interpolated linearly between keyframes. ForgeCAD does **not** auto-wrap revolute values across `-180/180`. Keep keyframe values continuous — a `-180 -> 171` jump spins the part the long way around. Use `-180 -> -189` instead. Author high-speed multi-turn joints as accumulating angles (`0, 360, 720, ...`) with `continuous: true`.\n\n**Tick-based keyframes:** Omit `at` from all keyframes to auto-distribute by tick weight:\n\n```js\nkeyframes: [\n { ticks: 3, values: { Shoulder: 20 } }, // slow segment (3x weight)\n { ticks: 1, values: { Shoulder: -10 } }, // fast segment (1x weight)\n { values: { Shoulder: 20 } }, // last keyframe; ticks ignored\n]\n// positions: 0, 0.75, 1.0\n```\n\nMixing explicit `at` and omitted `at` in the same animation is not allowed.\n\n```js\njointsView({\n joints: [{\n name: 'Shoulder', child: 'Upper Arm', parent: 'Base',\n type: 'revolute', axis: [0, -1, 0], pivot: [0, 0, 46],\n min: -30, max: 110, default: 15,\n }],\n animations: [{\n name: 'Walk Cycle', duration: 1.6, loop: true,\n keyframes: [\n { values: { Shoulder: 20 } },\n { values: { Shoulder: -10 } },\n { values: { Shoulder: 20 } },\n ],\n }],\n});\n```\n\n```ts\njointsView(options?: JointsViewOptions): void\n```\n\n**`JointsViewOptions`**: `enabled?: boolean`, `joints?: JointViewInput[]`, `couplings?: JointViewCouplingInput[]`, `animations?: JointViewAnimationInput[]`, `defaultAnimation?: string`\n\n**`JointViewInput`**: `name: string`, `child: string`, `parent?: string`, `type?: JointViewType`, `axis?: JointViewAxis`, `pivot?: [ number, number, number ]`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `hidden?: boolean`\n\n`JointViewCouplingInput`: `{ joint: string, terms: JointViewCouplingTermInput[], offset?: number }`\n\n`JointViewCouplingTermInput`: `{ joint: string, ratio?: number }`\n\n`JointViewAnimationInput`: `{ name: string, duration?: number, loop?: boolean, continuous?: boolean, keyframes: JointViewAnimationKeyframeInput[] }`\n\n**`JointViewAnimationKeyframeInput`**\n- `at?: number` — Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights.\n- `ticks?: number` — Relative weight of the segment from this keyframe to the next (default 1). Only used in tick-based mode (when `at` is omitted). Last keyframe's ticks value is ignored.\n- Also: `values: Record<string, number>`\n\n#### `cutPlane()` — Define a named section plane for inspecting internal geometry.\n\nRegisters a cut plane that appears as a toggle in the viewport View Panel. When enabled, geometry on the positive side of the plane (the side the normal points toward) is clipped away, revealing the internal cross-section. The newly exposed section faces render with a hatched overlay; pre-existing coplanar boundary faces are left unhatched.\n\nPlanes are registered once per script run. The viewport toggle state (on/off) persists across parameter changes without re-running the script. The `exclude` option only works correctly when the excluded object names are stable across parameter changes.\n\nAccepts two overloads: `cutPlane(name, normal, offset?, options?)` or `cutPlane(name, normal, options?)` where options may include `offset`.\n\n```js\nconst cutZ = param('Cut Height', 10, { min: -50, max: 50, unit: 'mm' });\ncutPlane('Inspection', [0, 0, 1], cutZ, { exclude: ['Probe', 'Fasteners'] });\n```\n\nOverloads:\n\n- `cutPlane(name: string, normal: [ number, number, number ], offset?: number, options?: CutPlaneOptions): void`\n- `cutPlane(name: string, normal: [ number, number, number ], options?: CutPlaneOptions): void`\n\n**`CutPlaneOptions`**\n- `offset?: number` — Optional offset along the plane normal (primarily for object-form overload).\n- `exclude?: CutPlaneExcludeInput` — Object names to keep uncut for this plane.\n\n#### `mock()` — Register a mock (context) object for visualization and collision checking.\n\nMock objects appear in the viewport and spatial analysis when you run a file directly, but are excluded when the file is imported via [`require()`](/docs/core#require). This lets you model the surrounding context — walls, bolts, mating parts — without polluting the module's exports.\n\nThe shape is returned unchanged, so you can reference it for alignment, dimensioning, and `verify` checks.\n\nMock objects participate in `forgecad run --spatial bounded|exact` collision detection and spatial analysis. Their names appear with a `(mock)` suffix in reports.\n\nIn the viewport, mock objects render at reduced opacity so they are visually distinct from real geometry.\n\n```ts\n// bracket.forge.js\nconst wall = mock(box(100, 200, 10).translate(0, 0, -5), \"wall\");\nconst bolt = mock(cylinder(3, 15).translate(10, 15, 0), \"bolt\");\n\nconst bracket = box(20, 30, 5);\nverify.notColliding(\"bracket vs wall\", bracket, wall);\n\nreturn bracket;\n// When imported: only bracket is exported\n// When run directly: bracket + wall + bolt all visible\n```\n\n```ts\nmock<T extends Shape>(shape: T, name?: string): T\n```\n\n#### `showLabels()` — Highlight all user-labeled faces on a shape for visual debugging.\n\nShows each user-authored label name in the viewport for visual debugging. Returns the shape unchanged for chaining: `return showLabels(myShape)`.\n\n```ts\nshowLabels(shape: Shape): Shape\n```\n\n#### `highlight()` — Highlight any geometry for visual debugging in the viewport.\n\nSupported inputs:\n\n- `string` — sketch entity ID (e.g. `'L0'`, `'P0'`, `'C0'`)\n- `[x, y, z]` — 3D point\n- `[[x1,y1,z1], [x2,y2,z2]]` — edge (line segment)\n- `{ normal: [x,y,z], offset: number }` — plane by normal + distance from origin\n- `{ normal: [x,y,z], point: [x,y,z] }` — plane by normal + point on plane\n- [`Shape`](/docs/core#shape) — highlight entire 3D shape\n- `FaceRef` (from `shape.face('top')`) — highlight as plane at face center\n- `EdgeRef` (from `shape.edge('left')`) — highlight as edge segment\n\nOverloads:\n\n- `highlight(entityId: string, opts?: HighlightOptions): void`\n- `highlight(point: [ number, number, number ], opts?: HighlightOptions): void`\n- `highlight(edge: [ [ number, number, number ], [ number, number, number ] ], opts?: HighlightOptions): void`\n- `highlight(plane: { normal: [ number, number, number ]; offset: number; }, opts?: HighlightOptions): void`\n- `highlight(plane: { normal: [ number, number, number ]; point: [ number, number, number ]; }, opts?: HighlightOptions): void`\n- `highlight(shape: Shape, opts?: HighlightOptions): void`\n- `highlight(face: FaceRef, opts?: HighlightOptions): void`\n- `highlight(edge: EdgeRef, opts?: HighlightOptions): void`\n\n**`HighlightOptions`**\n- `size?: number` — Size hint for points (radius in mm) or planes (disc radius in mm).\n- Also: `color?: string, label?: string, pulse?: boolean`\n\n**`FaceRef`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `normal` | `[ number, number, number ]` | Normal direction of the face |\n| `center` | `[ number, number, number ]` | Center point of the face |\n| `query?` | `FaceQueryRef` | Compiler-owned face query when available. |\n| `planar?` | `boolean` | True when the face can host a 2D sketch placement frame |\n| `uAxis?` | `[ number, number, number ]` | Face-local horizontal axis for planar faces |\n| `vAxis?` | `[ number, number, number ]` | Face-local vertical axis for planar faces |\n| `surface?` | `FaceSurface` | Analytic surface family when the backend can identify one. |\n| `descendant?` | `FaceDescendantMetadata` | Shared descendant-resolution metadata when this face is a semantic region/set. |\n| `name` | | — |\n\n**`FaceDescendantMetadata`**: `kind: \"single\" | \"face-set\"`, `semantic: FaceDescendantSemantic`, `memberCount: number`, `memberNames: string[]`, `coplanar: boolean`\n\n**`EdgeRef`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `start` | `[ number, number, number ]` | Start point |\n| `end` | `[ number, number, number ]` | End point |\n| `query?` | `EdgeQueryRef` | Compiler-owned edge query when available. |\n| `curve?` | `EdgeCurve` | Exact or parametric curve family when the backend/source can identify one. |\n| `faceName?` | `string` | Owning face name when the edge is associated with one face in a larger topology. |\n| `name` | | — |\n\n---\n\n## Classes\n\n### `RouteBuilder`\n\n#### `up()` — Vertical line going +Y. Length is optional (solver determines it from constraints).\n\n```ts\nup(length?: number): LineId\n```\n\n#### `down()` — Vertical line going -Y. Length is optional.\n\n```ts\ndown(length?: number): LineId\n```\n\n#### `right()` — Horizontal line going +X. Length is optional.\n\n```ts\nright(length?: number): LineId\n```\n\n#### `left()` — Horizontal line going -X. Length is optional.\n\n```ts\nleft(length?: number): LineId\n```\n\n#### `lineAt()` — Line at an arbitrary angle (degrees from +X). Length is optional.\n\n```ts\nlineAt(angleDeg: number, length?: number): LineId\n```\n\n#### [`line()`](/docs/sketch#line) — Line with solver-determined direction. Length is optional. Direction comes from tangency to previous arc or from constraints.\n\n```ts\nline(length?: number): LineId\n```\n\n#### `toward()` — Line toward a specific point. Length defaults to the distance to that point.\n\n```ts\ntoward(x: number, y: number): LineId\n```\n\n#### `arcLeft()` — Tangent arc turning left relative to travel direction.\n\nor `{ minSweep: degrees }` to seed the geometry without constraining. `minSweep` guides the solver to the correct branch for arcs that sweep more than the default 90° seed.\n\n```ts\narcLeft(radius?: number, sweepDegOrOpts?: number | { minSweep: number; }): ArcId\n```\n\n#### `arcRight()` — Tangent arc turning right relative to travel direction.\n\nor `{ minSweep: degrees }` to seed without constraining.\n\n```ts\narcRight(radius?: number, sweepDegOrOpts?: number | { minSweep: number; }): ArcId\n```\n\n#### `close()` — Close the route with a straight line back to the start point.\n\n```ts\nclose(): void\n```\n\n#### `done()` — Close the route back to its start point and register as a profile loop.\n\nNo extra line segment is added. A coincident constraint connects the last point to the start, and tangency is added for G1 smoothness when arcs are at the junction. The session's incremental solver processes these constraints, keeping seed positions accurate for the final solve.\n\n```ts\ndone(): void\n```\n\n#### `start()` — PointId of the route's start point.\n\n```ts\nget start(): PointId\n```\n\n#### `end()` — PointId of the current cursor (route's end).\n\n```ts\nget end(): PointId\n```\n\n#### `startOf()` — Get the start point of a segment.\n\n```ts\nstartOf(segId: LineId | ArcId): PointId\n```\n\n#### `endOf()` — Get the end point of a segment.\n\n```ts\nendOf(segId: LineId | ArcId): PointId\n```\n\n---\n\n## Constants\n\n### `route`\n\nRoute step factories. Access via `route.line()`, `route.fillet()`, etc.\n\n---\n\n<!-- guides/modeling-recipes.md -->\n\n# Modeling Recipes\n\n## Iteration Bias\n\n- Default to a buildable first pass instead of a long proposal.\n- Replace a broken model wholesale when that is faster than incremental patching.\n- Validate early with `forgecad run <file>`.\n\n## Common Patterns\n\n### Hollow Shell\n```javascript\nconst innerSize = outer - 2 * wall;\nconst outerBox = box(outer, outer, outer).placeReference('center', [0, 0, 0]);\nconst innerBox = box(innerSize, innerSize, innerSize).placeReference('center', [0, 0, 0]);\nreturn outerBox.subtract(innerBox);\n```\n\n### Sketch-Based Twist\n```javascript\nconst outer = ngon(sides, radius);\nconst inner = ngon(sides, radius - wall);\nreturn outer.subtract(inner).extrude(height, { twist: 45, divisions: 32 });\n```\n\n### Rounded Profiles\n```javascript\n// All convex corners — offset trick\nconst base = rect(50, 30).offset(-3, 'Round').offset(3, 'Round');\n\n// Selected corners only\nconst roof = filletCorners(roofPoints, [\n { index: 3, radius: 19 },\n { index: 4, radius: 19 },\n { index: 5, radius: 19 },\n]);\n```\n\n### Choosing the right sketch-rounding tool\n\n- `offset(-r).offset(+r)` — round every convex corner of a closed outline\n- `stroke(points, width, 'Round')` — centerline-based geometry (ribs, traces)\n- `filletCorners(points, ...)` — selective true-corner fillets on mixed profiles\n\n## Best Practices\n\n- All dimensions in millimeters; angles in degrees.\n- Primitives are centered on XY, base at Z=0. Use `placeReference('center', [0,0,0])` to center on all axes.\n- Prefer named intermediate values over deeply nested one-liners.\n- `union2d`, `difference2d`, `intersection2d` batch faster than chained `.add()` / `.subtract()`.\n\n## Debugging\n\n```javascript\nconsole.log(\"Volume:\", shape.volume());\n```\n\nFor sketch-heavy work, compare the raw profile and rounded profile side-by-side before extruding:\n\n```javascript\nreturn [\n { name: \"Raw\", sketch: polygon(roofPoints) },\n { name: \"Rounded\", sketch: filletCorners(roofPoints, [...]).translate(120, 0) },\n];\n```\n\n## Common Errors\n\n- `\"Kernel not initialized\"` — internal/runtime issue, reload the app\n- zero dimensions or self-intersecting sketches → invalid geometry\n- wrong variable name → `\"Cannot read property of undefined\"`\n\nFor deeper API coverage, load the relevant generated doc group from the skill source map instead of reaching for repo examples by default.\n\n---\n\n<!-- guides/joint-design.md -->\n\n# Joint Design Recipes\n\nHow to build mechanical joints — clevis-tongue hinges, ball-and-socket, dovetails — that actually rotate without binding and stop where they should.\n\n## The Cavity Rule\n\nEvery mechanical joint has a **cavity** in one part and a **tenon** in the other. The cavity must be a real empty volume — not a gap implied by the absence of two separate solids.\n\nIf two adjacent parts in an assembly show a collision volume larger than the expected clearance volume in `forgecad run`, one part is missing its cavity. Both parts have solid material at the same joint position. This will look fine at rest pose but will block rotation and produce confusing joint behavior.\n\n```ts\n// BAD — body has a stadium cap at both ends; the \"slot\" between two clevis tines\n// is just empty space next to a solid body cap. The next phalanx's tongue knuckle\n// has nowhere to go (it intersects the previous body's cap).\nconst body = stadiumBar(L); // cap at X=0 AND X=L\nconst tine1 = box(...).translate(L, Y_OFF, 0);\nconst tine2 = box(...).translate(L, -Y_OFF, 0);\nlet phalanx = union(body, tine1, tine2);\n\n// GOOD — body ends FLAT before the joint. Tines extend forward to the pivot.\n// The X = L-KNUCK_R..L+KNUCK_R volume between the tines is genuinely empty.\nconst body = box(L - KNUCK_R, TONG_T, H).translate((L - KNUCK_R) / 2, 0, -H / 2);\nconst tongueKnuckle = knuckleDisc(0, 0, TONG_T); // proximal cap only\nlet phalanx = union(tongueKnuckle, body, tine1, tine2, ...tineCaps);\n```\n\nAfter applying the cavity rule, `forgecad run` collision volume between adjacent parts in a clevis-tongue chain should drop to **zero** (or a few mm³ of clearance overlap). If it doesn't, there's still solid material where there should be a cavity.\n\n## Connecting Cantilevers\n\nA clevis tine arm at Y=±Y_OFF is geometrically separate from a body at Y=±TONG_T/2. With Y_OFF > TONG_T/2 + clearance, there is a **physical gap** between them. The tines float — they would snap off as soon as load is applied.\n\nAlways add a **yoke**: a short slab spanning the full clevis width, sitting between the body's flat distal end and the tines' attachment point. The yoke fills the Y gap so material is continuous from the body through to each tine.\n\n```ts\nconst yokeLen = 3; // a few mm of structural overlap\nconst yokeStart = L - KNUCK_R - yokeLen;\nconst totalY = (Y_OFF + TINE_T / 2) * 2; // full clevis width\nconst yoke = box(yokeLen, totalY, H)\n .translate(yokeStart + yokeLen / 2, 0, -H / 2);\nphalanx = union(phalanx, yoke);\n```\n\n## Hard Stops vs Slider Limits\n\n`addRevolute({ min: 0, max: 90 })` sets **slider limits** — the viewport won't let the user drag past them, but the geometry permits any rotation. There is no physical stop.\n\nFor a **geometric** hard stop (parts can't backbend past extension, or can't curl past full closure), add a small protrusion on one part that interferes with the other at the limit angle:\n\n- **Extension stop at 0°** (typical for fingers, knees, elbows): add a small \"lip\" on the dorsal side of the proximal end of the child phalanx, sized so it just touches the parent's distal dorsal corner at 0°. Negative rotation (backbending) is then blocked by part-on-part contact.\n- **Flexion stop at θmax**: add a similar lip on the palmar side, or rely on the body-to-body collision when bodies meet.\n\nVerify with `forgecad run` at the limit poses — the contact pair should show ~0 mm³ collision (just touching), and rotation past the limit should report a non-zero collision volume.\n\n## Knuckle Sizing\n\nFor a clevis-tongue joint with body height H, the tongue knuckle radius and clevis tine knuckle radius must satisfy:\n\n```\nKNUCK_R >= H / 2\n```\n\nIf the knuckle radius is smaller than the body's half-height, the body's corners protrude beyond the knuckle envelope. When the joint rotates, those corners sweep through space outside the cylindrical envelope and collide with the adjacent part.\n\nSetting `KNUCK_R = H / 2` exactly makes the body cross-section a stadium that perfectly fits the knuckle envelope.\n\n## Verification Workflow\n\n1. Build the joint at rest pose. Run `forgecad run`. Check collision volumes.\n2. If adjacent parts in the joint show > clearance-volume of overlap → missing cavity (apply the cavity rule).\n3. Render with `--focus PartName` to inspect each part in isolation. The clevis end should clearly show a gap between the tines (the cavity).\n4. Render at curl angles (set joint debug params) at 30°, 60°, 90°. No new collisions should appear from rotation.\n5. Render at -10° (backbend test). Either no rotation possible (geometric stop in place) or rotation occurs and you need to add a stop.\n\n---\n\n<!-- guides/inspection-bundles.md -->\n\n# Inspection Bundles\n\n`forgecad render inspect` writes a deterministic directory bundle for agents,\ntests, and automation. Use it when a single shaded PNG is too ambiguous and the\nconsumer needs geometry-aware signals such as depth, normals, Zebra stripes,\nsurface roughness, part identity, physical connected components, collisions,\nlocal thickness, or cross-sections.\n\n## When To Use It\n\n- Use `forgecad render inspect` for agent repair loops, model debugging, CI\n artifacts, and structured visual comparison.\n- Use `forgecad render 3d` for a quick human viewport PNG.\n- Use `forgecad render section` when you only need one specific cut plane.\n- Use `forgecad render hq` for presentation-quality output, docs, and marketing\n renders.\n\n## Command\n\n```bash\nforgecad render inspect examples/api/static-assembly-connectors.forge.js --channels rgb,mask\nforgecad render inspect model.forge.js out/model-inspect --channels rgb,section --force\nforgecad render inspect model.forge.js --channels rgb,mask,section\nforgecad render inspect model.forge.js --channels collisions --focus \"Bench.*\"\nforgecad render inspect model.forge.js --channels rgb,mask --hide \"Bench.Slat0,Bench.Slat1\"\nforgecad render inspect model.forge.js --channels thickness --min-thickness 1.2 --warn-thickness 2.0\nforgecad render inspect channels\n```\n\nThe default output directory is `<script-name>-inspect/` next to the input file.\nPass `--force` to replace an existing bundle directory.\n\nThere are no default channels. Pass `--channels` every time as a\ncomma-separated subset. Run `forgecad render inspect channels` to list the\nsupported channels in the installed CLI. Keep bundles targeted to the current\nquestion so heavy analyses do not run unnecessarily.\n\n`--focus` and `--hide` use the same object-name filtering semantics as\n`forgecad run` and `forgecad render 3d`. A bare `--focus` hides mock objects;\n`--focus name1,name2` emits only matching objects; `--hide name1,name2` removes\nmatching objects from an otherwise visible scene. Matching is case-insensitive\nand supports `*` / `?` globs, so grouped child objects are usually best matched\nwith patterns such as `Bench.*`.\n\n## Bundle Layout\n\nA bundle that asks for `--channels rgb,depth,normals,zebra,mask,section` has this\nlayout:\n\n```text\nmodel-inspect/\n manifest.json\n channels/\n rgb/\n front.png\n right.png\n top.png\n iso.png\n depth/\n front.png\n right.png\n top.png\n iso.png\n normals/\n front.png\n right.png\n top.png\n iso.png\n zebra/\n front.png\n right.png\n top.png\n iso.png\n mask/\n front.png\n right.png\n top.png\n iso.png\n section/\n xy/\n 000.png\n 001.png\n 002.png\n 003.png\n 004.png\n xz/\n 000.png\n ...\n yz/\n 000.png\n ...\n```\n\nUse targeted channel groups for expensive analyses instead of running every\nimplemented channel in one bundle:\n\n```bash\nforgecad render inspect model.forge.js --channels depth,normals\nforgecad render inspect model.forge.js --channels rgb,zebra,normals\nforgecad render inspect model.forge.js --channels rgb,roughness\nforgecad render inspect model.forge.js --channels rgb,mask,collisions\nforgecad render inspect model.forge.js --channels rgb,section,thickness\n```\n\nSupported channels are `rgb`, `depth`, `normals`, `zebra`, `roughness`, `mask`,\n`connectivity`, `floating`, `distance`, `collisions`, `thickness`, and\n`section`.\n\n## Channel Semantics\n\n`rgb` emits the standard solid viewport render with a thin edge overlay. Views\nare canonical `front`, `right`, `top`, and `iso`.\n\n`depth` emits visible ray-distance heatmaps. Each shaded pixel is colored by the\ndistance from the camera position to the visible surface point, normalized per\nview between `minDistance` and `maxDistance` from the manifest:\n\n```text\nrayDistance = distance(cameraPosition, surfacePoint)\nnormalized = (rayDistance - minDistance) / (maxDistance - minDistance)\n```\n\nThe ramp is blue near the camera, green in the middle, and red far from the\ncamera. Background pixels are black and should be treated as `null`.\n\n`normals` emits camera-view normals packed into RGB:\n\n```text\nnormal = normalize((rgb / 255) * 2 - 1)\n```\n\nBackground pixels are black and should be treated as `null`.\n\n`zebra` emits reflective black-and-white stripe renders for visual\nsurface-continuity inspection. Stripes are generated from the visible\ncamera-view normal and simulated reflection direction, so smooth surfaces show\nsmooth flowing bands while normal discontinuities, faceting, and unexpected\ncreases kink or break the bands.\n\nUse Zebra with `rgb` and `normals` when judging lofts, fillets, swept surfaces,\nand skin-like forms. It is a human-readable shader diagnostic, not an exact\ncurvature-continuity proof; mesh tessellation quality and available smooth\nnormals determine how faithfully it represents the underlying surface.\n\n`roughness` emits a mesh-dihedral surface-quality heatmap. Smooth and gently\ncurved triangles render as a faint translucent shadow over black, while\ntriangles adjacent to sharp, harsh, boundary, or non-manifold mesh edges render\nin orange or magenta:\n\n```text\nshadow = max adjacent angle < sharpAngleDeg\norange = sharpAngleDeg <= angle < harshAngleDeg\nmagenta = angle >= harshAngleDeg, boundary, or non-manifold\n```\n\nThe default thresholds are `smoothAngleDeg=5`, `sharpAngleDeg=30`, and\n`harshAngleDeg=90`. The manifest stores the method, thresholds, palette, object\nlist, per-object triangle and edge counts, area percentages by smooth,\nmoderate, sharp, and harsh classes, angle percentiles, maximum angle, quality\nscore, and warnings. Moderate angles are reported in the manifest but stay in\nthe shadow layer by default so intentionally curved surfaces do not light up as\ndefects. Use this channel to spot spiky tessellation, accidental faceting,\njagged boolean residue, and dense sharp-corner regions without losing the\nsilhouette of otherwise smooth surfaces.\n\nThe channel also writes `channels/roughness/point-cloud.json`. Each point sample\nstores object identity, object-local position, normal, dihedral angle, class,\nRGB color, and represented surface area. The PNG renders those samples over\nmuted source geometry so the visual evidence stays point-level instead of\npainting a whole object.\n\n`mask` emits one object-color image per view. Black is background. Non-black\npixels resolve through `manifest.channels.mask.objects`, which includes object\nindex, RGB color, object id, name, group, tree path, and mock flag. Edge pixels\nmay be antialiased blends; use solid interior colors for exact object lookup.\n\n`connectivity` emits one physical-component-color image per view. Black is\nbackground. Non-black pixels resolve through\n`manifest.channels.connectivity.components`, and every visible object also has a\n`componentIndex` in `manifest.channels.connectivity.objects`.\n\nConnectivity is computed from visible scene objects:\n\n```text\nbbox candidate = bbox interiors overlap or bbox contact gap <= 0.05 model units\nmesh contact edge = minimum mesh-surface distance <= contactTolerance\noverlap edge = exact boolean intersection volume > 0.1 model units^3 for positive-volume overlap\ncomponent = transitive closure over mesh contact and exact overlap edges\n```\n\nThe manifest stores the edge list, component list, per-object body counts, and\nwarnings. Component colors group scene objects and mesh body entries. If one\nscene object contains multiple disconnected mesh islands, those islands are\nreported and colored separately as entries such as `Part body 1` and\n`Part body 2`.\n\nConnectivity uses bbox only as a broadphase. Bbox contact alone is not enough to\nmerge separate scene objects by default, but mesh surfaces within contact\ntolerance count as physically connected. This keeps concave assemblies such as\ncages and captive balls from being falsely colored as one component while still\nallowing stacked or nearly touching parts to share a component. Use the\n`collisions` channel when you need positive-volume overlap evidence as a defect\nreport rather than a component grouping.\n\n`floating` emits one disconnected-body highlight image per view. Black is\nbackground or ground-reachable geometry. The highlight color marks physical\ncomponents that have no contact path to the ground plane.\n\nFloating body detection splits visible meshes into disconnected body islands,\nlinks bodies only when their minimum mesh-surface distance is within contact\ntolerance (or exact positive-volume overlap when only shape evidence is\navailable), treats any connected component whose lower Z reaches the viewport\nground plane plus bed tolerance as grounded, then highlights every ungrounded\ncomponent. The default ground plane is the visible model's minimum Z;\n`scene({ ground: { offset } })` moves it below that by the configured offset.\n\n```text\ngrounded = component bbox minZ <= groundZ + bedTolerance\nfloating body = !grounded\n```\n\nThis means a `union()` result with two disconnected mesh islands is inspected as\ntwo separate bodies instead of being treated as one safe object. Bbox overlap or\nbbox face contact alone is not support evidence. Use `connectivity`, `distance`,\nor `collisions` when you need the full physical graph, rooted gap distances, or\ncollision defects.\n\n`thickness` emits one local wall-thickness heatmap per view. Black is\nbackground. Red/orange pixels are below the configured critical/warning\nthresholds, green-to-blue pixels are acceptable/thick, and gray means the local\nraycast could not resolve an opposite surface.\n\nThickness uses the same physical-contact edges as `connectivity` and `floating`.\nWhen a ray crosses from one object to a direct physical-contact neighbor, hits\nwithin `contactTolerance` are treated as contact seams and the ray continues to\nthe next surface. This prevents a tiny modeled gap between touching parts from\nbeing reported as a paper-thin wall.\n\n`distance` emits one rooted physical-component-distance heatmap per view. Black\nis background. Non-black pixels resolve through\n`manifest.channels.distance.components`, and every visible object also has\n`componentIndex`, `rootDistance`, `nearestGap`, and parent-tree metadata in\n`manifest.channels.distance.objects`.\n\nDistance is computed from visible scene objects:\n\n```text\ncomponent = physical connectivity component\ngap edge = Euclidean distance between component bounding boxes\nroot = largest component by body count, object count, then bbox volume\nrootDistance = shortest accumulated gap distance from root component\n```\n\nFor large scenes the manifest does not materialize the complete component gap\ngraph, because that graph is quadratic in the number of components. The\n`gapEdgeCount` field reports the logical complete-graph edge count used by the\nanalysis. `gapEdges` stores a compact evidence subset containing nearest-gap\nand root-parent edges.\n\nThe PNG colors components from green at the root/near distances through yellow to\nred at the farthest rooted component. The manifest stores the root component,\nmaximum rooted distance, compact gap edge evidence, nearest-gap data, and\nshortest-path parent fields. The current v1 metric is bbox-based: it measures air\ngaps between component bounding boxes, not exact closest mesh-surface distance.\n\n`collisions` emits one ghosted-overlap image per view. It uses the same\n`--focus` / `--hide` visibility set as every other inspect channel: focused\nobjects are the only inspected objects. Source objects render as translucent\nghosts, while actual boolean intersection volumes render as solid per-finding\npalette colors.\n\nCollision findings are computed from visible scene objects:\n\n```text\ncollision = boolean intersection volume > 0.1mm^3\n```\n\nThe manifest stores the inspected objects, collision pair names/ids, overlap\nvolume, broadphase counters, warnings, render style, and each collision finding's\n`groupIndex`, `color`, and `hex`. Exact interior pixels can be matched against\n`manifest.channels.collisions.collisions[].color`; antialiased edges may blend\nwith the ghosted source geometry. If `--focus PartA,PartB` is used, everything\nexcept those objects is hidden, `PartA` and `PartB` are ghosted, and their\noverlap volume is highlighted if present.\n\nCollision broadphase prunes exact boolean checks when the bbox intersection\nvolume is already below the overlap threshold. This does not change findings:\nthe real intersection volume cannot exceed the bbox intersection volume.\n\n`thickness` emits one local wall-thickness heatmap per view. The renderer places\ndeterministic area-weighted point samples across visible mesh surfaces, casts\nthrough the object along each sample normal, and colors each point by the first\nopposite-surface distance:\n\n```text\nred = thickness <= minThickness\norange = thickness <= warnThickness\ngreen = acceptable thickness\nblue = thickness >= maxThickness\ngray = unresolved sample\n```\n\nThe default thresholds are `minThickness=1.2`, `warnThickness=2.0`, and\n`maxThickness=6.0` model units. Override them with `--min-thickness`,\n`--warn-thickness`, and `--max-thickness`. Use `--thickness-samples` to raise or\nlower the maximum thickness point samples per object.\n\nThe manifest stores the method, thresholds, palette, object list, per-object\ntriangle counts, sampled-triangle counts, minimum, p05, median, mean, maximum,\ncritical-area percentage, warning-area percentage, below-warning percentage, and\nunresolved-area percentage. This makes the PNG useful for visual debugging while\nthe manifest remains the machine-readable source of truth.\n\nThe channel also writes `channels/thickness/point-cloud.json`. Each point sample\nstores object identity, object-local position, normal, measured thickness,\nclass, RGB color, and represented surface area. The PNG renders those samples\nover muted source geometry, so local evidence survives even when neighboring\ntriangles have very different values.\n\n`roughness` uses the same area-weighted point placement. Point colors are local\nto nearby physical feature edges: smooth tessellation diagonals do not become\nvisible roughness lines. Use `--roughness-samples` to raise or lower the maximum\nroughness point samples per object.\n\n`section` emits five interior slices per principal plane. The current slicing\npolicy is:\n\n```text\noffset = bbox.min[axis] + fraction * (bbox.max[axis] - bbox.min[axis])\nfractions = [1/6, 2/6, 3/6, 4/6, 5/6]\nplanes = xy, xz, yz\n```\n\nEach section slice records its exact offset, fraction, area, path count, size,\nand contributing object count in the manifest.\n\n## Manifest\n\n`manifest.json` is the authoritative contract for consuming a bundle. It\ncontains:\n\n- `schemaVersion` and generator metadata.\n- Source entry file and project root paths.\n- Requested channels, emitted channels, filters, image size, and quality.\n- Canonical views.\n- Scene metadata: bbox, volume, params, cut planes, animations, verifications,\n and objects.\n- Channel metadata and relative file paths.\n\nA consumer should prefer paths from the manifest over hard-coding bundle layout.\nThe layout is intentionally simple, but the manifest is where encoding details,\nper-view depth ranges, and object-mask mappings live.\n\n## Current Limits\n\n- Depth is a visual heatmap, not an EXR or raw float array.\n- Normals are camera-view normals, not world-space normals.\n- Mask indices are stable within a bundle and resolved through the manifest; do\n not infer identity from object order alone.\n- Connectivity is object-level. It reports disconnected kernel bodies in the\n manifest, but the PNG does not split a single scene object into per-body colors.\n- Bbox contact is only broadphase evidence and does not merge separate scene\n objects by default. Boolean-overlap edges are exact.\n- Distance is a physical-component bbox-gap metric in v1, not exact nearest\n mesh-surface distance. Concave components and loose bounding boxes can make the\n reported gap smaller than the real closest-surface distance.\n- Collisions are only positive-volume boolean overlaps. Face-touching parts are\n not collision findings.\n- Thickness is a mesh/raycast approximation, not FEA or a manufacturability\n guarantee. Open meshes, concave geometry, very coarse tessellation, or low\n `--thickness-samples` values can leave gray/unresolved or approximate regions.\n- Section atlases use five default interior slices today.\n- Zebra is a shader-based visual continuity aid, not exact curvature analysis.\n\n---\n\n<!-- generated/sdf.md -->\n\n# SDF Modeling\n\nSigned Distance Field modeling for organic forms, smooth booleans, TPMS lattices, and deformations. SDFs are inherently implicit fields, not B-rep/exact geometry; use them with caution when precision or exact export matters. Return raw `SdfShape` values directly for native preview; use `toShape(...)` when materializing SDF trees for CAD/export workflows.\n\n## Contents\n\n- [SDF Materialization](#sdf-materialization) — `toShape`, `combine`\n- [SdfShape](#sdfshape)\n- [sdf](#sdf)\n- [Sculpt](#sculpt)\n\n## Functions\n\n### SDF Materialization\n\n#### `toShape()` — Materialize one SDF leaf or all SDF leaves in a renderable tree.\n\nRaw `SdfShape` values become mesh-backed [`Shape`](/docs/core#shape)s. Plain objects and arrays preserve their renderable children as a [`ShapeGroup`](/docs/core#shapegroup) when more than one leaf is found. Non-renderable metadata is ignored for materialization and remains available to callers through normal [`require()`](/docs/core#require) return values.\n\n```ts\ntoShape(value: unknown, options?: SdfToShapeOptions): ToShapeTreeResult\n```\n\n**`SdfToShapeOptions`**\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `edgeLength?` | `number` | Target mesh edge length. Smaller = finer mesh. Overrides quality-derived resolution. |\n| `bounds?` | `{ min: Vec3; max: Vec3; }` | Override auto-computed bounds. Strongly recommended for infinite/repeated fields. |\n| `quality?` | `SdfMeshingQuality` | Coarse quality preset. Default: 'preview'. |\n| `tolerance?` | `number` | Preferred absolute surface tolerance in millimeters. |\n| `minFeatureSize?` | `number` | Smallest feature that should survive meshing, in millimeters. |\n| `simplify?` | `boolean \\| \"safe\"` | Simplification control. `false` disables, `true` and `'safe'` use topology-validated simplification. |\n| `maxTriangles?` | `number` | Optional post-extraction triangle budget. |\n| `maxGridPoints?` | `number` | Optional pre-extraction grid-point budget. Default is browser-safe. |\n| `minEdgeLength?` | `number` | Lower clamp for resolved edge length. Default: 0.15mm. |\n| `diagnostics?` | `boolean` | Log resolved meshing settings and backend extraction timings. |\n\n#### `combine()` — Collapse a tree of SDF leaves into one continuous SDF field.\n\nThis intentionally discards per-leaf color/material identity because the result is one scalar field. Use plain object returns for multi-material SDF preview, and use `combine(...)` only when you want one implicit body.\n\n```ts\ncombine(value: unknown, options?: CombineOptions): SdfShape\n```\n\n`CombineOptions`: `{ op?: \"union\" | \"intersection\" }`\n\n---\n\n## Classes\n\n### `SdfShape`\n\nAn immutable SDF expression. Supports SDF-specific operations (smooth booleans, domain warps, etc.), can be returned directly for native preview, and converts to a ForgeCAD Shape via `.toShape()` when materialization is needed.\n\n#### `colorHex()` — Display color carried by this implicit leaf.\n\n```ts\nget colorHex(): string | undefined\n```\n\n#### `materialProps()` — Display material carried by this implicit leaf.\n\n```ts\nget materialProps(): ShapeMaterialProps | undefined\n```\n\n#### `explicitBounds()` — Explicit bounds carried by this implicit leaf, if any.\n\n```ts\nget explicitBounds(): SdfBounds | undefined\n```\n\n#### `clone()` — Clone this SDF expression and its visual metadata.\n\n```ts\nclone(): SdfShape\n```\n\n#### `toShape()` — Mesh this SDF into a ForgeCAD Shape through ForgeCAD's Surface Nets pipeline. Once converted, the result is a regular Shape — booleans, transforms, export all work.\n\n```ts\ntoShape(options?: SdfToShapeOptions): Shape\n```\n\n#### `color()` — Set the display color for this implicit leaf.\n\n```ts\ncolor(value: string | undefined): SdfShape\n```\n\n#### `material()` — Set PBR display material properties for this implicit leaf.\n\n```ts\nmaterial(props: ShapeMaterialProps): SdfShape\n```\n\n#### `bounds()` — Set explicit preview/meshing bounds for this implicit leaf.\n\n```ts\nbounds(bounds: SdfBounds | [ Vec3, Vec3 ]): SdfShape\n```\n\n#### `at()` — Sculpt-style alias for translate().\n\n```ts\nat(x: number, y: number, z: number): SdfShape\n```\n\n#### `move()` — Sculpt-style alias for translate().\n\n```ts\nmove(x: number, y: number, z: number): SdfShape\n```\n\n#### `spin()` — Sculpt-style alias for rotateZ().\n\n```ts\nspin(angleDeg: number): SdfShape\n```\n\n#### `tilt()` — Sculpt-style tilt around X, Y, Z, or a custom axis.\n\n```ts\ntilt(angleDeg: number, axis?: \"x\" | \"y\" | \"z\" | Vec3): SdfShape\n```\n\n#### `round()` — Sculpt-style rounded-box helper. Currently applies directly to primitive SDF boxes.\n\n```ts\nround(radius: number): SdfShape\n```\n\n#### `blend()` — Sculpt-style smooth blend with another implicit shape.\n\n```ts\nblend(other: SdfShape, options?: number | { radius?: number; }): SdfShape\n```\n\n#### `goop()` — Sculpt-style alias for blend().\n\n```ts\ngoop(other: SdfShape, options?: number | { radius?: number; }): SdfShape\n```\n\n#### `carve()` — Sculpt-style smooth carve/subtract.\n\n```ts\ncarve(other: SdfShape, options?: number | { radius?: number; }): SdfShape\n```\n\n#### `keep()` — Sculpt-style smooth intersection/keep operation.\n\n```ts\nkeep(other: SdfShape, options?: number | { radius?: number; }): SdfShape\n```\n\n#### `polish()` — Apply a Sculpt material preset or direct material props.\n\n```ts\npolish(input?: SculptPolishInput): SdfShape\n```\n\n#### [`union()`](/docs/core#union) — SDF union (sharp).\n\n```ts\nunion(...others: SdfShape[]): SdfShape\n```\n\n#### `subtract()` — SDF difference (sharp) — subtracts others from this.\n\n```ts\nsubtract(...others: SdfShape[]): SdfShape\n```\n\n#### `intersect()` — SDF intersection (sharp).\n\n```ts\nintersect(...others: SdfShape[]): SdfShape\n```\n\n#### `clipBox()` — Clip this SDF to an explicit box-shaped design space.\n\n```ts\nclipBox(x: number, y: number, z: number): SdfShape\n```\n\n#### `fillWith()` — Keep only the material where this shape overlaps another SDF pattern.\n\n```ts\nfillWith(pattern: SdfShape): SdfShape\n```\n\n#### `fillWithGyroid()` — Keep only the gyroid lattice inside this shape.\n\n```ts\nfillWithGyroid(options: TpmsOptions): SdfShape\n```\n\n#### `fillWithSchwarzP()` — Keep only the Schwarz-P lattice inside this shape.\n\n```ts\nfillWithSchwarzP(options: TpmsOptions): SdfShape\n```\n\n#### `fillWithDiamond()` — Keep only the diamond TPMS lattice inside this shape.\n\n```ts\nfillWithDiamond(options: TpmsOptions): SdfShape\n```\n\n#### `fillWithLidinoid()` — Keep only the lidinoid TPMS lattice inside this shape.\n\n```ts\nfillWithLidinoid(options: TpmsOptions): SdfShape\n```\n\n#### `smoothUnion()` — Smooth union — blends shapes together with a smooth radius.\n\n```ts\nsmoothUnion(other: SdfShape, radius: number): SdfShape\n```\n\n#### `smoothSubtract()` — Smooth difference — smoothly carves other from this.\n\n```ts\nsmoothSubtract(other: SdfShape, radius: number): SdfShape\n```\n\n#### `smoothIntersect()` — Smooth intersection — smoothly intersects.\n\n```ts\nsmoothIntersect(other: SdfShape, radius: number): SdfShape\n```\n\n#### `morph()` — Morph between this shape and another. t=0 → this, t=1 → other.\n\n```ts\nmorph(other: SdfShape, t: number): SdfShape\n```\n\n#### `translate()` — Translate this SDF by the given offsets in millimeters.\n\n```ts\ntranslate(x: number, y: number, z: number): SdfShape\n```\n\n#### `rotate()` — Rotate around an arbitrary axis through the origin.\n\n```ts\nrotate(axis: [ number, number, number ], angleDeg: number): SdfShape\n```\n\n#### `rotateX()` — Rotate around the X axis by the given angle in degrees.\n\n```ts\nrotateX(angleDeg: number): SdfShape\n```\n\n#### `rotateY()` — Rotate around the Y axis by the given angle in degrees.\n\n```ts\nrotateY(angleDeg: number): SdfShape\n```\n\n#### `rotateZ()` — Rotate around the Z axis by the given angle in degrees.\n\n```ts\nrotateZ(angleDeg: number): SdfShape\n```\n\n#### `scale()` — Uniformly scale this SDF around the origin.\n\n```ts\nscale(factor: number): SdfShape\n```\n\n#### `twist()` — Twist around the Z axis.\n\n```ts\ntwist(degreesPerUnit: number): SdfShape\n```\n\n#### `bend()` — Bend around the Z axis with given radius.\n\n```ts\nbend(radius: number): SdfShape\n```\n\n#### `repeat()` — Repeat in space. Spacing of 0 on an axis means no repetition. Count of 0 = infinite.\n\n```ts\nrepeat(spacing: Vec3, count?: Vec3): SdfShape\n```\n\n#### `circularArray()` — Arrange this SDF in a circular array around the Z axis.\n\nThe source shape is translated by `offset` in +X before arraying. This uses angular domain folding, so evaluation stays O(1): the source SDF is sampled twice no matter how many copies are requested.\n\n```ts\ncircularArray(count: number, offset?: number): SdfShape\n```\n\n#### `shell()` — Hollow out, keeping only a shell of given thickness.\n\n```ts\nshell(thickness: number): SdfShape\n```\n\n#### `displace()` — Displace the surface by a function of position, or by a pattern SdfShape.\n\n```js\n// Function displacement\nshape.displace((x, y, z) => Math.sin(x) * 0.5)\n\n// Pattern displacement from a 3D SDF field\nshape.displace(sdf.knurl({ pitch: 2, depth: 0.3 }))\n```\n\n```ts\ndisplace(fn: ((x: number, y: number, z: number) => number) | SdfShape, constants?: Record<string, number>): SdfShape\n```\n\n#### `surfaceDisplace()` — Displace the surface using a 2D pattern in surface-local UV coordinates.\n\nAutomatically detects the shape's UV parametrization (sphere, cylinder, torus) from the SDF tree. Falls back to triplanar mapping for arbitrary shapes.\n\nUV coordinates are in **surface millimeters** — patterns defined with `spacing: 3` always produce 3mm spacing, regardless of shape size.\n\nPrefer `sdf.pattern2d()` or built-in surface patterns when the relief should stay on the native shader and meshing path. Callback functions are supported for experimentation, but they are opaque to the typed pattern optimizer.\n\n```js\n// Native typed pattern — auto-detects sphere UV\nconst p = sdf.pattern2d()\nconst ribs = p.stripes({ spacing: 3, width: 0.8, depth: 0.35 })\n .add(p.sineWave({ direction: [0, 1], wavelength: 14, amplitude: 0.08 }))\n\nsdf.sphere(27).shell(3)\n .surfaceDisplace(ribs)\n .toShape()\n\n// Custom 2D pattern via function\nshape.surfaceDisplace((u, v) => -Math.sin(u * 2) * 0.3)\n```\n\n```ts\nsurfaceDisplace(pattern: SurfacePattern | ((u: number, v: number) => number), options?: SurfaceDisplaceOptions): SdfShape\n```\n\n#### `onion()` — Create concentric onion layers.\n\n```ts\nonion(layers: number, thickness: number): SdfShape\n```\n\n---\n\n## Constants\n\n### `sdf`\n\nSDF modeling — signed distance field primitives, smooth booleans, TPMS lattices, domain warps, and surface patterns.\n\nReturn `SdfShape` values directly from a ForgeCAD script for native raymarch preview. Plain objects and arrays of SDF leaves are renderable too, so object keys become named preview parts.\n\nCall `.toShape()` or `toShape(...)` only when you need a mesh-backed ForgeCAD Shape for export, mesh booleans, or mixed SDF/manifold projects. All shapes live as a lazy expression tree until that materialization boundary.\n\nSDF is inherently implicit and sampled, not B-rep/exact geometry. Use it with caution when precision, tolerances, or exact export matter.\n\n```js\nreturn sdf.smoothUnion(sdf.sphere(10), sdf.box(15, 15, 15), { radius: 3 })\n .color('#4488cc');\n```\n\n```js\nreturn {\n shell: sdf.sphere(20).shell(2).color('#9be7ff'),\n core: sdf.gyroid({ cellSize: 6, wallThickness: 0.8 })\n .intersect(sdf.sphere(18))\n .color('#ffcf5a'),\n};\n```\n\n- `sphere(radius: number): SdfShape` — Create an SDF sphere centered at the origin.\n- `box(x: number, y: number, z: number): SdfShape` — Create an SDF box centered at the origin with given full dimensions (not half-extents).\n- `cylinder(height: number, radius: number): SdfShape` — Create an SDF cylinder centered at the origin, axis along Z.\n- `torus(majorRadius: number, minorRadius: number): SdfShape` — Create an SDF torus centered at the origin, lying in the XY plane.\n- `capsule(height: number, radius: number): SdfShape` — Create an SDF capsule centered at the origin, axis along Z.\n- `cone(height: number, radius: number): SdfShape` — Create an SDF cone with base at z=0 and tip at z=height.\n- `smoothUnion(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape` — Smooth union — blends shapes together with a smooth transition radius.\n- `smoothDifference(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape` — Smooth difference — smoothly subtracts b from a.\n- `smoothIntersection(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape` — Smooth intersection — smoothly intersects a and b.\n- `morph(a: SdfShape, b: SdfShape, t: number): SdfShape` — Morph between two SDF shapes. t=0 → a, t=1 → b.\n- `blend(a: SdfShape, b: SdfShape, fn: (x: number, y: number, z: number) => number, options?: BlendOptions): SdfShape` — Spatially blend between two SDF patterns. The blend function receives (x, y, z) and returns 0..1: 0 = fully pattern `a`, 1 = fully pattern `b`.\n- `gyroid(options: TpmsOptions): SdfShape` — Gyroid TPMS lattice — the most common lattice for additive manufacturing.\n- `schwarzP(options: TpmsOptions): SdfShape` — Schwarz-P TPMS lattice — isotropic pore structure.\n- `diamond(options: TpmsOptions): SdfShape` — Diamond TPMS lattice — stiffest TPMS structure.\n- `lidinoid(options: TpmsOptions): SdfShape` — Lidinoid TPMS lattice — visually distinct from gyroid, popular in research and art.\n- `tpmsBlock(options: TpmsBlockOptions): SdfShape` — TPMS block preset clipped to an explicit design space.\n- `withinBox(shape: SdfShape, options: { size: Vec3; }): SdfShape` — Clip an SDF shape to a box-shaped design space.\n- `noise(options?: NoiseOptions): SdfShape` — 3D Simplex noise field — produces organic, natural-looking displacements.\n- `voronoi(options?: VoronoiOptions): SdfShape` — 3D Voronoi pattern — organic cellular structures like bone, coral, or soap bubbles.\n- `honeycomb(options?: HoneycombOptions): SdfShape` — Honeycomb (hexagonal) lattice pattern. Intersect with your shape to apply.\n- `waves(options?: WavesOptions): SdfShape` — Sinusoidal wave ridges — parallel ridges along an axis.\n- `knurl(options?: KnurlOptions): SdfShape` — Knurl pattern — crossed helical grooves for grips and handles.\n- `perforated(options?: PerforatedOptions): SdfShape` — Perforated plate pattern — regular array of cylindrical holes.\n- `scales(options?: ScalesOptions): SdfShape` — Fish/dragon scale pattern — overlapping circular scales in hex-packed rows.\n- `brick(options?: BrickOptions): SdfShape` — Brick/stone wall pattern — running bond with mortar grooves.\n- `weave(options?: WeaveOptions): SdfShape` — Grid lattice pattern — two families of infinite slabs crossing at 90°.\n- `basketWeave(options?: BasketWeaveOptions): SurfacePattern` — Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`.\n- `pattern2d(): Pattern2DBuilder` — Create typed, composable 2D surface patterns for `.surfaceDisplace()`.\n- `twist(shape: SdfShape, degreesPerUnit: number): SdfShape` — Twist an SDF shape around the Z axis.\n- `bend(shape: SdfShape, radius: number): SdfShape` — Bend an SDF shape around the Z axis.\n- `repeat(shape: SdfShape, spacing: Vec3, count?: Vec3): SdfShape` — Repeat an SDF shape in space.\n- `circularArray(shape: SdfShape, count: number, offset?: number): SdfShape` — Arrange an SDF shape in a circular array around the Z axis with O(1) folded-domain evaluation.\n- `SurfacePattern: typeof SurfacePattern` — A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`.\n- `fromFunction(fn: SdfFunctionSource, options: SdfFunctionOptions): SdfShape` — Create a custom SDF from one expression; shader-safe expressions raymarch directly.\n- `Sculpt: { sphere: (radius: number) => SdfShape; box: (x: number, y: number, z: number, options?: SculptBoxOptions) => SdfShape; cylinder: (height: number, radius: number) => SdfShape; disk: (radius: number, thickness?: number) => SdfShape; circle: (radius: number, thickness?: number) => SdfShape; capsule: (height: number, radius: number) => SdfShape; torus: (majorRadius: number, minorRadius: number) => SdfShape; cone: (height: number, radius: number) => SdfShape; tube: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; curve: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; path: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; blend: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape; union: (first?: SculptBlendInput, ...rest: SculptBlendInput[]) => SdfShape; carve: (base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions) => SdfShape; keep: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape; polish: (shape: SdfShape, input?: SculptPolishInput) => SdfShape; material: (input?: SculptPolishInput) => ShapeMaterialProps & { color?: string; }; look: (preset?: SculptLookPreset) => SceneOptions; knownMaterials: typeof knownSculptMaterialPresets; }` — Sculpt-like facade: friendly liquid-modeling verbs backed by the same SDF kernel.\n\n### `Sculpt`\n\n- `sphere(radius: number): SdfShape` — Create a liquid SDF sphere centered at the origin.\n- `box(x: number, y: number, z: number, options?: SculptBoxOptions): SdfShape` — Create a liquid SDF box; pass `{ radius }` for a rounded box.\n- `cylinder(height: number, radius: number): SdfShape` — Create a liquid SDF cylinder centered at the origin, axis along Z.\n- `disk(radius: number, thickness?: number): SdfShape` — Create a thin circular disk centered at the origin, axis along Z. Useful as a circular cutter or insert.\n- `circle(radius: number, thickness?: number): SdfShape` — Alias for `Sculpt.disk()`.\n- `capsule(height: number, radius: number): SdfShape` — Create a liquid SDF capsule centered at the origin, axis along Z.\n- `torus(majorRadius: number, minorRadius: number): SdfShape` — Create a liquid SDF torus lying in the XY plane.\n- `cone(height: number, radius: number): SdfShape` — Create a liquid SDF cone.\n- `tube(points: SculptPointList, options?: SculptTubeOptions): SdfShape` — Create a smooth tube through a list of 3D points.\n- `curve(points: SculptPointList, options?: SculptTubeOptions): SdfShape` — Create a smooth variable-thickness sweep through 3D control points.\n- `path(points: SculptPointList, options?: SculptTubeOptions): SdfShape` — Alias for `Sculpt.tube()`; points may use [x, y, z, radius] for variable thickness.\n- `blend(first?: SculptBlendArg, optionsOrShape?: SculptBlendArg, ...rest: SculptBlendArg[]): SdfShape` — Smoothly blend one or more SDF shapes into a continuous body.\n- `union(first?: SculptBlendInput, ...rest: SculptBlendInput[]): SdfShape` — Sharply union one or more SDF shapes.\n- `carve(base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions): SdfShape` — Smoothly subtract one or more cutter shapes from a base shape.\n- `keep(first?: SculptBlendArg, optionsOrShape?: SculptBlendArg, ...rest: SculptBlendArg[]): SdfShape` — Smoothly intersect one or more SDF shapes.\n- `polish(shape: SdfShape, input?: SculptPolishInput): SdfShape` — Apply a Sculpt material preset or direct material properties.\n- `material(input?: SculptPolishInput): ShapeMaterialProps & { color?: string; }` — Resolve a Sculpt material preset to ForgeCAD material properties.\n- `look(preset?: SculptLookPreset): SceneOptions` — Return a polished scene preset tuned for liquid SDF preview.\n- `knownMaterials(): SculptMaterialPreset[]` — List the built-in Sculpt material preset names.\n";
105
105
  const AI_USAGE_DOC = "/docs/ai-usage";
106
106
  function copyText(text) {
107
107
  var _a;
@@ -1416,6 +1416,8 @@ function CommandPalette() {
1416
1416
  const setTheme = useForgeStore((s) => s.setTheme);
1417
1417
  const theme = useForgeStore((s) => s.theme);
1418
1418
  const result = useForgeStore((s) => s.lastValidResult);
1419
+ const axesVisible = useForgeStore((s) => s.axesVisible);
1420
+ const setAxesVisible = useForgeStore((s) => s.setAxesVisible);
1419
1421
  const showPerformanceInfo = useForgeStore((s) => s.showPerformanceInfo);
1420
1422
  const setShowPerformanceInfo = useForgeStore((s) => s.setShowPerformanceInfo);
1421
1423
  const objectSettings = useForgeStore((s) => s.objectSettings);
@@ -1736,6 +1738,15 @@ function CommandPalette() {
1736
1738
  }
1737
1739
  }
1738
1740
  ] : [],
1741
+ {
1742
+ id: "toggle-xyz-axes",
1743
+ label: `${axesVisible ? "Hide" : "Show"} XYZ Axes`,
1744
+ searchText: "axis axes x y z vectors viewport screenshot",
1745
+ action: () => {
1746
+ setAxesVisible(!axesVisible);
1747
+ close();
1748
+ }
1749
+ },
1739
1750
  {
1740
1751
  id: "toggle-performance-info",
1741
1752
  label: `${showPerformanceInfo ? "Hide" : "Show"} Performance Info`,
@@ -8422,6 +8433,9 @@ function useViewPanelState() {
8422
8433
  const setRenderStyle = useForgeStore((s) => s.setRenderStyle);
8423
8434
  const renderMode = useForgeStore((s) => s.renderMode);
8424
8435
  const setRenderMode = useForgeStore((s) => s.setRenderMode);
8436
+ const manualScene = useForgeStore((s) => s.manualScene);
8437
+ const setManualScene = useForgeStore((s) => s.setManualScene);
8438
+ const resetManualScene = useForgeStore((s) => s.resetManualScene);
8425
8439
  const inspectChannel = useForgeStore((s) => s.inspectChannel);
8426
8440
  const setInspectChannel = useForgeStore((s) => s.setInspectChannel);
8427
8441
  const inspectDisplayMode = useForgeStore((s) => s.inspectDisplayMode);
@@ -8431,8 +8445,10 @@ function useViewPanelState() {
8431
8445
  const projectionMode = useForgeStore((s) => s.projectionMode);
8432
8446
  const setProjectionMode = useForgeStore((s) => s.setProjectionMode);
8433
8447
  const gridEnabled = useForgeStore((s) => s.gridEnabled);
8448
+ const axesVisible = useForgeStore((s) => s.axesVisible);
8434
8449
  const gridSize = useForgeStore((s) => s.gridSize);
8435
8450
  const setGridEnabled = useForgeStore((s) => s.setGridEnabled);
8451
+ const setAxesVisible = useForgeStore((s) => s.setAxesVisible);
8436
8452
  const setGridSize = useForgeStore((s) => s.setGridSize);
8437
8453
  const showPerformanceInfo = useForgeStore((s) => s.showPerformanceInfo);
8438
8454
  const setShowPerformanceInfo = useForgeStore((s) => s.setShowPerformanceInfo);
@@ -8581,6 +8597,9 @@ function useViewPanelState() {
8581
8597
  setRenderStyle,
8582
8598
  renderMode,
8583
8599
  setRenderMode,
8600
+ manualScene,
8601
+ setManualScene,
8602
+ resetManualScene,
8584
8603
  inspectChannel,
8585
8604
  setInspectChannel,
8586
8605
  inspectDisplayMode,
@@ -8590,8 +8609,10 @@ function useViewPanelState() {
8590
8609
  projectionMode,
8591
8610
  setProjectionMode,
8592
8611
  gridEnabled,
8612
+ axesVisible,
8593
8613
  gridSize,
8594
8614
  setGridEnabled,
8615
+ setAxesVisible,
8595
8616
  setGridSize,
8596
8617
  showPerformanceInfo,
8597
8618
  setShowPerformanceInfo,
@@ -8704,7 +8725,10 @@ const TABS = [
8704
8725
  const INSPECT_CHANNEL_OPTIONS = [
8705
8726
  { id: "none", label: "Off", title: "Standard viewport" },
8706
8727
  { id: "mask", label: "Mask", title: "Object identity mask" },
8728
+ { id: "normals", label: "Normals", title: "View-space surface normal colors" },
8729
+ { id: "zebra", label: "Zebra", title: "Reflective stripe surface continuity view" },
8707
8730
  { id: "connectivity", label: "Connect", title: "Physical connected components" },
8731
+ { id: "floating", label: "Floating", title: "Unsupported disconnected bodies above the build plane" },
8708
8732
  { id: "distance", label: "Distance", title: "Rooted component gap distance" },
8709
8733
  { id: "collisions", label: "Collision", title: "Collision overlap mask" },
8710
8734
  { id: "thickness", label: "Thickness", title: "Local wall thickness heatmap" },
@@ -8715,6 +8739,17 @@ const INSPECT_DISPLAY_OPTIONS = [
8715
8739
  { id: "points", label: "Points", title: "Show raw point-cloud samples only" },
8716
8740
  { id: "both", label: "Both", title: "Show heatmap and raw samples together" }
8717
8741
  ];
8742
+ const MANUAL_SCENE_LIGHT_FIELDS = [
8743
+ { id: "environmentIntensity", label: "Env", max: 3, step: 0.05 },
8744
+ { id: "ambientIntensity", label: "Ambient", max: 3, step: 0.05 },
8745
+ { id: "keyIntensity", label: "Key", max: 5, step: 0.05 },
8746
+ { id: "fillIntensity", label: "Fill", max: 3, step: 0.05 },
8747
+ { id: "rimIntensity", label: "Rim", max: 3, step: 0.05 }
8748
+ ];
8749
+ function clampSceneNumber(value, max) {
8750
+ if (!Number.isFinite(value)) return 0;
8751
+ return Math.max(0, Math.min(max, value));
8752
+ }
8718
8753
  const formatVector = (value) => value.map((entry) => entry.toFixed(3)).join(", ");
8719
8754
  function Section({ title, children }) {
8720
8755
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "fc-view-panel-section", children: [
@@ -8745,6 +8780,9 @@ function ViewPanel() {
8745
8780
  setRenderStyle,
8746
8781
  renderMode,
8747
8782
  setRenderMode,
8783
+ manualScene,
8784
+ setManualScene,
8785
+ resetManualScene,
8748
8786
  inspectChannel,
8749
8787
  setInspectChannel,
8750
8788
  inspectDisplayMode,
@@ -8754,8 +8792,10 @@ function ViewPanel() {
8754
8792
  projectionMode,
8755
8793
  setProjectionMode,
8756
8794
  gridEnabled,
8795
+ axesVisible,
8757
8796
  gridSize,
8758
8797
  setGridEnabled,
8798
+ setAxesVisible,
8759
8799
  setGridSize,
8760
8800
  showPerformanceInfo,
8761
8801
  setShowPerformanceInfo,
@@ -8842,6 +8882,9 @@ function ViewPanel() {
8842
8882
  copySceneCliArg,
8843
8883
  hasSceneCamera
8844
8884
  } = state;
8885
+ const updateManualSceneNumber = (id, value, max) => {
8886
+ setManualScene({ [id]: clampSceneNumber(value, max) });
8887
+ };
8845
8888
  const visibleJointCount = reactExports.useMemo(() => joints.filter((joint) => !joint.hidden).length, [joints]);
8846
8889
  const selectedLabel = ((_a = selectedObject == null ? void 0 : selectedObject.name) == null ? void 0 : _a.trim()) || (selectedObject == null ? void 0 : selectedObject.id) || (objects.length > 0 ? `${objects.length} objects` : "No model");
8847
8890
  const motionAvailable = visibleJointCount > 0 || animationClips.length > 0;
@@ -8885,6 +8928,88 @@ function ViewPanel() {
8885
8928
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: btn(renderMode === "overlay"), onClick: () => setRenderMode("overlay"), children: "Overlay" })
8886
8929
  ] })
8887
8930
  ] }),
8931
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fc-view-panel-control-block", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(ToggleRow, { children: [
8932
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: manualScene.enabled, onChange: (event) => setManualScene({ enabled: event.target.checked }) }),
8933
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Manual scene" })
8934
+ ] }) }),
8935
+ manualScene.enabled && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-subsection", children: [
8936
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-control-block", children: [
8937
+ /* @__PURE__ */ jsxRuntimeExports.jsx(FieldLabel, { children: "Colors" }),
8938
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-color-grid", children: [
8939
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "fc-view-panel-color-row", children: [
8940
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
8941
+ "input",
8942
+ {
8943
+ type: "color",
8944
+ value: manualScene.backgroundColor,
8945
+ onChange: (event) => setManualScene({ backgroundColor: event.target.value }),
8946
+ "aria-label": "Viewport background color",
8947
+ title: "Background"
8948
+ }
8949
+ ),
8950
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Background" })
8951
+ ] }),
8952
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "fc-view-panel-color-row", children: [
8953
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
8954
+ "input",
8955
+ {
8956
+ type: "color",
8957
+ value: manualScene.groundColor,
8958
+ onChange: (event) => setManualScene({ groundColor: event.target.value }),
8959
+ "aria-label": "Viewport ground color",
8960
+ title: "Ground"
8961
+ }
8962
+ ),
8963
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Ground" })
8964
+ ] })
8965
+ ] })
8966
+ ] }),
8967
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fc-view-panel-control-block", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(ToggleRow, { children: [
8968
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
8969
+ "input",
8970
+ {
8971
+ type: "checkbox",
8972
+ checked: manualScene.groundVisible,
8973
+ onChange: (event) => setManualScene({ groundVisible: event.target.checked })
8974
+ }
8975
+ ),
8976
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Ground plane" })
8977
+ ] }) }),
8978
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-control-block", children: [
8979
+ /* @__PURE__ */ jsxRuntimeExports.jsx(FieldLabel, { children: "Lighting" }),
8980
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fc-view-panel-slider-stack", children: MANUAL_SCENE_LIGHT_FIELDS.map((field) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-scene-slider", children: [
8981
+ /* @__PURE__ */ jsxRuntimeExports.jsx(FieldLabel, { children: field.label }),
8982
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-range-row", children: [
8983
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
8984
+ "input",
8985
+ {
8986
+ type: "range",
8987
+ min: 0,
8988
+ max: field.max,
8989
+ step: field.step,
8990
+ value: manualScene[field.id],
8991
+ onChange: (event) => updateManualSceneNumber(field.id, Number(event.target.value), field.max),
8992
+ "aria-label": `${field.label} intensity`
8993
+ }
8994
+ ),
8995
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
8996
+ "input",
8997
+ {
8998
+ className: "fc-view-panel-input fc-view-panel-number-input",
8999
+ type: "number",
9000
+ min: 0,
9001
+ max: field.max,
9002
+ step: field.step,
9003
+ value: Number(manualScene[field.id].toFixed(2)),
9004
+ onChange: (event) => updateManualSceneNumber(field.id, Number(event.target.value), field.max),
9005
+ "aria-label": `${field.label} intensity value`
9006
+ }
9007
+ )
9008
+ ] })
9009
+ ] }, field.id)) })
9010
+ ] }),
9011
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "fc-btn fc-view-panel-btn fc-view-panel-full-button", onClick: resetManualScene, children: "Reset Scene" })
9012
+ ] }),
8888
9013
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-control-block", children: [
8889
9014
  /* @__PURE__ */ jsxRuntimeExports.jsx(FieldLabel, { children: "Projection" }),
8890
9015
  /* @__PURE__ */ jsxRuntimeExports.jsxs(ButtonGrid, { min: 108, children: [
@@ -8958,6 +9083,10 @@ function ViewPanel() {
8958
9083
  /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: gridEnabled, onChange: (event) => setGridEnabled(event.target.checked) }),
8959
9084
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Grid" })
8960
9085
  ] }),
9086
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(ToggleRow, { children: [
9087
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: axesVisible, onChange: (event) => setAxesVisible(event.target.checked) }),
9088
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "XYZ axes" })
9089
+ ] }),
8961
9090
  /* @__PURE__ */ jsxRuntimeExports.jsxs(ToggleRow, { children: [
8962
9091
  /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: dimensionsVisible, onChange: toggleDimensions }),
8963
9092
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Dimensions" })
@@ -9456,6 +9585,8 @@ function MobileCommandPalette({ onClose, onOpenFilePicker, onOpenJoints }) {
9456
9585
  const activeFile = useForgeStore((s) => s.activeFile);
9457
9586
  const openShare = useForgeStore((s) => s.openShare);
9458
9587
  const requestViewCommand = useForgeStore((s) => s.requestViewCommand);
9588
+ const axesVisible = useForgeStore((s) => s.axesVisible);
9589
+ const setAxesVisible = useForgeStore((s) => s.setAxesVisible);
9459
9590
  const showPerformanceInfo = useForgeStore((s) => s.showPerformanceInfo);
9460
9591
  const setShowPerformanceInfo = useForgeStore((s) => s.setShowPerformanceInfo);
9461
9592
  const showAllObjects = useForgeStore((s) => s.showAllObjects);
@@ -9721,6 +9852,16 @@ function MobileCommandPalette({ onClose, onOpenFilePicker, onOpenJoints }) {
9721
9852
  }
9722
9853
  }
9723
9854
  ] : [],
9855
+ {
9856
+ id: "toggle-xyz-axes",
9857
+ label: `${axesVisible ? "Hide" : "Show"} XYZ Axes`,
9858
+ icon: "XYZ",
9859
+ searchText: "axis axes x y z vectors viewport screenshot",
9860
+ action: () => {
9861
+ setAxesVisible(!axesVisible);
9862
+ onClose();
9863
+ }
9864
+ },
9724
9865
  {
9725
9866
  id: "toggle-performance-info",
9726
9867
  label: `${showPerformanceInfo ? "Hide" : "Show"} Performance Info`,
@@ -9741,7 +9882,7 @@ function MobileCommandPalette({ onClose, onOpenFilePicker, onOpenJoints }) {
9741
9882
  const match = input.match(/gist\.github\.com\/(?:[^/]+\/)?([a-f0-9]+)/i);
9742
9883
  const gistId = match ? match[1] : input.trim();
9743
9884
  __vitePreload(async () => {
9744
- const { fetchGistModel: fetchGistModel2 } = await import("./app-BE2nD6Yz.js").then((n) => n.ao);
9885
+ const { fetchGistModel: fetchGistModel2 } = await import("./app-Dn4EwHhN.js").then((n) => n.ao);
9745
9886
  return { fetchGistModel: fetchGistModel2 };
9746
9887
  }, true ? __vite__mapDeps([0]) : void 0).then(
9747
9888
  ({ fetchGistModel: fetchGistModel2 }) => fetchGistModel2(gistId).then((model) => {
@@ -9759,7 +9900,7 @@ function MobileCommandPalette({ onClose, onOpenFilePicker, onOpenJoints }) {
9759
9900
  const input = window.prompt("Paste a URL to a .forge.js file:");
9760
9901
  if (!input) return;
9761
9902
  __vitePreload(async () => {
9762
- const { fetchUrlModel: fetchUrlModel2 } = await import("./app-BE2nD6Yz.js").then((n) => n.ao);
9903
+ const { fetchUrlModel: fetchUrlModel2 } = await import("./app-Dn4EwHhN.js").then((n) => n.ao);
9763
9904
  return { fetchUrlModel: fetchUrlModel2 };
9764
9905
  }, true ? __vite__mapDeps([0]) : void 0).then(
9765
9906
  ({ fetchUrlModel: fetchUrlModel2 }) => fetchUrlModel2(input.trim()).then((model) => {
@@ -11701,7 +11842,7 @@ function MobileViewport({ jointMatrices, onShare, onExport }) {
11701
11842
  Canvas,
11702
11843
  {
11703
11844
  style: { background: themeBg },
11704
- dpr: [1, 2],
11845
+ dpr: isInteracting ? 1 : [1, 2],
11705
11846
  gl: {
11706
11847
  antialias: true,
11707
11848
  logarithmicDepthBuffer: true,
@@ -11729,14 +11870,15 @@ function MobileViewport({ jointMatrices, onShare, onExport }) {
11729
11870
  SceneConfigurator,
11730
11871
  {
11731
11872
  config: sceneConfig,
11873
+ interactivePreview: isInteracting,
11732
11874
  controlsRef,
11733
11875
  modelBoundsMinZ,
11734
11876
  onDefaultLightsOverridden: handleDefaultLightsOverridden,
11735
11877
  onDefaultEnvironmentOverridden: handleDefaultEnvironmentOverridden
11736
11878
  }
11737
11879
  ),
11738
- !defaultEnvironmentOverridden && /* @__PURE__ */ jsxRuntimeExports.jsx(LocalStudioEnvironment, {}),
11739
- !defaultLightsOverridden && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
11880
+ !isInteracting && !defaultEnvironmentOverridden && /* @__PURE__ */ jsxRuntimeExports.jsx(LocalStudioEnvironment, {}),
11881
+ (isInteracting || !defaultLightsOverridden) && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
11740
11882
  /* @__PURE__ */ jsxRuntimeExports.jsx("ambientLight", { intensity: 0.3 }),
11741
11883
  /* @__PURE__ */ jsxRuntimeExports.jsx("directionalLight", { position: [100, 150, 80], intensity: 1.2, castShadow: true }),
11742
11884
  /* @__PURE__ */ jsxRuntimeExports.jsx("directionalLight", { position: [-60, -40, -80], intensity: 0.3 }),