forgecad 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/assets/{evalWorker-DuS4V-H5.js → evalWorker-1m873KWd.js} +107 -107
  2. package/dist/assets/{index-d60dJDhX.js → index-Dvz3nSDc.js} +342 -326
  3. package/dist/assets/{manifold-DEOPQqeC.js → manifold-C38sUiKu.js} +1 -1
  4. package/dist/assets/{manifold-cL7A7HeM.js → manifold-Dk2u-lhj.js} +1 -1
  5. package/dist/assets/{manifold-DbvY5u9x.js → manifold-rOWQW9fU.js} +1 -1
  6. package/dist/assets/{reportWorker-GZVKtf9S.js → reportWorker-Cj587shw.js} +129 -129
  7. package/dist/index.html +1 -1
  8. package/dist-cli/forgecad.js +204 -19
  9. package/dist-skill/SKILL.md +31 -4561
  10. package/dist-skill/docs/API/README.md +24 -0
  11. package/dist-skill/docs/API/guides/modeling-recipes.md +246 -0
  12. package/dist-skill/docs/API/internals/compiler.md +300 -0
  13. package/dist-skill/docs/API/internals/manifold.md +7 -0
  14. package/dist-skill/docs/API/model-building/README.md +31 -0
  15. package/dist-skill/docs/API/model-building/assembly.md +205 -0
  16. package/dist-skill/docs/API/model-building/coordinate-system.md +43 -0
  17. package/dist-skill/docs/API/model-building/entities.md +282 -0
  18. package/dist-skill/docs/API/model-building/geometry-conventions.md +104 -0
  19. package/dist-skill/docs/API/model-building/positioning.md +170 -0
  20. package/dist-skill/docs/API/model-building/reference.md +1936 -0
  21. package/dist-skill/docs/API/model-building/sheet-metal.md +180 -0
  22. package/dist-skill/docs/API/model-building/sketch-anchor.md +32 -0
  23. package/dist-skill/docs/API/model-building/sketch-booleans.md +101 -0
  24. package/dist-skill/docs/API/model-building/sketch-core.md +68 -0
  25. package/dist-skill/docs/API/model-building/sketch-extrude.md +57 -0
  26. package/dist-skill/docs/API/model-building/sketch-on-face.md +98 -0
  27. package/dist-skill/docs/API/model-building/sketch-operations.md +92 -0
  28. package/dist-skill/docs/API/model-building/sketch-path.md +70 -0
  29. package/dist-skill/docs/API/model-building/sketch-primitives.md +104 -0
  30. package/dist-skill/docs/API/model-building/sketch-transforms.md +60 -0
  31. package/dist-skill/docs/API/output/bom.md +53 -0
  32. package/dist-skill/docs/API/output/brep-export.md +82 -0
  33. package/dist-skill/docs/API/output/dimensions.md +62 -0
  34. package/dist-skill/docs/API/runtime/viewport.md +229 -0
  35. package/dist-skill/docs/CLI.md +672 -0
  36. package/dist-skill/docs/CODING.md +345 -0
  37. package/dist-skill/docs/PROGRAM-LEAD.md +180 -0
  38. package/dist-skill/docs/VISION.md +77 -0
  39. package/examples/api/import-group-assembly.forge.js +34 -0
  40. package/examples/api/import-group-source.forge.js +35 -0
  41. package/package.json +1 -1
@@ -0,0 +1,1936 @@
1
+ # ForgeCAD Model-Building Reference
2
+
3
+ This file covers the script authoring surface for geometry, sketches, topology, assemblies, imports, and reusable parts.
4
+
5
+ It intentionally excludes:
6
+ - viewport-only APIs in [../runtime/viewport.md](../runtime/viewport.md)
7
+ - 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)
8
+ - implementation notes in [../internals/manifold.md](../internals/manifold.md)
9
+
10
+ ## Core Concepts
11
+
12
+ ForgeCAD scripts are JavaScript code that returns geometry. The forge API is globally available — no imports needed.
13
+
14
+ ### Basic Structure
15
+ ```javascript
16
+ // 1. Declare parameters (creates UI sliders)
17
+ const width = param("Width", 50, { min: 20, max: 100, unit: "mm" });
18
+
19
+ // 2. Create geometry
20
+ const shape = box(width, 30, 10);
21
+
22
+ // 3. Return the final shape
23
+ return shape;
24
+ ```
25
+
26
+ ### Execution Model
27
+ - Scripts execute on every parameter change (400ms debounce)
28
+ - All operations are **immutable** — they return new shapes, never modify in place
29
+ - Must return one of:
30
+ - A `Shape` (3D solid)
31
+ - A `Sketch` (2D profile — rendered flat on XY plane)
32
+ - A `TrackedShape` (3D solid with named faces/edges — auto-unwrapped)
33
+ - A `ShapeGroup` (multiple shapes/sketches grouped for joint transforms)
34
+ - An `Array` of shapes/sketches/groups (multi-object scene)
35
+ - An `Array` of `{ name, shape?, sketch?, color? }` objects (named multi-object scene)
36
+
37
+ ### ⚠️ Important: Unions Remove Colors
38
+
39
+ 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:
40
+
41
+ ```javascript
42
+ // ❌ BAD: Colors are lost after union!
43
+ const red = box(30, 30, 30).color('#ff0000');
44
+ const blue = box(20, 20, 20).translate(30, 0, 0).color('#0066ff');
45
+ return union(red, blue); // Result is all one color (red)
46
+ ```
47
+
48
+ **Solution**: Return objects as a composite response instead:
49
+
50
+ ```javascript
51
+ // ✅ GOOD: Each object keeps its color
52
+ const red = box(30, 30, 30).color('#ff0000');
53
+ const blue = box(20, 20, 20).translate(30, 0, 0).color('#0066ff');
54
+
55
+ // Return as named objects to preserve individual colors and materials
56
+ return [
57
+ { "label": red }, // Each gets its own color, toggle, and controls
58
+ { "label": blue }
59
+ ];
60
+ ```
61
+
62
+ Each object in the returned array stays independently colorable and selectable, which is the main alternative to booleaning everything into one solid.
63
+
64
+ ### Coordinate System
65
+ ForgeCAD uses **Z-up** right-handed coordinates:
66
+ - **X** = left/right
67
+ - **Y** = forward/back
68
+ - **Z** = up/down
69
+
70
+ See [coordinate-system.md](coordinate-system.md) for view mapping details.
71
+
72
+ ## Parameters
73
+
74
+ ### `param(name, default, options?)`
75
+ Declares a parameter and creates a UI slider.
76
+
77
+ **Parameters:**
78
+ - `name` (string) - Display name in UI
79
+ - `default` (number) - Initial value
80
+ - `options` (object, optional):
81
+ - `min` (number) - Minimum value (default: 0)
82
+ - `max` (number) - Maximum value (default: default * 4)
83
+ - `step` (number) - Slider increment (auto-calculated if not provided)
84
+ - `unit` (string) - Display unit like "mm", "°", "%"
85
+ - `integer` (boolean) - If true, value is always rounded to whole number. Step defaults to 1. Use for counts, quantities, sides, etc.
86
+
87
+ **Returns:** Current parameter value (number)
88
+
89
+ **Examples:**
90
+ ```javascript
91
+ const width = param("Width", 50);
92
+ const angle = param("Angle", 45, { min: 0, max: 180, unit: "°" });
93
+ const thick = param("Thickness", 2, { min: 0.5, max: 10, step: 0.5, unit: "mm" });
94
+ const count = param("Count", 5, { min: 1, max: 20, integer: true });
95
+ ```
96
+
97
+ ## Colors
98
+
99
+ Both Shape and Sketch support colors via `.color()`:
100
+
101
+ ```javascript
102
+ const red = box(50, 50, 50).color('#ff0000');
103
+ const blue = circle2d(25).color('#0066ff');
104
+ ```
105
+
106
+ Colors are preserved through transforms and boolean operations (the first operand's color wins).
107
+
108
+ When returning multiple objects, colors can also be set per-object:
109
+
110
+ ```javascript
111
+ return [
112
+ { name: "Base", shape: box(100, 100, 5), color: "#888888" },
113
+ { name: "Column", shape: cylinder(50, 10).translate(50, 50, 5), color: "#4488cc" },
114
+ ];
115
+ ```
116
+
117
+ ## 3D Primitives
118
+
119
+ ### `box(x, y, z, center?)`
120
+ Creates a rectangular box with named faces and edges.
121
+
122
+ **Parameters:**
123
+ - `x, y, z` (number) - Dimensions
124
+ - `center` (boolean, optional) - If true, centers at origin. Default: false (corner at origin)
125
+
126
+ **Returns:** `TrackedShape` (with faces: top, bottom, side-left, side-right, side-top, side-bottom; edges: vert-bl, vert-br, vert-tr, vert-tl, etc.)
127
+
128
+ ```javascript
129
+ const cube = box(50, 50, 50, true); // Centered cube
130
+ const plate = box(100, 80, 5); // Corner at origin
131
+ plate.face('top'); // FaceRef { normal, center, planar, uAxis, vAxis }
132
+ plate.edge('vert-bl'); // EdgeRef { start, end }
133
+ ```
134
+
135
+ ### `cylinder(height, radius, radiusTop?, segments?, center?)`
136
+ Creates a cylinder or cone with named faces and edges.
137
+
138
+ **Parameters:**
139
+ - `height` (number) - Height along Z axis
140
+ - `radius` (number) - Bottom radius
141
+ - `radiusTop` (number, optional) - Top radius. If different from radius, creates a cone. Default: same as radius
142
+ - `segments` (number, optional) - Number of sides. Default: auto (smooth circle)
143
+ - `center` (boolean, optional) - If true, centers along Z. Default: false
144
+
145
+ **Returns:** `TrackedShape` (with faces: top, bottom, side; edges: top-rim, bottom-rim)
146
+
147
+ ```javascript
148
+ const cyl = cylinder(50, 10); // Cylinder
149
+ const cone = cylinder(50, 20, 5); // Cone (tapered)
150
+ const hex = cylinder(10, 15, 15, 6); // Hexagonal prism
151
+ cyl.face('top'); // FaceRef (planar)
152
+ cyl.face('side'); // FaceRef (curved, planar === false)
153
+ ```
154
+
155
+ ### `sphere(radius, segments?)`
156
+ Creates a sphere.
157
+
158
+ **Parameters:**
159
+ - `radius` (number) - Sphere radius
160
+ - `segments` (number, optional) - Tessellation detail. Default: auto (smooth)
161
+
162
+ **Returns:** `Shape`
163
+
164
+ ```javascript
165
+ const ball = sphere(25);
166
+ const lowPoly = sphere(25, 8); // Octahedron-like
167
+ ```
168
+
169
+ ## 3D Transforms
170
+
171
+ All transforms are **chainable** and **immutable** (return new shapes).
172
+ The core 3D transform set uses the same names/signatures on `Shape`, `TrackedShape`, and `ShapeGroup`.
173
+ For mixed groups that include `Sketch` children, `transform` / `rotateAround` / `pointAlong` are 3D-only.
174
+
175
+ ### `.clone()` / `.duplicate()`
176
+ Create an explicit copy handle of a shape (same geometry/color) so you can branch variants clearly.
177
+
178
+ ```javascript
179
+ const bracket = box(60, 20, 8);
180
+ const left = bracket.clone().translate(-40, 0, 0);
181
+ const right = bracket.duplicate().translate(40, 0, 0);
182
+ ```
183
+
184
+ ### `.translate(x, y, z)`
185
+ Moves the shape relative to its current position.
186
+
187
+ ```javascript
188
+ const moved = box(10, 10, 10).translate(50, 0, 0);
189
+ ```
190
+
191
+ ### `.moveTo(x, y, z)`
192
+ Positions the shape so its bounding box min corner is at the given global coordinate.
193
+
194
+ ```javascript
195
+ // Place a box at exactly (100, 50, 0) in world space
196
+ const placed = box(30, 30, 10).moveTo(100, 50, 0);
197
+ ```
198
+
199
+ ### `.moveToLocal(target, x, y, z)`
200
+ Positions the shape relative to another shape's local coordinate system (bounding box min corner).
201
+
202
+ **Parameters:**
203
+ - `target` (Shape | TrackedShape) — The reference shape
204
+ - `x, y, z` (number) — Offset from target's bounding box min corner
205
+
206
+ ```javascript
207
+ const base = box(100, 100, 10);
208
+ const part = box(20, 20, 30);
209
+
210
+ // Place part at (10, 10, 10) relative to base's origin corner
211
+ const placed = part.moveToLocal(base, 10, 10, 10);
212
+ ```
213
+
214
+ ### `.rotate(x, y, z)`
215
+ Rotates using Euler angles in **degrees**.
216
+
217
+ **Parameters:**
218
+ - `x, y, z` (number) - Rotation in degrees around each axis
219
+
220
+ ```javascript
221
+ const rotated = box(50, 20, 10).rotate(0, 0, 45); // 45° around Z
222
+ const tilted = cylinder(50, 10).rotate(90, 0, 0); // Lay on side
223
+ ```
224
+
225
+ ### `.scale(v)`
226
+ Scales the shape.
227
+
228
+ **Parameters:**
229
+ - `v` (number | [number, number, number]) - Uniform scale or per-axis scale
230
+
231
+ ```javascript
232
+ const bigger = sphere(10).scale(2); // 2x larger
233
+ const stretched = box(10, 10, 10).scale([2, 1, 0.5]); // Non-uniform
234
+ ```
235
+
236
+ ### `.mirror(normal)`
237
+ Mirrors across a plane defined by its normal vector.
238
+
239
+ **Parameters:**
240
+ - `normal` ([number, number, number]) - Plane normal (doesn't need to be unit length)
241
+
242
+ ```javascript
243
+ const mirrored = shape.mirror([1, 0, 0]); // Mirror across YZ plane
244
+ ```
245
+
246
+ ### `.transform(m)`
247
+ Applies a custom 4x4 transform matrix or `Transform` object.
248
+
249
+ **Parameters:**
250
+ - `m` (`number[] | Transform`) - 4x4 column-major matrix (`number[16]`) or a `Transform`
251
+
252
+ ```javascript
253
+ const T = Transform.identity()
254
+ .translate(0, 0, 1.5)
255
+ .rotateAxis([1, 0, 0], 35, [0, hingeY, 0]);
256
+
257
+ const moved = lid.transform(T);
258
+ ```
259
+
260
+ ### `.rotateAround(axis, angleDeg, pivot?)`
261
+ Rotates around an arbitrary axis through a pivot point.
262
+
263
+ **Parameters:**
264
+ - `axis` ([number, number, number]) - Rotation axis direction
265
+ - `angleDeg` (number) - Rotation angle in degrees
266
+ - `pivot` ([number, number, number], optional) - Pivot point. Default: origin
267
+
268
+ ```javascript
269
+ // Rotate a door 45° around Z axis at the hinge position
270
+ const opened = door.rotateAround([0, 0, 1], 45, [hingeX, hingeY, 0]);
271
+ ```
272
+
273
+ ### `.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)`
274
+ Solves the rotation around an axis that makes `movingPoint` reach the target line/plane implied by `targetPoint`.
275
+
276
+ **Parameters:**
277
+ - `axis` ([number, number, number]) - Rotation axis direction
278
+ - `pivot` ([number, number, number]) - Point on the rotation axis
279
+ - `movingPoint` (`[number, number, number] | string`) - World-space point or this shape's anchor/reference
280
+ - `targetPoint` (`[number, number, number] | string`) - World-space point or this shape's anchor/reference
281
+ - `opts` (object, optional)
282
+ - `mode` (`'plane' | 'line'`) - Default: `'plane'`
283
+
284
+ Modes:
285
+ - `'plane'` — rotate until `movingPoint` lies in the plane defined by `axis` + `targetPoint`
286
+ - `'line'` — rotate until `movingPoint` lies on the infinite line from `pivot` through `targetPoint`; throws if the geometry makes that impossible
287
+
288
+ ```javascript
289
+ const arm = box(80, 8, 8, true)
290
+ .translate(40, 0, 0)
291
+ .withReferences({ points: { tip: [80, 0, 0] } });
292
+
293
+ const aimed = arm.rotateAroundTo(
294
+ [0, 0, 1],
295
+ [0, 0, 0],
296
+ "tip",
297
+ [30, 30, 20],
298
+ );
299
+ ```
300
+
301
+ ### `.pointAlong(direction)`
302
+ 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.
303
+
304
+ **Parameters:**
305
+ - `direction` ([number, number, number]) - Target direction vector
306
+
307
+ ```javascript
308
+ // Lay a cylinder along the X axis
309
+ const axle = cylinder(100, 5).pointAlong([1, 0, 0]);
310
+
311
+ // Symmetric hinges pointing outward from center
312
+ const hingeL = cylinder(40, 5).pointAlong([-1, 0, 0]).translate(-50, 0, 0);
313
+ const hingeR = cylinder(40, 5).pointAlong([1, 0, 0]).translate(50, 0, 0);
314
+ ```
315
+
316
+ ### `Transform` primitives (for kinematic chains)
317
+ Use `Transform` when manual pivot math becomes hard to maintain.
318
+
319
+ ```javascript
320
+ const T = Transform.identity()
321
+ .translate(0, 0, 120)
322
+ .rotateAxis([0, 0, 1], 35);
323
+
324
+ const p = T.point([10, 0, 0]); // transform a point
325
+ const v = T.vector([1, 0, 0]); // transform a direction (no translation)
326
+ ```
327
+
328
+ Core methods:
329
+ - `Transform.identity()`
330
+ - `Transform.translation(x, y, z)`
331
+ - `Transform.rotationAxis(axis, angleDeg, pivot?)`
332
+ - `Transform.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)`
333
+ - `Transform.scale(v)`
334
+ - `T.mul(other)` (chain-composition order)
335
+ - `composeChain(a, b, c, ...)` explicit left-to-right chain composition
336
+ - `T.inverse()`
337
+ - `shape.transform(T)` / `trackedShape.transform(T)` / `group.transform(T)`
338
+
339
+ ## Joints
340
+
341
+ ### `joint(name, shape, pivot, opts?)`
342
+ Create a revolute (hinge) joint. Auto-creates a param slider and rotates the shape.
343
+
344
+ **Parameters:**
345
+ - `name` (string) - Display name for the angle parameter
346
+ - `shape` (Shape) - The shape to rotate
347
+ - `pivot` ([number, number, number]) - The pivot point
348
+ - `opts` (object, optional):
349
+ - `axis` ([number, number, number]) - Rotation axis. Default: [0, 0, 1] (Z axis)
350
+ - `min` (number) - Minimum angle. Default: 0
351
+ - `max` (number) - Maximum angle. Default: 360
352
+ - `default` (number) - Initial angle. Default: 0
353
+ - `unit` (string) - Display unit. Default: "°"
354
+
355
+ **Returns:** `Shape` (rotated by the current slider value)
356
+
357
+ ```javascript
358
+ // One line: creates a "Lid Angle" slider and rotates the lid around the hinge
359
+ const openLid = joint("Lid Angle", lid, [0, boxDepth, boxHeight], {
360
+ axis: [1, 0, 0],
361
+ max: 120,
362
+ default: 45,
363
+ });
364
+ ```
365
+
366
+ ## Assembly Graph (Mechanisms)
367
+
368
+ See also: `assembly.md`.
369
+
370
+ ### `assembly(name?)`
371
+ Creates an assembly container with named parts + joints.
372
+
373
+ ```javascript
374
+ const mech = assembly("Two-Link Arm")
375
+ .addPart("base", box(80, 80, 20, true))
376
+ .addPart("link1", box(120, 24, 24).translate(0, -12, -12))
377
+ .addPart("link2", box(100, 20, 20).translate(0, -10, -10))
378
+ .addJoint("shoulder", "revolute", "base", "link1", {
379
+ axis: [0, 1, 0],
380
+ min: -30, max: 120, default: 20,
381
+ frame: Transform.identity().translate(0, 0, 20),
382
+ })
383
+ .addJoint("elbow", "revolute", "link1", "link2", {
384
+ axis: [0, 1, 0],
385
+ min: -20, max: 140, default: 40,
386
+ frame: Transform.identity().translate(120, 0, 0),
387
+ });
388
+
389
+ const solved = mech.solve();
390
+ return solved.toScene();
391
+ ```
392
+
393
+ Linked joint example:
394
+
395
+ ```javascript
396
+ mech
397
+ .addJointCoupling("Top Gear", {
398
+ terms: [
399
+ { joint: "Steering", ratio: 1 },
400
+ { joint: "Wheel Drive", ratio: 20 / 14 },
401
+ ],
402
+ })
403
+ .addJointCoupling("Motor 1", {
404
+ terms: [{ joint: "Top Gear", ratio: -2 }],
405
+ });
406
+ ```
407
+
408
+ Gear helper example:
409
+
410
+ ```javascript
411
+ const pair = lib.gearPair({
412
+ pinion: { module: 1.25, teeth: 14, faceWidth: 8 },
413
+ gear: { module: 1.25, teeth: 42, faceWidth: 8 },
414
+ });
415
+
416
+ mech.addGearCoupling("Driven", "Pinion", { pair }); // uses pair.jointRatio
417
+ ```
418
+
419
+ Bevel/face helper example:
420
+
421
+ ```javascript
422
+ const bevel = lib.bevelGearPair({
423
+ pinion: { module: 1.5, teeth: 18, faceWidth: 10 },
424
+ gear: { module: 1.5, teeth: 36, faceWidth: 9 },
425
+ shaftAngleDeg: 90,
426
+ });
427
+
428
+ const face = lib.faceGearPair({
429
+ face: { module: 1.5, teeth: 44, faceWidth: 7, toothHeight: 1.2, side: 'top' },
430
+ vertical: { module: 1.5, teeth: 16, faceWidth: 8 },
431
+ });
432
+
433
+ mech.addGearCoupling("Bevel Driven", "Bevel Driver", { pair: bevel });
434
+ mech.addGearCoupling("Face Driven", "Face Driver", { pair: face });
435
+ ```
436
+
437
+ Key methods:
438
+ - `addPart(name, shape, { transform?, metadata? })`
439
+ - `addFrame(name, { transform? })` for virtual mechanism frames
440
+ - `addJoint(name, type, parent, child, opts)` where `type` is `'fixed' | 'revolute' | 'prismatic'`
441
+ - `addRevolute(...)`, `addPrismatic(...)`, `addFixed(...)` shorthand helpers
442
+ - `addJointCoupling(jointName, { terms, offset? })` for linked joints:
443
+ - `jointName`: driven joint
444
+ - `terms`: `[{ joint, ratio? }, ...]` where each source contributes `ratio * sourceValue` (default ratio `1`)
445
+ - `offset`: additive bias after term sum (default `0`)
446
+ - `addGearCoupling(drivenJoint, driverJoint, opts)` for revolute gear meshes:
447
+ - ratio source (exactly one): `ratio`, `pair` (`pair.jointRatio`), or `driverTeeth + drivenTeeth`
448
+ - `mesh`: `'external' | 'internal' | 'bevel' | 'face'` (teeth mode only, default `'external'`; `bevel`/`face` follow external sign)
449
+ - `offset`: additive bias after gear ratio
450
+ - `solve(state?)` with per-joint value overrides
451
+ - `sweepJoint(jointName, from, to, steps, baseState?, collisionOptions?)`
452
+
453
+ Solved assembly helpers:
454
+ - `solved.toScene()` for rendering
455
+ - `solved.collisionReport()` for interference checks
456
+ - `solved.minClearance(partA, partB, searchLength?)`
457
+ - `solved.bom()` / `solved.bomCsv()`
458
+ - `bomToCsv(rows)` (standalone helper)
459
+
460
+ ### `robotExport(options)`
461
+ Declares that the current script should also export an `assembly(...)` as a robot package for the SDF CLI.
462
+
463
+ Key fields:
464
+ - `assembly`: required source assembly graph
465
+ - `modelName`: simulator-facing model name
466
+ - `links.<part>.massKg` or `densityKgM3`: inertial hints
467
+ - `joints.<joint>.effort|velocity|damping|friction`: simulator tuning
468
+ - `plugins.diffDrive`: diff-drive plugin wiring for Gazebo
469
+ - `world.generateDemoWorld`: emit a simple obstacle-course world alongside the model
470
+
471
+ Example:
472
+
473
+ ```javascript
474
+ robotExport({
475
+ assembly: mech,
476
+ modelName: "Forge Scout",
477
+ links: {
478
+ Base: { massKg: 12 },
479
+ },
480
+ plugins: {
481
+ diffDrive: {
482
+ leftJoints: ["leftFront", "leftRear"],
483
+ rightJoints: ["rightFront", "rightRear"],
484
+ wheelSeparationMm: 320,
485
+ wheelRadiusMm: 72,
486
+ },
487
+ },
488
+ });
489
+ ```
490
+
491
+ ## 3D Boolean Operations
492
+
493
+ ### `union(...shapes)`
494
+ Combines shapes (additive).
495
+
496
+ ```javascript
497
+ const combined = union(
498
+ box(50, 50, 10),
499
+ cylinder(20, 15).translate(25, 25, 10)
500
+ );
501
+
502
+ const parts = [
503
+ box(50, 50, 10),
504
+ cylinder(20, 15).translate(25, 25, 10),
505
+ ];
506
+ const combinedFromArray = union(parts);
507
+ ```
508
+
509
+ ### `difference(...shapes)`
510
+ Subtracts shapes[1..n] from shapes[0].
511
+
512
+ ```javascript
513
+ const plate = box(100, 100, 5);
514
+ const hole1 = cylinder(6, 10).translate(25, 50, 0);
515
+ const hole2 = cylinder(6, 10).translate(75, 50, 0);
516
+ const result = difference(plate, hole1, hole2);
517
+ const resultFromArray = difference([plate, hole1, hole2]);
518
+
519
+ // Or using method syntax:
520
+ const result = plate.subtract(hole1, hole2);
521
+ const sameResult = plate.subtract([hole1, hole2]);
522
+ ```
523
+
524
+ ### `intersection(...shapes)`
525
+ Keeps only overlapping volume.
526
+
527
+ ```javascript
528
+ const overlap = intersection(
529
+ sphere(30),
530
+ box(40, 40, 40, true)
531
+ );
532
+
533
+ const overlapFromArray = intersection([
534
+ sphere(30),
535
+ box(40, 40, 40, true),
536
+ ]);
537
+ ```
538
+
539
+ ### Method Syntax
540
+ Shapes also have boolean methods:
541
+
542
+ ```javascript
543
+ shape.add(other1, other2)
544
+ shape.add([other1, other2])
545
+ shape.subtract(other1, other2)
546
+ shape.subtract([other1, other2])
547
+ shape.intersect(other1, other2)
548
+ shape.intersect([other1, other2])
549
+ ```
550
+
551
+ ## Group
552
+
553
+ ### `group(...items)`
554
+ Groups multiple shapes/sketches for joint transforms without merging them into a single mesh. Unlike `union`, colors and individual identities are preserved.
555
+
556
+ **Parameters:**
557
+ - `...items` (Shape | Sketch | TrackedShape | ShapeGroup | `{ name, shape? | sketch? | group? }`) - Items to group (nested groups allowed)
558
+
559
+ **Returns:** `ShapeGroup`
560
+
561
+ ```javascript
562
+ const base = box(100, 100, 5).color('#888888');
563
+ const column = cylinder(40, 5).translate(50, 50, 5).color('#4488cc');
564
+
565
+ // Group them — they stay separate but transform together
566
+ const assembly = group(base, column).translate(200, 0, 0);
567
+ return assembly;
568
+ ```
569
+
570
+ Named child descriptors are useful when the group will be flattened later, especially inside assemblies:
571
+
572
+ ```javascript
573
+ const shell = box(80, 60, 24).color('#6e7b88');
574
+ const lid = box(80, 60, 4).translate(0, 0, 24).color('#c9d2db');
575
+
576
+ const housing = group(
577
+ { name: 'Shell', shape: shell },
578
+ { name: 'Lid', shape: lid },
579
+ );
580
+ ```
581
+
582
+ 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`.
583
+
584
+ ### ShapeGroup Methods
585
+
586
+ **Transforms** — all chainable, return a new ShapeGroup. Placement references (see below) are transformed along with the children.
587
+
588
+ ```javascript
589
+ group.translate(x, y, z)
590
+ group.moveTo(x, y, z)
591
+ group.moveToLocal(target, x, y, z)
592
+ group.attachTo(target, targetAnchor, selfAnchor?, offset?)
593
+ group.rotate(x, y, z)
594
+ group.rotateAround(axis, angleDeg, pivot?)
595
+ group.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)
596
+ group.pointAlong(direction)
597
+ group.transform(m)
598
+ group.scale(v)
599
+ group.mirror(normal)
600
+ group.color(hex) // applies to all children
601
+ group.clone()
602
+ group.duplicate() // alias
603
+ ```
604
+
605
+ **Placement references** — same API as `Shape`. References survive all transforms and `importGroup()` round-trips.
606
+
607
+ ```javascript
608
+ group.withReferences({ points?, edges?, surfaces?, objects? })
609
+ group.referenceNames(kind?)
610
+ group.referencePoint(ref)
611
+ group.placeReference(ref, target, offset?)
612
+ ```
613
+
614
+ **Child access** — get a named child as a live `Shape`/`TrackedShape`/`ShapeGroup`.
615
+
616
+ ```javascript
617
+ group.child(name) // throws if name not found
618
+ group.children // raw array
619
+ group.childNames // name array (parallel to children)
620
+ group.childName(index) // name at index
621
+ ```
622
+
623
+ `group.rotateAround(...)` is convenience sugar for `group.transform(Transform.rotationAxis(...))`.
624
+ `group.rotateAroundTo(...)` is convenience sugar for `group.transform(Transform.rotateAroundTo(...))`.
625
+ `group.pointAlong(...)` is convenience sugar for a group-wide axis rotation from Z to `direction`.
626
+
627
+ ```javascript
628
+ const hingeY = 40;
629
+ const lid = group(shell, logo);
630
+
631
+ const openedA = lid.rotateAround([1, 0, 0], 35, [0, hingeY, 0]); // sugar
632
+ const openedB = lid.transform(Transform.rotationAxis([1, 0, 0], 35, [0, hingeY, 0])); // equivalent
633
+
634
+ const laidDown = lid.pointAlong([1, 0, 0]); // same intent as Shape/TrackedShape.pointAlong
635
+ ```
636
+
637
+ ### ShapeGroup and placement references
638
+
639
+ `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.).
640
+
641
+ ```javascript
642
+ const bracketGroup = group(
643
+ { name: 'Bracket Left', shape: leftBracket },
644
+ { name: 'Bracket Right', shape: rightBracket },
645
+ { name: 'Dowel', shape: dowel },
646
+ ).withReferences({
647
+ points: {
648
+ mountCenter: [0, 0, 0],
649
+ topCenter: [34, 30, 40],
650
+ },
651
+ });
652
+
653
+ // Place the group by a named reference point
654
+ const placed = bracketGroup.placeReference('mountCenter', [100, 0, 0]);
655
+
656
+ // Reference survives the translation:
657
+ const pt = placed.referencePoint('mountCenter'); // → [100, 0, 0]
658
+ ```
659
+
660
+ 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.
661
+
662
+ ## 3D Anchor Positioning
663
+
664
+ ### `.attachTo(target, targetAnchor, selfAnchor?, offset?)`
665
+ Position a shape relative to another using named 3D anchor points based on bounding boxes.
666
+
667
+ Available on both `Shape` and `TrackedShape`.
668
+
669
+ **Parameters:**
670
+ - `target` (Shape | TrackedShape) — The shape to attach to
671
+ - `targetAnchor` (Anchor3D) — Point on target
672
+ - `selfAnchor` (Anchor3D, optional) — Point on this shape to align. Default: 'center'
673
+ - `offset` ([number, number, number], optional) — Additional offset after alignment
674
+
675
+ **Anchor3D values:**
676
+ - `'center'` — bounding box center
677
+ - Face centers (1 axis pinned): `'front'` (−Y), `'back'` (+Y), `'left'` (−X), `'right'` (+X), `'top'` (+Z), `'bottom'` (−Z)
678
+ - 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'`
679
+ - 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'`
680
+
681
+ Anchor word order is flexible for built-ins: `'front-left'` and `'left-front'` are treated the same.
682
+
683
+ **Returns:** Same type as caller (Shape or TrackedShape)
684
+
685
+ ```javascript
686
+ const base = box(100, 100, 10);
687
+ const column = cylinder(50, 8);
688
+
689
+ // Place column on top of base, centered
690
+ const placed = column.attachTo(base, 'top', 'bottom');
691
+
692
+ // Stack boxes: place b on top of a, aligned at back-left corner
693
+ const a = box(50, 50, 20);
694
+ const b = box(30, 30, 10);
695
+ const stacked = b.attachTo(a, 'top-back-left', 'bottom-back-left');
696
+
697
+ // Place with offset: center on top, then shift 10mm right
698
+ const shifted = column.attachTo(base, 'top', 'bottom', [10, 0, 0]);
699
+ ```
700
+
701
+ ### `.onFace(parent, face, opts?)`
702
+ Place a shape on a specific face of a parent shape. Think of it like sticking a label on a box surface.
703
+
704
+ **Parameters:**
705
+ - `parent` (Shape | TrackedShape) — The parent shape
706
+ - `face` ('front' | 'back' | 'left' | 'right' | 'top' | 'bottom') — Which face to place on
707
+ - `opts` (object, optional):
708
+ - `u` (number) — Horizontal offset within the face (from center). Default: 0
709
+ - `v` (number) — Vertical offset within the face (from center). Default: 0
710
+ - `protrude` (number) — How far the child sticks out from the face. Default: 0
711
+
712
+ **Face coordinate mapping (u, v):**
713
+ - front/back: u = left/right (X), v = up/down (Z)
714
+ - left/right: u = forward/back (Y), v = up/down (Z)
715
+ - top/bottom: u = left/right (X), v = forward/back (Y)
716
+
717
+ **Returns:** Same type as caller
718
+
719
+ ```javascript
720
+ const body = box(100, 40, 60, true);
721
+
722
+ // Vent on front face, centered, 15mm below center, protruding 2mm
723
+ const vent = box(80, 2, 12, true).color('#333')
724
+ .onFace(body, 'front', { v: -15, protrude: 2 });
725
+
726
+ // Display near top-right of front face
727
+ const display = box(35, 1.5, 8, true).color('#00ddee')
728
+ .onFace(body, 'front', { u: 20, v: 15, protrude: 1 });
729
+
730
+ // Fan on top, protruding 5mm
731
+ const fan = cylinder(10, 40).color('#333')
732
+ .onFace(body, 'top', { protrude: 5 });
733
+
734
+ // Side vent on left face
735
+ const sideVent = box(2, 30, 40, true).color('#666')
736
+ .onFace(body, 'left', { protrude: 1 });
737
+ ```
738
+
739
+ **When to use `onFace()` vs `attachTo()`:**
740
+ - `onFace()` — placing surface details (vents, displays, buttons, labels) on a parent body
741
+ - `attachTo()` — stacking independent parts (column on base, unit on wall)
742
+
743
+ ## Advanced 3D Operations
744
+
745
+ ### `hull3d(...args)`
746
+ Convex hull of multiple shapes and/or points.
747
+
748
+ ```javascript
749
+ const hull = hull3d(
750
+ sphere(10),
751
+ sphere(10).translate(50, 0, 0),
752
+ [25, 0, 30], // bare point
753
+ );
754
+ ```
755
+
756
+ ### `levelSet(sdf, bounds, edgeLength, level?)`
757
+ Create a shape from a signed distance function (SDF). Positive = inside.
758
+
759
+ ```javascript
760
+ const gyroid = levelSet(
761
+ ([x, y, z]) => Math.sin(x) * Math.cos(y) + Math.sin(y) * Math.cos(z) + Math.sin(z) * Math.cos(x),
762
+ { min: [-10, -10, -10], max: [10, 10, 10] },
763
+ 0.5, // edge length (resolution)
764
+ );
765
+ ```
766
+
767
+ ### Smoothing
768
+
769
+ ```javascript
770
+ // Mark edges for smoothing, then subdivide
771
+ const smooth = box(50, 50, 50, true)
772
+ .smoothOut(60) // edges sharper than 60° get smoothed
773
+ .refine(4); // subdivide 4 times
774
+
775
+ // Or refine by edge length / tolerance
776
+ shape.refineToLength(2); // max edge length 2mm
777
+ shape.refineToTolerance(0.1); // max deviation 0.1mm from smooth surface
778
+ ```
779
+
780
+ ### Cutting
781
+
782
+ ```javascript
783
+ // Split by another shape → [inside, outside]
784
+ const [inside, outside] = shape.split(cutter);
785
+
786
+ // Split by infinite plane → [positive side, opposite side]
787
+ const [above, below] = shape.splitByPlane([0, 0, 1], 10); // Z=10 plane
788
+
789
+ // Trim: keep the positive side of the plane
790
+ const trimmed = shape.trimByPlane([0, 0, 1], 10);
791
+ ```
792
+
793
+ #### `shape.shell(thickness, options?)`
794
+ Hollow out a supported solid by keeping the outside wall and subtracting an inward cavity.
795
+
796
+ Current compiler-backed `shell()` support is intentionally narrow:
797
+
798
+ - compile-covered `box()`, `cylinder()`, and straight `Sketch.extrude()` solids
799
+ - optional `openFaces: ['top' | 'bottom']`
800
+ - rigid transforms before shelling are preserved through both lowerers
801
+ - defended named-face queries on the resulting outer shell faces plus created inner faces where Forge can model them exactly
802
+
803
+ Not supported yet:
804
+
805
+ - tapered extrudes (`scaleTop`)
806
+ - scale transforms before shelling
807
+ - already-shelled results
808
+ - general boolean, `revolve()`, `loft()`, `sweep()`, `sphere()`, hull, and plane-trim bases
809
+
810
+ Forge keeps `shell` as semantic compiler intent, then lowers it into backend-supported boolean/extrude/cylinder plans for both Manifold and CadQuery/OCCT.
811
+
812
+ Supported named-face subset on the shell result:
813
+
814
+ - preserved outer faces stay queryable on defended bases
815
+ - created inner faces use names like `inner-top`, `inner-bottom`, `inner-side`, or `inner-side-right`
816
+ - downstream `onFace(result, 'inner-side-right', ...)` placement is defended on the planar members of that subset
817
+ - 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`)
818
+
819
+ ```javascript
820
+ const cup = roundedRect(80, 50, 6, true)
821
+ .extrude(30)
822
+ .translate(4, -3, 2)
823
+ .shell(2.5, { openFaces: ['top'] });
824
+ ```
825
+
826
+ #### `shape.hole(face, options)`
827
+ Compiler-owned circular hole workflow anchored to a face/workplane query.
828
+
829
+ - `depth` omitted = through-hole
830
+ - `depth` provided = blind hole
831
+ - `upToFace` = stop on a queried planar face parallel to the hole direction
832
+ - `extent: { forward, reverse? }` = two-sided extent with forward/reverse termination options
833
+ - `u` / `v` place the hole in face-local coordinates
834
+ - `counterbore: { diameter, depth }` adds a wider cylindrical recess at the entry
835
+ - `countersink: { diameter, angleDeg? }` adds a conical entry (default `90°`)
836
+ - `thread: { designation?, pitch?, class?, handedness?, depth?, modeled? }` carries thread intent metadata
837
+ - `depth` and `upToFace` are mutually exclusive with `extent`
838
+ - `counterbore` and `countersink` are mutually exclusive
839
+
840
+ ```javascript
841
+ const block = roundedRect(90, 60, 8, true).extrude(24);
842
+ const exitFace = block.face('bottom');
843
+
844
+ const body = block
845
+ .hole('front', { diameter: 8, u: 0, v: 2 }) // through
846
+ .hole('top', { diameter: 6, u: -18, v: 10, depth: 10 }) // blind
847
+ .hole('top', {
848
+ diameter: 5,
849
+ u: 18,
850
+ v: 10,
851
+ upToFace: exitFace,
852
+ counterbore: { diameter: 9, depth: 4 },
853
+ });
854
+ ```
855
+
856
+ Supported subset:
857
+
858
+ - compile-covered target bodies
859
+ - canonical faces, tracked planar faces, and `FaceRef` targets
860
+ - through, blind, and planar `upToFace` circular holes
861
+ - two-sided extents with forward/reverse termination (blind or `upToFace` per side)
862
+ - counterbore and countersink entry variants on one-sided hole extents
863
+ - thread metadata for deferred thread intent (modeled threads not yet supported)
864
+ - exact lowering through both Manifold and CadQuery/OCCT
865
+ - created hole faces in the defended subset:
866
+ - `wall` on all supported hole variants
867
+ - `floor` on blind holes
868
+ - `cap` on reverse blind two-sided holes
869
+ - `counterbore-wall` and `counterbore-floor` on counterbored holes
870
+ - `countersink-wall` on countersunk holes
871
+ - preserved non-host faces stay queryable where Forge can defend them
872
+ - rewritten host/exit faces now stay queryable as defended descendant regions when Forge can keep one stable source surface/frame
873
+ - 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')`)
874
+
875
+ Not supported yet:
876
+
877
+ - non-planar or non-parallel `upToFace` termination faces
878
+ - two-sided extents combined with counterbore or countersink heads
879
+ - modeled helical threads (pass thread metadata with `modeled: false` or omitted for deferred intent)
880
+ - combined counterbore+countersink heads
881
+ - drafted/tapered main holes
882
+ - segmented polygonal hole cutters
883
+ - runtime-only target bodies without compiler intent
884
+
885
+ #### `shape.cutout(sketch, options?)`
886
+ Compiler-owned cut-extrude workflow using a sketch already placed with `Sketch.onFace(...)`.
887
+
888
+ - `depth` omitted = through-cut
889
+ - `depth` provided = blind cut
890
+ - `upToFace` = stop on a queried planar face parallel to the cut direction
891
+ - `extent: { forward, reverse? }` = two-sided extent with forward/reverse termination options
892
+ - `taperScale` = draft angle for tapered cuts (uniform or `[x, y]` pair)
893
+ - the sketch must carry semantic workplane placement from `onFace(...)`
894
+
895
+ ```javascript
896
+ const block = roundedRect(90, 60, 8, true).extrude(24);
897
+ const exitFace = block.face('bottom');
898
+ const pocket = roundedRect(18, 10, 2, true)
899
+ .onFace(block, 'top', { u: 14, v: -8, selfAnchor: 'center' });
900
+
901
+ const body = block.cutout(pocket, { upToFace: exitFace });
902
+ ```
903
+
904
+ Supported subset:
905
+
906
+ - compile-covered sketch profiles that CadQuery/OCCT already supports
907
+ - sketches placed on queried faces via `onFace(...)`
908
+ - through, blind, and planar `upToFace` cut extents
909
+ - two-sided extents with forward/reverse termination
910
+ - tapered cuts via `taperScale` on circle, rect, and roundedRect profiles
911
+ - exact/runtime parity through the shared compiler node family
912
+ - created cut faces in the defended subset:
913
+ - `floor` on blind cuts
914
+ - `wall` on circular cuts
915
+ - `wall-bottom`, `wall-right`, `wall-top`, `wall-left` on rectangular and rounded-rectangle cuts
916
+ - `cap` on reverse blind two-sided cuts
917
+ - preserved non-host faces stay queryable where Forge can defend them
918
+ - rewritten host/exit faces now stay queryable as defended descendant regions when Forge can keep one stable source surface/frame
919
+ - reusing the same `upToFace` stop plane through later rewrites is supported when you keep a `FaceRef` from the earlier face
920
+
921
+ Not supported yet:
922
+
923
+ - free-floating sketches without face/workplane provenance
924
+ - non-planar or non-parallel `upToFace` termination faces
925
+ - two-sided extents combined with `taperScale`
926
+ - named created-wall support for arbitrary boolean/offset/projected cut profiles
927
+
928
+ #### `sheetMetal(options)`
929
+ Compiler-owned sheet-metal v1 builder.
930
+
931
+ `sheetMetal(...)` returns a `SheetMetalPart`, not a solid directly.
932
+
933
+ Core flow:
934
+
935
+ - define the base panel, thickness, bend radius, and explicit `kFactor`
936
+ - add `90°` edge flanges with `.flange(...)`
937
+ - add planar panel/flange cutouts with `.cutout(...)`
938
+ - materialize either `.folded()` or `.flatPattern()`
939
+
940
+ ```javascript
941
+ const cover = sheetMetal({
942
+ panel: { width: 180, height: 110 },
943
+ thickness: 1.5,
944
+ bendRadius: 2,
945
+ bendAllowance: { kFactor: 0.42 },
946
+ cornerRelief: { size: 4 },
947
+ })
948
+ .flange('top', { length: 18 })
949
+ .flange('right', { length: 18 })
950
+ .flange('bottom', { length: 18 })
951
+ .flange('left', { length: 18 })
952
+ .cutout('panel', rect(72, 36, true), { selfAnchor: 'center' })
953
+ .cutout('flange-right', roundedRect(26, 10, 5, true), { selfAnchor: 'center' });
954
+
955
+ const folded = cover.folded();
956
+ const flat = cover.flatPattern();
957
+ ```
958
+
959
+ Supported v1 subset:
960
+
961
+ - one base panel
962
+ - up to four `90°` edge flanges
963
+ - constant thickness
964
+ - explicit bend radius plus explicit K-factor bend metadata
965
+ - rectangular corner reliefs
966
+ - planar cutouts on the panel and existing flange regions
967
+ - defended semantic regions such as `panel`, `flange-right`, and `bend-right`
968
+
969
+ Not supported yet:
970
+
971
+ - arbitrary solid conversion into sheet metal
972
+ - hems, jogs, lofted bends, or broader miter-corner logic
973
+ - nonuniform thickness
974
+ - cutouts directly on bend regions
975
+
976
+ Use [sheet-metal.md](sheet-metal.md) for the full sheet-metal contract and the maintained `folded-service-panel-cover` proof model.
977
+
978
+ ### Warping
979
+
980
+ ```javascript
981
+ // Deform vertices with arbitrary function
982
+ const warped = box(50, 50, 50, true).warp(([x, y, z]) => {
983
+ // Twist around Z axis
984
+ const angle = z * 0.05;
985
+ const cos = Math.cos(angle), sin = Math.sin(angle);
986
+ const nx = x * cos - y * sin;
987
+ const ny = x * sin + y * cos;
988
+ return [nx, ny, z];
989
+ });
990
+ ```
991
+
992
+ ### Plane Operations
993
+
994
+ ```javascript
995
+ // Cross-section: intersect shape with a plane → Sketch
996
+ const section = intersectWithPlane(shape, { plane: 'XY', offset: 10 });
997
+
998
+ // Project: flatten shape onto a plane → Sketch
999
+ const shadow = projectToPlane(shape, { origin: [0, 0, 0], normal: [0, 0, 1] });
1000
+ ```
1001
+
1002
+ `projectToPlane()` always works as a runtime modeling utility.
1003
+
1004
+ Compiler-owned replay is narrower today:
1005
+ - supported for downstream exact export when the projected source reduces to one defended planar projection basis:
1006
+ - plain `extrude()` / `box()` / `cylinder()` sources in their native XY basis
1007
+ - `Sketch.onFace()`-placed straight extrusions on a matching parallel target plane
1008
+ - compatible `shell()` / through-`hole()` / through-`cutout()` descendants aligned to that same basis
1009
+ - boolean `union()` combinations of compatible projected operands, including mirrored/patterned descendants
1010
+ - aligned blind holes/cuts keep the same replayed silhouette, while aligned through holes/cuts subtract their projected profile from it
1011
+ - the target plane still has to stay parallel to that defended basis without introducing in-plane shear
1012
+ - 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
1013
+
1014
+ ## 2D Sketches
1015
+
1016
+ This file intentionally avoids repeating full 2D API signatures that already live in dedicated docs.
1017
+
1018
+ Use these canonical files:
1019
+
1020
+ - [sketch-core.md](sketch-core.md) - `Sketch` basics, queries, anchors, color, clone/duplicate
1021
+ - [sketch-primitives.md](sketch-primitives.md) - `rect`, `circle2d`, `roundedRect`, `polygon`, `ngon`, `ellipse`, `slot`, `star`
1022
+ - [sketch-path.md](sketch-path.md) - `path()` builder and `stroke(...)`
1023
+ - [sketch-transforms.md](sketch-transforms.md) - translate/rotate/scale/mirror for sketches
1024
+ - [sketch-booleans.md](sketch-booleans.md) - `union2d`/`difference2d`/`intersection2d`/`hull2d` and method forms
1025
+ - [sketch-operations.md](sketch-operations.md) - `offset`, `filletCorners`, `simplify`, `warp`, hull
1026
+ - [sketch-on-face.md](sketch-on-face.md) - sketch placement on planar faces
1027
+ - [sketch-extrude.md](sketch-extrude.md) - `extrude` and `revolve`
1028
+ - [sketch-anchor.md](sketch-anchor.md) - 2D anchor-based positioning
1029
+ - [entities.md](entities.md) - constrained sketches, named entities, topology-aware utilities
1030
+
1031
+ Integration rule: start with the smallest relevant doc set and add more only when the task expands.
1032
+
1033
+ ### Curves & Surfacing
1034
+
1035
+ #### `spline2d(points, options?)`
1036
+ Build a smooth Catmull-Rom spline sketch from 2D control points.
1037
+
1038
+ **Options:**
1039
+ - `closed` (boolean) - Default: `true`
1040
+ - `tension` (number, 0..1) - Default: `0.5`
1041
+ - `samplesPerSegment` (number) - Default: `16`
1042
+ - `strokeWidth` (number) - Required when `closed: false` (creates a stroked solid)
1043
+ - `join` (`'Round' | 'Square'`) - Stroke corner style for open splines. Default: `'Round'`
1044
+
1045
+ ```javascript
1046
+ const closed = spline2d([[20,0],[12,10],[0,12],[-12,10],[-20,0],[0,-14]]);
1047
+ const openRail = spline2d([[0,0],[30,20],[70,10]], { closed: false, strokeWidth: 3 });
1048
+ ```
1049
+
1050
+ #### `spline3d(points, options?)` / `Curve3D`
1051
+ Create a reusable 3D spline curve object.
1052
+
1053
+ `Curve3D` methods:
1054
+ - `.sample(count?)`
1055
+ - `.sampleBySegment(samplesPerSegment?)`
1056
+ - `.pointAt(t)` where `t` is `[0..1]`
1057
+ - `.tangentAt(t)`
1058
+ - `.length(samples?)`
1059
+
1060
+ ```javascript
1061
+ const rail = spline3d(
1062
+ [[0,0,0], [20,10,30], [40,0,60]],
1063
+ { tension: 0.45 }
1064
+ );
1065
+ ```
1066
+
1067
+ #### `loft(profiles, heights, options?)`
1068
+ Loft between multiple sketches along Z stations.
1069
+
1070
+ 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.
1071
+
1072
+ 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.
1073
+
1074
+ **Parameters:**
1075
+ - `profiles` (`Sketch[]`) - At least 2
1076
+ - `heights` (`number[]`) - Same length as `profiles`, strictly increasing
1077
+ - `options`:
1078
+ - `edgeLength` (number) - Mesh resolution
1079
+ - `boundsPadding` (number) - Extra level-set bounds padding
1080
+
1081
+ ```javascript
1082
+ const body = loft(
1083
+ [circle2d(20), roundedRect(30, 24, 6, true), circle2d(10)],
1084
+ [0, 40, 70],
1085
+ { edgeLength: 1.0 }
1086
+ );
1087
+ ```
1088
+
1089
+ #### `sweep(profile, path, options?)`
1090
+ Sweep a 2D profile along a 3D path (`Curve3D` or point polyline).
1091
+
1092
+ 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.
1093
+
1094
+ **Parameters:**
1095
+ - `profile` (`Sketch`) - Local cross-section in XY plane
1096
+ - `path` (`Curve3D | [x,y,z][]`)
1097
+ - `options`:
1098
+ - `samples` (number) - Sampling count for `Curve3D` paths (default `48`)
1099
+ - `edgeLength` (number) - Mesh resolution
1100
+ - `boundsPadding` (number) - Extra level-set bounds padding
1101
+ - `up` (`[x,y,z]`) - Preferred frame-up vector
1102
+
1103
+ ```javascript
1104
+ const tubePath = spline3d([[0,0,0], [20,0,20], [40,10,30]]);
1105
+ const tube = sweep(circle2d(3), tubePath, { samples: 36, edgeLength: 0.7 });
1106
+ ```
1107
+
1108
+ ## Entities, Constraints, and Patterns
1109
+
1110
+ `reference.md` delegates detailed entity/constraint coverage to [entities.md](entities.md) to avoid duplication.
1111
+
1112
+ Use [entities.md](entities.md) for:
1113
+
1114
+ - `Point2D`, `Line2D`, `Circle2D`, `Rectangle2D`
1115
+ - `TrackedShape` topology access (`face`, `edge`, `faceNames`, `edgeNames`, `toShape`)
1116
+ - `constrainedSketch()` and `Constraint.*`
1117
+ - `linearPattern`, `circularPattern`, `mirrorCopy`
1118
+ - `filletEdge`, `chamferEdge`, `arcBridgeBetweenRects`
1119
+ - Utility helpers like `degrees(...)` and `radians(...)`
1120
+
1121
+ ## Multi-File Projects
1122
+
1123
+ 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`).
1124
+
1125
+ ### File Types
1126
+ - `*.sketch.js` — 2D sketch file; when used with `importSketch()`, must return a `Sketch`
1127
+ - `*.forge.js` — 3D Forge file:
1128
+ - when used with `importPart()`, must return a `Shape` or `TrackedShape`
1129
+ - when used with `importGroup()`, must return a `ShapeGroup` (via `group(...)`)
1130
+ - `*.svg` — vector artwork file, imported as sketch geometry
1131
+
1132
+ ### Import Path Resolution
1133
+ - `./file.forge.js`, `./file.sketch.js`, `./asset.svg` (and `../...`) resolve relative to the file that calls imports
1134
+ - Bare paths like `api/bracket.forge.js` resolve from the opened project root
1135
+ - Leading `/` is treated as project-root relative
1136
+
1137
+ ### `importSketch(fileName, paramOverridesOrSvgOptions?)`
1138
+ Imports a sketch and returns `Sketch`.
1139
+
1140
+ - For `*.sketch.js`: executes the file (must return `Sketch`)
1141
+ - For `*.svg`: parses vector geometry into a `Sketch`
1142
+
1143
+ **Parameters:**
1144
+ - `fileName` (string) — Import path (e.g. `"./profile.sketch.js"` or `"api/profile.sketch.js"`)
1145
+ - `paramOverridesOrSvgOptions` (optional object)
1146
+ - For `*.sketch.js`: import-time param overrides by param name
1147
+ - For `*.svg`: SVG import options (see `importSvgSketch`)
1148
+
1149
+ **Returns:** `Sketch`
1150
+
1151
+ ```javascript
1152
+ // In a .forge.js file:
1153
+ const profile = importSketch("bracket-profile.sketch.js", {
1154
+ "Width": 42,
1155
+ "Height": 18,
1156
+ });
1157
+ return profile.extrude(50);
1158
+ ```
1159
+
1160
+ ```javascript
1161
+ // Import SVG and keep only the largest connected region
1162
+ const logo = importSketch("assets/logo.svg", {
1163
+ include: "auto",
1164
+ regionSelection: "largest",
1165
+ flattenTolerance: 0.25,
1166
+ });
1167
+ return logo.extrude(2);
1168
+ ```
1169
+
1170
+ ### `importSvgSketch(fileName, options?)`
1171
+ Parses an SVG file and returns a `Sketch`.
1172
+
1173
+ **Parameters:**
1174
+ - `fileName` (string) — Import path to an `.svg`
1175
+ - `options` (optional object):
1176
+ - `include`: `'auto' | 'fill' | 'stroke' | 'fill-and-stroke'` (default: `'auto'`)
1177
+ - `regionSelection`: `'all' | 'largest'` (default: `'all'`)
1178
+ - `maxRegions`: number (largest-first cap)
1179
+ - `minRegionArea`: number
1180
+ - `minRegionAreaRatio`: number (fraction of largest region area)
1181
+ - `flattenTolerance`: number (curve discretization tolerance)
1182
+ - `arcSegments`: number (minimum arc segment count)
1183
+ - `scale`: number (uniform scale factor)
1184
+ - `maxWidth`: number (uniformly downscale to keep final sketch width within this limit)
1185
+ - `maxHeight`: number (uniformly downscale to keep final sketch height within this limit)
1186
+ - `centerOnOrigin`: boolean (default: `false`, recenters final sketch bounds center to `(0, 0)`)
1187
+ - `simplify`: number (final simplify tolerance, default: `0`)
1188
+ - `invertY`: boolean (default: `true`, converts SVG Y-down to CAD Y-up)
1189
+
1190
+ **Returns:** `Sketch`
1191
+
1192
+ ```javascript
1193
+ const badge = importSvgSketch("assets/badge.svg", {
1194
+ include: "fill-and-stroke",
1195
+ minRegionAreaRatio: 0.001,
1196
+ maxRegions: 8,
1197
+ maxWidth: 120,
1198
+ maxHeight: 80,
1199
+ centerOnOrigin: true,
1200
+ });
1201
+ return badge;
1202
+ ```
1203
+
1204
+ ### `importPart(fileName, paramOverrides?)`
1205
+ 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`).
1206
+
1207
+ **Parameters:**
1208
+ - `fileName` (string) — Import path (e.g. `"./bracket.forge.js"` or `"api/bracket.forge.js"`)
1209
+ - `paramOverrides` (optional object) — Import-time parameter overrides by param name
1210
+
1211
+ **Returns:** `Shape` (chainable)
1212
+
1213
+ ```javascript
1214
+ // Assembly: import parts and position them
1215
+ const bracket = importPart("bracket.forge.js", { "Thickness": 4 });
1216
+ const bracket2 = importPart("bracket.forge.js", { "Thickness": 8 })
1217
+ .translate(100, 0, 0)
1218
+ .rotate(0, 0, 180);
1219
+
1220
+ return union(bracket, bracket2);
1221
+ ```
1222
+
1223
+ Imported parts can also carry named placement references:
1224
+
1225
+ ```javascript
1226
+ // widget.forge.js
1227
+ return union(base, post).withReferences({
1228
+ points: {
1229
+ mount: [0, -16, -4],
1230
+ },
1231
+ objects: {
1232
+ post,
1233
+ },
1234
+ });
1235
+ ```
1236
+
1237
+ ```javascript
1238
+ // assembly.forge.js
1239
+ const widget = importPart("widget.forge.js")
1240
+ .placeReference("mount", [120, 40, 0]);
1241
+
1242
+ const cap = box(18, 18, 8, true)
1243
+ .attachTo(widget, "objects.post.top", "bottom");
1244
+
1245
+ return [widget, cap];
1246
+ ```
1247
+
1248
+ ### `importGroup(fileName, paramOverrides?)`
1249
+ 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.
1250
+
1251
+ **Parameters:**
1252
+ - `fileName` (string) — Import path (e.g. `"./bracket-assembly.forge.js"`)
1253
+ - `paramOverrides` (optional object) — Import-time parameter overrides by param name
1254
+
1255
+ **Returns:** `ShapeGroup` (chainable)
1256
+
1257
+ ```javascript
1258
+ // bracket-assembly.forge.js ← the source file
1259
+ const thickness = param("Thickness", 4);
1260
+ const leftBracket = box(thickness, 60, 40).color('#5b7c8d');
1261
+ const rightBracket = box(thickness, 60, 40).translate(64, 0, 0).color('#5b7c8d');
1262
+ const dowel = cylinder(60, 3).rotate(90,0,0).translate(34, 60, 20).color('#d38b4d');
1263
+
1264
+ return group(
1265
+ { name: "Bracket Left", shape: leftBracket },
1266
+ { name: "Bracket Right", shape: rightBracket },
1267
+ { name: "Dowel", shape: dowel },
1268
+ ).withReferences({
1269
+ points: { mountCenter: [0, 30, 20] },
1270
+ });
1271
+ ```
1272
+
1273
+ ```javascript
1274
+ // scene.forge.js ← the consumer
1275
+ // Import and use as a whole unit
1276
+ const bracketA = importGroup("bracket-assembly.forge.js")
1277
+ .placeReference("mountCenter", [0, 0, 0]);
1278
+
1279
+ // Import with param overrides, then work on children individually
1280
+ const bracketB = importGroup("bracket-assembly.forge.js", { "Thickness": 6 });
1281
+ const leftOnly = bracketB.child("Bracket Left").color('#ff4444');
1282
+ const rightOnly = bracketB.child("Bracket Right").translate(200, 0, 0);
1283
+
1284
+ return [bracketA, leftOnly, rightOnly];
1285
+ ```
1286
+
1287
+ **When to use `importGroup` vs `importPart`:**
1288
+
1289
+ | | `importPart` | `importGroup` |
1290
+ |---|---|---|
1291
+ | Source returns | `Shape` or `TrackedShape` | `ShapeGroup` via `group(...)` |
1292
+ | Result type | `Shape` — chainable, supports all boolean ops | `ShapeGroup` — children stay separate |
1293
+ | Access children | Not possible | `group.child("Name")` |
1294
+ | Placement refs | `.withReferences()` on the Shape | `.withReferences()` on the group |
1295
+
1296
+ ### Import Rules
1297
+ - Circular imports are detected and throw an error
1298
+ - Imported files can be instantiated multiple times (each call is a fresh execution)
1299
+ - `paramOverrides` only affects that import call (other imports are independent)
1300
+ - Params supplied through `paramOverrides` are treated as fixed arguments for that import call
1301
+ - Relative imports (`./` / `../`) are resolved from the current file path
1302
+ - `importPart()` accepts `Shape` or `TrackedShape` results and always returns a chainable `Shape`
1303
+ - `importGroup()` accepts only `ShapeGroup` results; use `group(...)` as the return value in the source file
1304
+ - Source files can attach placement references with `.withReferences({ points, edges, surfaces, objects })` — works on both `Shape` and `ShapeGroup`
1305
+ - Imported tracked solids keep their named faces/edges as `surfaces.<faceName>` and `edges.<edgeName>` references
1306
+ - SVG import supports deterministic region filtering (`regionSelection`, `maxRegions`, area thresholds)
1307
+ - The returned `Shape`, `Sketch`, or `ShapeGroup` is fully chainable — use `.translate()`, `.rotate()`, etc.
1308
+
1309
+ ### Plain JS Module Imports
1310
+ Alongside `importPart()` / `importSketch()`, regular JS `import` / `require(...)` is supported for utility modules.
1311
+
1312
+ - If a module uses `export` / `module.exports`, that export value is used.
1313
+ - If a module has no explicit exports and uses a top-level `return`, that return value becomes the module value (including arrays).
1314
+ - Do not mix explicit exports with top-level `return` in the same module; this throws an error.
1315
+
1316
+ ```javascript
1317
+ // scene-items.js
1318
+ import { box, cylinder } from "forgecad";
1319
+
1320
+ return [
1321
+ { name: "Plate", shape: box(20, 12, 2, true) },
1322
+ { name: "Pin", shape: cylinder(14, 3, undefined, undefined, true).translate(0, 0, 8) },
1323
+ ];
1324
+ ```
1325
+
1326
+ ```javascript
1327
+ // main.forge.js
1328
+ import items from "./scene-items.js";
1329
+
1330
+ return items.map((entry, index) => ({
1331
+ name: entry.name,
1332
+ shape: entry.shape.translate(index === 0 ? -20 : 20, 0, 0),
1333
+ }));
1334
+ ```
1335
+
1336
+ ### Placement References
1337
+
1338
+ ### `.withReferences({ points?, edges?, surfaces?, objects? })`
1339
+ Attach named placement references to a `Shape`, `TrackedShape`, or `ShapeGroup`. References survive all normal transforms and import round-trips (`importPart()`, `importGroup()`).
1340
+
1341
+ **Reference kinds:**
1342
+ - `points`: exact 3D coordinates
1343
+ - `edges`: `{ start, end }` segments; default reference point is the midpoint
1344
+ - `surfaces`: `{ center, normal }`; default reference point is the center
1345
+ - `objects`: bounding boxes derived from another shape/group or explicit `{ min, max }`
1346
+
1347
+ ```javascript
1348
+ const part = union(base, post).withReferences({
1349
+ points: {
1350
+ mount: [0, -16, -4],
1351
+ },
1352
+ edges: {
1353
+ postAxis: { start: [12, 0, 4], end: [12, 0, 30] },
1354
+ },
1355
+ surfaces: {
1356
+ mountingFace: { center: [0, -16, 0], normal: [0, -1, 0] },
1357
+ },
1358
+ objects: {
1359
+ base,
1360
+ post,
1361
+ },
1362
+ });
1363
+ ```
1364
+
1365
+ ### `.referenceNames(kind?)`
1366
+ Lists named placement references on a shape.
1367
+
1368
+ ```javascript
1369
+ part.referenceNames(); // ['edges.postAxis', 'objects.base', 'objects.post', 'points.mount', ...]
1370
+ part.referenceNames('points'); // ['mount']
1371
+ ```
1372
+
1373
+ ### `.referencePoint(ref)`
1374
+ Resolve a placement reference to a world-space point.
1375
+
1376
+ Supported forms:
1377
+ - `mount` or `points.mount`
1378
+ - `edges.postAxis`
1379
+ - `edges.postAxis.start`
1380
+ - `surfaces.mountingFace`
1381
+ - `objects.post.top`
1382
+
1383
+ ```javascript
1384
+ const p = part.referencePoint("objects.post.top");
1385
+ ```
1386
+
1387
+ ### `.placeReference(ref, [x, y, z], offset?)`
1388
+ Translate a shape so the given placement reference lands on a target coordinate.
1389
+
1390
+ ```javascript
1391
+ const placed = importPart("widget.forge.js")
1392
+ .placeReference("mount", [120, 40, 0]);
1393
+ ```
1394
+
1395
+ ### `attachTo()` with named references
1396
+
1397
+ `attachTo()` still accepts the built-in 3D anchors, but it can now also consume named placement references:
1398
+
1399
+ ```javascript
1400
+ const cap = box(18, 18, 8, true)
1401
+ .attachTo(widget, "objects.post.top", "bottom");
1402
+ ```
1403
+
1404
+ ### Typical Project Structure
1405
+ ```
1406
+ my-project/
1407
+ ├── base-profile.sketch.js ← 2D cross-section
1408
+ ├── bracket.forge.js ← extrudes the sketch, adds holes
1409
+ └── assembly.forge.js ← imports multiple parts, positions them
1410
+ ```
1411
+
1412
+ ## Part Library
1413
+
1414
+ Pre-built parametric parts available via `lib.xxx()`. No imports needed.
1415
+
1416
+ ### `lib.boltHole(diameter, depth)`
1417
+ Through-hole cylinder (centered).
1418
+
1419
+ ### `lib.fastenerHole(opts)`
1420
+ Standardized metric hole helper with fits and optional counterbore/countersink.
1421
+
1422
+ ```javascript
1423
+ const m4 = lib.fastenerHole({
1424
+ size: "M4",
1425
+ fit: "normal", // close | normal | loose | tap
1426
+ depth: 12,
1427
+ counterbore: { depth: 3.5 }, // diameter auto from size unless provided
1428
+ });
1429
+ ```
1430
+
1431
+ ### `lib.counterbore(holeDia, boreDia, boreDepth, totalDepth)`
1432
+ Through-hole with wider recess at top.
1433
+
1434
+ ### `lib.tube(outerX, outerY, outerZ, wall)`
1435
+ Rectangular hollow tube.
1436
+
1437
+ ### `lib.pipe(height, outerRadius, wall, segments?)`
1438
+ Hollow cylinder.
1439
+
1440
+ ### `lib.hexNut(acrossFlats, height, holeDia)`
1441
+ Hex nut via intersection of 3 rotated slabs, with center bore.
1442
+
1443
+ ### `lib.roundedBox(x, y, z, radius)`
1444
+ Approximate rounded box via union of axis-aligned slabs.
1445
+
1446
+ ### `lib.bracket(width, height, depth, thick, holeDia?)`
1447
+ L-shaped mounting bracket with optional holes.
1448
+
1449
+ ### `lib.holePattern(rows, cols, spacingX, spacingY, holeDia, depth)`
1450
+ Grid of cylindrical holes.
1451
+
1452
+ ### `lib.spurGear(options)`
1453
+ Involute external spur gear with optional bore.
1454
+
1455
+ **Options:**
1456
+ - `module` (number) - Metric module (pitch diameter / tooth count)
1457
+ - `teeth` (integer) - Tooth count (>= 6)
1458
+ - `faceWidth` (number) - Extrusion width along Z
1459
+ - `pressureAngleDeg` (number, optional) - Default: `20`
1460
+ - `backlash` (number, optional) - Tangential backlash at pitch circle. Default: `0`
1461
+ - `clearance` (number, optional) - Root clearance. Default: `0.25 * module`
1462
+ - `addendum` (number, optional) - Tooth addendum. Default: `module`
1463
+ - `dedendum` (number, optional) - Tooth dedendum. Default: `addendum + clearance`
1464
+ - `boreDiameter` (number, optional) - Center bore diameter
1465
+ - `center` (boolean, optional) - Center extrusion around Z=0. Default: `true`
1466
+ - `segmentsPerTooth` (number, optional) - Involute sampling quality. Default: `10`
1467
+
1468
+ ```javascript
1469
+ const pinion = lib.spurGear({
1470
+ module: 1.25,
1471
+ teeth: 14,
1472
+ faceWidth: 8,
1473
+ boreDiameter: 5,
1474
+ });
1475
+ ```
1476
+
1477
+ ### `lib.faceGear(options)`
1478
+ Face gear (crown style) where teeth are on one face (`top` or `bottom`) instead of the outer rim.
1479
+
1480
+ Uses the same involute tooth sizing inputs as `lib.spurGear(...)`, then projects the tooth band axially from one side.
1481
+
1482
+ **Options:**
1483
+ - all `lib.spurGear(...)` options, plus:
1484
+ - `side` (`'top' | 'bottom'`, optional) - Which face gets the teeth. Default: `'top'`
1485
+ - `toothHeight` (number, optional) - Tooth projection height from the selected face. Default: `module`
1486
+
1487
+ ```javascript
1488
+ const face = lib.faceGear({
1489
+ module: 1.25,
1490
+ teeth: 36,
1491
+ faceWidth: 8,
1492
+ toothHeight: 1.2,
1493
+ side: 'top',
1494
+ boreDiameter: 8,
1495
+ });
1496
+ ```
1497
+
1498
+ `lib.sideGear(...)` is kept as a compatibility alias.
1499
+
1500
+ ### `lib.ringGear(options)`
1501
+ Internal ring gear with involute-derived tooth spaces.
1502
+
1503
+ **Options:**
1504
+ - `module` (number)
1505
+ - `teeth` (integer, >= 12)
1506
+ - `faceWidth` (number)
1507
+ - `pressureAngleDeg` (number, optional) - Default: `20`
1508
+ - `backlash` (number, optional) - Default: `0`
1509
+ - `clearance` (number, optional) - Default: `0.25 * module`
1510
+ - `addendum` (number, optional) - Default: `module`
1511
+ - `dedendum` (number, optional) - Default: `addendum + clearance`
1512
+ - `rimWidth` (number, optional) - Radial ring thickness outside tooth roots
1513
+ - `outerDiameter` (number, optional) - Overrides `rimWidth` if provided
1514
+ - `center` (boolean, optional) - Default: `true`
1515
+ - `segmentsPerTooth` (number, optional) - Default: `10`
1516
+
1517
+ ```javascript
1518
+ const ring = lib.ringGear({
1519
+ module: 1.25,
1520
+ teeth: 58,
1521
+ faceWidth: 10,
1522
+ rimWidth: 4,
1523
+ });
1524
+ ```
1525
+
1526
+ ### `lib.rackGear(options)`
1527
+ Linear rack gear with pressure-angle flanks.
1528
+
1529
+ **Options:**
1530
+ - `module` (number)
1531
+ - `teeth` (integer, >= 2)
1532
+ - `faceWidth` (number)
1533
+ - `pressureAngleDeg` (number, optional) - Default: `20`
1534
+ - `backlash` (number, optional) - Default: `0`
1535
+ - `clearance` (number, optional) - Default: `0.25 * module`
1536
+ - `addendum` (number, optional) - Default: `module`
1537
+ - `dedendum` (number, optional) - Default: `addendum + clearance`
1538
+ - `baseHeight` (number, optional) - Rack body thickness behind root line
1539
+ - `center` (boolean, optional) - Default: `true`
1540
+
1541
+ ```javascript
1542
+ const rack = lib.rackGear({
1543
+ module: 1.25,
1544
+ teeth: 24,
1545
+ faceWidth: 8,
1546
+ baseHeight: 3.5,
1547
+ });
1548
+ ```
1549
+
1550
+ ### `lib.bevelGear(options)`
1551
+ Conical bevel gear generated from a tapered involute extrusion.
1552
+
1553
+ **Options:**
1554
+ - `module` (number)
1555
+ - `teeth` (integer, >= 6)
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
+ - `boreDiameter` (number, optional)
1563
+ - pitch cone setup (choose one):
1564
+ - `pitchAngleDeg` (number, optional), or
1565
+ - `mateTeeth` (+ optional `shaftAngleDeg`, default `90`) for auto pitch-angle derivation
1566
+ - `center` (boolean, optional) - Default: `true`
1567
+ - `segmentsPerTooth` (number, optional) - Default: `10`
1568
+
1569
+ ```javascript
1570
+ const bevelPinion = lib.bevelGear({
1571
+ module: 1.5,
1572
+ teeth: 18,
1573
+ faceWidth: 10,
1574
+ mateTeeth: 36,
1575
+ shaftAngleDeg: 90,
1576
+ boreDiameter: 5,
1577
+ });
1578
+ ```
1579
+
1580
+ ### `lib.gearPair(options)`
1581
+ Build or validate a spur-gear pair and return ratio/backlash/mesh diagnostics.
1582
+
1583
+ Accepts either:
1584
+ - spur gear shapes produced by `lib.spurGear(...)`, or
1585
+ - analytical specs (`{ module, teeth, ... }`) for each member
1586
+
1587
+ **Options:**
1588
+ - `pinion` (`Shape | GearPairSpec`) - input gear
1589
+ - `gear` (`Shape | GearPairSpec`) - mating output gear
1590
+ - `backlash` (number, optional) - target backlash used for auto center distance
1591
+ - `centerDistance` (number, optional) - override center distance directly
1592
+ - `place` (boolean, optional) - auto-place `gear` at +X center distance. Default: `true`
1593
+ - `phaseDeg` (number, optional) - additional Z rotation applied to placed gear before translation
1594
+
1595
+ **Returns:** `GearPairResult` with:
1596
+ - `pinion`, `gear` (shapes)
1597
+ - `jointRatio`, `speedReduction`
1598
+ - `centerDistance`, `centerDistanceNominal`, `backlash`
1599
+ - `pressureAngleDeg`, `workingPressureAngleDeg`, `contactRatio`
1600
+ - `diagnostics[]` and `status` (`ok | warn | error`)
1601
+
1602
+ ```javascript
1603
+ const pair = lib.gearPair({
1604
+ pinion: { module: 1.25, teeth: 14, faceWidth: 8, boreDiameter: 5 },
1605
+ gear: { module: 1.25, teeth: 42, faceWidth: 8, boreDiameter: 8 },
1606
+ backlash: 0.05,
1607
+ });
1608
+
1609
+ if (pair.status !== 'ok') {
1610
+ console.warn(pair.diagnostics);
1611
+ }
1612
+
1613
+ return [pair.pinion, pair.gear];
1614
+ ```
1615
+
1616
+ ### `lib.bevelGearPair(options)`
1617
+ Build or validate a bevel-gear pair and return ratio diagnostics plus recommended joint placement vectors.
1618
+
1619
+ Accepts either:
1620
+ - bevel gear shapes produced by `lib.bevelGear(...)`, or
1621
+ - analytical specs (`{ module, teeth, ... }`) for each member
1622
+
1623
+ **Options:**
1624
+ - `pinion` (`Shape | GearPairSpec`)
1625
+ - `gear` (`Shape | GearPairSpec`)
1626
+ - `shaftAngleDeg` (number, optional) - Default: `90`
1627
+ - `backlash` (number, optional)
1628
+ - `place` (boolean, optional) - Apply recommended transforms to returned shapes. Default: `true`
1629
+ - `phaseDeg` (number, optional) - Extra phase on the placed driven bevel gear
1630
+
1631
+ **Returns:** `BevelGearPairResult` with:
1632
+ - `pinion`, `gear` (shapes)
1633
+ - `jointRatio`, `speedReduction`
1634
+ - `shaftAngleDeg`, `pinionPitchAngleDeg`, `gearPitchAngleDeg`, `coneDistance`
1635
+ - `pinionAxis`, `gearAxis`, `pinionCenter`, `gearCenter` (joint setup helpers)
1636
+ - `diagnostics[]` and `status` (`ok | warn | error`)
1637
+
1638
+ ```javascript
1639
+ const bevelPair = lib.bevelGearPair({
1640
+ pinion: { module: 1.5, teeth: 18, faceWidth: 10 },
1641
+ gear: { module: 1.5, teeth: 36, faceWidth: 9 },
1642
+ shaftAngleDeg: 90,
1643
+ });
1644
+ ```
1645
+
1646
+ ### `lib.faceGearPair(options)`
1647
+ Build or validate a perpendicular pair between a face gear and a vertical spur gear.
1648
+
1649
+ Accepts either:
1650
+ - face gear shapes produced by `lib.faceGear(...)` or face-gear specs (`{ module, teeth, ... }`)
1651
+ - vertical spur shapes produced by `lib.spurGear(...)` or spur specs (`{ module, teeth, ... }`)
1652
+
1653
+ **Options:**
1654
+ - `face` (`Shape | FaceGearSpec`) - face/crown gear member
1655
+ - `vertical` (`Shape | GearPairSpec`) - mating perpendicular spur gear
1656
+ - `backlash` (number, optional) - target radial backlash for auto center distance
1657
+ - `centerDistance` (number, optional) - override center distance directly
1658
+ - `meshPlaneZ` (number, optional) - override the Z plane where the vertical gear is placed
1659
+ - `place` (boolean, optional) - auto-place `vertical`. Default: `true`
1660
+ - `phaseDeg` (number, optional) - phase rotation applied before perpendicular placement
1661
+
1662
+ **Returns:** `FaceGearPairResult` with:
1663
+ - `face`, `vertical` (shapes)
1664
+ - `jointRatio`, `speedReduction`
1665
+ - `centerDistance`, `centerDistanceNominal`, `backlash`
1666
+ - `meshPlaneZ`, `radialOverlap`
1667
+ - `diagnostics[]` and `status` (`ok | warn | error`)
1668
+
1669
+ ```javascript
1670
+ const pair = lib.faceGearPair({
1671
+ face: { module: 1.25, teeth: 36, faceWidth: 8, toothHeight: 1.2, side: 'top' },
1672
+ vertical: { module: 1.25, teeth: 12, faceWidth: 8 },
1673
+ backlash: 0.05,
1674
+ });
1675
+
1676
+ if (pair.status !== 'ok') {
1677
+ console.warn(pair.diagnostics);
1678
+ }
1679
+
1680
+ return [pair.face, pair.vertical];
1681
+ ```
1682
+
1683
+ `lib.sideGearPair(...)` is kept as a compatibility alias.
1684
+
1685
+ ### `lib.tSlotProfile(options?)`
1686
+ Build a 2D T-slot cross-section sketch.
1687
+
1688
+ This is a generic, tunable T-slot generator.
1689
+
1690
+ **Options:**
1691
+ - `size` (number) - Outer profile size. Default: `20`
1692
+ - `slotWidth` (number) - Slot mouth width. Default: `6`
1693
+ - `slotInnerWidth` (number) - Wider interior slot cavity width. Default: `10.4`
1694
+ - `slotDepth` (number) - Slot depth from outer face. Default: `6`
1695
+ - `slotNeckDepth` (number) - Narrow mouth depth before widening. Default: `1.6`
1696
+ - `wall` (number) - Outer shell thickness. Default: `1.4`
1697
+ - `web` (number) - Central cross-web thickness. Default: `2.1`
1698
+ - `centerBossDia` (number) - Center boss diameter. Default: `8.2`
1699
+ - `centerBoreDia` (number) - Center bore diameter. Default: `4.2`
1700
+ - `outerCornerRadius` (number) - Outer corner radius. Default: `1`
1701
+ - `segments` (number) - Circle smoothness for 2D bores/bosses. Default: `36`
1702
+
1703
+ **Returns:** `Sketch`
1704
+
1705
+ ```javascript
1706
+ const profile = lib.tSlotProfile();
1707
+ return profile; // 2D drawing-ready cross-section
1708
+ ```
1709
+
1710
+ ### `lib.tSlotExtrusion(length, options?)`
1711
+ Build a 3D extrusion from `lib.tSlotProfile(...)`.
1712
+
1713
+ **Parameters:**
1714
+ - `length` (number) - Extrusion length along Z
1715
+ - `options` - Same options as `lib.tSlotProfile(...)` plus:
1716
+ - `center` (boolean) - Center the length around Z=0. Default: `false`
1717
+
1718
+ **Returns:** `Shape`
1719
+
1720
+ ```javascript
1721
+ const rail = lib.tSlotExtrusion(300, { center: true });
1722
+ ```
1723
+
1724
+ ### `lib.profile2020BSlot6Profile(options?)`
1725
+ Profile-accurate 2D helper for a 20x20 B-type slot 6 section.
1726
+
1727
+ Defaults target common B-type 20x20 conventions:
1728
+ - slot width `6.0`
1729
+ - slot depth `5.5`
1730
+ - center bore `5.5`
1731
+ - center boss `8.4`
1732
+ - diagonal web width `4.4`
1733
+ - no edge pocket holes (only central bore is cut)
1734
+
1735
+ **Options:**
1736
+ - `slotWidth` (number) - Default: `6.0`
1737
+ - `slotInnerWidth` (number) - Default: `8.2`
1738
+ - `slotDepth` (number) - Default: `5.5`
1739
+ - `slotNeckDepth` (number) - Default: `1.8`
1740
+ - `centerBoreDia` (number) - Default: `5.5` (set `0` to disable)
1741
+ - `centerBossDia` (number) - Default: `8.4`
1742
+ - `diagonalWebWidth` (number) - Default: `4.4`
1743
+ - `outerCornerRadius` (number) - Default: `1.0`
1744
+ - `segments` (number) - Default: `40`
1745
+
1746
+ ```javascript
1747
+ const profile2d = lib.profile2020BSlot6Profile();
1748
+ ```
1749
+
1750
+ ### `lib.profile2020BSlot6(length, options?)`
1751
+ 3D extrusion helper built from `lib.profile2020BSlot6Profile(...)`.
1752
+
1753
+ Use `options` to override supplier-specific tolerances.
1754
+ - Supports all profile options above
1755
+ - Plus `center` (boolean) to center length about Z=0
1756
+
1757
+ ```javascript
1758
+ const profile = lib.profile2020BSlot6(500, { center: true });
1759
+ ```
1760
+
1761
+ ### Exploded-view helpers
1762
+ For scene-layout helpers such as `lib.explode(...)` and viewport explode overrides, see [../runtime/viewport.md](../runtime/viewport.md).
1763
+
1764
+ ### `lib.pipeRoute(points, radius, options?)`
1765
+ Route a pipe through 3D waypoints with smooth torus bends at corners.
1766
+
1767
+ **Parameters:**
1768
+ - `points` ([number, number, number][]) - Array of 3D waypoints
1769
+ - `radius` (number) - Pipe outer radius
1770
+ - `options` (object, optional):
1771
+ - `bendRadius` (number) - Radius of bends at corners. Default: `radius * 4`
1772
+ - `wall` (number) - Wall thickness for hollow pipe. If omitted, pipe is solid
1773
+ - `segments` (number) - Circumferential segments. Default: 32
1774
+
1775
+ **Returns:** `Shape`
1776
+
1777
+ ```javascript
1778
+ // Solid copper pipe with 90° bends
1779
+ const refrigPipe = lib.pipeRoute(
1780
+ [[0, 0, 0], [100, 0, 0], [100, 80, 0], [100, 80, 60]],
1781
+ 4,
1782
+ { bendRadius: 20 }
1783
+ ).color('#B87333');
1784
+
1785
+ // Hollow drain pipe
1786
+ const drainPipe = lib.pipeRoute(
1787
+ [[0, 0, 20], [60, 0, 20], [60, 80, 20]],
1788
+ 3,
1789
+ { bendRadius: 15, wall: 1 }
1790
+ ).color('#CCCCCC');
1791
+ ```
1792
+
1793
+ ### `lib.elbow(pipeRadius, bendRadius, angle?, options?)`
1794
+ Curved pipe section (torus arc) for connecting two pipe directions. Creates a bend at the origin.
1795
+
1796
+ **Parameters:**
1797
+ - `pipeRadius` (number) - Pipe outer radius
1798
+ - `bendRadius` (number) - Centerline bend radius
1799
+ - `angle` (number, optional) - Bend angle in degrees. Default: 90
1800
+
1801
+ **Options:**
1802
+ - `wall` (number) - Wall thickness for hollow pipe
1803
+ - `segments` (number) - Circumferential segments. Default: 32
1804
+ - `from` ([number, number, number]) - Incoming direction vector
1805
+ - `to` ([number, number, number]) - Outgoing direction vector (overrides angle)
1806
+
1807
+ **Alternative call:** `lib.elbow(pipeRadius, bendRadius, { from, to, wall, segments })`
1808
+
1809
+ ```javascript
1810
+ // Simple 90° elbow
1811
+ const bend = lib.elbow(5, 20, 90);
1812
+
1813
+ // 45° hollow elbow
1814
+ const bend45 = lib.elbow(5, 20, 45, { wall: 1.5 });
1815
+
1816
+ // Direction-based: connect Z-up pipe to X-right pipe
1817
+ const bend = lib.elbow(5, 20, { from: [0, 0, 1], to: [1, 0, 0] });
1818
+ ```
1819
+
1820
+ ### `lib.thread(diameter, pitch, length, options?)`
1821
+ External thread (helical ridge) via twisted extrusion. Returns a threaded cylinder along +Z.
1822
+
1823
+ **Options:**
1824
+ - `depth` (number) - Thread depth. Default: `pitch * 0.35`
1825
+ - `segments` (number) - Circumferential segments. Default: 36
1826
+
1827
+ ```javascript
1828
+ const m8thread = lib.thread(8, 1.25, 30);
1829
+ const smooth = lib.thread(8, 1.0, 30, { segments: 48 });
1830
+ ```
1831
+
1832
+ ### `lib.bolt(diameter, length, options?)`
1833
+ Hex bolt with real helical threads. Head at z=0, shaft extends along −Z.
1834
+
1835
+ **Options:**
1836
+ - `pitch` (number) - Thread pitch. Default: `diameter * 0.15`
1837
+ - `headHeight` (number) - Default: `diameter * 0.65`
1838
+ - `headAcrossFlats` (number) - Default: `diameter * 1.6`
1839
+ - `threadLength` (number) - Threaded portion. Default: full length
1840
+ - `segments` (number) - Circumferential segments. Default: 36
1841
+
1842
+ ```javascript
1843
+ const m8bolt = lib.bolt(8, 30);
1844
+ const custom = lib.bolt(10, 40, { pitch: 1.5, headHeight: 7 });
1845
+ ```
1846
+
1847
+ ### `lib.nut(diameter, options?)`
1848
+ Hex nut with bore, centered at origin.
1849
+
1850
+ **Options:**
1851
+ - `pitch` (number) - Default: `diameter * 0.15`
1852
+ - `height` (number) - Default: `diameter * 0.8`
1853
+ - `acrossFlats` (number) - Default: `diameter * 1.6`
1854
+ - `segments` (number) - Circumferential segments. Default: 36
1855
+
1856
+ ```javascript
1857
+ const m8nut = lib.nut(8);
1858
+ const m8nut2 = lib.nut(8, { height: 6.5, acrossFlats: 13 });
1859
+ ```
1860
+
1861
+ ## Query Methods
1862
+
1863
+ ### 3D Shape Queries
1864
+ ```javascript
1865
+ shape.volume() // Volume in mm³
1866
+ shape.surfaceArea() // Surface area in mm²
1867
+ shape.boundingBox() // { min: [x,y,z], max: [x,y,z] }
1868
+ shape.isEmpty() // true if no geometry
1869
+ shape.numTri() // Triangle count
1870
+ shape.minGap(other, 50) // Minimum distance to another shape (within search radius)
1871
+ shape.geometryInfo() // { backend, representation, fidelity, topology, sources }
1872
+ ```
1873
+
1874
+ `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.
1875
+
1876
+ For the maintained exact STEP/BREP support matrix, see [../output/brep-export.md](../output/brep-export.md).
1877
+
1878
+ ### 2D Sketch Queries
1879
+ ```javascript
1880
+ sketch.area() // Area in mm²
1881
+ sketch.bounds() // { min: [x,y], max: [x,y] }
1882
+ sketch.isEmpty() // true if no area
1883
+ sketch.numVert() // Vertex count
1884
+ ```
1885
+
1886
+ ## Returning Multiple Objects
1887
+
1888
+ Scripts can return arrays to display multiple objects in the viewport:
1889
+
1890
+ ```javascript
1891
+ // Simple array — auto-named "Object 1", "Object 2", etc.
1892
+ return [
1893
+ box(50, 50, 10),
1894
+ cylinder(20, 8).translate(25, 25, 10),
1895
+ ];
1896
+
1897
+ // Named objects with colors
1898
+ return [
1899
+ { name: "Base Plate", shape: box(100, 100, 5), color: "#888888" },
1900
+ { name: "Column", shape: cylinder(50, 10).translate(50, 50, 5), color: "#4488cc" },
1901
+ { name: "Profile", sketch: circle2d(20), color: "#ff6600" },
1902
+ ];
1903
+ ```
1904
+
1905
+ Each object gets its own visibility toggle, opacity slider, and color picker in the View Panel.
1906
+
1907
+ ### Assembly Groups
1908
+
1909
+ For complex assemblies, use nested groups to organize related parts:
1910
+
1911
+ ```javascript
1912
+ return [
1913
+ { name: "Bed Assembly", group: [
1914
+ { name: "Bed Plate", shape: bedPlate },
1915
+ { name: "Glass Bed", shape: glass },
1916
+ { name: "Heater", shape: heater },
1917
+ ]},
1918
+ { name: "Gantry", group: [
1919
+ { name: "Left Rail", shape: leftRail },
1920
+ { name: "Right Rail", shape: rightRail },
1921
+ { name: "Cross Bar", shape: crossBar },
1922
+ ]},
1923
+ ];
1924
+ ```
1925
+
1926
+ **Benefits:**
1927
+ - **Spatial analysis** skips intra-group collision checks (intentional overlaps)
1928
+ - **Group-level summary** reports relationships between assemblies
1929
+ - **Object listing** shows group tags: `Bed Plate [Bed Assembly]`
1930
+ - **Parameter validation** (`param-check` CLI) ignores collisions within groups
1931
+
1932
+ ## Guides and Examples
1933
+
1934
+ See [../guides/modeling-recipes.md](../guides/modeling-recipes.md) for patterns, best practices, debugging, and sample snippets.
1935
+
1936
+ For runnable end-to-end models, read `examples/api/`.