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
@@ -5,12 +5,12 @@ skill-order: 100
5
5
 
6
6
  # Assembly API
7
7
 
8
- Kinematic assemblies, joints, couplings, and robot export.
8
+ Assembly-owned links, constraints, connectors, solved poses, and robot export.
9
9
 
10
10
  ## Contents
11
11
 
12
- - [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`, `joint`
13
- - [Assembly](#assembly) — Structure, Connectors, References, Joints, Solving
12
+ - [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`
13
+ - [Assembly](#assembly) — Kinematics, Structure, Connectors, References, Solving
14
14
  - [ImportedAssembly](#importedassembly)
15
15
  - [SolvedAssembly](#solvedassembly)
16
16
  - [MateBuilder](#matebuilder)
@@ -36,96 +36,121 @@ bomToCsv(rows: BomRow[]): string
36
36
  | `tags?` | `string \| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |
37
37
  | `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |
38
38
 
39
- #### `assembly()` — Create an assembly container with named parts and joints for kinematic mechanisms.
39
+ #### `assembly()` — Create an assembly container with named parts, connectors, and kinematic links.
40
40
 
41
- **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.
41
+ **Use this from iteration 1 for any model with moving parts.** Do not build one static pose and retrofit motion later.
42
42
 
43
- 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.
43
+ 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.
44
44
 
45
- 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.
45
+ `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.
46
46
 
47
- 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.
47
+ 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.
48
48
 
49
- 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.
50
-
51
- ```ts
52
- const mech = assembly("Arm")
53
- .addPart("base", box(80, 80, 20).translate(0, 0, -10), {
54
- metadata: { material: "PETG", process: "FDM", qty: 1 },
55
- })
56
- .addPart("link", box(140, 24, 24).translate(0, -12, -12))
57
- .addRevolute("shoulder", "base", "link", {
58
- axis: [0, 1, 0],
59
- min: -30, max: 120, default: 25,
60
- frame: Transform.identity().translate(0, 0, 20),
61
- });
49
+ If no link in a connected kinematic component is fixed, ForgeCAD chooses a deterministic gauge link for solving and reports a floating-component warning.
62
50
 
63
- return mech; // auto-solved at defaults, renders all parts
64
- ```
51
+ 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.
65
52
 
66
- ```ts
67
- assembly(name?: string): Assembly
68
- ```
53
+ 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.
69
54
 
70
- #### `joint()` — Create a revolute joint that auto-generates a parameter slider and rotates the shape.
55
+ **Point-link example**
71
56
 
72
- 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.
57
+ This snippet mates a marker to the solved `tip` point. It does not orient a bar along `ground -> tip`.
73
58
 
74
59
  ```ts
75
- const arm = joint("Shoulder", armShape, [0, 0, 20], {
76
- axis: [0, 1, 0],
77
- min: -30, max: 120, default: 25,
60
+ const marker = box(8, 8, 4).withConnectors({
61
+ center: connector({ origin: [0, 0, 0], axis: [0, 0, 1] }),
78
62
  });
79
- return arm;
63
+
64
+ const mech = assembly("Linkage")
65
+ .link("ground", { at: [0, 0, 0], fixed: true })
66
+ .link("worldX", { at: [10, 0, 0], fixed: true })
67
+ .link("tip", { at: [40, 0, 0] })
68
+ .edgeBetweenLinks("ground", "tip", { name: "bar" })
69
+ .addAngleBetweenLinks("worldX", "ground", "tip", {
70
+ name: "theta",
71
+ control: { min: 0, max: 120, default: 30 },
72
+ })
73
+ .addPart("Tip marker", marker, { mate: { connector: "center", toLink: "tip" } });
74
+
75
+ return mech;
80
76
  ```
81
77
 
82
78
  ```ts
83
- joint(name: string, shape: Shape, pivot: [ number, number, number ], opts?: RevoluteJointOpts): Shape
79
+ assembly(name?: string): Assembly
84
80
  ```
85
81
 
86
- `RevoluteJointOpts`: `{ axis?: [ number, number, number ], min?: number, max?: number, default?: number, unit?: string, reverse?: boolean }`
87
-
88
82
  ---
89
83
 
90
84
  ## Classes
91
85
 
92
86
  ### `Assembly`
93
87
 
94
- Container for a kinematic mechanism made up of named parts and joints.
88
+ Container for a kinematic mechanism made up of links, relationships, and parts.
95
89
 
96
- 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.
90
+ Assembly has two related but different motion tools:
97
91
 
98
- Each joint carries a `frame` transform (from the parent part frame to the joint's zero-state frame) and a motion formula:
92
+ - **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.
93
+ - **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.
99
94
 
100
- ```
101
- childWorld = parentWorld × frame × motion(value) × childBase
102
- ```
95
+ 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.
103
96
 
104
- Three joint types are supported:
97
+ **Point-link quick start**
105
98
 
106
- - **revolute** rotates the child around an axis by `value` degrees
107
- - **prismatic** — translates the child along an axis by `value` mm
108
- - **fixed** — no motion; rigidly attaches the child at `frame`
109
-
110
- **Quick start**
99
+ This attaches a marker to a solved point. It is intentionally not a bone or oriented part example.
111
100
 
112
101
  ```ts
113
- const mech = assembly("Arm")
114
- .addPart("base", box(80, 80, 20).translate(0, 0, -10))
115
- .addPart("link", box(140, 24, 24).translate(0, -12, -12))
116
- .addJoint("shoulder", "revolute", "base", "link", {
117
- axis: [0, 1, 0],
118
- min: -30, max: 120, default: 25,
119
- frame: Transform.identity().translate(0, 0, 20),
102
+ const marker = box(8, 8, 4).withConnectors({
103
+ center: connector({ origin: [0, 0, 0], axis: [0, 0, 1] }),
104
+ });
105
+
106
+ const mech = assembly("Linkage")
107
+ .link("ground", { at: [0, 0, 0], fixed: true })
108
+ .link("worldX", { at: [10, 0, 0], fixed: true })
109
+ .link("tip", { at: [40, 0, 0] })
110
+ .edgeBetweenLinks("ground", "tip", { name: "bar" })
111
+ .addAngleBetweenLinks("worldX", "ground", "tip", {
112
+ name: "theta",
113
+ control: { min: 0, max: 120, default: 30 },
114
+ })
115
+ .addPart("Tip marker", marker, {
116
+ mate: { connector: "center", toLink: "tip" },
120
117
  });
121
118
 
122
- return mech; // auto-solved at defaults
119
+ return mech;
123
120
  ```
124
121
 
125
- Returning an unsolved `Assembly` auto-solves at default joint values. Return a `SolvedAssembly` directly for a specific pose:
122
+ Returning an unsolved `Assembly` keeps the graph available to the runtime. Return a `SolvedAssembly` directly for a specific control state:
126
123
 
127
124
  ```ts
128
- return mech.solve({ shoulder: 60 });
125
+ return mech.solve({ theta: 60 });
126
+ ```
127
+
128
+ **Frame-aware serial joint**
129
+
130
+ ```ts
131
+ const hip = cylinder(12, 10).withConnectors({
132
+ socket: connector("hinge", {
133
+ origin: [0, 0, 6],
134
+ axis: [0, 0, 1],
135
+ up: [1, 0, 0],
136
+ kind: "revolute",
137
+ }),
138
+ });
139
+
140
+ const upperLeg = box(60, 10, 8).translate(30, 0, -4).withConnectors({
141
+ hip: connector("hinge", {
142
+ origin: [0, 0, 0],
143
+ axis: [0, 0, -1],
144
+ up: [1, 0, 0],
145
+ kind: "revolute",
146
+ }),
147
+ });
148
+
149
+ return assembly("Leg")
150
+ .addPart("Hip", hip)
151
+ .addPart("Upper Leg", upperLeg)
152
+ .connect("Hip.socket", "Upper Leg.hip", { as: "hip", min: -35, max: 55 })
153
+ .solve({ hip: 20 });
129
154
  ```
130
155
 
131
156
  **Return types**
@@ -141,6 +166,130 @@ return mech.solve({ shoulder: 60 });
141
166
  |----------|------|-------------|
142
167
  | `name` | `string` | — |
143
168
 
169
+ **Kinematics**
170
+
171
+ #### `link()` — Add a named kinematic link to the assembly graph.
172
+
173
+ 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.
174
+
175
+ 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.
176
+
177
+ ```ts
178
+ link(name: string, options?: AssemblyLinkOptions): Assembly
179
+ ```
180
+
181
+ **`AssemblyLinkOptions`**
182
+ - `at?: [ number, number, number ]` — Initial world-space position of this link before kinematic constraints solve it.
183
+ - `fixed?: boolean` — Keep the link locked at its authored `at` position during solves.
184
+ - `metadata?: Record<string, unknown>` — User metadata carried through the kinematic graph for inspection and tooling.
185
+
186
+ #### `edgeBetweenLinks()` — Add a relationship edge between two kinematic links.
187
+
188
+ 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.
189
+
190
+ ```ts
191
+ edgeBetweenLinks(a: string, b: string, options?: AssemblyEdgeBetweenLinksOptions): Assembly
192
+ ```
193
+
194
+ **`AssemblyEdgeBetweenLinksOptions`**: `name?: string`, `length?: number | "lockCurrent" | "free"`, `min?: number`, `max?: number`, `visualOnly?: boolean`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
195
+
196
+ `AssemblyKinematicControlOptions`: `{ min?: number, max?: number, default?: number, unit?: string }`
197
+
198
+ #### `addAngleBetweenLinks()` — Add an angle relationship among three kinematic links.
199
+
200
+ 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.
201
+
202
+ ```ts
203
+ addAngleBetweenLinks(a: string, b: string, c: string, options?: AssemblyAngleBetweenLinksOptions): Assembly
204
+ ```
205
+
206
+ **`AssemblyAngleBetweenLinksOptions`**: `name?: string`, `value?: number`, `min?: number`, `max?: number`, `control?: boolean | AssemblyKinematicControlOptions`, `limit?: AssemblyKinematicLimitOptions`, `metadata?: Record<string, unknown>`
207
+
208
+ `AssemblyKinematicLimitOptions`: `{ min?: number, max?: number }`
209
+
210
+ #### `addAngleBetweenLinkSegmentAndWorldDirection()` — Add an absolute angle relationship from a world direction to a link segment.
211
+
212
+ 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.
213
+
214
+ Use `Points.polar(1, angleDeg)` when the reference direction is planar and angle-based instead of axis-aligned.
215
+
216
+ ```ts
217
+ addAngleBetweenLinkSegmentAndWorldDirection(fromLink: string, toLink: string, direction: Vec3, options?: AssemblyAngleBetweenLinksOptions): Assembly
218
+ ```
219
+
220
+ #### `describeKinematics()` — Return the assembly-native kinematic graph definition.
221
+
222
+ ```ts
223
+ describeKinematics(): AssemblyKinematicGraphDef
224
+ ```
225
+
226
+ **Structure**
227
+
228
+ #### `addPart()` — Add a named part to the assembly.
229
+
230
+ 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.
231
+
232
+ `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.
233
+
234
+ 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"`):
235
+
236
+ ```ts
237
+ const housing = group(
238
+ { name: "Body", shape: body },
239
+ { name: "Lid", shape: lid },
240
+ );
241
+ assembly.addPart("Base Assembly", housing);
242
+ ```
243
+
244
+ ```ts
245
+ addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly
246
+ ```
247
+
248
+ **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
249
+
250
+ **`AssemblyPartMateInput`**
251
+ - `connector: string` — Name of a connector declared on the part (via `withConnectors()`).
252
+ - `toLink: string` — Name of the link this connector's origin is pinned to.
253
+ - `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).
254
+
255
+ #### `frame()` — Add a named rig frame to the assembly.
256
+
257
+ 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.
258
+
259
+ ```ts
260
+ frame(name: string, options: AssemblyFrameOptions): Assembly
261
+ ```
262
+
263
+ **`AssemblyFrameOptions`**: `origin: [ number, number, number ]`, `axis: [ number, number, number ]`, `up: [ number, number, number ]`, `fixed?: boolean`, `metadata?: Record<string, unknown>`
264
+
265
+ #### `fixedJoint()` — Rigidly attach a child rig frame to a parent rig frame.
266
+
267
+ Fixed joints carry frame hierarchy but do not expose a Motion control.
268
+
269
+ ```ts
270
+ fixedJoint(name: string, options: AssemblyFixedFrameJointOptions): Assembly
271
+ ```
272
+
273
+ `AssemblyFixedFrameJointOptions`: `{ parent: string, child: string, metadata?: Record<string, unknown> }`
274
+
275
+ #### `revoluteJoint()` — Add a revolute rig-frame joint.
276
+
277
+ 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.
278
+
279
+ ```ts
280
+ revoluteJoint(name: string, options: AssemblyMovingFrameJointOptions): Assembly
281
+ ```
282
+
283
+ **`AssemblyMovingFrameJointOptions`**: `parent: string`, `child: string`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `control?: boolean`, `metadata?: Record<string, unknown>`
284
+
285
+ #### `prismaticJoint()` — Add a prismatic rig-frame joint.
286
+
287
+ 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.
288
+
289
+ ```ts
290
+ prismaticJoint(name: string, options: AssemblyMovingFrameJointOptions): Assembly
291
+ ```
292
+
144
293
  **Connectors**
145
294
 
146
295
  #### `usedConnectorRefs()` — Connector refs (e.g. "PartName.connectorName") consumed by connect/match calls.
@@ -159,6 +308,10 @@ Use the single-argument overload to attach assembly-level connectors — these a
159
308
  withConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly
160
309
  ```
161
310
 
311
+ **`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`
312
+
313
+ `ConnectorInput`: `{ connectorType?: string, gender?: ConnectorGender, measurements?: Record<string, number | string> }`
314
+
162
315
  #### `getConnectors()` — Get connectors declared on a part in part-local space.
163
316
 
164
317
  ```ts
@@ -175,23 +328,35 @@ getConnector(ref: string): { partName: string; connectorName: string; connector:
175
328
 
176
329
  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.
177
330
 
331
+ 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.
332
+
178
333
  **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()`.
179
334
 
180
335
  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.
181
336
 
337
+ **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.
338
+
182
339
  The joint type is inferred from the connector's `kind` field if not specified in `options`.
183
340
 
184
341
  When connectors are defined with `start`/`end`, you can control which point on each connector meets via `align` / `parentAlign` / `childAlign` (`'start'`, `'middle'`, `'end'`).
185
342
 
186
- 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.
343
+ Use `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore).
187
344
 
188
345
  ```ts
189
346
  // Hinge: both axes point outward along the hinge line
190
347
  const frame = box(100, 10, 80).withConnectors({
191
- hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, 1] }),
348
+ hinge: connector("hinge", {
349
+ origin: [0, 0, 40],
350
+ axis: [0, 0, 1],
351
+ up: [1, 0, 0],
352
+ }),
192
353
  });
193
354
  const door = box(60, 4, 80).withConnectors({
194
- hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, -1] }),
355
+ hinge: connector("hinge", {
356
+ origin: [0, 0, 40],
357
+ axis: [0, 0, -1],
358
+ up: [1, 0, 0],
359
+ }),
195
360
  });
196
361
  assembly("Door")
197
362
  .addPart("Frame", frame)
@@ -203,6 +368,16 @@ assembly("Door")
203
368
  connect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly
204
369
  ```
205
370
 
371
+ **`ConnectOptions`**
372
+
373
+ | Option | Type | Description |
374
+ |--------|------|-------------|
375
+ | `flip?` | `boolean` | This parameter is ignored. If your connectors produce wrong orientation, fix the connector axis directions instead of using flip. |
376
+ | `parentAlign?` | `PortAlign` | Which point on the parent connector to align: 'start', 'middle' (default), or 'end'. |
377
+ | `childAlign?` | `PortAlign` | Which point on the child connector to align: 'start', 'middle' (default), or 'end'. |
378
+ | `align?` | `PortAlign` | Shorthand: set both parentAlign and childAlign at once. |
379
+ | `as?`, `type?`, `min?`, `max?`, `default?`, `unit?`, `effort?`, `velocity?`, `damping?`, `friction?` | | — |
380
+
206
381
  #### `match()` — Auto-create a joint by matching typed connectors between two parts.
207
382
 
208
383
  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.
@@ -225,13 +400,15 @@ const mech = assembly("Door")
225
400
  .addPart("Frame", frame)
226
401
  .addPart("Door", door)
227
402
  .match("Door", "Frame", { hinge_top: "hinge_top", hinge_bottom: "hinge_bottom" });
228
- // Revolute connectors auto-creates revolute joint. No manual addRevolute needed.
403
+ // Matching connectors computes the placement relationship automatically.
229
404
  ```
230
405
 
231
406
  ```ts
232
407
  match(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & { as?: string; }): Assembly
233
408
  ```
234
409
 
410
+ `MatchToOptions`: `{ force?: boolean, angle?: number, distance?: number }`
411
+
235
412
  **References**
236
413
 
237
414
  #### `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.
@@ -240,22 +417,28 @@ match(childPartName: string, parentPartName: string, pairs: Record<string, strin
240
417
  withReferences(refs: Pick<PlacementReferenceInput, "points">): Assembly
241
418
  ```
242
419
 
243
- **Solving**
420
+ **`PlacementReferenceInput`**: `points?: Record<string, [ number, number, number ]>`, `edges?: Record<string, PlacementEdgeRef>`, `surfaces?: Record<string, PlacementSurfaceRef>`, `objects?: Record<string, PlacementObjectInput>`
244
421
 
245
- #### `solve()` Solve the assembly at the given joint state and return positioned parts.
422
+ `PlacementEdgeRef`: `{ start: Vec3, end: Vec3 }`
423
+
424
+ `PlacementSurfaceRef`: `{ center: Vec3, normal: Vec3 }`
425
+
426
+ **Solving**
246
427
 
247
- 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()`).
428
+ #### `solve()` Solve the assembly at the given control state and return positioned parts.
248
429
 
249
- 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.
430
+ 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.
250
431
 
251
- **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.
432
+ Connector mates declared on `addPart(..., { mate })` attach geometry to solved links while preserving part and connector identity:
252
433
 
253
- This pitfall only applies when `toJointsView()` is active. If you only want a static posed result, return the solved assembly directly and skip `toJointsView()`.
434
+ - one mate **positions** the connector origin on its link;
435
+ - 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;
436
+ - a third mate **pins the roll** about the bone axis (full frame), e.g. a bore or clevis that must face a specific way.
254
437
 
255
- **Example static posed output (no `toJointsView()`)**
438
+ Connector-frame joints created by `connect()` / `match()` are also evaluated; their values are read from `state` by joint name and clamped to joint limits.
256
439
 
257
440
  ```ts
258
- return mech.solve({ shoulder: 45, elbow: -20 });
441
+ return mech.solve({ theta: 45 });
259
442
  ```
260
443
 
261
444
  ```ts
@@ -270,156 +453,30 @@ solve(state?: JointState): SolvedAssembly
270
453
  mate(fn: (m: MateBuilder) => void): Assembly
271
454
  ```
272
455
 
273
- #### `addFrame()` — Add a virtual reference frame (no geometry) to the assembly graph.
456
+ #### `edgeBetweenFrames()` — Add a visual skeleton edge between two rig frame origins.
274
457
 
275
- 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.
458
+ 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.
276
459
 
277
460
  ```ts
278
- addFrame(name: string, options?: PartOptions): Assembly
461
+ edgeBetweenFrames(a: string, b: string, options?: AssemblyFrameEdgeOptions): Assembly
279
462
  ```
280
463
 
281
- #### `addPart()` Add a named part to the assembly.
282
-
283
- 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.
284
-
285
- 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"`):
286
-
287
- ```ts
288
- const housing = group(
289
- { name: "Body", shape: body },
290
- { name: "Lid", shape: lid },
291
- );
292
- assembly.addPart("Base Assembly", housing);
293
- ```
294
-
295
- ```ts
296
- addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly
297
- ```
298
-
299
- #### `addJoint()` — Add a kinematic joint between a parent and child part.
300
-
301
- `frame` is a transform from the **parent part frame** to the **joint frame at zero state**. The child's world position is computed as:
302
-
303
- ```
304
- childWorld = parentWorld × frame × motion(value) × childBase
305
- ```
306
-
307
- 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.
308
-
309
- ```ts
310
- addJoint(name: string, type: JointType, parent: string, child: string, options?: JointOptions): Assembly
311
- ```
312
-
313
- #### `addRevolute()` — Shorthand for `addJoint(name, 'revolute', parent, child, options)`.
314
-
315
- ```ts
316
- addRevolute(name: string, parent: string, child: string, options?: JointOptions): Assembly
317
- ```
318
-
319
- #### `addPrismatic()` — Shorthand for `addJoint(name, 'prismatic', parent, child, options)`.
320
-
321
- ```ts
322
- addPrismatic(name: string, parent: string, child: string, options?: JointOptions): Assembly
323
- ```
324
-
325
- #### `addFixed()` — Shorthand for `addJoint(name, 'fixed', parent, child, options)`.
326
-
327
- 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.
328
-
329
- ```ts
330
- addFixed(name: string, parent: string, child: string, options?: JointOptions): Assembly
331
- ```
332
-
333
- #### `addJointCoupling()` — Link a joint's value to a linear combination of other joint values.
334
-
335
- The driven joint's value is computed as:
336
-
337
- ```
338
- driven = offset + Σ(ratio_i × source_i)
339
- ```
340
-
341
- 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.
342
-
343
- ```ts
344
- assembly
345
- .addRevolute("Steering", "Base", "Turret", { axis: [0, 0, 1] })
346
- .addRevolute("WheelDrive", "Turret", "Wheel", { axis: [1, 0, 0] })
347
- .addRevolute("TopGear", "Base", "TopInput", { axis: [0, 0, 1] })
348
- .addJointCoupling("TopGear", {
349
- terms: [
350
- { joint: "Steering", ratio: 1 },
351
- { joint: "WheelDrive", ratio: 20 / 14 },
352
- ],
353
- });
354
- ```
355
-
356
- ```ts
357
- addJointCoupling(jointName: string, options: JointCouplingOptions): Assembly
358
- ```
359
-
360
- #### `addGearCoupling()` — Link two revolute joints via a gear ratio.
361
-
362
- Choose exactly one ratio source:
363
-
364
- - `ratio` — explicit numeric ratio (driven/driver, negative for external mesh)
365
- - `pair` — a `GearRatioLike` from `lib.gearPair`, `lib.bevelGearPair`, etc. (uses `pair.jointRatio`)
366
- - `driverTeeth` + `drivenTeeth` — auto-computes ratio; use `mesh` to control sign (`'external'` = negative/opposite rotation, `'internal'` = positive, `'bevel'`/`'face'` = negative)
367
-
368
- 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.
369
-
370
- ```ts
371
- const pair = lib.gearPair({ pinion: { module: 1.25, teeth: 14 }, gear: { module: 1.25, teeth: 42 } });
372
- assembly
373
- .addRevolute("Pinion", "Base", "PinionPart", { axis: [0, 0, 1] })
374
- .addRevolute("Driven", "Base", "GearPart", { axis: [0, 0, 1] })
375
- .addGearCoupling("Driven", "Pinion", { pair });
376
- ```
464
+ `AssemblyFrameEdgeOptions`: `{ name?: string, metadata?: Record<string, unknown> }`
377
465
 
378
- ```ts
379
- addGearCoupling(drivenJointName: string, driverJointName: string, options?: GearCouplingOptions): Assembly
380
- ```
381
-
382
- #### `sweepJoint()` — Sample a joint through its motion range, collecting collision data at each step.
383
-
384
- 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.
466
+ #### `linkToward()` — Create a derived link at a fixed distance from `fromLink` toward `towardLink`.
385
467
 
386
- You cannot sweep a coupled joint sweep one of its source joints instead.
468
+ Derived links are trace/reference points. They are recomputed after the primary link solve and cannot participate in structural edges or angle constraints.
387
469
 
388
470
  ```ts
389
- const sweep = mech.sweepJoint("elbow", -10, 135, 12, { shoulder: 35 });
390
- const hits = sweep.filter(frame => frame.collisions.length > 0);
391
- console.log(`Collisions at ${hits.length} of ${sweep.length} poses`);
471
+ linkToward(name: string, fromLink: string, towardLink: string, distance: number): Assembly
392
472
  ```
393
473
 
394
- ```ts
395
- sweepJoint(jointName: string, from: number, to: number, steps: number, baseState?: JointState, collisionOptions?: CollisionOptions): JointSweepFrame[]
396
- ```
397
-
398
- #### `toJointsView()` — Derive viewport joint controls from the assembly graph and register them.
399
-
400
- 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.
474
+ #### `linkAwayFrom()` — Create a derived link at a fixed distance from `fromLink` away from `awayFromLink`.
401
475
 
402
- 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()`.
403
-
404
- **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.
405
-
406
- Do not solve at a non-zero angle when using `toJointsView()` — the viewport will apply the same rotation again, double-rotating the part.
407
-
408
- ```ts
409
- mech.toJointsView({
410
- defaults: { J1: 30 },
411
- animations: [{
412
- name: "Swing", duration: 2, loop: true,
413
- keyframes: [{ values: { J1: -45 } }, { values: { J1: 45 } }, { values: { J1: -45 } }],
414
- }],
415
- });
416
-
417
- // Solve at REST — viewport handles posing
418
- return mech.solve();
419
- ```
476
+ Use this for coupler trace/extension points such as the Chebyshev lambda linkage's point beyond the rocker joint.
420
477
 
421
478
  ```ts
422
- toJointsView(options?: ToJointsViewOptions): void
479
+ linkAwayFrom(name: string, fromLink: string, awayFromLink: string, distance: number): Assembly
423
480
  ```
424
481
 
425
482
  #### `describe()` — Return the serializable assembly definition used by solve/inspect pipelines.
@@ -439,7 +496,7 @@ describe(): AssemblyDefinition
439
496
 
440
497
  A wrapper around an imported `Assembly` that provides kinematic access and convenient transform helpers.
441
498
 
442
- 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.
499
+ 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.
443
500
 
444
501
  **Kinematic access**
445
502
 
@@ -468,7 +525,7 @@ require("./arm.forge.js").mergeInto(robot, {
468
525
  });
469
526
  ```
470
527
 
471
- #### `assembly()` — The underlying Assembly — use for sweepJoint, addPart into parent, etc.
528
+ #### `assembly()` — The underlying Assembly, for advanced composition and inspection.
472
529
 
473
530
  ```ts
474
531
  get assembly(): Assembly
@@ -486,6 +543,14 @@ solve(state?: JointState): SolvedAssembly
486
543
  part(name: string, state?: JointState): AssemblyPart
487
544
  ```
488
545
 
546
+ #### `getPart()` — Return a specific named part positioned at the default solved pose.
547
+
548
+ This mirrors `SolvedAssembly.getPart()` for imported assemblies. Use `solve(state).getPart(name)` when inspecting a non-default joint state.
549
+
550
+ ```ts
551
+ getPart(partName: string): AssemblyPart
552
+ ```
553
+
489
554
  #### `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.
490
555
 
491
556
  ```ts
@@ -564,17 +629,35 @@ color(hex: string): ShapeGroup
564
629
  child(name: string): Shape | Sketch | ShapeGroup
565
630
  ```
566
631
 
567
- #### `mergeInto()` — Flatten this sub-assembly's parts and joints into `parent` and wire a mount joint.
632
+ #### `collisionReport()` — Detect overlapping part pairs at the default solved pose.
568
633
 
569
- 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:
634
+ This mirrors `SolvedAssembly.collisionReport()` for imported assemblies. Use `solve(state).collisionReport(options)` when inspecting a non-default joint state.
570
635
 
571
636
  ```ts
572
- parent.solve({ "Left Arm.shoulder": 45, "Right Arm.shoulder": -20 })
637
+ collisionReport(options?: CollisionOptions): CollisionFinding[]
573
638
  ```
574
639
 
575
- Joint couplings inside the sub-assembly are preserved and rewritten with the prefix. Ports from sub-assembly parts are forwarded with the prefix.
640
+ `CollisionOptions`: `{ parts?: string[], ignorePairs?: Array<[ string, string ]>, minOverlapVolume?: number }`
641
+
642
+ #### `minClearance()` — Compute the minimum gap between two parts at the default solved pose.
576
643
 
577
- The sub-assembly must have exactly one root part. If it has multiple roots, use `addFixed()` first to consolidate them before merging.
644
+ This mirrors `SolvedAssembly.minClearance()` for imported assemblies. Use `solve(state).minClearance(partA, partB, searchLength)` when inspecting a non-default joint state.
645
+
646
+ ```ts
647
+ minClearance(partA: string, partB: string, searchLength?: number): number
648
+ ```
649
+
650
+ #### `mergeInto()` — Flatten this sub-assembly's parts and relationships into `parent` and wire a mount relationship.
651
+
652
+ 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:
653
+
654
+ ```ts
655
+ parent.solve({ "Left Arm.theta": 45, "Right Arm.theta": -20 })
656
+ ```
657
+
658
+ Connectors from sub-assembly parts are forwarded with the prefix.
659
+
660
+ The sub-assembly must have exactly one root part before it can be merged.
578
661
 
579
662
  ```ts
580
663
  const robot = assembly("Robot").addPart("Chassis", chassis);
@@ -591,6 +674,25 @@ require("./arm.forge.js").mergeInto(robot, {
591
674
  mergeInto(parent: Assembly, options: MergeIntoOptions): Assembly
592
675
  ```
593
676
 
677
+ **`MergeIntoOptions`**
678
+
679
+ | Option | Type | Description |
680
+ |--------|------|-------------|
681
+ | `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. |
682
+ | `mountParent` | `string` | Part name in the parent assembly to attach the sub-assembly root to. |
683
+ | `mountJoint` | `string` | Name for the new mount joint in the parent graph. |
684
+ | `mountType?` | `JointType` | Joint type for the mount connection (default: 'fixed'). |
685
+ | `mountOptions?` | `JointOptions` | Frame, axis, limits, and other options for the mount joint. |
686
+
687
+ **`JointOptions`**
688
+
689
+ | Option | Type | Description |
690
+ |--------|------|-------------|
691
+ | `connectorRefs?` | `JointConnectorRefs` | Connector refs that define this joint contract. Usually set by `connect()` / `match()`. |
692
+ | `frame?`, `origin?`, `axis?`, `min?`, `max?`, `default?`, `unit?`, `effort?`, `velocity?`, `damping?`, `friction?` | | — |
693
+
694
+ `JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`
695
+
594
696
  ### `SolvedAssembly`
595
697
 
596
698
  The result of solving an assembly at a specific joint state.
@@ -599,7 +701,7 @@ The result of solving an assembly at a specific joint state.
599
701
 
600
702
  **Validation**
601
703
 
602
- Call `collisionReport()` to detect overlapping parts, or `sweepJoint()` on the parent `Assembly` to check for interference across the joint's motion range.
704
+ Call `collisionReport()` to detect overlapping parts at this solved pose.
603
705
 
604
706
  ```ts
605
707
  const solved = mech.solve({ shoulder: 45, elbow: -20 });
@@ -645,6 +747,30 @@ get mateDof(): number | null
645
747
  get mateConverged(): boolean | null
646
748
  ```
647
749
 
750
+ #### `kinematics()` — Solved assembly-native kinematic or frame-edge overlay data, or null when no rig overlay data was declared.
751
+
752
+ ```ts
753
+ get kinematics(): SolvedAssemblyKinematics | null
754
+ ```
755
+
756
+ #### `getLinkPosition()` — Return the solved world position of a kinematic link.
757
+
758
+ ```ts
759
+ getLinkPosition(linkName: string): Vec3
760
+ ```
761
+
762
+ #### `getFrame()` — Return the solved world transform for a named rig frame.
763
+
764
+ ```ts
765
+ getFrame(frameName: string): Transform
766
+ ```
767
+
768
+ #### `frames()` — Return solved rig frames, including origin, axis, up, and transform.
769
+
770
+ ```ts
771
+ get frames(): SolvedAssemblyFrameDef[]
772
+ ```
773
+
648
774
  #### `getTransform()` — Return the world-space [`Transform`](/docs/core#transform) for the named part at the solved pose.
649
775
 
650
776
  ```ts