forgecad 0.10.4 → 0.11.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 (121) hide show
  1. package/dist/assets/{AdminPage-B3L3W1Uo.js → AdminPage-B1nIvqLS.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DXKVXMrJ.js → BenchmarkPage-YZJbw5nd.js} +2 -2
  3. package/dist/assets/{BlogPage-B7BWxOCg.js → BlogPage-DIWRApKS.js} +1 -1
  4. package/dist/assets/{DocsPage-BPGGwht1.js → DocsPage-ClL6X1hR.js} +8 -22
  5. package/dist/assets/EditorApp-CYBDvSyT.js +17067 -0
  6. package/dist/assets/{EmbedViewer-DygByZS2.js → EmbedViewer-Dmfu_LIw.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-BoVE7JGY.js → LandingPageProofDriven-XYTiYxfM.js} +2 -2
  8. package/dist/assets/{LegalPage-Din8wv8d.js → LegalPage-D5Z3CscF.js} +2 -2
  9. package/dist/assets/{PricingPage-C2PMzmDc.js → PricingPage-BP4lIGio.js} +2 -2
  10. package/dist/assets/{SettingsPage-BlJDCRe8.js → SettingsPage-D3bcPBsC.js} +1 -1
  11. package/dist/assets/{app-BsRYSfxY.js → app-BKjogwIZ.js} +3288 -512
  12. package/dist/assets/{backendInit-6C0DLgH0.js → backendInit-6a9-ilom.js} +80498 -74979
  13. package/dist/assets/cli/{render-XXol_ET7.js → render-CMNudGb0.js} +1264 -113
  14. package/dist/assets/{constructionHistoryWorker-cTHWRJEi.js → constructionHistoryWorker-BuZgc606.js} +8369 -6839
  15. package/dist/assets/{evalWorker-BssDYW9u.js → evalWorker-DQ82ueGu.js} +45438 -39996
  16. package/dist/assets/{forgecad_geometry-CZ_IfuvA.js → forgecad_geometry-D8rWX7nQ.js} +1 -1
  17. package/dist/assets/{forgecad_geometry_bg-C3rQHfwg.wasm → forgecad_geometry_bg-ObqfqjJT.wasm} +0 -0
  18. package/dist/assets/{inspectWorker-ymhBV4Ll.js → inspectWorker-Cuby2qfT.js} +4899 -1303
  19. package/dist/assets/{jointPose-B0blBj9A.js → jointPose-CFql5I-u.js} +1 -1
  20. package/dist/assets/{landing-proof-driven-Cpf-MIbI.css → landing-proof-driven-_u4v_xQb.css} +2 -2
  21. package/dist/assets/{manifold-CYlIm-M6.js → manifold-02pmr7O7.js} +2 -2
  22. package/dist/assets/{manifold-B_7QXpGB.js → manifold-C6KU0oII.js} +1 -1
  23. package/dist/assets/{manifold-CNShmpEJ.js → manifold-P1yF3GKn.js} +1 -1
  24. package/dist/assets/{reportWorker-Cb5eyM7D.js → reportWorker-kg065BVL.js} +76583 -65731
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +2 -2
  27. package/dist/docs-raw/AI/usage.md +6 -8
  28. package/dist/docs-raw/CLI.md +14 -12
  29. package/dist/docs-raw/component-model.md +28 -9
  30. package/dist/docs-raw/generated/assembly.md +76 -3
  31. package/dist/docs-raw/generated/concepts.md +43 -7
  32. package/dist/docs-raw/generated/core.md +399 -73
  33. package/dist/docs-raw/generated/curves.md +357 -6
  34. package/dist/docs-raw/generated/runtime-names.md +12 -12
  35. package/dist/docs-raw/generated/sketch.md +16 -3
  36. package/dist/docs-raw/guides/inspection-bundles.md +5 -3
  37. package/dist/docs-raw/guides/structural-fea.md +235 -0
  38. package/dist/docs-raw/skills/forgecad-build-model.md +70 -147
  39. package/dist/docs-raw/skills/forgecad-image-prompt.md +1 -1
  40. package/dist/docs-raw/skills/forgecad-project-sync.md +3 -3
  41. package/dist/docs-raw/skills/forgecad-reconstruct-cad-file.md +2 -2
  42. package/dist/docs-raw/skills/forgecad-reconstruct-from-images.md +4 -5
  43. package/dist/docs-raw/skills/forgecad.md +4 -1
  44. package/dist/docs-raw/skills/index.md +1 -5
  45. package/dist/docs-raw/welcome.md +3 -4
  46. package/dist/index.html +1 -1
  47. package/dist/llms.txt +1 -2
  48. package/dist/sitemap.xml +15 -15
  49. package/dist-cli/{check-compiler-4RPB6SB5.js → check-compiler-UJWUEIDC.js} +1 -1
  50. package/dist-cli/{check-query-propagation-KN3DFQTX.js → check-query-propagation-O2EPDJSY.js} +1 -1
  51. package/dist-cli/{chunk-UHBRMYA6.js → chunk-MNDROM7T.js} +78926 -73392
  52. package/dist-cli/forgecad.js +6306 -1061
  53. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  54. package/dist-skill/CONTEXT.md +1257 -110
  55. package/dist-skill/SKILL.md +4 -1
  56. package/dist-skill/docs/API/core/concepts.md +31 -4
  57. package/dist-skill/docs/CLI.md +14 -12
  58. package/dist-skill/docs/generated/assembly.md +73 -3
  59. package/dist-skill/docs/generated/core.md +395 -74
  60. package/dist-skill/docs/generated/curves.md +356 -6
  61. package/dist-skill/docs/generated/runtime-names.md +12 -12
  62. package/dist-skill/docs/generated/sketch.md +16 -3
  63. package/dist-skill/docs/guides/inspection-bundles.md +5 -3
  64. package/dist-skill/docs/guides/manual-parameters.md +130 -0
  65. package/dist-skill/docs/guides/structural-fea.md +235 -0
  66. package/dist-skill/library/README.md +0 -4
  67. package/dist-skill/library/forgecad-build-model/SKILL.md +57 -150
  68. package/dist-skill/library/forgecad-build-model/references/inspection-feedback.md +58 -0
  69. package/dist-skill/library/forgecad-build-model/references/module-contracts.md +53 -0
  70. package/dist-skill/library/forgecad-build-model/references/parameter-controls.md +22 -0
  71. package/dist-skill/library/forgecad-build-model/references/readiness-review.md +43 -0
  72. package/dist-skill/library/forgecad-build-model/references/simulation-feedback.md +49 -0
  73. package/dist-skill/library/forgecad-build-model/references/stage-1-design-intent.md +21 -0
  74. package/dist-skill/library/forgecad-build-model/references/stage-2-architecture-plan.md +23 -0
  75. package/dist-skill/library/forgecad-build-model/references/stage-3-build-slices.md +39 -0
  76. package/dist-skill/library/forgecad-build-model/references/stage-4-feedback-iteration.md +24 -0
  77. package/dist-skill/library/forgecad-build-model/references/stage-5-readiness-package.md +34 -0
  78. package/dist-skill/library/forgecad-image-prompt/SKILL.md +1 -1
  79. package/dist-skill/library/forgecad-project-sync/SKILL.md +3 -3
  80. package/dist-skill/library/forgecad-reconstruct-cad-file/SKILL.md +2 -2
  81. package/dist-skill/library/forgecad-reconstruct-from-images/SKILL.md +4 -5
  82. package/dist-skill/website/skills/forgecad-build-model.md +70 -147
  83. package/dist-skill/website/skills/forgecad-image-prompt.md +1 -1
  84. package/dist-skill/website/skills/forgecad-project-sync.md +3 -3
  85. package/dist-skill/website/skills/forgecad-reconstruct-cad-file.md +2 -2
  86. package/dist-skill/website/skills/forgecad-reconstruct-from-images.md +4 -5
  87. package/dist-skill/website/skills/forgecad.md +4 -1
  88. package/dist-skill/website/skills/index.md +1 -5
  89. package/examples/analysis/structural-stress-fea.forge.js +19 -0
  90. package/examples/api/blend-full-round.forge.js +37 -0
  91. package/examples/api/blend-variable-radius.forge.js +51 -0
  92. package/examples/api/curve-project-and-intersect.forge.js +59 -0
  93. package/examples/api/extrude-up-to-face.forge.js +47 -0
  94. package/examples/api/param-path2d.forge.js +65 -0
  95. package/examples/api/param-placement2d.forge.js +80 -0
  96. package/examples/api/param-spline2d-g-continuity.forge.js +57 -0
  97. package/examples/api/spoon-full-tang-handle.forge.js +188 -0
  98. package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
  99. package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
  100. package/examples/api/surface-variable-thickness-panel.forge.js +62 -0
  101. package/examples/mechanical/airplane-propeller.forge.js +81 -28
  102. package/package.json +5 -2
  103. package/dist/assets/EditorApp-BWUGCdD5.js +0 -16610
  104. package/dist/docs-raw/skills/forgecad-design-spec.md +0 -145
  105. package/dist/docs-raw/skills/forgecad-grade-model.md +0 -84
  106. package/dist/docs-raw/skills/forgecad-inspect-model.md +0 -80
  107. package/dist/docs-raw/skills/forgecad-verify-mujoco.md +0 -78
  108. package/dist-skill/library/forgecad-design-spec/SKILL.md +0 -132
  109. package/dist-skill/library/forgecad-design-spec/references/default-profiles.md +0 -99
  110. package/dist-skill/library/forgecad-design-spec/references/master-prompt.md +0 -73
  111. package/dist-skill/library/forgecad-grade-model/SKILL.md +0 -72
  112. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +0 -4
  113. package/dist-skill/library/forgecad-inspect-model/SKILL.md +0 -68
  114. package/dist-skill/library/forgecad-verify-mujoco/SKILL.md +0 -66
  115. package/dist-skill/website/skills/forgecad-design-spec.md +0 -145
  116. package/dist-skill/website/skills/forgecad-grade-model.md +0 -84
  117. package/dist-skill/website/skills/forgecad-inspect-model.md +0 -80
  118. package/dist-skill/website/skills/forgecad-verify-mujoco.md +0 -78
  119. /package/dist/assets/{landing-proof-driven-BxZZh5r5.js → landing-proof-driven-DNPRKL_p.js} +0 -0
  120. /package/dist-skill/library/{forgecad-verify-mujoco → forgecad-build-model}/scripts/mujoco_verify.py +0 -0
  121. /package/dist-skill/library/{forgecad-inspect-model → forgecad-build-model/scripts}/summarize_manifest.py +0 -0
@@ -28,7 +28,8 @@ Author or modify ForgeCAD models, sketches, assemblies, and CLI workflows. Prefe
28
28
 
29
29
  ### Import and composition
30
30
 
31
- - Always include the extension in relative imports: `require("./file.forge.js", { Param: value })` for model files, `require("./helpers.js")` for plain helper modules. Extensionless imports such as `require("./file")` do not resolve; ForgeCAD resolves project imports by exact path.
31
+ - Always include the extension in relative imports: `require("./file.forge.js")` for model files, `require("./helpers.js")` for plain helper modules. Extensionless imports such as `require("./file")` do not resolve; ForgeCAD resolves project imports by exact path.
32
+ - Reusable `.forge.js` part files should return builder functions such as `return { buildPart }`; direct-run preview params belong inside `if (require.main === module)`.
32
33
  - ForgeCAD APIs are injected globals in `.forge.js` files. Use `bom()`, `box()`, `scene()`, `Shape`, etc. directly; never destructure those names from helpers (`const { bom } = require("./bom.js")`). Import helper files under a project-specific name such as `const bomHelpers = require("./bom.js")`.
33
34
  - For static multi-part models, connectors + `matchTo()` are the default way to assemble touching parts.
34
35
  - Top-level scripts can return `Assembly` or `SolvedAssembly` directly. Do not call `.toGroup()` just to render an assembly; use it only when you need `ShapeGroup` composition, transforms, or named-child lookup.
@@ -77,10 +78,10 @@ A `.forge.js` script is plain JavaScript that returns geometry. The entire forge
77
78
 
78
79
  All geometry operations are **immutable** — shapes, sketches, groups, assemblies, and boards return new values, never mutate in place.
79
80
 
80
- A script must return one of three shapes:
81
+ A script should return one of three shapes:
81
82
 
82
83
  1. **A single renderable** — `Shape`, `Sketch`, `ShapeGroup`, `Assembly`, `SolvedAssembly`, or `SdfShape`.
83
- 2. **An array** of renderables or named descriptors `{ name, tags?, shape | sketch | group, color? }`:
84
+ 2. **An array** of renderables or named descriptors `{ name, tags?, shape | sketch | group, color? }`, usually for direct-run previews and multi-object display:
84
85
 
85
86
  ```javascript
86
87
  return [
@@ -89,11 +90,38 @@ A script must return one of three shapes:
89
90
  ];
90
91
  ```
91
92
 
92
- 3. **A metadata object** — a plain object whose renderable values are rendered and whose non-renderable values (numbers, hole tables, builder functions) are silently skipped at render but flow to importers via `require()`. Each key becomes a named group, so don't pile independent parts into one array key (`{ parts: [a, b, c] }`) — the integrity gate reads that as a single fragmented part. Give each part its own key (`{ collar12, collar16, plug }`) or use named descriptors (form 2).
93
+ 3. **A module interface object** — usually builder functions, optionally a built shape plus interface data:
94
+
95
+ ```javascript
96
+ return { buildBracket };
97
+ // or, when the file's useful output is already built:
98
+ return { shape, connectors, boltPattern };
99
+ ```
100
+
101
+ For reusable part files, prefer a builder export and keep direct-run preview controls inside the entry guard:
102
+
103
+ ```javascript
104
+ function buildThing(props) {
105
+ return box(props.width, props.depth, props.height);
106
+ }
107
+
108
+ if (require.main === module) {
109
+ const previewProps = {
110
+ width: param("Width", 80),
111
+ depth: param("Depth", 40),
112
+ height: param("Height", 12),
113
+ };
114
+ return buildThing(previewProps);
115
+ }
116
+
117
+ return { buildThing };
118
+ ```
119
+
120
+ When a plain object is returned directly, renderable values are shown in the viewport and non-renderable values are available to importers through `require()`.
93
121
 
94
122
  Return an unsolved `Assembly` directly — ForgeCAD solves it at default joint values for display. Use `assembly.solve(state)` for a specific pose. Never call `.toGroup()` just to make an assembly render; use it only when you need `ShapeGroup` composition or named-child lookup.
95
123
 
96
- For multi-file projects import path rules, the metadata pattern, and Forge-aware builder modules see the [`require()` docs](../../generated/core.md).
124
+ For multi-file projects, import path rules, and reusable builder modules, see the [`require()` docs](../../generated/core.md).
97
125
 
98
126
  ## Identity
99
127
 
@@ -135,24 +163,154 @@ activateBackend, Analysis, arcSlot, assembly, Assembly, Blend, bom, box
135
163
  cameraTrajectory, Carrier, chamfer, circle2d, Circle2D, circularLayout, circularPattern, circularPattern2d
136
164
  coalesceEdges, compareWith, connector, console, constrainedSketch, Curve, Curve3D, cutPlane
137
165
  cylinder, difference, difference2d, dim, draft, ellipse, explodeView, faceProfile
138
- fillet, Function, gcode, GCodeBuilder, getActiveBackend, global, globalThis, group
139
- Import, ImportedAssembly, initKernel, intersection, intersection2d, intersectWithPlane, joint, Laser
140
- lib, Line2D, linearPattern, linearPattern2d, loadFont, loft, Loft, mirrorCopy
141
- mock, ngon, NurbsCurve3D, NurbsSurface, offsetSolid, param, Param, path
142
- Point2D, Points, polygon, polygonVertices, port, Product, ProductPanelBuilder, ProductRibbonBuilder
143
- ProductSkin, ProductSkinBuilder, ProductStationBuilder, ProductSurfaceBuilder, ProductSurfaceRef, projectToPlane, queueMicrotask, rect
144
- Rectangle2D, roundedRect, Route3D, scene, Sculpt, sdf, SdfShape, selectEdge
145
- selectEdges, self, setActiveBackend, setImmediate, setInterval, setTimeout, Shape, ShapeGroup
146
- sheetMetal, SheetMetalPart, sheetStock, Sim, Sketch, sketchToDxf, sketchToSvg, slot
147
- SolvedAssembly, spec, sphere, spline2d, stroke, Surface, SurfaceBody, SurfaceMembers
148
- sweep, text2d, textWidth, torus, toShape, Transform, union, union2d
149
- variableSweep, verify, Viewport, window, Wood, Wrap
166
+ Fea, fillet, Function, gcode, GCodeBuilder, getActiveBackend, global, globalThis
167
+ group, Import, ImportedAssembly, initKernel, intersection, intersection2d, intersectWithPlane, joint
168
+ Laser, lib, Line2D, linearPattern, linearPattern2d, loadFont, loft, Loft
169
+ mirrorCopy, mock, ngon, NurbsCurve3D, NurbsSurface, offsetSolid, param, Param
170
+ path, Point2D, Points, polygon, polygonVertices, port, Product, ProductPanelBuilder
171
+ ProductRibbonBuilder, ProductSkin, ProductSkinBuilder, ProductStationBuilder, ProductSurfaceBuilder, ProductSurfaceRef, projectToPlane, queueMicrotask
172
+ rect, Rectangle2D, roundedRect, Route3D, scene, Sculpt, sdf, SdfShape
173
+ selectEdge, selectEdges, self, setActiveBackend, setImmediate, setInterval, setTimeout, Shape
174
+ ShapeGroup, sheetMetal, SheetMetalPart, sheetStock, Sim, Sketch, sketchToDxf, sketchToSvg
175
+ slot, SolvedAssembly, spec, sphere, spline2d, stroke, Surface, SurfaceBody
176
+ SurfaceMembers, sweep, text2d, textWidth, Thickness, torus, toShape, Transform
177
+ union, union2d, variableSweep, verify, Viewport, window, Wood, Wrap
150
178
  ```
151
179
 
152
180
  `showLabels` is also a runtime global, but it is not part of the top-level collision check. Avoid reusing it unless you intentionally want a local value with that name.
153
181
 
154
182
  ---
155
183
 
184
+ <!-- guides/manual-parameters.md -->
185
+
186
+ # Manual Parameter Sheets
187
+
188
+ Manual parameters are constrained design data: the script owns the structure and
189
+ the user edits only the declared values. They are for cases where a numeric
190
+ slider is the wrong shape of input.
191
+
192
+ ## Contents
193
+
194
+ - Decision Ladder
195
+ - Path2D
196
+ - Spline2D
197
+ - Placement2D
198
+ - Spatial Anchors
199
+ - Saving
200
+
201
+ ## Decision Ladder
202
+
203
+ Use the smallest parameter type that matches the design intent:
204
+
205
+ | Intent | Use |
206
+ | --- | --- |
207
+ | One scalar dimension, count, angle, or toggle | `Param.number()`, `Param.bool()`, `Param.choice()` |
208
+ | A repeated table of named scalar fields | `Param.list()` |
209
+ | A hand-shaped polygon, section outline, or open centerline | `Param.path2d()` |
210
+ | A smooth hand-shaped curve with tangent/curvature intent | `Param.spline2d()` |
211
+ | Named semantic blocks arranged in zones | `Param.placement2d()` |
212
+
213
+ Do not use `path2d` or `spline2d` as a generic table. Use them when dragging
214
+ points is meaningfully better than editing numbers.
215
+
216
+ ## Path2D
217
+
218
+ `Param.path2d(name, points, opts)` returns a `Path2DParamValue`.
219
+
220
+ - Closed paths are filled profile intent: call `.toSketch()`.
221
+ - Open paths are centerline intent: call `.toStroke(width)`.
222
+ - `x` and `y` ranges define the initial editor frame, not hard movement limits.
223
+ - Override keys are `Name[0].x`, `Name[0].y`, and `Name.__count__`.
224
+
225
+ ```javascript
226
+ const outline = Param.path2d('Outline', [[-30, -15], [30, -15], [24, 18], [-28, 16]], {
227
+ closed: true,
228
+ minPoints: 3,
229
+ maxPoints: 12,
230
+ unit: 'mm',
231
+ anchor: Param.anchor.sheetOnXY([0, 0, 8], { label: 'Top outline' }),
232
+ });
233
+
234
+ return outline.toSketch().extrude(4);
235
+ ```
236
+
237
+ ## Spline2D
238
+
239
+ `Param.spline2d(name, points, opts)` returns a `Spline2DParamValue`.
240
+
241
+ - Each point has `g`: `G0` for a hard break, `G1` for tangent smooth, `G2` for
242
+ curvature smooth.
243
+ - Use `.toCurveOnXY()`, `.toCurveOnXZ()`, or `.toCurveOnYZ()` for sweeps and
244
+ curve consumers.
245
+ - Use `.toPathOnXY()`, `.toPathOnXZ()`, or `.toPathOnYZ()` when an API expects
246
+ sampled points.
247
+ - Override keys are `Name[0].x`, `Name[0].y`, `Name[0].g`, and `Name.__count__`.
248
+
249
+ ```javascript
250
+ const sideProfile = Param.spline2d('Side Profile', [
251
+ { x: 0, y: 0, g: 'G2' },
252
+ { x: 35, y: 8, g: 'G2' },
253
+ { x: 70, y: 3, g: 'G1' },
254
+ ], {
255
+ unit: 'mm',
256
+ anchor: Param.anchor.sheetOnXZ([0, -20, 0], { label: 'Side profile' }),
257
+ });
258
+
259
+ const rail = sideProfile.toCurveOnXZ();
260
+ ```
261
+
262
+ ## Placement2D
263
+
264
+ `Param.placement2d(name, spec)` returns a `Placement2DParamValue`.
265
+
266
+ - The script declares stable item IDs, footprints, optional zones, and rules.
267
+ - The user moves named items; the model decides what each item creates.
268
+ - Use `.item(id)` for one placement or `.positions()` for keyed lookup.
269
+ - Override keys are item based: `Layout.battery.x`, `Layout.battery.y`,
270
+ `Layout.battery.angle`, and `Layout.battery.zone`.
271
+
272
+ ```javascript
273
+ const layout = Param.placement2d('Internal Layout', {
274
+ frame: { size: [120, 80] },
275
+ zones: [{ id: 'electronics', size: [70, 70], center: [-20, 0] }],
276
+ items: [
277
+ { id: 'battery', footprint: { type: 'rect', size: [42, 24] }, zone: 'electronics', at: [-25, 0] },
278
+ { id: 'speaker', footprint: { type: 'circle', radius: 12 }, at: [32, 8] },
279
+ ],
280
+ rules: { bounds: 'prevent', collisions: 'warn', snap: 1 },
281
+ anchor: Param.anchor.sheetOnXY([0, 0, 12], { label: 'Internal layout' }),
282
+ });
283
+
284
+ const battery = layout.item('battery');
285
+ const batteryBlock = box(42, 24, 6).translate(battery.x, battery.y, 3);
286
+ ```
287
+
288
+ ## Spatial Anchors
289
+
290
+ Every parameter type can carry optional viewport metadata through `anchor`.
291
+ Anchors do not change geometry.
292
+
293
+ - `Param.anchor.point([x, y, z])` creates a clickable pin for scalar, string,
294
+ list, boolean, or choice parameters.
295
+ - `Param.anchor.sheetOnXY([x, y, z])` places a 2D sheet in the XY plane at
296
+ fixed Z.
297
+ - `Param.anchor.sheetOnXZ([x, y, z])` places a 2D sheet in the XZ plane at
298
+ fixed Y.
299
+ - `Param.anchor.sheetOnYZ([x, y, z])` places a 2D sheet in the YZ plane at
300
+ fixed X.
301
+
302
+ The parameter still appears in the parameter panel when `anchor` is omitted; it
303
+ just has no 3D pin or spatial sheet.
304
+
305
+ ## Saving
306
+
307
+ Dragging a manual sheet writes parameter overrides first. The source model keeps
308
+ the declared defaults until those overrides are intentionally folded back into
309
+ code. Use snapshots for named parameter states, and use the parameter panel's
310
+ AI handoff for manual canvas edits when the desired result should become source.
311
+
312
+ ---
313
+
156
314
  <!-- generated/core.md -->
157
315
 
158
316
  # Core API
@@ -170,13 +328,15 @@ variableSweep, verify, Viewport, window, Wood, Wrap
170
328
  - [Grouping & Local Coordinates](#grouping-local-coordinates)
171
329
  - [Section & Projection](#section-projection)
172
330
  - [Verification](#verification)
173
- - [Shape](#shape) — Appearance, Face Topology, Edge Topology, Transforms, Booleans & Cutting, Features, Placement, Connectors, References, Measurement
331
+ - [Shape](#shape) — Freeform Construction, Appearance, Face Topology, Edge Topology, Transforms, Booleans & Cutting, Features, Placement, Connectors, References, Measurement
174
332
  - [Transform](#transform)
175
333
  - [ShapeGroup](#shapegroup) — Children, Transforms, Placement, Connectors, References
176
334
  - [SurfacePattern](#surfacepattern)
177
335
  - [Pattern2D](#pattern2d)
178
336
  - [Pattern2DBuilder](#pattern2dbuilder)
179
- - [Sheet](#sheet)
337
+ - [Path2DParamValue](#path2dparamvalue)
338
+ - [Spline2DParamValue](#spline2dparamvalue)
339
+ - [Placement2DParamValue](#placement2dparamvalue)
180
340
  - [CurveNetBuilder](#curvenetbuilder)
181
341
  - [MatchEdgeBuilder](#matchedgebuilder)
182
342
  - [BridgeBuilder](#bridgebuilder)
@@ -505,11 +665,11 @@ for (const edge of coalesceEdges(topEdges)) {
505
665
 
506
666
  ### Imports & Composition
507
667
 
508
- #### `require(path: string, paramOverrides?: Record<string, number | string>): any` — Import a module with optional ForgeCAD parameter overrides. Returns the module's exports.
668
+ #### `require(path: string): any` — Import a ForgeCAD or helper module. Returns the file's returned value.
509
669
 
510
- When importing a `.forge.js` file, most return values are passed through exactly as the script returns them. Assembly returns have one extra composition rule: an unsolved [`Assembly`](/docs/assembly#assembly) is wrapped as an [`ImportedAssembly`](/docs/assembly#importedassembly), preserving `solve(state)` and `mergeInto()` across file boundaries, while a returned [`SolvedAssembly`](/docs/assembly#solvedassembly) stays a [`SolvedAssembly`](/docs/assembly#solvedassembly). If the script returns a metadata object (e.g. `{ shape: myShape, bolts: {...} }`), the caller receives the full object — renderable values and metadata together.
670
+ When importing a `.forge.js` file, return values are passed through exactly as the script returns them, except for unsolved assemblies: a returned [`Assembly`](/docs/assembly#assembly) is wrapped as an [`ImportedAssembly`](/docs/assembly#importedassembly), preserving `solve(state)` and `mergeInto()` across file boundaries. A returned [`SolvedAssembly`](/docs/assembly#solvedassembly) stays a [`SolvedAssembly`](/docs/assembly#solvedassembly).
511
671
 
512
- **Script return contract:** a `.forge.js` script returns one of three shapes: a single renderable (Shape, ShapeGroup, Sketch, SdfShape, Assembly), an array of renderables or named descriptors (`{ name, shape|sketch|group }`), or a metadata object mixing renderable values with plain data. When a script runs directly, renderable entries of a metadata object are rendered under their key names and non-renderable entries are silently skipped — both halves of the metadata contract: one return value serves the viewport and `require()` callers.
672
+ **Script return contract:** a `.forge.js` script should return one of three shapes: a single renderable (Shape, ShapeGroup, Sketch, SdfShape, Assembly), an array of renderables or named descriptors (`{ name, shape|sketch|group }`) for previews and multi-object display, or a module interface object. Reusable part files should return builders such as `return { buildBracket }`; files that already build useful geometry may return `{ shape, connectors, boltPattern }`. When a script runs directly, renderable entries of a plain object are rendered under their key names and non-renderable entries are skipped.
513
673
 
514
674
  **Assembly return contract**
515
675
 
@@ -522,52 +682,56 @@ When importing a `.forge.js` file, most return values are passed through exactly
522
682
 
523
683
  **Path rule:** Always include the file extension in relative imports: use `require("./part.forge.js")` for model files and `require("./helpers.js")` for plain helper modules. ForgeCAD does not apply Node-style extension inference, so `require("./part")` will not find `part.forge.js` or `part.js`.
524
684
 
525
- **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.
526
-
527
- **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.
528
-
529
- **Multi-file assembly pattern** — pass cross-cutting design values from the assembly to parts:
685
+ **Multi-file assembly pattern** the assembly owns params and passes ordinary props to child builders:
530
686
 
531
687
  ```js
532
688
  // assembly.forge.js — owns cross-cutting params, passes to parts
533
689
  const wall = param("Wall", 3);
534
690
  const baseH = param("Base Height", 20);
535
691
 
536
- const mount = require('./motor-mount.forge.js', { Wall: wall });
537
- const base = require('./base-body.forge.js', { Wall: wall, Height: baseH });
692
+ const mountModule = require('./motor-mount.forge.js');
693
+ const baseModule = require('./base-body.forge.js');
694
+
695
+ const mount = mountModule.buildMount({ wall });
696
+ const base = baseModule.buildBase({ wall, height: baseH });
538
697
  ```
539
698
 
540
- **Metadata pattern** — parts publish interface data alongside geometry:
699
+ **Builder result pattern** — parts publish interface data alongside geometry:
541
700
 
542
701
  ```js
543
702
  // motor-mount.forge.js
544
- return { shape: mount, bolts: { dia: 5.3, pos: holePositions } };
703
+ function buildMount({ wall }) {
704
+ const shape = box(80, 40, wall);
705
+ return { shape, boltPattern: { dia: 5.3, positions: [[-25, 0], [25, 0]] } };
706
+ }
545
707
 
546
708
  // base-body.forge.js
547
709
  const mount = require('./motor-mount.forge.js');
548
- mount.bolts.pos // access the metadata
549
- mount.shape // access the geometry
710
+ const built = mount.buildMount({ wall: 3 });
711
+ built.boltPattern // access interface data
712
+ built.shape // access geometry
550
713
  ```
551
714
 
552
715
  **Forge-aware builder module pattern** — use `.forge.js` modules for reusable sketch, profile, shape, or assembly builders that need ForgeCAD runtime APIs:
553
716
 
554
717
  ```js
555
- // profiles.forge.js — inspectable on its own, reusable through require()
718
+ // profiles.forge.js
556
719
  function wheelProfile() {
557
720
  return circle2d(40).subtract(circle2d(18));
558
721
  }
559
722
 
560
- return {
561
- preview: [{ name: 'Wheel profile', sketch: wheelProfile() }],
562
- make: { wheelProfile },
563
- };
723
+ if (require.main === module) {
724
+ return [{ name: 'Wheel profile', sketch: wheelProfile() }];
725
+ }
726
+
727
+ return { wheelProfile };
564
728
 
565
729
  // main.forge.js
566
730
  const profiles = require('./profiles.forge.js');
567
- const wheel = profiles.make.wheelProfile().extrude(8);
731
+ const wheel = profiles.wheelProfile().extrude(8);
568
732
  ```
569
733
 
570
- Keep exported builders pure over top-level constants, top-level `param()` values, or explicit function arguments. Do not declare new `param()` values inside an exported builder if callers need `require('./profiles.forge.js', { Width: 80 })` overrides: import overrides are validated while the module loads, before any exported builder is called. Use plain `.js` modules only for pure constants, tables, math helpers, and formatting code that does not construct ForgeCAD geometry.
734
+ Keep returned builders pure over top-level constants or explicit function arguments. Put preview-only `param()` values inside `if (require.main === module)` so parent assemblies stay in charge of design parameters. Use plain `.js` modules only for pure constants, tables, math helpers, and formatting code that does not construct ForgeCAD geometry.
571
735
 
572
736
  **Entry detection (Node semantics):** `require.main` is the entry script's module object, so `require.main === module` is true only in the file being run directly. Part files use it to build standalone preview geometry only when opened directly — importers then skip that work entirely:
573
737
 
@@ -575,16 +739,30 @@ Keep exported builders pure over top-level constants, top-level `param()` values
575
739
  // part.forge.js
576
740
  function bracket() { ... }
577
741
  if (require.main === module) {
578
- return { preview: [{ name: 'Bracket', shape: bracket() }] }; // direct run: render it
742
+ return bracket({ width: param('Width', 80), height: param('Height', 40) });
579
743
  }
580
- return { make: { bracket } }; // imported: builders only
744
+ return { bracket };
581
745
  ```
582
746
 
583
747
  ### Parameters
584
748
 
585
- #### `Param.number(name: string, defaultValue: number, opts?: { min?: number; max?: number; step?: number; unit?: string; integer?: boolean; reverse?: boolean; }): number` — Declare a numeric parameter that renders as a slider in the UI.
749
+ #### `Param.anchor: { ... }` — Viewport anchor builders for spatial parameter editing.
586
750
 
587
- 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.
751
+ Anchors are metadata only: they do not change geometry. The editor uses them to show clickable parameter pins and manual-editing sheets in the 3D viewport. Use point anchors for scalar/text/list parameters and sheet anchors for `path2d`, [`spline2d`](/docs/curves#spline2d), and `placement2d` editors.
752
+
753
+ A sheet anchor's origin is a world/model-space point. Its plane selects how the 2D editor coordinates map into the viewport:
754
+
755
+ - `sheetOnXY([x, y, z])`: editor x/y map to world X/Y at fixed Z.
756
+ - `sheetOnXZ([x, y, z])`: editor x/y map to world X/Z at fixed Y.
757
+ - `sheetOnYZ([x, y, z])`: editor x/y map to world Y/Z at fixed X.
758
+
759
+ Omitting `anchor` still registers the parameter in the parameter panel; it just will not create a viewport pin/sheet.
760
+
761
+ `ParamAnchorOptions`: `{ label?: string, color?: string }`
762
+
763
+ #### `Param.number(name: string, defaultValue: number, opts?: NumberParamOptions): number` — Declare a numeric parameter that renders as a slider in the UI.
764
+
765
+ Each call registers a slider control. When the user moves the slider the entire script re-executes with the new value. The `name` string is the UI label and the CLI `--param` key.
588
766
 
589
767
  Default range rules when options are omitted:
590
768
 
@@ -600,38 +778,34 @@ const angle = Param.number("Angle", 45, { min: 0, max: 180, unit: "°" });
600
778
  const sides = Param.number("Sides", 6, { min: 3, max: 12, integer: true });
601
779
  ```
602
780
 
603
- **Parameter overrides** key must match `name` exactly:
604
-
605
- ```ts
606
- // Via require()
607
- const bracket = require("./bracket.forge.js", { Width: 80 });
781
+ CLI overrides use the parameter name:
608
782
 
609
- // Via CLI
610
- // forgecad run model.forge.js --param "Wall Thickness=3"
783
+ ```bash
784
+ forgecad run model.forge.js --param "Wall Thickness=3"
611
785
  ```
612
786
 
613
787
  Also available as the shorthand alias `param()`.
614
788
 
615
- #### `Param.string(name: string, defaultValue: string, opts?: { maxLength?: number; }): string` — Declare a string parameter that renders as a text input in the UI.
789
+ `ParamAnchorableOptions`: `{ anchor?: ParamAnchorDef }`
616
790
 
617
- String parameters let users type free-form text — labels, names, inscriptions, file paths, etc. The `name` string is the override key.
791
+ `NumberParamOptions`: `{ min?: number, max?: number, step?: number, unit?: string, integer?: boolean, reverse?: boolean }`
618
792
 
619
- ```ts
620
- const label = Param.string("Label", "Hello World");
621
- const name = Param.string("Name", "Part-001", { maxLength: 20 });
622
- ```
793
+ #### `Param.string(name: string, defaultValue: string, opts?: StringParamOptions): string` — Declare a string parameter that renders as a text input in the UI.
623
794
 
624
- Override via import:
795
+ String parameters let users type free-form text — labels, names, inscriptions, file paths, etc.
625
796
 
626
797
  ```ts
627
- const tag = require("./tag.forge.js", { Label: "Custom Text" });
798
+ const label = Param.string("Label", "Hello World");
799
+ const name = Param.string("Name", "Part-001", { maxLength: 20 });
628
800
  ```
629
801
 
630
802
  Only available as `Param.string()` — no standalone alias.
631
803
 
632
- #### `Param.bool(name: string, defaultValue: boolean): boolean` — Declare a boolean parameter that renders as a checkbox in the UI.
804
+ `StringParamOptions`: `{ maxLength?: number }`
805
+
806
+ #### `Param.bool(name: string, defaultValue: boolean, opts?: ParamAnchorableOptions): boolean` — Declare a boolean parameter that renders as a checkbox in the UI.
633
807
 
634
- 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.
808
+ Internally stored as `0`/`1` for CLI overrides. Pass `1` for true and `0` for false.
635
809
 
636
810
  ```ts
637
811
  const showHoles = Param.bool("Show Holes", true);
@@ -639,29 +813,17 @@ if (showHoles) return difference(plate, cylinder(10, 5).translate(50, 30, 0));
639
813
  return plate;
640
814
  ```
641
815
 
642
- Override via import:
643
-
644
- ```ts
645
- const pan = require("./pan.forge.js", { "Show Lid": 0 });
646
- ```
647
-
648
- #### `Param.choice(name: string, defaultValue: string, choices: string[]): string` — Declare a choice parameter that renders as a dropdown in the UI.
816
+ #### `Param.choice(name: string, defaultValue: string, choices: string[], opts?: ParamAnchorableOptions): string` — Declare a choice parameter that renders as a dropdown in the UI.
649
817
 
650
818
  `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.
651
819
 
652
- Overrides may be passed as the choice label string (preferred) or as a numeric index. The `name` string is the override key.
820
+ CLI overrides may be passed as the choice label string (preferred) or as a numeric index.
653
821
 
654
822
  ```ts
655
823
  const panStyle = Param.choice("Pan Style", "frying-pan", ["frying-pan", "saute-pan", "wok"]);
656
824
  if (panStyle === "wok") return buildWok();
657
825
  ```
658
826
 
659
- Override via import:
660
-
661
- ```ts
662
- const pan = require("./pan.forge.js", { "Pan Style": "wok" });
663
- ```
664
-
665
827
  Override via CLI:
666
828
 
667
829
  ```bash
@@ -680,6 +842,90 @@ Field types:
680
842
 
681
843
  `ListParamFieldDef`: `{ min?: number, max?: number, step?: number, unit?: string, integer?: boolean, boolean?: boolean, choices?: string[] }`
682
844
 
845
+ #### `Param.path2d(name: string, defaultPoints: Path2DPointInput[], opts?: Path2DParamOptions): Path2DParamValue` — Declare an editable 2D path parameter.
846
+
847
+ Use this for hand-shaped 2D profile data: plate outlines, slice profiles, stroke centerlines, and sweep rails. The returned value keeps the model code deterministic while the editor can render a drag-handle path UI.
848
+
849
+ Override keys use the same explicit row-field form as list params: `Path Name[0].x`, `Path Name[0].y`, and `Path Name.__count__`.
850
+
851
+ ```ts
852
+ const outline = Param.path2d("Bracket Outline", [
853
+ [-40, -20],
854
+ [40, -20],
855
+ [36, 24],
856
+ [-30, 28],
857
+ ], {
858
+ closed: true,
859
+ anchor: Param.anchor.sheetOnXY([0, 0, 8], { label: "Bracket outline" }),
860
+ });
861
+
862
+ return outline.toSketch().filletCorners(4).extrude(5);
863
+ ```
864
+
865
+ **`Path2DParamOptions`** extends ParamAnchorableOptions: `closed?: boolean`, `minPoints?: number`, `maxPoints?: number`, `x?: Partial<Path2DParamAxisDef>`, `y?: Partial<Path2DParamAxisDef>`, `unit?: string`
866
+
867
+ **`Path2DParamAxisDef`**
868
+ - `min: number` — Initial editor-frame minimum. Points may move outside this range on the infinite canvas.
869
+ - `max: number` — Initial editor-frame maximum. Points may move outside this range on the infinite canvas.
870
+ - Also: `step: number`.
871
+
872
+ #### `Param.spline2d(name: string, defaultPoints: Spline2DPointInput[], opts?: Spline2DParamOptions): Spline2DParamValue` — Declare an editable 2D spline parameter.
873
+
874
+ Use this when the model wants hand-shaped smooth curve data instead of a numeric table: handle spines, bowl station curves, sweep rails, and class-A guide profiles. Each node stores `g`, a continuity intent:
875
+
876
+ - `"G0"` starts/ends a hard curve segment at that point
877
+ - `"G1"` keeps the point in a tangent-smooth run
878
+ - `"G2"` keeps the point in a curvature-smooth cubic run
879
+
880
+ Override keys use explicit row-field form: `Curve Name[0].x`, `Curve Name[0].y`, `Curve Name[0].g`, and `Curve Name.__count__`.
881
+
882
+ ```ts
883
+ const spine = Param.spline2d("Handle Spine", [
884
+ { x: 0, y: 0, g: "G2" },
885
+ { x: 35, y: 8, g: "G2" },
886
+ { x: 80, y: 2, g: "G1" },
887
+ ], {
888
+ anchor: Param.anchor.sheetOnXZ([0, -18, 0], { label: "Side-view spine" }),
889
+ });
890
+
891
+ return sweep(circle2d(2), spine.toCurveOnXZ());
892
+ ```
893
+
894
+ **`Spline2DParamOptions`** extends ParamAnchorableOptions: `closed?: boolean`, `degree?: number`, `defaultContinuity?: Spline2DContinuity`, `minPoints?: number`, `maxPoints?: number`, `x?: Partial<Path2DParamAxisDef>`, `y?: Partial<Path2DParamAxisDef>`, `unit?: string`
895
+
896
+ #### `Param.placement2d(name: string, spec: Placement2DParamOptions): Placement2DParamValue` — Declare an editable 2D placement sheet parameter.
897
+
898
+ Use this when the user should arrange named model roles rather than draw geometry: batteries inside an enclosure, rooms across floors, controls on a panel, robot modules on a chassis, or other semantic blockouts.
899
+
900
+ The script declares stable item IDs, footprints, optional rectangular zones, and interaction rules. The returned value exposes named placements as data; the model code decides what those placements mean geometrically.
901
+
902
+ Override keys are item-ID based: `Layout.battery.x`, `Layout.battery.y`, `Layout.battery.angle`, and `Layout.battery.zone`.
903
+
904
+ ```ts
905
+ const layout = Param.placement2d("Internal Layout", {
906
+ frame: { size: [120, 80] },
907
+ items: [
908
+ { id: "battery", footprint: { type: "rect", size: [42, 24] }, at: [-25, 0] },
909
+ { id: "speaker", footprint: { type: "circle", radius: 12 }, at: [32, 8] },
910
+ ],
911
+ rules: { bounds: "prevent", collisions: "warn", snap: 1 },
912
+ anchor: Param.anchor.sheetOnXY([0, 0, 12], { label: "Internal layout" }),
913
+ });
914
+
915
+ const battery = layout.item("battery");
916
+ const batteryPocket = box(42, 24, 6).translate(battery.x, battery.y, 3);
917
+ ```
918
+
919
+ **`Placement2DParamOptions`** extends ParamAnchorableOptions: `frame?: Placement2DFrameInput`, `zones?: Placement2DZoneInput[]`, `items: Placement2DItemInput[]`, `rules?: Partial<Placement2DRulesDef>`, `unit?: string`
920
+
921
+ `Placement2DFrameInput`: `{ width?: number, height?: number, size?: Vec2, center?: Placement2DPointInput, at?: Placement2DPointInput }`
922
+
923
+ `Placement2DZoneInput`: `{ id: string, label?: string, frame?: Placement2DFrameInput }`
924
+
925
+ **`Placement2DItemInput`**: `id: string`, `label?: string`, `footprint: Placement2DFootprintInput`, `at?: Placement2DPointInput`, `center?: Placement2DPointInput`, `angle?: number`, `zone?: string`, `locked?: boolean`
926
+
927
+ `Placement2DRulesDef`: `{ bounds: Placement2DRuleMode, collisions: Placement2DRuleMode, snap: number }`
928
+
683
929
  ### Grouping & Local Coordinates
684
930
 
685
931
  #### `group(...items: GroupInput[]): ShapeGroup` — Group multiple shapes/sketches for joint transforms without merging into a single mesh.
@@ -857,6 +1103,96 @@ Supports transforms (translate, rotate, scale, mirror, transform, rotateAround,
857
1103
  |----------|------|-------------|
858
1104
  | `materialProps` | `ShapeMaterialProps \| undefined` | — |
859
1105
 
1106
+ **Freeform Construction**
1107
+
1108
+ #### `slicePerpendicularToX(x: number, profile: Sketch, options?: FromSlicesAxisSliceOptions): FromSlicesSlice` — Create a slice descriptor perpendicular to the X axis.
1109
+
1110
+ The profile is drawn in the YZ plane. `options.center` is `[y, z]`, so authors can place changing section centers without manually translating sketches in ForgeCAD's internal plane axes.
1111
+
1112
+ ```js
1113
+ Shape.fromSlices([
1114
+ Shape.slicePerpendicularToX(-20, ellipse(10, 2), { center: [0, 3] }),
1115
+ Shape.slicePerpendicularToX(20, ellipse(8, 1.5), { center: [0, 6] }),
1116
+ ]);
1117
+ ```
1118
+
1119
+ **`FromSlicesAxisSliceOptions`**
1120
+ - `center?: FromSlicesVec2` — Plane-local profile center. XY uses [x, y], XZ uses [x, z], YZ uses [y, z].
1121
+
1122
+ #### `slicePerpendicularToY(y: number, profile: Sketch, options?: FromSlicesAxisSliceOptions): FromSlicesSlice` — Create a slice descriptor perpendicular to the Y axis.
1123
+
1124
+ The profile is drawn in the XZ plane. `options.center` is `[x, z]`.
1125
+
1126
+ #### `slicePerpendicularToZ(z: number, profile: Sketch, options?: FromSlicesAxisSliceOptions): FromSlicesSlice` — Create a slice descriptor perpendicular to the Z axis.
1127
+
1128
+ The profile is drawn in the XY plane. `options.center` is `[x, y]`.
1129
+
1130
+ #### `sliceThrough(center: FromSlicesVec3, normal: FromSlicesVec3, profile: Sketch): FromSlicesSlice` — Create a slice descriptor through a world point with an arbitrary plane normal.
1131
+
1132
+ The profile origin lands at `center`. Use this when the section plane is not one of the world XY/XZ/YZ planes.
1133
+
1134
+ #### `sliceOnFrame(frame: FromSlicesFrameInput, profile: Sketch): FromSlicesSlice` — Create a slice descriptor on a full 3D work frame.
1135
+
1136
+ Sheet frame helpers return the right shape for `frame`. Use `Sheet.frameAt()` for tangent construction planes, or `Sheet.framePerpendicularToU()` / `Sheet.framePerpendicularToV()` for cross-sections normal to a surface path. On the Manifold backend, framed slices are lofted in input order when every slice comes from a frame.
1137
+
1138
+ **`FromSlicesFrameInput`**
1139
+
1140
+ | Option | Type | Description |
1141
+ |--------|------|-------------|
1142
+ | `point?` | `FromSlicesVec3` | World-space frame origin. Sheet frame helpers return this as `point`. |
1143
+ | `origin?` | `FromSlicesVec3` | Alias for `point` when using generic CAD frame terminology. |
1144
+ | `normal` | `FromSlicesVec3` | World-space frame normal. |
1145
+ | `tangentU?` | `FromSlicesVec3` | World-space direction for the profile's local X axis. Sheet frame helpers return this as `tangentU`. |
1146
+ | `tangentV?` | `FromSlicesVec3` | Optional world-space direction for the profile's local Y axis. Sheet frame helpers return this as `tangentV`. |
1147
+ | `xAxis?` | `FromSlicesVec3` | Alias for `tangentU`. |
1148
+ | `yAxis?` | `FromSlicesVec3` | Alias for `tangentV`. |
1149
+
1150
+ #### `fromSlices(slices: FromSlicesSlice[], options?: FromSlicesOptions): Shape` — Construct a 3D shape from cross-section slices on one or more planes.
1151
+
1152
+ On the Manifold backend, slices created with `Shape.sliceOnFrame()` are lofted in their input order while preserving each full 3D frame. Other slices with the same normal direction are lofted together. Slices with different normals are combined via smooth radial blending — each silhouette constrains the shape's extent, producing smooth ellipsoidal cross-sections.
1153
+
1154
+ ```js
1155
+ // Egg from two orthogonal silhouettes
1156
+ const eggProfile = ellipse(15, 25);
1157
+ return Shape.fromSlices([
1158
+ { on: 'xz', at: 0, profile: eggProfile },
1159
+ { on: 'yz', at: 0, profile: eggProfile },
1160
+ ]);
1161
+ ```
1162
+
1163
+ ```js
1164
+ // Vase with cross-section transitions
1165
+ return Shape.fromSlices([
1166
+ Shape.slicePerpendicularToZ(0, circle2d(20)),
1167
+ Shape.slicePerpendicularToZ(40, rect(25, 25)),
1168
+ Shape.slicePerpendicularToZ(80, circle2d(8)),
1169
+ Shape.slicePerpendicularToY(0, vaseOutline),
1170
+ ]);
1171
+ ```
1172
+
1173
+ **`FromSlicesSlice`**
1174
+
1175
+ | Option | Type | Description |
1176
+ |--------|------|-------------|
1177
+ | `on` | `SlicePlane` | Plane normal: axis name or arbitrary unit vector. |
1178
+ | `at?` | `number` | Signed offset along the normal from the origin. Omit when `center` defines the plane. |
1179
+ | `center?` | `FromSlicesVec3` | World-space point where the 2D profile origin should land on the slice plane. |
1180
+ | `profile` | `Sketch` | 2D cross-section profile on that plane. |
1181
+ | `frame?` | `FromSlicesFramePlacement` | Full 3D section frame, preserved for ordered lofts through rotating planes. |
1182
+
1183
+ **`FromSlicesFramePlacement`**
1184
+
1185
+ | Option | Type | Description |
1186
+ |--------|------|-------------|
1187
+ | `point` | `FromSlicesVec3` | World-space frame origin. |
1188
+ | `normal` | `FromSlicesVec3` | World-space section normal. |
1189
+ | `tangentU` | `FromSlicesVec3` | World-space direction for the profile's local X axis. |
1190
+ | `tangentV` | `FromSlicesVec3` | World-space direction for the profile's local Y axis. |
1191
+
1192
+ **`FromSlicesOptions`**
1193
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing (Manifold only).
1194
+ - `boundsPadding?: number` — Extra bounding-box padding (Manifold only).
1195
+
860
1196
  **Appearance**
861
1197
 
862
1198
  #### `color(value: string | undefined): Shape` — Set the color of this shape (hex string, e.g. "#ff0000"). Returns a new Shape with the color applied.
@@ -1267,7 +1603,7 @@ cylinder(60, 20).wrapTexture(label, Wrap.aroundCylinder({ axis: 'z' })); // wra
1267
1603
 
1268
1604
  #### `ref(path: string): ShapeRef` — Resolve a semantic reference path like `lid`, `lid/back`, or a midpoint selector on `lid/back`.
1269
1605
 
1270
- #### `thicken(thickness: number): Shape` — Offset-thicken an exact open surface or shell into a solid.
1606
+ #### `thicken(thickness: ThicknessInput): Shape` — Offset-thicken an exact open surface or shell into a solid.
1271
1607
 
1272
1608
  #### `getMesh(): ShapeRuntimeMesh` — Extract triangle mesh for Three.js rendering
1273
1609
 
@@ -1507,57 +1843,200 @@ const bracket = group(
1507
1843
  | `depth?` | `number` | Thread groove depth in millimeters. Default: 0.8. |
1508
1844
  | `underScale?` | `number` | Relative height of the under-crossing thread. Default: 0.15. |
1509
1845
 
1510
- ### `Sheet`
1846
+ ### `Path2DParamValue`
1511
1847
 
1512
- A parametric open surface value (control grid + knots + analytic differential geometry).
1848
+ Runtime value returned by `Param.path2d()`.
1849
+
1850
+ Use closed paths as editable profile outlines via `toSketch()`. Use open paths as editable centerlines via `toStroke(width)`.
1513
1851
 
1514
1852
  **Properties:**
1515
1853
 
1516
1854
  | Property | Type | Description |
1517
1855
  |----------|------|-------------|
1518
- | `surface` | `BSplineSurface` | |
1856
+ | `closed` | `boolean` | True when this path is intended to close back to its first point. |
1519
1857
 
1520
1858
  **Methods:**
1521
1859
 
1522
- #### `get frontEdge(): SheetEdge` — Edge naming follows parameter direction (documented): front=v0, rear=v1, left=u0, right=u1.
1860
+ #### `points(): Vec2[]` — Return the current points as `[x, y]` pairs in the editor coordinate system.
1523
1861
 
1524
- #### `thicken(wall: number, options?: { resolution?: number; }): Shape` — Offset the sheet along its analytic normals into a watertight solid shell of the given wall thickness. Throws if the wall would self-intersect on a concave region (no silent degenerate solid).
1862
+ #### `toSketch(): Sketch` — Convert a closed editable path into a sketch profile.
1525
1863
 
1526
- #### `matchEdge(edge: SheetEdge): MatchEdgeBuilder` Per-edge continuity match against a neighbor (returns a NEW Sheet).
1864
+ This is the common path for hand-edited plates, outlines, and 2D section profiles that will be extruded, subtracted, or used in sketch booleans. Throws for open paths; use `toStroke(width)` for editable centerlines.
1865
+
1866
+ #### `toStroke(width: number, join?: "Round" | "Square"): Sketch` — Convert an editable path into a stroked sketch with physical width.
1867
+
1868
+ Use this for rails, ribs, cable routes, gasket paths, and other open centerlines. Closed paths can also be stroked when the intent is a looped band rather than a filled profile.
1869
+
1870
+ ### `Spline2DParamValue`
1871
+
1872
+ Runtime value returned by `Param.spline2d()`.
1873
+
1874
+ Spline params preserve both editable point coordinates and each point's continuity intent (`G0`, `G1`, or `G2`). Convert them to curves for sweeps and loft rails, or to sampled paths when an API expects plain points.
1875
+
1876
+ **Properties:**
1877
+
1878
+ | Property | Type | Description |
1879
+ |----------|------|-------------|
1880
+ | `closed` | `boolean` | True when this spline is intended to close back to its first point. |
1881
+ | `degree` | `number` | Requested fitting degree before any automatic reduction for short spans. |
1882
+
1883
+ **Methods:**
1884
+
1885
+ #### `points(): Vec2[]` — Return the current control nodes as `[x, y]` pairs without continuity metadata.
1886
+
1887
+ #### `nodes(): Spline2DPointDef[]` — Return the current editable nodes, including per-point `g` continuity values.
1888
+
1889
+ #### `continuities(): Spline2DContinuity[]` — Return only the per-node continuity intents in point order.
1890
+
1891
+ #### `toPolyline(samples?: number): Vec2[]` — Sample the spline into 2D `[x, y]` points.
1892
+
1893
+ Use this when downstream code needs a polyline instead of a curve object.
1894
+
1895
+ #### `toCurveOnXY(z?: number, options?: Spline2DCurveOptions): NurbsCurve3D` — Fit the editable spline as a 3D curve on the XY plane at constant `z`.
1896
+
1897
+ The point's `x` maps to world X and `y` maps to world Y.
1898
+
1899
+ `Spline2DCurveOptions`: `{ degree?: number, tolerance?: number, samples?: number }`
1900
+
1901
+ #### `toCurveOnXZ(y?: number, options?: Spline2DCurveOptions): NurbsCurve3D` — Fit the editable spline as a 3D curve on the XZ plane at constant `y`.
1902
+
1903
+ The point's `x` maps to world X and `y` maps to world Z. This is useful for side-view height/depth profiles such as spoon bowls, handles, and rails.
1904
+
1905
+ #### `toCurveOnYZ(x?: number, options?: Spline2DCurveOptions): NurbsCurve3D` — Fit the editable spline as a 3D curve on the YZ plane at constant `x`.
1906
+
1907
+ The point's `x` maps to world Y and `y` maps to world Z.
1908
+
1909
+ #### `toCurveSegmentsOnXY(z?: number, options?: Spline2DCurveOptions): NurbsCurve3D[]` — Fit the spline on XY and return separate curve segments split at `G0` nodes.
1910
+
1911
+ Use segment output when a hard break should remain visible to downstream code instead of being joined into one continuous curve.
1912
+
1913
+ #### `toCurveSegmentsOnXZ(y?: number, options?: Spline2DCurveOptions): NurbsCurve3D[]` — Fit the spline on XZ and return separate curve segments split at `G0` nodes.
1914
+
1915
+ #### `toCurveSegmentsOnYZ(x?: number, options?: Spline2DCurveOptions): NurbsCurve3D[]` — Fit the spline on YZ and return separate curve segments split at `G0` nodes.
1916
+
1917
+ #### `toPathOnXY(z?: number, options?: Spline2DCurveOptions): Vec3[]` — Sample the spline as 3D points on the XY plane at constant `z`.
1918
+
1919
+ This is useful for sweeps and surface helpers that accept point paths.
1920
+
1921
+ #### `toPathOnXZ(y?: number, options?: Spline2DCurveOptions): Vec3[]` — Sample the spline as 3D points on the XZ plane at constant `y`.
1922
+
1923
+ #### `toPathOnYZ(x?: number, options?: Spline2DCurveOptions): Vec3[]` — Sample the spline as 3D points on the YZ plane at constant `x`.
1924
+
1925
+ ### `Placement2DParamValue`
1926
+
1927
+ Runtime value returned by `Param.placement2d()`.
1928
+
1929
+ Placement sheets return semantic item positions. The model decides what each item means geometrically, so users can drag named blocks without editing the construction code.
1930
+
1931
+ #### `items(): Placement2DItemPlacement[]` — Return all current item placements as immutable copies.
1932
+
1933
+ #### `positions(): Record<string, Placement2DItemPlacement>` — Return current placements keyed by item id for table-style lookup.
1934
+
1935
+ #### `item(id: string): Placement2DItemPlacement` — Return one named item placement.
1936
+
1937
+ Throws if `id` was not declared in the sheet, which keeps model code tied to stable semantic item IDs rather than fragile list indices.
1938
+
1939
+ ### `CurveNetBuilder`
1940
+
1941
+ #### `alongRails(railA: CurveInput, railB: CurveInput): this` — Use two lengthwise boundary curves as guide rails.
1942
+
1943
+ Chain `.sections(...)` to create a bi-rail surface: the rails define the sheet edges while each section curve shapes the cross-span at its station.
1944
+
1945
+ #### `sections(...curves: CurveInput[]): this` — Add crosswise section curves.
1946
+
1947
+ By itself this skins the sections into a surface. After `.alongRails(...)`, the sections are fitted between the two rails so the surface follows both the boundary guide curves and the section profiles.
1948
+
1949
+ #### `resolution(samples: number): this` — Set the sampling resolution used to build curve-family surface grids.
1950
+
1951
+ This affects `.lengthwise(...)`, `.crosswise(...)`, and `.alongRails(...).sections(...)` surfaces. It does not resample explicit `.cage(grid)` input because the cage already is the authored control net.
1952
+
1953
+ #### `matchStartU(condition: BoundaryCondition): this` — Enforce a continuity condition on the `u = 0` (left) boundary.
1954
+
1955
+ Pass `{ edge }` to match an adjacent sheet's tangent (G1) or curvature (G2), or `{ tangent }` to impose an explicit cross-boundary direction. See `BoundaryCondition`.
1956
+
1957
+ **`BoundaryCondition`**
1958
+
1959
+ | Option | Type | Description |
1960
+ |--------|------|-------------|
1961
+ | `edge?` | `SheetEdge` | Match the tangent (G1) and curvature (G2) of an existing sheet edge across this boundary. |
1962
+ | `tangent?` | `Vec3` | Or impose an explicit cross-boundary tangent direction in world space (auto-normalized). |
1963
+ | `tangentScale?` | `number` | Scalar magnitude for the imposed `tangent` ramp, in model units. Ignored when `edge` is given. Default: the local cross-boundary control-span length (chord-scaled), so the imposed tangent has the same strength as the surface already carries — no magic number. |
1964
+ | `continuity?` | `0 \| 1 \| 2` | Continuity order to enforce on this side. Default inferred: 1 if a tangent or edge is given, else 0. G2 (curvature) requires an `edge` to copy the neighbor's second difference. |
1527
1965
 
1528
1966
  **`SheetEdge`**
1529
1967
  - `fixed: "u" | "v"` — Which parameter is held fixed along this edge.
1530
1968
  - `value: 0 | 1` — The fixed value (0 or 1).
1531
1969
  - Also: `sheet: Sheet`.
1532
1970
 
1533
- - `get rearEdge(): SheetEdge`
1534
- - `get leftEdge(): SheetEdge`
1535
- - `get rightEdge(): SheetEdge`
1536
- - `pointAt(u: number, v: number): Vec3`
1537
- - `normalAt(u: number, v: number): Vec3`
1538
- - `curvatureAt(u: number, v: number): SurfaceCurvature`
1971
+ #### `matchEndU(condition: BoundaryCondition): this` — Enforce a continuity condition on the `u = 1` (right) boundary. See `matchStartU`.
1539
1972
 
1540
- ### `CurveNetBuilder`
1973
+ #### `matchStartV(condition: BoundaryCondition): this` — Enforce a continuity condition on the `v = 0` (front) boundary. See `matchStartU`.
1974
+
1975
+ #### `matchEndV(condition: BoundaryCondition): this` — Enforce a continuity condition on the `v = 1` (rear) boundary. See `matchStartU`.
1976
+
1977
+ #### `closedU(): this` — Weld the two ends of the U direction into a tangent-continuous periodic loop, so the `u = 0` and `u = 1` boundaries coincide with NO G0 kink (a closed tube/ring in U — e.g. a bowl's around-rim seam). The cage's first and last U rows must already be coincident (the loop must close in position).
1978
+
1979
+ #### `closedV(): this` — Weld the two ends of the V direction into a tangent-continuous periodic loop. See `closedU`.
1541
1980
 
1542
1981
  #### `toSheet(): Sheet` — Build (once) and return the Sheet.
1543
1982
 
1544
1983
  - `lengthwise(...curves: CurveInput[]): this`
1545
1984
  - `crosswise(...curves: CurveInput[]): this`
1546
- - `alongRails(railA: CurveInput, railB: CurveInput): this`
1547
- - `sections(...curves: CurveInput[]): this`
1548
1985
  - `cage(grid: Vec3[][]): this`
1549
1986
  - `degree(u: number, v: number): this`
1550
1987
  - `get frontEdge(): SheetEdge`
1551
1988
  - `get rearEdge(): SheetEdge`
1552
1989
  - `get leftEdge(): SheetEdge`
1553
1990
  - `get rightEdge(): SheetEdge`
1991
+ - `get frontCurve(): NurbsCurve3D`
1992
+ - `get rearCurve(): NurbsCurve3D`
1993
+ - `get leftCurve(): NurbsCurve3D`
1994
+ - `get rightCurve(): NurbsCurve3D`
1554
1995
  - `get surface(): BSplineSurface`
1555
1996
  - `pointAt(u: number, v: number): Vec3`
1556
1997
  - `normalAt(u: number, v: number): Vec3`
1998
+ - `frameAt(u: number, v: number, options?: SheetFrameOptions): SheetFrame`
1999
+ - `framePerpendicularToU(u: number, v: number, options?: SheetFrameOptions): SheetFrame`
2000
+ - `framePerpendicularToV(u: number, v: number, options?: SheetFrameOptions): SheetFrame`
1557
2001
  - `curvatureAt(u: number, v: number): SurfaceCurvature`
2002
+ - `curveAlong(edge: SheetEdge): NurbsCurve3D`
2003
+ - `curveAlongU(v: number): NurbsCurve3D`
2004
+ - `curveAlongV(u: number): NurbsCurve3D`
2005
+ - `pathAlong(edge: SheetEdge, options?: SheetPathAlongOptions): Vec3[]`
2006
+ - `pathAlongBoundary(spans: SheetBoundaryPathSpan[], options?: SheetBoundaryPathOptions): Vec3[]`
2007
+ - `pathAlongU(v: number, options?: SheetPathAlongOptions): Vec3[]`
2008
+ - `pathAlongV(u: number, options?: SheetPathAlongOptions): Vec3[]`
1558
2009
  - `thicken(wall: number, options?: { resolution?: number; }): Shape`
2010
+ - `thickenInsideBy(thickness: ThicknessInput, options?: { resolution?: number; }): Shape`
1559
2011
  - `matchEdge(edge: SheetEdge): MatchEdgeBuilder`
1560
2012
 
2013
+ **`SheetFrameOptions`**
2014
+ - `normalOffset?: number` — Offset the frame origin along the analytic surface normal. Default 0.
2015
+
2016
+ **`SheetPathAlongOptions`**
2017
+
2018
+ | Option | Type | Description |
2019
+ |--------|------|-------------|
2020
+ | `samples?` | `number` | Samples along the path span. Default 32. |
2021
+ | `start?` | `number` | Normalized start parameter along the path. Default 0. |
2022
+ | `end?` | `number` | Normalized end parameter along the path. Default 1. |
2023
+ | `reverse?` | `boolean` | Return points from end to start after sampling the span. Default false. |
2024
+ | `normalOffset?` | `number` | Offset each path point along the analytic surface normal. Default 0. |
2025
+
2026
+ **`SheetBoundaryPathSpan`**
2027
+
2028
+ | Option | Type | Description |
2029
+ |--------|------|-------------|
2030
+ | `edge` | `SheetEdge` | Boundary edge to sample for this span. |
2031
+ | `start?` | `SheetPathParameter` | Normalized edge parameter or world point projected to the closest edge parameter. Default 0. |
2032
+ | `end?` | `SheetPathParameter` | Normalized edge parameter or world point projected to the closest edge parameter. Default 1. |
2033
+ | `samples?` | `number` | Samples along this edge span. Defaults to options.samplesPerEdge or 32. |
2034
+
2035
+ **`SheetBoundaryPathOptions`**
2036
+ - `samplesPerEdge?: number` — Samples for spans that do not specify their own count. Default 32.
2037
+ - `normalOffset?: number` — Offset each path point along the analytic surface normal. Default 0.
2038
+ - `tolerance?: number` — Maximum allowed gap between adjacent sampled spans. Default 1e-6.
2039
+
1561
2040
  ### `MatchEdgeBuilder`
1562
2041
 
1563
2042
  - `toG0(neighbor: SheetEdge): Sheet`
@@ -2126,7 +2605,22 @@ donut.region([40, 0]).extrude(10); // seed at radius 40, inside the ring
2126
2605
 
2127
2606
  **Promotion**
2128
2607
 
2129
- #### `extrude(height: number, opts?: { twist?: number; divisions?: number; scaleTop?: number | Vec2; }): Shape` — Extrude this 2D sketch along Z to create a 3D solid. Supports twist and scale tapering.
2608
+ #### `extrude(extent: number | EndCondition, opts?: { twist?: number; divisions?: number; scaleTop?: number | Vec2; }): Shape` — Extrude this 2D sketch along Z to create a 3D solid. Supports twist and scale tapering. The extent may be a numeric height or an `EndCondition` (up to a face, plane, or vertex).
2609
+
2610
+ **`EndCondition`**
2611
+
2612
+ | Option | Type | Description |
2613
+ |--------|------|-------------|
2614
+ | `upToFace?` | `FaceRef \| SketchFaceTarget` | Terminate flush with a planar face perpendicular to the feature direction. |
2615
+ | `upToPlane?` | `PlaneOp` | Terminate at an arbitrary plane (ray-plane intersection along the direction). |
2616
+ | `upToVertex?` | `Vec3` | Terminate at the plane through this point, perpendicular to the direction. |
2617
+ | `offset?` | `number` | Signed offset PAST the resolved surface in the travel direction (default 0). |
2618
+
2619
+ `FaceRef` — defined in [core](/docs/core).
2620
+
2621
+ **`PlaneOp`**
2622
+ - `normal: Vec3` — Plane normal. Need not be unit length; it is normalized internally.
2623
+ - `offset?: number` — Signed offset of the plane along its normal from the world origin (default 0).
2130
2624
 
2131
2625
  #### `revolve(degrees?: number, segments?: number): Shape` — Revolve this 2D sketch around the world Z axis. Sketch X is radius; sketch Y becomes world Z height. Keep the profile at X > 0 unless it intentionally touches the axis.
2132
2626
 
@@ -2147,8 +2641,6 @@ const shifted = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left', [5, 0]);
2147
2641
 
2148
2642
  Use this when a 2D profile should be oriented onto a 3D face before extrusion or other downstream operations.
2149
2643
 
2150
- `FaceRef` — defined in [core](/docs/core).
2151
-
2152
2644
  **Labels**
2153
2645
 
2154
2646
  #### `labelEdge(name: string): Sketch` — Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch.
@@ -2709,6 +3201,7 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
2709
3201
  - [Route3D](#route3d)
2710
3202
  - [NurbsCurve3D](#nurbscurve3d)
2711
3203
  - [NurbsSurface](#nurbssurface)
3204
+ - [Sheet](#sheet)
2712
3205
  - [PathBuilder](#pathbuilder) — Line Segments, Arcs, Curves, Closing & Output
2713
3206
  - [ProductSkin](#productskin)
2714
3207
  - [ProductSurfaceRef](#productsurfaceref)
@@ -2732,6 +3225,7 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
2732
3225
  - [Surface](#surface)
2733
3226
  - [Blend](#blend)
2734
3227
  - [Analysis](#analysis)
3228
+ - [Thickness](#thickness)
2735
3229
  - [Product](#product)
2736
3230
  - [Carrier](#carrier)
2737
3231
  - [SurfaceMembers](#surfacemembers)
@@ -2771,17 +3265,45 @@ const rail = Curve.BlendG2(
2771
3265
  **`CurveBlendG2Endpoint`** extends CurveBlendEndpoint
2772
3266
  - `curvature?: Vec3` — Optional endpoint curvature/second-derivative vector. Default is zero.
2773
3267
 
2774
- #### `Curve.Arc(options: CurveArcOptions): NurbsCurve3D` — Create an exact circular 3D arc from start, end, and start tangent.
3268
+ #### `Curve.Bridge(options: CurveBridgeOptions): NurbsCurve3D` — Bridge two existing curve endpoints with inferred tangent or curvature continuity.
2775
3269
 
2776
- The returned curve is a rational quadratic `NurbsCurve3D`, split into stable spans when needed, so it can feed `sweep` without sampling the authoring intent away.
3270
+ This is the Onshape-style "select two curves and bridge them" primitive: ForgeCAD reads the endpoint positions, tangent directions, and NURBS curvature from the selected curves so callers do not hand-author tangent vectors. Use `continuity: 'G1'` for a cubic tangent bridge or the default `G2` for a quintic curvature bridge.
2777
3271
 
2778
3272
  ```js
2779
- const rail = Curve.Arc({
2780
- start: [40, 0, 0],
2781
- end: [0, 40, 0],
2782
- tangent: [0, 1, 0],
3273
+ const leftRim = Curve.Fit([[20, -12, 0], [0, -18, 2], [-30, -14, 1]]);
3274
+ const rightRim = Curve.Fit([[20, 12, 0], [0, 18, 2], [-30, 14, 1]]);
3275
+ const roundedNose = Curve.Bridge({
3276
+ from: { curve: leftRim, at: 'end' },
3277
+ to: { curve: rightRim, at: 'end' },
3278
+ continuity: 'G2',
2783
3279
  });
2784
- const tube = sweep(circle2d(2), rail);
3280
+ ```
3281
+
3282
+ **`CurveBridgeOptions`**
3283
+
3284
+ | Option | Type | Description |
3285
+ |--------|------|-------------|
3286
+ | `from` | `CurveBridgeEndpoint` | Endpoint where the bridge starts. |
3287
+ | `to` | `CurveBridgeEndpoint` | Endpoint where the bridge ends. |
3288
+ | `continuity?` | `CurveBridgeContinuity` | Continuity target. Default `G2`. |
3289
+ | `weight?` | `number` | Tangent reach relative to bridge chord. Default 0.6. |
3290
+
3291
+ **`CurveBridgeEndpoint`**
3292
+ - `curve: CurveTrimInput` — Existing curve endpoint to bridge from or to.
3293
+ - `at?: CurveBridgeEndpointAt` — Which endpoint of the curve to use. Default is `end`.
3294
+ - `weight?: number` — Tangent reach relative to bridge chord. Overrides `CurveBridgeOptions.weight` for this side.
3295
+
3296
+ #### `Curve.Arc(options: CurveArcOptions): NurbsCurve3D` — Create an exact circular 3D arc from start, end, and start tangent.
3297
+
3298
+ The returned curve is a rational quadratic `NurbsCurve3D`, split into stable spans when needed, so it can feed `sweep` without sampling the authoring intent away.
3299
+
3300
+ ```js
3301
+ const rail = Curve.Arc({
3302
+ start: [40, 0, 0],
3303
+ end: [0, 40, 0],
3304
+ tangent: [0, 1, 0],
3305
+ });
3306
+ const tube = sweep(circle2d(2), rail);
2785
3307
  ```
2786
3308
 
2787
3309
  **`CurveArcOptions`**
@@ -2835,6 +3357,52 @@ const loop = Curve.Fit(
2835
3357
  - `tolerance?: number` — Maximum allowed interpolation residual in model units. Default 1e-7.
2836
3358
  - `closed?: boolean` — Interpolate a closed periodic loop through the points. The loop closes from the last point back to the first automatically — do not repeat the first point at the end.
2837
3359
 
3360
+ #### `Curve.ProjectOnSurface(curve: NurbsCurve3D | Vec3[], sheet: Sheet, options?: CurveProjectOnSurfaceOptions): NurbsCurve3D` — Project a curve onto a `Sheet` along the surface normal and return the exact NURBS foot curve.
3361
+
3362
+ This is the "drape a curve onto a surface" primitive: each sampled point of the source curve is inverted onto the sheet (closest surface point via the analytic jet), and the foot points are fitted with `Curve.Fit`. Use it to trace a planned trim line, seam, or graphic onto a freeform panel, then sweep or trim from the real on-surface curve.
3363
+
3364
+ The source may be an exact `NurbsCurve3D` or a `Vec3[]` polyline (for example a placed 2D path). With `{ closed: true }` the result is a periodic loop, for draping a closed boundary onto the sheet. Set `maxGap` to require that every sample lands within a distance of the surface; the call throws if any sample misses (no silent fallback).
3365
+
3366
+ ```js
3367
+ const panel = Surface.Net().cage(cage);
3368
+ const guide = Curve.Line([-20, 0, 30], [20, 0, 30]);
3369
+ const seam = Curve.ProjectOnSurface(guide, panel);
3370
+ const bead = sweep(circle2d(0.6), seam);
3371
+ ```
3372
+
3373
+ **`CurveProjectOnSurfaceOptions`**
3374
+
3375
+ | Option | Type | Description |
3376
+ |--------|------|-------------|
3377
+ | `samples?` | `number` | Coarse samples taken along the source curve before per-sample projection. Default 64. |
3378
+ | `refineIterations?` | `number` | Newton refinement iterations of the (u, v) foot-point per sample. Default 8. |
3379
+ | `tolerance?` | `number` | Interpolation tolerance passed to Curve.Fit for the result. Default 1e-4. |
3380
+ | `closed?` | `boolean` | Fit a closed periodic loop (use when the source curve is a closed loop). Default false. |
3381
+ | `maxGap?` | `number` | Max allowed projection gap; throws if any sample lands farther than this from the surface foot. Default Infinity (no gate). |
3382
+
3383
+ #### `Curve.Intersect(sheetA: Sheet, sheetB: Sheet, options?: CurveIntersectOptions): NurbsCurve3D[]` — Intersect two `Sheet` surfaces and return the exact NURBS intersection branches.
3384
+
3385
+ This is curve-following surface-surface intersection (SSI): a transversal marcher follows each intersection branch using the analytic surface jets (the intersection tangent is `cross(normalA, normalB)`), converging every step onto both surfaces, then fits each branch with `Curve.Fit`. A single pair of sheets can intersect in several disjoint curves, so the result is an array (empty when the sheets do not meet).
3386
+
3387
+ Only transversal intersections (where the surfaces cross and the intersection tangent `cross(normalA, normalB)` is well defined) are detected. A tangential / grazing contact (where the surface normals are parallel and the tangent is undefined) is not marched and may be reported as no-intersection (an empty array) — tangential SSI is a documented follow-on, not a silent fallback.
3388
+
3389
+ ```js
3390
+ const wing = Surface.Net().cage(wingCage);
3391
+ const rib = Surface.Net().cage(ribCage);
3392
+ const [seam] = Curve.Intersect(wing, rib);
3393
+ const weld = sweep(circle2d(0.5), seam);
3394
+ ```
3395
+
3396
+ **`CurveIntersectOptions`**
3397
+
3398
+ | Option | Type | Description |
3399
+ |--------|------|-------------|
3400
+ | `samples?` | `number` | Grid resolution per axis for seeding the marcher (samples x samples on sheetA). Default 48. |
3401
+ | `step?` | `number` | Marching step as a fraction of sheetA's average sample spacing. Default 0.5. |
3402
+ | `refineIterations?` | `number` | Newton iterations to converge each marched point onto both surfaces. Default 12. |
3403
+ | `tolerance?` | `number` | Convergence/coincidence tolerance in model units. Default 1e-4. |
3404
+ | `fitTolerance?` | `number` | Fit tolerance passed to Curve.Fit. Default 1e-3. |
3405
+
2838
3406
  #### `Curve.Trim<T extends CurveTrimInput>(curve: T, start: number, end: number): CurveTrimOutput<T>` — Extract an exact curve segment from normalized parameter `start` to `end`.
2839
3407
 
2840
3408
  `NurbsCurve3D` inputs are trimmed with exact knot insertion/subdomain extraction. Polyline point arrays are trimmed by arclength over their exact line segments. Sampled `Curve3D` splines are rejected until ForgeCAD has a tolerance-controlled rebuild path.
@@ -2843,6 +3411,67 @@ const loop = Curve.Fit(
2843
3411
 
2844
3412
  `NurbsCurve3D` inputs reverse control points, weights, and knots. Polyline point arrays are cloned and reversed. Sampled `Curve3D` splines are rejected until ForgeCAD has a tolerance-controlled rebuild path.
2845
3413
 
3414
+ #### `Curve.closestParameter(curve: CurveClosestParameterInput, point: Vec3, options?: CurveClosestParameterOptions): number` — Find the normalized parameter on an exact curve closest to a world point.
3415
+
3416
+ This is the query companion to `Curve.Trim()`: use it to locate where a construction point lands on an existing rail, surface boundary, or edge curve, then trim/sweep from that real geometric station instead of copying the original construction points.
3417
+
3418
+ `NurbsCurve3D` inputs are searched by curve parameter with coarse sampling plus local refinement. Polyline point arrays are projected onto their exact line segments and return an arclength-normalized parameter.
3419
+
3420
+ ```js
3421
+ const start = Curve.closestParameter(sheet.frontCurve, [-20, -12, 4]);
3422
+ const end = Curve.closestParameter(sheet.frontCurve, [20, -12, 4]);
3423
+ const rimRail = Curve.Trim(sheet.frontCurve, start, end);
3424
+ ```
3425
+
3426
+ **`CurveClosestParameterOptions`**
3427
+ - `samples?: number` — Coarse samples before local refinement. Default 96.
3428
+
3429
+ #### `Curve.join(curves: CurveJoinInput[], options?: CurveJoinOptions): Vec3[]` — Join touching curve segments into one sampled sweep path.
3430
+
3431
+ This is the composite-curve primitive for downstream features that need to follow several real edges as one path: rolled rims, seam beads, trim strips, weld beads, and surface-boundary inlays. ForgeCAD validates that adjacent segment endpoints touch; it will not silently reverse or bridge gaps.
3432
+
3433
+ ```js
3434
+ const rimPath = Curve.join([
3435
+ Curve.Reverse(Curve.Trim(sheet.frontCurve, 0, u)),
3436
+ sheet.leftCurve,
3437
+ Curve.Trim(sheet.rearCurve, 0, u),
3438
+ ]);
3439
+ const rim = sweep(ellipse(0.8, 0.35), rimPath);
3440
+ ```
3441
+
3442
+ **`CurveJoinOptions`**
3443
+ - `samples?: number` — Points sampled per exact curve segment. Default 32.
3444
+ - `tolerance?: number` — Maximum allowed gap between adjacent segment endpoints. Default 1e-6.
3445
+
3446
+ #### `Curve.placeOnXY(path: CurvePath2D, z?: number, options?: CurvePathPlacementOptions): Vec3[]` — Place a 2D path onto the XY plane at world Z.
3447
+
3448
+ The returned 3D point array can feed `Surface.Net`, `Curve.Fit`, `sweep`, `Curve.Trim`, and any other curve consumer that accepts points.
3449
+
3450
+ ```js
3451
+ const rail = Curve.placeOnXY(path().moveTo(0, 0).bezierTo(30, 8, 60, -4, 90, 0), 12);
3452
+ const smoothRail = Curve.Fit(rail);
3453
+ ```
3454
+
3455
+ **`CurvePathPlacementOptions`**
3456
+ - `samples?: number` — Optional sample count when [`path`](/docs/sketch#path) is a path-like object.
3457
+
3458
+ #### `Curve.placeOnXZ(path: CurvePath2D, y?: number, options?: CurvePathPlacementOptions): Vec3[]` — Place a 2D path onto the XZ plane at world Y.
3459
+
3460
+ The path's first coordinate becomes X and the second becomes Z.
3461
+
3462
+ #### `Curve.placeOnYZ(path: CurvePath2D, x?: number, options?: CurvePathPlacementOptions): Vec3[]` — Place a 2D path onto the YZ plane at world X.
3463
+
3464
+ The path's first coordinate becomes Y and the second becomes Z. This is the direct "sketch cross-sections on offset planes" primitive for surface nets.
3465
+
3466
+ #### `Curve.placeOnPlane(path: CurvePath2D, options: CurvePlanePlacementOptions): Vec3[]` — Place a 2D path onto an arbitrary world plane.
3467
+
3468
+ `origin` is the 2D sketch origin in world space. `xAxis` and `yAxis` are perpendicular world directions for the local sketch axes.
3469
+
3470
+ **`CurvePlanePlacementOptions`** extends CurvePathPlacementOptions
3471
+ - `origin: Vec3` — World-space origin of the 2D sketch plane.
3472
+ - `xAxis: Vec3` — World-space direction for the sketch X axis.
3473
+ - `yAxis: Vec3` — World-space direction for the sketch Y axis.
3474
+
2846
3475
  #### `Curve.Route: typeof Route3D` — Build analytic 3D line/arc routes for sweeps.
2847
3476
 
2848
3477
  `Curve.Route.fromPolyline()` is the canonical route API. It returns a `Route3D` value object, preserving exact route segments, named port frames, and the lowerable `route3d` sweep compile plan.
@@ -2941,7 +3570,7 @@ A closed spline (default) returns a filled profile. An open spline requires a st
2941
3570
  | `strokeWidth?` | `number` | For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. |
2942
3571
  | `join?` | `"Round" \| "Square"` | Stroke join for open splines. Default 'Round'. |
2943
3572
 
2944
- #### `loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape` — Loft between multiple sketches along Z stations.
3573
+ #### `loft(profiles: Sketch[], heights: (number | EndCondition)[], options?: LoftOptions): Shape` — Loft between multiple sketches along Z stations.
2945
3574
 
2946
3575
  Profiles can differ in topology and vertex count: interpolation is done on signed-distance fields and meshed with level-set extraction. Heights must be strictly increasing. Compatible loft stacks can also stay on the maintained export-backend path.
2947
3576
 
@@ -2949,6 +3578,8 @@ The surface is smooth through 3+ stations (C1 spanwise interpolation, like CAD l
2949
3578
 
2950
3579
  Performance note: loft is significantly heavier than primitive/extrude/revolve. If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().
2951
3580
 
3581
+ `EndCondition` — defined in [sketch](/docs/sketch).
3582
+
2952
3583
  #### `sweep(profile: Sketch, path: SweepPathInput, options?: SweepOptions): Shape`
2953
3584
 
2954
3585
  **`SweepOptions`**
@@ -3128,6 +3759,95 @@ Uses Algorithm A2.3 basis-function derivatives with the rational quotient rule,
3128
3759
 
3129
3760
  #### `tessellate(resU?: number, resV?: number): { positions: Vec3[]; normals: Vec3[]; indices: number[]; }` — Tessellate the surface into a triangle mesh. Returns positions, normals, and triangle indices.
3130
3761
 
3762
+ ### `Sheet`
3763
+
3764
+ A parametric open surface value (control grid + knots + analytic differential geometry).
3765
+
3766
+ **Properties:**
3767
+
3768
+ | Property | Type | Description |
3769
+ |----------|------|-------------|
3770
+ | `surface` | `BSplineSurface` | — |
3771
+
3772
+ **Methods:**
3773
+
3774
+ #### `get frontEdge(): SheetEdge` — Edge naming follows parameter direction (documented): front=v0, rear=v1, left=u0, right=u1.
3775
+
3776
+ #### `get frontCurve(): NurbsCurve3D` — Exact curve along the front boundary (`v = 0`).
3777
+
3778
+ #### `get rearCurve(): NurbsCurve3D` — Exact curve along the rear boundary (`v = 1`).
3779
+
3780
+ #### `get leftCurve(): NurbsCurve3D` — Exact curve along the left boundary (`u = 0`).
3781
+
3782
+ #### `get rightCurve(): NurbsCurve3D` — Exact curve along the right boundary (`u = 1`).
3783
+
3784
+ #### `curveAlong(edge: SheetEdge): NurbsCurve3D` — Extract an exact NURBS iso-curve from one of this sheet's boundary edges.
3785
+
3786
+ Use this when a downstream feature should be driven by the actual sheet boundary, such as a swept rim, seam bead, trim strip, or adjacent blend.
3787
+
3788
+ `SheetEdge` — defined in [core](/docs/core).
3789
+
3790
+ #### `curveAlongU(vInput: number): NurbsCurve3D` — Extract an exact NURBS iso-curve in the sheet U direction at fixed `v`.
3791
+
3792
+ Use this for centerline rails, ribs, beads, and trim features that live on the interior of a freeform sheet instead of on a boundary edge.
3793
+
3794
+ #### `curveAlongV(uInput: number): NurbsCurve3D` — Extract an exact NURBS iso-curve in the sheet V direction at fixed `u`.
3795
+
3796
+ Use this for cross-surface rails, seam lines, straps, and inspection paths that should be driven by the sheet parameterization rather than global axes.
3797
+
3798
+ #### `pathAlong(edge: SheetEdge, options?: SheetPathAlongOptions): Vec3[]` — Sample a world-space path along a sheet boundary edge.
3799
+
3800
+ Use this when a downstream feature should follow the real surface edge but does not need to stay an exact NURBS curve, for example a lip, gasket bead, trim strip, weld seam, or inlay lifted off the surface by `normalOffset`.
3801
+
3802
+ `SheetPathAlongOptions` — defined in [core](/docs/core).
3803
+
3804
+ #### `pathAlongBoundary(spans: SheetBoundaryPathSpan[], options?: SheetBoundaryPathOptions): Vec3[]` — Sample one connected path across ordered sheet boundary edge spans.
3805
+
3806
+ This is the composite-boundary primitive for rolled rims, weld beads, gaskets, trim strips, and inlays that follow several sheet edges as one continuous rail. Span `start` / `end` values may be normalized parameters or world points; world points are resolved to the closest point on that edge so callers do not have to manually invoke `Curve.closestParameter()`.
3807
+
3808
+ `SheetBoundaryPathSpan` — defined in [core](/docs/core).
3809
+
3810
+ `SheetBoundaryPathOptions` — defined in [core](/docs/core).
3811
+
3812
+ #### `pathAlongU(vInput: number, options?: SheetPathAlongOptions): Vec3[]` — Sample a world-space path in the sheet U direction at fixed `v`.
3813
+
3814
+ Unlike `curveAlongU()`, this can lift points along the analytic normal with `normalOffset`, which is the common path for swept ribs, inlays, and raised details on a freeform carrier surface.
3815
+
3816
+ #### `pathAlongV(uInput: number, options?: SheetPathAlongOptions): Vec3[]` — Sample a world-space path in the sheet V direction at fixed `u`.
3817
+
3818
+ Use this for lifted cross-surface features where a sampled path is more useful than an exact iso-curve, for example straps, grooves, or probes.
3819
+
3820
+ #### `frameAt(uInput: number, vInput: number, options?: SheetFrameOptions): SheetFrame` — Build an orthonormal local work frame on the sheet.
3821
+
3822
+ This is the "construct tangent plane/frame on a surface" primitive for downstream sketches, profiles, fixtures, inspection probes, and features that must be oriented from the sheet itself rather than global axes.
3823
+
3824
+ `SheetFrameOptions` — defined in [core](/docs/core).
3825
+
3826
+ #### `framePerpendicularToU(uInput: number, vInput: number, options?: SheetFrameOptions): SheetFrame` — Build a section work frame perpendicular to the sheet's U direction.
3827
+
3828
+ This is the "plane normal to path" primitive for ribs, raised handles, bosses, and other details whose cross-sections should be sketched across a carrier surface while marching along the surface U direction. The returned frame can be passed directly to `Shape.sliceOnFrame()`.
3829
+
3830
+ #### `framePerpendicularToV(uInput: number, vInput: number, options?: SheetFrameOptions): SheetFrame` — Build a section work frame perpendicular to the sheet's V direction.
3831
+
3832
+ Use this when the guide path runs in the surface V direction and the sketch profile should span the U direction while its height follows the surface normal. The returned frame can be passed directly to `Shape.sliceOnFrame()`.
3833
+
3834
+ #### `toShape(options?: { resolution?: number; }): Shape` — Return this open sheet as a renderable surface Shape.
3835
+
3836
+ #### `thicken(wall: number, options?: { resolution?: number; }): Shape` — Offset the sheet along its analytic normals into a watertight solid shell of the given wall thickness. Throws if the wall would self-intersect on a concave region (no silent degenerate solid).
3837
+
3838
+ #### `thickenInsideBy(thickness: ThicknessInput, options?: { resolution?: number; }): Shape` — Thicken this sheet inward by a scalar UV thickness field.
3839
+
3840
+ Numeric input delegates to `Sheet.thicken()`. `Thickness.*` fields produce a sampled solid because a variable normal offset is not generally an exact NURBS surface.
3841
+
3842
+ #### `matchEdge(edge: SheetEdge): MatchEdgeBuilder` — Per-edge continuity match against a neighbor (returns a NEW Sheet).
3843
+
3844
+ - `get rearEdge(): SheetEdge`
3845
+ - `get leftEdge(): SheetEdge`
3846
+ - `get rightEdge(): SheetEdge`
3847
+ - `pointAt(u: number, v: number): Vec3`
3848
+ - `normalAt(u: number, v: number): Vec3`
3849
+ - `curvatureAt(u: number, v: number): SurfaceCurvature`
3850
+
3131
3851
  ### `PathBuilder`
3132
3852
 
3133
3853
  **Line Segments**
@@ -3822,7 +4542,7 @@ Canonical exact/smooth 3D curve constructors.
3822
4542
 
3823
4543
  `Curve.*` is the public home for reference curves and route centerlines that feed `sweep`, `variableSweep`, route visualization, and future path consumers. Standalone 3D curve constructors have been collapsed into this namespace.
3824
4544
 
3825
- Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Blend`, `Curve.BlendG2`, `Curve.Arc`, `Curve.Line`, `Curve.Nurbs`, `Curve.Fit`, `Curve.Trim`, `Curve.Reverse`, `Curve.Route`, `Curve.Helix`.
4545
+ Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Blend`, `Curve.BlendG2`, `Curve.Bridge`, `Curve.Arc`, `Curve.Line`, `Curve.Nurbs`, `Curve.Fit`, `Curve.ProjectOnSurface`, `Curve.Intersect`, `Curve.Trim`, `Curve.Reverse`, `Curve.closestParameter`, `Curve.join`, `Curve.placeOnXY`, `Curve.placeOnXZ`, `Curve.placeOnYZ`, `Curve.placeOnPlane`, `Curve.Route`, `Curve.Helix`.
3826
4546
 
3827
4547
  ### `Surface`
3828
4548
 
@@ -3831,6 +4551,31 @@ Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Ble
3831
4551
  - `Cone(options: SurfaceConeOptions): Shape` — Create a finite analytic conical or frustum sheet, optionally bounded by start/end angles.
3832
4552
  - `Sphere(options: SurfaceSphereOptions): Shape` — Create a finite analytic spherical sheet bounded by longitude and latitude ranges.
3833
4553
  - `Torus(options: SurfaceTorusOptions): Shape` — Create a finite analytic torus sheet bounded by major and tube angle ranges.
4554
+ - `Sweep(profile: SurfaceSweepProfileInput, spine: SweepPathInput, options?: SurfaceSweepOptions): Sheet` — Sweep an open 2D profile path along a 3D spine to create an open surface sheet.
4555
+
4556
+ This is the surface-first counterpart to the solid `sweep()` function. Use it for class-A/product workflows where the shape starts as an infinitely thin carrier sheet, then becomes physical material with `.thicken(...)`. The profile's local X axis maps across the sheet, local Y maps along the swept frame's up/normal direction, and the spine direction becomes sheet U.
4557
+
4558
+ ```js
4559
+ const sideProfile = Curve.Fit(Curve.placeOnXZ(path().moveTo(0, 0).bezierTo(40, -8, 90, 8, 140, 3)));
4560
+ const crossSection = path().moveTo(-20, 2).bezierTo(-8, -4, 8, -4, 20, 2);
4561
+ const sheet = Surface.Sweep(crossSection, sideProfile);
4562
+ const thinPart = sheet.thicken(1.2);
4563
+ ```
4564
+ - `Loft(input: SurfaceLoftInput): Sheet` — Loft an open surface sheet through ordered profile stations.
4565
+
4566
+ This is the surface-first counterpart to solid lofts: profiles are open 2D section curves, point stations intentionally collapse a smooth tip, and guide curves can pin named connection landmarks such as rims or low points. The result is an open `Sheet`; call `.thicken(...)` to make physical material.
4567
+
4568
+ ```js
4569
+ const sheet = Surface.Loft({
4570
+ axis: 'X',
4571
+ profiles: [
4572
+ { at: -40, point: [-40, 0, 0] },
4573
+ { at: 0, profile: path().moveTo(-12, 2).bezierTo(-4, -4, 4, -4, 12, 2) },
4574
+ { at: 40, profile: path().moveTo(-5, 0).lineTo(5, 0) },
4575
+ ],
4576
+ connections: [{ name: 'lowPoint', profileParameter: 0.5 }],
4577
+ });
4578
+ ```
3834
4579
  - `Nurbs(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape` — Create an exact NURBS surface from a grid of control points.
3835
4580
 
3836
4581
  The control grid is indexed as `controlGrid[u][v]` — each row is a curve in the V direction, and columns trace curves in the U direction. With default options this builds a bicubic non-rational B-spline sheet with uniform clamped knots; `NurbsSurfaceOptions` controls degrees, weights, knots, trim loops, tessellation, domain, and an optional `thickness` to return a thin solid instead of an open sheet.
@@ -3856,20 +4601,109 @@ Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Ble
3856
4601
  const panel = Surface.Patch({ bottom, top, left, right }).thicken(1.5);
3857
4602
  ```
3858
4603
  - `Boundary(input: SurfaceBoundaryInput): Shape`
3859
- - `Fill(input: SurfaceFillInput): Shape`
4604
+ - `Fill(input: SurfaceFillInput): Shape` — Create an n-sided open surface sheet from 3 or more boundary curves (energy-minimizing constrained fill).
4605
+
4606
+ Boundaries form a closed loop of any size (n >= 3). They are exact by default: pass `NurbsCurve3D` values or `Shape.edge()` refs, or set `{ approximate: true }` to accept sampled `Curve3D`/`Vec3[]` boundaries. Use `match` to make a named boundary G0/G1/G2-tangent to a neighboring face. The result is an open sheet — call `.thicken(t)` for a thin solid.
4607
+
4608
+ Two optional fields pull the fill onto interior features (OCCT backend only; the Truck/SDF backends reject them):
4609
+
4610
+ - `through` — interior constraint curves the surface must pass through (not part of the boundary loop). These are matched positionally (G0) only; G1/G2 on a free interior curve is not honored, so a non-`'G0'` `continuity` throws.
4611
+ - `points` — isolated interior points the surface must interpolate.
4612
+
4613
+ ```js
4614
+ const skin = Surface.Fill({
4615
+ boundaries: [
4616
+ { name: 'a', curve: edgeA }, { name: 'b', curve: edgeB },
4617
+ { name: 'c', curve: edgeC }, { name: 'd', curve: edgeD }, { name: 'e', curve: edgeE },
4618
+ ],
4619
+ match: { a: { target: neighbor.edge('top'), continuity: 'G1' } },
4620
+ through: [{ curve: interiorSpine }], // pull the fill onto an internal feature line
4621
+ points: [[10, 10, 4]], // pin an interior bump
4622
+ });
4623
+ ```
3860
4624
  - `Sew(shapes: Shape[], options?: { tolerance?: number; }): Shape`
3861
4625
  - `Solid(input: Shape | Shape[], options?: SurfaceSolidOptions): Shape` — Sew surface faces or consume an existing sewn shell and make a solid B-rep.
3862
4626
  - `Extend(shape: Shape, options: SurfaceExtendOptions): Shape`
3863
4627
  - `Trim(shape: Shape, tool: Shape | SurfacePlaneOp): Shape`
3864
4628
  - `Split(shape: Shape, tool: Shape | SurfacePlaneOp): [ Shape, Shape ]`
3865
4629
  - `Match(shape: Shape, options: { edge: "u0" | "u1" | "v0" | "v1"; target: EdgeRef; continuity?: SurfaceContinuity; }): Shape`
3866
- - `Net(): CurveNet` — Begin a curve-network (Gordon) surface — the class-A keystone. Chain `.lengthwise(...)/.crosswise(...)` (or `.alongRails(a,b).sections(...)`, or `.cage(grid)`), then `.thicken(wall)` to get a solid Shape. Returns a fluent [`Sheet`](/docs/core#sheet) builder with analytic point/normal/curvature queries and named edges.
4630
+ - `Net(): CurveNet` — Begin a curve-network (Gordon) surface — the class-A keystone. Chain `.lengthwise(...)/.crosswise(...)` (or `.alongRails(a,b).sections(...)`, or `.cage(grid)`), then `.thicken(wall)` to get a solid Shape. Returns a fluent `Sheet` builder with analytic point/normal/curvature queries and named edges.
4631
+ - `BoundaryNet(): CurveNet` — Begin a **Boundary Surface** — the canonical class-A surfacing primitive (SolidWorks "Boundary Boss/Base", Onshape "Boundary surface", Rhino NetworkSrf). It fills a curve cage exactly like `Net` (same Gordon interior), then adds the two foundational boundary capabilities a real boundary surface has:
4632
+
4633
+ 1. **Per-side continuity** — `.matchStartU/.matchEndU/.matchStartV/.matchEndV` enforce G0/G1/G2 across a side, either matching an adjacent sheet's edge (tangent/curvature) or imposing an explicit cross-boundary tangent.
4634
+ 2. **Closed / periodic form** — `.closedU()/.closedV()` weld a direction's two ends into a tangent-continuous loop, so a closed-rim surface (e.g. a bowl's around-rim seam) is smooth with NO G0 kink.
4635
+
4636
+ Returns the same fluent builder as `Net` (cage/families/degree plus the new boundary methods); finish with `.toSheet()` or `.thicken(wall)`. Verify a matched seam with `Analysis.EdgeMatch`.
4637
+
4638
+ ```js
4639
+ // Smooth closed-rim dished bowl: a closed rim + radial sections, welded.
4640
+ const bowl = Surface.BoundaryNet()
4641
+ .lengthwise(...radialSections) // rim -> center, one per around-rim station
4642
+ .closedV() // weld the around-rim seam (no kink)
4643
+ .thicken(1.2);
4644
+ ```
4645
+
4646
+ ```js
4647
+ // Match a fender panel to the hood with curvature (G2) continuity.
4648
+ const fender = Surface.BoundaryNet()
4649
+ .lengthwise(sill, belt, crown)
4650
+ .crosswise(nose, aPillar)
4651
+ .matchStartV({ edge: hood.rearEdge, continuity: 2 })
4652
+ .toSheet();
4653
+ ```
4654
+
4655
+ U-sides then V-sides, so clean continuity is guaranteed along edge interiors, not exactly at the four corners (same limitation as `Sheet.matchEdge`).
3867
4656
 
3868
4657
  ### `Blend`
3869
4658
 
3870
- - `Edge(options: BlendEdgeOptions): Shape`
4659
+ - `Edge(options: BlendEdgeOptions): Shape` — Fillet one or more edges with constant or variable radius.
4660
+
4661
+ Pass `variableRadius` for a tapered or station-law blend — it overrides the constant `radius`. Variable radius requires `--backend occt`.
4662
+
4663
+ Orientation caveat: for a linear `{ start, end }` taper, `start` maps to the matched edge's first vertex (u=0), whose orientation is not guaranteed to match the named edge's start. If the taper runs the wrong way, pass a `stations` law (explicit `at` positions) or swap `start`/`end`.
4664
+
4665
+ ```js
4666
+ // Constant radius
4667
+ let b = box(40, 20, 10)
4668
+ b = Blend.Edge({ edges: [b.edge('top-front')], radius: 3 })
4669
+ ```
4670
+
4671
+ ```js
4672
+ // Linear taper from 1mm to 5mm along the edge
4673
+ b = Blend.Edge({ edges: [b.edge('top-front')], variableRadius: { start: 1, end: 5 } })
4674
+ ```
4675
+
4676
+ ```js
4677
+ // Station law — bulge in the middle
4678
+ b = Blend.Edge({ edges: [b.edge('top-front')], variableRadius: {
4679
+ stations: [{ at: 0, radius: 1 }, { at: 0.5, radius: 4 }, { at: 1, radius: 1 }] } })
4680
+ ```
3871
4681
  - `Surface(options: BlendSurfaceOptions): Shape`
3872
- - `Bridge(edgeA: SheetEdge, edgeB: SheetEdge): BridgeBuilder` — Build a transition strip between two `Surface.Net` sheet edges. Chain `.bulge(a, b)` then `.g0()/.g1()/.g2()` for the continuity order. Returns a [`Sheet`](/docs/core#sheet); verify the seam with `Analysis.EdgeMatch`.
4682
+ - `Bridge(edgeA: SheetEdge, edgeB: SheetEdge): BridgeBuilder` — Build a transition strip between two `Surface.Net` sheet edges. Chain `.bulge(a, b)` then `.g0()/.g1()/.g2()` for the continuity order. Returns a `Sheet`; verify the seam with `Analysis.EdgeMatch`.
4683
+ - `Face(options: BlendFaceOptions): Shape` — Fillet every edge SHARED by a pair of faces — a "face fillet". Resolves the shared edges of the two faces and rolls a constant or variable-radius blend along all of them. Requires `--backend occt`.
4684
+
4685
+ ```js
4686
+ let body = box(80, 50, 30).faces({ lid: 'top', wall: 'side-left' })
4687
+ body = Blend.Face({ faces: [body.face('lid'), body.face('wall')], radius: 4 })
4688
+ ```
4689
+
4690
+ ```js
4691
+ // Variable radius along the shared edges
4692
+ body = Blend.Face({ faces: [body.face('lid'), body.face('wall')],
4693
+ variableRadius: { start: 2, end: 6 } })
4694
+ ```
4695
+ - `FullRound(options: BlendFullRoundOptions): Shape` — Full round — roll a blend over a narrow center face so it is consumed and its two neighbouring faces meet tangentially (classic 3-face full round). The radius defaults to half the center-face span. Requires `--backend occt`.
4696
+
4697
+ ```js
4698
+ let bar = box(60, 8, 20).faces({ topRail: 'top', left: 'side-left', right: 'side-right' })
4699
+ bar = Blend.FullRound({ centerFace: bar.face('topRail') })
4700
+ ```
4701
+
4702
+ ```js
4703
+ // Explicit neighbours
4704
+ bar = Blend.FullRound({ centerFace: bar.face('topRail'),
4705
+ sideFaces: [bar.face('left'), bar.face('right')] })
4706
+ ```
3873
4707
 
3874
4708
  ### `Analysis`
3875
4709
 
@@ -3880,6 +4714,14 @@ Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Ble
3880
4714
  - `SurfaceHealth(shape: Shape, options?: { tinyEdgeThreshold?: number; sliverThreshold?: number; }): SurfaceHealthReport`
3881
4715
  - `BRepValidity(shape: Shape, options?: BRepValidityOptions): BRepValidityReport` — Validate B-rep/shell/solid structure and return closedness, manifoldness, orientation, and issue diagnostics.
3882
4716
 
4717
+ ### `Thickness`
4718
+
4719
+ - `constant: (thickness: number) => ThicknessField` — Use the same wall thickness everywhere on the sheet.
4720
+ - `alongU: (profile: ThicknessStation[] | ThicknessStationProfile) => ThicknessField` — Vary wall thickness across the sheet U direction.
4721
+ - `alongV: (profile: ThicknessStation[] | ThicknessStationProfile) => ThicknessField` — Vary wall thickness across the sheet V direction.
4722
+ - `grid: (values: number[][], options?: ThicknessGridOptions) => ThicknessField` — Bilinearly interpolate wall thickness from a rectangular UV grid.
4723
+ - `nurbs: (values: number[][], options?: ThicknessNurbsOptions) => ThicknessField` — Interpolate wall thickness from a scalar tensor-product B-spline over sheet UV.
4724
+
3883
4725
  ### `Product`
3884
4726
 
3885
4727
  - `skin(name: string): ProductSkinBuilder` — Start a named product skin builder.
@@ -3998,12 +4840,14 @@ Assembly-owned links, constraints, connectors, solved poses, and source-level si
3998
4840
 
3999
4841
  #### `Sim.body(options: SimBodyOptions): SimBodyDef` — Describe one assembly part as a physical body with mass/density, material, collider intent, and optional contact surfaces.
4000
4842
 
4001
- **`SimBodyOptions`**: `massKg?: number`, `densityKgM3?: number`, `material?: SimMaterialDef`, `collider?: SimColliderDef`, `contacts?: Record<string, SimContactDef>`
4843
+ **`SimBodyOptions`**: `massKg?: number`, `densityKgM3?: number`, `material?: SimMaterialDef`, `collider?: SimColliderDef`, `contacts?: Record<string, SimContactDef>`, `motion?: SimMotionDef`
4002
4844
 
4003
- `SimColliderDef`: `{ kind: "collider", mode: SimColliderMode, reason?: string }`
4845
+ `SimColliderDef`: `{ kind: "collider", mode: SimColliderMode, reason?: string, sdfResolution?: number }`
4004
4846
 
4005
4847
  `SimContactDef`: `{ kind: "wheelSurface" | "gripperSurface", connectorName: string }`
4006
4848
 
4849
+ `SimMotionDef`: `{ kind: "motion", mode: SimMotionMode }`
4850
+
4007
4851
  `SimBodyDef`: `{ kind: "body" }`
4008
4852
 
4009
4853
  #### `Sim.collider` — Collision-geometry intent constructors for physical parts.
@@ -4011,8 +4855,15 @@ Assembly-owned links, constraints, connectors, solved poses, and source-level si
4011
4855
  - `Sim.collider.convexHull(): SimColliderDef` — Use a generated collision mesh for the part. This is the default fast rigid-body collider for irregular parts.
4012
4856
  - `Sim.collider.boundingBox(): SimColliderDef` — Use the part bounding box as the collision geometry. This is fastest and works well for chassis and simple blocks.
4013
4857
  - `Sim.collider.visualMesh(): SimColliderDef` — Use the visual mesh as collision geometry. This is exact but usually slower in physics engines.
4858
+ - `Sim.collider.sdfMesh: (options?: { resolution?: number` — Use an SDF mesh collider for complex concave contact geometry. Exporters warn when their target cannot encode it.
4014
4859
  - `Sim.collider.none(reason: string): SimColliderDef` — Disable collision for a part with an explicit reason, such as a sensor-only or decorative object.
4015
4860
 
4861
+ #### `Sim.motion` — Body motion-state intent for simulation export. Dynamic is the default when omitted.
4862
+
4863
+ - `Sim.motion.dynamic(): SimMotionDef` — Simulate this body as a normal dynamic rigid body with mass and inertia.
4864
+ - `Sim.motion.kinematic(): SimMotionDef` — Simulate this body as kinematic: moved by the simulator/user, but not force-integrated.
4865
+ - `Sim.motion.static(): SimMotionDef` — Keep this body fixed in the world as a static collision/environment body.
4866
+
4016
4867
  #### `Sim.drive` — Joint-drive intent constructors for passive or powered assembly joints.
4017
4868
 
4018
4869
  - `Sim.drive.passive(options?: SimPassiveDriveOptions): SimDriveDef` — Mark a joint as passive while preserving damping and friction metadata for simulation export.
@@ -4043,6 +4894,57 @@ Assembly-owned links, constraints, connectors, solved poses, and source-level si
4043
4894
 
4044
4895
  `SimDiffDriveControllerDef`: `{ kind: "diffDrive" }`
4045
4896
 
4897
+ #### `Fea.material(name: string, options: FeaMaterialOptions): FeaMaterialDef` — Create a named linear-elastic structural material for static stress studies.
4898
+
4899
+ #### `Fea.body(options: FeaBodyOptions): FeaBodyDef` — Mark one assembly part as a structural body with a `Fea.material(...)` value.
4900
+
4901
+ #### `Fea.region` — Stable explicit region references for solver package manifests.
4902
+
4903
+ - `Fea.region.face(partName: string, faceName: string): FeaPartFaceRegionRef` — Reference a named face on a named assembly part without relying on object identity.
4904
+ - `Fea.region.plane(partName: string, faceName: string, options: FeaPlaneRegionOptions): FeaPartPlaneRegionRef` — Reference a planar face by a point on the face and its outward normal in part-local coordinates.
4905
+
4906
+ `FeaPartFaceRegionRef`: `{ kind: "fea-region-face", partName: string, faceName: string }`
4907
+
4908
+ `FeaPlaneRegionOptions`: `{ center: Vec3, normal: Vec3 }`
4909
+
4910
+ `FeaPartPlaneRegionRef`: `{ kind: "fea-region-plane", partName: string, faceName: string, center: Vec3, normal: Vec3 }`
4911
+
4912
+ #### `Fea.fix` — Fixture constructors over authored face/region references.
4913
+
4914
+ - `Fea.fix.fixed(region: FeaRegionRef): FeaFixedFixtureDef` — Fully fix all translational degrees of freedom on a face/region.
4915
+
4916
+ `FeaFixedFixtureDef`: `{ kind: "fixed", region: FeaRegionRef }`
4917
+
4918
+ #### `Fea.load` — Load constructors over authored face/region references.
4919
+
4920
+ - `Fea.load.force(region: FeaRegionRef, options: FeaForceLoadOptions): FeaForceLoadDef` — Apply a force with magnitude in newtons along the given direction vector.
4921
+
4922
+ `FeaForceLoadOptions`: `{ newtons: number, direction: Vec3 }`
4923
+
4924
+ `FeaForceLoadDef`: `{ kind: "force", region: FeaRegionRef }`
4925
+
4926
+ #### `Fea.target` — Study target constructors used by feedback and pass/fail gates.
4927
+
4928
+ - `Fea.target.minSafetyFactor(value: number): FeaMinSafetyFactorTargetDef` — Require the solved minimum safety factor to be at least `value`.
4929
+
4930
+ `FeaMinSafetyFactorTargetDef`: `{ kind: "minSafetyFactor", value: number }`
4931
+
4932
+ #### `Fea.mesh` — Volume mesh intent. V1 structural stress uses second-order tetrahedra only.
4933
+
4934
+ - `Fea.mesh.quadraticTets(options: FeaQuadraticTetMeshOptions): FeaQuadraticTetMeshDef` — Request quadratic tetrahedral C3D10 elements with a maximum size in mm.
4935
+
4936
+ `FeaQuadraticTetMeshOptions`: `{ maxSizeMm: number, minQuality?: number }`
4937
+
4938
+ `FeaQuadraticTetMeshDef`: `{ kind: "quadraticTets", order: 2, element: "C3D10" }`
4939
+
4940
+ #### `Fea.study` — Study constructors.
4941
+
4942
+ - `Fea.study.staticStress(name: string, options: FeaStaticStressStudyOptions): FeaStaticStressStudyDef` — Create a linear static structural stress study.
4943
+
4944
+ `FeaStaticStressStudyOptions`: `{ fixtures: FeaFixtureDef[], loads: FeaLoadDef[], target?: FeaTargetDef, mesh: FeaMeshDef }`
4945
+
4946
+ `FeaStaticStressStudyDef`: `{ kind: "staticStress", name: string }`
4947
+
4046
4948
  #### `assembly(name?: string): Assembly` — Create an assembly container with named parts, connectors, and kinematic links.
4047
4949
 
4048
4950
  **Use this from iteration 1 for any model with moving parts.** Do not build one static pose and retrofit motion later.
@@ -4176,7 +5078,7 @@ const housing = group(
4176
5078
  assembly.addPart("Base Assembly", housing);
4177
5079
  ```
4178
5080
 
4179
- **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `sim?: SimBodyDef`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
5081
+ **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `sim?: SimBodyDef`, `fea?: FeaBodyDef`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
4180
5082
 
4181
5083
  **`PartMetadata`**
4182
5084
 
@@ -4186,6 +5088,12 @@ assembly.addPart("Base Assembly", housing);
4186
5088
 
4187
5089
  Also: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`.
4188
5090
 
5091
+ `FeaBodyDef`: `{ kind: "fea-body", material: FeaMaterialDef }`
5092
+
5093
+ `FeaMaterialOptions`: `{ densityKgM3: number, youngsModulusMPa: number, poissonRatio: number, yieldStrengthMPa: number }`
5094
+
5095
+ `FeaMaterialDef`: `{ kind: "fea-material", name: string }`
5096
+
4189
5097
  **`AssemblyPartMateInput`**
4190
5098
  - `connector: string` — Name of a connector declared on the part (via `withConnectors()`).
4191
5099
  - `toLink: string` — Name of the link this connector's origin is pinned to.
@@ -4332,6 +5240,10 @@ Use this after adding physical parts and joints. Robot-body profiles require `ro
4332
5240
 
4333
5241
  `SimAssemblySimulationOptions`: `{ profile: SimProfileDef, rootPart?: string, controllers?: SimControllerDef[] }`
4334
5242
 
5243
+ #### `withFeaStudy(study: FeaStudyDef): Assembly` — Attach a structural FEA study to this assembly.
5244
+
5245
+ The study is authored with `Fea.study.staticStress(...)` and consumed by `forgecad export fea`. This records load-case intent only; ForgeCAD refuses to invent fixtures, loads, mesh order, or region tags during export.
5246
+
4335
5247
  #### `edgeBetweenFrames(a: string, b: string, options?: AssemblyFrameEdgeOptions): Assembly` — Add a visual skeleton edge between two rig frame origins.
4336
5248
 
4337
5249
  Frame edges follow the solved frame poses produced by `fixedJoint()`, `revoluteJoint()`, and `prismaticJoint()`. They do not add constraints, degrees of freedom, parts, or geometry; use them to make a frame-only rig readable in the Motion/rig inspection overlay.
@@ -4814,3 +5726,238 @@ Check the loop, not just the rest pose:
4814
5726
  3. Render each part with `--focus PartName`; the clevis end must show a visible gap between tines.
4815
5727
  4. Re-check at swept angles (30°/60°/90°) — rotation reveals collisions the rest pose hides.
4816
5728
  5. Backbend test at -10°: blocked = hard stop exists; rotates = add a stop.
5729
+
5730
+ ---
5731
+
5732
+ <!-- guides/structural-fea.md -->
5733
+
5734
+ # Structural FEA Stress Inspection
5735
+
5736
+ Use structural FEA when you want a ForgeCAD model to answer a load-case question:
5737
+
5738
+ - Where does this part see the highest stress?
5739
+ - How far does it deflect?
5740
+ - What is the minimum safety factor against the material yield strength?
5741
+ - Did the mesh and solver produce evidence that is good enough to inspect?
5742
+
5743
+ ForgeCAD owns the authoring contract, solver orchestration, result feedback, and inspection report. The numerical solve is done out of process with Gmsh and CalculiX. Users author a study in the model, run `forgecad fea run`, and inspect a result bundle.
5744
+
5745
+ ## Contents
5746
+
5747
+ - What You Get
5748
+ - What You Need Installed
5749
+ - Author The Study
5750
+ - Choose Stable Regions
5751
+ - Run The Flow
5752
+ - Read The Results
5753
+ - Current Scope
5754
+ - Troubleshooting
5755
+
5756
+ ## What You Get
5757
+
5758
+ A solved FEA result bundle can produce:
5759
+
5760
+ - max von Mises stress
5761
+ - max displacement
5762
+ - minimum safety factor
5763
+ - mesh quality and solver trust flags
5764
+ - region-level hot spots
5765
+ - `report.html`
5766
+ - `summary.json`
5767
+ - a safety-factor heatmap PNG
5768
+ - a solver stress heatmap PNG
5769
+ - a displacement magnitude heatmap PNG
5770
+
5771
+ The deformed render is display-only. It helps explain the displacement shape; it does not change the stress, displacement, or safety-factor numbers reported by the solver.
5772
+
5773
+ ## What You Need Installed
5774
+
5775
+ The ForgeCAD CLI creates the package and renders the heatmap. The package runner uses self-contained `uv` Python scripts for Gmsh so every package resolves the same Python dependency set by default.
5776
+
5777
+ Run `forgecad doctor` to check these optional FEA tools in a separate section. Missing FEA tools do not block core ForgeCAD modeling, export, or render commands.
5778
+
5779
+ | Tool | Used For | Quick Check |
5780
+ | --- | --- | --- |
5781
+ | `uv` | Runs the packaged Python scripts with pinned dependencies | `uv --version` |
5782
+ | CalculiX `ccx` | Solves the static stress deck | `ccx -v` |
5783
+ | Bash | Runs the package script | `bash --version` |
5784
+ | Chrome or Chromium | Renders PNG heatmaps from solved evidence | Chrome installed in a standard location, `CHROME_PATH=/path/to/chrome`, or `--chrome-path /path/to/chrome` |
5785
+
5786
+ If `uv` is not on `PATH`, set `UV=/path/to/uv` when running `forgecad fea run` or `forgecad fea check`.
5787
+
5788
+ If `ccx` is not on `PATH`, set `CCX=/path/to/ccx` when running `forgecad fea run` or `forgecad fea check`.
5789
+
5790
+ If you need an offline or pre-provisioned Python environment, set `PYTHON=/path/to/python`. That opt-out Python must be able to `import gmsh`; use `GMSH_PYTHONPATH` / `GMSH_PYTHON_PATH` only for that override path.
5791
+
5792
+ ForgeCAD does not bundle CalculiX. The generated `uv` scripts pin the Gmsh Python package, and `uv` downloads/caches it from the configured Python package index. If you redistribute solver binaries or Python wheels to customers, handle their licenses as part of your distribution.
5793
+
5794
+ ## Author The Study
5795
+
5796
+ Structural FEA starts in the `.forge.js` file. The script should return an authored `assembly(...)` with:
5797
+
5798
+ 1. a structural part marked with `Fea.body(...)`
5799
+ 2. one or more static stress studies from `Fea.study.staticStress(...)`
5800
+ 3. explicit fixtures and loads
5801
+ 4. a second-order tetrahedral mesh intent
5802
+
5803
+ ```js
5804
+ const aluminum = Fea.material("6061-T6", {
5805
+ densityKgM3: 2700,
5806
+ youngsModulusMPa: 68900,
5807
+ poissonRatio: 0.33,
5808
+ yieldStrengthMPa: 276,
5809
+ });
5810
+
5811
+ const beam = box(120, 12, 12);
5812
+
5813
+ return assembly("Cantilever Stress Study")
5814
+ .addPart("Beam", beam, {
5815
+ fea: Fea.body({ material: aluminum }),
5816
+ })
5817
+ .withFeaStudy(
5818
+ Fea.study.staticStress("end-load", {
5819
+ fixtures: [
5820
+ Fea.fix.fixed(Fea.region.face("fixed-end", beam.face("left"))),
5821
+ ],
5822
+ loads: [
5823
+ Fea.load.force(Fea.region.face("load-end", beam.face("right")), {
5824
+ newtons: 80,
5825
+ direction: [0, 0, -1],
5826
+ }),
5827
+ ],
5828
+ target: Fea.target.minSafetyFactor(2),
5829
+ mesh: Fea.mesh.quadraticTets({ maxSizeMm: 4 }),
5830
+ }),
5831
+ );
5832
+ ```
5833
+
5834
+ The complete API reference is generated from source in [Assembly](../generated/assembly.md). Keep reusable examples in `.forge.js` files; do not duplicate every API signature in handwritten docs.
5835
+
5836
+ ## Choose Stable Regions
5837
+
5838
+ Fixtures and loads must name real geometric regions. ForgeCAD will not guess them later.
5839
+
5840
+ Use `Fea.region.face(...)` when you can refer to a compiler-owned exact face, such as a simple box face or a named face from the model API.
5841
+
5842
+ Use `Fea.region.plane(...)` when the target is a planar face created by profiles, booleans, or imported geometry and the face name is not stable enough. Make the plane specific enough that it matches exactly one STEP/Gmsh surface.
5843
+
5844
+ During export, ForgeCAD writes a region map and a STEP tag plan. During the package run, the Gmsh preflight matches every authored fixture/load region against the STEP surfaces. Missing or ambiguous matches fail hard. That is intentional: a silent substitute face would make the stress result untrustworthy.
5845
+
5846
+ ## Run The Flow
5847
+
5848
+ Installed users run the CLI as `forgecad`. Developers running inside this repository can replace `forgecad` with `node dist-cli/forgecad.js`.
5849
+
5850
+ Run every authored FEA study and save an inspection result bundle:
5851
+
5852
+ ```bash
5853
+ forgecad fea run examples/analysis/structural-stress-fea.forge.js
5854
+ ```
5855
+
5856
+ Run one named study:
5857
+
5858
+ ```bash
5859
+ forgecad fea run bracket.forge.js --study side-load
5860
+ ```
5861
+
5862
+ Open the report:
5863
+
5864
+ ```bash
5865
+ forgecad fea open out/bracket-fea
5866
+ ```
5867
+
5868
+ Render a customer-facing safety view:
5869
+
5870
+ ```bash
5871
+ forgecad fea render out/bracket-fea/side-load --field safety
5872
+ ```
5873
+
5874
+ Render the engineering stress heatmap:
5875
+
5876
+ ```bash
5877
+ forgecad fea render out/bracket-fea/side-load --field stress
5878
+ ```
5879
+
5880
+ Render the displacement magnitude heatmap:
5881
+
5882
+ ```bash
5883
+ forgecad fea render out/bracket-fea/side-load --field displacement
5884
+ ```
5885
+
5886
+ Render a deformed stress view only when the displacement shape is useful to inspect:
5887
+
5888
+ ```bash
5889
+ forgecad fea render out/bracket-fea/side-load \
5890
+ --field stress \
5891
+ --shape deformed \
5892
+ --exaggerate 10
5893
+ ```
5894
+
5895
+ The deformation scale only affects the render. It does not change the reported stress, displacement, or safety factor.
5896
+
5897
+ Each solved study result directory includes:
5898
+
5899
+ - `report.html` for the human inspection report
5900
+ - `summary.json` for automation
5901
+ - `renders/safety-factor.png` for the customer-facing safety heatmap
5902
+ - `renders/stress.png` for the engineering von Mises stress heatmap
5903
+
5904
+ Displacement and deformed-shape PNGs are explicit render outputs from `forgecad fea render --field displacement` or `--shape deformed`.
5905
+
5906
+ Compare two solved result bundles:
5907
+
5908
+ ```bash
5909
+ forgecad fea compare out/baseline-fea/side-load out/four-x-fea/side-load
5910
+ ```
5911
+
5912
+ Comparison renders use one shared camera, image size, and safety-factor legend.
5913
+
5914
+ Run in CI and fail the process when authored targets fail:
5915
+
5916
+ ```bash
5917
+ forgecad fea check bracket.forge.js --json
5918
+ ```
5919
+
5920
+ ## Read The Results
5921
+
5922
+ Start with `report.html` or `summary.json` in the result directory. The important fields are the maximum stress, maximum displacement, minimum safety factor, hot spots, and any mesh or solver trust findings.
5923
+
5924
+ The default user-facing result is safety factor because it answers "is this part okay?" Use stress when you need the raw engineering von Mises field.
5925
+
5926
+ Advanced users can still run the lower-level package flow:
5927
+
5928
+ ```bash
5929
+ forgecad export fea model.forge.js --output out/beam.feapkg
5930
+ forgecad sim fea out/beam.feapkg --json
5931
+ forgecad inspect structural stress out/beam.feapkg --camera iso --output out/stress.png
5932
+ ```
5933
+
5934
+ Those commands are useful for debugging package evidence. Customer docs should prefer `forgecad fea ...`.
5935
+
5936
+ ## Current Scope
5937
+
5938
+ Structural FEA V1 is intentionally narrow:
5939
+
5940
+ - linear static stress only
5941
+ - one structural body per package
5942
+ - exact OCCT STEP export only
5943
+ - second-order tetrahedral elements only
5944
+ - fixed fixtures and force loads only
5945
+ - no contacts, bonded assemblies, thermal loads, buckling, fatigue, plasticity, or certification workflow
5946
+
5947
+ ForgeCAD refuses mesh or faceted fallback for FEA export. If exact geometry export, region mapping, mesh quality, solver convergence, result parsing, or evidence trust fails, the command should fail with an actionable error instead of inventing a weaker path.
5948
+
5949
+ ## Troubleshooting
5950
+
5951
+ | Symptom | What It Means | What To Do |
5952
+ | --- | --- | --- |
5953
+ | `FEA.TOOLCHAIN_UV_MISSING` | The package runner cannot find `uv`. | Install `uv` or run with `UV=/path/to/uv`. |
5954
+ | `FEA.TOOLCHAIN_PYTHON_MISSING` | A `PYTHON=...` override points to a missing Python executable. | Install Python 3 or fix the `PYTHON` path. |
5955
+ | `FEA.TOOLCHAIN_GMSH_MISSING` | The selected Python process cannot import Gmsh. | Prefer the default `uv` path, or install the Gmsh Python module for the `PYTHON=...` override. |
5956
+ | `FEA.TOOLCHAIN_CCX_MISSING` | CalculiX is not available as `ccx`. | Install CalculiX or run with `CCX=/path/to/ccx`. |
5957
+ | `FEA.GMSH_FACE_MATCH_NONE` | An authored fixture/load region did not match a STEP surface. | Use a more stable face reference or a more precise planar region. |
5958
+ | `FEA.GMSH_FACE_MATCH_AMBIGUOUS` | A region matched more than one STEP surface. | Make the target region more specific or change the model so the load/fixture face is unique. |
5959
+ | `FEA.MESH_QUALITY_BELOW_TARGET` | The mesh exists but did not meet the package quality target. | Reduce mesh size, simplify tiny features, or improve the geometry around the hot area. |
5960
+ | `FEA.SOLVER_FAILED` | CalculiX did not complete the solve. | Inspect `solver/static_stress.log`, then check fixtures, loads, material values, and over-constraint. |
5961
+ | `FEA.FIELD_UNTRUSTED` | The heatmap input is not trusted package evidence. | Run inspection on the `.feapkg` directory after `forgecad sim fea`, not a copied JSON file. |
5962
+
5963
+ For command flags, use the [CLI reference](../CLI.md). For the public API, use the generated [Assembly reference](../generated/assembly.md).