forgecad 0.6.3 → 0.8.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 (234) hide show
  1. package/README.md +3 -12
  2. package/dist/assets/{AdminPage-CeqCUUgu.js → AdminPage-D4bocK4E.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-D3A_g8V3.js} +329 -163
  5. package/dist/assets/{EditorApp-CnC2k4cW.css → EditorApp-BWYUSpUN.css} +590 -136
  6. package/dist/assets/EditorApp-Cihhqcsq.js +11692 -0
  7. package/dist/assets/{EmbedViewer-DBlzmQ5i.js → EmbedViewer-kWjKaC_t.js} +2 -4
  8. package/dist/assets/LandingPageProofDriven-Bg2IUc3l.css +856 -0
  9. package/dist/assets/LandingPageProofDriven-DXkKlyhI.js +601 -0
  10. package/dist/assets/PricingPage-BsU5vzEx.js +232 -0
  11. package/dist/assets/{SettingsPage-BqCh9JcC.js → SettingsPage-PqvpAKIs.js} +129 -5
  12. package/dist/assets/{evalWorker-Ql-aKwLA.js → evalWorker-C-hzNUMy.js} +8949 -3161
  13. package/dist/assets/{Viewport-CoB46f5R.js → index-Pz321YAt.js} +38382 -7501
  14. package/dist/assets/{index-2hfs_ub0.css → index-ay13WNfa.css} +726 -53
  15. package/dist/assets/{javascript-DCxGoE5Y.js → javascript-DAl8Gmyo.js} +1 -1
  16. package/dist/assets/{manifold-CqNMHHKO.js → manifold-BcbjWLIo.js} +4 -3
  17. package/dist/assets/{manifold-Cce9wRFz.js → manifold-DBckbFgx.js} +1 -1
  18. package/dist/assets/{manifold-D6BeHIOo.js → manifold-O2AAGXyj.js} +1 -1
  19. package/dist/assets/{reportWorker-sFEFonXf.js → reportWorker-Dxr-5A7w.js} +8760 -3559
  20. package/dist/assets/{vendor-react-Dt7-aaJH.js → vendor-react-CG3i_wp0.js} +65 -8
  21. package/dist/docs/index.html +2 -2
  22. package/dist/docs-raw/CLI.md +341 -718
  23. package/dist/docs-raw/generated/assembly.md +699 -112
  24. package/dist/docs-raw/generated/concepts.md +1834 -1346
  25. package/dist/docs-raw/generated/core.md +1012 -1059
  26. package/dist/docs-raw/generated/curves.md +759 -116
  27. package/dist/docs-raw/generated/lib.md +43 -748
  28. package/dist/docs-raw/generated/output.md +139 -245
  29. package/dist/docs-raw/generated/sdf.md +208 -0
  30. package/dist/docs-raw/generated/sheet-metal.md +473 -21
  31. package/dist/docs-raw/generated/sketch.md +1518 -362
  32. package/dist/docs-raw/generated/viewport.md +368 -299
  33. package/dist/docs-raw/generated/wood.md +104 -0
  34. package/dist/index.html +2 -2
  35. package/dist/landing/proof-ams-adapter.png +0 -0
  36. package/dist/landing/proof-bolt-and-nut.png +0 -0
  37. package/dist/landing/proof-fillet-enclosure.png +0 -0
  38. package/dist/landing/proof-glasses.png +0 -0
  39. package/dist/landing/proof-gyroid.png +0 -0
  40. package/dist/sitemap.xml +6 -6
  41. package/dist-cli/forgecad.js +12321 -5700
  42. package/dist-cli/forgecad.js.map +1 -0
  43. package/dist-cli/solver-46FFSK2U.js +363 -0
  44. package/dist-cli/solver-46FFSK2U.js.map +1 -0
  45. package/dist-skill/CONTEXT.md +4890 -6302
  46. package/dist-skill/SKILL-dev.md +22 -66
  47. package/dist-skill/SKILL.md +20 -59
  48. package/dist-skill/docs/API/core/concepts.md +37 -92
  49. package/dist-skill/docs/CLI.md +341 -718
  50. package/dist-skill/docs/generated/assembly.md +699 -112
  51. package/dist-skill/docs/generated/core.md +1012 -1059
  52. package/dist-skill/docs/generated/curves.md +759 -116
  53. package/dist-skill/docs/generated/lib.md +43 -748
  54. package/dist-skill/docs/generated/output.md +139 -245
  55. package/dist-skill/docs/generated/sdf.md +208 -0
  56. package/dist-skill/docs/generated/sheet-metal.md +473 -21
  57. package/dist-skill/docs/generated/sketch.md +1518 -362
  58. package/dist-skill/docs/generated/viewport.md +368 -299
  59. package/dist-skill/docs/generated/wood.md +104 -0
  60. package/dist-skill/docs/guides/coordinate-system.md +11 -17
  61. package/dist-skill/docs/guides/geometry-conventions.md +13 -70
  62. package/dist-skill/docs/guides/joint-design.md +78 -0
  63. package/dist-skill/docs/guides/modeling-recipes.md +22 -195
  64. package/dist-skill/docs/guides/positioning.md +88 -147
  65. package/dist-skill/docs-dev/API/core/concepts.md +78 -0
  66. package/dist-skill/docs-dev/CLI.md +488 -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 +2 -4
  70. package/dist-skill/docs-dev/component-model.md +164 -0
  71. package/dist-skill/docs-dev/generated/assembly.md +779 -0
  72. package/dist-skill/docs-dev/generated/core.md +1676 -0
  73. package/dist-skill/docs-dev/generated/curves.md +855 -0
  74. package/dist-skill/docs-dev/generated/lib.md +55 -0
  75. package/dist-skill/docs-dev/generated/output.md +234 -0
  76. package/dist-skill/docs-dev/generated/sdf.md +208 -0
  77. package/dist-skill/docs-dev/generated/sheet-metal.md +506 -0
  78. package/dist-skill/docs-dev/generated/sketch.md +1753 -0
  79. package/dist-skill/docs-dev/generated/viewport.md +513 -0
  80. package/dist-skill/docs-dev/generated/wood.md +104 -0
  81. package/dist-skill/docs-dev/guides/coordinate-system.md +46 -0
  82. package/dist-skill/docs-dev/guides/geometry-conventions.md +52 -0
  83. package/dist-skill/docs-dev/guides/joint-design.md +78 -0
  84. package/dist-skill/docs-dev/guides/modeling-recipes.md +77 -0
  85. package/dist-skill/docs-dev/guides/positioning.md +151 -0
  86. package/dist-skill/{docs → docs-dev}/guides/skill-maintenance.md +21 -10
  87. package/dist-skill/{docs → docs-dev}/internals/compiler.md +5 -6
  88. package/dist-skill/{docs → docs-dev}/internals/constraint-solver-quality.md +0 -1
  89. package/dist-skill/{docs → docs-dev}/internals/constraint-solver.md +0 -1
  90. package/dist-skill/{docs → docs-dev}/internals/sketch-2d-pipeline.md +2 -3
  91. package/examples/api/attachTo-basics.forge.js +8 -8
  92. package/examples/api/bill-of-materials.forge.js +9 -9
  93. package/examples/api/bolt-pattern.forge.js +5 -5
  94. package/examples/api/boolean-operations.forge.js +5 -5
  95. package/examples/api/bounding-box-visualizer.forge.js +3 -3
  96. package/examples/api/clone-duplicate.forge.js +2 -2
  97. package/examples/api/colors-union-vs-array.forge.js +6 -6
  98. package/examples/api/connector-assembly.forge.js +8 -6
  99. package/examples/api/connector-basics.forge.js +7 -7
  100. package/examples/api/constrained-sketch-mechanical.forge.js +4 -4
  101. package/examples/api/elbow-test.forge.js +3 -3
  102. package/examples/api/extrude-options.forge.js +8 -14
  103. package/examples/api/feature-created-faces.forge.js +6 -10
  104. package/examples/api/fillet-showcase.forge.js +2 -2
  105. package/examples/api/folded-service-panel-cover.forge.js +2 -2
  106. package/examples/api/gears-tier1.forge.js +5 -5
  107. package/examples/api/group-test.forge.js +3 -3
  108. package/examples/api/group-vs-union.forge.js +1 -1
  109. package/examples/api/highlight-debug.forge.js +4 -0
  110. package/examples/api/js-module-pillars.js +1 -1
  111. package/examples/api/js-module-scene.js +2 -2
  112. package/examples/api/mesh-import-slats.forge.js +4 -4
  113. package/examples/api/patterns.forge.js +3 -3
  114. package/examples/api/pointAlong-orientation.forge.js +3 -3
  115. package/examples/api/profile-2020-b-slot6.forge.js +4 -5
  116. package/examples/api/route-perimeter-flange.forge.js +1 -1
  117. package/examples/api/sdf-rover-demo.forge.js +10 -10
  118. package/examples/api/sketch-on-face-demo.forge.js +2 -2
  119. package/examples/api/sketch-regions.forge.js +4 -4
  120. package/examples/api/sketch-rounding-strategies.forge.js +1 -1
  121. package/examples/api/smooth-curve-connections.forge.js +1 -1
  122. package/examples/api/transition-curves.forge.js +4 -4
  123. package/examples/api/variable-sweep-pure-sdf-test.forge.js +162 -0
  124. package/examples/api/variable-sweep-test.forge.js +2 -2
  125. package/examples/api/wood-joinery.forge.js +60 -0
  126. package/examples/compiler-corpus/enclosure-shell-cuts.forge.js +3 -3
  127. package/examples/compiler-corpus/fastener-plate-variants.forge.js +2 -2
  128. package/examples/constraints/01-fully-constrained-rect.forge.js +2 -2
  129. package/examples/constraints/02-underconstrained-triangle.forge.js +1 -1
  130. package/examples/constraints/03-redundant-constraints.forge.js +2 -2
  131. package/examples/constraints/05-parallel-with-linedistance.forge.js +2 -2
  132. package/examples/constraints/06-complex-spectrogram.forge.js +1 -1
  133. package/examples/constraints/07-perpendicular-chain.forge.js +4 -4
  134. package/examples/constraints/08-symmetric-bracket.forge.js +4 -4
  135. package/examples/constraints/09-stress-spiral.forge.js +1 -1
  136. package/examples/constraints/10-stress-honeycomb.forge.js +1 -1
  137. package/examples/constraints/11-surface-grid.forge.js +2 -2
  138. package/examples/constraints/12-surface-nested.forge.js +4 -4
  139. package/examples/constraints/13-surface-complex.forge.js +1 -1
  140. package/examples/exact-arc-housing.forge.js +12 -0
  141. package/examples/experiments/drone-arm.forge.js +53 -0
  142. package/examples/furniture/adjustable-table.forge.js +15 -15
  143. package/examples/furniture/bathroom.forge.js +26 -26
  144. package/examples/furniture/chair.forge.js +13 -13
  145. package/examples/furniture/picture-frame.forge.js +6 -6
  146. package/examples/furniture/shoe-rack-doors.forge.js +8 -8
  147. package/examples/furniture/shoe-rack.forge.js +7 -7
  148. package/examples/furniture/table-lamp.forge.js +8 -8
  149. package/examples/gcode/lissajous-vase.forge.js +4 -4
  150. package/examples/gcode/math-surface.forge.js +3 -3
  151. package/examples/gcode/parametric-vase.forge.js +4 -4
  152. package/examples/gcode/spiral-tower.forge.js +4 -4
  153. package/examples/generative/crystal-growth.forge.js +9 -9
  154. package/examples/generative/frost-spires.forge.js +9 -9
  155. package/examples/generative/golden-spiral-tower.forge.js +11 -11
  156. package/examples/generative/molten-forge.forge.js +6 -6
  157. package/examples/generative/neon-coral.forge.js +7 -7
  158. package/examples/mechanical/3d-printer.forge.js +37 -37
  159. package/examples/mechanical/5-finger-robot-hand.forge.js +19 -19
  160. package/examples/mechanical/airplane-propeller.forge.js +9 -9
  161. package/examples/mechanical/bolt-and-nut.forge.js +10 -10
  162. package/examples/mechanical/door-with-hinges.forge.js +7 -7
  163. package/examples/mechanical/fillet-enclosure.forge.js +15 -11
  164. package/examples/mechanical/headphone-hanger-v2.forge.js +11 -11
  165. package/examples/mechanical/robot_hand.forge.js +24 -24
  166. package/examples/mechanical/robot_hand_2.forge.js +26 -26
  167. package/examples/nurbs-surface.forge.js +8 -0
  168. package/examples/nurbs-tube.forge.js +7 -0
  169. package/examples/products/bottle.forge.js +8 -8
  170. package/examples/products/chess-set.forge.js +25 -25
  171. package/examples/products/classical-piano.forge.js +20 -20
  172. package/examples/products/clock.forge.js +33 -33
  173. package/examples/products/cup.forge.js +5 -5
  174. package/examples/products/iphone.forge.js +20 -20
  175. package/examples/products/laptop.forge.js +24 -24
  176. package/examples/products/laser-cut-box.forge.js +6 -6
  177. package/examples/products/laser-cut-tray.forge.js +6 -6
  178. package/examples/products/liquid-soap-dispenser.forge.js +23 -23
  179. package/examples/products/origami-fish.forge.js +14 -12
  180. package/examples/products/spiderman-cake.forge.js +6 -6
  181. package/examples/shelf/container.forge.js +5 -5
  182. package/examples/shelf/shelf-unit.forge.js +6 -6
  183. package/examples/toolbox/bolted-joint.forge.js +7 -7
  184. package/package.json +9 -4
  185. package/dist/assets/EditorApp-B-vQvgam.js +0 -9888
  186. package/dist/assets/LandingPage-C5n9hDXI.js +0 -322
  187. package/dist/assets/PublishedModelPage-Dt7PCVBj.js +0 -146
  188. package/dist/assets/__vite-browser-external-CURh0WXD.js +0 -8
  189. package/dist/assets/deserializeRunResult-BLAFoiE0.js +0 -19365
  190. package/dist/assets/index-1CYp3zUp.js +0 -1455
  191. package/dist-skill/docs/API/API.md +0 -1666
  192. package/dist-skill/docs/API/README.md +0 -37
  193. package/dist-skill/docs/API/assembly/assembly.md +0 -617
  194. package/dist-skill/docs/API/core/edge-queries.md +0 -130
  195. package/dist-skill/docs/API/core/parameters.md +0 -122
  196. package/dist-skill/docs/API/core/reserved-terms.md +0 -137
  197. package/dist-skill/docs/API/core/sdf.md +0 -326
  198. package/dist-skill/docs/API/core/skill-cli.md +0 -194
  199. package/dist-skill/docs/API/core/skill-guide.md +0 -205
  200. package/dist-skill/docs/API/core/specs.md +0 -186
  201. package/dist-skill/docs/API/core/topology.md +0 -372
  202. package/dist-skill/docs/API/entities.md +0 -268
  203. package/dist-skill/docs/API/output/bom.md +0 -58
  204. package/dist-skill/docs/API/output/brep-export.md +0 -87
  205. package/dist-skill/docs/API/output/dimensions.md +0 -67
  206. package/dist-skill/docs/API/output/export.md +0 -110
  207. package/dist-skill/docs/API/output/gcode.md +0 -195
  208. package/dist-skill/docs/API/runtime/viewport.md +0 -420
  209. package/dist-skill/docs/API/sheet-metal/sheet-metal.md +0 -185
  210. package/dist-skill/docs/API/sketch/anchor.md +0 -37
  211. package/dist-skill/docs/API/sketch/booleans.md +0 -91
  212. package/dist-skill/docs/API/sketch/core.md +0 -73
  213. package/dist-skill/docs/API/sketch/extrude.md +0 -62
  214. package/dist-skill/docs/API/sketch/on-face.md +0 -104
  215. package/dist-skill/docs/API/sketch/operations.md +0 -78
  216. package/dist-skill/docs/API/sketch/path.md +0 -75
  217. package/dist-skill/docs/API/sketch/primitives.md +0 -146
  218. package/dist-skill/docs/API/sketch/regions.md +0 -80
  219. package/dist-skill/docs/API/sketch/text.md +0 -108
  220. package/dist-skill/docs/API/sketch/transforms.md +0 -65
  221. package/dist-skill/docs/API/toolbox/fasteners.md +0 -129
  222. package/dist-skill/docs/INDEX.md +0 -94
  223. package/dist-skill/docs/RELEASING.md +0 -55
  224. package/dist-skill/docs/cli-monetization.md +0 -111
  225. package/dist-skill/docs/deployment.md +0 -281
  226. package/dist-skill/docs/generated/concepts.md +0 -2112
  227. package/dist-skill/docs/internals/shape-from-slices.md +0 -152
  228. package/dist-skill/docs/platform/admin.md +0 -45
  229. package/dist-skill/docs/platform/architecture.md +0 -79
  230. package/dist-skill/docs/platform/auth.md +0 -110
  231. package/dist-skill/docs/platform/email.md +0 -67
  232. package/dist-skill/docs/platform/projects.md +0 -111
  233. package/dist-skill/docs/platform/sharing.md +0 -90
  234. package/dist-skill/docs/runbook.md +0 -345
@@ -1,27 +1,25 @@
1
1
  # API by Concept
2
2
 
3
- > **Auto-generated** from `@concept` tags in `src/forge/forge-public-api.ts`. Do not edit by hand — run `npm run gen:docs` to regenerate.
4
-
5
3
  Every public API function belongs to one of 16 fundamental concepts. This document groups them by concept rather than by module, making it easy to see the full set of operations for each idea.
6
4
 
7
5
  ## Concepts
8
6
 
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)*
7
+ - **[C1: Primitive Construction](#c1-primitive-construction)** — Create geometry from parameters — no input geometry required. *(50 functions)*
8
+ - **[C2: Boolean Combination](#c2-boolean-combination)** — Combine same-dimension geometry using CSG set operations. *(6 functions)*
11
9
  - **[C3: Rigid Transform](#c3-rigid-transform)** — Reposition or reorient geometry without changing its shape. *(3 functions)*
12
10
  - **[C4: Dimensional Promotion](#c4-dimensional-promotion)** — Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep). *(12 functions)*
13
11
  - **[C5: Topology Query](#c5-topology-query)** — Select or inspect named faces and edges on a shape. *(3 functions)*
14
12
  - **[C6: Edge Feature](#c6-edge-feature)** — Modify edges of a solid — fillets, chamfers, draft, offset. *(7 functions)*
15
13
  - **[C7: Pattern Replication](#c7-pattern-replication)** — Duplicate geometry in regular arrangements (linear, circular, mirror). *(6 functions)*
16
- - **[C8: Constraint Solving](#c8-constraint-solving)** — Define geometry by relationships and let a solver find positions. *(7 functions)*
17
- - **[C9: Spatial Placement](#c9-spatial-placement)** — Position geometry relative to other geometry using semantic anchors. *(0 functions)*
14
+ - **[C8: Constraint Solving](#c8-constraint-solving)** — Define geometry by relationships and let a solver find positions. *(17 functions)*
15
+ - **[C9: Spatial Placement](#c9-spatial-placement)** — Position geometry relative to other geometry using semantic anchors. *(6 functions)*
18
16
  - **[C10: Assembly & Kinematics](#c10-assembly-kinematics)** — Compose parts with joints for kinematic simulation. *(4 functions)*
19
- - **[C11: Parameterization & UI](#c11-parameterization-ui)** — Declare user-facing controls that drive model geometry. *(6 functions)*
17
+ - **[C11: Parameterization & UI](#c11-parameterization-ui)** — Declare user-facing controls that drive model geometry. *(7 functions)*
20
18
  - **[C12: Dimensional Demotion](#c12-dimensional-demotion)** — Extract 2D geometry from a 3D solid (section, projection). *(3 functions)*
21
19
  - **[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)*
20
+ - **[C14: Visual & Debugging](#c14-visual-debugging)** — Control viewport appearance and debugging aids. *(26 functions)*
23
21
  - **[C15: Import & Composition](#c15-import-composition)** — Bring external geometry or other ForgeCAD modules into the current script. *(1 functions)*
24
- - **[C16: Part Library](#c16-part-library)** — Pre-built parametric parts accessible via `lib.*`. *(0 functions)*
22
+ - **[C16: Part Library](#c16-part-library)** — Pre-built parametric parts accessible via `lib.*`. *(4 functions)*
25
23
 
26
24
  ---
27
25
 
@@ -29,1362 +27,1882 @@ Every public API function belongs to one of 16 fundamental concepts. This docume
29
27
 
30
28
  Create geometry from parameters — no input geometry required.
31
29
 
32
- #### `arcBridgeBetweenRects()`
30
+ #### `sdf.sphere()` — Create an SDF sphere centered at the origin.
33
31
 
34
32
  ```ts
35
- arcBridgeBetweenRects(rectA: RectAreaArg, rectB: RectAreaArg, segments?: number): Shape
33
+ sdf.sphere(radius: number): SdfShape
36
34
  ```
37
35
 
38
- Build an arc bridge between two rectangular areas.
39
-
40
- #### `circle2d()`
36
+ #### `sdf.box()` — Create an SDF box centered at the origin with given full dimensions (not half-extents).
41
37
 
42
38
  ```ts
43
- circle2d(radius: number, segments?: number): Sketch
39
+ sdf.box(x: number, y: number, z: number): SdfShape
44
40
  ```
45
41
 
46
- Create a 2D circle centered at the origin. Use segments for lower-poly approximations.
47
-
48
- #### `ellipse()`
42
+ #### `sdf.cylinder()` — Create an SDF cylinder centered at the origin, axis along Z.
49
43
 
50
44
  ```ts
51
- ellipse(rx: number, ry: number, segments?: number): Sketch
45
+ sdf.cylinder(height: number, radius: number): SdfShape
52
46
  ```
53
47
 
54
- Create a 2D ellipse centered at the origin with the given X and Y radii.
55
-
56
- #### `loadFont()`
48
+ #### `sdf.torus()` — Create an SDF torus centered at the origin, lying in the XY plane.
57
49
 
58
50
  ```ts
59
- loadFont(source: string | ArrayBuffer, cacheKey?: string): opentype$1.Font
51
+ sdf.torus(majorRadius: number, minorRadius: number): SdfShape
60
52
  ```
61
53
 
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)
63
-
64
- #### `ngon()`
54
+ #### `sdf.capsule()` Create an SDF capsule centered at the origin, axis along Z.
65
55
 
66
56
  ```ts
67
- ngon(sides: number, radius: number): Sketch
57
+ sdf.capsule(height: number, radius: number): SdfShape
68
58
  ```
69
59
 
70
- Create a regular polygon (equilateral triangle, hexagon, etc.) inscribed in a circle of the given radius.
71
-
72
- #### `path()`
60
+ #### `sdf.cone()` Create an SDF cone with base at z=0 and tip at z=height.
73
61
 
74
62
  ```ts
75
- path(): PathBuilder
63
+ sdf.cone(height: number, radius: number): SdfShape
76
64
  ```
77
65
 
78
- Create a path builder for constructing 2D outlines.
79
-
80
- #### `polar()`
66
+ #### `sdf.smoothUnion()` Smooth union blends shapes together with a smooth transition radius.
81
67
 
82
68
  ```ts
83
- polar(length: number, angleDeg: number, from?: [ number, number ]): [ number, number ]
69
+ sdf.smoothUnion(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape
84
70
  ```
85
71
 
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) ```
87
-
88
- #### `polygon()`
72
+ #### `sdf.smoothDifference()` Smooth difference smoothly subtracts b from a.
89
73
 
90
74
  ```ts
91
- polygon(points: ([ number, number ] | Point2D)[]): Sketch
75
+ sdf.smoothDifference(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape
92
76
  ```
93
77
 
94
- Create a 2D polygon from an array of [x, y] points or Point2D objects. Winding is normalized to CCW.
95
-
96
- #### `polygonVertices()`
78
+ #### `sdf.smoothIntersection()` Smooth intersection smoothly intersects a and b.
97
79
 
98
80
  ```ts
99
- polygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[]
81
+ sdf.smoothIntersection(a: SdfShape, b: SdfShape, options: { radius: number; }): SdfShape
100
82
  ```
101
83
 
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); ```
103
-
104
- <details><summary><code>PolygonVerticesOptions</code></summary>
84
+ #### `sdf.morph()` — Morph between two SDF shapes. t=0 a, t=1 b.
105
85
 
106
86
  ```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
- }
87
+ sdf.morph(a: SdfShape, b: SdfShape, t: number): SdfShape
115
88
  ```
116
89
 
117
- </details>
118
-
119
- <details><summary><code>LayoutPoint</code></summary>
90
+ #### `sdf.blend()` — Spatially blend between two SDF patterns. The blend function receives (x, y, z) and returns 0..1: 0 = fully pattern `a`, 1 = fully pattern `b`.
120
91
 
121
92
  ```ts
122
- interface LayoutPoint {
123
- x: number;
124
- y: number;
125
- }
93
+ sdf.blend(a: SdfShape, b: SdfShape, fn: (x: number, y: number, z: number) => number, options?: BlendOptions): SdfShape
126
94
  ```
127
95
 
128
- </details>
96
+ **`BlendOptions`**
97
+ - `constants?: Record<string, number>` — Optional named constants accessible in the blend function.
129
98
 
130
- #### `rect()`
99
+ #### `sdf.gyroid()` — Gyroid TPMS lattice — the most common lattice for additive manufacturing.
131
100
 
132
101
  ```ts
133
- rect(width: number, height: number, center?: boolean): Sketch
102
+ sdf.gyroid(options: TpmsOptions): SdfShape
134
103
  ```
135
104
 
136
- Create a 2D rectangle. When center is true, the origin is at the rectangle center; otherwise at the bottom-left corner.
105
+ `TpmsOptions`: `{ cellSize: number, thickness: number }`
137
106
 
138
- #### `arcSlot()`
107
+ #### `sdf.schwarzP()` — Schwarz-P TPMS lattice — isotropic pore structure.
139
108
 
140
109
  ```ts
141
- arcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch
110
+ sdf.schwarzP(options: TpmsOptions): SdfShape
142
111
  ```
143
112
 
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 ```
145
-
146
- #### `routePerimeter()`
113
+ #### `sdf.diamond()` Diamond TPMS lattice stiffest TPMS structure.
147
114
 
148
115
  ```ts
149
- routePerimeter(steps: PerimeterStep[]): Sketch
116
+ sdf.diamond(options: TpmsOptions): SdfShape
150
117
  ```
151
118
 
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 }, ]) ```
153
-
154
- #### `roundedRect()`
119
+ #### `sdf.lidinoid()` Lidinoid TPMS lattice visually distinct from gyroid, popular in research and art.
155
120
 
156
121
  ```ts
157
- roundedRect(width: number, height: number, radius: number, center?: boolean): Sketch
122
+ sdf.lidinoid(options: TpmsOptions): SdfShape
158
123
  ```
159
124
 
160
- Create a 2D rectangle with rounded corners. The radius is clamped to fit within the dimensions.
161
-
162
- #### `slot()`
125
+ #### `sdf.noise()` 3D Simplex noise field produces organic, natural-looking displacements.
163
126
 
164
127
  ```ts
165
- slot(length: number, width: number): Sketch
128
+ sdf.noise(options?: NoiseOptions): SdfShape
166
129
  ```
167
130
 
168
- Create a slot (stadium/discorectangle) — a rectangle with semicircular ends, centered at origin.
131
+ **`NoiseOptions`**
169
132
 
170
- #### `spline2d()`
133
+ | Option | Type | Description |
134
+ |--------|------|-------------|
135
+ | `scale?` | `number` | Spatial frequency — smaller = larger features. Default: 0.1 |
136
+ | `amplitude?` | `number` | Peak displacement amplitude. Default: 1 |
137
+ | `octaves?` | `number` | fBm octaves (1 = plain simplex, higher = more detail). Default: 1 |
138
+ | `seed?` | `number` | Seed for deterministic variation. Default: 0 |
139
+
140
+ #### `sdf.voronoi()` — 3D Voronoi pattern — organic cellular structures like bone, coral, or soap bubbles.
171
141
 
172
142
  ```ts
173
- spline2d(points: Vec2[], options?: Spline2DOptions): Sketch
143
+ sdf.voronoi(options?: VoronoiOptions): SdfShape
174
144
  ```
175
145
 
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.
146
+ **`VoronoiOptions`**
177
147
 
178
- <details><summary><code>Spline2DOptions</code></summary>
148
+ | Option | Type | Description |
149
+ |--------|------|-------------|
150
+ | `cellSize?` | `number` | Size of each Voronoi cell in world units. Default: 10 |
151
+ | `wallThickness?` | `number` | Wall thickness between cells. Default: 1 |
152
+ | `seed?` | `number` | Seed for deterministic variation. Default: 0 |
153
+ | `suppressionThreshold?` | `number` | Projection weight for membrane suppression (0..1). Controls how much of the surface-normal distance component is removed from Voronoi cell distances. 0 = no projection (classic 3D voronoi with membranes). 1 = full tangent-plane projection (pure 2D pattern on surface). Default: 0.85. Only active when voronoi is intersected with another shape. |
154
+
155
+ #### `sdf.honeycomb()` — Honeycomb (hexagonal) lattice pattern. Intersect with your shape to apply.
179
156
 
180
157
  ```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
- }
158
+ sdf.honeycomb(options?: HoneycombOptions): SdfShape
193
159
  ```
194
160
 
195
- </details>
161
+ **`HoneycombOptions`**
162
+ - `cellSize?: number` — Size of each hex cell. Default: 8
163
+ - `wallThickness?: number` — Wall thickness. Default: 1
196
164
 
197
- #### `star()`
165
+ #### `sdf.waves()` — Sinusoidal wave ridges — parallel ridges along an axis.
198
166
 
199
167
  ```ts
200
- star(points: number, outerR: number, innerR: number): Sketch
168
+ sdf.waves(options?: WavesOptions): SdfShape
201
169
  ```
202
170
 
203
- Create a star shape with alternating outer and inner radii.
171
+ **`WavesOptions`**
172
+ - `wavelength?: number` — Distance between wave peaks. Default: 10
173
+ - `amplitude?: number` — Height of waves. Default: 1
174
+ - `axis?: "x" | "y" | "z"` — Axis along which waves propagate: 'x', 'y', or 'z'. Default: 'x'
204
175
 
205
- #### `stroke()`
176
+ #### `sdf.knurl()` — Knurl pattern — crossed helical grooves for grips and handles.
206
177
 
207
178
  ```ts
208
- stroke(points: [ number, number ][], width: number, join?: "Round" | "Square"): Sketch
179
+ sdf.knurl(options?: KnurlOptions): SdfShape
209
180
  ```
210
181
 
211
- Create a stroked polyline sketch from an array of 2D points.
182
+ **`KnurlOptions`**
183
+ - `pitch?: number` — Distance between knurl ridges. Default: 3
184
+ - `depth?: number` — Depth of knurl grooves. Default: 0.5
185
+ - `angle?: number` — Helix angle in degrees. Default: 30
212
186
 
213
- #### `text2d()`
187
+ #### `sdf.perforated()` — Perforated plate pattern — regular array of cylindrical holes.
214
188
 
215
189
  ```ts
216
- text2d(content: string, options?: TextOptions): Sketch
190
+ sdf.perforated(options?: PerforatedOptions): SdfShape
217
191
  ```
218
192
 
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' })
193
+ **`PerforatedOptions`**
194
+ - `radius?: number` — Hole radius. Default: 3
195
+ - `spacing?: number` — Center-to-center spacing. Default: 8
220
196
 
221
- <details><summary><code>TextOptions</code></summary>
197
+ #### `sdf.scales()` — Fish/dragon scale pattern — overlapping circular scales in hex-packed rows.
222
198
 
223
199
  ```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
- }
200
+ sdf.scales(options?: ScalesOptions): SdfShape
238
201
  ```
239
202
 
240
- </details>
203
+ **`ScalesOptions`**
204
+ - `size?: number` — Scale diameter. Default: 5
205
+ - `depth?: number` — How much scales protrude. Default: 0.8
241
206
 
242
- #### `textWidth()`
207
+ #### `sdf.brick()` — Brick/stone wall pattern — running bond with mortar grooves.
243
208
 
244
209
  ```ts
245
- textWidth(content: string, options?: Pick<TextOptions, "size" | "letterSpacing" | "font">): number
210
+ sdf.brick(options?: BrickOptions): SdfShape
246
211
  ```
247
212
 
248
- Returns the rendered width of a string in model units (same options as text2d).
213
+ **`BrickOptions`**
249
214
 
250
- ---
215
+ | Option | Type | Description |
216
+ |--------|------|-------------|
217
+ | `width?` | `number` | Brick width. Default: 10 |
218
+ | `height?` | `number` | Brick height. Default: 5 |
219
+ | `depth?` | `number` | Mortar groove depth. Default: 0.5 |
220
+ | `mortar?` | `number` | Mortar gap width. Default: 1 |
251
221
 
252
- ## C2: Boolean Combination
222
+ #### `sdf.weave()` Grid lattice pattern — two families of infinite slabs crossing at 90°.
253
223
 
254
- Combine same-dimension geometry using CSG set operations.
224
+ ```ts
225
+ sdf.weave(options?: WeaveOptions): SdfShape
226
+ ```
255
227
 
256
- #### `difference2d()`
228
+ **`WeaveOptions`**
229
+ - `spacing?: number` — Thread center-to-center spacing (for intersection patterns). Default: 5
230
+ - `threadRadius?: number` — Thread half-width. Default: 1
231
+
232
+ #### `sdf.basketWeave()` — Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`.
257
233
 
258
234
  ```ts
259
- difference2d(...inputs: SketchOperandInput[]): Sketch
235
+ sdf.basketWeave(options?: BasketWeaveOptions): SurfacePattern
260
236
  ```
261
237
 
262
- Subtract 2D sketches from a base sketch. The first sketch is the base; all others are subtracted.
238
+ **`BasketWeaveOptions`**
239
+ - `spacing?: number` — Spacing between threads in mm (both directions). Default: 3
240
+ - `threadWidth?: number` — Thread width in mm. Default: 1.5
241
+ - `depth?: number` — Thread protrusion depth in mm. Default: 0.8
263
242
 
264
- #### `intersection2d()`
243
+ #### `sdf.twist()` — Twist an SDF shape around the Z axis.
265
244
 
266
245
  ```ts
267
- intersection2d(...inputs: SketchOperandInput[]): Sketch
246
+ sdf.twist(shape: SdfShape, degreesPerUnit: number): SdfShape
268
247
  ```
269
248
 
270
- Keep only the overlapping area of the input sketches (intersection boolean).
271
-
272
- #### `union2d()`
249
+ #### `sdf.bend()` Bend an SDF shape around the Z axis.
273
250
 
274
251
  ```ts
275
- union2d(...inputs: SketchOperandInput[]): Sketch
252
+ sdf.bend(shape: SdfShape, radius: number): SdfShape
276
253
  ```
277
254
 
278
- Combine 2D sketches into a single profile (additive boolean). Accepts individual sketches or arrays.
255
+ #### `sdf.repeat()` Repeat an SDF shape in space.
279
256
 
280
- ---
257
+ ```ts
258
+ sdf.repeat(shape: SdfShape, spacing: Vec3, count?: Vec3): SdfShape
259
+ ```
281
260
 
282
- ## C3: Rigid Transform
261
+ #### `sdf.SurfacePattern()` A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`.
283
262
 
284
- Reposition or reorient geometry without changing its shape.
263
+ ```ts
264
+ sdf.SurfacePattern: typeof SurfacePattern
265
+ ```
285
266
 
286
- #### `degrees()`
267
+ #### `sdf.fromFunction()` — Create an SDF shape from an arbitrary distance function. You must provide bounds since the function is opaque.
287
268
 
288
269
  ```ts
289
- degrees(deg: number): number
270
+ sdf.fromFunction(fn: (x: number, y: number, z: number) => number, bounds: { min: Vec3; max: Vec3; }, constants?: Record<string, number>): SdfShape
290
271
  ```
291
272
 
292
- Convert degrees to degrees (identity for readability in scripts)
273
+ #### `circle2d()` Create a 2D circle centered at the origin.
293
274
 
294
- #### `radians()`
275
+ 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.
295
276
 
296
277
  ```ts
297
- radians(rad: number): number
278
+ circle2d(25).extrude(10); // smooth cylinder
279
+ circle2d(25, 6).extrude(10); // hexagonal prism
298
280
  ```
299
281
 
300
- Convert radians to degrees
282
+ ```ts
283
+ circle2d(radius: number, segments?: number): Sketch
284
+ ```
301
285
 
302
- #### `composeChain()`
286
+ #### `ellipse()` — Create a 2D ellipse centered at the origin.
303
287
 
304
288
  ```ts
305
- composeChain(...steps: TransformInput[]): Transform
289
+ ellipse(30, 15).extrude(5);
290
+ ellipse(30, 15, 32).extrude(5); // lower-resolution approximation
306
291
  ```
307
292
 
308
- Compose transforms in chain order. Equivalent to Transform.identity().mul(a).mul(b).mul(c)...
293
+ ```ts
294
+ ellipse(rx: number, ry: number, segments?: number): Sketch
295
+ ```
309
296
 
310
- ---
297
+ #### `loadFont()` — Pre-load and cache a font for use with `text2d()`.
311
298
 
312
- ## C4: Dimensional Promotion
299
+ 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.
313
300
 
314
- Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep).
301
+ Built-in font names that work everywhere (browser + CLI):
315
302
 
316
- #### `connectEdges()`
303
+ - `'sans-serif'` or `'inter'` — bundled Inter Regular
317
304
 
318
305
  ```ts
319
- connectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape
306
+ const font = loadFont('/path/to/Arial Bold.ttf');
307
+ text2d('Title', { size: 12, font }).extrude(1.5);
308
+ text2d('Subtitle', { size: 8, font }).extrude(1);
320
309
  ```
321
310
 
322
- <details><summary><code>EdgeSegment</code></summary>
323
-
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
- }
311
+ ```ts
312
+ loadFont(source: string | ArrayBuffer, cacheKey?: string): opentype.Font
345
313
  ```
346
314
 
347
- </details>
315
+ #### `ngon()` — Create a regular polygon inscribed in a circle of the given radius.
348
316
 
349
- <details><summary><code>TransitionCurveOptions</code></summary>
317
+ `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).
350
318
 
351
319
  ```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
- }
320
+ ngon(6, 20).extrude(10); // hexagonal prism, circumradius 20
360
321
  ```
361
322
 
362
- </details>
363
-
364
- <details><summary><code>TransitionSurfaceOptions</code> extends TransitionCurveOptions</summary>
365
-
366
323
  ```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
- }
324
+ ngon(sides: number, radius: number): Sketch
381
325
  ```
382
326
 
383
- </details>
384
-
385
- <details><summary><code>ConnectEdgesOptions</code> extends TransitionSurfaceOptions</summary>
386
-
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
- ```
327
+ #### `path()` — Create a new `PathBuilder` for tracing a 2D outline point by point.
407
328
 
408
- </details>
329
+ `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.
409
330
 
410
- #### `hermiteTransition()`
331
+ 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`.
411
332
 
412
333
  ```ts
413
- hermiteTransition(a: EdgeEndpoint, b: EdgeEndpoint): HermiteCurve3D
414
- ```
334
+ // Closed triangle
335
+ const triangle = path().moveTo(0, 0).lineH(50).lineV(30).close();
415
336
 
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
337
+ // L-shaped bracket as a stroke
338
+ const bracket = path().moveTo(0, 0).lineH(50).lineV(-70).lineAngled(20, 235).stroke(4);
417
339
 
418
- <details><summary><code>EdgeEndpoint</code></summary>
340
+ // Labeled edges for downstream face references
341
+ const slot = path()
342
+ .moveTo(0, 0)
343
+ .lineTo(30, 0).label('bottom')
344
+ .lineTo(30, 10)
345
+ .lineTo(0, 10).label('top')
346
+ .close();
347
+ ```
419
348
 
420
349
  ```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
- }
350
+ path(): PathBuilder
431
351
  ```
432
352
 
433
- </details>
353
+ #### `polygon()` — Create a 2D polygon from an array of `[x, y]` points or `Point2D` objects.
434
354
 
435
- #### `hermiteTransitionG2()`
355
+ Winding order is normalized automatically — clockwise (CW) input is silently reversed to CCW before being passed to the geometry kernel.
436
356
 
437
357
  ```ts
438
- hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D
358
+ polygon([[0, 0], [50, 0], [25, 40]]).extrude(5); // triangle
439
359
  ```
440
360
 
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.
442
-
443
- <details><summary><code>QuinticHermiteCurveEndpoint</code></summary>
444
-
445
361
  ```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
- }
362
+ polygon(points: ([ number, number ] | Point2D)[]): Sketch
456
363
  ```
457
364
 
458
- </details>
365
+ #### `polygonVertices()` — Compute the vertex positions of a regular polygon.
459
366
 
460
- #### `loft()`
367
+ Default orientation places the first vertex at the top (90 degrees), matching the convention used by `ngon()`.
461
368
 
462
- ```ts
463
- loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
464
- ```
369
+ Eliminates manual Math.sqrt(3) for triangles, pentagon vertex math, etc:
465
370
 
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().
371
+ ```js
372
+ // Before — manual equilateral triangle
373
+ const v1 = [center.x - r/2, center.y + r * Math.sqrt(3)/2];
374
+ const v2 = [center.x - r/2, center.y - r * Math.sqrt(3)/2];
375
+ const v3 = [center.x + r, center.y];
467
376
 
468
- <details><summary><code>LoftOptions</code></summary>
377
+ // After — declarative
378
+ const [v1, v2, v3] = polygonVertices(3, r);
379
+ ```
469
380
 
470
381
  ```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
- }
382
+ polygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[]
477
383
  ```
478
384
 
479
- </details>
385
+ **`PolygonVerticesOptions`**
386
+ - `startDeg?: number` — Angle of the first vertex in degrees (default: 90 = top).
387
+ - `centerX?: number` — Center X coordinate (default: 0).
388
+ - `centerY?: number` — Center Y coordinate (default: 0).
389
+
390
+ `LayoutPoint`: `{ x: number, y: number }`
480
391
 
481
- #### `loftAlongSpine()`
392
+ #### `rect()` — Create a 2D rectangle centered at the origin.
482
393
 
483
394
  ```ts
484
- loftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3$4[], tValues: number[], options?: LoftAlongSpineOptions): Shape
395
+ rect(40, 20).extrude(5);
485
396
  ```
486
397
 
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().
488
-
489
- <details><summary><code>LoftAlongSpineOptions</code></summary>
490
-
491
398
  ```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
- }
399
+ rect(width: number, height: number): Sketch
502
400
  ```
503
401
 
504
- </details>
402
+ #### `arcSlot()` — Create an arc-shaped slot (banana / annular sector) centered at the origin.
505
403
 
506
- #### `spline3d()`
404
+ 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.
507
405
 
508
406
  ```ts
509
- spline3d(points: Vec3$4[], options?: Spline3DOptions): Curve3D
407
+ arcSlot(135, 74, 40).extrude(5); // pitch R135, 74° sweep, 40mm wide
510
408
  ```
511
409
 
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.
513
-
514
- <details><summary><code>Spline3DOptions</code></summary>
515
-
516
410
  ```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
- }
411
+ arcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch
523
412
  ```
524
413
 
525
- </details>
414
+ #### `roundedRect()` — Create a 2D rectangle with rounded corners, centered at the origin.
526
415
 
527
- #### `surfacePatch()`
416
+ The corner radius is automatically clamped to `min(width/2, height/2)` so it can never exceed the shape dimensions.
528
417
 
529
418
  ```ts
530
- surfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape
419
+ roundedRect(60, 30, 5).extrude(3);
531
420
  ```
532
421
 
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.
534
-
535
- <details><summary><code>SurfacePatchOptions</code></summary>
536
-
537
422
  ```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
- }
423
+ roundedRect(width: number, height: number, radius: number): Sketch
544
424
  ```
545
425
 
546
- </details>
547
-
548
- #### `sweep()`
426
+ #### `slot()` — Create a slot (oblong / stadium shape) — a rectangle with semicircular ends, centered at the origin.
549
427
 
550
428
  ```ts
551
- sweep(profile: Sketch, path: Curve3D | Vec3$4[], options?: SweepOptions): Shape
429
+ slot(40, 10).extrude(3); // 40mm long, 10mm wide slot
552
430
  ```
553
431
 
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.
555
-
556
- <details><summary><code>SweepOptions</code></summary>
557
-
558
432
  ```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
- }
433
+ slot(length: number, width: number): Sketch
569
434
  ```
570
435
 
571
- </details>
436
+ #### `spline2d()` — Build a smooth Catmull-Rom spline sketch from 2D control points.
572
437
 
573
- #### `variableSweep()`
438
+ 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.
574
439
 
575
440
  ```ts
576
- variableSweep(spine: Curve3D | Vec3$4[], sections: VariableSweepSection[], options?: VariableSweepOptions): Shape
441
+ spline2d(points: Vec2[], options?: Spline2DOptions): Sketch
577
442
  ```
578
443
 
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.
444
+ **`Spline2DOptions`**
580
445
 
581
- <details><summary><code>VariableSweepSection</code></summary>
446
+ | Option | Type | Description |
447
+ |--------|------|-------------|
448
+ | `closed?` | `boolean` | Closed loop (default true). |
449
+ | `tension?` | `number` | Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5. |
450
+ | `samplesPerSegment?` | `number` | Samples per segment (minimum 3). Default 16. |
451
+ | `strokeWidth?` | `number` | For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. |
452
+ | `join?` | `"Round" | "Square"` | Stroke join for open splines. Default 'Round'. |
453
+
454
+ #### `star()` — Create a star shape with alternating outer and inner radii.
582
455
 
583
456
  ```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
- }
457
+ star(5, 30, 12).extrude(4); // five-pointed star
590
458
  ```
591
459
 
592
- </details>
593
-
594
- <details><summary><code>VariableSweepOptions</code></summary>
595
-
596
460
  ```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
- }
461
+ star(points: number, outerR: number, innerR: number): Sketch
607
462
  ```
608
463
 
609
- </details>
610
-
611
- #### `transitionCurve()`
464
+ #### `stroke()` — Create a stroked polyline sketch from an array of 2D points.
612
465
 
613
466
  ```ts
614
- transitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D
467
+ stroke(points: [ number, number ][], width: number, join?: "Round" | "Square"): Sketch
615
468
  ```
616
469
 
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 }, ); ```
618
-
619
- <details><summary><code>TransitionEdge</code></summary>
470
+ #### `text2d()` Build a filled 2D Sketch from a text string.
620
471
 
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
- }
630
- ```
472
+ 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.
631
473
 
632
- </details>
474
+ Alignment reference table:
633
475
 
634
- #### `transitionCurveFromPoints()`
476
+ | `align` | `baseline` | Origin | |------------|--------------|-------------------------------------| | `'left'` | `'baseline'` | Bottom-left of first char (default) | | `'center'` | `'center'` | Dead center of text block | | `'right'` | `'top'` | Top-right corner |
635
477
 
636
478
  ```ts
637
- transitionCurveFromPoints(startPoint: Vec3$7, startTangent: Vec3$7, endPoint: Vec3$7, endTangent: Vec3$7, options?: TransitionCurveOptions): HermiteCurve3D
638
- ```
479
+ // Extruded nameplate
480
+ text2d('FORGE CAD', { size: 8 }).extrude(1.2);
639
481
 
640
- Convenience: create a transition curve from raw coordinate data. Useful when you have endpoints and directions as plain arrays without constructing TransitionEdge objects.
482
+ // Centered label on the XY plane
483
+ text2d('V 2.0', { size: 6, align: 'center', baseline: 'center' });
641
484
 
642
- #### `transitionSurface()`
485
+ // Engraved text cut into the top face of a box
486
+ const label = text2d('REV A', { size: 5, align: 'center', baseline: 'center' });
487
+ plate.subtract(label.onFace(plate, 'top', { protrude: -0.5 }).extrude(1));
643
488
 
644
- ```ts
645
- transitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape
489
+ // Custom TTF font
490
+ text2d('Hello', { size: 10, font: '/path/to/Arial.ttf' }).extrude(1);
491
+
492
+ // Pre-loaded font for reuse
493
+ const font = loadFont('/path/to/Arial Bold.ttf');
494
+ text2d('Title', { size: 12, font }).extrude(1.5);
646
495
  ```
647
496
 
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 }, ); ```
497
+ ```ts
498
+ text2d(content: string, options?: TextOptions): Sketch
499
+ ```
649
500
 
650
- ---
501
+ **`TextOptions`**
651
502
 
652
- ## C5: Topology Query
503
+ | Option | Type | Description |
504
+ |--------|------|-------------|
505
+ | `size?` | `number` | Cap height of the text in model units. All other dimensions (stroke weight, spacing) scale proportionally. |
506
+ | `letterSpacing?` | `number` | Extra space between characters in model units. Negative values tighten the tracking. |
507
+ | `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 |
508
+ | `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 |
509
+ | `font?` | `string | opentype.Font` | Font to use for text rendering. - `'sans-serif'` or `'inter'` — bundled Inter font (works everywhere, including browser) - **file path** — path to a TTF, OTF, or WOFF font file (CLI/Node only) - **Font object** — a previously loaded opentype.js Font (from `loadFont()`) - **omitted** — uses the bundled Inter font (same as `'sans-serif'`) text2d('Hello World', { size: 10 }) // default Inter text2d('Custom Font', { size: 10, font: '/path/to/font.ttf' }) |
510
+ | `flattenTolerance?` | `number` | Bezier flattening tolerance in model units. Smaller = more polygon segments = smoother curves. |
653
511
 
654
- Select or inspect named faces and edges on a shape.
512
+ #### `textWidth()` Measure the rendered advance width of a string without creating any geometry.
655
513
 
656
- #### `coalesceEdges()`
514
+ 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.
657
515
 
658
516
  ```ts
659
- coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]
517
+ const w = textWidth('SERIAL: 001', { size: 6 });
518
+ const plate = box(w + 10, 12, 2);
660
519
  ```
661
520
 
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.
663
-
664
- <details><summary><code>EdgeSegment</code></summary>
665
-
666
- ```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;
686
- }
521
+ ```ts
522
+ textWidth(content: string, options?: Pick<TextOptions, "size" | "letterSpacing" | "font">): number
687
523
  ```
688
524
 
689
- </details>
525
+ #### `box()` — Create a rectangular box. Centered on XY, base at Z=0.
690
526
 
691
- #### `selectEdge()`
527
+ For named faces, build from a labeled sketch: `rect(x, y).labelEdges('s', 'e', 'n', 'w').extrude(z, { labels: { start: 'bottom', end: 'top' } })`.
692
528
 
693
529
  ```ts
694
- selectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment
695
- ```
696
-
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.
698
-
699
- <details><summary><code>EdgeQuery</code></summary>
700
-
701
- ```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
- }
530
+ box(x: number, y: number, z: number): Shape
730
531
  ```
731
532
 
732
- </details>
533
+ #### `cylinder()` — Create a cylinder or cone with named faces and edges. Centered on XY, base at Z=0.
733
534
 
734
- <details><summary><code>BoundingRegion</code></summary>
535
+ 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.
735
536
 
736
537
  ```ts
737
- interface BoundingRegion {
738
- xMin?: number;
739
- xMax?: number;
740
- yMin?: number;
741
- yMax?: number;
742
- zMin?: number;
743
- zMax?: number;
744
- }
538
+ cylinder(height: number, radius: number, radiusTop?: number, segments?: number): Shape
745
539
  ```
746
540
 
747
- </details>
748
-
749
- #### `selectEdges()`
541
+ #### `sphere()` — Create a sphere centered at the origin. Use segments for lower-poly approximations.
750
542
 
751
543
  ```ts
752
- selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]
544
+ sphere(radius: number, segments?: number): Shape
753
545
  ```
754
546
 
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.
547
+ #### `torus()` Create a torus (donut shape) lying in the XY plane. Centered on all axes (origin is the ring center).
548
+
549
+ ```ts
550
+ torus(majorRadius: number, minorRadius: number, segments?: number): Shape
551
+ ```
756
552
 
757
553
  ---
758
554
 
759
- ## C6: Edge Feature
555
+ ## C2: Boolean Combination
760
556
 
761
- Modify edges of a solid fillets, chamfers, draft, offset.
557
+ Combine same-dimension geometry using CSG set operations.
558
+
559
+ #### `difference2d()` — Subtract one or more 2D sketches from a base sketch.
762
560
 
763
- #### `filletEdgeSegment()`
561
+ 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.
764
562
 
765
563
  ```ts
766
- filletEdgeSegment(shape: Shape, segment: EdgeSegment, radius: number, segments?: number): Shape
564
+ const donut = difference2d(circle2d(50), circle2d(30));
767
565
  ```
768
566
 
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().
770
-
771
- <details><summary><code>EdgeSegment</code></summary>
772
-
773
567
  ```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
- }
568
+ difference2d(...inputs: SketchOperandInput[]): Sketch
794
569
  ```
795
570
 
796
- </details>
571
+ #### `intersection2d()` — Keep only the area where all input sketches overlap (intersection boolean).
797
572
 
798
- #### `chamferEdgeSegment()`
573
+ 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.
799
574
 
800
575
  ```ts
801
- chamferEdgeSegment(shape: Shape, segment: EdgeSegment, size: number): Shape
576
+ const lens = intersection2d(circle2d(30).translate(-10, 0), circle2d(30).translate(10, 0));
802
577
  ```
803
578
 
804
- Apply a chamfer (beveled edge) to a mesh-selected edge. Works on any straight edge of any shape — not limited to tracked box edges.
805
-
806
- #### `chamfer()`
807
-
808
579
  ```ts
809
- chamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape
580
+ intersection2d(...inputs: SketchOperandInput[]): Sketch
810
581
  ```
811
582
 
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] })
583
+ #### `union2d()` Combine 2D sketches into a single profile using an additive boolean union.
813
584
 
814
- #### `draft()`
585
+ 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.
815
586
 
816
587
  ```ts
817
- draft(shape: Shape, angleDeg: number, pullDirection?: [ number, number, number ], neutralPlaneOffset?: number): Shape
588
+ const cross = union2d(rect(60, 10), rect(10, 60));
818
589
  ```
819
590
 
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)
821
-
822
- #### `fillet()`
823
-
824
591
  ```ts
825
- fillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape
592
+ union2d(...inputs: SketchOperandInput[]): Sketch
826
593
  ```
827
594
 
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)
595
+ #### `union()` Combine shapes into a single solid (additive boolean).
829
596
 
830
- #### `offsetSolid()`
597
+ Accepts individual shapes, or an array of shapes. The first operand's color is preserved in the result.
831
598
 
832
599
  ```ts
833
- offsetSolid(shape: Shape, thickness: number): Shape
600
+ union(...inputs: ShapeOperandInput[]): Shape
834
601
  ```
835
602
 
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)
603
+ #### `difference()` Subtract shapes from a base shape (subtractive boolean).
837
604
 
838
- #### `filletCorners()`
605
+ The first shape is the base; all subsequent shapes are subtracted from it. Accepts individual shapes, or an array of shapes.
839
606
 
840
607
  ```ts
841
- filletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch
608
+ difference(...inputs: ShapeOperandInput[]): Shape
842
609
  ```
843
610
 
844
- Create a polygon from points with specified corners rounded to arc fillets. Each corner spec identifies a vertex index and radius.
611
+ #### `intersection()` Keep only the overlapping volume of the input shapes (intersection boolean).
845
612
 
846
- <details><summary><code>FilletCornerSpec</code></summary>
613
+ Requires at least two shapes. Accepts individual shapes, or an array.
847
614
 
848
615
  ```ts
849
- interface FilletCornerSpec {
850
- index: number;
851
- radius: number;
852
- segments?: number;
853
- }
616
+ intersection(...inputs: ShapeOperandInput[]): Shape
854
617
  ```
855
618
 
856
- </details>
857
-
858
619
  ---
859
620
 
860
- ## C7: Pattern Replication
621
+ ## C3: Rigid Transform
861
622
 
862
- Duplicate geometry in regular arrangements (linear, circular, mirror).
623
+ Reposition or reorient geometry without changing its shape.
624
+
625
+ #### `degrees()` — Identity function that returns degrees unchanged.
863
626
 
864
- #### `circularLayout()`
627
+ Use for clarity when the unit of an angle value would otherwise be ambiguous — e.g. `param("Angle", degrees(45))`.
865
628
 
866
629
  ```ts
867
- circularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[]
630
+ degrees(deg: number): number
868
631
  ```
869
632
 
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)); } ```
633
+ #### `radians()`Convert radians to degrees.
871
634
 
872
- <details><summary><code>CircularLayoutOptions</code></summary>
635
+ 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.
873
636
 
874
637
  ```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;
882
- }
638
+ radians(rad: number): number
883
639
  ```
884
640
 
885
- </details>
886
-
887
- <details><summary><code>LayoutPoint</code></summary>
641
+ #### `composeChain()` — Compose transforms in chain order. Equivalent to Transform.identity().mul(a).mul(b).mul(c)...
888
642
 
889
643
  ```ts
890
- interface LayoutPoint {
891
- x: number;
892
- y: number;
893
- }
644
+ composeChain(...steps: TransformInput[]): Transform
894
645
  ```
895
646
 
896
- </details>
647
+ ---
897
648
 
898
- #### `circularPattern()`
649
+ ## C4: Dimensional Promotion
899
650
 
900
- ```ts
901
- circularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape
902
- ```
651
+ Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep).
903
652
 
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] })
653
+ #### `nurbs3d()` Create a NURBS curve from control points.
905
654
 
906
- <details><summary><code>CircularPatternOptions</code></summary>
655
+ With default options, creates a cubic non-rational B-spline with uniform clamped knots. Set `weights` for rational curves (exact circles, conics). Set `degree` for linear (1), quadratic (2), cubic (3), or higher-order curves.
907
656
 
908
- ```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
- }
657
+ ```js
658
+ // Simple cubic B-spline through control points
659
+ const curve = nurbs3d([[0,0,0], [10,5,0], [20,-5,10], [30,0,5]]);
660
+ const tube = sweep(circle(2), curve);
915
661
  ```
916
662
 
917
- </details>
918
-
919
- #### `circularPattern2d()`
663
+ ```js
664
+ // Rational quadratic — exact circular arc
665
+ const arc = nurbs3d(
666
+ [[10,0,0], [10,10,0], [0,10,0]],
667
+ { degree: 2, weights: [1, Math.SQRT1_2, 1] }
668
+ );
669
+ ```
920
670
 
921
671
  ```ts
922
- circularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | { centerX?: number; centerY?: number; startDeg?: number; }, centerY?: number): Sketch
672
+ nurbs3d(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D
923
673
  ```
924
674
 
925
- Repeat a sketch in a circular pattern around a center point
675
+ **`NurbsCurve3DOptions`**
926
676
 
927
- #### `linearPattern()`
677
+ | Option | Type | Description |
678
+ |--------|------|-------------|
679
+ | `degree?` | `number` | Polynomial degree (default 3 = cubic). Must be ≥ 1. |
680
+ | `weights?` | `number[]` | Rational weights, one per control point (default: all 1.0 = non-rational). |
681
+ | `knots?` | `number[]` | Knot vector (default: uniform clamped). Must have length = controlPoints.length + degree + 1. |
682
+ | `closed?` | `boolean` | Whether the curve is closed/periodic (default false). |
928
683
 
929
- ```ts
930
- linearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape
931
- ```
684
+ #### `nurbsSurface()` — Create a NURBS surface from a grid of control points.
932
685
 
933
- Repeat a shape in a linear pattern along a direction vector and union the copies.
686
+ The control grid is indexed as `controlGrid[u][v]` each row is a curve in the V direction, and columns trace curves in the U direction.
934
687
 
935
- #### `linearPattern2d()`
688
+ With default options, creates a bicubic non-rational B-spline surface with uniform clamped knots.
936
689
 
937
- ```ts
938
- linearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch
690
+ ```js
691
+ // Simple 4×4 control grid a gently curved surface
692
+ const grid = [
693
+ [[0,0,0], [10,0,2], [20,0,2], [30,0,0]],
694
+ [[0,10,1], [10,10,5], [20,10,5], [30,10,1]],
695
+ [[0,20,1], [10,20,5], [20,20,5], [30,20,1]],
696
+ [[0,30,0], [10,30,2], [20,30,2], [30,30,0]],
697
+ ];
698
+ const surface = nurbsSurface(grid, { thickness: 2 });
939
699
  ```
940
700
 
941
- Repeat a sketch in a linear pattern
942
-
943
- #### `mirrorCopy()`
944
-
945
701
  ```ts
946
- mirrorCopy(shape: Shape, normal: [ number, number, number ]): Shape
702
+ nurbsSurface(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape
947
703
  ```
948
704
 
949
- Mirror a shape across a plane defined by its normal and union the mirror with the original.
705
+ **`NurbsSurfaceOptions`**
950
706
 
951
- ---
952
-
953
- ## C8: Constraint Solving
707
+ | Option | Type | Description |
708
+ |--------|------|-------------|
709
+ | `degreeU?` | `number` | Degree in U direction (default 3). |
710
+ | `degreeV?` | `number` | Degree in V direction (default 3). |
711
+ | `weights?` | `number[][]` | Weights grid — same dimensions as controlGrid (default: all 1.0). |
712
+ | `knotsU?` | `number[]` | Knot vector in U direction (default: uniform clamped). |
713
+ | `knotsV?` | `number[]` | Knot vector in V direction (default: uniform clamped). |
714
+ | `thickness?` | `number` | Sheet thickness — if > 0, thickens the surface into a solid (default 0 = surface only). |
715
+ | `resolution?` | `number` | Tessellation resolution — points per direction (default 32). |
954
716
 
955
- Define geometry by relationships and let a solver find positions.
717
+ #### `connectEdges()` Create a transition surface or solid bridge between two edge segments.
956
718
 
957
- #### `addPolygon()`
719
+ 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.
958
720
 
959
721
  ```ts
960
- addPolygon(sk: ConstrainedSketchBuilder, options: PolygonOptions): ConstrainedPolygon
722
+ connectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape
961
723
  ```
962
724
 
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); ```
725
+ **`EdgeSegment`**
964
726
 
965
- <details><summary><code>PolygonOptions</code></summary>
727
+ | Option | Type | Description |
728
+ |--------|------|-------------|
729
+ | `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |
730
+ | `direction` | `Vec3` | Normalized direction from start → end. |
731
+ | `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |
732
+ | `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |
733
+ | `normalA` | `Vec3` | Normal of first adjacent face. |
734
+ | `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |
735
+ | `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |
736
+ | `start`, `end`, `midpoint`, `length` | | — |
966
737
 
967
- ```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
- }
974
- ```
738
+ **`TransitionCurveOptions`**
739
+ - `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
740
+ - `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
741
+ - `samples?: number` — Number of sample points for the output polyline. Default 64. Higher values give smoother curves at the cost of more geometry.
975
742
 
976
- </details>
743
+ **`TransitionSurfaceOptions`** extends TransitionCurveOptions
977
744
 
978
- <details><summary><code>ConstrainedPolygon</code></summary>
745
+ | Option | Type | Description |
746
+ |--------|------|-------------|
747
+ | `profile?` | `Sketch` | Cross-section profile to sweep along the transition curve. If omitted, a circular profile with `radius` is used. |
748
+ | `radius?` | `number` | Radius of circular cross-section (used when `profile` is omitted). Default: 5% of chord length. |
749
+ | `up?` | `Vec3` | Preferred up vector for the sweep frame. Default: auto-detected. |
750
+ | `edgeLength?` | `number` | Edge length for level-set meshing. Smaller = finer. |
751
+ | `boundsPadding?` | `number` | Extra bounds padding for level-set meshing. |
752
+ | `width`, `height` | | — |
979
753
 
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
- ```
754
+ **`ConnectEdgesOptions`** extends TransitionSurfaceOptions
990
755
 
991
- </details>
756
+ | Option | Type | Description |
757
+ |--------|------|-------------|
758
+ | `endA?` | `EdgeEnd` | Which end of edge A to connect. Default: 'start'. |
759
+ | `endB?` | `EdgeEnd` | Which end of edge B to connect. Default: 'start'. |
760
+ | `tangentModeA?` | `TangentMode` | Tangent mode for edge A. Default: 'along'. |
761
+ | `tangentModeB?` | `TangentMode` | Tangent mode for edge B. Default: 'along'. |
762
+ | `tangentA?` | `Vec3` | Explicit tangent for edge A. |
763
+ | `tangentB?` | `Vec3` | Explicit tangent for edge B. |
764
+ | `flipA?` | `boolean` | Flip tangent A. |
765
+ | `flipB?` | `boolean` | Flip tangent B. |
992
766
 
993
- #### `addRect()`
767
+ #### `hermiteTransitionG2()` — Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).
768
+
769
+ 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.
994
770
 
995
771
  ```ts
996
- addRect(sk: ConstrainedSketchBuilder, options?: RectOptions): ConstrainedRect
772
+ hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D
997
773
  ```
998
774
 
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); ```
775
+ **`QuinticHermiteCurveEndpoint`**
776
+
777
+ | Option | Type | Description |
778
+ |--------|------|-------------|
779
+ | `point` | `Vec3` | Position |
780
+ | `tangent` | `Vec3` | Tangent direction (will be normalized internally) |
781
+ | `curvature?` | `Vec3` | Second derivative / curvature vector. Default [0, 0, 0]. |
782
+ | `weight?` | `number` | Weight: scales tangent magnitude relative to chord length. Default 1.0. |
1000
783
 
1001
- <details><summary><code>RectOptions</code></summary>
784
+ #### `loft()` — Loft between multiple sketches along Z stations.
785
+
786
+ 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.
787
+
788
+ Performance note: loft is significantly heavier than primitive/extrude/revolve. If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().
1002
789
 
1003
790
  ```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
- }
791
+ loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
1016
792
  ```
1017
793
 
1018
- </details>
1019
-
1020
- <details><summary><code>ConstrainedRect</code></summary>
1021
-
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
- ```
794
+ **`LoftOptions`**
795
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
796
+ - `boundsPadding?: number` — Optional extra bounds padding.
1042
797
 
1043
- </details>
798
+ #### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.
1044
799
 
1045
- #### `addRegularPolygon()`
800
+ 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.
1046
801
 
1047
- ```ts
1048
- addRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions): ConstrainedRegularPolygon
1049
- ```
802
+ 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].
1050
803
 
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) ```
804
+ Internally uses variableSweep infrastructure with SDF interpolation.
1052
805
 
1053
- <details><summary><code>RegularPolygonOptions</code></summary>
806
+ Performance note: uses level-set meshing, heavier than simple loft().
1054
807
 
1055
808
  ```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
- }
809
+ loftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3[], tValues: number[], options?: LoftAlongSpineOptions): Shape
1070
810
  ```
1071
811
 
1072
- </details>
812
+ **`LoftAlongSpineOptions`**
1073
813
 
814
+ | Option | Type | Description |
815
+ |--------|------|-------------|
816
+ | `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |
817
+ | `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |
818
+ | `boundsPadding?` | `number` | Optional extra bounds padding. |
819
+ | `up?` | `Vec3` | Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. |
1074
820
 
1075
- <details><summary><code>ConstrainedRegularPolygon</code> extends ConstrainedPolygon</summary>
821
+ #### `spline3d()` — Create a reusable 3D spline curve object (Catmull-Rom).
822
+
823
+ The returned Curve3D provides sample(), pointAt(t), tangentAt(t), and length() for downstream use in sweep() or manual path operations.
1076
824
 
1077
825
  ```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
- }
826
+ spline3d(points: Vec3[], options?: Spline3DOptions): Curve3D
1082
827
  ```
1083
828
 
1084
- </details>
829
+ **`Spline3DOptions`**
830
+ - `closed?: boolean` — Closed loop (default false).
831
+ - `tension?: number` — Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5.
1085
832
 
1086
- #### `circle()`
833
+ #### `surfacePatch()` — Create a smooth surface patch from 4 boundary curves (Coons patch).
1087
834
 
1088
- ```ts
1089
- circle(cx: number, cy: number, radius: number): Circle2D
1090
- ```
835
+ The four curves form the boundary of a quadrilateral patch:
836
+
837
+ - bottom: u=0..1 at v=0 (from corner00 to corner10)
838
+ - top: u=0..1 at v=1 (from corner01 to corner11)
839
+ - left: v=0..1 at u=0 (from corner00 to corner01)
840
+ - right: v=0..1 at u=1 (from corner10 to corner11)
1091
841
 
1092
- Create an analytic 2D circle for measurement, construction, and extrusion. Provides diameter, circumference, area, and toSketch().
842
+ The interior is filled using bilinear Coons patch interpolation: P(u,v) = Lc(u,v) + Ld(u,v) - B(u,v)
1093
843
 
1094
- #### `constrainedSketch()`
844
+ The result is a thin solid created by offsetting the surface mesh along its normals by the specified thickness.
845
+
846
+ Note: curves should meet at corners. Small gaps are tolerated.
1095
847
 
1096
848
  ```ts
1097
- constrainedSketch(options?: ConstrainedSketchOptions): ConstrainedSketchBuilder
849
+ surfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape
1098
850
  ```
1099
851
 
1100
- Build a parametric 2D sketch with geometric constraints solved by the built-in constraint solver.
852
+ **`SurfacePatchOptions`**
853
+ - `resolution?: number` — Number of samples along each direction. Default 24.
854
+ - `thickness?: number` — Thickness of the generated solid. Default 0.5.
1101
855
 
1102
- <details><summary><code>ConstrainedSketchOptions</code></summary>
856
+ #### `sweep()`
1103
857
 
1104
858
  ```ts
1105
- interface ConstrainedSketchOptions {
1106
- /** When true, adding a constraint that cannot be satisfied throws instead of silently discarding it. */
1107
- strict?: boolean;
1108
- }
859
+ sweep(profile: Sketch, path: SweepPathInput, options?: SweepOptions): Shape
1109
860
  ```
1110
861
 
1111
- </details>
862
+ **`SweepOptions`**
1112
863
 
1113
- #### `line()`
864
+ | Option | Type | Description |
865
+ |--------|------|-------------|
866
+ | `samples?` | `number` | Number of samples when path is a Curve3D. Default 48. |
867
+ | `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |
868
+ | `boundsPadding?` | `number` | Optional extra bounds padding. |
869
+ | `up?` | `Vec3` | Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. |
1114
870
 
1115
- ```ts
1116
- line(x1: number, y1: number, x2: number, y2: number): Line2D
1117
- ```
871
+ #### `variableSweep()` — Sweep a variable cross-section along a 3D spine curve.
1118
872
 
1119
- Create an analytic 2D line segment between two points. Provides length, midpoint, angle, intersection, and parallel helpers.
873
+ 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.
1120
874
 
1121
- #### `point()`
875
+ 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.
876
+
877
+ Performance note: like sweep(), this uses level-set meshing internally.
1122
878
 
1123
879
  ```ts
1124
- point(x: number, y: number): Point2D
880
+ variableSweep(spine: SweepPathInput, sections: VariableSweepSection[], options?: VariableSweepOptions): Shape
1125
881
  ```
1126
882
 
1127
- Create an analytic 2D point for measurement and construction geometry.
1128
-
1129
- ---
883
+ **`VariableSweepSection`**
884
+ - `t: number` — Parameter along the spine (0 = start, 1 = end).
885
+ - `profile: Sketch` — Cross-section profile at this station.
1130
886
 
1131
- ## C9: Spatial Placement
887
+ **`VariableSweepOptions`**
1132
888
 
1133
- Position geometry relative to other geometry using semantic anchors.
889
+ | Option | Type | Description |
890
+ |--------|------|-------------|
891
+ | `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |
892
+ | `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |
893
+ | `boundsPadding?` | `number` | Optional extra bounds padding. |
894
+ | `up?` | `Vec3` | Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. |
1134
895
 
1135
- *No free functions see class methods (Shape, Sketch, ConstrainedSketchBuilder).*
896
+ #### `transitionCurve()`Create a smooth transition curve between two edges.
1136
897
 
1137
- ---
898
+ Returns a `HermiteCurve3D` that starts at `edgeA.point` tangent to `edgeA.tangent` and ends at `edgeB.point` tangent to `edgeB.tangent`.
1138
899
 
1139
- ## C10: Assembly & Kinematics
900
+ The curve maintains G1 continuity (matching tangent direction) at both endpoints. Weight parameters control the shape of the transition.
1140
901
 
1141
- Compose parts with joints for kinematic simulation.
902
+ ```js
903
+ ```js
1142
904
 
1143
- #### `assembly()`
905
+ // 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] }, );
1144
906
 
1145
- ```ts
1146
- assembly(name?: string): Assembly
1147
907
  ```
1148
908
 
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.
1150
-
1151
- #### `bomToCsv()`
909
+ // Weighted: curve hugs edge A longer
910
+ const weighted = transitionCurve(
911
+ { point: [0, 0, 0], tangent: [1, 0, 0] },
912
+ { point: [10, 5, 0], tangent: [1, 0, 0] },
913
+ { weightA: 2.0, weightB: 0.5 },
914
+ );
915
+ ```
1152
916
 
1153
917
  ```ts
1154
- bomToCsv(rows: BomRow[]): string
918
+ transitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D
1155
919
  ```
1156
920
 
1157
- Convert BOM rows from a solved assembly into a CSV string.
921
+ **`TransitionEdge`**
922
+ - `point: Vec3` — Connection point on the edge. Can be any point along the edge where the transition should connect.
923
+ - `tangent: Vec3` — Tangent direction at the connection point. This is the direction the curve should initially follow when leaving this edge. For a straight edge, this is typically the edge direction pointing "outward" (away from the body of the edge, toward the other edge).
924
+ - `normal?: Vec3` — Surface normal at the connection point (optional). Used as a hint for the sweep frame's up vector.
1158
925
 
1159
- <details><summary><code>BomRow</code></summary>
926
+ #### `transitionSurface()` — Create a solid transition surface between two edges by sweeping a profile along a Hermite transition curve.
1160
927
 
1161
- ```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
- }
1171
- ```
928
+ This produces a watertight solid that smoothly connects the two edges. Works with both Manifold and OCCT backends.
1172
929
 
1173
- </details>
930
+ ```js
931
+ ```js
1174
932
 
1175
- <details><summary><code>PartMetadata</code></summary>
933
+ // 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 }, );
1176
934
 
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
935
  ```
1188
936
 
1189
- </details>
937
+ // Custom profile with weights
938
+ const custom = transitionSurface(
939
+ { point: [0, 0, 0], tangent: [1, 0, 0] },
940
+ { point: [10, 5, 3], tangent: [0, 1, 0] },
941
+ { profile: mySketch, weightA: 1.5, weightB: 0.8 },
942
+ );
943
+ ```
944
+
945
+ ```ts
946
+ transitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape
947
+ ```
948
+
949
+ ---
950
+
951
+ ## C5: Topology Query
952
+
953
+ Select or inspect named faces and edges on a shape.
954
+
955
+ #### `coalesceEdges()` — Merge collinear edge segments into longer logical edges.
956
+
957
+ 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.
958
+
959
+ The `tolerance` controls the maximum perpendicular distance from collinearity before two segments are considered non-collinear. Default: `0.01`.
960
+
961
+ ```ts
962
+ const topEdges = selectEdges(part, { atZ: 20 });
963
+ for (const edge of coalesceEdges(topEdges)) {
964
+ result = fillet(result, 2, edge);
965
+ }
966
+ ```
967
+
968
+ ```ts
969
+ coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]
970
+ ```
971
+
972
+ **`EdgeSegment`**
973
+
974
+ | Option | Type | Description |
975
+ |--------|------|-------------|
976
+ | `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |
977
+ | `direction` | `Vec3` | Normalized direction from start → end. |
978
+ | `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |
979
+ | `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |
980
+ | `normalA` | `Vec3` | Normal of first adjacent face. |
981
+ | `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |
982
+ | `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |
983
+ | `start`, `end`, `midpoint`, `length` | | — |
984
+
985
+ #### `selectEdge()` — Select the single best-matching edge from a shape.
986
+
987
+ 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.
988
+
989
+ ```ts
990
+ // Chamfer one specific edge near a known point
991
+ const bottomEdge = selectEdge(part, { near: [25, 0, 0], atZ: 0 });
992
+ result = chamfer(result, 1.5, bottomEdge);
993
+ ```
994
+
995
+ ```ts
996
+ selectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment
997
+ ```
998
+
999
+ **`EdgeQuery`**
1000
+
1001
+ | Option | Type | Description |
1002
+ |--------|------|-------------|
1003
+ | `near?` | `Vec3` | Sort by proximity to this point (closest first). When used with `selectEdge`, picks the closest match. |
1004
+ | `parallel?` | `Vec3` | Filter: edge direction approximately parallel to this vector. |
1005
+ | `perpendicular?` | `Vec3` | Filter: edge direction approximately perpendicular to this vector. |
1006
+ | `convex?` | `boolean` | Filter: only convex (outside corner) edges. |
1007
+ | `concave?` | `boolean` | Filter: only concave (inside corner) edges. |
1008
+ | `minAngle?` | `number` | Filter: minimum dihedral angle in degrees. |
1009
+ | `maxAngle?` | `number` | Filter: maximum dihedral angle in degrees. |
1010
+ | `minLength?` | `number` | Filter: minimum edge length. |
1011
+ | `maxLength?` | `number` | Filter: maximum edge length. |
1012
+ | `within?` | `BoundingRegion` | Filter: edge midpoint must be within this bounding region. |
1013
+ | `atZ?` | `number` | Shorthand: edge midpoint Z ≈ this value (within `tolerance`). Equivalent to `within: { zMin: atZ - tol, zMax: atZ + tol }`. |
1014
+ | `tolerance?` | `number` | Position tolerance for approximate matches (default: `1.0`). Used by `atZ` and `near`. |
1015
+ | `angleTolerance?` | `number` | Angular tolerance in degrees for `parallel`/`perpendicular` filters (default: `10`). |
1016
+
1017
+ `BoundingRegion`: `{ xMin?: number, xMax?: number, yMin?: number, yMax?: number, zMin?: number, zMax?: number }`
1018
+
1019
+ #### `selectEdges()` — Select all edges from a shape that match the given query.
1020
+
1021
+ 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.
1022
+
1023
+ 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.
1024
+
1025
+ ```ts
1026
+ // Fillet all top edges of a box
1027
+ const topEdges = selectEdges(part, { atZ: 20, perpendicular: [0, 0, 1] });
1028
+ let result = part;
1029
+ for (const edge of coalesceEdges(topEdges)) {
1030
+ result = fillet(result, 2, edge);
1031
+ }
1032
+ ```
1033
+
1034
+ ```ts
1035
+ selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]
1036
+ ```
1037
+
1038
+ ---
1039
+
1040
+ ## C6: Edge Feature
1041
+
1042
+ Modify edges of a solid — fillets, chamfers, draft, offset.
1043
+
1044
+ #### `chamfer()` — Apply chamfers (beveled edges) to one or more edges of a shape.
1045
+
1046
+ Produces a 45° bevel at the specified `size` (distance from edge). Works on both straight and curved edges. Supports OCCT and Manifold backends.
1047
+
1048
+ The `edges` parameter accepts the same options as `fillet()`: inline `EdgeQuery`, pre-selected `EdgeSegment`/`EdgeSegment[]`, or `undefined` (all sharp edges).
1049
+
1050
+ ```ts
1051
+ // Chamfer all edges
1052
+ chamfer(myShape, 1)
1053
+
1054
+ // Chamfer only vertical edges
1055
+ chamfer(myShape, 2, { parallel: [0, 0, 1] })
1056
+ ```
1057
+
1058
+ ```ts
1059
+ chamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape
1060
+ ```
1061
+
1062
+ #### `draft()` — Apply a draft angle (taper) to vertical faces for mold extraction.
1063
+
1064
+ 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°.
1065
+
1066
+ Requires the OCCT backend. Throws on Manifold.
1067
+
1068
+ ```ts
1069
+ // Add 3° draft to a box for injection molding
1070
+ draft(myBox, 3)
1071
+
1072
+ // Draft with custom pull direction and neutral plane
1073
+ draft(myShape, 2, [0, 0, 1], 10)
1074
+ ```
1075
+
1076
+ ```ts
1077
+ draft(shape: Shape, angleDeg: number, pullDirection?: [ number, number, number ], neutralPlaneOffset?: number): Shape
1078
+ ```
1079
+
1080
+ #### `fillet()` — Apply fillets (rounded edges) to one or more edges of a shape.
1081
+
1082
+ 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.
1083
+
1084
+ The `edges` parameter is flexible:
1085
+
1086
+ - Omit to fillet **all** sharp edges
1087
+ - Pass an `EdgeQuery` for an inline filter (most common)
1088
+ - Pass an `EdgeSegment` or `EdgeSegment[]` from `selectEdges()` for pre-selected edges
1089
+
1090
+ Throws if no edges match the selection, or if `radius` is not a positive finite number.
1091
+
1092
+ ```ts
1093
+ // Fillet all edges
1094
+ fillet(myShape, 2)
1095
+
1096
+ // Fillet only top convex edges
1097
+ fillet(myShape, 1.5, { atZ: 20, convex: true })
1098
+
1099
+ // Fillet vertical edges selected beforehand
1100
+ const edges = selectEdges(myShape, { parallel: [0, 0, 1] })
1101
+ fillet(myShape, 3, edges)
1102
+ ```
1103
+
1104
+ ```ts
1105
+ fillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape
1106
+ ```
1107
+
1108
+ #### `offsetSolid()` — Uniformly offset all surfaces of a solid inward or outward.
1109
+
1110
+ 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.
1111
+
1112
+ Requires the OCCT backend. Throws on Manifold.
1113
+
1114
+ ```ts
1115
+ // Grow a box outward by 1mm on all sides
1116
+ offsetSolid(myBox, 1)
1117
+
1118
+ // Shrink a shape inward by 0.5mm
1119
+ offsetSolid(myShape, -0.5)
1120
+ ```
1121
+
1122
+ ```ts
1123
+ offsetSolid(shape: Shape, thickness: number): Shape
1124
+ ```
1125
+
1126
+ #### `chamfer2d()` — Bevel a named vertical edge of a shape with a 45° chamfer.
1127
+
1128
+ 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.
1129
+
1130
+ ```ts
1131
+ const b = rectangle(0, 0, 50, 50).extrude(20);
1132
+ const chamfered = chamfer2d(b.toShape(), b.edge('vert-br'), 3, [-1, -1]);
1133
+ ```
1134
+
1135
+ ```ts
1136
+ chamfer2d(shape: Shape, edge: EdgeRef, size: number, quadrant?: [ number, number ]): Shape
1137
+ ```
1138
+
1139
+ **`EdgeRef`**
1140
+ - `query?: EdgeQueryRef` — Compiler-owned edge query when available.
1141
+ - Also: `name: EdgeName`
1142
+
1143
+ #### `fillet2d()` — Round a named vertical edge of a shape with a circular fillet.
1144
+
1145
+ Compiler-owned fillet for tracked vertical edges. Requires a compile-plan-covered target (shapes from `box()`, `rectangle().extrude()`, or rigid transforms of those).
1146
+
1147
+ **Supported edges:**
1148
+
1149
+ - Tracked vertical edges from `box()` or `rectangle().extrude()`
1150
+ - Rigid transforms between tracked source and target
1151
+ - Untouched sibling tracked vertical edges after earlier `fillet2d`/`chamfer2d`
1152
+
1153
+ **Not supported:** edges after shell, hole, cut, trim, difference, intersection, generic sketch extrudes, or tapered extrudes. Use `fillet()` with an `EdgeQuery` for those cases.
1154
+
1155
+ Canonical quadrants: `vert-bl → [1,-1]`, `vert-br → [-1,-1]`, `vert-tr → [-1,1]`, `vert-tl → [1,1]`
1156
+
1157
+ ```ts
1158
+ const b = rectangle(0, 0, 50, 50).extrude(20);
1159
+ const filleted = fillet2d(b.toShape(), b.edge('vert-br'), 5, [-1, -1]);
1160
+ ```
1161
+
1162
+ ```ts
1163
+ fillet2d(shape: Shape, edge: EdgeRef, radius: number, quadrant?: [ number, number ], segments?: number): Shape
1164
+ ```
1165
+
1166
+ #### `filletCorners()` — Create a polygon from points with specific corners rounded to arc fillets.
1167
+
1168
+ 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.
1169
+
1170
+ Constraints:
1171
+
1172
+ - Collinear corners cannot be filleted (throws an error)
1173
+ - Two neighboring fillets whose tangent lengths overlap the same edge will throw
1174
+ - Radius must be positive and small enough to fit within the adjacent edge lengths
1175
+
1176
+ 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.
1177
+
1178
+ ```ts
1179
+ const roof = filletCorners(roofPoints, [
1180
+ { index: 3, radius: 19 },
1181
+ { index: 4, radius: 19 },
1182
+ { index: 5, radius: 19 },
1183
+ ]);
1184
+ ```
1185
+
1186
+ ```ts
1187
+ filletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch
1188
+ ```
1189
+
1190
+ `FilletCornerSpec`: `{ index: number, radius: number, segments?: number }`
1191
+
1192
+ ---
1193
+
1194
+ ## C7: Pattern Replication
1195
+
1196
+ Duplicate geometry in regular arrangements (linear, circular, mirror).
1197
+
1198
+ #### `circularLayout()` — Compute evenly-spaced positions around a circle.
1199
+
1200
+ Eliminates the most common trig pattern in CAD scripts:
1201
+
1202
+ ```js
1203
+ // Before — manual trig
1204
+ for (let i = 0; i < 12; i++) {
1205
+ const angle = i * 30 * Math.PI / 180;
1206
+ markers.push(marker.translate(r * Math.cos(angle), r * Math.sin(angle), 0));
1207
+ }
1208
+
1209
+ // After — declarative
1210
+ for (const {x, y} of circularLayout(12, r)) {
1211
+ markers.push(marker.translate(x, y, 0));
1212
+ }
1213
+ ```
1214
+
1215
+ ```ts
1216
+ circularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[]
1217
+ ```
1218
+
1219
+ **`CircularLayoutOptions`**
1220
+ - `startDeg?: number` — Angle of the first element in degrees (default: 0 = +X axis).
1221
+ - `centerX?: number` — Center X coordinate (default: 0).
1222
+ - `centerY?: number` — Center Y coordinate (default: 0).
1223
+
1224
+ `LayoutPoint`: `{ x: number, y: number }`
1225
+
1226
+ #### `circularPattern()` — Repeat a shape in a circular pattern around an axis and union the copies.
1227
+
1228
+ 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.
1229
+
1230
+ Two calling conventions:
1231
+
1232
+ - **Simple** (Z axis): `circularPattern(shape, 6)` or `circularPattern(shape, 6, centerX, centerY)`
1233
+ - **Advanced** (arbitrary axis): `circularPattern(shape, 6, { axis, origin })`
1234
+
1235
+ ```ts
1236
+ // 8 holes evenly spaced around origin
1237
+ circularPattern(cylinder(12, 4).translate(30, 0, -1), 8)
1238
+
1239
+ // Circular pattern around X axis
1240
+ circularPattern(myFeature, 4, { axis: [1, 0, 0], origin: [0, 0, 50] })
1241
+ ```
1242
+
1243
+ ```ts
1244
+ circularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape
1245
+ ```
1246
+
1247
+ **`CircularPatternOptions`**
1248
+ - `centerX?: number` — Center X of the rotation (default: 0). Used when axis is Z (legacy mode).
1249
+ - `centerY?: number` — Center Y of the rotation (default: 0). Used when axis is Z (legacy mode).
1250
+
1251
+ #### `circularPattern2d()` — Repeat a 2D sketch in a circular pattern around a center point and union the copies.
1252
+
1253
+ ```ts
1254
+ circularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | { centerX?: number; centerY?: number; startDeg?: number; }, centerY?: number): Sketch
1255
+ ```
1256
+
1257
+ #### `linearPattern()` — Repeat a shape in a linear pattern along a direction vector and union the copies.
1258
+
1259
+ 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.
1260
+
1261
+ ```ts
1262
+ // 5 cylinders, 20mm apart along X
1263
+ linearPattern(cylinder(10, 3), 5, 20, 0)
1264
+ ```
1265
+
1266
+ ```ts
1267
+ linearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape
1268
+ ```
1269
+
1270
+ #### `linearPattern2d()` — Repeat a 2D sketch in a linear pattern and union the copies.
1271
+
1272
+ ```ts
1273
+ linearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch
1274
+ ```
1275
+
1276
+ #### `mirrorCopy()` — Mirror a shape across a plane and union the mirror with the original.
1277
+
1278
+ 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.
1279
+
1280
+ ```ts
1281
+ // Mirror across the YZ plane (X=0)
1282
+ mirrorCopy(box(50, 30, 10), [1, 0, 0])
1283
+ ```
1284
+
1285
+ ```ts
1286
+ mirrorCopy(shape: Shape, normal: [ number, number, number ]): Shape
1287
+ ```
1288
+
1289
+ ---
1290
+
1291
+ ## C8: Constraint Solving
1292
+
1293
+ Define geometry by relationships and let a solver find positions.
1294
+
1295
+ #### `Constraint.makeParallel()` — Constrain two lines to be parallel.
1296
+
1297
+ ```ts
1298
+ Constraint.makeParallel(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder
1299
+ ```
1300
+
1301
+ #### `Constraint.enforceAngle()` — Constrain the signed angle from line `a` to line `b`.
1302
+
1303
+ ```ts
1304
+ Constraint.enforceAngle(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg, angleDeg: number): ConstrainedSketchBuilder
1305
+ ```
1306
+
1307
+ #### `Constraint.horizontal()` — Constrain a line to be horizontal.
1308
+
1309
+ ```ts
1310
+ Constraint.horizontal(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder
1311
+ ```
1312
+
1313
+ #### `Constraint.vertical()` — Constrain a line to be vertical.
1314
+
1315
+ ```ts
1316
+ Constraint.vertical(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder
1317
+ ```
1318
+
1319
+ #### `Constraint.equalLength()` — Constrain two lines to have equal length.
1320
+
1321
+ ```ts
1322
+ Constraint.equalLength(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder
1323
+ ```
1324
+
1325
+ #### `Constraint.distance()` — Constrain the distance between two points.
1326
+
1327
+ ```ts
1328
+ Constraint.distance(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg, value: number): ConstrainedSketchBuilder
1329
+ ```
1330
+
1331
+ #### `Constraint.fix()` — Fix a point at a specific coordinate.
1332
+
1333
+ ```ts
1334
+ Constraint.fix(builder: ConstrainedSketchBuilder, pt: PointArg, x: number, y: number): ConstrainedSketchBuilder
1335
+ ```
1336
+
1337
+ #### `Constraint.coincident()` — Constrain two points to occupy the same location.
1338
+
1339
+ ```ts
1340
+ Constraint.coincident(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg): ConstrainedSketchBuilder
1341
+ ```
1342
+
1343
+ #### `Constraint.perpendicular()` — Constrain two lines to be perpendicular.
1344
+
1345
+ ```ts
1346
+ Constraint.perpendicular(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder
1347
+ ```
1348
+
1349
+ #### `Constraint.length()` — Constrain the length of a line.
1350
+
1351
+ ```ts
1352
+ Constraint.length(builder: ConstrainedSketchBuilder, line: LineArg, value: number): ConstrainedSketchBuilder
1353
+ ```
1354
+
1355
+ #### `addPolygon()` — Add a general polygon concept to the builder.
1356
+
1357
+ 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.
1358
+
1359
+ Use `sk.addPolygon()` as the shorthand builder method.
1360
+
1361
+ ```ts
1362
+ const sk = constrainedSketch();
1363
+ const tri = sk.addPolygon({ points: [[0,0],[100,0],[50,80]] });
1364
+ sk.fix(tri.vertex(0), 0, 0);
1365
+ sk.length(tri.side(0), 100);
1366
+ return sk.solve().extrude(5);
1367
+ ```
1368
+
1369
+ ```ts
1370
+ addPolygon(sk: ConstrainedSketchBuilder, options: PolygonOptions): ConstrainedPolygon
1371
+ ```
1372
+
1373
+ **`PolygonOptions`**
1374
+ - `addLoop?: boolean` — Whether to register a closed loop for sketch generation. Default: true.
1375
+ - `blockRotation?: boolean` — Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false.
1376
+
1377
+ **`ConstrainedPolygon`**
1378
+ - `vertices: PointId[]` — CCW-ordered PointIds.
1379
+ - `sides: LineId[]` — CCW-ordered LineIds. `sides[i]` runs from `vertices[i]` → `vertices[(i+1) % n]`.
1380
+ - `shape: ShapeId` — ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`.
1381
+
1382
+ #### `addRect()` — Add an axis-aligned rectangle concept to the builder.
1383
+
1384
+ 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).
1385
+
1386
+ Use `sk.rect()` as the shorthand builder method.
1387
+
1388
+ ```ts
1389
+ const sk = constrainedSketch();
1390
+ const r = sk.rect({ x: 0, y: 0, width: 100, height: 50 });
1391
+ sk.fix(r.bottomLeft, 0, 0);
1392
+ sk.length(r.bottom, 120); // override initial width
1393
+ return sk.solve().extrude(10);
1394
+ ```
1395
+
1396
+ ```ts
1397
+ addRect(sk: ConstrainedSketchBuilder, options?: RectOptions): ConstrainedRect
1398
+ ```
1399
+
1400
+ **`RectOptions`**
1401
+
1402
+ | Option | Type | Description |
1403
+ |--------|------|-------------|
1404
+ | `x?` | `number` | Bottom-left x coordinate. Default: 0. |
1405
+ | `y?` | `number` | Bottom-left y coordinate. Default: 0. |
1406
+ | `width?` | `number` | Width (along x). Default: 10. |
1407
+ | `height?` | `number` | Height (along y). Default: 10. |
1408
+ | `blockRotation?` | `boolean` | Prevent 180° rotation (ensures bottom edge points rightward). Default: false. |
1409
+
1410
+ **`ConstrainedRect`**
1411
+
1412
+ | Option | Type | Description |
1413
+ |--------|------|-------------|
1414
+ | `bottom` | `LineId` | bottom-left → bottom-right |
1415
+ | `right` | `LineId` | bottom-right → top-right |
1416
+ | `top` | `LineId` | top-right → top-left |
1417
+ | `left` | `LineId` | top-left → bottom-left |
1418
+ | `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)`. |
1419
+ | `shape` | `ShapeId` | ShapeId for `shapeWidth`, `shapeHeight`, `shapeArea`, `shapeCentroidX/Y`. |
1420
+ | `bottomLeft`, `bottomRight`, `topRight`, `topLeft` | | — |
1421
+
1422
+ #### `addRegularPolygon()` — Add a regular n-gon concept to the builder.
1423
+
1424
+ 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.
1425
+
1426
+ Use `sk.regularPolygon()` as the shorthand builder method.
1427
+
1428
+ ```ts
1429
+ const sk = constrainedSketch();
1430
+ const hex = sk.regularPolygon({ sides: 6, radius: 25 });
1431
+ sk.fix(hex.center, 0, 0);
1432
+ sk.length(hex.side(0), 30); // all sides change (equal constraint)
1433
+ return sk.solve().extrude(5);
1434
+ ```
1435
+
1436
+ ```ts
1437
+ addRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions): ConstrainedRegularPolygon
1438
+ ```
1439
+
1440
+ **`RegularPolygonOptions`**
1441
+
1442
+ | Option | Type | Description |
1443
+ |--------|------|-------------|
1444
+ | `sides` | `number` | Number of sides (minimum 3). |
1445
+ | `radius?` | `number` | Circumradius — distance from center to vertex. Default: 10. |
1446
+ | `cx?` | `number` | Center x coordinate. Default: 0. |
1447
+ | `cy?` | `number` | Center y coordinate. Default: 0. |
1448
+ | `startAngle?` | `number` | Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive). Default: 0 (rightmost vertex). |
1449
+ | `blockRotation?` | `boolean` | Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. |
1450
+
1451
+
1452
+ **`ConstrainedRegularPolygon`** extends ConstrainedPolygon
1453
+ - `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.
1454
+
1455
+ #### `circle()` — Create an analytic 2D circle for measurement, construction, and extrusion.
1456
+
1457
+ ```ts
1458
+ const c = circle(0, 0, 25);
1459
+ c.diameter; c.circumference; c.area;
1460
+ c.pointAtAngle(90); // Point2D at top (90° CCW from +X)
1461
+
1462
+ // Extrude to cylinder with named faces
1463
+ const cyl = c.extrude(30);
1464
+ cyl.face('top'); // FaceRef (planar)
1465
+ cyl.face('side'); // FaceRef (curved)
1466
+
1467
+ Circle2D.fromDiameter(point(0, 0), 50);
1468
+ ```
1469
+
1470
+ ```ts
1471
+ circle(cx: number, cy: number, radius: number): Circle2D
1472
+ ```
1473
+
1474
+ #### `constrainedSketch()` — Create a parametric 2D sketch driven by geometric constraints and a nonlinear solver.
1475
+
1476
+ **Workflow**
1477
+
1478
+ 1. Create a builder with `constrainedSketch()`.
1479
+ 2. Add geometry — points, lines, circles, arcs — using the builder methods.
1480
+ 3. Add constraints (`horizontal`, `length`, `fix`, etc.) to drive the geometry.
1481
+ 4. Call `.solve()` to run the solver and get a `ConstraintSketch` (which extends `Sketch`).
1482
+
1483
+ ```ts
1484
+ const sk = constrainedSketch();
1485
+ const p1 = sk.point(0, 0);
1486
+ const p2 = sk.point(50, 0);
1487
+ const l1 = sk.line(p1, p2);
1488
+ sk.fix(p1, 0, 0);
1489
+ sk.horizontal(l1);
1490
+ sk.length(l1, 50);
1491
+ return sk.solve().extrude(10);
1492
+ ```
1493
+
1494
+ **Solver status**
1495
+
1496
+ ```ts
1497
+ const result = sk.solve();
1498
+ result.constraintMeta.status; // 'fully' | 'under' | 'over' | 'over-redundant'
1499
+ result.constraintMeta.dof; // 0 = fully constrained
1500
+ result.constraintMeta.maxError; // residual — should be < 1e-6
1501
+ result.inspect(); // human-readable summary
1502
+ result.withUpdatedConstraint('cst-5', 120); // update a dimension without rebuilding
1503
+ ```
1504
+
1505
+ ```ts
1506
+ constrainedSketch(options?: ConstrainedSketchOptions): ConstrainedSketchBuilder
1507
+ ```
1508
+
1509
+ **`ConstrainedSketchOptions`**
1510
+ - `strict?: boolean` — When true, adding a constraint that cannot be satisfied throws instead of silently discarding it.
1511
+
1512
+ #### `line()` — Create an analytic 2D line segment between two points.
1513
+
1514
+ ```ts
1515
+ const l = line(0, 0, 50, 0);
1516
+ l.length; l.midpoint; l.angle; l.direction;
1517
+ l.parallel(10); // parallel line offset 10 (positive = left)
1518
+ l.intersect(l2); // Point2D — treats lines as infinite
1519
+ l.intersectSegment(l2); // Point2D or null — segments only
1520
+
1521
+ Line2D.fromPointAndAngle(point(0, 0), 45, 100);
1522
+ Line2D.fromPointAndDirection(point(0, 0), [1, 1], 50);
1523
+ ```
1524
+
1525
+ ```ts
1526
+ line(x1: number, y1: number, x2: number, y2: number): Line2D
1527
+ ```
1528
+
1529
+ #### `point()` — Create an analytic 2D point for measurement and construction geometry.
1530
+
1531
+ ```ts
1532
+ const p = point(10, 20);
1533
+ p.distanceTo(point(30, 40)); // Euclidean distance
1534
+ p.midpointTo(point(30, 40)); // midpoint
1535
+ p.translate(5, 5); // new shifted point
1536
+ p.toTuple(); // [10, 20]
1537
+ ```
1538
+
1539
+ ```ts
1540
+ point(x: number, y: number): Point2D
1541
+ ```
1542
+
1543
+ ---
1544
+
1545
+ ## C9: Spatial Placement
1546
+
1547
+ Position geometry relative to other geometry using semantic anchors.
1548
+
1549
+
1550
+ #### `Points.distance()` — Euclidean distance between two 3D points.
1551
+
1552
+ ```ts
1553
+ Points.readonly distance: typeof distance
1554
+ ```
1555
+
1556
+ #### `Points.midpoint()` — Center point between two 3D points.
1557
+
1558
+ ```ts
1559
+ Points.readonly midpoint: typeof midpoint
1560
+ ```
1561
+
1562
+ #### `Points.lerp()` — Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b.
1563
+
1564
+ ```ts
1565
+ Points.readonly lerp: typeof lerp
1566
+ ```
1567
+
1568
+ #### `Points.direction()` — Unit direction vector from a to b. Throws if a and b are the same point.
1569
+
1570
+ ```ts
1571
+ Points.readonly direction: typeof direction
1572
+ ```
1573
+
1574
+ #### `Points.offset()` — Move a point along a direction vector by a given amount.
1575
+
1576
+ ```ts
1577
+ Points.readonly offset: typeof offset
1578
+ ```
1579
+
1580
+ #### `Points.polar()` — Compute a 2D point at distance and angle (degrees) from an optional origin.
1581
+
1582
+ ```ts
1583
+ Points.readonly polar: typeof polar
1584
+ ```
1585
+
1586
+ ---
1587
+
1588
+ ## C10: Assembly & Kinematics
1589
+
1590
+ Compose parts with joints for kinematic simulation.
1591
+
1592
+ #### `assembly()` — Create an assembly container with named parts and joints for kinematic mechanisms.
1593
+
1594
+ **Use this from iteration 1 for any model with moving parts.** Hinges, sliders, gears, articulated fingers, doors — all start with `assembly()`, not with manual rotation math. Don't build a static "extended pose" first and refactor to an assembly later: joint sliders, animations, sweeps, collision detection, and robot export all flow from the kinematic graph.
1595
+
1596
+ 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.
1597
+
1598
+ 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.
1599
+
1600
+ 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.
1601
+
1602
+ 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.
1603
+
1604
+ ```ts
1605
+ const mech = assembly("Arm")
1606
+ .addPart("base", box(80, 80, 20, true), {
1607
+ metadata: { material: "PETG", process: "FDM", qty: 1 },
1608
+ })
1609
+ .addPart("link", box(140, 24, 24).translate(0, -12, -12))
1610
+ .addRevolute("shoulder", "base", "link", {
1611
+ axis: [0, 1, 0],
1612
+ min: -30, max: 120, default: 25,
1613
+ frame: Transform.identity().translate(0, 0, 20),
1614
+ });
1615
+
1616
+ return mech; // auto-solved at defaults, renders all parts
1617
+ ```
1618
+
1619
+ ```ts
1620
+ assembly(name?: string): Assembly
1621
+ ```
1622
+
1623
+ #### `bomToCsv()` — Convert an array of BOM rows into a CSV string.
1624
+
1625
+ 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.
1626
+
1627
+ ```ts
1628
+ bomToCsv(rows: BomRow[]): string
1629
+ ```
1630
+
1631
+ **`BomRow`**: `part: string`, `qty: number`, `material?: string`, `process?: string`, `tolerance?: string`, `notes?: string`, `metadata?: PartMetadata`
1632
+
1633
+ **`PartMetadata`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`
1634
+
1635
+ #### `joint()` — Create a revolute joint that auto-generates a parameter slider and rotates the shape.
1636
+
1637
+ 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.
1190
1638
 
1191
- #### `joint()`
1639
+ ```ts
1640
+ const arm = joint("Shoulder", armShape, [0, 0, 20], {
1641
+ axis: [0, 1, 0],
1642
+ min: -30, max: 120, default: 25,
1643
+ });
1644
+ return arm;
1645
+ ```
1192
1646
 
1193
1647
  ```ts
1194
1648
  joint(name: string, shape: Shape, pivot: [ number, number, number ], opts?: RevoluteJointOpts): Shape
1195
1649
  ```
1196
1650
 
1197
- Create a revolute (hinge) joint. Auto-creates a param slider and rotates the shape.
1651
+ `RevoluteJointOpts`: `{ min?: number, max?: number, default?: number, unit?: string, reverse?: boolean }`
1198
1652
 
1199
- <details><summary><code>RevoluteJointOpts</code></summary>
1653
+ #### `jointsView()` — Register viewport-only mechanism controls that animate returned objects without re-running the script.
1200
1654
 
1201
- ```ts
1202
- interface RevoluteJointOpts {
1203
- min?: number;
1204
- max?: number;
1205
- default?: number;
1206
- unit?: string;
1207
- reverse?: boolean;
1208
- }
1655
+ 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.
1656
+
1657
+ **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.
1658
+
1659
+ ```js
1660
+ // BAD — double rotation
1661
+ const solved = mech.solve({ shoulder: 45, elbow: 30 });
1662
+ jointsView({ joints: [{ name: 'shoulder', ... }] });
1663
+ return solved;
1664
+
1665
+ // GOOD — rest pose, jointsView controls all posing
1666
+ const solved = mech.solve({ shoulder: 0, elbow: 0 });
1667
+ jointsView({
1668
+ joints: [
1669
+ { name: 'shoulder', child: 'Upper Arm', default: 45, ... },
1670
+ { name: 'elbow', child: 'Forearm', parent: 'Upper Arm', default: 30, ... },
1671
+ ],
1672
+ });
1673
+ return solved;
1209
1674
  ```
1210
1675
 
1211
- </details>
1676
+ **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]`.
1212
1677
 
1213
- #### `jointsView()`
1678
+ **Fixed attachments** that must follow a parent during animation need a zero-angle revolute joint in the chain:
1214
1679
 
1215
- ```ts
1216
- jointsView(options?: JointsViewOptions): void
1680
+ ```js
1681
+ { name: 'EE_Follow', child: 'End Effector', parent: 'Last Link',
1682
+ type: 'revolute', axis: [0, 0, 1], pivot: [linkLength, 0, 0],
1683
+ min: 0, max: 0, default: 0 }
1217
1684
  ```
1218
1685
 
1219
- Configure runtime joint controls that animate object transforms in the viewport without re-running the script.
1686
+ 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`.
1220
1687
 
1221
- <details><summary><code>JointsViewOptions</code></summary>
1688
+ **Tick-based keyframes:** Omit `at` from all keyframes to auto-distribute by tick weight:
1222
1689
 
1223
- ```ts
1224
- interface JointsViewOptions {
1225
- enabled?: boolean;
1226
- joints?: JointViewInput[];
1227
- couplings?: JointViewCouplingInput[];
1228
- animations?: JointViewAnimationInput[];
1229
- defaultAnimation?: string;
1230
- }
1690
+ ```js
1691
+ keyframes: [
1692
+ { ticks: 3, values: { Shoulder: 20 } }, // slow segment (3x weight)
1693
+ { ticks: 1, values: { Shoulder: -10 } }, // fast segment (1x weight)
1694
+ { values: { Shoulder: 20 } }, // last keyframe; ticks ignored
1695
+ ]
1696
+ // positions: 0, 0.75, 1.0
1231
1697
  ```
1232
1698
 
1233
- </details>
1699
+ Mixing explicit `at` and omitted `at` in the same animation is not allowed.
1234
1700
 
1235
- <details><summary><code>JointViewInput</code></summary>
1701
+ ```js
1702
+ jointsView({
1703
+ joints: [{
1704
+ name: 'Shoulder', child: 'Upper Arm', parent: 'Base',
1705
+ type: 'revolute', axis: [0, -1, 0], pivot: [0, 0, 46],
1706
+ min: -30, max: 110, default: 15,
1707
+ }],
1708
+ animations: [{
1709
+ name: 'Walk Cycle', duration: 1.6, loop: true,
1710
+ keyframes: [
1711
+ { values: { Shoulder: 20 } },
1712
+ { values: { Shoulder: -10 } },
1713
+ { values: { Shoulder: 20 } },
1714
+ ],
1715
+ }],
1716
+ });
1717
+ ```
1236
1718
 
1237
1719
  ```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
- }
1720
+ jointsView(options?: JointsViewOptions): void
1250
1721
  ```
1251
1722
 
1252
- </details>
1723
+ **`JointsViewOptions`**: `enabled?: boolean`, `joints?: JointViewInput[]`, `couplings?: JointViewCouplingInput[]`, `animations?: JointViewAnimationInput[]`, `defaultAnimation?: string`
1724
+
1725
+ **`JointViewInput`**: `name: string`, `child: string`, `parent?: string`, `type?: JointViewType`, `axis?: JointViewAxis`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `hidden?: boolean`
1726
+
1727
+ `JointViewCouplingInput`: `{ joint: string, terms: JointViewCouplingTermInput[], offset?: number }`
1253
1728
 
1254
- <details><summary><code>JointViewCouplingInput</code></summary>
1729
+ `JointViewCouplingTermInput`: `{ joint: string, ratio?: number }`
1730
+
1731
+ `JointViewAnimationInput`: `{ name: string, duration?: number, loop?: boolean, continuous?: boolean, keyframes: JointViewAnimationKeyframeInput[] }`
1732
+
1733
+ **`JointViewAnimationKeyframeInput`**
1734
+ - `at?: number` — Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights.
1735
+ - `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.
1736
+ - Also: `values: Record<string, number>`
1737
+
1738
+ ---
1739
+
1740
+ ## C11: Parameterization & UI
1741
+
1742
+ Declare user-facing controls that drive model geometry.
1743
+
1744
+ #### `Param.number()` — Declare a numeric parameter that renders as a slider in the UI.
1745
+
1746
+ Each 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.
1747
+
1748
+ Default range rules when options are omitted:
1749
+
1750
+ - `min` defaults to `0`
1751
+ - `max` defaults to `defaultValue * 4`
1752
+ - `step` is auto-calculated: `1` for integer params, `0.1` for ranges ≤ 100, `1` for larger ranges
1753
+
1754
+ 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`).
1255
1755
 
1256
1756
  ```ts
1257
- interface JointViewCouplingInput {
1258
- joint: string;
1259
- terms: JointViewCouplingTermInput[];
1260
- offset?: number;
1261
- }
1757
+ const width = Param.number("Width", 50);
1758
+ const angle = Param.number("Angle", 45, { min: 0, max: 180, unit: "°" });
1759
+ const sides = Param.number("Sides", 6, { min: 3, max: 12, integer: true });
1262
1760
  ```
1263
1761
 
1264
- </details>
1762
+ **Parameter overrides** — key must match `name` exactly:
1763
+
1764
+ ```ts
1765
+ // Via require()
1766
+ const bracket = require("./bracket.forge.js", { Width: 80 });
1767
+
1768
+ // Via CLI
1769
+ // forgecad run model.forge.js --param "Wall Thickness=3"
1770
+ ```
1265
1771
 
1266
- <details><summary><code>JointViewCouplingTermInput</code></summary>
1772
+ Also available as the shorthand alias `param()`.
1267
1773
 
1268
1774
  ```ts
1269
- interface JointViewCouplingTermInput {
1270
- joint: string;
1271
- ratio?: number;
1272
- }
1775
+ Param.number(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number
1273
1776
  ```
1274
1777
 
1275
- </details>
1778
+ #### `Param.string()` — Declare a string parameter that renders as a text input in the UI.
1276
1779
 
1277
- <details><summary><code>JointViewAnimationInput</code></summary>
1780
+ String parameters let users type free-form text — labels, names, inscriptions, file paths, etc. The `name` string is the override key.
1278
1781
 
1279
1782
  ```ts
1280
- interface JointViewAnimationInput {
1281
- name: string;
1282
- duration?: number;
1283
- loop?: boolean;
1284
- continuous?: boolean;
1285
- keyframes: JointViewAnimationKeyframeInput[];
1286
- }
1783
+ const label = Param.string("Label", "Hello World");
1784
+ const name = Param.string("Name", "Part-001", { maxLength: 20 });
1287
1785
  ```
1288
1786
 
1289
- </details>
1787
+ Override via import:
1290
1788
 
1291
- <details><summary><code>JointViewAnimationKeyframeInput</code></summary>
1789
+ ```ts
1790
+ const tag = require("./tag.forge.js", { Label: "Custom Text" });
1791
+ ```
1792
+
1793
+ Only available as `Param.string()` — no standalone alias.
1292
1794
 
1293
1795
  ```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
- }
1796
+ Param.string(name: string, defaultValue: string, opts?: { maxLength?: number; }): string
1301
1797
  ```
1302
1798
 
1303
- </details>
1799
+ #### `Param.bool()` — Declare a boolean parameter that renders as a checkbox in the UI.
1304
1800
 
1305
- ---
1801
+ 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.
1306
1802
 
1307
- ## C11: Parameterization & UI
1803
+ ```ts
1804
+ const showHoles = Param.bool("Show Holes", true);
1805
+ if (showHoles) return difference(plate, cylinder(10, 5).translate(50, 30, 0));
1806
+ return plate;
1807
+ ```
1308
1808
 
1309
- Declare user-facing controls that drive model geometry.
1809
+ Override via import:
1810
+
1811
+ ```ts
1812
+ const pan = require("./pan.forge.js", { "Show Lid": 0 });
1813
+ ```
1310
1814
 
1311
- #### `boolParam()`
1815
+ Also available as the shorthand alias `boolParam()`.
1312
1816
 
1313
1817
  ```ts
1314
- boolParam(name: string, defaultValue: boolean): boolean
1818
+ Param.bool(name: string, defaultValue: boolean): boolean
1315
1819
  ```
1316
1820
 
1317
- Declare a boolean parameter. Returns the current boolean value. Renders as a checkbox in the UI.
1821
+ #### `Param.choice()` — Declare a choice parameter that renders as a dropdown in the UI.
1318
1822
 
1319
- #### `choiceParam()`
1823
+ `defaultValue` must exactly match one entry in `choices`. Returns the selected string label. Prefer `Param.choice` over `Param.number` when a slider would hide intent — named choices like `"wok"` are self-describing.
1824
+
1825
+ Overrides may be passed as the choice label string (preferred) or as a numeric index. The `name` string is the override key.
1320
1826
 
1321
1827
  ```ts
1322
- choiceParam(name: string, defaultValue: string, choices: string[]): string
1828
+ const panStyle = Param.choice("Pan Style", "frying-pan", ["frying-pan", "saute-pan", "wok"]);
1829
+ if (panStyle === "wok") return buildWok();
1323
1830
  ```
1324
1831
 
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()`
1832
+ Override via import:
1328
1833
 
1329
1834
  ```ts
1330
- listParam<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: { ... }): T[]
1835
+ const pan = require("./pan.forge.js", { "Pan Style": "wok" });
1331
1836
  ```
1332
1837
 
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.
1838
+ Override via CLI:
1839
+
1840
+ ```bash
1841
+ forgecad run model.forge.js --param "Pan Style=wok"
1842
+ ```
1334
1843
 
1335
- <details><summary><code>ListParamFieldDef</code></summary>
1844
+ Also available as the shorthand alias `choiceParam()`.
1336
1845
 
1337
1846
  ```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
- }
1847
+ Param.choice(name: string, defaultValue: string, choices: string[]): string
1347
1848
  ```
1348
1849
 
1349
- </details>
1850
+ #### `Param.list()` — Declare a list parameter — an array of struct items with per-field UI controls.
1851
+
1852
+ 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.
1350
1853
 
1351
- #### `param()`
1854
+ Field types:
1855
+
1856
+ - Boolean fields (`boolean: true` in field defs) return as `boolean`
1857
+ - Choice fields (`choices: [...]` in field defs) return as `string`
1858
+ - All other fields return as `number`
1352
1859
 
1353
1860
  ```ts
1354
- param(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number
1861
+ Param.list<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: { ... }): T[]
1355
1862
  ```
1356
1863
 
1357
- Declare a parameter. Returns the current value (default or overridden). Each call registers the param for UI generation.
1864
+ `ListParamFieldDef`: `{ min?: number, max?: number, step?: number, unit?: string, integer?: boolean, boolean?: boolean, choices?: string[] }`
1865
+
1866
+ #### `dim()` — Add a dimension annotation between two points.
1358
1867
 
1359
- #### `dim()`
1868
+ Dimension annotations are purely visual callouts rendered in the viewport and report export. They do not affect geometry or constrain the model.
1869
+
1870
+ Point arguments accept 2D tuples `[x, y]`, 3D tuples `[x, y, z]`, or `Point2D` objects (Z is treated as 0 for 2D inputs).
1871
+
1872
+ **Ownership Rules (Report Pages)**
1873
+
1874
+ - `currentComponent: true` — deterministic ownership by the calling import instance. Use when authoring reusable imported parts.
1875
+ - `component: "Part Name"` — route dimension to another named returned object.
1876
+ - Multiple owners: dimension is shared and appears on the assembly overview page.
1877
+ - No ownership set: report export infers ownership via endpoint-in-bbox.
1360
1878
 
1361
1879
  ```ts
1362
- dim(from: PointArg$1, to: PointArg$1, opts?: DimOpts): void
1880
+ dim([-w / 2, 0, 0], [w / 2, 0, 0], { label: "Width" });
1881
+ dim([0, 0, -h / 2], [0, 0, h / 2], { label: "Height", offset: 14 });
1882
+ dim([0, 0, 0], [100, 0, 0], { component: "Base", color: "#00AAFF" });
1363
1883
  ```
1364
1884
 
1365
- Add a dimension annotation between two points.
1366
-
1367
- <details><summary><code>DimOpts</code></summary>
1885
+ `component` (string or string[] report ownership), `currentComponent` (boolean)
1368
1886
 
1369
1887
  ```ts
1370
- interface DimOpts {
1371
- offset?: number;
1372
- label?: string;
1373
- color?: string;
1374
- component?: string | string[];
1375
- currentComponent?: boolean;
1376
- }
1888
+ dim(from: PointArg, to: PointArg, opts?: DimOpts): void
1377
1889
  ```
1378
1890
 
1379
- </details>
1891
+ `DimOpts`: `{ offset?: number, label?: string, color?: string, component?: string | string[], currentComponent?: boolean }`
1892
+
1893
+ #### `dimLine()` — Add a dimension annotation along a `Line2D`.
1380
1894
 
1381
- #### `dimLine()`
1895
+ Convenience wrapper around { points from a constrained-sketch `Line2D` entity. All `opts` are forwarded unchanged.
1382
1896
 
1383
1897
  ```ts
1384
- dimLine(l: Line2D, opts?: DimOpts): void
1898
+ const a = point(0, 0);
1899
+ const b = point(100, 0);
1900
+ dimLine(line(a, b), { label: "Span", offset: -8 });
1385
1901
  ```
1386
1902
 
1387
- Add a dimension annotation along a Line2D.
1903
+ ```ts
1904
+ dimLine(l: Line2D, opts?: DimOpts): void
1905
+ ```
1388
1906
 
1389
1907
  ---
1390
1908
 
@@ -1392,700 +1910,618 @@ Add a dimension annotation along a Line2D.
1392
1910
 
1393
1911
  Extract 2D geometry from a 3D solid (section, projection).
1394
1912
 
1395
- #### `faceProfile()`
1913
+ #### `faceProfile()` — Extract the boundary profile of a named face as a 2D sketch.
1914
+
1915
+ 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.
1396
1916
 
1397
1917
  ```ts
1398
1918
  faceProfile(shape: Shape, face: FaceSelector): Sketch
1399
1919
  ```
1400
1920
 
1401
- #### `intersectWithPlane()`
1921
+ #### `intersectWithPlane()` — Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch.
1402
1922
 
1403
1923
  ```ts
1404
1924
  intersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch
1405
1925
  ```
1406
1926
 
1407
- Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch.
1408
-
1409
- #### `projectToPlane()`
1927
+ #### `projectToPlane()` — Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch.
1410
1928
 
1411
1929
  ```ts
1412
1930
  projectToPlane(shape: Shape, plane: PlaneSpec): Sketch
1413
1931
  ```
1414
1932
 
1415
- Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch.
1416
-
1417
1933
  ---
1418
1934
 
1419
1935
  ## C13: Export & Output
1420
1936
 
1421
1937
  Convert geometry to external formats (STL, 3MF, SVG, DXF, G-code, PDF).
1422
1938
 
1423
- #### `bom()`
1939
+ #### `bom()` — Register a Bill of Materials entry for report export.
1940
+
1941
+ 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.
1942
+
1943
+ - `quantity` must be a finite number `>= 0`. A quantity of `0` is silently ignored (useful for conditional scripting with `param()`-driven counts).
1944
+ - `unit` defaults to `"pieces"` when omitted or empty.
1945
+ - The assembly `solved.bom()` / `solved.bomCsv()` API is separate and covers per-part assembly metadata; this function is for free-form purchased-item annotation.
1946
+
1947
+ ```ts
1948
+ const tubeLen = param("Tube Length", 1200, { min: 300, max: 4000, unit: "mm" });
1949
+ const boltCount = param("Bolt Count", 16, { min: 0, max: 200, integer: true });
1950
+
1951
+ bom(tubeLen, "iron tube 30 x 20", { unit: "mm" });
1952
+ bom(boltCount, "M4 bolt, 16 mm length");
1953
+ bom(4, "rubber foot", { key: "foot-rubber" }); // explicit aggregation key
1954
+
1955
+ // Structured metadata for richer reports:
1956
+ bom(tubeLen, "rectangular steel tube", {
1957
+ unit: "mm",
1958
+ material: "steel",
1959
+ section: [30, 20],
1960
+ wall: 3,
1961
+ });
1962
+ ```
1424
1963
 
1425
1964
  ```ts
1426
1965
  bom(quantity: number, description: string, opts?: BomOpts): void
1427
1966
  ```
1428
1967
 
1429
- Add a bill-of-materials entry.
1968
+ **`BomOpts`**
1969
+
1970
+ | Option | Type | Description |
1971
+ |--------|------|-------------|
1972
+ | `unit?` | `string` | Quantity unit label, e.g. "mm", "pieces", "kg". Default: "pieces" |
1973
+ | `key?` | `string` | Optional explicit grouping key used during report aggregation. |
1974
+ | `material?` | `string` | Material name, e.g. "steel", "birch plywood", "nylon" |
1975
+ | `dimensions?` | `number[]` | Overall dimensions `[width, height]` or `[width, height, thickness]` in the entry's unit |
1976
+ | `section?` | `number[]` | Cross-section dimensions `[w, h]` for tubes and profiles |
1977
+ | `wall?` | `number` | Wall thickness for hollow sections (mm) |
1978
+ | `diameter?` | `number` | Diameter for round stock, bolts, dowels (mm) |
1979
+ | `length?` | `number` | Length for fasteners (mm) |
1980
+ | `process?` | `string` | Manufacturing process, e.g. "laser cut", "CNC", "welded" |
1981
+ | `notes?` | `string` | Free-form notes |
1982
+ | `grain?` | `string` | Wood grain direction, e.g. "long", "cross" |
1983
+
1984
+ #### `robotExport()` — Declare that this script should export the assembly as a SDF/URDF robot package.
1985
+
1986
+ 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:
1987
+
1988
+ - Mesh-based inertia tensors (full 6-component, not bounding-box approximations)
1989
+ - Separate collision meshes (convex hull by default — ~50–80% smaller)
1990
+ - Joint mimic elements derived from `addJointCoupling` / `addGearCoupling`
1430
1991
 
1431
- <details><summary><code>BomOpts</code></summary>
1992
+ **Collision mesh modes** (set per-link via `links["PartName"].collision`):
1993
+
1994
+ | 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 | |
1995
+
1996
+ **Unit conventions:**
1997
+
1998
+ - Revolute `velocity` is in degrees/second in Forge; exporters convert to rad/s.
1999
+ - Prismatic distances are in mm in Forge; exported in meters.
2000
+ - `massKg` is preferred; `densityKgM3` is used when mass is unknown.
2001
+ - 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.
1432
2002
 
1433
2003
  ```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
- }
2004
+ const rover = assembly("Scout")
2005
+ .addPart("Chassis", box(300, 220, 50, true))
2006
+ .addPart("Left Wheel", cylinder(30, 60, undefined, 48, true))
2007
+ .addRevolute("leftWheel", "Chassis", "Left Wheel", {
2008
+ axis: [0, 1, 0],
2009
+ frame: Transform.identity().translate(90, 140, 60),
2010
+ effort: 20, velocity: 1080,
2011
+ });
2012
+
2013
+ robotExport({
2014
+ assembly: rover,
2015
+ modelName: "Scout",
2016
+ links: {
2017
+ Chassis: { massKg: 10 },
2018
+ "Left Wheel": { massKg: 0.8 },
2019
+ },
2020
+ plugins: {
2021
+ diffDrive: {
2022
+ leftJoints: ["leftWheel"], rightJoints: ["rightWheel"],
2023
+ wheelSeparationMm: 280, wheelRadiusMm: 60,
2024
+ },
2025
+ },
2026
+ world: { generateDemoWorld: true },
2027
+ });
1440
2028
  ```
1441
2029
 
1442
- </details>
2030
+ **CLI usage**
1443
2031
 
1444
- #### `robotExport()`
2032
+ ```bash
2033
+ forgecad export sdf model.forge.js # SDF package (Gazebo/Ignition)
2034
+ forgecad export urdf model.forge.js # URDF package (ROS/PyBullet/MuJoCo)
2035
+ ```
1445
2036
 
1446
2037
  ```ts
1447
2038
  robotExport(options: RobotExportOptions): CollectedRobotExport
1448
2039
  ```
1449
2040
 
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).
2041
+ **`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`
1451
2042
 
1452
- <details><summary><code>RobotExportOptions</code></summary>
2043
+ `RobotLinkExportOptions`: `{ massKg?: number, densityKgM3?: number, collision?: "visual" | "convex" | "box" | "none" }`
1453
2044
 
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
- ```
2045
+ `RobotJointExportOptions`: `{ effort?: number, velocity?: number, damping?: number, friction?: number }`
1469
2046
 
1470
- </details>
2047
+ **`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`
1471
2048
 
1472
- <details><summary><code>RobotLinkExportOptions</code></summary>
2049
+ `RobotJointStatePublisherOptions`: `{ enabled?: boolean, joints?: string[], topic?: string, updateRate?: number }`
1473
2050
 
1474
- ```ts
1475
- interface RobotLinkExportOptions {
1476
- massKg?: number;
1477
- densityKgM3?: number;
1478
- collision?: "visual" | "convex" | "box" | "none";
1479
- }
1480
- ```
2051
+ `RobotWorldOptions`: `{ name?: string, generateDemoWorld?: boolean, spawnPose?: RobotPose6, keyboardTeleop?: RobotWorldKeyboardTeleopOptions }`
1481
2052
 
1482
- </details>
2053
+ `RobotWorldKeyboardTeleopOptions`: `{ enabled?: boolean, linearStep?: number, angularStep?: number }`
1483
2054
 
1484
- <details><summary><code>RobotJointExportOptions</code></summary>
2055
+ **`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`
1485
2056
 
1486
- ```ts
1487
- interface RobotJointExportOptions {
1488
- effort?: number;
1489
- velocity?: number;
1490
- damping?: number;
1491
- friction?: number;
1492
- }
1493
- ```
2057
+ `AssemblyDefinition`: `{ name: string, parts: AssemblyPartDef[], joints: AssemblyJointDef[], jointCouplings: AssemblyJointCouplingDef[] }`
1494
2058
 
1495
- </details>
2059
+ `AssemblyPartDef`: `{ name: string, part: AssemblyPart, base: Transform, metadata?: PartMetadata }`
1496
2060
 
1497
- <details><summary><code>RobotDiffDrivePluginOptions</code></summary>
2061
+ **`PartMetadata`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`
1498
2062
 
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
- ```
2063
+ **`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`
1516
2064
 
1517
- </details>
2065
+ `AssemblyJointCouplingDef`: `{ joint: string, terms: JointCouplingTermRecord[], offset: number }`
1518
2066
 
1519
- <details><summary><code>RobotJointStatePublisherOptions</code></summary>
2067
+ `JointCouplingTermRecord`: `{ joint: string, ratio: number }`
1520
2068
 
1521
- ```ts
1522
- interface RobotJointStatePublisherOptions {
1523
- enabled?: boolean;
1524
- joints?: string[];
1525
- topic?: string;
1526
- updateRate?: number;
1527
- }
1528
- ```
2069
+ #### `sheetMetal()` — Create a parametric sheet metal part with flanges, bend allowances, and flat-pattern unfolding.
1529
2070
 
1530
- </details>
2071
+ `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.
1531
2072
 
1532
- <details><summary><code>RobotWorldOptions</code></summary>
2073
+ **Recommended authoring order:**
1533
2074
 
1534
- ```ts
1535
- interface RobotWorldOptions {
1536
- name?: string;
1537
- generateDemoWorld?: boolean;
1538
- spawnPose?: RobotPose6;
1539
- keyboardTeleop?: RobotWorldKeyboardTeleopOptions;
1540
- }
1541
- ```
2075
+ 1. Define the base panel + thickness + bend parameters.
2076
+ 2. Chain `.flange()` calls for each edge. Validate with `.folded()` and `.flatPattern()` before adding cutouts.
2077
+ 3. Add panel cutouts, then flange cutouts one region at a time.
2078
+ 4. Validate after each new cutout region.
1542
2079
 
1543
- </details>
2080
+ **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.
1544
2081
 
1545
- <details><summary><code>RobotWorldKeyboardTeleopOptions</code></summary>
2082
+ ```ts
2083
+ const cover = sheetMetal({
2084
+ panel: { width: 180, height: 110 },
2085
+ thickness: 1.5,
2086
+ bendRadius: 2,
2087
+ bendAllowance: { kFactor: 0.42 },
2088
+ cornerRelief: { size: 4 },
2089
+ })
2090
+ .flange('top', { length: 18 })
2091
+ .flange('right', { length: 18 })
2092
+ .flange('bottom', { length: 18 })
2093
+ .flange('left', { length: 18 })
2094
+ .cutout('panel', rect(72, 36, true), { selfAnchor: 'center' })
2095
+ .cutout('flange-right', roundedRect(26, 10, 5, true), { selfAnchor: 'center' });
2096
+
2097
+ const folded = cover.folded();
2098
+ const flat = cover.flatPattern();
2099
+ ```
1546
2100
 
1547
2101
  ```ts
1548
- interface RobotWorldKeyboardTeleopOptions {
1549
- enabled?: boolean;
1550
- linearStep?: number;
1551
- angularStep?: number;
1552
- }
2102
+ sheetMetal(options: SheetMetalOptions): SheetMetalPart
1553
2103
  ```
1554
2104
 
1555
- </details>
2105
+ **`SheetMetalOptions`**
1556
2106
 
1557
- <details><summary><code>CollectedRobotExport</code></summary>
2107
+ | Option | Type | Description |
2108
+ |--------|------|-------------|
2109
+ | `width` | `number` | Width of the panel along the X axis. |
2110
+ | `height` | `number` | Height of the panel along the Y axis. |
2111
+ | `thickness` | `number` | Sheet thickness in mm. Applied uniformly across the panel and all flanges. |
2112
+ | `bendRadius` | `number` | Inside bend radius in mm. Must be ≥ 0. Typically 0.5–2× the sheet thickness. |
2113
+ | `kFactor` | `number` | K-factor (neutral-axis offset, 0–1). |
2114
+ | `kind?` | `"rect"` | Relief shape — only `'rect'` is supported in v1. |
2115
+ | `size` | `number` | Side length of the square relief cut in mm. |
1558
2116
 
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
- ```
2117
+ #### `sketchToDxf()` — Export a 2D sketch as a DXF string (R12/AC1009 — maximally compatible).
1574
2118
 
1575
- </details>
2119
+ 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.
1576
2120
 
1577
- <details><summary><code>AssemblyDefinition</code></summary>
2121
+ The R12 format is chosen for maximum compatibility with CAM tools, laser-cutter software, and older CAD readers.
1578
2122
 
1579
2123
  ```ts
1580
- interface AssemblyDefinition {
1581
- name: string;
1582
- parts: AssemblyPartDef[];
1583
- joints: AssemblyJointDef[];
1584
- jointCouplings: AssemblyJointCouplingDef[];
1585
- }
2124
+ const s = rect(100, 60);
2125
+ const dxf = sketchToDxf(s, { layer: 'cut' });
1586
2126
  ```
1587
2127
 
1588
- </details>
1589
-
1590
- <details><summary><code>AssemblyPartDef</code></summary>
1591
-
1592
2128
  ```ts
1593
- interface AssemblyPartDef {
1594
- name: string;
1595
- part: AssemblyPart;
1596
- base: Transform;
1597
- metadata?: PartMetadata;
1598
- }
2129
+ sketchToDxf(sketch: Sketch, options?: SketchDxfOptions): string
1599
2130
  ```
1600
2131
 
1601
- </details>
2132
+ **`SketchDxfOptions`**
2133
+ - `layer?: string` — DXF layer name. Default: "0"
2134
+ - `colorIndex?: number` — DXF color index (1–255, AutoCAD ACI). Default: 7 (white/black)
1602
2135
 
1603
- <details><summary><code>PartMetadata</code></summary>
2136
+ #### `sketchToSvg()` — Export a 2D sketch as an SVG string.
1604
2137
 
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
- ```
2138
+ 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.
1616
2139
 
1617
- </details>
2140
+ 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).
1618
2141
 
1619
- <details><summary><code>AssemblyJointDef</code></summary>
2142
+ ```ts
2143
+ const s = rect(100, 60);
2144
+ const svg = sketchToSvg(s, { stroke: '#333', strokeWidth: 0.8 });
2145
+ ```
1620
2146
 
1621
2147
  ```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
- }
2148
+ sketchToSvg(sketch: Sketch, options?: SketchSvgOptions): string
1638
2149
  ```
1639
2150
 
1640
- </details>
2151
+ **`SketchSvgOptions`**
1641
2152
 
1642
- <details><summary><code>AssemblyJointCouplingDef</code></summary>
2153
+ | Option | Type | Description |
2154
+ |--------|------|-------------|
2155
+ | `stroke?` | `string` | Stroke color. Default: "black" |
2156
+ | `strokeWidth?` | `number` | Stroke width in sketch units. Default: 0.5 |
2157
+ | `fill?` | `string` | Fill color. Default: "none" |
2158
+ | `padding?` | `number` | Padding around the sketch bounding box in sketch units. Default: 2 |
2159
+ | `pixelsPerUnit?` | `number` | If set, scale so 1 sketch-unit = this many px. Otherwise auto-fit. |
1643
2160
 
1644
- ```ts
1645
- interface AssemblyJointCouplingDef {
1646
- joint: string;
1647
- terms: JointCouplingTermRecord[];
1648
- offset: number;
1649
- }
1650
- ```
2161
+ ---
1651
2162
 
1652
- </details>
2163
+ ## C14: Visual & Debugging
2164
+
2165
+ Control viewport appearance and debugging aids.
1653
2166
 
1654
- <details><summary><code>JointCouplingTermRecord</code></summary>
2167
+ #### `verify.that()` — Custom predicate check.
1655
2168
 
1656
2169
  ```ts
1657
- interface JointCouplingTermRecord {
1658
- joint: string;
1659
- ratio: number;
1660
- }
2170
+ verify.that(label: string, check: () => boolean, message?: string): void
1661
2171
  ```
1662
2172
 
1663
- </details>
1664
-
1665
- #### `sheetMetal()`
2173
+ #### `verify.equal()` — Check that two numbers are approximately equal (within tolerance).
1666
2174
 
1667
2175
  ```ts
1668
- sheetMetal(options: SheetMetalOptions): SheetMetalPart
2176
+ verify.equal(label: string, actual: number, expected: number, tolerance?: number, message?: string): void
1669
2177
  ```
1670
2178
 
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().
1672
-
1673
- <details><summary><code>SheetMetalOptions</code></summary>
2179
+ #### `verify.notEqual()` Check that two numbers are NOT equal (differ by more than tolerance).
1674
2180
 
1675
2181
  ```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
- }
2182
+ verify.notEqual(label: string, actual: number, unexpected: number, tolerance?: number, message?: string): void
1685
2183
  ```
1686
2184
 
1687
- </details>
1688
-
1689
- #### `sketchToDxf()`
2185
+ #### `verify.greaterThan()` — Check that actual > min.
1690
2186
 
1691
2187
  ```ts
1692
- sketchToDxf(sketch: Sketch, options?: SketchDxfOptions): string
2188
+ verify.greaterThan(label: string, actual: number, min: number, message?: string): void
1693
2189
  ```
1694
2190
 
1695
- Export a 2D sketch as a DXF string (R12/AC1009maximally 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.
1696
-
1697
- <details><summary><code>SketchDxfOptions</code></summary>
2191
+ #### `verify.lessThan()`Check that actual < max.
1698
2192
 
1699
2193
  ```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
- }
2194
+ verify.lessThan(label: string, actual: number, max: number, message?: string): void
1706
2195
  ```
1707
2196
 
1708
- </details>
1709
-
1710
- #### `sketchToSvg()`
2197
+ #### `verify.inRange()` — Check that min <= actual <= max.
1711
2198
 
1712
2199
  ```ts
1713
- sketchToSvg(sketch: Sketch, options?: SketchSvgOptions): string
2200
+ verify.inRange(label: string, actual: number, min: number, max: number, message?: string): void
1714
2201
  ```
1715
2202
 
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).
1717
-
1718
- <details><summary><code>SketchSvgOptions</code></summary>
2203
+ #### `verify.centersCoincide()` Check that the bounding-box centers of two shapes coincide within tolerance (mm).
1719
2204
 
1720
2205
  ```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
- }
2206
+ verify.centersCoincide(label: string, a: ShapeLike, b: ShapeLike, tolerance?: number): void
1733
2207
  ```
1734
2208
 
1735
- </details>
2209
+ #### `verify.notColliding()` — Check that two shapes do not collide (minGap > 0).
1736
2210
 
1737
- ---
2211
+ ```ts
2212
+ verify.notColliding(label: string, a: ShapeLike, b: ShapeLike, searchLength?: number): void
2213
+ ```
1738
2214
 
1739
- ## C14: Visual & Debugging
2215
+ #### `verify.minClearance()` Check that a minimum clearance gap exists between two shapes.
1740
2216
 
1741
- Control viewport appearance and debugging aids.
2217
+ ```ts
2218
+ verify.minClearance(label: string, a: ShapeLike, b: ShapeLike, minGap: number, searchLength?: number): void
2219
+ ```
1742
2220
 
1743
- #### `explodeView()`
2221
+ #### `verify.parallel()` — Check that two face normals are parallel (within toleranceDeg degrees).
1744
2222
 
1745
2223
  ```ts
1746
- explodeView(options?: ExplodeViewOptions): void
2224
+ verify.parallel(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void
1747
2225
  ```
1748
2226
 
1749
- Configure viewport exploded-view behavior for the current script execution. Multiple calls merge; later values override earlier ones.
1750
-
1751
- <details><summary><code>ExplodeViewOptions</code></summary>
2227
+ #### `verify.perpendicular()` Check that two face normals are perpendicular (within toleranceDeg degrees).
1752
2228
 
1753
2229
  ```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
- }
2230
+ verify.perpendicular(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void
1770
2231
  ```
1771
2232
 
1772
- </details>
1773
-
1774
- <details><summary><code>ExplodeDirective</code></summary>
2233
+ #### `verify.coplanar()` — Check that a face is coplanar with (same plane as) another face, meaning they are parallel AND their centers lie on the same plane.
1775
2234
 
1776
2235
  ```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
- }
2236
+ verify.coplanar(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number, toleranceMm?: number): void
1785
2237
  ```
1786
2238
 
1787
- </details>
1788
-
1789
- <details><summary><code>ExplodeViewDirective</code> extends ExplodeDirective</summary>
2239
+ #### `verify.faceAt()` — Check that a face center lies at a specific position (within toleranceMm).
1790
2240
 
1791
2241
  ```ts
1792
- interface ExplodeViewDirective extends ExplodeDirective {
1793
- }
2242
+ verify.faceAt(label: string, face: FaceRefLike, expectedPos: [ number
1794
2243
  ```
1795
2244
 
1796
- </details>
1797
-
1798
- #### `cutPlane()`
2245
+ #### `verify.sameDirection()` — Check that two face normals point in the same direction (not antiparallel). Stricter than parallel — both |angle| AND sign must match.
1799
2246
 
1800
2247
  ```ts
1801
- cutPlane(name: string, normal: [ number, number, number ], options?: CutPlaneOptions): void
2248
+ verify.sameDirection(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void
1802
2249
  ```
1803
2250
 
1804
- <details><summary><code>CutPlaneOptions</code></summary>
2251
+ #### `verify.isEmpty()` — Check that a shape is empty.
1805
2252
 
1806
2253
  ```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
- }
2254
+ verify.isEmpty(label: string, shape: ShapeLike, message?: string): void
1813
2255
  ```
1814
2256
 
1815
- </details>
1816
-
1817
- #### `scene()`
2257
+ #### `verify.notEmpty()` — Check that a shape is NOT empty.
1818
2258
 
1819
2259
  ```ts
1820
- scene(options: SceneOptions): void
2260
+ verify.notEmpty(label: string, shape: ShapeLike, message?: string): void
1821
2261
  ```
1822
2262
 
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 }, }, }); ```
1824
-
1825
- <details><summary><code>SceneOptions</code></summary>
2263
+ #### `verify.volumeApprox()` Check that a shape's volume is approximately equal to expected (mm³).
1826
2264
 
1827
2265
  ```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
- }
2266
+ verify.volumeApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void
1839
2267
  ```
1840
2268
 
1841
- </details>
1842
-
1843
- <details><summary><code>SceneBackgroundGradient</code></summary>
2269
+ #### `verify.areaApprox()` — Check that a shape's surface area is approximately equal to expected (mm²).
1844
2270
 
1845
2271
  ```ts
1846
- interface SceneBackgroundGradient {
1847
- top: string;
1848
- bottom: string;
1849
- }
2272
+ verify.areaApprox(label: string, shape: ShapeLike, expected: number, tolerance?: number): void
1850
2273
  ```
1851
2274
 
1852
- </details>
1853
-
1854
- <details><summary><code>SceneCameraConfig</code></summary>
2275
+ #### `verify.boundingBoxSize()` — Check that a shape's bounding box has approximately the given size.
1855
2276
 
1856
2277
  ```ts
1857
- interface SceneCameraConfig {
1858
- fov?: number;
1859
- type?: "perspective" | "orthographic";
1860
- }
2278
+ verify.boundingBoxSize(label: string, shape: ShapeLike, expectedSize: [ number
1861
2279
  ```
1862
2280
 
1863
- </details>
1864
-
1865
- <details><summary><code>SceneLightConfig</code></summary>
1866
-
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
- ```
2281
+ #### `explodeView()` — Configure how the viewport explode slider offsets returned objects.
1888
2282
 
1889
- </details>
2283
+ 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.
1890
2284
 
1891
- <details><summary><code>SceneEnvironmentConfig</code></summary>
2285
+ Multiple calls merge — later values override earlier ones on a per-key basis. `byName` and `byPath` maps are merged entry-by-entry.
2286
+
2287
+ For programmatic explode applied before returning (without the slider), use `lib.explode()` instead.
2288
+
2289
+ ```js
2290
+ explodeView({
2291
+ amountScale: 1.2,
2292
+ stages: [0.35, 0.8],
2293
+ mode: 'radial',
2294
+ byPath: { 'Drive/Shaft': { direction: [1, 0, 0], stage: 1.6 } },
2295
+ });
2296
+ ```
1892
2297
 
1893
2298
  ```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
- }
2299
+ explodeView(options?: ExplodeViewOptions): void
1902
2300
  ```
1903
2301
 
1904
- </details>
2302
+ **`ExplodeViewOptions`**
2303
+
2304
+ | Option | Type | Description |
2305
+ |--------|------|-------------|
2306
+ | `enabled?` | `boolean` | Set false to disable viewport explode offsets for this script output. |
2307
+ | `amountScale?` | `number` | Scales the UI explode amount. Default: 1 |
2308
+ | `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, ...) |
2309
+ | `mode?` | `ExplodeViewDirection` | Global direction mode fallback. Default: 'radial' |
2310
+ | `axisLock?` | `ExplodeAxis` | Global axis lock fallback. |
2311
+ | `byName?` | `Record<string, ExplodeViewDirective>` | Per-object overrides by final object name. |
2312
+ | `byPath?` | `Record<string, ExplodeViewDirective>` | Per-tree-path overrides using slash-separated object tree segments. |
1905
2313
 
1906
- <details><summary><code>SceneFogConfig</code></summary>
2314
+ **`ExplodeDirective`**
2315
+ - `stage?: number` — Multiplier applied to `amount` for this node
2316
+ - `direction?: ExplodeDirection` — Direction mode for this node
2317
+ - `axisLock?: ExplodeAxis` — Optional axis lock after direction is resolved
2318
+
2319
+ #### `cutPlane()`
1907
2320
 
1908
2321
  ```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
- }
2322
+ cutPlane(name: string, normal: [ number, number, number ], options?: CutPlaneOptions): void
1918
2323
  ```
1919
2324
 
1920
- </details>
2325
+ **`CutPlaneOptions`**
2326
+ - `offset?: number` — Optional offset along the plane normal (primarily for object-form overload).
2327
+ - `exclude?: CutPlaneExcludeInput` — Object names to keep uncut for this plane.
1921
2328
 
1922
- <details><summary><code>ScenePostProcessingConfig</code></summary>
2329
+ #### `mock()` — Register a mock (context) object for visualization and collision checking.
1923
2330
 
1924
- ```ts
1925
- interface ScenePostProcessingConfig {
1926
- bloom?: SceneBloomConfig;
1927
- vignette?: SceneVignetteConfig;
1928
- grain?: SceneGrainConfig;
1929
- toneMappingExposure?: number;
1930
- }
1931
- ```
2331
+ Mock objects appear in the viewport and spatial analysis when you run a file directly, but are excluded when the file is imported via `require()`. This lets you model the surrounding context — walls, bolts, mating parts — without polluting the module's exports.
2332
+
2333
+ The shape is returned unchanged, so you can reference it for alignment, dimensioning, and `verify` checks.
1932
2334
 
1933
- </details>
2335
+ Mock objects participate in `forgecad run` collision detection and spatial analysis. Their names appear with a `(mock)` suffix in reports.
1934
2336
 
1935
- <details><summary><code>SceneBloomConfig</code></summary>
2337
+ In the viewport, mock objects render at reduced opacity so they are visually distinct from real geometry.
1936
2338
 
1937
2339
  ```ts
1938
- interface SceneBloomConfig {
1939
- intensity?: number;
1940
- threshold?: number;
1941
- radius?: number;
1942
- }
1943
- ```
2340
+ // bracket.forge.js
2341
+ const wall = mock(box(100, 200, 10).translate(0, 0, -5), "wall");
2342
+ const bolt = mock(cylinder(3, 15).translate(10, 15, 0), "bolt");
1944
2343
 
1945
- </details>
2344
+ const bracket = box(20, 30, 5);
2345
+ verify.notColliding("bracket vs wall", bracket, wall);
1946
2346
 
1947
- <details><summary><code>SceneVignetteConfig</code></summary>
2347
+ return bracket;
2348
+ // When imported: only bracket is exported
2349
+ // When run directly: bracket + wall + bolt all visible
2350
+ ```
1948
2351
 
1949
2352
  ```ts
1950
- interface SceneVignetteConfig {
1951
- darkness?: number;
1952
- offset?: number;
1953
- }
2353
+ mock<T extends Shape>(shape: T, name?: string): T
1954
2354
  ```
1955
2355
 
1956
- </details>
2356
+ #### `scene()` — Configure the scene environment for the current script execution.
1957
2357
 
1958
- <details><summary><code>SceneGrainConfig</code></summary>
2358
+ 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.
1959
2359
 
1960
- ```ts
1961
- interface SceneGrainConfig {
1962
- intensity?: number;
1963
- }
1964
- ```
2360
+ When `lights` is specified, **all** default lights are removed. You must include your own ambient light or the scene will be fully dark.
2361
+
2362
+ Setting `camera.position` overrides auto-framing — the viewport will no longer auto-fit the geometry on script reload.
1965
2363
 
1966
- </details>
2364
+ 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.
1967
2365
 
1968
- <details><summary><code>SceneGroundConfig</code></summary>
2366
+ All numeric values accept `param()` expressions.
2367
+
2368
+ ```js
2369
+ scene({
2370
+ background: { top: '#000814', bottom: '#001d3d' },
2371
+ camera: { position: [160, -120, 100], target: [0, 0, 50], fov: 52 },
2372
+ lights: [
2373
+ { type: 'ambient', color: '#001233', intensity: 0.08 },
2374
+ { type: 'point', position: [120, -80, 130], color: '#00f5d4', intensity: 4, distance: 400, decay: 1 },
2375
+ { type: 'point', position: [-100, 60, 20], color: '#f72585', intensity: 3, distance: 350 },
2376
+ { type: 'directional', position: [50, -30, 200], color: '#ffd60a', intensity: 1.2 },
2377
+ { type: 'hemisphere', skyColor: '#003566', groundColor: '#000814', intensity: 0.2 },
2378
+ ],
2379
+ fog: { color: '#000814', near: 100, far: 450 },
2380
+ postProcessing: {
2381
+ bloom: { intensity: param('bloom', 1.5, 0, 4), threshold: 0.5, radius: 0.7 },
2382
+ vignette: { darkness: 0.8, offset: 0.25 },
2383
+ grain: { intensity: 0.08 },
2384
+ toneMappingExposure: param('exposure', 1.5, 0.5, 4),
2385
+ },
2386
+ });
2387
+ ```
1969
2388
 
1970
2389
  ```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
- }
2390
+ scene(options: SceneOptions): void
1981
2391
  ```
1982
2392
 
1983
- </details>
2393
+ **`SceneOptions`**
2394
+
2395
+ | Option | Type | Description |
2396
+ |--------|------|-------------|
2397
+ | `capture?` | `SceneCaptureConfig` | Default capture parameters for `forgecad capture` — CLI flags override these. |
2398
+ | `background?`, `camera?`, `lights?`, `environment?`, `fog?`, `postProcessing?`, `ground?` | | — |
2399
+
2400
+ `SceneBackgroundGradient`: `{ top: string, bottom: string }`
2401
+
2402
+ `SceneCameraConfig`: `{ fov?: number, type?: "perspective" | "orthographic" }`
2403
+
2404
+ **`SceneLightConfig`**
2405
+
2406
+ | Option | Type | Description |
2407
+ |--------|------|-------------|
2408
+ | `groundColor?` | `string` | Ground color for hemisphere lights |
2409
+ | `skyColor?` | `string` | Sky color alias for hemisphere lights (same as color) |
2410
+ | `angle?` | `number` | Spot light cone angle in radians |
2411
+ | `penumbra?` | `number` | Spot light penumbra (0–1) |
2412
+ | `decay?` | `number` | Point/spot light decay |
2413
+ | `distance?` | `number` | Point/spot light distance (0 = infinite) |
2414
+ | `castShadow?` | `boolean` | Whether this light casts shadows |
2415
+ | `type`, `color?`, `intensity?` | | — |
2416
+
2417
+ **`SceneEnvironmentConfig`**
2418
+ - `preset?: "studio" | "sunset" | "dawn" | "warehouse" | "forest" | "apartment" | "lobby" | "city" | "park" | "night" | "none"` — Built-in preset name or 'none' to disable
2419
+ - `intensity?: number` — Environment map intensity
2420
+ - `background?: boolean` — Use environment map as scene background
2421
+
2422
+ **`SceneFogConfig`**
2423
+ - `near?: number` — Linear fog near distance
2424
+ - `far?: number` — Linear fog far distance
2425
+ - `density?: number` — Exponential fog density (if set, uses FogExp2 instead of linear Fog)
2426
+ - Also: `color?: string`
2427
+
2428
+ `ScenePostProcessingConfig`: `{ bloom?: SceneBloomConfig, vignette?: SceneVignetteConfig, grain?: SceneGrainConfig, toneMappingExposure?: number }`
2429
+
2430
+ `SceneBloomConfig`: `{ intensity?: number, threshold?: number, radius?: number }`
2431
+
2432
+ `SceneVignetteConfig`: `{ darkness?: number, offset?: number }`
1984
2433
 
1985
- <details><summary><code>SceneCaptureConfig</code></summary>
2434
+ `SceneGrainConfig`: `{ intensity?: number }`
2435
+
2436
+ **`SceneGroundConfig`**
2437
+
2438
+ | Option | Type | Description |
2439
+ |--------|------|-------------|
2440
+ | `visible?` | `boolean` | Show a ground plane |
2441
+ | `color?` | `string` | Ground color |
2442
+ | `offset?` | `number` | Offset below the model's bounding box minimum Z. Default 0 (flush with model bottom). |
2443
+ | `receiveShadow?` | `boolean` | Receive shadows on the ground |
2444
+
2445
+ **`SceneCaptureConfig`**
2446
+
2447
+ | Option | Type | Description |
2448
+ |--------|------|-------------|
2449
+ | `framesPerTurn?` | `number` | Frames for one full orbit rotation (default: 72) |
2450
+ | `holdFrames?` | `number` | Frozen frames before motion starts (default: 6) |
2451
+ | `pitchDeg?` | `number` | Orbit pitch angle in degrees (default: auto from camera) |
2452
+ | `fps?` | `number` | Output frame rate (default: 24) |
2453
+ | `size?` | `number` | Output frame size in pixels (default: 960) |
2454
+ | `background?` | `string` | Canvas background color for capture (default: '#252526') |
2455
+
2456
+ #### `showLabels()` — Highlight all user-labeled faces on a shape for visual debugging.
2457
+
2458
+ Shows each user-authored label name in the viewport for visual debugging. Returns the shape unchanged for chaining: `return showLabels(myShape)`.
1986
2459
 
1987
2460
  ```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
- }
2461
+ showLabels(shape: Shape): Shape
2002
2462
  ```
2003
2463
 
2004
- </details>
2464
+ #### `viewConfig()` — Configure viewport helper visuals for the current script execution.
2465
+
2466
+ 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.
2005
2467
 
2006
- #### `viewConfig()`
2468
+ This does **not** trigger a geometry recompute; it only affects the visual helpers drawn on top of the 3D scene.
2469
+
2470
+ ```js
2471
+ viewConfig({
2472
+ jointOverlay: {
2473
+ axisColor: '#13dfff',
2474
+ arcColor: '#ff7a1a',
2475
+ axisLineRadiusScale: 0.03,
2476
+ arcLineRadiusScale: 0.022,
2477
+ },
2478
+ });
2479
+ ```
2007
2480
 
2008
2481
  ```ts
2009
2482
  viewConfig(options?: ViewConfigOptions): void
2010
2483
  ```
2011
2484
 
2012
- Configure runtime viewport visuals for the current script execution. Multiple calls merge; later values override earlier ones.
2485
+ `ViewConfigOptions`: `{ jointOverlay?: JointOverlayViewConfigOptions }`
2013
2486
 
2014
- <details><summary><code>ViewConfigOptions</code></summary>
2487
+ **`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`
2015
2488
 
2016
- ```ts
2017
- interface ViewConfigOptions {
2018
- jointOverlay?: JointOverlayViewConfigOptions;
2019
- }
2020
- ```
2489
+ #### `spec()` — Create a named, reusable bundle of verification checks.
2021
2490
 
2022
- </details>
2023
-
2024
- <details><summary><code>JointOverlayViewConfigOptions</code></summary>
2025
-
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
- ```
2491
+ 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.
2068
2492
 
2069
- </details>
2493
+ Specs can be defined in separate `.forge.js` files and imported via `require()` to share them across models.
2070
2494
 
2071
- #### `spec()`
2495
+ `spec.check()` returns a `SpecResult` — you can inspect it programmatically or ignore the return value and let the Checks panel show results.
2072
2496
 
2073
2497
  ```ts
2074
- spec(name: string, checkFn: (...args: any[]) => void): Spec
2075
- ```
2498
+ const printable = spec("Fits printer bed", (shape) => {
2499
+ verify.notEmpty("Has geometry", shape);
2500
+ const bb = shape.boundingBox();
2501
+ verify.lessThan("Width < 220mm", bb.max[0] - bb.min[0], 220);
2502
+ verify.lessThan("Depth < 220mm", bb.max[1] - bb.min[1], 220);
2503
+ verify.lessThan("Height < 250mm", bb.max[2] - bb.min[2], 250);
2504
+ });
2505
+
2506
+ // Reuse on multiple shapes
2507
+ printable.check(bracket);
2508
+ printable.check(standoff);
2076
2509
 
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.
2510
+ // Check relationships between parts
2511
+ const fitSpec = spec("Assembly fit", (partA, partB) => {
2512
+ verify.notColliding("No interference", partA, partB, 10);
2513
+ });
2514
+ fitSpec.check(bracket, standoff);
2515
+ ```
2078
2516
 
2079
- <details><summary><code>Spec</code></summary>
2517
+ **Spec-first workflow:** Write specs before building geometry. Checks go from red to green as you build — effectively TDD for CAD.
2080
2518
 
2081
2519
  ```ts
2082
- interface Spec {
2083
- /** The display name of this spec */
2084
- name: string;
2085
- }
2520
+ spec(name: string, checkFn: (...args: any[]) => void): Spec
2086
2521
  ```
2087
2522
 
2088
- </details>
2523
+ **`Spec`**
2524
+ - `name: string` — The display name of this spec
2089
2525
 
2090
2526
  ---
2091
2527
 
@@ -2093,20 +2529,72 @@ interface Spec {
2093
2529
 
2094
2530
  Bring external geometry or other ForgeCAD modules into the current script.
2095
2531
 
2096
- #### `group()`
2532
+ #### `group()` — Group multiple shapes/sketches for joint transforms without merging into a single mesh.
2533
+
2534
+ 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.).
2535
+
2536
+ **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.
2537
+
2538
+ ```js
2539
+ // BAD — every sub-part repeats the parent's global offset
2540
+ const unitX = 0, unitY = -18, unitZ = 70;
2541
+ const body = roundedBox(100, 20, 32, 4).translate(unitX, unitY, unitZ);
2542
+ const panel = box(98, 2, 18).translate(unitX, unitY - 12, unitZ + 4);
2543
+ const louver = box(88, 2, 6).translate(unitX, unitY - 14, unitZ - 11);
2544
+ ```
2545
+
2546
+ // 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);
2097
2547
 
2098
2548
  ```ts
2099
2549
  group(...items: GroupInput[]): ShapeGroup
2100
2550
  ```
2101
2551
 
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.).
2103
-
2104
2552
  ---
2105
2553
 
2106
2554
  ## C16: Part Library
2107
2555
 
2108
2556
  Pre-built parametric parts accessible via `lib.*`.
2109
2557
 
2110
- *No free functions see class methods (Shape, Sketch, ConstrainedSketchBuilder).*
2558
+ #### `Wood.board()`Create a wood board with metadata for manufacturing.
2559
+
2560
+ The board is a box(width, height, thickness) centered on XY, base at Z=0. Width along X, height along Y, thickness along Z (0 to thickness).
2561
+
2562
+ ```ts
2563
+ Wood.readonly board: (width: number, height: number, thickness: number, opts?: WoodBoardOptions) => WoodBoard
2564
+ ```
2565
+
2566
+ **`WoodBoardOptions`**
2567
+
2568
+ | Option | Type | Description |
2569
+ |--------|------|-------------|
2570
+ | `species?` | `string` | Wood species, e.g. "birch", "oak", "plywood". Default: "wood" |
2571
+ | `material?` | `string` | Material description for BOM, e.g. "birch plywood". Default: species value |
2572
+ | `grain?` | `string` | Grain direction: "long" (along width) or "cross" (along height). Default: "long" |
2573
+ | `color?` | `string` | Color hex string for visualization. Default: "#d2b48c" (tan/wood) |
2574
+ | `autoBom?` | `boolean` | If false, skip automatic BOM registration. Default: true |
2575
+
2576
+ #### `Wood.dado()` — Cut a dado (channel) across the face of a host board for a guest board to sit in.
2577
+
2578
+ Mutates `host.shape` by subtracting a rectangular channel.
2579
+
2580
+ ```ts
2581
+ Wood.readonly dado: typeof dado
2582
+ ```
2583
+
2584
+ #### `Wood.rabbet()` — Cut a rabbet (L-shaped step) along an edge of a board.
2585
+
2586
+ Mutates `board.shape` by subtracting a step from the specified edge.
2587
+
2588
+ ```ts
2589
+ Wood.readonly rabbet: typeof rabbet
2590
+ ```
2591
+
2592
+ #### `Wood.mortiseAndTenon()` — Cut a mortise in one board and shape a tenon on another.
2593
+
2594
+ Mutates both boards — subtracts the mortise pocket and removes shoulder material to form the tenon.
2595
+
2596
+ ```ts
2597
+ Wood.readonly mortiseAndTenon: typeof mortiseAndTenon
2598
+ ```
2111
2599
 
2112
2600
  ---