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
@@ -11,6 +11,16 @@ For any fixed assembly where parts are meant to stay in contact in the final mod
11
11
 
12
12
  Use raw `translate()` and `rotate()` when parts are intentionally free-floating or when you are doing quick exploratory layout. Use `attachTo()` for rough bounding-box placement. But if the relationship is a real interface, make it explicit with connectors.
13
13
 
14
+ ## Mechanisms: connector frames vs link points
15
+
16
+ For serial articulated parts (hinges, hips, knees, levers, wheels), use `assembly().connect()` with connectors. `connect()` aligns the full connector frame: `origin` is the pivot/contact point, `axis` is the hinge line or slide direction, and `up` locks the zero-angle twist of the child part. `up` is a local roll reference, not world up; author it explicitly whenever the rest pose matters.
17
+
18
+ Do not use `addPart(..., { mate: { connector, toLink } })` when the part must point along a bone or inherit orientation from a link edge. Link mates are point attachments only: they translate the connector origin onto the solved link position and preserve the part's existing rotation. That is good for markers, sensors, labels, and debug handles. It is not a bone-frame API.
19
+
20
+ Use link graphs (`link()`, `edgeBetweenLinks()`, `addAngleBetweenLinks()`) when the hard part is solving point positions, especially closed loops. Use connector-frame joints when the hard part is orienting real physical parts.
21
+
22
+ For bilateral mechanisms, remember that connector-frame revolute values are physical axis-local values. Mirrored hinge axes need negated physical revolute values for the same mirrored pose, and physical limits mirror as `[min, max] -> [-max, -min]`. If you want `HipR: 10` and `HipL: 10` to mean the same semantic pose, drive a mirrored link graph or write an explicit state mapping instead of assuming equal connector-joint values are symmetric.
23
+
14
24
  ## Primitive origin convention
15
25
 
16
26
  All 3D primitives are **centered on XY, base at Z=0**:
@@ -34,14 +44,14 @@ Most positioning bugs come from manual coordinate arithmetic. Use these methods
34
44
 
35
45
  ## 1. Connectors + `matchTo()` — default for mating interfaces
36
46
 
37
- Define connectors on parts; `matchTo()` provides automatic 6-DOF alignment. The child translates and rotates so its connector aligns with the target's — origins coincide, axes oppose (plug-in model).
47
+ Define connectors on parts; `matchTo()` provides automatic alignment. With one connector pair, the child translates and rotates so its connector aligns with the target's — origins coincide, axes oppose (plug-in model), and `up` pins the roll reference. With multiple connector pairs, the connector origins define the rigid transform; still author meaningful `axis` and `up` values so the same connectors remain useful for `connect()`, audits, and future matching.
38
48
 
39
49
  ```javascript
40
50
  const shelf = box(200, 120, 10).translate(0, 0, -5).withConnectors({
41
- left_tab: connector.male("dovetail", { origin: [-100, 0, 0], axis: [-1, 0, 0] }),
51
+ left_tab: connector.male("dovetail", { origin: [-100, 0, 0], axis: [-1, 0, 0], up: [0, 0, 1] }),
42
52
  });
43
53
  const panel = box(12, 120, 200).translate(0, 0, -100).withConnectors({
44
- shelf_0: connector.female("dovetail", { origin: [6, 0, -50], axis: [1, 0, 0] }),
54
+ shelf_0: connector.female("dovetail", { origin: [6, 0, -50], axis: [1, 0, 0], up: [0, 0, 1] }),
45
55
  });
46
56
  const placed = shelf.matchTo(panel, "left_tab", "shelf_0");
47
57
  // Dictionary form for multiple pairs on same target:
@@ -26,9 +26,11 @@ A part is a function from props to `{ shape, connectors, metadata }`. It builds
26
26
  - A connector = origin + axis (outward from the part)
27
27
  - Connectors meet **face-to-face**: both axes point outward, system brings them together
28
28
  - For prismatic joints: axes point along the shared slide direction
29
+ - Mirrored revolute axes need negated physical joint values for the same mirrored pose
29
30
 
30
31
  ### 3. Assembly Is Pure Composition
31
- - `addPart()` + `connect()` + `addJointCoupling()` nothing else
32
+ - Use `addPart()` + `connect()` for frame-aware serial assemblies
33
+ - Use `link()` + `edgeBetweenLinks()` + `addAngleBetweenLinks()` for solved point skeletons
32
34
  - Zero `translate()` calls for structural parts
33
35
  - Zero coordinate math
34
36
  - The assembly passes props down and reads metadata up
@@ -60,6 +62,13 @@ mount.withConnectors({
60
62
  assembly.connect("Base.mount_face", "Mount.flange", { as: "mount-fix" });
61
63
  ```
62
64
 
65
+ Revolute values are signed by the physical hinge axis. In a bilateral mechanism,
66
+ `axis: [1, 0, 0]` on the right side and `axis: [-1, 0, 0]` on the left side are
67
+ exact mirrors at rest, but the same `+theta` value rotates them in opposite
68
+ fore/aft senses. Use `Right: +theta`, `Left: -theta`, and mirror physical limits
69
+ as `[min, max] -> [-max, -min]`, or drive both sides from a side-neutral link
70
+ graph/control layer.
71
+
63
72
  ## Part Return Shape
64
73
 
65
74
  Every part file returns a structured object:
@@ -114,10 +114,10 @@ node dist-cli/forgecad.js render 3d path/to/model.forge.js /tmp/<slug>-replicate
114
114
  --size 1000 --edges thin
115
115
  ```
116
116
 
117
- Build side-by-side boards with the bundled helper:
117
+ Build side-by-side boards with the bundled helper. It is a self-contained `uv` script that installs Pillow on demand and does not require Chrome. The examples use the ForgeCAD source-checkout path; if the skill is installed elsewhere, resolve `scripts/compare_images.py` relative to the `forgecad-image-replicator` skill directory.
118
118
 
119
119
  ```bash
120
- node skills/forgecad-image-replicator/scripts/compare_images.mjs \
120
+ uv run agent-skill-library/forgecad-image-replicator/scripts/compare_images.py \
121
121
  /tmp/<slug>-replicate/refs/front.png \
122
122
  /tmp/<slug>-replicate/render-front.png \
123
123
  /tmp/<slug>-replicate/compare-front.png \
@@ -127,10 +127,10 @@ node skills/forgecad-image-replicator/scripts/compare_images.mjs \
127
127
  Common helper options:
128
128
 
129
129
  ```bash
130
- node skills/forgecad-image-replicator/scripts/compare_images.mjs ref.png render.png compare.png
131
- node skills/forgecad-image-replicator/scripts/compare_images.mjs ref.jpg render.png compare.png --height 1200 --fit contain
132
- node skills/forgecad-image-replicator/scripts/compare_images.mjs ref.png render.png compare.png --fit cover --labels "Target,Current"
133
- node skills/forgecad-image-replicator/scripts/compare_images.mjs ref.png render.png compare.png --no-labels
130
+ uv run agent-skill-library/forgecad-image-replicator/scripts/compare_images.py ref.png render.png compare.png
131
+ uv run agent-skill-library/forgecad-image-replicator/scripts/compare_images.py ref.jpg render.png compare.png --height 1200 --fit contain
132
+ uv run agent-skill-library/forgecad-image-replicator/scripts/compare_images.py ref.png render.png compare.png --fit cover --labels "Target,Current"
133
+ uv run agent-skill-library/forgecad-image-replicator/scripts/compare_images.py ref.png render.png compare.png --no-labels
134
134
  ```
135
135
 
136
136
  Use `--fit contain` by default. Use `--fit cover` only when both images already share the same crop and aspect.
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.9"
4
+ # dependencies = ["pillow>=10"]
5
+ # ///
6
+
7
+ """Build a reference-vs-render PNG board for ForgeCAD image replication."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ from math import ceil
13
+ from pathlib import Path
14
+
15
+ from PIL import Image, ImageColor, ImageDraw, ImageFont, ImageOps
16
+
17
+
18
+ def positive_int(raw: str) -> int:
19
+ value = int(raw)
20
+ if value <= 0:
21
+ raise argparse.ArgumentTypeError("must be a positive integer")
22
+ return value
23
+
24
+
25
+ def labels(raw: str) -> tuple[str, str]:
26
+ values = tuple(part.strip() for part in raw.split(",") if part.strip())
27
+ if len(values) != 2:
28
+ raise argparse.ArgumentTypeError("must contain two comma-separated labels")
29
+ return values
30
+
31
+
32
+ def parse_args() -> argparse.Namespace:
33
+ parser = argparse.ArgumentParser(
34
+ description="Build a side-by-side comparison board from a reference image and ForgeCAD render.",
35
+ )
36
+ parser.add_argument("reference_image")
37
+ parser.add_argument("forgecad_render")
38
+ parser.add_argument("output_png")
39
+ parser.add_argument("--height", type=positive_int, default=900, help="Panel height in pixels.")
40
+ parser.add_argument("--panel-width", type=positive_int, default=None, help="Panel width in pixels.")
41
+ parser.add_argument("--gap", type=positive_int, default=16, help="Gap between panels in pixels.")
42
+ parser.add_argument("--padding", type=positive_int, default=16, help="Outer padding in pixels.")
43
+ parser.add_argument("--background", default="#111111", help="Canvas background color.")
44
+ parser.add_argument("--fit", choices=("contain", "cover"), default="contain", help="Image fit mode.")
45
+ parser.add_argument("--labels", type=labels, default=("Reference", "ForgeCAD"), help="Two comma-separated labels.")
46
+ parser.add_argument("--no-labels", action="store_true", help="Disable label band.")
47
+ parser.add_argument(
48
+ "--chrome-path",
49
+ default=None,
50
+ help=argparse.SUPPRESS,
51
+ )
52
+ return parser.parse_args()
53
+
54
+
55
+ def open_image(path_arg: str) -> Image.Image:
56
+ path = Path(path_arg).expanduser()
57
+ if not path.exists():
58
+ raise SystemExit(f"Image not found: {path}")
59
+ try:
60
+ image = Image.open(path)
61
+ return ImageOps.exif_transpose(image).convert("RGBA")
62
+ except Exception as exc: # Pillow gives format-specific exceptions.
63
+ raise SystemExit(f"Failed to open image {path}: {exc}") from exc
64
+
65
+
66
+ def parse_background(raw: str) -> tuple[int, int, int, int]:
67
+ try:
68
+ color = ImageColor.getcolor(raw, "RGBA")
69
+ except ValueError as exc:
70
+ raise SystemExit(f"Invalid background color {raw!r}: {exc}") from exc
71
+ return color
72
+
73
+
74
+ def load_label_font() -> ImageFont.ImageFont:
75
+ candidates = [
76
+ "/System/Library/Fonts/Supplemental/Arial Bold.ttf",
77
+ "/System/Library/Fonts/Supplemental/Arial.ttf",
78
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
79
+ "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf",
80
+ ]
81
+ for candidate in candidates:
82
+ path = Path(candidate)
83
+ if path.exists():
84
+ return ImageFont.truetype(str(path), 18)
85
+ return ImageFont.load_default()
86
+
87
+
88
+ def scaled_to_panel(image: Image.Image, panel_width: int, panel_height: int, fit: str) -> Image.Image:
89
+ width, height = image.size
90
+ if width <= 0 or height <= 0:
91
+ raise SystemExit("Image dimensions must be positive.")
92
+ scale = (
93
+ max(panel_width / width, panel_height / height)
94
+ if fit == "cover"
95
+ else min(panel_width / width, panel_height / height)
96
+ )
97
+ if fit == "cover":
98
+ scaled_size = (max(panel_width, ceil(width * scale)), max(panel_height, ceil(height * scale)))
99
+ else:
100
+ scaled_size = (min(panel_width, max(1, round(width * scale))), min(panel_height, max(1, round(height * scale))))
101
+ resized = image.resize(scaled_size, Image.Resampling.LANCZOS)
102
+ if fit != "cover":
103
+ return resized
104
+
105
+ left = max(0, (resized.width - panel_width) // 2)
106
+ top = max(0, (resized.height - panel_height) // 2)
107
+ return resized.crop((left, top, left + panel_width, top + panel_height))
108
+
109
+
110
+ def paste_panel(
111
+ board: Image.Image,
112
+ image: Image.Image,
113
+ *,
114
+ x: int,
115
+ y: int,
116
+ panel_width: int,
117
+ panel_height: int,
118
+ fit: str,
119
+ ) -> None:
120
+ panel = Image.new("RGBA", (panel_width, panel_height), (0, 0, 0, 0))
121
+ fitted = scaled_to_panel(image, panel_width, panel_height, fit)
122
+ dx = (panel_width - fitted.width) // 2
123
+ dy = (panel_height - fitted.height) // 2
124
+ panel.alpha_composite(fitted, (dx, dy))
125
+ board.alpha_composite(panel, (x, y))
126
+
127
+
128
+ def main() -> None:
129
+ args = parse_args()
130
+ reference = open_image(args.reference_image)
131
+ render = open_image(args.forgecad_render)
132
+
133
+ panel_height = args.height
134
+ max_aspect = max(reference.width / reference.height, render.width / render.height)
135
+ panel_width = args.panel_width or int(panel_height * max_aspect + 0.9999)
136
+ label_values = None if args.no_labels else args.labels
137
+ label_height = 34 if label_values else 0
138
+ canvas_width = args.padding * 2 + panel_width * 2 + args.gap
139
+ canvas_height = args.padding * 2 + label_height + panel_height
140
+
141
+ board = Image.new("RGBA", (canvas_width, canvas_height), parse_background(args.background))
142
+ draw = ImageDraw.Draw(board)
143
+ left_x = args.padding
144
+ right_x = args.padding + panel_width + args.gap
145
+ panel_y = args.padding + label_height
146
+
147
+ if label_values:
148
+ font = load_label_font()
149
+ for text, x in ((label_values[0], left_x), (label_values[1], right_x)):
150
+ draw.text((x, args.padding + 4), text, fill=(255, 255, 255, 230), font=font)
151
+
152
+ paste_panel(board, reference, x=left_x, y=panel_y, panel_width=panel_width, panel_height=panel_height, fit=args.fit)
153
+ paste_panel(board, render, x=right_x, y=panel_y, panel_width=panel_width, panel_height=panel_height, fit=args.fit)
154
+
155
+ outline = (255, 255, 255, 64)
156
+ draw.rectangle((left_x, panel_y, left_x + panel_width - 1, panel_y + panel_height - 1), outline=outline)
157
+ draw.rectangle((right_x, panel_y, right_x + panel_width - 1, panel_y + panel_height - 1), outline=outline)
158
+
159
+ output_path = Path(args.output_png).expanduser()
160
+ output_path.parent.mkdir(parents=True, exist_ok=True)
161
+ board.save(output_path, "PNG")
162
+ print(f"Wrote {output_path} ({canvas_width}x{canvas_height})")
163
+
164
+
165
+ if __name__ == "__main__":
166
+ main()
@@ -44,9 +44,14 @@ Use today's date for the directory. Use the user's current ForgeCAD project when
44
44
 
45
45
  ## Workflow
46
46
 
47
- 1. Load the ForgeCAD skill — always invoke the `forgecad` skill first to get API docs and authoring guidance. Read at minimum the Core API reference. If any two parts are intended to touch or mate in the final model, read the positioning guide immediately and default to connectors + `matchTo()`.
47
+ 1. Load the ForgeCAD skill — always invoke the `forgecad` skill first to get API docs and authoring guidance. Read at minimum the Core API reference. If the model has moving parts, load the assembly group and `docs/permanent/guides/joint-design.md` before writing geometry. If any two parts are intended to touch or mate in the final model, read the positioning guide immediately and default to connectors + `matchTo()`.
48
48
  2. Create the directory — `mkdir -p YYYY/MM/DD/[folder]` as needed.
49
- 3. Write the modelcreate the `.forge.js` file(s) following ForgeCAD conventions:
49
+ 3. Prove moving-part kinematics before detailed geometry for any mechanism, robot, linkage, hinge, slider, suspension, gripper, wheel train, drawer, door, linkage toy, or articulated product:
50
+ - Author the rig first: links, frames, fixed ground references, parent/child relationships, joints, couplings, limits, defaults, mirrored-side sign mapping, and named controls.
51
+ - Choose the right rig primitive before modeling the skin: use link graphs (`link()`, `edgeBetweenLinks()`, `addAngleBetweenLinks()`) for point skeletons and closed loops; use frame/connector joints (`frame()`, `revoluteJoint()`, `prismaticJoint()`, `connect()`, `match()`) when part orientation matters.
52
+ - Attach only simple markers, bars, or proxy parts at first. Return the `Assembly` directly and run it at rest, mid-travel, and limit joint values until the solver, controls, connector/frame alignment, and `verify.*` checks pass.
53
+ - Only after the rig works should you replace proxies with manufacture-real geometry attached to the solved links, frames, and connectors. Do not bake a posed `SolvedAssembly` as the default return just to make one pose look right.
54
+ 4. Write the model — create the `.forge.js` file(s) following ForgeCAD conventions:
50
55
  - Treat the default build profile as `manufacture-realistic prototype`; choose and encode the artifact's manufacturing/process cues before adding styling detail
51
56
  - Declare `param()` / `boolParam()` for all tunable dimensions
52
57
  - If the model is split across files, use `main.forge.js` as the primary entry point, import renderable parts from neighboring `.forge.js` files, and keep only pure helpers/constants in plain `.js` modules
@@ -57,11 +62,12 @@ Use today's date for the directory. Use the user's current ForgeCAD project when
57
62
  - Make final mating geometry physically plausible: parts may touch, clear each other, or be boolean-joined, but should not unintentionally pass through each other
58
63
  - Model the physical artifact, not an educational diagram: no explanatory arrows, floating labels, section labels, legends, or text plaques unless the user explicitly requested a presentation/teaching view
59
64
  - Do not make the default returned model a cutaway, sectioned shell, permanently exploded assembly, or hidden-parts teaching view. ForgeCAD gives the user viewer and inspection tools for slicing, exploding, hiding, and looking inside after the real CAD exists.
60
- - Return the final geometry (single shape, array, or named objects array)
65
+ - Return the final artifact (single shape, array, named objects array, or `Assembly` for moving mechanisms)
66
+ - For moving assemblies, return the `Assembly` itself whenever possible so Motion controls re-run the real kinematic solve; avoid legacy viewport-only animation APIs for new work unless the user explicitly asks for them
61
67
  - Treat `fillet(shape, r)` and `chamfer(shape, r)` as experimental edge treatments: Manifold can produce incorrect results and OCCT can be very slow. Prefer simpler primitive profiles, lower segment counts, targeted edge selectors, and inspection before relying on the result.
62
- 4. Validate — run `forgecad run <file>` to check for errors. For multi-file projects, always validate `main.forge.js`.
63
- 5. Verify geometry — render a multi-angle visual evidence set before final delivery: whole-model context plus agent-chosen orthographic, oblique, underside, or hidden-object views that expose the relevant components and interfaces. Choose camera directions from the model's shape and likely failure modes, not from a fixed recipe. Use those views to look for internals that are accidentally visible, parts that visibly do not fit, floating details, blocked access, missing seats, and unexpected interference. Run `forgecad inspect physical components` when the model has multiple returned objects or visible attachments, run `forgecad debug assembly --fail-on warning` when the script uses `assembly()`, run `forgecad inspect mechanical-integrity <project-or-file> --collisions` before sharing generated mechanical work, and run the targeted `forgecad inspect <family> <mode>` commands that match the task (see Final Acceptance Gate and Render-Verify Loop below). For multi-file projects, render and inspect `main.forge.js`. Collision findings are model work, not FYI: remove unexpected overlaps before delivery.
64
- 6. Iterate from visual and inspection feedback — treat every render and inspection bundle as model evidence, not a checkbox. Read the normal PNGs, manifest, and evidence PNGs; convert each unexpected collision, thin region, missing section detail, wrong component count, floating body, distance gap, confusing object-color result, accidentally exposed internal structure, bad fit, or visually unsupported interface into a concrete model edit; then rerun the same targeted evidence pass until the result matches the intended physical component graph.
68
+ 5. Validate — run `forgecad run <file>` to check for errors. For multi-file projects, always validate `main.forge.js`. For moving assemblies, also run representative joint states with `--joint`, including rest/default, mid-travel, both limits, and any coupled or mirrored poses.
69
+ 6. Verify geometry — render a multi-angle visual evidence set before final delivery: whole-model context plus agent-chosen orthographic, oblique, underside, or hidden-object views that expose the relevant components and interfaces. Choose camera directions from the model's shape and likely failure modes, not from a fixed recipe. Use those views to look for internals that are accidentally visible, parts that visibly do not fit, floating details, blocked access, missing seats, and unexpected interference. For moving assemblies, render at several joint values with `--joint` so clearance, range, stops, and connected followers are proven in motion, not only at rest. Run `forgecad inspect physical components` when the model has multiple returned objects or visible attachments, run `forgecad debug assembly --fail-on warning` when the script uses `assembly()`, run `forgecad inspect mechanical-integrity <project-or-file> --collisions` before sharing generated mechanical work, and run the targeted `forgecad inspect <family> <mode>` commands that match the task (see Final Acceptance Gate and Render-Verify Loop below). For multi-file projects, render and inspect `main.forge.js`. Collision findings are model work, not FYI: remove unexpected overlaps before delivery.
70
+ 7. Iterate from visual and inspection feedback — treat every render and inspection bundle as model evidence, not a checkbox. Read the normal PNGs, manifest, and evidence PNGs; convert each unexpected collision, thin region, missing section detail, wrong component count, floating body, distance gap, confusing object-color result, accidentally exposed internal structure, bad fit, bad joint sweep, unconverged pose, follower drift, or visually unsupported interface into a concrete model edit; then rerun the same targeted evidence pass until the result matches the intended physical component graph.
65
71
 
66
72
  ## Manufacturing Process Is Not Assumed
67
73
 
@@ -118,6 +124,28 @@ Do not reveal hidden structure by permanently cutting away the production geomet
118
124
 
119
125
  When internals are hidden by the final exterior, verify them with exploration tools instead of changing the artifact: render underside or alternate camera views, use `forgecad inspect sections at|stack|sample`, use viewer-only cut planes or explode controls, temporarily make a shell transparent, or add named ghost objects for fit checks. Those views are diagnostic/presentation modes; they must not replace the real model unless the user explicitly asked for a cutaway teaching model.
120
126
 
127
+ ## Kinematics-First for Moving Parts
128
+
129
+ For moving models, the mechanism rig is the source of truth. Do not start with finished housings, arms, shells, covers, or decorative surfaces and then try to retrofit motion. Build the motion structure first, prove it, and attach geometry to it.
130
+
131
+ - Decide the rig shape before geometry: point links for closed-loop skeletons and distance/angle constraints; frames and connector joints for serial rigid parts whose orientation matters.
132
+ - Name every moving degree of freedom and encode its limits/defaults at the rig level. For mirrored revolute axes, make the sign mapping explicit instead of assuming equal values create mirrored poses.
133
+ - Add simple proxy geometry only to expose the rig: pivot markers, bars between links, slider blocks, wheel discs, or frame axes. Use those proxies to verify the mechanism before adding real material.
134
+ - Attach final geometry to solved links, frames, and connectors. A visible arm, cover, wheel, drawer, or follower should move because it is mated to the rig, not because a final `rotate()` or `translate()` happens to make one pose look assembled.
135
+ - Keep the default return as the unsolved `Assembly` when possible. This lets Motion controls and CLI `--joint` overrides re-run the real solve.
136
+ - Put representative pose checks in the script with `verify.*`: convergence, connector origins coinciding, link lengths holding, end effectors reaching their expected points, followers remaining attached, and required running clearances staying positive.
137
+
138
+ At minimum, run the mechanism at rest, mid-travel, both limits, and any mirrored/coupled poses:
139
+
140
+ ```bash
141
+ node dist-cli/forgecad.js run model.forge.js --joint "theta=0"
142
+ node dist-cli/forgecad.js run model.forge.js --joint "theta=45"
143
+ node dist-cli/forgecad.js run model.forge.js --joint "theta=90"
144
+ node dist-cli/forgecad.js render 3d model.forge.js /tmp/model-theta-45.png --joint "theta=45" --camera iso --size 700
145
+ ```
146
+
147
+ Use the real joint/control names from the assembly. For multiple controls, repeat `--joint`: `--joint "hip=20" --joint "knee=-35"`. If any pose fails to solve, clamps unexpectedly, breaks a connector check, or creates a new collision, the rig is not ready for final geometry.
148
+
121
149
  ## Mechanical Assembly Contract
122
150
 
123
151
  For mechanical models, a ForgeCAD script is not done when it merely looks assembled. Every visible piece must have a believable physical reason to be where it is: fused material, contact faces, a screw stack, a pin in a bore, a tab in a slot, a gasket on a land, a bearing in a seat, a cable in a channel, or a named intentional ghost.
@@ -132,37 +160,7 @@ For mechanical models, a ForgeCAD script is not done when it merely looks assemb
132
160
  - Purchased loose parts may remain separate bodies, but they should be named as purchased hardware or consumables and should sit in believable sockets, bores, races, guides, or fastener stacks.
133
161
  - Encode interface intent with `verify.*`, not only comments. Use `verify.clearanceBetween("cover is seated on gasket", cover, gasket, -0.01, 0.05)` for contact/seated fits and clearance bands, `verify.minClearance(...)` or `verify.notColliding(...)` for keep-out/running gaps, and `verify.connectorDistance(...)` for connector-authored mates. Part counts and generic dimensions are useful supporting checks, but they do not prove an interface by themselves.
134
162
 
135
- For ordinary removable covers, prefer `lib.boltedServiceCover(...)` before hand-placing plates, tabs, screw heads, gaskets, and holes. It creates the parent ledge, gasket, cover plate with fused pull tabs, shared bolt pattern, and installed screws as one mechanically accountable interface.
136
-
137
- For electronics boxes, backplates, service-stack housings, and camera/monitor enclosures, prefer `lib.datumEnclosureAssembly(...)` before independently placing panels, ribs, bosses, ports, covers, and screws. It creates the tray, ledges, standoffs, ribs, service port, gasket, cover, and screws from one shared datum system.
138
-
139
- For PCB-mounted terminal blocks, thermostat backplates, control boards, and wire-entry electronics panels, prefer `lib.pcbTerminalBlockAssembly(...)` before placing a loose green block near a board or cover. It creates the backplate, fused standoffs, PCB mounting screws, PCB pin holes, terminal pins, and seated purchased terminal block from one shared datum system.
140
-
141
- For snap-retained covers, cartridges, small clasps, and housings, prefer `lib.snapLatchCoverAssembly(...)` before drawing decorative snap tabs. It creates latch windows, underside catch lands, fused snap hooks, barbs, and clearance checks so the cover is retained by real geometry.
142
-
143
- For ordinary pinned handles, cam levers, release levers, and latch arms, prefer `lib.pinnedLeverAssembly(...)` before hand-placing a hub, arm, washers, and pin. It creates a fused lever body, aligned pivot bore, retained pin, thrust washers, support land, and low stop land as one mechanically accountable pivot stack.
144
-
145
- For trunnions, side knobs, adjustable pivots, and clamp shafts, prefer `lib.retainedShaftAssembly(...)` before hand-placing rods, washers, and knobs. It creates bored support cheeks, a through shaft, thrust washers, knobs, retaining heads, and shared bore dimensions as one mechanically accountable stack.
146
-
147
- For thumb screws, desk clamps, vise screws, capo pressure screws, and small fixture hold-downs, prefer `lib.thumbScrewClampAssembly(...)` before hand-placing a knob, screw cylinder, pressure pad, and bracket jaw. It creates the C-frame, threaded boss/bore, captive pressure pad, hand knob, and seated workpiece contact from one shared datum system.
148
-
149
- For drawer slides, quick-release plates, and guided linear carriages, prefer `lib.capturedLinearSlide(...)` before hand-placing rails and a block. It creates a U-channel rail with return lips, end stops, a captured carriage, and explicit travel/clearance dimensions.
150
-
151
- For pump cartridges, filter cassettes, battery cartridges, skeg cassettes, and removable slide-in modules, prefer `lib.capturedCartridgeGuideAssembly(...)` before placing a loose tray and block. It creates return lips, a rear stop, a captured cartridge flange, pull tab, insertion travel, and explicit clearance dimensions.
152
-
153
- For molded flexible battery doors, sample covers, blister latches, and polypropylene-style service flaps, prefer `lib.livingHingeCoverAssembly(...)` before drawing two plates with a decorative hinge strip. It creates one fused molded strip with fixed leaf, thin flexible web, moving cover leaf, pull lip, snap barb, catch land, and web-thickness checks.
154
-
155
- For doors, barn-door leaves, lids, locket leaves, and small hinged access panels, prefer `lib.knuckledHingeAssembly(...)` before hand-placing barrels and a pin. It creates alternating fused knuckles, two leaves, a shared bore, and a retained pin as one mechanically accountable hinge.
156
-
157
- For crank links, damper rod ends, crossheads, and clevis-yoke pivots, prefer `lib.clevisPinJointAssembly(...)` before hand-placing an eyelet and pin. It creates bored clevis ears, a captured center link eye, a rear bridge, and a retained pin as one mechanically accountable load path.
158
-
159
- For bearings, rollers, burr cartridges, spindle supports, and purchased radial bearings, prefer `lib.seatedBearingAssembly(...)` before hand-placing a ring and shaft near a block. It creates a bored housing, counterbore pocket, bearing shoulder, seated bearing, shaft, collars, and shared clearance dimensions as one mechanically accountable support.
160
-
161
- For cables, wires, hoses, pump tubes, and panel pass-throughs, prefer `lib.cableGlandAnchorAssembly(...)` before hand-placing a loose cylinder near a wall. It creates the panel clearance hole, hollow gland body, compression nut, and routed cable/tube as one mechanically accountable pass-through.
162
-
163
- For routed cables, wires, hoses, pump tubes, and sensor leads that run along a surface, prefer `lib.routedTubeClipAssembly(...)` before drawing a tube that floats between endpoints. It creates a base panel, saddle clip bores, clip screw holes, installed screws, and the retained tube route from one shared datum system.
164
-
165
- For fluid hoses, pump inlets/outlets, filter ports, and lab tubing, prefer `lib.hoseBarbPortAssembly(...)` before drawing a tube that stops at a block. It creates the bored receiver, raised boss, hollow barbed fitting, installed hose, and clamp band as one accountable hose-port interface.
163
+ Do not introduce canned finished-object helpers for project-specific assemblies. When a repeated physical pattern appears, build the project-specific interface explicitly: use `lib.fastenerSet()` or `lib.boltPattern()` for screw stacks, real bores/counterbores/pockets for purchased parts, connectors plus `matchTo()` for mating parts, and `verify.*` checks for seated contact and running clearances. Reusable library vocabulary should be primitives, cutters, patterns, and mechanical contracts, not finished thermostat backplates, clamp brackets, hinge leaves, or other canned objects.
166
164
 
167
165
  ## Final Geometry Should Be Physically Plausible
168
166
 
@@ -206,6 +204,8 @@ Before telling the user the model is done, prove both technical validity and vis
206
204
 
207
205
  The model should include at least one verification that proves a mechanical interface, not just object count. Prefer checks such as `verify.clearanceBetween("bearing is seated in pocket", bearing, housing, -0.01, 0.1)`, `verify.minClearance("carriage clears rail", carriage, rail, 0.15)`, `verify.notColliding("cover screw clears parent hole", screw, parent)`, or `verify.connectorDistance("leg connector is seated", bench, "Rail.leg_0", "Leg0.head", 0, 0.01)`.
208
206
 
207
+ For moving assemblies, this gate is incomplete until the rig has been run through representative control states. Use `--joint` overrides and/or in-script `solve(state)` verification to cover rest/default, mid-travel, physical limits, coupled states, and mirrored-side poses. Each tested pose should keep the solver converged, attached geometry following the rig, and clearances/collisions within the intended mechanical contract.
208
+
209
209
  2. Run collision evidence and read both the manifest and images:
210
210
 
211
211
  ```bash
@@ -241,7 +241,7 @@ Before telling the user the model is done, prove both technical validity and vis
241
241
 
242
242
  5. Treat ProductSkin and surface-member limitations honestly. If `inspect fit interference` reports boolean-test warnings because a sampled `Product.skin` loft has boundary edges, distinguish that from real collision findings. You may still deliver if `collisionCount` is clean, the intended connectivity is correct, and the visual attachment audit passes. Mention the residual warning briefly in the final response.
243
243
 
244
- 6. Final response must name the evidence: commands run, render views checked, any focus/hide filters used, component count, collision count, and any residual warnings or intentional exceptions. Do not just say "validated."
244
+ 6. Final response must name the evidence: commands run, render views checked, joint values tested for moving assemblies, any focus/hide filters used, component count, collision count, and any residual warnings or intentional exceptions. Do not just say "validated."
245
245
 
246
246
  ## Render-Verify Loop
247
247
 
@@ -438,6 +438,7 @@ Primitive placement convention:
438
438
  Key composition tools:
439
439
 
440
440
  - Connectors + `matchTo()` for parts that should touch in the final model
441
+ - `assembly()` link graphs, frames, and connector joints for moving mechanisms
441
442
  - `group()` for local-coordinate subassemblies
442
443
  - `attachTo()` for quick bounding-box placement
443
444
  - `.translate()` / `.rotate()` for free offsets or bridging computed locations, not as the default assembly contract
@@ -59,7 +59,7 @@ This skill is an evaluator workflow. Do not edit the model unless the user expli
59
59
 
60
60
  ```bash
61
61
  node dist-cli/forgecad.js inspect fit interference path/to/model.forge.js /tmp/model-grade/collisions --camera iso --force --size 700
62
- python skills/forgecad-render-inspect/summarize_manifest.py /tmp/model-grade/collisions
62
+ python agent-skill-library/forgecad-render-inspect/summarize_manifest.py /tmp/model-grade/collisions
63
63
  ```
64
64
 
65
65
  Read the manifest and inspect the relevant evidence PNGs. Treat unexpected collisions, thin regions, missing sections, wrong component counts, floating bodies, and confusing object colors as evidence, not as warnings to wave away.
@@ -176,7 +176,7 @@ By the end of this skill, there should be:
176
176
 
177
177
  9. If implementation continues immediately, hand off to `forgecad`.
178
178
  For moving mechanisms, load:
179
- - `skills/forgecad/SKILL.md`
179
+ - `~/.agents/skills/forgecad/SKILL.md`
180
180
  - `docs/permanent/generated/assembly.md`
181
181
  - `docs/permanent/generated/output.md`
182
182
  - `docs/permanent/guides/joint-design.md`
@@ -10,6 +10,8 @@ forgecad-public: true
10
10
 
11
11
  **forgecad.io** is the primary platform for ForgeCAD projects. The CLI is the main way AI agents interact with it — creating projects, managing files, publishing models, and collaborating. `forgecad studio <project-path> [project-path ...]` opens the installed local editor for users; `forgecad dev <project-path> [project-path ...]` is mainly for ForgeCAD source development.
12
12
 
13
+ Keep one long-running `forgecad studio <project-path> [project-path ...]` process open with every active project folder listed in its arguments; the user opens the single printed localhost port once, and AI agents should only create or edit files under those folders so the browser updates live without starting more servers.
14
+
13
15
  ## Authentication
14
16
 
15
17
  ```sh
@@ -62,7 +62,7 @@ Routing:
62
62
  Run the bundled helper:
63
63
 
64
64
  ```bash
65
- python skills/forgecad-render-inspect/summarize_manifest.py /tmp/model-inspect
65
+ python agent-skill-library/forgecad-render-inspect/summarize_manifest.py /tmp/model-inspect
66
66
  ```
67
67
 
68
68
  Use `jq` for targeted follow-up when needed:
@@ -88,6 +88,7 @@ Routing:
88
88
  | Question | Evidence command |
89
89
  |----------|------------------|
90
90
  | Quick visual sanity | `inspect visual image` |
91
+ | Kinematic rig, joints, axes, and links | `inspect visual rig` |
91
92
  | Object naming and identity | `inspect visual objects` |
92
93
  | Exact local section measurement, bore widths, rib thickness through a chosen line | `inspect section --ray ...` |
93
94
  | Hidden internals, cavities, pockets, screw paths, captured components | `inspect sections at|stack|sample` |
@@ -106,6 +107,7 @@ Explicit fast bundle:
106
107
 
107
108
  ```bash
108
109
  forgecad inspect visual objects model.forge.js --camera iso --size 700
110
+ forgecad inspect visual rig model.forge.js --camera iso --size 700
109
111
  forgecad inspect sections at model.forge.js --plane yz --offset 12.5 --size 700
110
112
  forgecad inspect sections stack model.forge.js --plane yz --every 1 --size 700
111
113
  forgecad inspect sections sample model.forge.js --count 5 --size 700
@@ -0,0 +1,65 @@
1
+ // Assembly-owned point kinematics: return the rig, then let viewport controls
2
+ // drive solve(state). Closed loops stay solver-backed instead of viewport-FK,
3
+ // and the editor updates part transforms without rebuilding the marker meshes.
4
+ //
5
+ // This file deliberately keeps the skin as point markers. For physical bars
6
+ // that orient from one solved link to another with two connector mates, see
7
+ // assembly-kinematics-four-bar.forge.js.
8
+
9
+ const DEFAULT_THETA = 42;
10
+ const MAX_THETA = 95;
11
+
12
+ const GROUND_A = [0, 0, 0];
13
+ const GROUND_B = [90, 0, 0];
14
+ const CRANK_TIP = [35, 0, 0];
15
+ const ROCKER_TIP = [55, 42, 0];
16
+
17
+ const crankLen = Points.distance(GROUND_A, CRANK_TIP);
18
+ const couplerLen = Points.distance(CRANK_TIP, ROCKER_TIP);
19
+ const rockerLen = Points.distance(GROUND_B, ROCKER_TIP);
20
+
21
+ function linkMarker(color) {
22
+ return box(8, 8, 4)
23
+ .translate(0, 0, -2)
24
+ .color(color)
25
+ .withConnectors({
26
+ center: connector({ origin: [0, 0, 0], axis: [0, 0, 1] }),
27
+ });
28
+ }
29
+
30
+ const mechanism = assembly("Assembly Kinematics Foundation")
31
+ .link("groundA", { at: GROUND_A, fixed: true })
32
+ .link("groundB", { at: GROUND_B, fixed: true })
33
+ .link("crankTip", { at: CRANK_TIP })
34
+ .link("rockerTip", { at: ROCKER_TIP })
35
+ .edgeBetweenLinks("groundA", "crankTip", { name: "crank" })
36
+ .edgeBetweenLinks("crankTip", "rockerTip", { name: "coupler" })
37
+ .edgeBetweenLinks("groundB", "rockerTip", { name: "rocker" })
38
+ .addAngleBetweenLinks("groundB", "groundA", "crankTip", {
39
+ name: "theta",
40
+ // Past about 98 degrees this link set has no exact four-bar solution:
41
+ // the coupler and rocker are fully extended.
42
+ control: { min: 0, max: MAX_THETA, default: DEFAULT_THETA },
43
+ })
44
+ .addPart("Ground A marker", linkMarker("#4f5f67"), { mate: { connector: "center", toLink: "groundA" } })
45
+ .addPart("Ground B marker", linkMarker("#4f5f67"), { mate: { connector: "center", toLink: "groundB" } })
46
+ .addPart("Crank tip marker", linkMarker("#d55e5e"), { mate: { connector: "center", toLink: "crankTip" } })
47
+ .addPart("Rocker tip marker", linkMarker("#3d87c7"), { mate: { connector: "center", toLink: "rockerTip" } });
48
+
49
+ const solved = mechanism.solve({ theta: DEFAULT_THETA });
50
+ verify.equal("kinematic controls are preserved", solved.kinematics.controls.theta, DEFAULT_THETA, 0.000001);
51
+
52
+ for (const theta of [0, DEFAULT_THETA, 70, MAX_THETA]) {
53
+ const pose = mechanism.solve({ theta });
54
+ const groundA = pose.getLinkPosition("groundA");
55
+ const groundB = pose.getLinkPosition("groundB");
56
+ const crankTip = pose.getLinkPosition("crankTip");
57
+ const rockerTip = pose.getLinkPosition("rockerTip");
58
+
59
+ verify.that(`closed loop converges at theta ${theta}`, () => pose.kinematics.converged);
60
+ verify.equal(`crank length holds at theta ${theta}`, Points.distance(groundA, crankTip), crankLen, 0.01);
61
+ verify.equal(`coupler length holds at theta ${theta}`, Points.distance(crankTip, rockerTip), couplerLen, 0.01);
62
+ verify.equal(`rocker length holds at theta ${theta}`, Points.distance(groundB, rockerTip), rockerLen, 0.01);
63
+ }
64
+
65
+ return mechanism;
@@ -0,0 +1,115 @@
1
+ // Closed-loop four-bar linkage using the assembly kinematic graph.
2
+ //
3
+ // The links define the mechanism. The bars are ordinary local geometry with
4
+ // two connector mates, so the editor slider re-solves the link graph and then
5
+ // updates part transforms. The bar meshes are not rebuilt while theta changes.
6
+
7
+ scene({
8
+ background: { top: "#e6ebef", bottom: "#7b858d" },
9
+ views: {
10
+ top: { camera: { position: [45, 25, 220], target: [45, 24, 0], up: [0, 1, 0], fov: 32 } },
11
+ iso: { camera: { position: [150, -135, 115], target: [45, 24, 0], up: [0, 0, 1], fov: 34 } },
12
+ },
13
+ lights: [
14
+ { type: "ambient", color: "#f4f7fa", intensity: 0.45 },
15
+ { type: "directional", position: [160, -140, 260], color: "#fff4dc", intensity: 2.0, castShadow: true },
16
+ ],
17
+ });
18
+
19
+ const DEFAULT_THETA = 42;
20
+
21
+ const GROUND_A = [0, 0, 0];
22
+ const GROUND_B = [90, 0, 0];
23
+ const CRANK_TIP = [35, 0, 0];
24
+ const ROCKER_TIP = [55, 42, 0];
25
+
26
+ const groundLen = Points.distance(GROUND_A, GROUND_B);
27
+ const crankLen = Points.distance(GROUND_A, CRANK_TIP);
28
+ const couplerLen = Points.distance(CRANK_TIP, ROCKER_TIP);
29
+ const rockerLen = Points.distance(GROUND_B, ROCKER_TIP);
30
+
31
+ function bar(length, width, color) {
32
+ return box(length, width, 5)
33
+ .translate(length / 2, 0, 0)
34
+ .color(color)
35
+ .material({ metalness: 0.1, roughness: 0.55 })
36
+ .withConnectors({
37
+ near: connector({ origin: [0, 0, 0], axis: [1, 0, 0], up: [0, 0, 1] }),
38
+ far: connector({ origin: [length, 0, 0], axis: [1, 0, 0], up: [0, 0, 1] }),
39
+ });
40
+ }
41
+
42
+ function pivot(color) {
43
+ return cylinder(7, 5.5)
44
+ .translate(0, 0, -3.5)
45
+ .color(color)
46
+ .material({ metalness: 0.2, roughness: 0.45 })
47
+ .withConnectors({
48
+ center: connector({ origin: [0, 0, 0], axis: [0, 0, 1], up: [1, 0, 0] }),
49
+ });
50
+ }
51
+
52
+ const fourBar = assembly("Closed Loop Four Bar")
53
+ .link("groundA", { at: GROUND_A, fixed: true })
54
+ .link("groundB", { at: GROUND_B, fixed: true })
55
+ .link("crankTip", { at: CRANK_TIP })
56
+ .link("rockerTip", { at: ROCKER_TIP })
57
+ .edgeBetweenLinks("groundA", "crankTip", { name: "crank" })
58
+ .edgeBetweenLinks("crankTip", "rockerTip", { name: "coupler" })
59
+ .edgeBetweenLinks("groundB", "rockerTip", { name: "rocker" })
60
+ .addAngleBetweenLinks("groundB", "groundA", "crankTip", {
61
+ name: "theta",
62
+ control: { min: 0, max: 95, default: DEFAULT_THETA },
63
+ });
64
+
65
+ fourBar.addPart("Ground", bar(groundLen, 10, "#56636b"), {
66
+ mate: [
67
+ { connector: "near", toLink: "groundA" },
68
+ { connector: "far", toLink: "groundB" },
69
+ ],
70
+ });
71
+
72
+ fourBar.addPart("Crank", bar(crankLen, 8, "#d95c4b"), {
73
+ mate: [
74
+ { connector: "near", toLink: "groundA" },
75
+ { connector: "far", toLink: "crankTip" },
76
+ ],
77
+ });
78
+
79
+ fourBar.addPart("Coupler", bar(couplerLen, 7, "#e5b84c"), {
80
+ mate: [
81
+ { connector: "near", toLink: "crankTip" },
82
+ { connector: "far", toLink: "rockerTip" },
83
+ ],
84
+ });
85
+
86
+ fourBar.addPart("Rocker", bar(rockerLen, 8, "#3b82c4"), {
87
+ mate: [
88
+ { connector: "near", toLink: "groundB" },
89
+ { connector: "far", toLink: "rockerTip" },
90
+ ],
91
+ });
92
+
93
+ for (const [name, link, color] of [
94
+ ["Ground A Pivot", "groundA", "#263238"],
95
+ ["Ground B Pivot", "groundB", "#263238"],
96
+ ["Crank Pin", "crankTip", "#8c2d25"],
97
+ ["Rocker Pin", "rockerTip", "#244b74"],
98
+ ]) {
99
+ fourBar.addPart(name, pivot(color), { mate: { connector: "center", toLink: link } });
100
+ }
101
+
102
+ for (const theta of [0, DEFAULT_THETA, 70, 95]) {
103
+ const solved = fourBar.solve({ theta });
104
+ const groundA = solved.getLinkPosition("groundA");
105
+ const groundB = solved.getLinkPosition("groundB");
106
+ const crankTip = solved.getLinkPosition("crankTip");
107
+ const rockerTip = solved.getLinkPosition("rockerTip");
108
+
109
+ verify.that(`closed loop converges at theta ${theta}`, () => solved.kinematics.converged);
110
+ verify.equal(`crank length holds at theta ${theta}`, Points.distance(groundA, crankTip), crankLen, 0.01);
111
+ verify.equal(`coupler length holds at theta ${theta}`, Points.distance(crankTip, rockerTip), couplerLen, 0.01);
112
+ verify.equal(`rocker length holds at theta ${theta}`, Points.distance(groundB, rockerTip), rockerLen, 0.01);
113
+ }
114
+
115
+ return fourBar;