forgecad 0.9.2 → 0.9.3

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 (79) hide show
  1. package/LICENSE +7 -5
  2. package/README.md +1 -1
  3. package/README.public.md +24 -2
  4. package/dist/assets/{AdminPage-Bs4PiK00.js → AdminPage-4jihcEk_.js} +1 -1
  5. package/dist/assets/{BlogPage-DVmgN0ma.js → BlogPage-BvzruKtw.js} +1 -1
  6. package/dist/assets/{DocsPage-BP6wlsBN.js → DocsPage-DHbd-WS-.js} +13 -13
  7. package/dist/assets/{EditorApp-Arw2NnGJ.js → EditorApp-C5P2rBfh.js} +433 -84
  8. package/dist/assets/{EditorApp-VY9lXx0N.css → EditorApp-DS0AIUrZ.css} +25 -0
  9. package/dist/assets/{EmbedViewer-qgQiOahL.js → EmbedViewer-B70wQwlE.js} +2 -2
  10. package/dist/assets/{LandingPageProofDriven-DvhtmWOz.js → LandingPageProofDriven-DIsYTnep.js} +1 -1
  11. package/dist/assets/{PricingPage-Ck3CP2ti.css → PricingPage-BMedqFef.css} +48 -0
  12. package/dist/assets/{PricingPage-657oLvWh.js → PricingPage-YPOr12pP.js} +34 -6
  13. package/dist/assets/{SettingsPage-wNy3_2yn.js → SettingsPage-rntoyJ3b.js} +10 -13
  14. package/dist/assets/{app-BdBoMQeO.js → app-CWucmnLZ.js} +801 -1208
  15. package/dist/assets/cli/{render-Ci3jjyT1.js → render-DZHmUySW.js} +214 -23
  16. package/dist/assets/copy-CQKQppF-.js +8 -0
  17. package/dist/assets/{evalWorker-CMCAbK8r.js → evalWorker-C3dKxi9Y.js} +1117 -95
  18. package/dist/assets/{manifold-BMn-8Vf8.js → manifold-CQ3FhfWB.js} +1 -1
  19. package/dist/assets/{manifold-jlYQ6E5R.js → manifold-CU0G1yYL.js} +1 -1
  20. package/dist/assets/{manifold-DbyILno4.js → manifold-CYWZMfjB.js} +2 -2
  21. package/dist/assets/{renderSceneState-DAnqvxSt.js → renderSceneState-BBUrnsUN.js} +1 -1
  22. package/dist/assets/{reportWorker-BcRVMHK-.js → reportWorker-BhZ7DjxQ.js} +1091 -95
  23. package/dist/assets/{sectionPlaneMath-DXJ_TdIW.js → sectionPlaneMath-BxfokaJE.js} +1091 -95
  24. package/dist/cli/render.html +1 -1
  25. package/dist/docs/index.html +2 -2
  26. package/dist/docs-raw/AI/usage.md +182 -89
  27. package/dist/docs-raw/API/core/concepts.md +26 -0
  28. package/dist/docs-raw/CLI.md +58 -37
  29. package/dist/docs-raw/INDEX.md +81 -64
  30. package/dist/docs-raw/cli-monetization.md +9 -8
  31. package/dist/docs-raw/generated/concepts.md +111 -4
  32. package/dist/docs-raw/generated/core.md +2 -0
  33. package/dist/docs-raw/generated/curves.md +480 -1
  34. package/dist/docs-raw/generated/output.md +1 -0
  35. package/dist/docs-raw/generated/sketch.md +2 -0
  36. package/dist/docs-raw/generated/viewport.md +81 -3
  37. package/dist/docs-raw/product/user-outreach-email-templates.md +159 -0
  38. package/dist/docs-raw/skills/forgecad-image-replicator.md +1 -1
  39. package/dist/docs-raw/skills/forgecad-make-a-model.md +33 -4
  40. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +1 -1
  41. package/dist/docs-raw/skills/forgecad-project.md +1 -1
  42. package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
  43. package/dist/docs-raw/skills/forgecad.md +2 -1
  44. package/dist/docs-raw/welcome.md +85 -137
  45. package/dist/index.html +1 -1
  46. package/dist/llms.txt +4 -3
  47. package/dist/sitemap.xml +6 -6
  48. package/dist-cli/forgecad.js +1413 -219
  49. package/dist-cli/forgecad.js.map +1 -1
  50. package/dist-skill/CONTEXT.md +594 -5
  51. package/dist-skill/SKILL-dev.md +2 -1
  52. package/dist-skill/SKILL.md +2 -1
  53. package/dist-skill/docs/API/core/concepts.md +26 -0
  54. package/dist-skill/docs/CLI.md +58 -37
  55. package/dist-skill/docs/generated/core.md +2 -0
  56. package/dist-skill/docs/generated/curves.md +480 -1
  57. package/dist-skill/docs/generated/output.md +1 -0
  58. package/dist-skill/docs/generated/sketch.md +2 -0
  59. package/dist-skill/docs/generated/viewport.md +81 -3
  60. package/dist-skill/docs-dev/API/core/concepts.md +26 -0
  61. package/dist-skill/docs-dev/CLI.md +58 -37
  62. package/dist-skill/docs-dev/generated/core.md +2 -0
  63. package/dist-skill/docs-dev/generated/curves.md +480 -1
  64. package/dist-skill/docs-dev/generated/output.md +1 -0
  65. package/dist-skill/docs-dev/generated/sketch.md +2 -0
  66. package/dist-skill/docs-dev/generated/viewport.md +81 -3
  67. package/dist-skill/library/README.md +0 -1
  68. package/dist-skill/library/forgecad-image-replicator/SKILL.md +1 -1
  69. package/dist-skill/library/forgecad-make-a-model/SKILL.md +33 -4
  70. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
  71. package/dist-skill/library/forgecad-project/SKILL.md +1 -1
  72. package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
  73. package/examples/api/conformal-product-ribbon.forge.js +77 -0
  74. package/examples/api/render-labels.forge.js +33 -0
  75. package/examples/api/text2d-basics.forge.js +6 -3
  76. package/package.json +1 -1
  77. package/dist-skill/library/forgecad-deep-dive/SKILL.md +0 -120
  78. package/dist-skill/library/forgecad-deep-dive/agents/openai.yaml +0 -4
  79. package/dist-skill/library/forgecad-deep-dive/references/output-shape.md +0 -64
@@ -1,8 +1,9 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/app-aLOpQQza.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-mDaEzY3y.js";
3
- import { bO as generateCuttingLayoutPdf, aA as getShapeCompilePlan, bP as getCameraForwardVector, bQ as RENDER_STYLE_OPTIONS, _ as __vitePreload, $ as Matrix4, bq as getSketchWorldMatrix, G as Box3, A as ACESFilmicToneMapping, b2 as shapeToGeometry, D as DoubleSide, bR as initKernelManifoldOnly, bF as initSolverWasm, bE as initKernel } from "./sectionPlaneMath-DXJ_TdIW.js";
4
- import { u as useAuthStore, c as authApi, s as showToast, A as AuthApiError, d as useProjectStore, t as triggerDownload, e as useForgeStore, r as readProjectFilesFromDataTransfer, h as hasImports, f as collectDependencies, g as buildBundleShareUrl, i as buildBundleEmbedUrl, j as buildShareUrl, k as buildEmbedUrl, l as fileSystem, m as useFeatureFlagStore, n as fetchGistModel, o as fetchUrlModel, F as FLAG_DEFINITIONS, p as exportMeshFromStore, q as exportReportFromStore, v as exportOrbitVideoFromStore, w as exportSketchFromStore, x as buildGistShareUrl, y as exportExactFromStore, z as deriveExportStem, b as authFetch, C as isImportableProjectMeshFile, D as isImportableProjectBinaryFile, E as hasExternalFiles, G as isImportableProjectExactFile, H as resolvePreviewFile, I as countParamSnapshotDiff, J as buildEmbedSnippet, a as applyTheme, K as themes, L as formatArea, M as sliderToAnimationSpeed, N as animationSpeedToSlider, O as formatAnimationSpeed, P as resolveJointRange, Q as useJointsConfig, R as useJointAnimationValues, S as expandBoundsByTransformedAabb, T as Canvas, U as PerspectiveCamera, V as ControlsInteractionBridge, W as ViewController, X as SceneConfigurator, Y as LocalStudioEnvironment, Z as Grid, _ as OrbitControls, $ as TOUCH_GESTURES_3D, a0 as MOUSE_BUTTONS_3D, a1 as FOCUS_MODE_DIM_OPACITY, a2 as useJointAnimationLoop, a3 as computeJointNodeMatrices, a4 as computeObjectJointMatrices, a5 as readLastActiveFileForUser, a6 as ToastContainer, a7 as isMobile, a8 as useFeatureFlag, a9 as decodeSharedHash, aa as decodeSharedBundle, ab as getExternalUrl, ac as getGistId, ad as Viewport, ae as shouldBlockBrowserShortcut, af as useDrawStore, ag as storePendingShareCopy } from "./app-BdBoMQeO.js";
5
- import { f as formatRenderSceneCliSpec } from "./renderSceneState-DAnqvxSt.js";
3
+ import { bO as generateCuttingLayoutPdf, aA as getShapeCompilePlan, bP as getCameraForwardVector, bQ as RENDER_STYLE_OPTIONS, _ as __vitePreload, $ as Matrix4, bq as getSketchWorldMatrix, G as Box3, A as ACESFilmicToneMapping, b2 as shapeToGeometry, D as DoubleSide, bR as initKernelManifoldOnly, bF as initSolverWasm, bE as initKernel } from "./sectionPlaneMath-BxfokaJE.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 expandBoundsByTransformedAabb, W as Canvas, X as PerspectiveCamera, Y as ControlsInteractionBridge, Z as ViewController, _ as SceneConfigurator, $ as LocalStudioEnvironment, a0 as RenderLabelsOverlay, a1 as Grid, a2 as OrbitControls, a3 as TOUCH_GESTURES_3D, a4 as MOUSE_BUTTONS_3D, a5 as ModelJourneyBar, a6 as FOCUS_MODE_DIM_OPACITY, a7 as useJointAnimationLoop, a8 as computeJointNodeMatrices, a9 as computeObjectJointMatrices, aa as readLastActiveFileForUser, ab as ToastContainer, ac as isMobile, ad as useFeatureFlag, ae as decodeSharedHash, af as decodeSharedBundle, ag as getExternalUrl, ah as getGistId, ai as Viewport, aj as shouldBlockBrowserShortcut, ak as useDrawStore, al as storePendingShareCopy } from "./app-CWucmnLZ.js";
5
+ import { a as PRODUCTION_EXPORT_COPY } from "./copy-CQKQppF-.js";
6
+ import { f as formatRenderSceneCliSpec } from "./renderSceneState-BBUrnsUN.js";
6
7
  import { H as HighlightJS, j as javascript } from "./javascript-C0oc9u8V.js";
7
8
  const RESEND_COOLDOWN_S = 60;
8
9
  function EmailVerificationBanner() {
@@ -100,7 +101,7 @@ function logoutAndRedirectToLanding(logout, navigate) {
100
101
  navigate("/", { replace: true });
101
102
  });
102
103
  }
103
- 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\n### Import and composition\n\n- `require(\"./file.forge.js\", { Param: value })` for any model file, with optional param overrides.\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 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 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## 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, 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\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\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## 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- [Backend Runtime](#backend-runtime) — `initKernel`, `setActiveBackend`, `activateBackend`, `getActiveBackend`\n- [Verification](#verification) — `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- [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 fillets (rounded edges) to one or more edges of a shape.\n\nWorks on both straight and curved edges. Supports OCCT and Manifold backends. When using OCCT, all edges are filleted in a single kernel operation for best quality. When using Manifold, edges are filleted sequentially.\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 chamfers (beveled edges) to one or more edges of a shape.\n\nProduces a 45° bevel at the specified `size` (distance from edge). Works on both straight and curved edges. Supports OCCT and Manifold backends.\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\nRequires the OCCT backend. Throws on Manifold.\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\nExtracts 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**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 `setActiveBackend('occt')`.\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\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\n// BAD — every sub-part repeats the parent's global offset\nconst unitX = 0, unitY = -18, unitZ = 70;\nconst body = roundedBox(100, 20, 32, 4).translate(unitX, unitY, unitZ);\nconst panel = box(98, 2, 18).translate(unitX, unitY - 12, unitZ + 4);\nconst louver = box(88, 2, 6).translate(unitX, unitY - 14, unitZ - 11);\n```\n\n// GOOD — build at origin, group, translate once const body = roundedBox(100, 20, 32, 4); const panel = box(98, 2, 18).translate(0, -12, 4); const louver = box(88, 2, 6).translate(0, -14, -11); const indoorUnit = group( { name: 'Body', shape: body }, { name: 'Panel', shape: panel }, { name: 'Louver', shape: louver }, ).translate(0, -18, 70);\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### Backend Runtime\n\n#### `initKernel()`\n\n```ts\ninitKernel(): Promise<unknown>\n```\n\n#### `setActiveBackend()`\n\n```ts\nsetActiveBackend(backend: ActiveBackend): void\n```\n\n#### `activateBackend()` — Set the active backend and ensure its WASM module is initialized. Call this instead of `setActiveBackend` when you're about to execute code — it guarantees the backend is ready, not just selected.\n\n```ts\nactivateBackend(backend: ActiveBackend): Promise<void>\n```\n\n#### `getActiveBackend()`\n\n```ts\ngetActiveBackend(): ActiveBackend\n```\n\n### Verification\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### `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- `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- `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- `start: [ number, number, number ]` — Start point\n- `end: [ number, number, number ]` — End point\n- `query?: EdgeQueryRef` — Compiler-owned edge query when available.\n- Also: `name: EdgeName`\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\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'`) text2d('Hello World', { size: 10 }) // default Inter text2d('Custom Font', { size: 10, font: '/path/to/font.ttf' }) |\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#### `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) — `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`\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- [ProductSkinBuilder](#productskinbuilder)\n- [ProductStationBuilder](#productstationbuilder)\n- [ProductPanelBuilder](#productpanelbuilder)\n- [ProductSpoutBuilder](#productspoutbuilder)\n- [ProductHandleBuilder](#producthandlebuilder)\n- [ProductHandleFeature](#producthandlefeature)\n- [Surface](#surface)\n- [Blend](#blend)\n- [Analysis](#analysis)\n- [Product](#product)\n\n## Functions\n\n### Curves & Surfacing\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**`LoftOptions`**\n- `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.\n- `boundsPadding?: number` — Optional extra bounds padding.\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| `approximate?` | `boolean` | Explicit opt-in for sampled fallback paths on non-exact backends. |\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---\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\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#### `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#### `diagnostics()` — Return lowering representation, station names, rail names, and warnings.\n\n```ts\ndiagnostics(): ProductSkinDiagnostics\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#### `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#### `stationAt()` — Interpolate center, width, and depth at a normalized v or absolute axis value.\n\n```ts\nstationAt(vOrAxis: number): { center: Vec3; width: number; depth: number; dWidth: number; dDepth: number; axisValue: number; }\n```\n\n#### `frame()` — Build a local surface frame from a side/u/v query.\n\n```ts\nframe(query: ProductSkinRefQuery): ProductSurfaceFrame\n```\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#### `querySpec()` — Return the serializable side/u/v query behind this ref.\n\n```ts\nquerySpec(): ProductSkinRefQuery\n```\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#### `stations()` — Set named cross-section stations for the product skin.\n\n```ts\nstations(stations: Array<ProductStationBuilder | ProductStationSpec>): this\n```\n\n#### `rails()` — Attach guide rails as ProductSkin IR metadata and diagnostics.\n\n```ts\nrails(rails: Record<string, ProductRailSpec>): this\n```\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()` — Records a target wall thickness; v1 keeps exterior skin lowering sampled and reports wall as a diagnostic.\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#### [`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()` — Stores a semantic crown amount for diagnostics and future rail solving.\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### `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---\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- `CornerY(options: BlendCornerYOptions): Shape` — Current implementation uses continuity-controlled edge fillets on solid edges. It does not yet provide a dedicated open-sheet or multi-patch Y-corner solver. Follow progress: https://github.com/KoStard/forgecad-private/issues/162\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- `panel(name: string): ProductPanelBuilder` — Start a panel feature builder.\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---\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`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`\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; }>; 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; }>; 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\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`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`\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- [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---\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 on the `lib` namespace exposed to `.forge.js` scripts. The catalog includes:\n\n**Fasteners:** `bolt`, `nut`, `washer`, `fastenerSet`, `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`, `bevelGear`, `faceGear`, `sideGear`, `ringGear`, `rackGear`, `gearPair`, `bevelGearPair`, `faceGearPair`, `sideGearPair`\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\nExtend this by adding new entries here and registering the corresponding runner binding in `runner.ts`. Sizes 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- `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). ```js assembly.addJointCoupling(\"B_spin\", { terms: [{ joint: \"A_spin\", ratio: lib.gearRatio(12, 24) }], // -0.5 }); ``` 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. ```js // Pinion spin driven by rack slide: assembly.addJointCoupling(\"pinion_spin\", { terms: [{ joint: \"rack_slide\", ratio: lib.rackRatio(1.5, 12) }], // ~6.37 deg/mm }); ```\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. ```js const bolts = lib.boltPattern({ size: 'M5', positions: [[20, 15], [-20, 15], [20, -15], [-20, -15]], }); 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\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) — `scene`, `viewConfig`, `explodeView`, `jointsView`, `cutPlane`, `mock`, `showLabels`, `highlight`\n- [RouteBuilder](#routebuilder)\n- [route](#route)\n\n## Functions\n\n### Viewport & Runtime\n\n#### `scene()` — Configure the scene environment for the current script execution.\n\nControls camera position, 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\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 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?`, `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**`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`ViewConfigOptions`: `{ jointOverlay?: JointOverlayViewConfigOptions }`\n\n**`JointOverlayViewConfigOptions`**: `enabled?: boolean`, `axisColor?: string`, `axisCoreColor?: string`, `arcColor?: string`, `zeroColor?: string`, `arcVisualLimitDeg?: number`, `axisLengthScale?: number`, `axisLengthMin?: number`, `axisLineRadiusScale?: number`, `axisLineRadiusMin?: number`, `axisLineRadiusMax?: number`, `spokeLineRadiusScale?: number`, `spokeLineRadiusMin?: number`, `spokeLineRadiusMax?: number`, `arcLineRadiusScale?: number`, `arcLineRadiusMin?: number`, `arcLineRadiusMax?: number`, `axisDotRadiusScale?: number`, `axisDotRadiusMin?: number`, `axisArrowRadiusScale?: number`, `axisArrowRadiusMin?: number`, `axisArrowLengthScale?: number`, `axisArrowLengthMin?: number`, `axisArrowOffsetFactor?: number`, `arcRadiusScale?: number`, `arcRadiusMin?: number`, `arcDotRadiusScale?: number`, `arcDotRadiusMin?: number`, `arcArrowRadiusScale?: number`, `arcArrowRadiusMin?: number`, `arcArrowLengthScale?: number`, `arcArrowLengthMin?: number`, `arcArrowOffsetFactor?: number`, `arcStepDeg?: number`, `arcMinSteps?: number`, `arcTubeSegmentsMin?: number`, `arcTubeSegmentsFactor?: number`, `arcTubeRadialSegments?: number`\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` 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| `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- `start: [ number, number, number ]` — Start point\n- `end: [ number, number, number ]` — End point\n- `query?: EdgeQueryRef` — Compiler-owned edge query when available.\n- Also: `name: EdgeName`\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, part identity,\nphysical connected components, collisions, local 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\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. Keep bundles targeted to the current question so heavy\nanalyses 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.\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,mask,collisions\nforgecad render inspect model.forge.js --channels rgb,section,thickness\n```\n\nSupported channels are `rgb`, `depth`, `normals`, `mask`, `connectivity`,\n`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`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 overlap edge = bbox interiors overlap\ntouching edge = bbox contact gap <= 0.05 model units\ncomponent = transitive closure over overlap/touching 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 is a fast bbox-neighborhood graph. Use the `collisions` channel\nwhen you need exact positive-volume boolean overlap evidence.\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\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, complete component gap edge list, 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, warnings, render style, and each collision finding's `groupIndex`,\n`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\nsamples visible mesh triangles, casts through the object along each triangle\nnormal, and colors the surface by the first opposite-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 sampled triangles 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\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 intentionally simple and may over-connect concave shapes whose\n bounding boxes touch while surfaces do not. 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#### `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#### `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 (e.g. basketWeave)\nshape.displace(sdf.basketWeave({ threads: 16, spacing: 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\n```js\n// Surface-following basket weave — auto-detects sphere UV\nsdf.sphere(27).shell(3)\n .surfaceDisplace(sdf.basketWeave({ spacing: 3, depth: 0.8 }))\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- `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- `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\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 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 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, 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\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\nUse `text2d()` only when the letters are part of the model: raised text, engraving, cut labels, serial plates, exported markings, or geometry that should survive into STL/STEP output. `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)` when the goal is to explain the model in the viewport. 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.\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- [Backend Runtime](#backend-runtime) — `initKernel`, `setActiveBackend`, `activateBackend`, `getActiveBackend`\n- [Verification](#verification) — `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- [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 fillets (rounded edges) to one or more edges of a shape.\n\nWorks on both straight and curved edges. Supports OCCT and Manifold backends. When using OCCT, all edges are filleted in a single kernel operation for best quality. When using Manifold, edges are filleted sequentially.\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 chamfers (beveled edges) to one or more edges of a shape.\n\nProduces a 45° bevel at the specified `size` (distance from edge). Works on both straight and curved edges. Supports OCCT and Manifold backends.\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\nRequires the OCCT backend. Throws on Manifold.\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\nExtracts 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 `setActiveBackend(\'occt\')`.\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\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\n// BAD — every sub-part repeats the parent\'s global offset\nconst unitX = 0, unitY = -18, unitZ = 70;\nconst body = roundedBox(100, 20, 32, 4).translate(unitX, unitY, unitZ);\nconst panel = box(98, 2, 18).translate(unitX, unitY - 12, unitZ + 4);\nconst louver = box(88, 2, 6).translate(unitX, unitY - 14, unitZ - 11);\n```\n\n// GOOD — build at origin, group, translate once const body = roundedBox(100, 20, 32, 4); const panel = box(98, 2, 18).translate(0, -12, 4); const louver = box(88, 2, 6).translate(0, -14, -11); const indoorUnit = group( { name: \'Body\', shape: body }, { name: \'Panel\', shape: panel }, { name: \'Louver\', shape: louver }, ).translate(0, -18, 70);\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### Backend Runtime\n\n#### `initKernel()`\n\n```ts\ninitKernel(): Promise<unknown>\n```\n\n#### `setActiveBackend()`\n\n```ts\nsetActiveBackend(backend: ActiveBackend): void\n```\n\n#### `activateBackend()` — Set the active backend and ensure its WASM module is initialized. Call this instead of `setActiveBackend` when you\'re about to execute code — it guarantees the backend is ready, not just selected.\n\n```ts\nactivateBackend(backend: ActiveBackend): Promise<void>\n```\n\n#### `getActiveBackend()`\n\n```ts\ngetActiveBackend(): ActiveBackend\n```\n\n### Verification\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### `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- `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- `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- `start: [ number, number, number ]` — Start point\n- `end: [ number, number, number ]` — End point\n- `query?: EdgeQueryRef` — Compiler-owned edge query when available.\n- Also: `name: EdgeName`\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 non-exported explanatory labels in the viewport, prefer `Viewport.label()` so the text stays off the geometry and OCCT compile paths.\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\'`) text2d(\'Hello World\', { size: 10 }) // default Inter text2d(\'Custom Font\', { size: 10, font: \'/path/to/font.ttf\' }) |\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#### `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) — `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`\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- [Surface](#surface)\n- [Blend](#blend)\n- [Analysis](#analysis)\n- [Product](#product)\n\n## Functions\n\n### Curves & Surfacing\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**`LoftOptions`**\n- `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.\n- `boundsPadding?: number` — Optional extra bounds padding.\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| `approximate?` | `boolean` | Explicit opt-in for sampled fallback paths on non-exact backends. |\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---\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\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#### `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#### `diagnostics()` — Return lowering representation, station names, rail names, and warnings.\n\n```ts\ndiagnostics(): ProductSkinDiagnostics\n```\n\n**`ProductSkinDiagnostics`**: `representation: ProductSkinRepresentation`, `lowering: string[]`, `warnings: string[]`, `stationNames: string[]`, `railNames: string[]`\n\n**`ProductSkinRepresentation`** — Reported lowering mode for ProductSkin and conformal feature diagnostics.\n\n`"exact" | "sampled" | "mixed" | "fallback"`\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`, `representation: ProductSkinRepresentation`\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**`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**`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**`ProductSurfaceFrame`**: `point: Vec3`, `normal: Vec3`, `tangentU: Vec3`, `tangentV: Vec3`, `matrix: Mat4`, `skin: string`, `representation: ProductSkinRepresentation`\n\n**`ProductSkinRepresentation`** — Reported lowering mode for ProductSkin and conformal feature diagnostics.\n\n`"exact" | "sampled" | "mixed" | "fallback"`\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**`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**`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#### `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`**\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**`ProductProfileKind`**\n\n`"oval" | "roundedRect" | "circle" | "superEllipse" | "custom"`\n\n#### `rails()` — Attach guide rails as ProductSkin IR metadata and diagnostics.\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**`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**`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#### `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`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#### `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()` — Records a target wall thickness; v1 keeps exterior skin lowering sampled and reports wall as a diagnostic.\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()` — Stores a semantic crown amount for diagnostics and future rail solving.\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`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**`ProductProfileKind`**\n\n`"oval" | "roundedRect" | "circle" | "superEllipse" | "custom"`\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`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#### `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`ProductAttachOptions`: `{ offset?: number, inset?: number }`\n\n`ProductPanelAttachOptions`: `{ at?: Partial<ProductSkinRefQuery>, thickness?: number, material?: ProductMaterial, color?: string }`\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**`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### `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**`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**`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**`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#### `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#### `buildWithDiagnostics()` — Build a conformal ribbon and return surface-feature diagnostics.\n\nUse this while validating API usage or model fidelity; diagnostics report sampling counts, side-span clamping, lowering mode, and warnings that should be visible in reviews.\n\n```ts\nbuildWithDiagnostics(options?: ProductRibbonBuildOptions): ProductRibbonResult\n```\n\n**`ProductRibbonResult`** — Shape plus diagnostics returned by ProductRibbonBuilder.buildWithDiagnostics().\n- `shape: Shape` — Lowered conformal ribbon shape.\n- `diagnostics: ProductRibbonDiagnostics` — Sampling and lowering diagnostics for the returned shape.\n\n**`ProductRibbonDiagnostics`** — Diagnostics describing how a conformal ribbon was sampled and lowered.\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `name` | `string` | Ribbon shape name. |\n| `skin?` | `string` | Source skin name when the ribbon follows a ProductSkin directly. |\n| `side?` | `ProductSkinSide` | Source skin side when all path points are on one semantic side. |\n| `pathPointCount` | `number` | Number of control path points supplied before interpolation. |\n| `width` | `number` | Final ribbon width in millimeters. |\n| `thickness` | `number` | Final ribbon solid thickness in millimeters. |\n| `offset` | `number` | Final normal offset from the source surface in millimeters. |\n| `samples` | `number` | Final sample count along the ribbon path. |\n| `widthSamples` | `number` | Final sample count across the ribbon width. |\n| `resolution` | `number` | NURBS tessellation resolution used for the lowered surface. |\n| `lowering` | `"nurbsSurface"` | Lowering primitive used for the ribbon shape. |\n| `expectedFidelity` | `ProductSkinRepresentation` | Expected fidelity inherited from the source skin/ref sampling mode. |\n| `clampedUCount` | `number` | Number of generated width samples clamped to the valid side span. |\n| `maxUClampDistance` | `number` | Largest absolute u-distance lost to side-span clamping. |\n| `warnings` | `string[]` | Non-fatal sampling and lowering warnings. |\n\n**`ProductSkinRepresentation`** — Reported lowering mode for ProductSkin and conformal feature diagnostics.\n\n`"exact" | "sampled" | "mixed" | "fallback"`\n\n#### `diagnostics()` — Return diagnostics from the most recent build, if this builder has been built.\n\n```ts\ndiagnostics(): ProductRibbonDiagnostics | undefined\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`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**`ProductProfileKind`**\n\n`"oval" | "roundedRect" | "circle" | "superEllipse" | "custom"`\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`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#### `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`ProductAttachOptions`: `{ offset?: number, inset?: number }`\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`ProductRailSpec`: `{ kind: ProductRailKind, points: Vec3[], degree?: number, name?: string }`\n\n**`ProductRailKind`**\n\n`"bezier" | "nurbs" | "polyline"`\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`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#### `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---\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- `CornerY(options: BlendCornerYOptions): Shape` — Current implementation uses continuity-controlled edge fillets on solid edges. It does not yet provide a dedicated open-sheet or multi-patch Y-corner solver. Follow progress: https://github.com/KoStard/forgecad-private/issues/162\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---\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`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`\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; }>; 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; }>; 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`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`\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- [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---\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 on the `lib` namespace exposed to `.forge.js` scripts. The catalog includes:\n\n**Fasteners:** `bolt`, `nut`, `washer`, `fastenerSet`, `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`, `bevelGear`, `faceGear`, `sideGear`, `ringGear`, `rackGear`, `gearPair`, `bevelGearPair`, `faceGearPair`, `sideGearPair`\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\nExtend this by adding new entries here and registering the corresponding runner binding in `runner.ts`. Sizes 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- `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). ```js assembly.addJointCoupling("B_spin", { terms: [{ joint: "A_spin", ratio: lib.gearRatio(12, 24) }], // -0.5 }); ``` 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. ```js // Pinion spin driven by rack slide: assembly.addJointCoupling("pinion_spin", { terms: [{ joint: "rack_slide", ratio: lib.rackRatio(1.5, 12) }], // ~6.37 deg/mm }); ```\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. ```js const bolts = lib.boltPattern({ size: \'M5\', positions: [[20, 15], [-20, 15], [20, -15], [-20, -15]], }); 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\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 explanatory text that helps a viewer understand the model. It does not create sketches, meshes, B-rep topology, exported text, or face labels, so it stays off the OCCT path. Use [`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 `forgecad render 3d model.forge.js --view hero`.\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| `diagnostics?` | `SceneJourneyDiagnostic[]` | Whole-journey diagnostics, including unresolved startsAt and step target diagnostics. |\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| `diagnostics?` | `SceneJourneyDiagnostic[]` | Resolution diagnostics for this step. |\n\n`SceneJourneyDiagnostic`: `{ level: SceneJourneyDiagnosticLevel, message: string, stepId?: string, suggestions?: string[] }`\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`ViewConfigOptions`: `{ jointOverlay?: JointOverlayViewConfigOptions }`\n\n**`JointOverlayViewConfigOptions`**: `enabled?: boolean`, `axisColor?: string`, `axisCoreColor?: string`, `arcColor?: string`, `zeroColor?: string`, `arcVisualLimitDeg?: number`, `axisLengthScale?: number`, `axisLengthMin?: number`, `axisLineRadiusScale?: number`, `axisLineRadiusMin?: number`, `axisLineRadiusMax?: number`, `spokeLineRadiusScale?: number`, `spokeLineRadiusMin?: number`, `spokeLineRadiusMax?: number`, `arcLineRadiusScale?: number`, `arcLineRadiusMin?: number`, `arcLineRadiusMax?: number`, `axisDotRadiusScale?: number`, `axisDotRadiusMin?: number`, `axisArrowRadiusScale?: number`, `axisArrowRadiusMin?: number`, `axisArrowLengthScale?: number`, `axisArrowLengthMin?: number`, `axisArrowOffsetFactor?: number`, `arcRadiusScale?: number`, `arcRadiusMin?: number`, `arcDotRadiusScale?: number`, `arcDotRadiusMin?: number`, `arcArrowRadiusScale?: number`, `arcArrowRadiusMin?: number`, `arcArrowLengthScale?: number`, `arcArrowLengthMin?: number`, `arcArrowOffsetFactor?: number`, `arcStepDeg?: number`, `arcMinSteps?: number`, `arcTubeSegmentsMin?: number`, `arcTubeSegmentsFactor?: number`, `arcTubeRadialSegments?: number`\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` 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| `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- `start: [ number, number, number ]` — Start point\n- `end: [ number, number, number ]` — End point\n- `query?: EdgeQueryRef` — Compiler-owned edge query when available.\n- Also: `name: EdgeName`\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, part identity,\nphysical connected components, collisions, local 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\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. Keep bundles targeted to the current question so heavy\nanalyses 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.\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,mask,collisions\nforgecad render inspect model.forge.js --channels rgb,section,thickness\n```\n\nSupported channels are `rgb`, `depth`, `normals`, `mask`, `connectivity`,\n`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`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 overlap edge = bbox interiors overlap\ntouching edge = bbox contact gap <= 0.05 model units\ncomponent = transitive closure over overlap/touching 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 is a fast bbox-neighborhood graph. Use the `collisions` channel\nwhen you need exact positive-volume boolean overlap evidence.\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\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, complete component gap edge list, 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, warnings, render style, and each collision finding\'s `groupIndex`,\n`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\nsamples visible mesh triangles, casts through the object along each triangle\nnormal, and colors the surface by the first opposite-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 sampled triangles 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\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 intentionally simple and may over-connect concave shapes whose\n bounding boxes touch while surfaces do not. 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#### `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#### `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 (e.g. basketWeave)\nshape.displace(sdf.basketWeave({ threads: 16, spacing: 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\n```js\n// Surface-following basket weave — auto-detects sphere UV\nsdf.sphere(27).shell(3)\n .surfaceDisplace(sdf.basketWeave({ spacing: 3, depth: 0.8 }))\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- `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- `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
105
  const AI_USAGE_DOC = "/docs/ai-usage";
105
106
  function copyText(text) {
106
107
  var _a;
@@ -971,7 +972,7 @@ function Ve({ defaultValue: e, defaultLanguage: r, defaultPath: n, value: t, lan
971
972
  var fe = Ve;
972
973
  var de = reactExports.memo(fe);
973
974
  var Ft = de;
974
- const FORGE_TYPES = '// AUTO-GENERATED — do not edit by hand.\n// Regenerate: npm run gen:types (source: src/forge/forge-public-api.ts)\n// External type stubs (opaque — not user-facing)\ntype opentype$1 = unknown;\ntype Manifold = unknown;\ntype ManifoldToplevel = unknown;\n\n// Generated by dts-bundle-generator v9.5.1\n\ntype Vec3 = [\n number,\n number,\n number\n];\ntype Mat4 = [\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number\n];\ntype TransformInput = Transform | Mat4;\ntype RotateAroundToMode = "plane" | "line";\ninterface RotateAroundToOptions {\n mode?: RotateAroundToMode;\n}\ndeclare class Transform {\n private readonly m;\n private constructor();\n /** Return the identity transform. */\n static identity(): Transform;\n /** Wrap an existing `Transform` or raw 4x4 matrix as a `Transform`. */\n static from(input: TransformInput): Transform;\n /** Create a translation transform. */\n static translation(x: number, y: number, z: number): Transform;\n /** Create a uniform or per-axis scale transform. */\n static scale(v: number | Vec3): Transform;\n /** Create a rotation around an arbitrary axis, optionally about a pivot. */\n static rotationAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform;\n /** Solve the rotation needed to move one point onto a target line or plane. */\n static rotateAroundTo(axis: Vec3, pivot: Vec3, movingPoint: Vec3, targetPoint: Vec3, options?: RotateAroundToOptions): Transform;\n /** Compose transforms in chain order: `a.mul(b)` applies `a`, then `b`. */\n mul(other: TransformInput): Transform;\n /** Translate after the current transform. */\n translate(x: number, y: number, z: number): Transform;\n /** Rotate after the current transform. */\n rotateAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform;\n /** Scale after the current transform. */\n scale(v: number | Vec3): Transform;\n /** Return the inverse transform. */\n inverse(): Transform;\n /** Transform a point using homogeneous coordinates. */\n point(p: Vec3): Vec3;\n /** Transform a direction vector without translation. */\n vector(v: Vec3): Vec3;\n /** Return the transform as a raw 4x4 matrix array. */\n toArray(): Mat4;\n}\n/**\n * Compose transforms in chain order.\n * Equivalent to Transform.identity().mul(a).mul(b).mul(c)...\n */\ndeclare function composeChain(...steps: TransformInput[]): Transform;\ntype ConnectorGender = "male" | "female" | "neutral";\ninterface PortDef {\n origin: Vec3;\n axis: Vec3;\n up: Vec3;\n extent?: number;\n kind?: JointType;\n min?: number;\n max?: number;\n connectorType?: string;\n gender?: ConnectorGender;\n measurements?: Record<string, number | string>;\n}\ntype PortAlign = "middle" | "start" | "end";\ninterface PortInput {\n origin?: [\n number,\n number,\n number\n ];\n axis?: [\n number,\n number,\n number\n ];\n start?: [\n number,\n number,\n number\n ];\n end?: [\n number,\n number,\n number\n ];\n up?: [\n number,\n number,\n number\n ];\n kind?: JointType;\n min?: number;\n max?: number;\n}\ntype PortMap = Record<string, PortDef>;\ntype ConnectorGender$1 = "male" | "female" | "neutral";\ninterface ConnectorInput extends PortInput {\n connectorType?: string;\n gender?: ConnectorGender$1;\n measurements?: Record<string, number | string>;\n}\ntype ConnectorDef = PortDef;\ntype ConnectorMap = PortMap;\ninterface MatchToOptions {\n force?: boolean;\n angle?: number;\n distance?: number;\n}\n/**\n * Create a connector — a named attachment point on a shape.\n *\n * Overloads:\n * - `connector(geometry)` — bare connector (position + orientation only)\n * - `connector(type, geometry)` — typed connector for compatibility matching\n * - `connector(type, geometry, measurements)` — typed with measurement metadata\n */\ndeclare function connectorFactory(typeOrInput: string | PortInput, inputOrMeasurements?: PortInput | Record<string, number | string>, measurements?: Record<string, number | string>): ConnectorInput;\ndeclare namespace connectorFactory {\n var male: (typeOrInput: string | PortInput, inputOrMeasurements?: PortInput | Record<string, number | string>, measurements?: Record<string, number | string>) => ConnectorInput;\n var female: (typeOrInput: string | PortInput, inputOrMeasurements?: PortInput | Record<string, number | string>, measurements?: Record<string, number | string>) => ConnectorInput;\n var neutral: (typeOrInput: string | PortInput, inputOrMeasurements?: PortInput | Record<string, number | string>, measurements?: Record<string, number | string>) => ConnectorInput;\n}\ndeclare const ANCHOR3D_NAMES: readonly [\n "center",\n "front",\n "back",\n "left",\n "right",\n "top",\n "bottom",\n "front-left",\n "front-right",\n "back-left",\n "back-right",\n "top-front",\n "top-back",\n "top-left",\n "top-right",\n "bottom-front",\n "bottom-back",\n "bottom-left",\n "bottom-right",\n "top-front-left",\n "top-front-right",\n "top-back-left",\n "top-back-right",\n "bottom-front-left",\n "bottom-front-right",\n "bottom-back-left",\n "bottom-back-right"\n];\ntype Anchor3D = (typeof ANCHOR3D_NAMES)[number];\ntype SketchFace3D = "front" | "back" | "left" | "right" | "top" | "bottom";\ninterface ShapeQueryOwner {\n id: string;\n operation: string;\n}\ntype TopologyRewriteQueryOutcome = "preserved" | "split" | "merged";\ninterface CanonicalFaceQueryRef {\n kind: "canonical-face";\n face: SketchFace3D;\n owner?: ShapeQueryOwner;\n}\ninterface TrackedFaceQueryRef {\n kind: "tracked-face";\n faceName: string;\n owner?: ShapeQueryOwner;\n}\ninterface DirectFaceQueryRef {\n kind: "face-ref";\n faceName?: string;\n owner?: ShapeQueryOwner;\n}\ninterface PropagatedFaceQueryRef {\n kind: "propagated-face";\n rewriteId: string;\n outcome: TopologyRewriteQueryOutcome;\n source: FaceQueryRef;\n owner?: ShapeQueryOwner;\n}\ninterface CreatedFaceQueryRef {\n kind: "created-face";\n rewriteId: string;\n operation: string;\n slot: string;\n owner?: ShapeQueryOwner;\n}\ntype FaceQueryRef = CanonicalFaceQueryRef | TrackedFaceQueryRef | DirectFaceQueryRef | PropagatedFaceQueryRef | CreatedFaceQueryRef;\ntype EdgeQuerySelector = "edge" | "start" | "end" | "midpoint";\ninterface TrackedEdgeQueryRef {\n kind: "tracked-edge";\n edgeName: string;\n selector: EdgeQuerySelector;\n owner?: ShapeQueryOwner;\n}\ninterface DirectEdgeQueryRef {\n kind: "edge-ref";\n edgeName?: string;\n selector: EdgeQuerySelector;\n owner?: ShapeQueryOwner;\n}\ninterface PropagatedEdgeQueryRef {\n kind: "propagated-edge";\n rewriteId: string;\n outcome: TopologyRewriteQueryOutcome;\n source: EdgeQueryRef;\n selector: EdgeQuerySelector;\n owner?: ShapeQueryOwner;\n}\ninterface CreatedEdgeQueryRef {\n kind: "created-edge";\n rewriteId: string;\n operation: string;\n slot: string;\n selector: EdgeQuerySelector;\n owner?: ShapeQueryOwner;\n}\ntype EdgeQueryRef = TrackedEdgeQueryRef | DirectEdgeQueryRef | PropagatedEdgeQueryRef | CreatedEdgeQueryRef;\ntype FaceDescendantSemantic = "face" | "region" | "set";\ninterface FaceDescendantMetadata {\n kind: "single" | "face-set";\n semantic: FaceDescendantSemantic;\n memberCount: number;\n memberNames: string[];\n coplanar: boolean;\n}\ninterface ConstraintTypeMap {\n /**\n * Forces two points to occupy the same position.\n *\n * This is the most fundamental connectivity constraint — use it to join\n * line endpoints, close a polygon, or snap a point to another point.\n * Contributes **2 equations** (one per axis).\n */\n coincident: {\n a: PointId;\n b: PointId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a line to be horizontal (parallel to the X axis).\n *\n * Both endpoints are moved to their average Y coordinate so the line\n * remains centered in place. Contributes **1 equation**: `b.y − a.y = 0`.\n */\n horizontal: {\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a line to be vertical (parallel to the Y axis).\n *\n * Both endpoints are moved to their average X coordinate so the line\n * remains centered in place. Contributes **1 equation**: `b.x − a.x = 0`.\n */\n vertical: {\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to be parallel.\n *\n * The direction of `b` is rotated to match `a`\'s direction (either\n * co-directional or anti-parallel — whichever is closer to the current\n * orientation). Line `a` is treated as the reference; only `b` is moved.\n * Contributes **1 equation**: `cross(unit_a, unit_b) = 0`.\n */\n parallel: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to be perpendicular (90° apart).\n *\n * The direction of `b` is rotated to be ±90° from `a`, choosing the\n * sign closest to the current orientation. Line `a` is the reference;\n * only `b` is moved. Contributes **1 equation**: `dot(unit_a, unit_b) = 0`.\n */\n perpendicular: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains tangency between a line and a circle, or between two circles.\n *\n * **Line–circle** (`line` + `circle`): the perpendicular distance from the\n * circle\'s center to the infinite line equals the circle\'s radius.\n *\n * **Circle–circle** (`a` + `b`): the two circles are externally tangent —\n * the distance between centers equals the sum of their radii.\n *\n * Exactly one mode must be active (provide either `line`+`circle` or `a`+`b`).\n * Contributes **1 equation**.\n */\n tangent: {\n line?: LineId;\n circle?: CircleId;\n a?: CircleId;\n b?: CircleId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to have the same length.\n *\n * Line `a`\'s length is used as the target; `b`\'s endpoints are scaled\n * symmetrically along `b`\'s current direction to match it.\n * Contributes **1 equation**: `|b| − |a| = 0`.\n */\n equal: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces points `a` and `b` to be mirror images of each other across\n * the infinite line through `axis`.\n *\n * When neither point is fixed, `b` is moved to the reflection of `a`.\n * When `b` is fixed, `a` is moved instead. Contributes **2 equations**\n * (one per axis): `b − reflect(a, axis) = [0, 0]`.\n */\n symmetric: {\n a: PointId;\n b: PointId;\n axis: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces two circles to share the same center point.\n *\n * The centers are merged to their average position (or snapped to the fixed\n * one if either is fixed). Contributes **2 equations**\n * (one per axis): `center_b − center_a = [0, 0]`.\n */\n concentric: {\n a: CircleId;\n b: CircleId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a point to lie on the infinite line passing through a line segment.\n *\n * The point is projected onto the line\'s infinite extension (not clamped to\n * the segment). Contributes **1 equation**: signed distance from the point\n * to the line = 0.\n */\n collinear: {\n point: PointId;\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Pins a point to an absolute position `(x, y)` in sketch space.\n *\n * Rust treats the point as externally pinned geometry. `equations: 0`\n * is intentional because the point\'s mobility is removed through the\n * `fixed` flag rather than by adding a residual row.\n */\n fixed: {\n point: PointId;\n x: number;\n y: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a point to sit at the exact midpoint of a line segment.\n *\n * Rust enforces this as two scalar equations, one per axis:\n * `point - (a + b) / 2 = [0, 0]`.\n */\n midpoint: {\n point: PointId;\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a point to lie exactly on the circumference of a circle.\n *\n * The point is moved radially so its distance from the center equals the\n * radius. Contributes **1 equation**: `|point − center| − radius = 0`.\n */\n pointOnCircle: {\n point: PointId;\n circle: CircleId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a point to lie on a **bounded** line segment — not merely the\n * infinite extension of the line.\n *\n * The point is projected onto the segment and the parameter `t` is clamped\n * to `[0, 1]`. When `t` is already inside that range the behaviour is\n * identical to `collinear`. When the point would project outside the\n * segment it is snapped to the nearest endpoint instead.\n *\n * Contributes **1 equation** (like `collinear`): the point can still slide\n * along the segment, giving it one remaining degree of freedom.\n */\n pointOnLine: {\n point: PointId;\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the Euclidean distance between two points to `value`.\n *\n * Points are moved symmetrically along the current direction vector so the\n * center of the pair stays fixed. Contributes **1 equation**:\n * `|b − a| − value = 0`.\n */\n distance: {\n a: PointId;\n b: PointId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the length of a line segment to `value`.\n *\n * Endpoints are scaled symmetrically about the line\'s midpoint while\n * preserving its direction. Contributes **1 equation**:\n * `|b − a| − value = 0`.\n */\n length: {\n line: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the angle from line `a` to line `b` to `value` degrees (CCW).\n * Line `a` is the reference; only `b` is rotated. Contributes **1 equation**.\n */\n angle: {\n a: LineId;\n b: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the radius of a circle to `value`.\n *\n * Has no effect if the circle\'s `fixedRadius` flag is set.\n * Contributes **1 equation**: `radius − value = 0`.\n */\n radius: {\n circle: CircleId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the diameter of a circle to `value` (i.e. `radius = value / 2`).\n *\n * Has no effect if the circle\'s `fixedRadius` flag is set.\n * Contributes **1 equation**: `radius − value / 2 = 0`.\n */\n diameter: {\n circle: CircleId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the signed horizontal distance from point `a` to point `b` to `value`.\n *\n * The constraint is directional: `b.x − a.x = value`. A positive value places\n * `b` to the right of `a`; negative places it to the left.\n * Contributes **1 equation**.\n */\n hDistance: {\n a: PointId;\n b: PointId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the signed vertical distance from point `a` to point `b` to `value`.\n *\n * The constraint is directional: `b.y − a.y = value`. A positive value places\n * `b` above `a`; negative places it below.\n * Contributes **1 equation**.\n */\n vDistance: {\n a: PointId;\n b: PointId;\n value: number;\n };\n}\ninterface HighlightDef {\n /** Entity ID to highlight (edge, point, surface, circle, arc). */\n entityId: string;\n /** Override color (CSS color string). Default: \'#ff00ff\' (magenta). */\n color?: string;\n /** Optional label to display near the entity. */\n label?: string;\n /** When true, animate opacity between 0.5 and 1.0 for attention. */\n pulse?: boolean;\n}\ninterface HighlightOptions {\n color?: string;\n label?: string;\n pulse?: boolean;\n /** Size hint for points (radius in mm) or planes (disc radius in mm). */\n size?: number;\n}\ndeclare const __brand: unique symbol;\ntype Brand<T, B extends string> = T & {\n readonly [__brand]: B;\n};\ntype PointId = Brand<string, "PointId">;\ntype LineId = Brand<string, "LineId">;\ntype CircleId = Brand<string, "CircleId">;\ntype ArcId = Brand<string, "ArcId">;\ntype BezierId = Brand<string, "BezierId">;\ntype ShapeId = Brand<string, "ShapeId">;\ntype GroupId = Brand<string, "GroupId">;\ninterface SketchArc {\n id: ArcId;\n /** Center point of the arc\'s circle. */\n center: PointId;\n /** Point on the arc where it begins. Must lie on the circle. */\n start: PointId;\n /** Point on the arc where it ends. Must lie on the circle. */\n end: PointId;\n /** Current solved radius — kept consistent with |center–start| and |center–end| by the solver. */\n radius: number;\n /** True → arc sweeps clockwise from start to end; false → counter-clockwise. */\n clockwise: boolean;\n /** When true, the solver treats radius as a constant (no variable, no perturbation). */\n fixedRadius: boolean;\n construction: boolean;\n /** Optional human-readable name for display. */\n name?: string;\n}\ninterface SketchShape {\n id: ShapeId;\n /** Ordered list of line IDs forming a closed polygon. */\n lines: LineId[];\n}\ninterface SketchGroupLocalPoint {\n id: PointId;\n lx: number;\n ly: number;\n}\ninterface SketchGroup {\n id: GroupId;\n /** World position of the group\'s local origin. */\n x: number;\n y: number;\n /** Rotation angle (radians) of the group frame. */\n theta: number;\n /** When true, all 3 DOF are frozen. */\n fixed: boolean;\n /** When true, θ is frozen — only translation DOF remain. */\n fixedRotation: boolean;\n /** Points in local coordinates. */\n points: SketchGroupLocalPoint[];\n /** Lines connecting local points (using their global IDs). */\n lines: {\n id: LineId;\n a: PointId;\n b: PointId;\n }[];\n}\ninterface SketchPoint {\n id: PointId;\n x: number;\n y: number;\n fixed: boolean;\n}\ninterface SketchLine {\n id: LineId;\n a: PointId;\n b: PointId;\n construction: boolean;\n /** Optional human-readable name for display (e.g. "top", "bottom"). */\n name?: string;\n}\ninterface SketchCircle {\n id: CircleId;\n center: PointId;\n radius: number;\n construction: boolean;\n fixedRadius: boolean;\n segments: number;\n /** Optional human-readable name for display. */\n name?: string;\n}\ninterface SketchBezier {\n id: BezierId;\n /** First control point (start of curve). */\n p0: PointId;\n /** Second control point (controls tangent at start). */\n p1: PointId;\n /** Third control point (controls tangent at end). */\n p2: PointId;\n /** Fourth control point (end of curve). */\n p3: PointId;\n construction: boolean;\n /** Optional human-readable name for display. */\n name?: string;\n}\ntype ProfileSegment = {\n kind: "line";\n line: LineId;\n label?: string;\n} | {\n kind: "arc";\n arc: ArcId;\n label?: string;\n} | {\n kind: "bezier";\n bezier: BezierId;\n label?: string;\n};\ntype SketchLoop = {\n type: "poly";\n points: PointId[];\n} | {\n type: "circle";\n circle: CircleId;\n}\n/** Mixed profile of line and arc segments forming a closed loop. */\n | {\n type: "profile";\n segments: ProfileSegment[];\n};\ntype AnnotationElement = \n/** Symbol placed at a specific position (geometric constraints like parallel, equal, fixed). */\n{\n kind: "symbol";\n position: [\n number,\n number\n ];\n symbol: ConstraintSymbol;\n rotation?: number;\n}\n/** Dimension line with extension lines (length, distance). */\n | {\n kind: "dimension";\n from: [\n number,\n number\n ];\n to: [\n number,\n number\n ];\n offset: number;\n value: string;\n}\n/** Angle arc between two directions (angle, absoluteAngle). */\n | {\n kind: "angle-arc";\n center: [\n number,\n number\n ];\n startAngle: number;\n endAngle: number;\n radius: number;\n value: string;\n}\n/** Fallback text label for constraints not yet migrated to annotations. */\n | {\n kind: "text";\n position: [\n number,\n number\n ];\n text: string;\n};\ntype ConstraintSymbol = "parallel" | "equal" | "perpendicular" | "horizontal" | "vertical" | "fixed" | "midpoint" | "coincident" | "collinear" | "tangent" | "concentric" | "ccw" | "symmetric";\ninterface ConstraintDisplay {\n id: string;\n type: string;\n label: string;\n /** Legacy position — used as fallback when annotations are not defined. */\n position: [\n number,\n number\n ];\n value?: number;\n isDimension: boolean;\n /** True when the solver failed to satisfy this constraint (genuinely conflicting geometry). */\n isConflicting: boolean;\n /** True when this constraint is mathematically redundant — it duplicates an equation already\n * provided by another constraint, making the DOF count negative even though the solver converges. */\n isRedundant: boolean;\n /** For rejected constraints: why the builder rejected it (maxError, constraint params, blame). */\n rejectionReason?: string;\n /** Entity IDs referenced by this constraint (points, lines, circles, etc.). */\n entityIds: string[];\n /** Per-equation residual error for this constraint (how far off it is). */\n residual: number;\n /** Annotation elements for this constraint. Empty array → use legacy text fallback. */\n annotations: AnnotationElement[];\n}\ninterface SurfaceDisplay {\n /** Zero-based index, largest-first by area. */\n index: number;\n /** Region area in mm². */\n area: number;\n /** Centroid of the region polygon. */\n centroid: [\n number,\n number\n ];\n /** Axis-aligned bounding box. */\n bounds: {\n min: [\n number,\n number\n ];\n max: [\n number,\n number\n ];\n };\n /** A point guaranteed to be inside the region — usable as seed for detectArrangementRegion(). */\n seed: [\n number,\n number\n ];\n /** Polygon vertices (CCW winding) for rendering the region fill. */\n polygon: [\n number,\n number\n ][];\n}\ninterface SketchConstraintMeta {\n status: "under" | "fully" | "over" | "over-redundant";\n /** Net degrees of freedom: positive = under-constrained, 0 = fully, negative = over-constrained. */\n dof: number;\n maxError: number;\n constraints: ConstraintDisplay[];\n rejected: ConstraintDisplay[];\n /** Detected surfaces from line arrangement (DCEL face detection). Empty if no closed regions. */\n surfaces: SurfaceDisplay[];\n construction: {\n lines: {\n id: string;\n a: [\n number,\n number\n ];\n b: [\n number,\n number\n ];\n }[];\n circles: {\n id: string;\n center: [\n number,\n number\n ];\n radius: number;\n }[];\n arcs: {\n id: string;\n center: [\n number,\n number\n ];\n start: [\n number,\n number\n ];\n end: [\n number,\n number\n ];\n radius: number;\n clockwise: boolean;\n }[];\n };\n /** Non-construction geometry edges rendered as solid wireframe overlay. */\n edges: {\n lines: {\n id: string;\n name?: string;\n a: [\n number,\n number\n ];\n b: [\n number,\n number\n ];\n }[];\n circles: {\n id: string;\n name?: string;\n center: [\n number,\n number\n ];\n radius: number;\n }[];\n arcs: {\n id: string;\n name?: string;\n center: [\n number,\n number\n ];\n start: [\n number,\n number\n ];\n end: [\n number,\n number\n ];\n radius: number;\n clockwise: boolean;\n }[];\n beziers: {\n id: string;\n name?: string;\n points: [\n number,\n number\n ][];\n }[];\n points: {\n id: string;\n pos: [\n number,\n number\n ];\n }[];\n };\n /** True when the solver hit its time budget before fully converging. */\n timedOut?: boolean;\n /** Opt-in solver debug artifacts returned by the Rust planner. */\n debug?: SolverDebugArtifacts;\n /** Programmatic debug highlights from user code. */\n highlights?: HighlightDef[];\n}\ninterface ConstraintDefinition {\n points: SketchPoint[];\n lines: SketchLine[];\n circles: SketchCircle[];\n arcs: SketchArc[];\n beziers: SketchBezier[];\n shapes: SketchShape[];\n /** Rigid-body groups — the solver sees 3 DOF per group instead of 2N per point. */\n groups: SketchGroup[];\n loops: SketchLoop[];\n constraints: SketchConstraint[];\n rejectedConstraints: SketchConstraint[];\n /** Maps rejected constraint ID → human-readable reason. Populated by the builder. */\n rejectionReasons?: Map<string, string>;\n}\ninterface SolveOptions {\n /** Maximum number of LM outer iterations per restart. */\n iterations?: number;\n /** Infinity-norm residual tolerance for declaring convergence. */\n tolerance?: number;\n /** Number of deterministic restart seeds used by the global solver. */\n restarts?: number;\n /** Optional projector iterations used only for initialisation, not as the main solver. */\n warmStartIterations?: number;\n /** Maximum LM step length in scaled variable space. Larger = bolder, smaller = safer. */\n maxScaledStep?: number;\n /** Skip redundancy detection (safe when topology is unchanged and previous DOF >= 0). */\n skipRedundancyCheck?: boolean;\n /** Run the targeted presolve hook for this constraint before the main solve. */\n presolveConstraintId?: string;\n /** When set and the first solve exceeds tolerance*5, retry with this many restarts. */\n fallbackRestarts?: number;\n /** Add constraints progressively with short LM solves, all in one WASM call. */\n progressive?: boolean;\n /** Wall-clock time budget in ms for the entire solve. 0 = no limit. */\n timeBudgetMs?: number;\n /** Capture a readable constructive transcript in `constraintMeta.debug`. */\n debugConstructiveTranscript?: boolean;\n /** Capture SVG snapshots for constructive steps in `constraintMeta.debug`. */\n debugSvgSnapshots?: boolean;\n}\ninterface SolverConstraintResidual {\n id: string;\n residual: number;\n}\ninterface ConstructiveTranscriptStep {\n index: number;\n label: string;\n kind: string;\n summary: string;\n outcome: string;\n localError: number;\n topResiduals: SolverConstraintResidual[];\n branch?: string;\n snapshotLabel?: string;\n}\ninterface SolverSvgSnapshot {\n label: string;\n error: number;\n svg: string;\n}\ninterface SolverDebugArtifacts {\n constructiveTranscript: ConstructiveTranscriptStep[];\n svgSnapshots: SolverSvgSnapshot[];\n}\ninterface ConstraintTypeMap {\n}\ninterface ConstraintBuilderMethods {\n moveTo(x: number, y: number): this;\n lineTo(x: number, y: number): this;\n lineH(dx: number): this;\n lineV(dy: number): this;\n lineAngled(length: number, degrees: number): this;\n arcTo(x: number, y: number, radius: number, clockwise?: boolean): this;\n arcByCenter(centerId: PointId, startId: PointId, endId: PointId, clockwise?: boolean, name?: string): ArcId;\n bezier(p0: any, p1: any, p2: any, p3: any, name?: string): BezierId;\n bezierTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): this;\n blendTo(x: number, y: number, weight?: number): this;\n close(): this;\n addLoopCircle(center: PointId, radius: number, segments?: number): this;\n addLoop(points: any[]): this;\n addProfileLoop(segments: Array<{\n kind: "line";\n line: any;\n } | {\n kind: "arc";\n arc: any;\n } | {\n kind: "bezier";\n bezier: any;\n }>): this;\n horizontal(line: any): this;\n vertical(line: any): this;\n parallel(a: any, b: any): this;\n sameDirection(a: any, b: any): this;\n oppositeDirection(a: any, b: any): this;\n blockRotation(points: any[], axis?: "x" | "y"): this;\n perpendicular(a: any, b: any): this;\n tangent(a: any, b: any): this;\n equal(a: any, b: any): this;\n coincident(a: any, b: any): this;\n concentric(a: any, b: any): this;\n collinear(point: any, line: any): this;\n symmetric(a: any, b: any, axis: any): this;\n fix(point: any, x?: number, y?: number): this;\n midpoint(point: any, line: any): this;\n pointOnCircle(point: any, circle: any): this;\n pointOnLine(point: any, line: any): this;\n distance(a: any, b: any, value: number): this;\n length(line: any, value: number): this;\n angle(a: any, b: any, value: number): this;\n radius(circle: any, value: number): this;\n diameter(circle: any, value: number): this;\n hDistance(a: any, b: any, value: number): this;\n vDistance(a: any, b: any, value: number): this;\n pointLineDistance(point: any, line: any, value: number): this;\n lineDistance(a: any, b: any, value: number): this;\n absoluteAngle(line: any, value: number): this;\n equalRadius(a: any, b: any): this;\n arcLength(arc: any, value: number): this;\n lineTangentArc(line: any, arc: any, atStart: boolean): this;\n arcTangentArc(arcA: any, arcB: any, aAtStart?: boolean, bAtStart?: boolean): this;\n bezierTangentArc(bezier: any, arc: any, atBezierStart: boolean, atArcStart: boolean): this;\n smoothBlend(arc1: any, arc2: any, options?: {\n weight?: number;\n arc1End?: "start" | "end";\n arc2End?: "start" | "end";\n }): BezierId;\n shapeWidth(shape: any, value: number): this;\n shapeHeight(shape: any, value: number): this;\n shapeCentroidX(shape: any, value: number): this;\n shapeCentroidY(shape: any, value: number): this;\n shapeArea(shape: any, value: number): this;\n shapeEqualCentroid(a: any, b: any): this;\n angleBetween(a: any, b: any, value: number): this;\n ccw(...points: any[]): this;\n importPoint(pt: {\n x: number;\n y: number;\n }, fixed?: boolean): PointId;\n importLine(l: {\n start: {\n x: number;\n y: number;\n };\n end: {\n x: number;\n y: number;\n };\n }, fixed?: boolean): LineId;\n importRectangle(r: {\n vertices: [\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n }\n ];\n }, fixed?: boolean): {\n bottom: LineId;\n right: LineId;\n top: LineId;\n left: LineId;\n points: [\n PointId,\n PointId,\n PointId,\n PointId\n ];\n };\n referencePoint(x: number, y: number): PointId;\n referenceLine(x1: number, y1: number, x2: number, y2: number): LineId;\n referenceFrom(source: any, entityId: string): PointId | LineId | null;\n referenceAllFrom(source: any): {\n points: Map<string, PointId>;\n lines: Map<string, LineId>;\n };\n}\ntype SketchConstraint = {\n [K in keyof ConstraintTypeMap]: {\n id: string;\n type: K;\n } & ConstraintTypeMap[K];\n}[keyof ConstraintTypeMap];\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to be parallel **and** separated by a signed\n * perpendicular distance of `value`.\n *\n * The distance is measured from the midpoint of `a` to the midpoint of `b`\n * along `a`\'s left-normal direction. Positive values place `b` to the left\n * of `a` (when facing `a`\'s direction).\n *\n * This constraint combines two equations:\n * 1. `cross(unit_a, unit_b) = 0` — parallelism\n * 2. `perpDist(mid_b, line_a) − value = 0` — offset distance\n *\n * Contributes **2 equations**.\n */\n lineDistance: {\n a: LineId;\n b: LineId;\n value: number;\n };\n}\n/** Exported for backward-compatibility with forge-public-api.ts */\ntype LineDistanceConstraint = {\n id: string;\n type: "lineDistance";\n} & {\n a: LineId;\n b: LineId;\n value: number;\n};\ninterface ConstraintTypeMap {\n /**\n * Sets the angle of a line from the positive X axis to exactly `value` degrees.\n * The direction is enforced as-is (a→b). Contributes **1 equation**:\n * `normalizeAngle(angle − target) = 0`.\n */\n absoluteAngle: {\n line: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces two circles to have the same radius.\n *\n * Rust enforces one scalar equality: `radius_b - radius_a = 0`.\n */\n equalRadius: {\n a: CircleId;\n b: CircleId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the arc length of an arc to `value`.\n *\n * Arc length is defined as `radius × sweep`, where `sweep` is the angle\n * (in radians) from the start point to the end point in the arc\'s direction.\n * A zero-length sweep is treated as a full circle (2π).\n *\n * Rust enforces this as one scalar equation: `radius × sweep - value = 0`.\n */\n arcLength: {\n arc: ArcId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains a line to be tangent to an arc at the arc\'s start or end point.\n *\n * Tangency requires the line\'s direction to be perpendicular to the arc\'s\n * radius at the contact point. Set `atStart: true` to use the arc\'s start\n * point as the tangency contact; `false` uses the end point.\n *\n * Rust enforces tangency as one scalar orthogonality equation between the\n * line direction and the chosen radius direction.\n */\n lineTangentArc: {\n line: LineId;\n arc: ArcId;\n atStart: boolean;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the X coordinate of a polygon\'s arithmetic centroid to `value`.\n *\n * All non-fixed vertices are translated horizontally by the same amount so\n * that `mean(vertices.x) = value`. The shape\'s size, proportions, and Y\n * position are unaffected. Contributes **1 equation**.\n */\n shapeCentroidX: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the Y coordinate of a polygon\'s arithmetic centroid to `value`.\n *\n * All non-fixed vertices are translated vertically by the same amount so\n * that `mean(vertices.y) = value`. The shape\'s size, proportions, and X\n * position are unaffected. Contributes **1 equation**.\n */\n shapeCentroidY: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the axis-aligned bounding-box width of a polygon shape to `value`.\n *\n * All non-fixed vertices are scaled horizontally (`x` only) from the\n * bounding-box center: `pt.x = cx + (pt.x − cx) × (value / width)`.\n * The shape\'s height and Y position are unaffected.\n * Contributes **1 equation**.\n */\n shapeWidth: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the axis-aligned bounding-box height of a polygon shape to `value`.\n *\n * All non-fixed vertices are scaled vertically (`y` only) from the\n * bounding-box center: `pt.y = cy + (pt.y − cy) × (value / height)`.\n * The shape\'s width and X position are unaffected.\n * Contributes **1 equation**.\n */\n shapeHeight: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the enclosed area of a polygon shape to `value`.\n *\n * Area is computed via the shoelace formula on the ordered vertex list.\n * All non-fixed vertices are scaled uniformly from the polygon\'s arithmetic\n * centroid: `scale = sqrt(target / current)`, so the shape\'s proportions\n * and position are preserved. Contributes **1 equation**.\n */\n shapeArea: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces two shapes to share the same centroid.\n *\n * Translates the non-fixed vertices of each shape so that their arithmetic\n * centroids coincide at the mean of the two current centroids.\n * Contributes **2 equations**: `cx(a) − cx(b) = 0` and `cy(a) − cy(b) = 0`.\n */\n shapeEqualCentroid: {\n a: ShapeId;\n b: ShapeId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains the signed perpendicular distance from a point to an infinite line.\n *\n * Positive `value` places the point to the **left** of the line\n * (when facing the line\'s direction from `a` to `b`). Negative places it\n * to the right. Zero is equivalent to `collinear`.\n * Contributes **1 equation**: `perpDist(point, line) − value = 0`.\n */\n pointLineDistance: {\n point: PointId;\n line: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Enforces counter-clockwise winding order on a polygon defined by `points`.\n *\n * This resolves the discrete orientation ambiguity that arises when a polygon\'s\n * shape is fully determined but its mirror image also satisfies all constraints\n * (e.g. an equilateral triangle with a fixed vertex and side angle).\n *\n * Rust owns the actual branch handling and one-sided residual logic.\n * `equations: 0` is intentional: this constraint removes the mirror branch\n * without claiming an extra continuous degree of freedom.\n */\n ccw: {\n points: PointId[];\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the unsigned angle between lines `a` and `b` to `value` degrees.\n *\n * Unlike `angle` (which is directional — 90 ≠ −90), this constraint\n * accepts both orientations of `b`: whichever of `+value` or `+value+180°`\n * is closer to the current direction is chosen. Use this when you care\n * about the magnitude of the angle but not the sign (e.g. "these two lines\n * are 60° apart" without specifying which side).\n *\n * Contributes **1 equation**: `sin(angleB − angleA − target) = 0`.\n */\n angleBetween: {\n a: LineId;\n b: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to point in the **same direction** (co-directional).\n *\n * Unlike `parallel` (which allows either co-directional or anti-parallel),\n * this constraint pins the relative direction: `dot(unit_a, unit_b) > 0`\n * AND `cross(unit_a, unit_b) = 0`.\n *\n * Use this with `lineDistance` when the sign of the distance matters —\n * `sameDirection` guarantees the normals point the same way, so positive\n * distance means the same physical side for both lines.\n *\n * Rust uses one continuous parallelism equation plus orientation-aware\n * branch handling to keep `b` facing the same way as `a`.\n */\n sameDirection: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to point in **opposite directions** (anti-parallel).\n *\n * Unlike `parallel` (which allows either co-directional or anti-parallel),\n * this constraint pins the relative direction: `dot(unit_a, unit_b) < 0`\n * AND `cross(unit_a, unit_b) = 0`.\n *\n * Use this with `lineDistance` when you need two lines facing each other —\n * e.g. the top of one rect facing the bottom of another.\n *\n * Rust uses one continuous parallelism equation plus orientation-aware\n * branch handling to keep `b` facing opposite to `a`.\n */\n oppositeDirection: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Prevents 180° rotation of a polygon by ensuring the first edge\n * (p0 → p1) maintains its initial direction sign.\n *\n * For an axis-aligned rectangle [bl, br, tr, tl], this guarantees\n * `br.x > bl.x` — i.e. the "bottom" edge points rightward (+x).\n * Without this, a rect can satisfy CCW winding while being inside-out\n * (negative width AND negative height → positive area → CCW blind spot).\n *\n * Rust treats this as an orientation guard with `equations: 0`, so it\n * preserves the intended branch without claiming an extra continuous DOF.\n *\n * The constraint stores `axis: \'x\' | \'y\'` — which coordinate of the\n * first edge must increase. For rects, this is `\'x\'` (bottom goes right).\n */\n blockRotation: {\n points: PointId[];\n axis: "x" | "y";\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains two arcs to be tangent (G1 smooth) at a shared junction point.\n *\n * The radius vectors of both arcs at the junction must be collinear\n * (cross product of unit radii = 0), ensuring the tangent directions match.\n *\n * Use `coincident` separately to enforce the shared endpoint.\n * Contributes **1 equation**.\n */\n arcTangentArc: {\n arcA: ArcId;\n arcB: ArcId;\n /** Use arc A\'s start (true) or end (false) as the junction. */\n aAtStart: boolean;\n /** Use arc B\'s start (true) or end (false) as the junction. */\n bAtStart: boolean;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains a cubic Bezier curve to be tangent to an arc at one of their endpoints.\n *\n * The Bezier\'s tangent direction (tangent_control − tangent_base) must be\n * perpendicular to the arc\'s radius at the contact point.\n *\n * The builder resolves the Bezier entity to two point IDs:\n * - at bezier start: tangentBase=P0, tangentControl=P1\n * - at bezier end: tangentBase=P3, tangentControl=P2\n *\n * Contributes **1 equation**.\n */\n bezierTangentArc: {\n /** Point on the Bezier at the tangent end (P0 for start, P3 for end). */\n tangentBase: PointId;\n /** Control point defining tangent direction (P1 for start, P2 for end). */\n tangentControl: PointId;\n arc: ArcId;\n /** Use arc start (true) or end (false) as the contact point. */\n atArcStart: boolean;\n };\n}\ninterface ConstrainedSketchBuilder {\n /** Move the cursor to `(x, y)` and start a new profile loop. */\n moveTo(x: number, y: number): this;\n /** Draw a line from the current cursor to `(x, y)`. */\n lineTo(x: number, y: number): this;\n /** Draw a horizontal line of length `dx` from the current cursor. */\n lineH(dx: number): this;\n /** Draw a vertical line of length `dy` from the current cursor. */\n lineV(dy: number): this;\n /** Draw a line of the given `length` at `degrees` from +X. */\n lineAngled(length: number, degrees: number): this;\n /** Draw a circular arc from the current cursor to `(x, y)` with the given radius. */\n arcTo(x: number, y: number, radius: number, clockwise?: boolean): this;\n /** Create an arc from an explicit center point and endpoint IDs. */\n arcByCenter(centerId: PointId, startId: PointId, endId: PointId, clockwise?: boolean, name?: string, fixedRadius?: boolean): ArcId;\n /** Create a cubic Bezier curve from four control points. */\n bezier(p0: any, p1: any, p2: any, p3: any, name?: string): BezierId;\n /** Draw a cubic Bezier from the current cursor to `(x3, y3)`. */\n bezierTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): this;\n /** Draw a smooth Bezier tangent to the previous arc. */\n blendTo(x: number, y: number, weight?: number): this;\n /** Label the current path segment. */\n label(name: string): this;\n /** Close the current path and register the loop. */\n close(): this;\n /** Add a circle loop to the path. */\n addLoopCircle(center: PointId, radius: number, segments?: number): this;\n /** Add a closed polygon loop from point IDs. */\n addLoop(points: any[]): this;\n /** Add a profile loop from prebuilt line/arc/bezier segments. */\n addProfileLoop(segments: Array<{\n kind: "line";\n line: any;\n } | {\n kind: "arc";\n arc: any;\n } | {\n kind: "bezier";\n bezier: any;\n }>): this;\n}\ninterface ConstrainedSketchBuilder {\n /** Constrain a line to be horizontal (parallel to the X axis). */\n horizontal(line: any): this;\n /** Constrain a line to be vertical (parallel to the Y axis). */\n vertical(line: any): this;\n /** Constrain two lines to be parallel. */\n parallel(a: any, b: any): this;\n /** Constrain two lines to point in the same direction. */\n sameDirection(a: any, b: any): this;\n /** Constrain two lines to point in opposite directions. */\n oppositeDirection(a: any, b: any): this;\n /** Prevent 180° rotation of a polygon by anchoring its first edge. */\n blockRotation(points: any[], axis?: "x" | "y"): this;\n /** Constrain two lines to be perpendicular. */\n perpendicular(a: any, b: any): this;\n /** Constrain a line/circle or circle/circle tangency relationship. */\n tangent(a: any, b: any): this;\n /** Constrain two lines to have equal length. */\n equal(a: any, b: any): this;\n /** Constrain two points to coincide. */\n coincident(a: any, b: any): this;\n /** Constrain two circles to share a center. */\n concentric(a: any, b: any): this;\n /** Constrain a point to lie on the infinite extension of a line. */\n collinear(point: any, line: any): this;\n /** Constrain two points to be symmetric about an axis line. */\n symmetric(a: any, b: any, axis: any): this;\n /** Pin a point at a specific world location. */\n fix(point: any, x?: number, y?: number): this;\n /** Constrain a point to lie at the midpoint of a line. */\n midpoint(point: any, line: any): this;\n /** Constrain a point to lie on the perimeter of a circle. */\n pointOnCircle(point: any, circle: any): this;\n /** Constrain a point to lie on the bounded segment of a line. */\n pointOnLine(point: any, line: any): this;\n}\ninterface ConstrainedSketchBuilder {\n /** Constrain the Euclidean distance between two points. */\n distance(a: any, b: any, value: number): this;\n /** Constrain the length of a line segment. */\n length(line: any, value: number): this;\n /** Constrain the signed angle from line `a` to line `b`. */\n angle(a: any, b: any, value: number): this;\n /** Constrain the radius of a circle. */\n radius(circle: any, value: number): this;\n /** Constrain the diameter of a circle. */\n diameter(circle: any, value: number): this;\n /** Constrain the horizontal distance between two points. */\n hDistance(a: any, b: any, value: number): this;\n /** Constrain the vertical distance between two points. */\n vDistance(a: any, b: any, value: number): this;\n /** Constrain the signed perpendicular distance from a point to a line. */\n pointLineDistance(point: any, line: any, value: number): this;\n /** Constrain the perpendicular offset distance between two lines. */\n lineDistance(a: any, b: any, value: number): this;\n /** Constrain the absolute angle of a line measured from +X. */\n absoluteAngle(line: any, value: number): this;\n /** Constrain two circles to have equal radii. */\n equalRadius(a: any, b: any): this;\n /** Constrain the arc length of an arc. */\n arcLength(arc: any, value: number): this;\n /** Constrain a line to be tangent to an arc at its start or end point. */\n lineTangentArc(line: any, arc: any, atStart: boolean): this;\n /** Constrain two arcs to be tangent at their shared junction point. */\n arcTangentArc(arcA: any, arcB: any, aAtStart?: boolean, bAtStart?: boolean): this;\n /** Constrain a Bezier to be tangent to an arc at one endpoint. */\n bezierTangentArc(bezier: any, arc: any, atBezierStart: boolean, atArcStart: boolean): this;\n /** Create a Bezier blend between two arcs. */\n smoothBlend(arc1: any, arc2: any, options?: {\n weight?: number;\n arc1End?: "start" | "end";\n arc2End?: "start" | "end";\n }): BezierId;\n /** Constrain a shape\'s width. */\n shapeWidth(shape: any, value: number): this;\n /** Constrain a shape\'s height. */\n shapeHeight(shape: any, value: number): this;\n /** Constrain a shape\'s centroid X position. */\n shapeCentroidX(shape: any, value: number): this;\n /** Constrain a shape\'s centroid Y position. */\n shapeCentroidY(shape: any, value: number): this;\n /** Constrain a shape\'s area. */\n shapeArea(shape: any, value: number): this;\n /** Constrain two shapes to have the same centroid. */\n shapeEqualCentroid(a: any, b: any): this;\n /** Constrain the unsigned angle between two lines. */\n angleBetween(a: any, b: any, value: number): this;\n /** Constrain all given points to be in counter-clockwise order. */\n ccw(...points: any[]): this;\n /**\n * Constrain the horizontal (X-axis) offset between two lines.\n * Uses the start-point of each line to measure horizontal distance.\n * `value` is the signed distance: b.startPt.x − a.startPt.x = value.\n */\n offsetX(a: any, b: any, value: number): this;\n /**\n * Constrain the vertical (Y-axis) offset between two lines.\n * Uses the start-point of each line to measure vertical distance.\n * `value` is the signed distance: b.startPt.y − a.startPt.y = value.\n */\n offsetY(a: any, b: any, value: number): this;\n}\ndeclare const SHAPE_BACKEND_MARKER: unique symbol;\ninterface ShapeRuntimeBounds {\n readonly min: [\n number,\n number,\n number\n ];\n readonly max: [\n number,\n number,\n number\n ];\n}\ninterface ShapeRuntimeMesh {\n readonly numProp: number;\n readonly numTri: number;\n readonly triVerts: Uint32Array;\n readonly vertProperties: Float32Array;\n readonly numVert?: number;\n readonly mergeFromVert?: Uint32Array;\n readonly mergeToVert?: Uint32Array;\n readonly runIndex?: Uint32Array;\n readonly runOriginalID?: Uint32Array;\n readonly runTransform?: Float32Array;\n readonly faceID?: Uint32Array | Int32Array;\n readonly halfedgeTangent?: Float32Array;\n}\ntype ShapeRuntimeCrossSection = any;\ninterface ShapeBackend {\n readonly [SHAPE_BACKEND_MARKER]: true;\n clone(): ShapeBackend;\n translate(x: number, y: number, z: number): ShapeBackend;\n rotate(x: number, y: number, z: number): ShapeBackend;\n transform(m: Mat4): ShapeBackend;\n scale(v: number | [\n number,\n number,\n number\n ]): ShapeBackend;\n mirror(normal: [\n number,\n number,\n number\n ]): ShapeBackend;\n split(other: ShapeBackend): [\n ShapeBackend,\n ShapeBackend\n ];\n splitByPlane(normal: [\n number,\n number,\n number\n ], originOffset: number): [\n ShapeBackend,\n ShapeBackend\n ];\n trimByPlane(normal: [\n number,\n number,\n number\n ], originOffset: number): ShapeBackend;\n boundingBox(): ShapeRuntimeBounds;\n volume(): number;\n surfaceArea(): number;\n isEmpty(): boolean;\n /** Number of disconnected solid bodies in this shape. */\n numBodies(): number;\n numTri(): number;\n getMesh(): ShapeRuntimeMesh;\n slice(offset: number): ShapeRuntimeCrossSection;\n project(): ShapeRuntimeCrossSection;\n dispose?(): void;\n}\ndeclare const PROFILE_BACKEND_MARKER: unique symbol;\ninterface ProfileBounds {\n min: [\n number,\n number\n ];\n max: [\n number,\n number\n ];\n}\ninterface ProfileBackend {\n readonly [PROFILE_BACKEND_MARKER]: true;\n area(): number;\n bounds(): ProfileBounds;\n isEmpty(): boolean;\n numVert(): number;\n toPolygons(): number[][][];\n translate(x: number, y: number): ProfileBackend;\n rotate(degrees: number): ProfileBackend;\n scale(v: number | [\n number,\n number\n ]): ProfileBackend;\n mirror(ax: [\n number,\n number\n ]): ProfileBackend;\n offset(delta: number, join: "Square" | "Round" | "Miter"): ProfileBackend;\n simplify(epsilon: number): ProfileBackend;\n subtract(other: ProfileBackend): ProfileBackend;\n extrude(height: number, divisions: number, twist: number, scaleTop?: [\n number,\n number\n ]): ShapeBackend;\n revolve(segments: number, degrees: number): ShapeBackend;\n}\ntype Anchor = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top" | "bottom" | "left" | "right";\ntype SketchOperandInput = Sketch | readonly Sketch[];\n/**\n * Immutable 2D profile for extrusion, revolve, and other operations.\n *\n * **Details**\n *\n * `Sketch` wraps Manifold\'s `CrossSection` with a chainable 2D API. Every method\n * returns a new `Sketch` — the original is never mutated. Colors, edge labels, and\n * placement data are preserved through all transforms and boolean operations.\n *\n * Supported operations:\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 *\n * Named anchor positions used by `attachTo()`:\n * `\'center\'` | `\'top-left\'` | `\'top-right\'` | `\'bottom-left\'` | `\'bottom-right\'`\n * | `\'top\'` | `\'bottom\'` | `\'left\'` | `\'right\'`\n */\ndeclare class Sketch {\n readonly cross: ProfileBackend;\n colorHex: string | undefined;\n constructor(cross: ProfileBackend, color?: string);\n /**\n * Set the display color of this sketch.\n *\n * **Details**\n *\n * Color is preserved through all transforms and boolean operations. Pass\n * `undefined` to clear the color.\n *\n * **Example**\n *\n * ```ts\n * circle2d(20).color(\'#ff0000\').extrude(5);\n * ```\n *\n * @param value - Hex color string (e.g. `\'#ff0000\'`) or `undefined` to clear\n * @returns A new sketch with the color set\n * @category Sketch Core\n */\n color(value: string | undefined): Sketch;\n /**\n * Create an explicit copy of this sketch for branching variants.\n *\n * **Details**\n *\n * Because all Sketch operations are immutable, `clone()` is rarely needed.\n * Use it when you want to assign the same sketch to multiple names and\n * continue modifying each independently without confusion.\n *\n * @returns A new Sketch with the same geometry and metadata\n * @see {@link duplicate} for an alias\n * @category Sketch Core\n */\n clone(): Sketch;\n /**\n * Alias for `clone()` — create an explicit copy handle for branching variants.\n *\n * @returns A new Sketch with the same geometry and metadata\n * @see {@link clone}\n * @category Sketch Core\n */\n duplicate(): Sketch;\n /**\n * Return the total filled area of the sketch.\n *\n * @returns Area in square model units\n * @category Sketch Core\n */\n area(): number;\n /**\n * Return the axis-aligned bounding box of the sketch.\n *\n * @returns `{ min: [x, y], max: [x, y] }`\n * @category Sketch Core\n */\n bounds(): ProfileBounds;\n /**\n * Return `true` if the sketch contains no filled area.\n *\n * @returns `true` when the sketch has no geometry\n * @category Sketch Core\n */\n isEmpty(): boolean;\n /**\n * Return the number of vertices in the polygon representation of the sketch contours.\n *\n * @returns Vertex count\n * @category Sketch Core\n */\n numVert(): number;\n /**\n * Return the sketch as a list of polygons matching its contour topology.\n *\n * Useful when you need raw polygon data for inspection or custom export.\n * @category Sketch Core\n */\n toPolygons(): number[][][];\n /**\n * Move the sketch by the given X and Y offset.\n *\n * @param x - Offset along X\n * @param y - Offset along Y (default 0)\n * @returns A translated sketch\n * @category Sketch Transforms\n */\n translate(x: number, y?: number): Sketch;\n /**\n * Rotate the sketch around its bounding-box center.\n *\n * @param degrees - Rotation angle in degrees (CCW positive)\n * @returns A rotated sketch\n * @see {@link rotateAround} to rotate around an arbitrary point\n * @category Sketch Transforms\n */\n rotate(degrees: number): Sketch;\n /**\n * Rotate the sketch around a specific pivot point.\n *\n * **Example**\n *\n * ```ts\n * rect(20, 20).rotateAround(45, [0, 0]);\n * ```\n *\n * @param degrees - Rotation angle in degrees (CCW positive)\n * @param pivot - 2D pivot point `[x, y]`\n * @returns A rotated sketch\n * @category Sketch Transforms\n */\n rotateAround(degrees: number, pivot: [\n number,\n number\n ]): Sketch;\n /**\n * Scale the sketch relative to its bounding-box center.\n *\n * **Details**\n *\n * Pass a single number for uniform scaling, or `[sx, sy]` for per-axis scaling.\n *\n * @param v - Scale factor — uniform `number` or per-axis `[sx, sy]`\n * @returns A scaled sketch\n * @see {@link scaleAround} to scale relative to an arbitrary point\n * @category Sketch Transforms\n */\n scale(v: number | [\n number,\n number\n ]): Sketch;\n /**\n * Mirror the sketch across a line through its bounding-box center.\n *\n * **Details**\n *\n * `normal` is the normal vector of the mirror line (not the line direction).\n * For example, `[1, 0]` mirrors across a vertical line (Y axis direction),\n * and `[0, 1]` mirrors across a horizontal line.\n *\n * @param normal - Normal of the mirror line as `[x, y]`\n * @returns A mirrored sketch\n * @see {@link mirrorThrough} to mirror across a line through an arbitrary point\n * @category Sketch Transforms\n */\n mirror(normal: [\n number,\n number\n ]): Sketch;\n /**\n * Scale the sketch relative to an arbitrary pivot point.\n *\n * @param pivot - 2D pivot point `[x, y]`\n * @param v - Scale factor — uniform `number` or per-axis `[sx, sy]`\n * @returns A scaled sketch\n * @category Sketch Transforms\n */\n scaleAround(pivot: [\n number,\n number\n ], v: number | [\n number,\n number\n ]): Sketch;\n /**\n * Mirror the sketch across a line defined by a point and a normal direction.\n *\n * @param point - A point on the mirror line\n * @param normal - Normal of the mirror line as `[x, y]`\n * @returns A mirrored sketch\n * @category Sketch Transforms\n */\n mirrorThrough(point: [\n number,\n number\n ], normal: [\n number,\n number\n ]): Sketch;\n /**\n * Add (union) one or more sketches to this sketch.\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `sketch.add(a, b)` or `sketch.add([a, b])`.\n * For combining many sketches at once, prefer the free function `union2d()` which\n * uses Manifold\'s batch operation and is faster than chaining.\n *\n * **Example**\n *\n * ```ts\n * circle2d(20).add(rect(10, 30)).extrude(5);\n * ```\n *\n * @returns A new sketch with all inputs unioned together\n * @see {@link union2d} for the batch free-function alternative\n * @category Sketch Booleans\n */\n add(...others: SketchOperandInput[]): Sketch;\n /**\n * Subtract one or more sketches from this sketch.\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `sketch.subtract(a, b)` or `sketch.subtract([a, b])`.\n * For subtracting many cutters at once, prefer the free function `difference2d()`.\n *\n * **Example**\n *\n * ```ts\n * rect(40, 40).subtract(circle2d(10)).extrude(5);\n * ```\n *\n * @returns A new sketch with all inputs subtracted\n * @see {@link difference2d} for the batch free-function alternative\n * @category Sketch Booleans\n */\n subtract(...others: SketchOperandInput[]): Sketch;\n /**\n * Intersect this sketch with one or more others (keep overlapping area only).\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `sketch.intersect(a, b)` or `sketch.intersect([a, b])`.\n * For intersecting many sketches, prefer the free function `intersection2d()`.\n *\n * @returns A new sketch containing only the area shared by all inputs\n * @see {@link intersection2d} for the batch free-function alternative\n * @category Sketch Booleans\n */\n intersect(...others: SketchOperandInput[]): Sketch;\n /**\n * Alias for `add()` — matches the free-function `union2d()` naming.\n *\n * @see {@link add}\n * @category Sketch Booleans\n */\n union(...others: SketchOperandInput[]): Sketch;\n /**\n * Alias for `subtract()` — matches the free-function `difference2d()` naming.\n *\n * @see {@link subtract}\n * @category Sketch Booleans\n */\n difference(...others: SketchOperandInput[]): Sketch;\n /**\n * Alias for `intersect()` — matches the free-function `intersection2d()` naming.\n *\n * @see {@link intersect}\n * @category Sketch Booleans\n */\n intersection(...others: SketchOperandInput[]): Sketch;\n /**\n * Inflate (positive delta) or deflate (negative delta) the sketch contour.\n *\n * **Details**\n *\n * Use `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 * **Example**\n *\n * ```ts\n * rect(40, 20).offset(3); // expand by 3\n * rect(40, 20).offset(-2).offset(2); // round all convex corners\n * ```\n *\n * @param delta - Offset distance (positive = expand, negative = shrink)\n * @param join - Corner join style: `\'Round\'` | `\'Square\'` | `\'Miter\'` (default `\'Round\'`)\n * @returns A new offset sketch\n * @category Sketch Operations\n */\n offset(delta: number, join?: "Square" | "Round" | "Miter"): Sketch;\n /**\n * Decompose this sketch into its distinct filled regions, sorted largest-first by area.\n *\n * **Details**\n *\n * A single sketch can contain several disconnected filled areas (e.g., two separate\n * rectangles, or a ring shape with a hole). This method enumerates all top-level\n * connected regions as independent `Sketch` objects, each with its own outer boundary\n * and associated holes.\n *\n * **Example**\n *\n * ```ts\n * const pair = union2d(rect(40, 40), rect(40, 40).translate(60, 0));\n * const [left, right] = pair.regions(); // largest first\n * left.extrude(5);\n * ```\n *\n * @returns Array of region sketches, sorted by area descending\n * @see {@link region} to pick one region by seed point\n * @category Sketch Regions\n */\n regions(): Sketch[];\n /**\n * Select the single filled region that contains the given 2D seed point.\n *\n * **Details**\n *\n * The seed must lie strictly inside the filled area — not on a boundary edge and\n * not inside a hole. Throws a descriptive error if the seed is outside all regions.\n * If unsure where regions are, use `.regions()` first — each result has `.bounds()`.\n *\n * **Example**\n *\n * ```ts\n * const donut = circle2d(50).subtract(circle2d(30));\n * donut.region([40, 0]).extrude(10); // seed at radius 40, inside the ring\n * ```\n *\n * @param seed - A 2D point `[x, y]` strictly inside the desired region\n * @returns The sketch region containing the seed point\n * @see {@link regions} to enumerate all regions\n * @category Sketch Regions\n */\n region(seed: [\n number,\n number\n ]): Sketch;\n /** Extrude this 2D sketch along Z to create a 3D solid. Supports twist and scale tapering. */\n extrude(height: number, opts?: {\n twist?: number;\n divisions?: number;\n scaleTop?: number | [\n number,\n number\n ];\n }): Shape;\n /** Revolve this 2D sketch around the Y axis to create a 3D solid of revolution. */\n revolve(degrees?: number, segments?: number): Shape;\n /**\n * Position this sketch relative to another using named anchor points.\n *\n * **Details**\n *\n * Computes the translation needed to align `selfAnchor` on this sketch with\n * `targetAnchor` on the target sketch, then applies an optional pixel-exact offset.\n *\n * Anchor positions: `\'center\'` | `\'top-left\'` | `\'top-right\'` | `\'bottom-left\'`\n * | `\'bottom-right\'` | `\'top\'` | `\'bottom\'` | `\'left\'` | `\'right\'`\n *\n * **Example**\n *\n * ```ts\n * const arm = rect(4, 70).attachTo(plate, \'bottom-left\', \'top-left\');\n * const shifted = rect(4, 70).attachTo(plate, \'bottom-left\', \'top-left\', [5, 0]);\n * ```\n *\n * @param target - The sketch to attach to\n * @param targetAnchor - Named anchor point on the target sketch\n * @param selfAnchor - Named anchor point on this sketch to align (default `\'center\'`)\n * @param offset - Additional `[dx, dy]` offset applied after alignment\n * @returns A translated sketch\n * @category Sketch Transforms\n */\n attachTo(target: Sketch, targetAnchor: Anchor, selfAnchor?: Anchor, offset?: [\n number,\n number\n ]): Sketch;\n /**\n * Place this sketch on a face or planar target in 3D space.\n *\n * Use this when a 2D profile should be oriented onto a 3D face before\n * extrusion or other downstream operations.\n *\n * @param parentOrFace - Parent shape, face reference, or shape-like target.\n * @param faceOrOpts - Face selector or placement options.\n * @param opts - Additional placement options when a face selector is provided separately.\n * @returns A sketch positioned on the target face.\n * @category Sketch Transforms\n */\n onFace(parentOrFace: Shape | {\n toShape(): Shape;\n } | {\n _bbox(): {\n min: number[];\n max: number[];\n };\n } | FaceRef, faceOrOpts?: "front" | "back" | "left" | "right" | "top" | "bottom" | string | FaceRef | {\n u?: number;\n v?: number;\n protrude?: number;\n selfAnchor?: Anchor;\n }, opts?: {\n u?: number;\n v?: number;\n protrude?: number;\n selfAnchor?: Anchor;\n }): Sketch;\n /** Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch. */\n labelEdge(name: string): Sketch;\n /**\n * Label edges in winding order, or by named map for rect.\n *\n * Positional: `labelEdges(\'bottom\', \'right\', \'top\', \'left\')` — one per edge, `null` to skip.\n * Named (rect only): `labelEdges({ bottom: \'floor\', top: \'ceiling\' })`.\n * Returns a new sketch.\n */\n labelEdges(...args: (string | null)[] | [\n Record<string, string>\n ]): Sketch;\n /** List current edge label names. */\n edgeLabels(): string[];\n /** Prefix all edge labels. Returns a new sketch with prefixed labels. */\n prefixLabels(prefix: string): Sketch;\n /** Rename a single edge label. Returns a new sketch. */\n renameLabel(from: string, to: string): Sketch;\n /** Remove specific labels. Returns a new sketch. */\n dropLabels(...names: string[]): Sketch;\n /** Remove all labels. Returns a new sketch. */\n dropAllLabels(): Sketch;\n}\ndeclare class ConstraintSketch extends Sketch {\n readonly constraintMeta: SketchConstraintMeta;\n readonly definition: ConstraintDefinition;\n constructor(cross: Sketch["cross"], constraintMeta: SketchConstraintMeta, definition: ConstraintDefinition);\n /**\n * Enumerate all bounded regions formed by the line arrangement of this sketch.\n * Construction lines are excluded. Regions are returned largest-first by area.\n */\n detectArrangement(): Sketch[];\n /**\n * Select the single arrangement region that contains the given seed point.\n * Throws if no region contains the seed.\n */\n detectArrangementRegion(seed: [\n number,\n number\n ]): Sketch;\n /**\n * Re-solve the sketch after changing the value of one existing constraint.\n *\n * Use this for interactive dimension edits without rebuilding the whole sketch graph.\n * It attempts a warm-started solve first, then falls back to a full solve if needed.\n *\n * @param constraintId - ID of the existing constraint to update.\n * @param value - New numeric value for the constraint.\n * @returns A new solved `ConstraintSketch`.\n */\n withUpdatedConstraint(constraintId: string, value: number): ConstraintSketch;\n /**\n * Return a human-readable diagnostic string of the solved state.\n */\n inspect(): string;\n}\ninterface ConstrainedSketchBuilder {\n /** Import a `Point2D` object into the sketch. */\n importPoint(pt: {\n x: number;\n y: number;\n }, fixed?: boolean): PointId;\n /** Import a `Line2D` object into the sketch. */\n importLine(l: {\n start: {\n x: number;\n y: number;\n };\n end: {\n x: number;\n y: number;\n };\n }, fixed?: boolean): LineId;\n /** Import a `Rectangle2D` as four points and four lines. */\n importRectangle(r: {\n vertices: [\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n }\n ];\n }, fixed?: boolean): {\n bottom: LineId;\n right: LineId;\n top: LineId;\n left: LineId;\n points: [\n PointId,\n PointId,\n PointId,\n PointId\n ];\n };\n /** Add a fixed reference point at `(x, y)`. */\n referencePoint(x: number, y: number): PointId;\n /** Add a fixed reference line from `(x1, y1)` to `(x2, y2)`. */\n referenceLine(x1: number, y1: number, x2: number, y2: number): LineId;\n /** Import a single named entity from a solved sketch as fixed reference geometry. */\n referenceFrom(source: ConstraintSketch, entityId: string): PointId | LineId | null;\n /** Import all non-construction entities from a solved sketch as fixed references. */\n referenceAllFrom(source: ConstraintSketch): {\n points: Map<string, PointId>;\n lines: Map<string, LineId>;\n };\n}\ninterface ConstrainedSketchOptions {\n /** When true, adding a constraint that cannot be satisfied throws instead of silently discarding it. */\n strict?: boolean;\n}\ninterface ConstrainedSketchBuilder extends ConstraintBuilderMethods {\n}\ndeclare class ConstrainedSketchBuilder {\n private points;\n private lines;\n private circles;\n private arcs;\n private beziers;\n private shapes;\n private _groups;\n /** Point IDs owned by groups — excluded from the serialized points array. */\n private groupOwnedPointIds;\n /** Line IDs owned by groups — excluded from the serialized lines array. */\n private groupOwnedLineIds;\n private constraints;\n private loops;\n private rejectedConstraints;\n /** Maps rejected constraint ID → human-readable reason string. */\n private rejectionReasons;\n private cursor;\n private loopStart;\n /** Last arc created by the path API (arcTo), used by blendTo. */\n private lastPathArc;\n private nextId;\n private strict;\n /** Cumulative time spent in seedIncrementalGeometry calls (ms). */\n private seedTimeMs;\n /** Max cumulative time for all seed calls (ms). After this, seeding is skipped. */\n private static readonly SEED_BUDGET_MS;\n /** WASM solver session handle — persists state across seed steps. */\n private _sessionHandle;\n private _sessionApi;\n private _sessionFailed;\n constructor(options?: ConstrainedSketchOptions);\n /** Try to create a WASM solver session. Returns true if session is active. */\n private ensureSession;\n private destroySession;\n /**\n * Add a free point to the sketch at `(x, y)`.\n *\n * If `x` or `y` are omitted, the point is placed at the bounding-box center\n * of existing geometry so it starts near other entities rather than at the origin.\n * Throws if either coordinate is `NaN` or `Infinity`.\n *\n * @param x - X coordinate. Defaults to existing geometry center.\n * @param y - Y coordinate. Defaults to existing geometry center.\n * @param fixed - When true the point is pinned at (x, y) and acts like a `fix` constraint.\n * @returns A `PointId` that can be passed to `line()`, `circle()`, and constraints.\n * @category Constrained Sketches\n */\n point(x?: number, y?: number, fixed?: boolean): PointId;\n /**\n * Return the `PointId` of the point created at the given insertion index (0-based).\n *\n * Useful for referencing points by order when the original `PointId` was not stored.\n *\n * @param index - 0-based creation index.\n * @returns The `PointId` at that index.\n * @category Constrained Sketches\n */\n /** Return the `PointId` of the point created at the given insertion index. */\n pointAt(index: number): PointId;\n /**\n * Connect two existing points with a line segment.\n *\n * Pass `construction = true` for a helper line that participates in constraints\n * but is excluded from the solved sketch output (not part of any profile loop).\n *\n * **Example** (construction axis for symmetry)\n *\n * ```ts\n * const axis = sk.line(sk.point(0, -50), sk.point(0, 50), true);\n * sk.symmetric(p1, p2, axis);\n * ```\n *\n * @param a - Start `PointId`.\n * @param b - End `PointId`.\n * @param construction - When true this line is excluded from the solved output. Default: false.\n * @param name - Optional label for debugging.\n * @returns A `LineId` for use in constraints and path loops.\n * @category Constrained Sketches\n */\n line(a: PointId, b: PointId, construction?: boolean, name?: string): LineId;\n /**\n * Return the `LineId` of the line created at the given insertion index (0-based).\n *\n * Useful in path-drawing workflows where lines are created implicitly via\n * `lineTo()` and you need to reference one by position.\n *\n * ```ts\n * sk.moveTo(0, 0).lineTo(50, 0).lineTo(50, 30).close();\n * sk.length(sk.lineAt(0), 50);\n * ```\n *\n * @param index - 0-based creation index.\n * @returns The `LineId` at that index.\n * @category Constrained Sketches\n */\n /** Return the `LineId` of the line created at the given insertion index. */\n lineAt(index: number): LineId;\n /**\n * Add a circle to the sketch with the given center point and initial radius.\n *\n * The radius is a starting value — if you add a `radius()` or `diameter()` constraint,\n * the solver will adjust it. Non-construction circles automatically register a loop.\n *\n * @param center - Center `PointId`.\n * @param radius - Initial radius (must be finite and positive).\n * @param construction - When true the circle is excluded from the solved output. Default: false.\n * @param segments - Tessellation resolution for display. Default: 48.\n * @param name - Optional label for debugging.\n * @returns A `CircleId` for use in constraints.\n * @category Constrained Sketches\n */\n circle(center: PointId, radius: number, construction?: boolean, segments?: number, name?: string): CircleId;\n /**\n * Return the `CircleId` of the circle created at the given insertion index (0-based).\n *\n * @param index - 0-based creation index.\n * @returns The `CircleId` at that index.\n * @category Constrained Sketches\n */\n /** Return the `CircleId` of the circle created at the given insertion index. */\n circleAt(index: number): CircleId;\n /**\n * Register a named shape (closed polygon) from an ordered list of line IDs.\n *\n * The `ShapeId` can be passed to `shapeWidth()`, `shapeHeight()`, `shapeArea()`,\n * `shapeCentroidX()`, `shapeCentroidY()`, and `shapeEqualCentroid()` constraints.\n * Shape registration is done automatically by concept factories like `rect()` and `addPolygon()`.\n *\n * @param lines - Ordered line IDs forming the closed polygon boundary.\n * @returns A `ShapeId` for use in shape constraints.\n * @category Constrained Sketches\n */\n shape(lines: LineId[]): ShapeId;\n /**\n * Create a rigid-body group with a local coordinate frame.\n *\n * Points and lines added to the group move together as a unit — the solver\n * sees 3 DOF (x, y, θ) instead of 2N per point. After configuring the group,\n * call `.done()` to register it and receive a `SketchGroupHandle`.\n *\n * Group points are addressable by their `PointId` in all sketch constraints\n * (e.g. `sk.coincident`, `sk.distance`) just like any other points.\n *\n * **Example**\n *\n * ```ts\n * const g = sk.group({ x: 50, y: 30 });\n * const p0 = g.point(0, 0); // local origin → world (50, 30)\n * const p1 = g.point(100, 0); // local (100,0) → world (150, 30)\n * const l = g.line(p0, p1);\n * g.fixRotation();\n * const handle = g.done();\n * // p0, p1 work in constraints like any other PointId:\n * sk.coincident(p0, someExternalPoint);\n * ```\n *\n * @param opts - Initial frame position (x, y) in degrees theta, and optional id.\n * @returns A `SketchGroupBuilder` — call `.done()` to finalize.\n * @category Constrained Sketches\n */\n group(opts?: {\n x?: number;\n y?: number;\n theta?: number;\n id?: string;\n }): SketchGroupBuilder;\n /**\n * Register a group directly (called by SketchGroupBuilder).\n * @internal\n */\n _registerGroup(group: SketchGroup): void;\n /** Add a raw constraint object to the builder. */\n constrain(constraint: Omit<SketchConstraint, "id">): this;\n /**\n * Keep the live builder geometry near a solved state as constraints are added.\n * This restores the progressive seeding path used by large sketches in the browser\n * without rejecting constraints or changing the final solve API.\n */\n private seedIncrementalGeometry;\n /** Serialize a constraint for the session API (matches Rust serde format). */\n private serializeConstraintForSession;\n /** Sync point positions from session state back to builder entities. */\n private syncPointsFromSession;\n private resolvePointId;\n private resolveLineId;\n private resolveCircleId;\n private resolveArcId;\n private resolveBezierId;\n private resolveShapeId;\n /**\n * Validate that a constraint value is a finite number.\n * Throws with the constraint name for clear debugging.\n */\n private requireFinite;\n /**\n * Run the constraint solver and return a solved sketch.\n *\n * The returned `ConstraintSketch` extends `Sketch` and can be used directly\n * in all 3D operations (`extrude`, `revolve`, etc.). It also exposes\n * `constraintMeta` with the solver status:\n *\n * ```ts\n * const result = sk.solve();\n * result.constraintMeta.status; // \'fully\' | \'under\' | \'over\' | \'over-redundant\'\n * result.constraintMeta.dof; // 0 = fully constrained\n * result.constraintMeta.maxError; // residual — should be < 1e-6\n * result.inspect(); // human-readable summary\n * result.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 * @param options - Advanced solver options (iterations, restarts, tolerance, etc.).\n * @returns A `ConstraintSketch` (also a valid `Sketch`) with solved geometry.\n * @category Constrained Sketches\n */\n solve(options?: SolveOptions): ConstraintSketch | Sketch;\n /**\n * Run the solver without building a full `ConstraintSketch`.\n *\n * Lighter than `solve()` — skips profile and DOF analysis. Useful for\n * lightweight constraint validation or progress monitoring mid-construction.\n *\n * @param options - Advanced solver options (iterations, restarts, tolerance, etc.).\n * @returns Object with `maxError`, `rejectedCount`, and the solved `ConstraintDefinition`.\n * @category Constrained Sketches\n */\n solveConstraintsOnly(options?: SolveOptions): {\n maxError: number;\n rejectedCount: number;\n definition: ConstraintDefinition;\n };\n private buildDefinition;\n /** Sync solved positions from a definition back to the builder\'s live entities. */\n private syncFromDefinition;\n private getPoint;\n /** Compute arc center from start/end points, radius, and clockwise flag; create the arc entity. */\n private addArc;\n /**\n * Bounding box of all existing non-construction points.\n * Returns null when no points exist yet.\n * Used by concept factories to auto-offset initial geometry.\n */\n _pointBounds(): {\n minX: number;\n maxX: number;\n minY: number;\n maxY: number;\n } | null;\n}\n/**\n * Create a parametric 2D sketch driven by geometric constraints and a nonlinear solver.\n *\n * **Workflow**\n *\n * 1. Create a builder with `constrainedSketch()`.\n * 2. Add geometry — points, lines, circles, arcs — using the builder methods.\n * 3. Add constraints (`horizontal`, `length`, `fix`, etc.) to drive the geometry.\n * 4. Call `.solve()` to run the solver and get a `ConstraintSketch` (which extends `Sketch`).\n *\n * **Example**\n *\n * ```ts\n * const sk = constrainedSketch();\n * const p1 = sk.point(0, 0);\n * const p2 = sk.point(50, 0);\n * const l1 = sk.line(p1, p2);\n * sk.fix(p1, 0, 0);\n * sk.horizontal(l1);\n * sk.length(l1, 50);\n * return sk.solve().extrude(10);\n * ```\n *\n * **Solver status**\n *\n * ```ts\n * const result = sk.solve();\n * result.constraintMeta.status; // \'fully\' | \'under\' | \'over\' | \'over-redundant\'\n * result.constraintMeta.dof; // 0 = fully constrained\n * result.constraintMeta.maxError; // residual — should be < 1e-6\n * result.inspect(); // human-readable summary\n * result.withUpdatedConstraint(\'cst-5\', 120); // update a dimension without rebuilding\n * ```\n *\n * @param options - Optional builder configuration.\n * @returns A `ConstrainedSketchBuilder` ready to accept geometry and constraints.\n * @category Constrained Sketches\n */\ndeclare function constrainedSketch(options?: ConstrainedSketchOptions): ConstrainedSketchBuilder;\ninterface SketchGroupHandle {\n readonly id: GroupId;\n /** Get a group vertex PointId by its index (order of `.point()` calls). */\n point(index: number): PointId;\n /** Get a group line LineId by its index (order of `.line()` calls). */\n line(index: number): LineId;\n /** All group vertex PointIds in creation order. */\n readonly vertices: PointId[];\n /** All group line LineIds in creation order. */\n readonly sides: LineId[];\n}\ndeclare class SketchGroupBuilder {\n private sk;\n private groupId;\n private gx;\n private gy;\n private gtheta;\n private localPoints;\n private localLines;\n private isFixed;\n private isFixedRotation;\n private registered;\n constructor(sk: ConstrainedSketchBuilder, opts: {\n x?: number;\n y?: number;\n theta?: number;\n id?: string;\n });\n /** Add a point in local coordinates. Returns its globally-addressable PointId. */\n /**\n * Add a point in local coordinates. Returns its globally-addressable PointId.\n */\n point(lx: number, ly: number): PointId;\n /** Connect two group points with a line. Both must be PointIds from this group. */\n /**\n * Connect two group points with a line. Both must be PointIds from this group.\n */\n line(a: PointId, b: PointId, name?: string): LineId;\n /** Freeze rotation (θ). Group can still translate - 2 DOF remain. */\n /** Freeze rotation (theta). Group can still translate - 2 DOF remain. */\n fixRotation(): this;\n /** Freeze all 3 DOF - group is completely fixed. */\n /** Freeze all 3 DOF - group is completely fixed. */\n fix(): this;\n /**\n * Finalize and register the group with the builder.\n * Returns a handle for referencing group points/lines in constraints.\n */\n /** Finalize and register the group with the builder. */\n done(): SketchGroupHandle;\n}\ndeclare class RouteBuilder {\n private readonly sk;\n private readonly _startPt;\n private cursorPt;\n private cursorX;\n private cursorY;\n /**\n * Current travel direction in radians from +X.\n * null = direction not yet established (first segment will set it).\n */\n private direction;\n private segments;\n private lastLineId;\n private lastArcId;\n private finished;\n constructor(sk: ConstrainedSketchBuilder, startPt: PointId, x: number, y: number);\n /** Vertical line going +Y. Length is optional (solver determines it from constraints). */\n up(length?: number): LineId;\n /** Vertical line going -Y. Length is optional. */\n down(length?: number): LineId;\n /** Horizontal line going +X. Length is optional. */\n right(length?: number): LineId;\n /** Horizontal line going -X. Length is optional. */\n left(length?: number): LineId;\n /** Line at an arbitrary angle (degrees from +X). Length is optional. */\n lineAt(angleDeg: number, length?: number): LineId;\n /**\n * Line with solver-determined direction. Length is optional.\n * Direction comes from tangency to previous arc or from constraints.\n */\n line(length?: number): LineId;\n /**\n * Line toward a specific point.\n * Length defaults to the distance to that point.\n */\n toward(x: number, y: number): LineId;\n /**\n * Tangent arc turning left relative to travel direction.\n * @param radius - Arc radius. If omitted, solver determines it.\n * @param sweepDegOrOpts - Sweep angle in degrees (constrains the arc),\n * or `{ minSweep: degrees }` to seed the geometry without constraining.\n * `minSweep` guides the solver to the correct branch for arcs that\n * sweep more than the default 90° seed.\n */\n arcLeft(radius?: number, sweepDegOrOpts?: number | {\n minSweep: number;\n }): ArcId;\n /**\n * Tangent arc turning right relative to travel direction.\n * @param radius - Arc radius. If omitted, solver determines it.\n * @param sweepDegOrOpts - Sweep angle in degrees (constrains the arc),\n * or `{ minSweep: degrees }` to seed without constraining.\n */\n arcRight(radius?: number, sweepDegOrOpts?: number | {\n minSweep: number;\n }): ArcId;\n /** Close the route with a straight line back to the start point. */\n close(): void;\n /**\n * Close the route back to its start point and register as a profile loop.\n *\n * No extra line segment is added. A coincident constraint connects the\n * last point to the start, and tangency is added for G1 smoothness\n * when arcs are at the junction. The session\'s incremental solver\n * processes these constraints, keeping seed positions accurate for\n * the final solve.\n */\n done(): void;\n /** PointId of the route\'s start point. */\n get start(): PointId;\n /** PointId of the current cursor (route\'s end). */\n get end(): PointId;\n /** Get the start point of a segment. */\n startOf(segId: LineId | ArcId): PointId;\n /** Get the end point of a segment. */\n endOf(segId: LineId | ArcId): PointId;\n /**\n * @param angle - Direction in radians from +X\n * @param length - Optional length\n * @param constrainDirection - Whether to add horizontal/vertical constraint (default true for axis-aligned)\n */\n private _addLine;\n private _addArc;\n private _ensureNotFinished;\n private _registerLoop;\n private _normalizeAngle;\n private _isVertical;\n private _isHorizontal;\n}\ninterface RouteLine {\n /** \'x\' for vertical line at x=offset, \'y\' for horizontal line at y=offset */\n axis: "x" | "y";\n offset: number;\n}\ninterface RouteCircle {\n center: [\n number,\n number\n ];\n radius: number;\n}\ninterface RouteTangent {\n tangent: RouteCircle | CircleId;\n}\ninterface RouteFillet {\n fillet: number;\n /** When \'tangent\', adds a tangent line from the smaller circle before the fillet arc. */\n approach?: "tangent";\n}\ninterface RouteTangentArc {\n tangentArc: number;\n}\ninterface RoutePoint {\n point: [\n number,\n number\n ];\n}\ninterface RouteUntil {\n line: RouteLine | LineId;\n until: number;\n}\ntype RouteStep = RouteLine | RouteCircle | CircleId | RouteTangent | RouteFillet | RouteTangentArc | RoutePoint | RouteUntil;\ninterface ConstrainedSketchBuilder {\n /**\n * Route a profile through a sequence of geometric elements (legacy API).\n * The solver computes all tangent points and intersections automatically.\n *\n * Steps can include:\n * - `{ point: [x, y] }` — route through a point\n * - `{ axis: \'x\'|\'y\', offset: n }` — follow a construction line\n * - `{ line: {...}, until: n }` — follow a line clipped to a coordinate\n * - `{ tangent: { center, radius } }` — tangent arc onto a construction circle\n * - `{ fillet: radius }` — fillet between adjacent elements\n * - `{ tangentArc: radius }` — free tangent arc (solver finds center)\n *\n * Returns `this` for chaining. Call `.solve()` after to get the Sketch.\n */\n route(steps: RouteStep[]): this;\n /**\n * Start a directional route from coordinates.\n *\n * Returns a `RouteBuilder` - describe the path with up/down/left/right/arcLeft/arcRight.\n * Each method returns the entity ID (`LineId` or `ArcId`) for use in `sk.*` constraints.\n *\n * @example\n * ```ts\n * const r = sk.route(0, 0);\n * const stem = r.up(18);\n * r.arcLeft(8.9);\n * const neck = r.down();\n * r.done();\n * sk.offsetX(stem, neck, 10.8);\n * ```\n */\n route(x: number, y: number): RouteBuilder;\n /**\n * Start a directional route from an existing sketch point.\n */\n route(startPoint: PointId): RouteBuilder;\n}\n/**\n * An immutable 2D point with measurement and construction helpers.\n *\n * Used as construction geometry in sketches, constraints, and analytic\n * measurements. All methods return new instances — `Point2D` is immutable.\n *\n * @category 2D Entities\n */\ndeclare class Point2D {\n readonly x: number;\n readonly y: number;\n constructor(x: number, y: number);\n /**\n * Measure straight-line distance to another point.\n *\n * @param other - The other point.\n * @returns Euclidean distance.\n */\n distanceTo(other: Point2D): number;\n /**\n * Compute the midpoint between this point and another point.\n *\n * @param other - The other point.\n * @returns A new midpoint.\n */\n midpointTo(other: Point2D): Point2D;\n /**\n * Return a point shifted by the given delta.\n *\n * @param dx - Horizontal offset.\n * @param dy - Vertical offset.\n * @returns A translated point.\n */\n translate(dx: number, dy: number): Point2D;\n /**\n * Convert this point to a plain `[x, y]` tuple.\n *\n * @returns A coordinate tuple.\n */\n toTuple(): [\n number,\n number\n ];\n}\n/**\n * Create an analytic 2D point for measurement and construction geometry.\n *\n * **Example**\n *\n * ```ts\n * const p = point(10, 20);\n * p.distanceTo(point(30, 40)); // Euclidean distance\n * p.midpointTo(point(30, 40)); // midpoint\n * p.translate(5, 5); // new shifted point\n * p.toTuple(); // [10, 20]\n * ```\n *\n * @param x - X coordinate\n * @param y - Y coordinate\n * @returns A new `Point2D`\n * @category 2D Entities\n */\ndeclare function point(x: number, y: number): Point2D;\n/**\n * An immutable 2D line segment with length, angle, intersection, and parallel helpers.\n *\n * Provides both segment-only (`intersectSegment`) and infinite-line\n * (`intersect`) intersection queries. All methods return new instances.\n *\n * @category 2D Entities\n */\ndeclare class Line2D {\n readonly start: Point2D;\n readonly end: Point2D;\n constructor(start: Point2D, end: Point2D);\n /** Length of the line segment. */\n get length(): number;\n /** Midpoint of the line segment. */\n get midpoint(): Point2D;\n /** Direction angle in degrees, measured CCW from +X. */\n get angle(): number;\n /** Unit direction vector from start to end. */\n get direction(): [\n number,\n number\n ];\n /**\n * Create a parallel line offset by the given distance.\n *\n * Positive distance shifts to the left of the line direction.\n */\n parallel(distance: number): Line2D;\n /**\n * Intersect this line with another infinite line.\n *\n * @param other - The other line.\n * @returns The intersection point, or `null` if parallel.\n */\n intersect(other: Line2D): Point2D | null;\n /**\n * Intersect this line with another as bounded segments.\n *\n * @param other - The other line.\n * @returns The intersection point, or `null` if the segments do not cross.\n */\n intersectSegment(other: Line2D): Point2D | null;\n /**\n * Create a line from raw coordinates.\n *\n * @param x1 - Start X.\n * @param y1 - Start Y.\n * @param x2 - End X.\n * @param y2 - End Y.\n * @returns A new line segment.\n */\n static fromCoordinates(x1: number, y1: number, x2: number, y2: number): Line2D;\n /**\n * Create a line from a start point, angle, and length.\n *\n * @param origin - Start point.\n * @param angleDeg - Angle in degrees, CCW from +X.\n * @param length - Segment length.\n * @returns A new line segment.\n */\n static fromPointAndAngle(origin: Point2D, angleDeg: number, length: number): Line2D;\n /**\n * Create a line from a start point, direction vector, and length.\n *\n * @param origin - Start point.\n * @param dir - Direction vector `[x, y]`.\n * @param length - Segment length.\n * @returns A new line segment.\n */\n static fromPointAndDirection(origin: Point2D, dir: [\n number,\n number\n ], length: number): Line2D;\n}\n/**\n * Create an analytic 2D line segment between two points.\n *\n * **Example**\n *\n * ```ts\n * const l = line(0, 0, 50, 0);\n * l.length; l.midpoint; l.angle; l.direction;\n * l.parallel(10); // parallel line offset 10 (positive = left)\n * l.intersect(l2); // Point2D — treats lines as infinite\n * l.intersectSegment(l2); // Point2D or null — segments only\n *\n * Line2D.fromPointAndAngle(point(0, 0), 45, 100);\n * Line2D.fromPointAndDirection(point(0, 0), [1, 1], 50);\n * ```\n *\n * @param x1 - Start X\n * @param y1 - Start Y\n * @param x2 - End X\n * @param y2 - End Y\n * @returns A new `Line2D`\n * @category 2D Entities\n */\ndeclare function line(x1: number, y1: number, x2: number, y2: number): Line2D;\n/**\n * An immutable 2D circle with area, circumference, and extrusion support.\n *\n * Extruding a `Circle2D` produces a cylinder with named `top`, `bottom`,\n * and `side` faces accessible via the topology API.\n *\n * @category 2D Entities\n */\ndeclare class Circle2D {\n readonly center: Point2D;\n readonly radius: number;\n constructor(center: Point2D, radius: number);\n /** Diameter of the circle. */\n get diameter(): number;\n /** Circumference of the circle. */\n get circumference(): number;\n /** Area of the circle. */\n get area(): number;\n /**\n * Return a point on the circle at the given angle.\n *\n * @param angleDeg - Angle in degrees, where 0 is right and CCW is positive.\n * @returns A point on the perimeter.\n */\n pointAtAngle(angleDeg: number): Point2D;\n /**\n * Return a translated circle.\n *\n * @param dx - Horizontal offset.\n * @param dy - Vertical offset.\n * @returns A shifted circle.\n */\n translate(dx: number, dy: number): Circle2D;\n /**\n * Convert this circle to a sketch profile.\n *\n * @param segments - Optional polygon approximation resolution.\n * @returns A sketch representation of the circle.\n */\n toSketch(segments?: number): Sketch;\n /**\n * Extrude the circle into a solid cylinder.\n *\n * @param height - Extrusion height.\n * @param segments - Optional tessellation resolution.\n * @returns A new shape with named cylinder faces.\n */\n extrude(height: number, segments?: number): Shape;\n /**\n * Create a circle from its center and radius.\n *\n * @param center - Circle center.\n * @param radius - Circle radius.\n * @returns A new circle.\n */\n static fromCenterAndRadius(center: Point2D, radius: number): Circle2D;\n /**\n * Create a circle from its center and diameter.\n *\n * @param center - Circle center.\n * @param diameter - Circle diameter.\n * @returns A new circle.\n */\n static fromDiameter(center: Point2D, diameter: number): Circle2D;\n}\n/**\n * Create an analytic 2D circle for measurement, construction, and extrusion.\n *\n * **Example**\n *\n * ```ts\n * const c = circle(0, 0, 25);\n * c.diameter; c.circumference; c.area;\n * c.pointAtAngle(90); // Point2D at top (90° CCW from +X)\n *\n * // Extrude to cylinder with named faces\n * const cyl = c.extrude(30);\n * cyl.face(\'top\'); // FaceRef (planar)\n * cyl.face(\'side\'); // FaceRef (curved)\n *\n * Circle2D.fromDiameter(point(0, 0), 50);\n * ```\n *\n * @param cx - Center X\n * @param cy - Center Y\n * @param radius - Radius\n * @returns A new `Circle2D`\n * @category 2D Entities\n */\ndeclare function circle(cx: number, cy: number, radius: number): Circle2D;\ntype RectSide = "top" | "bottom" | "left" | "right";\ntype RectVertex = "top-left" | "top-right" | "bottom-left" | "bottom-right";\n/**\n * A rectangle with named sides, vertices, and extrusion support.\n *\n * **Details**\n *\n * Sides are named based on the rectangle\'s local orientation at construction\n * time. Vertices go: bottom-left, bottom-right, top-right, top-left (CCW).\n *\n * Use `rect()` for the normal centered sketch primitive. Use `Rectangle2D`\n * when you need named sides/vertices, or an extrusion with tracked vertical\n * edges such as `vert-br` for `filletTrackedEdge()` / `chamferTrackedEdge()`.\n *\n * Extruding a `Rectangle2D` produces a `Shape` with named faces:\n * `top`, `bottom`, `side-left`, `side-right`, `side-top`, `side-bottom`.\n * These are accessible via the topology API (`.face()`, `.edge()`).\n *\n * **Example**\n *\n * ```ts\n * const r = Rectangle2D.fromDimensions(0, 0, 100, 60);\n * r.side(\'top\'); r.side(\'left\'); // Line2D\n * r.vertex(\'top-left\'); // Point2D\n * r.width; r.height; r.center;\n * const [d1, d2] = r.diagonals(); // [bl-tr, br-tl]\n *\n * r.toSketch(); // Sketch (for 2D operations)\n * r.extrude(20); // Shape with named faces\n *\n * Rectangle2D.fromCenterAndDimensions(point(50, 30), 100, 60);\n * Rectangle2D.from2Corners(point(0, 0), point(100, 60));\n * Rectangle2D.from3Points(p1, p2, p3); // free-angle rectangle\n * ```\n *\n * @category 2D Entities\n */\ndeclare class Rectangle2D {\n /** Vertices in order: bottom-left, bottom-right, top-right, top-left */\n readonly vertices: [\n Point2D,\n Point2D,\n Point2D,\n Point2D\n ];\n constructor(vertices: [\n Point2D,\n Point2D,\n Point2D,\n Point2D\n ]);\n /** Width of the rectangle. */\n get width(): number;\n /** Height of the rectangle. */\n get height(): number;\n /** Geometric center of the rectangle. */\n get center(): Point2D;\n /**\n * Return a named side of the rectangle.\n *\n * @param name - One of `top`, `bottom`, `left`, or `right`.\n * @returns The requested side as a line segment.\n */\n side(name: RectSide): Line2D;\n /**\n * Return a side by index.\n *\n * @param index - Side index: `0=bottom`, `1=right`, `2=top`, `3=left`.\n * @returns The requested side as a line segment.\n */\n sideAt(index: number): Line2D;\n /**\n * Return a named vertex of the rectangle.\n *\n * @param name - One of `bottom-left`, `bottom-right`, `top-right`, or `top-left`.\n * @returns The requested vertex.\n */\n vertex(name: RectVertex): Point2D;\n /**\n * Return the two diagonals of the rectangle.\n *\n * @returns `[bottom-left to top-right, bottom-right to top-left]`.\n */\n diagonals(): [\n Line2D,\n Line2D\n ];\n /**\n * Convert the rectangle to a sketch profile.\n *\n * @returns A sketch representation of the rectangle.\n */\n toSketch(): Sketch;\n /**\n * Return a translated rectangle.\n *\n * @param dx - Horizontal offset.\n * @param dy - Vertical offset.\n * @returns A shifted rectangle.\n */\n translate(dx: number, dy: number): Rectangle2D;\n /**\n * Create an axis-aligned rectangle from origin corner plus width and height.\n *\n * @param x - Bottom-left X.\n * @param y - Bottom-left Y.\n * @param width - Rectangle width.\n * @param height - Rectangle height.\n * @returns A new rectangle.\n */\n static fromDimensions(x: number, y: number, width: number, height: number): Rectangle2D;\n /**\n * Create a rectangle centered on a point.\n *\n * @param center - Rectangle center.\n * @param width - Rectangle width.\n * @param height - Rectangle height.\n * @returns A new rectangle.\n */\n static fromCenterAndDimensions(center: Point2D, width: number, height: number): Rectangle2D;\n /**\n * Create an axis-aligned rectangle from two opposite corners.\n *\n * @param p1 - First corner.\n * @param p2 - Opposite corner.\n * @returns A new rectangle.\n */\n static from2Corners(p1: Point2D, p2: Point2D): Rectangle2D;\n /**\n * Create a free-angle rectangle from three points.\n *\n * `p1` and `p2` define one edge, and `p3` chooses the perpendicular side.\n *\n * @param p1 - First edge point.\n * @param p2 - Second edge point.\n * @param p3 - Point indicating the rectangle height direction.\n * @returns A new rectangle.\n */\n static from3Points(p1: Point2D, p2: Point2D, p3: Point2D): Rectangle2D;\n /**\n * Extrude the rectangle into a solid prism with named topology.\n *\n * @param height - Extrusion height.\n * @param up - When false, extrude downward instead of upward.\n * @returns A new shape with named faces and edges.\n */\n extrude(height: number, up?: boolean): Shape;\n}\n/**\n * Identity function that returns degrees unchanged.\n *\n * Use for clarity when the unit of an angle value would otherwise be\n * ambiguous — e.g. `param("Angle", degrees(45))`.\n *\n * @param deg - Angle in degrees\n * @returns The same value unchanged\n * @category 2D Entities\n */\ndeclare function degrees(deg: number): number;\n/**\n * Convert radians to degrees.\n *\n * ForgeCAD\'s public API uses degrees throughout. Use this when you have a\n * radian value (e.g. from `Math.atan2`) that you want to express in degrees.\n *\n * @param rad - Angle in radians\n * @returns Equivalent angle in degrees\n * @category 2D Entities\n */\ndeclare function radians(rad: number): number;\ndeclare function polar(length: number, angleDeg: number, from?: [\n number,\n number\n]): [\n number,\n number\n];\ntype LineArg = LineId | Line2D;\ntype PointArg = PointId | Point2D;\ndeclare const Constraint: {\n /** Constrain two lines to be parallel. */\n makeParallel(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder;\n /** Constrain the signed angle from line `a` to line `b`. */\n enforceAngle(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg, angleDeg: number): ConstrainedSketchBuilder;\n /** Constrain a line to be horizontal. */\n horizontal(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder;\n /** Constrain a line to be vertical. */\n vertical(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder;\n /** Constrain two lines to have equal length. */\n equalLength(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder;\n /** Constrain the distance between two points. */\n distance(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg, value: number): ConstrainedSketchBuilder;\n /** Fix a point at a specific coordinate. */\n fix(builder: ConstrainedSketchBuilder, pt: PointArg, x: number, y: number): ConstrainedSketchBuilder;\n /** Constrain two points to occupy the same location. */\n coincident(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg): ConstrainedSketchBuilder;\n /** Constrain two lines to be perpendicular. */\n perpendicular(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder;\n /** Constrain the length of a line. */\n length(builder: ConstrainedSketchBuilder, line: LineArg, value: number): ConstrainedSketchBuilder;\n};\ntype FaceName = string;\ntype EdgeName = string;\ninterface FaceRef {\n name: FaceName;\n /** Normal direction of the face */\n normal: [\n number,\n number,\n number\n ];\n /** Center point of the face */\n center: [\n number,\n number,\n number\n ];\n /** Compiler-owned face query when available. */\n query?: FaceQueryRef;\n /** True when the face can host a 2D sketch placement frame */\n planar?: boolean;\n /** Face-local horizontal axis for planar faces */\n uAxis?: [\n number,\n number,\n number\n ];\n /** Face-local vertical axis for planar faces */\n vAxis?: [\n number,\n number,\n number\n ];\n /** Shared descendant-resolution metadata when this face is a semantic region/set. */\n descendant?: FaceDescendantMetadata;\n}\ninterface EdgeRef {\n name: EdgeName;\n /** Start point */\n start: [\n number,\n number,\n number\n ];\n /** End point */\n end: [\n number,\n number,\n number\n ];\n /** Compiler-owned edge query when available. */\n query?: EdgeQueryRef;\n}\ntype PlaneSpec = {\n origin: Vec3;\n normal: Vec3;\n} | {\n plane: "XY" | "XZ" | "YZ";\n offset?: number;\n} | {\n face: FaceRef;\n};\ntype Vec3$1 = [\n number,\n number,\n number\n];\ntype TpmsThicknessMode = "legacy-field-threshold" | "metric-approx";\ntype SdfFunctionConstant = number | string | boolean | null | SdfFunctionConstant[] | {\n [key: string]: SdfFunctionConstant;\n};\ntype SdfFunctionConstants = Record<string, SdfFunctionConstant>;\ninterface SdfSphereNode {\n kind: "sdf:sphere";\n radius: number;\n}\ninterface SdfBoxNode {\n kind: "sdf:box";\n halfExtents: Vec3$1;\n}\ninterface SdfCylinderNode {\n kind: "sdf:cylinder";\n height: number;\n radius: number;\n}\ninterface SdfTorusNode {\n kind: "sdf:torus";\n majorRadius: number;\n minorRadius: number;\n}\ninterface SdfCapsuleNode {\n kind: "sdf:capsule";\n height: number;\n radius: number;\n}\ninterface SdfConeNode {\n kind: "sdf:cone";\n height: number;\n radius: number;\n}\ninterface SdfPolylineSweepNode {\n kind: "sdf:polylineSweep";\n /** Ordered path samples in world/local SDF space. */\n points: Vec3$1[];\n /** Radius at each corresponding point. */\n radii: number[];\n /** Smooth blend radius between adjacent tapered segments. */\n blend: number;\n}\ninterface SdfUnionNode {\n kind: "sdf:union";\n children: SdfNode[];\n}\ninterface SdfDifferenceNode {\n kind: "sdf:difference";\n /** First child is the base; subsequent children are subtracted. */\n children: SdfNode[];\n}\ninterface SdfIntersectionNode {\n kind: "sdf:intersection";\n children: SdfNode[];\n}\ninterface SdfSmoothUnionNode {\n kind: "sdf:smoothUnion";\n children: SdfNode[];\n radius: number;\n}\ninterface SdfSmoothDifferenceNode {\n kind: "sdf:smoothDifference";\n children: SdfNode[];\n radius: number;\n}\ninterface SdfSmoothIntersectionNode {\n kind: "sdf:smoothIntersection";\n children: SdfNode[];\n radius: number;\n}\ninterface SdfMorphNode {\n kind: "sdf:morph";\n a: SdfNode;\n b: SdfNode;\n /** 0 = fully a, 1 = fully b */\n t: number;\n}\ninterface SdfTranslateNode {\n kind: "sdf:translate";\n child: SdfNode;\n offset: Vec3$1;\n}\ninterface SdfRotateNode {\n kind: "sdf:rotate";\n child: SdfNode;\n /** Euler angles in degrees (X, Y, Z) */\n degrees: Vec3$1;\n}\ninterface SdfScaleNode {\n kind: "sdf:scale";\n child: SdfNode;\n factor: number;\n}\ninterface SdfTwistNode {\n kind: "sdf:twist";\n child: SdfNode;\n /** Total twist angle in degrees over the full height of the shape */\n degreesPerUnit: number;\n}\ninterface SdfBendNode {\n kind: "sdf:bend";\n child: SdfNode;\n /** Bend radius — larger = gentler bend */\n radius: number;\n}\ninterface SdfRepeatNode {\n kind: "sdf:repeat";\n child: SdfNode;\n /** Spacing between repetitions [x, y, z]. 0 = no repetition on that axis. */\n spacing: Vec3$1;\n /** Max repetition count per side. 0 = infinite. */\n count: Vec3$1;\n}\ninterface SdfShellNode {\n kind: "sdf:shell";\n child: SdfNode;\n thickness: number;\n}\ninterface SdfDisplaceNode {\n kind: "sdf:displace";\n child: SdfNode;\n /** Serialized as a function body string: receives (x, y, z) and returns a number. */\n functionBody: string;\n /** Named constants injected as additional function parameters (avoids closure serialization issues). */\n constants?: SdfFunctionConstants;\n}\ninterface SdfSurfaceDisplaceNode {\n kind: "sdf:surfaceDisplace";\n child: SdfNode;\n /**\n * Function body string: receives (u, v) in surface millimeters and returns a height\n * displacement value. Negative = into surface, positive = outward.\n */\n patternBody: string;\n /** Named constants injected as additional function parameters. */\n constants?: SdfFunctionConstants;\n /** Override auto-detected UV mode. Undefined or \'auto\' = auto-detect from child tree. */\n uvMode?: "auto" | "sphere" | "cylinder" | "torus" | "triplanar";\n /** Triplanar blend sharpness (only used when UV mode is triplanar). Default: 4. */\n triplanarSharpness?: number;\n}\ninterface SdfOnionNode {\n kind: "sdf:onion";\n child: SdfNode;\n /** Number of concentric layers */\n layers: number;\n thickness: number;\n}\ninterface SdfGyroidNode {\n kind: "sdf:gyroid";\n cellSize: number;\n thickness: number;\n thicknessMode?: TpmsThicknessMode;\n}\ninterface SdfSchwarzPNode {\n kind: "sdf:schwarzP";\n cellSize: number;\n thickness: number;\n thicknessMode?: TpmsThicknessMode;\n}\ninterface SdfDiamondNode {\n kind: "sdf:diamond";\n cellSize: number;\n thickness: number;\n thicknessMode?: TpmsThicknessMode;\n}\ninterface SdfLidinoidNode {\n kind: "sdf:lidinoid";\n cellSize: number;\n thickness: number;\n thicknessMode?: TpmsThicknessMode;\n}\ninterface SdfSpatialBlendNode {\n kind: "sdf:spatialBlend";\n a: SdfNode;\n b: SdfNode;\n /** Function body returning 0..1. 0 = fully a, 1 = fully b. */\n functionBody: string;\n constants?: SdfFunctionConstants;\n}\ninterface SdfNoiseNode {\n kind: "sdf:noise";\n /** Spatial frequency — smaller = larger features. */\n scale: number;\n /** Peak displacement amplitude. */\n amplitude: number;\n /** Number of octaves for fractal Brownian motion (1 = plain simplex). */\n octaves: number;\n /** Seed for deterministic variation. 0 = default permutation. */\n seed: number;\n}\ninterface SdfVoronoiNode {\n kind: "sdf:voronoi";\n /** Size of each Voronoi cell in world units. */\n cellSize: number;\n /** Wall thickness between cells. */\n wallThickness: number;\n /** Seed for deterministic variation. */\n seed: number;\n /**\n * When set, enables surface-aware mode using IQ two-pass with membrane suppression.\n * The child SDF\'s gradient is used to estimate the surface normal, and walls aligned\n * with that normal are suppressed. This is the child SDF tree whose gradient provides\n * the surface normal for filtering.\n */\n surfaceChild?: SdfNode;\n /**\n * Membrane suppression threshold (0..1). Higher = more aggressive suppression.\n * 0 = no filtering, 1 = suppress all walls. Default: 0.7.\n */\n suppressionThreshold?: number;\n}\ninterface SdfCustomNode {\n kind: "sdf:custom";\n /** Function body string: receives (x, y, z) and returns signed distance. */\n functionBody: string;\n /** GLSL expression generated from the same source expression, when raymarchable. */\n shaderBody?: string;\n /** Why shaderBody could not be generated. */\n shaderUnsupportedReason?: string;\n /** Conservative maximum shader raymarch step for this field. */\n raymarchStepLimit?: number;\n /** Optional divisor applied to shader distance for non-distance fields. */\n raymarchLipschitz?: number;\n bounds: {\n min: Vec3$1;\n max: Vec3$1;\n };\n /** Named constants injected as additional function parameters (avoids closure serialization issues). */\n constants?: SdfFunctionConstants;\n}\ntype SdfNode = SdfSphereNode | SdfBoxNode | SdfCylinderNode | SdfTorusNode | SdfCapsuleNode | SdfConeNode | SdfPolylineSweepNode | SdfUnionNode | SdfDifferenceNode | SdfIntersectionNode | SdfSmoothUnionNode | SdfSmoothDifferenceNode | SdfSmoothIntersectionNode | SdfMorphNode | SdfTranslateNode | SdfRotateNode | SdfScaleNode | SdfTwistNode | SdfBendNode | SdfRepeatNode | SdfShellNode | SdfDisplaceNode | SdfSurfaceDisplaceNode | SdfOnionNode | SdfGyroidNode | SdfSchwarzPNode | SdfDiamondNode | SdfLidinoidNode | SdfSpatialBlendNode | SdfNoiseNode | SdfVoronoiNode | SdfCustomNode;\ninterface SdfBounds {\n min: Vec3$1;\n max: Vec3$1;\n}\ntype SdfMeshingQuality = "draft" | "preview" | "export";\ninterface SdfToShapeOptions {\n /** Target mesh edge length. Smaller = finer mesh. Overrides quality-derived resolution. */\n edgeLength?: number;\n /** Override auto-computed bounds. Strongly recommended for infinite/repeated fields. */\n bounds?: {\n min: Vec3$1;\n max: Vec3$1;\n };\n /** Coarse quality preset. Default: \'preview\'. */\n quality?: SdfMeshingQuality;\n /** Preferred absolute surface tolerance in millimeters. */\n tolerance?: number;\n /** Smallest feature that should survive meshing, in millimeters. */\n minFeatureSize?: number;\n /** Simplification control. `false` disables, `true` and `\'safe\'` use topology-validated simplification. */\n simplify?: boolean | "safe";\n /** Optional post-extraction triangle budget. */\n maxTriangles?: number;\n /** Optional pre-extraction grid-point budget. Default is browser-safe. */\n maxGridPoints?: number;\n /** Lower clamp for resolved edge length. Default: 0.15mm. */\n minEdgeLength?: number;\n /** Log resolved meshing settings and backend extraction timings. */\n diagnostics?: boolean;\n}\ndeclare const SHEET_METAL_EDGES: readonly [\n "top",\n "right",\n "bottom",\n "left"\n];\ntype SheetMetalEdge = (typeof SHEET_METAL_EDGES)[number];\ntype SheetMetalPlanarRegionName = "panel" | `flange-${SheetMetalEdge}`;\ntype SheetMetalRegionName = SheetMetalPlanarRegionName | `bend-${SheetMetalEdge}`;\ninterface SheetMetalBendAllowance {\n kind: "k-factor";\n kFactor: number;\n}\ninterface SheetMetalPanelSpec {\n width: number;\n height: number;\n}\ninterface SheetMetalFlangeSpec {\n edge: SheetMetalEdge;\n length: number;\n angleDeg: number;\n}\ninterface SheetMetalCornerReliefSpec {\n kind: "rect";\n size: number;\n}\ninterface SheetMetalModel {\n panel: SheetMetalPanelSpec;\n thickness: number;\n bendRadius: number;\n bendAllowance: SheetMetalBendAllowance;\n cornerRelief: SheetMetalCornerReliefSpec;\n flanges: SheetMetalFlangeSpec[];\n}\ntype SurfaceContinuity = "G0" | "G1" | "G2";\ntype SurfaceFillStyle = "coons" | "curved" | "stretch";\ninterface TransformationStep {\n kind: string;\n description: string;\n details?: Record<string, unknown>;\n}\ntype TimelineEntryCategory = "primitive" | "sketch" | "modifier" | "boolean" | "transform";\ninterface TimelineEntry {\n kind: string;\n label: string;\n summary: string;\n category: TimelineEntryCategory;\n}\ninterface FaceTransformationHistory {\n faceName: string;\n origin: {\n operation: string;\n owner?: ShapeQueryOwner;\n };\n transformations: TransformationStep[];\n query?: FaceQueryRef;\n /** Ordered list of operations that built this shape, oldest first. */\n timeline: TimelineEntry[];\n}\n/**\n * FaceQuery — declarative face selector types for geometric face matching.\n *\n * `FaceSelector` is a string label name. `FaceQuery` is a structured\n * object that matches faces by geometry (normal, nearest, area, etc.)\n * and is used internally by compile-plan face resolution.\n */\ninterface FaceQuery {\n /** Filter by face normal direction (cosine similarity > 0.9998) */\n normal?: [\n number,\n number,\n number\n ];\n /** Pick face whose centroid is nearest to this point (XY or XYZ) */\n nearest?: [\n number,\n number\n ] | [\n number,\n number,\n number\n ];\n /** Pick face containing/nearest to this world point */\n at?: [\n number,\n number,\n number\n ];\n /** Disambiguation when multiple faces match */\n pick?: "largest" | "smallest" | "max-x" | "max-y" | "max-z" | "min-x" | "min-y" | "min-z";\n /** Filter by area range (mm²) */\n area?: {\n min?: number;\n max?: number;\n };\n /** Only match planar faces (default: true) */\n planar?: boolean;\n}\n/** A face label name. Pass to `shape.face()` to look up a user-authored label. */\ntype FaceSelector = string;\ntype PlacementReferenceKind = "points" | "edges" | "surfaces" | "objects";\ninterface PlacementEdgeRef {\n start: Vec3;\n end: Vec3;\n}\ninterface PlacementSurfaceRef {\n center: Vec3;\n normal: Vec3;\n}\ninterface PlacementObjectRef {\n min: Vec3;\n max: Vec3;\n}\ninterface PlacementReferences {\n points: Record<string, Vec3>;\n edges: Record<string, PlacementEdgeRef>;\n surfaces: Record<string, PlacementSurfaceRef>;\n objects: Record<string, PlacementObjectRef>;\n}\ntype PlacementObjectInput = PlacementObjectRef | {\n min: [\n number,\n number,\n number\n ];\n max: [\n number,\n number,\n number\n ];\n} | {\n boundingBox(): {\n min: number[];\n max: number[];\n };\n} | {\n _bbox(): {\n min: number[];\n max: number[];\n };\n};\ninterface PlacementReferenceInput {\n points?: Record<string, [\n number,\n number,\n number\n ]>;\n edges?: Record<string, PlacementEdgeRef>;\n surfaces?: Record<string, PlacementSurfaceRef>;\n objects?: Record<string, PlacementObjectInput>;\n}\ntype PlacementAnchorLike = Anchor3D | string;\ntype GroupChild = Shape | Sketch | ShapeGroup;\ninterface NamedGroupChild {\n name: string;\n shape?: Shape | ShapeGroup;\n sketch?: Sketch;\n group?: GroupInput[] | ShapeGroup;\n}\ntype GroupInput = GroupChild | NamedGroupChild;\ndeclare class ShapeGroup {\n readonly children: GroupChild[];\n readonly childNames: Array<string | undefined>;\n constructor(children: GroupChild[], childNames?: Array<string | undefined>);\n /** Return the optional name of the child at `index`. */\n childName(index: number): string | undefined;\n /**\n * Return the named child by name. Throws if not found.\n * Useful when importing a multipart group and working on components individually.\n */\n child(name: string): GroupChild;\n /** Apply fn to all children, producing a new ShapeGroup that also copies placement refs. */\n private mapChildren;\n /** Apply fn to all children and also transform placement refs by the given matrix. */\n private mapChildrenTransform;\n /** Return a deep-cloned ShapeGroup tree (refs copied). */\n clone(): ShapeGroup;\n /** Alias for clone() */\n duplicate(): ShapeGroup;\n /** Move the entire group by (x, y, z). All children move together as a unit. */\n translate(x: number, y: number, z: number): ShapeGroup;\n /** Compute combined bounding box of all 3D children */\n private _bbox;\n private _bboxCenter;\n /** Return the combined 3D bounding box of all children. */\n boundingBox(): {\n min: [\n number,\n number,\n number\n ];\n max: [\n number,\n number,\n number\n ];\n };\n private resolveRotatePoint;\n /** Move the group so its bounding-box min corner lands at the given coordinate. */\n moveTo(x: number, y: number, z: number): ShapeGroup;\n /** Move the group relative to another part\'s bounding-box min corner. */\n moveToLocal(target: Shape | ShapeGroup, x: number, y: number, z: number): ShapeGroup;\n /**\n * 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\n * on the target. `selfAnchor` selects the anchor on this group to align.\n */\n attachTo(target: Shape | ShapeGroup, targetAnchor: Anchor3D | string, selfAnchor?: Anchor3D, offset?: [\n number,\n number,\n number\n ]): ShapeGroup;\n /**\n * Place this group on a face of a parent shape.\n * See Shape.onFace() for full documentation.\n */\n onFace(parent: Shape | ShapeGroup, face: "front" | "back" | "left" | "right" | "top" | "bottom", opts?: {\n u?: number;\n v?: number;\n protrude?: number;\n }): ShapeGroup;\n /** Rotate the group around an arbitrary axis through the origin. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Rotate the group around the X axis. */\n rotateX(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Rotate the group around the Y axis. */\n rotateY(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Rotate the group around the Z axis. */\n rotateZ(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Rotate around an arbitrary axis, optionally through a pivot point. */\n rotateAroundAxis(axis: [\n number,\n number,\n number\n ], angleDeg: number, pivot?: [\n number,\n number,\n number\n ]): ShapeGroup;\n /**\n * Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point.\n * ShapeGroup string points use built-in anchors only.\n */\n rotateAroundTo(axis: [\n number,\n number,\n number\n ], pivot: [\n number,\n number,\n number\n ], movingPoint: Anchor3D | [\n number,\n number,\n number\n ], targetPoint: Anchor3D | [\n number,\n number,\n number\n ], options?: RotateAroundToOptions): ShapeGroup;\n /** Reorient the group so its local Z axis points along `direction`. */\n pointAlong(direction: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Apply a 4x4 transform matrix or `Transform` to all 3D children. */\n transform(m: Mat4 | Transform): ShapeGroup;\n /** Scale uniformly or per-axis from the group\'s bounding-box center. */\n scale(v: number | [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Scale uniformly or per-axis from an explicit pivot point. */\n scaleAround(pivot: [\n number,\n number,\n number\n ], v: number | [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Mirror across a plane through the group\'s bounding-box center. */\n mirror(normal: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Mirror across a plane through an explicit point. */\n mirrorThrough(point: [\n number,\n number,\n number\n ], normal: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Return a copy of the group with the given display color applied to each child. */\n color(hex: string): ShapeGroup;\n /**\n * Attach named placement references to this group.\n * References survive normal transforms (translate/rotate/scale/mirror/transform).\n *\n * ```javascript\n * const bracket = group(\n * { name: \'Left\', shape: leftShape },\n * { name: \'Right\', shape: rightShape },\n * ).withReferences({\n * points: { mountCenter: [0, 0, 0] },\n * });\n * ```\n */\n withReferences(refs: PlacementReferenceInput): ShapeGroup;\n /** List named placement references carried by this group. */\n referenceNames(kind?: PlacementReferenceKind): string[];\n /** Backward-compatible alias for `withConnectors()`. @deprecated Use `withConnectors()` instead. */\n withPorts(ports: Record<string, PortInput>): ShapeGroup;\n /** Backward-compatible alias for `connectorNames()`. @deprecated Use `connectorNames()` instead. */\n portNames(): string[];\n /**\n * Resolve a named placement reference or built-in Anchor3D to a 3D point.\n * Named refs take priority over built-in anchors.\n */\n referencePoint(ref: PlacementAnchorLike): [\n number,\n number,\n number\n ];\n /**\n * Translate the group so the given anchor or reference lands on the target coordinate.\n *\n * Accepts any built-in anchor name (`\'bottom\'`, `\'center\'`, `\'top-front-left\'`, etc.)\n * or a custom placement reference attached via `withReferences()`.\n *\n * ```javascript\n * // Ground a group — put its bottom at Z = 0\n * assembly.placeReference(\'bottom\', [0, 0, 0])\n *\n * // Use a custom reference from a multi-file part\n * const placed = require(\'./bracket-assembly.forge.js\').group\n * .placeReference(\'mountCenter\', [0, 0, 50]);\n * ```\n */\n placeReference(ref: PlacementAnchorLike, target: [\n number,\n number,\n number\n ], offset?: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Attach named connectors — attachment points that survive transforms.\n * Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching). */\n withConnectors(connectors: Record<string, ConnectorInput>): ShapeGroup;\n /** List all connector names, including "ChildName.connectorName" from named children. */\n connectorNames(): string[];\n /** Get all connectors of a given type, including from named children. */\n connectorsByType(type: string): Array<{\n name: string;\n port: ConnectorDef;\n }>;\n /** Distance between two connector origins on this group (supports dotted child paths). */\n connectorDistance(nameA: string, nameB: string): number;\n /** Get measurements metadata from a connector (supports dotted child paths). */\n connectorMeasurements(name: string): Record<string, number | string>;\n /**\n * Position this group by matching connectors to a target.\n * Connector names support dotted paths into named children: "ChildName.connectorName".\n *\n * Overloads:\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 matchTo(targetOrPairs: Shape | ShapeGroup | Array<[\n Shape | ShapeGroup,\n string,\n string\n ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): ShapeGroup;\n}\n/**\n * Group multiple shapes/sketches for joint transforms without merging into a single mesh.\n *\n * Unlike union(), child colors and individual identities are preserved.\n * Children can be plain shapes, named descriptors ({ name, shape/sketch/group }),\n * or nested groups. The returned ShapeGroup supports all Shape transforms\n * (translate, rotate, etc.).\n *\n * **Local coordinate pattern:** Build child parts at the origin (local coordinates),\n * then group and translate once to place the whole assembly. This eliminates the\n * error-prone pattern of manually adding parent offsets to every sub-part.\n *\n * @example\n * // BAD — every sub-part repeats the parent\'s global offset\n * const unitX = 0, unitY = -18, unitZ = 70;\n * const body = roundedBox(100, 20, 32, 4).translate(unitX, unitY, unitZ);\n * const panel = box(98, 2, 18).translate(unitX, unitY - 12, unitZ + 4);\n * const louver = box(88, 2, 6).translate(unitX, unitY - 14, unitZ - 11);\n *\n * // GOOD — build at origin, group, translate once\n * const body = roundedBox(100, 20, 32, 4);\n * const panel = box(98, 2, 18).translate(0, -12, 4);\n * const louver = box(88, 2, 6).translate(0, -14, -11);\n * const indoorUnit = group(\n * { name: \'Body\', shape: body },\n * { name: \'Panel\', shape: panel },\n * { name: \'Louver\', shape: louver },\n * ).translate(0, -18, 70);\n */\ndeclare function group(...items: GroupInput[]): ShapeGroup;\ninterface EdgeSegment {\n /** Stable index within the extraction (deterministic for a given mesh). */\n index: number;\n start: Vec3;\n end: Vec3;\n midpoint: Vec3;\n /** Normalized direction from start → end. */\n direction: Vec3;\n length: number;\n /** Dihedral angle in degrees (0 = coplanar, 180 = knife edge). */\n dihedralAngle: number;\n /** true = outside corner (convex), false = inside corner (concave). */\n convex: boolean;\n /** Normal of first adjacent face. */\n normalA: Vec3;\n /** Normal of second adjacent face (same as normalA for boundary edges). */\n normalB: Vec3;\n /** true if this is a boundary (unmatched) edge — unusual for closed solids. */\n boundary: boolean;\n}\n/** Options for `edgesOfFace()`. */\ninterface EdgesOfOptions {\n /** Exclude edges shared with these named faces. */\n exclude?: string | string[];\n /** Additional geometric filter: only convex edges. */\n convex?: boolean;\n /** Additional geometric filter: only concave edges. */\n concave?: boolean;\n /** Minimum edge length filter. */\n minLength?: number;\n}\ninterface SeatIntoOptions {\n /** Movement axis. Default: inverted face normal (points into target). */\n along?: [\n number,\n number,\n number\n ];\n /** How deep to embed. \'full\' = entire face inside. \'flush\' = nearest point touches. number = mm past flush. Default: \'full\'. */\n depth?: "full" | "flush" | number;\n /** Standoff gap in mm. Positive = gap between face and target. Negative = extra penetration. Default: 0. */\n gap?: number;\n}\ntype SdfFunctionSource = string | ((x: number, y: number, z: number, ...constants: number[]) => number);\ndeclare class SurfacePattern {\n /** Function body: receives (u, v) in surface mm, returns height displacement. */\n readonly body: string;\n /** Named constants injected into the function. */\n readonly constants?: Record<string, number>;\n constructor(body: string, constants?: Record<string, number>);\n}\ninterface SurfaceDisplaceOptions {\n /** Override auto-detected UV mode. Default: \'auto\' (detects from SDF tree). */\n uv?: "auto" | "sphere" | "cylinder" | "torus" | "triplanar";\n /** Triplanar blend sharpness — higher = crisper transitions. Default: 4. Only used in triplanar mode. */\n triplanarSharpness?: number;\n}\ninterface SdfFunctionOptions {\n /** Finite design bounds for this opaque custom field. */\n bounds: SdfBounds | [\n Vec3$1,\n Vec3$1\n ];\n /** Named finite constants referenced from the expression body. */\n constants?: SdfFunctionConstants;\n /** Conservative maximum raymarch step for native preview. */\n maxStep?: number;\n /** Optional divisor for non-distance fields. Values above 1 slow preview marching. */\n lipschitz?: number;\n}\ninterface SdfVisualMetadata {\n colorHex?: string;\n materialProps?: ShapeMaterialProps;\n bounds?: SdfBounds;\n}\ntype SculptMaterialPreset = "ceramic" | "porcelain" | "soft-rubber" | "glass" | "mint-glass" | "candy" | "strawberry-gel" | "metal" | "clay";\ntype SculptPolishInput = SculptMaterialPreset | (ShapeMaterialProps & {\n color?: string;\n});\ndeclare function knownSculptMaterialPresets(): SculptMaterialPreset[];\n/**\n * An immutable SDF expression. Supports SDF-specific operations (smooth booleans,\n * domain warps, etc.), can be returned directly for native preview, and converts\n * to a ForgeCAD Shape via `.toShape()` when materialization is needed.\n */\ndeclare class SdfShape {\n /** @internal */\n readonly _node: SdfNode;\n /** @internal */\n readonly _visual: SdfVisualMetadata;\n /** @internal */\n constructor(node: SdfNode, visual?: SdfVisualMetadata);\n /** Display color carried by this implicit leaf. */\n get colorHex(): string | undefined;\n /** Display material carried by this implicit leaf. */\n get materialProps(): ShapeMaterialProps | undefined;\n /** Explicit bounds carried by this implicit leaf, if any. */\n get explicitBounds(): SdfBounds | undefined;\n private withNode;\n private withVisual;\n /** Clone this SDF expression and its visual metadata. */\n clone(): SdfShape;\n /** Alias for clone(). */\n duplicate(): SdfShape;\n /**\n * Mesh this SDF into a ForgeCAD Shape through ForgeCAD\'s Surface Nets pipeline.\n * Once converted, the result is a regular Shape — booleans, transforms, export all work.\n */\n toShape(options?: SdfToShapeOptions): Shape;\n /** Set the display color for this implicit leaf. */\n color(value: string | undefined): SdfShape;\n /** Set PBR display material properties for this implicit leaf. */\n material(props: ShapeMaterialProps): SdfShape;\n /** Set explicit preview/meshing bounds for this implicit leaf. */\n bounds(bounds: SdfBounds | [\n Vec3$1,\n Vec3$1\n ]): SdfShape;\n bounds(min: Vec3$1, max: Vec3$1): SdfShape;\n /** Sculpt-style alias for translate(). */\n at(x: number, y: number, z: number): SdfShape;\n /** Sculpt-style alias for translate(). */\n move(x: number, y: number, z: number): SdfShape;\n /** Sculpt-style alias for rotateZ(). */\n spin(angleDeg: number): SdfShape;\n /** Sculpt-style tilt around X, Y, Z, or a custom axis. */\n tilt(angleDeg: number, axis?: "x" | "y" | "z" | Vec3$1): SdfShape;\n /** Sculpt-style rounded-box helper. Currently applies directly to primitive SDF boxes. */\n round(radius: number): SdfShape;\n /** Sculpt-style smooth blend with another implicit shape. */\n blend(other: SdfShape, options?: number | {\n radius?: number;\n }): SdfShape;\n /** Sculpt-style alias for blend(). */\n goop(other: SdfShape, options?: number | {\n radius?: number;\n }): SdfShape;\n /** Sculpt-style smooth carve/subtract. */\n carve(other: SdfShape, options?: number | {\n radius?: number;\n }): SdfShape;\n /** Sculpt-style smooth intersection/keep operation. */\n keep(other: SdfShape, options?: number | {\n radius?: number;\n }): SdfShape;\n /** Apply a Sculpt material preset or direct material props. */\n polish(input?: SculptPolishInput): SdfShape;\n /** SDF union (sharp). */\n union(...others: SdfShape[]): SdfShape;\n /** SDF difference (sharp) — subtracts others from this. */\n subtract(...others: SdfShape[]): SdfShape;\n /** SDF intersection (sharp). */\n intersect(...others: SdfShape[]): SdfShape;\n /** Clip this SDF to an explicit box-shaped design space. */\n clipBox(x: number, y: number, z: number): SdfShape;\n /** Smooth union — blends shapes together with a smooth radius. */\n smoothUnion(other: SdfShape, radius: number): SdfShape;\n /** Smooth difference — smoothly carves other from this. */\n smoothSubtract(other: SdfShape, radius: number): SdfShape;\n /** Smooth intersection — smoothly intersects. */\n smoothIntersect(other: SdfShape, radius: number): SdfShape;\n /** Morph between this shape and another. t=0 → this, t=1 → other. */\n morph(other: SdfShape, t: number): SdfShape;\n /** Translate this SDF by the given offsets in millimeters. */\n translate(x: number, y: number, z: number): SdfShape;\n /** Rotate around an arbitrary axis through the origin. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number): SdfShape;\n /** Rotate around the X axis by the given angle in degrees. */\n rotateX(angleDeg: number): SdfShape;\n /** Rotate around the Y axis by the given angle in degrees. */\n rotateY(angleDeg: number): SdfShape;\n /** Rotate around the Z axis by the given angle in degrees. */\n rotateZ(angleDeg: number): SdfShape;\n /** Uniformly scale this SDF around the origin. */\n scale(factor: number): SdfShape;\n /** Twist around the Z axis. */\n twist(degreesPerUnit: number): SdfShape;\n /** Bend around the Z axis with given radius. */\n bend(radius: number): SdfShape;\n /** Repeat in space. Spacing of 0 on an axis means no repetition. Count of 0 = infinite. */\n repeat(spacing: Vec3$1, count?: Vec3$1): SdfShape;\n /** Hollow out, keeping only a shell of given thickness. */\n shell(thickness: number): SdfShape;\n /**\n * Displace the surface by a function of position, or by a pattern SdfShape.\n *\n * ```js\n * // Function displacement\n * shape.displace((x, y, z) => Math.sin(x) * 0.5)\n *\n * // Pattern displacement (e.g. basketWeave)\n * shape.displace(sdf.basketWeave({ threads: 16, spacing: 3 }))\n * ```\n */\n displace(fn: ((x: number, y: number, z: number) => number) | SdfShape, constants?: Record<string, number>): SdfShape;\n /**\n * Displace the surface using a 2D pattern in surface-local UV coordinates.\n *\n * Automatically detects the shape\'s UV parametrization (sphere, cylinder, torus)\n * from the SDF tree. Falls back to triplanar mapping for arbitrary shapes.\n *\n * UV coordinates are in **surface millimeters** — patterns defined with `spacing: 3`\n * always produce 3mm spacing, regardless of shape size.\n *\n * ```js\n * // Surface-following basket weave — auto-detects sphere UV\n * sdf.sphere(27).shell(3)\n * .surfaceDisplace(sdf.basketWeave({ spacing: 3, depth: 0.8 }))\n * .toShape()\n *\n * // Custom 2D pattern via function\n * shape.surfaceDisplace((u, v) => -Math.sin(u * 2) * 0.3)\n * ```\n */\n surfaceDisplace(pattern: SurfacePattern | ((u: number, v: number) => number), options?: SurfaceDisplaceOptions): SdfShape;\n /** Create concentric onion layers. */\n onion(layers: number, thickness: number): SdfShape;\n}\ndeclare function sphere(radius: number): SdfShape;\ndeclare function box(x: number, y: number, z: number): SdfShape;\ndeclare function cylinder(height: number, radius: number): SdfShape;\ndeclare function torus(majorRadius: number, minorRadius: number): SdfShape;\ndeclare function capsule(height: number, radius: number): SdfShape;\ndeclare function cone(height: number, radius: number): SdfShape;\ndeclare function smoothUnion(a: SdfShape, b: SdfShape, options: {\n radius: number;\n}): SdfShape;\ndeclare function smoothDifference(a: SdfShape, b: SdfShape, options: {\n radius: number;\n}): SdfShape;\ndeclare function smoothIntersection(a: SdfShape, b: SdfShape, options: {\n radius: number;\n}): SdfShape;\ndeclare function morph(a: SdfShape, b: SdfShape, t: number): SdfShape;\ninterface BlendOptions {\n /** Optional named constants accessible in the blend function. */\n constants?: Record<string, number>;\n}\ndeclare function blend(a: SdfShape, b: SdfShape, fn: (x: number, y: number, z: number) => number, options?: BlendOptions): SdfShape;\ninterface TpmsOptions {\n cellSize: number;\n /**\n * Legacy dimensionless field threshold. Kept for compatibility.\n * Prefer `wallThickness` for approximate millimeter units.\n */\n thickness?: number;\n /** Approximate physical wall thickness in millimeters. */\n wallThickness?: number;\n /** Override TPMS thickness interpretation. Defaults to metric when `wallThickness` is used, legacy when `thickness` is used. */\n tpmsThicknessMode?: TpmsThicknessMode;\n}\ninterface TpmsBlockOptions extends TpmsOptions {\n type?: "gyroid" | "schwarzP" | "diamond" | "lidinoid";\n size: Vec3$1;\n}\ndeclare function gyroid(options: TpmsOptions): SdfShape;\ndeclare function schwarzP(options: TpmsOptions): SdfShape;\ndeclare function diamond(options: TpmsOptions): SdfShape;\ndeclare function lidinoid(options: TpmsOptions): SdfShape;\ndeclare function withinBox(shape: SdfShape, options: {\n size: Vec3$1;\n}): SdfShape;\ndeclare function tpmsBlock(options: TpmsBlockOptions): SdfShape;\ninterface NoiseOptions {\n /** Spatial frequency — smaller = larger features. Default: 0.1 */\n scale?: number;\n /** Peak displacement amplitude. Default: 1 */\n amplitude?: number;\n /** fBm octaves (1 = plain simplex, higher = more detail). Default: 1 */\n octaves?: number;\n /** Seed for deterministic variation. Default: 0 */\n seed?: number;\n}\ndeclare function noise(options?: NoiseOptions): SdfShape;\ninterface VoronoiOptions {\n /** Size of each Voronoi cell in world units. Default: 10 */\n cellSize?: number;\n /** Wall thickness between cells. Default: 1 */\n wallThickness?: number;\n /** Seed for deterministic variation. Default: 0 */\n seed?: number;\n /**\n * Projection weight for membrane suppression (0..1). Controls how much of\n * the surface-normal distance component is removed from Voronoi cell distances.\n * 0 = no projection (classic 3D voronoi with membranes).\n * 1 = full tangent-plane projection (pure 2D pattern on surface).\n * Default: 0.85. Only active when voronoi is intersected with another shape.\n */\n suppressionThreshold?: number;\n}\ndeclare function voronoi(options?: VoronoiOptions): SdfShape;\ninterface HoneycombOptions {\n /** Size of each hex cell. Default: 8 */\n cellSize?: number;\n /** Wall thickness. Default: 1 */\n wallThickness?: number;\n}\ndeclare function honeycomb(options?: HoneycombOptions): SdfShape;\ninterface WavesOptions {\n /** Distance between wave peaks. Default: 10 */\n wavelength?: number;\n /** Height of waves. Default: 1 */\n amplitude?: number;\n /** Axis along which waves propagate: \'x\', \'y\', or \'z\'. Default: \'x\' */\n axis?: "x" | "y" | "z";\n}\ndeclare function waves(options?: WavesOptions): SdfShape;\ninterface KnurlOptions {\n /** Distance between knurl ridges. Default: 3 */\n pitch?: number;\n /** Depth of knurl grooves. Default: 0.5 */\n depth?: number;\n /** Helix angle in degrees. Default: 30 */\n angle?: number;\n}\ndeclare function knurl(options?: KnurlOptions): SdfShape;\ninterface PerforatedOptions {\n /** Hole radius. Default: 3 */\n radius?: number;\n /** Center-to-center spacing. Default: 8 */\n spacing?: number;\n}\ndeclare function perforated(options?: PerforatedOptions): SdfShape;\ninterface ScalesOptions {\n /** Scale diameter. Default: 5 */\n size?: number;\n /** How much scales protrude. Default: 0.8 */\n depth?: number;\n}\ndeclare function scales(options?: ScalesOptions): SdfShape;\ninterface BrickOptions {\n /** Brick width. Default: 10 */\n width?: number;\n /** Brick height. Default: 5 */\n height?: number;\n /** Mortar groove depth. Default: 0.5 */\n depth?: number;\n /** Mortar gap width. Default: 1 */\n mortar?: number;\n}\ndeclare function brick(options?: BrickOptions): SdfShape;\ninterface WeaveOptions {\n /** Thread center-to-center spacing (for intersection patterns). Default: 5 */\n spacing?: number;\n /** Thread half-width. Default: 1 */\n threadRadius?: number;\n}\ndeclare function weave(options?: WeaveOptions): SdfShape;\ninterface BasketWeaveOptions {\n /** Spacing between threads in mm (both directions). Default: 3 */\n spacing?: number;\n /** Thread width in mm. Default: 1.5 */\n threadWidth?: number;\n /** Thread protrusion depth in mm. Default: 0.8 */\n depth?: number;\n}\ndeclare function basketWeave(options?: BasketWeaveOptions): SurfacePattern;\ndeclare function fromFunction(fn: SdfFunctionSource, options: SdfFunctionOptions): SdfShape;\ndeclare function twist(shape: SdfShape, degreesPerUnit: number): SdfShape;\ndeclare function bend(shape: SdfShape, radius: number): SdfShape;\ndeclare function repeat(shape: SdfShape, spacing: Vec3$1, count?: Vec3$1): SdfShape;\n/**\n * ForgeCAD — Scene Configuration API\n *\n * Lets .forge.js scripts control camera, lighting, background, fog,\n * and post-processing for generative art and presentation renders.\n *\n * Follows the collect-during-execution pattern used by viewConfig, cutPlane, etc.\n */\ninterface SceneCameraConfig {\n position?: [\n number,\n number,\n number\n ];\n target?: [\n number,\n number,\n number\n ];\n up?: [\n number,\n number,\n number\n ];\n fov?: number;\n type?: "perspective" | "orthographic";\n}\ntype SceneLightType = "ambient" | "directional" | "point" | "spot" | "hemisphere";\ninterface SceneLightConfig {\n type: SceneLightType;\n color?: string;\n intensity?: number;\n position?: [\n number,\n number,\n number\n ];\n /** Target for directional/spot lights */\n target?: [\n number,\n number,\n number\n ];\n /** Ground color for hemisphere lights */\n groundColor?: string;\n /** Sky color alias for hemisphere lights (same as color) */\n skyColor?: string;\n /** Spot light cone angle in radians */\n angle?: number;\n /** Spot light penumbra (0–1) */\n penumbra?: number;\n /** Point/spot light decay */\n decay?: number;\n /** Point/spot light distance (0 = infinite) */\n distance?: number;\n /** Whether this light casts shadows */\n castShadow?: boolean;\n}\ninterface SceneEnvironmentConfig {\n /** Built-in preset name or \'none\' to disable */\n preset?: "studio" | "sunset" | "dawn" | "warehouse" | "forest" | "apartment" | "lobby" | "city" | "park" | "night" | "none";\n /** Environment map intensity */\n intensity?: number;\n /** Use environment map as scene background */\n background?: boolean;\n}\ninterface SceneBackgroundGradient {\n top: string;\n bottom: string;\n}\ninterface SceneFogConfig {\n color?: string;\n /** Linear fog near distance */\n near?: number;\n /** Linear fog far distance */\n far?: number;\n /** Exponential fog density (if set, uses FogExp2 instead of linear Fog) */\n density?: number;\n}\ninterface SceneBloomConfig {\n intensity?: number;\n threshold?: number;\n radius?: number;\n}\ninterface SceneVignetteConfig {\n darkness?: number;\n offset?: number;\n}\ninterface SceneGrainConfig {\n intensity?: number;\n}\ninterface ScenePostProcessingConfig {\n bloom?: SceneBloomConfig;\n vignette?: SceneVignetteConfig;\n grain?: SceneGrainConfig;\n toneMappingExposure?: number;\n}\ninterface SceneGroundConfig {\n /** Show a ground plane */\n visible?: boolean;\n /** Ground color */\n color?: string;\n /** Offset below the model\'s bounding box minimum Z. Default 0 (flush with model bottom). */\n offset?: number;\n /** Receive shadows on the ground */\n receiveShadow?: boolean;\n}\ninterface SceneCaptureConfig {\n /** Frames for one full orbit rotation (default: 72) */\n framesPerTurn?: number;\n /** Frozen frames before motion starts (default: 6) */\n holdFrames?: number;\n /** Orbit pitch angle in degrees (default: auto from camera) */\n pitchDeg?: number;\n /** Output frame rate (default: 24) */\n fps?: number;\n /** Output frame size in pixels (default: 960) */\n size?: number;\n /** Canvas background color for capture (default: \'#252526\') */\n background?: string;\n}\ninterface SceneOptions {\n background?: string | SceneBackgroundGradient;\n camera?: SceneCameraConfig;\n lights?: SceneLightConfig[];\n environment?: SceneEnvironmentConfig;\n fog?: SceneFogConfig;\n postProcessing?: ScenePostProcessingConfig;\n ground?: SceneGroundConfig;\n /** Default capture parameters for `forgecad capture` — CLI flags override these. */\n capture?: SceneCaptureConfig;\n}\n/**\n * Configure the scene environment for the current script execution.\n *\n * **Details**\n *\n * Controls camera position, lighting rig, background color or gradient, atmospheric fog,\n * environment maps, post-processing effects, and capture parameters for the `forgecad capture`\n * command. Multiple calls merge — later values override earlier ones on a per-key basis, so you\n * can split configuration across multiple `scene()` calls.\n *\n * When `lights` is specified, **all** default lights are removed. You must include your own\n * ambient light or the scene will be fully dark.\n *\n * Setting `camera.position` overrides auto-framing — the viewport will no longer auto-fit the\n * geometry on script reload.\n *\n * Post-processing effects (`bloom`, `vignette`, `grain`) work in the browser viewport only. The\n * CLI applies camera, lights, background, fog, and `toneMappingExposure` but skips shader effects.\n *\n * All numeric values accept `param()` expressions.\n *\n * **Example**\n *\n * ```js\n * scene({\n * background: { top: \'#000814\', bottom: \'#001d3d\' },\n * camera: { position: [160, -120, 100], target: [0, 0, 50], fov: 52 },\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 * @param options.background - Solid color hex string or `{ top, bottom }` vertical gradient\n * @param options.camera - Camera override: `position` `[x,y,z]`, `target` `[x,y,z]`, `fov` (degrees 1–179), `up` `[x,y,z]`, `type` (`\'perspective\' | \'orthographic\'`)\n * @param options.lights - Full light array (replaces all defaults). Each entry: `type` (`\'ambient\' | \'directional\' | \'point\' | \'spot\' | \'hemisphere\'`), `color`, `intensity`, `position`, `target`, `castShadow`. Hemisphere accepts `skyColor`/`groundColor`. Spot/point accept `angle`, `penumbra`, `decay`, `distance`\n * @param options.environment - Built-in HDRI preset: `preset` (`\'studio\' | \'sunset\' | \'dawn\' | \'warehouse\' | \'forest\' | \'apartment\' | \'lobby\' | \'city\' | \'park\' | \'night\' | \'none\'`), `intensity`, `background`\n * @param options.fog - Atmospheric fog: `color`, `near`/`far` (linear) or `density` (exponential FogExp2)\n * @param options.postProcessing - Shader effects: `bloom` `{ intensity?, threshold?, radius? }`, `vignette` `{ darkness?, offset? }`, `grain` `{ intensity? }`, `toneMappingExposure`\n * @param options.ground - Ground plane: `visible`, `color`, `offset` (below model bottom), `receiveShadow`\n * @param options.capture - Capture defaults for `forgecad capture`: `framesPerTurn` (12–720, default 72), `holdFrames` (0–300, default 6), `pitchDeg` (-80–80), `fps` (1–60, default 24), `size` (pixels, default 960), `background`\n * @returns void\n * @category Scene Configuration\n */\ndeclare function scene(options: SceneOptions): void;\ninterface JointOverlayViewConfigOptions {\n enabled?: boolean;\n axisColor?: string;\n axisCoreColor?: string;\n arcColor?: string;\n zeroColor?: string;\n arcVisualLimitDeg?: number;\n axisLengthScale?: number;\n axisLengthMin?: number;\n axisLineRadiusScale?: number;\n axisLineRadiusMin?: number;\n axisLineRadiusMax?: number;\n spokeLineRadiusScale?: number;\n spokeLineRadiusMin?: number;\n spokeLineRadiusMax?: number;\n arcLineRadiusScale?: number;\n arcLineRadiusMin?: number;\n arcLineRadiusMax?: number;\n axisDotRadiusScale?: number;\n axisDotRadiusMin?: number;\n axisArrowRadiusScale?: number;\n axisArrowRadiusMin?: number;\n axisArrowLengthScale?: number;\n axisArrowLengthMin?: number;\n axisArrowOffsetFactor?: number;\n arcRadiusScale?: number;\n arcRadiusMin?: number;\n arcDotRadiusScale?: number;\n arcDotRadiusMin?: number;\n arcArrowRadiusScale?: number;\n arcArrowRadiusMin?: number;\n arcArrowLengthScale?: number;\n arcArrowLengthMin?: number;\n arcArrowOffsetFactor?: number;\n arcStepDeg?: number;\n arcMinSteps?: number;\n arcTubeSegmentsMin?: number;\n arcTubeSegmentsFactor?: number;\n arcTubeRadialSegments?: number;\n}\ninterface ViewConfigOptions {\n jointOverlay?: JointOverlayViewConfigOptions;\n}\n/**\n * Configure viewport helper visuals for the current script execution.\n *\n * **Details**\n *\n * Controls renderer-only overlays that appear in the viewport but are not part of the geometry.\n * Currently supports the joint overlay that renders axis arrows and arc indicators when\n * `jointsView` is active. Multiple calls merge — later values override earlier ones per key.\n *\n * This does **not** trigger a geometry recompute; it only affects the visual helpers drawn on\n * top of the 3D scene.\n *\n * **Example**\n *\n * ```js\n * viewConfig({\n * jointOverlay: {\n * axisColor: \'#13dfff\',\n * arcColor: \'#ff7a1a\',\n * axisLineRadiusScale: 0.03,\n * arcLineRadiusScale: 0.022,\n * },\n * });\n * ```\n *\n * @param options.jointOverlay - Joint overlay appearance. Supports `enabled`, `axisColor`, `axisCoreColor`, `arcColor`, `zeroColor`, `axisLengthScale`, `arcVisualLimitDeg`, `arcStepDeg`, and detailed radius/arrow sizing properties\n * @returns void\n * @category Scene Configuration\n */\ndeclare function viewConfig(options?: ViewConfigOptions): void;\ntype SketchFaceTarget = SketchFace3D | string | FaceRef;\ninterface ShapeFeatureExtentSideOptions {\n depth?: number;\n upToFace?: SketchFaceTarget | FaceRef;\n through?: boolean;\n}\ninterface ShapeFeatureExtentOptions {\n forward: ShapeFeatureExtentSideOptions;\n reverse?: ShapeFeatureExtentSideOptions;\n}\ninterface ShapeHoleThreadOptions {\n designation?: string;\n pitch?: number;\n class?: string;\n handedness?: "right" | "left";\n depth?: number;\n modeled?: boolean;\n}\ninterface ShapeHoleOptions {\n diameter: number;\n depth?: number;\n upToFace?: SketchFaceTarget | FaceRef;\n extent?: ShapeFeatureExtentOptions;\n u?: number;\n v?: number;\n counterbore?: {\n diameter: number;\n depth: number;\n };\n countersink?: {\n diameter: number;\n angleDeg?: number;\n };\n thread?: ShapeHoleThreadOptions;\n}\ninterface ShapeCutoutOptions {\n depth?: number;\n upToFace?: SketchFaceTarget | FaceRef;\n extent?: ShapeFeatureExtentOptions;\n taperScale?: number | [\n number,\n number\n ];\n}\ninterface Shape {\n /**\n * Drill a hole into this solid at a face.\n *\n * @param faceOrRef Target face — a face selector string (e.g. `\'top\'`), a placed sketch, or a `FaceRef`.\n * @param opts Hole parameters: `diameter` (required), optional `depth`, `counterbore`, `countersink`, `thread`, `u`/`v` offset, `upToFace`, `extent`.\n *\n * @example\n * box(50, 50, 20).hole(\'top\', { diameter: 8, depth: 10 })\n * box(50, 50, 20).hole(\'top\', { diameter: 6, counterbore: { diameter: 12, depth: 3 } })\n */\n hole(faceOrRef: SketchFaceTarget | FaceRef, opts: ShapeHoleOptions): Shape;\n /**\n * Cut a profile-shaped pocket through a face using a placed sketch.\n *\n * The sketch must be placed on a face with `Sketch.onFace(...)`. The cut follows the sketch\'s 2D profile.\n *\n * @param sketch Sketch placed on a face via `.onFace()`.\n * @param opts Optional `depth`, `upToFace`, `extent`, `taperScale`.\n *\n * @example\n * const profile = circle2d(10).onFace(body, \'top\');\n * body.cutout(profile, { depth: 5 })\n */\n cutout(sketch: Sketch, opts?: ShapeCutoutOptions): Shape;\n}\ninterface PocketOptions {\n /**\n * Shrink the face boundary inward by this many mm before extruding.\n * Produces angled walls when combined with depth. Default: 0 (full face).\n */\n inset?: number;\n /**\n * Scale the face profile uniformly (e.g. 0.8 = 80% of the face area).\n * Mutually exclusive with `inset`; `inset` takes precedence if both are set.\n */\n scale?: number;\n /** Corner join style when using `inset`. Default: \'Round\'. */\n join?: "Square" | "Round" | "Miter";\n}\ntype BossOptions = PocketOptions;\ninterface Shape {\n /**\n * Cut a pocket (cavity) into this solid through the named face.\n *\n * @param face Which face to cut into — e.g. `\'top\'`, `\'front\'`, or a face query.\n * @param depth How deep the pocket goes into the solid (mm).\n * @param opts Optional `inset` (shrink boundary, mm), `scale` (uniform scale, e.g. 0.8), `join` (`\'Round\'`/`\'Square\'`/`\'Miter\'`).\n *\n * @example\n * box(100, 100, 20).pocket(\'top\', 8)\n * box(100, 100, 20).pocket(\'top\', 8, { inset: 5 })\n * box(100, 100, 20).pocket(\'top\', 8, { scale: 0.8 })\n */\n pocket(face: FaceSelector, depth: number, opts?: PocketOptions): Shape;\n /**\n * Add a boss (protrusion) from the named face.\n *\n * @param face Which face to protrude from — e.g. `\'top\'`, `\'front\'`, or a face query.\n * @param height Height of the protrusion above the face (mm).\n * @param opts Optional `inset` (shrink boundary, mm), `scale` (uniform scale, e.g. 0.8), `join` (`\'Round\'`/`\'Square\'`/`\'Miter\'`).\n *\n * @example\n * box(100, 100, 20).boss(\'top\', 5)\n * box(100, 100, 20).boss(\'top\', 10, { scale: 0.6 })\n */\n boss(face: FaceSelector, height: number, opts?: BossOptions): Shape;\n}\ntype Vec3$2 = [\n number,\n number,\n number\n];\ntype SlicePlane = "xy" | "xz" | "yz" | Vec3$2;\ninterface FromSlicesSlice {\n /** Plane normal: axis name or arbitrary unit vector. */\n on: SlicePlane;\n /** Signed offset along the normal from the origin. */\n at: number;\n /** 2D cross-section profile on that plane. */\n profile: Sketch;\n}\ninterface FromSlicesOptions {\n /** Marching-grid edge length for level-set meshing (Manifold only). */\n edgeLength?: number;\n /** Extra bounding-box padding (Manifold only). */\n boundsPadding?: number;\n}\nexport namespace Shape {\n /**\n * Construct a 3D shape from cross-section slices on one or more planes.\n *\n * Slices with the same normal direction are lofted together. Slices with\n * different normals are combined via smooth radial blending — each\n * silhouette constrains the shape\'s extent, producing smooth ellipsoidal\n * cross-sections.\n *\n * @example\n * ```js\n * // Egg from two orthogonal silhouettes\n * const eggProfile = ellipse(15, 25);\n * return Shape.fromSlices([\n * { on: \'xz\', at: 0, profile: eggProfile },\n * { on: \'yz\', at: 0, profile: eggProfile },\n * ]);\n * ```\n *\n * @example\n * ```js\n * // Vase with cross-section transitions\n * return Shape.fromSlices([\n * { on: \'xy\', at: 0, profile: circle2d(20) },\n * { on: \'xy\', at: 40, profile: rect(25, 25) },\n * { on: \'xy\', at: 80, profile: circle2d(8) },\n * { on: \'xz\', at: 0, profile: vaseOutline },\n * ]);\n * ```\n */\n function fromSlices(slices: FromSlicesSlice[], options?: FromSlicesOptions): Shape;\n}\ntype ExplodeAxis = "x" | "y" | "z";\ntype ExplodeDirection = "radial" | ExplodeAxis | [\n number,\n number,\n number\n];\ninterface ExplodeDirective {\n /** Multiplier applied to `amount` for this node */\n stage?: number;\n /** Direction mode for this node */\n direction?: ExplodeDirection;\n /** Optional axis lock after direction is resolved */\n axisLock?: ExplodeAxis;\n}\ninterface ExplodeConfigOptions {\n amount?: number;\n stages?: number[];\n mode?: ExplodeDirection;\n axisLock?: ExplodeAxis;\n byName?: Record<string, ExplodeDirective>;\n byPath?: Record<string, ExplodeDirective>;\n}\ntype ExplodeViewDirection = ExplodeDirection;\ninterface ExplodeViewDirective extends ExplodeDirective {\n}\ninterface ExplodeViewOptions {\n /** Set false to disable viewport explode offsets for this script output. */\n enabled?: boolean;\n /** Scales the UI explode amount. Default: 1 */\n amountScale?: number;\n /**\n * Per-depth stage multipliers (depth 1 = first level).\n * If depth exceeds this array, the last value is reused.\n * Default when omitted: reciprocal depth (1, 1/2, 1/3, ...)\n */\n stages?: number[];\n /** Global direction mode fallback. Default: \'radial\' */\n mode?: ExplodeViewDirection;\n /** Global axis lock fallback. */\n axisLock?: ExplodeAxis;\n /** Per-object overrides by final object name. */\n byName?: Record<string, ExplodeViewDirective>;\n /** Per-tree-path overrides using slash-separated object tree segments. */\n byPath?: Record<string, ExplodeViewDirective>;\n}\n/**\n * Configure how the viewport explode slider offsets returned objects.\n *\n * **Details**\n *\n * Offsets are resolved from the returned object tree, not a flat list. In `radial` mode each\n * node follows its parent branch direction, then fans locally from the immediate parent\n * center — nested assemblies peel apart level by level. In fixed-axis or fixed-vector modes,\n * the branch follows that axis/vector but nested descendants fan out perpendicular by default.\n *\n * Multiple calls merge — later values override earlier ones on a per-key basis. `byName` and\n * `byPath` maps are merged entry-by-entry.\n *\n * For programmatic explode applied before returning (without the slider), use `lib.explode()`\n * instead.\n *\n * **Example**\n *\n * ```js\n * explodeView({\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 * @param options.enabled - Set `false` to disable viewport explode offsets for this script\n * @param options.amountScale - Multiplier applied to the UI slider amount. Default: 1\n * @param options.stages - Per-depth stage multipliers (depth 1 = first level). If depth exceeds the array, the last value is reused. Default: reciprocal depth (`1, 1/2, 1/3, ...`)\n * @param options.mode - Global direction mode: `\'radial\'` | `\'x\'` | `\'y\'` | `\'z\'` | `[x, y, z]`. Default: `\'radial\'`\n * @param options.axisLock - Constrain motion to a single world axis: `\'x\'` | `\'y\'` | `\'z\'`\n * @param options.byName - Per-object overrides keyed by final returned object name: `{ stage?, direction?, axisLock? }`\n * @param options.byPath - Per-tree-path overrides using slash-separated path segments (e.g. `\'Drive/Shaft\'`): `{ stage?, direction?, axisLock? }`\n * @returns void\n * @see {@link explode} for applying explode offsets programmatically before returning geometry\n * @category Explode View\n */\ndeclare function explodeView(options?: ExplodeViewOptions): void;\ntype JointViewType = "revolute" | "prismatic";\ntype JointViewAxis = [\n number,\n number,\n number\n];\ninterface JointViewInput {\n name: string;\n child: string;\n parent?: string;\n type?: JointViewType;\n axis?: JointViewAxis;\n pivot?: [\n number,\n number,\n number\n ];\n min?: number;\n max?: number;\n default?: number;\n unit?: string;\n hidden?: boolean;\n}\ninterface JointViewAnimationKeyframeInput {\n /** Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights. */\n at?: number;\n /** 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 ticks?: number;\n values: Record<string, number>;\n}\ninterface JointViewAnimationInput {\n name: string;\n duration?: number;\n loop?: boolean;\n continuous?: boolean;\n keyframes: JointViewAnimationKeyframeInput[];\n}\ninterface JointViewCouplingTermInput {\n joint: string;\n ratio?: number;\n}\ninterface JointViewCouplingInput {\n joint: string;\n terms: JointViewCouplingTermInput[];\n offset?: number;\n}\ninterface JointsViewOptions {\n enabled?: boolean;\n joints?: JointViewInput[];\n couplings?: JointViewCouplingInput[];\n animations?: JointViewAnimationInput[];\n defaultAnimation?: string;\n}\n/**\n * Register viewport-only mechanism controls that animate returned objects without re-running the script.\n *\n * **Details**\n *\n * Defines joints (revolute or prismatic), optional gear/rack couplings, and named animations.\n * The viewport resolves transforms through the joint chain at display time — the script geometry\n * is computed only once at rest pose.\n *\n * **Critical:** Solve the assembly at **rest pose** (all animated joints = 0). The viewport\n * applies `jointsView` transforms on top of the returned scene. If geometry is already solved\n * at non-zero angles, animation will double-rotate everything.\n *\n * ```js\n * // BAD — double rotation\n * const solved = mech.solve({ shoulder: 45, elbow: 30 });\n * jointsView({ joints: [{ name: \'shoulder\', ... }] });\n * return solved;\n *\n * // GOOD — rest pose, jointsView controls all posing\n * const solved = mech.solve({ shoulder: 0, elbow: 0 });\n * jointsView({\n * joints: [\n * { name: \'shoulder\', child: \'Upper Arm\', default: 45, ... },\n * { name: \'elbow\', child: \'Forearm\', parent: \'Upper Arm\', default: 30, ... },\n * ],\n * });\n * return solved;\n * ```\n *\n * **Pivot coordinates** are world-space positions of each joint origin at rest pose. For\n * `addRevolute(\'shoulder\', \'Base\', \'Link\', { frame: Transform.identity().translate(0, 0, 20) })`\n * 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\n * joint in the chain:\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 *\n * Animation values are interpolated linearly between keyframes. ForgeCAD does **not** auto-wrap\n * revolute values across `-180/180`. Keep keyframe values continuous — a `-180 -> 171` jump\n * spins the part the long way around. Use `-180 -> -189` instead. Author high-speed multi-turn\n * 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 * ```js\n * keyframes: [\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 * Mixing explicit `at` and omitted `at` in the same animation is not allowed.\n *\n * **Example**\n *\n * ```js\n * jointsView({\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 * @param options.enabled - Set `false` to hide all joint controls\n * @param options.joints - Joint definitions: `{ name, child, parent?, type?, axis?, pivot?, min?, max?, default?, unit?, hidden? }[]`. `min`/`max` control the slider range only — they do **not** clamp animation keyframe values\n * @param options.couplings - Gear/rack couplings: `{ joint, terms: [{ joint, ratio? }][], offset? }[]`. Coupled joints cannot appear as animation keyframe targets\n * @param options.animations - Named animations: `{ name, duration?, loop?, continuous?, keyframes }[]`\n * @param options.defaultAnimation - Name of the animation to play on load\n * @returns void\n * @category Animation\n */\ndeclare function jointsView(options?: JointsViewOptions): void;\ninterface BomOpts {\n /**\n * Quantity unit label, e.g. "mm", "pieces", "kg".\n * Default: "pieces"\n */\n unit?: string;\n /**\n * Optional explicit grouping key used during report aggregation.\n */\n key?: string;\n /** Material name, e.g. "steel", "birch plywood", "nylon" */\n material?: string;\n /** Overall dimensions `[width, height]` or `[width, height, thickness]` in the entry\'s unit */\n dimensions?: number[];\n /** Cross-section dimensions `[w, h]` for tubes and profiles */\n section?: number[];\n /** Wall thickness for hollow sections (mm) */\n wall?: number;\n /** Diameter for round stock, bolts, dowels (mm) */\n diameter?: number;\n /** Length for fasteners (mm) */\n length?: number;\n /** Manufacturing process, e.g. "laser cut", "CNC", "welded" */\n process?: string;\n /** Free-form notes */\n notes?: string;\n /** Wood grain direction, e.g. "long", "cross" */\n grain?: string;\n /** Extensible for domain-specific fields */\n [key: string]: unknown;\n}\n/**\n * Register a Bill of Materials entry for report export.\n *\n * **Details**\n *\n * BOM entries are accumulated during script execution and exported alongside\n * the model in report views. Rows are grouped by normalized\n * `description + unit`. Pass an explicit `key` to force multiple descriptions\n * to collapse into a single line item.\n *\n * - `quantity` must be a finite number `>= 0`. A quantity of `0` is silently\n * 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\n * covers per-part assembly metadata; this function is for free-form\n * purchased-item annotation.\n *\n * **Example**\n *\n * ```ts\n * const tubeLen = param("Tube Length", 1200, { min: 300, max: 4000, unit: "mm" });\n * const boltCount = param("Bolt Count", 16, { min: 0, max: 200, integer: true });\n *\n * bom(tubeLen, "iron tube 30 x 20", { unit: "mm" });\n * bom(boltCount, "M4 bolt, 16 mm length");\n * bom(4, "rubber foot", { key: "foot-rubber" }); // explicit aggregation key\n *\n * // Structured metadata for richer reports:\n * bom(tubeLen, "rectangular steel tube", {\n * unit: "mm",\n * material: "steel",\n * section: [30, 20],\n * wall: 3,\n * });\n * ```\n *\n * @param quantity - Quantity of the item; must be finite and `>= 0`\n * @param description - Human-readable item text, e.g. `"M4 bolt, 16 mm length"`\n * @param opts - Optional `unit` label (default `"pieces"`), aggregation `key`, and structured metadata (`material`, `section`, `wall`, `diameter`, `length`, `process`, `notes`, `grain`, etc.)\n * @category Bill of Materials\n */\ndeclare function bom(quantity: number, description: string, opts?: BomOpts): void;\ntype CutPlaneExcludeInput = string | string[];\ninterface CutPlaneOptions {\n /** Optional offset along the plane normal (primarily for object-form overload). */\n offset?: number;\n /** Object names to keep uncut for this plane. */\n exclude?: CutPlaneExcludeInput;\n}\n/**\n * Define a named section plane for inspecting internal geometry.\n *\n * **Details**\n *\n * Registers a cut plane that appears as a toggle in the viewport View Panel. When enabled,\n * geometry on the positive side of the plane (the side the normal points toward) is clipped away,\n * revealing the internal cross-section. The newly exposed section faces render with a hatched\n * overlay; pre-existing coplanar boundary faces are left unhatched.\n *\n * Planes are registered once per script run. The viewport toggle state (on/off) persists across\n * parameter changes without re-running the script. The `exclude` option only works correctly when\n * the excluded object names are stable across parameter changes.\n *\n * Accepts two overloads: `cutPlane(name, normal, offset?, options?)` or\n * `cutPlane(name, normal, options?)` where options may include `offset`.\n *\n * **Example**\n *\n * ```js\n * const cutZ = param(\'Cut Height\', 10, { min: -50, max: 50, unit: \'mm\' });\n * cutPlane(\'Inspection\', [0, 0, 1], cutZ, { exclude: [\'Probe\', \'Fasteners\'] });\n * ```\n *\n * @param name - Display label shown in the viewport View Panel\n * @param normal - Plane normal `[x, y, z]` — geometry on this side is clipped away\n * @param offset - Distance from origin along the normal. Default: 0\n * @param options.exclude - Object name(s) to keep uncut for this plane\n * @returns void\n * @category Cut Plane\n */\ndeclare function cutPlane(name: string, normal: [\n number,\n number,\n number\n], offset?: number, options?: CutPlaneOptions): void;\ndeclare function cutPlane(name: string, normal: [\n number,\n number,\n number\n], options?: CutPlaneOptions): void;\ninterface RobotLinkExportOptions {\n massKg?: number;\n densityKgM3?: number;\n collision?: "visual" | "convex" | "box" | "none";\n}\ninterface RobotJointExportOptions {\n effort?: number;\n velocity?: number;\n damping?: number;\n friction?: number;\n}\ninterface RobotDiffDrivePluginOptions {\n leftJoints: string[];\n rightJoints: string[];\n wheelSeparationMm: number;\n wheelRadiusMm: number;\n topic?: string;\n odomTopic?: string;\n tfTopic?: string;\n frameId?: string;\n odomFrameId?: string;\n maxLinearVelocity?: number;\n maxAngularVelocity?: number;\n linearAcceleration?: number;\n angularAcceleration?: number;\n}\ninterface RobotJointStatePublisherOptions {\n enabled?: boolean;\n joints?: string[];\n topic?: string;\n updateRate?: number;\n}\ntype RobotPose6 = [\n number,\n number,\n number,\n number,\n number,\n number\n];\ninterface RobotWorldKeyboardTeleopOptions {\n enabled?: boolean;\n linearStep?: number;\n angularStep?: number;\n}\ninterface RobotWorldOptions {\n name?: string;\n generateDemoWorld?: boolean;\n spawnPose?: RobotPose6;\n keyboardTeleop?: RobotWorldKeyboardTeleopOptions;\n}\ninterface RobotExportOptions {\n assembly: Assembly;\n modelName?: string;\n state?: JointState;\n static?: boolean;\n selfCollide?: boolean;\n allowAutoDisable?: boolean;\n links?: Record<string, RobotLinkExportOptions>;\n joints?: Record<string, RobotJointExportOptions>;\n plugins?: {\n diffDrive?: RobotDiffDrivePluginOptions;\n jointStatePublisher?: RobotJointStatePublisherOptions;\n };\n world?: RobotWorldOptions;\n}\ninterface CollectedRobotExport {\n modelName: string;\n assembly: AssemblyDefinition;\n state: JointState;\n static: boolean;\n selfCollide: boolean;\n allowAutoDisable: boolean;\n links: Record<string, RobotLinkExportOptions>;\n joints: Record<string, RobotJointExportOptions>;\n plugins: {\n diffDrive?: RobotDiffDrivePluginOptions;\n jointStatePublisher?: RobotJointStatePublisherOptions;\n };\n world: RobotWorldOptions | null;\n}\n/**\n * Declare that this script should export the assembly as a SDF/URDF robot package.\n *\n * **Details**\n *\n * Call `robotExport()` alongside your assembly definition. The CLI commands\n * `forgecad export sdf` and `forgecad export urdf` pick up the declaration and\n * produce a robot package with:\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 * - 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\n * `<mimic>` — SDF/URDF support single-leader mimic only. Dropped terms emit a warning.\n *\n* **Example**\n*\n* ```ts\n* const 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 *\n * robotExport({\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\n * forgecad export sdf model.forge.js # SDF package (Gazebo/Ignition)\n * forgecad export urdf model.forge.js # URDF package (ROS/PyBullet/MuJoCo)\n * ```\n *\n * @param options - Assembly, per-link mass/collision, per-joint overrides, plugins, world config\n * @returns The collected export descriptor (also stored globally for the CLI)\n * @category Robot Export\n */\ndeclare function robotExport(options: RobotExportOptions): CollectedRobotExport;\n/**\n * Combine 2D sketches into a single profile using an additive boolean union.\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `union2d(a, b, c)` or `union2d([a, b, c])`.\n * Uses Manifold\'s batch operation — faster than chaining `.add()` one by one when\n * combining many sketches.\n *\n * **Example**\n *\n * ```ts\n * const cross = union2d(rect(60, 10), rect(10, 60));\n * ```\n *\n * @returns A new sketch containing the union of all inputs\n * @see {@link Sketch.add} for the chainable method equivalent\n * @category Sketch Booleans\n */\ndeclare function union2d(...inputs: SketchOperandInput[]): Sketch;\n/**\n * Subtract one or more 2D sketches from a base sketch.\n *\n * **Details**\n *\n * The first sketch is the base; all subsequent sketches are subtracted from it.\n * Accepts individual sketches or arrays: `difference2d(base, c1, c2)` or\n * `difference2d([base, c1, c2])`.\n * Uses Manifold\'s batch operation — faster than chaining `.subtract()` one by one.\n *\n * **Example**\n *\n * ```ts\n * const donut = difference2d(circle2d(50), circle2d(30));\n * ```\n *\n * @returns A new sketch with all subsequent inputs subtracted from the first\n * @see {@link Sketch.subtract} for the chainable method equivalent\n * @category Sketch Booleans\n */\ndeclare function difference2d(...inputs: SketchOperandInput[]): Sketch;\n/**\n * Keep only the area where all input sketches overlap (intersection boolean).\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `intersection2d(a, b)` or `intersection2d([a, b, c])`.\n * Uses Manifold\'s batch operation — faster than chaining `.intersect()` one by one.\n *\n * **Example**\n *\n * ```ts\n * const lens = intersection2d(circle2d(30).translate(-10, 0), circle2d(30).translate(10, 0));\n * ```\n *\n * @returns A new sketch containing only the shared area of all inputs\n * @see {@link Sketch.intersect} for the chainable method equivalent\n * @category Sketch Booleans\n */\ndeclare function intersection2d(...inputs: SketchOperandInput[]): Sketch;\ntype RectVertexName = "bottomLeft" | "bottomRight" | "topRight" | "topLeft";\ntype RectSideName = "bottom" | "right" | "top" | "left";\ninterface RectOptions {\n /** Bottom-left x coordinate. Default: 0. */\n x?: number;\n /** Bottom-left y coordinate. Default: 0. */\n y?: number;\n /** Width (along x). Default: 10. */\n width?: number;\n /** Height (along y). Default: 10. */\n height?: number;\n /** Prevent 180° rotation (ensures bottom edge points rightward). Default: false. */\n blockRotation?: boolean;\n}\n/**\n * Typed handle for a constrained axis-aligned rectangle in the solver.\n *\n * Structural constraints pre-applied:\n * `horizontal(bottom)`, `horizontal(top)`, `vertical(left)`, `vertical(right)`,\n * `ccw(bl, br, tr, tl)`.\n *\n * This leaves **4 DOF** (position x/y, width, height). Use `sk.fix()`,\n * `sk.length()`, `sk.shapeWidth()`, etc. to pin them.\n */\ninterface ConstrainedRect {\n readonly bottomLeft: PointId;\n readonly bottomRight: PointId;\n readonly topRight: PointId;\n readonly topLeft: PointId;\n /** bottom-left → bottom-right */\n readonly bottom: LineId;\n /** bottom-right → top-right */\n readonly right: LineId;\n /** top-right → top-left */\n readonly top: LineId;\n /** top-left → bottom-left */\n readonly left: LineId;\n /**\n * Center point constrained to the geometric center via `midpoint` on the diagonal.\n * Can be used in further constraints: `sk.fix(rect.center, 0, 0)`,\n * `sk.coincident(rect.center, other)`.\n */\n readonly center: PointId;\n /** ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. */\n readonly shape: ShapeId;\n /** CCW-ordered vertex array: [bottomLeft, bottomRight, topRight, topLeft]. */\n readonly vertices: [\n PointId,\n PointId,\n PointId,\n PointId\n ];\n /** CCW-ordered side array: [bottom, right, top, left]. */\n readonly sides: [\n LineId,\n LineId,\n LineId,\n LineId\n ];\n /** Named vertex lookup. */\n vertex(name: RectVertexName): PointId;\n /** Named side lookup. */\n side(name: RectSideName): LineId;\n}\n/**\n * Add an axis-aligned rectangle concept to the builder.\n *\n * Creates 4 vertices (CCW: bl→br→tr→tl), 4 sides, 4 structural constraints\n * (`horizontal`/`vertical` on each side), CCW winding, a center point, a loop,\n * and a shape. Returns a `ConstrainedRect` handle with 4 DOF (x, y, width, height).\n *\n * Use `sk.rect()` as the shorthand builder method.\n *\n * **Example**\n *\n * ```ts\n * const sk = constrainedSketch();\n * const r = sk.rect({ x: 0, y: 0, width: 100, height: 50 });\n * sk.fix(r.bottomLeft, 0, 0);\n * sk.length(r.bottom, 120); // override initial width\n * return sk.solve().extrude(10);\n * ```\n *\n * @param sk - The builder to add the rectangle to.\n * @param options - Initial position and size.\n * @returns A `ConstrainedRect` handle with named vertices, sides, and center.\n * @category Constrained Sketches\n */\ndeclare function addRect(sk: ConstrainedSketchBuilder, options?: RectOptions): ConstrainedRect;\ninterface ConstrainedSketchBuilder {\n /**\n * Add an axis-aligned rectangle concept.\n * Returns a `ConstrainedRect` handle with named vertices, sides, and center.\n */\n rect(options?: RectOptions): ConstrainedRect;\n}\ninterface PolygonOptions {\n /** Initial vertex coordinates. Minimum 3 points. */\n points: ReadonlyArray<readonly [\n number,\n number\n ]>;\n /**\n * Whether to register a closed loop for sketch generation.\n * Default: true.\n */\n addLoop?: boolean;\n /** Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. */\n blockRotation?: boolean;\n}\n/**\n * Typed handle for a general constrained polygon in the solver.\n *\n * Structural constraints pre-applied: `ccw(vertices)` for winding enforcement.\n *\n * `sides[i]` goes from `vertices[i]` → `vertices[(i+1) % n]`.\n */\ninterface ConstrainedPolygon {\n /** CCW-ordered PointIds. */\n readonly vertices: PointId[];\n /**\n * CCW-ordered LineIds.\n * `sides[i]` runs from `vertices[i]` → `vertices[(i+1) % n]`.\n */\n readonly sides: LineId[];\n /** ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. */\n readonly shape: ShapeId;\n /** Get vertex by index. */\n vertex(index: number): PointId;\n /** Get side by index (side `i` goes vertex `i` → vertex `(i+1) % n`). */\n side(index: number): LineId;\n}\n/**\n * Add a general polygon concept to the builder.\n *\n * Creates n vertices and n sides (CCW: `sides[i]` from `vertices[i]` →\n * `vertices[(i+1) % n]`). Applies a `ccw` constraint to enforce winding.\n * All dimensional constraints (lengths, angles, position) are left to the caller.\n *\n * Use `sk.addPolygon()` as the shorthand builder method.\n *\n * **Example**\n *\n * ```ts\n * const sk = constrainedSketch();\n * const tri = sk.addPolygon({ points: [[0,0],[100,0],[50,80]] });\n * sk.fix(tri.vertex(0), 0, 0);\n * sk.length(tri.side(0), 100);\n * return sk.solve().extrude(5);\n * ```\n *\n * @param sk - The builder to add the polygon to.\n * @param options - Vertex coordinates and loop registration options.\n * @returns A `ConstrainedPolygon` handle with indexed vertex and side access.\n * @category Constrained Sketches\n */\ndeclare function addPolygon(sk: ConstrainedSketchBuilder, options: PolygonOptions): ConstrainedPolygon;\ninterface ConstrainedSketchBuilder {\n /**\n * Add a general polygon concept (CCW winding enforced).\n * Returns a `ConstrainedPolygon` handle.\n */\n addPolygon(options: PolygonOptions): ConstrainedPolygon;\n}\ninterface RegularPolygonOptions {\n /** Number of sides (minimum 3). */\n sides: number;\n /** Circumradius — distance from center to vertex. Default: 10. */\n radius?: number;\n /** Center x coordinate. Default: 0. */\n cx?: number;\n /** Center y coordinate. Default: 0. */\n cy?: number;\n /**\n * Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive).\n * Default: 0 (rightmost vertex).\n */\n startAngle?: number;\n /** Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. */\n blockRotation?: boolean;\n}\n/**\n * Typed handle for a constrained regular polygon in the solver.\n *\n * Structural constraints pre-applied:\n * - `equal(sides[0], sides[1])`, ..., `equal(sides[n-2], sides[n-1])` — equal sides\n * - `ccw(vertices)` — CCW winding\n *\n * Leaves **4 DOF** (center x/y, radius/scale, rotation). The center point is\n * tracked by the solver and exposed for further constraints.\n *\n * Note: Equal sides + CCW + regular initial placement makes this\n * "practically regular" for the solver. If you need geometrically exact\n * regularity (equal angles too), add `angleBetween` constraints on each pair\n * of adjacent sides.\n */\ninterface ConstrainedRegularPolygon extends ConstrainedPolygon {\n /**\n * Center point. Use `sk.fix(poly.center, x, y)` to pin location,\n * or `sk.coincident(poly.center, other)` to align with other geometry.\n */\n readonly center: PointId;\n}\n/**\n * Add a regular n-gon concept to the builder.\n *\n * Vertices are placed at `(cx + r·cos(startAngle + i·2π/n), cy + r·sin(...))`.\n * Equal-radius and equal-side constraints enforce regularity (4 DOF: center x/y, radius, rotation).\n * The center point is tracked by the solver and exposed via the returned handle.\n *\n * Use `sk.regularPolygon()` as the shorthand builder method.\n *\n * **Example**\n *\n * ```ts\n * const sk = constrainedSketch();\n * const hex = sk.regularPolygon({ sides: 6, radius: 25 });\n * sk.fix(hex.center, 0, 0);\n * sk.length(hex.side(0), 30); // all sides change (equal constraint)\n * return sk.solve().extrude(5);\n * ```\n *\n * @param sk - The builder to add the polygon to.\n * @param options - Number of sides, circumradius, center position, and rotation.\n * @returns A `ConstrainedRegularPolygon` handle extending `ConstrainedPolygon` with a `center` point.\n * @category Constrained Sketches\n */\ndeclare function addRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions): ConstrainedRegularPolygon;\ninterface ConstrainedSketchBuilder {\n /**\n * Add a regular n-gon concept (equal sides, CCW winding).\n * Returns a `ConstrainedRegularPolygon` handle with a center point.\n */\n regularPolygon(options: RegularPolygonOptions): ConstrainedRegularPolygon;\n}\ninterface GroupRectOptions {\n /** Bottom-left x coordinate (world). Default: 0. */\n x?: number;\n /** Bottom-left y coordinate (world). Default: 0. */\n y?: number;\n /** Width (along x in local coords). Required. */\n width: number;\n /** Height (along y in local coords). Required. */\n height: number;\n /** Allow the solver to rotate this rectangle. Default: false. */\n allowRotation?: boolean;\n}\ninterface ConstrainedGroupRect extends SketchGroupHandle {\n readonly bottomLeft: PointId;\n readonly bottomRight: PointId;\n readonly topRight: PointId;\n readonly topLeft: PointId;\n readonly bottom: LineId;\n readonly right: LineId;\n readonly top: LineId;\n readonly left: LineId;\n readonly shape: ShapeId;\n}\ninterface ConstrainedSketchBuilder {\n /**\n * Add a rigid rectangle as a group concept.\n * Returns a `ConstrainedGroupRect` handle with named vertices and sides.\n * The rectangle is fixed in shape — only position (and optionally rotation) varies.\n */\n groupRect(options: GroupRectOptions): ConstrainedGroupRect;\n}\ntype Vec3$3 = [\n number,\n number,\n number\n];\ninterface HermiteCurveEndpoint {\n /** Position */\n point: Vec3$3;\n /** Tangent direction (will be normalized internally) */\n tangent: Vec3$3;\n /** Weight: scales tangent magnitude relative to chord length. Default 1.0. */\n weight?: number;\n}\ndeclare class HermiteCurve3D {\n /** Start position */\n readonly p0: Vec3$3;\n /** End position */\n readonly p1: Vec3$3;\n /** Scaled tangent at start (direction * weight * chordLength) */\n readonly t0: Vec3$3;\n /** Scaled tangent at end (direction * weight * chordLength) */\n readonly t1: Vec3$3;\n /** Chord length (straight-line distance between endpoints) */\n readonly chordLength: number;\n constructor(start: HermiteCurveEndpoint, end: HermiteCurveEndpoint);\n /** Evaluate position at parameter t ∈ [0, 1] */\n pointAt(t: number): Vec3$3;\n /** Evaluate tangent (first derivative) at parameter t ∈ [0, 1] */\n tangentAt(t: number): Vec3$3;\n /** Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1] */\n curvatureAt(t: number): Vec3$3;\n /** Sample the curve as a polyline of evenly-spaced parameter values. */\n sample(count?: number): Vec3$3[];\n /** Approximate arc length by sampling. */\n length(samples?: number): number;\n /**\n * Sample with adaptive density — more points where curvature is higher.\n * Returns at least `minCount` points, up to `maxCount`.\n */\n sampleAdaptive(minCount?: number, maxCount?: number): Vec3$3[];\n /** Convert to a format compatible with sweep() path input. */\n toPolyline(samples?: number): Vec3$3[];\n}\ninterface QuinticHermiteCurveEndpoint {\n /** Position */\n point: Vec3$3;\n /** Tangent direction (will be normalized internally) */\n tangent: Vec3$3;\n /** Second derivative / curvature vector. Default [0, 0, 0]. */\n curvature?: Vec3$3;\n /** Weight: scales tangent magnitude relative to chord length. Default 1.0. */\n weight?: number;\n}\ndeclare class QuinticHermiteCurve3D {\n /** Start position */\n readonly p0: Vec3$3;\n /** End position */\n readonly p1: Vec3$3;\n /** Scaled tangent at start (direction * weight * chordLength) */\n readonly t0: Vec3$3;\n /** Scaled tangent at end (direction * weight * chordLength) */\n readonly t1: Vec3$3;\n /** Scaled second derivative at start (curvature * weight² * chordLength²) */\n readonly c0: Vec3$3;\n /** Scaled second derivative at end (curvature * weight² * chordLength²) */\n readonly c1: Vec3$3;\n /** Chord length (straight-line distance between endpoints) */\n readonly chordLength: number;\n constructor(start: QuinticHermiteCurveEndpoint, end: QuinticHermiteCurveEndpoint);\n /** Evaluate position at parameter t ∈ [0, 1] */\n pointAt(t: number): Vec3$3;\n /** Evaluate tangent (first derivative, normalized) at parameter t ∈ [0, 1] */\n tangentAt(t: number): Vec3$3;\n /** Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1] */\n curvatureAt(t: number): Vec3$3;\n /** Sample the curve as a polyline of evenly-spaced parameter values. */\n sample(count?: number): Vec3$3[];\n /** Approximate arc length by sampling. */\n length(samples?: number): number;\n /**\n * Sample with adaptive density — more points where curvature is higher.\n * Returns at least `minCount` points, up to `maxCount`.\n */\n sampleAdaptive(minCount?: number, maxCount?: number): Vec3$3[];\n /** Convert to a format compatible with sweep() path input. */\n toPolyline(samples?: number): Vec3$3[];\n}\n/**\n * Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).\n *\n * The curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`,\n * and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`,\n * with smooth G2-continuous interpolation matching position, tangent, and curvature.\n *\n * @param a - Start endpoint with position, tangent, optional curvature and weight\n * @param b - End endpoint with position, tangent, optional curvature and weight\n * @returns QuinticHermiteCurve3D instance\n */\ndeclare function hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D;\ntype Vec3$4 = [\n number,\n number,\n number\n];\ninterface NurbsCurve3DOptions {\n /** Polynomial degree (default 3 = cubic). Must be ≥ 1. */\n degree?: number;\n /** Rational weights, one per control point (default: all 1.0 = non-rational). */\n weights?: number[];\n /** Knot vector (default: uniform clamped). Must have length = controlPoints.length + degree + 1. */\n knots?: number[];\n /** Whether the curve is closed/periodic (default false). */\n closed?: boolean;\n}\ndeclare class NurbsCurve3D {\n readonly controlPoints: Vec3$4[];\n readonly weights: number[];\n readonly knots: number[];\n readonly degree: number;\n readonly closed: boolean;\n constructor(points: Vec3$4[], options?: NurbsCurve3DOptions);\n /**\n * Evaluate the curve at parameter t ∈ [0, 1].\n * Uses De Boor\'s algorithm — exact, O(degree²).\n */\n pointAt(t: number): Vec3$4;\n /**\n * Evaluate the unit tangent vector at parameter t ∈ [0, 1].\n */\n tangentAt(t: number): Vec3$4;\n /**\n * Sample the curve uniformly at `count` points.\n */\n sample(count?: number): Vec3$4[];\n /**\n * Sample with adaptive density — more points in high-curvature regions.\n */\n sampleAdaptive(minCount?: number, maxCount?: number): Vec3$4[];\n /**\n * Approximate arc length by summing polyline segment lengths.\n */\n length(samples?: number): number;\n /** Convert to a format compatible with sweep() path input. */\n toPolyline(samples?: number): Vec3$4[];\n private estimateCurvature;\n}\n/**\n * Create a NURBS curve from control points.\n *\n * With default options, creates a cubic non-rational B-spline with uniform clamped knots.\n * Set `weights` for rational curves (exact circles, conics).\n * Set `degree` for linear (1), quadratic (2), cubic (3), or higher-order curves.\n *\n * @example\n * // Simple cubic B-spline through control points\n * const curve = nurbs3d([[0,0,0], [10,5,0], [20,-5,10], [30,0,5]]);\n * const tube = sweep(circle(2), curve);\n *\n * @example\n * // Rational quadratic — exact circular arc\n * const arc = nurbs3d(\n * [[10,0,0], [10,10,0], [0,10,0]],\n * { degree: 2, weights: [1, Math.SQRT1_2, 1] }\n * );\n */\ndeclare function nurbs3d(points: Vec3$4[], options?: NurbsCurve3DOptions): NurbsCurve3D;\ntype Vec2 = [\n number,\n number\n];\ntype Vec3$5 = [\n number,\n number,\n number\n];\ninterface Spline2DOptions {\n /** Closed loop (default true). */\n closed?: boolean;\n /** Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. */\n tension?: number;\n /** Samples per segment (minimum 3). Default 16. */\n samplesPerSegment?: number;\n /**\n * For open splines, provide stroke width to return a solid Sketch.\n * If omitted for open splines, an error is thrown.\n */\n strokeWidth?: number;\n /** Stroke join for open splines. Default \'Round\'. */\n join?: "Round" | "Square";\n}\ninterface Spline3DOptions {\n /** Closed loop (default false). */\n closed?: boolean;\n /** Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. */\n tension?: number;\n}\ninterface LoftOptions {\n /** Marching-grid edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Optional extra bounds padding. */\n boundsPadding?: number;\n}\ninterface SweepOptions {\n /** Number of samples when path is a Curve3D. Default 48. */\n samples?: number;\n /** Marching-grid edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Optional extra bounds padding. */\n boundsPadding?: number;\n /**\n * Preferred "up" vector for local profile frame.\n * Auto fallback is used near parallel segments.\n */\n up?: Vec3$5;\n}\ninterface VariableSweepSection {\n /** Parameter along the spine (0 = start, 1 = end). */\n t: number;\n /** Cross-section profile at this station. */\n profile: Sketch;\n}\ninterface VariableSweepOptions {\n /** Number of samples when spine is a Curve3D. Default 48. */\n samples?: number;\n /** Marching-grid edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Optional extra bounds padding. */\n boundsPadding?: number;\n /**\n * Preferred "up" vector for local profile frame.\n * Auto fallback is used near parallel segments.\n */\n up?: Vec3$5;\n}\ndeclare class Curve3D {\n readonly points: Vec3$5[];\n readonly closed: boolean;\n readonly tension: number;\n constructor(points: Vec3$5[], options?: Spline3DOptions);\n /** Sample the curve with a fixed number of points per segment. */\n sampleBySegment(samplesPerSegment?: number): Vec3$5[];\n /** Sample the curve to an approximate total point count. */\n sample(count?: number): Vec3$5[];\n /** Map normalized t ∈ [0,1] to segment index + local parameter. */\n private segmentAt;\n /** Return the position on the curve at normalized parameter `t` in `[0, 1]`. O(1), no allocations. */\n pointAt(t: number): Vec3$5;\n /** Return a unit tangent vector at normalized parameter `t` in `[0, 1]`. O(1), analytical derivative. */\n tangentAt(t: number): Vec3$5;\n /** Approximate the curve length by polyline sampling. */\n length(samples?: number): number;\n}\n/**\n * Build a smooth Catmull-Rom spline sketch from 2D control points.\n *\n * A closed spline (default) returns a filled profile. An open spline requires\n * a strokeWidth option to produce a solid sketch. Use tension (0..1, default 0.5)\n * to control curve tightness.\n */\ndeclare function spline2d(points: Vec2[], options?: Spline2DOptions): Sketch;\n/**\n * Create a reusable 3D spline curve object (Catmull-Rom).\n *\n * The returned Curve3D provides sample(), pointAt(t), tangentAt(t), and length() for\n * downstream use in sweep() or manual path operations.\n */\ndeclare function spline3d(points: Vec3$5[], options?: Spline3DOptions): Curve3D;\n/**\n * Loft between multiple sketches along Z stations.\n *\n * Profiles can differ in topology and vertex count: interpolation is done on\n * signed-distance fields and meshed with level-set extraction. Heights must be\n * strictly increasing. Compatible loft stacks can also stay on the maintained\n * export-backend path.\n *\n * Performance note: loft is significantly heavier than primitive/extrude/revolve.\n * If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().\n */\ndeclare function loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape;\ninterface LoftAlongSpineOptions {\n /** Number of samples when spine is a Curve3D. Default 48. */\n samples?: number;\n /** Marching-grid edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Optional extra bounds padding. */\n boundsPadding?: number;\n /**\n * Preferred "up" vector for local profile frame.\n * Auto fallback is used near parallel segments.\n */\n up?: Vec3$5;\n}\n/**\n * Loft between multiple profiles positioned along an arbitrary 3D spine curve.\n *\n * Unlike loft() which only supports Z heights, loftAlongSpine() places each\n * profile at a position along a 3D spine, oriented perpendicular to the spine\n * tangent. This enables lofting along curved paths — e.g., a wing root-to-tip\n * transition that follows a swept-back leading edge.\n *\n * The tValues array specifies where each profile sits along the spine (0 = start,\n * 1 = end). Must have the same length as profiles and be in [0, 1].\n *\n * Internally uses variableSweep infrastructure with SDF interpolation.\n *\n * Performance note: uses level-set meshing, heavier than simple loft().\n */\ndeclare function loftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3$5[], tValues: number[], options?: LoftAlongSpineOptions): Shape;\ntype SweepPathInput = Curve3D | HermiteCurve3D | QuinticHermiteCurve3D | NurbsCurve3D | Vec3$5[];\ndeclare function sweep(profile: Sketch, path: SweepPathInput, options?: SweepOptions): Shape;\n/**\n * Sweep a variable cross-section along a 3D spine curve.\n *\n * Unlike sweep(), which uses a single constant profile, variableSweep()\n * interpolates between multiple profiles at different stations along the spine.\n * This enables organic shapes like tapering tubes, bone-like structures, and\n * sculptural forms.\n *\n * Each section specifies a t parameter (0 = start, 1 = end of spine) and a\n * 2D profile sketch. The SDF-based level-set mesher smoothly blends between\n * profiles at intermediate positions.\n *\n * Performance note: like sweep(), this uses level-set meshing internally.\n */\ndeclare function variableSweep(spine: SweepPathInput, sections: VariableSweepSection[], options?: VariableSweepOptions): Shape;\ntype PointArg$1 = [\n number,\n number\n] | [\n number,\n number,\n number\n] | Point2D;\ninterface DimOpts {\n offset?: number;\n label?: string;\n color?: string;\n component?: string | string[];\n currentComponent?: boolean;\n}\n/**\n * Add a dimension annotation between two points.\n *\n * **Details**\n *\n * Dimension annotations are purely visual callouts rendered in the viewport\n * and report export. They do not affect geometry or constrain the model.\n *\n * Point arguments accept 2D tuples `[x, y]`, 3D tuples `[x, y, z]`, or\n * `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\n * 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 * **Example**\n *\n * ```ts\n * dim([-w / 2, 0, 0], [w / 2, 0, 0], { label: "Width" });\n * dim([0, 0, -h / 2], [0, 0, h / 2], { label: "Height", offset: 14 });\n * dim([0, 0, 0], [100, 0, 0], { component: "Base", color: "#00AAFF" });\n * ```\n *\n * @param from - Start point as `[x, y]`, `[x, y, z]`, or `Point2D`\n * @param to - End point as `[x, y]`, `[x, y, z]`, or `Point2D`\n * @param opts - Optional: `offset` (default 10), `label`, `color` (hex),\n * `component` (string or string[] — report ownership), `currentComponent` (boolean)\n * @category Dimensions\n */\ndeclare function dim(from: PointArg$1, to: PointArg$1, opts?: DimOpts): void;\n/**\n * Add a dimension annotation along a `Line2D`.\n *\n * **Details**\n *\n * Convenience wrapper around {@link dim} that extracts the start and end\n * points from a constrained-sketch `Line2D` entity. All `opts` are forwarded\n * unchanged.\n *\n * **Example**\n *\n * ```ts\n * const a = point(0, 0);\n * const b = point(100, 0);\n * dimLine(line(a, b), { label: "Span", offset: -8 });\n * ```\n *\n * @param l - A `Line2D` entity from the constrained sketch builder\n * @param opts - Optional: `offset`, `label`, `color`, `component`, `currentComponent`\n * @see {@link dim} for point-to-point annotations\n * @category Dimensions\n */\ndeclare function dimLine(l: Line2D, opts?: DimOpts): void;\ninterface SketchDxfOptions {\n /** DXF layer name. Default: "0" */\n layer?: string;\n /** DXF color index (1–255, AutoCAD ACI). Default: 7 (white/black) */\n colorIndex?: number;\n}\n/**\n * Export a 2D sketch as a DXF string (R12/AC1009 — maximally compatible).\n *\n * **Details**\n *\n * For regular sketches, each polygon loop becomes a closed `LWPOLYLINE`.\n * For constrained sketches, exports raw `LINE`, `CIRCLE`, and `ARC` entities\n * from the constraint edge geometry, which preserves internal/shared edges\n * that `toPolygons()` would merge away.\n *\n * The R12 format is chosen for maximum compatibility with CAM tools,\n * laser-cutter software, and older CAD readers.\n *\n * **Example**\n *\n * ```ts\n * const s = rect(100, 60);\n * const dxf = sketchToDxf(s, { layer: \'cut\' });\n * ```\n *\n * @param sketch - A 2D `Sketch` to export\n * @param options - Optional DXF `layer` name and AutoCAD `colorIndex` (ACI 1–255)\n * @returns DXF markup string in R12/AC1009 format\n * @see {@link sketchToSvg} for SVG export\n * @category Export\n */\ndeclare function sketchToDxf(sketch: Sketch, options?: SketchDxfOptions): string;\ninterface SketchSvgOptions {\n /** Stroke color. Default: "black" */\n stroke?: string;\n /** Stroke width in sketch units. Default: 0.5 */\n strokeWidth?: number;\n /** Fill color. Default: "none" */\n fill?: string;\n /** Padding around the sketch bounding box in sketch units. Default: 2 */\n padding?: number;\n /** If set, scale so 1 sketch-unit = this many px. Otherwise auto-fit. */\n pixelsPerUnit?: number;\n}\n/**\n * Export a 2D sketch as an SVG string.\n *\n * **Details**\n *\n * For regular sketches, exports filled polygon regions. For constrained\n * sketches, exports raw edge geometry (LINE, ARC, CIRCLE) which preserves\n * internal/shared edges that `toPolygons()` would merge away.\n *\n * The SVG uses the sketch\'s native coordinate system (Y-up) with a CSS\n * transform that flips Y so the output renders correctly in SVG\'s Y-down\n * space. Coordinates are in sketch units (typically mm).\n *\n * **Example**\n *\n * ```ts\n * const s = rect(100, 60);\n * const svg = sketchToSvg(s, { stroke: \'#333\', strokeWidth: 0.8 });\n * ```\n *\n * @param sketch - A 2D `Sketch` to export\n * @param options - Optional stroke, fill, padding, and pixel-per-unit scale\n * @returns SVG markup string\n * @see {@link sketchToDxf} for DXF export\n * @category Export\n */\ndeclare function sketchToSvg(sketch: Sketch, options?: SketchSvgOptions): string;\n/**\n * Round a tracked vertical solid edge with a circular fillet.\n *\n * **Details**\n *\n * Compiler-owned fillet for a narrow tracked-edge subset on solids.\n *\n * This is **not** a general 2D sketch-corner fillet. It currently works only\n * on tracked vertical edges from `box()` or `Rectangle2D` extrusions (plus\n * rigid transforms and supported preserved descendants of those). Generic\n * sketch extrudes, including `rect(...).extrude(...)`, are outside the\n * supported subset right now.\n *\n * **Supported edges:**\n * - Tracked vertical edges from `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,\n * intersection, generic sketch extrudes, or tapered extrudes.\n *\n * Canonical quadrants: `vert-bl → [1,-1]`, `vert-br → [-1,-1]`,\n * `vert-tr → [-1,1]`, `vert-tl → [1,1]`\n *\n * **Example**\n *\n * ```ts\n * const base = Rectangle2D.fromDimensions(0, 0, 50, 50).extrude(20);\n * const filleted = filletTrackedEdge(base, base.edge(\'vert-br\'), 5, [-1, -1]);\n * ```\n *\n * @param shape - Compile-covered solid to modify\n * @param edge - `EdgeRef` from the same tracked target body (e.g. `base.edge(\'vert-br\')`)\n * @param radius - Fillet radius (must be positive and finite)\n * @param quadrant - Corner direction sign pair (default: `[-1, -1]`)\n * @param segments - Arc resolution (default: `16`)\n * @returns A new Shape with the fillet applied\n * @see {@link fillet} for the general-purpose query-based variant\n * @see {@link chamferTrackedEdge} for beveled tracked edges\n * @see {@link filletCorners} for selective 2D sketch-corner rounding\n * @see {@link Sketch.offset} for rounding all convex corners of a sketch\n * @category Edge Features\n */\ndeclare function filletTrackedEdge(shape: Shape, edge: EdgeRef, radius: number, quadrant?: [\n number,\n number\n], segments?: number): Shape;\n/**\n * Bevel a tracked vertical solid edge with a 45° chamfer.\n *\n * **Details**\n *\n * Compiler-owned chamfer for tracked vertical edges. Requires a\n * compile-plan-covered target. This is not a general 2D sketch-corner tool;\n * supported subset and quadrant semantics are the same as `filletTrackedEdge()` - see\n * that function for details.\n *\n * **Example**\n *\n * ```ts\n * const base = Rectangle2D.fromDimensions(0, 0, 50, 50).extrude(20);\n * const chamfered = chamferTrackedEdge(base, base.edge(\'vert-br\'), 3, [-1, -1]);\n * ```\n *\n * @param shape - Compile-covered solid to modify\n * @param edge - `EdgeRef` from the same tracked target body (e.g. `base.edge(\'vert-br\')`)\n * @param size - Chamfer size (must be positive and finite)\n * @param quadrant - Corner direction sign pair (default: `[-1, -1]`)\n * @returns A new Shape with the chamfer applied\n * @see {@link chamfer} for the general-purpose query-based variant\n * @see {@link filletTrackedEdge} for rounded tracked edges\n * @see {@link filletCorners} for selective 2D sketch-corner rounding\n * @category Edge Features\n */\ndeclare function chamferTrackedEdge(shape: Shape, edge: EdgeRef, size: number, quadrant?: [\n number,\n number\n]): Shape;\ninterface FilletCornerSpec {\n index: number;\n radius: number;\n segments?: number;\n}\ntype PointInput = [\n number,\n number\n] | Point2D;\n/**\n * Create a polygon from points with specific corners rounded to arc fillets.\n *\n * **Details**\n *\n * Each corner spec identifies a vertex by its index in the `points` array and\n * the desired fillet `radius`. Both convex and concave corners are supported.\n *\n * Constraints:\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 *\n * Use `offset(-r).offset(+r)` instead if you want to round **all** convex corners\n * uniformly. Use `filletCorners` when you need selective or mixed sharp/rounded profiles.\n *\n * **Example**\n *\n * ```ts\n * const roof = filletCorners(roofPoints, [\n * { index: 3, radius: 19 },\n * { index: 4, radius: 19 },\n * { index: 5, radius: 19 },\n * ]);\n * ```\n *\n * @param points - Closed polygon vertices as `[x, y][]` or `Point2D[]`\n * @param corners - Fillet specs — each has `index` (vertex index), `radius`, and optional `segments`\n * @returns A new polygon sketch with the specified corners filleted\n * @see {@link Sketch.offset} for rounding all convex corners uniformly\n * @category Sketch Operations\n */\ndeclare function filletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch;\n/**\n * Pre-load and cache a font for use with `text2d()`.\n *\n * **Details**\n *\n * Fonts are cached by their source string (or `cacheKey` for `ArrayBuffer` sources),\n * so repeated calls with the same path are free. Pre-loading is useful when you call\n * `text2d()` many times with the same font — it avoids repeated disk reads.\n *\n * Built-in font names that work everywhere (browser + CLI):\n * - `\'sans-serif\'` or `\'inter\'` — bundled Inter Regular\n *\n * **Example**\n *\n * ```ts\n * const font = loadFont(\'/path/to/Arial Bold.ttf\');\n * text2d(\'Title\', { size: 12, font }).extrude(1.5);\n * text2d(\'Subtitle\', { size: 8, font }).extrude(1);\n * ```\n *\n * @param source - Built-in name (`\'sans-serif\'`/`\'inter\'`), file path (CLI only), or `ArrayBuffer`\n * @param cacheKey - Optional cache key when passing an `ArrayBuffer`\n * @returns Parsed opentype.js Font object\n * @see {@link text2d} to use the loaded font\n * @category Sketch Text\n */\ndeclare function loadFont(source: string | ArrayBuffer, cacheKey?: string): opentype$1.Font;\ntype Vec3$6 = [\n number,\n number,\n number\n];\ninterface NurbsSurfaceOptions {\n /** Degree in U direction (default 3). */\n degreeU?: number;\n /** Degree in V direction (default 3). */\n degreeV?: number;\n /** Weights grid — same dimensions as controlGrid (default: all 1.0). */\n weights?: number[][];\n /** Knot vector in U direction (default: uniform clamped). */\n knotsU?: number[];\n /** Knot vector in V direction (default: uniform clamped). */\n knotsV?: number[];\n /** Sheet thickness — if > 0, thickens the surface into a solid (default 0 = surface only). */\n thickness?: number;\n /** Tessellation resolution — points per direction (default 32). */\n resolution?: number;\n /** Explicit opt-in for sampled fallback paths on non-exact backends. */\n approximate?: boolean;\n}\ndeclare class NurbsSurface {\n readonly controlGrid: Vec3$6[][];\n readonly weightsGrid: number[][];\n readonly knotsU: number[];\n readonly knotsV: number[];\n readonly degreeU: number;\n readonly degreeV: number;\n readonly nU: number;\n readonly nV: number;\n constructor(controlGrid: Vec3$6[][], options?: NurbsSurfaceOptions);\n /**\n * Evaluate the surface at parameters (u, v) ∈ [0, 1]².\n * Uses tensor product evaluation: evaluate basis functions in U and V independently.\n */\n pointAt(u: number, v: number): Vec3$6;\n /**\n * Evaluate the surface normal at (u, v) via cross product of partial derivatives.\n */\n normalAt(u: number, v: number): Vec3$6;\n /**\n * Tessellate the surface into a triangle mesh.\n * Returns positions, normals, and triangle indices.\n */\n tessellate(resU?: number, resV?: number): {\n positions: Vec3$6[];\n normals: Vec3$6[];\n indices: number[];\n };\n private remapU;\n private remapV;\n}\n/**\n * Create a NURBS surface from a grid of control points.\n *\n * The control grid is indexed as `controlGrid[u][v]` — each row is a curve\n * in the V direction, and columns trace curves in the U direction.\n *\n * With default options, creates a bicubic non-rational B-spline surface\n * with uniform clamped knots.\n *\n * @example\n * // Simple 4×4 control grid — a gently curved surface\n * const 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 * ];\n * const surface = nurbsSurface(grid, { thickness: 2 });\n */\ndeclare function nurbsSurface(controlGrid: Vec3$6[][], options?: NurbsSurfaceOptions): Shape;\n/**\n * Layout helpers — eliminate manual trigonometry for common positioning patterns.\n *\n * These functions return arrays of {x, y} positions that can be used with\n * translate(), circularPattern seed placement, or any other positioning API.\n */\ninterface LayoutPoint {\n x: number;\n y: number;\n}\ninterface CircularLayoutOptions {\n /** Angle of the first element in degrees (default: 0 = +X axis). */\n startDeg?: number;\n /** Center X coordinate (default: 0). */\n centerX?: number;\n /** Center Y coordinate (default: 0). */\n centerY?: number;\n}\n/**\n * Compute evenly-spaced positions around a circle.\n *\n * Eliminates the most common trig pattern in CAD scripts:\n * ```js\n * // Before — manual trig\n * for (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\n * for (const {x, y} of circularLayout(12, r)) {\n * markers.push(marker.translate(x, y, 0));\n * }\n * ```\n */\ndeclare function circularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[];\ninterface PolygonVerticesOptions {\n /** Angle of the first vertex in degrees (default: 90 = top). */\n startDeg?: number;\n /** Center X coordinate (default: 0). */\n centerX?: number;\n /** Center Y coordinate (default: 0). */\n centerY?: number;\n}\n/**\n * Compute the vertex positions of a regular polygon.\n *\n * Default orientation places the first vertex at the top (90 degrees),\n * matching the convention used by `ngon()`.\n *\n * Eliminates manual Math.sqrt(3) for triangles, pentagon vertex math, etc:\n * ```js\n * // Before — manual equilateral triangle\n * const v1 = [center.x - r/2, center.y + r * Math.sqrt(3)/2];\n * const v2 = [center.x - r/2, center.y - r * Math.sqrt(3)/2];\n * const v3 = [center.x + r, center.y];\n *\n * // After — declarative\n * const [v1, v2, v3] = polygonVertices(3, r);\n * ```\n */\ndeclare function polygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[];\ndeclare class PathBuilder {\n private segs;\n private x;\n private y;\n /** Current departure tangent unit vector. */\n private dirX;\n private dirY;\n /**\n * Current cursor X position.\n *\n * @returns The current X coordinate.\n */\n getX(): number;\n /**\n * Current cursor Y position.\n *\n * @returns The current Y coordinate.\n */\n getY(): number;\n /**\n * Move the cursor to an absolute position without drawing a segment.\n *\n * When called after the initial `path()`, this establishes the start of the outline.\n * Calling `moveTo` again mid-path starts a new sub-path (hole in `close()`, separate\n * segment for `stroke()`).\n *\n * @param x - Absolute X coordinate.\n * @param y - Absolute Y coordinate.\n * @category Path Builder\n */\n moveTo(x: number, y: number): this;\n /**\n * Draw a straight line from the current cursor to an absolute position.\n *\n * @param x - Absolute X coordinate of the endpoint.\n * @param y - Absolute Y coordinate of the endpoint.\n * @category Path Builder\n */\n lineTo(x: number, y: number): this;\n /**\n * Draw a horizontal line segment by `dx` units from the current cursor.\n *\n * Positive `dx` moves right; negative moves left.\n *\n * @param dx - Horizontal displacement.\n * @category Path Builder\n */\n lineH(dx: number): this;\n /**\n * Draw a vertical line segment by `dy` units from the current cursor.\n *\n * Positive `dy` moves up; negative moves down.\n *\n * @param dy - Vertical displacement.\n * @category Path Builder\n */\n lineV(dy: number): this;\n /**\n * Draw a line at the given angle and length from the current cursor.\n *\n * Angle convention: `0°` points right (+X), `90°` points up (+Y).\n *\n * **Example**\n *\n * ```ts\n * // L-bracket with angled return\n * path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n * ```\n *\n * @param length - Length of the line segment.\n * @param degrees - Angle in degrees (0 = right, 90 = up).\n * @category Path Builder\n */\n lineAngled(length: number, degrees: number): this;\n /**\n * Draw a line by a relative `(dx, dy)` displacement from the current cursor.\n *\n * @param dx - Horizontal displacement.\n * @param dy - Vertical displacement.\n * @category Path Builder\n */\n lineBy(dx: number, dy: number): this;\n /**\n * Draw an arc to a point offset from the current cursor.\n *\n * @param dx - Relative X displacement to the end point.\n * @param dy - Relative Y displacement to the end point.\n * @param radius - Arc radius.\n * @param clockwise - Sweep direction. Default: `false` (CCW).\n * @category Path Builder\n */\n arcBy(dx: number, dy: number, radius: number, clockwise?: boolean): this;\n /**\n * Draw a cubic Bezier using control points relative to the current cursor.\n *\n * @param dcp1x - Relative X of the first control point.\n * @param dcp1y - Relative Y of the first control point.\n * @param dcp2x - Relative X of the second control point.\n * @param dcp2y - Relative Y of the second control point.\n * @param dx - Relative X of the end point.\n * @param dy - Relative Y of the end point.\n * @category Path Builder\n */\n bezierBy(dcp1x: number, dcp1y: number, dcp2x: number, dcp2y: number, dx: number, dy: number): this;\n /**\n * Draw a circular arc from the current position to (x, y) with the given radius.\n * `clockwise=true` → arc curves to the right of the start→end direction.\n * `clockwise=false` → arc curves to the left of the start→end direction.\n */\n arcTo(x: number, y: number, radius: number, clockwise?: boolean): this;\n /**\n * G1-continuous arc — radius derived from current tangent + endpoint.\n * Throws if endpoint is collinear with current direction.\n */\n tangentArcTo(x: number, y: number): this;\n /**\n * Draw an arc defined by center, radius, and angle range (no trig needed).\n * If the path has no segments yet, automatically moves to the arc start.\n * Positive sweep (startDeg < endDeg) = CCW, negative = CW.\n *\n * ```js\n * // Arc centered at (10, 0), radius 50, from -30° to +30°\n * path().arc(10, 0, 50, -30, 30).stroke(8, \'Round\')\n * ```\n */\n arc(cx: number, cy: number, radius: number, startDeg: number, endDeg: number): this;\n /**\n * Arc around a known center point, sweeping by the given angle.\n * Radius is derived from the distance between the current position and the center.\n * Positive sweep = CCW (math convention), negative = CW.\n *\n * ```js\n * // Arc 90° CCW around (50, 50)\n * path().moveTo(70, 50).arcAround(50, 50, 90)\n * // Arc 45° CW around the origin\n * path().moveTo(10, 0).arcAround(0, 0, -45)\n * ```\n */\n arcAround(cx: number, cy: number, sweepDeg: number): this;\n /**\n * Arc around a center point given as an offset from the current position.\n * `(dx, dy)` is the vector from the current point to the center.\n * Positive sweep = CCW (math convention), negative = CW.\n *\n * ```js\n * // Arc 90° CCW around a center 20 units to the right\n * path().moveTo(50, 50).arcAroundRelative(20, 0, 90)\n * // Equivalent to: path().moveTo(50, 50).arcAround(70, 50, 90)\n * ```\n */\n arcAroundRelative(dx: number, dy: number, sweepDeg: number): this;\n /**\n * Smooth three-arc end cap from the current position to (endX, endY).\n * Inserts: small corner arc → large cap arc → small corner arc, all G1-continuous.\n */\n smoothCapTo(endX: number, endY: number, cornerRadius: number, capRadius: number): this;\n /**\n * Cubic bezier from current position to (x, y) via two control points.\n */\n bezierTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): this;\n /**\n * G1-continuous cubic bezier — first control point is auto-derived from\n * the current tangent direction. `weight` controls how far the auto-placed\n * control point extends along the tangent (default: 1/3 of the chord).\n *\n * The second control point `(cp2x, cp2y)` must be provided — it controls\n * the arrival curvature. For a fully automatic smooth curve, see `smoothThrough`.\n */\n tangentBezierTo(cp2x: number, cp2y: number, x: number, y: number, weight?: number): this;\n /**\n * Catmull-Rom spline through a list of waypoints from the current position.\n * The current position is included as the first point. The last waypoint\n * becomes the new cursor position.\n *\n * @param waypoints — intermediate + final points (at least 1)\n * @param tension — 0 = very round, 1 = linear (default 0.5)\n */\n smoothThrough(waypoints: [\n number,\n number\n ][], tension?: number): this;\n /**\n * Rational B-spline edge to (x, y) with explicit control points and weights.\n *\n * The control points define the B-spline shape between the current position\n * and (x, y). The current position is NOT included in `controlPoints` — it is\n * automatically prepended. The endpoint (x, y) is the last control point.\n *\n * @param controlPoints — interior + endpoint control points (endpoint = last)\n * @param opts.weights — rational weights (default: all 1.0)\n * @param opts.degree — B-spline degree (default: control point count - 1, capped at 3)\n */\n nurbsTo(controlPoints: [\n number,\n number\n ][], opts?: {\n weights?: number[];\n degree?: number;\n }): this;\n /**\n * Exact circular arc to (x, y) using a rational quadratic NURBS.\n *\n * Unlike `arcTo()` which tessellates to a polyline, this preserves the\n * exact arc definition. When extruded through the OCCT backend, it produces\n * a true cylindrical face — not a faceted approximation.\n *\n * @param x — endpoint X\n * @param y — endpoint Y\n * @param opts.radius — arc radius (default: auto-computed from chord)\n * @param opts.clockwise — winding direction (default: false = CCW)\n */\n exactArcTo(x: number, y: number, opts?: {\n radius?: number;\n clockwise?: boolean;\n }): this;\n /**\n * Round the last corner (the junction between the previous two segments)\n * with a tangent arc of the given radius.\n *\n * Must be called after at least two line/arc segments that form a corner.\n * The fillet trims back both segments and inserts a tangent arc.\n *\n * ```js\n * path().moveTo(0,0).lineTo(10,0).lineTo(10,10).fillet(2).lineTo(0,10).close()\n * ```\n */\n fillet(radius: number): this;\n /**\n * Chamfer the last corner with a straight cut of the given distance.\n *\n * ```js\n * path().moveTo(0,0).lineTo(10,0).lineTo(10,10).chamfer(2).lineTo(0,10).close()\n * ```\n */\n chamfer(distance: number): this;\n private computeFilletGeom;\n private computeChamferGeom;\n private getSegEnd;\n private getSegDirAt;\n private trimLastSegEnd;\n /**\n * Mirror all existing segments across an axis and append the mirrored copy\n * in reverse order, creating a symmetric path. The axis passes through the\n * current cursor position.\n *\n * @param axis — \'x\' mirrors across the local X-axis (flips Y),\n * \'y\' mirrors across the local Y-axis (flips X),\n * or `[nx, ny]` for an arbitrary axis direction.\n *\n * ```js\n * // Build right half, mirror to get full symmetric profile\n * path().moveTo(0,0).lineTo(10,0).lineTo(10,5).mirror(\'x\').close()\n * ```\n */\n mirror(axis: "x" | "y" | [\n number,\n number\n ]): this;\n /**\n * Label the most recently added segment. Labels are born here and grow\n * into face names when the sketch is extruded, lofted, swept, or revolved.\n *\n * Labels must be unique within a path. Each segment can have at most one label.\n */\n label(name: string): this;\n /**\n * Label the closing segment and close the path. Shorthand for labeling the\n * implicit line from the last point back to the start, then closing.\n */\n closeLabel(name: string): Sketch;\n /** Label for the implicit closing segment (set by closeLabel). */\n private _closingLabel;\n /** Expand all segments into a flat tessellated polyline. */\n private tessellate;\n /**\n * Close the path and return a filled `Sketch`.\n *\n * **Details**\n *\n * The winding of the polygon is automatically corrected to CCW (the expected\n * orientation for ForgeCAD sketches). If the path contains multiple sub-paths\n * (started with subsequent `moveTo` calls), the first sub-path is the outer\n * contour and subsequent sub-paths become holes subtracted from it.\n *\n * Edge labels (assigned with `.label(\'name\')`) are transferred to the resulting\n * sketch and propagate through `extrude()`, `revolve()`, `loft()`, and `sweep()`\n * into named faces on the resulting `Shape`.\n *\n * **Example**\n *\n * ```ts\n * const triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();\n *\n * // With a hole (second sub-path)\n * const 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 * @returns A filled `Sketch` with correct CCW winding.\n * @category Path Builder\n */\n close(): Sketch;\n /**\n * Close the path and return an offset version of the filled Sketch.\n * Positive delta expands outward, negative shrinks inward.\n */\n closeOffset(delta: number, join?: "Round" | "Square" | "Miter"): Sketch;\n /**\n * Thicken an open polyline (centerline) into a solid filled profile with uniform width.\n *\n * **Details**\n *\n * Expands the path into a closed profile `width` units wide (half-width on each side\n * of the centerline). Use `\'Round\'` for ribs, wire traces, and organic profiles —\n * it adds semicircular endcaps and rounds joins. Use `\'Square\'` (default) for sharp\n * miter joins without endcaps.\n *\n * Not the same as rounding corners of a closed polygon — for mixed sharp-and-rounded\n * outlines, build the polygon first and apply `filletCorners()`.\n *\n * **Example**\n *\n * ```ts\n * // Square-join L-bracket\n * const bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n *\n * // Round-join rib\n * const rib = path().moveTo(0, 0).lineH(60).stroke(6, \'Round\');\n *\n * // Equivalent standalone form\n * const wire = stroke([[0, 0], [50, 0], [50, -70]], 4);\n * ```\n *\n * @param width - Total stroke width (half on each side of the centerline).\n * @param join - `\'Square\'` for miter joins (default) or `\'Round\'` for rounded joins\n * and semicircular endcaps.\n * @returns A filled `Sketch` representing the thickened profile.\n * @category Path Builder\n */\n stroke(width: number, join?: "Round" | "Square"): Sketch;\n /** Split segments into sub-paths at each moveTo. */\n private splitSubPaths;\n /** Build semantic ProfileEdge array from path segments (for pathProfile compile plan). */\n private buildProfileEdges;\n private profileEdgeCountForSeg;\n /** Tessellate a sub-path (sequence of segments). */\n private tessellateSegs;\n}\n/**\n * Create a new `PathBuilder` for tracing a 2D outline point by point.\n *\n * **Details**\n *\n * `PathBuilder` is a fluent API for constructing 2D profiles using a mix of line\n * segments, arcs, bezier curves, and splines. Always start with `.moveTo(x, y)` to\n * set the starting point. Call `.close()` to get a filled `Sketch`, or `.stroke(width)`\n * to thicken an open polyline into a solid profile.\n *\n * Edge labels can be assigned with `.label(\'name\')` after any segment — they propagate\n * through extrusion, revolve, loft, and sweep into named faces on the resulting `Shape`.\n *\n * **Example**\n *\n * ```ts\n * // Closed triangle\n * const triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();\n *\n * // L-shaped bracket as a stroke\n * const bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n *\n * // Labeled edges for downstream face references\n * const 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 * @returns A new `PathBuilder` instance with the cursor at the origin.\n * @category Path Builder\n */\ndeclare function path(): PathBuilder;\n/** Create a stroked polyline sketch from an array of 2D points. */\ndeclare function stroke(points: [\n number,\n number\n][], width: number, join?: "Round" | "Square"): Sketch;\n/**\n * Repeat a shape in a linear pattern along a direction vector and union the copies.\n *\n * **Details**\n *\n * Creates `count` copies of `shape`, each offset by `(dx*i, dy*i, dz*i)`\n * from the original. All copies are unioned into a single `Shape`. Distinct\n * compiler ownership is assigned to each copy so face identity via\n * owner-scoped canonical queries still works post-merge.\n *\n * **Example**\n *\n * ```ts\n * // 5 cylinders, 20mm apart along X\n * linearPattern(cylinder(10, 3), 5, 20, 0)\n * ```\n *\n * @param shape - The shape to repeat\n * @param count - Total number of copies (including the original at index 0)\n * @param dx - X step per copy\n * @param dy - Y step per copy\n * @param dz - Z step per copy (default: `0`)\n * @returns Union of all copies\n * @see {@link circularPattern} for rotational patterns\n * @see {@link mirrorCopy} for mirror patterns\n * @category Tracked Shapes\n */\ndeclare function linearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape;\ninterface CircularPatternOptions {\n /** Center X of the rotation (default: 0). Used when axis is Z (legacy mode). */\n centerX?: number;\n /** Center Y of the rotation (default: 0). Used when axis is Z (legacy mode). */\n centerY?: number;\n /** Rotation axis direction (default: [0, 0, 1] = Z axis). */\n axis?: [\n number,\n number,\n number\n ];\n /** Pivot point for the rotation (default: [0, 0, 0]). Overrides centerX/centerY when set. */\n origin?: [\n number,\n number,\n number\n ];\n}\n/**\n * Repeat a shape in a circular pattern around an axis and union the copies.\n *\n * **Details**\n *\n * Distributes `count` copies evenly around the rotation axis (360° / count\n * per step). All copies are unioned into a single `Shape`. Distinct compiler\n * ownership is assigned to each copy — post-merge face identity via\n * owner-scoped canonical queries still works for pattern descendants.\n *\n * Two calling conventions:\n * - **Simple** (Z axis): `circularPattern(shape, 6)` or\n * `circularPattern(shape, 6, centerX, centerY)`\n * - **Advanced** (arbitrary axis): `circularPattern(shape, 6, { axis, origin })`\n *\n * **Example**\n *\n * ```ts\n * // 8 holes evenly spaced around origin\n * circularPattern(cylinder(12, 4).translate(30, 0, -1), 8)\n *\n * // Circular pattern around X axis\n * circularPattern(myFeature, 4, { axis: [1, 0, 0], origin: [0, 0, 50] })\n * ```\n *\n * @param shape - The shape to repeat\n * @param count - Total number of copies (including the original at index 0)\n * @param centerXOrOpts - Center X offset, or options object with `axis`/`origin`/`centerX`/`centerY`\n * @param centerY - Center Y offset (only when using the simple two-number form)\n * @returns Union of all copies\n * @see {@link linearPattern} for linear patterns\n * @see {@link mirrorCopy} for mirror patterns\n * @category Tracked Shapes\n */\ndeclare function circularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape;\n/**\n * Repeat a 2D sketch in a linear pattern and union the copies.\n *\n * @param sketch - The sketch to repeat\n * @param count - Total number of copies\n * @param dx - X step per copy\n * @param dy - Y step per copy (default: `0`)\n * @returns Union of all copies as a single Sketch\n * @category Tracked Shapes\n */\ndeclare function linearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch;\n/**\n * Repeat a 2D sketch in a circular pattern around a center point and union the copies.\n *\n * @param sketch - The sketch to repeat\n * @param count - Total number of copies\n * @param centerXOrOpts - Center X, or options object with `centerX`/`centerY`/`startDeg`\n * @param centerY - Center Y (only when using the two-number form)\n * @returns Union of all copies as a single Sketch\n * @category Tracked Shapes\n */\ndeclare function circularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | {\n centerX?: number;\n centerY?: number;\n startDeg?: number;\n}, centerY?: number): Sketch;\n/**\n * Mirror a shape across a plane and union the mirror with the original.\n *\n * **Details**\n *\n * The mirror plane passes through the origin and is defined by its normal\n * vector. The mirrored copy is unioned with the original to produce a single\n * symmetric Shape.\n *\n * **Example**\n *\n * ```ts\n * // Mirror across the YZ plane (X=0)\n * mirrorCopy(box(50, 30, 10), [1, 0, 0])\n * ```\n *\n * @param shape - The shape to mirror\n * @param normal - Normal of the mirror plane (e.g. `[1, 0, 0]` for YZ plane)\n * @returns Union of original and its mirror\n * @see {@link linearPattern} for linear repetition\n * @see {@link circularPattern} for circular repetition\n * @category Tracked Shapes\n */\ndeclare function mirrorCopy(shape: Shape, normal: [\n number,\n number,\n number\n]): Shape;\n/**\n * Create a 2D rectangle centered at the origin.\n *\n * **Example**\n *\n * ```ts\n * rect(40, 20).extrude(5);\n * ```\n *\n * @param width - Width along the X axis\n * @param height - Height along the Y axis\n * @returns A centered rectangle sketch\n * @category Sketch Primitives\n */\ndeclare function rect(width: number, height: number): Sketch;\n/**\n * Create a 2D circle centered at the origin.\n *\n * **Details**\n *\n * Omit `segments` for a smooth (auto-tessellated) circle. Pass an integer to get a\n * regular polygon approximation — e.g. `6` for a hexagon, `8` for an octagon.\n *\n * **Example**\n *\n * ```ts\n * circle2d(25).extrude(10); // smooth cylinder\n * circle2d(25, 6).extrude(10); // hexagonal prism\n * ```\n *\n * @param radius - Radius of the circle\n * @param segments - Number of polygon sides (omit for smooth)\n * @returns A circle sketch\n * @category Sketch Primitives\n */\ndeclare function circle2d(radius: number, segments?: number): Sketch;\n/**\n * Create a 2D rectangle with rounded corners, centered at the origin.\n *\n * **Details**\n *\n * The corner radius is automatically clamped to `min(width/2, height/2)` so it\n * can never exceed the shape dimensions.\n *\n * **Example**\n *\n * ```ts\n * roundedRect(60, 30, 5).extrude(3);\n * ```\n *\n * @param width - Width along the X axis\n * @param height - Height along the Y axis\n * @param radius - Corner fillet radius (clamped to fit)\n * @returns A rounded rectangle sketch\n * @category Sketch Primitives\n */\ndeclare function roundedRect(width: number, height: number, radius: number): Sketch;\n/**\n * Create a 2D polygon from an array of `[x, y]` points or `Point2D` objects.\n *\n * **Details**\n *\n * Winding order is normalized automatically — clockwise (CW) input is silently\n * reversed to CCW before being passed to the geometry kernel.\n *\n * **Example**\n *\n * ```ts\n * polygon([[0, 0], [50, 0], [25, 40]]).extrude(5); // triangle\n * ```\n *\n * @param points - Closed polygon vertices as `[x, y]` pairs or `Point2D` objects\n * @returns A polygon sketch\n * @category Sketch Primitives\n */\ndeclare function polygon(points: ([\n number,\n number\n] | Point2D)[]): Sketch;\n/**\n * Create a regular polygon inscribed in a circle of the given radius.\n *\n * **Details**\n *\n * `radius` is the center-to-vertex (circumradius) distance. Use `sides` of `3` for a\n * triangle, `6` for a hexagon, etc. The first vertex is at the top (−90° from +X).\n *\n * **Example**\n *\n * ```ts\n * ngon(6, 20).extrude(10); // hexagonal prism, circumradius 20\n * ```\n *\n * @param sides - Number of polygon sides\n * @param radius - Circumradius (center to vertex)\n * @returns A regular polygon sketch\n * @category Sketch Primitives\n */\ndeclare function ngon(sides: number, radius: number): Sketch;\n/**\n * Create a 2D ellipse centered at the origin.\n *\n * **Example**\n *\n * ```ts\n * ellipse(30, 15).extrude(5);\n * ellipse(30, 15, 32).extrude(5); // lower-resolution approximation\n * ```\n *\n * @param rx - Radius along the X axis\n * @param ry - Radius along the Y axis\n * @param segments - Number of polygon sides (default 64)\n * @returns An ellipse sketch\n * @category Sketch Primitives\n */\ndeclare function ellipse(rx: number, ry: number, segments?: number): Sketch;\n/**\n * Create a slot (oblong / stadium shape) — a rectangle with semicircular ends, centered at the origin.\n *\n * **Example**\n *\n * ```ts\n * slot(40, 10).extrude(3); // 40mm long, 10mm wide slot\n * ```\n *\n * @param length - Overall length (tip to tip)\n * @param width - Overall width (= diameter of the semicircular ends)\n * @returns A slot sketch\n * @category Sketch Primitives\n */\ndeclare function slot(length: number, width: number): Sketch;\n/**\n * Create an arc-shaped slot (banana / annular sector) centered at the origin.\n *\n * **Details**\n *\n * The slot is symmetric about the +X axis. The two ends are closed with\n * semicircular caps. `pitchRadius` is the distance from the origin to the\n * centerline of the slot, and `thickness` is the radial width of the slot.\n *\n * **Example**\n *\n * ```ts\n * arcSlot(135, 74, 40).extrude(5); // pitch R135, 74° sweep, 40mm wide\n * ```\n *\n * @param pitchRadius - Distance from the center to the middle of the slot\n * @param sweepDeg - Angular extent of the slot in degrees\n * @param thickness - Radial width of the slot\n * @returns An arc slot sketch\n * @category Sketch Primitives\n */\ndeclare function arcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch;\n/**\n * Create a star shape with alternating outer and inner radii.\n *\n * **Example**\n *\n * ```ts\n * star(5, 30, 12).extrude(4); // five-pointed star\n * ```\n *\n * @param points - Number of star points\n * @param outerR - Radius to the tips\n * @param innerR - Radius to the valleys\n * @returns A star sketch\n * @category Sketch Primitives\n */\ndeclare function star(points: number, outerR: number, innerR: number): Sketch;\ntype Vec3$7 = [\n number,\n number,\n number\n];\ninterface SurfacePatchOptions {\n /** Number of samples along each direction. Default 24. */\n resolution?: number;\n /** Thickness of the generated solid. Default 0 for an open exact sheet. */\n thickness?: number;\n /** Allow explicit approximation for non-exact curve inputs such as Curve3D samples. */\n approximate?: boolean;\n}\n/**\n * Create a smooth surface patch from 4 boundary curves (Coons patch).\n *\n * The four curves form the boundary of a quadrilateral patch:\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 *\n * The interior is filled using bilinear Coons patch interpolation:\n * P(u,v) = Lc(u,v) + Ld(u,v) - B(u,v)\n *\n * The result is a thin solid created by offsetting the surface mesh\n * along its normals by the specified thickness.\n *\n * Note: curves should meet at corners. Small gaps are tolerated.\n */\ndeclare function surfacePatch(curves: {\n bottom: Curve3D | Vec3$7[];\n top: Curve3D | Vec3$7[];\n left: Curve3D | Vec3$7[];\n right: Curve3D | Vec3$7[];\n}, options?: SurfacePatchOptions): Shape;\ninterface SvgImportOptions {\n /**\n * Which geometry channels to include:\n * - `auto`: prefer fills; if no fill geometry exists, fall back to strokes\n * - `fill`: import only filled regions\n * - `stroke`: import only stroke geometry\n * - `fill-and-stroke`: include both\n */\n include?: "auto" | "fill" | "stroke" | "fill-and-stroke";\n /** Keep all disconnected regions, or only the largest. */\n regionSelection?: "all" | "largest";\n /** Keep at most this many regions (largest-first). */\n maxRegions?: number;\n /** Drop regions below this absolute area threshold. */\n minRegionArea?: number;\n /** Drop regions below this ratio of largest-region area. */\n minRegionAreaRatio?: number;\n /**\n * Curve flattening tolerance in SVG user units.\n * Smaller = more segments, higher fidelity.\n */\n flattenTolerance?: number;\n /** Minimum segment count for arc discretization. */\n arcSegments?: number;\n /** Global scale applied after SVG parsing. */\n scale?: number;\n /**\n * Maximum imported sketch width.\n * If exceeded, geometry is uniformly downscaled to fit.\n */\n maxWidth?: number;\n /**\n * Maximum imported sketch height.\n * If exceeded, geometry is uniformly downscaled to fit.\n */\n maxHeight?: number;\n /**\n * Recenter imported geometry so its 2D bounds center is at CAD origin.\n */\n centerOnOrigin?: boolean;\n /** Simplification tolerance for final sketch cleanup. */\n simplify?: number;\n /**\n * Flip SVG Y-down coordinates to CAD Y-up.\n * Enabled by default.\n */\n invertY?: boolean;\n}\ninterface TextOptions {\n /**\n * Cap height of the text in model units.\n * All other dimensions (stroke weight, spacing) scale proportionally.\n * @default 10\n */\n size?: number;\n /**\n * Extra space between characters in model units.\n * Negative values tighten the tracking.\n * @default 0\n */\n letterSpacing?: number;\n /**\n * Horizontal alignment relative to x = 0.\n * - `\'left\'` — left edge at x = 0 (default)\n * - `\'center\'` — centred on x = 0\n * - `\'right\'` — right edge at x = 0\n * @default \'left\'\n */\n align?: "left" | "center" | "right";\n /**\n * Vertical alignment relative to y = 0.\n * - `\'baseline\'` — y = 0 is the text baseline (bottom of capital letters)\n * - `\'center\'` — y = 0 is the vertical midpoint of the cap height\n * - `\'top\'` — y = 0 is the top of capital letters\n * @default \'baseline\'\n */\n baseline?: "baseline" | "center" | "top";\n /**\n * Font to use for text rendering.\n *\n * - `\'sans-serif\'` or `\'inter\'` — bundled Inter font (works everywhere, including browser)\n * - **file path** — path to a TTF, OTF, or WOFF font file (CLI/Node only)\n * - **Font object** — a previously loaded opentype.js Font (from `loadFont()`)\n * - **omitted** — uses the bundled Inter font (same as `\'sans-serif\'`)\n *\n * @example\n * text2d(\'Hello World\', { size: 10 }) // default Inter\n * text2d(\'Custom Font\', { size: 10, font: \'/path/to/font.ttf\' })\n */\n font?: string | opentype$1.Font;\n /**\n * Bezier flattening tolerance in model units.\n * Smaller = more polygon segments = smoother curves.\n * @default auto (0.5% of size)\n */\n flattenTolerance?: number;\n}\n/**\n * Build a filled 2D Sketch from a text string.\n *\n * **Details**\n *\n * The Sketch origin is at the left end of the text baseline by default. Use `align`\n * and `baseline` options to adjust placement. Text is rendered using the bundled\n * Inter font by default, or any TTF/OTF/WOFF font you provide.\n *\n * Alignment 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 * **Example**\n *\n * ```ts\n * // Extruded nameplate\n * text2d(\'FORGE CAD\', { size: 8 }).extrude(1.2);\n *\n * // Centered label on the XY plane\n * text2d(\'V 2.0\', { size: 6, align: \'center\', baseline: \'center\' });\n *\n * // Engraved text cut into the top face of a box\n * const label = text2d(\'REV A\', { size: 5, align: \'center\', baseline: \'center\' });\n * plate.subtract(label.onFace(plate, \'top\', { protrude: -0.5 }).extrude(1));\n *\n * // Custom TTF font\n * text2d(\'Hello\', { size: 10, font: \'/path/to/Arial.ttf\' }).extrude(1);\n *\n * // Pre-loaded font for reuse\n * const font = loadFont(\'/path/to/Arial Bold.ttf\');\n * text2d(\'Title\', { size: 12, font }).extrude(1.5);\n * ```\n *\n * @param content - The text string to render\n * @param options - Rendering options (size, letterSpacing, align, baseline, font, flattenTolerance)\n * @returns A filled Sketch of the rendered letterforms\n * @see {@link textWidth} to measure text width without creating geometry\n * @see {@link loadFont} to pre-load a font for reuse\n * @category Sketch Text\n */\ndeclare function text2d(content: string, options?: TextOptions): Sketch;\n/**\n * Measure the rendered advance width of a string without creating any geometry.\n *\n * **Details**\n *\n * Uses the same font metrics as `text2d()`. Useful for computing layout dimensions\n * before building the actual sketch — e.g. sizing a plate to fit a label.\n *\n * **Example**\n *\n * ```ts\n * const w = textWidth(\'SERIAL: 001\', { size: 6 });\n * const plate = box(w + 10, 12, 2);\n * ```\n *\n * @param content - The text string to measure\n * @param options - Accepts `size`, `letterSpacing`, and `font` (same as `text2d`)\n * @returns Rendered advance width in model units\n * @see {@link text2d} to create the actual geometry\n * @category Sketch Text\n */\ndeclare function textWidth(content: string, options?: Pick<TextOptions, "size" | "letterSpacing" | "font">): number;\ntype Vec3$8 = [\n number,\n number,\n number\n];\ninterface TransitionCurveOptions {\n /**\n * Weight for the start edge. Controls tangent magnitude at the start.\n * - 1.0 (default): balanced transition\n * - > 1.0: curve follows start edge longer before turning\n * - < 1.0: curve turns sooner at the start\n */\n weightA?: number;\n /**\n * Weight for the end edge. Controls tangent magnitude at the end.\n * - 1.0 (default): balanced transition\n * - > 1.0: curve follows end edge longer before turning\n * - < 1.0: curve turns sooner at the end\n */\n weightB?: number;\n /**\n * Number of sample points for the output polyline. Default 64.\n * Higher values give smoother curves at the cost of more geometry.\n */\n samples?: number;\n}\ninterface TransitionSurfaceOptions extends TransitionCurveOptions {\n /**\n * Cross-section profile to sweep along the transition curve.\n * If omitted, a circular profile with `radius` is used.\n */\n profile?: Sketch;\n /**\n * Radius of circular cross-section (used when `profile` is omitted).\n * Default: 5% of chord length.\n */\n radius?: number;\n /**\n * Width and height for rectangular cross-section.\n * Alternative to `radius` when `profile` is omitted.\n */\n rectangleSection?: {\n width: number;\n height: number;\n };\n /**\n * Preferred up vector for the sweep frame. Default: auto-detected.\n */\n up?: Vec3$8;\n /** Edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Extra bounds padding for level-set meshing. */\n boundsPadding?: number;\n}\ninterface TransitionEdge {\n /**\n * Connection point on the edge.\n * Can be any point along the edge where the transition should connect.\n */\n point: Vec3$8;\n /**\n * Tangent direction at the connection point.\n * This is the direction the curve should initially follow when leaving this edge.\n * For a straight edge, this is typically the edge direction pointing "outward"\n * (away from the body of the edge, toward the other edge).\n */\n tangent: Vec3$8;\n /**\n * Surface normal at the connection point (optional).\n * Used as a hint for the sweep frame\'s up vector.\n */\n normal?: Vec3$8;\n}\n/**\n * Create a smooth transition curve between two edges.\n *\n * Returns a `HermiteCurve3D` that starts at `edgeA.point` tangent to\n * `edgeA.tangent` and ends at `edgeB.point` tangent to `edgeB.tangent`.\n *\n * The curve maintains G1 continuity (matching tangent direction) at both\n * endpoints. Weight parameters control the shape of the transition.\n *\n * @example\n * ```js\n * // Connect two edges with a balanced transition\n * const curve = transitionCurve(\n * { point: [0, 0, 0], tangent: [1, 0, 0] },\n * { point: [10, 5, 0], tangent: [1, 0, 0] },\n * );\n *\n * // Weighted: curve hugs edge A longer\n * const weighted = transitionCurve(\n * { point: [0, 0, 0], tangent: [1, 0, 0] },\n * { point: [10, 5, 0], tangent: [1, 0, 0] },\n * { weightA: 2.0, weightB: 0.5 },\n * );\n * ```\n */\ndeclare function transitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D;\n/**\n * Create a solid transition surface between two edges by sweeping a profile\n * along a Hermite transition curve.\n *\n * This produces a watertight solid that smoothly connects the two edges.\n * Works with both Manifold and OCCT backends.\n *\n * @example\n * ```js\n * // Circular tube connecting two edges\n * const 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 * // Custom profile with weights\n * const custom = transitionSurface(\n * { point: [0, 0, 0], tangent: [1, 0, 0] },\n * { point: [10, 5, 3], tangent: [0, 1, 0] },\n * { profile: mySketch, weightA: 1.5, weightB: 0.8 },\n * );\n * ```\n */\ndeclare function transitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape;\ntype EdgeEnd = "start" | "end" | "mid";\ntype TangentMode = "along" | "outward" | "auto";\ninterface ConnectEdgesOptions extends TransitionSurfaceOptions {\n /** Which end of edge A to connect. Default: \'start\'. */\n endA?: EdgeEnd;\n /** Which end of edge B to connect. Default: \'start\'. */\n endB?: EdgeEnd;\n /** Tangent mode for edge A. Default: \'along\'. */\n tangentModeA?: TangentMode;\n /** Tangent mode for edge B. Default: \'along\'. */\n tangentModeB?: TangentMode;\n /** Explicit tangent for edge A. */\n tangentA?: Vec3$8;\n /** Explicit tangent for edge B. */\n tangentB?: Vec3$8;\n /** Flip tangent A. */\n flipA?: boolean;\n /** Flip tangent B. */\n flipB?: boolean;\n}\n/**\n * Create a transition surface or solid bridge between two edge segments.\n *\n * Tangents can be inferred from neighboring geometry or supplied explicitly through `options`.\n * This is useful for loft-like blends where you want a direct connection between two edge spans.\n *\n * @param edgeA - First source edge segment.\n * @param edgeB - Second source edge segment.\n * @param options - Tangent and end-selection options for both edges.\n * @returns The transition shape connecting the two edges.\n */\ndeclare function connectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape;\ninterface ListParamFieldDef {\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n integer?: boolean;\n boolean?: boolean;\n choices?: string[];\n}\n/**\n * Shorthand alias for `Param.number()` — declare a numeric slider parameter.\n *\n * See `Param.number()` for full documentation, examples, and default range rules.\n *\n * @param name - Display label and override key\n * @param defaultValue - Initial value when no override is set\n * @param opts - Range, step, unit, and display options\n * @returns The current value (default or overridden)\n * @see {@link Param} for the full parameter namespace\n * @category Parameters\n * @internal\n */\ndeclare function param(name: string, defaultValue: number, opts?: {\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n integer?: boolean;\n reverse?: boolean;\n}): number;\n/**\n * Shorthand alias for `Param.bool()` — declare a boolean checkbox parameter.\n *\n * See `Param.bool()` for full documentation and examples.\n *\n * @param name - Display label and override key\n * @param defaultValue - Initial checked/unchecked state\n * @returns The current boolean value\n * @see {@link Param} for the full parameter namespace\n * @category Parameters\n * @internal\n */\ndeclare function boolParam(name: string, defaultValue: boolean): boolean;\n/**\n * Shorthand alias for `Param.choice()` — declare a dropdown choice parameter.\n *\n * See `Param.choice()` for full documentation and examples.\n *\n * @param name - Display label and override key\n * @param defaultValue - Must exactly match one of the entries in `choices`\n * @param choices - The list of options shown in the dropdown\n * @returns The currently selected string label\n * @see {@link Param} for the full parameter namespace\n * @category Parameters\n * @internal\n */\ndeclare function choiceParam(name: string, defaultValue: string, choices: string[]): string;\n/**\n * Parameter namespace — the primary way to declare user-facing parameters.\n *\n * **Details**\n *\n * All parameter types live under `Param.*` for discoverability and clean scripts.\n * The standalone functions `param()`, `boolParam()`, and `choiceParam()` are\n * kept as shorthand aliases.\n *\n * **Example**\n *\n * ```ts\n * const width = Param.number("Width", 50, { min: 10, max: 100, unit: "mm" });\n * const label = Param.string("Label", "Hello World");\n * const show = Param.bool("Show Holes", true);\n * const style = Param.choice("Style", "round", ["round", "square"]);\n * ```\n *\n * @concept\n * @category Parameters\n */\ndeclare const Param: {\n /**\n * Declare a numeric parameter that renders as a slider in the UI.\n *\n * **Details**\n *\n * Each call registers a slider control. When the user moves the\n * slider the entire script re-executes with the new value. Parameter values\n * are also overridable from `require()` imports or the CLI `--param` flag —\n * the `name` string is the key used in both cases.\n *\n * Default range rules when options are omitted:\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,\n * `1` for larger ranges\n *\n * The `unit` option is cosmetic only — no conversion is performed.\n * Use `integer: true` for counts, sides, quantities (rounds to whole numbers;\n * step defaults to `1`).\n *\n * **Example**\n *\n * ```ts\n * const width = Param.number("Width", 50);\n * const angle = Param.number("Angle", 45, { min: 0, max: 180, unit: "°" });\n * const 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()\n * const bracket = require("./bracket.forge.js", { Width: 80 });\n *\n * // Via CLI\n * // forgecad run model.forge.js --param "Wall Thickness=3"\n * ```\n *\n * Also available as the shorthand alias `param()`.\n */\n number(name: string, defaultValue: number, opts?: {\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n integer?: boolean;\n reverse?: boolean;\n }): number;\n /**\n * Declare a string parameter that renders as a text input in the UI.\n *\n * **Details**\n *\n * String parameters let users type free-form text — labels, names, inscriptions,\n * file paths, etc. The `name` string is the override key.\n *\n * **Example**\n *\n * ```ts\n * const label = Param.string("Label", "Hello World");\n * const name = Param.string("Name", "Part-001", { maxLength: 20 });\n * ```\n *\n * Override via import:\n * ```ts\n * const tag = require("./tag.forge.js", { Label: "Custom Text" });\n * ```\n *\n * Only available as `Param.string()` — no standalone alias.\n */\n string(name: string, defaultValue: string, opts?: {\n maxLength?: number;\n }): string;\n /**\n * Declare a boolean parameter that renders as a checkbox in the UI.\n *\n * **Details**\n *\n * Internally stored as `0`/`1`. When overriding from CLI or `require()`, pass\n * `1` for true and `0` for false. The `name` string is the override key.\n *\n * **Example**\n *\n * ```ts\n * const showHoles = Param.bool("Show Holes", true);\n * if (showHoles) return difference(plate, cylinder(10, 5).translate(50, 30, 0));\n * return plate;\n * ```\n *\n * Override via import:\n * ```ts\n * const pan = require("./pan.forge.js", { "Show Lid": 0 });\n * ```\n *\n * Also available as the shorthand alias `boolParam()`.\n */\n bool(name: string, defaultValue: boolean): boolean;\n /**\n * Declare a choice parameter that renders as a dropdown in the UI.\n *\n * **Details**\n *\n * `defaultValue` must exactly match one entry in `choices`. Returns the\n * selected string label. Prefer `Param.choice` over `Param.number` when a slider\n * would hide intent — named choices like `"wok"` are self-describing.\n *\n * Overrides may be passed as the choice label string (preferred) or as a\n * numeric index. The `name` string is the override key.\n *\n * **Example**\n *\n * ```ts\n * const panStyle = Param.choice("Pan Style", "frying-pan", ["frying-pan", "saute-pan", "wok"]);\n * if (panStyle === "wok") return buildWok();\n * ```\n *\n * Override via import:\n * ```ts\n * const pan = require("./pan.forge.js", { "Pan Style": "wok" });\n * ```\n *\n * Override via CLI:\n * ```bash\n * forgecad run model.forge.js --param "Pan Style=wok"\n * ```\n *\n * Also available as the shorthand alias `choiceParam()`.\n */\n choice(name: string, defaultValue: string, choices: string[]): string;\n /**\n * Declare a list parameter — an array of struct items with per-field UI controls.\n *\n * **Details**\n *\n * Each item in the list is a struct whose fields each render as their own\n * control (slider, checkbox, or dropdown). The user can add/remove rows up to\n * `minItems`/`maxItems` bounds.\n *\n * Field types:\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 list<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: {\n fields: Partial<Record<keyof T & string, ListParamFieldDef>>;\n minItems?: number;\n maxItems?: number;\n }): T[];\n};\ntype ExactCurveInput = EdgeRef | NurbsCurve3D | Curve3D | HermiteCurve3D | QuinticHermiteCurve3D | Vec3[];\ninterface SurfaceMatchConstraintInput {\n target: EdgeRef;\n continuity?: SurfaceContinuity;\n}\ninterface SurfaceCommonOptions {\n resolution?: number;\n approximate?: boolean;\n}\ninterface SurfacePatchOptions$1 extends SurfaceCommonOptions {\n match?: Partial<Record<"u0" | "u1" | "v0" | "v1", SurfaceMatchConstraintInput>>;\n style?: SurfaceFillStyle;\n}\ninterface SurfaceFillInput extends SurfaceCommonOptions {\n boundaries: Array<{\n name: string;\n curve: ExactCurveInput;\n }>;\n match?: Record<string, SurfaceMatchConstraintInput>;\n style?: SurfaceFillStyle;\n}\ninterface SurfaceBoundaryInput extends SurfaceCommonOptions {\n u: [\n ExactCurveInput,\n ExactCurveInput\n ];\n v: [\n ExactCurveInput,\n ExactCurveInput\n ];\n match?: Partial<Record<"u0" | "u1" | "v0" | "v1", SurfaceMatchConstraintInput>>;\n style?: SurfaceFillStyle;\n}\ninterface SurfaceExtendOptions {\n edge: "u0" | "u1" | "v0" | "v1";\n length: number;\n continuity?: SurfaceContinuity;\n}\ninterface SurfacePlaneOp {\n normal: Vec3;\n offset?: number;\n}\ninterface BlendEdgeOptions {\n shape?: Shape;\n edges: EdgeRef[];\n radius: number;\n continuity?: SurfaceContinuity;\n segments?: number;\n}\ninterface BlendCornerYOptions extends Omit<BlendEdgeOptions, "edges"> {\n edges: EdgeRef[];\n cornerTolerance?: number;\n minBranchAngleDeg?: number;\n}\ninterface BlendSurfaceOptions extends SurfaceFillInput {\n}\ninterface EdgeContinuityThresholds {\n continuity?: SurfaceContinuity;\n samples?: number;\n positionTolerance?: number;\n tangentToleranceDeg?: number;\n curvatureTolerance?: number;\n}\ninterface EdgeContinuityEdgeReport {\n edgeIndex: number;\n continuity: SurfaceContinuity;\n maxPositionalGap: number;\n maxTangentAngleDeg: number;\n maxCurvatureGap: number;\n}\ninterface EdgeContinuityReport {\n requested: SurfaceContinuity;\n passed: boolean;\n sampledEdges: number;\n edges: EdgeContinuityEdgeReport[];\n worst: {\n continuity: SurfaceContinuity;\n maxPositionalGap: number;\n maxTangentAngleDeg: number;\n maxCurvatureGap: number;\n };\n}\ninterface CurvatureSample {\n t: number;\n curvature: number;\n point: Vec3;\n}\ninterface SurfaceHealthReport {\n tinyEdges: Array<{\n index: number;\n length: number;\n }>;\n sliverFaces: Array<{\n index: number;\n area: number;\n sliverScore: number;\n }>;\n valid: boolean;\n}\ndeclare const Surface: {\n Nurbs(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape;\n Ruled(curveA: ExactCurveInput, curveB: ExactCurveInput, options?: SurfaceCommonOptions): Shape;\n Patch(curves: {\n bottom: ExactCurveInput;\n top: ExactCurveInput;\n left: ExactCurveInput;\n right: ExactCurveInput;\n }, options?: SurfacePatchOptions$1): Shape;\n Boundary(input: SurfaceBoundaryInput): Shape;\n Fill(input: SurfaceFillInput): Shape;\n Sew(shapes: Shape[], options?: {\n tolerance?: number;\n }): Shape;\n Extend(shape: Shape, options: SurfaceExtendOptions): Shape;\n Trim(shape: Shape, tool: Shape | SurfacePlaneOp): Shape;\n Split(shape: Shape, tool: Shape | SurfacePlaneOp): [\n Shape,\n Shape\n ];\n Match(shape: Shape, options: {\n edge: "u0" | "u1" | "v0" | "v1";\n target: EdgeRef;\n continuity?: SurfaceContinuity;\n }): Shape;\n MatchEdge(shape: Shape, options: {\n edge: "u0" | "u1" | "v0" | "v1";\n target: EdgeRef;\n continuity?: SurfaceContinuity;\n }): Shape;\n};\ndeclare const Blend: {\n Edge(options: BlendEdgeOptions): Shape;\n Surface(options: BlendSurfaceOptions): Shape;\n /**\n * @alpha\n * Current implementation uses continuity-controlled edge fillets on solid edges.\n * It does not yet provide a dedicated open-sheet or multi-patch Y-corner solver.\n * Follow progress: https://github.com/KoStard/forgecad-private/issues/162\n */\n CornerY(options: BlendCornerYOptions): Shape;\n};\ndeclare const Analysis: {\n EdgeContinuity(shape: Shape, options?: EdgeContinuityThresholds): EdgeContinuityReport;\n SurfaceContinuity(shape: Shape, options?: EdgeContinuityThresholds): EdgeContinuityReport;\n CurvatureComb(input: NurbsCurve3D | EdgeRef, options?: {\n samples?: number;\n }): CurvatureSample[];\n SurfaceHealth(shape: Shape, options?: {\n tinyEdgeThreshold?: number;\n sliverThreshold?: number;\n }): SurfaceHealthReport;\n};\ntype VerificationStatus = "pass" | "fail";\ninterface VerificationResult {\n id: string;\n label: string;\n status: VerificationStatus;\n message: string;\n /** 1-based source line number, if captured from the call stack */\n line?: number;\n /** Human-readable expected value for display */\n expected?: string;\n /** Human-readable actual value for display */\n actual?: string;\n /** Spec name — when set, the UI groups this result under a collapsible section */\n group?: string;\n}\ninterface SpecResult {\n /** Spec name */\n name: string;\n /** Number of checks that passed */\n passed: number;\n /** Total number of checks */\n total: number;\n /** The individual verification results added by this spec */\n results: VerificationResult[];\n}\n/**\n * A named, reusable bundle of verification checks.\n *\n * Create one with `spec(name, checkFn)`, then call `.check(...shapes)` to\n * run the checks. Results appear grouped under the spec name in the Checks\n * panel. Can be reused across multiple shapes and imported via `require()`.\n *\n * @category Parameters\n */\ninterface Spec {\n /** The display name of this spec */\n readonly name: string;\n /**\n * Run this spec\'s checks against one or more shapes.\n * All `verify.*` calls inside the check function are automatically\n * grouped under this spec\'s name in the Checks panel.\n */\n check(...args: unknown[]): SpecResult;\n}\n/**\n * Create a named, reusable bundle of verification checks.\n *\n * **Details**\n *\n * A spec groups related `verify.*` calls under a collapsible header in the\n * Checks panel. This makes large check suites scannable. Specs can be applied\n * to multiple shapes and can check relationships between parts.\n *\n * Specs can be defined in separate `.forge.js` files and imported via\n * `require()` to share them across models.\n *\n * `spec.check()` returns a `SpecResult` — you can inspect it programmatically\n * or ignore the return value and let the Checks panel show results.\n *\n * **Example**\n *\n * ```ts\n * const 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\n * printable.check(bracket);\n * printable.check(standoff);\n *\n * // Check relationships between parts\n * const fitSpec = spec("Assembly fit", (partA, partB) => {\n * verify.notColliding("No interference", partA, partB, 10);\n * });\n * fitSpec.check(bracket, standoff);\n * ```\n *\n * **Spec-first workflow:** Write specs before building geometry. Checks go\n * from red to green as you build — effectively TDD for CAD.\n *\n * @param name - Display name in the Checks panel (e.g. `"Fits printer bed"`)\n * @param checkFn - Function that calls `verify.*` methods; receives args from `.check()`\n * @returns A `Spec` object with a `.check(...args)` method\n * @category Parameters\n */\ndeclare function spec(name: string, checkFn: (...args: any[]) => void): Spec;\ninterface ShapeLike {\n boundingBox(): {\n min: number[];\n max: number[];\n };\n isEmpty(): boolean;\n volume(): number;\n surfaceArea(): number;\n}\ninterface FaceRefLike {\n normal: [\n number,\n number,\n number\n ];\n center: [\n number,\n number,\n number\n ];\n}\ndeclare const verify: {\n /**\n * Custom predicate check.\n *\n * @param label Short name shown in the panel ("gear clearance")\n * @param check Function that returns true (pass) or false (fail)\n * @param message Optional extra context shown on failure\n */\n that(label: string, check: () => boolean, message?: string): void;\n /**\n * Check that two numbers are approximately equal (within tolerance).\n */\n equal(label: string, actual: number, expected: number, tolerance?: number, message?: string): void;\n /**\n * Check that two numbers are NOT equal (differ by more than tolerance).\n */\n notEqual(label: string, actual: number, unexpected: number, tolerance?: number, message?: string): void;\n /** Check that actual > min. */\n greaterThan(label: string, actual: number, min: number, message?: string): void;\n /** Check that actual < max. */\n lessThan(label: string, actual: number, max: number, message?: string): void;\n /** Check that min <= actual <= max. */\n inRange(label: string, actual: number, min: number, max: number, message?: string): void;\n /**\n * Check that the bounding-box centers of two shapes coincide within tolerance (mm).\n */\n centersCoincide(label: string, a: ShapeLike, b: ShapeLike, tolerance?: number): void;\n /**\n * Check that two shapes do not collide (minGap > 0).\n *\n * @param searchLength Search radius for minGap (mm, default 1.0)\n */\n notColliding(label: string, a: ShapeLike, b: ShapeLike, searchLength?: number): void;\n /**\n * Check that a minimum clearance gap exists between two shapes.\n */\n minClearance(label: string, a: ShapeLike, b: ShapeLike, minGap: number, searchLength?: number): void;\n /**\n * Check that two face normals are parallel (within toleranceDeg degrees).\n */\n parallel(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void;\n /**\n * Check that two face normals are perpendicular (within toleranceDeg degrees).\n */\n perpendicular(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void;\n /**\n * Check that a face is coplanar with (same plane as) another face,\n * meaning they are parallel AND their centers lie on the same plane.\n */\n coplanar(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number, toleranceMm?: number): void;\n /**\n * Check that a face center lies at a specific position (within toleranceMm).\n */\n faceAt(label: string, face: FaceRefLike, expectedPos: [\n number,\n number,\n number\n ], toleranceMm?: number): void;\n /**\n * Check that two face normals point in the same direction (not antiparallel).\n * Stricter than parallel — both |angle| AND sign must match.\n */\n sameDirection(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void;\n /**\n * Check that a shape is empty.\n */\n isEmpty(label: string, shape: ShapeLike, message?: string): void;\n /**\n * Check that a shape is NOT empty.\n */\n notEmpty(label: string, shape: ShapeLike, message?: string): void;\n /**\n * Check that a shape\'s volume is approximately equal to expected (mm³).\n *\n * @param expected Expected volume in mm³\n * @param tolerance Absolute tolerance in mm³ (default 1.0)\n */\n volumeApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void;\n /**\n * Check that a shape\'s surface area is approximately equal to expected (mm²).\n *\n * @param expected Expected surface area in mm²\n * @param tolerance Absolute tolerance in mm² (default 1.0)\n */\n areaApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void;\n /**\n * Check that a shape\'s bounding box has approximately the given size.\n *\n * @param expectedSize [sizeX, sizeY, sizeZ] in mm\n * @param tolerance Per-axis tolerance in mm (default 0.1)\n */\n boundingBoxSize(label: string, shape: ShapeLike, expectedSize: [\n number,\n number,\n number\n ], tolerance?: number): void;\n /**\n * Check that every sampled seam on a shape meets a requested continuity threshold.\n */\n edgeContinuity(label: string, shape: ShapeLike, options?: EdgeContinuityThresholds): void;\n /**\n * Check that a shape has no tiny edges below the requested threshold.\n */\n noTinyEdges(label: string, shape: ShapeLike, threshold?: number): void;\n /**\n * Check that a shape has no sliver faces below the requested score threshold.\n */\n noSliverFaces(label: string, shape: ShapeLike, threshold?: number): void;\n /**\n * Best-effort exact-shape validity guard for self-intersections or broken B-Rep topology.\n */\n noSelfIntersection(label: string, shape: ShapeLike): void;\n};\n/**\n * Register a mock (context) object for visualization and collision checking.\n *\n * **Details**\n *\n * Mock objects appear in the viewport and spatial analysis when you run a\n * file directly, but are excluded when the file is imported via `require()`.\n * This lets you model the surrounding context — walls, bolts, mating parts —\n * without polluting the module\'s exports.\n *\n * The shape is returned unchanged, so you can reference it for alignment,\n * dimensioning, and `verify` checks.\n *\n * Mock objects participate in `forgecad run` collision detection and spatial\n * analysis. Their names appear with a `(mock)` suffix in reports.\n *\n * In the viewport, mock objects render at reduced opacity so they are\n * visually distinct from real geometry.\n *\n * **Example**\n *\n * ```ts\n * // bracket.forge.js\n * const wall = mock(box(100, 200, 10).translate(0, 0, -5), "wall");\n * const bolt = mock(cylinder(3, 15).translate(10, 15, 0), "bolt");\n *\n * const bracket = box(20, 30, 5);\n * verify.notColliding("bracket vs wall", bracket, wall);\n *\n * return bracket;\n * // When imported: only bracket is exported\n * // When run directly: bracket + wall + bolt all visible\n * ```\n *\n * @param shape - The context shape to register as a mock\n * @param name - Optional display name (defaults to "Mock N")\n * @returns The same shape, unchanged — use it for alignment and verify checks\n * @category Context\n */\ndeclare function mock<T extends Shape>(shape: T, name?: string): T;\ntype SculptBlendInput = SdfShape | readonly SdfShape[];\ninterface SculptBlendOptions {\n /** Smooth blend radius in model units. Default: 4. */\n radius?: number;\n}\ninterface SculptBoxOptions {\n /** Rounded-box radius. Omit for a sharp SDF box. */\n radius?: number;\n}\ninterface SculptTubeOptions {\n /** Tube radius in model units. Used as the default when points do not carry their own radius. Default: 3. */\n radius?: number;\n /** Smooth blend radius between tube segments and joints. Default: smallest point radius. */\n blend?: number;\n /** Smooth the control polyline into a Catmull-Rom curve before sweeping. Default: false for tube/path, true for curve. */\n curve?: boolean | "linear" | "catmull-rom";\n /** Curve samples per control-point interval when curve smoothing is enabled. Default: 8, capped at 8 for realtime preview. */\n segments?: number;\n /** Catmull-Rom tangent scale when curve smoothing is enabled. Default: 0.5. */\n tension?: number;\n /** Material/color preset applied to the final tube. */\n polish?: SculptPolishInput;\n}\ntype SculptVec3 = Vec3$1 | readonly [\n number,\n number,\n number\n];\ntype SculptPathPoint = SculptVec3 | readonly [\n number,\n number,\n number,\n number\n] | {\n point: SculptVec3;\n radius?: number;\n} | {\n position: SculptVec3;\n radius?: number;\n} | {\n at: SculptVec3;\n radius?: number;\n};\ntype SculptPointList = readonly SculptPathPoint[];\ntype SculptLookPreset = "gallery" | "soft-studio" | "candy-shop" | "midnight" | "workbench";\ntype SculptBlendArg = SculptBlendInput | SculptBlendOptions;\ndeclare function sculptSphere(radius: number): SdfShape;\ndeclare function sculptBox(x: number, y: number, z: number, options?: SculptBoxOptions): SdfShape;\ndeclare function sculptCylinder(height: number, radius: number): SdfShape;\ndeclare function sculptDisk(radius: number, thickness?: number): SdfShape;\ndeclare function sculptCapsule(height: number, radius: number): SdfShape;\ndeclare function sculptTorus(majorRadius: number, minorRadius: number): SdfShape;\ndeclare function sculptCone(height: number, radius: number): SdfShape;\ndeclare function sculptTubePublic(points: SculptPointList, options?: SculptTubeOptions): SdfShape;\ndeclare function sculptCurve(points: SculptPointList, options?: SculptTubeOptions): SdfShape;\ndeclare function sculptBlend(first?: SculptBlendArg, optionsOrShape?: SculptBlendArg, ...rest: SculptBlendArg[]): SdfShape;\ndeclare function sculptUnion(first?: SculptBlendInput, ...rest: SculptBlendInput[]): SdfShape;\ndeclare function sculptCarve(base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions): SdfShape;\ndeclare function sculptKeep(first?: SculptBlendArg, optionsOrShape?: SculptBlendArg, ...rest: SculptBlendArg[]): SdfShape;\ndeclare function sculptPolish(shape: SdfShape, input?: SculptPolishInput): SdfShape;\ndeclare function sculptMaterial(input?: SculptPolishInput): ShapeMaterialProps & {\n color?: string;\n};\ndeclare function sculptLook(preset?: SculptLookPreset): SceneOptions;\ndeclare const Sculpt: {\n /** Create a liquid SDF sphere centered at the origin. */\n sphere: typeof sculptSphere;\n /** Create a liquid SDF box; pass `{ radius }` for a rounded box. */\n box: typeof sculptBox;\n /** Create a liquid SDF cylinder centered at the origin, axis along Z. */\n cylinder: typeof sculptCylinder;\n /** Create a thin circular disk centered at the origin, axis along Z. Useful as a circular cutter or insert. */\n disk: typeof sculptDisk;\n /** Alias for `Sculpt.disk()`. */\n circle: typeof sculptDisk;\n /** Create a liquid SDF capsule centered at the origin, axis along Z. */\n capsule: typeof sculptCapsule;\n /** Create a liquid SDF torus lying in the XY plane. */\n torus: typeof sculptTorus;\n /** Create a liquid SDF cone. */\n cone: typeof sculptCone;\n /** Create a smooth tube through a list of 3D points. */\n tube: typeof sculptTubePublic;\n /** Create a smooth variable-thickness sweep through 3D control points. */\n curve: typeof sculptCurve;\n /** Alias for `Sculpt.tube()`; points may use [x, y, z, radius] for variable thickness. */\n path: typeof sculptTubePublic;\n /** Smoothly blend one or more SDF shapes into a continuous body. */\n blend: typeof sculptBlend;\n /** Sharply union one or more SDF shapes. */\n union: typeof sculptUnion;\n /** Smoothly subtract one or more cutter shapes from a base shape. */\n carve: typeof sculptCarve;\n /** Smoothly intersect one or more SDF shapes. */\n keep: typeof sculptKeep;\n /** Apply a Sculpt material preset or direct material properties. */\n polish: typeof sculptPolish;\n /** Resolve a Sculpt material preset to ForgeCAD material properties. */\n material: typeof sculptMaterial;\n /** Return a polished scene preset tuned for liquid SDF preview. */\n look: typeof sculptLook;\n /** List the built-in Sculpt material preset names. */\n knownMaterials: typeof knownSculptMaterialPresets;\n};\n/**\n * SDF modeling — signed distance field primitives, smooth booleans, TPMS lattices,\n * domain warps, and surface patterns.\n *\n * Return `SdfShape` values directly from a ForgeCAD script for native raymarch\n * preview. Plain objects and arrays of SDF leaves are renderable too, so object\n * keys become named preview parts.\n *\n * Call `.toShape()` or `toShape(...)` only when you need a mesh-backed ForgeCAD\n * Shape for export, mesh booleans, or mixed SDF/manifold projects. All shapes\n * live as a lazy expression tree until that materialization boundary.\n *\n * SDF is inherently implicit and sampled, not B-rep/exact geometry. Use it with\n * caution when precision, tolerances, or exact export matter.\n *\n * @example\n * ```js\n * return sdf.smoothUnion(sdf.sphere(10), sdf.box(15, 15, 15), { radius: 3 })\n * .color(\'#4488cc\');\n * ```\n *\n * @example\n * ```js\n * return {\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 */\ndeclare const sdf: {\n /** Create an SDF sphere centered at the origin. */\n sphere: typeof sphere;\n /** Create an SDF box centered at the origin with given full dimensions (not half-extents). */\n box: typeof box;\n /** Create an SDF cylinder centered at the origin, axis along Z. */\n cylinder: typeof cylinder;\n /** Create an SDF torus centered at the origin, lying in the XY plane. */\n torus: typeof torus;\n /** Create an SDF capsule centered at the origin, axis along Z. */\n capsule: typeof capsule;\n /** Create an SDF cone with base at z=0 and tip at z=height. */\n cone: typeof cone;\n /** Smooth union — blends shapes together with a smooth transition radius. */\n smoothUnion: typeof smoothUnion;\n /** Smooth difference — smoothly subtracts b from a. */\n smoothDifference: typeof smoothDifference;\n /** Smooth intersection — smoothly intersects a and b. */\n smoothIntersection: typeof smoothIntersection;\n /** Morph between two SDF shapes. t=0 → a, t=1 → b. */\n morph: typeof morph;\n /**\n * Spatially blend between two SDF patterns.\n * The blend function receives (x, y, z) and returns 0..1:\n * 0 = fully pattern `a`, 1 = fully pattern `b`.\n */\n blend: typeof blend;\n /** Gyroid TPMS lattice — the most common lattice for additive manufacturing. */\n gyroid: typeof gyroid;\n /** Schwarz-P TPMS lattice — isotropic pore structure. */\n schwarzP: typeof schwarzP;\n /** Diamond TPMS lattice — stiffest TPMS structure. */\n diamond: typeof diamond;\n /** Lidinoid TPMS lattice — visually distinct from gyroid, popular in research and art. */\n lidinoid: typeof lidinoid;\n /** TPMS block preset clipped to an explicit design space. */\n tpmsBlock: typeof tpmsBlock;\n /** Clip an SDF shape to a box-shaped design space. */\n withinBox: typeof withinBox;\n /** 3D Simplex noise field — produces organic, natural-looking displacements. */\n noise: typeof noise;\n /** 3D Voronoi pattern — organic cellular structures like bone, coral, or soap bubbles. */\n voronoi: typeof voronoi;\n /** Honeycomb (hexagonal) lattice pattern. Intersect with your shape to apply. */\n honeycomb: typeof honeycomb;\n /** Sinusoidal wave ridges — parallel ridges along an axis. */\n waves: typeof waves;\n /** Knurl pattern — crossed helical grooves for grips and handles. */\n knurl: typeof knurl;\n /** Perforated plate pattern — regular array of cylindrical holes. */\n perforated: typeof perforated;\n /** Fish/dragon scale pattern — overlapping circular scales in hex-packed rows. */\n scales: typeof scales;\n /** Brick/stone wall pattern — running bond with mortar grooves. */\n brick: typeof brick;\n /** Grid lattice pattern — two families of infinite slabs crossing at 90°. */\n weave: typeof weave;\n /** Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`. */\n basketWeave: typeof basketWeave;\n /** Twist an SDF shape around the Z axis. */\n twist: typeof twist;\n /** Bend an SDF shape around the Z axis. */\n bend: typeof bend;\n /** Repeat an SDF shape in space. */\n repeat: typeof repeat;\n /** A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`. */\n SurfacePattern: typeof SurfacePattern;\n /** Create a custom SDF from one expression; shader-safe expressions raymarch directly. */\n fromFunction: typeof fromFunction;\n /** Sculpt-like facade: friendly liquid-modeling verbs backed by the same SDF kernel. */\n Sculpt: {\n sphere: (radius: number) => SdfShape;\n box: (x: number, y: number, z: number, options?: SculptBoxOptions) => SdfShape;\n cylinder: (height: number, radius: number) => SdfShape;\n disk: (radius: number, thickness?: number) => SdfShape;\n circle: (radius: number, thickness?: number) => SdfShape;\n capsule: (height: number, radius: number) => SdfShape;\n torus: (majorRadius: number, minorRadius: number) => SdfShape;\n cone: (height: number, radius: number) => SdfShape;\n tube: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape;\n curve: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape;\n path: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape;\n blend: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape;\n union: (first?: SculptBlendInput, ...rest: SculptBlendInput[]) => SdfShape;\n carve: (base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions) => SdfShape;\n keep: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape;\n polish: (shape: SdfShape, input?: SculptPolishInput) => SdfShape;\n material: (input?: SculptPolishInput) => ShapeMaterialProps & {\n color?: string;\n };\n look: (preset?: SculptLookPreset) => SceneOptions;\n knownMaterials: typeof knownSculptMaterialPresets;\n };\n};\ndeclare function initKernel(): Promise<unknown>;\ntype ActiveBackend = "occt" | "manifold";\ndeclare function setActiveBackend(backend: ActiveBackend): void;\n/**\n * Set the active backend and ensure its WASM module is initialized.\n * Call this instead of `setActiveBackend` when you\'re about to execute code —\n * it guarantees the backend is ready, not just selected.\n */\ndeclare function activateBackend(backend: ActiveBackend): Promise<void>;\ndeclare function getActiveBackend(): ActiveBackend;\ntype GeometryBackend = "manifold" | "occt" | "hybrid" | "unknown";\ntype GeometryRepresentation = "mesh-solid" | "brep-solid" | "surface" | "mixed";\ntype GeometryFidelity = "kernel-native" | "exact" | "sampled" | "deformed" | "mixed" | "unknown";\ntype GeometryTopology = "none" | "synthetic" | "kernel";\ntype GeometrySource = "primitive" | "extrude" | "revolve" | "boolean" | "sheet-metal" | "shell" | "fillet" | "chamfer" | "level-set" | "loft" | "sweep" | "surface-patch" | "deform" | "draft" | "offset-solid" | "imported" | "sdf" | "fromSlices" | "unknown";\ninterface GeometryInfo {\n backend: GeometryBackend;\n representation: GeometryRepresentation;\n fidelity: GeometryFidelity;\n topology: GeometryTopology;\n sources: GeometrySource[];\n}\ninterface ShapeLike$1 {\n toShape(): Shape;\n}\ntype ShapeOperandInput = Shape | ShapeLike$1 | readonly (Shape | ShapeLike$1)[];\ntype ShapeRuntimePayload = ShapeBackend | Manifold;\ntype ShapeAnchorTarget = Shape | {\n referencePoint(ref: string): [\n number,\n number,\n number\n ];\n} | {\n _bbox(): {\n min: number[];\n max: number[];\n };\n};\ntype RotationPointLike = PlacementAnchorLike | Vec3;\ninterface ShapeMaterialProps {\n /** Metalness factor (0 = dielectric, 1 = metal). Default: 0.05 */\n metalness?: number;\n /** Roughness factor (0 = mirror, 1 = fully diffuse). Default: 0.35 */\n roughness?: number;\n /** Emissive glow color (hex string, e.g. "#ff6b35"). */\n emissive?: string;\n /** Emissive intensity multiplier. Default: 1 */\n emissiveIntensity?: number;\n /** Opacity (0 = fully transparent, 1 = fully opaque). Default: 1 */\n opacity?: number;\n /** Render as wireframe. Default: false */\n wireframe?: boolean;\n /** Clearcoat intensity (0–1). Default: 0.1 */\n clearcoat?: number;\n /** Clearcoat roughness (0–1). Default: 0.4 */\n clearcoatRoughness?: number;\n /** Glass/translucency transmission factor (0–1). Renderer support depends on target. */\n transmission?: number;\n /** Index of refraction for transmissive materials. Typical glass is ~1.45. */\n ior?: number;\n /** Approximate transmissive volume thickness in model units. */\n thickness?: number;\n /** Specular highlight intensity (0–1). */\n specularIntensity?: number;\n /** Specular highlight tint. */\n specularColor?: string;\n /** Reflection strength for supported renderers (0–1). */\n reflectivity?: number;\n}\ntype ShapeReferenceKind = "face" | "face-set" | "edge" | "edge-set" | "point" | "point-set";\ntype ShapeReferenceStatus = "alive" | "deleted" | "ambiguous";\ntype ShapeReferenceCardinality = "zero" | "one" | "many";\ninterface ShapeReferenceResolution {\n path: string;\n sourcePath: string;\n shapeName?: string;\n kind: ShapeReferenceKind;\n status: ShapeReferenceStatus;\n cardinality: ShapeReferenceCardinality;\n rule: string;\n faces: FaceRef[];\n edges: EdgeSegment[];\n points: Vec3[];\n}\n/**\n * A first-class reference path over a shape\'s semantic faces and face relationships.\n *\n * Created with `shape.ref("lid/back")`, then refined through methods such as\n * `.point()` or `.edges()`. The reference stores intent as a readable path and\n * resolves lazily against the current shape metadata.\n */\ndeclare class ShapeRef {\n private readonly shape;\n readonly path: string;\n private readonly optional;\n constructor(shape: Shape, path: string, optional?: boolean);\n /** Resolve this reference into its current faces, edges, or points. */\n resolve(): ShapeReferenceResolution;\n /** The resolved reference kind, such as `face`, `edge-set`, or `point`. */\n get kind(): ShapeReferenceKind;\n /** Whether the reference currently resolves to zero, one, or many matches. */\n get cardinality(): ShapeReferenceCardinality;\n /** Return the reference lifecycle status for the current shape state. */\n status(): ShapeReferenceStatus;\n /** Return a human-readable explanation of how this reference resolved. */\n explain(): string;\n /** Name this derived reference so the same shape can resolve it by `shape.ref(name)`. */\n as(name: string): ShapeRef;\n /** Return an optional reference that resolves to zero matches instead of throwing when missing. */\n maybe(): ShapeRef;\n /** Mark that a multi-match reference is intentionally being used as a set. */\n all(): ShapeRef;\n /** Require this reference to resolve to exactly one match. */\n one(): ShapeRef;\n /** Resolve this reference as one or more faces. */\n faces(): FaceRef[];\n /** Resolve this reference as exactly one face. */\n face(): FaceRef;\n /** Resolve this reference as one or more edges. Face references return boundary edges. */\n edges(): EdgeSegment[];\n /** Resolve this reference as exactly one edge. */\n edge(): EdgeSegment;\n /** Resolve this reference as one or more points. Faces use centers and edges use midpoints. */\n points(): Vec3[];\n /** Resolve this reference as exactly one point. */\n point(): Vec3;\n /** Return the structured JSON-friendly reference resolution. */\n toJSON(): ShapeReferenceResolution;\n /** Return a compact display form for this reference path. */\n toString(): string;\n}\n/**\n * Core 3D solid shape. All operations are immutable and return new shapes.\n *\n * Supports transforms (translate, rotate, scale, mirror, transform, rotateAround, pointAlong),\n * booleans (add, subtract, intersect), cutting (split, splitByPlane, trimByPlane),\n * shelling, anchor positioning (attachTo, onFace), placement references, and queries\n * (volume, surfaceArea, boundingBox, isEmpty, numTri, geometryInfo).\n */\ndeclare class Shape {\n colorHex: string | undefined;\n materialProps: ShapeMaterialProps | undefined;\n constructor(payload: ShapeRuntimePayload, color?: string, geometryInfo?: Partial<GeometryInfo>);\n /** @internal Use .color() instead. */\n setColor(value: string | undefined): Shape;\n /** Set the color of this shape (hex string, e.g. "#ff0000"). Returns a new Shape with the color applied. */\n color(value: string | undefined): Shape;\n /**\n * Set PBR material properties for this shape\'s visual appearance.\n *\n * **Details**\n *\n * Returns a new Shape with the specified material properties merged on top of any previously\n * set properties. All properties are optional — omitted keys retain their current value.\n * Material properties survive transforms and boolean operations.\n *\n * Use `.color()` to set the base diffuse color; `.material()` controls how that color behaves\n * under light (metalness, roughness, clearcoat) and can add emissive glow independent of\n * lighting. Emissive glow pairs naturally with the `postProcessing.bloom` effect in `scene()`.\n *\n * **Example**\n *\n * ```js\n * box(50, 50, 50).material({ metalness: 0.9, roughness: 0.1 }); // polished metal\n * sphere(30).material({ emissive: \'#ff6b35\', emissiveIntensity: 2 }); // glowing\n * cylinder(40, 20).material({ opacity: 0.4, clearcoat: 1.0, clearcoatRoughness: 0.02 }); // ice\n *\n * // Chainable with other shape methods\n * box(100, 100, 10).color(\'#gold\').material({ metalness: 0.95, roughness: 0.05 }).translate(0, 0, 50);\n * ```\n *\n * @param props.metalness - Metallic vs. dielectric (0–1). Default: 0.05\n * @param props.roughness - Surface roughness; 0 = mirror, 1 = matte. Default: 0.35\n * @param props.emissive - Glow color hex string (e.g. `\'#ff6b35\'`). Glows independently of lighting\n * @param props.emissiveIntensity - Glow multiplier. Default: 1\n * @param props.opacity - Transparency; 0 = fully transparent, 1 = fully opaque. Default: 1\n * @param props.wireframe - Render as wireframe. Default: false\n * @param props.clearcoat - Clearcoat layer intensity (0–1). Default: 0.1\n * @param props.clearcoatRoughness - Clearcoat layer roughness (0–1). Default: 0.4\n * @returns A new Shape with the merged material properties applied\n * @category Materials\n */\n material(props: ShapeMaterialProps): Shape;\n /** Return a new Shape wrapper for explicit duplication in scripts. */\n clone(): Shape;\n /** Alias for clone() */\n duplicate(): Shape;\n /** Inspect which backend/representation produced this solid. */\n geometryInfo(): GeometryInfo;\n /** Name this shape as a reference namespace for diagnostics and future published refs. */\n as(name: string): Shape;\n /** Resolve a semantic reference path like `lid`, `lid/back`, or a midpoint selector on `lid/back`. */\n ref(path: string): ShapeRef;\n /** Attach named placement references that survive normal transforms and imports. */\n withReferences(refs: PlacementReferenceInput): Shape;\n /** List named placement references carried by this shape. */\n referenceNames(kind?: PlacementReferenceKind): string[];\n /**\n * Deprecated alias for `withConnectors()`.\n * @deprecated Use `withConnectors()` instead.\n */\n withPorts(ports: Record<string, PortInput>): Shape;\n /**\n * Deprecated alias for `connectorNames()`.\n * @deprecated Use `connectorNames()` instead.\n */\n portNames(): string[];\n /** Resolve a named placement reference or built-in anchor to a 3D point. */\n referencePoint(ref: PlacementAnchorLike): [\n number,\n number,\n number\n ];\n /**\n * Resolve a face by user-authored label or compiler-owned name. Returns a `FaceRef`\n * that can be passed to `.onFace()`, `projectToPlane()`, or used directly in placement.\n *\n * **Details**\n *\n * `.face(name)` is a pure label lookup — it finds faces by user-authored labels, not by\n * geometric queries. Labels are born in sketches via `.label()` / `.labelEdges()` and\n * grow into face names through extrude, loft, revolve, and sweep. They are stable\n * references that travel with the geometry.\n *\n * Labels must be unique within a shape. Use `.prefixLabels()` before combining shapes\n * with `union()` / `difference()` to avoid collisions. Collision detection throws a\n * clear error with a fix suggestion.\n *\n * For compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape\'s\n * compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops)\n * are resolved via coplanar triangle clustering.\n *\n * **Example**\n *\n * ```ts\n * // Edge labels become side face names after extrude\n * const 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\');\n * const room = profile.extrude(30, { labels: { start: \'base\', end: \'top\' } });\n * room.face(\'floor\'); // side face from the labeled edge\n * room.face(\'base\'); // base cap (user-specified)\n *\n * // .labelEdges() shorthand for sequential edge labeling\n * const plate = rect(100, 50).labelEdges(\'south\', \'east\', \'north\', \'west\');\n * const solid = plate.extrude(20, { labels: { start: \'bottom\', end: \'top\' } });\n * solid.face(\'south\'); // side face\n *\n * // Prefix before combining to avoid collisions\n * const left = wing.prefixLabels(\'l/\');\n * const right = wing.mirror([1, 0, 0]).prefixLabels(\'r/\');\n * const full = union(left, right);\n * full.face(\'l/upper\'); // left wing upper surface\n * ```\n *\n * @param selector - A user-authored label string, compiler-owned face name, or FaceQuery.\n * @returns A `FaceRef` carrying the face\'s center, normal, and local axes.\n * @see {@link Shape.faceNames} — list available face names on this shape\n * @see {@link Shape.prefixLabels} — prefix labels before combining shapes\n * @category Face References\n */\n face(selector: FaceSelector): FaceRef;\n /**\n * Return faces matching a query, or label semantic faces when passed a mapping.\n *\n * Mapping form returns a new shape:\n * `shape.faces({ lid: \'top\', walls: [\'front\', \'back\', \'left\', \'right\'] })`.\n */\n faces(): FaceRef[];\n faces(query: FaceQuery): FaceRef[];\n faces(mapping: Record<string, string | string[]>): Shape;\n /** List defined semantic face names currently available on this shape. */\n faceNames(): string[];\n /** Prefix all user-authored face labels, including semantic labels from `faces(mapping)`. Returns a new shape with modified labels. */\n prefixLabels(prefix: string): Shape;\n /** Rename a single face label. Returns a new shape. */\n renameLabel(from: string, to: string): Shape;\n /** Remove specific face labels. Returns a new shape. */\n dropLabels(...names: string[]): Shape;\n /** Remove all face labels. Returns a new shape. */\n dropAllLabels(): Shape;\n /** Get a named topology edge. Only available on shapes with tracked topology (from box/cylinder/extrude). */\n edge(name: string): EdgeRef;\n /** List named topology edge names. Returns empty array if shape has no tracked topology. */\n edgeNames(): string[];\n /**\n * Return all boundary edges of a named face.\n *\n * **Details**\n *\n * Finds edges where one adjacent mesh face belongs to the target face and the\n * other belongs to a different face. The result is coalesced (tessellation\n * fragments merged) and can be passed directly to `fillet()` or `chamfer()`.\n *\n * This is a topological query — no coordinates, no tolerances, no minimum-length\n * hacks. It works because an edge is the boundary between two faces.\n *\n * **Example**\n *\n * ```js\n * // Fillet all top edges of a mounting plate\n * let plate = box(120, 80, 6).faces({ workSurface: \'top\' })\n * plate = fillet(plate, 3, plate.edgesOf(\'workSurface\'))\n *\n * // Shelled enclosure — fillet the outer lip\n * let body = box(80, 50, 35).faces({ opening: \'top\' })\n * body = body.shell(2, { openFaces: [\'top\'] })\n * body = fillet(body, 1.5, body.edgesOf(\'opening\'))\n *\n * // Filter: only concave edges (after a boolean subtraction)\n * body.edgesOf(\'top\', { concave: true })\n * ```\n *\n * @param faceLabel - Name of the face whose edges to return.\n * @param options - Optional: exclude faces, convexity filter, minLength.\n * @returns EdgeSegment[] ready for fillet/chamfer. Throws if face not found.\n * @see {@link Shape.edgesBetween} — edges shared between two faces\n * @see {@link Shape.faces} — assign labels to faces\n * @see {@link Shape.ref} — resolve semantic face and edge paths\n * @category Edge Queries\n */\n edgesOf(faceLabel: string, options?: EdgesOfOptions): EdgeSegment[];\n /**\n * Return edges shared between two named faces.\n *\n * **Details**\n *\n * An edge is "between" faces A and B when one of its adjacent mesh triangles\n * belongs to A and the other belongs to B. This is the most precise topological\n * edge selection — "fillet the edges where the top meets the wall."\n *\n * The second argument can be a single face name or an array (edges between A\n * and any of B1, B2, ...).\n *\n * **Example**\n *\n * ```js\n * // Fillet the edge where lid meets one wall\n * let body = box(100, 60, 30).faces({ lid: \'top\', wall: \'side-left\' })\n * body = fillet(body, 2, body.edgesBetween(\'lid\', \'wall\'))\n *\n * // Fillet a cylinder rim — where the flat cap meets the curved barrel\n * let tube = cylinder(30, 10).faces({ cap: \'top\', barrel: \'side\' })\n * tube = fillet(tube, 1, tube.edgesBetween(\'cap\', \'barrel\'))\n *\n * // Multiple target faces at once\n * body.edgesBetween(\'lid\', [\'left-wall\', \'right-wall\', \'front-wall\', \'back-wall\'])\n * ```\n *\n * @param faceA - First face name.\n * @param faceB - Second face name, or array of face names.\n * @returns EdgeSegment[] ready for fillet/chamfer. Throws if faces don\'t share edges.\n * @see {@link Shape.edgesOf} — all edges of a single face\n * @see {@link Shape.faces} — assign labels to faces\n * @see {@link Shape.ref} — resolve semantic face and edge paths\n * @category Edge Queries\n */\n edgesBetween(faceA: string, faceB: string | string[]): EdgeSegment[];\n /** @internal Resolve a user face label or canonical name to a FaceRef. */\n private _resolveFaceLabel;\n /** @internal Resolve a user face label, face set, or canonical name to one or more FaceRefs. */\n private _resolveFaceLabels;\n /** Get the transformation history for a specific face. */\n faceHistory(name: string): FaceTransformationHistory;\n /**\n * Translate the shape so the given anchor or reference lands on the target coordinate.\n *\n * Accepts any built-in anchor name (`\'bottom\'`, `\'center\'`, `\'top-front-left\'`, etc.)\n * or a custom placement reference attached via `withReferences()`.\n *\n * ```javascript\n * // Ground a shape — put its bottom face center at Z = 0\n * shape.placeReference(\'bottom\', [0, 0, 0])\n *\n * // Center at the world origin\n * shape.placeReference(\'center\', [0, 0, 0])\n *\n * // Align left edge to X = 10\n * shape.placeReference(\'left\', [10, 0, 0])\n * ```\n */\n placeReference(ref: PlacementAnchorLike, target: [\n number,\n number,\n number\n ], offset?: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Translate using polar coordinates (radius + angle in degrees).\n * Eliminates manual `r * Math.cos(angle * PI/180)` calculations.\n *\n * Example: `shape.translatePolar(50, 30)` moves 50mm at 30 degrees from +X.\n */\n translatePolar(radius: number, angleDeg: number, z?: number): Shape;\n /** Move the shape relative to its current position. All transforms are immutable and return new shapes. */\n translate(x: number, y: number, z: number): Shape;\n /** Position the shape so its bounding box min corner is at the given global coordinate. */\n moveTo(x: number, y: number, z: number): Shape;\n /** Position the shape relative to another shape\'s local coordinate system (bounding box min corner). */\n moveToLocal(target: Shape | {\n toShape(): Shape;\n }, x: number, y: number, z: number): Shape;\n /** Rotate around an arbitrary axis through the origin. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): Shape;\n /** Rotate around the X axis by the given angle in degrees. */\n rotateX(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): Shape;\n /** Rotate around the Y axis by the given angle in degrees. */\n rotateY(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): Shape;\n /** Rotate around the Z axis by the given angle in degrees. */\n rotateZ(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): Shape;\n /** Apply a 4x4 affine transform matrix (column-major) or a Transform object. */\n transform(m: Mat4 | Transform): Shape;\n /** Scale the shape uniformly or per-axis from the shape\'s bounding box center. Accepts a single number or [x, y, z] array. */\n scale(v: number | [\n number,\n number,\n number\n ]): Shape;\n /** Scale the shape uniformly or per-axis from an explicit pivot point. */\n scaleAround(pivot: [\n number,\n number,\n number\n ], v: number | [\n number,\n number,\n number\n ]): Shape;\n /** Mirror across a plane through the shape\'s bounding box center, defined by its normal vector. */\n mirror(normal: [\n number,\n number,\n number\n ]): Shape;\n /** Mirror across a plane through an explicit point, defined by its normal vector. */\n mirrorThrough(point: [\n number,\n number,\n number\n ], normal: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Reorient a shape so its primary axis (Z) points along the given direction.\n * Useful for laying cylinders/extrusions along X or Y without thinking about Euler angles.\n * The shape\'s origin stays at [0,0,0] — translate after pointAlong to position it.\n *\n * Example: cylinder(40, 5).pointAlong([1, 0, 0]) — lays cylinder along X, starting at origin\n */\n pointAlong(direction: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Rotate around an arbitrary axis through a pivot point.\n * Equivalent to: translate(-pivot) → rotate around axis → translate(+pivot)\n * @internal Prefer rotate(), rotateX(), rotateY(), rotateZ() for public use.\n */\n rotateAroundAxis(axis: [\n number,\n number,\n number\n ], angleDeg: number, pivot?: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point.\n * `movingPoint` / `targetPoint` may be raw world points or this shape\'s anchors/references.\n */\n rotateAroundTo(axis: [\n number,\n number,\n number\n ], pivot: [\n number,\n number,\n number\n ], movingPoint: RotationPointLike, targetPoint: RotationPointLike, options?: RotateAroundToOptions): Shape;\n /** Unwrap any object with toShape() without circular import. */\n private static _unwrap;\n /**\n * Warn if a boolean operation had no geometric effect.\n * Compares volumes before and after; if they match within tolerance, the operation was a no-op.\n */\n private static _checkBooleanNoOp;\n /** Union this shape with others (additive boolean). Method form of union(). */\n add(...others: ShapeOperandInput[]): Shape;\n /** Subtract other shapes from this one. Method form of difference(). */\n subtract(...others: ShapeOperandInput[]): Shape;\n /** Keep only the overlap with other shapes. Method form of intersection(). */\n intersect(...others: ShapeOperandInput[]): Shape;\n /** Alias for add() — matches the free-function union() naming. */\n union(...others: ShapeOperandInput[]): Shape;\n /** Alias for subtract() — matches the free-function difference() naming. */\n difference(...others: ShapeOperandInput[]): Shape;\n /** Alias for intersect() — matches the free-function intersection() naming. */\n intersection(...others: ShapeOperandInput[]): Shape;\n /** Split into [inside, outside] by another shape. */\n split(cutter: Shape | {\n toShape(): Shape;\n }): [\n Shape,\n Shape\n ];\n /** Split by infinite plane. Returns [positive-side, negative-side]. */\n splitByPlane(normal: [\n number,\n number,\n number\n ], originOffset?: number): [\n Shape,\n Shape\n ];\n /** Keep the positive side of the plane and discard the opposite side. */\n trimByPlane(normal: [\n number,\n number,\n number\n ], originOffset?: number): Shape;\n /** Hollow out compile-covered boxes, cylinders, and straight extrudes.\n * `openFaces` names any subset of the base shape\'s labeled faces to leave open (no wall).\n */\n shell(thickness: number, opts?: {\n openFaces?: string[];\n }): Shape;\n /** Offset-thicken an exact open surface or shell into a solid. */\n thicken(thickness: number): Shape;\n /** Get the axis-aligned bounding box as { min: [x,y,z], max: [x,y,z] }. */\n boundingBox(): ShapeRuntimeBounds;\n /** Volume in mm cubed. */\n volume(): number;\n /** Surface area in mm squared. */\n surfaceArea(): number;\n /** True if the shape contains no geometry. */\n isEmpty(): boolean;\n /** Number of disconnected solid bodies in this shape. */\n numBodies(): number;\n /** Triangle count of the mesh representation. */\n numTri(): number;\n /** Extract triangle mesh for Three.js rendering */\n getMesh(): ShapeRuntimeMesh;\n /** Slice the runtime solid by a plane normal to local Z at the given offset. */\n slice(offset?: number): any;\n /** Orthographically project the runtime solid onto the local XY plane. */\n project(): any;\n /**\n * Position this shape relative to another using named 3D anchor points.\n *\n * Anchors are bounding-box-relative: \'center\', face centers (\'top\', \'front\', ...),\n * edge midpoints (\'top-front\', \'back-left\', ...), and corners (\'top-front-left\', ...).\n * Anchor word order is flexible: \'front-left\' and \'left-front\' are equivalent.\n * Named placement references (from withReferences) can also be used as anchors.\n */\n attachTo(target: ShapeAnchorTarget, targetAnchor: PlacementAnchorLike, selfAnchor?: PlacementAnchorLike, offset?: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Place this shape on a face of a parent shape.\n *\n * Think of it like sticking a label on a box surface:\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 onFace(parent: ShapeAnchorTarget, face: "front" | "back" | "left" | "right" | "top" | "bottom", opts?: {\n u?: number;\n v?: number;\n protrude?: number;\n }): Shape;\n /**\n * Slide this shape along an axis until a labeled face is embedded in the target body.\n *\n * Position the shape roughly first (translate/rotate), then call seatInto to\n * auto-adjust the penetration depth. No manual coordinate math needed.\n *\n * ```js\n * // Wing root embeds into fuselage — adapts to any fuselage shape\n * wing.translate(0, wingY, 0).seatInto(fuselage, \'root\');\n *\n * // Sensor pod sits flush on fuselage surface\n * pod.translate(0, station, radius + 20).seatInto(fuselage, \'base\', { depth: \'flush\' });\n *\n * // Antenna with 3mm gasket standoff\n * mast.translate(0, station, radius + 50).seatInto(fuselage, \'mount\', { depth: \'flush\', gap: 3 });\n * ```\n *\n * @param target - The body to embed into (must be a watertight solid).\n * @param surface - Label of the face on this shape that connects (from `.faces(mapping)`).\n * @param options - Movement axis (defaults to face normal direction), depth mode, and gap control.\n * @returns A new Shape translated along the movement axis.\n */\n seatInto(target: Shape, surface: string, options?: SeatIntoOptions): Shape;\n /**\n * Slide this shape until a target\'s labeled face is fully covered (inside this shape).\n *\n * The inverse of `seatInto`: instead of embedding *your* face into the target,\n * 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\n * nacelle.translate(rough).seatOver(pylon, \'bottom\');\n *\n * // Cap slides down over a post until post\'s top face is covered\n * cap.translate(rough).seatOver(post, \'top\');\n * ```\n *\n * @param target - The shape whose face you want to cover.\n * @param targetSurface - Label of the face on the TARGET shape to cover.\n * @param options - Movement axis (defaults to negative of target face normal), depth mode, and gap control.\n * @returns A new Shape translated along the movement axis.\n */\n seatOver(target: Shape, targetSurface: string, options?: SeatIntoOptions): Shape;\n /** Attach named connectors — attachment points that survive transforms and imports.\n * Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching). */\n withConnectors(connectors: Record<string, ConnectorInput>): Shape;\n /** List all connector names on this shape. */\n connectorNames(): string[];\n /** Get all connectors of a given type. */\n connectorsByType(type: string): Array<{\n name: string;\n port: ConnectorDef;\n }>;\n /** Distance between two connector origins on this shape. */\n connectorDistance(nameA: string, nameB: string): number;\n /** Get measurements metadata from a connector. */\n connectorMeasurements(name: string): Record<string, number | string>;\n /**\n * Position this shape by matching connectors to a target.\n *\n * Overloads:\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 matchTo(targetOrPairs: Shape | MatchTarget | Array<[\n Shape | MatchTarget,\n string,\n string\n ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): Shape;\n}\ntype MatchTarget = Shape | {\n portNames?(): string[];\n};\n/**\n * Create a rectangular box. Centered on XY, base at Z=0.\n *\n * Extents:\n * - X: `[-width/2, width/2]`\n * - Y: `[-depth/2, depth/2]`\n * - Z: `[0, height]`\n *\n * For named faces, build from a labeled sketch:\n * `rect(width, depth).labelEdges(\'s\', \'e\', \'n\', \'w\').extrude(height, { labels: { start: \'bottom\', end: \'top\' } })`.\n */\ndeclare function box$1(width: number, depth: number, height: number): Shape;\n/**\n * Create a cylinder or cone with named faces and edges.\n * Centered on XY, base at Z=0.\n *\n * Extents:\n * - X/Y: centered at the origin\n * - Z: `[0, height]`\n *\n * `radiusTop` defaults to `radius`. Set `radiusTop` smaller to taper the side,\n * or `0` for a pointy cone. Use `segments` to create regular prisms\n * (for example `6` for a hexagonal prism).\n *\n * Named faces: `top`, `bottom`, `side`\n * Named edges: `top-rim`, `bottom-rim`\n */\ndeclare function cylinder$1(height: number, radius: number, radiusTop?: number, segments?: number): Shape;\n/**\n * Create a sphere centered at the origin.\n *\n * Extents:\n * - X: `[-radius, radius]`\n * - Y: `[-radius, radius]`\n * - Z: `[-radius, radius]`\n *\n * Use `segments` for lower-poly approximations.\n */\ndeclare function sphere$1(radius: number, segments?: number): Shape;\n/**\n * Create a torus (donut shape) lying in the XY plane. Centered on all axes.\n *\n * Extents:\n * - X: `[-(majorRadius + minorRadius), +(majorRadius + minorRadius)]`\n * - Y: `[-(majorRadius + minorRadius), +(majorRadius + minorRadius)]`\n * - Z: `[-minorRadius, minorRadius]`\n *\n * The origin is the center of the ring.\n */\ndeclare function torus$1(majorRadius: number, minorRadius: number, segments?: number): Shape;\n/**\n * Combine shapes into a single solid (additive boolean).\n *\n * Accepts individual shapes, or an array of shapes. `union()` returns one\n * solid, so only the first operand\'s color is preserved in the result. Use\n * `group()` when you want separate child colors or identities.\n */\ndeclare function union(...inputs: ShapeOperandInput[]): Shape;\n/**\n * Subtract shapes from a base shape (subtractive boolean).\n *\n * The first shape is the base; all subsequent shapes are subtracted from it.\n * Accepts individual shapes, or an array of shapes.\n */\ndeclare function difference(...inputs: ShapeOperandInput[]): Shape;\n/**\n * Keep only the overlapping volume of the input shapes (intersection boolean).\n *\n * Requires at least two shapes. Accepts individual shapes, or an array.\n */\ndeclare function intersection(...inputs: ShapeOperandInput[]): Shape;\ntype Constraint3DType = "flush" | "align" | "parallel" | "faceDistance" | "concentric" | "axisParallel" | "pointCoincident" | "pointOnFace" | "pointOnAxis" | "angle" | "fixed";\ninterface BodyFeatureRef {\n bodyId: string;\n featureName: string;\n}\ninterface Constraint3D {\n id: string;\n type: Constraint3DType;\n refA: BodyFeatureRef;\n refB: BodyFeatureRef;\n value?: number;\n}\ndeclare class MateBuilder {\n readonly constraints: Constraint3D[];\n private nextId;\n private add;\n /** Constrain two faces so they stay flush. */\n flush(faceA: string, faceB: string): string;\n /** Constrain two faces so their normals align. */\n align(faceA: string, faceB: string): string;\n /** Constrain two faces so they remain parallel. */\n parallel(faceA: string, faceB: string): string;\n /** Constrain the distance between two faces. */\n faceDistance(faceA: string, faceB: string, distance: number): string;\n /** Constrain two axes to share the same center line. */\n concentric(axisA: string, axisB: string): string;\n /** Constrain two axes to remain parallel. */\n axisParallel(axisA: string, axisB: string): string;\n /** Constrain two points to coincide. */\n pointCoincident(pointA: string, pointB: string): string;\n /** Constrain a point to lie on a face. */\n pointOnFace(point: string, face: string): string;\n /** Constrain a point to lie on an axis. */\n pointOnAxis(point: string, axis: string): string;\n /** Constrain the angle between two faces. */\n angle(faceA: string, faceB: string, degrees: number): string;\n /** Total constraint equations. */\n get totalEquations(): number;\n}\ntype AssemblyPart = Shape | ShapeGroup;\ntype JointType = "fixed" | "revolute" | "prismatic";\ntype JointState = Record<string, number | undefined>;\ninterface PartMetadata {\n material?: string;\n process?: string;\n tolerance?: string;\n qty?: number;\n notes?: string;\n densityKgM3?: number;\n massKg?: number;\n [key: string]: unknown;\n}\ninterface PartOptions {\n transform?: TransformInput;\n metadata?: PartMetadata;\n}\ninterface JointOptions {\n frame?: TransformInput;\n origin?: [\n number,\n number,\n number\n ];\n axis?: Vec3;\n min?: number;\n max?: number;\n default?: number;\n unit?: string;\n effort?: number;\n velocity?: number;\n damping?: number;\n friction?: number;\n /** Connector refs that define this joint contract. Usually set by `connect()` / `match()`. */\n connectorRefs?: JointConnectorRefs;\n}\ninterface JointConnectorRefs {\n parent: string;\n child: string;\n parentAlign?: PortAlign;\n childAlign?: PortAlign;\n}\ninterface JointCouplingTerm {\n joint: string;\n ratio?: number;\n}\ninterface JointCouplingOptions {\n terms: JointCouplingTerm[];\n offset?: number;\n}\ninterface GearRatioLike {\n jointRatio: number;\n /** Phase offset (degrees) for tooth mesh alignment. Auto-applied as coupling\n * offset when provided via `addGearCoupling({ pair })` and no explicit offset is set. */\n phaseDeg?: number;\n}\ninterface GearCouplingOptions {\n ratio?: number;\n pair?: GearRatioLike;\n driverTeeth?: number;\n drivenTeeth?: number;\n mesh?: "external" | "internal" | "bevel" | "face";\n offset?: number;\n driverOrigin?: [\n number,\n number,\n number\n ];\n drivenOrigin?: [\n number,\n number,\n number\n ];\n}\ninterface PartRecord {\n name: string;\n part: AssemblyPart;\n base: Transform;\n metadata?: PartMetadata;\n}\ninterface JointCouplingTermRecord {\n joint: string;\n ratio: number;\n}\ninterface AssemblyPartDef {\n name: string;\n part: AssemblyPart;\n base: Transform;\n metadata?: PartMetadata;\n}\ninterface AssemblyJointDef {\n name: string;\n type: JointType;\n parent: string;\n child: string;\n frame: Transform;\n axis: Vec3;\n min?: number;\n max?: number;\n defaultValue: number;\n unit?: string;\n effort?: number;\n velocity?: number;\n damping?: number;\n friction?: number;\n connectorRefs?: JointConnectorRefs;\n}\ninterface AssemblyJointCouplingDef {\n joint: string;\n terms: JointCouplingTermRecord[];\n offset: number;\n}\ninterface AssemblyDefinition {\n name: string;\n parts: AssemblyPartDef[];\n joints: AssemblyJointDef[];\n jointCouplings: AssemblyJointCouplingDef[];\n}\ninterface BomRow {\n part: string;\n qty: number;\n material?: string;\n process?: string;\n tolerance?: string;\n notes?: string;\n metadata?: PartMetadata;\n}\ninterface CollisionOptions {\n parts?: string[];\n ignorePairs?: Array<[\n string,\n string\n ]>;\n minOverlapVolume?: number;\n}\ninterface CollisionFinding {\n partA: string;\n partB: string;\n overlapVolume: number;\n}\ninterface JointSweepFrame {\n value: number;\n collisions: CollisionFinding[];\n warnings: string[];\n}\n/**\n * Convert an array of BOM rows into a CSV string.\n *\n * **Details**\n *\n * Produces a CSV with columns: `part`, `qty`, `material`, `process`, `tolerance`, `notes`.\n * String values are quoted and internal double-quotes are escaped. Prefer calling\n * `solvedAssembly.bomCsv()` directly — this function is exposed for custom BOM processing.\n *\n * @param rows - BOM rows from `SolvedAssembly.bom()`\n * @returns CSV string with header row\n * @see {@link SolvedAssembly.bomCsv} for the all-in-one method\n * @category Assembly\n */\ndeclare function bomToCsv(rows: BomRow[]): string;\ninterface MateMetadata {\n explodeHints: Record<string, {\n direction: Vec3;\n }>;\n dof: number;\n converged: boolean;\n}\n/**\n * The result of solving an assembly at a specific joint state.\n *\n * **Details**\n *\n * `SolvedAssembly` holds world-space transforms for every part at a given pose.\n * Top-level scripts can return a `SolvedAssembly` directly for display. Use\n * `toGroup()` when you specifically need a `ShapeGroup` for composition,\n * group-style transforms, or named-child lookup. Do not call `toGroup()` just\n * to make a solved assembly render. Use `getPart()` / `getTransform()` to\n * inspect individual parts programmatically.\n *\n * **Validation**\n *\n * Call `collisionReport()` to detect overlapping parts, or `sweepJoint()` on\n * the parent `Assembly` to check for interference across the joint\'s motion range.\n *\n * **Example**\n *\n * ```ts\n * const solved = mech.solve({ shoulder: 45, elbow: -20 });\n * console.log("Collisions", solved.collisionReport());\n * return solved;\n * ```\n *\n * @category Assembly\n */\ndeclare class SolvedAssembly {\n readonly name: string;\n private readonly parts;\n private readonly transforms;\n private readonly jointValues;\n private readonly solveWarnings;\n private readonly _mateMetadata;\n private readonly _usedPortRefs;\n constructor(name: string, parts: Map<string, PartRecord>, transforms: Map<string, Transform>, jointValues: JointState, solveWarnings: string[], _mateMetadata?: MateMetadata | null, usedPortRefs?: ReadonlySet<string>);\n /** Return any warnings generated during solve (clamped joints, unconverged mates, etc.). */\n warnings(): string[];\n /** Return a snapshot of resolved joint values (after clamping and coupling). */\n getJointState(): JointState;\n /** Explode direction hints derived from mate constraints, or null if no mates. */\n get mateExplodeHints(): Record<string, {\n direction: Vec3;\n }> | null;\n /** Remaining degrees of freedom after mate constraints, or null if no mates. */\n get mateDof(): number | null;\n /** Whether the mate constraint solver converged, or null if no mates. */\n get mateConverged(): boolean | null;\n /**\n * Return the world-space `Transform` for the named part at the solved pose.\n *\n * @param partName - Name as registered with `addPart()`\n * @returns The world transform (position + orientation) of the part\n * @category Assembly\n */\n getTransform(partName: string): Transform;\n /**\n * Return the named part already positioned at its solved world transform.\n *\n * @param partName - Name as registered with `addPart()`\n * @returns The part (`Shape` or `ShapeGroup`) at its solved world position\n * @category Assembly\n */\n getPart(partName: string): AssemblyPart;\n /**\n * Convert all solved parts into a `ShapeGroup` with named children.\n *\n * **Details**\n *\n * Each part becomes a named child in the group, already positioned at its\n * solved world transform. Use this only when you specifically need a\n * `ShapeGroup` for composition, `ShapeGroup` transforms, or named-child\n * access. Top-level scripts can return the `SolvedAssembly` directly; do not\n * call `toGroup()` just to make a solved assembly render.\n *\n * **Example**\n *\n * ```ts\n * const armGroup = mech.solve({ shoulder: 60 }).toGroup(); // only because we need rotateZ()\n * return armGroup.rotateZ(90);\n * ```\n *\n * @returns `ShapeGroup` with one child per part, each at its solved world position\n * @category Assembly\n */\n toGroup(): ShapeGroup;\n /**\n * Return an array of named scene objects for the viewport renderer.\n *\n * **Details**\n *\n * Each part becomes `{ name, shape }` or `{ name, group: [...] }` if the\n * part is a `ShapeGroup`. Top-level scripts should normally return the\n * `SolvedAssembly` directly. Use `toGroup()` when you need `ShapeGroup`\n * behavior; use this method only for advanced scene-graph control where you\n * need access to the flat per-part array with metadata.\n *\n * @returns Array of `{ name, shape?, group?, metadata? }` for each part\n * @category Assembly\n */\n toSceneObjects(): Array<{\n name: string;\n shape?: Shape;\n group?: Array<{\n name: string;\n shape: Shape;\n }>;\n metadata?: PartMetadata;\n }>;\n /**\n * Backward-compatible alias for `toSceneObjects()`.\n *\n * @deprecated Use `toSceneObjects()` or `toGroup()` instead.\n */\n toScene(): Array<{\n name: string;\n shape?: Shape;\n group?: Array<{\n name: string;\n shape: Shape;\n }>;\n metadata?: PartMetadata;\n }>;\n /**\n * Generate a bill of materials for all parts in the solved assembly.\n *\n * @returns Array of `BomRow` with part name, qty, and any metadata (material, process, etc.)\n * @see {@link bomCsv} for CSV-formatted output\n * @category Assembly\n */\n bom(): BomRow[];\n /**\n * Generate a bill of materials as a CSV string.\n *\n * @returns CSV-formatted BOM with columns: part, qty, material, process, tolerance, notes\n * @see {@link bom} for the structured row array\n * @category Assembly\n */\n bomCsv(): string;\n /**\n * Detect overlapping (colliding) part pairs in this solved pose.\n *\n * **Details**\n *\n * Computes boolean intersections between all part pairs and returns findings\n * where the overlap volume exceeds `minOverlapVolume` (default 0.1 mm³).\n *\n * **Example**\n *\n * ```ts\n * const solved = mech.solve({ shoulder: 35, elbow: 60 });\n * console.log("Collisions", solved.collisionReport());\n * ```\n *\n * @param options - Filter by `parts`, ignore specific `ignorePairs`, or set `minOverlapVolume`\n * @returns Array of `{ partA, partB, overlapVolume }` for each interference found\n * @category Assembly\n */\n collisionReport(options?: CollisionOptions): CollisionFinding[];\n /**\n * Compute the minimum gap (clearance) between two parts in this solved pose.\n *\n * **Details**\n *\n * Returns `0` if the parts are touching or overlapping. Requires the Manifold backend.\n * `searchLength` bounds the search radius in mm — increase it for widely separated parts.\n *\n * @param partA - Name of the first part\n * @param partB - Name of the second part\n * @param searchLength - Maximum search radius in mm (default 10)\n * @returns Minimum gap in mm; `0` means touching or interfering\n * @category Assembly\n */\n minClearance(partA: string, partB: string, searchLength?: number): number;\n}\ninterface ConnectOptions {\n as?: string;\n type?: JointType;\n min?: number;\n max?: number;\n default?: number;\n unit?: string;\n /**\n * @deprecated Connectors now always meet face-to-face (anti-parallel axes).\n * This parameter is ignored. If your connectors produce wrong orientation,\n * fix the connector axis directions instead of using flip.\n */\n flip?: boolean;\n /** Which point on the parent connector to align: \'start\', \'middle\' (default), or \'end\'. */\n parentAlign?: PortAlign;\n /** Which point on the child connector to align: \'start\', \'middle\' (default), or \'end\'. */\n childAlign?: PortAlign;\n /** Shorthand: set both parentAlign and childAlign at once. */\n align?: PortAlign;\n effort?: number;\n velocity?: number;\n damping?: number;\n friction?: number;\n}\ninterface ToJointsViewOptions {\n defaults?: Record<string, number>;\n overrides?: Record<string, Partial<JointViewInput>>;\n animations?: JointViewAnimationInput[];\n couplings?: JointViewCouplingInput[];\n defaultAnimation?: string;\n enabled?: boolean;\n}\n/**\n * Container for a kinematic mechanism made up of named parts and joints.\n *\n * **Details**\n *\n * An assembly is a directed graph where **parts** are nodes and **joints** are\n * directed edges from parent to child. The graph must be a forest (one or more\n * trees with no cycles). Root parts (no incoming joint) are fixed to world space.\n *\n * Each joint carries a `frame` transform (from the parent part frame to the\n * joint\'s zero-state frame) and a motion formula:\n *\n * ```\n * childWorld = parentWorld × frame × motion(value) × childBase\n * ```\n *\n * Three joint types are supported:\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\n* const 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 *\n * return mech; // auto-solved at defaults\n * ```\n *\n * Returning an unsolved `Assembly` auto-solves at default joint values. Return\n * a `SolvedAssembly` directly for a specific pose:\n *\n * ```ts\n * return 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 * @category Assembly\n */\ndeclare class Assembly {\n readonly name: string;\n private readonly parts;\n private readonly joints;\n private readonly jointCouplings;\n private readonly _mateFns;\n private _refs;\n private readonly _portsByPart;\n private readonly _usedPortRefs;\n private _connectCounter;\n constructor(name?: string);\n private _fork;\n /** Connector refs (e.g. "PartName.connectorName") consumed by connect/match calls. */\n get usedConnectorRefs(): ReadonlySet<string>;\n /**\n * Legacy alias for `usedConnectorRefs`.\n *\n * @deprecated Use `usedConnectorRefs` instead.\n */\n get usedPortRefs(): ReadonlySet<string>;\n /**\n * Register mate constraints between parts.\n * Constraints are solved during `solve()` to derive part positions and explode hints.\n * Part references use "partName:featureName" format.\n */\n mate(fn: (m: MateBuilder) => void): Assembly;\n /**\n * Attach named placement reference points to this assembly.\n * These are surfaced automatically on the ImportedAssembly when this file is\n * imported via require(), so consumers can use placeReference() without\n * re-declaring them.\n * Returns a new Assembly — does not mutate.\n */\n withReferences(refs: Pick<PlacementReferenceInput, "points">): Assembly;\n /** @internal — used by require() to seed ImportedAssembly refs. */\n getReferences(): PlacementReferences;\n /**\n * Attach named connectors to a specific part or the assembly as a whole.\n *\n * **Details**\n *\n * Connectors declared this way are in the part\'s local coordinate system.\n * They are captured automatically if the incoming `Shape` already has\n * connectors via `shape.withConnectors(...)`, but you can also add or\n * override connectors after the fact with this method.\n *\n * Use the single-argument overload to attach assembly-level connectors —\n * these are exposed when this assembly is imported as a sub-assembly.\n *\n * @param partName - Part name (must already be added via `addPart`)\n * @param connectors - Map of connector name → `ConnectorInput`\n * @returns `this` for chaining\n * @see {@link connect} for using connectors to create joints\n * @category Connectors\n */\n withConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly;\n withConnectors(connectors: Record<string, ConnectorInput>): Assembly;\n /**\n * Backward-compatible alias for `withConnectors()`.\n *\n * @deprecated Use `withConnectors()` instead.\n */\n withPorts(partNameOrPorts: string | Record<string, PortInput>, maybePorts?: Record<string, PortInput>): Assembly;\n /** Get connectors declared on a part in part-local space. */\n getConnectors(partName: string): ConnectorMap;\n /**\n * Backward-compatible alias for `getConnectors()`.\n *\n * @deprecated Use `getConnectors()` instead.\n */\n getPorts(partName: string): PortMap;\n /** @internal — set already-normalized connectors directly (used by mergeInto). */\n _setPortsDirect(partName: string, ports: PortMap): void;\n /** @internal — get all connector maps, keyed by part name. */\n getAllPorts(): Map<string, PortMap>;\n /**\n * Parse a "PartName.connectorName" reference and return the resolved connector.\n * Throws descriptive errors if the part or connector doesn\'t exist.\n */\n getConnector(ref: string): {\n partName: string;\n connectorName: string;\n connector: ConnectorDef;\n };\n /**\n * Legacy alias for `getConnector()`.\n *\n * @deprecated Use `getConnector()` instead.\n */\n getPort(ref: string): {\n partName: string;\n portName: string;\n port: PortDef;\n };\n /**\n * Add a virtual reference frame (no geometry) to the assembly graph.\n *\n * **Details**\n *\n * Useful when you need a named pivot point or coordinate frame that has no\n * visual geometry. Acts like a zero-volume part and can be connected to\n * other parts via joints.\n *\n * @param name - Unique part name for the frame in the assembly graph\n * @param options - Optional transform and metadata\n * @returns `this` for chaining\n * @category Assembly\n */\n addFrame(name: string, options?: PartOptions): Assembly;\n /**\n * Add a named part to the assembly.\n *\n * **Details**\n *\n * Connectors declared on the part (via `withConnectors()`) are captured automatically.\n * Parts are positioned at world origin by default unless a `transform` is\n * provided in `options`. For root parts (no incoming joint), `transform` is\n * their final world position.\n *\n * When a part is a `ShapeGroup`, name the group children explicitly to get\n * readable viewport labels (e.g. `"Base Assembly.Body"` instead of\n * `"Base Assembly.1"`):\n *\n * ```ts\n * const housing = group(\n * { name: "Body", shape: body },\n * { name: "Lid", shape: lid },\n * );\n * assembly.addPart("Base Assembly", housing);\n * ```\n *\n * @param name - Unique part name (must not already exist)\n * @param part - The `Shape` or `ShapeGroup` geometry\n * @param options - Optional `{ transform, metadata }` (material, process, qty, etc.)\n * @returns `this` for chaining\n * @category Assembly\n */\n addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly;\n /**\n * Add a kinematic joint between a parent and child part.\n *\n * **Details**\n *\n * `frame` is a transform from the **parent part frame** to the **joint frame\n * at zero state**. The child\'s world position is computed as:\n *\n * ```\n * childWorld = parentWorld × frame × motion(value) × childBase\n * ```\n *\n * For revolute joints `value` is in degrees; for prismatic joints `value` is\n * in mm. Coupled joints (see `addJointCoupling`) ignore the `state` value\n * passed to `solve()` and compute their value from source joints.\n *\n * @param name - Unique joint name\n * @param type - `\'revolute\'`, `\'prismatic\'`, or `\'fixed\'`\n * @param parent - Name of the parent part (already added via `addPart`)\n * @param child - Name of the child part (already added via `addPart`)\n * @param options - Frame, axis, limits, effort, velocity, damping, friction\n * @returns `this` for chaining\n * @category Joints\n */\n addJoint(name: string, type: JointType, parent: string, child: string, options?: JointOptions): Assembly;\n /**\n * Shorthand for `addJoint(name, \'revolute\', parent, child, options)`.\n *\n * @param name - Unique joint name\n * @param parent - Parent part name\n * @param child - Child part name\n * @param options - Frame, axis, min/max degrees, effort, velocity, damping, friction\n * @returns `this` for chaining\n * @category Joints\n */\n addRevolute(name: string, parent: string, child: string, options?: JointOptions): Assembly;\n /**\n * Shorthand for `addJoint(name, \'prismatic\', parent, child, options)`.\n *\n * @param name - Unique joint name\n * @param parent - Parent part name\n * @param child - Child part name\n * @param options - Frame, axis, min/max mm, effort, velocity, damping, friction\n * @returns `this` for chaining\n * @category Joints\n */\n addPrismatic(name: string, parent: string, child: string, options?: JointOptions): Assembly;\n /**\n * Shorthand for `addJoint(name, \'fixed\', parent, child, options)`.\n *\n * Fixed joints rigidly attach a child part to its parent at `frame` with no motion.\n * Before calling `mergeInto()`, use `addFixed()` to collapse multiple root parts\n * into a single root.\n *\n * @param name - Unique joint name\n * @param parent - Parent part name\n * @param child - Child part name\n * @param options - Optional frame transform for placement offset\n * @returns `this` for chaining\n * @category Joints\n */\n addFixed(name: string, parent: string, child: string, options?: JointOptions): Assembly;\n /**\n * Connect two parts by aligning their declared connectors, automatically computing frame and axis.\n *\n * **Details**\n *\n * Connector references use `"PartName.connectorName"` format. The system aligns connector\n * origins (child connector lands exactly on parent connector) and derives the joint frame\n * 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\n * meeting a socket. Each connector\'s axis points "outward" from its part. When two\n * connectors mate, the system brings them together so their axes oppose (anti-parallel).\n * This is the same convention used by `matchTo()`.\n *\n * For a revolute joint (hinge), both connectors\' axes should point outward from their\n * respective parts along the hinge line. For a prismatic joint (slider), both axes\n * should point along the slide direction from their part\'s perspective.\n *\n * The joint type is inferred from the connector\'s `kind` field if not specified in `options`.\n *\n * When connectors are defined with `start`/`end`, you can control which point on each\n * connector meets via `align` / `parentAlign` / `childAlign` (`\'start\'`, `\'middle\'`, `\'end\'`).\n *\n * Use `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore).\n * For mechanisms where parts share an axis but are deliberately spaced apart, use\n * `addRevolute()` with pre-positioned parts instead.\n *\n * **Example**\n *\n * ```ts\n * // Hinge: both axes point outward along the hinge line\n * const frame = box(100, 10, 80).withConnectors({\n * hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, 1] }),\n * });\n * const door = box(60, 4, 80).withConnectors({\n * hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, -1] }),\n * });\n * assembly("Door")\n * .addPart("Frame", frame)\n * .addPart("Door", door)\n * .connect("Frame.hinge", "Door.hinge", { as: "swing", min: 0, max: 110 });\n * ```\n *\n * @param parentConnectorRef - `"PartName.connectorName"` on the parent side\n * @param childConnectorRef - `"PartName.connectorName"` on the child side\n * @param options - `as` (joint name), `type`, `min`, `max`, `default`, `align`, effort, velocity, etc.\n * @returns `this` for chaining\n * @see {@link match} for typed connector matching with gender/type validation\n * @category Connectors\n */\n connect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly;\n /**\n * Auto-create a joint by matching typed connectors between two parts.\n *\n * **Details**\n *\n * Connectors can carry a `connectorType` string and a `gender`\n * (`\'male\'`, `\'female\'`, or `\'neutral\'`). `match()` validates type and gender\n * compatibility (use `{ force: true }` to skip validation) and creates the\n * joint automatically from the connector\'s `kind` metadata.\n *\n * The `pairs` map is `{ childConnector: parentConnector }`. The first pair\n * drives joint creation; additional pairs are validated but do not create\n * additional joints (they constrain the same rigid connection).\n *\n * Define connectors on shapes with `shape.withConnectors(...)`:\n *\n * ```ts\n * const 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 *\n * Then match in the assembly:\n *\n * ```ts\n * const 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 * @param childPartName - Part name of the child\n * @param parentPartName - Part name of the parent\n * @param pairs - Map of `{ childConnectorName: parentConnectorName }`\n * @param options - `force` (skip gender/type validation), `as` (joint name override)\n * @returns `this` for chaining\n * @see {@link connect} for connector-based connections without type/gender validation\n * @category Connectors\n */\n match(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & {\n as?: string;\n }): Assembly;\n /**\n * Link a joint\'s value to a linear combination of other joint values.\n *\n * **Details**\n *\n * The driven joint\'s value is computed as:\n *\n * ```\n * driven = offset + Σ(ratio_i × source_i)\n * ```\n *\n * Coupled joints ignore any value passed in `solve(state)` — a warning is\n * emitted if you try to override one. Coupling cycles are rejected. You cannot\n * sweep a coupled joint directly; sweep one of its source joints instead.\n *\n * **Example**\n *\n * ```ts\n * assembly\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 * @param jointName - Name of the joint to drive (must exist and not be fixed)\n * @param options - `{ terms, offset? }` where each term is `{ joint, ratio? }`\n * @returns `this` for chaining\n * @see {@link addGearCoupling} for a gear-ratio shorthand\n * @category Joints\n */\n addJointCoupling(jointName: string, options: JointCouplingOptions): Assembly;\n /**\n * Link two revolute joints via a gear ratio.\n *\n * **Details**\n *\n * Choose exactly one ratio source:\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\n * (`\'external\'` = negative/opposite rotation, `\'internal\'` = positive, `\'bevel\'`/`\'face\'` = negative)\n *\n * When `pair` carries a `phaseDeg`, it is auto-applied as the coupling `offset`\n * to align teeth correctly. Override with `offset: 0` if gear shapes already\n * have the phase baked in.\n *\n * **Example**\n *\n * ```ts\n * const pair = lib.gearPair({ pinion: { module: 1.25, teeth: 14 }, gear: { module: 1.25, teeth: 42 } });\n * assembly\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 * @param drivenJointName - The joint to be driven (must be revolute)\n * @param driverJointName - The driving joint (must be revolute)\n * @param options - Ratio source (`ratio`, `pair`, or `driverTeeth`/`drivenTeeth`); optional `offset`, `mesh`\n * @returns `this` for chaining\n * @see {@link addJointCoupling} for multi-term linear couplings\n * @category Joints\n */\n addGearCoupling(drivenJointName: string, driverJointName: string, options?: GearCouplingOptions): Assembly;\n private assertJointCouplingsAcyclic;\n /**\n * Solve the assembly at the given joint state and return positioned parts.\n *\n * **Details**\n *\n * Performs a depth-first traversal of the joint graph. Each joint\'s value\n * is taken from `state`, falling back to `defaultValue`. Coupled joints\n * compute their value from source joints. Values outside `[min, max]` are\n * clamped (a warning is added to `SolvedAssembly.warnings()`).\n *\n * If mate constraints were registered via `mate()`, the solver runs a\n * pre-pass to derive base transforms, then the kinematic DFS applies joints\n * on top of those positions.\n *\n * **Pitfall — `jointsView` double-rotation:**\n * When calling `toJointsView()`, always solve at the rest pose (all joint\n * values = 0 or default). Solving at a non-zero angle and then animating\n * will double-rotate parts. Use the `defaults` option on `toJointsView()`\n * to set the initial display angle instead.\n *\n * This pitfall only applies when `toJointsView()` is active. If you only want\n * a static posed result, return the solved assembly directly and skip\n * `toJointsView()`.\n *\n * **Example — static posed output (no `toJointsView()`)**\n *\n * ```ts\n * return mech.solve({ shoulder: 45, elbow: -20 });\n * ```\n *\n * @param state - Map of joint name → value; omitted joints use their `default`\n * @returns `SolvedAssembly` with all parts at their computed world positions\n * @category Assembly\n */\n solve(state?: JointState): SolvedAssembly;\n /**\n * Sample a joint through its motion range, collecting collision data at each step.\n *\n * **Details**\n *\n * Divides `[from, to]` into `steps` intervals (producing `steps + 1` frames).\n * At each sample, the assembly is solved with the sweeping joint at that value and\n * `baseState` for all others. Returns one `JointSweepFrame` per sample with the\n * joint value, collision findings, and any solve warnings.\n *\n * You cannot sweep a coupled joint — sweep one of its source joints instead.\n *\n * **Example**\n *\n * ```ts\n * const sweep = mech.sweepJoint("elbow", -10, 135, 12, { shoulder: 35 });\n * const hits = sweep.filter(frame => frame.collisions.length > 0);\n * console.log(`Collisions at ${hits.length} of ${sweep.length} poses`);\n * ```\n *\n * @param jointName - Joint to sweep (must not be coupled)\n * @param from - Start value (degrees for revolute, mm for prismatic)\n * @param to - End value\n * @param steps - Number of intervals (produces `steps + 1` frames)\n * @param baseState - Fixed values for all other joints during the sweep\n * @param collisionOptions - Filter options forwarded to `collisionReport()`\n * @returns Array of `{ value, collisions, warnings }` frames\n * @category Assembly\n */\n sweepJoint(jointName: string, from: number, to: number, steps: number, baseState?: JointState, collisionOptions?: CollisionOptions): JointSweepFrame[];\n /**\n * Derive viewport joint controls from the assembly graph and register them.\n *\n * **Details**\n *\n * Solves the assembly at rest (all joints = default), then converts each joint\n * into a `JointViewInput` with world-space pivot and axis. Fixed joints become\n * hidden zero-range revolute entries so attached parts follow their parent during\n * animation. Joint couplings are forwarded to the viewport automatically.\n *\n * This method is optional. Call it only when you want viewport joint sliders,\n * coupled controls, or playback animations. If you only want geometry, return\n * the `Assembly` or `SolvedAssembly` directly and skip `toJointsView()`.\n *\n * **Critical pitfall:** Always call `toJointsView()` before solving for display.\n * Then solve at the **rest pose** (no state overrides) and return that solved\n * assembly result directly. Do not flatten it with `.toGroup()` if you want the\n * viewport joint animation to keep working.\n *\n * Do not solve at a non-zero angle when using `toJointsView()` — the viewport\n * will apply the same rotation again, double-rotating the part.\n *\n * **Example**\n *\n * ```ts\n * mech.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\n * return mech.solve();\n * ```\n *\n * @param options - `defaults` (initial display angles), `animations`, `couplings`, `overrides`, `enabled`\n * @category Assembly\n */\n toJointsView(options?: ToJointsViewOptions): void;\n /** Return the serializable assembly definition used by solve/inspect pipelines. */\n describe(): AssemblyDefinition;\n}\n/**\n * 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,\n * articulated fingers, doors — all start with `assembly()`, not with manual rotation math.\n * Don\'t build a static "extended pose" first and refactor to an assembly later: joint sliders,\n * animations, sweeps, collision detection, and robot export all flow from the kinematic graph.\n *\n * **Details**\n *\n * An assembly models a mechanism as a directed graph of parts connected by joints.\n * Parts are the nodes; joints are directed edges from parent to child. The graph\n * must be a forest (no cycles). Root parts (those with no incoming joint) are\n * anchored to world space.\n *\n * Three joint types are supported: `\'revolute\'` (hinge), `\'prismatic\'` (slider),\n * and `\'fixed\'` (rigid attachment). Use `addPart()` to add geometry, `addJoint()`\n * (or the shorthands `addRevolute()`, `addPrismatic()`, `addFixed()`) to connect\n * parts, and `solve()` to compute world-space positions at a given joint state.\n *\n * The higher-level `connect()` API uses declared **connectors** to compute joint\n * frames automatically. The `match()` API uses typed connectors (with gender and\n * type metadata) for automatic compatibility validation and joint creation.\n *\n * For multi-file assemblies, a file that returns an `Assembly` is importable via\n * `require()` and yields an `ImportedAssembly`. Use `mergeInto()` to flatten a\n * sub-assembly into a parent assembly.\n *\n * **Example**\n *\n * ```ts\n* const 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 *\n * return mech; // auto-solved at defaults, renders all parts\n * ```\n *\n * @param name - Display name for the assembly (used in BOM, robot export, etc.)\n * @returns A new `Assembly` builder\n * @category Assembly\n */\ndeclare function assembly(name?: string): Assembly;\ninterface MergeIntoOptions {\n /**\n * Prefix applied to every part name and joint name from the sub-assembly.\n * E.g. prefix "Left Arm" turns part "Base" into "Left Arm.Base".\n * Strongly recommended to avoid name collisions when merging multiple instances.\n */\n prefix?: string;\n /** Part name in the parent assembly to attach the sub-assembly root to. */\n mountParent: string;\n /** Name for the new mount joint in the parent graph. */\n mountJoint: string;\n /** Joint type for the mount connection (default: \'fixed\'). */\n mountType?: JointType;\n /** Frame, axis, limits, and other options for the mount joint. */\n mountOptions?: JointOptions;\n}\n/**\n * A wrapper around an imported `Assembly` that provides kinematic access and\n * convenient transform helpers.\n *\n * **Details**\n *\n * When a `.forge.js` file returns an unsolved `Assembly`, `require()` wraps it\n * in an `ImportedAssembly`. This preserves the kinematic structure — you can\n * call `solve()`, `sweepJoint()`, and `mergeInto()` — while also allowing\n * convenience transforms that auto-solve at default values.\n *\n * **Kinematic access**\n *\n * ```ts\n * const arm = require("./arm.forge.js");\n *\n * const solved = arm.solve({ shoulder: 45 }); // full kinematic solve\n * const link = arm.part("Link", { shoulder: 60 }); // single part at state\n * const group = arm.toGroup({ shoulder: 45 }); // only when ShapeGroup behavior is needed\n * ```\n *\n * **Convenience transforms** (auto-solve at defaults, return `ShapeGroup`):\n *\n * ```ts\n * const positioned = arm.rotateZ(-90).translate(0, -20, 50);\n * ```\n *\n * **Merging into a parent**\n *\n * ```ts\n * require("./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 * @category Assembly\n */\ndeclare class ImportedAssembly {\n private readonly _assembly;\n private readonly _refs;\n private readonly _offset;\n constructor(_assembly: Assembly, _refs?: PlacementReferences, _offset?: readonly [\n number,\n number,\n number\n ]);\n /** The underlying Assembly — use for sweepJoint, addPart into parent, etc. */\n get assembly(): Assembly;\n /** Solve the assembly at the given joint state (defaults to each joint\'s default value). */\n solve(state?: JointState): SolvedAssembly;\n /**\n * Return a specific named part positioned at the given joint state, with any\n * stored placement offset applied.\n */\n part(name: string, state?: JointState): AssemblyPart;\n /**\n * Convert all assembly parts to a ShapeGroup with named children.\n * Use this for composition, transforms, or child lookup — not as a required\n * render step for assemblies.\n * Child names match the part names used in the assembly.\n * Any stored placement offset and placement references are forwarded to the group.\n */\n toGroup(state?: JointState): ShapeGroup;\n /**\n * Attach named placement reference points to this assembly.\n * Points are simple 3D coordinates (relative to the assembly\'s own origin).\n * Returns a new ImportedAssembly — does not mutate.\n */\n withReferences(refs: Pick<PlacementReferenceInput, "points">): ImportedAssembly;\n /** List all attached placement reference names. */\n referenceNames(kind?: PlacementReferenceKind): string[];\n /**\n * Translate the assembly so the named reference point lands on `target`.\n * Returns a new ImportedAssembly — does not mutate.\n * All point refs are translated by the same delta.\n */\n placeReference(ref: string, target: [\n number,\n number,\n number\n ], offset?: [\n number,\n number,\n number\n ]): ImportedAssembly;\n /** Solve at defaults and return a translated ShapeGroup. */\n translate(x: number, y: number, z: number): ShapeGroup;\n /** Solve at defaults and return a rotated ShapeGroup. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Solve at defaults and return a ShapeGroup rotated around X. */\n rotateX(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Solve at defaults and return a ShapeGroup rotated around Y. */\n rotateY(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Solve at defaults and return a ShapeGroup rotated around Z. */\n rotateZ(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Solve at defaults and return a scaled ShapeGroup. */\n scale(v: number | [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Solve at defaults and return a mirrored ShapeGroup. */\n mirror(normal: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Solve at defaults and return a colored ShapeGroup. */\n color(hex: string): ShapeGroup;\n /** Solve at defaults, get a named child part from the resulting group. */\n child(name: string): Shape | Sketch | ShapeGroup;\n /**\n * Flatten this sub-assembly\'s parts and joints into `parent` and wire a mount joint.\n *\n * **Details**\n *\n * All part and joint names from the sub-assembly are prefixed with\n * `"${options.prefix}."` to avoid collisions. After the merge, sub-assembly\n * joints are driven from the parent using the prefixed names:\n *\n * ```ts\n * parent.solve({ "Left Arm.shoulder": 45, "Right Arm.shoulder": -20 })\n * ```\n *\n * Joint couplings inside the sub-assembly are preserved and rewritten with\n * the prefix. Ports from sub-assembly parts are forwarded with the prefix.\n *\n * The sub-assembly must have exactly one root part. If it has multiple roots,\n * use `addFixed()` first to consolidate them before merging.\n *\n * **Example**\n *\n * ```ts\n * const robot = assembly("Robot").addPart("Chassis", chassis);\n *\n * require("./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 * @param parent - The target assembly to merge into\n * @param options - `prefix`, `mountParent`, `mountJoint`, `mountType`, `mountOptions`\n * @returns `parent` for chaining\n * @category Assembly\n */\n mergeInto(parent: Assembly, options: MergeIntoOptions): Assembly;\n}\ninterface RevoluteJointOpts {\n axis?: [\n number,\n number,\n number\n ];\n min?: number;\n max?: number;\n default?: number;\n unit?: string;\n reverse?: boolean;\n}\n/**\n * Create a revolute joint that auto-generates a parameter slider and rotates the shape.\n *\n * **Details**\n *\n * This is a convenience wrapper for single-shape, single-joint use cases. It calls\n * `param()` to create a named angle slider, then applies `rotateAroundAxis()` to the\n * shape. Use the full `Assembly` API for mechanisms with multiple parts and joints.\n *\n * **Example**\n *\n * ```ts\n * const arm = joint("Shoulder", armShape, [0, 0, 20], {\n * axis: [0, 1, 0],\n * min: -30, max: 120, default: 25,\n * });\n * return arm;\n * ```\n *\n * @param name - Display name for the generated angle parameter (shown as a UI slider)\n * @param shape - The shape to rotate\n * @param pivot - World-space pivot point `[x, y, z]`\n * @param opts - `axis`, `min`/`max` angles (degrees), `default` angle, `unit`, `reverse`\n * @returns The rotated shape at the current parameter value\n * @category Joints\n */\ndeclare function joint(name: string, shape: Shape, pivot: [\n number,\n number,\n number\n], opts?: RevoluteJointOpts): Shape;\ninterface BoundingRegion {\n xMin?: number;\n xMax?: number;\n yMin?: number;\n yMax?: number;\n zMin?: number;\n zMax?: number;\n}\n/**\n * Filter and sort criteria for edge selection.\n *\n * All filters are combined with AND logic — an edge must pass every\n * specified filter to be included. Omitting a filter means "any".\n *\n * @category Edge Queries\n */\ninterface EdgeQuery {\n /** Sort by proximity to this point (closest first). When used with `selectEdge`, picks the closest match. */\n near?: Vec3;\n /** Filter: edge direction approximately parallel to this vector. */\n parallel?: Vec3;\n /** Filter: edge direction approximately perpendicular to this vector. */\n perpendicular?: Vec3;\n /** Filter: only convex (outside corner) edges. */\n convex?: boolean;\n /** Filter: only concave (inside corner) edges. */\n concave?: boolean;\n /** Filter: minimum dihedral angle in degrees. */\n minAngle?: number;\n /** Filter: maximum dihedral angle in degrees. */\n maxAngle?: number;\n /** Filter: minimum edge length. */\n minLength?: number;\n /** Filter: maximum edge length. */\n maxLength?: number;\n /** Filter: edge midpoint must be within this bounding region. */\n within?: BoundingRegion;\n /** Shorthand: edge midpoint Z ≈ this value (within `tolerance`). Equivalent to `within: { zMin: atZ - tol, zMax: atZ + tol }`. */\n atZ?: number;\n /** Position tolerance for approximate matches (default: `1.0`). Used by `atZ` and `near`. */\n tolerance?: number;\n /** Angular tolerance in degrees for `parallel`/`perpendicular` filters (default: `10`). */\n angleTolerance?: number;\n}\n/**\n * Select all edges from a shape that match the given query.\n *\n * **Details**\n *\n * Extracts sharp edges from the mesh (dihedral angle > 1°), applies all\n * filters in the query, and returns the matching `EdgeSegment[]`. When `near`\n * is specified the results are sorted closest-first.\n *\n * Works on any shape — primitives, booleans, shells, and imported meshes.\n * Use this when tracked topology is unavailable (e.g. after a difference or\n * on imported geometry). For simpler cases, pass an `EdgeQuery` directly to\n * `fillet()` or `chamfer()` instead of calling `selectEdges` separately.\n *\n * **Example**\n *\n * ```ts\n * // Fillet all top edges of a box\n * const topEdges = selectEdges(part, { atZ: 20, perpendicular: [0, 0, 1] });\n * let result = part;\n * for (const edge of coalesceEdges(topEdges)) {\n * result = fillet(result, 2, edge);\n * }\n * ```\n *\n * @param shape - The solid to extract edges from\n * @param query - Optional filter/sort criteria\n * @returns Array of matching edge segments (may be empty)\n * @see {@link selectEdge} for the single-match variant\n * @see {@link coalesceEdges} to merge split tessellation segments\n * @category Edge Queries\n */\ndeclare function selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[];\n/**\n * Select the single best-matching edge from a shape.\n *\n * **Details**\n *\n * When `near` is specified, returns the edge whose midpoint is closest to\n * that point. Otherwise returns the first matching edge in mesh order.\n * Throws if no edges match the query — useful as a guard when you expect\n * exactly one result.\n *\n * **Example**\n *\n * ```ts\n * // Chamfer one specific edge near a known point\n * const bottomEdge = selectEdge(part, { near: [25, 0, 0], atZ: 0 });\n * result = chamfer(result, 1.5, bottomEdge);\n * ```\n *\n * @param shape - The solid to extract edges from\n * @param query - Filter/sort criteria; use `near` to pick the closest match\n * @returns The single best-matching edge segment\n * @throws If no edges match the query\n * @see {@link selectEdges} for multi-match variant\n * @category Edge Queries\n */\ndeclare function selectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment;\n/**\n * Merge collinear edge segments into longer logical edges.\n *\n * **Details**\n *\n * Tessellation often splits one geometric edge into multiple short segments.\n * `coalesceEdges` groups adjacent collinear segments and merges each group\n * into a single `EdgeSegment` spanning the full extent. This is usually\n * needed before passing edges to `fillet()` or `chamfer()` on non-primitive\n * shapes.\n *\n * The `tolerance` controls the maximum perpendicular distance from\n * collinearity before two segments are considered non-collinear. Default: `0.01`.\n *\n * **Example**\n *\n * ```ts\n * const topEdges = selectEdges(part, { atZ: 20 });\n * for (const edge of coalesceEdges(topEdges)) {\n * result = fillet(result, 2, edge);\n * }\n * ```\n *\n * @param segments - Edge segments from `selectEdges()`\n * @param tolerance - Max perpendicular deviation for collinearity (default: `0.01`)\n * @returns Merged edge segments\n * @see {@link selectEdges} to obtain the input segments\n * @category Edge Queries\n */\ndeclare function coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[];\ntype EdgeReferenceLike = {\n edges(): EdgeSegment[];\n};\ntype EdgeSelector = EdgeSegment | EdgeSegment[] | EdgeQuery | EdgeReferenceLike;\n/**\n * Apply fillets (rounded edges) to one or more edges of a shape.\n *\n * **Details**\n *\n * Works on both straight and curved edges. Supports OCCT and Manifold\n * backends. When using OCCT, all edges are filleted in a single kernel\n * operation for best quality. When using Manifold, edges are filleted\n * sequentially.\n *\n * The `edges` parameter is flexible:\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\n * pre-selected edges\n *\n * Throws if no edges match the selection, or if `radius` is not a positive\n * finite number.\n *\n * **Example**\n *\n * ```ts\n * // Fillet all edges\n * fillet(myShape, 2)\n *\n * // Fillet only top convex edges\n * fillet(myShape, 1.5, { atZ: 20, convex: true })\n *\n * // Fillet vertical edges selected beforehand\n * const edges = selectEdges(myShape, { parallel: [0, 0, 1] })\n * fillet(myShape, 3, edges)\n * ```\n *\n * @param shape - The solid to modify\n * @param radius - Fillet radius (must be positive and finite)\n * @param edges - Which edges to fillet: `EdgeSegment`, `EdgeSegment[]`, `EdgeQuery`, or `undefined` (all)\n * @param segments - Arc resolution for Manifold backend (default: `16`)\n * @returns A new Shape with the fillets applied\n * @see {@link chamfer} for beveled edges\n * @see {@link selectEdges} to pre-select edges\n * @see {@link filletTrackedEdge} for the compile-plan-based variant (tracked box edges)\n * @category Edge Features\n */\ndeclare function fillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape;\n/**\n * Apply chamfers (beveled edges) to one or more edges of a shape.\n *\n * **Details**\n *\n * Produces a 45° bevel at the specified `size` (distance from edge). Works on\n * both straight and curved edges. Supports OCCT and Manifold backends.\n *\n * The `edges` parameter accepts the same options as `fillet()`: inline\n * `EdgeQuery`, pre-selected `EdgeSegment`/`EdgeSegment[]`, or `undefined`\n * (all sharp edges).\n *\n * **Example**\n *\n * ```ts\n * // Chamfer all edges\n * chamfer(myShape, 1)\n *\n * // Chamfer only vertical edges\n * chamfer(myShape, 2, { parallel: [0, 0, 1] })\n * ```\n *\n * @param shape - The solid to modify\n * @param size - Chamfer size — distance from the edge (must be positive and finite)\n * @param edges - Which edges to chamfer: `EdgeSegment`, `EdgeSegment[]`, `EdgeQuery`, or `undefined` (all)\n * @returns A new Shape with the chamfers applied\n * @see {@link fillet} for rounded edges\n * @see {@link chamferTrackedEdge} for the compile-plan-based variant (tracked box edges)\n * @category Edge Features\n */\ndeclare function chamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape;\n/**\n * Apply a draft angle (taper) to vertical faces for mold extraction.\n *\n * **Details**\n *\n * Adds a taper angle to the vertical faces of a solid so that it can be\n * extracted from a mold. The neutral plane is the Z position where the draft\n * angle is zero — faces above and below are tapered symmetrically. Typical\n * values for injection molding are 1–5°.\n *\n * Requires the OCCT backend. Throws on Manifold.\n *\n * **Example**\n *\n * ```ts\n * // Add 3° draft to a box for injection molding\n * draft(myBox, 3)\n *\n * // Draft with custom pull direction and neutral plane\n * draft(myShape, 2, [0, 0, 1], 10)\n * ```\n *\n * @param shape - The solid to modify\n * @param angleDeg - Draft angle in degrees (typically 1–5 for injection molding)\n * @param pullDirection - Mold pull direction (default: `[0, 0, 1]` = Z-up)\n * @param neutralPlaneOffset - Z-offset of the neutral plane (default: `0`)\n * @returns A new Shape with draft applied\n * @category Edge Features\n */\ndeclare function draft(shape: Shape, angleDeg: number, pullDirection?: [\n number,\n number,\n number\n], neutralPlaneOffset?: number): Shape;\n/**\n * Uniformly offset all surfaces of a solid inward or outward.\n *\n * **Details**\n *\n * Unlike `shell()`, which hollows a solid by removing one face, `offsetSolid()`\n * produces a new solid whose every surface is shifted by `thickness`. Positive\n * values grow the shape outward; negative values shrink it inward.\n *\n * Requires the OCCT backend. Throws on Manifold.\n *\n * **Example**\n *\n * ```ts\n * // Grow a box outward by 1mm on all sides\n * offsetSolid(myBox, 1)\n *\n * // Shrink a shape inward by 0.5mm\n * offsetSolid(myShape, -0.5)\n * ```\n *\n * @param shape - The solid to offset\n * @param thickness - Offset distance (positive = outward, negative = inward; must be non-zero and finite)\n * @returns A new Shape with offset surfaces\n * @category Edge Features\n */\ndeclare function offsetSolid(shape: Shape, thickness: number): Shape;\ntype ToShapeTreeResult = Shape | ShapeGroup;\ninterface CombineOptions {\n op?: "union" | "intersection";\n}\n/**\n * Materialize one SDF leaf or all SDF leaves in a renderable tree.\n *\n * Raw `SdfShape` values become mesh-backed `Shape`s. Plain objects and arrays\n * preserve their renderable children as a `ShapeGroup` when more than one leaf\n * is found. Non-renderable metadata is ignored for materialization and remains\n * available to callers through normal `require()` return values.\n */\ndeclare function toShape(value: unknown, options?: SdfToShapeOptions): ToShapeTreeResult;\n/**\n * Collapse a tree of SDF leaves into one continuous SDF field.\n *\n * This intentionally discards per-leaf color/material identity because the\n * result is one scalar field. Use plain object returns for multi-material SDF\n * preview, and use `combine(...)` only when you want one implicit body.\n */\ndeclare function combine(value: unknown, options?: CombineOptions): SdfShape;\ntype MetricSize = "M2" | "M2.5" | "M3" | "M4" | "M5" | "M6" | "M8" | "M10";\ntype FastenerFit = "close" | "normal" | "loose" | "tap";\ninterface FastenerHoleOptions {\n /** Thread standard — only `\'iso-metric\'` is supported. */\n standard?: "iso-metric";\n /** ISO metric thread size, e.g. `\'M5\'`. Supported: M2–M10. */\n size: MetricSize;\n /**\n * Clearance fit class (default: `\'normal\'`).\n *\n * | Fit | Hole dia | Use when |\n * |-----|----------|----------|\n * | `\'close\'` | nominal + ~0.2 mm | Press-location or close-tolerance |\n * | `\'normal\'` | nominal + ~0.5 mm | Standard through-bolt clearance |\n * | `\'loose\'` | nominal + 1–2 mm | Adjustment slots or misaligned patterns |\n * | `\'tap\'` | ISO tap drill | Tapped hole in mating part |\n */\n fit?: FastenerFit;\n /** Hole depth in mm. */\n depth: number;\n /**\n * Optional counterbore at the top of the hole (wider recess for a socket-head cap).\n * `diameter` defaults to the ISO head-clearance diameter for the chosen size.\n */\n counterbore?: {\n depth: number;\n diameter?: number;\n };\n /**\n * Optional countersink at the top of the hole (conical recess for a flat-head screw).\n * `angleDeg` defaults to `90`.\n */\n countersink?: {\n diameter: number;\n angleDeg?: number;\n };\n /**\n * When `true` (default), the hole is centered on Z=0 (extends from −depth/2 to +depth/2).\n * When `false`, the top face is at Z=0 and the hole extends downward.\n */\n center?: boolean;\n /** Polygon segment count for the cylinder approximation (default: 48). */\n segments?: number;\n}\ninterface TSlotProfileOptions {\n /** Outer profile size (square). */\n size?: number;\n /** Slot mouth width (the narrow opening at each side). */\n slotWidth?: number;\n /** Wider interior slot cavity width. Must be >= slotWidth. */\n slotInnerWidth?: number;\n /** Total slot depth from outer face inward. */\n slotDepth?: number;\n /** Depth of the narrow mouth before it widens into slotInnerWidth. */\n slotNeckDepth?: number;\n /** Outer shell wall thickness. */\n wall?: number;\n /** Central cross-web thickness. */\n web?: number;\n /** Center boss diameter (solid material around center bore). */\n centerBossDia?: number;\n /** Center bore diameter (for tapping/through-hole). Set 0 to disable. */\n centerBoreDia?: number;\n /** Outer corner radius. */\n outerCornerRadius?: number;\n /** Segment count used for circular features in 2D. */\n segments?: number;\n}\ninterface TSlotExtrusionOptions extends TSlotProfileOptions {\n}\ninterface Profile2020BSlot6ProfileOptions {\n /** Slot mouth width. */\n slotWidth?: number;\n /** Wider inner slot width. */\n slotInnerWidth?: number;\n /** Slot depth from outer face. */\n slotDepth?: number;\n /** Depth of the narrow neck before widening into slotInnerWidth. */\n slotNeckDepth?: number;\n /** Center core bore diameter (set 0 to disable). */\n centerBoreDia?: number;\n /** Solid boss diameter around center bore (must exceed centerBoreDia when bore is enabled). */\n centerBossDia?: number;\n /** Width of diagonal ribs connecting center boss to corner regions. */\n diagonalWebWidth?: number;\n /** Outside corner radius. */\n outerCornerRadius?: number;\n /** Circle segment count. */\n segments?: number;\n}\ninterface Profile2020BSlot6Options extends Profile2020BSlot6ProfileOptions {\n}\ninterface ExplodeNamedItem {\n name: string;\n shape?: Shape | ShapeGroup;\n sketch?: Sketch;\n color?: string;\n group?: ExplodeItem[];\n explode?: ExplodeDirective;\n}\ntype ExplodeItem = Shape | Sketch | ShapeGroup | ExplodeNamedItem;\ninterface ExplodeOptions extends ExplodeConfigOptions {\n}\ntype BeltVec2 = [\n number,\n number\n];\ntype BeltMode = "open" | "crossed";\ninterface TangentCircle2D {\n name?: string;\n center: BeltVec2;\n radius: number;\n}\ninterface BeltPulley2D {\n name?: string;\n center: BeltVec2;\n pitchRadius: number;\n}\ninterface TangentLoop2DOptions {\n /** `open` uses external tangents; `crossed` uses internal tangents. */\n mode?: BeltMode;\n}\ninterface BeltDriveOptions {\n pulleys: BeltPulley2D[] | Record<string, Omit<BeltPulley2D, "name"> & {\n name?: string;\n }>;\n /** Belt width along +Z. */\n beltWidth: number;\n /** Belt thickness in the pulley plane. Default 2mm. */\n beltThickness?: number;\n /**\n * Reserved for multi-pulley route intent. The first implementation supports\n * two-pulley routes and rejects multi-pulley calls with explicit guidance.\n */\n route?: "outer" | BeltRouteContact[];\n /** Visual stroke width for the returned pitch path sketch. Default 0.25mm. */\n pitchPathWidth?: number;\n}\ninterface BeltRouteContact {\n pulley: string;\n wrap?: "cw" | "ccw" | "short" | "long";\n tangentIn?: "left" | "right" | "internal" | "external";\n tangentOut?: "left" | "right" | "internal" | "external";\n}\ninterface BeltLineSpan {\n kind: "line";\n fromPulley: string;\n toPulley: string;\n from: BeltVec2;\n to: BeltVec2;\n length: number;\n}\ninterface BeltWrapArc {\n kind: "arc";\n pulley: string;\n center: BeltVec2;\n pitchRadius: number;\n from: BeltVec2;\n to: BeltVec2;\n sweepDeg: number;\n wrapDeg: number;\n length: number;\n tangentIn: BeltVec2;\n tangentOut: BeltVec2;\n}\ntype BeltPathSegment = BeltLineSpan | BeltWrapArc;\ninterface BeltDriveResult {\n belt: Shape;\n beltProfile: Sketch;\n pitchPath: Sketch;\n route: TangentLoop2D;\n length: number;\n wraps: BeltWrapArc[];\n wrapByPulley: Record<string, BeltWrapArc>;\n straightSpans: BeltLineSpan[];\n skippedPulleys: string[];\n}\ndeclare class TangentLoop2D {\n readonly circles: TangentCircle2D[];\n readonly mode: BeltMode;\n readonly segments: BeltPathSegment[];\n readonly straightSpans: BeltLineSpan[];\n readonly wraps: BeltWrapArc[];\n readonly wrapByPulley: Record<string, BeltWrapArc>;\n readonly length: number;\n constructor(circles: TangentCircle2D[], options?: TangentLoop2DOptions);\n /** Convert the loop centerline into a thin visual sketch. */\n toSketch(width?: number): Sketch;\n /** Convert the loop into a filled profile using the pitch path itself as the boundary. */\n toProfile(): Sketch;\n /** Build a belt band sketch by offsetting the route to inner and outer pulley radii. */\n offsetBand(thickness: number): Sketch;\n}\ninterface SpurGearOptions {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n boreDiameter?: number;\n segmentsPerTooth?: number;\n}\ninterface SideGearOptions extends SpurGearOptions {\n side?: "top" | "bottom";\n toothHeight?: number;\n}\ninterface FaceGearOptions extends SideGearOptions {\n}\ninterface RingGearOptions {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n rimWidth?: number;\n outerDiameter?: number;\n segmentsPerTooth?: number;\n}\ninterface RackGearOptions {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n baseHeight?: number;\n}\ninterface BevelGearOptions {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n boreDiameter?: number;\n pitchAngleDeg?: number;\n mateTeeth?: number;\n shaftAngleDeg?: number;\n segmentsPerTooth?: number;\n}\ninterface GearPairSpec {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth?: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n boreDiameter?: number;\n segmentsPerTooth?: number;\n}\ninterface GearPairOptions {\n pinion: Shape | GearPairSpec;\n gear: Shape | GearPairSpec;\n backlash?: number;\n centerDistance?: number;\n place?: boolean;\n phaseDeg?: number;\n}\ninterface GearPairDiagnostic {\n level: "info" | "warn" | "error";\n code: string;\n message: string;\n}\ninterface GearPairResult {\n pinion: Shape;\n gear: Shape;\n centerDistance: number;\n centerDistanceNominal: number;\n backlash: number;\n pressureAngleDeg: number;\n workingPressureAngleDeg: number;\n contactRatio: number;\n jointRatio: number;\n speedReduction: number;\n /** Phase rotation (degrees) for the gear around its shaft axis for correct tooth\n * mesh alignment. When `place: true` this is already baked into `gear`.\n * When `place: false`, rotate the gear by this amount before positioning. */\n phaseDeg: number;\n diagnostics: GearPairDiagnostic[];\n status: "ok" | "warn" | "error";\n}\ninterface GearMeshPlacement {\n pinionAxis: [\n number,\n number,\n number\n ];\n gearAxis: [\n number,\n number,\n number\n ];\n pinionCenter: [\n number,\n number,\n number\n ];\n gearCenter: [\n number,\n number,\n number\n ];\n}\ninterface BevelGearPairSpec extends GearPairSpec {\n}\ninterface BevelGearPairOptions {\n pinion: Shape | BevelGearPairSpec;\n gear: Shape | BevelGearPairSpec;\n shaftAngleDeg?: number;\n backlash?: number;\n place?: boolean;\n phaseDeg?: number;\n}\ninterface SideGearSpec extends GearPairSpec {\n side?: "top" | "bottom";\n toothHeight?: number;\n}\ninterface FaceGearSpec extends SideGearSpec {\n}\ninterface SideGearPairOptions {\n side: Shape | SideGearSpec;\n vertical: Shape | GearPairSpec;\n backlash?: number;\n centerDistance?: number;\n meshPlaneZ?: number;\n place?: boolean;\n phaseDeg?: number;\n}\ninterface BevelGearPairResult extends GearMeshPlacement {\n pinion: Shape;\n gear: Shape;\n shaftAngleDeg: number;\n pinionPitchAngleDeg: number;\n gearPitchAngleDeg: number;\n coneDistance: number;\n backlash: number;\n jointRatio: number;\n speedReduction: number;\n /** Phase rotation (degrees) for gear tooth mesh alignment. See GearPairResult.phaseDeg. */\n phaseDeg: number;\n diagnostics: GearPairDiagnostic[];\n status: "ok" | "warn" | "error";\n}\ninterface SideGearPairResult {\n side: Shape;\n vertical: Shape;\n centerDistance: number;\n centerDistanceNominal: number;\n backlash: number;\n pressureAngleDeg: number;\n meshPlaneZ: number;\n radialOverlap: number;\n jointRatio: number;\n speedReduction: number;\n /** Phase rotation (degrees) for the vertical gear. See GearPairResult.phaseDeg. */\n phaseDeg: number;\n diagnostics: GearPairDiagnostic[];\n status: "ok" | "warn" | "error";\n}\ninterface FaceGearPairOptions {\n face: Shape | FaceGearSpec;\n vertical: Shape | GearPairSpec;\n backlash?: number;\n centerDistance?: number;\n meshPlaneZ?: number;\n place?: boolean;\n phaseDeg?: number;\n}\ninterface FaceGearPairResult {\n face: Shape;\n vertical: Shape;\n centerDistance: number;\n centerDistanceNominal: number;\n backlash: number;\n pressureAngleDeg: number;\n meshPlaneZ: number;\n radialOverlap: number;\n jointRatio: number;\n speedReduction: number;\n /** Phase rotation (degrees) for the vertical gear. See GearPairResult.phaseDeg. */\n phaseDeg: number;\n diagnostics: GearPairDiagnostic[];\n status: "ok" | "warn" | "error";\n}\ntype Vec2$1 = [\n number,\n number\n];\ninterface BoltPatternOptions {\n /** Metric bolt size (\'M2\'–\'M10\'). */\n size: MetricSize;\n /** Clearance class. Default: \'normal\'. */\n fit?: FastenerFit;\n /** List of [x, y] positions in the pattern\'s 2D coordinate frame. */\n positions: Vec2$1[];\n /** Number of circle segments per hole. Default: 48. */\n segments?: number;\n}\ninterface BoltPatternCutOptions {\n /**\n * Z position of the top of the cutters (before punching through). For a cut\n * from z=0 downward through a 20mm plate, pass `from: -1` and `depth: 22` to\n * ensure the cutter fully punches through with some margin.\n */\n from?: number;\n /** Optional counterbore. */\n counterbore?: {\n depth: number;\n diameter?: number;\n };\n /** Optional countersink. */\n countersink?: {\n diameter: number;\n angleDeg?: number;\n };\n}\ninterface BoltPattern {\n /** Bolt size (\'M5\' etc.). */\n readonly size: MetricSize;\n /** Clearance diameter in mm, derived from size + fit. */\n readonly dia: number;\n /** Positions in 2D (XY of the plane being cut). */\n readonly positions: ReadonlyArray<Vec2$1>;\n /** Min X across all positions — useful for housing sizing. */\n readonly minX: number;\n /** Max X across all positions. */\n readonly maxX: number;\n /** Min Y across all positions. */\n readonly minY: number;\n /** Max Y across all positions. */\n readonly maxY: number;\n /**\n * Subtract the pattern from a shape. Cuts one ISO-clearance hole at each position.\n *\n * @param shape - Shape to cut.\n * @param depth - Depth of the cut along Z.\n * @param options - Optional counterbore/countersink and Z offset.\n */\n cut(shape: Shape, depth: number, options?: BoltPatternCutOptions): Shape;\n}\ntype WasherStandard = "din-125-a";\ninterface FastenerSetDimensions {\n size: MetricSize;\n nominalDiameter: number;\n boltLength: number;\n clearanceDia: number;\n tapDia: number;\n nutAcrossFlats: number;\n nutHeight: number;\n washerOuterDia: number;\n washerInnerDia: number;\n washerThickness: number;\n}\ninterface FastenerSetOptions {\n /** Include a DIN 125-A washer under the bolt head (default: `true`). */\n washerUnderHead?: boolean;\n /** Include a DIN 125-A washer under the nut (default: `true`). */\n washerUnderNut?: boolean;\n /**\n * Clearance fit class for the through-hole cutter (default: `\'normal\'`).\n *\n * | Fit | Hole dia | Use when |\n * |-----|----------|----------|\n * | `\'close\'` | nominal + ~0.2 mm | Press-location or close-tolerance |\n * | `\'normal\'` | nominal + ~0.5 mm | Standard through-bolt clearance |\n * | `\'loose\'` | nominal + 1–2 mm | Adjustment slots or misaligned patterns |\n * | `\'tap\'` | ISO tap drill | Tapped hole in mating part |\n */\n fit?: FastenerFit;\n /** Polygon segment count for all circular geometry (default: 36). */\n segments?: number;\n}\ninterface FastenerSetResult {\n /** Hex bolt: head top at z=0, threaded shaft extends toward −Z by `boltLength`. */\n bolt: Shape;\n /** Hex nut centered at z=0. */\n nut: Shape;\n /** Flat washer centered at z=0. Null when washerUnderHead is false. */\n washerUnderHead: Shape | null;\n /** Flat washer centered at z=0. Null when washerUnderNut is false. */\n washerUnderNut: Shape | null;\n /** Clearance-hole cutter (cylinder) centered at z=0, for subtracting from a through-plate. */\n clearanceHole: Shape;\n /** Tap-drill cutter (cylinder) centered at z=0, for subtracting from a tapped plate. */\n tappedHole: Shape;\n /** Reference dimensions for BOM, placement calculations, and documentation. */\n dims: FastenerSetDimensions;\n}\n/**\n * Pre-built parametric parts available in user scripts as `lib.*`.\n *\n * **Details**\n *\n * Every key in this object becomes a method on the `lib` namespace exposed to\n * `.forge.js` scripts. The catalog includes:\n *\n * **Fasteners:** `bolt`, `nut`, `washer`, `fastenerSet`, `fastenerHole`,\n * `boltHole`, `counterbore`, `hexNut`, `holePattern`\n *\n * **Structure:** `tube`, `pipe`, `bracket`, `pipeRoute`, `elbow`,\n * `tSlotProfile`, `tSlotExtrusion`, `profile2020BSlot6Profile`, `profile2020BSlot6`\n *\n * **Belt drives:** `beltDrive`, `tangentLoop2d`\n *\n * **Threads:** `thread`\n *\n * **Gears:** `spurGear`, `bevelGear`, `faceGear`, `sideGear`, `ringGear`,\n * `rackGear`, `gearPair`, `bevelGearPair`, `faceGearPair`, `sideGearPair`\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 *\n * Extend this by adding new entries here and registering the corresponding\n * runner binding in `runner.ts`. Sizes outside the supported ranges will throw\n * at runtime with a descriptive error.\n *\n * @category Part Library\n */\ndeclare const partLibrary: {\n/**\n * Simple cylindrical through-hole cutter centered on Z=0.\n *\n * Subtract the result from a part to produce a plain cylindrical clearance hole.\n * For ISO metric sizes with fit classes and counterbore/countersink, use\n * {@link fastenerHole} instead.\n *\n * @param diameter - Hole diameter in mm.\n * @param depth - Hole depth in mm.\n * @returns A cylinder shape to be subtracted from a solid.\n * @category Fasteners\n */\n\n boltHole(diameter: number, depth: number): Shape;\n/**\n * ISO metric fastener hole cutter with optional counterbore or countersink.\n *\n * **Details**\n *\n * Returns a cutter shape (subtract from a solid to produce the hole). Sizes\n * outside M2–M10 will throw. Extend `METRIC_HOLE_TABLE` in this file to add\n * new sizes.\n *\n * **Example**\n *\n * ```ts\n * const plate = box(60, 40, 8)\n * .subtract(lib.fastenerHole({ size: \'M5\', fit: \'normal\', depth: 8 })\n * .translate(15, 10, 4));\n * ```\n *\n * @param opts - Hole configuration including size, fit class, depth, and optional recesses.\n * @returns A cutter shape centered on Z=0 (or positioned at Z=0 top when `center: false`).\n * @category Fasteners\n */\n\n fastenerHole(opts: FastenerHoleOptions): Shape;\n/**\n * Counterbore hole cutter — through-hole with a wider cylindrical recess at the top.\n *\n * Use for socket-head cap screws that must sit flush. Subtract from a solid.\n * For ISO metric sizing and fit classes, prefer {@link fastenerHole} with `counterbore`.\n *\n * @param holeDia - Clearance hole diameter in mm.\n * @param boreDia - Counterbore diameter in mm (must be ≥ holeDia).\n * @param boreDepth - Counterbore depth in mm.\n * @param totalDepth - Total through-hole depth in mm.\n * @returns A cutter shape centered on Z=0.\n * @category Fasteners\n */\n\n counterbore(holeDia: number, boreDia: number, boreDepth: number, totalDepth: number): Shape;\n/**\n * Rectangular hollow tube (thin-wall box section).\n *\n * Both the outer and inner boxes are centered on the XY plane with their base at Z=0.\n *\n * @param outerX - Outer width in mm.\n * @param outerY - Outer depth in mm.\n * @param outerZ - Length (height) of the tube in mm.\n * @param wall - Wall thickness in mm (removed uniformly from all four sides).\n * @returns A hollow rectangular tube solid.\n * @category Fasteners\n */\n\n tube(outerX: number, outerY: number, outerZ: number, wall: number): Shape;\n/**\n * Hollow cylindrical pipe.\n *\n * Centered on the XY plane, extending upward along +Z from z=0 to z=height.\n * For complex routed pipe geometry, see `lib.pipeRoute`.\n *\n * @param height - Pipe length in mm.\n * @param outerRadius - Outer radius in mm.\n * @param wall - Wall thickness in mm.\n * @param segments - Polygon approximation segments (default: 32).\n * @returns A hollow cylinder solid.\n * @category Fasteners\n */\n\n pipe(height: number, outerRadius: number, wall: number, segments?: number): Shape;\n/**\n * Apply deterministic exploded-view offsets to an assembly tree.\n *\n * **Details**\n *\n * Traverses arrays of shapes/sketches/named items, nested `{ name, group: [...] }` structures,\n * and `ShapeGroup` outputs, translating each node by a computed offset while preserving names,\n * colors, and nesting. Returns the same structure type as the input.\n *\n * In `radial` mode the algorithm is branch-aware and parent-relative: each node fans out from\n * its immediate parent\'s center, so nested assemblies peel apart level by level. Named items may\n * also include an inline `explode: { stage?, direction?, axisLock? }` property to override\n * per-item behavior.\n *\n * Use this function when you want to bake the explode offset into the geometry before returning\n * (e.g. to drive the amount with a `param()` slider). For a viewport-only explode slider without\n * rerunning the script, use `explodeView()` instead.\n *\n * **Example**\n *\n * ```js\n * const explodeAmt = param(\'Explode\', 0, { min: 0, max: 40, unit: \'mm\' });\n *\n * return lib.explode(assembly, {\n * amount: explodeAmt,\n * stages: [0.4, 0.8],\n * mode: \'radial\',\n * byName: { Shaft: { direction: [1, 0, 0], stage: 1.4 } },\n * });\n * ```\n *\n * @param items - Array of `Shape | Sketch | ShapeGroup | { name, shape?, sketch?, group?, explode? }`, or a `ShapeGroup`\n * @param options.amount - Total explode distance in model units\n * @param options.stages - Per-depth stage multipliers (depth 1 = first level). Default: reciprocal depth (`1, 1/2, 1/3, ...`)\n * @param options.mode - Direction mode: `\'radial\'` | `\'x\'` | `\'y\'` | `\'z\'` | `[x, y, z]`. Default: `\'radial\'`\n * @param options.axisLock - Constrain motion to a world axis: `\'x\'` | `\'y\'` | `\'z\'`\n * @param options.byName - Per-object overrides keyed by name: `{ stage?, direction?, axisLock? }`\n * @param options.byPath - Per-tree-path overrides using slash-separated path segments\n * @returns Same structure type as input, with translated geometry\n * @see {@link explodeView} for viewport-slider-driven explode without rerunning the script\n * @category Explode View\n */\n\n explode<T extends ExplodeItem[] | ShapeGroup>(items: T, options?: ExplodeOptions): T;\n/**\n * Generic hex nut with a cylindrical bore.\n *\n * Constructed via intersection of three rotated rectangular slabs, then a bore\n * is subtracted. Centered at origin, height along Z.\n *\n * For standard ISO metric nuts by thread size, use `lib.nut` instead.\n *\n * @param acrossFlats - Distance across flats in mm.\n * @param height - Nut height (thickness) in mm.\n * @param holeDia - Inner bore diameter in mm.\n * @returns A hexagonal nut solid centered at origin.\n * @category Fasteners\n */\n\n hexNut(acrossFlats: number, height: number, holeDia: number): Shape;\n/**\n * L-shaped mounting bracket with optional through-holes.\n *\n * Produces a right-angle bracket: a horizontal base plate and a vertical wall.\n * Both legs share `width`. Optional holes are drilled through the base (along Z)\n * and the wall (along Y).\n *\n * @param width - Width of the bracket (both legs share this dimension) in mm.\n * @param height - Wall leg height in mm.\n * @param depth - Base leg depth in mm.\n * @param thick - Material thickness in mm (both legs).\n * @param holeDia - Hole diameter in mm. Pass `0` (default) to omit holes.\n * @returns An L-bracket solid with base at Z=0.\n * @category Fasteners\n */\n\n bracket(width: number, height: number, depth: number, thick: number, holeDia?: number): Shape;\n/**\n * Rectangular grid of cylindrical hole cutters.\n *\n * Returns the union of `rows × cols` cylinders laid out on a regular grid.\n * Subtract from a solid to produce the full pattern.\n *\n * **Example**\n *\n * ```ts\n * const pattern = lib.holePattern(3, 4, 20, 20, 4, 10);\n * const panel = box(80, 70, 10).subtract(pattern.translate(-30, -20, 0));\n * ```\n *\n * @param rows - Number of rows in the grid.\n * @param cols - Number of columns in the grid.\n * @param spacingX - Column pitch in mm.\n * @param spacingY - Row pitch in mm.\n * @param holeDia - Hole diameter in mm.\n * @param depth - Hole depth in mm.\n * @returns Union of cutter cylinders, origin at the first hole center.\n * @category Fasteners\n */\n\n holePattern(rows: number, cols: number, spacingX: number, spacingY: number, holeDia: number, depth: number): Shape;\n/**\n * External helical thread — clean mesh, no SDF grid artifacts.\n *\n * **Details**\n *\n * Builds a cross-section with a single trapezoidal tooth from the root radius\n * out to the crest radius, then twist-extrudes it so the tooth traces a helix.\n * Manifold\'s extrude+twist produces structured quad-based geometry that follows\n * the thread profile cleanly.\n *\n * Returns a threaded cylinder along +Z from z=0 to z=length.\n *\n * **Example**\n *\n * ```ts\n * const t = lib.thread(5, 0.8, 12); // M5 × 0.8 pitch, 12 mm long\n * ```\n *\n * @param diameter - Nominal (crest) diameter in mm.\n * @param pitch - Thread pitch in mm (axial distance between crests).\n * @param length - Thread length in mm.\n * @param options - Optional overrides: `depth` (tooth height, default 0.35×pitch), `segments` (default 36).\n * @returns A threaded cylinder solid.\n * @category Fasteners\n */\n\n thread(diameter: number, pitch: number, length: number, options?: {\n depth?: number;\n segments?: number;\n}): Shape;\n/**\n * ISO-style hex bolt with real helical threads.\n *\n * **Details**\n *\n * The hex head sits from z=0 up to z=headHeight. The shaft extends downward\n * along −Z by `length` mm. An unthreaded shank section is included when\n * `threadLength < length`.\n *\n * Default proportions follow ISO 4762 loosely: pitch ≈ 0.15×diameter,\n * head height ≈ 0.65×diameter, across-flats ≈ 1.6×diameter.\n *\n * For standard M-size bolts pre-configured for a complete joint, use\n * {@link fastenerSet} instead.\n *\n * **Example**\n *\n * ```ts\n * const b = lib.bolt(5, 20); // M5 × 20 mm\n * ```\n *\n * @param diameter - Nominal shaft diameter in mm.\n * @param length - Shaft length in mm (head height not included).\n * @param options - Optional overrides for pitch, head height, across-flats, thread length, and segments.\n * @returns A hex bolt solid with head top at z=0.\n * @category Fasteners\n */\n\n bolt(diameter: number, length: number, options?: {\n pitch?: number;\n headHeight?: number;\n headAcrossFlats?: number;\n threadLength?: number;\n segments?: number;\n}): Shape;\n/**\n * ISO-style hex nut with a threaded bore.\n *\n * **Details**\n *\n * Constructed from the intersection of three rotated slabs with a cylindrical\n * bore subtracted. The nut is centered at the origin, height along Z.\n *\n * Default proportions follow ISO 4032 loosely: height ≈ 0.8×diameter,\n * across-flats ≈ 1.6×diameter. The bore is a clearance bore (not modelled\n * with helical threads) for rendering efficiency.\n *\n * For standard M-size nuts pre-configured for a complete joint, use\n * {@link fastenerSet} instead.\n *\n * **Example**\n *\n * ```ts\n * const n = lib.nut(5); // M5 nut\n * ```\n *\n * @param diameter - Nominal thread diameter in mm.\n * @param options - Optional overrides for pitch, height, across-flats, and segments.\n * @returns A hex nut solid centered at origin.\n * @category Fasteners\n */\n\n nut(diameter: number, options?: {\n pitch?: number;\n height?: number;\n acrossFlats?: number;\n segments?: number;\n}): Shape;\n/**\n * ISO metric flat washer (DIN 125-A).\n *\n * **Details**\n *\n * Returns a flat ring centered at the origin, thickness along Z. Dimensions\n * are taken from {@link WASHER_TABLE}. Sizes outside M2–M10 will throw.\n *\n * **Example**\n *\n * ```ts\n * const w = lib.washer(\'M5\'); // DIN 125-A M5 washer\n * ```\n *\n * @param size - ISO metric thread size, e.g. `\'M5\'`. Supported: M2–M10.\n * @param options - Optional `standard` (only `\'din-125-a\'` supported) and `segments` (default 48).\n * @returns A flat washer ring solid centered at origin.\n * @category Fasteners\n */\n\n washer(size: MetricSize, options?: {\n standard?: WasherStandard;\n segments?: number;\n}): Shape;\n/**\n * Complete ISO metric fastener set — bolt, nut, optional washers, and matching hole cutters.\n *\n * **Details**\n *\n * Returns all geometry for one bolted joint: the bolt, nut, up to two washers,\n * a clearance-hole cutter, and a tap-drill cutter. All shapes are returned\n * **un-positioned** (each on the Z-axis). Place them with `.translate()`.\n *\n * Sizes outside M4–M10 are supported for the washer (M2–M10); unsupported\n * combinations will throw.\n *\n * **Example**\n *\n * ```ts\n * const hw = lib.fastenerSet(\'M5\', 20);\n *\n * const topPlate = box(60, 40, 8).translate(0, 0, 12)\n * .subtract(hw.clearanceHole.translate(15, 10, 12));\n * const botPlate = box(60, 40, 8)\n * .subtract(hw.clearanceHole.translate(15, 10, 0));\n *\n * return [\n * { name: \'Top Plate\', shape: topPlate },\n * { name: \'Bot Plate\', shape: botPlate },\n * { name: \'Bolt\', shape: hw.bolt.translate(15, 10, 20) },\n * { name: \'Nut\', shape: hw.nut.translate(15, 10, -4) },\n * ];\n * ```\n *\n * @param size - ISO metric thread size, e.g. `\'M5\'`. Supported: M4–M10 (bolt/nut), M2–M10 (washer).\n * @param boltLength - Nominal shaft length in mm (head height not included). Must be > 0.\n * @param options - Washer inclusion, fit class, and segment count.\n * @returns A {@link FastenerSetResult} with all joint shapes and reference dimensions.\n * @category Fasteners\n */\n\n fastenerSet(size: MetricSize, boltLength: number, options?: FastenerSetOptions): FastenerSetResult;\n/**\n * Route a pipe (solid or hollow) through 3D waypoints with smooth bends.\n *\n * Each interior waypoint gets a torus-section bend. Straight segments connect them.\n * Returns a single unioned Shape.\n */\n\n pipeRoute(points: [\n number,\n number,\n number\n][], radius: number, options?: {\n bendRadius?: number;\n wall?: number;\n segments?: number;\n}): Shape;\n/**\n * Pipe elbow — a curved pipe section (torus arc) for connecting two pipe directions.\n *\n * By default creates a bend in the XZ plane: incoming along +Z, outgoing rotated by `angle`.\n * The bend starts at the origin, curving away from it.\n *\n * @param pipeRadius - Pipe outer radius\n * @param bendRadius - Centerline bend radius (distance from arc center to pipe center)\n * @param angle - Bend angle in degrees (e.g. 90 for a right-angle bend)\n * @param options.wall - Wall thickness for hollow pipe\n * @param options.segments - Circumferential segments (default 32)\n * @param options.from - Incoming direction vector (default [0,0,1])\n * @param options.to - Outgoing direction vector (overrides angle if both from/to given)\n */\n\n elbow(pipeRadius: number, bendRadius: number, angle?: number | {\n from?: [\n number,\n number,\n number\n ];\n to?: [\n number,\n number,\n number\n ];\n wall?: number;\n segments?: number;\n}, options?: {\n wall?: number;\n segments?: number;\n from?: [\n number,\n number,\n number\n ];\n to?: [\n number,\n number,\n number\n ];\n}): Shape;\n/**\n * Create a flat open-belt body around two pulley pitch circles.\n *\n * The belt is generated as a tangent loop in the XY plane and extruded along\n * +Z by `beltWidth`. The result includes the solid belt, the 2D belt profile,\n * a thin pitch-path sketch for visualization, total belt length, tangent spans,\n * and wrap metadata for each pulley.\n *\n * For more than two pulleys, the API intentionally asks for route intent before\n * geometry is created. Use `route: "outer"` for the future outside-envelope\n * mode, or an ordered route for future serpentine/idler layouts.\n *\n * ```ts\n * const drive = lib.beltDrive({\n * pulleys: [\n * { name: "motor", center: [0, 0], pitchRadius: 12 },\n * { name: "output", center: [80, 0], pitchRadius: 28 },\n * ],\n * beltWidth: 8,\n * beltThickness: 2,\n * });\n * return drive.belt;\n * ```\n *\n * @category Belt Drives\n */\n\n beltDrive(options: BeltDriveOptions): BeltDriveResult;\n/**\n * Build a closed 2D route made from common tangent spans and pulley wrap arcs.\n *\n * Use this when you need reusable belt/chain route geometry before creating a\n * solid body. The first implementation supports two circles. `mode: "open"`\n * uses external tangents; `mode: "crossed"` uses internal tangents.\n *\n * ```ts\n * const route = lib.tangentLoop2d([\n * { center: [0, 0], radius: 12 },\n * { center: [80, 0], radius: 28 },\n * ]);\n * const belt = route.offsetBand(2).extrude(8);\n * ```\n *\n * @category Belt Drives\n */\n\n tangentLoop2d(circles: TangentCircle2D[], options?: TangentLoop2DOptions): TangentLoop2D;\n/**\n * Build a 2D T-slot cross-section sketch.\n *\n * Default parameters describe a 20x20 B-type profile with slot 6.\n * Use this when you want a drawing-ready profile sketch before extrusion.\n */\n\n tSlotProfile(options?: TSlotProfileOptions): Sketch;\n/**\n * Build a T-slot extrusion from the generated 2D profile.\n * Extrudes along +Z by default.\n */\n\n tSlotExtrusion(length: number, options?: TSlotExtrusionOptions): Shape;\n/**\n * Accurate-ish 2D profile for 20x20 B-type slot 6.\n *\n * Returns a drawing-ready Sketch centered at origin.\n */\n\n profile2020BSlot6Profile(options?: Profile2020BSlot6ProfileOptions): Sketch;\n/**\n * 20x20 B-type slot 6 extrusion with profile-accurate defaults.\n *\n * Pass option overrides if your supplier\'s profile differs slightly.\n */\n\n profile2020BSlot6(length: number, options?: Profile2020BSlot6Options): Shape;\n/**\n * Involute external spur gear with optional center bore.\n *\n * Specify module, teeth, faceWidth as required parameters. Optional tuning includes\n * pressureAngleDeg (default 20), backlash, clearance, addendum, dedendum, boreDiameter,\n * and segmentsPerTooth (default 10).\n *\n * **Connectors (for assembly-based positioning):**\n * - `bore`: revolute connector at the bore center, axis along +Z. Carries\n * measurements: `{ module, teeth, pitchRadius, outerRadius, faceWidth }`.\n *\n * Use `.connect("Housing.seat", "Gear.bore")` to mount a gear on a shaft seat.\n */\n\n spurGear(options: SpurGearOptions): Shape;\n/**\n * Conical bevel gear generated from a tapered involute extrusion. Specify pitchAngleDeg directly or derive it from mateTeeth + shaftAngleDeg.\n *\n * **Connectors (for assembly-based positioning):**\n * - `bore`: revolute connector at the large-end bore center (Z=0), axis along -Z (away from teeth).\n * - `apex`: connector at the cone apex above the gear (the point where the pitch cone converges),\n * axis along +Z. Useful for meshing two bevel gears — their apices should coincide.\n *\n * Carries measurements: `{ module, teeth, pitchRadius, pitchAngleDeg, coneDistance, faceWidth }`.\n */\n\n bevelGear(options: BevelGearOptions): Shape;\n/**\n * Face gear (crown style) where teeth are on one face (top or bottom) instead of the outer rim.\n *\n * Uses the same involute tooth sizing as spurGear, then projects the tooth band axially from one side.\n * Alias for sideGear (which is kept for backward compatibility).\n */\n\n faceGear(options: FaceGearOptions): Shape;\n/**\n * Crown/face style gear where the teeth project from one side of the disk\n * instead of the outer cylindrical rim.\n */\n\n sideGear(options: SideGearOptions): Shape;\n/**\n * Internal ring gear with involute-derived tooth spaces. Specify rimWidth or outerDiameter for the annular body.\n *\n * **Connectors (for assembly-based positioning):**\n * - `bore`: connector at the ring center, axis along +Z. For planetary gearboxes, this is\n * where the ring mounts to the housing. Carries measurements:\n * `{ module, teeth, pitchRadius, innerRadius, outerRadius, faceWidth }`.\n */\n\n ringGear(options: RingGearOptions): Shape;\n/**\n * Linear rack gear with pressure-angle flanks. Use with spurGear for rack-and-pinion mechanisms.\n *\n * **Orientation:** teeth run along the X axis with tooth tips pointing +Y (pitch line at Y=0).\n * The rack is extruded +Z by `faceWidth`. Rotate the rack to align with a different slide axis.\n *\n * **Connectors (for assembly-based positioning):**\n * - `teeth`: prismatic connector at the pitch line center, axis along +X (slide direction).\n * Carries measurements: `{ module, teeth, faceWidth, length }`.\n *\n * Connect to a housing\'s rack channel:\n * ```js\n * housing.withConnectors({\n * rack_channel: connector("rack-channel", {\n * origin: [pitchR, 0, channelZ], axis: [1, 0, 0], kind: "prismatic",\n * }),\n * });\n * assembly.connect("Housing.rack_channel", "Rack.teeth", { as: "slide" });\n * ```\n */\n\n rackGear(options: RackGearOptions): Shape;\n/**\n * Build or validate a spur-gear pair and return ratio, backlash, and mesh diagnostics.\n *\n * Accepts either shapes from spurGear() or analytical specs for each member.\n * When place is true (default), the gear is auto-positioned at the correct center distance.\n */\n\n gearPair(options: GearPairOptions): GearPairResult;\n/** Build or validate a bevel-gear pair and return ratio diagnostics plus recommended joint placement vectors. */\n\n bevelGearPair(options: BevelGearPairOptions): BevelGearPairResult;\n/** Build or validate a perpendicular pair between a face gear and a vertical spur gear. */\n\n faceGearPair(options: FaceGearPairOptions): FaceGearPairResult;\n/**\n * Pair helper for side (crown/face) gear + perpendicular "vertical" spur gear.\n * Auto-placement rotates the spur around +Y and positions it to mesh at the side tooth band.\n */\n\n sideGearPair(options: SideGearPairOptions): SideGearPairResult;\n/**\n * Coupling ratio between two meshed spur gears.\n *\n * When gear A turns 1°, gear B turns `-teethA / teethB` degrees (negative because\n * meshed external gears rotate in opposite directions).\n *\n * @example\n * ```js\n * assembly.addJointCoupling("B_spin", {\n * terms: [{ joint: "A_spin", ratio: lib.gearRatio(12, 24) }], // -0.5\n * });\n * ```\n *\n * Pass `{ internal: true }` for internal gear pairs (ring gear + spur/planet),\n * where the two rotate in the same direction.\n */\n\n gearRatio(teethA: number, teethB: number, options?: {\n internal?: boolean;\n}): number;\n/**\n * Coupling ratio between a pinion and a rack.\n *\n * When the pinion rotates by `θ` degrees, the rack slides by `θ × (π × module × teeth / 360)` mm.\n * Equivalently, 1mm of rack travel = `180 / (π × pitchRadius)` degrees of pinion rotation.\n *\n * @example\n * ```js\n * // Pinion spin driven by rack slide:\n * assembly.addJointCoupling("pinion_spin", {\n * terms: [{ joint: "rack_slide", ratio: lib.rackRatio(1.5, 12) }], // ~6.37 deg/mm\n * });\n * ```\n */\n\n rackRatio(module: number, pinionTeeth: number): number;\n/**\n * Planetary gear reduction ratio when the ring is held fixed.\n *\n * Input: sun. Output: carrier. Ratio: `1 + ringTeeth / sunTeeth`.\n * One turn of the sun produces `1 / ratio` turns of the carrier.\n */\n\n planetaryRatio(sunTeeth: number, ringTeeth: number): number;\n/**\n * Define a bolt pattern once and cut it from multiple parts.\n *\n * @example\n * ```js\n * const bolts = lib.boltPattern({\n * size: \'M5\',\n * positions: [[20, 15], [-20, 15], [20, -15], [-20, -15]],\n * });\n *\n * const base = bolts.cut(box(60, 50, 10), 12, { from: -1 });\n * const cover = bolts.cut(box(60, 50, 3), 5, { from: -1 });\n * // Same positions in both parts — guaranteed aligned.\n * ```\n *\n * @category Part Library\n */\n\n boltPattern(options: BoltPatternOptions): BoltPattern;\n};\ninterface ProductMaterial {\n color?: string;\n material?: ShapeMaterialProps;\n}\ninterface ClearMaterialOptions {\n tint?: string;\n opacity?: number;\n}\ninterface ColorMaterialOptions {\n color?: string;\n}\ntype ProductProfileKind = "oval" | "roundedRect" | "circle" | "superEllipse" | "custom";\ninterface ProductProfileDescriptor {\n sketch: Sketch;\n width: number;\n depth: number;\n kind: ProductProfileKind;\n radius?: number;\n}\ninterface ProductProfileOptions {\n segments?: number;\n}\ninterface ProductSuperEllipseOptions extends ProductProfileOptions {\n /** Higher values produce squarer product surfaces; 2 is an ellipse. */\n exponent?: number;\n}\ntype ProductRailKind = "bezier" | "nurbs" | "polyline";\ninterface ProductRailSpec {\n kind: ProductRailKind;\n points: Vec3[];\n degree?: number;\n name?: string;\n}\ntype ProductScenePreset = "product" | "service" | "mechanical";\ninterface ProductStationProfile {\n sketch: Sketch;\n width: number;\n depth: number;\n kind: ProductProfileKind;\n radius?: number;\n exponent?: number;\n}\ninterface ProductStationSpec {\n name: string;\n center: Vec3;\n profile: ProductStationProfile;\n crown?: number;\n}\ninterface ProductStationSuperEllipseOptions {\n segments?: number;\n exponent?: number;\n}\ndeclare class ProductStationBuilder {\n readonly name: string;\n private centerValue;\n private profileValue?;\n private crownValue;\n constructor(name: string);\n /** Position this station in world coordinates. */\n at(point: Vec3): this;\n /** Convenience for traditional Z-up section stacks. */\n z(z: number): this;\n /** Convenience for product bodies running front-to-back along Y. */\n y(y: number): this;\n /** Convenience for product bodies running left-to-right along X. */\n x(x: number): this;\n /** Use an oval cross-section with full width and depth dimensions. */\n oval(width: number, depth: number, options?: {\n segments?: number;\n }): this;\n /** Use a superellipse cross-section for soft-square product surfaces. */\n superEllipse(width: number, depth: number, options?: ProductStationSuperEllipseOptions): this;\n /** Use a rounded-rectangle cross-section with the given corner radius. */\n roundedRect(width: number, depth: number, radius: number): this;\n /** Use a circular cross-section from a full diameter. */\n circle(diameter: number, options?: {\n segments?: number;\n }): this;\n /** Use a custom 2D sketch as the station cross-section. */\n custom(sketch: Sketch, width: number, depth: number): this;\n /** Stores a semantic crown amount for diagnostics and future rail solving. */\n crown(amount: number): this;\n /** Return the immutable station spec consumed by Product.skin(). */\n toSpec(): ProductStationSpec;\n}\ntype ProductSkinAxis = "X" | "Y" | "Z";\ntype ProductSkinSide = "left" | "right" | "top" | "bottom" | "front" | "rear" | "back";\ntype ProductSkinRepresentation = "exact" | "sampled" | "mixed" | "fallback";\ninterface ProductSkinRefQuery {\n /** Side of the product skin. `front` is the minimum axis cap, `rear`/`back` is the maximum axis cap. */\n side: ProductSkinSide;\n /** Across-side parameter for side refs. Defaults to 0.5. */\n u?: number;\n /** Along-axis parameter, 0 at the first cap and 1 at the rear/back cap. Defaults to 0.5. */\n v?: number;\n /** Positive distance away from the surface along the resolved normal. */\n offset?: number;\n}\ninterface ProductSurfaceFrame {\n point: Vec3;\n normal: Vec3;\n tangentU: Vec3;\n tangentV: Vec3;\n matrix: Mat4;\n skin: string;\n representation: ProductSkinRepresentation;\n}\ninterface ProductSkinDiagnostics {\n representation: ProductSkinRepresentation;\n lowering: string[];\n warnings: string[];\n stationNames: string[];\n railNames: string[];\n}\ninterface ProductAttachOptions {\n offset?: number;\n inset?: number;\n}\ninterface ProductPanelAttachOptions extends ProductAttachOptions {\n at?: Partial<ProductSkinRefQuery>;\n thickness?: number;\n material?: ProductMaterial;\n color?: string;\n}\ntype ProductRefInput = ProductSurfaceRef;\ndeclare class ProductSurfaceRef {\n private readonly skin;\n private readonly query;\n readonly name?: string | undefined;\n constructor(skin: ProductSkin, query: ProductSkinRefQuery, name?: string | undefined);\n /** Resolve this semantic surface ref into a point, normal, tangents, and placement matrix. */\n frame(overrides?: Partial<ProductSkinRefQuery>): ProductSurfaceFrame;\n /** Return a copy of this ref with side/u/v/offset overrides. */\n with(overrides: Partial<ProductSkinRefQuery>): ProductSurfaceRef;\n /** Place a detail shape or group on this ref\'s local surface frame. */\n attach(detail: Shape | ShapeGroup, options?: ProductAttachOptions): Shape | ShapeGroup;\n /** Return the serializable side/u/v query behind this ref. */\n querySpec(): ProductSkinRefQuery;\n}\ndeclare class ProductSkin {\n readonly name: string;\n readonly shape: Shape;\n readonly axis: ProductSkinAxis;\n readonly stations: ProductStationSpec[];\n readonly rails: Record<string, ProductRailSpec>;\n private readonly refQueries;\n private readonly axisMin;\n private readonly axisMax;\n private readonly diagnosticsValue;\n constructor(name: string, shape: Shape, axis: ProductSkinAxis, stations: ProductStationSpec[], rails: Record<string, ProductRailSpec>, refs: Record<string, ProductSkinRefQuery>, diagnostics: Omit<ProductSkinDiagnostics, "stationNames" | "railNames">);\n /** Return the renderable shape generated for this product skin. */\n toShape(): Shape;\n /** Create a group containing this skin plus named child details. */\n with(...children: GroupInput[]): ShapeGroup;\n /** Boolean-union structural details into the skin body. */\n integrate(...details: Shape[]): Shape;\n /** Return lowering representation, station names, rail names, and warnings. */\n diagnostics(): ProductSkinDiagnostics;\n /** Create a side/u/v surface-ref query on this skin. */\n uv(side: ProductSkinSide, u?: number, v?: number): ProductSkinRefQuery;\n /** Resolve a named ref published with Product.skin().refs(...). */\n ref(name: string): ProductSurfaceRef;\n /** Create a sampled curve as a sequence of surface refs on this skin. */\n curveOnSurface(name: string, points: Array<Partial<ProductSkinRefQuery> & {\n side: ProductSkinSide;\n }>): ProductSurfaceRef[];\n /** Interpolate center, width, and depth at a normalized v or absolute axis value. */\n stationAt(vOrAxis: number): {\n center: Vec3;\n width: number;\n depth: number;\n dWidth: number;\n dDepth: number;\n axisValue: number;\n };\n /** Build a local surface frame from a side/u/v query. */\n frame(query: ProductSkinRefQuery): ProductSurfaceFrame;\n}\ndeclare class ProductSkinBuilder {\n readonly name: string;\n private axisValue;\n private stationsValue;\n private railsValue;\n private refsValue;\n private materialValue;\n private colorValue;\n private edgeLengthValue;\n private wallValue;\n constructor(name: string);\n /** Choose the primary station axis for the skin loft. */\n axis(axis: ProductSkinAxis): this;\n /** Set named cross-section stations for the product skin. */\n stations(stations: Array<ProductStationBuilder | ProductStationSpec>): this;\n /** Attach guide rails as ProductSkin IR metadata and diagnostics. */\n rails(rails: Record<string, ProductRailSpec>): this;\n /** Publish a named semantic surface ref on the skin. */\n ref(name: string, query: ProductSkinRefQuery): this;\n /** Publish multiple named semantic surface refs on the skin. */\n refs(refs: Record<string, ProductSkinRefQuery>): this;\n /** Create a side/u/v surface-ref query for use in refs(...) or Product.ref(...). */\n uv(side: ProductSkinSide, u?: number, v?: number): ProductSkinRefQuery;\n /** Apply a product material preset to the lowered skin. */\n material(material: ProductMaterial): this;\n /** Apply a simple color override to the lowered skin. */\n color(color: string): this;\n /** Set the sampled loft target edge length. */\n edgeLength(value: number): this;\n /** Records a target wall thickness; v1 keeps exterior skin lowering sampled and reports wall as a diagnostic. */\n wall(thickness: number): this;\n /** Lower stations and refs into a ProductSkin body. */\n build(): ProductSkin;\n}\ndeclare class ProductPanelBuilder {\n readonly name: string;\n private profileValue;\n private thicknessValue;\n private materialValue;\n private colorValue;\n constructor(name: string);\n /** Use a rounded rectangle panel profile. */\n rounded(width: number, height: number, radius?: number): this;\n /** Use an oval panel profile. */\n oval(width: number, height: number): this;\n /** Use a custom 2D panel profile. */\n profile(profile: Sketch): this;\n /** Set panel extrusion thickness. */\n thickness(thickness: number): this;\n /** Apply a product material preset to the panel. */\n material(material: ProductMaterial): this;\n /** Apply a simple color override to the panel. */\n color(color: string): this;\n /** Build the panel in local coordinates. */\n build(): Shape;\n /** Build and attach this panel to a ProductSurfaceRef. */\n attachTo(ref: ProductRefInput, options?: ProductPanelAttachOptions): Shape;\n}\ndeclare class ProductSpoutBuilder {\n readonly name: string;\n private fromValue?;\n private sectionsValue;\n private projectionValue;\n private materialValue;\n private colorValue;\n private edgeLengthValue;\n constructor(name: string);\n /** Set the skin ref this spout projects from. */\n from(ref: ProductSurfaceRef): this;\n /** Set local spout section profiles from root to mouth. */\n sections(sections: Array<Sketch | ProductStationBuilder | ProductStationSpec>): this;\n /** Set the projection length along the source ref normal. */\n projection(length: number): this;\n /** Set the sampled loft target edge length for the spout. */\n edgeLength(value: number): this;\n /** Apply a product material preset to the spout. */\n material(material: ProductMaterial): this;\n /** Apply a simple color override to the spout. */\n color(color: string): this;\n /** Build the spout in local coordinates. */\n build(): Shape;\n /** Build and place the spout on its source ref. */\n attach(options?: ProductAttachOptions): Shape;\n}\ndeclare class ProductHandleFeature {\n readonly grip: Shape;\n readonly upperPad: Shape;\n readonly lowerPad: Shape;\n constructor(grip: Shape, upperPad: Shape, lowerPad: Shape);\n /** Return the physical shapes that make up this handle feature. */\n structural(): Shape[];\n /** Boolean-union the handle feature into a single shape. */\n toShape(): Shape;\n /** Return the handle as a named ShapeGroup preserving child colors. */\n toGroup(): ShapeGroup;\n}\ndeclare class ProductHandleBuilder {\n readonly name: string;\n private upperRef?;\n private lowerPoint?;\n private spineValue?;\n private gripProfile;\n private materialValue;\n private padMaterialValue;\n private edgeLengthValue;\n constructor(name: string);\n /** Set the upper body ref and lower world anchor for the handle. */\n between(upper: ProductSurfaceRef, lower: Vec3): this;\n /** Set an explicit handle centerline from points or a rail spec. */\n spine(points: Vec3[] | ProductRailSpec): this;\n /** Set the grip cross-section profile. */\n grip(profile: Sketch): this;\n /** Apply a product material preset to the grip. */\n material(material: ProductMaterial): this;\n /** Apply a product material preset to handle landing pads. */\n padMaterial(material: ProductMaterial): this;\n /** Set the sampled loft target edge length for the grip. */\n edgeLength(value: number): this;\n /** Build the handle grip and landing pads. */\n build(): ProductHandleFeature;\n}\ndeclare const Product: {\n /** Start a named product skin builder. */\n skin(name: string): ProductSkinBuilder;\n /** Start a named cross-section station for Product.skin(...).stations(...). */\n station(name: string): ProductStationBuilder;\n /** Namespaced rail builders for product skin guide rails and handle spines. */\n rail: {\n bezier(points: Vec3[], options?: {\n name?: string;\n }): ProductRailSpec;\n nurbs(points: Vec3[], options?: {\n degree?: number;\n name?: string;\n }): ProductRailSpec;\n polyline(points: Vec3[], options?: {\n name?: string;\n }): ProductRailSpec;\n };\n /** Namespaced product profile helpers for stations, panels, trims, and openings. */\n profiles: {\n /** Create a centered oval profile using full product width and depth dimensions. */\n oval(width: number, depth: number, options?: ProductProfileOptions): Sketch;\n /** Create a centered superellipse profile for soft square-to-oval product sections. */\n superEllipse(width: number, depth: number, options?: ProductSuperEllipseOptions): Sketch;\n /** Create a centered rounded-rectangle product profile. */\n roundedRect(width: number, depth: number, radius: number): Sketch;\n /** Create a centered circular product profile from its diameter. */\n circle(diameter: number, options?: ProductProfileOptions): Sketch;\n };\n /** Namespaced product material presets for molded plastic, rubber, metal, and transparent parts. */\n materials: {\n mattePlastic(color?: string): ProductMaterial;\n softRubber(options?: ColorMaterialOptions): ProductMaterial;\n clearPolycarbonate(options?: ClearMaterialOptions): ProductMaterial;\n brushedSteel(options?: ColorMaterialOptions): ProductMaterial;\n };\n /** Apply a product material preset to a Shape. */\n applyMaterial(shape: Shape, preset: ProductMaterial | undefined): Shape;\n /** Apply an opinionated scene preset for product review renders. */\n scenePreset(name: ProductScenePreset): void;\n /** Create a centered oval profile from full width/depth dimensions. */\n ovalProfile(width: number, depth: number, options?: ProductProfileOptions): Sketch;\n /** Create a centered rounded-rectangle profile. */\n roundedRectProfile(width: number, depth: number, radius: number): Sketch;\n /** Create a centered circular profile from full diameter. */\n circleProfile(diameter: number, options?: ProductProfileOptions): Sketch;\n /** Create a centered superellipse profile for soft-square product sections. */\n superEllipseProfile(width: number, depth: number, options?: ProductSuperEllipseOptions): Sketch;\n /** Measure the width and depth of a 2D profile sketch. */\n profileSize(sketch: Sketch): {\n width: number;\n depth: number;\n };\n /** Describe a custom sketch as a product profile. */\n describeProfile(sketch: Sketch, kind?: ProductProfileKind, radius?: number): ProductProfileDescriptor;\n /** Scale an existing profile sketch to a target width/depth. */\n scaleProfileTo(sketch: Sketch, width: number, depth: number): Sketch;\n /** Create an ad-hoc ProductSurfaceRef from a skin and side/u/v query. */\n ref(skin: ProductSkin, query: ProductSkinRefQuery): ProductSurfaceRef;\n /** Start a panel feature builder. */\n panel(name: string): ProductPanelBuilder;\n /** Start a spout/nozzle feature builder. */\n spout(name: string): ProductSpoutBuilder;\n /** Start a handle feature builder. */\n handle(name: string): ProductHandleBuilder;\n /** Place a shape or group on a ProductSurfaceRef. */\n place(detail: Shape | ShapeGroup, ref: ProductRefInput, options?: ProductAttachOptions): Shape | ShapeGroup;\n /** Small blended landing volume for manual structural bridges and connection proofs. */\n landing(name: string, radius?: number, material?: ProductMaterial): Shape;\n};\ndeclare function distance(a: Vec3, b: Vec3): number;\ndeclare function midpoint(a: Vec3, b: Vec3): Vec3;\ndeclare function lerp(a: Vec3, b: Vec3, t: number): Vec3;\ndeclare function direction(a: Vec3, b: Vec3): Vec3;\ndeclare function offset(point: Vec3, dir: Vec3, amount: number): Vec3;\ndeclare const Points: {\n /** Euclidean distance between two 3D points. */\n readonly distance: typeof distance;\n /** Center point between two 3D points. */\n readonly midpoint: typeof midpoint;\n /** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */\n readonly lerp: typeof lerp;\n /** Unit direction vector from a to b. Throws if a and b are the same point. */\n readonly direction: typeof direction;\n /** Move a point along a direction vector by a given amount. */\n readonly offset: typeof offset;\n /** Compute a 2D point at distance and angle (degrees) from an optional origin. */\n readonly polar: typeof polar;\n};\ninterface WoodBoardOptions {\n /** Wood species, e.g. "birch", "oak", "plywood". Default: "wood" */\n species?: string;\n /** Material description for BOM, e.g. "birch plywood". Default: species value */\n material?: string;\n /** Grain direction: "long" (along width) or "cross" (along height). Default: "long" */\n grain?: string;\n /** Color hex string for visualization. Default: "#d2b48c" (tan/wood) */\n color?: string;\n /** If false, skip automatic BOM registration. Default: true */\n autoBom?: boolean;\n}\n/**\n * A board of wood with metadata for manufacturing: grain direction, species,\n * and dimensions. The underlying geometry is a simple box.\n *\n * WoodBoard operations are immutable. Joint operations return new boards instead\n * of carving the original in-place, and transform methods preserve all metadata.\n *\n * @category Woodworking\n */\ndeclare class WoodBoard {\n /** The underlying 3D shape. */\n shape: Shape;\n /** Board width (mm) — the longer flat dimension */\n readonly width: number;\n /** Board height (mm) — the shorter flat dimension */\n readonly height: number;\n /** Board thickness (mm) */\n readonly thickness: number;\n /** Grain direction: "long" or "cross" */\n readonly grain: string;\n /** Wood species, e.g. "birch", "oak" */\n readonly species: string;\n /** Material label for BOM */\n readonly material: string;\n constructor(width: number, height: number, thickness: number, opts?: WoodBoardOptions);\n /**\n * Subtract a cutter from this board, returning a new board.\n * Used by joint functions (dado, rabbet, mortiseAndTenon).\n */\n cut(cutter: Shape): WoodBoard;\n /** Create a new WoodBoard with a pre-built shape, preserving metadata. No BOM re-registration. */\n private _withShape;\n /** Translate the board in 3D space. */\n translate(x: number, y: number, z: number): WoodBoard;\n /** Rotate the board around an axis by a given angle in degrees. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): WoodBoard;\n /** Rotate the board around the X axis by a given angle in degrees. */\n rotateX(angleDeg: number): WoodBoard;\n /** Rotate the board around the Y axis by a given angle in degrees. */\n rotateY(angleDeg: number): WoodBoard;\n /** Rotate the board around the Z axis by a given angle in degrees. */\n rotateZ(angleDeg: number): WoodBoard;\n /** Mirror the board across a plane defined by its normal. */\n mirror(normal: [\n number,\n number,\n number\n ]): WoodBoard;\n /** Set the board\'s display color. */\n color(value: string): WoodBoard;\n /** Clone the board (creates an independent copy of the underlying shape). */\n clone(): WoodBoard;\n}\n/**\n * Options for a dado joint — a channel cut across the face of a board.\n * @category Woodworking\n */\ninterface DadoOptions {\n /** Distance from the bottom edge of the host to the bottom of the channel (mm). */\n fromBottom?: number;\n /** Distance from the top edge of the host to the top of the channel (mm). Alternative to fromBottom. */\n fromTop?: number;\n /** Channel depth into the board thickness (mm). Default: 1/3 of host thickness. */\n depth?: number;\n /** Fit tolerance. Default: \'snug\'. */\n fit?: "friction" | "snug" | "slip";\n /** If set, the dado stops this far from the front edge (mm), creating a stopped dado. */\n stopped?: number;\n}\ndeclare function dado(host: WoodBoard, guest: WoodBoard, opts: DadoOptions): WoodBoard;\n/**\n * Options for a rabbet joint — an L-shaped step along an edge.\n * @category Woodworking\n */\ninterface RabbetOptions {\n /** Which edge to cut the rabbet on. */\n edge: "top" | "bottom" | "left" | "right" | "back";\n /** How far into the board face the rabbet extends (mm). */\n width: number;\n /** How deep the step is cut into the thickness (mm). */\n depth: number;\n}\ndeclare function rabbet(board: WoodBoard, opts: RabbetOptions): WoodBoard;\n/**\n * Options for a mortise-and-tenon joint.\n * @category Woodworking\n */\ninterface MortiseAndTenonOptions {\n /** \'blind\' (default): mortise doesn\'t go through. \'through\': mortise goes all the way. */\n style?: "blind" | "through";\n /** Position of the mortise center along the mortise board height. */\n position?: {\n fromTop?: number;\n fromBottom?: number;\n };\n /** Tenon thickness (mm). Default: 1/3 of tenon board thickness. */\n tenonThickness?: number;\n /** Tenon width (mm). Default: 60% of tenon board height. */\n tenonWidth?: number;\n /** Tenon length (mm). Default: 2/3 of mortise board thickness for blind, full for through. */\n tenonLength?: number;\n /** Corner radius for mortise (mm). Default: 0 (square corners, hand tools). */\n cornerRadius?: number;\n /** Fit tolerance. Default: \'snug\'. */\n fit?: "friction" | "snug" | "slip" | "knockdown";\n}\ninterface MortiseAndTenonResult {\n mortiseBoard: WoodBoard;\n tenonBoard: WoodBoard;\n}\ndeclare function mortiseAndTenon(mortiseBoard: WoodBoard, tenonBoard: WoodBoard, opts?: MortiseAndTenonOptions): MortiseAndTenonResult;\n/**\n * Woodworking 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\n * immutable — they return new board value(s) with the joint cut applied.\n *\n * @category Woodworking\n */\ndeclare const Wood: {\n /**\n * Create a wood board with metadata for manufacturing.\n *\n * The board is a box(width, height, thickness) centered on XY, base at Z=0.\n * Width along X, height along Y, thickness along Z (0 to thickness).\n *\n * @param width - Board width in mm (X-axis).\n * @param height - Board height in mm (Y-axis).\n * @param thickness - Board thickness in mm (Z-axis).\n * @param opts - Species, grain, color, and BOM options.\n * @returns A new WoodBoard instance.\n * @category Woodworking\n */\n readonly board: (width: number, height: number, thickness: number, opts?: WoodBoardOptions) => WoodBoard;\n /**\n * Cut a dado (channel) across the face of a host board for a guest board to sit in.\n *\n * Returns a new host board with the dado cut applied.\n *\n * @category Woodworking\n */\n readonly dado: typeof dado;\n /**\n * Cut a rabbet (L-shaped step) along an edge of a board.\n *\n * Returns a new board with the rabbet cut applied.\n *\n * @category Woodworking\n */\n readonly rabbet: typeof rabbet;\n /**\n * Cut a mortise in one board and shape a tenon on another.\n *\n * Returns new boards with the mortise pocket and tenon cuts applied.\n *\n * @category Woodworking\n */\n readonly mortiseAndTenon: typeof mortiseAndTenon;\n};\n/**\n * Highlight all user-labeled faces on a shape for visual debugging.\n *\n * Shows each user-authored label name in the viewport for visual debugging.\n * Returns the shape unchanged for chaining: `return showLabels(myShape)`.\n */\ndeclare function showLabels(shape: Shape): Shape;\n/** Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch. */\ndeclare function intersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch;\n/**\n * Extract the boundary profile of a named face as a 2D sketch.\n *\n * The result is returned in the face\'s local 2D coordinate system, making it convenient\n * for offsets, pocket profiles, or follow-up sketch operations driven by an existing face.\n *\n * @param shape - Source shape containing the face.\n * @param face - Named face selector to extract.\n * @returns A sketch of the face boundary in face-local coordinates.\n */\ndeclare function faceProfile(shape: Shape, face: FaceSelector): Sketch;\n/** Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch. */\ndeclare function projectToPlane(shape: Shape, plane: PlaneSpec): Sketch;\ninterface SheetMetalOptions {\n /**\n * Base panel dimensions. This is the flat blank before flanges are applied.\n */\n panel: {\n /** Width of the panel along the X axis. */\n width: number;\n /** Height of the panel along the Y axis. */\n height: number;\n };\n /** Sheet thickness in mm. Applied uniformly across the panel and all flanges. */\n thickness: number;\n /** Inside bend radius in mm. Must be ≥ 0. Typically 0.5–2× the sheet thickness. */\n bendRadius: number;\n /**\n * Bend allowance model used when computing the flat-pattern developed length.\n *\n * Currently only K-factor is supported. The K-factor (0–1) describes how far\n * the neutral axis sits from the inner bend surface. Typical values:\n * - Soft materials / large radius: 0.50\n * - General sheet steel: 0.42–0.44\n * - Hard materials / tight radius: 0.30–0.38\n */\n bendAllowance: {\n /** K-factor (neutral-axis offset, 0–1). */\n kFactor: number;\n };\n /**\n * Corner relief cut at each bend intersection.\n *\n * Prevents material overlap when two flanges meet at a corner. Defaults to a\n * rectangular relief sized to `bendRadius + thickness` if omitted.\n */\n cornerRelief?: {\n /** Relief shape — only `\'rect\'` is supported in v1. */\n kind?: "rect";\n /** Side length of the square relief cut in mm. */\n size: number;\n };\n}\ninterface SheetMetalFlangeOptions {\n /** Flange leg length in mm, measured from the outside of the bend to the tip. */\n length: number;\n /**\n * Bend angle in degrees (default: `90`).\n *\n * Only `90°` is supported in v1. Values other than 90 will be rejected at\n * build time.\n */\n angleDeg?: number;\n}\ninterface SheetMetalCutoutOptions {\n /** Horizontal offset within the region, measured from the region centre (mm). Default: `0`. */\n u?: number;\n /** Vertical offset within the region, measured from the region centre (mm). Default: `0`. */\n v?: number;\n /**\n * Anchor point on the sketch that aligns to `(u, v)`.\n *\n * Use `\'center\'` for most cases. For asymmetric profiles, verify orientation\n * by placing one test cutout before committing to the final position.\n *\n * Default: `\'center\'`.\n */\n selfAnchor?: Anchor;\n}\ninterface SheetMetalCutoutOp {\n region: SheetMetalPlanarRegionName;\n sketch: Sketch;\n u: number;\n v: number;\n selfAnchor: Anchor;\n}\n/**\n * An immutable sheet metal part that accumulates flanges and cutouts.\n *\n * Each mutating method returns a **new** `SheetMetalPart`; the original is\n * unchanged. The part does not produce geometry until you call `.folded()` or\n * `.flatPattern()`.\n *\n * @category Sheet Metal\n */\ndeclare class SheetMetalPart {\n private readonly model;\n private readonly cutouts;\n constructor(model: SheetMetalModel, cutouts?: readonly SheetMetalCutoutOp[]);\n /**\n * Add a 90° flange along one edge of the base panel.\n *\n * **Details**\n *\n * Each of the four edges (`\'top\'`, `\'right\'`, `\'bottom\'`, `\'left\'`) may carry\n * at most one flange. Calling `.flange()` twice for the same edge throws.\n *\n * Corner reliefs are automatically inserted at the intersections of adjacent\n * flanges. Build flanges before cutouts — validate with `.folded()` and\n * `.flatPattern()` after each addition.\n *\n * **Example**\n *\n * ```ts\n * const 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 * @param edge - Which panel edge gets the flange: `\'top\' | \'right\' | \'bottom\' | \'left\'`.\n * @param options - Flange leg length and optional bend angle (only 90° in v1).\n * @returns A new `SheetMetalPart` with the flange added.\n * @category Sheet Metal\n */\n flange(edge: SheetMetalEdge, options: SheetMetalFlangeOptions): SheetMetalPart;\n /**\n * Subtract a 2D sketch cutout from a planar region of the sheet metal part.\n *\n * **Details**\n *\n * `region` must be `\'panel\'` or one of `\'flange-top\'`, `\'flange-right\'`,\n * `\'flange-bottom\'`, `\'flange-left\'` (only available once the corresponding\n * flange has been added). Cutouts inside bend regions are **not** supported\n * in v1.\n *\n * `sketch` must be an **unplaced** compile-covered 2D profile (e.g. the result\n * of `circle2d()`, `rect()`, `roundedRect()`). Passing an already-placed\n * sketch (one that has had `.onFace(...)` called on it) will throw.\n *\n * **Authoring order:** Add all flanges before adding cutouts. Add panel\n * cutouts before flange cutouts. Add one region at a time and validate with\n * `.folded()` / `.flatPattern()` after each step.\n *\n * **Example**\n *\n * ```ts\n * const 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 * @param region - Target planar region: `\'panel\'` or `\'flange-<edge>\'`.\n * @param sketch - Unplaced 2D sketch profile to cut through the region.\n * @param options - Optional placement offsets and self-anchor point.\n * @returns A new `SheetMetalPart` with the cutout added.\n * @category Sheet Metal\n */\n cutout(region: SheetMetalPlanarRegionName, sketch: Sketch, options?: SheetMetalCutoutOptions): SheetMetalPart;\n /**\n * Return all semantic region names currently available on this part.\n *\n * **Details**\n *\n * The returned list always includes `\'panel\'`. For every flange that has been\n * added, the list also includes the corresponding `\'flange-<edge>\'` and\n * `\'bend-<edge>\'` entries.\n *\n * Use this to discover valid targets for `.cutout()` or for querying faces\n * by region after materializing with `.folded()`.\n *\n * Defended region names:\n * `panel` | `flange-top` | `flange-right` | `flange-bottom` | `flange-left` |\n * `bend-top` | `bend-right` | `bend-bottom` | `bend-left`\n *\n * @returns Array of semantic region name strings for this part\'s current configuration.\n * @category Sheet Metal\n */\n regionNames(): SheetMetalRegionName[];\n /**\n * Materialize the 3D folded solid.\n *\n * **Details**\n *\n * Applies all flanges (bent up at their configured angles) and all registered\n * cutouts, then returns the resulting `Shape`. The shape is compiler-owned\n * and exact-exportable (STEP, IGES, etc.).\n *\n * Prefer calling `.folded()` to validate each build step before proceeding\n * to the final model.\n *\n * @returns The fully folded 3D sheet metal solid.\n * @see {@link SheetMetalPart.flatPattern} for the unfolded blank.\n * @category Sheet Metal\n */\n folded(): Shape;\n /**\n * Materialize the flat-pattern (unfolded blank) for fabrication.\n *\n * **Details**\n *\n * Unfolds all flanges using the K-factor bend allowance and lays the result\n * flat in the XY plane. Cutouts are projected into the flat geometry.\n * The returned shape is exact-exportable and ready for laser / waterjet / CNC\n * nesting workflows.\n *\n * The developed length of each bend zone is:\n * `BA = (bendRadius + kFactor × thickness) × angleDeg × π / 180`\n *\n * @returns The flat 2D blank as a 3D solid of uniform thickness.\n * @see {@link SheetMetalPart.folded} for the 3D folded shape.\n * @category Sheet Metal\n */\n flatPattern(): Shape;\n private buildOutput;\n}\n/**\n * Create a parametric sheet metal part with flanges, bend allowances, and flat-pattern unfolding.\n *\n * **Details**\n *\n * `sheetMetal()` keeps one semantic model and derives both a folded 3D solid\n * and an accurate flat pattern from it. The K-factor bend allowance is applied\n * during unfolding. This is a strict v1 subset — it does not infer sheet metal\n * from arbitrary solids.\n *\n * **Recommended authoring order:**\n * 1. Define the base panel + thickness + bend parameters.\n * 2. Chain `.flange()` calls for each edge. Validate with `.folded()` and\n * `.flatPattern()` before adding cutouts.\n * 3. Add panel cutouts, then flange cutouts one region at a time.\n * 4. Validate after each new cutout region.\n *\n * **v1 limitations:** one base panel, up to four 90° edge flanges, constant\n * thickness, explicit K-factor, rectangular corner reliefs, planar cutouts\n * only. No hems, jogs, lofted bends, non-90° flanges, or bend-region cutouts.\n *\n * **Example**\n *\n * ```ts\n * const 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 *\n * const folded = cover.folded();\n * const flat = cover.flatPattern();\n * ```\n *\n * @param options - Panel geometry, thickness, bend radius, K-factor, and optional corner relief.\n * @returns A `SheetMetalPart` builder. Call `.folded()` or `.flatPattern()` to produce geometry.\n * @category Sheet Metal\n */\ndeclare function sheetMetal(options: SheetMetalOptions): SheetMetalPart;\ninterface FingerJointOptions {\n /** Explicit finger count (must be odd, >= 3). Default: auto from length/thickness. */\n fingers?: number;\n /** Explicit finger width. Default: auto. */\n fingerWidth?: number;\n /** Extra clearance per side (mm). Default: 0. */\n clearance?: number;\n /** Laser kerf (mm). Default: 0. */\n kerf?: number;\n /** Whether edge starts with full finger or half. Default: \'full\'. */\n endStyle?: "full" | "half";\n}\ninterface FingerJointResult {\n /** Even-position finger rects (tabs for side A, slots for side B). */\n tabProfile: Sketch;\n /** Odd-position finger rects (tabs for side B, slots for side A). */\n matingProfile: Sketch;\n /** Legacy: full rectangle minus odd slot cuts. */\n slotProfile: Sketch;\n}\ninterface TabSlotOptions {\n /** Number of tabs. Default: auto (length / (4 * thickness)). */\n tabCount?: number;\n /** Tab width. Default: 2 * thickness. */\n tabWidth?: number;\n /** Extra clearance per side (mm). Default: 0. */\n clearance?: number;\n /** Laser kerf (mm). Default: 0. */\n kerf?: number;\n /** Distance from panel edges to first/last tab center. Default: thickness. */\n inset?: number;\n}\ninterface TabSlotResult {\n tabs: Sketch;\n slots: Sketch;\n}\ninterface LivingHingeOptions {\n /** Slit pattern style. Default: \'straight\'. */\n pattern?: "straight" | "serpentine";\n /** Explicit slit width (beyond kerf). Default: 0. */\n slitWidth?: number;\n /** Distance between slit rows. Default: 2 * thickness. */\n rowSpacing?: number;\n /** Length of each slit. Default: 0.7 * (length - 2 * landWidth). */\n slitLength?: number;\n /** Uncut material between slit ends and row edges. Default: 2 * thickness. */\n landWidth?: number;\n /** Target bend radius - auto-computes row spacing. Overrides rowSpacing. */\n bendRadius?: number;\n /** Material thickness (needed for bend radius calc). Default: 3. */\n thickness?: number;\n}\ninterface SnapFitOptions {\n /** Tab beam length. Default: 4 * thickness. */\n tabLength?: number;\n /** Tab beam width. Default: thickness. */\n tabWidth?: number;\n /** How much the barb protrudes beyond the beam. Default: 0.3 * thickness. */\n overhang?: number;\n /** Slot clearance per side. Default: 0.1. */\n clearance?: number;\n /** Laser kerf. Default: 0. */\n kerf?: number;\n /** Barb style. Default: \'barb\'. */\n style?: "arrow" | "barb";\n}\ninterface SnapFitResult {\n tab: Sketch;\n slot: Sketch;\n}\n/**\n * Apply kerf compensation to a complete part outline (outer boundary + holes).\n *\n * Offsets inward by half-kerf: the outer boundary shrinks and inner holes grow.\n * This is correct because the laser beam removes material on both sides of the\n * cut line.\n */\ndeclare function kerfCompensateOutline(sketch: Sketch, kerf: number): Sketch;\n/**\n * Apply kerf compensation to joint protrusions (tabs, fingers).\n *\n * These grow by half-kerf so they are slightly oversized and fit tightly in\n * their mating slots after the laser removes material.\n */\ndeclare function kerfCompensateTabs(sketch: Sketch, kerf: number): Sketch;\n/**\n * Apply kerf compensation to joint cutouts (slots, holes that receive tabs).\n *\n * These grow by half-kerf so tabs can fit into them after the laser removes\n * material from both sides of the slot walls.\n */\ndeclare function kerfCompensateSlots(sketch: Sketch, kerf: number): Sketch;\ninterface PartJoints {\n /** Geometry to ADD to the base profile (tabs, fingers protruding from edges). */\n additions?: Sketch[];\n /** Geometry to SUBTRACT from the base profile (slots, holes for mating tabs). */\n subtractions?: Sketch[];\n}\n/**\n * Build a kerf-compensated part profile.\n *\n * 1. Start with the base profile.\n * 2. Kerf-compensate each tab addition (grow by kerf/2), then union with base.\n * 3. Kerf-compensate each slot subtraction (grow by kerf/2), then subtract from base.\n * 4. Kerf-compensate the resulting outline (shrink by kerf/2).\n *\n * Order matters: joints modify geometry BEFORE outline compensation so the\n * final inward offset applies uniformly to the assembled profile.\n */\ndeclare function kerfCompensatePart(baseProfile: Sketch, joints: PartJoints, kerf: number): Sketch;\ninterface MaterialKerfEntry {\n material: string;\n thickness: number;\n kerf: number;\n laserType: string;\n notes?: string;\n}\n/** Common kerf values. Users should always test-cut to verify for their specific setup. */\ndeclare const COMMON_KERFS: MaterialKerfEntry[];\n/**\n * Look up kerf for a material + thickness + laser combo.\n *\n * If `laserType` is omitted, returns the first matching material + thickness\n * entry. Returns `undefined` when no match is found.\n */\ndeclare function lookupKerf(material: string, thickness: number, laserType?: string): number | undefined;\ninterface EdgeInfo {\n name: string;\n start: [\n number,\n number\n ];\n end: [\n number,\n number\n ];\n length: number;\n /** Outward-facing normal. */\n normal: [\n number,\n number\n ];\n}\ninterface FlatPartOptions {\n material?: string;\n qty?: number;\n color?: string;\n}\n/** Tracks a joint connection for assembly preview. */\ninterface JointRecord {\n type: "finger" | "tabSlot" | "snapFit";\n partA: string;\n partB: string;\n edgeA: string;\n edgeB: string;\n /** Fold angle in degrees. Default: 90. */\n foldAngle: number;\n}\ndeclare class FlatPart {\n readonly name: string;\n readonly thickness: number;\n readonly options: FlatPartOptions;\n private _baseProfile;\n private _edges;\n private _additions;\n private _subtractions;\n private _joints;\n private _partNumber;\n constructor(name: string, baseProfile: Sketch, thickness: number, edges: Map<string, EdgeInfo>, options?: FlatPartOptions);\n /** All edges as a read-only map. */\n get edges(): ReadonlyMap<string, EdgeInfo>;\n /** Look up a named edge. Throws if the edge does not exist. */\n edge(name: string): EdgeInfo;\n /** All edge names on this part. */\n edgeNames(): string[];\n /** BOM part number assigned to this flat part. */\n get partNumber(): number;\n /** Update the BOM part number for this flat part. */\n set partNumber(n: number);\n /** Joint records that attach this part to other parts in the kit. */\n get joints(): readonly JointRecord[];\n /** Requested quantity of this part in the kit. Defaults to `1`. */\n get quantity(): number;\n /** Add geometry (e.g. protruding tabs) to the part profile. */\n addGeometry(sketch: Sketch): void;\n /** Subtract geometry (e.g. slot cuts) from the part profile. */\n subtractGeometry(sketch: Sketch): void;\n /** Record a joint connection for assembly preview. */\n addJoint(record: JointRecord): void;\n /** Final 2D profile with joints and optional kerf compensation. */\n profile(kerf?: number): Sketch;\n /** 3D solid — extrude the profile by material thickness. */\n solid(kerf?: number): Shape;\n}\n/**\n * Create a rectangular flat panel with 4 named edges.\n *\n * Profile origin at bottom-left corner.\n * Edges: bottom (y=0), right (x=width), top (y=height), left (x=0).\n * Edge traversal follows CCW winding order.\n */\ndeclare function flatPanel(name: string, width: number, height: number, thickness: number, options?: FlatPartOptions): FlatPart;\n/**\n * Create a flat part from an arbitrary profile with user-named edges.\n *\n * Edge normals are computed automatically (perpendicular to direction, rotated 90deg CW).\n */\ndeclare function flatPart(name: string, profile: Sketch, thickness: number, edges?: Record<string, {\n start: [\n number,\n number\n ];\n end: [\n number,\n number\n ];\n}>, options?: FlatPartOptions): FlatPart;\n/**\n * Connect two parts with finger joints along specified edges.\n *\n * Adds finger geometry to partA\'s edge, cuts matching slots from partB\'s edge.\n * The joint profiles are positioned along each edge using rotation + translation.\n */\ndeclare function fingerJoint(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: FingerJointOptions & {\n foldAngle?: number;\n}): void;\n/**\n * Connect two parts with tab-and-slot joints along specified edges.\n *\n * Adds tab geometry to partA\'s edge, cuts matching slots from partB\'s edge.\n */\ndeclare function tabSlot(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: TabSlotOptions & {\n foldAngle?: number;\n}): void;\ninterface PackedPiece {\n description: string;\n material: string;\n /** Width as placed (may differ from original if rotated). */\n width: number;\n /** Height as placed. */\n height: number;\n /** Original width before placement. */\n origWidth: number;\n /** Original height before placement. */\n origHeight: number;\n x: number;\n y: number;\n rotated: boolean;\n}\ninterface PackedSheet {\n sheetIndex: number;\n material: string;\n pieces: PackedPiece[];\n sheetWidth: number;\n sheetHeight: number;\n usedArea: number;\n /** Ordered guillotine cuts to separate all pieces on this sheet. */\n cuts: GuillotineCut[];\n}\ninterface GuillotineCut {\n /** 1-based sequence number. */\n step: number;\n /** \'V\' = vertical cut (splits left/right), \'H\' = horizontal cut (splits top/bottom). */\n direction: "V" | "H";\n /** Cut line start X (mm from sheet origin). */\n x1: number;\n /** Cut line start Y (mm from sheet origin). */\n y1: number;\n /** Cut line end X (mm from sheet origin). */\n x2: number;\n /** Cut line end Y (mm from sheet origin). */\n y2: number;\n /** Cut length in mm. */\n lengthMm: number;\n}\ninterface CuttingLayoutResult {\n sheets: PackedSheet[];\n totalPieces: number;\n totalSheets: number;\n totalUsedArea: number;\n totalSheetArea: number;\n wastePercent: number;\n kerf: number;\n /** Total length of all cuts across all sheets (mm). */\n totalCutLength: number;\n}\ninterface AssemblyPreviewOptions {\n /** Kerf compensation passed to each part\'s solid(). Default: 0 */\n kerf?: number;\n /** Fold amount: 0 = flat layout, 1 = fully assembled. Default: 1 */\n fold?: number;\n /** Explode distance: 0 = assembled, >0 = parts spread outward. Default: 0 */\n explode?: number;\n}\ninterface AssemblyPreviewResult {\n /** All part shapes grouped for display. */\n shapes: ShapeGroup;\n /** Individual transformed shapes keyed by part name. */\n partShapes: Map<string, Shape>;\n}\n/**\n * Generate a 3D assembly preview from flat parts and their joint records.\n *\n * The preview can fold joints partially or fully and optionally apply exploded spacing\n * so part relationships are easier to inspect visually.\n *\n * @param parts - Flat parts to assemble.\n * @param joints - Joint definitions connecting the parts.\n * @param options - Fold, explode, and kerf preview options.\n * @returns Preview shapes plus a per-part shape map.\n */\ndeclare function assemblyPreview(parts: FlatPart[], joints: JointRecord[], options?: AssemblyPreviewOptions): AssemblyPreviewResult;\ninterface AssemblyStep {\n /** 1-based step number. */\n stepNumber: number;\n /** Human-readable instruction. */\n description: string;\n /** The part being added in this step. */\n partName: string;\n /** Part number (for cross-ref with cut sheets). */\n partNumber: number;\n /** Which existing part it connects to. */\n connectsTo: string;\n /** Joint type used. */\n jointType: "finger" | "tabSlot" | "snapFit";\n /** The edge on the new part. */\n newPartEdge: string;\n /** The edge on the existing part. */\n existingPartEdge: string;\n /** Fold angle in degrees. */\n foldAngle: number;\n /** Part names in the assembly so far (after this step). */\n assembledParts: string[];\n}\ninterface AssemblyInstructionsOptions {\n /** Part to start from. Default: part with most joint connections. */\n rootPart?: string;\n}\ninterface AssemblyInstructionsResult {\n steps: AssemblyStep[];\n /** Total number of parts in the assembly. */\n totalParts: number;\n /** Parts not connected to the joint graph (orphans). */\n orphanParts: string[];\n}\n/**\n * Generate step-by-step assembly instructions from flat parts and joints.\n *\n * Algorithm:\n * 1. Build adjacency graph from joints\n * 2. Pick root part (most connections, or user-specified)\n * 3. BFS from root, creating one step per part addition\n * 4. Each step describes: which part to add, where it connects, how to orient it\n *\n * Heuristics for step ordering:\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\n * connections to already-assembled parts (structurally stable)\n */\ndeclare function assemblyInstructions(parts: FlatPart[], joints: JointRecord[], options?: AssemblyInstructionsOptions): AssemblyInstructionsResult;\n/**\n * Format assembly instructions as a human-readable text document.\n *\n * Includes a "Step 0" preamble identifying the base part, followed by\n * numbered steps, and a note about any orphan parts.\n */\ndeclare function formatInstructions(result: AssemblyInstructionsResult): string;\ninterface LaserKitOptions {\n /** Default material label for parts that don\'t specify one. */\n material?: string;\n /** Stock sheet width in mm (default 600). */\n sheetWidth?: number;\n /** Stock sheet height in mm (default 400). */\n sheetHeight?: number;\n /** Laser kerf in mm (default 0.2). */\n kerf?: number;\n}\ninterface LaserKitBomEntry {\n partNumber: number;\n name: string;\n quantity: number;\n material: string;\n widthMm: number;\n heightMm: number;\n}\ndeclare class LaserKit {\n private _parts;\n private _nextPartNumber;\n private _options;\n constructor(options?: LaserKitOptions);\n /** Laser kerf in mm. */\n get kerf(): number;\n /** All registered parts (flat, in insertion order). */\n get parts(): readonly FlatPart[];\n /** Default material label. */\n get material(): string;\n /** Stock sheet width in mm. */\n get sheetWidth(): number;\n /** Stock sheet height in mm. */\n get sheetHeight(): number;\n /**\n * Register a flat part with this kit.\n * Assigns a sequential part number and records the quantity.\n */\n addPart(part: FlatPart, overrides?: {\n qty?: number;\n }): this;\n /** Generate nested cut sheets using guillotine bin-packing. */\n cutSheets(): CuttingLayoutResult;\n /** Bill of materials listing every part with dimensions. */\n bom(): LaserKitBomEntry[];\n /** Individual SVG string for each part profile, keyed by part name. */\n partSvgs(): Map<string, string>;\n /** Combined inventory SVG showing all parts in a labeled grid. */\n inventorySvg(): string;\n /** Collect deduplicated joints from all registered parts. */\n private _collectJoints;\n /** 3D fold-up preview of the assembled kit. */\n assemblyPreview(options?: Omit<AssemblyPreviewOptions, "kerf">): AssemblyPreviewResult;\n /** Step-by-step assembly instructions. */\n assemblyInstructions(options?: AssemblyInstructionsOptions): AssemblyInstructionsResult;\n /** Human-readable assembly instructions text. */\n formatInstructions(options?: AssemblyInstructionsOptions): string;\n}\n/** Top-level factory for creating a LaserKit container. */\ndeclare function laserKit(options?: LaserKitOptions): LaserKit;\n/**\n * Import a module with optional ForgeCAD parameter overrides. Returns the module\'s exports.\n *\n * When importing a `.forge.js` file, the return value is what the script returns. If the script\n * returns a metadata object (e.g. `{ shape: myShape, bolts: {...} }`), the caller receives the\n * full object — renderable values and metadata together.\n *\n * **Parameter scoping:** Parameters declared in required files are automatically namespaced with\n * a `"filename#N / "` prefix (e.g. `"bracket.forge.js#1 / Width"`). This prevents collisions\n * 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).\n * 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\n * const wall = param("Wall", 3);\n * const baseH = param("Base Height", 20);\n *\n * const mount = require(\'./motor-mount.forge.js\', { Wall: wall });\n * const 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\n * return { shape: mount, bolts: { dia: 5.3, pos: holePositions } };\n *\n * // base-body.forge.js\n * const mount = require(\'./motor-mount.forge.js\');\n * mount.bolts.pos // access the metadata\n * mount.shape // access the geometry\n * ```\n *\n * @concept import\n */\ndeclare function require$1(path: string, paramOverrides?: Record<string, number | string>): any;\n/** Parse an SVG file and return it as a Sketch with options for region filtering, scaling, and simplification. @concept import */\ndeclare function importSvgSketch(fileName: string, options?: SvgImportOptions): Sketch;\n/** Import an external mesh file (STL, OBJ, 3MF) as a Shape. @concept import */\ndeclare function importMesh(fileName: string, options?: {\n scale?: number;\n center?: boolean;\n}): Shape;\n/** Import a STEP file (.step, .stp) as an exact OCCT-backed Shape. Preserves NURBS curves, B-spline surfaces, and exact topology. Requires `setActiveBackend(\'occt\')`. @concept import */\ndeclare function importStep(fileName: string): Shape;\n/**\n * Highlight any geometry for visual debugging in the viewport.\n *\n * Supported inputs:\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` — 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 * @concept visual\n */\ndeclare function highlight(entityId: string, opts?: HighlightOptions): void;\ndeclare function highlight(point: [\n number,\n number,\n number\n], opts?: HighlightOptions): void;\ndeclare function highlight(edge: [\n [\n number,\n number,\n number\n ],\n [\n number,\n number,\n number\n ]\n], opts?: HighlightOptions): void;\ndeclare function highlight(plane: {\n normal: [\n number,\n number,\n number\n ];\n offset: number;\n}, opts?: HighlightOptions): void;\ndeclare function highlight(plane: {\n normal: [\n number,\n number,\n number\n ];\n point: [\n number,\n number,\n number\n ];\n}, opts?: HighlightOptions): void;\ndeclare function highlight(shape: Shape, opts?: HighlightOptions): void;\ndeclare function highlight(face: FaceRef, opts?: HighlightOptions): void;\ndeclare function highlight(edge: EdgeRef, opts?: HighlightOptions): void;\n/** All library parts. Access via `lib.xxx()` in scripts. */\ndeclare const lib: typeof partLibrary;\n\n/** Route step factories. Access via `route.line()`, `route.fillet()`, etc. */\ndeclare const route: typeof routeStepFactories;\n\n/** Connector factory. Create attachment points: `connector({...})`, `connector.male(type, {...})`, etc. */\ndeclare const connector: typeof connectorFactory;\n';
975
+ const FORGE_TYPES = '// AUTO-GENERATED — do not edit by hand.\n// Regenerate: npm run gen:types (source: src/forge/forge-public-api.ts)\n// External type stubs (opaque — not user-facing)\ntype opentype$1 = unknown;\ntype Manifold = unknown;\ntype ManifoldToplevel = unknown;\n\n// Generated by dts-bundle-generator v9.5.1\n\ntype Vec3 = [\n number,\n number,\n number\n];\ntype Mat4 = [\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number\n];\ntype TransformInput = Transform | Mat4;\ntype RotateAroundToMode = "plane" | "line";\ninterface RotateAroundToOptions {\n mode?: RotateAroundToMode;\n}\ndeclare class Transform {\n private readonly m;\n private constructor();\n /** Return the identity transform. */\n static identity(): Transform;\n /** Wrap an existing `Transform` or raw 4x4 matrix as a `Transform`. */\n static from(input: TransformInput): Transform;\n /** Create a translation transform. */\n static translation(x: number, y: number, z: number): Transform;\n /** Create a uniform or per-axis scale transform. */\n static scale(v: number | Vec3): Transform;\n /** Create a rotation around an arbitrary axis, optionally about a pivot. */\n static rotationAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform;\n /** Solve the rotation needed to move one point onto a target line or plane. */\n static rotateAroundTo(axis: Vec3, pivot: Vec3, movingPoint: Vec3, targetPoint: Vec3, options?: RotateAroundToOptions): Transform;\n /** Compose transforms in chain order: `a.mul(b)` applies `a`, then `b`. */\n mul(other: TransformInput): Transform;\n /** Translate after the current transform. */\n translate(x: number, y: number, z: number): Transform;\n /** Rotate after the current transform. */\n rotateAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform;\n /** Scale after the current transform. */\n scale(v: number | Vec3): Transform;\n /** Return the inverse transform. */\n inverse(): Transform;\n /** Transform a point using homogeneous coordinates. */\n point(p: Vec3): Vec3;\n /** Transform a direction vector without translation. */\n vector(v: Vec3): Vec3;\n /** Return the transform as a raw 4x4 matrix array. */\n toArray(): Mat4;\n}\n/**\n * Compose transforms in chain order.\n * Equivalent to Transform.identity().mul(a).mul(b).mul(c)...\n */\ndeclare function composeChain(...steps: TransformInput[]): Transform;\ntype ConnectorGender = "male" | "female" | "neutral";\ninterface PortDef {\n origin: Vec3;\n axis: Vec3;\n up: Vec3;\n extent?: number;\n kind?: JointType;\n min?: number;\n max?: number;\n connectorType?: string;\n gender?: ConnectorGender;\n measurements?: Record<string, number | string>;\n}\ntype PortAlign = "middle" | "start" | "end";\ninterface PortInput {\n origin?: [\n number,\n number,\n number\n ];\n axis?: [\n number,\n number,\n number\n ];\n start?: [\n number,\n number,\n number\n ];\n end?: [\n number,\n number,\n number\n ];\n up?: [\n number,\n number,\n number\n ];\n kind?: JointType;\n min?: number;\n max?: number;\n}\ntype PortMap = Record<string, PortDef>;\ntype ConnectorGender$1 = "male" | "female" | "neutral";\ninterface ConnectorInput extends PortInput {\n connectorType?: string;\n gender?: ConnectorGender$1;\n measurements?: Record<string, number | string>;\n}\ntype ConnectorDef = PortDef;\ntype ConnectorMap = PortMap;\ninterface MatchToOptions {\n force?: boolean;\n angle?: number;\n distance?: number;\n}\n/**\n * Create a connector — a named attachment point on a shape.\n *\n * Overloads:\n * - `connector(geometry)` — bare connector (position + orientation only)\n * - `connector(type, geometry)` — typed connector for compatibility matching\n * - `connector(type, geometry, measurements)` — typed with measurement metadata\n */\ndeclare function connectorFactory(typeOrInput: string | PortInput, inputOrMeasurements?: PortInput | Record<string, number | string>, measurements?: Record<string, number | string>): ConnectorInput;\ndeclare namespace connectorFactory {\n var male: (typeOrInput: string | PortInput, inputOrMeasurements?: PortInput | Record<string, number | string>, measurements?: Record<string, number | string>) => ConnectorInput;\n var female: (typeOrInput: string | PortInput, inputOrMeasurements?: PortInput | Record<string, number | string>, measurements?: Record<string, number | string>) => ConnectorInput;\n var neutral: (typeOrInput: string | PortInput, inputOrMeasurements?: PortInput | Record<string, number | string>, measurements?: Record<string, number | string>) => ConnectorInput;\n}\ndeclare const ANCHOR3D_NAMES: readonly [\n "center",\n "front",\n "back",\n "left",\n "right",\n "top",\n "bottom",\n "front-left",\n "front-right",\n "back-left",\n "back-right",\n "top-front",\n "top-back",\n "top-left",\n "top-right",\n "bottom-front",\n "bottom-back",\n "bottom-left",\n "bottom-right",\n "top-front-left",\n "top-front-right",\n "top-back-left",\n "top-back-right",\n "bottom-front-left",\n "bottom-front-right",\n "bottom-back-left",\n "bottom-back-right"\n];\ntype Anchor3D = (typeof ANCHOR3D_NAMES)[number];\ntype SketchFace3D = "front" | "back" | "left" | "right" | "top" | "bottom";\ninterface ShapeQueryOwner {\n id: string;\n operation: string;\n}\ntype TopologyRewriteQueryOutcome = "preserved" | "split" | "merged";\ninterface CanonicalFaceQueryRef {\n kind: "canonical-face";\n face: SketchFace3D;\n owner?: ShapeQueryOwner;\n}\ninterface TrackedFaceQueryRef {\n kind: "tracked-face";\n faceName: string;\n owner?: ShapeQueryOwner;\n}\ninterface DirectFaceQueryRef {\n kind: "face-ref";\n faceName?: string;\n owner?: ShapeQueryOwner;\n}\ninterface PropagatedFaceQueryRef {\n kind: "propagated-face";\n rewriteId: string;\n outcome: TopologyRewriteQueryOutcome;\n source: FaceQueryRef;\n owner?: ShapeQueryOwner;\n}\ninterface CreatedFaceQueryRef {\n kind: "created-face";\n rewriteId: string;\n operation: string;\n slot: string;\n owner?: ShapeQueryOwner;\n}\ntype FaceQueryRef = CanonicalFaceQueryRef | TrackedFaceQueryRef | DirectFaceQueryRef | PropagatedFaceQueryRef | CreatedFaceQueryRef;\ntype EdgeQuerySelector = "edge" | "start" | "end" | "midpoint";\ninterface TrackedEdgeQueryRef {\n kind: "tracked-edge";\n edgeName: string;\n selector: EdgeQuerySelector;\n owner?: ShapeQueryOwner;\n}\ninterface DirectEdgeQueryRef {\n kind: "edge-ref";\n edgeName?: string;\n selector: EdgeQuerySelector;\n owner?: ShapeQueryOwner;\n}\ninterface PropagatedEdgeQueryRef {\n kind: "propagated-edge";\n rewriteId: string;\n outcome: TopologyRewriteQueryOutcome;\n source: EdgeQueryRef;\n selector: EdgeQuerySelector;\n owner?: ShapeQueryOwner;\n}\ninterface CreatedEdgeQueryRef {\n kind: "created-edge";\n rewriteId: string;\n operation: string;\n slot: string;\n selector: EdgeQuerySelector;\n owner?: ShapeQueryOwner;\n}\ntype EdgeQueryRef = TrackedEdgeQueryRef | DirectEdgeQueryRef | PropagatedEdgeQueryRef | CreatedEdgeQueryRef;\ntype FaceDescendantSemantic = "face" | "region" | "set";\ninterface FaceDescendantMetadata {\n kind: "single" | "face-set";\n semantic: FaceDescendantSemantic;\n memberCount: number;\n memberNames: string[];\n coplanar: boolean;\n}\ninterface ConstraintTypeMap {\n /**\n * Forces two points to occupy the same position.\n *\n * This is the most fundamental connectivity constraint — use it to join\n * line endpoints, close a polygon, or snap a point to another point.\n * Contributes **2 equations** (one per axis).\n */\n coincident: {\n a: PointId;\n b: PointId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a line to be horizontal (parallel to the X axis).\n *\n * Both endpoints are moved to their average Y coordinate so the line\n * remains centered in place. Contributes **1 equation**: `b.y − a.y = 0`.\n */\n horizontal: {\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a line to be vertical (parallel to the Y axis).\n *\n * Both endpoints are moved to their average X coordinate so the line\n * remains centered in place. Contributes **1 equation**: `b.x − a.x = 0`.\n */\n vertical: {\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to be parallel.\n *\n * The direction of `b` is rotated to match `a`\'s direction (either\n * co-directional or anti-parallel — whichever is closer to the current\n * orientation). Line `a` is treated as the reference; only `b` is moved.\n * Contributes **1 equation**: `cross(unit_a, unit_b) = 0`.\n */\n parallel: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to be perpendicular (90° apart).\n *\n * The direction of `b` is rotated to be ±90° from `a`, choosing the\n * sign closest to the current orientation. Line `a` is the reference;\n * only `b` is moved. Contributes **1 equation**: `dot(unit_a, unit_b) = 0`.\n */\n perpendicular: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains tangency between a line and a circle, or between two circles.\n *\n * **Line–circle** (`line` + `circle`): the perpendicular distance from the\n * circle\'s center to the infinite line equals the circle\'s radius.\n *\n * **Circle–circle** (`a` + `b`): the two circles are externally tangent —\n * the distance between centers equals the sum of their radii.\n *\n * Exactly one mode must be active (provide either `line`+`circle` or `a`+`b`).\n * Contributes **1 equation**.\n */\n tangent: {\n line?: LineId;\n circle?: CircleId;\n a?: CircleId;\n b?: CircleId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to have the same length.\n *\n * Line `a`\'s length is used as the target; `b`\'s endpoints are scaled\n * symmetrically along `b`\'s current direction to match it.\n * Contributes **1 equation**: `|b| − |a| = 0`.\n */\n equal: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces points `a` and `b` to be mirror images of each other across\n * the infinite line through `axis`.\n *\n * When neither point is fixed, `b` is moved to the reflection of `a`.\n * When `b` is fixed, `a` is moved instead. Contributes **2 equations**\n * (one per axis): `b − reflect(a, axis) = [0, 0]`.\n */\n symmetric: {\n a: PointId;\n b: PointId;\n axis: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces two circles to share the same center point.\n *\n * The centers are merged to their average position (or snapped to the fixed\n * one if either is fixed). Contributes **2 equations**\n * (one per axis): `center_b − center_a = [0, 0]`.\n */\n concentric: {\n a: CircleId;\n b: CircleId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a point to lie on the infinite line passing through a line segment.\n *\n * The point is projected onto the line\'s infinite extension (not clamped to\n * the segment). Contributes **1 equation**: signed distance from the point\n * to the line = 0.\n */\n collinear: {\n point: PointId;\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Pins a point to an absolute position `(x, y)` in sketch space.\n *\n * Rust treats the point as externally pinned geometry. `equations: 0`\n * is intentional because the point\'s mobility is removed through the\n * `fixed` flag rather than by adding a residual row.\n */\n fixed: {\n point: PointId;\n x: number;\n y: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a point to sit at the exact midpoint of a line segment.\n *\n * Rust enforces this as two scalar equations, one per axis:\n * `point - (a + b) / 2 = [0, 0]`.\n */\n midpoint: {\n point: PointId;\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a point to lie exactly on the circumference of a circle.\n *\n * The point is moved radially so its distance from the center equals the\n * radius. Contributes **1 equation**: `|point − center| − radius = 0`.\n */\n pointOnCircle: {\n point: PointId;\n circle: CircleId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces a point to lie on a **bounded** line segment — not merely the\n * infinite extension of the line.\n *\n * The point is projected onto the segment and the parameter `t` is clamped\n * to `[0, 1]`. When `t` is already inside that range the behaviour is\n * identical to `collinear`. When the point would project outside the\n * segment it is snapped to the nearest endpoint instead.\n *\n * Contributes **1 equation** (like `collinear`): the point can still slide\n * along the segment, giving it one remaining degree of freedom.\n */\n pointOnLine: {\n point: PointId;\n line: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the Euclidean distance between two points to `value`.\n *\n * Points are moved symmetrically along the current direction vector so the\n * center of the pair stays fixed. Contributes **1 equation**:\n * `|b − a| − value = 0`.\n */\n distance: {\n a: PointId;\n b: PointId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the length of a line segment to `value`.\n *\n * Endpoints are scaled symmetrically about the line\'s midpoint while\n * preserving its direction. Contributes **1 equation**:\n * `|b − a| − value = 0`.\n */\n length: {\n line: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the angle from line `a` to line `b` to `value` degrees (CCW).\n * Line `a` is the reference; only `b` is rotated. Contributes **1 equation**.\n */\n angle: {\n a: LineId;\n b: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the radius of a circle to `value`.\n *\n * Has no effect if the circle\'s `fixedRadius` flag is set.\n * Contributes **1 equation**: `radius − value = 0`.\n */\n radius: {\n circle: CircleId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the diameter of a circle to `value` (i.e. `radius = value / 2`).\n *\n * Has no effect if the circle\'s `fixedRadius` flag is set.\n * Contributes **1 equation**: `radius − value / 2 = 0`.\n */\n diameter: {\n circle: CircleId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the signed horizontal distance from point `a` to point `b` to `value`.\n *\n * The constraint is directional: `b.x − a.x = value`. A positive value places\n * `b` to the right of `a`; negative places it to the left.\n * Contributes **1 equation**.\n */\n hDistance: {\n a: PointId;\n b: PointId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the signed vertical distance from point `a` to point `b` to `value`.\n *\n * The constraint is directional: `b.y − a.y = value`. A positive value places\n * `b` above `a`; negative places it below.\n * Contributes **1 equation**.\n */\n vDistance: {\n a: PointId;\n b: PointId;\n value: number;\n };\n}\ninterface HighlightDef {\n /** Entity ID to highlight (edge, point, surface, circle, arc). */\n entityId: string;\n /** Override color (CSS color string). Default: \'#ff00ff\' (magenta). */\n color?: string;\n /** Optional label to display near the entity. */\n label?: string;\n /** When true, animate opacity between 0.5 and 1.0 for attention. */\n pulse?: boolean;\n}\ninterface HighlightOptions {\n color?: string;\n label?: string;\n pulse?: boolean;\n /** Size hint for points (radius in mm) or planes (disc radius in mm). */\n size?: number;\n}\ndeclare const __brand: unique symbol;\ntype Brand<T, B extends string> = T & {\n readonly [__brand]: B;\n};\ntype PointId = Brand<string, "PointId">;\ntype LineId = Brand<string, "LineId">;\ntype CircleId = Brand<string, "CircleId">;\ntype ArcId = Brand<string, "ArcId">;\ntype BezierId = Brand<string, "BezierId">;\ntype ShapeId = Brand<string, "ShapeId">;\ntype GroupId = Brand<string, "GroupId">;\ninterface SketchArc {\n id: ArcId;\n /** Center point of the arc\'s circle. */\n center: PointId;\n /** Point on the arc where it begins. Must lie on the circle. */\n start: PointId;\n /** Point on the arc where it ends. Must lie on the circle. */\n end: PointId;\n /** Current solved radius — kept consistent with |center–start| and |center–end| by the solver. */\n radius: number;\n /** True → arc sweeps clockwise from start to end; false → counter-clockwise. */\n clockwise: boolean;\n /** When true, the solver treats radius as a constant (no variable, no perturbation). */\n fixedRadius: boolean;\n construction: boolean;\n /** Optional human-readable name for display. */\n name?: string;\n}\ninterface SketchShape {\n id: ShapeId;\n /** Ordered list of line IDs forming a closed polygon. */\n lines: LineId[];\n}\ninterface SketchGroupLocalPoint {\n id: PointId;\n lx: number;\n ly: number;\n}\ninterface SketchGroup {\n id: GroupId;\n /** World position of the group\'s local origin. */\n x: number;\n y: number;\n /** Rotation angle (radians) of the group frame. */\n theta: number;\n /** When true, all 3 DOF are frozen. */\n fixed: boolean;\n /** When true, θ is frozen — only translation DOF remain. */\n fixedRotation: boolean;\n /** Points in local coordinates. */\n points: SketchGroupLocalPoint[];\n /** Lines connecting local points (using their global IDs). */\n lines: {\n id: LineId;\n a: PointId;\n b: PointId;\n }[];\n}\ninterface SketchPoint {\n id: PointId;\n x: number;\n y: number;\n fixed: boolean;\n}\ninterface SketchLine {\n id: LineId;\n a: PointId;\n b: PointId;\n construction: boolean;\n /** Optional human-readable name for display (e.g. "top", "bottom"). */\n name?: string;\n}\ninterface SketchCircle {\n id: CircleId;\n center: PointId;\n radius: number;\n construction: boolean;\n fixedRadius: boolean;\n segments: number;\n /** Optional human-readable name for display. */\n name?: string;\n}\ninterface SketchBezier {\n id: BezierId;\n /** First control point (start of curve). */\n p0: PointId;\n /** Second control point (controls tangent at start). */\n p1: PointId;\n /** Third control point (controls tangent at end). */\n p2: PointId;\n /** Fourth control point (end of curve). */\n p3: PointId;\n construction: boolean;\n /** Optional human-readable name for display. */\n name?: string;\n}\ntype ProfileSegment = {\n kind: "line";\n line: LineId;\n label?: string;\n} | {\n kind: "arc";\n arc: ArcId;\n label?: string;\n} | {\n kind: "bezier";\n bezier: BezierId;\n label?: string;\n};\ntype SketchLoop = {\n type: "poly";\n points: PointId[];\n} | {\n type: "circle";\n circle: CircleId;\n}\n/** Mixed profile of line and arc segments forming a closed loop. */\n | {\n type: "profile";\n segments: ProfileSegment[];\n};\ntype AnnotationElement = \n/** Symbol placed at a specific position (geometric constraints like parallel, equal, fixed). */\n{\n kind: "symbol";\n position: [\n number,\n number\n ];\n symbol: ConstraintSymbol;\n rotation?: number;\n}\n/** Dimension line with extension lines (length, distance). */\n | {\n kind: "dimension";\n from: [\n number,\n number\n ];\n to: [\n number,\n number\n ];\n offset: number;\n value: string;\n}\n/** Angle arc between two directions (angle, absoluteAngle). */\n | {\n kind: "angle-arc";\n center: [\n number,\n number\n ];\n startAngle: number;\n endAngle: number;\n radius: number;\n value: string;\n}\n/** Fallback text label for constraints not yet migrated to annotations. */\n | {\n kind: "text";\n position: [\n number,\n number\n ];\n text: string;\n};\ntype ConstraintSymbol = "parallel" | "equal" | "perpendicular" | "horizontal" | "vertical" | "fixed" | "midpoint" | "coincident" | "collinear" | "tangent" | "concentric" | "ccw" | "symmetric";\ninterface ConstraintDisplay {\n id: string;\n type: string;\n label: string;\n /** Legacy position — used as fallback when annotations are not defined. */\n position: [\n number,\n number\n ];\n value?: number;\n isDimension: boolean;\n /** True when the solver failed to satisfy this constraint (genuinely conflicting geometry). */\n isConflicting: boolean;\n /** True when this constraint is mathematically redundant — it duplicates an equation already\n * provided by another constraint, making the DOF count negative even though the solver converges. */\n isRedundant: boolean;\n /** For rejected constraints: why the builder rejected it (maxError, constraint params, blame). */\n rejectionReason?: string;\n /** Entity IDs referenced by this constraint (points, lines, circles, etc.). */\n entityIds: string[];\n /** Per-equation residual error for this constraint (how far off it is). */\n residual: number;\n /** Annotation elements for this constraint. Empty array → use legacy text fallback. */\n annotations: AnnotationElement[];\n}\ninterface SurfaceDisplay {\n /** Zero-based index, largest-first by area. */\n index: number;\n /** Region area in mm². */\n area: number;\n /** Centroid of the region polygon. */\n centroid: [\n number,\n number\n ];\n /** Axis-aligned bounding box. */\n bounds: {\n min: [\n number,\n number\n ];\n max: [\n number,\n number\n ];\n };\n /** A point guaranteed to be inside the region — usable as seed for detectArrangementRegion(). */\n seed: [\n number,\n number\n ];\n /** Polygon vertices (CCW winding) for rendering the region fill. */\n polygon: [\n number,\n number\n ][];\n}\ninterface SketchConstraintMeta {\n status: "under" | "fully" | "over" | "over-redundant";\n /** Net degrees of freedom: positive = under-constrained, 0 = fully, negative = over-constrained. */\n dof: number;\n maxError: number;\n constraints: ConstraintDisplay[];\n rejected: ConstraintDisplay[];\n /** Detected surfaces from line arrangement (DCEL face detection). Empty if no closed regions. */\n surfaces: SurfaceDisplay[];\n construction: {\n lines: {\n id: string;\n a: [\n number,\n number\n ];\n b: [\n number,\n number\n ];\n }[];\n circles: {\n id: string;\n center: [\n number,\n number\n ];\n radius: number;\n }[];\n arcs: {\n id: string;\n center: [\n number,\n number\n ];\n start: [\n number,\n number\n ];\n end: [\n number,\n number\n ];\n radius: number;\n clockwise: boolean;\n }[];\n };\n /** Non-construction geometry edges rendered as solid wireframe overlay. */\n edges: {\n lines: {\n id: string;\n name?: string;\n a: [\n number,\n number\n ];\n b: [\n number,\n number\n ];\n }[];\n circles: {\n id: string;\n name?: string;\n center: [\n number,\n number\n ];\n radius: number;\n }[];\n arcs: {\n id: string;\n name?: string;\n center: [\n number,\n number\n ];\n start: [\n number,\n number\n ];\n end: [\n number,\n number\n ];\n radius: number;\n clockwise: boolean;\n }[];\n beziers: {\n id: string;\n name?: string;\n points: [\n number,\n number\n ][];\n }[];\n points: {\n id: string;\n pos: [\n number,\n number\n ];\n }[];\n };\n /** True when the solver hit its time budget before fully converging. */\n timedOut?: boolean;\n /** Opt-in solver debug artifacts returned by the Rust planner. */\n debug?: SolverDebugArtifacts;\n /** Programmatic debug highlights from user code. */\n highlights?: HighlightDef[];\n}\ninterface ConstraintDefinition {\n points: SketchPoint[];\n lines: SketchLine[];\n circles: SketchCircle[];\n arcs: SketchArc[];\n beziers: SketchBezier[];\n shapes: SketchShape[];\n /** Rigid-body groups — the solver sees 3 DOF per group instead of 2N per point. */\n groups: SketchGroup[];\n loops: SketchLoop[];\n constraints: SketchConstraint[];\n rejectedConstraints: SketchConstraint[];\n /** Maps rejected constraint ID → human-readable reason. Populated by the builder. */\n rejectionReasons?: Map<string, string>;\n}\ninterface SolveOptions {\n /** Maximum number of LM outer iterations per restart. */\n iterations?: number;\n /** Infinity-norm residual tolerance for declaring convergence. */\n tolerance?: number;\n /** Number of deterministic restart seeds used by the global solver. */\n restarts?: number;\n /** Optional projector iterations used only for initialisation, not as the main solver. */\n warmStartIterations?: number;\n /** Maximum LM step length in scaled variable space. Larger = bolder, smaller = safer. */\n maxScaledStep?: number;\n /** Skip redundancy detection (safe when topology is unchanged and previous DOF >= 0). */\n skipRedundancyCheck?: boolean;\n /** Run the targeted presolve hook for this constraint before the main solve. */\n presolveConstraintId?: string;\n /** When set and the first solve exceeds tolerance*5, retry with this many restarts. */\n fallbackRestarts?: number;\n /** Add constraints progressively with short LM solves, all in one WASM call. */\n progressive?: boolean;\n /** Wall-clock time budget in ms for the entire solve. 0 = no limit. */\n timeBudgetMs?: number;\n /** Capture a readable constructive transcript in `constraintMeta.debug`. */\n debugConstructiveTranscript?: boolean;\n /** Capture SVG snapshots for constructive steps in `constraintMeta.debug`. */\n debugSvgSnapshots?: boolean;\n}\ninterface SolverConstraintResidual {\n id: string;\n residual: number;\n}\ninterface ConstructiveTranscriptStep {\n index: number;\n label: string;\n kind: string;\n summary: string;\n outcome: string;\n localError: number;\n topResiduals: SolverConstraintResidual[];\n branch?: string;\n snapshotLabel?: string;\n}\ninterface SolverSvgSnapshot {\n label: string;\n error: number;\n svg: string;\n}\ninterface SolverDebugArtifacts {\n constructiveTranscript: ConstructiveTranscriptStep[];\n svgSnapshots: SolverSvgSnapshot[];\n}\ninterface ConstraintTypeMap {\n}\ninterface ConstraintBuilderMethods {\n moveTo(x: number, y: number): this;\n lineTo(x: number, y: number): this;\n lineH(dx: number): this;\n lineV(dy: number): this;\n lineAngled(length: number, degrees: number): this;\n arcTo(x: number, y: number, radius: number, clockwise?: boolean): this;\n arcByCenter(centerId: PointId, startId: PointId, endId: PointId, clockwise?: boolean, name?: string): ArcId;\n bezier(p0: any, p1: any, p2: any, p3: any, name?: string): BezierId;\n bezierTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): this;\n blendTo(x: number, y: number, weight?: number): this;\n close(): this;\n addLoopCircle(center: PointId, radius: number, segments?: number): this;\n addLoop(points: any[]): this;\n addProfileLoop(segments: Array<{\n kind: "line";\n line: any;\n } | {\n kind: "arc";\n arc: any;\n } | {\n kind: "bezier";\n bezier: any;\n }>): this;\n horizontal(line: any): this;\n vertical(line: any): this;\n parallel(a: any, b: any): this;\n sameDirection(a: any, b: any): this;\n oppositeDirection(a: any, b: any): this;\n blockRotation(points: any[], axis?: "x" | "y"): this;\n perpendicular(a: any, b: any): this;\n tangent(a: any, b: any): this;\n equal(a: any, b: any): this;\n coincident(a: any, b: any): this;\n concentric(a: any, b: any): this;\n collinear(point: any, line: any): this;\n symmetric(a: any, b: any, axis: any): this;\n fix(point: any, x?: number, y?: number): this;\n midpoint(point: any, line: any): this;\n pointOnCircle(point: any, circle: any): this;\n pointOnLine(point: any, line: any): this;\n distance(a: any, b: any, value: number): this;\n length(line: any, value: number): this;\n angle(a: any, b: any, value: number): this;\n radius(circle: any, value: number): this;\n diameter(circle: any, value: number): this;\n hDistance(a: any, b: any, value: number): this;\n vDistance(a: any, b: any, value: number): this;\n pointLineDistance(point: any, line: any, value: number): this;\n lineDistance(a: any, b: any, value: number): this;\n absoluteAngle(line: any, value: number): this;\n equalRadius(a: any, b: any): this;\n arcLength(arc: any, value: number): this;\n lineTangentArc(line: any, arc: any, atStart: boolean): this;\n arcTangentArc(arcA: any, arcB: any, aAtStart?: boolean, bAtStart?: boolean): this;\n bezierTangentArc(bezier: any, arc: any, atBezierStart: boolean, atArcStart: boolean): this;\n smoothBlend(arc1: any, arc2: any, options?: {\n weight?: number;\n arc1End?: "start" | "end";\n arc2End?: "start" | "end";\n }): BezierId;\n shapeWidth(shape: any, value: number): this;\n shapeHeight(shape: any, value: number): this;\n shapeCentroidX(shape: any, value: number): this;\n shapeCentroidY(shape: any, value: number): this;\n shapeArea(shape: any, value: number): this;\n shapeEqualCentroid(a: any, b: any): this;\n angleBetween(a: any, b: any, value: number): this;\n ccw(...points: any[]): this;\n importPoint(pt: {\n x: number;\n y: number;\n }, fixed?: boolean): PointId;\n importLine(l: {\n start: {\n x: number;\n y: number;\n };\n end: {\n x: number;\n y: number;\n };\n }, fixed?: boolean): LineId;\n importRectangle(r: {\n vertices: [\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n }\n ];\n }, fixed?: boolean): {\n bottom: LineId;\n right: LineId;\n top: LineId;\n left: LineId;\n points: [\n PointId,\n PointId,\n PointId,\n PointId\n ];\n };\n referencePoint(x: number, y: number): PointId;\n referenceLine(x1: number, y1: number, x2: number, y2: number): LineId;\n referenceFrom(source: any, entityId: string): PointId | LineId | null;\n referenceAllFrom(source: any): {\n points: Map<string, PointId>;\n lines: Map<string, LineId>;\n };\n}\ntype SketchConstraint = {\n [K in keyof ConstraintTypeMap]: {\n id: string;\n type: K;\n } & ConstraintTypeMap[K];\n}[keyof ConstraintTypeMap];\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to be parallel **and** separated by a signed\n * perpendicular distance of `value`.\n *\n * The distance is measured from the midpoint of `a` to the midpoint of `b`\n * along `a`\'s left-normal direction. Positive values place `b` to the left\n * of `a` (when facing `a`\'s direction).\n *\n * This constraint combines two equations:\n * 1. `cross(unit_a, unit_b) = 0` — parallelism\n * 2. `perpDist(mid_b, line_a) − value = 0` — offset distance\n *\n * Contributes **2 equations**.\n */\n lineDistance: {\n a: LineId;\n b: LineId;\n value: number;\n };\n}\n/** Exported for backward-compatibility with forge-public-api.ts */\ntype LineDistanceConstraint = {\n id: string;\n type: "lineDistance";\n} & {\n a: LineId;\n b: LineId;\n value: number;\n};\ninterface ConstraintTypeMap {\n /**\n * Sets the angle of a line from the positive X axis to exactly `value` degrees.\n * The direction is enforced as-is (a→b). Contributes **1 equation**:\n * `normalizeAngle(angle − target) = 0`.\n */\n absoluteAngle: {\n line: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces two circles to have the same radius.\n *\n * Rust enforces one scalar equality: `radius_b - radius_a = 0`.\n */\n equalRadius: {\n a: CircleId;\n b: CircleId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the arc length of an arc to `value`.\n *\n * Arc length is defined as `radius × sweep`, where `sweep` is the angle\n * (in radians) from the start point to the end point in the arc\'s direction.\n * A zero-length sweep is treated as a full circle (2π).\n *\n * Rust enforces this as one scalar equation: `radius × sweep - value = 0`.\n */\n arcLength: {\n arc: ArcId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains a line to be tangent to an arc at the arc\'s start or end point.\n *\n * Tangency requires the line\'s direction to be perpendicular to the arc\'s\n * radius at the contact point. Set `atStart: true` to use the arc\'s start\n * point as the tangency contact; `false` uses the end point.\n *\n * Rust enforces tangency as one scalar orthogonality equation between the\n * line direction and the chosen radius direction.\n */\n lineTangentArc: {\n line: LineId;\n arc: ArcId;\n atStart: boolean;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the X coordinate of a polygon\'s arithmetic centroid to `value`.\n *\n * All non-fixed vertices are translated horizontally by the same amount so\n * that `mean(vertices.x) = value`. The shape\'s size, proportions, and Y\n * position are unaffected. Contributes **1 equation**.\n */\n shapeCentroidX: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the Y coordinate of a polygon\'s arithmetic centroid to `value`.\n *\n * All non-fixed vertices are translated vertically by the same amount so\n * that `mean(vertices.y) = value`. The shape\'s size, proportions, and X\n * position are unaffected. Contributes **1 equation**.\n */\n shapeCentroidY: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the axis-aligned bounding-box width of a polygon shape to `value`.\n *\n * All non-fixed vertices are scaled horizontally (`x` only) from the\n * bounding-box center: `pt.x = cx + (pt.x − cx) × (value / width)`.\n * The shape\'s height and Y position are unaffected.\n * Contributes **1 equation**.\n */\n shapeWidth: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the axis-aligned bounding-box height of a polygon shape to `value`.\n *\n * All non-fixed vertices are scaled vertically (`y` only) from the\n * bounding-box center: `pt.y = cy + (pt.y − cy) × (value / height)`.\n * The shape\'s width and X position are unaffected.\n * Contributes **1 equation**.\n */\n shapeHeight: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the enclosed area of a polygon shape to `value`.\n *\n * Area is computed via the shoelace formula on the ordered vertex list.\n * All non-fixed vertices are scaled uniformly from the polygon\'s arithmetic\n * centroid: `scale = sqrt(target / current)`, so the shape\'s proportions\n * and position are preserved. Contributes **1 equation**.\n */\n shapeArea: {\n shape: ShapeId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces two shapes to share the same centroid.\n *\n * Translates the non-fixed vertices of each shape so that their arithmetic\n * centroids coincide at the mean of the two current centroids.\n * Contributes **2 equations**: `cx(a) − cx(b) = 0` and `cy(a) − cy(b) = 0`.\n */\n shapeEqualCentroid: {\n a: ShapeId;\n b: ShapeId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains the signed perpendicular distance from a point to an infinite line.\n *\n * Positive `value` places the point to the **left** of the line\n * (when facing the line\'s direction from `a` to `b`). Negative places it\n * to the right. Zero is equivalent to `collinear`.\n * Contributes **1 equation**: `perpDist(point, line) − value = 0`.\n */\n pointLineDistance: {\n point: PointId;\n line: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Enforces counter-clockwise winding order on a polygon defined by `points`.\n *\n * This resolves the discrete orientation ambiguity that arises when a polygon\'s\n * shape is fully determined but its mirror image also satisfies all constraints\n * (e.g. an equilateral triangle with a fixed vertex and side angle).\n *\n * Rust owns the actual branch handling and one-sided residual logic.\n * `equations: 0` is intentional: this constraint removes the mirror branch\n * without claiming an extra continuous degree of freedom.\n */\n ccw: {\n points: PointId[];\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Sets the unsigned angle between lines `a` and `b` to `value` degrees.\n *\n * Unlike `angle` (which is directional — 90 ≠ −90), this constraint\n * accepts both orientations of `b`: whichever of `+value` or `+value+180°`\n * is closer to the current direction is chosen. Use this when you care\n * about the magnitude of the angle but not the sign (e.g. "these two lines\n * are 60° apart" without specifying which side).\n *\n * Contributes **1 equation**: `sin(angleB − angleA − target) = 0`.\n */\n angleBetween: {\n a: LineId;\n b: LineId;\n value: number;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to point in the **same direction** (co-directional).\n *\n * Unlike `parallel` (which allows either co-directional or anti-parallel),\n * this constraint pins the relative direction: `dot(unit_a, unit_b) > 0`\n * AND `cross(unit_a, unit_b) = 0`.\n *\n * Use this with `lineDistance` when the sign of the distance matters —\n * `sameDirection` guarantees the normals point the same way, so positive\n * distance means the same physical side for both lines.\n *\n * Rust uses one continuous parallelism equation plus orientation-aware\n * branch handling to keep `b` facing the same way as `a`.\n */\n sameDirection: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Forces lines `a` and `b` to point in **opposite directions** (anti-parallel).\n *\n * Unlike `parallel` (which allows either co-directional or anti-parallel),\n * this constraint pins the relative direction: `dot(unit_a, unit_b) < 0`\n * AND `cross(unit_a, unit_b) = 0`.\n *\n * Use this with `lineDistance` when you need two lines facing each other —\n * e.g. the top of one rect facing the bottom of another.\n *\n * Rust uses one continuous parallelism equation plus orientation-aware\n * branch handling to keep `b` facing opposite to `a`.\n */\n oppositeDirection: {\n a: LineId;\n b: LineId;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Prevents 180° rotation of a polygon by ensuring the first edge\n * (p0 → p1) maintains its initial direction sign.\n *\n * For an axis-aligned rectangle [bl, br, tr, tl], this guarantees\n * `br.x > bl.x` — i.e. the "bottom" edge points rightward (+x).\n * Without this, a rect can satisfy CCW winding while being inside-out\n * (negative width AND negative height → positive area → CCW blind spot).\n *\n * Rust treats this as an orientation guard with `equations: 0`, so it\n * preserves the intended branch without claiming an extra continuous DOF.\n *\n * The constraint stores `axis: \'x\' | \'y\'` — which coordinate of the\n * first edge must increase. For rects, this is `\'x\'` (bottom goes right).\n */\n blockRotation: {\n points: PointId[];\n axis: "x" | "y";\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains two arcs to be tangent (G1 smooth) at a shared junction point.\n *\n * The radius vectors of both arcs at the junction must be collinear\n * (cross product of unit radii = 0), ensuring the tangent directions match.\n *\n * Use `coincident` separately to enforce the shared endpoint.\n * Contributes **1 equation**.\n */\n arcTangentArc: {\n arcA: ArcId;\n arcB: ArcId;\n /** Use arc A\'s start (true) or end (false) as the junction. */\n aAtStart: boolean;\n /** Use arc B\'s start (true) or end (false) as the junction. */\n bAtStart: boolean;\n };\n}\ninterface ConstraintTypeMap {\n /**\n * Constrains a cubic Bezier curve to be tangent to an arc at one of their endpoints.\n *\n * The Bezier\'s tangent direction (tangent_control − tangent_base) must be\n * perpendicular to the arc\'s radius at the contact point.\n *\n * The builder resolves the Bezier entity to two point IDs:\n * - at bezier start: tangentBase=P0, tangentControl=P1\n * - at bezier end: tangentBase=P3, tangentControl=P2\n *\n * Contributes **1 equation**.\n */\n bezierTangentArc: {\n /** Point on the Bezier at the tangent end (P0 for start, P3 for end). */\n tangentBase: PointId;\n /** Control point defining tangent direction (P1 for start, P2 for end). */\n tangentControl: PointId;\n arc: ArcId;\n /** Use arc start (true) or end (false) as the contact point. */\n atArcStart: boolean;\n };\n}\ninterface ConstrainedSketchBuilder {\n /** Move the cursor to `(x, y)` and start a new profile loop. */\n moveTo(x: number, y: number): this;\n /** Draw a line from the current cursor to `(x, y)`. */\n lineTo(x: number, y: number): this;\n /** Draw a horizontal line of length `dx` from the current cursor. */\n lineH(dx: number): this;\n /** Draw a vertical line of length `dy` from the current cursor. */\n lineV(dy: number): this;\n /** Draw a line of the given `length` at `degrees` from +X. */\n lineAngled(length: number, degrees: number): this;\n /** Draw a circular arc from the current cursor to `(x, y)` with the given radius. */\n arcTo(x: number, y: number, radius: number, clockwise?: boolean): this;\n /** Create an arc from an explicit center point and endpoint IDs. */\n arcByCenter(centerId: PointId, startId: PointId, endId: PointId, clockwise?: boolean, name?: string, fixedRadius?: boolean): ArcId;\n /** Create a cubic Bezier curve from four control points. */\n bezier(p0: any, p1: any, p2: any, p3: any, name?: string): BezierId;\n /** Draw a cubic Bezier from the current cursor to `(x3, y3)`. */\n bezierTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): this;\n /** Draw a smooth Bezier tangent to the previous arc. */\n blendTo(x: number, y: number, weight?: number): this;\n /** Label the current path segment. */\n label(name: string): this;\n /** Close the current path and register the loop. */\n close(): this;\n /** Add a circle loop to the path. */\n addLoopCircle(center: PointId, radius: number, segments?: number): this;\n /** Add a closed polygon loop from point IDs. */\n addLoop(points: any[]): this;\n /** Add a profile loop from prebuilt line/arc/bezier segments. */\n addProfileLoop(segments: Array<{\n kind: "line";\n line: any;\n } | {\n kind: "arc";\n arc: any;\n } | {\n kind: "bezier";\n bezier: any;\n }>): this;\n}\ninterface ConstrainedSketchBuilder {\n /** Constrain a line to be horizontal (parallel to the X axis). */\n horizontal(line: any): this;\n /** Constrain a line to be vertical (parallel to the Y axis). */\n vertical(line: any): this;\n /** Constrain two lines to be parallel. */\n parallel(a: any, b: any): this;\n /** Constrain two lines to point in the same direction. */\n sameDirection(a: any, b: any): this;\n /** Constrain two lines to point in opposite directions. */\n oppositeDirection(a: any, b: any): this;\n /** Prevent 180° rotation of a polygon by anchoring its first edge. */\n blockRotation(points: any[], axis?: "x" | "y"): this;\n /** Constrain two lines to be perpendicular. */\n perpendicular(a: any, b: any): this;\n /** Constrain a line/circle or circle/circle tangency relationship. */\n tangent(a: any, b: any): this;\n /** Constrain two lines to have equal length. */\n equal(a: any, b: any): this;\n /** Constrain two points to coincide. */\n coincident(a: any, b: any): this;\n /** Constrain two circles to share a center. */\n concentric(a: any, b: any): this;\n /** Constrain a point to lie on the infinite extension of a line. */\n collinear(point: any, line: any): this;\n /** Constrain two points to be symmetric about an axis line. */\n symmetric(a: any, b: any, axis: any): this;\n /** Pin a point at a specific world location. */\n fix(point: any, x?: number, y?: number): this;\n /** Constrain a point to lie at the midpoint of a line. */\n midpoint(point: any, line: any): this;\n /** Constrain a point to lie on the perimeter of a circle. */\n pointOnCircle(point: any, circle: any): this;\n /** Constrain a point to lie on the bounded segment of a line. */\n pointOnLine(point: any, line: any): this;\n}\ninterface ConstrainedSketchBuilder {\n /** Constrain the Euclidean distance between two points. */\n distance(a: any, b: any, value: number): this;\n /** Constrain the length of a line segment. */\n length(line: any, value: number): this;\n /** Constrain the signed angle from line `a` to line `b`. */\n angle(a: any, b: any, value: number): this;\n /** Constrain the radius of a circle. */\n radius(circle: any, value: number): this;\n /** Constrain the diameter of a circle. */\n diameter(circle: any, value: number): this;\n /** Constrain the horizontal distance between two points. */\n hDistance(a: any, b: any, value: number): this;\n /** Constrain the vertical distance between two points. */\n vDistance(a: any, b: any, value: number): this;\n /** Constrain the signed perpendicular distance from a point to a line. */\n pointLineDistance(point: any, line: any, value: number): this;\n /** Constrain the perpendicular offset distance between two lines. */\n lineDistance(a: any, b: any, value: number): this;\n /** Constrain the absolute angle of a line measured from +X. */\n absoluteAngle(line: any, value: number): this;\n /** Constrain two circles to have equal radii. */\n equalRadius(a: any, b: any): this;\n /** Constrain the arc length of an arc. */\n arcLength(arc: any, value: number): this;\n /** Constrain a line to be tangent to an arc at its start or end point. */\n lineTangentArc(line: any, arc: any, atStart: boolean): this;\n /** Constrain two arcs to be tangent at their shared junction point. */\n arcTangentArc(arcA: any, arcB: any, aAtStart?: boolean, bAtStart?: boolean): this;\n /** Constrain a Bezier to be tangent to an arc at one endpoint. */\n bezierTangentArc(bezier: any, arc: any, atBezierStart: boolean, atArcStart: boolean): this;\n /** Create a Bezier blend between two arcs. */\n smoothBlend(arc1: any, arc2: any, options?: {\n weight?: number;\n arc1End?: "start" | "end";\n arc2End?: "start" | "end";\n }): BezierId;\n /** Constrain a shape\'s width. */\n shapeWidth(shape: any, value: number): this;\n /** Constrain a shape\'s height. */\n shapeHeight(shape: any, value: number): this;\n /** Constrain a shape\'s centroid X position. */\n shapeCentroidX(shape: any, value: number): this;\n /** Constrain a shape\'s centroid Y position. */\n shapeCentroidY(shape: any, value: number): this;\n /** Constrain a shape\'s area. */\n shapeArea(shape: any, value: number): this;\n /** Constrain two shapes to have the same centroid. */\n shapeEqualCentroid(a: any, b: any): this;\n /** Constrain the unsigned angle between two lines. */\n angleBetween(a: any, b: any, value: number): this;\n /** Constrain all given points to be in counter-clockwise order. */\n ccw(...points: any[]): this;\n /**\n * Constrain the horizontal (X-axis) offset between two lines.\n * Uses the start-point of each line to measure horizontal distance.\n * `value` is the signed distance: b.startPt.x − a.startPt.x = value.\n */\n offsetX(a: any, b: any, value: number): this;\n /**\n * Constrain the vertical (Y-axis) offset between two lines.\n * Uses the start-point of each line to measure vertical distance.\n * `value` is the signed distance: b.startPt.y − a.startPt.y = value.\n */\n offsetY(a: any, b: any, value: number): this;\n}\ndeclare const SHAPE_BACKEND_MARKER: unique symbol;\ninterface ShapeRuntimeBounds {\n readonly min: [\n number,\n number,\n number\n ];\n readonly max: [\n number,\n number,\n number\n ];\n}\ninterface ShapeRuntimeMesh {\n readonly numProp: number;\n readonly numTri: number;\n readonly triVerts: Uint32Array;\n readonly vertProperties: Float32Array;\n readonly numVert?: number;\n readonly mergeFromVert?: Uint32Array;\n readonly mergeToVert?: Uint32Array;\n readonly runIndex?: Uint32Array;\n readonly runOriginalID?: Uint32Array;\n readonly runTransform?: Float32Array;\n readonly faceID?: Uint32Array | Int32Array;\n readonly halfedgeTangent?: Float32Array;\n}\ntype ShapeRuntimeCrossSection = any;\ninterface ShapeBackend {\n readonly [SHAPE_BACKEND_MARKER]: true;\n clone(): ShapeBackend;\n translate(x: number, y: number, z: number): ShapeBackend;\n rotate(x: number, y: number, z: number): ShapeBackend;\n transform(m: Mat4): ShapeBackend;\n scale(v: number | [\n number,\n number,\n number\n ]): ShapeBackend;\n mirror(normal: [\n number,\n number,\n number\n ]): ShapeBackend;\n split(other: ShapeBackend): [\n ShapeBackend,\n ShapeBackend\n ];\n splitByPlane(normal: [\n number,\n number,\n number\n ], originOffset: number): [\n ShapeBackend,\n ShapeBackend\n ];\n trimByPlane(normal: [\n number,\n number,\n number\n ], originOffset: number): ShapeBackend;\n boundingBox(): ShapeRuntimeBounds;\n volume(): number;\n surfaceArea(): number;\n isEmpty(): boolean;\n /** Number of disconnected solid bodies in this shape. */\n numBodies(): number;\n numTri(): number;\n getMesh(): ShapeRuntimeMesh;\n slice(offset: number): ShapeRuntimeCrossSection;\n project(): ShapeRuntimeCrossSection;\n dispose?(): void;\n}\ndeclare const PROFILE_BACKEND_MARKER: unique symbol;\ninterface ProfileBounds {\n min: [\n number,\n number\n ];\n max: [\n number,\n number\n ];\n}\ninterface ProfileBackend {\n readonly [PROFILE_BACKEND_MARKER]: true;\n area(): number;\n bounds(): ProfileBounds;\n isEmpty(): boolean;\n numVert(): number;\n toPolygons(): number[][][];\n translate(x: number, y: number): ProfileBackend;\n rotate(degrees: number): ProfileBackend;\n scale(v: number | [\n number,\n number\n ]): ProfileBackend;\n mirror(ax: [\n number,\n number\n ]): ProfileBackend;\n offset(delta: number, join: "Square" | "Round" | "Miter"): ProfileBackend;\n simplify(epsilon: number): ProfileBackend;\n subtract(other: ProfileBackend): ProfileBackend;\n extrude(height: number, divisions: number, twist: number, scaleTop?: [\n number,\n number\n ]): ShapeBackend;\n revolve(segments: number, degrees: number): ShapeBackend;\n}\ntype Anchor = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top" | "bottom" | "left" | "right";\ntype SketchOperandInput = Sketch | readonly Sketch[];\n/**\n * Immutable 2D profile for extrusion, revolve, and other operations.\n *\n * **Details**\n *\n * `Sketch` wraps Manifold\'s `CrossSection` with a chainable 2D API. Every method\n * returns a new `Sketch` — the original is never mutated. Colors, edge labels, and\n * placement data are preserved through all transforms and boolean operations.\n *\n * Supported operations:\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 *\n * Named anchor positions used by `attachTo()`:\n * `\'center\'` | `\'top-left\'` | `\'top-right\'` | `\'bottom-left\'` | `\'bottom-right\'`\n * | `\'top\'` | `\'bottom\'` | `\'left\'` | `\'right\'`\n */\ndeclare class Sketch {\n readonly cross: ProfileBackend;\n colorHex: string | undefined;\n constructor(cross: ProfileBackend, color?: string);\n /**\n * Set the display color of this sketch.\n *\n * **Details**\n *\n * Color is preserved through all transforms and boolean operations. Pass\n * `undefined` to clear the color.\n *\n * **Example**\n *\n * ```ts\n * circle2d(20).color(\'#ff0000\').extrude(5);\n * ```\n *\n * @param value - Hex color string (e.g. `\'#ff0000\'`) or `undefined` to clear\n * @returns A new sketch with the color set\n * @category Sketch Core\n */\n color(value: string | undefined): Sketch;\n /**\n * Create an explicit copy of this sketch for branching variants.\n *\n * **Details**\n *\n * Because all Sketch operations are immutable, `clone()` is rarely needed.\n * Use it when you want to assign the same sketch to multiple names and\n * continue modifying each independently without confusion.\n *\n * @returns A new Sketch with the same geometry and metadata\n * @see {@link duplicate} for an alias\n * @category Sketch Core\n */\n clone(): Sketch;\n /**\n * Alias for `clone()` — create an explicit copy handle for branching variants.\n *\n * @returns A new Sketch with the same geometry and metadata\n * @see {@link clone}\n * @category Sketch Core\n */\n duplicate(): Sketch;\n /**\n * Return the total filled area of the sketch.\n *\n * @returns Area in square model units\n * @category Sketch Core\n */\n area(): number;\n /**\n * Return the axis-aligned bounding box of the sketch.\n *\n * @returns `{ min: [x, y], max: [x, y] }`\n * @category Sketch Core\n */\n bounds(): ProfileBounds;\n /**\n * Return `true` if the sketch contains no filled area.\n *\n * @returns `true` when the sketch has no geometry\n * @category Sketch Core\n */\n isEmpty(): boolean;\n /**\n * Return the number of vertices in the polygon representation of the sketch contours.\n *\n * @returns Vertex count\n * @category Sketch Core\n */\n numVert(): number;\n /**\n * Return the sketch as a list of polygons matching its contour topology.\n *\n * Useful when you need raw polygon data for inspection or custom export.\n * @category Sketch Core\n */\n toPolygons(): number[][][];\n /**\n * Move the sketch by the given X and Y offset.\n *\n * @param x - Offset along X\n * @param y - Offset along Y (default 0)\n * @returns A translated sketch\n * @category Sketch Transforms\n */\n translate(x: number, y?: number): Sketch;\n /**\n * Rotate the sketch around its bounding-box center.\n *\n * @param degrees - Rotation angle in degrees (CCW positive)\n * @returns A rotated sketch\n * @see {@link rotateAround} to rotate around an arbitrary point\n * @category Sketch Transforms\n */\n rotate(degrees: number): Sketch;\n /**\n * Rotate the sketch around a specific pivot point.\n *\n * **Example**\n *\n * ```ts\n * rect(20, 20).rotateAround(45, [0, 0]);\n * ```\n *\n * @param degrees - Rotation angle in degrees (CCW positive)\n * @param pivot - 2D pivot point `[x, y]`\n * @returns A rotated sketch\n * @category Sketch Transforms\n */\n rotateAround(degrees: number, pivot: [\n number,\n number\n ]): Sketch;\n /**\n * Scale the sketch relative to its bounding-box center.\n *\n * **Details**\n *\n * Pass a single number for uniform scaling, or `[sx, sy]` for per-axis scaling.\n *\n * @param v - Scale factor — uniform `number` or per-axis `[sx, sy]`\n * @returns A scaled sketch\n * @see {@link scaleAround} to scale relative to an arbitrary point\n * @category Sketch Transforms\n */\n scale(v: number | [\n number,\n number\n ]): Sketch;\n /**\n * Mirror the sketch across a line through its bounding-box center.\n *\n * **Details**\n *\n * `normal` is the normal vector of the mirror line (not the line direction).\n * For example, `[1, 0]` mirrors across a vertical line (Y axis direction),\n * and `[0, 1]` mirrors across a horizontal line.\n *\n * @param normal - Normal of the mirror line as `[x, y]`\n * @returns A mirrored sketch\n * @see {@link mirrorThrough} to mirror across a line through an arbitrary point\n * @category Sketch Transforms\n */\n mirror(normal: [\n number,\n number\n ]): Sketch;\n /**\n * Scale the sketch relative to an arbitrary pivot point.\n *\n * @param pivot - 2D pivot point `[x, y]`\n * @param v - Scale factor — uniform `number` or per-axis `[sx, sy]`\n * @returns A scaled sketch\n * @category Sketch Transforms\n */\n scaleAround(pivot: [\n number,\n number\n ], v: number | [\n number,\n number\n ]): Sketch;\n /**\n * Mirror the sketch across a line defined by a point and a normal direction.\n *\n * @param point - A point on the mirror line\n * @param normal - Normal of the mirror line as `[x, y]`\n * @returns A mirrored sketch\n * @category Sketch Transforms\n */\n mirrorThrough(point: [\n number,\n number\n ], normal: [\n number,\n number\n ]): Sketch;\n /**\n * Add (union) one or more sketches to this sketch.\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `sketch.add(a, b)` or `sketch.add([a, b])`.\n * For combining many sketches at once, prefer the free function `union2d()` which\n * uses Manifold\'s batch operation and is faster than chaining.\n *\n * **Example**\n *\n * ```ts\n * circle2d(20).add(rect(10, 30)).extrude(5);\n * ```\n *\n * @returns A new sketch with all inputs unioned together\n * @see {@link union2d} for the batch free-function alternative\n * @category Sketch Booleans\n */\n add(...others: SketchOperandInput[]): Sketch;\n /**\n * Subtract one or more sketches from this sketch.\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `sketch.subtract(a, b)` or `sketch.subtract([a, b])`.\n * For subtracting many cutters at once, prefer the free function `difference2d()`.\n *\n * **Example**\n *\n * ```ts\n * rect(40, 40).subtract(circle2d(10)).extrude(5);\n * ```\n *\n * @returns A new sketch with all inputs subtracted\n * @see {@link difference2d} for the batch free-function alternative\n * @category Sketch Booleans\n */\n subtract(...others: SketchOperandInput[]): Sketch;\n /**\n * Intersect this sketch with one or more others (keep overlapping area only).\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `sketch.intersect(a, b)` or `sketch.intersect([a, b])`.\n * For intersecting many sketches, prefer the free function `intersection2d()`.\n *\n * @returns A new sketch containing only the area shared by all inputs\n * @see {@link intersection2d} for the batch free-function alternative\n * @category Sketch Booleans\n */\n intersect(...others: SketchOperandInput[]): Sketch;\n /**\n * Alias for `add()` — matches the free-function `union2d()` naming.\n *\n * @see {@link add}\n * @category Sketch Booleans\n */\n union(...others: SketchOperandInput[]): Sketch;\n /**\n * Alias for `subtract()` — matches the free-function `difference2d()` naming.\n *\n * @see {@link subtract}\n * @category Sketch Booleans\n */\n difference(...others: SketchOperandInput[]): Sketch;\n /**\n * Alias for `intersect()` — matches the free-function `intersection2d()` naming.\n *\n * @see {@link intersect}\n * @category Sketch Booleans\n */\n intersection(...others: SketchOperandInput[]): Sketch;\n /**\n * Inflate (positive delta) or deflate (negative delta) the sketch contour.\n *\n * **Details**\n *\n * Use `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 * **Example**\n *\n * ```ts\n * rect(40, 20).offset(3); // expand by 3\n * rect(40, 20).offset(-2).offset(2); // round all convex corners\n * ```\n *\n * @param delta - Offset distance (positive = expand, negative = shrink)\n * @param join - Corner join style: `\'Round\'` | `\'Square\'` | `\'Miter\'` (default `\'Round\'`)\n * @returns A new offset sketch\n * @category Sketch Operations\n */\n offset(delta: number, join?: "Square" | "Round" | "Miter"): Sketch;\n /**\n * Decompose this sketch into its distinct filled regions, sorted largest-first by area.\n *\n * **Details**\n *\n * A single sketch can contain several disconnected filled areas (e.g., two separate\n * rectangles, or a ring shape with a hole). This method enumerates all top-level\n * connected regions as independent `Sketch` objects, each with its own outer boundary\n * and associated holes.\n *\n * **Example**\n *\n * ```ts\n * const pair = union2d(rect(40, 40), rect(40, 40).translate(60, 0));\n * const [left, right] = pair.regions(); // largest first\n * left.extrude(5);\n * ```\n *\n * @returns Array of region sketches, sorted by area descending\n * @see {@link region} to pick one region by seed point\n * @category Sketch Regions\n */\n regions(): Sketch[];\n /**\n * Select the single filled region that contains the given 2D seed point.\n *\n * **Details**\n *\n * The seed must lie strictly inside the filled area — not on a boundary edge and\n * not inside a hole. Throws a descriptive error if the seed is outside all regions.\n * If unsure where regions are, use `.regions()` first — each result has `.bounds()`.\n *\n * **Example**\n *\n * ```ts\n * const donut = circle2d(50).subtract(circle2d(30));\n * donut.region([40, 0]).extrude(10); // seed at radius 40, inside the ring\n * ```\n *\n * @param seed - A 2D point `[x, y]` strictly inside the desired region\n * @returns The sketch region containing the seed point\n * @see {@link regions} to enumerate all regions\n * @category Sketch Regions\n */\n region(seed: [\n number,\n number\n ]): Sketch;\n /** Extrude this 2D sketch along Z to create a 3D solid. Supports twist and scale tapering. */\n extrude(height: number, opts?: {\n twist?: number;\n divisions?: number;\n scaleTop?: number | [\n number,\n number\n ];\n }): Shape;\n /** Revolve this 2D sketch around the Y axis to create a 3D solid of revolution. */\n revolve(degrees?: number, segments?: number): Shape;\n /**\n * Position this sketch relative to another using named anchor points.\n *\n * **Details**\n *\n * Computes the translation needed to align `selfAnchor` on this sketch with\n * `targetAnchor` on the target sketch, then applies an optional pixel-exact offset.\n *\n * Anchor positions: `\'center\'` | `\'top-left\'` | `\'top-right\'` | `\'bottom-left\'`\n * | `\'bottom-right\'` | `\'top\'` | `\'bottom\'` | `\'left\'` | `\'right\'`\n *\n * **Example**\n *\n * ```ts\n * const arm = rect(4, 70).attachTo(plate, \'bottom-left\', \'top-left\');\n * const shifted = rect(4, 70).attachTo(plate, \'bottom-left\', \'top-left\', [5, 0]);\n * ```\n *\n * @param target - The sketch to attach to\n * @param targetAnchor - Named anchor point on the target sketch\n * @param selfAnchor - Named anchor point on this sketch to align (default `\'center\'`)\n * @param offset - Additional `[dx, dy]` offset applied after alignment\n * @returns A translated sketch\n * @category Sketch Transforms\n */\n attachTo(target: Sketch, targetAnchor: Anchor, selfAnchor?: Anchor, offset?: [\n number,\n number\n ]): Sketch;\n /**\n * Place this sketch on a face or planar target in 3D space.\n *\n * Use this when a 2D profile should be oriented onto a 3D face before\n * extrusion or other downstream operations.\n *\n * @param parentOrFace - Parent shape, face reference, or shape-like target.\n * @param faceOrOpts - Face selector or placement options.\n * @param opts - Additional placement options when a face selector is provided separately.\n * @returns A sketch positioned on the target face.\n * @category Sketch Transforms\n */\n onFace(parentOrFace: Shape | {\n toShape(): Shape;\n } | {\n _bbox(): {\n min: number[];\n max: number[];\n };\n } | FaceRef, faceOrOpts?: "front" | "back" | "left" | "right" | "top" | "bottom" | string | FaceRef | {\n u?: number;\n v?: number;\n protrude?: number;\n selfAnchor?: Anchor;\n }, opts?: {\n u?: number;\n v?: number;\n protrude?: number;\n selfAnchor?: Anchor;\n }): Sketch;\n /** Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch. */\n labelEdge(name: string): Sketch;\n /**\n * Label edges in winding order, or by named map for rect.\n *\n * Positional: `labelEdges(\'bottom\', \'right\', \'top\', \'left\')` — one per edge, `null` to skip.\n * Named (rect only): `labelEdges({ bottom: \'floor\', top: \'ceiling\' })`.\n * Returns a new sketch.\n */\n labelEdges(...args: (string | null)[] | [\n Record<string, string>\n ]): Sketch;\n /** List current edge label names. */\n edgeLabels(): string[];\n /** Prefix all edge labels. Returns a new sketch with prefixed labels. */\n prefixLabels(prefix: string): Sketch;\n /** Rename a single edge label. Returns a new sketch. */\n renameLabel(from: string, to: string): Sketch;\n /** Remove specific labels. Returns a new sketch. */\n dropLabels(...names: string[]): Sketch;\n /** Remove all labels. Returns a new sketch. */\n dropAllLabels(): Sketch;\n}\ndeclare class ConstraintSketch extends Sketch {\n readonly constraintMeta: SketchConstraintMeta;\n readonly definition: ConstraintDefinition;\n constructor(cross: Sketch["cross"], constraintMeta: SketchConstraintMeta, definition: ConstraintDefinition);\n /**\n * Enumerate all bounded regions formed by the line arrangement of this sketch.\n * Construction lines are excluded. Regions are returned largest-first by area.\n */\n detectArrangement(): Sketch[];\n /**\n * Select the single arrangement region that contains the given seed point.\n * Throws if no region contains the seed.\n */\n detectArrangementRegion(seed: [\n number,\n number\n ]): Sketch;\n /**\n * Re-solve the sketch after changing the value of one existing constraint.\n *\n * Use this for interactive dimension edits without rebuilding the whole sketch graph.\n * It attempts a warm-started solve first, then falls back to a full solve if needed.\n *\n * @param constraintId - ID of the existing constraint to update.\n * @param value - New numeric value for the constraint.\n * @returns A new solved `ConstraintSketch`.\n */\n withUpdatedConstraint(constraintId: string, value: number): ConstraintSketch;\n /**\n * Return a human-readable diagnostic string of the solved state.\n */\n inspect(): string;\n}\ninterface ConstrainedSketchBuilder {\n /** Import a `Point2D` object into the sketch. */\n importPoint(pt: {\n x: number;\n y: number;\n }, fixed?: boolean): PointId;\n /** Import a `Line2D` object into the sketch. */\n importLine(l: {\n start: {\n x: number;\n y: number;\n };\n end: {\n x: number;\n y: number;\n };\n }, fixed?: boolean): LineId;\n /** Import a `Rectangle2D` as four points and four lines. */\n importRectangle(r: {\n vertices: [\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n },\n {\n x: number;\n y: number;\n }\n ];\n }, fixed?: boolean): {\n bottom: LineId;\n right: LineId;\n top: LineId;\n left: LineId;\n points: [\n PointId,\n PointId,\n PointId,\n PointId\n ];\n };\n /** Add a fixed reference point at `(x, y)`. */\n referencePoint(x: number, y: number): PointId;\n /** Add a fixed reference line from `(x1, y1)` to `(x2, y2)`. */\n referenceLine(x1: number, y1: number, x2: number, y2: number): LineId;\n /** Import a single named entity from a solved sketch as fixed reference geometry. */\n referenceFrom(source: ConstraintSketch, entityId: string): PointId | LineId | null;\n /** Import all non-construction entities from a solved sketch as fixed references. */\n referenceAllFrom(source: ConstraintSketch): {\n points: Map<string, PointId>;\n lines: Map<string, LineId>;\n };\n}\ninterface ConstrainedSketchOptions {\n /** When true, adding a constraint that cannot be satisfied throws instead of silently discarding it. */\n strict?: boolean;\n}\ninterface ConstrainedSketchBuilder extends ConstraintBuilderMethods {\n}\ndeclare class ConstrainedSketchBuilder {\n private points;\n private lines;\n private circles;\n private arcs;\n private beziers;\n private shapes;\n private _groups;\n /** Point IDs owned by groups — excluded from the serialized points array. */\n private groupOwnedPointIds;\n /** Line IDs owned by groups — excluded from the serialized lines array. */\n private groupOwnedLineIds;\n private constraints;\n private loops;\n private rejectedConstraints;\n /** Maps rejected constraint ID → human-readable reason string. */\n private rejectionReasons;\n private cursor;\n private loopStart;\n /** Last arc created by the path API (arcTo), used by blendTo. */\n private lastPathArc;\n private nextId;\n private strict;\n /** Cumulative time spent in seedIncrementalGeometry calls (ms). */\n private seedTimeMs;\n /** Max cumulative time for all seed calls (ms). After this, seeding is skipped. */\n private static readonly SEED_BUDGET_MS;\n /** WASM solver session handle — persists state across seed steps. */\n private _sessionHandle;\n private _sessionApi;\n private _sessionFailed;\n constructor(options?: ConstrainedSketchOptions);\n /** Try to create a WASM solver session. Returns true if session is active. */\n private ensureSession;\n private destroySession;\n /**\n * Add a free point to the sketch at `(x, y)`.\n *\n * If `x` or `y` are omitted, the point is placed at the bounding-box center\n * of existing geometry so it starts near other entities rather than at the origin.\n * Throws if either coordinate is `NaN` or `Infinity`.\n *\n * @param x - X coordinate. Defaults to existing geometry center.\n * @param y - Y coordinate. Defaults to existing geometry center.\n * @param fixed - When true the point is pinned at (x, y) and acts like a `fix` constraint.\n * @returns A `PointId` that can be passed to `line()`, `circle()`, and constraints.\n * @category Constrained Sketches\n */\n point(x?: number, y?: number, fixed?: boolean): PointId;\n /**\n * Return the `PointId` of the point created at the given insertion index (0-based).\n *\n * Useful for referencing points by order when the original `PointId` was not stored.\n *\n * @param index - 0-based creation index.\n * @returns The `PointId` at that index.\n * @category Constrained Sketches\n */\n /** Return the `PointId` of the point created at the given insertion index. */\n pointAt(index: number): PointId;\n /**\n * Connect two existing points with a line segment.\n *\n * Pass `construction = true` for a helper line that participates in constraints\n * but is excluded from the solved sketch output (not part of any profile loop).\n *\n * **Example** (construction axis for symmetry)\n *\n * ```ts\n * const axis = sk.line(sk.point(0, -50), sk.point(0, 50), true);\n * sk.symmetric(p1, p2, axis);\n * ```\n *\n * @param a - Start `PointId`.\n * @param b - End `PointId`.\n * @param construction - When true this line is excluded from the solved output. Default: false.\n * @param name - Optional label for debugging.\n * @returns A `LineId` for use in constraints and path loops.\n * @category Constrained Sketches\n */\n line(a: PointId, b: PointId, construction?: boolean, name?: string): LineId;\n /**\n * Return the `LineId` of the line created at the given insertion index (0-based).\n *\n * Useful in path-drawing workflows where lines are created implicitly via\n * `lineTo()` and you need to reference one by position.\n *\n * ```ts\n * sk.moveTo(0, 0).lineTo(50, 0).lineTo(50, 30).close();\n * sk.length(sk.lineAt(0), 50);\n * ```\n *\n * @param index - 0-based creation index.\n * @returns The `LineId` at that index.\n * @category Constrained Sketches\n */\n /** Return the `LineId` of the line created at the given insertion index. */\n lineAt(index: number): LineId;\n /**\n * Add a circle to the sketch with the given center point and initial radius.\n *\n * The radius is a starting value — if you add a `radius()` or `diameter()` constraint,\n * the solver will adjust it. Non-construction circles automatically register a loop.\n *\n * @param center - Center `PointId`.\n * @param radius - Initial radius (must be finite and positive).\n * @param construction - When true the circle is excluded from the solved output. Default: false.\n * @param segments - Tessellation resolution for display. Default: 48.\n * @param name - Optional label for debugging.\n * @returns A `CircleId` for use in constraints.\n * @category Constrained Sketches\n */\n circle(center: PointId, radius: number, construction?: boolean, segments?: number, name?: string): CircleId;\n /**\n * Return the `CircleId` of the circle created at the given insertion index (0-based).\n *\n * @param index - 0-based creation index.\n * @returns The `CircleId` at that index.\n * @category Constrained Sketches\n */\n /** Return the `CircleId` of the circle created at the given insertion index. */\n circleAt(index: number): CircleId;\n /**\n * Register a named shape (closed polygon) from an ordered list of line IDs.\n *\n * The `ShapeId` can be passed to `shapeWidth()`, `shapeHeight()`, `shapeArea()`,\n * `shapeCentroidX()`, `shapeCentroidY()`, and `shapeEqualCentroid()` constraints.\n * Shape registration is done automatically by concept factories like `rect()` and `addPolygon()`.\n *\n * @param lines - Ordered line IDs forming the closed polygon boundary.\n * @returns A `ShapeId` for use in shape constraints.\n * @category Constrained Sketches\n */\n shape(lines: LineId[]): ShapeId;\n /**\n * Create a rigid-body group with a local coordinate frame.\n *\n * Points and lines added to the group move together as a unit — the solver\n * sees 3 DOF (x, y, θ) instead of 2N per point. After configuring the group,\n * call `.done()` to register it and receive a `SketchGroupHandle`.\n *\n * Group points are addressable by their `PointId` in all sketch constraints\n * (e.g. `sk.coincident`, `sk.distance`) just like any other points.\n *\n * **Example**\n *\n * ```ts\n * const g = sk.group({ x: 50, y: 30 });\n * const p0 = g.point(0, 0); // local origin → world (50, 30)\n * const p1 = g.point(100, 0); // local (100,0) → world (150, 30)\n * const l = g.line(p0, p1);\n * g.fixRotation();\n * const handle = g.done();\n * // p0, p1 work in constraints like any other PointId:\n * sk.coincident(p0, someExternalPoint);\n * ```\n *\n * @param opts - Initial frame position (x, y) in degrees theta, and optional id.\n * @returns A `SketchGroupBuilder` — call `.done()` to finalize.\n * @category Constrained Sketches\n */\n group(opts?: {\n x?: number;\n y?: number;\n theta?: number;\n id?: string;\n }): SketchGroupBuilder;\n /**\n * Register a group directly (called by SketchGroupBuilder).\n * @internal\n */\n _registerGroup(group: SketchGroup): void;\n /** Add a raw constraint object to the builder. */\n constrain(constraint: Omit<SketchConstraint, "id">): this;\n /**\n * Keep the live builder geometry near a solved state as constraints are added.\n * This restores the progressive seeding path used by large sketches in the browser\n * without rejecting constraints or changing the final solve API.\n */\n private seedIncrementalGeometry;\n /** Serialize a constraint for the session API (matches Rust serde format). */\n private serializeConstraintForSession;\n /** Sync point positions from session state back to builder entities. */\n private syncPointsFromSession;\n private resolvePointId;\n private resolveLineId;\n private resolveCircleId;\n private resolveArcId;\n private resolveBezierId;\n private resolveShapeId;\n /**\n * Validate that a constraint value is a finite number.\n * Throws with the constraint name for clear debugging.\n */\n private requireFinite;\n /**\n * Run the constraint solver and return a solved sketch.\n *\n * The returned `ConstraintSketch` extends `Sketch` and can be used directly\n * in all 3D operations (`extrude`, `revolve`, etc.). It also exposes\n * `constraintMeta` with the solver status:\n *\n * ```ts\n * const result = sk.solve();\n * result.constraintMeta.status; // \'fully\' | \'under\' | \'over\' | \'over-redundant\'\n * result.constraintMeta.dof; // 0 = fully constrained\n * result.constraintMeta.maxError; // residual — should be < 1e-6\n * result.inspect(); // human-readable summary\n * result.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 * @param options - Advanced solver options (iterations, restarts, tolerance, etc.).\n * @returns A `ConstraintSketch` (also a valid `Sketch`) with solved geometry.\n * @category Constrained Sketches\n */\n solve(options?: SolveOptions): ConstraintSketch | Sketch;\n /**\n * Run the solver without building a full `ConstraintSketch`.\n *\n * Lighter than `solve()` — skips profile and DOF analysis. Useful for\n * lightweight constraint validation or progress monitoring mid-construction.\n *\n * @param options - Advanced solver options (iterations, restarts, tolerance, etc.).\n * @returns Object with `maxError`, `rejectedCount`, and the solved `ConstraintDefinition`.\n * @category Constrained Sketches\n */\n solveConstraintsOnly(options?: SolveOptions): {\n maxError: number;\n rejectedCount: number;\n definition: ConstraintDefinition;\n };\n private buildDefinition;\n /** Sync solved positions from a definition back to the builder\'s live entities. */\n private syncFromDefinition;\n private getPoint;\n /** Compute arc center from start/end points, radius, and clockwise flag; create the arc entity. */\n private addArc;\n /**\n * Bounding box of all existing non-construction points.\n * Returns null when no points exist yet.\n * Used by concept factories to auto-offset initial geometry.\n */\n _pointBounds(): {\n minX: number;\n maxX: number;\n minY: number;\n maxY: number;\n } | null;\n}\n/**\n * Create a parametric 2D sketch driven by geometric constraints and a nonlinear solver.\n *\n * **Workflow**\n *\n * 1. Create a builder with `constrainedSketch()`.\n * 2. Add geometry — points, lines, circles, arcs — using the builder methods.\n * 3. Add constraints (`horizontal`, `length`, `fix`, etc.) to drive the geometry.\n * 4. Call `.solve()` to run the solver and get a `ConstraintSketch` (which extends `Sketch`).\n *\n * **Example**\n *\n * ```ts\n * const sk = constrainedSketch();\n * const p1 = sk.point(0, 0);\n * const p2 = sk.point(50, 0);\n * const l1 = sk.line(p1, p2);\n * sk.fix(p1, 0, 0);\n * sk.horizontal(l1);\n * sk.length(l1, 50);\n * return sk.solve().extrude(10);\n * ```\n *\n * **Solver status**\n *\n * ```ts\n * const result = sk.solve();\n * result.constraintMeta.status; // \'fully\' | \'under\' | \'over\' | \'over-redundant\'\n * result.constraintMeta.dof; // 0 = fully constrained\n * result.constraintMeta.maxError; // residual — should be < 1e-6\n * result.inspect(); // human-readable summary\n * result.withUpdatedConstraint(\'cst-5\', 120); // update a dimension without rebuilding\n * ```\n *\n * @param options - Optional builder configuration.\n * @returns A `ConstrainedSketchBuilder` ready to accept geometry and constraints.\n * @category Constrained Sketches\n */\ndeclare function constrainedSketch(options?: ConstrainedSketchOptions): ConstrainedSketchBuilder;\ninterface SketchGroupHandle {\n readonly id: GroupId;\n /** Get a group vertex PointId by its index (order of `.point()` calls). */\n point(index: number): PointId;\n /** Get a group line LineId by its index (order of `.line()` calls). */\n line(index: number): LineId;\n /** All group vertex PointIds in creation order. */\n readonly vertices: PointId[];\n /** All group line LineIds in creation order. */\n readonly sides: LineId[];\n}\ndeclare class SketchGroupBuilder {\n private sk;\n private groupId;\n private gx;\n private gy;\n private gtheta;\n private localPoints;\n private localLines;\n private isFixed;\n private isFixedRotation;\n private registered;\n constructor(sk: ConstrainedSketchBuilder, opts: {\n x?: number;\n y?: number;\n theta?: number;\n id?: string;\n });\n /** Add a point in local coordinates. Returns its globally-addressable PointId. */\n /**\n * Add a point in local coordinates. Returns its globally-addressable PointId.\n */\n point(lx: number, ly: number): PointId;\n /** Connect two group points with a line. Both must be PointIds from this group. */\n /**\n * Connect two group points with a line. Both must be PointIds from this group.\n */\n line(a: PointId, b: PointId, name?: string): LineId;\n /** Freeze rotation (θ). Group can still translate - 2 DOF remain. */\n /** Freeze rotation (theta). Group can still translate - 2 DOF remain. */\n fixRotation(): this;\n /** Freeze all 3 DOF - group is completely fixed. */\n /** Freeze all 3 DOF - group is completely fixed. */\n fix(): this;\n /**\n * Finalize and register the group with the builder.\n * Returns a handle for referencing group points/lines in constraints.\n */\n /** Finalize and register the group with the builder. */\n done(): SketchGroupHandle;\n}\ndeclare class RouteBuilder {\n private readonly sk;\n private readonly _startPt;\n private cursorPt;\n private cursorX;\n private cursorY;\n /**\n * Current travel direction in radians from +X.\n * null = direction not yet established (first segment will set it).\n */\n private direction;\n private segments;\n private lastLineId;\n private lastArcId;\n private finished;\n constructor(sk: ConstrainedSketchBuilder, startPt: PointId, x: number, y: number);\n /** Vertical line going +Y. Length is optional (solver determines it from constraints). */\n up(length?: number): LineId;\n /** Vertical line going -Y. Length is optional. */\n down(length?: number): LineId;\n /** Horizontal line going +X. Length is optional. */\n right(length?: number): LineId;\n /** Horizontal line going -X. Length is optional. */\n left(length?: number): LineId;\n /** Line at an arbitrary angle (degrees from +X). Length is optional. */\n lineAt(angleDeg: number, length?: number): LineId;\n /**\n * Line with solver-determined direction. Length is optional.\n * Direction comes from tangency to previous arc or from constraints.\n */\n line(length?: number): LineId;\n /**\n * Line toward a specific point.\n * Length defaults to the distance to that point.\n */\n toward(x: number, y: number): LineId;\n /**\n * Tangent arc turning left relative to travel direction.\n * @param radius - Arc radius. If omitted, solver determines it.\n * @param sweepDegOrOpts - Sweep angle in degrees (constrains the arc),\n * or `{ minSweep: degrees }` to seed the geometry without constraining.\n * `minSweep` guides the solver to the correct branch for arcs that\n * sweep more than the default 90° seed.\n */\n arcLeft(radius?: number, sweepDegOrOpts?: number | {\n minSweep: number;\n }): ArcId;\n /**\n * Tangent arc turning right relative to travel direction.\n * @param radius - Arc radius. If omitted, solver determines it.\n * @param sweepDegOrOpts - Sweep angle in degrees (constrains the arc),\n * or `{ minSweep: degrees }` to seed without constraining.\n */\n arcRight(radius?: number, sweepDegOrOpts?: number | {\n minSweep: number;\n }): ArcId;\n /** Close the route with a straight line back to the start point. */\n close(): void;\n /**\n * Close the route back to its start point and register as a profile loop.\n *\n * No extra line segment is added. A coincident constraint connects the\n * last point to the start, and tangency is added for G1 smoothness\n * when arcs are at the junction. The session\'s incremental solver\n * processes these constraints, keeping seed positions accurate for\n * the final solve.\n */\n done(): void;\n /** PointId of the route\'s start point. */\n get start(): PointId;\n /** PointId of the current cursor (route\'s end). */\n get end(): PointId;\n /** Get the start point of a segment. */\n startOf(segId: LineId | ArcId): PointId;\n /** Get the end point of a segment. */\n endOf(segId: LineId | ArcId): PointId;\n /**\n * @param angle - Direction in radians from +X\n * @param length - Optional length\n * @param constrainDirection - Whether to add horizontal/vertical constraint (default true for axis-aligned)\n */\n private _addLine;\n private _addArc;\n private _ensureNotFinished;\n private _registerLoop;\n private _normalizeAngle;\n private _isVertical;\n private _isHorizontal;\n}\ninterface RouteLine {\n /** \'x\' for vertical line at x=offset, \'y\' for horizontal line at y=offset */\n axis: "x" | "y";\n offset: number;\n}\ninterface RouteCircle {\n center: [\n number,\n number\n ];\n radius: number;\n}\ninterface RouteTangent {\n tangent: RouteCircle | CircleId;\n}\ninterface RouteFillet {\n fillet: number;\n /** When \'tangent\', adds a tangent line from the smaller circle before the fillet arc. */\n approach?: "tangent";\n}\ninterface RouteTangentArc {\n tangentArc: number;\n}\ninterface RoutePoint {\n point: [\n number,\n number\n ];\n}\ninterface RouteUntil {\n line: RouteLine | LineId;\n until: number;\n}\ntype RouteStep = RouteLine | RouteCircle | CircleId | RouteTangent | RouteFillet | RouteTangentArc | RoutePoint | RouteUntil;\ninterface ConstrainedSketchBuilder {\n /**\n * Route a profile through a sequence of geometric elements (legacy API).\n * The solver computes all tangent points and intersections automatically.\n *\n * Steps can include:\n * - `{ point: [x, y] }` — route through a point\n * - `{ axis: \'x\'|\'y\', offset: n }` — follow a construction line\n * - `{ line: {...}, until: n }` — follow a line clipped to a coordinate\n * - `{ tangent: { center, radius } }` — tangent arc onto a construction circle\n * - `{ fillet: radius }` — fillet between adjacent elements\n * - `{ tangentArc: radius }` — free tangent arc (solver finds center)\n *\n * Returns `this` for chaining. Call `.solve()` after to get the Sketch.\n */\n route(steps: RouteStep[]): this;\n /**\n * Start a directional route from coordinates.\n *\n * Returns a `RouteBuilder` - describe the path with up/down/left/right/arcLeft/arcRight.\n * Each method returns the entity ID (`LineId` or `ArcId`) for use in `sk.*` constraints.\n *\n * @example\n * ```ts\n * const r = sk.route(0, 0);\n * const stem = r.up(18);\n * r.arcLeft(8.9);\n * const neck = r.down();\n * r.done();\n * sk.offsetX(stem, neck, 10.8);\n * ```\n */\n route(x: number, y: number): RouteBuilder;\n /**\n * Start a directional route from an existing sketch point.\n */\n route(startPoint: PointId): RouteBuilder;\n}\n/**\n * An immutable 2D point with measurement and construction helpers.\n *\n * Used as construction geometry in sketches, constraints, and analytic\n * measurements. All methods return new instances — `Point2D` is immutable.\n *\n * @category 2D Entities\n */\ndeclare class Point2D {\n readonly x: number;\n readonly y: number;\n constructor(x: number, y: number);\n /**\n * Measure straight-line distance to another point.\n *\n * @param other - The other point.\n * @returns Euclidean distance.\n */\n distanceTo(other: Point2D): number;\n /**\n * Compute the midpoint between this point and another point.\n *\n * @param other - The other point.\n * @returns A new midpoint.\n */\n midpointTo(other: Point2D): Point2D;\n /**\n * Return a point shifted by the given delta.\n *\n * @param dx - Horizontal offset.\n * @param dy - Vertical offset.\n * @returns A translated point.\n */\n translate(dx: number, dy: number): Point2D;\n /**\n * Convert this point to a plain `[x, y]` tuple.\n *\n * @returns A coordinate tuple.\n */\n toTuple(): [\n number,\n number\n ];\n}\n/**\n * Create an analytic 2D point for measurement and construction geometry.\n *\n * **Example**\n *\n * ```ts\n * const p = point(10, 20);\n * p.distanceTo(point(30, 40)); // Euclidean distance\n * p.midpointTo(point(30, 40)); // midpoint\n * p.translate(5, 5); // new shifted point\n * p.toTuple(); // [10, 20]\n * ```\n *\n * @param x - X coordinate\n * @param y - Y coordinate\n * @returns A new `Point2D`\n * @category 2D Entities\n */\ndeclare function point(x: number, y: number): Point2D;\n/**\n * An immutable 2D line segment with length, angle, intersection, and parallel helpers.\n *\n * Provides both segment-only (`intersectSegment`) and infinite-line\n * (`intersect`) intersection queries. All methods return new instances.\n *\n * @category 2D Entities\n */\ndeclare class Line2D {\n readonly start: Point2D;\n readonly end: Point2D;\n constructor(start: Point2D, end: Point2D);\n /** Length of the line segment. */\n get length(): number;\n /** Midpoint of the line segment. */\n get midpoint(): Point2D;\n /** Direction angle in degrees, measured CCW from +X. */\n get angle(): number;\n /** Unit direction vector from start to end. */\n get direction(): [\n number,\n number\n ];\n /**\n * Create a parallel line offset by the given distance.\n *\n * Positive distance shifts to the left of the line direction.\n */\n parallel(distance: number): Line2D;\n /**\n * Intersect this line with another infinite line.\n *\n * @param other - The other line.\n * @returns The intersection point, or `null` if parallel.\n */\n intersect(other: Line2D): Point2D | null;\n /**\n * Intersect this line with another as bounded segments.\n *\n * @param other - The other line.\n * @returns The intersection point, or `null` if the segments do not cross.\n */\n intersectSegment(other: Line2D): Point2D | null;\n /**\n * Create a line from raw coordinates.\n *\n * @param x1 - Start X.\n * @param y1 - Start Y.\n * @param x2 - End X.\n * @param y2 - End Y.\n * @returns A new line segment.\n */\n static fromCoordinates(x1: number, y1: number, x2: number, y2: number): Line2D;\n /**\n * Create a line from a start point, angle, and length.\n *\n * @param origin - Start point.\n * @param angleDeg - Angle in degrees, CCW from +X.\n * @param length - Segment length.\n * @returns A new line segment.\n */\n static fromPointAndAngle(origin: Point2D, angleDeg: number, length: number): Line2D;\n /**\n * Create a line from a start point, direction vector, and length.\n *\n * @param origin - Start point.\n * @param dir - Direction vector `[x, y]`.\n * @param length - Segment length.\n * @returns A new line segment.\n */\n static fromPointAndDirection(origin: Point2D, dir: [\n number,\n number\n ], length: number): Line2D;\n}\n/**\n * Create an analytic 2D line segment between two points.\n *\n * **Example**\n *\n * ```ts\n * const l = line(0, 0, 50, 0);\n * l.length; l.midpoint; l.angle; l.direction;\n * l.parallel(10); // parallel line offset 10 (positive = left)\n * l.intersect(l2); // Point2D — treats lines as infinite\n * l.intersectSegment(l2); // Point2D or null — segments only\n *\n * Line2D.fromPointAndAngle(point(0, 0), 45, 100);\n * Line2D.fromPointAndDirection(point(0, 0), [1, 1], 50);\n * ```\n *\n * @param x1 - Start X\n * @param y1 - Start Y\n * @param x2 - End X\n * @param y2 - End Y\n * @returns A new `Line2D`\n * @category 2D Entities\n */\ndeclare function line(x1: number, y1: number, x2: number, y2: number): Line2D;\n/**\n * An immutable 2D circle with area, circumference, and extrusion support.\n *\n * Extruding a `Circle2D` produces a cylinder with named `top`, `bottom`,\n * and `side` faces accessible via the topology API.\n *\n * @category 2D Entities\n */\ndeclare class Circle2D {\n readonly center: Point2D;\n readonly radius: number;\n constructor(center: Point2D, radius: number);\n /** Diameter of the circle. */\n get diameter(): number;\n /** Circumference of the circle. */\n get circumference(): number;\n /** Area of the circle. */\n get area(): number;\n /**\n * Return a point on the circle at the given angle.\n *\n * @param angleDeg - Angle in degrees, where 0 is right and CCW is positive.\n * @returns A point on the perimeter.\n */\n pointAtAngle(angleDeg: number): Point2D;\n /**\n * Return a translated circle.\n *\n * @param dx - Horizontal offset.\n * @param dy - Vertical offset.\n * @returns A shifted circle.\n */\n translate(dx: number, dy: number): Circle2D;\n /**\n * Convert this circle to a sketch profile.\n *\n * @param segments - Optional polygon approximation resolution.\n * @returns A sketch representation of the circle.\n */\n toSketch(segments?: number): Sketch;\n /**\n * Extrude the circle into a solid cylinder.\n *\n * @param height - Extrusion height.\n * @param segments - Optional tessellation resolution.\n * @returns A new shape with named cylinder faces.\n */\n extrude(height: number, segments?: number): Shape;\n /**\n * Create a circle from its center and radius.\n *\n * @param center - Circle center.\n * @param radius - Circle radius.\n * @returns A new circle.\n */\n static fromCenterAndRadius(center: Point2D, radius: number): Circle2D;\n /**\n * Create a circle from its center and diameter.\n *\n * @param center - Circle center.\n * @param diameter - Circle diameter.\n * @returns A new circle.\n */\n static fromDiameter(center: Point2D, diameter: number): Circle2D;\n}\n/**\n * Create an analytic 2D circle for measurement, construction, and extrusion.\n *\n * **Example**\n *\n * ```ts\n * const c = circle(0, 0, 25);\n * c.diameter; c.circumference; c.area;\n * c.pointAtAngle(90); // Point2D at top (90° CCW from +X)\n *\n * // Extrude to cylinder with named faces\n * const cyl = c.extrude(30);\n * cyl.face(\'top\'); // FaceRef (planar)\n * cyl.face(\'side\'); // FaceRef (curved)\n *\n * Circle2D.fromDiameter(point(0, 0), 50);\n * ```\n *\n * @param cx - Center X\n * @param cy - Center Y\n * @param radius - Radius\n * @returns A new `Circle2D`\n * @category 2D Entities\n */\ndeclare function circle(cx: number, cy: number, radius: number): Circle2D;\ntype RectSide = "top" | "bottom" | "left" | "right";\ntype RectVertex = "top-left" | "top-right" | "bottom-left" | "bottom-right";\n/**\n * A rectangle with named sides, vertices, and extrusion support.\n *\n * **Details**\n *\n * Sides are named based on the rectangle\'s local orientation at construction\n * time. Vertices go: bottom-left, bottom-right, top-right, top-left (CCW).\n *\n * Use `rect()` for the normal centered sketch primitive. Use `Rectangle2D`\n * when you need named sides/vertices, or an extrusion with tracked vertical\n * edges such as `vert-br` for `filletTrackedEdge()` / `chamferTrackedEdge()`.\n *\n * Extruding a `Rectangle2D` produces a `Shape` with named faces:\n * `top`, `bottom`, `side-left`, `side-right`, `side-top`, `side-bottom`.\n * These are accessible via the topology API (`.face()`, `.edge()`).\n *\n * **Example**\n *\n * ```ts\n * const r = Rectangle2D.fromDimensions(0, 0, 100, 60);\n * r.side(\'top\'); r.side(\'left\'); // Line2D\n * r.vertex(\'top-left\'); // Point2D\n * r.width; r.height; r.center;\n * const [d1, d2] = r.diagonals(); // [bl-tr, br-tl]\n *\n * r.toSketch(); // Sketch (for 2D operations)\n * r.extrude(20); // Shape with named faces\n *\n * Rectangle2D.fromCenterAndDimensions(point(50, 30), 100, 60);\n * Rectangle2D.from2Corners(point(0, 0), point(100, 60));\n * Rectangle2D.from3Points(p1, p2, p3); // free-angle rectangle\n * ```\n *\n * @category 2D Entities\n */\ndeclare class Rectangle2D {\n /** Vertices in order: bottom-left, bottom-right, top-right, top-left */\n readonly vertices: [\n Point2D,\n Point2D,\n Point2D,\n Point2D\n ];\n constructor(vertices: [\n Point2D,\n Point2D,\n Point2D,\n Point2D\n ]);\n /** Width of the rectangle. */\n get width(): number;\n /** Height of the rectangle. */\n get height(): number;\n /** Geometric center of the rectangle. */\n get center(): Point2D;\n /**\n * Return a named side of the rectangle.\n *\n * @param name - One of `top`, `bottom`, `left`, or `right`.\n * @returns The requested side as a line segment.\n */\n side(name: RectSide): Line2D;\n /**\n * Return a side by index.\n *\n * @param index - Side index: `0=bottom`, `1=right`, `2=top`, `3=left`.\n * @returns The requested side as a line segment.\n */\n sideAt(index: number): Line2D;\n /**\n * Return a named vertex of the rectangle.\n *\n * @param name - One of `bottom-left`, `bottom-right`, `top-right`, or `top-left`.\n * @returns The requested vertex.\n */\n vertex(name: RectVertex): Point2D;\n /**\n * Return the two diagonals of the rectangle.\n *\n * @returns `[bottom-left to top-right, bottom-right to top-left]`.\n */\n diagonals(): [\n Line2D,\n Line2D\n ];\n /**\n * Convert the rectangle to a sketch profile.\n *\n * @returns A sketch representation of the rectangle.\n */\n toSketch(): Sketch;\n /**\n * Return a translated rectangle.\n *\n * @param dx - Horizontal offset.\n * @param dy - Vertical offset.\n * @returns A shifted rectangle.\n */\n translate(dx: number, dy: number): Rectangle2D;\n /**\n * Create an axis-aligned rectangle from origin corner plus width and height.\n *\n * @param x - Bottom-left X.\n * @param y - Bottom-left Y.\n * @param width - Rectangle width.\n * @param height - Rectangle height.\n * @returns A new rectangle.\n */\n static fromDimensions(x: number, y: number, width: number, height: number): Rectangle2D;\n /**\n * Create a rectangle centered on a point.\n *\n * @param center - Rectangle center.\n * @param width - Rectangle width.\n * @param height - Rectangle height.\n * @returns A new rectangle.\n */\n static fromCenterAndDimensions(center: Point2D, width: number, height: number): Rectangle2D;\n /**\n * Create an axis-aligned rectangle from two opposite corners.\n *\n * @param p1 - First corner.\n * @param p2 - Opposite corner.\n * @returns A new rectangle.\n */\n static from2Corners(p1: Point2D, p2: Point2D): Rectangle2D;\n /**\n * Create a free-angle rectangle from three points.\n *\n * `p1` and `p2` define one edge, and `p3` chooses the perpendicular side.\n *\n * @param p1 - First edge point.\n * @param p2 - Second edge point.\n * @param p3 - Point indicating the rectangle height direction.\n * @returns A new rectangle.\n */\n static from3Points(p1: Point2D, p2: Point2D, p3: Point2D): Rectangle2D;\n /**\n * Extrude the rectangle into a solid prism with named topology.\n *\n * @param height - Extrusion height.\n * @param up - When false, extrude downward instead of upward.\n * @returns A new shape with named faces and edges.\n */\n extrude(height: number, up?: boolean): Shape;\n}\n/**\n * Identity function that returns degrees unchanged.\n *\n * Use for clarity when the unit of an angle value would otherwise be\n * ambiguous — e.g. `param("Angle", degrees(45))`.\n *\n * @param deg - Angle in degrees\n * @returns The same value unchanged\n * @category 2D Entities\n */\ndeclare function degrees(deg: number): number;\n/**\n * Convert radians to degrees.\n *\n * ForgeCAD\'s public API uses degrees throughout. Use this when you have a\n * radian value (e.g. from `Math.atan2`) that you want to express in degrees.\n *\n * @param rad - Angle in radians\n * @returns Equivalent angle in degrees\n * @category 2D Entities\n */\ndeclare function radians(rad: number): number;\ndeclare function polar(length: number, angleDeg: number, from?: [\n number,\n number\n]): [\n number,\n number\n];\ntype LineArg = LineId | Line2D;\ntype PointArg = PointId | Point2D;\ndeclare const Constraint: {\n /** Constrain two lines to be parallel. */\n makeParallel(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder;\n /** Constrain the signed angle from line `a` to line `b`. */\n enforceAngle(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg, angleDeg: number): ConstrainedSketchBuilder;\n /** Constrain a line to be horizontal. */\n horizontal(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder;\n /** Constrain a line to be vertical. */\n vertical(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder;\n /** Constrain two lines to have equal length. */\n equalLength(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder;\n /** Constrain the distance between two points. */\n distance(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg, value: number): ConstrainedSketchBuilder;\n /** Fix a point at a specific coordinate. */\n fix(builder: ConstrainedSketchBuilder, pt: PointArg, x: number, y: number): ConstrainedSketchBuilder;\n /** Constrain two points to occupy the same location. */\n coincident(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg): ConstrainedSketchBuilder;\n /** Constrain two lines to be perpendicular. */\n perpendicular(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder;\n /** Constrain the length of a line. */\n length(builder: ConstrainedSketchBuilder, line: LineArg, value: number): ConstrainedSketchBuilder;\n};\ntype FaceName = string;\ntype EdgeName = string;\ninterface FaceRef {\n name: FaceName;\n /** Normal direction of the face */\n normal: [\n number,\n number,\n number\n ];\n /** Center point of the face */\n center: [\n number,\n number,\n number\n ];\n /** Compiler-owned face query when available. */\n query?: FaceQueryRef;\n /** True when the face can host a 2D sketch placement frame */\n planar?: boolean;\n /** Face-local horizontal axis for planar faces */\n uAxis?: [\n number,\n number,\n number\n ];\n /** Face-local vertical axis for planar faces */\n vAxis?: [\n number,\n number,\n number\n ];\n /** Shared descendant-resolution metadata when this face is a semantic region/set. */\n descendant?: FaceDescendantMetadata;\n}\ninterface EdgeRef {\n name: EdgeName;\n /** Start point */\n start: [\n number,\n number,\n number\n ];\n /** End point */\n end: [\n number,\n number,\n number\n ];\n /** Compiler-owned edge query when available. */\n query?: EdgeQueryRef;\n}\ntype PlaneSpec = {\n origin: Vec3;\n normal: Vec3;\n} | {\n plane: "XY" | "XZ" | "YZ";\n offset?: number;\n} | {\n face: FaceRef;\n};\ntype Vec3$1 = [\n number,\n number,\n number\n];\ntype TpmsThicknessMode = "legacy-field-threshold" | "metric-approx";\ntype SdfFunctionConstant = number | string | boolean | null | SdfFunctionConstant[] | {\n [key: string]: SdfFunctionConstant;\n};\ntype SdfFunctionConstants = Record<string, SdfFunctionConstant>;\ninterface SdfSphereNode {\n kind: "sdf:sphere";\n radius: number;\n}\ninterface SdfBoxNode {\n kind: "sdf:box";\n halfExtents: Vec3$1;\n}\ninterface SdfCylinderNode {\n kind: "sdf:cylinder";\n height: number;\n radius: number;\n}\ninterface SdfTorusNode {\n kind: "sdf:torus";\n majorRadius: number;\n minorRadius: number;\n}\ninterface SdfCapsuleNode {\n kind: "sdf:capsule";\n height: number;\n radius: number;\n}\ninterface SdfConeNode {\n kind: "sdf:cone";\n height: number;\n radius: number;\n}\ninterface SdfPolylineSweepNode {\n kind: "sdf:polylineSweep";\n /** Ordered path samples in world/local SDF space. */\n points: Vec3$1[];\n /** Radius at each corresponding point. */\n radii: number[];\n /** Smooth blend radius between adjacent tapered segments. */\n blend: number;\n}\ninterface SdfUnionNode {\n kind: "sdf:union";\n children: SdfNode[];\n}\ninterface SdfDifferenceNode {\n kind: "sdf:difference";\n /** First child is the base; subsequent children are subtracted. */\n children: SdfNode[];\n}\ninterface SdfIntersectionNode {\n kind: "sdf:intersection";\n children: SdfNode[];\n}\ninterface SdfSmoothUnionNode {\n kind: "sdf:smoothUnion";\n children: SdfNode[];\n radius: number;\n}\ninterface SdfSmoothDifferenceNode {\n kind: "sdf:smoothDifference";\n children: SdfNode[];\n radius: number;\n}\ninterface SdfSmoothIntersectionNode {\n kind: "sdf:smoothIntersection";\n children: SdfNode[];\n radius: number;\n}\ninterface SdfMorphNode {\n kind: "sdf:morph";\n a: SdfNode;\n b: SdfNode;\n /** 0 = fully a, 1 = fully b */\n t: number;\n}\ninterface SdfTranslateNode {\n kind: "sdf:translate";\n child: SdfNode;\n offset: Vec3$1;\n}\ninterface SdfRotateNode {\n kind: "sdf:rotate";\n child: SdfNode;\n /** Euler angles in degrees (X, Y, Z) */\n degrees: Vec3$1;\n}\ninterface SdfScaleNode {\n kind: "sdf:scale";\n child: SdfNode;\n factor: number;\n}\ninterface SdfTwistNode {\n kind: "sdf:twist";\n child: SdfNode;\n /** Total twist angle in degrees over the full height of the shape */\n degreesPerUnit: number;\n}\ninterface SdfBendNode {\n kind: "sdf:bend";\n child: SdfNode;\n /** Bend radius — larger = gentler bend */\n radius: number;\n}\ninterface SdfRepeatNode {\n kind: "sdf:repeat";\n child: SdfNode;\n /** Spacing between repetitions [x, y, z]. 0 = no repetition on that axis. */\n spacing: Vec3$1;\n /** Max repetition count per side. 0 = infinite. */\n count: Vec3$1;\n}\ninterface SdfShellNode {\n kind: "sdf:shell";\n child: SdfNode;\n thickness: number;\n}\ninterface SdfDisplaceNode {\n kind: "sdf:displace";\n child: SdfNode;\n /** Serialized as a function body string: receives (x, y, z) and returns a number. */\n functionBody: string;\n /** Named constants injected as additional function parameters (avoids closure serialization issues). */\n constants?: SdfFunctionConstants;\n}\ninterface SdfSurfaceDisplaceNode {\n kind: "sdf:surfaceDisplace";\n child: SdfNode;\n /**\n * Function body string: receives (u, v) in surface millimeters and returns a height\n * displacement value. Negative = into surface, positive = outward.\n */\n patternBody: string;\n /** Named constants injected as additional function parameters. */\n constants?: SdfFunctionConstants;\n /** Override auto-detected UV mode. Undefined or \'auto\' = auto-detect from child tree. */\n uvMode?: "auto" | "sphere" | "cylinder" | "torus" | "triplanar";\n /** Triplanar blend sharpness (only used when UV mode is triplanar). Default: 4. */\n triplanarSharpness?: number;\n}\ninterface SdfOnionNode {\n kind: "sdf:onion";\n child: SdfNode;\n /** Number of concentric layers */\n layers: number;\n thickness: number;\n}\ninterface SdfGyroidNode {\n kind: "sdf:gyroid";\n cellSize: number;\n thickness: number;\n thicknessMode?: TpmsThicknessMode;\n}\ninterface SdfSchwarzPNode {\n kind: "sdf:schwarzP";\n cellSize: number;\n thickness: number;\n thicknessMode?: TpmsThicknessMode;\n}\ninterface SdfDiamondNode {\n kind: "sdf:diamond";\n cellSize: number;\n thickness: number;\n thicknessMode?: TpmsThicknessMode;\n}\ninterface SdfLidinoidNode {\n kind: "sdf:lidinoid";\n cellSize: number;\n thickness: number;\n thicknessMode?: TpmsThicknessMode;\n}\ninterface SdfSpatialBlendNode {\n kind: "sdf:spatialBlend";\n a: SdfNode;\n b: SdfNode;\n /** Function body returning 0..1. 0 = fully a, 1 = fully b. */\n functionBody: string;\n constants?: SdfFunctionConstants;\n}\ninterface SdfNoiseNode {\n kind: "sdf:noise";\n /** Spatial frequency — smaller = larger features. */\n scale: number;\n /** Peak displacement amplitude. */\n amplitude: number;\n /** Number of octaves for fractal Brownian motion (1 = plain simplex). */\n octaves: number;\n /** Seed for deterministic variation. 0 = default permutation. */\n seed: number;\n}\ninterface SdfVoronoiNode {\n kind: "sdf:voronoi";\n /** Size of each Voronoi cell in world units. */\n cellSize: number;\n /** Wall thickness between cells. */\n wallThickness: number;\n /** Seed for deterministic variation. */\n seed: number;\n /**\n * When set, enables surface-aware mode using IQ two-pass with membrane suppression.\n * The child SDF\'s gradient is used to estimate the surface normal, and walls aligned\n * with that normal are suppressed. This is the child SDF tree whose gradient provides\n * the surface normal for filtering.\n */\n surfaceChild?: SdfNode;\n /**\n * Membrane suppression threshold (0..1). Higher = more aggressive suppression.\n * 0 = no filtering, 1 = suppress all walls. Default: 0.7.\n */\n suppressionThreshold?: number;\n}\ninterface SdfCustomNode {\n kind: "sdf:custom";\n /** Function body string: receives (x, y, z) and returns signed distance. */\n functionBody: string;\n /** GLSL expression generated from the same source expression, when raymarchable. */\n shaderBody?: string;\n /** Why shaderBody could not be generated. */\n shaderUnsupportedReason?: string;\n /** Conservative maximum shader raymarch step for this field. */\n raymarchStepLimit?: number;\n /** Optional divisor applied to shader distance for non-distance fields. */\n raymarchLipschitz?: number;\n bounds: {\n min: Vec3$1;\n max: Vec3$1;\n };\n /** Named constants injected as additional function parameters (avoids closure serialization issues). */\n constants?: SdfFunctionConstants;\n}\ntype SdfNode = SdfSphereNode | SdfBoxNode | SdfCylinderNode | SdfTorusNode | SdfCapsuleNode | SdfConeNode | SdfPolylineSweepNode | SdfUnionNode | SdfDifferenceNode | SdfIntersectionNode | SdfSmoothUnionNode | SdfSmoothDifferenceNode | SdfSmoothIntersectionNode | SdfMorphNode | SdfTranslateNode | SdfRotateNode | SdfScaleNode | SdfTwistNode | SdfBendNode | SdfRepeatNode | SdfShellNode | SdfDisplaceNode | SdfSurfaceDisplaceNode | SdfOnionNode | SdfGyroidNode | SdfSchwarzPNode | SdfDiamondNode | SdfLidinoidNode | SdfSpatialBlendNode | SdfNoiseNode | SdfVoronoiNode | SdfCustomNode;\ninterface SdfBounds {\n min: Vec3$1;\n max: Vec3$1;\n}\ntype SdfMeshingQuality = "draft" | "preview" | "export";\ninterface SdfToShapeOptions {\n /** Target mesh edge length. Smaller = finer mesh. Overrides quality-derived resolution. */\n edgeLength?: number;\n /** Override auto-computed bounds. Strongly recommended for infinite/repeated fields. */\n bounds?: {\n min: Vec3$1;\n max: Vec3$1;\n };\n /** Coarse quality preset. Default: \'preview\'. */\n quality?: SdfMeshingQuality;\n /** Preferred absolute surface tolerance in millimeters. */\n tolerance?: number;\n /** Smallest feature that should survive meshing, in millimeters. */\n minFeatureSize?: number;\n /** Simplification control. `false` disables, `true` and `\'safe\'` use topology-validated simplification. */\n simplify?: boolean | "safe";\n /** Optional post-extraction triangle budget. */\n maxTriangles?: number;\n /** Optional pre-extraction grid-point budget. Default is browser-safe. */\n maxGridPoints?: number;\n /** Lower clamp for resolved edge length. Default: 0.15mm. */\n minEdgeLength?: number;\n /** Log resolved meshing settings and backend extraction timings. */\n diagnostics?: boolean;\n}\ndeclare const SHEET_METAL_EDGES: readonly [\n "top",\n "right",\n "bottom",\n "left"\n];\ntype SheetMetalEdge = (typeof SHEET_METAL_EDGES)[number];\ntype SheetMetalPlanarRegionName = "panel" | `flange-${SheetMetalEdge}`;\ntype SheetMetalRegionName = SheetMetalPlanarRegionName | `bend-${SheetMetalEdge}`;\ninterface SheetMetalBendAllowance {\n kind: "k-factor";\n kFactor: number;\n}\ninterface SheetMetalPanelSpec {\n width: number;\n height: number;\n}\ninterface SheetMetalFlangeSpec {\n edge: SheetMetalEdge;\n length: number;\n angleDeg: number;\n}\ninterface SheetMetalCornerReliefSpec {\n kind: "rect";\n size: number;\n}\ninterface SheetMetalModel {\n panel: SheetMetalPanelSpec;\n thickness: number;\n bendRadius: number;\n bendAllowance: SheetMetalBendAllowance;\n cornerRelief: SheetMetalCornerReliefSpec;\n flanges: SheetMetalFlangeSpec[];\n}\ntype SurfaceContinuity = "G0" | "G1" | "G2";\ntype SurfaceFillStyle = "coons" | "curved" | "stretch";\ninterface TransformationStep {\n kind: string;\n description: string;\n details?: Record<string, unknown>;\n}\ntype TimelineEntryCategory = "primitive" | "sketch" | "modifier" | "boolean" | "transform";\ninterface TimelineEntry {\n kind: string;\n label: string;\n summary: string;\n category: TimelineEntryCategory;\n}\ninterface FaceTransformationHistory {\n faceName: string;\n origin: {\n operation: string;\n owner?: ShapeQueryOwner;\n };\n transformations: TransformationStep[];\n query?: FaceQueryRef;\n /** Ordered list of operations that built this shape, oldest first. */\n timeline: TimelineEntry[];\n}\n/**\n * FaceQuery — declarative face selector types for geometric face matching.\n *\n * `FaceSelector` is a string label name. `FaceQuery` is a structured\n * object that matches faces by geometry (normal, nearest, area, etc.)\n * and is used internally by compile-plan face resolution.\n */\ninterface FaceQuery {\n /** Filter by face normal direction (cosine similarity > 0.9998) */\n normal?: [\n number,\n number,\n number\n ];\n /** Pick face whose centroid is nearest to this point (XY or XYZ) */\n nearest?: [\n number,\n number\n ] | [\n number,\n number,\n number\n ];\n /** Pick face containing/nearest to this world point */\n at?: [\n number,\n number,\n number\n ];\n /** Disambiguation when multiple faces match */\n pick?: "largest" | "smallest" | "max-x" | "max-y" | "max-z" | "min-x" | "min-y" | "min-z";\n /** Filter by area range (mm²) */\n area?: {\n min?: number;\n max?: number;\n };\n /** Only match planar faces (default: true) */\n planar?: boolean;\n}\n/** A face label name. Pass to `shape.face()` to look up a user-authored label. */\ntype FaceSelector = string;\ntype PlacementReferenceKind = "points" | "edges" | "surfaces" | "objects";\ninterface PlacementEdgeRef {\n start: Vec3;\n end: Vec3;\n}\ninterface PlacementSurfaceRef {\n center: Vec3;\n normal: Vec3;\n}\ninterface PlacementObjectRef {\n min: Vec3;\n max: Vec3;\n}\ninterface PlacementReferences {\n points: Record<string, Vec3>;\n edges: Record<string, PlacementEdgeRef>;\n surfaces: Record<string, PlacementSurfaceRef>;\n objects: Record<string, PlacementObjectRef>;\n}\ntype PlacementObjectInput = PlacementObjectRef | {\n min: [\n number,\n number,\n number\n ];\n max: [\n number,\n number,\n number\n ];\n} | {\n boundingBox(): {\n min: number[];\n max: number[];\n };\n} | {\n _bbox(): {\n min: number[];\n max: number[];\n };\n};\ninterface PlacementReferenceInput {\n points?: Record<string, [\n number,\n number,\n number\n ]>;\n edges?: Record<string, PlacementEdgeRef>;\n surfaces?: Record<string, PlacementSurfaceRef>;\n objects?: Record<string, PlacementObjectInput>;\n}\ntype PlacementAnchorLike = Anchor3D | string;\ntype GroupChild = Shape | Sketch | ShapeGroup;\ninterface NamedGroupChild {\n name: string;\n shape?: Shape | ShapeGroup;\n sketch?: Sketch;\n group?: GroupInput[] | ShapeGroup;\n}\ntype GroupInput = GroupChild | NamedGroupChild;\ndeclare class ShapeGroup {\n readonly children: GroupChild[];\n readonly childNames: Array<string | undefined>;\n constructor(children: GroupChild[], childNames?: Array<string | undefined>);\n /** Return the optional name of the child at `index`. */\n childName(index: number): string | undefined;\n /**\n * Return the named child by name. Throws if not found.\n * Useful when importing a multipart group and working on components individually.\n */\n child(name: string): GroupChild;\n /** Apply fn to all children, producing a new ShapeGroup that also copies placement refs. */\n private mapChildren;\n /** Apply fn to all children and also transform placement refs by the given matrix. */\n private mapChildrenTransform;\n /** Return a deep-cloned ShapeGroup tree (refs copied). */\n clone(): ShapeGroup;\n /** Alias for clone() */\n duplicate(): ShapeGroup;\n /** Move the entire group by (x, y, z). All children move together as a unit. */\n translate(x: number, y: number, z: number): ShapeGroup;\n /** Compute combined bounding box of all 3D children */\n private _bbox;\n private _bboxCenter;\n /** Return the combined 3D bounding box of all children. */\n boundingBox(): {\n min: [\n number,\n number,\n number\n ];\n max: [\n number,\n number,\n number\n ];\n };\n private resolveRotatePoint;\n /** Move the group so its bounding-box min corner lands at the given coordinate. */\n moveTo(x: number, y: number, z: number): ShapeGroup;\n /** Move the group relative to another part\'s bounding-box min corner. */\n moveToLocal(target: Shape | ShapeGroup, x: number, y: number, z: number): ShapeGroup;\n /**\n * 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\n * on the target. `selfAnchor` selects the anchor on this group to align.\n */\n attachTo(target: Shape | ShapeGroup, targetAnchor: Anchor3D | string, selfAnchor?: Anchor3D, offset?: [\n number,\n number,\n number\n ]): ShapeGroup;\n /**\n * Place this group on a face of a parent shape.\n * See Shape.onFace() for full documentation.\n */\n onFace(parent: Shape | ShapeGroup, face: "front" | "back" | "left" | "right" | "top" | "bottom", opts?: {\n u?: number;\n v?: number;\n protrude?: number;\n }): ShapeGroup;\n /** Rotate the group around an arbitrary axis through the origin. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Rotate the group around the X axis. */\n rotateX(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Rotate the group around the Y axis. */\n rotateY(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Rotate the group around the Z axis. */\n rotateZ(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Rotate around an arbitrary axis, optionally through a pivot point. */\n rotateAroundAxis(axis: [\n number,\n number,\n number\n ], angleDeg: number, pivot?: [\n number,\n number,\n number\n ]): ShapeGroup;\n /**\n * Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point.\n * ShapeGroup string points use built-in anchors only.\n */\n rotateAroundTo(axis: [\n number,\n number,\n number\n ], pivot: [\n number,\n number,\n number\n ], movingPoint: Anchor3D | [\n number,\n number,\n number\n ], targetPoint: Anchor3D | [\n number,\n number,\n number\n ], options?: RotateAroundToOptions): ShapeGroup;\n /** Reorient the group so its local Z axis points along `direction`. */\n pointAlong(direction: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Apply a 4x4 transform matrix or `Transform` to all 3D children. */\n transform(m: Mat4 | Transform): ShapeGroup;\n /** Scale uniformly or per-axis from the group\'s bounding-box center. */\n scale(v: number | [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Scale uniformly or per-axis from an explicit pivot point. */\n scaleAround(pivot: [\n number,\n number,\n number\n ], v: number | [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Mirror across a plane through the group\'s bounding-box center. */\n mirror(normal: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Mirror across a plane through an explicit point. */\n mirrorThrough(point: [\n number,\n number,\n number\n ], normal: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Return a copy of the group with the given display color applied to each child. */\n color(hex: string): ShapeGroup;\n /**\n * Attach named placement references to this group.\n * References survive normal transforms (translate/rotate/scale/mirror/transform).\n *\n * ```javascript\n * const bracket = group(\n * { name: \'Left\', shape: leftShape },\n * { name: \'Right\', shape: rightShape },\n * ).withReferences({\n * points: { mountCenter: [0, 0, 0] },\n * });\n * ```\n */\n withReferences(refs: PlacementReferenceInput): ShapeGroup;\n /** List named placement references carried by this group. */\n referenceNames(kind?: PlacementReferenceKind): string[];\n /** Backward-compatible alias for `withConnectors()`. @deprecated Use `withConnectors()` instead. */\n withPorts(ports: Record<string, PortInput>): ShapeGroup;\n /** Backward-compatible alias for `connectorNames()`. @deprecated Use `connectorNames()` instead. */\n portNames(): string[];\n /**\n * Resolve a named placement reference or built-in Anchor3D to a 3D point.\n * Named refs take priority over built-in anchors.\n */\n referencePoint(ref: PlacementAnchorLike): [\n number,\n number,\n number\n ];\n /**\n * Translate the group so the given anchor or reference lands on the target coordinate.\n *\n * Accepts any built-in anchor name (`\'bottom\'`, `\'center\'`, `\'top-front-left\'`, etc.)\n * or a custom placement reference attached via `withReferences()`.\n *\n * ```javascript\n * // Ground a group — put its bottom at Z = 0\n * assembly.placeReference(\'bottom\', [0, 0, 0])\n *\n * // Use a custom reference from a multi-file part\n * const placed = require(\'./bracket-assembly.forge.js\').group\n * .placeReference(\'mountCenter\', [0, 0, 50]);\n * ```\n */\n placeReference(ref: PlacementAnchorLike, target: [\n number,\n number,\n number\n ], offset?: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Attach named connectors — attachment points that survive transforms.\n * Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching). */\n withConnectors(connectors: Record<string, ConnectorInput>): ShapeGroup;\n /** List all connector names, including "ChildName.connectorName" from named children. */\n connectorNames(): string[];\n /** Get all connectors of a given type, including from named children. */\n connectorsByType(type: string): Array<{\n name: string;\n port: ConnectorDef;\n }>;\n /** Distance between two connector origins on this group (supports dotted child paths). */\n connectorDistance(nameA: string, nameB: string): number;\n /** Get measurements metadata from a connector (supports dotted child paths). */\n connectorMeasurements(name: string): Record<string, number | string>;\n /**\n * Position this group by matching connectors to a target.\n * Connector names support dotted paths into named children: "ChildName.connectorName".\n *\n * Overloads:\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 matchTo(targetOrPairs: Shape | ShapeGroup | Array<[\n Shape | ShapeGroup,\n string,\n string\n ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): ShapeGroup;\n}\n/**\n * Group multiple shapes/sketches for joint transforms without merging into a single mesh.\n *\n * Unlike union(), child colors and individual identities are preserved.\n * Children can be plain shapes, named descriptors ({ name, shape/sketch/group }),\n * or nested groups. The returned ShapeGroup supports all Shape transforms\n * (translate, rotate, etc.).\n *\n * **Local coordinate pattern:** Build child parts at the origin (local coordinates),\n * then group and translate once to place the whole assembly. This eliminates the\n * error-prone pattern of manually adding parent offsets to every sub-part.\n *\n * @example\n * // BAD — every sub-part repeats the parent\'s global offset\n * const unitX = 0, unitY = -18, unitZ = 70;\n * const body = roundedBox(100, 20, 32, 4).translate(unitX, unitY, unitZ);\n * const panel = box(98, 2, 18).translate(unitX, unitY - 12, unitZ + 4);\n * const louver = box(88, 2, 6).translate(unitX, unitY - 14, unitZ - 11);\n *\n * // GOOD — build at origin, group, translate once\n * const body = roundedBox(100, 20, 32, 4);\n * const panel = box(98, 2, 18).translate(0, -12, 4);\n * const louver = box(88, 2, 6).translate(0, -14, -11);\n * const indoorUnit = group(\n * { name: \'Body\', shape: body },\n * { name: \'Panel\', shape: panel },\n * { name: \'Louver\', shape: louver },\n * ).translate(0, -18, 70);\n */\ndeclare function group(...items: GroupInput[]): ShapeGroup;\ninterface EdgeSegment {\n /** Stable index within the extraction (deterministic for a given mesh). */\n index: number;\n start: Vec3;\n end: Vec3;\n midpoint: Vec3;\n /** Normalized direction from start → end. */\n direction: Vec3;\n length: number;\n /** Dihedral angle in degrees (0 = coplanar, 180 = knife edge). */\n dihedralAngle: number;\n /** true = outside corner (convex), false = inside corner (concave). */\n convex: boolean;\n /** Normal of first adjacent face. */\n normalA: Vec3;\n /** Normal of second adjacent face (same as normalA for boundary edges). */\n normalB: Vec3;\n /** true if this is a boundary (unmatched) edge — unusual for closed solids. */\n boundary: boolean;\n}\n/** Options for `edgesOfFace()`. */\ninterface EdgesOfOptions {\n /** Exclude edges shared with these named faces. */\n exclude?: string | string[];\n /** Additional geometric filter: only convex edges. */\n convex?: boolean;\n /** Additional geometric filter: only concave edges. */\n concave?: boolean;\n /** Minimum edge length filter. */\n minLength?: number;\n}\ninterface SeatIntoOptions {\n /** Movement axis. Default: inverted face normal (points into target). */\n along?: [\n number,\n number,\n number\n ];\n /** How deep to embed. \'full\' = entire face inside. \'flush\' = nearest point touches. number = mm past flush. Default: \'full\'. */\n depth?: "full" | "flush" | number;\n /** Standoff gap in mm. Positive = gap between face and target. Negative = extra penetration. Default: 0. */\n gap?: number;\n}\ntype SdfFunctionSource = string | ((x: number, y: number, z: number, ...constants: number[]) => number);\ndeclare class SurfacePattern {\n /** Function body: receives (u, v) in surface mm, returns height displacement. */\n readonly body: string;\n /** Named constants injected into the function. */\n readonly constants?: Record<string, number>;\n constructor(body: string, constants?: Record<string, number>);\n}\ninterface SurfaceDisplaceOptions {\n /** Override auto-detected UV mode. Default: \'auto\' (detects from SDF tree). */\n uv?: "auto" | "sphere" | "cylinder" | "torus" | "triplanar";\n /** Triplanar blend sharpness — higher = crisper transitions. Default: 4. Only used in triplanar mode. */\n triplanarSharpness?: number;\n}\ninterface SdfFunctionOptions {\n /** Finite design bounds for this opaque custom field. */\n bounds: SdfBounds | [\n Vec3$1,\n Vec3$1\n ];\n /** Named finite constants referenced from the expression body. */\n constants?: SdfFunctionConstants;\n /** Conservative maximum raymarch step for native preview. */\n maxStep?: number;\n /** Optional divisor for non-distance fields. Values above 1 slow preview marching. */\n lipschitz?: number;\n}\ninterface SdfVisualMetadata {\n colorHex?: string;\n materialProps?: ShapeMaterialProps;\n bounds?: SdfBounds;\n}\ntype SculptMaterialPreset = "ceramic" | "porcelain" | "soft-rubber" | "glass" | "mint-glass" | "candy" | "strawberry-gel" | "metal" | "clay";\ntype SculptPolishInput = SculptMaterialPreset | (ShapeMaterialProps & {\n color?: string;\n});\ndeclare function knownSculptMaterialPresets(): SculptMaterialPreset[];\n/**\n * An immutable SDF expression. Supports SDF-specific operations (smooth booleans,\n * domain warps, etc.), can be returned directly for native preview, and converts\n * to a ForgeCAD Shape via `.toShape()` when materialization is needed.\n */\ndeclare class SdfShape {\n /** @internal */\n readonly _node: SdfNode;\n /** @internal */\n readonly _visual: SdfVisualMetadata;\n /** @internal */\n constructor(node: SdfNode, visual?: SdfVisualMetadata);\n /** Display color carried by this implicit leaf. */\n get colorHex(): string | undefined;\n /** Display material carried by this implicit leaf. */\n get materialProps(): ShapeMaterialProps | undefined;\n /** Explicit bounds carried by this implicit leaf, if any. */\n get explicitBounds(): SdfBounds | undefined;\n private withNode;\n private withVisual;\n /** Clone this SDF expression and its visual metadata. */\n clone(): SdfShape;\n /** Alias for clone(). */\n duplicate(): SdfShape;\n /**\n * Mesh this SDF into a ForgeCAD Shape through ForgeCAD\'s Surface Nets pipeline.\n * Once converted, the result is a regular Shape — booleans, transforms, export all work.\n */\n toShape(options?: SdfToShapeOptions): Shape;\n /** Set the display color for this implicit leaf. */\n color(value: string | undefined): SdfShape;\n /** Set PBR display material properties for this implicit leaf. */\n material(props: ShapeMaterialProps): SdfShape;\n /** Set explicit preview/meshing bounds for this implicit leaf. */\n bounds(bounds: SdfBounds | [\n Vec3$1,\n Vec3$1\n ]): SdfShape;\n bounds(min: Vec3$1, max: Vec3$1): SdfShape;\n /** Sculpt-style alias for translate(). */\n at(x: number, y: number, z: number): SdfShape;\n /** Sculpt-style alias for translate(). */\n move(x: number, y: number, z: number): SdfShape;\n /** Sculpt-style alias for rotateZ(). */\n spin(angleDeg: number): SdfShape;\n /** Sculpt-style tilt around X, Y, Z, or a custom axis. */\n tilt(angleDeg: number, axis?: "x" | "y" | "z" | Vec3$1): SdfShape;\n /** Sculpt-style rounded-box helper. Currently applies directly to primitive SDF boxes. */\n round(radius: number): SdfShape;\n /** Sculpt-style smooth blend with another implicit shape. */\n blend(other: SdfShape, options?: number | {\n radius?: number;\n }): SdfShape;\n /** Sculpt-style alias for blend(). */\n goop(other: SdfShape, options?: number | {\n radius?: number;\n }): SdfShape;\n /** Sculpt-style smooth carve/subtract. */\n carve(other: SdfShape, options?: number | {\n radius?: number;\n }): SdfShape;\n /** Sculpt-style smooth intersection/keep operation. */\n keep(other: SdfShape, options?: number | {\n radius?: number;\n }): SdfShape;\n /** Apply a Sculpt material preset or direct material props. */\n polish(input?: SculptPolishInput): SdfShape;\n /** SDF union (sharp). */\n union(...others: SdfShape[]): SdfShape;\n /** SDF difference (sharp) — subtracts others from this. */\n subtract(...others: SdfShape[]): SdfShape;\n /** SDF intersection (sharp). */\n intersect(...others: SdfShape[]): SdfShape;\n /** Clip this SDF to an explicit box-shaped design space. */\n clipBox(x: number, y: number, z: number): SdfShape;\n /** Smooth union — blends shapes together with a smooth radius. */\n smoothUnion(other: SdfShape, radius: number): SdfShape;\n /** Smooth difference — smoothly carves other from this. */\n smoothSubtract(other: SdfShape, radius: number): SdfShape;\n /** Smooth intersection — smoothly intersects. */\n smoothIntersect(other: SdfShape, radius: number): SdfShape;\n /** Morph between this shape and another. t=0 → this, t=1 → other. */\n morph(other: SdfShape, t: number): SdfShape;\n /** Translate this SDF by the given offsets in millimeters. */\n translate(x: number, y: number, z: number): SdfShape;\n /** Rotate around an arbitrary axis through the origin. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number): SdfShape;\n /** Rotate around the X axis by the given angle in degrees. */\n rotateX(angleDeg: number): SdfShape;\n /** Rotate around the Y axis by the given angle in degrees. */\n rotateY(angleDeg: number): SdfShape;\n /** Rotate around the Z axis by the given angle in degrees. */\n rotateZ(angleDeg: number): SdfShape;\n /** Uniformly scale this SDF around the origin. */\n scale(factor: number): SdfShape;\n /** Twist around the Z axis. */\n twist(degreesPerUnit: number): SdfShape;\n /** Bend around the Z axis with given radius. */\n bend(radius: number): SdfShape;\n /** Repeat in space. Spacing of 0 on an axis means no repetition. Count of 0 = infinite. */\n repeat(spacing: Vec3$1, count?: Vec3$1): SdfShape;\n /** Hollow out, keeping only a shell of given thickness. */\n shell(thickness: number): SdfShape;\n /**\n * Displace the surface by a function of position, or by a pattern SdfShape.\n *\n * ```js\n * // Function displacement\n * shape.displace((x, y, z) => Math.sin(x) * 0.5)\n *\n * // Pattern displacement (e.g. basketWeave)\n * shape.displace(sdf.basketWeave({ threads: 16, spacing: 3 }))\n * ```\n */\n displace(fn: ((x: number, y: number, z: number) => number) | SdfShape, constants?: Record<string, number>): SdfShape;\n /**\n * Displace the surface using a 2D pattern in surface-local UV coordinates.\n *\n * Automatically detects the shape\'s UV parametrization (sphere, cylinder, torus)\n * from the SDF tree. Falls back to triplanar mapping for arbitrary shapes.\n *\n * UV coordinates are in **surface millimeters** — patterns defined with `spacing: 3`\n * always produce 3mm spacing, regardless of shape size.\n *\n * ```js\n * // Surface-following basket weave — auto-detects sphere UV\n * sdf.sphere(27).shell(3)\n * .surfaceDisplace(sdf.basketWeave({ spacing: 3, depth: 0.8 }))\n * .toShape()\n *\n * // Custom 2D pattern via function\n * shape.surfaceDisplace((u, v) => -Math.sin(u * 2) * 0.3)\n * ```\n */\n surfaceDisplace(pattern: SurfacePattern | ((u: number, v: number) => number), options?: SurfaceDisplaceOptions): SdfShape;\n /** Create concentric onion layers. */\n onion(layers: number, thickness: number): SdfShape;\n}\ndeclare function sphere(radius: number): SdfShape;\ndeclare function box(x: number, y: number, z: number): SdfShape;\ndeclare function cylinder(height: number, radius: number): SdfShape;\ndeclare function torus(majorRadius: number, minorRadius: number): SdfShape;\ndeclare function capsule(height: number, radius: number): SdfShape;\ndeclare function cone(height: number, radius: number): SdfShape;\ndeclare function smoothUnion(a: SdfShape, b: SdfShape, options: {\n radius: number;\n}): SdfShape;\ndeclare function smoothDifference(a: SdfShape, b: SdfShape, options: {\n radius: number;\n}): SdfShape;\ndeclare function smoothIntersection(a: SdfShape, b: SdfShape, options: {\n radius: number;\n}): SdfShape;\ndeclare function morph(a: SdfShape, b: SdfShape, t: number): SdfShape;\ninterface BlendOptions {\n /** Optional named constants accessible in the blend function. */\n constants?: Record<string, number>;\n}\ndeclare function blend(a: SdfShape, b: SdfShape, fn: (x: number, y: number, z: number) => number, options?: BlendOptions): SdfShape;\ninterface TpmsOptions {\n cellSize: number;\n /**\n * Legacy dimensionless field threshold. Kept for compatibility.\n * Prefer `wallThickness` for approximate millimeter units.\n */\n thickness?: number;\n /** Approximate physical wall thickness in millimeters. */\n wallThickness?: number;\n /** Override TPMS thickness interpretation. Defaults to metric when `wallThickness` is used, legacy when `thickness` is used. */\n tpmsThicknessMode?: TpmsThicknessMode;\n}\ninterface TpmsBlockOptions extends TpmsOptions {\n type?: "gyroid" | "schwarzP" | "diamond" | "lidinoid";\n size: Vec3$1;\n}\ndeclare function gyroid(options: TpmsOptions): SdfShape;\ndeclare function schwarzP(options: TpmsOptions): SdfShape;\ndeclare function diamond(options: TpmsOptions): SdfShape;\ndeclare function lidinoid(options: TpmsOptions): SdfShape;\ndeclare function withinBox(shape: SdfShape, options: {\n size: Vec3$1;\n}): SdfShape;\ndeclare function tpmsBlock(options: TpmsBlockOptions): SdfShape;\ninterface NoiseOptions {\n /** Spatial frequency — smaller = larger features. Default: 0.1 */\n scale?: number;\n /** Peak displacement amplitude. Default: 1 */\n amplitude?: number;\n /** fBm octaves (1 = plain simplex, higher = more detail). Default: 1 */\n octaves?: number;\n /** Seed for deterministic variation. Default: 0 */\n seed?: number;\n}\ndeclare function noise(options?: NoiseOptions): SdfShape;\ninterface VoronoiOptions {\n /** Size of each Voronoi cell in world units. Default: 10 */\n cellSize?: number;\n /** Wall thickness between cells. Default: 1 */\n wallThickness?: number;\n /** Seed for deterministic variation. Default: 0 */\n seed?: number;\n /**\n * Projection weight for membrane suppression (0..1). Controls how much of\n * the surface-normal distance component is removed from Voronoi cell distances.\n * 0 = no projection (classic 3D voronoi with membranes).\n * 1 = full tangent-plane projection (pure 2D pattern on surface).\n * Default: 0.85. Only active when voronoi is intersected with another shape.\n */\n suppressionThreshold?: number;\n}\ndeclare function voronoi(options?: VoronoiOptions): SdfShape;\ninterface HoneycombOptions {\n /** Size of each hex cell. Default: 8 */\n cellSize?: number;\n /** Wall thickness. Default: 1 */\n wallThickness?: number;\n}\ndeclare function honeycomb(options?: HoneycombOptions): SdfShape;\ninterface WavesOptions {\n /** Distance between wave peaks. Default: 10 */\n wavelength?: number;\n /** Height of waves. Default: 1 */\n amplitude?: number;\n /** Axis along which waves propagate: \'x\', \'y\', or \'z\'. Default: \'x\' */\n axis?: "x" | "y" | "z";\n}\ndeclare function waves(options?: WavesOptions): SdfShape;\ninterface KnurlOptions {\n /** Distance between knurl ridges. Default: 3 */\n pitch?: number;\n /** Depth of knurl grooves. Default: 0.5 */\n depth?: number;\n /** Helix angle in degrees. Default: 30 */\n angle?: number;\n}\ndeclare function knurl(options?: KnurlOptions): SdfShape;\ninterface PerforatedOptions {\n /** Hole radius. Default: 3 */\n radius?: number;\n /** Center-to-center spacing. Default: 8 */\n spacing?: number;\n}\ndeclare function perforated(options?: PerforatedOptions): SdfShape;\ninterface ScalesOptions {\n /** Scale diameter. Default: 5 */\n size?: number;\n /** How much scales protrude. Default: 0.8 */\n depth?: number;\n}\ndeclare function scales(options?: ScalesOptions): SdfShape;\ninterface BrickOptions {\n /** Brick width. Default: 10 */\n width?: number;\n /** Brick height. Default: 5 */\n height?: number;\n /** Mortar groove depth. Default: 0.5 */\n depth?: number;\n /** Mortar gap width. Default: 1 */\n mortar?: number;\n}\ndeclare function brick(options?: BrickOptions): SdfShape;\ninterface WeaveOptions {\n /** Thread center-to-center spacing (for intersection patterns). Default: 5 */\n spacing?: number;\n /** Thread half-width. Default: 1 */\n threadRadius?: number;\n}\ndeclare function weave(options?: WeaveOptions): SdfShape;\ninterface BasketWeaveOptions {\n /** Spacing between threads in mm (both directions). Default: 3 */\n spacing?: number;\n /** Thread width in mm. Default: 1.5 */\n threadWidth?: number;\n /** Thread protrusion depth in mm. Default: 0.8 */\n depth?: number;\n}\ndeclare function basketWeave(options?: BasketWeaveOptions): SurfacePattern;\ndeclare function fromFunction(fn: SdfFunctionSource, options: SdfFunctionOptions): SdfShape;\ndeclare function twist(shape: SdfShape, degreesPerUnit: number): SdfShape;\ndeclare function bend(shape: SdfShape, radius: number): SdfShape;\ndeclare function repeat(shape: SdfShape, spacing: Vec3$1, count?: Vec3$1): SdfShape;\n/**\n * ForgeCAD — Scene Configuration API\n *\n * Lets .forge.js scripts control camera, lighting, background, fog,\n * and post-processing for generative art and presentation renders.\n *\n * Follows the collect-during-execution pattern used by viewConfig, cutPlane, etc.\n */\ninterface SceneCameraConfig {\n position?: [\n number,\n number,\n number\n ];\n target?: [\n number,\n number,\n number\n ];\n up?: [\n number,\n number,\n number\n ];\n fov?: number;\n type?: "perspective" | "orthographic";\n}\ntype SceneViewCameraConfig = SceneCameraConfig & {\n position: [\n number,\n number,\n number\n ];\n target: [\n number,\n number,\n number\n ];\n};\ninterface SceneViewConfig {\n /** Explicit camera state for this named render view. */\n camera: SceneViewCameraConfig;\n}\ntype SceneViewInputConfig = SceneViewConfig | SceneViewCameraConfig;\ntype SceneViewMap = Record<string, SceneViewConfig>;\ntype SceneJourneyDiagnosticLevel = "error" | "warning";\ninterface SceneJourneyDiagnostic {\n level: SceneJourneyDiagnosticLevel;\n message: string;\n stepId?: string;\n suggestions?: string[];\n}\ninterface SceneJourneyStepConfig {\n /** Stable step id used by viewer links and Next/Back state. */\n id: string;\n /** Viewer-facing title. Defaults to the step id. */\n title?: string;\n /** Object name or slash-separated tree path to focus. */\n focus?: string;\n /** Short optional viewer caption. */\n caption?: string;\n /** Optional explicit camera for this step. When omitted, the viewer fits `focus`. */\n camera?: SceneViewCameraConfig;\n /** Resolved object id after script execution, when `focus` matched exactly one object. */\n resolvedFocusId?: string | null;\n /** Resolved object tree path or name after script execution. */\n resolvedFocusPath?: string | null;\n /** Resolution diagnostics for this step. */\n diagnostics?: SceneJourneyDiagnostic[];\n}\ninterface SceneJourneyConfig {\n /** Viewer-facing journey title. Defaults to the journey id. */\n title?: string;\n /** Optional starting step id. Defaults to the first step. */\n startsAt?: string;\n /** Whether the viewer should offer or auto-open the journey. First slice supports opt-in. */\n behavior?: "opt-in" | "auto";\n /** Ordered journey spine. Branches can be added later without changing this core contract. */\n steps: SceneJourneyStepConfig[];\n /** True unless any journey or step diagnostic has level "error". */\n valid?: boolean;\n /** Whole-journey diagnostics, including unresolved startsAt and step target diagnostics. */\n diagnostics?: SceneJourneyDiagnostic[];\n}\ntype SceneJourneyMap = Record<string, SceneJourneyConfig>;\ntype SceneLightType = "ambient" | "directional" | "point" | "spot" | "hemisphere";\ninterface SceneLightConfig {\n type: SceneLightType;\n color?: string;\n intensity?: number;\n position?: [\n number,\n number,\n number\n ];\n /** Target for directional/spot lights */\n target?: [\n number,\n number,\n number\n ];\n /** Ground color for hemisphere lights */\n groundColor?: string;\n /** Sky color alias for hemisphere lights (same as color) */\n skyColor?: string;\n /** Spot light cone angle in radians */\n angle?: number;\n /** Spot light penumbra (0–1) */\n penumbra?: number;\n /** Point/spot light decay */\n decay?: number;\n /** Point/spot light distance (0 = infinite) */\n distance?: number;\n /** Whether this light casts shadows */\n castShadow?: boolean;\n}\ninterface SceneEnvironmentConfig {\n /** Built-in preset name or \'none\' to disable */\n preset?: "studio" | "sunset" | "dawn" | "warehouse" | "forest" | "apartment" | "lobby" | "city" | "park" | "night" | "none";\n /** Environment map intensity */\n intensity?: number;\n /** Use environment map as scene background */\n background?: boolean;\n}\ninterface SceneBackgroundGradient {\n top: string;\n bottom: string;\n}\ninterface SceneFogConfig {\n color?: string;\n /** Linear fog near distance */\n near?: number;\n /** Linear fog far distance */\n far?: number;\n /** Exponential fog density (if set, uses FogExp2 instead of linear Fog) */\n density?: number;\n}\ninterface SceneBloomConfig {\n intensity?: number;\n threshold?: number;\n radius?: number;\n}\ninterface SceneVignetteConfig {\n darkness?: number;\n offset?: number;\n}\ninterface SceneGrainConfig {\n intensity?: number;\n}\ninterface ScenePostProcessingConfig {\n bloom?: SceneBloomConfig;\n vignette?: SceneVignetteConfig;\n grain?: SceneGrainConfig;\n toneMappingExposure?: number;\n}\ninterface SceneGroundConfig {\n /** Show a ground plane */\n visible?: boolean;\n /** Ground color */\n color?: string;\n /** Offset below the model\'s bounding box minimum Z. Default 0 (flush with model bottom). */\n offset?: number;\n /** Receive shadows on the ground */\n receiveShadow?: boolean;\n}\ninterface SceneCaptureConfig {\n /** Frames for one full orbit rotation (default: 72) */\n framesPerTurn?: number;\n /** Frozen frames before motion starts (default: 6) */\n holdFrames?: number;\n /** Orbit pitch angle in degrees (default: auto from camera) */\n pitchDeg?: number;\n /** Output frame rate (default: 24) */\n fps?: number;\n /** Output frame size in pixels (default: 960) */\n size?: number;\n /** Canvas background color for capture (default: \'#252526\') */\n background?: string;\n}\ninterface SceneOptions {\n background?: string | SceneBackgroundGradient;\n camera?: SceneCameraConfig;\n views?: Record<string, SceneViewInputConfig>;\n journeys?: Record<string, SceneJourneyConfig>;\n lights?: SceneLightConfig[];\n environment?: SceneEnvironmentConfig;\n fog?: SceneFogConfig;\n postProcessing?: ScenePostProcessingConfig;\n ground?: SceneGroundConfig;\n /** Default capture parameters for `forgecad capture` — CLI flags override these. */\n capture?: SceneCaptureConfig;\n}\n/**\n * Configure the scene environment for the current script execution.\n *\n * **Details**\n *\n * Controls camera position, named render views, optional model journeys, lighting rig,\n * background color or gradient, atmospheric fog, environment maps, post-processing effects, and\n * capture parameters for the `forgecad capture` command. Multiple calls merge — later values\n * override earlier ones on a per-key basis, so you can split configuration across multiple\n * `scene()` calls.\n *\n * When `lights` is specified, **all** default lights are removed. You must include your own\n * ambient light or the scene will be fully dark.\n *\n * Setting `camera.position` overrides auto-framing — the viewport will no longer auto-fit the\n * geometry on script reload.\n *\n * Named render views let scripts check in repeatable cameras next to the model code. The canonical\n * shape is `{ camera: { position, target } }`, and a direct camera shorthand\n * `{ position, target }` is also accepted. Use the canonical shape when you may add view metadata\n * later. Use it from the CLI with `forgecad render 3d model.forge.js --view hero`.\n *\n * Model journeys let scripts check in a compact guided path through named objects. Each journey has\n * ordered `steps`; each step can name a `focus` target by object name/tree path, provide a caption,\n * and optionally provide an explicit camera. In the viewer, journeys are opt-in: they appear as a\n * small Explore control and do not move the camera until the user starts them. Use\n * `forgecad run model.forge.js --journeys` or `--journeys-json` to inspect resolved targets.\n *\n * Post-processing effects (`bloom`, `vignette`, `grain`) work in the browser viewport only. The\n * CLI applies camera, lights, background, fog, and `toneMappingExposure` but skips shader effects.\n *\n * All numeric values accept `param()` expressions.\n *\n * **Example**\n *\n * ```js\n * scene({\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 * @param options.background - Solid color hex string or `{ top, bottom }` vertical gradient\n * @param options.camera - Camera override: `position` `[x,y,z]`, `target` `[x,y,z]`, `fov` (degrees 1–179), `up` `[x,y,z]`, `type` (`\'perspective\' | \'orthographic\'`)\n * @param options.views - Named render cameras for `forgecad render 3d --view <name>`. Each view is either `{ camera }` or a direct camera shorthand with required `position` and `target`, optional `up`, `fov`, and `type`\n * @param options.journeys - Guided viewer journeys keyed by id. Each journey has ordered `steps`; each step can use `focus` to target a returned object by name/tree path, plus optional `title`, `caption`, and explicit `camera`\n * @param options.lights - Full light array (replaces all defaults). Each entry: `type` (`\'ambient\' | \'directional\' | \'point\' | \'spot\' | \'hemisphere\'`), `color`, `intensity`, `position`, `target`, `castShadow`. Hemisphere accepts `skyColor`/`groundColor`. Spot/point accept `angle`, `penumbra`, `decay`, `distance`\n * @param options.environment - Built-in HDRI preset: `preset` (`\'studio\' | \'sunset\' | \'dawn\' | \'warehouse\' | \'forest\' | \'apartment\' | \'lobby\' | \'city\' | \'park\' | \'night\' | \'none\'`), `intensity`, `background`\n * @param options.fog - Atmospheric fog: `color`, `near`/`far` (linear) or `density` (exponential FogExp2)\n * @param options.postProcessing - Shader effects: `bloom` `{ intensity?, threshold?, radius? }`, `vignette` `{ darkness?, offset? }`, `grain` `{ intensity? }`, `toneMappingExposure`\n * @param options.ground - Ground plane: `visible`, `color`, `offset` (below model bottom), `receiveShadow`\n * @param options.capture - Capture defaults for `forgecad capture`: `framesPerTurn` (12–720, default 72), `holdFrames` (0–300, default 6), `pitchDeg` (-80–80), `fps` (1–60, default 24), `size` (pixels, default 960), `background`\n * @returns void\n * @category Scene Configuration\n */\ndeclare function scene(options: SceneOptions): void;\ninterface JointOverlayViewConfigOptions {\n enabled?: boolean;\n axisColor?: string;\n axisCoreColor?: string;\n arcColor?: string;\n zeroColor?: string;\n arcVisualLimitDeg?: number;\n axisLengthScale?: number;\n axisLengthMin?: number;\n axisLineRadiusScale?: number;\n axisLineRadiusMin?: number;\n axisLineRadiusMax?: number;\n spokeLineRadiusScale?: number;\n spokeLineRadiusMin?: number;\n spokeLineRadiusMax?: number;\n arcLineRadiusScale?: number;\n arcLineRadiusMin?: number;\n arcLineRadiusMax?: number;\n axisDotRadiusScale?: number;\n axisDotRadiusMin?: number;\n axisArrowRadiusScale?: number;\n axisArrowRadiusMin?: number;\n axisArrowLengthScale?: number;\n axisArrowLengthMin?: number;\n axisArrowOffsetFactor?: number;\n arcRadiusScale?: number;\n arcRadiusMin?: number;\n arcDotRadiusScale?: number;\n arcDotRadiusMin?: number;\n arcArrowRadiusScale?: number;\n arcArrowRadiusMin?: number;\n arcArrowLengthScale?: number;\n arcArrowLengthMin?: number;\n arcArrowOffsetFactor?: number;\n arcStepDeg?: number;\n arcMinSteps?: number;\n arcTubeSegmentsMin?: number;\n arcTubeSegmentsFactor?: number;\n arcTubeRadialSegments?: number;\n}\ninterface ViewConfigOptions {\n jointOverlay?: JointOverlayViewConfigOptions;\n}\n/**\n * Configure viewport helper visuals for the current script execution.\n *\n * **Details**\n *\n * Controls renderer-only overlays that appear in the viewport but are not part of the geometry.\n * Currently supports the joint overlay that renders axis arrows and arc indicators when\n * `jointsView` is active. Multiple calls merge — later values override earlier ones per key.\n *\n * This does **not** trigger a geometry recompute; it only affects the visual helpers drawn on\n * top of the 3D scene.\n *\n * **Example**\n *\n * ```js\n * viewConfig({\n * jointOverlay: {\n * axisColor: \'#13dfff\',\n * arcColor: \'#ff7a1a\',\n * axisLineRadiusScale: 0.03,\n * arcLineRadiusScale: 0.022,\n * },\n * });\n * ```\n *\n * @param options.jointOverlay - Joint overlay appearance. Supports `enabled`, `axisColor`, `axisCoreColor`, `arcColor`, `zeroColor`, `axisLengthScale`, `arcVisualLimitDeg`, `arcStepDeg`, and detailed radius/arrow sizing properties\n * @returns void\n * @category Scene Configuration\n */\ndeclare function viewConfig(options?: ViewConfigOptions): void;\ntype SketchFaceTarget = SketchFace3D | string | FaceRef;\ninterface ShapeFeatureExtentSideOptions {\n depth?: number;\n upToFace?: SketchFaceTarget | FaceRef;\n through?: boolean;\n}\ninterface ShapeFeatureExtentOptions {\n forward: ShapeFeatureExtentSideOptions;\n reverse?: ShapeFeatureExtentSideOptions;\n}\ninterface ShapeHoleThreadOptions {\n designation?: string;\n pitch?: number;\n class?: string;\n handedness?: "right" | "left";\n depth?: number;\n modeled?: boolean;\n}\ninterface ShapeHoleOptions {\n diameter: number;\n depth?: number;\n upToFace?: SketchFaceTarget | FaceRef;\n extent?: ShapeFeatureExtentOptions;\n u?: number;\n v?: number;\n counterbore?: {\n diameter: number;\n depth: number;\n };\n countersink?: {\n diameter: number;\n angleDeg?: number;\n };\n thread?: ShapeHoleThreadOptions;\n}\ninterface ShapeCutoutOptions {\n depth?: number;\n upToFace?: SketchFaceTarget | FaceRef;\n extent?: ShapeFeatureExtentOptions;\n taperScale?: number | [\n number,\n number\n ];\n}\ninterface Shape {\n /**\n * Drill a hole into this solid at a face.\n *\n * @param faceOrRef Target face — a face selector string (e.g. `\'top\'`), a placed sketch, or a `FaceRef`.\n * @param opts Hole parameters: `diameter` (required), optional `depth`, `counterbore`, `countersink`, `thread`, `u`/`v` offset, `upToFace`, `extent`.\n *\n * @example\n * box(50, 50, 20).hole(\'top\', { diameter: 8, depth: 10 })\n * box(50, 50, 20).hole(\'top\', { diameter: 6, counterbore: { diameter: 12, depth: 3 } })\n */\n hole(faceOrRef: SketchFaceTarget | FaceRef, opts: ShapeHoleOptions): Shape;\n /**\n * Cut a profile-shaped pocket through a face using a placed sketch.\n *\n * The sketch must be placed on a face with `Sketch.onFace(...)`. The cut follows the sketch\'s 2D profile.\n *\n * @param sketch Sketch placed on a face via `.onFace()`.\n * @param opts Optional `depth`, `upToFace`, `extent`, `taperScale`.\n *\n * @example\n * const profile = circle2d(10).onFace(body, \'top\');\n * body.cutout(profile, { depth: 5 })\n */\n cutout(sketch: Sketch, opts?: ShapeCutoutOptions): Shape;\n}\ninterface PocketOptions {\n /**\n * Shrink the face boundary inward by this many mm before extruding.\n * Produces angled walls when combined with depth. Default: 0 (full face).\n */\n inset?: number;\n /**\n * Scale the face profile uniformly (e.g. 0.8 = 80% of the face area).\n * Mutually exclusive with `inset`; `inset` takes precedence if both are set.\n */\n scale?: number;\n /** Corner join style when using `inset`. Default: \'Round\'. */\n join?: "Square" | "Round" | "Miter";\n}\ntype BossOptions = PocketOptions;\ninterface Shape {\n /**\n * Cut a pocket (cavity) into this solid through the named face.\n *\n * @param face Which face to cut into — e.g. `\'top\'`, `\'front\'`, or a face query.\n * @param depth How deep the pocket goes into the solid (mm).\n * @param opts Optional `inset` (shrink boundary, mm), `scale` (uniform scale, e.g. 0.8), `join` (`\'Round\'`/`\'Square\'`/`\'Miter\'`).\n *\n * @example\n * box(100, 100, 20).pocket(\'top\', 8)\n * box(100, 100, 20).pocket(\'top\', 8, { inset: 5 })\n * box(100, 100, 20).pocket(\'top\', 8, { scale: 0.8 })\n */\n pocket(face: FaceSelector, depth: number, opts?: PocketOptions): Shape;\n /**\n * Add a boss (protrusion) from the named face.\n *\n * @param face Which face to protrude from — e.g. `\'top\'`, `\'front\'`, or a face query.\n * @param height Height of the protrusion above the face (mm).\n * @param opts Optional `inset` (shrink boundary, mm), `scale` (uniform scale, e.g. 0.8), `join` (`\'Round\'`/`\'Square\'`/`\'Miter\'`).\n *\n * @example\n * box(100, 100, 20).boss(\'top\', 5)\n * box(100, 100, 20).boss(\'top\', 10, { scale: 0.6 })\n */\n boss(face: FaceSelector, height: number, opts?: BossOptions): Shape;\n}\ntype Vec3$2 = [\n number,\n number,\n number\n];\ntype SlicePlane = "xy" | "xz" | "yz" | Vec3$2;\ninterface FromSlicesSlice {\n /** Plane normal: axis name or arbitrary unit vector. */\n on: SlicePlane;\n /** Signed offset along the normal from the origin. */\n at: number;\n /** 2D cross-section profile on that plane. */\n profile: Sketch;\n}\ninterface FromSlicesOptions {\n /** Marching-grid edge length for level-set meshing (Manifold only). */\n edgeLength?: number;\n /** Extra bounding-box padding (Manifold only). */\n boundsPadding?: number;\n}\nexport namespace Shape {\n /**\n * Construct a 3D shape from cross-section slices on one or more planes.\n *\n * Slices with the same normal direction are lofted together. Slices with\n * different normals are combined via smooth radial blending — each\n * silhouette constrains the shape\'s extent, producing smooth ellipsoidal\n * cross-sections.\n *\n * @example\n * ```js\n * // Egg from two orthogonal silhouettes\n * const eggProfile = ellipse(15, 25);\n * return Shape.fromSlices([\n * { on: \'xz\', at: 0, profile: eggProfile },\n * { on: \'yz\', at: 0, profile: eggProfile },\n * ]);\n * ```\n *\n * @example\n * ```js\n * // Vase with cross-section transitions\n * return Shape.fromSlices([\n * { on: \'xy\', at: 0, profile: circle2d(20) },\n * { on: \'xy\', at: 40, profile: rect(25, 25) },\n * { on: \'xy\', at: 80, profile: circle2d(8) },\n * { on: \'xz\', at: 0, profile: vaseOutline },\n * ]);\n * ```\n */\n function fromSlices(slices: FromSlicesSlice[], options?: FromSlicesOptions): Shape;\n}\ntype ExplodeAxis = "x" | "y" | "z";\ntype ExplodeDirection = "radial" | ExplodeAxis | [\n number,\n number,\n number\n];\ninterface ExplodeDirective {\n /** Multiplier applied to `amount` for this node */\n stage?: number;\n /** Direction mode for this node */\n direction?: ExplodeDirection;\n /** Optional axis lock after direction is resolved */\n axisLock?: ExplodeAxis;\n}\ninterface ExplodeConfigOptions {\n amount?: number;\n stages?: number[];\n mode?: ExplodeDirection;\n axisLock?: ExplodeAxis;\n byName?: Record<string, ExplodeDirective>;\n byPath?: Record<string, ExplodeDirective>;\n}\ntype ExplodeViewDirection = ExplodeDirection;\ninterface ExplodeViewDirective extends ExplodeDirective {\n}\ninterface ExplodeViewOptions {\n /** Set false to disable viewport explode offsets for this script output. */\n enabled?: boolean;\n /** Scales the UI explode amount. Default: 1 */\n amountScale?: number;\n /**\n * Per-depth stage multipliers (depth 1 = first level).\n * If depth exceeds this array, the last value is reused.\n * Default when omitted: reciprocal depth (1, 1/2, 1/3, ...)\n */\n stages?: number[];\n /** Global direction mode fallback. Default: \'radial\' */\n mode?: ExplodeViewDirection;\n /** Global axis lock fallback. */\n axisLock?: ExplodeAxis;\n /** Per-object overrides by final object name. */\n byName?: Record<string, ExplodeViewDirective>;\n /** Per-tree-path overrides using slash-separated object tree segments. */\n byPath?: Record<string, ExplodeViewDirective>;\n}\n/**\n * Configure how the viewport explode slider offsets returned objects.\n *\n * **Details**\n *\n * Offsets are resolved from the returned object tree, not a flat list. In `radial` mode each\n * node follows its parent branch direction, then fans locally from the immediate parent\n * center — nested assemblies peel apart level by level. In fixed-axis or fixed-vector modes,\n * the branch follows that axis/vector but nested descendants fan out perpendicular by default.\n *\n * Multiple calls merge — later values override earlier ones on a per-key basis. `byName` and\n * `byPath` maps are merged entry-by-entry.\n *\n * For programmatic explode applied before returning (without the slider), use `lib.explode()`\n * instead.\n *\n * **Example**\n *\n * ```js\n * explodeView({\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 * @param options.enabled - Set `false` to disable viewport explode offsets for this script\n * @param options.amountScale - Multiplier applied to the UI slider amount. Default: 1\n * @param options.stages - Per-depth stage multipliers (depth 1 = first level). If depth exceeds the array, the last value is reused. Default: reciprocal depth (`1, 1/2, 1/3, ...`)\n * @param options.mode - Global direction mode: `\'radial\'` | `\'x\'` | `\'y\'` | `\'z\'` | `[x, y, z]`. Default: `\'radial\'`\n * @param options.axisLock - Constrain motion to a single world axis: `\'x\'` | `\'y\'` | `\'z\'`\n * @param options.byName - Per-object overrides keyed by final returned object name: `{ stage?, direction?, axisLock? }`\n * @param options.byPath - Per-tree-path overrides using slash-separated path segments (e.g. `\'Drive/Shaft\'`): `{ stage?, direction?, axisLock? }`\n * @returns void\n * @see {@link explode} for applying explode offsets programmatically before returning geometry\n * @category Explode View\n */\ndeclare function explodeView(options?: ExplodeViewOptions): void;\ntype JointViewType = "revolute" | "prismatic";\ntype JointViewAxis = [\n number,\n number,\n number\n];\ninterface JointViewInput {\n name: string;\n child: string;\n parent?: string;\n type?: JointViewType;\n axis?: JointViewAxis;\n pivot?: [\n number,\n number,\n number\n ];\n min?: number;\n max?: number;\n default?: number;\n unit?: string;\n hidden?: boolean;\n}\ninterface JointViewAnimationKeyframeInput {\n /** Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights. */\n at?: number;\n /** 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 ticks?: number;\n values: Record<string, number>;\n}\ninterface JointViewAnimationInput {\n name: string;\n duration?: number;\n loop?: boolean;\n continuous?: boolean;\n keyframes: JointViewAnimationKeyframeInput[];\n}\ninterface JointViewCouplingTermInput {\n joint: string;\n ratio?: number;\n}\ninterface JointViewCouplingInput {\n joint: string;\n terms: JointViewCouplingTermInput[];\n offset?: number;\n}\ninterface JointsViewOptions {\n enabled?: boolean;\n joints?: JointViewInput[];\n couplings?: JointViewCouplingInput[];\n animations?: JointViewAnimationInput[];\n defaultAnimation?: string;\n}\n/**\n * Register viewport-only mechanism controls that animate returned objects without re-running the script.\n *\n * **Details**\n *\n * Defines joints (revolute or prismatic), optional gear/rack couplings, and named animations.\n * The viewport resolves transforms through the joint chain at display time — the script geometry\n * is computed only once at rest pose.\n *\n * **Critical:** Solve the assembly at **rest pose** (all animated joints = 0). The viewport\n * applies `jointsView` transforms on top of the returned scene. If geometry is already solved\n * at non-zero angles, animation will double-rotate everything.\n *\n * ```js\n * // BAD — double rotation\n * const solved = mech.solve({ shoulder: 45, elbow: 30 });\n * jointsView({ joints: [{ name: \'shoulder\', ... }] });\n * return solved;\n *\n * // GOOD — rest pose, jointsView controls all posing\n * const solved = mech.solve({ shoulder: 0, elbow: 0 });\n * jointsView({\n * joints: [\n * { name: \'shoulder\', child: \'Upper Arm\', default: 45, ... },\n * { name: \'elbow\', child: \'Forearm\', parent: \'Upper Arm\', default: 30, ... },\n * ],\n * });\n * return solved;\n * ```\n *\n * **Pivot coordinates** are world-space positions of each joint origin at rest pose. For\n * `addRevolute(\'shoulder\', \'Base\', \'Link\', { frame: Transform.identity().translate(0, 0, 20) })`\n * 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\n * joint in the chain:\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 *\n * Animation values are interpolated linearly between keyframes. ForgeCAD does **not** auto-wrap\n * revolute values across `-180/180`. Keep keyframe values continuous — a `-180 -> 171` jump\n * spins the part the long way around. Use `-180 -> -189` instead. Author high-speed multi-turn\n * 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 * ```js\n * keyframes: [\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 * Mixing explicit `at` and omitted `at` in the same animation is not allowed.\n *\n * **Example**\n *\n * ```js\n * jointsView({\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 * @param options.enabled - Set `false` to hide all joint controls\n * @param options.joints - Joint definitions: `{ name, child, parent?, type?, axis?, pivot?, min?, max?, default?, unit?, hidden? }[]`. `min`/`max` control the slider range only — they do **not** clamp animation keyframe values\n * @param options.couplings - Gear/rack couplings: `{ joint, terms: [{ joint, ratio? }][], offset? }[]`. Coupled joints cannot appear as animation keyframe targets\n * @param options.animations - Named animations: `{ name, duration?, loop?, continuous?, keyframes }[]`\n * @param options.defaultAnimation - Name of the animation to play on load\n * @returns void\n * @category Animation\n */\ndeclare function jointsView(options?: JointsViewOptions): void;\ninterface BomOpts {\n /**\n * Quantity unit label, e.g. "mm", "pieces", "kg".\n * Default: "pieces"\n */\n unit?: string;\n /**\n * Optional explicit grouping key used during report aggregation.\n */\n key?: string;\n /** Material name, e.g. "steel", "birch plywood", "nylon" */\n material?: string;\n /** Overall dimensions `[width, height]` or `[width, height, thickness]` in the entry\'s unit */\n dimensions?: number[];\n /** Cross-section dimensions `[w, h]` for tubes and profiles */\n section?: number[];\n /** Wall thickness for hollow sections (mm) */\n wall?: number;\n /** Diameter for round stock, bolts, dowels (mm) */\n diameter?: number;\n /** Length for fasteners (mm) */\n length?: number;\n /** Manufacturing process, e.g. "laser cut", "CNC", "welded" */\n process?: string;\n /** Free-form notes */\n notes?: string;\n /** Wood grain direction, e.g. "long", "cross" */\n grain?: string;\n /** Extensible for domain-specific fields */\n [key: string]: unknown;\n}\n/**\n * Register a Bill of Materials entry for report export.\n *\n * **Details**\n *\n * BOM entries are accumulated during script execution and exported alongside\n * the model in report views. Rows are grouped by normalized\n * `description + unit`. Pass an explicit `key` to force multiple descriptions\n * to collapse into a single line item.\n *\n * - `quantity` must be a finite number `>= 0`. A quantity of `0` is silently\n * 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\n * covers per-part assembly metadata; this function is for free-form\n * purchased-item annotation.\n * - `bom()` is injected into every `.forge.js` script. Call it directly; do\n * not write `const { bom } = require(...)`, because top-level declarations\n * named `bom` collide with the built-in runtime name.\n *\n * **Example**\n *\n * ```ts\n * const tubeLen = param("Tube Length", 1200, { min: 300, max: 4000, unit: "mm" });\n * const boltCount = param("Bolt Count", 16, { min: 0, max: 200, integer: true });\n *\n * bom(tubeLen, "iron tube 30 x 20", { unit: "mm" });\n * bom(boltCount, "M4 bolt, 16 mm length");\n * bom(4, "rubber foot", { key: "foot-rubber" }); // explicit aggregation key\n *\n * // Structured metadata for richer reports:\n * bom(tubeLen, "rectangular steel tube", {\n * unit: "mm",\n * material: "steel",\n * section: [30, 20],\n * wall: 3,\n * });\n * ```\n *\n * @param quantity - Quantity of the item; must be finite and `>= 0`\n * @param description - Human-readable item text, e.g. `"M4 bolt, 16 mm length"`\n * @param opts - Optional `unit` label (default `"pieces"`), aggregation `key`, and structured metadata (`material`, `section`, `wall`, `diameter`, `length`, `process`, `notes`, `grain`, etc.)\n * @category Bill of Materials\n */\ndeclare function bom(quantity: number, description: string, opts?: BomOpts): void;\ntype CutPlaneExcludeInput = string | string[];\ninterface CutPlaneOptions {\n /** Optional offset along the plane normal (primarily for object-form overload). */\n offset?: number;\n /** Object names to keep uncut for this plane. */\n exclude?: CutPlaneExcludeInput;\n}\n/**\n * Define a named section plane for inspecting internal geometry.\n *\n * **Details**\n *\n * Registers a cut plane that appears as a toggle in the viewport View Panel. When enabled,\n * geometry on the positive side of the plane (the side the normal points toward) is clipped away,\n * revealing the internal cross-section. The newly exposed section faces render with a hatched\n * overlay; pre-existing coplanar boundary faces are left unhatched.\n *\n * Planes are registered once per script run. The viewport toggle state (on/off) persists across\n * parameter changes without re-running the script. The `exclude` option only works correctly when\n * the excluded object names are stable across parameter changes.\n *\n * Accepts two overloads: `cutPlane(name, normal, offset?, options?)` or\n * `cutPlane(name, normal, options?)` where options may include `offset`.\n *\n * **Example**\n *\n * ```js\n * const cutZ = param(\'Cut Height\', 10, { min: -50, max: 50, unit: \'mm\' });\n * cutPlane(\'Inspection\', [0, 0, 1], cutZ, { exclude: [\'Probe\', \'Fasteners\'] });\n * ```\n *\n * @param name - Display label shown in the viewport View Panel\n * @param normal - Plane normal `[x, y, z]` — geometry on this side is clipped away\n * @param offset - Distance from origin along the normal. Default: 0\n * @param options.exclude - Object name(s) to keep uncut for this plane\n * @returns void\n * @category Cut Plane\n */\ndeclare function cutPlane(name: string, normal: [\n number,\n number,\n number\n], offset?: number, options?: CutPlaneOptions): void;\ndeclare function cutPlane(name: string, normal: [\n number,\n number,\n number\n], options?: CutPlaneOptions): void;\n/** Render-only viewport label definitions collected during script execution. */\ntype RenderLabelAnchor = "center" | "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right";\ninterface RenderLabelOptions {\n /** Text color as any CSS color string. */\n color?: string;\n /** Background color as any CSS color string. Use `\'transparent\'` for no pill background. */\n background?: string;\n /** Font size in CSS pixels. Defaults to 12. */\n size?: number;\n /** Additional world-space offset from `at`. */\n offset?: [\n number,\n number,\n number\n ];\n /** Which point of the label box is anchored to `at`. Defaults to `\'center\'`. */\n anchor?: RenderLabelAnchor;\n /** When false, the label is hidden when occluded by scene geometry. Defaults to true. */\n alwaysOnTop?: boolean;\n}\ninterface RenderLabelDef extends RenderLabelOptions {\n id: string;\n text: string;\n at: [\n number,\n number,\n number\n ];\n offset: [\n number,\n number,\n number\n ];\n anchor: RenderLabelAnchor;\n alwaysOnTop: boolean;\n}\n/**\n * Viewport-only presentation helpers.\n *\n * Use `Viewport.label()` for explanatory text that should appear in the viewer\n * but should not become CAD geometry. This keeps annotations separate from\n * semantic face labels and from `text2d()` geometry.\n *\n * @category Viewport Labels\n */\ndeclare const Viewport: {\n /**\n * Add a render-only viewport label at a world-space point.\n *\n * **Details**\n *\n * `Viewport.label()` is for explanatory text that helps a viewer understand\n * the model. It does not create sketches, meshes, B-rep topology, exported\n * text, or face labels, so it stays off the OCCT path. Use `text2d()` only\n * when the letters should become manufactured geometry, such as raised\n * lettering, engraved serial numbers, or exported nameplates.\n *\n * Labels are collected during script execution and rendered by the viewport\n * as lightweight overlay annotations. They are ignored by exports and do not\n * appear in `objects`.\n *\n * **Example**\n *\n * ```js\n * Viewport.label(\'Bearing bore\', [0, 0, 18], {\n * color: \'#f8fafc\',\n * background: \'#0f172acc\',\n * offset: [0, 0, 8],\n * anchor: \'bottom\',\n * });\n *\n * return box(40, 30, 12);\n * ```\n *\n * @param text - Label text to display in the viewport\n * @param at - World-space anchor point `[x, y, z]`\n * @param options - Visual label options\n * @returns void\n * @category Viewport Labels\n */\n label(text: string, at: [\n number,\n number,\n number\n ], options?: RenderLabelOptions): void;\n};\ninterface RobotLinkExportOptions {\n massKg?: number;\n densityKgM3?: number;\n collision?: "visual" | "convex" | "box" | "none";\n}\ninterface RobotJointExportOptions {\n effort?: number;\n velocity?: number;\n damping?: number;\n friction?: number;\n}\ninterface RobotDiffDrivePluginOptions {\n leftJoints: string[];\n rightJoints: string[];\n wheelSeparationMm: number;\n wheelRadiusMm: number;\n topic?: string;\n odomTopic?: string;\n tfTopic?: string;\n frameId?: string;\n odomFrameId?: string;\n maxLinearVelocity?: number;\n maxAngularVelocity?: number;\n linearAcceleration?: number;\n angularAcceleration?: number;\n}\ninterface RobotJointStatePublisherOptions {\n enabled?: boolean;\n joints?: string[];\n topic?: string;\n updateRate?: number;\n}\ntype RobotPose6 = [\n number,\n number,\n number,\n number,\n number,\n number\n];\ninterface RobotWorldKeyboardTeleopOptions {\n enabled?: boolean;\n linearStep?: number;\n angularStep?: number;\n}\ninterface RobotWorldOptions {\n name?: string;\n generateDemoWorld?: boolean;\n spawnPose?: RobotPose6;\n keyboardTeleop?: RobotWorldKeyboardTeleopOptions;\n}\ninterface RobotExportOptions {\n assembly: Assembly;\n modelName?: string;\n state?: JointState;\n static?: boolean;\n selfCollide?: boolean;\n allowAutoDisable?: boolean;\n links?: Record<string, RobotLinkExportOptions>;\n joints?: Record<string, RobotJointExportOptions>;\n plugins?: {\n diffDrive?: RobotDiffDrivePluginOptions;\n jointStatePublisher?: RobotJointStatePublisherOptions;\n };\n world?: RobotWorldOptions;\n}\ninterface CollectedRobotExport {\n modelName: string;\n assembly: AssemblyDefinition;\n state: JointState;\n static: boolean;\n selfCollide: boolean;\n allowAutoDisable: boolean;\n links: Record<string, RobotLinkExportOptions>;\n joints: Record<string, RobotJointExportOptions>;\n plugins: {\n diffDrive?: RobotDiffDrivePluginOptions;\n jointStatePublisher?: RobotJointStatePublisherOptions;\n };\n world: RobotWorldOptions | null;\n}\n/**\n * Declare that this script should export the assembly as a SDF/URDF robot package.\n *\n * **Details**\n *\n * Call `robotExport()` alongside your assembly definition. The CLI commands\n * `forgecad export sdf` and `forgecad export urdf` pick up the declaration and\n * produce a robot package with:\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 * - 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\n * `<mimic>` — SDF/URDF support single-leader mimic only. Dropped terms emit a warning.\n *\n* **Example**\n*\n* ```ts\n* const 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 *\n * robotExport({\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\n * forgecad export sdf model.forge.js # SDF package (Gazebo/Ignition)\n * forgecad export urdf model.forge.js # URDF package (ROS/PyBullet/MuJoCo)\n * ```\n *\n * @param options - Assembly, per-link mass/collision, per-joint overrides, plugins, world config\n * @returns The collected export descriptor (also stored globally for the CLI)\n * @category Robot Export\n */\ndeclare function robotExport(options: RobotExportOptions): CollectedRobotExport;\n/**\n * Combine 2D sketches into a single profile using an additive boolean union.\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `union2d(a, b, c)` or `union2d([a, b, c])`.\n * Uses Manifold\'s batch operation — faster than chaining `.add()` one by one when\n * combining many sketches.\n *\n * **Example**\n *\n * ```ts\n * const cross = union2d(rect(60, 10), rect(10, 60));\n * ```\n *\n * @returns A new sketch containing the union of all inputs\n * @see {@link Sketch.add} for the chainable method equivalent\n * @category Sketch Booleans\n */\ndeclare function union2d(...inputs: SketchOperandInput[]): Sketch;\n/**\n * Subtract one or more 2D sketches from a base sketch.\n *\n * **Details**\n *\n * The first sketch is the base; all subsequent sketches are subtracted from it.\n * Accepts individual sketches or arrays: `difference2d(base, c1, c2)` or\n * `difference2d([base, c1, c2])`.\n * Uses Manifold\'s batch operation — faster than chaining `.subtract()` one by one.\n *\n * **Example**\n *\n * ```ts\n * const donut = difference2d(circle2d(50), circle2d(30));\n * ```\n *\n * @returns A new sketch with all subsequent inputs subtracted from the first\n * @see {@link Sketch.subtract} for the chainable method equivalent\n * @category Sketch Booleans\n */\ndeclare function difference2d(...inputs: SketchOperandInput[]): Sketch;\n/**\n * Keep only the area where all input sketches overlap (intersection boolean).\n *\n * **Details**\n *\n * Accepts individual sketches or arrays: `intersection2d(a, b)` or `intersection2d([a, b, c])`.\n * Uses Manifold\'s batch operation — faster than chaining `.intersect()` one by one.\n *\n * **Example**\n *\n * ```ts\n * const lens = intersection2d(circle2d(30).translate(-10, 0), circle2d(30).translate(10, 0));\n * ```\n *\n * @returns A new sketch containing only the shared area of all inputs\n * @see {@link Sketch.intersect} for the chainable method equivalent\n * @category Sketch Booleans\n */\ndeclare function intersection2d(...inputs: SketchOperandInput[]): Sketch;\ntype RectVertexName = "bottomLeft" | "bottomRight" | "topRight" | "topLeft";\ntype RectSideName = "bottom" | "right" | "top" | "left";\ninterface RectOptions {\n /** Bottom-left x coordinate. Default: 0. */\n x?: number;\n /** Bottom-left y coordinate. Default: 0. */\n y?: number;\n /** Width (along x). Default: 10. */\n width?: number;\n /** Height (along y). Default: 10. */\n height?: number;\n /** Prevent 180° rotation (ensures bottom edge points rightward). Default: false. */\n blockRotation?: boolean;\n}\n/**\n * Typed handle for a constrained axis-aligned rectangle in the solver.\n *\n * Structural constraints pre-applied:\n * `horizontal(bottom)`, `horizontal(top)`, `vertical(left)`, `vertical(right)`,\n * `ccw(bl, br, tr, tl)`.\n *\n * This leaves **4 DOF** (position x/y, width, height). Use `sk.fix()`,\n * `sk.length()`, `sk.shapeWidth()`, etc. to pin them.\n */\ninterface ConstrainedRect {\n readonly bottomLeft: PointId;\n readonly bottomRight: PointId;\n readonly topRight: PointId;\n readonly topLeft: PointId;\n /** bottom-left → bottom-right */\n readonly bottom: LineId;\n /** bottom-right → top-right */\n readonly right: LineId;\n /** top-right → top-left */\n readonly top: LineId;\n /** top-left → bottom-left */\n readonly left: LineId;\n /**\n * Center point constrained to the geometric center via `midpoint` on the diagonal.\n * Can be used in further constraints: `sk.fix(rect.center, 0, 0)`,\n * `sk.coincident(rect.center, other)`.\n */\n readonly center: PointId;\n /** ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. */\n readonly shape: ShapeId;\n /** CCW-ordered vertex array: [bottomLeft, bottomRight, topRight, topLeft]. */\n readonly vertices: [\n PointId,\n PointId,\n PointId,\n PointId\n ];\n /** CCW-ordered side array: [bottom, right, top, left]. */\n readonly sides: [\n LineId,\n LineId,\n LineId,\n LineId\n ];\n /** Named vertex lookup. */\n vertex(name: RectVertexName): PointId;\n /** Named side lookup. */\n side(name: RectSideName): LineId;\n}\n/**\n * Add an axis-aligned rectangle concept to the builder.\n *\n * Creates 4 vertices (CCW: bl→br→tr→tl), 4 sides, 4 structural constraints\n * (`horizontal`/`vertical` on each side), CCW winding, a center point, a loop,\n * and a shape. Returns a `ConstrainedRect` handle with 4 DOF (x, y, width, height).\n *\n * Use `sk.rect()` as the shorthand builder method.\n *\n * **Example**\n *\n * ```ts\n * const sk = constrainedSketch();\n * const r = sk.rect({ x: 0, y: 0, width: 100, height: 50 });\n * sk.fix(r.bottomLeft, 0, 0);\n * sk.length(r.bottom, 120); // override initial width\n * return sk.solve().extrude(10);\n * ```\n *\n * @param sk - The builder to add the rectangle to.\n * @param options - Initial position and size.\n * @returns A `ConstrainedRect` handle with named vertices, sides, and center.\n * @category Constrained Sketches\n */\ndeclare function addRect(sk: ConstrainedSketchBuilder, options?: RectOptions): ConstrainedRect;\ninterface ConstrainedSketchBuilder {\n /**\n * Add an axis-aligned rectangle concept.\n * Returns a `ConstrainedRect` handle with named vertices, sides, and center.\n */\n rect(options?: RectOptions): ConstrainedRect;\n}\ninterface PolygonOptions {\n /** Initial vertex coordinates. Minimum 3 points. */\n points: ReadonlyArray<readonly [\n number,\n number\n ]>;\n /**\n * Whether to register a closed loop for sketch generation.\n * Default: true.\n */\n addLoop?: boolean;\n /** Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. */\n blockRotation?: boolean;\n}\n/**\n * Typed handle for a general constrained polygon in the solver.\n *\n * Structural constraints pre-applied: `ccw(vertices)` for winding enforcement.\n *\n * `sides[i]` goes from `vertices[i]` → `vertices[(i+1) % n]`.\n */\ninterface ConstrainedPolygon {\n /** CCW-ordered PointIds. */\n readonly vertices: PointId[];\n /**\n * CCW-ordered LineIds.\n * `sides[i]` runs from `vertices[i]` → `vertices[(i+1) % n]`.\n */\n readonly sides: LineId[];\n /** ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. */\n readonly shape: ShapeId;\n /** Get vertex by index. */\n vertex(index: number): PointId;\n /** Get side by index (side `i` goes vertex `i` → vertex `(i+1) % n`). */\n side(index: number): LineId;\n}\n/**\n * Add a general polygon concept to the builder.\n *\n * Creates n vertices and n sides (CCW: `sides[i]` from `vertices[i]` →\n * `vertices[(i+1) % n]`). Applies a `ccw` constraint to enforce winding.\n * All dimensional constraints (lengths, angles, position) are left to the caller.\n *\n * Use `sk.addPolygon()` as the shorthand builder method.\n *\n * **Example**\n *\n * ```ts\n * const sk = constrainedSketch();\n * const tri = sk.addPolygon({ points: [[0,0],[100,0],[50,80]] });\n * sk.fix(tri.vertex(0), 0, 0);\n * sk.length(tri.side(0), 100);\n * return sk.solve().extrude(5);\n * ```\n *\n * @param sk - The builder to add the polygon to.\n * @param options - Vertex coordinates and loop registration options.\n * @returns A `ConstrainedPolygon` handle with indexed vertex and side access.\n * @category Constrained Sketches\n */\ndeclare function addPolygon(sk: ConstrainedSketchBuilder, options: PolygonOptions): ConstrainedPolygon;\ninterface ConstrainedSketchBuilder {\n /**\n * Add a general polygon concept (CCW winding enforced).\n * Returns a `ConstrainedPolygon` handle.\n */\n addPolygon(options: PolygonOptions): ConstrainedPolygon;\n}\ninterface RegularPolygonOptions {\n /** Number of sides (minimum 3). */\n sides: number;\n /** Circumradius — distance from center to vertex. Default: 10. */\n radius?: number;\n /** Center x coordinate. Default: 0. */\n cx?: number;\n /** Center y coordinate. Default: 0. */\n cy?: number;\n /**\n * Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive).\n * Default: 0 (rightmost vertex).\n */\n startAngle?: number;\n /** Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. */\n blockRotation?: boolean;\n}\n/**\n * Typed handle for a constrained regular polygon in the solver.\n *\n * Structural constraints pre-applied:\n * - `equal(sides[0], sides[1])`, ..., `equal(sides[n-2], sides[n-1])` — equal sides\n * - `ccw(vertices)` — CCW winding\n *\n * Leaves **4 DOF** (center x/y, radius/scale, rotation). The center point is\n * tracked by the solver and exposed for further constraints.\n *\n * Note: Equal sides + CCW + regular initial placement makes this\n * "practically regular" for the solver. If you need geometrically exact\n * regularity (equal angles too), add `angleBetween` constraints on each pair\n * of adjacent sides.\n */\ninterface ConstrainedRegularPolygon extends ConstrainedPolygon {\n /**\n * Center point. Use `sk.fix(poly.center, x, y)` to pin location,\n * or `sk.coincident(poly.center, other)` to align with other geometry.\n */\n readonly center: PointId;\n}\n/**\n * Add a regular n-gon concept to the builder.\n *\n * Vertices are placed at `(cx + r·cos(startAngle + i·2π/n), cy + r·sin(...))`.\n * Equal-radius and equal-side constraints enforce regularity (4 DOF: center x/y, radius, rotation).\n * The center point is tracked by the solver and exposed via the returned handle.\n *\n * Use `sk.regularPolygon()` as the shorthand builder method.\n *\n * **Example**\n *\n * ```ts\n * const sk = constrainedSketch();\n * const hex = sk.regularPolygon({ sides: 6, radius: 25 });\n * sk.fix(hex.center, 0, 0);\n * sk.length(hex.side(0), 30); // all sides change (equal constraint)\n * return sk.solve().extrude(5);\n * ```\n *\n * @param sk - The builder to add the polygon to.\n * @param options - Number of sides, circumradius, center position, and rotation.\n * @returns A `ConstrainedRegularPolygon` handle extending `ConstrainedPolygon` with a `center` point.\n * @category Constrained Sketches\n */\ndeclare function addRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions): ConstrainedRegularPolygon;\ninterface ConstrainedSketchBuilder {\n /**\n * Add a regular n-gon concept (equal sides, CCW winding).\n * Returns a `ConstrainedRegularPolygon` handle with a center point.\n */\n regularPolygon(options: RegularPolygonOptions): ConstrainedRegularPolygon;\n}\ninterface GroupRectOptions {\n /** Bottom-left x coordinate (world). Default: 0. */\n x?: number;\n /** Bottom-left y coordinate (world). Default: 0. */\n y?: number;\n /** Width (along x in local coords). Required. */\n width: number;\n /** Height (along y in local coords). Required. */\n height: number;\n /** Allow the solver to rotate this rectangle. Default: false. */\n allowRotation?: boolean;\n}\ninterface ConstrainedGroupRect extends SketchGroupHandle {\n readonly bottomLeft: PointId;\n readonly bottomRight: PointId;\n readonly topRight: PointId;\n readonly topLeft: PointId;\n readonly bottom: LineId;\n readonly right: LineId;\n readonly top: LineId;\n readonly left: LineId;\n readonly shape: ShapeId;\n}\ninterface ConstrainedSketchBuilder {\n /**\n * Add a rigid rectangle as a group concept.\n * Returns a `ConstrainedGroupRect` handle with named vertices and sides.\n * The rectangle is fixed in shape — only position (and optionally rotation) varies.\n */\n groupRect(options: GroupRectOptions): ConstrainedGroupRect;\n}\ntype Vec3$3 = [\n number,\n number,\n number\n];\ninterface HermiteCurveEndpoint {\n /** Position */\n point: Vec3$3;\n /** Tangent direction (will be normalized internally) */\n tangent: Vec3$3;\n /** Weight: scales tangent magnitude relative to chord length. Default 1.0. */\n weight?: number;\n}\ndeclare class HermiteCurve3D {\n /** Start position */\n readonly p0: Vec3$3;\n /** End position */\n readonly p1: Vec3$3;\n /** Scaled tangent at start (direction * weight * chordLength) */\n readonly t0: Vec3$3;\n /** Scaled tangent at end (direction * weight * chordLength) */\n readonly t1: Vec3$3;\n /** Chord length (straight-line distance between endpoints) */\n readonly chordLength: number;\n constructor(start: HermiteCurveEndpoint, end: HermiteCurveEndpoint);\n /** Evaluate position at parameter t ∈ [0, 1] */\n pointAt(t: number): Vec3$3;\n /** Evaluate tangent (first derivative) at parameter t ∈ [0, 1] */\n tangentAt(t: number): Vec3$3;\n /** Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1] */\n curvatureAt(t: number): Vec3$3;\n /** Sample the curve as a polyline of evenly-spaced parameter values. */\n sample(count?: number): Vec3$3[];\n /** Approximate arc length by sampling. */\n length(samples?: number): number;\n /**\n * Sample with adaptive density — more points where curvature is higher.\n * Returns at least `minCount` points, up to `maxCount`.\n */\n sampleAdaptive(minCount?: number, maxCount?: number): Vec3$3[];\n /** Convert to a format compatible with sweep() path input. */\n toPolyline(samples?: number): Vec3$3[];\n}\ninterface QuinticHermiteCurveEndpoint {\n /** Position */\n point: Vec3$3;\n /** Tangent direction (will be normalized internally) */\n tangent: Vec3$3;\n /** Second derivative / curvature vector. Default [0, 0, 0]. */\n curvature?: Vec3$3;\n /** Weight: scales tangent magnitude relative to chord length. Default 1.0. */\n weight?: number;\n}\ndeclare class QuinticHermiteCurve3D {\n /** Start position */\n readonly p0: Vec3$3;\n /** End position */\n readonly p1: Vec3$3;\n /** Scaled tangent at start (direction * weight * chordLength) */\n readonly t0: Vec3$3;\n /** Scaled tangent at end (direction * weight * chordLength) */\n readonly t1: Vec3$3;\n /** Scaled second derivative at start (curvature * weight² * chordLength²) */\n readonly c0: Vec3$3;\n /** Scaled second derivative at end (curvature * weight² * chordLength²) */\n readonly c1: Vec3$3;\n /** Chord length (straight-line distance between endpoints) */\n readonly chordLength: number;\n constructor(start: QuinticHermiteCurveEndpoint, end: QuinticHermiteCurveEndpoint);\n /** Evaluate position at parameter t ∈ [0, 1] */\n pointAt(t: number): Vec3$3;\n /** Evaluate tangent (first derivative, normalized) at parameter t ∈ [0, 1] */\n tangentAt(t: number): Vec3$3;\n /** Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1] */\n curvatureAt(t: number): Vec3$3;\n /** Sample the curve as a polyline of evenly-spaced parameter values. */\n sample(count?: number): Vec3$3[];\n /** Approximate arc length by sampling. */\n length(samples?: number): number;\n /**\n * Sample with adaptive density — more points where curvature is higher.\n * Returns at least `minCount` points, up to `maxCount`.\n */\n sampleAdaptive(minCount?: number, maxCount?: number): Vec3$3[];\n /** Convert to a format compatible with sweep() path input. */\n toPolyline(samples?: number): Vec3$3[];\n}\n/**\n * Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).\n *\n * The curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`,\n * and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`,\n * with smooth G2-continuous interpolation matching position, tangent, and curvature.\n *\n * @param a - Start endpoint with position, tangent, optional curvature and weight\n * @param b - End endpoint with position, tangent, optional curvature and weight\n * @returns QuinticHermiteCurve3D instance\n */\ndeclare function hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D;\ntype Vec3$4 = [\n number,\n number,\n number\n];\ninterface NurbsCurve3DOptions {\n /** Polynomial degree (default 3 = cubic). Must be ≥ 1. */\n degree?: number;\n /** Rational weights, one per control point (default: all 1.0 = non-rational). */\n weights?: number[];\n /** Knot vector (default: uniform clamped). Must have length = controlPoints.length + degree + 1. */\n knots?: number[];\n /** Whether the curve is closed/periodic (default false). */\n closed?: boolean;\n}\ndeclare class NurbsCurve3D {\n readonly controlPoints: Vec3$4[];\n readonly weights: number[];\n readonly knots: number[];\n readonly degree: number;\n readonly closed: boolean;\n constructor(points: Vec3$4[], options?: NurbsCurve3DOptions);\n /**\n * Evaluate the curve at parameter t ∈ [0, 1].\n * Uses De Boor\'s algorithm — exact, O(degree²).\n */\n pointAt(t: number): Vec3$4;\n /**\n * Evaluate the unit tangent vector at parameter t ∈ [0, 1].\n */\n tangentAt(t: number): Vec3$4;\n /**\n * Sample the curve uniformly at `count` points.\n */\n sample(count?: number): Vec3$4[];\n /**\n * Sample with adaptive density — more points in high-curvature regions.\n */\n sampleAdaptive(minCount?: number, maxCount?: number): Vec3$4[];\n /**\n * Approximate arc length by summing polyline segment lengths.\n */\n length(samples?: number): number;\n /** Convert to a format compatible with sweep() path input. */\n toPolyline(samples?: number): Vec3$4[];\n private estimateCurvature;\n}\n/**\n * Create a NURBS curve from control points.\n *\n * With default options, creates a cubic non-rational B-spline with uniform clamped knots.\n * Set `weights` for rational curves (exact circles, conics).\n * Set `degree` for linear (1), quadratic (2), cubic (3), or higher-order curves.\n *\n * @example\n * // Simple cubic B-spline through control points\n * const curve = nurbs3d([[0,0,0], [10,5,0], [20,-5,10], [30,0,5]]);\n * const tube = sweep(circle(2), curve);\n *\n * @example\n * // Rational quadratic — exact circular arc\n * const arc = nurbs3d(\n * [[10,0,0], [10,10,0], [0,10,0]],\n * { degree: 2, weights: [1, Math.SQRT1_2, 1] }\n * );\n */\ndeclare function nurbs3d(points: Vec3$4[], options?: NurbsCurve3DOptions): NurbsCurve3D;\ntype Vec2 = [\n number,\n number\n];\ntype Vec3$5 = [\n number,\n number,\n number\n];\ninterface Spline2DOptions {\n /** Closed loop (default true). */\n closed?: boolean;\n /** Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. */\n tension?: number;\n /** Samples per segment (minimum 3). Default 16. */\n samplesPerSegment?: number;\n /**\n * For open splines, provide stroke width to return a solid Sketch.\n * If omitted for open splines, an error is thrown.\n */\n strokeWidth?: number;\n /** Stroke join for open splines. Default \'Round\'. */\n join?: "Round" | "Square";\n}\ninterface Spline3DOptions {\n /** Closed loop (default false). */\n closed?: boolean;\n /** Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. */\n tension?: number;\n}\ninterface LoftOptions {\n /** Marching-grid edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Optional extra bounds padding. */\n boundsPadding?: number;\n}\ninterface SweepOptions {\n /** Number of samples when path is a Curve3D. Default 48. */\n samples?: number;\n /** Marching-grid edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Optional extra bounds padding. */\n boundsPadding?: number;\n /**\n * Preferred "up" vector for local profile frame.\n * Auto fallback is used near parallel segments.\n */\n up?: Vec3$5;\n}\ninterface VariableSweepSection {\n /** Parameter along the spine (0 = start, 1 = end). */\n t: number;\n /** Cross-section profile at this station. */\n profile: Sketch;\n}\ninterface VariableSweepOptions {\n /** Number of samples when spine is a Curve3D. Default 48. */\n samples?: number;\n /** Marching-grid edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Optional extra bounds padding. */\n boundsPadding?: number;\n /**\n * Preferred "up" vector for local profile frame.\n * Auto fallback is used near parallel segments.\n */\n up?: Vec3$5;\n}\ndeclare class Curve3D {\n readonly points: Vec3$5[];\n readonly closed: boolean;\n readonly tension: number;\n constructor(points: Vec3$5[], options?: Spline3DOptions);\n /** Sample the curve with a fixed number of points per segment. */\n sampleBySegment(samplesPerSegment?: number): Vec3$5[];\n /** Sample the curve to an approximate total point count. */\n sample(count?: number): Vec3$5[];\n /** Map normalized t ∈ [0,1] to segment index + local parameter. */\n private segmentAt;\n /** Return the position on the curve at normalized parameter `t` in `[0, 1]`. O(1), no allocations. */\n pointAt(t: number): Vec3$5;\n /** Return a unit tangent vector at normalized parameter `t` in `[0, 1]`. O(1), analytical derivative. */\n tangentAt(t: number): Vec3$5;\n /** Approximate the curve length by polyline sampling. */\n length(samples?: number): number;\n}\n/**\n * Build a smooth Catmull-Rom spline sketch from 2D control points.\n *\n * A closed spline (default) returns a filled profile. An open spline requires\n * a strokeWidth option to produce a solid sketch. Use tension (0..1, default 0.5)\n * to control curve tightness.\n */\ndeclare function spline2d(points: Vec2[], options?: Spline2DOptions): Sketch;\n/**\n * Create a reusable 3D spline curve object (Catmull-Rom).\n *\n * The returned Curve3D provides sample(), pointAt(t), tangentAt(t), and length() for\n * downstream use in sweep() or manual path operations.\n */\ndeclare function spline3d(points: Vec3$5[], options?: Spline3DOptions): Curve3D;\n/**\n * Loft between multiple sketches along Z stations.\n *\n * Profiles can differ in topology and vertex count: interpolation is done on\n * signed-distance fields and meshed with level-set extraction. Heights must be\n * strictly increasing. Compatible loft stacks can also stay on the maintained\n * export-backend path.\n *\n * Performance note: loft is significantly heavier than primitive/extrude/revolve.\n * If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().\n */\ndeclare function loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape;\ninterface LoftAlongSpineOptions {\n /** Number of samples when spine is a Curve3D. Default 48. */\n samples?: number;\n /** Marching-grid edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Optional extra bounds padding. */\n boundsPadding?: number;\n /**\n * Preferred "up" vector for local profile frame.\n * Auto fallback is used near parallel segments.\n */\n up?: Vec3$5;\n}\n/**\n * Loft between multiple profiles positioned along an arbitrary 3D spine curve.\n *\n * Unlike loft() which only supports Z heights, loftAlongSpine() places each\n * profile at a position along a 3D spine, oriented perpendicular to the spine\n * tangent. This enables lofting along curved paths — e.g., a wing root-to-tip\n * transition that follows a swept-back leading edge.\n *\n * The tValues array specifies where each profile sits along the spine (0 = start,\n * 1 = end). Must have the same length as profiles and be in [0, 1].\n *\n * Internally uses variableSweep infrastructure with SDF interpolation.\n *\n * Performance note: uses level-set meshing, heavier than simple loft().\n */\ndeclare function loftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3$5[], tValues: number[], options?: LoftAlongSpineOptions): Shape;\ntype SweepPathInput = Curve3D | HermiteCurve3D | QuinticHermiteCurve3D | NurbsCurve3D | Vec3$5[];\ndeclare function sweep(profile: Sketch, path: SweepPathInput, options?: SweepOptions): Shape;\n/**\n * Sweep a variable cross-section along a 3D spine curve.\n *\n * Unlike sweep(), which uses a single constant profile, variableSweep()\n * interpolates between multiple profiles at different stations along the spine.\n * This enables organic shapes like tapering tubes, bone-like structures, and\n * sculptural forms.\n *\n * Each section specifies a t parameter (0 = start, 1 = end of spine) and a\n * 2D profile sketch. The SDF-based level-set mesher smoothly blends between\n * profiles at intermediate positions.\n *\n * Performance note: like sweep(), this uses level-set meshing internally.\n */\ndeclare function variableSweep(spine: SweepPathInput, sections: VariableSweepSection[], options?: VariableSweepOptions): Shape;\ntype PointArg$1 = [\n number,\n number\n] | [\n number,\n number,\n number\n] | Point2D;\ninterface DimOpts {\n offset?: number;\n label?: string;\n color?: string;\n component?: string | string[];\n currentComponent?: boolean;\n}\n/**\n * Add a dimension annotation between two points.\n *\n * **Details**\n *\n * Dimension annotations are purely visual callouts rendered in the viewport\n * and report export. They do not affect geometry or constrain the model.\n *\n * Point arguments accept 2D tuples `[x, y]`, 3D tuples `[x, y, z]`, or\n * `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\n * 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 * **Example**\n *\n * ```ts\n * dim([-w / 2, 0, 0], [w / 2, 0, 0], { label: "Width" });\n * dim([0, 0, -h / 2], [0, 0, h / 2], { label: "Height", offset: 14 });\n * dim([0, 0, 0], [100, 0, 0], { component: "Base", color: "#00AAFF" });\n * ```\n *\n * @param from - Start point as `[x, y]`, `[x, y, z]`, or `Point2D`\n * @param to - End point as `[x, y]`, `[x, y, z]`, or `Point2D`\n * @param opts - Optional: `offset` (default 10), `label`, `color` (hex),\n * `component` (string or string[] — report ownership), `currentComponent` (boolean)\n * @category Dimensions\n */\ndeclare function dim(from: PointArg$1, to: PointArg$1, opts?: DimOpts): void;\n/**\n * Add a dimension annotation along a `Line2D`.\n *\n * **Details**\n *\n * Convenience wrapper around {@link dim} that extracts the start and end\n * points from a constrained-sketch `Line2D` entity. All `opts` are forwarded\n * unchanged.\n *\n * **Example**\n *\n * ```ts\n * const a = point(0, 0);\n * const b = point(100, 0);\n * dimLine(line(a, b), { label: "Span", offset: -8 });\n * ```\n *\n * @param l - A `Line2D` entity from the constrained sketch builder\n * @param opts - Optional: `offset`, `label`, `color`, `component`, `currentComponent`\n * @see {@link dim} for point-to-point annotations\n * @category Dimensions\n */\ndeclare function dimLine(l: Line2D, opts?: DimOpts): void;\ninterface SketchDxfOptions {\n /** DXF layer name. Default: "0" */\n layer?: string;\n /** DXF color index (1–255, AutoCAD ACI). Default: 7 (white/black) */\n colorIndex?: number;\n}\n/**\n * Export a 2D sketch as a DXF string (R12/AC1009 — maximally compatible).\n *\n * **Details**\n *\n * For regular sketches, each polygon loop becomes a closed `LWPOLYLINE`.\n * For constrained sketches, exports raw `LINE`, `CIRCLE`, and `ARC` entities\n * from the constraint edge geometry, which preserves internal/shared edges\n * that `toPolygons()` would merge away.\n *\n * The R12 format is chosen for maximum compatibility with CAM tools,\n * laser-cutter software, and older CAD readers.\n *\n * **Example**\n *\n * ```ts\n * const s = rect(100, 60);\n * const dxf = sketchToDxf(s, { layer: \'cut\' });\n * ```\n *\n * @param sketch - A 2D `Sketch` to export\n * @param options - Optional DXF `layer` name and AutoCAD `colorIndex` (ACI 1–255)\n * @returns DXF markup string in R12/AC1009 format\n * @see {@link sketchToSvg} for SVG export\n * @category Export\n */\ndeclare function sketchToDxf(sketch: Sketch, options?: SketchDxfOptions): string;\ninterface SketchSvgOptions {\n /** Stroke color. Default: "black" */\n stroke?: string;\n /** Stroke width in sketch units. Default: 0.5 */\n strokeWidth?: number;\n /** Fill color. Default: "none" */\n fill?: string;\n /** Padding around the sketch bounding box in sketch units. Default: 2 */\n padding?: number;\n /** If set, scale so 1 sketch-unit = this many px. Otherwise auto-fit. */\n pixelsPerUnit?: number;\n}\n/**\n * Export a 2D sketch as an SVG string.\n *\n * **Details**\n *\n * For regular sketches, exports filled polygon regions. For constrained\n * sketches, exports raw edge geometry (LINE, ARC, CIRCLE) which preserves\n * internal/shared edges that `toPolygons()` would merge away.\n *\n * The SVG uses the sketch\'s native coordinate system (Y-up) with a CSS\n * transform that flips Y so the output renders correctly in SVG\'s Y-down\n * space. Coordinates are in sketch units (typically mm).\n *\n * **Example**\n *\n * ```ts\n * const s = rect(100, 60);\n * const svg = sketchToSvg(s, { stroke: \'#333\', strokeWidth: 0.8 });\n * ```\n *\n * @param sketch - A 2D `Sketch` to export\n * @param options - Optional stroke, fill, padding, and pixel-per-unit scale\n * @returns SVG markup string\n * @see {@link sketchToDxf} for DXF export\n * @category Export\n */\ndeclare function sketchToSvg(sketch: Sketch, options?: SketchSvgOptions): string;\n/**\n * Round a tracked vertical solid edge with a circular fillet.\n *\n * **Details**\n *\n * Compiler-owned fillet for a narrow tracked-edge subset on solids.\n *\n * This is **not** a general 2D sketch-corner fillet. It currently works only\n * on tracked vertical edges from `box()` or `Rectangle2D` extrusions (plus\n * rigid transforms and supported preserved descendants of those). Generic\n * sketch extrudes, including `rect(...).extrude(...)`, are outside the\n * supported subset right now.\n *\n * **Supported edges:**\n * - Tracked vertical edges from `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,\n * intersection, generic sketch extrudes, or tapered extrudes.\n *\n * Canonical quadrants: `vert-bl → [1,-1]`, `vert-br → [-1,-1]`,\n * `vert-tr → [-1,1]`, `vert-tl → [1,1]`\n *\n * **Example**\n *\n * ```ts\n * const base = Rectangle2D.fromDimensions(0, 0, 50, 50).extrude(20);\n * const filleted = filletTrackedEdge(base, base.edge(\'vert-br\'), 5, [-1, -1]);\n * ```\n *\n * @param shape - Compile-covered solid to modify\n * @param edge - `EdgeRef` from the same tracked target body (e.g. `base.edge(\'vert-br\')`)\n * @param radius - Fillet radius (must be positive and finite)\n * @param quadrant - Corner direction sign pair (default: `[-1, -1]`)\n * @param segments - Arc resolution (default: `16`)\n * @returns A new Shape with the fillet applied\n * @see {@link fillet} for the general-purpose query-based variant\n * @see {@link chamferTrackedEdge} for beveled tracked edges\n * @see {@link filletCorners} for selective 2D sketch-corner rounding\n * @see {@link Sketch.offset} for rounding all convex corners of a sketch\n * @category Edge Features\n */\ndeclare function filletTrackedEdge(shape: Shape, edge: EdgeRef, radius: number, quadrant?: [\n number,\n number\n], segments?: number): Shape;\n/**\n * Bevel a tracked vertical solid edge with a 45° chamfer.\n *\n * **Details**\n *\n * Compiler-owned chamfer for tracked vertical edges. Requires a\n * compile-plan-covered target. This is not a general 2D sketch-corner tool;\n * supported subset and quadrant semantics are the same as `filletTrackedEdge()` - see\n * that function for details.\n *\n * **Example**\n *\n * ```ts\n * const base = Rectangle2D.fromDimensions(0, 0, 50, 50).extrude(20);\n * const chamfered = chamferTrackedEdge(base, base.edge(\'vert-br\'), 3, [-1, -1]);\n * ```\n *\n * @param shape - Compile-covered solid to modify\n * @param edge - `EdgeRef` from the same tracked target body (e.g. `base.edge(\'vert-br\')`)\n * @param size - Chamfer size (must be positive and finite)\n * @param quadrant - Corner direction sign pair (default: `[-1, -1]`)\n * @returns A new Shape with the chamfer applied\n * @see {@link chamfer} for the general-purpose query-based variant\n * @see {@link filletTrackedEdge} for rounded tracked edges\n * @see {@link filletCorners} for selective 2D sketch-corner rounding\n * @category Edge Features\n */\ndeclare function chamferTrackedEdge(shape: Shape, edge: EdgeRef, size: number, quadrant?: [\n number,\n number\n]): Shape;\ninterface FilletCornerSpec {\n index: number;\n radius: number;\n segments?: number;\n}\ntype PointInput = [\n number,\n number\n] | Point2D;\n/**\n * Create a polygon from points with specific corners rounded to arc fillets.\n *\n * **Details**\n *\n * Each corner spec identifies a vertex by its index in the `points` array and\n * the desired fillet `radius`. Both convex and concave corners are supported.\n *\n * Constraints:\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 *\n * Use `offset(-r).offset(+r)` instead if you want to round **all** convex corners\n * uniformly. Use `filletCorners` when you need selective or mixed sharp/rounded profiles.\n *\n * **Example**\n *\n * ```ts\n * const roof = filletCorners(roofPoints, [\n * { index: 3, radius: 19 },\n * { index: 4, radius: 19 },\n * { index: 5, radius: 19 },\n * ]);\n * ```\n *\n * @param points - Closed polygon vertices as `[x, y][]` or `Point2D[]`\n * @param corners - Fillet specs — each has `index` (vertex index), `radius`, and optional `segments`\n * @returns A new polygon sketch with the specified corners filleted\n * @see {@link Sketch.offset} for rounding all convex corners uniformly\n * @category Sketch Operations\n */\ndeclare function filletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch;\n/**\n * Pre-load and cache a font for use with `text2d()`.\n *\n * **Details**\n *\n * Fonts are cached by their source string (or `cacheKey` for `ArrayBuffer` sources),\n * so repeated calls with the same path are free. Pre-loading is useful when you call\n * `text2d()` many times with the same font — it avoids repeated disk reads.\n *\n * Built-in font names that work everywhere (browser + CLI):\n * - `\'sans-serif\'` or `\'inter\'` — bundled Inter Regular\n *\n * **Example**\n *\n * ```ts\n * const font = loadFont(\'/path/to/Arial Bold.ttf\');\n * text2d(\'Title\', { size: 12, font }).extrude(1.5);\n * text2d(\'Subtitle\', { size: 8, font }).extrude(1);\n * ```\n *\n * @param source - Built-in name (`\'sans-serif\'`/`\'inter\'`), file path (CLI only), or `ArrayBuffer`\n * @param cacheKey - Optional cache key when passing an `ArrayBuffer`\n * @returns Parsed opentype.js Font object\n * @see {@link text2d} to use the loaded font\n * @category Sketch Text\n */\ndeclare function loadFont(source: string | ArrayBuffer, cacheKey?: string): opentype$1.Font;\ntype Vec3$6 = [\n number,\n number,\n number\n];\ninterface NurbsSurfaceOptions {\n /** Degree in U direction (default 3). */\n degreeU?: number;\n /** Degree in V direction (default 3). */\n degreeV?: number;\n /** Weights grid — same dimensions as controlGrid (default: all 1.0). */\n weights?: number[][];\n /** Knot vector in U direction (default: uniform clamped). */\n knotsU?: number[];\n /** Knot vector in V direction (default: uniform clamped). */\n knotsV?: number[];\n /** Sheet thickness — if > 0, thickens the surface into a solid (default 0 = surface only). */\n thickness?: number;\n /** Tessellation resolution — points per direction (default 32). */\n resolution?: number;\n /** Explicit opt-in for sampled fallback paths on non-exact backends. */\n approximate?: boolean;\n}\ndeclare class NurbsSurface {\n readonly controlGrid: Vec3$6[][];\n readonly weightsGrid: number[][];\n readonly knotsU: number[];\n readonly knotsV: number[];\n readonly degreeU: number;\n readonly degreeV: number;\n readonly nU: number;\n readonly nV: number;\n constructor(controlGrid: Vec3$6[][], options?: NurbsSurfaceOptions);\n /**\n * Evaluate the surface at parameters (u, v) ∈ [0, 1]².\n * Uses tensor product evaluation: evaluate basis functions in U and V independently.\n */\n pointAt(u: number, v: number): Vec3$6;\n /**\n * Evaluate the surface normal at (u, v) via cross product of partial derivatives.\n */\n normalAt(u: number, v: number): Vec3$6;\n /**\n * Tessellate the surface into a triangle mesh.\n * Returns positions, normals, and triangle indices.\n */\n tessellate(resU?: number, resV?: number): {\n positions: Vec3$6[];\n normals: Vec3$6[];\n indices: number[];\n };\n private remapU;\n private remapV;\n}\n/**\n * Create a NURBS surface from a grid of control points.\n *\n * The control grid is indexed as `controlGrid[u][v]` — each row is a curve\n * in the V direction, and columns trace curves in the U direction.\n *\n * With default options, creates a bicubic non-rational B-spline surface\n * with uniform clamped knots.\n *\n * @example\n * // Simple 4×4 control grid — a gently curved surface\n * const 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 * ];\n * const surface = nurbsSurface(grid, { thickness: 2 });\n */\ndeclare function nurbsSurface(controlGrid: Vec3$6[][], options?: NurbsSurfaceOptions): Shape;\n/**\n * Layout helpers — eliminate manual trigonometry for common positioning patterns.\n *\n * These functions return arrays of {x, y} positions that can be used with\n * translate(), circularPattern seed placement, or any other positioning API.\n */\ninterface LayoutPoint {\n x: number;\n y: number;\n}\ninterface CircularLayoutOptions {\n /** Angle of the first element in degrees (default: 0 = +X axis). */\n startDeg?: number;\n /** Center X coordinate (default: 0). */\n centerX?: number;\n /** Center Y coordinate (default: 0). */\n centerY?: number;\n}\n/**\n * Compute evenly-spaced positions around a circle.\n *\n * Eliminates the most common trig pattern in CAD scripts:\n * ```js\n * // Before — manual trig\n * for (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\n * for (const {x, y} of circularLayout(12, r)) {\n * markers.push(marker.translate(x, y, 0));\n * }\n * ```\n */\ndeclare function circularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[];\ninterface PolygonVerticesOptions {\n /** Angle of the first vertex in degrees (default: 90 = top). */\n startDeg?: number;\n /** Center X coordinate (default: 0). */\n centerX?: number;\n /** Center Y coordinate (default: 0). */\n centerY?: number;\n}\n/**\n * Compute the vertex positions of a regular polygon.\n *\n * Default orientation places the first vertex at the top (90 degrees),\n * matching the convention used by `ngon()`.\n *\n * Eliminates manual Math.sqrt(3) for triangles, pentagon vertex math, etc:\n * ```js\n * // Before — manual equilateral triangle\n * const v1 = [center.x - r/2, center.y + r * Math.sqrt(3)/2];\n * const v2 = [center.x - r/2, center.y - r * Math.sqrt(3)/2];\n * const v3 = [center.x + r, center.y];\n *\n * // After — declarative\n * const [v1, v2, v3] = polygonVertices(3, r);\n * ```\n */\ndeclare function polygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[];\ndeclare class PathBuilder {\n private segs;\n private x;\n private y;\n /** Current departure tangent unit vector. */\n private dirX;\n private dirY;\n /**\n * Current cursor X position.\n *\n * @returns The current X coordinate.\n */\n getX(): number;\n /**\n * Current cursor Y position.\n *\n * @returns The current Y coordinate.\n */\n getY(): number;\n /**\n * Move the cursor to an absolute position without drawing a segment.\n *\n * When called after the initial `path()`, this establishes the start of the outline.\n * Calling `moveTo` again mid-path starts a new sub-path (hole in `close()`, separate\n * segment for `stroke()`).\n *\n * @param x - Absolute X coordinate.\n * @param y - Absolute Y coordinate.\n * @category Path Builder\n */\n moveTo(x: number, y: number): this;\n /**\n * Draw a straight line from the current cursor to an absolute position.\n *\n * @param x - Absolute X coordinate of the endpoint.\n * @param y - Absolute Y coordinate of the endpoint.\n * @category Path Builder\n */\n lineTo(x: number, y: number): this;\n /**\n * Draw a horizontal line segment by `dx` units from the current cursor.\n *\n * Positive `dx` moves right; negative moves left.\n *\n * @param dx - Horizontal displacement.\n * @category Path Builder\n */\n lineH(dx: number): this;\n /**\n * Draw a vertical line segment by `dy` units from the current cursor.\n *\n * Positive `dy` moves up; negative moves down.\n *\n * @param dy - Vertical displacement.\n * @category Path Builder\n */\n lineV(dy: number): this;\n /**\n * Draw a line at the given angle and length from the current cursor.\n *\n * Angle convention: `0°` points right (+X), `90°` points up (+Y).\n *\n * **Example**\n *\n * ```ts\n * // L-bracket with angled return\n * path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n * ```\n *\n * @param length - Length of the line segment.\n * @param degrees - Angle in degrees (0 = right, 90 = up).\n * @category Path Builder\n */\n lineAngled(length: number, degrees: number): this;\n /**\n * Draw a line by a relative `(dx, dy)` displacement from the current cursor.\n *\n * @param dx - Horizontal displacement.\n * @param dy - Vertical displacement.\n * @category Path Builder\n */\n lineBy(dx: number, dy: number): this;\n /**\n * Draw an arc to a point offset from the current cursor.\n *\n * @param dx - Relative X displacement to the end point.\n * @param dy - Relative Y displacement to the end point.\n * @param radius - Arc radius.\n * @param clockwise - Sweep direction. Default: `false` (CCW).\n * @category Path Builder\n */\n arcBy(dx: number, dy: number, radius: number, clockwise?: boolean): this;\n /**\n * Draw a cubic Bezier using control points relative to the current cursor.\n *\n * @param dcp1x - Relative X of the first control point.\n * @param dcp1y - Relative Y of the first control point.\n * @param dcp2x - Relative X of the second control point.\n * @param dcp2y - Relative Y of the second control point.\n * @param dx - Relative X of the end point.\n * @param dy - Relative Y of the end point.\n * @category Path Builder\n */\n bezierBy(dcp1x: number, dcp1y: number, dcp2x: number, dcp2y: number, dx: number, dy: number): this;\n /**\n * Draw a circular arc from the current position to (x, y) with the given radius.\n * `clockwise=true` → arc curves to the right of the start→end direction.\n * `clockwise=false` → arc curves to the left of the start→end direction.\n */\n arcTo(x: number, y: number, radius: number, clockwise?: boolean): this;\n /**\n * G1-continuous arc — radius derived from current tangent + endpoint.\n * Throws if endpoint is collinear with current direction.\n */\n tangentArcTo(x: number, y: number): this;\n /**\n * Draw an arc defined by center, radius, and angle range (no trig needed).\n * If the path has no segments yet, automatically moves to the arc start.\n * Positive sweep (startDeg < endDeg) = CCW, negative = CW.\n *\n * ```js\n * // Arc centered at (10, 0), radius 50, from -30° to +30°\n * path().arc(10, 0, 50, -30, 30).stroke(8, \'Round\')\n * ```\n */\n arc(cx: number, cy: number, radius: number, startDeg: number, endDeg: number): this;\n /**\n * Arc around a known center point, sweeping by the given angle.\n * Radius is derived from the distance between the current position and the center.\n * Positive sweep = CCW (math convention), negative = CW.\n *\n * ```js\n * // Arc 90° CCW around (50, 50)\n * path().moveTo(70, 50).arcAround(50, 50, 90)\n * // Arc 45° CW around the origin\n * path().moveTo(10, 0).arcAround(0, 0, -45)\n * ```\n */\n arcAround(cx: number, cy: number, sweepDeg: number): this;\n /**\n * Arc around a center point given as an offset from the current position.\n * `(dx, dy)` is the vector from the current point to the center.\n * Positive sweep = CCW (math convention), negative = CW.\n *\n * ```js\n * // Arc 90° CCW around a center 20 units to the right\n * path().moveTo(50, 50).arcAroundRelative(20, 0, 90)\n * // Equivalent to: path().moveTo(50, 50).arcAround(70, 50, 90)\n * ```\n */\n arcAroundRelative(dx: number, dy: number, sweepDeg: number): this;\n /**\n * Smooth three-arc end cap from the current position to (endX, endY).\n * Inserts: small corner arc → large cap arc → small corner arc, all G1-continuous.\n */\n smoothCapTo(endX: number, endY: number, cornerRadius: number, capRadius: number): this;\n /**\n * Cubic bezier from current position to (x, y) via two control points.\n */\n bezierTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): this;\n /**\n * G1-continuous cubic bezier — first control point is auto-derived from\n * the current tangent direction. `weight` controls how far the auto-placed\n * control point extends along the tangent (default: 1/3 of the chord).\n *\n * The second control point `(cp2x, cp2y)` must be provided — it controls\n * the arrival curvature. For a fully automatic smooth curve, see `smoothThrough`.\n */\n tangentBezierTo(cp2x: number, cp2y: number, x: number, y: number, weight?: number): this;\n /**\n * Catmull-Rom spline through a list of waypoints from the current position.\n * The current position is included as the first point. The last waypoint\n * becomes the new cursor position.\n *\n * @param waypoints — intermediate + final points (at least 1)\n * @param tension — 0 = very round, 1 = linear (default 0.5)\n */\n smoothThrough(waypoints: [\n number,\n number\n ][], tension?: number): this;\n /**\n * Rational B-spline edge to (x, y) with explicit control points and weights.\n *\n * The control points define the B-spline shape between the current position\n * and (x, y). The current position is NOT included in `controlPoints` — it is\n * automatically prepended. The endpoint (x, y) is the last control point.\n *\n * @param controlPoints — interior + endpoint control points (endpoint = last)\n * @param opts.weights — rational weights (default: all 1.0)\n * @param opts.degree — B-spline degree (default: control point count - 1, capped at 3)\n */\n nurbsTo(controlPoints: [\n number,\n number\n ][], opts?: {\n weights?: number[];\n degree?: number;\n }): this;\n /**\n * Exact circular arc to (x, y) using a rational quadratic NURBS.\n *\n * Unlike `arcTo()` which tessellates to a polyline, this preserves the\n * exact arc definition. When extruded through the OCCT backend, it produces\n * a true cylindrical face — not a faceted approximation.\n *\n * @param x — endpoint X\n * @param y — endpoint Y\n * @param opts.radius — arc radius (default: auto-computed from chord)\n * @param opts.clockwise — winding direction (default: false = CCW)\n */\n exactArcTo(x: number, y: number, opts?: {\n radius?: number;\n clockwise?: boolean;\n }): this;\n /**\n * Round the last corner (the junction between the previous two segments)\n * with a tangent arc of the given radius.\n *\n * Must be called after at least two line/arc segments that form a corner.\n * The fillet trims back both segments and inserts a tangent arc.\n *\n * ```js\n * path().moveTo(0,0).lineTo(10,0).lineTo(10,10).fillet(2).lineTo(0,10).close()\n * ```\n */\n fillet(radius: number): this;\n /**\n * Chamfer the last corner with a straight cut of the given distance.\n *\n * ```js\n * path().moveTo(0,0).lineTo(10,0).lineTo(10,10).chamfer(2).lineTo(0,10).close()\n * ```\n */\n chamfer(distance: number): this;\n private computeFilletGeom;\n private computeChamferGeom;\n private getSegEnd;\n private getSegDirAt;\n private trimLastSegEnd;\n /**\n * Mirror all existing segments across an axis and append the mirrored copy\n * in reverse order, creating a symmetric path. The axis passes through the\n * current cursor position.\n *\n * @param axis — \'x\' mirrors across the local X-axis (flips Y),\n * \'y\' mirrors across the local Y-axis (flips X),\n * or `[nx, ny]` for an arbitrary axis direction.\n *\n * ```js\n * // Build right half, mirror to get full symmetric profile\n * path().moveTo(0,0).lineTo(10,0).lineTo(10,5).mirror(\'x\').close()\n * ```\n */\n mirror(axis: "x" | "y" | [\n number,\n number\n ]): this;\n /**\n * Label the most recently added segment. Labels are born here and grow\n * into face names when the sketch is extruded, lofted, swept, or revolved.\n *\n * Labels must be unique within a path. Each segment can have at most one label.\n */\n label(name: string): this;\n /**\n * Label the closing segment and close the path. Shorthand for labeling the\n * implicit line from the last point back to the start, then closing.\n */\n closeLabel(name: string): Sketch;\n /** Label for the implicit closing segment (set by closeLabel). */\n private _closingLabel;\n /** Expand all segments into a flat tessellated polyline. */\n private tessellate;\n /**\n * Close the path and return a filled `Sketch`.\n *\n * **Details**\n *\n * The winding of the polygon is automatically corrected to CCW (the expected\n * orientation for ForgeCAD sketches). If the path contains multiple sub-paths\n * (started with subsequent `moveTo` calls), the first sub-path is the outer\n * contour and subsequent sub-paths become holes subtracted from it.\n *\n * Edge labels (assigned with `.label(\'name\')`) are transferred to the resulting\n * sketch and propagate through `extrude()`, `revolve()`, `loft()`, and `sweep()`\n * into named faces on the resulting `Shape`.\n *\n * **Example**\n *\n * ```ts\n * const triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();\n *\n * // With a hole (second sub-path)\n * const 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 * @returns A filled `Sketch` with correct CCW winding.\n * @category Path Builder\n */\n close(): Sketch;\n /**\n * Close the path and return an offset version of the filled Sketch.\n * Positive delta expands outward, negative shrinks inward.\n */\n closeOffset(delta: number, join?: "Round" | "Square" | "Miter"): Sketch;\n /**\n * Thicken an open polyline (centerline) into a solid filled profile with uniform width.\n *\n * **Details**\n *\n * Expands the path into a closed profile `width` units wide (half-width on each side\n * of the centerline). Use `\'Round\'` for ribs, wire traces, and organic profiles —\n * it adds semicircular endcaps and rounds joins. Use `\'Square\'` (default) for sharp\n * miter joins without endcaps.\n *\n * Not the same as rounding corners of a closed polygon — for mixed sharp-and-rounded\n * outlines, build the polygon first and apply `filletCorners()`.\n *\n * **Example**\n *\n * ```ts\n * // Square-join L-bracket\n * const bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n *\n * // Round-join rib\n * const rib = path().moveTo(0, 0).lineH(60).stroke(6, \'Round\');\n *\n * // Equivalent standalone form\n * const wire = stroke([[0, 0], [50, 0], [50, -70]], 4);\n * ```\n *\n * @param width - Total stroke width (half on each side of the centerline).\n * @param join - `\'Square\'` for miter joins (default) or `\'Round\'` for rounded joins\n * and semicircular endcaps.\n * @returns A filled `Sketch` representing the thickened profile.\n * @category Path Builder\n */\n stroke(width: number, join?: "Round" | "Square"): Sketch;\n /** Split segments into sub-paths at each moveTo. */\n private splitSubPaths;\n /** Build semantic ProfileEdge array from path segments (for pathProfile compile plan). */\n private buildProfileEdges;\n private profileEdgeCountForSeg;\n /** Tessellate a sub-path (sequence of segments). */\n private tessellateSegs;\n}\n/**\n * Create a new `PathBuilder` for tracing a 2D outline point by point.\n *\n * **Details**\n *\n * `PathBuilder` is a fluent API for constructing 2D profiles using a mix of line\n * segments, arcs, bezier curves, and splines. Always start with `.moveTo(x, y)` to\n * set the starting point. Call `.close()` to get a filled `Sketch`, or `.stroke(width)`\n * to thicken an open polyline into a solid profile.\n *\n * Edge labels can be assigned with `.label(\'name\')` after any segment — they propagate\n * through extrusion, revolve, loft, and sweep into named faces on the resulting `Shape`.\n *\n * **Example**\n *\n * ```ts\n * // Closed triangle\n * const triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();\n *\n * // L-shaped bracket as a stroke\n * const bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);\n *\n * // Labeled edges for downstream face references\n * const 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 * @returns A new `PathBuilder` instance with the cursor at the origin.\n * @category Path Builder\n */\ndeclare function path(): PathBuilder;\n/** Create a stroked polyline sketch from an array of 2D points. */\ndeclare function stroke(points: [\n number,\n number\n][], width: number, join?: "Round" | "Square"): Sketch;\n/**\n * Repeat a shape in a linear pattern along a direction vector and union the copies.\n *\n * **Details**\n *\n * Creates `count` copies of `shape`, each offset by `(dx*i, dy*i, dz*i)`\n * from the original. All copies are unioned into a single `Shape`. Distinct\n * compiler ownership is assigned to each copy so face identity via\n * owner-scoped canonical queries still works post-merge.\n *\n * **Example**\n *\n * ```ts\n * // 5 cylinders, 20mm apart along X\n * linearPattern(cylinder(10, 3), 5, 20, 0)\n * ```\n *\n * @param shape - The shape to repeat\n * @param count - Total number of copies (including the original at index 0)\n * @param dx - X step per copy\n * @param dy - Y step per copy\n * @param dz - Z step per copy (default: `0`)\n * @returns Union of all copies\n * @see {@link circularPattern} for rotational patterns\n * @see {@link mirrorCopy} for mirror patterns\n * @category Tracked Shapes\n */\ndeclare function linearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape;\ninterface CircularPatternOptions {\n /** Center X of the rotation (default: 0). Used when axis is Z (legacy mode). */\n centerX?: number;\n /** Center Y of the rotation (default: 0). Used when axis is Z (legacy mode). */\n centerY?: number;\n /** Rotation axis direction (default: [0, 0, 1] = Z axis). */\n axis?: [\n number,\n number,\n number\n ];\n /** Pivot point for the rotation (default: [0, 0, 0]). Overrides centerX/centerY when set. */\n origin?: [\n number,\n number,\n number\n ];\n}\n/**\n * Repeat a shape in a circular pattern around an axis and union the copies.\n *\n * **Details**\n *\n * Distributes `count` copies evenly around the rotation axis (360° / count\n * per step). All copies are unioned into a single `Shape`. Distinct compiler\n * ownership is assigned to each copy — post-merge face identity via\n * owner-scoped canonical queries still works for pattern descendants.\n *\n * Two calling conventions:\n * - **Simple** (Z axis): `circularPattern(shape, 6)` or\n * `circularPattern(shape, 6, centerX, centerY)`\n * - **Advanced** (arbitrary axis): `circularPattern(shape, 6, { axis, origin })`\n *\n * **Example**\n *\n * ```ts\n * // 8 holes evenly spaced around origin\n * circularPattern(cylinder(12, 4).translate(30, 0, -1), 8)\n *\n * // Circular pattern around X axis\n * circularPattern(myFeature, 4, { axis: [1, 0, 0], origin: [0, 0, 50] })\n * ```\n *\n * @param shape - The shape to repeat\n * @param count - Total number of copies (including the original at index 0)\n * @param centerXOrOpts - Center X offset, or options object with `axis`/`origin`/`centerX`/`centerY`\n * @param centerY - Center Y offset (only when using the simple two-number form)\n * @returns Union of all copies\n * @see {@link linearPattern} for linear patterns\n * @see {@link mirrorCopy} for mirror patterns\n * @category Tracked Shapes\n */\ndeclare function circularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape;\n/**\n * Repeat a 2D sketch in a linear pattern and union the copies.\n *\n * @param sketch - The sketch to repeat\n * @param count - Total number of copies\n * @param dx - X step per copy\n * @param dy - Y step per copy (default: `0`)\n * @returns Union of all copies as a single Sketch\n * @category Tracked Shapes\n */\ndeclare function linearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch;\n/**\n * Repeat a 2D sketch in a circular pattern around a center point and union the copies.\n *\n * @param sketch - The sketch to repeat\n * @param count - Total number of copies\n * @param centerXOrOpts - Center X, or options object with `centerX`/`centerY`/`startDeg`\n * @param centerY - Center Y (only when using the two-number form)\n * @returns Union of all copies as a single Sketch\n * @category Tracked Shapes\n */\ndeclare function circularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | {\n centerX?: number;\n centerY?: number;\n startDeg?: number;\n}, centerY?: number): Sketch;\n/**\n * Mirror a shape across a plane and union the mirror with the original.\n *\n * **Details**\n *\n * The mirror plane passes through the origin and is defined by its normal\n * vector. The mirrored copy is unioned with the original to produce a single\n * symmetric Shape.\n *\n * **Example**\n *\n * ```ts\n * // Mirror across the YZ plane (X=0)\n * mirrorCopy(box(50, 30, 10), [1, 0, 0])\n * ```\n *\n * @param shape - The shape to mirror\n * @param normal - Normal of the mirror plane (e.g. `[1, 0, 0]` for YZ plane)\n * @returns Union of original and its mirror\n * @see {@link linearPattern} for linear repetition\n * @see {@link circularPattern} for circular repetition\n * @category Tracked Shapes\n */\ndeclare function mirrorCopy(shape: Shape, normal: [\n number,\n number,\n number\n]): Shape;\n/**\n * Create a 2D rectangle centered at the origin.\n *\n * **Example**\n *\n * ```ts\n * rect(40, 20).extrude(5);\n * ```\n *\n * @param width - Width along the X axis\n * @param height - Height along the Y axis\n * @returns A centered rectangle sketch\n * @category Sketch Primitives\n */\ndeclare function rect(width: number, height: number): Sketch;\n/**\n * Create a 2D circle centered at the origin.\n *\n * **Details**\n *\n * Omit `segments` for a smooth (auto-tessellated) circle. Pass an integer to get a\n * regular polygon approximation — e.g. `6` for a hexagon, `8` for an octagon.\n *\n * **Example**\n *\n * ```ts\n * circle2d(25).extrude(10); // smooth cylinder\n * circle2d(25, 6).extrude(10); // hexagonal prism\n * ```\n *\n * @param radius - Radius of the circle\n * @param segments - Number of polygon sides (omit for smooth)\n * @returns A circle sketch\n * @category Sketch Primitives\n */\ndeclare function circle2d(radius: number, segments?: number): Sketch;\n/**\n * Create a 2D rectangle with rounded corners, centered at the origin.\n *\n * **Details**\n *\n * The corner radius is automatically clamped to `min(width/2, height/2)` so it\n * can never exceed the shape dimensions.\n *\n * **Example**\n *\n * ```ts\n * roundedRect(60, 30, 5).extrude(3);\n * ```\n *\n * @param width - Width along the X axis\n * @param height - Height along the Y axis\n * @param radius - Corner fillet radius (clamped to fit)\n * @returns A rounded rectangle sketch\n * @category Sketch Primitives\n */\ndeclare function roundedRect(width: number, height: number, radius: number): Sketch;\n/**\n * Create a 2D polygon from an array of `[x, y]` points or `Point2D` objects.\n *\n * **Details**\n *\n * Winding order is normalized automatically — clockwise (CW) input is silently\n * reversed to CCW before being passed to the geometry kernel.\n *\n * **Example**\n *\n * ```ts\n * polygon([[0, 0], [50, 0], [25, 40]]).extrude(5); // triangle\n * ```\n *\n * @param points - Closed polygon vertices as `[x, y]` pairs or `Point2D` objects\n * @returns A polygon sketch\n * @category Sketch Primitives\n */\ndeclare function polygon(points: ([\n number,\n number\n] | Point2D)[]): Sketch;\n/**\n * Create a regular polygon inscribed in a circle of the given radius.\n *\n * **Details**\n *\n * `radius` is the center-to-vertex (circumradius) distance. Use `sides` of `3` for a\n * triangle, `6` for a hexagon, etc. The first vertex is at the top (−90° from +X).\n *\n * **Example**\n *\n * ```ts\n * ngon(6, 20).extrude(10); // hexagonal prism, circumradius 20\n * ```\n *\n * @param sides - Number of polygon sides\n * @param radius - Circumradius (center to vertex)\n * @returns A regular polygon sketch\n * @category Sketch Primitives\n */\ndeclare function ngon(sides: number, radius: number): Sketch;\n/**\n * Create a 2D ellipse centered at the origin.\n *\n * **Example**\n *\n * ```ts\n * ellipse(30, 15).extrude(5);\n * ellipse(30, 15, 32).extrude(5); // lower-resolution approximation\n * ```\n *\n * @param rx - Radius along the X axis\n * @param ry - Radius along the Y axis\n * @param segments - Number of polygon sides (default 64)\n * @returns An ellipse sketch\n * @category Sketch Primitives\n */\ndeclare function ellipse(rx: number, ry: number, segments?: number): Sketch;\n/**\n * Create a slot (oblong / stadium shape) — a rectangle with semicircular ends, centered at the origin.\n *\n * **Example**\n *\n * ```ts\n * slot(40, 10).extrude(3); // 40mm long, 10mm wide slot\n * ```\n *\n * @param length - Overall length (tip to tip)\n * @param width - Overall width (= diameter of the semicircular ends)\n * @returns A slot sketch\n * @category Sketch Primitives\n */\ndeclare function slot(length: number, width: number): Sketch;\n/**\n * Create an arc-shaped slot (banana / annular sector) centered at the origin.\n *\n * **Details**\n *\n * The slot is symmetric about the +X axis. The two ends are closed with\n * semicircular caps. `pitchRadius` is the distance from the origin to the\n * centerline of the slot, and `thickness` is the radial width of the slot.\n *\n * **Example**\n *\n * ```ts\n * arcSlot(135, 74, 40).extrude(5); // pitch R135, 74° sweep, 40mm wide\n * ```\n *\n * @param pitchRadius - Distance from the center to the middle of the slot\n * @param sweepDeg - Angular extent of the slot in degrees\n * @param thickness - Radial width of the slot\n * @returns An arc slot sketch\n * @category Sketch Primitives\n */\ndeclare function arcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch;\n/**\n * Create a star shape with alternating outer and inner radii.\n *\n * **Example**\n *\n * ```ts\n * star(5, 30, 12).extrude(4); // five-pointed star\n * ```\n *\n * @param points - Number of star points\n * @param outerR - Radius to the tips\n * @param innerR - Radius to the valleys\n * @returns A star sketch\n * @category Sketch Primitives\n */\ndeclare function star(points: number, outerR: number, innerR: number): Sketch;\ntype Vec3$7 = [\n number,\n number,\n number\n];\ninterface SurfacePatchOptions {\n /** Number of samples along each direction. Default 24. */\n resolution?: number;\n /** Thickness of the generated solid. Default 0 for an open exact sheet. */\n thickness?: number;\n /** Allow explicit approximation for non-exact curve inputs such as Curve3D samples. */\n approximate?: boolean;\n}\n/**\n * Create a smooth surface patch from 4 boundary curves (Coons patch).\n *\n * The four curves form the boundary of a quadrilateral patch:\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 *\n * The interior is filled using bilinear Coons patch interpolation:\n * P(u,v) = Lc(u,v) + Ld(u,v) - B(u,v)\n *\n * The result is a thin solid created by offsetting the surface mesh\n * along its normals by the specified thickness.\n *\n * Note: curves should meet at corners. Small gaps are tolerated.\n */\ndeclare function surfacePatch(curves: {\n bottom: Curve3D | Vec3$7[];\n top: Curve3D | Vec3$7[];\n left: Curve3D | Vec3$7[];\n right: Curve3D | Vec3$7[];\n}, options?: SurfacePatchOptions): Shape;\ninterface SvgImportOptions {\n /**\n * Which geometry channels to include:\n * - `auto`: prefer fills; if no fill geometry exists, fall back to strokes\n * - `fill`: import only filled regions\n * - `stroke`: import only stroke geometry\n * - `fill-and-stroke`: include both\n */\n include?: "auto" | "fill" | "stroke" | "fill-and-stroke";\n /** Keep all disconnected regions, or only the largest. */\n regionSelection?: "all" | "largest";\n /** Keep at most this many regions (largest-first). */\n maxRegions?: number;\n /** Drop regions below this absolute area threshold. */\n minRegionArea?: number;\n /** Drop regions below this ratio of largest-region area. */\n minRegionAreaRatio?: number;\n /**\n * Curve flattening tolerance in SVG user units.\n * Smaller = more segments, higher fidelity.\n */\n flattenTolerance?: number;\n /** Minimum segment count for arc discretization. */\n arcSegments?: number;\n /** Global scale applied after SVG parsing. */\n scale?: number;\n /**\n * Maximum imported sketch width.\n * If exceeded, geometry is uniformly downscaled to fit.\n */\n maxWidth?: number;\n /**\n * Maximum imported sketch height.\n * If exceeded, geometry is uniformly downscaled to fit.\n */\n maxHeight?: number;\n /**\n * Recenter imported geometry so its 2D bounds center is at CAD origin.\n */\n centerOnOrigin?: boolean;\n /** Simplification tolerance for final sketch cleanup. */\n simplify?: number;\n /**\n * Flip SVG Y-down coordinates to CAD Y-up.\n * Enabled by default.\n */\n invertY?: boolean;\n}\ninterface TextOptions {\n /**\n * Cap height of the text in model units.\n * All other dimensions (stroke weight, spacing) scale proportionally.\n * @default 10\n */\n size?: number;\n /**\n * Extra space between characters in model units.\n * Negative values tighten the tracking.\n * @default 0\n */\n letterSpacing?: number;\n /**\n * Horizontal alignment relative to x = 0.\n * - `\'left\'` — left edge at x = 0 (default)\n * - `\'center\'` — centred on x = 0\n * - `\'right\'` — right edge at x = 0\n * @default \'left\'\n */\n align?: "left" | "center" | "right";\n /**\n * Vertical alignment relative to y = 0.\n * - `\'baseline\'` — y = 0 is the text baseline (bottom of capital letters)\n * - `\'center\'` — y = 0 is the vertical midpoint of the cap height\n * - `\'top\'` — y = 0 is the top of capital letters\n * @default \'baseline\'\n */\n baseline?: "baseline" | "center" | "top";\n /**\n * Font to use for text rendering.\n *\n * - `\'sans-serif\'` or `\'inter\'` — bundled Inter font (works everywhere, including browser)\n * - **file path** — path to a TTF, OTF, or WOFF font file (CLI/Node only)\n * - **Font object** — a previously loaded opentype.js Font (from `loadFont()`)\n * - **omitted** — uses the bundled Inter font (same as `\'sans-serif\'`)\n *\n * @example\n * text2d(\'Hello World\', { size: 10 }) // default Inter\n * text2d(\'Custom Font\', { size: 10, font: \'/path/to/font.ttf\' })\n */\n font?: string | opentype$1.Font;\n /**\n * Bezier flattening tolerance in model units.\n * Smaller = more polygon segments = smoother curves.\n * @default auto (0.5% of size)\n */\n flattenTolerance?: number;\n}\n/**\n * Build a filled 2D Sketch from a text string.\n *\n * **Details**\n *\n * The Sketch origin is at the left end of the text baseline by default. Use `align`\n * and `baseline` options to adjust placement. Text is rendered using the bundled\n * Inter font by default, or any TTF/OTF/WOFF font you provide.\n *\n * `text2d()` creates real geometry. For non-exported explanatory labels in the\n * viewport, prefer `Viewport.label()` so the text stays off the geometry and OCCT\n * compile paths.\n *\n * Alignment 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 * **Example**\n *\n * ```ts\n * // Extruded nameplate\n * text2d(\'FORGE CAD\', { size: 8 }).extrude(1.2);\n *\n * // Centered label on the XY plane\n * text2d(\'V 2.0\', { size: 6, align: \'center\', baseline: \'center\' });\n *\n * // Engraved text cut into the top face of a box\n * const label = text2d(\'REV A\', { size: 5, align: \'center\', baseline: \'center\' });\n * plate.subtract(label.onFace(plate, \'top\', { protrude: -0.5 }).extrude(1));\n *\n * // Custom TTF font\n * text2d(\'Hello\', { size: 10, font: \'/path/to/Arial.ttf\' }).extrude(1);\n *\n * // Pre-loaded font for reuse\n * const font = loadFont(\'/path/to/Arial Bold.ttf\');\n * text2d(\'Title\', { size: 12, font }).extrude(1.5);\n * ```\n *\n * @param content - The text string to render\n * @param options - Rendering options (size, letterSpacing, align, baseline, font, flattenTolerance)\n * @returns A filled Sketch of the rendered letterforms\n * @see {@link textWidth} to measure text width without creating geometry\n * @see {@link loadFont} to pre-load a font for reuse\n * @see {@link Viewport.label} for viewport-only explanatory text\n * @category Sketch Text\n */\ndeclare function text2d(content: string, options?: TextOptions): Sketch;\n/**\n * Measure the rendered advance width of a string without creating any geometry.\n *\n * **Details**\n *\n * Uses the same font metrics as `text2d()`. Useful for computing layout dimensions\n * before building the actual sketch — e.g. sizing a plate to fit a label.\n *\n * **Example**\n *\n * ```ts\n * const w = textWidth(\'SERIAL: 001\', { size: 6 });\n * const plate = box(w + 10, 12, 2);\n * ```\n *\n * @param content - The text string to measure\n * @param options - Accepts `size`, `letterSpacing`, and `font` (same as `text2d`)\n * @returns Rendered advance width in model units\n * @see {@link text2d} to create the actual geometry\n * @category Sketch Text\n */\ndeclare function textWidth(content: string, options?: Pick<TextOptions, "size" | "letterSpacing" | "font">): number;\ntype Vec3$8 = [\n number,\n number,\n number\n];\ninterface TransitionCurveOptions {\n /**\n * Weight for the start edge. Controls tangent magnitude at the start.\n * - 1.0 (default): balanced transition\n * - > 1.0: curve follows start edge longer before turning\n * - < 1.0: curve turns sooner at the start\n */\n weightA?: number;\n /**\n * Weight for the end edge. Controls tangent magnitude at the end.\n * - 1.0 (default): balanced transition\n * - > 1.0: curve follows end edge longer before turning\n * - < 1.0: curve turns sooner at the end\n */\n weightB?: number;\n /**\n * Number of sample points for the output polyline. Default 64.\n * Higher values give smoother curves at the cost of more geometry.\n */\n samples?: number;\n}\ninterface TransitionSurfaceOptions extends TransitionCurveOptions {\n /**\n * Cross-section profile to sweep along the transition curve.\n * If omitted, a circular profile with `radius` is used.\n */\n profile?: Sketch;\n /**\n * Radius of circular cross-section (used when `profile` is omitted).\n * Default: 5% of chord length.\n */\n radius?: number;\n /**\n * Width and height for rectangular cross-section.\n * Alternative to `radius` when `profile` is omitted.\n */\n rectangleSection?: {\n width: number;\n height: number;\n };\n /**\n * Preferred up vector for the sweep frame. Default: auto-detected.\n */\n up?: Vec3$8;\n /** Edge length for level-set meshing. Smaller = finer. */\n edgeLength?: number;\n /** Extra bounds padding for level-set meshing. */\n boundsPadding?: number;\n}\ninterface TransitionEdge {\n /**\n * Connection point on the edge.\n * Can be any point along the edge where the transition should connect.\n */\n point: Vec3$8;\n /**\n * Tangent direction at the connection point.\n * This is the direction the curve should initially follow when leaving this edge.\n * For a straight edge, this is typically the edge direction pointing "outward"\n * (away from the body of the edge, toward the other edge).\n */\n tangent: Vec3$8;\n /**\n * Surface normal at the connection point (optional).\n * Used as a hint for the sweep frame\'s up vector.\n */\n normal?: Vec3$8;\n}\n/**\n * Create a smooth transition curve between two edges.\n *\n * Returns a `HermiteCurve3D` that starts at `edgeA.point` tangent to\n * `edgeA.tangent` and ends at `edgeB.point` tangent to `edgeB.tangent`.\n *\n * The curve maintains G1 continuity (matching tangent direction) at both\n * endpoints. Weight parameters control the shape of the transition.\n *\n * @example\n * ```js\n * // Connect two edges with a balanced transition\n * const curve = transitionCurve(\n * { point: [0, 0, 0], tangent: [1, 0, 0] },\n * { point: [10, 5, 0], tangent: [1, 0, 0] },\n * );\n *\n * // Weighted: curve hugs edge A longer\n * const weighted = transitionCurve(\n * { point: [0, 0, 0], tangent: [1, 0, 0] },\n * { point: [10, 5, 0], tangent: [1, 0, 0] },\n * { weightA: 2.0, weightB: 0.5 },\n * );\n * ```\n */\ndeclare function transitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D;\n/**\n * Create a solid transition surface between two edges by sweeping a profile\n * along a Hermite transition curve.\n *\n * This produces a watertight solid that smoothly connects the two edges.\n * Works with both Manifold and OCCT backends.\n *\n * @example\n * ```js\n * // Circular tube connecting two edges\n * const 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 * // Custom profile with weights\n * const custom = transitionSurface(\n * { point: [0, 0, 0], tangent: [1, 0, 0] },\n * { point: [10, 5, 3], tangent: [0, 1, 0] },\n * { profile: mySketch, weightA: 1.5, weightB: 0.8 },\n * );\n * ```\n */\ndeclare function transitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape;\ntype EdgeEnd = "start" | "end" | "mid";\ntype TangentMode = "along" | "outward" | "auto";\ninterface ConnectEdgesOptions extends TransitionSurfaceOptions {\n /** Which end of edge A to connect. Default: \'start\'. */\n endA?: EdgeEnd;\n /** Which end of edge B to connect. Default: \'start\'. */\n endB?: EdgeEnd;\n /** Tangent mode for edge A. Default: \'along\'. */\n tangentModeA?: TangentMode;\n /** Tangent mode for edge B. Default: \'along\'. */\n tangentModeB?: TangentMode;\n /** Explicit tangent for edge A. */\n tangentA?: Vec3$8;\n /** Explicit tangent for edge B. */\n tangentB?: Vec3$8;\n /** Flip tangent A. */\n flipA?: boolean;\n /** Flip tangent B. */\n flipB?: boolean;\n}\n/**\n * Create a transition surface or solid bridge between two edge segments.\n *\n * Tangents can be inferred from neighboring geometry or supplied explicitly through `options`.\n * This is useful for loft-like blends where you want a direct connection between two edge spans.\n *\n * @param edgeA - First source edge segment.\n * @param edgeB - Second source edge segment.\n * @param options - Tangent and end-selection options for both edges.\n * @returns The transition shape connecting the two edges.\n */\ndeclare function connectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape;\ninterface ListParamFieldDef {\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n integer?: boolean;\n boolean?: boolean;\n choices?: string[];\n}\n/**\n * Shorthand alias for `Param.number()` — declare a numeric slider parameter.\n *\n * See `Param.number()` for full documentation, examples, and default range rules.\n *\n * @param name - Display label and override key\n * @param defaultValue - Initial value when no override is set\n * @param opts - Range, step, unit, and display options\n * @returns The current value (default or overridden)\n * @see {@link Param} for the full parameter namespace\n * @category Parameters\n * @internal\n */\ndeclare function param(name: string, defaultValue: number, opts?: {\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n integer?: boolean;\n reverse?: boolean;\n}): number;\n/**\n * Shorthand alias for `Param.bool()` — declare a boolean checkbox parameter.\n *\n * See `Param.bool()` for full documentation and examples.\n *\n * @param name - Display label and override key\n * @param defaultValue - Initial checked/unchecked state\n * @returns The current boolean value\n * @see {@link Param} for the full parameter namespace\n * @category Parameters\n * @internal\n */\ndeclare function boolParam(name: string, defaultValue: boolean): boolean;\n/**\n * Shorthand alias for `Param.choice()` — declare a dropdown choice parameter.\n *\n * See `Param.choice()` for full documentation and examples.\n *\n * @param name - Display label and override key\n * @param defaultValue - Must exactly match one of the entries in `choices`\n * @param choices - The list of options shown in the dropdown\n * @returns The currently selected string label\n * @see {@link Param} for the full parameter namespace\n * @category Parameters\n * @internal\n */\ndeclare function choiceParam(name: string, defaultValue: string, choices: string[]): string;\n/**\n * Parameter namespace — the primary way to declare user-facing parameters.\n *\n * **Details**\n *\n * All parameter types live under `Param.*` for discoverability and clean scripts.\n * The standalone functions `param()`, `boolParam()`, and `choiceParam()` are\n * kept as shorthand aliases.\n *\n * **Example**\n *\n * ```ts\n * const width = Param.number("Width", 50, { min: 10, max: 100, unit: "mm" });\n * const label = Param.string("Label", "Hello World");\n * const show = Param.bool("Show Holes", true);\n * const style = Param.choice("Style", "round", ["round", "square"]);\n * ```\n *\n * @concept\n * @category Parameters\n */\ndeclare const Param: {\n /**\n * Declare a numeric parameter that renders as a slider in the UI.\n *\n * **Details**\n *\n * Each call registers a slider control. When the user moves the\n * slider the entire script re-executes with the new value. Parameter values\n * are also overridable from `require()` imports or the CLI `--param` flag —\n * the `name` string is the key used in both cases.\n *\n * Default range rules when options are omitted:\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,\n * `1` for larger ranges\n *\n * The `unit` option is cosmetic only — no conversion is performed.\n * Use `integer: true` for counts, sides, quantities (rounds to whole numbers;\n * step defaults to `1`).\n *\n * **Example**\n *\n * ```ts\n * const width = Param.number("Width", 50);\n * const angle = Param.number("Angle", 45, { min: 0, max: 180, unit: "°" });\n * const 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()\n * const bracket = require("./bracket.forge.js", { Width: 80 });\n *\n * // Via CLI\n * // forgecad run model.forge.js --param "Wall Thickness=3"\n * ```\n *\n * Also available as the shorthand alias `param()`.\n */\n number(name: string, defaultValue: number, opts?: {\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n integer?: boolean;\n reverse?: boolean;\n }): number;\n /**\n * Declare a string parameter that renders as a text input in the UI.\n *\n * **Details**\n *\n * String parameters let users type free-form text — labels, names, inscriptions,\n * file paths, etc. The `name` string is the override key.\n *\n * **Example**\n *\n * ```ts\n * const label = Param.string("Label", "Hello World");\n * const name = Param.string("Name", "Part-001", { maxLength: 20 });\n * ```\n *\n * Override via import:\n * ```ts\n * const tag = require("./tag.forge.js", { Label: "Custom Text" });\n * ```\n *\n * Only available as `Param.string()` — no standalone alias.\n */\n string(name: string, defaultValue: string, opts?: {\n maxLength?: number;\n }): string;\n /**\n * Declare a boolean parameter that renders as a checkbox in the UI.\n *\n * **Details**\n *\n * Internally stored as `0`/`1`. When overriding from CLI or `require()`, pass\n * `1` for true and `0` for false. The `name` string is the override key.\n *\n * **Example**\n *\n * ```ts\n * const showHoles = Param.bool("Show Holes", true);\n * if (showHoles) return difference(plate, cylinder(10, 5).translate(50, 30, 0));\n * return plate;\n * ```\n *\n * Override via import:\n * ```ts\n * const pan = require("./pan.forge.js", { "Show Lid": 0 });\n * ```\n *\n * Also available as the shorthand alias `boolParam()`.\n */\n bool(name: string, defaultValue: boolean): boolean;\n /**\n * Declare a choice parameter that renders as a dropdown in the UI.\n *\n * **Details**\n *\n * `defaultValue` must exactly match one entry in `choices`. Returns the\n * selected string label. Prefer `Param.choice` over `Param.number` when a slider\n * would hide intent — named choices like `"wok"` are self-describing.\n *\n * Overrides may be passed as the choice label string (preferred) or as a\n * numeric index. The `name` string is the override key.\n *\n * **Example**\n *\n * ```ts\n * const panStyle = Param.choice("Pan Style", "frying-pan", ["frying-pan", "saute-pan", "wok"]);\n * if (panStyle === "wok") return buildWok();\n * ```\n *\n * Override via import:\n * ```ts\n * const pan = require("./pan.forge.js", { "Pan Style": "wok" });\n * ```\n *\n * Override via CLI:\n * ```bash\n * forgecad run model.forge.js --param "Pan Style=wok"\n * ```\n *\n * Also available as the shorthand alias `choiceParam()`.\n */\n choice(name: string, defaultValue: string, choices: string[]): string;\n /**\n * Declare a list parameter — an array of struct items with per-field UI controls.\n *\n * **Details**\n *\n * Each item in the list is a struct whose fields each render as their own\n * control (slider, checkbox, or dropdown). The user can add/remove rows up to\n * `minItems`/`maxItems` bounds.\n *\n * Field types:\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 list<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: {\n fields: Partial<Record<keyof T & string, ListParamFieldDef>>;\n minItems?: number;\n maxItems?: number;\n }): T[];\n};\ntype ExactCurveInput = EdgeRef | NurbsCurve3D | Curve3D | HermiteCurve3D | QuinticHermiteCurve3D | Vec3[];\ninterface SurfaceMatchConstraintInput {\n target: EdgeRef;\n continuity?: SurfaceContinuity;\n}\ninterface SurfaceCommonOptions {\n resolution?: number;\n approximate?: boolean;\n}\ninterface SurfacePatchOptions$1 extends SurfaceCommonOptions {\n match?: Partial<Record<"u0" | "u1" | "v0" | "v1", SurfaceMatchConstraintInput>>;\n style?: SurfaceFillStyle;\n}\ninterface SurfaceFillInput extends SurfaceCommonOptions {\n boundaries: Array<{\n name: string;\n curve: ExactCurveInput;\n }>;\n match?: Record<string, SurfaceMatchConstraintInput>;\n style?: SurfaceFillStyle;\n}\ninterface SurfaceBoundaryInput extends SurfaceCommonOptions {\n u: [\n ExactCurveInput,\n ExactCurveInput\n ];\n v: [\n ExactCurveInput,\n ExactCurveInput\n ];\n match?: Partial<Record<"u0" | "u1" | "v0" | "v1", SurfaceMatchConstraintInput>>;\n style?: SurfaceFillStyle;\n}\ninterface SurfaceExtendOptions {\n edge: "u0" | "u1" | "v0" | "v1";\n length: number;\n continuity?: SurfaceContinuity;\n}\ninterface SurfacePlaneOp {\n normal: Vec3;\n offset?: number;\n}\ninterface BlendEdgeOptions {\n shape?: Shape;\n edges: EdgeRef[];\n radius: number;\n continuity?: SurfaceContinuity;\n segments?: number;\n}\ninterface BlendCornerYOptions extends Omit<BlendEdgeOptions, "edges"> {\n edges: EdgeRef[];\n cornerTolerance?: number;\n minBranchAngleDeg?: number;\n}\ninterface BlendSurfaceOptions extends SurfaceFillInput {\n}\ninterface EdgeContinuityThresholds {\n continuity?: SurfaceContinuity;\n samples?: number;\n positionTolerance?: number;\n tangentToleranceDeg?: number;\n curvatureTolerance?: number;\n}\ninterface EdgeContinuityEdgeReport {\n edgeIndex: number;\n continuity: SurfaceContinuity;\n maxPositionalGap: number;\n maxTangentAngleDeg: number;\n maxCurvatureGap: number;\n}\ninterface EdgeContinuityReport {\n requested: SurfaceContinuity;\n passed: boolean;\n sampledEdges: number;\n edges: EdgeContinuityEdgeReport[];\n worst: {\n continuity: SurfaceContinuity;\n maxPositionalGap: number;\n maxTangentAngleDeg: number;\n maxCurvatureGap: number;\n };\n}\ninterface CurvatureSample {\n t: number;\n curvature: number;\n point: Vec3;\n}\ninterface SurfaceHealthReport {\n tinyEdges: Array<{\n index: number;\n length: number;\n }>;\n sliverFaces: Array<{\n index: number;\n area: number;\n sliverScore: number;\n }>;\n valid: boolean;\n}\ndeclare const Surface: {\n Nurbs(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape;\n Ruled(curveA: ExactCurveInput, curveB: ExactCurveInput, options?: SurfaceCommonOptions): Shape;\n Patch(curves: {\n bottom: ExactCurveInput;\n top: ExactCurveInput;\n left: ExactCurveInput;\n right: ExactCurveInput;\n }, options?: SurfacePatchOptions$1): Shape;\n Boundary(input: SurfaceBoundaryInput): Shape;\n Fill(input: SurfaceFillInput): Shape;\n Sew(shapes: Shape[], options?: {\n tolerance?: number;\n }): Shape;\n Extend(shape: Shape, options: SurfaceExtendOptions): Shape;\n Trim(shape: Shape, tool: Shape | SurfacePlaneOp): Shape;\n Split(shape: Shape, tool: Shape | SurfacePlaneOp): [\n Shape,\n Shape\n ];\n Match(shape: Shape, options: {\n edge: "u0" | "u1" | "v0" | "v1";\n target: EdgeRef;\n continuity?: SurfaceContinuity;\n }): Shape;\n MatchEdge(shape: Shape, options: {\n edge: "u0" | "u1" | "v0" | "v1";\n target: EdgeRef;\n continuity?: SurfaceContinuity;\n }): Shape;\n};\ndeclare const Blend: {\n Edge(options: BlendEdgeOptions): Shape;\n Surface(options: BlendSurfaceOptions): Shape;\n /**\n * @alpha\n * Current implementation uses continuity-controlled edge fillets on solid edges.\n * It does not yet provide a dedicated open-sheet or multi-patch Y-corner solver.\n * Follow progress: https://github.com/KoStard/forgecad-private/issues/162\n */\n CornerY(options: BlendCornerYOptions): Shape;\n};\ndeclare const Analysis: {\n EdgeContinuity(shape: Shape, options?: EdgeContinuityThresholds): EdgeContinuityReport;\n SurfaceContinuity(shape: Shape, options?: EdgeContinuityThresholds): EdgeContinuityReport;\n CurvatureComb(input: NurbsCurve3D | EdgeRef, options?: {\n samples?: number;\n }): CurvatureSample[];\n SurfaceHealth(shape: Shape, options?: {\n tinyEdgeThreshold?: number;\n sliverThreshold?: number;\n }): SurfaceHealthReport;\n};\ntype VerificationStatus = "pass" | "fail";\ninterface VerificationResult {\n id: string;\n label: string;\n status: VerificationStatus;\n message: string;\n /** 1-based source line number, if captured from the call stack */\n line?: number;\n /** Human-readable expected value for display */\n expected?: string;\n /** Human-readable actual value for display */\n actual?: string;\n /** Spec name — when set, the UI groups this result under a collapsible section */\n group?: string;\n}\ninterface SpecResult {\n /** Spec name */\n name: string;\n /** Number of checks that passed */\n passed: number;\n /** Total number of checks */\n total: number;\n /** The individual verification results added by this spec */\n results: VerificationResult[];\n}\n/**\n * A named, reusable bundle of verification checks.\n *\n * Create one with `spec(name, checkFn)`, then call `.check(...shapes)` to\n * run the checks. Results appear grouped under the spec name in the Checks\n * panel. Can be reused across multiple shapes and imported via `require()`.\n *\n * @category Parameters\n */\ninterface Spec {\n /** The display name of this spec */\n readonly name: string;\n /**\n * Run this spec\'s checks against one or more shapes.\n * All `verify.*` calls inside the check function are automatically\n * grouped under this spec\'s name in the Checks panel.\n */\n check(...args: unknown[]): SpecResult;\n}\n/**\n * Create a named, reusable bundle of verification checks.\n *\n * **Details**\n *\n * A spec groups related `verify.*` calls under a collapsible header in the\n * Checks panel. This makes large check suites scannable. Specs can be applied\n * to multiple shapes and can check relationships between parts.\n *\n * Specs can be defined in separate `.forge.js` files and imported via\n * `require()` to share them across models.\n *\n * `spec.check()` returns a `SpecResult` — you can inspect it programmatically\n * or ignore the return value and let the Checks panel show results.\n *\n * **Example**\n *\n * ```ts\n * const 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\n * printable.check(bracket);\n * printable.check(standoff);\n *\n * // Check relationships between parts\n * const fitSpec = spec("Assembly fit", (partA, partB) => {\n * verify.notColliding("No interference", partA, partB, 10);\n * });\n * fitSpec.check(bracket, standoff);\n * ```\n *\n * **Spec-first workflow:** Write specs before building geometry. Checks go\n * from red to green as you build — effectively TDD for CAD.\n *\n * @param name - Display name in the Checks panel (e.g. `"Fits printer bed"`)\n * @param checkFn - Function that calls `verify.*` methods; receives args from `.check()`\n * @returns A `Spec` object with a `.check(...args)` method\n * @category Parameters\n */\ndeclare function spec(name: string, checkFn: (...args: any[]) => void): Spec;\ninterface ShapeLike {\n boundingBox(): {\n min: number[];\n max: number[];\n };\n isEmpty(): boolean;\n volume(): number;\n surfaceArea(): number;\n}\ninterface FaceRefLike {\n normal: [\n number,\n number,\n number\n ];\n center: [\n number,\n number,\n number\n ];\n}\ndeclare const verify: {\n /**\n * Custom predicate check.\n *\n * @param label Short name shown in the panel ("gear clearance")\n * @param check Function that returns true (pass) or false (fail)\n * @param message Optional extra context shown on failure\n */\n that(label: string, check: () => boolean, message?: string): void;\n /**\n * Check that two numbers are approximately equal (within tolerance).\n */\n equal(label: string, actual: number, expected: number, tolerance?: number, message?: string): void;\n /**\n * Check that two numbers are NOT equal (differ by more than tolerance).\n */\n notEqual(label: string, actual: number, unexpected: number, tolerance?: number, message?: string): void;\n /** Check that actual > min. */\n greaterThan(label: string, actual: number, min: number, message?: string): void;\n /** Check that actual < max. */\n lessThan(label: string, actual: number, max: number, message?: string): void;\n /** Check that min <= actual <= max. */\n inRange(label: string, actual: number, min: number, max: number, message?: string): void;\n /**\n * Check that the bounding-box centers of two shapes coincide within tolerance (mm).\n */\n centersCoincide(label: string, a: ShapeLike, b: ShapeLike, tolerance?: number): void;\n /**\n * Check that two shapes do not collide (minGap > 0).\n *\n * @param searchLength Search radius for minGap (mm, default 1.0)\n */\n notColliding(label: string, a: ShapeLike, b: ShapeLike, searchLength?: number): void;\n /**\n * Check that a minimum clearance gap exists between two shapes.\n */\n minClearance(label: string, a: ShapeLike, b: ShapeLike, minGap: number, searchLength?: number): void;\n /**\n * Check that two face normals are parallel (within toleranceDeg degrees).\n */\n parallel(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void;\n /**\n * Check that two face normals are perpendicular (within toleranceDeg degrees).\n */\n perpendicular(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void;\n /**\n * Check that a face is coplanar with (same plane as) another face,\n * meaning they are parallel AND their centers lie on the same plane.\n */\n coplanar(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number, toleranceMm?: number): void;\n /**\n * Check that a face center lies at a specific position (within toleranceMm).\n */\n faceAt(label: string, face: FaceRefLike, expectedPos: [\n number,\n number,\n number\n ], toleranceMm?: number): void;\n /**\n * Check that two face normals point in the same direction (not antiparallel).\n * Stricter than parallel — both |angle| AND sign must match.\n */\n sameDirection(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void;\n /**\n * Check that a shape is empty.\n */\n isEmpty(label: string, shape: ShapeLike, message?: string): void;\n /**\n * Check that a shape is NOT empty.\n */\n notEmpty(label: string, shape: ShapeLike, message?: string): void;\n /**\n * Check that a shape\'s volume is approximately equal to expected (mm³).\n *\n * @param expected Expected volume in mm³\n * @param tolerance Absolute tolerance in mm³ (default 1.0)\n */\n volumeApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void;\n /**\n * Check that a shape\'s surface area is approximately equal to expected (mm²).\n *\n * @param expected Expected surface area in mm²\n * @param tolerance Absolute tolerance in mm² (default 1.0)\n */\n areaApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void;\n /**\n * Check that a shape\'s bounding box has approximately the given size.\n *\n * @param expectedSize [sizeX, sizeY, sizeZ] in mm\n * @param tolerance Per-axis tolerance in mm (default 0.1)\n */\n boundingBoxSize(label: string, shape: ShapeLike, expectedSize: [\n number,\n number,\n number\n ], tolerance?: number): void;\n /**\n * Check that every sampled seam on a shape meets a requested continuity threshold.\n */\n edgeContinuity(label: string, shape: ShapeLike, options?: EdgeContinuityThresholds): void;\n /**\n * Check that a shape has no tiny edges below the requested threshold.\n */\n noTinyEdges(label: string, shape: ShapeLike, threshold?: number): void;\n /**\n * Check that a shape has no sliver faces below the requested score threshold.\n */\n noSliverFaces(label: string, shape: ShapeLike, threshold?: number): void;\n /**\n * Best-effort exact-shape validity guard for self-intersections or broken B-Rep topology.\n */\n noSelfIntersection(label: string, shape: ShapeLike): void;\n};\n/**\n * Register a mock (context) object for visualization and collision checking.\n *\n * **Details**\n *\n * Mock objects appear in the viewport and spatial analysis when you run a\n * file directly, but are excluded when the file is imported via `require()`.\n * This lets you model the surrounding context — walls, bolts, mating parts —\n * without polluting the module\'s exports.\n *\n * The shape is returned unchanged, so you can reference it for alignment,\n * dimensioning, and `verify` checks.\n *\n * Mock objects participate in `forgecad run` collision detection and spatial\n * analysis. Their names appear with a `(mock)` suffix in reports.\n *\n * In the viewport, mock objects render at reduced opacity so they are\n * visually distinct from real geometry.\n *\n * **Example**\n *\n * ```ts\n * // bracket.forge.js\n * const wall = mock(box(100, 200, 10).translate(0, 0, -5), "wall");\n * const bolt = mock(cylinder(3, 15).translate(10, 15, 0), "bolt");\n *\n * const bracket = box(20, 30, 5);\n * verify.notColliding("bracket vs wall", bracket, wall);\n *\n * return bracket;\n * // When imported: only bracket is exported\n * // When run directly: bracket + wall + bolt all visible\n * ```\n *\n * @param shape - The context shape to register as a mock\n * @param name - Optional display name (defaults to "Mock N")\n * @returns The same shape, unchanged — use it for alignment and verify checks\n * @category Context\n */\ndeclare function mock<T extends Shape>(shape: T, name?: string): T;\ntype SculptBlendInput = SdfShape | readonly SdfShape[];\ninterface SculptBlendOptions {\n /** Smooth blend radius in model units. Default: 4. */\n radius?: number;\n}\ninterface SculptBoxOptions {\n /** Rounded-box radius. Omit for a sharp SDF box. */\n radius?: number;\n}\ninterface SculptTubeOptions {\n /** Tube radius in model units. Used as the default when points do not carry their own radius. Default: 3. */\n radius?: number;\n /** Smooth blend radius between tube segments and joints. Default: smallest point radius. */\n blend?: number;\n /** Smooth the control polyline into a Catmull-Rom curve before sweeping. Default: false for tube/path, true for curve. */\n curve?: boolean | "linear" | "catmull-rom";\n /** Curve samples per control-point interval when curve smoothing is enabled. Default: 8, capped at 8 for realtime preview. */\n segments?: number;\n /** Catmull-Rom tangent scale when curve smoothing is enabled. Default: 0.5. */\n tension?: number;\n /** Material/color preset applied to the final tube. */\n polish?: SculptPolishInput;\n}\ntype SculptVec3 = Vec3$1 | readonly [\n number,\n number,\n number\n];\ntype SculptPathPoint = SculptVec3 | readonly [\n number,\n number,\n number,\n number\n] | {\n point: SculptVec3;\n radius?: number;\n} | {\n position: SculptVec3;\n radius?: number;\n} | {\n at: SculptVec3;\n radius?: number;\n};\ntype SculptPointList = readonly SculptPathPoint[];\ntype SculptLookPreset = "gallery" | "soft-studio" | "candy-shop" | "midnight" | "workbench";\ntype SculptBlendArg = SculptBlendInput | SculptBlendOptions;\ndeclare function sculptSphere(radius: number): SdfShape;\ndeclare function sculptBox(x: number, y: number, z: number, options?: SculptBoxOptions): SdfShape;\ndeclare function sculptCylinder(height: number, radius: number): SdfShape;\ndeclare function sculptDisk(radius: number, thickness?: number): SdfShape;\ndeclare function sculptCapsule(height: number, radius: number): SdfShape;\ndeclare function sculptTorus(majorRadius: number, minorRadius: number): SdfShape;\ndeclare function sculptCone(height: number, radius: number): SdfShape;\ndeclare function sculptTubePublic(points: SculptPointList, options?: SculptTubeOptions): SdfShape;\ndeclare function sculptCurve(points: SculptPointList, options?: SculptTubeOptions): SdfShape;\ndeclare function sculptBlend(first?: SculptBlendArg, optionsOrShape?: SculptBlendArg, ...rest: SculptBlendArg[]): SdfShape;\ndeclare function sculptUnion(first?: SculptBlendInput, ...rest: SculptBlendInput[]): SdfShape;\ndeclare function sculptCarve(base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions): SdfShape;\ndeclare function sculptKeep(first?: SculptBlendArg, optionsOrShape?: SculptBlendArg, ...rest: SculptBlendArg[]): SdfShape;\ndeclare function sculptPolish(shape: SdfShape, input?: SculptPolishInput): SdfShape;\ndeclare function sculptMaterial(input?: SculptPolishInput): ShapeMaterialProps & {\n color?: string;\n};\ndeclare function sculptLook(preset?: SculptLookPreset): SceneOptions;\ndeclare const Sculpt: {\n /** Create a liquid SDF sphere centered at the origin. */\n sphere: typeof sculptSphere;\n /** Create a liquid SDF box; pass `{ radius }` for a rounded box. */\n box: typeof sculptBox;\n /** Create a liquid SDF cylinder centered at the origin, axis along Z. */\n cylinder: typeof sculptCylinder;\n /** Create a thin circular disk centered at the origin, axis along Z. Useful as a circular cutter or insert. */\n disk: typeof sculptDisk;\n /** Alias for `Sculpt.disk()`. */\n circle: typeof sculptDisk;\n /** Create a liquid SDF capsule centered at the origin, axis along Z. */\n capsule: typeof sculptCapsule;\n /** Create a liquid SDF torus lying in the XY plane. */\n torus: typeof sculptTorus;\n /** Create a liquid SDF cone. */\n cone: typeof sculptCone;\n /** Create a smooth tube through a list of 3D points. */\n tube: typeof sculptTubePublic;\n /** Create a smooth variable-thickness sweep through 3D control points. */\n curve: typeof sculptCurve;\n /** Alias for `Sculpt.tube()`; points may use [x, y, z, radius] for variable thickness. */\n path: typeof sculptTubePublic;\n /** Smoothly blend one or more SDF shapes into a continuous body. */\n blend: typeof sculptBlend;\n /** Sharply union one or more SDF shapes. */\n union: typeof sculptUnion;\n /** Smoothly subtract one or more cutter shapes from a base shape. */\n carve: typeof sculptCarve;\n /** Smoothly intersect one or more SDF shapes. */\n keep: typeof sculptKeep;\n /** Apply a Sculpt material preset or direct material properties. */\n polish: typeof sculptPolish;\n /** Resolve a Sculpt material preset to ForgeCAD material properties. */\n material: typeof sculptMaterial;\n /** Return a polished scene preset tuned for liquid SDF preview. */\n look: typeof sculptLook;\n /** List the built-in Sculpt material preset names. */\n knownMaterials: typeof knownSculptMaterialPresets;\n};\n/**\n * SDF modeling — signed distance field primitives, smooth booleans, TPMS lattices,\n * domain warps, and surface patterns.\n *\n * Return `SdfShape` values directly from a ForgeCAD script for native raymarch\n * preview. Plain objects and arrays of SDF leaves are renderable too, so object\n * keys become named preview parts.\n *\n * Call `.toShape()` or `toShape(...)` only when you need a mesh-backed ForgeCAD\n * Shape for export, mesh booleans, or mixed SDF/manifold projects. All shapes\n * live as a lazy expression tree until that materialization boundary.\n *\n * SDF is inherently implicit and sampled, not B-rep/exact geometry. Use it with\n * caution when precision, tolerances, or exact export matter.\n *\n * @example\n * ```js\n * return sdf.smoothUnion(sdf.sphere(10), sdf.box(15, 15, 15), { radius: 3 })\n * .color(\'#4488cc\');\n * ```\n *\n * @example\n * ```js\n * return {\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 */\ndeclare const sdf: {\n /** Create an SDF sphere centered at the origin. */\n sphere: typeof sphere;\n /** Create an SDF box centered at the origin with given full dimensions (not half-extents). */\n box: typeof box;\n /** Create an SDF cylinder centered at the origin, axis along Z. */\n cylinder: typeof cylinder;\n /** Create an SDF torus centered at the origin, lying in the XY plane. */\n torus: typeof torus;\n /** Create an SDF capsule centered at the origin, axis along Z. */\n capsule: typeof capsule;\n /** Create an SDF cone with base at z=0 and tip at z=height. */\n cone: typeof cone;\n /** Smooth union — blends shapes together with a smooth transition radius. */\n smoothUnion: typeof smoothUnion;\n /** Smooth difference — smoothly subtracts b from a. */\n smoothDifference: typeof smoothDifference;\n /** Smooth intersection — smoothly intersects a and b. */\n smoothIntersection: typeof smoothIntersection;\n /** Morph between two SDF shapes. t=0 → a, t=1 → b. */\n morph: typeof morph;\n /**\n * Spatially blend between two SDF patterns.\n * The blend function receives (x, y, z) and returns 0..1:\n * 0 = fully pattern `a`, 1 = fully pattern `b`.\n */\n blend: typeof blend;\n /** Gyroid TPMS lattice — the most common lattice for additive manufacturing. */\n gyroid: typeof gyroid;\n /** Schwarz-P TPMS lattice — isotropic pore structure. */\n schwarzP: typeof schwarzP;\n /** Diamond TPMS lattice — stiffest TPMS structure. */\n diamond: typeof diamond;\n /** Lidinoid TPMS lattice — visually distinct from gyroid, popular in research and art. */\n lidinoid: typeof lidinoid;\n /** TPMS block preset clipped to an explicit design space. */\n tpmsBlock: typeof tpmsBlock;\n /** Clip an SDF shape to a box-shaped design space. */\n withinBox: typeof withinBox;\n /** 3D Simplex noise field — produces organic, natural-looking displacements. */\n noise: typeof noise;\n /** 3D Voronoi pattern — organic cellular structures like bone, coral, or soap bubbles. */\n voronoi: typeof voronoi;\n /** Honeycomb (hexagonal) lattice pattern. Intersect with your shape to apply. */\n honeycomb: typeof honeycomb;\n /** Sinusoidal wave ridges — parallel ridges along an axis. */\n waves: typeof waves;\n /** Knurl pattern — crossed helical grooves for grips and handles. */\n knurl: typeof knurl;\n /** Perforated plate pattern — regular array of cylindrical holes. */\n perforated: typeof perforated;\n /** Fish/dragon scale pattern — overlapping circular scales in hex-packed rows. */\n scales: typeof scales;\n /** Brick/stone wall pattern — running bond with mortar grooves. */\n brick: typeof brick;\n /** Grid lattice pattern — two families of infinite slabs crossing at 90°. */\n weave: typeof weave;\n /** Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`. */\n basketWeave: typeof basketWeave;\n /** Twist an SDF shape around the Z axis. */\n twist: typeof twist;\n /** Bend an SDF shape around the Z axis. */\n bend: typeof bend;\n /** Repeat an SDF shape in space. */\n repeat: typeof repeat;\n /** A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`. */\n SurfacePattern: typeof SurfacePattern;\n /** Create a custom SDF from one expression; shader-safe expressions raymarch directly. */\n fromFunction: typeof fromFunction;\n /** Sculpt-like facade: friendly liquid-modeling verbs backed by the same SDF kernel. */\n Sculpt: {\n sphere: (radius: number) => SdfShape;\n box: (x: number, y: number, z: number, options?: SculptBoxOptions) => SdfShape;\n cylinder: (height: number, radius: number) => SdfShape;\n disk: (radius: number, thickness?: number) => SdfShape;\n circle: (radius: number, thickness?: number) => SdfShape;\n capsule: (height: number, radius: number) => SdfShape;\n torus: (majorRadius: number, minorRadius: number) => SdfShape;\n cone: (height: number, radius: number) => SdfShape;\n tube: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape;\n curve: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape;\n path: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape;\n blend: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape;\n union: (first?: SculptBlendInput, ...rest: SculptBlendInput[]) => SdfShape;\n carve: (base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions) => SdfShape;\n keep: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape;\n polish: (shape: SdfShape, input?: SculptPolishInput) => SdfShape;\n material: (input?: SculptPolishInput) => ShapeMaterialProps & {\n color?: string;\n };\n look: (preset?: SculptLookPreset) => SceneOptions;\n knownMaterials: typeof knownSculptMaterialPresets;\n };\n};\ndeclare function initKernel(): Promise<unknown>;\ntype ActiveBackend = "occt" | "manifold";\ndeclare function setActiveBackend(backend: ActiveBackend): void;\n/**\n * Set the active backend and ensure its WASM module is initialized.\n * Call this instead of `setActiveBackend` when you\'re about to execute code —\n * it guarantees the backend is ready, not just selected.\n */\ndeclare function activateBackend(backend: ActiveBackend): Promise<void>;\ndeclare function getActiveBackend(): ActiveBackend;\ntype GeometryBackend = "manifold" | "occt" | "hybrid" | "unknown";\ntype GeometryRepresentation = "mesh-solid" | "brep-solid" | "surface" | "mixed";\ntype GeometryFidelity = "kernel-native" | "exact" | "sampled" | "deformed" | "mixed" | "unknown";\ntype GeometryTopology = "none" | "synthetic" | "kernel";\ntype GeometrySource = "primitive" | "extrude" | "revolve" | "boolean" | "sheet-metal" | "shell" | "fillet" | "chamfer" | "level-set" | "loft" | "sweep" | "surface-patch" | "deform" | "draft" | "offset-solid" | "imported" | "sdf" | "fromSlices" | "unknown";\ninterface GeometryInfo {\n backend: GeometryBackend;\n representation: GeometryRepresentation;\n fidelity: GeometryFidelity;\n topology: GeometryTopology;\n sources: GeometrySource[];\n}\ninterface ShapeLike$1 {\n toShape(): Shape;\n}\ntype ShapeOperandInput = Shape | ShapeLike$1 | readonly (Shape | ShapeLike$1)[];\ntype ShapeRuntimePayload = ShapeBackend | Manifold;\ntype ShapeAnchorTarget = Shape | {\n referencePoint(ref: string): [\n number,\n number,\n number\n ];\n} | {\n _bbox(): {\n min: number[];\n max: number[];\n };\n};\ntype RotationPointLike = PlacementAnchorLike | Vec3;\ninterface ShapeMaterialProps {\n /** Metalness factor (0 = dielectric, 1 = metal). Default: 0.05 */\n metalness?: number;\n /** Roughness factor (0 = mirror, 1 = fully diffuse). Default: 0.35 */\n roughness?: number;\n /** Emissive glow color (hex string, e.g. "#ff6b35"). */\n emissive?: string;\n /** Emissive intensity multiplier. Default: 1 */\n emissiveIntensity?: number;\n /** Opacity (0 = fully transparent, 1 = fully opaque). Default: 1 */\n opacity?: number;\n /** Render as wireframe. Default: false */\n wireframe?: boolean;\n /** Clearcoat intensity (0–1). Default: 0.1 */\n clearcoat?: number;\n /** Clearcoat roughness (0–1). Default: 0.4 */\n clearcoatRoughness?: number;\n /** Glass/translucency transmission factor (0–1). Renderer support depends on target. */\n transmission?: number;\n /** Index of refraction for transmissive materials. Typical glass is ~1.45. */\n ior?: number;\n /** Approximate transmissive volume thickness in model units. */\n thickness?: number;\n /** Specular highlight intensity (0–1). */\n specularIntensity?: number;\n /** Specular highlight tint. */\n specularColor?: string;\n /** Reflection strength for supported renderers (0–1). */\n reflectivity?: number;\n}\ntype ShapeReferenceKind = "face" | "face-set" | "edge" | "edge-set" | "point" | "point-set";\ntype ShapeReferenceStatus = "alive" | "deleted" | "ambiguous";\ntype ShapeReferenceCardinality = "zero" | "one" | "many";\ninterface ShapeReferenceResolution {\n path: string;\n sourcePath: string;\n shapeName?: string;\n kind: ShapeReferenceKind;\n status: ShapeReferenceStatus;\n cardinality: ShapeReferenceCardinality;\n rule: string;\n faces: FaceRef[];\n edges: EdgeSegment[];\n points: Vec3[];\n}\n/**\n * A first-class reference path over a shape\'s semantic faces and face relationships.\n *\n * Created with `shape.ref("lid/back")`, then refined through methods such as\n * `.point()` or `.edges()`. The reference stores intent as a readable path and\n * resolves lazily against the current shape metadata.\n */\ndeclare class ShapeRef {\n private readonly shape;\n readonly path: string;\n private readonly optional;\n constructor(shape: Shape, path: string, optional?: boolean);\n /** Resolve this reference into its current faces, edges, or points. */\n resolve(): ShapeReferenceResolution;\n /** The resolved reference kind, such as `face`, `edge-set`, or `point`. */\n get kind(): ShapeReferenceKind;\n /** Whether the reference currently resolves to zero, one, or many matches. */\n get cardinality(): ShapeReferenceCardinality;\n /** Return the reference lifecycle status for the current shape state. */\n status(): ShapeReferenceStatus;\n /** Return a human-readable explanation of how this reference resolved. */\n explain(): string;\n /** Name this derived reference so the same shape can resolve it by `shape.ref(name)`. */\n as(name: string): ShapeRef;\n /** Return an optional reference that resolves to zero matches instead of throwing when missing. */\n maybe(): ShapeRef;\n /** Mark that a multi-match reference is intentionally being used as a set. */\n all(): ShapeRef;\n /** Require this reference to resolve to exactly one match. */\n one(): ShapeRef;\n /** Resolve this reference as one or more faces. */\n faces(): FaceRef[];\n /** Resolve this reference as exactly one face. */\n face(): FaceRef;\n /** Resolve this reference as one or more edges. Face references return boundary edges. */\n edges(): EdgeSegment[];\n /** Resolve this reference as exactly one edge. */\n edge(): EdgeSegment;\n /** Resolve this reference as one or more points. Faces use centers and edges use midpoints. */\n points(): Vec3[];\n /** Resolve this reference as exactly one point. */\n point(): Vec3;\n /** Return the structured JSON-friendly reference resolution. */\n toJSON(): ShapeReferenceResolution;\n /** Return a compact display form for this reference path. */\n toString(): string;\n}\n/**\n * Core 3D solid shape. All operations are immutable and return new shapes.\n *\n * Supports transforms (translate, rotate, scale, mirror, transform, rotateAround, pointAlong),\n * booleans (add, subtract, intersect), cutting (split, splitByPlane, trimByPlane),\n * shelling, anchor positioning (attachTo, onFace), placement references, and queries\n * (volume, surfaceArea, boundingBox, isEmpty, numTri, geometryInfo).\n */\ndeclare class Shape {\n colorHex: string | undefined;\n materialProps: ShapeMaterialProps | undefined;\n constructor(payload: ShapeRuntimePayload, color?: string, geometryInfo?: Partial<GeometryInfo>);\n /** @internal Use .color() instead. */\n setColor(value: string | undefined): Shape;\n /** Set the color of this shape (hex string, e.g. "#ff0000"). Returns a new Shape with the color applied. */\n color(value: string | undefined): Shape;\n /**\n * Set PBR material properties for this shape\'s visual appearance.\n *\n * **Details**\n *\n * Returns a new Shape with the specified material properties merged on top of any previously\n * set properties. All properties are optional — omitted keys retain their current value.\n * Material properties survive transforms and boolean operations.\n *\n * Use `.color()` to set the base diffuse color; `.material()` controls how that color behaves\n * under light (metalness, roughness, clearcoat) and can add emissive glow independent of\n * lighting. Emissive glow pairs naturally with the `postProcessing.bloom` effect in `scene()`.\n *\n * **Example**\n *\n * ```js\n * box(50, 50, 50).material({ metalness: 0.9, roughness: 0.1 }); // polished metal\n * sphere(30).material({ emissive: \'#ff6b35\', emissiveIntensity: 2 }); // glowing\n * cylinder(40, 20).material({ opacity: 0.4, clearcoat: 1.0, clearcoatRoughness: 0.02 }); // ice\n *\n * // Chainable with other shape methods\n * box(100, 100, 10).color(\'#gold\').material({ metalness: 0.95, roughness: 0.05 }).translate(0, 0, 50);\n * ```\n *\n * @param props.metalness - Metallic vs. dielectric (0–1). Default: 0.05\n * @param props.roughness - Surface roughness; 0 = mirror, 1 = matte. Default: 0.35\n * @param props.emissive - Glow color hex string (e.g. `\'#ff6b35\'`). Glows independently of lighting\n * @param props.emissiveIntensity - Glow multiplier. Default: 1\n * @param props.opacity - Transparency; 0 = fully transparent, 1 = fully opaque. Default: 1\n * @param props.wireframe - Render as wireframe. Default: false\n * @param props.clearcoat - Clearcoat layer intensity (0–1). Default: 0.1\n * @param props.clearcoatRoughness - Clearcoat layer roughness (0–1). Default: 0.4\n * @returns A new Shape with the merged material properties applied\n * @category Materials\n */\n material(props: ShapeMaterialProps): Shape;\n /** Return a new Shape wrapper for explicit duplication in scripts. */\n clone(): Shape;\n /** Alias for clone() */\n duplicate(): Shape;\n /** Inspect which backend/representation produced this solid. */\n geometryInfo(): GeometryInfo;\n /** Name this shape as a reference namespace for diagnostics and future published refs. */\n as(name: string): Shape;\n /** Resolve a semantic reference path like `lid`, `lid/back`, or a midpoint selector on `lid/back`. */\n ref(path: string): ShapeRef;\n /** Attach named placement references that survive normal transforms and imports. */\n withReferences(refs: PlacementReferenceInput): Shape;\n /** List named placement references carried by this shape. */\n referenceNames(kind?: PlacementReferenceKind): string[];\n /**\n * Deprecated alias for `withConnectors()`.\n * @deprecated Use `withConnectors()` instead.\n */\n withPorts(ports: Record<string, PortInput>): Shape;\n /**\n * Deprecated alias for `connectorNames()`.\n * @deprecated Use `connectorNames()` instead.\n */\n portNames(): string[];\n /** Resolve a named placement reference or built-in anchor to a 3D point. */\n referencePoint(ref: PlacementAnchorLike): [\n number,\n number,\n number\n ];\n /**\n * Resolve a face by user-authored label or compiler-owned name. Returns a `FaceRef`\n * that can be passed to `.onFace()`, `projectToPlane()`, or used directly in placement.\n *\n * **Details**\n *\n * `.face(name)` is a pure label lookup — it finds faces by user-authored labels, not by\n * geometric queries. Labels are born in sketches via `.label()` / `.labelEdges()` and\n * grow into face names through extrude, loft, revolve, and sweep. They are stable\n * references that travel with the geometry.\n *\n * Labels must be unique within a shape. Use `.prefixLabels()` before combining shapes\n * with `union()` / `difference()` to avoid collisions. Collision detection throws a\n * clear error with a fix suggestion.\n *\n * For compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape\'s\n * compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops)\n * are resolved via coplanar triangle clustering.\n *\n * **Example**\n *\n * ```ts\n * // Edge labels become side face names after extrude\n * const 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\');\n * const room = profile.extrude(30, { labels: { start: \'base\', end: \'top\' } });\n * room.face(\'floor\'); // side face from the labeled edge\n * room.face(\'base\'); // base cap (user-specified)\n *\n * // .labelEdges() shorthand for sequential edge labeling\n * const plate = rect(100, 50).labelEdges(\'south\', \'east\', \'north\', \'west\');\n * const solid = plate.extrude(20, { labels: { start: \'bottom\', end: \'top\' } });\n * solid.face(\'south\'); // side face\n *\n * // Prefix before combining to avoid collisions\n * const left = wing.prefixLabels(\'l/\');\n * const right = wing.mirror([1, 0, 0]).prefixLabels(\'r/\');\n * const full = union(left, right);\n * full.face(\'l/upper\'); // left wing upper surface\n * ```\n *\n * @param selector - A user-authored label string, compiler-owned face name, or FaceQuery.\n * @returns A `FaceRef` carrying the face\'s center, normal, and local axes.\n * @see {@link Shape.faceNames} — list available face names on this shape\n * @see {@link Shape.prefixLabels} — prefix labels before combining shapes\n * @category Face References\n */\n face(selector: FaceSelector): FaceRef;\n /**\n * Return faces matching a query, or label semantic faces when passed a mapping.\n *\n * Mapping form returns a new shape:\n * `shape.faces({ lid: \'top\', walls: [\'front\', \'back\', \'left\', \'right\'] })`.\n */\n faces(): FaceRef[];\n faces(query: FaceQuery): FaceRef[];\n faces(mapping: Record<string, string | string[]>): Shape;\n /** List defined semantic face names currently available on this shape. */\n faceNames(): string[];\n /** Prefix all user-authored face labels, including semantic labels from `faces(mapping)`. Returns a new shape with modified labels. */\n prefixLabels(prefix: string): Shape;\n /** Rename a single face label. Returns a new shape. */\n renameLabel(from: string, to: string): Shape;\n /** Remove specific face labels. Returns a new shape. */\n dropLabels(...names: string[]): Shape;\n /** Remove all face labels. Returns a new shape. */\n dropAllLabels(): Shape;\n /** Get a named topology edge. Only available on shapes with tracked topology (from box/cylinder/extrude). */\n edge(name: string): EdgeRef;\n /** List named topology edge names. Returns empty array if shape has no tracked topology. */\n edgeNames(): string[];\n /**\n * Return all boundary edges of a named face.\n *\n * **Details**\n *\n * Finds edges where one adjacent mesh face belongs to the target face and the\n * other belongs to a different face. The result is coalesced (tessellation\n * fragments merged) and can be passed directly to `fillet()` or `chamfer()`.\n *\n * This is a topological query — no coordinates, no tolerances, no minimum-length\n * hacks. It works because an edge is the boundary between two faces.\n *\n * **Example**\n *\n * ```js\n * // Fillet all top edges of a mounting plate\n * let plate = box(120, 80, 6).faces({ workSurface: \'top\' })\n * plate = fillet(plate, 3, plate.edgesOf(\'workSurface\'))\n *\n * // Shelled enclosure — fillet the outer lip\n * let body = box(80, 50, 35).faces({ opening: \'top\' })\n * body = body.shell(2, { openFaces: [\'top\'] })\n * body = fillet(body, 1.5, body.edgesOf(\'opening\'))\n *\n * // Filter: only concave edges (after a boolean subtraction)\n * body.edgesOf(\'top\', { concave: true })\n * ```\n *\n * @param faceLabel - Name of the face whose edges to return.\n * @param options - Optional: exclude faces, convexity filter, minLength.\n * @returns EdgeSegment[] ready for fillet/chamfer. Throws if face not found.\n * @see {@link Shape.edgesBetween} — edges shared between two faces\n * @see {@link Shape.faces} — assign labels to faces\n * @see {@link Shape.ref} — resolve semantic face and edge paths\n * @category Edge Queries\n */\n edgesOf(faceLabel: string, options?: EdgesOfOptions): EdgeSegment[];\n /**\n * Return edges shared between two named faces.\n *\n * **Details**\n *\n * An edge is "between" faces A and B when one of its adjacent mesh triangles\n * belongs to A and the other belongs to B. This is the most precise topological\n * edge selection — "fillet the edges where the top meets the wall."\n *\n * The second argument can be a single face name or an array (edges between A\n * and any of B1, B2, ...).\n *\n * **Example**\n *\n * ```js\n * // Fillet the edge where lid meets one wall\n * let body = box(100, 60, 30).faces({ lid: \'top\', wall: \'side-left\' })\n * body = fillet(body, 2, body.edgesBetween(\'lid\', \'wall\'))\n *\n * // Fillet a cylinder rim — where the flat cap meets the curved barrel\n * let tube = cylinder(30, 10).faces({ cap: \'top\', barrel: \'side\' })\n * tube = fillet(tube, 1, tube.edgesBetween(\'cap\', \'barrel\'))\n *\n * // Multiple target faces at once\n * body.edgesBetween(\'lid\', [\'left-wall\', \'right-wall\', \'front-wall\', \'back-wall\'])\n * ```\n *\n * @param faceA - First face name.\n * @param faceB - Second face name, or array of face names.\n * @returns EdgeSegment[] ready for fillet/chamfer. Throws if faces don\'t share edges.\n * @see {@link Shape.edgesOf} — all edges of a single face\n * @see {@link Shape.faces} — assign labels to faces\n * @see {@link Shape.ref} — resolve semantic face and edge paths\n * @category Edge Queries\n */\n edgesBetween(faceA: string, faceB: string | string[]): EdgeSegment[];\n /** @internal Resolve a user face label or canonical name to a FaceRef. */\n private _resolveFaceLabel;\n /** @internal Resolve a user face label, face set, or canonical name to one or more FaceRefs. */\n private _resolveFaceLabels;\n /** Get the transformation history for a specific face. */\n faceHistory(name: string): FaceTransformationHistory;\n /**\n * Translate the shape so the given anchor or reference lands on the target coordinate.\n *\n * Accepts any built-in anchor name (`\'bottom\'`, `\'center\'`, `\'top-front-left\'`, etc.)\n * or a custom placement reference attached via `withReferences()`.\n *\n * ```javascript\n * // Ground a shape — put its bottom face center at Z = 0\n * shape.placeReference(\'bottom\', [0, 0, 0])\n *\n * // Center at the world origin\n * shape.placeReference(\'center\', [0, 0, 0])\n *\n * // Align left edge to X = 10\n * shape.placeReference(\'left\', [10, 0, 0])\n * ```\n */\n placeReference(ref: PlacementAnchorLike, target: [\n number,\n number,\n number\n ], offset?: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Translate using polar coordinates (radius + angle in degrees).\n * Eliminates manual `r * Math.cos(angle * PI/180)` calculations.\n *\n * Example: `shape.translatePolar(50, 30)` moves 50mm at 30 degrees from +X.\n */\n translatePolar(radius: number, angleDeg: number, z?: number): Shape;\n /** Move the shape relative to its current position. All transforms are immutable and return new shapes. */\n translate(x: number, y: number, z: number): Shape;\n /** Position the shape so its bounding box min corner is at the given global coordinate. */\n moveTo(x: number, y: number, z: number): Shape;\n /** Position the shape relative to another shape\'s local coordinate system (bounding box min corner). */\n moveToLocal(target: Shape | {\n toShape(): Shape;\n }, x: number, y: number, z: number): Shape;\n /** Rotate around an arbitrary axis through the origin. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): Shape;\n /** Rotate around the X axis by the given angle in degrees. */\n rotateX(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): Shape;\n /** Rotate around the Y axis by the given angle in degrees. */\n rotateY(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): Shape;\n /** Rotate around the Z axis by the given angle in degrees. */\n rotateZ(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): Shape;\n /** Apply a 4x4 affine transform matrix (column-major) or a Transform object. */\n transform(m: Mat4 | Transform): Shape;\n /** Scale the shape uniformly or per-axis from the shape\'s bounding box center. Accepts a single number or [x, y, z] array. */\n scale(v: number | [\n number,\n number,\n number\n ]): Shape;\n /** Scale the shape uniformly or per-axis from an explicit pivot point. */\n scaleAround(pivot: [\n number,\n number,\n number\n ], v: number | [\n number,\n number,\n number\n ]): Shape;\n /** Mirror across a plane through the shape\'s bounding box center, defined by its normal vector. */\n mirror(normal: [\n number,\n number,\n number\n ]): Shape;\n /** Mirror across a plane through an explicit point, defined by its normal vector. */\n mirrorThrough(point: [\n number,\n number,\n number\n ], normal: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Reorient a shape so its primary axis (Z) points along the given direction.\n * Useful for laying cylinders/extrusions along X or Y without thinking about Euler angles.\n * The shape\'s origin stays at [0,0,0] — translate after pointAlong to position it.\n *\n * Example: cylinder(40, 5).pointAlong([1, 0, 0]) — lays cylinder along X, starting at origin\n */\n pointAlong(direction: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Rotate around an arbitrary axis through a pivot point.\n * Equivalent to: translate(-pivot) → rotate around axis → translate(+pivot)\n * @internal Prefer rotate(), rotateX(), rotateY(), rotateZ() for public use.\n */\n rotateAroundAxis(axis: [\n number,\n number,\n number\n ], angleDeg: number, pivot?: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point.\n * `movingPoint` / `targetPoint` may be raw world points or this shape\'s anchors/references.\n */\n rotateAroundTo(axis: [\n number,\n number,\n number\n ], pivot: [\n number,\n number,\n number\n ], movingPoint: RotationPointLike, targetPoint: RotationPointLike, options?: RotateAroundToOptions): Shape;\n /** Unwrap any object with toShape() without circular import. */\n private static _unwrap;\n /**\n * Warn if a boolean operation had no geometric effect.\n * Compares volumes before and after; if they match within tolerance, the operation was a no-op.\n */\n private static _checkBooleanNoOp;\n /** Union this shape with others (additive boolean). Method form of union(). */\n add(...others: ShapeOperandInput[]): Shape;\n /** Subtract other shapes from this one. Method form of difference(). */\n subtract(...others: ShapeOperandInput[]): Shape;\n /** Keep only the overlap with other shapes. Method form of intersection(). */\n intersect(...others: ShapeOperandInput[]): Shape;\n /** Alias for add() — matches the free-function union() naming. */\n union(...others: ShapeOperandInput[]): Shape;\n /** Alias for subtract() — matches the free-function difference() naming. */\n difference(...others: ShapeOperandInput[]): Shape;\n /** Alias for intersect() — matches the free-function intersection() naming. */\n intersection(...others: ShapeOperandInput[]): Shape;\n /** Split into [inside, outside] by another shape. */\n split(cutter: Shape | {\n toShape(): Shape;\n }): [\n Shape,\n Shape\n ];\n /** Split by infinite plane. Returns [positive-side, negative-side]. */\n splitByPlane(normal: [\n number,\n number,\n number\n ], originOffset?: number): [\n Shape,\n Shape\n ];\n /** Keep the positive side of the plane and discard the opposite side. */\n trimByPlane(normal: [\n number,\n number,\n number\n ], originOffset?: number): Shape;\n /** Hollow out compile-covered boxes, cylinders, and straight extrudes.\n * `openFaces` names any subset of the base shape\'s labeled faces to leave open (no wall).\n */\n shell(thickness: number, opts?: {\n openFaces?: string[];\n }): Shape;\n /** Offset-thicken an exact open surface or shell into a solid. */\n thicken(thickness: number): Shape;\n /** Get the axis-aligned bounding box as { min: [x,y,z], max: [x,y,z] }. */\n boundingBox(): ShapeRuntimeBounds;\n /** Volume in mm cubed. */\n volume(): number;\n /** Surface area in mm squared. */\n surfaceArea(): number;\n /** True if the shape contains no geometry. */\n isEmpty(): boolean;\n /** Number of disconnected solid bodies in this shape. */\n numBodies(): number;\n /** Triangle count of the mesh representation. */\n numTri(): number;\n /** Extract triangle mesh for Three.js rendering */\n getMesh(): ShapeRuntimeMesh;\n /** Slice the runtime solid by a plane normal to local Z at the given offset. */\n slice(offset?: number): any;\n /** Orthographically project the runtime solid onto the local XY plane. */\n project(): any;\n /**\n * Position this shape relative to another using named 3D anchor points.\n *\n * Anchors are bounding-box-relative: \'center\', face centers (\'top\', \'front\', ...),\n * edge midpoints (\'top-front\', \'back-left\', ...), and corners (\'top-front-left\', ...).\n * Anchor word order is flexible: \'front-left\' and \'left-front\' are equivalent.\n * Named placement references (from withReferences) can also be used as anchors.\n */\n attachTo(target: ShapeAnchorTarget, targetAnchor: PlacementAnchorLike, selfAnchor?: PlacementAnchorLike, offset?: [\n number,\n number,\n number\n ]): Shape;\n /**\n * Place this shape on a face of a parent shape.\n *\n * Think of it like sticking a label on a box surface:\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 onFace(parent: ShapeAnchorTarget, face: "front" | "back" | "left" | "right" | "top" | "bottom", opts?: {\n u?: number;\n v?: number;\n protrude?: number;\n }): Shape;\n /**\n * Slide this shape along an axis until a labeled face is embedded in the target body.\n *\n * Position the shape roughly first (translate/rotate), then call seatInto to\n * auto-adjust the penetration depth. No manual coordinate math needed.\n *\n * ```js\n * // Wing root embeds into fuselage — adapts to any fuselage shape\n * wing.translate(0, wingY, 0).seatInto(fuselage, \'root\');\n *\n * // Sensor pod sits flush on fuselage surface\n * pod.translate(0, station, radius + 20).seatInto(fuselage, \'base\', { depth: \'flush\' });\n *\n * // Antenna with 3mm gasket standoff\n * mast.translate(0, station, radius + 50).seatInto(fuselage, \'mount\', { depth: \'flush\', gap: 3 });\n * ```\n *\n * @param target - The body to embed into (must be a watertight solid).\n * @param surface - Label of the face on this shape that connects (from `.faces(mapping)`).\n * @param options - Movement axis (defaults to face normal direction), depth mode, and gap control.\n * @returns A new Shape translated along the movement axis.\n */\n seatInto(target: Shape, surface: string, options?: SeatIntoOptions): Shape;\n /**\n * Slide this shape until a target\'s labeled face is fully covered (inside this shape).\n *\n * The inverse of `seatInto`: instead of embedding *your* face into the target,\n * 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\n * nacelle.translate(rough).seatOver(pylon, \'bottom\');\n *\n * // Cap slides down over a post until post\'s top face is covered\n * cap.translate(rough).seatOver(post, \'top\');\n * ```\n *\n * @param target - The shape whose face you want to cover.\n * @param targetSurface - Label of the face on the TARGET shape to cover.\n * @param options - Movement axis (defaults to negative of target face normal), depth mode, and gap control.\n * @returns A new Shape translated along the movement axis.\n */\n seatOver(target: Shape, targetSurface: string, options?: SeatIntoOptions): Shape;\n /** Attach named connectors — attachment points that survive transforms and imports.\n * Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching). */\n withConnectors(connectors: Record<string, ConnectorInput>): Shape;\n /** List all connector names on this shape. */\n connectorNames(): string[];\n /** Get all connectors of a given type. */\n connectorsByType(type: string): Array<{\n name: string;\n port: ConnectorDef;\n }>;\n /** Distance between two connector origins on this shape. */\n connectorDistance(nameA: string, nameB: string): number;\n /** Get measurements metadata from a connector. */\n connectorMeasurements(name: string): Record<string, number | string>;\n /**\n * Position this shape by matching connectors to a target.\n *\n * Overloads:\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 matchTo(targetOrPairs: Shape | MatchTarget | Array<[\n Shape | MatchTarget,\n string,\n string\n ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): Shape;\n}\ntype MatchTarget = Shape | {\n portNames?(): string[];\n};\n/**\n * Create a rectangular box. Centered on XY, base at Z=0.\n *\n * Extents:\n * - X: `[-width/2, width/2]`\n * - Y: `[-depth/2, depth/2]`\n * - Z: `[0, height]`\n *\n * For named faces, build from a labeled sketch:\n * `rect(width, depth).labelEdges(\'s\', \'e\', \'n\', \'w\').extrude(height, { labels: { start: \'bottom\', end: \'top\' } })`.\n */\ndeclare function box$1(width: number, depth: number, height: number): Shape;\n/**\n * Create a cylinder or cone with named faces and edges.\n * Centered on XY, base at Z=0.\n *\n * Extents:\n * - X/Y: centered at the origin\n * - Z: `[0, height]`\n *\n * `radiusTop` defaults to `radius`. Set `radiusTop` smaller to taper the side,\n * or `0` for a pointy cone. Use `segments` to create regular prisms\n * (for example `6` for a hexagonal prism).\n *\n * Named faces: `top`, `bottom`, `side`\n * Named edges: `top-rim`, `bottom-rim`\n */\ndeclare function cylinder$1(height: number, radius: number, radiusTop?: number, segments?: number): Shape;\n/**\n * Create a sphere centered at the origin.\n *\n * Extents:\n * - X: `[-radius, radius]`\n * - Y: `[-radius, radius]`\n * - Z: `[-radius, radius]`\n *\n * Use `segments` for lower-poly approximations.\n */\ndeclare function sphere$1(radius: number, segments?: number): Shape;\n/**\n * Create a torus (donut shape) lying in the XY plane. Centered on all axes.\n *\n * Extents:\n * - X: `[-(majorRadius + minorRadius), +(majorRadius + minorRadius)]`\n * - Y: `[-(majorRadius + minorRadius), +(majorRadius + minorRadius)]`\n * - Z: `[-minorRadius, minorRadius]`\n *\n * The origin is the center of the ring.\n */\ndeclare function torus$1(majorRadius: number, minorRadius: number, segments?: number): Shape;\n/**\n * Combine shapes into a single solid (additive boolean).\n *\n * Accepts individual shapes, or an array of shapes. `union()` returns one\n * solid, so only the first operand\'s color is preserved in the result. Use\n * `group()` when you want separate child colors or identities.\n */\ndeclare function union(...inputs: ShapeOperandInput[]): Shape;\n/**\n * Subtract shapes from a base shape (subtractive boolean).\n *\n * The first shape is the base; all subsequent shapes are subtracted from it.\n * Accepts individual shapes, or an array of shapes.\n */\ndeclare function difference(...inputs: ShapeOperandInput[]): Shape;\n/**\n * Keep only the overlapping volume of the input shapes (intersection boolean).\n *\n * Requires at least two shapes. Accepts individual shapes, or an array.\n */\ndeclare function intersection(...inputs: ShapeOperandInput[]): Shape;\ntype Constraint3DType = "flush" | "align" | "parallel" | "faceDistance" | "concentric" | "axisParallel" | "pointCoincident" | "pointOnFace" | "pointOnAxis" | "angle" | "fixed";\ninterface BodyFeatureRef {\n bodyId: string;\n featureName: string;\n}\ninterface Constraint3D {\n id: string;\n type: Constraint3DType;\n refA: BodyFeatureRef;\n refB: BodyFeatureRef;\n value?: number;\n}\ndeclare class MateBuilder {\n readonly constraints: Constraint3D[];\n private nextId;\n private add;\n /** Constrain two faces so they stay flush. */\n flush(faceA: string, faceB: string): string;\n /** Constrain two faces so their normals align. */\n align(faceA: string, faceB: string): string;\n /** Constrain two faces so they remain parallel. */\n parallel(faceA: string, faceB: string): string;\n /** Constrain the distance between two faces. */\n faceDistance(faceA: string, faceB: string, distance: number): string;\n /** Constrain two axes to share the same center line. */\n concentric(axisA: string, axisB: string): string;\n /** Constrain two axes to remain parallel. */\n axisParallel(axisA: string, axisB: string): string;\n /** Constrain two points to coincide. */\n pointCoincident(pointA: string, pointB: string): string;\n /** Constrain a point to lie on a face. */\n pointOnFace(point: string, face: string): string;\n /** Constrain a point to lie on an axis. */\n pointOnAxis(point: string, axis: string): string;\n /** Constrain the angle between two faces. */\n angle(faceA: string, faceB: string, degrees: number): string;\n /** Total constraint equations. */\n get totalEquations(): number;\n}\ntype AssemblyPart = Shape | ShapeGroup;\ntype JointType = "fixed" | "revolute" | "prismatic";\ntype JointState = Record<string, number | undefined>;\ninterface PartMetadata {\n material?: string;\n process?: string;\n tolerance?: string;\n qty?: number;\n notes?: string;\n densityKgM3?: number;\n massKg?: number;\n [key: string]: unknown;\n}\ninterface PartOptions {\n transform?: TransformInput;\n metadata?: PartMetadata;\n}\ninterface JointOptions {\n frame?: TransformInput;\n origin?: [\n number,\n number,\n number\n ];\n axis?: Vec3;\n min?: number;\n max?: number;\n default?: number;\n unit?: string;\n effort?: number;\n velocity?: number;\n damping?: number;\n friction?: number;\n /** Connector refs that define this joint contract. Usually set by `connect()` / `match()`. */\n connectorRefs?: JointConnectorRefs;\n}\ninterface JointConnectorRefs {\n parent: string;\n child: string;\n parentAlign?: PortAlign;\n childAlign?: PortAlign;\n}\ninterface JointCouplingTerm {\n joint: string;\n ratio?: number;\n}\ninterface JointCouplingOptions {\n terms: JointCouplingTerm[];\n offset?: number;\n}\ninterface GearRatioLike {\n jointRatio: number;\n /** Phase offset (degrees) for tooth mesh alignment. Auto-applied as coupling\n * offset when provided via `addGearCoupling({ pair })` and no explicit offset is set. */\n phaseDeg?: number;\n}\ninterface GearCouplingOptions {\n ratio?: number;\n pair?: GearRatioLike;\n driverTeeth?: number;\n drivenTeeth?: number;\n mesh?: "external" | "internal" | "bevel" | "face";\n offset?: number;\n driverOrigin?: [\n number,\n number,\n number\n ];\n drivenOrigin?: [\n number,\n number,\n number\n ];\n}\ninterface PartRecord {\n name: string;\n part: AssemblyPart;\n base: Transform;\n metadata?: PartMetadata;\n}\ninterface JointCouplingTermRecord {\n joint: string;\n ratio: number;\n}\ninterface AssemblyPartDef {\n name: string;\n part: AssemblyPart;\n base: Transform;\n metadata?: PartMetadata;\n}\ninterface AssemblyJointDef {\n name: string;\n type: JointType;\n parent: string;\n child: string;\n frame: Transform;\n axis: Vec3;\n min?: number;\n max?: number;\n defaultValue: number;\n unit?: string;\n effort?: number;\n velocity?: number;\n damping?: number;\n friction?: number;\n connectorRefs?: JointConnectorRefs;\n}\ninterface AssemblyJointCouplingDef {\n joint: string;\n terms: JointCouplingTermRecord[];\n offset: number;\n}\ninterface AssemblyDefinition {\n name: string;\n parts: AssemblyPartDef[];\n joints: AssemblyJointDef[];\n jointCouplings: AssemblyJointCouplingDef[];\n}\ninterface BomRow {\n part: string;\n qty: number;\n material?: string;\n process?: string;\n tolerance?: string;\n notes?: string;\n metadata?: PartMetadata;\n}\ninterface CollisionOptions {\n parts?: string[];\n ignorePairs?: Array<[\n string,\n string\n ]>;\n minOverlapVolume?: number;\n}\ninterface CollisionFinding {\n partA: string;\n partB: string;\n overlapVolume: number;\n}\ninterface JointSweepFrame {\n value: number;\n collisions: CollisionFinding[];\n warnings: string[];\n}\n/**\n * Convert an array of BOM rows into a CSV string.\n *\n * **Details**\n *\n * Produces a CSV with columns: `part`, `qty`, `material`, `process`, `tolerance`, `notes`.\n * String values are quoted and internal double-quotes are escaped. Prefer calling\n * `solvedAssembly.bomCsv()` directly — this function is exposed for custom BOM processing.\n *\n * @param rows - BOM rows from `SolvedAssembly.bom()`\n * @returns CSV string with header row\n * @see {@link SolvedAssembly.bomCsv} for the all-in-one method\n * @category Assembly\n */\ndeclare function bomToCsv(rows: BomRow[]): string;\ninterface MateMetadata {\n explodeHints: Record<string, {\n direction: Vec3;\n }>;\n dof: number;\n converged: boolean;\n}\n/**\n * The result of solving an assembly at a specific joint state.\n *\n * **Details**\n *\n * `SolvedAssembly` holds world-space transforms for every part at a given pose.\n * Top-level scripts can return a `SolvedAssembly` directly for display. Use\n * `toGroup()` when you specifically need a `ShapeGroup` for composition,\n * group-style transforms, or named-child lookup. Do not call `toGroup()` just\n * to make a solved assembly render. Use `getPart()` / `getTransform()` to\n * inspect individual parts programmatically.\n *\n * **Validation**\n *\n * Call `collisionReport()` to detect overlapping parts, or `sweepJoint()` on\n * the parent `Assembly` to check for interference across the joint\'s motion range.\n *\n * **Example**\n *\n * ```ts\n * const solved = mech.solve({ shoulder: 45, elbow: -20 });\n * console.log("Collisions", solved.collisionReport());\n * return solved;\n * ```\n *\n * @category Assembly\n */\ndeclare class SolvedAssembly {\n readonly name: string;\n private readonly parts;\n private readonly transforms;\n private readonly jointValues;\n private readonly solveWarnings;\n private readonly _mateMetadata;\n private readonly _usedPortRefs;\n constructor(name: string, parts: Map<string, PartRecord>, transforms: Map<string, Transform>, jointValues: JointState, solveWarnings: string[], _mateMetadata?: MateMetadata | null, usedPortRefs?: ReadonlySet<string>);\n /** Return any warnings generated during solve (clamped joints, unconverged mates, etc.). */\n warnings(): string[];\n /** Return a snapshot of resolved joint values (after clamping and coupling). */\n getJointState(): JointState;\n /** Explode direction hints derived from mate constraints, or null if no mates. */\n get mateExplodeHints(): Record<string, {\n direction: Vec3;\n }> | null;\n /** Remaining degrees of freedom after mate constraints, or null if no mates. */\n get mateDof(): number | null;\n /** Whether the mate constraint solver converged, or null if no mates. */\n get mateConverged(): boolean | null;\n /**\n * Return the world-space `Transform` for the named part at the solved pose.\n *\n * @param partName - Name as registered with `addPart()`\n * @returns The world transform (position + orientation) of the part\n * @category Assembly\n */\n getTransform(partName: string): Transform;\n /**\n * Return the named part already positioned at its solved world transform.\n *\n * @param partName - Name as registered with `addPart()`\n * @returns The part (`Shape` or `ShapeGroup`) at its solved world position\n * @category Assembly\n */\n getPart(partName: string): AssemblyPart;\n /**\n * Convert all solved parts into a `ShapeGroup` with named children.\n *\n * **Details**\n *\n * Each part becomes a named child in the group, already positioned at its\n * solved world transform. Use this only when you specifically need a\n * `ShapeGroup` for composition, `ShapeGroup` transforms, or named-child\n * access. Top-level scripts can return the `SolvedAssembly` directly; do not\n * call `toGroup()` just to make a solved assembly render.\n *\n * **Example**\n *\n * ```ts\n * const armGroup = mech.solve({ shoulder: 60 }).toGroup(); // only because we need rotateZ()\n * return armGroup.rotateZ(90);\n * ```\n *\n * @returns `ShapeGroup` with one child per part, each at its solved world position\n * @category Assembly\n */\n toGroup(): ShapeGroup;\n /**\n * Return an array of named scene objects for the viewport renderer.\n *\n * **Details**\n *\n * Each part becomes `{ name, shape }` or `{ name, group: [...] }` if the\n * part is a `ShapeGroup`. Top-level scripts should normally return the\n * `SolvedAssembly` directly. Use `toGroup()` when you need `ShapeGroup`\n * behavior; use this method only for advanced scene-graph control where you\n * need access to the flat per-part array with metadata.\n *\n * @returns Array of `{ name, shape?, group?, metadata? }` for each part\n * @category Assembly\n */\n toSceneObjects(): Array<{\n name: string;\n shape?: Shape;\n group?: Array<{\n name: string;\n shape: Shape;\n }>;\n metadata?: PartMetadata;\n }>;\n /**\n * Backward-compatible alias for `toSceneObjects()`.\n *\n * @deprecated Use `toSceneObjects()` or `toGroup()` instead.\n */\n toScene(): Array<{\n name: string;\n shape?: Shape;\n group?: Array<{\n name: string;\n shape: Shape;\n }>;\n metadata?: PartMetadata;\n }>;\n /**\n * Generate a bill of materials for all parts in the solved assembly.\n *\n * @returns Array of `BomRow` with part name, qty, and any metadata (material, process, etc.)\n * @see {@link bomCsv} for CSV-formatted output\n * @category Assembly\n */\n bom(): BomRow[];\n /**\n * Generate a bill of materials as a CSV string.\n *\n * @returns CSV-formatted BOM with columns: part, qty, material, process, tolerance, notes\n * @see {@link bom} for the structured row array\n * @category Assembly\n */\n bomCsv(): string;\n /**\n * Detect overlapping (colliding) part pairs in this solved pose.\n *\n * **Details**\n *\n * Computes boolean intersections between all part pairs and returns findings\n * where the overlap volume exceeds `minOverlapVolume` (default 0.1 mm³).\n *\n * **Example**\n *\n * ```ts\n * const solved = mech.solve({ shoulder: 35, elbow: 60 });\n * console.log("Collisions", solved.collisionReport());\n * ```\n *\n * @param options - Filter by `parts`, ignore specific `ignorePairs`, or set `minOverlapVolume`\n * @returns Array of `{ partA, partB, overlapVolume }` for each interference found\n * @category Assembly\n */\n collisionReport(options?: CollisionOptions): CollisionFinding[];\n /**\n * Compute the minimum gap (clearance) between two parts in this solved pose.\n *\n * **Details**\n *\n * Returns `0` if the parts are touching or overlapping. Requires the Manifold backend.\n * `searchLength` bounds the search radius in mm — increase it for widely separated parts.\n *\n * @param partA - Name of the first part\n * @param partB - Name of the second part\n * @param searchLength - Maximum search radius in mm (default 10)\n * @returns Minimum gap in mm; `0` means touching or interfering\n * @category Assembly\n */\n minClearance(partA: string, partB: string, searchLength?: number): number;\n}\ninterface ConnectOptions {\n as?: string;\n type?: JointType;\n min?: number;\n max?: number;\n default?: number;\n unit?: string;\n /**\n * @deprecated Connectors now always meet face-to-face (anti-parallel axes).\n * This parameter is ignored. If your connectors produce wrong orientation,\n * fix the connector axis directions instead of using flip.\n */\n flip?: boolean;\n /** Which point on the parent connector to align: \'start\', \'middle\' (default), or \'end\'. */\n parentAlign?: PortAlign;\n /** Which point on the child connector to align: \'start\', \'middle\' (default), or \'end\'. */\n childAlign?: PortAlign;\n /** Shorthand: set both parentAlign and childAlign at once. */\n align?: PortAlign;\n effort?: number;\n velocity?: number;\n damping?: number;\n friction?: number;\n}\ninterface ToJointsViewOptions {\n defaults?: Record<string, number>;\n overrides?: Record<string, Partial<JointViewInput>>;\n animations?: JointViewAnimationInput[];\n couplings?: JointViewCouplingInput[];\n defaultAnimation?: string;\n enabled?: boolean;\n}\n/**\n * Container for a kinematic mechanism made up of named parts and joints.\n *\n * **Details**\n *\n * An assembly is a directed graph where **parts** are nodes and **joints** are\n * directed edges from parent to child. The graph must be a forest (one or more\n * trees with no cycles). Root parts (no incoming joint) are fixed to world space.\n *\n * Each joint carries a `frame` transform (from the parent part frame to the\n * joint\'s zero-state frame) and a motion formula:\n *\n * ```\n * childWorld = parentWorld × frame × motion(value) × childBase\n * ```\n *\n * Three joint types are supported:\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\n* const 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 *\n * return mech; // auto-solved at defaults\n * ```\n *\n * Returning an unsolved `Assembly` auto-solves at default joint values. Return\n * a `SolvedAssembly` directly for a specific pose:\n *\n * ```ts\n * return 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 * @category Assembly\n */\ndeclare class Assembly {\n readonly name: string;\n private readonly parts;\n private readonly joints;\n private readonly jointCouplings;\n private readonly _mateFns;\n private _refs;\n private readonly _portsByPart;\n private readonly _usedPortRefs;\n private _connectCounter;\n constructor(name?: string);\n private _fork;\n /** Connector refs (e.g. "PartName.connectorName") consumed by connect/match calls. */\n get usedConnectorRefs(): ReadonlySet<string>;\n /**\n * Legacy alias for `usedConnectorRefs`.\n *\n * @deprecated Use `usedConnectorRefs` instead.\n */\n get usedPortRefs(): ReadonlySet<string>;\n /**\n * Register mate constraints between parts.\n * Constraints are solved during `solve()` to derive part positions and explode hints.\n * Part references use "partName:featureName" format.\n */\n mate(fn: (m: MateBuilder) => void): Assembly;\n /**\n * Attach named placement reference points to this assembly.\n * These are surfaced automatically on the ImportedAssembly when this file is\n * imported via require(), so consumers can use placeReference() without\n * re-declaring them.\n * Returns a new Assembly — does not mutate.\n */\n withReferences(refs: Pick<PlacementReferenceInput, "points">): Assembly;\n /** @internal — used by require() to seed ImportedAssembly refs. */\n getReferences(): PlacementReferences;\n /**\n * Attach named connectors to a specific part or the assembly as a whole.\n *\n * **Details**\n *\n * Connectors declared this way are in the part\'s local coordinate system.\n * They are captured automatically if the incoming `Shape` already has\n * connectors via `shape.withConnectors(...)`, but you can also add or\n * override connectors after the fact with this method.\n *\n * Use the single-argument overload to attach assembly-level connectors —\n * these are exposed when this assembly is imported as a sub-assembly.\n *\n * @param partName - Part name (must already be added via `addPart`)\n * @param connectors - Map of connector name → `ConnectorInput`\n * @returns `this` for chaining\n * @see {@link connect} for using connectors to create joints\n * @category Connectors\n */\n withConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly;\n withConnectors(connectors: Record<string, ConnectorInput>): Assembly;\n /**\n * Backward-compatible alias for `withConnectors()`.\n *\n * @deprecated Use `withConnectors()` instead.\n */\n withPorts(partNameOrPorts: string | Record<string, PortInput>, maybePorts?: Record<string, PortInput>): Assembly;\n /** Get connectors declared on a part in part-local space. */\n getConnectors(partName: string): ConnectorMap;\n /**\n * Backward-compatible alias for `getConnectors()`.\n *\n * @deprecated Use `getConnectors()` instead.\n */\n getPorts(partName: string): PortMap;\n /** @internal — set already-normalized connectors directly (used by mergeInto). */\n _setPortsDirect(partName: string, ports: PortMap): void;\n /** @internal — get all connector maps, keyed by part name. */\n getAllPorts(): Map<string, PortMap>;\n /**\n * Parse a "PartName.connectorName" reference and return the resolved connector.\n * Throws descriptive errors if the part or connector doesn\'t exist.\n */\n getConnector(ref: string): {\n partName: string;\n connectorName: string;\n connector: ConnectorDef;\n };\n /**\n * Legacy alias for `getConnector()`.\n *\n * @deprecated Use `getConnector()` instead.\n */\n getPort(ref: string): {\n partName: string;\n portName: string;\n port: PortDef;\n };\n /**\n * Add a virtual reference frame (no geometry) to the assembly graph.\n *\n * **Details**\n *\n * Useful when you need a named pivot point or coordinate frame that has no\n * visual geometry. Acts like a zero-volume part and can be connected to\n * other parts via joints.\n *\n * @param name - Unique part name for the frame in the assembly graph\n * @param options - Optional transform and metadata\n * @returns `this` for chaining\n * @category Assembly\n */\n addFrame(name: string, options?: PartOptions): Assembly;\n /**\n * Add a named part to the assembly.\n *\n * **Details**\n *\n * Connectors declared on the part (via `withConnectors()`) are captured automatically.\n * Parts are positioned at world origin by default unless a `transform` is\n * provided in `options`. For root parts (no incoming joint), `transform` is\n * their final world position.\n *\n * When a part is a `ShapeGroup`, name the group children explicitly to get\n * readable viewport labels (e.g. `"Base Assembly.Body"` instead of\n * `"Base Assembly.1"`):\n *\n * ```ts\n * const housing = group(\n * { name: "Body", shape: body },\n * { name: "Lid", shape: lid },\n * );\n * assembly.addPart("Base Assembly", housing);\n * ```\n *\n * @param name - Unique part name (must not already exist)\n * @param part - The `Shape` or `ShapeGroup` geometry\n * @param options - Optional `{ transform, metadata }` (material, process, qty, etc.)\n * @returns `this` for chaining\n * @category Assembly\n */\n addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly;\n /**\n * Add a kinematic joint between a parent and child part.\n *\n * **Details**\n *\n * `frame` is a transform from the **parent part frame** to the **joint frame\n * at zero state**. The child\'s world position is computed as:\n *\n * ```\n * childWorld = parentWorld × frame × motion(value) × childBase\n * ```\n *\n * For revolute joints `value` is in degrees; for prismatic joints `value` is\n * in mm. Coupled joints (see `addJointCoupling`) ignore the `state` value\n * passed to `solve()` and compute their value from source joints.\n *\n * @param name - Unique joint name\n * @param type - `\'revolute\'`, `\'prismatic\'`, or `\'fixed\'`\n * @param parent - Name of the parent part (already added via `addPart`)\n * @param child - Name of the child part (already added via `addPart`)\n * @param options - Frame, axis, limits, effort, velocity, damping, friction\n * @returns `this` for chaining\n * @category Joints\n */\n addJoint(name: string, type: JointType, parent: string, child: string, options?: JointOptions): Assembly;\n /**\n * Shorthand for `addJoint(name, \'revolute\', parent, child, options)`.\n *\n * @param name - Unique joint name\n * @param parent - Parent part name\n * @param child - Child part name\n * @param options - Frame, axis, min/max degrees, effort, velocity, damping, friction\n * @returns `this` for chaining\n * @category Joints\n */\n addRevolute(name: string, parent: string, child: string, options?: JointOptions): Assembly;\n /**\n * Shorthand for `addJoint(name, \'prismatic\', parent, child, options)`.\n *\n * @param name - Unique joint name\n * @param parent - Parent part name\n * @param child - Child part name\n * @param options - Frame, axis, min/max mm, effort, velocity, damping, friction\n * @returns `this` for chaining\n * @category Joints\n */\n addPrismatic(name: string, parent: string, child: string, options?: JointOptions): Assembly;\n /**\n * Shorthand for `addJoint(name, \'fixed\', parent, child, options)`.\n *\n * Fixed joints rigidly attach a child part to its parent at `frame` with no motion.\n * Before calling `mergeInto()`, use `addFixed()` to collapse multiple root parts\n * into a single root.\n *\n * @param name - Unique joint name\n * @param parent - Parent part name\n * @param child - Child part name\n * @param options - Optional frame transform for placement offset\n * @returns `this` for chaining\n * @category Joints\n */\n addFixed(name: string, parent: string, child: string, options?: JointOptions): Assembly;\n /**\n * Connect two parts by aligning their declared connectors, automatically computing frame and axis.\n *\n * **Details**\n *\n * Connector references use `"PartName.connectorName"` format. The system aligns connector\n * origins (child connector lands exactly on parent connector) and derives the joint frame\n * 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\n * meeting a socket. Each connector\'s axis points "outward" from its part. When two\n * connectors mate, the system brings them together so their axes oppose (anti-parallel).\n * This is the same convention used by `matchTo()`.\n *\n * For a revolute joint (hinge), both connectors\' axes should point outward from their\n * respective parts along the hinge line. For a prismatic joint (slider), both axes\n * should point along the slide direction from their part\'s perspective.\n *\n * The joint type is inferred from the connector\'s `kind` field if not specified in `options`.\n *\n * When connectors are defined with `start`/`end`, you can control which point on each\n * connector meets via `align` / `parentAlign` / `childAlign` (`\'start\'`, `\'middle\'`, `\'end\'`).\n *\n * Use `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore).\n * For mechanisms where parts share an axis but are deliberately spaced apart, use\n * `addRevolute()` with pre-positioned parts instead.\n *\n * **Example**\n *\n * ```ts\n * // Hinge: both axes point outward along the hinge line\n * const frame = box(100, 10, 80).withConnectors({\n * hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, 1] }),\n * });\n * const door = box(60, 4, 80).withConnectors({\n * hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, -1] }),\n * });\n * assembly("Door")\n * .addPart("Frame", frame)\n * .addPart("Door", door)\n * .connect("Frame.hinge", "Door.hinge", { as: "swing", min: 0, max: 110 });\n * ```\n *\n * @param parentConnectorRef - `"PartName.connectorName"` on the parent side\n * @param childConnectorRef - `"PartName.connectorName"` on the child side\n * @param options - `as` (joint name), `type`, `min`, `max`, `default`, `align`, effort, velocity, etc.\n * @returns `this` for chaining\n * @see {@link match} for typed connector matching with gender/type validation\n * @category Connectors\n */\n connect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly;\n /**\n * Auto-create a joint by matching typed connectors between two parts.\n *\n * **Details**\n *\n * Connectors can carry a `connectorType` string and a `gender`\n * (`\'male\'`, `\'female\'`, or `\'neutral\'`). `match()` validates type and gender\n * compatibility (use `{ force: true }` to skip validation) and creates the\n * joint automatically from the connector\'s `kind` metadata.\n *\n * The `pairs` map is `{ childConnector: parentConnector }`. The first pair\n * drives joint creation; additional pairs are validated but do not create\n * additional joints (they constrain the same rigid connection).\n *\n * Define connectors on shapes with `shape.withConnectors(...)`:\n *\n * ```ts\n * const 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 *\n * Then match in the assembly:\n *\n * ```ts\n * const 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 * @param childPartName - Part name of the child\n * @param parentPartName - Part name of the parent\n * @param pairs - Map of `{ childConnectorName: parentConnectorName }`\n * @param options - `force` (skip gender/type validation), `as` (joint name override)\n * @returns `this` for chaining\n * @see {@link connect} for connector-based connections without type/gender validation\n * @category Connectors\n */\n match(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & {\n as?: string;\n }): Assembly;\n /**\n * Link a joint\'s value to a linear combination of other joint values.\n *\n * **Details**\n *\n * The driven joint\'s value is computed as:\n *\n * ```\n * driven = offset + Σ(ratio_i × source_i)\n * ```\n *\n * Coupled joints ignore any value passed in `solve(state)` — a warning is\n * emitted if you try to override one. Coupling cycles are rejected. You cannot\n * sweep a coupled joint directly; sweep one of its source joints instead.\n *\n * **Example**\n *\n * ```ts\n * assembly\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 * @param jointName - Name of the joint to drive (must exist and not be fixed)\n * @param options - `{ terms, offset? }` where each term is `{ joint, ratio? }`\n * @returns `this` for chaining\n * @see {@link addGearCoupling} for a gear-ratio shorthand\n * @category Joints\n */\n addJointCoupling(jointName: string, options: JointCouplingOptions): Assembly;\n /**\n * Link two revolute joints via a gear ratio.\n *\n * **Details**\n *\n * Choose exactly one ratio source:\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\n * (`\'external\'` = negative/opposite rotation, `\'internal\'` = positive, `\'bevel\'`/`\'face\'` = negative)\n *\n * When `pair` carries a `phaseDeg`, it is auto-applied as the coupling `offset`\n * to align teeth correctly. Override with `offset: 0` if gear shapes already\n * have the phase baked in.\n *\n * **Example**\n *\n * ```ts\n * const pair = lib.gearPair({ pinion: { module: 1.25, teeth: 14 }, gear: { module: 1.25, teeth: 42 } });\n * assembly\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 * @param drivenJointName - The joint to be driven (must be revolute)\n * @param driverJointName - The driving joint (must be revolute)\n * @param options - Ratio source (`ratio`, `pair`, or `driverTeeth`/`drivenTeeth`); optional `offset`, `mesh`\n * @returns `this` for chaining\n * @see {@link addJointCoupling} for multi-term linear couplings\n * @category Joints\n */\n addGearCoupling(drivenJointName: string, driverJointName: string, options?: GearCouplingOptions): Assembly;\n private assertJointCouplingsAcyclic;\n /**\n * Solve the assembly at the given joint state and return positioned parts.\n *\n * **Details**\n *\n * Performs a depth-first traversal of the joint graph. Each joint\'s value\n * is taken from `state`, falling back to `defaultValue`. Coupled joints\n * compute their value from source joints. Values outside `[min, max]` are\n * clamped (a warning is added to `SolvedAssembly.warnings()`).\n *\n * If mate constraints were registered via `mate()`, the solver runs a\n * pre-pass to derive base transforms, then the kinematic DFS applies joints\n * on top of those positions.\n *\n * **Pitfall — `jointsView` double-rotation:**\n * When calling `toJointsView()`, always solve at the rest pose (all joint\n * values = 0 or default). Solving at a non-zero angle and then animating\n * will double-rotate parts. Use the `defaults` option on `toJointsView()`\n * to set the initial display angle instead.\n *\n * This pitfall only applies when `toJointsView()` is active. If you only want\n * a static posed result, return the solved assembly directly and skip\n * `toJointsView()`.\n *\n * **Example — static posed output (no `toJointsView()`)**\n *\n * ```ts\n * return mech.solve({ shoulder: 45, elbow: -20 });\n * ```\n *\n * @param state - Map of joint name → value; omitted joints use their `default`\n * @returns `SolvedAssembly` with all parts at their computed world positions\n * @category Assembly\n */\n solve(state?: JointState): SolvedAssembly;\n /**\n * Sample a joint through its motion range, collecting collision data at each step.\n *\n * **Details**\n *\n * Divides `[from, to]` into `steps` intervals (producing `steps + 1` frames).\n * At each sample, the assembly is solved with the sweeping joint at that value and\n * `baseState` for all others. Returns one `JointSweepFrame` per sample with the\n * joint value, collision findings, and any solve warnings.\n *\n * You cannot sweep a coupled joint — sweep one of its source joints instead.\n *\n * **Example**\n *\n * ```ts\n * const sweep = mech.sweepJoint("elbow", -10, 135, 12, { shoulder: 35 });\n * const hits = sweep.filter(frame => frame.collisions.length > 0);\n * console.log(`Collisions at ${hits.length} of ${sweep.length} poses`);\n * ```\n *\n * @param jointName - Joint to sweep (must not be coupled)\n * @param from - Start value (degrees for revolute, mm for prismatic)\n * @param to - End value\n * @param steps - Number of intervals (produces `steps + 1` frames)\n * @param baseState - Fixed values for all other joints during the sweep\n * @param collisionOptions - Filter options forwarded to `collisionReport()`\n * @returns Array of `{ value, collisions, warnings }` frames\n * @category Assembly\n */\n sweepJoint(jointName: string, from: number, to: number, steps: number, baseState?: JointState, collisionOptions?: CollisionOptions): JointSweepFrame[];\n /**\n * Derive viewport joint controls from the assembly graph and register them.\n *\n * **Details**\n *\n * Solves the assembly at rest (all joints = default), then converts each joint\n * into a `JointViewInput` with world-space pivot and axis. Fixed joints become\n * hidden zero-range revolute entries so attached parts follow their parent during\n * animation. Joint couplings are forwarded to the viewport automatically.\n *\n * This method is optional. Call it only when you want viewport joint sliders,\n * coupled controls, or playback animations. If you only want geometry, return\n * the `Assembly` or `SolvedAssembly` directly and skip `toJointsView()`.\n *\n * **Critical pitfall:** Always call `toJointsView()` before solving for display.\n * Then solve at the **rest pose** (no state overrides) and return that solved\n * assembly result directly. Do not flatten it with `.toGroup()` if you want the\n * viewport joint animation to keep working.\n *\n * Do not solve at a non-zero angle when using `toJointsView()` — the viewport\n * will apply the same rotation again, double-rotating the part.\n *\n * **Example**\n *\n * ```ts\n * mech.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\n * return mech.solve();\n * ```\n *\n * @param options - `defaults` (initial display angles), `animations`, `couplings`, `overrides`, `enabled`\n * @category Assembly\n */\n toJointsView(options?: ToJointsViewOptions): void;\n /** Return the serializable assembly definition used by solve/inspect pipelines. */\n describe(): AssemblyDefinition;\n}\n/**\n * 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,\n * articulated fingers, doors — all start with `assembly()`, not with manual rotation math.\n * Don\'t build a static "extended pose" first and refactor to an assembly later: joint sliders,\n * animations, sweeps, collision detection, and robot export all flow from the kinematic graph.\n *\n * **Details**\n *\n * An assembly models a mechanism as a directed graph of parts connected by joints.\n * Parts are the nodes; joints are directed edges from parent to child. The graph\n * must be a forest (no cycles). Root parts (those with no incoming joint) are\n * anchored to world space.\n *\n * Three joint types are supported: `\'revolute\'` (hinge), `\'prismatic\'` (slider),\n * and `\'fixed\'` (rigid attachment). Use `addPart()` to add geometry, `addJoint()`\n * (or the shorthands `addRevolute()`, `addPrismatic()`, `addFixed()`) to connect\n * parts, and `solve()` to compute world-space positions at a given joint state.\n *\n * The higher-level `connect()` API uses declared **connectors** to compute joint\n * frames automatically. The `match()` API uses typed connectors (with gender and\n * type metadata) for automatic compatibility validation and joint creation.\n *\n * For multi-file assemblies, a file that returns an `Assembly` is importable via\n * `require()` and yields an `ImportedAssembly`. Use `mergeInto()` to flatten a\n * sub-assembly into a parent assembly.\n *\n * **Example**\n *\n * ```ts\n* const 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 *\n * return mech; // auto-solved at defaults, renders all parts\n * ```\n *\n * @param name - Display name for the assembly (used in BOM, robot export, etc.)\n * @returns A new `Assembly` builder\n * @category Assembly\n */\ndeclare function assembly(name?: string): Assembly;\ninterface MergeIntoOptions {\n /**\n * Prefix applied to every part name and joint name from the sub-assembly.\n * E.g. prefix "Left Arm" turns part "Base" into "Left Arm.Base".\n * Strongly recommended to avoid name collisions when merging multiple instances.\n */\n prefix?: string;\n /** Part name in the parent assembly to attach the sub-assembly root to. */\n mountParent: string;\n /** Name for the new mount joint in the parent graph. */\n mountJoint: string;\n /** Joint type for the mount connection (default: \'fixed\'). */\n mountType?: JointType;\n /** Frame, axis, limits, and other options for the mount joint. */\n mountOptions?: JointOptions;\n}\n/**\n * A wrapper around an imported `Assembly` that provides kinematic access and\n * convenient transform helpers.\n *\n * **Details**\n *\n * When a `.forge.js` file returns an unsolved `Assembly`, `require()` wraps it\n * in an `ImportedAssembly`. This preserves the kinematic structure — you can\n * call `solve()`, `sweepJoint()`, and `mergeInto()` — while also allowing\n * convenience transforms that auto-solve at default values.\n *\n * **Kinematic access**\n *\n * ```ts\n * const arm = require("./arm.forge.js");\n *\n * const solved = arm.solve({ shoulder: 45 }); // full kinematic solve\n * const link = arm.part("Link", { shoulder: 60 }); // single part at state\n * const group = arm.toGroup({ shoulder: 45 }); // only when ShapeGroup behavior is needed\n * ```\n *\n * **Convenience transforms** (auto-solve at defaults, return `ShapeGroup`):\n *\n * ```ts\n * const positioned = arm.rotateZ(-90).translate(0, -20, 50);\n * ```\n *\n * **Merging into a parent**\n *\n * ```ts\n * require("./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 * @category Assembly\n */\ndeclare class ImportedAssembly {\n private readonly _assembly;\n private readonly _refs;\n private readonly _offset;\n constructor(_assembly: Assembly, _refs?: PlacementReferences, _offset?: readonly [\n number,\n number,\n number\n ]);\n /** The underlying Assembly — use for sweepJoint, addPart into parent, etc. */\n get assembly(): Assembly;\n /** Solve the assembly at the given joint state (defaults to each joint\'s default value). */\n solve(state?: JointState): SolvedAssembly;\n /**\n * Return a specific named part positioned at the given joint state, with any\n * stored placement offset applied.\n */\n part(name: string, state?: JointState): AssemblyPart;\n /**\n * Convert all assembly parts to a ShapeGroup with named children.\n * Use this for composition, transforms, or child lookup — not as a required\n * render step for assemblies.\n * Child names match the part names used in the assembly.\n * Any stored placement offset and placement references are forwarded to the group.\n */\n toGroup(state?: JointState): ShapeGroup;\n /**\n * Attach named placement reference points to this assembly.\n * Points are simple 3D coordinates (relative to the assembly\'s own origin).\n * Returns a new ImportedAssembly — does not mutate.\n */\n withReferences(refs: Pick<PlacementReferenceInput, "points">): ImportedAssembly;\n /** List all attached placement reference names. */\n referenceNames(kind?: PlacementReferenceKind): string[];\n /**\n * Translate the assembly so the named reference point lands on `target`.\n * Returns a new ImportedAssembly — does not mutate.\n * All point refs are translated by the same delta.\n */\n placeReference(ref: string, target: [\n number,\n number,\n number\n ], offset?: [\n number,\n number,\n number\n ]): ImportedAssembly;\n /** Solve at defaults and return a translated ShapeGroup. */\n translate(x: number, y: number, z: number): ShapeGroup;\n /** Solve at defaults and return a rotated ShapeGroup. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Solve at defaults and return a ShapeGroup rotated around X. */\n rotateX(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Solve at defaults and return a ShapeGroup rotated around Y. */\n rotateY(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Solve at defaults and return a ShapeGroup rotated around Z. */\n rotateZ(angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): ShapeGroup;\n /** Solve at defaults and return a scaled ShapeGroup. */\n scale(v: number | [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Solve at defaults and return a mirrored ShapeGroup. */\n mirror(normal: [\n number,\n number,\n number\n ]): ShapeGroup;\n /** Solve at defaults and return a colored ShapeGroup. */\n color(hex: string): ShapeGroup;\n /** Solve at defaults, get a named child part from the resulting group. */\n child(name: string): Shape | Sketch | ShapeGroup;\n /**\n * Flatten this sub-assembly\'s parts and joints into `parent` and wire a mount joint.\n *\n * **Details**\n *\n * All part and joint names from the sub-assembly are prefixed with\n * `"${options.prefix}."` to avoid collisions. After the merge, sub-assembly\n * joints are driven from the parent using the prefixed names:\n *\n * ```ts\n * parent.solve({ "Left Arm.shoulder": 45, "Right Arm.shoulder": -20 })\n * ```\n *\n * Joint couplings inside the sub-assembly are preserved and rewritten with\n * the prefix. Ports from sub-assembly parts are forwarded with the prefix.\n *\n * The sub-assembly must have exactly one root part. If it has multiple roots,\n * use `addFixed()` first to consolidate them before merging.\n *\n * **Example**\n *\n * ```ts\n * const robot = assembly("Robot").addPart("Chassis", chassis);\n *\n * require("./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 * @param parent - The target assembly to merge into\n * @param options - `prefix`, `mountParent`, `mountJoint`, `mountType`, `mountOptions`\n * @returns `parent` for chaining\n * @category Assembly\n */\n mergeInto(parent: Assembly, options: MergeIntoOptions): Assembly;\n}\ninterface RevoluteJointOpts {\n axis?: [\n number,\n number,\n number\n ];\n min?: number;\n max?: number;\n default?: number;\n unit?: string;\n reverse?: boolean;\n}\n/**\n * Create a revolute joint that auto-generates a parameter slider and rotates the shape.\n *\n * **Details**\n *\n * This is a convenience wrapper for single-shape, single-joint use cases. It calls\n * `param()` to create a named angle slider, then applies `rotateAroundAxis()` to the\n * shape. Use the full `Assembly` API for mechanisms with multiple parts and joints.\n *\n * **Example**\n *\n * ```ts\n * const arm = joint("Shoulder", armShape, [0, 0, 20], {\n * axis: [0, 1, 0],\n * min: -30, max: 120, default: 25,\n * });\n * return arm;\n * ```\n *\n * @param name - Display name for the generated angle parameter (shown as a UI slider)\n * @param shape - The shape to rotate\n * @param pivot - World-space pivot point `[x, y, z]`\n * @param opts - `axis`, `min`/`max` angles (degrees), `default` angle, `unit`, `reverse`\n * @returns The rotated shape at the current parameter value\n * @category Joints\n */\ndeclare function joint(name: string, shape: Shape, pivot: [\n number,\n number,\n number\n], opts?: RevoluteJointOpts): Shape;\ninterface BoundingRegion {\n xMin?: number;\n xMax?: number;\n yMin?: number;\n yMax?: number;\n zMin?: number;\n zMax?: number;\n}\n/**\n * Filter and sort criteria for edge selection.\n *\n * All filters are combined with AND logic — an edge must pass every\n * specified filter to be included. Omitting a filter means "any".\n *\n * @category Edge Queries\n */\ninterface EdgeQuery {\n /** Sort by proximity to this point (closest first). When used with `selectEdge`, picks the closest match. */\n near?: Vec3;\n /** Filter: edge direction approximately parallel to this vector. */\n parallel?: Vec3;\n /** Filter: edge direction approximately perpendicular to this vector. */\n perpendicular?: Vec3;\n /** Filter: only convex (outside corner) edges. */\n convex?: boolean;\n /** Filter: only concave (inside corner) edges. */\n concave?: boolean;\n /** Filter: minimum dihedral angle in degrees. */\n minAngle?: number;\n /** Filter: maximum dihedral angle in degrees. */\n maxAngle?: number;\n /** Filter: minimum edge length. */\n minLength?: number;\n /** Filter: maximum edge length. */\n maxLength?: number;\n /** Filter: edge midpoint must be within this bounding region. */\n within?: BoundingRegion;\n /** Shorthand: edge midpoint Z ≈ this value (within `tolerance`). Equivalent to `within: { zMin: atZ - tol, zMax: atZ + tol }`. */\n atZ?: number;\n /** Position tolerance for approximate matches (default: `1.0`). Used by `atZ` and `near`. */\n tolerance?: number;\n /** Angular tolerance in degrees for `parallel`/`perpendicular` filters (default: `10`). */\n angleTolerance?: number;\n}\n/**\n * Select all edges from a shape that match the given query.\n *\n * **Details**\n *\n * Extracts sharp edges from the mesh (dihedral angle > 1°), applies all\n * filters in the query, and returns the matching `EdgeSegment[]`. When `near`\n * is specified the results are sorted closest-first.\n *\n * Works on any shape — primitives, booleans, shells, and imported meshes.\n * Use this when tracked topology is unavailable (e.g. after a difference or\n * on imported geometry). For simpler cases, pass an `EdgeQuery` directly to\n * `fillet()` or `chamfer()` instead of calling `selectEdges` separately.\n *\n * **Example**\n *\n * ```ts\n * // Fillet all top edges of a box\n * const topEdges = selectEdges(part, { atZ: 20, perpendicular: [0, 0, 1] });\n * let result = part;\n * for (const edge of coalesceEdges(topEdges)) {\n * result = fillet(result, 2, edge);\n * }\n * ```\n *\n * @param shape - The solid to extract edges from\n * @param query - Optional filter/sort criteria\n * @returns Array of matching edge segments (may be empty)\n * @see {@link selectEdge} for the single-match variant\n * @see {@link coalesceEdges} to merge split tessellation segments\n * @category Edge Queries\n */\ndeclare function selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[];\n/**\n * Select the single best-matching edge from a shape.\n *\n * **Details**\n *\n * When `near` is specified, returns the edge whose midpoint is closest to\n * that point. Otherwise returns the first matching edge in mesh order.\n * Throws if no edges match the query — useful as a guard when you expect\n * exactly one result.\n *\n * **Example**\n *\n * ```ts\n * // Chamfer one specific edge near a known point\n * const bottomEdge = selectEdge(part, { near: [25, 0, 0], atZ: 0 });\n * result = chamfer(result, 1.5, bottomEdge);\n * ```\n *\n * @param shape - The solid to extract edges from\n * @param query - Filter/sort criteria; use `near` to pick the closest match\n * @returns The single best-matching edge segment\n * @throws If no edges match the query\n * @see {@link selectEdges} for multi-match variant\n * @category Edge Queries\n */\ndeclare function selectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment;\n/**\n * Merge collinear edge segments into longer logical edges.\n *\n * **Details**\n *\n * Tessellation often splits one geometric edge into multiple short segments.\n * `coalesceEdges` groups adjacent collinear segments and merges each group\n * into a single `EdgeSegment` spanning the full extent. This is usually\n * needed before passing edges to `fillet()` or `chamfer()` on non-primitive\n * shapes.\n *\n * The `tolerance` controls the maximum perpendicular distance from\n * collinearity before two segments are considered non-collinear. Default: `0.01`.\n *\n * **Example**\n *\n * ```ts\n * const topEdges = selectEdges(part, { atZ: 20 });\n * for (const edge of coalesceEdges(topEdges)) {\n * result = fillet(result, 2, edge);\n * }\n * ```\n *\n * @param segments - Edge segments from `selectEdges()`\n * @param tolerance - Max perpendicular deviation for collinearity (default: `0.01`)\n * @returns Merged edge segments\n * @see {@link selectEdges} to obtain the input segments\n * @category Edge Queries\n */\ndeclare function coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[];\ntype EdgeReferenceLike = {\n edges(): EdgeSegment[];\n};\ntype EdgeSelector = EdgeSegment | EdgeSegment[] | EdgeQuery | EdgeReferenceLike;\n/**\n * Apply fillets (rounded edges) to one or more edges of a shape.\n *\n * **Details**\n *\n * Works on both straight and curved edges. Supports OCCT and Manifold\n * backends. When using OCCT, all edges are filleted in a single kernel\n * operation for best quality. When using Manifold, edges are filleted\n * sequentially.\n *\n * The `edges` parameter is flexible:\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\n * pre-selected edges\n *\n * Throws if no edges match the selection, or if `radius` is not a positive\n * finite number.\n *\n * **Example**\n *\n * ```ts\n * // Fillet all edges\n * fillet(myShape, 2)\n *\n * // Fillet only top convex edges\n * fillet(myShape, 1.5, { atZ: 20, convex: true })\n *\n * // Fillet vertical edges selected beforehand\n * const edges = selectEdges(myShape, { parallel: [0, 0, 1] })\n * fillet(myShape, 3, edges)\n * ```\n *\n * @param shape - The solid to modify\n * @param radius - Fillet radius (must be positive and finite)\n * @param edges - Which edges to fillet: `EdgeSegment`, `EdgeSegment[]`, `EdgeQuery`, or `undefined` (all)\n * @param segments - Arc resolution for Manifold backend (default: `16`)\n * @returns A new Shape with the fillets applied\n * @see {@link chamfer} for beveled edges\n * @see {@link selectEdges} to pre-select edges\n * @see {@link filletTrackedEdge} for the compile-plan-based variant (tracked box edges)\n * @category Edge Features\n */\ndeclare function fillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape;\n/**\n * Apply chamfers (beveled edges) to one or more edges of a shape.\n *\n * **Details**\n *\n * Produces a 45° bevel at the specified `size` (distance from edge). Works on\n * both straight and curved edges. Supports OCCT and Manifold backends.\n *\n * The `edges` parameter accepts the same options as `fillet()`: inline\n * `EdgeQuery`, pre-selected `EdgeSegment`/`EdgeSegment[]`, or `undefined`\n * (all sharp edges).\n *\n * **Example**\n *\n * ```ts\n * // Chamfer all edges\n * chamfer(myShape, 1)\n *\n * // Chamfer only vertical edges\n * chamfer(myShape, 2, { parallel: [0, 0, 1] })\n * ```\n *\n * @param shape - The solid to modify\n * @param size - Chamfer size — distance from the edge (must be positive and finite)\n * @param edges - Which edges to chamfer: `EdgeSegment`, `EdgeSegment[]`, `EdgeQuery`, or `undefined` (all)\n * @returns A new Shape with the chamfers applied\n * @see {@link fillet} for rounded edges\n * @see {@link chamferTrackedEdge} for the compile-plan-based variant (tracked box edges)\n * @category Edge Features\n */\ndeclare function chamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape;\n/**\n * Apply a draft angle (taper) to vertical faces for mold extraction.\n *\n * **Details**\n *\n * Adds a taper angle to the vertical faces of a solid so that it can be\n * extracted from a mold. The neutral plane is the Z position where the draft\n * angle is zero — faces above and below are tapered symmetrically. Typical\n * values for injection molding are 1–5°.\n *\n * Requires the OCCT backend. Throws on Manifold.\n *\n * **Example**\n *\n * ```ts\n * // Add 3° draft to a box for injection molding\n * draft(myBox, 3)\n *\n * // Draft with custom pull direction and neutral plane\n * draft(myShape, 2, [0, 0, 1], 10)\n * ```\n *\n * @param shape - The solid to modify\n * @param angleDeg - Draft angle in degrees (typically 1–5 for injection molding)\n * @param pullDirection - Mold pull direction (default: `[0, 0, 1]` = Z-up)\n * @param neutralPlaneOffset - Z-offset of the neutral plane (default: `0`)\n * @returns A new Shape with draft applied\n * @category Edge Features\n */\ndeclare function draft(shape: Shape, angleDeg: number, pullDirection?: [\n number,\n number,\n number\n], neutralPlaneOffset?: number): Shape;\n/**\n * Uniformly offset all surfaces of a solid inward or outward.\n *\n * **Details**\n *\n * Unlike `shell()`, which hollows a solid by removing one face, `offsetSolid()`\n * produces a new solid whose every surface is shifted by `thickness`. Positive\n * values grow the shape outward; negative values shrink it inward.\n *\n * Requires the OCCT backend. Throws on Manifold.\n *\n * **Example**\n *\n * ```ts\n * // Grow a box outward by 1mm on all sides\n * offsetSolid(myBox, 1)\n *\n * // Shrink a shape inward by 0.5mm\n * offsetSolid(myShape, -0.5)\n * ```\n *\n * @param shape - The solid to offset\n * @param thickness - Offset distance (positive = outward, negative = inward; must be non-zero and finite)\n * @returns A new Shape with offset surfaces\n * @category Edge Features\n */\ndeclare function offsetSolid(shape: Shape, thickness: number): Shape;\ntype ToShapeTreeResult = Shape | ShapeGroup;\ninterface CombineOptions {\n op?: "union" | "intersection";\n}\n/**\n * Materialize one SDF leaf or all SDF leaves in a renderable tree.\n *\n * Raw `SdfShape` values become mesh-backed `Shape`s. Plain objects and arrays\n * preserve their renderable children as a `ShapeGroup` when more than one leaf\n * is found. Non-renderable metadata is ignored for materialization and remains\n * available to callers through normal `require()` return values.\n */\ndeclare function toShape(value: unknown, options?: SdfToShapeOptions): ToShapeTreeResult;\n/**\n * Collapse a tree of SDF leaves into one continuous SDF field.\n *\n * This intentionally discards per-leaf color/material identity because the\n * result is one scalar field. Use plain object returns for multi-material SDF\n * preview, and use `combine(...)` only when you want one implicit body.\n */\ndeclare function combine(value: unknown, options?: CombineOptions): SdfShape;\ntype MetricSize = "M2" | "M2.5" | "M3" | "M4" | "M5" | "M6" | "M8" | "M10";\ntype FastenerFit = "close" | "normal" | "loose" | "tap";\ninterface FastenerHoleOptions {\n /** Thread standard — only `\'iso-metric\'` is supported. */\n standard?: "iso-metric";\n /** ISO metric thread size, e.g. `\'M5\'`. Supported: M2–M10. */\n size: MetricSize;\n /**\n * Clearance fit class (default: `\'normal\'`).\n *\n * | Fit | Hole dia | Use when |\n * |-----|----------|----------|\n * | `\'close\'` | nominal + ~0.2 mm | Press-location or close-tolerance |\n * | `\'normal\'` | nominal + ~0.5 mm | Standard through-bolt clearance |\n * | `\'loose\'` | nominal + 1–2 mm | Adjustment slots or misaligned patterns |\n * | `\'tap\'` | ISO tap drill | Tapped hole in mating part |\n */\n fit?: FastenerFit;\n /** Hole depth in mm. */\n depth: number;\n /**\n * Optional counterbore at the top of the hole (wider recess for a socket-head cap).\n * `diameter` defaults to the ISO head-clearance diameter for the chosen size.\n */\n counterbore?: {\n depth: number;\n diameter?: number;\n };\n /**\n * Optional countersink at the top of the hole (conical recess for a flat-head screw).\n * `angleDeg` defaults to `90`.\n */\n countersink?: {\n diameter: number;\n angleDeg?: number;\n };\n /**\n * When `true` (default), the hole is centered on Z=0 (extends from −depth/2 to +depth/2).\n * When `false`, the top face is at Z=0 and the hole extends downward.\n */\n center?: boolean;\n /** Polygon segment count for the cylinder approximation (default: 48). */\n segments?: number;\n}\ninterface TSlotProfileOptions {\n /** Outer profile size (square). */\n size?: number;\n /** Slot mouth width (the narrow opening at each side). */\n slotWidth?: number;\n /** Wider interior slot cavity width. Must be >= slotWidth. */\n slotInnerWidth?: number;\n /** Total slot depth from outer face inward. */\n slotDepth?: number;\n /** Depth of the narrow mouth before it widens into slotInnerWidth. */\n slotNeckDepth?: number;\n /** Outer shell wall thickness. */\n wall?: number;\n /** Central cross-web thickness. */\n web?: number;\n /** Center boss diameter (solid material around center bore). */\n centerBossDia?: number;\n /** Center bore diameter (for tapping/through-hole). Set 0 to disable. */\n centerBoreDia?: number;\n /** Outer corner radius. */\n outerCornerRadius?: number;\n /** Segment count used for circular features in 2D. */\n segments?: number;\n}\ninterface TSlotExtrusionOptions extends TSlotProfileOptions {\n}\ninterface Profile2020BSlot6ProfileOptions {\n /** Slot mouth width. */\n slotWidth?: number;\n /** Wider inner slot width. */\n slotInnerWidth?: number;\n /** Slot depth from outer face. */\n slotDepth?: number;\n /** Depth of the narrow neck before widening into slotInnerWidth. */\n slotNeckDepth?: number;\n /** Center core bore diameter (set 0 to disable). */\n centerBoreDia?: number;\n /** Solid boss diameter around center bore (must exceed centerBoreDia when bore is enabled). */\n centerBossDia?: number;\n /** Width of diagonal ribs connecting center boss to corner regions. */\n diagonalWebWidth?: number;\n /** Outside corner radius. */\n outerCornerRadius?: number;\n /** Circle segment count. */\n segments?: number;\n}\ninterface Profile2020BSlot6Options extends Profile2020BSlot6ProfileOptions {\n}\ninterface ExplodeNamedItem {\n name: string;\n shape?: Shape | ShapeGroup;\n sketch?: Sketch;\n color?: string;\n group?: ExplodeItem[];\n explode?: ExplodeDirective;\n}\ntype ExplodeItem = Shape | Sketch | ShapeGroup | ExplodeNamedItem;\ninterface ExplodeOptions extends ExplodeConfigOptions {\n}\ntype BeltVec2 = [\n number,\n number\n];\ntype BeltMode = "open" | "crossed";\ninterface TangentCircle2D {\n name?: string;\n center: BeltVec2;\n radius: number;\n}\ninterface BeltPulley2D {\n name?: string;\n center: BeltVec2;\n pitchRadius: number;\n}\ninterface TangentLoop2DOptions {\n /** `open` uses external tangents; `crossed` uses internal tangents. */\n mode?: BeltMode;\n}\ninterface BeltDriveOptions {\n pulleys: BeltPulley2D[] | Record<string, Omit<BeltPulley2D, "name"> & {\n name?: string;\n }>;\n /** Belt width along +Z. */\n beltWidth: number;\n /** Belt thickness in the pulley plane. Default 2mm. */\n beltThickness?: number;\n /**\n * Reserved for multi-pulley route intent. The first implementation supports\n * two-pulley routes and rejects multi-pulley calls with explicit guidance.\n */\n route?: "outer" | BeltRouteContact[];\n /** Visual stroke width for the returned pitch path sketch. Default 0.25mm. */\n pitchPathWidth?: number;\n}\ninterface BeltRouteContact {\n pulley: string;\n wrap?: "cw" | "ccw" | "short" | "long";\n tangentIn?: "left" | "right" | "internal" | "external";\n tangentOut?: "left" | "right" | "internal" | "external";\n}\ninterface BeltLineSpan {\n kind: "line";\n fromPulley: string;\n toPulley: string;\n from: BeltVec2;\n to: BeltVec2;\n length: number;\n}\ninterface BeltWrapArc {\n kind: "arc";\n pulley: string;\n center: BeltVec2;\n pitchRadius: number;\n from: BeltVec2;\n to: BeltVec2;\n sweepDeg: number;\n wrapDeg: number;\n length: number;\n tangentIn: BeltVec2;\n tangentOut: BeltVec2;\n}\ntype BeltPathSegment = BeltLineSpan | BeltWrapArc;\ninterface BeltDriveResult {\n belt: Shape;\n beltProfile: Sketch;\n pitchPath: Sketch;\n route: TangentLoop2D;\n length: number;\n wraps: BeltWrapArc[];\n wrapByPulley: Record<string, BeltWrapArc>;\n straightSpans: BeltLineSpan[];\n skippedPulleys: string[];\n}\ndeclare class TangentLoop2D {\n readonly circles: TangentCircle2D[];\n readonly mode: BeltMode;\n readonly segments: BeltPathSegment[];\n readonly straightSpans: BeltLineSpan[];\n readonly wraps: BeltWrapArc[];\n readonly wrapByPulley: Record<string, BeltWrapArc>;\n readonly length: number;\n constructor(circles: TangentCircle2D[], options?: TangentLoop2DOptions);\n /** Convert the loop centerline into a thin visual sketch. */\n toSketch(width?: number): Sketch;\n /** Convert the loop into a filled profile using the pitch path itself as the boundary. */\n toProfile(): Sketch;\n /** Build a belt band sketch by offsetting the route to inner and outer pulley radii. */\n offsetBand(thickness: number): Sketch;\n}\ninterface SpurGearOptions {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n boreDiameter?: number;\n segmentsPerTooth?: number;\n}\ninterface SideGearOptions extends SpurGearOptions {\n side?: "top" | "bottom";\n toothHeight?: number;\n}\ninterface FaceGearOptions extends SideGearOptions {\n}\ninterface RingGearOptions {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n rimWidth?: number;\n outerDiameter?: number;\n segmentsPerTooth?: number;\n}\ninterface RackGearOptions {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n baseHeight?: number;\n}\ninterface BevelGearOptions {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n boreDiameter?: number;\n pitchAngleDeg?: number;\n mateTeeth?: number;\n shaftAngleDeg?: number;\n segmentsPerTooth?: number;\n}\ninterface GearPairSpec {\n module: number;\n teeth: number;\n pressureAngleDeg?: number;\n faceWidth?: number;\n backlash?: number;\n clearance?: number;\n addendum?: number;\n dedendum?: number;\n boreDiameter?: number;\n segmentsPerTooth?: number;\n}\ninterface GearPairOptions {\n pinion: Shape | GearPairSpec;\n gear: Shape | GearPairSpec;\n backlash?: number;\n centerDistance?: number;\n place?: boolean;\n phaseDeg?: number;\n}\ninterface GearPairDiagnostic {\n level: "info" | "warn" | "error";\n code: string;\n message: string;\n}\ninterface GearPairResult {\n pinion: Shape;\n gear: Shape;\n centerDistance: number;\n centerDistanceNominal: number;\n backlash: number;\n pressureAngleDeg: number;\n workingPressureAngleDeg: number;\n contactRatio: number;\n jointRatio: number;\n speedReduction: number;\n /** Phase rotation (degrees) for the gear around its shaft axis for correct tooth\n * mesh alignment. When `place: true` this is already baked into `gear`.\n * When `place: false`, rotate the gear by this amount before positioning. */\n phaseDeg: number;\n diagnostics: GearPairDiagnostic[];\n status: "ok" | "warn" | "error";\n}\ninterface GearMeshPlacement {\n pinionAxis: [\n number,\n number,\n number\n ];\n gearAxis: [\n number,\n number,\n number\n ];\n pinionCenter: [\n number,\n number,\n number\n ];\n gearCenter: [\n number,\n number,\n number\n ];\n}\ninterface BevelGearPairSpec extends GearPairSpec {\n}\ninterface BevelGearPairOptions {\n pinion: Shape | BevelGearPairSpec;\n gear: Shape | BevelGearPairSpec;\n shaftAngleDeg?: number;\n backlash?: number;\n place?: boolean;\n phaseDeg?: number;\n}\ninterface SideGearSpec extends GearPairSpec {\n side?: "top" | "bottom";\n toothHeight?: number;\n}\ninterface FaceGearSpec extends SideGearSpec {\n}\ninterface SideGearPairOptions {\n side: Shape | SideGearSpec;\n vertical: Shape | GearPairSpec;\n backlash?: number;\n centerDistance?: number;\n meshPlaneZ?: number;\n place?: boolean;\n phaseDeg?: number;\n}\ninterface BevelGearPairResult extends GearMeshPlacement {\n pinion: Shape;\n gear: Shape;\n shaftAngleDeg: number;\n pinionPitchAngleDeg: number;\n gearPitchAngleDeg: number;\n coneDistance: number;\n backlash: number;\n jointRatio: number;\n speedReduction: number;\n /** Phase rotation (degrees) for gear tooth mesh alignment. See GearPairResult.phaseDeg. */\n phaseDeg: number;\n diagnostics: GearPairDiagnostic[];\n status: "ok" | "warn" | "error";\n}\ninterface SideGearPairResult {\n side: Shape;\n vertical: Shape;\n centerDistance: number;\n centerDistanceNominal: number;\n backlash: number;\n pressureAngleDeg: number;\n meshPlaneZ: number;\n radialOverlap: number;\n jointRatio: number;\n speedReduction: number;\n /** Phase rotation (degrees) for the vertical gear. See GearPairResult.phaseDeg. */\n phaseDeg: number;\n diagnostics: GearPairDiagnostic[];\n status: "ok" | "warn" | "error";\n}\ninterface FaceGearPairOptions {\n face: Shape | FaceGearSpec;\n vertical: Shape | GearPairSpec;\n backlash?: number;\n centerDistance?: number;\n meshPlaneZ?: number;\n place?: boolean;\n phaseDeg?: number;\n}\ninterface FaceGearPairResult {\n face: Shape;\n vertical: Shape;\n centerDistance: number;\n centerDistanceNominal: number;\n backlash: number;\n pressureAngleDeg: number;\n meshPlaneZ: number;\n radialOverlap: number;\n jointRatio: number;\n speedReduction: number;\n /** Phase rotation (degrees) for the vertical gear. See GearPairResult.phaseDeg. */\n phaseDeg: number;\n diagnostics: GearPairDiagnostic[];\n status: "ok" | "warn" | "error";\n}\ntype Vec2$1 = [\n number,\n number\n];\ninterface BoltPatternOptions {\n /** Metric bolt size (\'M2\'–\'M10\'). */\n size: MetricSize;\n /** Clearance class. Default: \'normal\'. */\n fit?: FastenerFit;\n /** List of [x, y] positions in the pattern\'s 2D coordinate frame. */\n positions: Vec2$1[];\n /** Number of circle segments per hole. Default: 48. */\n segments?: number;\n}\ninterface BoltPatternCutOptions {\n /**\n * Z position of the top of the cutters (before punching through). For a cut\n * from z=0 downward through a 20mm plate, pass `from: -1` and `depth: 22` to\n * ensure the cutter fully punches through with some margin.\n */\n from?: number;\n /** Optional counterbore. */\n counterbore?: {\n depth: number;\n diameter?: number;\n };\n /** Optional countersink. */\n countersink?: {\n diameter: number;\n angleDeg?: number;\n };\n}\ninterface BoltPattern {\n /** Bolt size (\'M5\' etc.). */\n readonly size: MetricSize;\n /** Clearance diameter in mm, derived from size + fit. */\n readonly dia: number;\n /** Positions in 2D (XY of the plane being cut). */\n readonly positions: ReadonlyArray<Vec2$1>;\n /** Min X across all positions — useful for housing sizing. */\n readonly minX: number;\n /** Max X across all positions. */\n readonly maxX: number;\n /** Min Y across all positions. */\n readonly minY: number;\n /** Max Y across all positions. */\n readonly maxY: number;\n /**\n * Subtract the pattern from a shape. Cuts one ISO-clearance hole at each position.\n *\n * @param shape - Shape to cut.\n * @param depth - Depth of the cut along Z.\n * @param options - Optional counterbore/countersink and Z offset.\n */\n cut(shape: Shape, depth: number, options?: BoltPatternCutOptions): Shape;\n}\ntype WasherStandard = "din-125-a";\ninterface FastenerSetDimensions {\n size: MetricSize;\n nominalDiameter: number;\n boltLength: number;\n clearanceDia: number;\n tapDia: number;\n nutAcrossFlats: number;\n nutHeight: number;\n washerOuterDia: number;\n washerInnerDia: number;\n washerThickness: number;\n}\ninterface FastenerSetOptions {\n /** Include a DIN 125-A washer under the bolt head (default: `true`). */\n washerUnderHead?: boolean;\n /** Include a DIN 125-A washer under the nut (default: `true`). */\n washerUnderNut?: boolean;\n /**\n * Clearance fit class for the through-hole cutter (default: `\'normal\'`).\n *\n * | Fit | Hole dia | Use when |\n * |-----|----------|----------|\n * | `\'close\'` | nominal + ~0.2 mm | Press-location or close-tolerance |\n * | `\'normal\'` | nominal + ~0.5 mm | Standard through-bolt clearance |\n * | `\'loose\'` | nominal + 1–2 mm | Adjustment slots or misaligned patterns |\n * | `\'tap\'` | ISO tap drill | Tapped hole in mating part |\n */\n fit?: FastenerFit;\n /** Polygon segment count for all circular geometry (default: 36). */\n segments?: number;\n}\ninterface FastenerSetResult {\n /** Hex bolt: head top at z=0, threaded shaft extends toward −Z by `boltLength`. */\n bolt: Shape;\n /** Hex nut centered at z=0. */\n nut: Shape;\n /** Flat washer centered at z=0. Null when washerUnderHead is false. */\n washerUnderHead: Shape | null;\n /** Flat washer centered at z=0. Null when washerUnderNut is false. */\n washerUnderNut: Shape | null;\n /** Clearance-hole cutter (cylinder) centered at z=0, for subtracting from a through-plate. */\n clearanceHole: Shape;\n /** Tap-drill cutter (cylinder) centered at z=0, for subtracting from a tapped plate. */\n tappedHole: Shape;\n /** Reference dimensions for BOM, placement calculations, and documentation. */\n dims: FastenerSetDimensions;\n}\n/**\n * Pre-built parametric parts available in user scripts as `lib.*`.\n *\n * **Details**\n *\n * Every key in this object becomes a method on the `lib` namespace exposed to\n * `.forge.js` scripts. The catalog includes:\n *\n * **Fasteners:** `bolt`, `nut`, `washer`, `fastenerSet`, `fastenerHole`,\n * `boltHole`, `counterbore`, `hexNut`, `holePattern`\n *\n * **Structure:** `tube`, `pipe`, `bracket`, `pipeRoute`, `elbow`,\n * `tSlotProfile`, `tSlotExtrusion`, `profile2020BSlot6Profile`, `profile2020BSlot6`\n *\n * **Belt drives:** `beltDrive`, `tangentLoop2d`\n *\n * **Threads:** `thread`\n *\n * **Gears:** `spurGear`, `bevelGear`, `faceGear`, `sideGear`, `ringGear`,\n * `rackGear`, `gearPair`, `bevelGearPair`, `faceGearPair`, `sideGearPair`\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 *\n * Extend this by adding new entries here and registering the corresponding\n * runner binding in `runner.ts`. Sizes outside the supported ranges will throw\n * at runtime with a descriptive error.\n *\n * @category Part Library\n */\ndeclare const partLibrary: {\n/**\n * Simple cylindrical through-hole cutter centered on Z=0.\n *\n * Subtract the result from a part to produce a plain cylindrical clearance hole.\n * For ISO metric sizes with fit classes and counterbore/countersink, use\n * {@link fastenerHole} instead.\n *\n * @param diameter - Hole diameter in mm.\n * @param depth - Hole depth in mm.\n * @returns A cylinder shape to be subtracted from a solid.\n * @category Fasteners\n */\n\n boltHole(diameter: number, depth: number): Shape;\n/**\n * ISO metric fastener hole cutter with optional counterbore or countersink.\n *\n * **Details**\n *\n * Returns a cutter shape (subtract from a solid to produce the hole). Sizes\n * outside M2–M10 will throw. Extend `METRIC_HOLE_TABLE` in this file to add\n * new sizes.\n *\n * **Example**\n *\n * ```ts\n * const plate = box(60, 40, 8)\n * .subtract(lib.fastenerHole({ size: \'M5\', fit: \'normal\', depth: 8 })\n * .translate(15, 10, 4));\n * ```\n *\n * @param opts - Hole configuration including size, fit class, depth, and optional recesses.\n * @returns A cutter shape centered on Z=0 (or positioned at Z=0 top when `center: false`).\n * @category Fasteners\n */\n\n fastenerHole(opts: FastenerHoleOptions): Shape;\n/**\n * Counterbore hole cutter — through-hole with a wider cylindrical recess at the top.\n *\n * Use for socket-head cap screws that must sit flush. Subtract from a solid.\n * For ISO metric sizing and fit classes, prefer {@link fastenerHole} with `counterbore`.\n *\n * @param holeDia - Clearance hole diameter in mm.\n * @param boreDia - Counterbore diameter in mm (must be ≥ holeDia).\n * @param boreDepth - Counterbore depth in mm.\n * @param totalDepth - Total through-hole depth in mm.\n * @returns A cutter shape centered on Z=0.\n * @category Fasteners\n */\n\n counterbore(holeDia: number, boreDia: number, boreDepth: number, totalDepth: number): Shape;\n/**\n * Rectangular hollow tube (thin-wall box section).\n *\n * Both the outer and inner boxes are centered on the XY plane with their base at Z=0.\n *\n * @param outerX - Outer width in mm.\n * @param outerY - Outer depth in mm.\n * @param outerZ - Length (height) of the tube in mm.\n * @param wall - Wall thickness in mm (removed uniformly from all four sides).\n * @returns A hollow rectangular tube solid.\n * @category Fasteners\n */\n\n tube(outerX: number, outerY: number, outerZ: number, wall: number): Shape;\n/**\n * Hollow cylindrical pipe.\n *\n * Centered on the XY plane, extending upward along +Z from z=0 to z=height.\n * For complex routed pipe geometry, see `lib.pipeRoute`.\n *\n * @param height - Pipe length in mm.\n * @param outerRadius - Outer radius in mm.\n * @param wall - Wall thickness in mm.\n * @param segments - Polygon approximation segments (default: 32).\n * @returns A hollow cylinder solid.\n * @category Fasteners\n */\n\n pipe(height: number, outerRadius: number, wall: number, segments?: number): Shape;\n/**\n * Apply deterministic exploded-view offsets to an assembly tree.\n *\n * **Details**\n *\n * Traverses arrays of shapes/sketches/named items, nested `{ name, group: [...] }` structures,\n * and `ShapeGroup` outputs, translating each node by a computed offset while preserving names,\n * colors, and nesting. Returns the same structure type as the input.\n *\n * In `radial` mode the algorithm is branch-aware and parent-relative: each node fans out from\n * its immediate parent\'s center, so nested assemblies peel apart level by level. Named items may\n * also include an inline `explode: { stage?, direction?, axisLock? }` property to override\n * per-item behavior.\n *\n * Use this function when you want to bake the explode offset into the geometry before returning\n * (e.g. to drive the amount with a `param()` slider). For a viewport-only explode slider without\n * rerunning the script, use `explodeView()` instead.\n *\n * **Example**\n *\n * ```js\n * const explodeAmt = param(\'Explode\', 0, { min: 0, max: 40, unit: \'mm\' });\n *\n * return lib.explode(assembly, {\n * amount: explodeAmt,\n * stages: [0.4, 0.8],\n * mode: \'radial\',\n * byName: { Shaft: { direction: [1, 0, 0], stage: 1.4 } },\n * });\n * ```\n *\n * @param items - Array of `Shape | Sketch | ShapeGroup | { name, shape?, sketch?, group?, explode? }`, or a `ShapeGroup`\n * @param options.amount - Total explode distance in model units\n * @param options.stages - Per-depth stage multipliers (depth 1 = first level). Default: reciprocal depth (`1, 1/2, 1/3, ...`)\n * @param options.mode - Direction mode: `\'radial\'` | `\'x\'` | `\'y\'` | `\'z\'` | `[x, y, z]`. Default: `\'radial\'`\n * @param options.axisLock - Constrain motion to a world axis: `\'x\'` | `\'y\'` | `\'z\'`\n * @param options.byName - Per-object overrides keyed by name: `{ stage?, direction?, axisLock? }`\n * @param options.byPath - Per-tree-path overrides using slash-separated path segments\n * @returns Same structure type as input, with translated geometry\n * @see {@link explodeView} for viewport-slider-driven explode without rerunning the script\n * @category Explode View\n */\n\n explode<T extends ExplodeItem[] | ShapeGroup>(items: T, options?: ExplodeOptions): T;\n/**\n * Generic hex nut with a cylindrical bore.\n *\n * Constructed via intersection of three rotated rectangular slabs, then a bore\n * is subtracted. Centered at origin, height along Z.\n *\n * For standard ISO metric nuts by thread size, use `lib.nut` instead.\n *\n * @param acrossFlats - Distance across flats in mm.\n * @param height - Nut height (thickness) in mm.\n * @param holeDia - Inner bore diameter in mm.\n * @returns A hexagonal nut solid centered at origin.\n * @category Fasteners\n */\n\n hexNut(acrossFlats: number, height: number, holeDia: number): Shape;\n/**\n * L-shaped mounting bracket with optional through-holes.\n *\n * Produces a right-angle bracket: a horizontal base plate and a vertical wall.\n * Both legs share `width`. Optional holes are drilled through the base (along Z)\n * and the wall (along Y).\n *\n * @param width - Width of the bracket (both legs share this dimension) in mm.\n * @param height - Wall leg height in mm.\n * @param depth - Base leg depth in mm.\n * @param thick - Material thickness in mm (both legs).\n * @param holeDia - Hole diameter in mm. Pass `0` (default) to omit holes.\n * @returns An L-bracket solid with base at Z=0.\n * @category Fasteners\n */\n\n bracket(width: number, height: number, depth: number, thick: number, holeDia?: number): Shape;\n/**\n * Rectangular grid of cylindrical hole cutters.\n *\n * Returns the union of `rows × cols` cylinders laid out on a regular grid.\n * Subtract from a solid to produce the full pattern.\n *\n * **Example**\n *\n * ```ts\n * const pattern = lib.holePattern(3, 4, 20, 20, 4, 10);\n * const panel = box(80, 70, 10).subtract(pattern.translate(-30, -20, 0));\n * ```\n *\n * @param rows - Number of rows in the grid.\n * @param cols - Number of columns in the grid.\n * @param spacingX - Column pitch in mm.\n * @param spacingY - Row pitch in mm.\n * @param holeDia - Hole diameter in mm.\n * @param depth - Hole depth in mm.\n * @returns Union of cutter cylinders, origin at the first hole center.\n * @category Fasteners\n */\n\n holePattern(rows: number, cols: number, spacingX: number, spacingY: number, holeDia: number, depth: number): Shape;\n/**\n * External helical thread — clean mesh, no SDF grid artifacts.\n *\n * **Details**\n *\n * Builds a cross-section with a single trapezoidal tooth from the root radius\n * out to the crest radius, then twist-extrudes it so the tooth traces a helix.\n * Manifold\'s extrude+twist produces structured quad-based geometry that follows\n * the thread profile cleanly.\n *\n * Returns a threaded cylinder along +Z from z=0 to z=length.\n *\n * **Example**\n *\n * ```ts\n * const t = lib.thread(5, 0.8, 12); // M5 × 0.8 pitch, 12 mm long\n * ```\n *\n * @param diameter - Nominal (crest) diameter in mm.\n * @param pitch - Thread pitch in mm (axial distance between crests).\n * @param length - Thread length in mm.\n * @param options - Optional overrides: `depth` (tooth height, default 0.35×pitch), `segments` (default 36).\n * @returns A threaded cylinder solid.\n * @category Fasteners\n */\n\n thread(diameter: number, pitch: number, length: number, options?: {\n depth?: number;\n segments?: number;\n}): Shape;\n/**\n * ISO-style hex bolt with real helical threads.\n *\n * **Details**\n *\n * The hex head sits from z=0 up to z=headHeight. The shaft extends downward\n * along −Z by `length` mm. An unthreaded shank section is included when\n * `threadLength < length`.\n *\n * Default proportions follow ISO 4762 loosely: pitch ≈ 0.15×diameter,\n * head height ≈ 0.65×diameter, across-flats ≈ 1.6×diameter.\n *\n * For standard M-size bolts pre-configured for a complete joint, use\n * {@link fastenerSet} instead.\n *\n * **Example**\n *\n * ```ts\n * const b = lib.bolt(5, 20); // M5 × 20 mm\n * ```\n *\n * @param diameter - Nominal shaft diameter in mm.\n * @param length - Shaft length in mm (head height not included).\n * @param options - Optional overrides for pitch, head height, across-flats, thread length, and segments.\n * @returns A hex bolt solid with head top at z=0.\n * @category Fasteners\n */\n\n bolt(diameter: number, length: number, options?: {\n pitch?: number;\n headHeight?: number;\n headAcrossFlats?: number;\n threadLength?: number;\n segments?: number;\n}): Shape;\n/**\n * ISO-style hex nut with a threaded bore.\n *\n * **Details**\n *\n * Constructed from the intersection of three rotated slabs with a cylindrical\n * bore subtracted. The nut is centered at the origin, height along Z.\n *\n * Default proportions follow ISO 4032 loosely: height ≈ 0.8×diameter,\n * across-flats ≈ 1.6×diameter. The bore is a clearance bore (not modelled\n * with helical threads) for rendering efficiency.\n *\n * For standard M-size nuts pre-configured for a complete joint, use\n * {@link fastenerSet} instead.\n *\n * **Example**\n *\n * ```ts\n * const n = lib.nut(5); // M5 nut\n * ```\n *\n * @param diameter - Nominal thread diameter in mm.\n * @param options - Optional overrides for pitch, height, across-flats, and segments.\n * @returns A hex nut solid centered at origin.\n * @category Fasteners\n */\n\n nut(diameter: number, options?: {\n pitch?: number;\n height?: number;\n acrossFlats?: number;\n segments?: number;\n}): Shape;\n/**\n * ISO metric flat washer (DIN 125-A).\n *\n * **Details**\n *\n * Returns a flat ring centered at the origin, thickness along Z. Dimensions\n * are taken from {@link WASHER_TABLE}. Sizes outside M2–M10 will throw.\n *\n * **Example**\n *\n * ```ts\n * const w = lib.washer(\'M5\'); // DIN 125-A M5 washer\n * ```\n *\n * @param size - ISO metric thread size, e.g. `\'M5\'`. Supported: M2–M10.\n * @param options - Optional `standard` (only `\'din-125-a\'` supported) and `segments` (default 48).\n * @returns A flat washer ring solid centered at origin.\n * @category Fasteners\n */\n\n washer(size: MetricSize, options?: {\n standard?: WasherStandard;\n segments?: number;\n}): Shape;\n/**\n * Complete ISO metric fastener set — bolt, nut, optional washers, and matching hole cutters.\n *\n * **Details**\n *\n * Returns all geometry for one bolted joint: the bolt, nut, up to two washers,\n * a clearance-hole cutter, and a tap-drill cutter. All shapes are returned\n * **un-positioned** (each on the Z-axis). Place them with `.translate()`.\n *\n * Sizes outside M4–M10 are supported for the washer (M2–M10); unsupported\n * combinations will throw.\n *\n * **Example**\n *\n * ```ts\n * const hw = lib.fastenerSet(\'M5\', 20);\n *\n * const topPlate = box(60, 40, 8).translate(0, 0, 12)\n * .subtract(hw.clearanceHole.translate(15, 10, 12));\n * const botPlate = box(60, 40, 8)\n * .subtract(hw.clearanceHole.translate(15, 10, 0));\n *\n * return [\n * { name: \'Top Plate\', shape: topPlate },\n * { name: \'Bot Plate\', shape: botPlate },\n * { name: \'Bolt\', shape: hw.bolt.translate(15, 10, 20) },\n * { name: \'Nut\', shape: hw.nut.translate(15, 10, -4) },\n * ];\n * ```\n *\n * @param size - ISO metric thread size, e.g. `\'M5\'`. Supported: M4–M10 (bolt/nut), M2–M10 (washer).\n * @param boltLength - Nominal shaft length in mm (head height not included). Must be > 0.\n * @param options - Washer inclusion, fit class, and segment count.\n * @returns A {@link FastenerSetResult} with all joint shapes and reference dimensions.\n * @category Fasteners\n */\n\n fastenerSet(size: MetricSize, boltLength: number, options?: FastenerSetOptions): FastenerSetResult;\n/**\n * Route a pipe (solid or hollow) through 3D waypoints with smooth bends.\n *\n * Each interior waypoint gets a torus-section bend. Straight segments connect them.\n * Returns a single unioned Shape.\n */\n\n pipeRoute(points: [\n number,\n number,\n number\n][], radius: number, options?: {\n bendRadius?: number;\n wall?: number;\n segments?: number;\n}): Shape;\n/**\n * Pipe elbow — a curved pipe section (torus arc) for connecting two pipe directions.\n *\n * By default creates a bend in the XZ plane: incoming along +Z, outgoing rotated by `angle`.\n * The bend starts at the origin, curving away from it.\n *\n * @param pipeRadius - Pipe outer radius\n * @param bendRadius - Centerline bend radius (distance from arc center to pipe center)\n * @param angle - Bend angle in degrees (e.g. 90 for a right-angle bend)\n * @param options.wall - Wall thickness for hollow pipe\n * @param options.segments - Circumferential segments (default 32)\n * @param options.from - Incoming direction vector (default [0,0,1])\n * @param options.to - Outgoing direction vector (overrides angle if both from/to given)\n */\n\n elbow(pipeRadius: number, bendRadius: number, angle?: number | {\n from?: [\n number,\n number,\n number\n ];\n to?: [\n number,\n number,\n number\n ];\n wall?: number;\n segments?: number;\n}, options?: {\n wall?: number;\n segments?: number;\n from?: [\n number,\n number,\n number\n ];\n to?: [\n number,\n number,\n number\n ];\n}): Shape;\n/**\n * Create a flat open-belt body around two pulley pitch circles.\n *\n * The belt is generated as a tangent loop in the XY plane and extruded along\n * +Z by `beltWidth`. The result includes the solid belt, the 2D belt profile,\n * a thin pitch-path sketch for visualization, total belt length, tangent spans,\n * and wrap metadata for each pulley.\n *\n * For more than two pulleys, the API intentionally asks for route intent before\n * geometry is created. Use `route: "outer"` for the future outside-envelope\n * mode, or an ordered route for future serpentine/idler layouts.\n *\n * ```ts\n * const drive = lib.beltDrive({\n * pulleys: [\n * { name: "motor", center: [0, 0], pitchRadius: 12 },\n * { name: "output", center: [80, 0], pitchRadius: 28 },\n * ],\n * beltWidth: 8,\n * beltThickness: 2,\n * });\n * return drive.belt;\n * ```\n *\n * @category Belt Drives\n */\n\n beltDrive(options: BeltDriveOptions): BeltDriveResult;\n/**\n * Build a closed 2D route made from common tangent spans and pulley wrap arcs.\n *\n * Use this when you need reusable belt/chain route geometry before creating a\n * solid body. The first implementation supports two circles. `mode: "open"`\n * uses external tangents; `mode: "crossed"` uses internal tangents.\n *\n * ```ts\n * const route = lib.tangentLoop2d([\n * { center: [0, 0], radius: 12 },\n * { center: [80, 0], radius: 28 },\n * ]);\n * const belt = route.offsetBand(2).extrude(8);\n * ```\n *\n * @category Belt Drives\n */\n\n tangentLoop2d(circles: TangentCircle2D[], options?: TangentLoop2DOptions): TangentLoop2D;\n/**\n * Build a 2D T-slot cross-section sketch.\n *\n * Default parameters describe a 20x20 B-type profile with slot 6.\n * Use this when you want a drawing-ready profile sketch before extrusion.\n */\n\n tSlotProfile(options?: TSlotProfileOptions): Sketch;\n/**\n * Build a T-slot extrusion from the generated 2D profile.\n * Extrudes along +Z by default.\n */\n\n tSlotExtrusion(length: number, options?: TSlotExtrusionOptions): Shape;\n/**\n * Accurate-ish 2D profile for 20x20 B-type slot 6.\n *\n * Returns a drawing-ready Sketch centered at origin.\n */\n\n profile2020BSlot6Profile(options?: Profile2020BSlot6ProfileOptions): Sketch;\n/**\n * 20x20 B-type slot 6 extrusion with profile-accurate defaults.\n *\n * Pass option overrides if your supplier\'s profile differs slightly.\n */\n\n profile2020BSlot6(length: number, options?: Profile2020BSlot6Options): Shape;\n/**\n * Involute external spur gear with optional center bore.\n *\n * Specify module, teeth, faceWidth as required parameters. Optional tuning includes\n * pressureAngleDeg (default 20), backlash, clearance, addendum, dedendum, boreDiameter,\n * and segmentsPerTooth (default 10).\n *\n * **Connectors (for assembly-based positioning):**\n * - `bore`: revolute connector at the bore center, axis along +Z. Carries\n * measurements: `{ module, teeth, pitchRadius, outerRadius, faceWidth }`.\n *\n * Use `.connect("Housing.seat", "Gear.bore")` to mount a gear on a shaft seat.\n */\n\n spurGear(options: SpurGearOptions): Shape;\n/**\n * Conical bevel gear generated from a tapered involute extrusion. Specify pitchAngleDeg directly or derive it from mateTeeth + shaftAngleDeg.\n *\n * **Connectors (for assembly-based positioning):**\n * - `bore`: revolute connector at the large-end bore center (Z=0), axis along -Z (away from teeth).\n * - `apex`: connector at the cone apex above the gear (the point where the pitch cone converges),\n * axis along +Z. Useful for meshing two bevel gears — their apices should coincide.\n *\n * Carries measurements: `{ module, teeth, pitchRadius, pitchAngleDeg, coneDistance, faceWidth }`.\n */\n\n bevelGear(options: BevelGearOptions): Shape;\n/**\n * Face gear (crown style) where teeth are on one face (top or bottom) instead of the outer rim.\n *\n * Uses the same involute tooth sizing as spurGear, then projects the tooth band axially from one side.\n * Alias for sideGear (which is kept for backward compatibility).\n */\n\n faceGear(options: FaceGearOptions): Shape;\n/**\n * Crown/face style gear where the teeth project from one side of the disk\n * instead of the outer cylindrical rim.\n */\n\n sideGear(options: SideGearOptions): Shape;\n/**\n * Internal ring gear with involute-derived tooth spaces. Specify rimWidth or outerDiameter for the annular body.\n *\n * **Connectors (for assembly-based positioning):**\n * - `bore`: connector at the ring center, axis along +Z. For planetary gearboxes, this is\n * where the ring mounts to the housing. Carries measurements:\n * `{ module, teeth, pitchRadius, innerRadius, outerRadius, faceWidth }`.\n */\n\n ringGear(options: RingGearOptions): Shape;\n/**\n * Linear rack gear with pressure-angle flanks. Use with spurGear for rack-and-pinion mechanisms.\n *\n * **Orientation:** teeth run along the X axis with tooth tips pointing +Y (pitch line at Y=0).\n * The rack is extruded +Z by `faceWidth`. Rotate the rack to align with a different slide axis.\n *\n * **Connectors (for assembly-based positioning):**\n * - `teeth`: prismatic connector at the pitch line center, axis along +X (slide direction).\n * Carries measurements: `{ module, teeth, faceWidth, length }`.\n *\n * Connect to a housing\'s rack channel:\n * ```js\n * housing.withConnectors({\n * rack_channel: connector("rack-channel", {\n * origin: [pitchR, 0, channelZ], axis: [1, 0, 0], kind: "prismatic",\n * }),\n * });\n * assembly.connect("Housing.rack_channel", "Rack.teeth", { as: "slide" });\n * ```\n */\n\n rackGear(options: RackGearOptions): Shape;\n/**\n * Build or validate a spur-gear pair and return ratio, backlash, and mesh diagnostics.\n *\n * Accepts either shapes from spurGear() or analytical specs for each member.\n * When place is true (default), the gear is auto-positioned at the correct center distance.\n */\n\n gearPair(options: GearPairOptions): GearPairResult;\n/** Build or validate a bevel-gear pair and return ratio diagnostics plus recommended joint placement vectors. */\n\n bevelGearPair(options: BevelGearPairOptions): BevelGearPairResult;\n/** Build or validate a perpendicular pair between a face gear and a vertical spur gear. */\n\n faceGearPair(options: FaceGearPairOptions): FaceGearPairResult;\n/**\n * Pair helper for side (crown/face) gear + perpendicular "vertical" spur gear.\n * Auto-placement rotates the spur around +Y and positions it to mesh at the side tooth band.\n */\n\n sideGearPair(options: SideGearPairOptions): SideGearPairResult;\n/**\n * Coupling ratio between two meshed spur gears.\n *\n * When gear A turns 1°, gear B turns `-teethA / teethB` degrees (negative because\n * meshed external gears rotate in opposite directions).\n *\n * @example\n * ```js\n * assembly.addJointCoupling("B_spin", {\n * terms: [{ joint: "A_spin", ratio: lib.gearRatio(12, 24) }], // -0.5\n * });\n * ```\n *\n * Pass `{ internal: true }` for internal gear pairs (ring gear + spur/planet),\n * where the two rotate in the same direction.\n */\n\n gearRatio(teethA: number, teethB: number, options?: {\n internal?: boolean;\n}): number;\n/**\n * Coupling ratio between a pinion and a rack.\n *\n * When the pinion rotates by `θ` degrees, the rack slides by `θ × (π × module × teeth / 360)` mm.\n * Equivalently, 1mm of rack travel = `180 / (π × pitchRadius)` degrees of pinion rotation.\n *\n * @example\n * ```js\n * // Pinion spin driven by rack slide:\n * assembly.addJointCoupling("pinion_spin", {\n * terms: [{ joint: "rack_slide", ratio: lib.rackRatio(1.5, 12) }], // ~6.37 deg/mm\n * });\n * ```\n */\n\n rackRatio(module: number, pinionTeeth: number): number;\n/**\n * Planetary gear reduction ratio when the ring is held fixed.\n *\n * Input: sun. Output: carrier. Ratio: `1 + ringTeeth / sunTeeth`.\n * One turn of the sun produces `1 / ratio` turns of the carrier.\n */\n\n planetaryRatio(sunTeeth: number, ringTeeth: number): number;\n/**\n * Define a bolt pattern once and cut it from multiple parts.\n *\n * @example\n * ```js\n * const bolts = lib.boltPattern({\n * size: \'M5\',\n * positions: [[20, 15], [-20, 15], [20, -15], [-20, -15]],\n * });\n *\n * const base = bolts.cut(box(60, 50, 10), 12, { from: -1 });\n * const cover = bolts.cut(box(60, 50, 3), 5, { from: -1 });\n * // Same positions in both parts — guaranteed aligned.\n * ```\n *\n * @category Part Library\n */\n\n boltPattern(options: BoltPatternOptions): BoltPattern;\n};\ninterface ProductMaterial {\n color?: string;\n material?: ShapeMaterialProps;\n}\ninterface ClearMaterialOptions {\n tint?: string;\n opacity?: number;\n}\ninterface ColorMaterialOptions {\n color?: string;\n}\ntype ProductProfileKind = "oval" | "roundedRect" | "circle" | "superEllipse" | "custom";\ninterface ProductProfileDescriptor {\n sketch: Sketch;\n width: number;\n depth: number;\n kind: ProductProfileKind;\n radius?: number;\n}\ninterface ProductProfileOptions {\n segments?: number;\n}\ninterface ProductSuperEllipseOptions extends ProductProfileOptions {\n /** Higher values produce squarer product surfaces; 2 is an ellipse. */\n exponent?: number;\n}\ntype ProductRailKind = "bezier" | "nurbs" | "polyline";\ninterface ProductRailSpec {\n kind: ProductRailKind;\n points: Vec3[];\n degree?: number;\n name?: string;\n}\ntype ProductScenePreset = "product" | "service" | "mechanical";\ninterface ProductStationProfile {\n sketch: Sketch;\n width: number;\n depth: number;\n kind: ProductProfileKind;\n radius?: number;\n exponent?: number;\n}\ninterface ProductStationSpec {\n name: string;\n center: Vec3;\n profile: ProductStationProfile;\n crown?: number;\n}\ninterface ProductStationSuperEllipseOptions {\n segments?: number;\n exponent?: number;\n}\ndeclare class ProductStationBuilder {\n readonly name: string;\n private centerValue;\n private profileValue?;\n private crownValue;\n constructor(name: string);\n /** Position this station in world coordinates. */\n at(point: Vec3): this;\n /** Convenience for traditional Z-up section stacks. */\n z(z: number): this;\n /** Convenience for product bodies running front-to-back along Y. */\n y(y: number): this;\n /** Convenience for product bodies running left-to-right along X. */\n x(x: number): this;\n /** Use an oval cross-section with full width and depth dimensions. */\n oval(width: number, depth: number, options?: {\n segments?: number;\n }): this;\n /** Use a superellipse cross-section for soft-square product surfaces. */\n superEllipse(width: number, depth: number, options?: ProductStationSuperEllipseOptions): this;\n /** Use a rounded-rectangle cross-section with the given corner radius. */\n roundedRect(width: number, depth: number, radius: number): this;\n /** Use a circular cross-section from a full diameter. */\n circle(diameter: number, options?: {\n segments?: number;\n }): this;\n /** Use a custom 2D sketch as the station cross-section. */\n custom(sketch: Sketch, width: number, depth: number): this;\n /** Stores a semantic crown amount for diagnostics and future rail solving. */\n crown(amount: number): this;\n /** Return the immutable station spec consumed by Product.skin(). */\n toSpec(): ProductStationSpec;\n}\n/** Primary world axis used to order ProductSkin loft stations. */\ntype ProductSkinAxis = "X" | "Y" | "Z";\n/** Semantic side of a ProductSkin. `back` is accepted as an alias for `rear`. */\ntype ProductSkinSide = "left" | "right" | "top" | "bottom" | "front" | "rear" | "back";\n/** Reported lowering mode for ProductSkin and conformal feature diagnostics. */\ntype ProductSkinRepresentation = "exact" | "sampled" | "mixed" | "fallback";\ninterface ProductSkinRefQuery {\n /** Side of the product skin. `front` is the minimum axis cap, `rear`/`back` is the maximum axis cap. */\n side: ProductSkinSide;\n /** Across-side parameter for side refs. Defaults to 0.5. */\n u?: number;\n /** Along-axis parameter, 0 at the first cap and 1 at the rear/back cap. Defaults to 0.5. */\n v?: number;\n /** Positive distance away from the surface along the resolved normal. */\n offset?: number;\n}\ninterface ProductSurfaceFrame {\n point: Vec3;\n normal: Vec3;\n tangentU: Vec3;\n tangentV: Vec3;\n matrix: Mat4;\n skin: string;\n representation: ProductSkinRepresentation;\n}\ninterface ProductSkinDiagnostics {\n representation: ProductSkinRepresentation;\n lowering: string[];\n warnings: string[];\n stationNames: string[];\n railNames: string[];\n}\ninterface ProductAttachOptions {\n offset?: number;\n inset?: number;\n}\ninterface ProductPanelAttachOptions extends ProductAttachOptions {\n at?: Partial<ProductSkinRefQuery>;\n thickness?: number;\n material?: ProductMaterial;\n color?: string;\n}\ntype ProductRefInput = ProductSurfaceRef;\n/** Options shared by Product.ribbon() builders and Product.surface(...).ribbon(...). */\ninterface ProductRibbonBuildOptions {\n /** Width across the surface in millimeters. */\n width?: number;\n /** Solid thickness outward from the source surface in millimeters. */\n thickness?: number;\n /** Positive clearance between the source surface and the ribbon\'s inner face. */\n offset?: number;\n /** Samples along the ribbon path. Higher values bend more smoothly. */\n samples?: number;\n /** Samples across the ribbon width. Use 3+ to visibly wrap over curved cross-sections. */\n widthSamples?: number;\n /** Tessellation resolution passed to the lowered NURBS surface. */\n resolution?: number;\n /** Apply a product material preset to the ribbon. */\n material?: ProductMaterial;\n /** Apply a simple color override. */\n color?: string;\n}\n/** Path point for Product.ribbon().on(...): either a side/u/v query or a resolved surface ref. */\ntype ProductRibbonPathPoint = ProductSkinRefQuery | ProductSurfaceRef;\ninterface ProductSurfacePathPoint {\n /** Across-side parameter on the bound side. Defaults to 0.5. */\n u?: number;\n /** Along-axis parameter, 0 at the first cap and 1 at the rear/back cap. Defaults to 0.5. */\n v?: number;\n /** Positive distance away from the surface along the resolved normal. */\n offset?: number;\n}\n/** Diagnostics describing how a conformal ribbon was sampled and lowered. */\ninterface ProductRibbonDiagnostics {\n /** Ribbon shape name. */\n name: string;\n /** Source skin name when the ribbon follows a ProductSkin directly. */\n skin?: string;\n /** Source skin side when all path points are on one semantic side. */\n side?: ProductSkinSide;\n /** Number of control path points supplied before interpolation. */\n pathPointCount: number;\n /** Final ribbon width in millimeters. */\n width: number;\n /** Final ribbon solid thickness in millimeters. */\n thickness: number;\n /** Final normal offset from the source surface in millimeters. */\n offset: number;\n /** Final sample count along the ribbon path. */\n samples: number;\n /** Final sample count across the ribbon width. */\n widthSamples: number;\n /** NURBS tessellation resolution used for the lowered surface. */\n resolution: number;\n /** Lowering primitive used for the ribbon shape. */\n lowering: "nurbsSurface";\n /** Expected fidelity inherited from the source skin/ref sampling mode. */\n expectedFidelity: ProductSkinRepresentation;\n /** Number of generated width samples clamped to the valid side span. */\n clampedUCount: number;\n /** Largest absolute u-distance lost to side-span clamping. */\n maxUClampDistance: number;\n /** Non-fatal sampling and lowering warnings. */\n warnings: string[];\n}\n/** Shape plus diagnostics returned by ProductRibbonBuilder.buildWithDiagnostics(). */\ninterface ProductRibbonResult {\n /** Lowered conformal ribbon shape. */\n shape: Shape;\n /** Sampling and lowering diagnostics for the returned shape. */\n diagnostics: ProductRibbonDiagnostics;\n}\ndeclare class ProductSurfaceRef {\n private readonly skin;\n private readonly query;\n readonly name?: string | undefined;\n constructor(skin: ProductSkin, query: ProductSkinRefQuery, name?: string | undefined);\n /** Resolve this semantic surface ref into a point, normal, tangents, and placement matrix. */\n frame(overrides?: Partial<ProductSkinRefQuery>): ProductSurfaceFrame;\n /** Return a copy of this ref with side/u/v/offset overrides. */\n with(overrides: Partial<ProductSkinRefQuery>): ProductSurfaceRef;\n /** Place a detail shape or group on this ref\'s local surface frame. */\n attach(detail: Shape | ShapeGroup, options?: ProductAttachOptions): Shape | ShapeGroup;\n /** Return the serializable side/u/v query behind this ref. */\n querySpec(): ProductSkinRefQuery;\n}\ndeclare class ProductSkin {\n readonly name: string;\n readonly shape: Shape;\n readonly axis: ProductSkinAxis;\n readonly stations: ProductStationSpec[];\n readonly rails: Record<string, ProductRailSpec>;\n private readonly refQueries;\n private readonly axisMin;\n private readonly axisMax;\n private readonly diagnosticsValue;\n constructor(name: string, shape: Shape, axis: ProductSkinAxis, stations: ProductStationSpec[], rails: Record<string, ProductRailSpec>, refs: Record<string, ProductSkinRefQuery>, diagnostics: Omit<ProductSkinDiagnostics, "stationNames" | "railNames">);\n /** Return the renderable shape generated for this product skin. */\n toShape(): Shape;\n /** Create a group containing this skin plus named child details. */\n with(...children: GroupInput[]): ShapeGroup;\n /** Boolean-union structural details into the skin body. */\n integrate(...details: Shape[]): Shape;\n /** Return lowering representation, station names, rail names, and warnings. */\n diagnostics(): ProductSkinDiagnostics;\n /** Create a side/u/v surface-ref query on this skin. */\n uv(side: ProductSkinSide, u?: number, v?: number): ProductSkinRefQuery;\n /** Resolve a named ref published with Product.skin().refs(...). */\n ref(name: string): ProductSurfaceRef;\n /** Create a sampled curve as a sequence of surface refs on this skin. */\n curveOnSurface(name: string, points: Array<Partial<ProductSkinRefQuery> & {\n side: ProductSkinSide;\n }>): ProductSurfaceRef[];\n /**\n * Create a fluent surface helper for refs and conformal features on one side of this skin.\n *\n * Use this when several refs or ribbons share the same skin side; side-local helpers keep\n * path points concise and make it harder to mix sides accidentally.\n */\n surface(side: ProductSkinSide): ProductSurfaceBuilder;\n /** Interpolate center, width, and depth at a normalized v or absolute axis value. */\n stationAt(vOrAxis: number): {\n center: Vec3;\n width: number;\n depth: number;\n dWidth: number;\n dDepth: number;\n axisValue: number;\n exponent: number;\n kind: ProductProfileKind;\n };\n /** Build a local surface frame from a side/u/v query. */\n frame(query: ProductSkinRefQuery): ProductSurfaceFrame;\n}\ndeclare class ProductSkinBuilder {\n readonly name: string;\n private axisValue;\n private stationsValue;\n private railsValue;\n private refsValue;\n private materialValue;\n private colorValue;\n private edgeLengthValue;\n private wallValue;\n constructor(name: string);\n /** Choose the primary station axis for the skin loft. */\n axis(axis: ProductSkinAxis): this;\n /** Set named cross-section stations for the product skin. */\n stations(stations: Array<ProductStationBuilder | ProductStationSpec>): this;\n /** Attach guide rails as ProductSkin IR metadata and diagnostics. */\n rails(rails: Record<string, ProductRailSpec>): this;\n /** Publish a named semantic surface ref on the skin. */\n ref(name: string, query: ProductSkinRefQuery): this;\n /** Publish multiple named semantic surface refs on the skin. */\n refs(refs: Record<string, ProductSkinRefQuery>): this;\n /** Create a side/u/v surface-ref query for use in refs(...) or Product.ref(...). */\n uv(side: ProductSkinSide, u?: number, v?: number): ProductSkinRefQuery;\n /** Apply a product material preset to the lowered skin. */\n material(material: ProductMaterial): this;\n /** Apply a simple color override to the lowered skin. */\n color(color: string): this;\n /** Set the sampled loft target edge length. */\n edgeLength(value: number): this;\n /** Records a target wall thickness; v1 keeps exterior skin lowering sampled and reports wall as a diagnostic. */\n wall(thickness: number): this;\n /** Lower stations and refs into a ProductSkin body. */\n build(): ProductSkin;\n}\ndeclare class ProductPanelBuilder {\n readonly name: string;\n private profileValue;\n private thicknessValue;\n private materialValue;\n private colorValue;\n constructor(name: string);\n /** Use a rounded rectangle panel profile. */\n rounded(width: number, height: number, radius?: number): this;\n /** Use an oval panel profile. */\n oval(width: number, height: number): this;\n /** Use a custom 2D panel profile. */\n profile(profile: Sketch): this;\n /** Set panel extrusion thickness. */\n thickness(thickness: number): this;\n /** Apply a product material preset to the panel. */\n material(material: ProductMaterial): this;\n /** Apply a simple color override to the panel. */\n color(color: string): this;\n /** Build the panel in local coordinates. */\n build(): Shape;\n /** Build and attach this panel to a ProductSurfaceRef. */\n attachTo(ref: ProductRefInput, options?: ProductPanelAttachOptions): Shape;\n}\ndeclare class ProductSpoutBuilder {\n readonly name: string;\n private fromValue?;\n private sectionsValue;\n private projectionValue;\n private materialValue;\n private colorValue;\n private edgeLengthValue;\n constructor(name: string);\n /** Set the skin ref this spout projects from. */\n from(ref: ProductSurfaceRef): this;\n /** Set local spout section profiles from root to mouth. */\n sections(sections: Array<Sketch | ProductStationBuilder | ProductStationSpec>): this;\n /** Set the projection length along the source ref normal. */\n projection(length: number): this;\n /** Set the sampled loft target edge length for the spout. */\n edgeLength(value: number): this;\n /** Apply a product material preset to the spout. */\n material(material: ProductMaterial): this;\n /** Apply a simple color override to the spout. */\n color(color: string): this;\n /** Build the spout in local coordinates. */\n build(): Shape;\n /** Build and place the spout on its source ref. */\n attach(options?: ProductAttachOptions): Shape;\n}\ndeclare class ProductHandleFeature {\n readonly grip: Shape;\n readonly upperPad: Shape;\n readonly lowerPad: Shape;\n constructor(grip: Shape, upperPad: Shape, lowerPad: Shape);\n /** Return the physical shapes that make up this handle feature. */\n structural(): Shape[];\n /** Boolean-union the handle feature into a single shape. */\n toShape(): Shape;\n /** Return the handle as a named ShapeGroup preserving child colors. */\n toGroup(): ShapeGroup;\n}\ndeclare class ProductHandleBuilder {\n readonly name: string;\n private upperRef?;\n private lowerPoint?;\n private spineValue?;\n private gripProfile;\n private materialValue;\n private padMaterialValue;\n private edgeLengthValue;\n constructor(name: string);\n /** Set the upper body ref and lower world anchor for the handle. */\n between(upper: ProductSurfaceRef, lower: Vec3): this;\n /** Set an explicit handle centerline from points or a rail spec. */\n spine(points: Vec3[] | ProductRailSpec): this;\n /** Set the grip cross-section profile. */\n grip(profile: Sketch): this;\n /** Apply a product material preset to the grip. */\n material(material: ProductMaterial): this;\n /** Apply a product material preset to handle landing pads. */\n padMaterial(material: ProductMaterial): this;\n /** Set the sampled loft target edge length for the grip. */\n edgeLength(value: number): this;\n /** Build the handle grip and landing pads. */\n build(): ProductHandleFeature;\n}\n/** Fluent helper bound to one ProductSkin side for refs and side-local conformal features. */\ndeclare class ProductSurfaceBuilder {\n private readonly skin;\n readonly side: ProductSkinSide;\n constructor(skin: ProductSkin, side: ProductSkinSide);\n /** Create a ref on this skin side. */\n ref(u?: number, v?: number, offset?: number): ProductSurfaceRef;\n /** Create a side/u/v query on this skin side. */\n uv(u?: number, v?: number, offset?: number): ProductSkinRefQuery;\n /**\n * Start a conformal ribbon on this skin side.\n *\n * Path points use side-local `u`/`v` coordinates; this builder supplies the side.\n * The returned ProductRibbonBuilder is already bound to the source skin and can be further\n * configured before build(). Use `widthSamples` >= 3 when the ribbon must visibly wrap over\n * curved product sections instead of behaving like a flat strip.\n */\n ribbon(name: string, points: ProductSurfacePathPoint[], options?: ProductRibbonBuildOptions): ProductRibbonBuilder;\n}\n/** Builder for thin trim, label, grip, and split-line features that bend with a ProductSkin surface. */\ndeclare class ProductRibbonBuilder {\n readonly name: string;\n private skinValue?;\n private queryPath;\n private refPath;\n private widthValue;\n private thicknessValue;\n private offsetValue;\n private samplesValue;\n private widthSamplesValue;\n private resolutionValue;\n private materialValue;\n private colorValue;\n private lastDiagnosticsValue?;\n constructor(name: string);\n /**\n * Follow a ProductSkin with side/u/v path queries or refs.\n *\n * This is the highest-fidelity mode because every interpolated sample is resolved through\n * ProductSkin.frame(), so the ribbon bends along the selected side as station width/depth changes.\n * All query path points must stay on one side; split side transitions into separate ribbons.\n */\n on(skin: ProductSkin, points: ProductRibbonPathPoint[], options?: ProductRibbonBuildOptions): this;\n /**\n * Follow explicit surface refs.\n *\n * Useful for named refs or paths assembled elsewhere. The builder resolves each ref frame and\n * interpolates between those frames; use on(skin, points) when you need full skin-side sampling\n * between sparse control points.\n */\n fromRefs(points: ProductSurfaceRef[], options?: ProductRibbonBuildOptions): this;\n /** Set ribbon width in millimeters. */\n width(width: number): this;\n /** Set solid thickness outward from the source surface in millimeters. */\n thickness(thickness: number): this;\n /** Set positive clearance between the source surface and the ribbon\'s inner face. */\n offset(offset: number): this;\n /** Set samples along the path. */\n samples(samples: number): this;\n /** Set samples across the width. Use 3+ to bend over curved cross-sections. */\n widthSamples(samples: number): this;\n /** Set NURBS tessellation resolution. */\n resolution(resolution: number): this;\n /** Apply a product material preset. */\n material(material: ProductMaterial): this;\n /** Apply a simple color override. */\n color(color: string): this;\n /** Build a conformal ribbon as a thin NURBS surface solid. */\n build(options?: ProductRibbonBuildOptions): Shape;\n /**\n * Build a conformal ribbon and return surface-feature diagnostics.\n *\n * Use this while validating API usage or model fidelity; diagnostics report sampling counts,\n * side-span clamping, lowering mode, and warnings that should be visible in reviews.\n */\n buildWithDiagnostics(options?: ProductRibbonBuildOptions): ProductRibbonResult;\n /** Return diagnostics from the most recent build, if this builder has been built. */\n diagnostics(): ProductRibbonDiagnostics | undefined;\n private applyOptions;\n private centerDesiredNormal;\n private samplePathQuery;\n private buildSkinGrid;\n private buildRefGrid;\n private makeDiagnostics;\n private cloneDiagnostics;\n}\ndeclare const Product: {\n /** Start a named product skin builder. */\n skin(name: string): ProductSkinBuilder;\n /** Start a named cross-section station for Product.skin(...).stations(...). */\n station(name: string): ProductStationBuilder;\n /** Namespaced rail builders for product skin guide rails and handle spines. */\n rail: {\n bezier(points: Vec3[], options?: {\n name?: string;\n }): ProductRailSpec;\n nurbs(points: Vec3[], options?: {\n degree?: number;\n name?: string;\n }): ProductRailSpec;\n polyline(points: Vec3[], options?: {\n name?: string;\n }): ProductRailSpec;\n };\n /** Namespaced product profile helpers for stations, panels, trims, and openings. */\n profiles: {\n /** Create a centered oval profile using full product width and depth dimensions. */\n oval(width: number, depth: number, options?: ProductProfileOptions): Sketch;\n /** Create a centered superellipse profile for soft square-to-oval product sections. */\n superEllipse(width: number, depth: number, options?: ProductSuperEllipseOptions): Sketch;\n /** Create a centered rounded-rectangle product profile. */\n roundedRect(width: number, depth: number, radius: number): Sketch;\n /** Create a centered circular product profile from its diameter. */\n circle(diameter: number, options?: ProductProfileOptions): Sketch;\n };\n /** Namespaced product material presets for molded plastic, rubber, metal, and transparent parts. */\n materials: {\n mattePlastic(color?: string): ProductMaterial;\n softRubber(options?: ColorMaterialOptions): ProductMaterial;\n clearPolycarbonate(options?: ClearMaterialOptions): ProductMaterial;\n brushedSteel(options?: ColorMaterialOptions): ProductMaterial;\n };\n /** Apply a product material preset to a Shape. */\n applyMaterial(shape: Shape, preset: ProductMaterial | undefined): Shape;\n /** Apply an opinionated scene preset for product review renders. */\n scenePreset(name: ProductScenePreset): void;\n /** Create a centered oval profile from full width/depth dimensions. */\n ovalProfile(width: number, depth: number, options?: ProductProfileOptions): Sketch;\n /** Create a centered rounded-rectangle profile. */\n roundedRectProfile(width: number, depth: number, radius: number): Sketch;\n /** Create a centered circular profile from full diameter. */\n circleProfile(diameter: number, options?: ProductProfileOptions): Sketch;\n /** Create a centered superellipse profile for soft-square product sections. */\n superEllipseProfile(width: number, depth: number, options?: ProductSuperEllipseOptions): Sketch;\n /** Measure the width and depth of a 2D profile sketch. */\n profileSize(sketch: Sketch): {\n width: number;\n depth: number;\n };\n /** Describe a custom sketch as a product profile. */\n describeProfile(sketch: Sketch, kind?: ProductProfileKind, radius?: number): ProductProfileDescriptor;\n /** Scale an existing profile sketch to a target width/depth. */\n scaleProfileTo(sketch: Sketch, width: number, depth: number): Sketch;\n /** Create an ad-hoc ProductSurfaceRef from a skin and side/u/v query. */\n ref(skin: ProductSkin, query: ProductSkinRefQuery): ProductSurfaceRef;\n /**\n * Create a fluent surface helper for refs and conformal features on one side of a skin.\n *\n * Equivalent to skin.surface(side), useful when writing in Product.* namespace style.\n */\n surface(skin: ProductSkin, side: ProductSkinSide): ProductSurfaceBuilder;\n /** Start a panel feature builder. */\n panel(name: string): ProductPanelBuilder;\n /**\n * Start a conformal ribbon/trim builder for details that should bend with a ProductSkin.\n *\n * Call .on(skin, points) for side/u/v sampling or .fromRefs(points) for explicit surface refs,\n * then configure width, thickness, offset, sampling, material, and color before build().\n */\n ribbon(name: string): ProductRibbonBuilder;\n /** Start a spout/nozzle feature builder. */\n spout(name: string): ProductSpoutBuilder;\n /** Start a handle feature builder. */\n handle(name: string): ProductHandleBuilder;\n /** Place a shape or group on a ProductSurfaceRef. */\n place(detail: Shape | ShapeGroup, ref: ProductRefInput, options?: ProductAttachOptions): Shape | ShapeGroup;\n /** Small blended landing volume for manual structural bridges and connection proofs. */\n landing(name: string, radius?: number, material?: ProductMaterial): Shape;\n};\ndeclare function distance(a: Vec3, b: Vec3): number;\ndeclare function midpoint(a: Vec3, b: Vec3): Vec3;\ndeclare function lerp(a: Vec3, b: Vec3, t: number): Vec3;\ndeclare function direction(a: Vec3, b: Vec3): Vec3;\ndeclare function offset(point: Vec3, dir: Vec3, amount: number): Vec3;\ndeclare const Points: {\n /** Euclidean distance between two 3D points. */\n readonly distance: typeof distance;\n /** Center point between two 3D points. */\n readonly midpoint: typeof midpoint;\n /** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */\n readonly lerp: typeof lerp;\n /** Unit direction vector from a to b. Throws if a and b are the same point. */\n readonly direction: typeof direction;\n /** Move a point along a direction vector by a given amount. */\n readonly offset: typeof offset;\n /** Compute a 2D point at distance and angle (degrees) from an optional origin. */\n readonly polar: typeof polar;\n};\ninterface WoodBoardOptions {\n /** Wood species, e.g. "birch", "oak", "plywood". Default: "wood" */\n species?: string;\n /** Material description for BOM, e.g. "birch plywood". Default: species value */\n material?: string;\n /** Grain direction: "long" (along width) or "cross" (along height). Default: "long" */\n grain?: string;\n /** Color hex string for visualization. Default: "#d2b48c" (tan/wood) */\n color?: string;\n /** If false, skip automatic BOM registration. Default: true */\n autoBom?: boolean;\n}\n/**\n * A board of wood with metadata for manufacturing: grain direction, species,\n * and dimensions. The underlying geometry is a simple box.\n *\n * WoodBoard operations are immutable. Joint operations return new boards instead\n * of carving the original in-place, and transform methods preserve all metadata.\n *\n * @category Woodworking\n */\ndeclare class WoodBoard {\n /** The underlying 3D shape. */\n shape: Shape;\n /** Board width (mm) — the longer flat dimension */\n readonly width: number;\n /** Board height (mm) — the shorter flat dimension */\n readonly height: number;\n /** Board thickness (mm) */\n readonly thickness: number;\n /** Grain direction: "long" or "cross" */\n readonly grain: string;\n /** Wood species, e.g. "birch", "oak" */\n readonly species: string;\n /** Material label for BOM */\n readonly material: string;\n constructor(width: number, height: number, thickness: number, opts?: WoodBoardOptions);\n /**\n * Subtract a cutter from this board, returning a new board.\n * Used by joint functions (dado, rabbet, mortiseAndTenon).\n */\n cut(cutter: Shape): WoodBoard;\n /** Create a new WoodBoard with a pre-built shape, preserving metadata. No BOM re-registration. */\n private _withShape;\n /** Translate the board in 3D space. */\n translate(x: number, y: number, z: number): WoodBoard;\n /** Rotate the board around an axis by a given angle in degrees. */\n rotate(axis: [\n number,\n number,\n number\n ], angleDeg: number, options?: {\n pivot?: [\n number,\n number,\n number\n ];\n }): WoodBoard;\n /** Rotate the board around the X axis by a given angle in degrees. */\n rotateX(angleDeg: number): WoodBoard;\n /** Rotate the board around the Y axis by a given angle in degrees. */\n rotateY(angleDeg: number): WoodBoard;\n /** Rotate the board around the Z axis by a given angle in degrees. */\n rotateZ(angleDeg: number): WoodBoard;\n /** Mirror the board across a plane defined by its normal. */\n mirror(normal: [\n number,\n number,\n number\n ]): WoodBoard;\n /** Set the board\'s display color. */\n color(value: string): WoodBoard;\n /** Clone the board (creates an independent copy of the underlying shape). */\n clone(): WoodBoard;\n}\n/**\n * Options for a dado joint — a channel cut across the face of a board.\n * @category Woodworking\n */\ninterface DadoOptions {\n /** Distance from the bottom edge of the host to the bottom of the channel (mm). */\n fromBottom?: number;\n /** Distance from the top edge of the host to the top of the channel (mm). Alternative to fromBottom. */\n fromTop?: number;\n /** Channel depth into the board thickness (mm). Default: 1/3 of host thickness. */\n depth?: number;\n /** Fit tolerance. Default: \'snug\'. */\n fit?: "friction" | "snug" | "slip";\n /** If set, the dado stops this far from the front edge (mm), creating a stopped dado. */\n stopped?: number;\n}\ndeclare function dado(host: WoodBoard, guest: WoodBoard, opts: DadoOptions): WoodBoard;\n/**\n * Options for a rabbet joint — an L-shaped step along an edge.\n * @category Woodworking\n */\ninterface RabbetOptions {\n /** Which edge to cut the rabbet on. */\n edge: "top" | "bottom" | "left" | "right" | "back";\n /** How far into the board face the rabbet extends (mm). */\n width: number;\n /** How deep the step is cut into the thickness (mm). */\n depth: number;\n}\ndeclare function rabbet(board: WoodBoard, opts: RabbetOptions): WoodBoard;\n/**\n * Options for a mortise-and-tenon joint.\n * @category Woodworking\n */\ninterface MortiseAndTenonOptions {\n /** \'blind\' (default): mortise doesn\'t go through. \'through\': mortise goes all the way. */\n style?: "blind" | "through";\n /** Position of the mortise center along the mortise board height. */\n position?: {\n fromTop?: number;\n fromBottom?: number;\n };\n /** Tenon thickness (mm). Default: 1/3 of tenon board thickness. */\n tenonThickness?: number;\n /** Tenon width (mm). Default: 60% of tenon board height. */\n tenonWidth?: number;\n /** Tenon length (mm). Default: 2/3 of mortise board thickness for blind, full for through. */\n tenonLength?: number;\n /** Corner radius for mortise (mm). Default: 0 (square corners, hand tools). */\n cornerRadius?: number;\n /** Fit tolerance. Default: \'snug\'. */\n fit?: "friction" | "snug" | "slip" | "knockdown";\n}\ninterface MortiseAndTenonResult {\n mortiseBoard: WoodBoard;\n tenonBoard: WoodBoard;\n}\ndeclare function mortiseAndTenon(mortiseBoard: WoodBoard, tenonBoard: WoodBoard, opts?: MortiseAndTenonOptions): MortiseAndTenonResult;\n/**\n * Woodworking 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\n * immutable — they return new board value(s) with the joint cut applied.\n *\n * @category Woodworking\n */\ndeclare const Wood: {\n /**\n * Create a wood board with metadata for manufacturing.\n *\n * The board is a box(width, height, thickness) centered on XY, base at Z=0.\n * Width along X, height along Y, thickness along Z (0 to thickness).\n *\n * @param width - Board width in mm (X-axis).\n * @param height - Board height in mm (Y-axis).\n * @param thickness - Board thickness in mm (Z-axis).\n * @param opts - Species, grain, color, and BOM options.\n * @returns A new WoodBoard instance.\n * @category Woodworking\n */\n readonly board: (width: number, height: number, thickness: number, opts?: WoodBoardOptions) => WoodBoard;\n /**\n * Cut a dado (channel) across the face of a host board for a guest board to sit in.\n *\n * Returns a new host board with the dado cut applied.\n *\n * @category Woodworking\n */\n readonly dado: typeof dado;\n /**\n * Cut a rabbet (L-shaped step) along an edge of a board.\n *\n * Returns a new board with the rabbet cut applied.\n *\n * @category Woodworking\n */\n readonly rabbet: typeof rabbet;\n /**\n * Cut a mortise in one board and shape a tenon on another.\n *\n * Returns new boards with the mortise pocket and tenon cuts applied.\n *\n * @category Woodworking\n */\n readonly mortiseAndTenon: typeof mortiseAndTenon;\n};\n/**\n * Highlight all user-labeled faces on a shape for visual debugging.\n *\n * Shows each user-authored label name in the viewport for visual debugging.\n * Returns the shape unchanged for chaining: `return showLabels(myShape)`.\n */\ndeclare function showLabels(shape: Shape): Shape;\n/** Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch. */\ndeclare function intersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch;\n/**\n * Extract the boundary profile of a named face as a 2D sketch.\n *\n * The result is returned in the face\'s local 2D coordinate system, making it convenient\n * for offsets, pocket profiles, or follow-up sketch operations driven by an existing face.\n *\n * @param shape - Source shape containing the face.\n * @param face - Named face selector to extract.\n * @returns A sketch of the face boundary in face-local coordinates.\n */\ndeclare function faceProfile(shape: Shape, face: FaceSelector): Sketch;\n/** Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch. */\ndeclare function projectToPlane(shape: Shape, plane: PlaneSpec): Sketch;\ninterface SheetMetalOptions {\n /**\n * Base panel dimensions. This is the flat blank before flanges are applied.\n */\n panel: {\n /** Width of the panel along the X axis. */\n width: number;\n /** Height of the panel along the Y axis. */\n height: number;\n };\n /** Sheet thickness in mm. Applied uniformly across the panel and all flanges. */\n thickness: number;\n /** Inside bend radius in mm. Must be ≥ 0. Typically 0.5–2× the sheet thickness. */\n bendRadius: number;\n /**\n * Bend allowance model used when computing the flat-pattern developed length.\n *\n * Currently only K-factor is supported. The K-factor (0–1) describes how far\n * the neutral axis sits from the inner bend surface. Typical values:\n * - Soft materials / large radius: 0.50\n * - General sheet steel: 0.42–0.44\n * - Hard materials / tight radius: 0.30–0.38\n */\n bendAllowance: {\n /** K-factor (neutral-axis offset, 0–1). */\n kFactor: number;\n };\n /**\n * Corner relief cut at each bend intersection.\n *\n * Prevents material overlap when two flanges meet at a corner. Defaults to a\n * rectangular relief sized to `bendRadius + thickness` if omitted.\n */\n cornerRelief?: {\n /** Relief shape — only `\'rect\'` is supported in v1. */\n kind?: "rect";\n /** Side length of the square relief cut in mm. */\n size: number;\n };\n}\ninterface SheetMetalFlangeOptions {\n /** Flange leg length in mm, measured from the outside of the bend to the tip. */\n length: number;\n /**\n * Bend angle in degrees (default: `90`).\n *\n * Only `90°` is supported in v1. Values other than 90 will be rejected at\n * build time.\n */\n angleDeg?: number;\n}\ninterface SheetMetalCutoutOptions {\n /** Horizontal offset within the region, measured from the region centre (mm). Default: `0`. */\n u?: number;\n /** Vertical offset within the region, measured from the region centre (mm). Default: `0`. */\n v?: number;\n /**\n * Anchor point on the sketch that aligns to `(u, v)`.\n *\n * Use `\'center\'` for most cases. For asymmetric profiles, verify orientation\n * by placing one test cutout before committing to the final position.\n *\n * Default: `\'center\'`.\n */\n selfAnchor?: Anchor;\n}\ninterface SheetMetalCutoutOp {\n region: SheetMetalPlanarRegionName;\n sketch: Sketch;\n u: number;\n v: number;\n selfAnchor: Anchor;\n}\n/**\n * An immutable sheet metal part that accumulates flanges and cutouts.\n *\n * Each mutating method returns a **new** `SheetMetalPart`; the original is\n * unchanged. The part does not produce geometry until you call `.folded()` or\n * `.flatPattern()`.\n *\n * @category Sheet Metal\n */\ndeclare class SheetMetalPart {\n private readonly model;\n private readonly cutouts;\n constructor(model: SheetMetalModel, cutouts?: readonly SheetMetalCutoutOp[]);\n /**\n * Add a 90° flange along one edge of the base panel.\n *\n * **Details**\n *\n * Each of the four edges (`\'top\'`, `\'right\'`, `\'bottom\'`, `\'left\'`) may carry\n * at most one flange. Calling `.flange()` twice for the same edge throws.\n *\n * Corner reliefs are automatically inserted at the intersections of adjacent\n * flanges. Build flanges before cutouts — validate with `.folded()` and\n * `.flatPattern()` after each addition.\n *\n * **Example**\n *\n * ```ts\n * const 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 * @param edge - Which panel edge gets the flange: `\'top\' | \'right\' | \'bottom\' | \'left\'`.\n * @param options - Flange leg length and optional bend angle (only 90° in v1).\n * @returns A new `SheetMetalPart` with the flange added.\n * @category Sheet Metal\n */\n flange(edge: SheetMetalEdge, options: SheetMetalFlangeOptions): SheetMetalPart;\n /**\n * Subtract a 2D sketch cutout from a planar region of the sheet metal part.\n *\n * **Details**\n *\n * `region` must be `\'panel\'` or one of `\'flange-top\'`, `\'flange-right\'`,\n * `\'flange-bottom\'`, `\'flange-left\'` (only available once the corresponding\n * flange has been added). Cutouts inside bend regions are **not** supported\n * in v1.\n *\n * `sketch` must be an **unplaced** compile-covered 2D profile (e.g. the result\n * of `circle2d()`, `rect()`, `roundedRect()`). Passing an already-placed\n * sketch (one that has had `.onFace(...)` called on it) will throw.\n *\n * **Authoring order:** Add all flanges before adding cutouts. Add panel\n * cutouts before flange cutouts. Add one region at a time and validate with\n * `.folded()` / `.flatPattern()` after each step.\n *\n * **Example**\n *\n * ```ts\n * const 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 * @param region - Target planar region: `\'panel\'` or `\'flange-<edge>\'`.\n * @param sketch - Unplaced 2D sketch profile to cut through the region.\n * @param options - Optional placement offsets and self-anchor point.\n * @returns A new `SheetMetalPart` with the cutout added.\n * @category Sheet Metal\n */\n cutout(region: SheetMetalPlanarRegionName, sketch: Sketch, options?: SheetMetalCutoutOptions): SheetMetalPart;\n /**\n * Return all semantic region names currently available on this part.\n *\n * **Details**\n *\n * The returned list always includes `\'panel\'`. For every flange that has been\n * added, the list also includes the corresponding `\'flange-<edge>\'` and\n * `\'bend-<edge>\'` entries.\n *\n * Use this to discover valid targets for `.cutout()` or for querying faces\n * by region after materializing with `.folded()`.\n *\n * Defended region names:\n * `panel` | `flange-top` | `flange-right` | `flange-bottom` | `flange-left` |\n * `bend-top` | `bend-right` | `bend-bottom` | `bend-left`\n *\n * @returns Array of semantic region name strings for this part\'s current configuration.\n * @category Sheet Metal\n */\n regionNames(): SheetMetalRegionName[];\n /**\n * Materialize the 3D folded solid.\n *\n * **Details**\n *\n * Applies all flanges (bent up at their configured angles) and all registered\n * cutouts, then returns the resulting `Shape`. The shape is compiler-owned\n * and exact-exportable (STEP, IGES, etc.).\n *\n * Prefer calling `.folded()` to validate each build step before proceeding\n * to the final model.\n *\n * @returns The fully folded 3D sheet metal solid.\n * @see {@link SheetMetalPart.flatPattern} for the unfolded blank.\n * @category Sheet Metal\n */\n folded(): Shape;\n /**\n * Materialize the flat-pattern (unfolded blank) for fabrication.\n *\n * **Details**\n *\n * Unfolds all flanges using the K-factor bend allowance and lays the result\n * flat in the XY plane. Cutouts are projected into the flat geometry.\n * The returned shape is exact-exportable and ready for laser / waterjet / CNC\n * nesting workflows.\n *\n * The developed length of each bend zone is:\n * `BA = (bendRadius + kFactor × thickness) × angleDeg × π / 180`\n *\n * @returns The flat 2D blank as a 3D solid of uniform thickness.\n * @see {@link SheetMetalPart.folded} for the 3D folded shape.\n * @category Sheet Metal\n */\n flatPattern(): Shape;\n private buildOutput;\n}\n/**\n * Create a parametric sheet metal part with flanges, bend allowances, and flat-pattern unfolding.\n *\n * **Details**\n *\n * `sheetMetal()` keeps one semantic model and derives both a folded 3D solid\n * and an accurate flat pattern from it. The K-factor bend allowance is applied\n * during unfolding. This is a strict v1 subset — it does not infer sheet metal\n * from arbitrary solids.\n *\n * **Recommended authoring order:**\n * 1. Define the base panel + thickness + bend parameters.\n * 2. Chain `.flange()` calls for each edge. Validate with `.folded()` and\n * `.flatPattern()` before adding cutouts.\n * 3. Add panel cutouts, then flange cutouts one region at a time.\n * 4. Validate after each new cutout region.\n *\n * **v1 limitations:** one base panel, up to four 90° edge flanges, constant\n * thickness, explicit K-factor, rectangular corner reliefs, planar cutouts\n * only. No hems, jogs, lofted bends, non-90° flanges, or bend-region cutouts.\n *\n * **Example**\n *\n * ```ts\n * const 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 *\n * const folded = cover.folded();\n * const flat = cover.flatPattern();\n * ```\n *\n * @param options - Panel geometry, thickness, bend radius, K-factor, and optional corner relief.\n * @returns A `SheetMetalPart` builder. Call `.folded()` or `.flatPattern()` to produce geometry.\n * @category Sheet Metal\n */\ndeclare function sheetMetal(options: SheetMetalOptions): SheetMetalPart;\ninterface FingerJointOptions {\n /** Explicit finger count (must be odd, >= 3). Default: auto from length/thickness. */\n fingers?: number;\n /** Explicit finger width. Default: auto. */\n fingerWidth?: number;\n /** Extra clearance per side (mm). Default: 0. */\n clearance?: number;\n /** Laser kerf (mm). Default: 0. */\n kerf?: number;\n /** Whether edge starts with full finger or half. Default: \'full\'. */\n endStyle?: "full" | "half";\n}\ninterface FingerJointResult {\n /** Even-position finger rects (tabs for side A, slots for side B). */\n tabProfile: Sketch;\n /** Odd-position finger rects (tabs for side B, slots for side A). */\n matingProfile: Sketch;\n /** Legacy: full rectangle minus odd slot cuts. */\n slotProfile: Sketch;\n}\ninterface TabSlotOptions {\n /** Number of tabs. Default: auto (length / (4 * thickness)). */\n tabCount?: number;\n /** Tab width. Default: 2 * thickness. */\n tabWidth?: number;\n /** Extra clearance per side (mm). Default: 0. */\n clearance?: number;\n /** Laser kerf (mm). Default: 0. */\n kerf?: number;\n /** Distance from panel edges to first/last tab center. Default: thickness. */\n inset?: number;\n}\ninterface TabSlotResult {\n tabs: Sketch;\n slots: Sketch;\n}\ninterface LivingHingeOptions {\n /** Slit pattern style. Default: \'straight\'. */\n pattern?: "straight" | "serpentine";\n /** Explicit slit width (beyond kerf). Default: 0. */\n slitWidth?: number;\n /** Distance between slit rows. Default: 2 * thickness. */\n rowSpacing?: number;\n /** Length of each slit. Default: 0.7 * (length - 2 * landWidth). */\n slitLength?: number;\n /** Uncut material between slit ends and row edges. Default: 2 * thickness. */\n landWidth?: number;\n /** Target bend radius - auto-computes row spacing. Overrides rowSpacing. */\n bendRadius?: number;\n /** Material thickness (needed for bend radius calc). Default: 3. */\n thickness?: number;\n}\ninterface SnapFitOptions {\n /** Tab beam length. Default: 4 * thickness. */\n tabLength?: number;\n /** Tab beam width. Default: thickness. */\n tabWidth?: number;\n /** How much the barb protrudes beyond the beam. Default: 0.3 * thickness. */\n overhang?: number;\n /** Slot clearance per side. Default: 0.1. */\n clearance?: number;\n /** Laser kerf. Default: 0. */\n kerf?: number;\n /** Barb style. Default: \'barb\'. */\n style?: "arrow" | "barb";\n}\ninterface SnapFitResult {\n tab: Sketch;\n slot: Sketch;\n}\n/**\n * Apply kerf compensation to a complete part outline (outer boundary + holes).\n *\n * Offsets inward by half-kerf: the outer boundary shrinks and inner holes grow.\n * This is correct because the laser beam removes material on both sides of the\n * cut line.\n */\ndeclare function kerfCompensateOutline(sketch: Sketch, kerf: number): Sketch;\n/**\n * Apply kerf compensation to joint protrusions (tabs, fingers).\n *\n * These grow by half-kerf so they are slightly oversized and fit tightly in\n * their mating slots after the laser removes material.\n */\ndeclare function kerfCompensateTabs(sketch: Sketch, kerf: number): Sketch;\n/**\n * Apply kerf compensation to joint cutouts (slots, holes that receive tabs).\n *\n * These grow by half-kerf so tabs can fit into them after the laser removes\n * material from both sides of the slot walls.\n */\ndeclare function kerfCompensateSlots(sketch: Sketch, kerf: number): Sketch;\ninterface PartJoints {\n /** Geometry to ADD to the base profile (tabs, fingers protruding from edges). */\n additions?: Sketch[];\n /** Geometry to SUBTRACT from the base profile (slots, holes for mating tabs). */\n subtractions?: Sketch[];\n}\n/**\n * Build a kerf-compensated part profile.\n *\n * 1. Start with the base profile.\n * 2. Kerf-compensate each tab addition (grow by kerf/2), then union with base.\n * 3. Kerf-compensate each slot subtraction (grow by kerf/2), then subtract from base.\n * 4. Kerf-compensate the resulting outline (shrink by kerf/2).\n *\n * Order matters: joints modify geometry BEFORE outline compensation so the\n * final inward offset applies uniformly to the assembled profile.\n */\ndeclare function kerfCompensatePart(baseProfile: Sketch, joints: PartJoints, kerf: number): Sketch;\ninterface MaterialKerfEntry {\n material: string;\n thickness: number;\n kerf: number;\n laserType: string;\n notes?: string;\n}\n/** Common kerf values. Users should always test-cut to verify for their specific setup. */\ndeclare const COMMON_KERFS: MaterialKerfEntry[];\n/**\n * Look up kerf for a material + thickness + laser combo.\n *\n * If `laserType` is omitted, returns the first matching material + thickness\n * entry. Returns `undefined` when no match is found.\n */\ndeclare function lookupKerf(material: string, thickness: number, laserType?: string): number | undefined;\ninterface EdgeInfo {\n name: string;\n start: [\n number,\n number\n ];\n end: [\n number,\n number\n ];\n length: number;\n /** Outward-facing normal. */\n normal: [\n number,\n number\n ];\n}\ninterface FlatPartOptions {\n material?: string;\n qty?: number;\n color?: string;\n}\n/** Tracks a joint connection for assembly preview. */\ninterface JointRecord {\n type: "finger" | "tabSlot" | "snapFit";\n partA: string;\n partB: string;\n edgeA: string;\n edgeB: string;\n /** Fold angle in degrees. Default: 90. */\n foldAngle: number;\n}\ndeclare class FlatPart {\n readonly name: string;\n readonly thickness: number;\n readonly options: FlatPartOptions;\n private _baseProfile;\n private _edges;\n private _additions;\n private _subtractions;\n private _joints;\n private _partNumber;\n constructor(name: string, baseProfile: Sketch, thickness: number, edges: Map<string, EdgeInfo>, options?: FlatPartOptions);\n /** All edges as a read-only map. */\n get edges(): ReadonlyMap<string, EdgeInfo>;\n /** Look up a named edge. Throws if the edge does not exist. */\n edge(name: string): EdgeInfo;\n /** All edge names on this part. */\n edgeNames(): string[];\n /** BOM part number assigned to this flat part. */\n get partNumber(): number;\n /** Update the BOM part number for this flat part. */\n set partNumber(n: number);\n /** Joint records that attach this part to other parts in the kit. */\n get joints(): readonly JointRecord[];\n /** Requested quantity of this part in the kit. Defaults to `1`. */\n get quantity(): number;\n /** Add geometry (e.g. protruding tabs) to the part profile. */\n addGeometry(sketch: Sketch): void;\n /** Subtract geometry (e.g. slot cuts) from the part profile. */\n subtractGeometry(sketch: Sketch): void;\n /** Record a joint connection for assembly preview. */\n addJoint(record: JointRecord): void;\n /** Final 2D profile with joints and optional kerf compensation. */\n profile(kerf?: number): Sketch;\n /** 3D solid — extrude the profile by material thickness. */\n solid(kerf?: number): Shape;\n}\n/**\n * Create a rectangular flat panel with 4 named edges.\n *\n * Profile origin at bottom-left corner.\n * Edges: bottom (y=0), right (x=width), top (y=height), left (x=0).\n * Edge traversal follows CCW winding order.\n */\ndeclare function flatPanel(name: string, width: number, height: number, thickness: number, options?: FlatPartOptions): FlatPart;\n/**\n * Create a flat part from an arbitrary profile with user-named edges.\n *\n * Edge normals are computed automatically (perpendicular to direction, rotated 90deg CW).\n */\ndeclare function flatPart(name: string, profile: Sketch, thickness: number, edges?: Record<string, {\n start: [\n number,\n number\n ];\n end: [\n number,\n number\n ];\n}>, options?: FlatPartOptions): FlatPart;\n/**\n * Connect two parts with finger joints along specified edges.\n *\n * Adds finger geometry to partA\'s edge, cuts matching slots from partB\'s edge.\n * The joint profiles are positioned along each edge using rotation + translation.\n */\ndeclare function fingerJoint(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: FingerJointOptions & {\n foldAngle?: number;\n}): void;\n/**\n * Connect two parts with tab-and-slot joints along specified edges.\n *\n * Adds tab geometry to partA\'s edge, cuts matching slots from partB\'s edge.\n */\ndeclare function tabSlot(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: TabSlotOptions & {\n foldAngle?: number;\n}): void;\ninterface PackedPiece {\n description: string;\n material: string;\n /** Width as placed (may differ from original if rotated). */\n width: number;\n /** Height as placed. */\n height: number;\n /** Original width before placement. */\n origWidth: number;\n /** Original height before placement. */\n origHeight: number;\n x: number;\n y: number;\n rotated: boolean;\n}\ninterface PackedSheet {\n sheetIndex: number;\n material: string;\n pieces: PackedPiece[];\n sheetWidth: number;\n sheetHeight: number;\n usedArea: number;\n /** Ordered guillotine cuts to separate all pieces on this sheet. */\n cuts: GuillotineCut[];\n}\ninterface GuillotineCut {\n /** 1-based sequence number. */\n step: number;\n /** \'V\' = vertical cut (splits left/right), \'H\' = horizontal cut (splits top/bottom). */\n direction: "V" | "H";\n /** Cut line start X (mm from sheet origin). */\n x1: number;\n /** Cut line start Y (mm from sheet origin). */\n y1: number;\n /** Cut line end X (mm from sheet origin). */\n x2: number;\n /** Cut line end Y (mm from sheet origin). */\n y2: number;\n /** Cut length in mm. */\n lengthMm: number;\n}\ninterface CuttingLayoutResult {\n sheets: PackedSheet[];\n totalPieces: number;\n totalSheets: number;\n totalUsedArea: number;\n totalSheetArea: number;\n wastePercent: number;\n kerf: number;\n /** Total length of all cuts across all sheets (mm). */\n totalCutLength: number;\n}\ninterface AssemblyPreviewOptions {\n /** Kerf compensation passed to each part\'s solid(). Default: 0 */\n kerf?: number;\n /** Fold amount: 0 = flat layout, 1 = fully assembled. Default: 1 */\n fold?: number;\n /** Explode distance: 0 = assembled, >0 = parts spread outward. Default: 0 */\n explode?: number;\n}\ninterface AssemblyPreviewResult {\n /** All part shapes grouped for display. */\n shapes: ShapeGroup;\n /** Individual transformed shapes keyed by part name. */\n partShapes: Map<string, Shape>;\n}\n/**\n * Generate a 3D assembly preview from flat parts and their joint records.\n *\n * The preview can fold joints partially or fully and optionally apply exploded spacing\n * so part relationships are easier to inspect visually.\n *\n * @param parts - Flat parts to assemble.\n * @param joints - Joint definitions connecting the parts.\n * @param options - Fold, explode, and kerf preview options.\n * @returns Preview shapes plus a per-part shape map.\n */\ndeclare function assemblyPreview(parts: FlatPart[], joints: JointRecord[], options?: AssemblyPreviewOptions): AssemblyPreviewResult;\ninterface AssemblyStep {\n /** 1-based step number. */\n stepNumber: number;\n /** Human-readable instruction. */\n description: string;\n /** The part being added in this step. */\n partName: string;\n /** Part number (for cross-ref with cut sheets). */\n partNumber: number;\n /** Which existing part it connects to. */\n connectsTo: string;\n /** Joint type used. */\n jointType: "finger" | "tabSlot" | "snapFit";\n /** The edge on the new part. */\n newPartEdge: string;\n /** The edge on the existing part. */\n existingPartEdge: string;\n /** Fold angle in degrees. */\n foldAngle: number;\n /** Part names in the assembly so far (after this step). */\n assembledParts: string[];\n}\ninterface AssemblyInstructionsOptions {\n /** Part to start from. Default: part with most joint connections. */\n rootPart?: string;\n}\ninterface AssemblyInstructionsResult {\n steps: AssemblyStep[];\n /** Total number of parts in the assembly. */\n totalParts: number;\n /** Parts not connected to the joint graph (orphans). */\n orphanParts: string[];\n}\n/**\n * Generate step-by-step assembly instructions from flat parts and joints.\n *\n * Algorithm:\n * 1. Build adjacency graph from joints\n * 2. Pick root part (most connections, or user-specified)\n * 3. BFS from root, creating one step per part addition\n * 4. Each step describes: which part to add, where it connects, how to orient it\n *\n * Heuristics for step ordering:\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\n * connections to already-assembled parts (structurally stable)\n */\ndeclare function assemblyInstructions(parts: FlatPart[], joints: JointRecord[], options?: AssemblyInstructionsOptions): AssemblyInstructionsResult;\n/**\n * Format assembly instructions as a human-readable text document.\n *\n * Includes a "Step 0" preamble identifying the base part, followed by\n * numbered steps, and a note about any orphan parts.\n */\ndeclare function formatInstructions(result: AssemblyInstructionsResult): string;\ninterface LaserKitOptions {\n /** Default material label for parts that don\'t specify one. */\n material?: string;\n /** Stock sheet width in mm (default 600). */\n sheetWidth?: number;\n /** Stock sheet height in mm (default 400). */\n sheetHeight?: number;\n /** Laser kerf in mm (default 0.2). */\n kerf?: number;\n}\ninterface LaserKitBomEntry {\n partNumber: number;\n name: string;\n quantity: number;\n material: string;\n widthMm: number;\n heightMm: number;\n}\ndeclare class LaserKit {\n private _parts;\n private _nextPartNumber;\n private _options;\n constructor(options?: LaserKitOptions);\n /** Laser kerf in mm. */\n get kerf(): number;\n /** All registered parts (flat, in insertion order). */\n get parts(): readonly FlatPart[];\n /** Default material label. */\n get material(): string;\n /** Stock sheet width in mm. */\n get sheetWidth(): number;\n /** Stock sheet height in mm. */\n get sheetHeight(): number;\n /**\n * Register a flat part with this kit.\n * Assigns a sequential part number and records the quantity.\n */\n addPart(part: FlatPart, overrides?: {\n qty?: number;\n }): this;\n /** Generate nested cut sheets using guillotine bin-packing. */\n cutSheets(): CuttingLayoutResult;\n /** Bill of materials listing every part with dimensions. */\n bom(): LaserKitBomEntry[];\n /** Individual SVG string for each part profile, keyed by part name. */\n partSvgs(): Map<string, string>;\n /** Combined inventory SVG showing all parts in a labeled grid. */\n inventorySvg(): string;\n /** Collect deduplicated joints from all registered parts. */\n private _collectJoints;\n /** 3D fold-up preview of the assembled kit. */\n assemblyPreview(options?: Omit<AssemblyPreviewOptions, "kerf">): AssemblyPreviewResult;\n /** Step-by-step assembly instructions. */\n assemblyInstructions(options?: AssemblyInstructionsOptions): AssemblyInstructionsResult;\n /** Human-readable assembly instructions text. */\n formatInstructions(options?: AssemblyInstructionsOptions): string;\n}\n/** Top-level factory for creating a LaserKit container. */\ndeclare function laserKit(options?: LaserKitOptions): LaserKit;\n/**\n * Import a module with optional ForgeCAD parameter overrides. Returns the module\'s exports.\n *\n * When importing a `.forge.js` file, the return value is what the script returns. If the script\n * returns a metadata object (e.g. `{ shape: myShape, bolts: {...} }`), the caller receives the\n * full object — renderable values and metadata together.\n *\n * **Path rule:** Always include the file extension in relative imports: use\n * `require("./part.forge.js")` for model files and `require("./helpers.js")` for plain helper\n * modules. ForgeCAD does not apply Node-style extension inference, so `require("./part")` will\n * not find `part.forge.js` or `part.js`.\n *\n * **Parameter scoping:** Parameters declared in required files are automatically namespaced with\n * a `"filename#N / "` prefix (e.g. `"bracket.forge.js#1 / Width"`). This prevents collisions\n * 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).\n * 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\n * const wall = param("Wall", 3);\n * const baseH = param("Base Height", 20);\n *\n * const mount = require(\'./motor-mount.forge.js\', { Wall: wall });\n * const 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\n * return { shape: mount, bolts: { dia: 5.3, pos: holePositions } };\n *\n * // base-body.forge.js\n * const mount = require(\'./motor-mount.forge.js\');\n * mount.bolts.pos // access the metadata\n * mount.shape // access the geometry\n * ```\n *\n * @concept import\n */\ndeclare function require$1(path: string, paramOverrides?: Record<string, number | string>): any;\n/** Parse an SVG file and return it as a Sketch with options for region filtering, scaling, and simplification. @concept import */\ndeclare function importSvgSketch(fileName: string, options?: SvgImportOptions): Sketch;\n/** Import an external mesh file (STL, OBJ, 3MF) as a Shape. @concept import */\ndeclare function importMesh(fileName: string, options?: {\n scale?: number;\n center?: boolean;\n}): Shape;\n/** Import a STEP file (.step, .stp) as an exact OCCT-backed Shape. Preserves NURBS curves, B-spline surfaces, and exact topology. Requires `setActiveBackend(\'occt\')`. @concept import */\ndeclare function importStep(fileName: string): Shape;\n/**\n * Highlight any geometry for visual debugging in the viewport.\n *\n * Supported inputs:\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` — 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 * @concept visual\n */\ndeclare function highlight(entityId: string, opts?: HighlightOptions): void;\ndeclare function highlight(point: [\n number,\n number,\n number\n], opts?: HighlightOptions): void;\ndeclare function highlight(edge: [\n [\n number,\n number,\n number\n ],\n [\n number,\n number,\n number\n ]\n], opts?: HighlightOptions): void;\ndeclare function highlight(plane: {\n normal: [\n number,\n number,\n number\n ];\n offset: number;\n}, opts?: HighlightOptions): void;\ndeclare function highlight(plane: {\n normal: [\n number,\n number,\n number\n ];\n point: [\n number,\n number,\n number\n ];\n}, opts?: HighlightOptions): void;\ndeclare function highlight(shape: Shape, opts?: HighlightOptions): void;\ndeclare function highlight(face: FaceRef, opts?: HighlightOptions): void;\ndeclare function highlight(edge: EdgeRef, opts?: HighlightOptions): void;\n/** All library parts. Access via `lib.xxx()` in scripts. */\ndeclare const lib: typeof partLibrary;\n\n/** Route step factories. Access via `route.line()`, `route.fillet()`, etc. */\ndeclare const route: typeof routeStepFactories;\n\n/** Connector factory. Create attachment points: `connector({...})`, `connector.male(type, {...})`, etc. */\ndeclare const connector: typeof connectorFactory;\n';
975
976
  const EDITOR_RESULT_STATUS_BOTTOM_PADDING = 28;
976
977
  const EDITOR_ERROR_STATUS_BOTTOM_PADDING = 88;
977
978
  function CodeEditor() {
@@ -1297,7 +1298,7 @@ function handleCommandError$1(err) {
1297
1298
  alert(message);
1298
1299
  }
1299
1300
  function CommandPalette() {
1300
- var _a, _b, _c;
1301
+ var _a, _b, _c, _d;
1301
1302
  useNavigate();
1302
1303
  const open = useForgeStore((s) => s.commandPaletteOpen);
1303
1304
  const close = useForgeStore((s) => s.closeCommandPalette);
@@ -1315,6 +1316,7 @@ function CommandPalette() {
1315
1316
  const viewPanelOpen = useForgeStore((s) => s.viewPanelOpen);
1316
1317
  const toggleViewPanel = useForgeStore((s) => s.toggleViewPanel);
1317
1318
  const requestViewCommand = useForgeStore((s) => s.requestViewCommand);
1319
+ const openObjectSearch = useForgeStore((s) => s.openObjectSearch);
1318
1320
  const openShortcutsOverlay = useForgeStore((s) => s.openShortcutsOverlay);
1319
1321
  const featureFlags = useFeatureFlagStore((s) => s.flags);
1320
1322
  const toggleFeatureFlag = useFeatureFlagStore((s) => s.toggle);
@@ -1341,6 +1343,7 @@ function CommandPalette() {
1341
1343
  return !(((_a2 = objectSettings[obj.id]) == null ? void 0 : _a2.visible) ?? true);
1342
1344
  }).length;
1343
1345
  const hasObjectCommands = (((_c = result == null ? void 0 : result.objects) == null ? void 0 : _c.length) ?? 0) > 0 || Object.keys(objectSettings).length > 0;
1346
+ const hasSearchableObjects = (((_d = result == null ? void 0 : result.objects) == null ? void 0 : _d.length) ?? 0) > 0;
1344
1347
  const themeChoices = ["dark", "light", "gruvbox", "tokyo-night", "kanagawa-lotus"].map((t) => ({
1345
1348
  id: `theme-${t}`,
1346
1349
  label: `${t.charAt(0).toUpperCase() + t.slice(1)}${theme === t ? " ✓" : ""}`,
@@ -1532,6 +1535,16 @@ function CommandPalette() {
1532
1535
  close();
1533
1536
  }
1534
1537
  },
1538
+ ...hasSearchableObjects ? [
1539
+ {
1540
+ id: "search-objects",
1541
+ label: "Search Objects",
1542
+ action: () => {
1543
+ close();
1544
+ openObjectSearch();
1545
+ }
1546
+ }
1547
+ ] : [],
1535
1548
  {
1536
1549
  id: "iso-view",
1537
1550
  label: "Snap to Isometric View",
@@ -1920,7 +1933,23 @@ function CuttingLayoutPanel({ fileStem, entries, onClose }) {
1920
1933
  width: 52
1921
1934
  };
1922
1935
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1923
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 12, color: "var(--fc-textDim)", marginBottom: 6 }, children: "Sheet Cutting Layout" }),
1936
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6 }, children: [
1937
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 12, color: "var(--fc-textDim)" }, children: "Sheet Cutting Layout" }),
1938
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
1939
+ "span",
1940
+ {
1941
+ style: {
1942
+ padding: "1px 5px",
1943
+ borderRadius: 4,
1944
+ background: "color-mix(in srgb, var(--fc-success) 14%, transparent)",
1945
+ color: "var(--fc-success)",
1946
+ fontSize: 10,
1947
+ fontWeight: 700
1948
+ },
1949
+ children: "Production"
1950
+ }
1951
+ )
1952
+ ] }),
1924
1953
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { fontSize: 11, color: "var(--fc-textDim)", marginBottom: 10 }, children: [
1925
1954
  stats.totalPieces,
1926
1955
  " piece",
@@ -1933,6 +1962,7 @@ function CuttingLayoutPanel({ fileStem, entries, onClose }) {
1933
1962
  stats.stockAreaM2.toFixed(3),
1934
1963
  " m²"
1935
1964
  ] }),
1965
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 11, color: "var(--fc-textDim)", lineHeight: 1.4, marginBottom: 10 }, children: PRODUCTION_EXPORT_COPY }),
1936
1966
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 8, flexWrap: "wrap" }, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { style: { fontSize: 11, color: "var(--fc-textDim)", display: "flex", alignItems: "center", gap: 4 }, children: [
1937
1967
  "Stock sheet",
1938
1968
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -2014,8 +2044,8 @@ const FORMAT_META = {
2014
2044
  extension: "stl",
2015
2045
  tag: "Legacy"
2016
2046
  },
2017
- step: { label: "STEP", desc: "Exact CAD exchange for downstream CAD tools.", extension: "step" },
2018
- brep: { label: "BREP", desc: "Native OpenCascade boundary representation.", extension: "brep" }
2047
+ step: { label: "STEP", desc: "Exact CAD exchange for downstream CAD tools.", extension: "step", tag: "Production" },
2048
+ brep: { label: "BREP", desc: "Native OpenCascade boundary representation.", extension: "brep", tag: "Production" }
2019
2049
  };
2020
2050
  const MESH_FORMATS = ["3mf", "obj", "stl"];
2021
2051
  const EXACT_FORMATS = ["step", "brep"];
@@ -2122,6 +2152,14 @@ function Export3DPanel({ fileStem, defaultStem, shapeCount, totalTriangles: _tot
2122
2152
  fontWeight: 700,
2123
2153
  cursor: anyBusy ? "default" : "pointer"
2124
2154
  });
2155
+ const tagStyle = (tag) => ({
2156
+ padding: "1px 5px",
2157
+ borderRadius: 4,
2158
+ background: tag === "Legacy" ? "color-mix(in srgb, var(--fc-warning) 16%, transparent)" : tag === "Production" ? "color-mix(in srgb, var(--fc-success) 14%, transparent)" : "color-mix(in srgb, var(--fc-accent) 12%, transparent)",
2159
+ color: tag === "Legacy" ? "var(--fc-warning)" : tag === "Production" ? "var(--fc-success)" : "var(--fc-accent)",
2160
+ fontSize: 10,
2161
+ fontWeight: 700
2162
+ });
2125
2163
  const renderExportRow = (targetFormat) => {
2126
2164
  const meta = FORMAT_META[targetFormat];
2127
2165
  const busy = exportingFormat === targetFormat;
@@ -2141,20 +2179,7 @@ function Export3DPanel({ fileStem, defaultStem, shapeCount, totalTriangles: _tot
2141
2179
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { minWidth: 0 }, children: [
2142
2180
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 }, children: [
2143
2181
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontSize: 13, fontWeight: 700, color: "var(--fc-text)" }, children: meta.label }),
2144
- meta.tag && /* @__PURE__ */ jsxRuntimeExports.jsx(
2145
- "span",
2146
- {
2147
- style: {
2148
- padding: "1px 5px",
2149
- borderRadius: 4,
2150
- background: meta.tag === "Legacy" ? "color-mix(in srgb, var(--fc-warning) 16%, transparent)" : "color-mix(in srgb, var(--fc-accent) 12%, transparent)",
2151
- color: meta.tag === "Legacy" ? "var(--fc-warning)" : "var(--fc-accent)",
2152
- fontSize: 10,
2153
- fontWeight: 700
2154
- },
2155
- children: meta.tag
2156
- }
2157
- )
2182
+ meta.tag && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: tagStyle(meta.tag), children: meta.tag })
2158
2183
  ] }),
2159
2184
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { display: "block", marginTop: 2, fontSize: 11, color: "var(--fc-textDim)", lineHeight: 1.35 }, children: meta.desc })
2160
2185
  ] }),
@@ -2187,6 +2212,7 @@ function Export3DPanel({ fileStem, defaultStem, shapeCount, totalTriangles: _tot
2187
2212
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: sectionBorder, children: [
2188
2213
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: sectionTitle, children: "Exact geometry" }),
2189
2214
  activeBackend !== "occt" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 11, color: "var(--fc-textDim)", marginBottom: 7 }, children: "STEP and BREP export re-evaluates with OCCT when needed." }),
2215
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 11, color: "var(--fc-textDim)", marginBottom: 7, lineHeight: 1.4 }, children: PRODUCTION_EXPORT_COPY }),
2190
2216
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { display: "grid", gap: 8 }, children: EXACT_FORMATS.map(renderExportRow) })
2191
2217
  ] }),
2192
2218
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: sectionBorder, children: [
@@ -2202,12 +2228,16 @@ function Export3DPanel({ fileStem, defaultStem, shapeCount, totalTriangles: _tot
2202
2228
  children: [
2203
2229
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: extensionPill, children: ".pdf" }),
2204
2230
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { minWidth: 0 }, children: [
2205
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { display: "block", fontSize: 13, fontWeight: 700, color: "var(--fc-text)" }, children: "Report PDF" }),
2231
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 }, children: [
2232
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontSize: 13, fontWeight: 700, color: "var(--fc-text)" }, children: "Report PDF" }),
2233
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: tagStyle("Production"), children: "Production" })
2234
+ ] }),
2206
2235
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { display: "block", marginTop: 2, fontSize: 11, color: "var(--fc-textDim)", lineHeight: 1.35 }, children: [
2207
2236
  "Multi-view model report",
2208
2237
  shapeCount > 1 ? ` for ${shapeCount} components` : "",
2209
2238
  "."
2210
- ] })
2239
+ ] }),
2240
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { display: "block", marginTop: 2, fontSize: 11, color: "var(--fc-textDim)", lineHeight: 1.35 }, children: PRODUCTION_EXPORT_COPY })
2211
2241
  ] }),
2212
2242
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: actionPill(reportBusy), children: reportBusy ? "Generating..." : "Export" })
2213
2243
  ]
@@ -2599,7 +2629,8 @@ function ProjectPickerModal({ files, sourceProjectId, onClose }) {
2599
2629
  showToast(`Copied ${fileLabel} to ${dest.name}`, "success", 3e3);
2600
2630
  onClose();
2601
2631
  } catch (err) {
2602
- showToast(err.message || "Failed to copy file", "error");
2632
+ const quotaMessage = storageQuotaUpgradeMessage(err);
2633
+ showToast(quotaMessage ?? err.message ?? "Failed to copy file", "error", quotaMessage ? 8e3 : 3e3);
2603
2634
  } finally {
2604
2635
  setBusy(false);
2605
2636
  }
@@ -2614,7 +2645,8 @@ function ProjectPickerModal({ files, sourceProjectId, onClose }) {
2614
2645
  showToast(`Created "${name.trim()}"`, "success", 2e3);
2615
2646
  await handleSelect(newProject);
2616
2647
  } catch (err) {
2617
- showToast(err.message || "Failed to create project", "error");
2648
+ const quotaMessage = storageQuotaUpgradeMessage(err);
2649
+ showToast(quotaMessage ?? err.message ?? "Failed to create project", "error", quotaMessage ? 8e3 : 3e3);
2618
2650
  }
2619
2651
  }, [createProject, handleSelect]);
2620
2652
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -3754,7 +3786,8 @@ function ProjectSelector() {
3754
3786
  setOpen(false);
3755
3787
  navigate(`/app/p/${newProject.id}`);
3756
3788
  } catch (err) {
3757
- showToast(err.message || "Failed to create project", "error");
3789
+ const quotaMessage = storageQuotaUpgradeMessage(err);
3790
+ showToast(quotaMessage ?? err.message ?? "Failed to create project", "error", quotaMessage ? 8e3 : 3e3);
3758
3791
  }
3759
3792
  }, [createProject, navigate]);
3760
3793
  reactExports.useCallback(
@@ -4027,6 +4060,304 @@ function KeyboardShortcutsOverlay() {
4027
4060
  }
4028
4061
  );
4029
4062
  }
4063
+ const normalizeSearchText = (value) => value.trim().toLowerCase();
4064
+ const isBoundary = (value, index2) => {
4065
+ if (index2 <= 0) return true;
4066
+ const prev = value[index2 - 1];
4067
+ const current = value[index2];
4068
+ return /[\s._\-/>:()[\]{}]/.test(prev) || prev === prev.toLowerCase() && current === current.toUpperCase();
4069
+ };
4070
+ function fuzzyScore(query, candidate) {
4071
+ const normalizedQuery = normalizeSearchText(query);
4072
+ const normalizedCandidate = normalizeSearchText(candidate);
4073
+ if (!normalizedQuery) return 0;
4074
+ if (!normalizedCandidate) return null;
4075
+ let candidateIndex = 0;
4076
+ let previousMatch = -1;
4077
+ let score = 0;
4078
+ for (const char of normalizedQuery) {
4079
+ const matchIndex = normalizedCandidate.indexOf(char, candidateIndex);
4080
+ if (matchIndex < 0) return null;
4081
+ const gap = previousMatch < 0 ? matchIndex : matchIndex - previousMatch - 1;
4082
+ score += 2;
4083
+ if (gap === 0) score += 8;
4084
+ if (isBoundary(candidate, matchIndex)) score += 6;
4085
+ score -= Math.min(gap, 12) * 0.35;
4086
+ previousMatch = matchIndex;
4087
+ candidateIndex = matchIndex + 1;
4088
+ }
4089
+ if (normalizedCandidate.startsWith(normalizedQuery)) score += 20;
4090
+ if (normalizedCandidate.includes(normalizedQuery)) score += 12;
4091
+ score -= normalizedCandidate.length * 0.02;
4092
+ return score;
4093
+ }
4094
+ function rankObjectSearchItems(items, query) {
4095
+ const normalizedQuery = normalizeSearchText(query);
4096
+ if (!normalizedQuery) return items;
4097
+ const tokens = normalizedQuery.split(/\s+/).filter(Boolean);
4098
+ const ranked = [];
4099
+ items.forEach((item, index2) => {
4100
+ const fields = [item.label, item.path, item.group ?? "", item.kind, item.id].filter(Boolean);
4101
+ let totalScore = 0;
4102
+ for (const token of tokens) {
4103
+ const bestFieldScore = fields.reduce((best, field, fieldIndex) => {
4104
+ const score = fuzzyScore(token, field);
4105
+ if (score === null) return best;
4106
+ const weightedScore = fieldIndex === 0 ? score + 10 : score;
4107
+ return best === null ? weightedScore : Math.max(best, weightedScore);
4108
+ }, null);
4109
+ if (bestFieldScore === null) return;
4110
+ totalScore += bestFieldScore;
4111
+ }
4112
+ ranked.push({ item, score: totalScore - index2 * 1e-3 });
4113
+ });
4114
+ return ranked.sort((a, b) => b.score - a.score).map((rankedItem) => rankedItem.item);
4115
+ }
4116
+ const cleanObjectTreeSegments = (segments) => (segments ?? []).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
4117
+ const getObjectTreePath = (object) => {
4118
+ var _a;
4119
+ const explicitTreePath = cleanObjectTreeSegments(object.treePath);
4120
+ if (explicitTreePath.length > 0) return explicitTreePath;
4121
+ const name = object.name.trim() || object.id;
4122
+ const groupName = (_a = object.groupName) == null ? void 0 : _a.trim();
4123
+ if (!groupName) return [name];
4124
+ const groupPath = groupName.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
4125
+ const prefixedLeaf = `${groupName}.`;
4126
+ if (name.startsWith(prefixedLeaf)) {
4127
+ const leafName = name.slice(prefixedLeaf.length).trim();
4128
+ return [...groupPath, leafName || name];
4129
+ }
4130
+ return [...groupPath, name];
4131
+ };
4132
+ function getObjectKind(object) {
4133
+ if (object.mock) return "mock";
4134
+ if (object.sketch) return "sketch";
4135
+ if (object.toolpath) return "toolpath";
4136
+ if (object.sdf) return "sdf";
4137
+ if (object.shape) return "shape";
4138
+ return "object";
4139
+ }
4140
+ function buildObjectSearchItem(object) {
4141
+ var _a;
4142
+ const pathSegments = getObjectTreePath(object);
4143
+ const label = (pathSegments[pathSegments.length - 1] ?? object.name.trim()) || object.id;
4144
+ const group = pathSegments.length > 1 ? pathSegments.slice(0, -1).join(" / ") : (_a = object.groupName) == null ? void 0 : _a.trim();
4145
+ return {
4146
+ id: object.id,
4147
+ label,
4148
+ path: pathSegments.join(" / "),
4149
+ group,
4150
+ kind: getObjectKind(object)
4151
+ };
4152
+ }
4153
+ function ObjectSearchPalette() {
4154
+ const open = useForgeStore((s) => s.objectSearchOpen);
4155
+ const close = useForgeStore((s) => s.closeObjectSearch);
4156
+ const result = useForgeStore((s) => s.lastValidResult);
4157
+ const objectSettings = useForgeStore((s) => s.objectSettings);
4158
+ const selectObject = useForgeStore((s) => s.selectObject);
4159
+ const clearFocusedObject = useForgeStore((s) => s.clearFocusedObject);
4160
+ const requestViewCommand = useForgeStore((s) => s.requestViewCommand);
4161
+ const setHoveredObjectId = useForgeStore((s) => s.setHoveredObjectId);
4162
+ const [query, setQuery] = reactExports.useState("");
4163
+ const [selected, setSelected] = reactExports.useState(0);
4164
+ const inputRef = reactExports.useRef(null);
4165
+ const rowRefs = reactExports.useRef({});
4166
+ const items = reactExports.useMemo(
4167
+ () => ((result == null ? void 0 : result.objects) ?? []).filter((object) => {
4168
+ var _a;
4169
+ return ((_a = objectSettings[object.id]) == null ? void 0 : _a.visible) !== false;
4170
+ }).map((object) => buildObjectSearchItem(object)),
4171
+ [objectSettings, result == null ? void 0 : result.objects]
4172
+ );
4173
+ const filtered = reactExports.useMemo(() => rankObjectSearchItems(items, query), [items, query]);
4174
+ reactExports.useEffect(() => {
4175
+ if (!open) {
4176
+ setHoveredObjectId(null);
4177
+ return;
4178
+ }
4179
+ setQuery("");
4180
+ setSelected(0);
4181
+ setTimeout(() => {
4182
+ var _a;
4183
+ return (_a = inputRef.current) == null ? void 0 : _a.focus();
4184
+ }, 0);
4185
+ }, [open, setHoveredObjectId]);
4186
+ reactExports.useEffect(() => {
4187
+ setSelected(0);
4188
+ }, [query]);
4189
+ reactExports.useEffect(() => {
4190
+ setSelected((current) => Math.min(current, Math.max(0, filtered.length - 1)));
4191
+ }, [filtered.length]);
4192
+ reactExports.useEffect(() => {
4193
+ var _a;
4194
+ const selectedItem = filtered[selected];
4195
+ if (!selectedItem) {
4196
+ setHoveredObjectId(null);
4197
+ return;
4198
+ }
4199
+ setHoveredObjectId(selectedItem.id);
4200
+ (_a = rowRefs.current[selectedItem.id]) == null ? void 0 : _a.scrollIntoView({ block: "nearest" });
4201
+ }, [filtered, selected, setHoveredObjectId]);
4202
+ const chooseObject = reactExports.useCallback(
4203
+ (item) => {
4204
+ selectObject(item.id);
4205
+ requestViewCommand({ type: "fit", targetId: item.id });
4206
+ clearFocusedObject();
4207
+ setHoveredObjectId(null);
4208
+ close();
4209
+ },
4210
+ [clearFocusedObject, close, requestViewCommand, selectObject, setHoveredObjectId]
4211
+ );
4212
+ const handleKeyDown = (event) => {
4213
+ if (event.key === "Escape") {
4214
+ event.preventDefault();
4215
+ close();
4216
+ } else if (event.key === "ArrowDown") {
4217
+ event.preventDefault();
4218
+ setSelected((current) => Math.min(current + 1, filtered.length - 1));
4219
+ } else if (event.key === "ArrowUp") {
4220
+ event.preventDefault();
4221
+ setSelected((current) => Math.max(current - 1, 0));
4222
+ } else if (event.key === "Enter") {
4223
+ event.preventDefault();
4224
+ const item = filtered[selected];
4225
+ if (item) chooseObject(item);
4226
+ }
4227
+ };
4228
+ if (!open) return null;
4229
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4230
+ "div",
4231
+ {
4232
+ style: {
4233
+ position: "fixed",
4234
+ inset: 0,
4235
+ zIndex: 9998,
4236
+ display: "flex",
4237
+ justifyContent: "center",
4238
+ paddingTop: "15vh"
4239
+ },
4240
+ onClick: close,
4241
+ children: [
4242
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { position: "absolute", inset: 0, background: "var(--fc-bg)", opacity: 0.5 } }),
4243
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4244
+ "div",
4245
+ {
4246
+ onClick: (event) => event.stopPropagation(),
4247
+ style: {
4248
+ position: "relative",
4249
+ width: 520,
4250
+ maxWidth: "calc(100vw - 32px)",
4251
+ maxHeight: 420,
4252
+ background: "var(--fc-bgPanel)",
4253
+ border: "1px solid var(--fc-border)",
4254
+ borderRadius: 8,
4255
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
4256
+ display: "flex",
4257
+ flexDirection: "column",
4258
+ overflow: "hidden"
4259
+ },
4260
+ children: [
4261
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4262
+ "input",
4263
+ {
4264
+ ref: inputRef,
4265
+ value: query,
4266
+ onChange: (event) => setQuery(event.target.value),
4267
+ onKeyDown: handleKeyDown,
4268
+ placeholder: "Search model objects...",
4269
+ style: {
4270
+ padding: "10px 14px",
4271
+ background: "transparent",
4272
+ border: "none",
4273
+ borderBottom: "1px solid var(--fc-border)",
4274
+ color: "var(--fc-text)",
4275
+ fontSize: 14,
4276
+ outline: "none"
4277
+ }
4278
+ }
4279
+ ),
4280
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { overflowY: "auto", flex: 1 }, children: [
4281
+ filtered.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { padding: "12px 14px", color: "var(--fc-textDim)", fontSize: 13 }, children: items.length === 0 ? "No visible objects" : "No matching objects" }),
4282
+ filtered.map((item, index2) => {
4283
+ const isSelected = index2 === selected;
4284
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4285
+ "div",
4286
+ {
4287
+ ref: (element) => {
4288
+ rowRefs.current[item.id] = element;
4289
+ },
4290
+ onClick: () => chooseObject(item),
4291
+ onMouseEnter: () => setSelected(index2),
4292
+ style: {
4293
+ padding: "9px 14px",
4294
+ cursor: "pointer",
4295
+ color: "var(--fc-text)",
4296
+ background: isSelected ? "var(--fc-bgHover)" : "transparent",
4297
+ display: "grid",
4298
+ gridTemplateColumns: "minmax(0, 1fr) auto",
4299
+ gap: 10,
4300
+ alignItems: "center"
4301
+ },
4302
+ children: [
4303
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { minWidth: 0 }, children: [
4304
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4305
+ "div",
4306
+ {
4307
+ style: {
4308
+ fontSize: 13,
4309
+ overflow: "hidden",
4310
+ textOverflow: "ellipsis",
4311
+ whiteSpace: "nowrap"
4312
+ },
4313
+ title: item.path,
4314
+ children: item.label
4315
+ }
4316
+ ),
4317
+ item.group && /* @__PURE__ */ jsxRuntimeExports.jsx(
4318
+ "div",
4319
+ {
4320
+ style: {
4321
+ marginTop: 2,
4322
+ fontSize: 11,
4323
+ color: "var(--fc-textDim)",
4324
+ overflow: "hidden",
4325
+ textOverflow: "ellipsis",
4326
+ whiteSpace: "nowrap"
4327
+ },
4328
+ title: item.group,
4329
+ children: item.group
4330
+ }
4331
+ )
4332
+ ] }),
4333
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4334
+ "span",
4335
+ {
4336
+ style: {
4337
+ fontSize: 11,
4338
+ color: "var(--fc-textDim)",
4339
+ background: "var(--fc-bgSurface)",
4340
+ border: "1px solid var(--fc-border)",
4341
+ borderRadius: 3,
4342
+ padding: "1px 5px",
4343
+ whiteSpace: "nowrap"
4344
+ },
4345
+ children: item.kind
4346
+ }
4347
+ )
4348
+ ]
4349
+ },
4350
+ item.id
4351
+ );
4352
+ })
4353
+ ] })
4354
+ ]
4355
+ }
4356
+ )
4357
+ ]
4358
+ }
4359
+ );
4360
+ }
4030
4361
  const numberInputStyle = {
4031
4362
  width: 72,
4032
4363
  minWidth: 0,
@@ -4848,14 +5179,7 @@ function suggestRename(filename, authorName) {
4848
5179
  }
4849
5180
  return dir + base + `-${slug}`;
4850
5181
  }
4851
- function ShareCopyModal({
4852
- shareId,
4853
- entryFile,
4854
- files,
4855
- authorName,
4856
- onCopyComplete,
4857
- onClose
4858
- }) {
5182
+ function ShareCopyModal({ shareId, entryFile, files, authorName, onCopyComplete, onClose }) {
4859
5183
  const projects = useProjectStore((s) => s.projects);
4860
5184
  const fetchProjects = useProjectStore((s) => s.fetchProjects);
4861
5185
  const createProject = useProjectStore((s) => s.createProject);
@@ -4918,7 +5242,8 @@ function ShareCopyModal({
4918
5242
  });
4919
5243
  setStep("conflicts");
4920
5244
  } catch (err) {
4921
- showToast(err.message || "Failed to check conflicts", "error");
5245
+ const quotaMessage = storageQuotaUpgradeMessage(err);
5246
+ showToast(quotaMessage ?? err.message ?? "Failed to check conflicts", "error", quotaMessage ? 8e3 : 3e3);
4922
5247
  } finally {
4923
5248
  setBusy(false);
4924
5249
  }
@@ -4936,7 +5261,8 @@ function ShareCopyModal({
4936
5261
  });
4937
5262
  onCopyComplete(destProjectId, entryFile);
4938
5263
  } catch (err) {
4939
- showToast(err.message || "Failed to copy files", "error");
5264
+ const quotaMessage = storageQuotaUpgradeMessage(err);
5265
+ showToast(quotaMessage ?? err.message ?? "Failed to copy files", "error", quotaMessage ? 8e3 : 3e3);
4940
5266
  setStep(conflict ? "conflicts" : "pick");
4941
5267
  } finally {
4942
5268
  setBusy(false);
@@ -4963,7 +5289,8 @@ function ShareCopyModal({
4963
5289
  setBusy(true);
4964
5290
  await doCopy(newProject.id, {}, []);
4965
5291
  } catch (err) {
4966
- showToast(err.message || "Failed to create project", "error");
5292
+ const quotaMessage = storageQuotaUpgradeMessage(err);
5293
+ showToast(quotaMessage ?? err.message ?? "Failed to create project", "error", quotaMessage ? 8e3 : 3e3);
4967
5294
  }
4968
5295
  }, [createProject, doCopy]);
4969
5296
  const handleCopyWithResolutions = reactExports.useCallback(() => {
@@ -5200,26 +5527,8 @@ function ShareCopyModal({
5200
5527
  alignItems: "center"
5201
5528
  },
5202
5529
  children: [
5203
- /* @__PURE__ */ jsxRuntimeExports.jsx(
5204
- "button",
5205
- {
5206
- className: "fc-btn",
5207
- onClick: onClose,
5208
- style: { padding: "4px 10px", fontSize: 12 },
5209
- disabled: busy,
5210
- children: "Cancel"
5211
- }
5212
- ),
5213
- /* @__PURE__ */ jsxRuntimeExports.jsx(
5214
- "button",
5215
- {
5216
- className: "fc-btn",
5217
- onClick: handleNewProject,
5218
- style: { padding: "4px 10px", fontSize: 12 },
5219
- disabled: busy,
5220
- children: "+ New Project"
5221
- }
5222
- )
5530
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "fc-btn", onClick: onClose, style: { padding: "4px 10px", fontSize: 12 }, disabled: busy, children: "Cancel" }),
5531
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "fc-btn", onClick: handleNewProject, style: { padding: "4px 10px", fontSize: 12 }, disabled: busy, children: "+ New Project" })
5223
5532
  ]
5224
5533
  }
5225
5534
  )
@@ -7678,22 +7987,6 @@ function JointControls({
7678
7987
  ] });
7679
7988
  }
7680
7989
  const DEFAULT_OBJECT_SETTINGS$1 = { visible: true, opacity: 1, color: "#5b9bd5" };
7681
- const cleanTreeSegments = (segments) => (segments ?? []).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
7682
- const getObjectTreePath = (object) => {
7683
- var _a;
7684
- const explicitTreePath = cleanTreeSegments(object.treePath);
7685
- if (explicitTreePath.length > 0) return explicitTreePath;
7686
- const name = object.name.trim() || object.id;
7687
- const groupName = (_a = object.groupName) == null ? void 0 : _a.trim();
7688
- if (!groupName) return [name];
7689
- const groupPath = groupName.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
7690
- const prefixedLeaf = `${groupName}.`;
7691
- if (name.startsWith(prefixedLeaf)) {
7692
- const leafName = name.slice(prefixedLeaf.length).trim();
7693
- return [...groupPath, leafName || name];
7694
- }
7695
- return [...groupPath, name];
7696
- };
7697
7990
  const createMutableObjectGroup = (label, path) => ({
7698
7991
  kind: "group",
7699
7992
  key: `group:${path.join(" > ")}`,
@@ -7778,9 +8071,19 @@ function ObjectTree({
7778
8071
  focusObject,
7779
8072
  clearFocusedObject,
7780
8073
  setHoveredObjectId,
7781
- setConstructionGhost
8074
+ setConstructionGhost,
8075
+ searchQuery = ""
7782
8076
  }) {
7783
- const objectTree = reactExports.useMemo(() => buildObjectTree(objects), [objects]);
8077
+ const trimmedSearchQuery = searchQuery.trim();
8078
+ const filteredObjects = reactExports.useMemo(() => {
8079
+ if (!trimmedSearchQuery) return objects;
8080
+ const itemsById = new Map(objects.map((object) => [object.id, object]));
8081
+ return rankObjectSearchItems(
8082
+ objects.map((object) => buildObjectSearchItem(object)),
8083
+ trimmedSearchQuery
8084
+ ).map((item) => itemsById.get(item.id)).filter((object) => Boolean(object));
8085
+ }, [objects, trimmedSearchQuery]);
8086
+ const objectTree = reactExports.useMemo(() => buildObjectTree(filteredObjects), [filteredObjects]);
7784
8087
  const getObjectVisibilityState = (ids) => {
7785
8088
  let visibleCount = 0;
7786
8089
  ids.forEach((id) => {
@@ -7972,6 +8275,7 @@ function ObjectTree({
7972
8275
  },
7973
8276
  children: [
7974
8277
  objects.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 12, color: "var(--fc-textDim)", padding: "6px 0" }, children: "No objects loaded" }),
8278
+ objects.length > 0 && filteredObjects.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 12, color: "var(--fc-textDim)", padding: "6px 0" }, children: "No matching objects" }),
7975
8279
  objectTree.map(renderObjectTreeNode)
7976
8280
  ]
7977
8281
  }
@@ -8015,7 +8319,9 @@ function useViewPanelState() {
8015
8319
  const setShowPerformanceInfo = useForgeStore((s) => s.setShowPerformanceInfo);
8016
8320
  const disableRunCache = useForgeStore((s) => s.disableRunCache);
8017
8321
  const setDisableRunCache = useForgeStore((s) => s.setDisableRunCache);
8018
- const result = useForgeStore((s) => s.lastValidResult);
8322
+ const latestResult = useForgeStore((s) => s.result);
8323
+ const renderedResult = useForgeStore((s) => s.lastValidResult);
8324
+ const result = (latestResult == null ? void 0 : latestResult.objects.length) ? latestResult : renderedResult;
8019
8325
  const objectSettings = useForgeStore((s) => s.objectSettings);
8020
8326
  const setObjectVisibility = useForgeStore((s) => s.setObjectVisibility);
8021
8327
  const setObjectsVisibility = useForgeStore((s) => s.setObjectsVisibility);
@@ -8289,6 +8595,7 @@ function ToggleRow({ children }) {
8289
8595
  function ViewPanel() {
8290
8596
  var _a;
8291
8597
  const [activeTab, setActiveTab] = reactExports.useState("view");
8598
+ const [objectSearchQuery, setObjectSearchQuery] = reactExports.useState("");
8292
8599
  const state = useViewPanelState();
8293
8600
  const {
8294
8601
  activeBackend,
@@ -8489,7 +8796,33 @@ function ViewPanel() {
8489
8796
  ] })
8490
8797
  ] });
8491
8798
  const renderObjectsTab = () => /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
8492
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fc-view-panel-section fc-view-panel-section--flush-bottom", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fc-view-panel-section-title", children: "Objects" }) }),
8799
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-section fc-view-panel-section--flush-bottom", children: [
8800
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fc-view-panel-section-title", children: "Objects" }),
8801
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-view-panel-search-row", children: [
8802
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
8803
+ "input",
8804
+ {
8805
+ className: "fc-view-panel-input fc-view-panel-search-input",
8806
+ type: "search",
8807
+ value: objectSearchQuery,
8808
+ onChange: (event) => setObjectSearchQuery(event.target.value),
8809
+ placeholder: "Search objects...",
8810
+ "aria-label": "Search model objects"
8811
+ }
8812
+ ),
8813
+ objectSearchQuery && /* @__PURE__ */ jsxRuntimeExports.jsx(
8814
+ "button",
8815
+ {
8816
+ type: "button",
8817
+ className: "fc-btn fc-view-panel-search-clear",
8818
+ onClick: () => setObjectSearchQuery(""),
8819
+ "aria-label": "Clear object search",
8820
+ title: "Clear",
8821
+ children: "x"
8822
+ }
8823
+ )
8824
+ ] })
8825
+ ] }),
8493
8826
  /* @__PURE__ */ jsxRuntimeExports.jsx(
8494
8827
  ObjectTree,
8495
8828
  {
@@ -8506,7 +8839,8 @@ function ViewPanel() {
8506
8839
  focusObject,
8507
8840
  clearFocusedObject,
8508
8841
  setHoveredObjectId,
8509
- setConstructionGhost
8842
+ setConstructionGhost,
8843
+ searchQuery: objectSearchQuery
8510
8844
  }
8511
8845
  ),
8512
8846
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -8661,8 +8995,8 @@ function ViewPanel() {
8661
8995
  {
8662
8996
  type: "range",
8663
8997
  min: 0,
8664
- max: 0.12,
8665
- step: 0.01,
8998
+ max: 0.04,
8999
+ step: 5e-3,
8666
9000
  value: sectionPlaneFillOpacity,
8667
9001
  onChange: (event) => setSectionPlaneFillOpacity(Number(event.target.value)),
8668
9002
  disabled: !sectionPlaneGuidesEnabled || !sectionPlaneFillEnabled
@@ -9100,7 +9434,7 @@ function MobileCommandPalette({ onClose, onOpenFilePicker, onOpenJoints }) {
9100
9434
  const match = input.match(/gist\.github\.com\/(?:[^/]+\/)?([a-f0-9]+)/i);
9101
9435
  const gistId = match ? match[1] : input.trim();
9102
9436
  __vitePreload(async () => {
9103
- const { fetchGistModel: fetchGistModel2 } = await import("./app-BdBoMQeO.js").then((n) => n.ah);
9437
+ const { fetchGistModel: fetchGistModel2 } = await import("./app-CWucmnLZ.js").then((n) => n.am);
9104
9438
  return { fetchGistModel: fetchGistModel2 };
9105
9439
  }, true ? __vite__mapDeps([0]) : void 0).then(
9106
9440
  ({ fetchGistModel: fetchGistModel2 }) => fetchGistModel2(gistId).then((model) => {
@@ -9118,7 +9452,7 @@ function MobileCommandPalette({ onClose, onOpenFilePicker, onOpenJoints }) {
9118
9452
  const input = window.prompt("Paste a URL to a .forge.js file:");
9119
9453
  if (!input) return;
9120
9454
  __vitePreload(async () => {
9121
- const { fetchUrlModel: fetchUrlModel2 } = await import("./app-BdBoMQeO.js").then((n) => n.ah);
9455
+ const { fetchUrlModel: fetchUrlModel2 } = await import("./app-CWucmnLZ.js").then((n) => n.am);
9122
9456
  return { fetchUrlModel: fetchUrlModel2 };
9123
9457
  }, true ? __vite__mapDeps([0]) : void 0).then(
9124
9458
  ({ fetchUrlModel: fetchUrlModel2 }) => fetchUrlModel2(input.trim()).then((model) => {
@@ -10977,12 +11311,14 @@ function MobileViewport({ jointMatrices, onShare, onExport }) {
10977
11311
  useForgeStore((s) => s.isEvaluating);
10978
11312
  const objectSettings = useForgeStore((s) => s.objectSettings);
10979
11313
  const focusedObjectIds = useForgeStore((s) => s.focusedObjectIds);
11314
+ const selectObject = useForgeStore((s) => s.selectObject);
10980
11315
  const focusObject = useForgeStore((s) => s.focusObject);
10981
11316
  const clearFocusedObject = useForgeStore((s) => s.clearFocusedObject);
10982
11317
  const viewCommand = useForgeStore((s) => s.viewCommand);
10983
11318
  const requestViewCommand = useForgeStore((s) => s.requestViewCommand);
10984
11319
  const clearViewCommand = useForgeStore((s) => s.clearViewCommand);
10985
11320
  const objects = reactExports.useMemo(() => (result == null ? void 0 : result.objects) ?? [], [result]);
11321
+ const renderLabels = reactExports.useMemo(() => (result == null ? void 0 : result.renderLabels) ?? [], [result]);
10986
11322
  const sceneConfig = reactExports.useMemo(() => (result == null ? void 0 : result.sceneConfig) ?? null, [result]);
10987
11323
  const controlsRef = reactExports.useRef(null);
10988
11324
  const [isInteracting, setIsInteracting] = reactExports.useState(false);
@@ -11117,6 +11453,7 @@ function MobileViewport({ jointMatrices, onShare, onExport }) {
11117
11453
  obj.id
11118
11454
  );
11119
11455
  }),
11456
+ /* @__PURE__ */ jsxRuntimeExports.jsx(RenderLabelsOverlay, { labels: renderLabels }),
11120
11457
  /* @__PURE__ */ jsxRuntimeExports.jsx(
11121
11458
  Grid,
11122
11459
  {
@@ -11161,6 +11498,17 @@ function MobileViewport({ jointMatrices, onShare, onExport }) {
11161
11498
  onClearFocus: handleClearFocus
11162
11499
  }
11163
11500
  ),
11501
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
11502
+ ModelJourneyBar,
11503
+ {
11504
+ journeys: sceneConfig == null ? void 0 : sceneConfig.journeys,
11505
+ isViewportInteracting: isInteracting,
11506
+ requestViewCommand,
11507
+ selectObject,
11508
+ focusObject,
11509
+ clearFocusedObject
11510
+ }
11511
+ ),
11164
11512
  tappedObj && /* @__PURE__ */ jsxRuntimeExports.jsx(
11165
11513
  MobileObjectInfoPanel,
11166
11514
  {
@@ -12103,6 +12451,7 @@ function FullApp({ autoOpenShareCopy, onShareCopyComplete } = {}) {
12103
12451
  ] }),
12104
12452
  /* @__PURE__ */ jsxRuntimeExports.jsx(StatusBar, {}),
12105
12453
  /* @__PURE__ */ jsxRuntimeExports.jsx(CommandPalette, {}),
12454
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ObjectSearchPalette, {}),
12106
12455
  /* @__PURE__ */ jsxRuntimeExports.jsx(FileSwitcher, {}),
12107
12456
  /* @__PURE__ */ jsxRuntimeExports.jsx(KeyboardShortcutsOverlay, {}),
12108
12457
  /* @__PURE__ */ jsxRuntimeExports.jsx(SharePopover, {}),