forgecad 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +2 -11
  2. package/dist/assets/{AdminPage-CeqCUUgu.js → AdminPage-DAu1C1ST.js} +250 -151
  3. package/dist/assets/{BlogPage-P_AJP0v9.js → BlogPage-CJEXL_zJ.js} +94 -70
  4. package/dist/assets/{DocsPage-CKRV2iq2.js → DocsPage-Gc_BCdqC.js} +269 -143
  5. package/dist/assets/EditorApp-D9bJvtf7.js +11338 -0
  6. package/dist/assets/{EditorApp-CnC2k4cW.css → EditorApp-DG1-oUSV.css} +459 -87
  7. package/dist/assets/{EmbedViewer-DBlzmQ5i.js → EmbedViewer-CEO8XbV8.js} +2 -4
  8. package/dist/assets/LandingPage-CdCuEOdC.js +451 -0
  9. package/dist/assets/PricingPage-BSrxu6d7.js +232 -0
  10. package/dist/assets/{SettingsPage-BqCh9JcC.js → SettingsPage-FUCSIRq6.js} +129 -5
  11. package/dist/assets/{evalWorker-Ql-aKwLA.js → evalWorker-KoR0SNKq.js} +6770 -2914
  12. package/dist/assets/{index-2hfs_ub0.css → index-CyVd1D4D.css} +227 -53
  13. package/dist/assets/{Viewport-CoB46f5R.js → index-wTEK39at.js} +31385 -6439
  14. package/dist/assets/{javascript-DCxGoE5Y.js → javascript-DAl8Gmyo.js} +1 -1
  15. package/dist/assets/{manifold-CqNMHHKO.js → manifold-B1sGWdYk.js} +4 -3
  16. package/dist/assets/{manifold-Cce9wRFz.js → manifold-D7o0N50J.js} +1 -1
  17. package/dist/assets/{manifold-D6BeHIOo.js → manifold-G5sBaXzi.js} +1 -1
  18. package/dist/assets/{reportWorker-sFEFonXf.js → reportWorker-DYcRHhv9.js} +6798 -3341
  19. package/dist/assets/{vendor-react-Dt7-aaJH.js → vendor-react-CG3i_wp0.js} +65 -8
  20. package/dist/docs-raw/generated/assembly.md +691 -112
  21. package/dist/docs-raw/generated/concepts.md +1225 -1400
  22. package/dist/docs-raw/generated/core.md +464 -1412
  23. package/dist/docs-raw/generated/curves.md +593 -117
  24. package/dist/docs-raw/generated/lib.md +38 -748
  25. package/dist/docs-raw/generated/output.md +139 -245
  26. package/dist/docs-raw/generated/sheet-metal.md +473 -21
  27. package/dist/docs-raw/generated/sketch.md +553 -349
  28. package/dist/docs-raw/generated/viewport.md +345 -303
  29. package/dist/docs-raw/generated/wood.md +104 -0
  30. package/dist/index.html +2 -2
  31. package/dist/sitemap.xml +6 -6
  32. package/dist-cli/chunk-PZ5AY32C.js +10 -0
  33. package/dist-cli/chunk-PZ5AY32C.js.map +1 -0
  34. package/dist-cli/forgecad.js +9435 -5407
  35. package/dist-cli/forgecad.js.map +1 -0
  36. package/dist-cli/solver-FV7TJZGI.js +365 -0
  37. package/dist-cli/solver-FV7TJZGI.js.map +1 -0
  38. package/dist-skill/CONTEXT.md +3186 -7145
  39. package/dist-skill/SKILL-dev.md +21 -63
  40. package/dist-skill/SKILL.md +12 -56
  41. package/dist-skill/docs/API/core/concepts.md +16 -98
  42. package/dist-skill/docs/CLI/export.md +91 -0
  43. package/dist-skill/docs/CLI/projects.md +107 -0
  44. package/dist-skill/docs/CLI/studio_publishing.md +52 -0
  45. package/dist-skill/docs/CLI/validation.md +66 -0
  46. package/dist-skill/docs/generated/assembly.md +691 -112
  47. package/dist-skill/docs/generated/core.md +464 -1412
  48. package/dist-skill/docs/generated/curves.md +593 -117
  49. package/dist-skill/docs/generated/lib.md +38 -748
  50. package/dist-skill/docs/generated/output.md +139 -245
  51. package/dist-skill/docs/generated/sheet-metal.md +473 -21
  52. package/dist-skill/docs/generated/sketch.md +553 -349
  53. package/dist-skill/docs/generated/viewport.md +345 -303
  54. package/dist-skill/docs/generated/wood.md +104 -0
  55. package/dist-skill/docs/guides/coordinate-system.md +11 -17
  56. package/dist-skill/docs/guides/geometry-conventions.md +13 -70
  57. package/dist-skill/docs/guides/modeling-recipes.md +22 -195
  58. package/dist-skill/docs/guides/positioning.md +88 -147
  59. package/dist-skill/docs-dev/API/core/concepts.md +51 -0
  60. package/dist-skill/docs-dev/API/core/sdf-advanced.md +92 -0
  61. package/dist-skill/docs-dev/API/core/sdf-primitives.md +58 -0
  62. package/dist-skill/docs-dev/API/core/sdf-workflow.md +42 -0
  63. package/dist-skill/docs-dev/CLI/export.md +91 -0
  64. package/dist-skill/docs-dev/CLI/projects.md +107 -0
  65. package/dist-skill/docs-dev/CLI/studio_publishing.md +52 -0
  66. package/dist-skill/docs-dev/CLI/validation.md +66 -0
  67. package/dist-skill/{docs → docs-dev}/blueprint-first.md +5 -0
  68. package/dist-skill/{docs → docs-dev}/coding-best-practices.md +6 -8
  69. package/dist-skill/{docs → docs-dev}/coding.md +1 -3
  70. package/dist-skill/docs-dev/generated/assembly.md +771 -0
  71. package/dist-skill/docs-dev/generated/core.md +775 -0
  72. package/dist-skill/docs-dev/generated/curves.md +688 -0
  73. package/dist-skill/docs-dev/generated/lib.md +50 -0
  74. package/dist-skill/docs-dev/generated/output.md +234 -0
  75. package/dist-skill/docs-dev/generated/sheet-metal.md +506 -0
  76. package/dist-skill/docs-dev/generated/sketch.md +801 -0
  77. package/dist-skill/docs-dev/generated/viewport.md +486 -0
  78. package/dist-skill/docs-dev/generated/wood.md +104 -0
  79. package/dist-skill/docs-dev/guides/coordinate-system.md +46 -0
  80. package/dist-skill/docs-dev/guides/geometry-conventions.md +52 -0
  81. package/dist-skill/docs-dev/guides/modeling-recipes.md +77 -0
  82. package/dist-skill/docs-dev/guides/positioning.md +151 -0
  83. package/dist-skill/{docs → docs-dev}/guides/skill-maintenance.md +21 -10
  84. package/dist-skill/{docs → docs-dev}/internals/compiler.md +5 -6
  85. package/dist-skill/{docs → docs-dev}/internals/constraint-solver-quality.md +0 -1
  86. package/dist-skill/{docs → docs-dev}/internals/constraint-solver.md +0 -1
  87. package/dist-skill/{docs → docs-dev}/internals/sketch-2d-pipeline.md +2 -3
  88. package/examples/api/attachTo-basics.forge.js +5 -5
  89. package/examples/api/boolean-operations.forge.js +3 -3
  90. package/examples/api/bounding-box-visualizer.forge.js +2 -2
  91. package/examples/api/clone-duplicate.forge.js +1 -1
  92. package/examples/api/colors-union-vs-array.forge.js +6 -6
  93. package/examples/api/connector-assembly.forge.js +4 -4
  94. package/examples/api/connector-basics.forge.js +2 -2
  95. package/examples/api/extrude-options.forge.js +4 -10
  96. package/examples/api/feature-created-faces.forge.js +6 -10
  97. package/examples/api/fillet-showcase.forge.js +1 -1
  98. package/examples/api/folded-service-panel-cover.forge.js +2 -2
  99. package/examples/api/group-test.forge.js +1 -1
  100. package/examples/api/group-vs-union.forge.js +1 -1
  101. package/examples/api/highlight-debug.forge.js +4 -0
  102. package/examples/api/js-module-pillars.js +1 -1
  103. package/examples/api/js-module-scene.js +2 -2
  104. package/examples/api/mesh-import-slats.forge.js +1 -1
  105. package/examples/api/pointAlong-orientation.forge.js +1 -1
  106. package/examples/api/profile-2020-b-slot6.forge.js +0 -1
  107. package/examples/api/route-perimeter-flange.forge.js +1 -1
  108. package/examples/api/sdf-rover-demo.forge.js +10 -10
  109. package/examples/api/sketch-on-face-demo.forge.js +2 -2
  110. package/examples/api/sketch-regions.forge.js +4 -4
  111. package/examples/api/transition-curves.forge.js +1 -1
  112. package/examples/api/variable-sweep-pure-sdf-test.forge.js +162 -0
  113. package/examples/api/variable-sweep-test.forge.js +2 -2
  114. package/examples/api/wood-joinery.forge.js +60 -0
  115. package/examples/compiler-corpus/enclosure-shell-cuts.forge.js +3 -3
  116. package/examples/compiler-corpus/fastener-plate-variants.forge.js +2 -2
  117. package/examples/experiments/drone-arm.forge.js +53 -0
  118. package/examples/furniture/adjustable-table.forge.js +2 -2
  119. package/examples/furniture/bathroom.forge.js +11 -11
  120. package/examples/furniture/chair.forge.js +1 -1
  121. package/examples/generative/crystal-growth.forge.js +2 -2
  122. package/examples/generative/frost-spires.forge.js +3 -3
  123. package/examples/generative/golden-spiral-tower.forge.js +3 -3
  124. package/examples/mechanical/3d-printer.forge.js +28 -28
  125. package/examples/mechanical/5-finger-robot-hand.forge.js +15 -15
  126. package/examples/mechanical/airplane-propeller.forge.js +2 -2
  127. package/examples/mechanical/fillet-enclosure.forge.js +1 -1
  128. package/examples/mechanical/headphone-hanger-v2.forge.js +2 -2
  129. package/examples/mechanical/robot_hand.forge.js +15 -15
  130. package/examples/mechanical/robot_hand_2.forge.js +9 -9
  131. package/examples/products/bottle.forge.js +1 -1
  132. package/examples/products/chess-set.forge.js +19 -19
  133. package/examples/products/classical-piano.forge.js +11 -11
  134. package/examples/products/clock.forge.js +12 -12
  135. package/examples/products/iphone.forge.js +8 -8
  136. package/examples/products/laptop.forge.js +15 -15
  137. package/examples/products/liquid-soap-dispenser.forge.js +18 -18
  138. package/examples/products/origami-fish.forge.js +8 -6
  139. package/examples/products/spiderman-cake.forge.js +4 -4
  140. package/examples/toolbox/bolted-joint.forge.js +2 -2
  141. package/package.json +7 -4
  142. package/dist/assets/EditorApp-B-vQvgam.js +0 -9888
  143. package/dist/assets/LandingPage-C5n9hDXI.js +0 -322
  144. package/dist/assets/PublishedModelPage-Dt7PCVBj.js +0 -146
  145. package/dist/assets/__vite-browser-external-CURh0WXD.js +0 -8
  146. package/dist/assets/deserializeRunResult-BLAFoiE0.js +0 -19365
  147. package/dist/assets/index-1CYp3zUp.js +0 -1455
  148. package/dist/docs-raw/CLI.md +0 -865
  149. package/dist-skill/docs/API/API.md +0 -1666
  150. package/dist-skill/docs/API/README.md +0 -37
  151. package/dist-skill/docs/API/assembly/assembly.md +0 -617
  152. package/dist-skill/docs/API/core/edge-queries.md +0 -130
  153. package/dist-skill/docs/API/core/parameters.md +0 -122
  154. package/dist-skill/docs/API/core/reserved-terms.md +0 -137
  155. package/dist-skill/docs/API/core/sdf.md +0 -326
  156. package/dist-skill/docs/API/core/skill-cli.md +0 -194
  157. package/dist-skill/docs/API/core/skill-guide.md +0 -205
  158. package/dist-skill/docs/API/core/specs.md +0 -186
  159. package/dist-skill/docs/API/core/topology.md +0 -372
  160. package/dist-skill/docs/API/entities.md +0 -268
  161. package/dist-skill/docs/API/output/bom.md +0 -58
  162. package/dist-skill/docs/API/output/brep-export.md +0 -87
  163. package/dist-skill/docs/API/output/dimensions.md +0 -67
  164. package/dist-skill/docs/API/output/export.md +0 -110
  165. package/dist-skill/docs/API/output/gcode.md +0 -195
  166. package/dist-skill/docs/API/runtime/viewport.md +0 -420
  167. package/dist-skill/docs/API/sheet-metal/sheet-metal.md +0 -185
  168. package/dist-skill/docs/API/sketch/anchor.md +0 -37
  169. package/dist-skill/docs/API/sketch/booleans.md +0 -91
  170. package/dist-skill/docs/API/sketch/core.md +0 -73
  171. package/dist-skill/docs/API/sketch/extrude.md +0 -62
  172. package/dist-skill/docs/API/sketch/on-face.md +0 -104
  173. package/dist-skill/docs/API/sketch/operations.md +0 -78
  174. package/dist-skill/docs/API/sketch/path.md +0 -75
  175. package/dist-skill/docs/API/sketch/primitives.md +0 -146
  176. package/dist-skill/docs/API/sketch/regions.md +0 -80
  177. package/dist-skill/docs/API/sketch/text.md +0 -108
  178. package/dist-skill/docs/API/sketch/transforms.md +0 -65
  179. package/dist-skill/docs/API/toolbox/fasteners.md +0 -129
  180. package/dist-skill/docs/CLI.md +0 -865
  181. package/dist-skill/docs/INDEX.md +0 -94
  182. package/dist-skill/docs/RELEASING.md +0 -55
  183. package/dist-skill/docs/cli-monetization.md +0 -111
  184. package/dist-skill/docs/deployment.md +0 -281
  185. package/dist-skill/docs/generated/concepts.md +0 -2112
  186. package/dist-skill/docs/internals/shape-from-slices.md +0 -152
  187. package/dist-skill/docs/platform/admin.md +0 -45
  188. package/dist-skill/docs/platform/architecture.md +0 -79
  189. package/dist-skill/docs/platform/auth.md +0 -110
  190. package/dist-skill/docs/platform/email.md +0 -67
  191. package/dist-skill/docs/platform/projects.md +0 -111
  192. package/dist-skill/docs/platform/sharing.md +0 -90
  193. package/dist-skill/docs/runbook.md +0 -345
@@ -13,1454 +13,563 @@ skill-order: 100
13
13
 
14
14
  ### 3D Primitives
15
15
 
16
- Create basic 3D shapes.
16
+ #### `box()` — Create a rectangular box. Centered on XY, base at Z=0.
17
17
 
18
- #### `box()`
18
+ For named faces, build from a labeled sketch: `rect(x, y).labelEdges('s', 'e', 'n', 'w').extrude(z, { labels: { start: 'bottom', end: 'top' } })`.
19
19
 
20
- ```ts
21
- box$1(x: number, y: number, z: number, center?: boolean): Shape
22
- ```
20
+ `box$1(x: number, y: number, z: number): Shape`
23
21
 
24
- Create a rectangular box with named faces and edges. When center is false (default), one corner sits at the origin. Returns a Shape with faces (top, bottom, side-left, side-right, side-top, side-bottom) and edges (vert-bl, vert-br, vert-tr, vert-tl, etc.).
22
+ #### `cylinder()` — Create a cylinder or cone with named faces and edges. Centered on XY, base at Z=0.
25
23
 
26
- #### `cylinder()`
24
+ 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.
27
25
 
28
- ```ts
29
- cylinder$1(height: number, radius: number, radiusTop?: number, segments?: number, center?: boolean): Shape
30
- ```
26
+ `cylinder$1(height: number, radius: number, radiusTop?: number, segments?: number): Shape`
31
27
 
32
- Create a cylinder or cone with named faces and edges. When radiusTop differs from radius, creates a tapered cone. Use segments for regular prisms. Returns a Shape with faces (top, bottom, side) and edges (top-rim, bottom-rim).
28
+ #### `sphere()` Create a sphere centered at the origin. Use segments for lower-poly approximations.
33
29
 
34
- #### `sphere()`
30
+ `sphere$1(radius: number, segments?: number): Shape`
35
31
 
36
- ```ts
37
- sphere$1(radius: number, segments?: number): Shape
38
- ```
32
+ #### `torus()` — Create a torus (donut shape) lying in the XY plane. Centered on all axes (origin is the ring center).
39
33
 
40
- Create a sphere centered at the origin. Use segments for lower-poly approximations. @concept primitive
34
+ `torus$1(majorRadius: number, minorRadius: number, segments?: number): Shape`
41
35
 
42
36
  ### Boolean Operations
43
37
 
44
- Combine shapes using set operations.
45
-
46
- #### `union()`
47
-
48
- ```ts
49
- union(...shapes: (_ShapeOperand | _ShapeOperand[])[]): Shape
50
- ```
51
-
52
- Combine shapes into a single solid (additive boolean). Accepts individual shapes or arrays. @concept boolean
53
-
54
- #### `difference()`
55
-
56
- ```ts
57
- difference(...shapes: (_ShapeOperand | _ShapeOperand[])[]): Shape
58
- ```
59
-
60
- Subtract shapes from a base shape. The first shape is the base; all subsequent shapes are subtracted. @concept boolean
61
-
62
- #### `intersection()`
63
-
64
- ```ts
65
- intersection(...shapes: (_ShapeOperand | _ShapeOperand[])[]): Shape
66
- ```
67
-
68
- Keep only the overlapping volume of the input shapes (intersection boolean). @concept boolean
69
-
70
- ### Patterns & Topology
71
-
72
- Repeat, mirror, fillet, and chamfer geometry.
73
-
74
- #### `filletEdgeSegment()`
75
-
76
- ```ts
77
- filletEdgeSegment(shape: Shape, segment: EdgeSegment, radius: number, segments?: number): Shape
78
- ```
79
-
80
- 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().
81
-
82
- <details><summary><code>EdgeSegment</code></summary>
83
-
84
- ```ts
85
- interface EdgeSegment {
86
- /** Stable index within the extraction (deterministic for a given mesh). */
87
- index: number;
88
- start: Vec3;
89
- end: Vec3;
90
- midpoint: Vec3;
91
- /** Normalized direction from start → end. */
92
- direction: Vec3;
93
- length: number;
94
- /** Dihedral angle in degrees (0 = coplanar, 180 = knife edge). */
95
- dihedralAngle: number;
96
- /** true = outside corner (convex), false = inside corner (concave). */
97
- convex: boolean;
98
- /** Normal of first adjacent face. */
99
- normalA: Vec3;
100
- /** Normal of second adjacent face (same as normalA for boundary edges). */
101
- normalB: Vec3;
102
- /** true if this is a boundary (unmatched) edge — unusual for closed solids. */
103
- boundary: boolean;
104
- }
105
- ```
106
-
107
- </details>
108
-
109
- #### `chamferEdgeSegment()`
110
-
111
- ```ts
112
- chamferEdgeSegment(shape: Shape, segment: EdgeSegment, size: number): Shape
113
- ```
114
-
115
- Apply a chamfer (beveled edge) to a mesh-selected edge. Works on any straight edge of any shape — not limited to tracked box edges.
116
-
117
- #### `selectEdges()`
118
-
119
- ```ts
120
- selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]
121
- ```
122
-
123
- 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.
124
-
125
- <details><summary><code>EdgeQuery</code></summary>
126
-
127
- ```ts
128
- interface EdgeQuery {
129
- /** Sort by proximity to this point (closest first). */
130
- near?: Vec3;
131
- /** Filter: edge direction approximately parallel to this vector. */
132
- parallel?: Vec3;
133
- /** Filter: edge direction approximately perpendicular to this vector. */
134
- perpendicular?: Vec3;
135
- /** Filter: only convex (outside corner) edges. */
136
- convex?: boolean;
137
- /** Filter: only concave (inside corner) edges. */
138
- concave?: boolean;
139
- /** Filter: minimum dihedral angle in degrees. */
140
- minAngle?: number;
141
- /** Filter: maximum dihedral angle in degrees. */
142
- maxAngle?: number;
143
- /** Filter: minimum edge length. */
144
- minLength?: number;
145
- /** Filter: maximum edge length. */
146
- maxLength?: number;
147
- /** Filter: edge midpoint must be within this bounding region. */
148
- within?: BoundingRegion;
149
- /** Shorthand: edge midpoint Z ≈ this value (within tolerance). */
150
- atZ?: number;
151
- /** Tolerance for approximate matches (default: 1.0). */
152
- tolerance?: number;
153
- /** Angular tolerance in degrees for parallel/perpendicular (default: 10). */
154
- angleTolerance?: number;
155
- }
156
- ```
157
-
158
- </details>
159
-
160
- <details><summary><code>BoundingRegion</code></summary>
161
-
162
- ```ts
163
- interface BoundingRegion {
164
- xMin?: number;
165
- xMax?: number;
166
- yMin?: number;
167
- yMax?: number;
168
- zMin?: number;
169
- zMax?: number;
170
- }
171
- ```
172
-
173
- </details>
174
-
175
- #### `selectEdge()`
176
-
177
- ```ts
178
- selectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment
179
- ```
180
-
181
- 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.
182
-
183
- #### `coalesceEdges()`
184
-
185
- ```ts
186
- coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]
187
- ```
188
-
189
- 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.
190
-
191
- #### `arcBridgeBetweenRects()`
192
-
193
- ```ts
194
- arcBridgeBetweenRects(rectA: RectAreaArg, rectB: RectAreaArg, segments?: number): Shape
195
- ```
196
-
197
- Build an arc bridge between two rectangular areas.
198
-
199
- #### `filletEdge()`
200
-
201
- ```ts
202
- filletEdge(shape: Shape, edge: EdgeRef, radius: number, quadrant?: [ number, number ], segments?: number): Shape
203
- ```
204
-
205
- Round a named edge of a shape with a circular fillet of the given radius. Requires a compile-covered target.
206
-
207
- <details><summary><code>EdgeRef</code></summary>
208
-
209
- ```ts
210
- interface EdgeRef {
211
- name: EdgeName;
212
- /** Compiler-owned edge query when available. */
213
- query?: EdgeQueryRef;
214
- }
215
- ```
216
-
217
- </details>
218
-
219
- #### `chamferEdge()`
220
-
221
- ```ts
222
- chamferEdge(shape: Shape, edge: EdgeRef, size: number, quadrant?: [ number, number ]): Shape
223
- ```
224
-
225
- Bevel a named edge of a shape with a 45-degree chamfer of the given size. Requires a compile-covered target.
226
-
227
- #### `filletCorners()`
228
-
229
- ```ts
230
- filletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch
231
- ```
232
-
233
- Create a polygon from points with specified corners rounded to arc fillets. Each corner spec identifies a vertex index and radius.
234
-
235
- <details><summary><code>FilletCornerSpec</code></summary>
236
-
237
- ```ts
238
- interface FilletCornerSpec {
239
- index: number;
240
- radius: number;
241
- segments?: number;
242
- }
243
- ```
244
-
245
- </details>
246
-
247
- #### `linearPattern()`
248
-
249
- ```ts
250
- linearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape
251
- ```
252
-
253
- Repeat a shape in a linear pattern along a direction vector and union the copies.
254
-
255
- #### `circularPattern()`
256
-
257
- ```ts
258
- circularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape
259
- ```
38
+ #### `union()` — Combine shapes into a single solid (additive boolean).
260
39
 
261
- 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] })
40
+ Accepts individual shapes, or an array of shapes. The first operand's color is preserved in the result.
262
41
 
263
- <details><summary><code>CircularPatternOptions</code></summary>
42
+ `union(...inputs: ShapeOperandInput[]): Shape`
264
43
 
265
- ```ts
266
- interface CircularPatternOptions {
267
- /** Center X of the rotation (default: 0). Used when axis is Z (legacy mode). */
268
- centerX?: number;
269
- /** Center Y of the rotation (default: 0). Used when axis is Z (legacy mode). */
270
- centerY?: number;
271
- }
272
- ```
44
+ #### `difference()` — Subtract shapes from a base shape (subtractive boolean).
273
45
 
274
- </details>
46
+ The first shape is the base; all subsequent shapes are subtracted from it. Accepts individual shapes, or an array of shapes.
275
47
 
276
- #### `mirrorCopy()`
48
+ `difference(...inputs: ShapeOperandInput[]): Shape`
277
49
 
278
- ```ts
279
- mirrorCopy(shape: Shape, normal: [ number, number, number ]): Shape
280
- ```
50
+ #### `intersection()` — Keep only the overlapping volume of the input shapes (intersection boolean).
281
51
 
282
- Mirror a shape across a plane defined by its normal and union the mirror with the original.
52
+ Requires at least two shapes. Accepts individual shapes, or an array.
283
53
 
284
- ### Imports & Composition
54
+ `intersection(...inputs: ShapeOperandInput[]): Shape`
285
55
 
286
- Import model files and SVG assets from other files.
56
+ ### Edge Features
287
57
 
288
- #### `require()`
289
-
290
- ```ts
291
- require$1(path: string, paramOverrides?: Record<string, number>): any
292
- ```
293
-
294
- Import a module with optional ForgeCAD parameter overrides. Returns the module's exports. @concept import
295
-
296
- #### `importSvgSketch()`
297
-
298
- ```ts
299
- importSvgSketch(fileName: string, options?: SvgImportOptions): Sketch
300
- ```
301
-
302
- Parse an SVG file and return it as a Sketch with options for region filtering, scaling, and simplification. @concept import
303
-
304
- <details><summary><code>SvgImportOptions</code></summary>
305
-
306
- ```ts
307
- interface SvgImportOptions {
308
- /** Which geometry channels to include: - `auto`: prefer fills; if no fill geometry exists, fall back to strokes - `fill`: import only filled regions - `stroke`: import only stroke geometry - `fill-and-stroke`: include both */
309
- include?: "auto" | "fill" | "stroke" | "fill-and-stroke";
310
- /** Keep all disconnected regions, or only the largest. */
311
- regionSelection?: "all" | "largest";
312
- /** Keep at most this many regions (largest-first). */
313
- maxRegions?: number;
314
- /** Drop regions below this absolute area threshold. */
315
- minRegionArea?: number;
316
- /** Drop regions below this ratio of largest-region area. */
317
- minRegionAreaRatio?: number;
318
- /** Curve flattening tolerance in SVG user units. Smaller = more segments, higher fidelity. */
319
- flattenTolerance?: number;
320
- /** Minimum segment count for arc discretization. */
321
- arcSegments?: number;
322
- /** Global scale applied after SVG parsing. */
323
- scale?: number;
324
- /** Maximum imported sketch width. If exceeded, geometry is uniformly downscaled to fit. */
325
- maxWidth?: number;
326
- /** Maximum imported sketch height. If exceeded, geometry is uniformly downscaled to fit. */
327
- maxHeight?: number;
328
- /** Recenter imported geometry so its 2D bounds center is at CAD origin. */
329
- centerOnOrigin?: boolean;
330
- /** Simplification tolerance for final sketch cleanup. */
331
- simplify?: number;
332
- /** Flip SVG Y-down coordinates to CAD Y-up. Enabled by default. */
333
- invertY?: boolean;
334
- }
335
- ```
336
-
337
- </details>
338
-
339
- ### Parameters
340
-
341
- Declare user-adjustable parameters with UI controls.
342
-
343
- #### `param()`
344
-
345
- ```ts
346
- param(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number
347
- ```
348
-
349
- Declare a parameter. Returns the current value (default or overridden). Each call registers the param for UI generation.
350
-
351
- #### `boolParam()`
352
-
353
- ```ts
354
- boolParam(name: string, defaultValue: boolean): boolean
355
- ```
356
-
357
- Declare a boolean parameter. Returns the current boolean value. Renders as a checkbox in the UI.
358
-
359
- ### Grouping
360
-
361
- Organize multiple shapes into named groups.
362
-
363
- #### `group()`
364
-
365
- ```ts
366
- group(...items: GroupInput[]): ShapeGroup
367
- ```
368
-
369
- 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.).
370
-
371
- ### Section & Projection
372
-
373
- Slice or project 3D shapes to 2D.
374
-
375
- #### `intersectWithPlane()`
376
-
377
- ```ts
378
- intersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch
379
- ```
380
-
381
- Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch.
382
-
383
- #### `projectToPlane()`
384
-
385
- ```ts
386
- projectToPlane(shape: Shape, plane: PlaneSpec): Sketch
387
- ```
388
-
389
- Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch.
390
-
391
- ### Other
392
-
393
- #### `composeChain()`
394
-
395
- ```ts
396
- composeChain(...steps: TransformInput[]): Transform
397
- ```
398
-
399
- Compose transforms in chain order. Equivalent to Transform.identity().mul(a).mul(b).mul(c)...
400
-
401
- #### `portFactory()`
402
-
403
- ```ts
404
- portFactory(input: PortInput): PortDef
405
- ```
406
-
407
- <details><summary><code>PortInput</code></summary>
408
-
409
- ```ts
410
- interface PortInput {
411
- kind?: JointType;
412
- min?: number;
413
- max?: number;
414
- }
415
- ```
416
-
417
- </details>
418
-
419
- <details><summary><code>PortDef</code></summary>
420
-
421
- ```ts
422
- interface PortDef {
423
- origin: Vec3;
424
- axis: Vec3;
425
- up: Vec3;
426
- extent?: number;
427
- kind?: JointType;
428
- min?: number;
429
- max?: number;
430
- connectorType?: string;
431
- gender?: ConnectorGender;
432
- measurements?: Record<string, number | string>;
433
- }
434
- ```
435
-
436
- </details>
437
-
438
- #### `connectorFactory()`
439
-
440
- ```ts
441
- connectorFactory(connectorType: string, input: PortInput, measurements?: Record<string, number | string>): ConnectorInput
442
- ```
443
-
444
-
445
- <details><summary><code>ConnectorInput</code> extends PortInput</summary>
446
-
447
- ```ts
448
- interface ConnectorInput extends PortInput {
449
- connectorType: string;
450
- gender: ConnectorGender$1;
451
- measurements?: Record<string, number | string>;
452
- }
453
- ```
454
-
455
- </details>
456
-
457
- #### `polar()`
458
-
459
- ```ts
460
- polar(length: number, angleDeg: number, from?: [ number, number ]): [ number, number ]
461
- ```
462
-
463
- 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) ```
464
-
465
- #### `fillet()`
466
-
467
- ```ts
468
- fillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape
469
- ```
470
-
471
- 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)
472
-
473
- #### `chamfer()`
474
-
475
- ```ts
476
- chamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape
477
- ```
478
-
479
- 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] })
480
-
481
- #### `draft()`
482
-
483
- ```ts
484
- draft(shape: Shape, angleDeg: number, pullDirection?: [ number, number, number ], neutralPlaneOffset?: number): Shape
485
- ```
486
-
487
- 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)
488
-
489
- #### `offsetSolid()`
490
-
491
- ```ts
492
- offsetSolid(shape: Shape, thickness: number): Shape
493
- ```
494
-
495
- 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)
496
-
497
- #### `choiceParam()`
498
-
499
- ```ts
500
- choiceParam(name: string, defaultValue: string, choices: string[]): string
501
- ```
502
-
503
- 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.
504
-
505
- #### `listParam()`
506
-
507
- ```ts
508
- listParam<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: { ... }): T[]
509
- ```
510
-
511
- 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.
512
-
513
- <details><summary><code>ListParamFieldDef</code></summary>
514
-
515
- ```ts
516
- interface ListParamFieldDef {
517
- min?: number;
518
- max?: number;
519
- step?: number;
520
- unit?: string;
521
- integer?: boolean;
522
- boolean?: boolean;
523
- choices?: string[];
524
- }
525
- ```
526
-
527
- </details>
528
-
529
- #### `distance()`
530
-
531
- ```ts
532
- distance(a: Vec3, b: Vec3): number
533
- ```
58
+ #### `fillet()` — Apply fillets (rounded edges) to one or more edges of a shape.
534
59
 
535
- #### `midpoint()`
60
+ **Details**
536
61
 
537
- ```ts
538
- midpoint(a: Vec3, b: Vec3): Vec3
539
- ```
62
+ 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.
540
63
 
541
- #### `lerp()`
64
+ The `edges` parameter is flexible: - Omit to fillet **all** sharp edges - Pass an `EdgeQuery` for an inline filter (most common) - Pass an `EdgeSegment` or `EdgeSegment[]` from `selectEdges()` for pre-selected edges
542
65
 
543
- ```ts
544
- lerp(a: Vec3, b: Vec3, t: number): Vec3
545
- ```
66
+ Throws if no edges match the selection, or if `radius` is not a positive finite number.
546
67
 
547
- #### `direction()`
68
+ **Example**
548
69
 
549
70
  ```ts
550
- direction(a: Vec3, b: Vec3): Vec3
551
- ```
71
+ // Fillet all edges
72
+ fillet(myShape, 2)
552
73
 
553
- #### `offset()`
74
+ // Fillet only top convex edges
75
+ fillet(myShape, 1.5, { atZ: 20, convex: true })
554
76
 
555
- ```ts
556
- offset(point: Vec3, dir: Vec3, amount: number): Vec3
77
+ // Fillet vertical edges selected beforehand
78
+ const edges = selectEdges(myShape, { parallel: [0, 0, 1] })
79
+ fillet(myShape, 3, edges)
557
80
  ```
558
81
 
559
- #### `loftAlongSpine()`
82
+ `fillet(shape: Shape, radius: number, edges?: EdgeSelector, segments?: number): Shape`
560
83
 
561
- ```ts
562
- loftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3$4[], tValues: number[], options?: LoftAlongSpineOptions): Shape
563
- ```
84
+ #### `chamfer()` — Apply chamfers (beveled edges) to one or more edges of a shape.
564
85
 
565
- 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().
86
+ **Details**
566
87
 
567
- <details><summary><code>LoftAlongSpineOptions</code></summary>
88
+ Produces a 45° bevel at the specified `size` (distance from edge). Works on both straight and curved edges. Supports OCCT and Manifold backends.
568
89
 
569
- ```ts
570
- interface LoftAlongSpineOptions {
571
- /** Number of samples when spine is a Curve3D. Default 48. */
572
- samples?: number;
573
- /** Marching-grid edge length for level-set meshing. Smaller = finer. */
574
- edgeLength?: number;
575
- /** Optional extra bounds padding. */
576
- boundsPadding?: number;
577
- /** Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. */
578
- up?: Vec3$4;
579
- }
580
- ```
581
-
582
- </details>
90
+ The `edges` parameter accepts the same options as `fillet()`: inline `EdgeQuery`, pre-selected `EdgeSegment`/`EdgeSegment[]`, or `undefined` (all sharp edges).
583
91
 
584
- #### `variableSweep()`
92
+ **Example**
585
93
 
586
94
  ```ts
587
- variableSweep(spine: Curve3D | Vec3$4[], sections: VariableSweepSection[], options?: VariableSweepOptions): Shape
588
- ```
589
-
590
- 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.
591
-
592
- <details><summary><code>VariableSweepSection</code></summary>
95
+ // Chamfer all edges
96
+ chamfer(myShape, 1)
593
97
 
594
- ```ts
595
- interface VariableSweepSection {
596
- /** Parameter along the spine (0 = start, 1 = end). */
597
- t: number;
598
- /** Cross-section profile at this station. */
599
- profile: Sketch;
600
- }
98
+ // Chamfer only vertical edges
99
+ chamfer(myShape, 2, { parallel: [0, 0, 1] })
601
100
  ```
602
101
 
603
- </details>
102
+ `chamfer(shape: Shape, size: number, edges?: EdgeSelector): Shape`
604
103
 
605
- <details><summary><code>VariableSweepOptions</code></summary>
606
-
607
- ```ts
608
- interface VariableSweepOptions {
609
- /** Number of samples when spine is a Curve3D. Default 48. */
610
- samples?: number;
611
- /** Marching-grid edge length for level-set meshing. Smaller = finer. */
612
- edgeLength?: number;
613
- /** Optional extra bounds padding. */
614
- boundsPadding?: number;
615
- /** Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. */
616
- up?: Vec3$4;
617
- }
618
- ```
104
+ #### `draft()` — Apply a draft angle (taper) to vertical faces for mold extraction.
619
105
 
620
- </details>
106
+ **Details**
621
107
 
622
- #### `loadFont()`
108
+ 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°.
623
109
 
624
- ```ts
625
- loadFont(source: string | ArrayBuffer, cacheKey?: string): opentype$1.Font
626
- ```
110
+ Requires the OCCT backend. Throws on Manifold.
627
111
 
628
- 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)
629
-
630
- #### `hermiteTransition()`
112
+ **Example**
631
113
 
632
114
  ```ts
633
- hermiteTransition(a: EdgeEndpoint, b: EdgeEndpoint): HermiteCurve3D
634
- ```
635
-
636
- 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
637
-
638
- <details><summary><code>EdgeEndpoint</code></summary>
115
+ // Add 3° draft to a box for injection molding
116
+ draft(myBox, 3)
639
117
 
640
- ```ts
641
- interface EdgeEndpoint {
642
- /** Connection point on the edge */
643
- point: Vec3$5;
644
- /** Tangent direction along the edge at the connection point */
645
- tangent: Vec3$5;
646
- /** Surface normal at the connection point (optional, for future G2 support) */
647
- normal?: Vec3$5;
648
- /** Weight controlling how far the curve follows this edge's tangent. Default 1.0. */
649
- weight?: number;
650
- }
118
+ // Draft with custom pull direction and neutral plane
119
+ draft(myShape, 2, [0, 0, 1], 10)
651
120
  ```
652
121
 
653
- </details>
654
-
655
- #### `hermiteTransitionG2()`
122
+ `draft(shape: Shape, angleDeg: number, pullDirection?: [ number, number, number ], neutralPlaneOffset?: number): Shape`
656
123
 
657
- ```ts
658
- hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D
659
- ```
124
+ #### `offsetSolid()` — Uniformly offset all surfaces of a solid inward or outward.
660
125
 
661
- 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.
126
+ **Details**
662
127
 
663
- <details><summary><code>QuinticHermiteCurveEndpoint</code></summary>
664
-
665
- ```ts
666
- interface QuinticHermiteCurveEndpoint {
667
- /** Position */
668
- point: Vec3$5;
669
- /** Tangent direction (will be normalized internally) */
670
- tangent: Vec3$5;
671
- /** Second derivative / curvature vector. Default [0, 0, 0]. */
672
- curvature?: Vec3$5;
673
- /** Weight: scales tangent magnitude relative to chord length. Default 1.0. */
674
- weight?: number;
675
- }
676
- ```
128
+ 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.
677
129
 
678
- </details>
130
+ Requires the OCCT backend. Throws on Manifold.
679
131
 
680
- #### `circularLayout()`
132
+ **Example**
681
133
 
682
134
  ```ts
683
- circularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[]
684
- ```
685
-
686
- 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)); } ```
135
+ // Grow a box outward by 1mm on all sides
136
+ offsetSolid(myBox, 1)
687
137
 
688
- <details><summary><code>CircularLayoutOptions</code></summary>
689
-
690
- ```ts
691
- interface CircularLayoutOptions {
692
- /** Angle of the first element in degrees (default: 0 = +X axis). */
693
- startDeg?: number;
694
- /** Center X coordinate (default: 0). */
695
- centerX?: number;
696
- /** Center Y coordinate (default: 0). */
697
- centerY?: number;
698
- }
138
+ // Shrink a shape inward by 0.5mm
139
+ offsetSolid(myShape, -0.5)
699
140
  ```
700
141
 
701
- </details>
702
-
703
- <details><summary><code>LayoutPoint</code></summary>
142
+ `offsetSolid(shape: Shape, thickness: number): Shape`
704
143
 
705
- ```ts
706
- interface LayoutPoint {
707
- x: number;
708
- y: number;
709
- }
710
- ```
144
+ ### Patterns & Layout
711
145
 
712
- </details>
146
+ #### `selectEdges()` — Select all edges from a shape that match the given query.
713
147
 
714
- #### `polygonVertices()`
148
+ **Details**
715
149
 
716
- ```ts
717
- polygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[]
718
- ```
150
+ 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.
719
151
 
720
- 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); ```
152
+ 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.
721
153
 
722
- <details><summary><code>PolygonVerticesOptions</code></summary>
154
+ **Example**
723
155
 
724
156
  ```ts
725
- interface PolygonVerticesOptions {
726
- /** Angle of the first vertex in degrees (default: 90 = top). */
727
- startDeg?: number;
728
- /** Center X coordinate (default: 0). */
729
- centerX?: number;
730
- /** Center Y coordinate (default: 0). */
731
- centerY?: number;
157
+ // Fillet all top edges of a box
158
+ const topEdges = selectEdges(part, { atZ: 20, perpendicular: [0, 0, 1] });
159
+ let result = part;
160
+ for (const edge of coalesceEdges(topEdges)) {
161
+ result = fillet(result, 2, edge);
732
162
  }
733
163
  ```
734
164
 
735
- </details>
736
-
737
- #### `routePerimeter()`
738
-
739
- ```ts
740
- routePerimeter(steps: PerimeterStep[]): Sketch
741
- ```
742
-
743
- 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 }, ]) ```
744
-
745
- #### `linearPattern2d()`
165
+ `selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]`
746
166
 
747
- ```ts
748
- linearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch
749
- ```
750
-
751
- Repeat a sketch in a linear pattern
752
-
753
- #### `circularPattern2d()`
754
-
755
- ```ts
756
- circularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | { centerX?: number; centerY?: number; startDeg?: number; }, centerY?: number): Sketch
757
- ```
167
+ **`EdgeQuery`**
758
168
 
759
- Repeat a sketch in a circular pattern around a center point
169
+ | Option | Type | Description |
170
+ |--------|------|-------------|
171
+ | `near?` | `Vec3` | Sort by proximity to this point (closest first). When used with `selectEdge`, picks the closest match. |
172
+ | `parallel?` | `Vec3` | Filter: edge direction approximately parallel to this vector. |
173
+ | `perpendicular?` | `Vec3` | Filter: edge direction approximately perpendicular to this vector. |
174
+ | `convex?` | `boolean` | Filter: only convex (outside corner) edges. |
175
+ | `concave?` | `boolean` | Filter: only concave (inside corner) edges. |
176
+ | `minAngle?` | `number` | Filter: minimum dihedral angle in degrees. |
177
+ | `maxAngle?` | `number` | Filter: maximum dihedral angle in degrees. |
178
+ | `minLength?` | `number` | Filter: minimum edge length. |
179
+ | `maxLength?` | `number` | Filter: maximum edge length. |
180
+ | `within?` | `BoundingRegion` | Filter: edge midpoint must be within this bounding region. |
181
+ | `atZ?` | `number` | Shorthand: edge midpoint Z ≈ this value (within `tolerance`). Equivalent to `within: { zMin: atZ - tol, zMax: atZ + tol }`. |
182
+ | `tolerance?` | `number` | Position tolerance for approximate matches (default: `1.0`). Used by `atZ` and `near`. |
183
+ | `angleTolerance?` | `number` | Angular tolerance in degrees for `parallel`/`perpendicular` filters (default: `10`). |
760
184
 
761
- #### `arcSlot()`
185
+ `BoundingRegion`: `{ xMin?: number, xMax?: number, yMin?: number, yMax?: number, zMin?: number, zMax?: number }`
762
186
 
763
- ```ts
764
- arcSlot(pitchRadius: number, sweepDeg: number, thickness: number): Sketch
765
- ```
187
+ **`EdgeSegment`**
766
188
 
767
- 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 ```
189
+ | Option | Type | Description |
190
+ |--------|------|-------------|
191
+ | `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |
192
+ | `direction` | `Vec3` | Normalized direction from start → end. |
193
+ | `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |
194
+ | `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |
195
+ | `normalA` | `Vec3` | Normal of first adjacent face. |
196
+ | `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |
197
+ | `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |
198
+ | `start`, `end`, `midpoint`, `length` | | — |
768
199
 
769
- #### `surfacePatch()`
200
+ #### `selectEdge()` — Select the single best-matching edge from a shape.
770
201
 
771
- ```ts
772
- surfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape
773
- ```
202
+ **Details**
774
203
 
775
- 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.
204
+ 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.
776
205
 
777
- <details><summary><code>SurfacePatchOptions</code></summary>
206
+ **Example**
778
207
 
779
208
  ```ts
780
- interface SurfacePatchOptions {
781
- /** Number of samples along each direction. Default 24. */
782
- resolution?: number;
783
- /** Thickness of the generated solid. Default 0.5. */
784
- thickness?: number;
785
- }
209
+ // Chamfer one specific edge near a known point
210
+ const bottomEdge = selectEdge(part, { near: [25, 0, 0], atZ: 0 });
211
+ result = chamfer(result, 1.5, bottomEdge);
786
212
  ```
787
213
 
788
- </details>
214
+ `selectEdge(shape: Shape, query?: EdgeQuery): EdgeSegment`
789
215
 
790
- #### `transitionCurve()`
216
+ #### `coalesceEdges()` — Merge collinear edge segments into longer logical edges.
791
217
 
792
- ```ts
793
- transitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D
794
- ```
218
+ **Details**
795
219
 
796
- 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 }, ); ```
220
+ 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.
797
221
 
798
- <details><summary><code>TransitionEdge</code></summary>
222
+ The `tolerance` controls the maximum perpendicular distance from collinearity before two segments are considered non-collinear. Default: `0.01`.
799
223
 
800
- ```ts
801
- interface TransitionEdge {
802
- /** Connection point on the edge. Can be any point along the edge where the transition should connect. */
803
- point: Vec3$7;
804
- /** 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). */
805
- tangent: Vec3$7;
806
- /** Surface normal at the connection point (optional). Used as a hint for the sweep frame's up vector. */
807
- normal?: Vec3$7;
808
- }
809
- ```
810
-
811
- </details>
812
-
813
- <details><summary><code>TransitionCurveOptions</code></summary>
224
+ **Example**
814
225
 
815
226
  ```ts
816
- interface TransitionCurveOptions {
817
- /** 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 */
818
- weightA?: number;
819
- /** 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 */
820
- weightB?: number;
821
- /** Number of sample points for the output polyline. Default 64. Higher values give smoother curves at the cost of more geometry. */
822
- samples?: number;
227
+ const topEdges = selectEdges(part, { atZ: 20 });
228
+ for (const edge of coalesceEdges(topEdges)) {
229
+ result = fillet(result, 2, edge);
823
230
  }
824
231
  ```
825
232
 
826
- </details>
827
-
828
- #### `transitionSurface()`
829
-
830
- ```ts
831
- transitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape
832
- ```
233
+ `coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]`
833
234
 
834
- 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 }, ); ```
235
+ #### `filletCorners()` Create a polygon from points with specific corners rounded to arc fillets.
835
236
 
237
+ **Details**
836
238
 
837
- <details><summary><code>TransitionSurfaceOptions</code> extends TransitionCurveOptions</summary>
239
+ 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.
838
240
 
839
- ```ts
840
- interface TransitionSurfaceOptions extends TransitionCurveOptions {
841
- /** Cross-section profile to sweep along the transition curve. If omitted, a circular profile with `radius` is used. */
842
- profile?: Sketch;
843
- /** Radius of circular cross-section (used when `profile` is omitted). Default: 5% of chord length. */
844
- radius?: number;
845
- width: number;
846
- height: number;
847
- /** Preferred up vector for the sweep frame. Default: auto-detected. */
848
- up?: Vec3$7;
849
- /** Edge length for level-set meshing. Smaller = finer. */
850
- edgeLength?: number;
851
- /** Extra bounds padding for level-set meshing. */
852
- boundsPadding?: number;
853
- }
854
- ```
241
+ Constraints: - Collinear corners cannot be filleted (throws an error) - Two neighboring fillets whose tangent lengths overlap the same edge will throw - Radius must be positive and small enough to fit within the adjacent edge lengths
855
242
 
856
- </details>
243
+ 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.
857
244
 
858
- #### `transitionCurveFromPoints()`
245
+ **Example**
859
246
 
860
247
  ```ts
861
- transitionCurveFromPoints(startPoint: Vec3$7, startTangent: Vec3$7, endPoint: Vec3$7, endTangent: Vec3$7, options?: TransitionCurveOptions): HermiteCurve3D
248
+ const roof = filletCorners(roofPoints, [
249
+ { index: 3, radius: 19 },
250
+ { index: 4, radius: 19 },
251
+ { index: 5, radius: 19 },
252
+ ]);
862
253
  ```
863
254
 
864
- Convenience: create a transition curve from raw coordinate data. Useful when you have endpoints and directions as plain arrays without constructing TransitionEdge objects.
255
+ `filletCorners(points: PointInput[], corners: FilletCornerSpec[]): Sketch`
865
256
 
866
- #### `connectEdges()`
257
+ `FilletCornerSpec`: `{ index: number, radius: number, segments?: number }`
867
258
 
868
- ```ts
869
- connectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape
870
- ```
259
+ #### `circularLayout()` — Compute evenly-spaced positions around a circle.
871
260
 
872
- <details><summary><code>EdgeSegment</code></summary>
261
+ Eliminates the most common trig pattern in CAD scripts:
873
262
 
874
- ```ts
875
- interface EdgeSegment {
876
- /** Stable index within the extraction (deterministic for a given mesh). */
877
- index: number;
878
- start: Vec3;
879
- end: Vec3;
880
- midpoint: Vec3;
881
- /** Normalized direction from start → end. */
882
- direction: Vec3;
883
- length: number;
884
- /** Dihedral angle in degrees (0 = coplanar, 180 = knife edge). */
885
- dihedralAngle: number;
886
- /** true = outside corner (convex), false = inside corner (concave). */
887
- convex: boolean;
888
- /** Normal of first adjacent face. */
889
- normalA: Vec3;
890
- /** Normal of second adjacent face (same as normalA for boundary edges). */
891
- normalB: Vec3;
892
- /** true if this is a boundary (unmatched) edge — unusual for closed solids. */
893
- boundary: boolean;
263
+ ```js
264
+ // Before — manual trig
265
+ for (let i = 0; i < 12; i++) {
266
+ const angle = i * 30 * Math.PI / 180;
267
+ markers.push(marker.translate(r * Math.cos(angle), r * Math.sin(angle), 0));
894
268
  }
895
- ```
896
269
 
897
- </details>
898
-
899
-
900
- <details><summary><code>ConnectEdgesOptions</code> extends TransitionSurfaceOptions</summary>
901
-
902
- ```ts
903
- interface ConnectEdgesOptions extends TransitionSurfaceOptions {
904
- /** Which end of edge A to connect. Default: 'start'. */
905
- endA?: EdgeEnd;
906
- /** Which end of edge B to connect. Default: 'start'. */
907
- endB?: EdgeEnd;
908
- /** Tangent mode for edge A. Default: 'along'. */
909
- tangentModeA?: TangentMode;
910
- /** Tangent mode for edge B. Default: 'along'. */
911
- tangentModeB?: TangentMode;
912
- /** Explicit tangent for edge A. */
913
- tangentA?: Vec3$7;
914
- /** Explicit tangent for edge B. */
915
- tangentB?: Vec3$7;
916
- /** Flip tangent A. */
917
- flipA?: boolean;
918
- /** Flip tangent B. */
919
- flipB?: boolean;
270
+ // After — declarative
271
+ for (const {x, y} of circularLayout(12, r)) {
272
+ markers.push(marker.translate(x, y, 0));
920
273
  }
921
274
  ```
922
275
 
923
- </details>
276
+ `circularLayout(count: number, radius: number, options?: CircularLayoutOptions): LayoutPoint[]`
924
277
 
925
- #### `spec()`
926
-
927
- ```ts
928
- spec(name: string, checkFn: (...args: any[]) => void): Spec
929
- ```
278
+ **`CircularLayoutOptions`**
279
+ - `startDeg?: number` — Angle of the first element in degrees (default: 0 = +X axis).
280
+ - `centerX?: number` — Center X coordinate (default: 0).
281
+ - `centerY?: number` Center Y coordinate (default: 0).
930
282
 
931
- 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.
283
+ `LayoutPoint`: `{ x: number, y: number }`
932
284
 
933
- <details><summary><code>Spec</code></summary>
285
+ #### `polygonVertices()` — Compute the vertex positions of a regular polygon.
934
286
 
935
- ```ts
936
- interface Spec {
937
- /** The display name of this spec */
938
- name: string;
939
- }
940
- ```
287
+ Default orientation places the first vertex at the top (90 degrees), matching the convention used by `ngon()`.
941
288
 
942
- </details>
289
+ Eliminates manual Math.sqrt(3) for triangles, pentagon vertex math, etc:
943
290
 
944
- #### `faceProfile()`
291
+ ```js
292
+ // Before — manual equilateral triangle
293
+ const v1 = [center.x - r/2, center.y + r * Math.sqrt(3)/2];
294
+ const v2 = [center.x - r/2, center.y - r * Math.sqrt(3)/2];
295
+ const v3 = [center.x + r, center.y];
945
296
 
946
- ```ts
947
- faceProfile(shape: Shape, face: FaceSelector): Sketch
297
+ // After — declarative
298
+ const [v1, v2, v3] = polygonVertices(3, r);
948
299
  ```
949
300
 
950
- #### `fingerJointProfile()`
301
+ `polygonVertices(sides: number, radius: number, options?: PolygonVerticesOptions): LayoutPoint[]`
951
302
 
952
- ```ts
953
- fingerJointProfile(length: number, thickness: number, options?: FingerJointOptions): FingerJointResult
954
- ```
303
+ **`PolygonVerticesOptions`**
304
+ - `startDeg?: number` — Angle of the first vertex in degrees (default: 90 = top).
305
+ - `centerX?: number` — Center X coordinate (default: 0).
306
+ - `centerY?: number` — Center Y coordinate (default: 0).
955
307
 
956
- Generate a finger joint edge profile for two mating edges. The tab side gets finger protrusions along its bottom edge (y=0), the slot side gets matching gaps cut from its edge. Both profiles span [0, length] along X and [0, thickness] along Y.
308
+ #### `linearPattern()` Repeat a shape in a linear pattern along a direction vector and union the copies.
957
309
 
958
- <details><summary><code>FingerJointOptions</code></summary>
310
+ **Details**
959
311
 
960
- ```ts
961
- interface FingerJointOptions {
962
- /** Explicit finger count (must be odd, >= 3). Default: auto from length/thickness. */
963
- fingers?: number;
964
- /** Explicit finger width. Default: auto. */
965
- fingerWidth?: number;
966
- /** Extra clearance per side (mm). Default: 0. */
967
- clearance?: number;
968
- /** Laser kerf (mm). Default: 0. */
969
- kerf?: number;
970
- /** Whether edge starts with full finger or half. Default: 'full'. */
971
- endStyle?: "full" | "half";
972
- }
973
- ```
974
-
975
- </details>
312
+ 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.
976
313
 
977
- <details><summary><code>FingerJointResult</code></summary>
314
+ **Example**
978
315
 
979
316
  ```ts
980
- interface FingerJointResult {
981
- /** Even-position finger rects (tabs for side A, slots for side B). */
982
- tabProfile: Sketch;
983
- /** Odd-position finger rects (tabs for side B, slots for side A). */
984
- matingProfile: Sketch;
985
- /** Legacy: full rectangle minus odd slot cuts. */
986
- slotProfile: Sketch;
987
- }
317
+ // 5 cylinders, 20mm apart along X
318
+ linearPattern(cylinder(10, 3), 5, 20, 0)
988
319
  ```
989
320
 
990
- </details>
991
-
992
- #### `tabSlotProfile()`
993
-
994
- ```ts
995
- tabSlotProfile(length: number, thickness: number, options?: TabSlotOptions): TabSlotResult
996
- ```
321
+ `linearPattern(shape: Shape, count: number, dx: number, dy: number, dz?: number): Shape`
997
322
 
998
- Generate tabs on one edge and matching slots for the mating panel face. Tabs protrude in +Y from y=0, spanning the tab width along X. Slots are the cutout geometry to subtract from the mating panel.
323
+ #### `circularPattern()` Repeat a shape in a circular pattern around an axis and union the copies.
999
324
 
1000
- <details><summary><code>TabSlotOptions</code></summary>
325
+ **Details**
1001
326
 
1002
- ```ts
1003
- interface TabSlotOptions {
1004
- /** Number of tabs. Default: auto (length / (4 * thickness)). */
1005
- tabCount?: number;
1006
- /** Tab width. Default: 2 * thickness. */
1007
- tabWidth?: number;
1008
- /** Extra clearance per side (mm). Default: 0. */
1009
- clearance?: number;
1010
- /** Laser kerf (mm). Default: 0. */
1011
- kerf?: number;
1012
- /** Distance from panel edges to first/last tab center. Default: thickness. */
1013
- inset?: number;
1014
- }
1015
- ```
327
+ 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.
1016
328
 
1017
- </details>
329
+ Two calling conventions: - **Simple** (Z axis): `circularPattern(shape, 6)` or `circularPattern(shape, 6, centerX, centerY)` - **Advanced** (arbitrary axis): `circularPattern(shape, 6, { axis, origin })`
1018
330
 
1019
- <details><summary><code>TabSlotResult</code></summary>
331
+ **Example**
1020
332
 
1021
333
  ```ts
1022
- interface TabSlotResult {
1023
- tabs: Sketch;
1024
- slots: Sketch;
1025
- }
1026
- ```
1027
-
1028
- </details>
334
+ // 8 holes evenly spaced around origin
335
+ circularPattern(cylinder(12, 4).translate(30, 0, -1), 8)
1029
336
 
1030
- #### `livingHingeProfile()`
1031
-
1032
- ```ts
1033
- livingHingeProfile(length: number, width: number, options?: LivingHingeOptions): Sketch
337
+ // Circular pattern around X axis
338
+ circularPattern(myFeature, 4, { axis: [1, 0, 0], origin: [0, 0, 50] })
1034
339
  ```
1035
340
 
1036
- Generate a living hinge slit pattern to subtract from a panel. The pattern fills a region [0, length] x [0, width] with staggered slits that allow the material to flex.
341
+ `circularPattern(shape: Shape, count: number, centerXOrOpts?: number | CircularPatternOptions, centerY?: number): Shape`
1037
342
 
1038
- <details><summary><code>LivingHingeOptions</code></summary>
343
+ **`CircularPatternOptions`**
344
+ - `centerX?: number` — Center X of the rotation (default: 0). Used when axis is Z (legacy mode).
345
+ - `centerY?: number` — Center Y of the rotation (default: 0). Used when axis is Z (legacy mode).
1039
346
 
1040
- ```ts
1041
- interface LivingHingeOptions {
1042
- /** Slit pattern style. Default: 'straight'. */
1043
- pattern?: "straight" | "serpentine";
1044
- /** Explicit slit width (beyond kerf). Default: 0. */
1045
- slitWidth?: number;
1046
- /** Distance between slit rows. Default: 2 * thickness. */
1047
- rowSpacing?: number;
1048
- /** Length of each slit. Default: 0.7 * (length - 2 * landWidth). */
1049
- slitLength?: number;
1050
- /** Uncut material between slit ends and row edges. Default: 2 * thickness. */
1051
- landWidth?: number;
1052
- /** Target bend radius - auto-computes row spacing. Overrides rowSpacing. */
1053
- bendRadius?: number;
1054
- /** Material thickness (needed for bend radius calc). Default: 3. */
1055
- thickness?: number;
1056
- }
1057
- ```
347
+ #### `linearPattern2d()` — Repeat a 2D sketch in a linear pattern and union the copies.
1058
348
 
1059
- </details>
349
+ `linearPattern2d(sketch: Sketch, count: number, dx: number, dy?: number): Sketch`
1060
350
 
1061
- #### `snapFitProfile()`
351
+ #### `circularPattern2d()` — Repeat a 2D sketch in a circular pattern around a center point and union the copies.
1062
352
 
1063
- ```ts
1064
- snapFitProfile(thickness: number, options?: SnapFitOptions): SnapFitResult
1065
- ```
353
+ `circularPattern2d(sketch: Sketch, count: number, centerXOrOpts?: number | { centerX?: number; centerY?: number; startDeg?: number; }, centerY?: number): Sketch`
1066
354
 
1067
- Generate a cantilever snap-fit tab and matching slot. The tab beam extends in +Y from y=0, with a barb/arrow tip at the top. The slot is the cutout for the mating panel.
355
+ #### `mirrorCopy()` Mirror a shape across a plane and union the mirror with the original.
1068
356
 
1069
- <details><summary><code>SnapFitOptions</code></summary>
357
+ **Details**
1070
358
 
1071
- ```ts
1072
- interface SnapFitOptions {
1073
- /** Tab beam length. Default: 4 * thickness. */
1074
- tabLength?: number;
1075
- /** Tab beam width. Default: thickness. */
1076
- tabWidth?: number;
1077
- /** How much the barb protrudes beyond the beam. Default: 0.3 * thickness. */
1078
- overhang?: number;
1079
- /** Slot clearance per side. Default: 0.1. */
1080
- clearance?: number;
1081
- /** Laser kerf. Default: 0. */
1082
- kerf?: number;
1083
- /** Barb style. Default: 'barb'. */
1084
- style?: "arrow" | "barb";
1085
- }
1086
- ```
1087
-
1088
- </details>
1089
-
1090
- <details><summary><code>SnapFitResult</code></summary>
1091
-
1092
- ```ts
1093
- interface SnapFitResult {
1094
- tab: Sketch;
1095
- slot: Sketch;
1096
- }
1097
- ```
1098
-
1099
- </details>
359
+ 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.
1100
360
 
1101
- #### `kerfCompensateOutline()`
361
+ **Example**
1102
362
 
1103
363
  ```ts
1104
- kerfCompensateOutline(sketch: Sketch, kerf: number): Sketch
364
+ // Mirror across the YZ plane (X=0)
365
+ mirrorCopy(box(50, 30, 10), [1, 0, 0])
1105
366
  ```
1106
367
 
1107
- Apply kerf compensation to a complete part outline (outer boundary + holes). Offsets inward by half-kerf: the outer boundary shrinks and inner holes grow. This is correct because the laser beam removes material on both sides of the cut line.
368
+ `mirrorCopy(shape: Shape, normal: [ number, number, number ]): Shape`
1108
369
 
1109
- #### `kerfCompensateTabs()`
1110
-
1111
- ```ts
1112
- kerfCompensateTabs(sketch: Sketch, kerf: number): Sketch
1113
- ```
1114
-
1115
- Apply kerf compensation to joint protrusions (tabs, fingers). These grow by half-kerf so they are slightly oversized and fit tightly in their mating slots after the laser removes material.
1116
-
1117
- #### `kerfCompensateSlots()`
370
+ ### Imports & Composition
1118
371
 
1119
- ```ts
1120
- kerfCompensateSlots(sketch: Sketch, kerf: number): Sketch
1121
- ```
372
+ #### `require()` — Import a module with optional ForgeCAD parameter overrides. Returns the module's exports.
1122
373
 
1123
- Apply kerf compensation to joint cutouts (slots, holes that receive tabs). These grow by half-kerf so tabs can fit into them after the laser removes material from both sides of the slot walls.
374
+ `require$1(path: string, paramOverrides?: Record<string, number>): any`
1124
375
 
1125
- #### `kerfCompensatePart()`
376
+ #### `importSvgSketch()` — Parse an SVG file and return it as a Sketch with options for region filtering, scaling, and simplification.
1126
377
 
1127
- ```ts
1128
- kerfCompensatePart(baseProfile: Sketch, joints: PartJoints, kerf: number): Sketch
1129
- ```
378
+ `importSvgSketch(fileName: string, options?: SvgImportOptions): Sketch`
1130
379
 
1131
- Build a kerf-compensated part profile. 1. Start with the base profile. 2. Kerf-compensate each tab addition (grow by kerf/2), then union with base. 3. Kerf-compensate each slot subtraction (grow by kerf/2), then subtract from base. 4. Kerf-compensate the resulting outline (shrink by kerf/2). Order matters: joints modify geometry BEFORE outline compensation so the final inward offset applies uniformly to the assembled profile.
380
+ **`SvgImportOptions`**
1132
381
 
1133
- <details><summary><code>PartJoints</code></summary>
382
+ | Option | Type | Description |
383
+ |--------|------|-------------|
384
+ | `include?` | `"auto" | "fill" | "stroke" | "fill-and-stroke"` | Which geometry channels to include: - `auto`: prefer fills; if no fill geometry exists, fall back to strokes - `fill`: import only filled regions - `stroke`: import only stroke geometry - `fill-and-stroke`: include both |
385
+ | `regionSelection?` | `"all" | "largest"` | Keep all disconnected regions, or only the largest. |
386
+ | `maxRegions?` | `number` | Keep at most this many regions (largest-first). |
387
+ | `minRegionArea?` | `number` | Drop regions below this absolute area threshold. |
388
+ | `minRegionAreaRatio?` | `number` | Drop regions below this ratio of largest-region area. |
389
+ | `flattenTolerance?` | `number` | Curve flattening tolerance in SVG user units. Smaller = more segments, higher fidelity. |
390
+ | `arcSegments?` | `number` | Minimum segment count for arc discretization. |
391
+ | `scale?` | `number` | Global scale applied after SVG parsing. |
392
+ | `maxWidth?` | `number` | Maximum imported sketch width. If exceeded, geometry is uniformly downscaled to fit. |
393
+ | `maxHeight?` | `number` | Maximum imported sketch height. If exceeded, geometry is uniformly downscaled to fit. |
394
+ | `centerOnOrigin?` | `boolean` | Recenter imported geometry so its 2D bounds center is at CAD origin. |
395
+ | `simplify?` | `number` | Simplification tolerance for final sketch cleanup. |
396
+ | `invertY?` | `boolean` | Flip SVG Y-down coordinates to CAD Y-up. Enabled by default. |
1134
397
 
1135
- ```ts
1136
- interface PartJoints {
1137
- /** Geometry to ADD to the base profile (tabs, fingers protruding from edges). */
1138
- additions?: Sketch[];
1139
- /** Geometry to SUBTRACT from the base profile (slots, holes for mating tabs). */
1140
- subtractions?: Sketch[];
1141
- }
1142
- ```
398
+ #### `importMesh()` — Import an external mesh file (STL, OBJ, 3MF) as a Shape.
1143
399
 
1144
- </details>
400
+ `importMesh(fileName: string, options?: { scale?: number; center?: boolean; }): Shape`
1145
401
 
1146
- #### `lookupKerf()`
402
+ ### Parameters
1147
403
 
1148
- ```ts
1149
- lookupKerf(material: string, thickness: number, laserType?: string): number | undefined
1150
- ```
404
+ #### `param()` — Declare a numeric parameter that renders as a slider in the UI.
1151
405
 
1152
- Look up kerf for a material + thickness + laser combo. If `laserType` is omitted, returns the first matching material + thickness entry. Returns `undefined` when no match is found.
406
+ **Details**
1153
407
 
1154
- #### `flatPanel()`
408
+ Each `param()` call registers a slider control. When the user moves the slider the entire script re-executes with the new value. Parameter values are also overridable from `require()` imports or the CLI `--param` flag — the `name` string is the key used in both cases.
1155
409
 
1156
- ```ts
1157
- flatPanel(name: string, width: number, height: number, thickness: number, options?: FlatPartOptions): FlatPart
1158
- ```
410
+ Default range rules when options are omitted: - `min` defaults to `0` - `max` defaults to `defaultValue * 4` - `step` is auto-calculated: `1` for integer params, `0.1` for ranges ≤ 100, `1` for larger ranges
1159
411
 
1160
- Create a rectangular flat panel with 4 named edges. Profile origin at bottom-left corner. Edges: bottom (y=0), right (x=width), top (y=height), left (x=0). Edge traversal follows CCW winding order.
412
+ 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`).
1161
413
 
1162
- <details><summary><code>FlatPartOptions</code></summary>
414
+ **Example**
1163
415
 
1164
416
  ```ts
1165
- interface FlatPartOptions {
1166
- material?: string;
1167
- qty?: number;
1168
- color?: string;
1169
- }
417
+ const width = param("Width", 50);
418
+ const angle = param("Angle", 45, { min: 0, max: 180, unit: "°" });
419
+ const sides = param("Sides", 6, { min: 3, max: 12, integer: true });
1170
420
  ```
1171
421
 
1172
- </details>
1173
-
1174
- #### `flatPart()`
422
+ **Parameter overrides** — key must match `name` exactly:
1175
423
 
1176
424
  ```ts
1177
- flatPart(name: string, profile: Sketch, thickness: number, edges?: Record<string, { start: [ number, number ]; end: [ number, number ]; }>, options?: FlatPartOptions): FlatPart
1178
- ```
1179
-
1180
- Create a flat part from an arbitrary profile with user-named edges. Edge normals are computed automatically (perpendicular to direction, rotated 90deg CW).
425
+ // Via require()
426
+ const bracket = require("./bracket.forge.js", { Width: 80 });
1181
427
 
1182
- #### `fingerJoint()`
1183
-
1184
- ```ts
1185
- fingerJoint(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: FingerJointOptions & { foldAngle?: number; }): void
428
+ // Via CLI
429
+ // forgecad run model.forge.js --param "Wall Thickness=3"
1186
430
  ```
1187
431
 
1188
- Connect two parts with finger joints along specified edges. Adds finger geometry to partA's edge, cuts matching slots from partB's edge. The joint profiles are positioned along each edge using rotation + translation.
432
+ `param(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number`
1189
433
 
1190
- #### `tabSlot()`
434
+ #### `boolParam()` — Declare a boolean parameter that renders as a checkbox in the UI.
1191
435
 
1192
- ```ts
1193
- tabSlot(partA: FlatPart, edgeNameA: string, partB: FlatPart, edgeNameB: string, options?: TabSlotOptions & { foldAngle?: number; }): void
1194
- ```
436
+ **Details**
1195
437
 
1196
- Connect two parts with tab-and-slot joints along specified edges. Adds tab geometry to partA's edge, cuts matching slots from partB's edge.
438
+ 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.
1197
439
 
1198
- #### `assemblyPreview()`
440
+ **Example**
1199
441
 
1200
442
  ```ts
1201
- assemblyPreview(parts: FlatPart[], joints: JointRecord[], options?: AssemblyPreviewOptions): AssemblyPreviewResult
443
+ const showHoles = boolParam("Show Holes", true);
444
+ if (showHoles) return difference(plate, cylinder(10, 5).translate(50, 30, 0));
445
+ return plate;
1202
446
  ```
1203
447
 
1204
- <details><summary><code>JointRecord</code></summary>
448
+ Override via import:
1205
449
 
1206
450
  ```ts
1207
- interface JointRecord {
1208
- type: "finger" | "tabSlot" | "snapFit";
1209
- partA: string;
1210
- partB: string;
1211
- edgeA: string;
1212
- edgeB: string;
1213
- /** Fold angle in degrees. Default: 90. */
1214
- foldAngle: number;
1215
- }
451
+ const pan = require("./pan.forge.js", { "Show Lid": 0 });
1216
452
  ```
1217
453
 
1218
- </details>
454
+ `boolParam(name: string, defaultValue: boolean): boolean`
1219
455
 
1220
- <details><summary><code>AssemblyPreviewOptions</code></summary>
456
+ #### `choiceParam()` — Declare a choice parameter that renders as a dropdown in the UI.
1221
457
 
1222
- ```ts
1223
- interface AssemblyPreviewOptions {
1224
- /** Kerf compensation passed to each part's solid(). Default: 0 */
1225
- kerf?: number;
1226
- /** Fold amount: 0 = flat layout, 1 = fully assembled. Default: 1 */
1227
- fold?: number;
1228
- /** Explode distance: 0 = assembled, >0 = parts spread outward. Default: 0 */
1229
- explode?: number;
1230
- }
1231
- ```
458
+ **Details**
1232
459
 
1233
- </details>
460
+ `defaultValue` must exactly match one entry in `choices`. Returns the selected string label. Prefer `choiceParam` over `param` when a slider would hide intent — named choices like `"wok"` are self-describing.
1234
461
 
1235
- <details><summary><code>AssemblyPreviewResult</code></summary>
462
+ Overrides may be passed as the choice label string (preferred) or as a numeric index. The `name` string is the override key.
1236
463
 
1237
- ```ts
1238
- interface AssemblyPreviewResult {
1239
- /** All part shapes grouped for display. */
1240
- shapes: ShapeGroup;
1241
- /** Individual transformed shapes keyed by part name. */
1242
- partShapes: Map<string, Shape>;
1243
- }
1244
- ```
1245
-
1246
- </details>
1247
-
1248
- #### `assemblyInstructions()`
464
+ **Example**
1249
465
 
1250
466
  ```ts
1251
- assemblyInstructions(parts: FlatPart[], joints: JointRecord[], options?: AssemblyInstructionsOptions): AssemblyInstructionsResult
467
+ const panStyle = choiceParam("Pan Style", "frying-pan", ["frying-pan", "saute-pan", "wok"]);
468
+ if (panStyle === "wok") return buildWok();
1252
469
  ```
1253
470
 
1254
- Generate step-by-step assembly instructions from flat parts and joints. Algorithm: 1. Build adjacency graph from joints 2. Pick root part (most connections, or user-specified) 3. BFS from root, creating one step per part addition 4. Each step describes: which part to add, where it connects, how to orient it Heuristics for step ordering: - Start with the part that has the most connections (the base) - Add parts that connect to already-assembled parts first (BFS order) - Among candidates at the same BFS depth, prefer parts with more connections to already-assembled parts (structurally stable)
1255
-
1256
- <details><summary><code>AssemblyInstructionsOptions</code></summary>
471
+ Override via import:
1257
472
 
1258
473
  ```ts
1259
- interface AssemblyInstructionsOptions {
1260
- /** Part to start from. Default: part with most joint connections. */
1261
- rootPart?: string;
1262
- }
474
+ const pan = require("./pan.forge.js", { "Pan Style": "wok" });
1263
475
  ```
1264
476
 
1265
- </details>
477
+ Override via CLI:
1266
478
 
1267
- <details><summary><code>AssemblyInstructionsResult</code></summary>
1268
-
1269
- ```ts
1270
- interface AssemblyInstructionsResult {
1271
- steps: AssemblyStep[];
1272
- /** Total number of parts in the assembly. */
1273
- totalParts: number;
1274
- /** Parts not connected to the joint graph (orphans). */
1275
- orphanParts: string[];
1276
- }
479
+ ```bash
480
+ forgecad run model.forge.js --param "Pan Style=wok"
1277
481
  ```
1278
482
 
1279
- </details>
483
+ `choiceParam(name: string, defaultValue: string, choices: string[]): string`
1280
484
 
1281
- <details><summary><code>AssemblyStep</code></summary>
485
+ #### `listParam()` — Declare a list parameter — an array of struct items with per-field UI controls.
1282
486
 
1283
- ```ts
1284
- interface AssemblyStep {
1285
- /** 1-based step number. */
1286
- stepNumber: number;
1287
- /** Human-readable instruction. */
1288
- description: string;
1289
- /** The part being added in this step. */
1290
- partName: string;
1291
- /** Part number (for cross-ref with cut sheets). */
1292
- partNumber: number;
1293
- /** Which existing part it connects to. */
1294
- connectsTo: string;
1295
- /** Joint type used. */
1296
- jointType: "finger" | "tabSlot" | "snapFit";
1297
- /** The edge on the new part. */
1298
- newPartEdge: string;
1299
- /** The edge on the existing part. */
1300
- existingPartEdge: string;
1301
- /** Fold angle in degrees. */
1302
- foldAngle: number;
1303
- /** Part names in the assembly so far (after this step). */
1304
- assembledParts: string[];
1305
- }
1306
- ```
487
+ **Details**
1307
488
 
1308
- </details>
489
+ 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.
1309
490
 
1310
- #### `formatInstructions()`
491
+ Field types: - Boolean fields (`boolean: true` in field defs) return as `boolean` - Choice fields (`choices: [...]` in field defs) return as `string` - All other fields return as `number`
1311
492
 
1312
- ```ts
1313
- formatInstructions(result: AssemblyInstructionsResult): string
1314
- ```
493
+ `listParam<T extends Record<string, number | boolean | string>>(name: string, defaultItems: T[], opts: { ... }): T[]`
1315
494
 
1316
- Format assembly instructions as a human-readable text document. Includes a "Step 0" preamble identifying the base part, followed by numbered steps, and a note about any orphan parts.
495
+ `ListParamFieldDef`: `{ min?: number, max?: number, step?: number, unit?: string, integer?: boolean, boolean?: boolean, choices?: string[] }`
1317
496
 
1318
- #### `laserKit()`
497
+ ### Grouping & Local Coordinates
1319
498
 
1320
- ```ts
1321
- laserKit(options?: LaserKitOptions): LaserKit
1322
- ```
499
+ #### `group()` — Group multiple shapes/sketches for joint transforms without merging into a single mesh.
1323
500
 
1324
- Top-level factory for creating a LaserKit container.
501
+ 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.).
1325
502
 
1326
- <details><summary><code>LaserKitOptions</code></summary>
503
+ **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.
1327
504
 
1328
- ```ts
1329
- interface LaserKitOptions {
1330
- /** Default material label for parts that don't specify one. */
1331
- material?: string;
1332
- /** Stock sheet width in mm (default 600). */
1333
- sheetWidth?: number;
1334
- /** Stock sheet height in mm (default 400). */
1335
- sheetHeight?: number;
1336
- /** Laser kerf in mm (default 0.2). */
1337
- kerf?: number;
1338
- }
1339
- ```
505
+ // BAD — every sub-part repeats the parent's global offset const unitX = 0, unitY = -18, unitZ = 70; const body = roundedBox(100, 20, 32, 4).translate(unitX, unitY, unitZ); const panel = box(98, 2, 18).translate(unitX, unitY - 12, unitZ + 4); const louver = box(88, 2, 6).translate(unitX, unitY - 14, unitZ - 11);
1340
506
 
1341
- </details>
507
+ // 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);
1342
508
 
1343
- #### `torus()`
509
+ `group(...items: GroupInput[]): ShapeGroup`
1344
510
 
1345
- ```ts
1346
- torus$1(majorRadius: number, minorRadius: number, segments?: number): Shape
1347
- ```
511
+ ### Section & Projection
1348
512
 
1349
- Create a torus (donut shape) centered at the origin, lying in the XY plane. @concept primitive
513
+ #### `intersectWithPlane()` — Cross-section: slice a 3D shape with a plane and return the intersection as a 2D Sketch.
1350
514
 
1351
- #### `importMesh()`
515
+ `intersectWithPlane(shape: Shape, plane: PlaneSpec): Sketch`
1352
516
 
1353
- ```ts
1354
- importMesh(fileName: string, options?: { scale?: number; center?: boolean; }): Shape
1355
- ```
517
+ #### `faceProfile()` — Extract the boundary profile of a named face as a 2D sketch.
1356
518
 
1357
- Import an external mesh file (STL, OBJ, 3MF) as a Shape. @concept import
519
+ 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.
1358
520
 
1359
- #### `highlight()`
521
+ `faceProfile(shape: Shape, face: FaceSelector): Sketch`
1360
522
 
1361
- ```ts
1362
- highlight(entityId: string, opts?: HighlightOptions): void
1363
- ```
523
+ #### `projectToPlane()` — Orthographically project a 3D shape onto a plane and return the silhouette as a 2D Sketch.
1364
524
 
1365
- Highlight any geometry for visual debugging in the viewport. Supported inputs: - `string` — sketch entity ID (e.g. `'L0'`, `'P0'`, `'C0'`) - `[x, y, z]` — 3D point - `[[x1,y1,z1], [x2,y2,z2]]` — edge (line segment) - `{ normal: [x,y,z], offset: number }` — plane by normal + distance from origin - `{ normal: [x,y,z], point: [x,y,z] }` — plane by normal + point on plane - `Shape` — highlight entire 3D shape - `FaceRef` (from `shape.face('top')`) — highlight as plane at face center - `EdgeRef` (from `shape.edge('left')`) — highlight as edge segment
525
+ `projectToPlane(shape: Shape, plane: PlaneSpec): Sketch`
1366
526
 
1367
- <details><summary><code>HighlightOptions</code></summary>
527
+ ### Transforms
1368
528
 
1369
- ```ts
1370
- interface HighlightOptions {
1371
- color?: string;
1372
- label?: string;
1373
- pulse?: boolean;
1374
- /** Size hint for points (radius in mm) or planes (disc radius in mm). */
1375
- size?: number;
1376
- }
1377
- ```
529
+ #### `composeChain()` — Compose transforms in chain order. Equivalent to Transform.identity().mul(a).mul(b).mul(c)...
1378
530
 
1379
- </details>
1380
-
1381
- #### `highlight()`
1382
-
1383
- ```ts
1384
- highlight(point: [ number, number, number ], opts?: HighlightOptions): void
1385
- ```
1386
-
1387
- #### `highlight()`
1388
-
1389
- ```ts
1390
- highlight(edge: [ [ number, number, number ], [ number, number, number ] ], opts?: HighlightOptions): void
1391
- ```
531
+ `composeChain(...steps: TransformInput[]): Transform`
1392
532
 
1393
- #### `highlight()`
533
+ ### Verification
1394
534
 
1395
- ```ts
1396
- highlight(plane: { normal: [ number, number, number ]; offset: number; }, opts?: HighlightOptions): void
1397
- ```
535
+ #### `spec()` — Create a named, reusable bundle of verification checks.
1398
536
 
1399
- #### `highlight()`
537
+ **Details**
1400
538
 
1401
- ```ts
1402
- highlight(plane: { normal: [ number, number, number ]; point: [ number, number, number ]; }, opts?: HighlightOptions): void
1403
- ```
539
+ 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.
1404
540
 
1405
- #### `highlight()`
541
+ Specs can be defined in separate `.forge.js` files and imported via `require()` to share them across models.
1406
542
 
1407
- ```ts
1408
- highlight(shape: Shape, opts?: HighlightOptions): void
1409
- ```
543
+ `spec.check()` returns a `SpecResult` — you can inspect it programmatically or ignore the return value and let the Checks panel show results.
1410
544
 
1411
- #### `highlight()`
545
+ **Example**
1412
546
 
1413
547
  ```ts
1414
- highlight(face: FaceRef, opts?: HighlightOptions): void
1415
- ```
548
+ const printable = spec("Fits printer bed", (shape) => {
549
+ verify.notEmpty("Has geometry", shape);
550
+ const bb = shape.boundingBox();
551
+ verify.lessThan("Width < 220mm", bb.max[0] - bb.min[0], 220);
552
+ verify.lessThan("Depth < 220mm", bb.max[1] - bb.min[1], 220);
553
+ verify.lessThan("Height < 250mm", bb.max[2] - bb.min[2], 250);
554
+ });
1416
555
 
1417
- <details><summary><code>FaceRef</code></summary>
556
+ // Reuse on multiple shapes
557
+ printable.check(bracket);
558
+ printable.check(standoff);
1418
559
 
1419
- ```ts
1420
- interface FaceRef {
1421
- name: FaceName;
1422
- /** Compiler-owned face query when available. */
1423
- query?: FaceQueryRef;
1424
- /** True when the face can host a 2D sketch placement frame */
1425
- planar?: boolean;
1426
- /** Shared descendant-resolution metadata when this face is a semantic region/set. */
1427
- descendant?: FaceDescendantMetadata;
1428
- }
560
+ // Check relationships between parts
561
+ const fitSpec = spec("Assembly fit", (partA, partB) => {
562
+ verify.notColliding("No interference", partA, partB, 10);
563
+ });
564
+ fitSpec.check(bracket, standoff);
1429
565
  ```
1430
566
 
1431
- </details>
1432
-
1433
- <details><summary><code>FaceDescendantMetadata</code></summary>
1434
-
1435
- ```ts
1436
- interface FaceDescendantMetadata {
1437
- kind: "single" | "face-set";
1438
- semantic: FaceDescendantSemantic;
1439
- memberCount: number;
1440
- memberNames: string[];
1441
- coplanar: boolean;
1442
- }
1443
- ```
567
+ **Spec-first workflow:** Write specs before building geometry. Checks go from red to green as you build — effectively TDD for CAD.
1444
568
 
1445
- </details>
569
+ `spec(name: string, checkFn: (...args: any[]) => void): Spec`
1446
570
 
1447
- #### `highlight()`
1448
-
1449
- ```ts
1450
- highlight(edge: EdgeRef, opts?: HighlightOptions): void
1451
- ```
1452
-
1453
- <details><summary><code>EdgeRef</code></summary>
1454
-
1455
- ```ts
1456
- interface EdgeRef {
1457
- name: EdgeName;
1458
- /** Compiler-owned edge query when available. */
1459
- query?: EdgeQueryRef;
1460
- }
1461
- ```
1462
-
1463
- </details>
571
+ **`Spec`**
572
+ - `name: string` — The display name of this spec
1464
573
 
1465
574
  ---
1466
575
 
@@ -1468,7 +577,9 @@ interface EdgeRef {
1468
577
 
1469
578
  ### `Shape`
1470
579
 
1471
- Core 3D solid shape. All operations are immutable and return new shapes. Supports transforms (translate, rotate, scale, mirror, transform, rotateAround, pointAlong), booleans (add, subtract, intersect), cutting (split, splitByPlane, trimByPlane), shelling, anchor positioning (attachTo, onFace), placement references, and queries (volume, surfaceArea, boundingBox, isEmpty, numTri, geometryInfo).
580
+ Core 3D solid shape. All operations are immutable and return new shapes.
581
+
582
+ Supports transforms (translate, rotate, scale, mirror, transform, rotateAround, pointAlong), booleans (add, subtract, intersect), cutting (split, splitByPlane, trimByPlane), shelling, anchor positioning (attachTo, onFace), placement references, and queries (volume, surfaceArea, boundingBox, isEmpty, numTri, geometryInfo).
1472
583
 
1473
584
  **Properties:**
1474
585
 
@@ -1478,87 +589,90 @@ Core 3D solid shape. All operations are immutable and return new shapes. Support
1478
589
 
1479
590
  **Methods:**
1480
591
 
1481
- - `setColor()` — Set the color of this shape (hex string, e.g. "#ff0000")
1482
- - `color()` — Alias for setColor
1483
- - `material()` — Set material properties for this shape's visual appearance. Returns a new Shape with the specified material properties merged. ```js box(50, 50, 50).material({ metalness: 0.9, roughness: 0.1 }); sphere(30).material({ emissive: '#ff6b35', emissiveIntensity: 2 }); cylinder(40, 20).material({ opacity: 0.3 }); ```
1484
- - `clone()` — Return a new Shape wrapper for explicit duplication in scripts.
1485
- - `duplicate()` — Alias for clone()
1486
- - `geometryInfo()` — Inspect which backend/representation produced this solid.
1487
- - `withReferences()` — Attach named placement references that survive normal transforms and imports.
1488
- - `referenceNames()` — List named placement references carried by this shape.
1489
- - `withPorts()` — Attach named assembly ports (origin + axis + up) that survive transforms and imports.
1490
- - `portNames()` — List named port identifiers carried by this shape.
1491
- - `referencePoint()` — Resolve a named placement reference or built-in anchor to a 3D point.
1492
- - `face()` — Resolve a semantic face by name or query. Works on compile-covered shapes and, as a fallback, on any planar-faced mesh (e.g. the result of boolean ops) via coplanar triangle clustering.
1493
- - `faces()` — Return all faces matching a query, or all mesh-detected faces when no query is given.
1494
- - `faceNames()` — List defended semantic face names currently available on this shape.
1495
- - `edge()` — Get a named topology edge. Only available on shapes with tracked topology (from box/cylinder/extrude).
1496
- - `edgeNames()` — List named topology edge names. Returns empty array if shape has no tracked topology.
1497
- - `faceHistory()` — Get the transformation history for a specific face.
1498
- - `placeReference()` — Translate the shape so the given reference lands on the target coordinate.
1499
- - `translatePolar()` — Translate using polar coordinates (radius + angle in degrees). Eliminates manual `r * Math.cos(angle * PI/180)` calculations. Example: `shape.translatePolar(50, 30)` moves 50mm at 30 degrees from +X.
1500
- - `translate()` — Move the shape relative to its current position. All transforms are immutable and return new shapes.
1501
- - `moveTo()` — Position the shape so its bounding box min corner is at the given global coordinate.
1502
- - `moveToLocal()` — Position the shape relative to another shape's local coordinate system (bounding box min corner).
1503
- - `rotate()` — Rotate using Euler angles in degrees around the shape's bounding box center.
1504
- - `rotateAround()` — Rotate using Euler angles in degrees around an explicit pivot point.
1505
- - `transform()` — Apply a 4x4 affine transform matrix (column-major) or a Transform object.
1506
- - `scale()` Scale the shape uniformly or per-axis from the shape's bounding box center. Accepts a single number or [x, y, z] array.
1507
- - `scaleAround()` — Scale the shape uniformly or per-axis from an explicit pivot point.
1508
- - `mirror()` Mirror across a plane through the shape's bounding box center, defined by its normal vector.
1509
- - `mirrorThrough()` — Mirror across a plane through an explicit point, defined by its normal vector.
1510
- - `pointAlong()` Reorient a shape so its primary axis (Z) points along the given direction. Useful for laying cylinders/extrusions along X or Y without thinking about Euler angles. Example: cylinder(40, 5).pointAlong([1, 0, 0]) — lays cylinder along X
1511
- - `rotateAroundAxis()` Rotate around an arbitrary axis through a pivot point. Equivalent to: translate(-pivot) rotate around axis translate(+pivot)
1512
- - `rotateAroundTo()` Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point. `movingPoint` / `targetPoint` may be raw world points or this shape's anchors/references.
1513
- - `add()` — Union this shape with others (additive boolean). Method form of union().
1514
- - `subtract()` — Subtract other shapes from this one. Method form of difference().
1515
- - `intersect()` — Keep only the overlap with other shapes. Method form of intersection().
1516
- - `union()` — Alias for add() matches the free-function union() naming.
1517
- - `difference()` — Alias for subtract() matches the free-function difference() naming.
1518
- - `intersection()` — Alias for intersect() matches the free-function intersection() naming.
1519
- - `split()` — Split into [inside, outside] by another shape.
1520
- - `splitByPlane()` — Split by infinite plane. Returns [positive-side, negative-side].
1521
- - `trimByPlane()` — Keep the positive side of the plane and discard the opposite side.
1522
- - `shell()` Hollow out compile-covered boxes, cylinders, and straight extrudes. `openFaces` names any subset of the base shape's faces to leave open (no wall). Box bases accept any of: top, bottom, front (=side-bottom), back (=side-top), left (=side-left), right (=side-right), or the raw internal names. Cylinder and extrude bases accept top and bottom only.
1523
- - `boundingBox()` Get the axis-aligned bounding box as { min: [x,y,z], max: [x,y,z] }.
1524
- - `volume()` — Volume in mm cubed.
1525
- - `surfaceArea()` — Surface area in mm squared.
1526
- - `isEmpty()` — True if the shape contains no geometry.
1527
- - `numBodies()` — Number of disconnected solid bodies in this shape.
1528
- - `numTri()` — Triangle count of the mesh representation.
1529
- - `getMesh()` — Extract triangle mesh for Three.js rendering
1530
- - `slice()` — Slice the runtime solid by a plane normal to local Z at the given offset.
1531
- - `project()` — Orthographically project the runtime solid onto the local XY plane.
1532
- - `attachTo()` — Position this shape relative to another using named 3D anchor points. Anchors are bounding-box-relative: 'center', face centers ('top', 'front', ...), edge midpoints ('top-front', 'back-left', ...), and corners ('top-front-left', ...). Anchor word order is flexible: 'front-left' and 'left-front' are equivalent. Named placement references (from withReferences) can also be used as anchors.
1533
- - `onFace()` — Place this shape on a face of a parent shape. Think of it like sticking a label on a box surface: - `face` picks which surface ('front', 'back', 'top', etc.) - `u, v` position within that face's 2D plane (from center) - front/back: u = left/right (X), v = up/down (Z) - left/right: u = forward/back (Y), v = up/down (Z) - top/bottom: u = left/right (X), v = forward/back (Y) - `protrude` = how far the child sticks out (positive = outward from face)
1534
- - `withConnectors()` — Attach named connectors (typed, gendered ports) that survive transforms.
1535
- - `connectorNames()` — List connector names (ports that have a connectorType).
1536
- - `connectorsByType()` — Get all connectors of a given type.
1537
- - `connectorDistance()` — Distance between two connector origins on this shape.
1538
- - `connectorMeasurements()` — Get measurements metadata from a connector.
1539
- - `matchTo()` — Position this shape by matching connectors to a target. Overloads: - Single pair: `matchTo(target, selfConn, targetConn, options?)` - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)` - Multi-target: `matchTo([ [target1, selfConn1, targetConn1], ... ], options?)`
1540
- - `pocket()` — Cut a pocket (cavity) into this solid through the named face. box(100, 100, 20).pocket('top', 8) box(100, 100, 20).pocket('top', 8, { inset: 5 }) box(100, 100, 20).pocket('top', 8, { scale: 0.8 })
1541
- - `boss()` — Add a boss (protrusion) from the named face. box(100, 100, 20).boss('top', 5) box(100, 100, 20).boss('top', 10, { scale: 0.6 })
1542
- - `hole()` — Drill a hole into this solid at a face. box(50, 50, 20).hole('top', { diameter: 8, depth: 10 }) box(50, 50, 20).hole('top', { diameter: 6, counterbore: { diameter: 12, depth: 3 } })
1543
- - `cutout()` Cut a profile-shaped pocket through a face using a placed sketch. The sketch must be placed on a face with `Sketch.onFace(...)`. The cut follows the sketch's 2D profile. const profile = circle2d(10).onFace(body, 'top'); body.cutout(profile, { depth: 5 })
592
+ - `color(value: string | undefined): Shape` — Set the color of this shape (hex string, e.g. "#ff0000"). Returns a new Shape with the color applied.
593
+ - `material(props: ShapeMaterialProps): Shape` — Set PBR material properties for this shape's visual appearance. **Details** Returns a new Shape with the specified material properties merged on top of any previously set properties. All properties are optional — omitted keys retain their current value. Material properties survive transforms and boolean operations. Use `.color()` to set the base diffuse color; `.material()` controls how that color behaves under light (metalness, roughness, clearcoat) and can add emissive glow independent of lighting. Emissive glow pairs naturally with the `postProcessing.bloom` effect in `scene()`. **Example** ```js box(50, 50, 50).material({ metalness: 0.9, roughness: 0.1 }); // polished metal sphere(30).material({ emissive: '#ff6b35', emissiveIntensity: 2 }); // glowing cylinder(40, 20).material({ opacity: 0.4, clearcoat: 1.0, clearcoatRoughness: 0.02 }); // ice // Chainable with other shape methods box(100, 100, 10).color('#gold').material({ metalness: 0.95, roughness: 0.05 }).translate(0, 0, 50); ```
594
+ - `clone(): Shape` — Return a new Shape wrapper for explicit duplication in scripts.
595
+ - `geometryInfo(): GeometryInfo` — Inspect which backend/representation produced this solid.
596
+ - `withReferences(refs: PlacementReferenceInput): Shape` — Attach named placement references that survive normal transforms and imports.
597
+ - `referenceNames(kind?: PlacementReferenceKind): string[]` — List named placement references carried by this shape.
598
+ - `withPorts(ports: Record<string, PortInput>): Shape` — Deprecated alias for `withConnectors()`.
599
+ - `portNames(): string[]` — Deprecated alias for `connectorNames()`.
600
+ - `referencePoint(ref: PlacementAnchorLike): [ number, number, number ]` — Resolve a named placement reference or built-in anchor to a 3D point.
601
+ - `face(selector: FaceSelector): FaceRef` — Resolve a face by user-authored label or compiler-owned name. Returns a `FaceRef` that can be passed to `.onFace()`, `projectToPlane()`, or used directly in placement. **Details** `.face(name)` is a pure label lookup — it finds faces by user-authored labels, not by geometric queries. Labels are born in sketches via `.label()` / `.labelEdges()` and grow into face names through extrude, loft, revolve, and sweep. They are stable references that travel with the geometry. Labels must be unique within a shape. Use `.prefixLabels()` before combining shapes with `union()` / `difference()` to avoid collisions. Collision detection throws a clear error with a fix suggestion. For compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape's compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops) are resolved via coplanar triangle clustering. **Example** ```ts // Edge labels become side face names after extrude const profile = path() .moveTo(0, 0) .lineTo(100, 0).label('floor') .lineTo(100, 50).label('wall') .lineTo(0, 50).label('ceiling') .closeLabel('left-wall'); const room = profile.extrude(30, { labels: { start: 'base', end: 'top' } }); room.face('floor'); // side face from the labeled edge room.face('base'); // base cap (user-specified) // .labelEdges() shorthand for sequential edge labeling const plate = rect(100, 50).labelEdges('south', 'east', 'north', 'west'); const solid = plate.extrude(20, { labels: { start: 'bottom', end: 'top' } }); solid.face('south'); // side face // Prefix before combining to avoid collisions const left = wing.prefixLabels('l/'); const right = wing.mirror([1, 0, 0]).prefixLabels('r/'); const full = union(left, right); full.face('l/upper'); // left wing upper surface ```
602
+ - `faces(query?: FaceQuery): FaceRef[]` — Return all faces matching a query, or all mesh-detected faces when no query is given.
603
+ - `faceNames(): string[]` — List defended semantic face names currently available on this shape, including user labels from `labelFaces()`.
604
+ - `prefixLabels(prefix: string): Shape` — Prefix all user-authored face labels (both sketch-edge labels and labelFaces labels). Returns a new shape with modified labels.
605
+ - `renameLabel(from: string, to: string): Shape` — Rename a single face label. Returns a new shape.
606
+ - `dropLabels(...names: string[]): Shape` — Remove specific face labels. Returns a new shape.
607
+ - `dropAllLabels(): Shape` — Remove all face labels. Returns a new shape.
608
+ - `edge(name: string): EdgeRef` — Get a named topology edge. Only available on shapes with tracked topology (from box/cylinder/extrude).
609
+ - `edgeNames(): string[]` — List named topology edge names. Returns empty array if shape has no tracked topology.
610
+ - `labelFaces(mapping: Record<string, string>): Shape` — Assign user-chosen labels to faces identified by their canonical position keys. **Details** Primitives (`box`, `cylinder`) and extrusions have internal canonical face positions (`top`, `bottom`, `side`, `side-left`, etc.) but these are **not** labels — they are just position selectors. Use `labelFaces()` to give faces meaningful, project-specific names that survive through transforms, booleans, fillets, and chamfers. The mapping keys are canonical position selectors; the values are your labels. Labels are the recommended way to identify faces for topological edge queries (`edgesOf`, `edgesBetween`). **Example** ```js // Full workflow: label → query edges → fillet let plate = box(100, 60, 5).labelFaces({ top: 'work-surface', bottom: 'mount-face' }) plate = fillet(plate, 2, plate.edgesOf('work-surface')) // Cylinder: fillet the rim where cap meets barrel let tube = cylinder(30, 10).labelFaces({ top: 'cap', side: 'barrel' }) tube = fillet(tube, 1, tube.edgesBetween('cap', 'barrel')) // Prefix before combining shapes to avoid label collisions const left = plate.prefixLabels('l/') const right = plate.mirror([1, 0, 0]).prefixLabels('r/') const full = union(left, right) full.edgesOf('l/work-surface') // still works ```
611
+ - `edgesOf(faceLabel: string, options?: EdgesOfOptions): EdgeSegment[]` — Return all boundary edges of a named face. **Details** Finds edges where one adjacent mesh face belongs to the target face and the other belongs to a different face. The result is coalesced (tessellation fragments merged) and can be passed directly to `fillet()` or `chamfer()`. This is a topological query — no coordinates, no tolerances, no minimum-length hacks. It works because an edge is the boundary between two faces. **Example** ```js // Fillet all top edges of a mounting plate let plate = box(120, 80, 6).labelFaces({ top: 'work-surface' }) plate = fillet(plate, 3, plate.edgesOf('work-surface')) // Shelled enclosure — fillet the outer lip let body = box(80, 50, 35).labelFaces({ top: 'opening' }) body = body.shell(2, { openFaces: ['top'] }) body = fillet(body, 1.5, body.edgesOf('opening')) // Filter: only concave edges (after a boolean subtraction) body.edgesOf('top', { concave: true }) ```
612
+ - `edgesBetween(faceA: string, faceB: string | string[]): EdgeSegment[]` — Return edges shared between two named faces. **Details** An edge is "between" faces A and B when one of its adjacent mesh triangles belongs to A and the other belongs to B. This is the most precise topological edge selection — "fillet the edges where the top meets the wall." The second argument can be a single face name or an array (edges between A and any of B1, B2, ...). **Example** ```js // Fillet the edge where lid meets one wall let body = box(100, 60, 30).labelFaces({ top: 'lid', 'side-left': 'wall' }) body = fillet(body, 2, body.edgesBetween('lid', 'wall')) // Fillet a cylinder rim — where the flat cap meets the curved barrel let tube = cylinder(30, 10).labelFaces({ top: 'cap', side: 'barrel' }) tube = fillet(tube, 1, tube.edgesBetween('cap', 'barrel')) // Multiple target faces at once body.edgesBetween('lid', ['left-wall', 'right-wall', 'front-wall', 'back-wall']) ```
613
+ - `faceHistory(name: string): FaceTransformationHistory` — Get the transformation history for a specific face.
614
+ - `placeReference(ref: PlacementAnchorLike, target: [ number, number, number ], offset?: [ number, number, number ]): Shape` — Translate the shape so the given anchor or reference lands on the target coordinate. Accepts any built-in anchor name (`'bottom'`, `'center'`, `'top-front-left'`, etc.) or a custom placement reference attached via `withReferences()`. ```javascript // Ground a shape — put its bottom face center at Z = 0 shape.placeReference('bottom', [0, 0, 0]) // Center at the world origin shape.placeReference('center', [0, 0, 0]) // Align left edge to X = 10 shape.placeReference('left', [10, 0, 0]) ```
615
+ - `translatePolar(radius: number, angleDeg: number, z?: number): Shape` — Translate using polar coordinates (radius + angle in degrees). Eliminates manual `r * Math.cos(angle * PI/180)` calculations. Example: `shape.translatePolar(50, 30)` moves 50mm at 30 degrees from +X.
616
+ - `translate(x: number, y: number, z: number): Shape` — Move the shape relative to its current position. All transforms are immutable and return new shapes.
617
+ - `moveTo(x: number, y: number, z: number): Shape` Position the shape so its bounding box min corner is at the given global coordinate.
618
+ - `moveToLocal(target: Shape | { toShape(): Shape; }, x: number, y: number, z: number): Shape` — Position the shape relative to another shape's local coordinate system (bounding box min corner).
619
+ - `rotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape` — Rotate around an arbitrary axis through the origin.
620
+ - `rotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape` — Rotate around the X axis by the given angle in degrees.
621
+ - `rotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape` Rotate around the Y axis by the given angle in degrees.
622
+ - `rotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Shape` Rotate around the Z axis by the given angle in degrees.
623
+ - `transform(m: Mat4 | Transform): Shape` Apply a 4x4 affine transform matrix (column-major) or a Transform object.
624
+ - `scale(v: number | [ number, number, number ]): Shape` — Scale the shape uniformly or per-axis from the shape's bounding box center. Accepts a single number or [x, y, z] array.
625
+ - `scaleAround(pivot: [ number, number, number ], v: number | [ number, number, number ]): Shape` — Scale the shape uniformly or per-axis from an explicit pivot point.
626
+ - `mirror(normal: [ number, number, number ]): Shape` — Mirror across a plane through the shape's bounding box center, defined by its normal vector.
627
+ - `mirrorThrough(point: [ number, number, number ], normal: [ number, number, number ]): Shape` — Mirror across a plane through an explicit point, defined by its normal vector.
628
+ - `pointAlong(direction: [ number, number, number ]): Shape` — Reorient a shape so its primary axis (Z) points along the given direction. Useful for laying cylinders/extrusions along X or Y without thinking about Euler angles. The shape's origin stays at [0,0,0] — translate after pointAlong to position it. Example: cylinder(40, 5).pointAlong([1, 0, 0]) — lays cylinder along X, starting at origin
629
+ - `rotateAroundTo(axis: [ number, number, number ], pivot: [ number, number, number ], movingPoint: RotationPointLike, targetPoint: RotationPointLike, options?: RotateAroundToOptions): Shape` — Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point. `movingPoint` / `targetPoint` may be raw world points or this shape's anchors/references.
630
+ - `add(...others: ShapeOperandInput[]): Shape` — Union this shape with others (additive boolean). Method form of union().
631
+ - `subtract(...others: ShapeOperandInput[]): Shape` — Subtract other shapes from this one. Method form of difference().
632
+ - `intersect(...others: ShapeOperandInput[]): Shape` — Keep only the overlap with other shapes. Method form of intersection().
633
+ - `split(cutter: Shape | { toShape(): Shape; }): [ Shape, Shape ]` Split into [inside, outside] by another shape.
634
+ - `splitByPlane(normal: [ number, number, number ], originOffset?: number): [ Shape, Shape ]` Split by infinite plane. Returns [positive-side, negative-side].
635
+ - `trimByPlane(normal: [ number, number, number ], originOffset?: number): Shape` — Keep the positive side of the plane and discard the opposite side.
636
+ - `shell(thickness: number, opts?: { openFaces?: string[]; }): Shape` — Hollow out compile-covered boxes, cylinders, and straight extrudes. `openFaces` names any subset of the base shape's labeled faces to leave open (no wall).
637
+ - `boundingBox(): ShapeRuntimeBounds` — Get the axis-aligned bounding box as { min: [x,y,z], max: [x,y,z] }.
638
+ - `volume(): number` — Volume in mm cubed.
639
+ - `surfaceArea(): number` — Surface area in mm squared.
640
+ - `isEmpty(): boolean` — True if the shape contains no geometry.
641
+ - `numBodies(): number` — Number of disconnected solid bodies in this shape.
642
+ - `numTri(): number` — Triangle count of the mesh representation.
643
+ - `getMesh(): ShapeRuntimeMesh` — Extract triangle mesh for Three.js rendering
644
+ - `slice(offset?: number): any` — Slice the runtime solid by a plane normal to local Z at the given offset.
645
+ - `project(): any` — Orthographically project the runtime solid onto the local XY plane.
646
+ - `attachTo(target: ShapeAnchorTarget, targetAnchor: PlacementAnchorLike, selfAnchor?: PlacementAnchorLike, offset?: [ number, number, number ]): Shape` — Position this shape relative to another using named 3D anchor points. Anchors are bounding-box-relative: 'center', face centers ('top', 'front', ...), edge midpoints ('top-front', 'back-left', ...), and corners ('top-front-left', ...). Anchor word order is flexible: 'front-left' and 'left-front' are equivalent. Named placement references (from withReferences) can also be used as anchors.
647
+ - `onFace(parent: ShapeAnchorTarget, face: "front" | "back" | "left" | "right" | "top" | "bottom", opts?: { u?: number; v?: number; protrude?: number; }): Shape` — Place this shape on a face of a parent shape. Think of it like sticking a label on a box surface: - `face` picks which surface ('front', 'back', 'top', etc.) - `u, v` position within that face's 2D plane (from center) - front/back: u = left/right (X), v = up/down (Z) - left/right: u = forward/back (Y), v = up/down (Z) - top/bottom: u = left/right (X), v = forward/back (Y) - `protrude` = how far the child sticks out (positive = outward from face)
648
+ - `seatInto(target: Shape, surface: string, options?: SeatIntoOptions): Shape` — Slide this shape along an axis until a labeled face is embedded in the target body. Position the shape roughly first (translate/rotate), then call seatInto to auto-adjust the penetration depth. No manual coordinate math needed. ```js // Wing root embeds into fuselage — adapts to any fuselage shape wing.translate(0, wingY, 0).seatInto(fuselage, 'root'); // Sensor pod sits flush on fuselage surface pod.translate(0, station, radius + 20).seatInto(fuselage, 'base', { depth: 'flush' }); // Antenna with 3mm gasket standoff mast.translate(0, station, radius + 50).seatInto(fuselage, 'mount', { depth: 'flush', gap: 3 }); ```
649
+ - `seatOver(target: Shape, targetSurface: string, options?: SeatIntoOptions): Shape` — Slide this shape until a target's labeled face is fully covered (inside this shape). The inverse of `seatInto`: instead of embedding *your* face into the target, you move until the *target's* face is embedded inside you. ```js // Nacelle moves up until pylon's bottom face is inside the nacelle nacelle.translate(rough).seatOver(pylon, 'bottom'); // Cap slides down over a post until post's top face is covered cap.translate(rough).seatOver(post, 'top'); ```
650
+ - `withConnectors(connectors: Record<string, ConnectorInput>): Shape` — Attach named connectors attachment points that survive transforms and imports. Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching).
651
+ - `connectorNames(): string[]` — List all connector names on this shape.
652
+ - `connectorsByType(type: string): Array<{ name: string; port: PortDef; }>` Get all connectors of a given type.
653
+ - `connectorDistance(nameA: string, nameB: string): number` Distance between two connector origins on this shape.
654
+ - `connectorMeasurements(name: string): Record<string, number | string>` Get measurements metadata from a connector.
655
+ - `matchTo(targetOrPairs: Shape | MatchTarget | Array<[ Shape | MatchTarget, string, string ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): Shape` — Position this shape by matching connectors to a target. Overloads: - Single pair: `matchTo(target, selfConn, targetConn, options?)` - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)` - Multi-target: `matchTo([ [target1, selfConn1, targetConn1], ... ], options?)`
656
+ - `pocket(face: FaceSelector, depth: number, opts?: PocketOptions): Shape` — Cut a pocket (cavity) into this solid through the named face. box(100, 100, 20).pocket('top', 8) box(100, 100, 20).pocket('top', 8, { inset: 5 }) box(100, 100, 20).pocket('top', 8, { scale: 0.8 })
657
+ - `boss(face: FaceSelector, height: number, opts?: BossOptions): Shape` — Add a boss (protrusion) from the named face. box(100, 100, 20).boss('top', 5) box(100, 100, 20).boss('top', 10, { scale: 0.6 })
658
+ - `hole(faceOrRef: SketchFaceTarget | FaceRef, opts: ShapeHoleOptions): Shape` — Drill a hole into this solid at a face. box(50, 50, 20).hole('top', { diameter: 8, depth: 10 }) box(50, 50, 20).hole('top', { diameter: 6, counterbore: { diameter: 12, depth: 3 } })
659
+ - `cutout(sketch: Sketch, opts?: ShapeCutoutOptions): Shape` — Cut a profile-shaped pocket through a face using a placed sketch. The sketch must be placed on a face with `Sketch.onFace(...)`. The cut follows the sketch's 2D profile. const profile = circle2d(10).onFace(body, 'top'); body.cutout(profile, { depth: 5 })
1544
660
 
1545
661
  ### `Transform`
1546
662
 
1547
- **Methods:**
1548
-
1549
- - `static identity()` — static identity(): Transform
1550
- - `static from()` — static from(input: TransformInput): Transform
1551
- - `static translation()` — static translation(x: number, y: number, z: number): Transform
1552
- - `static scale()` static scale(v: number | Vec3): Transform
1553
- - `static rotationAxis()` — static rotationAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform
1554
- - `static rotateAroundTo()` — static rotateAroundTo(axis: Vec3, pivot: Vec3, movingPoint: Vec3, targetPoint: V
1555
- - `mul()` Compose transforms in chain order. `a.mul(b)` means apply `a`, then `b`.
1556
- - `translate()` — translate(x: number, y: number, z: number): Transform
1557
- - `rotateAxis()` — rotateAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform
1558
- - `inverse()` — inverse(): Transform
1559
- - `point()` — point(p: Vec3): Vec3
1560
- - `vector()` — vector(v: Vec3): Vec3
1561
- - `toArray()` — toArray(): Mat4
663
+ - `static identity(): Transform` — Return the identity transform.
664
+ - `static from(input: TransformInput): Transform` — Wrap an existing `Transform` or raw 4x4 matrix as a `Transform`.
665
+ - `static translation(x: number, y: number, z: number): Transform` — Create a translation transform.
666
+ - `static scale(v: number | Vec3): Transform` — Create a uniform or per-axis scale transform.
667
+ - `static rotationAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform` — Create a rotation around an arbitrary axis, optionally about a pivot.
668
+ - `static rotateAroundTo(axis: Vec3, pivot: Vec3, movingPoint: Vec3, targetPoint: Vec3, options?: RotateAroundToOptions): Transform` — Solve the rotation needed to move one point onto a target line or plane.
669
+ - `mul(other: TransformInput): Transform` — Compose transforms in chain order: `a.mul(b)` applies `a`, then `b`.
670
+ - `translate(x: number, y: number, z: number): Transform` — Translate after the current transform.
671
+ - `rotateAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform` Rotate after the current transform.
672
+ - `inverse(): Transform` — Return the inverse transform.
673
+ - `point(p: Vec3): Vec3` — Transform a point using homogeneous coordinates.
674
+ - `vector(v: Vec3): Vec3` — Transform a direction vector without translation.
675
+ - `toArray(): Mat4` — Return the transform as a raw 4x4 matrix array.
1562
676
 
1563
677
  ### `ShapeGroup`
1564
678
 
@@ -1571,101 +685,40 @@ Core 3D solid shape. All operations are immutable and return new shapes. Support
1571
685
 
1572
686
  **Methods:**
1573
687
 
1574
- - `childName()` — childName(index: number): string | undefined
1575
- - `child()` — Return the named child by name. Throws if not found. Useful when importing a multipart group and working on components individually.
1576
- - `clone()` — Return a deep-cloned ShapeGroup tree (refs copied).
1577
- - `duplicate()` — Alias for clone()
1578
- - `translate()` translate(x: number, y: number, z: number): ShapeGroup
1579
- - `boundingBox()` — boundingBox(): { min: [ number, number, number ]; max: [ number, number, number
1580
- - `moveTo()` — Move so combined bounding box min corner is at the given global coordinate
1581
- - `moveToLocal()` — Move so combined bounding box min corner is at target's bounding box min + (x, y, z) offset
1582
- - `attachTo()` — attachTo(target: Shape | ShapeGroup, targetAnchor: Anchor3D | string, selfAnchor
1583
- - `onFace()` Place this group on a face of a parent shape. See Shape.onFace() for full documentation.
1584
- - `rotate()` Rotate using Euler angles in degrees around the group's bounding box center.
1585
- - `rotateAround()` — Rotate using Euler angles in degrees around an explicit pivot point.
1586
- - `rotateAroundAxis()` — Rotate around an arbitrary axis, optionally through a pivot point.
1587
- - `rotateAroundTo()` Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point. ShapeGroup string points use built-in anchors only.
1588
- - `pointAlong()` Reorient all 3D children so their primary axis (Z) points along direction. Sugar for a single group-wide axis rotation via Transform.rotationAxis(...).
1589
- - `transform()` — Apply a 4x4 transform matrix or Transform object to all 3D children.
1590
- - `scale()` — Scale uniformly or per-axis from the group's bounding box center.
1591
- - `scaleAround()` — Scale uniformly or per-axis from an explicit pivot point.
1592
- - `mirror()` — Mirror across a plane through the group's bounding box center.
1593
- - `mirrorThrough()` — Mirror across a plane through an explicit point.
1594
- - `color()` color(hex: string): ShapeGroup
1595
- - `withReferences()` — Attach named placement references to this group. References survive normal transforms (translate/rotate/scale/mirror/transform). ```javascript const bracket = group( { name: 'Left', shape: leftShape }, { name: 'Right', shape: rightShape }, ).withReferences({ points: { mountCenter: [0, 0, 0] }, }); ```
1596
- - `referenceNames()` — List named placement references carried by this group.
1597
- - `withPorts()` — Attach named assembly ports (origin + axis + up) that survive transforms.
1598
- - `portNames()` — List named port identifiers carried by this group.
1599
- - `referencePoint()` — Resolve a named placement reference or built-in Anchor3D to a 3D point. Named refs take priority over built-in anchors.
1600
- - `placeReference()` — Translate the group so the given reference lands on the target coordinate. ```javascript const placed = require('./bracket-assembly.forge.js').group .placeReference('mountCenter', [0, 0, 50]); ```
1601
- - `withConnectors()` — Attach named connectors (typed, gendered ports) that survive transforms.
1602
- - `connectorNames()` — List connector names (ports that have a connectorType), including "ChildName.connectorName" from named children.
1603
- - `connectorsByType()` — Get all connectors of a given type, including from named children.
1604
- - `connectorDistance()`Distance between two connector origins on this group (supports dotted child paths).
1605
- - `connectorMeasurements()` — Get measurements metadata from a connector (supports dotted child paths).
1606
- - `matchTo()` — Position this group by matching connectors to a target. Connector names support dotted paths into named children: "ChildName.connectorName". Overloads: - Single pair: `matchTo(target, selfConn, targetConn, options?)` - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)` - Multi-target: `matchTo([ [target1, selfConn1, targetConn1], ... ], options?)`
1607
-
1608
- ### `RouteBuilder`
1609
-
1610
- **Methods:**
1611
-
1612
- - `up()` — Vertical line going +Y. Length is optional (solver determines it from constraints).
1613
- - `down()` — Vertical line going -Y. Length is optional.
1614
- - `right()` — Horizontal line going +X. Length is optional.
1615
- - `left()` — Horizontal line going -X. Length is optional.
1616
- - `lineAt()` — Line at an arbitrary angle (degrees from +X). Length is optional.
1617
- - `line()` — Line with solver-determined direction. Length is optional. Direction comes from tangency to previous arc or from constraints.
1618
- - `toward()` — Line toward a specific point. Length defaults to the distance to that point.
1619
- - `arcLeft()` — Tangent arc turning left relative to travel direction. or `{ minSweep: degrees }` to seed the geometry without constraining. `minSweep` guides the solver to the correct branch for arcs that sweep more than the default 90° seed.
1620
- - `arcRight()` — Tangent arc turning right relative to travel direction. or `{ minSweep: degrees }` to seed without constraining.
1621
- - `close()` — Close the route with a straight line back to the start point.
1622
- - `done()` — Close the route back to its start point and register as a profile loop. No extra line segment is added. A coincident constraint connects the last point to the start, and tangency is added for G1 smoothness when arcs are at the junction. The session's incremental solver processes these constraints, keeping seed positions accurate for the final solve.
1623
- - `get start()` — PointId of the route's start point.
1624
- - `get end()` — PointId of the current cursor (route's end).
1625
- - `startOf()` — Get the start point of a segment.
1626
- - `endOf()` — Get the end point of a segment.
1627
-
1628
- ### `FlatPart`
1629
-
1630
- **Properties:**
1631
-
1632
- | Property | Type | Description |
1633
- |----------|------|-------------|
1634
- | `name` | `string` | — |
1635
- | `thickness` | `number` | — |
1636
- | `options` | `FlatPartOptions` | — |
1637
-
1638
- **Methods:**
1639
-
1640
- - `get edges()` — All edges as a read-only map.
1641
- - `edge()` — Look up a named edge. Throws if the edge does not exist.
1642
- - `edgeNames()` — All edge names on this part.
1643
- - `get partNumber()` — get partNumber(): number
1644
- - `get joints()` — get joints(): readonly JointRecord[]
1645
- - `get quantity()` — get quantity(): number
1646
- - `addGeometry()` — Add geometry (e.g. protruding tabs) to the part profile.
1647
- - `subtractGeometry()` — Subtract geometry (e.g. slot cuts) from the part profile.
1648
- - `addJoint()` — Record a joint connection for assembly preview.
1649
- - `profile()` — Final 2D profile with joints and optional kerf compensation.
1650
- - `solid()` — 3D solid — extrude the profile by material thickness.
1651
-
1652
- ### `LaserKit`
1653
-
1654
- **Methods:**
1655
-
1656
- - `get kerf()` — Laser kerf in mm.
1657
- - `get parts()` — All registered parts (flat, in insertion order).
1658
- - `get material()` — Default material label.
1659
- - `get sheetWidth()` — Stock sheet width in mm.
1660
- - `get sheetHeight()` — Stock sheet height in mm.
1661
- - `addPart()` — Register a flat part with this kit. Assigns a sequential part number and records the quantity.
1662
- - `cutSheets()` — Generate nested cut sheets using guillotine bin-packing.
1663
- - `bom()` — Bill of materials listing every part with dimensions.
1664
- - `partSvgs()` — Individual SVG string for each part profile, keyed by part name.
1665
- - `inventorySvg()` — Combined inventory SVG showing all parts in a labeled grid.
1666
- - `assemblyPreview()` — 3D fold-up preview of the assembled kit.
1667
- - `assemblyInstructions()` — Step-by-step assembly instructions.
1668
- - `formatInstructions()` — Human-readable assembly instructions text.
688
+ - `childName(index: number): string | undefined` — Return the optional name of the child at `index`.
689
+ - `child(name: string): GroupChild` — Return the named child by name. Throws if not found. Useful when importing a multipart group and working on components individually.
690
+ - `clone(): ShapeGroup` — Return a deep-cloned ShapeGroup tree (refs copied).
691
+ - `translate(x: number, y: number, z: number): ShapeGroup` — Move the entire group by (x, y, z). All children move together as a unit.
692
+ - `boundingBox(): { min: [ number, number, number ]; max: [ number, number, number ]; }` — Return the combined 3D bounding box of all children.
693
+ - `moveTo(x: number, y: number, z: number): ShapeGroup` — Move the group so its bounding-box min corner lands at the given coordinate.
694
+ - `moveToLocal(target: Shape | ShapeGroup, x: number, y: number, z: number): ShapeGroup` — Move the group relative to another part's bounding-box min corner.
695
+ - `attachTo(target: Shape | ShapeGroup, targetAnchor: Anchor3D | string, selfAnchor?: Anchor3D, offset?: [ number, number, number ]): ShapeGroup` — Attach this group to a face or anchor on another part. `targetAnchor` can be a built-in anchor name or a custom reference name on the target. `selfAnchor` selects the anchor on this group to align.
696
+ - `onFace(parent: Shape | ShapeGroup, face: "front" | "back" | "left" | "right" | "top" | "bottom", opts?: { u?: number; v?: number; protrude?: number; }): ShapeGroup` — Place this group on a face of a parent shape. See Shape.onFace() for full documentation.
697
+ - `rotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup` Rotate the group around an arbitrary axis through the origin.
698
+ - `rotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup` — Rotate the group around the X axis.
699
+ - `rotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup` — Rotate the group around the Y axis.
700
+ - `rotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup` — Rotate the group around the Z axis.
701
+ - `rotateAroundAxis(axis: [ number, number, number ], angleDeg: number, pivot?: [ number, number, number ]): ShapeGroup` Rotate around an arbitrary axis, optionally through a pivot point.
702
+ - `rotateAroundTo(axis: [ number, number, number ], pivot: [ number, number, number ], movingPoint: Anchor3D | [ number, number, number ], targetPoint: Anchor3D | [ number, number, number ], options?: RotateAroundToOptions): ShapeGroup` Rotate around an axis until a moving point reaches the target line/plane defined by the axis and target point. ShapeGroup string points use built-in anchors only.
703
+ - `pointAlong(direction: [ number, number, number ]): ShapeGroup` — Reorient the group so its local Z axis points along `direction`.
704
+ - `transform(m: Mat4 | Transform): ShapeGroup` — Apply a 4x4 transform matrix or `Transform` to all 3D children.
705
+ - `scale(v: number | [ number, number, number ]): ShapeGroup` — Scale uniformly or per-axis from the group's bounding-box center.
706
+ - `scaleAround(pivot: [ number, number, number ], v: number | [ number, number, number ]): ShapeGroup` — Scale uniformly or per-axis from an explicit pivot point.
707
+ - `mirror(normal: [ number, number, number ]): ShapeGroup` — Mirror across a plane through the group's bounding-box center.
708
+ - `mirrorThrough(point: [ number, number, number ], normal: [ number, number, number ]): ShapeGroup` — Mirror across a plane through an explicit point.
709
+ - `color(hex: string): ShapeGroup` — Return a copy of the group with the given display color applied to each child.
710
+ - `withReferences(refs: PlacementReferenceInput): ShapeGroup` — Attach named placement references to this group. References survive normal transforms (translate/rotate/scale/mirror/transform). ```javascript const bracket = group( { name: 'Left', shape: leftShape }, { name: 'Right', shape: rightShape }, ).withReferences({ points: { mountCenter: [0, 0, 0] }, }); ```
711
+ - `referenceNames(kind?: PlacementReferenceKind): string[]` — List named placement references carried by this group.
712
+ - `withPorts(ports: Record<string, PortInput>): ShapeGroup` — Backward-compatible alias for `withConnectors()`.
713
+ - `portNames(): string[]` — Backward-compatible alias for `connectorNames()`.
714
+ - `referencePoint(ref: PlacementAnchorLike): [ number, number, number ]` — Resolve a named placement reference or built-in Anchor3D to a 3D point. Named refs take priority over built-in anchors.
715
+ - `placeReference(ref: PlacementAnchorLike, target: [ number, number, number ], offset?: [ number, number, number ]): ShapeGroup` — Translate the group so the given anchor or reference lands on the target coordinate. Accepts any built-in anchor name (`'bottom'`, `'center'`, `'top-front-left'`, etc.) or a custom placement reference attached via `withReferences()`. ```javascript // Ground a group — put its bottom at Z = 0 assembly.placeReference('bottom', [0, 0, 0]) // Use a custom reference from a multi-file part const placed = require('./bracket-assembly.forge.js').group .placeReference('mountCenter', [0, 0, 50]); ```
716
+ - `withConnectors(connectors: Record<string, ConnectorInput>): ShapeGroup` — Attach named connectors attachment points that survive transforms. Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching).
717
+ - `connectorNames(): string[]` — List all connector names, including "ChildName.connectorName" from named children.
718
+ - `connectorsByType(type: string): Array<{ name: string; port: PortDef; }>` Get all connectors of a given type, including from named children.
719
+ - `connectorDistance(nameA: string, nameB: string): number` — Distance between two connector origins on this group (supports dotted child paths).
720
+ - `connectorMeasurements(name: string): Record<string, number | string>` Get measurements metadata from a connector (supports dotted child paths).
721
+ - `matchTo(targetOrPairs: Shape | ShapeGroup | Array<[ Shape | ShapeGroup, string, string ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): ShapeGroup` — Position this group by matching connectors to a target. Connector names support dotted paths into named children: "ChildName.connectorName". Overloads: - Single pair: `matchTo(target, selfConn, targetConn, options?)` - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)` - Multi-target: `matchTo([ [target1, selfConn1, targetConn1], ... ], options?)`
1669
722
 
1670
723
  ---
1671
724
 
@@ -1675,49 +728,48 @@ Core 3D solid shape. All operations are immutable and return new shapes. Support
1675
728
 
1676
729
  ### `verify`
1677
730
 
1678
- **Members:**
1679
-
1680
- - `that()` — Custom predicate check.
1681
- - `equal()` — Check that two numbers are approximately equal (within tolerance).
1682
- - `notEqual()` Check that two numbers are NOT equal (differ by more than tolerance).
1683
- - `greaterThan()` — Check that actual > min.
1684
- - `lessThan()` — Check that actual < max.
1685
- - `inRange()` — Check that min <= actual <= max.
1686
- - `centersCoincide()` — Check that the bounding-box centers of two shapes coincide within tolerance (mm).
1687
- - `notColliding()` — Check that two shapes do not collide (minGap > 0).
1688
- - `minClearance()` — Check that a minimum clearance gap exists between two shapes.
1689
- - `parallel()` — Check that two face normals are parallel (within toleranceDeg degrees).
1690
- - `perpendicular()` — Check that two face normals are perpendicular (within toleranceDeg degrees).
1691
- - `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.
1692
- - `faceAt()` — Check that a face center lies at a specific position (within toleranceMm).
1693
- - `sameDirection()` Check that two face normals point in the same direction (not antiparallel). Stricter than parallel both |angle| AND sign must match.
1694
- - `isEmpty()` — Check that a shape is empty.
1695
- - `notEmpty()` — Check that a shape is NOT empty.
1696
- - `volumeApprox()` — Check that a shape's volume is approximately equal to expected (mm³).
1697
- - `areaApprox()` — Check that a shape's surface area is approximately equal to expected (mm²).
1698
- - `boundingBoxSize()` — Check that a shape's bounding box has approximately the given size.
731
+ - `that(label: string, check: () => boolean, message?: string): void` — Custom predicate check.
732
+ - `equal(label: string, actual: number, expected: number, tolerance?: number, message?: string): void` — Check that two numbers are approximately equal (within tolerance).
733
+ - `notEqual(label: string, actual: number, unexpected: number, tolerance?: number, message?: string): void` — Check that two numbers are NOT equal (differ by more than tolerance).
734
+ - `greaterThan(label: string, actual: number, min: number, message?: string): void` — Check that actual > min.
735
+ - `lessThan(label: string, actual: number, max: number, message?: string): void` Check that actual < max.
736
+ - `inRange(label: string, actual: number, min: number, max: number, message?: string): void` — Check that min <= actual <= max.
737
+ - `centersCoincide(label: string, a: ShapeLike$1, b: ShapeLike$1, tolerance?: number): void` — Check that the bounding-box centers of two shapes coincide within tolerance (mm).
738
+ - `notColliding(label: string, a: ShapeLike$1, b: ShapeLike$1, searchLength?: number): void` — Check that two shapes do not collide (minGap > 0).
739
+ - `minClearance(label: string, a: ShapeLike$1, b: ShapeLike$1, minGap: number, searchLength?: number): void` — Check that a minimum clearance gap exists between two shapes.
740
+ - `parallel(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals are parallel (within toleranceDeg degrees).
741
+ - `perpendicular(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals are perpendicular (within toleranceDeg degrees).
742
+ - `coplanar(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number, toleranceMm?: number): void` — Check that a face is coplanar with (same plane as) another face, meaning they are parallel AND their centers lie on the same plane.
743
+ - `faceAt(label: string, face: FaceRefLike, expectedPos: [ number` — Check that a face center lies at a specific position (within toleranceMm).
744
+ - `sameDirection(label: string, faceA: FaceRefLike, faceB: FaceRefLike, toleranceDeg?: number): void` — Check that two face normals point in the same direction (not antiparallel). Stricter than parallel both |angle| AND sign must match.
745
+ - `isEmpty(label: string, shape: ShapeLike$1, message?: string): void` — Check that a shape is empty.
746
+ - `notEmpty(label: string, shape: ShapeLike$1, message?: string): void` Check that a shape is NOT empty.
747
+ - `volumeApprox(label: string, shape: ShapeLike$1, expected: number, tolerance?: number): void` — Check that a shape's volume is approximately equal to expected (mm³).
748
+ - `areaApprox(label: string, shape: ShapeLike$1, expected: number, tolerance?: number): void` — Check that a shape's surface area is approximately equal to expected (mm²).
749
+ - `boundingBoxSize(label: string, shape: ShapeLike$1, expectedSize: [ number` — Check that a shape's bounding box has approximately the given size.
1699
750
 
1700
751
  ### `Constraint`
1701
752
 
1702
- **Members:**
1703
-
1704
- - `makeParallel()` — makeParallel(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): Constra
1705
- - `enforceAngle()` — enforceAngle(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg, angleDeg
1706
- - `horizontal()` — horizontal(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchB
1707
- - `vertical()` — vertical(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBui
1708
- - `equalLength()` — equalLength(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): Constrai
1709
- - `distance()` — distance(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg, value: num
1710
- - `fix()` — fix(builder: ConstrainedSketchBuilder, pt: PointArg, x: number, y: number): Cons
1711
- - `coincident()` — coincident(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg): Constra
1712
- - `perpendicular()` — perpendicular(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): Constr
1713
- - `length()` — length(builder: ConstrainedSketchBuilder, line: LineArg, value: number): Constra
753
+ - `makeParallel(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to be parallel.
754
+ - `enforceAngle(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg, angleDeg: number): ConstrainedSketchBuilder` — Constrain the signed angle from line `a` to line `b`.
755
+ - `horizontal(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder` — Constrain a line to be horizontal.
756
+ - `vertical(builder: ConstrainedSketchBuilder, line: LineArg): ConstrainedSketchBuilder` — Constrain a line to be vertical.
757
+ - `equalLength(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to have equal length.
758
+ - `distance(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg, value: number): ConstrainedSketchBuilder` — Constrain the distance between two points.
759
+ - `fix(builder: ConstrainedSketchBuilder, pt: PointArg, x: number, y: number): ConstrainedSketchBuilder` — Fix a point at a specific coordinate.
760
+ - `coincident(builder: ConstrainedSketchBuilder, a: PointArg, b: PointArg): ConstrainedSketchBuilder` — Constrain two points to occupy the same location.
761
+ - `perpendicular(builder: ConstrainedSketchBuilder, a: LineArg, b: LineArg): ConstrainedSketchBuilder` — Constrain two lines to be perpendicular.
762
+ - `length(builder: ConstrainedSketchBuilder, line: LineArg, value: number): ConstrainedSketchBuilder` — Constrain the length of a line.
1714
763
 
1715
764
  ### `Points`
1716
765
 
1717
- **Members:**
766
+ - `distance(a: Vec3, b: Vec3): number` — Euclidean distance between two 3D points.
767
+ - `midpoint(a: Vec3, b: Vec3): Vec3` — Center point between two 3D points.
768
+ - `lerp(a: Vec3, b: Vec3, t: number): Vec3` — Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b.
769
+ - `direction(a: Vec3, b: Vec3): Vec3` — Unit direction vector from a to b. Throws if a and b are the same point.
770
+ - `offset(point: Vec3, dir: Vec3, amount: number): Vec3` — Move a point along a direction vector by a given amount.
771
+ - `polar(length: number, angleDeg: number, from?: [ number, number ]): [ number, number ]` — Compute a 2D point at distance and angle (degrees) from an optional origin.
772
+
773
+ ### `connector`
1718
774
 
1719
- - `distance()` Euclidean distance between two 3D points.
1720
- - `midpoint()` — Center point between two 3D points.
1721
- - `lerp()` — Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b.
1722
- - `direction()` — Unit direction vector from a to b. Throws if a and b are the same point.
1723
- - `offset()` — Move a point along a direction vector by a given amount.
775
+ Connector factory. Create attachment points: `connector({...})`, `connector.male(type, {...})`, etc.