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.
Files changed (137) hide show
  1. package/README.md +5 -4
  2. package/bin/axstack.js +4 -18
  3. package/docs/getting-started.md +6 -6
  4. package/docs/host-operations.md +9 -5
  5. package/docs/installation.md +20 -41
  6. package/docs/workflows.md +12 -9
  7. package/package.json +1 -1
  8. package/profiles/presets/claude-only.json +3 -3
  9. package/profiles/presets/codex-only.json +3 -3
  10. package/profiles/presets/mixed.json +5 -4
  11. package/skills/axstack/references/automations.md +7 -2
  12. package/skills/axstack/references/lifecycle.md +2 -1
  13. package/skills/axstack/references/review-manager-prompt.md +44 -3
  14. package/skills/axstack/references/role-roster.md +13 -6
  15. package/skills/axstack/references/routing.md +2 -1
  16. package/skills/axstack/references/run-record.md +2 -0
  17. package/skills/axstack/references/ui-verification.md +6 -10
  18. package/skills/axstack-anatomy/ANATOMY.md +172 -0
  19. package/skills/axstack-anatomy/CHANGELOG.md +70 -0
  20. package/skills/axstack-anatomy/LICENSE +21 -0
  21. package/skills/axstack-anatomy/PROVENANCE.md +21 -0
  22. package/skills/axstack-anatomy/README.md +316 -0
  23. package/skills/axstack-anatomy/SKILL.md +74 -0
  24. package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
  25. package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
  26. package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
  27. package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
  28. package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
  29. package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
  30. package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
  31. package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
  32. package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
  33. package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
  34. package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
  35. package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
  36. package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
  37. package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
  38. package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
  39. package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
  40. package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
  41. package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
  42. package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
  43. package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
  44. package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
  45. package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
  46. package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
  47. package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
  48. package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
  49. package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
  50. package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
  51. package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
  52. package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
  53. package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
  54. package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
  55. package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
  56. package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
  57. package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
  58. package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
  59. package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
  60. package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
  61. package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
  62. package/skills/axstack-anatomy/kit/gl.mjs +184 -0
  63. package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
  64. package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
  65. package/skills/axstack-anatomy/kit/iso.css +106 -0
  66. package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
  67. package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
  68. package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
  69. package/skills/axstack-anatomy/kit/tube.mjs +306 -0
  70. package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
  71. package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
  72. package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
  73. package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
  74. package/skills/axstack-anatomy/references/3d.md +352 -0
  75. package/skills/axstack-anatomy/references/craft.md +325 -0
  76. package/skills/axstack-anatomy/references/kit.md +419 -0
  77. package/skills/axstack-anatomy/references/motion.md +165 -0
  78. package/skills/axstack-anatomy/references/react.md +42 -0
  79. package/skills/axstack-anatomy/references/verify.md +351 -0
  80. package/skills/axstack-anatomy/references/walkthrough.md +75 -0
  81. package/skills/axstack-anatomy/references/webgl.md +473 -0
  82. package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
  83. package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
  84. package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
  85. package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
  86. package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
  87. package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
  88. package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
  89. package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
  90. package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
  91. package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
  92. package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
  93. package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
  94. package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
  95. package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
  96. package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
  97. package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
  98. package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
  99. package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
  100. package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
  101. package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
  102. package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
  103. package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
  104. package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
  105. package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
  106. package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
  107. package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
  108. package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
  109. package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
  110. package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
  111. package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
  112. package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
  113. package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
  114. package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
  115. package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
  116. package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
  117. package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
  118. package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
  119. package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
  120. package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
  121. package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
  122. package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
  123. package/skills/axstack-anatomy/theme.js +56 -0
  124. package/skills/axstack-explain/SKILL.md +28 -15
  125. package/skills/axstack-explain/references/delivery.md +22 -0
  126. package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
  127. package/skills/axstack-explain/references/inline-pages.md +8 -8
  128. package/skills/axstack-explain/references/visual-qa.md +3 -5
  129. package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
  130. package/skills/axstack-research/SKILL.md +18 -6
  131. package/src/archify-legacy.js +70 -0
  132. package/src/capabilities.js +1 -29
  133. package/src/installer.js +11 -31
  134. package/skills/axstack-diagram/SKILL.md +0 -28
  135. package/skills/axstack-diagram/references/archify.md +0 -76
  136. package/src/archify-pin.js +0 -5
  137. 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 };