@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-player.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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 __export = (target, all) => {
|
|
@@ -66,13 +66,146 @@ __export(index_exports, {
|
|
|
66
66
|
withRoughness: () => withRoughness
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
// src/state.ts
|
|
70
|
+
var MAX_NAME = 64;
|
|
71
|
+
function isName(name) {
|
|
72
|
+
return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
73
|
+
}
|
|
74
|
+
var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
|
|
75
|
+
var OPS = [">=", "<=", "==", "!=", ">", "<"];
|
|
76
|
+
function metricTerm(term) {
|
|
77
|
+
const t = trim(term);
|
|
78
|
+
if (!t.includes("(")) {
|
|
79
|
+
const op2 = t.search(/[>=<!]/);
|
|
80
|
+
if (op2 < 0) return null;
|
|
81
|
+
const name2 = trim(t.slice(0, op2));
|
|
82
|
+
if (!METRIC_NAMES.includes(name2)) return null;
|
|
83
|
+
const m = metricTerm(name2 + "() " + t.slice(op2));
|
|
84
|
+
return m ? { ...m, target: "" } : null;
|
|
85
|
+
}
|
|
86
|
+
const open = t.indexOf("("), close = t.indexOf(")");
|
|
87
|
+
if (open < 0 || close < 0 || open >= close) return null;
|
|
88
|
+
const name = trim(t.slice(0, open)), target = trim(t.slice(open + 1, close));
|
|
89
|
+
let rest = trim(t.slice(close + 1));
|
|
90
|
+
if (!METRIC_NAMES.includes(name) || target.includes("(")) return null;
|
|
91
|
+
const op = OPS.find((o) => rest.startsWith(o));
|
|
92
|
+
if (op === void 0) return null;
|
|
93
|
+
rest = trim(rest.slice(op.length));
|
|
94
|
+
if (!/^-?[0-9]+(\.[0-9]+)?$/.test(rest)) return null;
|
|
95
|
+
return { metric: name, target, op, value: Number(rest) };
|
|
96
|
+
}
|
|
97
|
+
function passes(m, v) {
|
|
98
|
+
switch (m.op) {
|
|
99
|
+
case ">":
|
|
100
|
+
return v > m.value;
|
|
101
|
+
case ">=":
|
|
102
|
+
return v >= m.value;
|
|
103
|
+
case "<":
|
|
104
|
+
return v < m.value;
|
|
105
|
+
case "<=":
|
|
106
|
+
return v <= m.value;
|
|
107
|
+
case "==":
|
|
108
|
+
return v === m.value;
|
|
109
|
+
default:
|
|
110
|
+
return v !== m.value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function mentionsMetric(condition) {
|
|
114
|
+
return condition.split("||").some((any) => any.split("&&").some((all) => metricTerm(all) !== null));
|
|
115
|
+
}
|
|
116
|
+
function conditionNames(condition) {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const any of condition.split("||")) {
|
|
119
|
+
for (const all of any.split("&&")) {
|
|
120
|
+
if (metricTerm(all) !== null) continue;
|
|
121
|
+
let term = trim(all);
|
|
122
|
+
if (term.startsWith("!")) term = trim(term.slice(1));
|
|
123
|
+
if (!isName(term)) return null;
|
|
124
|
+
out.push(term);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
function holds(condition, state, metric = null) {
|
|
130
|
+
if (conditionNames(condition) === null) return false;
|
|
131
|
+
return condition.split("||").some((any) => any.split("&&").every((all) => {
|
|
132
|
+
const m = metricTerm(all);
|
|
133
|
+
if (m !== null) {
|
|
134
|
+
const v = metric ? metric(m) : null;
|
|
135
|
+
return v !== null && passes(m, v);
|
|
136
|
+
}
|
|
137
|
+
let term = trim(all);
|
|
138
|
+
let negated = false;
|
|
139
|
+
if (term.startsWith("!")) {
|
|
140
|
+
negated = true;
|
|
141
|
+
term = trim(term.slice(1));
|
|
142
|
+
}
|
|
143
|
+
return (state[term] ?? false) !== negated;
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
function stateValues(page, choice) {
|
|
147
|
+
if (choice === "all") return null;
|
|
148
|
+
if (choice === "initial") return { ...page.variables ?? {} };
|
|
149
|
+
return { ...page.variables ?? {}, ...choice };
|
|
150
|
+
}
|
|
151
|
+
function stateHidden(page, choice) {
|
|
152
|
+
const values = stateValues(page, choice);
|
|
153
|
+
const out = /* @__PURE__ */ new Set();
|
|
154
|
+
if (!values) return out;
|
|
155
|
+
for (const e of page.elements) if (e.when !== null && !holds(e.when, values)) out.add(e.id);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function activeOverrides(e, values) {
|
|
159
|
+
if (!values || !e.overrides) return [];
|
|
160
|
+
return e.overrides.filter((o) => !mentionsMetric(o.when) && holds(o.when, values));
|
|
161
|
+
}
|
|
162
|
+
function looksOnly(o) {
|
|
163
|
+
return { ...o, reroute: null, offline: null, cost: null, capacity: null, delay: null };
|
|
164
|
+
}
|
|
165
|
+
function applyOverride(o, e) {
|
|
166
|
+
const out = cloneElement(e);
|
|
167
|
+
if (o.stroke) out.style.stroke = { ...o.stroke };
|
|
168
|
+
if (o.fill) out.style.fill = { ...o.fill };
|
|
169
|
+
if (o.textColor) out.style.textColor = { ...o.textColor };
|
|
170
|
+
if (o.strokeWidth !== null) out.style.strokeWidth = Math.max(0, o.strokeWidth);
|
|
171
|
+
if (o.dash !== null) out.style.dash = o.dash;
|
|
172
|
+
if (o.text !== null) out.text = o.text;
|
|
173
|
+
if (o.cost !== null && out.connector) out.connector.cost = Math.min(Math.max(o.cost, 0), 1e6);
|
|
174
|
+
if (o.capacity !== null) out.capacity = Math.max(o.capacity, 0);
|
|
175
|
+
if (o.delay !== null && out.kind !== "connector") out.delay = Math.max(o.delay, 0);
|
|
176
|
+
if (o.opacity !== null) {
|
|
177
|
+
const v = Math.min(Math.max(o.opacity, 0), 1) * (out.style.opacity ?? 1);
|
|
178
|
+
out.style.opacity = v >= 1 ? null : v;
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function changesState(c) {
|
|
183
|
+
return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
|
|
184
|
+
}
|
|
185
|
+
function applyClick(c, state, initial = {}) {
|
|
186
|
+
let out = { ...state };
|
|
187
|
+
if (c.reset === true) {
|
|
188
|
+
out = {};
|
|
189
|
+
for (const k of Object.keys(state)) out[k] = false;
|
|
190
|
+
Object.assign(out, initial);
|
|
191
|
+
}
|
|
192
|
+
for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
|
|
193
|
+
for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function trim(s) {
|
|
197
|
+
return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
|
|
198
|
+
}
|
|
199
|
+
|
|
69
200
|
// src/model.ts
|
|
70
201
|
var SUPPORTED_VERSION = 2;
|
|
71
202
|
var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
|
|
72
203
|
var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
|
|
73
204
|
var KNOWN_FIELDS = {
|
|
74
205
|
file: ["version", "pages", "images"],
|
|
75
|
-
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
|
|
206
|
+
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
|
|
207
|
+
flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
|
|
208
|
+
flowOverride: ["when", "volume"],
|
|
76
209
|
symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
|
|
77
210
|
layer: ["id", "name", "visible", "locked"],
|
|
78
211
|
rgba: ["r", "g", "b", "a"],
|
|
@@ -100,6 +233,8 @@ var KNOWN_FIELDS = {
|
|
|
100
233
|
"anchor",
|
|
101
234
|
"click",
|
|
102
235
|
"hover",
|
|
236
|
+
"when",
|
|
237
|
+
"overrides",
|
|
103
238
|
"symbol",
|
|
104
239
|
"playback",
|
|
105
240
|
"offset",
|
|
@@ -108,8 +243,13 @@ var KNOWN_FIELDS = {
|
|
|
108
243
|
"reveal",
|
|
109
244
|
"ports",
|
|
110
245
|
"labelRect",
|
|
111
|
-
"extension"
|
|
246
|
+
"extension",
|
|
247
|
+
"capacity",
|
|
248
|
+
"delay",
|
|
249
|
+
"queue",
|
|
250
|
+
"gauge"
|
|
112
251
|
],
|
|
252
|
+
gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
|
|
113
253
|
strokePoint: ["x", "y"],
|
|
114
254
|
subpath: ["closed", "coords"],
|
|
115
255
|
inlineImage: ["data", "format"],
|
|
@@ -117,7 +257,7 @@ var KNOWN_FIELDS = {
|
|
|
117
257
|
extensionRef: ["id", "shape", "version", "params"],
|
|
118
258
|
normalizedPoint: ["x", "y"],
|
|
119
259
|
normalizedRect: ["x", "y", "width", "height"],
|
|
120
|
-
connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
|
|
260
|
+
connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
|
|
121
261
|
waypoint: ["x", "y"],
|
|
122
262
|
timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
|
|
123
263
|
track: ["element", "keys", "boil", "follow", "repeats", "march"],
|
|
@@ -125,9 +265,10 @@ var KNOWN_FIELDS = {
|
|
|
125
265
|
follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
|
|
126
266
|
repeat: ["start", "end", "count"],
|
|
127
267
|
cameraKey: ["t", "x", "y", "width", "height", "ease"],
|
|
128
|
-
marker: ["t", "name"],
|
|
129
|
-
click: ["scene", "url"],
|
|
130
|
-
hover: ["stroke", "fill", "opacity"]
|
|
268
|
+
marker: ["t", "name", "loop"],
|
|
269
|
+
click: ["scene", "url", "set", "toggle", "reset"],
|
|
270
|
+
hover: ["stroke", "fill", "opacity"],
|
|
271
|
+
override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
|
|
131
272
|
};
|
|
132
273
|
var MAX_DURATION = 3600;
|
|
133
274
|
var MAX_MARCH = 1e4;
|
|
@@ -143,6 +284,8 @@ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
|
143
284
|
var str = (v) => typeof v === "string" ? v : void 0;
|
|
144
285
|
var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
|
|
145
286
|
var bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
287
|
+
var NO_ID = "00000000-0000-0000-0000-000000000000";
|
|
288
|
+
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}$/;
|
|
146
289
|
var uuid = (v) => typeof v === "string" ? v.toUpperCase() : void 0;
|
|
147
290
|
function clampTime(t) {
|
|
148
291
|
if (!Number.isFinite(t)) return t === Infinity ? MAX_DURATION : 0;
|
|
@@ -234,7 +377,8 @@ var Reader = class {
|
|
|
234
377
|
this.fields(w, "waypoint");
|
|
235
378
|
return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
|
|
236
379
|
}) : null,
|
|
237
|
-
hops: typeof v.hops === "boolean" ? v.hops : null
|
|
380
|
+
hops: typeof v.hops === "boolean" ? v.hops : null,
|
|
381
|
+
cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
|
|
238
382
|
};
|
|
239
383
|
}
|
|
240
384
|
image(v, table) {
|
|
@@ -285,6 +429,8 @@ var Reader = class {
|
|
|
285
429
|
anchor: str(v.anchor) ?? null,
|
|
286
430
|
click: this.click(v.click),
|
|
287
431
|
hover: this.hover(v.hover),
|
|
432
|
+
when: str(v.when) ?? null,
|
|
433
|
+
overrides: this.overrides(v.overrides),
|
|
288
434
|
symbol: uuid(v.symbol) ?? null,
|
|
289
435
|
playback: this.playback(v.playback),
|
|
290
436
|
offset: num(v.offset) ?? null,
|
|
@@ -296,7 +442,26 @@ var Reader = class {
|
|
|
296
442
|
return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
|
|
297
443
|
}) : null,
|
|
298
444
|
labelRect: this.labelRect(v.labelRect),
|
|
299
|
-
extension: this.extension(v.extension)
|
|
445
|
+
extension: this.extension(v.extension),
|
|
446
|
+
capacity: num(v.capacity) ?? null,
|
|
447
|
+
delay: num(v.delay) ?? null,
|
|
448
|
+
queue: v.queue === true ? true : null,
|
|
449
|
+
gauge: this.gauge(v.gauge)
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
gauge(v) {
|
|
453
|
+
if (!isObj(v)) return null;
|
|
454
|
+
this.fields(v, "gauge");
|
|
455
|
+
const finite = (x) => typeof x === "number" && Number.isFinite(x) ? x : null;
|
|
456
|
+
const style2 = str(v.style);
|
|
457
|
+
return {
|
|
458
|
+
metric: str(v.metric) ?? "",
|
|
459
|
+
of: typeof v.of === "string" && UUID.test(v.of) ? v.of.toUpperCase() : NO_ID,
|
|
460
|
+
max: finite(v.max),
|
|
461
|
+
warn: finite(v.warn),
|
|
462
|
+
alarm: finite(v.alarm),
|
|
463
|
+
style: style2 === "number" || style2 === "bar" || style2 === "dial" ? style2 : null,
|
|
464
|
+
label: str(v.label) ?? null
|
|
300
465
|
};
|
|
301
466
|
}
|
|
302
467
|
labelRect(v) {
|
|
@@ -320,13 +485,27 @@ var Reader = class {
|
|
|
320
485
|
}
|
|
321
486
|
return { id, shape, version, params };
|
|
322
487
|
}
|
|
488
|
+
variables(v) {
|
|
489
|
+
if (!isObj(v)) return null;
|
|
490
|
+
const out = {};
|
|
491
|
+
for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
|
|
492
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
493
|
+
}
|
|
323
494
|
click(v) {
|
|
324
495
|
if (!isObj(v)) return null;
|
|
325
496
|
this.fields(v, "click");
|
|
326
497
|
const url = str(v.url);
|
|
327
498
|
const safe = url === void 0 ? null : allowedURL(url);
|
|
328
499
|
if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
|
|
329
|
-
|
|
500
|
+
let set = null;
|
|
501
|
+
if (isObj(v.set)) {
|
|
502
|
+
set = {};
|
|
503
|
+
for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
|
|
504
|
+
if (Object.keys(set).length === 0) set = null;
|
|
505
|
+
}
|
|
506
|
+
const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
|
|
507
|
+
const toggle = raw ? raw.filter(isName) : null;
|
|
508
|
+
return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
|
|
330
509
|
}
|
|
331
510
|
playback(v) {
|
|
332
511
|
const s = str(v);
|
|
@@ -362,6 +541,35 @@ var Reader = class {
|
|
|
362
541
|
params: strings(v.params)
|
|
363
542
|
};
|
|
364
543
|
}
|
|
544
|
+
/** As DottieKit reads them: a reroute whose targets aren't all ids is dropped, as are keys that aren't ids. */
|
|
545
|
+
overrides(v) {
|
|
546
|
+
if (!Array.isArray(v)) return null;
|
|
547
|
+
return v.filter(isObj).map((o) => {
|
|
548
|
+
this.fields(o, "override");
|
|
549
|
+
const dash = str(o.dash);
|
|
550
|
+
let reroute = null;
|
|
551
|
+
if (isObj(o.reroute) && Object.values(o.reroute).every((x) => typeof x === "string" && UUID.test(x))) {
|
|
552
|
+
reroute = {};
|
|
553
|
+
for (const [k, x] of Object.entries(o.reroute)) if (UUID.test(k)) reroute[k.toUpperCase()] = x.toUpperCase();
|
|
554
|
+
if (Object.keys(reroute).length === 0) reroute = null;
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
when: str(o.when) ?? "",
|
|
558
|
+
stroke: rgba(o.stroke) ?? null,
|
|
559
|
+
fill: rgba(o.fill) ?? null,
|
|
560
|
+
textColor: rgba(o.textColor) ?? null,
|
|
561
|
+
opacity: num(o.opacity) ?? null,
|
|
562
|
+
strokeWidth: num(o.strokeWidth) ?? null,
|
|
563
|
+
dash: dash !== void 0 && ["solid", "dashed", "dotted"].includes(dash) ? dash : null,
|
|
564
|
+
text: str(o.text) ?? null,
|
|
565
|
+
reroute,
|
|
566
|
+
offline: o.offline === true ? true : null,
|
|
567
|
+
cost: typeof o.cost === "number" && Number.isFinite(o.cost) ? o.cost : null,
|
|
568
|
+
capacity: typeof o.capacity === "number" && Number.isFinite(o.capacity) ? o.capacity : null,
|
|
569
|
+
delay: typeof o.delay === "number" && Number.isFinite(o.delay) ? o.delay : null
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
}
|
|
365
573
|
hover(v) {
|
|
366
574
|
if (!isObj(v)) return null;
|
|
367
575
|
this.fields(v, "hover");
|
|
@@ -427,7 +635,7 @@ var Reader = class {
|
|
|
427
635
|
if (camera) stableSortBy(camera, (c) => c.t);
|
|
428
636
|
const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
|
|
429
637
|
const t = num(m.t);
|
|
430
|
-
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
|
|
638
|
+
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
|
|
431
639
|
}) : null;
|
|
432
640
|
if (markers) stableSortBy(markers, (m) => m.t);
|
|
433
641
|
const poster = num(v.poster);
|
|
@@ -460,7 +668,34 @@ var Reader = class {
|
|
|
460
668
|
elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
|
|
461
669
|
theme: str(v.theme) ?? null,
|
|
462
670
|
timeline: this.timeline(v.timeline),
|
|
463
|
-
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
|
|
671
|
+
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
|
|
672
|
+
variables: this.variables(v.variables),
|
|
673
|
+
flows: Array.isArray(v.flows) ? v.flows.filter(isObj).map((f) => this.flow(f)) : null
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** As DottieKit reads one: a missing or bad id, source or destination sends nothing; bad destinations are dropped. */
|
|
677
|
+
flow(f) {
|
|
678
|
+
this.fields(f, "flow");
|
|
679
|
+
const id = (x) => typeof x === "string" && UUID.test(x) ? x.toUpperCase() : NO_ID;
|
|
680
|
+
const finite = (x) => {
|
|
681
|
+
const n = num(x);
|
|
682
|
+
return n !== void 0 && Number.isFinite(n) ? n : null;
|
|
683
|
+
};
|
|
684
|
+
return {
|
|
685
|
+
id: id(f.id),
|
|
686
|
+
from: id(f.from),
|
|
687
|
+
to: Array.isArray(f.to) && f.to.every((x) => typeof x === "string") ? f.to.filter((x) => UUID.test(x)).map((x) => x.toUpperCase()) : [],
|
|
688
|
+
every: finite(f.every),
|
|
689
|
+
speed: finite(f.speed),
|
|
690
|
+
size: finite(f.size),
|
|
691
|
+
color: isObj(f.color) && ["r", "g", "b", "a"].every((k) => typeof f.color[k] === "number") ? rgba(f.color) ?? null : null,
|
|
692
|
+
when: str(f.when) ?? null,
|
|
693
|
+
spread: f.spread === true ? true : null,
|
|
694
|
+
volume: finite(f.volume),
|
|
695
|
+
overrides: Array.isArray(f.overrides) && f.overrides.length > 0 ? f.overrides.filter(isObj).map((o) => {
|
|
696
|
+
this.fields(o, "flowOverride");
|
|
697
|
+
return { when: str(o.when) ?? "", volume: finite(o.volume) };
|
|
698
|
+
}) : null
|
|
464
699
|
};
|
|
465
700
|
}
|
|
466
701
|
};
|
|
@@ -522,7 +757,8 @@ function cloneElement(e) {
|
|
|
522
757
|
start: { ...e.connector.start },
|
|
523
758
|
end: { ...e.connector.end },
|
|
524
759
|
waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
|
|
525
|
-
} : null
|
|
760
|
+
} : null,
|
|
761
|
+
gauge: e.gauge ? { ...e.gauge } : e.gauge
|
|
526
762
|
};
|
|
527
763
|
}
|
|
528
764
|
function orderedElements(page) {
|
|
@@ -1273,8 +1509,617 @@ function mulConst(step) {
|
|
|
1273
1509
|
return [p[0], p[1]];
|
|
1274
1510
|
}
|
|
1275
1511
|
|
|
1512
|
+
// src/flows.ts
|
|
1513
|
+
var DEFAULT_EVERY = 0.5;
|
|
1514
|
+
var DEFAULT_SPEED = 160;
|
|
1515
|
+
var DEFAULT_SIZE = 12;
|
|
1516
|
+
var DEFAULT_COLOR = { r: 0.95, g: 0.36, b: 0.33, a: 1 };
|
|
1517
|
+
var clamp = (v, fallback, lo, hi) => v === null || !Number.isFinite(v) ? fallback : Math.min(Math.max(v, lo), hi);
|
|
1518
|
+
function packetID(flow, k) {
|
|
1519
|
+
const n = (k + 2 ** 40) % 2 ** 48;
|
|
1520
|
+
return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
|
|
1521
|
+
}
|
|
1522
|
+
var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
|
|
1523
|
+
function flowTree(from, frame2, offline = /* @__PURE__ */ new Set()) {
|
|
1524
|
+
const shapes2 = new Set(frame2.elements.filter((e) => e.kind !== "connector" && !offline.has(e.id)).map((e) => e.id));
|
|
1525
|
+
if (!shapes2.has(from)) return null;
|
|
1526
|
+
const next = /* @__PURE__ */ new Map();
|
|
1527
|
+
const add2 = (a, edge) => {
|
|
1528
|
+
const list = next.get(a);
|
|
1529
|
+
if (list) list.push(edge);
|
|
1530
|
+
else next.set(a, [edge]);
|
|
1531
|
+
};
|
|
1532
|
+
for (const e of frame2.elements) {
|
|
1533
|
+
const c = e.connector;
|
|
1534
|
+
const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
|
|
1535
|
+
if (!c || offline.has(e.id) || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
|
|
1536
|
+
const cost = routeCost(c.cost);
|
|
1537
|
+
add2(a, { to: b, via: e.id, back: false, cost });
|
|
1538
|
+
if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
|
|
1539
|
+
}
|
|
1540
|
+
const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
|
|
1541
|
+
let counter = 1;
|
|
1542
|
+
const tree = new FlowTree(from);
|
|
1543
|
+
const open = [from];
|
|
1544
|
+
while (open.length > 0) {
|
|
1545
|
+
let best = 0;
|
|
1546
|
+
for (let i = 1; i < open.length; i++) {
|
|
1547
|
+
const da = dist.get(open[i]), db = dist.get(open[best]);
|
|
1548
|
+
if (da < db || da === db && order.get(open[i]) < order.get(open[best])) best = i;
|
|
1549
|
+
}
|
|
1550
|
+
const n = open.splice(best, 1)[0];
|
|
1551
|
+
tree.settled.add(n);
|
|
1552
|
+
for (const edge of next.get(n) ?? []) {
|
|
1553
|
+
if (tree.settled.has(edge.to)) continue;
|
|
1554
|
+
const d = dist.get(n) + edge.cost;
|
|
1555
|
+
const old = dist.get(edge.to);
|
|
1556
|
+
if (old !== void 0 && old <= d) continue;
|
|
1557
|
+
if (old === void 0) open.push(edge.to);
|
|
1558
|
+
dist.set(edge.to, d);
|
|
1559
|
+
order.set(edge.to, counter++);
|
|
1560
|
+
tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return tree;
|
|
1564
|
+
}
|
|
1565
|
+
var FlowTree = class {
|
|
1566
|
+
constructor(from) {
|
|
1567
|
+
__publicField(this, "settled", /* @__PURE__ */ new Set());
|
|
1568
|
+
__publicField(this, "came", /* @__PURE__ */ new Map());
|
|
1569
|
+
__publicField(this, "from");
|
|
1570
|
+
this.from = from;
|
|
1571
|
+
}
|
|
1572
|
+
/** The first of `to` that can be reached; with `spread`, when the first can't be, every other one that can. */
|
|
1573
|
+
goals(to, spread) {
|
|
1574
|
+
const reachable = to.filter((x) => x !== this.from && this.settled.has(x));
|
|
1575
|
+
if (reachable.length === 0) return [];
|
|
1576
|
+
return spread && reachable[0] !== to[0] ? reachable : [reachable[0]];
|
|
1577
|
+
}
|
|
1578
|
+
/** The connectors to `goal`, as [connector, backwards] hops. */
|
|
1579
|
+
hops(goal) {
|
|
1580
|
+
const hops = [];
|
|
1581
|
+
for (let at = goal, step = this.came.get(at); step; at = step.from, step = this.came.get(at)) hops.push([step.via, step.back]);
|
|
1582
|
+
return hops.reverse();
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
// src/measure.ts
|
|
1587
|
+
var emptyMetrics = () => ({ elements: /* @__PURE__ */ new Map(), flows: /* @__PURE__ */ new Map(), carry: {} });
|
|
1588
|
+
function metricValue(m, metric, id) {
|
|
1589
|
+
const f = m.flows.get(id);
|
|
1590
|
+
if (f) {
|
|
1591
|
+
switch (metric) {
|
|
1592
|
+
case "throughput":
|
|
1593
|
+
return f.placed;
|
|
1594
|
+
case "backlog":
|
|
1595
|
+
return f.backlog;
|
|
1596
|
+
case "backup":
|
|
1597
|
+
return f.backup;
|
|
1598
|
+
case "transit":
|
|
1599
|
+
return f.transit;
|
|
1600
|
+
default:
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const e = m.elements.get(id);
|
|
1605
|
+
if (!e) return null;
|
|
1606
|
+
switch (metric) {
|
|
1607
|
+
case "throughput":
|
|
1608
|
+
return e.throughput;
|
|
1609
|
+
case "load":
|
|
1610
|
+
return e.load;
|
|
1611
|
+
case "delay":
|
|
1612
|
+
return e.delay;
|
|
1613
|
+
case "backlog":
|
|
1614
|
+
return e.backlog;
|
|
1615
|
+
case "inFlight":
|
|
1616
|
+
return e.inFlight;
|
|
1617
|
+
case "depth":
|
|
1618
|
+
return e.depth;
|
|
1619
|
+
default:
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
var TINY = 1e-9;
|
|
1624
|
+
var amount = (f) => clamp(f.volume, 1, 0, 1e12);
|
|
1625
|
+
function amountIn(f, values) {
|
|
1626
|
+
if (values === null || !f.overrides) return amount(f);
|
|
1627
|
+
let v = null;
|
|
1628
|
+
for (const o of f.overrides) if (o.volume !== null && !mentionsMetric(o.when) && holds(o.when, values)) v = o.volume;
|
|
1629
|
+
return v === null ? amount(f) : Math.min(Math.max(Number.isFinite(v) ? v : 1, 0), 1e12);
|
|
1630
|
+
}
|
|
1631
|
+
function delayOf(e) {
|
|
1632
|
+
const d = e.delay ?? null;
|
|
1633
|
+
if (e.kind === "connector" || d === null || !Number.isFinite(d)) return 0;
|
|
1634
|
+
return Math.min(Math.max(d, 0), 3600);
|
|
1635
|
+
}
|
|
1636
|
+
var queueKey = (shape, flow) => shape + "|" + flow;
|
|
1637
|
+
function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
|
|
1638
|
+
const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
|
|
1639
|
+
const byID = /* @__PURE__ */ new Map();
|
|
1640
|
+
for (const e of frame2.elements) if (!byID.has(e.id)) byID.set(e.id, e);
|
|
1641
|
+
const metrics2 = emptyMetrics();
|
|
1642
|
+
for (const e of frame2.elements) {
|
|
1643
|
+
if (e.kind === "connector" && !e.connector) continue;
|
|
1644
|
+
metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
|
|
1645
|
+
}
|
|
1646
|
+
const list = flows ?? [];
|
|
1647
|
+
const room = /* @__PURE__ */ new Map();
|
|
1648
|
+
for (const e of frame2.elements) {
|
|
1649
|
+
const c = e.capacity ?? null;
|
|
1650
|
+
if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
|
|
1651
|
+
}
|
|
1652
|
+
const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
|
|
1653
|
+
const route2 = (tree, goal) => {
|
|
1654
|
+
const hops = [], shapes2 = [];
|
|
1655
|
+
let at = goal;
|
|
1656
|
+
for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
|
|
1657
|
+
hops.push([step.via, step.back]);
|
|
1658
|
+
shapes2.push(at);
|
|
1659
|
+
at = step.from;
|
|
1660
|
+
}
|
|
1661
|
+
shapes2.push(at);
|
|
1662
|
+
return { hops: hops.reverse(), shapes: shapes2.reverse() };
|
|
1663
|
+
};
|
|
1664
|
+
const way = (hops, shapes2) => {
|
|
1665
|
+
const points = [], inside = [], stretches = [];
|
|
1666
|
+
let length = 0;
|
|
1667
|
+
hops.forEach(([id, back], k) => {
|
|
1668
|
+
const c = byID.get(id);
|
|
1669
|
+
if (!c) return;
|
|
1670
|
+
const line2 = back ? [...tracing(c, frame2).points].reverse() : tracing(c, frame2).points;
|
|
1671
|
+
if (line2.length === 0) return;
|
|
1672
|
+
const last = points[points.length - 1];
|
|
1673
|
+
if (last) {
|
|
1674
|
+
const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
|
|
1675
|
+
const shape = byID.get(shapes2[k]);
|
|
1676
|
+
inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
|
|
1677
|
+
length += gap;
|
|
1678
|
+
}
|
|
1679
|
+
const start = length;
|
|
1680
|
+
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);
|
|
1681
|
+
stretches.push([id, start, length]);
|
|
1682
|
+
points.push(...line2);
|
|
1683
|
+
});
|
|
1684
|
+
return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
|
|
1685
|
+
};
|
|
1686
|
+
const along = (w, elapsedAt, speed) => {
|
|
1687
|
+
let elapsed = elapsedAt, d = 0;
|
|
1688
|
+
for (const [a, b, delay] of w.inside) {
|
|
1689
|
+
const reach = (a - d) / speed;
|
|
1690
|
+
if (elapsed <= reach) break;
|
|
1691
|
+
elapsed -= reach;
|
|
1692
|
+
d = a;
|
|
1693
|
+
const stay = (b - a) / speed + delay;
|
|
1694
|
+
if (elapsed < stay) return null;
|
|
1695
|
+
elapsed -= stay;
|
|
1696
|
+
d = b;
|
|
1697
|
+
}
|
|
1698
|
+
d += elapsed * speed;
|
|
1699
|
+
return d >= 0 && d <= w.length ? d : null;
|
|
1700
|
+
};
|
|
1701
|
+
const union2 = (a, b) => /* @__PURE__ */ new Set([...a, ...b]);
|
|
1702
|
+
const bump = (id, f) => {
|
|
1703
|
+
const m = metrics2.elements.get(id);
|
|
1704
|
+
if (m) f(m);
|
|
1705
|
+
};
|
|
1706
|
+
const leaving = /* @__PURE__ */ new Map(), turn = [];
|
|
1707
|
+
for (const flow of list) {
|
|
1708
|
+
const n = leaving.get(flow.from) ?? 0;
|
|
1709
|
+
turn.push(n);
|
|
1710
|
+
leaving.set(flow.from, n + 1);
|
|
1711
|
+
}
|
|
1712
|
+
const biggest = list.length > 0 ? Math.max(...list.map(amount)) : 1;
|
|
1713
|
+
const packets = [];
|
|
1714
|
+
const waitingBy = /* @__PURE__ */ new Map();
|
|
1715
|
+
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);
|
|
1716
|
+
for (const [i, flow] of list.entries()) {
|
|
1717
|
+
if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
|
|
1718
|
+
const source = byID.get(flow.from);
|
|
1719
|
+
const base2 = source ? flowTree(flow.from, frame2, offline) : null;
|
|
1720
|
+
if (!source || !base2) continue;
|
|
1721
|
+
const volume = amountIn(flow, values);
|
|
1722
|
+
const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
|
|
1723
|
+
const placements = [];
|
|
1724
|
+
const waiting = [];
|
|
1725
|
+
const goals = base2.goals(flow.to, flow.spread === true);
|
|
1726
|
+
const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
|
|
1727
|
+
for (const [candidates, share] of shares) {
|
|
1728
|
+
if (!(share > TINY)) continue;
|
|
1729
|
+
let left = share;
|
|
1730
|
+
for (let n = 0; n < 32 && left > TINY; n++) {
|
|
1731
|
+
const tree = flowTree(flow.from, frame2, union2(offline, full()));
|
|
1732
|
+
const goal2 = tree?.goals(candidates, false)[0];
|
|
1733
|
+
if (!tree || goal2 === void 0) break;
|
|
1734
|
+
const r = route2(tree, goal2);
|
|
1735
|
+
const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
|
|
1736
|
+
const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
|
|
1737
|
+
const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
|
|
1738
|
+
if (!(fits > TINY)) break;
|
|
1739
|
+
for (const id of ids) if (room.has(id)) room.set(id, room.get(id) - fits);
|
|
1740
|
+
placements.push({ hops: r.hops, shapes: r.shapes, goal: goal2, amount: fits });
|
|
1741
|
+
left -= fits;
|
|
1742
|
+
}
|
|
1743
|
+
if (!(left > TINY)) continue;
|
|
1744
|
+
let at = flow.from;
|
|
1745
|
+
const goal = base2.goals(candidates, false)[0];
|
|
1746
|
+
if (goal !== void 0) {
|
|
1747
|
+
const r = route2(base2, goal), done = full();
|
|
1748
|
+
const sequence = [[r.shapes[0], r.shapes[0]]];
|
|
1749
|
+
r.hops.forEach(([id], k) => {
|
|
1750
|
+
sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
|
|
1751
|
+
});
|
|
1752
|
+
const first = sequence.find(([id]) => done.has(id));
|
|
1753
|
+
if (first) {
|
|
1754
|
+
at = first[1];
|
|
1755
|
+
const upTo = r.shapes.indexOf(at);
|
|
1756
|
+
for (let q = upTo; q >= 0; q--) {
|
|
1757
|
+
const s = byID.get(r.shapes[q]);
|
|
1758
|
+
if (s && s.queue === true && s.kind !== "connector") {
|
|
1759
|
+
at = r.shapes[q];
|
|
1760
|
+
break;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const n = r.shapes.indexOf(at);
|
|
1765
|
+
if (at !== flow.from && n >= 0) {
|
|
1766
|
+
const shapes2 = r.shapes.slice(0, n + 1), hops = r.hops.slice(0, n);
|
|
1767
|
+
for (const id of [...shapes2, ...hops.map(([h]) => h)]) if (room.has(id)) room.set(id, Math.max(0, room.get(id) - left));
|
|
1768
|
+
placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const existing = waiting.find(([s]) => s === at);
|
|
1772
|
+
if (existing) existing[1] += left;
|
|
1773
|
+
else waiting.push([at, left]);
|
|
1774
|
+
}
|
|
1775
|
+
waitingBy.set(flow.id, waiting);
|
|
1776
|
+
for (const p of placements) {
|
|
1777
|
+
for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
|
|
1778
|
+
m.throughput += p.amount;
|
|
1779
|
+
});
|
|
1780
|
+
if (!p.waits) fm.placed += p.amount;
|
|
1781
|
+
}
|
|
1782
|
+
for (const [at, a] of waiting) {
|
|
1783
|
+
bump(at, (m) => {
|
|
1784
|
+
m.backlog += a;
|
|
1785
|
+
});
|
|
1786
|
+
fm.backlog += a;
|
|
1787
|
+
}
|
|
1788
|
+
const reachedFirst = placements.filter((p) => !p.waits && p.goal === flow.to[0]).reduce((s, p) => s + p.amount, 0);
|
|
1789
|
+
fm.backup = volume > TINY ? Math.max(0, (volume - reachedFirst) / volume) : 0;
|
|
1790
|
+
const every = everyOf(flow);
|
|
1791
|
+
const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
|
|
1792
|
+
const phase = turn[i] / leaving.get(flow.from) * every;
|
|
1793
|
+
const color2 = flow.color ?? DEFAULT_COLOR;
|
|
1794
|
+
const cumulative = [];
|
|
1795
|
+
let sum = 0;
|
|
1796
|
+
for (const p of placements) {
|
|
1797
|
+
sum += p.amount / Math.max(volume, TINY);
|
|
1798
|
+
cumulative.push(sum);
|
|
1799
|
+
}
|
|
1800
|
+
if (cumulative.length > 0 && Math.abs(cumulative[cumulative.length - 1] - 1) < 1e-9) cumulative[cumulative.length - 1] = 1;
|
|
1801
|
+
let transitSum = 0;
|
|
1802
|
+
placements.forEach((p, r) => {
|
|
1803
|
+
const w = way(p.hops, p.shapes);
|
|
1804
|
+
if (!w) return;
|
|
1805
|
+
const travel = w.length / speed + w.inside.reduce((s, x) => s + x[2], 0);
|
|
1806
|
+
if (!p.waits) transitSum += travel * p.amount;
|
|
1807
|
+
const lo = Math.ceil((t - phase - travel) / every);
|
|
1808
|
+
const hi = Math.floor((t - phase) / every);
|
|
1809
|
+
const below = r === 0 ? 0 : cumulative[r - 1], upTo = cumulative[r];
|
|
1810
|
+
const dealt = (c, k) => Math.floor((k + 1) * c) - Math.floor(k * c);
|
|
1811
|
+
for (let k = Math.max(lo, hi - 3999); k <= hi; k++) {
|
|
1812
|
+
if (dealt(upTo, k) - dealt(below, k) !== 1) continue;
|
|
1813
|
+
const d = along(w, t - k * every - phase, speed);
|
|
1814
|
+
if (d === null) continue;
|
|
1815
|
+
const s = w.stretches.find(([, a, b]) => d >= a && d <= b);
|
|
1816
|
+
if (s) bump(s[0], (m) => {
|
|
1817
|
+
m.inFlight += 1;
|
|
1818
|
+
});
|
|
1819
|
+
const pt2 = w.line.point(d / w.length);
|
|
1820
|
+
packets.push(packet(packetID(flow.id, k), pt2.x - size / 2, pt2.y - size / 2, size, color2, source.layerID));
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
fm.transit = fm.placed > TINY ? transitSum / fm.placed : null;
|
|
1824
|
+
metrics2.flows.set(flow.id, fm);
|
|
1825
|
+
}
|
|
1826
|
+
const piles = /* @__PURE__ */ new Map();
|
|
1827
|
+
for (const flow of list) {
|
|
1828
|
+
const waiting = waitingBy.get(flow.id) ?? [];
|
|
1829
|
+
const shapes2 = waiting.map(([s]) => s);
|
|
1830
|
+
for (const e of frame2.elements) {
|
|
1831
|
+
const c = carried[queueKey(e.id, flow.id)];
|
|
1832
|
+
if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
|
|
1833
|
+
}
|
|
1834
|
+
if (shapes2.length === 0) continue;
|
|
1835
|
+
const volume = amountIn(flow, values);
|
|
1836
|
+
const every = everyOf(flow);
|
|
1837
|
+
const unit = every * Math.max(volume, TINY);
|
|
1838
|
+
const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
|
|
1839
|
+
const color2 = flow.color ?? DEFAULT_COLOR;
|
|
1840
|
+
shapes2.forEach((at, j) => {
|
|
1841
|
+
const shape = byID.get(at);
|
|
1842
|
+
if (!shape) return;
|
|
1843
|
+
const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
|
|
1844
|
+
const backlog = waiting.find(([s]) => s === at)?.[1] ?? 0;
|
|
1845
|
+
const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
|
|
1846
|
+
let depth = before + backlog * since;
|
|
1847
|
+
if (backlog <= TINY && before > TINY) {
|
|
1848
|
+
const trial = new Map(room);
|
|
1849
|
+
const outlets = [];
|
|
1850
|
+
let total = 0;
|
|
1851
|
+
for (let n2 = 0; n2 < 32; n2++) {
|
|
1852
|
+
const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
|
|
1853
|
+
const tree = flowTree(at, frame2, union2(offline, blocked));
|
|
1854
|
+
const goal = tree?.goals(flow.to, false)[0];
|
|
1855
|
+
if (!tree || goal === void 0) break;
|
|
1856
|
+
const r = route2(tree, goal);
|
|
1857
|
+
const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
|
|
1858
|
+
const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
|
|
1859
|
+
const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
|
|
1860
|
+
const rate = Number.isFinite(spare) ? spare : Math.max(before / 0.5 - total, TINY * 2);
|
|
1861
|
+
if (!(rate > TINY)) break;
|
|
1862
|
+
for (const id of ids) if (trial.has(id)) trial.set(id, trial.get(id) - rate);
|
|
1863
|
+
outlets.push({ hops: r.hops, shapes: r.shapes, rate });
|
|
1864
|
+
total += rate;
|
|
1865
|
+
if (!Number.isFinite(spare)) break;
|
|
1866
|
+
}
|
|
1867
|
+
if (total > TINY) {
|
|
1868
|
+
depth = Math.max(0, before - total * since);
|
|
1869
|
+
if (depth > TINY) {
|
|
1870
|
+
for (const [id, v] of trial) room.set(id, v);
|
|
1871
|
+
for (const o of outlets) for (const id of [...o.shapes, ...o.hops.map(([h]) => h)]) bump(id, (m) => {
|
|
1872
|
+
m.throughput += o.rate;
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
const lasts = before / total;
|
|
1876
|
+
outlets.forEach((o, i) => {
|
|
1877
|
+
const w = way(o.hops, o.shapes);
|
|
1878
|
+
if (!w) return;
|
|
1879
|
+
const spacing = Math.max(unit / o.rate, 0.05);
|
|
1880
|
+
for (let k = 0; k * spacing < lasts && k * spacing <= since && k < 2048; k++) {
|
|
1881
|
+
const d = along(w, since - k * spacing, speed);
|
|
1882
|
+
if (d === null) continue;
|
|
1883
|
+
const pt2 = w.line.point(d / w.length);
|
|
1884
|
+
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));
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (!(depth > TINY || backlog > TINY)) return;
|
|
1890
|
+
bump(at, (m) => {
|
|
1891
|
+
m.depth += depth;
|
|
1892
|
+
});
|
|
1893
|
+
if (depth > TINY) metrics2.carry[queueKey(at, flow.id)] = depth;
|
|
1894
|
+
const n = Math.min(Math.max(Math.ceil(depth / unit - 1e-9), 1), 30);
|
|
1895
|
+
const rows = Math.trunc((n + 9) / 10);
|
|
1896
|
+
const base2 = piles.get(at) ?? 0;
|
|
1897
|
+
piles.set(at, base2 + rows);
|
|
1898
|
+
for (let q = 0; q < n; q++) {
|
|
1899
|
+
const cx = shape.x + 2 + size / 2 + q % 10 * (size + 3);
|
|
1900
|
+
const cy = shape.y - 4 - size / 2 - (base2 + Math.trunc(q / 10)) * (size + 3);
|
|
1901
|
+
packets.push(packet(packetID(flow.id, 2 ** 39 + j * 64 + q), cx - size / 2, cy - size / 2, size, color2, layer));
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
for (const e of frame2.elements) {
|
|
1906
|
+
const c = e.capacity ?? null;
|
|
1907
|
+
const m = metrics2.elements.get(e.id);
|
|
1908
|
+
if (c === null || !Number.isFinite(c) || !m) continue;
|
|
1909
|
+
m.load = c > 0 ? m.throughput / c : 1;
|
|
1910
|
+
}
|
|
1911
|
+
return { packets, metrics: metrics2 };
|
|
1912
|
+
}
|
|
1913
|
+
var BLANK = {
|
|
1914
|
+
seed: 1,
|
|
1915
|
+
text: null,
|
|
1916
|
+
connector: null,
|
|
1917
|
+
groupID: null,
|
|
1918
|
+
points: null,
|
|
1919
|
+
path: null,
|
|
1920
|
+
subpaths: null,
|
|
1921
|
+
evenOdd: null,
|
|
1922
|
+
rotation: null,
|
|
1923
|
+
link: null,
|
|
1924
|
+
url: null,
|
|
1925
|
+
anchor: null,
|
|
1926
|
+
click: null,
|
|
1927
|
+
hover: null,
|
|
1928
|
+
when: null,
|
|
1929
|
+
overrides: null,
|
|
1930
|
+
symbol: null,
|
|
1931
|
+
playback: null,
|
|
1932
|
+
offset: null,
|
|
1933
|
+
args: null,
|
|
1934
|
+
image: null,
|
|
1935
|
+
reveal: null
|
|
1936
|
+
};
|
|
1937
|
+
function style(stroke, fill, strokeWidth, fillStyle = "solid", opacity2 = null) {
|
|
1938
|
+
return {
|
|
1939
|
+
stroke: { ...stroke },
|
|
1940
|
+
fill: { ...fill },
|
|
1941
|
+
fillStyle,
|
|
1942
|
+
strokeWidth,
|
|
1943
|
+
dash: "solid",
|
|
1944
|
+
roughness: 0,
|
|
1945
|
+
bowing: 1,
|
|
1946
|
+
fontSize: 18,
|
|
1947
|
+
font: null,
|
|
1948
|
+
gridSize: null,
|
|
1949
|
+
opacity: opacity2,
|
|
1950
|
+
dashOffset: null,
|
|
1951
|
+
textColor: null
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function packet(id, x, y, size, color2, layerID) {
|
|
1955
|
+
return { ...BLANK, id, kind: "ellipse", layerID, x, y, width: size, height: size, style: style(color2, color2, 1.5) };
|
|
1956
|
+
}
|
|
1957
|
+
function metricLookup(m, frame2) {
|
|
1958
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
1959
|
+
for (const e of frame2.elements) if (e.anchor !== null && !anchors.has(e.anchor)) anchors.set(e.anchor, e.id);
|
|
1960
|
+
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}$/;
|
|
1961
|
+
return (term, own) => {
|
|
1962
|
+
const id = term.target === "" ? own : anchors.get(term.target) ?? (UUID2.test(term.target) ? term.target.toUpperCase() : null);
|
|
1963
|
+
return id === null ? null : metricValue(m, term.metric, id);
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
|
|
1967
|
+
const lookup = metricLookup(metrics2, frame2);
|
|
1968
|
+
const out = [], readings = {};
|
|
1969
|
+
for (let e of elements) {
|
|
1970
|
+
if (values !== null && e.overrides) {
|
|
1971
|
+
for (const o of e.overrides) {
|
|
1972
|
+
const own = e.id;
|
|
1973
|
+
if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, own))) e = applyOverride(looksOnly(o), e);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
if (!e.gauge || values === null) {
|
|
1977
|
+
out.push(e);
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
const g = e.gauge;
|
|
1981
|
+
let shown = metricValue(metrics2, g.metric, g.of);
|
|
1982
|
+
const then = carried[gaugeKey(e.id)];
|
|
1983
|
+
if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
|
|
1984
|
+
const f = Math.max(0, since / GAUGE_EASE), smooth = f * f * (3 - 2 * f);
|
|
1985
|
+
shown = then + (shown - then) * smooth;
|
|
1986
|
+
}
|
|
1987
|
+
if (shown !== null) readings[gaugeKey(e.id)] = shown;
|
|
1988
|
+
out.push(...gauge(e, g, shown, frame2, flows));
|
|
1989
|
+
}
|
|
1990
|
+
return { elements: out, readings };
|
|
1991
|
+
}
|
|
1992
|
+
var gaugeKey = (id) => "gauge|" + id;
|
|
1993
|
+
var GAUGE_EASE = 1;
|
|
1994
|
+
var hex = (h) => {
|
|
1995
|
+
const v = parseInt(h.slice(1), 16);
|
|
1996
|
+
return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
|
|
1997
|
+
};
|
|
1998
|
+
var GAUGE_GREEN = hex("#16a34a");
|
|
1999
|
+
var GAUGE_AMBER = hex("#d97706");
|
|
2000
|
+
var GAUGE_RED = hex("#dc2626");
|
|
2001
|
+
var GAUGE_BLUE = hex("#2f6fdf");
|
|
2002
|
+
var GAUGE_TRACK = hex("#e5e7eb");
|
|
2003
|
+
var GAUGE_GRAY = hex("#9ca3af");
|
|
2004
|
+
function formatted(metric, v) {
|
|
2005
|
+
if (v === null || !Number.isFinite(v)) return "\u2013";
|
|
2006
|
+
switch (metric) {
|
|
2007
|
+
case "load":
|
|
2008
|
+
case "backup":
|
|
2009
|
+
return formatNumber(v * 100, 0) + "%";
|
|
2010
|
+
case "transit":
|
|
2011
|
+
case "delay":
|
|
2012
|
+
return formatNumber(v, Math.abs(v) >= 10 ? 1 : 2) + "s";
|
|
2013
|
+
default:
|
|
2014
|
+
return formatNumber(v, Math.abs(v) >= 100 ? 0 : Math.abs(v) >= 10 ? 1 : 2);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
function formatNumber(v, places) {
|
|
2018
|
+
const scale = places === 0 ? 1 : places === 1 ? 10 : 100;
|
|
2019
|
+
const n = Math.floor(Math.abs(v) * scale + 0.5);
|
|
2020
|
+
if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
|
|
2021
|
+
const whole = Math.trunc(n / scale), part = n - whole * scale;
|
|
2022
|
+
let s = String(whole);
|
|
2023
|
+
if (places > 0 && part > 0) {
|
|
2024
|
+
let digits = String(part);
|
|
2025
|
+
while (digits.length < places) digits = "0" + digits;
|
|
2026
|
+
while (digits.endsWith("0")) digits = digits.slice(0, -1);
|
|
2027
|
+
s += "." + digits;
|
|
2028
|
+
}
|
|
2029
|
+
return (v < 0 && n > 0 ? "-" : "") + s;
|
|
2030
|
+
}
|
|
2031
|
+
function gauge(e, g, value2, frame2, flows) {
|
|
2032
|
+
const target = frame2.elements.find((x) => x.id === g.of);
|
|
2033
|
+
const full = g.max ?? (g.metric === "load" ? 1 : target?.capacity ?? value2 ?? 1);
|
|
2034
|
+
const fraction = value2 === null ? 0 : full > 0 ? Math.min(Math.max(value2 / full, 0), 1) : 0;
|
|
2035
|
+
const warn = g.warn ?? (g.metric === "load" ? 0.7 : null), alarm = g.alarm ?? (g.metric === "load" ? 0.9 : null);
|
|
2036
|
+
let color2 = warn === null && alarm === null ? GAUGE_BLUE : GAUGE_GREEN;
|
|
2037
|
+
if (value2 !== null && warn !== null && value2 >= warn) color2 = GAUGE_AMBER;
|
|
2038
|
+
if (value2 !== null && alarm !== null && value2 >= alarm) color2 = GAUGE_RED;
|
|
2039
|
+
if (value2 === null) color2 = GAUGE_GRAY;
|
|
2040
|
+
let name = g.label;
|
|
2041
|
+
if (name === null) {
|
|
2042
|
+
const what = target ? target.text !== null ? target.text.split("\n")[0] : target.anchor : null;
|
|
2043
|
+
name = `${g.metric} ${what ?? (flows.some((f) => f.id === g.of) ? "flow" : "?")}`;
|
|
2044
|
+
}
|
|
2045
|
+
const kind = g.style ?? "number";
|
|
2046
|
+
const w = Math.max(e.width, 1), h = Math.max(e.height, 1);
|
|
2047
|
+
const line2 = (a, b) => [
|
|
2048
|
+
a.x / w,
|
|
2049
|
+
a.y / h,
|
|
2050
|
+
(a.x + (b.x - a.x) / 3) / w,
|
|
2051
|
+
(a.y + (b.y - a.y) / 3) / h,
|
|
2052
|
+
(a.x + 2 * (b.x - a.x) / 3) / w,
|
|
2053
|
+
(a.y + 2 * (b.y - a.y) / 3) / h,
|
|
2054
|
+
b.x / w,
|
|
2055
|
+
b.y / h
|
|
2056
|
+
];
|
|
2057
|
+
const box = (x, y, bw, bh) => {
|
|
2058
|
+
const p = [{ x, y }, { x: x + bw, y }, { x: x + bw, y: y + bh }, { x, y: y + bh }];
|
|
2059
|
+
let coords = line2(p[0], p[1]);
|
|
2060
|
+
for (let k = 1; k < 4; k++) coords = coords.concat(line2(p[k], p[(k + 1) % 4]).slice(2));
|
|
2061
|
+
return { closed: true, coords };
|
|
2062
|
+
};
|
|
2063
|
+
const arc = (c, radius, a0, a1) => {
|
|
2064
|
+
const pieces2 = Math.max(1, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 2) - 1e-9));
|
|
2065
|
+
const step = (a1 - a0) / pieces2, kappa = 4 / 3 * Math.tan(step / 4);
|
|
2066
|
+
const at = (a) => ({ x: c.x + radius * Math.cos(a), y: c.y + radius * Math.sin(a) });
|
|
2067
|
+
const s = at(a0);
|
|
2068
|
+
const coords = [s.x / w, s.y / h];
|
|
2069
|
+
for (let k = 0; k < pieces2; k++) {
|
|
2070
|
+
const a = a0 + k * step, b = a + step;
|
|
2071
|
+
const p0 = at(a), p3 = at(b);
|
|
2072
|
+
const c1 = { x: p0.x - kappa * radius * Math.sin(a), y: p0.y + kappa * radius * Math.cos(a) };
|
|
2073
|
+
const c2 = { x: p3.x + kappa * radius * Math.sin(b), y: p3.y - kappa * radius * Math.cos(b) };
|
|
2074
|
+
coords.push(c1.x / w, c1.y / h, c2.x / w, c2.y / h, p3.x / w, p3.y / h);
|
|
2075
|
+
}
|
|
2076
|
+
return { closed: false, coords };
|
|
2077
|
+
};
|
|
2078
|
+
const part = (k, subpaths, stroke, fill, width) => ({
|
|
2079
|
+
...BLANK,
|
|
2080
|
+
id: packetID(e.id, -(2 ** 38) - k),
|
|
2081
|
+
kind: "path",
|
|
2082
|
+
layerID: e.layerID,
|
|
2083
|
+
x: e.x,
|
|
2084
|
+
y: e.y,
|
|
2085
|
+
width: e.width,
|
|
2086
|
+
height: e.height,
|
|
2087
|
+
style: style(stroke, fill ?? { r: 0, g: 0, b: 0, a: 0 }, width, "solid", e.style.opacity),
|
|
2088
|
+
subpaths
|
|
2089
|
+
});
|
|
2090
|
+
const back = {
|
|
2091
|
+
...e,
|
|
2092
|
+
gauge: null,
|
|
2093
|
+
kind: "path",
|
|
2094
|
+
subpaths: [box(0, 0, w, h)],
|
|
2095
|
+
text: name + "\n" + formatted(g.metric, value2),
|
|
2096
|
+
style: { ...e.style }
|
|
2097
|
+
};
|
|
2098
|
+
const parts = [];
|
|
2099
|
+
if (kind === "number") {
|
|
2100
|
+
back.labelRect = { x: 0, y: 0, width: 1, height: 1 };
|
|
2101
|
+
back.style.textColor = { ...color2 };
|
|
2102
|
+
} else if (kind === "bar") {
|
|
2103
|
+
back.labelRect = { x: 0, y: 0, width: 1, height: 0.66 };
|
|
2104
|
+
const inset2 = Math.min(10, w * 0.08), barH = Math.min(12, h * 0.18), top = h * 0.72;
|
|
2105
|
+
const trackW = w - 2 * inset2;
|
|
2106
|
+
parts.push(part(1, [box(inset2, top, trackW, barH)], GAUGE_TRACK, GAUGE_TRACK, 0.5));
|
|
2107
|
+
if (fraction > 0) parts.push(part(2, [box(inset2, top, trackW * fraction, barH)], color2, color2, 0.5));
|
|
2108
|
+
} else {
|
|
2109
|
+
back.labelRect = { x: 0, y: 0.54, width: 1, height: 0.46 };
|
|
2110
|
+
const radius = Math.min(w * 0.38, h * 0.4), center2 = { x: w / 2, y: h * 0.5 };
|
|
2111
|
+
const thick = Math.max(2, radius * 0.16);
|
|
2112
|
+
parts.push(part(1, [arc(center2, radius, Math.PI, 2 * Math.PI)], GAUGE_TRACK, null, thick));
|
|
2113
|
+
if (fraction > 0) parts.push(part(2, [arc(center2, radius, Math.PI, Math.PI + Math.PI * fraction)], color2, null, thick));
|
|
2114
|
+
const a = Math.PI + Math.PI * fraction;
|
|
2115
|
+
const tip = { x: center2.x + radius * 0.85 * Math.cos(a), y: center2.y + radius * 0.85 * Math.sin(a) };
|
|
2116
|
+
parts.push(part(3, [{ closed: false, coords: line2(center2, tip) }], { r: 0.2, g: 0.2, b: 0.2, a: 1 }, null, 2));
|
|
2117
|
+
}
|
|
2118
|
+
return [back, ...parts];
|
|
2119
|
+
}
|
|
2120
|
+
|
|
1276
2121
|
// src/rough.ts
|
|
1277
|
-
var offset = (rng,
|
|
2122
|
+
var offset = (rng, amount2, roughness) => rng.signed() * amount2 * roughness;
|
|
1278
2123
|
function line(a, b, rng, roughness, bowing, preserveEnds, into) {
|
|
1279
2124
|
const length = Math.hypot(b.x - a.x, b.y - a.y);
|
|
1280
2125
|
if (!(length > 0.5)) return;
|
|
@@ -2130,17 +2975,43 @@ function trackMap(tl) {
|
|
|
2130
2975
|
for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
|
|
2131
2976
|
return m;
|
|
2132
2977
|
}
|
|
2133
|
-
function resolved(page, t) {
|
|
2134
|
-
return expandingSymbols(posed(page, t), clampTime(t));
|
|
2978
|
+
function resolved(page, t, state = "initial", since = null, carried = {}) {
|
|
2979
|
+
return expandingSymbols(posed(page, t, state, since, carried), clampTime(t));
|
|
2135
2980
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2981
|
+
var EMPTY_TIMELINE = { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
|
|
2982
|
+
function posed(page, t, state = "initial", since = null, carried = {}) {
|
|
2983
|
+
return posedWithMetrics(page, t, state, since, carried).frame;
|
|
2984
|
+
}
|
|
2985
|
+
function posedWithMetrics(page, t, state = "initial", since = null, carried = {}) {
|
|
2986
|
+
const off = stateHidden(page, state);
|
|
2987
|
+
const values = stateValues(page, state);
|
|
2988
|
+
const active = /* @__PURE__ */ new Map();
|
|
2989
|
+
for (const e of page.elements) {
|
|
2990
|
+
const o = activeOverrides(e, values);
|
|
2991
|
+
if (o.length > 0) active.set(e.id, o);
|
|
2992
|
+
}
|
|
2993
|
+
const flowing = (page.flows ?? []).length > 0;
|
|
2994
|
+
const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
|
|
2995
|
+
const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
|
|
2996
|
+
if (!timeline) return { frame: page, metrics: emptyMetrics() };
|
|
2139
2997
|
t = clampTime(t);
|
|
2140
2998
|
const tracks = trackMap(timeline);
|
|
2999
|
+
for (const [id, overrides] of active) {
|
|
3000
|
+
const track = tracks.get(id);
|
|
3001
|
+
if (!track?.follow || !overrides.some((o) => o.reroute)) continue;
|
|
3002
|
+
tracks.set(id, {
|
|
3003
|
+
...track,
|
|
3004
|
+
follow: track.follow.map((f) => {
|
|
3005
|
+
let path = f.path;
|
|
3006
|
+
for (const o of overrides) path = o.reroute?.[path] ?? path;
|
|
3007
|
+
return { ...f, path };
|
|
3008
|
+
})
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
2141
3011
|
const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
|
|
2142
3012
|
const hidden = /* @__PURE__ */ new Set();
|
|
2143
3013
|
for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
|
|
3014
|
+
for (const id of off) hidden.add(id);
|
|
2144
3015
|
let moving = null;
|
|
2145
3016
|
if (timeline.settle === true) {
|
|
2146
3017
|
moving = /* @__PURE__ */ new Set();
|
|
@@ -2156,6 +3027,14 @@ function posed(page, t) {
|
|
|
2156
3027
|
let elements = page.elements.map((e) => {
|
|
2157
3028
|
const track = tracks.get(e.id);
|
|
2158
3029
|
let out = track ? applyTrack(track, e, t) : e;
|
|
3030
|
+
for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
|
|
3031
|
+
const list = active.get(e.id);
|
|
3032
|
+
if (list) {
|
|
3033
|
+
const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
|
|
3034
|
+
const delays = list.map((o) => o.delay).filter((d) => d !== null && Number.isFinite(d));
|
|
3035
|
+
if (caps.length > 0) out.capacity = Math.max(Math.min(...caps), 0);
|
|
3036
|
+
if (delays.length > 0 && out.kind !== "connector") out.delay = Math.max(Math.max(...delays), 0);
|
|
3037
|
+
}
|
|
2159
3038
|
if (reseed !== null && reseed > 0 && boils(e)) {
|
|
2160
3039
|
if (out === e) out = cloneElement(e);
|
|
2161
3040
|
out.seed = boiled(e.seed, reseed);
|
|
@@ -2179,7 +3058,7 @@ function posed(page, t) {
|
|
|
2179
3058
|
elements = elements.map((e) => {
|
|
2180
3059
|
const c = e.connector;
|
|
2181
3060
|
if (!c || ![c.start.elementID, c.end.elementID].some((id) => id !== null && hidden.has(id))) return e;
|
|
2182
|
-
if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false)) {
|
|
3061
|
+
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))) {
|
|
2183
3062
|
gone.add(e.id);
|
|
2184
3063
|
return e;
|
|
2185
3064
|
}
|
|
@@ -2189,13 +3068,24 @@ function posed(page, t) {
|
|
|
2189
3068
|
pinned.connector.end = { elementID: null, port: c.end.port, x: b.x, y: b.y };
|
|
2190
3069
|
return pinned;
|
|
2191
3070
|
});
|
|
2192
|
-
|
|
3071
|
+
elements = elements.filter((e) => !gone.has(e.id));
|
|
3072
|
+
let metrics2 = emptyMetrics();
|
|
3073
|
+
if (measuring) {
|
|
3074
|
+
const offline = new Set([...active].filter(([, list]) => list.some((o) => o.offline === true)).map(([id]) => id));
|
|
3075
|
+
const frame2 = { ...page, elements, timeline: null };
|
|
3076
|
+
const out = flowFrame(page.flows, frame2, t, values, offline, since, carried);
|
|
3077
|
+
metrics2 = out.metrics;
|
|
3078
|
+
const finished = finishing(elements, out.metrics, values, frame2, page.flows ?? [], Math.max(0, Math.min(since ?? t, 1e6)), carried);
|
|
3079
|
+
Object.assign(metrics2.carry, finished.readings);
|
|
3080
|
+
elements = [...finished.elements, ...out.packets];
|
|
3081
|
+
}
|
|
3082
|
+
return { frame: { ...page, elements, timeline: null, flows: measuring ? null : page.flows }, metrics: metrics2 };
|
|
2193
3083
|
}
|
|
2194
3084
|
function posterTime(page) {
|
|
2195
3085
|
return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
|
|
2196
3086
|
}
|
|
2197
3087
|
function still(page) {
|
|
2198
|
-
return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(page, 0);
|
|
3088
|
+
return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(posed(page, 0), 0);
|
|
2199
3089
|
}
|
|
2200
3090
|
function cameraRect(tl, t) {
|
|
2201
3091
|
const keys = tl.camera ?? [];
|
|
@@ -2304,7 +3194,7 @@ function filled(s, args) {
|
|
|
2304
3194
|
}) };
|
|
2305
3195
|
}
|
|
2306
3196
|
function symbolPage(s) {
|
|
2307
|
-
return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null };
|
|
3197
|
+
return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null, flows: null };
|
|
2308
3198
|
}
|
|
2309
3199
|
function hasInstances(page) {
|
|
2310
3200
|
return page.elements.some((e) => e.kind === "symbol");
|
|
@@ -2411,8 +3301,8 @@ function mix2(x) {
|
|
|
2411
3301
|
return z ^ z >> 31n;
|
|
2412
3302
|
}
|
|
2413
3303
|
function halves(id) {
|
|
2414
|
-
const
|
|
2415
|
-
return [BigInt(`0x${
|
|
3304
|
+
const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
|
|
3305
|
+
return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
|
|
2416
3306
|
}
|
|
2417
3307
|
function derivedID(instance, inner) {
|
|
2418
3308
|
const [a, b] = halves(instance), [c, d] = halves(inner);
|
|
@@ -2433,16 +3323,21 @@ function clickTargets(page, file) {
|
|
|
2433
3323
|
let pageOf = null;
|
|
2434
3324
|
for (const e of page.elements) {
|
|
2435
3325
|
const c = e.click;
|
|
3326
|
+
const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
|
|
2436
3327
|
if (c?.scene) {
|
|
2437
3328
|
const i = sceneNamed(c.scene, markers);
|
|
2438
3329
|
if (i >= 0) {
|
|
2439
|
-
out.set(e.id, { scene: i });
|
|
3330
|
+
out.set(e.id, { scene: i, ...state });
|
|
2440
3331
|
continue;
|
|
2441
3332
|
}
|
|
2442
3333
|
}
|
|
2443
3334
|
const url = (c?.url ? allowedURL(c.url) : null) ?? webLink(e.url);
|
|
2444
3335
|
if (url) {
|
|
2445
|
-
out.set(e.id, { url });
|
|
3336
|
+
out.set(e.id, { url, ...state });
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
if (state) {
|
|
3340
|
+
out.set(e.id, { state: true, ...state });
|
|
2446
3341
|
continue;
|
|
2447
3342
|
}
|
|
2448
3343
|
if (e.link && file && file.pages.length > 1) {
|
|
@@ -2461,14 +3356,14 @@ function clickTargets(page, file) {
|
|
|
2461
3356
|
function hoveredElement(e) {
|
|
2462
3357
|
const h = e.hover;
|
|
2463
3358
|
if (!h) return e;
|
|
2464
|
-
const
|
|
2465
|
-
if (h.stroke)
|
|
2466
|
-
if (h.fill)
|
|
3359
|
+
const style2 = { ...e.style };
|
|
3360
|
+
if (h.stroke) style2.stroke = h.stroke;
|
|
3361
|
+
if (h.fill) style2.fill = h.fill;
|
|
2467
3362
|
if (h.opacity !== null && Number.isFinite(h.opacity)) {
|
|
2468
3363
|
const o = Math.min(Math.max(h.opacity, 0), 1);
|
|
2469
|
-
|
|
3364
|
+
style2.opacity = o >= 1 ? null : o;
|
|
2470
3365
|
}
|
|
2471
|
-
return { ...e, style };
|
|
3366
|
+
return { ...e, style: style2 };
|
|
2472
3367
|
}
|
|
2473
3368
|
function hovered(page, ids) {
|
|
2474
3369
|
return { ...page, elements: page.elements.map((e) => ids.has(e.id) ? hoveredElement(e) : e) };
|
|
@@ -2672,14 +3567,14 @@ function parseList(lines, start) {
|
|
|
2672
3567
|
}
|
|
2673
3568
|
return { block: { kind: "list", ordered, items }, next: i };
|
|
2674
3569
|
}
|
|
2675
|
-
function inline(src,
|
|
3570
|
+
function inline(src, style2 = {}) {
|
|
2676
3571
|
const out = [];
|
|
2677
3572
|
const push = (text, s) => {
|
|
2678
3573
|
if (text) out.push({ ...s, text });
|
|
2679
3574
|
};
|
|
2680
3575
|
let i = 0, buf = "";
|
|
2681
3576
|
const emit = () => {
|
|
2682
|
-
push(buf,
|
|
3577
|
+
push(buf, style2);
|
|
2683
3578
|
buf = "";
|
|
2684
3579
|
};
|
|
2685
3580
|
while (i < src.length) {
|
|
@@ -2707,43 +3602,43 @@ function inline(src, style = {}) {
|
|
|
2707
3602
|
emit();
|
|
2708
3603
|
let code = m[2].replace(/\n/g, " ");
|
|
2709
3604
|
if (/^ .* $/.test(code)) code = code.slice(1, -1);
|
|
2710
|
-
push(code, { ...
|
|
3605
|
+
push(code, { ...style2, mono: true });
|
|
2711
3606
|
i += m[0].length;
|
|
2712
3607
|
continue;
|
|
2713
3608
|
}
|
|
2714
3609
|
if (m = /^!\[([^\]]*)\]\([^)]*\)/.exec(rest)) {
|
|
2715
3610
|
emit();
|
|
2716
|
-
out.push(...inline(m[1],
|
|
3611
|
+
out.push(...inline(m[1], style2));
|
|
2717
3612
|
i += m[0].length;
|
|
2718
3613
|
continue;
|
|
2719
3614
|
}
|
|
2720
3615
|
if (m = /^\[([^\]]+)\]\([^)]*\)/.exec(rest)) {
|
|
2721
3616
|
emit();
|
|
2722
|
-
out.push(...inline(m[1], { ...
|
|
3617
|
+
out.push(...inline(m[1], { ...style2, link: true }));
|
|
2723
3618
|
i += m[0].length;
|
|
2724
3619
|
continue;
|
|
2725
3620
|
}
|
|
2726
3621
|
if (m = /^<((?:https?|ftp|mailto):[^>\s]+)>/.exec(rest)) {
|
|
2727
3622
|
emit();
|
|
2728
|
-
push(m[1], { ...
|
|
3623
|
+
push(m[1], { ...style2, link: true });
|
|
2729
3624
|
i += m[0].length;
|
|
2730
3625
|
continue;
|
|
2731
3626
|
}
|
|
2732
3627
|
if (m = /^~~(?=\S)([\s\S]*?\S)~~/.exec(rest)) {
|
|
2733
3628
|
emit();
|
|
2734
|
-
out.push(...inline(m[1], { ...
|
|
3629
|
+
out.push(...inline(m[1], { ...style2, strike: true }));
|
|
2735
3630
|
i += m[0].length;
|
|
2736
3631
|
continue;
|
|
2737
3632
|
}
|
|
2738
3633
|
if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
|
|
2739
3634
|
emit();
|
|
2740
|
-
out.push(...inline(m[2], { ...
|
|
3635
|
+
out.push(...inline(m[2], { ...style2, bold: true }));
|
|
2741
3636
|
i += m[0].length;
|
|
2742
3637
|
continue;
|
|
2743
3638
|
}
|
|
2744
3639
|
if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
|
|
2745
3640
|
emit();
|
|
2746
|
-
out.push(...inline(m[2], { ...
|
|
3641
|
+
out.push(...inline(m[2], { ...style2, italic: true }));
|
|
2747
3642
|
i += m[0].length;
|
|
2748
3643
|
continue;
|
|
2749
3644
|
}
|
|
@@ -3356,6 +4251,16 @@ var Playback = class {
|
|
|
3356
4251
|
__publicField(this, "plan");
|
|
3357
4252
|
__publicField(this, "scenes");
|
|
3358
4253
|
__publicField(this, "sceneNames");
|
|
4254
|
+
/** Whether each scene loops (Marker.loop). */
|
|
4255
|
+
__publicField(this, "sceneLoops");
|
|
4256
|
+
/** The page's variables now: their starting values until clicks change them. */
|
|
4257
|
+
__publicField(this, "state");
|
|
4258
|
+
/** When the variables last changed (performance.now() milliseconds): backlog piles up from then (ADR 0003). */
|
|
4259
|
+
__publicField(this, "stateSince", typeof performance === "undefined" ? 0 : performance.now());
|
|
4260
|
+
/** A still (the `at` attribute): backlog is drawn for the page time, not the wall clock. */
|
|
4261
|
+
__publicField(this, "still", false);
|
|
4262
|
+
/** What each queue held when the variables last changed, which it drains from (FrameMetrics.carry). */
|
|
4263
|
+
__publicField(this, "carried", {});
|
|
3359
4264
|
__publicField(this, "background");
|
|
3360
4265
|
__publicField(this, "ink");
|
|
3361
4266
|
/** What clicking each clickable element does, by element id. */
|
|
@@ -3415,6 +4320,8 @@ var Playback = class {
|
|
|
3415
4320
|
};
|
|
3416
4321
|
this.scenes = (page.timeline?.markers ?? []).map((m) => near(m.t));
|
|
3417
4322
|
this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
|
|
4323
|
+
this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
|
|
4324
|
+
this.state = { ...page.variables ?? {} };
|
|
3418
4325
|
const theme = themeNamed(page.theme);
|
|
3419
4326
|
this.background = theme?.background ?? WHITE;
|
|
3420
4327
|
this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
|
|
@@ -3425,6 +4332,10 @@ var Playback = class {
|
|
|
3425
4332
|
this.svg.style.maxWidth = `${Math.ceil(this.plan.size.width)}px`;
|
|
3426
4333
|
into.replaceChildren(this.svg);
|
|
3427
4334
|
}
|
|
4335
|
+
/** Seconds since the variables last changed, or null for a still. */
|
|
4336
|
+
sinceChange(now = typeof performance === "undefined" ? 0 : performance.now()) {
|
|
4337
|
+
return this.still || typeof performance === "undefined" ? null : (now - this.stateSince) / 1e3;
|
|
4338
|
+
}
|
|
3428
4339
|
get frameCount() {
|
|
3429
4340
|
return this.plan.times.length;
|
|
3430
4341
|
}
|
|
@@ -3465,7 +4376,7 @@ var Playback = class {
|
|
|
3465
4376
|
const times = /* @__PURE__ */ new Map();
|
|
3466
4377
|
for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
|
|
3467
4378
|
const tc = clampTime(t);
|
|
3468
|
-
const pose = posed(this.page, tc);
|
|
4379
|
+
const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
|
|
3469
4380
|
const frame2 = expandingSymbols(pose, tc, true, times);
|
|
3470
4381
|
const { parts } = svgParts(frame2, this.background, r);
|
|
3471
4382
|
const owned = (m, parts2) => {
|
|
@@ -3587,6 +4498,20 @@ ${h}</g>
|
|
|
3587
4498
|
});
|
|
3588
4499
|
return s;
|
|
3589
4500
|
}
|
|
4501
|
+
/** The frame after scene `s`'s last: the next scene's, or the frame count after the last scene. */
|
|
4502
|
+
sceneEnd(s) {
|
|
4503
|
+
return this.scenes.find((f, j) => j > s && f > this.scenes[s]) ?? this.frameCount;
|
|
4504
|
+
}
|
|
4505
|
+
/** Where playing goes from frame `i`, and whether it stops there: on into a looping scene, round to the start of
|
|
4506
|
+
* the looping scene `i` ends, or a stop at a scene or the end (`loops`: whether the timeline loops at its end). */
|
|
4507
|
+
step(i, loops) {
|
|
4508
|
+
const next = i + 1;
|
|
4509
|
+
const s = this.sceneAt(i);
|
|
4510
|
+
if (s >= 0 && this.sceneLoops[s] && next >= this.sceneEnd(s)) return { frame: this.scenes[s], stop: false };
|
|
4511
|
+
if (next >= this.frameCount) return { frame: 0, stop: !loops || this.scenes.length > 0 };
|
|
4512
|
+
const entered = this.scenes.indexOf(next);
|
|
4513
|
+
return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
|
|
4514
|
+
}
|
|
3590
4515
|
};
|
|
3591
4516
|
var ElementBase = typeof HTMLElement === "undefined" ? class {
|
|
3592
4517
|
} : HTMLElement;
|
|
@@ -3708,6 +4633,7 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3708
4633
|
const file = this.file;
|
|
3709
4634
|
const page = file.pages[pageIndex(file, this.getAttribute("page"))];
|
|
3710
4635
|
const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
|
|
4636
|
+
p.still = this.hasAttribute("at");
|
|
3711
4637
|
this.playback = p;
|
|
3712
4638
|
this.style.setProperty("--dottie-page", color(p.background));
|
|
3713
4639
|
this.style.setProperty("--dottie-ink", color(p.ink));
|
|
@@ -3740,7 +4666,7 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3740
4666
|
this.setPlaying(false);
|
|
3741
4667
|
return;
|
|
3742
4668
|
}
|
|
3743
|
-
this.setPlaying(!this.hasAttribute("no-autoplay") && !p.scenes.includes(0));
|
|
4669
|
+
this.setPlaying(!this.hasAttribute("no-autoplay") && (!p.scenes.includes(0) || p.sceneLoops[p.scenes.indexOf(0)]));
|
|
3744
4670
|
}
|
|
3745
4671
|
// MARK: Playback
|
|
3746
4672
|
get paused() {
|
|
@@ -3766,11 +4692,13 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3766
4692
|
pause() {
|
|
3767
4693
|
this.setPlaying(false);
|
|
3768
4694
|
}
|
|
3769
|
-
/** Plays on to the next scene (from the start after the last). */
|
|
4695
|
+
/** Plays on to the next scene (from the start after the last). Out of a looping scene it jumps to the next. */
|
|
3770
4696
|
nextScene() {
|
|
3771
4697
|
const p = this.playback;
|
|
3772
4698
|
if (!p) return;
|
|
3773
|
-
|
|
4699
|
+
const s = p.sceneAt(p.frame);
|
|
4700
|
+
if (s >= 0 && p.sceneLoops[s]) p.show(s + 1 < p.scenes.length ? p.scenes[s + 1] : 0);
|
|
4701
|
+
else if (p.frame >= p.frameCount - 1) p.show(0);
|
|
3774
4702
|
this.setPlaying(true);
|
|
3775
4703
|
}
|
|
3776
4704
|
/** Back to the start of this scene, or the one before if already there. */
|
|
@@ -3799,6 +4727,14 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3799
4727
|
this.scheduleReplay();
|
|
3800
4728
|
}
|
|
3801
4729
|
if (!click) return;
|
|
4730
|
+
if (click.set || click.toggle || click.reset) {
|
|
4731
|
+
this.setState(applyClick(
|
|
4732
|
+
{ scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
|
|
4733
|
+
p.state,
|
|
4734
|
+
p.page.variables ?? {}
|
|
4735
|
+
));
|
|
4736
|
+
}
|
|
4737
|
+
if ("state" in click) return;
|
|
3802
4738
|
if ("url" in click) {
|
|
3803
4739
|
if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
|
|
3804
4740
|
} else if ("page" in click) {
|
|
@@ -3806,13 +4742,30 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3806
4742
|
} else if (click.scene >= 0 && click.scene < p.scenes.length) {
|
|
3807
4743
|
if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
|
|
3808
4744
|
this.setPlaying(false);
|
|
3809
|
-
p.show(click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
|
|
4745
|
+
p.show(p.sceneLoops[click.scene] ? p.scenes[click.scene] : click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
|
|
3810
4746
|
} else {
|
|
3811
4747
|
p.show(p.scenes[click.scene]);
|
|
3812
4748
|
this.setPlaying(true);
|
|
3813
4749
|
}
|
|
3814
4750
|
}
|
|
3815
4751
|
}
|
|
4752
|
+
/** The page's variables now. */
|
|
4753
|
+
get variables() {
|
|
4754
|
+
return { ...this.playback?.state ?? {} };
|
|
4755
|
+
}
|
|
4756
|
+
/** Gives the page's variables these values (the others keep theirs), redraws, and fires `state`. */
|
|
4757
|
+
setState(values) {
|
|
4758
|
+
const p = this.playback;
|
|
4759
|
+
if (!p) return;
|
|
4760
|
+
const next = { ...p.state, ...values };
|
|
4761
|
+
if (Object.keys(next).every((k) => next[k] === p.state[k])) return;
|
|
4762
|
+
const t = clampTime(p.last.t);
|
|
4763
|
+
p.carried = posedWithMetrics(p.page, t, p.state, p.sinceChange(), p.carried).metrics.carry;
|
|
4764
|
+
p.state = next;
|
|
4765
|
+
p.stateSince = performance.now();
|
|
4766
|
+
p.redraw();
|
|
4767
|
+
this.dispatchEvent(new CustomEvent("state", { detail: { variables: { ...next } } }));
|
|
4768
|
+
}
|
|
3816
4769
|
/** Redraws, while the page itself stands still, as long as a clicked copy is playing through. */
|
|
3817
4770
|
scheduleReplay() {
|
|
3818
4771
|
if (this.replayRaf) return;
|
|
@@ -3838,7 +4791,8 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3838
4791
|
schedule() {
|
|
3839
4792
|
if (this.playing && this.visible && !this.raf && this.isConnected) this.raf = requestAnimationFrame((now) => this.tick(now));
|
|
3840
4793
|
}
|
|
3841
|
-
/** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end
|
|
4794
|
+
/** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end, except that a
|
|
4795
|
+
* looping scene plays round (Playback.step). */
|
|
3842
4796
|
tick(now) {
|
|
3843
4797
|
this.raf = 0;
|
|
3844
4798
|
const p = this.playback;
|
|
@@ -3849,14 +4803,14 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3849
4803
|
let advanced = false;
|
|
3850
4804
|
while (this.playing && this.acc >= step) {
|
|
3851
4805
|
this.acc -= step;
|
|
3852
|
-
const
|
|
3853
|
-
if (
|
|
4806
|
+
const { frame: frame2, stop } = p.step(p.frame, !this.hasAttribute("no-loop"));
|
|
4807
|
+
if (stop && frame2 === 0 && p.frame + 1 >= p.frameCount) {
|
|
3854
4808
|
this.setPlaying(false);
|
|
3855
4809
|
break;
|
|
3856
4810
|
}
|
|
3857
|
-
p.frame =
|
|
4811
|
+
p.frame = frame2;
|
|
3858
4812
|
advanced = true;
|
|
3859
|
-
if (
|
|
4813
|
+
if (stop) {
|
|
3860
4814
|
this.setPlaying(false);
|
|
3861
4815
|
break;
|
|
3862
4816
|
}
|
|
@@ -4006,7 +4960,7 @@ function render(source, options = {}) {
|
|
|
4006
4960
|
function isFile(v) {
|
|
4007
4961
|
return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
|
|
4008
4962
|
}
|
|
4009
|
-
var VERSION = "0.
|
|
4963
|
+
var VERSION = "0.8.0";
|
|
4010
4964
|
|
|
4011
4965
|
// src/player-entry.ts
|
|
4012
4966
|
definePlayer();
|