forgecad 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +2 -11
  2. package/dist/assets/{AdminPage-CeqCUUgu.js → AdminPage-DAu1C1ST.js} +250 -151
  3. package/dist/assets/{BlogPage-P_AJP0v9.js → BlogPage-CJEXL_zJ.js} +94 -70
  4. package/dist/assets/{DocsPage-CKRV2iq2.js → DocsPage-Gc_BCdqC.js} +269 -143
  5. package/dist/assets/EditorApp-D9bJvtf7.js +11338 -0
  6. package/dist/assets/{EditorApp-CnC2k4cW.css → EditorApp-DG1-oUSV.css} +459 -87
  7. package/dist/assets/{EmbedViewer-DBlzmQ5i.js → EmbedViewer-CEO8XbV8.js} +2 -4
  8. package/dist/assets/LandingPage-CdCuEOdC.js +451 -0
  9. package/dist/assets/PricingPage-BSrxu6d7.js +232 -0
  10. package/dist/assets/{SettingsPage-BqCh9JcC.js → SettingsPage-FUCSIRq6.js} +129 -5
  11. package/dist/assets/{evalWorker-Ql-aKwLA.js → evalWorker-KoR0SNKq.js} +6770 -2914
  12. package/dist/assets/{index-2hfs_ub0.css → index-CyVd1D4D.css} +227 -53
  13. package/dist/assets/{Viewport-CoB46f5R.js → index-wTEK39at.js} +31385 -6439
  14. package/dist/assets/{javascript-DCxGoE5Y.js → javascript-DAl8Gmyo.js} +1 -1
  15. package/dist/assets/{manifold-CqNMHHKO.js → manifold-B1sGWdYk.js} +4 -3
  16. package/dist/assets/{manifold-Cce9wRFz.js → manifold-D7o0N50J.js} +1 -1
  17. package/dist/assets/{manifold-D6BeHIOo.js → manifold-G5sBaXzi.js} +1 -1
  18. package/dist/assets/{reportWorker-sFEFonXf.js → reportWorker-DYcRHhv9.js} +6798 -3341
  19. package/dist/assets/{vendor-react-Dt7-aaJH.js → vendor-react-CG3i_wp0.js} +65 -8
  20. package/dist/docs-raw/generated/assembly.md +691 -112
  21. package/dist/docs-raw/generated/concepts.md +1225 -1400
  22. package/dist/docs-raw/generated/core.md +464 -1412
  23. package/dist/docs-raw/generated/curves.md +593 -117
  24. package/dist/docs-raw/generated/lib.md +38 -748
  25. package/dist/docs-raw/generated/output.md +139 -245
  26. package/dist/docs-raw/generated/sheet-metal.md +473 -21
  27. package/dist/docs-raw/generated/sketch.md +553 -349
  28. package/dist/docs-raw/generated/viewport.md +345 -303
  29. package/dist/docs-raw/generated/wood.md +104 -0
  30. package/dist/index.html +2 -2
  31. package/dist/sitemap.xml +6 -6
  32. package/dist-cli/chunk-PZ5AY32C.js +10 -0
  33. package/dist-cli/chunk-PZ5AY32C.js.map +1 -0
  34. package/dist-cli/forgecad.js +9435 -5407
  35. package/dist-cli/forgecad.js.map +1 -0
  36. package/dist-cli/solver-FV7TJZGI.js +365 -0
  37. package/dist-cli/solver-FV7TJZGI.js.map +1 -0
  38. package/dist-skill/CONTEXT.md +3186 -7145
  39. package/dist-skill/SKILL-dev.md +21 -63
  40. package/dist-skill/SKILL.md +12 -56
  41. package/dist-skill/docs/API/core/concepts.md +16 -98
  42. package/dist-skill/docs/CLI/export.md +91 -0
  43. package/dist-skill/docs/CLI/projects.md +107 -0
  44. package/dist-skill/docs/CLI/studio_publishing.md +52 -0
  45. package/dist-skill/docs/CLI/validation.md +66 -0
  46. package/dist-skill/docs/generated/assembly.md +691 -112
  47. package/dist-skill/docs/generated/core.md +464 -1412
  48. package/dist-skill/docs/generated/curves.md +593 -117
  49. package/dist-skill/docs/generated/lib.md +38 -748
  50. package/dist-skill/docs/generated/output.md +139 -245
  51. package/dist-skill/docs/generated/sheet-metal.md +473 -21
  52. package/dist-skill/docs/generated/sketch.md +553 -349
  53. package/dist-skill/docs/generated/viewport.md +345 -303
  54. package/dist-skill/docs/generated/wood.md +104 -0
  55. package/dist-skill/docs/guides/coordinate-system.md +11 -17
  56. package/dist-skill/docs/guides/geometry-conventions.md +13 -70
  57. package/dist-skill/docs/guides/modeling-recipes.md +22 -195
  58. package/dist-skill/docs/guides/positioning.md +88 -147
  59. package/dist-skill/docs-dev/API/core/concepts.md +51 -0
  60. package/dist-skill/docs-dev/API/core/sdf-advanced.md +92 -0
  61. package/dist-skill/docs-dev/API/core/sdf-primitives.md +58 -0
  62. package/dist-skill/docs-dev/API/core/sdf-workflow.md +42 -0
  63. package/dist-skill/docs-dev/CLI/export.md +91 -0
  64. package/dist-skill/docs-dev/CLI/projects.md +107 -0
  65. package/dist-skill/docs-dev/CLI/studio_publishing.md +52 -0
  66. package/dist-skill/docs-dev/CLI/validation.md +66 -0
  67. package/dist-skill/{docs → docs-dev}/blueprint-first.md +5 -0
  68. package/dist-skill/{docs → docs-dev}/coding-best-practices.md +6 -8
  69. package/dist-skill/{docs → docs-dev}/coding.md +1 -3
  70. package/dist-skill/docs-dev/generated/assembly.md +771 -0
  71. package/dist-skill/docs-dev/generated/core.md +775 -0
  72. package/dist-skill/docs-dev/generated/curves.md +688 -0
  73. package/dist-skill/docs-dev/generated/lib.md +50 -0
  74. package/dist-skill/docs-dev/generated/output.md +234 -0
  75. package/dist-skill/docs-dev/generated/sheet-metal.md +506 -0
  76. package/dist-skill/docs-dev/generated/sketch.md +801 -0
  77. package/dist-skill/docs-dev/generated/viewport.md +486 -0
  78. package/dist-skill/docs-dev/generated/wood.md +104 -0
  79. package/dist-skill/docs-dev/guides/coordinate-system.md +46 -0
  80. package/dist-skill/docs-dev/guides/geometry-conventions.md +52 -0
  81. package/dist-skill/docs-dev/guides/modeling-recipes.md +77 -0
  82. package/dist-skill/docs-dev/guides/positioning.md +151 -0
  83. package/dist-skill/{docs → docs-dev}/guides/skill-maintenance.md +21 -10
  84. package/dist-skill/{docs → docs-dev}/internals/compiler.md +5 -6
  85. package/dist-skill/{docs → docs-dev}/internals/constraint-solver-quality.md +0 -1
  86. package/dist-skill/{docs → docs-dev}/internals/constraint-solver.md +0 -1
  87. package/dist-skill/{docs → docs-dev}/internals/sketch-2d-pipeline.md +2 -3
  88. package/examples/api/attachTo-basics.forge.js +5 -5
  89. package/examples/api/boolean-operations.forge.js +3 -3
  90. package/examples/api/bounding-box-visualizer.forge.js +2 -2
  91. package/examples/api/clone-duplicate.forge.js +1 -1
  92. package/examples/api/colors-union-vs-array.forge.js +6 -6
  93. package/examples/api/connector-assembly.forge.js +4 -4
  94. package/examples/api/connector-basics.forge.js +2 -2
  95. package/examples/api/extrude-options.forge.js +4 -10
  96. package/examples/api/feature-created-faces.forge.js +6 -10
  97. package/examples/api/fillet-showcase.forge.js +1 -1
  98. package/examples/api/folded-service-panel-cover.forge.js +2 -2
  99. package/examples/api/group-test.forge.js +1 -1
  100. package/examples/api/group-vs-union.forge.js +1 -1
  101. package/examples/api/highlight-debug.forge.js +4 -0
  102. package/examples/api/js-module-pillars.js +1 -1
  103. package/examples/api/js-module-scene.js +2 -2
  104. package/examples/api/mesh-import-slats.forge.js +1 -1
  105. package/examples/api/pointAlong-orientation.forge.js +1 -1
  106. package/examples/api/profile-2020-b-slot6.forge.js +0 -1
  107. package/examples/api/route-perimeter-flange.forge.js +1 -1
  108. package/examples/api/sdf-rover-demo.forge.js +10 -10
  109. package/examples/api/sketch-on-face-demo.forge.js +2 -2
  110. package/examples/api/sketch-regions.forge.js +4 -4
  111. package/examples/api/transition-curves.forge.js +1 -1
  112. package/examples/api/variable-sweep-pure-sdf-test.forge.js +162 -0
  113. package/examples/api/variable-sweep-test.forge.js +2 -2
  114. package/examples/api/wood-joinery.forge.js +60 -0
  115. package/examples/compiler-corpus/enclosure-shell-cuts.forge.js +3 -3
  116. package/examples/compiler-corpus/fastener-plate-variants.forge.js +2 -2
  117. package/examples/experiments/drone-arm.forge.js +53 -0
  118. package/examples/furniture/adjustable-table.forge.js +2 -2
  119. package/examples/furniture/bathroom.forge.js +11 -11
  120. package/examples/furniture/chair.forge.js +1 -1
  121. package/examples/generative/crystal-growth.forge.js +2 -2
  122. package/examples/generative/frost-spires.forge.js +3 -3
  123. package/examples/generative/golden-spiral-tower.forge.js +3 -3
  124. package/examples/mechanical/3d-printer.forge.js +28 -28
  125. package/examples/mechanical/5-finger-robot-hand.forge.js +15 -15
  126. package/examples/mechanical/airplane-propeller.forge.js +2 -2
  127. package/examples/mechanical/fillet-enclosure.forge.js +1 -1
  128. package/examples/mechanical/headphone-hanger-v2.forge.js +2 -2
  129. package/examples/mechanical/robot_hand.forge.js +15 -15
  130. package/examples/mechanical/robot_hand_2.forge.js +9 -9
  131. package/examples/products/bottle.forge.js +1 -1
  132. package/examples/products/chess-set.forge.js +19 -19
  133. package/examples/products/classical-piano.forge.js +11 -11
  134. package/examples/products/clock.forge.js +12 -12
  135. package/examples/products/iphone.forge.js +8 -8
  136. package/examples/products/laptop.forge.js +15 -15
  137. package/examples/products/liquid-soap-dispenser.forge.js +18 -18
  138. package/examples/products/origami-fish.forge.js +8 -6
  139. package/examples/products/spiderman-cake.forge.js +4 -4
  140. package/examples/toolbox/bolted-joint.forge.js +2 -2
  141. package/package.json +7 -4
  142. package/dist/assets/EditorApp-B-vQvgam.js +0 -9888
  143. package/dist/assets/LandingPage-C5n9hDXI.js +0 -322
  144. package/dist/assets/PublishedModelPage-Dt7PCVBj.js +0 -146
  145. package/dist/assets/__vite-browser-external-CURh0WXD.js +0 -8
  146. package/dist/assets/deserializeRunResult-BLAFoiE0.js +0 -19365
  147. package/dist/assets/index-1CYp3zUp.js +0 -1455
  148. package/dist/docs-raw/CLI.md +0 -865
  149. package/dist-skill/docs/API/API.md +0 -1666
  150. package/dist-skill/docs/API/README.md +0 -37
  151. package/dist-skill/docs/API/assembly/assembly.md +0 -617
  152. package/dist-skill/docs/API/core/edge-queries.md +0 -130
  153. package/dist-skill/docs/API/core/parameters.md +0 -122
  154. package/dist-skill/docs/API/core/reserved-terms.md +0 -137
  155. package/dist-skill/docs/API/core/sdf.md +0 -326
  156. package/dist-skill/docs/API/core/skill-cli.md +0 -194
  157. package/dist-skill/docs/API/core/skill-guide.md +0 -205
  158. package/dist-skill/docs/API/core/specs.md +0 -186
  159. package/dist-skill/docs/API/core/topology.md +0 -372
  160. package/dist-skill/docs/API/entities.md +0 -268
  161. package/dist-skill/docs/API/output/bom.md +0 -58
  162. package/dist-skill/docs/API/output/brep-export.md +0 -87
  163. package/dist-skill/docs/API/output/dimensions.md +0 -67
  164. package/dist-skill/docs/API/output/export.md +0 -110
  165. package/dist-skill/docs/API/output/gcode.md +0 -195
  166. package/dist-skill/docs/API/runtime/viewport.md +0 -420
  167. package/dist-skill/docs/API/sheet-metal/sheet-metal.md +0 -185
  168. package/dist-skill/docs/API/sketch/anchor.md +0 -37
  169. package/dist-skill/docs/API/sketch/booleans.md +0 -91
  170. package/dist-skill/docs/API/sketch/core.md +0 -73
  171. package/dist-skill/docs/API/sketch/extrude.md +0 -62
  172. package/dist-skill/docs/API/sketch/on-face.md +0 -104
  173. package/dist-skill/docs/API/sketch/operations.md +0 -78
  174. package/dist-skill/docs/API/sketch/path.md +0 -75
  175. package/dist-skill/docs/API/sketch/primitives.md +0 -146
  176. package/dist-skill/docs/API/sketch/regions.md +0 -80
  177. package/dist-skill/docs/API/sketch/text.md +0 -108
  178. package/dist-skill/docs/API/sketch/transforms.md +0 -65
  179. package/dist-skill/docs/API/toolbox/fasteners.md +0 -129
  180. package/dist-skill/docs/CLI.md +0 -865
  181. package/dist-skill/docs/INDEX.md +0 -94
  182. package/dist-skill/docs/RELEASING.md +0 -55
  183. package/dist-skill/docs/cli-monetization.md +0 -111
  184. package/dist-skill/docs/deployment.md +0 -281
  185. package/dist-skill/docs/generated/concepts.md +0 -2112
  186. package/dist-skill/docs/internals/shape-from-slices.md +0 -152
  187. package/dist-skill/docs/platform/admin.md +0 -45
  188. package/dist-skill/docs/platform/architecture.md +0 -79
  189. package/dist-skill/docs/platform/auth.md +0 -110
  190. package/dist-skill/docs/platform/email.md +0 -67
  191. package/dist-skill/docs/platform/projects.md +0 -111
  192. package/dist-skill/docs/platform/sharing.md +0 -90
  193. package/dist-skill/docs/runbook.md +0 -345
@@ -6,10 +6,10 @@ Every public API function belongs to one of 16 fundamental concepts. This docume
6
6
 
7
7
  ## Concepts
8
8
 
9
- - **[C1: Primitive Construction](#c1-primitive-construction)** — Create geometry from parameters — no input geometry required. *(19 functions)*
10
- - **[C2: Boolean Combination](#c2-boolean-combination)** — Combine same-dimension geometry using CSG set operations. *(3 functions)*
9
+ - **[C1: Primitive Construction](#c1-primitive-construction)** — Create geometry from parameters — no input geometry required. *(20 functions)*
10
+ - **[C2: Boolean Combination](#c2-boolean-combination)** — Combine same-dimension geometry using CSG set operations. *(6 functions)*
11
11
  - **[C3: Rigid Transform](#c3-rigid-transform)** — Reposition or reorient geometry without changing its shape. *(3 functions)*
12
- - **[C4: Dimensional Promotion](#c4-dimensional-promotion)** — Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep). *(12 functions)*
12
+ - **[C4: Dimensional Promotion](#c4-dimensional-promotion)** — Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep). *(10 functions)*
13
13
  - **[C5: Topology Query](#c5-topology-query)** — Select or inspect named faces and edges on a shape. *(3 functions)*
14
14
  - **[C6: Edge Feature](#c6-edge-feature)** — Modify edges of a solid — fillets, chamfers, draft, offset. *(7 functions)*
15
15
  - **[C7: Pattern Replication](#c7-pattern-replication)** — Duplicate geometry in regular arrangements (linear, circular, mirror). *(6 functions)*
@@ -19,7 +19,7 @@ Every public API function belongs to one of 16 fundamental concepts. This docume
19
19
  - **[C11: Parameterization & UI](#c11-parameterization-ui)** — Declare user-facing controls that drive model geometry. *(6 functions)*
20
20
  - **[C12: Dimensional Demotion](#c12-dimensional-demotion)** — Extract 2D geometry from a 3D solid (section, projection). *(3 functions)*
21
21
  - **[C13: Export & Output](#c13-export-output)** — Convert geometry to external formats (STL, 3MF, SVG, DXF, G-code, PDF). *(5 functions)*
22
- - **[C14: Visual & Debugging](#c14-visual-debugging)** — Control viewport appearance and debugging aids. *(5 functions)*
22
+ - **[C14: Visual & Debugging](#c14-visual-debugging)** — Control viewport appearance and debugging aids. *(6 functions)*
23
23
  - **[C15: Import & Composition](#c15-import-composition)** — Bring external geometry or other ForgeCAD modules into the current script. *(1 functions)*
24
24
  - **[C16: Part Library](#c16-part-library)** — Pre-built parametric parts accessible via `lib.*`. *(0 functions)*
25
25
 
@@ -29,223 +29,287 @@ Every public API function belongs to one of 16 fundamental concepts. This docume
29
29
 
30
30
  Create geometry from parameters — no input geometry required.
31
31
 
32
- #### `arcBridgeBetweenRects()`
32
+ #### `circle2d()` — Create a 2D circle centered at the origin.
33
33
 
34
- ```ts
35
- arcBridgeBetweenRects(rectA: RectAreaArg, rectB: RectAreaArg, segments?: number): Shape
36
- ```
34
+ **Details**
37
35
 
38
- Build an arc bridge between two rectangular areas.
36
+ Omit `segments` for a smooth (auto-tessellated) circle. Pass an integer to get a regular polygon approximation — e.g. `6` for a hexagon, `8` for an octagon.
39
37
 
40
- #### `circle2d()`
38
+ **Example**
41
39
 
42
40
  ```ts
43
- circle2d(radius: number, segments?: number): Sketch
41
+ circle2d(25).extrude(10); // smooth cylinder
42
+ circle2d(25, 6).extrude(10); // hexagonal prism
44
43
  ```
45
44
 
46
- Create a 2D circle centered at the origin. Use segments for lower-poly approximations.
45
+ `circle2d(radius: number, segments?: number): Sketch`
47
46
 
48
- #### `ellipse()`
47
+ #### `ellipse()` — Create a 2D ellipse centered at the origin.
48
+
49
+ **Example**
49
50
 
50
51
  ```ts
51
- ellipse(rx: number, ry: number, segments?: number): Sketch
52
+ ellipse(30, 15).extrude(5);
53
+ ellipse(30, 15, 32).extrude(5); // lower-resolution approximation
52
54
  ```
53
55
 
54
- Create a 2D ellipse centered at the origin with the given X and Y radii.
56
+ `ellipse(rx: number, ry: number, segments?: number): Sketch`
55
57
 
56
- #### `loadFont()`
58
+ #### `loadFont()` — Pre-load and cache a font for use with `text2d()`.
57
59
 
58
- ```ts
59
- loadFont(source: string | ArrayBuffer, cacheKey?: string): opentype$1.Font
60
- ```
60
+ **Details**
61
+
62
+ Fonts are cached by their source string (or `cacheKey` for `ArrayBuffer` sources), so repeated calls with the same path are free. Pre-loading is useful when you call `text2d()` many times with the same font — it avoids repeated disk reads.
61
63
 
62
- Load and cache a font. - A built-in font name: `'sans-serif'` or `'inter'` (works everywhere) - A file path to a TTF/OTF/WOFF file (CLI/Node only) - An ArrayBuffer of font data (works everywhere)
64
+ Built-in font names that work everywhere (browser + CLI): - `'sans-serif'` or `'inter'` bundled Inter Regular
63
65
 
64
- #### `ngon()`
66
+ **Example**
65
67
 
66
68
  ```ts
67
- ngon(sides: number, radius: number): Sketch
69
+ const font = loadFont('/path/to/Arial Bold.ttf');
70
+ text2d('Title', { size: 12, font }).extrude(1.5);
71
+ text2d('Subtitle', { size: 8, font }).extrude(1);
68
72
  ```
69
73
 
70
- Create a regular polygon (equilateral triangle, hexagon, etc.) inscribed in a circle of the given radius.
74
+ `loadFont(source: string | ArrayBuffer, cacheKey?: string): opentype$1.Font`
71
75
 
72
- #### `path()`
76
+ #### `ngon()` — Create a regular polygon inscribed in a circle of the given radius.
73
77
 
74
- ```ts
75
- path(): PathBuilder
76
- ```
78
+ **Details**
77
79
 
78
- Create a path builder for constructing 2D outlines.
80
+ `radius` is the center-to-vertex (circumradius) distance. Use `sides` of `3` for a triangle, `6` for a hexagon, etc. The first vertex is at the top (−90° from +X).
79
81
 
80
- #### `polar()`
82
+ **Example**
81
83
 
82
84
  ```ts
83
- polar(length: number, angleDeg: number, from?: [ number, number ]): [ number, number ]
85
+ ngon(6, 20).extrude(10); // hexagonal prism, circumradius 20
84
86
  ```
85
87
 
86
- Compute a point by moving a given distance at a given angle from a start point. Angle is in degrees, measured CCW from the +X axis (standard math convention). Returns `[x, y]`. ```js polar(10, 45) // [7.07, 7.07] — from origin polar(10, 45, [5, 5]) // [12.07, 12.07] — from (5,5) ```
88
+ `ngon(sides: number, radius: number): Sketch`
87
89
 
88
- #### `polygon()`
90
+ #### `path()` — Create a new `PathBuilder` for tracing a 2D outline point by point.
89
91
 
90
- ```ts
91
- polygon(points: ([ number, number ] | Point2D)[]): Sketch
92
- ```
92
+ **Details**
93
93
 
94
- Create a 2D polygon from an array of [x, y] points or Point2D objects. Winding is normalized to CCW.
94
+ `PathBuilder` is a fluent API for constructing 2D profiles using a mix of line segments, arcs, bezier curves, and splines. Always start with `.moveTo(x, y)` to set the starting point. Call `.close()` to get a filled `Sketch`, or `.stroke(width)` to thicken an open polyline into a solid profile.
95
95
 
96
- #### `polygonVertices()`
96
+ Edge labels can be assigned with `.label('name')` after any segment — they propagate through extrusion, revolve, loft, and sweep into named faces on the resulting `Shape`.
97
+
98
+ **Example**
97
99
 
98
100
  ```ts
99
- polygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[]
101
+ // Closed triangle
102
+ const triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();
103
+
104
+ // L-shaped bracket as a stroke
105
+ const bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);
106
+
107
+ // Labeled edges for downstream face references
108
+ const slot = path()
109
+ .moveTo(0, 0)
110
+ .lineTo(30, 0).label('bottom')
111
+ .lineTo(30, 10)
112
+ .lineTo(0, 10).label('top')
113
+ .close();
100
114
  ```
101
115
 
102
- Compute the vertex positions of a regular polygon. Default orientation places the first vertex at the top (90 degrees), matching the convention used by `ngon()`. Eliminates manual Math.sqrt(3) for triangles, pentagon vertex math, etc: ```js // Before — manual equilateral triangle const v1 = [center.x - r/2, center.y + r * Math.sqrt(3)/2]; const v2 = [center.x - r/2, center.y - r * Math.sqrt(3)/2]; const v3 = [center.x + r, center.y]; // After — declarative const [v1, v2, v3] = polygonVertices(3, r); ```
116
+ `path(): PathBuilder`
103
117
 
104
- <details><summary><code>PolygonVerticesOptions</code></summary>
118
+ #### `polygon()` — Create a 2D polygon from an array of `[x, y]` points or `Point2D` objects.
105
119
 
106
- ```ts
107
- interface PolygonVerticesOptions {
108
- /** Angle of the first vertex in degrees (default: 90 = top). */
109
- startDeg?: number;
110
- /** Center X coordinate (default: 0). */
111
- centerX?: number;
112
- /** Center Y coordinate (default: 0). */
113
- centerY?: number;
114
- }
115
- ```
120
+ **Details**
116
121
 
117
- </details>
122
+ Winding order is normalized automatically — clockwise (CW) input is silently reversed to CCW before being passed to the geometry kernel.
118
123
 
119
- <details><summary><code>LayoutPoint</code></summary>
124
+ **Example**
120
125
 
121
126
  ```ts
122
- interface LayoutPoint {
123
- x: number;
124
- y: number;
125
- }
127
+ polygon([[0, 0], [50, 0], [25, 40]]).extrude(5); // triangle
126
128
  ```
127
129
 
128
- </details>
130
+ `polygon(points: ([ number, number ] | Point2D)[]): Sketch`
129
131
 
130
- #### `rect()`
132
+ #### `polygonVertices()` — Compute the vertex positions of a regular polygon.
131
133
 
132
- ```ts
133
- rect(width: number, height: number, center?: boolean): Sketch
134
- ```
134
+ Default orientation places the first vertex at the top (90 degrees), matching the convention used by `ngon()`.
135
135
 
136
- Create a 2D rectangle. When center is true, the origin is at the rectangle center; otherwise at the bottom-left corner.
136
+ Eliminates manual Math.sqrt(3) for triangles, pentagon vertex math, etc:
137
137
 
138
- #### `arcSlot()`
138
+ ```js
139
+ // Before — manual equilateral triangle
140
+ const v1 = [center.x - r/2, center.y + r * Math.sqrt(3)/2];
141
+ const v2 = [center.x - r/2, center.y - r * Math.sqrt(3)/2];
142
+ const v3 = [center.x + r, center.y];
139
143
 
140
- ```ts
141
- arcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch
144
+ // After — declarative
145
+ const [v1, v2, v3] = polygonVertices(3, r);
142
146
  ```
143
147
 
144
- Create an arc-shaped slot (banana/annular sector) centered at the origin. The slot is symmetric about the +X axis. ```js arcSlot(135, 74, 40) // pitch R135, 74° sweep, 40mm wide ```
148
+ `polygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[]`
145
149
 
146
- #### `routePerimeter()`
150
+ **`PolygonVerticesOptions`**
151
+ - `startDeg?: number` — Angle of the first vertex in degrees (default: 90 = top).
152
+ - `centerX?: number` — Center X coordinate (default: 0).
153
+ - `centerY?: number` — Center Y coordinate (default: 0).
147
154
 
148
- ```ts
149
- routePerimeter(steps: PerimeterStep[]): Sketch
150
- ```
155
+ `LayoutPoint`: `{ x: number, y: number }`
151
156
 
152
- Route a smooth closed perimeter around a sequence of construction circles, connected by tangent fillet arcs. Steps must alternate: circle, fillet, circle, fillet, ... The sequence wraps — the last fillet connects back to the first circle. ```js const outline = routePerimeter([ { center: [0, 0], radius: 45 }, { fillet: 5 }, { center: polar(60, 60), radius: 18 }, { fillet: 17 }, { center: polar(60, 120), radius: 18 }, { fillet: 5 }, ]) ```
157
+ #### `rect()` Create a 2D rectangle centered at the origin.
153
158
 
154
- #### `roundedRect()`
159
+ **Example**
155
160
 
156
161
  ```ts
157
- roundedRect(width: number, height: number, radius: number, center?: boolean): Sketch
162
+ rect(40, 20).extrude(5);
158
163
  ```
159
164
 
160
- Create a 2D rectangle with rounded corners. The radius is clamped to fit within the dimensions.
165
+ `rect(width: number, height: number): Sketch`
161
166
 
162
- #### `slot()`
167
+ #### `arcSlot()` — Create an arc-shaped slot (banana / annular sector) centered at the origin.
163
168
 
164
- ```ts
165
- slot(length: number, width: number): Sketch
166
- ```
169
+ **Details**
167
170
 
168
- Create a slot (stadium/discorectangle) a rectangle with semicircular ends, centered at origin.
171
+ The slot is symmetric about the +X axis. The two ends are closed with semicircular caps. `pitchRadius` is the distance from the origin to the centerline of the slot, and `thickness` is the radial width of the slot.
169
172
 
170
- #### `spline2d()`
173
+ **Example**
171
174
 
172
175
  ```ts
173
- spline2d(points: Vec2[], options?: Spline2DOptions): Sketch
176
+ arcSlot(135, 74, 40).extrude(5); // pitch R135, 74° sweep, 40mm wide
174
177
  ```
175
178
 
176
- Build a smooth Catmull-Rom spline sketch from 2D control points. A closed spline (default) returns a filled profile. An open spline requires a strokeWidth option to produce a solid sketch. Use tension (0..1, default 0.5) to control curve tightness.
179
+ `arcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch`
177
180
 
178
- <details><summary><code>Spline2DOptions</code></summary>
181
+ #### `roundedRect()` — Create a 2D rectangle with rounded corners, centered at the origin.
179
182
 
180
- ```ts
181
- interface Spline2DOptions {
182
- /** Closed loop (default true). */
183
- closed?: boolean;
184
- /** Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. */
185
- tension?: number;
186
- /** Samples per segment (minimum 3). Default 16. */
187
- samplesPerSegment?: number;
188
- /** For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. */
189
- strokeWidth?: number;
190
- /** Stroke join for open splines. Default 'Round'. */
191
- join?: "Round" | "Square";
192
- }
193
- ```
183
+ **Details**
194
184
 
195
- </details>
185
+ The corner radius is automatically clamped to `min(width/2, height/2)` so it can never exceed the shape dimensions.
196
186
 
197
- #### `star()`
187
+ **Example**
198
188
 
199
189
  ```ts
200
- star(points: number, outerR: number, innerR: number): Sketch
190
+ roundedRect(60, 30, 5).extrude(3);
201
191
  ```
202
192
 
203
- Create a star shape with alternating outer and inner radii.
193
+ `roundedRect(width: number, height: number, radius: number): Sketch`
194
+
195
+ #### `slot()` — Create a slot (oblong / stadium shape) — a rectangle with semicircular ends, centered at the origin.
204
196
 
205
- #### `stroke()`
197
+ **Example**
206
198
 
207
199
  ```ts
208
- stroke(points: [ number, number ][], width: number, join?: "Round" | "Square"): Sketch
200
+ slot(40, 10).extrude(3); // 40mm long, 10mm wide slot
209
201
  ```
210
202
 
211
- Create a stroked polyline sketch from an array of 2D points.
203
+ `slot(length: number, width: number): Sketch`
204
+
205
+ #### `spline2d()` — Build a smooth Catmull-Rom spline sketch from 2D control points.
212
206
 
213
- #### `text2d()`
207
+ A closed spline (default) returns a filled profile. An open spline requires a strokeWidth option to produce a solid sketch. Use tension (0..1, default 0.5) to control curve tightness.
208
+
209
+ `spline2d(points: Vec2[], options?: Spline2DOptions): Sketch`
210
+
211
+ **`Spline2DOptions`**
212
+
213
+ | Option | Type | Description |
214
+ |--------|------|-------------|
215
+ | `closed?` | `boolean` | Closed loop (default true). |
216
+ | `tension?` | `number` | Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. |
217
+ | `samplesPerSegment?` | `number` | Samples per segment (minimum 3). Default 16. |
218
+ | `strokeWidth?` | `number` | For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. |
219
+ | `join?` | `"Round" | "Square"` | Stroke join for open splines. Default 'Round'. |
220
+
221
+ #### `star()` — Create a star shape with alternating outer and inner radii.
222
+
223
+ **Example**
214
224
 
215
225
  ```ts
216
- text2d(content: string, options?: TextOptions): Sketch
226
+ star(5, 30, 12).extrude(4); // five-pointed star
217
227
  ```
218
228
 
219
- Build a 2-D filled Sketch from a text string. The Sketch origin is at the left end of the text baseline by default (see `align` and `baseline` options to adjust placement). Text is rendered using the bundled Inter font by default, or any TTF/OTF/WOFF font you provide. // Extruded nameplate text2d('FORGE CAD', { size: 8 }).extrude(1.2) // Centered label on the XY plane text2d('V 2.0', { size: 6, align: 'center', baseline: 'center' })
229
+ `star(points: number, outerR: number, innerR: number): Sketch`
230
+
231
+ #### `stroke()` — Create a stroked polyline sketch from an array of 2D points.
232
+
233
+ `stroke(points: [ number, number ][], width: number, join?: "Round" | "Square"): Sketch`
234
+
235
+ #### `text2d()` — Build a filled 2D Sketch from a text string.
220
236
 
221
- <details><summary><code>TextOptions</code></summary>
237
+ **Details**
238
+
239
+ The Sketch origin is at the left end of the text baseline by default. Use `align` and `baseline` options to adjust placement. Text is rendered using the bundled Inter font by default, or any TTF/OTF/WOFF font you provide.
240
+
241
+ Alignment reference table:
242
+
243
+ | `align` | `baseline` | Origin | |------------|--------------|-------------------------------------| | `'left'` | `'baseline'` | Bottom-left of first char (default) | | `'center'` | `'center'` | Dead center of text block | | `'right'` | `'top'` | Top-right corner |
244
+
245
+ **Example**
222
246
 
223
247
  ```ts
224
- interface TextOptions {
225
- /** Cap height of the text in model units. All other dimensions (stroke weight, spacing) scale proportionally. */
226
- size?: number;
227
- /** Extra space between characters in model units. Negative values tighten the tracking. */
228
- letterSpacing?: number;
229
- /** Horizontal alignment relative to x = 0. - `'left'` — left edge at x = 0 (default) - `'center'` — centred on x = 0 - `'right'` — right edge at x = 0 */
230
- align?: "left" | "center" | "right";
231
- /** Vertical alignment relative to y = 0. - `'baseline'` y = 0 is the text baseline (bottom of capital letters) - `'center'` — y = 0 is the vertical midpoint of the cap height - `'top'` — y = 0 is the top of capital letters */
232
- baseline?: "baseline" | "center" | "top";
233
- /** Font to use for text rendering. - `'sans-serif'` or `'inter'` — bundled Inter font (works everywhere, including browser) - **file path** — path to a TTF, OTF, or WOFF font file (CLI/Node only) - **Font object** — a previously loaded opentype.js Font (from `loadFont()`) - **omitted** — uses the bundled Inter font (same as `'sans-serif'`) text2d('Hello World', { size: 10 }) // default Inter text2d('Custom Font', { size: 10, font: '/path/to/font.ttf' }) */
234
- font?: string | opentype$1.Font;
235
- /** Bezier flattening tolerance in model units. Smaller = more polygon segments = smoother curves. */
236
- flattenTolerance?: number;
237
- }
248
+ // Extruded nameplate
249
+ text2d('FORGE CAD', { size: 8 }).extrude(1.2);
250
+
251
+ // Centered label on the XY plane
252
+ text2d('V 2.0', { size: 6, align: 'center', baseline: 'center' });
253
+
254
+ // Engraved text cut into the top face of a box
255
+ const label = text2d('REV A', { size: 5, align: 'center', baseline: 'center' });
256
+ plate.subtract(label.onFace(plate, 'top', { protrude: -0.5 }).extrude(1));
257
+
258
+ // Custom TTF font
259
+ text2d('Hello', { size: 10, font: '/path/to/Arial.ttf' }).extrude(1);
260
+
261
+ // Pre-loaded font for reuse
262
+ const font = loadFont('/path/to/Arial Bold.ttf');
263
+ text2d('Title', { size: 12, font }).extrude(1.5);
238
264
  ```
239
265
 
240
- </details>
266
+ `text2d(content: string, options?: TextOptions): Sketch`
267
+
268
+ **`TextOptions`**
269
+
270
+ | Option | Type | Description |
271
+ |--------|------|-------------|
272
+ | `size?` | `number` | Cap height of the text in model units. All other dimensions (stroke weight, spacing) scale proportionally. |
273
+ | `letterSpacing?` | `number` | Extra space between characters in model units. Negative values tighten the tracking. |
274
+ | `align?` | `"left" | "center" | "right"` | Horizontal alignment relative to x = 0. - `'left'` — left edge at x = 0 (default) - `'center'` — centred on x = 0 - `'right'` — right edge at x = 0 |
275
+ | `baseline?` | `"baseline" | "center" | "top"` | Vertical alignment relative to y = 0. - `'baseline'` — y = 0 is the text baseline (bottom of capital letters) - `'center'` — y = 0 is the vertical midpoint of the cap height - `'top'` — y = 0 is the top of capital letters |
276
+ | `font?` | `string | opentype$1.Font` | Font to use for text rendering. - `'sans-serif'` or `'inter'` — bundled Inter font (works everywhere, including browser) - **file path** — path to a TTF, OTF, or WOFF font file (CLI/Node only) - **Font object** — a previously loaded opentype.js Font (from `loadFont()`) - **omitted** — uses the bundled Inter font (same as `'sans-serif'`) text2d('Hello World', { size: 10 }) // default Inter text2d('Custom Font', { size: 10, font: '/path/to/font.ttf' }) |
277
+ | `flattenTolerance?` | `number` | Bezier flattening tolerance in model units. Smaller = more polygon segments = smoother curves. |
278
+
279
+ #### `textWidth()` — Measure the rendered advance width of a string without creating any geometry.
280
+
281
+ **Details**
282
+
283
+ Uses the same font metrics as `text2d()`. Useful for computing layout dimensions before building the actual sketch — e.g. sizing a plate to fit a label.
241
284
 
242
- #### `textWidth()`
285
+ **Example**
243
286
 
244
287
  ```ts
245
- textWidth(content: string, options?: Pick<TextOptions, "size" | "letterSpacing" | "font">): number
288
+ const w = textWidth('SERIAL: 001', { size: 6 });
289
+ const plate = box(w + 10, 12, 2);
246
290
  ```
247
291
 
248
- Returns the rendered width of a string in model units (same options as text2d).
292
+ `textWidth(content: string, options?: Pick<TextOptions, "size" | "letterSpacing" | "font">): number`
293
+
294
+ #### `box()` — Create a rectangular box. Centered on XY, base at Z=0.
295
+
296
+ For named faces, build from a labeled sketch: `rect(x, y).labelEdges('s', 'e', 'n', 'w').extrude(z, { labels: { start: 'bottom', end: 'top' } })`.
297
+
298
+ `box$1(x: number, y: number, z: number): Shape`
299
+
300
+ #### `cylinder()` — Create a cylinder or cone with named faces and edges. Centered on XY, base at Z=0.
301
+
302
+ When radiusTop differs from radius, creates a tapered cone. Use the segments parameter to create regular prisms (e.g. 6 for a hexagonal prism). Returns a Shape with faces: top, bottom, side; and edges: top-rim, bottom-rim.
303
+
304
+ `cylinder$1(height: number, radius: number, radiusTop?: number, segments?: number): Shape`
305
+
306
+ #### `sphere()` — Create a sphere centered at the origin. Use segments for lower-poly approximations.
307
+
308
+ `sphere$1(radius: number, segments?: number): Shape`
309
+
310
+ #### `torus()` — Create a torus (donut shape) lying in the XY plane. Centered on all axes (origin is the ring center).
311
+
312
+ `torus$1(majorRadius: number, minorRadius: number, segments?: number): Shape`
249
313
 
250
314
  ---
251
315
 
@@ -253,399 +317,305 @@ Returns the rendered width of a string in model units (same options as text2d).
253
317
 
254
318
  Combine same-dimension geometry using CSG set operations.
255
319
 
256
- #### `difference2d()`
320
+ #### `difference2d()` — Subtract one or more 2D sketches from a base sketch.
257
321
 
258
- ```ts
259
- difference2d(...inputs: SketchOperandInput[]): Sketch
260
- ```
322
+ **Details**
261
323
 
262
- Subtract 2D sketches from a base sketch. The first sketch is the base; all others are subtracted.
324
+ The first sketch is the base; all subsequent sketches are subtracted from it. Accepts individual sketches or arrays: `difference2d(base, c1, c2)` or `difference2d([base, c1, c2])`. Uses Manifold's batch operation — faster than chaining `.subtract()` one by one.
263
325
 
264
- #### `intersection2d()`
326
+ **Example**
265
327
 
266
328
  ```ts
267
- intersection2d(...inputs: SketchOperandInput[]): Sketch
329
+ const donut = difference2d(circle2d(50), circle2d(30));
268
330
  ```
269
331
 
270
- Keep only the overlapping area of the input sketches (intersection boolean).
332
+ `difference2d(...inputs: SketchOperandInput[]): Sketch`
333
+
334
+ #### `intersection2d()` — Keep only the area where all input sketches overlap (intersection boolean).
335
+
336
+ **Details**
337
+
338
+ Accepts individual sketches or arrays: `intersection2d(a, b)` or `intersection2d([a, b, c])`. Uses Manifold's batch operation — faster than chaining `.intersect()` one by one.
271
339
 
272
- #### `union2d()`
340
+ **Example**
273
341
 
274
342
  ```ts
275
- union2d(...inputs: SketchOperandInput[]): Sketch
343
+ const lens = intersection2d(circle2d(30).translate(-10, 0), circle2d(30).translate(10, 0));
276
344
  ```
277
345
 
278
- Combine 2D sketches into a single profile (additive boolean). Accepts individual sketches or arrays.
346
+ `intersection2d(...inputs: SketchOperandInput[]): Sketch`
279
347
 
280
- ---
348
+ #### `union2d()` — Combine 2D sketches into a single profile using an additive boolean union.
281
349
 
282
- ## C3: Rigid Transform
350
+ **Details**
283
351
 
284
- Reposition or reorient geometry without changing its shape.
352
+ Accepts individual sketches or arrays: `union2d(a, b, c)` or `union2d([a, b, c])`. Uses Manifold's batch operation — faster than chaining `.add()` one by one when combining many sketches.
285
353
 
286
- #### `degrees()`
354
+ **Example**
287
355
 
288
356
  ```ts
289
- degrees(deg: number): number
357
+ const cross = union2d(rect(60, 10), rect(10, 60));
290
358
  ```
291
359
 
292
- Convert degrees to degrees (identity for readability in scripts)
360
+ `union2d(...inputs: SketchOperandInput[]): Sketch`
293
361
 
294
- #### `radians()`
362
+ #### `union()` — Combine shapes into a single solid (additive boolean).
295
363
 
296
- ```ts
297
- radians(rad: number): number
298
- ```
364
+ Accepts individual shapes, or an array of shapes. The first operand's color is preserved in the result.
299
365
 
300
- Convert radians to degrees
366
+ `union(...inputs: ShapeOperandInput[]): Shape`
301
367
 
302
- #### `composeChain()`
368
+ #### `difference()` — Subtract shapes from a base shape (subtractive boolean).
303
369
 
304
- ```ts
305
- composeChain(...steps: TransformInput[]): Transform
306
- ```
370
+ The first shape is the base; all subsequent shapes are subtracted from it. Accepts individual shapes, or an array of shapes.
307
371
 
308
- Compose transforms in chain order. Equivalent to Transform.identity().mul(a).mul(b).mul(c)...
372
+ `difference(...inputs: ShapeOperandInput[]): Shape`
309
373
 
310
- ---
374
+ #### `intersection()` — Keep only the overlapping volume of the input shapes (intersection boolean).
311
375
 
312
- ## C4: Dimensional Promotion
376
+ Requires at least two shapes. Accepts individual shapes, or an array.
313
377
 
314
- Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep).
378
+ `intersection(...inputs: ShapeOperandInput[]): Shape`
315
379
 
316
- #### `connectEdges()`
380
+ ---
317
381
 
318
- ```ts
319
- connectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape
320
- ```
382
+ ## C3: Rigid Transform
321
383
 
322
- <details><summary><code>EdgeSegment</code></summary>
384
+ Reposition or reorient geometry without changing its shape.
323
385
 
324
- ```ts
325
- interface EdgeSegment {
326
- /** Stable index within the extraction (deterministic for a given mesh). */
327
- index: number;
328
- start: Vec3;
329
- end: Vec3;
330
- midpoint: Vec3;
331
- /** Normalized direction from start → end. */
332
- direction: Vec3;
333
- length: number;
334
- /** Dihedral angle in degrees (0 = coplanar, 180 = knife edge). */
335
- dihedralAngle: number;
336
- /** true = outside corner (convex), false = inside corner (concave). */
337
- convex: boolean;
338
- /** Normal of first adjacent face. */
339
- normalA: Vec3;
340
- /** Normal of second adjacent face (same as normalA for boundary edges). */
341
- normalB: Vec3;
342
- /** true if this is a boundary (unmatched) edge — unusual for closed solids. */
343
- boundary: boolean;
344
- }
345
- ```
386
+ #### `degrees()` — Identity function that returns degrees unchanged.
346
387
 
347
- </details>
388
+ Use for clarity when the unit of an angle value would otherwise be ambiguous — e.g. `param("Angle", degrees(45))`.
348
389
 
349
- <details><summary><code>TransitionCurveOptions</code></summary>
390
+ `degrees(deg: number): number`
350
391
 
351
- ```ts
352
- interface TransitionCurveOptions {
353
- /** Weight for the start edge. Controls tangent magnitude at the start. - 1.0 (default): balanced transition - > 1.0: curve follows start edge longer before turning - < 1.0: curve turns sooner at the start */
354
- weightA?: number;
355
- /** Weight for the end edge. Controls tangent magnitude at the end. - 1.0 (default): balanced transition - > 1.0: curve follows end edge longer before turning - < 1.0: curve turns sooner at the end */
356
- weightB?: number;
357
- /** Number of sample points for the output polyline. Default 64. Higher values give smoother curves at the cost of more geometry. */
358
- samples?: number;
359
- }
360
- ```
392
+ #### `radians()` — Convert radians to degrees.
361
393
 
362
- </details>
394
+ ForgeCAD's public API uses degrees throughout. Use this when you have a radian value (e.g. from `Math.atan2`) that you want to express in degrees.
363
395
 
364
- <details><summary><code>TransitionSurfaceOptions</code> extends TransitionCurveOptions</summary>
396
+ `radians(rad: number): number`
365
397
 
366
- ```ts
367
- interface TransitionSurfaceOptions extends TransitionCurveOptions {
368
- /** Cross-section profile to sweep along the transition curve. If omitted, a circular profile with `radius` is used. */
369
- profile?: Sketch;
370
- /** Radius of circular cross-section (used when `profile` is omitted). Default: 5% of chord length. */
371
- radius?: number;
372
- width: number;
373
- height: number;
374
- /** Preferred up vector for the sweep frame. Default: auto-detected. */
375
- up?: Vec3$7;
376
- /** Edge length for level-set meshing. Smaller = finer. */
377
- edgeLength?: number;
378
- /** Extra bounds padding for level-set meshing. */
379
- boundsPadding?: number;
380
- }
381
- ```
398
+ #### `composeChain()` — Compose transforms in chain order. Equivalent to Transform.identity().mul(a).mul(b).mul(c)...
382
399
 
383
- </details>
400
+ `composeChain(...steps: TransformInput[]): Transform`
384
401
 
385
- <details><summary><code>ConnectEdgesOptions</code> extends TransitionSurfaceOptions</summary>
402
+ ---
386
403
 
387
- ```ts
388
- interface ConnectEdgesOptions extends TransitionSurfaceOptions {
389
- /** Which end of edge A to connect. Default: 'start'. */
390
- endA?: EdgeEnd;
391
- /** Which end of edge B to connect. Default: 'start'. */
392
- endB?: EdgeEnd;
393
- /** Tangent mode for edge A. Default: 'along'. */
394
- tangentModeA?: TangentMode;
395
- /** Tangent mode for edge B. Default: 'along'. */
396
- tangentModeB?: TangentMode;
397
- /** Explicit tangent for edge A. */
398
- tangentA?: Vec3$7;
399
- /** Explicit tangent for edge B. */
400
- tangentB?: Vec3$7;
401
- /** Flip tangent A. */
402
- flipA?: boolean;
403
- /** Flip tangent B. */
404
- flipB?: boolean;
405
- }
406
- ```
404
+ ## C4: Dimensional Promotion
407
405
 
408
- </details>
406
+ Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep).
409
407
 
410
- #### `hermiteTransition()`
408
+ #### `connectEdges()` — Create a transition surface or solid bridge between two edge segments.
411
409
 
412
- ```ts
413
- hermiteTransition(a: EdgeEndpoint, b: EdgeEndpoint): HermiteCurve3D
414
- ```
410
+ Tangents can be inferred from neighboring geometry or supplied explicitly through `options`. This is useful for loft-like blends where you want a direct connection between two edge spans.
415
411
 
416
- Create a Hermite transition curve between two edge endpoints. The curve starts at `a.point` tangent to `a.tangent` and ends at `b.point` tangent to `b.tangent`, with smooth G1-continuous interpolation. Weight controls: - weight = 1.0 (default): balanced transition - weight > 1.0: curve follows this edge's direction longer before turning - weight < 1.0: curve turns sooner, shorter tangent influence
412
+ `connectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape`
417
413
 
418
- <details><summary><code>EdgeEndpoint</code></summary>
414
+ **`EdgeSegment`**
419
415
 
420
- ```ts
421
- interface EdgeEndpoint {
422
- /** Connection point on the edge */
423
- point: Vec3$5;
424
- /** Tangent direction along the edge at the connection point */
425
- tangent: Vec3$5;
426
- /** Surface normal at the connection point (optional, for future G2 support) */
427
- normal?: Vec3$5;
428
- /** Weight controlling how far the curve follows this edge's tangent. Default 1.0. */
429
- weight?: number;
430
- }
431
- ```
416
+ | Option | Type | Description |
417
+ |--------|------|-------------|
418
+ | `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |
419
+ | `direction` | `Vec3` | Normalized direction from start → end. |
420
+ | `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |
421
+ | `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |
422
+ | `normalA` | `Vec3` | Normal of first adjacent face. |
423
+ | `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |
424
+ | `boundary` | `boolean` | true if this is a boundary (unmatched) edge unusual for closed solids. |
425
+ | `start`, `end`, `midpoint`, `length` | | — |
432
426
 
433
- </details>
427
+ **`TransitionCurveOptions`**
428
+ - `weightA?: number` — Weight for the start edge. Controls tangent magnitude at the start. - 1.0 (default): balanced transition - > 1.0: curve follows start edge longer before turning - < 1.0: curve turns sooner at the start
429
+ - `weightB?: number` — Weight for the end edge. Controls tangent magnitude at the end. - 1.0 (default): balanced transition - > 1.0: curve follows end edge longer before turning - < 1.0: curve turns sooner at the end
430
+ - `samples?: number` — Number of sample points for the output polyline. Default 64. Higher values give smoother curves at the cost of more geometry.
434
431
 
435
- #### `hermiteTransitionG2()`
432
+ **`TransitionSurfaceOptions`** extends TransitionCurveOptions
436
433
 
437
- ```ts
438
- hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D
439
- ```
434
+ | Option | Type | Description |
435
+ |--------|------|-------------|
436
+ | `profile?` | `Sketch` | Cross-section profile to sweep along the transition curve. If omitted, a circular profile with `radius` is used. |
437
+ | `radius?` | `number` | Radius of circular cross-section (used when `profile` is omitted). Default: 5% of chord length. |
438
+ | `up?` | `Vec3$6` | Preferred up vector for the sweep frame. Default: auto-detected. |
439
+ | `edgeLength?` | `number` | Edge length for level-set meshing. Smaller = finer. |
440
+ | `boundsPadding?` | `number` | Extra bounds padding for level-set meshing. |
441
+ | `width`, `height` | | — |
440
442
 
441
- Create a quintic Hermite transition curve between two edge endpoints (G2 continuity). The curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`, and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`, with smooth G2-continuous interpolation matching position, tangent, and curvature.
443
+ **`ConnectEdgesOptions`** extends TransitionSurfaceOptions
442
444
 
443
- <details><summary><code>QuinticHermiteCurveEndpoint</code></summary>
445
+ | Option | Type | Description |
446
+ |--------|------|-------------|
447
+ | `endA?` | `EdgeEnd` | Which end of edge A to connect. Default: 'start'. |
448
+ | `endB?` | `EdgeEnd` | Which end of edge B to connect. Default: 'start'. |
449
+ | `tangentModeA?` | `TangentMode` | Tangent mode for edge A. Default: 'along'. |
450
+ | `tangentModeB?` | `TangentMode` | Tangent mode for edge B. Default: 'along'. |
451
+ | `tangentA?` | `Vec3$6` | Explicit tangent for edge A. |
452
+ | `tangentB?` | `Vec3$6` | Explicit tangent for edge B. |
453
+ | `flipA?` | `boolean` | Flip tangent A. |
454
+ | `flipB?` | `boolean` | Flip tangent B. |
444
455
 
445
- ```ts
446
- interface QuinticHermiteCurveEndpoint {
447
- /** Position */
448
- point: Vec3$5;
449
- /** Tangent direction (will be normalized internally) */
450
- tangent: Vec3$5;
451
- /** Second derivative / curvature vector. Default [0, 0, 0]. */
452
- curvature?: Vec3$5;
453
- /** Weight: scales tangent magnitude relative to chord length. Default 1.0. */
454
- weight?: number;
455
- }
456
- ```
456
+ #### `hermiteTransitionG2()` — Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).
457
457
 
458
- </details>
458
+ The curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`, and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`, with smooth G2-continuous interpolation matching position, tangent, and curvature.
459
459
 
460
- #### `loft()`
460
+ `hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D`
461
461
 
462
- ```ts
463
- loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
464
- ```
462
+ **`QuinticHermiteCurveEndpoint`**
465
463
 
466
- Loft between multiple sketches along Z stations. Profiles can differ in topology and vertex count: interpolation is done on signed-distance fields and meshed with level-set extraction. Heights must be strictly increasing. Compatible loft stacks can export through the OCCT exact route. Performance note: loft is significantly heavier than primitive/extrude/revolve. If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().
464
+ | Option | Type | Description |
465
+ |--------|------|-------------|
466
+ | `point` | `Vec3$4` | Position |
467
+ | `tangent` | `Vec3$4` | Tangent direction (will be normalized internally) |
468
+ | `curvature?` | `Vec3$4` | Second derivative / curvature vector. Default [0, 0, 0]. |
469
+ | `weight?` | `number` | Weight: scales tangent magnitude relative to chord length. Default 1.0. |
467
470
 
468
- <details><summary><code>LoftOptions</code></summary>
471
+ #### `loft()` — Loft between multiple sketches along Z stations.
469
472
 
470
- ```ts
471
- interface LoftOptions {
472
- /** Marching-grid edge length for level-set meshing. Smaller = finer. */
473
- edgeLength?: number;
474
- /** Optional extra bounds padding. */
475
- boundsPadding?: number;
476
- }
477
- ```
473
+ Profiles can differ in topology and vertex count: interpolation is done on signed-distance fields and meshed with level-set extraction. Heights must be strictly increasing. Compatible loft stacks can export through the OCCT exact route.
478
474
 
479
- </details>
475
+ Performance note: loft is significantly heavier than primitive/extrude/revolve. If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().
480
476
 
481
- #### `loftAlongSpine()`
477
+ `loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape`
482
478
 
483
- ```ts
484
- loftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3$4[], tValues: number[], options?: LoftAlongSpineOptions): Shape
485
- ```
479
+ **`LoftOptions`**
480
+ - `edgeLength?: number` Marching-grid edge length for level-set meshing. Smaller = finer.
481
+ - `boundsPadding?: number` — Optional extra bounds padding.
486
482
 
487
- Loft between multiple profiles positioned along an arbitrary 3D spine curve. Unlike loft() which only supports Z heights, loftAlongSpine() places each profile at a position along a 3D spine, oriented perpendicular to the spine tangent. This enables lofting along curved paths — e.g., a wing root-to-tip transition that follows a swept-back leading edge. The tValues array specifies where each profile sits along the spine (0 = start, 1 = end). Must have the same length as profiles and be in [0, 1]. Internally uses variableSweep infrastructure with SDF interpolation. Performance note: uses level-set meshing, heavier than simple loft().
483
+ #### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.
488
484
 
489
- <details><summary><code>LoftAlongSpineOptions</code></summary>
485
+ Unlike loft() which only supports Z heights, loftAlongSpine() places each profile at a position along a 3D spine, oriented perpendicular to the spine tangent. This enables lofting along curved paths — e.g., a wing root-to-tip transition that follows a swept-back leading edge.
490
486
 
491
- ```ts
492
- interface LoftAlongSpineOptions {
493
- /** Number of samples when spine is a Curve3D. Default 48. */
494
- samples?: number;
495
- /** Marching-grid edge length for level-set meshing. Smaller = finer. */
496
- edgeLength?: number;
497
- /** Optional extra bounds padding. */
498
- boundsPadding?: number;
499
- /** Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. */
500
- up?: Vec3$4;
501
- }
502
- ```
487
+ The tValues array specifies where each profile sits along the spine (0 = start, 1 = end). Must have the same length as profiles and be in [0, 1].
503
488
 
504
- </details>
489
+ Internally uses variableSweep infrastructure with SDF interpolation.
505
490
 
506
- #### `spline3d()`
491
+ Performance note: uses level-set meshing, heavier than simple loft().
507
492
 
508
- ```ts
509
- spline3d(points: Vec3$4[], options?: Spline3DOptions): Curve3D
510
- ```
493
+ `loftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3$3[], tValues: number[], options?: LoftAlongSpineOptions): Shape`
511
494
 
512
- Create a reusable 3D spline curve object (Catmull-Rom). The returned Curve3D provides sample(), pointAt(t), tangentAt(t), and length() for downstream use in sweep() or manual path operations.
495
+ **`LoftAlongSpineOptions`**
513
496
 
514
- <details><summary><code>Spline3DOptions</code></summary>
497
+ | Option | Type | Description |
498
+ |--------|------|-------------|
499
+ | `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |
500
+ | `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |
501
+ | `boundsPadding?` | `number` | Optional extra bounds padding. |
502
+ | `up?` | `Vec3$3` | Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. |
515
503
 
516
- ```ts
517
- interface Spline3DOptions {
518
- /** Closed loop (default false). */
519
- closed?: boolean;
520
- /** Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. */
521
- tension?: number;
522
- }
523
- ```
504
+ #### `spline3d()` — Create a reusable 3D spline curve object (Catmull-Rom).
524
505
 
525
- </details>
506
+ The returned Curve3D provides sample(), pointAt(t), tangentAt(t), and length() for downstream use in sweep() or manual path operations.
526
507
 
527
- #### `surfacePatch()`
508
+ `spline3d(points: Vec3$3[], options?: Spline3DOptions): Curve3D`
528
509
 
529
- ```ts
530
- surfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape
531
- ```
510
+ **`Spline3DOptions`**
511
+ - `closed?: boolean` Closed loop (default false).
512
+ - `tension?: number` — Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5.
532
513
 
533
- Create a smooth surface patch from 4 boundary curves (Coons patch). The four curves form the boundary of a quadrilateral patch: - bottom: u=0..1 at v=0 (from corner00 to corner10) - top: u=0..1 at v=1 (from corner01 to corner11) - left: v=0..1 at u=0 (from corner00 to corner01) - right: v=0..1 at u=1 (from corner10 to corner11) The interior is filled using bilinear Coons patch interpolation: P(u,v) = Lc(u,v) + Ld(u,v) - B(u,v) The result is a thin solid created by offsetting the surface mesh along its normals by the specified thickness. Note: curves should meet at corners. Small gaps are tolerated.
514
+ #### `surfacePatch()` — Create a smooth surface patch from 4 boundary curves (Coons patch).
534
515
 
535
- <details><summary><code>SurfacePatchOptions</code></summary>
516
+ The four curves form the boundary of a quadrilateral patch: - bottom: u=0..1 at v=0 (from corner00 to corner10) - top: u=0..1 at v=1 (from corner01 to corner11) - left: v=0..1 at u=0 (from corner00 to corner01) - right: v=0..1 at u=1 (from corner10 to corner11)
536
517
 
537
- ```ts
538
- interface SurfacePatchOptions {
539
- /** Number of samples along each direction. Default 24. */
540
- resolution?: number;
541
- /** Thickness of the generated solid. Default 0.5. */
542
- thickness?: number;
543
- }
544
- ```
518
+ The interior is filled using bilinear Coons patch interpolation: P(u,v) = Lc(u,v) + Ld(u,v) - B(u,v)
545
519
 
546
- </details>
520
+ The result is a thin solid created by offsetting the surface mesh along its normals by the specified thickness.
547
521
 
548
- #### `sweep()`
522
+ Note: curves should meet at corners. Small gaps are tolerated.
549
523
 
550
- ```ts
551
- sweep(profile: Sketch, path: Curve3D | Vec3$4[], options?: SweepOptions): Shape
552
- ```
524
+ `surfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape`
553
525
 
554
- Sweep a 2D profile along a 3D path to create a solid. Path can be a Curve3D from spline3d() or an array of [x,y,z] points (polyline). The profile is interpreted in the local frame normal plane. Compatible sweeps can export through the OCCT exact route using the canonical path representation. Performance note: sweep uses level-set meshing internally. Prefer direct primitives/extrude/revolve when they can express the same shape.
526
+ **`SurfacePatchOptions`**
527
+ - `resolution?: number` — Number of samples along each direction. Default 24.
528
+ - `thickness?: number` — Thickness of the generated solid. Default 0.5.
555
529
 
556
- <details><summary><code>SweepOptions</code></summary>
530
+ #### `sweep()` — Sweep a 2D profile along a 3D path to create a solid.
557
531
 
558
- ```ts
559
- interface SweepOptions {
560
- /** Number of samples when path is a Curve3D. Default 48. */
561
- samples?: number;
562
- /** Marching-grid edge length for level-set meshing. Smaller = finer. */
563
- edgeLength?: number;
564
- /** Optional extra bounds padding. */
565
- boundsPadding?: number;
566
- /** Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. */
567
- up?: Vec3$4;
568
- }
569
- ```
532
+ Path can be a Curve3D from spline3d() or an array of [x,y,z] points (polyline). The profile is interpreted in the local frame normal plane. Compatible sweeps can export through the OCCT exact route using the canonical path representation.
570
533
 
571
- </details>
534
+ Performance note: sweep uses level-set meshing internally. Prefer direct primitives/extrude/revolve when they can express the same shape.
572
535
 
573
- #### `variableSweep()`
536
+ `sweep(profile: Sketch, path: Curve3D | Vec3$3[], options?: SweepOptions): Shape`
574
537
 
575
- ```ts
576
- variableSweep(spine: Curve3D | Vec3$4[], sections: VariableSweepSection[], options?: VariableSweepOptions): Shape
577
- ```
538
+ **`SweepOptions`**
578
539
 
579
- Sweep a variable cross-section along a 3D spine curve. Unlike sweep(), which uses a single constant profile, variableSweep() interpolates between multiple profiles at different stations along the spine. This enables organic shapes like tapering tubes, bone-like structures, and sculptural forms. Each section specifies a t parameter (0 = start, 1 = end of spine) and a 2D profile sketch. The SDF-based level-set mesher smoothly blends between profiles at intermediate positions. Performance note: like sweep(), this uses level-set meshing internally.
540
+ | Option | Type | Description |
541
+ |--------|------|-------------|
542
+ | `samples?` | `number` | Number of samples when path is a Curve3D. Default 48. |
543
+ | `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |
544
+ | `boundsPadding?` | `number` | Optional extra bounds padding. |
545
+ | `up?` | `Vec3$3` | Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. |
580
546
 
581
- <details><summary><code>VariableSweepSection</code></summary>
547
+ #### `variableSweep()` — Sweep a variable cross-section along a 3D spine curve.
582
548
 
583
- ```ts
584
- interface VariableSweepSection {
585
- /** Parameter along the spine (0 = start, 1 = end). */
586
- t: number;
587
- /** Cross-section profile at this station. */
588
- profile: Sketch;
589
- }
590
- ```
549
+ Unlike sweep(), which uses a single constant profile, variableSweep() interpolates between multiple profiles at different stations along the spine. This enables organic shapes like tapering tubes, bone-like structures, and sculptural forms.
591
550
 
592
- </details>
551
+ Each section specifies a t parameter (0 = start, 1 = end of spine) and a 2D profile sketch. The SDF-based level-set mesher smoothly blends between profiles at intermediate positions.
593
552
 
594
- <details><summary><code>VariableSweepOptions</code></summary>
553
+ Performance note: like sweep(), this uses level-set meshing internally.
595
554
 
596
- ```ts
597
- interface VariableSweepOptions {
598
- /** Number of samples when spine is a Curve3D. Default 48. */
599
- samples?: number;
600
- /** Marching-grid edge length for level-set meshing. Smaller = finer. */
601
- edgeLength?: number;
602
- /** Optional extra bounds padding. */
603
- boundsPadding?: number;
604
- /** Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. */
605
- up?: Vec3$4;
606
- }
607
- ```
555
+ `variableSweep(spine: Curve3D | Vec3$3[], sections: VariableSweepSection[], options?: VariableSweepOptions): Shape`
608
556
 
609
- </details>
557
+ **`VariableSweepSection`**
558
+ - `t: number` — Parameter along the spine (0 = start, 1 = end).
559
+ - `profile: Sketch` — Cross-section profile at this station.
610
560
 
611
- #### `transitionCurve()`
561
+ **`VariableSweepOptions`**
612
562
 
613
- ```ts
614
- transitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D
615
- ```
563
+ | Option | Type | Description |
564
+ |--------|------|-------------|
565
+ | `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |
566
+ | `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |
567
+ | `boundsPadding?` | `number` | Optional extra bounds padding. |
568
+ | `up?` | `Vec3$3` | Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. |
616
569
 
617
- Create a smooth transition curve between two edges. Returns a `HermiteCurve3D` that starts at `edgeA.point` tangent to `edgeA.tangent` and ends at `edgeB.point` tangent to `edgeB.tangent`. The curve maintains G1 continuity (matching tangent direction) at both endpoints. Weight parameters control the shape of the transition. ```js // Connect two edges with a balanced transition const curve = transitionCurve( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 0], tangent: [1, 0, 0] }, ); // Weighted: curve hugs edge A longer const weighted = transitionCurve( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 0], tangent: [1, 0, 0] }, { weightA: 2.0, weightB: 0.5 }, ); ```
570
+ #### `transitionCurve()` Create a smooth transition curve between two edges.
618
571
 
619
- <details><summary><code>TransitionEdge</code></summary>
572
+ Returns a `HermiteCurve3D` that starts at `edgeA.point` tangent to `edgeA.tangent` and ends at `edgeB.point` tangent to `edgeB.tangent`.
620
573
 
621
- ```ts
622
- interface TransitionEdge {
623
- /** Connection point on the edge. Can be any point along the edge where the transition should connect. */
624
- point: Vec3$7;
625
- /** Tangent direction at the connection point. This is the direction the curve should initially follow when leaving this edge. For a straight edge, this is typically the edge direction pointing "outward" (away from the body of the edge, toward the other edge). */
626
- tangent: Vec3$7;
627
- /** Surface normal at the connection point (optional). Used as a hint for the sweep frame's up vector. */
628
- normal?: Vec3$7;
629
- }
574
+ The curve maintains G1 continuity (matching tangent direction) at both endpoints. Weight parameters control the shape of the transition.
575
+
576
+ ```js
577
+ // Connect two edges with a balanced transition
578
+ const curve = transitionCurve(
579
+ { point: [0, 0, 0], tangent: [1, 0, 0] },
580
+ { point: [10, 5, 0], tangent: [1, 0, 0] },
581
+ );
582
+
583
+ // Weighted: curve hugs edge A longer
584
+ const weighted = transitionCurve(
585
+ { point: [0, 0, 0], tangent: [1, 0, 0] },
586
+ { point: [10, 5, 0], tangent: [1, 0, 0] },
587
+ { weightA: 2.0, weightB: 0.5 },
588
+ );
630
589
  ```
631
590
 
632
- </details>
591
+ `transitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D`
633
592
 
634
- #### `transitionCurveFromPoints()`
593
+ **`TransitionEdge`**
594
+ - `point: Vec3$6` — Connection point on the edge. Can be any point along the edge where the transition should connect.
595
+ - `tangent: Vec3$6` — Tangent direction at the connection point. This is the direction the curve should initially follow when leaving this edge. For a straight edge, this is typically the edge direction pointing "outward" (away from the body of the edge, toward the other edge).
596
+ - `normal?: Vec3$6` — Surface normal at the connection point (optional). Used as a hint for the sweep frame's up vector.
635
597
 
636
- ```ts
637
- transitionCurveFromPoints(startPoint: Vec3$7, startTangent: Vec3$7, endPoint: Vec3$7, endTangent: Vec3$7, options?: TransitionCurveOptions): HermiteCurve3D
638
- ```
598
+ #### `transitionSurface()` — Create a solid transition surface between two edges by sweeping a profile along a Hermite transition curve.
639
599
 
640
- Convenience: create a transition curve from raw coordinate data. Useful when you have endpoints and directions as plain arrays without constructing TransitionEdge objects.
600
+ This produces a watertight solid that smoothly connects the two edges. Works with both Manifold and OCCT backends.
641
601
 
642
- #### `transitionSurface()`
602
+ ```js
603
+ // Circular tube connecting two edges
604
+ const tube = transitionSurface(
605
+ { point: [0, 0, 0], tangent: [1, 0, 0] },
606
+ { point: [10, 5, 3], tangent: [0, 1, 0] },
607
+ { radius: 0.5 },
608
+ );
643
609
 
644
- ```ts
645
- transitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape
610
+ // Custom profile with weights
611
+ const custom = transitionSurface(
612
+ { point: [0, 0, 0], tangent: [1, 0, 0] },
613
+ { point: [10, 5, 3], tangent: [0, 1, 0] },
614
+ { profile: mySketch, weightA: 1.5, weightB: 0.8 },
615
+ );
646
616
  ```
647
617
 
648
- Create a solid transition surface between two edges by sweeping a profile along a Hermite transition curve. This produces a watertight solid that smoothly connects the two edges. Works with both Manifold and OCCT backends. ```js // Circular tube connecting two edges const tube = transitionSurface( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 3], tangent: [0, 1, 0] }, { radius: 0.5 }, ); // Custom profile with weights const custom = transitionSurface( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 3], tangent: [0, 1, 0] }, { profile: mySketch, weightA: 1.5, weightB: 0.8 }, ); ```
618
+ `transitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape`
649
619
 
650
620
  ---
651
621
 
@@ -653,106 +623,94 @@ Create a solid transition surface between two edges by sweeping a profile along
653
623
 
654
624
  Select or inspect named faces and edges on a shape.
655
625
 
656
- #### `coalesceEdges()`
626
+ #### `coalesceEdges()` — Merge collinear edge segments into longer logical edges.
657
627
 
658
- ```ts
659
- coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]
660
- ```
628
+ **Details**
629
+
630
+ Tessellation often splits one geometric edge into multiple short segments. `coalesceEdges` groups adjacent collinear segments and merges each group into a single `EdgeSegment` spanning the full extent. This is usually needed before passing edges to `fillet()` or `chamfer()` on non-primitive shapes.
661
631
 
662
- Coalesce collinear edge segments into longer logical edges. Multiple short mesh segments along the same line (e.g. from tessellation) are merged into a single EdgeSegment spanning the full extent. The `tolerance` controls how far endpoints can deviate from collinearity.
632
+ The `tolerance` controls the maximum perpendicular distance from collinearity before two segments are considered non-collinear. Default: `0.01`.
663
633
 
664
- <details><summary><code>EdgeSegment</code></summary>
634
+ **Example**
665
635
 
666
636
  ```ts
667
- interface EdgeSegment {
668
- /** Stable index within the extraction (deterministic for a given mesh). */
669
- index: number;
670
- start: Vec3;
671
- end: Vec3;
672
- midpoint: Vec3;
673
- /** Normalized direction from start → end. */
674
- direction: Vec3;
675
- length: number;
676
- /** Dihedral angle in degrees (0 = coplanar, 180 = knife edge). */
677
- dihedralAngle: number;
678
- /** true = outside corner (convex), false = inside corner (concave). */
679
- convex: boolean;
680
- /** Normal of first adjacent face. */
681
- normalA: Vec3;
682
- /** Normal of second adjacent face (same as normalA for boundary edges). */
683
- normalB: Vec3;
684
- /** true if this is a boundary (unmatched) edge — unusual for closed solids. */
685
- boundary: boolean;
637
+ const topEdges = selectEdges(part, { atZ: 20 });
638
+ for (const edge of coalesceEdges(topEdges)) {
639
+ result = fillet(result, 2, edge);
686
640
  }
687
641
  ```
688
642
 
689
- </details>
643
+ `coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]`
690
644
 
691
- #### `selectEdge()`
645
+ **`EdgeSegment`**
692
646
 
693
- ```ts
694
- selectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment
695
- ```
647
+ | Option | Type | Description |
648
+ |--------|------|-------------|
649
+ | `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |
650
+ | `direction` | `Vec3` | Normalized direction from start → end. |
651
+ | `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |
652
+ | `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |
653
+ | `normalA` | `Vec3` | Normal of first adjacent face. |
654
+ | `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |
655
+ | `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |
656
+ | `start`, `end`, `midpoint`, `length` | | — |
696
657
 
697
- Select the single best-matching edge from a shape. When `near` is specified, returns the closest matching edge. Otherwise returns the first matching edge (by mesh order). Throws if no edges match.
658
+ #### `selectEdge()` — Select the single best-matching edge from a shape.
698
659
 
699
- <details><summary><code>EdgeQuery</code></summary>
660
+ **Details**
661
+
662
+ When `near` is specified, returns the edge whose midpoint is closest to that point. Otherwise returns the first matching edge in mesh order. Throws if no edges match the query — useful as a guard when you expect exactly one result.
663
+
664
+ **Example**
700
665
 
701
666
  ```ts
702
- interface EdgeQuery {
703
- /** Sort by proximity to this point (closest first). */
704
- near?: Vec3;
705
- /** Filter: edge direction approximately parallel to this vector. */
706
- parallel?: Vec3;
707
- /** Filter: edge direction approximately perpendicular to this vector. */
708
- perpendicular?: Vec3;
709
- /** Filter: only convex (outside corner) edges. */
710
- convex?: boolean;
711
- /** Filter: only concave (inside corner) edges. */
712
- concave?: boolean;
713
- /** Filter: minimum dihedral angle in degrees. */
714
- minAngle?: number;
715
- /** Filter: maximum dihedral angle in degrees. */
716
- maxAngle?: number;
717
- /** Filter: minimum edge length. */
718
- minLength?: number;
719
- /** Filter: maximum edge length. */
720
- maxLength?: number;
721
- /** Filter: edge midpoint must be within this bounding region. */
722
- within?: BoundingRegion;
723
- /** Shorthand: edge midpoint Z ≈ this value (within tolerance). */
724
- atZ?: number;
725
- /** Tolerance for approximate matches (default: 1.0). */
726
- tolerance?: number;
727
- /** Angular tolerance in degrees for parallel/perpendicular (default: 10). */
728
- angleTolerance?: number;
729
- }
667
+ // Chamfer one specific edge near a known point
668
+ const bottomEdge = selectEdge(part, { near: [25, 0, 0], atZ: 0 });
669
+ result = chamfer(result, 1.5, bottomEdge);
730
670
  ```
731
671
 
732
- </details>
672
+ `selectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment`
733
673
 
734
- <details><summary><code>BoundingRegion</code></summary>
674
+ **`EdgeQuery`**
735
675
 
736
- ```ts
737
- interface BoundingRegion {
738
- xMin?: number;
739
- xMax?: number;
740
- yMin?: number;
741
- yMax?: number;
742
- zMin?: number;
743
- zMax?: number;
744
- }
745
- ```
676
+ | Option | Type | Description |
677
+ |--------|------|-------------|
678
+ | `near?` | `Vec3` | Sort by proximity to this point (closest first). When used with `selectEdge`, picks the closest match. |
679
+ | `parallel?` | `Vec3` | Filter: edge direction approximately parallel to this vector. |
680
+ | `perpendicular?` | `Vec3` | Filter: edge direction approximately perpendicular to this vector. |
681
+ | `convex?` | `boolean` | Filter: only convex (outside corner) edges. |
682
+ | `concave?` | `boolean` | Filter: only concave (inside corner) edges. |
683
+ | `minAngle?` | `number` | Filter: minimum dihedral angle in degrees. |
684
+ | `maxAngle?` | `number` | Filter: maximum dihedral angle in degrees. |
685
+ | `minLength?` | `number` | Filter: minimum edge length. |
686
+ | `maxLength?` | `number` | Filter: maximum edge length. |
687
+ | `within?` | `BoundingRegion` | Filter: edge midpoint must be within this bounding region. |
688
+ | `atZ?` | `number` | Shorthand: edge midpoint Z ≈ this value (within `tolerance`). Equivalent to `within: { zMin: atZ - tol, zMax: atZ + tol }`. |
689
+ | `tolerance?` | `number` | Position tolerance for approximate matches (default: `1.0`). Used by `atZ` and `near`. |
690
+ | `angleTolerance?` | `number` | Angular tolerance in degrees for `parallel`/`perpendicular` filters (default: `10`). |
691
+
692
+ `BoundingRegion`: `{ xMin?: number, xMax?: number, yMin?: number, yMax?: number, zMin?: number, zMax?: number }`
746
693
 
747
- </details>
694
+ #### `selectEdges()` — Select all edges from a shape that match the given query.
748
695
 
749
- #### `selectEdges()`
696
+ **Details**
697
+
698
+ Extracts sharp edges from the mesh (dihedral angle > 1°), applies all filters in the query, and returns the matching `EdgeSegment[]`. When `near` is specified the results are sorted closest-first.
699
+
700
+ Works on any shape — primitives, booleans, shells, and imported meshes. Use this when tracked topology is unavailable (e.g. after a difference or on imported geometry). For simpler cases, pass an `EdgeQuery` directly to `fillet()` or `chamfer()` instead of calling `selectEdges` separately.
701
+
702
+ **Example**
750
703
 
751
704
  ```ts
752
- selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]
705
+ // Fillet all top edges of a box
706
+ const topEdges = selectEdges(part, { atZ: 20, perpendicular: [0, 0, 1] });
707
+ let result = part;
708
+ for (const edge of coalesceEdges(topEdges)) {
709
+ result = fillet(result, 2, edge);
710
+ }
753
711
  ```
754
712
 
755
- Select all edges from a shape that match the given query. Extracts sharp edges from the mesh (dihedral angle > 1°), applies filters, and returns the matching EdgeSegment array.
713
+ `selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]`
756
714
 
757
715
  ---
758
716
 
@@ -760,100 +718,155 @@ Select all edges from a shape that match the given query. Extracts sharp edges f
760
718
 
761
719
  Modify edges of a solid — fillets, chamfers, draft, offset.
762
720
 
763
- #### `filletEdgeSegment()`
721
+ #### `chamfer()` — Apply chamfers (beveled edges) to one or more edges of a shape.
764
722
 
765
- ```ts
766
- filletEdgeSegment(shape: Shape, segment: EdgeSegment, radius: number, segments?: number): Shape
767
- ```
723
+ **Details**
724
+
725
+ Produces a 45° bevel at the specified `size` (distance from edge). Works on both straight and curved edges. Supports OCCT and Manifold backends.
768
726
 
769
- Apply a fillet (rounded edge) to a mesh-selected edge. Works on any straight edge of any shape — not limited to tracked box edges. The edge must have been obtained from selectEdge() / selectEdges().
727
+ The `edges` parameter accepts the same options as `fillet()`: inline `EdgeQuery`, pre-selected `EdgeSegment`/`EdgeSegment[]`, or `undefined` (all sharp edges).
770
728
 
771
- <details><summary><code>EdgeSegment</code></summary>
729
+ **Example**
772
730
 
773
731
  ```ts
774
- interface EdgeSegment {
775
- /** Stable index within the extraction (deterministic for a given mesh). */
776
- index: number;
777
- start: Vec3;
778
- end: Vec3;
779
- midpoint: Vec3;
780
- /** Normalized direction from start → end. */
781
- direction: Vec3;
782
- length: number;
783
- /** Dihedral angle in degrees (0 = coplanar, 180 = knife edge). */
784
- dihedralAngle: number;
785
- /** true = outside corner (convex), false = inside corner (concave). */
786
- convex: boolean;
787
- /** Normal of first adjacent face. */
788
- normalA: Vec3;
789
- /** Normal of second adjacent face (same as normalA for boundary edges). */
790
- normalB: Vec3;
791
- /** true if this is a boundary (unmatched) edge — unusual for closed solids. */
792
- boundary: boolean;
793
- }
732
+ // Chamfer all edges
733
+ chamfer(myShape, 1)
734
+
735
+ // Chamfer only vertical edges
736
+ chamfer(myShape, 2, { parallel: [0, 0, 1] })
794
737
  ```
795
738
 
796
- </details>
739
+ `chamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape`
797
740
 
798
- #### `chamferEdgeSegment()`
741
+ #### `draft()` — Apply a draft angle (taper) to vertical faces for mold extraction.
799
742
 
800
- ```ts
801
- chamferEdgeSegment(shape: Shape, segment: EdgeSegment, size: number): Shape
802
- ```
743
+ **Details**
803
744
 
804
- Apply a chamfer (beveled edge) to a mesh-selected edge. Works on any straight edge of any shapenot limited to tracked box edges.
745
+ Adds a taper angle to the vertical faces of a solid so that it can be extracted from a mold. The neutral plane is the Z position where the draft angle is zero faces above and below are tapered symmetrically. Typical values for injection molding are 1–5°.
805
746
 
806
- #### `chamfer()`
747
+ Requires the OCCT backend. Throws on Manifold.
748
+
749
+ **Example**
807
750
 
808
751
  ```ts
809
- chamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape
752
+ // Add draft to a box for injection molding
753
+ draft(myBox, 3)
754
+
755
+ // Draft with custom pull direction and neutral plane
756
+ draft(myShape, 2, [0, 0, 1], 10)
810
757
  ```
811
758
 
812
- Apply chamfers (beveled edges) to one or more edges of a shape. Works on both straight and curved edges. Supports OCCT and Manifold backends. // Chamfer all edges chamfer(myShape, 1) // Chamfer vertical edges only chamfer(myShape, 2, { parallel: [0, 0, 1] })
759
+ `draft(shape: Shape, angleDeg: number, pullDirection?: [ number, number, number ], neutralPlaneOffset?: number): Shape`
760
+
761
+ #### `fillet()` — Apply fillets (rounded edges) to one or more edges of a shape.
813
762
 
814
- #### `draft()`
763
+ **Details**
764
+
765
+ Works on both straight and curved edges. Supports OCCT and Manifold backends. When using OCCT, all edges are filleted in a single kernel operation for best quality. When using Manifold, edges are filleted sequentially.
766
+
767
+ The `edges` parameter is flexible: - Omit to fillet **all** sharp edges - Pass an `EdgeQuery` for an inline filter (most common) - Pass an `EdgeSegment` or `EdgeSegment[]` from `selectEdges()` for pre-selected edges
768
+
769
+ Throws if no edges match the selection, or if `radius` is not a positive finite number.
770
+
771
+ **Example**
815
772
 
816
773
  ```ts
817
- draft(shape: Shape, angleDeg: number, pullDirection?: [ number, number, number ], neutralPlaneOffset?: number): Shape
774
+ // Fillet all edges
775
+ fillet(myShape, 2)
776
+
777
+ // Fillet only top convex edges
778
+ fillet(myShape, 1.5, { atZ: 20, convex: true })
779
+
780
+ // Fillet vertical edges selected beforehand
781
+ const edges = selectEdges(myShape, { parallel: [0, 0, 1] })
782
+ fillet(myShape, 3, edges)
818
783
  ```
819
784
 
820
- Apply a draft angle (taper) to all faces of a solid for mold extraction. Draft angle is a manufacturing feature that adds taper to the vertical faces of a solid so that it can be extracted from a mold. The neutral plane is where the draft angle is zero — faces above and below are tapered symmetrically. Requires the OCCT backend. Throws on Manifold. // Add 3° draft to a box for injection molding draft(myBox, 3) // Draft with custom pull direction and neutral plane draft(myShape, 2, [0, 0, 1], 10)
785
+ `fillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape`
786
+
787
+ #### `offsetSolid()` — Uniformly offset all surfaces of a solid inward or outward.
788
+
789
+ **Details**
821
790
 
822
- #### `fillet()`
791
+ Unlike `shell()`, which hollows a solid by removing one face, `offsetSolid()` produces a new solid whose every surface is shifted by `thickness`. Positive values grow the shape outward; negative values shrink it inward.
792
+
793
+ Requires the OCCT backend. Throws on Manifold.
794
+
795
+ **Example**
823
796
 
824
797
  ```ts
825
- fillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape
798
+ // Grow a box outward by 1mm on all sides
799
+ offsetSolid(myBox, 1)
800
+
801
+ // Shrink a shape inward by 0.5mm
802
+ offsetSolid(myShape, -0.5)
826
803
  ```
827
804
 
828
- Apply fillets (rounded edges) to one or more edges of a shape. Works on both straight and curved edges. Supports OCCT and Manifold backends. When using OCCT, all edges are filleted in a single kernel operation for best quality. When using Manifold, edges are filleted sequentially. - EdgeSegment: a single edge from selectEdge() - EdgeSegment[]: multiple edges from selectEdges() - EdgeQuery: inline query (same options as selectEdges) - undefined: all sharp edges on the shape // Fillet all edges fillet(myShape, 2) // Fillet edges at the top fillet(myShape, 1.5, { atZ: 20, convex: true }) // Fillet specific edges const edges = selectEdges(myShape, { parallel: [0, 0, 1] }) fillet(myShape, 3, edges)
805
+ `offsetSolid(shape: Shape, thickness: number): Shape`
806
+
807
+ #### `chamfer2d()` — Bevel a named vertical edge of a shape with a 45° chamfer.
829
808
 
830
- #### `offsetSolid()`
809
+ **Details**
810
+
811
+ Compiler-owned chamfer for tracked vertical edges. Requires a compile-plan-covered target. Supported subset and quadrant semantics are the same as `fillet2d()` — see that function for details.
812
+
813
+ **Example**
831
814
 
832
815
  ```ts
833
- offsetSolid(shape: Shape, thickness: number): Shape
816
+ const b = rectangle(0, 0, 50, 50).extrude(20);
817
+ const chamfered = chamfer2d(b.toShape(), b.edge('vert-br'), 3, [-1, -1]);
834
818
  ```
835
819
 
836
- Uniformly offset all surfaces of a solid inward or outward by a thickness value. Unlike shell(), which hollows a solid, offsetSolid() produces a new solid whose surfaces are all shifted by the given thickness. Positive = outward, negative = inward. Requires the OCCT backend. Throws on Manifold. // Grow a box outward by 1mm on all sides offsetSolid(myBox, 1) // Shrink a shape inward by 0.5mm offsetSolid(myShape, -0.5)
820
+ `chamfer2d(shape: Shape, edge: EdgeRef, size: number, quadrant?: [ number, number ]): Shape`
821
+
822
+ **`EdgeRef`**
823
+ - `query?: EdgeQueryRef` — Compiler-owned edge query when available.
824
+ - Also: `name: EdgeName`
837
825
 
838
- #### `filletCorners()`
826
+ #### `fillet2d()` — Round a named vertical edge of a shape with a circular fillet.
827
+
828
+ **Details**
829
+
830
+ Compiler-owned fillet for tracked vertical edges. Requires a compile-plan-covered target (shapes from `box()`, `rectangle().extrude()`, or rigid transforms of those).
831
+
832
+ **Supported edges:** - Tracked vertical edges from `box()` or `rectangle().extrude()` - Rigid transforms between tracked source and target - Untouched sibling tracked vertical edges after earlier `fillet2d`/`chamfer2d`
833
+
834
+ **Not supported:** edges after shell, hole, cut, trim, difference, intersection, generic sketch extrudes, or tapered extrudes. Use `fillet()` with an `EdgeQuery` for those cases.
835
+
836
+ Canonical quadrants: `vert-bl → [1,-1]`, `vert-br → [-1,-1]`, `vert-tr → [-1,1]`, `vert-tl → [1,1]`
837
+
838
+ **Example**
839
839
 
840
840
  ```ts
841
- filletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch
841
+ const b = rectangle(0, 0, 50, 50).extrude(20);
842
+ const filleted = fillet2d(b.toShape(), b.edge('vert-br'), 5, [-1, -1]);
842
843
  ```
843
844
 
844
- Create a polygon from points with specified corners rounded to arc fillets. Each corner spec identifies a vertex index and radius.
845
+ `fillet2d(shape: Shape, edge: EdgeRef, radius: number, quadrant?: [ number, number ], segments?: number): Shape`
846
+
847
+ #### `filletCorners()` — Create a polygon from points with specific corners rounded to arc fillets.
845
848
 
846
- <details><summary><code>FilletCornerSpec</code></summary>
849
+ **Details**
850
+
851
+ Each corner spec identifies a vertex by its index in the `points` array and the desired fillet `radius`. Both convex and concave corners are supported.
852
+
853
+ Constraints: - Collinear corners cannot be filleted (throws an error) - Two neighboring fillets whose tangent lengths overlap the same edge will throw - Radius must be positive and small enough to fit within the adjacent edge lengths
854
+
855
+ Use `offset(-r).offset(+r)` instead if you want to round **all** convex corners uniformly. Use `filletCorners` when you need selective or mixed sharp/rounded profiles.
856
+
857
+ **Example**
847
858
 
848
859
  ```ts
849
- interface FilletCornerSpec {
850
- index: number;
851
- radius: number;
852
- segments?: number;
853
- }
860
+ const roof = filletCorners(roofPoints, [
861
+ { index: 3, radius: 19 },
862
+ { index: 4, radius: 19 },
863
+ { index: 5, radius: 19 },
864
+ ]);
854
865
  ```
855
866
 
856
- </details>
867
+ `filletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch`
868
+
869
+ `FilletCornerSpec`: `{ index: number, radius: number, segments?: number }`
857
870
 
858
871
  ---
859
872
 
@@ -861,92 +874,93 @@ interface FilletCornerSpec {
861
874
 
862
875
  Duplicate geometry in regular arrangements (linear, circular, mirror).
863
876
 
864
- #### `circularLayout()`
877
+ #### `circularLayout()` — Compute evenly-spaced positions around a circle.
865
878
 
866
- ```ts
867
- circularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[]
868
- ```
869
-
870
- Compute evenly-spaced positions around a circle. Eliminates the most common trig pattern in CAD scripts: ```js // Before — manual trig for (let i = 0; i < 12; i++) { const angle = i * 30 * Math.PI / 180; markers.push(marker.translate(r * Math.cos(angle), r * Math.sin(angle), 0)); } // After — declarative for (const {x, y} of circularLayout(12, r)) { markers.push(marker.translate(x, y, 0)); } ```
879
+ Eliminates the most common trig pattern in CAD scripts:
871
880
 
872
- <details><summary><code>CircularLayoutOptions</code></summary>
881
+ ```js
882
+ // Before — manual trig
883
+ for (let i = 0; i < 12; i++) {
884
+ const angle = i * 30 * Math.PI / 180;
885
+ markers.push(marker.translate(r * Math.cos(angle), r * Math.sin(angle), 0));
886
+ }
873
887
 
874
- ```ts
875
- interface CircularLayoutOptions {
876
- /** Angle of the first element in degrees (default: 0 = +X axis). */
877
- startDeg?: number;
878
- /** Center X coordinate (default: 0). */
879
- centerX?: number;
880
- /** Center Y coordinate (default: 0). */
881
- centerY?: number;
888
+ // After — declarative
889
+ for (const {x, y} of circularLayout(12, r)) {
890
+ markers.push(marker.translate(x, y, 0));
882
891
  }
883
892
  ```
884
893
 
885
- </details>
894
+ `circularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[]`
886
895
 
887
- <details><summary><code>LayoutPoint</code></summary>
896
+ **`CircularLayoutOptions`**
897
+ - `startDeg?: number` — Angle of the first element in degrees (default: 0 = +X axis).
898
+ - `centerX?: number` — Center X coordinate (default: 0).
899
+ - `centerY?: number` — Center Y coordinate (default: 0).
888
900
 
889
- ```ts
890
- interface LayoutPoint {
891
- x: number;
892
- y: number;
893
- }
894
- ```
901
+ `LayoutPoint`: `{ x: number, y: number }`
895
902
 
896
- </details>
903
+ #### `circularPattern()` — Repeat a shape in a circular pattern around an axis and union the copies.
897
904
 
898
- #### `circularPattern()`
905
+ **Details**
899
906
 
900
- ```ts
901
- circularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape
902
- ```
907
+ Distributes `count` copies evenly around the rotation axis (360° / count per step). All copies are unioned into a single `Shape`. Distinct compiler ownership is assigned to each copy — post-merge face identity via owner-scoped canonical queries still works for pattern descendants.
903
908
 
904
- Repeat a shape in a circular pattern around an axis and union the copies. Simple usage (Z axis, matches legacy signature): circularPattern(shape, 6) circularPattern(shape, 6, 10, 20) // centerX=10, centerY=20 Advanced usage (arbitrary axis): circularPattern(shape, 6, { axis: [1, 0, 0], origin: [0, 0, 50] })
909
+ Two calling conventions: - **Simple** (Z axis): `circularPattern(shape, 6)` or `circularPattern(shape, 6, centerX, centerY)` - **Advanced** (arbitrary axis): `circularPattern(shape, 6, { axis, origin })`
905
910
 
906
- <details><summary><code>CircularPatternOptions</code></summary>
911
+ **Example**
907
912
 
908
913
  ```ts
909
- interface CircularPatternOptions {
910
- /** Center X of the rotation (default: 0). Used when axis is Z (legacy mode). */
911
- centerX?: number;
912
- /** Center Y of the rotation (default: 0). Used when axis is Z (legacy mode). */
913
- centerY?: number;
914
- }
914
+ // 8 holes evenly spaced around origin
915
+ circularPattern(cylinder(12, 4).translate(30, 0, -1), 8)
916
+
917
+ // Circular pattern around X axis
918
+ circularPattern(myFeature, 4, { axis: [1, 0, 0], origin: [0, 0, 50] })
915
919
  ```
916
920
 
917
- </details>
921
+ `circularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape`
918
922
 
919
- #### `circularPattern2d()`
923
+ **`CircularPatternOptions`**
924
+ - `centerX?: number` — Center X of the rotation (default: 0). Used when axis is Z (legacy mode).
925
+ - `centerY?: number` — Center Y of the rotation (default: 0). Used when axis is Z (legacy mode).
920
926
 
921
- ```ts
922
- circularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | { centerX?: number; centerY?: number; startDeg?: number; }, centerY?: number): Sketch
923
- ```
927
+ #### `circularPattern2d()` — Repeat a 2D sketch in a circular pattern around a center point and union the copies.
924
928
 
925
- Repeat a sketch in a circular pattern around a center point
929
+ `circularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | { centerX?: number; centerY?: number; startDeg?: number; }, centerY?: number): Sketch`
926
930
 
927
- #### `linearPattern()`
931
+ #### `linearPattern()` — Repeat a shape in a linear pattern along a direction vector and union the copies.
928
932
 
929
- ```ts
930
- linearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape
931
- ```
933
+ **Details**
932
934
 
933
- Repeat a shape in a linear pattern along a direction vector and union the copies.
935
+ Creates `count` copies of `shape`, each offset by `(dx*i, dy*i, dz*i)` from the original. All copies are unioned into a single `Shape`. Distinct compiler ownership is assigned to each copy so face identity via owner-scoped canonical queries still works post-merge.
934
936
 
935
- #### `linearPattern2d()`
937
+ **Example**
936
938
 
937
939
  ```ts
938
- linearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch
940
+ // 5 cylinders, 20mm apart along X
941
+ linearPattern(cylinder(10, 3), 5, 20, 0)
939
942
  ```
940
943
 
941
- Repeat a sketch in a linear pattern
944
+ `linearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape`
945
+
946
+ #### `linearPattern2d()` — Repeat a 2D sketch in a linear pattern and union the copies.
947
+
948
+ `linearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch`
949
+
950
+ #### `mirrorCopy()` — Mirror a shape across a plane and union the mirror with the original.
942
951
 
943
- #### `mirrorCopy()`
952
+ **Details**
953
+
954
+ The mirror plane passes through the origin and is defined by its normal vector. The mirrored copy is unioned with the original to produce a single symmetric Shape.
955
+
956
+ **Example**
944
957
 
945
958
  ```ts
946
- mirrorCopy(shape: Shape, normal: [ number, number, number ]): Shape
959
+ // Mirror across the YZ plane (X=0)
960
+ mirrorCopy(box(50, 30, 10), [1, 0, 0])
947
961
  ```
948
962
 
949
- Mirror a shape across a plane defined by its normal and union the mirror with the original.
963
+ `mirrorCopy(shape: Shape, normal: [ number, number, number ]): Shape`
950
964
 
951
965
  ---
952
966
 
@@ -954,177 +968,190 @@ Mirror a shape across a plane defined by its normal and union the mirror with th
954
968
 
955
969
  Define geometry by relationships and let a solver find positions.
956
970
 
957
- #### `addPolygon()`
971
+ #### `addPolygon()` — Add a general polygon concept to the builder.
958
972
 
959
- ```ts
960
- addPolygon(sk: ConstrainedSketchBuilder, options: PolygonOptions): ConstrainedPolygon
961
- ```
973
+ Creates n vertices and n sides (CCW: `sides[i]` from `vertices[i]` → `vertices[(i+1) % n]`). Applies a `ccw` constraint to enforce winding. All dimensional constraints (lengths, angles, position) are left to the caller.
962
974
 
963
- Add a general polygon concept to the builder. Creates n vertices and n sides (CCW: `sides[i]` from `vertices[i]` → `vertices[(i+1) % n]`). Applies a `ccw` constraint to enforce winding. The user is responsible for all dimensional constraints. ```ts const sk = constrainedSketch(); const tri = addPolygon(sk, { points: [[0,0],[100,0],[50,80]] }); sk.fix(tri.vertex(0), 0, 0); sk.length(tri.side(0), 100); ```
975
+ Use `sk.addPolygon()` as the shorthand builder method.
964
976
 
965
- <details><summary><code>PolygonOptions</code></summary>
977
+ **Example**
966
978
 
967
979
  ```ts
968
- interface PolygonOptions {
969
- /** Whether to register a closed loop for sketch generation. Default: true. */
970
- addLoop?: boolean;
971
- /** Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. */
972
- blockRotation?: boolean;
973
- }
980
+ const sk = constrainedSketch();
981
+ const tri = sk.addPolygon({ points: [[0,0],[100,0],[50,80]] });
982
+ sk.fix(tri.vertex(0), 0, 0);
983
+ sk.length(tri.side(0), 100);
984
+ return sk.solve().extrude(5);
974
985
  ```
975
986
 
976
- </details>
987
+ `addPolygon(sk: ConstrainedSketchBuilder, options: PolygonOptions): ConstrainedPolygon`
977
988
 
978
- <details><summary><code>ConstrainedPolygon</code></summary>
989
+ **`PolygonOptions`**
990
+ - `addLoop?: boolean` — Whether to register a closed loop for sketch generation. Default: true.
991
+ - `blockRotation?: boolean` — Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false.
979
992
 
980
- ```ts
981
- interface ConstrainedPolygon {
982
- /** CCW-ordered PointIds. */
983
- vertices: PointId[];
984
- /** CCW-ordered LineIds. `sides[i]` runs from `vertices[i]` → `vertices[(i+1) % n]`. */
985
- sides: LineId[];
986
- /** ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. */
987
- shape: ShapeId;
988
- }
989
- ```
993
+ **`ConstrainedPolygon`**
994
+ - `vertices: PointId[]` — CCW-ordered PointIds.
995
+ - `sides: LineId[]` — CCW-ordered LineIds. `sides[i]` runs from `vertices[i]` → `vertices[(i+1) % n]`.
996
+ - `shape: ShapeId` — ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`.
997
+
998
+ #### `addRect()` — Add an axis-aligned rectangle concept to the builder.
999
+
1000
+ Creates 4 vertices (CCW: bl→br→tr→tl), 4 sides, 4 structural constraints (`horizontal`/`vertical` on each side), CCW winding, a center point, a loop, and a shape. Returns a `ConstrainedRect` handle with 4 DOF (x, y, width, height).
990
1001
 
991
- </details>
1002
+ Use `sk.rect()` as the shorthand builder method.
992
1003
 
993
- #### `addRect()`
1004
+ **Example**
994
1005
 
995
1006
  ```ts
996
- addRect(sk: ConstrainedSketchBuilder, options?: RectOptions): ConstrainedRect
1007
+ const sk = constrainedSketch();
1008
+ const r = sk.rect({ x: 0, y: 0, width: 100, height: 50 });
1009
+ sk.fix(r.bottomLeft, 0, 0);
1010
+ sk.length(r.bottom, 120); // override initial width
1011
+ return sk.solve().extrude(10);
997
1012
  ```
998
1013
 
999
- Add an axis-aligned rectangle concept to the builder. Creates 4 vertices (CCW: bl→br→tr→tl), 4 sides, applies 4 structural constraints (`horizontal`/`vertical` on each side), enforces CCW winding, registers a loop and a shape, and returns a `ConstrainedRect` handle. ```ts const sk = constrainedSketch(); const rect = addRect(sk, { x: 0, y: 0, width: 100, height: 50 }); sk.fix(rect.bottomLeft, 0, 0); sk.length(rect.bottom, 120); ```
1014
+ `addRect(sk: ConstrainedSketchBuilder, options?: RectOptions): ConstrainedRect`
1000
1015
 
1001
- <details><summary><code>RectOptions</code></summary>
1016
+ **`RectOptions`**
1002
1017
 
1003
- ```ts
1004
- interface RectOptions {
1005
- /** Bottom-left x coordinate. Default: 0. */
1006
- x?: number;
1007
- /** Bottom-left y coordinate. Default: 0. */
1008
- y?: number;
1009
- /** Width (along x). Default: 10. */
1010
- width?: number;
1011
- /** Height (along y). Default: 10. */
1012
- height?: number;
1013
- /** Prevent 180° rotation (ensures bottom edge points rightward). Default: false. */
1014
- blockRotation?: boolean;
1015
- }
1016
- ```
1018
+ | Option | Type | Description |
1019
+ |--------|------|-------------|
1020
+ | `x?` | `number` | Bottom-left x coordinate. Default: 0. |
1021
+ | `y?` | `number` | Bottom-left y coordinate. Default: 0. |
1022
+ | `width?` | `number` | Width (along x). Default: 10. |
1023
+ | `height?` | `number` | Height (along y). Default: 10. |
1024
+ | `blockRotation?` | `boolean` | Prevent 180° rotation (ensures bottom edge points rightward). Default: false. |
1017
1025
 
1018
- </details>
1026
+ **`ConstrainedRect`**
1019
1027
 
1020
- <details><summary><code>ConstrainedRect</code></summary>
1028
+ | Option | Type | Description |
1029
+ |--------|------|-------------|
1030
+ | `bottom` | `LineId` | bottom-left → bottom-right |
1031
+ | `right` | `LineId` | bottom-right → top-right |
1032
+ | `top` | `LineId` | top-right → top-left |
1033
+ | `left` | `LineId` | top-left → bottom-left |
1034
+ | `center` | `PointId` | Center point constrained to the geometric center via `midpoint` on the diagonal. Can be used in further constraints: `sk.fix(rect.center, 0, 0)`, `sk.coincident(rect.center, other)`. |
1035
+ | `shape` | `ShapeId` | ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. |
1036
+ | `bottomLeft`, `bottomRight`, `topRight`, `topLeft` | | — |
1021
1037
 
1022
- ```ts
1023
- interface ConstrainedRect {
1024
- bottomLeft: PointId;
1025
- bottomRight: PointId;
1026
- topRight: PointId;
1027
- topLeft: PointId;
1028
- /** bottom-left → bottom-right */
1029
- bottom: LineId;
1030
- /** bottom-right → top-right */
1031
- right: LineId;
1032
- /** top-right → top-left */
1033
- top: LineId;
1034
- /** top-left → bottom-left */
1035
- left: LineId;
1036
- /** Center point constrained to the geometric center via `midpoint` on the diagonal. Can be used in further constraints: `sk.fix(rect.center, 0, 0)`, `sk.coincident(rect.center, other)`. */
1037
- center: PointId;
1038
- /** ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. */
1039
- shape: ShapeId;
1040
- }
1041
- ```
1038
+ #### `addRegularPolygon()` — Add a regular n-gon concept to the builder.
1042
1039
 
1043
- </details>
1040
+ Vertices are placed at `(cx + r·cos(startAngle + i·2π/n), cy + r·sin(...))`. Equal-radius and equal-side constraints enforce regularity (4 DOF: center x/y, radius, rotation). The center point is tracked by the solver and exposed via the returned handle.
1044
1041
 
1045
- #### `addRegularPolygon()`
1042
+ Use `sk.regularPolygon()` as the shorthand builder method.
1043
+
1044
+ **Example**
1046
1045
 
1047
1046
  ```ts
1048
- addRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions): ConstrainedRegularPolygon
1047
+ const sk = constrainedSketch();
1048
+ const hex = sk.regularPolygon({ sides: 6, radius: 25 });
1049
+ sk.fix(hex.center, 0, 0);
1050
+ sk.length(hex.side(0), 30); // all sides change (equal constraint)
1051
+ return sk.solve().extrude(5);
1049
1052
  ```
1050
1053
 
1051
- Add a regular n-gon concept to the builder. Vertices are placed at `(cx + r·cos(startAngle + i·2π/n), cy + r·sin(...))`. Equal-side constraints enforce regularity. The center point is constrained to the centroid via midpoint constraints on the first diagonal. ```ts const sk = constrainedSketch(); const hex = addRegularPolygon(sk, { sides: 6, radius: 25, cx: 0, cy: 0 }); sk.fix(hex.center, 0, 0); sk.length(hex.side(0), 30); // changes all sides (equal constraint) ```
1054
+ `addRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions): ConstrainedRegularPolygon`
1052
1055
 
1053
- <details><summary><code>RegularPolygonOptions</code></summary>
1056
+ **`RegularPolygonOptions`**
1054
1057
 
1055
- ```ts
1056
- interface RegularPolygonOptions {
1057
- /** Number of sides (minimum 3). */
1058
- sides: number;
1059
- /** Circumradius distance from center to vertex. Default: 10. */
1060
- radius?: number;
1061
- /** Center x coordinate. Default: 0. */
1062
- cx?: number;
1063
- /** Center y coordinate. Default: 0. */
1064
- cy?: number;
1065
- /** Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive). Default: 0 (rightmost vertex). */
1066
- startAngle?: number;
1067
- /** Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. */
1068
- blockRotation?: boolean;
1069
- }
1070
- ```
1058
+ | Option | Type | Description |
1059
+ |--------|------|-------------|
1060
+ | `sides` | `number` | Number of sides (minimum 3). |
1061
+ | `radius?` | `number` | Circumradius — distance from center to vertex. Default: 10. |
1062
+ | `cx?` | `number` | Center x coordinate. Default: 0. |
1063
+ | `cy?` | `number` | Center y coordinate. Default: 0. |
1064
+ | `startAngle?` | `number` | Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive). Default: 0 (rightmost vertex). |
1065
+ | `blockRotation?` | `boolean` | Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. |
1071
1066
 
1072
- </details>
1073
1067
 
1068
+ **`ConstrainedRegularPolygon`** extends ConstrainedPolygon
1069
+ - `center: PointId` — Center point. Use `sk.fix(poly.center, x, y)` to pin location, or `sk.coincident(poly.center, other)` to align with other geometry.
1074
1070
 
1075
- <details><summary><code>ConstrainedRegularPolygon</code> extends ConstrainedPolygon</summary>
1071
+ #### `circle()` — Create an analytic 2D circle for measurement, construction, and extrusion.
1072
+
1073
+ **Example**
1076
1074
 
1077
1075
  ```ts
1078
- interface ConstrainedRegularPolygon extends ConstrainedPolygon {
1079
- /** Center point. Use `sk.fix(poly.center, x, y)` to pin location, or `sk.coincident(poly.center, other)` to align with other geometry. */
1080
- center: PointId;
1081
- }
1076
+ const c = circle(0, 0, 25);
1077
+ c.diameter; c.circumference; c.area;
1078
+ c.pointAtAngle(90); // Point2D at top (90° CCW from +X)
1079
+
1080
+ // Extrude to cylinder with named faces
1081
+ const cyl = c.extrude(30);
1082
+ cyl.face('top'); // FaceRef (planar)
1083
+ cyl.face('side'); // FaceRef (curved)
1084
+
1085
+ Circle2D.fromDiameter(point(0, 0), 50);
1082
1086
  ```
1083
1087
 
1084
- </details>
1088
+ `circle(cx: number, cy: number, radius: number): Circle2D`
1085
1089
 
1086
- #### `circle()`
1090
+ #### `constrainedSketch()` — Create a parametric 2D sketch driven by geometric constraints and a nonlinear solver.
1087
1091
 
1088
- ```ts
1089
- circle(cx: number, cy: number, radius: number): Circle2D
1090
- ```
1092
+ **Workflow**
1091
1093
 
1092
- Create an analytic 2D circle for measurement, construction, and extrusion. Provides diameter, circumference, area, and toSketch().
1094
+ 1. Create a builder with `constrainedSketch()`. 2. Add geometry — points, lines, circles, arcs — using the builder methods. 3. Add constraints (`horizontal`, `length`, `fix`, etc.) to drive the geometry. 4. Call `.solve()` to run the solver and get a `ConstraintSketch` (which extends `Sketch`).
1093
1095
 
1094
- #### `constrainedSketch()`
1096
+ **Example**
1095
1097
 
1096
1098
  ```ts
1097
- constrainedSketch(options?: ConstrainedSketchOptions): ConstrainedSketchBuilder
1099
+ const sk = constrainedSketch();
1100
+ const p1 = sk.point(0, 0);
1101
+ const p2 = sk.point(50, 0);
1102
+ const l1 = sk.line(p1, p2);
1103
+ sk.fix(p1, 0, 0);
1104
+ sk.horizontal(l1);
1105
+ sk.length(l1, 50);
1106
+ return sk.solve().extrude(10);
1098
1107
  ```
1099
1108
 
1100
- Build a parametric 2D sketch with geometric constraints solved by the built-in constraint solver.
1101
-
1102
- <details><summary><code>ConstrainedSketchOptions</code></summary>
1109
+ **Solver status**
1103
1110
 
1104
1111
  ```ts
1105
- interface ConstrainedSketchOptions {
1106
- /** When true, adding a constraint that cannot be satisfied throws instead of silently discarding it. */
1107
- strict?: boolean;
1108
- }
1112
+ const result = sk.solve();
1113
+ result.constraintMeta.status; // 'fully' | 'under' | 'over' | 'over-redundant'
1114
+ result.constraintMeta.dof; // 0 = fully constrained
1115
+ result.constraintMeta.maxError; // residual — should be < 1e-6
1116
+ result.inspect(); // human-readable summary
1117
+ result.withUpdatedConstraint('cst-5', 120); // update a dimension without rebuilding
1109
1118
  ```
1110
1119
 
1111
- </details>
1120
+ `constrainedSketch(options?: ConstrainedSketchOptions): ConstrainedSketchBuilder`
1121
+
1122
+ **`ConstrainedSketchOptions`**
1123
+ - `strict?: boolean` — When true, adding a constraint that cannot be satisfied throws instead of silently discarding it.
1112
1124
 
1113
- #### `line()`
1125
+ #### `line()` — Create an analytic 2D line segment between two points.
1126
+
1127
+ **Example**
1114
1128
 
1115
1129
  ```ts
1116
- line(x1: number, y1: number, x2: number, y2: number): Line2D
1130
+ const l = line(0, 0, 50, 0);
1131
+ l.length; l.midpoint; l.angle; l.direction;
1132
+ l.parallel(10); // parallel line offset 10 (positive = left)
1133
+ l.intersect(l2); // Point2D — treats lines as infinite
1134
+ l.intersectSegment(l2); // Point2D or null — segments only
1135
+
1136
+ Line2D.fromPointAndAngle(point(0, 0), 45, 100);
1137
+ Line2D.fromPointAndDirection(point(0, 0), [1, 1], 50);
1117
1138
  ```
1118
1139
 
1119
- Create an analytic 2D line segment between two points. Provides length, midpoint, angle, intersection, and parallel helpers.
1140
+ `line(x1: number, y1: number, x2: number, y2: number): Line2D`
1141
+
1142
+ #### `point()` — Create an analytic 2D point for measurement and construction geometry.
1120
1143
 
1121
- #### `point()`
1144
+ **Example**
1122
1145
 
1123
1146
  ```ts
1124
- point(x: number, y: number): Point2D
1147
+ const p = point(10, 20);
1148
+ p.distanceTo(point(30, 40)); // Euclidean distance
1149
+ p.midpointTo(point(30, 40)); // midpoint
1150
+ p.translate(5, 5); // new shifted point
1151
+ p.toTuple(); // [10, 20]
1125
1152
  ```
1126
1153
 
1127
- Create an analytic 2D point for measurement and construction geometry.
1154
+ `point(x: number, y: number): Point2D`
1128
1155
 
1129
1156
  ---
1130
1157
 
@@ -1140,167 +1167,155 @@ Position geometry relative to other geometry using semantic anchors.
1140
1167
 
1141
1168
  Compose parts with joints for kinematic simulation.
1142
1169
 
1143
- #### `assembly()`
1170
+ #### `assembly()` — Create an assembly container with named parts and joints for kinematic mechanisms.
1144
1171
 
1145
- ```ts
1146
- assembly(name?: string): Assembly
1147
- ```
1172
+ **Details**
1148
1173
 
1149
- Create an assembly container with named parts and joints for kinematic mechanisms. Build with addPart(), addJoint(), addJointCoupling(), addGearCoupling(), then solve() to get positioned parts. Supports revolute, prismatic, and fixed joint types.
1174
+ An assembly models a mechanism as a directed graph of parts connected by joints. Parts are the nodes; joints are directed edges from parent to child. The graph must be a forest (no cycles). Root parts (those with no incoming joint) are anchored to world space.
1150
1175
 
1151
- #### `bomToCsv()`
1176
+ Three joint types are supported: `'revolute'` (hinge), `'prismatic'` (slider), and `'fixed'` (rigid attachment). Use `addPart()` to add geometry, `addJoint()` (or the shorthands `addRevolute()`, `addPrismatic()`, `addFixed()`) to connect parts, and `solve()` to compute world-space positions at a given joint state.
1152
1177
 
1153
- ```ts
1154
- bomToCsv(rows: BomRow[]): string
1155
- ```
1178
+ The higher-level `connect()` API uses declared **connectors** to compute joint frames automatically. The `match()` API uses typed connectors (with gender and type metadata) for automatic compatibility validation and joint creation.
1156
1179
 
1157
- Convert BOM rows from a solved assembly into a CSV string.
1180
+ For multi-file assemblies, a file that returns an `Assembly` is importable via `require()` and yields an `ImportedAssembly`. Use `mergeInto()` to flatten a sub-assembly into a parent assembly.
1158
1181
 
1159
- <details><summary><code>BomRow</code></summary>
1182
+ **Example**
1160
1183
 
1161
1184
  ```ts
1162
- interface BomRow {
1163
- part: string;
1164
- qty: number;
1165
- material?: string;
1166
- process?: string;
1167
- tolerance?: string;
1168
- notes?: string;
1169
- metadata?: PartMetadata;
1170
- }
1185
+ const mech = assembly("Arm")
1186
+ .addPart("base", box(80, 80, 20, true), {
1187
+ metadata: { material: "PETG", process: "FDM", qty: 1 },
1188
+ })
1189
+ .addPart("link", box(140, 24, 24).translate(0, -12, -12))
1190
+ .addRevolute("shoulder", "base", "link", {
1191
+ axis: [0, 1, 0],
1192
+ min: -30, max: 120, default: 25,
1193
+ frame: Transform.identity().translate(0, 0, 20),
1194
+ });
1195
+
1196
+ return mech; // auto-solved at defaults, renders all parts
1171
1197
  ```
1172
1198
 
1173
- </details>
1199
+ `assembly(name?: string): Assembly`
1174
1200
 
1175
- <details><summary><code>PartMetadata</code></summary>
1201
+ #### `bomToCsv()` — Convert an array of BOM rows into a CSV string.
1176
1202
 
1177
- ```ts
1178
- interface PartMetadata {
1179
- material?: string;
1180
- process?: string;
1181
- tolerance?: string;
1182
- qty?: number;
1183
- notes?: string;
1184
- densityKgM3?: number;
1185
- massKg?: number;
1186
- }
1187
- ```
1203
+ **Details**
1188
1204
 
1189
- </details>
1205
+ Produces a CSV with columns: `part`, `qty`, `material`, `process`, `tolerance`, `notes`. String values are quoted and internal double-quotes are escaped. Prefer calling `solvedAssembly.bomCsv()` directly — this function is exposed for custom BOM processing.
1190
1206
 
1191
- #### `joint()`
1207
+ `bomToCsv(rows: BomRow[]): string`
1192
1208
 
1193
- ```ts
1194
- joint(name: string, shape: Shape, pivot: [ number, number, number ], opts?: RevoluteJointOpts): Shape
1195
- ```
1209
+ **`BomRow`**: `part: string`, `qty: number`, `material?: string`, `process?: string`, `tolerance?: string`, `notes?: string`, `metadata?: PartMetadata`
1196
1210
 
1197
- Create a revolute (hinge) joint. Auto-creates a param slider and rotates the shape.
1211
+ **`PartMetadata`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`
1198
1212
 
1199
- <details><summary><code>RevoluteJointOpts</code></summary>
1213
+ #### `joint()` — Create a revolute joint that auto-generates a parameter slider and rotates the shape.
1200
1214
 
1201
- ```ts
1202
- interface RevoluteJointOpts {
1203
- min?: number;
1204
- max?: number;
1205
- default?: number;
1206
- unit?: string;
1207
- reverse?: boolean;
1208
- }
1209
- ```
1215
+ **Details**
1210
1216
 
1211
- </details>
1217
+ This is a convenience wrapper for single-shape, single-joint use cases. It calls `param()` to create a named angle slider, then applies `rotateAroundAxis()` to the shape. Use the full `Assembly` API for mechanisms with multiple parts and joints.
1212
1218
 
1213
- #### `jointsView()`
1219
+ **Example**
1214
1220
 
1215
1221
  ```ts
1216
- jointsView(options?: JointsViewOptions): void
1222
+ const arm = joint("Shoulder", armShape, [0, 0, 20], {
1223
+ axis: [0, 1, 0],
1224
+ min: -30, max: 120, default: 25,
1225
+ });
1226
+ return arm;
1217
1227
  ```
1218
1228
 
1219
- Configure runtime joint controls that animate object transforms in the viewport without re-running the script.
1229
+ `joint(name: string, shape: Shape, pivot: [ number, number, number ], opts?: RevoluteJointOpts): Shape`
1220
1230
 
1221
- <details><summary><code>JointsViewOptions</code></summary>
1231
+ `RevoluteJointOpts`: `{ min?: number, max?: number, default?: number, unit?: string, reverse?: boolean }`
1222
1232
 
1223
- ```ts
1224
- interface JointsViewOptions {
1225
- enabled?: boolean;
1226
- joints?: JointViewInput[];
1227
- couplings?: JointViewCouplingInput[];
1228
- animations?: JointViewAnimationInput[];
1229
- defaultAnimation?: string;
1230
- }
1231
- ```
1233
+ #### `jointsView()` — Register viewport-only mechanism controls that animate returned objects without re-running the script.
1232
1234
 
1233
- </details>
1235
+ **Details**
1234
1236
 
1235
- <details><summary><code>JointViewInput</code></summary>
1237
+ Defines joints (revolute or prismatic), optional gear/rack couplings, and named animations. The viewport resolves transforms through the joint chain at display time — the script geometry is computed only once at rest pose.
1236
1238
 
1237
- ```ts
1238
- interface JointViewInput {
1239
- name: string;
1240
- child: string;
1241
- parent?: string;
1242
- type?: JointViewType;
1243
- axis?: JointViewAxis;
1244
- min?: number;
1245
- max?: number;
1246
- default?: number;
1247
- unit?: string;
1248
- hidden?: boolean;
1249
- }
1239
+ **Critical:** Solve the assembly at **rest pose** (all animated joints = 0). The viewport applies `jointsView` transforms on top of the returned scene. If geometry is already solved at non-zero angles, animation will double-rotate everything.
1240
+
1241
+ ```js
1242
+ // BAD — double rotation
1243
+ const solved = mech.solve({ shoulder: 45, elbow: 30 });
1244
+ jointsView({ joints: [{ name: 'shoulder', ... }] });
1245
+ return solved;
1246
+
1247
+ // GOOD — rest pose, jointsView controls all posing
1248
+ const solved = mech.solve({ shoulder: 0, elbow: 0 });
1249
+ jointsView({
1250
+ joints: [
1251
+ { name: 'shoulder', child: 'Upper Arm', default: 45, ... },
1252
+ { name: 'elbow', child: 'Forearm', parent: 'Upper Arm', default: 30, ... },
1253
+ ],
1254
+ });
1255
+ return solved;
1250
1256
  ```
1251
1257
 
1252
- </details>
1258
+ **Pivot coordinates** are world-space positions of each joint origin at rest pose. For `addRevolute('shoulder', 'Base', 'Link', { frame: Transform.identity().translate(0, 0, 20) })` where "Base" is at world origin, the pivot is `[0, 0, 20]`.
1253
1259
 
1254
- <details><summary><code>JointViewCouplingInput</code></summary>
1260
+ **Fixed attachments** that must follow a parent during animation need a zero-angle revolute joint in the chain:
1255
1261
 
1256
- ```ts
1257
- interface JointViewCouplingInput {
1258
- joint: string;
1259
- terms: JointViewCouplingTermInput[];
1260
- offset?: number;
1261
- }
1262
+ ```js
1263
+ { name: 'EE_Follow', child: 'End Effector', parent: 'Last Link',
1264
+ type: 'revolute', axis: [0, 0, 1], pivot: [linkLength, 0, 0],
1265
+ min: 0, max: 0, default: 0 }
1262
1266
  ```
1263
1267
 
1264
- </details>
1268
+ Animation values are interpolated linearly between keyframes. ForgeCAD does **not** auto-wrap revolute values across `-180/180`. Keep keyframe values continuous — a `-180 -> 171` jump spins the part the long way around. Use `-180 -> -189` instead. Author high-speed multi-turn joints as accumulating angles (`0, 360, 720, ...`) with `continuous: true`.
1265
1269
 
1266
- <details><summary><code>JointViewCouplingTermInput</code></summary>
1270
+ **Tick-based keyframes:** Omit `at` from all keyframes to auto-distribute by tick weight:
1267
1271
 
1268
- ```ts
1269
- interface JointViewCouplingTermInput {
1270
- joint: string;
1271
- ratio?: number;
1272
- }
1272
+ ```js
1273
+ keyframes: [
1274
+ { ticks: 3, values: { Shoulder: 20 } }, // slow segment (3x weight)
1275
+ { ticks: 1, values: { Shoulder: -10 } }, // fast segment (1x weight)
1276
+ { values: { Shoulder: 20 } }, // last keyframe; ticks ignored
1277
+ ]
1278
+ // positions: 0, 0.75, 1.0
1273
1279
  ```
1274
1280
 
1275
- </details>
1281
+ Mixing explicit `at` and omitted `at` in the same animation is not allowed.
1276
1282
 
1277
- <details><summary><code>JointViewAnimationInput</code></summary>
1283
+ **Example**
1278
1284
 
1279
- ```ts
1280
- interface JointViewAnimationInput {
1281
- name: string;
1282
- duration?: number;
1283
- loop?: boolean;
1284
- continuous?: boolean;
1285
- keyframes: JointViewAnimationKeyframeInput[];
1286
- }
1285
+ ```js
1286
+ jointsView({
1287
+ joints: [{
1288
+ name: 'Shoulder', child: 'Upper Arm', parent: 'Base',
1289
+ type: 'revolute', axis: [0, -1, 0], pivot: [0, 0, 46],
1290
+ min: -30, max: 110, default: 15,
1291
+ }],
1292
+ animations: [{
1293
+ name: 'Walk Cycle', duration: 1.6, loop: true,
1294
+ keyframes: [
1295
+ { values: { Shoulder: 20 } },
1296
+ { values: { Shoulder: -10 } },
1297
+ { values: { Shoulder: 20 } },
1298
+ ],
1299
+ }],
1300
+ });
1287
1301
  ```
1288
1302
 
1289
- </details>
1303
+ `jointsView(options?: JointsViewOptions): void`
1290
1304
 
1291
- <details><summary><code>JointViewAnimationKeyframeInput</code></summary>
1305
+ **`JointsViewOptions`**: `enabled?: boolean`, `joints?: JointViewInput[]`, `couplings?: JointViewCouplingInput[]`, `animations?: JointViewAnimationInput[]`, `defaultAnimation?: string`
1292
1306
 
1293
- ```ts
1294
- interface JointViewAnimationKeyframeInput {
1295
- /** Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights. */
1296
- at?: number;
1297
- /** Relative weight of the segment from this keyframe to the next (default 1). Only used in tick-based mode (when `at` is omitted). Last keyframe's ticks value is ignored. */
1298
- ticks?: number;
1299
- values: Record<string, number>;
1300
- }
1301
- ```
1307
+ **`JointViewInput`**: `name: string`, `child: string`, `parent?: string`, `type?: JointViewType`, `axis?: JointViewAxis`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `hidden?: boolean`
1308
+
1309
+ `JointViewCouplingInput`: `{ joint: string, terms: JointViewCouplingTermInput[], offset?: number }`
1310
+
1311
+ `JointViewCouplingTermInput`: `{ joint: string, ratio?: number }`
1312
+
1313
+ `JointViewAnimationInput`: `{ name: string, duration?: number, loop?: boolean, continuous?: boolean, keyframes: JointViewAnimationKeyframeInput[] }`
1302
1314
 
1303
- </details>
1315
+ **`JointViewAnimationKeyframeInput`**
1316
+ - `at?: number` — Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights.
1317
+ - `ticks?: number` — Relative weight of the segment from this keyframe to the next (default 1). Only used in tick-based mode (when `at` is omitted). Last keyframe's ticks value is ignored.
1318
+ - Also: `values: Record<string, number>`
1304
1319
 
1305
1320
  ---
1306
1321
 
@@ -1308,784 +1323,590 @@ interface JointViewAnimationKeyframeInput {
1308
1323
 
1309
1324
  Declare user-facing controls that drive model geometry.
1310
1325
 
1311
- #### `boolParam()`
1326
+ #### `boolParam()` — Declare a boolean parameter that renders as a checkbox in the UI.
1312
1327
 
1313
- ```ts
1314
- boolParam(name: string, defaultValue: boolean): boolean
1315
- ```
1328
+ **Details**
1316
1329
 
1317
- Declare a boolean parameter. Returns the current boolean value. Renders as a checkbox in the UI.
1330
+ Internally stored as `0`/`1`. When overriding from CLI or `require()`, pass `1` for true and `0` for false. The `name` string is the override key.
1318
1331
 
1319
- #### `choiceParam()`
1332
+ **Example**
1320
1333
 
1321
1334
  ```ts
1322
- choiceParam(name: string, defaultValue: string, choices: string[]): string
1335
+ const showHoles = boolParam("Show Holes", true);
1336
+ if (showHoles) return difference(plate, cylinder(10, 5).translate(50, 30, 0));
1337
+ return plate;
1323
1338
  ```
1324
1339
 
1325
- Declare a choice parameter. Returns the selected string label. Renders as a dropdown in the UI. `defaultValue` must match one of the supplied `choices`. Overrides may be passed either as the choice label or as a numeric index, but labels are preferred because they are clearer in CLI/import usage.
1326
-
1327
- #### `listParam()`
1340
+ Override via import:
1328
1341
 
1329
1342
  ```ts
1330
- listParam<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: { ... }): T[]
1343
+ const pan = require("./pan.forge.js", { "Show Lid": 0 });
1331
1344
  ```
1332
1345
 
1333
- Declare a list parameter — an array of struct items with per-field controls. Returns the current list of items (with overrides applied). Each item is an object whose fields match the keys in defaultItems. Boolean fields (marked with `boolean: true` in field defs) return as booleans. Choice fields (marked with `choices: [...]` in field defs) return as strings. All other fields return as numbers.
1346
+ `boolParam(name: string, defaultValue: boolean): boolean`
1334
1347
 
1335
- <details><summary><code>ListParamFieldDef</code></summary>
1348
+ #### `choiceParam()` — Declare a choice parameter that renders as a dropdown in the UI.
1336
1349
 
1337
- ```ts
1338
- interface ListParamFieldDef {
1339
- min?: number;
1340
- max?: number;
1341
- step?: number;
1342
- unit?: string;
1343
- integer?: boolean;
1344
- boolean?: boolean;
1345
- choices?: string[];
1346
- }
1347
- ```
1350
+ **Details**
1351
+
1352
+ `defaultValue` must exactly match one entry in `choices`. Returns the selected string label. Prefer `choiceParam` over `param` when a slider would hide intent — named choices like `"wok"` are self-describing.
1348
1353
 
1349
- </details>
1354
+ Overrides may be passed as the choice label string (preferred) or as a numeric index. The `name` string is the override key.
1350
1355
 
1351
- #### `param()`
1356
+ **Example**
1352
1357
 
1353
1358
  ```ts
1354
- param(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number
1359
+ const panStyle = choiceParam("Pan Style", "frying-pan", ["frying-pan", "saute-pan", "wok"]);
1360
+ if (panStyle === "wok") return buildWok();
1355
1361
  ```
1356
1362
 
1357
- Declare a parameter. Returns the current value (default or overridden). Each call registers the param for UI generation.
1358
-
1359
- #### `dim()`
1363
+ Override via import:
1360
1364
 
1361
1365
  ```ts
1362
- dim(from: PointArg$1, to: PointArg$1, opts?: DimOpts): void
1366
+ const pan = require("./pan.forge.js", { "Pan Style": "wok" });
1363
1367
  ```
1364
1368
 
1365
- Add a dimension annotation between two points.
1366
-
1367
- <details><summary><code>DimOpts</code></summary>
1369
+ Override via CLI:
1368
1370
 
1369
- ```ts
1370
- interface DimOpts {
1371
- offset?: number;
1372
- label?: string;
1373
- color?: string;
1374
- component?: string | string[];
1375
- currentComponent?: boolean;
1376
- }
1371
+ ```bash
1372
+ forgecad run model.forge.js --param "Pan Style=wok"
1377
1373
  ```
1378
1374
 
1379
- </details>
1375
+ `choiceParam(name: string, defaultValue: string, choices: string[]): string`
1380
1376
 
1381
- #### `dimLine()`
1377
+ #### `listParam()` — Declare a list parameter — an array of struct items with per-field UI controls.
1382
1378
 
1383
- ```ts
1384
- dimLine(l: Line2D, opts?: DimOpts): void
1385
- ```
1379
+ **Details**
1386
1380
 
1387
- Add a dimension annotation along a Line2D.
1381
+ Each item in the list is a struct whose fields each render as their own control (slider, checkbox, or dropdown). The user can add/remove rows up to `minItems`/`maxItems` bounds.
1388
1382
 
1389
- ---
1383
+ Field types: - Boolean fields (`boolean: true` in field defs) return as `boolean` - Choice fields (`choices: [...]` in field defs) return as `string` - All other fields return as `number`
1390
1384
 
1391
- ## C12: Dimensional Demotion
1385
+ `listParam<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: { ... }): T[]`
1392
1386
 
1393
- Extract 2D geometry from a 3D solid (section, projection).
1387
+ `ListParamFieldDef`: `{ min?: number, max?: number, step?: number, unit?: string, integer?: boolean, boolean?: boolean, choices?: string[] }`
1394
1388
 
1395
- #### `faceProfile()`
1389
+ #### `param()` — Declare a numeric parameter that renders as a slider in the UI.
1396
1390
 
1397
- ```ts
1398
- faceProfile(shape: Shape, face: FaceSelector): Sketch
1399
- ```
1391
+ **Details**
1400
1392
 
1401
- #### `intersectWithPlane()`
1393
+ Each `param()` call registers a slider control. When the user moves the slider the entire script re-executes with the new value. Parameter values are also overridable from `require()` imports or the CLI `--param` flag — the `name` string is the key used in both cases.
1402
1394
 
1403
- ```ts
1404
- intersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch
1405
- ```
1395
+ Default range rules when options are omitted: - `min` defaults to `0` - `max` defaults to `defaultValue * 4` - `step` is auto-calculated: `1` for integer params, `0.1` for ranges ≤ 100, `1` for larger ranges
1406
1396
 
1407
- Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch.
1397
+ The `unit` option is cosmetic only no conversion is performed. Use `integer: true` for counts, sides, quantities (rounds to whole numbers; step defaults to `1`).
1408
1398
 
1409
- #### `projectToPlane()`
1399
+ **Example**
1410
1400
 
1411
1401
  ```ts
1412
- projectToPlane(shape: Shape, plane: PlaneSpec): Sketch
1402
+ const width = param("Width", 50);
1403
+ const angle = param("Angle", 45, { min: 0, max: 180, unit: "°" });
1404
+ const sides = param("Sides", 6, { min: 3, max: 12, integer: true });
1413
1405
  ```
1414
1406
 
1415
- Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch.
1407
+ **Parameter overrides** key must match `name` exactly:
1416
1408
 
1417
- ---
1409
+ ```ts
1410
+ // Via require()
1411
+ const bracket = require("./bracket.forge.js", { Width: 80 });
1418
1412
 
1419
- ## C13: Export & Output
1413
+ // Via CLI
1414
+ // forgecad run model.forge.js --param "Wall Thickness=3"
1415
+ ```
1420
1416
 
1421
- Convert geometry to external formats (STL, 3MF, SVG, DXF, G-code, PDF).
1417
+ `param(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number`
1422
1418
 
1423
- #### `bom()`
1419
+ #### `dim()` — Add a dimension annotation between two points.
1424
1420
 
1425
- ```ts
1426
- bom(quantity: number, description: string, opts?: BomOpts): void
1427
- ```
1421
+ **Details**
1428
1422
 
1429
- Add a bill-of-materials entry.
1423
+ Dimension annotations are purely visual callouts rendered in the viewport and report export. They do not affect geometry or constrain the model.
1430
1424
 
1431
- <details><summary><code>BomOpts</code></summary>
1425
+ Point arguments accept 2D tuples `[x, y]`, 3D tuples `[x, y, z]`, or `Point2D` objects (Z is treated as 0 for 2D inputs).
1432
1426
 
1433
- ```ts
1434
- interface BomOpts {
1435
- /** Quantity unit label, e.g. "mm", "pieces", "kg". Default: "pieces" */
1436
- unit?: string;
1437
- /** Optional explicit grouping key used during report aggregation. */
1438
- key?: string;
1439
- }
1440
- ```
1427
+ **Ownership Rules (Report Pages)**
1441
1428
 
1442
- </details>
1429
+ - `currentComponent: true` — deterministic ownership by the calling import instance. Use when authoring reusable imported parts. - `component: "Part Name"` — route dimension to another named returned object. - Multiple owners: dimension is shared and appears on the assembly overview page. - No ownership set: report export infers ownership via endpoint-in-bbox.
1443
1430
 
1444
- #### `robotExport()`
1431
+ **Example**
1445
1432
 
1446
1433
  ```ts
1447
- robotExport(options: RobotExportOptions): CollectedRobotExport
1434
+ dim([-w / 2, 0, 0], [w / 2, 0, 0], { label: "Width" });
1435
+ dim([0, 0, -h / 2], [0, 0, h / 2], { label: "Height", offset: 14 });
1436
+ dim([0, 0, 0], [100, 0, 0], { component: "Base", color: "#00AAFF" });
1448
1437
  ```
1449
1438
 
1450
- Declare that the current script should export an assembly as a robot package for the SDF CLI. Configures inertial properties, joint limits, and optional plugins (e.g. diff-drive for Gazebo).
1451
-
1452
- <details><summary><code>RobotExportOptions</code></summary>
1439
+ `component` (string or string[] report ownership), `currentComponent` (boolean)
1453
1440
 
1454
- ```ts
1455
- interface RobotExportOptions {
1456
- assembly: Assembly;
1457
- modelName?: string;
1458
- state?: JointState;
1459
- static?: boolean;
1460
- selfCollide?: boolean;
1461
- allowAutoDisable?: boolean;
1462
- links?: Record<string, RobotLinkExportOptions>;
1463
- joints?: Record<string, RobotJointExportOptions>;
1464
- diffDrive?: RobotDiffDrivePluginOptions;
1465
- jointStatePublisher?: RobotJointStatePublisherOptions;
1466
- world?: RobotWorldOptions;
1467
- }
1468
- ```
1441
+ `dim(from: PointArg$1, to: PointArg$1, opts?: DimOpts): void`
1469
1442
 
1470
- </details>
1443
+ `DimOpts`: `{ offset?: number, label?: string, color?: string, component?: string | string[], currentComponent?: boolean }`
1471
1444
 
1472
- <details><summary><code>RobotLinkExportOptions</code></summary>
1445
+ #### `dimLine()` — Add a dimension annotation along a `Line2D`.
1473
1446
 
1474
- ```ts
1475
- interface RobotLinkExportOptions {
1476
- massKg?: number;
1477
- densityKgM3?: number;
1478
- collision?: "visual" | "convex" | "box" | "none";
1479
- }
1480
- ```
1447
+ **Details**
1481
1448
 
1482
- </details>
1449
+ Convenience wrapper around { points from a constrained-sketch `Line2D` entity. All `opts` are forwarded unchanged.
1483
1450
 
1484
- <details><summary><code>RobotJointExportOptions</code></summary>
1451
+ **Example**
1485
1452
 
1486
1453
  ```ts
1487
- interface RobotJointExportOptions {
1488
- effort?: number;
1489
- velocity?: number;
1490
- damping?: number;
1491
- friction?: number;
1492
- }
1454
+ const a = point(0, 0);
1455
+ const b = point(100, 0);
1456
+ dimLine(line(a, b), { label: "Span", offset: -8 });
1493
1457
  ```
1494
1458
 
1495
- </details>
1459
+ `dimLine(l: Line2D, opts?: DimOpts): void`
1496
1460
 
1497
- <details><summary><code>RobotDiffDrivePluginOptions</code></summary>
1461
+ ---
1498
1462
 
1499
- ```ts
1500
- interface RobotDiffDrivePluginOptions {
1501
- leftJoints: string[];
1502
- rightJoints: string[];
1503
- wheelSeparationMm: number;
1504
- wheelRadiusMm: number;
1505
- topic?: string;
1506
- odomTopic?: string;
1507
- tfTopic?: string;
1508
- frameId?: string;
1509
- odomFrameId?: string;
1510
- maxLinearVelocity?: number;
1511
- maxAngularVelocity?: number;
1512
- linearAcceleration?: number;
1513
- angularAcceleration?: number;
1514
- }
1515
- ```
1463
+ ## C12: Dimensional Demotion
1516
1464
 
1517
- </details>
1465
+ Extract 2D geometry from a 3D solid (section, projection).
1518
1466
 
1519
- <details><summary><code>RobotJointStatePublisherOptions</code></summary>
1467
+ #### `faceProfile()` — Extract the boundary profile of a named face as a 2D sketch.
1520
1468
 
1521
- ```ts
1522
- interface RobotJointStatePublisherOptions {
1523
- enabled?: boolean;
1524
- joints?: string[];
1525
- topic?: string;
1526
- updateRate?: number;
1527
- }
1528
- ```
1469
+ The result is returned in the face's local 2D coordinate system, making it convenient for offsets, pocket profiles, or follow-up sketch operations driven by an existing face.
1529
1470
 
1530
- </details>
1471
+ `faceProfile(shape: Shape, face: FaceSelector): Sketch`
1531
1472
 
1532
- <details><summary><code>RobotWorldOptions</code></summary>
1473
+ #### `intersectWithPlane()` — Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch.
1533
1474
 
1534
- ```ts
1535
- interface RobotWorldOptions {
1536
- name?: string;
1537
- generateDemoWorld?: boolean;
1538
- spawnPose?: RobotPose6;
1539
- keyboardTeleop?: RobotWorldKeyboardTeleopOptions;
1540
- }
1541
- ```
1475
+ `intersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch`
1542
1476
 
1543
- </details>
1477
+ #### `projectToPlane()` — Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch.
1544
1478
 
1545
- <details><summary><code>RobotWorldKeyboardTeleopOptions</code></summary>
1479
+ `projectToPlane(shape: Shape, plane: PlaneSpec): Sketch`
1546
1480
 
1547
- ```ts
1548
- interface RobotWorldKeyboardTeleopOptions {
1549
- enabled?: boolean;
1550
- linearStep?: number;
1551
- angularStep?: number;
1552
- }
1553
- ```
1481
+ ---
1554
1482
 
1555
- </details>
1483
+ ## C13: Export & Output
1556
1484
 
1557
- <details><summary><code>CollectedRobotExport</code></summary>
1485
+ Convert geometry to external formats (STL, 3MF, SVG, DXF, G-code, PDF).
1558
1486
 
1559
- ```ts
1560
- interface CollectedRobotExport {
1561
- modelName: string;
1562
- assembly: AssemblyDefinition;
1563
- state: JointState;
1564
- static: boolean;
1565
- selfCollide: boolean;
1566
- allowAutoDisable: boolean;
1567
- links: Record<string, RobotLinkExportOptions>;
1568
- joints: Record<string, RobotJointExportOptions>;
1569
- diffDrive?: RobotDiffDrivePluginOptions;
1570
- jointStatePublisher?: RobotJointStatePublisherOptions;
1571
- world: RobotWorldOptions | null;
1572
- }
1573
- ```
1487
+ #### `bom()` — Register a Bill of Materials entry for report export.
1488
+
1489
+ **Details**
1490
+
1491
+ BOM entries are accumulated during script execution and exported alongside the model in report views. Rows are grouped by normalized `description + unit`. Pass an explicit `key` to force multiple descriptions to collapse into a single line item.
1574
1492
 
1575
- </details>
1493
+ - `quantity` must be a finite number `>= 0`. A quantity of `0` is silently ignored (useful for conditional scripting with `param()`-driven counts). - `unit` defaults to `"pieces"` when omitted or empty. - The assembly `solved.bom()` / `solved.bomCsv()` API is separate and covers per-part assembly metadata; this function is for free-form purchased-item annotation.
1576
1494
 
1577
- <details><summary><code>AssemblyDefinition</code></summary>
1495
+ **Example**
1578
1496
 
1579
1497
  ```ts
1580
- interface AssemblyDefinition {
1581
- name: string;
1582
- parts: AssemblyPartDef[];
1583
- joints: AssemblyJointDef[];
1584
- jointCouplings: AssemblyJointCouplingDef[];
1585
- }
1498
+ const tubeLen = param("Tube Length", 1200, { min: 300, max: 4000, unit: "mm" });
1499
+ const boltCount = param("Bolt Count", 16, { min: 0, max: 200, integer: true });
1500
+
1501
+ bom(tubeLen, "iron tube 30 x 20", { unit: "mm" });
1502
+ bom(boltCount, "M4 bolt, 16 mm length");
1503
+ bom(4, "rubber foot", { key: "foot-rubber" }); // explicit aggregation key
1586
1504
  ```
1587
1505
 
1588
- </details>
1506
+ `bom(quantity: number, description: string, opts?: BomOpts): void`
1589
1507
 
1590
- <details><summary><code>AssemblyPartDef</code></summary>
1508
+ **`BomOpts`**
1509
+ - `unit?: string` — Quantity unit label, e.g. "mm", "pieces", "kg". Default: "pieces"
1510
+ - `key?: string` — Optional explicit grouping key used during report aggregation.
1591
1511
 
1592
- ```ts
1593
- interface AssemblyPartDef {
1594
- name: string;
1595
- part: AssemblyPart;
1596
- base: Transform;
1597
- metadata?: PartMetadata;
1598
- }
1599
- ```
1512
+ #### `robotExport()` — Declare that this script should export the assembly as a SDF/URDF robot package.
1600
1513
 
1601
- </details>
1514
+ **Details**
1602
1515
 
1603
- <details><summary><code>PartMetadata</code></summary>
1516
+ Call `robotExport()` alongside your assembly definition. The CLI commands `forgecad export sdf` and `forgecad export urdf` pick up the declaration and produce a robot package with: - Mesh-based inertia tensors (full 6-component, not bounding-box approximations) - Separate collision meshes (convex hull by default — ~50–80% smaller) - Joint mimic elements derived from `addJointCoupling` / `addGearCoupling`
1604
1517
 
1605
- ```ts
1606
- interface PartMetadata {
1607
- material?: string;
1608
- process?: string;
1609
- tolerance?: string;
1610
- qty?: number;
1611
- notes?: string;
1612
- densityKgM3?: number;
1613
- massKg?: number;
1614
- }
1615
- ```
1518
+ **Collision mesh modes** (set per-link via `links["PartName"].collision`):
1616
1519
 
1617
- </details>
1520
+ | Mode | Description | Default | |------|-------------|---------| | `'convex'` | Convex hull (separate `_collision.stl`) | Yes | | `'box'` | AABB primitive — fastest physics | | | `'visual'` | Same mesh as visual — exact but slow | | | `'none'` | No collision geometry | |
1618
1521
 
1619
- <details><summary><code>AssemblyJointDef</code></summary>
1522
+ **Unit conventions:** - Revolute `velocity` is in degrees/second in Forge; exporters convert to rad/s. - Prismatic distances are in mm in Forge; exported in meters. - `massKg` is preferred; `densityKgM3` is used when mass is unknown. - Couplings with multiple terms: only the primary term (largest ratio) maps to `<mimic>` — SDF/URDF support single-leader mimic only. Dropped terms emit a warning.
1523
+
1524
+ **Example**
1620
1525
 
1621
1526
  ```ts
1622
- interface AssemblyJointDef {
1623
- name: string;
1624
- type: JointType;
1625
- parent: string;
1626
- child: string;
1627
- frame: Transform;
1628
- axis: Vec3;
1629
- min?: number;
1630
- max?: number;
1631
- defaultValue: number;
1632
- unit?: string;
1633
- effort?: number;
1634
- velocity?: number;
1635
- damping?: number;
1636
- friction?: number;
1637
- }
1638
- ```
1527
+ const rover = assembly("Scout")
1528
+ .addPart("Chassis", box(300, 220, 50, true))
1529
+ .addPart("Left Wheel", cylinder(30, 60, undefined, 48, true))
1530
+ .addRevolute("leftWheel", "Chassis", "Left Wheel", {
1531
+ axis: [0, 1, 0],
1532
+ frame: Transform.identity().translate(90, 140, 60),
1533
+ effort: 20, velocity: 1080,
1534
+ });
1639
1535
 
1640
- </details>
1536
+ robotExport({
1537
+ assembly: rover,
1538
+ modelName: "Scout",
1539
+ links: {
1540
+ Chassis: { massKg: 10 },
1541
+ "Left Wheel": { massKg: 0.8 },
1542
+ },
1543
+ plugins: {
1544
+ diffDrive: {
1545
+ leftJoints: ["leftWheel"], rightJoints: ["rightWheel"],
1546
+ wheelSeparationMm: 280, wheelRadiusMm: 60,
1547
+ },
1548
+ },
1549
+ world: { generateDemoWorld: true },
1550
+ });
1551
+ ```
1641
1552
 
1642
- <details><summary><code>AssemblyJointCouplingDef</code></summary>
1553
+ **CLI usage**
1643
1554
 
1644
- ```ts
1645
- interface AssemblyJointCouplingDef {
1646
- joint: string;
1647
- terms: JointCouplingTermRecord[];
1648
- offset: number;
1649
- }
1555
+ ```bash
1556
+ forgecad export sdf model.forge.js # SDF package (Gazebo/Ignition)
1557
+ forgecad export urdf model.forge.js # URDF package (ROS/PyBullet/MuJoCo)
1650
1558
  ```
1651
1559
 
1652
- </details>
1560
+ `robotExport(options: RobotExportOptions): CollectedRobotExport`
1653
1561
 
1654
- <details><summary><code>JointCouplingTermRecord</code></summary>
1562
+ **`RobotExportOptions`**: `assembly: Assembly`, `modelName?: string`, `state?: JointState`, `static?: boolean`, `selfCollide?: boolean`, `allowAutoDisable?: boolean`, `links?: Record<string, RobotLinkExportOptions>`, `joints?: Record<string, RobotJointExportOptions>`, `diffDrive?: RobotDiffDrivePluginOptions`, `jointStatePublisher?: RobotJointStatePublisherOptions`, `world?: RobotWorldOptions`
1655
1563
 
1656
- ```ts
1657
- interface JointCouplingTermRecord {
1658
- joint: string;
1659
- ratio: number;
1660
- }
1661
- ```
1564
+ `RobotLinkExportOptions`: `{ massKg?: number, densityKgM3?: number, collision?: "visual" | "convex" | "box" | "none" }`
1662
1565
 
1663
- </details>
1566
+ `RobotJointExportOptions`: `{ effort?: number, velocity?: number, damping?: number, friction?: number }`
1664
1567
 
1665
- #### `sheetMetal()`
1568
+ **`RobotDiffDrivePluginOptions`**: `leftJoints: string[]`, `rightJoints: string[]`, `wheelSeparationMm: number`, `wheelRadiusMm: number`, `topic?: string`, `odomTopic?: string`, `tfTopic?: string`, `frameId?: string`, `odomFrameId?: string`, `maxLinearVelocity?: number`, `maxAngularVelocity?: number`, `linearAcceleration?: number`, `angularAcceleration?: number`
1666
1569
 
1667
- ```ts
1668
- sheetMetal(options: SheetMetalOptions): SheetMetalPart
1669
- ```
1570
+ `RobotJointStatePublisherOptions`: `{ enabled?: boolean, joints?: string[], topic?: string, updateRate?: number }`
1670
1571
 
1671
- Create a sheet-metal part with flanges, bend allowances, and flat pattern unfolding. Define the base panel, thickness, bend radius, and K-factor, then chain .flange() and .cutout() calls. Materialize with .folded() or .flatPattern().
1572
+ `RobotWorldOptions`: `{ name?: string, generateDemoWorld?: boolean, spawnPose?: RobotPose6, keyboardTeleop?: RobotWorldKeyboardTeleopOptions }`
1672
1573
 
1673
- <details><summary><code>SheetMetalOptions</code></summary>
1574
+ `RobotWorldKeyboardTeleopOptions`: `{ enabled?: boolean, linearStep?: number, angularStep?: number }`
1674
1575
 
1675
- ```ts
1676
- interface SheetMetalOptions {
1677
- width: number;
1678
- height: number;
1679
- thickness: number;
1680
- bendRadius: number;
1681
- kFactor: number;
1682
- kind?: "rect";
1683
- size: number;
1684
- }
1685
- ```
1576
+ **`CollectedRobotExport`**: `modelName: string`, `assembly: AssemblyDefinition`, `state: JointState`, `static: boolean`, `selfCollide: boolean`, `allowAutoDisable: boolean`, `links: Record<string, RobotLinkExportOptions>`, `joints: Record<string, RobotJointExportOptions>`, `diffDrive?: RobotDiffDrivePluginOptions`, `jointStatePublisher?: RobotJointStatePublisherOptions`, `world: RobotWorldOptions | null`
1686
1577
 
1687
- </details>
1578
+ `AssemblyDefinition`: `{ name: string, parts: AssemblyPartDef[], joints: AssemblyJointDef[], jointCouplings: AssemblyJointCouplingDef[] }`
1688
1579
 
1689
- #### `sketchToDxf()`
1580
+ `AssemblyPartDef`: `{ name: string, part: AssemblyPart, base: Transform, metadata?: PartMetadata }`
1690
1581
 
1691
- ```ts
1692
- sketchToDxf(sketch: Sketch, options?: SketchDxfOptions): string
1693
- ```
1582
+ **`PartMetadata`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`
1694
1583
 
1695
- Export a 2D sketch as a DXF string (R12/AC1009 maximally compatible). For regular sketches, each polygon loop becomes a closed LWPOLYLINE. For constraint sketches, exports LINE, CIRCLE, and ARC entities from the constraint edge geometry.
1584
+ **`AssemblyJointDef`**: `name: string`, `type: JointType`, `parent: string`, `child: string`, `frame: Transform`, `axis: Vec3`, `min?: number`, `max?: number`, `defaultValue: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`
1696
1585
 
1697
- <details><summary><code>SketchDxfOptions</code></summary>
1586
+ `AssemblyJointCouplingDef`: `{ joint: string, terms: JointCouplingTermRecord[], offset: number }`
1698
1587
 
1699
- ```ts
1700
- interface SketchDxfOptions {
1701
- /** DXF layer name. Default: "0" */
1702
- layer?: string;
1703
- /** DXF color index (1–255, AutoCAD ACI). Default: 7 (white/black) */
1704
- colorIndex?: number;
1705
- }
1706
- ```
1588
+ `JointCouplingTermRecord`: `{ joint: string, ratio: number }`
1707
1589
 
1708
- </details>
1590
+ #### `sheetMetal()` — Create a parametric sheet metal part with flanges, bend allowances, and flat-pattern unfolding.
1709
1591
 
1710
- #### `sketchToSvg()`
1592
+ **Details**
1711
1593
 
1712
- ```ts
1713
- sketchToSvg(sketch: Sketch, options?: SketchSvgOptions): string
1714
- ```
1594
+ `sheetMetal()` keeps one semantic model and derives both a folded 3D solid and an accurate flat pattern from it. The K-factor bend allowance is applied during unfolding. This is a strict v1 subset — it does not infer sheet metal from arbitrary solids.
1715
1595
 
1716
- Export a 2D sketch as an SVG string. For regular sketches, exports filled polygon regions. For constraint sketches, exports line/arc/circle edge geometry. The SVG uses the sketch's native coordinate system (Y-up) with a transform that flips Y so the output renders correctly in SVG's Y-down space. Coordinates are in sketch units (typically mm).
1596
+ **Recommended authoring order:** 1. Define the base panel + thickness + bend parameters. 2. Chain `.flange()` calls for each edge. Validate with `.folded()` and `.flatPattern()` before adding cutouts. 3. Add panel cutouts, then flange cutouts one region at a time. 4. Validate after each new cutout region.
1717
1597
 
1718
- <details><summary><code>SketchSvgOptions</code></summary>
1598
+ **v1 limitations:** one base panel, up to four 90° edge flanges, constant thickness, explicit K-factor, rectangular corner reliefs, planar cutouts only. No hems, jogs, lofted bends, non-90° flanges, or bend-region cutouts.
1599
+
1600
+ **Example**
1719
1601
 
1720
1602
  ```ts
1721
- interface SketchSvgOptions {
1722
- /** Stroke color. Default: "black" */
1723
- stroke?: string;
1724
- /** Stroke width in sketch units. Default: 0.5 */
1725
- strokeWidth?: number;
1726
- /** Fill color. Default: "none" */
1727
- fill?: string;
1728
- /** Padding around the sketch bounding box in sketch units. Default: 2 */
1729
- padding?: number;
1730
- /** If set, scale so 1 sketch-unit = this many px. Otherwise auto-fit. */
1731
- pixelsPerUnit?: number;
1732
- }
1603
+ const cover = sheetMetal({
1604
+ panel: { width: 180, height: 110 },
1605
+ thickness: 1.5,
1606
+ bendRadius: 2,
1607
+ bendAllowance: { kFactor: 0.42 },
1608
+ cornerRelief: { size: 4 },
1609
+ })
1610
+ .flange('top', { length: 18 })
1611
+ .flange('right', { length: 18 })
1612
+ .flange('bottom', { length: 18 })
1613
+ .flange('left', { length: 18 })
1614
+ .cutout('panel', rect(72, 36, true), { selfAnchor: 'center' })
1615
+ .cutout('flange-right', roundedRect(26, 10, 5, true), { selfAnchor: 'center' });
1616
+
1617
+ const folded = cover.folded();
1618
+ const flat = cover.flatPattern();
1733
1619
  ```
1734
1620
 
1735
- </details>
1621
+ `sheetMetal(options: SheetMetalOptions): SheetMetalPart`
1736
1622
 
1737
- ---
1623
+ **`SheetMetalOptions`**
1738
1624
 
1739
- ## C14: Visual & Debugging
1625
+ | Option | Type | Description |
1626
+ |--------|------|-------------|
1627
+ | `width` | `number` | Width of the panel along the X axis. |
1628
+ | `height` | `number` | Height of the panel along the Y axis. |
1629
+ | `thickness` | `number` | Sheet thickness in mm. Applied uniformly across the panel and all flanges. |
1630
+ | `bendRadius` | `number` | Inside bend radius in mm. Must be ≥ 0. Typically 0.5–2× the sheet thickness. |
1631
+ | `kFactor` | `number` | K-factor (neutral-axis offset, 0–1). |
1632
+ | `kind?` | `"rect"` | Relief shape — only `'rect'` is supported in v1. |
1633
+ | `size` | `number` | Side length of the square relief cut in mm. |
1740
1634
 
1741
- Control viewport appearance and debugging aids.
1635
+ #### `sketchToDxf()` Export a 2D sketch as a DXF string (R12/AC1009 — maximally compatible).
1742
1636
 
1743
- #### `explodeView()`
1637
+ **Details**
1744
1638
 
1745
- ```ts
1746
- explodeView(options?: ExplodeViewOptions): void
1747
- ```
1639
+ For regular sketches, each polygon loop becomes a closed `LWPOLYLINE`. For constrained sketches, exports raw `LINE`, `CIRCLE`, and `ARC` entities from the constraint edge geometry, which preserves internal/shared edges that `toPolygons()` would merge away.
1748
1640
 
1749
- Configure viewport exploded-view behavior for the current script execution. Multiple calls merge; later values override earlier ones.
1641
+ The R12 format is chosen for maximum compatibility with CAM tools, laser-cutter software, and older CAD readers.
1750
1642
 
1751
- <details><summary><code>ExplodeViewOptions</code></summary>
1643
+ **Example**
1752
1644
 
1753
1645
  ```ts
1754
- interface ExplodeViewOptions {
1755
- /** Set false to disable viewport explode offsets for this script output. */
1756
- enabled?: boolean;
1757
- /** Scales the UI explode amount. Default: 1 */
1758
- amountScale?: number;
1759
- /** Per-depth stage multipliers (depth 1 = first level). If depth exceeds this array, the last value is reused. Default when omitted: reciprocal depth (1, 1/2, 1/3, ...) */
1760
- stages?: number[];
1761
- /** Global direction mode fallback. Default: 'radial' */
1762
- mode?: ExplodeViewDirection;
1763
- /** Global axis lock fallback. */
1764
- axisLock?: ExplodeAxis;
1765
- /** Per-object overrides by final object name. */
1766
- byName?: Record<string, ExplodeViewDirective>;
1767
- /** Per-tree-path overrides using slash-separated object tree segments. */
1768
- byPath?: Record<string, ExplodeViewDirective>;
1769
- }
1646
+ const s = rect(100, 60);
1647
+ const dxf = sketchToDxf(s, { layer: 'cut' });
1770
1648
  ```
1771
1649
 
1772
- </details>
1650
+ `sketchToDxf(sketch: Sketch, options?: SketchDxfOptions): string`
1773
1651
 
1774
- <details><summary><code>ExplodeDirective</code></summary>
1652
+ **`SketchDxfOptions`**
1653
+ - `layer?: string` — DXF layer name. Default: "0"
1654
+ - `colorIndex?: number` — DXF color index (1–255, AutoCAD ACI). Default: 7 (white/black)
1775
1655
 
1776
- ```ts
1777
- interface ExplodeDirective {
1778
- /** Multiplier applied to `amount` for this node */
1779
- stage?: number;
1780
- /** Direction mode for this node */
1781
- direction?: ExplodeDirection;
1782
- /** Optional axis lock after direction is resolved */
1783
- axisLock?: ExplodeAxis;
1784
- }
1785
- ```
1656
+ #### `sketchToSvg()` — Export a 2D sketch as an SVG string.
1786
1657
 
1787
- </details>
1658
+ **Details**
1788
1659
 
1789
- <details><summary><code>ExplodeViewDirective</code> extends ExplodeDirective</summary>
1660
+ For regular sketches, exports filled polygon regions. For constrained sketches, exports raw edge geometry (LINE, ARC, CIRCLE) which preserves internal/shared edges that `toPolygons()` would merge away.
1661
+
1662
+ The SVG uses the sketch's native coordinate system (Y-up) with a CSS transform that flips Y so the output renders correctly in SVG's Y-down space. Coordinates are in sketch units (typically mm).
1663
+
1664
+ **Example**
1790
1665
 
1791
1666
  ```ts
1792
- interface ExplodeViewDirective extends ExplodeDirective {
1793
- }
1667
+ const s = rect(100, 60);
1668
+ const svg = sketchToSvg(s, { stroke: '#333', strokeWidth: 0.8 });
1794
1669
  ```
1795
1670
 
1796
- </details>
1671
+ `sketchToSvg(sketch: Sketch, options?: SketchSvgOptions): string`
1797
1672
 
1798
- #### `cutPlane()`
1673
+ **`SketchSvgOptions`**
1799
1674
 
1800
- ```ts
1801
- cutPlane(name: string, normal: [ number, number, number ], options?: CutPlaneOptions): void
1802
- ```
1675
+ | Option | Type | Description |
1676
+ |--------|------|-------------|
1677
+ | `stroke?` | `string` | Stroke color. Default: "black" |
1678
+ | `strokeWidth?` | `number` | Stroke width in sketch units. Default: 0.5 |
1679
+ | `fill?` | `string` | Fill color. Default: "none" |
1680
+ | `padding?` | `number` | Padding around the sketch bounding box in sketch units. Default: 2 |
1681
+ | `pixelsPerUnit?` | `number` | If set, scale so 1 sketch-unit = this many px. Otherwise auto-fit. |
1803
1682
 
1804
- <details><summary><code>CutPlaneOptions</code></summary>
1683
+ ---
1805
1684
 
1806
- ```ts
1807
- interface CutPlaneOptions {
1808
- /** Optional offset along the plane normal (primarily for object-form overload). */
1809
- offset?: number;
1810
- /** Object names to keep uncut for this plane. */
1811
- exclude?: CutPlaneExcludeInput;
1812
- }
1813
- ```
1685
+ ## C14: Visual & Debugging
1814
1686
 
1815
- </details>
1687
+ Control viewport appearance and debugging aids.
1816
1688
 
1817
- #### `scene()`
1689
+ #### `explodeView()` — Configure how the viewport explode slider offsets returned objects.
1818
1690
 
1819
- ```ts
1820
- scene(options: SceneOptions): void
1821
- ```
1691
+ **Details**
1822
1692
 
1823
- Configure the scene environment for the current script execution. Controls camera, lighting, background, fog, and post-processing. Multiple calls merge; later values override earlier ones. ```js scene({ background: '#0a0a0a', camera: { position: [200, 100, 150], target: [0, 0, 30], fov: 60 }, lights: [ { type: 'ambient', color: '#1a1a2e', intensity: 0.2 }, { type: 'point', position: [0, 0, 100], color: '#ff6b35', intensity: 2 }, ], fog: { color: '#0a0a0a', near: 100, far: 500 }, postProcessing: { bloom: { intensity: 1.5, threshold: 0.8, radius: 0.4 }, }, }); ```
1693
+ Offsets are resolved from the returned object tree, not a flat list. In `radial` mode each node follows its parent branch direction, then fans locally from the immediate parent center nested assemblies peel apart level by level. In fixed-axis or fixed-vector modes, the branch follows that axis/vector but nested descendants fan out perpendicular by default.
1824
1694
 
1825
- <details><summary><code>SceneOptions</code></summary>
1695
+ Multiple calls merge — later values override earlier ones on a per-key basis. `byName` and `byPath` maps are merged entry-by-entry.
1826
1696
 
1827
- ```ts
1828
- interface SceneOptions {
1829
- background?: string | SceneBackgroundGradient;
1830
- camera?: SceneCameraConfig;
1831
- lights?: SceneLightConfig[];
1832
- environment?: SceneEnvironmentConfig;
1833
- fog?: SceneFogConfig;
1834
- postProcessing?: ScenePostProcessingConfig;
1835
- ground?: SceneGroundConfig;
1836
- /** Default capture parameters for `forgecad capture` CLI flags override these. */
1837
- capture?: SceneCaptureConfig;
1838
- }
1697
+ For programmatic explode applied before returning (without the slider), use `lib.explode()` instead.
1698
+
1699
+ **Example**
1700
+
1701
+ ```js
1702
+ explodeView({
1703
+ amountScale: 1.2,
1704
+ stages: [0.35, 0.8],
1705
+ mode: 'radial',
1706
+ byPath: { 'Drive/Shaft': { direction: [1, 0, 0], stage: 1.6 } },
1707
+ });
1839
1708
  ```
1840
1709
 
1841
- </details>
1710
+ `explodeView(options?: ExplodeViewOptions): void`
1842
1711
 
1843
- <details><summary><code>SceneBackgroundGradient</code></summary>
1712
+ **`ExplodeViewOptions`**
1844
1713
 
1845
- ```ts
1846
- interface SceneBackgroundGradient {
1847
- top: string;
1848
- bottom: string;
1849
- }
1850
- ```
1714
+ | Option | Type | Description |
1715
+ |--------|------|-------------|
1716
+ | `enabled?` | `boolean` | Set false to disable viewport explode offsets for this script output. |
1717
+ | `amountScale?` | `number` | Scales the UI explode amount. Default: 1 |
1718
+ | `stages?` | `number[]` | Per-depth stage multipliers (depth 1 = first level). If depth exceeds this array, the last value is reused. Default when omitted: reciprocal depth (1, 1/2, 1/3, ...) |
1719
+ | `mode?` | `ExplodeViewDirection` | Global direction mode fallback. Default: 'radial' |
1720
+ | `axisLock?` | `ExplodeAxis` | Global axis lock fallback. |
1721
+ | `byName?` | `Record<string, ExplodeViewDirective>` | Per-object overrides by final object name. |
1722
+ | `byPath?` | `Record<string, ExplodeViewDirective>` | Per-tree-path overrides using slash-separated object tree segments. |
1851
1723
 
1852
- </details>
1724
+ **`ExplodeDirective`**
1725
+ - `stage?: number` — Multiplier applied to `amount` for this node
1726
+ - `direction?: ExplodeDirection` — Direction mode for this node
1727
+ - `axisLock?: ExplodeAxis` — Optional axis lock after direction is resolved
1853
1728
 
1854
- <details><summary><code>SceneCameraConfig</code></summary>
1729
+ #### `cutPlane()`
1855
1730
 
1856
- ```ts
1857
- interface SceneCameraConfig {
1858
- fov?: number;
1859
- type?: "perspective" | "orthographic";
1860
- }
1861
- ```
1731
+ `cutPlane(name: string, normal: [ number, number, number ], options?: CutPlaneOptions): void`
1862
1732
 
1863
- </details>
1733
+ **`CutPlaneOptions`**
1734
+ - `offset?: number` — Optional offset along the plane normal (primarily for object-form overload).
1735
+ - `exclude?: CutPlaneExcludeInput` — Object names to keep uncut for this plane.
1864
1736
 
1865
- <details><summary><code>SceneLightConfig</code></summary>
1737
+ #### `scene()` — Configure the scene environment for the current script execution.
1866
1738
 
1867
- ```ts
1868
- interface SceneLightConfig {
1869
- type: SceneLightType;
1870
- color?: string;
1871
- intensity?: number;
1872
- /** Ground color for hemisphere lights */
1873
- groundColor?: string;
1874
- /** Sky color alias for hemisphere lights (same as color) */
1875
- skyColor?: string;
1876
- /** Spot light cone angle in radians */
1877
- angle?: number;
1878
- /** Spot light penumbra (0–1) */
1879
- penumbra?: number;
1880
- /** Point/spot light decay */
1881
- decay?: number;
1882
- /** Point/spot light distance (0 = infinite) */
1883
- distance?: number;
1884
- /** Whether this light casts shadows */
1885
- castShadow?: boolean;
1886
- }
1887
- ```
1739
+ **Details**
1888
1740
 
1889
- </details>
1741
+ Controls camera position, lighting rig, background color or gradient, atmospheric fog, environment maps, post-processing effects, and capture parameters for the `forgecad capture` command. Multiple calls merge — later values override earlier ones on a per-key basis, so you can split configuration across multiple `scene()` calls.
1890
1742
 
1891
- <details><summary><code>SceneEnvironmentConfig</code></summary>
1743
+ When `lights` is specified, **all** default lights are removed. You must include your own ambient light or the scene will be fully dark.
1892
1744
 
1893
- ```ts
1894
- interface SceneEnvironmentConfig {
1895
- /** Built-in preset name or 'none' to disable */
1896
- preset?: "studio" | "sunset" | "dawn" | "warehouse" | "forest" | "apartment" | "lobby" | "city" | "park" | "night" | "none";
1897
- /** Environment map intensity */
1898
- intensity?: number;
1899
- /** Use environment map as scene background */
1900
- background?: boolean;
1901
- }
1902
- ```
1745
+ Setting `camera.position` overrides auto-framing — the viewport will no longer auto-fit the geometry on script reload.
1903
1746
 
1904
- </details>
1747
+ Post-processing effects (`bloom`, `vignette`, `grain`) work in the browser viewport only. The CLI applies camera, lights, background, fog, and `toneMappingExposure` but skips shader effects.
1905
1748
 
1906
- <details><summary><code>SceneFogConfig</code></summary>
1749
+ All numeric values accept `param()` expressions.
1907
1750
 
1908
- ```ts
1909
- interface SceneFogConfig {
1910
- color?: string;
1911
- /** Linear fog near distance */
1912
- near?: number;
1913
- /** Linear fog far distance */
1914
- far?: number;
1915
- /** Exponential fog density (if set, uses FogExp2 instead of linear Fog) */
1916
- density?: number;
1917
- }
1751
+ **Example**
1752
+
1753
+ ```js
1754
+ scene({
1755
+ background: { top: '#000814', bottom: '#001d3d' },
1756
+ camera: { position: [160, -120, 100], target: [0, 0, 50], fov: 52 },
1757
+ lights: [
1758
+ { type: 'ambient', color: '#001233', intensity: 0.08 },
1759
+ { type: 'point', position: [120, -80, 130], color: '#00f5d4', intensity: 4, distance: 400, decay: 1 },
1760
+ { type: 'point', position: [-100, 60, 20], color: '#f72585', intensity: 3, distance: 350 },
1761
+ { type: 'directional', position: [50, -30, 200], color: '#ffd60a', intensity: 1.2 },
1762
+ { type: 'hemisphere', skyColor: '#003566', groundColor: '#000814', intensity: 0.2 },
1763
+ ],
1764
+ fog: { color: '#000814', near: 100, far: 450 },
1765
+ postProcessing: {
1766
+ bloom: { intensity: param('bloom', 1.5, 0, 4), threshold: 0.5, radius: 0.7 },
1767
+ vignette: { darkness: 0.8, offset: 0.25 },
1768
+ grain: { intensity: 0.08 },
1769
+ toneMappingExposure: param('exposure', 1.5, 0.5, 4),
1770
+ },
1771
+ });
1918
1772
  ```
1919
1773
 
1920
- </details>
1774
+ `scene(options: SceneOptions): void`
1921
1775
 
1922
- <details><summary><code>ScenePostProcessingConfig</code></summary>
1776
+ **`SceneOptions`**
1923
1777
 
1924
- ```ts
1925
- interface ScenePostProcessingConfig {
1926
- bloom?: SceneBloomConfig;
1927
- vignette?: SceneVignetteConfig;
1928
- grain?: SceneGrainConfig;
1929
- toneMappingExposure?: number;
1930
- }
1931
- ```
1778
+ | Option | Type | Description |
1779
+ |--------|------|-------------|
1780
+ | `capture?` | `SceneCaptureConfig` | Default capture parameters for `forgecad capture` — CLI flags override these. |
1781
+ | `background?`, `camera?`, `lights?`, `environment?`, `fog?`, `postProcessing?`, `ground?` | | — |
1932
1782
 
1933
- </details>
1783
+ `SceneBackgroundGradient`: `{ top: string, bottom: string }`
1934
1784
 
1935
- <details><summary><code>SceneBloomConfig</code></summary>
1785
+ `SceneCameraConfig`: `{ fov?: number, type?: "perspective" | "orthographic" }`
1936
1786
 
1937
- ```ts
1938
- interface SceneBloomConfig {
1939
- intensity?: number;
1940
- threshold?: number;
1941
- radius?: number;
1942
- }
1943
- ```
1787
+ **`SceneLightConfig`**
1944
1788
 
1945
- </details>
1789
+ | Option | Type | Description |
1790
+ |--------|------|-------------|
1791
+ | `groundColor?` | `string` | Ground color for hemisphere lights |
1792
+ | `skyColor?` | `string` | Sky color alias for hemisphere lights (same as color) |
1793
+ | `angle?` | `number` | Spot light cone angle in radians |
1794
+ | `penumbra?` | `number` | Spot light penumbra (0–1) |
1795
+ | `decay?` | `number` | Point/spot light decay |
1796
+ | `distance?` | `number` | Point/spot light distance (0 = infinite) |
1797
+ | `castShadow?` | `boolean` | Whether this light casts shadows |
1798
+ | `type`, `color?`, `intensity?` | | — |
1946
1799
 
1947
- <details><summary><code>SceneVignetteConfig</code></summary>
1800
+ **`SceneEnvironmentConfig`**
1801
+ - `preset?: "studio" | "sunset" | "dawn" | "warehouse" | "forest" | "apartment" | "lobby" | "city" | "park" | "night" | "none"` — Built-in preset name or 'none' to disable
1802
+ - `intensity?: number` — Environment map intensity
1803
+ - `background?: boolean` — Use environment map as scene background
1948
1804
 
1949
- ```ts
1950
- interface SceneVignetteConfig {
1951
- darkness?: number;
1952
- offset?: number;
1953
- }
1954
- ```
1805
+ **`SceneFogConfig`**
1806
+ - `near?: number` — Linear fog near distance
1807
+ - `far?: number` — Linear fog far distance
1808
+ - `density?: number` — Exponential fog density (if set, uses FogExp2 instead of linear Fog)
1809
+ - Also: `color?: string`
1955
1810
 
1956
- </details>
1811
+ `ScenePostProcessingConfig`: `{ bloom?: SceneBloomConfig, vignette?: SceneVignetteConfig, grain?: SceneGrainConfig, toneMappingExposure?: number }`
1957
1812
 
1958
- <details><summary><code>SceneGrainConfig</code></summary>
1813
+ `SceneBloomConfig`: `{ intensity?: number, threshold?: number, radius?: number }`
1959
1814
 
1960
- ```ts
1961
- interface SceneGrainConfig {
1962
- intensity?: number;
1963
- }
1964
- ```
1815
+ `SceneVignetteConfig`: `{ darkness?: number, offset?: number }`
1965
1816
 
1966
- </details>
1817
+ `SceneGrainConfig`: `{ intensity?: number }`
1967
1818
 
1968
- <details><summary><code>SceneGroundConfig</code></summary>
1819
+ **`SceneGroundConfig`**
1969
1820
 
1970
- ```ts
1971
- interface SceneGroundConfig {
1972
- /** Show a ground plane */
1973
- visible?: boolean;
1974
- /** Ground color */
1975
- color?: string;
1976
- /** Offset below the model's bounding box minimum Z. Default 0 (flush with model bottom). */
1977
- offset?: number;
1978
- /** Receive shadows on the ground */
1979
- receiveShadow?: boolean;
1980
- }
1981
- ```
1821
+ | Option | Type | Description |
1822
+ |--------|------|-------------|
1823
+ | `visible?` | `boolean` | Show a ground plane |
1824
+ | `color?` | `string` | Ground color |
1825
+ | `offset?` | `number` | Offset below the model's bounding box minimum Z. Default 0 (flush with model bottom). |
1826
+ | `receiveShadow?` | `boolean` | Receive shadows on the ground |
1982
1827
 
1983
- </details>
1828
+ **`SceneCaptureConfig`**
1984
1829
 
1985
- <details><summary><code>SceneCaptureConfig</code></summary>
1830
+ | Option | Type | Description |
1831
+ |--------|------|-------------|
1832
+ | `framesPerTurn?` | `number` | Frames for one full orbit rotation (default: 72) |
1833
+ | `holdFrames?` | `number` | Frozen frames before motion starts (default: 6) |
1834
+ | `pitchDeg?` | `number` | Orbit pitch angle in degrees (default: auto from camera) |
1835
+ | `fps?` | `number` | Output frame rate (default: 24) |
1836
+ | `size?` | `number` | Output frame size in pixels (default: 960) |
1837
+ | `background?` | `string` | Canvas background color for capture (default: '#252526') |
1986
1838
 
1987
- ```ts
1988
- interface SceneCaptureConfig {
1989
- /** Frames for one full orbit rotation (default: 72) */
1990
- framesPerTurn?: number;
1991
- /** Frozen frames before motion starts (default: 6) */
1992
- holdFrames?: number;
1993
- /** Orbit pitch angle in degrees (default: auto from camera) */
1994
- pitchDeg?: number;
1995
- /** Output frame rate (default: 24) */
1996
- fps?: number;
1997
- /** Output frame size in pixels (default: 960) */
1998
- size?: number;
1999
- /** Canvas background color for capture (default: '#252526') */
2000
- background?: string;
2001
- }
2002
- ```
1839
+ #### `showLabels()` — Highlight all user-labeled faces on a shape for visual debugging.
2003
1840
 
2004
- </details>
1841
+ Shows each user-authored label name in the viewport for visual debugging. Returns the shape unchanged for chaining: `return showLabels(myShape)`.
2005
1842
 
2006
- #### `viewConfig()`
1843
+ `showLabels(shape: Shape): Shape`
2007
1844
 
2008
- ```ts
2009
- viewConfig(options?: ViewConfigOptions): void
2010
- ```
1845
+ #### `viewConfig()` — Configure viewport helper visuals for the current script execution.
2011
1846
 
2012
- Configure runtime viewport visuals for the current script execution. Multiple calls merge; later values override earlier ones.
1847
+ **Details**
2013
1848
 
2014
- <details><summary><code>ViewConfigOptions</code></summary>
1849
+ Controls renderer-only overlays that appear in the viewport but are not part of the geometry. Currently supports the joint overlay that renders axis arrows and arc indicators when `jointsView` is active. Multiple calls merge — later values override earlier ones per key.
2015
1850
 
2016
- ```ts
2017
- interface ViewConfigOptions {
2018
- jointOverlay?: JointOverlayViewConfigOptions;
2019
- }
1851
+ This does **not** trigger a geometry recompute; it only affects the visual helpers drawn on top of the 3D scene.
1852
+
1853
+ **Example**
1854
+
1855
+ ```js
1856
+ viewConfig({
1857
+ jointOverlay: {
1858
+ axisColor: '#13dfff',
1859
+ arcColor: '#ff7a1a',
1860
+ axisLineRadiusScale: 0.03,
1861
+ arcLineRadiusScale: 0.022,
1862
+ },
1863
+ });
2020
1864
  ```
2021
1865
 
2022
- </details>
1866
+ `viewConfig(options?: ViewConfigOptions): void`
2023
1867
 
2024
- <details><summary><code>JointOverlayViewConfigOptions</code></summary>
1868
+ `ViewConfigOptions`: `{ jointOverlay?: JointOverlayViewConfigOptions }`
2025
1869
 
2026
- ```ts
2027
- interface JointOverlayViewConfigOptions {
2028
- enabled?: boolean;
2029
- axisColor?: string;
2030
- axisCoreColor?: string;
2031
- arcColor?: string;
2032
- zeroColor?: string;
2033
- arcVisualLimitDeg?: number;
2034
- axisLengthScale?: number;
2035
- axisLengthMin?: number;
2036
- axisLineRadiusScale?: number;
2037
- axisLineRadiusMin?: number;
2038
- axisLineRadiusMax?: number;
2039
- spokeLineRadiusScale?: number;
2040
- spokeLineRadiusMin?: number;
2041
- spokeLineRadiusMax?: number;
2042
- arcLineRadiusScale?: number;
2043
- arcLineRadiusMin?: number;
2044
- arcLineRadiusMax?: number;
2045
- axisDotRadiusScale?: number;
2046
- axisDotRadiusMin?: number;
2047
- axisArrowRadiusScale?: number;
2048
- axisArrowRadiusMin?: number;
2049
- axisArrowLengthScale?: number;
2050
- axisArrowLengthMin?: number;
2051
- axisArrowOffsetFactor?: number;
2052
- arcRadiusScale?: number;
2053
- arcRadiusMin?: number;
2054
- arcDotRadiusScale?: number;
2055
- arcDotRadiusMin?: number;
2056
- arcArrowRadiusScale?: number;
2057
- arcArrowRadiusMin?: number;
2058
- arcArrowLengthScale?: number;
2059
- arcArrowLengthMin?: number;
2060
- arcArrowOffsetFactor?: number;
2061
- arcStepDeg?: number;
2062
- arcMinSteps?: number;
2063
- arcTubeSegmentsMin?: number;
2064
- arcTubeSegmentsFactor?: number;
2065
- arcTubeRadialSegments?: number;
2066
- }
2067
- ```
1870
+ **`JointOverlayViewConfigOptions`**: `enabled?: boolean`, `axisColor?: string`, `axisCoreColor?: string`, `arcColor?: string`, `zeroColor?: string`, `arcVisualLimitDeg?: number`, `axisLengthScale?: number`, `axisLengthMin?: number`, `axisLineRadiusScale?: number`, `axisLineRadiusMin?: number`, `axisLineRadiusMax?: number`, `spokeLineRadiusScale?: number`, `spokeLineRadiusMin?: number`, `spokeLineRadiusMax?: number`, `arcLineRadiusScale?: number`, `arcLineRadiusMin?: number`, `arcLineRadiusMax?: number`, `axisDotRadiusScale?: number`, `axisDotRadiusMin?: number`, `axisArrowRadiusScale?: number`, `axisArrowRadiusMin?: number`, `axisArrowLengthScale?: number`, `axisArrowLengthMin?: number`, `axisArrowOffsetFactor?: number`, `arcRadiusScale?: number`, `arcRadiusMin?: number`, `arcDotRadiusScale?: number`, `arcDotRadiusMin?: number`, `arcArrowRadiusScale?: number`, `arcArrowRadiusMin?: number`, `arcArrowLengthScale?: number`, `arcArrowLengthMin?: number`, `arcArrowOffsetFactor?: number`, `arcStepDeg?: number`, `arcMinSteps?: number`, `arcTubeSegmentsMin?: number`, `arcTubeSegmentsFactor?: number`, `arcTubeRadialSegments?: number`
2068
1871
 
2069
- </details>
1872
+ #### `spec()` — Create a named, reusable bundle of verification checks.
2070
1873
 
2071
- #### `spec()`
1874
+ **Details**
2072
1875
 
2073
- ```ts
2074
- spec(name: string, checkFn: (...args: any[]) => void): Spec
2075
- ```
1876
+ A spec groups related `verify.*` calls under a collapsible header in the Checks panel. This makes large check suites scannable. Specs can be applied to multiple shapes and can check relationships between parts.
1877
+
1878
+ Specs can be defined in separate `.forge.js` files and imported via `require()` to share them across models.
2076
1879
 
2077
- Create a named spec — a reusable bundle of verification checks. ```js const fitSpec = spec("Fits enclosure", (shape) => { verify.lessThan("Width", shape.boundingBox().max[0] - shape.boundingBox().min[0], 200); verify.notEmpty("Has geometry", shape); }); fitSpec.check(myShape); // grouped as "Fits enclosure" in the Checks panel fitSpec.check(otherShape); // can be reused on multiple shapes ``` calls `verify.*` methods. Any verify calls made inside this function are tagged with the spec name for grouped display.
1880
+ `spec.check()` returns a `SpecResult` you can inspect it programmatically or ignore the return value and let the Checks panel show results.
2078
1881
 
2079
- <details><summary><code>Spec</code></summary>
1882
+ **Example**
2080
1883
 
2081
1884
  ```ts
2082
- interface Spec {
2083
- /** The display name of this spec */
2084
- name: string;
2085
- }
1885
+ const printable = spec("Fits printer bed", (shape) => {
1886
+ verify.notEmpty("Has geometry", shape);
1887
+ const bb = shape.boundingBox();
1888
+ verify.lessThan("Width < 220mm", bb.max[0] - bb.min[0], 220);
1889
+ verify.lessThan("Depth < 220mm", bb.max[1] - bb.min[1], 220);
1890
+ verify.lessThan("Height < 250mm", bb.max[2] - bb.min[2], 250);
1891
+ });
1892
+
1893
+ // Reuse on multiple shapes
1894
+ printable.check(bracket);
1895
+ printable.check(standoff);
1896
+
1897
+ // Check relationships between parts
1898
+ const fitSpec = spec("Assembly fit", (partA, partB) => {
1899
+ verify.notColliding("No interference", partA, partB, 10);
1900
+ });
1901
+ fitSpec.check(bracket, standoff);
2086
1902
  ```
2087
1903
 
2088
- </details>
1904
+ **Spec-first workflow:** Write specs before building geometry. Checks go from red to green as you build — effectively TDD for CAD.
1905
+
1906
+ `spec(name: string, checkFn: (...args: any[]) => void): Spec`
1907
+
1908
+ **`Spec`**
1909
+ - `name: string` — The display name of this spec
2089
1910
 
2090
1911
  ---
2091
1912
 
@@ -2093,13 +1914,17 @@ interface Spec {
2093
1914
 
2094
1915
  Bring external geometry or other ForgeCAD modules into the current script.
2095
1916
 
2096
- #### `group()`
1917
+ #### `group()` — Group multiple shapes/sketches for joint transforms without merging into a single mesh.
2097
1918
 
2098
- ```ts
2099
- group(...items: GroupInput[]): ShapeGroup
2100
- ```
1919
+ Unlike union(), colors and individual identities are preserved. Children can be plain shapes, named descriptors ({ name, shape/sketch/group }), or nested groups. The returned ShapeGroup supports all Shape transforms (translate, rotate, etc.).
1920
+
1921
+ **Local coordinate pattern:** Build child parts at the origin (local coordinates), then group and translate once to place the whole assembly. This eliminates the error-prone pattern of manually adding parent offsets to every sub-part.
1922
+
1923
+ // BAD — every sub-part repeats the parent's global offset const unitX = 0, unitY = -18, unitZ = 70; const body = roundedBox(100, 20, 32, 4).translate(unitX, unitY, unitZ); const panel = box(98, 2, 18).translate(unitX, unitY - 12, unitZ + 4); const louver = box(88, 2, 6).translate(unitX, unitY - 14, unitZ - 11);
1924
+
1925
+ // GOOD — build at origin, group, translate once const body = roundedBox(100, 20, 32, 4); const panel = box(98, 2, 18).translate(0, -12, 4); const louver = box(88, 2, 6).translate(0, -14, -11); const indoorUnit = group( { name: 'Body', shape: body }, { name: 'Panel', shape: panel }, { name: 'Louver', shape: louver }, ).translate(0, -18, 70);
2101
1926
 
2102
- Group multiple shapes/sketches for joint transforms without merging into a single mesh. Unlike union(), colors and individual identities are preserved. Children can be plain shapes, named descriptors ({ name, shape/sketch/group }), or nested groups. The returned ShapeGroup supports all Shape transforms (translate, rotate, etc.).
1927
+ `group(...items: GroupInput[]): ShapeGroup`
2103
1928
 
2104
1929
  ---
2105
1930