axstack 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +10 -6
  2. package/bin/axstack.js +4 -18
  3. package/docs/getting-started.md +6 -6
  4. package/docs/host-operations.md +77 -5
  5. package/docs/installation.md +25 -43
  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/role-roster.md +5 -3
  12. package/skills/axstack/references/ui-verification.md +6 -10
  13. package/skills/axstack/scripts/single-server.js +164 -0
  14. package/skills/axstack/systemd/axstack-single-server.service +8 -0
  15. package/skills/axstack/systemd/axstack-single-server.timer +10 -0
  16. package/skills/axstack-anatomy/ANATOMY.md +172 -0
  17. package/skills/axstack-anatomy/CHANGELOG.md +70 -0
  18. package/skills/axstack-anatomy/LICENSE +21 -0
  19. package/skills/axstack-anatomy/PROVENANCE.md +21 -0
  20. package/skills/axstack-anatomy/README.md +316 -0
  21. package/skills/axstack-anatomy/SKILL.md +74 -0
  22. package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
  23. package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
  24. package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
  25. package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
  26. package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
  27. package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
  28. package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
  29. package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
  30. package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
  31. package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
  32. package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
  33. package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
  34. package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
  35. package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
  36. package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
  37. package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
  38. package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
  39. package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
  40. package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
  41. package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
  42. package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
  43. package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
  44. package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
  45. package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
  46. package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
  47. package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
  48. package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
  49. package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
  50. package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
  51. package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
  52. package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
  53. package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
  54. package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
  55. package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
  56. package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
  57. package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
  58. package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
  59. package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
  60. package/skills/axstack-anatomy/kit/gl.mjs +184 -0
  61. package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
  62. package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
  63. package/skills/axstack-anatomy/kit/iso.css +106 -0
  64. package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
  65. package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
  66. package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
  67. package/skills/axstack-anatomy/kit/tube.mjs +306 -0
  68. package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
  69. package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
  70. package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
  71. package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
  72. package/skills/axstack-anatomy/references/3d.md +352 -0
  73. package/skills/axstack-anatomy/references/craft.md +325 -0
  74. package/skills/axstack-anatomy/references/kit.md +419 -0
  75. package/skills/axstack-anatomy/references/motion.md +165 -0
  76. package/skills/axstack-anatomy/references/react.md +42 -0
  77. package/skills/axstack-anatomy/references/verify.md +351 -0
  78. package/skills/axstack-anatomy/references/walkthrough.md +75 -0
  79. package/skills/axstack-anatomy/references/webgl.md +473 -0
  80. package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
  81. package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
  82. package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
  83. package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
  84. package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
  85. package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
  86. package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
  87. package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
  88. package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
  89. package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
  90. package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
  91. package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
  92. package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
  93. package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
  94. package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
  95. package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
  96. package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
  97. package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
  98. package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
  99. package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
  100. package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
  101. package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
  102. package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
  103. package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
  104. package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
  105. package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
  106. package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
  107. package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
  108. package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
  109. package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
  110. package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
  111. package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
  112. package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
  113. package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
  114. package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
  115. package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
  116. package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
  117. package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
  118. package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
  119. package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
  120. package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
  121. package/skills/axstack-anatomy/theme.js +56 -0
  122. package/skills/axstack-explain/SKILL.md +28 -15
  123. package/skills/axstack-explain/references/delivery.md +22 -0
  124. package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
  125. package/skills/axstack-explain/references/inline-pages.md +8 -8
  126. package/skills/axstack-explain/references/visual-qa.md +3 -5
  127. package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
  128. package/src/archify-legacy.js +70 -0
  129. package/src/capabilities.js +1 -29
  130. package/src/installer.js +11 -31
  131. package/skills/axstack-diagram/SKILL.md +0 -28
  132. package/skills/axstack-diagram/references/archify.md +0 -76
  133. package/src/archify-pin.js +0 -5
  134. package/src/archify.js +0 -151
@@ -0,0 +1,205 @@
1
+ import * as React from "react";
2
+ import { pathOf, type Flat, type KeyMark, type LegendKey, type LineTone, type SolidPaths, type Tone } from "../iso-kit";
3
+ import { Stage } from "./live";
4
+
5
+ interface SolidProps {
6
+ paths: SolidPaths;
7
+ tone?: Tone;
8
+ crease?: Tone | "none";
9
+ halo?: boolean;
10
+ lit?: boolean;
11
+ flat?: boolean;
12
+ className?: string;
13
+ inner?: React.ReactNode;
14
+ children?: React.ReactNode;
15
+ }
16
+
17
+ export function Solid({ paths, tone = "hi", crease = "faint", halo = false, lit = false, flat = false, className, inner, children }: SolidProps) {
18
+ return (
19
+ <g className={className ? `iso-solid ${className}` : "iso-solid"} data-lit={lit ? "" : undefined} data-seamless={paths.seamless ? "" : undefined}>
20
+ {halo ? <path className="iso-halo" d={paths.fill} /> : null}
21
+ <path className="iso-fill" d={paths.fill} />
22
+ {flat ? null : paths.shades.map((d, shade) => (d ? <path key={shade} className="iso-shade" data-shade={shade} d={d} /> : null))}
23
+ {flat ? null : <path className="iso-top" d={paths.top} />}
24
+ {crease === "none" || !paths.crease ? null : <path className="iso-line iso-crease" data-tone={crease} d={paths.crease} />}
25
+ {crease === "none" || !paths.bevel ? null : <path className="iso-line iso-bevel" data-tone={crease} d={paths.bevel} />}
26
+ {inner}
27
+ <path className="iso-line iso-edge" data-tone={tone} d={paths.outline} />
28
+ {children}
29
+ </g>
30
+ );
31
+ }
32
+
33
+ interface LineProps {
34
+ d: string;
35
+ tone?: LineTone;
36
+ dotted?: boolean;
37
+ dashed?: boolean;
38
+ flow?: boolean;
39
+ className?: string;
40
+ free?: boolean | "start" | "end";
41
+ }
42
+
43
+ export function Line({ d, tone = "lo", dotted = false, dashed = false, flow = false, className, free }: LineProps) {
44
+ if (!d) return null;
45
+ return (
46
+ <path
47
+ className={className ? `iso-line ${className}` : "iso-line"}
48
+ data-tone={tone}
49
+ data-dotted={dotted ? "" : undefined}
50
+ data-dashed={dashed ? "" : undefined}
51
+ data-flow={flow ? "" : undefined}
52
+ data-free={free === true ? "" : free || undefined}
53
+ d={d}
54
+ />
55
+ );
56
+ }
57
+
58
+ interface WireProps {
59
+ from: Flat;
60
+ to: Flat;
61
+ via?: Flat[];
62
+ tone?: LineTone;
63
+ dotted?: boolean;
64
+ dashed?: boolean;
65
+ flow?: boolean;
66
+ ends?: "both" | "from" | "to" | "none";
67
+ size?: number;
68
+ }
69
+
70
+ const tenth = (value: number) => Math.round(value * 10) / 10;
71
+
72
+ export function Wire({ from, to, via = [], tone = "lo", dotted = false, dashed = false, flow = false, ends = "both", size = 1.2 }: WireProps) {
73
+ return (
74
+ <g className="iso-wire" data-tone={tone}>
75
+ <Line d={pathOf([from, ...via, to])} tone={tone} dotted={dotted} dashed={dashed} flow={flow} />
76
+ {ends === "both" || ends === "from" ? <circle className="iso-wire-end" cx={tenth(from[0])} cy={tenth(from[1])} r={size} /> : null}
77
+ {ends === "both" || ends === "to" ? <circle className="iso-wire-end" cx={tenth(to[0])} cy={tenth(to[1])} r={size} /> : null}
78
+ </g>
79
+ );
80
+ }
81
+
82
+ export function Dots({ points, size = 0.5, tone = "mid", pulse = false }: { points: Array<Flat | [number, number, number]>; size?: number; tone?: Tone; pulse?: boolean }) {
83
+ return (
84
+ <g className="iso-dots" data-tone={tone}>
85
+ {points.map(([x, y, alpha], index) => (
86
+ <circle
87
+ key={index}
88
+ cx={tenth(x)}
89
+ cy={tenth(y)}
90
+ r={size}
91
+ fillOpacity={alpha !== undefined && alpha < 1 ? Math.max(0, alpha) : undefined}
92
+ data-pulse={pulse ? "" : undefined}
93
+ style={pulse ? ({ "--pulse-at": `${(index * 137) % 1600}ms` } as React.CSSProperties) : undefined}
94
+ />
95
+ ))}
96
+ </g>
97
+ );
98
+ }
99
+
100
+ export function FaceText({
101
+ transform,
102
+ children,
103
+ size = 3,
104
+ tone = "mid",
105
+ anchor = "start",
106
+ }: {
107
+ transform: string;
108
+ children: React.ReactNode;
109
+ size?: number;
110
+ tone?: Tone;
111
+ anchor?: "start" | "middle" | "end";
112
+ }) {
113
+ return (
114
+ <text className="iso-face-text" data-tone={tone} transform={transform} fontSize={size} textAnchor={anchor}>
115
+ {children}
116
+ </text>
117
+ );
118
+ }
119
+
120
+ interface IsoFigureProps {
121
+ width: number;
122
+ height: number;
123
+ label: string;
124
+ className?: string;
125
+ children: React.ReactNode;
126
+ }
127
+
128
+ export function IsoFigure({ width, height, label, className, children }: IsoFigureProps) {
129
+ return (
130
+ <Stage className={className ? `iso-stage ${className}` : "iso-stage"}>
131
+ <svg className="iso-svg" viewBox={`0 0 ${width} ${height}`} role="img" aria-label={label} focusable="false">
132
+ {children}
133
+ </svg>
134
+ </Stage>
135
+ );
136
+ }
137
+
138
+ function Swatch({ mark }: { mark: KeyMark }) {
139
+ if (mark === "raised" || mark === "flat" || mark === "lit") {
140
+ return (
141
+ <svg className="iso-swatch" data-mark={mark} viewBox="0 0 16 10" aria-hidden="true" focusable="false">
142
+ <path d={mark === "flat" ? "M2 6.5 8 3.5 14 6.5 8 9.5Z" : "M2 4.5 8 1.5 14 4.5 14 6 8 9 2 6Z"} />
143
+ {mark === "flat" ? null : <path className="iso-swatch-crease" d="M2 4.5 8 7.5 14 4.5" />}
144
+ </svg>
145
+ );
146
+ }
147
+ return (
148
+ <svg className="iso-swatch" data-mark={mark} viewBox="0 0 16 10" aria-hidden="true" focusable="false">
149
+ <path d="M1.5 5h13" />
150
+ </svg>
151
+ );
152
+ }
153
+
154
+ interface PlateProps {
155
+ fig: string;
156
+ title?: string;
157
+ hint?: React.ReactNode;
158
+ readout?: React.ReactNode;
159
+ keys?: LegendKey[];
160
+ caption?: React.ReactNode;
161
+ children: React.ReactNode;
162
+ }
163
+
164
+ export function Plate({ fig, title, hint, readout, keys = [], caption, children }: PlateProps) {
165
+ return (
166
+ <figure className="iso-figure">
167
+ <div className="iso-plate">
168
+ <div className="iso-plate-corners" aria-hidden="true">
169
+ <span className="iso-plate-corner" data-corner="fig">
170
+ {fig}
171
+ </span>
172
+ {title ? (
173
+ <span className="iso-plate-corner" data-corner="title">
174
+ {title}
175
+ </span>
176
+ ) : null}
177
+ {hint ? (
178
+ <span className="iso-plate-corner" data-corner="hint">
179
+ {hint}
180
+ </span>
181
+ ) : null}
182
+ <span className="iso-plate-corner" data-corner="readout" data-readout="">
183
+ {readout}
184
+ </span>
185
+ </div>
186
+ {children}
187
+ </div>
188
+ {keys.length || caption ? (
189
+ <figcaption className="iso-legend">
190
+ {keys.length ? (
191
+ <span className="iso-keys">
192
+ {keys.map((key) => (
193
+ <span className="iso-key" key={key.label}>
194
+ <Swatch mark={key.mark} />
195
+ {key.label}
196
+ </span>
197
+ ))}
198
+ </span>
199
+ ) : null}
200
+ {caption ? <span className="iso-caption">{caption}</span> : null}
201
+ </figcaption>
202
+ ) : null}
203
+ </figure>
204
+ );
205
+ }
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ const STILLNESS = "(prefers-reduced-motion: reduce)";
6
+ const MARGIN = "120px 0px";
7
+ const LONGEST_FRAME = 1 / 30;
8
+
9
+ export const asksForStillness = () => typeof window !== "undefined" && window.matchMedia(STILLNESS).matches;
10
+
11
+ export function useStillness() {
12
+ return React.useSyncExternalStore(
13
+ (changed) => {
14
+ const query = window.matchMedia(STILLNESS);
15
+ query.addEventListener("change", changed);
16
+ return () => query.removeEventListener("change", changed);
17
+ },
18
+ asksForStillness,
19
+ () => false,
20
+ );
21
+ }
22
+
23
+ export function useInView<T extends Element>(ref: React.RefObject<T | null>) {
24
+ const [inView, setInView] = React.useState(false);
25
+ const watched = React.useRef<Element | null>(null);
26
+ const observer = React.useRef<IntersectionObserver | null>(null);
27
+ React.useEffect(() => {
28
+ const node = ref.current;
29
+ if (node === watched.current) return;
30
+ observer.current?.disconnect();
31
+ observer.current = null;
32
+ watched.current = node;
33
+ setInView(false);
34
+ if (!node) return;
35
+ const binding = new IntersectionObserver((entries) => {
36
+ if (watched.current === node && observer.current === binding) setInView(Boolean(entries.at(-1)?.isIntersecting));
37
+ }, { rootMargin: MARGIN });
38
+ observer.current = binding;
39
+ binding.observe(node);
40
+ });
41
+ React.useEffect(() => () => {
42
+ observer.current?.disconnect();
43
+ observer.current = null;
44
+ watched.current = null;
45
+ }, []);
46
+ return inView;
47
+ }
48
+
49
+ export function Stage({ className, children }: { className: string; children: React.ReactNode }) {
50
+ const ref = React.useRef<HTMLDivElement>(null);
51
+ const inView = useInView(ref);
52
+ return (
53
+ <div ref={ref} className={className} data-inview={inView ? "" : undefined}>
54
+ {children}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ export function useReadout<T extends Element>() {
60
+ const anchor = React.useRef<T>(null);
61
+ const slot = React.useRef<Element | null>(null);
62
+ const said = React.useRef<string | null>(null);
63
+ React.useEffect(() => {
64
+ slot.current = anchor.current?.closest(".iso-plate")?.querySelector("[data-readout]") ?? null;
65
+ }, []);
66
+ const say = React.useCallback((text: string) => {
67
+ slot.current ??= anchor.current?.closest(".iso-plate")?.querySelector("[data-readout]") ?? null;
68
+ if (!slot.current || text === said.current) return;
69
+ said.current = text;
70
+ slot.current.textContent = text;
71
+ }, []);
72
+ return { anchor, say };
73
+ }
74
+
75
+ export function useLoop(tick: (dt: number, calm: boolean) => boolean, running: boolean) {
76
+ const latest = React.useRef(tick);
77
+ latest.current = tick;
78
+ const frame = React.useRef(0);
79
+ const last = React.useRef(0);
80
+ const kick = React.useCallback(() => {
81
+ if (frame.current) return;
82
+ last.current = 0;
83
+ const step = (now: number) => {
84
+ const dt = last.current ? Math.min((now - last.current) / 1000, LONGEST_FRAME) : 1 / 60;
85
+ last.current = now;
86
+ const more = latest.current(dt, asksForStillness());
87
+ frame.current = more ? requestAnimationFrame(step) : 0;
88
+ };
89
+ frame.current = requestAnimationFrame(step);
90
+ }, []);
91
+ React.useEffect(() => {
92
+ if (running) kick();
93
+ else {
94
+ cancelAnimationFrame(frame.current);
95
+ frame.current = 0;
96
+ }
97
+ return () => {
98
+ cancelAnimationFrame(frame.current);
99
+ frame.current = 0;
100
+ };
101
+ }, [running, kick]);
102
+ return kick;
103
+ }
104
+
105
+ export function follow(current: number, target: number, dt: number, seconds: number, calm = false) {
106
+ if (calm) return target;
107
+ const next = current + (target - current) * (1 - Math.exp(-dt / seconds));
108
+ return Math.abs(target - next) < 1e-3 ? target : next;
109
+ }
110
+
111
+ export interface Spring {
112
+ at: number;
113
+ rate: number;
114
+ }
115
+
116
+ export function spring(state: Spring, target: number, dt: number, stiffness: number, damping: number, calm = false) {
117
+ const drag = calm ? 2 * Math.sqrt(stiffness) : damping;
118
+ const steps = Math.max(Math.ceil(dt * 240), 1);
119
+ const slice = dt / steps;
120
+ for (let step = 0; step < steps; step++) {
121
+ state.rate += (target - state.at) * stiffness * slice - state.rate * drag * slice;
122
+ state.at += state.rate * slice;
123
+ }
124
+ if (Math.abs(target - state.at) < 5e-4 && Math.abs(state.rate) < 5e-4) {
125
+ state.at = target;
126
+ state.rate = 0;
127
+ }
128
+ return state.at === target && state.rate === 0;
129
+ }
130
+
131
+ export function useEased(target: number, seconds = 0.12) {
132
+ const [value, setValue] = React.useState(target);
133
+ const shown = React.useRef(target);
134
+ useLoop((dt, calm) => {
135
+ shown.current = follow(shown.current, target, dt, seconds, calm);
136
+ setValue(shown.current);
137
+ return shown.current !== target;
138
+ }, shown.current !== target);
139
+ return value;
140
+ }
@@ -0,0 +1,306 @@
1
+ import { iso, pathOf } from "./iso-kit.mjs";
2
+ import * as G from "./lathe.mjs";
3
+
4
+ const LIGHT = (() => {
5
+ const length = Math.hypot(-0.55, -0.835);
6
+ return [-0.55 / length, -0.835 / length];
7
+ })();
8
+
9
+ const poly = (points, closed = true) => pathOf(points, closed);
10
+
11
+ const area = (points) => points.reduce((sum, [x, y], index) => {
12
+ const [nx, ny] = points[(index + 1) % points.length];
13
+ return sum + x * ny - nx * y;
14
+ }, 0);
15
+
16
+ function sideOf(frames) {
17
+ const out = { left: [], right: [], shineA: [], shineB: [], shadeA: [], shadeB: [] };
18
+ for (const { p, nx, ny, half } of frames) {
19
+ out.left.push([p[0] + nx * half, p[1] + ny * half]);
20
+ out.right.push([p[0] - nx * half, p[1] - ny * half]);
21
+ const toward = nx * LIGHT[0] + ny * LIGHT[1];
22
+ const shineAt = half * 0.44 * toward;
23
+ const shineHalf = half * 0.24;
24
+ out.shineA.push([p[0] + nx * (shineAt + shineHalf), p[1] + ny * (shineAt + shineHalf)]);
25
+ out.shineB.push([p[0] + nx * (shineAt - shineHalf), p[1] + ny * (shineAt - shineHalf)]);
26
+ const shadeAt = -half * 0.62 * toward;
27
+ const shadeHalf = half * 0.2;
28
+ out.shadeA.push([p[0] + nx * (shadeAt + shadeHalf), p[1] + ny * (shadeAt + shadeHalf)]);
29
+ out.shadeB.push([p[0] + nx * (shadeAt - shadeHalf), p[1] + ny * (shadeAt - shadeHalf)]);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ function capOf(centre, tangent, radius, inward, P) {
35
+ const V = G.viewOf(P);
36
+ let n1 = G.cross3(tangent, V);
37
+ if (G.len3(n1) < 1e-6) n1 = G.frameAlong([0, 0, 0], tangent).u;
38
+ n1 = G.unit3(n1);
39
+ const n2 = G.unit3(G.cross3(tangent, n1));
40
+ const steps = 28;
41
+ const ring = Array.from({ length: steps }, (_, index) => {
42
+ const angle = (index / steps) * Math.PI * 2;
43
+ return iso(G.add3(centre, G.add3(G.mul3(n1, radius * Math.cos(angle)), G.mul3(n2, radius * Math.sin(angle)))), P);
44
+ });
45
+ const c = iso(centre, P);
46
+ const probe = iso(G.add3(centre, n2), P);
47
+ const along = (probe[0] - c[0]) * inward[0] + (probe[1] - c[1]) * inward[1];
48
+ const from = along > 0 ? steps / 2 : 0;
49
+ const half = (start) => Array.from({ length: steps / 2 + 1 }, (_, index) => ring[(start + index) % steps]);
50
+ return { ring, arc: half(from), inner: half((from + steps / 2) % steps) };
51
+ }
52
+
53
+ function stripeEnd(half, centre, nx, ny, from, to) {
54
+ const offset = (p) => (p[0] - centre[0]) * nx + (p[1] - centre[1]) * ny;
55
+ const values = half.map(offset);
56
+ const lo = Math.min(from, to);
57
+ const hi = Math.max(from, to);
58
+ const points = [];
59
+ const cross = (k, level) => {
60
+ const t = (level - values[k]) / (values[k + 1] - values[k] || 1e-9);
61
+ return [half[k][0] + (half[k + 1][0] - half[k][0]) * t, half[k][1] + (half[k + 1][1] - half[k][1]) * t];
62
+ };
63
+ for (let k = 0; k < half.length; k++) {
64
+ if (values[k] >= lo && values[k] <= hi) points.push(half[k]);
65
+ if (k < half.length - 1) for (const level of [lo, hi]) if ((values[k] - level) * (values[k + 1] - level) < 0) points.push(cross(k, level));
66
+ }
67
+ const order = points.map((p) => offset(p));
68
+ const sorted = points.map((p, index) => [p, order[index]]).sort((x, y) => (from > to ? y[1] - x[1] : x[1] - y[1]));
69
+ return sorted.map(([p]) => p);
70
+ }
71
+
72
+ function spansOf(total, gaps, breaks, maxLength, closed) {
73
+ const cuts = gaps
74
+ .map(([a, b]) => [Math.max(0, Math.min(a, b)), Math.min(total, Math.max(a, b))])
75
+ .filter(([a, b]) => b - a > 1e-6)
76
+ .sort((x, y) => x[0] - y[0]);
77
+ const runs = [];
78
+ let at = 0;
79
+ for (const [a, b] of cuts) {
80
+ if (a > at + 1e-6) runs.push([at, a, at > 0, true]);
81
+ at = Math.max(at, b);
82
+ }
83
+ if (total > at + 1e-6) runs.push([at, total, at > 0, cuts.length > 0 && cuts[cuts.length - 1][1] >= total - 1e-6]);
84
+ const pieces = [];
85
+ for (const [from, to, gapStart, gapEnd] of runs) {
86
+ const stops = [from, ...breaks.filter((b) => b > from + 1e-6 && b < to - 1e-6).sort((x, y) => x - y), to];
87
+ for (let index = 0; index < stops.length - 1; index++) {
88
+ const a = stops[index];
89
+ const b = stops[index + 1];
90
+ const count = Math.max(1, Math.ceil((b - a) / maxLength - 1e-9));
91
+ for (let c = 0; c < count; c++) {
92
+ pieces.push({
93
+ from: a + ((b - a) * c) / count,
94
+ to: a + ((b - a) * (c + 1)) / count,
95
+ open0: !(index === 0 && c === 0),
96
+ open1: !(index === stops.length - 2 && c === count - 1),
97
+ gap0: index === 0 && c === 0 && gapStart,
98
+ gap1: index === stops.length - 2 && c === count - 1 && gapEnd,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ if (closed && !cuts.length && pieces.length) {
104
+ pieces[0].open0 = true;
105
+ pieces[pieces.length - 1].open1 = true;
106
+ }
107
+ return pieces;
108
+ }
109
+
110
+ export function runsOf(total, gaps = []) {
111
+ const cuts = gaps
112
+ .map(([a, b]) => [Math.max(0, Math.min(a, b)), Math.min(total, Math.max(a, b))])
113
+ .filter(([a, b]) => b - a > 1e-6)
114
+ .sort((x, y) => x[0] - y[0]);
115
+ const runs = [];
116
+ let at = 0;
117
+ for (const [a, b] of cuts) {
118
+ if (a > at + 1e-6) runs.push([at, a]);
119
+ at = Math.max(at, b);
120
+ }
121
+ if (total > at + 1e-6) runs.push([at, total]);
122
+ return runs;
123
+ }
124
+
125
+ export function crossingsOf(points, centre, axis, radius) {
126
+ const n = G.unit3(axis);
127
+ const out = [];
128
+ let walked = 0;
129
+ for (let index = 1; index < points.length; index++) {
130
+ const a = points[index - 1];
131
+ const b = points[index];
132
+ const length = G.len3(G.sub3(b, a));
133
+ const da = G.dot3(G.sub3(a, centre), n);
134
+ const db = G.dot3(G.sub3(b, centre), n);
135
+ if ((da <= 0 && db > 0) || (da >= 0 && db < 0)) {
136
+ const t = da / (da - db);
137
+ const p = G.lerp3(a, b, t);
138
+ if (G.len3(G.sub3(p, centre)) <= radius) out.push(walked + t * length);
139
+ }
140
+ walked += length;
141
+ }
142
+ return out;
143
+ }
144
+
145
+ function ringsOf(dense, total, radiusAt, P, span, { pitch = 3, twist = 0, cross = false, fade = [0.1, 0.55], phase = 0, steps = 16 } = {}) {
146
+ const V = G.viewOf(P);
147
+ const [lo, hi] = span;
148
+ const margin = twist ? 0 : Math.min(pitch * 0.35, (hi - lo) / 2);
149
+ const from = Math.max(lo, span.real0 ? lo + margin : lo);
150
+ const to = Math.min(hi, span.real1 ? hi - margin : hi);
151
+ if (to <= from) return [];
152
+ const out = [];
153
+ const hands = twist ? (cross ? [1, -1] : [1]) : [0];
154
+ const reach = Math.abs(twist) * Math.max(...[0, 0.5, 1].map(radiusAt));
155
+ const first = Math.ceil((from - reach - phase) / pitch);
156
+ const last = Math.floor((to + reach - phase) / pitch);
157
+ const surface = (t, theta) => {
158
+ const at = Math.max(0, Math.min(total, t));
159
+ const c = G.pointAlong(dense, at);
160
+ const T = G.tangentAlong(dense, at);
161
+ let n1 = G.cross3(T, V);
162
+ if (G.len3(n1) < 1e-6) return null;
163
+ n1 = G.unit3(n1);
164
+ let n2 = G.unit3(G.cross3(T, n1));
165
+ if (G.dot3(n2, V) < 0) n2 = G.mul3(n2, -1);
166
+ const r = radiusAt(at / (total || 1));
167
+ const normal = G.add3(G.mul3(n1, Math.cos(theta)), G.mul3(n2, Math.sin(theta)));
168
+ return { p: iso(G.add3(c, G.mul3(normal, r)), P), facing: G.dot3(normal, V), r };
169
+ };
170
+ for (let index = first; index <= last; index++) {
171
+ const station = phase + index * pitch;
172
+ for (const hand of hands) {
173
+ const samples = [];
174
+ for (let step = 0; step <= steps; step++) {
175
+ const theta = (step / steps) * Math.PI;
176
+ const t = station + hand * twist * radiusAt(Math.max(0, Math.min(1, station / (total || 1)))) * (step / steps - 0.5);
177
+ samples.push({ t, theta });
178
+ }
179
+ for (let step = 0; step < steps; step++) {
180
+ let a = samples[step];
181
+ let b = samples[step + 1];
182
+ if ((a.t < from && b.t < from) || (a.t > to && b.t > to)) continue;
183
+ const clipTo = (x, y, edge) => ({ t: edge, theta: x.theta + ((y.theta - x.theta) * (edge - x.t)) / (y.t - x.t || 1e-9) });
184
+ if (a.t < from) a = clipTo(a, b, from);
185
+ if (b.t < from) b = clipTo(b, a, from);
186
+ if (a.t > to) a = clipTo(a, b, to);
187
+ if (b.t > to) b = clipTo(b, a, to);
188
+ const pa = surface(a.t, a.theta);
189
+ const pb = surface(b.t, b.theta);
190
+ if (!pa || !pb) continue;
191
+ const alpha = fade ? G.fadeOf((pa.facing + pb.facing) / 2, fade) : 1;
192
+ if (alpha <= 0.02) continue;
193
+ out.push({ d: pathOf([pa.p, pb.p]), alpha });
194
+ }
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+
200
+ export function tubePieces(points3, radius, P, { maxLength = 30, spacing = 2.2, closed = false, breaks = [], gaps = [], caps = true, stripes = "auto", rings = null } = {}) {
201
+ const dense = G.resample(points3, spacing);
202
+ const total = G.pathLength(dense);
203
+ const radiusAt = typeof radius === "function" ? radius : () => radius;
204
+ const k = G.scaleOf(P);
205
+ const wide = stripes === "auto" ? Math.max(...Array.from({ length: 33 }, (_, index) => radiusAt(index / 32))) * k > 1.3 : Boolean(stripes);
206
+ const lead = 0.5;
207
+ const out = [];
208
+ const looped = closed && !gaps.length;
209
+ const wrap = (at) => (looped ? ((at % total) + total) % total : Math.max(0, Math.min(total, at)));
210
+ const step = Math.max(0.05, Math.min(spacing, total / 4) * 0.5);
211
+ const frameAt = (at, p, width) => {
212
+ const a = iso(G.pointAlong(dense, wrap(at - step)), P);
213
+ const b = iso(G.pointAlong(dense, wrap(at + step)), P);
214
+ let tx = b[0] - a[0];
215
+ let ty = b[1] - a[1];
216
+ const length = Math.hypot(tx, ty) || 1;
217
+ tx /= length;
218
+ ty /= length;
219
+ return { p, nx: -ty, ny: tx, half: width };
220
+ };
221
+ const partOf = (start, end) => {
222
+ let points = G.cut(dense, Math.max(0, start), Math.min(total, end));
223
+ if (looped && start < 0) points = [...G.cut(dense, total + start, total), ...points.slice(1)];
224
+ if (looped && end > total) points = [...points, ...G.cut(dense, 0, end - total).slice(1)];
225
+ let walked = looped ? start : Math.max(0, start);
226
+ const along = points.map((point, index) => {
227
+ if (index > 0) walked += G.len3(G.sub3(point, points[index - 1]));
228
+ return walked;
229
+ });
230
+ const flat = points.map((point) => iso(point, P));
231
+ const widths = along.map((at) => radiusAt(looped ? wrap(at) / total : Math.min(1, Math.max(0, at / total))) * k);
232
+ const frames = flat.map((p, index) => frameAt(along[index], p, widths[index]));
233
+ return { points, flat, widths, frames };
234
+ };
235
+ for (const piece of spansOf(total, gaps, breaks, maxLength, closed)) {
236
+ const openStart = piece.open0 && (piece.from > 0 || looped);
237
+ const openEnd = piece.open1 && (piece.to < total || looped);
238
+ const body = partOf(openStart ? piece.from - lead : piece.from, openEnd ? piece.to + lead : piece.to);
239
+ if (body.points.length < 2) continue;
240
+ const stripe = openStart || openEnd ? partOf(openStart ? piece.from - 2 * lead : piece.from, openEnd ? piece.to + 2 * lead : piece.to) : body;
241
+ const { points: part, flat, widths, frames } = body;
242
+ const sides = sideOf(frames);
243
+ const bands = stripe === body ? sides : sideOf(stripe.frames);
244
+ let bodyPath = poly([...sides.left, ...sides.right.slice().reverse()]);
245
+ let edges = pathOf(sides.left) + pathOf(sides.right);
246
+ const ends = [!piece.open0 && !looped && (piece.from <= 1e-6 || piece.gap0), !piece.open1 && !looped && (piece.to >= total - 1e-6 || piece.gap1)];
247
+ const capped = [ends[0] && (piece.gap0 || [caps].flat()[0]), ends[1] && (piece.gap1 || [caps].flat().at(-1))];
248
+ const sign = Math.sign(area([...sides.left, ...sides.right.slice().reverse()]));
249
+ const tips = [null, null];
250
+ [0, 1].forEach((side) => {
251
+ if (!capped[side]) return;
252
+ const at = side ? part.length - 1 : 0;
253
+ const other = side ? part.length - 2 : 1;
254
+ const tangent = G.unit3(G.sub3(part[side ? at : other], part[side ? other : at]));
255
+ const outward = side ? tangent : G.mul3(tangent, -1);
256
+ const inward = [flat[other][0] - flat[at][0], flat[other][1] - flat[at][1]];
257
+ const { ring, arc, inner } = capOf(part[at], tangent, widths[at] / k, inward, P);
258
+ const oriented = Math.sign(area(ring)) === sign ? ring : ring.slice().reverse();
259
+ bodyPath += poly(oriented);
260
+ edges += pathOf(arc);
261
+ const open = G.dot3(outward, G.viewOf(P)) > 0;
262
+ if (open) edges += pathOf(inner);
263
+ tips[side] = { half: open ? inner : arc, frame: frames[at] };
264
+ });
265
+ const band = (a, b, offsets) => {
266
+ const head = tips[1] ? stripeEnd(tips[1].half, tips[1].frame.p, tips[1].frame.nx, tips[1].frame.ny, offsets(tips[1].frame)[0], offsets(tips[1].frame)[1]) : [];
267
+ const tail = tips[0] ? stripeEnd(tips[0].half, tips[0].frame.p, tips[0].frame.nx, tips[0].frame.ny, offsets(tips[0].frame)[1], offsets(tips[0].frame)[0]) : [];
268
+ return poly([...a, ...head, ...b.slice().reverse(), ...tail]);
269
+ };
270
+ const toward = ({ nx, ny }) => nx * LIGHT[0] + ny * LIGHT[1];
271
+ const shineOffsets = (frame) => [frame.half * 0.44 * toward(frame) + frame.half * 0.24, frame.half * 0.44 * toward(frame) - frame.half * 0.24];
272
+ const shadeOffsets = (frame) => [-frame.half * 0.62 * toward(frame) + frame.half * 0.2, -frame.half * 0.62 * toward(frame) - frame.half * 0.2];
273
+ const ringSpan = Object.assign([openStart ? piece.from - 2 * lead : piece.from, openEnd ? piece.to + 2 * lead : piece.to], { real0: !openStart, real1: !openEnd });
274
+ out.push({
275
+ rings: rings ? ringsOf(dense, total, radiusAt, P, ringSpan, rings) : [],
276
+ ringTone: rings?.tone ?? "lo",
277
+ mid: G.pointAlong(dense, (piece.from + piece.to) / 2),
278
+ body: bodyPath,
279
+ shine: band(bands.shineA, bands.shineB, shineOffsets),
280
+ shade: band(bands.shadeA, bands.shadeB, shadeOffsets),
281
+ edges,
282
+ wide,
283
+ points: part,
284
+ radii: widths.map((width) => width / k),
285
+ ends,
286
+ from: piece.from,
287
+ to: piece.to,
288
+ });
289
+ }
290
+ return out;
291
+ }
292
+
293
+ export function ringsSvg(rings, tone = "lo") {
294
+ const groups = new Map();
295
+ for (const { d, alpha } of rings ?? []) {
296
+ const level = Math.min(1, Math.round(alpha * 20) / 20);
297
+ if (level <= 0) continue;
298
+ groups.set(level, (groups.get(level) ?? "") + d);
299
+ }
300
+ return [...groups].map(([alpha, d]) => `<path class="tb-ring" data-tone="${tone}" d="${d}"${alpha < 1 ? ` opacity="${alpha.toFixed(2)}"` : ""}/>`).join("");
301
+ }
302
+
303
+ export function tubeSvg(piece, { tone = "hi" } = {}) {
304
+ const shine = piece.wide ? `<path class="tb-shine" d="${piece.shine}"/><path class="tb-shade" d="${piece.shade}"/>` : "";
305
+ return `<g class="tb"><path class="tb-body" d="${piece.body}"/>${shine}${ringsSvg(piece.rings, piece.ringTone)}<path class="iso-line tb-edge" data-tone="${tone}" d="${piece.edges}"/></g>`;
306
+ }