@sebastianwessel/isostate 0.4.0 → 0.5.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.
Files changed (49) hide show
  1. package/dist/animation/animation-engine.d.ts +1 -0
  2. package/dist/animation/animation-engine.d.ts.map +1 -1
  3. package/dist/animation/controller.d.ts +34 -2
  4. package/dist/animation/controller.d.ts.map +1 -1
  5. package/dist/browser/isostate.runtime.js +4 -4
  6. package/dist/browser/isostate.runtime.js.map +11 -10
  7. package/dist/chunks/{index-C3qcXbPI.js → index-CB6XEsbY.js} +257 -43
  8. package/dist/chunks/index-CB6XEsbY.js.map +1 -0
  9. package/dist/chunks/{rendering-engine-BUUV3Qq-.js → rendering-engine-nkqYx0qc.js} +215 -36
  10. package/dist/chunks/rendering-engine-nkqYx0qc.js.map +1 -0
  11. package/dist/chunks/{scene-parser-B9SPDLYs.js → scene-parser-F69XGE0l.js} +87 -32
  12. package/dist/chunks/scene-parser-F69XGE0l.js.map +1 -0
  13. package/dist/dsl/browser.js +1 -1
  14. package/dist/dsl/index.js +1 -1
  15. package/dist/dsl/scene-parser.d.ts.map +1 -1
  16. package/dist/dsl/scene-validator.d.ts.map +1 -1
  17. package/dist/editor-support/index.js +1 -1
  18. package/dist/index.d.ts +4 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +470 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/rendering/animation-css.d.ts.map +1 -1
  23. package/dist/rendering/rendering-engine.d.ts +9 -0
  24. package/dist/rendering/rendering-engine.d.ts.map +1 -1
  25. package/dist/rendering/theme.d.ts +4 -0
  26. package/dist/rendering/theme.d.ts.map +1 -1
  27. package/dist/runtime/diagnostics-overlay.d.ts +29 -0
  28. package/dist/runtime/diagnostics-overlay.d.ts.map +1 -0
  29. package/dist/runtime/export.d.ts +27 -0
  30. package/dist/runtime/export.d.ts.map +1 -0
  31. package/dist/runtime/index.d.ts +1 -0
  32. package/dist/runtime/index.d.ts.map +1 -1
  33. package/dist/runtime/index.js +2 -2
  34. package/dist/runtime/interactivity.d.ts +56 -0
  35. package/dist/runtime/interactivity.d.ts.map +1 -0
  36. package/dist/runtime/mount-scene.d.ts +11 -10
  37. package/dist/runtime/mount-scene.d.ts.map +1 -1
  38. package/dist/types/asset-registry.d.ts +1 -0
  39. package/dist/types/asset-registry.d.ts.map +1 -1
  40. package/dist/types/node.d.ts +14 -0
  41. package/dist/types/node.d.ts.map +1 -1
  42. package/dist/types/runtime-bundle.d.ts +35 -0
  43. package/dist/types/runtime-bundle.d.ts.map +1 -1
  44. package/dist/types/scene.d.ts +2 -0
  45. package/dist/types/scene.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/chunks/index-C3qcXbPI.js.map +0 -1
  48. package/dist/chunks/rendering-engine-BUUV3Qq-.js.map +0 -1
  49. package/dist/chunks/scene-parser-B9SPDLYs.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { o as getResolvedViewBox, k as getCurrentElementBounds, m as getGridAreaBounds, t as updateElementTransforms, b as applySceneViewBox, l as getElementState, u as unhideElementOnReadd, p as hideElementAfterExit, j as getConnectorState, a as animateElement, d as buildSceneDOM, s as resolveTheme } from './rendering-engine-BUUV3Qq-.js';
2
- import { C as ControllerError, R as RenderError } from './errors-DyqEkrm5.js';
1
+ import { A as AnimationError, C as ControllerError, R as RenderError } from './errors-DyqEkrm5.js';
2
+ import { a as getResolvedViewBox, b as getCurrentElementBounds, c as getElementState, q as getGridAreaBounds, e as updateElementTransforms, g as getResolvedProjectionLayout, f as projectToScreen, l as calculateVisualSize, s as applySceneViewBox, u as unhideElementOnReadd, h as hideElementAfterExit, d as getConnectorState, t as animateElement, j as buildSceneDOM, r as resolveTheme } from './rendering-engine-nkqYx0qc.js';
3
3
  import { s as sha256 } from './sha256-4JuurR-b.js';
4
4
 
5
5
  /**
@@ -55,6 +55,9 @@ class AnimationEngine {
55
55
  }
56
56
  /** Set current scroll progress (0-1) and compute frame update. */
57
57
  setProgress(progress) {
58
+ if (!Number.isFinite(progress)) {
59
+ throw new AnimationError("INVALID_PROGRESS", "setProgress() requires a finite progress value");
60
+ }
58
61
  const clamped = Math.max(0, Math.min(1, progress));
59
62
  if (this._paused)
60
63
  return;
@@ -234,7 +237,7 @@ function interpolateElement(id, prevStop, nextStop, t) {
234
237
  pos: interpolatePos(prev.pos, next.pos, t),
235
238
  size: prev.size + (next.size - prev.size) * t,
236
239
  lifecycle,
237
- ambient: cloneAmbient(next.ambient),
240
+ ambient: cloneAmbient(t < 1 ? prev.ambient : next.ambient),
238
241
  layer: t < 1 ? prev.layer : next.layer,
239
242
  entry: next.enter ?? prev.enter,
240
243
  exit: next.exit ?? prev.exit,
@@ -261,10 +264,10 @@ function interpolateConnector(id, prevStop, nextStop, t) {
261
264
  layer: t < 1 ? prev.layer : next.layer,
262
265
  lifecycle,
263
266
  style: cloneConnectorStyle(t < 1 ? prev.style : next.style),
264
- start: next.start,
265
- end: next.end,
266
- direction: next.direction,
267
- ambient: cloneAmbient(next.ambient),
267
+ start: t < 1 ? prev.start : next.start,
268
+ end: t < 1 ? prev.end : next.end,
269
+ direction: t < 1 ? prev.direction : next.direction,
270
+ ambient: cloneAmbient(t < 1 ? prev.ambient : next.ambient),
268
271
  entry: next.enter ?? prev.enter,
269
272
  exit: next.exit ?? prev.exit,
270
273
  };
@@ -570,8 +573,8 @@ class AnimationController {
570
573
  // Scroll tracking state
571
574
  _minScroll = 0;
572
575
  _maxScroll = 0;
573
- _touchStartY = 0;
574
- _touchStartX = 0;
576
+ _lastTouchY = 0;
577
+ _lastTouchX = 0;
575
578
  _isDragging = false;
576
579
  // Transition animation state
577
580
  _transitionAnim = null;
@@ -715,6 +718,11 @@ class AnimationController {
715
718
  }
716
719
  const elementBounds = getCurrentElementBounds(svg, id);
717
720
  if (!elementBounds) {
721
+ if (getElementState(svg, id)) {
722
+ throw new ControllerError("CAMERA_TARGET_NOT_VISIBLE", `Camera target element "${id}" is currently removed`, {
723
+ elementId: id,
724
+ });
725
+ }
718
726
  throw new ControllerError("CAMERA_TARGET_NOT_FOUND", `Camera target element "${id}" was not found`, {
719
727
  elementId: id,
720
728
  });
@@ -865,8 +873,8 @@ class AnimationController {
865
873
  const { previous, next } = surroundingStops(sceneStops, progress);
866
874
  const previousCamera = this._effectiveCameraAt(previous.index);
867
875
  const nextCamera = this._effectiveCameraAt(next.index);
868
- const previousViewBox = this._resolveCameraViewBox(previousCamera);
869
- const nextViewBox = this._resolveCameraViewBox(nextCamera);
876
+ const previousViewBox = this._resolveCameraViewBox(previousCamera, { forInterpolation: true });
877
+ const nextViewBox = this._resolveCameraViewBox(nextCamera, { forInterpolation: true });
870
878
  const range = next.scene.progress - previous.scene.progress;
871
879
  const rawT = range <= 0 ? 0 : (progress - previous.scene.progress) / range;
872
880
  const t = resolveCameraEasing(nextCamera.easing)(Math.max(0, Math.min(1, rawT)));
@@ -888,7 +896,7 @@ class AnimationController {
888
896
  }
889
897
  return { target: { type: "reset" } };
890
898
  }
891
- _resolveCameraViewBox(camera) {
899
+ _resolveCameraViewBox(camera, options = {}) {
892
900
  if (!this._bundle)
893
901
  throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized bundle");
894
902
  if (camera.target.type === "reset")
@@ -899,7 +907,9 @@ class AnimationController {
899
907
  const svg = this._getSceneSvg();
900
908
  if (!svg)
901
909
  throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized scene SVG");
902
- const bounds = getCurrentElementBounds(svg, camera.target.id);
910
+ const bounds = options.forInterpolation
911
+ ? this._resolveElementBoundsForInterpolation(svg, camera.target.id)
912
+ : getCurrentElementBounds(svg, camera.target.id);
903
913
  if (!bounds) {
904
914
  throw new ControllerError("CAMERA_TARGET_NOT_FOUND", `Camera target element "${camera.target.id}" was not found`, {
905
915
  elementId: camera.target.id,
@@ -907,6 +917,37 @@ class AnimationController {
907
917
  }
908
918
  return expandViewBox(bounds, camera.padding ?? 32);
909
919
  }
920
+ /**
921
+ * Resolve element bounds for scroll/setProgress camera interpolation.
922
+ *
923
+ * Unlike `getCurrentElementBounds()`, this tolerates an element whose
924
+ * resolved presence is transiently "removed" because progress has not yet
925
+ * reached the stop that introduces it (the engine still carries valid
926
+ * authored geometry for it via nearest-geometry fallback). It falls back to
927
+ * that geometry instead of reporting the target as missing so a scene that
928
+ * both adds an element and focuses the camera on it in the same stop can be
929
+ * interpolated into smoothly. A genuinely unknown element id still resolves
930
+ * to `undefined` so callers can throw `CAMERA_TARGET_NOT_FOUND`.
931
+ */
932
+ _resolveElementBoundsForInterpolation(svg, id) {
933
+ const direct = getCurrentElementBounds(svg, id);
934
+ if (direct)
935
+ return direct;
936
+ const state = getElementState(svg, id);
937
+ if (!state || !this._bundle)
938
+ return undefined;
939
+ const layout = getResolvedProjectionLayout(this._bundle);
940
+ const element = state.current;
941
+ const screen = projectToScreen(element.pos[0] + element.size, element.pos[1] + element.size, layout.cellSize, layout.selectedBounds.minX, layout.selectedBounds.minY, layout.padding.x, layout.padding.y);
942
+ const visualSize = calculateVisualSize(element.size, layout.cellSize);
943
+ const [anchorX, anchorY] = state.anchor;
944
+ return {
945
+ minX: roundCameraNumber(screen.screenX - visualSize * anchorX),
946
+ minY: roundCameraNumber(screen.screenY - visualSize * anchorY),
947
+ width: roundCameraNumber(Math.max(1, visualSize)),
948
+ height: roundCameraNumber(Math.max(1, visualSize)),
949
+ };
950
+ }
910
951
  _applyCameraDestination(viewBox, target, options) {
911
952
  const svg = this._getSceneSvg();
912
953
  if (!svg)
@@ -1104,19 +1145,20 @@ class AnimationController {
1104
1145
  // ── Scene transitions ──────────────────────────────────────────────────
1105
1146
  _transitionToScene(index) {
1106
1147
  this._cancelTransition();
1107
- if (index === this._sceneIndex)
1108
- return;
1109
- this._sceneIndex = index;
1110
- this._emit("scene-change", index);
1111
1148
  const stop = this.scenes[index];
1112
1149
  const from = this._progress;
1113
1150
  const to = stop.progress;
1151
+ if (index === this._sceneIndex && from === to)
1152
+ return;
1153
+ this._sceneIndex = index;
1154
+ this._emit("scene-change", index);
1114
1155
  const duration = this._config.transitionDuration;
1115
1156
  if (duration > 0 && from !== to) {
1116
1157
  this._animateProgress(from, to, duration);
1117
1158
  return;
1118
1159
  }
1119
1160
  this._progress = to;
1161
+ this._cancelCameraAnimation();
1120
1162
  this._scheduleProgressForward(to);
1121
1163
  }
1122
1164
  _animateProgress(from, to, duration) {
@@ -1155,6 +1197,9 @@ class AnimationController {
1155
1197
  }
1156
1198
  // ── Scroll binding ─────────────────────────────────────────────────────
1157
1199
  _bindScroll() {
1200
+ if (this._config.keyboardControls) {
1201
+ document.addEventListener("keydown", this._onKeyDown);
1202
+ }
1158
1203
  const container = this._config.container;
1159
1204
  if (!container)
1160
1205
  return;
@@ -1162,9 +1207,6 @@ class AnimationController {
1162
1207
  this._calculateScrollBounds();
1163
1208
  container.addEventListener("scroll", this._onScroll, { passive: true });
1164
1209
  window.addEventListener("resize", this._onResize, { passive: true });
1165
- if (this._config.keyboardControls) {
1166
- document.addEventListener("keydown", this._onKeyDown);
1167
- }
1168
1210
  if (this._config.touchControls) {
1169
1211
  container.addEventListener("touchstart", this._onTouchStart, {
1170
1212
  passive: true,
@@ -1176,12 +1218,12 @@ class AnimationController {
1176
1218
  }
1177
1219
  }
1178
1220
  _unbindScroll() {
1221
+ document.removeEventListener("keydown", this._onKeyDown);
1179
1222
  const container = this._config.container;
1180
1223
  if (!container)
1181
1224
  return;
1182
1225
  container.removeEventListener("scroll", this._onScroll);
1183
1226
  window.removeEventListener("resize", this._onResize);
1184
- document.removeEventListener("keydown", this._onKeyDown);
1185
1227
  container.removeEventListener("touchstart", this._onTouchStart);
1186
1228
  container.removeEventListener("touchmove", this._onTouchMove);
1187
1229
  container.removeEventListener("touchend", this._onTouchEnd);
@@ -1210,10 +1252,11 @@ class AnimationController {
1210
1252
  if (range <= 0)
1211
1253
  return;
1212
1254
  const rawProgress = (currentScroll - this._minScroll) / range;
1213
- const clampedProgress = Math.max(this._config.minProgress ?? 0, Math.min(this._config.maxProgress ?? 1, rawProgress));
1214
1255
  const sensitivity = this._config.scrollSensitivity ?? 1;
1256
+ const scaledProgress = rawProgress * sensitivity;
1257
+ const clampedProgress = Math.max(this._config.minProgress ?? 0, Math.min(this._config.maxProgress ?? 1, scaledProgress));
1215
1258
  if (clampedProgress !== this._progress) {
1216
- this.setProgress(clampedProgress * sensitivity);
1259
+ this.setProgress(clampedProgress);
1217
1260
  }
1218
1261
  };
1219
1262
  _onResize = () => {
@@ -1239,10 +1282,10 @@ class AnimationController {
1239
1282
  this._isDragging = true;
1240
1283
  const touch = e.touches[0];
1241
1284
  if (this._config.scrollDirection === "horizontal") {
1242
- this._touchStartX = touch.clientX;
1285
+ this._lastTouchX = touch.clientX;
1243
1286
  }
1244
1287
  else {
1245
- this._touchStartY = touch.clientY;
1288
+ this._lastTouchY = touch.clientY;
1246
1289
  }
1247
1290
  };
1248
1291
  _onTouchMove = (e) => {
@@ -1251,9 +1294,14 @@ class AnimationController {
1251
1294
  if (!this._isDragging)
1252
1295
  return;
1253
1296
  const touch = e.touches[0];
1254
- const delta = this._config.scrollDirection === "horizontal"
1255
- ? this._touchStartX - touch.clientX
1256
- : this._touchStartY - touch.clientY;
1297
+ const isHorizontal = this._config.scrollDirection === "horizontal";
1298
+ const delta = isHorizontal ? this._lastTouchX - touch.clientX : this._lastTouchY - touch.clientY;
1299
+ if (isHorizontal) {
1300
+ this._lastTouchX = touch.clientX;
1301
+ }
1302
+ else {
1303
+ this._lastTouchY = touch.clientY;
1304
+ }
1257
1305
  const sensitivity = this._config.scrollSensitivity ?? 1.0;
1258
1306
  const progressDelta = (delta / 300) * sensitivity;
1259
1307
  const newProgress = Math.max(0, Math.min(1, this._progress + progressDelta));
@@ -1266,10 +1314,7 @@ class AnimationController {
1266
1314
  };
1267
1315
  // ── Pause state ────────────────────────────────────────────────────────
1268
1316
  _applyPauseState(pause) {
1269
- const container = this._config.container;
1270
- if (!container)
1271
- return;
1272
- const svg = container.querySelector("svg");
1317
+ const svg = this._getSceneSvg();
1273
1318
  if (!svg)
1274
1319
  return;
1275
1320
  const playState = pause ? "paused" : "running";
@@ -1399,8 +1444,136 @@ function roundCameraNumber(value) {
1399
1444
  return Math.round(value * 1000) / 1000;
1400
1445
  }
1401
1446
 
1447
+ const CSS_CLASS_INTERACTIVE_ROOT = "iso-interactive";
1448
+ const CSS_CLASS_HOVER = "iso-hover";
1449
+ /**
1450
+ * Owns delegated pointer listeners and event subscriptions for one mounted
1451
+ * scene. Created unconditionally by `mountScene()`; the three DOM listeners
1452
+ * are only attached when `interactive: true`.
1453
+ */
1454
+ class SceneInteractivity {
1455
+ svg;
1456
+ listeners = new Map();
1457
+ destroyed = false;
1458
+ attached = false;
1459
+ onClick = (event) => this.handleClick(event);
1460
+ onPointerOver = (event) => this.handlePointerOver(event);
1461
+ onPointerOut = (event) => this.handlePointerOut(event);
1462
+ constructor(svg, interactive) {
1463
+ this.svg = svg;
1464
+ if (interactive) {
1465
+ svg.classList.add(CSS_CLASS_INTERACTIVE_ROOT);
1466
+ svg.addEventListener("click", this.onClick);
1467
+ svg.addEventListener("pointerover", this.onPointerOver);
1468
+ svg.addEventListener("pointerout", this.onPointerOut);
1469
+ this.attached = true;
1470
+ }
1471
+ }
1472
+ /** Subscribe to interactivity events. Returns an unsubscribe function. */
1473
+ on(event, listener) {
1474
+ if (this.destroyed) {
1475
+ throw new RenderError("MOUNT_DESTROYED", "Cannot call on() after destroy()");
1476
+ }
1477
+ const set = this.listeners.get(event) ?? new Set();
1478
+ set.add(listener);
1479
+ this.listeners.set(event, set);
1480
+ return () => {
1481
+ set.delete(listener);
1482
+ };
1483
+ }
1484
+ /** Remove the three delegated listeners and all subscriptions. Safe to call more than once. */
1485
+ destroy() {
1486
+ if (this.destroyed)
1487
+ return;
1488
+ this.destroyed = true;
1489
+ if (this.attached) {
1490
+ this.svg.removeEventListener("click", this.onClick);
1491
+ this.svg.removeEventListener("pointerover", this.onPointerOver);
1492
+ this.svg.removeEventListener("pointerout", this.onPointerOut);
1493
+ this.attached = false;
1494
+ }
1495
+ this.listeners.clear();
1496
+ }
1497
+ handleClick(event) {
1498
+ const id = this.resolveVisibleElementId(event.target);
1499
+ if (!id)
1500
+ return;
1501
+ this.emit("element-click", { id, originalEvent: event });
1502
+ }
1503
+ handlePointerOver(event) {
1504
+ const enteredId = this.resolveVisibleElementId(event.target);
1505
+ if (!enteredId)
1506
+ return;
1507
+ const previousId = this.resolveVisibleElementId(event.relatedTarget);
1508
+ if (enteredId === previousId)
1509
+ return;
1510
+ this.toggleHoverClass(enteredId, true);
1511
+ this.emit("element-enter", { id: enteredId, originalEvent: event });
1512
+ }
1513
+ handlePointerOut(event) {
1514
+ const leftId = this.resolveVisibleElementId(event.target);
1515
+ if (!leftId)
1516
+ return;
1517
+ const nextId = this.resolveVisibleElementId(event.relatedTarget);
1518
+ if (leftId === nextId)
1519
+ return;
1520
+ this.toggleHoverClass(leftId, false);
1521
+ this.emit("element-leave", { id: leftId, originalEvent: event });
1522
+ }
1523
+ toggleHoverClass(id, hovered) {
1524
+ const state = getElementState(this.svg, id);
1525
+ if (!state)
1526
+ return;
1527
+ state.node.classList.toggle(CSS_CLASS_HOVER, hovered);
1528
+ }
1529
+ /**
1530
+ * Resolve the scene element id for a pointer event target: walk up
1531
+ * `parentNode` from the event target until the root SVG, looking for the
1532
+ * first ancestor `<g>` that is a member of the engine's element state map
1533
+ * (connector groups also carry `data-id` but are never members of this
1534
+ * map, and floor/defs/diagnostics nodes carry no `data-id` at all). An
1535
+ * element whose current presence is `removed` never resolves.
1536
+ */
1537
+ resolveVisibleElementId(target) {
1538
+ let node = target;
1539
+ while (node && node !== this.svg) {
1540
+ const id = node.getAttribute?.("data-id");
1541
+ if (id) {
1542
+ const state = getElementState(this.svg, id);
1543
+ if (state && state.current.presence !== "removed")
1544
+ return id;
1545
+ return undefined;
1546
+ }
1547
+ node = node.parentNode;
1548
+ }
1549
+ return undefined;
1550
+ }
1551
+ /**
1552
+ * Invoke every listener for `event`. Exceptions are not caught: consistent
1553
+ * with `AnimationController`'s event system, a throwing listener is
1554
+ * rethrown asynchronously (via `queueMicrotask`) rather than swallowed, so
1555
+ * it surfaces as an unhandled error without breaking delivery to sibling
1556
+ * listeners in the same `Set`.
1557
+ */
1558
+ emit(event, payload) {
1559
+ const set = this.listeners.get(event);
1560
+ if (!set)
1561
+ return;
1562
+ for (const listener of set) {
1563
+ try {
1564
+ listener(payload);
1565
+ }
1566
+ catch (error) {
1567
+ queueMicrotask(() => {
1568
+ throw error;
1569
+ });
1570
+ }
1571
+ }
1572
+ }
1573
+ }
1574
+
1402
1575
  const RUNTIME_BUNDLE_FORMAT = "isostate-runtime-bundle";
1403
- const RUNTIME_VERSION = "0.4.0";
1576
+ const RUNTIME_VERSION = "0.5.0";
1404
1577
  const HEX_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
1405
1578
  /** Mount a compiled runtime bundle into an HTML element. */
1406
1579
  function mountScene(target, bundle, options = {}) {
@@ -1424,17 +1597,20 @@ function mountScene(target, bundle, options = {}) {
1424
1597
  sceneElement: svg,
1425
1598
  });
1426
1599
  }
1600
+ const interactivity = new SceneInteractivity(svg, options.interactive === true);
1427
1601
  let destroyed = false;
1428
1602
  return {
1429
1603
  svg,
1430
1604
  engine,
1431
1605
  controller,
1432
- getResolvedConfig: () => getResolvedConfig(bundle, options),
1606
+ getResolvedConfig: () => getResolvedConfig(bundle, options, controller),
1607
+ on: (event, listener) => interactivity.on(event, listener),
1433
1608
  destroy: () => {
1434
1609
  if (destroyed)
1435
1610
  return;
1436
1611
  destroyed = true;
1437
- controller?.destroy();
1612
+ interactivity.destroy();
1613
+ destroyControllerSafely(controller);
1438
1614
  engine.destroy();
1439
1615
  if (svg.parentNode === target) {
1440
1616
  target.removeChild(svg);
@@ -1445,6 +1621,24 @@ function mountScene(target, bundle, options = {}) {
1445
1621
  },
1446
1622
  };
1447
1623
  }
1624
+ /**
1625
+ * Destroy the controller, tolerating a controller that was already destroyed
1626
+ * independently (e.g. by application code calling `controller.destroy()`
1627
+ * before `mountScene().destroy()`). Keeps `mountScene().destroy()` idempotent
1628
+ * and defensive so engine cleanup and SVG removal always run.
1629
+ */
1630
+ function destroyControllerSafely(controller) {
1631
+ if (!controller)
1632
+ return;
1633
+ try {
1634
+ controller.destroy();
1635
+ }
1636
+ catch (error) {
1637
+ if (error instanceof ControllerError && error.code === "CONTROLLER_DESTROYED")
1638
+ return;
1639
+ throw error;
1640
+ }
1641
+ }
1448
1642
  function assertMountTarget(target) {
1449
1643
  if (!target || typeof target.appendChild !== "function" || typeof target.removeChild !== "function") {
1450
1644
  throw new RenderError("INVALID_MOUNT_TARGET", "mountScene() requires a DOM HTMLElement target");
@@ -1480,20 +1674,17 @@ function validateRuntimeBundle(bundle) {
1480
1674
  });
1481
1675
  }
1482
1676
  }
1483
- function getResolvedConfig(bundle, options = {}) {
1677
+ function getResolvedConfig(bundle, options = {}, controller) {
1484
1678
  return {
1485
1679
  grid: { cellSize: bundle.grid.cellSize },
1486
- floor: { ...bundle.floor },
1680
+ floor: { ...bundle.floor, size: [...bundle.floor.size], origin: [...bundle.floor.origin] },
1487
1681
  layout: {
1488
1682
  ...bundle.layout,
1489
1683
  padding: { ...bundle.layout.padding },
1490
1684
  align: [...bundle.layout.align],
1491
1685
  },
1492
1686
  viewBox: getResolvedViewBox(bundle),
1493
- camera: {
1494
- viewBox: getResolvedViewBox(bundle),
1495
- isZoomed: false,
1496
- },
1687
+ camera: resolveCameraState(bundle, controller),
1497
1688
  theme: bundle.theme,
1498
1689
  themeVars: getResolvedThemeVars(bundle, options.themeVars),
1499
1690
  scenes: bundle.scenes.map((scene) => ({
@@ -1502,9 +1693,32 @@ function getResolvedConfig(bundle, options = {}) {
1502
1693
  })),
1503
1694
  layerOrder: bundle.layers
1504
1695
  .map((layer) => ({ name: layer.name, order: layer.order }))
1505
- .sort((a, b) => a.order - b.order || a.name.localeCompare(b.name)),
1696
+ .sort((a, b) => a.order - b.order || compareCodePointOrder(a.name, b.name)),
1506
1697
  };
1507
1698
  }
1699
+ /**
1700
+ * Live camera state from the controller when one is attached and alive;
1701
+ * otherwise the static bundle-derived default.
1702
+ */
1703
+ function resolveCameraState(bundle, controller) {
1704
+ if (controller) {
1705
+ try {
1706
+ return controller.getCameraState();
1707
+ }
1708
+ catch (error) {
1709
+ if (!(error instanceof ControllerError))
1710
+ throw error;
1711
+ }
1712
+ }
1713
+ return {
1714
+ viewBox: getResolvedViewBox(bundle),
1715
+ isZoomed: false,
1716
+ };
1717
+ }
1718
+ /** Locale-independent string comparator so tie-break order is byte-deterministic across hosts. */
1719
+ function compareCodePointOrder(a, b) {
1720
+ return a < b ? -1 : a > b ? 1 : 0;
1721
+ }
1508
1722
  function getResolvedThemeVars(bundle, overrides = {}) {
1509
1723
  return {
1510
1724
  ...(resolveTheme(bundle.theme) ?? {}),
@@ -1539,4 +1753,4 @@ function isPlainObject(value) {
1539
1753
  }
1540
1754
 
1541
1755
  export { AnimationController as A, AnimationEngine as a, easeInOutCubic as b, easeOutCubic as c, easeInCubic as e, linear as l, mountScene as m, resolveEasing as r };
1542
- //# sourceMappingURL=index-C3qcXbPI.js.map
1756
+ //# sourceMappingURL=index-CB6XEsbY.js.map