forgecad 0.1.0

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 (119) hide show
  1. package/LICENSE +97 -0
  2. package/README.md +354 -0
  3. package/dist/assets/evalWorker-BYHXxh15.js +461 -0
  4. package/dist/assets/index--CYbOPKS.js +5797 -0
  5. package/dist/assets/manifold-65fIQlgQ.js +20 -0
  6. package/dist/assets/manifold-B85M7kop.js +20 -0
  7. package/dist/assets/manifold-B8h_vZ5O.js +16 -0
  8. package/dist/assets/manifold-D9yvTBHx.wasm +0 -0
  9. package/dist/assets/manifold-d1UpyLJ8.js +20 -0
  10. package/dist/assets/reportWorker-B1Zdrz9l.js +494 -0
  11. package/dist/index.html +16 -0
  12. package/dist-cli/forgecad.js +44464 -0
  13. package/dist-skill/SKILL.md +4635 -0
  14. package/examples/3d-printer.forge.js +328 -0
  15. package/examples/5-figen-robot-hand.forge.js +283 -0
  16. package/examples/ac-unit-glm47.forge.js +108 -0
  17. package/examples/ac-unit-glm5.forge.js +174 -0
  18. package/examples/ac-unit-kimi25.forge.js +236 -0
  19. package/examples/ac-unit-minimax.forge.js +123 -0
  20. package/examples/ac-unit.forge.js +126 -0
  21. package/examples/adjustable-table.forge.js +191 -0
  22. package/examples/api/assembly-gear-coupling.forge.js +32 -0
  23. package/examples/api/assembly-mechanism.forge.js +111 -0
  24. package/examples/api/attachTo-basics.forge.js +45 -0
  25. package/examples/api/benchy-style-hull.forge.js +89 -0
  26. package/examples/api/bill-of-materials.forge.js +46 -0
  27. package/examples/api/boolean-operations.forge.js +48 -0
  28. package/examples/api/bounding-box-visualizer.forge.js +58 -0
  29. package/examples/api/brep-exportable.forge.js +19 -0
  30. package/examples/api/center-true-vs-false.forge.js +40 -0
  31. package/examples/api/clone-duplicate.forge.js +41 -0
  32. package/examples/api/colors-union-vs-array.forge.js +27 -0
  33. package/examples/api/coordinate-system.forge.js +54 -0
  34. package/examples/api/curves-surfacing-basics.forge.js +91 -0
  35. package/examples/api/dimensioned-bracket.forge.js +19 -0
  36. package/examples/api/elbow-test.forge.js +23 -0
  37. package/examples/api/exploded-view.forge.js +60 -0
  38. package/examples/api/extrude-options.forge.js +44 -0
  39. package/examples/api/face-gears.forge.js +44 -0
  40. package/examples/api/face-transformation-history.forge.js +45 -0
  41. package/examples/api/feature-created-faces.forge.js +47 -0
  42. package/examples/api/folded-service-panel-cover.forge.js +3 -0
  43. package/examples/api/folded-service-panel-cover.js +117 -0
  44. package/examples/api/gears-bevel-face-joints.forge.js +157 -0
  45. package/examples/api/gears-tier1.forge.js +57 -0
  46. package/examples/api/geometry-info.forge.js +49 -0
  47. package/examples/api/group-test.forge.js +34 -0
  48. package/examples/api/group-vs-union.forge.js +25 -0
  49. package/examples/api/import-args-unit.forge.js +5 -0
  50. package/examples/api/import-args.forge.js +16 -0
  51. package/examples/api/import-dimensions-follow.forge.js +18 -0
  52. package/examples/api/import-placement-references.forge.js +18 -0
  53. package/examples/api/import-placement-widget-source.forge.js +30 -0
  54. package/examples/api/import-relative-paths.forge.js +18 -0
  55. package/examples/api/import-svg-sketch-shape.svg +15 -0
  56. package/examples/api/import-svg-sketch.forge.js +28 -0
  57. package/examples/api/js-module-imports.forge.js +9 -0
  58. package/examples/api/js-module-pillars.js +25 -0
  59. package/examples/api/js-module-scene.js +9 -0
  60. package/examples/api/notebook-assembly-debug.forge-notebook.json +90 -0
  61. package/examples/api/notebook-iteration.forge-notebook.json +75 -0
  62. package/examples/api/patterns.forge.js +32 -0
  63. package/examples/api/pointAlong-orientation.forge.js +52 -0
  64. package/examples/api/profile-2020-b-slot6.forge.js +36 -0
  65. package/examples/api/rotate-around-to.forge.js +31 -0
  66. package/examples/api/runtime-joints-view.forge.js +116 -0
  67. package/examples/api/sdf-rover-demo.forge.js +159 -0
  68. package/examples/api/section-plane-visualization.forge.js +38 -0
  69. package/examples/api/sketch-basics.forge.js +48 -0
  70. package/examples/api/sketch-on-face.forge.js +56 -0
  71. package/examples/api/sketch-rounding-strategies.forge.js +56 -0
  72. package/examples/api/spatial-recipes.forge.js +129 -0
  73. package/examples/bathroom.forge.js +197 -0
  74. package/examples/bolt-and-nut.forge.js +39 -0
  75. package/examples/bolt-pattern.forge.js +18 -0
  76. package/examples/bottle.forge.js +101 -0
  77. package/examples/chair.forge.js +62 -0
  78. package/examples/chess-set.forge.js +232 -0
  79. package/examples/classical-piano.forge.js +203 -0
  80. package/examples/clock.forge.js +169 -0
  81. package/examples/compiler-corpus/README.md +88 -0
  82. package/examples/compiler-corpus/edge-finished-mount.forge.js +18 -0
  83. package/examples/compiler-corpus/enclosure-shell-cuts.forge.js +24 -0
  84. package/examples/compiler-corpus/fastener-plate-variants.forge.js +42 -0
  85. package/examples/compiler-corpus/folded-service-panel-cover.forge.js +5 -0
  86. package/examples/compiler-corpus/motor-mount-plate.forge.js +32 -0
  87. package/examples/compiler-corpus/projection-relay-cover.forge.js +16 -0
  88. package/examples/compiler-corpus/sensor-bracket.forge.js +35 -0
  89. package/examples/compiler-corpus/service-panel-cover.forge.js +53 -0
  90. package/examples/compiler-corpus/trimmed-access-cover.forge.js +26 -0
  91. package/examples/cup.forge.js +25 -0
  92. package/examples/cut-plane-demo.forge.js +28 -0
  93. package/examples/door-with-hinges.forge.js +54 -0
  94. package/examples/frame.sketch.js +4 -0
  95. package/examples/headphone-hanger-profile.sketch.js +18 -0
  96. package/examples/headphone-hanger-v2.forge.js +88 -0
  97. package/examples/headphone-hanger.forge.js +5 -0
  98. package/examples/iphone-stand.forge.js +72 -0
  99. package/examples/iphone.forge.js +114 -0
  100. package/examples/ironman-helmet.js +79 -0
  101. package/examples/kitchen.forge.js +231 -0
  102. package/examples/lamp-shade.sketch.js +17 -0
  103. package/examples/laptop.forge.js +144 -0
  104. package/examples/liquid-soap-dispenser.forge.js +159 -0
  105. package/examples/modern-tv.forge.js +86 -0
  106. package/examples/picture-frame.forge.js +34 -0
  107. package/examples/robot_hand.forge.js +393 -0
  108. package/examples/robot_hand_2.forge.js +622 -0
  109. package/examples/sandbox.forge.js +3 -0
  110. package/examples/shelf/container.forge.js +30 -0
  111. package/examples/shelf/shelf-unit.forge.js +62 -0
  112. package/examples/shoe-rack-doors.forge.js +107 -0
  113. package/examples/shoe-rack.forge.js +65 -0
  114. package/examples/spiderman-cake.forge.js +92 -0
  115. package/examples/table-lamp.forge.js +33 -0
  116. package/examples/table.forge.js +44 -0
  117. package/examples/test-colors.forge.js +19 -0
  118. package/examples/tv-stand.forge.js +21 -0
  119. package/package.json +69 -0
@@ -0,0 +1,4635 @@
1
+ ---
2
+ name: forgecad
3
+ description: ForgeCAD model authoring, editing, debugging, and execution guidance for .forge.js, .sketch.js, .forge-notebook.json, SVG-import, assembly, and CLI workflows. Use when Codex needs to build or modify ForgeCAD geometry, structure multi-file projects, run notebook cells, validate scripts, or use ForgeCAD export/render tooling.
4
+ ---
5
+
6
+ # ForgeCAD
7
+
8
+ Author or modify ForgeCAD models, sketches, assemblies, notebooks, and CLI workflows. Prefer documented primitives, import rules, placement strategies, and CLI commands over inventing new APIs.
9
+
10
+ ## Workflow
11
+
12
+ 1. Identify the artifact: `.forge.js`, `.sketch.js`, `.forge-notebook.json`, SVG asset, or CLI/export task.
13
+ 2. Use the reference sections below — all API docs are inlined. Start from Core API, add others as needed.
14
+ 3. Default to a concrete first pass — easy iteration beats speculative design review.
15
+ 4. If an existing model is broken, replace the weak structure rather than preserving bad architecture.
16
+ 5. Validate with `forgecad run <file>` (add `--debug-imports` for import chain issues). This works for notebook preview cells too.
17
+ 6. For `jointsView()` animations, keep wrapped revolute tracks continuous across branch cuts; do not assume the viewport will auto-fix `-180/180` jumps.
18
+
19
+ ### Import and Composition
20
+
21
+ - `importPart()` for parts, `importSketch()` for sketches/SVGs, with explicit `paramOverrides`.
22
+ - `.withReferences()` + `.placeReference()` for reusable placement.
23
+ - Plain `.js` modules for shared helpers/constants (not model imports).
24
+
25
+ ### Notebooks
26
+
27
+ Use `.forge-notebook.json` for stateful iteration and debugging. Cells share state, `show()` pins visible geometry, and the preview cell can be validated or rendered directly from the CLI.
28
+
29
+ Prefer notebooks when:
30
+
31
+ - the task is exploratory or the geometry strategy is still unclear
32
+ - you are debugging booleans, placements, or assembly kinematics
33
+ - you want to inspect intermediate shapes or sketches without rewriting the whole file
34
+
35
+ Useful notebook loop:
36
+
37
+ - keep stable setup in early cells and the current experiment in the preview cell
38
+ - use `show(...)` for intermediate geometry you want pinned in the viewport
39
+ - use `forgecad notebook view <file> preview` to inspect the notebook from the terminal
40
+ - use `forgecad run <file>.forge-notebook.json` for preview-cell validation and spatial analysis
41
+ - use `forgecad render <file>.forge-notebook.json` or `forgecad capture gif <file>.forge-notebook.json --list` to inspect the preview cell through the CLI
42
+ - export to `.forge.js` when the exploratory phase is over and the structure is ready to stabilize
43
+
44
+ ## API Reference
45
+
46
+ All documentation is inlined below. Read the relevant sections based on your task — start from Core API, add others as needed.
47
+
48
+ ### 1. Core API (always read first)
49
+
50
+ Primitives, transforms, booleans, imports, topology, return formats, curves/surfacing.
51
+
52
+ <!-- docs/permanent/API/model-building/reference.md -->
53
+
54
+ # ForgeCAD Model-Building Reference
55
+
56
+ This file covers the script authoring surface for geometry, sketches, topology, assemblies, imports, and reusable parts.
57
+
58
+ It intentionally excludes:
59
+ - viewport-only APIs in [../runtime/viewport.md](../runtime/viewport.md)
60
+ - report/export APIs in [../output/bom.md](../output/bom.md), [../output/dimensions.md](../output/dimensions.md), and [../output/brep-export.md](../output/brep-export.md)
61
+ - implementation notes in [../internals/manifold.md](../internals/manifold.md)
62
+
63
+ ## Core Concepts
64
+
65
+ ForgeCAD scripts are JavaScript code that returns geometry. The forge API is globally available — no imports needed.
66
+
67
+ ### Basic Structure
68
+ ```javascript
69
+ // 1. Declare parameters (creates UI sliders)
70
+ const width = param("Width", 50, { min: 20, max: 100, unit: "mm" });
71
+
72
+ // 2. Create geometry
73
+ const shape = box(width, 30, 10);
74
+
75
+ // 3. Return the final shape
76
+ return shape;
77
+ ```
78
+
79
+ ### Execution Model
80
+ - Scripts execute on every parameter change (400ms debounce)
81
+ - All operations are **immutable** — they return new shapes, never modify in place
82
+ - Must return one of:
83
+ - A `Shape` (3D solid)
84
+ - A `Sketch` (2D profile — rendered flat on XY plane)
85
+ - A `TrackedShape` (3D solid with named faces/edges — auto-unwrapped)
86
+ - A `ShapeGroup` (multiple shapes/sketches grouped for joint transforms)
87
+ - An `Array` of shapes/sketches/groups (multi-object scene)
88
+ - An `Array` of `{ name, shape?, sketch?, color? }` objects (named multi-object scene)
89
+
90
+ ### ⚠️ Important: Unions Remove Colors
91
+
92
+ When you use `union()` to combine shapes, the result becomes a single solid mesh with only one color. Individual colors assigned to the original shapes are lost:
93
+
94
+ ```javascript
95
+ // ❌ BAD: Colors are lost after union!
96
+ const red = box(30, 30, 30).color('#ff0000');
97
+ const blue = box(20, 20, 20).translate(30, 0, 0).color('#0066ff');
98
+ return union(red, blue); // Result is all one color (red)
99
+ ```
100
+
101
+ **Solution**: Return objects as a composite response instead:
102
+
103
+ ```javascript
104
+ // ✅ GOOD: Each object keeps its color
105
+ const red = box(30, 30, 30).color('#ff0000');
106
+ const blue = box(20, 20, 20).translate(30, 0, 0).color('#0066ff');
107
+
108
+ // Return as named objects to preserve individual colors and materials
109
+ return [
110
+ { "label": red }, // Each gets its own color, toggle, and controls
111
+ { "label": blue }
112
+ ];
113
+ ```
114
+
115
+ Each object in the returned array stays independently colorable and selectable, which is the main alternative to booleaning everything into one solid.
116
+
117
+ ### Coordinate System
118
+ ForgeCAD uses **Z-up** right-handed coordinates:
119
+ - **X** = left/right
120
+ - **Y** = forward/back
121
+ - **Z** = up/down
122
+
123
+ See [coordinate-system.md](coordinate-system.md) for view mapping details.
124
+
125
+ ## Parameters
126
+
127
+ ### `param(name, default, options?)`
128
+ Declares a parameter and creates a UI slider.
129
+
130
+ **Parameters:**
131
+ - `name` (string) - Display name in UI
132
+ - `default` (number) - Initial value
133
+ - `options` (object, optional):
134
+ - `min` (number) - Minimum value (default: 0)
135
+ - `max` (number) - Maximum value (default: default * 4)
136
+ - `step` (number) - Slider increment (auto-calculated if not provided)
137
+ - `unit` (string) - Display unit like "mm", "°", "%"
138
+ - `integer` (boolean) - If true, value is always rounded to whole number. Step defaults to 1. Use for counts, quantities, sides, etc.
139
+
140
+ **Returns:** Current parameter value (number)
141
+
142
+ **Examples:**
143
+ ```javascript
144
+ const width = param("Width", 50);
145
+ const angle = param("Angle", 45, { min: 0, max: 180, unit: "°" });
146
+ const thick = param("Thickness", 2, { min: 0.5, max: 10, step: 0.5, unit: "mm" });
147
+ const count = param("Count", 5, { min: 1, max: 20, integer: true });
148
+ ```
149
+
150
+ ## Colors
151
+
152
+ Both Shape and Sketch support colors via `.color()`:
153
+
154
+ ```javascript
155
+ const red = box(50, 50, 50).color('#ff0000');
156
+ const blue = circle2d(25).color('#0066ff');
157
+ ```
158
+
159
+ Colors are preserved through transforms and boolean operations (the first operand's color wins).
160
+
161
+ When returning multiple objects, colors can also be set per-object:
162
+
163
+ ```javascript
164
+ return [
165
+ { name: "Base", shape: box(100, 100, 5), color: "#888888" },
166
+ { name: "Column", shape: cylinder(50, 10).translate(50, 50, 5), color: "#4488cc" },
167
+ ];
168
+ ```
169
+
170
+ ## 3D Primitives
171
+
172
+ ### `box(x, y, z, center?)`
173
+ Creates a rectangular box with named faces and edges.
174
+
175
+ **Parameters:**
176
+ - `x, y, z` (number) - Dimensions
177
+ - `center` (boolean, optional) - If true, centers at origin. Default: false (corner at origin)
178
+
179
+ **Returns:** `TrackedShape` (with faces: top, bottom, side-left, side-right, side-top, side-bottom; edges: vert-bl, vert-br, vert-tr, vert-tl, etc.)
180
+
181
+ ```javascript
182
+ const cube = box(50, 50, 50, true); // Centered cube
183
+ const plate = box(100, 80, 5); // Corner at origin
184
+ plate.face('top'); // FaceRef { normal, center, planar, uAxis, vAxis }
185
+ plate.edge('vert-bl'); // EdgeRef { start, end }
186
+ ```
187
+
188
+ ### `cylinder(height, radius, radiusTop?, segments?, center?)`
189
+ Creates a cylinder or cone with named faces and edges.
190
+
191
+ **Parameters:**
192
+ - `height` (number) - Height along Z axis
193
+ - `radius` (number) - Bottom radius
194
+ - `radiusTop` (number, optional) - Top radius. If different from radius, creates a cone. Default: same as radius
195
+ - `segments` (number, optional) - Number of sides. Default: auto (smooth circle)
196
+ - `center` (boolean, optional) - If true, centers along Z. Default: false
197
+
198
+ **Returns:** `TrackedShape` (with faces: top, bottom, side; edges: top-rim, bottom-rim)
199
+
200
+ ```javascript
201
+ const cyl = cylinder(50, 10); // Cylinder
202
+ const cone = cylinder(50, 20, 5); // Cone (tapered)
203
+ const hex = cylinder(10, 15, 15, 6); // Hexagonal prism
204
+ cyl.face('top'); // FaceRef (planar)
205
+ cyl.face('side'); // FaceRef (curved, planar === false)
206
+ ```
207
+
208
+ ### `sphere(radius, segments?)`
209
+ Creates a sphere.
210
+
211
+ **Parameters:**
212
+ - `radius` (number) - Sphere radius
213
+ - `segments` (number, optional) - Tessellation detail. Default: auto (smooth)
214
+
215
+ **Returns:** `Shape`
216
+
217
+ ```javascript
218
+ const ball = sphere(25);
219
+ const lowPoly = sphere(25, 8); // Octahedron-like
220
+ ```
221
+
222
+ ## 3D Transforms
223
+
224
+ All transforms are **chainable** and **immutable** (return new shapes).
225
+ The core 3D transform set uses the same names/signatures on `Shape`, `TrackedShape`, and `ShapeGroup`.
226
+ For mixed groups that include `Sketch` children, `transform` / `rotateAround` / `pointAlong` are 3D-only.
227
+
228
+ ### `.clone()` / `.duplicate()`
229
+ Create an explicit copy handle of a shape (same geometry/color) so you can branch variants clearly.
230
+
231
+ ```javascript
232
+ const bracket = box(60, 20, 8);
233
+ const left = bracket.clone().translate(-40, 0, 0);
234
+ const right = bracket.duplicate().translate(40, 0, 0);
235
+ ```
236
+
237
+ ### `.translate(x, y, z)`
238
+ Moves the shape relative to its current position.
239
+
240
+ ```javascript
241
+ const moved = box(10, 10, 10).translate(50, 0, 0);
242
+ ```
243
+
244
+ ### `.moveTo(x, y, z)`
245
+ Positions the shape so its bounding box min corner is at the given global coordinate.
246
+
247
+ ```javascript
248
+ // Place a box at exactly (100, 50, 0) in world space
249
+ const placed = box(30, 30, 10).moveTo(100, 50, 0);
250
+ ```
251
+
252
+ ### `.moveToLocal(target, x, y, z)`
253
+ Positions the shape relative to another shape's local coordinate system (bounding box min corner).
254
+
255
+ **Parameters:**
256
+ - `target` (Shape | TrackedShape) — The reference shape
257
+ - `x, y, z` (number) — Offset from target's bounding box min corner
258
+
259
+ ```javascript
260
+ const base = box(100, 100, 10);
261
+ const part = box(20, 20, 30);
262
+
263
+ // Place part at (10, 10, 10) relative to base's origin corner
264
+ const placed = part.moveToLocal(base, 10, 10, 10);
265
+ ```
266
+
267
+ ### `.rotate(x, y, z)`
268
+ Rotates using Euler angles in **degrees**.
269
+
270
+ **Parameters:**
271
+ - `x, y, z` (number) - Rotation in degrees around each axis
272
+
273
+ ```javascript
274
+ const rotated = box(50, 20, 10).rotate(0, 0, 45); // 45° around Z
275
+ const tilted = cylinder(50, 10).rotate(90, 0, 0); // Lay on side
276
+ ```
277
+
278
+ ### `.scale(v)`
279
+ Scales the shape.
280
+
281
+ **Parameters:**
282
+ - `v` (number | [number, number, number]) - Uniform scale or per-axis scale
283
+
284
+ ```javascript
285
+ const bigger = sphere(10).scale(2); // 2x larger
286
+ const stretched = box(10, 10, 10).scale([2, 1, 0.5]); // Non-uniform
287
+ ```
288
+
289
+ ### `.mirror(normal)`
290
+ Mirrors across a plane defined by its normal vector.
291
+
292
+ **Parameters:**
293
+ - `normal` ([number, number, number]) - Plane normal (doesn't need to be unit length)
294
+
295
+ ```javascript
296
+ const mirrored = shape.mirror([1, 0, 0]); // Mirror across YZ plane
297
+ ```
298
+
299
+ ### `.transform(m)`
300
+ Applies a custom 4x4 transform matrix or `Transform` object.
301
+
302
+ **Parameters:**
303
+ - `m` (`number[] | Transform`) - 4x4 column-major matrix (`number[16]`) or a `Transform`
304
+
305
+ ```javascript
306
+ const T = Transform.identity()
307
+ .translate(0, 0, 1.5)
308
+ .rotateAxis([1, 0, 0], 35, [0, hingeY, 0]);
309
+
310
+ const moved = lid.transform(T);
311
+ ```
312
+
313
+ ### `.rotateAround(axis, angleDeg, pivot?)`
314
+ Rotates around an arbitrary axis through a pivot point.
315
+
316
+ **Parameters:**
317
+ - `axis` ([number, number, number]) - Rotation axis direction
318
+ - `angleDeg` (number) - Rotation angle in degrees
319
+ - `pivot` ([number, number, number], optional) - Pivot point. Default: origin
320
+
321
+ ```javascript
322
+ // Rotate a door 45° around Z axis at the hinge position
323
+ const opened = door.rotateAround([0, 0, 1], 45, [hingeX, hingeY, 0]);
324
+ ```
325
+
326
+ ### `.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)`
327
+ Solves the rotation around an axis that makes `movingPoint` reach the target line/plane implied by `targetPoint`.
328
+
329
+ **Parameters:**
330
+ - `axis` ([number, number, number]) - Rotation axis direction
331
+ - `pivot` ([number, number, number]) - Point on the rotation axis
332
+ - `movingPoint` (`[number, number, number] | string`) - World-space point or this shape's anchor/reference
333
+ - `targetPoint` (`[number, number, number] | string`) - World-space point or this shape's anchor/reference
334
+ - `opts` (object, optional)
335
+ - `mode` (`'plane' | 'line'`) - Default: `'plane'`
336
+
337
+ Modes:
338
+ - `'plane'` — rotate until `movingPoint` lies in the plane defined by `axis` + `targetPoint`
339
+ - `'line'` — rotate until `movingPoint` lies on the infinite line from `pivot` through `targetPoint`; throws if the geometry makes that impossible
340
+
341
+ ```javascript
342
+ const arm = box(80, 8, 8, true)
343
+ .translate(40, 0, 0)
344
+ .withReferences({ points: { tip: [80, 0, 0] } });
345
+
346
+ const aimed = arm.rotateAroundTo(
347
+ [0, 0, 1],
348
+ [0, 0, 0],
349
+ "tip",
350
+ [30, 30, 20],
351
+ );
352
+ ```
353
+
354
+ ### `.pointAlong(direction)`
355
+ Reorients a shape so its primary axis (Z) points along the given direction. Useful for laying cylinders and extrusions along X or Y without thinking about Euler angles.
356
+
357
+ **Parameters:**
358
+ - `direction` ([number, number, number]) - Target direction vector
359
+
360
+ ```javascript
361
+ // Lay a cylinder along the X axis
362
+ const axle = cylinder(100, 5).pointAlong([1, 0, 0]);
363
+
364
+ // Symmetric hinges pointing outward from center
365
+ const hingeL = cylinder(40, 5).pointAlong([-1, 0, 0]).translate(-50, 0, 0);
366
+ const hingeR = cylinder(40, 5).pointAlong([1, 0, 0]).translate(50, 0, 0);
367
+ ```
368
+
369
+ ### `Transform` primitives (for kinematic chains)
370
+ Use `Transform` when manual pivot math becomes hard to maintain.
371
+
372
+ ```javascript
373
+ const T = Transform.identity()
374
+ .translate(0, 0, 120)
375
+ .rotateAxis([0, 0, 1], 35);
376
+
377
+ const p = T.point([10, 0, 0]); // transform a point
378
+ const v = T.vector([1, 0, 0]); // transform a direction (no translation)
379
+ ```
380
+
381
+ Core methods:
382
+ - `Transform.identity()`
383
+ - `Transform.translation(x, y, z)`
384
+ - `Transform.rotationAxis(axis, angleDeg, pivot?)`
385
+ - `Transform.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)`
386
+ - `Transform.scale(v)`
387
+ - `T.mul(other)` (chain-composition order)
388
+ - `composeChain(a, b, c, ...)` explicit left-to-right chain composition
389
+ - `T.inverse()`
390
+ - `shape.transform(T)` / `trackedShape.transform(T)` / `group.transform(T)`
391
+
392
+ ## Joints
393
+
394
+ ### `joint(name, shape, pivot, opts?)`
395
+ Create a revolute (hinge) joint. Auto-creates a param slider and rotates the shape.
396
+
397
+ **Parameters:**
398
+ - `name` (string) - Display name for the angle parameter
399
+ - `shape` (Shape) - The shape to rotate
400
+ - `pivot` ([number, number, number]) - The pivot point
401
+ - `opts` (object, optional):
402
+ - `axis` ([number, number, number]) - Rotation axis. Default: [0, 0, 1] (Z axis)
403
+ - `min` (number) - Minimum angle. Default: 0
404
+ - `max` (number) - Maximum angle. Default: 360
405
+ - `default` (number) - Initial angle. Default: 0
406
+ - `unit` (string) - Display unit. Default: "°"
407
+
408
+ **Returns:** `Shape` (rotated by the current slider value)
409
+
410
+ ```javascript
411
+ // One line: creates a "Lid Angle" slider and rotates the lid around the hinge
412
+ const openLid = joint("Lid Angle", lid, [0, boxDepth, boxHeight], {
413
+ axis: [1, 0, 0],
414
+ max: 120,
415
+ default: 45,
416
+ });
417
+ ```
418
+
419
+ ## Assembly Graph (Mechanisms)
420
+
421
+ See also: `assembly.md`.
422
+
423
+ ### `assembly(name?)`
424
+ Creates an assembly container with named parts + joints.
425
+
426
+ ```javascript
427
+ const mech = assembly("Two-Link Arm")
428
+ .addPart("base", box(80, 80, 20, true))
429
+ .addPart("link1", box(120, 24, 24).translate(0, -12, -12))
430
+ .addPart("link2", box(100, 20, 20).translate(0, -10, -10))
431
+ .addJoint("shoulder", "revolute", "base", "link1", {
432
+ axis: [0, 1, 0],
433
+ min: -30, max: 120, default: 20,
434
+ frame: Transform.identity().translate(0, 0, 20),
435
+ })
436
+ .addJoint("elbow", "revolute", "link1", "link2", {
437
+ axis: [0, 1, 0],
438
+ min: -20, max: 140, default: 40,
439
+ frame: Transform.identity().translate(120, 0, 0),
440
+ });
441
+
442
+ const solved = mech.solve();
443
+ return solved.toScene();
444
+ ```
445
+
446
+ Linked joint example:
447
+
448
+ ```javascript
449
+ mech
450
+ .addJointCoupling("Top Gear", {
451
+ terms: [
452
+ { joint: "Steering", ratio: 1 },
453
+ { joint: "Wheel Drive", ratio: 20 / 14 },
454
+ ],
455
+ })
456
+ .addJointCoupling("Motor 1", {
457
+ terms: [{ joint: "Top Gear", ratio: -2 }],
458
+ });
459
+ ```
460
+
461
+ Gear helper example:
462
+
463
+ ```javascript
464
+ const pair = lib.gearPair({
465
+ pinion: { module: 1.25, teeth: 14, faceWidth: 8 },
466
+ gear: { module: 1.25, teeth: 42, faceWidth: 8 },
467
+ });
468
+
469
+ mech.addGearCoupling("Driven", "Pinion", { pair }); // uses pair.jointRatio
470
+ ```
471
+
472
+ Bevel/face helper example:
473
+
474
+ ```javascript
475
+ const bevel = lib.bevelGearPair({
476
+ pinion: { module: 1.5, teeth: 18, faceWidth: 10 },
477
+ gear: { module: 1.5, teeth: 36, faceWidth: 9 },
478
+ shaftAngleDeg: 90,
479
+ });
480
+
481
+ const face = lib.faceGearPair({
482
+ face: { module: 1.5, teeth: 44, faceWidth: 7, toothHeight: 1.2, side: 'top' },
483
+ vertical: { module: 1.5, teeth: 16, faceWidth: 8 },
484
+ });
485
+
486
+ mech.addGearCoupling("Bevel Driven", "Bevel Driver", { pair: bevel });
487
+ mech.addGearCoupling("Face Driven", "Face Driver", { pair: face });
488
+ ```
489
+
490
+ Key methods:
491
+ - `addPart(name, shape, { transform?, metadata? })`
492
+ - `addFrame(name, { transform? })` for virtual mechanism frames
493
+ - `addJoint(name, type, parent, child, opts)` where `type` is `'fixed' | 'revolute' | 'prismatic'`
494
+ - `addRevolute(...)`, `addPrismatic(...)`, `addFixed(...)` shorthand helpers
495
+ - `addJointCoupling(jointName, { terms, offset? })` for linked joints:
496
+ - `jointName`: driven joint
497
+ - `terms`: `[{ joint, ratio? }, ...]` where each source contributes `ratio * sourceValue` (default ratio `1`)
498
+ - `offset`: additive bias after term sum (default `0`)
499
+ - `addGearCoupling(drivenJoint, driverJoint, opts)` for revolute gear meshes:
500
+ - ratio source (exactly one): `ratio`, `pair` (`pair.jointRatio`), or `driverTeeth + drivenTeeth`
501
+ - `mesh`: `'external' | 'internal' | 'bevel' | 'face'` (teeth mode only, default `'external'`; `bevel`/`face` follow external sign)
502
+ - `offset`: additive bias after gear ratio
503
+ - `solve(state?)` with per-joint value overrides
504
+ - `sweepJoint(jointName, from, to, steps, baseState?, collisionOptions?)`
505
+
506
+ Solved assembly helpers:
507
+ - `solved.toScene()` for rendering
508
+ - `solved.collisionReport()` for interference checks
509
+ - `solved.minClearance(partA, partB, searchLength?)`
510
+ - `solved.bom()` / `solved.bomCsv()`
511
+ - `bomToCsv(rows)` (standalone helper)
512
+
513
+ ### `robotExport(options)`
514
+ Declares that the current script should also export an `assembly(...)` as a robot package for the SDF CLI.
515
+
516
+ Key fields:
517
+ - `assembly`: required source assembly graph
518
+ - `modelName`: simulator-facing model name
519
+ - `links.<part>.massKg` or `densityKgM3`: inertial hints
520
+ - `joints.<joint>.effort|velocity|damping|friction`: simulator tuning
521
+ - `plugins.diffDrive`: diff-drive plugin wiring for Gazebo
522
+ - `world.generateDemoWorld`: emit a simple obstacle-course world alongside the model
523
+
524
+ Example:
525
+
526
+ ```javascript
527
+ robotExport({
528
+ assembly: mech,
529
+ modelName: "Forge Scout",
530
+ links: {
531
+ Base: { massKg: 12 },
532
+ },
533
+ plugins: {
534
+ diffDrive: {
535
+ leftJoints: ["leftFront", "leftRear"],
536
+ rightJoints: ["rightFront", "rightRear"],
537
+ wheelSeparationMm: 320,
538
+ wheelRadiusMm: 72,
539
+ },
540
+ },
541
+ });
542
+ ```
543
+
544
+ ## 3D Boolean Operations
545
+
546
+ ### `union(...shapes)`
547
+ Combines shapes (additive).
548
+
549
+ ```javascript
550
+ const combined = union(
551
+ box(50, 50, 10),
552
+ cylinder(20, 15).translate(25, 25, 10)
553
+ );
554
+
555
+ const parts = [
556
+ box(50, 50, 10),
557
+ cylinder(20, 15).translate(25, 25, 10),
558
+ ];
559
+ const combinedFromArray = union(parts);
560
+ ```
561
+
562
+ ### `difference(...shapes)`
563
+ Subtracts shapes[1..n] from shapes[0].
564
+
565
+ ```javascript
566
+ const plate = box(100, 100, 5);
567
+ const hole1 = cylinder(6, 10).translate(25, 50, 0);
568
+ const hole2 = cylinder(6, 10).translate(75, 50, 0);
569
+ const result = difference(plate, hole1, hole2);
570
+ const resultFromArray = difference([plate, hole1, hole2]);
571
+
572
+ // Or using method syntax:
573
+ const result = plate.subtract(hole1, hole2);
574
+ const sameResult = plate.subtract([hole1, hole2]);
575
+ ```
576
+
577
+ ### `intersection(...shapes)`
578
+ Keeps only overlapping volume.
579
+
580
+ ```javascript
581
+ const overlap = intersection(
582
+ sphere(30),
583
+ box(40, 40, 40, true)
584
+ );
585
+
586
+ const overlapFromArray = intersection([
587
+ sphere(30),
588
+ box(40, 40, 40, true),
589
+ ]);
590
+ ```
591
+
592
+ ### Method Syntax
593
+ Shapes also have boolean methods:
594
+
595
+ ```javascript
596
+ shape.add(other1, other2)
597
+ shape.add([other1, other2])
598
+ shape.subtract(other1, other2)
599
+ shape.subtract([other1, other2])
600
+ shape.intersect(other1, other2)
601
+ shape.intersect([other1, other2])
602
+ ```
603
+
604
+ ## Group
605
+
606
+ ### `group(...items)`
607
+ Groups multiple shapes/sketches for joint transforms without merging them into a single mesh. Unlike `union`, colors and individual identities are preserved.
608
+
609
+ **Parameters:**
610
+ - `...items` (Shape | Sketch | TrackedShape | ShapeGroup | `{ name, shape? | sketch? | group? }`) - Items to group (nested groups allowed)
611
+
612
+ **Returns:** `ShapeGroup`
613
+
614
+ ```javascript
615
+ const base = box(100, 100, 5).color('#888888');
616
+ const column = cylinder(40, 5).translate(50, 50, 5).color('#4488cc');
617
+
618
+ // Group them — they stay separate but transform together
619
+ const assembly = group(base, column).translate(200, 0, 0);
620
+ return assembly;
621
+ ```
622
+
623
+ Named child descriptors are useful when the group will be flattened later, especially inside assemblies:
624
+
625
+ ```javascript
626
+ const shell = box(80, 60, 24).color('#6e7b88');
627
+ const lid = box(80, 60, 4).translate(0, 0, 24).color('#c9d2db');
628
+
629
+ const housing = group(
630
+ { name: 'Shell', shape: shell },
631
+ { name: 'Lid', shape: lid },
632
+ );
633
+ ```
634
+
635
+ When that group is returned directly, each named child keeps its own viewport object. When the group is used as an assembly part, Forge uses those child names to produce labels such as `Base Assembly.Lid` instead of `Base Assembly.2`.
636
+
637
+ ### ShapeGroup Methods
638
+ All transforms are chainable and return a new ShapeGroup:
639
+
640
+ ```javascript
641
+ group.translate(x, y, z)
642
+ group.moveTo(x, y, z)
643
+ group.moveToLocal(target, x, y, z)
644
+ group.rotate(x, y, z)
645
+ group.rotateAround(axis, angleDeg, pivot?)
646
+ group.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)
647
+ group.pointAlong(direction)
648
+ group.transform(m)
649
+ group.scale(v)
650
+ group.mirror(normal)
651
+ group.color(hex) // applies to all children
652
+ group.clone()
653
+ group.duplicate() // alias
654
+ ```
655
+
656
+ `group.rotateAround(...)` is convenience sugar for `group.transform(Transform.rotationAxis(...))`.
657
+ `group.rotateAroundTo(...)` is convenience sugar for `group.transform(Transform.rotateAroundTo(...))`.
658
+ `group.pointAlong(...)` is convenience sugar for a group-wide axis rotation from Z to `direction`.
659
+
660
+ ```javascript
661
+ const hingeY = 40;
662
+ const lid = group(shell, logo);
663
+
664
+ const openedA = lid.rotateAround([1, 0, 0], 35, [0, hingeY, 0]); // sugar
665
+ const openedB = lid.transform(Transform.rotationAxis([1, 0, 0], 35, [0, hingeY, 0])); // equivalent
666
+
667
+ const laidDown = lid.pointAlong([1, 0, 0]); // same intent as Shape/TrackedShape.pointAlong
668
+ ```
669
+
670
+ When a ShapeGroup is returned from a script, each child becomes a separate viewport object with its own visibility/color controls. Named children keep those names in the viewport/object tree.
671
+
672
+ ## 3D Anchor Positioning
673
+
674
+ ### `.attachTo(target, targetAnchor, selfAnchor?, offset?)`
675
+ Position a shape relative to another using named 3D anchor points based on bounding boxes.
676
+
677
+ Available on both `Shape` and `TrackedShape`.
678
+
679
+ **Parameters:**
680
+ - `target` (Shape | TrackedShape) — The shape to attach to
681
+ - `targetAnchor` (Anchor3D) — Point on target
682
+ - `selfAnchor` (Anchor3D, optional) — Point on this shape to align. Default: 'center'
683
+ - `offset` ([number, number, number], optional) — Additional offset after alignment
684
+
685
+ **Anchor3D values:**
686
+ - `'center'` — bounding box center
687
+ - Face centers (1 axis pinned): `'front'` (−Y), `'back'` (+Y), `'left'` (−X), `'right'` (+X), `'top'` (+Z), `'bottom'` (−Z)
688
+ - Edge midpoints (2 axes pinned): `'front-left'`, `'front-right'`, `'back-left'`, `'back-right'`, `'top-front'`, `'top-back'`, `'top-left'`, `'top-right'`, `'bottom-front'`, `'bottom-back'`, `'bottom-left'`, `'bottom-right'`
689
+ - True corners (3 axes pinned): `'top-front-left'`, `'top-front-right'`, `'top-back-left'`, `'top-back-right'`, `'bottom-front-left'`, `'bottom-front-right'`, `'bottom-back-left'`, `'bottom-back-right'`
690
+
691
+ Anchor word order is flexible for built-ins: `'front-left'` and `'left-front'` are treated the same.
692
+
693
+ **Returns:** Same type as caller (Shape or TrackedShape)
694
+
695
+ ```javascript
696
+ const base = box(100, 100, 10);
697
+ const column = cylinder(50, 8);
698
+
699
+ // Place column on top of base, centered
700
+ const placed = column.attachTo(base, 'top', 'bottom');
701
+
702
+ // Stack boxes: place b on top of a, aligned at back-left corner
703
+ const a = box(50, 50, 20);
704
+ const b = box(30, 30, 10);
705
+ const stacked = b.attachTo(a, 'top-back-left', 'bottom-back-left');
706
+
707
+ // Place with offset: center on top, then shift 10mm right
708
+ const shifted = column.attachTo(base, 'top', 'bottom', [10, 0, 0]);
709
+ ```
710
+
711
+ ### `.onFace(parent, face, opts?)`
712
+ Place a shape on a specific face of a parent shape. Think of it like sticking a label on a box surface.
713
+
714
+ **Parameters:**
715
+ - `parent` (Shape | TrackedShape) — The parent shape
716
+ - `face` ('front' | 'back' | 'left' | 'right' | 'top' | 'bottom') — Which face to place on
717
+ - `opts` (object, optional):
718
+ - `u` (number) — Horizontal offset within the face (from center). Default: 0
719
+ - `v` (number) — Vertical offset within the face (from center). Default: 0
720
+ - `protrude` (number) — How far the child sticks out from the face. Default: 0
721
+
722
+ **Face coordinate mapping (u, v):**
723
+ - front/back: u = left/right (X), v = up/down (Z)
724
+ - left/right: u = forward/back (Y), v = up/down (Z)
725
+ - top/bottom: u = left/right (X), v = forward/back (Y)
726
+
727
+ **Returns:** Same type as caller
728
+
729
+ ```javascript
730
+ const body = box(100, 40, 60, true);
731
+
732
+ // Vent on front face, centered, 15mm below center, protruding 2mm
733
+ const vent = box(80, 2, 12, true).color('#333')
734
+ .onFace(body, 'front', { v: -15, protrude: 2 });
735
+
736
+ // Display near top-right of front face
737
+ const display = box(35, 1.5, 8, true).color('#00ddee')
738
+ .onFace(body, 'front', { u: 20, v: 15, protrude: 1 });
739
+
740
+ // Fan on top, protruding 5mm
741
+ const fan = cylinder(10, 40).color('#333')
742
+ .onFace(body, 'top', { protrude: 5 });
743
+
744
+ // Side vent on left face
745
+ const sideVent = box(2, 30, 40, true).color('#666')
746
+ .onFace(body, 'left', { protrude: 1 });
747
+ ```
748
+
749
+ **When to use `onFace()` vs `attachTo()`:**
750
+ - `onFace()` — placing surface details (vents, displays, buttons, labels) on a parent body
751
+ - `attachTo()` — stacking independent parts (column on base, unit on wall)
752
+
753
+ ## Advanced 3D Operations
754
+
755
+ ### `hull3d(...args)`
756
+ Convex hull of multiple shapes and/or points.
757
+
758
+ ```javascript
759
+ const hull = hull3d(
760
+ sphere(10),
761
+ sphere(10).translate(50, 0, 0),
762
+ [25, 0, 30], // bare point
763
+ );
764
+ ```
765
+
766
+ ### `levelSet(sdf, bounds, edgeLength, level?)`
767
+ Create a shape from a signed distance function (SDF). Positive = inside.
768
+
769
+ ```javascript
770
+ const gyroid = levelSet(
771
+ ([x, y, z]) => Math.sin(x) * Math.cos(y) + Math.sin(y) * Math.cos(z) + Math.sin(z) * Math.cos(x),
772
+ { min: [-10, -10, -10], max: [10, 10, 10] },
773
+ 0.5, // edge length (resolution)
774
+ );
775
+ ```
776
+
777
+ ### Smoothing
778
+
779
+ ```javascript
780
+ // Mark edges for smoothing, then subdivide
781
+ const smooth = box(50, 50, 50, true)
782
+ .smoothOut(60) // edges sharper than 60° get smoothed
783
+ .refine(4); // subdivide 4 times
784
+
785
+ // Or refine by edge length / tolerance
786
+ shape.refineToLength(2); // max edge length 2mm
787
+ shape.refineToTolerance(0.1); // max deviation 0.1mm from smooth surface
788
+ ```
789
+
790
+ ### Cutting
791
+
792
+ ```javascript
793
+ // Split by another shape → [inside, outside]
794
+ const [inside, outside] = shape.split(cutter);
795
+
796
+ // Split by infinite plane → [positive side, opposite side]
797
+ const [above, below] = shape.splitByPlane([0, 0, 1], 10); // Z=10 plane
798
+
799
+ // Trim: keep the positive side of the plane
800
+ const trimmed = shape.trimByPlane([0, 0, 1], 10);
801
+ ```
802
+
803
+ #### `shape.shell(thickness, options?)`
804
+ Hollow out a supported solid by keeping the outside wall and subtracting an inward cavity.
805
+
806
+ Current compiler-backed `shell()` support is intentionally narrow:
807
+
808
+ - compile-covered `box()`, `cylinder()`, and straight `Sketch.extrude()` solids
809
+ - optional `openFaces: ['top' | 'bottom']`
810
+ - rigid transforms before shelling are preserved through both lowerers
811
+ - defended named-face queries on the resulting outer shell faces plus created inner faces where Forge can model them exactly
812
+
813
+ Not supported yet:
814
+
815
+ - tapered extrudes (`scaleTop`)
816
+ - scale transforms before shelling
817
+ - already-shelled results
818
+ - general boolean, `revolve()`, `loft()`, `sweep()`, `sphere()`, hull, and plane-trim bases
819
+
820
+ Forge keeps `shell` as semantic compiler intent, then lowers it into backend-supported boolean/extrude/cylinder plans for both Manifold and CadQuery/OCCT.
821
+
822
+ Supported named-face subset on the shell result:
823
+
824
+ - preserved outer faces stay queryable on defended bases
825
+ - created inner faces use names like `inner-top`, `inner-bottom`, `inner-side`, or `inner-side-right`
826
+ - downstream `onFace(result, 'inner-side-right', ...)` placement is defended on the planar members of that subset
827
+ - generic straight extrudes still shell exactly, but named created-face support is currently limited to the defended profile families Forge can model directly (`rect`, `roundedRect`, `circle`)
828
+
829
+ ```javascript
830
+ const cup = roundedRect(80, 50, 6, true)
831
+ .extrude(30)
832
+ .translate(4, -3, 2)
833
+ .shell(2.5, { openFaces: ['top'] });
834
+ ```
835
+
836
+ #### `shape.hole(face, options)`
837
+ Compiler-owned circular hole workflow anchored to a face/workplane query.
838
+
839
+ - `depth` omitted = through-hole
840
+ - `depth` provided = blind hole
841
+ - `upToFace` = stop on a queried planar face parallel to the hole direction
842
+ - `extent: { forward, reverse? }` = two-sided extent with forward/reverse termination options
843
+ - `u` / `v` place the hole in face-local coordinates
844
+ - `counterbore: { diameter, depth }` adds a wider cylindrical recess at the entry
845
+ - `countersink: { diameter, angleDeg? }` adds a conical entry (default `90°`)
846
+ - `thread: { designation?, pitch?, class?, handedness?, depth?, modeled? }` carries thread intent metadata
847
+ - `depth` and `upToFace` are mutually exclusive with `extent`
848
+ - `counterbore` and `countersink` are mutually exclusive
849
+
850
+ ```javascript
851
+ const block = roundedRect(90, 60, 8, true).extrude(24);
852
+ const exitFace = block.face('bottom');
853
+
854
+ const body = block
855
+ .hole('front', { diameter: 8, u: 0, v: 2 }) // through
856
+ .hole('top', { diameter: 6, u: -18, v: 10, depth: 10 }) // blind
857
+ .hole('top', {
858
+ diameter: 5,
859
+ u: 18,
860
+ v: 10,
861
+ upToFace: exitFace,
862
+ counterbore: { diameter: 9, depth: 4 },
863
+ });
864
+ ```
865
+
866
+ Supported subset:
867
+
868
+ - compile-covered target bodies
869
+ - canonical faces, tracked planar faces, and `FaceRef` targets
870
+ - through, blind, and planar `upToFace` circular holes
871
+ - two-sided extents with forward/reverse termination (blind or `upToFace` per side)
872
+ - counterbore and countersink entry variants on one-sided hole extents
873
+ - thread metadata for deferred thread intent (modeled threads not yet supported)
874
+ - exact lowering through both Manifold and CadQuery/OCCT
875
+ - created hole faces in the defended subset:
876
+ - `wall` on all supported hole variants
877
+ - `floor` on blind holes
878
+ - `cap` on reverse blind two-sided holes
879
+ - `counterbore-wall` and `counterbore-floor` on counterbored holes
880
+ - `countersink-wall` on countersunk holes
881
+ - preserved non-host faces stay queryable where Forge can defend them
882
+ - rewritten host/exit faces now stay queryable as defended descendant regions when Forge can keep one stable source surface/frame
883
+ - reusing the same `upToFace` stop plane through later rewrites is supported when you keep a `FaceRef` from the earlier face (`const exitFace = block.face('bottom')`)
884
+
885
+ Not supported yet:
886
+
887
+ - non-planar or non-parallel `upToFace` termination faces
888
+ - two-sided extents combined with counterbore or countersink heads
889
+ - modeled helical threads (pass thread metadata with `modeled: false` or omitted for deferred intent)
890
+ - combined counterbore+countersink heads
891
+ - drafted/tapered main holes
892
+ - segmented polygonal hole cutters
893
+ - runtime-only target bodies without compiler intent
894
+
895
+ #### `shape.cutout(sketch, options?)`
896
+ Compiler-owned cut-extrude workflow using a sketch already placed with `Sketch.onFace(...)`.
897
+
898
+ - `depth` omitted = through-cut
899
+ - `depth` provided = blind cut
900
+ - `upToFace` = stop on a queried planar face parallel to the cut direction
901
+ - `extent: { forward, reverse? }` = two-sided extent with forward/reverse termination options
902
+ - `taperScale` = draft angle for tapered cuts (uniform or `[x, y]` pair)
903
+ - the sketch must carry semantic workplane placement from `onFace(...)`
904
+
905
+ ```javascript
906
+ const block = roundedRect(90, 60, 8, true).extrude(24);
907
+ const exitFace = block.face('bottom');
908
+ const pocket = roundedRect(18, 10, 2, true)
909
+ .onFace(block, 'top', { u: 14, v: -8, selfAnchor: 'center' });
910
+
911
+ const body = block.cutout(pocket, { upToFace: exitFace });
912
+ ```
913
+
914
+ Supported subset:
915
+
916
+ - compile-covered sketch profiles that CadQuery/OCCT already supports
917
+ - sketches placed on queried faces via `onFace(...)`
918
+ - through, blind, and planar `upToFace` cut extents
919
+ - two-sided extents with forward/reverse termination
920
+ - tapered cuts via `taperScale` on circle, rect, and roundedRect profiles
921
+ - exact/runtime parity through the shared compiler node family
922
+ - created cut faces in the defended subset:
923
+ - `floor` on blind cuts
924
+ - `wall` on circular cuts
925
+ - `wall-bottom`, `wall-right`, `wall-top`, `wall-left` on rectangular and rounded-rectangle cuts
926
+ - `cap` on reverse blind two-sided cuts
927
+ - preserved non-host faces stay queryable where Forge can defend them
928
+ - rewritten host/exit faces now stay queryable as defended descendant regions when Forge can keep one stable source surface/frame
929
+ - reusing the same `upToFace` stop plane through later rewrites is supported when you keep a `FaceRef` from the earlier face
930
+
931
+ Not supported yet:
932
+
933
+ - free-floating sketches without face/workplane provenance
934
+ - non-planar or non-parallel `upToFace` termination faces
935
+ - two-sided extents combined with `taperScale`
936
+ - named created-wall support for arbitrary boolean/offset/projected cut profiles
937
+
938
+ #### `sheetMetal(options)`
939
+ Compiler-owned sheet-metal v1 builder.
940
+
941
+ `sheetMetal(...)` returns a `SheetMetalPart`, not a solid directly.
942
+
943
+ Core flow:
944
+
945
+ - define the base panel, thickness, bend radius, and explicit `kFactor`
946
+ - add `90°` edge flanges with `.flange(...)`
947
+ - add planar panel/flange cutouts with `.cutout(...)`
948
+ - materialize either `.folded()` or `.flatPattern()`
949
+
950
+ ```javascript
951
+ const cover = sheetMetal({
952
+ panel: { width: 180, height: 110 },
953
+ thickness: 1.5,
954
+ bendRadius: 2,
955
+ bendAllowance: { kFactor: 0.42 },
956
+ cornerRelief: { size: 4 },
957
+ })
958
+ .flange('top', { length: 18 })
959
+ .flange('right', { length: 18 })
960
+ .flange('bottom', { length: 18 })
961
+ .flange('left', { length: 18 })
962
+ .cutout('panel', rect(72, 36, true), { selfAnchor: 'center' })
963
+ .cutout('flange-right', roundedRect(26, 10, 5, true), { selfAnchor: 'center' });
964
+
965
+ const folded = cover.folded();
966
+ const flat = cover.flatPattern();
967
+ ```
968
+
969
+ Supported v1 subset:
970
+
971
+ - one base panel
972
+ - up to four `90°` edge flanges
973
+ - constant thickness
974
+ - explicit bend radius plus explicit K-factor bend metadata
975
+ - rectangular corner reliefs
976
+ - planar cutouts on the panel and existing flange regions
977
+ - defended semantic regions such as `panel`, `flange-right`, and `bend-right`
978
+
979
+ Not supported yet:
980
+
981
+ - arbitrary solid conversion into sheet metal
982
+ - hems, jogs, lofted bends, or broader miter-corner logic
983
+ - nonuniform thickness
984
+ - cutouts directly on bend regions
985
+
986
+ Use [sheet-metal.md](sheet-metal.md) for the full sheet-metal contract and the maintained `folded-service-panel-cover` proof model.
987
+
988
+ ### Warping
989
+
990
+ ```javascript
991
+ // Deform vertices with arbitrary function
992
+ const warped = box(50, 50, 50, true).warp(([x, y, z]) => {
993
+ // Twist around Z axis
994
+ const angle = z * 0.05;
995
+ const cos = Math.cos(angle), sin = Math.sin(angle);
996
+ const nx = x * cos - y * sin;
997
+ const ny = x * sin + y * cos;
998
+ return [nx, ny, z];
999
+ });
1000
+ ```
1001
+
1002
+ ### Plane Operations
1003
+
1004
+ ```javascript
1005
+ // Cross-section: intersect shape with a plane → Sketch
1006
+ const section = intersectWithPlane(shape, { plane: 'XY', offset: 10 });
1007
+
1008
+ // Project: flatten shape onto a plane → Sketch
1009
+ const shadow = projectToPlane(shape, { origin: [0, 0, 0], normal: [0, 0, 1] });
1010
+ ```
1011
+
1012
+ `projectToPlane()` always works as a runtime modeling utility.
1013
+
1014
+ Compiler-owned replay is narrower today:
1015
+ - supported for downstream exact export when the projected source reduces to one defended planar projection basis:
1016
+ - plain `extrude()` / `box()` / `cylinder()` sources in their native XY basis
1017
+ - `Sketch.onFace()`-placed straight extrusions on a matching parallel target plane
1018
+ - compatible `shell()` / through-`hole()` / through-`cutout()` descendants aligned to that same basis
1019
+ - boolean `union()` combinations of compatible projected operands, including mirrored/patterned descendants
1020
+ - aligned blind holes/cuts keep the same replayed silhouette, while aligned through holes/cuts subtract their projected profile from it
1021
+ - the target plane still has to stay parallel to that defended basis without introducing in-plane shear
1022
+ - boolean `difference()` / `intersection()` sources, trim/fillet/chamfer silhouette changes, and non-parallel host workplanes still return a usable runtime sketch but fall back to explicit compiler diagnostics instead of pretending the projection is exact-safe
1023
+
1024
+ ## 2D Sketches
1025
+
1026
+ This file intentionally avoids repeating full 2D API signatures that already live in dedicated docs.
1027
+
1028
+ Use these canonical files:
1029
+
1030
+ - [sketch-core.md](sketch-core.md) - `Sketch` basics, queries, anchors, color, clone/duplicate
1031
+ - [sketch-primitives.md](sketch-primitives.md) - `rect`, `circle2d`, `roundedRect`, `polygon`, `ngon`, `ellipse`, `slot`, `star`
1032
+ - [sketch-path.md](sketch-path.md) - `path()` builder and `stroke(...)`
1033
+ - [sketch-transforms.md](sketch-transforms.md) - translate/rotate/scale/mirror for sketches
1034
+ - [sketch-booleans.md](sketch-booleans.md) - `union2d`/`difference2d`/`intersection2d`/`hull2d` and method forms
1035
+ - [sketch-operations.md](sketch-operations.md) - `offset`, `filletCorners`, `simplify`, `warp`, hull
1036
+ - [sketch-on-face.md](sketch-on-face.md) - sketch placement on planar faces
1037
+ - [sketch-extrude.md](sketch-extrude.md) - `extrude` and `revolve`
1038
+ - [sketch-anchor.md](sketch-anchor.md) - 2D anchor-based positioning
1039
+ - [entities.md](entities.md) - constrained sketches, named entities, topology-aware utilities
1040
+
1041
+ Integration rule: start with the smallest relevant doc set and add more only when the task expands.
1042
+
1043
+ ### Curves & Surfacing
1044
+
1045
+ #### `spline2d(points, options?)`
1046
+ Build a smooth Catmull-Rom spline sketch from 2D control points.
1047
+
1048
+ **Options:**
1049
+ - `closed` (boolean) - Default: `true`
1050
+ - `tension` (number, 0..1) - Default: `0.5`
1051
+ - `samplesPerSegment` (number) - Default: `16`
1052
+ - `strokeWidth` (number) - Required when `closed: false` (creates a stroked solid)
1053
+ - `join` (`'Round' | 'Square'`) - Stroke corner style for open splines. Default: `'Round'`
1054
+
1055
+ ```javascript
1056
+ const closed = spline2d([[20,0],[12,10],[0,12],[-12,10],[-20,0],[0,-14]]);
1057
+ const openRail = spline2d([[0,0],[30,20],[70,10]], { closed: false, strokeWidth: 3 });
1058
+ ```
1059
+
1060
+ #### `spline3d(points, options?)` / `Curve3D`
1061
+ Create a reusable 3D spline curve object.
1062
+
1063
+ `Curve3D` methods:
1064
+ - `.sample(count?)`
1065
+ - `.sampleBySegment(samplesPerSegment?)`
1066
+ - `.pointAt(t)` where `t` is `[0..1]`
1067
+ - `.tangentAt(t)`
1068
+ - `.length(samples?)`
1069
+
1070
+ ```javascript
1071
+ const rail = spline3d(
1072
+ [[0,0,0], [20,10,30], [40,0,60]],
1073
+ { tension: 0.45 }
1074
+ );
1075
+ ```
1076
+
1077
+ #### `loft(profiles, heights, options?)`
1078
+ Loft between multiple sketches along Z stations.
1079
+
1080
+ This implementation interpolates signed-distance fields and meshes via level-set extraction, so profiles can differ in vertex count/topology. Forge now records that loft intent in the compiler graph as well, and compatible loft stacks can export through the CadQuery/OCCT exact route even though viewport/runtime geometry remains sampled.
1081
+
1082
+ Performance note: `loft()` is significantly heavier than primitive/extrude/revolve paths. Use loft only when profile interpolation is required. If your part is axis-symmetric (bottles, vases, knobs, lathe-style parts), prefer `revolve()` for much faster generation.
1083
+
1084
+ **Parameters:**
1085
+ - `profiles` (`Sketch[]`) - At least 2
1086
+ - `heights` (`number[]`) - Same length as `profiles`, strictly increasing
1087
+ - `options`:
1088
+ - `edgeLength` (number) - Mesh resolution
1089
+ - `boundsPadding` (number) - Extra level-set bounds padding
1090
+
1091
+ ```javascript
1092
+ const body = loft(
1093
+ [circle2d(20), roundedRect(30, 24, 6, true), circle2d(10)],
1094
+ [0, 40, 70],
1095
+ { edgeLength: 1.0 }
1096
+ );
1097
+ ```
1098
+
1099
+ #### `sweep(profile, path, options?)`
1100
+ Sweep a 2D profile along a 3D path (`Curve3D` or point polyline).
1101
+
1102
+ Performance note: `sweep()` also uses level-set meshing internally. Prefer direct primitives/extrude/revolve when they can express the same shape. Forge records the sampled sweep path in the compiler graph, so compatible sweeps can export through the CadQuery/OCCT exact route using that canonical path representation.
1103
+
1104
+ **Parameters:**
1105
+ - `profile` (`Sketch`) - Local cross-section in XY plane
1106
+ - `path` (`Curve3D | [x,y,z][]`)
1107
+ - `options`:
1108
+ - `samples` (number) - Sampling count for `Curve3D` paths (default `48`)
1109
+ - `edgeLength` (number) - Mesh resolution
1110
+ - `boundsPadding` (number) - Extra level-set bounds padding
1111
+ - `up` (`[x,y,z]`) - Preferred frame-up vector
1112
+
1113
+ ```javascript
1114
+ const tubePath = spline3d([[0,0,0], [20,0,20], [40,10,30]]);
1115
+ const tube = sweep(circle2d(3), tubePath, { samples: 36, edgeLength: 0.7 });
1116
+ ```
1117
+
1118
+ ## Entities, Constraints, and Patterns
1119
+
1120
+ `reference.md` delegates detailed entity/constraint coverage to [entities.md](entities.md) to avoid duplication.
1121
+
1122
+ Use [entities.md](entities.md) for:
1123
+
1124
+ - `Point2D`, `Line2D`, `Circle2D`, `Rectangle2D`
1125
+ - `TrackedShape` topology access (`face`, `edge`, `faceNames`, `edgeNames`, `toShape`)
1126
+ - `constrainedSketch()` and `Constraint.*`
1127
+ - `linearPattern`, `circularPattern`, `mirrorCopy`
1128
+ - `filletEdge`, `chamferEdge`, `arcBridgeBetweenRects`
1129
+ - Utility helpers like `degrees(...)` and `radians(...)`
1130
+
1131
+ ## Multi-File Projects
1132
+
1133
+ ForgeCAD supports multi-file projects. Files are either **sketches** (`.sketch.js`, return a `Sketch`), **parts** (`.forge.js`, return a `Shape` or `TrackedShape`), or **SVG assets** (`.svg`, parsed into a `Sketch`).
1134
+
1135
+ ### File Types
1136
+ - `*.sketch.js` — 2D sketch file; when used with `importSketch()`, must return a `Sketch`
1137
+ - `*.forge.js` — 3D Forge file; when used with `importPart()`, must return a `Shape` or `TrackedShape`
1138
+ - `*.svg` — vector artwork file, imported as sketch geometry
1139
+
1140
+ ### Import Path Resolution
1141
+ - `./file.forge.js`, `./file.sketch.js`, `./asset.svg` (and `../...`) resolve relative to the file that calls imports
1142
+ - Bare paths like `api/bracket.forge.js` resolve from the opened project root
1143
+ - Leading `/` is treated as project-root relative
1144
+
1145
+ ### `importSketch(fileName, paramOverridesOrSvgOptions?)`
1146
+ Imports a sketch and returns `Sketch`.
1147
+
1148
+ - For `*.sketch.js`: executes the file (must return `Sketch`)
1149
+ - For `*.svg`: parses vector geometry into a `Sketch`
1150
+
1151
+ **Parameters:**
1152
+ - `fileName` (string) — Import path (e.g. `"./profile.sketch.js"` or `"api/profile.sketch.js"`)
1153
+ - `paramOverridesOrSvgOptions` (optional object)
1154
+ - For `*.sketch.js`: import-time param overrides by param name
1155
+ - For `*.svg`: SVG import options (see `importSvgSketch`)
1156
+
1157
+ **Returns:** `Sketch`
1158
+
1159
+ ```javascript
1160
+ // In a .forge.js file:
1161
+ const profile = importSketch("bracket-profile.sketch.js", {
1162
+ "Width": 42,
1163
+ "Height": 18,
1164
+ });
1165
+ return profile.extrude(50);
1166
+ ```
1167
+
1168
+ ```javascript
1169
+ // Import SVG and keep only the largest connected region
1170
+ const logo = importSketch("assets/logo.svg", {
1171
+ include: "auto",
1172
+ regionSelection: "largest",
1173
+ flattenTolerance: 0.25,
1174
+ });
1175
+ return logo.extrude(2);
1176
+ ```
1177
+
1178
+ ### `importSvgSketch(fileName, options?)`
1179
+ Parses an SVG file and returns a `Sketch`.
1180
+
1181
+ **Parameters:**
1182
+ - `fileName` (string) — Import path to an `.svg`
1183
+ - `options` (optional object):
1184
+ - `include`: `'auto' | 'fill' | 'stroke' | 'fill-and-stroke'` (default: `'auto'`)
1185
+ - `regionSelection`: `'all' | 'largest'` (default: `'all'`)
1186
+ - `maxRegions`: number (largest-first cap)
1187
+ - `minRegionArea`: number
1188
+ - `minRegionAreaRatio`: number (fraction of largest region area)
1189
+ - `flattenTolerance`: number (curve discretization tolerance)
1190
+ - `arcSegments`: number (minimum arc segment count)
1191
+ - `scale`: number (uniform scale factor)
1192
+ - `maxWidth`: number (uniformly downscale to keep final sketch width within this limit)
1193
+ - `maxHeight`: number (uniformly downscale to keep final sketch height within this limit)
1194
+ - `centerOnOrigin`: boolean (default: `false`, recenters final sketch bounds center to `(0, 0)`)
1195
+ - `simplify`: number (final simplify tolerance, default: `0`)
1196
+ - `invertY`: boolean (default: `true`, converts SVG Y-down to CAD Y-up)
1197
+
1198
+ **Returns:** `Sketch`
1199
+
1200
+ ```javascript
1201
+ const badge = importSvgSketch("assets/badge.svg", {
1202
+ include: "fill-and-stroke",
1203
+ minRegionAreaRatio: 0.001,
1204
+ maxRegions: 8,
1205
+ maxWidth: 120,
1206
+ maxHeight: 80,
1207
+ centerOnOrigin: true,
1208
+ });
1209
+ return badge;
1210
+ ```
1211
+
1212
+ ### `importPart(fileName, paramOverrides?)`
1213
+ Executes another file and returns its result as a `Shape`. The target file may return either `Shape` or `TrackedShape` (tracked results are auto-unwrapped to `Shape`).
1214
+
1215
+ **Parameters:**
1216
+ - `fileName` (string) — Import path (e.g. `"./bracket.forge.js"` or `"api/bracket.forge.js"`)
1217
+ - `paramOverrides` (optional object) — Import-time parameter overrides by param name
1218
+
1219
+ **Returns:** `Shape` (chainable)
1220
+
1221
+ ```javascript
1222
+ // Assembly: import parts and position them
1223
+ const bracket = importPart("bracket.forge.js", { "Thickness": 4 });
1224
+ const bracket2 = importPart("bracket.forge.js", { "Thickness": 8 })
1225
+ .translate(100, 0, 0)
1226
+ .rotate(0, 0, 180);
1227
+
1228
+ return union(bracket, bracket2);
1229
+ ```
1230
+
1231
+ Imported parts can also carry named placement references:
1232
+
1233
+ ```javascript
1234
+ // widget.forge.js
1235
+ return union(base, post).withReferences({
1236
+ points: {
1237
+ mount: [0, -16, -4],
1238
+ },
1239
+ objects: {
1240
+ post,
1241
+ },
1242
+ });
1243
+ ```
1244
+
1245
+ ```javascript
1246
+ // assembly.forge.js
1247
+ const widget = importPart("widget.forge.js")
1248
+ .placeReference("mount", [120, 40, 0]);
1249
+
1250
+ const cap = box(18, 18, 8, true)
1251
+ .attachTo(widget, "objects.post.top", "bottom");
1252
+
1253
+ return [widget, cap];
1254
+ ```
1255
+
1256
+ ### Import Rules
1257
+ - Circular imports are detected and throw an error
1258
+ - Imported files can be instantiated multiple times
1259
+ - `paramOverrides` only affects that import call (other imports are independent)
1260
+ - Params supplied through `paramOverrides` are treated as fixed arguments for that import call
1261
+ - Relative imports (`./` / `../`) are resolved from the current file path
1262
+ - `importPart()` accepts imported `Shape` or `TrackedShape` results and always returns a chainable `Shape`
1263
+ - Source files can attach placement references with `.withReferences({ points, edges, surfaces, objects })`
1264
+ - Imported tracked solids keep their named faces/edges as `surfaces.<faceName>` and `edges.<edgeName>` references
1265
+ - SVG import supports deterministic region filtering (`regionSelection`, `maxRegions`, area thresholds)
1266
+ - The returned `Shape` or `Sketch` is fully chainable — use `.translate()`, `.rotate()`, `.subtract()`, etc.
1267
+
1268
+ ### Plain JS Module Imports
1269
+ Alongside `importPart()` / `importSketch()`, regular JS `import` / `require(...)` is supported for utility modules.
1270
+
1271
+ - If a module uses `export` / `module.exports`, that export value is used.
1272
+ - If a module has no explicit exports and uses a top-level `return`, that return value becomes the module value (including arrays).
1273
+ - Do not mix explicit exports with top-level `return` in the same module; this throws an error.
1274
+
1275
+ ```javascript
1276
+ // scene-items.js
1277
+ import { box, cylinder } from "forgecad";
1278
+
1279
+ return [
1280
+ { name: "Plate", shape: box(20, 12, 2, true) },
1281
+ { name: "Pin", shape: cylinder(14, 3, undefined, undefined, true).translate(0, 0, 8) },
1282
+ ];
1283
+ ```
1284
+
1285
+ ```javascript
1286
+ // main.forge.js
1287
+ import items from "./scene-items.js";
1288
+
1289
+ return items.map((entry, index) => ({
1290
+ name: entry.name,
1291
+ shape: entry.shape.translate(index === 0 ? -20 : 20, 0, 0),
1292
+ }));
1293
+ ```
1294
+
1295
+ ### Placement References
1296
+
1297
+ ### `.withReferences({ points?, edges?, surfaces?, objects? })`
1298
+ Attach named placement references to a `Shape` or `TrackedShape`. These references survive normal transforms and `importPart()`.
1299
+
1300
+ **Reference kinds:**
1301
+ - `points`: exact 3D coordinates
1302
+ - `edges`: `{ start, end }` segments; default reference point is the midpoint
1303
+ - `surfaces`: `{ center, normal }`; default reference point is the center
1304
+ - `objects`: bounding boxes derived from another shape/group or explicit `{ min, max }`
1305
+
1306
+ ```javascript
1307
+ const part = union(base, post).withReferences({
1308
+ points: {
1309
+ mount: [0, -16, -4],
1310
+ },
1311
+ edges: {
1312
+ postAxis: { start: [12, 0, 4], end: [12, 0, 30] },
1313
+ },
1314
+ surfaces: {
1315
+ mountingFace: { center: [0, -16, 0], normal: [0, -1, 0] },
1316
+ },
1317
+ objects: {
1318
+ base,
1319
+ post,
1320
+ },
1321
+ });
1322
+ ```
1323
+
1324
+ ### `.referenceNames(kind?)`
1325
+ Lists named placement references on a shape.
1326
+
1327
+ ```javascript
1328
+ part.referenceNames(); // ['edges.postAxis', 'objects.base', 'objects.post', 'points.mount', ...]
1329
+ part.referenceNames('points'); // ['mount']
1330
+ ```
1331
+
1332
+ ### `.referencePoint(ref)`
1333
+ Resolve a placement reference to a world-space point.
1334
+
1335
+ Supported forms:
1336
+ - `mount` or `points.mount`
1337
+ - `edges.postAxis`
1338
+ - `edges.postAxis.start`
1339
+ - `surfaces.mountingFace`
1340
+ - `objects.post.top`
1341
+
1342
+ ```javascript
1343
+ const p = part.referencePoint("objects.post.top");
1344
+ ```
1345
+
1346
+ ### `.placeReference(ref, [x, y, z], offset?)`
1347
+ Translate a shape so the given placement reference lands on a target coordinate.
1348
+
1349
+ ```javascript
1350
+ const placed = importPart("widget.forge.js")
1351
+ .placeReference("mount", [120, 40, 0]);
1352
+ ```
1353
+
1354
+ ### `attachTo()` with named references
1355
+
1356
+ `attachTo()` still accepts the built-in 3D anchors, but it can now also consume named placement references:
1357
+
1358
+ ```javascript
1359
+ const cap = box(18, 18, 8, true)
1360
+ .attachTo(widget, "objects.post.top", "bottom");
1361
+ ```
1362
+
1363
+ ### Typical Project Structure
1364
+ ```
1365
+ my-project/
1366
+ ├── base-profile.sketch.js ← 2D cross-section
1367
+ ├── bracket.forge.js ← extrudes the sketch, adds holes
1368
+ └── assembly.forge.js ← imports multiple parts, positions them
1369
+ ```
1370
+
1371
+ ## Part Library
1372
+
1373
+ Pre-built parametric parts available via `lib.xxx()`. No imports needed.
1374
+
1375
+ ### `lib.boltHole(diameter, depth)`
1376
+ Through-hole cylinder (centered).
1377
+
1378
+ ### `lib.fastenerHole(opts)`
1379
+ Standardized metric hole helper with fits and optional counterbore/countersink.
1380
+
1381
+ ```javascript
1382
+ const m4 = lib.fastenerHole({
1383
+ size: "M4",
1384
+ fit: "normal", // close | normal | loose | tap
1385
+ depth: 12,
1386
+ counterbore: { depth: 3.5 }, // diameter auto from size unless provided
1387
+ });
1388
+ ```
1389
+
1390
+ ### `lib.counterbore(holeDia, boreDia, boreDepth, totalDepth)`
1391
+ Through-hole with wider recess at top.
1392
+
1393
+ ### `lib.tube(outerX, outerY, outerZ, wall)`
1394
+ Rectangular hollow tube.
1395
+
1396
+ ### `lib.pipe(height, outerRadius, wall, segments?)`
1397
+ Hollow cylinder.
1398
+
1399
+ ### `lib.hexNut(acrossFlats, height, holeDia)`
1400
+ Hex nut via intersection of 3 rotated slabs, with center bore.
1401
+
1402
+ ### `lib.roundedBox(x, y, z, radius)`
1403
+ Approximate rounded box via union of axis-aligned slabs.
1404
+
1405
+ ### `lib.bracket(width, height, depth, thick, holeDia?)`
1406
+ L-shaped mounting bracket with optional holes.
1407
+
1408
+ ### `lib.holePattern(rows, cols, spacingX, spacingY, holeDia, depth)`
1409
+ Grid of cylindrical holes.
1410
+
1411
+ ### `lib.spurGear(options)`
1412
+ Involute external spur gear with optional bore.
1413
+
1414
+ **Options:**
1415
+ - `module` (number) - Metric module (pitch diameter / tooth count)
1416
+ - `teeth` (integer) - Tooth count (>= 6)
1417
+ - `faceWidth` (number) - Extrusion width along Z
1418
+ - `pressureAngleDeg` (number, optional) - Default: `20`
1419
+ - `backlash` (number, optional) - Tangential backlash at pitch circle. Default: `0`
1420
+ - `clearance` (number, optional) - Root clearance. Default: `0.25 * module`
1421
+ - `addendum` (number, optional) - Tooth addendum. Default: `module`
1422
+ - `dedendum` (number, optional) - Tooth dedendum. Default: `addendum + clearance`
1423
+ - `boreDiameter` (number, optional) - Center bore diameter
1424
+ - `center` (boolean, optional) - Center extrusion around Z=0. Default: `true`
1425
+ - `segmentsPerTooth` (number, optional) - Involute sampling quality. Default: `10`
1426
+
1427
+ ```javascript
1428
+ const pinion = lib.spurGear({
1429
+ module: 1.25,
1430
+ teeth: 14,
1431
+ faceWidth: 8,
1432
+ boreDiameter: 5,
1433
+ });
1434
+ ```
1435
+
1436
+ ### `lib.faceGear(options)`
1437
+ Face gear (crown style) where teeth are on one face (`top` or `bottom`) instead of the outer rim.
1438
+
1439
+ Uses the same involute tooth sizing inputs as `lib.spurGear(...)`, then projects the tooth band axially from one side.
1440
+
1441
+ **Options:**
1442
+ - all `lib.spurGear(...)` options, plus:
1443
+ - `side` (`'top' | 'bottom'`, optional) - Which face gets the teeth. Default: `'top'`
1444
+ - `toothHeight` (number, optional) - Tooth projection height from the selected face. Default: `module`
1445
+
1446
+ ```javascript
1447
+ const face = lib.faceGear({
1448
+ module: 1.25,
1449
+ teeth: 36,
1450
+ faceWidth: 8,
1451
+ toothHeight: 1.2,
1452
+ side: 'top',
1453
+ boreDiameter: 8,
1454
+ });
1455
+ ```
1456
+
1457
+ `lib.sideGear(...)` is kept as a compatibility alias.
1458
+
1459
+ ### `lib.ringGear(options)`
1460
+ Internal ring gear with involute-derived tooth spaces.
1461
+
1462
+ **Options:**
1463
+ - `module` (number)
1464
+ - `teeth` (integer, >= 12)
1465
+ - `faceWidth` (number)
1466
+ - `pressureAngleDeg` (number, optional) - Default: `20`
1467
+ - `backlash` (number, optional) - Default: `0`
1468
+ - `clearance` (number, optional) - Default: `0.25 * module`
1469
+ - `addendum` (number, optional) - Default: `module`
1470
+ - `dedendum` (number, optional) - Default: `addendum + clearance`
1471
+ - `rimWidth` (number, optional) - Radial ring thickness outside tooth roots
1472
+ - `outerDiameter` (number, optional) - Overrides `rimWidth` if provided
1473
+ - `center` (boolean, optional) - Default: `true`
1474
+ - `segmentsPerTooth` (number, optional) - Default: `10`
1475
+
1476
+ ```javascript
1477
+ const ring = lib.ringGear({
1478
+ module: 1.25,
1479
+ teeth: 58,
1480
+ faceWidth: 10,
1481
+ rimWidth: 4,
1482
+ });
1483
+ ```
1484
+
1485
+ ### `lib.rackGear(options)`
1486
+ Linear rack gear with pressure-angle flanks.
1487
+
1488
+ **Options:**
1489
+ - `module` (number)
1490
+ - `teeth` (integer, >= 2)
1491
+ - `faceWidth` (number)
1492
+ - `pressureAngleDeg` (number, optional) - Default: `20`
1493
+ - `backlash` (number, optional) - Default: `0`
1494
+ - `clearance` (number, optional) - Default: `0.25 * module`
1495
+ - `addendum` (number, optional) - Default: `module`
1496
+ - `dedendum` (number, optional) - Default: `addendum + clearance`
1497
+ - `baseHeight` (number, optional) - Rack body thickness behind root line
1498
+ - `center` (boolean, optional) - Default: `true`
1499
+
1500
+ ```javascript
1501
+ const rack = lib.rackGear({
1502
+ module: 1.25,
1503
+ teeth: 24,
1504
+ faceWidth: 8,
1505
+ baseHeight: 3.5,
1506
+ });
1507
+ ```
1508
+
1509
+ ### `lib.bevelGear(options)`
1510
+ Conical bevel gear generated from a tapered involute extrusion.
1511
+
1512
+ **Options:**
1513
+ - `module` (number)
1514
+ - `teeth` (integer, >= 6)
1515
+ - `faceWidth` (number)
1516
+ - `pressureAngleDeg` (number, optional) - Default: `20`
1517
+ - `backlash` (number, optional) - Default: `0`
1518
+ - `clearance` (number, optional) - Default: `0.25 * module`
1519
+ - `addendum` (number, optional) - Default: `module`
1520
+ - `dedendum` (number, optional) - Default: `addendum + clearance`
1521
+ - `boreDiameter` (number, optional)
1522
+ - pitch cone setup (choose one):
1523
+ - `pitchAngleDeg` (number, optional), or
1524
+ - `mateTeeth` (+ optional `shaftAngleDeg`, default `90`) for auto pitch-angle derivation
1525
+ - `center` (boolean, optional) - Default: `true`
1526
+ - `segmentsPerTooth` (number, optional) - Default: `10`
1527
+
1528
+ ```javascript
1529
+ const bevelPinion = lib.bevelGear({
1530
+ module: 1.5,
1531
+ teeth: 18,
1532
+ faceWidth: 10,
1533
+ mateTeeth: 36,
1534
+ shaftAngleDeg: 90,
1535
+ boreDiameter: 5,
1536
+ });
1537
+ ```
1538
+
1539
+ ### `lib.gearPair(options)`
1540
+ Build or validate a spur-gear pair and return ratio/backlash/mesh diagnostics.
1541
+
1542
+ Accepts either:
1543
+ - spur gear shapes produced by `lib.spurGear(...)`, or
1544
+ - analytical specs (`{ module, teeth, ... }`) for each member
1545
+
1546
+ **Options:**
1547
+ - `pinion` (`Shape | GearPairSpec`) - input gear
1548
+ - `gear` (`Shape | GearPairSpec`) - mating output gear
1549
+ - `backlash` (number, optional) - target backlash used for auto center distance
1550
+ - `centerDistance` (number, optional) - override center distance directly
1551
+ - `place` (boolean, optional) - auto-place `gear` at +X center distance. Default: `true`
1552
+ - `phaseDeg` (number, optional) - additional Z rotation applied to placed gear before translation
1553
+
1554
+ **Returns:** `GearPairResult` with:
1555
+ - `pinion`, `gear` (shapes)
1556
+ - `jointRatio`, `speedReduction`
1557
+ - `centerDistance`, `centerDistanceNominal`, `backlash`
1558
+ - `pressureAngleDeg`, `workingPressureAngleDeg`, `contactRatio`
1559
+ - `diagnostics[]` and `status` (`ok | warn | error`)
1560
+
1561
+ ```javascript
1562
+ const pair = lib.gearPair({
1563
+ pinion: { module: 1.25, teeth: 14, faceWidth: 8, boreDiameter: 5 },
1564
+ gear: { module: 1.25, teeth: 42, faceWidth: 8, boreDiameter: 8 },
1565
+ backlash: 0.05,
1566
+ });
1567
+
1568
+ if (pair.status !== 'ok') {
1569
+ console.warn(pair.diagnostics);
1570
+ }
1571
+
1572
+ return [pair.pinion, pair.gear];
1573
+ ```
1574
+
1575
+ ### `lib.bevelGearPair(options)`
1576
+ Build or validate a bevel-gear pair and return ratio diagnostics plus recommended joint placement vectors.
1577
+
1578
+ Accepts either:
1579
+ - bevel gear shapes produced by `lib.bevelGear(...)`, or
1580
+ - analytical specs (`{ module, teeth, ... }`) for each member
1581
+
1582
+ **Options:**
1583
+ - `pinion` (`Shape | GearPairSpec`)
1584
+ - `gear` (`Shape | GearPairSpec`)
1585
+ - `shaftAngleDeg` (number, optional) - Default: `90`
1586
+ - `backlash` (number, optional)
1587
+ - `place` (boolean, optional) - Apply recommended transforms to returned shapes. Default: `true`
1588
+ - `phaseDeg` (number, optional) - Extra phase on the placed driven bevel gear
1589
+
1590
+ **Returns:** `BevelGearPairResult` with:
1591
+ - `pinion`, `gear` (shapes)
1592
+ - `jointRatio`, `speedReduction`
1593
+ - `shaftAngleDeg`, `pinionPitchAngleDeg`, `gearPitchAngleDeg`, `coneDistance`
1594
+ - `pinionAxis`, `gearAxis`, `pinionCenter`, `gearCenter` (joint setup helpers)
1595
+ - `diagnostics[]` and `status` (`ok | warn | error`)
1596
+
1597
+ ```javascript
1598
+ const bevelPair = lib.bevelGearPair({
1599
+ pinion: { module: 1.5, teeth: 18, faceWidth: 10 },
1600
+ gear: { module: 1.5, teeth: 36, faceWidth: 9 },
1601
+ shaftAngleDeg: 90,
1602
+ });
1603
+ ```
1604
+
1605
+ ### `lib.faceGearPair(options)`
1606
+ Build or validate a perpendicular pair between a face gear and a vertical spur gear.
1607
+
1608
+ Accepts either:
1609
+ - face gear shapes produced by `lib.faceGear(...)` or face-gear specs (`{ module, teeth, ... }`)
1610
+ - vertical spur shapes produced by `lib.spurGear(...)` or spur specs (`{ module, teeth, ... }`)
1611
+
1612
+ **Options:**
1613
+ - `face` (`Shape | FaceGearSpec`) - face/crown gear member
1614
+ - `vertical` (`Shape | GearPairSpec`) - mating perpendicular spur gear
1615
+ - `backlash` (number, optional) - target radial backlash for auto center distance
1616
+ - `centerDistance` (number, optional) - override center distance directly
1617
+ - `meshPlaneZ` (number, optional) - override the Z plane where the vertical gear is placed
1618
+ - `place` (boolean, optional) - auto-place `vertical`. Default: `true`
1619
+ - `phaseDeg` (number, optional) - phase rotation applied before perpendicular placement
1620
+
1621
+ **Returns:** `FaceGearPairResult` with:
1622
+ - `face`, `vertical` (shapes)
1623
+ - `jointRatio`, `speedReduction`
1624
+ - `centerDistance`, `centerDistanceNominal`, `backlash`
1625
+ - `meshPlaneZ`, `radialOverlap`
1626
+ - `diagnostics[]` and `status` (`ok | warn | error`)
1627
+
1628
+ ```javascript
1629
+ const pair = lib.faceGearPair({
1630
+ face: { module: 1.25, teeth: 36, faceWidth: 8, toothHeight: 1.2, side: 'top' },
1631
+ vertical: { module: 1.25, teeth: 12, faceWidth: 8 },
1632
+ backlash: 0.05,
1633
+ });
1634
+
1635
+ if (pair.status !== 'ok') {
1636
+ console.warn(pair.diagnostics);
1637
+ }
1638
+
1639
+ return [pair.face, pair.vertical];
1640
+ ```
1641
+
1642
+ `lib.sideGearPair(...)` is kept as a compatibility alias.
1643
+
1644
+ ### `lib.tSlotProfile(options?)`
1645
+ Build a 2D T-slot cross-section sketch.
1646
+
1647
+ This is a generic, tunable T-slot generator.
1648
+
1649
+ **Options:**
1650
+ - `size` (number) - Outer profile size. Default: `20`
1651
+ - `slotWidth` (number) - Slot mouth width. Default: `6`
1652
+ - `slotInnerWidth` (number) - Wider interior slot cavity width. Default: `10.4`
1653
+ - `slotDepth` (number) - Slot depth from outer face. Default: `6`
1654
+ - `slotNeckDepth` (number) - Narrow mouth depth before widening. Default: `1.6`
1655
+ - `wall` (number) - Outer shell thickness. Default: `1.4`
1656
+ - `web` (number) - Central cross-web thickness. Default: `2.1`
1657
+ - `centerBossDia` (number) - Center boss diameter. Default: `8.2`
1658
+ - `centerBoreDia` (number) - Center bore diameter. Default: `4.2`
1659
+ - `outerCornerRadius` (number) - Outer corner radius. Default: `1`
1660
+ - `segments` (number) - Circle smoothness for 2D bores/bosses. Default: `36`
1661
+
1662
+ **Returns:** `Sketch`
1663
+
1664
+ ```javascript
1665
+ const profile = lib.tSlotProfile();
1666
+ return profile; // 2D drawing-ready cross-section
1667
+ ```
1668
+
1669
+ ### `lib.tSlotExtrusion(length, options?)`
1670
+ Build a 3D extrusion from `lib.tSlotProfile(...)`.
1671
+
1672
+ **Parameters:**
1673
+ - `length` (number) - Extrusion length along Z
1674
+ - `options` - Same options as `lib.tSlotProfile(...)` plus:
1675
+ - `center` (boolean) - Center the length around Z=0. Default: `false`
1676
+
1677
+ **Returns:** `Shape`
1678
+
1679
+ ```javascript
1680
+ const rail = lib.tSlotExtrusion(300, { center: true });
1681
+ ```
1682
+
1683
+ ### `lib.profile2020BSlot6Profile(options?)`
1684
+ Profile-accurate 2D helper for a 20x20 B-type slot 6 section.
1685
+
1686
+ Defaults target common B-type 20x20 conventions:
1687
+ - slot width `6.0`
1688
+ - slot depth `5.5`
1689
+ - center bore `5.5`
1690
+ - center boss `8.4`
1691
+ - diagonal web width `4.4`
1692
+ - no edge pocket holes (only central bore is cut)
1693
+
1694
+ **Options:**
1695
+ - `slotWidth` (number) - Default: `6.0`
1696
+ - `slotInnerWidth` (number) - Default: `8.2`
1697
+ - `slotDepth` (number) - Default: `5.5`
1698
+ - `slotNeckDepth` (number) - Default: `1.8`
1699
+ - `centerBoreDia` (number) - Default: `5.5` (set `0` to disable)
1700
+ - `centerBossDia` (number) - Default: `8.4`
1701
+ - `diagonalWebWidth` (number) - Default: `4.4`
1702
+ - `outerCornerRadius` (number) - Default: `1.0`
1703
+ - `segments` (number) - Default: `40`
1704
+
1705
+ ```javascript
1706
+ const profile2d = lib.profile2020BSlot6Profile();
1707
+ ```
1708
+
1709
+ ### `lib.profile2020BSlot6(length, options?)`
1710
+ 3D extrusion helper built from `lib.profile2020BSlot6Profile(...)`.
1711
+
1712
+ Use `options` to override supplier-specific tolerances.
1713
+ - Supports all profile options above
1714
+ - Plus `center` (boolean) to center length about Z=0
1715
+
1716
+ ```javascript
1717
+ const profile = lib.profile2020BSlot6(500, { center: true });
1718
+ ```
1719
+
1720
+ ### Exploded-view helpers
1721
+ For scene-layout helpers such as `lib.explode(...)` and viewport explode overrides, see [../runtime/viewport.md](../runtime/viewport.md).
1722
+
1723
+ ### `lib.pipeRoute(points, radius, options?)`
1724
+ Route a pipe through 3D waypoints with smooth torus bends at corners.
1725
+
1726
+ **Parameters:**
1727
+ - `points` ([number, number, number][]) - Array of 3D waypoints
1728
+ - `radius` (number) - Pipe outer radius
1729
+ - `options` (object, optional):
1730
+ - `bendRadius` (number) - Radius of bends at corners. Default: `radius * 4`
1731
+ - `wall` (number) - Wall thickness for hollow pipe. If omitted, pipe is solid
1732
+ - `segments` (number) - Circumferential segments. Default: 32
1733
+
1734
+ **Returns:** `Shape`
1735
+
1736
+ ```javascript
1737
+ // Solid copper pipe with 90° bends
1738
+ const refrigPipe = lib.pipeRoute(
1739
+ [[0, 0, 0], [100, 0, 0], [100, 80, 0], [100, 80, 60]],
1740
+ 4,
1741
+ { bendRadius: 20 }
1742
+ ).color('#B87333');
1743
+
1744
+ // Hollow drain pipe
1745
+ const drainPipe = lib.pipeRoute(
1746
+ [[0, 0, 20], [60, 0, 20], [60, 80, 20]],
1747
+ 3,
1748
+ { bendRadius: 15, wall: 1 }
1749
+ ).color('#CCCCCC');
1750
+ ```
1751
+
1752
+ ### `lib.elbow(pipeRadius, bendRadius, angle?, options?)`
1753
+ Curved pipe section (torus arc) for connecting two pipe directions. Creates a bend at the origin.
1754
+
1755
+ **Parameters:**
1756
+ - `pipeRadius` (number) - Pipe outer radius
1757
+ - `bendRadius` (number) - Centerline bend radius
1758
+ - `angle` (number, optional) - Bend angle in degrees. Default: 90
1759
+
1760
+ **Options:**
1761
+ - `wall` (number) - Wall thickness for hollow pipe
1762
+ - `segments` (number) - Circumferential segments. Default: 32
1763
+ - `from` ([number, number, number]) - Incoming direction vector
1764
+ - `to` ([number, number, number]) - Outgoing direction vector (overrides angle)
1765
+
1766
+ **Alternative call:** `lib.elbow(pipeRadius, bendRadius, { from, to, wall, segments })`
1767
+
1768
+ ```javascript
1769
+ // Simple 90° elbow
1770
+ const bend = lib.elbow(5, 20, 90);
1771
+
1772
+ // 45° hollow elbow
1773
+ const bend45 = lib.elbow(5, 20, 45, { wall: 1.5 });
1774
+
1775
+ // Direction-based: connect Z-up pipe to X-right pipe
1776
+ const bend = lib.elbow(5, 20, { from: [0, 0, 1], to: [1, 0, 0] });
1777
+ ```
1778
+
1779
+ ### `lib.thread(diameter, pitch, length, options?)`
1780
+ External thread (helical ridge) via twisted extrusion. Returns a threaded cylinder along +Z.
1781
+
1782
+ **Options:**
1783
+ - `depth` (number) - Thread depth. Default: `pitch * 0.35`
1784
+ - `segments` (number) - Circumferential segments. Default: 36
1785
+
1786
+ ```javascript
1787
+ const m8thread = lib.thread(8, 1.25, 30);
1788
+ const smooth = lib.thread(8, 1.0, 30, { segments: 48 });
1789
+ ```
1790
+
1791
+ ### `lib.bolt(diameter, length, options?)`
1792
+ Hex bolt with real helical threads. Head at z=0, shaft extends along −Z.
1793
+
1794
+ **Options:**
1795
+ - `pitch` (number) - Thread pitch. Default: `diameter * 0.15`
1796
+ - `headHeight` (number) - Default: `diameter * 0.65`
1797
+ - `headAcrossFlats` (number) - Default: `diameter * 1.6`
1798
+ - `threadLength` (number) - Threaded portion. Default: full length
1799
+ - `segments` (number) - Circumferential segments. Default: 36
1800
+
1801
+ ```javascript
1802
+ const m8bolt = lib.bolt(8, 30);
1803
+ const custom = lib.bolt(10, 40, { pitch: 1.5, headHeight: 7 });
1804
+ ```
1805
+
1806
+ ### `lib.nut(diameter, options?)`
1807
+ Hex nut with bore, centered at origin.
1808
+
1809
+ **Options:**
1810
+ - `pitch` (number) - Default: `diameter * 0.15`
1811
+ - `height` (number) - Default: `diameter * 0.8`
1812
+ - `acrossFlats` (number) - Default: `diameter * 1.6`
1813
+ - `segments` (number) - Circumferential segments. Default: 36
1814
+
1815
+ ```javascript
1816
+ const m8nut = lib.nut(8);
1817
+ const m8nut2 = lib.nut(8, { height: 6.5, acrossFlats: 13 });
1818
+ ```
1819
+
1820
+ ## Query Methods
1821
+
1822
+ ### 3D Shape Queries
1823
+ ```javascript
1824
+ shape.volume() // Volume in mm³
1825
+ shape.surfaceArea() // Surface area in mm²
1826
+ shape.boundingBox() // { min: [x,y,z], max: [x,y,z] }
1827
+ shape.isEmpty() // true if no geometry
1828
+ shape.numTri() // Triangle count
1829
+ shape.minGap(other, 50) // Minimum distance to another shape (within search radius)
1830
+ shape.geometryInfo() // { backend, representation, fidelity, topology, sources }
1831
+ ```
1832
+
1833
+ `geometryInfo()` is the current contract boundary for future hybrid kernels. Today most results are `manifold` + `mesh-solid`; `loft()` / `sweep()` report `sampled`, and tracked extrusions report `topology: 'synthetic'`. A future OCCT/BREP backend can change these values without forcing a language rewrite.
1834
+
1835
+ For the maintained exact STEP/BREP support matrix, see [../output/brep-export.md](../output/brep-export.md).
1836
+
1837
+ ### 2D Sketch Queries
1838
+ ```javascript
1839
+ sketch.area() // Area in mm²
1840
+ sketch.bounds() // { min: [x,y], max: [x,y] }
1841
+ sketch.isEmpty() // true if no area
1842
+ sketch.numVert() // Vertex count
1843
+ ```
1844
+
1845
+ ## Returning Multiple Objects
1846
+
1847
+ Scripts can return arrays to display multiple objects in the viewport:
1848
+
1849
+ ```javascript
1850
+ // Simple array — auto-named "Object 1", "Object 2", etc.
1851
+ return [
1852
+ box(50, 50, 10),
1853
+ cylinder(20, 8).translate(25, 25, 10),
1854
+ ];
1855
+
1856
+ // Named objects with colors
1857
+ return [
1858
+ { name: "Base Plate", shape: box(100, 100, 5), color: "#888888" },
1859
+ { name: "Column", shape: cylinder(50, 10).translate(50, 50, 5), color: "#4488cc" },
1860
+ { name: "Profile", sketch: circle2d(20), color: "#ff6600" },
1861
+ ];
1862
+ ```
1863
+
1864
+ Each object gets its own visibility toggle, opacity slider, and color picker in the View Panel.
1865
+
1866
+ ### Assembly Groups
1867
+
1868
+ For complex assemblies, use nested groups to organize related parts:
1869
+
1870
+ ```javascript
1871
+ return [
1872
+ { name: "Bed Assembly", group: [
1873
+ { name: "Bed Plate", shape: bedPlate },
1874
+ { name: "Glass Bed", shape: glass },
1875
+ { name: "Heater", shape: heater },
1876
+ ]},
1877
+ { name: "Gantry", group: [
1878
+ { name: "Left Rail", shape: leftRail },
1879
+ { name: "Right Rail", shape: rightRail },
1880
+ { name: "Cross Bar", shape: crossBar },
1881
+ ]},
1882
+ ];
1883
+ ```
1884
+
1885
+ **Benefits:**
1886
+ - **Spatial analysis** skips intra-group collision checks (intentional overlaps)
1887
+ - **Group-level summary** reports relationships between assemblies
1888
+ - **Object listing** shows group tags: `Bed Plate [Bed Assembly]`
1889
+ - **Parameter validation** (`param-check` CLI) ignores collisions within groups
1890
+
1891
+ ## Guides and Examples
1892
+
1893
+ See [../guides/modeling-recipes.md](../guides/modeling-recipes.md) for patterns, best practices, debugging, and sample snippets.
1894
+
1895
+ For runnable end-to-end models, read `examples/api/`.
1896
+
1897
+
1898
+ ### 2. Geometry and Positioning (when placement/orientation matters)
1899
+
1900
+ Axis conventions, winding rules, and placement strategy.
1901
+
1902
+ <!-- docs/permanent/API/model-building/coordinate-system.md -->
1903
+
1904
+ # Coordinate System Convention
1905
+
1906
+ ForgeCAD uses a **Z-up** right-handed coordinate system.
1907
+
1908
+ ## Axes
1909
+
1910
+ | Axis | Direction | Positive |
1911
+ |------|-----------------|----------|
1912
+ | X | Left / Right | Right |
1913
+ | Y | Forward / Back | Forward |
1914
+ | Z | Up / Down | Up |
1915
+
1916
+ ## Standard Views
1917
+
1918
+ | View | Camera position direction | Sees plane | Camera up |
1919
+ |--------|--------------------------|------------|-----------|
1920
+ | Front | −Y (camera at −Y) | XZ | Z |
1921
+ | Back | +Y (camera at +Y) | XZ | Z |
1922
+ | Right | +X (camera at +X) | YZ | Z |
1923
+ | Left | −X (camera at −X) | YZ | Z |
1924
+ | Top | +Z (camera at +Z) | XY | +Y |
1925
+ | Bottom | −Z (camera at −Z) | XY | −Y |
1926
+ | Iso | +X −Y +Z (diagonal) | — | Z |
1927
+
1928
+ ## GizmoViewcube Face Mapping
1929
+
1930
+ Three.js BoxGeometry material indices (cube face order):
1931
+
1932
+ | Index | Three.js direction | ForgeCAD label |
1933
+ |-------|--------------------|----------------|
1934
+ | 0 | +X | Right |
1935
+ | 1 | −X | Left |
1936
+ | 2 | +Y | Front |
1937
+ | 3 | −Y | Back |
1938
+ | 4 | +Z | Top |
1939
+ | 5 | −Z | Bottom |
1940
+
1941
+ Default drei labels are `['Right', 'Left', 'Top', 'Bottom', 'Front', 'Back']` (Y-up).
1942
+ For Z-up we pass `faces={['Right', 'Left', 'Front', 'Back', 'Top', 'Bottom']}`.
1943
+
1944
+ ## Grid
1945
+
1946
+ The ground plane is XY (Z = 0). The grid lies on this plane.
1947
+
1948
+ ---
1949
+
1950
+ <!-- docs/permanent/API/model-building/geometry-conventions.md -->
1951
+
1952
+ # Geometry Conventions
1953
+
1954
+ ForgeCAD wraps Manifold (a mesh kernel) and Three.js (a Y-up renderer). These libraries have their own conventions that conflict with each other and with CAD norms. This doc captures every convention mismatch and how ForgeCAD resolves it.
1955
+
1956
+ **Core principle: the user script should never need to know about kernel or renderer internals.** If the user writes something geometrically reasonable, it should work. All convention translation happens inside ForgeCAD's layer.
1957
+
1958
+ ## Winding Order
1959
+
1960
+ **What it is:** The order of vertices in a 2D polygon determines its "direction" — counter-clockwise (CCW) = positive area, clockwise (CW) = negative/zero area in Manifold's `CrossSection`.
1961
+
1962
+ **The problem:** Manifold silently produces empty geometry for CW polygons. A user writing `polygon([[0,0], [50,0], [50,30]])` vs `polygon([[0,0], [50,30], [50,0]])` gets either a triangle or nothing, with no error.
1963
+
1964
+ **ForgeCAD's fix:** All entry points that accept raw points auto-fix winding:
1965
+ - `polygon(points)` — computes signed area, reverses if CW
1966
+ - `path().close()` — same fix
1967
+
1968
+ **Signed area test** (shoelace formula):
1969
+ ```
1970
+ signedArea = Σ (x₂ - x₁)(y₂ + y₁)
1971
+ ```
1972
+ If `signedArea > 0` → CW → reverse to make CCW.
1973
+
1974
+ **Implementation:** `src/forge/sketch/primitives.ts` (polygon), `src/forge/sketch/path.ts` (close).
1975
+
1976
+ **Rule for new code:** Any function that takes user-provided point arrays and creates a `CrossSection` MUST auto-fix winding. Never pass raw user points to Manifold without this check.
1977
+
1978
+ ## Coordinate System (Z-up vs Y-up)
1979
+
1980
+ **The problem:** Three.js uses Y-up. CAD convention (and ForgeCAD) uses Z-up.
1981
+
1982
+ **ForgeCAD's fix:** We set `camera.up = (0, 0, 1)` everywhere. Geometry coordinates are native Z-up — no matrix swizzling. The camera orientation handles the visual mapping.
1983
+
1984
+ **Where this matters:**
1985
+ - `camera.up.set(0, 0, 1)` in `sceneBuilder.ts` and `render.ts`
1986
+ - GizmoViewcube face labels remapped (see coordinate-system.md)
1987
+ - Grid plane is XY (Z=0)
1988
+ - Extrusion goes along +Z
1989
+ - Revolution axis is Y (sketch plane), result maps to Z-up space
1990
+
1991
+ **Rule for new code:** Never swap Y/Z in geometry. Always fix it at the camera/renderer level.
1992
+
1993
+ ## Revolution Axis
1994
+
1995
+ **What it is:** `CrossSection.revolve()` in Manifold revolves around the Y axis. The sketch profile must be in the X-Y plane with X = radius (distance from axis) and Y = height.
1996
+
1997
+ **The mapping:**
1998
+ - Profile X coordinate → radial distance from center
1999
+ - Profile Y coordinate → height (becomes Z after revolution)
2000
+ - Profile must be on the positive X side (X > 0) for valid geometry
2001
+
2002
+ **Rule for new code:** Document which axis any new sweep/revolution operation uses. If it differs from user expectation, add a transform wrapper.
2003
+
2004
+ ## Boolean Winding (3D)
2005
+
2006
+ **What it is:** Manifold requires consistent face normals (outward-pointing) for boolean operations. Manifold handles this internally for its own primitives, but imported meshes or degenerate operations can produce inside-out faces.
2007
+
2008
+ **ForgeCAD's fix:** We only create meshes through Manifold's own constructors (`extrude`, `revolve`, `cylinder`, `sphere`, etc.), which guarantee correct normals. No raw mesh import path exists yet.
2009
+
2010
+ **Rule for new code:** If adding mesh import (STL, OBJ), run `Manifold.asOriginal()` or validate manifoldness before allowing booleans.
2011
+
2012
+ ## Transform Order
2013
+
2014
+ **What it is:** Transforms are applied in call order (left to right in the chain). `shape.translate(10,0,0).rotate(0,0,45)` first moves, then rotates around origin — so the shape orbits.
2015
+
2016
+ **Convention:** This matches the standard "post-multiply" convention. No surprises here, but worth noting because some systems (OpenSCAD) apply transforms in reverse order.
2017
+
2018
+ For explicit transform objects:
2019
+ - `A.mul(B)` means **apply A, then B**.
2020
+ - `composeChain(A, B, C)` means **A -> B -> C**.
2021
+
2022
+ **Rule for new code:** Keep this chain order everywhere. Document any operation that deviates.
2023
+
2024
+ ## Assembly Frame Composition
2025
+
2026
+ This is where regressions are most likely if convention is unclear.
2027
+
2028
+ For a point in child geometry-local coordinates:
2029
+ - local -> `childBase` -> `jointMotion(value)` -> `jointFrame` -> `parentWorld`
2030
+
2031
+ In Forge chain notation:
2032
+ ```ts
2033
+ childWorld = composeChain(childBase, jointMotion, jointFrame, parentWorld)
2034
+ ```
2035
+
2036
+ Equivalent matrix-style equation (for reference):
2037
+ ```txt
2038
+ T_world_child = T_parent_world * T_joint_frame * T_joint_motion * T_child_base
2039
+ ```
2040
+
2041
+ **Rule for new code:** In kinematics/assembly code, prefer `composeChain(...)` over manual `.mul(...).mul(...)` sequences to avoid order mistakes.
2042
+
2043
+ ## Summary of Shield Points
2044
+
2045
+ These are the places where ForgeCAD translates between "what the user means" and "what the kernel needs":
2046
+
2047
+ | Convention | User sees | Kernel needs | Where we fix it |
2048
+ |---|---|---|---|
2049
+ | Winding | Any point order | CCW | `polygon()`, `path().close()` |
2050
+ | Up axis | Z-up | Y-up (Three.js) | `camera.up`, gizmo labels |
2051
+ | Revolution | "revolve this profile" | Profile in X-Y, X>0 | Documented, not auto-fixed |
2052
+ | Face normals | Doesn't think about it | Outward-pointing | Manifold constructors |
2053
+ | Transform order | Left-to-right chain | Post-multiply | Native match, no fix needed |
2054
+
2055
+ When adding new geometry operations, check this table. If the operation introduces a new convention mismatch between user intent and kernel requirement, either auto-fix it (preferred) or document it clearly in the API docs.
2056
+
2057
+ ---
2058
+
2059
+ <!-- docs/permanent/API/model-building/positioning.md -->
2060
+
2061
+ # Positioning Strategy
2062
+
2063
+ **This is the most important page for building multi-part assemblies.** Most positioning bugs come from manual coordinate arithmetic. Use the methods below in priority order.
2064
+
2065
+ ## Priority Order
2066
+
2067
+ ### 1. `attachTo()` — Default choice for child-on-parent positioning
2068
+
2069
+ When placing a part relative to another part, use `attachTo()`. It reads as English: "put my bottom on your top."
2070
+
2071
+ ```javascript
2072
+ const base = box(100, 100, 10);
2073
+
2074
+ // Column stands on top of base, centered
2075
+ const column = cylinder(50, 8).attachTo(base, 'top', 'bottom');
2076
+
2077
+ // Button sticks out from front face, near top-right corner
2078
+ const button = box(10, 4, 6, true)
2079
+ .attachTo(panel, 'top-front-right', 'top-back-right', [5, -2, -10]);
2080
+ ```
2081
+
2082
+ **How to read it:** `child.attachTo(parent, parentAnchor, selfAnchor, offset)`
2083
+ - `parentAnchor` = "where on the parent do I want to attach?"
2084
+ - `selfAnchor` = "which part of myself aligns to that point?"
2085
+ - `offset` = "then shift by this much" (optional)
2086
+
2087
+ **Common patterns:**
2088
+ | Intent | parentAnchor | selfAnchor | Why |
2089
+ |--------|-------------|------------|-----|
2090
+ | Stack on top | `'top'` | `'bottom'` | Bottom of child meets top of parent |
2091
+ | Hang below | `'bottom'` | `'top'` | Top of child meets bottom of parent |
2092
+ | Stick out from front | `'front'` | `'back'` | Back of child flush with front of parent |
2093
+ | Protrude from side | `'left'` | `'right'` | Right face of child meets left face of parent |
2094
+
2095
+ ### 2. `pointAlong()` — Orient cylinders/extrusions before positioning
2096
+
2097
+ Cylinders default to Z-up. Instead of `rotate(90, 0, 0)` (which is confusing), use `pointAlong()`:
2098
+
2099
+ ```javascript
2100
+ // Pipe running along Y axis
2101
+ const pipe = cylinder(100, 5).pointAlong([0, 1, 0]);
2102
+
2103
+ // Axle along X
2104
+ const axle = cylinder(80, 3).pointAlong([1, 0, 0]);
2105
+ ```
2106
+
2107
+ **Always call `pointAlong()` BEFORE `attachTo()` or `translate()`** — it reorients around the origin.
2108
+
2109
+ ```javascript
2110
+ // Correct: orient first, then position
2111
+ const grille = cylinder(4, 30)
2112
+ .pointAlong([0, 1, 0])
2113
+ .attachTo(outdoorUnit, 'back', 'front', [0, 2, 0]);
2114
+ ```
2115
+
2116
+ ### 3. `rotateAroundTo()` — Aim a point around a hinge/axis
2117
+
2118
+ Use this when a part already has the correct pivot/axis, and you want to solve the angle from geometry instead of doing trig by hand.
2119
+
2120
+ ```javascript
2121
+ const arm = box(80, 8, 8, true)
2122
+ .translate(40, 0, 0)
2123
+ .withReferences({ points: { tip: [80, 0, 0] } });
2124
+
2125
+ // Rotate around Z until the tip lies in the plane formed by the Z axis and the target point
2126
+ const aimed = arm.rotateAroundTo(
2127
+ [0, 0, 1],
2128
+ [0, 0, 0],
2129
+ "tip",
2130
+ [30, 30, 20],
2131
+ );
2132
+
2133
+ // Exact line solve: throws if the target line is unreachable while preserving radius about the axis
2134
+ const lineHit = arm.rotateAroundTo(
2135
+ [0, 0, 1],
2136
+ [0, 0, 0],
2137
+ "tip",
2138
+ [30, 30, 0],
2139
+ { mode: 'line' },
2140
+ );
2141
+ ```
2142
+
2143
+ ### 4. `moveToLocal()` — Position relative to another shape's corner
2144
+
2145
+ When you need to place something at a specific offset from another shape's bounding box origin (min corner):
2146
+
2147
+ ```javascript
2148
+ const base = box(100, 100, 10);
2149
+ const part = box(20, 20, 30).moveToLocal(base, 10, 10, 10);
2150
+ ```
2151
+
2152
+ ### 5. `translate()` — Only for simple offsets or connecting independently-positioned parts
2153
+
2154
+ Use `translate()` when:
2155
+ - Moving a shape by a known fixed amount
2156
+ - Positioning between two shapes whose locations you've already computed via `boundingBox()`
2157
+
2158
+ ```javascript
2159
+ // Pipe spanning between two independently-positioned units
2160
+ const bb1 = indoor.boundingBox();
2161
+ const bb2 = outdoor.boundingBox();
2162
+ const pipeLen = bb2.min[1] - bb1.max[1];
2163
+ const pipe = cylinder(pipeLen, 5)
2164
+ .pointAlong([0, 1, 0])
2165
+ .translate(40, (bb1.max[1] + bb2.min[1]) / 2, bb1.min[2] + 15);
2166
+ ```
2167
+
2168
+ ### 6. `placeReference()` / named import references — For reusable multi-file parts
2169
+
2170
+ When a part will be imported elsewhere, define semantic placement references once in the source file:
2171
+
2172
+ ```javascript
2173
+ // widget.forge.js
2174
+ return union(base, post).withReferences({
2175
+ points: {
2176
+ mount: [0, -16, -4],
2177
+ },
2178
+ objects: {
2179
+ post,
2180
+ },
2181
+ });
2182
+ ```
2183
+
2184
+ Then consume them in the importing file:
2185
+
2186
+ ```javascript
2187
+ const widget = importPart("widget.forge.js")
2188
+ .placeReference("mount", [120, 40, 0]);
2189
+
2190
+ const cap = box(18, 18, 8, true)
2191
+ .attachTo(widget, "objects.post.top", "bottom");
2192
+ ```
2193
+
2194
+ Use this when manual coordinate math starts to feel like assembly bookkeeping.
2195
+
2196
+ ## Common Mistakes
2197
+
2198
+ ### ❌ Manual center-offset math
2199
+ ```javascript
2200
+ // BAD: easy to get wrong, hard to read
2201
+ const child = box(w, d, h, true)
2202
+ .translate(0, -parentThickness/2 - d/2 - 5, parentHeight/2 - h/2 - 20);
2203
+ ```
2204
+
2205
+ ### ✅ Anchor-based positioning
2206
+ ```javascript
2207
+ // GOOD: intent is clear, no arithmetic
2208
+ const child = box(w, d, h, true)
2209
+ .attachTo(parent, 'top-front', 'top-back', [0, -5, -20]);
2210
+ ```
2211
+
2212
+ ### ❌ rotate() for cylinder orientation
2213
+ ```javascript
2214
+ // BAD: which axis? what happens to center?
2215
+ const pipe = cylinder(100, 5).rotate(90, 0, 0).translate(x, y, z);
2216
+ ```
2217
+
2218
+ ### ✅ pointAlong() for cylinder orientation
2219
+ ```javascript
2220
+ // GOOD: reads as "pipe pointing along Y"
2221
+ const pipe = cylinder(100, 5).pointAlong([0, 1, 0]).translate(x, y, z);
2222
+ ```
2223
+
2224
+ ## Anchor Reference
2225
+
2226
+ See the [main API doc](API.md#3d-anchor-positioning) for the full list of 26 anchor names. Quick mental model:
2227
+
2228
+ - **1 word** = face center: `'top'`, `'front'`, `'left'`...
2229
+ - **2 words** = edge midpoint: `'top-front'`, `'back-left'`...
2230
+ - **3 words** = corner: `'top-front-left'`, `'bottom-back-right'`...
2231
+
2232
+
2233
+ ### 3. Sketch APIs (when the task is sketch-heavy)
2234
+
2235
+ 2D construction, transforms, booleans, paths, on-face sketching, extrusion, anchors.
2236
+
2237
+ <!-- docs/permanent/API/model-building/sketch-core.md -->
2238
+
2239
+ # Sketch Core
2240
+
2241
+ The `Sketch` class is an immutable wrapper around Manifold's `CrossSection` that provides a chainable 2D API.
2242
+
2243
+ ## Class: Sketch
2244
+
2245
+ Represents a 2D profile that can be transformed, combined with other sketches, or converted to 3D.
2246
+
2247
+ ### Color
2248
+
2249
+ #### `.color(hex: string): Sketch`
2250
+ Set the display color of this sketch. Returns a new Sketch.
2251
+
2252
+ ```javascript
2253
+ const red = rect(50, 30).color('#ff0000');
2254
+ const blue = circle2d(25).color('#0066ff');
2255
+ ```
2256
+
2257
+ Colors are preserved through transforms and boolean operations.
2258
+
2259
+ #### `.clone()` / `.duplicate()`
2260
+ Create an explicit duplicate of a sketch wrapper.
2261
+
2262
+ ```javascript
2263
+ const base = rect(50, 30);
2264
+ const a = base.clone();
2265
+ const b = base.duplicate().translate(60, 0);
2266
+ ```
2267
+
2268
+ ### Query Methods
2269
+
2270
+ #### `.area(): number`
2271
+ Returns the area of the sketch.
2272
+
2273
+ ```javascript
2274
+ const sq = rect(50, 50);
2275
+ console.log(sq.area()); // 2500
2276
+ ```
2277
+
2278
+ #### `.bounds()`
2279
+ Returns the bounding box: `{ min: [x, y], max: [x, y] }`.
2280
+
2281
+ ```javascript
2282
+ const c = circle2d(25);
2283
+ const b = c.bounds();
2284
+ // b.min ≈ [-25, -25], b.max ≈ [25, 25]
2285
+ ```
2286
+
2287
+ #### `.isEmpty(): boolean`
2288
+ Returns true if the sketch has no area.
2289
+
2290
+ #### `.numVert(): number`
2291
+ Returns the number of vertices in the contour.
2292
+
2293
+ #### `.toPolygons()`
2294
+ Returns raw polygon contours for rendering (internal use).
2295
+
2296
+ ## Type: Anchor
2297
+
2298
+ Anchor points for positioning sketches:
2299
+ - `'center'` — geometric center
2300
+ - `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'` — corners
2301
+ - `'top'`, `'bottom'`, `'left'`, `'right'` — edge midpoints
2302
+
2303
+ ## Dimensions
2304
+
2305
+ Use `dim()` / `dimLine()` for visual measurement callouts and report annotations.
2306
+ See [../output/dimensions.md](../output/dimensions.md) for options and ownership behavior.
2307
+
2308
+ ---
2309
+
2310
+ <!-- docs/permanent/API/model-building/sketch-primitives.md -->
2311
+
2312
+ # Sketch Primitives
2313
+
2314
+ 2D primitive shapes for creating sketches.
2315
+
2316
+ ## Functions
2317
+
2318
+ ### `rect(width, height, center?)`
2319
+ Creates a rectangle.
2320
+
2321
+ **Parameters:**
2322
+ - `width` (number) - Width
2323
+ - `height` (number) - Height
2324
+ - `center` (boolean, optional) - If true, centers at origin. Default: false (corner at origin)
2325
+
2326
+ ```javascript
2327
+ const r = rect(50, 30);
2328
+ const centered = rect(50, 30, true);
2329
+ ```
2330
+
2331
+ ### `circle2d(radius, segments?)`
2332
+ Creates a circle.
2333
+
2334
+ **Parameters:**
2335
+ - `radius` (number) - Circle radius
2336
+ - `segments` (number, optional) - Number of segments. Default: auto (smooth)
2337
+
2338
+ ```javascript
2339
+ const c = circle2d(25);
2340
+ const octagon = circle2d(25, 8);
2341
+ ```
2342
+
2343
+ ### `roundedRect(width, height, radius, center?)`
2344
+ Creates a rectangle with rounded corners.
2345
+
2346
+ **Parameters:**
2347
+ - `width` (number) - Width
2348
+ - `height` (number) - Height
2349
+ - `radius` (number) - Corner radius
2350
+ - `center` (boolean, optional) - If true, centers at origin. Default: false
2351
+
2352
+ ```javascript
2353
+ const rounded = roundedRect(60, 40, 5);
2354
+ ```
2355
+
2356
+ ### `polygon(points)`
2357
+ Creates a polygon from an array of [x, y] points or Point2D objects.
2358
+
2359
+ **Parameters:**
2360
+ - `points` (([number, number] | Point2D)[]) - Array of vertex coordinates or Point2D objects
2361
+
2362
+ ```javascript
2363
+ const triangle = polygon([[0, 0], [50, 0], [25, 40]]);
2364
+
2365
+ // Also accepts Point2D objects
2366
+ const p1 = point(0, 0), p2 = point(50, 0), p3 = point(25, 40);
2367
+ const triangle2 = polygon([p1, p2, p3]);
2368
+ ```
2369
+
2370
+ ### `ngon(sides, radius)`
2371
+ Creates a regular polygon (equilateral).
2372
+
2373
+ **Parameters:**
2374
+ - `sides` (number) - Number of sides
2375
+ - `radius` (number) - Radius from center to vertex
2376
+
2377
+ ```javascript
2378
+ const hex = ngon(6, 25);
2379
+ const triangle = ngon(3, 30);
2380
+ ```
2381
+
2382
+ ### `ellipse(rx, ry, segments?)`
2383
+ Creates an ellipse.
2384
+
2385
+ **Parameters:**
2386
+ - `rx` (number) - X radius
2387
+ - `ry` (number) - Y radius
2388
+ - `segments` (number, optional) - Number of segments. Default: 64
2389
+
2390
+ ```javascript
2391
+ const oval = ellipse(40, 20);
2392
+ ```
2393
+
2394
+ ### `slot(length, width)`
2395
+ Creates an oblong shape (rectangle with semicircle ends).
2396
+
2397
+ **Parameters:**
2398
+ - `length` (number) - Total length
2399
+ - `width` (number) - Width
2400
+
2401
+ ```javascript
2402
+ const oblong = slot(60, 20);
2403
+ ```
2404
+
2405
+ ### `star(points, outerRadius, innerRadius)`
2406
+ Creates a star shape.
2407
+
2408
+ **Parameters:**
2409
+ - `points` (number) - Number of star points
2410
+ - `outerRadius` (number) - Outer radius (tip of points)
2411
+ - `innerRadius` (number) - Inner radius (between points)
2412
+
2413
+ ```javascript
2414
+ const star5 = star(5, 30, 15);
2415
+ ```
2416
+
2417
+ ---
2418
+
2419
+ <!-- docs/permanent/API/model-building/sketch-path.md -->
2420
+
2421
+ # Sketch Path Builder
2422
+
2423
+ Fluent API for tracing 2D outlines point by point.
2424
+
2425
+ ## Class: PathBuilder
2426
+
2427
+ ### `path()`
2428
+ Creates a new path builder.
2429
+
2430
+ ```javascript
2431
+ const triangle = path()
2432
+ .moveTo(0, 0)
2433
+ .lineH(50)
2434
+ .lineV(30)
2435
+ .close();
2436
+ ```
2437
+
2438
+ ### Methods
2439
+
2440
+ #### `.moveTo(x, y)`
2441
+ Set starting point.
2442
+
2443
+ #### `.lineTo(x, y)`
2444
+ Line to absolute position.
2445
+
2446
+ #### `.lineH(dx)`
2447
+ Horizontal line (relative).
2448
+
2449
+ #### `.lineV(dy)`
2450
+ Vertical line (relative).
2451
+
2452
+ #### `.lineAngled(length, degrees)`
2453
+ Line at angle (0°=right, 90°=up).
2454
+
2455
+ #### `.close()`
2456
+ Close path into a `Sketch` (auto-fixes winding).
2457
+
2458
+ #### `.stroke(width, join?)`
2459
+ Thicken path into solid profile (see below).
2460
+
2461
+ ## Stroke
2462
+
2463
+ Thicken a polyline (centerline) into a solid profile with uniform width. Proper miter joins at vertices.
2464
+
2465
+ ### `path().stroke(width, join?)`
2466
+ ### `stroke(points, width, join?)`
2467
+
2468
+ **Parameters:**
2469
+ - `width` (number) — Profile thickness
2470
+ - `join` ('Square' | 'Round', optional) — Corner style. Default: 'Square' (miter)
2471
+
2472
+ **Returns:** `Sketch`
2473
+
2474
+ ```javascript
2475
+ // Fluent path builder
2476
+ const bracket = path()
2477
+ .moveTo(0, 0)
2478
+ .lineH(50)
2479
+ .lineV(-70)
2480
+ .lineAngled(20, 235)
2481
+ .stroke(4);
2482
+
2483
+ // Or with point array
2484
+ const bracket = stroke([[0, 0], [50, 0], [50, -70]], 4);
2485
+
2486
+ // Rounded corners
2487
+ const rounded = stroke([[0, 0], [50, 0], [50, -50]], 4, 'Round');
2488
+ ```
2489
+
2490
+ Use `stroke(..., 'Round')` for centerline-based geometry such as ribs, traces, and wire-like profiles. It is not the same as rounding selected corners of an existing closed polygon. For mixed sharp-and-rounded outlines, build the polygon first and use `filletCorners(...)`.
2491
+
2492
+ ---
2493
+
2494
+ <!-- docs/permanent/API/model-building/sketch-transforms.md -->
2495
+
2496
+ # Sketch Transforms
2497
+
2498
+ 2D transformations for sketches. All transforms are **chainable** and **immutable** (return new sketches). Colors are preserved through all transforms.
2499
+
2500
+ ## Methods
2501
+
2502
+ ### `.clone()` / `.duplicate()`
2503
+ Create an explicit copy handle of a sketch (same profile/color) so variants are easy to branch.
2504
+
2505
+ ```javascript
2506
+ const profile = rect(40, 20);
2507
+ const left = profile.clone().translate(-30, 0);
2508
+ const right = profile.duplicate().translate(30, 0);
2509
+ ```
2510
+
2511
+ ### `.translate(x, y?)`
2512
+ Moves the sketch.
2513
+
2514
+ ```javascript
2515
+ const moved = rect(50, 30).translate(100, 50);
2516
+ ```
2517
+
2518
+ ### `.rotate(degrees)`
2519
+ Rotates around the origin.
2520
+
2521
+ ```javascript
2522
+ const rotated = rect(50, 30).rotate(45);
2523
+ ```
2524
+
2525
+ ### `.rotateAround(degrees, pivot)`
2526
+ Rotates around a specific point instead of origin.
2527
+
2528
+ **Parameters:**
2529
+ - `degrees` (number) — Rotation angle
2530
+ - `pivot` ([number, number]) — Point to rotate around
2531
+
2532
+ ```javascript
2533
+ const hook = rect(4, 20).rotateAround(-35, [2, 0]);
2534
+ ```
2535
+
2536
+ ### `.scale(v)`
2537
+ Scales the sketch.
2538
+
2539
+ **Parameters:**
2540
+ - `v` (number | [number, number]) — Uniform scale or per-axis scale
2541
+
2542
+ ```javascript
2543
+ const bigger = circle2d(10).scale(2);
2544
+ const stretched = rect(10, 10).scale([2, 0.5]);
2545
+ ```
2546
+
2547
+ ### `.mirror(normal)`
2548
+ Mirrors across a line defined by its normal vector.
2549
+
2550
+ **Parameters:**
2551
+ - `normal` ([number, number]) — Line normal (doesn't need to be unit length)
2552
+
2553
+ ```javascript
2554
+ const mirrored = sketch.mirror([1, 0]); // Mirror across Y axis
2555
+ ```
2556
+
2557
+ ---
2558
+
2559
+ <!-- docs/permanent/API/model-building/sketch-booleans.md -->
2560
+
2561
+ # Sketch Booleans
2562
+
2563
+ 2D boolean operations for combining, subtracting, and intersecting sketches.
2564
+
2565
+ ## Methods
2566
+
2567
+ ### `.add(...others)`
2568
+ Combines sketches (union). Accepts `sketch.add(a, b)` and `sketch.add([a, b])`.
2569
+
2570
+ ```javascript
2571
+ const combined = rect(50, 30).add(
2572
+ circle2d(20).translate(25, 15),
2573
+ ngon(6, 15).translate(40, 15)
2574
+ );
2575
+ ```
2576
+
2577
+ ### `.subtract(...others)`
2578
+ Subtracts one or more sketches from this one. Accepts `sketch.subtract(a, b)` and `sketch.subtract([a, b])`.
2579
+
2580
+ ```javascript
2581
+ const plate = rect(100, 80);
2582
+ const hole = circle2d(10);
2583
+ const slotCut = rect(18, 8).translate(41, 36);
2584
+ const result = plate.subtract(hole.translate(25, 40), slotCut);
2585
+ ```
2586
+
2587
+ ### `.intersect(...others)`
2588
+ Keeps only the area shared by every operand. Accepts `sketch.intersect(a, b)` and `sketch.intersect([a, b])`.
2589
+
2590
+ ```javascript
2591
+ const overlap = rect(50, 50).intersect(
2592
+ circle2d(30).translate(25, 25),
2593
+ rect(40, 20).translate(5, 15)
2594
+ );
2595
+ ```
2596
+
2597
+ ## Functions
2598
+
2599
+ ### `union2d(...sketches)`
2600
+ Combines multiple sketches into one.
2601
+
2602
+ ```javascript
2603
+ const combined = union2d(
2604
+ rect(50, 30),
2605
+ circle2d(20).translate(25, 15),
2606
+ ngon(6, 15).translate(75, 15)
2607
+ );
2608
+ ```
2609
+
2610
+ `union2d([a, b, c])` is also supported when your sketches are already in an array.
2611
+
2612
+ ### `difference2d(...sketches)`
2613
+ Subtracts sketches[1..n] from sketches[0].
2614
+
2615
+ ```javascript
2616
+ const plate = rect(100, 80);
2617
+ const hole1 = circle2d(10).translate(25, 40);
2618
+ const hole2 = circle2d(10).translate(75, 40);
2619
+ const result = difference2d(plate, hole1, hole2);
2620
+ ```
2621
+
2622
+ `difference2d([base, cutter1, cutter2])` works too.
2623
+
2624
+ ### `intersection2d(...sketches)`
2625
+ Keeps only the area where all sketches overlap.
2626
+
2627
+ ```javascript
2628
+ const overlap = intersection2d(
2629
+ rect(50, 50),
2630
+ circle2d(30).translate(25, 25)
2631
+ );
2632
+ ```
2633
+
2634
+ `intersection2d([a, b, c])` is also supported.
2635
+
2636
+ ### `hull2d(...sketches)`
2637
+ Creates the convex hull of multiple sketches.
2638
+
2639
+ ```javascript
2640
+ const hull = hull2d(
2641
+ circle2d(10),
2642
+ circle2d(10).translate(50, 0),
2643
+ circle2d(10).translate(25, 40)
2644
+ );
2645
+ ```
2646
+
2647
+ `hull2d([a, b, c])` is also supported when your sketches are already in an array.
2648
+
2649
+ `hull2d()` is best for intentionally blended convex silhouettes. If you need true corner fillets while keeping some neighboring corners sharp, use `filletCorners(...)` instead.
2650
+
2651
+ ## Performance Note
2652
+
2653
+ The multi-argument functions (`union2d`, `difference2d`, `intersection2d`) use Manifold's batch operations internally, which are faster than chaining `.add()` / `.subtract()` calls one by one. Prefer them when combining many sketches.
2654
+
2655
+ ```javascript
2656
+ // Fast — single batch operation
2657
+ const combined = union2d(s1, s2, s3, s4, s5);
2658
+
2659
+ // Slower — sequential pairwise operations
2660
+ const combined = s1.add(s2).add(s3).add(s4).add(s5);
2661
+ ```
2662
+
2663
+ ---
2664
+
2665
+ <!-- docs/permanent/API/model-building/sketch-operations.md -->
2666
+
2667
+ # Sketch Operations
2668
+
2669
+ 2D operations for modifying sketch contours.
2670
+
2671
+ ## Methods
2672
+
2673
+ All operations preserve the sketch's color.
2674
+
2675
+ ### `.offset(delta, join?)`
2676
+ Inflate (positive) or deflate (negative) the contour.
2677
+
2678
+ **Parameters:**
2679
+ - `delta` (number) - Offset distance. Positive = outward, negative = inward
2680
+ - `join` ('Square' | 'Round' | 'Miter', optional) - Corner style. Default: 'Round'
2681
+
2682
+ ```javascript
2683
+ const outer = rect(50, 30).offset(5); // Expand by 5mm
2684
+ const inner = circle2d(20).offset(-2); // Shrink by 2mm
2685
+ const sharp = ngon(6, 20).offset(3, 'Miter');
2686
+ ```
2687
+
2688
+ Use the common `offset(-r).offset(+r)` pattern when you want to round **every convex corner** of a closed sketch.
2689
+
2690
+ ### `filletCorners(points, corners)`
2691
+ Round only specific convex corners of a polygon point list.
2692
+
2693
+ **Parameters:**
2694
+ - `points` (([number, number] | Point2D)[]) - Closed polygon vertices in order
2695
+ - `corners` (`{ index: number, radius: number, segments?: number }[]`) - Which vertices to fillet
2696
+
2697
+ **Returns:** `Sketch`
2698
+
2699
+ ```javascript
2700
+ const roofPoints = [
2701
+ [0, 0],
2702
+ [90, 0],
2703
+ [90, 44],
2704
+ [66, 74],
2705
+ [45, 86],
2706
+ [24, 74],
2707
+ [0, 44],
2708
+ ];
2709
+
2710
+ const roof = filletCorners(roofPoints, [
2711
+ { index: 3, radius: 19 },
2712
+ { index: 4, radius: 19 },
2713
+ { index: 5, radius: 19 },
2714
+ ]);
2715
+ ```
2716
+
2717
+ Notes:
2718
+ - only convex corners are supported
2719
+ - if two neighboring fillets would overlap on the same edge, the function throws
2720
+ - compare `polygon(points)` and `filletCorners(points, ...)` before extruding when debugging mixed sharp-and-rounded outlines
2721
+
2722
+ ## Choosing A Rounding Strategy
2723
+
2724
+ - `offset(-r).offset(+r)` rounds all convex corners of an existing closed profile
2725
+ - `stroke(points, width, 'Round')` thickens a centerline path; use it for ribs, traces, and wire-like geometry
2726
+ - `hull2d()` of circles creates a blended convex silhouette, closer to a capsule or cap than a true corner fillet
2727
+ - `filletCorners(points, ...)` is the right tool when some corners stay sharp and others need true tangent fillets
2728
+ - See `examples/api/sketch-rounding-strategies.forge.js` for a side-by-side comparison
2729
+
2730
+ ### `.hull()`
2731
+ Returns the convex hull of this sketch.
2732
+
2733
+ ```javascript
2734
+ const hull = complexShape.hull();
2735
+ ```
2736
+
2737
+ ### `.simplify(epsilon?)`
2738
+ Removes vertices that don't significantly affect the shape.
2739
+
2740
+ **Parameters:**
2741
+ - `epsilon` (number, optional) - Tolerance for vertex removal. Default: 1e-6
2742
+
2743
+ ```javascript
2744
+ const simplified = complexSketch.simplify(0.1);
2745
+ ```
2746
+
2747
+ ### `.warp(fn)`
2748
+ Warp vertices with an arbitrary function.
2749
+
2750
+ **Parameters:**
2751
+ - `fn` ((vert: [number, number]) => void) - Function that modifies vertex coordinates in-place
2752
+
2753
+ ```javascript
2754
+ const warped = rect(50, 50).warp(([x, y]) => {
2755
+ // Modify x and y in place
2756
+ x += Math.sin(y * 0.1) * 5;
2757
+ });
2758
+ ```
2759
+
2760
+ ---
2761
+
2762
+ <!-- docs/permanent/API/model-building/sketch-on-face.md -->
2763
+
2764
+ # Sketch On Face
2765
+
2766
+ Attach a 2D sketch to a 3D face so it renders in-place and extrudes along that face normal.
2767
+
2768
+ This supports:
2769
+ - canonical body faces: `front`, `back`, `left`, `right`, `top`, `bottom`
2770
+ - tracked planar faces on `TrackedShape`, like `side-left`
2771
+ - direct `FaceRef` targets from `tracked.face('top')`
2772
+ - supported compiler-owned created faces on `shell()` / `hole()` / `cutout()` results, such as `inner-side-right`, `floor`, `counterbore-floor`, and `wall-right`
2773
+ - supported compiler-owned created faces on `shell()` / `hole()` / `cutout()` results, such as `inner-side-right`, `floor`, `counterbore-floor`, and `wall-right`
2774
+ - defended preserved faces on compile-covered boolean results when one propagated descendant keeps a unique name
2775
+ - direct `FaceRef` targets from preserved/repeated descendants that still validate against a later compile-covered boolean target
2776
+
2777
+ ## `.onFace(parent, face, opts?)`
2778
+
2779
+ Places a sketch onto a parent face using face-local coordinates.
2780
+
2781
+ **Parameters:**
2782
+ - `parent` (`Shape | TrackedShape`) - target body
2783
+ - `face` (`'front' | 'back' | 'left' | 'right' | 'top' | 'bottom' | string | FaceRef`)
2784
+ - `opts` (object, optional):
2785
+ - `u` (number) - face-local horizontal offset from the face center
2786
+ - `v` (number) - face-local vertical offset from the face center
2787
+ - `protrude` (number) - offset along the face normal. Positive = outward
2788
+ - `selfAnchor` (`Anchor`) - which 2D sketch anchor aligns to the face center. Default: `'center'`
2789
+
2790
+ **Returns:** `Sketch`
2791
+
2792
+ ## `.onFace(faceRef, opts?)`
2793
+
2794
+ Places a sketch directly from a tracked or compiler-owned planar `FaceRef`.
2795
+
2796
+ This is useful when the script has already selected a face semantically:
2797
+
2798
+ ```javascript
2799
+ const panel = Rectangle2D.from3Points(
2800
+ point(-30, -18),
2801
+ point(28, -6),
2802
+ point(18, 24),
2803
+ ).extrude(16);
2804
+
2805
+ const cap = circle2d(5)
2806
+ .onFace(panel.face('top'), { u: 12, protrude: 0.05 })
2807
+ .extrude(1.2);
2808
+ ```
2809
+
2810
+ ```javascript
2811
+ const cup = roundedRect(70, 42, 5, true)
2812
+ .extrude(22)
2813
+ .shell(2, { openFaces: ['top'] });
2814
+
2815
+ const rib = rect(6, 4)
2816
+ .onFace(cup, 'inner-side-right', { u: 0, v: 0, protrude: 0.05 })
2817
+ .extrude(1.2);
2818
+ ```
2819
+
2820
+ ```javascript
2821
+ const body = box(120, 60, 40, true).color('#d8dce3');
2822
+
2823
+ const badge = roundedRect(28, 10, 2, true)
2824
+ .onFace(body, 'front', { v: 8 })
2825
+ .extrude(2)
2826
+ .color('#1d2733');
2827
+
2828
+ return [
2829
+ { name: 'Body', shape: body },
2830
+ { name: 'Badge', shape: badge },
2831
+ ];
2832
+ ```
2833
+
2834
+ ## Face-local coordinates
2835
+
2836
+ - Canonical faces:
2837
+ - `front` / `back`: `u = X`, `v = Z`
2838
+ - `left` / `right`: `u` runs across the face, `v = Z`
2839
+ - `top` / `bottom`: `u = X`, `v` runs across the face
2840
+ - Tracked planar faces use their own stored local frame:
2841
+ - side faces of extruded rectangles: `u` follows the source edge, `v = Z`
2842
+ - tracked `top` / `bottom` faces follow the source sketch axes
2843
+ - direct `FaceRef` placement uses that face's `uAxis` / `vAxis`
2844
+ - supported shell inner walls, blind-hole floors, counterbore shoulder floors, and defended cut walls reuse compiler-owned local frames for downstream workplanes
2845
+ - supported shell inner walls, blind-hole floors, counterbore shoulder floors, and defended cut walls reuse compiler-owned local frames for downstream workplanes
2846
+ - compile-covered `Shape` targets now resolve defended named faces through the shared face-query table before falling back to bare canonical body heuristics
2847
+
2848
+ The sketch's local `+Z` becomes the face normal, so `extrude(positive)` goes outward from that face.
2849
+
2850
+ ## Notes
2851
+
2852
+ - This is a planar face-placement feature, not arbitrary curved-surface projection.
2853
+ - Tracked curved faces like `cylinder(...).face('side')` are rejected because they do not have a planar sketch frame.
2854
+ - Supported created-face names on compiler-owned feature results are intentionally narrow, but defended split descendants now stay visible as semantic regions where Forge can keep one stable source surface.
2855
+ - Hole/cut host faces, supported `upToFace` termination faces, and defended boolean-difference / boolean-intersection descendants can now stay queryable as face regions instead of collapsing straight to "missing face".
2856
+ - Coplanar boolean face sets now stay placeable through `onFace(shape, name, ...)` when Forge can defend one shared planar frame; non-coplanar sets stay explicit and reject planar placement honestly.
2857
+ - The placed sketch still supports normal 2D operations like `translate`, `rotate`, `scale`, and sketch booleans before extrusion.
2858
+ - If multiple sketches share the same face placement, their 2D booleans preserve that shared placement.
2859
+ - If booleans mix sketches with different 3D placements, the result drops back to an unplaced sketch.
2860
+ - Extruding a placed sketch keeps the tracked `top` / `bottom` / `side` metadata from that extrusion, transformed into world space.
2861
+ - Projection-driven follow-on sketches now keep compiler-visible provenance when you `projectToPlane()` a compatible projected source back onto a matching parallel plane. The defended exact subset now covers straight extrusions plus compatible shell/hole/cut/union descendants that reduce to one planar projection basis, but arbitrary projection targets still stay runtime-only.
2862
+
2863
+ ---
2864
+
2865
+ <!-- docs/permanent/API/model-building/sketch-extrude.md -->
2866
+
2867
+ # Sketch Extrude & Revolve
2868
+
2869
+ Convert 2D sketches into 3D shapes through extrusion or revolution. The sketch's color (if set) is carried over to the resulting Shape.
2870
+
2871
+ If a sketch has been placed with [`onFace()`](sketch-on-face.md), extrusion follows that face normal instead of the global Z axis.
2872
+
2873
+ ## Methods
2874
+
2875
+ ### `.extrude(height, options?)`
2876
+ Extrudes sketch along Z axis.
2877
+
2878
+ **Parameters:**
2879
+ - `height` (number) - Extrusion height
2880
+ - `options` (object, optional):
2881
+ - `twist` (number) - Twist angle in degrees
2882
+ - `divisions` (number) - Number of twist steps (needed for twist)
2883
+ - `scaleTop` (number | [number, number]) - Scale factor at top
2884
+ - `center` (boolean) - Center along Z axis
2885
+
2886
+ **Returns:** `TrackedShape` (with faces: top, bottom, side)
2887
+
2888
+ ```javascript
2889
+ const simple = rect(50, 30).extrude(10);
2890
+
2891
+ const twisted = ngon(6, 20).extrude(60, {
2892
+ twist: 90,
2893
+ divisions: 32
2894
+ });
2895
+
2896
+ const tapered = circle2d(20).extrude(50, {
2897
+ scaleTop: 0.5
2898
+ });
2899
+
2900
+ const badge = roundedRect(28, 10, 2, true)
2901
+ .onFace(box(120, 60, 40, true), 'front', { v: 8 })
2902
+ .extrude(2);
2903
+ ```
2904
+
2905
+ ### `.revolve(degrees?, segments?)`
2906
+ Revolves sketch around Y axis (becomes Z in result).
2907
+
2908
+ Performance tip: prefer `revolve()` over `loft()` whenever the part is rotationally symmetric. Loft is for profile interpolation and is substantially heavier.
2909
+
2910
+ **Parameters:**
2911
+ - `degrees` (number, optional) - Rotation angle. Default: 360 (full revolution)
2912
+ - `segments` (number, optional) - Number of segments. Default: auto
2913
+
2914
+ **Returns:** `Shape`
2915
+
2916
+ ```javascript
2917
+ // Vase profile
2918
+ const profile = polygon([[20, 0], [25, 30], [20, 60]]);
2919
+ const vase = profile.revolve();
2920
+
2921
+ // Partial revolution (C-shape)
2922
+ const partial = rect(5, 40).translate(20, 0).revolve(270);
2923
+ ```
2924
+
2925
+ ---
2926
+
2927
+ <!-- docs/permanent/API/model-building/sketch-anchor.md -->
2928
+
2929
+ # Sketch Anchor Positioning
2930
+
2931
+ Position sketches relative to each other using named anchor points.
2932
+
2933
+ ## Methods
2934
+
2935
+ ### `.attachTo(target, targetAnchor, selfAnchor?, offset?)`
2936
+ Position a sketch relative to another using named anchor points.
2937
+
2938
+ **Parameters:**
2939
+ - `target` (Sketch) — The sketch to attach to
2940
+ - `targetAnchor` (Anchor) — Point on target: 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right', 'top', 'bottom', 'left', 'right'
2941
+ - `selfAnchor` (Anchor, optional) — Point on this sketch to align. Default: 'center'
2942
+ - `offset` ([number, number], optional) — Additional offset after alignment
2943
+
2944
+ **Returns:** `Sketch`
2945
+
2946
+ ```javascript
2947
+ const plate = rect(50, 4);
2948
+ const arm = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left');
2949
+ return union2d(plate, arm);
2950
+
2951
+ // With offset: attach then shift 5mm right
2952
+ const shifted = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left', [5, 0]);
2953
+ ```
2954
+
2955
+ ## Anchor Points
2956
+
2957
+ Available anchor positions:
2958
+ - `'center'` — geometric center
2959
+ - `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'` — corners
2960
+ - `'top'`, `'bottom'`, `'left'`, `'right'` — edge midpoints
2961
+
2962
+
2963
+ ### 4. Entities and Topology (for tracked references, constraints, patterns)
2964
+
2965
+ Named entities, tracked 3D topology, constraints, patterns, fillet/chamfer helpers.
2966
+
2967
+ <!-- docs/permanent/API/model-building/entities.md -->
2968
+
2969
+ # Entity-Based API
2970
+
2971
+ Named geometric entities with stable identity, topology tracking, and constraint integration.
2972
+
2973
+ ## 2D Entities
2974
+
2975
+ ### `point(x, y)` / `new Point2D(x, y)`
2976
+ A named 2D point.
2977
+
2978
+ ```javascript
2979
+ const p = point(10, 20);
2980
+ p.distanceTo(point(30, 40)); // distance
2981
+ p.midpointTo(point(30, 40)); // midpoint
2982
+ p.translate(5, 5); // new point
2983
+ p.toTuple(); // [10, 20]
2984
+ ```
2985
+
2986
+ ### `line(x1, y1, x2, y2)` / `Line2D`
2987
+ A named 2D line segment.
2988
+
2989
+ ```javascript
2990
+ const l = line(0, 0, 50, 0);
2991
+ l.length; // 50
2992
+ l.midpoint; // Point2D
2993
+ l.angle; // degrees
2994
+ l.direction; // [1, 0]
2995
+ l.parallel(10); // parallel line offset by 10
2996
+
2997
+ // Line-line intersection (infinite lines)
2998
+ const l2 = line(25, -10, 25, 40);
2999
+ l.intersect(l2); // Point2D(25, 0) — treats as infinite lines
3000
+ l.intersectSegment(l2); // Point2D or null — only if segments actually cross
3001
+
3002
+ // Construction methods
3003
+ Line2D.fromCoordinates(0, 0, 50, 0);
3004
+ Line2D.fromPointAndAngle(point(0, 0), 45, 100);
3005
+ Line2D.fromPointAndDirection(point(0, 0), [1, 1], 50);
3006
+ ```
3007
+
3008
+ ### `circle(cx, cy, radius)` / `Circle2D`
3009
+ A named 2D circle.
3010
+
3011
+ ```javascript
3012
+ const c = circle(0, 0, 25);
3013
+ c.diameter; // 50
3014
+ c.circumference; // ~157
3015
+ c.area; // ~1963
3016
+ c.pointAtAngle(90); // Point2D at top
3017
+
3018
+ // Extrude to cylinder with topology
3019
+ const cyl = c.extrude(30);
3020
+ cyl.face('top'); // FaceRef (planar)
3021
+ cyl.face('side'); // FaceRef (curved, planar === false)
3022
+
3023
+ // Construction methods
3024
+ Circle2D.fromCenterAndRadius(point(0, 0), 25);
3025
+ Circle2D.fromDiameter(point(0, 0), 50);
3026
+ ```
3027
+
3028
+ ### `rectangle(x, y, w, h)` / `Rectangle2D`
3029
+ A rectangle with named sides and vertices.
3030
+
3031
+ ```javascript
3032
+ const r = rectangle(0, 0, 100, 60);
3033
+
3034
+ // Named sides
3035
+ r.side('top'); // Line2D
3036
+ r.side('bottom'); // Line2D
3037
+ r.side('left'); // Line2D
3038
+ r.side('right'); // Line2D
3039
+ r.sideAt(0); // bottom (by index)
3040
+
3041
+ // Named vertices
3042
+ r.vertex('top-left'); // Point2D
3043
+ r.vertex('bottom-right'); // Point2D
3044
+
3045
+ // Properties
3046
+ r.width; // 100
3047
+ r.height; // 60
3048
+ r.center; // Point2D
3049
+
3050
+ // Diagonals — returns [bl-tr, br-tl] as Line2D pair
3051
+ const [d1, d2] = r.diagonals();
3052
+ const center = d1.intersect(d2); // Point2D at center
3053
+
3054
+ // Convert to Sketch for rendering
3055
+ r.toSketch();
3056
+
3057
+ // Extrude to 3D with topology tracking
3058
+ const tracked = r.extrude(20); // TrackedShape
3059
+
3060
+ // Construction methods
3061
+ Rectangle2D.fromDimensions(0, 0, 100, 60);
3062
+ Rectangle2D.fromCenterAndDimensions(point(50, 30), 100, 60);
3063
+ Rectangle2D.from2Corners(point(0, 0), point(100, 60));
3064
+ Rectangle2D.from3Points(p1, p2, p3); // free-angle rectangle
3065
+ ```
3066
+
3067
+ ## 3D Topology (TrackedShape)
3068
+
3069
+ When you extrude a `Rectangle2D`, you get a `TrackedShape` that knows its faces and edges by name.
3070
+
3071
+ ```javascript
3072
+ const rect = Rectangle2D.fromCenterAndDimensions(point(0, 0), 100, 60);
3073
+ const box = rect.extrude(20);
3074
+
3075
+ // Named faces
3076
+ box.face('top'); // FaceRef { normal, center, planar, uAxis, vAxis }
3077
+ box.face('bottom');
3078
+ box.face('side-left');
3079
+ box.face('side-right');
3080
+ box.face('side-top'); // the side from rect's top edge
3081
+ box.face('side-bottom'); // the side from rect's bottom edge
3082
+
3083
+ // Named edges
3084
+ box.edge('top-left'); // EdgeRef { start, end } — top face, left side
3085
+ box.edge('bottom-right'); // bottom face, right side
3086
+ box.edge('vert-bl'); // vertical edge at bottom-left corner
3087
+
3088
+ // List all
3089
+ box.faceNames(); // ['top', 'bottom', 'side-bottom', 'side-right', 'side-top', 'side-left']
3090
+ box.edgeNames(); // all 12 edges
3091
+
3092
+ // Use the underlying Shape for booleans
3093
+ const result = box.toShape().subtract(cylinder(25, 10));
3094
+
3095
+ // Translate preserves topology
3096
+ const moved = box.translate(50, 0, 0);
3097
+ moved.face('top').center; // shifted by [50, 0, 0]
3098
+
3099
+ // Duplicate preserves topology metadata too
3100
+ const copy = box.clone();
3101
+ copy.face('side-left');
3102
+ ```
3103
+
3104
+ ## Constraint Helpers
3105
+
3106
+ ```javascript
3107
+ const sketch = constrainedSketch();
3108
+ const p1 = sketch.point(0, 0, true);
3109
+ const p2 = sketch.point(50, 0);
3110
+ const p3 = sketch.point(50, 30);
3111
+ const l1 = sketch.line(p1, p2);
3112
+ const l2 = sketch.line(p2, p3);
3113
+
3114
+ Constraint.horizontal(sketch, l1);
3115
+ Constraint.vertical(sketch, l2);
3116
+ Constraint.length(sketch, l1, 50);
3117
+ Constraint.perpendicular(sketch, l1, l2);
3118
+
3119
+ const result = sketch.close().solve();
3120
+ ```
3121
+
3122
+ ### Entity-aware constraints
3123
+
3124
+ Constraint functions accept `Point2D`/`Line2D` directly — they auto-import into the builder:
3125
+
3126
+ ```javascript
3127
+ const sketch = constrainedSketch();
3128
+ const myLine = line(0, 0, 50, 0);
3129
+ const myRect = rectangle(10, 10, 40, 30);
3130
+
3131
+ // Pass Line2D directly — auto-imported
3132
+ Constraint.makeParallel(sketch, myLine, myRect.side('top'));
3133
+ Constraint.horizontal(sketch, myLine);
3134
+ ```
3135
+
3136
+ ### Importing entities into a constrained sketch
3137
+
3138
+ ```javascript
3139
+ const sketch = constrainedSketch();
3140
+ const r = rectangle(0, 0, 100, 60);
3141
+ const sides = sketch.importRectangle(r);
3142
+ // sides.bottom, sides.right, sides.top, sides.left are LineIds
3143
+ // sides.points is [bl, br, tr, tl] PointIds
3144
+
3145
+ Constraint.horizontal(sketch, sides.bottom);
3146
+ Constraint.length(sketch, sides.bottom, 100);
3147
+ ```
3148
+
3149
+
3150
+ ## Patterns
3151
+
3152
+ ### `linearPattern(shape, count, dx, dy, dz?)`
3153
+ Repeat a shape along a direction vector, returning the union.
3154
+
3155
+ ```javascript
3156
+ const bolt = cylinder(10, 3);
3157
+ const row = linearPattern(bolt, 5, 20, 0); // 5 bolts, 20mm apart along X
3158
+ ```
3159
+
3160
+ ### `circularPattern(shape, count, centerX?, centerY?)`
3161
+ Repeat a shape around the Z axis, returning the union.
3162
+
3163
+ ```javascript
3164
+ const hole = cylinder(12, 4).translate(30, 0, -1);
3165
+ const holes = circularPattern(hole, 8); // 8 holes evenly spaced
3166
+ ```
3167
+
3168
+ ### `mirrorCopy(shape, normal)`
3169
+ Mirror a shape and union with the original.
3170
+
3171
+ ```javascript
3172
+ const half = box(50, 30, 10);
3173
+ const full = mirrorCopy(half, [1, 0, 0]); // Mirror across YZ plane
3174
+ ```
3175
+
3176
+ For compile-covered source shapes, repeated instances created by `linearPattern`, `circularPattern`, `Shape.mirror()`, and `mirrorCopy()` keep distinct compiler owner lineage. Supported boolean unions now preserve owner-scoped canonical face queries for those repeated descendants, so later compiler inspections can still trace which repeated instance a preserved face came from. Durable post-merge face identity is still narrower than full CAD-style topology naming: reusing the same owner lineage twice without a fresh mirror/pattern owner is reported as ambiguous, and downstream subtract/intersect rewrites still record split descendants explicitly instead of guessing.
3177
+
3178
+ ## Utility Functions
3179
+
3180
+ ### `degrees(deg)` / `radians(rad)`
3181
+ Angle conversion helpers for readability:
3182
+
3183
+ ```javascript
3184
+ degrees(45); // 45 (identity — just for clarity)
3185
+ radians(Math.PI / 4); // 45 (converts radians to degrees)
3186
+ ```
3187
+
3188
+ ## Fillets & Chamfers
3189
+
3190
+ ### `filletEdge(shape, edge, radius, quadrant?, segments?)`
3191
+ Compiler-owned edge fillet for the current tracked-edge subset.
3192
+
3193
+ Supported today:
3194
+ - tracked vertical edges from compile-covered `box()` bodies
3195
+ - tracked vertical edges from `rectangle(...).extrude(...)`
3196
+ - rigid transforms between the tracked source body and the target shape
3197
+ - untouched sibling tracked vertical edges after earlier supported `filletEdge(...)` / `chamferEdge(...)` rewrites on the same body
3198
+ - preserved propagated vertical-edge queries after those supported edge-finish rewrites when a later supported boolean union keeps one defended edge lineage
3199
+
3200
+ Still out of subset today:
3201
+ - the selected edge after an earlier `filletEdge(...)` / `chamferEdge(...)` rewrite as a new single finish target, because Forge now records that path as an explicit descendant edge-chain rather than pretending it stayed one edge
3202
+ - edge descendants after shell, hole/cut, trim, boolean difference/intersection, or boolean unions that did not already record one supported propagated edge lineage for the selection
3203
+ - generic sketch extrudes, tapered extrudes, and arbitrary feature-created edges
3204
+
3205
+ Canonical quadrants for the supported rectangle/box edges:
3206
+ - `vert-bl` -> `[1, -1]`
3207
+ - `vert-br` -> `[-1, -1]`
3208
+ - `vert-tr` -> `[-1, 1]`
3209
+ - `vert-tl` -> `[1, 1]`
3210
+
3211
+ ```javascript
3212
+ const b = rectangle(0, 0, 50, 50).extrude(20);
3213
+ const filleted = filletEdge(b.toShape(), b.edge('vert-br'), 5, [-1, -1]);
3214
+ ```
3215
+
3216
+ ### `chamferEdge(shape, edge, size, quadrant?)`
3217
+ Compiler-owned edge chamfer for the same tracked vertical-edge subset as `filletEdge(...)`.
3218
+
3219
+ ```javascript
3220
+ const b = rectangle(0, 0, 50, 50).extrude(20);
3221
+ const chamfered = chamferEdge(b.toShape(), b.edge('vert-br'), 3, [-1, -1]);
3222
+ ```
3223
+
3224
+ ## Arc Bridge
3225
+
3226
+ ### `arcBridgeBetweenRects(rectA, rectB, segments?)`
3227
+ Build a smooth arc surface connecting two rectangular areas. Automatically finds the closest pair of parallel edges and bridges them with a semicircular arc.
3228
+
3229
+ **Parameters:**
3230
+ - `rectA` — `Rectangle2D` or `{ corners: [[x,y,z], [x,y,z], [x,y,z], [x,y,z]] }`
3231
+ - `rectB` — same format as rectA
3232
+ - `segments` (number, optional) — Arc smoothness. Default: 12
3233
+
3234
+ **Returns:** `Shape` — thin arc solid
3235
+
3236
+ ```javascript
3237
+ // 2D rectangles (z=0)
3238
+ const base = rectangle(0, 0, 300, 200);
3239
+ const screen = rectangle(0, 200, 300, 200);
3240
+ const hinge = arcBridgeBetweenRects(base, screen, 16);
3241
+ ```
3242
+
3243
+ ```javascript
3244
+ // 3D corners for non-planar rectangles
3245
+ const hinge = arcBridgeBetweenRects(
3246
+ { corners: [[0,0,0], [300,0,0], [300,200,0], [0,200,0]] },
3247
+ { corners: [[0,200,15], [300,200,15], [300,400,15], [0,400,15]] },
3248
+ 16,
3249
+ );
3250
+ ```
3251
+
3252
+
3253
+ ### 5. Assemblies and Mechanisms (for joints or kinematics)
3254
+
3255
+ Assembly graph, joint types, couplings, validation, robot export.
3256
+
3257
+ <!-- docs/permanent/API/model-building/assembly.md -->
3258
+
3259
+ # Assembly + Mechanism API
3260
+
3261
+ Use this API when your model is a mechanism, not a single booleaned solid.
3262
+
3263
+ ## Mental model
3264
+ - `Part` = manufacturable object (shape + metadata)
3265
+ - `Joint` = relationship between parent and child part
3266
+ - `State` = current joint values
3267
+ - `Solve` = compute world transforms for all parts
3268
+ - `Validate` = collisions / clearances / sweep checks
3269
+
3270
+ ## Quick start
3271
+
3272
+ ```javascript
3273
+ const mech = assembly("Arm")
3274
+ .addPart("base", box(80, 80, 20, true), {
3275
+ metadata: { material: "PETG", process: "FDM", qty: 1 },
3276
+ })
3277
+ .addPart("link", box(140, 24, 24).translate(0, -12, -12))
3278
+ .addJoint("shoulder", "revolute", "base", "link", {
3279
+ axis: [0, 1, 0],
3280
+ min: -30,
3281
+ max: 120,
3282
+ default: 25,
3283
+ frame: Transform.identity().translate(0, 0, 20),
3284
+ });
3285
+
3286
+ const solved = mech.solve();
3287
+ return solved.toScene();
3288
+ ```
3289
+
3290
+ ## Ergonomic helpers
3291
+ - `addFrame(name, { transform? })` adds a virtual reference frame (no geometry)
3292
+ - `addRevolute(name, parent, child, opts)` shorthand for `addJoint(..., "revolute", ...)`
3293
+ - `addPrismatic(name, parent, child, opts)` shorthand for `addJoint(..., "prismatic", ...)`
3294
+ - `addFixed(name, parent, child, opts)` shorthand for `addJoint(..., "fixed", ...)`
3295
+ - `addJointCoupling(jointName, { terms, offset? })` links joints with linear relationships
3296
+ - `addGearCoupling(drivenJoint, driverJoint, opts)` links revolute joints using gear ratios
3297
+
3298
+ ## Joint couplings
3299
+
3300
+ Use couplings when one joint should be derived from other joints.
3301
+
3302
+ Formula:
3303
+ - `driven = offset + Σ(ratio_i * source_i)`
3304
+
3305
+ Example:
3306
+
3307
+ ```javascript
3308
+ const mech = assembly("Differential")
3309
+ .addFrame("Base")
3310
+ .addFrame("Turret")
3311
+ .addFrame("Wheel")
3312
+ .addFrame("TopInput")
3313
+ .addRevolute("Steering", "Base", "Turret", { axis: [0, 0, 1] })
3314
+ .addRevolute("WheelDrive", "Turret", "Wheel", { axis: [1, 0, 0] })
3315
+ .addRevolute("TopGear", "Base", "TopInput", { axis: [0, 0, 1] })
3316
+ .addJointCoupling("TopGear", {
3317
+ terms: [
3318
+ { joint: "Steering", ratio: 1 },
3319
+ { joint: "WheelDrive", ratio: 20 / 14 },
3320
+ ],
3321
+ });
3322
+ ```
3323
+
3324
+ Notes:
3325
+ - Coupled joints ignore direct values in `solve(state)` and emit a warning.
3326
+ - Coupling cycles are rejected.
3327
+ - `sweepJoint(...)` cannot sweep a coupled target; sweep one of its source joints instead.
3328
+
3329
+ ## Gear couplings
3330
+
3331
+ Use this helper to connect two **revolute** joints as a gear mesh without manually writing `addJointCoupling(...)`.
3332
+
3333
+ ```javascript
3334
+ const pair = lib.gearPair({
3335
+ pinion: { module: 1.25, teeth: 14, faceWidth: 8 },
3336
+ gear: { module: 1.25, teeth: 42, faceWidth: 8 },
3337
+ });
3338
+
3339
+ const mech = assembly("Spur Stage")
3340
+ .addFrame("Base")
3341
+ .addFrame("PinionPart")
3342
+ .addFrame("GearPart")
3343
+ .addRevolute("Pinion", "Base", "PinionPart", { axis: [0, 0, 1] })
3344
+ .addRevolute("Driven", "Base", "GearPart", { axis: [0, 0, 1] })
3345
+ .addGearCoupling("Driven", "Pinion", { pair }); // uses pair.jointRatio
3346
+ ```
3347
+
3348
+ `addGearCoupling(...)` ratio sources (choose exactly one):
3349
+ - `ratio` (explicit multiplier)
3350
+ - `pair` (`lib.gearPair(...)`, `lib.bevelGearPair(...)`, or `lib.faceGearPair(...)` result using `pair.jointRatio`)
3351
+ - `driverTeeth` + `drivenTeeth` (auto ratio; `internal` mesh is positive, `external`/`bevel`/`face` are negative)
3352
+
3353
+ For bevel stages, pairing helpers also return placement aids:
3354
+ - `pinionAxis`, `gearAxis`
3355
+ - `pinionCenter`, `gearCenter`
3356
+
3357
+ For face stages, use `centerDistance` and `meshPlaneZ` from `lib.faceGearPair(...)`; with `place: true`, the face gear stays on the Z axis and the vertical spur is placed at `[centerDistance, 0, meshPlaneZ]`.
3358
+
3359
+ ## Joint frames
3360
+
3361
+ `frame` is a transform from the **parent part frame** to the **joint frame at zero state**.
3362
+
3363
+ For a child part:
3364
+
3365
+ Matrix form:
3366
+ - `childWorld = parentWorld * frame * motion(value) * childBase`
3367
+
3368
+ Forge chain form:
3369
+ - `childWorld = composeChain(childBase, motion(value), frame, parentWorld)`
3370
+
3371
+ This keeps kinematic chains declarative and avoids repeated manual pivot math.
3372
+
3373
+ ## Validation helpers
3374
+ - `solved.collisionReport()` returns overlapping part pairs and volume
3375
+ - `solved.minClearance("PartA", "PartB", 10)` computes minimum gap
3376
+ - `assembly.sweepJoint("elbow", -20, 140, 24)` samples motion and reports collisions
3377
+
3378
+ Notebook-friendly pattern:
3379
+
3380
+ ```javascript
3381
+ const solved = mech.solve({ shoulder: 35, elbow: 60 });
3382
+ console.log("Collisions", solved.collisionReport());
3383
+
3384
+ const sweep = mech.sweepJoint("elbow", -10, 135, 12, { shoulder: 35 });
3385
+ console.log("Sweep collisions", sweep.filter((step) => step.collisions.length > 0).length);
3386
+
3387
+ show(solved.toScene());
3388
+ ```
3389
+
3390
+ That keeps mechanism setup in earlier cells and collision/sweep investigation in the current preview cell.
3391
+
3392
+ ## Common pitfalls
3393
+ - If parts vanish in the viewport, check whether a cut plane is active before debugging kinematics. The viewer-side APIs live in [../runtime/viewport.md](../runtime/viewport.md).
3394
+ - If a returned object is empty, Forge logs a warning in script output.
3395
+
3396
+ ## Metadata
3397
+ - `addPart(..., { metadata })` attaches per-part metadata to an assembly part.
3398
+ - BOM/report helpers such as `solved.bom()` and `solved.bomCsv()` live in [../output/bom.md](../output/bom.md).
3399
+
3400
+ ## Naming grouped assembly children
3401
+
3402
+ When an assembly part is a `ShapeGroup`, Forge flattens the group into separate viewport objects. To avoid opaque labels like `Base Assembly.1`, name the group children explicitly:
3403
+
3404
+ ```javascript
3405
+ const housing = group(
3406
+ { name: "Body", shape: body },
3407
+ { name: "Lid", shape: lid },
3408
+ );
3409
+
3410
+ const mech = assembly("Case")
3411
+ .addPart("Base Assembly", housing);
3412
+ ```
3413
+
3414
+ That produces labels such as `Base Assembly.Body` and `Base Assembly.Lid`.
3415
+
3416
+ ## Robot export
3417
+
3418
+ Use `robotExport({...})` when an assembly should become a simulator package instead of only a viewport scene.
3419
+
3420
+ ```javascript
3421
+ const rover = assembly("Scout")
3422
+ .addPart("Chassis", box(300, 220, 50, true))
3423
+ .addPart("Left Wheel", cylinder(30, 60, undefined, 48, true).pointAlong([0, 1, 0]))
3424
+ .addPart("Right Wheel", cylinder(30, 60, undefined, 48, true).pointAlong([0, 1, 0]))
3425
+ .addRevolute("leftWheel", "Chassis", "Left Wheel", {
3426
+ axis: [0, 1, 0],
3427
+ frame: Transform.identity().translate(90, 140, 60),
3428
+ effort: 20,
3429
+ velocity: 1080,
3430
+ })
3431
+ .addRevolute("rightWheel", "Chassis", "Right Wheel", {
3432
+ axis: [0, 1, 0],
3433
+ frame: Transform.identity().translate(90, -140, 60),
3434
+ effort: 20,
3435
+ velocity: 1080,
3436
+ });
3437
+
3438
+ robotExport({
3439
+ assembly: rover,
3440
+ modelName: "Scout",
3441
+ links: {
3442
+ Chassis: { massKg: 10 },
3443
+ "Left Wheel": { massKg: 0.8 },
3444
+ "Right Wheel": { massKg: 0.8 },
3445
+ },
3446
+ plugins: {
3447
+ diffDrive: {
3448
+ leftJoints: ["leftWheel"],
3449
+ rightJoints: ["rightWheel"],
3450
+ wheelSeparationMm: 280,
3451
+ wheelRadiusMm: 60,
3452
+ },
3453
+ },
3454
+ world: {
3455
+ generateDemoWorld: true,
3456
+ },
3457
+ });
3458
+ ```
3459
+
3460
+ Notes:
3461
+ - Revolute joint `velocity` values are expressed in degrees/second in Forge; the SDF exporter converts them to radians/second.
3462
+ - Prismatic distances are authored in millimeters and exported in meters.
3463
+ - `massKg` is preferred for demo robots; `densityKgM3` is a decent fallback when mass is unknown.
3464
+
3465
+
3466
+ ### 6. Runtime Viewport APIs (for cut planes, jointsView, and animation playback)
3467
+
3468
+ Viewer-only APIs such as cutPlane, explodeView, jointsView, and animation behavior.
3469
+
3470
+ <!-- docs/permanent/API/runtime/viewport.md -->
3471
+
3472
+ # Viewport Runtime APIs
3473
+
3474
+ These APIs affect the viewer and scene presentation. They do not change the underlying model geometry contract, so they are not part of the required model-building reading set.
3475
+
3476
+ ## `cutPlane(name, normal, offsetOrOptions?, options?)`
3477
+
3478
+ Define a named section plane for inspection.
3479
+
3480
+ **Parameters:**
3481
+ - `name` (string) - label shown in the viewport controls
3482
+ - `normal` (`[number, number, number]`) - direction toward the clipped side
3483
+ - `offsetOrOptions` (number or object, optional):
3484
+ - number: plane offset from origin along `normal`
3485
+ - object: `{ offset?: number, exclude?: string | string[] }`
3486
+ - `options` (object, optional; used with numeric offset):
3487
+ - `exclude` (`string | string[]`) - returned object `name` values to keep uncut
3488
+
3489
+ **Returns:** `void`
3490
+
3491
+ ```javascript
3492
+ const cutZ = param("Cut Height", 10, { min: -50, max: 50, unit: "mm" });
3493
+
3494
+ cutPlane("Inspection", [0, 0, 1], cutZ, {
3495
+ exclude: ["Probe", "Fasteners"],
3496
+ });
3497
+ ```
3498
+
3499
+ Notes:
3500
+ - planes are registered per script run
3501
+ - viewport toggle state persists across parameter changes
3502
+ - clipping is applied to returned named objects, so `exclude` only works when names are stable
3503
+ - newly exposed section faces render with a hatched overlay; pre-existing coplanar boundary faces are left unhatched
3504
+
3505
+ ## `explodeView(options?)`
3506
+
3507
+ Override how the viewport explode slider offsets returned objects.
3508
+
3509
+ Explode offsets are resolved from the returned object tree, not from a flat list.
3510
+ In `radial` mode each node follows its parent branch direction, then adds a smaller
3511
+ local fan from the immediate parent/subassembly center, so nested assemblies peel
3512
+ apart level by level without losing their branch structure.
3513
+
3514
+ In fixed-axis or fixed-vector modes, the branch itself follows that axis/vector, but
3515
+ nested descendants fan out perpendicular to the branch by default so deep trees do
3516
+ not keep stacking farther along the same axis.
3517
+
3518
+ By default this is container-oriented: named groups/subassemblies advance along the
3519
+ tree, while plain leaves inside a group stay much closer and mostly fan locally
3520
+ around their parent cluster unless you override them explicitly.
3521
+
3522
+ **Parameters:**
3523
+ - `enabled` (boolean) - disable explode offsets for this script when `false`
3524
+ - `amountScale` (number) - multiply the UI explode amount
3525
+ - `stages` (number[]) - per-depth multipliers (depth 1 = first level, defaults to `1, 1/2, 1/3, ...`)
3526
+ - `mode` (`'radial' | 'x' | 'y' | 'z' | [x, y, z]`) - default explode direction
3527
+ - `axisLock` (`'x' | 'y' | 'z'`) - optional global axis lock
3528
+ - `byName` (`Record<string, { stage?, direction?, axisLock? }>`)- per-object overrides keyed by returned object `name`
3529
+ - `byPath` (`Record<string, { stage?, direction?, axisLock? }>`)- per-tree-path overrides using slash-separated object tree paths such as `"Drive/Shaft"`
3530
+
3531
+ **Returns:** `void`
3532
+
3533
+ ```javascript
3534
+ explodeView({
3535
+ amountScale: 1.2,
3536
+ stages: [0.35, 0.8],
3537
+ mode: 'radial',
3538
+ byPath: {
3539
+ "Drive/Shaft": { direction: [1, 0, 0], stage: 1.6 },
3540
+ },
3541
+ });
3542
+ ```
3543
+
3544
+ ## `jointsView(options?)`
3545
+
3546
+ Register viewport-only mechanism controls that animate returned objects without rerunning the script.
3547
+
3548
+ Use this when you want interactive articulation in the viewer but the geometry itself stays fixed.
3549
+
3550
+ Animation values are interpolated linearly between keyframes. Forge does **not**
3551
+ auto-wrap revolute values across `-180/180` or `0/360` for you, because doing
3552
+ that globally would break intentional multi-turn tracks.
3553
+
3554
+ **Key options:**
3555
+ - `enabled`
3556
+ - `joints`: `{ name, child, parent?, type?, axis?, pivot?, min?, max?, default?, unit? }[]`
3557
+ - `couplings`: `{ joint, terms, offset? }[]`
3558
+ - `animations`: `{ name, duration?, loop?, continuous?, keyframes }[]`
3559
+ - `defaultAnimation`
3560
+
3561
+ ```javascript
3562
+ jointsView({
3563
+ joints: [
3564
+ {
3565
+ name: "Shoulder",
3566
+ child: "Upper Arm",
3567
+ parent: "Base",
3568
+ type: "revolute",
3569
+ axis: [0, -1, 0],
3570
+ pivot: [0, 0, 46],
3571
+ min: -30,
3572
+ max: 110,
3573
+ default: 15,
3574
+ },
3575
+ ],
3576
+ animations: [
3577
+ {
3578
+ name: "Walk Cycle",
3579
+ duration: 1.6,
3580
+ loop: true,
3581
+ keyframes: [
3582
+ { at: 0.0, values: { "Shoulder": 20 } },
3583
+ { at: 0.5, values: { "Shoulder": -10 } },
3584
+ { at: 1.0, values: { "Shoulder": 20 } },
3585
+ ],
3586
+ },
3587
+ ],
3588
+ });
3589
+ ```
3590
+
3591
+ `continuous: true` is for looping tracks that should keep accumulating across
3592
+ cycles instead of snapping back to the first keyframe each time. Use it for
3593
+ monotonic multi-turn drives such as `0 -> 360 -> 720`.
3594
+
3595
+ ### Animation continuity for revolute joints
3596
+
3597
+ If an animation channel comes from `atan2(...)`, `normalizeAngleDeg(...)`, or
3598
+ any other wrapped angle source, keep the sampled keyframes continuous before
3599
+ passing them to `jointsView()`.
3600
+
3601
+ Bad branch-cut sample stream:
3602
+
3603
+ ```javascript
3604
+ keyframes: [
3605
+ { at: 0.48, values: { "Power Rod": -171 } },
3606
+ { at: 0.50, values: { "Power Rod": -180 } },
3607
+ { at: 0.52, values: { "Power Rod": 171 } },
3608
+ ]
3609
+ ```
3610
+
3611
+ That `-180 -> 171` jump is interpreted literally and the viewer will spin the
3612
+ part the long way around.
3613
+
3614
+ Good continuous sample stream:
3615
+
3616
+ ```javascript
3617
+ keyframes: [
3618
+ { at: 0.48, values: { "Power Rod": -171 } },
3619
+ { at: 0.50, values: { "Power Rod": -180 } },
3620
+ { at: 0.52, values: { "Power Rod": -189 } },
3621
+ ]
3622
+ ```
3623
+
3624
+ Guidelines:
3625
+ - Keep high-speed multi-turn joints authored as continuous angles (`0`, `360`,
3626
+ `720`, etc.).
3627
+ - Only unwrap channels that represent cyclic angles. Do not apply angle
3628
+ unwrapping blindly to prismatic or other scalar values.
3629
+ - If you build sampled helper utilities, let them unwrap a named set of joints
3630
+ instead of guessing from every numeric channel.
3631
+
3632
+ ## `viewConfig(options?)`
3633
+
3634
+ Configure viewport helper visuals for the current script.
3635
+
3636
+ Current support:
3637
+ - `jointOverlay.enabled`
3638
+ - joint overlay colors such as `axisColor`, `axisCoreColor`, `arcColor`, `zeroColor`
3639
+ - joint overlay sizing and tessellation controls such as `axisLengthScale`, `arcVisualLimitDeg`, `arcStepDeg`
3640
+
3641
+ **Returns:** `void`
3642
+
3643
+ ```javascript
3644
+ viewConfig({
3645
+ jointOverlay: {
3646
+ axisColor: "#13dfff",
3647
+ arcColor: "#ff7a1a",
3648
+ axisLineRadiusScale: 0.03,
3649
+ arcLineRadiusScale: 0.022,
3650
+ },
3651
+ });
3652
+ ```
3653
+
3654
+ ## `lib.explode(items, options?)`
3655
+
3656
+ Apply deterministic exploded-view offsets to an assembly tree while preserving names, colors, and nesting.
3657
+
3658
+ `radial` separation is branch-aware and parent-relative: each child follows the
3659
+ direction of its parent branch, then fans out locally inside that branch. This keeps
3660
+ subassemblies visually grouped while still letting their internals break apart.
3661
+
3662
+ For non-radial fixed-axis or fixed-vector modes, nested descendants keep the branch
3663
+ offset but spread perpendicular to it by default.
3664
+
3665
+ Default behavior is tree-like rather than flat: containers separate recursively,
3666
+ while unconfigured leaves inside a container use a smaller local fan so sibling parts
3667
+ stay visually associated with their parent group.
3668
+
3669
+ Works with:
3670
+ - arrays of shapes/sketches/named items
3671
+ - nested `{ name, group: [...] }` structures
3672
+ - `ShapeGroup` outputs
3673
+
3674
+ **Parameters:**
3675
+ - `items` (`ExplodeItem[] | ShapeGroup`)
3676
+ - `options`:
3677
+ - `amount` (number)
3678
+ - `stages` (number[])
3679
+ - `mode` (`'radial' | 'x' | 'y' | 'z' | [x, y, z]`)
3680
+ - `axisLock` (`'x' | 'y' | 'z'`)
3681
+ - `byName`
3682
+ - `byPath`
3683
+
3684
+ Named items may also include:
3685
+ - `explode: { stage?, direction?, axisLock? }`
3686
+
3687
+ **Returns:** same structure type as input, with translated geometry
3688
+
3689
+ ```javascript
3690
+ const explodeAmt = param("Explode", 0, { min: 0, max: 40, unit: "mm" });
3691
+
3692
+ return lib.explode(assembly, {
3693
+ amount: explodeAmt,
3694
+ stages: [0.4, 0.8],
3695
+ mode: 'radial',
3696
+ byName: {
3697
+ "Shaft": { direction: [1, 0, 0], stage: 1.4 },
3698
+ },
3699
+ });
3700
+ ```
3701
+
3702
+
3703
+ ### 7. Recipes and Debugging (for patterns and troubleshooting)
3704
+
3705
+ Modeling patterns, debugging tactics, copyable snippets.
3706
+
3707
+ <!-- docs/permanent/API/guides/modeling-recipes.md -->
3708
+
3709
+ # Modeling Recipes
3710
+
3711
+ This file collects patterns, best practices, debugging tips, and example snippets that are useful once you already know the model-building API.
3712
+
3713
+ ## Iteration Bias
3714
+
3715
+ - For unfamiliar or high-risk geometry work, start in a notebook and keep setup, experiments, and validation in separate cells until the structure is obvious.
3716
+ - Default to a buildable first pass instead of a long proposal when the user clearly wants geometry changed.
3717
+ - Replace a broken or incoherent model wholesale when that is faster and cleaner than incremental patching.
3718
+ - Keep printed hardware structurally honest: use it for guides, spacers, retainers, and moderate-load mechanisms; use wood or metal for primary strength.
3719
+ - Validate early with `forgecad run <file>` and refine from the actual runtime result.
3720
+ - Prefer a few clean part files over one giant script once a design has repeated hardware or a small mechanism.
3721
+
3722
+ Notebook helpers worth using during iteration:
3723
+
3724
+ - `show(...)` pins the current intermediate geometry in the viewport
3725
+ - `forgecad notebook view <file> preview` prints the preview cell with stored outputs in the terminal
3726
+ - `forgecad run <file>.forge-notebook.json` validates the preview cell and runs the usual spatial analysis
3727
+
3728
+ ## Common Patterns
3729
+
3730
+ ### Parametric Box with Holes
3731
+ ```javascript
3732
+ const w = param("Width", 80, { min: 40, max: 150, unit: "mm" });
3733
+ const h = param("Height", 60, { min: 30, max: 100, unit: "mm" });
3734
+ const t = param("Thickness", 5, { min: 2, max: 10, unit: "mm" });
3735
+ const holeD = param("Hole Diameter", 8, { min: 4, max: 20, unit: "mm" });
3736
+
3737
+ const base = box(w, h, t);
3738
+ const hole = cylinder(t + 2, holeD / 2).translate(w / 2, h / 2, -1);
3739
+
3740
+ return base.subtract(hole);
3741
+ ```
3742
+
3743
+ ### Hollow Shell (Wall Thickness)
3744
+ ```javascript
3745
+ const outer = param("Outer Size", 50, { min: 20, max: 100, unit: "mm" });
3746
+ const wall = param("Wall", 3, { min: 1, max: 10, unit: "mm" });
3747
+
3748
+ const outerBox = box(outer, outer, outer, true);
3749
+ const innerBox = box(outer - 2 * wall, outer - 2 * wall, outer - 2 * wall, true);
3750
+
3751
+ return outerBox.subtract(innerBox);
3752
+ ```
3753
+
3754
+ ### Array/Pattern
3755
+ ```javascript
3756
+ const count = param("Count", 5, { min: 2, max: 10 });
3757
+ const spacing = param("Spacing", 15, { min: 5, max: 30, unit: "mm" });
3758
+
3759
+ let shapes = [];
3760
+ for (let i = 0; i < count; i++) {
3761
+ shapes.push(cylinder(10, 5).translate(i * spacing, 0, 0));
3762
+ }
3763
+
3764
+ return union(...shapes);
3765
+ ```
3766
+
3767
+ ### Sketch-Based Design
3768
+ ```javascript
3769
+ const sides = param("Sides", 6, { min: 3, max: 12 });
3770
+ const radius = param("Radius", 25, { min: 10, max: 50, unit: "mm" });
3771
+ const height = param("Height", 60, { min: 20, max: 120, unit: "mm" });
3772
+ const wall = param("Wall", 3, { min: 1, max: 8, unit: "mm" });
3773
+
3774
+ const outer = ngon(sides, radius);
3775
+ const inner = ngon(sides, radius - wall);
3776
+ const profile = outer.subtract(inner);
3777
+
3778
+ return profile.extrude(height, { twist: 45, divisions: 32 });
3779
+ ```
3780
+
3781
+ ### Rounded Profiles
3782
+ ```javascript
3783
+ const base = rect(50, 30).offset(-3, 'Round').offset(3, 'Round');
3784
+ return base.extrude(10);
3785
+ ```
3786
+
3787
+ Use that pattern when every convex corner should round. For mixed sharp-and-rounded outlines, fillet only the intended vertices instead:
3788
+
3789
+ ```javascript
3790
+ const roofPoints = [
3791
+ [0, 0],
3792
+ [90, 0],
3793
+ [90, 44],
3794
+ [66, 74],
3795
+ [45, 86],
3796
+ [24, 74],
3797
+ [0, 44],
3798
+ ];
3799
+
3800
+ const roof = filletCorners(roofPoints, [
3801
+ { index: 3, radius: 19 },
3802
+ { index: 4, radius: 19 },
3803
+ { index: 5, radius: 19 },
3804
+ ]);
3805
+
3806
+ return roof.extrude(12);
3807
+ ```
3808
+
3809
+ ### Chamfers and Fillets
3810
+ ```javascript
3811
+ const part = box(50, 50, 20);
3812
+ const chamfer = box(10, 60, 10)
3813
+ .rotate(0, 45, 0)
3814
+ .translate(50, -5, 15);
3815
+
3816
+ return part.subtract(chamfer);
3817
+ ```
3818
+
3819
+ ### Choosing the right sketch-rounding tool
3820
+
3821
+ - `offset(-r).offset(+r)` for rounding every convex corner of a closed outline
3822
+ - `stroke(points, width, 'Round')` for centerline-based geometry such as ribs or traces
3823
+ - `hull2d()` of circles for a blended cap/capsule silhouette
3824
+ - `filletCorners(points, ...)` for selective true-corner fillets on mixed profiles
3825
+
3826
+ ## Best Practices
3827
+
3828
+ ### Performance
3829
+ - Boolean operations are expensive; minimize them
3830
+ - Use parameters for values that might change
3831
+ - Avoid deep nesting of operations in loops
3832
+
3833
+ ### Readability
3834
+ ```javascript
3835
+ const base = box(100, 100, 10);
3836
+ const hole = cylinder(12, 8);
3837
+ const result = base.subtract(hole.translate(50, 50, 0));
3838
+ return result;
3839
+ ```
3840
+
3841
+ Prefer named intermediate values over deeply nested one-liners.
3842
+
3843
+ ### Units
3844
+ - All dimensions are millimeters by default
3845
+ - Angles are degrees
3846
+ - Use the `unit` parameter option when it helps the reader
3847
+
3848
+ ### Centering
3849
+ ```javascript
3850
+ const centered = box(50, 50, 50, true).translate(x, y, z);
3851
+ const corner = box(50, 50, 50).translate(x - 25, y - 25, z - 25);
3852
+ ```
3853
+
3854
+ Centered primitives are usually easier to position.
3855
+
3856
+ ## Debugging
3857
+
3858
+ ### Console Output
3859
+ ```javascript
3860
+ console.log("Width:", width);
3861
+ console.log("Volume:", shape.volume());
3862
+ ```
3863
+
3864
+ ### Incremental Building
3865
+ ```javascript
3866
+ const base = box(50, 50, 10);
3867
+ // return base;
3868
+
3869
+ const withHole = base.subtract(cylinder(12, 5).translate(25, 25, 0));
3870
+ // return withHole;
3871
+
3872
+ return withHole.add(cylinder(20, 3).translate(25, 25, 10));
3873
+ ```
3874
+
3875
+ For sketch-heavy work, compare the raw profile and the rounded profile before extruding:
3876
+
3877
+ ```javascript
3878
+ const raw = polygon(roofPoints);
3879
+ const rounded = filletCorners(roofPoints, [
3880
+ { index: 3, radius: 19 },
3881
+ { index: 4, radius: 19 },
3882
+ { index: 5, radius: 19 },
3883
+ ]);
3884
+
3885
+ return [
3886
+ { name: "Raw", sketch: raw },
3887
+ { name: "Rounded", sketch: rounded.translate(120, 0) },
3888
+ ];
3889
+ ```
3890
+
3891
+ ## Error Handling
3892
+
3893
+ Common errors:
3894
+ - `"Kernel not initialized"` - internal/runtime issue, reload the app
3895
+ - `"Cannot read property of undefined"` - usually a bad variable name or missing declaration
3896
+ - invalid geometry - commonly caused by zero dimensions or self-intersecting sketches
3897
+ - script execution error - inspect the JS error in console output
3898
+
3899
+ ## Example Snippets
3900
+
3901
+ ### Parametric Phone Stand
3902
+ ```javascript
3903
+ const width = param("Width", 80, { min: 40, max: 150, unit: "mm" });
3904
+ const depth = param("Depth", 60, { min: 30, max: 100, unit: "mm" });
3905
+ const thick = param("Thickness", 5, { min: 2, max: 15, unit: "mm" });
3906
+ const backH = param("Back Height", 40, { min: 20, max: 80, unit: "mm" });
3907
+ const cableD = param("Cable Hole", 8, { min: 4, max: 15, unit: "mm" });
3908
+
3909
+ const base = box(width, depth, thick);
3910
+ const back = box(width, thick, backH).translate(0, depth - thick, thick);
3911
+ const lip = box(width, 10, 8).translate(0, 0, thick);
3912
+ const hole = cylinder(thick + 2, cableD / 2)
3913
+ .rotate(90, 0, 0)
3914
+ .translate(width / 2, depth / 2, -1);
3915
+
3916
+ return union(base, back, lip).subtract(hole);
3917
+ ```
3918
+
3919
+ ### Multi-Object Scene with Colors
3920
+ ```javascript
3921
+ const base = box(100, 100, 5).color('#888888');
3922
+ const col1 = cylinder(40, 5).translate(20, 20, 5).color('#cc4444');
3923
+ const col2 = cylinder(40, 5).translate(80, 20, 5).color('#4444cc');
3924
+ const col3 = cylinder(40, 5).translate(50, 80, 5).color('#44cc44');
3925
+ const top = box(100, 100, 3).translate(0, 0, 45).color('#888888');
3926
+
3927
+ return [
3928
+ { name: "Base", shape: base },
3929
+ { name: "Column A", shape: col1 },
3930
+ { name: "Column B", shape: col2 },
3931
+ { name: "Column C", shape: col3 },
3932
+ { name: "Top", shape: top },
3933
+ ];
3934
+ ```
3935
+
3936
+ ### Entity-Based Design with Topology
3937
+ ```javascript
3938
+ const baseRect = rectangle(0, 0, 80, 60);
3939
+ const base = baseRect.extrude(20);
3940
+
3941
+ const result = filletEdge(base.toShape(), base.edge('vert-br'), 8, [-1, -1])
3942
+ .hole(base.face('top'), { diameter: 6, u: -16, v: 10, depth: 8 });
3943
+
3944
+ const holes = circularPattern(
3945
+ cylinder(25, 4).translate(40, 30, -1),
3946
+ 4, 40, 30,
3947
+ );
3948
+
3949
+ return result.subtract(holes);
3950
+ ```
3951
+
3952
+ Use the original tracked body (`base`) when you need semantic faces after edge finishing, and keep using its untouched sibling vertical tracked edges if you apply another supported fillet/chamfer later. Those sibling edges can now also survive a later supported union when the compiler still records one preserved propagated edge lineage for them. The currently selected finished edge is still recorded as a merged descendant set, so Forge does not claim a new durable tracked edge for that rewritten corner yet.
3953
+
3954
+ For larger runnable examples, read `examples/api/`.
3955
+
3956
+
3957
+ ### 8. CLI and Exports (for validation/render/export tasks)
3958
+
3959
+ Test-run, notebook execution, export pipelines, debug flags.
3960
+
3961
+ <!-- docs/permanent/CLI.md -->
3962
+
3963
+ # ForgeCAD CLI
3964
+
3965
+ ## Architecture
3966
+
3967
+ All CLI tools share the **same forge engine** as the browser UI. There is one source of truth for geometry logic — no code duplication.
3968
+
3969
+ ```
3970
+ src/forge/headless.ts ← Single entry point for all contexts
3971
+ ├── kernel.ts ← Manifold WASM wrapper (Shape, box, cylinder, sphere, etc.)
3972
+ ├── runner.ts ← Script sandbox (Function() with full forge API injected)
3973
+ ├── section.ts ← Plane intersection / projection
3974
+ ├── sketch/ ← Complete 2D sketch system (primitives, transforms, booleans,
3975
+ │ constraints, entities, topology, patterns, fillets, arc bridge)
3976
+ ├── params.ts ← Parameter system
3977
+ ├── library.ts ← Part library
3978
+ ├── meshToGeometry.ts ← Manifold mesh → Three.js BufferGeometry
3979
+ └── sceneBuilder.ts ← Three.js scene setup (lighting, camera, materials)
3980
+ ```
3981
+
3982
+ **Browser** imports via `src/forge/index.ts` → re-exports from `headless.ts`.
3983
+ **CLI tools** import directly from `src/forge/headless.ts`.
3984
+
3985
+ The key function is `runScript(code, fileName, allFiles)` — it wraps user code in a `Function()` sandbox with the entire forge API injected, and transpiles project files so standard JS `import` / `export` / `require(...)` work for shared utility modules. CLI scripts just call `init()` + `runScript()` and work with the results.
3986
+
3987
+ ## Install
3988
+
3989
+ Install the package and link the local binary once:
3990
+
3991
+ ```bash
3992
+ npm install
3993
+ npm link
3994
+ ```
3995
+
3996
+ After that, use `forgecad ...` directly from your shell.
3997
+
3998
+ ### Shell Autocomplete
3999
+
4000
+ ForgeCAD now ships shell completion scripts in the usual modern-tool style:
4001
+
4002
+ ```bash
4003
+ forgecad completion bash
4004
+ forgecad completion zsh
4005
+ forgecad completion fish
4006
+ ```
4007
+
4008
+ Quick install:
4009
+
4010
+ ```bash
4011
+ # bash
4012
+ echo 'source <(forgecad completion bash)' >> ~/.bashrc
4013
+
4014
+ # zsh
4015
+ mkdir -p ~/.zsh/completions
4016
+ forgecad completion zsh > ~/.zsh/completions/_forgecad
4017
+ echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
4018
+ echo 'autoload -Uz compinit && compinit' >> ~/.zshrc
4019
+
4020
+ # fish
4021
+ mkdir -p ~/.config/fish/completions
4022
+ forgecad completion fish > ~/.config/fish/completions/forgecad.fish
4023
+ ```
4024
+
4025
+ The completions are contextual:
4026
+
4027
+ - nested subcommands such as `forgecad notebook view` and `forgecad export step`
4028
+ - command-specific flags and common enum values
4029
+ - ForgeCAD file suggestions where a command expects `.forge.js`, `.sketch.js`, or `.forge-notebook.json`
4030
+
4031
+ ## Available Commands
4032
+
4033
+ ### Notebook Cells (server-backed)
4034
+
4035
+ Forge notebooks live in `.forge-notebook.json` files and behave like lightweight Jupyter notebooks for ForgeCAD code cells.
4036
+
4037
+ The browser and CLI both use the Vite server for notebook execution. The CLI does not run Forge locally for notebook cells; it auto-starts or reuses the Forge server, sends the cell code, then prints the returned output summary.
4038
+
4039
+ Append a new code cell and run it immediately in one command:
4040
+
4041
+ ```bash
4042
+ forgecad notebook examples/demo.forge-notebook.json --code "show(box(40, 20, 10));"
4043
+ ```
4044
+
4045
+ If the target notebook file does not exist yet, append mode auto-creates it first with the default ForgeCAD notebook structure, then adds the new cell.
4046
+
4047
+ Or pipe a larger cell in through stdin:
4048
+
4049
+ ```bash
4050
+ cat /tmp/cell.js | forgecad notebook examples/demo.forge-notebook.json
4051
+ ```
4052
+
4053
+ Re-run the last preview cell, or a specific cell id:
4054
+
4055
+ ```bash
4056
+ forgecad notebook examples/demo.forge-notebook.json
4057
+ forgecad notebook run examples/demo.forge-notebook.json <cell-id>
4058
+ ```
4059
+
4060
+ View the notebook in the terminal without dumping raw JSON:
4061
+
4062
+ ```bash
4063
+ forgecad notebook view examples/demo.forge-notebook.json
4064
+ forgecad notebook view examples/demo.forge-notebook.json preview
4065
+ forgecad notebook view examples/demo.forge-notebook.json 2
4066
+ ```
4067
+
4068
+ `view` is local-only. It parses the notebook JSON and renders notebook metadata, numbered source lines, and stored outputs for each cell. The optional selector accepts a 1-based cell number, an exact cell id, or `preview`.
4069
+
4070
+ `run`/`view` expect the notebook file to already exist. Auto-creation only applies to append flows (`--code`, `--file`, stdin, or the explicit `append` subcommand).
4071
+
4072
+ Export a notebook into a plain `.forge.js` script:
4073
+
4074
+ ```bash
4075
+ forgecad notebook export examples/demo.forge-notebook.json
4076
+ forgecad notebook export examples/demo.forge-notebook.json out/demo-from-notebook.forge.js
4077
+ ```
4078
+
4079
+ If you already have a Forge server running, point the CLI at it:
4080
+
4081
+ ```bash
4082
+ forgecad notebook examples/demo.forge-notebook.json --server http://localhost:5173 --code "show(box(40, 20, 10));"
4083
+ ```
4084
+
4085
+ Notebook paths are resolved from the shell working directory before the CLI calls the server, so the server's opened project root does not add an extra path prefix.
4086
+
4087
+ Notebook cell behavior:
4088
+
4089
+ - Cells share state top-to-bottom
4090
+ - `show(value)` pins the geometry that should stay visible in the viewport
4091
+ - A trailing expression is also treated as the cell value
4092
+ - Cell outputs are written back into the notebook JSON, similar to Jupyter
4093
+
4094
+ For the `forgecad` entrypoints below, passing a `.forge-notebook.json` uses that notebook's preview cell. That means you can inspect with `view`, validate with `run`, and render or capture the current preview without exporting first.
4095
+
4096
+ ### Script Validation
4097
+
4098
+ ```bash
4099
+ forgecad run examples/cup.forge.js
4100
+ forgecad run examples/api/notebook-iteration.forge-notebook.json
4101
+ forgecad run examples/cup.forge.js --debug-imports
4102
+ ```
4103
+
4104
+ Runs a `.forge.js`, `.sketch.js`, or notebook preview cell in the real runtime and prints object stats, diagnostics, and execution time.
4105
+
4106
+ `--debug-imports` adds an import trace (source file, target file, overrides, return type, success/error phase), useful when debugging `importPart()`/`importSketch()` behavior.
4107
+
4108
+ ### SVG Export (no browser needed)
4109
+
4110
+ ```bash
4111
+ forgecad export svg examples/frame.sketch.js [output.svg]
4112
+ ```
4113
+
4114
+ Runs a `.sketch.js` script in Node.js using the real forge engine and outputs SVG. No browser, no Puppeteer — pure Node.
4115
+
4116
+ **How it works:** Initializes the Manifold WASM kernel, runs the script through `runScript()`, extracts the Sketch result, converts polygons to SVG paths.
4117
+
4118
+ ### STEP / BREP Export (exact subset, Python + CadQuery)
4119
+
4120
+ ```bash
4121
+ forgecad export step examples/api/brep-exportable.forge.js
4122
+ forgecad export brep examples/api/brep-exportable.forge.js
4123
+
4124
+ # Optional overrides:
4125
+ forgecad export step examples/api/brep-exportable.forge.js --output out/demo.step
4126
+ forgecad export step examples/api/brep-exportable.forge.js --python 3.11
4127
+ forgecad export step examples/api/brep-exportable.forge.js --uv /custom/path/to/uv
4128
+ forgecad export step examples/chess-set.forge.js --allow-faceted
4129
+ ```
4130
+
4131
+ This exporter is `uv`-first. `cli/forge-brep-export.py` carries inline dependency metadata, so `uv run` provisions CadQuery automatically for the exporter environment.
4132
+
4133
+ By default this exporter is exact-subset only. It does **not** silently convert arbitrary triangle meshes back into fake BREP. Instead, Forge lowers compile-covered geometry into the `cadquery-occt` compiler target and exports that exact subset through CadQuery/OpenCascade.
4134
+
4135
+ If you pass `--allow-faceted`, unsupported closed mesh solids are exported as explicit faceted OCCT solids. This keeps hull-heavy designs exportable to STEP/BREP, but that fallback is tessellation-driven rather than exact replay.
4136
+
4137
+ The maintained feature matrix lives in [`docs/permanent/API/output/brep-export.md`](API/output/brep-export.md).
4138
+
4139
+ If any returned solid object falls outside the exact subset, the CLI fails with a reason instead of silently exporting degraded geometry. When a scene mixes solids and 2D sketches, the exact solids export and the sketch-only objects are skipped with a warning.
4140
+
4141
+ With `--allow-faceted`, mesh-solid blockers that still lack an exact replay plan are exported as faceted solids instead of failing. The CLI prints which objects used the fallback.
4142
+
4143
+ For coverage runs across many examples, use the `uv` matrix scripts:
4144
+
4145
+ ```bash
4146
+ uv run scripts/brep/matrix.py --format step examples
4147
+ uv run scripts/brep/matrix.py --format brep examples
4148
+ uv run scripts/brep/rerun_failures.py tmp/brep-matrix-step-20260306T120000Z.json
4149
+ ```
4150
+
4151
+ These scripts use the repo-local `.venv-brep/.venv/bin/python` by default, run exports through a bounded parallel worker pool, and write JSON reports under `tmp/`.
4152
+
4153
+ ### SDF Robot Export (Gazebo package)
4154
+
4155
+ ```bash
4156
+ forgecad export sdf examples/api/sdf-rover-demo.forge.js
4157
+
4158
+ # Optional output directory:
4159
+ forgecad export sdf examples/api/sdf-rover-demo.forge.js --output out/forge_scout
4160
+ ```
4161
+
4162
+ This exporter writes a Gazebo-friendly package workspace:
4163
+
4164
+ - `models/<model-name>/model.sdf`
4165
+ - `models/<model-name>/model.config`
4166
+ - `models/<model-name>/meshes/*.stl`
4167
+ - `worlds/<world-name>.sdf` when the script requests a demo world
4168
+ - `manifest.json` with topic names, link/joint mappings, and exporter warnings
4169
+
4170
+ The script must call `robotExport({...})` with an `assembly(...)` graph. The exporter uses the declared parts + joints directly; it does **not** try to infer a robot from flattened scene meshes.
4171
+
4172
+ When `world.generateDemoWorld` and `world.keyboardTeleop.enabled` are on, the exported world includes both:
4173
+
4174
+ - Gazebo's GUI `KeyPublisher` plugin
4175
+ - server-side `TriggeredPublisher` bindings that map arrow keys to the diff-drive `cmd_vel` topic
4176
+
4177
+ Recommended launch flow:
4178
+
4179
+ ```bash
4180
+ export GZ_SIM_RESOURCE_PATH="$PWD/out/forge_scout/models${GZ_SIM_RESOURCE_PATH:+:$GZ_SIM_RESOURCE_PATH}"
4181
+
4182
+ # Terminal 1: server
4183
+ gz sim -s -r out/forge_scout/worlds/forge_scout_trial.sdf
4184
+
4185
+ # Terminal 2: GUI client using the same world layout
4186
+ gz sim -g out/forge_scout/worlds/forge_scout_trial.sdf
4187
+ ```
4188
+
4189
+ Notes:
4190
+
4191
+ - On macOS, use the split `-s` / `-g` flow above. `gz sim <world.sdf>` is not supported there.
4192
+ - Click the 3D view so it has keyboard focus, then use `W` / `X` for forward / reverse, `A` / `D` to rotate, `Q` / `E` / `Z` / `C` for diagonals, and `S` or `Space` to stop.
4193
+ - For older exports created before the GUI plugin was added, load `Key Publisher` manually from the Gazebo GUI plugins menu.
4194
+
4195
+ Current behavior:
4196
+
4197
+ - Per-link geometry is exported as STL mesh assets
4198
+ - Collision geometry reuses the same mesh unless `collision: 'none'` is set on a link
4199
+ - Link mass comes from `massKg`, else `densityKgM3 * volume`, else a default density
4200
+ - Inertia is an approximate box fit based on link bounds
4201
+ - Coupled joints are currently rejected
4202
+ - Parts without geometry are currently rejected
4203
+
4204
+ ### PNG Render (requires Chrome)
4205
+
4206
+ ```bash
4207
+ forgecad render examples/cup.forge.js [output.png]
4208
+ forgecad render examples/api/notebook-iteration.forge-notebook.json [output.png]
4209
+ forgecad render examples/cup.forge.js out/scene.png --scene '{"camera":{"projectionMode":"perspective","position":[200,-160,120],"target":[0,0,20],"up":[0,0,1]},"objects":{"obj-2":{"visible":false},"obj-3":{"opacity":0.35}}}'
4210
+ ```
4211
+
4212
+ Renders 3D shapes to PNG images from multiple camera angles. Uses Puppeteer to launch headless Chrome with WebGL for Three.js rendering.
4213
+
4214
+ When the input is a notebook, `forgecad render` renders the notebook's preview cell.
4215
+
4216
+ **How it works:**
4217
+ 1. `cli/forge-render.mjs` — Node launcher script. Auto-starts Vite dev server if not running, launches Puppeteer.
4218
+ 2. `cli/render.html` + `cli/render.ts` — Loaded in the browser by Puppeteer. Imports from `src/forge/headless.ts`, runs the script, builds a Three.js scene, renders from multiple angles.
4219
+ 3. Screenshots are captured as base64 PNG and saved to disk.
4220
+
4221
+ **Environment variables:**
4222
+
4223
+ | Variable | Default | Description |
4224
+ |----------|---------|-------------|
4225
+ | `FORGE_ANGLES` | `front,side,top,iso` | Camera angles to render |
4226
+ | `FORGE_SIZE` | `1024` | Image size in pixels |
4227
+ | `FORGE_PORT` | `5173` | Vite dev server port |
4228
+ | `CHROME_PATH` | Auto-detected | Chrome/Chromium executable path |
4229
+
4230
+ **CLI options:**
4231
+ - `--angles <front,side,top,iso>` — standard angles to render
4232
+ - `--size <px>` — output size override
4233
+ - `--port <n>` — Vite port override
4234
+ - `--camera <spec>` — exact camera pose, e.g. `proj=perspective;pos=120,80,120;target=0,0,0;up=0,0,1`
4235
+ - `--scene <json>` — full scene state copied from the viewport, including camera plus object visibility/opacity/color overrides
4236
+ - `--background <color>` — background override
4237
+ - `--chrome-path <path>` — Chrome executable path override
4238
+
4239
+ **Camera angles:** `front` (−Y), `back` (+Y), `side` (+X), `top` (+Z), `iso` (diagonal)
4240
+
4241
+ ### Animated Capture (GIF or MP4, requires Chrome)
4242
+
4243
+ ```bash
4244
+ forgecad capture gif examples/cup.forge.js [output.gif]
4245
+ forgecad capture mp4 examples/cup.forge.js [output.mp4]
4246
+ forgecad capture gif examples/api/notebook-assembly-debug.forge-notebook.json --list
4247
+ forgecad capture mp4 examples/api/runtime-joints-view.forge.js out/step.mp4 --capture animation --animation Step
4248
+ forgecad capture gif examples/3d-printer.forge.js out/section.gif --cut-plane "Front Section"
4249
+ ```
4250
+
4251
+ Creates high-quality animated captures from the real Forge viewport renderer:
4252
+ - Orbit captures with optional wireframe pass
4253
+ - Fixed-camera animation captures for `jointsView()` clips
4254
+ - Named cut-plane captures
4255
+ - Exact camera replay via `--camera`
4256
+ - Full viewport scene replay via `--scene`
4257
+
4258
+ When the input is a notebook, `forgecad capture gif` / `forgecad capture mp4` capture the notebook's preview cell.
4259
+
4260
+ **How it works:**
4261
+ 1. Auto-starts (or reuses) the Vite dev server.
4262
+ 2. Loads `cli/render.html` in headless Chrome.
4263
+ 3. Runs the script once, then captures frames from the same scene while applying the selected animation, cut planes, and camera pose.
4264
+ 4. Encodes with `ffmpeg` when available:
4265
+ - GIF: palettegen/paletteuse for much better colors
4266
+ - MP4: H.264 via `libx264`
4267
+ 5. Falls back to the pure-JS GIF encoder only when `ffmpeg` is unavailable.
4268
+
4269
+ **Options:**
4270
+ - `--format <gif|mp4>` — output format
4271
+ - `--capture <orbit|animation>` — moving orbit camera or fixed animation camera
4272
+ - `--animation <name>` — select one `jointsView()` clip
4273
+ - `--animation-loops <n>` — repeat the chosen clip
4274
+ - `--cut-plane <name>` — enable a named cut plane (repeatable)
4275
+ - `--camera <spec>` — exact camera pose, e.g. `proj=perspective;pos=120,80,120;target=0,0,0;up=0,0,1`
4276
+ - `--scene <json>` — full scene state copied from the viewport, including camera plus object visibility/opacity/color overrides
4277
+ - `--render-mode <solid|wireframe>` — primary render mode
4278
+ - `--include-wireframe-pass` / `--no-wireframe-pass` — control the extra wireframe pass
4279
+ - `--size <px>` — output frame resolution (default `960`)
4280
+ - `--pixel-ratio <n>` — render supersampling factor (default `2`)
4281
+ - `--fps <n>` — capture frame rate (default `24`)
4282
+ - `--frames-per-turn <n>` — frames per full orbit pass (default `72`)
4283
+ - `--hold-frames <n>` — freeze frames before each pass (default `6`)
4284
+ - `--pitch <deg>` — orbit elevation override
4285
+ - `--background <color>` — background color (default `#252526`)
4286
+ - `--quality <default|live|high>` — Forge geometry quality preset for export (default `high`)
4287
+ - `--encoder <auto|ffmpeg|js>` — GIF encoder strategy
4288
+ - `--crf <n>` — MP4 quality for `libx264` (default `18`)
4289
+ - `--list` — print the script's available animation clips and cut planes
4290
+ - `--port <n>` — Vite port (default `5173`)
4291
+ - `--chrome-path <path>` — Chrome executable path override
4292
+ - `--ffmpeg-path <path>` — ffmpeg executable path override
4293
+
4294
+ **Environment variables:**
4295
+ - `FORGE_CAPTURE_SIZE`
4296
+ - `FORGE_CAPTURE_PIXEL_RATIO`
4297
+ - `FORGE_CAPTURE_FPS`
4298
+ - `FORGE_CAPTURE_FRAMES_PER_TURN`
4299
+ - `FORGE_CAPTURE_HOLD_FRAMES`
4300
+ - `FORGE_CAPTURE_PITCH_DEG`
4301
+ - `FORGE_CAPTURE_BACKGROUND`
4302
+ - `FORGE_CAPTURE_QUALITY`
4303
+ - `FORGE_CAPTURE_ANIMATION_LOOPS`
4304
+ - `FORGE_CAPTURE_CRF`
4305
+ - `FFMPEG_PATH`
4306
+ - Legacy `FORGE_GIF_*` vars are still honored as fallbacks
4307
+ - `FORGE_PORT`
4308
+ - `CHROME_PATH`
4309
+
4310
+ **UI scene handoff:**
4311
+ - The View Panel exposes a `Camera` section.
4312
+ - Use `Copy CLI --scene` to grab the current viewport framing plus per-object scene overrides and paste it directly into `render`, `capture gif`, or `capture mp4`.
4313
+
4314
+ ### PDF Report (2D drawing pack)
4315
+
4316
+ ```bash
4317
+ forgecad export report examples/cup.forge.js [output.pdf]
4318
+ forgecad export report examples/cup.forge.js [output.pdf] --dim-angle-tol 18
4319
+ ```
4320
+
4321
+ Generates a searchable-text PDF report with multiple projected drawing views:
4322
+ - Bill of Materials page (auto-summed from script `bom()` entries)
4323
+ - Combined model page (front/right/top/isometric)
4324
+ - Disassembled component pages (same view set per unique component geometry; repeated identical items collapse into one page)
4325
+ - Auto-generated detail continuation pages for elongated/high-detail views (separate pages, not overlayed)
4326
+ - `dim()` annotations included per view only when their axis aligns with that view's projection plane axes
4327
+
4328
+ BOM aggregation rules:
4329
+ - Each `bom(quantity, description, { unit })` call contributes one raw entry
4330
+ - Report export groups by `key` (if provided) else by normalized `description + unit`
4331
+ - Quantities are summed per group and rendered as line items in the BOM table
4332
+
4333
+ Component dimension ownership for disassembled pages:
4334
+ - Preferred: explicit binding via `dim(..., { component: \"Part Name\" })`
4335
+ - Imported-part ownership: `dim(..., { currentComponent: true })` to pin to the owning returned component instance (no bbox heuristic)
4336
+ - Other-component ownership: `dim(..., { component: \"Tabletop\" })`
4337
+ - If multiple owners are bound (e.g. `currentComponent: true` plus another component), it is treated as shared and stays on the overview page
4338
+ - Fallback: automatic ownership only when both dimension endpoints are unambiguously inside exactly one returned component bounding box
4339
+ - Ambiguous dimensions are intentionally skipped for disassembled pages
4340
+
4341
+ Optional report flag:
4342
+ - `--dim-angle-tol <degrees>`: include dimensions whose projected direction is within this many degrees of the nearest view axis (default: `12`)
4343
+
4344
+ ### STL Export (from browser)
4345
+
4346
+ STL export is available in the browser UI via the Export panel. Binary STL format.
4347
+
4348
+ ### Parameter Validation
4349
+
4350
+ ```bash
4351
+ forgecad check params examples/shoe-rack-doors.forge.js [--samples 10]
4352
+ ```
4353
+
4354
+ Samples each parameter across its range and checks for runtime errors, degenerate geometry (volume ≈ 0), and new collisions between parts. Skips intra-group collisions when assembly groups are used.
4355
+
4356
+ **Options:**
4357
+ - `--samples N` — Number of sample points per parameter (default: 8)
4358
+
4359
+ **Output example:**
4360
+ ```
4361
+ ✓ Baseline: 6 objects, 12 params
4362
+ ✓ Checked 91 parameter samples (8 per param)
4363
+
4364
+ ⚠ Found 8 issues across 4 parameters:
4365
+
4366
+ Parameter "Bottom Left Door":
4367
+ 💥 New collision at values: -120.0, -102.9
4368
+ Bottom Left Door ∩ Frame (shared vol: 2561.9mm³)
4369
+ ```
4370
+
4371
+ ### Transform/Assembly Invariant Check
4372
+
4373
+ ```bash
4374
+ forgecad check transforms
4375
+ ```
4376
+
4377
+ Runs fast math-level invariants to catch transform order and frame composition regressions before they leak into examples.
4378
+
4379
+ ### Compiler Snapshot Check
4380
+
4381
+ ```bash
4382
+ forgecad check compiler
4383
+ forgecad check compiler --case segmented-runtime-hints
4384
+ forgecad check compiler --update
4385
+ ```
4386
+
4387
+ Runs curated compiler regression cases and compares them against committed snapshots.
4388
+ This is a unit-style invariant check, not just a debugger convenience.
4389
+ The ordinary multi-feature part corpus lives in [`examples/compiler-corpus/README.md`](../../examples/compiler-corpus/README.md).
4390
+
4391
+ Each snapshot records:
4392
+ - Forge compile plans
4393
+ - CadQuery/OCCT lowerings
4394
+ - export routing decisions
4395
+ - quantized runtime Manifold mesh summaries
4396
+ - quantized compiler-lowered Manifold mesh summaries
4397
+
4398
+ This check also fails if:
4399
+ - a plan-covered shape or sketch no longer matches its compiler-lowered runtime output
4400
+ - export manifests drift away from the per-object compiler routing decisions
4401
+ - exact/faceted support claims stop matching the lowered artifacts and diagnostics
4402
+
4403
+ ### Query Propagation Snapshot Check
4404
+
4405
+ ```bash
4406
+ forgecad check query-propagation
4407
+ forgecad check query-propagation --case hull-runtime-boundary
4408
+ forgecad check query-propagation --update
4409
+ ```
4410
+
4411
+ Runs focused topology-rewrite query-propagation snapshots without dumping the
4412
+ entire compiler scene. This keeps supported, ambiguous, and intentionally
4413
+ unsupported rewrite semantics reviewable as the propagation layer evolves.
4414
+
4415
+ Each snapshot records:
4416
+ - the propagated shape objects that actually carry topology-rewrite metadata
4417
+ - exact versus faceted routing outcomes for those objects
4418
+ - deterministic rewrite-operation ordering
4419
+ - preserved and created query summaries
4420
+ - explicit ambiguity/unsupported diagnostic codes
4421
+
4422
+ This check also fails if:
4423
+ - a defended propagation case loses the expected preserved or created query shape
4424
+ - a known unsupported rewrite stops reporting its explicit diagnostic boundary
4425
+ - a multi-feature corpus part stops surfacing the expected rewrite ordering
4426
+
4427
+ ### Example Architecture Gate
4428
+
4429
+ ```bash
4430
+ forgecad check examples
4431
+ forgecad check examples --family api-parts --family compiler-corpus
4432
+ forgecad check examples --example examples/api/brep-exportable.forge.js
4433
+ ```
4434
+
4435
+ Runs the checked example manifest for the entire `examples/` tree.
4436
+
4437
+ The manifest currently lives in `cli/example-manifest/` and covers every:
4438
+
4439
+ - `.forge.js`
4440
+ - `.sketch.js`
4441
+ - `.forge-notebook.json`
4442
+
4443
+ The command always verifies manifest coverage first, so it fails if:
4444
+
4445
+ - a new example file was added without classification
4446
+ - a checked manifest entry points at a missing file
4447
+ - an example's assigned validation path fails
4448
+ - a `part` example's declared route expectation no longer matches the compiler report
4449
+
4450
+ Current example classes:
4451
+
4452
+ - `part`: runtime execution plus optional exact/faceted route assertions on the selected primary shapes
4453
+ - `assembly`: runtime solve + scene emission, not exact-route parity
4454
+ - `runtime-scene`: viewport/report/runtime examples that still need to execute successfully
4455
+ - `sketch`: sketch payload validation via the sketch export path
4456
+ - `notebook`: preview-cell validation for `.forge-notebook.json`
4457
+ - `experimental`: temporary fenced examples that still have to run
4458
+
4459
+ The gate dispatches by declared validation path, not just by class label:
4460
+
4461
+ - `part-runtime`: execute and then enforce any declared exact/faceted route contract
4462
+ - `assembly-runtime`: execute and validate solved-scene/assembly-owned runtime behavior
4463
+ - `runtime-scene`: execute as a viewport/report/runtime scene without treating it as part-route evidence
4464
+ - `sketch-svg`: render returned sketch payloads through the sketch SVG path
4465
+ - `notebook-preview`: materialize and execute the notebook preview cell
4466
+ - `experimental-runtime`: execute only, while the example stays outside the active architecture claim
4467
+
4468
+ For non-part entries, the manifest can also pin specific runtime surfaces that
4469
+ must remain available to repo checks, such as BOM entries, cut planes,
4470
+ `jointsView()` controls, grouped scene structure, or collected
4471
+ `robotExport(...)` data.
4472
+
4473
+ Current part route states:
4474
+
4475
+ - `exact`: selected primary shapes must stay on the exact compiler route
4476
+ - `faceted`: exact must stay blocked and allow-faceted must succeed with diagnostics
4477
+ - `holdout`: runtime-checked, but intentionally outside the exact-route claim because the example still mixes route outcomes or depends on a documented unsupported capability; this is a temporary recovery state and should normally trend back to zero
4478
+
4479
+ Successful runs also print the current temporary fence list, including each
4480
+ remaining `holdout` or `experimental` entry's blocker and follow-up task, so
4481
+ the command output can be used directly in a phase-entry review.
4482
+
4483
+ Use `--family` when a task owns only one manifest lane, and `--example` when you
4484
+ want to debug a single checked artifact.
4485
+
4486
+ ### Invariant Test Suite
4487
+
4488
+ ```bash
4489
+ forgecad check suite
4490
+ npm test
4491
+ npm run test:examples
4492
+ npm run test:compiler
4493
+ npm run test:compiler:update
4494
+ npm run test:query-propagation
4495
+ npm run test:query-propagation:update
4496
+ ```
4497
+
4498
+ ForgeCAD's current unit-test surface is assertion-based CLI checks, not a separate Vitest/Jest harness.
4499
+
4500
+ The important entrypoints are:
4501
+ - `npm test` runs the repo invariant suite (`transforms`, `dimensions`, `placement`, `js-modules`, `brep`, `compiler`, `query-propagation`, `examples`, `api`)
4502
+ - `npm run test:examples` runs the example architecture gate across the checked `examples/` manifest
4503
+ - `npm run test:compiler` runs just the compiler snapshot/invariant suite
4504
+ - `npm run test:compiler:update` refreshes committed compiler snapshots after an intentional change
4505
+ - `npm run test:query-propagation` runs the focused topology-rewrite query-propagation snapshots
4506
+ - `npm run test:query-propagation:update` refreshes those query-propagation snapshots after an intentional change
4507
+ - `forgecad check suite` is the CLI equivalent of the invariant suite runner
4508
+
4509
+ ### Dimension Propagation Invariant Check
4510
+
4511
+ ```bash
4512
+ forgecad check dimensions
4513
+ ```
4514
+
4515
+ Runs shape-level invariants for dimension metadata propagation across:
4516
+ - transform APIs (`translate`, `rotate`, `transform`, `scale`, `mirror`, `rotateAround`)
4517
+ - copy/style APIs (`clone`, `color`, `setColor`, `smooth/refine/simplify`)
4518
+ - boolean APIs (`add/subtract/intersect`, plus `union/difference/intersection/hull3d`)
4519
+ - import runtime path (`importPart(...).color(...).translate(...)`)
4520
+
4521
+ ### Dimension Debugger
4522
+
4523
+ ```bash
4524
+ forgecad debug dimensions /path/to/file.forge.js [--all]
4525
+ forgecad debug dimensions /path/to/file.forge.js [--all] [--dim-angle-tol 12]
4526
+ ```
4527
+
4528
+ Prints:
4529
+ - total object count
4530
+ - total dimension count
4531
+ - per-view visibility counts (`front/right/top/iso`) using report angle tolerance
4532
+ - report ownership routing (`combined` vs `component:<name>`) per dimension
4533
+ - per-object approximate dimension ownership (both endpoints inside object bbox)
4534
+ - a dimension coordinate list (first 20 by default, `--all` for full dump)
4535
+
4536
+ ### Compiler Debugger
4537
+
4538
+ ```bash
4539
+ forgecad debug compiler /path/to/file.forge.js
4540
+ forgecad debug compiler /path/to/file.forge.js --compact
4541
+ ```
4542
+
4543
+ Prints JSON for the current script's compiler state, including:
4544
+ - per-object compile plans
4545
+ - CadQuery/OCCT lowering diagnostics and lowered plans
4546
+ - faceted fallback eligibility
4547
+ - runtime Manifold summaries
4548
+ - compiler-lowered Manifold summaries
4549
+
4550
+ ### Local Branch Cleanup
4551
+
4552
+ ```bash
4553
+ uv run cli/forge-prune-local-branches.py
4554
+ uv run cli/forge-prune-local-branches.py --dry-run
4555
+ uv run cli/forge-prune-local-branches.py --base mainline
4556
+ ```
4557
+
4558
+ This is a `uv`-backed Python utility for repository housekeeping. It finds local branches with no matching remote branch that are already merged into the selected base ref, shows them in a Rich terminal UI, then prompts one by one before deleting anything.
4559
+
4560
+ Behavior:
4561
+ - Deletes with `git branch -d`, not force-delete
4562
+ - Removes linked worktrees first when the branch is checked out in a secondary worktree
4563
+ - Requires an explicit `force` choice if one of those linked worktrees is dirty
4564
+ - Refuses to touch the current worktree, the primary worktree, or prunable/missing worktree entries
4565
+ - `--path` lets you point at any location inside the target repository
4566
+
4567
+ ## Adding New CLI Commands
4568
+
4569
+ 1. Create or extend a module under `cli/`
4570
+ 2. Import from `../src/forge/headless`
4571
+ 3. Call `await init()` to load the WASM kernel
4572
+ 4. Use `runScript(code, fileName, allFiles)` to execute user scripts
4573
+ 5. Register the new subcommand in `cli/forgecad.ts`
4574
+
4575
+ ### Minimal Example
4576
+
4577
+ ```typescript
4578
+ #!/usr/bin/env node
4579
+ import { readFileSync } from 'fs';
4580
+ import { init, runScript } from '../src/forge/headless';
4581
+
4582
+ const code = readFileSync(process.argv[2], 'utf-8');
4583
+
4584
+ await init();
4585
+ const result = runScript(code, 'main.forge.js', {});
4586
+
4587
+ if (result.error) {
4588
+ console.error(result.error);
4589
+ process.exit(1);
4590
+ }
4591
+
4592
+ for (const obj of result.objects) {
4593
+ if (obj.shape) {
4594
+ console.log(`${obj.name}: volume=${obj.shape.volume().toFixed(1)}mm³`);
4595
+ }
4596
+ if (obj.sketch) {
4597
+ console.log(`${obj.name}: area=${obj.sketch.area().toFixed(1)}mm²`);
4598
+ }
4599
+ }
4600
+ ```
4601
+
4602
+ ### Cross-file imports
4603
+
4604
+ When running scripts that use `importSketch()` / `importSvgSketch()` / `importPart()` or plain JS module imports, pass all project files (or at least all files reachable by imports), keyed by project-relative path. This supports root-relative and relative imports, utility `.js` modules, and `.svg` assets (`./assets/logo.svg`):
4605
+
4606
+ ```typescript
4607
+ import { readdirSync, readFileSync } from 'fs';
4608
+
4609
+ const allFiles: Record<string, string> = {};
4610
+ for (const f of readdirSync(scriptDir)) {
4611
+ if (f.endsWith('.forge.js') || f.endsWith('.sketch.js') || f.endsWith('.js') || f.endsWith('.svg')) {
4612
+ allFiles[f] = readFileSync(join(scriptDir, f), 'utf-8');
4613
+ }
4614
+ }
4615
+
4616
+ const result = runScript(code, 'main.forge.js', allFiles);
4617
+ ```
4618
+
4619
+ For utility modules that want explicit ForgeCAD imports instead of globals, use the virtual runtime module:
4620
+
4621
+ ```javascript
4622
+ import { box, union } from "forgecad";
4623
+ ```
4624
+
4625
+ Keep using `importPart()` / `importSketch()` for model/sketch files when you want ForgeCAD-specific behavior like param override scopes or SVG parsing.
4626
+
4627
+ ## Dependencies
4628
+
4629
+ | Package | Purpose | Context |
4630
+ |---------|---------|---------|
4631
+ | `forgecad` | Installable CLI binary (`forgecad ...`) | Runtime package |
4632
+ | `puppeteer-core` | Headless Chrome for PNG/GIF/MP4 rendering | Runtime dependency |
4633
+ | `manifold-3d` | Geometry kernel (WASM) | Works in both Node and browser |
4634
+ | `three` | 3D rendering (used by render.ts) | Loaded in browser context by Puppeteer |
4635
+