forgecad 0.9.14 → 0.9.16

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 (239) hide show
  1. package/LICENSE +6 -4
  2. package/README.md +8 -4
  3. package/dist/assets/{AdminPage-eWGs2K6H.js → AdminPage-CXvls4-J.js} +2 -2
  4. package/dist/assets/{BenchmarkPage-CTrLKfpo.js → BenchmarkPage-B27zk8xL.js} +4 -15
  5. package/dist/assets/{BlogPage-5nPesyds.js → BlogPage-CMAVvgQL.js} +2 -2
  6. package/dist/assets/{DocsPage-C4Y3nbYc.js → DocsPage-knf4I4h7.js} +9 -3
  7. package/dist/assets/EditorApp-BHMQlJ-D.js +14686 -0
  8. package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-BpjZgzk0.css} +846 -0
  9. package/dist/assets/{EmbedViewer-C8fB4n5U.js → EmbedViewer-D7ZGlFjx.js} +3 -3
  10. package/dist/assets/{LandingPageProofDriven-jSz0LaMM.js → LandingPageProofDriven-CnevhTE8.js} +36 -38
  11. package/dist/assets/LegalPage-BPTUmqeg.js +39 -0
  12. package/dist/assets/LegalPage-BRlScr9A.css +91 -0
  13. package/dist/assets/{PricingPage-B83B90zh.js → PricingPage-B0D4goG_.js} +19 -19
  14. package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
  15. package/dist/assets/{SettingsPage-DY889pcu.js → SettingsPage-CFF-UgjI.js} +2 -2
  16. package/dist/assets/app-CE3sYcV7.css +3890 -0
  17. package/dist/assets/{app-bEww1ic4.js → app-T0pDcSX4.js} +3382 -1069
  18. package/dist/assets/cli/{render-Cho2uKG_.js → render-C5pcIISc.js} +477 -29
  19. package/dist/assets/{constructionHistoryWorker-HYwzJY4m.js → constructionHistoryWorker-Ba2Hm58b.js} +928 -243
  20. package/dist/assets/{evalWorker-CjQwJSE-.js → evalWorker-vkx310U2.js} +8883 -6040
  21. package/dist/assets/{forgecad_geometry-CH2nvuLA.js → forgecad_geometry-Dgceylq9.js} +43 -1
  22. package/dist/assets/forgecad_geometry_bg-dD4RNQF1.wasm +0 -0
  23. package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-BuTJDVX6.js} +1179 -273
  24. package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
  25. package/dist/assets/{targets-D6PWsv6X.js → jointPose-B_Cgedn9.js} +71 -3
  26. package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
  27. package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
  28. package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
  29. package/dist/assets/{manifold-rmfAcdwF.js → manifold-BWgsjmAM.js} +1 -1
  30. package/dist/assets/{manifold-uRzgk5O8.js → manifold-D6IFSkhH.js} +2 -2
  31. package/dist/assets/{manifold-CG9Fokx-.js → manifold-rZexZI0G.js} +1 -1
  32. package/dist/assets/{reportWorker-4cW_ZpoS.js → reportWorker-0AGij1Ru.js} +8659 -12771
  33. package/dist/assets/{scalar-sampling-budget-CfDiFvh7.js → scalar-sampling-budget-J5cuzxT1.js} +8050 -6203
  34. package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-Vl4Wxa1y.js} +50 -6
  35. package/dist/assets/{solver-DuJAO8S6.js → solver-BZ9LPTHs.js} +1 -1
  36. package/dist/assets/solver_bg-DAHZJ_rw.wasm +0 -0
  37. package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
  38. package/dist/cli/render.html +1 -1
  39. package/dist/docs/index.html +2 -2
  40. package/dist/docs-raw/AI/ai-native-cad.md +50 -0
  41. package/dist/docs-raw/AI/usage.md +5 -12
  42. package/dist/docs-raw/CLI.md +34 -10
  43. package/dist/docs-raw/component-model.md +27 -11
  44. package/dist/docs-raw/generated/assembly.md +374 -187
  45. package/dist/docs-raw/generated/concepts.md +245 -237
  46. package/dist/docs-raw/generated/core.md +283 -6
  47. package/dist/docs-raw/generated/curves.md +274 -361
  48. package/dist/docs-raw/generated/lib.md +9 -19
  49. package/dist/docs-raw/generated/output.md +29 -4
  50. package/dist/docs-raw/generated/runtime-names.md +49 -0
  51. package/dist/docs-raw/generated/sdf.md +31 -0
  52. package/dist/docs-raw/generated/sheet-metal.md +9 -0
  53. package/dist/docs-raw/generated/sketch.md +44 -1
  54. package/dist/docs-raw/generated/viewport.md +11 -3
  55. package/dist/docs-raw/guides/coordinate-system.md +20 -16
  56. package/dist/docs-raw/guides/geometry-conventions.md +2 -2
  57. package/dist/docs-raw/guides/inspection-bundles.md +2 -1
  58. package/dist/docs-raw/guides/joint-design.md +24 -0
  59. package/dist/docs-raw/guides/positioning.md +13 -3
  60. package/dist/docs-raw/legal/privacy.md +63 -0
  61. package/dist/docs-raw/legal/software-license.md +55 -0
  62. package/dist/docs-raw/legal/terms.md +87 -0
  63. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +1 -1
  64. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
  65. package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
  66. package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
  67. package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
  68. package/dist/docs-raw/skills/forgecad-lld.md +1 -1
  69. package/dist/docs-raw/skills/forgecad-make-a-model.md +40 -39
  70. package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
  71. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
  72. package/dist/docs-raw/skills/forgecad-project.md +3 -1
  73. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +1 -1
  74. package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
  75. package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
  76. package/dist/docs-raw/skills/forgecad.md +4 -3
  77. package/dist/docs-raw/welcome.md +2 -0
  78. package/dist/index.html +40 -12
  79. package/dist/llms.txt +8 -0
  80. package/dist/site.webmanifest +1 -1
  81. package/dist/sitemap.xml +49 -13
  82. package/dist-cli/{check-compiler-U5SOPN7X.js → check-compiler-SYQ2PWOB.js} +1 -2
  83. package/dist-cli/{check-query-propagation-XOKNSSYU.js → check-query-propagation-HIAGV62W.js} +1 -2
  84. package/dist-cli/{chunk-EXWGNL6K.js → chunk-SPZE3DUY.js} +20659 -17930
  85. package/dist-cli/forgecad.js +3568 -1250
  86. package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
  87. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  88. package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
  89. package/dist-cli/solver_bg.wasm +0 -0
  90. package/dist-skill/CONTEXT.md +1192 -725
  91. package/dist-skill/SKILL.md +3 -2
  92. package/dist-skill/docs/API/core/concepts.md +64 -1
  93. package/dist-skill/docs/CLI.md +34 -10
  94. package/dist-skill/docs/generated/assembly.md +339 -213
  95. package/dist-skill/docs/generated/core.md +283 -6
  96. package/dist-skill/docs/generated/curves.md +272 -362
  97. package/dist-skill/docs/generated/lib.md +9 -19
  98. package/dist-skill/docs/generated/output.md +29 -4
  99. package/dist-skill/docs/generated/runtime-names.md +40 -0
  100. package/dist-skill/docs/generated/sdf.md +31 -0
  101. package/dist-skill/docs/generated/sheet-metal.md +9 -0
  102. package/dist-skill/docs/generated/sketch.md +44 -2
  103. package/dist-skill/docs/generated/viewport.md +2 -87
  104. package/dist-skill/docs/guides/coordinate-system.md +20 -16
  105. package/dist-skill/docs/guides/geometry-conventions.md +2 -2
  106. package/dist-skill/docs/guides/inspection-bundles.md +2 -1
  107. package/dist-skill/docs/guides/joint-design.md +24 -0
  108. package/dist-skill/docs/guides/positioning.md +13 -3
  109. package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
  110. package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
  111. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
  112. package/dist-skill/library/forgecad-make-a-model/SKILL.md +39 -38
  113. package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
  114. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
  115. package/dist-skill/library/forgecad-project/SKILL.md +2 -0
  116. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  117. package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
  118. package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
  119. package/examples/api/assembly-kinematics-limb.forge.js +116 -0
  120. package/examples/api/connector-frame-rig-chain.forge.js +102 -0
  121. package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
  122. package/examples/api/exact-surface-studio.forge.js +6 -8
  123. package/examples/api/helix-basics.forge.js +8 -8
  124. package/examples/api/lean-foundations/README.md +12 -0
  125. package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
  126. package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
  127. package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
  128. package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
  129. package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
  130. package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
  131. package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
  132. package/examples/api/route3d-elbow.forge.js +71 -0
  133. package/examples/api/transition-curves.forge.js +44 -15
  134. package/examples/api/variable-sweep-test.forge.js +3 -1
  135. package/examples/api/y-blend-corner-showcase.forge.js +0 -2
  136. package/examples/generative/coral-vase.forge.js +1 -1
  137. package/examples/nurbs-tube.forge.js +1 -1
  138. package/package.json +17 -13
  139. package/dist/assets/EditorApp-lXv53A1m.js +0 -13610
  140. package/dist/assets/app-CsHnaBWt.css +0 -1789
  141. package/dist/assets/forgecad_geometry_bg-C5_E9Oa9.wasm +0 -0
  142. package/dist/assets/solver_bg-CWvv4lnN.wasm +0 -0
  143. package/dist/docs-raw/API/README.md +0 -16
  144. package/dist/docs-raw/API/core/concepts.md +0 -118
  145. package/dist/docs-raw/INDEX.md +0 -138
  146. package/dist/docs-raw/RELEASING.md +0 -87
  147. package/dist/docs-raw/agent-native-api.md +0 -27
  148. package/dist/docs-raw/beta-deployment.md +0 -304
  149. package/dist/docs-raw/beta-operations.md +0 -325
  150. package/dist/docs-raw/blueprint-first.md +0 -145
  151. package/dist/docs-raw/cli-monetization.md +0 -112
  152. package/dist/docs-raw/coding-best-practices.md +0 -120
  153. package/dist/docs-raw/coding.md +0 -340
  154. package/dist/docs-raw/deployment.md +0 -374
  155. package/dist/docs-raw/guides/skill-maintenance.md +0 -161
  156. package/dist/docs-raw/guides/surface-members.md +0 -82
  157. package/dist/docs-raw/harbor-cli.md +0 -854
  158. package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
  159. package/dist/docs-raw/internals/compiler.md +0 -307
  160. package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
  161. package/dist/docs-raw/internals/constraint-solver.md +0 -176
  162. package/dist/docs-raw/internals/shape-from-slices.md +0 -152
  163. package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
  164. package/dist/docs-raw/platform/admin.md +0 -45
  165. package/dist/docs-raw/platform/architecture.md +0 -82
  166. package/dist/docs-raw/platform/auth.md +0 -139
  167. package/dist/docs-raw/platform/email.md +0 -67
  168. package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
  169. package/dist/docs-raw/platform/observability.md +0 -197
  170. package/dist/docs-raw/platform/projects.md +0 -111
  171. package/dist/docs-raw/platform/sharing.md +0 -90
  172. package/dist/docs-raw/product/README.md +0 -39
  173. package/dist/docs-raw/product/api-as-product-language.md +0 -13
  174. package/dist/docs-raw/product/business-model.md +0 -15
  175. package/dist/docs-raw/product/competitive-positioning.md +0 -17
  176. package/dist/docs-raw/product/creative-manufacturing.md +0 -15
  177. package/dist/docs-raw/product/founder-story.md +0 -11
  178. package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
  179. package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
  180. package/dist/docs-raw/product/product-loop.md +0 -17
  181. package/dist/docs-raw/product/strategic-decisions.md +0 -22
  182. package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
  183. package/dist/docs-raw/product/user-segments.md +0 -15
  184. package/dist/docs-raw/product/vision.md +0 -26
  185. package/dist/docs-raw/rl-environments.md +0 -350
  186. package/dist/docs-raw/runbook.md +0 -611
  187. package/dist-cli/check-compiler-U5SOPN7X.js.map +0 -1
  188. package/dist-cli/check-query-propagation-XOKNSSYU.js.map +0 -1
  189. package/dist-cli/chunk-EXWGNL6K.js.map +0 -1
  190. package/dist-cli/forgecad.js.map +0 -1
  191. package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
  192. package/dist-cli/solver-46FFSK2U.js.map +0 -1
  193. package/dist-skill/SKILL-dev.md +0 -145
  194. package/dist-skill/docs-dev/API/core/concepts.md +0 -118
  195. package/dist-skill/docs-dev/CLI.md +0 -677
  196. package/dist-skill/docs-dev/agent-native-api.md +0 -27
  197. package/dist-skill/docs-dev/blueprint-first.md +0 -145
  198. package/dist-skill/docs-dev/coding-best-practices.md +0 -120
  199. package/dist-skill/docs-dev/coding.md +0 -340
  200. package/dist-skill/docs-dev/component-model.md +0 -164
  201. package/dist-skill/docs-dev/generated/assembly.md +0 -794
  202. package/dist-skill/docs-dev/generated/core.md +0 -2117
  203. package/dist-skill/docs-dev/generated/curves.md +0 -2583
  204. package/dist-skill/docs-dev/generated/lib.md +0 -169
  205. package/dist-skill/docs-dev/generated/output.md +0 -247
  206. package/dist-skill/docs-dev/generated/sdf.md +0 -446
  207. package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
  208. package/dist-skill/docs-dev/generated/sketch.md +0 -1811
  209. package/dist-skill/docs-dev/generated/viewport.md +0 -585
  210. package/dist-skill/docs-dev/generated/wood.md +0 -108
  211. package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
  212. package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
  213. package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
  214. package/dist-skill/docs-dev/guides/joint-design.md +0 -78
  215. package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
  216. package/dist-skill/docs-dev/guides/positioning.md +0 -161
  217. package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
  218. package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
  219. package/dist-skill/docs-dev/internals/compiler.md +0 -307
  220. package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
  221. package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
  222. package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
  223. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.mjs +0 -289
  224. package/examples/api/bolted-service-cover.forge.js +0 -17
  225. package/examples/api/cable-gland-anchor.forge.js +0 -14
  226. package/examples/api/captured-cartridge-guide.forge.js +0 -14
  227. package/examples/api/captured-linear-slide.forge.js +0 -13
  228. package/examples/api/clevis-pin-joint.forge.js +0 -13
  229. package/examples/api/datum-enclosure.forge.js +0 -16
  230. package/examples/api/hose-barb-port.forge.js +0 -14
  231. package/examples/api/knuckled-hinge-assembly.forge.js +0 -15
  232. package/examples/api/living-hinge-cover.forge.js +0 -14
  233. package/examples/api/pcb-terminal-block.forge.js +0 -22
  234. package/examples/api/pinned-lever-pivot-stack.forge.js +0 -14
  235. package/examples/api/retained-shaft-knob-stack.forge.js +0 -15
  236. package/examples/api/routed-tube-clip.forge.js +0 -15
  237. package/examples/api/seated-bearing-stack.forge.js +0 -30
  238. package/examples/api/snap-latch-cover.forge.js +0 -14
  239. package/examples/api/thumb-screw-clamp.forge.js +0 -15
@@ -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,46 @@ 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, connector, console, constrainedSketch, Constraint
297
+ Counterbore, Curve, Curve3D, cutPlane, cylinder, degrees, difference, difference2d
298
+ dim, dimLine, draft, ellipse, explodeView, faceProfile, fillet, filletCorners
299
+ filletTrackedEdge, fingerJoint, flatPanel, flatPart, formatInstructions, Function, gcode, GCodeBuilder
300
+ getActiveBackend, global, globalThis, group, highlight, Import, ImportedAssembly, importMesh
301
+ importStep, importSvgSketch, initKernel, intersection, intersection2d, intersectWithPlane, joint, jointsView
302
+ laserKit, lib, line, Line2D, linearPattern, linearPattern2d, listParam, loadFont
303
+ loft, Loft, lookupKerf, mirrorCopy, mock, ngon, NurbsCurve3D, nurbsSurface
304
+ NurbsSurface, offsetSolid, param, Param, path, point, Point2D, Points
305
+ polygon, polygonVertices, port, Product, ProductHandleBuilder, ProductHandleFeature, ProductPanelBuilder, ProductRibbonBuilder
306
+ ProductSkin, ProductSkinBuilder, ProductSpoutBuilder, ProductStationBuilder, ProductSurfaceBuilder, ProductSurfaceRef, projectToPlane, queueMicrotask
307
+ radians, rect, Rectangle2D, Ribs, robotExport, roundedRect, Route3D, scene
308
+ Sculpt, sdf, SdfShape, selectEdge, selectEdges, self, setActiveBackend, setImmediate
309
+ setInterval, setTimeout, Shape, ShapeGroup, sheetMetal, SheetMetalPart, sheetStock, Sketch
310
+ sketchToDxf, sketchToSvg, slot, Slot, SolvedAssembly, spec, sphere, spline2d
311
+ star, stroke, Surface, SurfaceBody, SurfaceMembers, surfacePatch, sweep, tabSlot
312
+ text2d, textWidth, torus, toShape, Transform, union, union2d, variableSweep
313
+ verify, viewConfig, Viewport, window, Wood
314
+ ```
315
+
316
+ `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.
317
+
318
+ ---
319
+
217
320
  <!-- generated/core.md -->
218
321
 
219
322
  # Core API
@@ -238,6 +341,8 @@ For organic shapes, smooth blending, TPMS lattices, and surface deformations. Re
238
341
  - [SurfacePattern](#surfacepattern)
239
342
  - [Pattern2D](#pattern2d)
240
343
  - [Pattern2DBuilder](#pattern2dbuilder)
344
+ - [HermiteCurve3D](#hermitecurve3d)
345
+ - [QuinticHermiteCurve3D](#quintichermitecurve3d)
241
346
  - [ShapeRef](#shaperef)
242
347
  - [ANCHOR3D_NAMES](#anchor3d-names)
243
348
  - [verify](#verify)
@@ -577,9 +682,9 @@ selectEdges(shape: Shape, query?: EdgeQuery): EdgeSegment[]
577
682
  | `minLength?` | `number` | Filter: minimum edge length. |
578
683
  | `maxLength?` | `number` | Filter: maximum edge length. |
579
684
  | `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`). |
685
+ | `atZ?` | `number` | Shorthand: edge midpoint Z is approximately this value within `tolerance`. |
686
+ | `tolerance?` | `number` | Position tolerance for approximate matches. Used by `atZ` and `near`. Default: `1.0`. |
687
+ | `angleTolerance?` | `number` | Angular tolerance in degrees for `parallel`/`perpendicular` filters. Default: `10`. |
583
688
 
584
689
  `BoundingRegion`: `{ xMin?: number, xMax?: number, yMin?: number, yMax?: number, zMin?: number, zMax?: number }`
585
690
 
@@ -631,7 +736,16 @@ coalesceEdges(segments: EdgeSegment[], tolerance?: number): EdgeSegment[]
631
736
 
632
737
  #### `require()` — Import a module with optional ForgeCAD parameter overrides. Returns the module's exports.
633
738
 
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.
739
+ 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.
740
+
741
+ **Assembly return contract**
742
+
743
+ | `.forge.js` return value | `require()` result |
744
+ |---|---|
745
+ | `Assembly` | `ImportedAssembly` |
746
+ | `SolvedAssembly` | `SolvedAssembly` |
747
+
748
+ [`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
749
 
636
750
  **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
751
 
@@ -662,6 +776,26 @@ mount.bolts.pos // access the metadata
662
776
  mount.shape // access the geometry
663
777
  ```
664
778
 
779
+ **Forge-aware builder module pattern** — use `.forge.js` modules for reusable sketch, profile, shape, or assembly builders that need ForgeCAD runtime APIs:
780
+
781
+ ```js
782
+ // profiles.forge.js — inspectable on its own, reusable through require()
783
+ function wheelProfile() {
784
+ return circle2d(40).subtract(circle2d(18));
785
+ }
786
+
787
+ return {
788
+ preview: [{ name: 'Wheel profile', sketch: wheelProfile() }],
789
+ make: { wheelProfile },
790
+ };
791
+
792
+ // main.forge.js
793
+ const profiles = require('./profiles.forge.js');
794
+ const wheel = profiles.make.wheelProfile().extrude(8);
795
+ ```
796
+
797
+ 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.
798
+
665
799
  ```ts
666
800
  require(path: string, paramOverrides?: Record<string, number | string>): any
667
801
  ```
@@ -690,10 +824,18 @@ importSvgSketch(fileName: string, options?: SvgImportOptions): Sketch
690
824
  | `simplify?` | `number` | Simplification tolerance for final sketch cleanup. |
691
825
  | `invertY?` | `boolean` | Flip SVG Y-down coordinates to CAD Y-up. Enabled by default. |
692
826
 
693
- #### `importMesh()` — Import an external mesh file (STL, OBJ, 3MF) as a Shape.
827
+ #### `importMesh()` — Import an external mesh file (STL, OBJ, 3MF).
828
+
829
+ 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`.
830
+
831
+ ```js
832
+ const all = importMesh("./assembly.3mf", { separateObjects: true });
833
+ const pin = all.child("Pin #001");
834
+ const plate = importMesh("./assembly.3mf", { object: "3mf:build:001:object:7" });
835
+ ```
694
836
 
695
837
  ```ts
696
- importMesh(fileName: string, options?: { scale?: number; center?: boolean; }): Shape
838
+ importMesh(fileName: string, options?: { scale?: number; center?: boolean; object?: string; separateObjects?: boolean; }): Shape | ShapeGroup
697
839
  ```
698
840
 
699
841
  #### `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 +1308,25 @@ box(100, 100, 10).color('#gold').material({ metalness: 0.95, roughness: 0.05 }).
1166
1308
  material(props: ShapeMaterialProps): Shape
1167
1309
  ```
1168
1310
 
1311
+ **`ShapeMaterialProps`**
1312
+
1313
+ | Option | Type | Description |
1314
+ |--------|------|-------------|
1315
+ | `metalness?` | `number` | Metalness factor (0 = dielectric, 1 = metal). Default: 0.05 |
1316
+ | `roughness?` | `number` | Roughness factor (0 = mirror, 1 = fully diffuse). Default: 0.35 |
1317
+ | `emissive?` | `string` | Emissive glow color (hex string, e.g. "#ff6b35"). |
1318
+ | `emissiveIntensity?` | `number` | Emissive intensity multiplier. Default: 1 |
1319
+ | `opacity?` | `number` | Opacity (0 = fully transparent, 1 = fully opaque). Default: 1 |
1320
+ | `wireframe?` | `boolean` | Render as wireframe. Default: false |
1321
+ | `clearcoat?` | `number` | Clearcoat intensity (0–1). Default: 0.1 |
1322
+ | `clearcoatRoughness?` | `number` | Clearcoat roughness (0–1). Default: 0.4 |
1323
+ | `transmission?` | `number` | Glass/translucency transmission factor (0–1). Renderer support depends on target. |
1324
+ | `ior?` | `number` | Index of refraction for transmissive materials. Typical glass is ~1.45. |
1325
+ | `thickness?` | `number` | Approximate transmissive volume thickness in model units. |
1326
+ | `specularIntensity?` | `number` | Specular highlight intensity (0–1). |
1327
+ | `specularColor?` | `string` | Specular highlight tint. |
1328
+ | `reflectivity?` | `number` | Reflection strength for supported renderers (0–1). |
1329
+
1169
1330
  **Face Topology**
1170
1331
 
1171
1332
  #### `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 +1447,15 @@ body.edgesOf('top', { concave: true })
1286
1447
  edgesOf(faceLabel: string, options?: EdgesOfOptions): EdgeSegment[]
1287
1448
  ```
1288
1449
 
1450
+ **`EdgesOfOptions`**
1451
+
1452
+ | Option | Type | Description |
1453
+ |--------|------|-------------|
1454
+ | `exclude?` | `string \| string[]` | Exclude edges shared with these named faces. |
1455
+ | `convex?` | `boolean` | Additional geometric filter: only convex edges. |
1456
+ | `concave?` | `boolean` | Additional geometric filter: only concave edges. |
1457
+ | `minLength?` | `number` | Minimum edge length filter. |
1458
+
1289
1459
  #### `edgesBetween()` — Return edges shared between two named faces.
1290
1460
 
1291
1461
  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 +1537,8 @@ rotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): Sh
1367
1537
  rotateAroundTo(axis: [ number, number, number ], pivot: [ number, number, number ], movingPoint: RotationPointLike, targetPoint: RotationPointLike, options?: RotateAroundToOptions): Shape
1368
1538
  ```
1369
1539
 
1540
+ `RotateAroundToOptions`: `{ mode?: RotateAroundToMode }`
1541
+
1370
1542
  #### `transform()` — Apply a 4x4 affine transform matrix (column-major) or a Transform object.
1371
1543
 
1372
1544
  ```ts
@@ -1463,6 +1635,11 @@ box(100, 100, 20).pocket('top', 8, { scale: 0.8 })
1463
1635
  pocket(face: FaceSelector, depth: number, opts?: PocketOptions): Shape
1464
1636
  ```
1465
1637
 
1638
+ **`PocketOptions`**
1639
+ - `inset?: number` — Shrink the face boundary inward by this many mm before extruding. Produces angled walls when combined with depth. Default: 0 (full face).
1640
+ - `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.
1641
+ - `join?: "Square" | "Round" | "Miter"` — Corner join style when using `inset`. Default: 'Round'.
1642
+
1466
1643
  #### `boss()` — Add a boss (protrusion) from the named face.
1467
1644
 
1468
1645
  ```js
@@ -1485,6 +1662,30 @@ box(50, 50, 20).hole('top', { diameter: 6, counterbore: { diameter: 12, depth: 3
1485
1662
  hole(faceOrRef: SketchFaceTarget | FaceRef, opts: ShapeHoleOptions): Shape
1486
1663
  ```
1487
1664
 
1665
+ **`FaceRef`**
1666
+
1667
+ | Option | Type | Description |
1668
+ |--------|------|-------------|
1669
+ | `normal` | `[ number, number, number ]` | Normal direction of the face |
1670
+ | `center` | `[ number, number, number ]` | Center point of the face |
1671
+ | `query?` | `FaceQueryRef` | Compiler-owned face query when available. |
1672
+ | `planar?` | `boolean` | True when the face can host a 2D sketch placement frame |
1673
+ | `uAxis?` | `[ number, number, number ]` | Face-local horizontal axis for planar faces |
1674
+ | `vAxis?` | `[ number, number, number ]` | Face-local vertical axis for planar faces |
1675
+ | `surface?` | `FaceSurface` | Analytic surface family when the backend can identify one. |
1676
+ | `descendant?` | `FaceDescendantMetadata` | Shared descendant-resolution metadata when this face is a semantic region/set. |
1677
+ | `name` | | — |
1678
+
1679
+ **`FaceDescendantMetadata`**: `kind: "single" | "face-set"`, `semantic: FaceDescendantSemantic`, `memberCount: number`, `memberNames: string[]`, `coplanar: boolean`
1680
+
1681
+ **`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`
1682
+
1683
+ `ShapeFeatureExtentOptions`: `{ forward: ShapeFeatureExtentSideOptions, reverse?: ShapeFeatureExtentSideOptions }`
1684
+
1685
+ `ShapeFeatureExtentSideOptions`: `{ depth?: number, upToFace?: SketchFaceTarget | FaceRef, through?: boolean }`
1686
+
1687
+ **`ShapeHoleThreadOptions`**: `designation?: string`, `pitch?: number`, `class?: string`, `handedness?: "right" | "left"`, `depth?: number`, `modeled?: boolean`
1688
+
1488
1689
  #### `cutout()` — Cut a profile-shaped pocket through a face using a placed sketch.
1489
1690
 
1490
1691
  The sketch must be placed on a face with `Sketch.onFace(...)`. The cut follows the sketch's 2D profile.
@@ -1498,6 +1699,8 @@ body.cutout(profile, { depth: 5 })
1498
1699
  cutout(sketch: Sketch, opts?: ShapeCutoutOptions): Shape
1499
1700
  ```
1500
1701
 
1702
+ **`ShapeCutoutOptions`**: `depth?: number`, `upToFace?: SketchFaceTarget | FaceRef`, `extent?: ShapeFeatureExtentOptions`, `taperScale?: number | [ number, number ]`
1703
+
1501
1704
  **Placement**
1502
1705
 
1503
1706
  #### `placeReference()` — Translate the shape so the given anchor or reference lands on the target coordinate.
@@ -1561,6 +1764,11 @@ mast.translate(0, station, radius + 50).seatInto(fuselage, 'mount', { depth: 'fl
1561
1764
  seatInto(target: Shape, surface: string, options?: SeatIntoOptions): Shape
1562
1765
  ```
1563
1766
 
1767
+ **`SeatIntoOptions`**
1768
+ - `along?: [ number, number, number ]` — Movement axis. Default: inverted face normal (points into target).
1769
+ - `depth?: "full" | "flush" | number` — How deep to embed. 'full' = entire face inside. 'flush' = nearest point touches. number = mm past flush. Default: 'full'.
1770
+ - `gap?: number` — Standoff gap in mm. Positive = gap between face and target. Negative = extra penetration. Default: 0.
1771
+
1564
1772
  #### `seatOver()` — Slide this shape until a target's labeled face is fully covered (inside this shape).
1565
1773
 
1566
1774
  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 +1793,10 @@ seatOver(target: Shape, targetSurface: string, options?: SeatIntoOptions): Shape
1585
1793
  withConnectors(connectors: Record<string, ConnectorInput>): Shape
1586
1794
  ```
1587
1795
 
1796
+ **`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`
1797
+
1798
+ `ConnectorInput`: `{ connectorType?: string, gender?: ConnectorGender, measurements?: Record<string, number | string> }`
1799
+
1588
1800
  #### `connectorNames()` — List all connector names on this shape.
1589
1801
 
1590
1802
  ```ts
@@ -1621,6 +1833,8 @@ Overloads:
1621
1833
  matchTo(targetOrPairs: Shape | MatchTarget | Array<[ Shape | MatchTarget, string, string ]>, selfConnOrDict?: string | Record<string, string>, targetConnOrOptions?: string | MatchToOptions, maybeOptions?: MatchToOptions): Shape
1622
1834
  ```
1623
1835
 
1836
+ `MatchToOptions`: `{ force?: boolean, angle?: number, distance?: number }`
1837
+
1624
1838
  **References**
1625
1839
 
1626
1840
  #### `withReferences()` — Attach named placement references that survive normal transforms and imports.
@@ -1629,6 +1843,12 @@ matchTo(targetOrPairs: Shape | MatchTarget | Array<[ Shape | MatchTarget, string
1629
1843
  withReferences(refs: PlacementReferenceInput): Shape
1630
1844
  ```
1631
1845
 
1846
+ **`PlacementReferenceInput`**: `points?: Record<string, [ number, number, number ]>`, `edges?: Record<string, PlacementEdgeRef>`, `surfaces?: Record<string, PlacementSurfaceRef>`, `objects?: Record<string, PlacementObjectInput>`
1847
+
1848
+ `PlacementEdgeRef`: `{ start: Vec3, end: Vec3 }`
1849
+
1850
+ `PlacementSurfaceRef`: `{ center: Vec3, normal: Vec3 }`
1851
+
1632
1852
  #### `referenceNames()` — List named placement references carried by this shape.
1633
1853
 
1634
1854
  ```ts
@@ -1790,6 +2010,24 @@ translate(x: number, y: number, z: number): Transform
1790
2010
  rotateAxis(axis: Vec3, angleDeg: number, pivot?: Vec3): Transform
1791
2011
  ```
1792
2012
 
2013
+ #### `rotateX()` — Rotate about the X axis after the current transform (parity with `Shape.rotateX`).
2014
+
2015
+ ```ts
2016
+ rotateX(angleDeg: number, pivot?: Vec3): Transform
2017
+ ```
2018
+
2019
+ #### `rotateY()` — Rotate about the Y axis after the current transform (parity with `Shape.rotateY`).
2020
+
2021
+ ```ts
2022
+ rotateY(angleDeg: number, pivot?: Vec3): Transform
2023
+ ```
2024
+
2025
+ #### `rotateZ()` — Rotate about the Z axis after the current transform (parity with `Shape.rotateZ`).
2026
+
2027
+ ```ts
2028
+ rotateZ(angleDeg: number, pivot?: Vec3): Transform
2029
+ ```
2030
+
1793
2031
  #### `inverse()` — Return the inverse transform.
1794
2032
 
1795
2033
  ```ts
@@ -2133,18 +2371,160 @@ constant(value?: number): Pattern2D
2133
2371
  sineWave(options: Pattern2DSineWaveOptions): Pattern2D
2134
2372
  ```
2135
2373
 
2374
+ **`Pattern2DSineWaveOptions`**
2375
+
2376
+ | Option | Type | Description |
2377
+ |--------|------|-------------|
2378
+ | `direction?` | `Vec2` | Direction the wave advances in UV space. Default: [1, 0]. |
2379
+ | `wavelength` | `number` | Distance between wave peaks in surface millimeters. |
2380
+ | `amplitude?` | `number` | Height amplitude in millimeters. Default: 1. |
2381
+ | `phase?` | `number` | Phase offset in radians. Default: 0. |
2382
+ | `bias?` | `number` | Constant height offset in millimeters. Default: 0. |
2383
+
2136
2384
  #### `stripes()` — Create recessed stripe bands in UV space.
2137
2385
 
2138
2386
  ```ts
2139
2387
  stripes(options: Pattern2DStripesOptions): Pattern2D
2140
2388
  ```
2141
2389
 
2390
+ **`Pattern2DStripesOptions`**
2391
+
2392
+ | Option | Type | Description |
2393
+ |--------|------|-------------|
2394
+ | `direction?` | `Vec2` | Direction perpendicular to the stripe bands in UV space. Default: [1, 0]. |
2395
+ | `spacing` | `number` | Center-to-center spacing in surface millimeters. |
2396
+ | `width` | `number` | Stripe width in surface millimeters. |
2397
+ | `depth?` | `number` | Stripe groove depth in millimeters. Default: 1. |
2398
+
2142
2399
  #### `overUnderWeave()` — Create an over-under woven relief pattern in UV space.
2143
2400
 
2144
2401
  ```ts
2145
2402
  overUnderWeave(options: Pattern2DOverUnderWeaveOptions): Pattern2D
2146
2403
  ```
2147
2404
 
2405
+ **`Pattern2DOverUnderWeaveOptions`**
2406
+
2407
+ | Option | Type | Description |
2408
+ |--------|------|-------------|
2409
+ | `spacing` | `number \| Vec2` | Thread center-to-center spacing. A number uses the same spacing for U and V. |
2410
+ | `threadWidth` | `number \| Vec2` | Thread width. A number uses the same width for U and V. |
2411
+ | `depth?` | `number` | Thread groove depth in millimeters. Default: 0.8. |
2412
+ | `underScale?` | `number` | Relative height of the under-crossing thread. Default: 0.15. |
2413
+
2414
+ ### `HermiteCurve3D`
2415
+
2416
+ **Properties:**
2417
+
2418
+ | Property | Type | Description |
2419
+ |----------|------|-------------|
2420
+ | `p0` | `Vec3` | Start position |
2421
+ | `p1` | `Vec3` | End position |
2422
+ | `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |
2423
+ | `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |
2424
+ | `chordLength` | `number` | Chord length (straight-line distance between endpoints) |
2425
+
2426
+ **Methods:**
2427
+
2428
+ #### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]
2429
+
2430
+ ```ts
2431
+ pointAt(t: number): Vec3
2432
+ ```
2433
+
2434
+ #### `tangentAt()` — Evaluate tangent (first derivative) at parameter t ∈ [0, 1]
2435
+
2436
+ ```ts
2437
+ tangentAt(t: number): Vec3
2438
+ ```
2439
+
2440
+ #### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]
2441
+
2442
+ ```ts
2443
+ curvatureAt(t: number): Vec3
2444
+ ```
2445
+
2446
+ #### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.
2447
+
2448
+ ```ts
2449
+ sample(count?: number): Vec3[]
2450
+ ```
2451
+
2452
+ #### `length()` — Approximate arc length by sampling.
2453
+
2454
+ ```ts
2455
+ length(samples?: number): number
2456
+ ```
2457
+
2458
+ #### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.
2459
+
2460
+ ```ts
2461
+ sampleAdaptive(minCount?: number, maxCount?: number): Vec3[]
2462
+ ```
2463
+
2464
+ #### `toPolyline()` — Convert to a format compatible with sweep() path input.
2465
+
2466
+ ```ts
2467
+ toPolyline(samples?: number): Vec3[]
2468
+ ```
2469
+
2470
+ ### `QuinticHermiteCurve3D`
2471
+
2472
+ **Properties:**
2473
+
2474
+ | Property | Type | Description |
2475
+ |----------|------|-------------|
2476
+ | `p0` | `Vec3` | Start position |
2477
+ | `p1` | `Vec3` | End position |
2478
+ | `t0` | `Vec3` | Scaled tangent at start (direction * weight * chordLength) |
2479
+ | `t1` | `Vec3` | Scaled tangent at end (direction * weight * chordLength) |
2480
+ | `c0` | `Vec3` | Scaled second derivative at start (curvature * weight² * chordLength²) |
2481
+ | `c1` | `Vec3` | Scaled second derivative at end (curvature * weight² * chordLength²) |
2482
+ | `chordLength` | `number` | Chord length (straight-line distance between endpoints) |
2483
+
2484
+ **Methods:**
2485
+
2486
+ #### `pointAt()` — Evaluate position at parameter t ∈ [0, 1]
2487
+
2488
+ ```ts
2489
+ pointAt(t: number): Vec3
2490
+ ```
2491
+
2492
+ #### `tangentAt()` — Evaluate tangent (first derivative, normalized) at parameter t ∈ [0, 1]
2493
+
2494
+ ```ts
2495
+ tangentAt(t: number): Vec3
2496
+ ```
2497
+
2498
+ #### `curvatureAt()` — Evaluate curvature vector (second derivative) at parameter t ∈ [0, 1]
2499
+
2500
+ ```ts
2501
+ curvatureAt(t: number): Vec3
2502
+ ```
2503
+
2504
+ #### `sample()` — Sample the curve as a polyline of evenly-spaced parameter values.
2505
+
2506
+ ```ts
2507
+ sample(count?: number): Vec3[]
2508
+ ```
2509
+
2510
+ #### `length()` — Approximate arc length by sampling.
2511
+
2512
+ ```ts
2513
+ length(samples?: number): number
2514
+ ```
2515
+
2516
+ #### `sampleAdaptive()` — Sample with adaptive density — more points where curvature is higher. Returns at least `minCount` points, up to `maxCount`.
2517
+
2518
+ ```ts
2519
+ sampleAdaptive(minCount?: number, maxCount?: number): Vec3[]
2520
+ ```
2521
+
2522
+ #### `toPolyline()` — Convert to a format compatible with sweep() path input.
2523
+
2524
+ ```ts
2525
+ toPolyline(samples?: number): Vec3[]
2526
+ ```
2527
+
2148
2528
  ### `ShapeRef`
2149
2529
 
2150
2530
  A first-class reference path over a shape's semantic faces and face relationships.
@@ -2337,39 +2717,43 @@ Namespaced file import helpers for formats that should not add new lowercase glo
2337
2717
 
2338
2718
  ForgeCAD uses a **Z-up** right-handed coordinate system.
2339
2719
 
2720
+ 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]`.
2721
+
2340
2722
  ## Axes
2341
2723
 
2342
2724
  | Axis | Direction | Positive |
2343
2725
  |------|-----------------|----------|
2344
2726
  | X | Left / Right | Right |
2345
- | Y | Forward / Back | Forward |
2727
+ | Y | Front / Back | Back |
2346
2728
  | Z | Up / Down | Up |
2347
2729
 
2348
2730
  ## Standard Views
2349
2731
 
2732
+ 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.
2733
+
2350
2734
  | View | Camera position direction | Sees plane |
2351
- |--------|--------------------------|------------|
2352
- | Front | Y | XZ |
2353
- | Back | +Y | XZ |
2735
+ |--------|---------------------------|------------|
2736
+ | Front | -Y | XZ |
2737
+ | Back | +Y | XZ |
2354
2738
  | Right | +X | YZ |
2355
- | Left | X | YZ |
2739
+ | Left | -X | YZ |
2356
2740
  | Top | +Z | XY |
2357
- | Bottom | Z | XY |
2741
+ | Bottom | -Z | XY |
2358
2742
 
2359
2743
  ## GizmoViewcube Face Mapping
2360
2744
 
2361
- Three.js BoxGeometry material indices vs ForgeCAD labels (Z-up remapping):
2745
+ Renderer/view-cube internals may have their own material ordering. Map any view-cube labels to the ForgeCAD directions below:
2362
2746
 
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 |
2747
+ | Direction | ForgeCAD label |
2748
+ |-----------|----------------|
2749
+ | +X | Right |
2750
+ | -X | Left |
2751
+ | +Y | Back |
2752
+ | -Y | Front |
2753
+ | +Z | Top |
2754
+ | -Z | Bottom |
2371
2755
 
2372
- Default drei labels are Y-up; ForgeCAD passes `faces={['Right','Left','Front','Back','Top','Bottom']}`.
2756
+ 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
2757
 
2374
2758
  ## Grid
2375
2759
 
@@ -2397,7 +2781,7 @@ Three.js is Y-up; ForgeCAD is Z-up. Fix applied at camera level (`camera.up = (0
2397
2781
 
2398
2782
  ## Revolution Axis
2399
2783
 
2400
- `CrossSection.revolve()` revolves around Y. Profile X = radial distance, Profile Y = height (becomes Z after revolution). Profile must be at X > 0.
2784
+ `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
2785
 
2402
2786
  ## Boolean Winding (3D)
2403
2787
 
@@ -2423,7 +2807,7 @@ Prefer `composeChain(...)` over manual `.mul(...).mul(...)` in kinematics code t
2423
2807
  |---|---|---|---|
2424
2808
  | Winding | Any point order | CCW | `polygon()`, `path().close()` |
2425
2809
  | 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 |
2810
+ | Revolution | "revolve this profile" | Profile X = radius, Profile Y = world Z height | Documented and regression-tested |
2427
2811
  | Face normals | Doesn't think about it | Outward-pointing | Manifold constructors |
2428
2812
  | Transform order | Left-to-right chain | Post-multiply | Native match |
2429
2813
 
@@ -2439,6 +2823,16 @@ For any fixed assembly where parts are meant to stay in contact in the final mod
2439
2823
 
2440
2824
  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
2825
 
2826
+ ## Mechanisms: connector frames vs link points
2827
+
2828
+ 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.
2829
+
2830
+ 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.
2831
+
2832
+ 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.
2833
+
2834
+ 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.
2835
+
2442
2836
  ## Primitive origin convention
2443
2837
 
2444
2838
  All 3D primitives are **centered on XY, base at Z=0**:
@@ -2462,14 +2856,14 @@ Most positioning bugs come from manual coordinate arithmetic. Use these methods
2462
2856
 
2463
2857
  ## 1. Connectors + `matchTo()` — default for mating interfaces
2464
2858
 
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).
2859
+ 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
2860
 
2467
2861
  ```javascript
2468
2862
  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] }),
2863
+ left_tab: connector.male("dovetail", { origin: [-100, 0, 0], axis: [-1, 0, 0], up: [0, 0, 1] }),
2470
2864
  });
2471
2865
  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] }),
2866
+ shelf_0: connector.female("dovetail", { origin: [6, 0, -50], axis: [1, 0, 0], up: [0, 0, 1] }),
2473
2867
  });
2474
2868
  const placed = shelf.matchTo(panel, "left_tab", "shelf_0");
2475
2869
  // Dictionary form for multiple pairs on same target:
@@ -3089,7 +3483,6 @@ addRegularPolygon(sk: ConstrainedSketchBuilder, options: RegularPolygonOptions):
3089
3483
  | `startAngle?` | `number` | Angle (in degrees) of vertex[0] measured from the +X axis (CCW positive). Default: 0 (rightmost vertex). |
3090
3484
  | `blockRotation?` | `boolean` | Prevent 180° rotation (ensures first edge maintains its initial direction). Default: false. |
3091
3485
 
3092
-
3093
3486
  **`ConstrainedRegularPolygon`** extends ConstrainedPolygon
3094
3487
  - `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
3488
 
@@ -3329,7 +3722,7 @@ region(seed: [ number, number ]): Sketch
3329
3722
  extrude(height: number, opts?: { twist?: number; divisions?: number; scaleTop?: number | [ number, number ]; }): Shape
3330
3723
  ```
3331
3724
 
3332
- #### `revolve()` — Revolve this 2D sketch around the Y axis to create a 3D solid of revolution.
3725
+ #### `revolve()` — Revolve this 2D sketch around the world Z axis. Sketch X is radius; sketch Y becomes world Z height.
3333
3726
 
3334
3727
  ```ts
3335
3728
  revolve(degrees?: number, segments?: number): Shape
@@ -3360,9 +3753,25 @@ Use this when a 2D profile should be oriented onto a 3D face before extrusion or
3360
3753
  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
3754
  ```
3362
3755
 
3363
- **Labels**
3756
+ **`FaceRef`**
3364
3757
 
3365
- #### `labelEdge()` Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch.
3758
+ | Option | Type | Description |
3759
+ |--------|------|-------------|
3760
+ | `normal` | `[ number, number, number ]` | Normal direction of the face |
3761
+ | `center` | `[ number, number, number ]` | Center point of the face |
3762
+ | `query?` | `FaceQueryRef` | Compiler-owned face query when available. |
3763
+ | `planar?` | `boolean` | True when the face can host a 2D sketch placement frame |
3764
+ | `uAxis?` | `[ number, number, number ]` | Face-local horizontal axis for planar faces |
3765
+ | `vAxis?` | `[ number, number, number ]` | Face-local vertical axis for planar faces |
3766
+ | `surface?` | `FaceSurface` | Analytic surface family when the backend can identify one. |
3767
+ | `descendant?` | `FaceDescendantMetadata` | Shared descendant-resolution metadata when this face is a semantic region/set. |
3768
+ | `name` | | — |
3769
+
3770
+ **`FaceDescendantMetadata`**: `kind: "single" | "face-set"`, `semantic: FaceDescendantSemantic`, `memberCount: number`, `memberNames: string[]`, `coplanar: boolean`
3771
+
3772
+ **Labels**
3773
+
3774
+ #### `labelEdge()` — Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch.
3366
3775
 
3367
3776
  ```ts
3368
3777
  labelEdge(name: string): Sketch
@@ -3658,6 +4067,16 @@ regularPolygon(options: RegularPolygonOptions): ConstrainedRegularPolygon
3658
4067
  groupRect(options: GroupRectOptions): ConstrainedGroupRect
3659
4068
  ```
3660
4069
 
4070
+ **`GroupRectOptions`**
4071
+
4072
+ | Option | Type | Description |
4073
+ |--------|------|-------------|
4074
+ | `x?` | `number` | Bottom-left x coordinate (world). Default: 0. |
4075
+ | `y?` | `number` | Bottom-left y coordinate (world). Default: 0. |
4076
+ | `width` | `number` | Width (along x in local coords). Required. |
4077
+ | `height` | `number` | Height (along y in local coords). Required. |
4078
+ | `allowRotation?` | `boolean` | Allow the solver to rotate this rectangle. Default: false. |
4079
+
3661
4080
  **Geometric Constraints**
3662
4081
 
3663
4082
  #### `horizontal()` — Constrain a line to be horizontal (parallel to the X axis).
@@ -4001,6 +4420,23 @@ result.withUpdatedConstraint('cst-5', 120); // update a dimension without rebuil
4001
4420
  solve(options?: SolveOptions): ConstraintSketch | Sketch
4002
4421
  ```
4003
4422
 
4423
+ **`SolveOptions`**
4424
+
4425
+ | Option | Type | Description |
4426
+ |--------|------|-------------|
4427
+ | `iterations?` | `number` | Maximum number of LM outer iterations per restart. |
4428
+ | `tolerance?` | `number` | Infinity-norm residual tolerance for declaring convergence. |
4429
+ | `restarts?` | `number` | Number of deterministic restart seeds used by the global solver. |
4430
+ | `warmStartIterations?` | `number` | Optional projector iterations used only for initialisation, not as the main solver. |
4431
+ | `maxScaledStep?` | `number` | Maximum LM step length in scaled variable space. Larger = bolder, smaller = safer. |
4432
+ | `skipRedundancyCheck?` | `boolean` | Skip redundancy detection (safe when topology is unchanged and previous DOF >= 0). |
4433
+ | `presolveConstraintId?` | `string` | Run the targeted presolve hook for this constraint before the main solve. |
4434
+ | `fallbackRestarts?` | `number` | When set and the first solve exceeds tolerance*5, retry with this many restarts. |
4435
+ | `progressive?` | `boolean` | Add constraints progressively with short LM solves, all in one WASM call. |
4436
+ | `timeBudgetMs?` | `number` | Wall-clock time budget in ms for the entire solve. 0 = no limit. |
4437
+ | `debugConstructiveTranscript?` | `boolean` | Capture a readable constructive transcript in `constraintMeta.debug`. |
4438
+ | `debugSvgSnapshots?` | `boolean` | Capture SVG snapshots for constructive steps in `constraintMeta.debug`. |
4439
+
4004
4440
  #### `solveConstraintsOnly()` — Run the solver without building a full `ConstraintSketch`.
4005
4441
 
4006
4442
  Lighter than `solve()` — skips profile and DOF analysis. Useful for lightweight constraint validation or progress monitoring mid-construction.
@@ -4409,15 +4845,13 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
4409
4845
 
4410
4846
  ## Contents
4411
4847
 
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`
4848
+ - [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
4849
  - [Surface Members](#surface-members) — `surfaceBand`, `SurfaceBody`
4414
4850
  - [Curve3D](#curve3d)
4415
- - [HelixCurve](#helixcurve)
4851
+ - [Route3D](#route3d)
4416
4852
  - [NurbsCurve3D](#nurbscurve3d)
4417
4853
  - [NurbsSurface](#nurbssurface)
4418
4854
  - [PathBuilder](#pathbuilder) — Line Segments, Arcs, Curves, Closing & Output
4419
- - [HermiteCurve3D](#hermitecurve3d)
4420
- - [QuinticHermiteCurve3D](#quintichermitecurve3d)
4421
4855
  - [ProductSkin](#productskin)
4422
4856
  - [ProductSurfaceRef](#productsurfaceref)
4423
4857
  - [ProductSurfaceBuilder](#productsurfacebuilder)
@@ -4439,6 +4873,7 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
4439
4873
  - [SurfaceJoinBuilder](#surfacejoinbuilder)
4440
4874
  - [CounterboreBuilder](#counterborebuilder)
4441
4875
  - [RoundedSlotBuilder](#roundedslotbuilder)
4876
+ - [Curve](#curve)
4442
4877
  - [Surface](#surface)
4443
4878
  - [Blend](#blend)
4444
4879
  - [Analysis](#analysis)
@@ -4448,97 +4883,186 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
4448
4883
  - [Slot](#slot)
4449
4884
  - [Counterbore](#counterbore)
4450
4885
  - [Ribs](#ribs)
4451
- - [Helix](#helix)
4452
4886
 
4453
4887
  ## Functions
4454
4888
 
4455
4889
  ### Curves & Surfacing
4456
4890
 
4457
- #### `Loft.station()` — Create a loft station from a 2D profile and an axis position.
4891
+ #### `Curve.Blend()` — Create an exact G1 blend curve between two directed endpoints.
4892
+
4893
+ 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.
4894
+
4895
+ ```js
4896
+ const rail = Curve.Blend(
4897
+ { point: [0, 0, 0], tangent: [1, 0, 0], weight: 0.8 },
4898
+ { point: [40, 20, 8], tangent: [0, 1, 0], weight: 0.8 },
4899
+ );
4900
+ const tube = sweep(circle2d(2), rail);
4901
+ ```
4458
4902
 
4459
4903
  ```ts
4460
- Loft.station(profile: Sketch, position: number): LoftStation
4904
+ Curve.Blend(start: CurveBlendEndpoint, end: CurveBlendEndpoint): NurbsCurve3D
4461
4905
  ```
4462
4906
 
4463
- `LoftStation`: `{ profile: Sketch, position: number }`
4907
+ **`CurveBlendEndpoint`**
4908
+ - `point: Vec3` — Endpoint position.
4909
+ - `tangent: Vec3` — Tangent direction at this endpoint. Magnitude is ignored.
4910
+ - `weight?: number` — Tangent reach relative to the endpoint chord length. Default 1.
4464
4911
 
4465
- #### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.
4912
+ #### `Curve.BlendG2()` — Create an exact G2 blend curve between two directed endpoints.
4913
+
4914
+ 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.
4915
+
4916
+ ```js
4917
+ const rail = Curve.BlendG2(
4918
+ { point: [0, 0, 0], tangent: [1, 0, 0], curvature: [0, 0.02, 0] },
4919
+ { point: [50, 20, 0], tangent: [0, 1, 0], curvature: [-0.02, 0, 0] },
4920
+ );
4921
+ ```
4466
4922
 
4467
4923
  ```ts
4468
- Loft.leftRail(path: LoftGuideRailPath): LoftGuideRail
4924
+ Curve.BlendG2(start: CurveBlendG2Endpoint, end: CurveBlendG2Endpoint): NurbsCurve3D
4469
4925
  ```
4470
4926
 
4471
- `LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`
4927
+ **`CurveBlendG2Endpoint`** extends CurveBlendEndpoint
4928
+ - `curvature?: Vec3` — Optional endpoint curvature/second-derivative vector. Default is zero.
4472
4929
 
4473
- #### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.
4930
+ #### `Curve.Arc()` — Create an exact circular 3D arc from start, end, and start tangent.
4931
+
4932
+ 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.
4933
+
4934
+ ```js
4935
+ const rail = Curve.Arc({
4936
+ start: [40, 0, 0],
4937
+ end: [0, 40, 0],
4938
+ tangent: [0, 1, 0],
4939
+ });
4940
+ const tube = sweep(circle2d(2), rail);
4941
+ ```
4474
4942
 
4475
4943
  ```ts
4476
- Loft.rightRail(path: LoftGuideRailPath): LoftGuideRail
4944
+ Curve.Arc(options: CurveArcOptions): NurbsCurve3D
4477
4945
  ```
4478
4946
 
4479
- #### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.
4947
+ **`CurveArcOptions`**
4948
+ - `start: Vec3` — Arc start point.
4949
+ - `end: Vec3` — Arc end point.
4950
+ - `tangent: Vec3` — Tangent direction at the start point. Magnitude is ignored.
4951
+
4952
+ #### `Curve.Line()` — Create an exact straight 3D NURBS line segment.
4953
+
4954
+ ```js
4955
+ const rail = Curve.Line([0, 0, 0], [80, 0, 15]);
4956
+ const rib = sweep(circle2d(2), rail);
4957
+ ```
4480
4958
 
4481
4959
  ```ts
4482
- Loft.frontRail(path: LoftGuideRailPath): LoftGuideRail
4960
+ Curve.Line(start: Vec3, end: Vec3): NurbsCurve3D
4483
4961
  ```
4484
4962
 
4485
- #### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.
4963
+ #### `Curve.Polyline()` — Create a polyline path as cloned 3D points.
4964
+
4965
+ Polylines are exact as route/path input to `sweep`. Use `Curve.Route` when the centerline needs bend and endpoint-frame metadata.
4486
4966
 
4487
4967
  ```ts
4488
- Loft.backRail(path: LoftGuideRailPath): LoftGuideRail
4968
+ Curve.Polyline(points: Vec3[]): Vec3[]
4489
4969
  ```
4490
4970
 
4491
- #### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.
4971
+ #### `Curve.Spline()` — Create a smooth Catmull-Rom spline path.
4972
+
4973
+ This is a smooth sampled curve object. Use `Curve.Nurbs` when the path must preserve exact control-point and knot data.
4492
4974
 
4493
4975
  ```ts
4494
- Loft.centerRail(path: LoftGuideRailPath): LoftGuideRail
4976
+ Curve.Spline(points: Vec3[], options?: Spline3DOptions): Curve3D
4495
4977
  ```
4496
4978
 
4497
- #### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.
4979
+ **`Spline3DOptions`**
4980
+ - `closed?: boolean` — Closed loop (default false).
4981
+ - `tension?: number` — Catmull-Rom tension in [0, 1]. 0 = very round, 1 = linear-ish. Default 0.5.
4498
4982
 
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).
4983
+ #### `Curve.Nurbs()` Create an exact NURBS 3D curve from control points, weights, knots, and degree.
4984
+
4985
+ ```js
4986
+ const rail = Curve.Nurbs([[0, 0, 0], [30, 4, 12], [60, -4, 12], [90, 0, 0]]);
4987
+ const tube = sweep(circle2d(2), rail);
4988
+ ```
4500
4989
 
4501
4990
  ```ts
4502
- Loft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]
4991
+ Curve.Nurbs(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D
4503
4992
  ```
4504
4993
 
4505
- #### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.
4994
+ **`NurbsCurve3DOptions`**
4506
4995
 
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).
4996
+ | Option | Type | Description |
4997
+ |--------|------|-------------|
4998
+ | `degree?` | `number` | Polynomial degree (default 3 = cubic). Must be ≥ 1. |
4999
+ | `weights?` | `number[]` | Rational weights, one per control point (default: all 1.0 = non-rational). |
5000
+ | `knots?` | `number[]` | Knot vector (default: uniform clamped). Must have length = controlPoints.length + degree + 1. |
5001
+ | `closed?` | `boolean` | Whether the curve is closed/periodic (default false). |
5002
+
5003
+ #### `Curve.Fit()` — Fit a non-rational NURBS curve that interpolates every input point.
5004
+
5005
+ 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`.
5006
+
5007
+ ```js
5008
+ const rail = Curve.Fit(
5009
+ [[0, 0, 0], [20, 8, 12], [50, -4, 18], [80, 0, 0]],
5010
+ { degree: 3, tolerance: 0.001 },
5011
+ );
5012
+ const tube = sweep(circle2d(2), rail);
5013
+ ```
4508
5014
 
4509
5015
  ```ts
4510
- Loft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]
5016
+ Curve.Fit(points: Vec3[], options?: CurveFitOptions): NurbsCurve3D
4511
5017
  ```
4512
5018
 
4513
- #### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.
5019
+ **`CurveFitOptions`**
5020
+ - `degree?: number` — Polynomial degree. Default is cubic, reduced automatically for short point lists.
5021
+ - `tolerance?: number` — Maximum allowed interpolation residual in model units. Default 1e-7.
4514
5022
 
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.
5023
+ #### `Curve.Trim()` Extract an exact curve segment from normalized parameter `start` to `end`.
5024
+
5025
+ `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
5026
 
4517
5027
  ```ts
4518
- Loft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]
5028
+ Curve.Trim<T extends CurveTrimInput>(curve: T, start: number, end: number): CurveTrimOutput<T>
4519
5029
  ```
4520
5030
 
4521
- #### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.
5031
+ #### `Curve.Reverse()` — Reverse an exact curve without changing its geometry.
4522
5032
 
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.
5033
+ `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
5034
 
4525
5035
  ```ts
4526
- Loft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape
5036
+ Curve.Reverse<T extends CurveTrimInput>(curve: T): CurveTrimOutput<T>
4527
5037
  ```
4528
5038
 
4529
- **`LoftOptions`**
4530
- - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
4531
- - `boundsPadding?: number` — Optional extra bounds padding.
5039
+ #### `Curve.Route()` — Build analytic 3D line/arc routes for sweeps.
4532
5040
 
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.
5041
+ `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.
5042
+
5043
+ ```js
5044
+ const route = Curve.Route.fromPolyline(
5045
+ [[0, 0, 0], [0, 0, 50], [40, 0, 50]],
5046
+ { cornerRadius: 12, startPort: 'inlet', endPort: 'outlet' },
5047
+ );
5048
+ const tube = sweep(circle2d(4), route);
5049
+ ```
5050
+
5051
+ ```ts
5052
+ Curve.Route: typeof Route3D
5053
+ ```
4537
5054
 
4538
- #### `Helix.path()` — Create a metadata-bearing helical centerline around the Z axis.
5055
+ #### `Curve.Helix()` — Build helical paths and swept coils.
5056
+
5057
+ `Curve.Helix` is the canonical namespace for helical paths and coils. It uses the same sweep-based lowering as other curve paths.
5058
+
5059
+ ```js
5060
+ const guide = Curve.Helix.path({ radius: 20, pitch: 6, turns: 4 });
5061
+ const spring = Curve.Helix.coil({ radius: 20, pitch: 6, turns: 4, wireRadius: 1 });
5062
+ ```
4539
5063
 
4540
5064
  ```ts
4541
- Helix.path(options: HelixOptions): HelixCurve
5065
+ Curve.Helix: { path(options: HelixOptions): CurveHelixPath; coil: CurveHelixCoil; }
4542
5066
  ```
4543
5067
 
4544
5068
  **`HelixOptions`**
@@ -4553,66 +5077,88 @@ Helix.path(options: HelixOptions): HelixCurve
4553
5077
  | `clockwise?` | `boolean` | Reverse winding direction when viewed from +Z. |
4554
5078
  | `samplesPerTurn?` | `number` | Point samples per turn for the metadata path. Default 32. |
4555
5079
 
4556
- #### `Helix.coil()` Create a solid helical coil by sweeping a profile through helix-local frames.
5080
+ `CurveHelixPath`: `{ radius: number, pitch: number, turns: number, height: number, startAngle: number, clockwise: boolean }`
4557
5081
 
4558
- Overloads:
5082
+ #### `Loft.station()` — Create a loft station from a 2D profile and an axis position.
5083
+
5084
+ ```ts
5085
+ Loft.station(profile: Sketch, position: number): LoftStation
5086
+ ```
4559
5087
 
4560
- - `Helix.coil(options: HelixCoilOptions): Shape`
4561
- - `Helix.coil(profile: Sketch, options: HelixCoilOptions): Shape`
5088
+ `LoftStation`: `{ profile: Sketch, position: number }`
4562
5089
 
5090
+ #### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.
4563
5091
 
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.
5092
+ ```ts
5093
+ Loft.leftRail(path: LoftGuideRailPath): LoftGuideRail
5094
+ ```
4568
5095
 
4569
- #### `hermiteTransitionG2()` Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).
5096
+ `LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`
4570
5097
 
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.
5098
+ #### `Loft.rightRail()` Create a guide rail that constrains the section-local positive-X side.
4572
5099
 
4573
5100
  ```ts
4574
- hermiteTransitionG2(a: QuinticHermiteCurveEndpoint, b: QuinticHermiteCurveEndpoint): QuinticHermiteCurve3D
5101
+ Loft.rightRail(path: LoftGuideRailPath): LoftGuideRail
4575
5102
  ```
4576
5103
 
4577
- **`QuinticHermiteCurveEndpoint`**
5104
+ #### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.
4578
5105
 
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. |
5106
+ ```ts
5107
+ Loft.frontRail(path: LoftGuideRailPath): LoftGuideRail
5108
+ ```
4585
5109
 
4586
- #### `nurbs3d()` — Create a NURBS curve from control points.
5110
+ #### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.
5111
+
5112
+ ```ts
5113
+ Loft.backRail(path: LoftGuideRailPath): LoftGuideRail
5114
+ ```
4587
5115
 
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.
5116
+ #### `Loft.centerRail()` Create a guide rail that moves section centers along the loft.
4589
5117
 
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);
5118
+ ```ts
5119
+ Loft.centerRail(path: LoftGuideRailPath): LoftGuideRail
4594
5120
  ```
4595
5121
 
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
- );
5122
+ #### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.
5123
+
5124
+ 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).
5125
+
5126
+ ```ts
5127
+ Loft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]
4602
5128
  ```
4603
5129
 
5130
+ #### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.
5131
+
5132
+ 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).
5133
+
4604
5134
  ```ts
4605
- nurbs3d(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D
5135
+ Loft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]
4606
5136
  ```
4607
5137
 
4608
- **`NurbsCurve3DOptions`**
5138
+ #### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.
4609
5139
 
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). |
5140
+ 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.
5141
+
5142
+ ```ts
5143
+ Loft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]
5144
+ ```
5145
+
5146
+ #### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.
5147
+
5148
+ 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.
5149
+
5150
+ ```ts
5151
+ Loft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape
5152
+ ```
5153
+
5154
+ **`LoftOptions`**
5155
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
5156
+ - `boundsPadding?: number` — Optional extra bounds padding.
5157
+
5158
+ **`LoftWithGuideRailsOptions`** extends LoftOptions
5159
+ - `axis?: LoftAxis` — Primary station axis. Default Z.
5160
+ - `samples?: number` — Number of generated loft stations including ends. Default scales with station count.
5161
+ - `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.
4616
5162
 
4617
5163
  #### `spline2d()` — Build a smooth Catmull-Rom spline sketch from 2D control points.
4618
5164
 
@@ -4632,18 +5178,6 @@ spline2d(points: Vec2[], options?: Spline2DOptions): Sketch
4632
5178
  | `strokeWidth?` | `number` | For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. |
4633
5179
  | `join?` | `"Round" \| "Square"` | Stroke join for open splines. Default 'Round'. |
4634
5180
 
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
5181
  #### `loft()` — Loft between multiple sketches along Z stations.
4648
5182
 
4649
5183
  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 +5188,6 @@ Performance note: loft is significantly heavier than primitive/extrude/revolve.
4654
5188
  loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
4655
5189
  ```
4656
5190
 
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
5191
  #### `sweep()`
4681
5192
 
4682
5193
  ```ts
@@ -4800,106 +5311,6 @@ surfacePatch(curves: { ... }, options?: SurfacePatchOptions): Shape
4800
5311
  - `thickness?: number` — Thickness of the generated solid. Default 0 for an open exact sheet.
4801
5312
  - `approximate?: boolean` — Allow explicit approximation for non-exact curve inputs such as Curve3D samples.
4802
5313
 
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
5314
  ### Surface Members
4904
5315
 
4905
5316
  #### `surfaceBand()`
@@ -4979,47 +5390,76 @@ tangentAt(t: number): Vec3
4979
5390
  length(samples?: number): number
4980
5391
  ```
4981
5392
 
4982
- ### `HelixCurve`
5393
+ ### `Route3D`
4983
5394
 
4984
- Metadata-bearing helical curve around the Z axis. Use `Helix.path(...)` for sampling, placement, or `sweep()`, and `Helix.coil(...)` for helix-oriented solids.
5395
+ Metadata-bearing analytic 3D route made from line and arc segments.
4985
5396
 
4986
- **Properties:**
5397
+ 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
5398
 
4988
- | Property | Type | Description |
4989
- |----------|------|-------------|
4990
- | `radius` | `number` | |
4991
- | `pitch` | `number` | |
4992
- | `turns` | `number` | — |
4993
- | `height` | `number` | — |
4994
- | `startAngle` | `number` | — |
4995
- | `clockwise` | `boolean` | — |
5399
+ ```js
5400
+ const route = Curve.Route.fromPolyline(
5401
+ [[0, 0, 0], [0, 0, 80], [60, 0, 80]],
5402
+ { cornerRadius: 24, startPort: "inlet", endPort: "outlet" },
5403
+ );
5404
+ const pipe = sweep(difference2d(circle2d(8), circle2d(6)), route);
5405
+ const outlet = route.port("outlet");
5406
+ ```
4996
5407
 
4997
- **Methods:**
5408
+ #### `fromPolyline()` — Build a line/arc route from virtual polyline corner points.
4998
5409
 
4999
- #### `pointAt()`
5410
+ ```ts
5411
+ static fromPolyline(points: Route3DVec3[], options?: Route3DFromPolylineOptions): Route3D
5412
+ ```
5413
+
5414
+ **`Route3DFromPolylineOptions`**
5415
+
5416
+ | Option | Type | Description |
5417
+ |--------|------|-------------|
5418
+ | `cornerRadius?` | `number` | Bend radius applied to every virtual interior corner. Default 0 keeps sharp polyline corners. |
5419
+ | `startPort?` | `string` | Name for the start port. Default "start". |
5420
+ | `endPort?` | `string` | Name for the end port. Default "end". |
5421
+ | `up?` | `Vec3` | Preferred up vector for deterministic port frames. Default [0, 0, 1]. |
5422
+
5423
+ #### `length()` — Total centerline length, including line and bend arc segments.
5000
5424
 
5001
5425
  ```ts
5002
- pointAt(t: number): Vec3
5426
+ get length(): number
5003
5427
  ```
5004
5428
 
5005
- #### `tangentAt()`
5429
+ #### `segments()` — Exact line and arc segments that make up this route.
5006
5430
 
5007
5431
  ```ts
5008
- tangentAt(t: number): Vec3
5432
+ get segments(): Route3DSegment[]
5009
5433
  ```
5010
5434
 
5011
- #### `sample()`
5435
+ #### `ports()` — Named port frames, keyed by port name.
5012
5436
 
5013
5437
  ```ts
5014
- sample(count?: number): Vec3[]
5438
+ get ports(): Record<string, RoutePortFrame>
5015
5439
  ```
5016
5440
 
5017
- #### `length()`
5441
+ #### `port()` — Return one named route port frame.
5442
+
5443
+ ```ts
5444
+ port(name: string): RoutePortFrame
5445
+ ```
5446
+
5447
+ #### `toSweepPathPlan()` — Convert this route to the compile plan consumed by sweep().
5448
+
5449
+ ```ts
5450
+ toSweepPathPlan(): SweepPathCompilePlan
5451
+ ```
5452
+
5453
+ #### `toPolyline()` — Sample this analytic route as a polyline for inspection or backend lowering.
5018
5454
 
5019
5455
  ```ts
5020
- length(): number
5456
+ toPolyline(options?: number | Route3DToPolylineOptions): Route3DVec3[]
5021
5457
  ```
5022
5458
 
5459
+ **`Route3DToPolylineOptions`**
5460
+ - `samples?: number` — Approximate target point count for the full route.
5461
+ - `maxAngleDeg?: number` — Maximum angular spacing on arc segments. Default 6 degrees.
5462
+
5023
5463
  ### `NurbsCurve3D`
5024
5464
 
5025
5465
  **Properties:**
@@ -5393,120 +5833,6 @@ toPolyline(): [ number, number ][]
5393
5833
  closeOffset(delta: number, join?: "Round" | "Square" | "Miter"): Sketch
5394
5834
  ```
5395
5835
 
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
5836
  ### `ProductSkin`
5511
5837
 
5512
5838
  **Properties:**
@@ -5939,7 +6265,6 @@ attachTo(ref: ProductRefInput, options?: ProductPanelAttachOptions): Shape
5939
6265
 
5940
6266
  `ProductSurfaceRef`
5941
6267
 
5942
-
5943
6268
  `ProductPanelAttachOptions`: `{ at?: Partial<ProductSkinRefQuery>, thickness?: number, material?: ProductMaterial, color?: string }`
5944
6269
 
5945
6270
  ### `ProductRibbonBuilder`
@@ -6263,6 +6588,8 @@ bottom(options?: { angle?: number; offset?: number; }): SurfaceAnchor<CylinderSu
6263
6588
  pointAt(coordinate: CylinderSurfaceCoordinate): Vec3
6264
6589
  ```
6265
6590
 
6591
+ `CylinderSurfaceCoordinate`: `{ kind?: "cylinder", angle: number, z: number, offset?: number }`
6592
+
6266
6593
  #### `mirrorPoint()`
6267
6594
 
6268
6595
  ```ts
@@ -6382,6 +6709,8 @@ bottom(options?: { x?: number; offset?: number; }): SurfaceAnchor<PlaneSurfaceCo
6382
6709
  pointAt(coordinate: PlaneSurfaceCoordinate): Vec3
6383
6710
  ```
6384
6711
 
6712
+ `PlaneSurfaceCoordinate`: `{ kind?: "plane", x: number, y: number, offset?: number }`
6713
+
6385
6714
  #### `mirrorPoint()`
6386
6715
 
6387
6716
  ```ts
@@ -6666,6 +6995,8 @@ boundaries(samples?: number): SurfaceBandBoundarySample[]
6666
6995
  withHole(name: string, input: SurfaceBandHoleInput): SurfaceBand<C>
6667
6996
  ```
6668
6997
 
6998
+ `SurfaceBandHoleInput`: `{ length: number, width: number, along?: number, across?: number }`
6999
+
6669
7000
  #### `holes()` — Resolve recorded hole regions into member-local across/along loops.
6670
7001
 
6671
7002
  ```ts
@@ -6688,6 +7019,8 @@ holes(): SurfaceBandHoleRegion[]
6688
7019
  carrier(carrier: CarrierSurface): this
6689
7020
  ```
6690
7021
 
7022
+ `CarrierSurface`: `{ name: string, kind: SurfaceCarrierKind }`
7023
+
6691
7024
  #### `member()`
6692
7025
 
6693
7026
  ```ts
@@ -6732,6 +7065,8 @@ band(): this
6732
7065
  at(anchor: SurfaceAnchor<C>): this
6733
7066
  ```
6734
7067
 
7068
+ `SurfaceAnchor`: `{ carrier: CarrierSurface<C>, coordinate: C }`
7069
+
6735
7070
  #### `size()`
6736
7071
 
6737
7072
  ```ts
@@ -6750,6 +7085,10 @@ path(path: SurfacePath<C> | SurfacePathBuilder<C>): this
6750
7085
  section(section: MemberSectionInput): this
6751
7086
  ```
6752
7087
 
7088
+ **`MemberSectionInput`**: `width?: number`, `thickness: number`, `edgeRadius?: number`, `direction?: MemberOutwardDirection`, `material?: ProductMaterial`, `stations?: MemberSectionStation[]`
7089
+
7090
+ `MemberSectionStation`: `{ t: number, width?: number, thickness?: number }`
7091
+
6753
7092
  #### `cap()`
6754
7093
 
6755
7094
  ```ts
@@ -6762,6 +7101,8 @@ cap(style: SurfaceBandCap): this
6762
7101
  slot(name: string, feature: MemberFeature | RoundedSlotBuilder): this
6763
7102
  ```
6764
7103
 
7104
+ **`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`
7105
+
6765
7106
  #### `cutout()`
6766
7107
 
6767
7108
  ```ts
@@ -6886,6 +7227,25 @@ toFeature(name?: string): MemberFeature
6886
7227
 
6887
7228
  ## Constants
6888
7229
 
7230
+ ### `Curve`
7231
+
7232
+ Canonical exact/smooth 3D curve constructors.
7233
+
7234
+ `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.
7235
+
7236
+ - `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.
7237
+ - `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.
7238
+ - `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.
7239
+ - `Line(start: Vec3, end: Vec3): NurbsCurve3D` — Create an exact straight 3D NURBS line segment.
7240
+ - `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.
7241
+ - `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.
7242
+ - `Nurbs(points: Vec3[], options?: NurbsCurve3DOptions): NurbsCurve3D` — Create an exact NURBS 3D curve from control points, weights, knots, and degree.
7243
+ - `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`.
7244
+ - `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.
7245
+ - `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.
7246
+ - `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.
7247
+ - `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.
7248
+
6889
7249
  ### `Surface`
6890
7250
 
6891
7251
  - `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 +7328,18 @@ toFeature(name?: string): MemberFeature
6968
7328
 
6969
7329
  - `repeated(input: { count: number; height: number; }): MemberFeature` — Create repeated ribs that belong to a surface member before lowering.
6970
7330
 
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
7331
  ---
6986
7332
 
6987
7333
  <!-- generated/assembly.md -->
6988
7334
 
6989
7335
  # Assembly API
6990
7336
 
6991
- Kinematic assemblies, joints, couplings, and robot export.
7337
+ Assembly-owned links, constraints, connectors, solved poses, and robot export.
6992
7338
 
6993
7339
  ## Contents
6994
7340
 
6995
- - [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`, `joint`
6996
- - [Assembly](#assembly) — Structure, Connectors, References, Joints, Solving
7341
+ - [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`
7342
+ - [Assembly](#assembly) — Kinematics, Structure, Connectors, References, Solving
6997
7343
  - [ImportedAssembly](#importedassembly)
6998
7344
  - [SolvedAssembly](#solvedassembly)
6999
7345
  - [MateBuilder](#matebuilder)
@@ -7019,110 +7365,259 @@ bomToCsv(rows: BomRow[]): string
7019
7365
  | `tags?` | `string \| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |
7020
7366
  | `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |
7021
7367
 
7022
- #### `assembly()` — Create an assembly container with named parts and joints for kinematic mechanisms.
7368
+ #### `assembly()` — Create an assembly container with named parts, connectors, and kinematic links.
7369
+
7370
+ **Use this from iteration 1 for any model with moving parts.** Do not build one static pose and retrofit motion later.
7023
7371
 
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.
7372
+ 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.
7025
7373
 
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.
7374
+ `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.
7027
7375
 
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.
7376
+ 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.
7029
7377
 
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.
7378
+ If no link in a connected kinematic component is fixed, ForgeCAD chooses a deterministic gauge link for solving and reports a floating-component warning.
7379
+
7380
+ 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
7381
 
7032
7382
  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
7383
 
7384
+ **Point-link example**
7385
+
7386
+ This snippet mates a marker to the solved `tip` point. It does not orient a bar along `ground -> tip`.
7387
+
7034
7388
  ```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 },
7389
+ const marker = box(8, 8, 4).withConnectors({
7390
+ center: connector({ origin: [0, 0, 0], axis: [0, 0, 1] }),
7391
+ });
7392
+
7393
+ const mech = assembly("Linkage")
7394
+ .link("ground", { at: [0, 0, 0], fixed: true })
7395
+ .link("worldX", { at: [10, 0, 0], fixed: true })
7396
+ .link("tip", { at: [40, 0, 0] })
7397
+ .edgeBetweenLinks("ground", "tip", { name: "bar" })
7398
+ .addAngleBetweenLinks("worldX", "ground", "tip", {
7399
+ name: "theta",
7400
+ control: { min: 0, max: 120, default: 30 },
7038
7401
  })
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),
7402
+ .addPart("Tip marker", marker, { mate: { connector: "center", toLink: "tip" } });
7403
+
7404
+ return mech;
7405
+ ```
7406
+
7407
+ ```ts
7408
+ assembly(name?: string): Assembly
7409
+ ```
7410
+
7411
+ ---
7412
+
7413
+ ## Classes
7414
+
7415
+ ### `Assembly`
7416
+
7417
+ Container for a kinematic mechanism made up of links, relationships, and parts.
7418
+
7419
+ Assembly has two related but different motion tools:
7420
+
7421
+ - **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.
7422
+ - **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.
7423
+
7424
+ 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.
7425
+
7426
+ **Point-link quick start**
7427
+
7428
+ This attaches a marker to a solved point. It is intentionally not a bone or oriented part example.
7429
+
7430
+ ```ts
7431
+ const marker = box(8, 8, 4).withConnectors({
7432
+ center: connector({ origin: [0, 0, 0], axis: [0, 0, 1] }),
7433
+ });
7434
+
7435
+ const mech = assembly("Linkage")
7436
+ .link("ground", { at: [0, 0, 0], fixed: true })
7437
+ .link("worldX", { at: [10, 0, 0], fixed: true })
7438
+ .link("tip", { at: [40, 0, 0] })
7439
+ .edgeBetweenLinks("ground", "tip", { name: "bar" })
7440
+ .addAngleBetweenLinks("worldX", "ground", "tip", {
7441
+ name: "theta",
7442
+ control: { min: 0, max: 120, default: 30 },
7443
+ })
7444
+ .addPart("Tip marker", marker, {
7445
+ mate: { connector: "center", toLink: "tip" },
7044
7446
  });
7045
7447
 
7046
- return mech; // auto-solved at defaults, renders all parts
7448
+ return mech;
7449
+ ```
7450
+
7451
+ Returning an unsolved `Assembly` keeps the graph available to the runtime. Return a `SolvedAssembly` directly for a specific control state:
7452
+
7453
+ ```ts
7454
+ return mech.solve({ theta: 60 });
7455
+ ```
7456
+
7457
+ **Frame-aware serial joint**
7458
+
7459
+ ```ts
7460
+ const hip = cylinder(12, 10).withConnectors({
7461
+ socket: connector("hinge", {
7462
+ origin: [0, 0, 6],
7463
+ axis: [0, 0, 1],
7464
+ up: [1, 0, 0],
7465
+ kind: "revolute",
7466
+ }),
7467
+ });
7468
+
7469
+ const upperLeg = box(60, 10, 8).translate(30, 0, -4).withConnectors({
7470
+ hip: connector("hinge", {
7471
+ origin: [0, 0, 0],
7472
+ axis: [0, 0, -1],
7473
+ up: [1, 0, 0],
7474
+ kind: "revolute",
7475
+ }),
7476
+ });
7477
+
7478
+ return assembly("Leg")
7479
+ .addPart("Hip", hip)
7480
+ .addPart("Upper Leg", upperLeg)
7481
+ .connect("Hip.socket", "Upper Leg.hip", { as: "hip", min: -35, max: 55 })
7482
+ .solve({ hip: 20 });
7483
+ ```
7484
+
7485
+ **Return types**
7486
+
7487
+ | Return value | Standalone | `require()` result type |
7488
+ |---|---|---|
7489
+ | `Assembly` (unsolved) | yes | `ImportedAssembly` |
7490
+ | `SolvedAssembly` | yes | `SolvedAssembly` |
7491
+
7492
+ **Properties:**
7493
+
7494
+ | Property | Type | Description |
7495
+ |----------|------|-------------|
7496
+ | `name` | `string` | — |
7497
+
7498
+ **Kinematics**
7499
+
7500
+ #### `link()` — Add a named kinematic link to the assembly graph.
7501
+
7502
+ 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.
7503
+
7504
+ 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.
7505
+
7506
+ ```ts
7507
+ link(name: string, options?: AssemblyLinkOptions): Assembly
7508
+ ```
7509
+
7510
+ **`AssemblyLinkOptions`**
7511
+ - `at?: [ number, number, number ]` — Initial world-space position of this link before kinematic constraints solve it.
7512
+ - `fixed?: boolean` — Keep the link locked at its authored `at` position during solves.
7513
+ - `metadata?: Record<string, unknown>` — User metadata carried through the kinematic graph for inspection and tooling.
7514
+
7515
+ #### `edgeBetweenLinks()` — Add a relationship edge between two kinematic links.
7516
+
7517
+ 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.
7518
+
7519
+ ```ts
7520
+ edgeBetweenLinks(a: string, b: string, options?: AssemblyEdgeBetweenLinksOptions): Assembly
7521
+ ```
7522
+
7523
+ **`AssemblyEdgeBetweenLinksOptions`**: `name?: string`, `length?: number | "lockCurrent" | "free"`, `min?: number`, `max?: number`, `visualOnly?: boolean`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
7524
+
7525
+ `AssemblyKinematicControlOptions`: `{ min?: number, max?: number, default?: number, unit?: string }`
7526
+
7527
+ #### `addAngleBetweenLinks()` — Add an angle relationship among three kinematic links.
7528
+
7529
+ 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.
7530
+
7531
+ ```ts
7532
+ addAngleBetweenLinks(a: string, b: string, c: string, options?: AssemblyAngleBetweenLinksOptions): Assembly
7533
+ ```
7534
+
7535
+ **`AssemblyAngleBetweenLinksOptions`**: `name?: string`, `value?: number`, `min?: number`, `max?: number`, `control?: boolean | AssemblyKinematicControlOptions`, `limit?: AssemblyKinematicLimitOptions`, `metadata?: Record<string, unknown>`
7536
+
7537
+ `AssemblyKinematicLimitOptions`: `{ min?: number, max?: number }`
7538
+
7539
+ #### `addAngleBetweenLinkSegmentAndWorldDirection()` — Add an absolute angle relationship from a world direction to a link segment.
7540
+
7541
+ The first link is the vertex/pivot and the second link is the moving point. A value of `0` places `fromLink -> toLink` along `direction` in the mechanism plane; positive angles rotate counter-clockwise in that plane.
7542
+
7543
+ Use `Points.polar(1, angleDeg)` when the reference direction is planar and angle-based instead of axis-aligned.
7544
+
7545
+ ```ts
7546
+ addAngleBetweenLinkSegmentAndWorldDirection(fromLink: string, toLink: string, direction: Vec3, options?: AssemblyAngleBetweenLinksOptions): Assembly
7047
7547
  ```
7048
7548
 
7549
+ #### `describeKinematics()` — Return the assembly-native kinematic graph definition.
7550
+
7049
7551
  ```ts
7050
- assembly(name?: string): Assembly
7552
+ describeKinematics(): AssemblyKinematicGraphDef
7051
7553
  ```
7052
7554
 
7053
- #### `joint()` — Create a revolute joint that auto-generates a parameter slider and rotates the shape.
7555
+ **Structure**
7556
+
7557
+ #### `addPart()` — Add a named part to the assembly.
7558
+
7559
+ 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.
7560
+
7561
+ `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.
7054
7562
 
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.
7563
+ 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"`):
7056
7564
 
7057
7565
  ```ts
7058
- const arm = joint("Shoulder", armShape, [0, 0, 20], {
7059
- axis: [0, 1, 0],
7060
- min: -30, max: 120, default: 25,
7061
- });
7062
- return arm;
7566
+ const housing = group(
7567
+ { name: "Body", shape: body },
7568
+ { name: "Lid", shape: lid },
7569
+ );
7570
+ assembly.addPart("Base Assembly", housing);
7063
7571
  ```
7064
7572
 
7065
7573
  ```ts
7066
- joint(name: string, shape: Shape, pivot: [ number, number, number ], opts?: RevoluteJointOpts): Shape
7574
+ addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly
7067
7575
  ```
7068
7576
 
7069
- `RevoluteJointOpts`: `{ axis?: [ number, number, number ], min?: number, max?: number, default?: number, unit?: string, reverse?: boolean }`
7577
+ **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
7070
7578
 
7071
- ---
7072
-
7073
- ## Classes
7074
-
7075
- ### `Assembly`
7076
-
7077
- Container for a kinematic mechanism made up of named parts and joints.
7579
+ **`AssemblyPartMateInput`**
7580
+ - `connector: string` — Name of a connector declared on the part (via `withConnectors()`).
7581
+ - `toLink: string` — Name of the link this connector's origin is pinned to.
7582
+ - `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).
7078
7583
 
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.
7584
+ #### `frame()` Add a named rig frame to the assembly.
7080
7585
 
7081
- Each joint carries a `frame` transform (from the parent part frame to the joint's zero-state frame) and a motion formula:
7586
+ A frame is a solved pose: `origin` plus orientation. `axis` is the frame's primary direction and `up` fixes roll around that axis. Use frames for robot links, joint axes, and parts that must carry orientation. Use `link()` for solved points in distance/angle graphs.
7082
7587
 
7083
- ```
7084
- childWorld = parentWorld × frame × motion(value) × childBase
7588
+ ```ts
7589
+ frame(name: string, options: AssemblyFrameOptions): Assembly
7085
7590
  ```
7086
7591
 
7087
- Three joint types are supported:
7592
+ **`AssemblyFrameOptions`**: `origin: [ number, number, number ]`, `axis: [ number, number, number ]`, `up: [ number, number, number ]`, `fixed?: boolean`, `metadata?: Record<string, unknown>`
7088
7593
 
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`
7594
+ #### `fixedJoint()`Rigidly attach a child rig frame to a parent rig frame.
7092
7595
 
7093
- **Quick start**
7596
+ Fixed joints carry frame hierarchy but do not expose a Motion control.
7094
7597
 
7095
7598
  ```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
7599
+ fixedJoint(name: string, options: AssemblyFixedFrameJointOptions): Assembly
7106
7600
  ```
7107
7601
 
7108
- Returning an unsolved `Assembly` auto-solves at default joint values. Return a `SolvedAssembly` directly for a specific pose:
7602
+ `AssemblyFixedFrameJointOptions`: `{ parent: string, child: string, metadata?: Record<string, unknown> }`
7603
+
7604
+ #### `revoluteJoint()` — Add a revolute rig-frame joint.
7605
+
7606
+ The child frame rotates around the parent frame's `axis` direction. Moving frame joints appear in Motion by default; pass `control: false` to keep the joint solved at its default value without showing a Motion control.
7109
7607
 
7110
7608
  ```ts
7111
- return mech.solve({ shoulder: 60 });
7609
+ revoluteJoint(name: string, options: AssemblyMovingFrameJointOptions): Assembly
7112
7610
  ```
7113
7611
 
7114
- **Return types**
7612
+ **`AssemblyMovingFrameJointOptions`**: `parent: string`, `child: string`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `control?: boolean`, `metadata?: Record<string, unknown>`
7115
7613
 
7116
- | Return value | Standalone | `require()` result type |
7117
- |---|---|---|
7118
- | `Assembly` (unsolved) | yes | `ImportedAssembly` |
7119
- | `SolvedAssembly` | yes | `SolvedAssembly` |
7614
+ #### `prismaticJoint()` Add a prismatic rig-frame joint.
7120
7615
 
7121
- **Properties:**
7616
+ The child frame translates along the parent frame's `axis` direction. Moving frame joints appear in Motion by default; pass `control: false` to keep the joint solved at its default value without showing a Motion control.
7122
7617
 
7123
- | Property | Type | Description |
7124
- |----------|------|-------------|
7125
- | `name` | `string` | — |
7618
+ ```ts
7619
+ prismaticJoint(name: string, options: AssemblyMovingFrameJointOptions): Assembly
7620
+ ```
7126
7621
 
7127
7622
  **Connectors**
7128
7623
 
@@ -7142,6 +7637,10 @@ Use the single-argument overload to attach assembly-level connectors — these a
7142
7637
  withConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly
7143
7638
  ```
7144
7639
 
7640
+ **`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`
7641
+
7642
+ `ConnectorInput`: `{ connectorType?: string, gender?: ConnectorGender, measurements?: Record<string, number | string> }`
7643
+
7145
7644
  #### `getConnectors()` — Get connectors declared on a part in part-local space.
7146
7645
 
7147
7646
  ```ts
@@ -7158,23 +7657,35 @@ getConnector(ref: string): { partName: string; connectorName: string; connector:
7158
7657
 
7159
7658
  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
7659
 
7660
+ 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.
7661
+
7161
7662
  **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
7663
 
7163
7664
  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
7665
 
7666
+ **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.
7667
+
7165
7668
  The joint type is inferred from the connector's `kind` field if not specified in `options`.
7166
7669
 
7167
7670
  When connectors are defined with `start`/`end`, you can control which point on each connector meets via `align` / `parentAlign` / `childAlign` (`'start'`, `'middle'`, `'end'`).
7168
7671
 
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.
7672
+ Use `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore).
7170
7673
 
7171
7674
  ```ts
7172
7675
  // Hinge: both axes point outward along the hinge line
7173
7676
  const frame = box(100, 10, 80).withConnectors({
7174
- hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, 1] }),
7677
+ hinge: connector("hinge", {
7678
+ origin: [0, 0, 40],
7679
+ axis: [0, 0, 1],
7680
+ up: [1, 0, 0],
7681
+ }),
7175
7682
  });
7176
7683
  const door = box(60, 4, 80).withConnectors({
7177
- hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, -1] }),
7684
+ hinge: connector("hinge", {
7685
+ origin: [0, 0, 40],
7686
+ axis: [0, 0, -1],
7687
+ up: [1, 0, 0],
7688
+ }),
7178
7689
  });
7179
7690
  assembly("Door")
7180
7691
  .addPart("Frame", frame)
@@ -7186,6 +7697,16 @@ assembly("Door")
7186
7697
  connect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly
7187
7698
  ```
7188
7699
 
7700
+ **`ConnectOptions`**
7701
+
7702
+ | Option | Type | Description |
7703
+ |--------|------|-------------|
7704
+ | `flip?` | `boolean` | This parameter is ignored. If your connectors produce wrong orientation, fix the connector axis directions instead of using flip. |
7705
+ | `parentAlign?` | `PortAlign` | Which point on the parent connector to align: 'start', 'middle' (default), or 'end'. |
7706
+ | `childAlign?` | `PortAlign` | Which point on the child connector to align: 'start', 'middle' (default), or 'end'. |
7707
+ | `align?` | `PortAlign` | Shorthand: set both parentAlign and childAlign at once. |
7708
+ | `as?`, `type?`, `min?`, `max?`, `default?`, `unit?`, `effort?`, `velocity?`, `damping?`, `friction?` | | — |
7709
+
7189
7710
  #### `match()` — Auto-create a joint by matching typed connectors between two parts.
7190
7711
 
7191
7712
  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 +7729,15 @@ const mech = assembly("Door")
7208
7729
  .addPart("Frame", frame)
7209
7730
  .addPart("Door", door)
7210
7731
  .match("Door", "Frame", { hinge_top: "hinge_top", hinge_bottom: "hinge_bottom" });
7211
- // Revolute connectors auto-creates revolute joint. No manual addRevolute needed.
7732
+ // Matching connectors computes the placement relationship automatically.
7212
7733
  ```
7213
7734
 
7214
7735
  ```ts
7215
7736
  match(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & { as?: string; }): Assembly
7216
7737
  ```
7217
7738
 
7739
+ `MatchToOptions`: `{ force?: boolean, angle?: number, distance?: number }`
7740
+
7218
7741
  **References**
7219
7742
 
7220
7743
  #### `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 +7746,28 @@ match(childPartName: string, parentPartName: string, pairs: Record<string, strin
7223
7746
  withReferences(refs: Pick<PlacementReferenceInput, "points">): Assembly
7224
7747
  ```
7225
7748
 
7226
- **Solving**
7749
+ **`PlacementReferenceInput`**: `points?: Record<string, [ number, number, number ]>`, `edges?: Record<string, PlacementEdgeRef>`, `surfaces?: Record<string, PlacementSurfaceRef>`, `objects?: Record<string, PlacementObjectInput>`
7750
+
7751
+ `PlacementEdgeRef`: `{ start: Vec3, end: Vec3 }`
7752
+
7753
+ `PlacementSurfaceRef`: `{ center: Vec3, normal: Vec3 }`
7227
7754
 
7228
- #### `solve()` — Solve the assembly at the given joint state and return positioned parts.
7755
+ **Solving**
7229
7756
 
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()`).
7757
+ #### `solve()` Solve the assembly at the given control state and return positioned parts.
7231
7758
 
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.
7759
+ 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.
7233
7760
 
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.
7761
+ Connector mates declared on `addPart(..., { mate })` attach geometry to solved links while preserving part and connector identity:
7235
7762
 
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()`.
7763
+ - one mate **positions** the connector origin on its link;
7764
+ - 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;
7765
+ - a third mate **pins the roll** about the bone axis (full frame), e.g. a bore or clevis that must face a specific way.
7237
7766
 
7238
- **Example static posed output (no `toJointsView()`)**
7767
+ 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
7768
 
7240
7769
  ```ts
7241
- return mech.solve({ shoulder: 45, elbow: -20 });
7770
+ return mech.solve({ theta: 45 });
7242
7771
  ```
7243
7772
 
7244
7773
  ```ts
@@ -7253,156 +7782,30 @@ solve(state?: JointState): SolvedAssembly
7253
7782
  mate(fn: (m: MateBuilder) => void): Assembly
7254
7783
  ```
7255
7784
 
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:
7785
+ #### `edgeBetweenFrames()` — Add a visual skeleton edge between two rig frame origins.
7346
7786
 
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
- ```
7787
+ Frame edges follow the solved frame poses produced by `fixedJoint()`, `revoluteJoint()`, and `prismaticJoint()`. They do not add constraints, degrees of freedom, parts, or geometry; use them to make a frame-only rig readable in the Motion/rig inspection overlay.
7360
7788
 
7361
7789
  ```ts
7362
- addGearCoupling(drivenJointName: string, driverJointName: string, options?: GearCouplingOptions): Assembly
7790
+ edgeBetweenFrames(a: string, b: string, options?: AssemblyFrameEdgeOptions): Assembly
7363
7791
  ```
7364
7792
 
7365
- #### `sweepJoint()` Sample a joint through its motion range, collecting collision data at each step.
7793
+ `AssemblyFrameEdgeOptions`: `{ name?: string, metadata?: Record<string, unknown> }`
7366
7794
 
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.
7795
+ #### `linkToward()` Create a derived link at a fixed distance from `fromLink` toward `towardLink`.
7368
7796
 
7369
- You cannot sweep a coupled joint sweep one of its source joints instead.
7797
+ Derived links are trace/reference points. They are recomputed after the primary link solve and cannot participate in structural edges or angle constraints.
7370
7798
 
7371
7799
  ```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`);
7800
+ linkToward(name: string, fromLink: string, towardLink: string, distance: number): Assembly
7375
7801
  ```
7376
7802
 
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.
7803
+ #### `linkAwayFrom()` — Create a derived link at a fixed distance from `fromLink` away from `awayFromLink`.
7384
7804
 
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
- ```
7805
+ Use this for coupler trace/extension points such as the Chebyshev lambda linkage's point beyond the rocker joint.
7403
7806
 
7404
7807
  ```ts
7405
- toJointsView(options?: ToJointsViewOptions): void
7808
+ linkAwayFrom(name: string, fromLink: string, awayFromLink: string, distance: number): Assembly
7406
7809
  ```
7407
7810
 
7408
7811
  #### `describe()` — Return the serializable assembly definition used by solve/inspect pipelines.
@@ -7422,7 +7825,7 @@ describe(): AssemblyDefinition
7422
7825
 
7423
7826
  A wrapper around an imported `Assembly` that provides kinematic access and convenient transform helpers.
7424
7827
 
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.
7828
+ 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
7829
 
7427
7830
  **Kinematic access**
7428
7831
 
@@ -7451,7 +7854,7 @@ require("./arm.forge.js").mergeInto(robot, {
7451
7854
  });
7452
7855
  ```
7453
7856
 
7454
- #### `assembly()` — The underlying Assembly — use for sweepJoint, addPart into parent, etc.
7857
+ #### `assembly()` — The underlying Assembly, for advanced composition and inspection.
7455
7858
 
7456
7859
  ```ts
7457
7860
  get assembly(): Assembly
@@ -7469,6 +7872,14 @@ solve(state?: JointState): SolvedAssembly
7469
7872
  part(name: string, state?: JointState): AssemblyPart
7470
7873
  ```
7471
7874
 
7875
+ #### `getPart()` — Return a specific named part positioned at the default solved pose.
7876
+
7877
+ This mirrors `SolvedAssembly.getPart()` for imported assemblies. Use `solve(state).getPart(name)` when inspecting a non-default joint state.
7878
+
7879
+ ```ts
7880
+ getPart(partName: string): AssemblyPart
7881
+ ```
7882
+
7472
7883
  #### `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
7884
 
7474
7885
  ```ts
@@ -7547,17 +7958,35 @@ color(hex: string): ShapeGroup
7547
7958
  child(name: string): Shape | Sketch | ShapeGroup
7548
7959
  ```
7549
7960
 
7550
- #### `mergeInto()` — Flatten this sub-assembly's parts and joints into `parent` and wire a mount joint.
7961
+ #### `collisionReport()` — Detect overlapping part pairs at the default solved pose.
7962
+
7963
+ This mirrors `SolvedAssembly.collisionReport()` for imported assemblies. Use `solve(state).collisionReport(options)` when inspecting a non-default joint state.
7964
+
7965
+ ```ts
7966
+ collisionReport(options?: CollisionOptions): CollisionFinding[]
7967
+ ```
7968
+
7969
+ `CollisionOptions`: `{ parts?: string[], ignorePairs?: Array<[ string, string ]>, minOverlapVolume?: number }`
7970
+
7971
+ #### `minClearance()` — Compute the minimum gap between two parts at the default solved pose.
7972
+
7973
+ This mirrors `SolvedAssembly.minClearance()` for imported assemblies. Use `solve(state).minClearance(partA, partB, searchLength)` when inspecting a non-default joint state.
7974
+
7975
+ ```ts
7976
+ minClearance(partA: string, partB: string, searchLength?: number): number
7977
+ ```
7978
+
7979
+ #### `mergeInto()` — Flatten this sub-assembly's parts and relationships into `parent` and wire a mount relationship.
7551
7980
 
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:
7981
+ 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
7982
 
7554
7983
  ```ts
7555
- parent.solve({ "Left Arm.shoulder": 45, "Right Arm.shoulder": -20 })
7984
+ parent.solve({ "Left Arm.theta": 45, "Right Arm.theta": -20 })
7556
7985
  ```
7557
7986
 
7558
- Joint couplings inside the sub-assembly are preserved and rewritten with the prefix. Ports from sub-assembly parts are forwarded with the prefix.
7987
+ Connectors from sub-assembly parts are forwarded with the prefix.
7559
7988
 
7560
- The sub-assembly must have exactly one root part. If it has multiple roots, use `addFixed()` first to consolidate them before merging.
7989
+ The sub-assembly must have exactly one root part before it can be merged.
7561
7990
 
7562
7991
  ```ts
7563
7992
  const robot = assembly("Robot").addPart("Chassis", chassis);
@@ -7574,6 +8003,25 @@ require("./arm.forge.js").mergeInto(robot, {
7574
8003
  mergeInto(parent: Assembly, options: MergeIntoOptions): Assembly
7575
8004
  ```
7576
8005
 
8006
+ **`MergeIntoOptions`**
8007
+
8008
+ | Option | Type | Description |
8009
+ |--------|------|-------------|
8010
+ | `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. |
8011
+ | `mountParent` | `string` | Part name in the parent assembly to attach the sub-assembly root to. |
8012
+ | `mountJoint` | `string` | Name for the new mount joint in the parent graph. |
8013
+ | `mountType?` | `JointType` | Joint type for the mount connection (default: 'fixed'). |
8014
+ | `mountOptions?` | `JointOptions` | Frame, axis, limits, and other options for the mount joint. |
8015
+
8016
+ **`JointOptions`**
8017
+
8018
+ | Option | Type | Description |
8019
+ |--------|------|-------------|
8020
+ | `connectorRefs?` | `JointConnectorRefs` | Connector refs that define this joint contract. Usually set by `connect()` / `match()`. |
8021
+ | `frame?`, `origin?`, `axis?`, `min?`, `max?`, `default?`, `unit?`, `effort?`, `velocity?`, `damping?`, `friction?` | | — |
8022
+
8023
+ `JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`
8024
+
7577
8025
  ### `SolvedAssembly`
7578
8026
 
7579
8027
  The result of solving an assembly at a specific joint state.
@@ -7582,7 +8030,7 @@ The result of solving an assembly at a specific joint state.
7582
8030
 
7583
8031
  **Validation**
7584
8032
 
7585
- Call `collisionReport()` to detect overlapping parts, or `sweepJoint()` on the parent `Assembly` to check for interference across the joint's motion range.
8033
+ Call `collisionReport()` to detect overlapping parts at this solved pose.
7586
8034
 
7587
8035
  ```ts
7588
8036
  const solved = mech.solve({ shoulder: 45, elbow: -20 });
@@ -7628,6 +8076,30 @@ get mateDof(): number | null
7628
8076
  get mateConverged(): boolean | null
7629
8077
  ```
7630
8078
 
8079
+ #### `kinematics()` — Solved assembly-native kinematic or frame-edge overlay data, or null when no rig overlay data was declared.
8080
+
8081
+ ```ts
8082
+ get kinematics(): SolvedAssemblyKinematics | null
8083
+ ```
8084
+
8085
+ #### `getLinkPosition()` — Return the solved world position of a kinematic link.
8086
+
8087
+ ```ts
8088
+ getLinkPosition(linkName: string): Vec3
8089
+ ```
8090
+
8091
+ #### `getFrame()` — Return the solved world transform for a named rig frame.
8092
+
8093
+ ```ts
8094
+ getFrame(frameName: string): Transform
8095
+ ```
8096
+
8097
+ #### `frames()` — Return solved rig frames, including origin, axis, up, and transform.
8098
+
8099
+ ```ts
8100
+ get frames(): SolvedAssemblyFrameDef[]
8101
+ ```
8102
+
7631
8103
  #### `getTransform()` — Return the world-space [`Transform`](/docs/core#transform) for the named part at the solved pose.
7632
8104
 
7633
8105
  ```ts
@@ -8061,6 +8533,10 @@ const part = sheetMetal({ panel: { width: 100, height: 60 }, thickness: 1.5, ben
8061
8533
  flange(edge: SheetMetalEdge, options: SheetMetalFlangeOptions): SheetMetalPart
8062
8534
  ```
8063
8535
 
8536
+ **`SheetMetalFlangeOptions`**
8537
+ - `length: number` — Flange leg length in mm, measured from the outside of the bend to the tip.
8538
+ - `angleDeg?: number` — Bend angle in degrees (default: `90`). Only `90°` is supported in v1. Values other than 90 will be rejected at build time.
8539
+
8064
8540
  #### `cutout()` — Subtract a 2D sketch cutout from a planar region of the sheet metal part.
8065
8541
 
8066
8542
  `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 +8556,11 @@ const part = sheetMetal({ panel: { width: 180, height: 110 }, thickness: 1.5, be
8080
8556
  cutout(region: SheetMetalPlanarRegionName, sketch: Sketch, options?: SheetMetalCutoutOptions): SheetMetalPart
8081
8557
  ```
8082
8558
 
8559
+ **`SheetMetalCutoutOptions`**
8560
+ - `u?: number` — Horizontal offset within the region, measured from the region centre (mm). Default: `0`.
8561
+ - `v?: number` — Vertical offset within the region, measured from the region centre (mm). Default: `0`.
8562
+ - `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'`.
8563
+
8083
8564
  #### `regionNames()` — Return all semantic region names currently available on this part.
8084
8565
 
8085
8566
  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 +8830,7 @@ Call `robotExport()` alongside your assembly definition. The CLI commands `forge
8349
8830
 
8350
8831
  - Mesh-based inertia tensors (full 6-component, not bounding-box approximations)
8351
8832
  - Separate collision meshes (convex hull by default — ~50–80% smaller)
8352
- - Joint mimic elements derived from `addJointCoupling` / `addGearCoupling`
8833
+ - Joint limits, effort/velocity/damping/friction metadata from assembly joints
8353
8834
 
8354
8835
  **Collision mesh modes** (set per-link via `links["PartName"].collision`):
8355
8836
 
@@ -8365,7 +8846,7 @@ Call `robotExport()` alongside your assembly definition. The CLI commands `forge
8365
8846
  - Revolute `velocity` is in degrees/second in Forge; exporters convert to rad/s.
8366
8847
  - Prismatic distances are in mm in Forge; exported in meters.
8367
8848
  - `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.
8849
+ - 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
8850
 
8370
8851
  ```ts
8371
8852
  const rover = assembly("Scout")
@@ -8421,9 +8902,9 @@ robotExport(options: RobotExportOptions): CollectedRobotExport
8421
8902
 
8422
8903
  **`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
8904
 
8424
- `AssemblyDefinition`: `{ name: string, parts: AssemblyPartDef[], joints: AssemblyJointDef[], jointCouplings: AssemblyJointCouplingDef[] }`
8905
+ **`AssemblyDefinition`**: `name: string`, `parts: AssemblyPartDef[]`, `joints: AssemblyJointDef[]`, `jointCouplings: AssemblyJointCouplingDef[]`, `kinematics: AssemblyKinematicGraphDef`, `frames: AssemblyFrameDef[]`, `frameJoints: AssemblyFrameJointDef[]`, `frameEdges: AssemblyFrameEdgeDef[]`
8425
8906
 
8426
- `AssemblyPartDef`: `{ name: string, part: AssemblyPart, base: Transform, metadata?: PartMetadata }`
8907
+ **`AssemblyPartDef`**: `name: string`, `part: AssemblyPart`, `base: Transform`, `metadata?: PartMetadata`, `mates: AssemblyPartMateInput[]`, `bindToFrame?: string`
8427
8908
 
8428
8909
  **`PartMetadata`**
8429
8910
 
@@ -8432,6 +8913,11 @@ robotExport(options: RobotExportOptions): CollectedRobotExport
8432
8913
  | `tags?` | `string \| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |
8433
8914
  | `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |
8434
8915
 
8916
+ **`AssemblyPartMateInput`**
8917
+ - `connector: string` — Name of a connector declared on the part (via `withConnectors()`).
8918
+ - `toLink: string` — Name of the link this connector's origin is pinned to.
8919
+ - `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).
8920
+
8435
8921
  **`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
8922
 
8437
8923
  `JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`
@@ -8440,6 +8926,26 @@ robotExport(options: RobotExportOptions): CollectedRobotExport
8440
8926
 
8441
8927
  `JointCouplingTermRecord`: `{ joint: string, ratio: number }`
8442
8928
 
8929
+ **`AssemblyKinematicGraphDef`**: `links: AssemblyLinkDef[]`, `edges: AssemblyEdgeBetweenLinksDef[]`, `angles: AssemblyAngleBetweenLinksDef[]`, `derivedLinks: AssemblyDerivedLinkDef[]`
8930
+
8931
+ `AssemblyLinkDef`: `{ name: string, at: Vec3, fixed: boolean, metadata?: Record<string, unknown> }`
8932
+
8933
+ **`AssemblyEdgeBetweenLinksDef`**: `name: string`, `a: string`, `b: string`, `length: number | null`, `min?: number`, `max?: number`, `visualOnly: boolean`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
8934
+
8935
+ `AssemblyKinematicControlOptions`: `{ min?: number, max?: number, default?: number, unit?: string }`
8936
+
8937
+ **`AssemblyAngleBetweenLinksDef`**: `name: string`, `a?: string`, `b: string`, `c: string`, `reference?: AssemblyAngleReferenceDef`, `target?: number`, `min?: number`, `max?: number`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
8938
+
8939
+ `AssemblyAngleReferenceDef`: `{ kind: "worldDirection", direction: Vec3 }`
8940
+
8941
+ `AssemblyDerivedLinkDef`: `{ name: string, fromLink: string, referenceLink: string, distance: number, direction: AssemblyDerivedLinkDirection }`
8942
+
8943
+ `AssemblyFrameDef`: `{ name: string, origin: Vec3, axis: Vec3, up: Vec3, fixed: boolean, metadata?: Record<string, unknown> }`
8944
+
8945
+ **`AssemblyFrameJointDef`**: `name: string`, `type: AssemblyFrameJointType`, `parent: string`, `child: string`, `rest: Transform`, `min?: number`, `max?: number`, `defaultValue: number`, `unit?: string`, `control: boolean`, `metadata?: Record<string, unknown>`
8946
+
8947
+ `AssemblyFrameEdgeDef`: `{ name: string, a: string, b: string, metadata?: Record<string, unknown> }`
8948
+
8443
8949
  #### `dim()` — Add a dimension annotation between two points.
8444
8950
 
8445
8951
  Dimension annotations are purely visual callouts rendered in the viewport and report export. They do not affect geometry or constrain the model.
@@ -8587,18 +9093,24 @@ offsetBand(thickness: number): Sketch
8587
9093
  addSpurTeethBetween(options: DriveWheelSpurTeethRegionOptions): this
8588
9094
  ```
8589
9095
 
9096
+ `DriveWheelSpurTeethRegionOptions`: `{ name?: string, teethOnFullCircle: number, toothCount: number, firstTooth?: number, faceWidth?: number }`
9097
+
8590
9098
  #### `addSolidArcBetween()` — Add a constant-radius solid arc region such as a dwell, stop, or pusher.
8591
9099
 
8592
9100
  ```ts
8593
9101
  addSolidArcBetween(options: DriveWheelSolidArcRegionOptions): this
8594
9102
  ```
8595
9103
 
9104
+ **`DriveWheelSolidArcRegionOptions`**: `name?: string`, `fromAngleDeg: number`, `toAngleDeg: number`, `innerRadius?: number`, `outerRadius: number`, `faceWidth?: number`, `segments?: number`
9105
+
8596
9106
  #### `addShapeRegion()` — Add a fully custom region shape while preserving region metadata.
8597
9107
 
8598
9108
  ```ts
8599
9109
  addShapeRegion(name: string, shape: Shape, options?: DriveWheelShapeRegionOptions): this
8600
9110
  ```
8601
9111
 
9112
+ `DriveWheelShapeRegionOptions`: `{ fromAngleDeg?: number, toAngleDeg?: number, innerRadius?: number, outerRadius?: number }`
9113
+
8602
9114
  #### `build()` — Build the final wheel shape with a bore connector and region metadata.
8603
9115
 
8604
9116
  ```ts
@@ -8615,7 +9127,7 @@ Pre-built parametric parts available in user scripts as `lib.*`.
8615
9127
 
8616
9128
  Every key in this object becomes a method or namespace on the `lib` object exposed to `.forge.js` scripts. The catalog includes:
8617
9129
 
8618
- **Fasteners and hardware patterns:** `bolt`, `nut`, `washer`, `fastenerSet`, `boltedServiceCover`, `datumEnclosureAssembly`, `snapLatchCoverAssembly`, `pinnedLeverAssembly`, `retainedShaftAssembly`, `capturedLinearSlide`, `capturedCartridgeGuideAssembly`, `livingHingeCoverAssembly`, `knuckledHingeAssembly`, `clevisPinJointAssembly`, `seatedBearingAssembly`, `cableGlandAnchorAssembly`, `hoseBarbPortAssembly`, `routedTubeClipAssembly`, `pcbTerminalBlockAssembly`, `thumbScrewClampAssembly`, `fastenerHole`, `boltHole`, `counterbore`, `hexNut`, `holePattern`
9130
+ **Fasteners and hardware patterns:** `bolt`, `nut`, `washer`, `fastenerSet`, `fastenerHole`, `boltHole`, `counterbore`, `hexNut`, `holePattern`
8619
9131
 
8620
9132
  **Structure:** `tube`, `pipe`, `bracket`, `pipeRoute`, `elbow`, `tSlotProfile`, `tSlotExtrusion`, `profile2020BSlot6Profile`, `profile2020BSlot6`
8621
9133
 
@@ -8649,24 +9161,8 @@ Sizes outside the supported ranges will throw at runtime with a descriptive erro
8649
9161
  - `nut(diameter: number, options?: { pitch?: number; height?: number; acrossFlats?: number; segments?: number; }): Shape` — ISO-style hex nut with a threaded bore. **Details** Constructed from the intersection of three rotated slabs with a cylindrical bore subtracted. The nut is centered at the origin, height along Z. Default proportions follow ISO 4032 loosely: height ≈ 0.8×diameter, across-flats ≈ 1.6×diameter. The bore is a clearance bore (not modelled with helical threads) for rendering efficiency. For standard M-size nuts pre-configured for a complete joint, use { **Example** ```ts const n = lib.nut(5); // M5 nut ```
8650
9162
  - `washer(size: MetricSize, options?: { standard?: WasherStandard; segments?: number; }): Shape` — ISO metric flat washer (DIN 125-A). **Details** Returns a flat ring centered at the origin, thickness along Z. Dimensions are taken from { **Example** ```ts const w = lib.washer('M5'); // DIN 125-A M5 washer ```
8651
9163
  - `fastenerSet(size: MetricSize, boltLength: number, options?: FastenerSetOptions): FastenerSetResult` — Complete ISO metric fastener set — bolt, nut, optional washers, and matching hole cutters. **Details** Returns all geometry for one bolted joint: the bolt, nut, up to two washers, a clearance-hole cutter, and a tap-drill cutter. All shapes are returned **un-positioned** (each on the Z-axis). Place them with `.translate()`. Sizes outside M4–M10 are supported for the washer (M2–M10); unsupported combinations will throw. **Example** ```ts const hw = lib.fastenerSet('M5', 20); const topPlate = box(60, 40, 8).translate(0, 0, 12) .subtract(hw.clearanceHole.translate(15, 10, 12)); const botPlate = box(60, 40, 8) .subtract(hw.clearanceHole.translate(15, 10, 0)); return [ { name: 'Top Plate', shape: topPlate }, { name: 'Bot Plate', shape: botPlate }, { name: 'Bolt', shape: hw.bolt.translate(15, 10, 20) }, { name: 'Nut', shape: hw.nut.translate(15, 10, -4) }, ]; ```
8652
- - `boltedServiceCover(options: BoltedServiceCoverOptions): BoltedServiceCoverResult` — Bolted service-cover interface with real seats, aligned holes, gasket, fused pull tabs, and installed screws. **Details** This is a higher-level mechanical pattern for the common "removable service cover" failure mode. It creates the parent ledge, cover, gasket, and screws from one shared bolt pattern so agents do not place decorative screw heads or floating pull tabs by eye. Coordinate convention: the parent frame sits from `z=0` to `parentThickness`, the gasket sits on the ledge, the cover sits above the gasket, and screw shafts run downward through the cover into the parent. All parts are centered on the XY origin. **Example** ```ts const cover = lib.boltedServiceCover({ width: 90, depth: 56, screwSize: 'M4', ledgeWidth: 10, boltInset: [6, 6], }); verify.equal('four retained cover screws', cover.screws.length, 4); return cover.parts; ```
8653
- - `datumEnclosureAssembly(options: DatumEnclosureAssemblyOptions): DatumEnclosureAssemblyResult` — Datum-driven enclosure tray with shared wall, ledge, standoff, cover, gasket, port, and screw geometry. **Details** This pattern is for electronics boxes, thermostat backplates, service-stack housings, camera housings, and small fixtures where generated models often place panels, ribs, bosses, ports, and covers by eye. The tray, internal ledges, standoffs, ribs, service port, gasket, cover holes, and installed screws all come from one datum system. This keeps screw axes, boss locations, wall thickness, and service openings aligned instead of relying on independent magic numbers. Coordinate convention: X/Y are the enclosure footprint, Z is up. The base tray starts at `z=0` and rises to `height`; the gasket and cover sit above the top ledge with small explicit face clearances. **Example** ```ts const enclosure = lib.datumEnclosureAssembly({ width: 96, depth: 64, height: 18, }); verify.notColliding('cover clears enclosure gasket', enclosure.cover, enclosure.gasket); verify.inRange('cover stack has small seating clearance', enclosure.dims.faceClearance, 0.01, 0.08); return enclosure.parts; ```
8654
- - `snapLatchCoverAssembly(options: SnapLatchCoverAssemblyOptions): SnapLatchCoverAssemblyResult` — Snap-retained cover with a receiver frame, latch windows, underside catch lands, and fused snap hooks. **Details** This pattern is for covers, cartridges, clasps, and small housings where agents often add decorative tabs without a catch. The receiver has a real service opening plus two clearance latch windows. The cover is one fused part with two flexible-looking snap fingers that pass through the windows and barb under the receiver underside. Nothing intersects in the final assembly; the hook geometry sits close enough to the catch lands to prove retention intent. Coordinate convention: the receiver frame sits from `z=0` to `parentThickness`; the cover is seated just above the receiver on +Z. Two snap hooks sit on the +/-Y ledges and tuck under the receiver. **Example** ```ts const snapCover = lib.snapLatchCoverAssembly({ width: 72, depth: 44, }); verify.notColliding('snap hooks clear receiver windows', snapCover.cover, snapCover.parent); verify.inRange('snap cover has small seating clearance', snapCover.dims.faceClearance, 0.01, 0.08); return snapCover.parts; ```
8655
- - `pinnedLeverAssembly(options: PinnedLeverAssemblyOptions): PinnedLeverAssemblyResult` — Retained pinned lever stack with a fused hub/arm/grip, low stop land, pivot pin, bore cutters, and thrust washers. **Details** This pattern is for the common handle/lever failure mode where a visual arm, hub, washer, and pin are placed near each other but never form a credible mechanism. The lever body is one fused part, the pin runs through aligned bores, washers sit on both sides of the lever, and the support includes a bearing land plus an optional low stop land beside the lever path. Coordinate convention: pivot axis is +Z at the XY origin. The support starts at `z=0`, the lower washer sits on top of the support, the lever sits on the lower washer, the upper washer sits on the lever, and the retained pin spans the full stack. **Example** ```ts const lever = lib.pinnedLeverAssembly({ armLength: 54, armWidth: 10, pinDiameter: 5, }); verify.equal('lever stack has five retained parts', lever.parts.length, 5); return lever.parts; ```
8656
- - `retainedShaftAssembly(options: RetainedShaftAssemblyOptions): RetainedShaftAssemblyResult` — Retained shaft, washer, knob, and support-cheek stack for trunnions, pivots, and adjustable clamps. **Details** This pattern replaces the common "pin, washers, and knob are near each other" visual shortcut with a mechanically accountable shaft stack. The two support cheeks get matching clearance bores, the through shaft spans the whole stack, washers and knobs share the same axis, and retaining heads keep the knobs from reading as loose floating cylinders. Coordinate convention: the shaft axis is +X through the world origin. Support cheeks are centered at `x = +/- supportSpacing / 2`. The supports are bored for clearance, so collision inspection should report no support/shaft overlap while the connectivity audit still sees one retained stack. **Example** ```ts const trunnion = lib.retainedShaftAssembly({ supportSpacing: 96, shaftDiameter: 8, supportHeight: 42, }); verify.equal('retained shaft stack has seven parts', trunnion.parts.length, 7); return trunnion.parts; ```
8657
- - `capturedLinearSlide(options: CapturedLinearSlideOptions): CapturedLinearSlideResult` — Captured linear slide with a U-channel rail, return lips, end stops, and a carriage posed inside the guide. **Details** This pattern is for drawer-slide, quick-release plate, and guided-carriage models where agents often place rail details and a moving block near each other without a capture relationship. The rail is one fused part with side walls, inward lips, and end stops; the carriage is wider than the lip throat but narrower than the inner rail width, so it is mechanically captured while retaining explicit clearance. Coordinate convention: rail length is along X, width is along Y, and Z is up. The rail base starts at `z=0`; the carriage sits above the base and below the return lips. `travel=0` places the carriage at the negative-X end of travel, and `travel=maxTravel` places it at the positive-X end. **Example** ```ts const slide = lib.capturedLinearSlide({ length: 160, carriageLength: 52, travel: 42, }); verify.greaterThan('carriage is captured by return lips', slide.dims.carriageWidth, slide.dims.throatWidth); return slide.parts; ```
8658
- - `capturedCartridgeGuideAssembly(options: CapturedCartridgeGuideAssemblyOptions): CapturedCartridgeGuideAssemblyResult` — Captured removable cartridge guide with return lips, rear stop, wide cartridge flange, and pull tab. **Details** This pattern is for pump cartridges, filter cassettes, skeg cassettes, battery cartridges, and slide-in service modules where generated models often place a tray and a loose block near each other. The guide is one fused part with side walls, inward return lips, and a rear stop. The cartridge has a wide lower flange captured under the lips and a narrower body that passes through the throat, so the model has a real retention contract without manual coordinate tuning. Coordinate convention: insertion travel is along +X. The open entry is at −X, the rear stop is at +X, the guide base starts at `z=0`, and `insertion=0` places the cartridge at the front travel limit. **Example** ```ts const cassette = lib.capturedCartridgeGuideAssembly({ length: 150, cartridgeLength: 72, }); verify.notColliding('cartridge clears guide rails', cassette.cartridge, cassette.guide); verify.greaterThan('cartridge flange is captured by lips', cassette.dims.cartridgeWidth, cassette.dims.throatWidth); return cassette.parts; ```
8659
- - `livingHingeCoverAssembly(options: LivingHingeCoverAssemblyOptions): LivingHingeCoverAssemblyResult` — One-piece molded living-hinge cover strip with a fixed leaf, thin flexible web, cover leaf, pull lip, snap barb, and catch land. **Details** This pattern is for small polypropylene-style lids, battery doors, sample covers, blister latches, and molded service flaps where generated models often draw a decorative hinge strip between two disconnected plates. It returns one fused molded part in its as-molded flat state: fixed mounting leaf, thin hinge web, moving cover leaf, pull lip, raised snap barb, and catch land. The flexible web is intentionally much thinner than the rigid leaves and shares material with both leaves. Coordinate convention: X is hinge length/part width, Y runs from fixed leaf through hinge web to cover leaf, and Z is thickness. The hinge web is centered on `y=0`; the fixed leaf lies at −Y and the cover leaf at +Y. **Example** ```ts const livingCover = lib.livingHingeCoverAssembly({ width: 64, coverDepth: 42, }); verify.greaterThan('living hinge is much thinner than rigid leaves', livingCover.dims.flexRatio, 3); return livingCover.parts; ```
8660
- - `knuckledHingeAssembly(options: KnuckledHingeAssemblyOptions): KnuckledHingeAssemblyResult` — Alternating knuckle hinge with two fused leaves and a retained pin. **Details** This pattern replaces hand-placed hinge barrels and pin ghosts with a mechanically accountable hinge. The fixed leaf owns every other knuckle, the moving leaf owns the alternating knuckles, all knuckles share one bore size, and the retained pin spans the full stack with heads outside the barrels. Coordinate convention: the hinge pin axis is +X through the world origin. The fixed leaf extends toward +Y. The moving leaf extends toward -Y and rotates about +X by `openAngleDeg`. **Example** ```ts const hinge = lib.knuckledHingeAssembly({ length: 70, leafLength: 28, openAngleDeg: 45, }); verify.equal('hinge has two leaves and one retained pin', hinge.parts.length, 3); return hinge.parts; ```
8661
- - `clevisPinJointAssembly(options?: ClevisPinJointAssemblyOptions): ClevisPinJointAssemblyResult` — Clevis-style pin joint with bored yoke ears, a center link eye, and a retained pin. **Details** This pattern is for crank links, damper rod ends, pump crossheads, capo/cam pivots, and small mechanism joints where agents often place an eyelet and a pin near a bracket without modeling the captured load path. The clevis is one fused part with two bored ears and a rear bridge, the center link has a real eye and arm, and the retained pin spans the full stack with heads outside the ears. Coordinate convention: the pin axis is +Y through the world origin. The center link arm extends toward +X. The clevis bridge sits behind the eye on -X, leaving the link eye clear inside the yoke. **Example** ```ts const clevis = lib.clevisPinJointAssembly({ pinDiameter: 4, linkArmLength: 38, }); verify.equal('clevis joint has three retained parts', clevis.parts.length, 3); return clevis.parts; ```
8662
- - `seatedBearingAssembly(options: SeatedBearingAssemblyOptions): SeatedBearingAssemblyResult` — Seated radial-bearing support with a real counterbore, shoulder, through shaft, and retaining collars. **Details** This pattern is for purchased bearings, rollers, burr-cartridge shafts, and small spindle supports where agents often place a ring and a shaft near a block without modelling the pocket that locates the bearing. The housing includes a through-bore and a larger counterbore that leaves a shoulder for the bearing outer race. The shaft is smaller than the bearing bore and carries collars outside the housing, so collision checks can distinguish intended clearance from impossible overlap. Coordinate convention: the shaft axis is +Z through the world origin. The housing block starts at `z=0`, the raised boss is on top of the block, the bearing is seated from the top counterbore, and the shaft extends above and below the housing. **Example** ```ts const bearingStack = lib.seatedBearingAssembly({ bearingOuterDiameter: 22, bearingInnerDiameter: 8, bearingWidth: 7, }); verify.greaterThan('housing has wall around bearing pocket', bearingStack.dims.bossOuterDiameter - bearingStack.dims.pocketDiameter, 4); return bearingStack.parts; ```
8663
- - `cableGlandAnchorAssembly(options: CableGlandAnchorAssemblyOptions): CableGlandAnchorAssemblyResult` — Cable, wire, or tube gland anchor with a real panel hole, hollow gland body, compression nut, and routed cable. **Details** This pattern is for pumps, filters, electronics boxes, vents, monitors, and fixtures where generated models often leave hoses or cables terminating in space. It creates the receiving panel hole, a hollow gland body with a panel-side flange seated in a shallow pocket, a hollow compression nut, and a cable/tube that runs through the gland bore with explicit clearance. Coordinate convention: the cable axis is +X through the world origin. The panel is centered around `x=0` with thickness along X; the flange sits on the +X side of the panel and the compression nut sits on the −X side. The cable spans the full anchor. **Example** ```ts const anchor = lib.cableGlandAnchorAssembly({ cableDiameter: 6, panelThickness: 3, }); verify.notColliding('cable clears gland bore', anchor.cable, anchor.gland); verify.clearanceBetween('gland flange is seated at panel pocket', anchor.gland, anchor.panel, 0.01, 0.2); return anchor.parts; ```
8664
- - `hoseBarbPortAssembly(options: HoseBarbPortAssemblyOptions): HoseBarbPortAssemblyResult` — Hose-barb pump/filter port with a bored receiver, shoulder, barb ridges, installed hose, and clamp band. **Details** This pattern is for pump heads, filters, vents, lab cartridges, and fluid fittings where generated models often leave tubes ending near a block. The receiver has a real through-port and raised boss, the fitting is hollow with a shoulder and multiple barb ridges, and the hose is modeled as an installed tube over the barb envelope with a clamp band. The hose bore is sized for the deformed installed hose, so collision checks distinguish the retained interface from impossible solid overlap. Coordinate convention: the fluid axis is +X through the world origin. The receiver block is centered around `x=0`; the raised boss and hose are on the +X side. **Example** ```ts const hosePort = lib.hoseBarbPortAssembly({ hoseInnerDiameter: 6, hoseOuterDiameter: 10, }); verify.notColliding('hose clears barb peaks', hosePort.hose, hosePort.fitting); verify.inRange('fitting shoulder seats near boss face', hosePort.dims.faceClearance, 0.01, 0.08); return hosePort.parts; ```
8665
- - `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
- - `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
- - `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.
8669
- - `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.
9164
+ - `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.
9165
+ - `elbow(pipeRadius: number, bendRadius: number, angle?: number | { ... }, options?: { ... }): Shape` Pipe elbow a curved pipe section for connecting two pipe directions. This is a convenience recipe over `Curve.Arc()` and [`sweep()`](/docs/curves#sweep). Use `Curve.Route.fromPolyline()` directly when you need named route ports, segment metadata, or multi-bend pipe runs.
8670
9166
  - `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
9167
  - `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); ```
8672
9168
  - `tSlotProfile(options?: TSlotProfileOptions): Sketch` — Build a 2D T-slot cross-section sketch. Default parameters describe a 20x20 B-type profile with slot 6. Use this when you want a drawing-ready profile sketch before extrusion.
@@ -8814,7 +9310,7 @@ Cut planes, exploded views, joint animations, and scene configuration.
8814
9310
 
8815
9311
  ## Contents
8816
9312
 
8817
- - [Viewport & Runtime](#viewport-runtime) — `Viewport.label`, `scene`, `viewConfig`, `explodeView`, `jointsView`, `compareWith`, `cutPlane`, `mock`, `showLabels`, `highlight`
9313
+ - [Viewport & Runtime](#viewport-runtime) — `Viewport.label`, `scene`, `viewConfig`, `explodeView`, `compareWith`, `cutPlane`, `mock`, `showLabels`, `highlight`
8818
9314
  - [RouteBuilder](#routebuilder)
8819
9315
  - [route](#route)
8820
9316
 
@@ -9001,7 +9497,7 @@ scene(options: SceneOptions): void
9001
9497
 
9002
9498
  #### `viewConfig()` — Configure viewport helper visuals for the current script execution.
9003
9499
 
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.
9500
+ 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
9501
 
9006
9502
  This does **not** trigger a geometry recompute; it only affects the visual helpers drawn on top of the 3D scene.
9007
9503
 
@@ -9058,91 +9554,6 @@ explodeView(options?: ExplodeViewOptions): void
9058
9554
  - `direction?: ExplodeDirection` — Direction mode for this node
9059
9555
  - `axisLock?: ExplodeAxis` — Optional axis lock after direction is resolved
9060
9556
 
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
9557
  #### `compareWith()` — Declare a reference model for comparison inspection.
9147
9558
 
9148
9559
  `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`.
@@ -9475,6 +9886,30 @@ For deeper API coverage, load the relevant generated doc group from the skill so
9475
9886
 
9476
9887
  How to build mechanical joints — clevis-tongue hinges, ball-and-socket, dovetails — that actually rotate without binding and stop where they should.
9477
9888
 
9889
+ ## Frame-Aware Connectors First
9890
+
9891
+ 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:
9892
+
9893
+ - `origin` is the pivot, pin center, socket center, or contact point.
9894
+ - `axis` is the hinge line or slide direction.
9895
+ - `up` is the secondary direction that fixes the part's zero-angle twist.
9896
+
9897
+ 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.
9898
+
9899
+ 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.
9900
+
9901
+ 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.
9902
+
9903
+ ## Mirrored Revolute Axes
9904
+
9905
+ 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.
9906
+
9907
+ 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`.
9908
+
9909
+ 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.
9910
+
9911
+ 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.
9912
+
9478
9913
  ## The Cavity Rule
9479
9914
 
9480
9915
  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 +10041,7 @@ stable; named scene views are still available through `--view`.
9606
10041
  The command tree is intentionally job-shaped:
9607
10042
 
9608
10043
  ```text
9609
- inspect visual image|cutaway|depth|normals|objects
10044
+ inspect visual image|cutaway|depth|normals|rig|objects
9610
10045
  inspect surface zebra|roughness
9611
10046
  inspect physical components|floating|gaps
9612
10047
  inspect fit interference
@@ -9647,6 +10082,7 @@ Use targeted evidence commands for expensive analyses:
9647
10082
  ```bash
9648
10083
  forgecad inspect visual depth model.forge.js --camera iso
9649
10084
  forgecad inspect visual normals model.forge.js --camera iso
10085
+ forgecad inspect visual rig model.forge.js --camera iso
9650
10086
  forgecad inspect surface zebra model.forge.js --camera iso
9651
10087
  forgecad inspect surface roughness model.forge.js --camera iso
9652
10088
  forgecad inspect visual objects model.forge.js --camera iso
@@ -10132,12 +10568,33 @@ color(value: string | undefined): SdfShape
10132
10568
  material(props: ShapeMaterialProps): SdfShape
10133
10569
  ```
10134
10570
 
10571
+ **`ShapeMaterialProps`**
10572
+
10573
+ | Option | Type | Description |
10574
+ |--------|------|-------------|
10575
+ | `metalness?` | `number` | Metalness factor (0 = dielectric, 1 = metal). Default: 0.05 |
10576
+ | `roughness?` | `number` | Roughness factor (0 = mirror, 1 = fully diffuse). Default: 0.35 |
10577
+ | `emissive?` | `string` | Emissive glow color (hex string, e.g. "#ff6b35"). |
10578
+ | `emissiveIntensity?` | `number` | Emissive intensity multiplier. Default: 1 |
10579
+ | `opacity?` | `number` | Opacity (0 = fully transparent, 1 = fully opaque). Default: 1 |
10580
+ | `wireframe?` | `boolean` | Render as wireframe. Default: false |
10581
+ | `clearcoat?` | `number` | Clearcoat intensity (0–1). Default: 0.1 |
10582
+ | `clearcoatRoughness?` | `number` | Clearcoat roughness (0–1). Default: 0.4 |
10583
+ | `transmission?` | `number` | Glass/translucency transmission factor (0–1). Renderer support depends on target. |
10584
+ | `ior?` | `number` | Index of refraction for transmissive materials. Typical glass is ~1.45. |
10585
+ | `thickness?` | `number` | Approximate transmissive volume thickness in model units. |
10586
+ | `specularIntensity?` | `number` | Specular highlight intensity (0–1). |
10587
+ | `specularColor?` | `string` | Specular highlight tint. |
10588
+ | `reflectivity?` | `number` | Reflection strength for supported renderers (0–1). |
10589
+
10135
10590
  #### `bounds()` — Set explicit preview/meshing bounds for this implicit leaf.
10136
10591
 
10137
10592
  ```ts
10138
10593
  bounds(bounds: SdfBounds | [ Vec3, Vec3 ]): SdfShape
10139
10594
  ```
10140
10595
 
10596
+ `SdfBounds`: `{ min: Vec3, max: Vec3 }`
10597
+
10141
10598
  #### `at()` — Sculpt-style alias for translate().
10142
10599
 
10143
10600
  ```ts
@@ -10234,6 +10691,12 @@ fillWith(pattern: SdfShape): SdfShape
10234
10691
  fillWithGyroid(options: TpmsOptions): SdfShape
10235
10692
  ```
10236
10693
 
10694
+ **`TpmsOptions`**
10695
+ - `thickness?: number` — Dimensionless field threshold kept for compatibility. Prefer `wallThickness` for approximate millimeter units.
10696
+ - `wallThickness?: number` — Approximate physical wall thickness in millimeters.
10697
+ - `tpmsThicknessMode?: TpmsThicknessMode` — Override TPMS thickness interpretation. Defaults to metric when `wallThickness` is used, field-threshold when `thickness` is used.
10698
+ - Also: `cellSize: number`
10699
+
10237
10700
  #### `fillWithSchwarzP()` — Keep only the Schwarz-P lattice inside this shape.
10238
10701
 
10239
10702
  ```ts
@@ -10384,6 +10847,10 @@ shape.surfaceDisplace((u, v) => -Math.sin(u * 2) * 0.3)
10384
10847
  surfaceDisplace(pattern: SurfacePattern | ((u: number, v: number) => number), options?: SurfaceDisplaceOptions): SdfShape
10385
10848
  ```
10386
10849
 
10850
+ **`SurfaceDisplaceOptions`**
10851
+ - `uv?: "auto" | "sphere" | "cylinder" | "torus" | "triplanar"` — Override auto-detected UV mode. Default: 'auto' (detects from SDF tree).
10852
+ - `triplanarSharpness?: number` — Triplanar blend sharpness — higher = crisper transitions. Default: 4. Only used in triplanar mode.
10853
+
10387
10854
  #### `onion()` — Create concentric onion layers.
10388
10855
 
10389
10856
  ```ts