axstack 0.36.1 → 0.37.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.
- package/README.md +3 -2
- package/bin/axstack.js +4 -18
- package/docs/getting-started.md +6 -6
- package/docs/installation.md +20 -41
- package/docs/workflows.md +12 -9
- package/package.json +1 -1
- package/profiles/presets/claude-only.json +3 -3
- package/profiles/presets/codex-only.json +3 -3
- package/profiles/presets/mixed.json +5 -4
- package/skills/axstack/references/role-roster.md +5 -3
- package/skills/axstack/references/ui-verification.md +6 -10
- package/skills/axstack-anatomy/ANATOMY.md +172 -0
- package/skills/axstack-anatomy/CHANGELOG.md +70 -0
- package/skills/axstack-anatomy/LICENSE +21 -0
- package/skills/axstack-anatomy/PROVENANCE.md +21 -0
- package/skills/axstack-anatomy/README.md +316 -0
- package/skills/axstack-anatomy/SKILL.md +74 -0
- package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
- package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
- package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
- package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
- package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
- package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
- package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
- package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
- package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
- package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
- package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
- package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
- package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
- package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
- package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
- package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
- package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
- package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
- package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
- package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
- package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
- package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
- package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
- package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
- package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
- package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
- package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
- package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
- package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
- package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
- package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
- package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
- package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
- package/skills/axstack-anatomy/kit/gl.mjs +184 -0
- package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
- package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
- package/skills/axstack-anatomy/kit/iso.css +106 -0
- package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
- package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
- package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
- package/skills/axstack-anatomy/kit/tube.mjs +306 -0
- package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
- package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
- package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
- package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
- package/skills/axstack-anatomy/references/3d.md +352 -0
- package/skills/axstack-anatomy/references/craft.md +325 -0
- package/skills/axstack-anatomy/references/kit.md +419 -0
- package/skills/axstack-anatomy/references/motion.md +165 -0
- package/skills/axstack-anatomy/references/react.md +42 -0
- package/skills/axstack-anatomy/references/verify.md +351 -0
- package/skills/axstack-anatomy/references/walkthrough.md +75 -0
- package/skills/axstack-anatomy/references/webgl.md +473 -0
- package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
- package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
- package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
- package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
- package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
- package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
- package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
- package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
- package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
- package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
- package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
- package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
- package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
- package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
- package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
- package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
- package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
- package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
- package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
- package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
- package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
- package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
- package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
- package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
- package/skills/axstack-anatomy/theme.js +56 -0
- package/skills/axstack-explain/SKILL.md +28 -15
- package/skills/axstack-explain/references/delivery.md +22 -0
- package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
- package/skills/axstack-explain/references/inline-pages.md +8 -8
- package/skills/axstack-explain/references/visual-qa.md +3 -5
- package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
- package/src/archify-legacy.js +70 -0
- package/src/capabilities.js +1 -29
- package/src/installer.js +11 -31
- package/skills/axstack-diagram/SKILL.md +0 -28
- package/skills/axstack-diagram/references/archify.md +0 -76
- package/src/archify-pin.js +0 -5
- package/src/archify.js +0 -151
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import * as k from "../../kit/iso-kit.mjs";
|
|
5
|
+
import * as G from "../../kit/lathe.mjs";
|
|
6
|
+
import * as A from "../../kit/audit.mjs";
|
|
7
|
+
import { turning } from "../../kit/turn-build.mjs";
|
|
8
|
+
import * as TA from "../../kit/turn-audit.mjs";
|
|
9
|
+
import { turnScript, glScript } from "../../scripts/inline-kit.mjs";
|
|
10
|
+
import { glCamera } from "../../kit/gl.mjs";
|
|
11
|
+
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const argv = process.argv.slice(2);
|
|
14
|
+
const theme = argv.includes("--light") ? "light" : "dark";
|
|
15
|
+
|
|
16
|
+
const WIDTH = 640;
|
|
17
|
+
const HEIGHT = 470;
|
|
18
|
+
const AZIMUTH = 40;
|
|
19
|
+
const ELEVATION = 31;
|
|
20
|
+
|
|
21
|
+
const BASE = { x: -82, y: -78, w: 164, d: 156, r: 8 };
|
|
22
|
+
const BASE_H = 12;
|
|
23
|
+
const FOOT_H = 5;
|
|
24
|
+
const PLATE = { x: -72, y: -68, w: 144, d: 136, r: 6 };
|
|
25
|
+
const PLATE_Z0 = 8;
|
|
26
|
+
const PLATE_Z1 = 10;
|
|
27
|
+
const DIAL_R = 50;
|
|
28
|
+
const SKIRT_TOP = 14.4;
|
|
29
|
+
const FACE_Z = 17.6;
|
|
30
|
+
const FACE_R = 46;
|
|
31
|
+
const HUB_R = 12;
|
|
32
|
+
const HUB_TOP = 19.4;
|
|
33
|
+
const PALM = { x: -18.5, y: -16.5, w: 30, d: 33, r: 3.2 };
|
|
34
|
+
const PALM_TOP = HUB_TOP + 8;
|
|
35
|
+
const COVER = { x: -15.5, y: -13.2, w: 26.6, d: 26.4, r: 2.6 };
|
|
36
|
+
const COVER_TOP = PALM_TOP + 1.2;
|
|
37
|
+
const INDEX_AT = ((AZIMUTH + 180) * Math.PI) / 180;
|
|
38
|
+
|
|
39
|
+
export const P = k.fitProjection([...k.boxCorners(BASE, -BASE_H - FOOT_H, 0), [0, 0, 40]], WIDTH, HEIGHT, { pad: 22, azimuth: AZIMUTH, elevation: ELEVATION });
|
|
40
|
+
const at = (p) => k.iso(p, P);
|
|
41
|
+
const S = (paths, style) => k.solidSvg(paths, style);
|
|
42
|
+
const L = (d, style) => k.lineSvg(d, style);
|
|
43
|
+
const D = (points, style) => k.dotsSvg(points, style);
|
|
44
|
+
const VERT = (x, y) => G.frameOf([x, y, 0], [0, 0, 1], [1, 0, 0], [0, 1, 0]);
|
|
45
|
+
const byDepth = (points) => [...points].sort((a, b) => k.depthOf([a[0], a[1], 0], P) - k.depthOf([b[0], b[1], 0], P));
|
|
46
|
+
|
|
47
|
+
export const R = A.recorder(P);
|
|
48
|
+
const put = (item) => R.put({ ...item, shapes: item.shapes.map((shape, index) => R.solid(Object.assign(shape, { name: shape.name ?? (index ? `${item.name}#${index + 1}` : item.name) }))) });
|
|
49
|
+
|
|
50
|
+
function base() {
|
|
51
|
+
for (const [x, y] of k.corners(BASE, 22)) {
|
|
52
|
+
const svg = S(k.cylinder(x, y, 7, -BASE_H - FOOT_H, 1.6, P, 28), { tone: "lo" }) + S(k.cylinder(x, y, 5.4, -BASE_H - FOOT_H + 1.6, FOOT_H - 1.6, P, 28), { tone: "mid" });
|
|
53
|
+
put({ name: `stand.foot.${x}.${y}`, layer: "back", at: [x, y, -BASE_H - FOOT_H], svg, shapes: [A.cylinder(x, y, 7, -BASE_H - FOOT_H, 1.6, { ground: true }), A.cylinder(x, y, 5.4, -BASE_H - FOOT_H + 1.6, FOOT_H - 1.6)] });
|
|
54
|
+
}
|
|
55
|
+
const screws = k.corners(BASE, 7);
|
|
56
|
+
const svg = S(k.slabOf(BASE, -BASE_H, BASE_H, P, 8, 1.6), { tone: "mid", inner: L(k.sideSeam(BASE, -BASE_H + 4, P, 8), { tone: "faint" }) }) + L(screws.map(([x, y]) => k.ring(x, y, 2.1, 0, P, 16)).join(""), { tone: "lo" }) + D(screws.map(([x, y]) => at([x, y, 0])), { size: 0.5 });
|
|
57
|
+
put({ name: "stand.base", layer: "back", at: [0, 0, -BASE_H], bias: 4, svg, shapes: [A.slab(BASE, -BASE_H, BASE_H, { ground: true })] });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const WHEEL_AT = [40 * Math.cos((AZIMUTH * Math.PI) / 180), 40 * Math.sin((AZIMUTH * Math.PI) / 180)];
|
|
61
|
+
const WHEEL_R = 32;
|
|
62
|
+
const PINION_R = 8;
|
|
63
|
+
const LOCK_Z0 = 3.2;
|
|
64
|
+
const LOCK_Z1 = 7.2;
|
|
65
|
+
const WINDOW = { x: 42.4, y: 35.6, w: 12.6, d: 10.4, r: 2.2 };
|
|
66
|
+
|
|
67
|
+
function backPosts() {
|
|
68
|
+
const out = [];
|
|
69
|
+
const V = [Math.cos((AZIMUTH * Math.PI) / 180), Math.sin((AZIMUTH * Math.PI) / 180)];
|
|
70
|
+
for (const [x, y] of byDepth(k.corners(PLATE, 12))) {
|
|
71
|
+
const front = x * V[0] + y * V[1] > 0;
|
|
72
|
+
const svg = S(k.cylinder(x, y, 3.4, 0, PLATE_Z0, P, 28), { tone: "mid", crease: "none" });
|
|
73
|
+
put({ name: `stand.post.${x}.${y}`, layer: front ? "mid" : "back", at: [x, y, 0], svg, shapes: [A.cylinder(x, y, 3.4, 0, PLATE_Z0)] });
|
|
74
|
+
}
|
|
75
|
+
for (const [name, [x, y], r] of [["stand.washer.dial", [0, 0], 4.2], ["stand.washer.wheel", WHEEL_AT, 6.4]]) {
|
|
76
|
+
const svg = S(k.cylinder(x, y, r, 0, LOCK_Z0, P, 32), { tone: "lo" });
|
|
77
|
+
put({ name, layer: "back", at: [x, y, 0], svg, shapes: [A.cylinder(x, y, r, 0, LOCK_Z0)] });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function windowed() {
|
|
82
|
+
const slab = k.slabOf(PLATE, PLATE_Z0, PLATE_Z1 - PLATE_Z0, P, 8, 1);
|
|
83
|
+
const ring = k.roundedPlan(WINDOW, 4);
|
|
84
|
+
const hole = k.onTop(ring, PLATE_Z1, P, true);
|
|
85
|
+
const V = [Math.cos((AZIMUTH * Math.PI) / 180), Math.sin((AZIMUTH * Math.PI) / 180)];
|
|
86
|
+
const lx = -Math.sin((AZIMUTH * Math.PI) / 180);
|
|
87
|
+
const ly = Math.cos((AZIMUTH * Math.PI) / 180);
|
|
88
|
+
const n = ring.length;
|
|
89
|
+
const walls = [[], [], [], []];
|
|
90
|
+
const bottoms = [];
|
|
91
|
+
for (let i = 0; i < n; i++) {
|
|
92
|
+
const a = ring[i];
|
|
93
|
+
const b = ring[(i + 1) % n];
|
|
94
|
+
const len = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
|
|
95
|
+
const nx = -(b[1] - a[1]) / len;
|
|
96
|
+
const ny = (b[0] - a[0]) / len;
|
|
97
|
+
if (nx * V[0] + ny * V[1] <= 1e-9) continue;
|
|
98
|
+
const shade = Math.min(3, Math.max(0, Math.floor(((nx * lx + ny * ly + 1) / 2) * 4)));
|
|
99
|
+
walls[shade].push(k.pathOf([at([a[0], a[1], PLATE_Z1]), at([b[0], b[1], PLATE_Z1]), at([b[0], b[1], PLATE_Z0]), at([a[0], a[1], PLATE_Z0])], true));
|
|
100
|
+
bottoms.push([a, b]);
|
|
101
|
+
}
|
|
102
|
+
const clip = `<clipPath id="td-window"><path d="${hole}"/></clipPath>`;
|
|
103
|
+
const wallSvg = walls.map((list, shade) => (list.length ? `<path class="iso-shade" data-shade="${shade}" d="${list.join("")}"/>` : "")).join("");
|
|
104
|
+
const under = bottoms.map(([a, b]) => k.pathOf([at([a[0], a[1], PLATE_Z0]), at([b[0], b[1], PLATE_Z0])])).join("");
|
|
105
|
+
const screws = byDepth(k.corners(PLATE, 12));
|
|
106
|
+
const inner = [
|
|
107
|
+
`<g clip-path="url(#td-window)">${wallSvg}${L(under, { tone: "lo" })}</g>`,
|
|
108
|
+
L(k.planOutline(k.insetPlan(PLATE, 4), PLATE_Z1, P), { tone: "faint" }),
|
|
109
|
+
L(screws.map(([x, y]) => k.ring(x, y, 2.4, PLATE_Z1, P, 18)).join(""), { tone: "lo" }),
|
|
110
|
+
L(k.planOutline(k.insetPlan(WINDOW, -1.4), PLATE_Z1, P, 4), { tone: "faint" }),
|
|
111
|
+
].join("");
|
|
112
|
+
const body = `<defs>${clip}</defs><g class="iso-solid"><path class="iso-fill" fill-rule="evenodd" d="${slab.fill}${hole}"/>${slab.shades.map((d, shade) => (d ? `<path class="iso-shade" data-shade="${shade}" d="${d}"/>` : "")).join("")}<path class="iso-top" fill-rule="evenodd" d="${slab.top}${hole}"/><path class="iso-line iso-crease" data-tone="faint" d="${slab.crease}"/><path class="iso-line iso-bevel" data-tone="faint" d="${slab.bevel}"/>${inner}<path class="iso-line iso-edge" data-tone="mid" d="${slab.outline}${hole}"/></g>`;
|
|
113
|
+
const dots = D(screws.map(([x, y]) => at([x, y, PLATE_Z1])), { size: 0.5 });
|
|
114
|
+
return `<path class="iso-halo" d="${k.haloOf(k.insetPlan(PLATE, 4), PLATE_Z0, 0.1, P)}" style="filter:blur(5px)"/>${body}${dots}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function midStatic() {
|
|
118
|
+
const svg = windowed();
|
|
119
|
+
const corners = k.roundedPlan(PLATE, 8);
|
|
120
|
+
void corners;
|
|
121
|
+
put({ name: "stand.plate", layer: "mid", at: [0, 0, PLATE_Z0], svg, shapes: [A.slab(PLATE, PLATE_Z0, PLATE_Z1 - PLATE_Z0)] });
|
|
122
|
+
const c = Math.cos(INDEX_AT);
|
|
123
|
+
const s = Math.sin(INDEX_AT);
|
|
124
|
+
const wedge = [[DIAL_R + 1.2, -0.7], [DIAL_R + 1.2, 0.7], [DIAL_R + 7.4, 2.6], [DIAL_R + 7.4, -2.6]].map(([r, t]) => [c * r - s * t, s * r + c * t]);
|
|
125
|
+
const wedgeSvg = S(k.extrude(wedge, PLATE_Z1, 3, P, { bevel: 0.35 }), { tone: "hi", lit: true });
|
|
126
|
+
put({ name: "stand.index", layer: "mid", at: [c * 48, s * 48, PLATE_Z1], svg: `<g data-mat="brass">${wedgeSvg}</g>`, shapes: [A.extruded(wedge, PLATE_Z1, 3)] });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
export const T = turning(P, { recorder: R });
|
|
131
|
+
const dial = T.group("dial", { turn: { pivot: [0, 0] } });
|
|
132
|
+
const wheel = T.group("wheel", { turn: { pivot: WHEEL_AT } });
|
|
133
|
+
const PAWL_AT = [0, DIAL_R + 8.3];
|
|
134
|
+
const pawl = T.group("pawl", { hinge: { point: [PAWL_AT[0], PAWL_AT[1], 0], axis: [0, 0, 1], range: [-4, 4] } });
|
|
135
|
+
const lock = T.layer("lock");
|
|
136
|
+
const face = T.layer("dial");
|
|
137
|
+
const hand = T.layer("hand");
|
|
138
|
+
T.stack(["back", "lock", "mid", "dial", "hand", "front"]);
|
|
139
|
+
export const follow = (values) => ({ ...values, wheel: -(values.dial ?? 0) / 4, pawl: 4 * Math.sin(((values.dial ?? 0) * Math.PI) / 18), "f2.mcp": -22 * (values.lift ?? 0), "f2.pip": -14 * (values.lift ?? 0), "f2.dip": -9 * (values.lift ?? 0) });
|
|
140
|
+
export const tapAt = (time) => {
|
|
141
|
+
const phase = ((time % 4) + 4) % 4;
|
|
142
|
+
const up = Math.min(1, Math.max(0, (phase - 0.4) / 0.9));
|
|
143
|
+
const down = Math.min(1, Math.max(0, (phase - 2.2) / 1.1));
|
|
144
|
+
const ease = (x) => x * x * (3 - 2 * x);
|
|
145
|
+
return ease(up) * (1 - ease(down));
|
|
146
|
+
};
|
|
147
|
+
export const breakAt = (time) => {
|
|
148
|
+
if (!(time > 0)) return null;
|
|
149
|
+
const flight = 0.79888;
|
|
150
|
+
const c = 1.0;
|
|
151
|
+
let phi = 0;
|
|
152
|
+
let psi = 0;
|
|
153
|
+
let y = 0;
|
|
154
|
+
let z = 0;
|
|
155
|
+
if (time < flight) {
|
|
156
|
+
const s = time / flight;
|
|
157
|
+
phi = 2 * Math.PI * s * s * (3 - 2 * s);
|
|
158
|
+
y = 16 * time;
|
|
159
|
+
z = 25 * time - 49 * time * time;
|
|
160
|
+
} else {
|
|
161
|
+
const k = 1 - Math.exp(-(time - flight) / 0.25);
|
|
162
|
+
phi = 2 * Math.PI;
|
|
163
|
+
y = 16 * flight + 1.5 * k;
|
|
164
|
+
z = -11.3;
|
|
165
|
+
psi = (40 * Math.PI * k) / 180;
|
|
166
|
+
}
|
|
167
|
+
const cp = Math.cos(phi);
|
|
168
|
+
const sp = Math.sin(phi);
|
|
169
|
+
const cz = Math.cos(psi);
|
|
170
|
+
const sz = Math.sin(psi);
|
|
171
|
+
const R = [cz, -sz * cp, sz * sp, sz, cz * cp, -cz * sp, 0, sp, cp];
|
|
172
|
+
return { R, t: [-R[2] * c, y - R[5] * c, z + c - R[8] * c] };
|
|
173
|
+
};
|
|
174
|
+
export const BREAK_END = 2.2;
|
|
175
|
+
export const poses = () => {
|
|
176
|
+
const out = Array.from({ length: 1440 }, (_, deg) => follow({ dial: deg, lift: tapAt(deg / 30) }));
|
|
177
|
+
for (let start = 0; start < 360; start += 45) for (let f = 0; f <= 66; f++) out.push(follow({ dial: start + f * 0.6, lift: tapAt(f / 30), pod: breakAt(f / 30) }));
|
|
178
|
+
return out;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const DIAL_PROFILE = [[PLATE_Z1, DIAL_R], [SKIRT_TOP, DIAL_R], [FACE_Z, FACE_R]];
|
|
182
|
+
const DIAL_F = VERT(0, 0);
|
|
183
|
+
const tick = (angle, from, to) => `M${(Math.sin(angle) * from).toFixed(3)} ${(-Math.cos(angle) * from).toFixed(3)}L${(Math.sin(angle) * to).toFixed(3)} ${(-Math.cos(angle) * to).toFixed(3)}`;
|
|
184
|
+
const ringPath = (r, steps) => Array.from({ length: steps }, (_, j) => `${j ? "L" : "M"}${(r * Math.cos((j / steps) * Math.PI * 2)).toFixed(3)} ${(r * Math.sin((j / steps) * Math.PI * 2)).toFixed(3)}`).join("") + "Z";
|
|
185
|
+
const faceMarkup = () => {
|
|
186
|
+
const rim = FACE_R - 0.8;
|
|
187
|
+
const minor = [];
|
|
188
|
+
const major = [];
|
|
189
|
+
for (let i = 0; i < 100; i++) {
|
|
190
|
+
const angle = (i / 100) * Math.PI * 2;
|
|
191
|
+
(i % 10 ? minor : major).push(tick(angle, rim - (i % 10 ? 1.4 : 2.6), rim));
|
|
192
|
+
}
|
|
193
|
+
const numerals = Array.from({ length: 10 }, (_, i) => {
|
|
194
|
+
const angle = (i / 10) * Math.PI * 2;
|
|
195
|
+
const r = rim - 5.4;
|
|
196
|
+
return `<text class="iso-face-text" data-tone="lo" font-size="3.4" text-anchor="middle" transform="translate(${(Math.sin(angle) * r).toFixed(3)} ${(-Math.cos(angle) * r).toFixed(3)}) rotate(${(i * 36).toFixed(1)}) translate(0 1.2)">${i * 10}</text>`;
|
|
197
|
+
}).join("");
|
|
198
|
+
return `<path class="iso-line" data-tone="lo" d="${ringPath(rim, 160)}"/><path class="iso-line" data-tone="lo" d="${minor.join("")}"/><path class="iso-line" data-tone="mid" d="${major.join("")}"/>${numerals}<path class="iso-line" data-tone="faint" d="${ringPath(33.5, 140)}"/>`;
|
|
199
|
+
};
|
|
200
|
+
const dialBody = T.round({ name: "dial.body", group: dial, layer: face, F: DIAL_F, s0: PLATE_Z1, s1: SKIRT_TOP, r: DIAL_R, tone: "hi", crease: "lo", material: "steel", details: { ribs: { s0: PLATE_Z1 + 0.9, s1: SKIRT_TOP - 0.7, count: 150, fade: [0.08, 0.5], seams: true, tone: "lo" } } });
|
|
201
|
+
const dialBevel = T.round({ name: "dial.bevel", group: dial, layer: face, F: DIAL_F, s0: SKIRT_TOP, s1: FACE_Z, r: [DIAL_R, FACE_R], tone: "hi", crease: "lo", material: "steel" });
|
|
202
|
+
T.plane({ part: dialBevel, o: [0, 0, FACE_Z], u: [1, 0, 0], v: [0, 1, 0], svg: faceMarkup(), fade: [0.04, 0.3] });
|
|
203
|
+
void dialBody;
|
|
204
|
+
T.round({ name: "dial.hub", group: dial, layer: hand, F: DIAL_F, s0: FACE_Z, s1: HUB_TOP, r: HUB_R, tone: "mid", material: "chrome", details: { seams: [FACE_Z + 0.9] } });
|
|
205
|
+
|
|
206
|
+
const wheelFace = () => {
|
|
207
|
+
const minor = [];
|
|
208
|
+
const major = [];
|
|
209
|
+
for (let i = 0; i < 100; i++) {
|
|
210
|
+
const angle = (i / 100) * Math.PI * 2;
|
|
211
|
+
(i % 10 ? minor : major).push(tick(angle, WHEEL_R - 3.2 - (i % 10 ? 1.2 : 2.4), WHEEL_R - 3.2));
|
|
212
|
+
}
|
|
213
|
+
const numerals = Array.from({ length: 10 }, (_, i) => {
|
|
214
|
+
const angle = (i / 10) * Math.PI * 2;
|
|
215
|
+
const r = WHEEL_R - 9.4;
|
|
216
|
+
return `<text class="iso-face-text" data-tone="mid" font-size="4.2" text-anchor="middle" transform="translate(${(Math.sin(angle) * r).toFixed(3)} ${(-Math.cos(angle) * r).toFixed(3)}) rotate(${(i * 36).toFixed(1)}) translate(0 1.5)">${i}</text>`;
|
|
217
|
+
}).join("");
|
|
218
|
+
return `<path class="iso-line" data-tone="lo" d="${ringPath(WHEEL_R - 3.2, 180)}"/><path class="iso-line" data-tone="lo" d="${minor.join("")}"/><path class="iso-line" data-tone="mid" d="${major.join("")}"/>${numerals}`;
|
|
219
|
+
};
|
|
220
|
+
T.round({ name: "lock.pinion", group: dial, layer: lock, F: VERT(0, 0), s0: LOCK_Z0, s1: LOCK_Z1, r: PINION_R, tone: "mid", material: "brass", details: { ribs: { s0: LOCK_Z0 + 0.5, s1: LOCK_Z1 - 0.5, count: 48, seams: true, tone: "lo" } } });
|
|
221
|
+
const wheelDisc = T.round({ name: "lock.wheel", group: wheel, layer: lock, F: VERT(WHEEL_AT[0], WHEEL_AT[1]), s0: LOCK_Z0, s1: LOCK_Z1, r: WHEEL_R, tone: "hi", crease: "lo", material: "brass", details: { ribs: { s0: LOCK_Z0 + 0.5, s1: LOCK_Z1 - 0.5, count: 192, seams: true, tone: "lo" } } });
|
|
222
|
+
T.plane({ part: wheelDisc, o: [WHEEL_AT[0], WHEEL_AT[1], LOCK_Z1], u: [1, 0, 0], v: [0, 1, 0], svg: wheelFace() });
|
|
223
|
+
T.round({ name: "lock.wheel.hub", group: wheel, layer: lock, F: VERT(WHEEL_AT[0], WHEEL_AT[1]), s0: LOCK_Z1, s1: LOCK_Z1 + 0.5, r: 6, tone: "mid", material: "steel", details: { bolts: [{ s: LOCK_Z1 + 0.5, r: 3.8, count: 4, size: 0.5, phase: 0.4 }] } });
|
|
224
|
+
|
|
225
|
+
T.round({ name: "pawl.post", layer: face, F: VERT(PAWL_AT[0], PAWL_AT[1]), s0: PLATE_Z1, s1: PLATE_Z1 + 1, r: 2.4, tone: "mid", material: "gunmetal" });
|
|
226
|
+
const pawlRing = [];
|
|
227
|
+
for (let j = 0; j <= 10; j++) pawlRing.push([PAWL_AT[0] + 2.2 * Math.cos((j / 10) * Math.PI), PAWL_AT[1] + 2.2 * Math.sin((j / 10) * Math.PI)]);
|
|
228
|
+
pawlRing.push([PAWL_AT[0] - 0.6, DIAL_R + 0.4], [PAWL_AT[0], DIAL_R + 0.15], [PAWL_AT[0] + 0.6, DIAL_R + 0.4]);
|
|
229
|
+
T.prism({ name: "pawl.arm", group: pawl, layer: face, F: { o: [0, 0, 0], a: [0, 0, 1], u: [1, 0, 0], v: [0, 1, 0] }, polygon: pawlRing, s0: PLATE_Z1 + 1, s1: PLATE_Z1 + 2.6, tone: "hi", crease: "lo", bevel: 0.3, material: "chrome" });
|
|
230
|
+
T.round({ name: "pawl.pin", group: pawl, layer: face, F: VERT(PAWL_AT[0], PAWL_AT[1]), s0: PLATE_Z1 + 2.6, s1: PLATE_Z1 + 3.3, r: 1.1, tone: "mid", material: "steel", details: { bolts: [{ s: PLATE_Z1 + 3.3, r: 0, count: 1, size: 0.45 }] } });
|
|
231
|
+
|
|
232
|
+
const H = { group: dial, layer: hand };
|
|
233
|
+
const plan = (name, rect, z, h, extra = {}) => T.prism({ name, ...H, plan: rect, z, h, steps: extra.steps ?? 2, tone: "mid", ...extra });
|
|
234
|
+
const along = (name, F, s0, s1, r, extra = {}) => T.round({ name, ...H, F, s0, s1, r, tone: "mid", ...extra });
|
|
235
|
+
const frameOf = (o, a, u) => {
|
|
236
|
+
const axis = G.unit3(a);
|
|
237
|
+
const uu = u ? G.unit3(G.sub3(u, G.mul3(axis, G.dot3(u, axis)))) : G.frameAlong(o, axis).u;
|
|
238
|
+
return { o, a: axis, u: uu, v: G.cross3(axis, uu) };
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const sideScrews = [];
|
|
242
|
+
for (const [y, ny] of [[PALM.y, -1], [PALM.y + PALM.d, 1]]) for (const x of [-11, 3]) sideScrews.push({ at: [HUB_TOP + 5.2, x, y], normal: [0, ny, 0], size: 0.45, tone: "mid" });
|
|
243
|
+
for (const y of [-7, 7]) sideScrews.push({ at: [HUB_TOP + 5.2, PALM.x, y], normal: [-1, 0, 0], size: 0.45, tone: "mid" });
|
|
244
|
+
T.prism({ name: "hand.palm", ...H, plan: PALM, z: HUB_TOP, h: PALM_TOP - HUB_TOP, steps: 4, bevel: 0.9, tone: "hi", crease: "lo", material: "gold", details: { seams: [HUB_TOP + 2.6], dots: sideScrews, rings: sideScrews.map((d) => ({ at: d.at, normal: d.normal, r: 0.95, tone: "lo" })) } });
|
|
245
|
+
const coverScrews = [[-13.7, -11.4], [-13.7, 11.4], [9.3, -11.4], [9.3, 11.4]].map(([x, y]) => ({ at: [COVER_TOP, x, y], normal: [0, 0, 1], size: 0.48, tone: "mid" }));
|
|
246
|
+
T.prism({ name: "hand.cover", ...H, plan: COVER, z: PALM_TOP, h: COVER_TOP - PALM_TOP, steps: 3, bevel: 0.45, tone: "mid", material: "gold", details: { dots: coverScrews, rings: coverScrews.map((d) => ({ at: d.at, normal: [0, 0, 1], r: 1.1, tone: "lo" })) } });
|
|
247
|
+
const CONN = { x: -15.1, y: -10.4, w: 4.4, d: 20.8, r: 0.9 };
|
|
248
|
+
plan("hand.connector", CONN, COVER_TOP, 5.2, { bevel: 0.35, material: "gunmetal", details: { seams: [COVER_TOP + 1.2] } });
|
|
249
|
+
const CABLE_Z = COVER_TOP + 1.9;
|
|
250
|
+
const PORT_X = CONN.x + CONN.w;
|
|
251
|
+
const PORTS = { red: -8.6, green: -5.4, blue: 8.6 };
|
|
252
|
+
const CLAMP_X = -2.3;
|
|
253
|
+
const CLAMP_R = 1.45;
|
|
254
|
+
for (const [colour, y] of Object.entries(PORTS)) along(`hand.port.${colour}`, frameOf([PORT_X, y, CABLE_Z], [1, 0, 0], [0, 0, 1]), 0, 0.7, 1.15, { owner: `cable.${colour}`, material: "brass" });
|
|
255
|
+
const CLAMP_T = 1.1;
|
|
256
|
+
for (const colour of Object.keys(PORTS)) along(`hand.clamp.${colour}`, frameOf([CLAMP_X, PORTS[colour], CABLE_Z], [1, 0, 0], [0, 0, 1]), -CLAMP_T / 2, CLAMP_T / 2, CLAMP_R, { owner: `cable.${colour}`, material: "brass" });
|
|
257
|
+
plan("hand.clamp.left", { x: CLAMP_X - 1.3, y: -10.4, w: 2.6, d: 6.6, r: 0.6 }, COVER_TOP, CABLE_Z - CLAMP_R - COVER_TOP, { material: "gunmetal" });
|
|
258
|
+
plan("hand.clamp.right", { x: CLAMP_X - 1.3, y: 6.9, w: 2.6, d: 3.4, r: 0.6 }, COVER_TOP, CABLE_Z - CLAMP_R - COVER_TOP, { material: "gunmetal" });
|
|
259
|
+
const TERM = { x: 7.3, y: PORTS.green - 1.6, w: 2.6, d: 3.2, r: 0.5 };
|
|
260
|
+
plan("hand.terminal", TERM, COVER_TOP, CABLE_Z + 1.8 - COVER_TOP, { bevel: 0.3, material: "gunmetal" });
|
|
261
|
+
|
|
262
|
+
const PIST_Y = 0;
|
|
263
|
+
const BODY_R = 2.4;
|
|
264
|
+
const SADDLE_H = 0.9;
|
|
265
|
+
const PIST_Z = COVER_TOP + SADDLE_H + BODY_R;
|
|
266
|
+
const PIST_F = frameOf([0, PIST_Y, PIST_Z], [1, 0, 0], [0, 0, 1]);
|
|
267
|
+
const BODY = [-6.1, 2.9];
|
|
268
|
+
for (const [index, x] of [-5.1, 0.1].entries()) plan(`hand.saddle.${index}`, { x, y: PIST_Y - 1.9, w: 2.4, d: 3.8, r: 0.5 }, COVER_TOP, SADDLE_H, { material: "gunmetal" });
|
|
269
|
+
along("hand.piston.body", PIST_F, BODY[0], BODY[1], BODY_R, { tone: "hi", crease: "lo", material: "gunmetal", details: { seams: [BODY[0] + 1.6, BODY[1] - 1.6] } });
|
|
270
|
+
const GLAND = [BODY[1], BODY[1] + 1.4];
|
|
271
|
+
const glandRadius = (sv) => 2.3 + 0.55 * Math.sqrt(Math.max(0, 1 - ((sv - GLAND[0]) / (GLAND[1] - GLAND[0])) ** 2));
|
|
272
|
+
const glandSlope = (sv) => {
|
|
273
|
+
const x = Math.min(0.999, (sv - GLAND[0]) / (GLAND[1] - GLAND[0]));
|
|
274
|
+
return (-0.55 * x) / ((GLAND[1] - GLAND[0]) * Math.sqrt(1 - x * x));
|
|
275
|
+
};
|
|
276
|
+
T.lathe({ name: "hand.piston.gland", ...H, F: PIST_F, profile: [[GLAND[0], glandRadius(GLAND[0])], [GLAND[1], glandRadius(GLAND[1])]], radius: glandRadius, slope: glandSlope, tone: "mid", material: "brass", details: { bolts: [{ s: GLAND[1], r: 1.75, count: 6, phase: 0.3, size: 0.34, tone: "mid" }] } });
|
|
277
|
+
const ROD_END = 8.9;
|
|
278
|
+
along("hand.piston.rod", PIST_F, GLAND[1], ROD_END, 0.95, { tone: "hi", material: "chrome" });
|
|
279
|
+
plan("hand.clevis", { x: ROD_END, y: PIST_Y - 1.9, w: 2.4, d: 3.8, r: 0.5 }, COVER_TOP, PIST_Z + 2.4 - COVER_TOP, { bevel: 0.3, material: "steel", details: { dots: [-1, 1].map((side) => ({ at: [PIST_Z, ROD_END + 1.2, PIST_Y + side * 1.9], normal: [0, side, 0], size: 0.42 })) } });
|
|
280
|
+
const BALL_R = 2.2;
|
|
281
|
+
const BALL = [BODY[0] - BALL_R, PIST_Y, PIST_Z];
|
|
282
|
+
T.ball({ name: "hand.ball", ...H, c: BALL, r: BALL_R, tone: "hi", material: "chrome" });
|
|
283
|
+
along("hand.ball.post", VERT(BALL[0], BALL[1]), COVER_TOP, BALL[2] - BALL_R, 1.3, { material: "steel" });
|
|
284
|
+
|
|
285
|
+
const FINGERS = [
|
|
286
|
+
{ id: 1, y: -9.2, psi: -13, port: true, cable: "red" },
|
|
287
|
+
{ id: 2, y: 0, psi: 0, guide: true, flex: true },
|
|
288
|
+
{ id: 3, y: 9.2, psi: 13, port: true, cable: "blue" },
|
|
289
|
+
];
|
|
290
|
+
const D2R = Math.PI / 180;
|
|
291
|
+
const FZ = HUB_TOP + 4;
|
|
292
|
+
const RL = 2.35;
|
|
293
|
+
const LENGTHS = [7.4, 5.8, 3.6];
|
|
294
|
+
const GAP = 0.6;
|
|
295
|
+
const DISC_T = 1.2;
|
|
296
|
+
const PITCH = 8 * D2R;
|
|
297
|
+
const JUNCTION_H = 6;
|
|
298
|
+
export const TIPS = [];
|
|
299
|
+
export const LINKS = [];
|
|
300
|
+
export const LED = [];
|
|
301
|
+
const FINGER_PORTS = {};
|
|
302
|
+
for (const finger of FINGERS) {
|
|
303
|
+
const psi = finger.psi * D2R;
|
|
304
|
+
const d1 = [Math.cos(psi), Math.sin(psi), 0];
|
|
305
|
+
const w = [-Math.sin(psi), Math.cos(psi), 0];
|
|
306
|
+
const pitch = (a) => [Math.cos(a) * Math.cos(psi), Math.cos(a) * Math.sin(psi), -Math.sin(a)];
|
|
307
|
+
const B = [PALM.x + PALM.w, finger.y, FZ];
|
|
308
|
+
const R0 = 3.7;
|
|
309
|
+
const e0 = R0 + Math.abs(Math.tan(psi)) * (RL + DISC_T);
|
|
310
|
+
const C1 = G.add3(B, G.mul3(d1, e0));
|
|
311
|
+
const E1 = G.add3(C1, G.mul3(d1, LENGTHS[0]));
|
|
312
|
+
const d2 = pitch(PITCH);
|
|
313
|
+
const C2 = G.add3(E1, G.mul3(G.unit3(G.add3(d1, d2)), 2 * RL + GAP));
|
|
314
|
+
const J1 = G.mul3(G.add3(E1, C2), 0.5);
|
|
315
|
+
const E2 = G.add3(C2, G.mul3(d2, LENGTHS[1]));
|
|
316
|
+
const tipOf = (a3) => {
|
|
317
|
+
const d3 = pitch(a3);
|
|
318
|
+
const C3 = G.add3(E2, G.mul3(G.unit3(G.add3(d2, d3)), 2 * RL + GAP));
|
|
319
|
+
return { d3, C3, E3: G.add3(C3, G.mul3(d3, LENGTHS[2])), J2: G.mul3(G.add3(E2, C3), 0.5) };
|
|
320
|
+
};
|
|
321
|
+
let lo = 0;
|
|
322
|
+
let hi = 70 * D2R;
|
|
323
|
+
for (let it = 0; it < 60; it++) {
|
|
324
|
+
const mid = (lo + hi) / 2;
|
|
325
|
+
if (tipOf(mid).E3[2] - RL > FACE_Z) lo = mid;
|
|
326
|
+
else hi = mid;
|
|
327
|
+
}
|
|
328
|
+
const { C3, E3, J2 } = tipOf(hi);
|
|
329
|
+
TIPS.push(E3);
|
|
330
|
+
const tag = `hand.f${finger.id}`;
|
|
331
|
+
const g1 = finger.flex ? T.group(`f${finger.id}.mcp`, { parent: dial, hinge: { point: C1, axis: w, range: [-22, 0] } }) : dial;
|
|
332
|
+
const g2 = finger.flex ? T.group(`f${finger.id}.pip`, { parent: g1, hinge: { point: J1, axis: w, range: [-14, 0] } }) : dial;
|
|
333
|
+
const g3 = finger.flex ? T.group(`f${finger.id}.dip`, { parent: g2, hinge: { point: J2, axis: w, range: [-9, 0] } }) : dial;
|
|
334
|
+
if (finger.flex) LINKS.push({ tip: E3, dip: J2, groups: [g1.name, g2.name, g3.name], d3: G.unit3(G.sub3(E3, C3)), w });
|
|
335
|
+
const link = (name, C, E, seams, group) => T.round({ name: `${tag}.${name}`, group, layer: hand, F: frameOf(C, G.sub3(E, C), [0, 0, 1]), s0: 0, s1: G.len3(G.sub3(E, C)), r: RL, ends: ["dome", "dome"], tone: "hi", crease: "lo", material: "gold", details: { seams } });
|
|
336
|
+
link("p1", C1, E1, [1.2, LENGTHS[0] - 1.2], g1);
|
|
337
|
+
link("p2", C2, E2, [1.0, LENGTHS[1] - 1.0], g2);
|
|
338
|
+
const tipLink = link("p3", C3, E3, [0.9], g3);
|
|
339
|
+
if (finger.flex) {
|
|
340
|
+
const d3 = G.unit3(G.sub3(E3, C3));
|
|
341
|
+
const led = G.add3(E3, G.mul3(d3, RL));
|
|
342
|
+
LED.push({ at: led, normal: d3, part: tipLink.name, group: g3.name });
|
|
343
|
+
T.billboard({ part: tipLink, at: led, normal: d3, fade: [0.02, 0.3], svg: `<g class="td-led"><circle r="1.15" class="td-led-halo"/><circle r="0.62" class="td-led-body"/><circle r="0.26" class="td-led-core"/></g>` });
|
|
344
|
+
}
|
|
345
|
+
const discs = (name, J, rDisc, group) => {
|
|
346
|
+
for (const side of [-1, 1]) {
|
|
347
|
+
const centre = G.add3(J, G.mul3(w, side * (RL + DISC_T / 2)));
|
|
348
|
+
const F = frameOf(centre, G.mul3(w, side), [0, 0, 1]);
|
|
349
|
+
T.round({ name: `${tag}.${name}${side > 0 ? "+" : "-"}`, group, layer: hand, F, s0: -DISC_T / 2, s1: DISC_T / 2, r: rDisc, tone: "mid", material: "chrome", details: { dots: [{ at: [DISC_T / 2, 0, 0], normal: G.mul3(w, side), size: 0.42 }], rings: [{ at: [DISC_T / 2, 0, 0], normal: G.mul3(w, side), r: 0.85, tone: "lo" }] } });
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
discs("k0", C1, R0, dial);
|
|
353
|
+
discs("k1", J1, 3.5, g1);
|
|
354
|
+
discs("k2", J2, 3.3, g2);
|
|
355
|
+
if (finger.port) {
|
|
356
|
+
const M = G.mul3(G.add3(C1, E1), 0.5);
|
|
357
|
+
void M;
|
|
358
|
+
const ring = [[-2.1, -1.7], [2.1, -1.7], [2.1, 1.7], [-2.1, 1.7]].map(([a, b]) => [M[0] + d1[0] * a + w[0] * b, M[1] + d1[1] * a + w[1] * b]);
|
|
359
|
+
const z0 = M[2] + RL;
|
|
360
|
+
T.prism({ name: `${tag}.junction`, ...H, F: { o: [0, 0, 0], a: [0, 0, 1], u: [1, 0, 0], v: [0, 1, 0] }, polygon: ring, s0: z0, s1: z0 + JUNCTION_H, tone: "mid", bevel: 0.3, material: "gunmetal", details: { seams: [z0 + 1] } });
|
|
361
|
+
const back = [M[0] - d1[0] * 2.1, M[1] - d1[1] * 2.1, z0 + JUNCTION_H * 0.62];
|
|
362
|
+
along(`${tag}.port`, frameOf(back, G.mul3(d1, -1), [0, 0, 1]), 0, 0.6, 0.95, { owner: `cable.${finger.cable}`, material: "brass" });
|
|
363
|
+
FINGER_PORTS[finger.id] = { face: back, d1 };
|
|
364
|
+
}
|
|
365
|
+
if (finger.guide) {
|
|
366
|
+
const n = G.unit3(G.cross3(d2, w));
|
|
367
|
+
const up = n[2] > 0 ? n : G.mul3(n, -1);
|
|
368
|
+
T.prism({ name: `${tag}.guide`, group: g2, layer: hand, F: { o: C2, a: d2, u: w, v: up }, polygon: [[-1.5, RL], [1.5, RL], [1.15, RL + 0.9], [-1.15, RL + 0.9]], s0: 1.1, s1: LENGTHS[1] - 1.1, tone: "mid", bevel: 0.25, material: "gunmetal" });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const CABLE_R = 0.7;
|
|
373
|
+
const BEND = 8 * CABLE_R;
|
|
374
|
+
const portStart = (colour) => [PORT_X, PORTS[colour], CABLE_Z];
|
|
375
|
+
const cable = (colour, waypoints, ends = [0.7, 0]) => {
|
|
376
|
+
const points = G.fillet(waypoints, BEND, 12);
|
|
377
|
+
const at = (x) => {
|
|
378
|
+
let walked = 0;
|
|
379
|
+
for (let i = 1; i < points.length; i++) {
|
|
380
|
+
const a = points[i - 1];
|
|
381
|
+
const b = points[i];
|
|
382
|
+
const length = G.len3(G.sub3(b, a));
|
|
383
|
+
if ((a[0] - x) * (b[0] - x) <= 0 && Math.abs(b[0] - a[0]) > 1e-9) return walked + (length * (x - a[0])) / (b[0] - a[0]);
|
|
384
|
+
walked += length;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
};
|
|
388
|
+
const hit = at(CLAMP_X);
|
|
389
|
+
const total = G.pathLength(points);
|
|
390
|
+
const gaps = [[0, ends[0]], ...(hit === null ? [] : [[hit - CLAMP_T / 2, hit + CLAMP_T / 2]]), ...(ends[1] ? [[total - ends[1], total]] : [])];
|
|
391
|
+
T.tube({ name: `cable.${colour}`, ...H, route: points, r: CABLE_R, chunk: 6, gaps, hue: colour, tone: "hi", material: "rubber" });
|
|
392
|
+
};
|
|
393
|
+
const approach = (port, length, rise) => G.add3(G.add3(port.face, G.mul3(port.d1, -length)), [0, 0, rise]);
|
|
394
|
+
cable("red", [portStart("red"), [-0.4, PORTS.red, CABLE_Z], approach(FINGER_PORTS[1], 7.6, 0.3), FINGER_PORTS[1].face], [0.7, 0.6]);
|
|
395
|
+
cable("green", [portStart("green"), [TERM.x, PORTS.green, CABLE_Z]]);
|
|
396
|
+
cable("blue", [portStart("blue"), [-0.4, PORTS.blue, CABLE_Z], approach(FINGER_PORTS[3], 7.6, 0.3), FINGER_PORTS[3].face], [0.7, 0.6]);
|
|
397
|
+
const HOSE_R = 0.9;
|
|
398
|
+
const HOSE_Y = 4.6;
|
|
399
|
+
const HOSE_Z = COVER_TOP + 3.3;
|
|
400
|
+
const VALVE = { x: 3.4, y: HOSE_Y - 1.8, w: 3.4, d: 3.6, r: 0.6 };
|
|
401
|
+
plan("hand.valve", VALVE, COVER_TOP, HOSE_Z + 1.9 - COVER_TOP, { bevel: 0.3, material: "gunmetal", details: { seams: [COVER_TOP + 1.2], dots: [[VALVE.x + 0.8, VALVE.y + 0.8], [VALVE.x + VALVE.w - 0.8, VALVE.y + VALVE.d - 0.8]].map(([x, y]) => ({ at: [HOSE_Z + 1.9, x, y], normal: [0, 0, 1], size: 0.4 })) } });
|
|
402
|
+
const FERRULE = 1.3;
|
|
403
|
+
const hoseFrom = [PORT_X, HOSE_Y, HOSE_Z];
|
|
404
|
+
const hoseTo = [VALVE.x, HOSE_Y, HOSE_Z];
|
|
405
|
+
along("hand.hose.ferrule.0", frameOf(hoseFrom, [1, 0, 0], [0, 0, 1]), 0, FERRULE, 1.25, { owner: "hand.hose", material: "brass", details: { seams: [0.45] } });
|
|
406
|
+
along("hand.hose.ferrule.1", frameOf(hoseTo, [-1, 0, 0], [0, 0, 1]), 0, FERRULE, 1.25, { owner: "hand.hose", material: "brass", details: { seams: [0.45] } });
|
|
407
|
+
const hoseLength = G.len3(G.sub3(hoseTo, hoseFrom));
|
|
408
|
+
T.tube({ name: "hand.hose", ...H, route: [hoseFrom, hoseTo], r: HOSE_R, chunk: 6, gaps: [[0, FERRULE], [hoseLength - FERRULE, hoseLength]], rings: { pitch: 1.5, twist: 1.6, cross: true, fade: [0.1, 0.55], tone: "lo" }, tone: "hi", material: "steel" });
|
|
409
|
+
|
|
410
|
+
const KNUCKLE_R = 3.2;
|
|
411
|
+
const KNUCKLE_D = 2.6;
|
|
412
|
+
const KNUCKLE = [-4, PALM.y - KNUCKLE_D, FZ];
|
|
413
|
+
T.ball({ name: "hand.thumb.knuckle", ...H, c: KNUCKLE, r: KNUCKLE_R, flats: [{ n: [0, 1, 0], d: KNUCKLE_D }, { n: [0, -1, 0], d: KNUCKLE_D }], tone: "hi", material: "chrome" });
|
|
414
|
+
const THUMB_F = frameOf([KNUCKLE[0], KNUCKLE[1] - KNUCKLE_D, KNUCKLE[2]], [0, -1, 0], [0, 0, 1]);
|
|
415
|
+
T.round({ name: "hand.thumb.cone", ...H, F: THUMB_F, s0: 0, s1: 3, r: [1.8, 1.25], tone: "mid", material: "steel" });
|
|
416
|
+
const THUMB_R = 1.9;
|
|
417
|
+
T.round({ name: "hand.thumb.link", ...H, F: frameOf(G.pointOf(THUMB_F, 3 + THUMB_R, 0, 0), [0, -1, 0], [0, 0, 1]), s0: 0, s1: 6.5, r: THUMB_R, ends: ["dome", "dome"], tone: "hi", crease: "lo", material: "gold", details: { seams: [1.2, 5.3] } });
|
|
418
|
+
|
|
419
|
+
const POD = [-7, 11.4];
|
|
420
|
+
const POD_Z = COVER_TOP + 0.3;
|
|
421
|
+
T.round({ name: "hand.pod.socket", ...H, F: VERT(POD[0], POD[1]), s0: COVER_TOP, s1: POD_Z, r: 1.7, tone: "mid", material: "gunmetal" });
|
|
422
|
+
const podGroup = T.group("pod", { parent: dial, free: { origin: [POD[0], POD[1], POD_Z] } });
|
|
423
|
+
const hex = Array.from({ length: 6 }, (_, j) => [1.35 * Math.cos(((2 * j + 1) * Math.PI) / 6), 1.35 * Math.sin(((2 * j + 1) * Math.PI) / 6)]);
|
|
424
|
+
T.prism({ name: "hand.pod.nut", group: podGroup, layer: hand, F: { o: [POD[0], POD[1], 0], a: [0, 0, 1], u: [1, 0, 0], v: [0, 1, 0] }, polygon: hex, s0: POD_Z, s1: POD_Z + 1.4, tone: "hi", crease: "lo", bevel: 0.2, material: "brass" });
|
|
425
|
+
T.round({ name: "hand.pod.cap", group: podGroup, layer: hand, F: VERT(POD[0], POD[1]), s0: POD_Z + 1.4, s1: POD_Z + 2.0, r: 0.9, tone: "mid", material: "chrome", details: { bolts: [{ s: POD_Z + 2.0, r: 0, count: 1, size: 0.4 }] } });
|
|
426
|
+
export const POD_AT = [POD[0], POD[1], POD_Z];
|
|
427
|
+
|
|
428
|
+
base();
|
|
429
|
+
backPosts();
|
|
430
|
+
midStatic();
|
|
431
|
+
const verify = argv.includes("--verify");
|
|
432
|
+
const out = T.build({ statics: R.items, poses, verify });
|
|
433
|
+
A.settle(R, P);
|
|
434
|
+
const layerOf = (name) => R.items.filter((item) => item.layer === name).sort((a, b) => a.key - b.key).map((item) => item.svg);
|
|
435
|
+
const BACK = [`<path class="iso-halo" d="${k.haloOf(BASE, -BASE_H - FOOT_H, BASE_H, P)}"/>`, ...layerOf("back")];
|
|
436
|
+
const MID = layerOf("mid");
|
|
437
|
+
const FRONT = layerOf("front");
|
|
438
|
+
if (argv.includes("--audit")) {
|
|
439
|
+
A.auditOrExit(R, P);
|
|
440
|
+
TA.orbitOrExit(T, R, P, {
|
|
441
|
+
sweep: { at: (x) => follow({ dial: x }), from: 0, to: 1440, frame: 0.3 },
|
|
442
|
+
clips: [
|
|
443
|
+
{ name: "tap", at: (t) => follow({ dial: 40 + 12 * t, lift: tapAt(t) }), from: 0, to: 4, frame: 1 / 60 },
|
|
444
|
+
{ name: "break", at: (t) => follow({ dial: 100 + 12 * t, lift: 0.3, pod: breakAt(t) }), from: 0, to: BREAK_END, frame: 1 / 60 },
|
|
445
|
+
],
|
|
446
|
+
crowd: [["cable.red", "hand.f1.p2"], ["cable.red", "hand.f2.p1"], ["cable.red", "hand.f2.p2"], ["cable.red", "hand.thumb.link"], ["cable.blue", "hand.f2.p1"], ["cable.blue", "hand.f3.p2"]], quick: argv.includes("--quick"), only: argv.find((a) => a.startsWith("--only="))?.slice(7).split(",") ?? null, log: argv.includes("--verbose") ? (text) => console.log(` … ${text}`) : null });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const LABEL = "A combination dial on a raised door plate. A small robot hand lies on the dial's hub and turns with it: a palm with a cover plate, a connector block, a piston on two saddles with a ball joint, three jointed fingers whose tips rest on the dial face, and a thumb on a ball knuckle.";
|
|
450
|
+
const backSvg = k.figureSvg({ width: WIDTH, height: HEIGHT, label: LABEL, body: BACK });
|
|
451
|
+
const staticSvg = (list) => `<svg xmlns="http://www.w3.org/2000/svg" class="iso-svg iso-layer" viewBox="0 0 ${WIDTH} ${HEIGHT}" aria-hidden="true">${list.join("")}</svg>`;
|
|
452
|
+
const frontSvg = staticSvg(FRONT);
|
|
453
|
+
const canvas = (name) => `<canvas class="iso-layer td-gl" data-canvas="${name}" aria-hidden="true"></canvas>`;
|
|
454
|
+
const stage = `<div class="iso-stage iso-turn td-stage" tabindex="0" role="slider" aria-label="Combination dial: drag around the dial or use the arrow keys to turn it, press B to break the pod off" aria-valuemin="0" aria-valuemax="99" aria-valuenow="0" aria-valuetext="dial at 0">${backSvg}${out.layerSvg("lock", { width: WIDTH, height: HEIGHT })}${staticSvg(MID)}${out.layerSvg("dial", { width: WIDTH, height: HEIGHT })}${canvas("back")}${out.layerSvg("hand", { width: WIDTH, height: HEIGHT })}${canvas("front")}${frontSvg}</div>`;
|
|
455
|
+
|
|
456
|
+
const DATA = { turn: out.data, index: INDEX_AT, pod: POD_AT, breakEnd: BREAK_END, links: LINKS, gl: { W: WIDTH, H: HEIGHT, camera: glCamera(P), led: LED[0], faceZ: FACE_Z, faceR: FACE_R, hubR: HUB_R, socket: [POD_AT[0], POD_AT[1], POD_AT[2]] } };
|
|
457
|
+
const FOLLOW = `const follow = ${follow.toString()};\nconst tapAt = ${tapAt.toString()};\nconst breakAt = ${breakAt.toString()};`;
|
|
458
|
+
const CSS = out.css + "\n" + readFileSync(join(HERE, "dial.css"), "utf8");
|
|
459
|
+
const LIVE = readFileSync(join(HERE, "live.js"), "utf8").replace("__DATA__", JSON.stringify(DATA)).replace("const follow = null;", FOLLOW);
|
|
460
|
+
const body = k.plateHtml({
|
|
461
|
+
fig: "Fig 3D",
|
|
462
|
+
title: "Turning dial",
|
|
463
|
+
hint: "Drag the dial · arrow keys · B breaks the pod",
|
|
464
|
+
readout: "dial 0 · 0.0°",
|
|
465
|
+
keys: [
|
|
466
|
+
{ mark: "raised", label: "The dial and the hand on it: one rigid group" },
|
|
467
|
+
{ mark: "lit", label: "The index" },
|
|
468
|
+
],
|
|
469
|
+
caption: "The optional 3D mode: every part on the dial is re-projected and re-shaded each frame by the kit's own rules, and painted in an order solved from separating planes.",
|
|
470
|
+
body: `<style>${CSS}</style>${stage}`,
|
|
471
|
+
});
|
|
472
|
+
const page = k.pageHtml({ title: "Turning dial", theme, body, script: turnScript() + glScript() + (verify ? `window.__isoVerify = ${JSON.stringify(out.verify)};\n` : "") + LIVE, width: 680 });
|
|
473
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
474
|
+
const name = `turning-dial${verify ? "-verify" : ""}${theme === "light" ? "-light" : ""}.html`;
|
|
475
|
+
writeFileSync(join(HERE, name), page);
|
|
476
|
+
console.log(`wrote ${name} · ${(page.length / 1024).toFixed(0)} KB · ${out.report.parts} parts · ${out.report.layers.map((l) => `${l.name}: ${l.planar} planes, ${l.dynamic} dynamic`).join(" · ")} · build ${out.report.seconds.toFixed(1)} s`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export { out };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
.td-stage{touch-action:pan-y;cursor:grab;outline:none;border-radius:8px}
|
|
2
|
+
.td-stage:focus-visible{outline:1px solid var(--anatomy-muted);outline-offset:6px}
|
|
3
|
+
.td-stage[data-drag]{cursor:grabbing}
|
|
4
|
+
.td-gl{display:block}
|
|
5
|
+
.td-led{--td-led:#2fb36f;--td-led-pale:#e9fff2}
|
|
6
|
+
.iso[data-theme="dark"] .td-led{--td-led:#5cf0a0;--td-led-pale:#f2fff7}
|
|
7
|
+
.td-led-halo{fill:var(--td-led);opacity:.18}
|
|
8
|
+
.td-led-body{fill:var(--td-led)}
|
|
9
|
+
.td-led-core{fill:var(--td-led-pale)}
|
|
10
|
+
.td-stage[data-gl] .td-led{opacity:var(--td-fallback,0)}
|