forgecad 0.6.3 → 0.8.0

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