flowfig 0.10.0 → 0.12.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/dist/world.js ADDED
@@ -0,0 +1,1577 @@
1
+ import { cast, failed, hopsOf, isCheck, moments, question } from './explain.js';
2
+ import { edgeId, isGroup, nodes, str } from './model.js';
3
+ import { HUD } from './looks.js';
4
+ import { textWidth } from './text.js';
5
+ import { BAR_Y, BED_ROWS, C, GL, GZ, INK, LANE, P, PHONE, S, TD, UNIT, gateArt, hull, street, truckArt } from './world-art.js';
6
+ const FORMS = ['box', 'barrel', 'notes'];
7
+ const BED = BED_ROWS.length * 4;
8
+ const units = (n) => Math.min(4, Math.max(1, Math.round(n ?? 1)));
9
+ export function scene(fig, form = 'narrow') {
10
+ const cs = cast(fig);
11
+ const all = nodes(fig.layout);
12
+ const byId = new Map(all.map((n) => [n.id, n]));
13
+ const actorOf = new Map(cs.flatMap((a, i) => a.boxes.map((id) => [id, i])));
14
+ const firstBox = new Map();
15
+ const walk = (g) => {
16
+ for (const c of g.children) {
17
+ if (!isGroup(c))
18
+ continue;
19
+ const first = nodes(c)[0];
20
+ if (c.id && first)
21
+ firstBox.set(c.id, first.id);
22
+ walk(c);
23
+ }
24
+ };
25
+ walk(fig.layout);
26
+ const truck = cs.findIndex((a) => a.look === 'truck');
27
+ const devices = cs.map((a, i) => (i === truck ? 'cab' : a.look === 'person' || a.look === 'phone' ? 'phone' : 'antenna'));
28
+ const stack = form === 'stack' || form === 'spread', { svg: ground, lots, bends, } = street(cs.map((a) => a.look), devices.map((d) => d === 'antenna'), form === 'wide' ? 8.5 : 10.5, stack ? (form === 'spread' ? 15 : 12) : 0);
29
+ const things = new Map();
30
+ for (const e of fig.edges) {
31
+ const c = typeof e.carries === 'object' ? e.carries : undefined;
32
+ if (c && str(c.thing) && !things.has(c.thing))
33
+ things.set(c.thing, { form: FORMS.find((x) => x === c.form) ?? 'box', count: units(c.count) });
34
+ }
35
+ const actors = cs.map((a, i) => {
36
+ const device = devices[i];
37
+ const xs = (k) => lots[k].hull.map((p) => p[0]);
38
+ const left = Math.min(...xs(i));
39
+ const prev = i > 0 && !stack ? Math.max(...xs(i - 1)) : left - 160, dy = lots[i].y;
40
+ const d = (left - Math.min(44, (left - prev) * 0.28)) / C + BAR_Y + dy;
41
+ return {
42
+ name: a.name,
43
+ look: a.look,
44
+ device,
45
+ lot: lots[i],
46
+ gate: { px: d - GL / 2, ex: d + GL / 2, y: BAR_Y + dy, z: GZ, L: GL },
47
+ port: [(left - 28) / C + 4.95 + dy, 4.95 + dy],
48
+ checks: a.boxes.some((id) => isCheck(byId.get(id))),
49
+ };
50
+ });
51
+ const boxes = {};
52
+ cs.forEach((a, i) => a.boxes.forEach((id) => {
53
+ const n = byId.get(id);
54
+ if (n)
55
+ boxes[id] = { label: str(n.label), actor: i, dead: n.tone === 'red' || n.tone === 'gray', check: question(n), done: n.done };
56
+ }));
57
+ const stories = (fig.steps ?? []).map((step) => {
58
+ const label = str(step.label);
59
+ const beats = hopsOf(fig, step);
60
+ const path = new Set();
61
+ const toHop = (h) => {
62
+ const c = typeof h.edge.carries === 'object' && str(h.edge.carries.thing) ? h.edge.carries : undefined;
63
+ const hop = {
64
+ edge: edgeId(h.edge),
65
+ from: h.source,
66
+ to: h.target,
67
+ label: str(h.edge.label),
68
+ data: str(h.data),
69
+ async: !!h.async,
70
+ fail: failed(byId, h),
71
+ };
72
+ if (c) {
73
+ const origin = c.from ? (byId.has(c.from) ? c.from : firstBox.get(c.from)) : h.source;
74
+ hop.carry = { thing: c.thing, ...things.get(c.thing), from: actorOf.get(origin ?? '') ?? -1 };
75
+ }
76
+ for (const a of [actorOf.get(h.source), actorOf.get(h.target), hop.carry?.from])
77
+ if (a !== undefined && a >= 0)
78
+ path.add(a);
79
+ return hop;
80
+ };
81
+ const hops = beats.map((b) => b.hops.map(toHop));
82
+ return {
83
+ name: label,
84
+ moments: hops.length ? [...hops, []] : [],
85
+ acting: moments(fig, label).map((m) => m.actor),
86
+ path: [...path],
87
+ end: hops.at(-1)?.at(-1)?.to ?? '',
88
+ };
89
+ });
90
+ const hopsAll = stories.flatMap((s) => s.moments.flat());
91
+ const sides = (h) => [boxes[h.from]?.actor, boxes[h.to]?.actor];
92
+ const keys = [];
93
+ if (hopsAll.some((h) => !h.carry && new Set(sides(h)).size === 2 && sides(h).every((a) => a !== undefined)))
94
+ keys.push('data');
95
+ if (hopsAll.some((h) => h.carry))
96
+ keys.push('thing');
97
+ if (hopsAll.some((h) => isCheck(byId.get(h.from))))
98
+ keys.push('check');
99
+ const art = truck >= 0 ? truckArt(lots[truck].depot) : null;
100
+ return {
101
+ clear: form === 'wide' ? 64 : CLEAR,
102
+ bends,
103
+ svg: ground +
104
+ actors
105
+ .filter((a) => a.checks)
106
+ .map((a) => gateArt(a.gate))
107
+ .join('') +
108
+ (art?.yard ?? ''),
109
+ actors,
110
+ boxes,
111
+ stories,
112
+ truck,
113
+ truckArt: art,
114
+ things,
115
+ keys,
116
+ };
117
+ }
118
+ const named = (ev, summary) => (summary && ev.done) || ev.label;
119
+ export const CAM = 700;
120
+ const CLEAR = 92;
121
+ function bezier(p1x, p1y, p2x, p2y) {
122
+ const cx = 3 * p1x, bx = 3 * (p2x - p1x) - cx, ax = 1 - cx - bx;
123
+ const cy = 3 * p1y, by = 3 * (p2y - p1y) - cy, ay = 1 - cy - by;
124
+ const X = (t) => ((ax * t + bx) * t + cx) * t;
125
+ const dX = (t) => (3 * ax * t + 2 * bx) * t + cx;
126
+ return (x) => {
127
+ if (x <= 0)
128
+ return 0;
129
+ if (x >= 1)
130
+ return 1;
131
+ let lo = 0, hi = 1, t = x;
132
+ for (let i = 0; i < 24; i++) {
133
+ const e = X(t) - x;
134
+ if (Math.abs(e) < 1e-5)
135
+ break;
136
+ if (e > 0)
137
+ hi = t;
138
+ else
139
+ lo = t;
140
+ const d = dX(t);
141
+ t = Math.abs(d) > 1e-6 ? t - e / d : (lo + hi) / 2;
142
+ if (t <= lo || t >= hi)
143
+ t = (lo + hi) / 2;
144
+ }
145
+ return ((ay * t + by) * t + cy) * t;
146
+ };
147
+ }
148
+ export const ease = { out: bezier(0.23, 1, 0.32, 1), inOut: bezier(0.77, 0, 0.175, 1) };
149
+ const linear = (p) => p;
150
+ const lerp = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
151
+ const bez = (r, t) => {
152
+ const u = 1 - t, k = (i) => u * u * u * r.a[i] + 3 * u * u * t * r.c1[i] + 3 * u * t * t * r.c2[i] + t * t * t * r.b[i];
153
+ return [k(0), k(1)];
154
+ };
155
+ const upright = (t) => 'yDU'.includes(t.orient);
156
+ const mean = (ps) => ps.length ? [ps.reduce((a, p) => a + p[0], 0) / ps.length, ps.reduce((a, p) => a + p[1], 0) / ps.length] : undefined;
157
+ const segLen = (p) => p.slice(1).reduce((l, q, i) => l + Math.hypot(q[0] - p[i][0], q[1] - p[i][1]), 0);
158
+ const along = (p, d) => {
159
+ for (let i = 1; i < p.length; i++) {
160
+ const l = Math.hypot(p[i][0] - p[i - 1][0], p[i][1] - p[i - 1][1]);
161
+ if (d <= l || i === p.length - 1)
162
+ return lerp(p[i - 1], p[i], l ? Math.min(1, d / l) : 1);
163
+ d -= l;
164
+ }
165
+ return p[p.length - 1];
166
+ };
167
+ const yardSpot = (sc, a, k) => [
168
+ sc.actors[a].lot.x + 2.2 + (k % 2) * 0.42,
169
+ 4.84 + Math.floor(k / 2) * 0.38 + sc.actors[a].lot.y,
170
+ ];
171
+ function inPoly(p, poly) {
172
+ let o = false;
173
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
174
+ const [xi, yi] = poly[i], [xj, yj] = poly[j];
175
+ if (yi > p[1] !== yj > p[1] && p[0] < ((xj - xi) * (p[1] - yi)) / (yj - yi) + xi)
176
+ o = !o;
177
+ }
178
+ return o;
179
+ }
180
+ function topAt(poly, x) {
181
+ let y = Infinity;
182
+ for (let i = 0; i < poly.length; i++) {
183
+ const [x0, y0] = poly[i], [x1, y1] = poly[(i + 1) % poly.length];
184
+ if ((x - x0) * (x - x1) <= 0 && x0 !== x1)
185
+ y = Math.min(y, y0 + ((y1 - y0) * (x - x0)) / (x1 - x0));
186
+ }
187
+ return y;
188
+ }
189
+ const coarse = (n) => Array.from({ length: n + 1 }, (_, i) => i).sort((x, y) => (x % 8 ? 1 : 0) - (y % 8 ? 1 : 0));
190
+ const SPOTS = coarse(160);
191
+ function clears(r, hulls, near, cl = CLEAR) {
192
+ for (const n of SPOTS) {
193
+ const p = bez(r, n / 160);
194
+ if (Math.hypot(p[0] - r.a[0], p[1] - r.a[1]) < 10 || Math.hypot(p[0] - r.b[0], p[1] - r.b[1]) < 10)
195
+ continue;
196
+ for (let i = 0; i < hulls.length; i++) {
197
+ if (near.includes(i))
198
+ continue;
199
+ const y = topAt(hulls[i], p[0]);
200
+ if (inPoly(p, hulls[i]) || (p[1] < y && y - p[1] < cl))
201
+ return false;
202
+ }
203
+ }
204
+ return true;
205
+ }
206
+ function route(a, b, hulls, near, cl = CLEAR) {
207
+ for (let top = Math.min(a[1], b[1]) - 80;; top -= 20) {
208
+ const r = { a, b, c1: [a[0], top], c2: [b[0], top] };
209
+ if (top < -1200 || clears(r, hulls, near, cl))
210
+ return r;
211
+ }
212
+ }
213
+ const TALL = [];
214
+ for (let r1 = 80; r1 <= 480; r1 += 80)
215
+ for (let r2 = 80; r2 <= 480; r2 += 80)
216
+ for (let d1 = -240; d1 <= 240; d1 += 120)
217
+ for (let d2 = -240; d2 <= 240; d2 += 120)
218
+ TALL.push([r1, r2, d1, d2]);
219
+ const edgeGap = (p, poly) => Math.min(...poly.map((a, i) => {
220
+ const b = poly[(i + 1) % poly.length], dx = b[0] - a[0], dy = b[1] - a[1], k = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / (dx * dx + dy * dy || 1)));
221
+ return Math.hypot(p[0] - a[0] - k * dx, p[1] - a[1] - k * dy);
222
+ }));
223
+ const COARSE = coarse(96);
224
+ const tallOrder = new Map();
225
+ function tall(a, b, hulls, near, over, side, mid) {
226
+ const sa = Math.sign(mid - a[0]), sb = Math.sign(mid - b[0]), key = `${sa}${sb}`;
227
+ let order = tallOrder.get(key);
228
+ if (!order) {
229
+ const cost = ([r1, r2, p, q]) => r1 + r2 + (Math.abs(p) + Math.abs(q)) / 4 + (p * sa < 0 ? 200 : 0) + (q * sb < 0 ? 200 : 0);
230
+ tallOrder.set(key, (order = TALL.map((c) => [...c, cost(c)]).sort((x, y) => x[4] - y[4])));
231
+ }
232
+ const box = hulls.map(bounds), lift = Math.min(a[1], b[1]) - 60;
233
+ next: for (const [r1, r2, p, q] of order) {
234
+ const r = { a, b, c1: [a[0] + p, a[1] - r1], c2: [b[0] + q, b[1] - r2] }, ps = COARSE.map((n) => bez(r, n / 96));
235
+ if (!ps.some((c) => c[1] <= lift))
236
+ continue;
237
+ for (const c of ps) {
238
+ if (Math.min(Math.hypot(c[0] - a[0], c[1] - a[1]), Math.hypot(c[0] - b[0], c[1] - b[1])) < 16)
239
+ continue;
240
+ for (let i = 0; i < hulls.length; i++) {
241
+ const h = hulls[i], o = box[i];
242
+ if (near.includes(i) || c[0] < o.x - side || c[0] > o.x + o.w + side || c[1] > o.y + o.h + side || c[1] < o.y - over)
243
+ continue;
244
+ const y = topAt(h, c[0]);
245
+ if (inPoly(c, h) || (c[1] < y ? y - c[1] < over : edgeGap(c, h) < side))
246
+ continue next;
247
+ }
248
+ }
249
+ return r;
250
+ }
251
+ }
252
+ function fresh(sc, story) {
253
+ const s = {
254
+ story,
255
+ moment: -1,
256
+ summary: false,
257
+ hi: [],
258
+ ever: {},
259
+ arcs: [],
260
+ units: {},
261
+ truck: null,
262
+ gates: {},
263
+ dev: [],
264
+ screens: {},
265
+ frame: [],
266
+ };
267
+ const depot = sc.truck >= 0 ? sc.actors[sc.truck].lot.depot : null;
268
+ if (depot)
269
+ s.truck = { orient: 'y', x: depot.x + 0.66, y: depot.y - 1.62, lot: sc.truck, door: 1, load: [] };
270
+ sc.actors.forEach((_, a) => (s.gates[a] = { rows: [], deg: 0, fail: false, on: -1 }));
271
+ const seen = new Set();
272
+ for (const h of sc.stories[story].moments.flat()) {
273
+ const c = h.carry;
274
+ const a = c && c.from >= 0 ? c.from : sc.boxes[h.from]?.actor;
275
+ if (!c || seen.has(c.thing) || a === undefined)
276
+ continue;
277
+ seen.add(c.thing);
278
+ for (let k = 0; k < c.count; k++) {
279
+ const id = `${c.thing}/${k}`, bed = a === sc.truck && c.form !== 'notes' && s.truck && s.truck.load.length < BED ? s.truck : null;
280
+ s.units[id] = {
281
+ thing: c.thing,
282
+ k,
283
+ p: bed ? [0, 0] : P(...yardSpot(sc, a, k)),
284
+ where: bed ? 'bed' : 'yard',
285
+ at: a,
286
+ fail: false,
287
+ moment: -1,
288
+ };
289
+ if (bed)
290
+ bed.load.push(id);
291
+ }
292
+ }
293
+ return s;
294
+ }
295
+ function kit(sc, s) {
296
+ const A = sc.actors, T = sc.truck, depot = T >= 0 ? A[T].lot.depot : { x: 0, y: 0 }, YX = depot.x + 0.66;
297
+ const parked = () => !s.truck || (s.truck.orient === 'y' && s.truck.y + 2.5 <= depot.y + 0.9);
298
+ const inCab = (a) => a === T && !parked();
299
+ const cabBase = (t) => upright(t)
300
+ ? P(t.x + TD + 0.03, t.y + (t.orient === 'U' ? 0.33 : 2.17), 0.58)
301
+ : P(t.orient === 'R' ? t.x + 2.18 : t.x + 0.31, t.y + TD / 2 + 0.41, 0.58);
302
+ const devBase = (a) => (inCab(a) ? cabBase(s.truck) : P(...A[a].port));
303
+ return {
304
+ YX,
305
+ parked,
306
+ inCab,
307
+ devBase,
308
+ devTop: (a) => {
309
+ const b = devBase(a);
310
+ return [b[0], b[1] - PHONE.h * (inCab(a) ? 0.82 : 1) - 4];
311
+ },
312
+ gateMid: (a) => {
313
+ const g = A[a].gate;
314
+ return P((g.px + g.ex) / 2, g.y, g.z);
315
+ },
316
+ gateEnds: (a) => {
317
+ const g = A[a].gate;
318
+ return [P(g.ex, g.y, g.z - 0.05), P(g.ex, g.y)];
319
+ },
320
+ truckPts: (t, g = 1) => {
321
+ const c = t.y + TD / 2, w = upright(t)
322
+ ? [t.x, t.x + TD].flatMap((x) => [P(x, t.y), P(x, t.y + 2.5), P(x, t.y, t.orient === 'y' ? 0.9 : 1.13), P(x, t.y + 2.5, 1.13)])
323
+ : [t.x, t.x + 2.5].flatMap((x) => [c - 0.4, c + 0.4].flatMap((y) => [P(x, y), P(x, y, 1.13)]));
324
+ if (s.dev.includes(T)) {
325
+ const b = cabBase(t);
326
+ w.push([b[0] - 11 * g, b[1] - 40 * g], [b[0] + 11 * g, b[1] - 40 * g]);
327
+ }
328
+ return w;
329
+ },
330
+ };
331
+ }
332
+ const answer = (h) => h.label.charAt(0).toUpperCase() + h.label.slice(1) || (h.fail ? '✗' : '✓');
333
+ const plans = new WeakMap();
334
+ function plan(sc, story, m) {
335
+ const memo = plans.get(sc) ?? new Map();
336
+ plans.set(sc, memo);
337
+ const key = `${story}/${m}`;
338
+ if (!memo.has(key))
339
+ memo.set(key, planOf(sc, story, m));
340
+ return memo.get(key);
341
+ }
342
+ function planOf(sc, story, m) {
343
+ const st = sc.stories[story], hops = st.moments[m] ?? [], A = sc.actors, T = sc.truck;
344
+ const start = stateAt(sc, story, m - 1, Infinity);
345
+ start.moment = m;
346
+ start.summary = !hops.length && m === st.moments.length - 1;
347
+ start.arcs = start.arcs.filter((a) => a.hold || start.summary);
348
+ start.frame = start.arcs.filter((a) => !start.summary || a.fail).map((a) => a.b);
349
+ start.hi = [];
350
+ start.dev = [];
351
+ const s = structuredClone(start), steps = [];
352
+ const add = (kind, hop, at, ms, apply, curve = ease.inOut, head) => {
353
+ steps.push({ kind, hop, start: at, end: at + ms, curve, apply, head });
354
+ apply(s, 1);
355
+ return at + ms;
356
+ };
357
+ const set = (hop, at, apply) => add(null, hop, at, 0, apply);
358
+ const { YX, gateMid, gateEnds, devBase, devTop, truckPts } = kit(sc, s), hulls = A.map((a) => a.lot.hull), mid = mean(hulls.flat())[0];
359
+ const bedAt = (t, i) => P(t.x + sc.truckArt.slots[t.orient === 'R' ? 'R' : 'L'][i % 4], BED_ROWS[Math.floor(i / 4)] + t.y + TD / 2 - LANE, 0.43);
360
+ const truckRef = (t) => (t.orient === 'y' ? t.x : t.orient === 'R' ? t.x + 2.18 : t.x + 0.31);
361
+ const front = (t) => upright(t) ? P(t.x + TD / 2, t.y + (t.orient === 'U' ? 0 : 2.5), 0.6) : P(t.orient === 'R' ? t.x + 2.5 : t.x, t.y + TD / 2, 0.6);
362
+ const road = (a, b, o) => {
363
+ const B = sc.bends, out = [];
364
+ for (let k = a; B.length && a !== b; k += b > a ? 1 : -1) {
365
+ out.push([B[k][0] - o, B[k][1] - o]);
366
+ if (k === b)
367
+ break;
368
+ }
369
+ return out;
370
+ };
371
+ const hl = (...as) => as.forEach((a) => {
372
+ if (!s.hi.includes(a))
373
+ s.hi.push(a);
374
+ s.frame.push(...A[a].lot.hull);
375
+ });
376
+ const idsOf = (thing) => Object.keys(s.units).filter((id) => s.units[id].thing === thing);
377
+ const moving = (ids) => (st) => mean(ids.filter((id) => ['air', 'road'].includes(st.units[id].where)).map((id) => st.units[id].p));
378
+ const hopUnit = (i, at, id, ids, from, to, where, ta) => add('unit', i, at, 460, (st, p) => {
379
+ const u = st.units[id], t = st.truck, q = lerp(from, to, p);
380
+ u.p = [q[0], q[1] - 46 * 4 * p * (1 - p)];
381
+ u.where = p < 1 ? 'air' : where;
382
+ u.at = ta;
383
+ t.load = t.load.filter((x) => x !== id);
384
+ if (where === 'bed' && p === 1)
385
+ t.load.push(id);
386
+ }, ease.inOut, moving(ids));
387
+ const endpoint = (i, at, a, box, near) => {
388
+ if (sc.boxes[box].check) {
389
+ s.frame.push(...gateEnds(a));
390
+ return gateMid(a);
391
+ }
392
+ if (A[a].device === 'antenna') {
393
+ near.push(a);
394
+ return A[a].lot.roof;
395
+ }
396
+ set(i, at, (st) => {
397
+ if (!st.dev.includes(a))
398
+ st.dev.push(a);
399
+ st.screens[a] ??= 'idle';
400
+ });
401
+ s.frame.push(devBase(a), devTop(a));
402
+ return devTop(a);
403
+ };
404
+ const pulse = (kind, i, at, a, b, extra, near) => {
405
+ const r = sc.bends.length
406
+ ? (tall(a, b, hulls, near, CLEAR, 14, mid) ?? tall(a, b, hulls, near, 0, 0, mid) ?? route(a, b, hulls, near))
407
+ : route(a, b, hulls, near, sc.clear), ps = Array.from({ length: 9 }, (_, k) => bez(r, k / 8));
408
+ s.frame.push(...ps);
409
+ return add(kind, i, at, Math.min(1500, 450 + 0.7 * segLen(ps)), (st, t) => void st.arcs.push({ ...r, ...extra, t, moment: m, near }), ease.inOut, (_, t) => bez(r, t));
410
+ };
411
+ const drive = (i, at, a) => {
412
+ const t = s.truck, head = (st) => front(st.truck), lot = A[a].lot, plan = [[truckRef(t), LANE + A[t.lot].lot.y], ...road(t.lot, a, 0.4), [lot.door, LANE + lot.y]];
413
+ for (let k = plan.length - 2; k > 0; k--) {
414
+ const [p, q, r] = plan.slice(k - 1, k + 2);
415
+ if (Math.abs((q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])) < 1e-6)
416
+ plan.splice(k, 1);
417
+ }
418
+ const orient = plan[1][0] < plan[0][0] ? 'L' : 'R', goL = plan.at(-1)[0] < plan.at(-2)[0];
419
+ s.frame.push(...truckPts(t));
420
+ if (t.orient === 'y') {
421
+ const y0 = t.y, y1 = A[T].lot.depot.y + 0.53;
422
+ if (t.door < 1)
423
+ at = add('door', i, at, 500, (st, p) => void (st.truck.door = p), ease.inOut, head);
424
+ at = add('drive', i, at, 1000, (st, p) => {
425
+ const tr = st.truck;
426
+ tr.y = y0 + (y1 - y0) * p;
427
+ if (p === 1)
428
+ Object.assign(tr, { orient, x: orient === 'R' ? YX - 1.78 : YX, y: LANE - TD / 2 + A[T].lot.y });
429
+ }, ease.inOut, head);
430
+ }
431
+ else if (t.orient !== orient) {
432
+ const x = t.x + (orient === 'R' ? -1.78 : 1.78);
433
+ set(i, at, (st) => Object.assign(st.truck, { orient, x }));
434
+ }
435
+ const x1 = goL ? lot.x + 0.95 : lot.x + 0.2, bends = plan.slice(1, -1), way = [[t.x + 1.25, t.y + TD / 2], ...bends, [x1 + 1.25, LANE + lot.y]];
436
+ for (const [x, y] of bends)
437
+ s.frame.push(...truckPts({ ...t, orient: 'D', x: x - TD / 2, y: y - 1.25 }));
438
+ s.frame.push(...truckPts({ ...t, orient: goL ? 'L' : 'R', x: x1, y: LANE - TD / 2 + lot.y }));
439
+ return add('drive', i, at, Math.min(2200, 900 + segLen(way) * 60), (st, p) => {
440
+ const tr = st.truck, d = segLen(way) * p, [x, y] = along(way, d);
441
+ let k = 1;
442
+ while (k < way.length - 1 && segLen(way.slice(0, k + 1)) < d)
443
+ k++;
444
+ const dx = way[k][0] - way[k - 1][0], dy = way[k][1] - way[k - 1][1], o = Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 'R' : 'L') : dy > 0 ? 'D' : 'U';
445
+ Object.assign(tr, o === 'D' || o === 'U' ? { orient: o, x: x - TD / 2, y: y - 1.25 } : { orient: o, x: x - 1.25, y: y - TD / 2 });
446
+ if (p === 1)
447
+ tr.lot = a;
448
+ }, ease.inOut, head);
449
+ };
450
+ const driveTo = (i, at, a) => {
451
+ const t = s.truck;
452
+ if (t.orient !== 'y' && t.lot === a) {
453
+ s.frame.push(...truckPts(t));
454
+ return at;
455
+ }
456
+ return drive(i, at, a);
457
+ };
458
+ const carry = (i, at, h, sa, ta) => {
459
+ const ids = idsOf(h.carry.thing), toGate = !!sc.boxes[h.to].check, where = toGate ? 'gate' : 'yard';
460
+ if (!ids.length)
461
+ return at;
462
+ const dest = (k) => toGate ? [(A[ta].gate.px + A[ta].gate.ex) / 2 + 0.1 - k * 0.42, 6.45 + A[ta].lot.y] : yardSpot(sc, ta, k);
463
+ set(i, at, (st) => ids.forEach((id) => (st.units[id].moment = m)));
464
+ if (toGate)
465
+ set(i, at, (st) => void (st.gates[ta].on = m));
466
+ const truck = h.carry.form !== 'notes' && s.truck;
467
+ if (truck && ta === T) {
468
+ const t0 = driveTo(i, at, sa), base = truck.load.length;
469
+ const fit = ids.slice(0, Math.max(0, BED - base));
470
+ s.frame.push(...fit.map((id) => s.units[id].p));
471
+ return Math.max(t0, ...fit.map((id, k) => hopUnit(i, t0 + 280 * k, id, fit, s.units[id].p, bedAt(s.truck, base + k), 'bed', ta)));
472
+ }
473
+ if (truck && sa === T) {
474
+ const t0 = driveTo(i, at, ta);
475
+ return Math.max(...ids.map((id, k) => {
476
+ const slot = s.truck.load.indexOf(id);
477
+ return hopUnit(i, t0 + 280 * k, id, ids, slot < 0 ? s.units[id].p : bedAt(s.truck, slot), P(...dest(k)), where, ta);
478
+ }));
479
+ }
480
+ const paths = ids.map((id, k) => {
481
+ const [x, y] = s.units[id].p, ux = (x / C + y / S) / 2, uy = (y / S - x / C) / 2, [tx, ty] = dest(k);
482
+ return [[ux, uy], [ux, 6.75 + A[s.units[id].at].lot.y], ...road(s.units[id].at, ta, 0.25), [tx, 6.75 + A[ta].lot.y], [tx, ty]];
483
+ });
484
+ paths.forEach((p) => s.frame.push(...p.map((q) => P(...q)), P(...p.at(-1), 1.2)));
485
+ return add('parcel', i, at, Math.min(2600, 700 + Math.max(...paths.map(segLen)) * 130), (st, e) => ids.forEach((id, k) => {
486
+ const u = st.units[id], d = segLen(paths[k]) * e, q = P(...along(paths[k], d));
487
+ u.p = [q[0], q[1] - (e < 1 ? Math.abs(Math.sin(d * Math.PI * 1.6)) * 5 : 0)];
488
+ u.where = e < 1 ? 'road' : where;
489
+ u.at = ta;
490
+ }), ease.inOut, moving(ids));
491
+ };
492
+ const hop = (i, at) => {
493
+ const h = hops[i], fb = sc.boxes[h.from], tb = sc.boxes[h.to];
494
+ if (!fb || !tb)
495
+ return at;
496
+ const fa = fb.actor, ta = tb.actor, sa = (h.carry && Object.values(s.units).find((u) => u.thing === h.carry.thing)?.at) ?? fa;
497
+ const reach = (a, b) => (st) => void (st.ever[a] = { label: b.label, fail: b.dead, done: b.done ?? (b.check ? b.label.replace(/\?$/, '') : undefined) });
498
+ set(i, at, reach(fa, fb));
499
+ hl(sa, ta);
500
+ if (fb.check) {
501
+ hl(fa);
502
+ s.frame.push(...gateEnds(fa));
503
+ add('answer', i, at, 0, (st) => {
504
+ const g = st.gates[fa], row = g.rows.find((r) => r.box === h.from) ?? g.rows[g.rows.push({ box: h.from, q: fb.check, a: '', fail: false }) - 1];
505
+ Object.assign(row, { a: answer(h), fail: h.fail });
506
+ g.on = m;
507
+ st.ever[fa].fail = h.fail || /^(no|false)$/i.test(h.label);
508
+ });
509
+ if (h.fail) {
510
+ for (const u of Object.values(s.units))
511
+ if (u.where === 'gate' && u.at === fa)
512
+ s.frame.push(u.p);
513
+ add(null, i, at, 160, (st, p) => st.arcs.forEach((x) => {
514
+ if (x.gate === fa && x.hold)
515
+ x.t = p < 1 ? 1 - (8 / segLen(Array.from({ length: 9 }, (_, k) => bez(x, k / 8)))) * Math.sin(Math.PI * p) : 1;
516
+ }));
517
+ at = add('fail', i, at, 90, (st, p) => {
518
+ st.gates[fa].deg = 12 * p;
519
+ if (p < 1)
520
+ return;
521
+ st.gates[fa].fail = true;
522
+ st.arcs.forEach((x) => (x.fail ||= x.gate === fa && x.hold));
523
+ for (const u of Object.values(st.units))
524
+ u.fail ||= u.where === 'gate' && u.at === fa;
525
+ st.ever[fa] = { label: ta === fa ? tb.label : h.label ? answer(h) : fb.label, fail: true };
526
+ }, ease.out);
527
+ const kw = (2 * Math.PI) / 0.25, kd = kw * Math.sqrt(0.75);
528
+ at = add('arm', i, at, 290, (st, p) => {
529
+ const T = p * 0.29;
530
+ st.gates[fa].deg =
531
+ p < 1 ? Math.max(0, 12 * Math.exp(-0.5 * kw * T) * (Math.cos(kd * T) + ((0.5 * kw) / kd) * Math.sin(kd * T))) : 0;
532
+ }, linear);
533
+ if (ta !== fa) {
534
+ const near = [];
535
+ at = pulse('reply', i, at, gateMid(fa), endpoint(i, at, ta, h.to, near), { gate: -1, hold: false, fail: true, text: tb.label, dashed: h.async, to: ta }, near);
536
+ }
537
+ set(i, at, reach(ta, tb));
538
+ return at;
539
+ }
540
+ at += 380;
541
+ const z = 0.8, w = (2 * Math.PI) / 0.4, wd = w * Math.sqrt(1 - z * z);
542
+ at = add('arm', i, at, 460, (st, p) => {
543
+ const T = p * 0.46;
544
+ st.gates[fa].deg = p < 1 ? 78 * (1 - Math.exp(-z * w * T) * (Math.cos(wd * T) + ((z * w) / wd) * Math.sin(wd * T))) : 78;
545
+ }, linear);
546
+ set(i, at, (st) => st.arcs.forEach((x) => (x.hold &&= x.gate !== fa)));
547
+ const us = Object.keys(s.units).filter((id) => s.units[id].where === 'gate' && s.units[id].at === fa);
548
+ if (us.length) {
549
+ const from = us.map((id) => s.units[id].p), to = P(A[fa].lot.door, 4.75 + A[fa].lot.y);
550
+ at = add('release', i, at, 520, (st, p) => us.forEach((id, k) => {
551
+ st.units[id].p = lerp(from[k], to, p);
552
+ st.units[id].where = p < 1 ? 'gate' : 'in';
553
+ }), ease.inOut, (st) => mean(us.map((id) => st.units[id].p)));
554
+ }
555
+ }
556
+ if (sa !== ta && h.carry)
557
+ at = carry(i, at, h, sa, ta);
558
+ else if (sa !== ta) {
559
+ const near = [], from = endpoint(i, at, sa, h.from, near), to = endpoint(i, at, ta, h.to, near), gate = !!tb.check, tx = !fb.check && A[sa].device !== 'antenna';
560
+ if (gate)
561
+ set(i, at, (st) => void (st.gates[ta].on = m));
562
+ if (tx)
563
+ set(i, at, (st) => void (st.screens[sa] = 'tx'));
564
+ at = pulse('pulse', i, at, from, to, { gate: gate ? ta : -1, hold: gate, fail: false, text: fb.check ? '' : h.data || h.label, dashed: h.async, to: ta }, near);
565
+ if (tx)
566
+ set(i, at, (st) => void (st.screens[sa] = 'idle'));
567
+ if (!gate)
568
+ set(i, at, (st) => void (st.dev.includes(ta) && (st.screens[ta] = 'in')));
569
+ }
570
+ if (tb.check) {
571
+ const reset = sa !== ta, d0 = s.gates[ta].deg;
572
+ s.frame.push(...gateEnds(ta));
573
+ if (d0 > 0)
574
+ at = add('arm', i, at, 260, (st, p) => void (st.gates[ta].deg = d0 * (1 - p)));
575
+ add('question', i, at, 0, (st) => {
576
+ const g = st.gates[ta];
577
+ if (reset)
578
+ Object.assign(g, { rows: [], fail: false });
579
+ if (!g.rows.some((r) => r.box === h.to))
580
+ g.rows.push({ box: h.to, q: tb.check, a: '', fail: false });
581
+ g.on = m;
582
+ });
583
+ at += 250;
584
+ const out = hops.some((x) => x.from === h.to)
585
+ ? undefined
586
+ : st.moments
587
+ .slice(m + 1)
588
+ .flat()
589
+ .find((x) => x.from === h.to);
590
+ if (out)
591
+ add('answer', i, at, 0, (x) => {
592
+ const row = x.gates[ta].rows.find((r) => r.box === h.to);
593
+ if (row)
594
+ Object.assign(row, { a: answer(out), fail: out.fail });
595
+ });
596
+ }
597
+ set(i, at, reach(ta, tb));
598
+ return at;
599
+ };
600
+ const c0 = s.summary ? 200 : 0;
601
+ if (s.summary)
602
+ add(null, -1, 0, c0, (st, p) => {
603
+ st.fade = 1 - p;
604
+ if (p === 1)
605
+ st.arcs = st.arcs.filter((a) => a.fail);
606
+ }, linear);
607
+ add('camera', -1, c0, s.summary ? 900 : CAM, () => { }, ease.out);
608
+ const ends = [];
609
+ hops.forEach((h, i) => {
610
+ let dep = -1;
611
+ for (let j = 0; j < i; j++)
612
+ if (hops[j].to === h.from)
613
+ dep = j;
614
+ ends[i] = hop(i, dep >= 0 ? ends[dep] : CAM);
615
+ });
616
+ if (s.summary) {
617
+ for (const a of st.path)
618
+ s.frame.push(...A[a].lot.hull);
619
+ if (s.truck && st.path.includes(T))
620
+ s.frame.push(...truckPts(s.truck));
621
+ for (const u of Object.values(s.units))
622
+ if (['yard', 'gate', 'in'].includes(u.where))
623
+ s.frame.push(u.p, [u.p[0], u.p[1] + 40]);
624
+ A.forEach((_, a) => s.gates[a].fail && s.frame.push(...gateEnds(a)));
625
+ }
626
+ return { steps, start, scratch: s, end: Math.max(s.summary ? c0 + 900 : CAM, ...ends) };
627
+ }
628
+ export function timeline(sc, story, moment) {
629
+ const { steps, end } = plan(sc, story, moment);
630
+ return { events: steps.flatMap(({ kind, hop, start, end }) => (kind ? [{ kind, hop, start, end }] : [])), end };
631
+ }
632
+ export function stateAt(sc, story, moment, t) {
633
+ if (moment < 0)
634
+ return fresh(sc, story);
635
+ const { steps, start, scratch } = plan(sc, story, moment);
636
+ const s = structuredClone(start), hopEnd = [];
637
+ for (const e of steps)
638
+ if (e.hop >= 0)
639
+ hopEnd[e.hop] = Math.max(hopEnd[e.hop] ?? 0, e.end);
640
+ let best = -1;
641
+ for (const e of [...steps].sort((a, b) => a.start - b.start)) {
642
+ if (e.start > t)
643
+ continue;
644
+ const raw = e.end > e.start ? Math.min(1, (t - e.start) / (e.end - e.start)) : 1, p = e.curve(raw);
645
+ e.apply(s, p);
646
+ const head = raw < 1 && e.hop >= 0 && hopEnd[e.hop] >= best ? e.head?.(s, p) : undefined;
647
+ if (head)
648
+ [s.lead, best] = [head, hopEnd[e.hop]];
649
+ }
650
+ s.frame = [...scratch.frame];
651
+ s.hi = s.summary ? (t === Infinity ? [...sc.stories[story].path] : []) : [...scratch.hi];
652
+ return s;
653
+ }
654
+ export const ZMIN = 0.45;
655
+ const OVER = new Error('over');
656
+ const GAP = 12, MARGIN = 32, TYPE = {
657
+ big: { name: 14.8, hero: 16, kick: 12, status: 13.8, mark: 20, tag: 46, q: 15, a: 13, row: 30, chip: 13, chipH: 26 },
658
+ small: { name: 13, hero: 14, kick: 0, status: 12, mark: 18, tag: 41, q: 13, a: 12, row: 24, chip: 12, chipH: 22 },
659
+ };
660
+ const chipped = (u, summary) => u.where === 'yard' || u.where === 'gate' || (summary && (u.where === 'bed' || u.where === 'in'));
661
+ const phoneScale = (z, st) => (st.narrow ? 1 : Math.min(1.9, Math.max(1, 0.85 / z)));
662
+ const nearest = (r, p) => [Math.max(r.x, Math.min(p[0], r.x + r.w)), Math.max(r.y, Math.min(p[1], r.y + r.h))];
663
+ const rings = new Map();
664
+ function ring(p, w, h, ds) {
665
+ const key = `${w},${h},${ds}`;
666
+ let off = rings.get(key);
667
+ if (!off) {
668
+ const side = (c, size) => (c > 0.38 ? [0] : c < -0.38 ? [size] : [size / 2, size * 0.1, size * 0.9]);
669
+ off = [];
670
+ for (const d of ds)
671
+ for (let i = 0; i < 16; i++) {
672
+ const c = Math.cos((i * Math.PI) / 8), n = Math.sin((i * Math.PI) / 8);
673
+ for (const sx of side(c, w))
674
+ for (const sy of side(n, h))
675
+ off.push(c * d - sx, n * d - sy);
676
+ }
677
+ rings.set(key, off);
678
+ }
679
+ const out = [];
680
+ for (let i = 0; i < off.length; i += 2)
681
+ out.push({ x: p[0] + off[i], y: p[1] + off[i + 1], w, h, anchor: p });
682
+ return out;
683
+ }
684
+ const area = (ps) => Math.abs(ps.reduce((n, p, i) => n + p[0] * ps[(i + 1) % ps.length][1] - ps[(i + 1) % ps.length][0] * p[1], 0)) / 2;
685
+ function share(ps, { l, t, r, b }) {
686
+ let out = ps;
687
+ for (const [k, v, lo] of [
688
+ [0, l, 1],
689
+ [0, r, 0],
690
+ [1, t, 1],
691
+ [1, b, 0],
692
+ ]) {
693
+ const keep = (p) => (lo ? p[k] >= v : p[k] <= v), next = [];
694
+ out.forEach((p, i) => {
695
+ const q = out[(i + 1) % out.length];
696
+ if (keep(p))
697
+ next.push(p);
698
+ if (keep(p) !== keep(q)) {
699
+ const u = (v - p[k]) / (q[k] - p[k]);
700
+ next.push([p[0] + (q[0] - p[0]) * u, p[1] + (q[1] - p[1]) * u]);
701
+ }
702
+ });
703
+ out = next;
704
+ }
705
+ return out.length > 2 ? area(out) / area(ps) : 0;
706
+ }
707
+ const bounds = (ps) => {
708
+ const xs = ps.map((p) => p[0]), ys = ps.map((p) => p[1]), x = Math.min(...xs), y = Math.min(...ys);
709
+ return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
710
+ };
711
+ // Measured system-ui widths in Chromium run up to 1.105x the textWidth estimate.
712
+ const widths = new Map();
713
+ const width = (text, size, bold) => {
714
+ const k = `${size}${bold}${text}`;
715
+ let w = widths.get(k);
716
+ if (w === undefined)
717
+ widths.set(k, (w = textWidth(text, size) * (bold ? 1.06 : 1) * 1.12));
718
+ return w;
719
+ };
720
+ export const hit = (a, b, pad = 0) => a.x < b.x + b.w + pad && b.x < a.x + a.w + pad && a.y < b.y + b.h + pad && b.y < a.y + a.h + pad;
721
+ const axes = new WeakMap();
722
+ export function polyHit(poly, r, pad = 0) {
723
+ const x0 = r.x - pad, y0 = r.y - pad, x1 = r.x + r.w + pad, y1 = r.y + r.h + pad;
724
+ let ax = axes.get(poly);
725
+ if (!ax) {
726
+ let px0 = Infinity, px1 = -Infinity, py0 = Infinity, py1 = -Infinity;
727
+ for (const [x, y] of poly) {
728
+ px0 = Math.min(px0, x);
729
+ px1 = Math.max(px1, x);
730
+ py0 = Math.min(py0, y);
731
+ py1 = Math.max(py1, y);
732
+ }
733
+ ax = [px0, px1, py0, py1];
734
+ for (let i = 0; i < poly.length; i++) {
735
+ const [qx, qy] = poly[i], [bx, by] = poly[(i + 1) % poly.length], nx = by - qy, ny = qx - bx;
736
+ let p0 = Infinity, p1 = -Infinity;
737
+ for (const [x, y] of poly) {
738
+ const v = x * nx + y * ny;
739
+ if (v < p0)
740
+ p0 = v;
741
+ if (v > p1)
742
+ p1 = v;
743
+ }
744
+ ax.push(nx, ny, p0, p1);
745
+ }
746
+ axes.set(poly, ax);
747
+ }
748
+ if (ax[1] <= x0 || x1 <= ax[0] || ax[3] <= y0 || y1 <= ax[2])
749
+ return false;
750
+ for (let i = 4; i < ax.length; i += 4) {
751
+ const nx = ax[i], ny = ax[i + 1], lo = Math.min(x0 * nx, x1 * nx) + Math.min(y0 * ny, y1 * ny), hi = Math.max(x0 * nx, x1 * nx) + Math.max(y0 * ny, y1 * ny);
752
+ if (ax[i + 3] <= lo || hi <= ax[i + 2])
753
+ return false;
754
+ }
755
+ return true;
756
+ }
757
+ export function stageFor(w, h, sheet = HUD.sheet) {
758
+ const side = w >= HUD.wide || (h < HUD.short && w >= HUD.slim * 2);
759
+ return {
760
+ w,
761
+ h,
762
+ narrow: w < HUD.roomy,
763
+ form: w >= HUD.slim * 2 ? (w < HUD.roomy ? 'narrow' : 'wide') : h < 740 || h > 780 ? 'spread' : 'stack',
764
+ region: side
765
+ ? { l: HUD.pad * 2 + (w >= HUD.wide ? HUD.panel : HUD.slim), t: HUD.chips + HUD.pad, r: w - HUD.pad, b: h - HUD.pad }
766
+ : { l: 8, t: HUD.chips + 8, r: w - 8, b: h - sheet - 8 },
767
+ };
768
+ }
769
+ const samples = new WeakMap();
770
+ const chunks = new WeakMap();
771
+ const solidsOf = new WeakMap();
772
+ export function layout(sc, s, stage, cam, full = false, cap = Infinity) {
773
+ const ty = TYPE[stage.w < HUD.slim * 2 ? 'small' : 'big'], CHIP_H = ty.chipH;
774
+ const A = sc.actors, story = sc.stories[s.story], k = kit(sc, s), { l, t, r, b } = stage.region, cx = (l + r) / 2, cy = (t + b) / 2;
775
+ const pr = (p) => [(p[0] - cam.x) * cam.z + cx, (p[1] - cam.y) * cam.z + cy];
776
+ const span = (ps) => bounds(ps.map(pr));
777
+ const truckW = s.truck && !k.parked() ? k.truckPts(s.truck, phoneScale(cam.z, stage)) : null;
778
+ const gates = A.flatMap((a, i) => {
779
+ const g = a.gate, st = s.gates[i];
780
+ if (!a.checks || (st.on !== s.moment && !st.fail))
781
+ return [];
782
+ const up = st.deg > 1 ? g.z + g.L : st.fail ? g.z + 1 : g.z + 0.3;
783
+ return [{ actor: i, r: span([P(g.px - 0.35, g.y), P(g.ex + 0.12, g.y), P(g.px, g.y, up), P(g.ex, g.y, g.z)]) }];
784
+ });
785
+ const grow = phoneScale(cam.z, stage);
786
+ const devices = s.dev
787
+ .filter((a) => !k.inCab(a))
788
+ .map((a) => {
789
+ const p = k.devBase(a);
790
+ return {
791
+ actor: a,
792
+ r: span([
793
+ [p[0] - (PHONE.w * grow) / 2, p[1] - PHONE.h * grow],
794
+ [p[0] + (PHONE.w * grow) / 2, p[1]],
795
+ ]),
796
+ };
797
+ });
798
+ let arcsW = samples.get(s.arcs);
799
+ if (!arcsW)
800
+ samples.set(s.arcs, (arcsW = s.arcs
801
+ .filter((a) => a.t > 0)
802
+ .map((a) => sc.bends.length
803
+ ? Array.from({ length: 97 }, (_, i) => bez(a, (a.t * i) / 96))
804
+ : Array.from({ length: 33 }, (_, i) => bez(a, a.t * (0.05 + (0.9 * i) / 32))))));
805
+ let lots = solidsOf.get(sc);
806
+ if (!lots)
807
+ solidsOf.set(sc, (lots = A.map((a) => ({ h: a.lot.hull, box: bounds(a.lot.hull) }))));
808
+ const taken = [...devices, ...gates].map((d) => d.r);
809
+ let bad = 0;
810
+ const inRegion = (o) => o.x >= l && o.y >= t && o.x + o.w <= r && o.y + o.h <= b;
811
+ const truckHull = truckW ? hull(truckW) : undefined;
812
+ const solids = truckHull ? [...lots, { h: truckHull, box: bounds(truckHull) }] : lots;
813
+ const world = (o) => ({ x: (o.x - cx) / cam.z + cam.x, y: (o.y - cy) / cam.z + cam.y, w: o.w / cam.z, h: o.h / cam.z });
814
+ const offB = (o) => {
815
+ const q = world(o), p = 2 / cam.z;
816
+ return !solids.some(({ h, box }) => hit(box, q, p) && polyHit(h, q, p));
817
+ };
818
+ let parts = chunks.get(arcsW);
819
+ if (!parts)
820
+ chunks.set(arcsW, (parts = arcsW.flatMap((arc) => Array.from({ length: Math.ceil(arc.length / 8) }, (_, k) => {
821
+ const ps = arc.slice(k * 8, k * 8 + 8);
822
+ return { ps, box: bounds(ps) };
823
+ }))));
824
+ const offA = (o) => {
825
+ const q = world(o), p = 4 / cam.z;
826
+ const x0 = q.x - p, x1 = q.x + q.w + p, y0 = q.y - p, y1 = q.y + q.h + p;
827
+ for (const { ps, box } of parts)
828
+ if (hit(box, q, p))
829
+ for (const c of ps)
830
+ if (c[0] > x0 && c[0] < x1 && c[1] > y0 && c[1] < y1)
831
+ return false;
832
+ return true;
833
+ };
834
+ const free = (o) => !taken.some((x) => hit(x, o, 6));
835
+ const ok = (o) => inRegion(o) && free(o) && offA(o) && offB(o);
836
+ const scan = (cands, test) => {
837
+ for (const c of cands)
838
+ for (const o of typeof c === 'function' ? c() : c)
839
+ if (test(o))
840
+ return o;
841
+ };
842
+ const clean = (cands) => scan(cands, ok);
843
+ const pick = (cands) => {
844
+ const o = scan(cands, ok);
845
+ if (o)
846
+ return o;
847
+ if (++bad > cap)
848
+ throw OVER;
849
+ return scan(cands, (o) => inRegion(o) && free(o)) ?? cands[0][0];
850
+ };
851
+ const tags = [];
852
+ const tagOf = (a) => {
853
+ const ev = s.ever[a], name = A[a].name, status = ev ? named(ev, s.summary) : 'Not reached', mark = ev ? (ev.fail ? '✗' : '✓') : '', end = s.summary && sc.boxes[story.end]?.actor === a, bed = !stage.narrow && s.summary && a === sc.truck ? Object.values(s.units).filter((u) => u.where === 'bed' && u.moment >= 0) : [], cargo = [...new Set(bed.map((u) => u.thing))], w = (end ? 22 : 20) +
854
+ Math.max(width(name, end ? ty.hero : ty.name, true), ty.mark + width(status, ty.status, false), ...cargo.map((c) => 20 + width(c, 13, true))), h = ty.tag + (end ? 3 : 0) + cargo.length * 20, g = GAP, onTruck = a === sc.truck && truckW, top = onTruck ? truckW.reduce((m, p) => (p[1] < m[1] ? p : m)) : A[a].lot.tag, [ax, ay] = pr(top), anchor = [ax, ay];
855
+ const opts = [];
856
+ if (onTruck) {
857
+ const [bx, by] = pr(truckW.reduce((m, p) => (p[1] > m[1] ? p : m)));
858
+ for (const x of [bx - w / 2, bx + 4, bx - w - 4])
859
+ opts.push({ x, y: by + g, w, h, anchor: [bx, by] });
860
+ }
861
+ for (const [x, y] of [
862
+ [ax - w / 2, ay - g - h],
863
+ [ax + g, ay - h / 2],
864
+ [ax - g - w, ay - h / 2],
865
+ [ax + 4, ay - g - h],
866
+ [ax - w - 4, ay - g - h],
867
+ [ax - w / 2, ay + g],
868
+ [ax + g, ay - g - h],
869
+ [ax - g - w, ay - g - h],
870
+ [ax + g, ay + 4],
871
+ [ax - g - w, ay + 4],
872
+ [ax - w / 2, ay - 2 * g - h],
873
+ [ax + 4, ay - 2 * g - h],
874
+ [ax - w - 4, ay - 2 * g - h],
875
+ [ax - w + 36, ay - g - h],
876
+ [ax - 36, ay - g - h],
877
+ ])
878
+ opts.push({ x, y, w, h, anchor });
879
+ const phone = stage.w < HUD.slim * 2 && s.summary;
880
+ const ds = stage.w < HUD.wide && s.summary && !(onTruck && phone) ? [12, 20, 30, 40] : stage.narrow ? [12, 20, 30, 44, 58] : [12, 18, 24];
881
+ const cands = [
882
+ opts,
883
+ () => ring(anchor, w, h, ds),
884
+ () => (onTruck ? ring(pr(truckW.reduce((m, p) => (p[1] > m[1] ? p : m))), w, h, ds) : []),
885
+ () => (onTruck && (!stage.narrow || phone) ? [] : ring(pr(A[a].lot.hull.reduce((m, p) => (p[1] > m[1] ? p : m))), w, h, ds)),
886
+ () => (s.dev.includes(a) && !k.inCab(a) ? ring(pr(k.devTop(a)), w, h, ds) : []),
887
+ ];
888
+ const must = s.hi.includes(a) || (s.summary && story.path.includes(a));
889
+ const seen = share((onTruck ? truckHull : A[a].lot.hull).map(pr), stage.region), part = seen < 0.6;
890
+ if (part && (!must || (full && stage.narrow && s.summary && !end && seen >= 0.01)))
891
+ return;
892
+ const base = { actor: a, name, status, mark, quiet: !story.path.includes(a), end, cargo };
893
+ const out = ax < l || ax > r || ay < t || ay > b;
894
+ if (must && stage.narrow && (out || part) && !(s.summary && end && part)) {
895
+ const tag = pin(base, w, h, [ax, ay]);
896
+ tags.push(tag);
897
+ taken.push(tag.r);
898
+ return;
899
+ }
900
+ if (part && ++bad > cap)
901
+ throw OVER;
902
+ const o = must ? pick(cands) : clean(cands);
903
+ if (!o)
904
+ return;
905
+ const tr = { x: o.x, y: o.y, w, h };
906
+ tags.push({ ...base, r: tr, anchor: o.anchor, line: [nearest(tr, o.anchor), o.anchor] });
907
+ taken.push(o);
908
+ };
909
+ if ((!stage.narrow || (stage.w < HUD.slim * 2 && s.summary)) && truckW && s.hi.includes(sc.truck))
910
+ tagOf(sc.truck);
911
+ const chips = [];
912
+ const chip = (text, opts, w, around, mark) => {
913
+ for (const [px, py] of around)
914
+ for (const d of [10, 30, 50])
915
+ for (let i = 0; i < 8; i++) {
916
+ const u = (i * Math.PI) / 4;
917
+ opts.push([px + Math.cos(u) * (d + w / 2) - w / 2, py + Math.sin(u) * (d + CHIP_H / 2) - CHIP_H / 2]);
918
+ }
919
+ const rs = opts.map(([x, y]) => ({ x, y, w, h: CHIP_H }));
920
+ const fits = (o) => inRegion(o) && offB(o) && free(o);
921
+ const o = rs.find(fits) ?? around.flatMap((p) => ring(p, w, CHIP_H, stage.narrow ? [20, 30, 44, 58] : [18, 24])).find(fits);
922
+ if (!o && ++bad > cap)
923
+ throw OVER;
924
+ chips.push({ text, r: o ?? rs[0], mark });
925
+ taken.push(o ?? rs[0]);
926
+ };
927
+ for (const a of s.arcs) {
928
+ if (a.moment !== s.moment || a.t !== 1 || !a.text || s.ever[a.to]?.label === a.text)
929
+ continue;
930
+ const w = 20 + width(a.text, ty.chip, true), g = 10;
931
+ let top = a.a;
932
+ for (let i = 0; i <= 40; i++) {
933
+ const p = bez(a, i / 40);
934
+ if (p[1] < top[1])
935
+ top = p;
936
+ }
937
+ const [px, py] = pr(top);
938
+ const opts = [[px - w / 2, py - CHIP_H - g]];
939
+ for (const u of [0.8, 0.2, 0.65, 0.35]) {
940
+ const [qx, qy] = pr(bez(a, u));
941
+ opts.push([qx + g, qy - CHIP_H / 2], [qx - g - w, qy - CHIP_H / 2]);
942
+ }
943
+ chip(a.text, opts, w, [0.5, 0.3, 0.7].map((u) => pr(bez(a, u))));
944
+ }
945
+ const seen = new Set();
946
+ for (const u of Object.values(s.units)) {
947
+ const at = s.summary && u.where === 'bed' ? truckW?.reduce((m, p) => (p[1] < m[1] ? p : m)) : u.p;
948
+ if ((u.moment !== s.moment && !s.summary) ||
949
+ u.moment < 0 ||
950
+ !at ||
951
+ !chipped(u, s.summary) ||
952
+ seen.has(u.thing) ||
953
+ (!stage.narrow && s.summary && u.where === 'bed'))
954
+ continue;
955
+ seen.add(u.thing);
956
+ const mark = s.summary && !u.fail ? '✓' : undefined, w = 20 + (mark ? ty.mark : 0) + width(u.thing, ty.chip, true), g = 10, h = CHIP_H, [px, py] = pr([at[0], at[1] + 6]);
957
+ chip(u.thing, [
958
+ [px - w / 2, py + g],
959
+ [px + g, py - h / 2],
960
+ [px - g - w, py - h / 2],
961
+ [px + g, py + g],
962
+ [px - g - w, py + g],
963
+ [px - w / 2, py + 3 * g],
964
+ ], w, [[px, py]], mark);
965
+ }
966
+ const signs = [];
967
+ for (const g of gates) {
968
+ const gs = s.gates[g.actor];
969
+ if (!gs.rows.length || !(gs.on === s.moment || (s.summary && gs.fail && !stage.narrow)))
970
+ continue;
971
+ const rows = gs.rows.map(({ q, a, fail }) => ({ q, a: s.ever[g.actor]?.label === a ? '' : a, fail }));
972
+ const w = 22 +
973
+ Math.max(width(A[g.actor].name, ty.kick, false), ...rows.map((x) => width(x.q, ty.q, true) + (x.a ? width(x.a, ty.a, true) + 40 : 26))), h = rows.length * ty.row + (rows.length - 1) * 6 + 14 + (ty.kick && 16);
974
+ const gr = g.r, gate = A[g.actor].gate, foot = pr(P(gate.ex, gate.y)), bot = gr.y + gr.h, mid = gr.x + gr.w / 2, gy = gr.y + gr.h / 2;
975
+ const ways = [
976
+ [mid - 14, gr.y - GAP - h, [mid, gr.y]],
977
+ [mid - w + 14, gr.y - GAP - h, [mid, gr.y]],
978
+ [gr.x + gr.w + GAP, gy - h / 2, [gr.x + gr.w, gy]],
979
+ [gr.x - GAP - w, gy - h / 2, [gr.x, gy]],
980
+ [gr.x + gr.w + GAP, bot - h, [gr.x + gr.w, bot - 4]],
981
+ [gr.x - GAP - w, bot - h, [gr.x, bot - 4]],
982
+ [foot[0] - 14, bot + GAP, foot],
983
+ [foot[0] - w + 14, bot + GAP, foot],
984
+ [mid - w / 2, bot + GAP, foot],
985
+ ].map(([x, y, anchor]) => ({ x, y, w, h, anchor }));
986
+ const opts = stage.narrow ? [...ways.slice(6), ...ways.slice(0, 6)] : ways;
987
+ const o = pick([opts, ...[foot, [mid, gr.y], [gr.x, gy], [gr.x + gr.w, gy]].map((p) => () => ring(p, w, h, [12, 24]))]);
988
+ signs.push({ actor: g.actor, r: { x: o.x, y: o.y, w, h }, anchor: o.anchor, rows, kick: !!ty.kick });
989
+ taken.push(o);
990
+ }
991
+ const rank = (a) => (s.hi.includes(a) ? 0 : story.path.includes(a) ? 1 : 2);
992
+ const order = A.map((_, a) => a).sort((p, q) => rank(p) - rank(q));
993
+ for (const a of order)
994
+ if (!tags.some((x) => x.actor === a))
995
+ tagOf(a);
996
+ function pin(base, w, h, [ax, ay]) {
997
+ const over = { l: l - ax, r: ax - r, t: t - ay, b: ay - b };
998
+ const side = Object.keys(over).reduce((m, e) => (over[e] > over[m] ? e : m));
999
+ const across = side === 'l' || side === 'r';
1000
+ const lo = across ? t : l, hi = Math.max(lo, across ? b - h : r - w), at = Math.max(lo, Math.min(hi, across ? ay - h / 2 : ax - w / 2));
1001
+ const opts = [];
1002
+ for (let d = 0; d <= hi - lo; d += 8)
1003
+ for (const v of d ? [at - d, at + d] : [at]) {
1004
+ if (v < lo || v > hi)
1005
+ continue;
1006
+ const x = across ? (side === 'l' ? l : r - w) : v, y = across ? v : side === 't' ? t : b - h;
1007
+ opts.push({ x, y, w, h, anchor: [ax, ay] });
1008
+ }
1009
+ const o = pick([opts]);
1010
+ const tr = { x: o.x, y: o.y, w, h };
1011
+ return { ...base, r: tr, anchor: o.anchor, line: [nearest(tr, o.anchor), o.anchor], pin: side };
1012
+ }
1013
+ for (const g of signs) {
1014
+ const tr = tags.find((x) => x.actor === g.actor)?.r, r = g.r;
1015
+ if (!g.kick || !tr || Math.max(tr.x - r.x - r.w, r.x - tr.x - tr.w, tr.y - r.y - r.h, r.y - tr.y - tr.h) > 120)
1016
+ continue;
1017
+ g.kick = false;
1018
+ g.r = { ...r, y: g.anchor[1] > r.y + r.h - 16 ? r.y + 16 : r.y, h: r.h - 16 };
1019
+ }
1020
+ bad += devices.filter((d) => !inRegion(d.r) || !offB(d.r)).length + gates.filter((g) => s.summary && !inRegion(g.r)).length;
1021
+ const placed = {
1022
+ cam,
1023
+ tags,
1024
+ signs,
1025
+ chips,
1026
+ devices,
1027
+ gates,
1028
+ hulls: full ? A.map((a) => a.lot.hull.map(pr)) : [],
1029
+ truck: full && truckW ? hull(truckW.map(pr)) : undefined,
1030
+ arcs: full ? arcsW.map((arc) => arc.map(pr)) : [],
1031
+ faults: bad,
1032
+ };
1033
+ return { placed, bad, pr };
1034
+ }
1035
+ export function place(sc, s, stage, top = stage.narrow ? 1 : stage.w >= 1800 ? 1.3 : 1.2) {
1036
+ const kt = kit(sc, s), last = s.summary && stage.w < HUD.wide ? sc.stories[s.story].moments.slice(-3, -1).flat() : [], near = [...new Set(last.flatMap((h) => [sc.boxes[h.from]?.actor, sc.boxes[h.to]?.actor]))].filter((a) => a !== undefined), pts = near.length
1037
+ ? [
1038
+ ...near.flatMap((a) => (a === sc.truck && s.truck && !kt.parked() ? kt.truckPts(s.truck) : sc.actors[a].lot.hull)),
1039
+ ...sc.actors.flatMap((a, i) => (s.gates[i].fail ? kt.gateEnds(i) : [])),
1040
+ ]
1041
+ : s.frame.length
1042
+ ? s.frame
1043
+ : sc.actors.flatMap((a) => a.lot.hull);
1044
+ const xs = pts.map((p) => p[0]), ys = pts.map((p) => p[1]);
1045
+ let x0 = Math.min(...xs), x1 = Math.max(...xs);
1046
+ const y0 = Math.min(...ys), y1 = Math.max(...ys);
1047
+ if (x1 - x0 < 520)
1048
+ [x0, x1] = [(x0 + x1) / 2 - 260, (x0 + x1) / 2 + 260];
1049
+ const { l, t, r, b } = stage.region, rw = r - l, rh = b - t, pad = stage.narrow ? 0 : MARGIN;
1050
+ const k = kit(sc, s), need = [];
1051
+ sc.actors.forEach(({ checks, gate: g }, a) => {
1052
+ const gs = s.gates[a];
1053
+ if (checks && gs.rows.length && (gs.on === s.moment || (s.summary && gs.fail)))
1054
+ need.push([P(g.px, g.y), 24], [P(g.ex, g.y), 24]);
1055
+ });
1056
+ for (const a of s.dev)
1057
+ if (!k.inCab(a))
1058
+ need.push([k.devBase(a), 0], [k.devTop(a), 0]);
1059
+ for (const a of s.arcs)
1060
+ if (a.moment === s.moment && a.t === 1 && a.text)
1061
+ need.push([bez(a, 0.5), 80]);
1062
+ for (const u of Object.values(s.units))
1063
+ if ((u.moment === s.moment || s.summary) && u.moment >= 0 && chipped(u, s.summary))
1064
+ need.push([u.where === 'bed' ? k.truckPts(s.truck).reduce((m, p) => (p[1] < m[1] ? p : m)) : u.p, 80]);
1065
+ const soft = s.dev.filter(k.inCab).map(k.devTop);
1066
+ const arcPts = s.arcs.filter((a) => a.t > 0).flatMap((a) => Array.from({ length: 9 }, (_, i) => bez(a, (a.t * i) / 8)));
1067
+ if (!stage.narrow)
1068
+ for (const p of arcPts)
1069
+ need.push([p, -14]);
1070
+ const inside = (c, x, y, m) => {
1071
+ const px = (x - c.x) * c.z + (l + r) / 2, py = (y - c.y) * c.z + (t + b) / 2;
1072
+ return px >= l - m && px <= r + m && py >= t - m && py <= b + m;
1073
+ };
1074
+ const holds = (c) => need.every(([[x, y], m]) => inside(c, x, y, m));
1075
+ const fit = (c) => {
1076
+ const lay = layout(sc, s, stage, c);
1077
+ const rects = (stage.narrow ? [...lay.placed.signs, ...lay.placed.tags, ...lay.placed.devices, ...lay.placed.chips] : []).map((o) => o.r);
1078
+ const sp = [...pts, ...arcPts, ...soft].map(lay.pr);
1079
+ const L = Math.min(...rects.map((o) => o.x), ...sp.map((p) => p[0])), R = Math.max(...rects.map((o) => o.x + o.w), ...sp.map((p) => p[0])), T = Math.min(...rects.map((o) => o.y), ...sp.map((p) => p[1])), B = Math.max(...rects.map((o) => o.y + o.h), ...sp.map((p) => p[1]));
1080
+ return { lay, L, R, T, B };
1081
+ };
1082
+ const search = (floor) => {
1083
+ let best = null, last;
1084
+ let z = Math.max(floor, Math.min((rw - pad) / (x1 - x0), (rh - 2 * pad) / (y1 - y0), top));
1085
+ for (let n = 0; n < 60 && z >= floor; n++, z *= 0.96) {
1086
+ let c = { x: (x0 + x1) / 2, y: (y0 + y1) / 2, z };
1087
+ for (let round = 0; round < 3; round++) {
1088
+ const f = fit(c);
1089
+ last = f.lay;
1090
+ if (round < 2) {
1091
+ c = { x: c.x + ((f.L + f.R) / 2 - (l + r) / 2) / z, y: c.y + ((f.T + f.B) / 2 - (t + b) / 2) / z, z };
1092
+ continue;
1093
+ }
1094
+ if (f.R - f.L <= rw - pad && f.B - f.T <= rh - 2 * pad) {
1095
+ if (!f.lay.bad && holds(c))
1096
+ return { lay: f.lay, ok: true };
1097
+ if (!best || f.lay.bad < best.bad)
1098
+ best = f.lay;
1099
+ }
1100
+ }
1101
+ }
1102
+ return { lay: best ?? last, ok: false };
1103
+ };
1104
+ const whole = search(Math.min(stage.narrow ? ZMIN : 0.3, top));
1105
+ const done = (lay) => layout(sc, s, stage, lay.placed.cam, true).placed;
1106
+ if (whole.ok)
1107
+ return done(whole.lay);
1108
+ const ref = stage.narrow ? Math.min(ZMIN, top) : whole.lay.placed.cam.z;
1109
+ const mid = { x: (x0 + x1) / 2, y: (y0 + y1) / 2 };
1110
+ const story = sc.stories[s.story], lift = s.truck && !k.parked() ? k.truckPts(s.truck).reduce((m, p) => (p[1] < m[1] ? p : m)) : null, must = sc.actors.flatMap((a, i) => s.hi.includes(i) || (s.summary && story.path.includes(i)) ? [i === sc.truck && lift ? lift : a.lot.tag] : []);
1111
+ const pins = (c) => (stage.narrow ? must.filter(([x, y]) => !inside(c, x, y, 0)).length : 0);
1112
+ const misses = [];
1113
+ let tries = 0;
1114
+ const score = (c, d) => d + Math.abs(c.z - ref) * 1000 + pins(c) * 300;
1115
+ const first = (cams) => {
1116
+ for (const { c } of cams.sort((p, q) => p.score - q.score)) {
1117
+ if (!holds(c))
1118
+ continue;
1119
+ if (++tries > TRIES)
1120
+ return;
1121
+ const cap = misses.length < 4 ? Infinity : misses.map((m) => m.bad).sort((p, q) => p - q)[3];
1122
+ try {
1123
+ const lay = layout(sc, s, stage, c, false, cap);
1124
+ if (!lay.bad)
1125
+ return lay;
1126
+ misses.push({ c, bad: lay.bad });
1127
+ }
1128
+ catch (e) {
1129
+ if (e !== OVER)
1130
+ throw e;
1131
+ }
1132
+ }
1133
+ };
1134
+ const grid = (aim, zs) => first(zs.flatMap((z) => SHIFT.flatMap((dy) => SHIFT.map((dx) => {
1135
+ const c = { x: aim.x + dx / z, y: aim.y + dy / z, z };
1136
+ return { c, score: score(c, Math.hypot(dx, dy)) };
1137
+ }))));
1138
+ const refine = () => first(misses
1139
+ .sort((p, q) => p.bad - q.bad)
1140
+ .slice(0, 4)
1141
+ .flatMap(({ c }) => NUDGE.flatMap((dy) => NUDGE.map((dx) => {
1142
+ const n = { x: c.x + dx / c.z, y: c.y + dy / c.z, z: c.z };
1143
+ return { c: n, score: score(n, Math.hypot(n.x - mid.x, n.y - mid.y) * c.z) };
1144
+ }))));
1145
+ const zs = [...new Set([0, 1, 2, 3, 4, 5].map((i) => Math.min(top, stage.narrow ? ZMIN + i * 0.1 : ref * (1 - i * 0.08))))], far = [0.4, 0.35, 0.3, 0.25].filter((z) => z < Math.min(...zs));
1146
+ return done(grid(focus(sc, s) ?? mid, zs) ?? grid(mid, zs) ?? grid(mid, far) ?? refine() ?? whole.lay);
1147
+ }
1148
+ export function storyCams(sc, story, stage) {
1149
+ const n = sc.stories[story].moments.length, out = [];
1150
+ if (!n)
1151
+ return out;
1152
+ out[n - 1] = place(sc, stateAt(sc, story, n - 1, Infinity), stage);
1153
+ for (let m = n - 2; m >= 0; m--)
1154
+ out[m] = place(sc, stateAt(sc, story, m, Infinity), stage, Math.min(stage.narrow ? 1 : stage.w >= 1800 ? 1.3 : 1.2, out[m + 1].cam.z * (m === n - 2 && stage.w < HUD.wide ? 1 : 1.2)));
1155
+ const fit = out[n - 1].cam.z, xs = sc.stories[story].path.flatMap((a) => sc.actors[a].lot.hull.map((p) => p[0])), aim = stage.w < HUD.wide
1156
+ ? out[n - 2].cam.z * 2
1157
+ : (Math.max(...xs) - Math.min(...xs)) * fit < 0.7 * (stage.region.r - stage.region.l)
1158
+ ? out[n - 2].cam.z * 0.92
1159
+ : out[n - 2].cam.z * 1.2;
1160
+ const end = stateAt(sc, story, n - 1, Infinity);
1161
+ if (fit > aim) {
1162
+ const back = place(sc, end, stage, aim);
1163
+ if (!back.faults)
1164
+ out[n - 1] = back;
1165
+ }
1166
+ return out;
1167
+ }
1168
+ export function storyView(get, story, stage) {
1169
+ const forms = stage.form === 'stack' || stage.form === 'spread' ? [stage.form, 'narrow'] : [stage.form];
1170
+ let view = { sc: get(forms[0]), cams: [] };
1171
+ for (const f of forms) {
1172
+ view = { sc: get(f), cams: storyCams(get(f), story, stage) };
1173
+ if (!view.cams.some((p) => p.faults))
1174
+ break;
1175
+ }
1176
+ return view;
1177
+ }
1178
+ const SHIFT = Array.from({ length: 13 }, (_, i) => (i - 6) * 40), NUDGE = [-30, -20, -10, 0, 10, 20, 30], TRIES = 400;
1179
+ function focus(sc, s) {
1180
+ if (s.lead)
1181
+ return { x: s.lead[0], y: s.lead[1] };
1182
+ const hops = sc.stories[s.story].moments[s.moment] ?? [];
1183
+ const ends = [];
1184
+ for (const e of plan(sc, s.story, s.moment).steps)
1185
+ if (e.hop >= 0)
1186
+ ends[e.hop] = Math.max(ends[e.hop] ?? 0, e.end);
1187
+ let last = -1;
1188
+ ends.forEach((e, i) => (last = last < 0 || e >= ends[last] ? i : last));
1189
+ const a = last >= 0 ? sc.boxes[hops[last].to]?.actor : undefined;
1190
+ const m = a === undefined ? undefined : mean(sc.actors[a].lot.hull);
1191
+ return m ? { x: m[0], y: m[1] } : null;
1192
+ }
1193
+ const OV = '#141A22', OV2 = '#4B5361', OVM = '#646C79', RIM = 'rgba(20,26,34,.16)';
1194
+ const OV_CSS = [
1195
+ `[data-stage=world]{overflow:hidden}[data-stage=world]>svg,.fw-ov>svg{position:absolute;inset:0;width:100%;height:100%}`,
1196
+ `.bld{transition:opacity .3s,filter .3s}.bld.quiet{opacity:.4;filter:saturate(.15)}.pad{opacity:0;transition:opacity .3s}.pad.on{opacity:1}`,
1197
+ `.fw-ov{position:absolute;inset:0;pointer-events:none;overflow:hidden;font-family:var(--flowfig-font);color:${OV}}`,
1198
+ `.fw-tag,.fw-sign,.fw-dchip{position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden;white-space:nowrap;background:#fff;border:1px solid ${RIM};transition:opacity .2s}`,
1199
+ `.fw-tag{padding:4px 9px;border-radius:6px}`,
1200
+ `.fw-tn{display:block;font:600 14.8px/19px var(--flowfig-font);color:${OV}}`,
1201
+ `.fw-ts{display:flex;font:400 13.8px/17px var(--flowfig-font);color:${OV2}}`,
1202
+ `.fw-mk{display:inline-grid;place-items:center;flex:none;width:14px;height:14px;margin:2px 6px 0 0;border-radius:3px;background:${OV};color:#fff;font:700 10px/1 var(--flowfig-font);font-style:normal}`,
1203
+ `.fw-no{color:${INK.fail}}.fw-no>.fw-mk{background:${INK.fail}}.fw-dchip>.fw-mk{margin-top:0}`,
1204
+ `.fw-tag{transition:opacity .2s,border-color .16s .2s,background-color .16s .2s}.fw-end{border:2px solid ${OV};background:#EEF1EA}.fw-end.fw-x{border-color:${INK.fail};background:#FCEBE8}.fw-end .fw-tn{font-size:16px;line-height:20px}`,
1205
+ `.fw-cg{font:500 13px/20px var(--flowfig-font)}`,
1206
+ `.fw-sk{font:500 12px/16px var(--flowfig-font);color:#5B6370}`,
1207
+ `.fw-quiet .fw-tn,.fw-quiet .fw-ts{color:${OVM}}`,
1208
+ `.fw-sign{padding:6px 10px;border-radius:8px}`,
1209
+ `.fw-row{display:flex;align-items:center;justify-content:space-between;height:30px}.fw-row+.fw-row{margin-top:6px}`,
1210
+ `.fw-q{font:600 15px/20px var(--flowfig-font);color:${OV}}`,
1211
+ `.fw-a{margin-left:12px;padding:0 6px;border-radius:5px;font:600 13px/24px var(--flowfig-font);background:#EEF1EA;color:${OV}}`,
1212
+ `.fw-a.fw-no{background:#FCEBE8;color:${INK.fail}}.fw-a.fw-no::before{content:"\\2717";margin-right:4px}`,
1213
+ `.fw-a.fw-m{display:inline-grid;place-items:center;width:14px;height:14px;padding:0;border-radius:3px;font:700 10px/1 var(--flowfig-font)}.fw-a.fw-m::before{content:none}`,
1214
+ `.fw-dchip{display:flex;align-items:center;padding:0 9px;border-radius:6px;font:600 13px/1 var(--flowfig-font);color:${INK.data}}`,
1215
+ `.fw-dchip.fw-thing{color:${OV}}.fw-dchip{transition:opacity .2s,scale .2s cubic-bezier(.23,1,.32,1)}.fw-dchip.fw-off{scale:.96}.gt{transition:opacity .2s}`,
1216
+ `.fw-off{opacity:0}`,
1217
+ `@container ff-guide (width < ${HUD.slim * 2}px){.fw-tn{font-size:13px;line-height:16px}.fw-ts{font-size:12px;line-height:15px}.fw-mk{margin:1px 4px 0 0}.fw-sk{display:none}.fw-end .fw-tn{font-size:14px;line-height:17px}.fw-row{height:24px}.fw-q{font-size:13px;line-height:17px}.fw-a{font-size:12px;line-height:20px}.fw-dchip{font-size:12px}}`,
1218
+ ].join('');
1219
+ const f1 = (n) => n.toFixed(1);
1220
+ const pt = (p) => `${f1(p[0])},${f1(p[1])}`;
1221
+ const cut = (r, t) => {
1222
+ const ab = lerp(r.a, r.c1, t), bc = lerp(r.c1, r.c2, t), cd = lerp(r.c2, r.b, t), abc = lerp(ab, bc, t), bcd = lerp(bc, cd, t), m = lerp(abc, bcd, t);
1223
+ return [
1224
+ { a: r.a, c1: ab, c2: abc, b: m },
1225
+ { a: m, c1: bcd, c2: cd, b: r.b },
1226
+ ];
1227
+ };
1228
+ const arcPart = (r, u0, u1) => {
1229
+ let s = u0 > 0 ? cut(r, u0)[1] : r;
1230
+ if (u1 < 1)
1231
+ s = cut(s, (u1 - u0) / (1 - u0))[0];
1232
+ return `M${pt(s.a)}C${pt(s.c1)} ${pt(s.c2)} ${pt(s.b)}`;
1233
+ };
1234
+ const NS = 'http://www.w3.org/2000/svg';
1235
+ const el = (tag, cls, text) => {
1236
+ const e = document.createElement(tag);
1237
+ e.className = cls;
1238
+ if (text !== undefined)
1239
+ e.textContent = text;
1240
+ return e;
1241
+ };
1242
+ const put = (e, r) => {
1243
+ e.style.transform = `translate(${f1(r.x)}px,${f1(r.y)}px)`;
1244
+ e.style.width = `${f1(r.w)}px`;
1245
+ e.style.height = `${f1(r.h)}px`;
1246
+ };
1247
+ const same = (e, text) => {
1248
+ if (e.textContent !== text)
1249
+ e.textContent = text;
1250
+ };
1251
+ let sheet;
1252
+ export function mountWorld(stage, fig, resized, hud) {
1253
+ const root = stage.getRootNode();
1254
+ if (!sheet) {
1255
+ sheet = new CSSStyleSheet();
1256
+ sheet.replaceSync(OV_CSS);
1257
+ }
1258
+ if (!root.adoptedStyleSheets.includes(sheet))
1259
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
1260
+ stage.dataset.stage = 'world';
1261
+ const scenes = {}, get = (form) => (scenes[form] ??= scene(fig, form));
1262
+ const svg = document.createElementNS(NS, 'svg');
1263
+ svg.setAttribute('role', 'img');
1264
+ const ov = el('div', 'fw-ov');
1265
+ stage.append(svg, ov);
1266
+ const q = (sel) => svg.querySelector(sel);
1267
+ const unitEls = new Map();
1268
+ let sc = get('narrow');
1269
+ const mount = () => {
1270
+ const art = sc.truckArt;
1271
+ svg.innerHTML =
1272
+ `<g class="fw-cam">${sc.svg}${art?.door ?? ''}<g class="fw-units"></g>${art?.road ?? ''}` +
1273
+ sc.actors.map((a, i) => (a.device === 'antenna' ? '' : `<g class="fw-dev" data-a="${i}">${PHONE.svg}</g>`)).join('') +
1274
+ `<g class="fw-arcs"></g></g>`;
1275
+ unitEls.clear();
1276
+ return {
1277
+ art,
1278
+ depot: sc.truck >= 0 ? sc.actors[sc.truck].lot.depot : null,
1279
+ camG: q('.fw-cam'),
1280
+ unitsG: q('.fw-units'),
1281
+ arcsG: q('.fw-arcs'),
1282
+ doorPanel: q('#doorPanel'),
1283
+ devs: new Map([...svg.querySelectorAll('.fw-dev')].map((g) => [Number(g.dataset.a), g])),
1284
+ arms: [...svg.querySelectorAll('.arm')],
1285
+ arms2: [...svg.querySelectorAll('.armW')],
1286
+ hubs: [...svg.querySelectorAll('.hub')],
1287
+ gts: [...svg.querySelectorAll('.gt')],
1288
+ checkers: sc.actors.flatMap((a, i) => (a.checks ? [i] : [])),
1289
+ };
1290
+ };
1291
+ let d = mount();
1292
+ const show = (e, on) => e?.setAttribute('display', on ? 'inline' : 'none');
1293
+ const move = (e, p, k = 1) => e?.setAttribute('transform', `translate(${f1(p[0])} ${f1(p[1])})${k === 1 ? '' : ` scale(${k})`}`);
1294
+ let stg = stageFor(0, 0), cam, cur, raf = 0, built = '', said = '';
1295
+ const lays = new Map();
1296
+ let tags = [], signs = [], chips = [];
1297
+ const stageOf = (story) => stageFor(stg.w, stg.h, hud(story));
1298
+ const viewFor = (story) => lays.get(story) ?? lays.set(story, storyView(get, story, stageOf(story))).get(story);
1299
+ const use = (s) => {
1300
+ if (s === sc)
1301
+ return;
1302
+ sc = s;
1303
+ d = mount();
1304
+ built = '';
1305
+ cam = undefined;
1306
+ };
1307
+ const layoutFor = (story, moment) => {
1308
+ const v = viewFor(story);
1309
+ use(v.sc);
1310
+ return v.cams[moment];
1311
+ };
1312
+ function draw(s, c) {
1313
+ const { l, t, r, b } = stg.region;
1314
+ d.camG.setAttribute('transform', `translate(${f1((l + r) / 2)} ${f1((t + b) / 2)}) scale(${c.z.toFixed(4)}) translate(${f1(-c.x)} ${f1(-c.y)})`);
1315
+ const k = kit(sc, s);
1316
+ const out = !!s.truck && !k.parked();
1317
+ if (s.truck && d.depot) {
1318
+ const tk = s.truck;
1319
+ for (const [id, o] of [
1320
+ ['#truck', 'L'],
1321
+ ['#truckR', 'R'],
1322
+ ['#truckY', 'y'],
1323
+ ['#truckD', 'D'],
1324
+ ['#truckU', 'U'],
1325
+ ]) {
1326
+ show(q(id), tk.orient === o);
1327
+ move(q(id), P(tk.x, tk.y - ('LR'.includes(o) ? LANE - TD / 2 : 0)));
1328
+ }
1329
+ const dy = d.depot.y + 0.004, x0 = d.depot.x + 0.3, x1 = d.depot.x + 1.8, zb = 1.03 * tk.door;
1330
+ const quad = (z0, z1, fill) => `<polygon points="${[P(x0, dy, z0), P(x1, dy, z0), P(x1, dy, z1), P(x0, dy, z1)].map(pt).join(' ')}" fill="${fill}"/>`;
1331
+ let o = quad(zb, 1.03, '#3b463d');
1332
+ for (let i = 0; i < 9; i++) {
1333
+ const z = 0.1 + i * 0.105;
1334
+ if (z > zb)
1335
+ o += quad(z, Math.min(1.03, z + 0.035), '#56645a');
1336
+ }
1337
+ if (d.doorPanel && d.doorPanel.dataset.k !== o) {
1338
+ d.doorPanel.innerHTML = o;
1339
+ d.doorPanel.dataset.k = o;
1340
+ }
1341
+ for (const side of 'lrdu')
1342
+ for (let i = 0; i < 8; i++) {
1343
+ const e = q(`#t${side}${i}`), u = tk.load[i] ? sc.things.get(s.units[tk.load[i]].thing)?.form : undefined;
1344
+ if (e && e.dataset.k !== (u ?? '')) {
1345
+ e.innerHTML = u ? UNIT[u] : '';
1346
+ e.dataset.k = u ?? '';
1347
+ }
1348
+ }
1349
+ }
1350
+ for (const [id, u] of Object.entries(s.units)) {
1351
+ let g = unitEls.get(id);
1352
+ if (!g) {
1353
+ g = document.createElementNS(NS, 'g');
1354
+ g.innerHTML = UNIT[sc.things.get(u.thing)?.form ?? 'box'];
1355
+ d.unitsG.append(g);
1356
+ unitEls.set(id, g);
1357
+ }
1358
+ const on = u.where === 'yard' || u.where === 'road' || u.where === 'gate' || u.where === 'air';
1359
+ show(g, on);
1360
+ if (on)
1361
+ move(g, u.p);
1362
+ }
1363
+ for (const [id, g] of unitEls)
1364
+ if (!s.units[id])
1365
+ show(g, false);
1366
+ for (const [a, g] of d.devs) {
1367
+ show(g, s.dev.includes(a));
1368
+ move(g, k.devBase(a), (k.inCab(a) ? 0.82 : 1) * phoneScale(c.z, stg));
1369
+ for (const sc2 of g.querySelectorAll('.scr'))
1370
+ show(sc2, sc2.getAttribute('data-s') === (s.screens[a] ?? 'idle'));
1371
+ }
1372
+ d.checkers.forEach((a, n) => {
1373
+ const g = sc.actors[a].gate, st = s.gates[a], rad = (st.deg * Math.PI) / 180;
1374
+ const b0 = P(g.px, g.y, g.z), e0 = P(g.px + g.L * Math.cos(rad), g.y, g.z + g.L * Math.sin(rad));
1375
+ for (const arm of [d.arms[n], d.arms2[n]]) {
1376
+ arm.setAttribute('x1', f1(b0[0]));
1377
+ arm.setAttribute('y1', f1(b0[1]));
1378
+ arm.setAttribute('x2', f1(e0[0]));
1379
+ arm.setAttribute('y2', f1(e0[1]));
1380
+ }
1381
+ d.arms[n].setAttribute('stroke', st.fail ? INK.fail : '#2e3440');
1382
+ d.gts[n].style.opacity = st.on === s.moment || st.fail ? '' : '0';
1383
+ d.hubs[n].setAttribute('cx', f1(b0[0]));
1384
+ d.hubs[n].setAttribute('cy', f1(b0[1]));
1385
+ });
1386
+ let a = '';
1387
+ for (const r of s.arcs) {
1388
+ const col = r.fail ? INK.fail : INK.data, dash = r.dashed ? ' stroke-dasharray="7 6"' : '';
1389
+ a += `<path d="${arcPart(r, 0, r.t)}" fill="none" stroke="${INK.data}" stroke-opacity="${s.summary ? (r.fail ? 0.38 : 0.38 * (s.fade ?? 0)) : r.moment === s.moment || r.hold ? 0.38 : 0.16}" stroke-width="2" stroke-linecap="round"${dash}/>`;
1390
+ if (r.t < 1)
1391
+ a += `<path d="${arcPart(r, Math.max(0, r.t - 0.16), r.t)}" fill="none" stroke="${col}" stroke-width="3.2" stroke-linecap="round"/>`;
1392
+ else if (r.fail)
1393
+ a += `<path d="${arcPart(r, 0.86, 1)}" fill="none" stroke="${INK.fail}" stroke-width="3.2" stroke-linecap="round"/>`;
1394
+ if (r.t > 0 && (r.t < 1 || r.hold))
1395
+ a += `<circle cx="${f1(bez(r, r.t)[0])}" cy="${f1(bez(r, r.t)[1])}" r="7" fill="${col}" stroke="#fff" stroke-width="2.4"/>`;
1396
+ }
1397
+ d.arcsG.innerHTML = a;
1398
+ sc.actors.forEach((_, i) => {
1399
+ q(`#pad-${i}`)?.classList.toggle('on', s.hi.includes(i) && !(i === sc.truck && out));
1400
+ q(`#b-${i}`)?.classList.toggle('quiet', !sc.stories[s.story].path.includes(i));
1401
+ });
1402
+ }
1403
+ function build(lay, story) {
1404
+ const lead = document.createElementNS(NS, 'svg');
1405
+ let ls = '';
1406
+ tags = lay.tags.map((t) => {
1407
+ const e = el('div', `fw-tag${t.quiet ? ' fw-quiet' : ''}`), mk = el('i', 'fw-mk'), st = el('span', 'fw-ts');
1408
+ e.dataset.actor = t.name;
1409
+ st.append(mk, '');
1410
+ e.append(el('b', 'fw-tn', t.name), st);
1411
+ for (const c of t.cargo) {
1412
+ const row = el('span', 'fw-ts fw-cg');
1413
+ row.append(el('i', 'fw-mk', '✓'), c);
1414
+ e.append(row);
1415
+ }
1416
+ put(e, t.r);
1417
+ if (story.acting[cur?.moment ?? -1] === t.name)
1418
+ e.setAttribute('aria-current', 'true');
1419
+ if (t.pin) {
1420
+ const { r } = t, [ax, ay] = t.anchor, px = t.pin === 'l' ? r.x : t.pin === 'r' ? r.x + r.w : Math.max(r.x + 9, Math.min(r.x + r.w - 9, ax)), py = t.pin === 't' ? r.y : t.pin === 'b' ? r.y + r.h : Math.max(r.y + 9, Math.min(r.y + r.h - 9, ay));
1421
+ ls += `<path d="M-6 0H6L0 8Z" transform="translate(${pt([px, py])}) rotate(${{ l: 90, t: 180, r: -90, b: 0 }[t.pin]})" fill="${OV}"/>`;
1422
+ }
1423
+ else
1424
+ ls += `<path d="M${pt(t.line[0])}L${pt(t.line[1])}" stroke="${OV}" stroke-opacity=".55"/><circle cx="${f1(t.line[1][0])}" cy="${f1(t.line[1][1])}" r="2.5" fill="${OV}"/>`;
1425
+ return { el: e, actor: t.actor, name: t.name, mk, st, end: t.end, w: e.style.width };
1426
+ });
1427
+ signs = lay.signs.map((g) => {
1428
+ const e = el('div', 'fw-sign');
1429
+ if (g.kick)
1430
+ e.append(el('div', 'fw-sk', sc.actors[g.actor].name));
1431
+ const rows = g.rows.map((r) => {
1432
+ const row = el('div', 'fw-row'), q = el('span', 'fw-q', r.q), a = el('span', r.a ? 'fw-a' : 'fw-a fw-m');
1433
+ row.append(q, a);
1434
+ e.append(row);
1435
+ return { q, a, m: !r.a };
1436
+ });
1437
+ put(e, g.r);
1438
+ ls += `<path d="M${pt(g.anchor)}L${pt(nearest(g.r, g.anchor))}" stroke="#2e3440" stroke-width="2"/>`;
1439
+ return { el: e, actor: g.actor, rows };
1440
+ });
1441
+ let n = 0;
1442
+ chips = lay.chips.map((c) => {
1443
+ const e = el('div', `fw-dchip${sc.things.has(c.text) ? ' fw-thing' : ''}`);
1444
+ if (c.mark) {
1445
+ e.append(el('i', 'fw-mk', c.mark));
1446
+ e.style.transitionDelay = `${n++ * 60}ms`;
1447
+ }
1448
+ e.append(c.text);
1449
+ put(e, c.r);
1450
+ return { el: e, text: c.text };
1451
+ });
1452
+ lead.innerHTML = ls;
1453
+ ov.replaceChildren(lead, ...tags.map((t) => t.el), ...signs.map((g) => g.el), ...chips.map((c) => c.el));
1454
+ }
1455
+ function sync(s, rest) {
1456
+ const story = sc.stories[s.story], acting = s.summary ? story.path : s.hi, fin = sc.boxes[story.end];
1457
+ const spoken = [];
1458
+ if (s.summary && fin) {
1459
+ spoken.push(`End: ${fin.label}${s.ever[fin.actor]?.fail ? ', failed' : ''}.`);
1460
+ const seen = new Set();
1461
+ for (const u of Object.values(s.units))
1462
+ if (u.moment >= 0 && !seen.has(u.thing))
1463
+ spoken.push(`${seen.add(u.thing) && u.thing}: ${u.fail ? 'stopped' : 'kept'}.`);
1464
+ }
1465
+ for (const t of tags) {
1466
+ const ev = s.ever[t.actor];
1467
+ if (ev && acting.includes(t.actor))
1468
+ spoken.push(`${t.name}: ${named(ev, s.summary)}${ev.fail ? ', failed' : ''}.`);
1469
+ }
1470
+ svg.setAttribute('aria-label', spoken.join(' ') || said);
1471
+ for (const t of tags) {
1472
+ const ev = s.ever[t.actor];
1473
+ t.mk.hidden = !ev;
1474
+ same(t.mk, ev ? (ev.fail ? '✗' : '✓') : '');
1475
+ same(t.st.lastChild, ev ? named(ev, s.summary) : 'Not reached');
1476
+ t.st.classList.toggle('fw-no', !!ev?.fail);
1477
+ t.el.classList.toggle('fw-end', s.summary && rest && t.end);
1478
+ t.el.style.width = rest ? t.w : 'max-content';
1479
+ t.el.style.minWidth = rest ? '' : t.w;
1480
+ t.el.classList.toggle('fw-x', !!ev?.fail);
1481
+ }
1482
+ for (const g of signs) {
1483
+ const gs = s.gates[g.actor];
1484
+ g.el.classList.toggle('fw-off', !rest && !(gs.rows.length && (gs.on === s.moment || s.summary)));
1485
+ g.rows.forEach((r, i) => {
1486
+ const row = gs.rows[i];
1487
+ r.q.parentElement.style.visibility = row ? '' : 'hidden';
1488
+ same(r.a, r.m && row?.a ? (row.fail ? '✗' : '✓') : (row?.a ?? ''));
1489
+ r.a.hidden = !row?.a;
1490
+ r.a.classList.toggle('fw-no', !!row?.fail);
1491
+ });
1492
+ }
1493
+ const live = new Set();
1494
+ for (const a of s.arcs)
1495
+ if (a.moment === s.moment && a.t === 1 && a.text)
1496
+ live.add(a.text);
1497
+ for (const u of Object.values(s.units))
1498
+ if (u.moment === s.moment && (u.where === 'yard' || u.where === 'gate'))
1499
+ live.add(u.thing);
1500
+ for (const c of chips)
1501
+ c.el.classList.toggle('fw-off', !rest && !live.has(c.text));
1502
+ }
1503
+ function frame(rest, t) {
1504
+ const { story, moment } = cur;
1505
+ const lay = layoutFor(story, moment);
1506
+ const key = `${story}/${moment}/${stg.w}x${stg.h}`;
1507
+ if (built !== key) {
1508
+ built = key;
1509
+ build(lay, sc.stories[story]);
1510
+ }
1511
+ const s = stateAt(sc, story, moment, rest ? Infinity : t);
1512
+ draw(s, cam);
1513
+ sync(s, rest);
1514
+ }
1515
+ const measure = () => {
1516
+ const { width: w, height: h } = stage.getBoundingClientRect();
1517
+ if (w === stg.w && h === stg.h)
1518
+ return false;
1519
+ stg = stageFor(w, h);
1520
+ lays.clear();
1521
+ use(get(stg.form));
1522
+ svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
1523
+ return true;
1524
+ };
1525
+ const observer = new ResizeObserver(() => {
1526
+ if (!measure() || !cur || !stg.w)
1527
+ return;
1528
+ cancelAnimationFrame(raf);
1529
+ resized();
1530
+ });
1531
+ observer.observe(stage);
1532
+ measure();
1533
+ return {
1534
+ show(story, moment, motion, say) {
1535
+ cancelAnimationFrame(raf);
1536
+ said = say;
1537
+ cur = { story, moment };
1538
+ if (!stg.w || !stg.h)
1539
+ return 0;
1540
+ stg = stageOf(story);
1541
+ const to = layoutFor(story, moment).cam;
1542
+ const from = cam ?? to;
1543
+ if (!motion) {
1544
+ cam = to;
1545
+ frame(true, 0);
1546
+ return 0;
1547
+ }
1548
+ const { end, events } = timeline(sc, story, moment), c = events.find((e) => e.kind === 'camera');
1549
+ const t0 = performance.now();
1550
+ const step = (now) => {
1551
+ const t = now - t0;
1552
+ if (t >= end) {
1553
+ cam = to;
1554
+ return frame(true, 0);
1555
+ }
1556
+ const e = ease.out(Math.min(1, Math.max(0, (t - c.start) / (c.end - c.start))));
1557
+ cam = { x: from.x + (to.x - from.x) * e, y: from.y + (to.y - from.y) * e, z: from.z + (to.z - from.z) * e };
1558
+ frame(false, t);
1559
+ raf = requestAnimationFrame(step);
1560
+ };
1561
+ step(t0);
1562
+ return end;
1563
+ },
1564
+ fits(story) {
1565
+ try {
1566
+ return !stg.w || viewFor(story).cams.every((p) => !p.faults);
1567
+ }
1568
+ catch {
1569
+ return false;
1570
+ }
1571
+ },
1572
+ stop() {
1573
+ cancelAnimationFrame(raf);
1574
+ observer.disconnect();
1575
+ },
1576
+ };
1577
+ }