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.
- package/README.md +10 -6
- package/bin/axstack.js +4 -18
- package/docs/getting-started.md +6 -6
- package/docs/host-operations.md +77 -5
- package/docs/installation.md +25 -43
- package/docs/workflows.md +12 -9
- package/package.json +1 -1
- package/profiles/presets/claude-only.json +3 -3
- package/profiles/presets/codex-only.json +3 -3
- package/profiles/presets/mixed.json +5 -4
- package/skills/axstack/references/role-roster.md +5 -3
- package/skills/axstack/references/ui-verification.md +6 -10
- package/skills/axstack/scripts/single-server.js +164 -0
- package/skills/axstack/systemd/axstack-single-server.service +8 -0
- package/skills/axstack/systemd/axstack-single-server.timer +10 -0
- package/skills/axstack-anatomy/ANATOMY.md +172 -0
- package/skills/axstack-anatomy/CHANGELOG.md +70 -0
- package/skills/axstack-anatomy/LICENSE +21 -0
- package/skills/axstack-anatomy/PROVENANCE.md +21 -0
- package/skills/axstack-anatomy/README.md +316 -0
- package/skills/axstack-anatomy/SKILL.md +74 -0
- package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
- package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
- package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
- package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
- package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
- package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
- package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
- package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
- package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
- package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
- package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
- package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
- package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
- package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
- package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
- package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
- package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
- package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
- package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
- package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
- package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
- package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
- package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
- package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
- package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
- package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
- package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
- package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
- package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
- package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
- package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
- package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
- package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
- package/skills/axstack-anatomy/kit/gl.mjs +184 -0
- package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
- package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
- package/skills/axstack-anatomy/kit/iso.css +106 -0
- package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
- package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
- package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
- package/skills/axstack-anatomy/kit/tube.mjs +306 -0
- package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
- package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
- package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
- package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
- package/skills/axstack-anatomy/references/3d.md +352 -0
- package/skills/axstack-anatomy/references/craft.md +325 -0
- package/skills/axstack-anatomy/references/kit.md +419 -0
- package/skills/axstack-anatomy/references/motion.md +165 -0
- package/skills/axstack-anatomy/references/react.md +42 -0
- package/skills/axstack-anatomy/references/verify.md +351 -0
- package/skills/axstack-anatomy/references/walkthrough.md +75 -0
- package/skills/axstack-anatomy/references/webgl.md +473 -0
- package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
- package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
- package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
- package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
- package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
- package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
- package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
- package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
- package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
- package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
- package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
- package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
- package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
- package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
- package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
- package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
- package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
- package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
- package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
- package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
- package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
- package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
- package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
- package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
- package/skills/axstack-anatomy/theme.js +56 -0
- package/skills/axstack-explain/SKILL.md +28 -15
- package/skills/axstack-explain/references/delivery.md +22 -0
- package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
- package/skills/axstack-explain/references/inline-pages.md +8 -8
- package/skills/axstack-explain/references/visual-qa.md +3 -5
- package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
- package/src/archify-legacy.js +70 -0
- package/src/capabilities.js +1 -29
- package/src/installer.js +11 -31
- package/skills/axstack-diagram/SKILL.md +0 -28
- package/skills/axstack-diagram/references/archify.md +0 -76
- package/src/archify-pin.js +0 -5
- package/src/archify.js +0 -151
|
@@ -0,0 +1,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
|
+
}
|