axstack 0.36.1 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +5 -4
  2. package/bin/axstack.js +4 -18
  3. package/docs/getting-started.md +6 -6
  4. package/docs/host-operations.md +9 -5
  5. package/docs/installation.md +20 -41
  6. package/docs/workflows.md +12 -9
  7. package/package.json +1 -1
  8. package/profiles/presets/claude-only.json +3 -3
  9. package/profiles/presets/codex-only.json +3 -3
  10. package/profiles/presets/mixed.json +5 -4
  11. package/skills/axstack/references/automations.md +7 -2
  12. package/skills/axstack/references/lifecycle.md +2 -1
  13. package/skills/axstack/references/review-manager-prompt.md +44 -3
  14. package/skills/axstack/references/role-roster.md +13 -6
  15. package/skills/axstack/references/routing.md +2 -1
  16. package/skills/axstack/references/run-record.md +2 -0
  17. package/skills/axstack/references/ui-verification.md +6 -10
  18. package/skills/axstack-anatomy/ANATOMY.md +172 -0
  19. package/skills/axstack-anatomy/CHANGELOG.md +70 -0
  20. package/skills/axstack-anatomy/LICENSE +21 -0
  21. package/skills/axstack-anatomy/PROVENANCE.md +21 -0
  22. package/skills/axstack-anatomy/README.md +316 -0
  23. package/skills/axstack-anatomy/SKILL.md +74 -0
  24. package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
  25. package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
  26. package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
  27. package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
  28. package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
  29. package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
  30. package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
  31. package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
  32. package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
  33. package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
  34. package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
  35. package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
  36. package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
  37. package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
  38. package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
  39. package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
  40. package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
  41. package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
  42. package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
  43. package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
  44. package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
  45. package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
  46. package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
  47. package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
  48. package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
  49. package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
  50. package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
  51. package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
  52. package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
  53. package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
  54. package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
  55. package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
  56. package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
  57. package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
  58. package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
  59. package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
  60. package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
  61. package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
  62. package/skills/axstack-anatomy/kit/gl.mjs +184 -0
  63. package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
  64. package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
  65. package/skills/axstack-anatomy/kit/iso.css +106 -0
  66. package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
  67. package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
  68. package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
  69. package/skills/axstack-anatomy/kit/tube.mjs +306 -0
  70. package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
  71. package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
  72. package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
  73. package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
  74. package/skills/axstack-anatomy/references/3d.md +352 -0
  75. package/skills/axstack-anatomy/references/craft.md +325 -0
  76. package/skills/axstack-anatomy/references/kit.md +419 -0
  77. package/skills/axstack-anatomy/references/motion.md +165 -0
  78. package/skills/axstack-anatomy/references/react.md +42 -0
  79. package/skills/axstack-anatomy/references/verify.md +351 -0
  80. package/skills/axstack-anatomy/references/walkthrough.md +75 -0
  81. package/skills/axstack-anatomy/references/webgl.md +473 -0
  82. package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
  83. package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
  84. package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
  85. package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
  86. package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
  87. package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
  88. package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
  89. package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
  90. package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
  91. package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
  92. package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
  93. package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
  94. package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
  95. package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
  96. package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
  97. package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
  98. package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
  99. package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
  100. package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
  101. package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
  102. package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
  103. package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
  104. package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
  105. package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
  106. package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
  107. package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
  108. package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
  109. package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
  110. package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
  111. package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
  112. package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
  113. package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
  114. package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
  115. package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
  116. package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
  117. package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
  118. package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
  119. package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
  120. package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
  121. package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
  122. package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
  123. package/skills/axstack-anatomy/theme.js +56 -0
  124. package/skills/axstack-explain/SKILL.md +28 -15
  125. package/skills/axstack-explain/references/delivery.md +22 -0
  126. package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
  127. package/skills/axstack-explain/references/inline-pages.md +8 -8
  128. package/skills/axstack-explain/references/visual-qa.md +3 -5
  129. package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
  130. package/skills/axstack-research/SKILL.md +18 -6
  131. package/src/archify-legacy.js +70 -0
  132. package/src/capabilities.js +1 -29
  133. package/src/installer.js +11 -31
  134. package/skills/axstack-diagram/SKILL.md +0 -28
  135. package/skills/axstack-diagram/references/archify.md +0 -76
  136. package/src/archify-pin.js +0 -5
  137. package/src/archify.js +0 -151
@@ -0,0 +1,541 @@
1
+ import { type Flat, iso, pathOf, type Plane } from "../../kit/iso-kit";
2
+ import { BIG_Y, CLIMB, FLOOR, GAPS, LIGHT, P, RA, RB, THICK, VIEW, fieldOf, rowX, rowY, spanOf, type Field } from "./rig";
3
+
4
+ const STEP = 2;
5
+ const FINE = 0.5;
6
+ const FINE_HALF = 6;
7
+ const NUDGE = -0.05;
8
+ const KISS = 0.75;
9
+ const WINDOW = 4;
10
+ const BEND = 0.3;
11
+ const BEVEL = 2.2;
12
+ const TAPER = 10;
13
+ const SHADES = 4;
14
+ const STRIP = 8;
15
+ const SHINE_IN = 3.4;
16
+ const SHINE_FACING = 0.82;
17
+ const FADE_SHARE = 0.3;
18
+ const FADE_MOST = 9;
19
+ const FADE_STEPS = 6;
20
+
21
+ interface Grid {
22
+ values: Float32Array;
23
+ us: Float64Array;
24
+ vs: Float64Array;
25
+ }
26
+
27
+ function ticksOf(centre: number, before: number, after: number) {
28
+ const ticks: number[] = [];
29
+ for (let k = -Math.ceil(before / STEP); k <= Math.ceil(after / STEP); k++) {
30
+ if (Math.abs(k * STEP) > FINE_HALF) ticks.push(centre + k * STEP);
31
+ }
32
+ const fine = Math.round(FINE_HALF / FINE);
33
+ for (let k = -fine; k <= fine; k++) ticks.push(centre + k * FINE);
34
+ return Float64Array.from(ticks.sort((a, b) => a - b));
35
+ }
36
+
37
+ function sample(field: Field, gap: number): Grid {
38
+ const middle = RA + gap / 2;
39
+ const us = ticksOf(middle, middle + RA + 6, spanOf(gap) + RB + 6 - middle);
40
+ const vs = ticksOf(0, RA + 6, RA + 6);
41
+ const nx = us.length;
42
+ const ny = vs.length;
43
+ const values = new Float32Array(nx * ny);
44
+ for (let j = 0; j < ny; j++) {
45
+ const mirror = ny - 1 - j;
46
+ if (mirror < j) {
47
+ values.copyWithin(j * nx, mirror * nx, mirror * nx + nx);
48
+ continue;
49
+ }
50
+ const v = vs[j]!;
51
+ for (let i = 0; i < nx; i++) values[j * nx + i] = field(us[i]!, v);
52
+ }
53
+ return { values, us, vs };
54
+ }
55
+
56
+ const SEGMENTS: Record<number, [number, number][]> = {
57
+ 1: [[3, 2]],
58
+ 2: [[2, 1]],
59
+ 3: [[3, 1]],
60
+ 4: [[0, 1]],
61
+ 6: [[0, 2]],
62
+ 7: [[3, 0]],
63
+ 8: [[3, 0]],
64
+ 9: [[0, 2]],
65
+ 11: [[0, 1]],
66
+ 12: [[3, 1]],
67
+ 13: [[2, 1]],
68
+ 14: [[3, 2]],
69
+ };
70
+
71
+ function rings({ values, us, vs }: Grid, level: number): Plane[][] {
72
+ const nx = us.length;
73
+ const ny = vs.length;
74
+ const at = (i: number, j: number) => values[j * nx + i]!;
75
+ const links = new Map<number, number[]>();
76
+ const points = new Map<number, Plane>();
77
+ const pointOf = (id: number) => {
78
+ if (points.has(id)) return;
79
+ const cell = id >> 1;
80
+ const i = cell % nx;
81
+ const j = (cell - i) / nx;
82
+ const down = id & 1;
83
+ const a = at(i, j);
84
+ const b = down ? at(i, j + 1) : at(i + 1, j);
85
+ const t = Math.min(Math.max((level - a) / (b - a), 0), 1);
86
+ points.set(id, down ? [us[i]!, vs[j]! + (vs[j + 1]! - vs[j]!) * t] : [us[i]! + (us[i + 1]! - us[i]!) * t, vs[j]!]);
87
+ };
88
+ const link = (a: number, b: number) => {
89
+ pointOf(a);
90
+ pointOf(b);
91
+ (links.get(a) ?? links.set(a, []).get(a)!).push(b);
92
+ (links.get(b) ?? links.set(b, []).get(b)!).push(a);
93
+ };
94
+ for (let j = 0; j < ny - 1; j++) {
95
+ for (let i = 0; i < nx - 1; i++) {
96
+ const tl = at(i, j);
97
+ const tr = at(i + 1, j);
98
+ const br = at(i + 1, j + 1);
99
+ const bl = at(i, j + 1);
100
+ const code = (tl < level ? 8 : 0) | (tr < level ? 4 : 0) | (br < level ? 2 : 0) | (bl < level ? 1 : 0);
101
+ if (code === 0 || code === 15) continue;
102
+ const edges = [(j * nx + i) * 2, ((j * nx + i + 1) * 2) | 1, ((j + 1) * nx + i) * 2, ((j * nx + i) * 2) | 1];
103
+ let pairs = SEGMENTS[code];
104
+ if (!pairs) {
105
+ const centre = (tl + tr + br + bl) / 4 < level;
106
+ if (code === 5) pairs = centre ? [[3, 0], [2, 1]] : [[0, 1], [3, 2]];
107
+ else pairs = centre ? [[0, 1], [3, 2]] : [[3, 0], [2, 1]];
108
+ }
109
+ for (const [a, b] of pairs) link(edges[a]!, edges[b]!);
110
+ }
111
+ }
112
+ const seen = new Set<number>();
113
+ const found: Plane[][] = [];
114
+ for (const start of links.keys()) {
115
+ if (seen.has(start)) continue;
116
+ const ring: Plane[] = [];
117
+ let previous = -1;
118
+ let current = start;
119
+ while (!seen.has(current)) {
120
+ seen.add(current);
121
+ ring.push(points.get(current)!);
122
+ const next = links.get(current)!.find((candidate) => candidate !== previous && !seen.has(candidate));
123
+ if (next === undefined) break;
124
+ previous = current;
125
+ current = next;
126
+ }
127
+ if (ring.length > 3) found.push(ring);
128
+ }
129
+ return found;
130
+ }
131
+
132
+ function simplify(points: Plane[], tolerance: number): Plane[] {
133
+ if (points.length < 3) return points;
134
+ const first = points[0]!;
135
+ const last = points[points.length - 1]!;
136
+ const dx = last[0] - first[0];
137
+ const dy = last[1] - first[1];
138
+ const length = Math.hypot(dx, dy) || 1;
139
+ let worst = 0;
140
+ let index = 0;
141
+ for (let step = 1; step < points.length - 1; step++) {
142
+ const point = points[step]!;
143
+ const off = Math.abs((point[0] - first[0]) * dy - (point[1] - first[1]) * dx) / length;
144
+ if (off > worst) {
145
+ worst = off;
146
+ index = step;
147
+ }
148
+ }
149
+ if (worst <= tolerance) return [first, last];
150
+ return [...simplify(points.slice(0, index + 1), tolerance).slice(0, -1), ...simplify(points.slice(index), tolerance)];
151
+ }
152
+
153
+ function simplifyRing(ring: Plane[], tolerance: number): Plane[] {
154
+ let far = 0;
155
+ let best = 0;
156
+ for (let index = 1; index < ring.length; index++) {
157
+ const distance = Math.hypot(ring[index]![0] - ring[0]![0], ring[index]![1] - ring[0]![1]);
158
+ if (distance > best) {
159
+ best = distance;
160
+ far = index;
161
+ }
162
+ }
163
+ const one = simplify(ring.slice(0, far + 1), tolerance);
164
+ const two = simplify([...ring.slice(far), ring[0]!], tolerance);
165
+ return [...one.slice(0, -1), ...two.slice(0, -1)];
166
+ }
167
+
168
+ function signedArea(points: Plane[] | Flat[]) {
169
+ let area = 0;
170
+ for (let index = 0; index < points.length; index++) {
171
+ const [ax, ay] = points[index]!;
172
+ const [bx, by] = points[(index + 1) % points.length]!;
173
+ area += ax * by - bx * ay;
174
+ }
175
+ return area / 2;
176
+ }
177
+
178
+ const oriented = (points: Flat[]) => (signedArea(points) < 0 ? [...points].reverse() : points);
179
+
180
+ function runsOf(flags: boolean[], want: boolean) {
181
+ const count = flags.length;
182
+ const runs: number[][] = [];
183
+ const start = flags.findIndex((flag, index) => flag !== flags[(index + count - 1) % count]);
184
+ if (start < 0) return flags[0] === want ? [Array.from({ length: count + 1 }, (_, index) => index % count)] : [];
185
+ let run: number[] | null = null;
186
+ for (let step = 0; step < count; step++) {
187
+ const index = (start + step) % count;
188
+ if (flags[index] === want) {
189
+ if (!run) run = [index];
190
+ run.push((index + 1) % count);
191
+ } else if (run) {
192
+ runs.push(run);
193
+ run = null;
194
+ }
195
+ }
196
+ if (run) runs.push(run);
197
+ return runs;
198
+ }
199
+
200
+ export interface GlassShape {
201
+ fill: string;
202
+ shades: { d: string; shade: number }[];
203
+ top: string;
204
+ crease: string;
205
+ bevel: string;
206
+ outline: string;
207
+ shine: string;
208
+ depth: number;
209
+ }
210
+
211
+ const ease = (t: number) => t * t * (3 - 2 * t);
212
+
213
+ type Hidden = (x: number, y: number, z: number, edge: number) => boolean;
214
+
215
+ function hiddenBy(ring: Plane[], top: number): Hidden {
216
+ const count = ring.length;
217
+ const xs = new Float64Array(count);
218
+ const ys = new Float64Array(count);
219
+ for (let index = 0; index < count; index++) {
220
+ xs[index] = ring[index]![0];
221
+ ys[index] = ring[index]![1];
222
+ }
223
+ const inside = (x: number, y: number) => {
224
+ let hit = false;
225
+ for (let index = 0, previous = count - 1; index < count; previous = index++) {
226
+ const ay = ys[index]!;
227
+ const by = ys[previous]!;
228
+ if (ay > y !== by > y && x < ((xs[previous]! - xs[index]!) * (y - ay)) / (by - ay) + xs[index]!) hit = !hit;
229
+ }
230
+ return hit;
231
+ };
232
+ return (x, y, z, edge) => {
233
+ const reach = (top - z) / CLIMB;
234
+ if (reach <= 0) return false;
235
+ if (inside(x + VIEW[0] * 0.05, y + VIEW[1] * 0.05)) return true;
236
+ const fx = x + VIEW[0] * reach;
237
+ const fy = y + VIEW[1] * reach;
238
+ const left = Math.min(x, fx);
239
+ const right = Math.max(x, fx);
240
+ const low = Math.min(y, fy);
241
+ const high = Math.max(y, fy);
242
+ for (let index = 0; index < count; index++) {
243
+ if (index === edge || (edge < 0 && (index === -edge - 1 || (index + 1) % count === -edge - 1))) continue;
244
+ const next = index + 1 === count ? 0 : index + 1;
245
+ const ax = xs[index]!;
246
+ const ay = ys[index]!;
247
+ const bx = xs[next]!;
248
+ const by = ys[next]!;
249
+ if ((ax < left && bx < left) || (ax > right && bx > right) || (ay < low && by < low) || (ay > high && by > high)) continue;
250
+ const ex = bx - ax;
251
+ const ey = by - ay;
252
+ const cross = VIEW[0] * ey - VIEW[1] * ex;
253
+ if (Math.abs(cross) < 1e-12) continue;
254
+ const ox = ax - x;
255
+ const oy = ay - y;
256
+ const t = (ox * ey - oy * ex) / cross;
257
+ const u = (ox * VIEW[1] - oy * VIEW[0]) / cross;
258
+ if (t > 0.05 && t < reach && u >= 0 && u <= 1) return true;
259
+ }
260
+ return false;
261
+ };
262
+ }
263
+
264
+ function visibleRuns(ring: Plane[], run: number[], z: number, hidden: Hidden): Flat[][] {
265
+ const runs: Flat[][] = [];
266
+ let line: Flat[] = [];
267
+ const shown = run.map((index) => !hidden(ring[index]![0], ring[index]![1], z, -index - 1));
268
+ const lift = ([x, y]: Plane) => iso([x, y, z], P);
269
+ const cut = (edge: number, fromShown: boolean): Plane => {
270
+ const a = ring[edge]!;
271
+ const b = ring[(edge + 1) % ring.length]!;
272
+ let low = 0;
273
+ let high = 1;
274
+ for (let round = 0; round < 14; round++) {
275
+ const mid = (low + high) / 2;
276
+ const seen = !hidden(a[0] + (b[0] - a[0]) * mid, a[1] + (b[1] - a[1]) * mid, z, edge);
277
+ if (seen === fromShown) low = mid;
278
+ else high = mid;
279
+ }
280
+ const t = (low + high) / 2;
281
+ return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
282
+ };
283
+ for (let index = 0; index < run.length; index++) {
284
+ const point = ring[run[index]!]!;
285
+ if (index > 0 && shown[index] !== shown[index - 1]) {
286
+ const edge = cut(run[index - 1]!, shown[index - 1]!);
287
+ if (shown[index - 1]) {
288
+ line.push(lift(edge));
289
+ if (line.length > 1) runs.push(line);
290
+ line = [];
291
+ } else {
292
+ line = [lift(edge)];
293
+ }
294
+ }
295
+ if (shown[index]) line.push(lift(point));
296
+ }
297
+ if (line.length > 1) runs.push(line);
298
+ return runs;
299
+ }
300
+
301
+ function solidOf(ring: Plane[], z: number, height: number, occlude: boolean): GlassShape {
302
+ const count = ring.length;
303
+ const counter = signedArea(ring) > 0;
304
+ const normals = ring.map(([ax, ay], index) => {
305
+ const [bx, by] = ring[(index + 1) % count]!;
306
+ const dx = bx - ax;
307
+ const dy = by - ay;
308
+ const length = Math.hypot(dx, dy) || 1;
309
+ return (counter ? [dy / length, -dx / length] : [-dy / length, dx / length]) as Plane;
310
+ });
311
+ const facing = normals.map(([nx, ny]) => nx * VIEW[0] + ny * VIEW[1] > 1e-9);
312
+ const top = ring.map(([x, y]) => iso([x, y, z + height], P));
313
+ const bottom = ring.map(([x, y]) => iso([x, y, z], P));
314
+ const hidden = occlude ? hiddenBy(ring, z + height) : null;
315
+
316
+ const strips: { depth: number; shade: number; d: string }[] = [];
317
+ let open: { from: number; to: number; shade: number } | null = null;
318
+ const close = () => {
319
+ if (!open) return;
320
+ const upper: Flat[] = [];
321
+ const lower: Flat[] = [];
322
+ let depth = 0;
323
+ for (let index = open.from; index <= open.to + 1; index++) {
324
+ const point = index % count;
325
+ upper.push(top[point]!);
326
+ lower.push(bottom[point]!);
327
+ depth += ring[point]![0] * VIEW[0] + ring[point]![1] * VIEW[1];
328
+ }
329
+ strips.push({ depth: depth / upper.length, shade: open.shade, d: pathOf(oriented([...upper, ...lower.reverse()]), true) });
330
+ open = null;
331
+ };
332
+ for (let index = 0; index < count; index++) {
333
+ if (!facing[index]) {
334
+ close();
335
+ continue;
336
+ }
337
+ const [nx, ny] = normals[index]!;
338
+ const lit = (nx * LIGHT[0] + ny * LIGHT[1] + 1) / 2;
339
+ const shade = Math.min(SHADES - 1, Math.max(0, Math.floor(lit * SHADES)));
340
+ if (open && open.shade === shade && index - open.from < STRIP) open.to = index;
341
+ else {
342
+ close();
343
+ open = { from: index, to: index, shade };
344
+ }
345
+ }
346
+ close();
347
+ strips.sort((a, b) => a.depth - b.depth);
348
+ const shades: { d: string; shade: number }[] = [];
349
+ for (const strip of strips) {
350
+ const last = shades[shades.length - 1];
351
+ if (last && last.shade === strip.shade) last.d += strip.d;
352
+ else shades.push({ d: strip.d, shade: strip.shade });
353
+ }
354
+
355
+ const inward = (index: number, by: number): Plane => {
356
+ const [ax, ay] = normals[(index + count - 1) % count]!;
357
+ const [bx, by2] = normals[index]!;
358
+ const mx = ax + bx;
359
+ const my = ay + by2;
360
+ const length = Math.hypot(mx, my) || 1;
361
+ const [x, y] = ring[index]!;
362
+ return [x - (mx / length) * by, y - (my / length) * by];
363
+ };
364
+
365
+ const crease: string[] = [];
366
+ const outline: string[] = [];
367
+ const bevel: string[] = [];
368
+ for (const run of runsOf(facing, false)) outline.push(pathOf(run.map((index) => top[index]!)));
369
+ for (const run of runsOf(facing, true)) {
370
+ crease.push(pathOf(run.map((index) => top[index]!)));
371
+ if (hidden) for (const seen of visibleRuns(ring, run, z, hidden)) outline.push(pathOf(seen));
372
+ else outline.push(pathOf(run.map((index) => bottom[index]!)));
373
+ const along = [0];
374
+ for (let step = 1; step < run.length; step++) {
375
+ const [ax, ay] = ring[run[step - 1]!]!;
376
+ const [bx, by] = ring[run[step]!]!;
377
+ along.push(along[step - 1]! + Math.hypot(bx - ax, by - ay));
378
+ }
379
+ const total = along[along.length - 1]!;
380
+ bevel.push(
381
+ pathOf(
382
+ run.map((index, step) => {
383
+ const near = Math.min(along[step]!, total - along[step]!);
384
+ const [x, y] = inward(index, BEVEL * ease(Math.min(1, near / TAPER)));
385
+ return iso([x, y, z + height], P);
386
+ }),
387
+ ),
388
+ );
389
+ }
390
+ for (let index = 0; index < count; index++) {
391
+ if (facing[(index + count - 1) % count] === facing[index]) continue;
392
+ const [x, y] = ring[index]!;
393
+ let from = z;
394
+ if (hidden && hidden(x, y, z, -index - 1)) {
395
+ let low = z;
396
+ let high = z + height;
397
+ for (let round = 0; round < 12; round++) {
398
+ const mid = (low + high) / 2;
399
+ if (hidden(x, y, mid, -index - 1)) low = mid;
400
+ else high = mid;
401
+ }
402
+ from = high;
403
+ }
404
+ if (z + height - from > 0.05) outline.push(pathOf([top[index]!, iso([x, y, from], P)]));
405
+ }
406
+
407
+ const toward = normals.map(([nx, ny], index) => {
408
+ const [px, py] = normals[(index + count - 1) % count]!;
409
+ return ((nx + px) * LIGHT[0] + (ny + py) * LIGHT[1]) / 2 > SHINE_FACING;
410
+ });
411
+ const shine = runsOf(toward, true)
412
+ .filter((run) => run.length > 3)
413
+ .map((run) => pathOf(run.slice(1, -1).map((index) => iso([...inward(index, SHINE_IN), z + height] as [number, number, number], P))))
414
+ .join("");
415
+
416
+ const cx = ring.reduce((sum, [x]) => sum + x, 0) / count;
417
+ const cy = ring.reduce((sum, [, y]) => sum + y, 0) / count;
418
+ return {
419
+ fill: [pathOf(oriented(top), true), pathOf(oriented(bottom), true)].join(""),
420
+ shades,
421
+ top: pathOf(oriented(top), true),
422
+ crease: crease.join(""),
423
+ bevel: bevel.join(""),
424
+ outline: outline.join(""),
425
+ shine,
426
+ depth: cx * VIEW[0] + cy * VIEW[1],
427
+ };
428
+ }
429
+
430
+ export interface Glint {
431
+ d: string;
432
+ alpha: number;
433
+ }
434
+
435
+ export interface RowShape {
436
+ row: number;
437
+ gap: number;
438
+ joined: boolean;
439
+ glass: GlassShape[];
440
+ neck: Glint[];
441
+ }
442
+
443
+ const atTop = (points: Plane[]) => points.map(([x, y]) => iso([x, y, FLOOR + THICK], P));
444
+
445
+ function concaveRuns(ring: Plane[]): number[][] {
446
+ const count = ring.length;
447
+ const along = [0];
448
+ for (let index = 0; index < count; index++) {
449
+ const [ax, ay] = ring[index]!;
450
+ const [bx, by] = ring[(index + 1) % count]!;
451
+ along.push(along[index]! + Math.hypot(bx - ax, by - ay));
452
+ }
453
+ const total = along[count]!;
454
+ const pointAt = (distance: number): Plane => {
455
+ const s = ((distance % total) + total) % total;
456
+ let low = 0;
457
+ let high = count;
458
+ while (high - low > 1) {
459
+ const mid = (low + high) >> 1;
460
+ if (along[mid]! <= s) low = mid;
461
+ else high = mid;
462
+ }
463
+ const [ax, ay] = ring[low]!;
464
+ const [bx, by] = ring[(low + 1) % count]!;
465
+ const t = (s - along[low]!) / Math.max(along[low + 1]! - along[low]!, 1e-9);
466
+ return [ax + (bx - ax) * t, ay + (by - ay) * t];
467
+ };
468
+ const turn = signedArea(ring) > 0 ? 1 : -1;
469
+ const flags = ring.map(([x, y], index) => {
470
+ const [ax, ay] = pointAt(along[index]! - WINDOW);
471
+ const [bx, by] = pointAt(along[index]! + WINDOW);
472
+ return turn * ((x - ax) * (by - y) - (y - ay) * (bx - x)) < -BEND;
473
+ });
474
+ return runsOf(flags, true).filter((run) => run.length > 2);
475
+ }
476
+
477
+ function glintsOf(points: Flat[]): Glint[] {
478
+ const along = [0];
479
+ for (let index = 1; index < points.length; index++) {
480
+ along.push(along[index - 1]! + Math.hypot(points[index]![0] - points[index - 1]![0], points[index]![1] - points[index - 1]![1]));
481
+ }
482
+ const total = along[along.length - 1]!;
483
+ if (total < 0.5) return [];
484
+ const cut = (from: number, to: number): Flat[] => {
485
+ const out: Flat[] = [];
486
+ const place = (distance: number) => {
487
+ let index = 1;
488
+ while (index < along.length - 1 && along[index]! < distance) index++;
489
+ const t = (distance - along[index - 1]!) / Math.max(along[index]! - along[index - 1]!, 1e-9);
490
+ const [ax, ay] = points[index - 1]!;
491
+ const [bx, by] = points[index]!;
492
+ return [ax + (bx - ax) * t, ay + (by - ay) * t] as Flat;
493
+ };
494
+ out.push(place(from));
495
+ for (let index = 0; index < points.length; index++) if (along[index]! > from && along[index]! < to) out.push(points[index]!);
496
+ out.push(place(to));
497
+ return out;
498
+ };
499
+ const fade = Math.min(total * FADE_SHARE, FADE_MOST);
500
+ const glints: Glint[] = [];
501
+ let before = 0;
502
+ for (let layer = 0; layer < FADE_STEPS; layer++) {
503
+ const now = layer === FADE_STEPS - 1 ? 1 : ease((layer + 1) / FADE_STEPS);
504
+ const alpha = Math.round((1 - (1 - now) / (1 - before)) * 100) / 100;
505
+ before = now;
506
+ const inset = (fade * layer) / (FADE_STEPS - 1);
507
+ glints.push({ d: pathOf(cut(inset, total - inset)), alpha });
508
+ }
509
+ return glints;
510
+ }
511
+
512
+ const cache = new Map<number, RowShape>();
513
+ const CACHE_MOST = 420;
514
+
515
+ export const keyOf = (merge: number) => Math.round(merge * 4);
516
+
517
+ export function rowOf(row: number, merge: number): RowShape {
518
+ const gap = GAPS[row]!;
519
+ const plain = gap >= 2 * merge;
520
+ const key = row * 1000 + (plain ? 999 : keyOf(merge));
521
+ const known = cache.get(key);
522
+ if (known) return known;
523
+ const exact = plain ? 0 : keyOf(merge) / 4;
524
+ const x0 = rowX(row);
525
+ const y0 = rowY(row) + BIG_Y;
526
+ const toWorld = ([u, v]: Plane): Plane => [x0 + v, y0 + u];
527
+ const field = fieldOf(gap, exact);
528
+ const kiss = !plain && keyOf(merge) === gap * 4;
529
+ const level = kiss ? field(RA + gap / 2, KISS) : NUDGE;
530
+ const found = rings(sample(field, gap), level).map((ring) => simplifyRing(ring, 0.06));
531
+ const joined = gap < exact && found.length === 1;
532
+ const whole = found.length === 1;
533
+ const glass = found.map((local) => solidOf(local.map(toWorld), FLOOR, THICK, whole)).sort((a, b) => a.depth - b.depth);
534
+ const neck = joined ? concaveRuns(found[0]!.map(toWorld)).flatMap((run) => glintsOf(atTop(run.map((index) => toWorld(found[0]![index]!))))) : [];
535
+ const shape: RowShape = { row, gap, joined, glass, neck };
536
+ if (cache.size >= CACHE_MOST) cache.delete(cache.keys().next().value!);
537
+ cache.set(key, shape);
538
+ return shape;
539
+ }
540
+
541
+ export const rowsOf = (merge: number) => GAPS.map((_, row) => rowOf(row, merge));
@@ -0,0 +1,84 @@
1
+ import { extrude, iso, pathOf, type Plane } from "../../kit/iso-kit";
2
+ import { BLADE_LONG, BLADE_Z, DECK_H, GAPS, LEAD_H, LEAD_LONG, P, PIVOT, ROWS, angleOf, thicknessOf } from "./rig";
3
+
4
+ const RADIANS = Math.PI / 180;
5
+ const BUTT = 7.2;
6
+ const TIP = 4.2;
7
+ const LEAD_TIP = 3.2;
8
+ const ARC = 10;
9
+
10
+ export interface BladePose {
11
+ fill: string;
12
+ shades: string[];
13
+ top: string;
14
+ crease: string;
15
+ outline: string;
16
+ line: string;
17
+ }
18
+
19
+ export interface FanPose {
20
+ blades: BladePose[];
21
+ halo: string;
22
+ out: boolean[];
23
+ }
24
+
25
+ function outlineOf(length: number, tip: number): Plane[] {
26
+ const out: Plane[] = [];
27
+ for (let step = 0; step <= ARC; step++) {
28
+ const angle = -90 + (180 * step) / ARC;
29
+ out.push([length + tip * Math.cos(angle * RADIANS), tip * Math.sin(angle * RADIANS)]);
30
+ }
31
+ for (let step = 0; step <= ARC; step++) {
32
+ const angle = 90 + (180 * step) / ARC;
33
+ out.push([BUTT * Math.cos(angle * RADIANS), BUTT * Math.sin(angle * RADIANS)]);
34
+ }
35
+ return out;
36
+ }
37
+
38
+ const BLADE = outlineOf(BLADE_LONG, TIP);
39
+ const LEAD = outlineOf(LEAD_LONG, LEAD_TIP);
40
+
41
+ function place(shape: Plane[], degrees: number): Plane[] {
42
+ const a = degrees * RADIANS;
43
+ const dx = Math.sin(a);
44
+ const dy = -Math.cos(a);
45
+ const nx = Math.cos(a);
46
+ const ny = Math.sin(a);
47
+ return shape.map(([u, v]) => [PIVOT[0] + u * dx + v * nx, PIVOT[1] + u * dy + v * ny]);
48
+ }
49
+
50
+ function along(degrees: number, from: number, to: number, z: number) {
51
+ const a = degrees * RADIANS;
52
+ const dx = Math.sin(a);
53
+ const dy = -Math.cos(a);
54
+ return pathOf([iso([PIVOT[0] + from * dx, PIVOT[1] + from * dy, z], P), iso([PIVOT[0] + to * dx, PIVOT[1] + to * dy, z], P)]);
55
+ }
56
+
57
+ function bladeAt(shape: Plane[], degrees: number, z: number, height: number, length: number): BladePose {
58
+ const ring = place(shape, degrees);
59
+ const solid = extrude(ring, z, height, P, { convex: true });
60
+ return {
61
+ fill: solid.fill,
62
+ shades: solid.shades ?? [],
63
+ top: solid.top,
64
+ crease: solid.crease,
65
+ outline: solid.outline,
66
+ line: along(degrees, 13, length - 6, z + height),
67
+ };
68
+ }
69
+
70
+ export function fanOf(merge: number): FanPose {
71
+ const lead = angleOf(merge);
72
+ const blades: BladePose[] = [];
73
+ const halo: string[] = [];
74
+ const out: boolean[] = [];
75
+ GAPS.forEach((gap, index) => {
76
+ const angle = Math.min(lead, angleOf(gap));
77
+ out.push(gap < merge);
78
+ blades.push(bladeAt(BLADE, angle, BLADE_Z[index]!, thicknessOf(gap), BLADE_LONG));
79
+ halo.push(pathOf(place(BLADE, angle).map(([x, y]) => iso([x, y, DECK_H], P)), true));
80
+ });
81
+ blades.push(bladeAt(LEAD, lead, BLADE_Z[ROWS]!, LEAD_H, LEAD_LONG));
82
+ halo.push(pathOf(place(LEAD, lead).map(([x, y]) => iso([x, y, DECK_H], P)), true));
83
+ return { blades, halo: halo.join(""), out };
84
+ }
@@ -0,0 +1,25 @@
1
+ import { Plate } from "../../kit/react/draw";
2
+ import { LiquidArt } from "./art";
3
+ import { GAPS, MERGE, RADII, bladesOutOf, joinedListOf, readoutOf } from "./rig";
4
+ import "./spot-plate.css";
5
+
6
+ const [LARGE, SMALL] = RADII;
7
+
8
+ export function SpotPlateFigure() {
9
+ return (
10
+ <Plate
11
+ fig="Fig 7"
12
+ title="Spot plate"
13
+ hint="Slide across to open the fan"
14
+ readout={readoutOf(MERGE)}
15
+ keys={[
16
+ { mark: "raised", label: `Joined: one piece of glass, R${LARGE} + R${SMALL}` },
17
+ { mark: "edge", label: "The neck, where a pair runs together" },
18
+ { mark: "lit", label: "The merge blade" },
19
+ ]}
20
+ caption={`A spot plate with six channels, each holding a pair of glass drops at gaps from ${GAPS[0]} to ${GAPS[GAPS.length - 1]} px. Each pair is drawn from one distance field, so a pair closer than merge runs together into one piece of glass. The fan sets merge: each feeler blade stops at its own gap. At ${MERGE} px the ${joinedListOf(MERGE)} pairs have joined, so ${bladesOutOf(MERGE)}.`}
21
+ >
22
+ <LiquidArt />
23
+ </Plate>
24
+ );
25
+ }