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,511 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { IsoFigure } from "../../kit/react/draw";
|
|
5
|
+
import { asksForStillness, useReadout } from "../../kit/react/live";
|
|
6
|
+
import { frameOf, type Frame } from "./pose";
|
|
7
|
+
import { HALF, REST, memberAt, one, poseOf, readoutOf, settled, sizeText, step, type Member, type Pose } from "./rig";
|
|
8
|
+
import { EX, FINGER_MOST, GRAB_MOST, HEIGHT, HOVER, PROBE_HOME, PROBE_MAX, PROBE_MIN, WIDTH, at } from "./view";
|
|
9
|
+
|
|
10
|
+
const FOLLOW = 0.09;
|
|
11
|
+
const DOWN = 0.07;
|
|
12
|
+
const UP = 0.16;
|
|
13
|
+
const NUDGE = 10;
|
|
14
|
+
const TOUR_FIRST = 1.4;
|
|
15
|
+
const TOUR_BACK = 3600;
|
|
16
|
+
const LOOP = 7.6;
|
|
17
|
+
const MOST_DT = 1 / 30;
|
|
18
|
+
|
|
19
|
+
interface Hand {
|
|
20
|
+
held: boolean;
|
|
21
|
+
grab: number;
|
|
22
|
+
finger: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const clamp = (value: number, low: number, high: number) => Math.min(high, Math.max(low, value));
|
|
26
|
+
const ease = (t: number) => {
|
|
27
|
+
const u = clamp(t, 0, 1);
|
|
28
|
+
return u * u * (3 - 2 * u);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function tour(t: number): Hand {
|
|
32
|
+
const at = t % LOOP;
|
|
33
|
+
if (at < 0.6) return { held: false, grab: PROBE_HOME, finger: PROBE_HOME };
|
|
34
|
+
if (at < 3.7) return { held: true, grab: PROBE_HOME, finger: PROBE_HOME - 80 * ease((at - 1.2) / 1.2) };
|
|
35
|
+
return { held: false, grab: PROBE_HOME, finger: PROBE_HOME - 80 + 80 * ease((at - 4.4) / 1.2) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Nodes {
|
|
39
|
+
glass: SVGGElement | null;
|
|
40
|
+
fill: SVGPathElement | null;
|
|
41
|
+
shades: (SVGPathElement | null)[];
|
|
42
|
+
top: SVGPathElement | null;
|
|
43
|
+
crease: SVGPathElement | null;
|
|
44
|
+
bevel: SVGPathElement | null;
|
|
45
|
+
edge: SVGPathElement | null;
|
|
46
|
+
rim: SVGPathElement | null;
|
|
47
|
+
shine: SVGPathElement | null;
|
|
48
|
+
coilsBack: SVGPathElement | null;
|
|
49
|
+
coilsLeft: SVGPathElement | null;
|
|
50
|
+
coilsRightBack: SVGPathElement | null;
|
|
51
|
+
coilsRightFront: SVGPathElement | null;
|
|
52
|
+
carriage: SVGGElement | null;
|
|
53
|
+
plunger: SVGGElement | null;
|
|
54
|
+
needle: SVGPathElement | null;
|
|
55
|
+
pull: SVGPathElement | null;
|
|
56
|
+
grab: SVGCircleElement | null;
|
|
57
|
+
touch: SVGCircleElement | null;
|
|
58
|
+
rider: SVGGElement | null;
|
|
59
|
+
probe: SVGGElement | null;
|
|
60
|
+
arm: SVGGElement | null;
|
|
61
|
+
tip: SVGGElement | null;
|
|
62
|
+
rest: SVGGElement | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function apply(nodes: Nodes, frame: Frame, held: boolean) {
|
|
66
|
+
const { glass } = frame;
|
|
67
|
+
nodes.fill?.setAttribute("d", glass.fill);
|
|
68
|
+
glass.shades?.forEach((d, index) => nodes.shades[index]?.setAttribute("d", d));
|
|
69
|
+
nodes.top?.setAttribute("d", glass.top);
|
|
70
|
+
nodes.crease?.setAttribute("d", glass.crease);
|
|
71
|
+
nodes.bevel?.setAttribute("d", glass.bevel ?? "");
|
|
72
|
+
nodes.edge?.setAttribute("d", glass.outline);
|
|
73
|
+
nodes.rim?.setAttribute("d", glass.rim);
|
|
74
|
+
nodes.shine?.setAttribute("d", glass.shine);
|
|
75
|
+
if (nodes.glass) {
|
|
76
|
+
if (held) nodes.glass.dataset.lit = "";
|
|
77
|
+
else delete nodes.glass.dataset.lit;
|
|
78
|
+
}
|
|
79
|
+
nodes.coilsBack?.setAttribute("d", frame.coilsBack);
|
|
80
|
+
nodes.coilsLeft?.setAttribute("d", frame.coilsLeft);
|
|
81
|
+
nodes.coilsRightBack?.setAttribute("d", frame.coilsRightBack);
|
|
82
|
+
nodes.coilsRightFront?.setAttribute("d", frame.coilsRightFront);
|
|
83
|
+
nodes.carriage?.setAttribute("transform", frame.carriage);
|
|
84
|
+
nodes.plunger?.setAttribute("transform", frame.carriage);
|
|
85
|
+
nodes.needle?.setAttribute("d", frame.needle);
|
|
86
|
+
nodes.pull?.setAttribute("d", frame.pull);
|
|
87
|
+
for (const [node, point] of [
|
|
88
|
+
[nodes.grab, frame.grab],
|
|
89
|
+
[nodes.touch, frame.finger],
|
|
90
|
+
] as const) {
|
|
91
|
+
if (!node) continue;
|
|
92
|
+
node.setAttribute("cx", `${point[0]}`);
|
|
93
|
+
node.setAttribute("cy", `${point[1]}`);
|
|
94
|
+
node.style.opacity = frame.pull ? "" : "0";
|
|
95
|
+
}
|
|
96
|
+
nodes.rider?.setAttribute("transform", frame.rider);
|
|
97
|
+
nodes.probe?.setAttribute("transform", frame.probe);
|
|
98
|
+
nodes.arm?.setAttribute("transform", frame.probe);
|
|
99
|
+
nodes.tip?.setAttribute("transform", frame.tip);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function restShown(pose: Pose) {
|
|
103
|
+
const off = Math.max(Math.abs(pose.mid - pose.half[0] + HALF[0]), Math.abs(pose.mid + pose.half[0] - HALF[0]), Math.abs(pose.half[1] - HALF[1]));
|
|
104
|
+
return `${Math.round(clamp((off - 0.4) / 1.6, 0, 1) * 100) / 100}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function spokenOf(member: Member, pull: number, held: boolean) {
|
|
108
|
+
const pose = poseOf(member);
|
|
109
|
+
const size = sizeText(pose).replace(" × ", " by ");
|
|
110
|
+
const state = held ? "Pressed" : member.swell.at === 0 && member.lean.at === 0 ? "At rest" : "Springing back";
|
|
111
|
+
return `${state}: ${size} pixels, pulled ${one(Math.abs(pull))} px, leaning ${one(Math.abs(pose.lean))} px`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const RESTING_TEXT = spokenOf(memberAt(), 0, false);
|
|
115
|
+
|
|
116
|
+
interface Props {
|
|
117
|
+
label: string;
|
|
118
|
+
base: React.ReactNode;
|
|
119
|
+
rail: React.ReactNode;
|
|
120
|
+
rider: React.ReactNode;
|
|
121
|
+
block: React.ReactNode;
|
|
122
|
+
frameBack: React.ReactNode;
|
|
123
|
+
rods: React.ReactNode;
|
|
124
|
+
carriage: React.ReactNode;
|
|
125
|
+
plunger: React.ReactNode;
|
|
126
|
+
rest: React.ReactNode;
|
|
127
|
+
frameFront: React.ReactNode;
|
|
128
|
+
dial: React.ReactNode;
|
|
129
|
+
finger: React.ReactNode;
|
|
130
|
+
arm: React.ReactNode;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function StretchLive(props: Props) {
|
|
134
|
+
const { anchor, say } = useReadout<HTMLDivElement>();
|
|
135
|
+
const root = React.useRef<HTMLDivElement | null>(null);
|
|
136
|
+
const nodes = React.useRef<Nodes>({
|
|
137
|
+
glass: null,
|
|
138
|
+
fill: null,
|
|
139
|
+
shades: [null, null, null, null],
|
|
140
|
+
top: null,
|
|
141
|
+
crease: null,
|
|
142
|
+
bevel: null,
|
|
143
|
+
edge: null,
|
|
144
|
+
rim: null,
|
|
145
|
+
shine: null,
|
|
146
|
+
coilsBack: null,
|
|
147
|
+
coilsLeft: null,
|
|
148
|
+
coilsRightBack: null,
|
|
149
|
+
coilsRightFront: null,
|
|
150
|
+
carriage: null,
|
|
151
|
+
plunger: null,
|
|
152
|
+
needle: null,
|
|
153
|
+
pull: null,
|
|
154
|
+
grab: null,
|
|
155
|
+
touch: null,
|
|
156
|
+
rider: null,
|
|
157
|
+
probe: null,
|
|
158
|
+
arm: null,
|
|
159
|
+
tip: null,
|
|
160
|
+
rest: null,
|
|
161
|
+
});
|
|
162
|
+
const member = React.useRef<Member>(memberAt());
|
|
163
|
+
const hand = React.useRef<Hand>({ held: false, grab: PROBE_HOME, finger: PROBE_HOME });
|
|
164
|
+
const local = React.useRef(PROBE_HOME);
|
|
165
|
+
const finger = React.useRef(PROBE_HOME);
|
|
166
|
+
const lift = React.useRef(1);
|
|
167
|
+
const frame = React.useRef(0);
|
|
168
|
+
const last = React.useRef(0);
|
|
169
|
+
const clock = React.useRef(0);
|
|
170
|
+
const script = React.useRef<((t: number) => Hand) | null>(null);
|
|
171
|
+
const [pressing, setPressing] = React.useState(false);
|
|
172
|
+
const user = React.useRef(false);
|
|
173
|
+
const [hovering, setHovering] = React.useState(false);
|
|
174
|
+
const [focused, setFocused] = React.useState(false);
|
|
175
|
+
const [visible, setVisible] = React.useState(false);
|
|
176
|
+
const [idle, setIdle] = React.useState(true);
|
|
177
|
+
const [poke, setPoke] = React.useState(0);
|
|
178
|
+
|
|
179
|
+
const rest = React.useMemo(() => frameOf(REST, 0, PROBE_HOME, PROBE_HOME, HOVER, PROBE_HOME), []);
|
|
180
|
+
|
|
181
|
+
const grip = React.useCallback((next: Hand) => {
|
|
182
|
+
const was = hand.current;
|
|
183
|
+
if (next.held && !was.held) {
|
|
184
|
+
const pose = poseOf(member.current);
|
|
185
|
+
local.current = (next.grab - pose.mid) / (pose.half[0] / HALF[0]);
|
|
186
|
+
}
|
|
187
|
+
hand.current = next;
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
const draw = React.useCallback(() => {
|
|
191
|
+
const pose = poseOf(member.current);
|
|
192
|
+
const now = hand.current;
|
|
193
|
+
apply(nodes.current, frameOf(pose, member.current.lean.at, local.current, finger.current, HOVER * lift.current, PROBE_HOME, now.held), now.held);
|
|
194
|
+
say(readoutOf(pose));
|
|
195
|
+
const restNode = nodes.current.rest;
|
|
196
|
+
const shownRest = restShown(pose);
|
|
197
|
+
if (restNode && restNode.style.opacity !== shownRest) restNode.style.opacity = shownRest;
|
|
198
|
+
const pulled = now.held ? finger.current - now.grab : 0;
|
|
199
|
+
const node = root.current;
|
|
200
|
+
if (node) {
|
|
201
|
+
const value = `${Math.round(pulled)}`;
|
|
202
|
+
if (node.getAttribute("aria-valuenow") !== value) node.setAttribute("aria-valuenow", value);
|
|
203
|
+
const spoken = spokenOf(member.current, pulled, now.held);
|
|
204
|
+
if (node.getAttribute("aria-valuetext") !== spoken) node.setAttribute("aria-valuetext", spoken);
|
|
205
|
+
}
|
|
206
|
+
}, [say]);
|
|
207
|
+
|
|
208
|
+
const stop = React.useCallback(() => {
|
|
209
|
+
cancelAnimationFrame(frame.current);
|
|
210
|
+
frame.current = 0;
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const run = React.useCallback(() => {
|
|
214
|
+
if (frame.current) return;
|
|
215
|
+
last.current = 0;
|
|
216
|
+
const tick = (now: number) => {
|
|
217
|
+
const dt = last.current ? Math.min((now - last.current) / 1000, MOST_DT) : 1 / 60;
|
|
218
|
+
last.current = now;
|
|
219
|
+
const calm = asksForStillness();
|
|
220
|
+
if (script.current) {
|
|
221
|
+
clock.current += dt;
|
|
222
|
+
grip(script.current(clock.current));
|
|
223
|
+
}
|
|
224
|
+
const want = hand.current;
|
|
225
|
+
const follow = calm ? 1 : 1 - Math.exp(-dt / FOLLOW);
|
|
226
|
+
finger.current += (want.finger - finger.current) * follow;
|
|
227
|
+
if (Math.abs(want.finger - finger.current) < 0.02) finger.current = want.finger;
|
|
228
|
+
const goal = want.held ? 0 : 1;
|
|
229
|
+
const drop = calm ? 1 : 1 - Math.exp(-dt / (want.held ? DOWN : UP));
|
|
230
|
+
lift.current += (goal - lift.current) * drop;
|
|
231
|
+
if (Math.abs(goal - lift.current) < 0.002) lift.current = goal;
|
|
232
|
+
const pulled = want.held ? finger.current - want.grab : 0;
|
|
233
|
+
step(member.current, want.held, pulled, dt, calm);
|
|
234
|
+
draw();
|
|
235
|
+
const done = !script.current && settled(member.current, want.held, pulled) && finger.current === want.finger && lift.current === goal;
|
|
236
|
+
frame.current = done ? 0 : requestAnimationFrame(tick);
|
|
237
|
+
};
|
|
238
|
+
frame.current = requestAnimationFrame(tick);
|
|
239
|
+
}, [draw, grip]);
|
|
240
|
+
|
|
241
|
+
const steer = React.useCallback(
|
|
242
|
+
(next: Hand) => {
|
|
243
|
+
script.current = null;
|
|
244
|
+
grip(next);
|
|
245
|
+
run();
|
|
246
|
+
},
|
|
247
|
+
[grip, run],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
React.useEffect(() => {
|
|
251
|
+
const node = root.current;
|
|
252
|
+
if (!node) return;
|
|
253
|
+
const observer = new IntersectionObserver((entries) => setVisible(Boolean(entries.at(-1)?.isIntersecting)));
|
|
254
|
+
observer.observe(node);
|
|
255
|
+
return () => {
|
|
256
|
+
observer.disconnect();
|
|
257
|
+
stop();
|
|
258
|
+
};
|
|
259
|
+
}, [stop]);
|
|
260
|
+
|
|
261
|
+
const engaged = hovering || focused || pressing;
|
|
262
|
+
|
|
263
|
+
React.useEffect(() => {
|
|
264
|
+
if (engaged) {
|
|
265
|
+
setIdle(false);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const timer = window.setTimeout(() => setIdle(true), TOUR_BACK);
|
|
269
|
+
return () => window.clearTimeout(timer);
|
|
270
|
+
}, [engaged, poke]);
|
|
271
|
+
|
|
272
|
+
const touring = visible && idle && !engaged;
|
|
273
|
+
|
|
274
|
+
React.useEffect(() => {
|
|
275
|
+
if (!visible) {
|
|
276
|
+
script.current = null;
|
|
277
|
+
stop();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (touring && !asksForStillness()) {
|
|
281
|
+
const timer = window.setTimeout(
|
|
282
|
+
() => {
|
|
283
|
+
clock.current = 0;
|
|
284
|
+
script.current = tour;
|
|
285
|
+
run();
|
|
286
|
+
},
|
|
287
|
+
TOUR_FIRST * 1000,
|
|
288
|
+
);
|
|
289
|
+
return () => {
|
|
290
|
+
window.clearTimeout(timer);
|
|
291
|
+
script.current = null;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
script.current = null;
|
|
295
|
+
if (user.current) {
|
|
296
|
+
run();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
steer({ held: false, grab: hand.current.grab, finger: hovering ? hand.current.finger : PROBE_HOME });
|
|
300
|
+
}, [visible, touring, hovering, run, steer, stop]);
|
|
301
|
+
|
|
302
|
+
const xAt = (clientX: number) => {
|
|
303
|
+
const svg = root.current?.querySelector("svg");
|
|
304
|
+
const box = svg?.getBoundingClientRect();
|
|
305
|
+
if (!box || !box.width) return null;
|
|
306
|
+
const sx = ((clientX - box.left) / box.width) * WIDTH;
|
|
307
|
+
const origin = at([0, 0, 0]);
|
|
308
|
+
return (sx - origin[0]) / EX[0];
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const wake = () => {
|
|
312
|
+
setIdle(false);
|
|
313
|
+
setPoke((count) => count + 1);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const hoverTo = (x: number) => steer({ held: false, grab: hand.current.grab, finger: clamp(x, PROBE_MIN, PROBE_MAX) });
|
|
317
|
+
|
|
318
|
+
const hold = (on: boolean) => {
|
|
319
|
+
user.current = on;
|
|
320
|
+
setPressing(on);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const pressAt = (x: number) => {
|
|
324
|
+
hold(true);
|
|
325
|
+
const grab = clamp(x, -GRAB_MOST, GRAB_MOST);
|
|
326
|
+
steer({ held: true, grab, finger: grab });
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const pullTo = (x: number) => steer({ held: true, grab: hand.current.grab, finger: clamp(x, -FINGER_MOST, FINGER_MOST) });
|
|
330
|
+
|
|
331
|
+
const release = () => {
|
|
332
|
+
hold(false);
|
|
333
|
+
steer({ held: false, grab: hand.current.grab, finger: hand.current.finger });
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const bind = <K extends keyof Nodes>(key: K) => (node: Nodes[K]) => {
|
|
337
|
+
nodes.current[key] = node;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const shown = rest.glass;
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<div
|
|
344
|
+
ref={(node) => {
|
|
345
|
+
root.current = node;
|
|
346
|
+
anchor.current = node;
|
|
347
|
+
}}
|
|
348
|
+
className="ms"
|
|
349
|
+
role="slider"
|
|
350
|
+
tabIndex={0}
|
|
351
|
+
aria-label="Glass button on a test rig: Space presses or lets go, arrow keys move the finger and pull"
|
|
352
|
+
aria-valuemin={-FINGER_MOST - GRAB_MOST}
|
|
353
|
+
aria-valuemax={FINGER_MOST + GRAB_MOST}
|
|
354
|
+
aria-valuenow={0}
|
|
355
|
+
aria-valuetext={RESTING_TEXT}
|
|
356
|
+
data-held={pressing ? "" : undefined}
|
|
357
|
+
onPointerEnter={(event) => {
|
|
358
|
+
if (event.pointerType === "mouse") setHovering(true);
|
|
359
|
+
}}
|
|
360
|
+
onPointerMove={(event) => {
|
|
361
|
+
const x = xAt(event.clientX);
|
|
362
|
+
if (x === null) return;
|
|
363
|
+
if (user.current && event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
364
|
+
wake();
|
|
365
|
+
pullTo(x);
|
|
366
|
+
} else if (event.pointerType === "mouse" && !user.current) {
|
|
367
|
+
wake();
|
|
368
|
+
hoverTo(x);
|
|
369
|
+
}
|
|
370
|
+
}}
|
|
371
|
+
onPointerDown={(event) => {
|
|
372
|
+
const x = xAt(event.clientX);
|
|
373
|
+
if (x === null) return;
|
|
374
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
375
|
+
wake();
|
|
376
|
+
pressAt(x);
|
|
377
|
+
}}
|
|
378
|
+
onPointerUp={(event) => {
|
|
379
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
380
|
+
if (user.current) release();
|
|
381
|
+
}}
|
|
382
|
+
onPointerCancel={() => {
|
|
383
|
+
if (user.current) release();
|
|
384
|
+
}}
|
|
385
|
+
onPointerLeave={(event) => {
|
|
386
|
+
if (event.pointerType !== "mouse" || user.current) return;
|
|
387
|
+
setHovering(false);
|
|
388
|
+
hoverTo(PROBE_HOME);
|
|
389
|
+
}}
|
|
390
|
+
onFocus={() => setFocused(true)}
|
|
391
|
+
onBlur={() => {
|
|
392
|
+
setFocused(false);
|
|
393
|
+
if (user.current) release();
|
|
394
|
+
}}
|
|
395
|
+
onKeyDown={(event) => {
|
|
396
|
+
const now = hand.current;
|
|
397
|
+
const by = event.shiftKey ? NUDGE * 3 : NUDGE;
|
|
398
|
+
if (event.key === " " || event.key === "Enter") {
|
|
399
|
+
event.preventDefault();
|
|
400
|
+
if (event.repeat) return;
|
|
401
|
+
wake();
|
|
402
|
+
if (user.current) release();
|
|
403
|
+
else pressAt(now.finger);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
let x: number | null = null;
|
|
407
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowDown") x = now.finger - by;
|
|
408
|
+
else if (event.key === "ArrowRight" || event.key === "ArrowUp") x = now.finger + by;
|
|
409
|
+
else if (event.key === "Home") x = -Infinity;
|
|
410
|
+
else if (event.key === "End") x = Infinity;
|
|
411
|
+
else if (event.key === "Escape") {
|
|
412
|
+
if (user.current) release();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (x === null) return;
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
wake();
|
|
418
|
+
if (user.current) pullTo(x);
|
|
419
|
+
else hoverTo(x);
|
|
420
|
+
}}
|
|
421
|
+
>
|
|
422
|
+
<IsoFigure width={WIDTH} height={HEIGHT} label={props.label}>
|
|
423
|
+
{props.base}
|
|
424
|
+
<g>
|
|
425
|
+
<g>
|
|
426
|
+
{props.rail}
|
|
427
|
+
<g ref={bind("rider")} transform={rest.rider}>
|
|
428
|
+
{props.rider}
|
|
429
|
+
</g>
|
|
430
|
+
<g ref={bind("probe")} transform={rest.probe}>
|
|
431
|
+
{props.block}
|
|
432
|
+
</g>
|
|
433
|
+
</g>
|
|
434
|
+
</g>
|
|
435
|
+
{props.frameBack}
|
|
436
|
+
<g>
|
|
437
|
+
<g>
|
|
438
|
+
<path ref={bind("coilsBack")} className="iso-line ms-coil" data-tone="lo" d={rest.coilsBack} />
|
|
439
|
+
{props.rods}
|
|
440
|
+
<path ref={bind("coilsLeft")} className="iso-line ms-coil" data-tone="mid" d={rest.coilsLeft} />
|
|
441
|
+
<g ref={bind("carriage")} transform={rest.carriage}>
|
|
442
|
+
{props.carriage}
|
|
443
|
+
</g>
|
|
444
|
+
</g>
|
|
445
|
+
</g>
|
|
446
|
+
<g>
|
|
447
|
+
<g ref={bind("glass")} className="iso-solid ms-glass">
|
|
448
|
+
<path ref={bind("fill")} className="iso-fill" d={shown.fill} />
|
|
449
|
+
{[0, 1, 2, 3].map((index) => (
|
|
450
|
+
<path
|
|
451
|
+
key={index}
|
|
452
|
+
ref={(node) => {
|
|
453
|
+
nodes.current.shades[index] = node;
|
|
454
|
+
}}
|
|
455
|
+
className="iso-shade"
|
|
456
|
+
data-shade={index}
|
|
457
|
+
d={shown.shades?.[index] ?? ""}
|
|
458
|
+
/>
|
|
459
|
+
))}
|
|
460
|
+
<path ref={bind("top")} className="iso-top" d={shown.top} />
|
|
461
|
+
<path ref={bind("crease")} className="iso-line" data-tone="lo" d={shown.crease} />
|
|
462
|
+
<path ref={bind("bevel")} className="iso-line iso-bevel" data-tone="lo" d={shown.bevel ?? ""} />
|
|
463
|
+
<path ref={bind("rim")} className="iso-line ms-rim" data-tone="mid" d={shown.rim} />
|
|
464
|
+
<path ref={bind("shine")} className="iso-line ms-shine" data-tone="hi" d={shown.shine} />
|
|
465
|
+
<path ref={bind("edge")} className="iso-line iso-edge" data-tone="hi" d={shown.outline} />
|
|
466
|
+
</g>
|
|
467
|
+
</g>
|
|
468
|
+
<g>
|
|
469
|
+
<g ref={bind("rest")} style={{ opacity: 0 }}>
|
|
470
|
+
{props.rest}
|
|
471
|
+
</g>
|
|
472
|
+
</g>
|
|
473
|
+
<g>
|
|
474
|
+
<g className="iso-wire ms-pull" data-tone="hi">
|
|
475
|
+
<path ref={bind("pull")} className="iso-line" data-tone="lit" d={rest.pull} />
|
|
476
|
+
<circle ref={bind("grab")} className="iso-wire-end" cx={rest.grab[0]} cy={rest.grab[1]} r={1.2} style={{ opacity: 0 }} />
|
|
477
|
+
<circle ref={bind("touch")} className="iso-wire-end" cx={rest.finger[0]} cy={rest.finger[1]} r={1.2} style={{ opacity: 0 }} />
|
|
478
|
+
</g>
|
|
479
|
+
</g>
|
|
480
|
+
<g>
|
|
481
|
+
<path ref={bind("coilsRightBack")} className="iso-line ms-coil" data-tone="mid" d={rest.coilsRightBack} />
|
|
482
|
+
</g>
|
|
483
|
+
<g>
|
|
484
|
+
<g ref={bind("plunger")} transform={rest.carriage}>
|
|
485
|
+
{props.plunger}
|
|
486
|
+
</g>
|
|
487
|
+
</g>
|
|
488
|
+
<g>
|
|
489
|
+
<path ref={bind("coilsRightFront")} className="iso-line ms-coil" data-tone="mid" d={rest.coilsRightFront} />
|
|
490
|
+
</g>
|
|
491
|
+
{props.frameFront}
|
|
492
|
+
<g>
|
|
493
|
+
<g>
|
|
494
|
+
{props.dial}
|
|
495
|
+
<path ref={bind("needle")} className="iso-line ms-needle" data-tone="hi" d={rest.needle} />
|
|
496
|
+
</g>
|
|
497
|
+
</g>
|
|
498
|
+
<g>
|
|
499
|
+
<g ref={bind("tip")} transform={rest.tip}>
|
|
500
|
+
{props.finger}
|
|
501
|
+
</g>
|
|
502
|
+
</g>
|
|
503
|
+
<g>
|
|
504
|
+
<g ref={bind("arm")} transform={rest.probe}>
|
|
505
|
+
{props.arm}
|
|
506
|
+
</g>
|
|
507
|
+
</g>
|
|
508
|
+
</IsoFigure>
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { insetPlan, onTop, pathOf, planOutline, slabOf, type Flat, type SolidPaths } from "../../kit/iso-kit";
|
|
2
|
+
import { carried, type Pose } from "./rig";
|
|
3
|
+
import { AZIMUTH, BUSH_END, COIL_R, COIL_TURNS, DIAL, DIAL_SPAN, DIAL_TOP, DIAL_R, FRAME_X, GLASS_H, GLASS_TOP, GLASS_Z, P, ROD_Y, ROD_Z, at, shift } from "./view";
|
|
4
|
+
|
|
5
|
+
export interface GlassPaths extends SolidPaths {
|
|
6
|
+
rim: string;
|
|
7
|
+
shine: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Frame {
|
|
11
|
+
glass: GlassPaths;
|
|
12
|
+
coilsBack: string;
|
|
13
|
+
coilsLeft: string;
|
|
14
|
+
coilsRightBack: string;
|
|
15
|
+
coilsRightFront: string;
|
|
16
|
+
carriage: string;
|
|
17
|
+
needle: string;
|
|
18
|
+
grab: Flat;
|
|
19
|
+
finger: Flat;
|
|
20
|
+
pull: string;
|
|
21
|
+
rider: string;
|
|
22
|
+
probe: string;
|
|
23
|
+
tip: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const RADIANS = Math.PI / 180;
|
|
27
|
+
const SIN_A = Math.sin(AZIMUTH * RADIANS);
|
|
28
|
+
const COS_E = Math.cos(30 * RADIANS);
|
|
29
|
+
const SIN_E = Math.sin(30 * RADIANS);
|
|
30
|
+
const PER_TURN = 16;
|
|
31
|
+
const tenth = (value: number) => Math.round(value * 10) / 10;
|
|
32
|
+
|
|
33
|
+
export function glassOf(pose: Pose): GlassPaths {
|
|
34
|
+
const [hx, hy] = pose.half;
|
|
35
|
+
const plan = { x: pose.mid - hx, y: -hy, w: hx * 2, d: hy * 2, r: hy };
|
|
36
|
+
const paths = slabOf(plan, GLASS_Z, GLASS_H, P, 10, 1.2);
|
|
37
|
+
const inner = insetPlan(plan, 3.2);
|
|
38
|
+
const shine = [
|
|
39
|
+
onTop([[pose.mid - hx * 0.62, -hy + 5.4], [pose.mid + hx * 0.18, -hy + 5.4]], GLASS_TOP, P),
|
|
40
|
+
onTop([[pose.mid + hx * 0.3, -hy + 5.4], [pose.mid + hx * 0.42, -hy + 5.4]], GLASS_TOP, P),
|
|
41
|
+
].join("");
|
|
42
|
+
return { ...paths, rim: planOutline(inner, GLASS_TOP, P, 10), shine };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function coil(from: number, to: number, y: number) {
|
|
46
|
+
const count = COIL_TURNS * PER_TURN;
|
|
47
|
+
const back: Flat[][] = [];
|
|
48
|
+
const front: Flat[][] = [];
|
|
49
|
+
let run: Flat[] = [];
|
|
50
|
+
let facing: boolean | null = null;
|
|
51
|
+
for (let index = 0; index <= count; index++) {
|
|
52
|
+
const share = index / count;
|
|
53
|
+
const angle = share * COIL_TURNS * Math.PI * 2;
|
|
54
|
+
const cy = Math.cos(angle);
|
|
55
|
+
const cz = Math.sin(angle);
|
|
56
|
+
const point = at([from + (to - from) * share, y + COIL_R * cy, ROD_Z + COIL_R * cz]);
|
|
57
|
+
const near = cy * SIN_A * COS_E + cz * SIN_E > 0;
|
|
58
|
+
if (facing === null) facing = near;
|
|
59
|
+
if (near !== facing) {
|
|
60
|
+
run.push(point);
|
|
61
|
+
(facing ? front : back).push(run);
|
|
62
|
+
run = [point];
|
|
63
|
+
facing = near;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
run.push(point);
|
|
67
|
+
}
|
|
68
|
+
if (run.length > 1) (facing ? front : back).push(run);
|
|
69
|
+
return { back: back.map((points) => pathOf(points)).join(""), front: front.map((points) => pathOf(points)).join("") };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function needleOf(lean: number) {
|
|
73
|
+
const angle = (-lean / DIAL_SPAN) * Math.PI * 2;
|
|
74
|
+
const ux = Math.sin(angle);
|
|
75
|
+
const uy = -Math.cos(angle);
|
|
76
|
+
const [cx, cy] = DIAL;
|
|
77
|
+
const point = (along: number, across: number) => at([cx + ux * along - uy * across, cy + uy * along + ux * across, DIAL_TOP + 0.1]);
|
|
78
|
+
return pathOf([point(DIAL_R - 3.4, 0), point(0, 0.75), point(-3, 0.35), point(-3, -0.35), point(0, -0.75)], true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function frameOf(pose: Pose, lean: number, grab: number, finger: number, lift: number, home: number, held = false): Frame {
|
|
82
|
+
const left = ROD_Y;
|
|
83
|
+
const coils = [
|
|
84
|
+
coil(-FRAME_X, -BUSH_END + lean, -left),
|
|
85
|
+
coil(-FRAME_X, -BUSH_END + lean, left),
|
|
86
|
+
coil(BUSH_END + lean, FRAME_X, -left),
|
|
87
|
+
coil(BUSH_END + lean, FRAME_X, left),
|
|
88
|
+
];
|
|
89
|
+
const grabX = carried(pose, grab);
|
|
90
|
+
const grabAt = at([grabX, 0, GLASS_TOP]);
|
|
91
|
+
const fingerAt = at([finger, 0, GLASS_TOP + lift]);
|
|
92
|
+
return {
|
|
93
|
+
glass: glassOf(pose),
|
|
94
|
+
coilsBack: coils.map((one) => one.back).join(""),
|
|
95
|
+
coilsLeft: coils[0]!.front + coils[1]!.front,
|
|
96
|
+
coilsRightBack: coils[2]!.front,
|
|
97
|
+
coilsRightFront: coils[3]!.front,
|
|
98
|
+
carriage: shift(lean),
|
|
99
|
+
needle: needleOf(lean),
|
|
100
|
+
grab: [tenth(grabAt[0]), tenth(grabAt[1])],
|
|
101
|
+
finger: [tenth(fingerAt[0]), tenth(fingerAt[1])],
|
|
102
|
+
pull: !held || Math.abs(grabX - finger) < 0.6 ? "" : pathOf([grabAt, fingerAt]),
|
|
103
|
+
rider: shift(grab),
|
|
104
|
+
probe: shift(finger - home),
|
|
105
|
+
tip: shift(finger - home, lift),
|
|
106
|
+
};
|
|
107
|
+
}
|