forgecad 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +97 -0
  2. package/README.md +354 -0
  3. package/dist/assets/evalWorker-BYHXxh15.js +461 -0
  4. package/dist/assets/index--CYbOPKS.js +5797 -0
  5. package/dist/assets/manifold-65fIQlgQ.js +20 -0
  6. package/dist/assets/manifold-B85M7kop.js +20 -0
  7. package/dist/assets/manifold-B8h_vZ5O.js +16 -0
  8. package/dist/assets/manifold-D9yvTBHx.wasm +0 -0
  9. package/dist/assets/manifold-d1UpyLJ8.js +20 -0
  10. package/dist/assets/reportWorker-B1Zdrz9l.js +494 -0
  11. package/dist/index.html +16 -0
  12. package/dist-cli/forgecad.js +44464 -0
  13. package/dist-skill/SKILL.md +4635 -0
  14. package/examples/3d-printer.forge.js +328 -0
  15. package/examples/5-figen-robot-hand.forge.js +283 -0
  16. package/examples/ac-unit-glm47.forge.js +108 -0
  17. package/examples/ac-unit-glm5.forge.js +174 -0
  18. package/examples/ac-unit-kimi25.forge.js +236 -0
  19. package/examples/ac-unit-minimax.forge.js +123 -0
  20. package/examples/ac-unit.forge.js +126 -0
  21. package/examples/adjustable-table.forge.js +191 -0
  22. package/examples/api/assembly-gear-coupling.forge.js +32 -0
  23. package/examples/api/assembly-mechanism.forge.js +111 -0
  24. package/examples/api/attachTo-basics.forge.js +45 -0
  25. package/examples/api/benchy-style-hull.forge.js +89 -0
  26. package/examples/api/bill-of-materials.forge.js +46 -0
  27. package/examples/api/boolean-operations.forge.js +48 -0
  28. package/examples/api/bounding-box-visualizer.forge.js +58 -0
  29. package/examples/api/brep-exportable.forge.js +19 -0
  30. package/examples/api/center-true-vs-false.forge.js +40 -0
  31. package/examples/api/clone-duplicate.forge.js +41 -0
  32. package/examples/api/colors-union-vs-array.forge.js +27 -0
  33. package/examples/api/coordinate-system.forge.js +54 -0
  34. package/examples/api/curves-surfacing-basics.forge.js +91 -0
  35. package/examples/api/dimensioned-bracket.forge.js +19 -0
  36. package/examples/api/elbow-test.forge.js +23 -0
  37. package/examples/api/exploded-view.forge.js +60 -0
  38. package/examples/api/extrude-options.forge.js +44 -0
  39. package/examples/api/face-gears.forge.js +44 -0
  40. package/examples/api/face-transformation-history.forge.js +45 -0
  41. package/examples/api/feature-created-faces.forge.js +47 -0
  42. package/examples/api/folded-service-panel-cover.forge.js +3 -0
  43. package/examples/api/folded-service-panel-cover.js +117 -0
  44. package/examples/api/gears-bevel-face-joints.forge.js +157 -0
  45. package/examples/api/gears-tier1.forge.js +57 -0
  46. package/examples/api/geometry-info.forge.js +49 -0
  47. package/examples/api/group-test.forge.js +34 -0
  48. package/examples/api/group-vs-union.forge.js +25 -0
  49. package/examples/api/import-args-unit.forge.js +5 -0
  50. package/examples/api/import-args.forge.js +16 -0
  51. package/examples/api/import-dimensions-follow.forge.js +18 -0
  52. package/examples/api/import-placement-references.forge.js +18 -0
  53. package/examples/api/import-placement-widget-source.forge.js +30 -0
  54. package/examples/api/import-relative-paths.forge.js +18 -0
  55. package/examples/api/import-svg-sketch-shape.svg +15 -0
  56. package/examples/api/import-svg-sketch.forge.js +28 -0
  57. package/examples/api/js-module-imports.forge.js +9 -0
  58. package/examples/api/js-module-pillars.js +25 -0
  59. package/examples/api/js-module-scene.js +9 -0
  60. package/examples/api/notebook-assembly-debug.forge-notebook.json +90 -0
  61. package/examples/api/notebook-iteration.forge-notebook.json +75 -0
  62. package/examples/api/patterns.forge.js +32 -0
  63. package/examples/api/pointAlong-orientation.forge.js +52 -0
  64. package/examples/api/profile-2020-b-slot6.forge.js +36 -0
  65. package/examples/api/rotate-around-to.forge.js +31 -0
  66. package/examples/api/runtime-joints-view.forge.js +116 -0
  67. package/examples/api/sdf-rover-demo.forge.js +159 -0
  68. package/examples/api/section-plane-visualization.forge.js +38 -0
  69. package/examples/api/sketch-basics.forge.js +48 -0
  70. package/examples/api/sketch-on-face.forge.js +56 -0
  71. package/examples/api/sketch-rounding-strategies.forge.js +56 -0
  72. package/examples/api/spatial-recipes.forge.js +129 -0
  73. package/examples/bathroom.forge.js +197 -0
  74. package/examples/bolt-and-nut.forge.js +39 -0
  75. package/examples/bolt-pattern.forge.js +18 -0
  76. package/examples/bottle.forge.js +101 -0
  77. package/examples/chair.forge.js +62 -0
  78. package/examples/chess-set.forge.js +232 -0
  79. package/examples/classical-piano.forge.js +203 -0
  80. package/examples/clock.forge.js +169 -0
  81. package/examples/compiler-corpus/README.md +88 -0
  82. package/examples/compiler-corpus/edge-finished-mount.forge.js +18 -0
  83. package/examples/compiler-corpus/enclosure-shell-cuts.forge.js +24 -0
  84. package/examples/compiler-corpus/fastener-plate-variants.forge.js +42 -0
  85. package/examples/compiler-corpus/folded-service-panel-cover.forge.js +5 -0
  86. package/examples/compiler-corpus/motor-mount-plate.forge.js +32 -0
  87. package/examples/compiler-corpus/projection-relay-cover.forge.js +16 -0
  88. package/examples/compiler-corpus/sensor-bracket.forge.js +35 -0
  89. package/examples/compiler-corpus/service-panel-cover.forge.js +53 -0
  90. package/examples/compiler-corpus/trimmed-access-cover.forge.js +26 -0
  91. package/examples/cup.forge.js +25 -0
  92. package/examples/cut-plane-demo.forge.js +28 -0
  93. package/examples/door-with-hinges.forge.js +54 -0
  94. package/examples/frame.sketch.js +4 -0
  95. package/examples/headphone-hanger-profile.sketch.js +18 -0
  96. package/examples/headphone-hanger-v2.forge.js +88 -0
  97. package/examples/headphone-hanger.forge.js +5 -0
  98. package/examples/iphone-stand.forge.js +72 -0
  99. package/examples/iphone.forge.js +114 -0
  100. package/examples/ironman-helmet.js +79 -0
  101. package/examples/kitchen.forge.js +231 -0
  102. package/examples/lamp-shade.sketch.js +17 -0
  103. package/examples/laptop.forge.js +144 -0
  104. package/examples/liquid-soap-dispenser.forge.js +159 -0
  105. package/examples/modern-tv.forge.js +86 -0
  106. package/examples/picture-frame.forge.js +34 -0
  107. package/examples/robot_hand.forge.js +393 -0
  108. package/examples/robot_hand_2.forge.js +622 -0
  109. package/examples/sandbox.forge.js +3 -0
  110. package/examples/shelf/container.forge.js +30 -0
  111. package/examples/shelf/shelf-unit.forge.js +62 -0
  112. package/examples/shoe-rack-doors.forge.js +107 -0
  113. package/examples/shoe-rack.forge.js +65 -0
  114. package/examples/spiderman-cake.forge.js +92 -0
  115. package/examples/table-lamp.forge.js +33 -0
  116. package/examples/table.forge.js +44 -0
  117. package/examples/test-colors.forge.js +19 -0
  118. package/examples/tv-stand.forge.js +21 -0
  119. package/package.json +69 -0
@@ -0,0 +1,159 @@
1
+ // Liquid Soap Dispenser — staged dispense animation without fluid simulation.
2
+ // Move "Dispense Progress" from 0 → 100 to preview one pump cycle.
3
+
4
+ const bodyH = param("Bottle Height", 130, { min: 90, max: 180, unit: "mm" });
5
+ const bodyR = param("Bottle Radius", 34, { min: 24, max: 45, unit: "mm" });
6
+ const wall = param("Wall Thickness", 2.8, { min: 1.5, max: 5, unit: "mm" });
7
+ const pumpStroke = param("Pump Stroke", 10, { min: 4, max: 18, unit: "mm" });
8
+ const progressPct = param("Dispense Progress", 0, { min: 0, max: 100, unit: "%" });
9
+
10
+ const progress = progressPct / 100;
11
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
12
+
13
+ // Press down (0..0.6), then spring release (0.6..1.0).
14
+ const pressIn = clamp01(progress / 0.6);
15
+ const release = clamp01((progress - 0.6) / 0.4);
16
+ const press = progress <= 0.6 ? pressIn : 1 - release;
17
+
18
+ // Add a center slice so internals are inspectable.
19
+ cutPlane("Middle Slice", [0, -1, 0], 0);
20
+
21
+ const neckH = 16;
22
+ const neckR = bodyR * 0.36;
23
+ const chamberH = 48;
24
+ const chamberR = neckR * 0.9;
25
+ const chamberWall = 2.4;
26
+ const chamberInnerR = chamberR - chamberWall;
27
+ const chamberBottomZ = bodyH + 4;
28
+ const chamberTopZ = chamberBottomZ + chamberH;
29
+
30
+ const headW = 54;
31
+ const headD = 22;
32
+ const headH = 14;
33
+ const headRestZ = chamberTopZ + 14;
34
+ const headZ = headRestZ - press * pumpStroke;
35
+
36
+ const nozzleL = 34;
37
+ const nozzleR = 3.6;
38
+ const nozzleRootX = headW * 0.22;
39
+ const nozzleCenterX = nozzleRootX + nozzleL * 0.5;
40
+ const nozzleTipX = nozzleRootX + nozzleL;
41
+ const nozzleZ = headZ + 1;
42
+
43
+ // Bottle shell + neck.
44
+ const bottleOuter = cylinder(bodyH, bodyR, bodyR, undefined, true)
45
+ .translate(0, 0, bodyH * 0.5);
46
+ const bottleInner = cylinder(bodyH - wall * 1.4, bodyR - wall, bodyR - wall, undefined, true)
47
+ .translate(0, 0, wall + (bodyH - wall * 1.4) * 0.5);
48
+ const bottleShell = bottleOuter.subtract(bottleInner);
49
+
50
+ const neckOuter = cylinder(neckH, neckR, neckR, undefined, true).translate(0, 0, bodyH + neckH * 0.5 - 2);
51
+ const neckHole = cylinder(neckH + 6, chamberR - 2.5, chamberR - 2.5, undefined, true)
52
+ .translate(0, 0, bodyH + neckH * 0.5 - 2);
53
+ const bottle = union(bottleShell, neckOuter).subtract(neckHole);
54
+
55
+ // Pump chamber.
56
+ const chamberOuter = cylinder(chamberH, chamberR, chamberR, undefined, true)
57
+ .translate(0, 0, chamberBottomZ + chamberH * 0.5);
58
+ const chamberInner = cylinder(chamberH - 4, chamberInnerR, chamberInnerR, undefined, true)
59
+ .translate(0, 0, chamberBottomZ + chamberH * 0.5 + 1.2);
60
+ const chamberShell = chamberOuter.subtract(chamberInner);
61
+
62
+ // Pump head + nozzle.
63
+ const head = box(headW, headD, headH, true).translate(0, 0, headZ);
64
+ const nozzleOuter = cylinder(nozzleL, nozzleR, nozzleR * 0.92, undefined, true)
65
+ .pointAlong([1, 0, 0])
66
+ .translate(nozzleCenterX, 0, nozzleZ);
67
+ const nozzleInner = cylinder(nozzleL + 2, 1.5, 1.35, undefined, true)
68
+ .pointAlong([1, 0, 0])
69
+ .translate(nozzleCenterX, 0, nozzleZ);
70
+ const nozzle = nozzleOuter.subtract(nozzleInner);
71
+
72
+ // Stem + piston (moves with the head).
73
+ const pistonH = 7;
74
+ const pistonZ = chamberTopZ - 8 - press * pumpStroke * 0.9;
75
+ const piston = cylinder(pistonH, chamberR - 3.1, chamberR - 3.1, undefined, true).translate(0, 0, pistonZ);
76
+
77
+ const stemTopZ = headZ - headH * 0.5;
78
+ const stemBottomZ = pistonZ + pistonH * 0.5;
79
+ const stemLen = Math.max(8, stemTopZ - stemBottomZ);
80
+ const stem = cylinder(stemLen, 2.2, 2.2, undefined, true).translate(0, 0, (stemTopZ + stemBottomZ) * 0.5);
81
+
82
+ // Return spring shown as compressed ring stack.
83
+ const springBottomZ = chamberBottomZ + 8;
84
+ const springTopZ = pistonZ - pistonH * 0.7;
85
+ const springSpan = Math.max(6, springTopZ - springBottomZ);
86
+ const springTurns = 7;
87
+ const springRings = [];
88
+ for (let i = 0; i < springTurns; i++) {
89
+ const t = i / Math.max(1, springTurns - 1);
90
+ const z = springBottomZ + t * springSpan;
91
+ const ringOuter = cylinder(1.2, chamberR - 4.2, chamberR - 4.2, undefined, true).translate(0, 0, z);
92
+ const ringInner = cylinder(2.4, chamberR - 5.3, chamberR - 5.3, undefined, true).translate(0, 0, z);
93
+ springRings.push(ringOuter.subtract(ringInner));
94
+ }
95
+ const spring = union(...springRings);
96
+
97
+ // Dip tube.
98
+ const tubeBottomZ = 8;
99
+ const tubeTopZ = chamberBottomZ + 6;
100
+ const tubeLen = tubeTopZ - tubeBottomZ;
101
+ const tubeOuter = cylinder(tubeLen, 2.5, 2.5, undefined, true).translate(0, 0, (tubeBottomZ + tubeTopZ) * 0.5);
102
+ const tubeInner = cylinder(tubeLen + 2, 1.2, 1.2, undefined, true).translate(0, 0, (tubeBottomZ + tubeTopZ) * 0.5);
103
+ const dipTube = tubeOuter.subtract(tubeInner);
104
+
105
+ // Check valves: inlet opens during release, outlet opens during press.
106
+ const inletSeatZ = chamberBottomZ + 3;
107
+ const outletSeatZ = chamberTopZ - 7;
108
+ const inletValve = sphere(2.7).translate(0, 0, inletSeatZ + release * 2.4);
109
+ const outletValveR = 2.2;
110
+ const outletSeatClearance = 0.35;
111
+ const outletSeatX = chamberInnerR - outletValveR - outletSeatClearance;
112
+ const outletValve = sphere(outletValveR).translate(outletSeatX - press * 0.9, 0, outletSeatZ + 0.6 + press * 0.9);
113
+
114
+ // Stylized liquid storyboarding (no simulation).
115
+ const drawPhase = clamp01(progress / 0.55);
116
+ const pushPhase = clamp01((progress - 0.42) / 0.34);
117
+
118
+ const reservoirLevel = bodyH * 0.62;
119
+ const reservoir = cylinder(reservoirLevel, bodyR - wall - 1, bodyR - wall - 1)
120
+ .translate(0, 0, 4 + reservoirLevel * 0.5);
121
+
122
+ const tubeSlugH = 6 + drawPhase * (tubeLen - 10);
123
+ const tubeLiquid = cylinder(tubeSlugH, 1.1, 1.1, undefined, true)
124
+ .translate(0, 0, tubeBottomZ + tubeSlugH * 0.5);
125
+
126
+ const chamberLiquidH = Math.max(2, 9 + drawPhase * 17 - pushPhase * 16);
127
+ const chamberLiquid = cylinder(chamberLiquidH, chamberR - 4.8, chamberR - 4.8, undefined, true)
128
+ .translate(0, 0, chamberBottomZ + 4 + chamberLiquidH * 0.5);
129
+
130
+ const flowIn = clamp01((progress - 0.45) / 0.2);
131
+ const flowOut = 1 - clamp01((progress - 0.8) / 0.15);
132
+ const nozzleFlow = clamp01(flowIn * flowOut);
133
+ const nozzleLiquidLen = Math.max(1, 1 + nozzleFlow * (nozzleL - 3));
134
+ const nozzleLiquid = cylinder(nozzleLiquidLen, 1.05, 0.95, undefined, true)
135
+ .pointAlong([1, 0, 0])
136
+ .translate(nozzleRootX + nozzleLiquidLen * 0.5, 0, nozzleZ);
137
+
138
+ const dropPhase = clamp01((progress - 0.68) / 0.32);
139
+ const dropR = dropPhase * 2.1;
140
+ const dropFall = clamp01((dropPhase - 0.45) / 0.55);
141
+ const droplet = dropR > 0.12
142
+ ? sphere(dropR).translate(nozzleTipX + 1.5, 0, nozzleZ - 2 - dropFall * 16)
143
+ : null;
144
+
145
+ const liquidParts = [reservoir, tubeLiquid, chamberLiquid, nozzleLiquid];
146
+ if (droplet) liquidParts.push(droplet);
147
+ const liquid = union(...liquidParts);
148
+
149
+ const pumpMetal = union(chamberShell, dipTube, stem, piston, spring, nozzle);
150
+ const pumpHead = head;
151
+
152
+ return [
153
+ { name: "Bottle", shape: bottle, color: "#f4f1ea" },
154
+ { name: "Pump Head", shape: pumpHead, color: "#303840" },
155
+ { name: "Pump Mechanics", shape: pumpMetal, color: "#a9b2bb" },
156
+ { name: "Inlet Valve", shape: inletValve, color: "#6f7a85" },
157
+ { name: "Outlet Valve", shape: outletValve, color: "#6f7a85" },
158
+ { name: "Liquid (Stylized)", shape: liquid, color: "#4fa6d8" },
159
+ ];
@@ -0,0 +1,86 @@
1
+ // Modern Flat-Screen TV
2
+ // Ultra-thin panel with bottom electronics bulge, narrow bezels,
3
+ // V-shaped stand legs, and cable management hole
4
+
5
+ const screenW = param("Screen Width", 1100, { min: 800, max: 1600, unit: "mm" });
6
+ const aspect = param("Aspect Ratio", 1.78, { min: 1.33, max: 2.35, step: 0.01 });
7
+ const bezel = param("Bezel", 6, { min: 3, max: 20, unit: "mm" });
8
+ const thinD = param("Thin Section", 8, { min: 5, max: 20, unit: "mm" });
9
+ const bulgeD = param("Bulge Depth", 35, { min: 20, max: 60, unit: "mm" });
10
+ const bulgeH = param("Bulge Height", 120, { min: 60, max: 200, unit: "mm" });
11
+ const standSpread = param("Stand Spread", 500, { min: 200, max: 800, unit: "mm" });
12
+ const standD = param("Stand Depth", 220, { min: 120, max: 350, unit: "mm" });
13
+ const standH = param("Stand Height", 40, { min: 15, max: 80, unit: "mm" });
14
+ const legW = param("Leg Width", 30, { min: 15, max: 60, unit: "mm" });
15
+
16
+ // Derived
17
+ const screenH = screenW / aspect;
18
+ const totalW = screenW + bezel * 2;
19
+ const totalH = screenH + bezel * 2;
20
+ const panelZ = standH; // panel bottom sits on stand
21
+
22
+ // --- Thin panel (upper portion) ---
23
+ const thinPanel = box(totalW, thinD, totalH, true)
24
+ .translate(0, 0, panelZ + totalH / 2);
25
+
26
+ // --- Electronics bulge (bottom-back of panel) ---
27
+ const bulge = box(totalW * 0.7, bulgeD, bulgeH, true)
28
+ .translate(0, -(bulgeD - thinD) / 2, panelZ + bulgeH / 2);
29
+
30
+ const panelBody = union(thinPanel, bulge);
31
+
32
+ // --- Screen (dark inset on front face) ---
33
+ const screenDepth = 1;
34
+ const screen = box(screenW, screenDepth + 1, screenH, true)
35
+ .translate(0, thinD / 2, panelZ + totalH / 2);
36
+
37
+ const panel = panelBody.subtract(screen);
38
+
39
+ // --- Stand: two V-shaped legs ---
40
+ // Each leg is a flat slab angled outward from center
41
+ const legThick = 8;
42
+ const halfSpread = standSpread / 2;
43
+
44
+ const makeLeg = (side) => {
45
+ // Leg runs from center-bottom of panel outward to the foot
46
+ const footX = side * halfSpread;
47
+ const legLen = Math.sqrt(halfSpread * halfSpread + standD * standD / 4);
48
+
49
+ // Simple approach: a box rotated to angle from center to foot
50
+ const angle = Math.atan2(halfSpread, standD / 2) * 180 / Math.PI;
51
+
52
+ return box(legW, legLen, legThick, true)
53
+ .rotate(0, 0, side * angle)
54
+ .translate(side * halfSpread / 2, 0, standH / 2);
55
+ };
56
+
57
+ const leftLeg = makeLeg(-1);
58
+ const rightLeg = makeLeg(1);
59
+
60
+ // Foot pads (flat rectangles at leg ends)
61
+ const footW = legW + 10;
62
+ const footD = 40;
63
+ const footH = 4;
64
+ const leftFoot = box(footW, footD, footH, true)
65
+ .translate(-halfSpread, 0, footH / 2);
66
+ const rightFoot = box(footW, footD, footH, true)
67
+ .translate(halfSpread, 0, footH / 2);
68
+
69
+ // Center bridge connecting legs to panel
70
+ const bridge = box(legW * 2, legThick, standH, true)
71
+ .translate(0, 0, standH / 2);
72
+
73
+ const stand = union(leftLeg, rightLeg, leftFoot, rightFoot, bridge);
74
+
75
+ // --- Cable hole (through the bridge) ---
76
+ const cableHole = cylinder(legThick + 2, 10)
77
+ .rotate(90, 0, 0)
78
+ .translate(0, 0, standH / 2);
79
+
80
+ const standFinal = stand.subtract(cableHole);
81
+
82
+ return [
83
+ { name: "Panel", shape: panel, color: "#1a1a1a" },
84
+ { name: "Screen", shape: screen, color: "#050515" },
85
+ { name: "Stand", shape: standFinal, color: "#2a2a2a" },
86
+ ];
@@ -0,0 +1,34 @@
1
+ // Picture Frame — parametric wall frame with optional mat border
2
+
3
+ const frameW = param("Frame Width", 120, { min: 60, max: 250, unit: "mm" });
4
+ const frameH = param("Frame Height", 160, { min: 80, max: 300, unit: "mm" });
5
+ const border = param("Border Width", 12, { min: 5, max: 30, unit: "mm" });
6
+ const depth = param("Frame Depth", 8, { min: 3, max: 20, unit: "mm" });
7
+ const mat = param("Mat Width", 8, { min: 0, max: 20, unit: "mm" });
8
+ const matDepth = param("Mat Depth", 2, { min: 1, max: 5, unit: "mm" });
9
+
10
+ // Outer frame
11
+ const outer = box(frameW, frameH, depth);
12
+ const opening = box(frameW - 2 * border, frameH - 2 * border, depth + 2)
13
+ .translate(border, border, -1);
14
+ const frame = outer.subtract(opening);
15
+
16
+ // Mat insert (thinner, slightly smaller opening)
17
+ const parts = [{ name: "Frame", shape: frame, color: "#5c3a1e" }];
18
+
19
+ if (mat > 0) {
20
+ const matOuter = box(frameW - 2 * border, frameH - 2 * border, matDepth)
21
+ .translate(border, border, 0);
22
+ const matHole = box(
23
+ frameW - 2 * border - 2 * mat,
24
+ frameH - 2 * border - 2 * mat,
25
+ matDepth + 2
26
+ ).translate(border + mat, border + mat, -1);
27
+ parts.push({ name: "Mat", shape: matOuter.subtract(matHole), color: "#f5f0e8" });
28
+ }
29
+
30
+ // Back panel
31
+ const back = box(frameW - 2, frameH - 2, 1).translate(1, 1, -1);
32
+ parts.push({ name: "Back", shape: back, color: "#3a3a3a" });
33
+
34
+ return parts;
@@ -0,0 +1,393 @@
1
+ // Parametric Robot Hand
2
+ // Design goals:
3
+ // 1) Home-buildable with FDM printing + metal pins + cord tendon
4
+ // 2) Clear mechanics: clevis joints, hinge pins, tendon guides, soft pads
5
+ // 3) Mount included: base, mast, wrist clevis
6
+ // 4) Iterative construction: build primitives -> segments -> fingers -> full assembly
7
+
8
+ const grip = param("Grip", 55, { min: 0, max: 100, unit: "%" });
9
+ const fingerSpread = param("Finger Spread", 18, { min: 0, max: 35, unit: "°" });
10
+ const thumbOpposition = param("Thumb Opposition", 42, { min: 20, max: 70, unit: "°" });
11
+ const wristPitch = param("Wrist Pitch", 8, { min: -35, max: 45, unit: "°" });
12
+ const explode = param("Explode", 0, { min: 0, max: 26, unit: "mm" });
13
+
14
+ const scale = param("Scale", 1.0, { min: 0.75, max: 1.25, step: 0.01 });
15
+ const pinD = param("Pin Diameter", 3.2, { min: 2.0, max: 5.0, step: 0.1, unit: "mm" }) * scale;
16
+ const jointClearance = param("Joint Clearance", 0.32, { min: 0.2, max: 0.7, step: 0.02, unit: "mm" }) * scale;
17
+ const wall = param("Min Wall", 2.4, { min: 1.6, max: 4.0, step: 0.1, unit: "mm" }) * scale;
18
+
19
+ // Core hand dimensions
20
+ const palmW = 84 * scale;
21
+ const palmD = 76 * scale;
22
+ const palmT = 18 * scale;
23
+
24
+ const fingerCount = 4;
25
+ const fingerWidths = [15, 16, 15, 14].map(v => v * scale);
26
+ const fingerLengths = [
27
+ [36, 25, 19],
28
+ [39, 28, 21],
29
+ [37, 27, 20],
30
+ [31, 23, 18],
31
+ ].map(row => row.map(v => v * scale));
32
+
33
+ const thumbWidth = 16 * scale;
34
+ const thumbLengths = [31, 25, 20].map(v => v * scale);
35
+
36
+ const lugGap = pinD + 2.3 * jointClearance;
37
+ const lugWidth = Math.max(wall * 1.4, (thumbWidth - lugGap) * 0.45);
38
+ const lugDepth = Math.max(9 * scale, pinD * 2.7);
39
+ const segmentThick = 14 * scale;
40
+ const pinR = pinD * 0.5;
41
+
42
+ function clamp(v, lo, hi) {
43
+ return Math.max(lo, Math.min(hi, v));
44
+ }
45
+
46
+ function rad(deg) {
47
+ return deg * Math.PI / 180;
48
+ }
49
+
50
+ function rotateZ2D(x, y, deg) {
51
+ const a = rad(deg);
52
+ const c = Math.cos(a);
53
+ const s = Math.sin(a);
54
+ return [x * c - y * s, x * s + y * c];
55
+ }
56
+
57
+ function localToWorld(localPt, basePt, yawDeg) {
58
+ const [rx, ry] = rotateZ2D(localPt[0], localPt[1], yawDeg);
59
+ return [basePt[0] + rx, basePt[1] + ry, basePt[2] + localPt[2]];
60
+ }
61
+
62
+ // Clevis mount with pin bore centered at origin
63
+ function makeClevis(width, height, depth, gap, lugW, boreR) {
64
+ const left = box(lugW, depth, height, true).translate(-(gap * 0.5 + lugW * 0.5), -depth * 0.5, 0);
65
+ const right = box(lugW, depth, height, true).translate(gap * 0.5 + lugW * 0.5, -depth * 0.5, 0);
66
+ const bridge = box(width, depth * 0.55, height * 0.55, true).translate(0, -depth * 0.78, 0);
67
+ const raw = union(left, right, bridge);
68
+ const bore = cylinder(width + 2, boreR).pointAlong([1, 0, 0]);
69
+ return raw.subtract(bore);
70
+ }
71
+
72
+ // Single-lug hinge ear centered at origin, extending +Y
73
+ function makeHingeEar(width, height, depth, boreR) {
74
+ const ear = union(
75
+ cylinder(width * 0.62, height * 0.45).pointAlong([1, 0, 0]).translate(0, 0, 0),
76
+ box(width * 0.62, depth * 0.9, height * 0.85, true).translate(0, depth * 0.45, 0)
77
+ );
78
+ const bore = cylinder(width + 2, boreR).pointAlong([1, 0, 0]);
79
+ return ear.subtract(bore);
80
+ }
81
+
82
+ // Printable phalanx: base ear + body + optional front clevis + tendon channel
83
+ function makePhalanxPart(length, width, thick, hasFrontClevis, frontScale = 1.0) {
84
+ const body = roundedRect(width * 0.72, length, Math.max(2.0 * scale, width * 0.12), true)
85
+ .extrude(thick * 0.82, { center: true })
86
+ .translate(0, length * 0.5, 0);
87
+
88
+ const baseEar = makeHingeEar(width, thick, lugDepth, pinR + jointClearance * 0.6)
89
+ .translate(0, 0, 0);
90
+
91
+ const topRib = box(width * 0.36, length * 0.72, thick * 0.22, true).translate(0, length * 0.52, thick * 0.28);
92
+ const bottomRib = box(width * 0.36, length * 0.72, thick * 0.18, true).translate(0, length * 0.52, -thick * 0.28);
93
+
94
+ let part = union(body, baseEar, topRib, bottomRib);
95
+
96
+ if (hasFrontClevis) {
97
+ const frontW = width * frontScale;
98
+ const frontClevis = makeClevis(frontW, thick * 0.9, lugDepth * 0.95, lugGap, lugWidth, pinR + jointClearance * 0.7)
99
+ .translate(0, length, 0);
100
+ part = union(part, frontClevis);
101
+ } else {
102
+ const tipCap = sphere(width * 0.28).scale([1.0, 1.35, 0.75]).translate(0, length + width * 0.15, 0);
103
+ part = union(part, tipCap);
104
+ }
105
+
106
+ const tendonChannel = cylinder(length + lugDepth * 0.8, Math.max(1.3 * scale, pinR * 0.55))
107
+ .pointAlong([0, 1, 0])
108
+ .translate(0, length * 0.55, thick * 0.1);
109
+
110
+ return part.subtract(tendonChannel);
111
+ }
112
+
113
+ function jointAnglesFromGrip(gripPercent, isThumb) {
114
+ const g = clamp(gripPercent / 100, 0, 1);
115
+ if (isThumb) {
116
+ return [
117
+ 8 + g * 40,
118
+ 10 + g * 55,
119
+ 8 + g * 62,
120
+ ];
121
+ }
122
+ return [
123
+ 6 + g * 46,
124
+ 10 + g * 64,
125
+ 6 + g * 58,
126
+ ];
127
+ }
128
+
129
+ function makeFingerAssembly(opts) {
130
+ const {
131
+ name,
132
+ basePivot,
133
+ yawDeg,
134
+ widths,
135
+ lengths,
136
+ angles,
137
+ color,
138
+ isThumb,
139
+ showCable,
140
+ } = opts;
141
+
142
+ const parts = [];
143
+ const pivots = [];
144
+ const localTipPts = [];
145
+
146
+ let accum = 0;
147
+ let py = 0;
148
+ let pz = 0;
149
+
150
+ for (let i = 0; i < lengths.length; i += 1) {
151
+ const segLen = lengths[i];
152
+ const segW = widths[i];
153
+ const hasFront = i < lengths.length - 1;
154
+ const a = angles[i] || 0;
155
+ accum += a;
156
+ const localPart = makePhalanxPart(segLen, segW, segmentThick * (isThumb ? 0.9 : 1.0), hasFront, 0.94);
157
+
158
+ const worldBase = localToWorld([0, py, pz], basePivot, yawDeg);
159
+ const part = localPart
160
+ .rotate(accum, 0, 0)
161
+ .rotate(0, 0, yawDeg)
162
+ .translate(worldBase[0], worldBase[1], worldBase[2])
163
+ .translate(0, 0, explode * i * 0.08);
164
+
165
+ parts.push({
166
+ name: `${name} Segment ${i + 1}`,
167
+ shape: part.color(color),
168
+ });
169
+
170
+ const pinLen = segW + wall * 2.4;
171
+ const pin = cylinder(pinLen, pinR * 0.92)
172
+ .pointAlong([1, 0, 0])
173
+ .rotate(0, 0, yawDeg)
174
+ .translate(worldBase[0], worldBase[1], worldBase[2])
175
+ .translate(0, 0, explode * i * 0.08)
176
+ .color('#B0B7C3');
177
+
178
+ parts.push({
179
+ name: `${name} Pin ${i + 1}`,
180
+ shape: pin,
181
+ });
182
+
183
+ pivots.push(worldBase);
184
+
185
+ py += segLen * Math.cos(rad(accum));
186
+ pz += segLen * Math.sin(rad(accum));
187
+ localTipPts.push(localToWorld([0, py, pz], basePivot, yawDeg));
188
+ }
189
+
190
+ const pad = sphere(widths[widths.length - 1] * 0.34)
191
+ .scale([1, 1.25, 0.7])
192
+ .translate(localTipPts[localTipPts.length - 1][0], localTipPts[localTipPts.length - 1][1], localTipPts[localTipPts.length - 1][2])
193
+ .translate(0, 0, -widths[widths.length - 1] * 0.08)
194
+ .color('#2E2E2E');
195
+
196
+ parts.push({ name: `${name} Soft Pad`, shape: pad });
197
+
198
+ if (showCable) {
199
+ let cable = null;
200
+ const cableR = Math.max(0.6 * scale, pinR * 0.3);
201
+ for (let i = 0; i < pivots.length - 1; i += 1) {
202
+ const a = pivots[i];
203
+ const b = pivots[i + 1];
204
+ const dx = b[0] - a[0];
205
+ const dy = b[1] - a[1];
206
+ const dz = b[2] - a[2];
207
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
208
+ const seg = cylinder(len, cableR)
209
+ .pointAlong([dx, dy, dz])
210
+ .translate(a[0], a[1], a[2] + segmentThick * 0.28);
211
+ cable = cable ? union(cable, seg) : seg;
212
+ }
213
+ if (cable) {
214
+ parts.push({ name: `${name} Tendon`, shape: cable.color('#D77C2D') });
215
+ }
216
+ }
217
+
218
+ return parts;
219
+ }
220
+
221
+ // Build palm with integrated finger clevises and wrist lug
222
+ function makePalm() {
223
+ const palmCore = roundedRect(palmW, palmD, 10 * scale, true).extrude(palmT, { center: true })
224
+ .translate(0, palmD * 0.5, 0);
225
+
226
+ const palmHoles = union(
227
+ slot(palmW * 0.32, palmD * 0.16).translate(0, palmD * 0.5).extrude(palmT + 2, { center: true }),
228
+ circle2d(palmW * 0.11).translate(-palmW * 0.23, palmD * 0.56).extrude(palmT + 2, { center: true }),
229
+ circle2d(palmW * 0.10).translate(palmW * 0.24, palmD * 0.42).extrude(palmT + 2, { center: true })
230
+ );
231
+
232
+ let palm = palmCore.subtract(palmHoles);
233
+
234
+ const spacing = palmW / (fingerCount + 1);
235
+ for (let i = 0; i < fingerCount; i += 1) {
236
+ const x = -palmW * 0.5 + spacing * (i + 1);
237
+ const clevis = makeClevis(
238
+ fingerWidths[i] * 0.98,
239
+ segmentThick * 0.88,
240
+ lugDepth,
241
+ lugGap,
242
+ lugWidth,
243
+ pinR + jointClearance
244
+ ).translate(x, palmD, segmentThick * 0.08);
245
+ palm = union(palm, clevis);
246
+ }
247
+
248
+ const thumbClevis = makeClevis(thumbWidth, segmentThick * 0.86, lugDepth, lugGap, lugWidth, pinR + jointClearance)
249
+ .rotate(0, 0, -thumbOpposition)
250
+ .translate(palmW * 0.45, palmD * 0.24, 0);
251
+
252
+ const wristEar = makeHingeEar(palmW * 0.45, segmentThick, lugDepth * 1.05, pinR + jointClearance)
253
+ .rotate(0, 180, 0)
254
+ .translate(0, 0, 0);
255
+
256
+ palm = union(palm, thumbClevis, wristEar);
257
+
258
+ return palm.color('#D9D6CF');
259
+ }
260
+
261
+ // Build mount: printable base + mast + wrist clevis
262
+ function makeMount() {
263
+ const baseW = 126 * scale;
264
+ const baseD = 108 * scale;
265
+ const baseT = 9 * scale;
266
+ const mastW = 28 * scale;
267
+ const mastD = 34 * scale;
268
+ const mastH = 86 * scale;
269
+
270
+ let base = roundedRect(baseW, baseD, 8 * scale, true).extrude(baseT, { center: true }).translate(0, -36 * scale, -mastH * 0.52);
271
+
272
+ const slotSketchA = slot(20 * scale, 7 * scale).translate(-baseW * 0.28, -36 * scale);
273
+ const slotSketchB = slot(20 * scale, 7 * scale).translate(baseW * 0.28, -36 * scale);
274
+ const slotSketchC = slot(20 * scale, 7 * scale).translate(0, -36 * scale - baseD * 0.22);
275
+ const slotSketchD = slot(20 * scale, 7 * scale).translate(0, -36 * scale + baseD * 0.22);
276
+ const slots = union(
277
+ slotSketchA.extrude(baseT + 2, { center: true }).translate(0, 0, -mastH * 0.52),
278
+ slotSketchB.extrude(baseT + 2, { center: true }).translate(0, 0, -mastH * 0.52),
279
+ slotSketchC.extrude(baseT + 2, { center: true }).translate(0, 0, -mastH * 0.52),
280
+ slotSketchD.extrude(baseT + 2, { center: true }).translate(0, 0, -mastH * 0.52)
281
+ );
282
+ base = base.subtract(slots);
283
+
284
+ const mast = box(mastW, mastD, mastH, true).translate(0, -8 * scale, -mastH * 0.02);
285
+ const gussetL = polygon([[0, 0], [26 * scale, 0], [0, 28 * scale]])
286
+ .extrude(6 * scale, { center: true })
287
+ .rotate(90, 0, 90)
288
+ .translate(-mastW * 0.5 - 3 * scale, -18 * scale, -mastH * 0.28);
289
+ const gussetR = gussetL.mirror([1, 0, 0]);
290
+
291
+ const wristClevis = makeClevis(palmW * 0.54, segmentThick * 0.98, lugDepth * 1.05, lugGap, lugWidth * 1.1, pinR + jointClearance)
292
+ .translate(0, 0, 0);
293
+
294
+ const wristPin = cylinder(palmW * 0.65, pinR * 0.96)
295
+ .pointAlong([1, 0, 0])
296
+ .translate(0, 0, 0)
297
+ .color('#B0B7C3');
298
+
299
+ return {
300
+ base: base.color('#646B73'),
301
+ mast: union(mast, gussetL, gussetR).color('#7A828A'),
302
+ wristClevis: wristClevis.color('#838B92'),
303
+ wristPin,
304
+ };
305
+ }
306
+
307
+ // 1) Mount (fixed reference)
308
+ const mount = makeMount();
309
+
310
+ // 2) Palm (mechanical hub)
311
+ const palm = makePalm();
312
+
313
+ // 3) Fingers (iterative generation)
314
+ const handParts = [];
315
+
316
+ const spacing = palmW / (fingerCount + 1);
317
+ for (let i = 0; i < fingerCount; i += 1) {
318
+ const x = -palmW * 0.5 + spacing * (i + 1);
319
+ const side = (i - (fingerCount - 1) * 0.5) / ((fingerCount - 1) * 0.5);
320
+ const yaw = -side * (fingerSpread * 0.5);
321
+ const baseAngles = jointAnglesFromGrip(grip, false);
322
+
323
+ const finger = makeFingerAssembly({
324
+ name: `Finger ${i + 1}`,
325
+ basePivot: [x, palmD, segmentThick * 0.08],
326
+ yawDeg: yaw,
327
+ widths: [fingerWidths[i], fingerWidths[i] * 0.92, fingerWidths[i] * 0.86],
328
+ lengths: fingerLengths[i],
329
+ angles: baseAngles,
330
+ color: ['#AAB7C6', '#B8C5D3', '#C4D1DB', '#9FB0C0'][i],
331
+ isThumb: false,
332
+ showCable: i === 1 || i === 2,
333
+ });
334
+
335
+ for (const p of finger) {
336
+ handParts.push(p);
337
+ }
338
+ }
339
+
340
+ // 4) Thumb (separate kinematic chain)
341
+ const thumbAngles = jointAnglesFromGrip(grip * 0.95 + 5, true);
342
+ const thumb = makeFingerAssembly({
343
+ name: "Thumb",
344
+ basePivot: [palmW * 0.45, palmD * 0.24, 0],
345
+ yawDeg: -thumbOpposition,
346
+ widths: [thumbWidth, thumbWidth * 0.92, thumbWidth * 0.86],
347
+ lengths: thumbLengths,
348
+ angles: thumbAngles,
349
+ color: '#B7B9C9',
350
+ isThumb: true,
351
+ showCable: true,
352
+ });
353
+
354
+ for (const p of thumb) {
355
+ handParts.push(p);
356
+ }
357
+
358
+ // 5) Wrist articulation: rotate all hand components around wrist pin axis
359
+ const wristRotated = [];
360
+
361
+ wristRotated.push({ name: "Palm", shape: palm.rotateAround([1, 0, 0], wristPitch, [0, 0, 0]) });
362
+
363
+ for (const p of handParts) {
364
+ wristRotated.push({
365
+ name: p.name,
366
+ shape: p.shape.rotateAround([1, 0, 0], wristPitch, [0, 0, 0]),
367
+ });
368
+ }
369
+
370
+ // 6) Return full inspectable assembly (colors preserved by separate objects)
371
+ return [
372
+ { name: "Mount Base", shape: mount.base },
373
+ { name: "Mount Mast", shape: mount.mast },
374
+ { name: "Wrist Clevis", shape: mount.wristClevis },
375
+ { name: "Wrist Pin", shape: mount.wristPin },
376
+ ...wristRotated,
377
+ ];
378
+
379
+
380
+ /**
381
+ *
382
+ * Make a fully functional robot hand. Should be easy to build, maybe even at home with some good tools. Show all the mechanics. Should be able to hold arbitrary shape
383
+ objects. Don't be a perfectionist, but be an artist and an engineer.
384
+ Make sure each component can be manufactured.
385
+ It should be mounted somewhere.
386
+ Make it controllable through params.
387
+ As this is a complex task, break it down to simpler ones, solve them, combine, iterate.
388
+ Read docs/permanent/API/model-building/README.md and examples/api/* to learn about ForgeCAD.
389
+ Call it examples/robot_hand.forge.js
390
+ Don't read any other files in the project, no other examples. It's an isolated experiment.
391
+ Make it reliable, easy to 3D print, robust.
392
+ Use iterative logic in the code.
393
+ */