forgecad 0.9.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/assets/{AdminPage-CDyGUinA.js → AdminPage-DwYHz72L.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DfPMY_-d.js → BenchmarkPage-a9_f-1US.js} +1 -1
  3. package/dist/assets/{BlogPage-kF0fkdJT.js → BlogPage-DodHpvmf.js} +1 -1
  4. package/dist/assets/{DocsPage-B954L3YN.js → DocsPage-B5LePEuj.js} +8 -858
  5. package/dist/assets/{EditorApp-CuDLxKqL.css → EditorApp-BpjZgzk0.css} +148 -0
  6. package/dist/assets/EditorApp-QXsAISLR.js +16307 -0
  7. package/dist/assets/{EmbedViewer-C77B-TrF.js → EmbedViewer-DdEHGUMU.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-Cr6fXMDj.js → LandingPageProofDriven-yhhOodbf.js} +2 -2
  9. package/dist/assets/{LegalPage-Dzklqmmg.js → LegalPage-5RbKRGYK.js} +1 -1
  10. package/dist/assets/{PricingPage-zWXkvlwl.js → PricingPage-E3Rma7aV.js} +1 -1
  11. package/dist/assets/{SettingsPage-Bz0of4KQ.js → SettingsPage-BJZcM97j.js} +1 -1
  12. package/dist/assets/{app-D3kDkggg.js → app-DSYrDg0V.js} +1846 -352
  13. package/dist/assets/cli/{render-DSY3mMQa.js → render-ZMHR9HkV.js} +161 -70
  14. package/dist/assets/{constructionHistoryWorker-gpDo-uH2.js → constructionHistoryWorker-AwMMWSxg.js} +1104 -349
  15. package/dist/assets/{evalWorker-CU0Ke6DP.js → evalWorker-DbNs7Dkp.js} +5155 -3772
  16. package/dist/assets/{inspectWorker-COyp8XXA.js → inspectWorker-CZsCFtQT.js} +1415 -439
  17. package/dist/assets/{targets-B9sGB5nB.js → jointPose-DO6mnXn_.js} +71 -3
  18. package/dist/assets/{manifold-DNkrUWpA.js → manifold-BGlQBBH9.js} +1 -1
  19. package/dist/assets/{manifold-BRI5prcH.js → manifold-BU-tJwQh.js} +1 -1
  20. package/dist/assets/{manifold-C-3h2M7p.js → manifold-fy2MV7K1.js} +2 -2
  21. package/dist/assets/{reportWorker-CdBz5bNg.js → reportWorker-DO6hcQbh.js} +8474 -4549
  22. package/dist/assets/{scalar-sampling-budget-wJF98aY9.js → scalar-sampling-budget-o90NSNmF.js} +5347 -3906
  23. package/dist/assets/{scanProxyWorker-B-9VbLIs.js → scanProxyWorker-2GtDLk-R.js} +19 -6
  24. package/dist/assets/{javascript-1kQXfVaz.js → typescript-DBQ6RN5l.js} +874 -22
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +3 -3
  27. package/dist/docs-raw/AI/usage.md +3 -1
  28. package/dist/docs-raw/CLI.md +65 -239
  29. package/dist/docs-raw/README.md +6 -0
  30. package/dist/docs-raw/component-model.md +17 -150
  31. package/dist/docs-raw/generated/assembly.md +159 -520
  32. package/dist/docs-raw/generated/concepts.md +245 -3491
  33. package/dist/docs-raw/generated/core.md +277 -1251
  34. package/dist/docs-raw/generated/curves.md +387 -1608
  35. package/dist/docs-raw/generated/legacy.md +162 -0
  36. package/dist/docs-raw/generated/lib.md +238 -112
  37. package/dist/docs-raw/generated/output.md +51 -76
  38. package/dist/docs-raw/generated/runtime-names.md +30 -22
  39. package/dist/docs-raw/generated/sdf.md +68 -284
  40. package/dist/docs-raw/generated/sheet-metal.md +68 -335
  41. package/dist/docs-raw/generated/sketch.md +240 -1161
  42. package/dist/docs-raw/generated/viewport.md +75 -316
  43. package/dist/docs-raw/generated/wood.md +21 -49
  44. package/dist/docs-raw/guides/coordinate-system.md +4 -42
  45. package/dist/docs-raw/guides/inspection-bundles.md +44 -442
  46. package/dist/docs-raw/guides/joint-design.md +18 -79
  47. package/dist/docs-raw/guides/positioning.md +21 -143
  48. package/dist/docs-raw/guides/scene-presentation.md +89 -0
  49. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +25 -111
  50. package/dist/docs-raw/skills/forgecad-blockout-model.md +20 -117
  51. package/dist/docs-raw/skills/forgecad-component-model.md +23 -107
  52. package/dist/docs-raw/skills/forgecad-high-level-spec.md +47 -155
  53. package/dist/docs-raw/skills/forgecad-image-replicator.md +26 -143
  54. package/dist/docs-raw/skills/forgecad-lld.md +19 -113
  55. package/dist/docs-raw/skills/forgecad-make-a-model.md +113 -532
  56. package/dist/docs-raw/skills/forgecad-model-grader.md +38 -108
  57. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +24 -211
  58. package/dist/docs-raw/skills/forgecad-project.md +13 -129
  59. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +42 -134
  60. package/dist/docs-raw/skills/forgecad-render-inspect.md +27 -174
  61. package/dist/docs-raw/skills/forgecad-visual-spec.md +32 -112
  62. package/dist/docs-raw/skills/forgecad.md +19 -18
  63. package/dist/docs-raw/skills/index.md +2 -0
  64. package/dist/docs-raw/welcome.md +4 -2
  65. package/dist/index.html +1 -1
  66. package/dist/llms.txt +1 -2
  67. package/dist/sitemap.xml +13 -13
  68. package/dist-cli/{check-compiler-SDX5QIXI.js → check-compiler-JTVBITCR.js} +1 -1
  69. package/dist-cli/{check-query-propagation-EAYEFT77.js → check-query-propagation-3FFLSMVN.js} +1 -1
  70. package/dist-cli/{chunk-N4O47JLF.js → chunk-OAN5T4XD.js} +5722 -4287
  71. package/dist-cli/forgecad.js +2195 -656
  72. package/dist-skill/CONTEXT.md +1778 -7912
  73. package/dist-skill/SKILL.md +15 -15
  74. package/dist-skill/docs/API/core/concepts.md +27 -157
  75. package/dist-skill/docs/CLI.md +65 -239
  76. package/dist-skill/docs/generated/assembly.md +160 -493
  77. package/dist-skill/docs/generated/core.md +277 -1251
  78. package/dist-skill/docs/generated/curves.md +387 -1609
  79. package/dist-skill/docs/generated/lib.md +238 -112
  80. package/dist-skill/docs/generated/output.md +51 -76
  81. package/dist-skill/docs/generated/runtime-names.md +16 -22
  82. package/dist-skill/docs/generated/sdf.md +68 -284
  83. package/dist-skill/docs/generated/sheet-metal.md +68 -335
  84. package/dist-skill/docs/generated/sketch.md +240 -1160
  85. package/dist-skill/docs/generated/viewport.md +75 -223
  86. package/dist-skill/docs/generated/wood.md +21 -49
  87. package/dist-skill/docs/guides/coordinate-system.md +4 -42
  88. package/dist-skill/docs/guides/inspection-bundles.md +44 -442
  89. package/dist-skill/docs/guides/joint-design.md +18 -79
  90. package/dist-skill/docs/guides/positioning.md +21 -143
  91. package/dist-skill/docs/guides/scene-presentation.md +89 -0
  92. package/dist-skill/docs/guides/surface-members.md +26 -0
  93. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +23 -111
  94. package/dist-skill/library/forgecad-blockout-model/SKILL.md +18 -117
  95. package/dist-skill/library/forgecad-component-model/SKILL.md +21 -107
  96. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +45 -155
  97. package/dist-skill/library/forgecad-image-replicator/SKILL.md +24 -143
  98. package/dist-skill/library/forgecad-lld/SKILL.md +17 -113
  99. package/dist-skill/library/forgecad-make-a-model/SKILL.md +111 -532
  100. package/dist-skill/library/forgecad-model-grader/SKILL.md +36 -108
  101. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +35 -224
  102. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +43 -271
  103. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +30 -99
  104. package/dist-skill/library/forgecad-project/SKILL.md +13 -131
  105. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +29 -123
  106. package/dist-skill/library/forgecad-render-inspect/SKILL.md +25 -174
  107. package/dist-skill/library/forgecad-visual-spec/SKILL.md +30 -111
  108. package/dist-skill/website/skills/forgecad-3d-reconstruction.md +58 -0
  109. package/dist-skill/website/skills/forgecad-blockout-model.md +49 -0
  110. package/dist-skill/website/skills/forgecad-component-model.md +53 -0
  111. package/dist-skill/website/skills/forgecad-high-level-spec.md +101 -0
  112. package/dist-skill/website/skills/forgecad-image-replicator.md +63 -0
  113. package/dist-skill/website/skills/forgecad-lld.md +41 -0
  114. package/dist-skill/website/skills/forgecad-make-a-model.md +186 -0
  115. package/dist-skill/website/skills/forgecad-model-grader.md +82 -0
  116. package/dist-skill/website/skills/forgecad-prepare-prompt.md +63 -0
  117. package/dist-skill/website/skills/forgecad-project.md +26 -0
  118. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +60 -0
  119. package/dist-skill/website/skills/forgecad-render-inspect.md +80 -0
  120. package/dist-skill/website/skills/forgecad-visual-spec.md +71 -0
  121. package/dist-skill/website/skills/forgecad.md +122 -0
  122. package/dist-skill/website/skills/index.md +26 -0
  123. package/examples/api/comparison-imported-sphere-candidate.forge.js +1 -1
  124. package/examples/api/conformal-product-ribbon.forge.js +1 -1
  125. package/examples/api/exact-sheet-shell-assembly.forge.js +1 -1
  126. package/examples/api/extrude-options.forge.js +4 -2
  127. package/examples/api/field-loft-drive-tip.forge.js +40 -0
  128. package/examples/api/guided-loft-olive-oil-bottle.forge.js +1 -1
  129. package/examples/api/helix-basics.forge.js +2 -2
  130. package/examples/api/highlight-debug.forge.js +10 -10
  131. package/examples/api/mesh-import-slats.forge.js +1 -1
  132. package/examples/api/real-product-curves.forge.js +1 -1
  133. package/examples/api/route3d-elbow.forge.js +3 -0
  134. package/examples/api/sculpt-box-circle-booleans.forge.js +1 -1
  135. package/examples/api/sdf-shapes.forge.js +2 -5
  136. package/examples/api/sketch-rounding-strategies.forge.js +6 -6
  137. package/examples/api/surface-member-bottle-cage.forge.js +3 -3
  138. package/examples/api/surface-member-conformal-product-ribbon.forge.js +3 -3
  139. package/examples/api/surface-member-razor-inlay.forge.js +1 -1
  140. package/examples/api/variable-sweep-test.forge.js +4 -2
  141. package/examples/mechanical/airplane-propeller.forge.js +74 -39
  142. package/examples/nurbs-surface.forge.js +1 -1
  143. package/examples/products/iphone.forge.js +1 -1
  144. package/package.json +4 -1
  145. package/dist/assets/EditorApp-Beb-IZ0y.js +0 -14014
  146. package/dist/docs-raw/guides/geometry-conventions.md +0 -52
  147. package/dist/docs-raw/guides/modeling-recipes.md +0 -78
  148. package/dist-skill/docs/guides/geometry-conventions.md +0 -52
  149. package/dist-skill/docs/guides/modeling-recipes.md +0 -78
  150. package/dist-skill/library/forgecad-visual-spec/references/prompt-template.md +0 -79
  151. package/examples/api/bolted-service-cover.forge.js +0 -17
  152. package/examples/api/cable-gland-anchor.forge.js +0 -14
  153. package/examples/api/captured-cartridge-guide.forge.js +0 -14
  154. package/examples/api/captured-linear-slide.forge.js +0 -13
  155. package/examples/api/clevis-pin-joint.forge.js +0 -13
  156. package/examples/api/datum-enclosure.forge.js +0 -16
  157. package/examples/api/hose-barb-port.forge.js +0 -14
  158. package/examples/api/knuckled-hinge-assembly.forge.js +0 -15
  159. package/examples/api/living-hinge-cover.forge.js +0 -14
  160. package/examples/api/pcb-terminal-block.forge.js +0 -22
  161. package/examples/api/pinned-lever-pivot-stack.forge.js +0 -14
  162. package/examples/api/retained-shaft-knob-stack.forge.js +0 -15
  163. package/examples/api/routed-tube-clip.forge.js +0 -15
  164. package/examples/api/seated-bearing-stack.forge.js +0 -30
  165. package/examples/api/snap-latch-cover.forge.js +0 -14
  166. package/examples/api/thumb-screw-clamp.forge.js +0 -15
@@ -9,52 +9,33 @@ Assembly-owned links, constraints, connectors, solved poses, and robot export.
9
9
 
10
10
  ## Contents
11
11
 
12
- - [Assembly & Joints](#assembly-joints) — `bomToCsv`, `assembly`
12
+ - [Assembly & Joints](#assembly-joints)
13
13
  - [Assembly](#assembly) — Kinematics, Structure, Connectors, References, Solving
14
14
  - [ImportedAssembly](#importedassembly)
15
15
  - [SolvedAssembly](#solvedassembly)
16
- - [MateBuilder](#matebuilder)
17
16
 
18
17
  ## Functions
19
18
 
20
19
  ### Assembly & Joints
21
20
 
22
- #### `bomToCsv()` — Convert an array of BOM rows into a CSV string.
23
-
24
- Produces a CSV with columns: `part`, `qty`, `material`, `process`, `tolerance`, `notes`. String values are quoted and internal double-quotes are escaped. Prefer calling `solvedAssembly.bomCsv()` directly — this function is exposed for custom BOM processing.
25
-
26
- ```ts
27
- bomToCsv(rows: BomRow[]): string
28
- ```
29
-
30
- **`BomRow`**: `part: string`, `qty: number`, `material?: string`, `process?: string`, `tolerance?: string`, `notes?: string`, `metadata?: PartMetadata`
31
-
32
- **`PartMetadata`**
33
-
34
- | Option | Type | Description |
35
- |--------|------|-------------|
36
- | `tags?` | `string \| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |
37
- | `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |
38
-
39
- #### `assembly()` — Create an assembly container with named parts, connectors, and kinematic links.
21
+ #### `assembly(name?: string): Assembly` — Create an assembly container with named parts, connectors, and kinematic links.
40
22
 
41
23
  **Use this from iteration 1 for any model with moving parts.** Do not build one static pose and retrofit motion later.
42
24
 
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.
25
+ Two motion tools:
44
26
 
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.
27
+ - **Link-graph kinematics** (`link()`, `edgeBetweenLinks()`, `addAngleBetweenLinks()`) solve named point positions a link is a point, not a rigid-body frame. Use when the hard part is solving positions, especially closed loops.
28
+ - **Connector-frame joints** (`connect()` / `match()`) align full connector frames (`origin`, `axis`, `up`) and derive joint frame + axis. Use for serial articulated parts whose orientation matters: hips, hinges, drums, sliders, wheels.
46
29
 
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.
30
+ `addPart(..., { mate })` places geometry on the solved link graph by **translation only**: one mate pins a connector origin to a link, two mates orient a part to span two solved links, a third pins roll. Right for markers and point-following geometry; use `connect()`/`match()` when the part needs a deterministic rest orientation.
48
31
 
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.
32
+ Return the `Assembly` itself to expose its joints and driven link controls in the editor; moving a control re-runs `solve(state)`, so closed loops move through the real solver instead of a viewport-only FK approximation.
50
33
 
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.
52
-
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.
34
+ If no link in a connected kinematic component is fixed, ForgeCAD chooses a deterministic gauge link for solving and reports a floating-component warning.
54
35
 
55
- **Point-link example**
36
+ A file that returns an `Assembly` is importable via [`require()`](/docs/core#require) and yields an `ImportedAssembly`; use `mergeInto()` to flatten it into a parent assembly.
56
37
 
57
- This snippet mates a marker to the solved `tip` point. It does not orient a bar along `ground -> tip`.
38
+ **Point-link example** (mates a marker to the solved `tip` point; does not orient a bar along `ground -> tip`):
58
39
 
59
40
  ```ts
60
41
  const marker = box(8, 8, 4).withConnectors({
@@ -75,83 +56,15 @@ const mech = assembly("Linkage")
75
56
  return mech;
76
57
  ```
77
58
 
78
- ```ts
79
- assembly(name?: string): Assembly
80
- ```
81
-
82
59
  ---
83
60
 
84
61
  ## Classes
85
62
 
86
63
  ### `Assembly`
87
64
 
88
- Container for a kinematic mechanism made up of links, relationships, and parts.
89
-
90
- Assembly has two related but different motion tools:
91
-
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.
65
+ Container for a kinematic mechanism made up of links, relationships, and parts. See `assembly` for the link-graph vs connector-frame decision rules.
94
66
 
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.
96
-
97
- **Point-link quick start**
98
-
99
- This attaches a marker to a solved point. It is intentionally not a bone or oriented part example.
100
-
101
- ```ts
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" },
117
- });
118
-
119
- return mech;
120
- ```
121
-
122
- Returning an unsolved `Assembly` keeps the graph available to the runtime. Return a `SolvedAssembly` directly for a specific control state:
123
-
124
- ```ts
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 });
154
- ```
67
+ Returning an unsolved `Assembly` keeps the graph available to the runtime; return `mech.solve({ theta: 60 })` for a fixed pose instead.
155
68
 
156
69
  **Return types**
157
70
 
@@ -168,54 +81,61 @@ return assembly("Leg")
168
81
 
169
82
  **Kinematics**
170
83
 
171
- #### `link()` — Add a named kinematic link to the assembly graph.
84
+ #### `link(name: string, options?: AssemblyLinkOptions): Assembly` — Add a named kinematic link to the assembly graph.
172
85
 
173
86
  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
87
 
175
88
  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
89
 
177
- ```ts
178
- link(name: string, options?: AssemblyLinkOptions): Assembly
179
- ```
180
-
181
90
  **`AssemblyLinkOptions`**
182
- - `at?: [ number, number, number ]` — Initial world-space position of this link before kinematic constraints solve it.
91
+ - `at?: Vec3` — Initial world-space position of this link before kinematic constraints solve it.
183
92
  - `fixed?: boolean` — Keep the link locked at its authored `at` position during solves.
184
93
  - `metadata?: Record<string, unknown>` — User metadata carried through the kinematic graph for inspection and tooling.
185
94
 
186
- #### `edgeBetweenLinks()` — Add a relationship edge between two kinematic links.
95
+ #### `linkAlong(name: string, fromLink: string, towardLink: string, distance: number): Assembly` — Create a derived link on the line through `fromLink` and `towardLink`, at a **signed** distance from `fromLink`.
187
96
 
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.
97
+ **Sign convention** (read this first):
98
+
99
+ - `distance > 0` — the point moves from `fromLink` **toward** `towardLink`.
100
+ - `distance < 0` — the point moves from `fromLink` **away from** `towardLink` (the coupler-extension case, e.g. the Chebyshev lambda linkage's trace point beyond the rocker joint).
101
+ - `distance` greater than the solved edge length places the point **beyond** `towardLink`, still on the same line.
102
+
103
+ Derived links are trace/reference points. They are recomputed after the primary link solve and cannot participate in structural edges or angle constraints. Because the distance is one signed parameter, a `param()`-driven value can sweep continuously from extension (negative) through `fromLink` (zero) to beyond `towardLink` (large positive).
189
104
 
190
105
  ```ts
191
- edgeBetweenLinks(a: string, b: string, options?: AssemblyEdgeBetweenLinksOptions): Assembly
106
+ // Chebyshev lambda linkage: trace point C3 extends beyond C2, away from C1.
107
+ mech.linkAlong('C3', 'C2', 'C1', -2.5 * a);
108
+ // Midpoint-style reference 30 mm from A toward B:
109
+ mech.linkAlong('probe', 'A', 'B', 30);
192
110
  ```
193
111
 
112
+ #### `edgeBetweenLinks(a: string, b: string, options?: AssemblyEdgeBetweenLinksOptions): Assembly` — Add a relationship edge between two kinematic links.
113
+
114
+ 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.
115
+
194
116
  **`AssemblyEdgeBetweenLinksOptions`**: `name?: string`, `length?: number | "lockCurrent" | "free"`, `min?: number`, `max?: number`, `visualOnly?: boolean`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
195
117
 
196
118
  `AssemblyKinematicControlOptions`: `{ min?: number, max?: number, default?: number, unit?: string }`
197
119
 
198
- #### `addAngleBetweenLinks()` — Add an angle relationship among three kinematic links.
120
+ #### `addAngleBetweenLinks(a: string, b: string, c: string, options?: AssemblyAngleBetweenLinksOptions): Assembly` — Add an angle relationship among three kinematic links.
199
121
 
200
122
  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
123
 
202
- ```ts
203
- addAngleBetweenLinks(a: string, b: string, c: string, options?: AssemblyAngleBetweenLinksOptions): Assembly
204
- ```
205
-
206
124
  **`AssemblyAngleBetweenLinksOptions`**: `name?: string`, `value?: number`, `min?: number`, `max?: number`, `control?: boolean | AssemblyKinematicControlOptions`, `limit?: AssemblyKinematicLimitOptions`, `metadata?: Record<string, unknown>`
207
125
 
208
126
  `AssemblyKinematicLimitOptions`: `{ min?: number, max?: number }`
209
127
 
210
- #### `describeKinematics()` — Return the assembly-native kinematic graph definition.
128
+ #### `addAngleBetweenLinkSegmentAndWorldDirection(fromLink: string, toLink: string, direction: Vec3, options?: AssemblyAngleBetweenLinksOptions): Assembly` — Add an absolute angle relationship from a world direction to a link segment.
211
129
 
212
- ```ts
213
- describeKinematics(): AssemblyKinematicGraphDef
214
- ```
130
+ 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.
131
+
132
+ Use `Points.polar(1, angleDeg)` when the reference direction is planar and angle-based instead of axis-aligned.
133
+
134
+ #### `describeKinematics(): AssemblyKinematicGraphDef` — Return the assembly-native kinematic graph definition.
215
135
 
216
136
  **Structure**
217
137
 
218
- #### `addPart()` — Add a named part to the assembly.
138
+ #### `addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly` — Add a named part to the assembly.
219
139
 
220
140
  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.
221
141
 
@@ -231,114 +151,102 @@ const housing = group(
231
151
  assembly.addPart("Base Assembly", housing);
232
152
  ```
233
153
 
234
- ```ts
235
- addPart(name: string, part: AssemblyPart, options?: PartOptions): Assembly
236
- ```
154
+ **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
237
155
 
238
- `PartOptions`: `{ transform?: TransformInput, metadata?: PartMetadata, mate?: AssemblyPartMateInput | AssemblyPartMateInput[] }`
156
+ **`PartMetadata`**
157
+
158
+ | Option | Type | Description |
159
+ |--------|------|-------------|
160
+ | `tags?` | `string \| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |
161
+
162
+ Also: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`.
239
163
 
240
164
  **`AssemblyPartMateInput`**
241
165
  - `connector: string` — Name of a connector declared on the part (via `withConnectors()`).
242
166
  - `toLink: string` — Name of the link this connector's origin is pinned to.
243
167
  - `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).
244
168
 
245
- #### `addFrame()` — Add a virtual reference frame (no geometry) to the assembly graph.
169
+ #### `frame(name: string, options: AssemblyFrameOptions): Assembly` — Add a named rig frame to the assembly.
246
170
 
247
- 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.
171
+ 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.
248
172
 
249
- ```ts
250
- addFrame(name: string, options?: PartOptions): Assembly
251
- ```
173
+ `AssemblyFrameOptions`: `{ origin: Vec3, axis: Vec3, up: Vec3, fixed?: boolean, metadata?: Record<string, unknown> }`
252
174
 
253
- **Connectors**
175
+ #### `fixedJoint(name: string, options: AssemblyFixedFrameJointOptions): Assembly` — Rigidly attach a child rig frame to a parent rig frame.
254
176
 
255
- #### `usedConnectorRefs()` Connector refs (e.g. "PartName.connectorName") consumed by connect/match calls.
177
+ Fixed joints carry frame hierarchy but do not expose a Motion control.
256
178
 
257
- ```ts
258
- get usedConnectorRefs(): ReadonlySet<string>
259
- ```
179
+ `AssemblyFixedFrameJointOptions`: `{ parent: string, child: string, metadata?: Record<string, unknown> }`
260
180
 
261
- #### `withConnectors()` — Attach named connectors to a specific part or the assembly as a whole.
181
+ #### `revoluteJoint(name: string, options: AssemblyMovingFrameJointOptions): Assembly` — Add a revolute rig-frame joint.
262
182
 
263
- Connectors declared this way are in the part's local coordinate system. They are captured automatically if the incoming [`Shape`](/docs/core#shape) already has connectors via `shape.withConnectors(...)`, but you can also add or override connectors after the fact with this method.
183
+ 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.
264
184
 
265
- Use the single-argument overload to attach assembly-level connectors these are exposed when this assembly is imported as a sub-assembly.
185
+ **`AssemblyMovingFrameJointOptions`**: `parent: string`, `child: string`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `control?: boolean`, `metadata?: Record<string, unknown>`
266
186
 
267
- ```ts
268
- withConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly
269
- ```
187
+ #### `prismaticJoint(name: string, options: AssemblyMovingFrameJointOptions): Assembly` — Add a prismatic rig-frame joint.
270
188
 
271
- **`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`
189
+ 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.
272
190
 
273
- `ConnectorInput`: `{ connectorType?: string, gender?: ConnectorGender, measurements?: Record<string, number | string> }`
191
+ **Connectors**
274
192
 
275
- #### `getConnectors()`Get connectors declared on a part in part-local space.
193
+ #### `get usedConnectorRefs(): ReadonlySet<string>` Connector refs (e.g. "PartName.connectorName") consumed by connect/match calls.
276
194
 
277
- ```ts
278
- getConnectors(partName: string): ConnectorMap
279
- ```
195
+ #### `withConnectors(partName: string, connectors: Record<string, ConnectorInput>): Assembly` — Attach named connectors to a specific part or the assembly as a whole.
280
196
 
281
- #### `getConnector()` Parse a "PartName.connectorName" reference and return the resolved connector. Throws descriptive errors if the part or connector doesn't exist.
197
+ Connectors declared this way are in the part's local coordinate system. They are captured automatically if the incoming [`Shape`](/docs/core#shape) already has connectors via `shape.withConnectors(...)`, but you can also add or override connectors after the fact with this method.
282
198
 
283
- ```ts
284
- getConnector(ref: string): { partName: string; connectorName: string; connector: ConnectorDef; }
285
- ```
199
+ Use the single-argument overload to attach assembly-level connectors — these are exposed when this assembly is imported as a sub-assembly.
286
200
 
287
- #### `connect()` — Connect two parts by aligning their declared connectors, automatically computing frame and axis.
201
+ `ConnectorInput` — defined in [core](/docs/core).
288
202
 
289
- 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.
203
+ #### `getConnectors(partName: string): ConnectorMap` Get connectors declared on a part in part-local space.
290
204
 
291
- 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.
205
+ #### `getConnector(ref: string): { partName: string; connectorName: string; connector: ConnectorDef; }` Parse a "PartName.connectorName" reference and return the resolved connector. Throws descriptive errors if the part or connector doesn't exist.
292
206
 
293
- **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()`.
207
+ #### `connect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly` Connect two parts by aligning their declared connectors, automatically computing frame and axis.
294
208
 
295
- 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.
209
+ Connector refs use `"PartName.connectorName"`. The child connector origin lands exactly on the parent connector origin; joint frame and axis are derived from the connector geometry no manual `frame`/`axis` math.
296
210
 
297
- **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.
211
+ Frame semantics: `origin` is the pivot/contact point, `axis` the hinge or slide direction, `up` locks the part's zero-state twist. Omitted `up` gets a deterministic perpendicular provide `up` whenever rest orientation matters. (`addPart(..., { mate })` translates only; see `addPart`.)
298
212
 
299
- The joint type is inferred from the connector's `kind` field if not specified in `options`.
213
+ **Face-to-face:** each connector's axis points outward from its part; mating makes the axes anti-parallel, like a plug meeting a socket (same convention as `matchTo()`).
300
214
 
301
- When connectors are defined with `start`/`end`, you can control which point on each connector meets via `align` / `parentAlign` / `childAlign` (`'start'`, `'middle'`, `'end'`).
215
+ **Mirrored revolute axes:** revolute values follow the right-hand rule, so a mirrored hinge axis (`[1, 0, 0]` vs `[-1, 0, 0]`) rotates oppositely for the same `+theta`: negate the mirrored side's value and mirror limits as `[min, max] -> [-max, -min]`. Prismatic joints have no handedness flip. Use an explicit per-side sign mapping (or side-neutral link controls) for bilateral mechanisms.
302
216
 
303
- Use `connect()` when connector origins must physically coincide (flange-to-flange, bolt-into-bore).
217
+ Joint type defaults to the connector's `kind`. For `start`/`end` connectors, `align` / `parentAlign` / `childAlign` (`'start' | 'middle' | 'end'`) choose which point meets.
304
218
 
305
219
  ```ts
306
- // Hinge: both axes point outward along the hinge line
307
220
  const frame = box(100, 10, 80).withConnectors({
308
- hinge: connector("hinge", {
309
- origin: [0, 0, 40],
310
- axis: [0, 0, 1],
311
- up: [1, 0, 0],
312
- }),
221
+ hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, 1], up: [1, 0, 0] }),
313
222
  });
314
223
  const door = box(60, 4, 80).withConnectors({
315
- hinge: connector("hinge", {
316
- origin: [0, 0, 40],
317
- axis: [0, 0, -1],
318
- up: [1, 0, 0],
319
- }),
224
+ hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, -1], up: [1, 0, 0] }),
320
225
  });
321
- assembly("Door")
322
- .addPart("Frame", frame)
323
- .addPart("Door", door)
226
+ assembly("Door").addPart("Frame", frame).addPart("Door", door)
324
227
  .connect("Frame.hinge", "Door.hinge", { as: "swing", min: 0, max: 110 });
325
228
  ```
326
229
 
327
- ```ts
328
- connect(parentConnectorRef: string, childConnectorRef: string, options?: ConnectOptions): Assembly
329
- ```
330
-
331
230
  **`ConnectOptions`**
332
231
 
333
232
  | Option | Type | Description |
334
233
  |--------|------|-------------|
234
+ | `min?` | `number` | Lower joint-slider limit; solve clamps to it with a warning. Not a physical stop — enforce real travel limits with stop geometry. |
235
+ | `max?` | `number` | Upper joint-slider limit; same semantics as `min`. |
335
236
  | `flip?` | `boolean` | This parameter is ignored. If your connectors produce wrong orientation, fix the connector axis directions instead of using flip. |
336
237
  | `parentAlign?` | `PortAlign` | Which point on the parent connector to align: 'start', 'middle' (default), or 'end'. |
337
238
  | `childAlign?` | `PortAlign` | Which point on the child connector to align: 'start', 'middle' (default), or 'end'. |
338
239
  | `align?` | `PortAlign` | Shorthand: set both parentAlign and childAlign at once. |
339
- | `as?`, `type?`, `min?`, `max?`, `default?`, `unit?`, `effort?`, `velocity?`, `damping?`, `friction?` | | |
240
+ | `follows?` | `JointFollowOptions` | Slave this joint to another joint: `value = ratio × source + offset` (e.g. a mirrored jaw with `ratio: -1`). |
241
+
242
+ Also: `as?: string`, `type?: JointType`, `default?: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`.
243
+
244
+ **`JointFollowOptions`**
245
+ - `joint: string` — Name of the source joint that drives this one.
246
+ - `ratio?: number` — Multiplier applied to the source joint value (default 1).
247
+ - `offset?: number` — Constant added after the ratio (default 0).
340
248
 
341
- #### `match()` — Auto-create a joint by matching typed connectors between two parts.
249
+ #### `match(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & { as?: string; }): Assembly` — Auto-create a joint by matching typed connectors between two parts.
342
250
 
343
251
  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.
344
252
 
@@ -363,29 +271,17 @@ const mech = assembly("Door")
363
271
  // Matching connectors computes the placement relationship automatically.
364
272
  ```
365
273
 
366
- ```ts
367
- match(childPartName: string, parentPartName: string, pairs: Record<string, string>, options?: MatchToOptions & { as?: string; }): Assembly
368
- ```
369
-
370
- `MatchToOptions`: `{ force?: boolean, angle?: number, distance?: number }`
274
+ `MatchToOptions` — defined in [core](/docs/core).
371
275
 
372
276
  **References**
373
277
 
374
- #### `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.
375
-
376
- ```ts
377
- withReferences(refs: Pick<PlacementReferenceInput, "points">): Assembly
378
- ```
379
-
380
- **`PlacementReferenceInput`**: `points?: Record<string, [ number, number, number ]>`, `edges?: Record<string, PlacementEdgeRef>`, `surfaces?: Record<string, PlacementSurfaceRef>`, `objects?: Record<string, PlacementObjectInput>`
278
+ #### `withReferences(refs: Pick<PlacementReferenceInput, "points">): Assembly` — 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.
381
279
 
382
- `PlacementEdgeRef`: `{ start: Vec3, end: Vec3 }`
383
-
384
- `PlacementSurfaceRef`: `{ center: Vec3, normal: Vec3 }`
280
+ `PlacementReferenceInput` defined in [core](/docs/core).
385
281
 
386
282
  **Solving**
387
283
 
388
- #### `solve()` — Solve the assembly at the given control state and return positioned parts.
284
+ #### `solve(state?: JointState): SolvedAssembly` — Solve the assembly at the given control state and return positioned parts.
389
285
 
390
286
  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.
391
287
 
@@ -401,36 +297,60 @@ Connector-frame joints created by `connect()` / `match()` are also evaluated; th
401
297
  return mech.solve({ theta: 45 });
402
298
  ```
403
299
 
404
- ```ts
405
- solve(state?: JointState): SolvedAssembly
406
- ```
407
-
408
300
  **Other**
409
301
 
410
- #### `mate()` Register mate constraints between parts. Constraints are solved during `solve()` to derive part positions and explode hints. Part references use "partName:featureName" format.
302
+ #### `edgeBetweenFrames(a: string, b: string, options?: AssemblyFrameEdgeOptions): Assembly` Add a visual skeleton edge between two rig frame origins.
411
303
 
412
- ```ts
413
- mate(fn: (m: MateBuilder) => void): Assembly
414
- ```
304
+ 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.
305
+
306
+ `AssemblyFrameEdgeOptions`: `{ name?: string, metadata?: Record<string, unknown> }`
415
307
 
416
- #### `describe()` — Return the serializable assembly definition used by solve/inspect pipelines.
308
+ #### `addAnimation(name: string, options: AssemblyAnimationOptions): Assembly` — Register a named keyframe animation for this assembly's Motion view.
309
+
310
+ Works with the returned-assembly controls path: return the unsolved `Assembly` and the animation appears in the Motion tab alongside the solver-backed joint controls. Keyframes hold control values by joint name; joints declared with `follows` are derived automatically and must not appear in keyframes.
417
311
 
418
312
  ```ts
419
- describe(): AssemblyDefinition
313
+ robot.addAnimation("Pick and place", {
314
+ duration: 12,
315
+ loop: true,
316
+ keyframes: [
317
+ { values: { J1: 0, J2: -90 } },
318
+ { values: { J1: 45, J2: -30 } },
319
+ { values: { J1: 0, J2: -90 } },
320
+ ],
321
+ });
322
+ return robot;
420
323
  ```
421
324
 
325
+ **`AssemblyAnimationOptions`**
326
+
327
+ | Option | Type | Description |
328
+ |--------|------|-------------|
329
+ | `duration?` | `number` | Animation length in seconds (default chosen by the viewer). |
330
+ | `loop?` | `boolean` | Loop the animation (default false). |
331
+ | `continuous?` | `boolean` | Interpolate continuously through keyframes instead of pausing on each. |
332
+ | `keyframes` | `JointViewAnimationInput["keyframes"]` | Keyframes of control values by joint/control name. `at` (0..1) or `ticks` control timing. |
333
+ | `default?` | `boolean` | Make this the animation that plays when the model loads. |
334
+
335
+ `JointViewAnimationInput`: `{ name: string, duration?: number, loop?: boolean, continuous?: boolean, keyframes: JointViewAnimationKeyframeInput[] }`
336
+
337
+ **`JointViewAnimationKeyframeInput`**
338
+ - `at?: number` — Timeline position [0, 1]. If omitted from ALL keyframes, positions are auto-computed from tick weights.
339
+ - `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.
340
+ - Also: `values: Record<string, number>`.
341
+
342
+ #### `describe(): AssemblyDefinition` — Return the serializable assembly definition used by solve/inspect pipelines.
343
+
422
344
  **Compatibility Aliases**
423
345
 
424
- - `usedPortRefs` -> `usedConnectorRefs`
425
346
  - `withPorts()` -> `withConnectors()`
426
347
  - `getPorts()` -> `getConnectors()`
427
- - `getPort()` -> `getConnector()`
428
348
 
429
349
  ### `ImportedAssembly`
430
350
 
431
351
  A wrapper around an imported `Assembly` that provides kinematic access and convenient transform helpers.
432
352
 
433
- 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.
353
+ 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()` — and converts to a static [`ShapeGroup`](/docs/core#shapegroup) via the explicit `toGroup(state?)` boundary when group-style transforms are needed.
434
354
 
435
355
  **Kinematic access**
436
356
 
@@ -438,14 +358,14 @@ When a `.forge.js` file returns an unsolved `Assembly`, [`require()`](/docs/core
438
358
  const arm = require("./arm.forge.js");
439
359
 
440
360
  const solved = arm.solve({ shoulder: 45 }); // full kinematic solve
441
- const link = arm.part("Link", { shoulder: 60 }); // single part at state
361
+ const link = arm.getPart("Link", { shoulder: 60 }); // single part at state
442
362
  const group = arm.toGroup({ shoulder: 45 }); // only when ShapeGroup behavior is needed
443
363
  ```
444
364
 
445
- **Convenience transforms** (auto-solve at defaults, return [`ShapeGroup`](/docs/core#shapegroup)):
365
+ **Static positioning** convert explicitly, then transform the group (`toGroup()` solves at default joint values and discards kinematics):
446
366
 
447
367
  ```ts
448
- const positioned = arm.rotateZ(-90).translate(0, -20, 50);
368
+ const positioned = arm.toGroup().rotateZ(-90).translate(0, -20, 50);
449
369
  ```
450
370
 
451
371
  **Merging into a parent**
@@ -459,154 +379,43 @@ require("./arm.forge.js").mergeInto(robot, {
459
379
  });
460
380
  ```
461
381
 
462
- #### `assembly()` — The underlying Assembly, for advanced composition and inspection.
463
-
464
- ```ts
465
- get assembly(): Assembly
466
- ```
467
-
468
- #### `solve()` — Solve the assembly at the given joint state (defaults to each joint's default value).
469
-
470
- ```ts
471
- solve(state?: JointState): SolvedAssembly
472
- ```
473
-
474
- #### `part()` — Return a specific named part positioned at the given joint state, with any stored placement offset applied.
475
-
476
- ```ts
477
- part(name: string, state?: JointState): AssemblyPart
478
- ```
479
-
480
- #### `getPart()` — Return a specific named part positioned at the default solved pose.
382
+ #### `get assembly(): Assembly` — The underlying Assembly, for advanced composition and inspection.
481
383
 
482
- This mirrors `SolvedAssembly.getPart()` for imported assemblies. Use `solve(state).getPart(name)` when inspecting a non-default joint state.
384
+ #### `solve(state?: JointState): SolvedAssembly` Solve the assembly at the given joint state (defaults to each joint's default value).
483
385
 
484
- ```ts
485
- getPart(partName: string): AssemblyPart
486
- ```
487
-
488
- #### `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.
386
+ #### `getPart(partName: string, state?: JointState): AssemblyPart` — Return a specific named part positioned at the solved pose, with any stored placement offset applied.
489
387
 
490
- ```ts
491
- toGroup(state?: JointState): ShapeGroup
492
- ```
388
+ This mirrors `SolvedAssembly.getPart()` for imported assemblies, with one addition: any offset stored by `placeReference()` is applied, so the part lands where the imported assembly was placed. (`solve(state).getPart(name)` returns the part in the assembly's own coordinates, without that offset.)
493
389
 
494
- #### `withReferences()` — Attach named placement reference points to this assembly. Points are simple 3D coordinates (relative to the assembly's own origin). Returns a new ImportedAssembly does not mutate.
390
+ #### `toGroup(state?: JointState): ShapeGroup` — 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.
495
391
 
496
- ```ts
497
- withReferences(refs: Pick<PlacementReferenceInput, "points">): ImportedAssembly
498
- ```
392
+ #### `withReferences(refs: Pick<PlacementReferenceInput, "points">): ImportedAssembly` — Attach named placement reference points to this assembly. Points are simple 3D coordinates (relative to the assembly's own origin). Returns a new ImportedAssembly — does not mutate.
499
393
 
500
- #### `referenceNames()` — List all attached placement reference names.
394
+ #### `referenceNames(kind?: PlacementReferenceKind): string[]` — List all attached placement reference names.
501
395
 
502
- ```ts
503
- referenceNames(kind?: PlacementReferenceKind): string[]
504
- ```
396
+ #### `placeReference(ref: string, target: Vec3, offset?: Vec3): ImportedAssembly` — Translate the assembly so the named reference point lands on `target`. Returns a new ImportedAssembly — does not mutate. All point refs are translated by the same delta.
505
397
 
506
- #### `placeReference()` Translate the assembly so the named reference point lands on `target`. Returns a new ImportedAssembly does not mutate. All point refs are translated by the same delta.
398
+ #### `child(name: string): Shape | Sketch | ShapeGroup` — Solve at defaults, get a named child part from the resulting group.
507
399
 
508
- ```ts
509
- placeReference(ref: string, target: [ number, number, number ], offset?: [ number, number, number ]): ImportedAssembly
510
- ```
511
-
512
- #### `translate()` — Solve at defaults and return a translated ShapeGroup.
513
-
514
- ```ts
515
- translate(x: number, y: number, z: number): ShapeGroup
516
- ```
517
-
518
- #### `rotate()` — Solve at defaults and return a rotated ShapeGroup.
519
-
520
- ```ts
521
- rotate(axis: [ number, number, number ], angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup
522
- ```
523
-
524
- #### `rotateX()` — Solve at defaults and return a ShapeGroup rotated around X.
525
-
526
- ```ts
527
- rotateX(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup
528
- ```
529
-
530
- #### `rotateY()` — Solve at defaults and return a ShapeGroup rotated around Y.
531
-
532
- ```ts
533
- rotateY(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup
534
- ```
535
-
536
- #### `rotateZ()` — Solve at defaults and return a ShapeGroup rotated around Z.
537
-
538
- ```ts
539
- rotateZ(angleDeg: number, options?: { pivot?: [ number, number, number ]; }): ShapeGroup
540
- ```
541
-
542
- #### `scale()` — Solve at defaults and return a scaled ShapeGroup.
543
-
544
- ```ts
545
- scale(v: number | [ number, number, number ]): ShapeGroup
546
- ```
547
-
548
- #### `mirror()` — Solve at defaults and return a mirrored ShapeGroup.
549
-
550
- ```ts
551
- mirror(normal: [ number, number, number ]): ShapeGroup
552
- ```
553
-
554
- #### `color()` — Solve at defaults and return a colored ShapeGroup.
555
-
556
- ```ts
557
- color(hex: string): ShapeGroup
558
- ```
559
-
560
- #### `child()` — Solve at defaults, get a named child part from the resulting group.
561
-
562
- ```ts
563
- child(name: string): Shape | Sketch | ShapeGroup
564
- ```
565
-
566
- #### `collisionReport()` — Detect overlapping part pairs at the default solved pose.
400
+ #### `collisionReport(options?: CollisionOptions): CollisionFinding[]` — Detect overlapping part pairs at the default solved pose.
567
401
 
568
402
  This mirrors `SolvedAssembly.collisionReport()` for imported assemblies. Use `solve(state).collisionReport(options)` when inspecting a non-default joint state.
569
403
 
570
- ```ts
571
- collisionReport(options?: CollisionOptions): CollisionFinding[]
572
- ```
573
-
574
404
  `CollisionOptions`: `{ parts?: string[], ignorePairs?: Array<[ string, string ]>, minOverlapVolume?: number }`
575
405
 
576
- #### `minClearance()` — Compute the minimum gap between two parts at the default solved pose.
406
+ #### `minClearance(partA: string, partB: string, searchLength?: number): number` — Compute the minimum gap between two parts at the default solved pose.
577
407
 
578
408
  This mirrors `SolvedAssembly.minClearance()` for imported assemblies. Use `solve(state).minClearance(partA, partB, searchLength)` when inspecting a non-default joint state.
579
409
 
580
- ```ts
581
- minClearance(partA: string, partB: string, searchLength?: number): number
582
- ```
410
+ #### `mergeInto(parent: Assembly, options: MergeIntoOptions): Assembly` — Flatten this sub-assembly's parts and relationships into `parent` and wire a mount relationship.
583
411
 
584
- #### `mergeInto()` Flatten this sub-assembly's parts and relationships into `parent` and wire a mount relationship.
585
-
586
- 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:
412
+ All part, link, and legacy joint names from the sub-assembly are prefixed with `"${options.prefix}."` to avoid collisions; connectors are forwarded with the same prefix. After the merge, drive controls from the parent using the prefixed names:
587
413
 
588
414
  ```ts
589
415
  parent.solve({ "Left Arm.theta": 45, "Right Arm.theta": -20 })
590
416
  ```
591
417
 
592
- Connectors from sub-assembly parts are forwarded with the prefix.
593
-
594
- The sub-assembly must have exactly one root part before it can be merged.
595
-
596
- ```ts
597
- const robot = assembly("Robot").addPart("Chassis", chassis);
598
-
599
- require("./arm.forge.js").mergeInto(robot, {
600
- prefix: "Left Arm",
601
- mountParent: "Chassis",
602
- mountJoint: "leftMount",
603
- mountOptions: { frame: Transform.identity().translate(-70, 0, 10) },
604
- });
605
- ```
606
-
607
- ```ts
608
- mergeInto(parent: Assembly, options: MergeIntoOptions): Assembly
609
- ```
418
+ The sub-assembly must have exactly one root part before it can be merged (collapse multiple roots with `addFixed()` first). See the `ImportedAssembly` class docs for a full merge example.
610
419
 
611
420
  **`MergeIntoOptions`**
612
421
 
@@ -623,7 +432,9 @@ mergeInto(parent: Assembly, options: MergeIntoOptions): Assembly
623
432
  | Option | Type | Description |
624
433
  |--------|------|-------------|
625
434
  | `connectorRefs?` | `JointConnectorRefs` | Connector refs that define this joint contract. Usually set by `connect()` / `match()`. |
626
- | `frame?`, `origin?`, `axis?`, `min?`, `max?`, `default?`, `unit?`, `effort?`, `velocity?`, `damping?`, `friction?` | | |
435
+ | `follows?` | `JointFollowOptions` | Slave this joint to another joint: `value = ratio × source + offset`. Use for mechanisms with one physical DOF expressed through several joints — a mirrored gripper jaw (`ratio: -1`), a gear pair, a drive crank turning with its servo. A followed joint stops being an independent control: the Motion view drives it from its source, `solve()` derives its value (a direct state override is ignored with a warning), and limits still clamp the derived value. |
436
+
437
+ Also: `frame?: TransformInput`, `origin?: Vec3`, `axis?: Vec3`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`.
627
438
 
628
439
  `JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`
629
440
 
@@ -651,61 +462,23 @@ return solved;
651
462
 
652
463
  **Methods:**
653
464
 
654
- #### `warnings()` — Return any warnings generated during solve (clamped joints, unconverged mates, etc.).
655
-
656
- ```ts
657
- warnings(): string[]
658
- ```
659
-
660
- #### `getJointState()` — Return a snapshot of resolved joint values (after clamping and coupling).
661
-
662
- ```ts
663
- getJointState(): JointState
664
- ```
665
-
666
- #### `mateExplodeHints()` — Explode direction hints derived from mate constraints, or null if no mates.
667
-
668
- ```ts
669
- get mateExplodeHints(): Record<string, { direction: Vec3; }> | null
670
- ```
671
-
672
- #### `mateDof()` — Remaining degrees of freedom after mate constraints, or null if no mates.
465
+ #### `warnings(): string[]` — Return any warnings generated during solve (clamped joints, unconverged mates, etc.).
673
466
 
674
- ```ts
675
- get mateDof(): number | null
676
- ```
467
+ #### `getJointState(): JointState` — Return a snapshot of resolved joint values (after clamping and coupling).
677
468
 
678
- #### `mateConverged()` — Whether the mate constraint solver converged, or null if no mates.
469
+ #### `get kinematics(): SolvedAssemblyKinematics | null` — Solved assembly-native kinematic or frame-edge overlay data, or null when no rig overlay data was declared.
679
470
 
680
- ```ts
681
- get mateConverged(): boolean | null
682
- ```
471
+ #### `getLinkPosition(linkName: string): Vec3` — Return the solved world position of a kinematic link.
683
472
 
684
- #### `kinematics()` — Solved assembly-native kinematic graph, or null when no links were declared.
473
+ #### `getFrame(frameName: string): Transform` — Return the solved world transform for a named rig frame.
685
474
 
686
- ```ts
687
- get kinematics(): SolvedAssemblyKinematics | null
688
- ```
689
-
690
- #### `getLinkPosition()` — Return the solved world position of a kinematic link.
691
-
692
- ```ts
693
- getLinkPosition(linkName: string): Vec3
694
- ```
695
-
696
- #### `getTransform()` — Return the world-space [`Transform`](/docs/core#transform) for the named part at the solved pose.
697
-
698
- ```ts
699
- getTransform(partName: string): Transform
700
- ```
475
+ #### `get frames(): SolvedAssemblyFrameDef[]` — Return solved rig frames, including origin, axis, up, and transform.
701
476
 
702
- #### `getPart()` — Return the named part already positioned at its solved world transform.
477
+ #### `getTransform(partName: string): Transform` — Return the world-space [`Transform`](/docs/core#transform) for the named part at the solved pose.
703
478
 
704
- ```ts
705
- getPart(partName: string): AssemblyPart
706
- ```
479
+ #### `getPart(partName: string): AssemblyPart` — Return the named part already positioned at its solved world transform.
707
480
 
708
- #### `toGroup()` — Convert all solved parts into a [`ShapeGroup`](/docs/core#shapegroup) with named children.
481
+ #### `toGroup(): ShapeGroup` — Convert all solved parts into a [`ShapeGroup`](/docs/core#shapegroup) with named children.
709
482
 
710
483
  Each part becomes a named child in the group, already positioned at its solved world transform. Use this only when you specifically need a [`ShapeGroup`](/docs/core#shapegroup) for composition, [`ShapeGroup`](/docs/core#shapegroup) transforms, or named-child access. Top-level scripts can return the `SolvedAssembly` directly; do not call `toGroup()` just to make a solved assembly render.
711
484
 
@@ -714,37 +487,15 @@ const armGroup = mech.solve({ shoulder: 60 }).toGroup(); // only because we need
714
487
  return armGroup.rotateZ(90);
715
488
  ```
716
489
 
717
- ```ts
718
- toGroup(): ShapeGroup
719
- ```
720
-
721
- #### `toSceneObjects()` — Return an array of named scene objects for the viewport renderer.
490
+ #### `toSceneObjects(): Array<{ ... }>` — Return an array of named scene objects for the viewport renderer.
722
491
 
723
492
  Each part becomes `{ name, shape }` or `{ name, group: [...] }` if the part is a [`ShapeGroup`](/docs/core#shapegroup). Top-level scripts should normally return the `SolvedAssembly` directly. Use `toGroup()` when you need [`ShapeGroup`](/docs/core#shapegroup) behavior; use this method only for advanced scene-graph control where you need access to the flat per-part array with metadata.
724
493
 
725
- ```ts
726
- toSceneObjects(): Array<{ name: string; shape?: Shape; group?: Array<{ name: string; shape: Shape; tags?: string[]; }>; metadata?: PartMetadata; }>
727
- ```
728
-
729
- #### `toScene()` — Backward-compatible alias for `toSceneObjects()`.
494
+ #### `bom(): BomRow[]` — Generate a bill of materials for all parts in the solved assembly.
730
495
 
731
- ```ts
732
- toScene(): Array<{ name: string; shape?: Shape; group?: Array<{ name: string; shape: Shape; tags?: string[]; }>; metadata?: PartMetadata; }>
733
- ```
496
+ #### `bomCsv(): string` — Generate a bill of materials as a CSV string.
734
497
 
735
- #### [`bom()`](/docs/output#bom)Generate a bill of materials for all parts in the solved assembly.
736
-
737
- ```ts
738
- bom(): BomRow[]
739
- ```
740
-
741
- #### `bomCsv()` — Generate a bill of materials as a CSV string.
742
-
743
- ```ts
744
- bomCsv(): string
745
- ```
746
-
747
- #### `collisionReport()` — Detect overlapping (colliding) part pairs in this solved pose.
498
+ #### `collisionReport(options?: CollisionOptions): CollisionFinding[]` — Detect overlapping (colliding) part pairs in this solved pose.
748
499
 
749
500
  Computes boolean intersections between all part pairs and returns findings where the overlap volume exceeds `minOverlapVolume` (default 0.1 mm³).
750
501
 
@@ -753,90 +504,6 @@ const solved = mech.solve({ shoulder: 35, elbow: 60 });
753
504
  console.log("Collisions", solved.collisionReport());
754
505
  ```
755
506
 
756
- ```ts
757
- collisionReport(options?: CollisionOptions): CollisionFinding[]
758
- ```
759
-
760
- #### `minClearance()` — Compute the minimum gap (clearance) between two parts in this solved pose.
507
+ #### `minClearance(partA: string, partB: string, searchLength?: number): number` — Compute the minimum gap (clearance) between two parts in this solved pose.
761
508
 
762
509
  Returns `0` if the parts are touching or overlapping. Requires the Manifold backend. `searchLength` bounds the search radius in mm — increase it for widely separated parts.
763
-
764
- ```ts
765
- minClearance(partA: string, partB: string, searchLength?: number): number
766
- ```
767
-
768
- ### `MateBuilder`
769
-
770
- **Properties:**
771
-
772
- | Property | Type | Description |
773
- |----------|------|-------------|
774
- | `constraints` | `Constraint3D[]` | — |
775
-
776
- **Methods:**
777
-
778
- #### `flush()` — Constrain two faces so they stay flush.
779
-
780
- ```ts
781
- flush(faceA: string, faceB: string): string
782
- ```
783
-
784
- #### `align()` — Constrain two faces so their normals align.
785
-
786
- ```ts
787
- align(faceA: string, faceB: string): string
788
- ```
789
-
790
- #### `parallel()` — Constrain two faces so they remain parallel.
791
-
792
- ```ts
793
- parallel(faceA: string, faceB: string): string
794
- ```
795
-
796
- #### `faceDistance()` — Constrain the distance between two faces.
797
-
798
- ```ts
799
- faceDistance(faceA: string, faceB: string, distance: number): string
800
- ```
801
-
802
- #### `concentric()` — Constrain two axes to share the same center line.
803
-
804
- ```ts
805
- concentric(axisA: string, axisB: string): string
806
- ```
807
-
808
- #### `axisParallel()` — Constrain two axes to remain parallel.
809
-
810
- ```ts
811
- axisParallel(axisA: string, axisB: string): string
812
- ```
813
-
814
- #### `pointCoincident()` — Constrain two points to coincide.
815
-
816
- ```ts
817
- pointCoincident(pointA: string, pointB: string): string
818
- ```
819
-
820
- #### `pointOnFace()` — Constrain a point to lie on a face.
821
-
822
- ```ts
823
- pointOnFace(point: string, face: string): string
824
- ```
825
-
826
- #### `pointOnAxis()` — Constrain a point to lie on an axis.
827
-
828
- ```ts
829
- pointOnAxis(point: string, axis: string): string
830
- ```
831
-
832
- #### `angle()` — Constrain the angle between two faces.
833
-
834
- ```ts
835
- angle(faceA: string, faceB: string, degrees: number): string
836
- ```
837
-
838
- #### `totalEquations()` — Total constraint equations.
839
-
840
- ```ts
841
- get totalEquations(): number
842
- ```