flowfig 0.11.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/README.md +148 -21
- package/dist/atlas.js +6 -2
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +9 -4
- package/dist/check.d.ts +2 -2
- package/dist/check.js +135 -5
- package/dist/cli.js +174 -70
- package/dist/diff.d.ts +2 -0
- package/dist/diff.js +79 -24
- package/dist/element-looks.js +4 -0
- package/dist/element-world.js +6 -0
- package/dist/element.d.ts +27 -0
- package/dist/element.js +67 -0
- package/dist/explain.d.ts +45 -0
- package/dist/explain.js +161 -0
- package/dist/frames.d.ts +34 -0
- package/dist/frames.js +117 -0
- package/dist/index.js +283 -131
- package/dist/load.js +9 -4
- package/dist/looks.d.ts +15 -0
- package/dist/looks.js +32 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +14 -6
- package/dist/model.d.ts +97 -0
- package/dist/model.js +182 -1
- package/dist/scene.d.ts +2 -0
- package/dist/source.d.ts +4 -2
- package/dist/source.js +15 -9
- package/dist/svg.d.ts +12 -0
- package/dist/svg.js +235 -75
- package/dist/verify.d.ts +6 -0
- package/dist/verify.js +104 -56
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/world-art.d.ts +59 -0
- package/dist/world-art.js +854 -0
- package/dist/world.d.ts +233 -0
- package/dist/world.js +1577 -0
- package/package.json +9 -3
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
|
+
}
|