@sebastianwessel/isostate 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/animation/controller.d.ts +25 -1
- package/dist/animation/controller.d.ts.map +1 -1
- package/dist/browser/isostate.runtime.js +5 -2448
- package/dist/browser/isostate.runtime.js.map +21 -1
- package/dist/browser-runtime.d.ts +5 -0
- package/dist/browser-runtime.d.ts.map +1 -0
- package/dist/chunks/{index-CDQt8CfR.js → index-DoyWxJDm.js} +321 -20
- package/dist/chunks/index-DoyWxJDm.js.map +1 -0
- package/dist/dsl/index.js +111 -2
- package/dist/dsl/index.js.map +1 -1
- package/dist/dsl/scene-parser.d.ts.map +1 -1
- package/dist/dsl/scene-validator.d.ts +2 -1
- package/dist/dsl/scene-validator.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/rendering/rendering-engine.d.ts +15 -1
- package/dist/rendering/rendering-engine.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/mount-scene.d.ts +9 -0
- package/dist/runtime/mount-scene.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/node.d.ts +39 -0
- package/dist/types/node.d.ts.map +1 -1
- package/dist/types/scene.d.ts +3 -1
- package/dist/types/scene.d.ts.map +1 -1
- package/package.json +13 -1
- package/dist/chunks/index-CDQt8CfR.js.map +0 -1
|
@@ -1,2449 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
* The engine does not manipulate DOM. It computes interpolation data and lifecycle
|
|
5
|
-
* transitions that the controller applies to the rendering engine.
|
|
6
|
-
*/
|
|
7
|
-
class AnimationEngine {
|
|
8
|
-
_bundle = null;
|
|
9
|
-
_progress = 0;
|
|
10
|
-
_paused = false;
|
|
11
|
-
_prevFrameMap = new Map();
|
|
12
|
-
_elementFrameMap = new Map();
|
|
13
|
-
_prevConnectorFrameMap = new Map();
|
|
14
|
-
_connectorFrameMap = new Map();
|
|
15
|
-
get bundle() {
|
|
16
|
-
return this._bundle;
|
|
17
|
-
}
|
|
18
|
-
get progress() {
|
|
19
|
-
return this._progress;
|
|
20
|
-
}
|
|
21
|
-
getProgress() {
|
|
22
|
-
return this._progress;
|
|
23
|
-
}
|
|
24
|
-
get paused() {
|
|
25
|
-
return this._paused;
|
|
26
|
-
}
|
|
27
|
-
isPaused() {
|
|
28
|
-
return this._paused;
|
|
29
|
-
}
|
|
30
|
-
get elementsCount() {
|
|
31
|
-
return this._elementFrameMap.size;
|
|
32
|
-
}
|
|
33
|
-
get connectorsCount() {
|
|
34
|
-
return this._connectorFrameMap.size;
|
|
35
|
-
}
|
|
36
|
-
/** Initialize with a compiled runtime bundle. */
|
|
37
|
-
init(bundle) {
|
|
38
|
-
this._bundle = bundle;
|
|
39
|
-
this._progress = 0;
|
|
40
|
-
this._paused = false;
|
|
41
|
-
this._prevFrameMap.clear();
|
|
42
|
-
this._elementFrameMap.clear();
|
|
43
|
-
this._prevConnectorFrameMap.clear();
|
|
44
|
-
this._connectorFrameMap.clear();
|
|
45
|
-
const initial = resolveFrameMap(bundle, 0);
|
|
46
|
-
this._elementFrameMap = initial;
|
|
47
|
-
this._prevFrameMap = cloneFrameMap(initial);
|
|
48
|
-
const initialConnectors = resolveConnectorFrameMap(bundle, 0);
|
|
49
|
-
this._connectorFrameMap = initialConnectors;
|
|
50
|
-
this._prevConnectorFrameMap = cloneConnectorFrameMap(initialConnectors);
|
|
51
|
-
}
|
|
52
|
-
/** Set current scroll progress (0-1) and compute frame update. */
|
|
53
|
-
setProgress(progress) {
|
|
54
|
-
const clamped = Math.max(0, Math.min(1, progress));
|
|
55
|
-
if (this._paused)
|
|
56
|
-
return;
|
|
57
|
-
this._progress = clamped;
|
|
58
|
-
if (!this._bundle)
|
|
59
|
-
return;
|
|
60
|
-
this._prevFrameMap = cloneFrameMap(this._elementFrameMap);
|
|
61
|
-
this._elementFrameMap = resolveFrameMap(this._bundle, clamped);
|
|
62
|
-
this._prevConnectorFrameMap = cloneConnectorFrameMap(this._connectorFrameMap);
|
|
63
|
-
this._connectorFrameMap = resolveConnectorFrameMap(this._bundle, clamped);
|
|
64
|
-
}
|
|
65
|
-
/** Get interpolated FrameUpdate for an element id or runtime element. */
|
|
66
|
-
getElementUpdate(element) {
|
|
67
|
-
const id = typeof element === "string" ? element : element.id;
|
|
68
|
-
const frame = this._elementFrameMap.get(id);
|
|
69
|
-
if (!frame) {
|
|
70
|
-
return {
|
|
71
|
-
id,
|
|
72
|
-
asset: "",
|
|
73
|
-
lifecycle: "removed",
|
|
74
|
-
ambient: [],
|
|
75
|
-
pos: [0, 0],
|
|
76
|
-
size: 1,
|
|
77
|
-
layer: "",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
return frameToUpdate(frame);
|
|
81
|
-
}
|
|
82
|
-
getFrameUpdates() {
|
|
83
|
-
return [...this._elementFrameMap.values()].map(frameToUpdate);
|
|
84
|
-
}
|
|
85
|
-
/** Get interpolated ConnectorFrameUpdate for a connector id or runtime connector. */
|
|
86
|
-
getConnectorUpdate(connector) {
|
|
87
|
-
const id = typeof connector === "string" ? connector : connector.id;
|
|
88
|
-
const frame = this._connectorFrameMap.get(id);
|
|
89
|
-
if (!frame)
|
|
90
|
-
return connectorFrameToUpdate(removedConnectorFrame(id));
|
|
91
|
-
return connectorFrameToUpdate(frame);
|
|
92
|
-
}
|
|
93
|
-
getConnectorFrameUpdates() {
|
|
94
|
-
return [...this._connectorFrameMap.values()].map(connectorFrameToUpdate);
|
|
95
|
-
}
|
|
96
|
-
/** Compute lifecycle transition between previous and current frame. */
|
|
97
|
-
getLifecycleTransition(elId) {
|
|
98
|
-
const prev = this._prevFrameMap.get(elId);
|
|
99
|
-
const current = this._elementFrameMap.get(elId);
|
|
100
|
-
const from = (prev?.lifecycle ?? "removed");
|
|
101
|
-
const to = (current?.lifecycle ?? "removed");
|
|
102
|
-
if (from === to)
|
|
103
|
-
return null;
|
|
104
|
-
return {
|
|
105
|
-
from,
|
|
106
|
-
to,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/** Compute lifecycle transition between previous and current connector frame. */
|
|
110
|
-
getConnectorLifecycleTransition(connectorId) {
|
|
111
|
-
const prev = this._prevConnectorFrameMap.get(connectorId);
|
|
112
|
-
const current = this._connectorFrameMap.get(connectorId);
|
|
113
|
-
const from = (prev?.lifecycle ?? "removed");
|
|
114
|
-
const to = (current?.lifecycle ?? "removed");
|
|
115
|
-
if (from === to)
|
|
116
|
-
return null;
|
|
117
|
-
return {
|
|
118
|
-
from,
|
|
119
|
-
to,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
getCurrentState() {
|
|
123
|
-
const bundle = this._bundle;
|
|
124
|
-
if (!bundle)
|
|
125
|
-
return null;
|
|
126
|
-
const pair = findSurroundingStops(bundle.scenes, this._progress);
|
|
127
|
-
return pair?.nextStop ?? bundle.scenes[0] ?? null;
|
|
128
|
-
}
|
|
129
|
-
pause() {
|
|
130
|
-
this._paused = true;
|
|
131
|
-
}
|
|
132
|
-
resume() {
|
|
133
|
-
this._paused = false;
|
|
134
|
-
}
|
|
135
|
-
destroy() {
|
|
136
|
-
this._bundle = null;
|
|
137
|
-
this._progress = 0;
|
|
138
|
-
this._paused = false;
|
|
139
|
-
this._elementFrameMap.clear();
|
|
140
|
-
this._prevFrameMap.clear();
|
|
141
|
-
this._connectorFrameMap.clear();
|
|
142
|
-
this._prevConnectorFrameMap.clear();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// ── Interpolation helpers ──────────────────────────────────────────────────
|
|
146
|
-
function resolveFrameMap(bundle, progress) {
|
|
147
|
-
const pair = findSurroundingStops(bundle.scenes, progress);
|
|
148
|
-
const result = new Map();
|
|
149
|
-
if (!pair)
|
|
150
|
-
return result;
|
|
151
|
-
const ids = new Set();
|
|
152
|
-
for (const stop of bundle.scenes) {
|
|
153
|
-
for (const element of stop.elements ?? [])
|
|
154
|
-
ids.add(element.id);
|
|
155
|
-
}
|
|
156
|
-
for (const element of pair.prevStop.elements ?? [])
|
|
157
|
-
ids.add(element.id);
|
|
158
|
-
for (const element of pair.nextStop.elements ?? [])
|
|
159
|
-
ids.add(element.id);
|
|
160
|
-
for (const id of ids) {
|
|
161
|
-
const frame = withRemovedElementGeometry(interpolateElement(id, pair.prevStop, pair.nextStop, pair.t), bundle.scenes, id, progress);
|
|
162
|
-
result.set(id, frame);
|
|
163
|
-
}
|
|
164
|
-
return result;
|
|
165
|
-
}
|
|
166
|
-
function resolveConnectorFrameMap(bundle, progress) {
|
|
167
|
-
const pair = findSurroundingStops(bundle.scenes, progress);
|
|
168
|
-
const result = new Map();
|
|
169
|
-
if (!pair)
|
|
170
|
-
return result;
|
|
171
|
-
const ids = new Set();
|
|
172
|
-
for (const stop of bundle.scenes) {
|
|
173
|
-
for (const connector of stop.connectors ?? [])
|
|
174
|
-
ids.add(connector.id);
|
|
175
|
-
}
|
|
176
|
-
for (const connector of pair.prevStop.connectors ?? [])
|
|
177
|
-
ids.add(connector.id);
|
|
178
|
-
for (const connector of pair.nextStop.connectors ?? [])
|
|
179
|
-
ids.add(connector.id);
|
|
180
|
-
for (const id of ids) {
|
|
181
|
-
const frame = interpolateConnector(id, pair.prevStop, pair.nextStop, pair.t);
|
|
182
|
-
result.set(id, frame);
|
|
183
|
-
}
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
|
-
function findSurroundingStops(stops, progress) {
|
|
187
|
-
if (stops.length === 0)
|
|
188
|
-
return null;
|
|
189
|
-
const sorted = [...stops].sort((a, b) => a.progress - b.progress);
|
|
190
|
-
if (sorted.length === 1 || progress <= sorted[0].progress) {
|
|
191
|
-
return { prevStop: sorted[0], nextStop: sorted[0], t: 0, nextIndex: 0 };
|
|
192
|
-
}
|
|
193
|
-
const lastIndex = sorted.length - 1;
|
|
194
|
-
if (progress >= sorted[lastIndex].progress) {
|
|
195
|
-
return {
|
|
196
|
-
prevStop: sorted[lastIndex - 1] ?? sorted[lastIndex],
|
|
197
|
-
nextStop: sorted[lastIndex],
|
|
198
|
-
t: 1,
|
|
199
|
-
nextIndex: lastIndex,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
203
|
-
const nextStop = sorted[i];
|
|
204
|
-
if (nextStop.progress >= progress) {
|
|
205
|
-
const prevStop = sorted[i - 1];
|
|
206
|
-
const range = nextStop.progress - prevStop.progress;
|
|
207
|
-
const t = range > 0 ? (progress - prevStop.progress) / range : 0;
|
|
208
|
-
return { prevStop, nextStop, t, nextIndex: i };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
function interpolateElement(id, prevStop, nextStop, t) {
|
|
214
|
-
const prev = findElement(prevStop, id);
|
|
215
|
-
const next = findElement(nextStop, id);
|
|
216
|
-
const source = next ?? prev;
|
|
217
|
-
if (!source) {
|
|
218
|
-
return removedFrame(id);
|
|
219
|
-
}
|
|
220
|
-
if (!prev || prev.presence === "removed") {
|
|
221
|
-
return frameFromElement(source, source.presence === "removed" || t < 1 ? "removed" : source.presence);
|
|
222
|
-
}
|
|
223
|
-
if (!next || next.presence === "removed") {
|
|
224
|
-
return frameFromElement(prev, t < 1 ? prev.presence : "removed");
|
|
225
|
-
}
|
|
226
|
-
const lifecycle = t < 1 ? prev.presence : next.presence;
|
|
227
|
-
return {
|
|
228
|
-
id,
|
|
229
|
-
asset: next.asset,
|
|
230
|
-
pos: interpolatePos(prev.pos, next.pos, t),
|
|
231
|
-
size: prev.size + (next.size - prev.size) * t,
|
|
232
|
-
lifecycle,
|
|
233
|
-
ambient: cloneAmbient(next.ambient),
|
|
234
|
-
layer: t < 1 ? prev.layer : next.layer,
|
|
235
|
-
entry: next.enter ?? prev.enter,
|
|
236
|
-
exit: next.exit ?? prev.exit,
|
|
237
|
-
text: cloneText(next.text ?? prev.text),
|
|
238
|
-
primitive: clonePrimitive(next.primitive ?? prev.primitive),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
function interpolateConnector(id, prevStop, nextStop, t) {
|
|
242
|
-
const prev = findConnector(prevStop, id);
|
|
243
|
-
const next = findConnector(nextStop, id);
|
|
244
|
-
const source = next ?? prev;
|
|
245
|
-
if (!source)
|
|
246
|
-
return removedConnectorFrame(id);
|
|
247
|
-
if (!prev || prev.presence === "removed") {
|
|
248
|
-
return frameFromConnector(source, source.presence === "removed" || t < 1 ? "removed" : source.presence);
|
|
249
|
-
}
|
|
250
|
-
if (!next || next.presence === "removed") {
|
|
251
|
-
return frameFromConnector(prev, t < 1 ? prev.presence : "removed");
|
|
252
|
-
}
|
|
253
|
-
const lifecycle = t < 1 ? prev.presence : next.presence;
|
|
254
|
-
return {
|
|
255
|
-
id,
|
|
256
|
-
route: interpolateRoute(prev.route, next.route, t),
|
|
257
|
-
layer: t < 1 ? prev.layer : next.layer,
|
|
258
|
-
lifecycle,
|
|
259
|
-
style: cloneConnectorStyle(next.style),
|
|
260
|
-
start: next.start,
|
|
261
|
-
end: next.end,
|
|
262
|
-
direction: next.direction,
|
|
263
|
-
ambient: cloneAmbient(next.ambient),
|
|
264
|
-
entry: next.enter ?? prev.enter,
|
|
265
|
-
exit: next.exit ?? prev.exit,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
function findElement(stop, id) {
|
|
269
|
-
return (stop.elements ?? []).find((element) => element.id === id);
|
|
270
|
-
}
|
|
271
|
-
function findConnector(stop, id) {
|
|
272
|
-
return (stop.connectors ?? []).find((connector) => connector.id === id);
|
|
273
|
-
}
|
|
274
|
-
function withRemovedElementGeometry(frame, stops, id, progress) {
|
|
275
|
-
if (frame.lifecycle !== "removed")
|
|
276
|
-
return frame;
|
|
277
|
-
const reference = findNearestElementGeometry(stops, id, progress);
|
|
278
|
-
if (!reference)
|
|
279
|
-
return frame;
|
|
280
|
-
return {
|
|
281
|
-
...frame,
|
|
282
|
-
asset: reference.asset,
|
|
283
|
-
pos: [...reference.pos],
|
|
284
|
-
size: reference.size,
|
|
285
|
-
ambient: cloneAmbient(reference.ambient),
|
|
286
|
-
layer: reference.layer,
|
|
287
|
-
entry: reference.enter,
|
|
288
|
-
exit: reference.exit,
|
|
289
|
-
text: cloneText(reference.text),
|
|
290
|
-
primitive: clonePrimitive(reference.primitive),
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function findNearestElementGeometry(stops, id, progress) {
|
|
294
|
-
const sorted = [...stops].sort((a, b) => a.progress - b.progress);
|
|
295
|
-
const next = sorted
|
|
296
|
-
.filter((stop) => stop.progress >= progress)
|
|
297
|
-
.flatMap((stop) => stop.elements ?? [])
|
|
298
|
-
.find((element) => element.id === id && element.presence !== "removed");
|
|
299
|
-
if (next)
|
|
300
|
-
return next;
|
|
301
|
-
for (let index = sorted.length - 1; index >= 0; index -= 1) {
|
|
302
|
-
if (sorted[index].progress > progress)
|
|
303
|
-
continue;
|
|
304
|
-
const previous = (sorted[index].elements ?? []).find((element) => element.id === id && element.presence !== "removed");
|
|
305
|
-
if (previous)
|
|
306
|
-
return previous;
|
|
307
|
-
}
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
function frameFromElement(element, lifecycle = element.presence) {
|
|
311
|
-
return {
|
|
312
|
-
id: element.id,
|
|
313
|
-
asset: element.asset,
|
|
314
|
-
pos: [...element.pos],
|
|
315
|
-
size: element.size,
|
|
316
|
-
lifecycle,
|
|
317
|
-
ambient: cloneAmbient(element.ambient),
|
|
318
|
-
layer: element.layer,
|
|
319
|
-
entry: element.enter,
|
|
320
|
-
exit: element.exit,
|
|
321
|
-
text: cloneText(element.text),
|
|
322
|
-
primitive: clonePrimitive(element.primitive),
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
function frameFromConnector(connector, lifecycle = connector.presence) {
|
|
326
|
-
return {
|
|
327
|
-
id: connector.id,
|
|
328
|
-
route: cloneRoute(connector.route),
|
|
329
|
-
layer: connector.layer,
|
|
330
|
-
lifecycle,
|
|
331
|
-
style: cloneConnectorStyle(connector.style),
|
|
332
|
-
start: connector.start,
|
|
333
|
-
end: connector.end,
|
|
334
|
-
direction: connector.direction,
|
|
335
|
-
ambient: cloneAmbient(connector.ambient),
|
|
336
|
-
entry: connector.enter,
|
|
337
|
-
exit: connector.exit,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
function removedFrame(id) {
|
|
341
|
-
return {
|
|
342
|
-
id,
|
|
343
|
-
asset: "",
|
|
344
|
-
pos: [0, 0],
|
|
345
|
-
size: 1,
|
|
346
|
-
lifecycle: "removed",
|
|
347
|
-
ambient: [],
|
|
348
|
-
layer: "",
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
function removedConnectorFrame(id) {
|
|
352
|
-
return {
|
|
353
|
-
id,
|
|
354
|
-
route: [],
|
|
355
|
-
layer: "",
|
|
356
|
-
lifecycle: "removed",
|
|
357
|
-
style: {
|
|
358
|
-
variant: "line",
|
|
359
|
-
pattern: "solid",
|
|
360
|
-
stroke: "#2563eb",
|
|
361
|
-
strokeWidth: 3,
|
|
362
|
-
opacity: 1,
|
|
363
|
-
outlineWidth: 0,
|
|
364
|
-
lane: "none",
|
|
365
|
-
},
|
|
366
|
-
start: "none",
|
|
367
|
-
end: "none",
|
|
368
|
-
direction: "route",
|
|
369
|
-
ambient: [],
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
function interpolatePos(prev, next, t) {
|
|
373
|
-
return [prev[0] + (next[0] - prev[0]) * t, prev[1] + (next[1] - prev[1]) * t];
|
|
374
|
-
}
|
|
375
|
-
function interpolateRoute(prev, next, t) {
|
|
376
|
-
if (prev.length !== next.length)
|
|
377
|
-
return cloneRoute(t < 1 ? prev : next);
|
|
378
|
-
return prev.map((point, index) => interpolatePos(point, next[index], t));
|
|
379
|
-
}
|
|
380
|
-
function cloneRoute(route) {
|
|
381
|
-
return route.map((point) => [point[0], point[1]]);
|
|
382
|
-
}
|
|
383
|
-
function cloneConnectorStyle(style) {
|
|
384
|
-
return {
|
|
385
|
-
...style,
|
|
386
|
-
...(style.dash ? { dash: [...style.dash] } : {}),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
function cloneAmbient(ambient) {
|
|
390
|
-
return (ambient ?? []).map((item) => ({ ...item }));
|
|
391
|
-
}
|
|
392
|
-
function frameToUpdate(frame) {
|
|
393
|
-
return {
|
|
394
|
-
id: frame.id,
|
|
395
|
-
asset: frame.asset,
|
|
396
|
-
lifecycle: frame.lifecycle,
|
|
397
|
-
ambient: cloneAmbient(frame.ambient),
|
|
398
|
-
pos: [...frame.pos],
|
|
399
|
-
size: frame.size,
|
|
400
|
-
layer: frame.layer,
|
|
401
|
-
entry: frame.entry,
|
|
402
|
-
exit: frame.exit,
|
|
403
|
-
text: cloneText(frame.text),
|
|
404
|
-
primitive: clonePrimitive(frame.primitive),
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
function connectorFrameToUpdate(frame) {
|
|
408
|
-
return {
|
|
409
|
-
id: frame.id,
|
|
410
|
-
route: cloneRoute(frame.route),
|
|
411
|
-
layer: frame.layer,
|
|
412
|
-
lifecycle: frame.lifecycle,
|
|
413
|
-
style: cloneConnectorStyle(frame.style),
|
|
414
|
-
start: frame.start,
|
|
415
|
-
end: frame.end,
|
|
416
|
-
direction: frame.direction,
|
|
417
|
-
ambient: cloneAmbient(frame.ambient),
|
|
418
|
-
entry: frame.entry,
|
|
419
|
-
exit: frame.exit,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
function cloneFrameMap(map) {
|
|
423
|
-
const clone = new Map();
|
|
424
|
-
for (const [id, frame] of map) {
|
|
425
|
-
clone.set(id, {
|
|
426
|
-
...frame,
|
|
427
|
-
pos: [...frame.pos],
|
|
428
|
-
ambient: cloneAmbient(frame.ambient),
|
|
429
|
-
text: cloneText(frame.text),
|
|
430
|
-
primitive: clonePrimitive(frame.primitive),
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
return clone;
|
|
434
|
-
}
|
|
435
|
-
function cloneText(text) {
|
|
436
|
-
return text ? { ...text } : undefined;
|
|
437
|
-
}
|
|
438
|
-
function clonePrimitive(primitive) {
|
|
439
|
-
if (!primitive)
|
|
440
|
-
return undefined;
|
|
441
|
-
return {
|
|
442
|
-
...(primitive.rectangle
|
|
443
|
-
? {
|
|
444
|
-
rectangle: {
|
|
445
|
-
...primitive.rectangle,
|
|
446
|
-
dash: cloneDash(primitive.rectangle.dash),
|
|
447
|
-
},
|
|
448
|
-
}
|
|
449
|
-
: {}),
|
|
450
|
-
...(primitive.circle
|
|
451
|
-
? {
|
|
452
|
-
circle: {
|
|
453
|
-
...primitive.circle,
|
|
454
|
-
dash: cloneDash(primitive.circle.dash),
|
|
455
|
-
},
|
|
456
|
-
}
|
|
457
|
-
: {}),
|
|
458
|
-
...(primitive.polygon
|
|
459
|
-
? {
|
|
460
|
-
polygon: {
|
|
461
|
-
...primitive.polygon,
|
|
462
|
-
dash: cloneDash(primitive.polygon.dash),
|
|
463
|
-
points: cloneRoute(primitive.polygon.points),
|
|
464
|
-
},
|
|
465
|
-
}
|
|
466
|
-
: {}),
|
|
467
|
-
...(primitive.line
|
|
468
|
-
? {
|
|
469
|
-
line: {
|
|
470
|
-
...primitive.line,
|
|
471
|
-
dash: cloneDash(primitive.line.dash),
|
|
472
|
-
points: cloneRoute(primitive.line.points),
|
|
473
|
-
},
|
|
474
|
-
}
|
|
475
|
-
: {}),
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
function cloneDash(dash) {
|
|
479
|
-
return dash ? [dash[0], dash[1]] : undefined;
|
|
480
|
-
}
|
|
481
|
-
function cloneConnectorFrameMap(map) {
|
|
482
|
-
const clone = new Map();
|
|
483
|
-
for (const [id, frame] of map) {
|
|
484
|
-
clone.set(id, {
|
|
485
|
-
...frame,
|
|
486
|
-
route: cloneRoute(frame.route),
|
|
487
|
-
style: cloneConnectorStyle(frame.style),
|
|
488
|
-
ambient: cloneAmbient(frame.ambient),
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
return clone;
|
|
492
|
-
}
|
|
1
|
+
class y{_bundle=null;_progress=0;_paused=!1;_prevFrameMap=new Map;_elementFrameMap=new Map;_prevConnectorFrameMap=new Map;_connectorFrameMap=new Map;get bundle(){return this._bundle}get progress(){return this._progress}getProgress(){return this._progress}get paused(){return this._paused}isPaused(){return this._paused}get elementsCount(){return this._elementFrameMap.size}get connectorsCount(){return this._connectorFrameMap.size}init($){this._bundle=$,this._progress=0,this._paused=!1,this._prevFrameMap.clear(),this._elementFrameMap.clear(),this._prevConnectorFrameMap.clear(),this._connectorFrameMap.clear();let J=M0($,0);this._elementFrameMap=J,this._prevFrameMap=T0(J);let K=X0($,0);this._connectorFrameMap=K,this._prevConnectorFrameMap=w0(K)}setProgress($){let J=Math.max(0,Math.min(1,$));if(this._paused)return;if(this._progress=J,!this._bundle)return;this._prevFrameMap=T0(this._elementFrameMap),this._elementFrameMap=M0(this._bundle,J),this._prevConnectorFrameMap=w0(this._connectorFrameMap),this._connectorFrameMap=X0(this._bundle,J)}getElementUpdate($){let J=typeof $==="string"?$:$.id,K=this._elementFrameMap.get(J);if(!K)return{id:J,asset:"",lifecycle:"removed",ambient:[],pos:[0,0],size:1,layer:""};return N0(K)}getFrameUpdates(){return[...this._elementFrameMap.values()].map(N0)}getConnectorUpdate($){let J=typeof $==="string"?$:$.id,K=this._connectorFrameMap.get(J);if(!K)return d(R0(J));return d(K)}getConnectorFrameUpdates(){return[...this._connectorFrameMap.values()].map(d)}getLifecycleTransition($){let J=this._prevFrameMap.get($),K=this._elementFrameMap.get($),Z=J?.lifecycle??"removed",k=K?.lifecycle??"removed";if(Z===k)return null;return{from:Z,to:k}}getConnectorLifecycleTransition($){let J=this._prevConnectorFrameMap.get($),K=this._connectorFrameMap.get($),Z=J?.lifecycle??"removed",k=K?.lifecycle??"removed";if(Z===k)return null;return{from:Z,to:k}}getCurrentState(){let $=this._bundle;if(!$)return null;return o($.scenes,this._progress)?.nextStop??$.scenes[0]??null}pause(){this._paused=!0}resume(){this._paused=!1}destroy(){this._bundle=null,this._progress=0,this._paused=!1,this._elementFrameMap.clear(),this._prevFrameMap.clear(),this._connectorFrameMap.clear(),this._prevConnectorFrameMap.clear()}}function M0($,J){let K=o($.scenes,J),Z=new Map;if(!K)return Z;let k=new Set;for(let q of $.scenes)for(let Q of q.elements??[])k.add(Q.id);for(let q of K.prevStop.elements??[])k.add(q.id);for(let q of K.nextStop.elements??[])k.add(q.id);for(let q of k){let Q=G1(U1(q,K.prevStop,K.nextStop,K.t),$.scenes,q,J);Z.set(q,Q)}return Z}function X0($,J){let K=o($.scenes,J),Z=new Map;if(!K)return Z;let k=new Set;for(let q of $.scenes)for(let Q of q.connectors??[])k.add(Q.id);for(let q of K.prevStop.connectors??[])k.add(q.id);for(let q of K.nextStop.connectors??[])k.add(q.id);for(let q of k){let Q=V1(q,K.prevStop,K.nextStop,K.t);Z.set(q,Q)}return Z}function o($,J){if($.length===0)return null;let K=[...$].sort((k,q)=>k.progress-q.progress);if(K.length===1||J<=K[0].progress)return{prevStop:K[0],nextStop:K[0],t:0,nextIndex:0};let Z=K.length-1;if(J>=K[Z].progress)return{prevStop:K[Z-1]??K[Z],nextStop:K[Z],t:1,nextIndex:Z};for(let k=1;k<K.length;k++){let q=K[k];if(q.progress>=J){let Q=K[k-1],H=q.progress-Q.progress,L=H>0?(J-Q.progress)/H:0;return{prevStop:Q,nextStop:q,t:L,nextIndex:k}}}return null}function U1($,J,K,Z){let k=P0(J,$),q=P0(K,$),Q=q??k;if(!Q)return O1($);if(!k||k.presence==="removed")return F0(Q,Q.presence==="removed"||Z<1?"removed":Q.presence);if(!q||q.presence==="removed")return F0(k,Z<1?k.presence:"removed");let H=Z<1?k.presence:q.presence;return{id:$,asset:q.asset,pos:z0(k.pos,q.pos,Z),size:k.size+(q.size-k.size)*Z,lifecycle:H,ambient:w(q.ambient),layer:Z<1?k.layer:q.layer,entry:q.enter??k.enter,exit:q.exit??k.exit,text:f(q.text??k.text),primitive:v(q.primitive??k.primitive)}}function V1($,J,K,Z){let k=_0(J,$),q=_0(K,$),Q=q??k;if(!Q)return R0($);if(!k||k.presence==="removed")return A0(Q,Q.presence==="removed"||Z<1?"removed":Q.presence);if(!q||q.presence==="removed")return A0(k,Z<1?k.presence:"removed");let H=Z<1?k.presence:q.presence;return{id:$,route:Y1(k.route,q.route,Z),layer:Z<1?k.layer:q.layer,lifecycle:H,style:p(Z<1?k.style:q.style),start:q.start,end:q.end,direction:q.direction,ambient:w(q.ambient),entry:q.enter??k.enter,exit:q.exit??k.exit}}function P0($,J){return($.elements??[]).find((K)=>K.id===J)}function _0($,J){return($.connectors??[]).find((K)=>K.id===J)}function G1($,J,K,Z){if($.lifecycle!=="removed")return $;let k=j1(J,K,Z);if(!k)return $;return{...$,asset:k.asset,pos:[...k.pos],size:k.size,ambient:w(k.ambient),layer:k.layer,entry:k.enter,exit:k.exit,text:f(k.text),primitive:v(k.primitive)}}function j1($,J,K){let Z=[...$].sort((q,Q)=>q.progress-Q.progress),k=Z.filter((q)=>q.progress>=K).flatMap((q)=>q.elements??[]).find((q)=>q.id===J&&q.presence!=="removed");if(k)return k;for(let q=Z.length-1;q>=0;q-=1){if(Z[q].progress>K)continue;let Q=(Z[q].elements??[]).find((H)=>H.id===J&&H.presence!=="removed");if(Q)return Q}return}function F0($,J=$.presence){return{id:$.id,asset:$.asset,pos:[...$.pos],size:$.size,lifecycle:J,ambient:w($.ambient),layer:$.layer,entry:$.enter,exit:$.exit,text:f($.text),primitive:v($.primitive)}}function A0($,J=$.presence){return{id:$.id,route:x($.route),layer:$.layer,lifecycle:J,style:p($.style),start:$.start,end:$.end,direction:$.direction,ambient:w($.ambient),entry:$.enter,exit:$.exit}}function O1($){return{id:$,asset:"",pos:[0,0],size:1,lifecycle:"removed",ambient:[],layer:""}}function R0($){return{id:$,route:[],layer:"",lifecycle:"removed",style:{variant:"line",pattern:"solid",stroke:"#2563eb",strokeWidth:3,opacity:1,outlineWidth:0,lane:"none"},start:"none",end:"none",direction:"route",ambient:[]}}function z0($,J,K){return[$[0]+(J[0]-$[0])*K,$[1]+(J[1]-$[1])*K]}function Y1($,J,K){if($.length!==J.length)return x(K<1?$:J);return $.map((Z,k)=>z0(Z,J[k],K))}function x($){return $.map((J)=>[J[0],J[1]])}function p($){return{...$,...$.dash?{dash:[...$.dash]}:{}}}function w($){return($??[]).map((J)=>({...J}))}function N0($){return{id:$.id,asset:$.asset,lifecycle:$.lifecycle,ambient:w($.ambient),pos:[...$.pos],size:$.size,layer:$.layer,entry:$.entry,exit:$.exit,text:f($.text),primitive:v($.primitive)}}function d($){return{id:$.id,route:x($.route),layer:$.layer,lifecycle:$.lifecycle,style:p($.style),start:$.start,end:$.end,direction:$.direction,ambient:w($.ambient),entry:$.entry,exit:$.exit}}function T0($){let J=new Map;for(let[K,Z]of $)J.set(K,{...Z,pos:[...Z.pos],ambient:w(Z.ambient),text:f(Z.text),primitive:v(Z.primitive)});return J}function f($){return $?{...$}:void 0}function v($){if(!$)return;return{...$.rectangle?{rectangle:{...$.rectangle,dash:c($.rectangle.dash)}}:{},...$.circle?{circle:{...$.circle,dash:c($.circle.dash)}}:{},...$.polygon?{polygon:{...$.polygon,dash:c($.polygon.dash),points:x($.polygon.points)}}:{},...$.line?{line:{...$.line,dash:c($.line.dash),points:x($.line.points)}}:{}}}function c($){return $?[$[0],$[1]]:void 0}function w0($){let J=new Map;for(let[K,Z]of $)J.set(K,{...Z,route:x(Z.route),style:p(Z.style),ambient:w(Z.ambient)});return J}var M1={light:{"--color-top":"#e2e8f0","--color-front":"#94a3b8","--color-side":"#64748b","--color-back":"#475569","--color-leaf":"#15803d","--color-trunk":"#78350f","--color-accent":"#3b82f6"},dark:{"--color-top":"#334155","--color-front":"#1e293b","--color-side":"#0f172a","--color-back":"#020617","--color-leaf":"#166534","--color-trunk":"#451a03","--color-accent":"#60a5fa"},brand:{"--color-top":"#c7d2fe","--color-front":"#818cf8","--color-side":"#6366f1","--color-back":"#4338ca","--color-leaf":"#22c55e","--color-trunk":"#854d0e","--color-accent":"#f59e0b"}};function u($){return M1[$]}class n extends Error{code;details;constructor($,J,K){super(J);this.code=$;this.details=K;this.name=this.constructor.name}}class G extends n{}class O extends n{}function D($,J,K){return{rawX:K*($-J)*0.5,rawY:K*($+J)*0.25}}function h($,J,K,Z=0,k=0,q=0,Q=0){let{rawX:H,rawY:L}=D($,J,K);return{screenX:H-Z+q,screenY:L-k+Q}}function s($,J){return J*$}function C0(){return["@keyframes iso-anim-fade-in{from{opacity:0}to{opacity:1}}","@keyframes iso-anim-fade-in-grow{from{opacity:0;scale:.01}to{opacity:1;scale:1}}","@keyframes iso-anim-fall-in{from{opacity:0;translate:0 -40px}to{opacity:1;translate:0 0}}","@keyframes iso-anim-rise-from-ground{from{opacity:0;translate:0 40px}to{opacity:1;translate:0 0}}","@keyframes iso-anim-slide-in-left{from{opacity:0;translate:-60px 0}to{opacity:1;translate:0 0}}","@keyframes iso-anim-slide-in-right{from{opacity:0;translate:60px 0}to{opacity:1;translate:0 0}}","@keyframes iso-anim-flip-in{from{opacity:1;scale:0 1}to{opacity:1;scale:1 1}}","@keyframes iso-anim-fade-out{from{opacity:1}to{opacity:0}}","@keyframes iso-anim-fade-out-shrink{from{opacity:1;scale:1}to{opacity:0;scale:.01}}","@keyframes iso-anim-fall-through-ground{from{opacity:1;translate:0 0}to{opacity:0;translate:0 40px}}","@keyframes iso-anim-rise-away{from{opacity:1;translate:0 0}to{opacity:0;translate:0 -40px}}","@keyframes iso-anim-slide-out-left{from{opacity:1;translate:0 0}to{opacity:0;translate:-60px 0}}","@keyframes iso-anim-slide-out-right{from{opacity:1;translate:0 0}to{opacity:0;translate:60px 0}}","@keyframes iso-anim-flip-out{from{opacity:1;scale:1 1}to{opacity:0;scale:0 1}}","@keyframes iso-anim-pulse{0%,100%{opacity:.7}50%{opacity:1}}","@keyframes iso-anim-float{0%,100%{translate:0 0}50%{translate:0 -6px}}","@keyframes iso-anim-shake{0%,25%,75%,100%{translate:0 0}50%{translate:3px 0}}","@keyframes iso-anim-glow{0%,100%{filter:drop-shadow(0 0 2px rgba(255,255,255,.5))}50%{filter:drop-shadow(0 0 8px rgba(255,255,255,.9))}}","@keyframes iso-anim-spin{from{rotate:0deg}to{rotate:360deg}}","@keyframes iso-anim-blink{0%,49%{opacity:1}50%,100%{opacity:0}}","@keyframes iso-connector-flow-route{from{stroke-dashoffset:0}to{stroke-dashoffset:calc(-1px * var(--iso-flow-distance,20))}}","@keyframes iso-connector-flow-reverse{from{stroke-dashoffset:0}to{stroke-dashoffset:calc(1px * var(--iso-flow-distance,20))}}",".iso-connector-pattern-dashed .iso-connector-shaft,.iso-connector-pattern-dotted .iso-connector-shaft{--iso-flow-distance:20}",".iso-connector-direction-route .iso-connector-shaft.iso-ambient-flow{animation:iso-connector-flow-route 900ms linear infinite}",".iso-connector-direction-reverse .iso-connector-shaft.iso-ambient-flow{animation:iso-connector-flow-reverse 900ms linear infinite}","@media (prefers-reduced-motion: reduce){.iso-element,.iso-connector{animation-duration:1ms!important}.iso-ambient-pulse,.iso-ambient-float,.iso-ambient-shake,.iso-ambient-glow,.iso-ambient-spin,.iso-ambient-blink,.iso-ambient-flow{animation:none!important}}"].join(`
|
|
2
|
+
`)}var R="http://www.w3.org/2000/svg";function S0($){let J=new Map,K=$?.assets;if(K&&typeof K==="object"){for(let[Z,k]of Object.entries(K))if(k&&typeof k==="object"&&typeof k.url==="string")J.set(Z,{url:k.url,anchor:k.anchor})}return(Z)=>J.get(Z)}function I0($,J,K){return N1($.url,J,K,$.anchor)}function t($,J,K){if(!$?.value)throw new G("TEXT_CONTENT_MISSING",`Text content is missing for built-in asset: ${J}`,{asset:J});let Z=document.createElementNS(R,"g"),k=document.createElementNS(R,"text"),q=$.align??"middle",Q=$.fontSize??12,H=$.lineHeight??1.2,L=X1(q,K);k.setAttribute("x",String(L)),k.setAttribute("y",String(-K)),k.setAttribute("text-anchor",q),k.setAttribute("dominant-baseline","text-before-edge"),k.setAttribute("font-family","Arial, Helvetica, sans-serif"),k.setAttribute("font-size",String(Q)),k.setAttribute("font-weight",String($.fontWeight??700)),k.setAttribute("fill",$.fill??"currentColor");let W=P1($.value);for(let[j,M]of W.entries()){let X=document.createElementNS(R,"tspan");X.setAttribute("x",String(L)),X.setAttribute("dy",j===0?"0":String(Q*H)),X.textContent=M,k.appendChild(X)}return Z.appendChild(k),Z}function e($,J,K){let Z=document.createElementNS(R,"g");switch($){case"rectangle":return D0(Z,_1(),J?.rectangle,K),Z;case"polygon":return D0(Z,J?.polygon?.points,J?.polygon,K),Z;case"line":return F1(Z,J?.line,J?.line,K),Z;case"circle":return A1(Z,J?.circle,K),Z;default:throw new G("PRIMITIVE_ASSET_UNKNOWN",`Unknown built-in primitive asset: ${$}`,{asset:$})}}function X1($,J){if($==="start")return-J/2;if($==="end")return J/2;return 0}function P1($){return $.replace(/\r\n?/g,`
|
|
3
|
+
`).replace(/\n$/,"").split(`
|
|
4
|
+
`)}function _1(){return[[0,0],[1,0],[1,1],[0,1]]}function D0($,J,K,Z){if(!J)return;let k=document.createElementNS(R,"polygon");k.setAttribute("points",J.map((q)=>$0(q,Z)).join(" ")),J0(k,K,{fill:"currentColor",stroke:"none"}),$.appendChild(k)}function F1($,J,K,Z){if(!J?.points)return;let k=document.createElementNS(R,"polyline");k.setAttribute("points",J.points.map((q)=>$0(q,Z)).join(" ")),k.setAttribute("fill","none"),k.setAttribute("stroke-linecap",J.lineCap??"round"),k.setAttribute("stroke-linejoin",J.lineJoin??"round"),J0(k,K,{fill:"none",stroke:"currentColor"}),$.appendChild(k)}function A1($,J,K){let Z=$0([0.5,0.5],K),k=document.createElementNS(R,"circle"),[q,Q]=Z.split(",").map(Number);k.setAttribute("cx",String(q)),k.setAttribute("cy",String(Q)),k.setAttribute("r",String(K*0.2)),J0(k,J,{fill:"currentColor",stroke:"none"}),$.appendChild(k)}function $0($,J){let K=D($[0],$[1],J),Z=D(1,1,J);return`${K.rawX-Z.rawX},${K.rawY-Z.rawY}`}function J0($,J,K){if($.setAttribute("fill",J?.fill??K.fill),$.setAttribute("stroke",J?.stroke??K.stroke),$.setAttribute("stroke-width",String(J?.strokeWidth??0)),$.setAttribute("opacity",String(J?.opacity??1)),J?.dash)$.setAttribute("stroke-dasharray",J.dash.join(" "))}function N1($,J,K,Z){if(!w1($))throw new G("INVALID_ASSET_URL",`Asset URL is unsafe: ${J}`,{asset:J});let k=document.createElementNS(R,"g"),q=document.createElementNS(R,"image"),Q=T1($),[H,L]=Z??[0.5,1];return q.setAttribute("href",Q),q.setAttributeNS("http://www.w3.org/1999/xlink","href",Q),q.setAttribute("x",String(-K*H)),q.setAttribute("y",String(-K*L)),q.setAttribute("width",String(K)),q.setAttribute("height",String(K)),q.setAttribute("preserveAspectRatio","xMidYMax meet"),k.appendChild(q),k}function T1($){try{let J=document.baseURI;return typeof J==="string"?new URL($,J).href:$}catch{return $}}function w1($){let J=$.trim().toLowerCase();return J.length>0&&!J.startsWith("javascript:")}var R1=/^--[a-zA-Z0-9-_]+$/;function h0($,J){for(let[K,Z]of Object.entries(J)){if(!R1.test(K))throw new G("INVALID_THEME_VAR",`Invalid CSS custom property name: ${K}`);$.style.setProperty(K,Z)}}var Y="http://www.w3.org/2000/svg",b0="text",z1=new Set(["rectangle","circle","polygon","line"]),C1={dashed:[12,8],dotted:[0,8]},q0=0.14,Z0=0.35,D1=0.28,f0=0.4;function v0($,J,K){let Z=W0(J),k=J.scenes[0],q=c1(J),Q=p1(J),H=new Map((k?.elements??[]).map((V)=>[V.id,V])),L=new Map((k?.connectors??[]).map((V)=>[V.id,V])),W=y1(Z,K?.label,J.className),j=S0(J);W.appendChild(g1()),h0(W,{...u(J.theme)??{},...J.themeVars??{},...K?.themeVars??{}});let M=v1(J.layers),X=f1(M);if(J.floor.visible)W.appendChild(b1(J,Z));let T=new Map;for(let V of Q){let E=L.get(V.id)??{...V,presence:"removed"},_=i1(E,Z);if(E.presence==="removed")_.isHidden=!0,z(_.node);t0(_,E.ambient??[]),W.appendChild(_.node),T.set(V.id,_)}let C=document.createElementNS(Y,"g");C.classList.add("iso-depth-layer"),C.setAttribute("data-layer","depth"),W.appendChild(C);let U=document.createElementNS(Y,"g");U.classList.add("iso-layer","iso-layer-labels"),U.setAttribute("data-layer","labels"),W.appendChild(U);let F=new Map,b=m1(q);for(let V of b){if(!X.get(V.layer))throw new G("MISSING_LAYER",`Unknown layer for "${V.id}": ${V.layer}`);let _=H.get(V.id)??{...V,presence:"removed"},A=u1(_,Z,j);if(A.anchor=s0(J,_),A.node.classList.add(`iso-layer-${V.layer}`),A.node.setAttribute("data-layer",V.layer),_.presence==="removed")A.isHidden=!0,z(A.node);n0(A,_.ambient??[]),(l(V.asset)?U:C).appendChild(A.node),F.set(V.id,A)}return W._layerMap=X,W._elementMap=F,W._connectorMap=T,$.appendChild(W),W}function m0($,J,K=[]){let Z=$._layout;if(!Z)return;let k=$._elementMap;if(k)for(let Q of J){let H=k.get(Q.id);if(!H)continue;H.current=Q,l1(H.node,Q,Z),o0(H.node,Q,Z),n0(H,Q.ambient??[])}let q=$._connectorMap;if(!q)return;for(let Q of K){let H=q.get(Q.id);if(!H)continue;r0(H,Q,Z)}}function c0($,J){return $._elementMap?.get(J)}function p0($,J){return $._connectorMap?.get(J)}function S($){return W0($).viewBox}function Q0($,J){let K=$._layout,Z=$._elementMap?.get(J);if(!K||!Z||Z.current.presence==="removed")return;return i0(h1(Z.current,K,Z.anchor))}function H0($,J){let K=W0($),[Z,k]=J.at,[q,Q]=J.size,H=i();for(let L of[[Z,k],[Z+q,k],[Z,k+Q],[Z+q,k+Q]]){let W=h(L[0],L[1],K.cellSize,K.selectedBounds.minX,K.selectedBounds.minY,K.padding.x,K.padding.y);H=L0(H,W.screenX,W.screenY)}return i0(H)}function u0($,J){$.setAttribute("viewBox",`${J.minX} ${J.minY} ${J.width} ${J.height}`)}function z($){$.style.visibility="hidden",$.style.pointerEvents="none"}function m($){$.style.visibility="visible",$.style.pointerEvents="auto"}function W0($){let J=$.grid.cellSize,K=$.layout.padding,Z=S1($,J),k=E1($,J),q=x1($.layout.bounds,Z,k),Q=q.maxX-q.minX+K.x*2,H=q.maxY-q.minY+K.y*2,L={minX:0,minY:0,width:g(Q||J),height:g(H||J)};return{cellSize:J,padding:K,contentBounds:Z,floorBounds:k,selectedBounds:q,viewBox:L}}function S1($,J){let K=i();for(let Z of $.scenes){for(let k of Z.elements??[]){if(k.presence==="removed")continue;K=k0(K,I1($,k,J))}for(let k of Z.connectors??[]){if(k.presence==="removed")continue;K=k0(K,B1(k,J))}}return l0(K,J)}function I1($,J,K){let{rawX:Z,rawY:k}=D(J.pos[0]+J.size,J.pos[1]+J.size,K),q=s(J.size,K),[Q,H]=s0($,J);return{minX:Z-q*Q,minY:k-q*H,maxX:Z+q*(1-Q),maxY:k+q*(1-H)}}function h1($,J,K){let Z=h($.pos[0]+$.size,$.pos[1]+$.size,J.cellSize,J.selectedBounds.minX,J.selectedBounds.minY,J.padding.x,J.padding.y),k=s($.size,J.cellSize);return{minX:Z.screenX-k*K[0],minY:Z.screenY-k*K[1],maxX:Z.screenX+k*(1-K[0]),maxY:Z.screenY+k*(1-K[1])}}function s0($,J){return $.assets?.[J.asset]?.anchor??[0.5,1]}function B1($,J){let K=i();for(let[Q,H]of $.route){let{rawX:L,rawY:W}=D(Q,H,J);K=L0(K,L,W)}let Z=Math.max(Z0,f0,q0*2)*J,k=$.style.strokeWidth/2+($.style.outlineWidth??0),q=Z+k;return{minX:K.minX-q,minY:K.minY-q,maxX:K.maxX+q,maxY:K.maxY+q}}function E1($,J){let K=$.floor.origin,[Z,k]=$.floor.size,q=[K,[K[0]+Z,K[1]],[K[0],K[1]+k],[K[0]+Z,K[1]+k]],Q=i();for(let[H,L]of q){let{rawX:W,rawY:j}=D(H,L,J);Q=L0(Q,W,j)}return l0(Q,J)}function x1($,J,K){if($==="content")return J;if($==="floor")return K;return k0(J,K)}function i(){return{minX:Number.POSITIVE_INFINITY,minY:Number.POSITIVE_INFINITY,maxX:Number.NEGATIVE_INFINITY,maxY:Number.NEGATIVE_INFINITY}}function l0($,J){if(Number.isFinite($.minX))return $;return{minX:0,minY:0,maxX:J,maxY:J}}function i0($){let J=($.minX+$.maxX)/2,K=($.minY+$.maxY)/2,Z=Math.max(1,$.maxX-$.minX),k=Math.max(1,$.maxY-$.minY);return{minX:g(J-Z/2),minY:g(K-k/2),width:g(Z),height:g(k)}}function L0($,J,K){return{minX:Math.min($.minX,J),minY:Math.min($.minY,K),maxX:Math.max($.maxX,J),maxY:Math.max($.maxY,K)}}function k0($,J){return{minX:Math.min($.minX,J.minX),minY:Math.min($.minY,J.minY),maxX:Math.max($.maxX,J.maxX),maxY:Math.max($.maxY,J.maxY)}}function g($){return Math.round($*1000)/1000}function y1($,J,K){let Z=document.createElementNS(Y,"svg");Z.classList.add("iso-scene");for(let k of K?.trim().split(/\s+/)??[])if(k)Z.classList.add(k);if(Z.setAttribute("width","100%"),Z.setAttribute("height","100%"),Z.setAttribute("viewBox",`${$.viewBox.minX} ${$.viewBox.minY} ${$.viewBox.width} ${$.viewBox.height}`),Z.setAttribute("preserveAspectRatio","xMidYMid meet"),Z.style.width="100%",Z.style.height="100%",Z.style.display="block",J)Z.setAttribute("role","img"),Z.setAttribute("aria-label",J);else Z.setAttribute("aria-hidden","true");return Z._layout=$,Z._viewBoxW=$.viewBox.width,Z._viewBoxH=$.viewBox.height,Z}function g1(){let $=document.createElementNS(Y,"style");return $.textContent=C0(),$}function b1($,J){let K=document.createElementNS(Y,"g");K.classList.add("iso-floor-grid",`iso-layer-${$.floor.layer}`),K.setAttribute("data-layer",$.floor.layer);let[Z,k]=$.floor.origin,[q,Q]=$.floor.size,H=[P(Z,k,J),P(Z+q,k,J),P(Z+q,k+Q,J),P(Z,k+Q,J)],L=document.createElementNS(Y,"polygon");L.classList.add("iso-floor-slab"),L.setAttribute("points",H.map(U0).join(" ")),L.setAttribute("fill","#dbe6f4"),L.setAttribute("fill-opacity","0.22"),L.setAttribute("stroke","#b9c9df"),L.setAttribute("stroke-width","1"),K.appendChild(L);for(let W=0;W<=q;W++)K.appendChild(B0(P(Z+W,k,J),P(Z+W,k+Q,J)));for(let W=0;W<=Q;W++)K.appendChild(B0(P(Z,k+W,J),P(Z+q,k+W,J)));return K}function B0($,J){let K=document.createElementNS(Y,"line");return K.setAttribute("x1",String($.x)),K.setAttribute("y1",String($.y)),K.setAttribute("x2",String(J.x)),K.setAttribute("y2",String(J.y)),K.setAttribute("stroke","#2563eb"),K.setAttribute("stroke-width","1"),K.setAttribute("stroke-dasharray","5 5"),K.setAttribute("stroke-opacity","0.2"),K}function P($,J,K){let Z=h($,J,K.cellSize,K.selectedBounds.minX,K.selectedBounds.minY,K.padding.x,K.padding.y);return{x:Z.screenX,y:Z.screenY}}function U0($){return`${$.x},${$.y}`}function f1($){return new Map($.map((J)=>[J.name,J]))}function v1($){return[...$].map((J,K)=>({name:J.name,order:J.order??K})).sort((J,K)=>J.order-K.order||J.name.localeCompare(K.name))}function m1($){return $.slice().sort((J,K)=>{let Z=E0(J)-E0(K);if(Z!==0)return Z;let k=J.pos[0]+J.pos[1]-(K.pos[0]+K.pos[1]);if(k!==0)return k;return J.id.localeCompare(K.id)})}function E0($){if(V0($.asset))return 0;if(l($.asset))return 2;return 1}function c1($){let J=new Map;for(let K of $.scenes)for(let Z of K.elements??[])if(!J.has(Z.id)||J.get(Z.id)?.presence==="removed")J.set(Z.id,Z);return[...J.values()]}function p1($){let J=new Map;for(let K of $.scenes)for(let Z of K.connectors??[])if(!J.has(Z.id)||J.get(Z.id)?.presence==="removed")J.set(Z.id,Z);return[...J.values()]}function u1($,J,K){let Z=$.asset===b0?t($.text,$.asset,J.cellSize):V0($.asset)?e($.asset,$.primitive,J.cellSize):s1($,K,J.cellSize);Z.classList.add("iso-element",`iso-element-${$.id}`),Z.setAttribute("data-id",$.id),Z.setAttribute("data-asset",$.asset),Z.style.overflow="visible",Z.style.pointerEvents="auto",o0(Z,$,J);let k=$.enter;if(k&&k!=="none"&&$.presence!=="removed"){let q=`iso-anim-${k}`;return B(Z,q,"enter"),Z.addEventListener("animationend",()=>{Z.style.animation=""},{once:!0}),{node:Z,current:$,anchor:[0.5,1],isHidden:!1,entryKey:k,ambient:new Set}}return{node:Z,current:$,anchor:[0.5,1],isHidden:!1,ambient:new Set}}function l($){return $===b0}function V0($){return z1.has($)}function s1($,J,K){let Z=J($.asset);if(!Z)throw new G("ASSET_NOT_FOUND",`Asset not found: ${$.asset}`,{asset:$.asset,elementId:$.id});return I0(Z,$.asset,K)}function l1($,J,K){if(!l(J.asset)&&!V0(J.asset))return;let Z=l(J.asset)?t(J.text,J.asset,K.cellSize):e(J.asset,J.primitive,K.cellSize);a0($);while(Z.firstChild){let k=Z.firstChild;Z.removeChild(k),$.appendChild(k)}}function i1($,J){let K=document.createElementNS(Y,"g"),Z=document.createElementNS(Y,"path"),k={node:K,shaft:Z,isHidden:!1,ambient:new Set};r0(k,$,J);let q=$.enter;if(q&&q!=="none"&&$.presence!=="removed")B(K,`iso-anim-${q}`,"enter"),K.addEventListener("animationend",()=>{K.style.animation=""},{once:!0});return k}function r0($,J,K){r1($.node,J),a0($.node);let Z=d1(J.route,K);if(a1(J)){let k=document.createElementNS(Y,"path");k.classList.add("iso-connector-outline"),K0(k,J,Z,{stroke:J.style.outline??J.style.stroke,strokeWidth:J.style.strokeWidth+J.style.outlineWidth*2,includeDash:!1}),$.node.appendChild(k)}if($.shaft=document.createElementNS(Y,"path"),$.shaft.classList.add("iso-connector-shaft"),K0($.shaft,J,Z,{stroke:J.style.stroke,strokeWidth:J.style.strokeWidth,includeDash:!0}),$.node.appendChild($.shaft),J.style.variant==="road"&&J.style.lane==="center-dashed"){let k=document.createElementNS(Y,"path");k.classList.add("iso-connector-lane"),K0(k,J,Z,{stroke:"#ffffff",strokeWidth:Math.max(1,J.style.strokeWidth*0.12),includeDash:!1}),k.setAttribute("stroke-dasharray","8 8"),$.node.appendChild(k)}x0($.node,J,"start",K),x0($.node,J,"end",K),t0($,J.ambient??[])}function a0($){while($.firstChild)$.removeChild($.firstChild)}function r1($,J){$.setAttribute("class",["iso-connector",`iso-connector-${J.id}`,`iso-connector-variant-${J.style.variant}`,`iso-connector-pattern-${J.style.pattern}`,`iso-connector-direction-${J.direction}`,`iso-layer-${J.layer}`].join(" ")),$.setAttribute("data-id",J.id),$.setAttribute("data-layer",J.layer),$.style.overflow="visible",$.style.pointerEvents="auto"}function a1($){return Boolean($.style.outline&&$.style.outlineWidth>0)}function K0($,J,K,Z){if($.setAttribute("d",K),$.setAttribute("fill","none"),$.setAttribute("stroke",Z.stroke),$.setAttribute("stroke-width",String(Z.strokeWidth)),$.setAttribute("stroke-linecap","round"),$.setAttribute("stroke-linejoin","round"),$.setAttribute("opacity",String(J.style.opacity)),Z.includeDash&&J.style.pattern!=="solid"){let k=J.style.dash??C1[J.style.pattern];$.setAttribute("stroke-dasharray",k.join(" "))}}function d1($,J){return $.map((K,Z)=>{let k=P(K[0],K[1],J);return`${Z===0?"M":"L"} ${k.x} ${k.y}`}).join(" ")}function x0($,J,K,Z){let k=J[K];if(k==="none"||J.route.length<2)return;let q=o1(k,J,K,Z);q.classList.add(`iso-connector-${K}`),$.appendChild(q)}function o1($,J,K,Z){switch($){case"arrow":return n1(J,K,Z);case"dot":return y0(J,K,Z,!0);case"circle":return y0(J,K,Z,!1);case"diamond":return t1(J,K,Z);case"bar":return e1(J,K,Z);case"none":throw new G("CONNECTOR_ENDPOINT_NONE","Cannot create geometry for endpoint none")}}function n1($,J,K){let Z=r($,J),k=d0($,J),q=[-k[1],k[0]],Q=[Z[0]-k[0]*Z0,Z[1]-k[1]*Z0],H=D1/2,L=[Z,[Q[0]+q[0]*H,Q[1]+q[1]*H],[Q[0]-q[0]*H,Q[1]-q[1]*H]].map((j)=>P(j[0],j[1],K)),W=document.createElementNS(Y,"polygon");return W.setAttribute("points",L.map(U0).join(" ")),W.setAttribute("fill",$.style.stroke),W.setAttribute("opacity",String($.style.opacity)),W}function y0($,J,K,Z){let k=r($,J),q=P(k[0],k[1],K),Q=document.createElementNS(Y,"circle");return Q.setAttribute("cx",String(q.x)),Q.setAttribute("cy",String(q.y)),Q.setAttribute("r",String(q0*K.cellSize)),Q.setAttribute("stroke",$.style.stroke),Q.setAttribute("stroke-width",String(Math.max(1,$.style.strokeWidth))),Q.setAttribute("opacity",String($.style.opacity)),Q.setAttribute("fill",Z?$.style.stroke:"none"),Q}function t1($,J,K){let Z=r($,J),k=q0,q=[[Z[0],Z[1]-k],[Z[0]+k,Z[1]],[Z[0],Z[1]+k],[Z[0]-k,Z[1]]],Q=document.createElementNS(Y,"polygon");return Q.setAttribute("points",q.map((H)=>P(H[0],H[1],K)).map(U0).join(" ")),Q.setAttribute("fill",$.style.stroke),Q.setAttribute("opacity",String($.style.opacity)),Q}function e1($,J,K){let Z=r($,J),k=d0($,J),q=[-k[1],k[0]],Q=f0/2,H=P(Z[0]+q[0]*Q,Z[1]+q[1]*Q,K),L=P(Z[0]-q[0]*Q,Z[1]-q[1]*Q,K),W=document.createElementNS(Y,"line");return W.setAttribute("x1",String(H.x)),W.setAttribute("y1",String(H.y)),W.setAttribute("x2",String(L.x)),W.setAttribute("y2",String(L.y)),W.setAttribute("stroke",$.style.stroke),W.setAttribute("stroke-width",String(Math.max(1,$.style.strokeWidth))),W.setAttribute("stroke-linecap","round"),W.setAttribute("opacity",String($.style.opacity)),W}function r($,J){return J==="start"?$.route[0]:$.route[$.route.length-1]}function d0($,J){let K=J==="start"?g0($.route[0],$.route[1]):g0($.route[$.route.length-2],$.route[$.route.length-1]),Z=$.direction==="reverse"?[-K[0],-K[1]]:K;return $5(Z)}function g0($,J){return[J[0]-$[0],J[1]-$[1]]}function $5($){let J=Math.hypot($[0],$[1]);if(J===0)return[1,0];return[$[0]/J,$[1]/J]}function o0($,J,K){let Z=h(J.pos[0]+J.size,J.pos[1]+J.size,K.cellSize,K.selectedBounds.minX,K.selectedBounds.minY,K.padding.x,K.padding.y),q=s(J.size,K.cellSize)/K.cellSize;$.setAttribute("transform",`translate(${Z.screenX} ${Z.screenY}) scale(${q})`)}function n0($,J){let K=new Set((J??[]).map((Z)=>Z.name));for(let Z of $.ambient)if(!K.has(Z))$.node.classList.remove(`iso-ambient-${Z}`);for(let Z of K)if(!$.ambient.has(Z))$.node.classList.add(`iso-ambient-${Z}`);$.ambient=K}function t0($,J){let K=new Set((J??[]).map((Z)=>Z.name));for(let Z of $.ambient)if(!K.has(Z))$.shaft.classList.remove(`iso-ambient-${Z}`);for(let Z of K)$.shaft.classList.add(`iso-ambient-${Z}`);$.ambient=K}function B($,J,K="enter"){$.style.opacity="1",$.style.animation="none",$.getBoundingClientRect();let Z=K==="exit"?"var(--iso-anim-duration-exit, 300ms)":"var(--iso-anim-duration-enter, 400ms)",k=K==="exit"?"var(--iso-anim-easing-exit, ease-in)":"var(--iso-anim-easing-enter, ease-out)";$.style.animation=`${J} ${Z} ${k} forwards`}function J5($){return $}function K5($){return $*$*$}function Z5($){return 1-(1-$)**3}function k5($){return $<0.5?4*$*$*$:1-(-2*$+2)**3/2}function G0($){switch($){case"linear":return J5;case"easeInCubic":return K5;case"easeOutCubic":return Z5;case"easeInOutCubic":return k5}}var e0={scrollDirection:"vertical",scrollOffset:{},minProgress:0,maxProgress:1,keyboardControls:!1,touchControls:!1,scrollSensitivity:1,transitionDuration:600,transitionEasing:"ease-in-out"};class j0{_engine=new y;_bundle=null;_sceneIndex=0;_progress=0;_paused=!1;_config=e0;_container=null;_sceneElement=null;_ownsEngine=!0;_rafId=null;_pendingProgress=null;_destroyed=!1;_listeners=new Map;_cameraAnim=null;_cameraState=null;_minScroll=0;_maxScroll=0;_touchStartY=0;_touchStartX=0;_isDragging=!1;_transitionAnim=null;get engine(){return this._engine}get progress(){return this._progress}getProgress(){return this._assertNotDestroyed(),this._progress}get sceneIndex(){return this._sceneIndex}getSceneIndex(){return this._assertNotDestroyed(),this._sceneIndex}get scenes(){return this._bundle?.scenes??[]}get paused(){return this._paused}get currentScene(){return this.scenes[this._sceneIndex]}init($,J={},K={}){if(this._assertNotDestroyed(),!$.scenes||$.scenes.length===0)throw new O("CONTROLLER_NO_SCENES","init() requires at least one compiled scene stop");this._cancelFrame(),this._bundle=$,this._engine=K.engine??new y,this._ownsEngine=!K.engine,this._sceneIndex=0,this._progress=0,this._paused=!1,this._config={...e0,...J},this._sceneElement=K.sceneElement??J.sceneElement??null,this._cameraState={viewBox:S($),target:{type:"reset"},isZoomed:!1},this._engine.init($),this._bindScroll()}setProgress($){if(this._assertNotDestroyed(),!Number.isFinite($))throw new O("CONTROLLER_PROGRESS_OUT_OF_RANGE","setProgress() requires a finite progress value");let J=Math.max(0,Math.min(1,$));if(J===this._progress&&!this._paused&&this._rafId!==null)return;if(this._progress=J,this._cancelCameraAnimation(),this._paused)return;this._scheduleProgressForward(J)}nextScene(){if(this._assertNotDestroyed(),this.scenes.length<=1)return;let $=(this._sceneIndex+1)%this.scenes.length;this._transitionToScene($)}prevScene(){if(this._assertNotDestroyed(),this.scenes.length<=1)return;let $=(this._sceneIndex-1+this.scenes.length)%this.scenes.length;this._transitionToScene($)}setSceneIndex($){if(this._assertNotDestroyed(),$<0||$>=this.scenes.length)throw new O("CONTROLLER_SCENE_INDEX_OUT_OF_RANGE",`Scene index ${$} is out of bounds [0, ${this.scenes.length-1}]`);this._transitionToScene($)}pause(){if(this._assertNotDestroyed(),this._paused)return;this._paused=!0,this._cancelFrame(),this._engine.pause(),this._applyPauseState(!0),this._emit("paused")}resume(){if(this._assertNotDestroyed(),!this._paused)return;this._paused=!1,this._engine.resume(),this._applyPauseState(!1),this._scheduleProgressForward(this._progress),this._emit("resumed")}isPaused(){return this._assertNotDestroyed(),this._paused}zoomToElement($,J={}){this._assertNotDestroyed();let K=this._getSceneSvg();if(!this._bundle||!K)throw new O("CAMERA_NOT_INITIALIZED","zoomToElement() requires an initialized scene SVG");let Z=Q0(K,$);if(!Z)throw new O("CAMERA_TARGET_NOT_FOUND",`Camera target element "${$}" was not found`,{elementId:$});this._applyCameraDestination(a(Z,this._resolveCameraPadding(J)),{type:"element",id:$},J)}zoomToArea($,J={}){if(this._assertNotDestroyed(),!this._bundle||!this._getSceneSvg())throw new O("CAMERA_NOT_INITIALIZED","zoomToArea() requires an initialized scene SVG");this._assertValidCameraArea($);let K=H0(this._bundle,$);this._applyCameraDestination(a(K,this._resolveCameraPadding(J)),{type:"area",at:[...$.at],size:[...$.size]},J)}resetZoom($={}){if(this._assertNotDestroyed(),!this._bundle||!this._getSceneSvg())throw new O("CAMERA_NOT_INITIALIZED","resetZoom() requires an initialized scene SVG");this._applyCameraDestination(S(this._bundle),{type:"reset"},$)}getCameraState(){if(this._assertNotDestroyed(),!this._cameraState)throw new O("CAMERA_NOT_INITIALIZED","Camera state is not initialized");return W5(this._cameraState)}destroy(){if(this._assertNotDestroyed(),this._unbindScroll(),this._cancelFrame(),this._cancelTransition(),this._cancelCameraAnimation(),this._ownsEngine)this._engine.destroy();this._listeners.clear(),this._bundle=null,this._container=null,this._pendingProgress=null,this._cameraState=null,this._destroyed=!0}on($,J){this._assertNotDestroyed();let K=this._listeners.get($)??new Set;K.add(J),this._listeners.set($,K)}off($,J){this._assertNotDestroyed();let K=this._listeners.get($);if(K)K.delete(J)}_emit($,...J){let K=this._listeners.get($);if(K)for(let Z of K)try{Z(...J)}catch(k){queueMicrotask(()=>{throw k})}}_scheduleProgressForward($){if(this._pendingProgress=$,this._rafId!==null)return;this._rafId=requestAnimationFrame(()=>{this._rafId=null;let J=this._pendingProgress;if(this._pendingProgress=null,J===null||this._paused||this._destroyed)return;this._engine.setProgress(J),this._applyFrameUpdate(),this._emit("progress-change",J)})}_cancelFrame(){if(this._rafId!==null)cancelAnimationFrame(this._rafId),this._rafId=null;this._pendingProgress=null}_applyFrameUpdate(){if(!this._bundle)return;let $=this._sceneElement??this._container?.querySelector("svg")??null;if(!$)return;let J=this._engine.getFrameUpdates().map((Z)=>({id:Z.id,asset:Z.asset,pos:Z.pos,size:Z.size,layer:Z.layer,presence:Z.lifecycle,enter:Z.entry,exit:Z.exit,ambient:Z.ambient,text:Z.text,primitive:Z.primitive})),K=this._engine.getConnectorFrameUpdates().map((Z)=>({id:Z.id,route:Z.route,layer:Z.layer,presence:Z.lifecycle,style:Z.style,start:Z.start,end:Z.end,direction:Z.direction,enter:Z.entry,exit:Z.exit,ambient:Z.ambient}));m0($,J,K),this._applyLifecycleChanges(J),this._applyConnectorLifecycleChanges(K),this._applyCameraForProgress(this._progress)}_applyCameraForProgress($){let J=this._getSceneSvg();if(!this._bundle||!J)return;let K=this.scenes;if(K.length===0)return;let{previous:Z,next:k}=q5(K,$),q=this._effectiveCameraAt(Z.index),Q=this._effectiveCameraAt(k.index),H=this._resolveCameraViewBox(q),L=this._resolveCameraViewBox(Q),W=k.scene.progress-Z.scene.progress,j=W<=0?0:($-Z.scene.progress)/W,M=H1(Q.easing)(Math.max(0,Math.min(1,j))),X=Q1(H,L,M),T=M<1?q.target:Q.target;this._setCameraViewBox(J,X,T)}_effectiveCameraAt($){for(let J=$;J>=0;J--){let K=this.scenes[J]?.camera;if(K)return{target:K.target,padding:K.padding,duration:K.duration,easing:K.easing}}return{target:{type:"reset"}}}_resolveCameraViewBox($){if(!this._bundle)throw new O("CAMERA_NOT_INITIALIZED","Camera requires an initialized bundle");if($.target.type==="reset")return S(this._bundle);if($.target.type==="area")return a(H0(this._bundle,{at:$.target.at,size:$.target.size}),$.padding??32);let J=this._getSceneSvg();if(!J)throw new O("CAMERA_NOT_INITIALIZED","Camera requires an initialized scene SVG");let K=Q0(J,$.target.id);if(!K)throw new O("CAMERA_TARGET_NOT_FOUND",`Camera target element "${$.target.id}" was not found`,{elementId:$.target.id});return a(K,$.padding??32)}_applyCameraDestination($,J,K){let Z=this._getSceneSvg();if(!Z)throw new O("CAMERA_NOT_INITIALIZED","Camera requires an initialized scene SVG");this._cancelCameraAnimation();let k=this._resolveCameraDuration(K);if(k===0){this._setCameraViewBox(Z,$,J);return}let q=H5(Z)??this._cameraState?.viewBox??$,Q=H1(K.easing??this._config.transitionEasing),H=performance.now(),L=(W)=>{let j=Math.min((W-H)/k,1);if(this._setCameraViewBox(Z,Q1(q,$,Q(j)),J),j<1)this._cameraAnim=requestAnimationFrame(L);else this._cameraAnim=null};this._cameraAnim=requestAnimationFrame(L)}_setCameraViewBox($,J,K){u0($,J);let Z=this._bundle?S(this._bundle):J;this._cameraState={viewBox:J,target:K,isZoomed:!Q5(J,Z)},this._emit("camera-change",this.getCameraState())}_getSceneSvg(){return this._sceneElement??this._container?.querySelector("svg")??null}_resolveCameraPadding($){let J=$.padding??32;if(!Number.isFinite(J)||J<0||J>2048)throw new O("INVALID_CAMERA_OPTIONS","Camera padding must be between 0 and 2048");return J}_resolveCameraDuration($){let J=$.duration??this._config.transitionDuration;if(!Number.isInteger(J)||J<0||J>1e4)throw new O("INVALID_CAMERA_OPTIONS","Camera duration must be an integer from 0 to 10000");return J}_assertValidCameraArea($){if(!Array.isArray($.at)||$.at.length!==2||!$.at.every((J)=>Number.isFinite(J)&&J>=0)||!Array.isArray($.size)||$.size.length!==2||!$.size.every((J)=>Number.isFinite(J)&&J>0))throw new O("INVALID_CAMERA_OPTIONS","Camera area must use non-negative at and positive size")}_applyLifecycleChanges($){for(let J of $){let K=this._engine.getLifecycleTransition(J.id);if(!K)continue;let Z=this._sceneElement??this._container?.querySelector("svg");if(!Z)continue;let k=c0(Z,J.id);if(!k)continue;if(K.to==="entering"||K.to==="present")k.isHidden=!1,m(k.node);if($1(K))this._applyEntryAnimation(J,k);if(K1(K)){this._applyExitAnimation({...J,exit:k1(J.enter??"fade-in")},k);continue}if(J1(K))this._applyExitAnimation(J,k);if(Z1(K)){k.isHidden=!1,m(k.node),this._applyEntryAnimation({...J,enter:q1(J.exit??"fade-out")},k);continue}if(K.to==="removed")k.isHidden=!0,z(k.node)}}_applyConnectorLifecycleChanges($){for(let J of $){let K=this._engine.getConnectorLifecycleTransition(J.id);if(!K)continue;let Z=this._sceneElement??this._container?.querySelector("svg");if(!Z)continue;let k=p0(Z,J.id);if(!k)continue;if(K.to==="entering"||K.to==="present")k.isHidden=!1,m(k.node);if($1(K))this._applyConnectorEntryAnimation(J,k);if(K1(K)){this._applyConnectorExitAnimation({...J,exit:k1(J.enter??"fade-in")},k);continue}if(J1(K))this._applyConnectorExitAnimation(J,k);if(Z1(K)){k.isHidden=!1,m(k.node),this._applyConnectorEntryAnimation({...J,enter:q1(J.exit??"fade-out")},k);continue}if(K.to==="removed")k.isHidden=!0,z(k.node)}}_applyEntryAnimation($,J){let K=$.enter??"fade-in";if(K==="none")return;B(J.node,`iso-anim-${K}`,"enter");let Z=J.node.style.animation;J.node.addEventListener("animationend",()=>{if(J.node.style.animation===Z)J.node.style.animation=""},{once:!0})}_applyExitAnimation($,J){let K=$.exit??"fade-out";if(K==="none"){z(J.node);return}B(J.node,`iso-anim-${K}`,"exit");let Z=J.node.style.animation;J.node.addEventListener("animationend",()=>{if(J.node.style.animation===Z)z(J.node)},{once:!0})}_applyConnectorEntryAnimation($,J){let K=$.enter??"fade-in";if(K==="none")return;B(J.node,`iso-anim-${K}`,"enter");let Z=J.node.style.animation;J.node.addEventListener("animationend",()=>{if(J.node.style.animation===Z)J.node.style.animation=""},{once:!0})}_applyConnectorExitAnimation($,J){let K=$.exit??"fade-out";if(K==="none"){z(J.node);return}B(J.node,`iso-anim-${K}`,"exit");let Z=J.node.style.animation;J.node.addEventListener("animationend",()=>{if(J.node.style.animation===Z)z(J.node)},{once:!0})}_transitionToScene($){if(this._cancelTransition(),$===this._sceneIndex)return;this._sceneIndex=$,this._emit("scene-change",$);let J=this.scenes[$],K=this._progress,Z=J.progress,k=this._config.transitionDuration;if(k>0&&K!==Z){this._animateProgress(K,Z,k);return}this._progress=Z,this._scheduleProgressForward(Z)}_animateProgress($,J,K){let Z=G0(this._config.transitionEasing==="ease-in-out"?"easeInOutCubic":this._config.transitionEasing==="ease-out"?"easeOutCubic":"linear"),k=performance.now(),q=(Q)=>{let H=Q-k,L=Math.min(H/K,1),W=Z(L),j=$+(J-$)*W;if(this.setProgress(j),L<1)this._transitionAnim=requestAnimationFrame(q);else this._transitionAnim=null};this._transitionAnim=requestAnimationFrame(q)}_cancelTransition(){if(this._transitionAnim!==null)cancelAnimationFrame(this._transitionAnim),this._transitionAnim=null}_cancelCameraAnimation(){if(this._cameraAnim!==null)cancelAnimationFrame(this._cameraAnim),this._cameraAnim=null}_bindScroll(){let $=this._config.container;if(!$)return;if(this._container=$,this._calculateScrollBounds(),$.addEventListener("scroll",this._onScroll,{passive:!0}),window.addEventListener("resize",this._onResize,{passive:!0}),this._config.keyboardControls)document.addEventListener("keydown",this._onKeyDown);if(this._config.touchControls)$.addEventListener("touchstart",this._onTouchStart,{passive:!0}),$.addEventListener("touchmove",this._onTouchMove,{passive:!1}),$.addEventListener("touchend",this._onTouchEnd)}_unbindScroll(){let $=this._config.container;if(!$)return;$.removeEventListener("scroll",this._onScroll),window.removeEventListener("resize",this._onResize),document.removeEventListener("keydown",this._onKeyDown),$.removeEventListener("touchstart",this._onTouchStart),$.removeEventListener("touchmove",this._onTouchMove),$.removeEventListener("touchend",this._onTouchEnd)}_calculateScrollBounds(){let $=this._config.container;if(!$)return;let J=this._config.scrollOffset??{};if(this._config.scrollDirection==="horizontal"){this._minScroll=J.left??0,this._maxScroll=$.scrollWidth-$.clientWidth-(J.right??0);return}this._minScroll=J.top??0,this._maxScroll=$.scrollHeight-$.clientHeight-(J.bottom??0)}_onScroll=()=>{if(this._paused||this._destroyed)return;let $=this._config.container;if(!$)return;let J=this._config.scrollDirection==="horizontal"?$.scrollLeft:$.scrollTop,K=this._maxScroll-this._minScroll;if(K<=0)return;let Z=(J-this._minScroll)/K,k=Math.max(this._config.minProgress??0,Math.min(this._config.maxProgress??1,Z)),q=this._config.scrollSensitivity??1;if(k!==this._progress)this.setProgress(k*q)};_onResize=()=>{if(this._destroyed)return;this._calculateScrollBounds()};_onKeyDown=($)=>{if(this._destroyed)return;if($.key==="ArrowRight"||$.key==="ArrowDown"||$.key===" ")$.preventDefault(),this.nextScene();else if($.key==="ArrowLeft"||$.key==="ArrowUp")$.preventDefault(),this.prevScene()};_onTouchStart=($)=>{if(this._destroyed)return;this._isDragging=!0;let J=$.touches[0];if(this._config.scrollDirection==="horizontal")this._touchStartX=J.clientX;else this._touchStartY=J.clientY};_onTouchMove=($)=>{if(this._destroyed)return;if(!this._isDragging)return;let J=$.touches[0],K=this._config.scrollDirection==="horizontal"?this._touchStartX-J.clientX:this._touchStartY-J.clientY,Z=this._config.scrollSensitivity??1,k=K/300*Z,q=Math.max(0,Math.min(1,this._progress+k));this.setProgress(q)};_onTouchEnd=()=>{if(this._destroyed)return;this._isDragging=!1};_applyPauseState($){let J=this._config.container;if(!J)return;let K=J.querySelector("svg");if(!K)return;let Z=$?"paused":"running",k=K.querySelectorAll('[class*="iso-ambient-"]');for(let q=0;q<k.length;q++){let Q=k[q];Q.style.animationPlayState=Z}}_assertNotDestroyed(){if(!this._destroyed)return;throw new O("CONTROLLER_DESTROYED","AnimationController has been destroyed")}}function $1($){return $.from==="removed"&&$.to==="entering"}function J1($){return $.to==="exiting"}function K1($){return $.to==="removed"&&$.from!=="exiting"}function Z1($){return $.from==="exiting"&&$.to!=="removed"}function k1($){switch($){case"fade-in":return"fade-out";case"fade-in-grow":return"fade-out-shrink";case"fall-in":return"rise-away";case"rise-from-ground":return"fall-through-ground";case"slide-in-left":return"slide-out-left";case"slide-in-right":return"slide-out-right";case"flip-in":return"flip-out";case"none":return"none"}}function q1($){switch($){case"fade-out":return"fade-in";case"fade-out-shrink":return"fade-in-grow";case"fall-through-ground":return"rise-from-ground";case"rise-away":return"fall-in";case"slide-out-left":return"slide-in-left";case"slide-out-right":return"slide-in-right";case"flip-out":return"flip-in";case"none":return"none"}}function q5($,J){let K=0,Z=$.length-1;for(let k=0;k<$.length;k++){if($[k].progress<=J)K=k;if($[k].progress>=J){Z=k;break}}return{previous:{scene:$[K],index:K},next:{scene:$[Z],index:Z}}}function a($,J){return{minX:I($.minX-J),minY:I($.minY-J),width:I(Math.max(1,$.width+J*2)),height:I(Math.max(1,$.height+J*2))}}function Q1($,J,K){return{minX:I($.minX+(J.minX-$.minX)*K),minY:I($.minY+(J.minY-$.minY)*K),width:I($.width+(J.width-$.width)*K),height:I($.height+(J.height-$.height)*K)}}function Q5($,J){return $.minX===J.minX&&$.minY===J.minY&&$.width===J.width&&$.height===J.height}function H5($){let J=$.getAttribute("viewBox");if(!J)return;let K=J.trim().split(/\s+/).map(Number);if(K.length!==4||K.some((Z)=>!Number.isFinite(Z)))return;return{minX:K[0],minY:K[1],width:K[2],height:K[3]}}function H1($){return G0($==="ease-in-out"?"easeInOutCubic":$==="ease-out"?"easeOutCubic":"linear")}function W5($){return{viewBox:{...$.viewBox},target:$.target?.type==="area"?{type:"area",at:[...$.target.at],size:[...$.target.size]}:$.target?{...$.target}:void 0,isZoomed:$.isZoomed}}function I($){return Math.round($*1000)/1000}var W1="isostate-runtime-bundle",O0="0.2.0",L5=/^[a-f0-9]{64}$/;function U5($,J,K={}){V5($),G5(J);let Z=new y;Z.init(J);let k=v0($,J,{label:K.label,themeVars:K.themeVars}),q;if(K.controller!==void 0&&K.controller!==!1)q=new j0,q.init(J,{...K.controller,container:K.controller.container??$,sceneElement:k},{engine:Z,sceneElement:k});let Q=!1;return{svg:k,engine:Z,controller:q,getResolvedConfig:()=>j5(J,K),destroy:()=>{if(Q)return;if(Q=!0,q?.destroy(),Z.destroy(),k.parentNode===$)$.removeChild(k);else k.parentNode?.removeChild(k)}}}function V5($){if(!$||typeof $.appendChild!=="function"||typeof $.removeChild!=="function")throw new G("INVALID_MOUNT_TARGET","mountScene() requires a DOM HTMLElement target")}function G5($){if(!$||typeof $!=="object")throw new G("BUNDLE_FORMAT_MISSING","Runtime bundle must be a plain object");if($._format!==W1)throw new G("BUNDLE_FORMAT_MISSING","Runtime bundle format is missing or unsupported",{expected:W1});if(!Array.isArray($.scenes)||$.scenes.length===0)throw new G("BUNDLE_FORMAT_MISSING","Runtime bundle must include compiled scenes[]");if(L1($._version)!==L1(O0))throw new G("BUNDLE_VERSION_MISMATCH",`Runtime bundle version ${$._version} is not compatible with runtime ${O0}`,{bundleVersion:$._version,runtimeVersion:O0});if(!$._digest)throw new G("BUNDLE_DIGEST_MISSING","Runtime bundle digest is missing");if(typeof $._digest!=="string"||!L5.test($._digest))throw new G("BUNDLE_DIGEST_MISMATCH","Runtime bundle digest is malformed");let{_digest:J,...K}=$,Z=X5(Y5(K));if(Z!==J)throw new G("BUNDLE_DIGEST_MISMATCH","Runtime bundle digest does not match bundle content",{expected:J,actual:Z})}function j5($,J={}){return{grid:{cellSize:$.grid.cellSize},floor:{...$.floor},layout:{...$.layout,padding:{...$.layout.padding},align:[...$.layout.align]},viewBox:S($),camera:{viewBox:S($),isZoomed:!1},theme:$.theme,themeVars:O5($,J.themeVars),scenes:$.scenes.map((K)=>({id:K.id,progress:K.progress})),layerOrder:$.layers.map((K)=>({name:K.name,order:K.order})).sort((K,Z)=>K.order-Z.order||K.name.localeCompare(Z.name))}}function O5($,J={}){return{...u($.theme)??{},...$.themeVars??{},...J}}function L1($){let J=Number.parseInt(String($).split(".")[0]??"",10);return Number.isFinite(J)?J:Number.NaN}function Y5($){return JSON.stringify(Y0($))}function Y0($){if(Array.isArray($))return $.map((K)=>K===void 0?null:Y0(K));if(!M5($))return $;let J={};for(let K of Object.keys($).sort()){let Z=$[K];if(Z!==void 0)J[K]=Y0(Z)}return J}function M5($){return typeof $==="object"&&$!==null&&!Array.isArray($)}function X5($){let J=P5($),K=J.length*8;J.push(128);while(J.length%64!==56)J.push(0);for(let Q=7;Q>=0;Q--)J.push(Math.floor(K/2**(Q*8))&255);let Z=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],k=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],q=Array(64);for(let Q=0;Q<J.length;Q+=64){for(let U=0;U<16;U++){let F=Q+U*4;q[U]=(J[F]<<24|J[F+1]<<16|J[F+2]<<8|J[F+3])>>>0}for(let U=16;U<64;U++){let F=N(q[U-15],7)^N(q[U-15],18)^q[U-15]>>>3,b=N(q[U-2],17)^N(q[U-2],19)^q[U-2]>>>10;q[U]=q[U-16]+F+q[U-7]+b>>>0}let[H,L,W,j,M,X,T,C]=Z;for(let U=0;U<64;U++){let F=N(M,6)^N(M,11)^N(M,25),b=M&X^~M&T,V=C+F+b+k[U]+q[U]>>>0,E=N(H,2)^N(H,13)^N(H,22),_=H&L^H&W^L&W,A=E+_>>>0;C=T,T=X,X=M,M=j+V>>>0,j=W,W=L,L=H,H=V+A>>>0}Z[0]=Z[0]+H>>>0,Z[1]=Z[1]+L>>>0,Z[2]=Z[2]+W>>>0,Z[3]=Z[3]+j>>>0,Z[4]=Z[4]+M>>>0,Z[5]=Z[5]+X>>>0,Z[6]=Z[6]+T>>>0,Z[7]=Z[7]+C>>>0}return Z.map((Q)=>Q.toString(16).padStart(8,"0")).join("")}function P5($){return Array.from(new TextEncoder().encode($))}function N($,J){return $>>>J|$<<32-J}function n5($,J,K,Z,k,q,Q){return h($,J,K,Z,k,q,Q)}export{n5 as projectToScreen,U5 as mountScene};
|
|
493
5
|
|
|
494
|
-
|
|
495
|
-
const BUILTIN_THEMES = {
|
|
496
|
-
light: {
|
|
497
|
-
"--color-top": "#e2e8f0",
|
|
498
|
-
"--color-front": "#94a3b8",
|
|
499
|
-
"--color-side": "#64748b",
|
|
500
|
-
"--color-back": "#475569",
|
|
501
|
-
"--color-leaf": "#15803d",
|
|
502
|
-
"--color-trunk": "#78350f",
|
|
503
|
-
"--color-accent": "#3b82f6",
|
|
504
|
-
},
|
|
505
|
-
dark: {
|
|
506
|
-
"--color-top": "#334155",
|
|
507
|
-
"--color-front": "#1e293b",
|
|
508
|
-
"--color-side": "#0f172a",
|
|
509
|
-
"--color-back": "#020617",
|
|
510
|
-
"--color-leaf": "#166534",
|
|
511
|
-
"--color-trunk": "#451a03",
|
|
512
|
-
"--color-accent": "#60a5fa",
|
|
513
|
-
},
|
|
514
|
-
brand: {
|
|
515
|
-
"--color-top": "#c7d2fe",
|
|
516
|
-
"--color-front": "#818cf8",
|
|
517
|
-
"--color-side": "#6366f1",
|
|
518
|
-
"--color-back": "#4338ca",
|
|
519
|
-
"--color-leaf": "#22c55e",
|
|
520
|
-
"--color-trunk": "#854d0e",
|
|
521
|
-
"--color-accent": "#f59e0b",
|
|
522
|
-
},
|
|
523
|
-
};
|
|
524
|
-
/**
|
|
525
|
-
* Resolve a theme name to its CSS variable map.
|
|
526
|
-
* Returns undefined if the theme is not found.
|
|
527
|
-
*/
|
|
528
|
-
function resolveTheme(name) {
|
|
529
|
-
return BUILTIN_THEMES[name];
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Compose a new theme by extending an existing one with overrides.
|
|
533
|
-
*/
|
|
534
|
-
function composeTheme(baseName, overrides) {
|
|
535
|
-
const base = BUILTIN_THEMES[baseName];
|
|
536
|
-
if (!base) {
|
|
537
|
-
return { name: baseName, vars: { ...overrides } };
|
|
538
|
-
}
|
|
539
|
-
return { name: baseName, vars: { ...base, ...overrides } };
|
|
540
|
-
}
|
|
541
|
-
// ── Asset registry implementation ──────────────────────────────────────────
|
|
542
|
-
/**
|
|
543
|
-
* Default asset registry implementation.
|
|
544
|
-
* Maps asset ids to definitions and supports category filtering.
|
|
545
|
-
*/
|
|
546
|
-
class AssetRegistryImpl {
|
|
547
|
-
_assets = new Map();
|
|
548
|
-
register(asset) {
|
|
549
|
-
this._assets.set(asset.id, asset);
|
|
550
|
-
}
|
|
551
|
-
get(id) {
|
|
552
|
-
return this._assets.get(id);
|
|
553
|
-
}
|
|
554
|
-
getAll(category) {
|
|
555
|
-
if (category) {
|
|
556
|
-
return [...this._assets.values()].filter((a) => a.category === category);
|
|
557
|
-
}
|
|
558
|
-
return [...this._assets.values()];
|
|
559
|
-
}
|
|
560
|
-
has(id) {
|
|
561
|
-
return this._assets.has(id);
|
|
562
|
-
}
|
|
563
|
-
remove(id) {
|
|
564
|
-
this._assets.delete(id);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
function createAssetRegistry(assets = []) {
|
|
568
|
-
const registry = new AssetRegistryImpl();
|
|
569
|
-
for (const asset of assets) {
|
|
570
|
-
registry.register(asset);
|
|
571
|
-
}
|
|
572
|
-
return registry;
|
|
573
|
-
}
|
|
574
|
-
/** Create a fresh registry populated with the built-in demo assets. */
|
|
575
|
-
function createDefaultRegistry() {
|
|
576
|
-
return createAssetRegistry([
|
|
577
|
-
{
|
|
578
|
-
id: "iso-platform",
|
|
579
|
-
category: "infrastructure",
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
id: "iso-server",
|
|
583
|
-
category: "equipment",
|
|
584
|
-
},
|
|
585
|
-
{
|
|
586
|
-
id: "iso-database",
|
|
587
|
-
category: "equipment",
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
id: "iso-connector",
|
|
591
|
-
category: "decoration",
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
id: "iso-cloud",
|
|
595
|
-
category: "decoration",
|
|
596
|
-
},
|
|
597
|
-
]);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/** Base class for all structured errors */
|
|
601
|
-
class IsostateError extends Error {
|
|
602
|
-
code;
|
|
603
|
-
details;
|
|
604
|
-
constructor(code, message, details) {
|
|
605
|
-
super(message);
|
|
606
|
-
this.code = code;
|
|
607
|
-
this.details = details;
|
|
608
|
-
this.name = this.constructor.name;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
/** DSL parsing error */
|
|
612
|
-
class ParseError extends IsostateError {
|
|
613
|
-
}
|
|
614
|
-
/** DSL validation error */
|
|
615
|
-
class ValidationErrorClass extends IsostateError {
|
|
616
|
-
}
|
|
617
|
-
/** Rendering engine error */
|
|
618
|
-
class RenderError extends IsostateError {
|
|
619
|
-
}
|
|
620
|
-
/** Animation engine error */
|
|
621
|
-
class AnimationError extends IsostateError {
|
|
622
|
-
}
|
|
623
|
-
/** Animation controller error */
|
|
624
|
-
class ControllerError extends IsostateError {
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/** Default cell size in pixels */
|
|
628
|
-
const DEFAULT_CELL_SIZE = 64;
|
|
629
|
-
/**
|
|
630
|
-
* Calculate raw scene-space coordinates from isometric grid position.
|
|
631
|
-
* Layout is applied separately by subtracting resolved bounds and adding padding.
|
|
632
|
-
*/
|
|
633
|
-
function projectToRaw(gridX, gridY, cellSize) {
|
|
634
|
-
return {
|
|
635
|
-
rawX: cellSize * (gridX - gridY) * 0.5,
|
|
636
|
-
rawY: cellSize * (gridX + gridY) * 0.25,
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* Calculate screen coordinates from raw isometric projection and resolved bounds.
|
|
641
|
-
*/
|
|
642
|
-
function projectToScreen(gridX, gridY, cellSize, boundsMinX = 0, boundsMinY = 0, paddingX = 0, paddingY = 0) {
|
|
643
|
-
const { rawX, rawY } = projectToRaw(gridX, gridY, cellSize);
|
|
644
|
-
return {
|
|
645
|
-
screenX: rawX - boundsMinX + paddingX,
|
|
646
|
-
screenY: rawY - boundsMinY + paddingY,
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
/** Calculate the screen size for an element based on its grid size. */
|
|
650
|
-
function calculateVisualSize(gridSize, cellSize) {
|
|
651
|
-
return cellSize * gridSize;
|
|
652
|
-
}
|
|
653
|
-
/** Calculate the element transform string for positioning and scaling. */
|
|
654
|
-
function calculateTransform(screenX, screenY, visualSize, cellSize) {
|
|
655
|
-
const scale = visualSize / cellSize;
|
|
656
|
-
return `translate(${screenX}px, ${screenY}px) scale(${scale})`;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/** Build built-in entry, exit, ambient, and reduced-motion CSS keyframes. */
|
|
660
|
-
function buildKeyframeCSS() {
|
|
661
|
-
return [
|
|
662
|
-
"@keyframes iso-anim-fade-in{from{opacity:0}to{opacity:1}}",
|
|
663
|
-
"@keyframes iso-anim-fade-in-grow{from{opacity:0;scale:.01}to{opacity:1;scale:1}}",
|
|
664
|
-
"@keyframes iso-anim-fall-in{from{opacity:0;translate:0 -40px}to{opacity:1;translate:0 0}}",
|
|
665
|
-
"@keyframes iso-anim-rise-from-ground{from{opacity:0;translate:0 40px}to{opacity:1;translate:0 0}}",
|
|
666
|
-
"@keyframes iso-anim-slide-in-left{from{opacity:0;translate:-60px 0}to{opacity:1;translate:0 0}}",
|
|
667
|
-
"@keyframes iso-anim-slide-in-right{from{opacity:0;translate:60px 0}to{opacity:1;translate:0 0}}",
|
|
668
|
-
"@keyframes iso-anim-flip-in{from{opacity:1;scale:0 1}to{opacity:1;scale:1 1}}",
|
|
669
|
-
"@keyframes iso-anim-fade-out{from{opacity:1}to{opacity:0}}",
|
|
670
|
-
"@keyframes iso-anim-fade-out-shrink{from{opacity:1;scale:1}to{opacity:0;scale:.01}}",
|
|
671
|
-
"@keyframes iso-anim-fall-through-ground{from{opacity:1;translate:0 0}to{opacity:0;translate:0 40px}}",
|
|
672
|
-
"@keyframes iso-anim-rise-away{from{opacity:1;translate:0 0}to{opacity:0;translate:0 -40px}}",
|
|
673
|
-
"@keyframes iso-anim-slide-out-left{from{opacity:1;translate:0 0}to{opacity:0;translate:-60px 0}}",
|
|
674
|
-
"@keyframes iso-anim-slide-out-right{from{opacity:1;translate:0 0}to{opacity:0;translate:60px 0}}",
|
|
675
|
-
"@keyframes iso-anim-flip-out{from{opacity:1;scale:1 1}to{opacity:0;scale:0 1}}",
|
|
676
|
-
"@keyframes iso-anim-pulse{0%,100%{opacity:.7}50%{opacity:1}}",
|
|
677
|
-
"@keyframes iso-anim-float{0%,100%{translate:0 0}50%{translate:0 -6px}}",
|
|
678
|
-
"@keyframes iso-anim-shake{0%,25%,75%,100%{translate:0 0}50%{translate:3px 0}}",
|
|
679
|
-
"@keyframes iso-anim-glow{0%,100%{filter:drop-shadow(0 0 2px rgba(255,255,255,.5))}50%{filter:drop-shadow(0 0 8px rgba(255,255,255,.9))}}",
|
|
680
|
-
"@keyframes iso-anim-spin{from{rotate:0deg}to{rotate:360deg}}",
|
|
681
|
-
"@keyframes iso-anim-blink{0%,49%{opacity:1}50%,100%{opacity:0}}",
|
|
682
|
-
"@keyframes iso-connector-flow-route{from{stroke-dashoffset:0}to{stroke-dashoffset:calc(-1px * var(--iso-flow-distance,20))}}",
|
|
683
|
-
"@keyframes iso-connector-flow-reverse{from{stroke-dashoffset:0}to{stroke-dashoffset:calc(1px * var(--iso-flow-distance,20))}}",
|
|
684
|
-
".iso-connector-pattern-dashed .iso-connector-shaft,.iso-connector-pattern-dotted .iso-connector-shaft{--iso-flow-distance:20}",
|
|
685
|
-
".iso-connector-direction-route .iso-connector-shaft.iso-ambient-flow{animation:iso-connector-flow-route 900ms linear infinite}",
|
|
686
|
-
".iso-connector-direction-reverse .iso-connector-shaft.iso-ambient-flow{animation:iso-connector-flow-reverse 900ms linear infinite}",
|
|
687
|
-
"@media (prefers-reduced-motion: reduce){.iso-element,.iso-connector{animation-duration:1ms!important}.iso-ambient-pulse,.iso-ambient-float,.iso-ambient-shake,.iso-ambient-glow,.iso-ambient-spin,.iso-ambient-blink,.iso-ambient-flow{animation:none!important}}",
|
|
688
|
-
].join("\n");
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const NS$1 = "http://www.w3.org/2000/svg";
|
|
692
|
-
function createAssetResolver(bundle) {
|
|
693
|
-
const embedded = new Map();
|
|
694
|
-
const bundleAssets = bundle?.assets;
|
|
695
|
-
if (bundleAssets && typeof bundleAssets === "object") {
|
|
696
|
-
for (const [name, asset] of Object.entries(bundleAssets)) {
|
|
697
|
-
if (asset && typeof asset === "object" && typeof asset.url === "string") {
|
|
698
|
-
embedded.set(name, { url: asset.url, anchor: asset.anchor });
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
return (name) => embedded.get(name);
|
|
703
|
-
}
|
|
704
|
-
function createAssetNode(asset, assetName, cellSize) {
|
|
705
|
-
return createUrlAssetNode(asset.url, assetName, cellSize, asset.anchor);
|
|
706
|
-
}
|
|
707
|
-
function createTextAssetNode(textContent, assetName, cellSize) {
|
|
708
|
-
if (!textContent?.value) {
|
|
709
|
-
throw new RenderError("TEXT_CONTENT_MISSING", `Text content is missing for built-in asset: ${assetName}`, {
|
|
710
|
-
asset: assetName,
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
const group = document.createElementNS(NS$1, "g");
|
|
714
|
-
const text = document.createElementNS(NS$1, "text");
|
|
715
|
-
const align = textContent.align ?? "middle";
|
|
716
|
-
const fontSize = textContent.fontSize ?? 12;
|
|
717
|
-
const lineHeight = textContent.lineHeight ?? 1.2;
|
|
718
|
-
const anchorX = textAnchorX(align, cellSize);
|
|
719
|
-
text.setAttribute("x", String(anchorX));
|
|
720
|
-
text.setAttribute("y", String(-cellSize));
|
|
721
|
-
text.setAttribute("text-anchor", align);
|
|
722
|
-
text.setAttribute("dominant-baseline", "text-before-edge");
|
|
723
|
-
text.setAttribute("font-family", "Arial, Helvetica, sans-serif");
|
|
724
|
-
text.setAttribute("font-size", String(fontSize));
|
|
725
|
-
text.setAttribute("font-weight", String(textContent.fontWeight ?? 700));
|
|
726
|
-
text.setAttribute("fill", textContent.fill ?? "currentColor");
|
|
727
|
-
const lines = normalizeTextLines(textContent.value);
|
|
728
|
-
for (const [index, line] of lines.entries()) {
|
|
729
|
-
const tspan = document.createElementNS(NS$1, "tspan");
|
|
730
|
-
tspan.setAttribute("x", String(anchorX));
|
|
731
|
-
tspan.setAttribute("dy", index === 0 ? "0" : String(fontSize * lineHeight));
|
|
732
|
-
tspan.textContent = line;
|
|
733
|
-
text.appendChild(tspan);
|
|
734
|
-
}
|
|
735
|
-
group.appendChild(text);
|
|
736
|
-
return group;
|
|
737
|
-
}
|
|
738
|
-
function createPrimitiveAssetNode(assetName, primitive, cellSize) {
|
|
739
|
-
const group = document.createElementNS(NS$1, "g");
|
|
740
|
-
switch (assetName) {
|
|
741
|
-
case "rectangle":
|
|
742
|
-
appendProjectedPolygon(group, rectanglePoints(), primitive?.rectangle, cellSize);
|
|
743
|
-
return group;
|
|
744
|
-
case "polygon":
|
|
745
|
-
appendProjectedPolygon(group, primitive?.polygon?.points, primitive?.polygon, cellSize);
|
|
746
|
-
return group;
|
|
747
|
-
case "line":
|
|
748
|
-
appendProjectedPolyline(group, primitive?.line, primitive?.line, cellSize);
|
|
749
|
-
return group;
|
|
750
|
-
case "circle":
|
|
751
|
-
appendCircle(group, primitive?.circle, cellSize);
|
|
752
|
-
return group;
|
|
753
|
-
default:
|
|
754
|
-
throw new RenderError("PRIMITIVE_ASSET_UNKNOWN", `Unknown built-in primitive asset: ${assetName}`, {
|
|
755
|
-
asset: assetName,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
function textAnchorX(align, cellSize) {
|
|
760
|
-
if (align === "start")
|
|
761
|
-
return -cellSize / 2;
|
|
762
|
-
if (align === "end")
|
|
763
|
-
return cellSize / 2;
|
|
764
|
-
return 0;
|
|
765
|
-
}
|
|
766
|
-
function normalizeTextLines(value) {
|
|
767
|
-
const normalized = value.replace(/\r\n?/g, "\n").replace(/\n$/, "");
|
|
768
|
-
return normalized.split("\n");
|
|
769
|
-
}
|
|
770
|
-
function rectanglePoints() {
|
|
771
|
-
return [
|
|
772
|
-
[0, 0],
|
|
773
|
-
[1, 0],
|
|
774
|
-
[1, 1],
|
|
775
|
-
[0, 1],
|
|
776
|
-
];
|
|
777
|
-
}
|
|
778
|
-
function appendProjectedPolygon(group, points, style, cellSize) {
|
|
779
|
-
if (!points)
|
|
780
|
-
return;
|
|
781
|
-
const polygon = document.createElementNS(NS$1, "polygon");
|
|
782
|
-
polygon.setAttribute("points", points.map((point) => projectLocalPoint(point, cellSize)).join(" "));
|
|
783
|
-
applyPrimitiveStyle(polygon, style, { fill: "currentColor", stroke: "none" });
|
|
784
|
-
group.appendChild(polygon);
|
|
785
|
-
}
|
|
786
|
-
function appendProjectedPolyline(group, line, style, cellSize) {
|
|
787
|
-
if (!line?.points)
|
|
788
|
-
return;
|
|
789
|
-
const polyline = document.createElementNS(NS$1, "polyline");
|
|
790
|
-
polyline.setAttribute("points", line.points.map((point) => projectLocalPoint(point, cellSize)).join(" "));
|
|
791
|
-
polyline.setAttribute("fill", "none");
|
|
792
|
-
polyline.setAttribute("stroke-linecap", line.lineCap ?? "round");
|
|
793
|
-
polyline.setAttribute("stroke-linejoin", line.lineJoin ?? "round");
|
|
794
|
-
applyPrimitiveStyle(polyline, style, {
|
|
795
|
-
fill: "none",
|
|
796
|
-
stroke: "currentColor",
|
|
797
|
-
});
|
|
798
|
-
group.appendChild(polyline);
|
|
799
|
-
}
|
|
800
|
-
function appendCircle(group, style, cellSize) {
|
|
801
|
-
const center = projectLocalPoint([0.5, 0.5], cellSize);
|
|
802
|
-
const circle = document.createElementNS(NS$1, "circle");
|
|
803
|
-
const [cx, cy] = center.split(",").map(Number);
|
|
804
|
-
circle.setAttribute("cx", String(cx));
|
|
805
|
-
circle.setAttribute("cy", String(cy));
|
|
806
|
-
circle.setAttribute("r", String(cellSize * 0.2));
|
|
807
|
-
applyPrimitiveStyle(circle, style, {
|
|
808
|
-
fill: "currentColor",
|
|
809
|
-
stroke: "none",
|
|
810
|
-
});
|
|
811
|
-
group.appendChild(circle);
|
|
812
|
-
}
|
|
813
|
-
function projectLocalPoint(point, cellSize) {
|
|
814
|
-
const projected = projectToRaw(point[0], point[1], cellSize);
|
|
815
|
-
const anchor = projectToRaw(1, 1, cellSize);
|
|
816
|
-
return `${projected.rawX - anchor.rawX},${projected.rawY - anchor.rawY}`;
|
|
817
|
-
}
|
|
818
|
-
function applyPrimitiveStyle(node, style, defaults) {
|
|
819
|
-
node.setAttribute("fill", style?.fill ?? defaults.fill);
|
|
820
|
-
node.setAttribute("stroke", style?.stroke ?? defaults.stroke);
|
|
821
|
-
node.setAttribute("stroke-width", String(style?.strokeWidth ?? 0));
|
|
822
|
-
node.setAttribute("opacity", String(style?.opacity ?? 1));
|
|
823
|
-
if (style?.dash)
|
|
824
|
-
node.setAttribute("stroke-dasharray", style.dash.join(" "));
|
|
825
|
-
}
|
|
826
|
-
function createUrlAssetNode(url, assetName, cellSize, anchor) {
|
|
827
|
-
if (!isSafeAssetUrl(url)) {
|
|
828
|
-
throw new RenderError("INVALID_ASSET_URL", `Asset URL is unsafe: ${assetName}`, { asset: assetName });
|
|
829
|
-
}
|
|
830
|
-
const group = document.createElementNS(NS$1, "g");
|
|
831
|
-
const image = document.createElementNS(NS$1, "image");
|
|
832
|
-
const resolvedUrl = resolveBrowserAssetUrl(url);
|
|
833
|
-
const [anchorX, anchorY] = anchor ?? [0.5, 1];
|
|
834
|
-
image.setAttribute("href", resolvedUrl);
|
|
835
|
-
image.setAttributeNS("http://www.w3.org/1999/xlink", "href", resolvedUrl);
|
|
836
|
-
image.setAttribute("x", String(-cellSize * anchorX));
|
|
837
|
-
image.setAttribute("y", String(-cellSize * anchorY));
|
|
838
|
-
image.setAttribute("width", String(cellSize));
|
|
839
|
-
image.setAttribute("height", String(cellSize));
|
|
840
|
-
image.setAttribute("preserveAspectRatio", "xMidYMax meet");
|
|
841
|
-
group.appendChild(image);
|
|
842
|
-
return group;
|
|
843
|
-
}
|
|
844
|
-
function resolveBrowserAssetUrl(url) {
|
|
845
|
-
try {
|
|
846
|
-
const baseURI = document.baseURI;
|
|
847
|
-
return typeof baseURI === "string" ? new URL(url, baseURI).href : url;
|
|
848
|
-
}
|
|
849
|
-
catch {
|
|
850
|
-
return url;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
function isSafeAssetUrl(url) {
|
|
854
|
-
const normalized = url.trim().toLowerCase();
|
|
855
|
-
return normalized.length > 0 && !normalized.startsWith("javascript:");
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
const CSS_CUSTOM_PROPERTY_PATTERN = /^--[a-zA-Z0-9-_]+$/;
|
|
859
|
-
function applyThemeToElement(element, themeVars) {
|
|
860
|
-
for (const [name, value] of Object.entries(themeVars)) {
|
|
861
|
-
if (!CSS_CUSTOM_PROPERTY_PATTERN.test(name)) {
|
|
862
|
-
throw new RenderError("INVALID_THEME_VAR", `Invalid CSS custom property name: ${name}`);
|
|
863
|
-
}
|
|
864
|
-
element.style.setProperty(name, value);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const NS = "http://www.w3.org/2000/svg";
|
|
869
|
-
const BUILT_IN_TEXT_ASSET_ID = "text";
|
|
870
|
-
const BUILT_IN_PRIMITIVE_ASSET_IDS = new Set(["rectangle", "circle", "polygon", "line"]);
|
|
871
|
-
const DEFAULT_CONNECTOR_DASH = {
|
|
872
|
-
dashed: [12, 8],
|
|
873
|
-
dotted: [0, 8],
|
|
874
|
-
};
|
|
875
|
-
const ENDPOINT_RADIUS_GRID = 0.14;
|
|
876
|
-
const ARROW_LENGTH_GRID = 0.35;
|
|
877
|
-
const ARROW_WIDTH_GRID = 0.28;
|
|
878
|
-
const BAR_WIDTH_GRID = 0.4;
|
|
879
|
-
// ── Public API ────────────────────────────────────────────────────────────
|
|
880
|
-
/** Build the SVG DOM for a compiled runtime bundle and mount it into a container. */
|
|
881
|
-
function buildSceneDOM(container, bundle, config) {
|
|
882
|
-
const layout = resolveSceneLayout(bundle);
|
|
883
|
-
const initialStop = bundle.scenes[0];
|
|
884
|
-
const allElements = collectElementDefinitions(bundle);
|
|
885
|
-
const allConnectors = collectConnectorDefinitions(bundle);
|
|
886
|
-
const initialById = new Map((initialStop?.elements ?? []).map((element) => [element.id, element]));
|
|
887
|
-
const initialConnectorsById = new Map((initialStop?.connectors ?? []).map((connector) => [connector.id, connector]));
|
|
888
|
-
const svg = createRootSvg(layout, config?.label, bundle.className);
|
|
889
|
-
const assetResolver = createAssetResolver(bundle);
|
|
890
|
-
svg.appendChild(createCssDefs());
|
|
891
|
-
applyThemeToElement(svg, {
|
|
892
|
-
...(resolveTheme(bundle.theme) ?? {}),
|
|
893
|
-
...(bundle.themeVars ?? {}),
|
|
894
|
-
...(config?.themeVars ?? {}),
|
|
895
|
-
});
|
|
896
|
-
const sortedLayers = sortLayers(bundle.layers);
|
|
897
|
-
const layerMap = createLayerMap(sortedLayers);
|
|
898
|
-
if (bundle.floor.visible) {
|
|
899
|
-
svg.appendChild(createFloorGrid(bundle, layout));
|
|
900
|
-
}
|
|
901
|
-
const connectorMap = new Map();
|
|
902
|
-
for (const def of allConnectors) {
|
|
903
|
-
const initial = initialConnectorsById.get(def.id) ?? {
|
|
904
|
-
...def,
|
|
905
|
-
presence: "removed",
|
|
906
|
-
};
|
|
907
|
-
const state = createConnectorInstance(initial, layout);
|
|
908
|
-
if (initial.presence === "removed") {
|
|
909
|
-
state.isHidden = true;
|
|
910
|
-
hideElementAfterExit(state.node);
|
|
911
|
-
}
|
|
912
|
-
applyConnectorAmbientClasses(state, initial.ambient ?? []);
|
|
913
|
-
svg.appendChild(state.node);
|
|
914
|
-
connectorMap.set(def.id, state);
|
|
915
|
-
}
|
|
916
|
-
const depthGroup = document.createElementNS(NS, "g");
|
|
917
|
-
depthGroup.classList.add("iso-depth-layer");
|
|
918
|
-
depthGroup.setAttribute("data-layer", "depth");
|
|
919
|
-
svg.appendChild(depthGroup);
|
|
920
|
-
const labelGroup = document.createElementNS(NS, "g");
|
|
921
|
-
labelGroup.classList.add("iso-layer", "iso-layer-labels");
|
|
922
|
-
labelGroup.setAttribute("data-layer", "labels");
|
|
923
|
-
svg.appendChild(labelGroup);
|
|
924
|
-
const elementMap = new Map();
|
|
925
|
-
const sortedElements = sortElementsForPerspective(allElements);
|
|
926
|
-
for (const def of sortedElements) {
|
|
927
|
-
const declaredLayer = layerMap.get(def.layer);
|
|
928
|
-
if (!declaredLayer) {
|
|
929
|
-
throw new RenderError("MISSING_LAYER", `Unknown layer for "${def.id}": ${def.layer}`);
|
|
930
|
-
}
|
|
931
|
-
const initial = initialById.get(def.id) ?? {
|
|
932
|
-
...def,
|
|
933
|
-
presence: "removed",
|
|
934
|
-
};
|
|
935
|
-
const instance = createElementInstance(initial, layout, assetResolver);
|
|
936
|
-
instance.node.classList.add(`iso-layer-${def.layer}`);
|
|
937
|
-
instance.node.setAttribute("data-layer", def.layer);
|
|
938
|
-
if (initial.presence === "removed") {
|
|
939
|
-
instance.isHidden = true;
|
|
940
|
-
hideElementAfterExit(instance.node);
|
|
941
|
-
}
|
|
942
|
-
applyAmbientClasses(instance, initial.ambient ?? []);
|
|
943
|
-
const parent = isTextAsset(def.asset) ? labelGroup : depthGroup;
|
|
944
|
-
parent.appendChild(instance.node);
|
|
945
|
-
elementMap.set(def.id, instance);
|
|
946
|
-
}
|
|
947
|
-
svg._layerMap = layerMap;
|
|
948
|
-
svg._elementMap = elementMap;
|
|
949
|
-
svg._connectorMap = connectorMap;
|
|
950
|
-
container.appendChild(svg);
|
|
951
|
-
return svg;
|
|
952
|
-
}
|
|
953
|
-
/** Update transforms and ambient classes for a live set of interpolated runtime values. */
|
|
954
|
-
function updateElementTransforms(svg, elements, connectors = []) {
|
|
955
|
-
const layout = svg._layout;
|
|
956
|
-
if (!layout)
|
|
957
|
-
return;
|
|
958
|
-
const map = svg._elementMap;
|
|
959
|
-
if (map) {
|
|
960
|
-
for (const def of elements) {
|
|
961
|
-
const state = map.get(def.id);
|
|
962
|
-
if (!state)
|
|
963
|
-
continue;
|
|
964
|
-
updateGeneratedElementContent(state.node, def, layout);
|
|
965
|
-
applyElementTransform(state.node, def, layout);
|
|
966
|
-
applyAmbientClasses(state, def.ambient ?? []);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
const connectorMap = svg._connectorMap;
|
|
970
|
-
if (!connectorMap)
|
|
971
|
-
return;
|
|
972
|
-
for (const def of connectors) {
|
|
973
|
-
const state = connectorMap.get(def.id);
|
|
974
|
-
if (!state)
|
|
975
|
-
continue;
|
|
976
|
-
applyConnectorState(state, def, layout);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
/** Read the internal ElementState for an element by its id. */
|
|
980
|
-
function getElementState(svg, id) {
|
|
981
|
-
return svg._elementMap?.get(id);
|
|
982
|
-
}
|
|
983
|
-
/** Read the internal ConnectorState for a connector by its id. */
|
|
984
|
-
function getConnectorState(svg, id) {
|
|
985
|
-
return svg._connectorMap?.get(id);
|
|
986
|
-
}
|
|
987
|
-
function getResolvedViewBox(bundle) {
|
|
988
|
-
return resolveSceneLayout(bundle).viewBox;
|
|
989
|
-
}
|
|
990
|
-
// ── Lifecycle helpers ─────────────────────────────────────────────────────
|
|
991
|
-
/** Hide an element after its exit animation completes. */
|
|
992
|
-
function hideElementAfterExit(node) {
|
|
993
|
-
node.style.visibility = "hidden";
|
|
994
|
-
node.style.pointerEvents = "none";
|
|
995
|
-
}
|
|
996
|
-
/** Show an element on re-addition. */
|
|
997
|
-
function unhideElementOnReadd(node) {
|
|
998
|
-
node.style.visibility = "visible";
|
|
999
|
-
node.style.pointerEvents = "auto";
|
|
1000
|
-
}
|
|
1001
|
-
// ── Layout helpers ────────────────────────────────────────────────────────
|
|
1002
|
-
function resolveSceneLayout(bundle) {
|
|
1003
|
-
const cellSize = bundle.grid.cellSize;
|
|
1004
|
-
const padding = bundle.layout.padding;
|
|
1005
|
-
const contentBounds = calculateContentBounds(bundle, cellSize);
|
|
1006
|
-
const floorBounds = calculateFloorBounds(bundle, cellSize);
|
|
1007
|
-
const selectedBounds = selectBounds(bundle.layout.bounds, contentBounds, floorBounds);
|
|
1008
|
-
const width = selectedBounds.maxX - selectedBounds.minX + padding.x * 2;
|
|
1009
|
-
const height = selectedBounds.maxY - selectedBounds.minY + padding.y * 2;
|
|
1010
|
-
const viewBox = {
|
|
1011
|
-
minX: 0,
|
|
1012
|
-
minY: 0,
|
|
1013
|
-
width: roundDimension(width || cellSize),
|
|
1014
|
-
height: roundDimension(height || cellSize),
|
|
1015
|
-
};
|
|
1016
|
-
return {
|
|
1017
|
-
cellSize,
|
|
1018
|
-
padding,
|
|
1019
|
-
contentBounds,
|
|
1020
|
-
floorBounds,
|
|
1021
|
-
selectedBounds,
|
|
1022
|
-
viewBox,
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
function calculateContentBounds(bundle, cellSize) {
|
|
1026
|
-
let bounds = emptyBounds();
|
|
1027
|
-
for (const stop of bundle.scenes) {
|
|
1028
|
-
for (const element of stop.elements ?? []) {
|
|
1029
|
-
if (element.presence === "removed")
|
|
1030
|
-
continue;
|
|
1031
|
-
const { rawX, rawY } = projectToRaw(element.pos[0] + element.size, element.pos[1] + element.size, cellSize);
|
|
1032
|
-
const visualSize = calculateVisualSize(element.size, cellSize);
|
|
1033
|
-
const [anchorX, anchorY] = assetAnchorForBounds(bundle, element);
|
|
1034
|
-
bounds = includeBounds(bounds, {
|
|
1035
|
-
minX: rawX - visualSize * anchorX,
|
|
1036
|
-
minY: rawY - visualSize * anchorY,
|
|
1037
|
-
maxX: rawX + visualSize * (1 - anchorX),
|
|
1038
|
-
maxY: rawY + visualSize * (1 - anchorY),
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
for (const connector of stop.connectors ?? []) {
|
|
1042
|
-
if (connector.presence === "removed")
|
|
1043
|
-
continue;
|
|
1044
|
-
bounds = includeBounds(bounds, calculateConnectorBounds(connector, cellSize));
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
return normalizeBounds(bounds, cellSize);
|
|
1048
|
-
}
|
|
1049
|
-
function assetAnchorForBounds(bundle, element) {
|
|
1050
|
-
return bundle.assets?.[element.asset]?.anchor ?? [0.5, 1];
|
|
1051
|
-
}
|
|
1052
|
-
function calculateConnectorBounds(connector, cellSize) {
|
|
1053
|
-
let bounds = emptyBounds();
|
|
1054
|
-
for (const [x, y] of connector.route) {
|
|
1055
|
-
const { rawX, rawY } = projectToRaw(x, y, cellSize);
|
|
1056
|
-
bounds = includePoint(bounds, rawX, rawY);
|
|
1057
|
-
}
|
|
1058
|
-
const endpointPadding = Math.max(ARROW_LENGTH_GRID, BAR_WIDTH_GRID, ENDPOINT_RADIUS_GRID * 2) * cellSize;
|
|
1059
|
-
const strokePadding = connector.style.strokeWidth / 2 + (connector.style.outlineWidth ?? 0);
|
|
1060
|
-
const padding = endpointPadding + strokePadding;
|
|
1061
|
-
return {
|
|
1062
|
-
minX: bounds.minX - padding,
|
|
1063
|
-
minY: bounds.minY - padding,
|
|
1064
|
-
maxX: bounds.maxX + padding,
|
|
1065
|
-
maxY: bounds.maxY + padding,
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
function calculateFloorBounds(bundle, cellSize) {
|
|
1069
|
-
const origin = bundle.floor.origin;
|
|
1070
|
-
const [width, height] = bundle.floor.size;
|
|
1071
|
-
const points = [
|
|
1072
|
-
origin,
|
|
1073
|
-
[origin[0] + width, origin[1]],
|
|
1074
|
-
[origin[0], origin[1] + height],
|
|
1075
|
-
[origin[0] + width, origin[1] + height],
|
|
1076
|
-
];
|
|
1077
|
-
let bounds = emptyBounds();
|
|
1078
|
-
for (const [x, y] of points) {
|
|
1079
|
-
const { rawX, rawY } = projectToRaw(x, y, cellSize);
|
|
1080
|
-
bounds = includePoint(bounds, rawX, rawY);
|
|
1081
|
-
}
|
|
1082
|
-
return normalizeBounds(bounds, cellSize);
|
|
1083
|
-
}
|
|
1084
|
-
function selectBounds(mode, content, floor) {
|
|
1085
|
-
if (mode === "content")
|
|
1086
|
-
return content;
|
|
1087
|
-
if (mode === "floor")
|
|
1088
|
-
return floor;
|
|
1089
|
-
return includeBounds(content, floor);
|
|
1090
|
-
}
|
|
1091
|
-
function emptyBounds() {
|
|
1092
|
-
return {
|
|
1093
|
-
minX: Number.POSITIVE_INFINITY,
|
|
1094
|
-
minY: Number.POSITIVE_INFINITY,
|
|
1095
|
-
maxX: Number.NEGATIVE_INFINITY,
|
|
1096
|
-
maxY: Number.NEGATIVE_INFINITY,
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
function normalizeBounds(bounds, cellSize) {
|
|
1100
|
-
if (Number.isFinite(bounds.minX))
|
|
1101
|
-
return bounds;
|
|
1102
|
-
return { minX: 0, minY: 0, maxX: cellSize, maxY: cellSize };
|
|
1103
|
-
}
|
|
1104
|
-
function includePoint(bounds, x, y) {
|
|
1105
|
-
return {
|
|
1106
|
-
minX: Math.min(bounds.minX, x),
|
|
1107
|
-
minY: Math.min(bounds.minY, y),
|
|
1108
|
-
maxX: Math.max(bounds.maxX, x),
|
|
1109
|
-
maxY: Math.max(bounds.maxY, y),
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
function includeBounds(bounds, next) {
|
|
1113
|
-
return {
|
|
1114
|
-
minX: Math.min(bounds.minX, next.minX),
|
|
1115
|
-
minY: Math.min(bounds.minY, next.minY),
|
|
1116
|
-
maxX: Math.max(bounds.maxX, next.maxX),
|
|
1117
|
-
maxY: Math.max(bounds.maxY, next.maxY),
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
function roundDimension(value) {
|
|
1121
|
-
return Math.round(value * 1000) / 1000;
|
|
1122
|
-
}
|
|
1123
|
-
// ── Private helpers ───────────────────────────────────────────────────────
|
|
1124
|
-
function createRootSvg(layout, label, className) {
|
|
1125
|
-
const svg = document.createElementNS(NS, "svg");
|
|
1126
|
-
svg.classList.add("iso-scene");
|
|
1127
|
-
for (const token of className?.trim().split(/\s+/) ?? []) {
|
|
1128
|
-
if (token)
|
|
1129
|
-
svg.classList.add(token);
|
|
1130
|
-
}
|
|
1131
|
-
svg.setAttribute("width", "100%");
|
|
1132
|
-
svg.setAttribute("height", "100%");
|
|
1133
|
-
svg.setAttribute("viewBox", `${layout.viewBox.minX} ${layout.viewBox.minY} ${layout.viewBox.width} ${layout.viewBox.height}`);
|
|
1134
|
-
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1135
|
-
svg.style.width = "100%";
|
|
1136
|
-
svg.style.height = "100%";
|
|
1137
|
-
svg.style.display = "block";
|
|
1138
|
-
if (label) {
|
|
1139
|
-
svg.setAttribute("role", "img");
|
|
1140
|
-
svg.setAttribute("aria-label", label);
|
|
1141
|
-
}
|
|
1142
|
-
else {
|
|
1143
|
-
svg.setAttribute("aria-hidden", "true");
|
|
1144
|
-
}
|
|
1145
|
-
svg._layout = layout;
|
|
1146
|
-
svg._viewBoxW = layout.viewBox.width;
|
|
1147
|
-
svg._viewBoxH = layout.viewBox.height;
|
|
1148
|
-
return svg;
|
|
1149
|
-
}
|
|
1150
|
-
function createCssDefs() {
|
|
1151
|
-
const styleEl = document.createElementNS(NS, "style");
|
|
1152
|
-
styleEl.textContent = buildKeyframeCSS();
|
|
1153
|
-
return styleEl;
|
|
1154
|
-
}
|
|
1155
|
-
function createFloorGrid(bundle, layout) {
|
|
1156
|
-
const group = document.createElementNS(NS, "g");
|
|
1157
|
-
group.classList.add("iso-floor-grid", `iso-layer-${bundle.floor.layer}`);
|
|
1158
|
-
group.setAttribute("data-layer", bundle.floor.layer);
|
|
1159
|
-
const [originX, originY] = bundle.floor.origin;
|
|
1160
|
-
const [columns, rows] = bundle.floor.size;
|
|
1161
|
-
const corners = [
|
|
1162
|
-
projectGridPoint(originX, originY, layout),
|
|
1163
|
-
projectGridPoint(originX + columns, originY, layout),
|
|
1164
|
-
projectGridPoint(originX + columns, originY + rows, layout),
|
|
1165
|
-
projectGridPoint(originX, originY + rows, layout),
|
|
1166
|
-
];
|
|
1167
|
-
const slab = document.createElementNS(NS, "polygon");
|
|
1168
|
-
slab.classList.add("iso-floor-slab");
|
|
1169
|
-
slab.setAttribute("points", corners.map(pointToString).join(" "));
|
|
1170
|
-
slab.setAttribute("fill", "#dbe6f4");
|
|
1171
|
-
slab.setAttribute("fill-opacity", "0.22");
|
|
1172
|
-
slab.setAttribute("stroke", "#b9c9df");
|
|
1173
|
-
slab.setAttribute("stroke-width", "1");
|
|
1174
|
-
group.appendChild(slab);
|
|
1175
|
-
for (let x = 0; x <= columns; x++) {
|
|
1176
|
-
group.appendChild(createFloorLine(projectGridPoint(originX + x, originY, layout), projectGridPoint(originX + x, originY + rows, layout)));
|
|
1177
|
-
}
|
|
1178
|
-
for (let y = 0; y <= rows; y++) {
|
|
1179
|
-
group.appendChild(createFloorLine(projectGridPoint(originX, originY + y, layout), projectGridPoint(originX + columns, originY + y, layout)));
|
|
1180
|
-
}
|
|
1181
|
-
return group;
|
|
1182
|
-
}
|
|
1183
|
-
function createFloorLine(start, end) {
|
|
1184
|
-
const line = document.createElementNS(NS, "line");
|
|
1185
|
-
line.setAttribute("x1", String(start.x));
|
|
1186
|
-
line.setAttribute("y1", String(start.y));
|
|
1187
|
-
line.setAttribute("x2", String(end.x));
|
|
1188
|
-
line.setAttribute("y2", String(end.y));
|
|
1189
|
-
line.setAttribute("stroke", "#2563eb");
|
|
1190
|
-
line.setAttribute("stroke-width", "1");
|
|
1191
|
-
line.setAttribute("stroke-dasharray", "5 5");
|
|
1192
|
-
line.setAttribute("stroke-opacity", "0.2");
|
|
1193
|
-
return line;
|
|
1194
|
-
}
|
|
1195
|
-
function projectGridPoint(x, y, layout) {
|
|
1196
|
-
const screen = projectToScreen(x, y, layout.cellSize, layout.selectedBounds.minX, layout.selectedBounds.minY, layout.padding.x, layout.padding.y);
|
|
1197
|
-
return { x: screen.screenX, y: screen.screenY };
|
|
1198
|
-
}
|
|
1199
|
-
function pointToString(point) {
|
|
1200
|
-
return `${point.x},${point.y}`;
|
|
1201
|
-
}
|
|
1202
|
-
function createLayerMap(layers) {
|
|
1203
|
-
return new Map(layers.map((layer) => [layer.name, layer]));
|
|
1204
|
-
}
|
|
1205
|
-
function sortLayers(layers) {
|
|
1206
|
-
return [...layers]
|
|
1207
|
-
.map((layer, index) => ({ name: layer.name, order: layer.order ?? index }))
|
|
1208
|
-
.sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
|
|
1209
|
-
}
|
|
1210
|
-
function sortElementsForPerspective(elements) {
|
|
1211
|
-
return elements.slice().sort((a, b) => {
|
|
1212
|
-
const bucket = renderBucket(a) - renderBucket(b);
|
|
1213
|
-
if (bucket !== 0)
|
|
1214
|
-
return bucket;
|
|
1215
|
-
const depth = a.pos[0] + a.pos[1] - (b.pos[0] + b.pos[1]);
|
|
1216
|
-
if (depth !== 0)
|
|
1217
|
-
return depth;
|
|
1218
|
-
return a.id.localeCompare(b.id);
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
function renderBucket(element) {
|
|
1222
|
-
if (isPrimitiveAsset(element.asset))
|
|
1223
|
-
return 0;
|
|
1224
|
-
if (isTextAsset(element.asset))
|
|
1225
|
-
return 2;
|
|
1226
|
-
return 1;
|
|
1227
|
-
}
|
|
1228
|
-
function collectElementDefinitions(bundle) {
|
|
1229
|
-
const byId = new Map();
|
|
1230
|
-
for (const stop of bundle.scenes) {
|
|
1231
|
-
for (const element of stop.elements ?? []) {
|
|
1232
|
-
if (!byId.has(element.id) || byId.get(element.id)?.presence === "removed") {
|
|
1233
|
-
byId.set(element.id, element);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
return [...byId.values()];
|
|
1238
|
-
}
|
|
1239
|
-
function collectConnectorDefinitions(bundle) {
|
|
1240
|
-
const byId = new Map();
|
|
1241
|
-
for (const stop of bundle.scenes) {
|
|
1242
|
-
for (const connector of stop.connectors ?? []) {
|
|
1243
|
-
if (!byId.has(connector.id) || byId.get(connector.id)?.presence === "removed") {
|
|
1244
|
-
byId.set(connector.id, connector);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
return [...byId.values()];
|
|
1249
|
-
}
|
|
1250
|
-
/** Create a single element SVG instance applying entry animation. */
|
|
1251
|
-
function createElementInstance(def, layout, resolveAsset) {
|
|
1252
|
-
const node = def.asset === BUILT_IN_TEXT_ASSET_ID
|
|
1253
|
-
? createTextAssetNode(def.text, def.asset, layout.cellSize)
|
|
1254
|
-
: isPrimitiveAsset(def.asset)
|
|
1255
|
-
? createPrimitiveAssetNode(def.asset, def.primitive, layout.cellSize)
|
|
1256
|
-
: createResolvedAssetNode(def, resolveAsset, layout.cellSize);
|
|
1257
|
-
node.classList.add("iso-element", `iso-element-${def.id}`);
|
|
1258
|
-
node.setAttribute("data-id", def.id);
|
|
1259
|
-
node.setAttribute("data-asset", def.asset);
|
|
1260
|
-
node.style.overflow = "visible";
|
|
1261
|
-
node.style.pointerEvents = "auto";
|
|
1262
|
-
applyElementTransform(node, def, layout);
|
|
1263
|
-
const entryAnim = def.enter;
|
|
1264
|
-
if (entryAnim && entryAnim !== "none" && def.presence !== "removed") {
|
|
1265
|
-
const keyName = `iso-anim-${entryAnim}`;
|
|
1266
|
-
animateElement(node, keyName, "enter");
|
|
1267
|
-
node.addEventListener("animationend", () => {
|
|
1268
|
-
node.style.animation = "";
|
|
1269
|
-
}, { once: true });
|
|
1270
|
-
return { node, isHidden: false, entryKey: entryAnim, ambient: new Set() };
|
|
1271
|
-
}
|
|
1272
|
-
return { node, isHidden: false, ambient: new Set() };
|
|
1273
|
-
}
|
|
1274
|
-
function isTextAsset(assetId) {
|
|
1275
|
-
return assetId === BUILT_IN_TEXT_ASSET_ID;
|
|
1276
|
-
}
|
|
1277
|
-
function isPrimitiveAsset(assetId) {
|
|
1278
|
-
return BUILT_IN_PRIMITIVE_ASSET_IDS.has(assetId);
|
|
1279
|
-
}
|
|
1280
|
-
function createResolvedAssetNode(def, resolveAsset, cellSize) {
|
|
1281
|
-
const asset = resolveAsset(def.asset);
|
|
1282
|
-
if (!asset) {
|
|
1283
|
-
throw new RenderError("ASSET_NOT_FOUND", `Asset not found: ${def.asset}`, {
|
|
1284
|
-
asset: def.asset,
|
|
1285
|
-
elementId: def.id,
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
return createAssetNode(asset, def.asset, cellSize);
|
|
1289
|
-
}
|
|
1290
|
-
function updateGeneratedElementContent(node, def, layout) {
|
|
1291
|
-
if (!isTextAsset(def.asset) && !isPrimitiveAsset(def.asset))
|
|
1292
|
-
return;
|
|
1293
|
-
const replacement = isTextAsset(def.asset)
|
|
1294
|
-
? createTextAssetNode(def.text, def.asset, layout.cellSize)
|
|
1295
|
-
: createPrimitiveAssetNode(def.asset, def.primitive, layout.cellSize);
|
|
1296
|
-
clearChildren(node);
|
|
1297
|
-
while (replacement.firstChild) {
|
|
1298
|
-
const child = replacement.firstChild;
|
|
1299
|
-
replacement.removeChild(child);
|
|
1300
|
-
node.appendChild(child);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
function createConnectorInstance(def, layout) {
|
|
1304
|
-
const node = document.createElementNS(NS, "g");
|
|
1305
|
-
const shaft = document.createElementNS(NS, "path");
|
|
1306
|
-
const state = { node, shaft, isHidden: false, ambient: new Set() };
|
|
1307
|
-
applyConnectorState(state, def, layout);
|
|
1308
|
-
const entryAnim = def.enter;
|
|
1309
|
-
if (entryAnim && entryAnim !== "none" && def.presence !== "removed") {
|
|
1310
|
-
animateElement(node, `iso-anim-${entryAnim}`, "enter");
|
|
1311
|
-
node.addEventListener("animationend", () => {
|
|
1312
|
-
node.style.animation = "";
|
|
1313
|
-
}, { once: true });
|
|
1314
|
-
}
|
|
1315
|
-
return state;
|
|
1316
|
-
}
|
|
1317
|
-
function applyConnectorState(state, def, layout) {
|
|
1318
|
-
applyConnectorGroupAttrs(state.node, def);
|
|
1319
|
-
clearChildren(state.node);
|
|
1320
|
-
const d = routePath(def.route, layout);
|
|
1321
|
-
if (shouldRenderOutline(def)) {
|
|
1322
|
-
const outline = document.createElementNS(NS, "path");
|
|
1323
|
-
outline.classList.add("iso-connector-outline");
|
|
1324
|
-
applyConnectorPathAttrs(outline, def, d, {
|
|
1325
|
-
stroke: def.style.outline ?? def.style.stroke,
|
|
1326
|
-
strokeWidth: def.style.strokeWidth + def.style.outlineWidth * 2,
|
|
1327
|
-
includeDash: false,
|
|
1328
|
-
});
|
|
1329
|
-
state.node.appendChild(outline);
|
|
1330
|
-
}
|
|
1331
|
-
state.shaft = document.createElementNS(NS, "path");
|
|
1332
|
-
state.shaft.classList.add("iso-connector-shaft");
|
|
1333
|
-
applyConnectorPathAttrs(state.shaft, def, d, {
|
|
1334
|
-
stroke: def.style.stroke,
|
|
1335
|
-
strokeWidth: def.style.strokeWidth,
|
|
1336
|
-
includeDash: true,
|
|
1337
|
-
});
|
|
1338
|
-
state.node.appendChild(state.shaft);
|
|
1339
|
-
if (def.style.variant === "road" && def.style.lane === "center-dashed") {
|
|
1340
|
-
const lane = document.createElementNS(NS, "path");
|
|
1341
|
-
lane.classList.add("iso-connector-lane");
|
|
1342
|
-
applyConnectorPathAttrs(lane, def, d, {
|
|
1343
|
-
stroke: "#ffffff",
|
|
1344
|
-
strokeWidth: Math.max(1, def.style.strokeWidth * 0.12),
|
|
1345
|
-
includeDash: false,
|
|
1346
|
-
});
|
|
1347
|
-
lane.setAttribute("stroke-dasharray", "8 8");
|
|
1348
|
-
state.node.appendChild(lane);
|
|
1349
|
-
}
|
|
1350
|
-
appendEndpoint(state.node, def, "start", layout);
|
|
1351
|
-
appendEndpoint(state.node, def, "end", layout);
|
|
1352
|
-
applyConnectorAmbientClasses(state, def.ambient ?? []);
|
|
1353
|
-
}
|
|
1354
|
-
function clearChildren(node) {
|
|
1355
|
-
while (node.firstChild)
|
|
1356
|
-
node.removeChild(node.firstChild);
|
|
1357
|
-
}
|
|
1358
|
-
function applyConnectorGroupAttrs(node, def) {
|
|
1359
|
-
node.setAttribute("class", [
|
|
1360
|
-
"iso-connector",
|
|
1361
|
-
`iso-connector-${def.id}`,
|
|
1362
|
-
`iso-connector-variant-${def.style.variant}`,
|
|
1363
|
-
`iso-connector-pattern-${def.style.pattern}`,
|
|
1364
|
-
`iso-connector-direction-${def.direction}`,
|
|
1365
|
-
`iso-layer-${def.layer}`,
|
|
1366
|
-
].join(" "));
|
|
1367
|
-
node.setAttribute("data-id", def.id);
|
|
1368
|
-
node.setAttribute("data-layer", def.layer);
|
|
1369
|
-
node.style.overflow = "visible";
|
|
1370
|
-
node.style.pointerEvents = "auto";
|
|
1371
|
-
}
|
|
1372
|
-
function shouldRenderOutline(def) {
|
|
1373
|
-
return Boolean(def.style.outline && def.style.outlineWidth > 0);
|
|
1374
|
-
}
|
|
1375
|
-
function applyConnectorPathAttrs(path, def, d, options) {
|
|
1376
|
-
path.setAttribute("d", d);
|
|
1377
|
-
path.setAttribute("fill", "none");
|
|
1378
|
-
path.setAttribute("stroke", options.stroke);
|
|
1379
|
-
path.setAttribute("stroke-width", String(options.strokeWidth));
|
|
1380
|
-
path.setAttribute("stroke-linecap", "round");
|
|
1381
|
-
path.setAttribute("stroke-linejoin", "round");
|
|
1382
|
-
path.setAttribute("opacity", String(def.style.opacity));
|
|
1383
|
-
if (options.includeDash && def.style.pattern !== "solid") {
|
|
1384
|
-
const dash = def.style.dash ?? DEFAULT_CONNECTOR_DASH[def.style.pattern];
|
|
1385
|
-
path.setAttribute("stroke-dasharray", dash.join(" "));
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
function routePath(route, layout) {
|
|
1389
|
-
return route
|
|
1390
|
-
.map((point, index) => {
|
|
1391
|
-
const projected = projectGridPoint(point[0], point[1], layout);
|
|
1392
|
-
return `${index === 0 ? "M" : "L"} ${projected.x} ${projected.y}`;
|
|
1393
|
-
})
|
|
1394
|
-
.join(" ");
|
|
1395
|
-
}
|
|
1396
|
-
function appendEndpoint(group, def, kind, layout) {
|
|
1397
|
-
const endpoint = def[kind];
|
|
1398
|
-
if (endpoint === "none" || def.route.length < 2)
|
|
1399
|
-
return;
|
|
1400
|
-
const node = createEndpointNode(endpoint, def, kind, layout);
|
|
1401
|
-
node.classList.add(`iso-connector-${kind}`);
|
|
1402
|
-
group.appendChild(node);
|
|
1403
|
-
}
|
|
1404
|
-
function createEndpointNode(endpoint, def, kind, layout) {
|
|
1405
|
-
switch (endpoint) {
|
|
1406
|
-
case "arrow":
|
|
1407
|
-
return createArrowEndpoint(def, kind, layout);
|
|
1408
|
-
case "dot":
|
|
1409
|
-
return createCircleEndpoint(def, kind, layout, true);
|
|
1410
|
-
case "circle":
|
|
1411
|
-
return createCircleEndpoint(def, kind, layout, false);
|
|
1412
|
-
case "diamond":
|
|
1413
|
-
return createDiamondEndpoint(def, kind, layout);
|
|
1414
|
-
case "bar":
|
|
1415
|
-
return createBarEndpoint(def, kind, layout);
|
|
1416
|
-
case "none":
|
|
1417
|
-
throw new RenderError("CONNECTOR_ENDPOINT_NONE", "Cannot create geometry for endpoint none");
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
function createArrowEndpoint(def, kind, layout) {
|
|
1421
|
-
const tip = endpointPoint(def, kind);
|
|
1422
|
-
const direction = endpointDirection(def, kind);
|
|
1423
|
-
const perpendicular = [-direction[1], direction[0]];
|
|
1424
|
-
const base = [tip[0] - direction[0] * ARROW_LENGTH_GRID, tip[1] - direction[1] * ARROW_LENGTH_GRID];
|
|
1425
|
-
const halfWidth = ARROW_WIDTH_GRID / 2;
|
|
1426
|
-
const points = [
|
|
1427
|
-
tip,
|
|
1428
|
-
[base[0] + perpendicular[0] * halfWidth, base[1] + perpendicular[1] * halfWidth],
|
|
1429
|
-
[base[0] - perpendicular[0] * halfWidth, base[1] - perpendicular[1] * halfWidth],
|
|
1430
|
-
].map((point) => projectGridPoint(point[0], point[1], layout));
|
|
1431
|
-
const polygon = document.createElementNS(NS, "polygon");
|
|
1432
|
-
polygon.setAttribute("points", points.map(pointToString).join(" "));
|
|
1433
|
-
polygon.setAttribute("fill", def.style.stroke);
|
|
1434
|
-
polygon.setAttribute("opacity", String(def.style.opacity));
|
|
1435
|
-
return polygon;
|
|
1436
|
-
}
|
|
1437
|
-
function createCircleEndpoint(def, kind, layout, filled) {
|
|
1438
|
-
const point = endpointPoint(def, kind);
|
|
1439
|
-
const projected = projectGridPoint(point[0], point[1], layout);
|
|
1440
|
-
const circle = document.createElementNS(NS, "circle");
|
|
1441
|
-
circle.setAttribute("cx", String(projected.x));
|
|
1442
|
-
circle.setAttribute("cy", String(projected.y));
|
|
1443
|
-
circle.setAttribute("r", String(ENDPOINT_RADIUS_GRID * layout.cellSize));
|
|
1444
|
-
circle.setAttribute("stroke", def.style.stroke);
|
|
1445
|
-
circle.setAttribute("stroke-width", String(Math.max(1, def.style.strokeWidth)));
|
|
1446
|
-
circle.setAttribute("opacity", String(def.style.opacity));
|
|
1447
|
-
circle.setAttribute("fill", filled ? def.style.stroke : "none");
|
|
1448
|
-
return circle;
|
|
1449
|
-
}
|
|
1450
|
-
function createDiamondEndpoint(def, kind, layout) {
|
|
1451
|
-
const center = endpointPoint(def, kind);
|
|
1452
|
-
const radius = ENDPOINT_RADIUS_GRID;
|
|
1453
|
-
const points = [
|
|
1454
|
-
[center[0], center[1] - radius],
|
|
1455
|
-
[center[0] + radius, center[1]],
|
|
1456
|
-
[center[0], center[1] + radius],
|
|
1457
|
-
[center[0] - radius, center[1]],
|
|
1458
|
-
];
|
|
1459
|
-
const polygon = document.createElementNS(NS, "polygon");
|
|
1460
|
-
polygon.setAttribute("points", points
|
|
1461
|
-
.map((point) => projectGridPoint(point[0], point[1], layout))
|
|
1462
|
-
.map(pointToString)
|
|
1463
|
-
.join(" "));
|
|
1464
|
-
polygon.setAttribute("fill", def.style.stroke);
|
|
1465
|
-
polygon.setAttribute("opacity", String(def.style.opacity));
|
|
1466
|
-
return polygon;
|
|
1467
|
-
}
|
|
1468
|
-
function createBarEndpoint(def, kind, layout) {
|
|
1469
|
-
const center = endpointPoint(def, kind);
|
|
1470
|
-
const direction = endpointDirection(def, kind);
|
|
1471
|
-
const perpendicular = [-direction[1], direction[0]];
|
|
1472
|
-
const half = BAR_WIDTH_GRID / 2;
|
|
1473
|
-
const a = projectGridPoint(center[0] + perpendicular[0] * half, center[1] + perpendicular[1] * half, layout);
|
|
1474
|
-
const b = projectGridPoint(center[0] - perpendicular[0] * half, center[1] - perpendicular[1] * half, layout);
|
|
1475
|
-
const line = document.createElementNS(NS, "line");
|
|
1476
|
-
line.setAttribute("x1", String(a.x));
|
|
1477
|
-
line.setAttribute("y1", String(a.y));
|
|
1478
|
-
line.setAttribute("x2", String(b.x));
|
|
1479
|
-
line.setAttribute("y2", String(b.y));
|
|
1480
|
-
line.setAttribute("stroke", def.style.stroke);
|
|
1481
|
-
line.setAttribute("stroke-width", String(Math.max(1, def.style.strokeWidth)));
|
|
1482
|
-
line.setAttribute("stroke-linecap", "round");
|
|
1483
|
-
line.setAttribute("opacity", String(def.style.opacity));
|
|
1484
|
-
return line;
|
|
1485
|
-
}
|
|
1486
|
-
function endpointPoint(def, kind) {
|
|
1487
|
-
return kind === "start" ? def.route[0] : def.route[def.route.length - 1];
|
|
1488
|
-
}
|
|
1489
|
-
function endpointDirection(def, kind) {
|
|
1490
|
-
const point = kind === "start"
|
|
1491
|
-
? vectorBetween(def.route[0], def.route[1])
|
|
1492
|
-
: vectorBetween(def.route[def.route.length - 2], def.route[def.route.length - 1]);
|
|
1493
|
-
const effective = def.direction === "reverse" ? [-point[0], -point[1]] : point;
|
|
1494
|
-
return normalizeVector(effective);
|
|
1495
|
-
}
|
|
1496
|
-
function vectorBetween(start, end) {
|
|
1497
|
-
return [end[0] - start[0], end[1] - start[1]];
|
|
1498
|
-
}
|
|
1499
|
-
function normalizeVector(vector) {
|
|
1500
|
-
const length = Math.hypot(vector[0], vector[1]);
|
|
1501
|
-
if (length === 0)
|
|
1502
|
-
return [1, 0];
|
|
1503
|
-
return [vector[0] / length, vector[1] / length];
|
|
1504
|
-
}
|
|
1505
|
-
function applyElementTransform(node, def, layout) {
|
|
1506
|
-
const screen = projectToScreen(def.pos[0] + def.size, def.pos[1] + def.size, layout.cellSize, layout.selectedBounds.minX, layout.selectedBounds.minY, layout.padding.x, layout.padding.y);
|
|
1507
|
-
const visualSize = calculateVisualSize(def.size, layout.cellSize);
|
|
1508
|
-
const scale = visualSize / layout.cellSize;
|
|
1509
|
-
node.setAttribute("transform", `translate(${screen.screenX} ${screen.screenY}) scale(${scale})`);
|
|
1510
|
-
}
|
|
1511
|
-
function applyAmbientClasses(state, ambient) {
|
|
1512
|
-
const next = new Set((ambient ?? []).map((item) => item.name));
|
|
1513
|
-
for (const name of state.ambient) {
|
|
1514
|
-
if (!next.has(name))
|
|
1515
|
-
state.node.classList.remove(`iso-ambient-${name}`);
|
|
1516
|
-
}
|
|
1517
|
-
for (const name of next) {
|
|
1518
|
-
if (!state.ambient.has(name))
|
|
1519
|
-
state.node.classList.add(`iso-ambient-${name}`);
|
|
1520
|
-
}
|
|
1521
|
-
state.ambient = next;
|
|
1522
|
-
}
|
|
1523
|
-
function applyConnectorAmbientClasses(state, ambient) {
|
|
1524
|
-
const next = new Set((ambient ?? []).map((item) => item.name));
|
|
1525
|
-
for (const name of state.ambient) {
|
|
1526
|
-
if (!next.has(name))
|
|
1527
|
-
state.shaft.classList.remove(`iso-ambient-${name}`);
|
|
1528
|
-
}
|
|
1529
|
-
for (const name of next) {
|
|
1530
|
-
if (!state.ambient.has(name))
|
|
1531
|
-
state.shaft.classList.add(`iso-ambient-${name}`);
|
|
1532
|
-
}
|
|
1533
|
-
state.ambient = next;
|
|
1534
|
-
}
|
|
1535
|
-
/** Apply a CSS keyframe animation to an element. */
|
|
1536
|
-
function animateElement(node, keyframeName, type = "enter") {
|
|
1537
|
-
node.style.opacity = "1";
|
|
1538
|
-
node.style.animation = "none";
|
|
1539
|
-
node.getBoundingClientRect();
|
|
1540
|
-
const duration = type === "exit" ? "var(--iso-anim-duration-exit, 300ms)" : "var(--iso-anim-duration-enter, 400ms)";
|
|
1541
|
-
const easing = type === "exit" ? "var(--iso-anim-easing-exit, ease-in)" : "var(--iso-anim-easing-enter, ease-out)";
|
|
1542
|
-
node.style.animation = `${keyframeName} ${duration} ${easing} forwards`;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
/**
|
|
1546
|
-
* Linear easing (no interpolation).
|
|
1547
|
-
*/
|
|
1548
|
-
function linear(t) {
|
|
1549
|
-
return t;
|
|
1550
|
-
}
|
|
1551
|
-
/**
|
|
1552
|
-
* Cubic ease-in: starts slowly, accelerates.
|
|
1553
|
-
*/
|
|
1554
|
-
function easeInCubic(t) {
|
|
1555
|
-
return t * t * t;
|
|
1556
|
-
}
|
|
1557
|
-
/**
|
|
1558
|
-
* Cubic ease-out: starts fast, decelerates.
|
|
1559
|
-
*/
|
|
1560
|
-
function easeOutCubic(t) {
|
|
1561
|
-
return 1 - (1 - t) ** 3;
|
|
1562
|
-
}
|
|
1563
|
-
/**
|
|
1564
|
-
* Cubic ease-in-out: slow start, fast middle, slow end.
|
|
1565
|
-
*/
|
|
1566
|
-
function easeInOutCubic(t) {
|
|
1567
|
-
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
1568
|
-
}
|
|
1569
|
-
/**
|
|
1570
|
-
* Resolve an easing type string to a function.
|
|
1571
|
-
*/
|
|
1572
|
-
function resolveEasing(type) {
|
|
1573
|
-
switch (type) {
|
|
1574
|
-
case "linear":
|
|
1575
|
-
return linear;
|
|
1576
|
-
case "easeInCubic":
|
|
1577
|
-
return easeInCubic;
|
|
1578
|
-
case "easeOutCubic":
|
|
1579
|
-
return easeOutCubic;
|
|
1580
|
-
case "easeInOutCubic":
|
|
1581
|
-
return easeInOutCubic;
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
const DEFAULT_CONFIG = {
|
|
1586
|
-
scrollDirection: "vertical",
|
|
1587
|
-
scrollOffset: {},
|
|
1588
|
-
minProgress: 0,
|
|
1589
|
-
maxProgress: 1,
|
|
1590
|
-
keyboardControls: false,
|
|
1591
|
-
touchControls: false,
|
|
1592
|
-
scrollSensitivity: 1.0,
|
|
1593
|
-
transitionDuration: 600,
|
|
1594
|
-
transitionEasing: "ease-in-out",
|
|
1595
|
-
};
|
|
1596
|
-
// ── Controller implementation ──────────────────────────────────────────────
|
|
1597
|
-
/**
|
|
1598
|
-
* Animation controller — manages scroll progress, scene navigation,
|
|
1599
|
-
* and delegates to the animation engine for frame updates.
|
|
1600
|
-
*/
|
|
1601
|
-
class AnimationController {
|
|
1602
|
-
_engine = new AnimationEngine();
|
|
1603
|
-
_bundle = null;
|
|
1604
|
-
_sceneIndex = 0;
|
|
1605
|
-
_progress = 0;
|
|
1606
|
-
_paused = false;
|
|
1607
|
-
_config = DEFAULT_CONFIG;
|
|
1608
|
-
_container = null;
|
|
1609
|
-
_sceneElement = null;
|
|
1610
|
-
_ownsEngine = true;
|
|
1611
|
-
_rafId = null;
|
|
1612
|
-
_pendingProgress = null;
|
|
1613
|
-
_destroyed = false;
|
|
1614
|
-
_listeners = new Map();
|
|
1615
|
-
// Scroll tracking state
|
|
1616
|
-
_minScroll = 0;
|
|
1617
|
-
_maxScroll = 0;
|
|
1618
|
-
_touchStartY = 0;
|
|
1619
|
-
_touchStartX = 0;
|
|
1620
|
-
_isDragging = false;
|
|
1621
|
-
// Transition animation state
|
|
1622
|
-
_transitionAnim = null;
|
|
1623
|
-
get engine() {
|
|
1624
|
-
return this._engine;
|
|
1625
|
-
}
|
|
1626
|
-
get progress() {
|
|
1627
|
-
return this._progress;
|
|
1628
|
-
}
|
|
1629
|
-
getProgress() {
|
|
1630
|
-
this._assertNotDestroyed();
|
|
1631
|
-
return this._progress;
|
|
1632
|
-
}
|
|
1633
|
-
get sceneIndex() {
|
|
1634
|
-
return this._sceneIndex;
|
|
1635
|
-
}
|
|
1636
|
-
getSceneIndex() {
|
|
1637
|
-
this._assertNotDestroyed();
|
|
1638
|
-
return this._sceneIndex;
|
|
1639
|
-
}
|
|
1640
|
-
get scenes() {
|
|
1641
|
-
return this._bundle?.scenes ?? [];
|
|
1642
|
-
}
|
|
1643
|
-
get paused() {
|
|
1644
|
-
return this._paused;
|
|
1645
|
-
}
|
|
1646
|
-
get currentScene() {
|
|
1647
|
-
return this.scenes[this._sceneIndex];
|
|
1648
|
-
}
|
|
1649
|
-
/**
|
|
1650
|
-
* Initialize the controller with a compiled bundle and optional runtime resources.
|
|
1651
|
-
*/
|
|
1652
|
-
init(bundle, config = {}, runtime = {}) {
|
|
1653
|
-
this._assertNotDestroyed();
|
|
1654
|
-
if (!bundle.scenes || bundle.scenes.length === 0) {
|
|
1655
|
-
throw new ControllerError("CONTROLLER_NO_SCENES", "init() requires at least one compiled scene stop");
|
|
1656
|
-
}
|
|
1657
|
-
this._cancelFrame();
|
|
1658
|
-
this._bundle = bundle;
|
|
1659
|
-
this._engine = runtime.engine ?? new AnimationEngine();
|
|
1660
|
-
this._ownsEngine = !runtime.engine;
|
|
1661
|
-
this._sceneIndex = 0;
|
|
1662
|
-
this._progress = 0;
|
|
1663
|
-
this._paused = false;
|
|
1664
|
-
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
1665
|
-
this._sceneElement = runtime.sceneElement ?? config.sceneElement ?? null;
|
|
1666
|
-
this._engine.init(bundle);
|
|
1667
|
-
this._bindScroll();
|
|
1668
|
-
}
|
|
1669
|
-
/**
|
|
1670
|
-
* Set scroll progress (0–1, clamped) and trigger frame update.
|
|
1671
|
-
*/
|
|
1672
|
-
setProgress(progress) {
|
|
1673
|
-
this._assertNotDestroyed();
|
|
1674
|
-
if (!Number.isFinite(progress)) {
|
|
1675
|
-
throw new ControllerError("CONTROLLER_PROGRESS_OUT_OF_RANGE", "setProgress() requires a finite progress value");
|
|
1676
|
-
}
|
|
1677
|
-
const clamped = Math.max(0, Math.min(1, progress));
|
|
1678
|
-
if (clamped === this._progress && !this._paused && this._rafId !== null) {
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
|
-
this._progress = clamped;
|
|
1682
|
-
if (this._paused)
|
|
1683
|
-
return;
|
|
1684
|
-
this._scheduleProgressForward(clamped);
|
|
1685
|
-
}
|
|
1686
|
-
/**
|
|
1687
|
-
* Navigate to next scene (wraps to 0 if at end).
|
|
1688
|
-
*/
|
|
1689
|
-
nextScene() {
|
|
1690
|
-
this._assertNotDestroyed();
|
|
1691
|
-
if (this.scenes.length <= 1)
|
|
1692
|
-
return;
|
|
1693
|
-
const nextIndex = (this._sceneIndex + 1) % this.scenes.length;
|
|
1694
|
-
this._transitionToScene(nextIndex);
|
|
1695
|
-
}
|
|
1696
|
-
/**
|
|
1697
|
-
* Navigate to previous scene (wraps to last if at beginning).
|
|
1698
|
-
*/
|
|
1699
|
-
prevScene() {
|
|
1700
|
-
this._assertNotDestroyed();
|
|
1701
|
-
if (this.scenes.length <= 1)
|
|
1702
|
-
return;
|
|
1703
|
-
const prevIndex = (this._sceneIndex - 1 + this.scenes.length) % this.scenes.length;
|
|
1704
|
-
this._transitionToScene(prevIndex);
|
|
1705
|
-
}
|
|
1706
|
-
/**
|
|
1707
|
-
* Set scene index directly.
|
|
1708
|
-
*/
|
|
1709
|
-
setSceneIndex(index) {
|
|
1710
|
-
this._assertNotDestroyed();
|
|
1711
|
-
if (index < 0 || index >= this.scenes.length) {
|
|
1712
|
-
throw new ControllerError("CONTROLLER_SCENE_INDEX_OUT_OF_RANGE", `Scene index ${index} is out of bounds [0, ${this.scenes.length - 1}]`);
|
|
1713
|
-
}
|
|
1714
|
-
this._transitionToScene(index);
|
|
1715
|
-
}
|
|
1716
|
-
/**
|
|
1717
|
-
* Pause all animations.
|
|
1718
|
-
*/
|
|
1719
|
-
pause() {
|
|
1720
|
-
this._assertNotDestroyed();
|
|
1721
|
-
if (this._paused)
|
|
1722
|
-
return;
|
|
1723
|
-
this._paused = true;
|
|
1724
|
-
this._cancelFrame();
|
|
1725
|
-
this._engine.pause();
|
|
1726
|
-
this._applyPauseState(true);
|
|
1727
|
-
this._emit("paused");
|
|
1728
|
-
}
|
|
1729
|
-
/**
|
|
1730
|
-
* Resume from paused state.
|
|
1731
|
-
*/
|
|
1732
|
-
resume() {
|
|
1733
|
-
this._assertNotDestroyed();
|
|
1734
|
-
if (!this._paused)
|
|
1735
|
-
return;
|
|
1736
|
-
this._paused = false;
|
|
1737
|
-
this._engine.resume();
|
|
1738
|
-
this._applyPauseState(false);
|
|
1739
|
-
this._scheduleProgressForward(this._progress);
|
|
1740
|
-
this._emit("resumed");
|
|
1741
|
-
}
|
|
1742
|
-
/**
|
|
1743
|
-
* Check if controller is paused.
|
|
1744
|
-
*/
|
|
1745
|
-
isPaused() {
|
|
1746
|
-
this._assertNotDestroyed();
|
|
1747
|
-
return this._paused;
|
|
1748
|
-
}
|
|
1749
|
-
/**
|
|
1750
|
-
* Destroy controller and clean up all listeners and resources.
|
|
1751
|
-
*/
|
|
1752
|
-
destroy() {
|
|
1753
|
-
this._assertNotDestroyed();
|
|
1754
|
-
this._unbindScroll();
|
|
1755
|
-
this._cancelFrame();
|
|
1756
|
-
this._cancelTransition();
|
|
1757
|
-
if (this._ownsEngine)
|
|
1758
|
-
this._engine.destroy();
|
|
1759
|
-
this._listeners.clear();
|
|
1760
|
-
this._bundle = null;
|
|
1761
|
-
this._container = null;
|
|
1762
|
-
this._pendingProgress = null;
|
|
1763
|
-
this._destroyed = true;
|
|
1764
|
-
}
|
|
1765
|
-
// ── Event system ───────────────────────────────────────────────────────
|
|
1766
|
-
on(event, listener) {
|
|
1767
|
-
this._assertNotDestroyed();
|
|
1768
|
-
const set = this._listeners.get(event) ?? new Set();
|
|
1769
|
-
set.add(listener);
|
|
1770
|
-
this._listeners.set(event, set);
|
|
1771
|
-
}
|
|
1772
|
-
off(event, listener) {
|
|
1773
|
-
this._assertNotDestroyed();
|
|
1774
|
-
const set = this._listeners.get(event);
|
|
1775
|
-
if (set) {
|
|
1776
|
-
set.delete(listener);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
_emit(event, ...args) {
|
|
1780
|
-
const set = this._listeners.get(event);
|
|
1781
|
-
if (set) {
|
|
1782
|
-
for (const listener of set) {
|
|
1783
|
-
try {
|
|
1784
|
-
listener(...args);
|
|
1785
|
-
}
|
|
1786
|
-
catch (error) {
|
|
1787
|
-
queueMicrotask(() => {
|
|
1788
|
-
throw error;
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
// ── Frame update ───────────────────────────────────────────────────────
|
|
1795
|
-
_scheduleProgressForward(progress) {
|
|
1796
|
-
this._pendingProgress = progress;
|
|
1797
|
-
if (this._rafId !== null)
|
|
1798
|
-
return;
|
|
1799
|
-
this._rafId = requestAnimationFrame(() => {
|
|
1800
|
-
this._rafId = null;
|
|
1801
|
-
const pending = this._pendingProgress;
|
|
1802
|
-
this._pendingProgress = null;
|
|
1803
|
-
if (pending === null || this._paused || this._destroyed)
|
|
1804
|
-
return;
|
|
1805
|
-
this._engine.setProgress(pending);
|
|
1806
|
-
this._applyFrameUpdate();
|
|
1807
|
-
this._emit("progress-change", pending);
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
_cancelFrame() {
|
|
1811
|
-
if (this._rafId !== null) {
|
|
1812
|
-
cancelAnimationFrame(this._rafId);
|
|
1813
|
-
this._rafId = null;
|
|
1814
|
-
}
|
|
1815
|
-
this._pendingProgress = null;
|
|
1816
|
-
}
|
|
1817
|
-
_applyFrameUpdate() {
|
|
1818
|
-
if (!this._bundle)
|
|
1819
|
-
return;
|
|
1820
|
-
const svg = (this._sceneElement ?? this._container?.querySelector("svg") ?? null);
|
|
1821
|
-
if (!svg)
|
|
1822
|
-
return;
|
|
1823
|
-
const updates = this._engine.getFrameUpdates().map((update) => ({
|
|
1824
|
-
id: update.id,
|
|
1825
|
-
asset: update.asset,
|
|
1826
|
-
pos: update.pos,
|
|
1827
|
-
size: update.size,
|
|
1828
|
-
layer: update.layer,
|
|
1829
|
-
presence: update.lifecycle,
|
|
1830
|
-
enter: update.entry,
|
|
1831
|
-
exit: update.exit,
|
|
1832
|
-
ambient: update.ambient,
|
|
1833
|
-
text: update.text,
|
|
1834
|
-
primitive: update.primitive,
|
|
1835
|
-
}));
|
|
1836
|
-
const connectors = this._engine.getConnectorFrameUpdates().map((update) => ({
|
|
1837
|
-
id: update.id,
|
|
1838
|
-
route: update.route,
|
|
1839
|
-
layer: update.layer,
|
|
1840
|
-
presence: update.lifecycle,
|
|
1841
|
-
style: update.style,
|
|
1842
|
-
start: update.start,
|
|
1843
|
-
end: update.end,
|
|
1844
|
-
direction: update.direction,
|
|
1845
|
-
enter: update.entry,
|
|
1846
|
-
exit: update.exit,
|
|
1847
|
-
ambient: update.ambient,
|
|
1848
|
-
}));
|
|
1849
|
-
updateElementTransforms(svg, updates, connectors);
|
|
1850
|
-
this._applyLifecycleChanges(updates);
|
|
1851
|
-
this._applyConnectorLifecycleChanges(connectors);
|
|
1852
|
-
}
|
|
1853
|
-
_applyLifecycleChanges(elements) {
|
|
1854
|
-
for (const elDef of elements) {
|
|
1855
|
-
const transition = this._engine.getLifecycleTransition(elDef.id);
|
|
1856
|
-
if (!transition)
|
|
1857
|
-
continue;
|
|
1858
|
-
const svgForState = (this._sceneElement ?? this._container?.querySelector("svg"));
|
|
1859
|
-
if (!svgForState)
|
|
1860
|
-
continue;
|
|
1861
|
-
const state = getElementState(svgForState, elDef.id);
|
|
1862
|
-
if (!state)
|
|
1863
|
-
continue;
|
|
1864
|
-
if (transition.to === "entering" || transition.to === "present") {
|
|
1865
|
-
state.isHidden = false;
|
|
1866
|
-
unhideElementOnReadd(state.node);
|
|
1867
|
-
}
|
|
1868
|
-
if (isForwardEntryTransition(transition)) {
|
|
1869
|
-
this._applyEntryAnimation(elDef, state);
|
|
1870
|
-
}
|
|
1871
|
-
if (isReverseExitTransition(transition)) {
|
|
1872
|
-
this._applyExitAnimation({ ...elDef, exit: oppositeExitAnimation(elDef.enter ?? "fade-in") }, state);
|
|
1873
|
-
continue;
|
|
1874
|
-
}
|
|
1875
|
-
if (isForwardExitTransition(transition)) {
|
|
1876
|
-
this._applyExitAnimation(elDef, state);
|
|
1877
|
-
}
|
|
1878
|
-
if (isReverseEntryTransition(transition)) {
|
|
1879
|
-
state.isHidden = false;
|
|
1880
|
-
unhideElementOnReadd(state.node);
|
|
1881
|
-
this._applyEntryAnimation({ ...elDef, enter: oppositeEntryAnimation(elDef.exit ?? "fade-out") }, state);
|
|
1882
|
-
continue;
|
|
1883
|
-
}
|
|
1884
|
-
if (transition.to === "removed") {
|
|
1885
|
-
state.isHidden = true;
|
|
1886
|
-
hideElementAfterExit(state.node);
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
_applyConnectorLifecycleChanges(connectors) {
|
|
1891
|
-
for (const connectorDef of connectors) {
|
|
1892
|
-
const transition = this._engine.getConnectorLifecycleTransition(connectorDef.id);
|
|
1893
|
-
if (!transition)
|
|
1894
|
-
continue;
|
|
1895
|
-
const svgForState = (this._sceneElement ?? this._container?.querySelector("svg"));
|
|
1896
|
-
if (!svgForState)
|
|
1897
|
-
continue;
|
|
1898
|
-
const state = getConnectorState(svgForState, connectorDef.id);
|
|
1899
|
-
if (!state)
|
|
1900
|
-
continue;
|
|
1901
|
-
if (transition.to === "entering" || transition.to === "present") {
|
|
1902
|
-
state.isHidden = false;
|
|
1903
|
-
unhideElementOnReadd(state.node);
|
|
1904
|
-
}
|
|
1905
|
-
if (isForwardEntryTransition(transition)) {
|
|
1906
|
-
this._applyConnectorEntryAnimation(connectorDef, state);
|
|
1907
|
-
}
|
|
1908
|
-
if (isReverseExitTransition(transition)) {
|
|
1909
|
-
this._applyConnectorExitAnimation({
|
|
1910
|
-
...connectorDef,
|
|
1911
|
-
exit: oppositeExitAnimation(connectorDef.enter ?? "fade-in"),
|
|
1912
|
-
}, state);
|
|
1913
|
-
continue;
|
|
1914
|
-
}
|
|
1915
|
-
if (isForwardExitTransition(transition)) {
|
|
1916
|
-
this._applyConnectorExitAnimation(connectorDef, state);
|
|
1917
|
-
}
|
|
1918
|
-
if (isReverseEntryTransition(transition)) {
|
|
1919
|
-
state.isHidden = false;
|
|
1920
|
-
unhideElementOnReadd(state.node);
|
|
1921
|
-
this._applyConnectorEntryAnimation({
|
|
1922
|
-
...connectorDef,
|
|
1923
|
-
enter: oppositeEntryAnimation(connectorDef.exit ?? "fade-out"),
|
|
1924
|
-
}, state);
|
|
1925
|
-
continue;
|
|
1926
|
-
}
|
|
1927
|
-
if (transition.to === "removed") {
|
|
1928
|
-
state.isHidden = true;
|
|
1929
|
-
hideElementAfterExit(state.node);
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
_applyEntryAnimation(elDef, state) {
|
|
1934
|
-
const entryAnim = elDef.enter ?? "fade-in";
|
|
1935
|
-
if (entryAnim === "none")
|
|
1936
|
-
return;
|
|
1937
|
-
animateElement(state.node, `iso-anim-${entryAnim}`, "enter");
|
|
1938
|
-
state.node.addEventListener("animationend", () => {
|
|
1939
|
-
state.node.style.animation = "";
|
|
1940
|
-
}, { once: true });
|
|
1941
|
-
}
|
|
1942
|
-
_applyExitAnimation(elDef, state) {
|
|
1943
|
-
const exitAnim = elDef.exit ?? "fade-out";
|
|
1944
|
-
if (exitAnim === "none") {
|
|
1945
|
-
hideElementAfterExit(state.node);
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
animateElement(state.node, `iso-anim-${exitAnim}`, "exit");
|
|
1949
|
-
state.node.addEventListener("animationend", () => {
|
|
1950
|
-
hideElementAfterExit(state.node);
|
|
1951
|
-
}, { once: true });
|
|
1952
|
-
}
|
|
1953
|
-
_applyConnectorEntryAnimation(connectorDef, state) {
|
|
1954
|
-
const entryAnim = connectorDef.enter ?? "fade-in";
|
|
1955
|
-
if (entryAnim === "none")
|
|
1956
|
-
return;
|
|
1957
|
-
animateElement(state.node, `iso-anim-${entryAnim}`, "enter");
|
|
1958
|
-
state.node.addEventListener("animationend", () => {
|
|
1959
|
-
state.node.style.animation = "";
|
|
1960
|
-
}, { once: true });
|
|
1961
|
-
}
|
|
1962
|
-
_applyConnectorExitAnimation(connectorDef, state) {
|
|
1963
|
-
const exitAnim = connectorDef.exit ?? "fade-out";
|
|
1964
|
-
if (exitAnim === "none") {
|
|
1965
|
-
hideElementAfterExit(state.node);
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
animateElement(state.node, `iso-anim-${exitAnim}`, "exit");
|
|
1969
|
-
state.node.addEventListener("animationend", () => {
|
|
1970
|
-
hideElementAfterExit(state.node);
|
|
1971
|
-
}, { once: true });
|
|
1972
|
-
}
|
|
1973
|
-
// ── Scene transitions ──────────────────────────────────────────────────
|
|
1974
|
-
_transitionToScene(index) {
|
|
1975
|
-
this._cancelTransition();
|
|
1976
|
-
if (index === this._sceneIndex)
|
|
1977
|
-
return;
|
|
1978
|
-
this._sceneIndex = index;
|
|
1979
|
-
this._emit("scene-change", index);
|
|
1980
|
-
const stop = this.scenes[index];
|
|
1981
|
-
const from = this._progress;
|
|
1982
|
-
const to = stop.progress;
|
|
1983
|
-
const duration = this._config.transitionDuration;
|
|
1984
|
-
if (duration > 0 && from !== to) {
|
|
1985
|
-
this._animateProgress(from, to, duration);
|
|
1986
|
-
return;
|
|
1987
|
-
}
|
|
1988
|
-
this._progress = to;
|
|
1989
|
-
this._scheduleProgressForward(to);
|
|
1990
|
-
}
|
|
1991
|
-
_animateProgress(from, to, duration) {
|
|
1992
|
-
const easing = resolveEasing((this._config.transitionEasing === "ease-in-out"
|
|
1993
|
-
? "easeInOutCubic"
|
|
1994
|
-
: this._config.transitionEasing === "ease-out"
|
|
1995
|
-
? "easeOutCubic"
|
|
1996
|
-
: "linear"));
|
|
1997
|
-
const start = performance.now();
|
|
1998
|
-
const step = (now) => {
|
|
1999
|
-
const elapsed = now - start;
|
|
2000
|
-
const t = Math.min(elapsed / duration, 1);
|
|
2001
|
-
const easedT = easing(t);
|
|
2002
|
-
const currentProgress = from + (to - from) * easedT;
|
|
2003
|
-
this.setProgress(currentProgress);
|
|
2004
|
-
if (t < 1) {
|
|
2005
|
-
this._transitionAnim = requestAnimationFrame(step);
|
|
2006
|
-
}
|
|
2007
|
-
else {
|
|
2008
|
-
this._transitionAnim = null;
|
|
2009
|
-
}
|
|
2010
|
-
};
|
|
2011
|
-
this._transitionAnim = requestAnimationFrame(step);
|
|
2012
|
-
}
|
|
2013
|
-
_cancelTransition() {
|
|
2014
|
-
if (this._transitionAnim !== null) {
|
|
2015
|
-
cancelAnimationFrame(this._transitionAnim);
|
|
2016
|
-
this._transitionAnim = null;
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
// ── Scroll binding ─────────────────────────────────────────────────────
|
|
2020
|
-
_bindScroll() {
|
|
2021
|
-
const container = this._config.container;
|
|
2022
|
-
if (!container)
|
|
2023
|
-
return;
|
|
2024
|
-
this._container = container;
|
|
2025
|
-
this._calculateScrollBounds();
|
|
2026
|
-
container.addEventListener("scroll", this._onScroll, { passive: true });
|
|
2027
|
-
window.addEventListener("resize", this._onResize, { passive: true });
|
|
2028
|
-
if (this._config.keyboardControls) {
|
|
2029
|
-
document.addEventListener("keydown", this._onKeyDown);
|
|
2030
|
-
}
|
|
2031
|
-
if (this._config.touchControls) {
|
|
2032
|
-
container.addEventListener("touchstart", this._onTouchStart, {
|
|
2033
|
-
passive: true,
|
|
2034
|
-
});
|
|
2035
|
-
container.addEventListener("touchmove", this._onTouchMove, {
|
|
2036
|
-
passive: false,
|
|
2037
|
-
});
|
|
2038
|
-
container.addEventListener("touchend", this._onTouchEnd);
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
_unbindScroll() {
|
|
2042
|
-
const container = this._config.container;
|
|
2043
|
-
if (!container)
|
|
2044
|
-
return;
|
|
2045
|
-
container.removeEventListener("scroll", this._onScroll);
|
|
2046
|
-
window.removeEventListener("resize", this._onResize);
|
|
2047
|
-
document.removeEventListener("keydown", this._onKeyDown);
|
|
2048
|
-
container.removeEventListener("touchstart", this._onTouchStart);
|
|
2049
|
-
container.removeEventListener("touchmove", this._onTouchMove);
|
|
2050
|
-
container.removeEventListener("touchend", this._onTouchEnd);
|
|
2051
|
-
}
|
|
2052
|
-
_calculateScrollBounds() {
|
|
2053
|
-
const container = this._config.container;
|
|
2054
|
-
if (!container)
|
|
2055
|
-
return;
|
|
2056
|
-
const offset = this._config.scrollOffset ?? {};
|
|
2057
|
-
if (this._config.scrollDirection === "horizontal") {
|
|
2058
|
-
this._minScroll = offset.left ?? 0;
|
|
2059
|
-
this._maxScroll = container.scrollWidth - container.clientWidth - (offset.right ?? 0);
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
|
-
this._minScroll = offset.top ?? 0;
|
|
2063
|
-
this._maxScroll = container.scrollHeight - container.clientHeight - (offset.bottom ?? 0);
|
|
2064
|
-
}
|
|
2065
|
-
_onScroll = () => {
|
|
2066
|
-
if (this._paused || this._destroyed)
|
|
2067
|
-
return;
|
|
2068
|
-
const container = this._config.container;
|
|
2069
|
-
if (!container)
|
|
2070
|
-
return;
|
|
2071
|
-
const currentScroll = this._config.scrollDirection === "horizontal" ? container.scrollLeft : container.scrollTop;
|
|
2072
|
-
const range = this._maxScroll - this._minScroll;
|
|
2073
|
-
if (range <= 0)
|
|
2074
|
-
return;
|
|
2075
|
-
const rawProgress = (currentScroll - this._minScroll) / range;
|
|
2076
|
-
const clampedProgress = Math.max(this._config.minProgress ?? 0, Math.min(this._config.maxProgress ?? 1, rawProgress));
|
|
2077
|
-
const sensitivity = this._config.scrollSensitivity ?? 1;
|
|
2078
|
-
if (clampedProgress !== this._progress) {
|
|
2079
|
-
this.setProgress(clampedProgress * sensitivity);
|
|
2080
|
-
}
|
|
2081
|
-
};
|
|
2082
|
-
_onResize = () => {
|
|
2083
|
-
if (this._destroyed)
|
|
2084
|
-
return;
|
|
2085
|
-
this._calculateScrollBounds();
|
|
2086
|
-
};
|
|
2087
|
-
_onKeyDown = (e) => {
|
|
2088
|
-
if (this._destroyed)
|
|
2089
|
-
return;
|
|
2090
|
-
if (e.key === "ArrowRight" || e.key === "ArrowDown" || e.key === " ") {
|
|
2091
|
-
e.preventDefault();
|
|
2092
|
-
this.nextScene();
|
|
2093
|
-
}
|
|
2094
|
-
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
2095
|
-
e.preventDefault();
|
|
2096
|
-
this.prevScene();
|
|
2097
|
-
}
|
|
2098
|
-
};
|
|
2099
|
-
_onTouchStart = (e) => {
|
|
2100
|
-
if (this._destroyed)
|
|
2101
|
-
return;
|
|
2102
|
-
this._isDragging = true;
|
|
2103
|
-
const touch = e.touches[0];
|
|
2104
|
-
if (this._config.scrollDirection === "horizontal") {
|
|
2105
|
-
this._touchStartX = touch.clientX;
|
|
2106
|
-
}
|
|
2107
|
-
else {
|
|
2108
|
-
this._touchStartY = touch.clientY;
|
|
2109
|
-
}
|
|
2110
|
-
};
|
|
2111
|
-
_onTouchMove = (e) => {
|
|
2112
|
-
if (this._destroyed)
|
|
2113
|
-
return;
|
|
2114
|
-
if (!this._isDragging)
|
|
2115
|
-
return;
|
|
2116
|
-
const touch = e.touches[0];
|
|
2117
|
-
const delta = this._config.scrollDirection === "horizontal"
|
|
2118
|
-
? this._touchStartX - touch.clientX
|
|
2119
|
-
: this._touchStartY - touch.clientY;
|
|
2120
|
-
const sensitivity = this._config.scrollSensitivity ?? 1.0;
|
|
2121
|
-
const progressDelta = (delta / 300) * sensitivity;
|
|
2122
|
-
const newProgress = Math.max(0, Math.min(1, this._progress + progressDelta));
|
|
2123
|
-
this.setProgress(newProgress);
|
|
2124
|
-
};
|
|
2125
|
-
_onTouchEnd = () => {
|
|
2126
|
-
if (this._destroyed)
|
|
2127
|
-
return;
|
|
2128
|
-
this._isDragging = false;
|
|
2129
|
-
};
|
|
2130
|
-
// ── Pause state ────────────────────────────────────────────────────────
|
|
2131
|
-
_applyPauseState(pause) {
|
|
2132
|
-
const container = this._config.container;
|
|
2133
|
-
if (!container)
|
|
2134
|
-
return;
|
|
2135
|
-
const svg = container.querySelector("svg");
|
|
2136
|
-
if (!svg)
|
|
2137
|
-
return;
|
|
2138
|
-
const playState = pause ? "paused" : "running";
|
|
2139
|
-
const ambientElements = svg.querySelectorAll('[class*="iso-ambient-"]');
|
|
2140
|
-
for (let i = 0; i < ambientElements.length; i++) {
|
|
2141
|
-
const el = ambientElements[i];
|
|
2142
|
-
el.style.animationPlayState = playState;
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
_assertNotDestroyed() {
|
|
2146
|
-
if (!this._destroyed)
|
|
2147
|
-
return;
|
|
2148
|
-
throw new ControllerError("CONTROLLER_DESTROYED", "AnimationController has been destroyed");
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
function isForwardEntryTransition(transition) {
|
|
2152
|
-
return transition.from === "removed" && transition.to === "entering";
|
|
2153
|
-
}
|
|
2154
|
-
function isForwardExitTransition(transition) {
|
|
2155
|
-
return transition.to === "exiting";
|
|
2156
|
-
}
|
|
2157
|
-
function isReverseExitTransition(transition) {
|
|
2158
|
-
return transition.to === "removed" && transition.from !== "exiting";
|
|
2159
|
-
}
|
|
2160
|
-
function isReverseEntryTransition(transition) {
|
|
2161
|
-
return transition.from === "exiting" && transition.to !== "removed";
|
|
2162
|
-
}
|
|
2163
|
-
function oppositeExitAnimation(entry) {
|
|
2164
|
-
switch (entry) {
|
|
2165
|
-
case "fade-in":
|
|
2166
|
-
return "fade-out";
|
|
2167
|
-
case "fade-in-grow":
|
|
2168
|
-
return "fade-out-shrink";
|
|
2169
|
-
case "fall-in":
|
|
2170
|
-
return "rise-away";
|
|
2171
|
-
case "rise-from-ground":
|
|
2172
|
-
return "fall-through-ground";
|
|
2173
|
-
case "slide-in-left":
|
|
2174
|
-
return "slide-out-left";
|
|
2175
|
-
case "slide-in-right":
|
|
2176
|
-
return "slide-out-right";
|
|
2177
|
-
case "flip-in":
|
|
2178
|
-
return "flip-out";
|
|
2179
|
-
case "none":
|
|
2180
|
-
return "none";
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
function oppositeEntryAnimation(exit) {
|
|
2184
|
-
switch (exit) {
|
|
2185
|
-
case "fade-out":
|
|
2186
|
-
return "fade-in";
|
|
2187
|
-
case "fade-out-shrink":
|
|
2188
|
-
return "fade-in-grow";
|
|
2189
|
-
case "fall-through-ground":
|
|
2190
|
-
return "rise-from-ground";
|
|
2191
|
-
case "rise-away":
|
|
2192
|
-
return "fall-in";
|
|
2193
|
-
case "slide-out-left":
|
|
2194
|
-
return "slide-in-left";
|
|
2195
|
-
case "slide-out-right":
|
|
2196
|
-
return "slide-in-right";
|
|
2197
|
-
case "flip-out":
|
|
2198
|
-
return "flip-in";
|
|
2199
|
-
case "none":
|
|
2200
|
-
return "none";
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
const RUNTIME_BUNDLE_FORMAT = "isostate-runtime-bundle";
|
|
2205
|
-
const RUNTIME_VERSION = "0.1.0";
|
|
2206
|
-
const HEX_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
2207
|
-
/** Mount a compiled runtime bundle into an HTML element. */
|
|
2208
|
-
function mountScene(target, bundle, options = {}) {
|
|
2209
|
-
assertMountTarget(target);
|
|
2210
|
-
validateRuntimeBundle(bundle);
|
|
2211
|
-
const engine = new AnimationEngine();
|
|
2212
|
-
engine.init(bundle);
|
|
2213
|
-
const svg = buildSceneDOM(target, bundle, {
|
|
2214
|
-
label: options.label,
|
|
2215
|
-
themeVars: options.themeVars,
|
|
2216
|
-
});
|
|
2217
|
-
let controller;
|
|
2218
|
-
if (options.controller !== undefined && options.controller !== false) {
|
|
2219
|
-
controller = new AnimationController();
|
|
2220
|
-
controller.init(bundle, {
|
|
2221
|
-
...options.controller,
|
|
2222
|
-
container: options.controller.container ?? target,
|
|
2223
|
-
sceneElement: svg,
|
|
2224
|
-
}, {
|
|
2225
|
-
engine,
|
|
2226
|
-
sceneElement: svg,
|
|
2227
|
-
});
|
|
2228
|
-
}
|
|
2229
|
-
let destroyed = false;
|
|
2230
|
-
return {
|
|
2231
|
-
svg,
|
|
2232
|
-
engine,
|
|
2233
|
-
controller,
|
|
2234
|
-
getResolvedConfig: () => getResolvedConfig(bundle, options),
|
|
2235
|
-
destroy: () => {
|
|
2236
|
-
if (destroyed)
|
|
2237
|
-
return;
|
|
2238
|
-
destroyed = true;
|
|
2239
|
-
controller?.destroy();
|
|
2240
|
-
engine.destroy();
|
|
2241
|
-
if (svg.parentNode === target) {
|
|
2242
|
-
target.removeChild(svg);
|
|
2243
|
-
}
|
|
2244
|
-
else {
|
|
2245
|
-
svg.parentNode?.removeChild(svg);
|
|
2246
|
-
}
|
|
2247
|
-
},
|
|
2248
|
-
};
|
|
2249
|
-
}
|
|
2250
|
-
function assertMountTarget(target) {
|
|
2251
|
-
if (!target || typeof target.appendChild !== "function" || typeof target.removeChild !== "function") {
|
|
2252
|
-
throw new RenderError("INVALID_MOUNT_TARGET", "mountScene() requires a DOM HTMLElement target");
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
function validateRuntimeBundle(bundle) {
|
|
2256
|
-
if (!bundle || typeof bundle !== "object") {
|
|
2257
|
-
throw new RenderError("BUNDLE_FORMAT_MISSING", "Runtime bundle must be a plain object");
|
|
2258
|
-
}
|
|
2259
|
-
if (bundle._format !== RUNTIME_BUNDLE_FORMAT) {
|
|
2260
|
-
throw new RenderError("BUNDLE_FORMAT_MISSING", "Runtime bundle format is missing or unsupported", {
|
|
2261
|
-
expected: RUNTIME_BUNDLE_FORMAT,
|
|
2262
|
-
});
|
|
2263
|
-
}
|
|
2264
|
-
if (!Array.isArray(bundle.scenes) || bundle.scenes.length === 0) {
|
|
2265
|
-
throw new RenderError("BUNDLE_FORMAT_MISSING", "Runtime bundle must include compiled scenes[]");
|
|
2266
|
-
}
|
|
2267
|
-
if (majorVersion(bundle._version) !== majorVersion(RUNTIME_VERSION)) {
|
|
2268
|
-
throw new RenderError("BUNDLE_VERSION_MISMATCH", `Runtime bundle version ${bundle._version} is not compatible with runtime ${RUNTIME_VERSION}`, { bundleVersion: bundle._version, runtimeVersion: RUNTIME_VERSION });
|
|
2269
|
-
}
|
|
2270
|
-
if (!bundle._digest) {
|
|
2271
|
-
throw new RenderError("BUNDLE_DIGEST_MISSING", "Runtime bundle digest is missing");
|
|
2272
|
-
}
|
|
2273
|
-
if (typeof bundle._digest !== "string" || !HEX_DIGEST_PATTERN.test(bundle._digest)) {
|
|
2274
|
-
throw new RenderError("BUNDLE_DIGEST_MISMATCH", "Runtime bundle digest is malformed");
|
|
2275
|
-
}
|
|
2276
|
-
const { _digest, ...bundleWithoutDigest } = bundle;
|
|
2277
|
-
const actualDigest = sha256(canonicalStringify(bundleWithoutDigest));
|
|
2278
|
-
if (actualDigest !== _digest) {
|
|
2279
|
-
throw new RenderError("BUNDLE_DIGEST_MISMATCH", "Runtime bundle digest does not match bundle content", {
|
|
2280
|
-
expected: _digest,
|
|
2281
|
-
actual: actualDigest,
|
|
2282
|
-
});
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
function getResolvedConfig(bundle, options = {}) {
|
|
2286
|
-
return {
|
|
2287
|
-
grid: { cellSize: bundle.grid.cellSize },
|
|
2288
|
-
floor: { ...bundle.floor },
|
|
2289
|
-
layout: {
|
|
2290
|
-
...bundle.layout,
|
|
2291
|
-
padding: { ...bundle.layout.padding },
|
|
2292
|
-
align: [...bundle.layout.align],
|
|
2293
|
-
},
|
|
2294
|
-
viewBox: getResolvedViewBox(bundle),
|
|
2295
|
-
theme: bundle.theme,
|
|
2296
|
-
themeVars: getResolvedThemeVars(bundle, options.themeVars),
|
|
2297
|
-
scenes: bundle.scenes.map((scene) => ({
|
|
2298
|
-
id: scene.id,
|
|
2299
|
-
progress: scene.progress,
|
|
2300
|
-
})),
|
|
2301
|
-
layerOrder: bundle.layers
|
|
2302
|
-
.map((layer) => ({ name: layer.name, order: layer.order }))
|
|
2303
|
-
.sort((a, b) => a.order - b.order || a.name.localeCompare(b.name)),
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
function getResolvedThemeVars(bundle, overrides = {}) {
|
|
2307
|
-
return {
|
|
2308
|
-
...(resolveTheme(bundle.theme) ?? {}),
|
|
2309
|
-
...(bundle.themeVars ?? {}),
|
|
2310
|
-
...overrides,
|
|
2311
|
-
};
|
|
2312
|
-
}
|
|
2313
|
-
function majorVersion(version) {
|
|
2314
|
-
const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
|
|
2315
|
-
return Number.isFinite(major) ? major : Number.NaN;
|
|
2316
|
-
}
|
|
2317
|
-
function canonicalStringify(value) {
|
|
2318
|
-
return JSON.stringify(normalizeValue(value));
|
|
2319
|
-
}
|
|
2320
|
-
function normalizeValue(value) {
|
|
2321
|
-
if (Array.isArray(value)) {
|
|
2322
|
-
return value.map((item) => (item === undefined ? null : normalizeValue(item)));
|
|
2323
|
-
}
|
|
2324
|
-
if (!isPlainObject(value))
|
|
2325
|
-
return value;
|
|
2326
|
-
const normalized = {};
|
|
2327
|
-
for (const key of Object.keys(value).sort()) {
|
|
2328
|
-
const child = value[key];
|
|
2329
|
-
if (child !== undefined) {
|
|
2330
|
-
normalized[key] = normalizeValue(child);
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
return normalized;
|
|
2334
|
-
}
|
|
2335
|
-
function isPlainObject(value) {
|
|
2336
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2337
|
-
}
|
|
2338
|
-
function sha256(input) {
|
|
2339
|
-
const bytes = utf8Bytes(input);
|
|
2340
|
-
const bitLength = bytes.length * 8;
|
|
2341
|
-
bytes.push(0x80);
|
|
2342
|
-
while (bytes.length % 64 !== 56)
|
|
2343
|
-
bytes.push(0);
|
|
2344
|
-
for (let i = 7; i >= 0; i--) {
|
|
2345
|
-
bytes.push(Math.floor(bitLength / 2 ** (i * 8)) & 0xff);
|
|
2346
|
-
}
|
|
2347
|
-
const h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
2348
|
-
const k = [
|
|
2349
|
-
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
|
|
2350
|
-
0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
2351
|
-
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
|
|
2352
|
-
0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
2353
|
-
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
|
|
2354
|
-
0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
2355
|
-
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
|
|
2356
|
-
0xc67178f2,
|
|
2357
|
-
];
|
|
2358
|
-
const w = new Array(64);
|
|
2359
|
-
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
2360
|
-
for (let i = 0; i < 16; i++) {
|
|
2361
|
-
const j = offset + i * 4;
|
|
2362
|
-
w[i] = ((bytes[j] << 24) | (bytes[j + 1] << 16) | (bytes[j + 2] << 8) | bytes[j + 3]) >>> 0;
|
|
2363
|
-
}
|
|
2364
|
-
for (let i = 16; i < 64; i++) {
|
|
2365
|
-
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
2366
|
-
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
2367
|
-
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
2368
|
-
}
|
|
2369
|
-
let [a, b, c, d, e, f, g, hh] = h;
|
|
2370
|
-
for (let i = 0; i < 64; i++) {
|
|
2371
|
-
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
2372
|
-
const ch = (e & f) ^ (~e & g);
|
|
2373
|
-
const temp1 = (hh + s1 + ch + k[i] + w[i]) >>> 0;
|
|
2374
|
-
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
2375
|
-
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
2376
|
-
const temp2 = (s0 + maj) >>> 0;
|
|
2377
|
-
hh = g;
|
|
2378
|
-
g = f;
|
|
2379
|
-
f = e;
|
|
2380
|
-
e = (d + temp1) >>> 0;
|
|
2381
|
-
d = c;
|
|
2382
|
-
c = b;
|
|
2383
|
-
b = a;
|
|
2384
|
-
a = (temp1 + temp2) >>> 0;
|
|
2385
|
-
}
|
|
2386
|
-
h[0] = (h[0] + a) >>> 0;
|
|
2387
|
-
h[1] = (h[1] + b) >>> 0;
|
|
2388
|
-
h[2] = (h[2] + c) >>> 0;
|
|
2389
|
-
h[3] = (h[3] + d) >>> 0;
|
|
2390
|
-
h[4] = (h[4] + e) >>> 0;
|
|
2391
|
-
h[5] = (h[5] + f) >>> 0;
|
|
2392
|
-
h[6] = (h[6] + g) >>> 0;
|
|
2393
|
-
h[7] = (h[7] + hh) >>> 0;
|
|
2394
|
-
}
|
|
2395
|
-
return h.map((value) => value.toString(16).padStart(8, "0")).join("");
|
|
2396
|
-
}
|
|
2397
|
-
function utf8Bytes(input) {
|
|
2398
|
-
return Array.from(new TextEncoder().encode(input));
|
|
2399
|
-
}
|
|
2400
|
-
function rotr(value, bits) {
|
|
2401
|
-
return (value >>> bits) | (value << (32 - bits));
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
/** Runtime set of valid entry animation strings */
|
|
2405
|
-
const ENTRY_ANIMATIONS = new Set([
|
|
2406
|
-
"fade-in",
|
|
2407
|
-
"fade-in-grow",
|
|
2408
|
-
"fall-in",
|
|
2409
|
-
"rise-from-ground",
|
|
2410
|
-
"slide-in-left",
|
|
2411
|
-
"slide-in-right",
|
|
2412
|
-
"flip-in",
|
|
2413
|
-
"none",
|
|
2414
|
-
]);
|
|
2415
|
-
/** Runtime set of valid exit animation strings */
|
|
2416
|
-
const EXIT_ANIMATIONS = new Set([
|
|
2417
|
-
"fade-out",
|
|
2418
|
-
"fade-out-shrink",
|
|
2419
|
-
"fall-through-ground",
|
|
2420
|
-
"rise-away",
|
|
2421
|
-
"slide-out-left",
|
|
2422
|
-
"slide-out-right",
|
|
2423
|
-
"flip-out",
|
|
2424
|
-
"none",
|
|
2425
|
-
]);
|
|
2426
|
-
/** Runtime set of valid lifecycle status strings */
|
|
2427
|
-
const LIFECYCLE_STATUSES = new Set(["entering", "present", "exiting", "removed"]);
|
|
2428
|
-
// ── Type guards ──────────────────────────────────────────────────────────────
|
|
2429
|
-
/** Narrow a raw string to EntryAnimation. Returns undefined if invalid. */
|
|
2430
|
-
function guardEntryAnimation(v) {
|
|
2431
|
-
if (typeof v !== "string")
|
|
2432
|
-
return undefined;
|
|
2433
|
-
return ENTRY_ANIMATIONS.has(v) ? v : undefined;
|
|
2434
|
-
}
|
|
2435
|
-
/** Narrow a raw string to ExitAnimation. Returns undefined if invalid. */
|
|
2436
|
-
function guardExitAnimation(v) {
|
|
2437
|
-
if (typeof v !== "string")
|
|
2438
|
-
return undefined;
|
|
2439
|
-
return EXIT_ANIMATIONS.has(v) ? v : undefined;
|
|
2440
|
-
}
|
|
2441
|
-
/** Narrow a raw string to LifecycleStatus. Returns undefined if invalid. */
|
|
2442
|
-
function guardLifecycleStatus(v) {
|
|
2443
|
-
if (typeof v !== "string")
|
|
2444
|
-
return undefined;
|
|
2445
|
-
return LIFECYCLE_STATUSES.has(v) ? v : undefined;
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
export { AnimationController, AnimationEngine, AnimationError, AssetRegistryImpl, ControllerError, DEFAULT_CELL_SIZE, ParseError, RenderError, ValidationErrorClass, applyThemeToElement, buildSceneDOM, calculateTransform, calculateVisualSize, composeTheme, createAssetRegistry, createDefaultRegistry, easeInCubic, easeInOutCubic, easeOutCubic, guardEntryAnimation, guardExitAnimation, guardLifecycleStatus, linear, mountScene, projectToScreen, resolveEasing, resolveTheme };
|
|
2449
|
-
//# sourceMappingURL=isostate.runtime.js.map
|
|
6
|
+
//# debugId=00B086D81CE9D85E64756E2164756E21
|