@phin-tech/dottie 0.6.3 → 0.8.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/PARITY.md +6 -0
- package/README.md +9 -3
- package/dist/compile.js +122 -12
- package/dist/dottie-compile.mjs +123 -13
- package/dist/dottie-player.js +1006 -52
- package/dist/dottie-player.min.js +55 -53
- package/dist/dottie.js +1006 -52
- package/dist/types/clicks.d.ts +9 -2
- package/dist/types/flows.d.ts +26 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/measure.d.ts +58 -0
- package/dist/types/model.d.ts +76 -1
- package/dist/types/player.d.ts +31 -3
- package/dist/types/state.d.ts +40 -0
- package/dist/types/timeline.d.ts +13 -3
- package/package.json +1 -1
package/dist/dottie.js
CHANGED
|
@@ -1,15 +1,148 @@
|
|
|
1
|
-
/*! dottie.js 0.
|
|
1
|
+
/*! dottie.js 0.8.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
|
|
4
4
|
var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
|
|
5
5
|
|
|
6
|
+
// src/state.ts
|
|
7
|
+
var MAX_NAME = 64;
|
|
8
|
+
function isName(name) {
|
|
9
|
+
return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
10
|
+
}
|
|
11
|
+
var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
|
|
12
|
+
var OPS = [">=", "<=", "==", "!=", ">", "<"];
|
|
13
|
+
function metricTerm(term) {
|
|
14
|
+
const t = trim(term);
|
|
15
|
+
if (!t.includes("(")) {
|
|
16
|
+
const op2 = t.search(/[>=<!]/);
|
|
17
|
+
if (op2 < 0) return null;
|
|
18
|
+
const name2 = trim(t.slice(0, op2));
|
|
19
|
+
if (!METRIC_NAMES.includes(name2)) return null;
|
|
20
|
+
const m = metricTerm(name2 + "() " + t.slice(op2));
|
|
21
|
+
return m ? { ...m, target: "" } : null;
|
|
22
|
+
}
|
|
23
|
+
const open = t.indexOf("("), close = t.indexOf(")");
|
|
24
|
+
if (open < 0 || close < 0 || open >= close) return null;
|
|
25
|
+
const name = trim(t.slice(0, open)), target = trim(t.slice(open + 1, close));
|
|
26
|
+
let rest = trim(t.slice(close + 1));
|
|
27
|
+
if (!METRIC_NAMES.includes(name) || target.includes("(")) return null;
|
|
28
|
+
const op = OPS.find((o) => rest.startsWith(o));
|
|
29
|
+
if (op === void 0) return null;
|
|
30
|
+
rest = trim(rest.slice(op.length));
|
|
31
|
+
if (!/^-?[0-9]+(\.[0-9]+)?$/.test(rest)) return null;
|
|
32
|
+
return { metric: name, target, op, value: Number(rest) };
|
|
33
|
+
}
|
|
34
|
+
function passes(m, v) {
|
|
35
|
+
switch (m.op) {
|
|
36
|
+
case ">":
|
|
37
|
+
return v > m.value;
|
|
38
|
+
case ">=":
|
|
39
|
+
return v >= m.value;
|
|
40
|
+
case "<":
|
|
41
|
+
return v < m.value;
|
|
42
|
+
case "<=":
|
|
43
|
+
return v <= m.value;
|
|
44
|
+
case "==":
|
|
45
|
+
return v === m.value;
|
|
46
|
+
default:
|
|
47
|
+
return v !== m.value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function mentionsMetric(condition) {
|
|
51
|
+
return condition.split("||").some((any) => any.split("&&").some((all) => metricTerm(all) !== null));
|
|
52
|
+
}
|
|
53
|
+
function conditionNames(condition) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const any of condition.split("||")) {
|
|
56
|
+
for (const all of any.split("&&")) {
|
|
57
|
+
if (metricTerm(all) !== null) continue;
|
|
58
|
+
let term = trim(all);
|
|
59
|
+
if (term.startsWith("!")) term = trim(term.slice(1));
|
|
60
|
+
if (!isName(term)) return null;
|
|
61
|
+
out.push(term);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function holds(condition, state, metric = null) {
|
|
67
|
+
if (conditionNames(condition) === null) return false;
|
|
68
|
+
return condition.split("||").some((any) => any.split("&&").every((all) => {
|
|
69
|
+
const m = metricTerm(all);
|
|
70
|
+
if (m !== null) {
|
|
71
|
+
const v = metric ? metric(m) : null;
|
|
72
|
+
return v !== null && passes(m, v);
|
|
73
|
+
}
|
|
74
|
+
let term = trim(all);
|
|
75
|
+
let negated = false;
|
|
76
|
+
if (term.startsWith("!")) {
|
|
77
|
+
negated = true;
|
|
78
|
+
term = trim(term.slice(1));
|
|
79
|
+
}
|
|
80
|
+
return (state[term] ?? false) !== negated;
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
function stateValues(page, choice) {
|
|
84
|
+
if (choice === "all") return null;
|
|
85
|
+
if (choice === "initial") return { ...page.variables ?? {} };
|
|
86
|
+
return { ...page.variables ?? {}, ...choice };
|
|
87
|
+
}
|
|
88
|
+
function stateHidden(page, choice) {
|
|
89
|
+
const values = stateValues(page, choice);
|
|
90
|
+
const out = /* @__PURE__ */ new Set();
|
|
91
|
+
if (!values) return out;
|
|
92
|
+
for (const e of page.elements) if (e.when !== null && !holds(e.when, values)) out.add(e.id);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
function activeOverrides(e, values) {
|
|
96
|
+
if (!values || !e.overrides) return [];
|
|
97
|
+
return e.overrides.filter((o) => !mentionsMetric(o.when) && holds(o.when, values));
|
|
98
|
+
}
|
|
99
|
+
function looksOnly(o) {
|
|
100
|
+
return { ...o, reroute: null, offline: null, cost: null, capacity: null, delay: null };
|
|
101
|
+
}
|
|
102
|
+
function applyOverride(o, e) {
|
|
103
|
+
const out = cloneElement(e);
|
|
104
|
+
if (o.stroke) out.style.stroke = { ...o.stroke };
|
|
105
|
+
if (o.fill) out.style.fill = { ...o.fill };
|
|
106
|
+
if (o.textColor) out.style.textColor = { ...o.textColor };
|
|
107
|
+
if (o.strokeWidth !== null) out.style.strokeWidth = Math.max(0, o.strokeWidth);
|
|
108
|
+
if (o.dash !== null) out.style.dash = o.dash;
|
|
109
|
+
if (o.text !== null) out.text = o.text;
|
|
110
|
+
if (o.cost !== null && out.connector) out.connector.cost = Math.min(Math.max(o.cost, 0), 1e6);
|
|
111
|
+
if (o.capacity !== null) out.capacity = Math.max(o.capacity, 0);
|
|
112
|
+
if (o.delay !== null && out.kind !== "connector") out.delay = Math.max(o.delay, 0);
|
|
113
|
+
if (o.opacity !== null) {
|
|
114
|
+
const v = Math.min(Math.max(o.opacity, 0), 1) * (out.style.opacity ?? 1);
|
|
115
|
+
out.style.opacity = v >= 1 ? null : v;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function changesState(c) {
|
|
120
|
+
return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
|
|
121
|
+
}
|
|
122
|
+
function applyClick(c, state, initial = {}) {
|
|
123
|
+
let out = { ...state };
|
|
124
|
+
if (c.reset === true) {
|
|
125
|
+
out = {};
|
|
126
|
+
for (const k of Object.keys(state)) out[k] = false;
|
|
127
|
+
Object.assign(out, initial);
|
|
128
|
+
}
|
|
129
|
+
for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
|
|
130
|
+
for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function trim(s) {
|
|
134
|
+
return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
|
|
135
|
+
}
|
|
136
|
+
|
|
6
137
|
// src/model.ts
|
|
7
138
|
var SUPPORTED_VERSION = 2;
|
|
8
139
|
var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
|
|
9
140
|
var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
|
|
10
141
|
var KNOWN_FIELDS = {
|
|
11
142
|
file: ["version", "pages", "images"],
|
|
12
|
-
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
|
|
143
|
+
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
|
|
144
|
+
flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
|
|
145
|
+
flowOverride: ["when", "volume"],
|
|
13
146
|
symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
|
|
14
147
|
layer: ["id", "name", "visible", "locked"],
|
|
15
148
|
rgba: ["r", "g", "b", "a"],
|
|
@@ -37,6 +170,8 @@ var KNOWN_FIELDS = {
|
|
|
37
170
|
"anchor",
|
|
38
171
|
"click",
|
|
39
172
|
"hover",
|
|
173
|
+
"when",
|
|
174
|
+
"overrides",
|
|
40
175
|
"symbol",
|
|
41
176
|
"playback",
|
|
42
177
|
"offset",
|
|
@@ -45,8 +180,13 @@ var KNOWN_FIELDS = {
|
|
|
45
180
|
"reveal",
|
|
46
181
|
"ports",
|
|
47
182
|
"labelRect",
|
|
48
|
-
"extension"
|
|
183
|
+
"extension",
|
|
184
|
+
"capacity",
|
|
185
|
+
"delay",
|
|
186
|
+
"queue",
|
|
187
|
+
"gauge"
|
|
49
188
|
],
|
|
189
|
+
gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
|
|
50
190
|
strokePoint: ["x", "y"],
|
|
51
191
|
subpath: ["closed", "coords"],
|
|
52
192
|
inlineImage: ["data", "format"],
|
|
@@ -54,7 +194,7 @@ var KNOWN_FIELDS = {
|
|
|
54
194
|
extensionRef: ["id", "shape", "version", "params"],
|
|
55
195
|
normalizedPoint: ["x", "y"],
|
|
56
196
|
normalizedRect: ["x", "y", "width", "height"],
|
|
57
|
-
connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
|
|
197
|
+
connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
|
|
58
198
|
waypoint: ["x", "y"],
|
|
59
199
|
timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
|
|
60
200
|
track: ["element", "keys", "boil", "follow", "repeats", "march"],
|
|
@@ -62,9 +202,10 @@ var KNOWN_FIELDS = {
|
|
|
62
202
|
follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
|
|
63
203
|
repeat: ["start", "end", "count"],
|
|
64
204
|
cameraKey: ["t", "x", "y", "width", "height", "ease"],
|
|
65
|
-
marker: ["t", "name"],
|
|
66
|
-
click: ["scene", "url"],
|
|
67
|
-
hover: ["stroke", "fill", "opacity"]
|
|
205
|
+
marker: ["t", "name", "loop"],
|
|
206
|
+
click: ["scene", "url", "set", "toggle", "reset"],
|
|
207
|
+
hover: ["stroke", "fill", "opacity"],
|
|
208
|
+
override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
|
|
68
209
|
};
|
|
69
210
|
var MAX_DURATION = 3600;
|
|
70
211
|
var MAX_MARCH = 1e4;
|
|
@@ -80,6 +221,8 @@ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
|
80
221
|
var str = (v) => typeof v === "string" ? v : void 0;
|
|
81
222
|
var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
|
|
82
223
|
var bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
224
|
+
var NO_ID = "00000000-0000-0000-0000-000000000000";
|
|
225
|
+
var UUID = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
|
|
83
226
|
var uuid = (v) => typeof v === "string" ? v.toUpperCase() : void 0;
|
|
84
227
|
function clampTime(t) {
|
|
85
228
|
if (!Number.isFinite(t)) return t === Infinity ? MAX_DURATION : 0;
|
|
@@ -171,7 +314,8 @@ var Reader = class {
|
|
|
171
314
|
this.fields(w, "waypoint");
|
|
172
315
|
return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
|
|
173
316
|
}) : null,
|
|
174
|
-
hops: typeof v.hops === "boolean" ? v.hops : null
|
|
317
|
+
hops: typeof v.hops === "boolean" ? v.hops : null,
|
|
318
|
+
cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
|
|
175
319
|
};
|
|
176
320
|
}
|
|
177
321
|
image(v, table) {
|
|
@@ -222,6 +366,8 @@ var Reader = class {
|
|
|
222
366
|
anchor: str(v.anchor) ?? null,
|
|
223
367
|
click: this.click(v.click),
|
|
224
368
|
hover: this.hover(v.hover),
|
|
369
|
+
when: str(v.when) ?? null,
|
|
370
|
+
overrides: this.overrides(v.overrides),
|
|
225
371
|
symbol: uuid(v.symbol) ?? null,
|
|
226
372
|
playback: this.playback(v.playback),
|
|
227
373
|
offset: num(v.offset) ?? null,
|
|
@@ -233,7 +379,26 @@ var Reader = class {
|
|
|
233
379
|
return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
|
|
234
380
|
}) : null,
|
|
235
381
|
labelRect: this.labelRect(v.labelRect),
|
|
236
|
-
extension: this.extension(v.extension)
|
|
382
|
+
extension: this.extension(v.extension),
|
|
383
|
+
capacity: num(v.capacity) ?? null,
|
|
384
|
+
delay: num(v.delay) ?? null,
|
|
385
|
+
queue: v.queue === true ? true : null,
|
|
386
|
+
gauge: this.gauge(v.gauge)
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
gauge(v) {
|
|
390
|
+
if (!isObj(v)) return null;
|
|
391
|
+
this.fields(v, "gauge");
|
|
392
|
+
const finite = (x) => typeof x === "number" && Number.isFinite(x) ? x : null;
|
|
393
|
+
const style2 = str(v.style);
|
|
394
|
+
return {
|
|
395
|
+
metric: str(v.metric) ?? "",
|
|
396
|
+
of: typeof v.of === "string" && UUID.test(v.of) ? v.of.toUpperCase() : NO_ID,
|
|
397
|
+
max: finite(v.max),
|
|
398
|
+
warn: finite(v.warn),
|
|
399
|
+
alarm: finite(v.alarm),
|
|
400
|
+
style: style2 === "number" || style2 === "bar" || style2 === "dial" ? style2 : null,
|
|
401
|
+
label: str(v.label) ?? null
|
|
237
402
|
};
|
|
238
403
|
}
|
|
239
404
|
labelRect(v) {
|
|
@@ -257,13 +422,27 @@ var Reader = class {
|
|
|
257
422
|
}
|
|
258
423
|
return { id, shape, version, params };
|
|
259
424
|
}
|
|
425
|
+
variables(v) {
|
|
426
|
+
if (!isObj(v)) return null;
|
|
427
|
+
const out = {};
|
|
428
|
+
for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
|
|
429
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
430
|
+
}
|
|
260
431
|
click(v) {
|
|
261
432
|
if (!isObj(v)) return null;
|
|
262
433
|
this.fields(v, "click");
|
|
263
434
|
const url = str(v.url);
|
|
264
435
|
const safe = url === void 0 ? null : allowedURL(url);
|
|
265
436
|
if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
|
|
266
|
-
|
|
437
|
+
let set = null;
|
|
438
|
+
if (isObj(v.set)) {
|
|
439
|
+
set = {};
|
|
440
|
+
for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
|
|
441
|
+
if (Object.keys(set).length === 0) set = null;
|
|
442
|
+
}
|
|
443
|
+
const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
|
|
444
|
+
const toggle = raw ? raw.filter(isName) : null;
|
|
445
|
+
return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
|
|
267
446
|
}
|
|
268
447
|
playback(v) {
|
|
269
448
|
const s = str(v);
|
|
@@ -299,6 +478,35 @@ var Reader = class {
|
|
|
299
478
|
params: strings(v.params)
|
|
300
479
|
};
|
|
301
480
|
}
|
|
481
|
+
/** As DottieKit reads them: a reroute whose targets aren't all ids is dropped, as are keys that aren't ids. */
|
|
482
|
+
overrides(v) {
|
|
483
|
+
if (!Array.isArray(v)) return null;
|
|
484
|
+
return v.filter(isObj).map((o) => {
|
|
485
|
+
this.fields(o, "override");
|
|
486
|
+
const dash = str(o.dash);
|
|
487
|
+
let reroute = null;
|
|
488
|
+
if (isObj(o.reroute) && Object.values(o.reroute).every((x) => typeof x === "string" && UUID.test(x))) {
|
|
489
|
+
reroute = {};
|
|
490
|
+
for (const [k, x] of Object.entries(o.reroute)) if (UUID.test(k)) reroute[k.toUpperCase()] = x.toUpperCase();
|
|
491
|
+
if (Object.keys(reroute).length === 0) reroute = null;
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
when: str(o.when) ?? "",
|
|
495
|
+
stroke: rgba(o.stroke) ?? null,
|
|
496
|
+
fill: rgba(o.fill) ?? null,
|
|
497
|
+
textColor: rgba(o.textColor) ?? null,
|
|
498
|
+
opacity: num(o.opacity) ?? null,
|
|
499
|
+
strokeWidth: num(o.strokeWidth) ?? null,
|
|
500
|
+
dash: dash !== void 0 && ["solid", "dashed", "dotted"].includes(dash) ? dash : null,
|
|
501
|
+
text: str(o.text) ?? null,
|
|
502
|
+
reroute,
|
|
503
|
+
offline: o.offline === true ? true : null,
|
|
504
|
+
cost: typeof o.cost === "number" && Number.isFinite(o.cost) ? o.cost : null,
|
|
505
|
+
capacity: typeof o.capacity === "number" && Number.isFinite(o.capacity) ? o.capacity : null,
|
|
506
|
+
delay: typeof o.delay === "number" && Number.isFinite(o.delay) ? o.delay : null
|
|
507
|
+
};
|
|
508
|
+
});
|
|
509
|
+
}
|
|
302
510
|
hover(v) {
|
|
303
511
|
if (!isObj(v)) return null;
|
|
304
512
|
this.fields(v, "hover");
|
|
@@ -364,7 +572,7 @@ var Reader = class {
|
|
|
364
572
|
if (camera) stableSortBy(camera, (c) => c.t);
|
|
365
573
|
const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
|
|
366
574
|
const t = num(m.t);
|
|
367
|
-
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
|
|
575
|
+
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
|
|
368
576
|
}) : null;
|
|
369
577
|
if (markers) stableSortBy(markers, (m) => m.t);
|
|
370
578
|
const poster = num(v.poster);
|
|
@@ -397,7 +605,34 @@ var Reader = class {
|
|
|
397
605
|
elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
|
|
398
606
|
theme: str(v.theme) ?? null,
|
|
399
607
|
timeline: this.timeline(v.timeline),
|
|
400
|
-
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
|
|
608
|
+
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
|
|
609
|
+
variables: this.variables(v.variables),
|
|
610
|
+
flows: Array.isArray(v.flows) ? v.flows.filter(isObj).map((f) => this.flow(f)) : null
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/** As DottieKit reads one: a missing or bad id, source or destination sends nothing; bad destinations are dropped. */
|
|
614
|
+
flow(f) {
|
|
615
|
+
this.fields(f, "flow");
|
|
616
|
+
const id = (x) => typeof x === "string" && UUID.test(x) ? x.toUpperCase() : NO_ID;
|
|
617
|
+
const finite = (x) => {
|
|
618
|
+
const n = num(x);
|
|
619
|
+
return n !== void 0 && Number.isFinite(n) ? n : null;
|
|
620
|
+
};
|
|
621
|
+
return {
|
|
622
|
+
id: id(f.id),
|
|
623
|
+
from: id(f.from),
|
|
624
|
+
to: Array.isArray(f.to) && f.to.every((x) => typeof x === "string") ? f.to.filter((x) => UUID.test(x)).map((x) => x.toUpperCase()) : [],
|
|
625
|
+
every: finite(f.every),
|
|
626
|
+
speed: finite(f.speed),
|
|
627
|
+
size: finite(f.size),
|
|
628
|
+
color: isObj(f.color) && ["r", "g", "b", "a"].every((k) => typeof f.color[k] === "number") ? rgba(f.color) ?? null : null,
|
|
629
|
+
when: str(f.when) ?? null,
|
|
630
|
+
spread: f.spread === true ? true : null,
|
|
631
|
+
volume: finite(f.volume),
|
|
632
|
+
overrides: Array.isArray(f.overrides) && f.overrides.length > 0 ? f.overrides.filter(isObj).map((o) => {
|
|
633
|
+
this.fields(o, "flowOverride");
|
|
634
|
+
return { when: str(o.when) ?? "", volume: finite(o.volume) };
|
|
635
|
+
}) : null
|
|
401
636
|
};
|
|
402
637
|
}
|
|
403
638
|
};
|
|
@@ -459,7 +694,8 @@ function cloneElement(e) {
|
|
|
459
694
|
start: { ...e.connector.start },
|
|
460
695
|
end: { ...e.connector.end },
|
|
461
696
|
waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
|
|
462
|
-
} : null
|
|
697
|
+
} : null,
|
|
698
|
+
gauge: e.gauge ? { ...e.gauge } : e.gauge
|
|
463
699
|
};
|
|
464
700
|
}
|
|
465
701
|
function orderedElements(page) {
|
|
@@ -1210,8 +1446,617 @@ function mulConst(step) {
|
|
|
1210
1446
|
return [p[0], p[1]];
|
|
1211
1447
|
}
|
|
1212
1448
|
|
|
1449
|
+
// src/flows.ts
|
|
1450
|
+
var DEFAULT_EVERY = 0.5;
|
|
1451
|
+
var DEFAULT_SPEED = 160;
|
|
1452
|
+
var DEFAULT_SIZE = 12;
|
|
1453
|
+
var DEFAULT_COLOR = { r: 0.95, g: 0.36, b: 0.33, a: 1 };
|
|
1454
|
+
var clamp = (v, fallback, lo, hi) => v === null || !Number.isFinite(v) ? fallback : Math.min(Math.max(v, lo), hi);
|
|
1455
|
+
function packetID(flow, k) {
|
|
1456
|
+
const n = (k + 2 ** 40) % 2 ** 48;
|
|
1457
|
+
return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
|
|
1458
|
+
}
|
|
1459
|
+
var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
|
|
1460
|
+
function flowTree(from, frame2, offline = /* @__PURE__ */ new Set()) {
|
|
1461
|
+
const shapes2 = new Set(frame2.elements.filter((e) => e.kind !== "connector" && !offline.has(e.id)).map((e) => e.id));
|
|
1462
|
+
if (!shapes2.has(from)) return null;
|
|
1463
|
+
const next = /* @__PURE__ */ new Map();
|
|
1464
|
+
const add2 = (a, edge) => {
|
|
1465
|
+
const list = next.get(a);
|
|
1466
|
+
if (list) list.push(edge);
|
|
1467
|
+
else next.set(a, [edge]);
|
|
1468
|
+
};
|
|
1469
|
+
for (const e of frame2.elements) {
|
|
1470
|
+
const c = e.connector;
|
|
1471
|
+
const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
|
|
1472
|
+
if (!c || offline.has(e.id) || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
|
|
1473
|
+
const cost = routeCost(c.cost);
|
|
1474
|
+
add2(a, { to: b, via: e.id, back: false, cost });
|
|
1475
|
+
if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
|
|
1476
|
+
}
|
|
1477
|
+
const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
|
|
1478
|
+
let counter = 1;
|
|
1479
|
+
const tree = new FlowTree(from);
|
|
1480
|
+
const open = [from];
|
|
1481
|
+
while (open.length > 0) {
|
|
1482
|
+
let best = 0;
|
|
1483
|
+
for (let i = 1; i < open.length; i++) {
|
|
1484
|
+
const da = dist.get(open[i]), db = dist.get(open[best]);
|
|
1485
|
+
if (da < db || da === db && order.get(open[i]) < order.get(open[best])) best = i;
|
|
1486
|
+
}
|
|
1487
|
+
const n = open.splice(best, 1)[0];
|
|
1488
|
+
tree.settled.add(n);
|
|
1489
|
+
for (const edge of next.get(n) ?? []) {
|
|
1490
|
+
if (tree.settled.has(edge.to)) continue;
|
|
1491
|
+
const d = dist.get(n) + edge.cost;
|
|
1492
|
+
const old = dist.get(edge.to);
|
|
1493
|
+
if (old !== void 0 && old <= d) continue;
|
|
1494
|
+
if (old === void 0) open.push(edge.to);
|
|
1495
|
+
dist.set(edge.to, d);
|
|
1496
|
+
order.set(edge.to, counter++);
|
|
1497
|
+
tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return tree;
|
|
1501
|
+
}
|
|
1502
|
+
var FlowTree = class {
|
|
1503
|
+
constructor(from) {
|
|
1504
|
+
__publicField(this, "settled", /* @__PURE__ */ new Set());
|
|
1505
|
+
__publicField(this, "came", /* @__PURE__ */ new Map());
|
|
1506
|
+
__publicField(this, "from");
|
|
1507
|
+
this.from = from;
|
|
1508
|
+
}
|
|
1509
|
+
/** The first of `to` that can be reached; with `spread`, when the first can't be, every other one that can. */
|
|
1510
|
+
goals(to, spread) {
|
|
1511
|
+
const reachable = to.filter((x) => x !== this.from && this.settled.has(x));
|
|
1512
|
+
if (reachable.length === 0) return [];
|
|
1513
|
+
return spread && reachable[0] !== to[0] ? reachable : [reachable[0]];
|
|
1514
|
+
}
|
|
1515
|
+
/** The connectors to `goal`, as [connector, backwards] hops. */
|
|
1516
|
+
hops(goal) {
|
|
1517
|
+
const hops = [];
|
|
1518
|
+
for (let at = goal, step = this.came.get(at); step; at = step.from, step = this.came.get(at)) hops.push([step.via, step.back]);
|
|
1519
|
+
return hops.reverse();
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
// src/measure.ts
|
|
1524
|
+
var emptyMetrics = () => ({ elements: /* @__PURE__ */ new Map(), flows: /* @__PURE__ */ new Map(), carry: {} });
|
|
1525
|
+
function metricValue(m, metric, id) {
|
|
1526
|
+
const f = m.flows.get(id);
|
|
1527
|
+
if (f) {
|
|
1528
|
+
switch (metric) {
|
|
1529
|
+
case "throughput":
|
|
1530
|
+
return f.placed;
|
|
1531
|
+
case "backlog":
|
|
1532
|
+
return f.backlog;
|
|
1533
|
+
case "backup":
|
|
1534
|
+
return f.backup;
|
|
1535
|
+
case "transit":
|
|
1536
|
+
return f.transit;
|
|
1537
|
+
default:
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const e = m.elements.get(id);
|
|
1542
|
+
if (!e) return null;
|
|
1543
|
+
switch (metric) {
|
|
1544
|
+
case "throughput":
|
|
1545
|
+
return e.throughput;
|
|
1546
|
+
case "load":
|
|
1547
|
+
return e.load;
|
|
1548
|
+
case "delay":
|
|
1549
|
+
return e.delay;
|
|
1550
|
+
case "backlog":
|
|
1551
|
+
return e.backlog;
|
|
1552
|
+
case "inFlight":
|
|
1553
|
+
return e.inFlight;
|
|
1554
|
+
case "depth":
|
|
1555
|
+
return e.depth;
|
|
1556
|
+
default:
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
var TINY = 1e-9;
|
|
1561
|
+
var amount = (f) => clamp(f.volume, 1, 0, 1e12);
|
|
1562
|
+
function amountIn(f, values) {
|
|
1563
|
+
if (values === null || !f.overrides) return amount(f);
|
|
1564
|
+
let v = null;
|
|
1565
|
+
for (const o of f.overrides) if (o.volume !== null && !mentionsMetric(o.when) && holds(o.when, values)) v = o.volume;
|
|
1566
|
+
return v === null ? amount(f) : Math.min(Math.max(Number.isFinite(v) ? v : 1, 0), 1e12);
|
|
1567
|
+
}
|
|
1568
|
+
function delayOf(e) {
|
|
1569
|
+
const d = e.delay ?? null;
|
|
1570
|
+
if (e.kind === "connector" || d === null || !Number.isFinite(d)) return 0;
|
|
1571
|
+
return Math.min(Math.max(d, 0), 3600);
|
|
1572
|
+
}
|
|
1573
|
+
var queueKey = (shape, flow) => shape + "|" + flow;
|
|
1574
|
+
function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
|
|
1575
|
+
const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
|
|
1576
|
+
const byID = /* @__PURE__ */ new Map();
|
|
1577
|
+
for (const e of frame2.elements) if (!byID.has(e.id)) byID.set(e.id, e);
|
|
1578
|
+
const metrics2 = emptyMetrics();
|
|
1579
|
+
for (const e of frame2.elements) {
|
|
1580
|
+
if (e.kind === "connector" && !e.connector) continue;
|
|
1581
|
+
metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
|
|
1582
|
+
}
|
|
1583
|
+
const list = flows ?? [];
|
|
1584
|
+
const room = /* @__PURE__ */ new Map();
|
|
1585
|
+
for (const e of frame2.elements) {
|
|
1586
|
+
const c = e.capacity ?? null;
|
|
1587
|
+
if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
|
|
1588
|
+
}
|
|
1589
|
+
const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
|
|
1590
|
+
const route2 = (tree, goal) => {
|
|
1591
|
+
const hops = [], shapes2 = [];
|
|
1592
|
+
let at = goal;
|
|
1593
|
+
for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
|
|
1594
|
+
hops.push([step.via, step.back]);
|
|
1595
|
+
shapes2.push(at);
|
|
1596
|
+
at = step.from;
|
|
1597
|
+
}
|
|
1598
|
+
shapes2.push(at);
|
|
1599
|
+
return { hops: hops.reverse(), shapes: shapes2.reverse() };
|
|
1600
|
+
};
|
|
1601
|
+
const way = (hops, shapes2) => {
|
|
1602
|
+
const points = [], inside = [], stretches = [];
|
|
1603
|
+
let length = 0;
|
|
1604
|
+
hops.forEach(([id, back], k) => {
|
|
1605
|
+
const c = byID.get(id);
|
|
1606
|
+
if (!c) return;
|
|
1607
|
+
const line2 = back ? [...tracing(c, frame2).points].reverse() : tracing(c, frame2).points;
|
|
1608
|
+
if (line2.length === 0) return;
|
|
1609
|
+
const last = points[points.length - 1];
|
|
1610
|
+
if (last) {
|
|
1611
|
+
const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
|
|
1612
|
+
const shape = byID.get(shapes2[k]);
|
|
1613
|
+
inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
|
|
1614
|
+
length += gap;
|
|
1615
|
+
}
|
|
1616
|
+
const start = length;
|
|
1617
|
+
for (let q = 1; q < line2.length; q++) length += Math.hypot(line2[q].x - line2[q - 1].x, line2[q].y - line2[q - 1].y);
|
|
1618
|
+
stretches.push([id, start, length]);
|
|
1619
|
+
points.push(...line2);
|
|
1620
|
+
});
|
|
1621
|
+
return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
|
|
1622
|
+
};
|
|
1623
|
+
const along = (w, elapsedAt, speed) => {
|
|
1624
|
+
let elapsed = elapsedAt, d = 0;
|
|
1625
|
+
for (const [a, b, delay] of w.inside) {
|
|
1626
|
+
const reach = (a - d) / speed;
|
|
1627
|
+
if (elapsed <= reach) break;
|
|
1628
|
+
elapsed -= reach;
|
|
1629
|
+
d = a;
|
|
1630
|
+
const stay = (b - a) / speed + delay;
|
|
1631
|
+
if (elapsed < stay) return null;
|
|
1632
|
+
elapsed -= stay;
|
|
1633
|
+
d = b;
|
|
1634
|
+
}
|
|
1635
|
+
d += elapsed * speed;
|
|
1636
|
+
return d >= 0 && d <= w.length ? d : null;
|
|
1637
|
+
};
|
|
1638
|
+
const union2 = (a, b) => /* @__PURE__ */ new Set([...a, ...b]);
|
|
1639
|
+
const bump = (id, f) => {
|
|
1640
|
+
const m = metrics2.elements.get(id);
|
|
1641
|
+
if (m) f(m);
|
|
1642
|
+
};
|
|
1643
|
+
const leaving = /* @__PURE__ */ new Map(), turn = [];
|
|
1644
|
+
for (const flow of list) {
|
|
1645
|
+
const n = leaving.get(flow.from) ?? 0;
|
|
1646
|
+
turn.push(n);
|
|
1647
|
+
leaving.set(flow.from, n + 1);
|
|
1648
|
+
}
|
|
1649
|
+
const biggest = list.length > 0 ? Math.max(...list.map(amount)) : 1;
|
|
1650
|
+
const packets = [];
|
|
1651
|
+
const waitingBy = /* @__PURE__ */ new Map();
|
|
1652
|
+
const everyOf = (flow) => Math.min(Math.max(clamp(flow.every, DEFAULT_EVERY, 0.05, 60) * biggest / Math.max(amountIn(flow, values), TINY), 0.05), 60);
|
|
1653
|
+
for (const [i, flow] of list.entries()) {
|
|
1654
|
+
if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
|
|
1655
|
+
const source = byID.get(flow.from);
|
|
1656
|
+
const base2 = source ? flowTree(flow.from, frame2, offline) : null;
|
|
1657
|
+
if (!source || !base2) continue;
|
|
1658
|
+
const volume = amountIn(flow, values);
|
|
1659
|
+
const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
|
|
1660
|
+
const placements = [];
|
|
1661
|
+
const waiting = [];
|
|
1662
|
+
const goals = base2.goals(flow.to, flow.spread === true);
|
|
1663
|
+
const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
|
|
1664
|
+
for (const [candidates, share] of shares) {
|
|
1665
|
+
if (!(share > TINY)) continue;
|
|
1666
|
+
let left = share;
|
|
1667
|
+
for (let n = 0; n < 32 && left > TINY; n++) {
|
|
1668
|
+
const tree = flowTree(flow.from, frame2, union2(offline, full()));
|
|
1669
|
+
const goal2 = tree?.goals(candidates, false)[0];
|
|
1670
|
+
if (!tree || goal2 === void 0) break;
|
|
1671
|
+
const r = route2(tree, goal2);
|
|
1672
|
+
const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
|
|
1673
|
+
const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
|
|
1674
|
+
const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
|
|
1675
|
+
if (!(fits > TINY)) break;
|
|
1676
|
+
for (const id of ids) if (room.has(id)) room.set(id, room.get(id) - fits);
|
|
1677
|
+
placements.push({ hops: r.hops, shapes: r.shapes, goal: goal2, amount: fits });
|
|
1678
|
+
left -= fits;
|
|
1679
|
+
}
|
|
1680
|
+
if (!(left > TINY)) continue;
|
|
1681
|
+
let at = flow.from;
|
|
1682
|
+
const goal = base2.goals(candidates, false)[0];
|
|
1683
|
+
if (goal !== void 0) {
|
|
1684
|
+
const r = route2(base2, goal), done = full();
|
|
1685
|
+
const sequence = [[r.shapes[0], r.shapes[0]]];
|
|
1686
|
+
r.hops.forEach(([id], k) => {
|
|
1687
|
+
sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
|
|
1688
|
+
});
|
|
1689
|
+
const first = sequence.find(([id]) => done.has(id));
|
|
1690
|
+
if (first) {
|
|
1691
|
+
at = first[1];
|
|
1692
|
+
const upTo = r.shapes.indexOf(at);
|
|
1693
|
+
for (let q = upTo; q >= 0; q--) {
|
|
1694
|
+
const s = byID.get(r.shapes[q]);
|
|
1695
|
+
if (s && s.queue === true && s.kind !== "connector") {
|
|
1696
|
+
at = r.shapes[q];
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
const n = r.shapes.indexOf(at);
|
|
1702
|
+
if (at !== flow.from && n >= 0) {
|
|
1703
|
+
const shapes2 = r.shapes.slice(0, n + 1), hops = r.hops.slice(0, n);
|
|
1704
|
+
for (const id of [...shapes2, ...hops.map(([h]) => h)]) if (room.has(id)) room.set(id, Math.max(0, room.get(id) - left));
|
|
1705
|
+
placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const existing = waiting.find(([s]) => s === at);
|
|
1709
|
+
if (existing) existing[1] += left;
|
|
1710
|
+
else waiting.push([at, left]);
|
|
1711
|
+
}
|
|
1712
|
+
waitingBy.set(flow.id, waiting);
|
|
1713
|
+
for (const p of placements) {
|
|
1714
|
+
for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
|
|
1715
|
+
m.throughput += p.amount;
|
|
1716
|
+
});
|
|
1717
|
+
if (!p.waits) fm.placed += p.amount;
|
|
1718
|
+
}
|
|
1719
|
+
for (const [at, a] of waiting) {
|
|
1720
|
+
bump(at, (m) => {
|
|
1721
|
+
m.backlog += a;
|
|
1722
|
+
});
|
|
1723
|
+
fm.backlog += a;
|
|
1724
|
+
}
|
|
1725
|
+
const reachedFirst = placements.filter((p) => !p.waits && p.goal === flow.to[0]).reduce((s, p) => s + p.amount, 0);
|
|
1726
|
+
fm.backup = volume > TINY ? Math.max(0, (volume - reachedFirst) / volume) : 0;
|
|
1727
|
+
const every = everyOf(flow);
|
|
1728
|
+
const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
|
|
1729
|
+
const phase = turn[i] / leaving.get(flow.from) * every;
|
|
1730
|
+
const color2 = flow.color ?? DEFAULT_COLOR;
|
|
1731
|
+
const cumulative = [];
|
|
1732
|
+
let sum = 0;
|
|
1733
|
+
for (const p of placements) {
|
|
1734
|
+
sum += p.amount / Math.max(volume, TINY);
|
|
1735
|
+
cumulative.push(sum);
|
|
1736
|
+
}
|
|
1737
|
+
if (cumulative.length > 0 && Math.abs(cumulative[cumulative.length - 1] - 1) < 1e-9) cumulative[cumulative.length - 1] = 1;
|
|
1738
|
+
let transitSum = 0;
|
|
1739
|
+
placements.forEach((p, r) => {
|
|
1740
|
+
const w = way(p.hops, p.shapes);
|
|
1741
|
+
if (!w) return;
|
|
1742
|
+
const travel = w.length / speed + w.inside.reduce((s, x) => s + x[2], 0);
|
|
1743
|
+
if (!p.waits) transitSum += travel * p.amount;
|
|
1744
|
+
const lo = Math.ceil((t - phase - travel) / every);
|
|
1745
|
+
const hi = Math.floor((t - phase) / every);
|
|
1746
|
+
const below = r === 0 ? 0 : cumulative[r - 1], upTo = cumulative[r];
|
|
1747
|
+
const dealt = (c, k) => Math.floor((k + 1) * c) - Math.floor(k * c);
|
|
1748
|
+
for (let k = Math.max(lo, hi - 3999); k <= hi; k++) {
|
|
1749
|
+
if (dealt(upTo, k) - dealt(below, k) !== 1) continue;
|
|
1750
|
+
const d = along(w, t - k * every - phase, speed);
|
|
1751
|
+
if (d === null) continue;
|
|
1752
|
+
const s = w.stretches.find(([, a, b]) => d >= a && d <= b);
|
|
1753
|
+
if (s) bump(s[0], (m) => {
|
|
1754
|
+
m.inFlight += 1;
|
|
1755
|
+
});
|
|
1756
|
+
const pt2 = w.line.point(d / w.length);
|
|
1757
|
+
packets.push(packet(packetID(flow.id, k), pt2.x - size / 2, pt2.y - size / 2, size, color2, source.layerID));
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
fm.transit = fm.placed > TINY ? transitSum / fm.placed : null;
|
|
1761
|
+
metrics2.flows.set(flow.id, fm);
|
|
1762
|
+
}
|
|
1763
|
+
const piles = /* @__PURE__ */ new Map();
|
|
1764
|
+
for (const flow of list) {
|
|
1765
|
+
const waiting = waitingBy.get(flow.id) ?? [];
|
|
1766
|
+
const shapes2 = waiting.map(([s]) => s);
|
|
1767
|
+
for (const e of frame2.elements) {
|
|
1768
|
+
const c = carried[queueKey(e.id, flow.id)];
|
|
1769
|
+
if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
|
|
1770
|
+
}
|
|
1771
|
+
if (shapes2.length === 0) continue;
|
|
1772
|
+
const volume = amountIn(flow, values);
|
|
1773
|
+
const every = everyOf(flow);
|
|
1774
|
+
const unit = every * Math.max(volume, TINY);
|
|
1775
|
+
const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
|
|
1776
|
+
const color2 = flow.color ?? DEFAULT_COLOR;
|
|
1777
|
+
shapes2.forEach((at, j) => {
|
|
1778
|
+
const shape = byID.get(at);
|
|
1779
|
+
if (!shape) return;
|
|
1780
|
+
const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
|
|
1781
|
+
const backlog = waiting.find(([s]) => s === at)?.[1] ?? 0;
|
|
1782
|
+
const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
|
|
1783
|
+
let depth = before + backlog * since;
|
|
1784
|
+
if (backlog <= TINY && before > TINY) {
|
|
1785
|
+
const trial = new Map(room);
|
|
1786
|
+
const outlets = [];
|
|
1787
|
+
let total = 0;
|
|
1788
|
+
for (let n2 = 0; n2 < 32; n2++) {
|
|
1789
|
+
const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
|
|
1790
|
+
const tree = flowTree(at, frame2, union2(offline, blocked));
|
|
1791
|
+
const goal = tree?.goals(flow.to, false)[0];
|
|
1792
|
+
if (!tree || goal === void 0) break;
|
|
1793
|
+
const r = route2(tree, goal);
|
|
1794
|
+
const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
|
|
1795
|
+
const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
|
|
1796
|
+
const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
|
|
1797
|
+
const rate = Number.isFinite(spare) ? spare : Math.max(before / 0.5 - total, TINY * 2);
|
|
1798
|
+
if (!(rate > TINY)) break;
|
|
1799
|
+
for (const id of ids) if (trial.has(id)) trial.set(id, trial.get(id) - rate);
|
|
1800
|
+
outlets.push({ hops: r.hops, shapes: r.shapes, rate });
|
|
1801
|
+
total += rate;
|
|
1802
|
+
if (!Number.isFinite(spare)) break;
|
|
1803
|
+
}
|
|
1804
|
+
if (total > TINY) {
|
|
1805
|
+
depth = Math.max(0, before - total * since);
|
|
1806
|
+
if (depth > TINY) {
|
|
1807
|
+
for (const [id, v] of trial) room.set(id, v);
|
|
1808
|
+
for (const o of outlets) for (const id of [...o.shapes, ...o.hops.map(([h]) => h)]) bump(id, (m) => {
|
|
1809
|
+
m.throughput += o.rate;
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
const lasts = before / total;
|
|
1813
|
+
outlets.forEach((o, i) => {
|
|
1814
|
+
const w = way(o.hops, o.shapes);
|
|
1815
|
+
if (!w) return;
|
|
1816
|
+
const spacing = Math.max(unit / o.rate, 0.05);
|
|
1817
|
+
for (let k = 0; k * spacing < lasts && k * spacing <= since && k < 2048; k++) {
|
|
1818
|
+
const d = along(w, since - k * spacing, speed);
|
|
1819
|
+
if (d === null) continue;
|
|
1820
|
+
const pt2 = w.line.point(d / w.length);
|
|
1821
|
+
packets.push(packet(packetID(flow.id, 2 ** 39 + 2 ** 20 + j * 2 ** 14 + i * 2 ** 11 + k), pt2.x - size / 2, pt2.y - size / 2, size, color2, layer));
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (!(depth > TINY || backlog > TINY)) return;
|
|
1827
|
+
bump(at, (m) => {
|
|
1828
|
+
m.depth += depth;
|
|
1829
|
+
});
|
|
1830
|
+
if (depth > TINY) metrics2.carry[queueKey(at, flow.id)] = depth;
|
|
1831
|
+
const n = Math.min(Math.max(Math.ceil(depth / unit - 1e-9), 1), 30);
|
|
1832
|
+
const rows = Math.trunc((n + 9) / 10);
|
|
1833
|
+
const base2 = piles.get(at) ?? 0;
|
|
1834
|
+
piles.set(at, base2 + rows);
|
|
1835
|
+
for (let q = 0; q < n; q++) {
|
|
1836
|
+
const cx = shape.x + 2 + size / 2 + q % 10 * (size + 3);
|
|
1837
|
+
const cy = shape.y - 4 - size / 2 - (base2 + Math.trunc(q / 10)) * (size + 3);
|
|
1838
|
+
packets.push(packet(packetID(flow.id, 2 ** 39 + j * 64 + q), cx - size / 2, cy - size / 2, size, color2, layer));
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
for (const e of frame2.elements) {
|
|
1843
|
+
const c = e.capacity ?? null;
|
|
1844
|
+
const m = metrics2.elements.get(e.id);
|
|
1845
|
+
if (c === null || !Number.isFinite(c) || !m) continue;
|
|
1846
|
+
m.load = c > 0 ? m.throughput / c : 1;
|
|
1847
|
+
}
|
|
1848
|
+
return { packets, metrics: metrics2 };
|
|
1849
|
+
}
|
|
1850
|
+
var BLANK = {
|
|
1851
|
+
seed: 1,
|
|
1852
|
+
text: null,
|
|
1853
|
+
connector: null,
|
|
1854
|
+
groupID: null,
|
|
1855
|
+
points: null,
|
|
1856
|
+
path: null,
|
|
1857
|
+
subpaths: null,
|
|
1858
|
+
evenOdd: null,
|
|
1859
|
+
rotation: null,
|
|
1860
|
+
link: null,
|
|
1861
|
+
url: null,
|
|
1862
|
+
anchor: null,
|
|
1863
|
+
click: null,
|
|
1864
|
+
hover: null,
|
|
1865
|
+
when: null,
|
|
1866
|
+
overrides: null,
|
|
1867
|
+
symbol: null,
|
|
1868
|
+
playback: null,
|
|
1869
|
+
offset: null,
|
|
1870
|
+
args: null,
|
|
1871
|
+
image: null,
|
|
1872
|
+
reveal: null
|
|
1873
|
+
};
|
|
1874
|
+
function style(stroke, fill, strokeWidth, fillStyle = "solid", opacity2 = null) {
|
|
1875
|
+
return {
|
|
1876
|
+
stroke: { ...stroke },
|
|
1877
|
+
fill: { ...fill },
|
|
1878
|
+
fillStyle,
|
|
1879
|
+
strokeWidth,
|
|
1880
|
+
dash: "solid",
|
|
1881
|
+
roughness: 0,
|
|
1882
|
+
bowing: 1,
|
|
1883
|
+
fontSize: 18,
|
|
1884
|
+
font: null,
|
|
1885
|
+
gridSize: null,
|
|
1886
|
+
opacity: opacity2,
|
|
1887
|
+
dashOffset: null,
|
|
1888
|
+
textColor: null
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function packet(id, x, y, size, color2, layerID) {
|
|
1892
|
+
return { ...BLANK, id, kind: "ellipse", layerID, x, y, width: size, height: size, style: style(color2, color2, 1.5) };
|
|
1893
|
+
}
|
|
1894
|
+
function metricLookup(m, frame2) {
|
|
1895
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
1896
|
+
for (const e of frame2.elements) if (e.anchor !== null && !anchors.has(e.anchor)) anchors.set(e.anchor, e.id);
|
|
1897
|
+
const UUID2 = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
|
|
1898
|
+
return (term, own) => {
|
|
1899
|
+
const id = term.target === "" ? own : anchors.get(term.target) ?? (UUID2.test(term.target) ? term.target.toUpperCase() : null);
|
|
1900
|
+
return id === null ? null : metricValue(m, term.metric, id);
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
|
|
1904
|
+
const lookup = metricLookup(metrics2, frame2);
|
|
1905
|
+
const out = [], readings = {};
|
|
1906
|
+
for (let e of elements) {
|
|
1907
|
+
if (values !== null && e.overrides) {
|
|
1908
|
+
for (const o of e.overrides) {
|
|
1909
|
+
const own = e.id;
|
|
1910
|
+
if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, own))) e = applyOverride(looksOnly(o), e);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
if (!e.gauge || values === null) {
|
|
1914
|
+
out.push(e);
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
const g = e.gauge;
|
|
1918
|
+
let shown = metricValue(metrics2, g.metric, g.of);
|
|
1919
|
+
const then = carried[gaugeKey(e.id)];
|
|
1920
|
+
if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
|
|
1921
|
+
const f = Math.max(0, since / GAUGE_EASE), smooth = f * f * (3 - 2 * f);
|
|
1922
|
+
shown = then + (shown - then) * smooth;
|
|
1923
|
+
}
|
|
1924
|
+
if (shown !== null) readings[gaugeKey(e.id)] = shown;
|
|
1925
|
+
out.push(...gauge(e, g, shown, frame2, flows));
|
|
1926
|
+
}
|
|
1927
|
+
return { elements: out, readings };
|
|
1928
|
+
}
|
|
1929
|
+
var gaugeKey = (id) => "gauge|" + id;
|
|
1930
|
+
var GAUGE_EASE = 1;
|
|
1931
|
+
var hex = (h) => {
|
|
1932
|
+
const v = parseInt(h.slice(1), 16);
|
|
1933
|
+
return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
|
|
1934
|
+
};
|
|
1935
|
+
var GAUGE_GREEN = hex("#16a34a");
|
|
1936
|
+
var GAUGE_AMBER = hex("#d97706");
|
|
1937
|
+
var GAUGE_RED = hex("#dc2626");
|
|
1938
|
+
var GAUGE_BLUE = hex("#2f6fdf");
|
|
1939
|
+
var GAUGE_TRACK = hex("#e5e7eb");
|
|
1940
|
+
var GAUGE_GRAY = hex("#9ca3af");
|
|
1941
|
+
function formatted(metric, v) {
|
|
1942
|
+
if (v === null || !Number.isFinite(v)) return "\u2013";
|
|
1943
|
+
switch (metric) {
|
|
1944
|
+
case "load":
|
|
1945
|
+
case "backup":
|
|
1946
|
+
return formatNumber(v * 100, 0) + "%";
|
|
1947
|
+
case "transit":
|
|
1948
|
+
case "delay":
|
|
1949
|
+
return formatNumber(v, Math.abs(v) >= 10 ? 1 : 2) + "s";
|
|
1950
|
+
default:
|
|
1951
|
+
return formatNumber(v, Math.abs(v) >= 100 ? 0 : Math.abs(v) >= 10 ? 1 : 2);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
function formatNumber(v, places) {
|
|
1955
|
+
const scale = places === 0 ? 1 : places === 1 ? 10 : 100;
|
|
1956
|
+
const n = Math.floor(Math.abs(v) * scale + 0.5);
|
|
1957
|
+
if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
|
|
1958
|
+
const whole = Math.trunc(n / scale), part = n - whole * scale;
|
|
1959
|
+
let s = String(whole);
|
|
1960
|
+
if (places > 0 && part > 0) {
|
|
1961
|
+
let digits = String(part);
|
|
1962
|
+
while (digits.length < places) digits = "0" + digits;
|
|
1963
|
+
while (digits.endsWith("0")) digits = digits.slice(0, -1);
|
|
1964
|
+
s += "." + digits;
|
|
1965
|
+
}
|
|
1966
|
+
return (v < 0 && n > 0 ? "-" : "") + s;
|
|
1967
|
+
}
|
|
1968
|
+
function gauge(e, g, value2, frame2, flows) {
|
|
1969
|
+
const target = frame2.elements.find((x) => x.id === g.of);
|
|
1970
|
+
const full = g.max ?? (g.metric === "load" ? 1 : target?.capacity ?? value2 ?? 1);
|
|
1971
|
+
const fraction = value2 === null ? 0 : full > 0 ? Math.min(Math.max(value2 / full, 0), 1) : 0;
|
|
1972
|
+
const warn = g.warn ?? (g.metric === "load" ? 0.7 : null), alarm = g.alarm ?? (g.metric === "load" ? 0.9 : null);
|
|
1973
|
+
let color2 = warn === null && alarm === null ? GAUGE_BLUE : GAUGE_GREEN;
|
|
1974
|
+
if (value2 !== null && warn !== null && value2 >= warn) color2 = GAUGE_AMBER;
|
|
1975
|
+
if (value2 !== null && alarm !== null && value2 >= alarm) color2 = GAUGE_RED;
|
|
1976
|
+
if (value2 === null) color2 = GAUGE_GRAY;
|
|
1977
|
+
let name = g.label;
|
|
1978
|
+
if (name === null) {
|
|
1979
|
+
const what = target ? target.text !== null ? target.text.split("\n")[0] : target.anchor : null;
|
|
1980
|
+
name = `${g.metric} ${what ?? (flows.some((f) => f.id === g.of) ? "flow" : "?")}`;
|
|
1981
|
+
}
|
|
1982
|
+
const kind = g.style ?? "number";
|
|
1983
|
+
const w = Math.max(e.width, 1), h = Math.max(e.height, 1);
|
|
1984
|
+
const line2 = (a, b) => [
|
|
1985
|
+
a.x / w,
|
|
1986
|
+
a.y / h,
|
|
1987
|
+
(a.x + (b.x - a.x) / 3) / w,
|
|
1988
|
+
(a.y + (b.y - a.y) / 3) / h,
|
|
1989
|
+
(a.x + 2 * (b.x - a.x) / 3) / w,
|
|
1990
|
+
(a.y + 2 * (b.y - a.y) / 3) / h,
|
|
1991
|
+
b.x / w,
|
|
1992
|
+
b.y / h
|
|
1993
|
+
];
|
|
1994
|
+
const box = (x, y, bw, bh) => {
|
|
1995
|
+
const p = [{ x, y }, { x: x + bw, y }, { x: x + bw, y: y + bh }, { x, y: y + bh }];
|
|
1996
|
+
let coords = line2(p[0], p[1]);
|
|
1997
|
+
for (let k = 1; k < 4; k++) coords = coords.concat(line2(p[k], p[(k + 1) % 4]).slice(2));
|
|
1998
|
+
return { closed: true, coords };
|
|
1999
|
+
};
|
|
2000
|
+
const arc = (c, radius, a0, a1) => {
|
|
2001
|
+
const pieces2 = Math.max(1, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 2) - 1e-9));
|
|
2002
|
+
const step = (a1 - a0) / pieces2, kappa = 4 / 3 * Math.tan(step / 4);
|
|
2003
|
+
const at = (a) => ({ x: c.x + radius * Math.cos(a), y: c.y + radius * Math.sin(a) });
|
|
2004
|
+
const s = at(a0);
|
|
2005
|
+
const coords = [s.x / w, s.y / h];
|
|
2006
|
+
for (let k = 0; k < pieces2; k++) {
|
|
2007
|
+
const a = a0 + k * step, b = a + step;
|
|
2008
|
+
const p0 = at(a), p3 = at(b);
|
|
2009
|
+
const c1 = { x: p0.x - kappa * radius * Math.sin(a), y: p0.y + kappa * radius * Math.cos(a) };
|
|
2010
|
+
const c2 = { x: p3.x + kappa * radius * Math.sin(b), y: p3.y - kappa * radius * Math.cos(b) };
|
|
2011
|
+
coords.push(c1.x / w, c1.y / h, c2.x / w, c2.y / h, p3.x / w, p3.y / h);
|
|
2012
|
+
}
|
|
2013
|
+
return { closed: false, coords };
|
|
2014
|
+
};
|
|
2015
|
+
const part = (k, subpaths, stroke, fill, width) => ({
|
|
2016
|
+
...BLANK,
|
|
2017
|
+
id: packetID(e.id, -(2 ** 38) - k),
|
|
2018
|
+
kind: "path",
|
|
2019
|
+
layerID: e.layerID,
|
|
2020
|
+
x: e.x,
|
|
2021
|
+
y: e.y,
|
|
2022
|
+
width: e.width,
|
|
2023
|
+
height: e.height,
|
|
2024
|
+
style: style(stroke, fill ?? { r: 0, g: 0, b: 0, a: 0 }, width, "solid", e.style.opacity),
|
|
2025
|
+
subpaths
|
|
2026
|
+
});
|
|
2027
|
+
const back = {
|
|
2028
|
+
...e,
|
|
2029
|
+
gauge: null,
|
|
2030
|
+
kind: "path",
|
|
2031
|
+
subpaths: [box(0, 0, w, h)],
|
|
2032
|
+
text: name + "\n" + formatted(g.metric, value2),
|
|
2033
|
+
style: { ...e.style }
|
|
2034
|
+
};
|
|
2035
|
+
const parts = [];
|
|
2036
|
+
if (kind === "number") {
|
|
2037
|
+
back.labelRect = { x: 0, y: 0, width: 1, height: 1 };
|
|
2038
|
+
back.style.textColor = { ...color2 };
|
|
2039
|
+
} else if (kind === "bar") {
|
|
2040
|
+
back.labelRect = { x: 0, y: 0, width: 1, height: 0.66 };
|
|
2041
|
+
const inset2 = Math.min(10, w * 0.08), barH = Math.min(12, h * 0.18), top = h * 0.72;
|
|
2042
|
+
const trackW = w - 2 * inset2;
|
|
2043
|
+
parts.push(part(1, [box(inset2, top, trackW, barH)], GAUGE_TRACK, GAUGE_TRACK, 0.5));
|
|
2044
|
+
if (fraction > 0) parts.push(part(2, [box(inset2, top, trackW * fraction, barH)], color2, color2, 0.5));
|
|
2045
|
+
} else {
|
|
2046
|
+
back.labelRect = { x: 0, y: 0.54, width: 1, height: 0.46 };
|
|
2047
|
+
const radius = Math.min(w * 0.38, h * 0.4), center2 = { x: w / 2, y: h * 0.5 };
|
|
2048
|
+
const thick = Math.max(2, radius * 0.16);
|
|
2049
|
+
parts.push(part(1, [arc(center2, radius, Math.PI, 2 * Math.PI)], GAUGE_TRACK, null, thick));
|
|
2050
|
+
if (fraction > 0) parts.push(part(2, [arc(center2, radius, Math.PI, Math.PI + Math.PI * fraction)], color2, null, thick));
|
|
2051
|
+
const a = Math.PI + Math.PI * fraction;
|
|
2052
|
+
const tip = { x: center2.x + radius * 0.85 * Math.cos(a), y: center2.y + radius * 0.85 * Math.sin(a) };
|
|
2053
|
+
parts.push(part(3, [{ closed: false, coords: line2(center2, tip) }], { r: 0.2, g: 0.2, b: 0.2, a: 1 }, null, 2));
|
|
2054
|
+
}
|
|
2055
|
+
return [back, ...parts];
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1213
2058
|
// src/rough.ts
|
|
1214
|
-
var offset = (rng,
|
|
2059
|
+
var offset = (rng, amount2, roughness) => rng.signed() * amount2 * roughness;
|
|
1215
2060
|
function line(a, b, rng, roughness, bowing, preserveEnds, into) {
|
|
1216
2061
|
const length = Math.hypot(b.x - a.x, b.y - a.y);
|
|
1217
2062
|
if (!(length > 0.5)) return;
|
|
@@ -2067,17 +2912,43 @@ function trackMap(tl) {
|
|
|
2067
2912
|
for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
|
|
2068
2913
|
return m;
|
|
2069
2914
|
}
|
|
2070
|
-
function resolved(page, t) {
|
|
2071
|
-
return expandingSymbols(posed(page, t), clampTime(t));
|
|
2915
|
+
function resolved(page, t, state = "initial", since = null, carried = {}) {
|
|
2916
|
+
return expandingSymbols(posed(page, t, state, since, carried), clampTime(t));
|
|
2072
2917
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2918
|
+
var EMPTY_TIMELINE = { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
|
|
2919
|
+
function posed(page, t, state = "initial", since = null, carried = {}) {
|
|
2920
|
+
return posedWithMetrics(page, t, state, since, carried).frame;
|
|
2921
|
+
}
|
|
2922
|
+
function posedWithMetrics(page, t, state = "initial", since = null, carried = {}) {
|
|
2923
|
+
const off = stateHidden(page, state);
|
|
2924
|
+
const values = stateValues(page, state);
|
|
2925
|
+
const active = /* @__PURE__ */ new Map();
|
|
2926
|
+
for (const e of page.elements) {
|
|
2927
|
+
const o = activeOverrides(e, values);
|
|
2928
|
+
if (o.length > 0) active.set(e.id, o);
|
|
2929
|
+
}
|
|
2930
|
+
const flowing = (page.flows ?? []).length > 0;
|
|
2931
|
+
const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
|
|
2932
|
+
const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
|
|
2933
|
+
if (!timeline) return { frame: page, metrics: emptyMetrics() };
|
|
2076
2934
|
t = clampTime(t);
|
|
2077
2935
|
const tracks = trackMap(timeline);
|
|
2936
|
+
for (const [id, overrides] of active) {
|
|
2937
|
+
const track = tracks.get(id);
|
|
2938
|
+
if (!track?.follow || !overrides.some((o) => o.reroute)) continue;
|
|
2939
|
+
tracks.set(id, {
|
|
2940
|
+
...track,
|
|
2941
|
+
follow: track.follow.map((f) => {
|
|
2942
|
+
let path = f.path;
|
|
2943
|
+
for (const o of overrides) path = o.reroute?.[path] ?? path;
|
|
2944
|
+
return { ...f, path };
|
|
2945
|
+
})
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2078
2948
|
const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
|
|
2079
2949
|
const hidden = /* @__PURE__ */ new Set();
|
|
2080
2950
|
for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
|
|
2951
|
+
for (const id of off) hidden.add(id);
|
|
2081
2952
|
let moving = null;
|
|
2082
2953
|
if (timeline.settle === true) {
|
|
2083
2954
|
moving = /* @__PURE__ */ new Set();
|
|
@@ -2093,6 +2964,14 @@ function posed(page, t) {
|
|
|
2093
2964
|
let elements = page.elements.map((e) => {
|
|
2094
2965
|
const track = tracks.get(e.id);
|
|
2095
2966
|
let out = track ? applyTrack(track, e, t) : e;
|
|
2967
|
+
for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
|
|
2968
|
+
const list = active.get(e.id);
|
|
2969
|
+
if (list) {
|
|
2970
|
+
const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
|
|
2971
|
+
const delays = list.map((o) => o.delay).filter((d) => d !== null && Number.isFinite(d));
|
|
2972
|
+
if (caps.length > 0) out.capacity = Math.max(Math.min(...caps), 0);
|
|
2973
|
+
if (delays.length > 0 && out.kind !== "connector") out.delay = Math.max(Math.max(...delays), 0);
|
|
2974
|
+
}
|
|
2096
2975
|
if (reseed !== null && reseed > 0 && boils(e)) {
|
|
2097
2976
|
if (out === e) out = cloneElement(e);
|
|
2098
2977
|
out.seed = boiled(e.seed, reseed);
|
|
@@ -2116,7 +2995,7 @@ function posed(page, t) {
|
|
|
2116
2995
|
elements = elements.map((e) => {
|
|
2117
2996
|
const c = e.connector;
|
|
2118
2997
|
if (!c || ![c.start.elementID, c.end.elementID].some((id) => id !== null && hidden.has(id))) return e;
|
|
2119
|
-
if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false)) {
|
|
2998
|
+
if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false) || [c.start.elementID, c.end.elementID].some((id) => id !== null && off.has(id))) {
|
|
2120
2999
|
gone.add(e.id);
|
|
2121
3000
|
return e;
|
|
2122
3001
|
}
|
|
@@ -2126,13 +3005,24 @@ function posed(page, t) {
|
|
|
2126
3005
|
pinned.connector.end = { elementID: null, port: c.end.port, x: b.x, y: b.y };
|
|
2127
3006
|
return pinned;
|
|
2128
3007
|
});
|
|
2129
|
-
|
|
3008
|
+
elements = elements.filter((e) => !gone.has(e.id));
|
|
3009
|
+
let metrics2 = emptyMetrics();
|
|
3010
|
+
if (measuring) {
|
|
3011
|
+
const offline = new Set([...active].filter(([, list]) => list.some((o) => o.offline === true)).map(([id]) => id));
|
|
3012
|
+
const frame2 = { ...page, elements, timeline: null };
|
|
3013
|
+
const out = flowFrame(page.flows, frame2, t, values, offline, since, carried);
|
|
3014
|
+
metrics2 = out.metrics;
|
|
3015
|
+
const finished = finishing(elements, out.metrics, values, frame2, page.flows ?? [], Math.max(0, Math.min(since ?? t, 1e6)), carried);
|
|
3016
|
+
Object.assign(metrics2.carry, finished.readings);
|
|
3017
|
+
elements = [...finished.elements, ...out.packets];
|
|
3018
|
+
}
|
|
3019
|
+
return { frame: { ...page, elements, timeline: null, flows: measuring ? null : page.flows }, metrics: metrics2 };
|
|
2130
3020
|
}
|
|
2131
3021
|
function posterTime(page) {
|
|
2132
3022
|
return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
|
|
2133
3023
|
}
|
|
2134
3024
|
function still(page) {
|
|
2135
|
-
return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(page, 0);
|
|
3025
|
+
return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(posed(page, 0), 0);
|
|
2136
3026
|
}
|
|
2137
3027
|
function cameraRect(tl, t) {
|
|
2138
3028
|
const keys = tl.camera ?? [];
|
|
@@ -2241,7 +3131,7 @@ function filled(s, args) {
|
|
|
2241
3131
|
}) };
|
|
2242
3132
|
}
|
|
2243
3133
|
function symbolPage(s) {
|
|
2244
|
-
return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null };
|
|
3134
|
+
return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null, flows: null };
|
|
2245
3135
|
}
|
|
2246
3136
|
function hasInstances(page) {
|
|
2247
3137
|
return page.elements.some((e) => e.kind === "symbol");
|
|
@@ -2348,8 +3238,8 @@ function mix2(x) {
|
|
|
2348
3238
|
return z ^ z >> 31n;
|
|
2349
3239
|
}
|
|
2350
3240
|
function halves(id) {
|
|
2351
|
-
const
|
|
2352
|
-
return [BigInt(`0x${
|
|
3241
|
+
const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
|
|
3242
|
+
return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
|
|
2353
3243
|
}
|
|
2354
3244
|
function derivedID(instance, inner) {
|
|
2355
3245
|
const [a, b] = halves(instance), [c, d] = halves(inner);
|
|
@@ -2370,16 +3260,21 @@ function clickTargets(page, file) {
|
|
|
2370
3260
|
let pageOf = null;
|
|
2371
3261
|
for (const e of page.elements) {
|
|
2372
3262
|
const c = e.click;
|
|
3263
|
+
const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
|
|
2373
3264
|
if (c?.scene) {
|
|
2374
3265
|
const i = sceneNamed(c.scene, markers);
|
|
2375
3266
|
if (i >= 0) {
|
|
2376
|
-
out.set(e.id, { scene: i });
|
|
3267
|
+
out.set(e.id, { scene: i, ...state });
|
|
2377
3268
|
continue;
|
|
2378
3269
|
}
|
|
2379
3270
|
}
|
|
2380
3271
|
const url = (c?.url ? allowedURL(c.url) : null) ?? webLink(e.url);
|
|
2381
3272
|
if (url) {
|
|
2382
|
-
out.set(e.id, { url });
|
|
3273
|
+
out.set(e.id, { url, ...state });
|
|
3274
|
+
continue;
|
|
3275
|
+
}
|
|
3276
|
+
if (state) {
|
|
3277
|
+
out.set(e.id, { state: true, ...state });
|
|
2383
3278
|
continue;
|
|
2384
3279
|
}
|
|
2385
3280
|
if (e.link && file && file.pages.length > 1) {
|
|
@@ -2398,14 +3293,14 @@ function clickTargets(page, file) {
|
|
|
2398
3293
|
function hoveredElement(e) {
|
|
2399
3294
|
const h = e.hover;
|
|
2400
3295
|
if (!h) return e;
|
|
2401
|
-
const
|
|
2402
|
-
if (h.stroke)
|
|
2403
|
-
if (h.fill)
|
|
3296
|
+
const style2 = { ...e.style };
|
|
3297
|
+
if (h.stroke) style2.stroke = h.stroke;
|
|
3298
|
+
if (h.fill) style2.fill = h.fill;
|
|
2404
3299
|
if (h.opacity !== null && Number.isFinite(h.opacity)) {
|
|
2405
3300
|
const o = Math.min(Math.max(h.opacity, 0), 1);
|
|
2406
|
-
|
|
3301
|
+
style2.opacity = o >= 1 ? null : o;
|
|
2407
3302
|
}
|
|
2408
|
-
return { ...e, style };
|
|
3303
|
+
return { ...e, style: style2 };
|
|
2409
3304
|
}
|
|
2410
3305
|
function hovered(page, ids) {
|
|
2411
3306
|
return { ...page, elements: page.elements.map((e) => ids.has(e.id) ? hoveredElement(e) : e) };
|
|
@@ -2609,14 +3504,14 @@ function parseList(lines, start) {
|
|
|
2609
3504
|
}
|
|
2610
3505
|
return { block: { kind: "list", ordered, items }, next: i };
|
|
2611
3506
|
}
|
|
2612
|
-
function inline(src,
|
|
3507
|
+
function inline(src, style2 = {}) {
|
|
2613
3508
|
const out = [];
|
|
2614
3509
|
const push = (text, s) => {
|
|
2615
3510
|
if (text) out.push({ ...s, text });
|
|
2616
3511
|
};
|
|
2617
3512
|
let i = 0, buf = "";
|
|
2618
3513
|
const emit = () => {
|
|
2619
|
-
push(buf,
|
|
3514
|
+
push(buf, style2);
|
|
2620
3515
|
buf = "";
|
|
2621
3516
|
};
|
|
2622
3517
|
while (i < src.length) {
|
|
@@ -2644,43 +3539,43 @@ function inline(src, style = {}) {
|
|
|
2644
3539
|
emit();
|
|
2645
3540
|
let code = m[2].replace(/\n/g, " ");
|
|
2646
3541
|
if (/^ .* $/.test(code)) code = code.slice(1, -1);
|
|
2647
|
-
push(code, { ...
|
|
3542
|
+
push(code, { ...style2, mono: true });
|
|
2648
3543
|
i += m[0].length;
|
|
2649
3544
|
continue;
|
|
2650
3545
|
}
|
|
2651
3546
|
if (m = /^!\[([^\]]*)\]\([^)]*\)/.exec(rest)) {
|
|
2652
3547
|
emit();
|
|
2653
|
-
out.push(...inline(m[1],
|
|
3548
|
+
out.push(...inline(m[1], style2));
|
|
2654
3549
|
i += m[0].length;
|
|
2655
3550
|
continue;
|
|
2656
3551
|
}
|
|
2657
3552
|
if (m = /^\[([^\]]+)\]\([^)]*\)/.exec(rest)) {
|
|
2658
3553
|
emit();
|
|
2659
|
-
out.push(...inline(m[1], { ...
|
|
3554
|
+
out.push(...inline(m[1], { ...style2, link: true }));
|
|
2660
3555
|
i += m[0].length;
|
|
2661
3556
|
continue;
|
|
2662
3557
|
}
|
|
2663
3558
|
if (m = /^<((?:https?|ftp|mailto):[^>\s]+)>/.exec(rest)) {
|
|
2664
3559
|
emit();
|
|
2665
|
-
push(m[1], { ...
|
|
3560
|
+
push(m[1], { ...style2, link: true });
|
|
2666
3561
|
i += m[0].length;
|
|
2667
3562
|
continue;
|
|
2668
3563
|
}
|
|
2669
3564
|
if (m = /^~~(?=\S)([\s\S]*?\S)~~/.exec(rest)) {
|
|
2670
3565
|
emit();
|
|
2671
|
-
out.push(...inline(m[1], { ...
|
|
3566
|
+
out.push(...inline(m[1], { ...style2, strike: true }));
|
|
2672
3567
|
i += m[0].length;
|
|
2673
3568
|
continue;
|
|
2674
3569
|
}
|
|
2675
3570
|
if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
|
|
2676
3571
|
emit();
|
|
2677
|
-
out.push(...inline(m[2], { ...
|
|
3572
|
+
out.push(...inline(m[2], { ...style2, bold: true }));
|
|
2678
3573
|
i += m[0].length;
|
|
2679
3574
|
continue;
|
|
2680
3575
|
}
|
|
2681
3576
|
if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
|
|
2682
3577
|
emit();
|
|
2683
|
-
out.push(...inline(m[2], { ...
|
|
3578
|
+
out.push(...inline(m[2], { ...style2, italic: true }));
|
|
2684
3579
|
i += m[0].length;
|
|
2685
3580
|
continue;
|
|
2686
3581
|
}
|
|
@@ -3293,6 +4188,16 @@ var Playback = class {
|
|
|
3293
4188
|
__publicField(this, "plan");
|
|
3294
4189
|
__publicField(this, "scenes");
|
|
3295
4190
|
__publicField(this, "sceneNames");
|
|
4191
|
+
/** Whether each scene loops (Marker.loop). */
|
|
4192
|
+
__publicField(this, "sceneLoops");
|
|
4193
|
+
/** The page's variables now: their starting values until clicks change them. */
|
|
4194
|
+
__publicField(this, "state");
|
|
4195
|
+
/** When the variables last changed (performance.now() milliseconds): backlog piles up from then (ADR 0003). */
|
|
4196
|
+
__publicField(this, "stateSince", typeof performance === "undefined" ? 0 : performance.now());
|
|
4197
|
+
/** A still (the `at` attribute): backlog is drawn for the page time, not the wall clock. */
|
|
4198
|
+
__publicField(this, "still", false);
|
|
4199
|
+
/** What each queue held when the variables last changed, which it drains from (FrameMetrics.carry). */
|
|
4200
|
+
__publicField(this, "carried", {});
|
|
3296
4201
|
__publicField(this, "background");
|
|
3297
4202
|
__publicField(this, "ink");
|
|
3298
4203
|
/** What clicking each clickable element does, by element id. */
|
|
@@ -3352,6 +4257,8 @@ var Playback = class {
|
|
|
3352
4257
|
};
|
|
3353
4258
|
this.scenes = (page.timeline?.markers ?? []).map((m) => near(m.t));
|
|
3354
4259
|
this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
|
|
4260
|
+
this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
|
|
4261
|
+
this.state = { ...page.variables ?? {} };
|
|
3355
4262
|
const theme = themeNamed(page.theme);
|
|
3356
4263
|
this.background = theme?.background ?? WHITE;
|
|
3357
4264
|
this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
|
|
@@ -3362,6 +4269,10 @@ var Playback = class {
|
|
|
3362
4269
|
this.svg.style.maxWidth = `${Math.ceil(this.plan.size.width)}px`;
|
|
3363
4270
|
into.replaceChildren(this.svg);
|
|
3364
4271
|
}
|
|
4272
|
+
/** Seconds since the variables last changed, or null for a still. */
|
|
4273
|
+
sinceChange(now = typeof performance === "undefined" ? 0 : performance.now()) {
|
|
4274
|
+
return this.still || typeof performance === "undefined" ? null : (now - this.stateSince) / 1e3;
|
|
4275
|
+
}
|
|
3365
4276
|
get frameCount() {
|
|
3366
4277
|
return this.plan.times.length;
|
|
3367
4278
|
}
|
|
@@ -3402,7 +4313,7 @@ var Playback = class {
|
|
|
3402
4313
|
const times = /* @__PURE__ */ new Map();
|
|
3403
4314
|
for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
|
|
3404
4315
|
const tc = clampTime(t);
|
|
3405
|
-
const pose = posed(this.page, tc);
|
|
4316
|
+
const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
|
|
3406
4317
|
const frame2 = expandingSymbols(pose, tc, true, times);
|
|
3407
4318
|
const { parts } = svgParts(frame2, this.background, r);
|
|
3408
4319
|
const owned = (m, parts2) => {
|
|
@@ -3524,6 +4435,20 @@ ${h}</g>
|
|
|
3524
4435
|
});
|
|
3525
4436
|
return s;
|
|
3526
4437
|
}
|
|
4438
|
+
/** The frame after scene `s`'s last: the next scene's, or the frame count after the last scene. */
|
|
4439
|
+
sceneEnd(s) {
|
|
4440
|
+
return this.scenes.find((f, j) => j > s && f > this.scenes[s]) ?? this.frameCount;
|
|
4441
|
+
}
|
|
4442
|
+
/** Where playing goes from frame `i`, and whether it stops there: on into a looping scene, round to the start of
|
|
4443
|
+
* the looping scene `i` ends, or a stop at a scene or the end (`loops`: whether the timeline loops at its end). */
|
|
4444
|
+
step(i, loops) {
|
|
4445
|
+
const next = i + 1;
|
|
4446
|
+
const s = this.sceneAt(i);
|
|
4447
|
+
if (s >= 0 && this.sceneLoops[s] && next >= this.sceneEnd(s)) return { frame: this.scenes[s], stop: false };
|
|
4448
|
+
if (next >= this.frameCount) return { frame: 0, stop: !loops || this.scenes.length > 0 };
|
|
4449
|
+
const entered = this.scenes.indexOf(next);
|
|
4450
|
+
return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
|
|
4451
|
+
}
|
|
3527
4452
|
};
|
|
3528
4453
|
var ElementBase = typeof HTMLElement === "undefined" ? class {
|
|
3529
4454
|
} : HTMLElement;
|
|
@@ -3645,6 +4570,7 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3645
4570
|
const file = this.file;
|
|
3646
4571
|
const page = file.pages[pageIndex(file, this.getAttribute("page"))];
|
|
3647
4572
|
const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
|
|
4573
|
+
p.still = this.hasAttribute("at");
|
|
3648
4574
|
this.playback = p;
|
|
3649
4575
|
this.style.setProperty("--dottie-page", color(p.background));
|
|
3650
4576
|
this.style.setProperty("--dottie-ink", color(p.ink));
|
|
@@ -3677,7 +4603,7 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3677
4603
|
this.setPlaying(false);
|
|
3678
4604
|
return;
|
|
3679
4605
|
}
|
|
3680
|
-
this.setPlaying(!this.hasAttribute("no-autoplay") && !p.scenes.includes(0));
|
|
4606
|
+
this.setPlaying(!this.hasAttribute("no-autoplay") && (!p.scenes.includes(0) || p.sceneLoops[p.scenes.indexOf(0)]));
|
|
3681
4607
|
}
|
|
3682
4608
|
// MARK: Playback
|
|
3683
4609
|
get paused() {
|
|
@@ -3703,11 +4629,13 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3703
4629
|
pause() {
|
|
3704
4630
|
this.setPlaying(false);
|
|
3705
4631
|
}
|
|
3706
|
-
/** Plays on to the next scene (from the start after the last). */
|
|
4632
|
+
/** Plays on to the next scene (from the start after the last). Out of a looping scene it jumps to the next. */
|
|
3707
4633
|
nextScene() {
|
|
3708
4634
|
const p = this.playback;
|
|
3709
4635
|
if (!p) return;
|
|
3710
|
-
|
|
4636
|
+
const s = p.sceneAt(p.frame);
|
|
4637
|
+
if (s >= 0 && p.sceneLoops[s]) p.show(s + 1 < p.scenes.length ? p.scenes[s + 1] : 0);
|
|
4638
|
+
else if (p.frame >= p.frameCount - 1) p.show(0);
|
|
3711
4639
|
this.setPlaying(true);
|
|
3712
4640
|
}
|
|
3713
4641
|
/** Back to the start of this scene, or the one before if already there. */
|
|
@@ -3736,6 +4664,14 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3736
4664
|
this.scheduleReplay();
|
|
3737
4665
|
}
|
|
3738
4666
|
if (!click) return;
|
|
4667
|
+
if (click.set || click.toggle || click.reset) {
|
|
4668
|
+
this.setState(applyClick(
|
|
4669
|
+
{ scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
|
|
4670
|
+
p.state,
|
|
4671
|
+
p.page.variables ?? {}
|
|
4672
|
+
));
|
|
4673
|
+
}
|
|
4674
|
+
if ("state" in click) return;
|
|
3739
4675
|
if ("url" in click) {
|
|
3740
4676
|
if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
|
|
3741
4677
|
} else if ("page" in click) {
|
|
@@ -3743,13 +4679,30 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3743
4679
|
} else if (click.scene >= 0 && click.scene < p.scenes.length) {
|
|
3744
4680
|
if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
|
|
3745
4681
|
this.setPlaying(false);
|
|
3746
|
-
p.show(click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
|
|
4682
|
+
p.show(p.sceneLoops[click.scene] ? p.scenes[click.scene] : click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
|
|
3747
4683
|
} else {
|
|
3748
4684
|
p.show(p.scenes[click.scene]);
|
|
3749
4685
|
this.setPlaying(true);
|
|
3750
4686
|
}
|
|
3751
4687
|
}
|
|
3752
4688
|
}
|
|
4689
|
+
/** The page's variables now. */
|
|
4690
|
+
get variables() {
|
|
4691
|
+
return { ...this.playback?.state ?? {} };
|
|
4692
|
+
}
|
|
4693
|
+
/** Gives the page's variables these values (the others keep theirs), redraws, and fires `state`. */
|
|
4694
|
+
setState(values) {
|
|
4695
|
+
const p = this.playback;
|
|
4696
|
+
if (!p) return;
|
|
4697
|
+
const next = { ...p.state, ...values };
|
|
4698
|
+
if (Object.keys(next).every((k) => next[k] === p.state[k])) return;
|
|
4699
|
+
const t = clampTime(p.last.t);
|
|
4700
|
+
p.carried = posedWithMetrics(p.page, t, p.state, p.sinceChange(), p.carried).metrics.carry;
|
|
4701
|
+
p.state = next;
|
|
4702
|
+
p.stateSince = performance.now();
|
|
4703
|
+
p.redraw();
|
|
4704
|
+
this.dispatchEvent(new CustomEvent("state", { detail: { variables: { ...next } } }));
|
|
4705
|
+
}
|
|
3753
4706
|
/** Redraws, while the page itself stands still, as long as a clicked copy is playing through. */
|
|
3754
4707
|
scheduleReplay() {
|
|
3755
4708
|
if (this.replayRaf) return;
|
|
@@ -3775,7 +4728,8 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3775
4728
|
schedule() {
|
|
3776
4729
|
if (this.playing && this.visible && !this.raf && this.isConnected) this.raf = requestAnimationFrame((now) => this.tick(now));
|
|
3777
4730
|
}
|
|
3778
|
-
/** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end
|
|
4731
|
+
/** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end, except that a
|
|
4732
|
+
* looping scene plays round (Playback.step). */
|
|
3779
4733
|
tick(now) {
|
|
3780
4734
|
this.raf = 0;
|
|
3781
4735
|
const p = this.playback;
|
|
@@ -3786,14 +4740,14 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3786
4740
|
let advanced = false;
|
|
3787
4741
|
while (this.playing && this.acc >= step) {
|
|
3788
4742
|
this.acc -= step;
|
|
3789
|
-
const
|
|
3790
|
-
if (
|
|
4743
|
+
const { frame: frame2, stop } = p.step(p.frame, !this.hasAttribute("no-loop"));
|
|
4744
|
+
if (stop && frame2 === 0 && p.frame + 1 >= p.frameCount) {
|
|
3791
4745
|
this.setPlaying(false);
|
|
3792
4746
|
break;
|
|
3793
4747
|
}
|
|
3794
|
-
p.frame =
|
|
4748
|
+
p.frame = frame2;
|
|
3795
4749
|
advanced = true;
|
|
3796
|
-
if (
|
|
4750
|
+
if (stop) {
|
|
3797
4751
|
this.setPlaying(false);
|
|
3798
4752
|
break;
|
|
3799
4753
|
}
|
|
@@ -3943,7 +4897,7 @@ function render(source, options = {}) {
|
|
|
3943
4897
|
function isFile(v) {
|
|
3944
4898
|
return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
|
|
3945
4899
|
}
|
|
3946
|
-
var VERSION = "0.
|
|
4900
|
+
var VERSION = "0.8.0";
|
|
3947
4901
|
export {
|
|
3948
4902
|
BOIL_DRAWINGS,
|
|
3949
4903
|
BUNDLED_FONTS,
|