forgecad 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/LICENSE +6 -4
  2. package/README.md +8 -4
  3. package/dist/assets/{AdminPage-DramHHDf.js → AdminPage-CDyGUinA.js} +2 -2
  4. package/dist/assets/{BenchmarkPage-Bjgkh5m9.js → BenchmarkPage-DfPMY_-d.js} +4 -15
  5. package/dist/assets/{BlogPage-n_HGP3Qm.js → BlogPage-kF0fkdJT.js} +2 -2
  6. package/dist/assets/{DocsPage-WCIkPmzC.js → DocsPage-B954L3YN.js} +9 -3
  7. package/dist/assets/EditorApp-Beb-IZ0y.js +14014 -0
  8. package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-CuDLxKqL.css} +698 -0
  9. package/dist/assets/{EmbedViewer-DEZKqdfW.js → EmbedViewer-C77B-TrF.js} +3 -3
  10. package/dist/assets/{LandingPageProofDriven-CeRIctuj.js → LandingPageProofDriven-Cr6fXMDj.js} +35 -37
  11. package/dist/assets/LegalPage-BRlScr9A.css +91 -0
  12. package/dist/assets/LegalPage-Dzklqmmg.js +39 -0
  13. package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
  14. package/dist/assets/{PricingPage-rIRa8p4Y.js → PricingPage-zWXkvlwl.js} +19 -19
  15. package/dist/assets/{SettingsPage-BqCUvEXM.js → SettingsPage-Bz0of4KQ.js} +2 -2
  16. package/dist/assets/app-CE3sYcV7.css +3890 -0
  17. package/dist/assets/{app-BUZqJvSO.js → app-D3kDkggg.js} +2305 -960
  18. package/dist/assets/cli/{render-lhGxj50Y.js → render-DSY3mMQa.js} +423 -30
  19. package/dist/assets/{constructionHistoryWorker-ipD1jcIv.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
  20. package/dist/assets/{evalWorker-CHXSe_-u.js → evalWorker-CU0Ke6DP.js} +7799 -4163
  21. package/dist/assets/{forgecad_geometry-BVnIeXMG.js → forgecad_geometry-Dgceylq9.js} +43 -1
  22. package/dist/assets/{forgecad_geometry_bg-DufhhCBV.wasm → forgecad_geometry_bg-dD4RNQF1.wasm} +0 -0
  23. package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-COyp8XXA.js} +927 -243
  24. package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
  25. package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
  26. package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
  27. package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
  28. package/dist/assets/{manifold-D1LZIHqn.js → manifold-BRI5prcH.js} +1 -1
  29. package/dist/assets/{manifold-C2fwoTgd.js → manifold-C-3h2M7p.js} +2 -2
  30. package/dist/assets/{manifold-BTkzxi9V.js → manifold-DNkrUWpA.js} +1 -1
  31. package/dist/assets/{reportWorker-Cq1qGmg0.js → reportWorker-CdBz5bNg.js} +7537 -10856
  32. package/dist/assets/{scalar-sampling-budget-D9Qv_UlJ.js → scalar-sampling-budget-wJF98aY9.js} +6943 -4345
  33. package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
  34. package/dist/assets/{renderSceneState-Dr0xPq1A.js → targets-B9sGB5nB.js} +27 -1
  35. package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
  36. package/dist/cli/render.html +1 -1
  37. package/dist/docs/index.html +2 -2
  38. package/dist/docs-raw/AI/ai-native-cad.md +50 -0
  39. package/dist/docs-raw/AI/usage.md +9 -17
  40. package/dist/docs-raw/CLI.md +71 -21
  41. package/dist/docs-raw/component-model.md +27 -11
  42. package/dist/docs-raw/generated/assembly.md +301 -212
  43. package/dist/docs-raw/generated/concepts.md +238 -240
  44. package/dist/docs-raw/generated/core.md +283 -6
  45. package/dist/docs-raw/generated/curves.md +274 -361
  46. package/dist/docs-raw/generated/lib.md +7 -1
  47. package/dist/docs-raw/generated/output.md +19 -4
  48. package/dist/docs-raw/generated/runtime-names.md +41 -0
  49. package/dist/docs-raw/generated/sdf.md +31 -0
  50. package/dist/docs-raw/generated/sheet-metal.md +9 -0
  51. package/dist/docs-raw/generated/sketch.md +44 -1
  52. package/dist/docs-raw/generated/viewport.md +14 -6
  53. package/dist/docs-raw/guides/coordinate-system.md +20 -16
  54. package/dist/docs-raw/guides/geometry-conventions.md +2 -2
  55. package/dist/docs-raw/guides/inspection-bundles.md +2 -1
  56. package/dist/docs-raw/guides/joint-design.md +24 -0
  57. package/dist/docs-raw/guides/positioning.md +13 -3
  58. package/dist/docs-raw/legal/privacy.md +63 -0
  59. package/dist/docs-raw/legal/software-license.md +55 -0
  60. package/dist/docs-raw/legal/terms.md +87 -0
  61. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +3 -3
  62. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
  63. package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
  64. package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
  65. package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
  66. package/dist/docs-raw/skills/forgecad-lld.md +1 -1
  67. package/dist/docs-raw/skills/forgecad-make-a-model.md +4 -4
  68. package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
  69. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
  70. package/dist/docs-raw/skills/forgecad-project.md +1 -1
  71. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +4 -4
  72. package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
  73. package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
  74. package/dist/docs-raw/skills/forgecad.md +4 -3
  75. package/dist/index.html +40 -12
  76. package/dist/llms.txt +8 -0
  77. package/dist/site.webmanifest +1 -1
  78. package/dist/sitemap.xml +49 -13
  79. package/dist-cli/{check-compiler-LOXCPEOI.js → check-compiler-SDX5QIXI.js} +1 -2
  80. package/dist-cli/{check-query-propagation-BAKNVWXR.js → check-query-propagation-EAYEFT77.js} +1 -2
  81. package/dist-cli/{chunk-RY43WF46.js → chunk-N4O47JLF.js} +13772 -9938
  82. package/dist-cli/forgecad.js +2387 -899
  83. package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
  84. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  85. package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
  86. package/dist-skill/CONTEXT.md +1120 -724
  87. package/dist-skill/SKILL.md +3 -2
  88. package/dist-skill/docs/API/core/concepts.md +64 -1
  89. package/dist-skill/docs/CLI.md +71 -21
  90. package/dist-skill/docs/generated/assembly.md +277 -229
  91. package/dist-skill/docs/generated/core.md +283 -6
  92. package/dist-skill/docs/generated/curves.md +272 -362
  93. package/dist-skill/docs/generated/lib.md +7 -1
  94. package/dist-skill/docs/generated/output.md +19 -4
  95. package/dist-skill/docs/generated/runtime-names.md +41 -0
  96. package/dist-skill/docs/generated/sdf.md +31 -0
  97. package/dist-skill/docs/generated/sheet-metal.md +9 -0
  98. package/dist-skill/docs/generated/sketch.md +44 -2
  99. package/dist-skill/docs/generated/viewport.md +5 -90
  100. package/dist-skill/docs/guides/coordinate-system.md +20 -16
  101. package/dist-skill/docs/guides/geometry-conventions.md +2 -2
  102. package/dist-skill/docs/guides/inspection-bundles.md +2 -1
  103. package/dist-skill/docs/guides/joint-design.md +24 -0
  104. package/dist-skill/docs/guides/positioning.md +13 -3
  105. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +2 -2
  106. package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
  107. package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
  108. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
  109. package/dist-skill/library/forgecad-make-a-model/SKILL.md +3 -3
  110. package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
  111. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
  112. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +3 -3
  113. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  114. package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
  115. package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
  116. package/examples/api/assembly-kinematics-limb.forge.js +116 -0
  117. package/examples/api/connector-frame-rig-chain.forge.js +102 -0
  118. package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
  119. package/examples/api/exact-surface-studio.forge.js +6 -8
  120. package/examples/api/helix-basics.forge.js +6 -6
  121. package/examples/api/lean-foundations/README.md +12 -0
  122. package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
  123. package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
  124. package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
  125. package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
  126. package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
  127. package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
  128. package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
  129. package/examples/api/route3d-elbow.forge.js +68 -0
  130. package/examples/api/transition-curves.forge.js +44 -15
  131. package/examples/api/y-blend-corner-showcase.forge.js +0 -2
  132. package/examples/generative/coral-vase.forge.js +1 -1
  133. package/examples/nurbs-tube.forge.js +1 -1
  134. package/package.json +14 -18
  135. package/dist/assets/EditorApp-CP9Za6tm.js +0 -13630
  136. package/dist/assets/app-CsHnaBWt.css +0 -1789
  137. package/dist/docs-raw/API/README.md +0 -16
  138. package/dist/docs-raw/API/core/concepts.md +0 -118
  139. package/dist/docs-raw/INDEX.md +0 -138
  140. package/dist/docs-raw/RELEASING.md +0 -87
  141. package/dist/docs-raw/agent-native-api.md +0 -27
  142. package/dist/docs-raw/beta-deployment.md +0 -304
  143. package/dist/docs-raw/beta-operations.md +0 -325
  144. package/dist/docs-raw/blueprint-first.md +0 -145
  145. package/dist/docs-raw/cli-monetization.md +0 -112
  146. package/dist/docs-raw/coding-best-practices.md +0 -120
  147. package/dist/docs-raw/coding.md +0 -340
  148. package/dist/docs-raw/deployment.md +0 -374
  149. package/dist/docs-raw/guides/skill-maintenance.md +0 -161
  150. package/dist/docs-raw/guides/surface-members.md +0 -82
  151. package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
  152. package/dist/docs-raw/internals/compiler.md +0 -307
  153. package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
  154. package/dist/docs-raw/internals/constraint-solver.md +0 -176
  155. package/dist/docs-raw/internals/shape-from-slices.md +0 -152
  156. package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
  157. package/dist/docs-raw/platform/admin.md +0 -45
  158. package/dist/docs-raw/platform/architecture.md +0 -82
  159. package/dist/docs-raw/platform/auth.md +0 -139
  160. package/dist/docs-raw/platform/email.md +0 -67
  161. package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
  162. package/dist/docs-raw/platform/observability.md +0 -197
  163. package/dist/docs-raw/platform/projects.md +0 -111
  164. package/dist/docs-raw/platform/sharing.md +0 -90
  165. package/dist/docs-raw/product/README.md +0 -39
  166. package/dist/docs-raw/product/api-as-product-language.md +0 -13
  167. package/dist/docs-raw/product/business-model.md +0 -15
  168. package/dist/docs-raw/product/competitive-positioning.md +0 -17
  169. package/dist/docs-raw/product/creative-manufacturing.md +0 -15
  170. package/dist/docs-raw/product/founder-story.md +0 -11
  171. package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
  172. package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
  173. package/dist/docs-raw/product/product-loop.md +0 -17
  174. package/dist/docs-raw/product/strategic-decisions.md +0 -22
  175. package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
  176. package/dist/docs-raw/product/user-segments.md +0 -15
  177. package/dist/docs-raw/product/vision.md +0 -26
  178. package/dist/docs-raw/rl-environments.md +0 -508
  179. package/dist/docs-raw/runbook.md +0 -611
  180. package/dist-cli/check-compiler-LOXCPEOI.js.map +0 -1
  181. package/dist-cli/check-query-propagation-BAKNVWXR.js.map +0 -1
  182. package/dist-cli/chunk-RY43WF46.js.map +0 -1
  183. package/dist-cli/forgecad.js.map +0 -1
  184. package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
  185. package/dist-cli/solver-46FFSK2U.js.map +0 -1
  186. package/dist-skill/SKILL-dev.md +0 -145
  187. package/dist-skill/docs-dev/API/core/concepts.md +0 -118
  188. package/dist-skill/docs-dev/CLI.md +0 -647
  189. package/dist-skill/docs-dev/agent-native-api.md +0 -27
  190. package/dist-skill/docs-dev/blueprint-first.md +0 -145
  191. package/dist-skill/docs-dev/coding-best-practices.md +0 -120
  192. package/dist-skill/docs-dev/coding.md +0 -340
  193. package/dist-skill/docs-dev/component-model.md +0 -164
  194. package/dist-skill/docs-dev/generated/assembly.md +0 -794
  195. package/dist-skill/docs-dev/generated/core.md +0 -2117
  196. package/dist-skill/docs-dev/generated/curves.md +0 -2583
  197. package/dist-skill/docs-dev/generated/lib.md +0 -169
  198. package/dist-skill/docs-dev/generated/output.md +0 -247
  199. package/dist-skill/docs-dev/generated/sdf.md +0 -446
  200. package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
  201. package/dist-skill/docs-dev/generated/sketch.md +0 -1811
  202. package/dist-skill/docs-dev/generated/viewport.md +0 -585
  203. package/dist-skill/docs-dev/generated/wood.md +0 -108
  204. package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
  205. package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
  206. package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
  207. package/dist-skill/docs-dev/guides/joint-design.md +0 -78
  208. package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
  209. package/dist-skill/docs-dev/guides/positioning.md +0 -161
  210. package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
  211. package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
  212. package/dist-skill/docs-dev/internals/compiler.md +0 -307
  213. package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
  214. package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
  215. package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
  216. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.mjs +0 -289
@@ -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()
@@ -60,7 +60,7 @@ Use today's date for the directory. Use the user's current ForgeCAD project when
60
60
  - Return the final geometry (single shape, array, or named objects array)
61
61
  - 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
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 run --connectivity` 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.
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
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.
65
65
 
66
66
  ## Manufacturing Process Is Not Assumed
@@ -183,7 +183,7 @@ Before telling the user the model is done, prove both technical validity and vis
183
183
  1. State the intended physical component graph. Decide whether the final artifact should be one connected component, several intentionally separate components, or a selected assembly plus named ghosts. Then run:
184
184
 
185
185
  ```bash
186
- forgecad run model.forge.js --connectivity
186
+ forgecad inspect physical components model.forge.js --camera iso
187
187
  ```
188
188
 
189
189
  The reported component count must match the design intent. Treat unexpected islands, accidental fusion, or bbox-only "touching" that does not make physical sense as model bugs.
@@ -292,7 +292,7 @@ For important components, collect both:
292
292
  - Context view — neighbors present, proving the part belongs in the final assembly.
293
293
  - Focus view — only the relevant objects visible, making small gaps, intersections, missing seats, and floating parts easy to see.
294
294
 
295
- Prefer CLI `--focus` / `--hide` filters, named views, or parameter-selected diagnostic modes over changing production geometry. Use the object names from `node dist-cli/forgecad.js run model.forge.js --quality live` when you are unsure what the filters should target.
295
+ Prefer CLI `--focus` / `--hide` filters, named views, or parameter-selected diagnostic modes over changing production geometry. Use the object paths from `node dist-cli/forgecad.js ls model.forge.js --tree` when you are unsure what the filters should target.
296
296
 
297
297
  ### Structured inspection bundles
298
298
 
@@ -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`
@@ -72,13 +72,13 @@ ForgeCAD commands.
72
72
  inference.
73
73
 
74
74
  ```bash
75
- ./bin/forgecad run task/reference/<asset> --quality live --details
75
+ ./bin/forgecad ls task/reference/<asset> --quality live --long
76
76
  ./bin/forgecad render 3d task/reference/<asset> outputs/reference.png --camera iso --edges thin --size 900
77
77
  ./bin/forgecad inspect sections sample task/reference/<asset> outputs/reference-sections --count 5 --size 700
78
78
  ./bin/forgecad inspect section task/reference/<asset> --plane yz --offset 0 --ray width:-50,0:50,0 --size 700
79
79
  ```
80
80
 
81
- For 3MF references, the `run --details` output includes a source structure
81
+ For 3MF references, the `forgecad run` output includes a source structure
82
82
  table with stable `3mf:build:...:object:...` refs, automatic item names,
83
83
  per-item bounding boxes, and triangle counts. Treat that table as part of
84
84
  the evidence: the final model may be one part or many parts, but you should
@@ -88,7 +88,7 @@ ForgeCAD commands.
88
88
  unique probe directory with `result.json`; later you can replay that probe
89
89
  against the candidate with `./bin/forgecad inspect replay <result.json>
90
90
  --source submission/main.forge.js`.
91
- For 3MF references, the `run --details` output includes a source structure
91
+ For 3MF references, the `forgecad run` output includes a source structure
92
92
  table with stable `3mf:build:...:object:...` refs, automatic item names,
93
93
  per-item bounding boxes, and triangle counts. Treat that table as part of
94
94
  the evidence: the final model may be one part or many parts, but you should
@@ -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;
@@ -0,0 +1,116 @@
1
+ // Oriented serial-chain limb posed entirely by the assembly rig API.
2
+ //
3
+ // The motion skeleton is authored as kinematic LINKS in the limb's sagittal
4
+ // (Y/Z) plane; EDGES hold the bone lengths; ANGLES drive the hip and knee.
5
+ // Bone geometry is built once at the origin and attached with two-connector
6
+ // MATES, so each bone ROTATES to span between its two solved links — it does
7
+ // not merely translate. This is the oriented serial chain that position-only
8
+ // link mates could not pose.
9
+
10
+ scene({
11
+ background: { top: '#cdd3da', bottom: '#5d6770' },
12
+ views: {
13
+ // Look straight down +X so the Y/Z motion plane faces the camera.
14
+ side: { camera: { position: [320, -25, -35], target: [0, -25, -35], up: [0, 0, 1], fov: 34 } },
15
+ iso: { camera: { position: [240, -190, 90], target: [0, -25, -40], up: [0, 0, 1], fov: 34 } },
16
+ },
17
+ lights: [
18
+ { type: 'ambient', color: '#eef0f4', intensity: 0.4 },
19
+ { type: 'directional', position: [240, -200, 320], color: '#fff1dd', intensity: 2.2, castShadow: true },
20
+ ],
21
+ });
22
+
23
+ const r = 7;
24
+
25
+ // A bone running from its `near` connector at the origin to `far` at [0, L, 0].
26
+ // Its connector axis (+Y) is the bone's own length direction.
27
+ function bone(L, color) {
28
+ return cylinder(L, r)
29
+ .pointAlong([0, 1, 0])
30
+ .color(color)
31
+ .material({ metalness: 0.1, roughness: 0.55 })
32
+ .withConnectors({
33
+ near: connector({ origin: [0, 0, 0], axis: [0, 1, 0] }),
34
+ far: connector({ origin: [0, L, 0], axis: [0, 1, 0] }),
35
+ });
36
+ }
37
+
38
+ // ── Skeleton geometry (all at x = 0 → the limb lives in the Y/Z plane) ───────
39
+ const HIP = [0, 0, 0];
40
+ const KNEE = [0, 20, -70]; // forward + down from the hip
41
+ const ANKLE = [0, 50, -110]; // forward + down from the knee
42
+ const HIP_REF = [0, -40, 0]; // a forward marker that fixes the hip's zero angle
43
+
44
+ const thighLen = Points.distance(HIP, KNEE);
45
+ const shinLen = Points.distance(KNEE, ANKLE);
46
+
47
+ // Drivers: return the rig and the editor exposes these as solver-backed
48
+ // assembly controls. Dragging them re-solves the link graph, then updates
49
+ // part transforms; the bone meshes stay local and are reused.
50
+ const DEFAULT_HIP_SWING = 18;
51
+ const DEFAULT_KNEE_BEND = 38;
52
+
53
+ const limb = assembly('Oriented Limb')
54
+ .link('hipRef', { at: HIP_REF, fixed: true })
55
+ .link('hip', { at: HIP, fixed: true })
56
+ .link('knee', { at: KNEE })
57
+ .link('ankle', { at: ANKLE })
58
+ .edgeBetweenLinks('hip', 'knee', { name: 'thigh' })
59
+ .edgeBetweenLinks('knee', 'ankle', { name: 'shin' })
60
+ // The control `default` is the live param value, so the editor sliders (and
61
+ // `--param` on the CLI) drive the pose directly.
62
+ .addAngleBetweenLinks('hipRef', 'hip', 'knee', {
63
+ name: 'hipSwing',
64
+ control: { min: -40, max: 60, default: DEFAULT_HIP_SWING },
65
+ })
66
+ .addAngleBetweenLinks('hip', 'knee', 'ankle', {
67
+ name: 'kneeBend',
68
+ control: { min: 0, max: 120, default: DEFAULT_KNEE_BEND },
69
+ });
70
+
71
+ // ── Skin: bones span their links via two-connector mates ─────────────────────
72
+ limb.addPart(
73
+ 'HipHub',
74
+ sphere(11)
75
+ .color('#2c2f36')
76
+ .material({ metalness: 0.2, roughness: 0.5 })
77
+ .withConnectors({ center: connector({ origin: [0, 0, 0], axis: [1, 0, 0] }) }),
78
+ { mate: { connector: 'center', toLink: 'hip' } },
79
+ );
80
+
81
+ limb.addPart('Thigh', bone(thighLen, '#d24b4b'), {
82
+ mate: [
83
+ { connector: 'near', toLink: 'hip' },
84
+ { connector: 'far', toLink: 'knee' },
85
+ ],
86
+ });
87
+
88
+ limb.addPart('Shin', bone(shinLen, '#2f3338'), {
89
+ mate: [
90
+ { connector: 'near', toLink: 'knee' },
91
+ { connector: 'far', toLink: 'ankle' },
92
+ ],
93
+ });
94
+
95
+ // Wheel hub at the ankle — single mate, position-only is exactly right here
96
+ // (a spin-symmetric wheel needs no orientation).
97
+ limb.addPart(
98
+ 'Wheel',
99
+ cylinder(13, 18)
100
+ .pointAlong([1, 0, 0])
101
+ .translate(-6.5, 0, 0)
102
+ .color('#b23b3b')
103
+ .material({ metalness: 0.1, roughness: 0.6 })
104
+ .withConnectors({ hub: connector({ origin: [0, 0, 0], axis: [1, 0, 0] }) }),
105
+ { mate: { connector: 'hub', toLink: 'ankle' } },
106
+ );
107
+
108
+ // ── Validation: the solved bones actually reach their joints ─────────────────
109
+ const solved = limb.solve({ hipSwing: DEFAULT_HIP_SWING, kneeBend: DEFAULT_KNEE_BEND });
110
+ const knee = solved.getLinkPosition('knee');
111
+ const ankle = solved.getLinkPosition('ankle');
112
+ verify.equal('thigh edge holds its length', Points.distance(solved.getLinkPosition('hip'), knee), thighLen, 0.01);
113
+ verify.equal('shin edge holds its length', Points.distance(knee, ankle), shinLen, 0.01);
114
+ verify.that('the limb stays in the Y/Z plane', () => Math.abs(knee[0]) < 1e-6 && Math.abs(ankle[0]) < 1e-6);
115
+
116
+ return limb;