forgecad 0.9.13 → 0.9.15

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 (216) hide show
  1. package/LICENSE +6 -4
  2. package/README.md +8 -4
  3. package/dist/assets/{AdminPage-DramHHDf.js → AdminPage-CDyGUinA.js} +2 -2
  4. package/dist/assets/{BenchmarkPage-Bjgkh5m9.js → BenchmarkPage-DfPMY_-d.js} +4 -15
  5. package/dist/assets/{BlogPage-n_HGP3Qm.js → BlogPage-kF0fkdJT.js} +2 -2
  6. package/dist/assets/{DocsPage-WCIkPmzC.js → DocsPage-B954L3YN.js} +9 -3
  7. package/dist/assets/EditorApp-Beb-IZ0y.js +14014 -0
  8. package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-CuDLxKqL.css} +698 -0
  9. package/dist/assets/{EmbedViewer-DEZKqdfW.js → EmbedViewer-C77B-TrF.js} +3 -3
  10. package/dist/assets/{LandingPageProofDriven-CeRIctuj.js → LandingPageProofDriven-Cr6fXMDj.js} +35 -37
  11. package/dist/assets/LegalPage-BRlScr9A.css +91 -0
  12. package/dist/assets/LegalPage-Dzklqmmg.js +39 -0
  13. package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
  14. package/dist/assets/{PricingPage-rIRa8p4Y.js → PricingPage-zWXkvlwl.js} +19 -19
  15. package/dist/assets/{SettingsPage-BqCUvEXM.js → SettingsPage-Bz0of4KQ.js} +2 -2
  16. package/dist/assets/app-CE3sYcV7.css +3890 -0
  17. package/dist/assets/{app-BUZqJvSO.js → app-D3kDkggg.js} +2305 -960
  18. package/dist/assets/cli/{render-lhGxj50Y.js → render-DSY3mMQa.js} +423 -30
  19. package/dist/assets/{constructionHistoryWorker-ipD1jcIv.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
  20. package/dist/assets/{evalWorker-CHXSe_-u.js → evalWorker-CU0Ke6DP.js} +7799 -4163
  21. package/dist/assets/{forgecad_geometry-BVnIeXMG.js → forgecad_geometry-Dgceylq9.js} +43 -1
  22. package/dist/assets/{forgecad_geometry_bg-DufhhCBV.wasm → forgecad_geometry_bg-dD4RNQF1.wasm} +0 -0
  23. package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-COyp8XXA.js} +927 -243
  24. package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
  25. package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
  26. package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
  27. package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
  28. package/dist/assets/{manifold-D1LZIHqn.js → manifold-BRI5prcH.js} +1 -1
  29. package/dist/assets/{manifold-C2fwoTgd.js → manifold-C-3h2M7p.js} +2 -2
  30. package/dist/assets/{manifold-BTkzxi9V.js → manifold-DNkrUWpA.js} +1 -1
  31. package/dist/assets/{reportWorker-Cq1qGmg0.js → reportWorker-CdBz5bNg.js} +7537 -10856
  32. package/dist/assets/{scalar-sampling-budget-D9Qv_UlJ.js → scalar-sampling-budget-wJF98aY9.js} +6943 -4345
  33. package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
  34. package/dist/assets/{renderSceneState-Dr0xPq1A.js → targets-B9sGB5nB.js} +27 -1
  35. package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
  36. package/dist/cli/render.html +1 -1
  37. package/dist/docs/index.html +2 -2
  38. package/dist/docs-raw/AI/ai-native-cad.md +50 -0
  39. package/dist/docs-raw/AI/usage.md +9 -17
  40. package/dist/docs-raw/CLI.md +71 -21
  41. package/dist/docs-raw/component-model.md +27 -11
  42. package/dist/docs-raw/generated/assembly.md +301 -212
  43. package/dist/docs-raw/generated/concepts.md +238 -240
  44. package/dist/docs-raw/generated/core.md +283 -6
  45. package/dist/docs-raw/generated/curves.md +274 -361
  46. package/dist/docs-raw/generated/lib.md +7 -1
  47. package/dist/docs-raw/generated/output.md +19 -4
  48. package/dist/docs-raw/generated/runtime-names.md +41 -0
  49. package/dist/docs-raw/generated/sdf.md +31 -0
  50. package/dist/docs-raw/generated/sheet-metal.md +9 -0
  51. package/dist/docs-raw/generated/sketch.md +44 -1
  52. package/dist/docs-raw/generated/viewport.md +14 -6
  53. package/dist/docs-raw/guides/coordinate-system.md +20 -16
  54. package/dist/docs-raw/guides/geometry-conventions.md +2 -2
  55. package/dist/docs-raw/guides/inspection-bundles.md +2 -1
  56. package/dist/docs-raw/guides/joint-design.md +24 -0
  57. package/dist/docs-raw/guides/positioning.md +13 -3
  58. package/dist/docs-raw/legal/privacy.md +63 -0
  59. package/dist/docs-raw/legal/software-license.md +55 -0
  60. package/dist/docs-raw/legal/terms.md +87 -0
  61. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +3 -3
  62. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
  63. package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
  64. package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
  65. package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
  66. package/dist/docs-raw/skills/forgecad-lld.md +1 -1
  67. package/dist/docs-raw/skills/forgecad-make-a-model.md +4 -4
  68. package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
  69. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
  70. package/dist/docs-raw/skills/forgecad-project.md +1 -1
  71. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +4 -4
  72. package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
  73. package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
  74. package/dist/docs-raw/skills/forgecad.md +4 -3
  75. package/dist/index.html +40 -12
  76. package/dist/llms.txt +8 -0
  77. package/dist/site.webmanifest +1 -1
  78. package/dist/sitemap.xml +49 -13
  79. package/dist-cli/{check-compiler-LOXCPEOI.js → check-compiler-SDX5QIXI.js} +1 -2
  80. package/dist-cli/{check-query-propagation-BAKNVWXR.js → check-query-propagation-EAYEFT77.js} +1 -2
  81. package/dist-cli/{chunk-RY43WF46.js → chunk-N4O47JLF.js} +13772 -9938
  82. package/dist-cli/forgecad.js +2387 -899
  83. package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
  84. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  85. package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
  86. package/dist-skill/CONTEXT.md +1120 -724
  87. package/dist-skill/SKILL.md +3 -2
  88. package/dist-skill/docs/API/core/concepts.md +64 -1
  89. package/dist-skill/docs/CLI.md +71 -21
  90. package/dist-skill/docs/generated/assembly.md +277 -229
  91. package/dist-skill/docs/generated/core.md +283 -6
  92. package/dist-skill/docs/generated/curves.md +272 -362
  93. package/dist-skill/docs/generated/lib.md +7 -1
  94. package/dist-skill/docs/generated/output.md +19 -4
  95. package/dist-skill/docs/generated/runtime-names.md +41 -0
  96. package/dist-skill/docs/generated/sdf.md +31 -0
  97. package/dist-skill/docs/generated/sheet-metal.md +9 -0
  98. package/dist-skill/docs/generated/sketch.md +44 -2
  99. package/dist-skill/docs/generated/viewport.md +5 -90
  100. package/dist-skill/docs/guides/coordinate-system.md +20 -16
  101. package/dist-skill/docs/guides/geometry-conventions.md +2 -2
  102. package/dist-skill/docs/guides/inspection-bundles.md +2 -1
  103. package/dist-skill/docs/guides/joint-design.md +24 -0
  104. package/dist-skill/docs/guides/positioning.md +13 -3
  105. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +2 -2
  106. package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
  107. package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
  108. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
  109. package/dist-skill/library/forgecad-make-a-model/SKILL.md +3 -3
  110. package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
  111. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
  112. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +3 -3
  113. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  114. package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
  115. package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
  116. package/examples/api/assembly-kinematics-limb.forge.js +116 -0
  117. package/examples/api/connector-frame-rig-chain.forge.js +102 -0
  118. package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
  119. package/examples/api/exact-surface-studio.forge.js +6 -8
  120. package/examples/api/helix-basics.forge.js +6 -6
  121. package/examples/api/lean-foundations/README.md +12 -0
  122. package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
  123. package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
  124. package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
  125. package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
  126. package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
  127. package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
  128. package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
  129. package/examples/api/route3d-elbow.forge.js +68 -0
  130. package/examples/api/transition-curves.forge.js +44 -15
  131. package/examples/api/y-blend-corner-showcase.forge.js +0 -2
  132. package/examples/generative/coral-vase.forge.js +1 -1
  133. package/examples/nurbs-tube.forge.js +1 -1
  134. package/package.json +14 -18
  135. package/dist/assets/EditorApp-CP9Za6tm.js +0 -13630
  136. package/dist/assets/app-CsHnaBWt.css +0 -1789
  137. package/dist/docs-raw/API/README.md +0 -16
  138. package/dist/docs-raw/API/core/concepts.md +0 -118
  139. package/dist/docs-raw/INDEX.md +0 -138
  140. package/dist/docs-raw/RELEASING.md +0 -87
  141. package/dist/docs-raw/agent-native-api.md +0 -27
  142. package/dist/docs-raw/beta-deployment.md +0 -304
  143. package/dist/docs-raw/beta-operations.md +0 -325
  144. package/dist/docs-raw/blueprint-first.md +0 -145
  145. package/dist/docs-raw/cli-monetization.md +0 -112
  146. package/dist/docs-raw/coding-best-practices.md +0 -120
  147. package/dist/docs-raw/coding.md +0 -340
  148. package/dist/docs-raw/deployment.md +0 -374
  149. package/dist/docs-raw/guides/skill-maintenance.md +0 -161
  150. package/dist/docs-raw/guides/surface-members.md +0 -82
  151. package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
  152. package/dist/docs-raw/internals/compiler.md +0 -307
  153. package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
  154. package/dist/docs-raw/internals/constraint-solver.md +0 -176
  155. package/dist/docs-raw/internals/shape-from-slices.md +0 -152
  156. package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
  157. package/dist/docs-raw/platform/admin.md +0 -45
  158. package/dist/docs-raw/platform/architecture.md +0 -82
  159. package/dist/docs-raw/platform/auth.md +0 -139
  160. package/dist/docs-raw/platform/email.md +0 -67
  161. package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
  162. package/dist/docs-raw/platform/observability.md +0 -197
  163. package/dist/docs-raw/platform/projects.md +0 -111
  164. package/dist/docs-raw/platform/sharing.md +0 -90
  165. package/dist/docs-raw/product/README.md +0 -39
  166. package/dist/docs-raw/product/api-as-product-language.md +0 -13
  167. package/dist/docs-raw/product/business-model.md +0 -15
  168. package/dist/docs-raw/product/competitive-positioning.md +0 -17
  169. package/dist/docs-raw/product/creative-manufacturing.md +0 -15
  170. package/dist/docs-raw/product/founder-story.md +0 -11
  171. package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
  172. package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
  173. package/dist/docs-raw/product/product-loop.md +0 -17
  174. package/dist/docs-raw/product/strategic-decisions.md +0 -22
  175. package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
  176. package/dist/docs-raw/product/user-segments.md +0 -15
  177. package/dist/docs-raw/product/vision.md +0 -26
  178. package/dist/docs-raw/rl-environments.md +0 -508
  179. package/dist/docs-raw/runbook.md +0 -611
  180. package/dist-cli/check-compiler-LOXCPEOI.js.map +0 -1
  181. package/dist-cli/check-query-propagation-BAKNVWXR.js.map +0 -1
  182. package/dist-cli/chunk-RY43WF46.js.map +0 -1
  183. package/dist-cli/forgecad.js.map +0 -1
  184. package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
  185. package/dist-cli/solver-46FFSK2U.js.map +0 -1
  186. package/dist-skill/SKILL-dev.md +0 -145
  187. package/dist-skill/docs-dev/API/core/concepts.md +0 -118
  188. package/dist-skill/docs-dev/CLI.md +0 -647
  189. package/dist-skill/docs-dev/agent-native-api.md +0 -27
  190. package/dist-skill/docs-dev/blueprint-first.md +0 -145
  191. package/dist-skill/docs-dev/coding-best-practices.md +0 -120
  192. package/dist-skill/docs-dev/coding.md +0 -340
  193. package/dist-skill/docs-dev/component-model.md +0 -164
  194. package/dist-skill/docs-dev/generated/assembly.md +0 -794
  195. package/dist-skill/docs-dev/generated/core.md +0 -2117
  196. package/dist-skill/docs-dev/generated/curves.md +0 -2583
  197. package/dist-skill/docs-dev/generated/lib.md +0 -169
  198. package/dist-skill/docs-dev/generated/output.md +0 -247
  199. package/dist-skill/docs-dev/generated/sdf.md +0 -446
  200. package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
  201. package/dist-skill/docs-dev/generated/sketch.md +0 -1811
  202. package/dist-skill/docs-dev/generated/viewport.md +0 -585
  203. package/dist-skill/docs-dev/generated/wood.md +0 -108
  204. package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
  205. package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
  206. package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
  207. package/dist-skill/docs-dev/guides/joint-design.md +0 -78
  208. package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
  209. package/dist-skill/docs-dev/guides/positioning.md +0 -161
  210. package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
  211. package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
  212. package/dist-skill/docs-dev/internals/compiler.md +0 -307
  213. package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
  214. package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
  215. package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
  216. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.mjs +0 -289
@@ -123,7 +123,32 @@ const bomHelpers = require("./bom.js");
123
123
  bomHelpers.addFasteners(...);
124
124
  ```
125
125
 
126
- Top-level declarations such as `const bom = ...`, `let scene = ...`, or `class Shape {}` collide with the injected runtime names. If you need a local helper, choose a project-specific name like `projectBom`, `sceneConfig`, or `makeShape`.
126
+ Top-level lexical declarations such as `const bom = ...`, `let slot = ...`, `const lib = ...`, `const joint = ...`, or `class Shape {}` collide with the injected runtime names. This is checked when the script runs, so agents should treat these names as reserved before executing the model.
127
+
128
+ Use project-specific local names instead:
129
+
130
+ ```javascript
131
+ // BAD — `lib` and `slot` are injected runtime names.
132
+ const lib = require("./wheel-lib.js");
133
+ const slot = rect(20, 5);
134
+
135
+ // GOOD — local names describe the project role.
136
+ const wheelLib = require("./wheel-lib.js");
137
+ const axleSlotSketch = rect(20, 5);
138
+ ```
139
+
140
+ If a helper module exports a name that matches a ForgeCAD runtime name, keep the module under one local object name or rename during destructuring:
141
+
142
+ ```javascript
143
+ // GOOD — no top-level `slot` declaration.
144
+ const wheelHelpers = require("./wheel-helpers.js");
145
+ const axleSlot = wheelHelpers.slot(...);
146
+
147
+ // GOOD — imported helper is renamed.
148
+ const { slot: makeAxleSlot } = require("./wheel-helpers.js");
149
+ ```
150
+
151
+ The complete collision-reserved list is generated from the runtime source in [Runtime Names](../../generated/runtime-names.md). Check that list before using natural local names such as `lib`, `slot`, `joint`, `group`, `path`, `text2d`, `scene`, or `Shape`.
127
152
 
128
153
  ## Execution Model
129
154
 
@@ -162,6 +187,44 @@ return {
162
187
  };
163
188
  ```
164
189
 
190
+ ### Forge-Aware Builder Modules
191
+
192
+ A `.forge.js` file can also return builder functions. Those functions keep access to
193
+ the ForgeCAD runtime names from their defining file, so this is the supported way
194
+ to share sketch, profile, shape, or assembly builders that need APIs such as
195
+ `path()`, `circle2d()`, `box()`, or `assembly()`.
196
+
197
+ This pattern works well when a file should be both directly inspectable and
198
+ importable: render the flat profiles or part preview when the file is opened on
199
+ its own, and export builders under a named object for the assembly file.
200
+
201
+ ```javascript
202
+ // profiles.forge.js - inspectable on its own, reusable through require()
203
+ function wheelProfile() {
204
+ return circle2d(40).subtract(circle2d(18));
205
+ }
206
+
207
+ return {
208
+ preview: [{ name: 'Wheel profile', sketch: wheelProfile() }],
209
+ make: { wheelProfile },
210
+ };
211
+ ```
212
+
213
+ ```javascript
214
+ // main.forge.js - reuses the exact inspected profile
215
+ const profiles = require('./profiles.forge.js');
216
+ const wheel = profiles.make.wheelProfile().extrude(8);
217
+ return wheel;
218
+ ```
219
+
220
+ Keep builder modules pure over top-level constants, top-level `param()` values,
221
+ or explicit function arguments. Do not declare new `param()` values inside an
222
+ exported builder if callers need `require('./profiles.forge.js', { Width: 80 })`
223
+ overrides: import overrides are validated while the module loads, before any
224
+ exported builder is called. For plain constants, tables, math helpers, and
225
+ formatting code that does not construct ForgeCAD geometry, use `.js` helper
226
+ modules instead.
227
+
165
228
  Named return objects and named `group(...)` children can include `tags`. Tags are viewport metadata: they do not affect geometry, exports, face labels, or BOM rows, but the command palette can hide, show only, or focus every object with a selected tag.
166
229
 
167
230
  ```javascript
@@ -214,6 +277,47 @@ For organic shapes, smooth blending, TPMS lattices, and surface deformations. Re
214
277
 
215
278
  ---
216
279
 
280
+ <!-- generated/runtime-names.md -->
281
+
282
+ # Runtime Names
283
+
284
+ Generated by `scripts/gen-api-docs.mjs` from `src/forge/script-runtime/runScript.ts`. Do not edit by hand.
285
+
286
+ ForgeCAD injects API functions, classes, namespaces, and sandbox guard names into every `.forge.js` script. Top-level lexical declarations using these names collide with the injected runtime bindings when the script runs.
287
+
288
+ Agents should avoid declaring these names with top-level `const`, `let`, destructured imports, or `class` declarations. Use project-specific local names such as `wheelLib`, `axleSlotSketch`, `driveJointConfig`, or `labelTextSketch` instead.
289
+
290
+ These collision-reserved names are case-sensitive:
291
+
292
+ ```text
293
+ activateBackend, Analysis, arcSlot, assembly, Assembly, assemblyInstructions, assemblyPreview, Blend
294
+ bom, bomToCsv, boolParam, box, cameraTrajectory, Carrier, chamfer, chamferTrackedEdge
295
+ choiceParam, circle, circle2d, Circle2D, circularLayout, circularPattern, circularPattern2d, coalesceEdges
296
+ combine, COMMON_KERFS, compareWith, composeChain, connectEdges, connector, console, constrainedSketch
297
+ Constraint, Counterbore, Curve, Curve3D, cutPlane, cylinder, degrees, difference
298
+ difference2d, dim, dimLine, draft, ellipse, explodeView, faceProfile, fillet
299
+ filletCorners, filletTrackedEdge, fingerJoint, flatPanel, flatPart, formatInstructions, Function, gcode
300
+ GCodeBuilder, getActiveBackend, global, globalThis, group, Helix, HelixCurve, hermiteTransitionG2
301
+ highlight, Import, ImportedAssembly, importMesh, importStep, importSvgSketch, initKernel, intersection
302
+ intersection2d, intersectWithPlane, joint, jointsView, laserKit, lib, line, Line2D
303
+ linearPattern, linearPattern2d, listParam, loadFont, loft, Loft, loftAlongSpine, lookupKerf
304
+ mirrorCopy, mock, ngon, nurbs3d, NurbsCurve3D, nurbsSurface, NurbsSurface, offsetSolid
305
+ param, Param, path, point, Point2D, Points, polygon, polygonVertices
306
+ port, Product, ProductHandleBuilder, ProductHandleFeature, ProductPanelBuilder, ProductRibbonBuilder, ProductSkin, ProductSkinBuilder
307
+ ProductSpoutBuilder, ProductStationBuilder, ProductSurfaceBuilder, ProductSurfaceRef, projectToPlane, queueMicrotask, radians, rect
308
+ Rectangle2D, Ribs, robotExport, roundedRect, Route3D, scene, Sculpt, sdf
309
+ SdfShape, selectEdge, selectEdges, self, setActiveBackend, setImmediate, setInterval, setTimeout
310
+ Shape, ShapeGroup, sheetMetal, SheetMetalPart, sheetStock, Sketch, sketchToDxf, sketchToSvg
311
+ slot, Slot, SolvedAssembly, spec, sphere, spline2d, spline3d, star
312
+ stroke, Surface, SurfaceBody, SurfaceMembers, surfacePatch, sweep, tabSlot, text2d
313
+ textWidth, torus, toShape, Transform, transitionCurve, transitionSurface, union, union2d
314
+ variableSweep, verify, viewConfig, Viewport, window, Wood
315
+ ```
316
+
317
+ `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.
318
+
319
+ ---
320
+
217
321
  <!-- generated/core.md -->
218
322
 
219
323
  # Core API
@@ -238,6 +342,8 @@ For organic shapes, smooth blending, TPMS lattices, and surface deformations. Re
238
342
  - [SurfacePattern](#surfacepattern)
239
343
  - [Pattern2D](#pattern2d)
240
344
  - [Pattern2DBuilder](#pattern2dbuilder)
345
+ - [HermiteCurve3D](#hermitecurve3d)
346
+ - [QuinticHermiteCurve3D](#quintichermitecurve3d)
241
347
  - [ShapeRef](#shaperef)
242
348
  - [ANCHOR3D_NAMES](#anchor3d-names)
243
349
  - [verify](#verify)
@@ -577,9 +683,9 @@ selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]
577
683
  | `minLength?` | `number` | Filter: minimum edge length. |
578
684
  | `maxLength?` | `number` | Filter: maximum edge length. |
579
685
  | `within?` | `BoundingRegion` | Filter: edge midpoint must be within this bounding region. |
580
- | `atZ?` | `number` | Shorthand: edge midpoint Z this value (within `tolerance`). Equivalent to `within: { zMin: atZ - tol, zMax: atZ + tol }`. |
581
- | `tolerance?` | `number` | Position tolerance for approximate matches (default: `1.0`). Used by `atZ` and `near`. |
582
- | `angleTolerance?` | `number` | Angular tolerance in degrees for `parallel`/`perpendicular` filters (default: `10`). |
686
+ | `atZ?` | `number` | Shorthand: edge midpoint Z is approximately this value within `tolerance`. |
687
+ | `tolerance?` | `number` | Position tolerance for approximate matches. Used by `atZ` and `near`. Default: `1.0`. |
688
+ | `angleTolerance?` | `number` | Angular tolerance in degrees for `parallel`/`perpendicular` filters. Default: `10`. |
583
689
 
584
690
  `BoundingRegion`: `{ xMin?: number, xMax?: number, yMin?: number, yMax?: number, zMin?: number, zMax?: number }`
585
691
 
@@ -631,7 +737,16 @@ coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]
631
737
 
632
738
  #### `require()` — Import a module with optional ForgeCAD parameter overrides. Returns the module's exports.
633
739
 
634
- When importing a `.forge.js` file, the return value is what the script returns. If the script returns a metadata object (e.g. `{ shape: myShape, bolts: {...} }`), the caller receives the full object — renderable values and metadata together.
740
+ 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.
741
+
742
+ **Assembly return contract**
743
+
744
+ | `.forge.js` return value | `require()` result |
745
+ |---|---|
746
+ | `Assembly` | `ImportedAssembly` |
747
+ | `SolvedAssembly` | `SolvedAssembly` |
748
+
749
+ [`ImportedAssembly`](/docs/assembly#importedassembly) exposes default-pose helpers such as `getPart()`, `collisionReport()`, and `minClearance()`. Use `solve(state)` first when inspecting a non-default pose.
635
750
 
636
751
  **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`.
637
752
 
@@ -662,6 +777,26 @@ mount.bolts.pos // access the metadata
662
777
  mount.shape // access the geometry
663
778
  ```
664
779
 
780
+ **Forge-aware builder module pattern** — use `.forge.js` modules for reusable sketch, profile, shape, or assembly builders that need ForgeCAD runtime APIs:
781
+
782
+ ```js
783
+ // profiles.forge.js — inspectable on its own, reusable through require()
784
+ function wheelProfile() {
785
+ return circle2d(40).subtract(circle2d(18));
786
+ }
787
+
788
+ return {
789
+ preview: [{ name: 'Wheel profile', sketch: wheelProfile() }],
790
+ make: { wheelProfile },
791
+ };
792
+
793
+ // main.forge.js
794
+ const profiles = require('./profiles.forge.js');
795
+ const wheel = profiles.make.wheelProfile().extrude(8);
796
+ ```
797
+
798
+ 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.
799
+
665
800
  ```ts
666
801
  require(path: string, paramOverrides?: Record<string, number | string>): any
667
802
  ```
@@ -690,10 +825,18 @@ importSvgSketch(fileName: string, options?: SvgImportOptions): Sketch
690
825
  | `simplify?` | `number` | Simplification tolerance for final sketch cleanup. |
691
826
  | `invertY?` | `boolean` | Flip SVG Y-down coordinates to CAD Y-up. Enabled by default. |
692
827
 
693
- #### `importMesh()` — Import an external mesh file (STL, OBJ, 3MF) as a Shape.
828
+ #### `importMesh()` — Import an external mesh file (STL, OBJ, 3MF).
829
+
830
+ By default, 3MF build items are flattened into one Shape for compatibility. Use `separateObjects: true` to import 3MF build items/resource objects as a named ShapeGroup whose children are targetable by `forgecad ls`. Use `object` to import one item by the stable ref/name reported by `forgecad run`.
831
+
832
+ ```js
833
+ const all = importMesh("./assembly.3mf", { separateObjects: true });
834
+ const pin = all.child("Pin #001");
835
+ const plate = importMesh("./assembly.3mf", { object: "3mf:build:001:object:7" });
836
+ ```
694
837
 
695
838
  ```ts
696
- importMesh(fileName: string, options?: { scale?: number; center?: boolean; }): Shape
839
+ importMesh(fileName: string, options?: { scale?: number; center?: boolean; object?: string; separateObjects?: boolean; }): Shape | ShapeGroup
697
840
  ```
698
841
 
699
842
  #### `importStep()` — Import a STEP file (.step, .stp) as an exact OCCT-backed Shape. Preserves NURBS curves, B-spline surfaces, and exact topology. Requires running with the OCCT backend.
@@ -1166,6 +1309,25 @@ box(100, 100, 10).color('#gold').material({ metalness: 0.95, roughness: 0.05 }).
1166
1309
  material(props: ShapeMaterialProps): Shape
1167
1310
  ```
1168
1311
 
1312
+ **`ShapeMaterialProps`**
1313
+
1314
+ | Option | Type | Description |
1315
+ |--------|------|-------------|
1316
+ | `metalness?` | `number` | Metalness factor (0 = dielectric, 1 = metal). Default: 0.05 |
1317
+ | `roughness?` | `number` | Roughness factor (0 = mirror, 1 = fully diffuse). Default: 0.35 |
1318
+ | `emissive?` | `string` | Emissive glow color (hex string, e.g. "#ff6b35"). |
1319
+ | `emissiveIntensity?` | `number` | Emissive intensity multiplier. Default: 1 |
1320
+ | `opacity?` | `number` | Opacity (0 = fully transparent, 1 = fully opaque). Default: 1 |
1321
+ | `wireframe?` | `boolean` | Render as wireframe. Default: false |
1322
+ | `clearcoat?` | `number` | Clearcoat intensity (0–1). Default: 0.1 |
1323
+ | `clearcoatRoughness?` | `number` | Clearcoat roughness (0–1). Default: 0.4 |
1324
+ | `transmission?` | `number` | Glass/translucency transmission factor (0–1). Renderer support depends on target. |
1325
+ | `ior?` | `number` | Index of refraction for transmissive materials. Typical glass is ~1.45. |
1326
+ | `thickness?` | `number` | Approximate transmissive volume thickness in model units. |
1327
+ | `specularIntensity?` | `number` | Specular highlight intensity (0–1). |
1328
+ | `specularColor?` | `string` | Specular highlight tint. |
1329
+ | `reflectivity?` | `number` | Reflection strength for supported renderers (0–1). |
1330
+
1169
1331
  **Face Topology**
1170
1332
 
1171
1333
  #### `face()` — Resolve a face by user-authored label or compiler-owned name. Returns a `FaceRef` that can be passed to `.onFace()`, `projectToPlane()`, or used directly in placement.
@@ -1286,6 +1448,15 @@ body.edgesOf('top', { concave: true })
1286
1448
  edgesOf(faceLabel: string, options?: EdgesOfOptions): EdgeSegment[]
1287
1449
  ```
1288
1450
 
1451
+ **`EdgesOfOptions`**
1452
+
1453
+ | Option | Type | Description |
1454
+ |--------|------|-------------|
1455
+ | `exclude?` | `string \| string[]` | Exclude edges shared with these named faces. |
1456
+ | `convex?` | `boolean` | Additional geometric filter: only convex edges. |
1457
+ | `concave?` | `boolean` | Additional geometric filter: only concave edges. |
1458
+ | `minLength?` | `number` | Minimum edge length filter. |
1459
+
1289
1460
  #### `edgesBetween()` — Return edges shared between two named faces.
1290
1461
 
1291
1462
  An edge is "between" faces A and B when one of its adjacent mesh triangles belongs to A and the other belongs to B. This is the most precise topological edge selection — "fillet the edges where the top meets the wall."
@@ -1367,6 +1538,8 @@ rotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Sh
1367
1538
  rotateAroundTo(axis: [ number, number, number ], pivot: [ number, number, number ], movingPoint: RotationPointLike, targetPoint: RotationPointLike, options?: RotateAroundToOptions): Shape
1368
1539
  ```
1369
1540
 
1541
+ `RotateAroundToOptions`: `{ mode?: RotateAroundToMode }`
1542
+
1370
1543
  #### `transform()` — Apply a 4x4 affine transform matrix (column-major) or a Transform object.
1371
1544
 
1372
1545
  ```ts
@@ -1463,6 +1636,11 @@ box(100, 100, 20).pocket('top', 8, { scale: 0.8 })
1463
1636
  pocket(face: FaceSelector, depth: number, opts?: PocketOptions): Shape
1464
1637
  ```
1465
1638
 
1639
+ **`PocketOptions`**
1640
+ - `inset?: number` — Shrink the face boundary inward by this many mm before extruding. Produces angled walls when combined with depth. Default: 0 (full face).
1641
+ - `scale?: number` — Scale the face profile uniformly (e.g. 0.8 = 80% of the face area). Mutually exclusive with `inset`; `inset` takes precedence if both are set.
1642
+ - `join?: "Square" | "Round" | "Miter"` — Corner join style when using `inset`. Default: 'Round'.
1643
+
1466
1644
  #### `boss()` — Add a boss (protrusion) from the named face.
1467
1645
 
1468
1646
  ```js
@@ -1485,6 +1663,30 @@ box(50, 50, 20).hole('top', { diameter: 6, counterbore: { diameter: 12, depth: 3
1485
1663
  hole(faceOrRef: SketchFaceTarget | FaceRef, opts: ShapeHoleOptions): Shape
1486
1664
  ```
1487
1665
 
1666
+ **`FaceRef`**
1667
+
1668
+ | Option | Type | Description |
1669
+ |--------|------|-------------|
1670
+ | `normal` | `[ number, number, number ]` | Normal direction of the face |
1671
+ | `center` | `[ number, number, number ]` | Center point of the face |
1672
+ | `query?` | `FaceQueryRef` | Compiler-owned face query when available. |
1673
+ | `planar?` | `boolean` | True when the face can host a 2D sketch placement frame |
1674
+ | `uAxis?` | `[ number, number, number ]` | Face-local horizontal axis for planar faces |
1675
+ | `vAxis?` | `[ number, number, number ]` | Face-local vertical axis for planar faces |
1676
+ | `surface?` | `FaceSurface` | Analytic surface family when the backend can identify one. |
1677
+ | `descendant?` | `FaceDescendantMetadata` | Shared descendant-resolution metadata when this face is a semantic region/set. |
1678
+ | `name` | | — |
1679
+
1680
+ **`FaceDescendantMetadata`**: `kind: "single" | "face-set"`, `semantic: FaceDescendantSemantic`, `memberCount: number`, `memberNames: string[]`, `coplanar: boolean`
1681
+
1682
+ **`ShapeHoleOptions`**: `diameter: number`, `depth?: number`, `upToFace?: SketchFaceTarget | FaceRef`, `extent?: ShapeFeatureExtentOptions`, `u?: number`, `v?: number`, `counterbore?: { diameter: number; depth: number; }`, `countersink?: { diameter: number; angleDeg?: number; }`, `thread?: ShapeHoleThreadOptions`
1683
+
1684
+ `ShapeFeatureExtentOptions`: `{ forward: ShapeFeatureExtentSideOptions, reverse?: ShapeFeatureExtentSideOptions }`
1685
+
1686
+ `ShapeFeatureExtentSideOptions`: `{ depth?: number, upToFace?: SketchFaceTarget | FaceRef, through?: boolean }`
1687
+
1688
+ **`ShapeHoleThreadOptions`**: `designation?: string`, `pitch?: number`, `class?: string`, `handedness?: "right" | "left"`, `depth?: number`, `modeled?: boolean`
1689
+
1488
1690
  #### `cutout()` — Cut a profile-shaped pocket through a face using a placed sketch.
1489
1691
 
1490
1692
  The sketch must be placed on a face with `Sketch.onFace(...)`. The cut follows the sketch's 2D profile.
@@ -1498,6 +1700,8 @@ body.cutout(profile, { depth: 5 })
1498
1700
  cutout(sketch: Sketch, opts?: ShapeCutoutOptions): Shape
1499
1701
  ```
1500
1702
 
1703
+ **`ShapeCutoutOptions`**: `depth?: number`, `upToFace?: SketchFaceTarget | FaceRef`, `extent?: ShapeFeatureExtentOptions`, `taperScale?: number | [ number, number ]`
1704
+
1501
1705
  **Placement**
1502
1706
 
1503
1707
  #### `placeReference()` — Translate the shape so the given anchor or reference lands on the target coordinate.
@@ -1561,6 +1765,11 @@ mast.translate(0, station, radius + 50).seatInto(fuselage, 'mount', { depth: 'fl
1561
1765
  seatInto(target: Shape, surface: string, options?: SeatIntoOptions): Shape
1562
1766
  ```
1563
1767
 
1768
+ **`SeatIntoOptions`**
1769
+ - `along?: [ number, number, number ]` — Movement axis. Default: inverted face normal (points into target).
1770
+ - `depth?: "full" | "flush" | number` — How deep to embed. 'full' = entire face inside. 'flush' = nearest point touches. number = mm past flush. Default: 'full'.
1771
+ - `gap?: number` — Standoff gap in mm. Positive = gap between face and target. Negative = extra penetration. Default: 0.
1772
+
1564
1773
  #### `seatOver()` — Slide this shape until a target's labeled face is fully covered (inside this shape).
1565
1774
 
1566
1775
  The inverse of `seatInto`: instead of embedding *your* face into the target, you move until the *target's* face is embedded inside you.
@@ -1585,6 +1794,10 @@ seatOver(target: Shape, targetSurface: string, options?: SeatIntoOptions): Shape
1585
1794
  withConnectors(connectors: Record<string, ConnectorInput>): Shape
1586
1795
  ```
1587
1796
 
1797
+ **`PortInput`**: `origin?: [ number, number, number ]`, `axis?: [ number, number, number ]`, `start?: [ number, number, number ]`, `end?: [ number, number, number ]`, `up?: [ number, number, number ]`, `kind?: JointType`, `min?: number`, `max?: number`
1798
+
1799
+ `ConnectorInput`: `{ connectorType?: string, gender?: ConnectorGender, measurements?: Record<string, number | string> }`
1800
+
1588
1801
  #### `connectorNames()` — List all connector names on this shape.
1589
1802
 
1590
1803
  ```ts
@@ -1621,6 +1834,8 @@ Overloads:
1621
1834
  matchTo(targetOrPairs: Shape | MatchTarget | Array<[ Shape | MatchTarget, string, string ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): Shape
1622
1835
  ```
1623
1836
 
1837
+ `MatchToOptions`: `{ force?: boolean, angle?: number, distance?: number }`
1838
+
1624
1839
  **References**
1625
1840
 
1626
1841
  #### `withReferences()` — Attach named placement references that survive normal transforms and imports.
@@ -1629,6 +1844,12 @@ matchTo(targetOrPairs: Shape | MatchTarget | Array<[ Shape | MatchTarget, string
1629
1844
  withReferences(refs: PlacementReferenceInput): Shape
1630
1845
  ```
1631
1846
 
1847
+ **`PlacementReferenceInput`**: `points?: Record<string, [ number, number, number ]>`, `edges?: Record<string, PlacementEdgeRef>`, `surfaces?: Record<string, PlacementSurfaceRef>`, `objects?: Record<string, PlacementObjectInput>`
1848
+
1849
+ `PlacementEdgeRef`: `{ start: Vec3, end: Vec3 }`
1850
+
1851
+ `PlacementSurfaceRef`: `{ center: Vec3, normal: Vec3 }`
1852
+
1632
1853
  #### `referenceNames()` — List named placement references carried by this shape.
1633
1854
 
1634
1855
  ```ts
@@ -1790,6 +2011,24 @@ translate(x: number, y: number, z: number): Transform
1790
2011
  rotateAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform
1791
2012
  ```
1792
2013
 
2014
+ #### `rotateX()` — Rotate about the X axis after the current transform (parity with `Shape.rotateX`).
2015
+
2016
+ ```ts
2017
+ rotateX(angleDeg: number, pivot?: Vec3): Transform
2018
+ ```
2019
+
2020
+ #### `rotateY()` — Rotate about the Y axis after the current transform (parity with `Shape.rotateY`).
2021
+
2022
+ ```ts
2023
+ rotateY(angleDeg: number, pivot?: Vec3): Transform
2024
+ ```
2025
+
2026
+ #### `rotateZ()` — Rotate about the Z axis after the current transform (parity with `Shape.rotateZ`).
2027
+
2028
+ ```ts
2029
+ rotateZ(angleDeg: number, pivot?: Vec3): Transform
2030
+ ```
2031
+
1793
2032
  #### `inverse()` — Return the inverse transform.
1794
2033
 
1795
2034
  ```ts
@@ -2133,18 +2372,160 @@ constant(value?: number): Pattern2D
2133
2372
  sineWave(options: Pattern2DSineWaveOptions): Pattern2D
2134
2373
  ```
2135
2374
 
2375
+ **`Pattern2DSineWaveOptions`**
2376
+
2377
+ | Option | Type | Description |
2378
+ |--------|------|-------------|
2379
+ | `direction?` | `Vec2` | Direction the wave advances in UV space. Default: [1, 0]. |
2380
+ | `wavelength` | `number` | Distance between wave peaks in surface millimeters. |
2381
+ | `amplitude?` | `number` | Height amplitude in millimeters. Default: 1. |
2382
+ | `phase?` | `number` | Phase offset in radians. Default: 0. |
2383
+ | `bias?` | `number` | Constant height offset in millimeters. Default: 0. |
2384
+
2136
2385
  #### `stripes()` — Create recessed stripe bands in UV space.
2137
2386
 
2138
2387
  ```ts
2139
2388
  stripes(options: Pattern2DStripesOptions): Pattern2D
2140
2389
  ```
2141
2390
 
2391
+ **`Pattern2DStripesOptions`**
2392
+
2393
+ | Option | Type | Description |
2394
+ |--------|------|-------------|
2395
+ | `direction?` | `Vec2` | Direction perpendicular to the stripe bands in UV space. Default: [1, 0]. |
2396
+ | `spacing` | `number` | Center-to-center spacing in surface millimeters. |
2397
+ | `width` | `number` | Stripe width in surface millimeters. |
2398
+ | `depth?` | `number` | Stripe groove depth in millimeters. Default: 1. |
2399
+
2142
2400
  #### `overUnderWeave()` — Create an over-under woven relief pattern in UV space.
2143
2401
 
2144
2402
  ```ts
2145
2403
  overUnderWeave(options: Pattern2DOverUnderWeaveOptions): Pattern2D
2146
2404
  ```
2147
2405
 
2406
+ **`Pattern2DOverUnderWeaveOptions`**
2407
+
2408
+ | Option | Type | Description |
2409
+ |--------|------|-------------|
2410
+ | `spacing` | `number \| Vec2` | Thread center-to-center spacing. A number uses the same spacing for U and V. |
2411
+ | `threadWidth` | `number \| Vec2` | Thread width. A number uses the same width for U and V. |
2412
+ | `depth?` | `number` | Thread groove depth in millimeters. Default: 0.8. |
2413
+ | `underScale?` | `number` | Relative height of the under-crossing thread. Default: 0.15. |
2414
+
2415
+ ### `HermiteCurve3D`
2416
+
2417
+ **Properties:**
2418
+
2419
+ | Property | Type | Description |
2420
+ |----------|------|-------------|
2421
+ | `p0` | `Vec3` | Start position |
2422
+ | `p1` | `Vec3` | End position |
2423
+ | `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |
2424
+ | `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |
2425
+ | `chordLength` | `number` | Chord length (straight-line distance between endpoints) |
2426
+
2427
+ **Methods:**
2428
+
2429
+ #### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]
2430
+
2431
+ ```ts
2432
+ pointAt(t: number): Vec3
2433
+ ```
2434
+
2435
+ #### `tangentAt()` — Evaluate tangent (first derivative) at parameter t ∈ [0, 1]
2436
+
2437
+ ```ts
2438
+ tangentAt(t: number): Vec3
2439
+ ```
2440
+
2441
+ #### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]
2442
+
2443
+ ```ts
2444
+ curvatureAt(t: number): Vec3
2445
+ ```
2446
+
2447
+ #### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.
2448
+
2449
+ ```ts
2450
+ sample(count?: number): Vec3[]
2451
+ ```
2452
+
2453
+ #### `length()` — Approximate arc length by sampling.
2454
+
2455
+ ```ts
2456
+ length(samples?: number): number
2457
+ ```
2458
+
2459
+ #### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.
2460
+
2461
+ ```ts
2462
+ sampleAdaptive(minCount?: number, maxCount?: number): Vec3[]
2463
+ ```
2464
+
2465
+ #### `toPolyline()` — Convert to a format compatible with sweep() path input.
2466
+
2467
+ ```ts
2468
+ toPolyline(samples?: number): Vec3[]
2469
+ ```
2470
+
2471
+ ### `QuinticHermiteCurve3D`
2472
+
2473
+ **Properties:**
2474
+
2475
+ | Property | Type | Description |
2476
+ |----------|------|-------------|
2477
+ | `p0` | `Vec3` | Start position |
2478
+ | `p1` | `Vec3` | End position |
2479
+ | `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |
2480
+ | `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |
2481
+ | `c0` | `Vec3` | Scaled second derivative at start (curvature * weight² * chordLength²) |
2482
+ | `c1` | `Vec3` | Scaled second derivative at end (curvature * weight² * chordLength²) |
2483
+ | `chordLength` | `number` | Chord length (straight-line distance between endpoints) |
2484
+
2485
+ **Methods:**
2486
+
2487
+ #### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]
2488
+
2489
+ ```ts
2490
+ pointAt(t: number): Vec3
2491
+ ```
2492
+
2493
+ #### `tangentAt()` — Evaluate tangent (first derivative, normalized) at parameter t ∈ [0, 1]
2494
+
2495
+ ```ts
2496
+ tangentAt(t: number): Vec3
2497
+ ```
2498
+
2499
+ #### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]
2500
+
2501
+ ```ts
2502
+ curvatureAt(t: number): Vec3
2503
+ ```
2504
+
2505
+ #### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.
2506
+
2507
+ ```ts
2508
+ sample(count?: number): Vec3[]
2509
+ ```
2510
+
2511
+ #### `length()` — Approximate arc length by sampling.
2512
+
2513
+ ```ts
2514
+ length(samples?: number): number
2515
+ ```
2516
+
2517
+ #### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.
2518
+
2519
+ ```ts
2520
+ sampleAdaptive(minCount?: number, maxCount?: number): Vec3[]
2521
+ ```
2522
+
2523
+ #### `toPolyline()` — Convert to a format compatible with sweep() path input.
2524
+
2525
+ ```ts
2526
+ toPolyline(samples?: number): Vec3[]
2527
+ ```
2528
+
2148
2529
  ### `ShapeRef`
2149
2530
 
2150
2531
  A first-class reference path over a shape's semantic faces and face relationships.
@@ -2337,39 +2718,43 @@ Namespaced file import helpers for formats that should not add new lowercase glo
2337
2718
 
2338
2719
  ForgeCAD uses a **Z-up** right-handed coordinate system.
2339
2720
 
2721
+ For objects with a clear facing direction, model the front/face/nose/camera side toward **-Y**. The rear/back side is **+Y**. In code, a forward/fore direction vector is `[0, -1, 0]`.
2722
+
2340
2723
  ## Axes
2341
2724
 
2342
2725
  | Axis | Direction | Positive |
2343
2726
  |------|-----------------|----------|
2344
2727
  | X | Left / Right | Right |
2345
- | Y | Forward / Back | Forward |
2728
+ | Y | Front / Back | Back |
2346
2729
  | Z | Up / Down | Up |
2347
2730
 
2348
2731
  ## Standard Views
2349
2732
 
2733
+ The camera position direction says where the camera sits relative to the model. A front view camera sits at `-Y` and looks toward `+Y`, so it sees the model's `-Y` front face.
2734
+
2350
2735
  | View | Camera position direction | Sees plane |
2351
- |--------|--------------------------|------------|
2352
- | Front | Y | XZ |
2353
- | Back | +Y | XZ |
2736
+ |--------|---------------------------|------------|
2737
+ | Front | -Y | XZ |
2738
+ | Back | +Y | XZ |
2354
2739
  | Right | +X | YZ |
2355
- | Left | X | YZ |
2740
+ | Left | -X | YZ |
2356
2741
  | Top | +Z | XY |
2357
- | Bottom | Z | XY |
2742
+ | Bottom | -Z | XY |
2358
2743
 
2359
2744
  ## GizmoViewcube Face Mapping
2360
2745
 
2361
- Three.js BoxGeometry material indices vs ForgeCAD labels (Z-up remapping):
2746
+ Renderer/view-cube internals may have their own material ordering. Map any view-cube labels to the ForgeCAD directions below:
2362
2747
 
2363
- | Index | Three.js direction | ForgeCAD label |
2364
- |-------|--------------------|----------------|
2365
- | 0 | +X | Right |
2366
- | 1 | −X | Left |
2367
- | 2 | +Y | Front |
2368
- | 3 | −Y | Back |
2369
- | 4 | +Z | Top |
2370
- | 5 | −Z | Bottom |
2748
+ | Direction | ForgeCAD label |
2749
+ |-----------|----------------|
2750
+ | +X | Right |
2751
+ | -X | Left |
2752
+ | +Y | Back |
2753
+ | -Y | Front |
2754
+ | +Z | Top |
2755
+ | -Z | Bottom |
2371
2756
 
2372
- Default drei labels are Y-up; ForgeCAD passes `faces={['Right','Left','Front','Back','Top','Bottom']}`.
2757
+ The face/anchor API is the source of truth: `front` resolves to the minimum-Y side and `back` resolves to the maximum-Y side.
2373
2758
 
2374
2759
  ## Grid
2375
2760
 
@@ -2397,7 +2782,7 @@ Three.js is Y-up; ForgeCAD is Z-up. Fix applied at camera level (`camera.up = (0
2397
2782
 
2398
2783
  ## Revolution Axis
2399
2784
 
2400
- `CrossSection.revolve()` revolves around Y. Profile X = radial distance, Profile Y = height (becomes Z after revolution). Profile must be at X > 0.
2785
+ `Sketch.revolve()` produces a world Z-axis solid of revolution. Profile X = radial distance from the Z axis, Profile Y = world Z height after revolution. Profile should stay at X > 0 unless intentionally touching the axis.
2401
2786
 
2402
2787
  ## Boolean Winding (3D)
2403
2788
 
@@ -2423,7 +2808,7 @@ Prefer `composeChain(...)` over manual `.mul(...).mul(...)` in kinematics code t
2423
2808
  |---|---|---|---|
2424
2809
  | Winding | Any point order | CCW | `polygon()`, `path().close()` |
2425
2810
  | Up axis | Z-up | Y-up (Three.js) | `camera.up`, gizmo labels |
2426
- | Revolution | "revolve this profile" | Profile in X-Y, X>0 | Documented only |
2811
+ | Revolution | "revolve this profile" | Profile X = radius, Profile Y = world Z height | Documented and regression-tested |
2427
2812
  | Face normals | Doesn't think about it | Outward-pointing | Manifold constructors |
2428
2813
  | Transform order | Left-to-right chain | Post-multiply | Native match |
2429
2814
 
@@ -2439,6 +2824,16 @@ For any fixed assembly where parts are meant to stay in contact in the final mod
2439
2824
 
2440
2825
  Use raw `translate()` and `rotate()` when parts are intentionally free-floating or when you are doing quick exploratory layout. Use `attachTo()` for rough bounding-box placement. But if the relationship is a real interface, make it explicit with connectors.
2441
2826
 
2827
+ ## Mechanisms: connector frames vs link points
2828
+
2829
+ For serial articulated parts (hinges, hips, knees, levers, wheels), use `assembly().connect()` with connectors. `connect()` aligns the full connector frame: `origin` is the pivot/contact point, `axis` is the hinge line or slide direction, and `up` locks the zero-angle twist of the child part. `up` is a local roll reference, not world up; author it explicitly whenever the rest pose matters.
2830
+
2831
+ Do not use `addPart(..., { mate: { connector, toLink } })` when the part must point along a bone or inherit orientation from a link edge. Link mates are point attachments only: they translate the connector origin onto the solved link position and preserve the part's existing rotation. That is good for markers, sensors, labels, and debug handles. It is not a bone-frame API.
2832
+
2833
+ Use link graphs (`link()`, `edgeBetweenLinks()`, `addAngleBetweenLinks()`) when the hard part is solving point positions, especially closed loops. Use connector-frame joints when the hard part is orienting real physical parts.
2834
+
2835
+ For bilateral mechanisms, remember that connector-frame revolute values are physical axis-local values. Mirrored hinge axes need negated physical revolute values for the same mirrored pose, and physical limits mirror as `[min, max] -> [-max, -min]`. If you want `HipR: 10` and `HipL: 10` to mean the same semantic pose, drive a mirrored link graph or write an explicit state mapping instead of assuming equal connector-joint values are symmetric.
2836
+
2442
2837
  ## Primitive origin convention
2443
2838
 
2444
2839
  All 3D primitives are **centered on XY, base at Z=0**:
@@ -2462,14 +2857,14 @@ Most positioning bugs come from manual coordinate arithmetic. Use these methods
2462
2857
 
2463
2858
  ## 1. Connectors + `matchTo()` — default for mating interfaces
2464
2859
 
2465
- Define connectors on parts; `matchTo()` provides automatic 6-DOF alignment. The child translates and rotates so its connector aligns with the target's — origins coincide, axes oppose (plug-in model).
2860
+ Define connectors on parts; `matchTo()` provides automatic alignment. With one connector pair, the child translates and rotates so its connector aligns with the target's — origins coincide, axes oppose (plug-in model), and `up` pins the roll reference. With multiple connector pairs, the connector origins define the rigid transform; still author meaningful `axis` and `up` values so the same connectors remain useful for `connect()`, audits, and future matching.
2466
2861
 
2467
2862
  ```javascript
2468
2863
  const shelf = box(200, 120, 10).translate(0, 0, -5).withConnectors({
2469
- left_tab: connector.male("dovetail", { origin: [-100, 0, 0], axis: [-1, 0, 0] }),
2864
+ left_tab: connector.male("dovetail", { origin: [-100, 0, 0], axis: [-1, 0, 0], up: [0, 0, 1] }),
2470
2865
  });
2471
2866
  const panel = box(12, 120, 200).translate(0, 0, -100).withConnectors({
2472
- shelf_0: connector.female("dovetail", { origin: [6, 0, -50], axis: [1, 0, 0] }),
2867
+ shelf_0: connector.female("dovetail", { origin: [6, 0, -50], axis: [1, 0, 0], up: [0, 0, 1] }),
2473
2868
  });
2474
2869
  const placed = shelf.matchTo(panel, "left_tab", "shelf_0");
2475
2870
  // Dictionary form for multiple pairs on same target:
@@ -3089,7 +3484,6 @@ addRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions):
3089
3484
  | `startAngle?` | `number` | Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive). Default: 0 (rightmost vertex). |
3090
3485
  | `blockRotation?` | `boolean` | Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. |
3091
3486
 
3092
-
3093
3487
  **`ConstrainedRegularPolygon`** extends ConstrainedPolygon
3094
3488
  - `center: PointId` — Center point. Use `sk.fix(poly.center, x, y)` to pin location, or `sk.coincident(poly.center, other)` to align with other geometry.
3095
3489
 
@@ -3329,7 +3723,7 @@ region(seed: [ number, number ]): Sketch
3329
3723
  extrude(height: number, opts?: { twist?: number; divisions?: number; scaleTop?: number | [ number, number ]; }): Shape
3330
3724
  ```
3331
3725
 
3332
- #### `revolve()` — Revolve this 2D sketch around the Y axis to create a 3D solid of revolution.
3726
+ #### `revolve()` — Revolve this 2D sketch around the world Z axis. Sketch X is radius; sketch Y becomes world Z height.
3333
3727
 
3334
3728
  ```ts
3335
3729
  revolve(degrees?: number, segments?: number): Shape
@@ -3360,9 +3754,25 @@ Use this when a 2D profile should be oriented onto a 3D face before extrusion or
3360
3754
  onFace(parentOrFace: Shape | { toShape(): Shape; } | { _bbox(): { min: number[]; max: number[]; }; } | FaceRef, faceOrOpts?: "front" | "back" | "left" | "right" | "top" | "bottom" | string | FaceRef | { u?: number; v?: number; protrude?: number; selfAnchor?: Anchor; }, opts?: { u?: number; v?: number; protrude?: number; selfAnchor?: Anchor; }): Sketch
3361
3755
  ```
3362
3756
 
3363
- **Labels**
3757
+ **`FaceRef`**
3364
3758
 
3365
- #### `labelEdge()` Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch.
3759
+ | Option | Type | Description |
3760
+ |--------|------|-------------|
3761
+ | `normal` | `[ number, number, number ]` | Normal direction of the face |
3762
+ | `center` | `[ number, number, number ]` | Center point of the face |
3763
+ | `query?` | `FaceQueryRef` | Compiler-owned face query when available. |
3764
+ | `planar?` | `boolean` | True when the face can host a 2D sketch placement frame |
3765
+ | `uAxis?` | `[ number, number, number ]` | Face-local horizontal axis for planar faces |
3766
+ | `vAxis?` | `[ number, number, number ]` | Face-local vertical axis for planar faces |
3767
+ | `surface?` | `FaceSurface` | Analytic surface family when the backend can identify one. |
3768
+ | `descendant?` | `FaceDescendantMetadata` | Shared descendant-resolution metadata when this face is a semantic region/set. |
3769
+ | `name` | | — |
3770
+
3771
+ **`FaceDescendantMetadata`**: `kind: "single" | "face-set"`, `semantic: FaceDescendantSemantic`, `memberCount: number`, `memberNames: string[]`, `coplanar: boolean`
3772
+
3773
+ **Labels**
3774
+
3775
+ #### `labelEdge()` — Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch.
3366
3776
 
3367
3777
  ```ts
3368
3778
  labelEdge(name: string): Sketch
@@ -3658,6 +4068,16 @@ regularPolygon(options: RegularPolygonOptions): ConstrainedRegularPolygon
3658
4068
  groupRect(options: GroupRectOptions): ConstrainedGroupRect
3659
4069
  ```
3660
4070
 
4071
+ **`GroupRectOptions`**
4072
+
4073
+ | Option | Type | Description |
4074
+ |--------|------|-------------|
4075
+ | `x?` | `number` | Bottom-left x coordinate (world). Default: 0. |
4076
+ | `y?` | `number` | Bottom-left y coordinate (world). Default: 0. |
4077
+ | `width` | `number` | Width (along x in local coords). Required. |
4078
+ | `height` | `number` | Height (along y in local coords). Required. |
4079
+ | `allowRotation?` | `boolean` | Allow the solver to rotate this rectangle. Default: false. |
4080
+
3661
4081
  **Geometric Constraints**
3662
4082
 
3663
4083
  #### `horizontal()` — Constrain a line to be horizontal (parallel to the X axis).
@@ -4001,6 +4421,23 @@ result.withUpdatedConstraint('cst-5', 120); // update a dimension without rebuil
4001
4421
  solve(options?: SolveOptions): ConstraintSketch | Sketch
4002
4422
  ```
4003
4423
 
4424
+ **`SolveOptions`**
4425
+
4426
+ | Option | Type | Description |
4427
+ |--------|------|-------------|
4428
+ | `iterations?` | `number` | Maximum number of LM outer iterations per restart. |
4429
+ | `tolerance?` | `number` | Infinity-norm residual tolerance for declaring convergence. |
4430
+ | `restarts?` | `number` | Number of deterministic restart seeds used by the global solver. |
4431
+ | `warmStartIterations?` | `number` | Optional projector iterations used only for initialisation, not as the main solver. |
4432
+ | `maxScaledStep?` | `number` | Maximum LM step length in scaled variable space. Larger = bolder, smaller = safer. |
4433
+ | `skipRedundancyCheck?` | `boolean` | Skip redundancy detection (safe when topology is unchanged and previous DOF >= 0). |
4434
+ | `presolveConstraintId?` | `string` | Run the targeted presolve hook for this constraint before the main solve. |
4435
+ | `fallbackRestarts?` | `number` | When set and the first solve exceeds tolerance*5, retry with this many restarts. |
4436
+ | `progressive?` | `boolean` | Add constraints progressively with short LM solves, all in one WASM call. |
4437
+ | `timeBudgetMs?` | `number` | Wall-clock time budget in ms for the entire solve. 0 = no limit. |
4438
+ | `debugConstructiveTranscript?` | `boolean` | Capture a readable constructive transcript in `constraintMeta.debug`. |
4439
+ | `debugSvgSnapshots?` | `boolean` | Capture SVG snapshots for constructive steps in `constraintMeta.debug`. |
4440
+
4004
4441
  #### `solveConstraintsOnly()` — Run the solver without building a full `ConstraintSketch`.
4005
4442
 
4006
4443
  Lighter than `solve()` — skips profile and DOF analysis. Useful for lightweight constraint validation or progress monitoring mid-construction.
@@ -4409,15 +4846,13 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
4409
4846
 
4410
4847
  ## Contents
4411
4848
 
4412
- - [Curves & Surfacing](#curves-surfacing) — `Loft.station`, `Loft.leftRail`, `Loft.rightRail`, `Loft.frontRail`, `Loft.backRail`, `Loft.centerRail`, `Loft.pathOnXz`, `Loft.pathOnYz`, `Loft.pathOnXy`, `Loft.withGuideRails`, `Helix.path`, `Helix.coil`, `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`
4849
+ - [Curves & Surfacing](#curves-surfacing) — `Curve.Blend`, `Curve.BlendG2`, `Curve.Arc`, `Curve.Line`, `Curve.Polyline`, `Curve.Spline`, `Curve.Nurbs`, `Curve.Fit`, `Curve.Trim`, `Curve.Reverse`, `Curve.Route`, `Curve.Helix`, `Loft.station`, `Loft.leftRail`, `Loft.rightRail`, `Loft.frontRail`, `Loft.backRail`, `Loft.centerRail`, `Loft.pathOnXz`, `Loft.pathOnYz`, `Loft.pathOnXy`, `Loft.withGuideRails`, `spline2d`, `loft`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`
4413
4850
  - [Surface Members](#surface-members) — `surfaceBand`, `SurfaceBody`
4414
4851
  - [Curve3D](#curve3d)
4415
- - [HelixCurve](#helixcurve)
4852
+ - [Route3D](#route3d)
4416
4853
  - [NurbsCurve3D](#nurbscurve3d)
4417
4854
  - [NurbsSurface](#nurbssurface)
4418
4855
  - [PathBuilder](#pathbuilder) — Line Segments, Arcs, Curves, Closing & Output
4419
- - [HermiteCurve3D](#hermitecurve3d)
4420
- - [QuinticHermiteCurve3D](#quintichermitecurve3d)
4421
4856
  - [ProductSkin](#productskin)
4422
4857
  - [ProductSurfaceRef](#productsurfaceref)
4423
4858
  - [ProductSurfaceBuilder](#productsurfacebuilder)
@@ -4439,6 +4874,7 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
4439
4874
  - [SurfaceJoinBuilder](#surfacejoinbuilder)
4440
4875
  - [CounterboreBuilder](#counterborebuilder)
4441
4876
  - [RoundedSlotBuilder](#roundedslotbuilder)
4877
+ - [Curve](#curve)
4442
4878
  - [Surface](#surface)
4443
4879
  - [Blend](#blend)
4444
4880
  - [Analysis](#analysis)
@@ -4448,97 +4884,186 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
4448
4884
  - [Slot](#slot)
4449
4885
  - [Counterbore](#counterbore)
4450
4886
  - [Ribs](#ribs)
4451
- - [Helix](#helix)
4452
4887
 
4453
4888
  ## Functions
4454
4889
 
4455
4890
  ### Curves & Surfacing
4456
4891
 
4457
- #### `Loft.station()` — Create a loft station from a 2D profile and an axis position.
4892
+ #### `Curve.Blend()` — Create an exact G1 blend curve between two directed endpoints.
4893
+
4894
+ The returned curve is a cubic non-rational `NurbsCurve3D`: ForgeCAD converts the endpoint positions and tangents into Bezier control points, so the curve can feed `sweep` and exact surface boundaries through the existing `nurbs` IR rather than a sampled polyline.
4895
+
4896
+ ```js
4897
+ const rail = Curve.Blend(
4898
+ { point: [0, 0, 0], tangent: [1, 0, 0], weight: 0.8 },
4899
+ { point: [40, 20, 8], tangent: [0, 1, 0], weight: 0.8 },
4900
+ );
4901
+ const tube = sweep(circle2d(2), rail);
4902
+ ```
4458
4903
 
4459
4904
  ```ts
4460
- Loft.station(profile: Sketch, position: number): LoftStation
4905
+ Curve.Blend(start: CurveBlendEndpoint, end: CurveBlendEndpoint): NurbsCurve3D
4461
4906
  ```
4462
4907
 
4463
- `LoftStation`: `{ profile: Sketch, position: number }`
4908
+ **`CurveBlendEndpoint`**
4909
+ - `point: Vec3` — Endpoint position.
4910
+ - `tangent: Vec3` — Tangent direction at this endpoint. Magnitude is ignored.
4911
+ - `weight?: number` — Tangent reach relative to the endpoint chord length. Default 1.
4464
4912
 
4465
- #### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.
4913
+ #### `Curve.BlendG2()` — Create an exact G2 blend curve between two directed endpoints.
4914
+
4915
+ This is the curvature-aware companion to `Curve.Blend()`. It returns a degree-5 non-rational `NurbsCurve3D` that matches endpoint position, tangent direction, and optional curvature/second-derivative vectors.
4916
+
4917
+ ```js
4918
+ const rail = Curve.BlendG2(
4919
+ { point: [0, 0, 0], tangent: [1, 0, 0], curvature: [0, 0.02, 0] },
4920
+ { point: [50, 20, 0], tangent: [0, 1, 0], curvature: [-0.02, 0, 0] },
4921
+ );
4922
+ ```
4466
4923
 
4467
4924
  ```ts
4468
- Loft.leftRail(path: LoftGuideRailPath): LoftGuideRail
4925
+ Curve.BlendG2(start: CurveBlendG2Endpoint, end: CurveBlendG2Endpoint): NurbsCurve3D
4469
4926
  ```
4470
4927
 
4471
- `LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`
4928
+ **`CurveBlendG2Endpoint`** extends CurveBlendEndpoint
4929
+ - `curvature?: Vec3` — Optional endpoint curvature/second-derivative vector. Default is zero.
4472
4930
 
4473
- #### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.
4931
+ #### `Curve.Arc()` — Create an exact circular 3D arc from start, end, and start tangent.
4932
+
4933
+ 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.
4934
+
4935
+ ```js
4936
+ const rail = Curve.Arc({
4937
+ start: [40, 0, 0],
4938
+ end: [0, 40, 0],
4939
+ tangent: [0, 1, 0],
4940
+ });
4941
+ const tube = sweep(circle2d(2), rail);
4942
+ ```
4474
4943
 
4475
4944
  ```ts
4476
- Loft.rightRail(path: LoftGuideRailPath): LoftGuideRail
4945
+ Curve.Arc(options: CurveArcOptions): NurbsCurve3D
4477
4946
  ```
4478
4947
 
4479
- #### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.
4948
+ **`CurveArcOptions`**
4949
+ - `start: Vec3` — Arc start point.
4950
+ - `end: Vec3` — Arc end point.
4951
+ - `tangent: Vec3` — Tangent direction at the start point. Magnitude is ignored.
4952
+
4953
+ #### `Curve.Line()` — Create an exact straight 3D NURBS line segment.
4954
+
4955
+ ```js
4956
+ const rail = Curve.Line([0, 0, 0], [80, 0, 15]);
4957
+ const rib = sweep(circle2d(2), rail);
4958
+ ```
4480
4959
 
4481
4960
  ```ts
4482
- Loft.frontRail(path: LoftGuideRailPath): LoftGuideRail
4961
+ Curve.Line(start: Vec3, end: Vec3): NurbsCurve3D
4483
4962
  ```
4484
4963
 
4485
- #### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.
4964
+ #### `Curve.Polyline()` — Create a polyline path as cloned 3D points.
4965
+
4966
+ Polylines are exact as route/path input to `sweep`. Use `Curve.Route` when the centerline needs bend and endpoint-frame metadata.
4486
4967
 
4487
4968
  ```ts
4488
- Loft.backRail(path: LoftGuideRailPath): LoftGuideRail
4969
+ Curve.Polyline(points: Vec3[]): Vec3[]
4489
4970
  ```
4490
4971
 
4491
- #### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.
4972
+ #### `Curve.Spline()` — Create a smooth Catmull-Rom spline path.
4973
+
4974
+ This is a smooth sampled curve object. Use `Curve.Nurbs` when the path must preserve exact control-point and knot data.
4492
4975
 
4493
4976
  ```ts
4494
- Loft.centerRail(path: LoftGuideRailPath): LoftGuideRail
4977
+ Curve.Spline(points: Vec3[], options?: Spline3DOptions): Curve3D
4495
4978
  ```
4496
4979
 
4497
- #### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.
4980
+ **`Spline3DOptions`**
4981
+ - `closed?: boolean` — Closed loop (default false).
4982
+ - `tension?: number` — Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5.
4498
4983
 
4499
- The path's first coordinate becomes X and its second coordinate becomes Z. Use this for left/right silhouette rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
4984
+ #### `Curve.Nurbs()` Create an exact NURBS 3D curve from control points, weights, knots, and degree.
4985
+
4986
+ ```js
4987
+ const rail = Curve.Nurbs([[0, 0, 0], [30, 4, 12], [60, -4, 12], [90, 0, 0]]);
4988
+ const tube = sweep(circle2d(2), rail);
4989
+ ```
4500
4990
 
4501
4991
  ```ts
4502
- Loft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]
4992
+ Curve.Nurbs(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D
4503
4993
  ```
4504
4994
 
4505
- #### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.
4995
+ **`NurbsCurve3DOptions`**
4506
4996
 
4507
- The path's first coordinate becomes Y and its second coordinate becomes Z. Use this for front/back crown rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
4997
+ | Option | Type | Description |
4998
+ |--------|------|-------------|
4999
+ | `degree?` | `number` | Polynomial degree (default 3 = cubic). Must be ≥ 1. |
5000
+ | `weights?` | `number[]` | Rational weights, one per control point (default: all 1.0 = non-rational). |
5001
+ | `knots?` | `number[]` | Knot vector (default: uniform clamped). Must have length = controlPoints.length + degree + 1. |
5002
+ | `closed?` | `boolean` | Whether the curve is closed/periodic (default false). |
5003
+
5004
+ #### `Curve.Fit()` — Fit a non-rational NURBS curve that interpolates every input point.
5005
+
5006
+ This is global B-spline interpolation, not approximate curve reduction: ForgeCAD computes chord-length parameters, averaged clamped knots, solves the control points, then verifies the interpolation residual against `tolerance`.
5007
+
5008
+ ```js
5009
+ const rail = Curve.Fit(
5010
+ [[0, 0, 0], [20, 8, 12], [50, -4, 18], [80, 0, 0]],
5011
+ { degree: 3, tolerance: 0.001 },
5012
+ );
5013
+ const tube = sweep(circle2d(2), rail);
5014
+ ```
4508
5015
 
4509
5016
  ```ts
4510
- Loft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]
5017
+ Curve.Fit(points: Vec3[], options?: CurveFitOptions): NurbsCurve3D
4511
5018
  ```
4512
5019
 
4513
- #### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.
5020
+ **`CurveFitOptions`**
5021
+ - `degree?: number` — Polynomial degree. Default is cubic, reduced automatically for short point lists.
5022
+ - `tolerance?: number` — Maximum allowed interpolation residual in model units. Default 1e-7.
4514
5023
 
4515
- The path's first coordinate becomes X and its second coordinate becomes Y. Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
5024
+ #### `Curve.Trim()` Extract an exact curve segment from normalized parameter `start` to `end`.
5025
+
5026
+ `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.
4516
5027
 
4517
5028
  ```ts
4518
- Loft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]
5029
+ Curve.Trim<T extends CurveTrimInput>(curve: T, start: number, end: number): CurveTrimOutput<T>
4519
5030
  ```
4520
5031
 
4521
- #### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.
5032
+ #### `Curve.Reverse()` — Reverse an exact curve without changing its geometry.
4522
5033
 
4523
- Stations define the cross-section family. Guide rails define the side or center paths the loft must pass through. With opposite side rails, the section is scaled to touch both rails. With one side rail, the section keeps its interpolated size unless a center rail is also present.
5034
+ `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.
4524
5035
 
4525
5036
  ```ts
4526
- Loft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape
5037
+ Curve.Reverse<T extends CurveTrimInput>(curve: T): CurveTrimOutput<T>
4527
5038
  ```
4528
5039
 
4529
- **`LoftOptions`**
4530
- - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
4531
- - `boundsPadding?: number` — Optional extra bounds padding.
5040
+ #### `Curve.Route()` — Build analytic 3D line/arc routes for sweeps.
4532
5041
 
4533
- **`LoftWithGuideRailsOptions`** extends LoftOptions
4534
- - `axis?: LoftAxis` — Primary station axis. Default Z.
4535
- - `samples?: number` — Number of generated loft stations including ends. Default scales with station count.
4536
- - `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.
5042
+ `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.
5043
+
5044
+ ```js
5045
+ const route = Curve.Route.fromPolyline(
5046
+ [[0, 0, 0], [0, 0, 50], [40, 0, 50]],
5047
+ { cornerRadius: 12, startPort: 'inlet', endPort: 'outlet' },
5048
+ );
5049
+ const tube = sweep(circle2d(4), route);
5050
+ ```
5051
+
5052
+ ```ts
5053
+ Curve.Route: typeof Route3D
5054
+ ```
4537
5055
 
4538
- #### `Helix.path()` — Create a metadata-bearing helical centerline around the Z axis.
5056
+ #### `Curve.Helix()` — Build helical paths and swept coils.
5057
+
5058
+ `Curve.Helix` is the canonical namespace for helical paths and coils. It uses the same sweep-based lowering as other curve paths.
5059
+
5060
+ ```js
5061
+ const guide = Curve.Helix.path({ radius: 20, pitch: 6, turns: 4 });
5062
+ const spring = Curve.Helix.coil({ radius: 20, pitch: 6, turns: 4, wireRadius: 1 });
5063
+ ```
4539
5064
 
4540
5065
  ```ts
4541
- Helix.path(options: HelixOptions): HelixCurve
5066
+ Curve.Helix: { path(options: HelixOptions): CurveHelixPath; coil: CurveHelixCoil; }
4542
5067
  ```
4543
5068
 
4544
5069
  **`HelixOptions`**
@@ -4553,66 +5078,88 @@ Helix.path(options: HelixOptions): HelixCurve
4553
5078
  | `clockwise?` | `boolean` | Reverse winding direction when viewed from +Z. |
4554
5079
  | `samplesPerTurn?` | `number` | Point samples per turn for the metadata path. Default 32. |
4555
5080
 
4556
- #### `Helix.coil()` Create a solid helical coil by sweeping a profile through helix-local frames.
5081
+ `CurveHelixPath`: `{ radius: number, pitch: number, turns: number, height: number, startAngle: number, clockwise: boolean }`
4557
5082
 
4558
- Overloads:
5083
+ #### `Loft.station()` — Create a loft station from a 2D profile and an axis position.
5084
+
5085
+ ```ts
5086
+ Loft.station(profile: Sketch, position: number): LoftStation
5087
+ ```
5088
+
5089
+ `LoftStation`: `{ profile: Sketch, position: number }`
5090
+
5091
+ #### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.
5092
+
5093
+ ```ts
5094
+ Loft.leftRail(path: LoftGuideRailPath): LoftGuideRail
5095
+ ```
5096
+
5097
+ `LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`
4559
5098
 
4560
- - `Helix.coil(options: HelixCoilOptions): Shape`
4561
- - `Helix.coil(profile: Sketch, options: HelixCoilOptions): Shape`
5099
+ #### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.
4562
5100
 
5101
+ ```ts
5102
+ Loft.rightRail(path: LoftGuideRailPath): LoftGuideRail
5103
+ ```
4563
5104
 
4564
- **`HelixCoilOptions`** extends HelixOptions
4565
- - `wireRadius?: number` — Radius of the circular wire profile. Required unless a custom profile is passed.
4566
- - `profileSegments?: number` — Segment count for the default circular wire profile. Default 24.
4567
- - `divisionsPerTurn?: number` — Sweep path samples per turn. Default 32.
5105
+ #### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.
4568
5106
 
4569
- #### `hermiteTransitionG2()` — Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).
5107
+ ```ts
5108
+ Loft.frontRail(path: LoftGuideRailPath): LoftGuideRail
5109
+ ```
4570
5110
 
4571
- The curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`, and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`, with smooth G2-continuous interpolation matching position, tangent, and curvature.
5111
+ #### `Loft.backRail()` Create a guide rail that constrains the section-local negative-Y side.
4572
5112
 
4573
5113
  ```ts
4574
- hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D
5114
+ Loft.backRail(path: LoftGuideRailPath): LoftGuideRail
4575
5115
  ```
4576
5116
 
4577
- **`QuinticHermiteCurveEndpoint`**
5117
+ #### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.
4578
5118
 
4579
- | Option | Type | Description |
4580
- |--------|------|-------------|
4581
- | `point` | `Vec3` | Position |
4582
- | `tangent` | `Vec3` | Tangent direction (will be normalized internally) |
4583
- | `curvature?` | `Vec3` | Second derivative / curvature vector. Default [0, 0, 0]. |
4584
- | `weight?` | `number` | Weight: scales tangent magnitude relative to chord length. Default 1.0. |
5119
+ ```ts
5120
+ Loft.centerRail(path: LoftGuideRailPath): LoftGuideRail
5121
+ ```
4585
5122
 
4586
- #### `nurbs3d()` — Create a NURBS curve from control points.
5123
+ #### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.
4587
5124
 
4588
- With default options, creates a cubic non-rational B-spline with uniform clamped knots. Set `weights` for rational curves (exact circles, conics). Set `degree` for linear (1), quadratic (2), cubic (3), or higher-order curves.
5125
+ The path's first coordinate becomes X and its second coordinate becomes Z. Use this for left/right silhouette rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
4589
5126
 
4590
- ```js
4591
- // Simple cubic B-spline through control points
4592
- const curve = nurbs3d([[0,0,0], [10,5,0], [20,-5,10], [30,0,5]]);
4593
- const tube = sweep(circle(2), curve);
5127
+ ```ts
5128
+ Loft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]
4594
5129
  ```
4595
5130
 
4596
- ```js
4597
- // Rational quadratic — exact circular arc
4598
- const arc = nurbs3d(
4599
- [[10,0,0], [10,10,0], [0,10,0]],
4600
- { degree: 2, weights: [1, Math.SQRT1_2, 1] }
4601
- );
5131
+ #### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.
5132
+
5133
+ The path's first coordinate becomes Y and its second coordinate becomes Z. Use this for front/back crown rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
5134
+
5135
+ ```ts
5136
+ Loft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]
4602
5137
  ```
4603
5138
 
5139
+ #### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.
5140
+
5141
+ The path's first coordinate becomes X and its second coordinate becomes Y. Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
5142
+
4604
5143
  ```ts
4605
- nurbs3d(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D
5144
+ Loft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]
4606
5145
  ```
4607
5146
 
4608
- **`NurbsCurve3DOptions`**
5147
+ #### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.
4609
5148
 
4610
- | Option | Type | Description |
4611
- |--------|------|-------------|
4612
- | `degree?` | `number` | Polynomial degree (default 3 = cubic). Must be ≥ 1. |
4613
- | `weights?` | `number[]` | Rational weights, one per control point (default: all 1.0 = non-rational). |
4614
- | `knots?` | `number[]` | Knot vector (default: uniform clamped). Must have length = controlPoints.length + degree + 1. |
4615
- | `closed?` | `boolean` | Whether the curve is closed/periodic (default false). |
5149
+ Stations define the cross-section family. Guide rails define the side or center paths the loft must pass through. With opposite side rails, the section is scaled to touch both rails. With one side rail, the section keeps its interpolated size unless a center rail is also present.
5150
+
5151
+ ```ts
5152
+ Loft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape
5153
+ ```
5154
+
5155
+ **`LoftOptions`**
5156
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
5157
+ - `boundsPadding?: number` — Optional extra bounds padding.
5158
+
5159
+ **`LoftWithGuideRailsOptions`** extends LoftOptions
5160
+ - `axis?: LoftAxis` — Primary station axis. Default Z.
5161
+ - `samples?: number` — Number of generated loft stations including ends. Default scales with station count.
5162
+ - `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.
4616
5163
 
4617
5164
  #### `spline2d()` — Build a smooth Catmull-Rom spline sketch from 2D control points.
4618
5165
 
@@ -4632,18 +5179,6 @@ spline2d(points: Vec2[], options?: Spline2DOptions): Sketch
4632
5179
  | `strokeWidth?` | `number` | For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. |
4633
5180
  | `join?` | `"Round" \| "Square"` | Stroke join for open splines. Default 'Round'. |
4634
5181
 
4635
- #### `spline3d()` — Create a reusable 3D spline curve object (Catmull-Rom).
4636
-
4637
- The returned Curve3D provides sample(), pointAt(t), tangentAt(t), and length() for downstream use in sweep() or manual path operations.
4638
-
4639
- ```ts
4640
- spline3d(points: Vec3[], options?: Spline3DOptions): Curve3D
4641
- ```
4642
-
4643
- **`Spline3DOptions`**
4644
- - `closed?: boolean` — Closed loop (default false).
4645
- - `tension?: number` — Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5.
4646
-
4647
5182
  #### `loft()` — Loft between multiple sketches along Z stations.
4648
5183
 
4649
5184
  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.
@@ -4654,29 +5189,6 @@ Performance note: loft is significantly heavier than primitive/extrude/revolve.
4654
5189
  loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
4655
5190
  ```
4656
5191
 
4657
- #### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.
4658
-
4659
- Unlike loft() which only supports Z heights, loftAlongSpine() places each profile at a position along a 3D spine, oriented perpendicular to the spine tangent. This enables lofting along curved paths — e.g., a wing root-to-tip transition that follows a swept-back leading edge.
4660
-
4661
- The tValues array specifies where each profile sits along the spine (0 = start, 1 = end). Must have the same length as profiles and be in [0, 1].
4662
-
4663
- Internally uses variableSweep infrastructure with SDF interpolation.
4664
-
4665
- Performance note: uses level-set meshing, heavier than simple loft().
4666
-
4667
- ```ts
4668
- loftAlongSpine(profiles: Sketch[], spine: Curve3D | Vec3[], tValues: number[], options?: LoftAlongSpineOptions): Shape
4669
- ```
4670
-
4671
- **`LoftAlongSpineOptions`**
4672
-
4673
- | Option | Type | Description |
4674
- |--------|------|-------------|
4675
- | `samples?` | `number` | Number of samples when spine is a Curve3D. Default 48. |
4676
- | `edgeLength?` | `number` | Marching-grid edge length for level-set meshing. Smaller = finer. |
4677
- | `boundsPadding?` | `number` | Optional extra bounds padding. |
4678
- | `up?` | `Vec3` | Preferred "up" vector for local profile frame. Auto fallback is used near parallel segments. |
4679
-
4680
5192
  #### `sweep()`
4681
5193
 
4682
5194
  ```ts
@@ -4800,106 +5312,6 @@ surfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape
4800
5312
  - `thickness?: number` — Thickness of the generated solid. Default 0 for an open exact sheet.
4801
5313
  - `approximate?: boolean` — Allow explicit approximation for non-exact curve inputs such as Curve3D samples.
4802
5314
 
4803
- #### `transitionCurve()` — Create a smooth transition curve between two edges.
4804
-
4805
- Returns a `HermiteCurve3D` that starts at `edgeA.point` tangent to `edgeA.tangent` and ends at `edgeB.point` tangent to `edgeB.tangent`.
4806
-
4807
- The curve maintains G1 continuity (matching tangent direction) at both endpoints. Weight parameters control the shape of the transition.
4808
-
4809
- ```js
4810
- // Connect two edges with a balanced transition
4811
- const curve = transitionCurve(
4812
- { point: [0, 0, 0], tangent: [1, 0, 0] },
4813
- { point: [10, 5, 0], tangent: [1, 0, 0] },
4814
- );
4815
- ```
4816
-
4817
- // Weighted: curve hugs edge A longer const weighted = transitionCurve( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 0], tangent: [1, 0, 0] }, { weightA: 2.0, weightB: 0.5 }, );
4818
-
4819
- ```
4820
-
4821
- ```ts
4822
- transitionCurve(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionCurveOptions): HermiteCurve3D
4823
- ```
4824
-
4825
- **`TransitionEdge`**
4826
- - `point: Vec3` — Connection point on the edge. Can be any point along the edge where the transition should connect.
4827
- - `tangent: Vec3` — Tangent direction at the connection point. This is the direction the curve should initially follow when leaving this edge. For a straight edge, this is typically the edge direction pointing "outward" (away from the body of the edge, toward the other edge).
4828
- - `normal?: Vec3` — Surface normal at the connection point (optional). Used as a hint for the sweep frame's up vector.
4829
-
4830
- **`TransitionCurveOptions`**
4831
- - `weightA?: number` — Weight for the start edge. Controls tangent magnitude at the start. - 1.0 (default): balanced transition - > 1.0: curve follows start edge longer before turning - < 1.0: curve turns sooner at the start
4832
- - `weightB?: number` — Weight for the end edge. Controls tangent magnitude at the end. - 1.0 (default): balanced transition - > 1.0: curve follows end edge longer before turning - < 1.0: curve turns sooner at the end
4833
- - `samples?: number` — Number of sample points for the output polyline. Default 64. Higher values give smoother curves at the cost of more geometry.
4834
-
4835
- #### `transitionSurface()` — Create a solid transition surface between two edges by sweeping a profile along a Hermite transition curve.
4836
-
4837
- This produces a watertight solid that smoothly connects the two edges. Works with both Manifold and OCCT backends.
4838
-
4839
- ```js
4840
- // Circular tube connecting two edges
4841
- const tube = transitionSurface(
4842
- { point: [0, 0, 0], tangent: [1, 0, 0] },
4843
- { point: [10, 5, 3], tangent: [0, 1, 0] },
4844
- { radius: 0.5 },
4845
- );
4846
- ```
4847
-
4848
- // Custom profile with weights const custom = transitionSurface( { point: [0, 0, 0], tangent: [1, 0, 0] }, { point: [10, 5, 3], tangent: [0, 1, 0] }, { profile: mySketch, weightA: 1.5, weightB: 0.8 }, );
4849
-
4850
- ```
4851
-
4852
- ```ts
4853
- transitionSurface(edgeA: TransitionEdge, edgeB: TransitionEdge, options?: TransitionSurfaceOptions): Shape
4854
- ```
4855
-
4856
-
4857
- **`TransitionSurfaceOptions`** extends TransitionCurveOptions
4858
-
4859
- | Option | Type | Description |
4860
- |--------|------|-------------|
4861
- | `profile?` | `Sketch` | Cross-section profile to sweep along the transition curve. If omitted, a circular profile with `radius` is used. |
4862
- | `radius?` | `number` | Radius of circular cross-section (used when `profile` is omitted). Default: 5% of chord length. |
4863
- | `rectangleSection?` | `{ width: number; height: number; }` | Width and height for rectangular cross-section. Alternative to `radius` when `profile` is omitted. |
4864
- | `up?` | `Vec3` | Preferred up vector for the sweep frame. Default: auto-detected. |
4865
- | `edgeLength?` | `number` | Edge length for level-set meshing. Smaller = finer. |
4866
- | `boundsPadding?` | `number` | Extra bounds padding for level-set meshing. |
4867
-
4868
- #### `connectEdges()` — Create a transition surface or solid bridge between two edge segments.
4869
-
4870
- Tangents can be inferred from neighboring geometry or supplied explicitly through `options`. This is useful for loft-like blends where you want a direct connection between two edge spans.
4871
-
4872
- ```ts
4873
- connectEdges(edgeA: EdgeSegment, edgeB: EdgeSegment, options?: ConnectEdgesOptions): Shape
4874
- ```
4875
-
4876
- **`EdgeSegment`**
4877
-
4878
- | Option | Type | Description |
4879
- |--------|------|-------------|
4880
- | `index` | `number` | Stable index within the extraction (deterministic for a given mesh). |
4881
- | `direction` | `Vec3` | Normalized direction from start → end. |
4882
- | `dihedralAngle` | `number` | Dihedral angle in degrees (0 = coplanar, 180 = knife edge). |
4883
- | `convex` | `boolean` | true = outside corner (convex), false = inside corner (concave). |
4884
- | `normalA` | `Vec3` | Normal of first adjacent face. |
4885
- | `normalB` | `Vec3` | Normal of second adjacent face (same as normalA for boundary edges). |
4886
- | `boundary` | `boolean` | true if this is a boundary (unmatched) edge — unusual for closed solids. |
4887
- | `start`, `end`, `midpoint`, `length` | | — |
4888
-
4889
-
4890
- **`ConnectEdgesOptions`** extends TransitionSurfaceOptions
4891
-
4892
- | Option | Type | Description |
4893
- |--------|------|-------------|
4894
- | `endA?` | `EdgeEnd` | Which end of edge A to connect. Default: 'start'. |
4895
- | `endB?` | `EdgeEnd` | Which end of edge B to connect. Default: 'start'. |
4896
- | `tangentModeA?` | `TangentMode` | Tangent mode for edge A. Default: 'along'. |
4897
- | `tangentModeB?` | `TangentMode` | Tangent mode for edge B. Default: 'along'. |
4898
- | `tangentA?` | `Vec3` | Explicit tangent for edge A. |
4899
- | `tangentB?` | `Vec3` | Explicit tangent for edge B. |
4900
- | `flipA?` | `boolean` | Flip tangent A. |
4901
- | `flipB?` | `boolean` | Flip tangent B. |
4902
-
4903
5315
  ### Surface Members
4904
5316
 
4905
5317
  #### `surfaceBand()`
@@ -4979,47 +5391,76 @@ tangentAt(t: number): Vec3
4979
5391
  length(samples?: number): number
4980
5392
  ```
4981
5393
 
4982
- ### `HelixCurve`
5394
+ ### `Route3D`
4983
5395
 
4984
- Metadata-bearing helical curve around the Z axis. Use `Helix.path(...)` for sampling, placement, or `sweep()`, and `Helix.coil(...)` for helix-oriented solids.
5396
+ Metadata-bearing analytic 3D route made from line and arc segments.
4985
5397
 
4986
- **Properties:**
5398
+ Use `Curve.Route.fromPolyline()` when you know the virtual design skeleton points and bend radius. ForgeCAD computes tangent trim points, bend arcs, total length, and named start/end port frames. Pass the route directly to `sweep()`.
4987
5399
 
4988
- | Property | Type | Description |
4989
- |----------|------|-------------|
4990
- | `radius` | `number` | |
4991
- | `pitch` | `number` | |
4992
- | `turns` | `number` | — |
4993
- | `height` | `number` | — |
4994
- | `startAngle` | `number` | — |
4995
- | `clockwise` | `boolean` | — |
5400
+ ```js
5401
+ const route = Curve.Route.fromPolyline(
5402
+ [[0, 0, 0], [0, 0, 80], [60, 0, 80]],
5403
+ { cornerRadius: 24, startPort: "inlet", endPort: "outlet" },
5404
+ );
5405
+ const pipe = sweep(difference2d(circle2d(8), circle2d(6)), route);
5406
+ const outlet = route.port("outlet");
5407
+ ```
4996
5408
 
4997
- **Methods:**
5409
+ #### `fromPolyline()` — Build a line/arc route from virtual polyline corner points.
4998
5410
 
4999
- #### `pointAt()`
5411
+ ```ts
5412
+ static fromPolyline(points: Route3DVec3[], options?: Route3DFromPolylineOptions): Route3D
5413
+ ```
5414
+
5415
+ **`Route3DFromPolylineOptions`**
5416
+
5417
+ | Option | Type | Description |
5418
+ |--------|------|-------------|
5419
+ | `cornerRadius?` | `number` | Bend radius applied to every virtual interior corner. Default 0 keeps sharp polyline corners. |
5420
+ | `startPort?` | `string` | Name for the start port. Default "start". |
5421
+ | `endPort?` | `string` | Name for the end port. Default "end". |
5422
+ | `up?` | `Vec3` | Preferred up vector for deterministic port frames. Default [0, 0, 1]. |
5423
+
5424
+ #### `length()` — Total centerline length, including line and bend arc segments.
5000
5425
 
5001
5426
  ```ts
5002
- pointAt(t: number): Vec3
5427
+ get length(): number
5003
5428
  ```
5004
5429
 
5005
- #### `tangentAt()`
5430
+ #### `segments()` — Exact line and arc segments that make up this route.
5006
5431
 
5007
5432
  ```ts
5008
- tangentAt(t: number): Vec3
5433
+ get segments(): Route3DSegment[]
5009
5434
  ```
5010
5435
 
5011
- #### `sample()`
5436
+ #### `ports()` — Named port frames, keyed by port name.
5012
5437
 
5013
5438
  ```ts
5014
- sample(count?: number): Vec3[]
5439
+ get ports(): Record<string, RoutePortFrame>
5015
5440
  ```
5016
5441
 
5017
- #### `length()`
5442
+ #### `port()` — Return one named route port frame.
5443
+
5444
+ ```ts
5445
+ port(name: string): RoutePortFrame
5446
+ ```
5447
+
5448
+ #### `toSweepPathPlan()` — Convert this route to the compile plan consumed by sweep().
5018
5449
 
5019
5450
  ```ts
5020
- length(): number
5451
+ toSweepPathPlan(): SweepPathCompilePlan
5021
5452
  ```
5022
5453
 
5454
+ #### `toPolyline()` — Sample this analytic route as a polyline for inspection or backend lowering.
5455
+
5456
+ ```ts
5457
+ toPolyline(options?: number | Route3DToPolylineOptions): Route3DVec3[]
5458
+ ```
5459
+
5460
+ **`Route3DToPolylineOptions`**
5461
+ - `samples?: number` — Approximate target point count for the full route.
5462
+ - `maxAngleDeg?: number` — Maximum angular spacing on arc segments. Default 6 degrees.
5463
+
5023
5464
  ### `NurbsCurve3D`
5024
5465
 
5025
5466
  **Properties:**
@@ -5393,120 +5834,6 @@ toPolyline(): [ number, number ][]
5393
5834
  closeOffset(delta: number, join?: "Round" | "Square" | "Miter"): Sketch
5394
5835
  ```
5395
5836
 
5396
- ### `HermiteCurve3D`
5397
-
5398
- **Properties:**
5399
-
5400
- | Property | Type | Description |
5401
- |----------|------|-------------|
5402
- | `p0` | `Vec3` | Start position |
5403
- | `p1` | `Vec3` | End position |
5404
- | `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |
5405
- | `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |
5406
- | `chordLength` | `number` | Chord length (straight-line distance between endpoints) |
5407
-
5408
- **Methods:**
5409
-
5410
- #### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]
5411
-
5412
- ```ts
5413
- pointAt(t: number): Vec3
5414
- ```
5415
-
5416
- #### `tangentAt()` — Evaluate tangent (first derivative) at parameter t ∈ [0, 1]
5417
-
5418
- ```ts
5419
- tangentAt(t: number): Vec3
5420
- ```
5421
-
5422
- #### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]
5423
-
5424
- ```ts
5425
- curvatureAt(t: number): Vec3
5426
- ```
5427
-
5428
- #### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.
5429
-
5430
- ```ts
5431
- sample(count?: number): Vec3[]
5432
- ```
5433
-
5434
- #### `length()` — Approximate arc length by sampling.
5435
-
5436
- ```ts
5437
- length(samples?: number): number
5438
- ```
5439
-
5440
- #### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.
5441
-
5442
- ```ts
5443
- sampleAdaptive(minCount?: number, maxCount?: number): Vec3[]
5444
- ```
5445
-
5446
- #### `toPolyline()` — Convert to a format compatible with sweep() path input.
5447
-
5448
- ```ts
5449
- toPolyline(samples?: number): Vec3[]
5450
- ```
5451
-
5452
- ### `QuinticHermiteCurve3D`
5453
-
5454
- **Properties:**
5455
-
5456
- | Property | Type | Description |
5457
- |----------|------|-------------|
5458
- | `p0` | `Vec3` | Start position |
5459
- | `p1` | `Vec3` | End position |
5460
- | `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |
5461
- | `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |
5462
- | `c0` | `Vec3` | Scaled second derivative at start (curvature * weight² * chordLength²) |
5463
- | `c1` | `Vec3` | Scaled second derivative at end (curvature * weight² * chordLength²) |
5464
- | `chordLength` | `number` | Chord length (straight-line distance between endpoints) |
5465
-
5466
- **Methods:**
5467
-
5468
- #### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]
5469
-
5470
- ```ts
5471
- pointAt(t: number): Vec3
5472
- ```
5473
-
5474
- #### `tangentAt()` — Evaluate tangent (first derivative, normalized) at parameter t ∈ [0, 1]
5475
-
5476
- ```ts
5477
- tangentAt(t: number): Vec3
5478
- ```
5479
-
5480
- #### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]
5481
-
5482
- ```ts
5483
- curvatureAt(t: number): Vec3
5484
- ```
5485
-
5486
- #### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.
5487
-
5488
- ```ts
5489
- sample(count?: number): Vec3[]
5490
- ```
5491
-
5492
- #### `length()` — Approximate arc length by sampling.
5493
-
5494
- ```ts
5495
- length(samples?: number): number
5496
- ```
5497
-
5498
- #### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.
5499
-
5500
- ```ts
5501
- sampleAdaptive(minCount?: number, maxCount?: number): Vec3[]
5502
- ```
5503
-
5504
- #### `toPolyline()` — Convert to a format compatible with sweep() path input.
5505
-
5506
- ```ts
5507
- toPolyline(samples?: number): Vec3[]
5508
- ```
5509
-
5510
5837
  ### `ProductSkin`
5511
5838
 
5512
5839
  **Properties:**
@@ -5939,7 +6266,6 @@ attachTo(ref: ProductRefInput, options?: ProductPanelAttachOptions): Shape
5939
6266
 
5940
6267
  `ProductSurfaceRef`
5941
6268
 
5942
-
5943
6269
  `ProductPanelAttachOptions`: `{ at?: Partial<ProductSkinRefQuery>, thickness?: number, material?: ProductMaterial, color?: string }`
5944
6270
 
5945
6271
  ### `ProductRibbonBuilder`
@@ -6263,6 +6589,8 @@ bottom(options?: { angle?: number; offset?: number; }): SurfaceAnchor<CylinderSu
6263
6589
  pointAt(coordinate: CylinderSurfaceCoordinate): Vec3
6264
6590
  ```
6265
6591
 
6592
+ `CylinderSurfaceCoordinate`: `{ kind?: "cylinder", angle: number, z: number, offset?: number }`
6593
+
6266
6594
  #### `mirrorPoint()`
6267
6595
 
6268
6596
  ```ts
@@ -6382,6 +6710,8 @@ bottom(options?: { x?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCo
6382
6710
  pointAt(coordinate: PlaneSurfaceCoordinate): Vec3
6383
6711
  ```
6384
6712
 
6713
+ `PlaneSurfaceCoordinate`: `{ kind?: "plane", x: number, y: number, offset?: number }`
6714
+
6385
6715
  #### `mirrorPoint()`
6386
6716
 
6387
6717
  ```ts
@@ -6666,6 +6996,8 @@ boundaries(samples?: number): SurfaceBandBoundarySample[]
6666
6996
  withHole(name: string, input: SurfaceBandHoleInput): SurfaceBand<C>
6667
6997
  ```
6668
6998
 
6999
+ `SurfaceBandHoleInput`: `{ length: number, width: number, along?: number, across?: number }`
7000
+
6669
7001
  #### `holes()` — Resolve recorded hole regions into member-local across/along loops.
6670
7002
 
6671
7003
  ```ts
@@ -6688,6 +7020,8 @@ holes(): SurfaceBandHoleRegion[]
6688
7020
  carrier(carrier: CarrierSurface): this
6689
7021
  ```
6690
7022
 
7023
+ `CarrierSurface`: `{ name: string, kind: SurfaceCarrierKind }`
7024
+
6691
7025
  #### `member()`
6692
7026
 
6693
7027
  ```ts
@@ -6732,6 +7066,8 @@ band(): this
6732
7066
  at(anchor: SurfaceAnchor<C>): this
6733
7067
  ```
6734
7068
 
7069
+ `SurfaceAnchor`: `{ carrier: CarrierSurface<C>, coordinate: C }`
7070
+
6735
7071
  #### `size()`
6736
7072
 
6737
7073
  ```ts
@@ -6750,6 +7086,10 @@ path(path: SurfacePath<C> | SurfacePathBuilder<C>): this
6750
7086
  section(section: MemberSectionInput): this
6751
7087
  ```
6752
7088
 
7089
+ **`MemberSectionInput`**: `width?: number`, `thickness: number`, `edgeRadius?: number`, `direction?: MemberOutwardDirection`, `material?: ProductMaterial`, `stations?: MemberSectionStation[]`
7090
+
7091
+ `MemberSectionStation`: `{ t: number, width?: number, thickness?: number }`
7092
+
6753
7093
  #### `cap()`
6754
7094
 
6755
7095
  ```ts
@@ -6762,6 +7102,8 @@ cap(style: SurfaceBandCap): this
6762
7102
  slot(name: string, feature: MemberFeature | RoundedSlotBuilder): this
6763
7103
  ```
6764
7104
 
7105
+ **`MemberFeature`**: `type: MemberFeatureType`, `name?: string`, `length?: number`, `width?: number`, `diameter?: number`, `counterboreDiameter?: number`, `clearanceDiameter?: number`, `height?: number`, `depth?: number`, `count?: number`, `along?: number`, `across?: number`, `verticalTravel?: number`
7106
+
6765
7107
  #### `cutout()`
6766
7108
 
6767
7109
  ```ts
@@ -6886,6 +7228,25 @@ toFeature(name?: string): MemberFeature
6886
7228
 
6887
7229
  ## Constants
6888
7230
 
7231
+ ### `Curve`
7232
+
7233
+ Canonical exact/smooth 3D curve constructors.
7234
+
7235
+ `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.
7236
+
7237
+ - `Blend(start: CurveBlendEndpoint, end: CurveBlendEndpoint): NurbsCurve3D` — Create an exact G1 blend curve between two directed endpoints. The returned curve is a cubic non-rational `NurbsCurve3D`: ForgeCAD converts the endpoint positions and tangents into Bezier control points, so the curve can feed `sweep` and exact surface boundaries through the existing `nurbs` IR rather than a sampled polyline.
7238
+ - `BlendG2(start: CurveBlendG2Endpoint, end: CurveBlendG2Endpoint): NurbsCurve3D` — Create an exact G2 blend curve between two directed endpoints. This is the curvature-aware companion to `Curve.Blend()`. It returns a degree-5 non-rational `NurbsCurve3D` that matches endpoint position, tangent direction, and optional curvature/second-derivative vectors.
7239
+ - `Arc(options: CurveArcOptions): NurbsCurve3D` — Create an exact circular 3D arc from start, end, and start tangent. 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.
7240
+ - `Line(start: Vec3, end: Vec3): NurbsCurve3D` — Create an exact straight 3D NURBS line segment.
7241
+ - `Polyline(points: Vec3[]): Vec3[]` — Create a polyline path as cloned 3D points. Polylines are exact as route/path input to `sweep`. Use `Curve.Route` when the centerline needs bend and endpoint-frame metadata.
7242
+ - `Spline(points: Vec3[], options?: Spline3DOptions): Curve3D` — Create a smooth Catmull-Rom spline path. This is a smooth sampled curve object. Use `Curve.Nurbs` when the path must preserve exact control-point and knot data.
7243
+ - `Nurbs(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D` — Create an exact NURBS 3D curve from control points, weights, knots, and degree.
7244
+ - `Fit(points: Vec3[], options?: CurveFitOptions): NurbsCurve3D` — Fit a non-rational NURBS curve that interpolates every input point. This is global B-spline interpolation, not approximate curve reduction: ForgeCAD computes chord-length parameters, averaged clamped knots, solves the control points, then verifies the interpolation residual against `tolerance`.
7245
+ - `Trim<T extends CurveTrimInput>(curve: T, start: number, end: number): CurveTrimOutput<T>` — Extract an exact curve segment from normalized parameter `start` to `end`. `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.
7246
+ - `Reverse<T extends CurveTrimInput>(curve: T): CurveTrimOutput<T>` — Reverse an exact curve without changing its geometry. `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.
7247
+ - `Route: typeof Route3D` — Build analytic 3D line/arc routes for sweeps. `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.
7248
+ - `Helix: { path(options: HelixOptions): CurveHelixPath; coil: CurveHelixCoil; }` — Build helical paths and swept coils. `Curve.Helix` is the canonical namespace for helical paths and coils. It uses the same sweep-based lowering as other curve paths.
7249
+
6889
7250
  ### `Surface`
6890
7251
 
6891
7252
  - `Plane(options: SurfacePlaneOptions): Shape` — Create a finite analytic plane sheet that can be trimmed, sewn, thickened, or used as a low-level face.
@@ -6968,32 +7329,18 @@ toFeature(name?: string): MemberFeature
6968
7329
 
6969
7330
  - `repeated(input: { count: number; height: number; }): MemberFeature` — Create repeated ribs that belong to a surface member before lowering.
6970
7331
 
6971
- ### `Helix`
6972
-
6973
- Helical curve helpers.
6974
-
6975
- ```ts
6976
- const guide = Helix.path({ radius: 20, pitch: 6, turns: 4 });
6977
- const wire = Helix.coil({ radius: 20, pitch: 6, turns: 4, wireRadius: 1 });
6978
- const rectangular = Helix.coil(rect(2, 1), { radius: 20, height: 30, turns: 5 });
6979
- ```
6980
-
6981
- - `path(options: HelixOptions): HelixCurve` — Create a metadata-bearing helical centerline around the Z axis.
6982
- - `coil(options: HelixCoilOptions): Shape` — Create a solid helical coil by sweeping a circular profile through helix-local frames.
6983
- - `coil(profile: Sketch, options: HelixCoilOptions): Shape` — Create a solid helical coil by sweeping a profile through helix-local frames.
6984
-
6985
7332
  ---
6986
7333
 
6987
7334
  <!-- generated/assembly.md -->
6988
7335
 
6989
7336
  # Assembly API
6990
7337
 
6991
- Kinematic assemblies, joints, couplings, and robot export.
7338
+ Assembly-owned links, constraints, connectors, solved poses, and robot export.
6992
7339
 
6993
7340
  ## Contents
6994
7341
 
6995
- - [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`, `joint`
6996
- - [Assembly](#assembly) — Structure, Connectors, References, Joints, Solving
7342
+ - [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`
7343
+ - [Assembly](#assembly) — Kinematics, Structure, Connectors, References, Solving
6997
7344
  - [ImportedAssembly](#importedassembly)
6998
7345
  - [SolvedAssembly](#solvedassembly)
6999
7346
  - [MateBuilder](#matebuilder)
@@ -7019,110 +7366,219 @@ bomToCsv(rows: BomRow[]): string
7019
7366
  | `tags?` | `string \| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |
7020
7367
  | `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |
7021
7368
 
7022
- #### `assembly()` — Create an assembly container with named parts and joints for kinematic mechanisms.
7369
+ #### `assembly()` — Create an assembly container with named parts, connectors, and kinematic links.
7370
+
7371
+ **Use this from iteration 1 for any model with moving parts.** Do not build one static pose and retrofit motion later.
7372
+
7373
+ Links are named kinematic markers in the assembly. `edgeBetweenLinks()` records structural distances or visual relationships. `addAngleBetweenLinks()` records measured, limited, or controlled angles. These APIs solve point positions, not rigid-body frames.
7023
7374
 
7024
- **Use this from iteration 1 for any model with moving parts.** Hinges, sliders, gears, articulated fingers, doors all start with `assembly()`, not with manual rotation math. Don't build a static "extended pose" first and refactor to an assembly later: joint sliders, animations, sweeps, collision detection, and robot export all flow from the kinematic graph.
7375
+ `addPart(..., { mate })` attaches a part connector origin to a solved link point by translation only. It is right for markers and point-following geometry. Use `connect()` / `match()` for physical articulated parts that need full connector frame alignment and deterministic rest orientation.
7025
7376
 
7026
- An assembly models a mechanism as a directed graph of parts connected by joints. Parts are the nodes; joints are directed edges from parent to child. The graph must be a forest (no cycles). Root parts (those with no incoming joint) are anchored to world space.
7377
+ Return an `Assembly` directly to expose its connector joints and driven link controls in the editor. Moving those controls re-runs the assembly solve with the new state, so closed-loop link/edge mechanisms move through the real kinematic solver instead of a viewport-only forward-kinematics approximation.
7027
7378
 
7028
- Three joint types are supported: `'revolute'` (hinge), `'prismatic'` (slider), and `'fixed'` (rigid attachment). Use `addPart()` to add geometry, `addJoint()` (or the shorthands `addRevolute()`, `addPrismatic()`, `addFixed()`) to connect parts, and `solve()` to compute world-space positions at a given joint state.
7379
+ If no link in a connected kinematic component is fixed, ForgeCAD chooses a deterministic gauge link for solving and reports a floating-component warning.
7029
7380
 
7030
- The higher-level `connect()` API uses declared **connectors** to compute joint frames automatically. The `match()` API uses typed connectors (with gender and type metadata) for automatic compatibility validation and joint creation.
7381
+ The legacy joint-chain APIs still exist for compatibility and exporter plumbing. New work should choose between point-link kinematics and connector-frame joints based on whether the part needs orientation.
7031
7382
 
7032
7383
  For multi-file assemblies, a file that returns an `Assembly` is importable via [`require()`](/docs/core#require) and yields an `ImportedAssembly`. Use `mergeInto()` to flatten a sub-assembly into a parent assembly.
7033
7384
 
7385
+ **Point-link example**
7386
+
7387
+ This snippet mates a marker to the solved `tip` point. It does not orient a bar along `ground -> tip`.
7388
+
7034
7389
  ```ts
7035
- const mech = assembly("Arm")
7036
- .addPart("base", box(80, 80, 20).translate(0, 0, -10), {
7037
- metadata: { material: "PETG", process: "FDM", qty: 1 },
7390
+ const marker = box(8, 8, 4).withConnectors({
7391
+ center: connector({ origin: [0, 0, 0], axis: [0, 0, 1] }),
7392
+ });
7393
+
7394
+ const mech = assembly("Linkage")
7395
+ .link("ground", { at: [0, 0, 0], fixed: true })
7396
+ .link("worldX", { at: [10, 0, 0], fixed: true })
7397
+ .link("tip", { at: [40, 0, 0] })
7398
+ .edgeBetweenLinks("ground", "tip", { name: "bar" })
7399
+ .addAngleBetweenLinks("worldX", "ground", "tip", {
7400
+ name: "theta",
7401
+ control: { min: 0, max: 120, default: 30 },
7038
7402
  })
7039
- .addPart("link", box(140, 24, 24).translate(0, -12, -12))
7040
- .addRevolute("shoulder", "base", "link", {
7041
- axis: [0, 1, 0],
7042
- min: -30, max: 120, default: 25,
7043
- frame: Transform.identity().translate(0, 0, 20),
7044
- });
7403
+ .addPart("Tip marker", marker, { mate: { connector: "center", toLink: "tip" } });
7045
7404
 
7046
- return mech; // auto-solved at defaults, renders all parts
7405
+ return mech;
7047
7406
  ```
7048
7407
 
7049
7408
  ```ts
7050
7409
  assembly(name?: string): Assembly
7051
7410
  ```
7052
7411
 
7053
- #### `joint()` — Create a revolute joint that auto-generates a parameter slider and rotates the shape.
7412
+ ---
7413
+
7414
+ ## Classes
7415
+
7416
+ ### `Assembly`
7417
+
7418
+ Container for a kinematic mechanism made up of links, relationships, and parts.
7419
+
7420
+ Assembly has two related but different motion tools:
7421
+
7422
+ - **Link graph kinematics** (`link()`, `edgeBetweenLinks()`, `addAngleBetweenLinks()`) solve named point positions. A link is a point, not a rigid-body frame or bone. Connector-to-link mates then place geometry on the solved graph: one mate translates a connector origin onto a link, two mates orient a part so it spans between two solved links, and a third mate pins roll about that span.
7423
+ - **Connector-frame joints** (`connect()` / `match()`) align full connector frames and derive joint frame + axis from `origin`, `axis`, and `up`. Use this for serial articulated geometry such as hips, knees, hinges, drums, sliders, and wheels where the physical part orientation matters.
7424
+
7425
+ Use link graphs when the hard part is solving positions, especially closed loops. Use connector-frame joints when the hard part is a serial tree of explicit rigid-body joint frames such as hinges, sliders, drums, and wheels.
7426
+
7427
+ **Point-link quick start**
7054
7428
 
7055
- This is a convenience wrapper for single-shape, single-joint use cases. It calls `param()` to create a named angle slider, then applies `rotateAroundAxis()` to the shape. Use the full `Assembly` API for mechanisms with multiple parts and joints.
7429
+ This attaches a marker to a solved point. It is intentionally not a bone or oriented part example.
7056
7430
 
7057
7431
  ```ts
7058
- const arm = joint("Shoulder", armShape, [0, 0, 20], {
7059
- axis: [0, 1, 0],
7060
- min: -30, max: 120, default: 25,
7432
+ const marker = box(8, 8, 4).withConnectors({
7433
+ center: connector({ origin: [0, 0, 0], axis: [0, 0, 1] }),
7061
7434
  });
7062
- return arm;
7435
+
7436
+ const mech = assembly("Linkage")
7437
+ .link("ground", { at: [0, 0, 0], fixed: true })
7438
+ .link("worldX", { at: [10, 0, 0], fixed: true })
7439
+ .link("tip", { at: [40, 0, 0] })
7440
+ .edgeBetweenLinks("ground", "tip", { name: "bar" })
7441
+ .addAngleBetweenLinks("worldX", "ground", "tip", {
7442
+ name: "theta",
7443
+ control: { min: 0, max: 120, default: 30 },
7444
+ })
7445
+ .addPart("Tip marker", marker, {
7446
+ mate: { connector: "center", toLink: "tip" },
7447
+ });
7448
+
7449
+ return mech;
7063
7450
  ```
7064
7451
 
7452
+ Returning an unsolved `Assembly` keeps the graph available to the runtime. Return a `SolvedAssembly` directly for a specific control state:
7453
+
7065
7454
  ```ts
7066
- joint(name: string, shape: Shape, pivot: [ number, number, number ], opts?: RevoluteJointOpts): Shape
7455
+ return mech.solve({ theta: 60 });
7067
7456
  ```
7068
7457
 
7069
- `RevoluteJointOpts`: `{ axis?: [ number, number, number ], min?: number, max?: number, default?: number, unit?: string, reverse?: boolean }`
7458
+ **Frame-aware serial joint**
7070
7459
 
7071
- ---
7460
+ ```ts
7461
+ const hip = cylinder(12, 10).withConnectors({
7462
+ socket: connector("hinge", {
7463
+ origin: [0, 0, 6],
7464
+ axis: [0, 0, 1],
7465
+ up: [1, 0, 0],
7466
+ kind: "revolute",
7467
+ }),
7468
+ });
7469
+
7470
+ const upperLeg = box(60, 10, 8).translate(30, 0, -4).withConnectors({
7471
+ hip: connector("hinge", {
7472
+ origin: [0, 0, 0],
7473
+ axis: [0, 0, -1],
7474
+ up: [1, 0, 0],
7475
+ kind: "revolute",
7476
+ }),
7477
+ });
7478
+
7479
+ return assembly("Leg")
7480
+ .addPart("Hip", hip)
7481
+ .addPart("Upper Leg", upperLeg)
7482
+ .connect("Hip.socket", "Upper Leg.hip", { as: "hip", min: -35, max: 55 })
7483
+ .solve({ hip: 20 });
7484
+ ```
7485
+
7486
+ **Return types**
7487
+
7488
+ | Return value | Standalone | `require()` result type |
7489
+ |---|---|---|
7490
+ | `Assembly` (unsolved) | yes | `ImportedAssembly` |
7491
+ | `SolvedAssembly` | yes | `SolvedAssembly` |
7492
+
7493
+ **Properties:**
7494
+
7495
+ | Property | Type | Description |
7496
+ |----------|------|-------------|
7497
+ | `name` | `string` | — |
7498
+
7499
+ **Kinematics**
7500
+
7501
+ #### `link()` — Add a named kinematic link to the assembly graph.
7502
+
7503
+ Links are assembly-native solved points. They can exist before any geometry is attached, can be displayed by the viewport, and are solved by link/edge/angle constraints.
7504
+
7505
+ A link is not a rigid-body frame. It has a world position but no orientation basis. Use `connect()` when a physical part must inherit a connector frame and rotate about a real hinge/slider axis.
7506
+
7507
+ ```ts
7508
+ link(name: string, options?: AssemblyLinkOptions): Assembly
7509
+ ```
7510
+
7511
+ **`AssemblyLinkOptions`**
7512
+ - `at?: [ number, number, number ]` — Initial world-space position of this link before kinematic constraints solve it.
7513
+ - `fixed?: boolean` — Keep the link locked at its authored `at` position during solves.
7514
+ - `metadata?: Record<string, unknown>` — User metadata carried through the kinematic graph for inspection and tooling.
7515
+
7516
+ #### `edgeBetweenLinks()` — Add a relationship edge between two kinematic links.
7517
+
7518
+ By default the edge captures the authored distance between links as a structural length. Pass `{ length: 'free' }` or `{ visualOnly: true }` for a non-structural overlay edge.
7519
+
7520
+ ```ts
7521
+ edgeBetweenLinks(a: string, b: string, options?: AssemblyEdgeBetweenLinksOptions): Assembly
7522
+ ```
7523
+
7524
+ **`AssemblyEdgeBetweenLinksOptions`**: `name?: string`, `length?: number | "lockCurrent" | "free"`, `min?: number`, `max?: number`, `visualOnly?: boolean`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
7525
+
7526
+ `AssemblyKinematicControlOptions`: `{ min?: number, max?: number, default?: number, unit?: string }`
7527
+
7528
+ #### `addAngleBetweenLinks()` — Add an angle relationship among three kinematic links.
7529
+
7530
+ The middle link is the vertex. When `control` is set, `solve(state)` reads the control value from `state[name]` and solves dependent links from that driven angle.
7531
+
7532
+ ```ts
7533
+ addAngleBetweenLinks(a: string, b: string, c: string, options?: AssemblyAngleBetweenLinksOptions): Assembly
7534
+ ```
7072
7535
 
7073
- ## Classes
7536
+ **`AssemblyAngleBetweenLinksOptions`**: `name?: string`, `value?: number`, `min?: number`, `max?: number`, `control?: boolean | AssemblyKinematicControlOptions`, `limit?: AssemblyKinematicLimitOptions`, `metadata?: Record<string, unknown>`
7074
7537
 
7075
- ### `Assembly`
7538
+ `AssemblyKinematicLimitOptions`: `{ min?: number, max?: number }`
7076
7539
 
7077
- Container for a kinematic mechanism made up of named parts and joints.
7540
+ #### `describeKinematics()` Return the assembly-native kinematic graph definition.
7078
7541
 
7079
- An assembly is a directed graph where **parts** are nodes and **joints** are directed edges from parent to child. The graph must be a forest (one or more trees with no cycles). Root parts (no incoming joint) are fixed to world space.
7542
+ ```ts
7543
+ describeKinematics(): AssemblyKinematicGraphDef
7544
+ ```
7080
7545
 
7081
- Each joint carries a `frame` transform (from the parent part frame to the joint's zero-state frame) and a motion formula:
7546
+ **Structure**
7082
7547
 
7083
- ```
7084
- childWorld = parentWorld × frame × motion(value) × childBase
7085
- ```
7548
+ #### `addPart()` — Add a named part to the assembly.
7086
7549
 
7087
- Three joint types are supported:
7550
+ Connectors declared on the part (via `withConnectors()`) are captured automatically. Parts are positioned at world origin by default unless a `transform` is provided in `options`. For root parts (no incoming joint), `transform` is their final world position.
7088
7551
 
7089
- - **revolute** rotates the child around an axis by `value` degrees
7090
- - **prismatic** — translates the child along an axis by `value` mm
7091
- - **fixed** — no motion; rigidly attaches the child at `frame`
7552
+ `options.mate` is for point-link attachments. During `solve()`, ForgeCAD translates the part so the named connector origin lands on the solved link position. The part keeps its existing orientation; connector `axis` and `up` are not used for link mating. Use this for markers, sensors, labels, and other geometry that should ride on a solved point. Use `connect()` for oriented physical parts such as limbs, levers, hinges, and wheels.
7092
7553
 
7093
- **Quick start**
7554
+ When a part is a [`ShapeGroup`](/docs/core#shapegroup), name the group children explicitly to get readable viewport labels (e.g. `"Base Assembly.Body"` instead of `"Base Assembly.1"`):
7094
7555
 
7095
7556
  ```ts
7096
- const mech = assembly("Arm")
7097
- .addPart("base", box(80, 80, 20).translate(0, 0, -10))
7098
- .addPart("link", box(140, 24, 24).translate(0, -12, -12))
7099
- .addJoint("shoulder", "revolute", "base", "link", {
7100
- axis: [0, 1, 0],
7101
- min: -30, max: 120, default: 25,
7102
- frame: Transform.identity().translate(0, 0, 20),
7103
- });
7104
-
7105
- return mech; // auto-solved at defaults
7557
+ const housing = group(
7558
+ { name: "Body", shape: body },
7559
+ { name: "Lid", shape: lid },
7560
+ );
7561
+ assembly.addPart("Base Assembly", housing);
7106
7562
  ```
7107
7563
 
7108
- Returning an unsolved `Assembly` auto-solves at default joint values. Return a `SolvedAssembly` directly for a specific pose:
7109
-
7110
7564
  ```ts
7111
- return mech.solve({ shoulder: 60 });
7565
+ addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly
7112
7566
  ```
7113
7567
 
7114
- **Return types**
7568
+ `PartOptions`: `{ transform?: TransformInput, metadata?: PartMetadata, mate?: AssemblyPartMateInput | AssemblyPartMateInput[] }`
7115
7569
 
7116
- | Return value | Standalone | `require()` result type |
7117
- |---|---|---|
7118
- | `Assembly` (unsolved) | yes | `ImportedAssembly` |
7119
- | `SolvedAssembly` | yes | `SolvedAssembly` |
7570
+ **`AssemblyPartMateInput`**
7571
+ - `connector: string` — Name of a connector declared on the part (via `withConnectors()`).
7572
+ - `toLink: string` Name of the link this connector's origin is pinned to.
7573
+ - `aimLink?: string` Optional second link to orient toward. When set, the part is rotated so the connector's **axis** aims from `toLink` toward `aimLink`, posing an oriented bone instead of only translating it. For full pose without relying on a connector axis, declare a second mate (two connectors → two links).
7120
7574
 
7121
- **Properties:**
7575
+ #### `addFrame()` — Add a virtual reference frame (no geometry) to the assembly graph.
7122
7576
 
7123
- | Property | Type | Description |
7124
- |----------|------|-------------|
7125
- | `name` | `string` | — |
7577
+ Useful when you need a named pivot point or coordinate frame that has no visual geometry. Acts like a zero-volume part and can be connected to other parts via joints.
7578
+
7579
+ ```ts
7580
+ addFrame(name: string, options?: PartOptions): Assembly
7581
+ ```
7126
7582
 
7127
7583
  **Connectors**
7128
7584
 
@@ -7142,6 +7598,10 @@ Use the single-argument overload to attach assembly-level connectors — these a
7142
7598
  withConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly
7143
7599
  ```
7144
7600
 
7601
+ **`PortInput`**: `origin?: [ number, number, number ]`, `axis?: [ number, number, number ]`, `start?: [ number, number, number ]`, `end?: [ number, number, number ]`, `up?: [ number, number, number ]`, `kind?: JointType`, `min?: number`, `max?: number`
7602
+
7603
+ `ConnectorInput`: `{ connectorType?: string, gender?: ConnectorGender, measurements?: Record<string, number | string> }`
7604
+
7145
7605
  #### `getConnectors()` — Get connectors declared on a part in part-local space.
7146
7606
 
7147
7607
  ```ts
@@ -7158,23 +7618,35 @@ getConnector(ref: string): { partName: string; connectorName: string; connector:
7158
7618
 
7159
7619
  Connector references use `"PartName.connectorName"` format. The system aligns connector origins (child connector lands exactly on parent connector) and derives the joint frame and axis from the connector geometry — no manual `frame` or `axis` math needed.
7160
7620
 
7621
+ Use `connect()` for frame-aware articulated geometry. A connector's `origin` is the pivot/contact point, `axis` is the hinge line or slide direction, and `up` is the secondary orientation vector that locks the part's zero-angle twist. If `up` is omitted, ForgeCAD chooses a deterministic perpendicular vector, but authored mechanisms should provide `up` whenever rest orientation matters.
7622
+
7161
7623
  **Face-to-face convention:** Connectors always meet face-to-face, like a USB plug meeting a socket. Each connector's axis points "outward" from its part. When two connectors mate, the system brings them together so their axes oppose (anti-parallel). This is the same convention used by `matchTo()`.
7162
7624
 
7163
7625
  For a revolute joint (hinge), both connectors' axes should point outward from their respective parts along the hinge line. For a prismatic joint (slider), both axes should point along the slide direction from their part's perspective.
7164
7626
 
7627
+ **Mirrored revolute axes:** Revolute values follow the right-hand rule around the joint axis. If a bilateral mechanism mirrors a hinge axis, for example `[1, 0, 0]` on the right side and `[-1, 0, 0]` on the left side, the same physical `+theta` value rotates the two sides in opposite fore/aft senses. The mirrored pose uses the negated revolute value; physical limits mirror as `[min, max] -> [-max, -min]`. Prismatic joints do not have this handedness flip. For bilateral mechanisms, prefer side-neutral link controls or an explicit state mapping when you want equal semantic pose values on both sides.
7628
+
7165
7629
  The joint type is inferred from the connector's `kind` field if not specified in `options`.
7166
7630
 
7167
7631
  When connectors are defined with `start`/`end`, you can control which point on each connector meets via `align` / `parentAlign` / `childAlign` (`'start'`, `'middle'`, `'end'`).
7168
7632
 
7169
- Use `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore). For mechanisms where parts share an axis but are deliberately spaced apart, use `addRevolute()` with pre-positioned parts instead.
7633
+ Use `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore).
7170
7634
 
7171
7635
  ```ts
7172
7636
  // Hinge: both axes point outward along the hinge line
7173
7637
  const frame = box(100, 10, 80).withConnectors({
7174
- hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, 1] }),
7638
+ hinge: connector("hinge", {
7639
+ origin: [0, 0, 40],
7640
+ axis: [0, 0, 1],
7641
+ up: [1, 0, 0],
7642
+ }),
7175
7643
  });
7176
7644
  const door = box(60, 4, 80).withConnectors({
7177
- hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, -1] }),
7645
+ hinge: connector("hinge", {
7646
+ origin: [0, 0, 40],
7647
+ axis: [0, 0, -1],
7648
+ up: [1, 0, 0],
7649
+ }),
7178
7650
  });
7179
7651
  assembly("Door")
7180
7652
  .addPart("Frame", frame)
@@ -7186,6 +7658,16 @@ assembly("Door")
7186
7658
  connect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly
7187
7659
  ```
7188
7660
 
7661
+ **`ConnectOptions`**
7662
+
7663
+ | Option | Type | Description |
7664
+ |--------|------|-------------|
7665
+ | `flip?` | `boolean` | This parameter is ignored. If your connectors produce wrong orientation, fix the connector axis directions instead of using flip. |
7666
+ | `parentAlign?` | `PortAlign` | Which point on the parent connector to align: 'start', 'middle' (default), or 'end'. |
7667
+ | `childAlign?` | `PortAlign` | Which point on the child connector to align: 'start', 'middle' (default), or 'end'. |
7668
+ | `align?` | `PortAlign` | Shorthand: set both parentAlign and childAlign at once. |
7669
+ | `as?`, `type?`, `min?`, `max?`, `default?`, `unit?`, `effort?`, `velocity?`, `damping?`, `friction?` | | — |
7670
+
7189
7671
  #### `match()` — Auto-create a joint by matching typed connectors between two parts.
7190
7672
 
7191
7673
  Connectors can carry a `connectorType` string and a `gender` (`'male'`, `'female'`, or `'neutral'`). `match()` validates type and gender compatibility (use `{ force: true }` to skip validation) and creates the joint automatically from the connector's `kind` metadata.
@@ -7208,13 +7690,15 @@ const mech = assembly("Door")
7208
7690
  .addPart("Frame", frame)
7209
7691
  .addPart("Door", door)
7210
7692
  .match("Door", "Frame", { hinge_top: "hinge_top", hinge_bottom: "hinge_bottom" });
7211
- // Revolute connectors auto-creates revolute joint. No manual addRevolute needed.
7693
+ // Matching connectors computes the placement relationship automatically.
7212
7694
  ```
7213
7695
 
7214
7696
  ```ts
7215
7697
  match(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & { as?: string; }): Assembly
7216
7698
  ```
7217
7699
 
7700
+ `MatchToOptions`: `{ force?: boolean, angle?: number, distance?: number }`
7701
+
7218
7702
  **References**
7219
7703
 
7220
7704
  #### `withReferences()` — Attach named placement reference points to this assembly. These are surfaced automatically on the ImportedAssembly when this file is imported via require(), so consumers can use placeReference() without re-declaring them. Returns a new Assembly — does not mutate.
@@ -7223,22 +7707,28 @@ match(childPartName: string, parentPartName: string, pairs: Record<string, strin
7223
7707
  withReferences(refs: Pick<PlacementReferenceInput, "points">): Assembly
7224
7708
  ```
7225
7709
 
7226
- **Solving**
7710
+ **`PlacementReferenceInput`**: `points?: Record<string, [ number, number, number ]>`, `edges?: Record<string, PlacementEdgeRef>`, `surfaces?: Record<string, PlacementSurfaceRef>`, `objects?: Record<string, PlacementObjectInput>`
7227
7711
 
7228
- #### `solve()` Solve the assembly at the given joint state and return positioned parts.
7712
+ `PlacementEdgeRef`: `{ start: Vec3, end: Vec3 }`
7229
7713
 
7230
- Performs a depth-first traversal of the joint graph. Each joint's value is taken from `state`, falling back to `defaultValue`. Coupled joints compute their value from source joints. Values outside `[min, max]` are clamped (a warning is added to `SolvedAssembly.warnings()`).
7714
+ `PlacementSurfaceRef`: `{ center: Vec3, normal: Vec3 }`
7231
7715
 
7232
- If mate constraints were registered via `mate()`, the solver runs a pre-pass to derive base transforms, then the kinematic DFS applies joints on top of those positions.
7716
+ **Solving**
7717
+
7718
+ #### `solve()` — Solve the assembly at the given control state and return positioned parts.
7233
7719
 
7234
- **Pitfall [`jointsView`](/docs/viewport#jointsview) double-rotation:** When calling `toJointsView()`, always solve at the rest pose (all joint values = 0 or default). Solving at a non-zero angle and then animating will double-rotate parts. Use the `defaults` option on `toJointsView()` to set the initial display angle instead.
7720
+ Solves assembly-native kinematic links first. Controlled `addAngleBetweenLinks()` relationships read values from `state` by name, clamp to their declared limits, and expose the solved graph on `SolvedAssembly.kinematics`. Angles solve in the plane of their three authored link positions, so a limb that swings out of the `z = 0` plane poses correctly; structural edges hold their bone lengths so a fully angle-driven serial chain follows forward kinematics.
7235
7721
 
7236
- This pitfall only applies when `toJointsView()` is active. If you only want a static posed result, return the solved assembly directly and skip `toJointsView()`.
7722
+ Connector mates declared on `addPart(..., { mate })` attach geometry to solved links while preserving part and connector identity:
7237
7723
 
7238
- **Example static posed output (no `toJointsView()`)**
7724
+ - one mate **positions** the connector origin on its link;
7725
+ - a mate with `aimLink` (or a second mate to another link) also **orients** the part, rotating an oriented bone to span its links rather than only translating it;
7726
+ - a third mate **pins the roll** about the bone axis (full frame), e.g. a bore or clevis that must face a specific way.
7727
+
7728
+ Connector-frame joints created by `connect()` / `match()` are also evaluated; their values are read from `state` by joint name and clamped to joint limits.
7239
7729
 
7240
7730
  ```ts
7241
- return mech.solve({ shoulder: 45, elbow: -20 });
7731
+ return mech.solve({ theta: 45 });
7242
7732
  ```
7243
7733
 
7244
7734
  ```ts
@@ -7253,158 +7743,6 @@ solve(state?: JointState): SolvedAssembly
7253
7743
  mate(fn: (m: MateBuilder) => void): Assembly
7254
7744
  ```
7255
7745
 
7256
- #### `addFrame()` — Add a virtual reference frame (no geometry) to the assembly graph.
7257
-
7258
- Useful when you need a named pivot point or coordinate frame that has no visual geometry. Acts like a zero-volume part and can be connected to other parts via joints.
7259
-
7260
- ```ts
7261
- addFrame(name: string, options?: PartOptions): Assembly
7262
- ```
7263
-
7264
- #### `addPart()` — Add a named part to the assembly.
7265
-
7266
- Connectors declared on the part (via `withConnectors()`) are captured automatically. Parts are positioned at world origin by default unless a `transform` is provided in `options`. For root parts (no incoming joint), `transform` is their final world position.
7267
-
7268
- When a part is a [`ShapeGroup`](/docs/core#shapegroup), name the group children explicitly to get readable viewport labels (e.g. `"Base Assembly.Body"` instead of `"Base Assembly.1"`):
7269
-
7270
- ```ts
7271
- const housing = group(
7272
- { name: "Body", shape: body },
7273
- { name: "Lid", shape: lid },
7274
- );
7275
- assembly.addPart("Base Assembly", housing);
7276
- ```
7277
-
7278
- ```ts
7279
- addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly
7280
- ```
7281
-
7282
- #### `addJoint()` — Add a kinematic joint between a parent and child part.
7283
-
7284
- `frame` is a transform from the **parent part frame** to the **joint frame at zero state**. The child's world position is computed as:
7285
-
7286
- ```
7287
- childWorld = parentWorld × frame × motion(value) × childBase
7288
- ```
7289
-
7290
- For revolute joints `value` is in degrees; for prismatic joints `value` is in mm. Coupled joints (see `addJointCoupling`) ignore the `state` value passed to `solve()` and compute their value from source joints.
7291
-
7292
- ```ts
7293
- addJoint(name: string, type: JointType, parent: string, child: string, options?: JointOptions): Assembly
7294
- ```
7295
-
7296
- #### `addRevolute()` — Shorthand for `addJoint(name, 'revolute', parent, child, options)`.
7297
-
7298
- ```ts
7299
- addRevolute(name: string, parent: string, child: string, options?: JointOptions): Assembly
7300
- ```
7301
-
7302
- #### `addPrismatic()` — Shorthand for `addJoint(name, 'prismatic', parent, child, options)`.
7303
-
7304
- ```ts
7305
- addPrismatic(name: string, parent: string, child: string, options?: JointOptions): Assembly
7306
- ```
7307
-
7308
- #### `addFixed()` — Shorthand for `addJoint(name, 'fixed', parent, child, options)`.
7309
-
7310
- Fixed joints rigidly attach a child part to its parent at `frame` with no motion. Before calling `mergeInto()`, use `addFixed()` to collapse multiple root parts into a single root.
7311
-
7312
- ```ts
7313
- addFixed(name: string, parent: string, child: string, options?: JointOptions): Assembly
7314
- ```
7315
-
7316
- #### `addJointCoupling()` — Link a joint's value to a linear combination of other joint values.
7317
-
7318
- The driven joint's value is computed as:
7319
-
7320
- ```
7321
- driven = offset + Σ(ratio_i × source_i)
7322
- ```
7323
-
7324
- Coupled joints ignore any value passed in `solve(state)` — a warning is emitted if you try to override one. Coupling cycles are rejected. You cannot sweep a coupled joint directly; sweep one of its source joints instead.
7325
-
7326
- ```ts
7327
- assembly
7328
- .addRevolute("Steering", "Base", "Turret", { axis: [0, 0, 1] })
7329
- .addRevolute("WheelDrive", "Turret", "Wheel", { axis: [1, 0, 0] })
7330
- .addRevolute("TopGear", "Base", "TopInput", { axis: [0, 0, 1] })
7331
- .addJointCoupling("TopGear", {
7332
- terms: [
7333
- { joint: "Steering", ratio: 1 },
7334
- { joint: "WheelDrive", ratio: 20 / 14 },
7335
- ],
7336
- });
7337
- ```
7338
-
7339
- ```ts
7340
- addJointCoupling(jointName: string, options: JointCouplingOptions): Assembly
7341
- ```
7342
-
7343
- #### `addGearCoupling()` — Link two revolute joints via a gear ratio.
7344
-
7345
- Choose exactly one ratio source:
7346
-
7347
- - `ratio` — explicit numeric ratio (driven/driver, negative for external mesh)
7348
- - `pair` — a `GearRatioLike` from `lib.gearPair`, `lib.bevelGearPair`, etc. (uses `pair.jointRatio`)
7349
- - `driverTeeth` + `drivenTeeth` — auto-computes ratio; use `mesh` to control sign (`'external'` = negative/opposite rotation, `'internal'` = positive, `'bevel'`/`'face'` = negative)
7350
-
7351
- When `pair` carries a `phaseDeg`, it is auto-applied as the coupling `offset` to align teeth correctly. Override with `offset: 0` if gear shapes already have the phase baked in.
7352
-
7353
- ```ts
7354
- const pair = lib.gearPair({ pinion: { module: 1.25, teeth: 14 }, gear: { module: 1.25, teeth: 42 } });
7355
- assembly
7356
- .addRevolute("Pinion", "Base", "PinionPart", { axis: [0, 0, 1] })
7357
- .addRevolute("Driven", "Base", "GearPart", { axis: [0, 0, 1] })
7358
- .addGearCoupling("Driven", "Pinion", { pair });
7359
- ```
7360
-
7361
- ```ts
7362
- addGearCoupling(drivenJointName: string, driverJointName: string, options?: GearCouplingOptions): Assembly
7363
- ```
7364
-
7365
- #### `sweepJoint()` — Sample a joint through its motion range, collecting collision data at each step.
7366
-
7367
- Divides `[from, to]` into `steps` intervals (producing `steps + 1` frames). At each sample, the assembly is solved with the sweeping joint at that value and `baseState` for all others. Returns one `JointSweepFrame` per sample with the joint value, collision findings, and any solve warnings.
7368
-
7369
- You cannot sweep a coupled joint — sweep one of its source joints instead.
7370
-
7371
- ```ts
7372
- const sweep = mech.sweepJoint("elbow", -10, 135, 12, { shoulder: 35 });
7373
- const hits = sweep.filter(frame => frame.collisions.length > 0);
7374
- console.log(`Collisions at ${hits.length} of ${sweep.length} poses`);
7375
- ```
7376
-
7377
- ```ts
7378
- sweepJoint(jointName: string, from: number, to: number, steps: number, baseState?: JointState, collisionOptions?: CollisionOptions): JointSweepFrame[]
7379
- ```
7380
-
7381
- #### `toJointsView()` — Derive viewport joint controls from the assembly graph and register them.
7382
-
7383
- Solves the assembly at rest (all joints = default), then converts each joint into a `JointViewInput` with world-space pivot and axis. Fixed joints become hidden zero-range revolute entries so attached parts follow their parent during animation. Joint couplings are forwarded to the viewport automatically.
7384
-
7385
- This method is optional. Call it only when you want viewport joint sliders, coupled controls, or playback animations. If you only want geometry, return the `Assembly` or `SolvedAssembly` directly and skip `toJointsView()`.
7386
-
7387
- **Critical pitfall:** Always call `toJointsView()` before solving for display. Then solve at the **rest pose** (no state overrides) and return that solved assembly result directly. Do not flatten it with `.toGroup()` if you want the viewport joint animation to keep working.
7388
-
7389
- Do not solve at a non-zero angle when using `toJointsView()` — the viewport will apply the same rotation again, double-rotating the part.
7390
-
7391
- ```ts
7392
- mech.toJointsView({
7393
- defaults: { J1: 30 },
7394
- animations: [{
7395
- name: "Swing", duration: 2, loop: true,
7396
- keyframes: [{ values: { J1: -45 } }, { values: { J1: 45 } }, { values: { J1: -45 } }],
7397
- }],
7398
- });
7399
-
7400
- // Solve at REST — viewport handles posing
7401
- return mech.solve();
7402
- ```
7403
-
7404
- ```ts
7405
- toJointsView(options?: ToJointsViewOptions): void
7406
- ```
7407
-
7408
7746
  #### `describe()` — Return the serializable assembly definition used by solve/inspect pipelines.
7409
7747
 
7410
7748
  ```ts
@@ -7422,7 +7760,7 @@ describe(): AssemblyDefinition
7422
7760
 
7423
7761
  A wrapper around an imported `Assembly` that provides kinematic access and convenient transform helpers.
7424
7762
 
7425
- When a `.forge.js` file returns an unsolved `Assembly`, [`require()`](/docs/core#require) wraps it in an `ImportedAssembly`. This preserves the kinematic structure — you can call `solve()`, `sweepJoint()`, and `mergeInto()` — while also allowing convenience transforms that auto-solve at default values.
7763
+ When a `.forge.js` file returns an unsolved `Assembly`, [`require()`](/docs/core#require) wraps it in an `ImportedAssembly`. This preserves the kinematic structure — you can call `solve()` and `mergeInto()` — while also allowing convenience transforms that auto-solve at default values.
7426
7764
 
7427
7765
  **Kinematic access**
7428
7766
 
@@ -7451,7 +7789,7 @@ require("./arm.forge.js").mergeInto(robot, {
7451
7789
  });
7452
7790
  ```
7453
7791
 
7454
- #### `assembly()` — The underlying Assembly — use for sweepJoint, addPart into parent, etc.
7792
+ #### `assembly()` — The underlying Assembly, for advanced composition and inspection.
7455
7793
 
7456
7794
  ```ts
7457
7795
  get assembly(): Assembly
@@ -7469,6 +7807,14 @@ solve(state?: JointState): SolvedAssembly
7469
7807
  part(name: string, state?: JointState): AssemblyPart
7470
7808
  ```
7471
7809
 
7810
+ #### `getPart()` — Return a specific named part positioned at the default solved pose.
7811
+
7812
+ This mirrors `SolvedAssembly.getPart()` for imported assemblies. Use `solve(state).getPart(name)` when inspecting a non-default joint state.
7813
+
7814
+ ```ts
7815
+ getPart(partName: string): AssemblyPart
7816
+ ```
7817
+
7472
7818
  #### `toGroup()` — Convert all assembly parts to a ShapeGroup with named children. Use this for composition, transforms, or child lookup — not as a required render step for assemblies. Child names match the part names used in the assembly. Any stored placement offset and placement references are forwarded to the group.
7473
7819
 
7474
7820
  ```ts
@@ -7547,17 +7893,35 @@ color(hex: string): ShapeGroup
7547
7893
  child(name: string): Shape | Sketch | ShapeGroup
7548
7894
  ```
7549
7895
 
7550
- #### `mergeInto()` — Flatten this sub-assembly's parts and joints into `parent` and wire a mount joint.
7896
+ #### `collisionReport()` — Detect overlapping part pairs at the default solved pose.
7897
+
7898
+ This mirrors `SolvedAssembly.collisionReport()` for imported assemblies. Use `solve(state).collisionReport(options)` when inspecting a non-default joint state.
7899
+
7900
+ ```ts
7901
+ collisionReport(options?: CollisionOptions): CollisionFinding[]
7902
+ ```
7903
+
7904
+ `CollisionOptions`: `{ parts?: string[], ignorePairs?: Array<[ string, string ]>, minOverlapVolume?: number }`
7551
7905
 
7552
- All part and joint names from the sub-assembly are prefixed with `"${options.prefix}."` to avoid collisions. After the merge, sub-assembly joints are driven from the parent using the prefixed names:
7906
+ #### `minClearance()` Compute the minimum gap between two parts at the default solved pose.
7907
+
7908
+ This mirrors `SolvedAssembly.minClearance()` for imported assemblies. Use `solve(state).minClearance(partA, partB, searchLength)` when inspecting a non-default joint state.
7909
+
7910
+ ```ts
7911
+ minClearance(partA: string, partB: string, searchLength?: number): number
7912
+ ```
7913
+
7914
+ #### `mergeInto()` — Flatten this sub-assembly's parts and relationships into `parent` and wire a mount relationship.
7915
+
7916
+ All part, link, and legacy joint names from the sub-assembly are prefixed with `"${options.prefix}."` to avoid collisions. After the merge, controls are driven from the parent using the prefixed names:
7553
7917
 
7554
7918
  ```ts
7555
- parent.solve({ "Left Arm.shoulder": 45, "Right Arm.shoulder": -20 })
7919
+ parent.solve({ "Left Arm.theta": 45, "Right Arm.theta": -20 })
7556
7920
  ```
7557
7921
 
7558
- Joint couplings inside the sub-assembly are preserved and rewritten with the prefix. Ports from sub-assembly parts are forwarded with the prefix.
7922
+ Connectors from sub-assembly parts are forwarded with the prefix.
7559
7923
 
7560
- The sub-assembly must have exactly one root part. If it has multiple roots, use `addFixed()` first to consolidate them before merging.
7924
+ The sub-assembly must have exactly one root part before it can be merged.
7561
7925
 
7562
7926
  ```ts
7563
7927
  const robot = assembly("Robot").addPart("Chassis", chassis);
@@ -7574,6 +7938,25 @@ require("./arm.forge.js").mergeInto(robot, {
7574
7938
  mergeInto(parent: Assembly, options: MergeIntoOptions): Assembly
7575
7939
  ```
7576
7940
 
7941
+ **`MergeIntoOptions`**
7942
+
7943
+ | Option | Type | Description |
7944
+ |--------|------|-------------|
7945
+ | `prefix?` | `string` | Prefix applied to every part name and joint name from the sub-assembly. E.g. prefix "Left Arm" turns part "Base" into "Left Arm.Base". Strongly recommended to avoid name collisions when merging multiple instances. |
7946
+ | `mountParent` | `string` | Part name in the parent assembly to attach the sub-assembly root to. |
7947
+ | `mountJoint` | `string` | Name for the new mount joint in the parent graph. |
7948
+ | `mountType?` | `JointType` | Joint type for the mount connection (default: 'fixed'). |
7949
+ | `mountOptions?` | `JointOptions` | Frame, axis, limits, and other options for the mount joint. |
7950
+
7951
+ **`JointOptions`**
7952
+
7953
+ | Option | Type | Description |
7954
+ |--------|------|-------------|
7955
+ | `connectorRefs?` | `JointConnectorRefs` | Connector refs that define this joint contract. Usually set by `connect()` / `match()`. |
7956
+ | `frame?`, `origin?`, `axis?`, `min?`, `max?`, `default?`, `unit?`, `effort?`, `velocity?`, `damping?`, `friction?` | | — |
7957
+
7958
+ `JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`
7959
+
7577
7960
  ### `SolvedAssembly`
7578
7961
 
7579
7962
  The result of solving an assembly at a specific joint state.
@@ -7582,7 +7965,7 @@ The result of solving an assembly at a specific joint state.
7582
7965
 
7583
7966
  **Validation**
7584
7967
 
7585
- Call `collisionReport()` to detect overlapping parts, or `sweepJoint()` on the parent `Assembly` to check for interference across the joint's motion range.
7968
+ Call `collisionReport()` to detect overlapping parts at this solved pose.
7586
7969
 
7587
7970
  ```ts
7588
7971
  const solved = mech.solve({ shoulder: 45, elbow: -20 });
@@ -7628,6 +8011,18 @@ get mateDof(): number | null
7628
8011
  get mateConverged(): boolean | null
7629
8012
  ```
7630
8013
 
8014
+ #### `kinematics()` — Solved assembly-native kinematic graph, or null when no links were declared.
8015
+
8016
+ ```ts
8017
+ get kinematics(): SolvedAssemblyKinematics | null
8018
+ ```
8019
+
8020
+ #### `getLinkPosition()` — Return the solved world position of a kinematic link.
8021
+
8022
+ ```ts
8023
+ getLinkPosition(linkName: string): Vec3
8024
+ ```
8025
+
7631
8026
  #### `getTransform()` — Return the world-space [`Transform`](/docs/core#transform) for the named part at the solved pose.
7632
8027
 
7633
8028
  ```ts
@@ -8061,6 +8456,10 @@ const part = sheetMetal({ panel: { width: 100, height: 60 }, thickness: 1.5, ben
8061
8456
  flange(edge: SheetMetalEdge, options: SheetMetalFlangeOptions): SheetMetalPart
8062
8457
  ```
8063
8458
 
8459
+ **`SheetMetalFlangeOptions`**
8460
+ - `length: number` — Flange leg length in mm, measured from the outside of the bend to the tip.
8461
+ - `angleDeg?: number` — Bend angle in degrees (default: `90`). Only `90°` is supported in v1. Values other than 90 will be rejected at build time.
8462
+
8064
8463
  #### `cutout()` — Subtract a 2D sketch cutout from a planar region of the sheet metal part.
8065
8464
 
8066
8465
  `region` must be `'panel'` or one of `'flange-top'`, `'flange-right'`, `'flange-bottom'`, `'flange-left'` (only available once the corresponding flange has been added). Cutouts inside bend regions are **not** supported in v1.
@@ -8080,6 +8479,11 @@ const part = sheetMetal({ panel: { width: 180, height: 110 }, thickness: 1.5, be
8080
8479
  cutout(region: SheetMetalPlanarRegionName, sketch: Sketch, options?: SheetMetalCutoutOptions): SheetMetalPart
8081
8480
  ```
8082
8481
 
8482
+ **`SheetMetalCutoutOptions`**
8483
+ - `u?: number` — Horizontal offset within the region, measured from the region centre (mm). Default: `0`.
8484
+ - `v?: number` — Vertical offset within the region, measured from the region centre (mm). Default: `0`.
8485
+ - `selfAnchor?: Anchor` — Anchor point on the sketch that aligns to `(u, v)`. Use `'center'` for most cases. For asymmetric profiles, verify orientation by placing one test cutout before committing to the final position. Default: `'center'`.
8486
+
8083
8487
  #### `regionNames()` — Return all semantic region names currently available on this part.
8084
8488
 
8085
8489
  The returned list always includes `'panel'`. For every flange that has been added, the list also includes the corresponding `'flange-<edge>'` and `'bend-<edge>'` entries.
@@ -8349,7 +8753,7 @@ Call `robotExport()` alongside your assembly definition. The CLI commands `forge
8349
8753
 
8350
8754
  - Mesh-based inertia tensors (full 6-component, not bounding-box approximations)
8351
8755
  - Separate collision meshes (convex hull by default — ~50–80% smaller)
8352
- - Joint mimic elements derived from `addJointCoupling` / `addGearCoupling`
8756
+ - Joint limits, effort/velocity/damping/friction metadata from assembly joints
8353
8757
 
8354
8758
  **Collision mesh modes** (set per-link via `links["PartName"].collision`):
8355
8759
 
@@ -8365,7 +8769,7 @@ Call `robotExport()` alongside your assembly definition. The CLI commands `forge
8365
8769
  - Revolute `velocity` is in degrees/second in Forge; exporters convert to rad/s.
8366
8770
  - Prismatic distances are in mm in Forge; exported in meters.
8367
8771
  - `massKg` is preferred; `densityKgM3` is used when mass is unknown.
8368
- - Couplings with multiple terms: only the primary term (largest ratio) maps to `<mimic>` — SDF/URDF support single-leader mimic only. Dropped terms emit a warning.
8772
+ - Compatibility coupling metadata, when present, maps only the primary term (largest ratio) to `<mimic>` — SDF/URDF support single-leader mimic only. Dropped terms emit a warning.
8369
8773
 
8370
8774
  ```ts
8371
8775
  const rover = assembly("Scout")
@@ -8421,9 +8825,9 @@ robotExport(options: RobotExportOptions): CollectedRobotExport
8421
8825
 
8422
8826
  **`CollectedRobotExport`**: `modelName: string`, `assembly: AssemblyDefinition`, `state: JointState`, `static: boolean`, `selfCollide: boolean`, `allowAutoDisable: boolean`, `links: Record<string, RobotLinkExportOptions>`, `joints: Record<string, RobotJointExportOptions>`, `plugins: { diffDrive?: RobotDiffDrivePluginOptions; jointStatePublisher?: RobotJointStatePublisherOptions; }`, `world: RobotWorldOptions | null`
8423
8827
 
8424
- `AssemblyDefinition`: `{ name: string, parts: AssemblyPartDef[], joints: AssemblyJointDef[], jointCouplings: AssemblyJointCouplingDef[] }`
8828
+ **`AssemblyDefinition`**: `name: string`, `parts: AssemblyPartDef[]`, `joints: AssemblyJointDef[]`, `jointCouplings: AssemblyJointCouplingDef[]`, `kinematics: AssemblyKinematicGraphDef`
8425
8829
 
8426
- `AssemblyPartDef`: `{ name: string, part: AssemblyPart, base: Transform, metadata?: PartMetadata }`
8830
+ `AssemblyPartDef`: `{ name: string, part: AssemblyPart, base: Transform, metadata?: PartMetadata, mates: AssemblyPartMateInput[] }`
8427
8831
 
8428
8832
  **`PartMetadata`**
8429
8833
 
@@ -8432,6 +8836,11 @@ robotExport(options: RobotExportOptions): CollectedRobotExport
8432
8836
  | `tags?` | `string \| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |
8433
8837
  | `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |
8434
8838
 
8839
+ **`AssemblyPartMateInput`**
8840
+ - `connector: string` — Name of a connector declared on the part (via `withConnectors()`).
8841
+ - `toLink: string` — Name of the link this connector's origin is pinned to.
8842
+ - `aimLink?: string` — Optional second link to orient toward. When set, the part is rotated so the connector's **axis** aims from `toLink` toward `aimLink`, posing an oriented bone instead of only translating it. For full pose without relying on a connector axis, declare a second mate (two connectors → two links).
8843
+
8435
8844
  **`AssemblyJointDef`**: `name: string`, `type: JointType`, `parent: string`, `child: string`, `frame: Transform`, `axis: Vec3`, `min?: number`, `max?: number`, `defaultValue: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`, `connectorRefs?: JointConnectorRefs`
8436
8845
 
8437
8846
  `JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`
@@ -8440,6 +8849,16 @@ robotExport(options: RobotExportOptions): CollectedRobotExport
8440
8849
 
8441
8850
  `JointCouplingTermRecord`: `{ joint: string, ratio: number }`
8442
8851
 
8852
+ `AssemblyKinematicGraphDef`: `{ links: AssemblyLinkDef[], edges: AssemblyEdgeBetweenLinksDef[], angles: AssemblyAngleBetweenLinksDef[] }`
8853
+
8854
+ `AssemblyLinkDef`: `{ name: string, at: Vec3, fixed: boolean, metadata?: Record<string, unknown> }`
8855
+
8856
+ **`AssemblyEdgeBetweenLinksDef`**: `name: string`, `a: string`, `b: string`, `length: number | null`, `min?: number`, `max?: number`, `visualOnly: boolean`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
8857
+
8858
+ `AssemblyKinematicControlOptions`: `{ min?: number, max?: number, default?: number, unit?: string }`
8859
+
8860
+ **`AssemblyAngleBetweenLinksDef`**: `name: string`, `a: string`, `b: string`, `c: string`, `target?: number`, `min?: number`, `max?: number`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
8861
+
8443
8862
  #### `dim()` — Add a dimension annotation between two points.
8444
8863
 
8445
8864
  Dimension annotations are purely visual callouts rendered in the viewport and report export. They do not affect geometry or constrain the model.
@@ -8587,18 +9006,24 @@ offsetBand(thickness: number): Sketch
8587
9006
  addSpurTeethBetween(options: DriveWheelSpurTeethRegionOptions): this
8588
9007
  ```
8589
9008
 
9009
+ `DriveWheelSpurTeethRegionOptions`: `{ name?: string, teethOnFullCircle: number, toothCount: number, firstTooth?: number, faceWidth?: number }`
9010
+
8590
9011
  #### `addSolidArcBetween()` — Add a constant-radius solid arc region such as a dwell, stop, or pusher.
8591
9012
 
8592
9013
  ```ts
8593
9014
  addSolidArcBetween(options: DriveWheelSolidArcRegionOptions): this
8594
9015
  ```
8595
9016
 
9017
+ **`DriveWheelSolidArcRegionOptions`**: `name?: string`, `fromAngleDeg: number`, `toAngleDeg: number`, `innerRadius?: number`, `outerRadius: number`, `faceWidth?: number`, `segments?: number`
9018
+
8596
9019
  #### `addShapeRegion()` — Add a fully custom region shape while preserving region metadata.
8597
9020
 
8598
9021
  ```ts
8599
9022
  addShapeRegion(name: string, shape: Shape, options?: DriveWheelShapeRegionOptions): this
8600
9023
  ```
8601
9024
 
9025
+ `DriveWheelShapeRegionOptions`: `{ fromAngleDeg?: number, toAngleDeg?: number, innerRadius?: number, outerRadius?: number }`
9026
+
8602
9027
  #### `build()` — Build the final wheel shape with a bore connector and region metadata.
8603
9028
 
8604
9029
  ```ts
@@ -8665,7 +9090,7 @@ Sizes outside the supported ranges will throw at runtime with a descriptive erro
8665
9090
  - `routedTubeClipAssembly(options: RoutedTubeClipAssemblyOptions): RoutedTubeClipAssemblyResult` — Routed tube or cable retained by saddle clips with real bores, screw holes, and installed screws. **Details** This pattern is for hoses, wires, pump tubes, sensor leads, and appliance cable runs where generated models often draw a cylinder near a wall without clips or strain relief. The base panel has receiving screw envelopes, each saddle clip has a real through-bore around the tube and vertical screw clearances, and the installed screws share those positions. Coordinate convention: the routed tube runs along +X through the world origin. The base panel starts at `z=0`; clips sit on top of the panel, and the tube passes through their bores. **Example** ```ts const route = lib.routedTubeClipAssembly({ tubeDiameter: 6, clipCount: 3, }); verify.notColliding('tube clears clip bores', route.tube, union(...route.clips)); verify.notColliding('clip screws clear retained stack', union(...route.screws), union(route.panel, ...route.clips)); return route.parts; ```
8666
9091
  - `pcbTerminalBlockAssembly(options?: PcbTerminalBlockAssemblyOptions): PcbTerminalBlockAssemblyResult` — PCB terminal-block stack with a backplate, standoffs, mounting screws, pin holes, and a seated terminal block. **Details** This pattern is for thermostat backplates, appliance control panels, sensor boards, and small electronics where generated models often place a terminal block, screw heads, and holes as independent decorations. The PCB mounting holes, fused standoffs, installed screws, terminal pins, and PCB pin clearances all come from one shared datum system so the purchased block is mechanically seated and the board is actually mounted. Coordinate convention: X/Y are the board footprint, Z is up. The backplate starts at `z=0`, standoffs rise from the plate, the PCB rests on the standoffs, and the terminal block sits on top of the PCB near the front edge. **Example** ```ts const terminalStack = lib.pcbTerminalBlockAssembly({ terminalCount: 5, screwSize: 'M3', }); verify.notColliding('terminal pins clear PCB holes', terminalStack.terminalBlock, terminalStack.pcb); verify.notColliding('mounting screws clear PCB and standoff holes', union(...terminalStack.screws), union(terminalStack.pcb, terminalStack.backplate)); return terminalStack.parts; ```
8667
9092
  - `thumbScrewClampAssembly(options?: ThumbScrewClampAssemblyOptions): ThumbScrewClampAssemblyResult` — Thumb-screw clamp with a C-frame, threaded boss, captive pressure pad, knob, and clamped workpiece. **Details** This pattern is for bench clamps, monitor-arm desk clamps, small vise screws, capo pressure screws, fixture hold-downs, and service brackets where generated models often place a loose screw, knob, or pressure pad near a bracket. The helper creates a one-piece clamp frame with a fixed anvil pad, a bored threaded support and boss, an installed screw with a captive pressure pad and hand knob, and a representative clamped workpiece seated between the pads. Coordinate convention: the clamp screw runs along +X. The fixed anvil is on the -X side, the threaded support and knob are on the +X side, and Z is up from the base bridge. **Example** ```ts const clamp = lib.thumbScrewClampAssembly({ screwSize: 'M6', workpieceThickness: 20, }); verify.notColliding('thumb screw clears threaded boss', clamp.clampScrew, clamp.frame); verify.clearanceBetween('pressure pad is seated on workpiece', clamp.clampScrew, clamp.workpiece, -0.01, 0.05); return clamp.parts; ```
8668
- - `pipeRoute(points: [ number, number, number ][], radius: number, options?: { bendRadius?: number; wall?: number; segments?: number; }): Shape` — Route a pipe (solid or hollow) through 3D waypoints with smooth bends. Each interior waypoint gets a torus-section bend. Straight segments connect them. Returns a single unioned Shape.
9093
+ - `pipeRoute(points: [ number, number, number ][], radius: number, options?: { bendRadius?: number; wall?: number; segments?: number; }): Shape` — Route a pipe (solid or hollow) through 3D waypoints with smooth bends. This is a convenience recipe over `Curve.Route.fromPolyline()` and [`sweep()`](/docs/curves#sweep). Use `Curve.Route` directly when you need route metadata, named ports, or custom swept profiles.
8669
9094
  - `elbow(pipeRadius: number, bendRadius: number, angle?: number | { ... }, options?: { ... }): Shape` — Pipe elbow — a curved pipe section (torus arc) for connecting two pipe directions. By default creates a bend in the XZ plane: incoming along +Z, outgoing rotated by `angle`. The bend starts at the origin, curving away from it.
8670
9095
  - `beltDrive(options: BeltDriveOptions): BeltDriveResult` — Create a flat open-belt body around two pulley pitch circles. The belt is generated as a tangent loop in the XY plane and extruded along +Z by `beltWidth`. The result includes the solid belt, the 2D belt profile, a thin pitch-path sketch for visualization, total belt length, tangent spans, and wrap metadata for each pulley. For more than two pulleys, the API intentionally asks for route intent before geometry is created. Use `route: "outer"` for the future outside-envelope mode, or an ordered route for future serpentine/idler layouts. ```ts const drive = lib.beltDrive({ pulleys: [ { name: "motor", center: [0, 0], pitchRadius: 12 }, { name: "output", center: [80, 0], pitchRadius: 28 }, ], beltWidth: 8, beltThickness: 2, }); return drive.belt; ```
8671
9096
  - `tangentLoop2d(circles: TangentCircle2D[], options?: TangentLoop2DOptions): TangentLoop2D` — Build a closed 2D route made from common tangent spans and pulley wrap arcs. Use this when you need reusable belt/chain route geometry before creating a solid body. The first implementation supports two circles. `mode: "open"` uses external tangents; `mode: "crossed"` uses internal tangents. ```ts const route = lib.tangentLoop2d([ { center: [0, 0], radius: 12 }, { center: [80, 0], radius: 28 }, ]); const belt = route.offsetBand(2).extrude(8); ```
@@ -8814,7 +9239,7 @@ Cut planes, exploded views, joint animations, and scene configuration.
8814
9239
 
8815
9240
  ## Contents
8816
9241
 
8817
- - [Viewport & Runtime](#viewport-runtime) — `Viewport.label`, `scene`, `viewConfig`, `explodeView`, `jointsView`, `compareWith`, `cutPlane`, `mock`, `showLabels`, `highlight`
9242
+ - [Viewport & Runtime](#viewport-runtime) — `Viewport.label`, `scene`, `viewConfig`, `explodeView`, `compareWith`, `cutPlane`, `mock`, `showLabels`, `highlight`
8818
9243
  - [RouteBuilder](#routebuilder)
8819
9244
  - [route](#route)
8820
9245
 
@@ -9001,7 +9426,7 @@ scene(options: SceneOptions): void
9001
9426
 
9002
9427
  #### `viewConfig()` — Configure viewport helper visuals for the current script execution.
9003
9428
 
9004
- Controls renderer-only overlays that appear in the viewport but are not part of the geometry. Currently supports the joint overlay that renders axis arrows and arc indicators when `jointsView` is active. Multiple calls merge — later values override earlier ones per key.
9429
+ Controls renderer-only overlays that appear in the viewport but are not part of the geometry. Currently supports the joint overlay that renders axis arrows and arc indicators when joint controls are active. Multiple calls merge — later values override earlier ones per key.
9005
9430
 
9006
9431
  This does **not** trigger a geometry recompute; it only affects the visual helpers drawn on top of the 3D scene.
9007
9432
 
@@ -9058,91 +9483,6 @@ explodeView(options?: ExplodeViewOptions): void
9058
9483
  - `direction?: ExplodeDirection` — Direction mode for this node
9059
9484
  - `axisLock?: ExplodeAxis` — Optional axis lock after direction is resolved
9060
9485
 
9061
- #### `jointsView()` — Register viewport-only mechanism controls that animate returned objects without re-running the script.
9062
-
9063
- Defines joints (revolute or prismatic), optional gear/rack couplings, and named animations. The viewport resolves transforms through the joint chain at display time — the script geometry is computed only once at rest pose.
9064
-
9065
- **Critical:** Solve the assembly at **rest pose** (all animated joints = 0). The viewport applies `jointsView` transforms on top of the returned scene. If geometry is already solved at non-zero angles, animation will double-rotate everything.
9066
-
9067
- ```js
9068
- // BAD — double rotation
9069
- const solved = mech.solve({ shoulder: 45, elbow: 30 });
9070
- jointsView({ joints: [{ name: 'shoulder', ... }] });
9071
- return solved;
9072
-
9073
- // GOOD — rest pose, jointsView controls all posing
9074
- const solved = mech.solve({ shoulder: 0, elbow: 0 });
9075
- jointsView({
9076
- joints: [
9077
- { name: 'shoulder', child: 'Upper Arm', default: 45, ... },
9078
- { name: 'elbow', child: 'Forearm', parent: 'Upper Arm', default: 30, ... },
9079
- ],
9080
- });
9081
- return solved;
9082
- ```
9083
-
9084
- **Pivot coordinates** are world-space positions of each joint origin at rest pose. For `addRevolute('shoulder', 'Base', 'Link', { frame: Transform.identity().translate(0, 0, 20) })` where "Base" is at world origin, the pivot is `[0, 0, 20]`.
9085
-
9086
- **Fixed attachments** that must follow a parent during animation need a zero-angle revolute joint in the chain:
9087
-
9088
- ```js
9089
- { name: 'EE_Follow', child: 'End Effector', parent: 'Last Link',
9090
- type: 'revolute', axis: [0, 0, 1], pivot: [linkLength, 0, 0],
9091
- min: 0, max: 0, default: 0 }
9092
- ```
9093
-
9094
- Animation values are interpolated linearly between keyframes. ForgeCAD does **not** auto-wrap revolute values across `-180/180`. Keep keyframe values continuous — a `-180 -> 171` jump spins the part the long way around. Use `-180 -> -189` instead. Author high-speed multi-turn joints as accumulating angles (`0, 360, 720, ...`) with `continuous: true`.
9095
-
9096
- **Tick-based keyframes:** Omit `at` from all keyframes to auto-distribute by tick weight:
9097
-
9098
- ```js
9099
- keyframes: [
9100
- { ticks: 3, values: { Shoulder: 20 } }, // slow segment (3x weight)
9101
- { ticks: 1, values: { Shoulder: -10 } }, // fast segment (1x weight)
9102
- { values: { Shoulder: 20 } }, // last keyframe; ticks ignored
9103
- ]
9104
- // positions: 0, 0.75, 1.0
9105
- ```
9106
-
9107
- Mixing explicit `at` and omitted `at` in the same animation is not allowed.
9108
-
9109
- ```js
9110
- jointsView({
9111
- joints: [{
9112
- name: 'Shoulder', child: 'Upper Arm', parent: 'Base',
9113
- type: 'revolute', axis: [0, -1, 0], pivot: [0, 0, 46],
9114
- min: -30, max: 110, default: 15,
9115
- }],
9116
- animations: [{
9117
- name: 'Walk Cycle', duration: 1.6, loop: true,
9118
- keyframes: [
9119
- { values: { Shoulder: 20 } },
9120
- { values: { Shoulder: -10 } },
9121
- { values: { Shoulder: 20 } },
9122
- ],
9123
- }],
9124
- });
9125
- ```
9126
-
9127
- ```ts
9128
- jointsView(options?: JointsViewOptions): void
9129
- ```
9130
-
9131
- **`JointsViewOptions`**: `enabled?: boolean`, `joints?: JointViewInput[]`, `couplings?: JointViewCouplingInput[]`, `animations?: JointViewAnimationInput[]`, `defaultAnimation?: string`
9132
-
9133
- **`JointViewInput`**: `name: string`, `child: string`, `parent?: string`, `type?: JointViewType`, `axis?: JointViewAxis`, `pivot?: [ number, number, number ]`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `hidden?: boolean`
9134
-
9135
- `JointViewCouplingInput`: `{ joint: string, terms: JointViewCouplingTermInput[], offset?: number }`
9136
-
9137
- `JointViewCouplingTermInput`: `{ joint: string, ratio?: number }`
9138
-
9139
- `JointViewAnimationInput`: `{ name: string, duration?: number, loop?: boolean, continuous?: boolean, keyframes: JointViewAnimationKeyframeInput[] }`
9140
-
9141
- **`JointViewAnimationKeyframeInput`**
9142
- - `at?: number` — Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights.
9143
- - `ticks?: number` — Relative weight of the segment from this keyframe to the next (default 1). Only used in tick-based mode (when `at` is omitted). Last keyframe's ticks value is ignored.
9144
- - Also: `values: Record<string, number>`
9145
-
9146
9486
  #### `compareWith()` — Declare a reference model for comparison inspection.
9147
9487
 
9148
9488
  `compareWith()` lets a model carry its own comparison target for inspection workflows. `forgecad inspect compare overlay model.forge.js` uses this reference to render the same Difference Only comparison overlay as the live viewport. Amber marks candidate mismatch evidence, cyan marks reference mismatch evidence, and faint model context keeps the overlay readable. When the CLI can resolve the referenced file, the manifest also includes the same geometric score produced by `forgecad compare 3d`.
@@ -9189,13 +9529,13 @@ Overloads:
9189
9529
  - `offset?: number` — Optional offset along the plane normal (primarily for object-form overload).
9190
9530
  - `exclude?: CutPlaneExcludeInput` — Object names to keep uncut for this plane.
9191
9531
 
9192
- #### `mock()` — Register a mock (context) object for visualization and collision checking.
9532
+ #### `mock()` — Register a mock (context) object for visualization and inspection.
9193
9533
 
9194
- Mock objects appear in the viewport and spatial analysis when you run a file directly, but are excluded when the file is imported via [`require()`](/docs/core#require). This lets you model the surrounding context — walls, bolts, mating parts — without polluting the module's exports.
9534
+ Mock objects appear in the viewport and inspection analysis when you run a file directly, but are excluded when the file is imported via [`require()`](/docs/core#require). This lets you model the surrounding context — walls, bolts, mating parts — without polluting the module's exports.
9195
9535
 
9196
9536
  The shape is returned unchanged, so you can reference it for alignment, dimensioning, and `verify` checks.
9197
9537
 
9198
- Mock objects participate in `forgecad run --spatial bounded|exact` collision detection and spatial analysis. Their names appear with a `(mock)` suffix in reports.
9538
+ Mock objects participate in focused inspection commands such as `forgecad inspect fit interference` and `forgecad inspect physical gaps`. Their names appear with a `(mock)` suffix in reports.
9199
9539
 
9200
9540
  In the viewport, mock objects render at reduced opacity so they are visually distinct from real geometry.
9201
9541
 
@@ -9475,6 +9815,30 @@ For deeper API coverage, load the relevant generated doc group from the skill so
9475
9815
 
9476
9816
  How to build mechanical joints — clevis-tongue hinges, ball-and-socket, dovetails — that actually rotate without binding and stop where they should.
9477
9817
 
9818
+ ## Frame-Aware Connectors First
9819
+
9820
+ If a part must rotate, slide, or point in a specific physical direction, define connectors and use `assembly().connect()`. A connector is a small coordinate frame on the part:
9821
+
9822
+ - `origin` is the pivot, pin center, socket center, or contact point.
9823
+ - `axis` is the hinge line or slide direction.
9824
+ - `up` is the secondary direction that fixes the part's zero-angle twist.
9825
+
9826
+ For a hip -> knee -> wheel chain, the upper leg should be a real part with a hip connector and a knee connector. The hip connector frame defines how the upper leg sits on the hip drum at rest; the knee connector frame defines where the next part attaches. `connect()` aligns those frames and derives the joint axis.
9827
+
9828
+ Do not treat `up` as optional on hinges, wheels, levers, or keyed sliders. If `up` is omitted, ForgeCAD chooses a deterministic perpendicular vector, which keeps the model stable but may not match the intended mechanical rest pose.
9829
+
9830
+ Use `link()`, `edgeBetweenLinks()`, and `addAngleBetweenLinks()` for solved point skeletons and closed-loop relationships. A link is a point, not a bone frame. `addPart(..., { mate: { connector, toLink } })` only moves a connector origin onto a solved link point; it does not rotate the part to aim along an edge.
9831
+
9832
+ ## Mirrored Revolute Axes
9833
+
9834
+ Revolute joint values are physical values signed by the right-hand rule around the joint axis. If a bilateral mechanism mirrors a hinge axis, the same numeric joint value does not mean "same pose" on both sides.
9835
+
9836
+ Example: a right ankle hinge with `axis: [1, 0, 0]` and a left ankle hinge with `axis: [-1, 0, 0]` are exact geometric mirrors at rest. But `+20` degrees around those two axes rotates the feet in opposite fore/aft senses. The mirrored pose uses `RightAnkle: 20` and `LeftAnkle: -20`.
9837
+
9838
+ When you expose physical joint limits directly, mirror revolute limits as `[min, max] -> [-max, -min]`. Prismatic joints do not have this angle-handedness flip because their scalar value translates along the mirrored axis.
9839
+
9840
+ For bilateral robots, legs, grippers, and suspension mechanisms, prefer a side-neutral control layer when possible: solve mirrored link positions with `link()`, `edgeBetweenLinks()`, and `addAngleBetweenLinks()`, then attach real parts to the solved skeleton. If you use connector-frame joints directly, make the sign mapping explicit in defaults, keyframes, and verification.
9841
+
9478
9842
  ## The Cavity Rule
9479
9843
 
9480
9844
  Every mechanical joint has a **cavity** in one part and a **tenon** in the other. The cavity must be a real empty volume — not a gap implied by the absence of two separate solids.
@@ -9606,7 +9970,7 @@ stable; named scene views are still available through `--view`.
9606
9970
  The command tree is intentionally job-shaped:
9607
9971
 
9608
9972
  ```text
9609
- inspect visual image|cutaway|depth|normals|objects
9973
+ inspect visual image|cutaway|depth|normals|rig|objects
9610
9974
  inspect surface zebra|roughness
9611
9975
  inspect physical components|floating|gaps
9612
9976
  inspect fit interference
@@ -9647,6 +10011,7 @@ Use targeted evidence commands for expensive analyses:
9647
10011
  ```bash
9648
10012
  forgecad inspect visual depth model.forge.js --camera iso
9649
10013
  forgecad inspect visual normals model.forge.js --camera iso
10014
+ forgecad inspect visual rig model.forge.js --camera iso
9650
10015
  forgecad inspect surface zebra model.forge.js --camera iso
9651
10016
  forgecad inspect surface roughness model.forge.js --camera iso
9652
10017
  forgecad inspect visual objects model.forge.js --camera iso
@@ -10132,12 +10497,33 @@ color(value: string | undefined): SdfShape
10132
10497
  material(props: ShapeMaterialProps): SdfShape
10133
10498
  ```
10134
10499
 
10500
+ **`ShapeMaterialProps`**
10501
+
10502
+ | Option | Type | Description |
10503
+ |--------|------|-------------|
10504
+ | `metalness?` | `number` | Metalness factor (0 = dielectric, 1 = metal). Default: 0.05 |
10505
+ | `roughness?` | `number` | Roughness factor (0 = mirror, 1 = fully diffuse). Default: 0.35 |
10506
+ | `emissive?` | `string` | Emissive glow color (hex string, e.g. "#ff6b35"). |
10507
+ | `emissiveIntensity?` | `number` | Emissive intensity multiplier. Default: 1 |
10508
+ | `opacity?` | `number` | Opacity (0 = fully transparent, 1 = fully opaque). Default: 1 |
10509
+ | `wireframe?` | `boolean` | Render as wireframe. Default: false |
10510
+ | `clearcoat?` | `number` | Clearcoat intensity (0–1). Default: 0.1 |
10511
+ | `clearcoatRoughness?` | `number` | Clearcoat roughness (0–1). Default: 0.4 |
10512
+ | `transmission?` | `number` | Glass/translucency transmission factor (0–1). Renderer support depends on target. |
10513
+ | `ior?` | `number` | Index of refraction for transmissive materials. Typical glass is ~1.45. |
10514
+ | `thickness?` | `number` | Approximate transmissive volume thickness in model units. |
10515
+ | `specularIntensity?` | `number` | Specular highlight intensity (0–1). |
10516
+ | `specularColor?` | `string` | Specular highlight tint. |
10517
+ | `reflectivity?` | `number` | Reflection strength for supported renderers (0–1). |
10518
+
10135
10519
  #### `bounds()` — Set explicit preview/meshing bounds for this implicit leaf.
10136
10520
 
10137
10521
  ```ts
10138
10522
  bounds(bounds: SdfBounds | [ Vec3, Vec3 ]): SdfShape
10139
10523
  ```
10140
10524
 
10525
+ `SdfBounds`: `{ min: Vec3, max: Vec3 }`
10526
+
10141
10527
  #### `at()` — Sculpt-style alias for translate().
10142
10528
 
10143
10529
  ```ts
@@ -10234,6 +10620,12 @@ fillWith(pattern: SdfShape): SdfShape
10234
10620
  fillWithGyroid(options: TpmsOptions): SdfShape
10235
10621
  ```
10236
10622
 
10623
+ **`TpmsOptions`**
10624
+ - `thickness?: number` — Dimensionless field threshold kept for compatibility. Prefer `wallThickness` for approximate millimeter units.
10625
+ - `wallThickness?: number` — Approximate physical wall thickness in millimeters.
10626
+ - `tpmsThicknessMode?: TpmsThicknessMode` — Override TPMS thickness interpretation. Defaults to metric when `wallThickness` is used, field-threshold when `thickness` is used.
10627
+ - Also: `cellSize: number`
10628
+
10237
10629
  #### `fillWithSchwarzP()` — Keep only the Schwarz-P lattice inside this shape.
10238
10630
 
10239
10631
  ```ts
@@ -10384,6 +10776,10 @@ shape.surfaceDisplace((u, v) => -Math.sin(u * 2) * 0.3)
10384
10776
  surfaceDisplace(pattern: SurfacePattern | ((u: number, v: number) => number), options?: SurfaceDisplaceOptions): SdfShape
10385
10777
  ```
10386
10778
 
10779
+ **`SurfaceDisplaceOptions`**
10780
+ - `uv?: "auto" | "sphere" | "cylinder" | "torus" | "triplanar"` — Override auto-detected UV mode. Default: 'auto' (detects from SDF tree).
10781
+ - `triplanarSharpness?: number` — Triplanar blend sharpness — higher = crisper transitions. Default: 4. Only used in triplanar mode.
10782
+
10387
10783
  #### `onion()` — Create concentric onion layers.
10388
10784
 
10389
10785
  ```ts