forgecad 0.1.2 → 0.1.4

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