axstack 0.36.1 → 0.38.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 +5 -4
- package/bin/axstack.js +4 -18
- package/docs/getting-started.md +6 -6
- package/docs/host-operations.md +9 -5
- 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/automations.md +7 -2
- package/skills/axstack/references/lifecycle.md +2 -1
- package/skills/axstack/references/review-manager-prompt.md +44 -3
- package/skills/axstack/references/role-roster.md +13 -6
- package/skills/axstack/references/routing.md +2 -1
- package/skills/axstack/references/run-record.md +2 -0
- 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/skills/axstack-research/SKILL.md +18 -6
- 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,342 @@
|
|
|
1
|
+
const DATA = __DATA__;
|
|
2
|
+
const follow = null;
|
|
3
|
+
const stage = document.querySelector(".td-stage");
|
|
4
|
+
const readout = document.querySelector("[data-readout]");
|
|
5
|
+
const still = matchMedia("(prefers-reduced-motion: reduce)");
|
|
6
|
+
const INDEX = (DATA.index * 180) / Math.PI;
|
|
7
|
+
let angle = 0;
|
|
8
|
+
let target = 0;
|
|
9
|
+
let frame = 0;
|
|
10
|
+
let last = 0;
|
|
11
|
+
let visible = false;
|
|
12
|
+
let touring = !still.matches;
|
|
13
|
+
let idle = 0;
|
|
14
|
+
let drawn = null;
|
|
15
|
+
let clock = 0;
|
|
16
|
+
let lift = 0;
|
|
17
|
+
let broken = 0;
|
|
18
|
+
let breaking = null;
|
|
19
|
+
let podHook;
|
|
20
|
+
const SPEED = 18;
|
|
21
|
+
const readingOf = (turn) => ((((INDEX + 90 - turn) / 3.6) % 100) + 100) % 100;
|
|
22
|
+
const describe = () => {
|
|
23
|
+
const reading = Math.round(readingOf(target)) % 100;
|
|
24
|
+
stage.setAttribute("aria-valuenow", String(reading));
|
|
25
|
+
stage.setAttribute("aria-valuetext", `dial at ${reading}${broken ? ", the pod has broken off" : ""}`);
|
|
26
|
+
};
|
|
27
|
+
const hold = () => {
|
|
28
|
+
touring = false;
|
|
29
|
+
clearTimeout(idle);
|
|
30
|
+
idle = setTimeout(() => {
|
|
31
|
+
touring = !still.matches;
|
|
32
|
+
run();
|
|
33
|
+
}, 3600);
|
|
34
|
+
};
|
|
35
|
+
let own = false;
|
|
36
|
+
const show = (value) => {
|
|
37
|
+
const text = `dial ${Math.round(readingOf(value)) % 100} · ${(((value % 360) + 360) % 360).toFixed(1)}°${broken ? " · pod off" : ""}`;
|
|
38
|
+
if (readout.textContent !== text) readout.textContent = text;
|
|
39
|
+
};
|
|
40
|
+
const podAt = () => {
|
|
41
|
+
if (!breaking && podHook !== undefined) return podHook;
|
|
42
|
+
if (!breaking) return broken ? breakAt(DATA.breakEnd) : null;
|
|
43
|
+
const t = Math.min(DATA.breakEnd, breaking.time);
|
|
44
|
+
return breakAt(breaking.reverse ? DATA.breakEnd - t : t);
|
|
45
|
+
};
|
|
46
|
+
let paint = () => {};
|
|
47
|
+
const controller = TURN.mount(stage, DATA.turn, {
|
|
48
|
+
follow,
|
|
49
|
+
onHold: () => {
|
|
50
|
+
touring = false;
|
|
51
|
+
clearTimeout(idle);
|
|
52
|
+
},
|
|
53
|
+
onFrame: (turned) => {
|
|
54
|
+
const value = turned.values.dial;
|
|
55
|
+
if (!own) {
|
|
56
|
+
angle = target = drawn = value;
|
|
57
|
+
lift = turned.values.lift ?? lift;
|
|
58
|
+
if ("pod" in turned.values) podHook = turned.values.pod;
|
|
59
|
+
describe();
|
|
60
|
+
}
|
|
61
|
+
show(value);
|
|
62
|
+
paint();
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const GLD = DATA.gl;
|
|
66
|
+
const LIGHT = (stage.closest("[data-theme]")?.getAttribute("data-theme") ?? "dark") !== "dark" ? 1 : 0;
|
|
67
|
+
const INK = `
|
|
68
|
+
uniform float uLight;
|
|
69
|
+
vec4 over(vec4 top,vec4 under){return top+under*(1.-top.a);}
|
|
70
|
+
vec4 inkFor(vec3 seen,float cover){vec3 absorbed=1.-seen;float a=max(max(absorbed.r,max(absorbed.g,absorbed.b)),cover);return vec4(vec3(a)-absorbed,a);}
|
|
71
|
+
vec4 finish(vec4 ink,vec2 frag){ink.rgb=min(ink.rgb,vec3(ink.a));float n=(hash12(frag)-.5)/255.*smoothstep(0.,1.5/255.,ink.a);return clamp(ink+vec4(n),0.,1.);}
|
|
72
|
+
`;
|
|
73
|
+
const BACK = `${glslPose("Dial")}${INK}
|
|
74
|
+
uniform vec3 uLed;
|
|
75
|
+
uniform vec3 uLedDir;
|
|
76
|
+
uniform float uOn;
|
|
77
|
+
const float FACE_Z=${GLD.faceZ.toFixed(3)};
|
|
78
|
+
const float FACE_R=${GLD.faceR.toFixed(3)};
|
|
79
|
+
const float HUB_R=${GLD.hubR.toFixed(3)};
|
|
80
|
+
void main(){
|
|
81
|
+
vec2 vb=viewOf(gl_FragCoord.xy);
|
|
82
|
+
vec3 p=onFloor(vb,FACE_Z);
|
|
83
|
+
vec3 q=toDial(p);
|
|
84
|
+
float r=length(q.xy);
|
|
85
|
+
float face=(1.-smoothstep(FACE_R-3.,FACE_R-1.,r))*smoothstep(HUB_R+.2,HUB_R+1.6,r);
|
|
86
|
+
vec3 v=p-uLed;
|
|
87
|
+
float d2=dot(v,v);
|
|
88
|
+
float d=sqrt(d2);
|
|
89
|
+
float lens=.3+.7*pow(max(0.,dot(v/d,uLedDir)),1.5);
|
|
90
|
+
float lambert=max(0.,(uLed.z-FACE_Z+.6)/d);
|
|
91
|
+
float grain=.84+.16*noise2(vec2(atan(q.y,q.x)*180.,r*.6));
|
|
92
|
+
float e=420.*lens*lambert/(d2+10.)*grain*face;
|
|
93
|
+
float lit=1.-exp(-e);
|
|
94
|
+
vec3 tint=vec3(.40,.84,.60);
|
|
95
|
+
vec4 light=inkFor(mix(vec3(1.),tint,.7*lit),0.);
|
|
96
|
+
vec3 glow=vec3(.25,1.,.58)*lit*.55;
|
|
97
|
+
vec4 dark=vec4(glow,max(glow.r,max(glow.g,glow.b)));
|
|
98
|
+
gl_FragColor=finish(mix(dark,light,uLight)*uOn,gl_FragCoord.xy);
|
|
99
|
+
}`;
|
|
100
|
+
const FRONT = `${GLSL_TURN}${INK}
|
|
101
|
+
uniform vec2 uLedVb;
|
|
102
|
+
uniform float uLedR;
|
|
103
|
+
uniform float uLedFace;
|
|
104
|
+
uniform float uOn;
|
|
105
|
+
uniform vec4 uSpark[24];
|
|
106
|
+
uniform float uSparkHeat[24];
|
|
107
|
+
vec4 sparkInk(float heat,float along,float young){
|
|
108
|
+
vec3 tail=mix(vec3(.78,.2,.04),vec3(1.,.55,.08),smoothstep(.35,.8,heat));
|
|
109
|
+
vec3 head=mix(tail,vec3(1.,.74,.2),smoothstep(.55,.9,heat)*smoothstep(.5,1.,along));
|
|
110
|
+
vec3 seen=mix(head,vec3(1.,.95,.82),young*smoothstep(.8,1.,along));
|
|
111
|
+
float a=.88*smoothstep(0.,.3,along)*smoothstep(.12,.45,heat);
|
|
112
|
+
vec4 light=vec4(seen*a,a);
|
|
113
|
+
vec3 hot=mix(vec3(1.,.35,.05),vec3(1.,.9,.6),young*along)*(.4+.6*heat);
|
|
114
|
+
vec4 dark=vec4(hot*a,a);
|
|
115
|
+
return mix(dark,light,uLight);
|
|
116
|
+
}
|
|
117
|
+
void main(){
|
|
118
|
+
vec2 vb=viewOf(gl_FragCoord.xy);
|
|
119
|
+
float px=uView.x/uCanvas.x;
|
|
120
|
+
float soft=.7*px;
|
|
121
|
+
float d=length(vb-uLedVb);
|
|
122
|
+
float body=1.-smoothstep(uLedR-.6*px,uLedR+.6*px,d);
|
|
123
|
+
float core=1.-smoothstep(uLedR*.32,uLedR*.55+.5*px,d);
|
|
124
|
+
float rim=smoothstep(uLedR*.55,uLedR,d);
|
|
125
|
+
vec3 seen=mix(mix(vec3(.18,.70,.43),vec3(.11,.52,.31),rim),vec3(.90,1.,.94),core);
|
|
126
|
+
vec4 lightLed=inkFor(seen,.92)*body;
|
|
127
|
+
vec3 hot=mix(vec3(.2,1.,.55),vec3(.92,1.,.95),core)*body;
|
|
128
|
+
vec4 darkLed=vec4(hot,body);
|
|
129
|
+
vec4 led=mix(darkLed,lightLed,uLight)*smoothstep(.02,.3,uLedFace)*uOn;
|
|
130
|
+
vec4 sparks=vec4(0.);
|
|
131
|
+
float w=max(.35+.6*px,1.25*px);
|
|
132
|
+
for(int i=0;i<24;i++){
|
|
133
|
+
float heat=uSparkHeat[i];
|
|
134
|
+
if(heat<=0.)continue;
|
|
135
|
+
vec4 s=uSpark[i];
|
|
136
|
+
vec2 a=s.zw;
|
|
137
|
+
vec2 b=s.xy;
|
|
138
|
+
vec2 ab=b-a;
|
|
139
|
+
float l2=max(dot(ab,ab),1e-6);
|
|
140
|
+
float t=clamp(dot(vb-a,ab)/l2,0.,1.);
|
|
141
|
+
float dist=length(vb-a-ab*t);
|
|
142
|
+
float cover=1.-smoothstep(w*.5-.5*px,w*.5+.5*px,dist);
|
|
143
|
+
if(cover<=0.)continue;
|
|
144
|
+
sparks=over(sparkInk(heat,t,step(.65,heat))*cover,sparks);
|
|
145
|
+
}
|
|
146
|
+
vec4 ink=over(sparks,led)*(1.-coverOf(vb,soft));
|
|
147
|
+
gl_FragColor=finish(ink,gl_FragCoord.xy);
|
|
148
|
+
}`;
|
|
149
|
+
const canvasOf = (name) => stage.querySelector(`[data-canvas="${name}"]`);
|
|
150
|
+
const view = [GLD.W, GLD.H];
|
|
151
|
+
const backLayer = glLayer({ canvas: canvasOf("back"), fragment: BACK, view, camera: GLD.camera, uniforms: ["uLed", "uLedDir", "uOn", "uDialX", "uDialY", "uDialZ", "uDialT"], fixed: { uLight: LIGHT } });
|
|
152
|
+
const frontLayer = backLayer ? glLayer({ canvas: canvasOf("front"), fragment: FRONT, view, camera: GLD.camera, uniforms: ["uLedVb", "uLedR", "uLedFace", "uOn", "uSpark", "uSparkHeat", "uCoverEdge", "uCoverCount"], fixed: { uLight: LIGHT } }) : null;
|
|
153
|
+
const roomy = frontLayer ? frontLayer.gl.getParameter(frontLayer.gl.MAX_FRAGMENT_UNIFORM_VECTORS) >= 128 : false;
|
|
154
|
+
if (backLayer && frontLayer && roomy) stage.setAttribute("data-gl", "");
|
|
155
|
+
const SPARKS = 24;
|
|
156
|
+
const sparkData = new Float32Array(SPARKS * 4);
|
|
157
|
+
const sparkHeat = new Float32Array(SPARKS);
|
|
158
|
+
const sparks = [];
|
|
159
|
+
let first = { x: 0, v: 0 };
|
|
160
|
+
let glClock = 0;
|
|
161
|
+
let seed = 7;
|
|
162
|
+
const random = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
|
|
163
|
+
const ignite = () => {
|
|
164
|
+
const at = controller.anchor("dial", GLD.socket).world;
|
|
165
|
+
const spin = ((angle - (drawn ?? angle)) * Math.PI) / 180;
|
|
166
|
+
void spin;
|
|
167
|
+
for (let i = 0; i < SPARKS; i++) {
|
|
168
|
+
const a = random() * Math.PI * 2;
|
|
169
|
+
const tilt = 0.35 + random() * 0.9;
|
|
170
|
+
const speed = 16 + random() * 22;
|
|
171
|
+
sparks[i] = { p: at.slice(), v: [Math.cos(a) * Math.sin(tilt) * speed, Math.sin(a) * Math.sin(tilt) * speed, Math.cos(tilt) * speed], age: -random() * 0.08, life: 0.3 + random() * 0.45 };
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const stepSparks = (dt) => {
|
|
175
|
+
let live = false;
|
|
176
|
+
for (let i = 0; i < SPARKS; i++) {
|
|
177
|
+
const s = sparks[i];
|
|
178
|
+
sparkHeat[i] = 0;
|
|
179
|
+
if (!s) continue;
|
|
180
|
+
s.age += dt;
|
|
181
|
+
if (s.age < 0) {
|
|
182
|
+
live = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (s.age > s.life) {
|
|
186
|
+
sparks[i] = null;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
s.v[2] -= 98 * dt;
|
|
190
|
+
for (let c = 0; c < 3; c++) s.p[c] += s.v[c] * dt;
|
|
191
|
+
if (s.p[2] < GLD.faceZ) {
|
|
192
|
+
sparks[i] = null;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
live = true;
|
|
196
|
+
}
|
|
197
|
+
return live;
|
|
198
|
+
};
|
|
199
|
+
const writeSparks = () => {
|
|
200
|
+
const w = controller.cams().world;
|
|
201
|
+
const project = (p) => [w.ox + w.m[0] * p[0] + w.m[1] * p[1] + w.m[2] * p[2], w.oy + w.m[3] * p[0] + w.m[4] * p[1] + w.m[5] * p[2]];
|
|
202
|
+
for (let i = 0; i < SPARKS; i++) {
|
|
203
|
+
const s = sparks[i];
|
|
204
|
+
if (!s || s.age < 0) {
|
|
205
|
+
sparkHeat[i] = 0;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const head = project(s.p);
|
|
209
|
+
const tail = project([s.p[0] - s.v[0] * 0.05, s.p[1] - s.v[1] * 0.05, s.p[2] - s.v[2] * 0.05]);
|
|
210
|
+
sparkData.set([head[0], head[1], tail[0], tail[1]], 4 * i);
|
|
211
|
+
sparkHeat[i] = 1 - s.age / s.life;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
paint = () => {
|
|
215
|
+
if (!stage.hasAttribute("data-gl")) return;
|
|
216
|
+
const led = controller.anchor(GLD.led.group, GLD.led.at, GLD.led.normal);
|
|
217
|
+
const world = controller.cams().world;
|
|
218
|
+
const facing = led.direction[0] * world.V[0] + led.direction[1] * world.V[1] + led.direction[2] * world.V[2];
|
|
219
|
+
const on = first.x;
|
|
220
|
+
backLayer.draw({ uLed: led.world, uLedDir: led.direction, uOn: on, ...TURN.poseUniforms(controller, "dial", "uDial") }, glClock);
|
|
221
|
+
writeSparks();
|
|
222
|
+
const live = sparks.some(Boolean);
|
|
223
|
+
const cover = TURN.cover(controller, live ? [[GLD.led.part], ["hand.pod.socket"]] : [GLD.led.part]);
|
|
224
|
+
frontLayer.draw({ uLedVb: led.screen, uLedR: 0.52 * world.k, uLedFace: facing, uOn: on, uSpark: sparkData, uSparkHeat: sparkHeat, uCoverEdge: cover.uCoverEdge, uCoverCount: cover.uCoverCount }, glClock);
|
|
225
|
+
const u = on;
|
|
226
|
+
const alpha = 0.92;
|
|
227
|
+
stage.style.setProperty("--td-fallback", String(Math.max(0, (1 - u) / (1 - u * alpha + 1e-6))));
|
|
228
|
+
};
|
|
229
|
+
const draw = () => {
|
|
230
|
+
own = true;
|
|
231
|
+
controller.set({ dial: angle, lift, pod: podAt() });
|
|
232
|
+
own = false;
|
|
233
|
+
drawn = angle;
|
|
234
|
+
};
|
|
235
|
+
function tick(now) {
|
|
236
|
+
const dt = last ? Math.min((now - last) / 1000, 1 / 30) : 1 / 60;
|
|
237
|
+
last = now;
|
|
238
|
+
const calm = still.matches;
|
|
239
|
+
if (calm) {
|
|
240
|
+
touring = false;
|
|
241
|
+
clearTimeout(idle);
|
|
242
|
+
}
|
|
243
|
+
if (touring) {
|
|
244
|
+
target += SPEED * dt;
|
|
245
|
+
clock += dt;
|
|
246
|
+
}
|
|
247
|
+
angle = calm ? target : angle + (target - angle) * (1 - Math.exp(-dt / 0.12));
|
|
248
|
+
if (Math.abs(target - angle) < 1e-3) angle = target;
|
|
249
|
+
const nextLift = calm ? 0 : touring ? lift + (tapAt(clock) - lift) * (1 - Math.exp(-dt / 0.15)) : lift;
|
|
250
|
+
let moved = Math.abs(nextLift - lift) > 1e-5;
|
|
251
|
+
lift = nextLift;
|
|
252
|
+
glClock += dt;
|
|
253
|
+
const before = first.x;
|
|
254
|
+
const lighting = stage.hasAttribute("data-gl") && settle(first, 1, dt, 0.35, calm);
|
|
255
|
+
if (first.x !== before) moved = true;
|
|
256
|
+
const sparking = stage.hasAttribute("data-gl") && stepSparks(calm ? 1 : dt);
|
|
257
|
+
if (lighting || sparking) moved = true;
|
|
258
|
+
if (breaking) {
|
|
259
|
+
if (!breaking.reverse && breaking.time === 0 && !calm) ignite();
|
|
260
|
+
breaking.time += calm ? DATA.breakEnd : dt;
|
|
261
|
+
moved = true;
|
|
262
|
+
if (breaking.time >= DATA.breakEnd) {
|
|
263
|
+
broken = breaking.reverse ? 0 : 1;
|
|
264
|
+
breaking = null;
|
|
265
|
+
describe();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (moved || drawn === null || Math.abs(angle - drawn) > 1e-4) draw();
|
|
269
|
+
const settled = !touring && angle === target && !breaking && !lighting && !sparking;
|
|
270
|
+
frame = visible && !settled ? requestAnimationFrame(tick) : 0;
|
|
271
|
+
}
|
|
272
|
+
const run = () => {
|
|
273
|
+
if (!frame && visible) {
|
|
274
|
+
last = 0;
|
|
275
|
+
frame = requestAnimationFrame(tick);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
new IntersectionObserver(
|
|
279
|
+
(entries) => {
|
|
280
|
+
visible = entries[entries.length - 1].isIntersecting;
|
|
281
|
+
if (visible) run();
|
|
282
|
+
else if (frame) {
|
|
283
|
+
cancelAnimationFrame(frame);
|
|
284
|
+
frame = 0;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
{ rootMargin: "120px 0px" },
|
|
288
|
+
).observe(stage);
|
|
289
|
+
let drag = null;
|
|
290
|
+
stage.addEventListener("pointerdown", (event) => {
|
|
291
|
+
hold();
|
|
292
|
+
drag = { from: controller.angleAt(event.clientX, event.clientY, 17.2, "dial"), base: target, id: event.pointerId, captured: false };
|
|
293
|
+
});
|
|
294
|
+
stage.addEventListener("pointermove", (event) => {
|
|
295
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
296
|
+
hold();
|
|
297
|
+
const now = controller.angleAt(event.clientX, event.clientY, 17.2, "dial");
|
|
298
|
+
const delta = now - drag.from;
|
|
299
|
+
if (!drag.captured && Math.abs(delta) > 1.5) {
|
|
300
|
+
drag.captured = true;
|
|
301
|
+
stage.setPointerCapture(event.pointerId);
|
|
302
|
+
stage.setAttribute("data-drag", "");
|
|
303
|
+
}
|
|
304
|
+
if (!drag.captured) return;
|
|
305
|
+
target = drag.base + delta;
|
|
306
|
+
describe();
|
|
307
|
+
run();
|
|
308
|
+
});
|
|
309
|
+
const release = (event) => {
|
|
310
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
311
|
+
drag = null;
|
|
312
|
+
stage.removeAttribute("data-drag");
|
|
313
|
+
try {
|
|
314
|
+
stage.releasePointerCapture(event.pointerId);
|
|
315
|
+
} catch {}
|
|
316
|
+
};
|
|
317
|
+
stage.addEventListener("pointerup", release);
|
|
318
|
+
stage.addEventListener("pointercancel", release);
|
|
319
|
+
stage.addEventListener("keydown", (event) => {
|
|
320
|
+
if (event.key === "b" || event.key === "B") {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
podHook = undefined;
|
|
323
|
+
if (breaking) breaking = { reverse: !breaking.reverse, time: DATA.breakEnd - breaking.time };
|
|
324
|
+
else breaking = { reverse: Boolean(broken), time: 0 };
|
|
325
|
+
run();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const step = event.shiftKey ? 36 : 3.6;
|
|
329
|
+
let next = null;
|
|
330
|
+
if (event.key === "ArrowRight" || event.key === "ArrowUp") next = Math.round(target / 3.6) * 3.6 - step;
|
|
331
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowDown") next = Math.round(target / 3.6) * 3.6 + step;
|
|
332
|
+
if (event.key === "Home") next = INDEX + 90 - 360 * Math.round((INDEX + 90 - target) / 360);
|
|
333
|
+
if (next === null) return;
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
hold();
|
|
336
|
+
target = next;
|
|
337
|
+
describe();
|
|
338
|
+
run();
|
|
339
|
+
});
|
|
340
|
+
if (still.matches) touring = false;
|
|
341
|
+
draw();
|
|
342
|
+
describe();
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { 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 { tubePieces, tubeSvg } from "../../kit/tube.mjs";
|
|
8
|
+
import { turning } from "../../kit/turn-build.mjs";
|
|
9
|
+
import * as TA from "../../kit/turn-audit.mjs";
|
|
10
|
+
import { turnScript } from "../../scripts/inline-kit.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
|
+
const WIDTH = 760;
|
|
16
|
+
const HEIGHT = 560;
|
|
17
|
+
const BENCH = { x: -120, y: -105, w: 240, d: 210, r: 6 };
|
|
18
|
+
const BENCH_TOP = -20;
|
|
19
|
+
|
|
20
|
+
export const P = k.fitProjection([...k.boxCorners(BENCH, BENCH_TOP - 6, BENCH_TOP), [0, 0, 30]], WIDTH, HEIGHT, { pad: 20, azimuth: 40, elevation: 31 });
|
|
21
|
+
export const R = A.recorder(P);
|
|
22
|
+
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) }))) });
|
|
23
|
+
const S = (paths, style) => k.solidSvg(paths, style);
|
|
24
|
+
const L = (d, style) => k.lineSvg(d, style);
|
|
25
|
+
const VERT = (x, y) => ({ o: [x, y, 0], a: [0, 0, 1], u: [1, 0, 0], v: [0, 1, 0] });
|
|
26
|
+
const mat = (name, svg) => `<g data-mat="${name}">${svg}</g>`;
|
|
27
|
+
|
|
28
|
+
function workshop() {
|
|
29
|
+
put({ name: "stand.bench", layer: "back", at: [0, 0, BENCH_TOP - 6], bias: 8, svg: S(k.slabOf(BENCH, BENCH_TOP - 6, 6, P, 8, 1.2), { tone: "lo", inner: L(k.sideSeam(BENCH, BENCH_TOP - 2.4, P, 8), { tone: "faint" }) }), shapes: [A.slab(BENCH, BENCH_TOP - 6, 6, { ground: true })] });
|
|
30
|
+
put({ name: "stand.base.flange", layer: "back", at: [0, 0, BENCH_TOP], svg: mat("gunmetal", S(k.cylinder(0, 0, 48, BENCH_TOP, 2, P, 96), { tone: "mid" })), shapes: [A.cylinder(0, 0, 48, BENCH_TOP, 2)] });
|
|
31
|
+
put({ name: "stand.base", layer: "back", at: [0, 0, BENCH_TOP + 2], svg: mat("gunmetal", S(k.cylinder(0, 0, 44, BENCH_TOP + 2, 14, P, 96), { tone: "mid", inner: L(G.arcOf(VERT(0, 0), BENCH_TOP + 9, 44, P, { least: 0 }), { tone: "faint" }) })), shapes: [A.cylinder(0, 0, 44, BENCH_TOP + 2, 14)] });
|
|
32
|
+
const unit = { x: -104, y: -62, w: 34, d: 26, r: 2 };
|
|
33
|
+
const vents = Array.from({ length: 9 }, (_, i) => k.pathOf([k.iso([unit.x + 4 + i * 3.2, unit.y, BENCH_TOP + 5], P), k.iso([unit.x + 4 + i * 3.2, unit.y, BENCH_TOP + 15], P)])).join("");
|
|
34
|
+
put({ name: "stand.power", layer: "back", at: [unit.x + 17, unit.y + 13, BENCH_TOP], svg: mat("gunmetal", S(k.slabOf(unit, BENCH_TOP, 20, P, 4, 0.8), { tone: "hi", inner: L(vents, { tone: "faint" }) })), shapes: [A.slab(unit, BENCH_TOP, 20)] });
|
|
35
|
+
for (const [i, x] of [-98, -90, -82].entries()) put({ name: `stand.power.knob.${i}`, layer: "back", at: [x, unit.y + unit.d + 0.6, BENCH_TOP + 22], svg: mat("brass", S(k.cylinder(x, unit.y + 8 + i * 5, 1.6, BENCH_TOP + 20, 1.6, P, 24), { tone: "hi" })), shapes: [A.cylinder(x, unit.y + 8 + i * 5, 1.6, BENCH_TOP + 20, 1.6)] });
|
|
36
|
+
const rack = { x: -60, y: -100, w: 70, d: 10, r: 1 };
|
|
37
|
+
put({ name: "stand.rack", layer: "back", at: [rack.x + 35, rack.y + 5, BENCH_TOP], svg: mat("steel", S(k.slabOf(rack, BENCH_TOP, 26, P, 2, 0.6), { tone: "mid" })), shapes: [A.slab(rack, BENCH_TOP, 26)] });
|
|
38
|
+
for (let i = 0; i < 7; i++) {
|
|
39
|
+
const x = rack.x + 6 + i * 9.4;
|
|
40
|
+
const ring = [[x - 1, rack.y + rack.d], [x + 1, rack.y + rack.d], [x + 1, rack.y + rack.d + 1.4], [x - 1, rack.y + rack.d + 1.4]];
|
|
41
|
+
put({ name: `stand.rack.tool.${i}`, layer: "back", at: [x, rack.y + rack.d, BENCH_TOP + 8], svg: mat(i % 2 ? "chrome" : "steel", S(k.extrude(ring, BENCH_TOP + 6 + (i % 3) * 2, 16, P), { tone: "hi" })), shapes: [A.extruded(ring, BENCH_TOP + 6 + (i % 3) * 2, 16)] });
|
|
42
|
+
}
|
|
43
|
+
const reel = [-88, 40];
|
|
44
|
+
put({ name: "stand.reel", layer: "back", at: [reel[0], reel[1], BENCH_TOP], svg: mat("copper", S(k.cylinder(reel[0], reel[1], 9, BENCH_TOP, 4, P, 64), { tone: "hi" })), shapes: [A.cylinder(reel[0], reel[1], 9, BENCH_TOP, 4)] });
|
|
45
|
+
const tray = { x: 62, y: 46, w: 36, d: 22, r: 2 };
|
|
46
|
+
put({ name: "stand.tray", layer: "back", at: [tray.x + 18, tray.y + 11, BENCH_TOP], svg: mat("steel", S(k.slabOf(tray, BENCH_TOP, 3, P, 4, 0.5), { tone: "mid" })), shapes: [A.slab(tray, BENCH_TOP, 3)] });
|
|
47
|
+
const wrench = [[70, -70], [96, -64], [97, -61], [71, -67]];
|
|
48
|
+
put({ name: "stand.wrench", layer: "back", at: [84, -66, BENCH_TOP], svg: mat("chrome", S(k.extrude(wrench, BENCH_TOP, 0.9, P), { tone: "hi" })), shapes: [A.extruded(wrench, BENCH_TOP, 0.9)] });
|
|
49
|
+
const cables = [
|
|
50
|
+
{ name: "stand.feed", r: 1.1, route: G.fillet([[unit.x + unit.w, unit.y + 8, BENCH_TOP + 1.1], [-60, unit.y + 8, BENCH_TOP + 1.1], [-46, -30, BENCH_TOP + 1.1], [-44 * Math.cos(0.6), -44 * Math.sin(0.6), BENCH_TOP + 1.1]], 9, 12), hue: "red" },
|
|
51
|
+
{ name: "stand.feed.2", r: 0.8, route: G.fillet([[unit.x + unit.w, unit.y + 16, BENCH_TOP + 0.8], [-62, unit.y + 16, BENCH_TOP + 0.8], [-50, -24, BENCH_TOP + 0.8], [-44 * Math.cos(0.5), -44 * Math.sin(0.5), BENCH_TOP + 0.8]], 7, 12), hue: "blue" },
|
|
52
|
+
{ name: "stand.loose", r: 0.7, route: G.fillet([[reel[0] + 9, reel[1], BENCH_TOP + 0.7], [-66, 52, BENCH_TOP + 0.7], [-60, 76, BENCH_TOP + 0.7], [-30, 84, BENCH_TOP + 0.7]], 6, 12), hue: "green" },
|
|
53
|
+
];
|
|
54
|
+
for (const c of cables) {
|
|
55
|
+
R.route(c.name, c.route, c.r, { owner: c.name, limp: true, ends: [true, c.name !== "stand.loose"], free: [false, c.name === "stand.loose"] });
|
|
56
|
+
tubePieces(c.route, c.r, P, { maxLength: 30, spacing: 0.9 }).forEach((piece, index) => put({ name: `${c.name}:${index}`, layer: "back", route: c.name, chunk: index, at: piece.mid, svg: `<g data-mat="rubber">${tubeSvg(piece, { tone: "hi" }).replace('class="tb"', `class="tb" data-hue="${c.hue}"`)}</g>`, shapes: [A.tube(piece.points, piece.radii, { name: `${c.name}:${index}`, owner: c.name })] }));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const T = turning(P, { recorder: R });
|
|
61
|
+
const table = T.group("table", { turn: { pivot: [0, 0] } });
|
|
62
|
+
const W0 = [-30, 0, 12.5];
|
|
63
|
+
const wrist = T.group("wrist", { parent: table, hinge: { point: W0, axis: [0, 1, 0], range: [-3, 3] } });
|
|
64
|
+
const thumbBase = T.group("thumb", { parent: wrist, hinge: { point: [-8, -16, 0], axis: [0, 0, 1], range: [-10, 10] } });
|
|
65
|
+
const hand = T.layer("hand");
|
|
66
|
+
T.stack(["back", "hand", "front"]);
|
|
67
|
+
|
|
68
|
+
const groupsOf = [];
|
|
69
|
+
const H = (group) => ({ group, layer: hand });
|
|
70
|
+
const prismAt = (name, group, rect, z0, z1, extra = {}) => T.prism({ name, ...H(group), plan: rect, z: z0, h: z1 - z0, steps: extra.steps ?? 2, tone: extra.tone ?? "mid", ...extra });
|
|
71
|
+
const frameOf = (o, a, u) => {
|
|
72
|
+
const axis = G.unit3(a);
|
|
73
|
+
const uu = G.unit3(G.sub3(u, G.mul3(axis, G.dot3(u, axis))));
|
|
74
|
+
return { o, a: axis, u: uu, v: G.cross3(axis, uu) };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
T.round({ name: "table.platter", ...H(table), F: VERT(0, 0), s0: -4, s1: 0, r: 50, tone: "hi", crease: "lo", material: "steel", details: { ribs: { s0: -3.4, s1: -0.6, count: 160, seams: true, tone: "lo" } } });
|
|
78
|
+
T.round({ name: "table.hub", ...H(table), F: VERT(0, 0), s0: 0, s1: 1, r: 8, tone: "mid", material: "chrome" });
|
|
79
|
+
T.round({ name: "table.foot", ...H(table), F: VERT(W0[0], W0[1]), s0: 0, s1: 1.2, r: 8, tone: "mid", material: "gunmetal" });
|
|
80
|
+
T.round({ name: "table.column", ...H(table), F: VERT(W0[0], W0[1]), s0: 1.2, s1: 9.5, r: 4, tone: "hi", material: "chrome" });
|
|
81
|
+
for (let i = 0; i < 8; i++) {
|
|
82
|
+
const t = (i / 8) * Math.PI * 2 + 0.2;
|
|
83
|
+
T.round({ name: `table.bolt.${i}`, ...H(table), F: VERT(42 * Math.cos(t), 42 * Math.sin(t)), s0: 0, s1: 0.6, r: 1.1, tone: "mid", material: "brass" });
|
|
84
|
+
}
|
|
85
|
+
prismAt("table.term.a", table, { x: -11.5, y: -4.6, w: 2, d: 9.2, r: 0.4 }, 0, 1.3, { material: "gunmetal", bevel: 0.2 });
|
|
86
|
+
prismAt("table.term.b", table, { x: -21.5, y: -4.6, w: 2, d: 9.2, r: 0.4 }, 0, 1.3, { material: "gunmetal", bevel: 0.2 });
|
|
87
|
+
const tableCables = [];
|
|
88
|
+
for (let i = 0; i < 7; i++) {
|
|
89
|
+
const y = -3.6 + i * 1.2;
|
|
90
|
+
tableCables.push(T.tube({ name: `table.cable.${i}`, ...H(table), route: [[-11.5, y, 0.5], [-19.5, y, 0.5]], r: 0.42, chunk: 6, hue: ["red", "green", "blue"][i % 3], material: "rubber", bundle: "table.loom" }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
T.round({ name: "wrist.barrel", ...H(wrist), F: frameOf([W0[0], -13.2, W0[2]], [0, 1, 0], [0, 0, 1]), s0: 0, s1: 26.4, r: 2.9, tone: "hi", material: "chrome" });
|
|
94
|
+
for (const side of [-1, 1]) T.round({ name: `wrist.cap.${side}`, ...H(wrist), F: frameOf([W0[0], side * 13.2, W0[2]], [0, side, 0], [0, 0, 1]), s0: 0, s1: 0.8, r: 3.4, tone: "mid", material: "gunmetal" });
|
|
95
|
+
const PALM = { x: -27, y: -13, w: 36, d: 26, r: 2 };
|
|
96
|
+
const PALM_TOP = 14.5;
|
|
97
|
+
prismAt("palm.plate", wrist, PALM, 11, PALM_TOP, { steps: 4, bevel: 0.6, tone: "hi", material: "gold", details: { seams: [12.6] } });
|
|
98
|
+
prismAt("palm.manifold", wrist, { x: -26.8, y: -12, w: 3, d: 24, r: 0.6 }, PALM_TOP, 17, { material: "gunmetal", bevel: 0.25 });
|
|
99
|
+
for (let i = 0; i < 6; i++) prismAt(`palm.valve.${i}`, wrist, { x: -26.4, y: -11 + i * 4, w: 2.2, d: 2.4, r: 0.4 }, 17, 18.2, { material: "brass" });
|
|
100
|
+
prismAt("palm.knuckles", wrist, { x: 4, y: -13, w: 5, d: 26, r: 0.8 }, PALM_TOP, 18, { material: "gunmetal", bevel: 0.3 });
|
|
101
|
+
for (let i = 0; i < 10; i++) T.round({ name: `palm.bolt.${i}`, ...H(wrist), F: VERT(i % 2 ? 7.6 : 5.4, -11 + Math.floor(i / 2) * 5.5), s0: 18, s1: 18.4, r: 0.45, tone: "mid", material: "steel" });
|
|
102
|
+
const FINGER_Y = [-9.6, -3.2, 3.2, 9.6];
|
|
103
|
+
for (const [i, yk] of FINGER_Y.entries()) {
|
|
104
|
+
const z = 16.5;
|
|
105
|
+
const F = frameOf([0, yk, z], [1, 0, 0], [0, 0, 1]);
|
|
106
|
+
for (const x of [-20, -12]) prismAt(`palm.ram${i}.saddle.${x}`, wrist, { x, y: yk - 1.2, w: 2, d: 2.4, r: 0.4 }, PALM_TOP, 15.1, { material: "gunmetal" });
|
|
107
|
+
T.round({ name: `palm.ram${i}.cap`, ...H(wrist), F, s0: -22.6, s1: -22, r: 1.55, tone: "mid", material: "steel" });
|
|
108
|
+
T.round({ name: `palm.ram${i}.body`, ...H(wrist), F, s0: -22, s1: -8, r: 1.4, tone: "hi", material: i % 2 ? "copper" : "gunmetal", details: { seams: [-20.5, -9.5] } });
|
|
109
|
+
T.round({ name: `palm.ram${i}.gland`, ...H(wrist), F, s0: -8, s1: -6.6, r: 1.6, tone: "mid", material: "brass" });
|
|
110
|
+
T.round({ name: `palm.ram${i}.rod`, ...H(wrist), F, s0: -6.6, s1: 1, r: 0.6, tone: "hi", material: "chrome" });
|
|
111
|
+
prismAt(`palm.ram${i}.clevis`, wrist, { x: 1, y: yk - 1, w: 2, d: 2, r: 0.3 }, 15.1, 17.9, { material: "steel" });
|
|
112
|
+
}
|
|
113
|
+
const lanes = [[-12.5, -11.6], [-7.5, -6.4, -5.3], [-1.1, 0, 1.1], [5.3, 6.4, 7.5], [11.6, 12.5]];
|
|
114
|
+
const palmCables = [];
|
|
115
|
+
lanes.forEach((lane, l) =>
|
|
116
|
+
lane.forEach((y, j) => {
|
|
117
|
+
const r = Math.abs(y) > 11 ? 0.4 : 0.45;
|
|
118
|
+
palmCables.push(T.tube({ name: `palm.cable.${l}.${j}`, ...H(wrist), route: [[-23.8, y, PALM_TOP + r + 0.05], [4, y, PALM_TOP + r + 0.05]], r, chunk: 6, hue: ["red", "green", "blue"][(l + j) % 3], material: "rubber", bundle: `palm.lane.${l}` }));
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const LINK = [10, 8, 6];
|
|
123
|
+
const RL = 1.5;
|
|
124
|
+
function digit(tag, J0, d, w, parent, ranges) {
|
|
125
|
+
const up = G.cross3(d, w);
|
|
126
|
+
const at = (base, along, side, height) => G.add3(base, G.add3(G.mul3(d, along), G.add3(G.mul3(w, side), G.mul3(up, height))));
|
|
127
|
+
const joints = [J0];
|
|
128
|
+
for (const length of LINK) joints.push(G.add3(joints[joints.length - 1], G.mul3(d, length)));
|
|
129
|
+
const groups = [];
|
|
130
|
+
let g = parent;
|
|
131
|
+
for (let i = 0; i < 3; i++) {
|
|
132
|
+
g = T.group(`${tag}.j${i}`, { parent: g, hinge: { point: joints[i], axis: w, range: ranges[i] } });
|
|
133
|
+
groups.push(g);
|
|
134
|
+
}
|
|
135
|
+
groupsOf.push(groups.map((group) => group.name));
|
|
136
|
+
const owners = [parent, groups[0], groups[1]];
|
|
137
|
+
const rect = (group, name, a0, a1, s0, s1, h0, h1, extra = {}) => {
|
|
138
|
+
const F = { o: joints[0], a: up, u: d, v: w };
|
|
139
|
+
void F;
|
|
140
|
+
const o = at(joints[0], 0, 0, 0);
|
|
141
|
+
T.prism({ name, ...H(group), F: { o, a: up, u: d, v: w }, polygon: [[a0, s0], [a1, s0], [a1, s1], [a0, s1]], s0: h0, s1: h1, tone: extra.tone ?? "mid", bevel: extra.bevel ?? 0, material: extra.material ?? "gunmetal" });
|
|
142
|
+
};
|
|
143
|
+
for (let i = 0; i < 3; i++) {
|
|
144
|
+
const J = joints[i];
|
|
145
|
+
const group = groups[i];
|
|
146
|
+
const s = LINK.slice(0, i).reduce((sum, v) => sum + v, 0);
|
|
147
|
+
const e = s + LINK[i];
|
|
148
|
+
for (const side of [-1, 1]) T.round({ name: `${tag}.k${i}${side > 0 ? "+" : "-"}`, ...H(owners[i]), F: frameOf(G.add3(J, G.mul3(w, side * RL)), G.mul3(w, side), up), s0: 0, s1: 1, r: 2.2, tone: "mid", material: "chrome", details: { dots: [{ at: [1, 0, 0], normal: G.mul3(w, side), size: 0.45 }] } });
|
|
149
|
+
T.round({ name: `${tag}.p${i}`, ...H(group), F: frameOf(at(J0, s + 1.8, 0, 0), d, up), s0: 0, s1: LINK[i] - 3.6, r: RL, ends: ["dome", "dome"], tone: "hi", crease: "lo", material: "gold", details: { seams: [0.8, LINK[i] - 4.4] } });
|
|
150
|
+
const a0 = s + 2.4;
|
|
151
|
+
const a1 = e - 2.4;
|
|
152
|
+
for (const side of [-1, 1]) {
|
|
153
|
+
rect(group, `${tag}.p${i}.plate${side > 0 ? "+" : "-"}`, a0, a1, side > 0 ? RL : -RL - 0.6, side > 0 ? RL + 0.6 : -RL, -1.2, 1.2, { material: "gold", tone: "hi", bevel: 0.15 });
|
|
154
|
+
const count = Math.max(1, Math.floor((a1 - a0 - 0.8) / 1.2));
|
|
155
|
+
for (let b = 0; b < count; b++) {
|
|
156
|
+
const along = a0 + 0.8 + ((a1 - a0 - 1.6) * (b + 0.5)) / count;
|
|
157
|
+
T.round({ name: `${tag}.p${i}.boss${side > 0 ? "+" : "-"}${b}`, ...H(group), F: frameOf(at(J0, along, side * (RL + 0.6), 0), G.mul3(w, side), up), s0: 0, s1: 0.4, r: 0.4, tone: "mid", material: "steel" });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
rect(group, `${tag}.p${i}.spine`, a0, a1, -0.9, 0.9, RL, RL + 0.8, { material: "gunmetal", bevel: 0.15 });
|
|
161
|
+
const blocks = Math.max(1, Math.floor((a1 - a0 - 1.2) / 1.6));
|
|
162
|
+
for (let b = 0; b < blocks; b++) {
|
|
163
|
+
const c = a0 + 0.6 + ((a1 - a0 - 1.2) * (b + 0.5)) / blocks;
|
|
164
|
+
rect(group, `${tag}.p${i}.block${b}`, c - 0.5, c + 0.5, -0.6, 0.6, RL + 0.8, RL + 1.5, { material: b % 2 ? "brass" : "steel" });
|
|
165
|
+
}
|
|
166
|
+
rect(group, `${tag}.p${i}.pad`, s + 2.6, e - 2.6, -0.8, 0.8, -RL - 0.6, -RL, { material: "rubber" });
|
|
167
|
+
const cableSides = i === 0 ? [-1, 1] : i === 1 ? [-1, 1] : [];
|
|
168
|
+
for (const side of cableSides) {
|
|
169
|
+
const y0 = side * 1.8;
|
|
170
|
+
rect(group, `${tag}.p${i}.clip${side > 0 ? "+" : "-"}0`, a0, a0 + 0.6, side > 0 ? RL : -RL - 0.6, side > 0 ? RL + 0.6 : -RL, 1.2, 2.0, { material: "brass" });
|
|
171
|
+
rect(group, `${tag}.p${i}.clip${side > 0 ? "+" : "-"}1`, a1 - 0.6, a1, side > 0 ? RL : -RL - 0.6, side > 0 ? RL + 0.6 : -RL, 1.2, 2.0, { material: "brass" });
|
|
172
|
+
T.tube({ name: `${tag}.p${i}.cable${side > 0 ? "+" : "-"}`, ...H(group), route: [at(J0, a0 + 0.6, y0, 1.55), at(J0, a1 - 0.6, y0, 1.55)], r: 0.35, chunk: 6, hue: side > 0 ? "red" : "blue", material: "rubber" });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return groups;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const FZ = 16.25;
|
|
179
|
+
for (const [i, yk] of FINGER_Y.entries()) digit(`f${i}`, [11.5, yk, FZ], [1, 0, 0], [0, 1, 0], wrist, [[-5, 5], [-5, 5], [-5, 5]]);
|
|
180
|
+
T.round({ name: "thumb.turret", ...H(thumbBase), F: VERT(-8, -16), s0: 11, s1: 16.5, r: 3, tone: "hi", material: "chrome" });
|
|
181
|
+
prismAt("thumb.arm", thumbBase, { x: -9.4, y: -21.6, w: 2.8, d: 2.55, r: 0.3 }, 12.5, 15, { material: "gunmetal" });
|
|
182
|
+
digit("thumb", [-8, -21.5, 13.75], [0, -1, 0], [1, 0, 0], thumbBase, [[-5, 5], [-5, 5], [-5, 5]]);
|
|
183
|
+
|
|
184
|
+
const flexAt = (time, phase) => 0.5 + 0.5 * Math.sin(time * 1.3 + phase);
|
|
185
|
+
export const follow = (values) => {
|
|
186
|
+
const out = { ...values };
|
|
187
|
+
const lift = values.lift ?? 0;
|
|
188
|
+
const t = values.time ?? 0;
|
|
189
|
+
out.wrist = 3 * Math.sin(t * 0.7) * lift;
|
|
190
|
+
out.thumb = 10 * Math.sin(t * 0.5) * lift;
|
|
191
|
+
for (let f = 0; f < 5; f++) {
|
|
192
|
+
const tag = f < 4 ? `f${f}` : "thumb";
|
|
193
|
+
const curl = (Math.sin(t * 1.3 + f * 0.9) * 0.5 + 0.5) * lift;
|
|
194
|
+
out[`${tag}.j0`] = -5 + 10 * curl;
|
|
195
|
+
out[`${tag}.j1`] = -5 + 10 * curl;
|
|
196
|
+
out[`${tag}.j2`] = -5 + 10 * curl;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
};
|
|
200
|
+
void flexAt;
|
|
201
|
+
export const poses = () => {
|
|
202
|
+
const out = [];
|
|
203
|
+
for (let i = 0; i < 720; i++) out.push(follow({ table: i * 0.5, lift: 1, time: i * 0.11 }));
|
|
204
|
+
for (let i = 0; i < 72; i++) out.push(follow({ table: i * 5, lift: 0 }));
|
|
205
|
+
return out;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
workshop();
|
|
209
|
+
const started = Date.now();
|
|
210
|
+
const out = T.build({ statics: R.items, poses, verify: argv.includes("--verify") });
|
|
211
|
+
A.settle(R, P);
|
|
212
|
+
if (argv.includes("--audit")) {
|
|
213
|
+
A.auditOrExit(R, P);
|
|
214
|
+
TA.orbitOrExit(T, R, P, {
|
|
215
|
+
sweep: { at: (x) => follow({ table: x, lift: 0 }), from: 0, to: 360, frame: 0.3 },
|
|
216
|
+
clips: [{ name: "flex", at: (t) => follow({ table: 30 + 18 * t, lift: 1, time: t }), from: 0, to: 6, frame: 1 / 30 }],
|
|
217
|
+
quick: argv.includes("--quick"),
|
|
218
|
+
only: argv.find((a) => a.startsWith("--only="))?.slice(7).split(",") ?? null,
|
|
219
|
+
log: argv.includes("--verbose") ? (text) => console.log(` … ${text}`) : null,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const layerOf = (name) => R.items.filter((item) => item.layer === name).sort((a, b) => a.key - b.key).map((item) => item.svg);
|
|
223
|
+
const BACK = [`<path class="iso-halo" d="${k.haloOf(BENCH, BENCH_TOP - 6, 6, P)}"/>`, ...layerOf("back")];
|
|
224
|
+
const backSvg = k.figureSvg({ width: WIDTH, height: HEIGHT, label: "A dense robot hand on a turntable in a workshop: a stress scene for the 3D mode.", body: BACK });
|
|
225
|
+
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>`;
|
|
226
|
+
const stage = `<div class="iso-stage iso-turn ts-stage" tabindex="0" role="img" aria-label="A dense robot hand turning on a turntable">${backSvg}${out.layerSvg("hand", { width: WIDTH, height: HEIGHT })}${staticSvg(layerOf("front"))}</div>`;
|
|
227
|
+
const LIVE = `const DATA = ${JSON.stringify(out.data)};
|
|
228
|
+
const follow = ${follow.toString()};
|
|
229
|
+
const stage = document.querySelector(".ts-stage");
|
|
230
|
+
const still = matchMedia("(prefers-reduced-motion: reduce)");
|
|
231
|
+
let touring = !still.matches;
|
|
232
|
+
let angle = 0;
|
|
233
|
+
let time = 0;
|
|
234
|
+
let last = 0;
|
|
235
|
+
let frame = 0;
|
|
236
|
+
let visible = false;
|
|
237
|
+
let slow = 0;
|
|
238
|
+
let skip = false;
|
|
239
|
+
const controller = TURN.mount(stage, DATA, { follow, onHold: () => (touring = false) });
|
|
240
|
+
function tick(now) {
|
|
241
|
+
const dt = last ? Math.min((now - last) / 1000, 1 / 30) : 1 / 60;
|
|
242
|
+
last = now;
|
|
243
|
+
if (touring) {
|
|
244
|
+
angle += 14 * dt;
|
|
245
|
+
time += dt;
|
|
246
|
+
}
|
|
247
|
+
skip = !skip;
|
|
248
|
+
if (!(slow > 14 && skip)) {
|
|
249
|
+
const t0 = performance.now();
|
|
250
|
+
controller.set({ table: angle, lift: 1, time });
|
|
251
|
+
slow = slow * 0.9 + (performance.now() - t0) * 0.1;
|
|
252
|
+
if (slow < 9) skip = false;
|
|
253
|
+
}
|
|
254
|
+
frame = visible && touring ? requestAnimationFrame(tick) : 0;
|
|
255
|
+
}
|
|
256
|
+
new IntersectionObserver((entries) => {
|
|
257
|
+
visible = entries[entries.length - 1].isIntersecting;
|
|
258
|
+
if (visible && touring && !frame) frame = requestAnimationFrame(tick);
|
|
259
|
+
}).observe(stage);
|
|
260
|
+
`;
|
|
261
|
+
const body = k.plateHtml({ fig: "Fig 3D·S", title: "Stress hand", hint: "Tour only", readout: `${out.report.parts} live parts`, caption: "A stress scene for the 3D mode: a dense hand of hinge chains on a turntable among static workshop clutter.", body: `<style>${out.css}\n.ts-stage{outline:none}</style>${stage}` });
|
|
262
|
+
const page = k.pageHtml({ title: "Stress hand", theme, body, script: turnScript() + (out.verify ? `window.__isoVerify = ${JSON.stringify(out.verify)};\n` : "") + LIVE, width: WIDTH + 40 });
|
|
263
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
264
|
+
const name = `turning-stress${out.verify ? "-verify" : ""}${theme === "light" ? "-light" : ""}.html`;
|
|
265
|
+
writeFileSync(join(HERE, name), page);
|
|
266
|
+
const routes = T.routes.length;
|
|
267
|
+
console.log(`wrote ${name} · ${(page.length / 1024).toFixed(0)} KB · ${out.report.parts} parts · ${routes} routes · ${T.groups.length} groups · ${out.report.layers.map((l) => `${l.name}: ${l.planar} planes, ${l.dynamic} dynamic`).join(" · ")} · ${out.report.proofs.length} layer proofs · data ${(JSON.stringify(out.data).length / 1024).toFixed(0)} KB · build ${((Date.now() - started) / 1000).toFixed(1)} s`);
|
|
268
|
+
}
|
|
269
|
+
export { out };
|