forgecad 0.9.14 → 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.
- package/LICENSE +6 -4
- package/README.md +8 -4
- package/dist/assets/{AdminPage-eWGs2K6H.js → AdminPage-CDyGUinA.js} +2 -2
- package/dist/assets/{BenchmarkPage-CTrLKfpo.js → BenchmarkPage-DfPMY_-d.js} +4 -15
- package/dist/assets/{BlogPage-5nPesyds.js → BlogPage-kF0fkdJT.js} +2 -2
- package/dist/assets/{DocsPage-C4Y3nbYc.js → DocsPage-B954L3YN.js} +9 -3
- package/dist/assets/EditorApp-Beb-IZ0y.js +14014 -0
- package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-CuDLxKqL.css} +698 -0
- package/dist/assets/{EmbedViewer-C8fB4n5U.js → EmbedViewer-C77B-TrF.js} +3 -3
- package/dist/assets/{LandingPageProofDriven-jSz0LaMM.js → LandingPageProofDriven-Cr6fXMDj.js} +35 -37
- package/dist/assets/LegalPage-BRlScr9A.css +91 -0
- package/dist/assets/LegalPage-Dzklqmmg.js +39 -0
- package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
- package/dist/assets/{PricingPage-B83B90zh.js → PricingPage-zWXkvlwl.js} +19 -19
- package/dist/assets/{SettingsPage-DY889pcu.js → SettingsPage-Bz0of4KQ.js} +2 -2
- package/dist/assets/app-CE3sYcV7.css +3890 -0
- package/dist/assets/{app-bEww1ic4.js → app-D3kDkggg.js} +2293 -946
- package/dist/assets/cli/{render-Cho2uKG_.js → render-DSY3mMQa.js} +337 -7
- package/dist/assets/{constructionHistoryWorker-HYwzJY4m.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
- package/dist/assets/{evalWorker-CjQwJSE-.js → evalWorker-CU0Ke6DP.js} +7800 -4164
- package/dist/assets/{forgecad_geometry-CH2nvuLA.js → forgecad_geometry-Dgceylq9.js} +43 -1
- package/dist/assets/forgecad_geometry_bg-dD4RNQF1.wasm +0 -0
- package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-COyp8XXA.js} +927 -243
- package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
- package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
- package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
- package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
- package/dist/assets/{manifold-CG9Fokx-.js → manifold-BRI5prcH.js} +1 -1
- package/dist/assets/{manifold-uRzgk5O8.js → manifold-C-3h2M7p.js} +2 -2
- package/dist/assets/{manifold-rmfAcdwF.js → manifold-DNkrUWpA.js} +1 -1
- package/dist/assets/{reportWorker-4cW_ZpoS.js → reportWorker-CdBz5bNg.js} +7538 -10857
- package/dist/assets/{scalar-sampling-budget-CfDiFvh7.js → scalar-sampling-budget-wJF98aY9.js} +6935 -4331
- package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
- package/dist/assets/{solver-DuJAO8S6.js → solver-BZ9LPTHs.js} +1 -1
- package/dist/assets/solver_bg-DAHZJ_rw.wasm +0 -0
- package/dist/assets/{targets-D6PWsv6X.js → targets-B9sGB5nB.js} +1 -1
- package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/AI/ai-native-cad.md +50 -0
- package/dist/docs-raw/AI/usage.md +3 -12
- package/dist/docs-raw/CLI.md +30 -10
- package/dist/docs-raw/component-model.md +27 -11
- package/dist/docs-raw/generated/assembly.md +301 -212
- package/dist/docs-raw/generated/concepts.md +235 -237
- package/dist/docs-raw/generated/core.md +283 -6
- package/dist/docs-raw/generated/curves.md +274 -361
- package/dist/docs-raw/generated/lib.md +7 -1
- package/dist/docs-raw/generated/output.md +19 -4
- package/dist/docs-raw/generated/runtime-names.md +41 -0
- package/dist/docs-raw/generated/sdf.md +31 -0
- package/dist/docs-raw/generated/sheet-metal.md +9 -0
- package/dist/docs-raw/generated/sketch.md +44 -1
- package/dist/docs-raw/generated/viewport.md +11 -3
- package/dist/docs-raw/guides/coordinate-system.md +20 -16
- package/dist/docs-raw/guides/geometry-conventions.md +2 -2
- package/dist/docs-raw/guides/inspection-bundles.md +2 -1
- package/dist/docs-raw/guides/joint-design.md +24 -0
- package/dist/docs-raw/guides/positioning.md +13 -3
- package/dist/docs-raw/legal/privacy.md +63 -0
- package/dist/docs-raw/legal/software-license.md +55 -0
- package/dist/docs-raw/legal/terms.md +87 -0
- package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +1 -1
- package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
- package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
- package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
- package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
- package/dist/docs-raw/skills/forgecad-lld.md +1 -1
- package/dist/docs-raw/skills/forgecad-make-a-model.md +1 -1
- package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
- package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
- package/dist/docs-raw/skills/forgecad-project.md +1 -1
- package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +1 -1
- package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
- package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
- package/dist/docs-raw/skills/forgecad.md +4 -3
- package/dist/index.html +40 -12
- package/dist/llms.txt +8 -0
- package/dist/site.webmanifest +1 -1
- package/dist/sitemap.xml +49 -13
- package/dist-cli/{check-compiler-U5SOPN7X.js → check-compiler-SDX5QIXI.js} +1 -2
- package/dist-cli/{check-query-propagation-XOKNSSYU.js → check-query-propagation-EAYEFT77.js} +1 -2
- package/dist-cli/{chunk-EXWGNL6K.js → chunk-N4O47JLF.js} +12540 -9046
- package/dist-cli/forgecad.js +1786 -679
- package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
- package/dist-cli/forgecad_geometry_bg.wasm +0 -0
- package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
- package/dist-cli/solver_bg.wasm +0 -0
- package/dist-skill/CONTEXT.md +1117 -721
- package/dist-skill/SKILL.md +3 -2
- package/dist-skill/docs/API/core/concepts.md +64 -1
- package/dist-skill/docs/CLI.md +30 -10
- package/dist-skill/docs/generated/assembly.md +277 -229
- package/dist-skill/docs/generated/core.md +283 -6
- package/dist-skill/docs/generated/curves.md +272 -362
- package/dist-skill/docs/generated/lib.md +7 -1
- package/dist-skill/docs/generated/output.md +19 -4
- package/dist-skill/docs/generated/runtime-names.md +41 -0
- package/dist-skill/docs/generated/sdf.md +31 -0
- package/dist-skill/docs/generated/sheet-metal.md +9 -0
- package/dist-skill/docs/generated/sketch.md +44 -2
- package/dist-skill/docs/generated/viewport.md +2 -87
- package/dist-skill/docs/guides/coordinate-system.md +20 -16
- package/dist-skill/docs/guides/geometry-conventions.md +2 -2
- package/dist-skill/docs/guides/inspection-bundles.md +2 -1
- package/dist-skill/docs/guides/joint-design.md +24 -0
- package/dist-skill/docs/guides/positioning.md +13 -3
- package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
- package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
- package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
- package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
- package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
- package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
- package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
- package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
- package/examples/api/assembly-kinematics-limb.forge.js +116 -0
- package/examples/api/connector-frame-rig-chain.forge.js +102 -0
- package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
- package/examples/api/exact-surface-studio.forge.js +6 -8
- package/examples/api/helix-basics.forge.js +6 -6
- package/examples/api/lean-foundations/README.md +12 -0
- package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
- package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
- package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
- package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
- package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
- package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
- package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
- package/examples/api/route3d-elbow.forge.js +68 -0
- package/examples/api/transition-curves.forge.js +44 -15
- package/examples/api/y-blend-corner-showcase.forge.js +0 -2
- package/examples/generative/coral-vase.forge.js +1 -1
- package/examples/nurbs-tube.forge.js +1 -1
- package/package.json +14 -13
- package/dist/assets/EditorApp-lXv53A1m.js +0 -13610
- package/dist/assets/app-CsHnaBWt.css +0 -1789
- package/dist/assets/forgecad_geometry_bg-C5_E9Oa9.wasm +0 -0
- package/dist/assets/solver_bg-CWvv4lnN.wasm +0 -0
- package/dist/docs-raw/API/README.md +0 -16
- package/dist/docs-raw/API/core/concepts.md +0 -118
- package/dist/docs-raw/INDEX.md +0 -138
- package/dist/docs-raw/RELEASING.md +0 -87
- package/dist/docs-raw/agent-native-api.md +0 -27
- package/dist/docs-raw/beta-deployment.md +0 -304
- package/dist/docs-raw/beta-operations.md +0 -325
- package/dist/docs-raw/blueprint-first.md +0 -145
- package/dist/docs-raw/cli-monetization.md +0 -112
- package/dist/docs-raw/coding-best-practices.md +0 -120
- package/dist/docs-raw/coding.md +0 -340
- package/dist/docs-raw/deployment.md +0 -374
- package/dist/docs-raw/guides/skill-maintenance.md +0 -161
- package/dist/docs-raw/guides/surface-members.md +0 -82
- package/dist/docs-raw/harbor-cli.md +0 -854
- package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
- package/dist/docs-raw/internals/compiler.md +0 -307
- package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
- package/dist/docs-raw/internals/constraint-solver.md +0 -176
- package/dist/docs-raw/internals/shape-from-slices.md +0 -152
- package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
- package/dist/docs-raw/platform/admin.md +0 -45
- package/dist/docs-raw/platform/architecture.md +0 -82
- package/dist/docs-raw/platform/auth.md +0 -139
- package/dist/docs-raw/platform/email.md +0 -67
- package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
- package/dist/docs-raw/platform/observability.md +0 -197
- package/dist/docs-raw/platform/projects.md +0 -111
- package/dist/docs-raw/platform/sharing.md +0 -90
- package/dist/docs-raw/product/README.md +0 -39
- package/dist/docs-raw/product/api-as-product-language.md +0 -13
- package/dist/docs-raw/product/business-model.md +0 -15
- package/dist/docs-raw/product/competitive-positioning.md +0 -17
- package/dist/docs-raw/product/creative-manufacturing.md +0 -15
- package/dist/docs-raw/product/founder-story.md +0 -11
- package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
- package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
- package/dist/docs-raw/product/product-loop.md +0 -17
- package/dist/docs-raw/product/strategic-decisions.md +0 -22
- package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
- package/dist/docs-raw/product/user-segments.md +0 -15
- package/dist/docs-raw/product/vision.md +0 -26
- package/dist/docs-raw/rl-environments.md +0 -350
- package/dist/docs-raw/runbook.md +0 -611
- package/dist-cli/check-compiler-U5SOPN7X.js.map +0 -1
- package/dist-cli/check-query-propagation-XOKNSSYU.js.map +0 -1
- package/dist-cli/chunk-EXWGNL6K.js.map +0 -1
- package/dist-cli/forgecad.js.map +0 -1
- package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
- package/dist-cli/solver-46FFSK2U.js.map +0 -1
- package/dist-skill/SKILL-dev.md +0 -145
- package/dist-skill/docs-dev/API/core/concepts.md +0 -118
- package/dist-skill/docs-dev/CLI.md +0 -677
- package/dist-skill/docs-dev/agent-native-api.md +0 -27
- package/dist-skill/docs-dev/blueprint-first.md +0 -145
- package/dist-skill/docs-dev/coding-best-practices.md +0 -120
- package/dist-skill/docs-dev/coding.md +0 -340
- package/dist-skill/docs-dev/component-model.md +0 -164
- package/dist-skill/docs-dev/generated/assembly.md +0 -794
- package/dist-skill/docs-dev/generated/core.md +0 -2117
- package/dist-skill/docs-dev/generated/curves.md +0 -2583
- package/dist-skill/docs-dev/generated/lib.md +0 -169
- package/dist-skill/docs-dev/generated/output.md +0 -247
- package/dist-skill/docs-dev/generated/sdf.md +0 -446
- package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
- package/dist-skill/docs-dev/generated/sketch.md +0 -1811
- package/dist-skill/docs-dev/generated/viewport.md +0 -585
- package/dist-skill/docs-dev/generated/wood.md +0 -108
- package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
- package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
- package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
- package/dist-skill/docs-dev/guides/joint-design.md +0 -78
- package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
- package/dist-skill/docs-dev/guides/positioning.md +0 -161
- package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
- package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
- package/dist-skill/docs-dev/internals/compiler.md +0 -307
- package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
- package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
- package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
- package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.mjs +0 -289
|
@@ -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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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()
|
|
@@ -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
|
|
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
|
-
-
|
|
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`
|
|
@@ -62,7 +62,7 @@ Routing:
|
|
|
62
62
|
Run the bundled helper:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
python
|
|
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;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Connector-frame rig chain: serial articulated parts need full connector frames.
|
|
2
|
+
//
|
|
3
|
+
// Use link() graphs for solved point skeletons and closed loops. Use connect()
|
|
4
|
+
// when real geometry must inherit orientation from a joint frame. Here the upper
|
|
5
|
+
// leg's local +X axis is locked by connector up vectors, so the part points from
|
|
6
|
+
// the hip pivot toward the knee pivot at every solved pose. Return the Assembly
|
|
7
|
+
// itself so editor controls call solve(state) instead of stacking viewport FK.
|
|
8
|
+
|
|
9
|
+
const DEFAULT_POSE = { hip: 22, knee: -42, wheel: 0 };
|
|
10
|
+
|
|
11
|
+
const upperLength = 68;
|
|
12
|
+
const lowerLength = 58;
|
|
13
|
+
const legWidth = 10;
|
|
14
|
+
const legThickness = 8;
|
|
15
|
+
const hubRadius = 9;
|
|
16
|
+
const wheelRadius = 16;
|
|
17
|
+
const wheelThickness = 8;
|
|
18
|
+
|
|
19
|
+
function hingeConnector(origin, axis, up, type = "planar-hinge") {
|
|
20
|
+
return connector(type, { origin, axis, up, kind: "revolute" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function legSegment(length, colorHex) {
|
|
24
|
+
const body = box(length, legWidth, legThickness).translate(length / 2, 0, -legThickness / 2);
|
|
25
|
+
const nearHub = cylinder(legThickness, hubRadius).translate(0, 0, -legThickness / 2);
|
|
26
|
+
const farHub = cylinder(legThickness, hubRadius).translate(length, 0, -legThickness / 2);
|
|
27
|
+
|
|
28
|
+
return group(
|
|
29
|
+
{ name: "Body", shape: body },
|
|
30
|
+
{ name: "Near hub", shape: nearHub },
|
|
31
|
+
{ name: "Far hub", shape: farHub },
|
|
32
|
+
).color(colorHex);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hipDrum = group({
|
|
36
|
+
name: "Drum",
|
|
37
|
+
shape: cylinder(18, 14).translate(0, 0, -9).color("#495057"),
|
|
38
|
+
}).withConnectors({
|
|
39
|
+
hip: hingeConnector([0, 0, 0], [0, 0, 1], [1, 0, 0]),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const upperLeg = legSegment(upperLength, "#d55e5e").withConnectors({
|
|
43
|
+
hip: hingeConnector([0, 0, 0], [0, 0, -1], [1, 0, 0]),
|
|
44
|
+
knee: hingeConnector([upperLength, 0, 0], [0, 0, 1], [1, 0, 0]),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const lowerLeg = legSegment(lowerLength, "#3d87c7").withConnectors({
|
|
48
|
+
knee: hingeConnector([0, 0, 0], [0, 0, -1], [1, 0, 0]),
|
|
49
|
+
axle: hingeConnector([lowerLength, 0, 0], [0, 1, 0], [1, 0, 0], "wheel-axle"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const wheel = group({
|
|
53
|
+
name: "Wheel",
|
|
54
|
+
shape: cylinder(wheelThickness, wheelRadius)
|
|
55
|
+
.pointAlong([0, 1, 0])
|
|
56
|
+
.translate(0, -wheelThickness / 2, 0)
|
|
57
|
+
.color("#212529"),
|
|
58
|
+
}).withConnectors({
|
|
59
|
+
axle: hingeConnector([0, 0, 0], [0, -1, 0], [1, 0, 0], "wheel-axle"),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const rig = assembly("Connector Frame Rig Chain")
|
|
63
|
+
.addPart("Hip Drum", hipDrum)
|
|
64
|
+
.addPart("Upper Leg", upperLeg)
|
|
65
|
+
.addPart("Lower Leg", lowerLeg)
|
|
66
|
+
.addPart("Wheel", wheel)
|
|
67
|
+
.connect("Hip Drum.hip", "Upper Leg.hip", { as: "hip", min: -35, max: 55, default: DEFAULT_POSE.hip })
|
|
68
|
+
.connect("Upper Leg.knee", "Lower Leg.knee", { as: "knee", min: -90, max: 20, default: DEFAULT_POSE.knee })
|
|
69
|
+
.connect("Lower Leg.axle", "Wheel.axle", { as: "wheel", min: -180, max: 180, default: DEFAULT_POSE.wheel });
|
|
70
|
+
|
|
71
|
+
const solved = rig.solve(DEFAULT_POSE);
|
|
72
|
+
|
|
73
|
+
const upper = solved.getTransform("Upper Leg");
|
|
74
|
+
const lower = solved.getTransform("Lower Leg");
|
|
75
|
+
const wheelTx = solved.getTransform("Wheel");
|
|
76
|
+
|
|
77
|
+
const hip = upper.point([0, 0, 0]);
|
|
78
|
+
const kneeFromUpper = upper.point([upperLength, 0, 0]);
|
|
79
|
+
const kneeFromLower = lower.point([0, 0, 0]);
|
|
80
|
+
const axleFromLower = lower.point([lowerLength, 0, 0]);
|
|
81
|
+
const axleFromWheel = wheelTx.point([0, 0, 0]);
|
|
82
|
+
|
|
83
|
+
const upperLocalX = upper.vector([1, 0, 0]);
|
|
84
|
+
const hipToKnee = Points.direction(hip, kneeFromUpper);
|
|
85
|
+
|
|
86
|
+
verify.equal("knee connector origins coincide", Points.distance(kneeFromUpper, kneeFromLower), 0, 0.001);
|
|
87
|
+
verify.equal("wheel axle connector origins coincide", Points.distance(axleFromLower, axleFromWheel), 0, 0.001);
|
|
88
|
+
verify.equal("upper leg local +X points hip to knee", Points.distance(upperLocalX, hipToKnee), 0, 0.001);
|
|
89
|
+
|
|
90
|
+
scene({
|
|
91
|
+
background: { top: "#edf2f7", bottom: "#ffffff" },
|
|
92
|
+
camera: { position: [115, -190, 120], target: [45, 12, 0], fov: 34 },
|
|
93
|
+
environment: { preset: "studio", intensity: 0.45 },
|
|
94
|
+
lights: [
|
|
95
|
+
{ type: "ambient", color: "#e8edf5", intensity: 0.25 },
|
|
96
|
+
{ type: "directional", position: [120, -140, 180], target: [35, 5, 0], color: "#fff5e6", intensity: 1.6, castShadow: true },
|
|
97
|
+
{ type: "directional", position: [-80, 100, 120], target: [40, 15, 0], color: "#c9ddff", intensity: 0.7 },
|
|
98
|
+
],
|
|
99
|
+
ground: { visible: true, color: "#f6f8fa", height: -wheelRadius, receiveShadow: true },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return rig;
|