@sebastianwessel/isostate 0.1.2 → 0.3.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/dist/animation/controller.d.ts +25 -1
- package/dist/animation/controller.d.ts.map +1 -1
- package/dist/browser/isostate.runtime.js +5 -2459
- package/dist/browser/isostate.runtime.js.map +22 -1
- package/dist/browser-runtime.d.ts +5 -0
- package/dist/browser-runtime.d.ts.map +1 -0
- package/dist/chunks/{index-DEv2-G9F.js → index-B6HlPnov.js} +243 -1092
- package/dist/chunks/index-B6HlPnov.js.map +1 -0
- package/dist/chunks/rendering-engine-BgHel7yX.js +1117 -0
- package/dist/chunks/rendering-engine-BgHel7yX.js.map +1 -0
- package/dist/chunks/scene-parser-xgVJh2FU.js +2676 -0
- package/dist/chunks/scene-parser-xgVJh2FU.js.map +1 -0
- package/dist/chunks/sha256-4JuurR-b.js +69 -0
- package/dist/chunks/sha256-4JuurR-b.js.map +1 -0
- package/dist/dsl/browser.d.ts +7 -0
- package/dist/dsl/browser.d.ts.map +1 -0
- package/dist/dsl/browser.js +5 -0
- package/dist/dsl/browser.js.map +1 -0
- package/dist/dsl/compiler.d.ts.map +1 -1
- package/dist/dsl/index.js +4 -2190
- 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/editor-support/adapter.d.ts +46 -0
- package/dist/editor-support/adapter.d.ts.map +1 -0
- package/dist/editor-support/geometry.d.ts +32 -0
- package/dist/editor-support/geometry.d.ts.map +1 -0
- package/dist/editor-support/hit-test.d.ts +29 -0
- package/dist/editor-support/hit-test.d.ts.map +1 -0
- package/dist/editor-support/index.d.ts +7 -0
- package/dist/editor-support/index.d.ts.map +1 -0
- package/dist/editor-support/index.js +426 -0
- package/dist/editor-support/index.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/rendering/asset-node.d.ts +2 -1
- package/dist/rendering/asset-node.d.ts.map +1 -1
- package/dist/rendering/rendering-engine.d.ts +28 -1
- package/dist/rendering/rendering-engine.d.ts.map +1 -1
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/mount-scene.d.ts +9 -0
- package/dist/runtime/mount-scene.d.ts.map +1 -1
- package/dist/types/assets.d.ts +27 -2
- package/dist/types/assets.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/node.d.ts +50 -2
- package/dist/types/node.d.ts.map +1 -1
- package/dist/types/runtime-bundle.d.ts +5 -0
- package/dist/types/runtime-bundle.d.ts.map +1 -1
- package/dist/types/scene.d.ts +28 -3
- package/dist/types/scene.d.ts.map +1 -1
- package/dist/types/validation.d.ts +6 -0
- package/dist/types/validation.d.ts.map +1 -1
- package/dist/utils/sha256.d.ts +3 -0
- package/dist/utils/sha256.d.ts.map +1 -0
- package/package.json +9 -1
- package/dist/chunks/index-DEv2-G9F.js.map +0 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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-BgHel7yX.js';
|
|
2
|
+
import { C as ControllerError, R as RenderError } from './errors-DyqEkrm5.js';
|
|
3
|
+
import { s as sha256 } from './sha256-4JuurR-b.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Animation engine — resolves progress against compiled RuntimeBundle scene stops.
|
|
@@ -493,1029 +495,6 @@ function cloneConnectorFrameMap(map) {
|
|
|
493
495
|
return clone;
|
|
494
496
|
}
|
|
495
497
|
|
|
496
|
-
// ── Built-in themes ────────────────────────────────────────────────────────
|
|
497
|
-
const BUILTIN_THEMES = {
|
|
498
|
-
light: {
|
|
499
|
-
"--color-top": "#e2e8f0",
|
|
500
|
-
"--color-front": "#94a3b8",
|
|
501
|
-
"--color-side": "#64748b",
|
|
502
|
-
"--color-back": "#475569",
|
|
503
|
-
"--color-leaf": "#15803d",
|
|
504
|
-
"--color-trunk": "#78350f",
|
|
505
|
-
"--color-accent": "#3b82f6",
|
|
506
|
-
},
|
|
507
|
-
dark: {
|
|
508
|
-
"--color-top": "#334155",
|
|
509
|
-
"--color-front": "#1e293b",
|
|
510
|
-
"--color-side": "#0f172a",
|
|
511
|
-
"--color-back": "#020617",
|
|
512
|
-
"--color-leaf": "#166534",
|
|
513
|
-
"--color-trunk": "#451a03",
|
|
514
|
-
"--color-accent": "#60a5fa",
|
|
515
|
-
},
|
|
516
|
-
brand: {
|
|
517
|
-
"--color-top": "#c7d2fe",
|
|
518
|
-
"--color-front": "#818cf8",
|
|
519
|
-
"--color-side": "#6366f1",
|
|
520
|
-
"--color-back": "#4338ca",
|
|
521
|
-
"--color-leaf": "#22c55e",
|
|
522
|
-
"--color-trunk": "#854d0e",
|
|
523
|
-
"--color-accent": "#f59e0b",
|
|
524
|
-
},
|
|
525
|
-
};
|
|
526
|
-
/**
|
|
527
|
-
* Resolve a theme name to its CSS variable map.
|
|
528
|
-
* Returns undefined if the theme is not found.
|
|
529
|
-
*/
|
|
530
|
-
function resolveTheme(name) {
|
|
531
|
-
return BUILTIN_THEMES[name];
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* Compose a new theme by extending an existing one with overrides.
|
|
535
|
-
*/
|
|
536
|
-
function composeTheme(baseName, overrides) {
|
|
537
|
-
const base = BUILTIN_THEMES[baseName];
|
|
538
|
-
if (!base) {
|
|
539
|
-
return { name: baseName, vars: { ...overrides } };
|
|
540
|
-
}
|
|
541
|
-
return { name: baseName, vars: { ...base, ...overrides } };
|
|
542
|
-
}
|
|
543
|
-
// ── Asset registry implementation ──────────────────────────────────────────
|
|
544
|
-
/**
|
|
545
|
-
* Default asset registry implementation.
|
|
546
|
-
* Maps asset ids to definitions and supports category filtering.
|
|
547
|
-
*/
|
|
548
|
-
class AssetRegistryImpl {
|
|
549
|
-
_assets = new Map();
|
|
550
|
-
register(asset) {
|
|
551
|
-
this._assets.set(asset.id, asset);
|
|
552
|
-
}
|
|
553
|
-
get(id) {
|
|
554
|
-
return this._assets.get(id);
|
|
555
|
-
}
|
|
556
|
-
getAll(category) {
|
|
557
|
-
if (category) {
|
|
558
|
-
return [...this._assets.values()].filter((a) => a.category === category);
|
|
559
|
-
}
|
|
560
|
-
return [...this._assets.values()];
|
|
561
|
-
}
|
|
562
|
-
has(id) {
|
|
563
|
-
return this._assets.has(id);
|
|
564
|
-
}
|
|
565
|
-
remove(id) {
|
|
566
|
-
this._assets.delete(id);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
function createAssetRegistry(assets = []) {
|
|
570
|
-
const registry = new AssetRegistryImpl();
|
|
571
|
-
for (const asset of assets) {
|
|
572
|
-
registry.register(asset);
|
|
573
|
-
}
|
|
574
|
-
return registry;
|
|
575
|
-
}
|
|
576
|
-
/** Create a fresh registry populated with the built-in demo assets. */
|
|
577
|
-
function createDefaultRegistry() {
|
|
578
|
-
return createAssetRegistry([
|
|
579
|
-
{
|
|
580
|
-
id: "iso-platform",
|
|
581
|
-
category: "infrastructure",
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
id: "iso-server",
|
|
585
|
-
category: "equipment",
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
id: "iso-database",
|
|
589
|
-
category: "equipment",
|
|
590
|
-
},
|
|
591
|
-
{
|
|
592
|
-
id: "iso-connector",
|
|
593
|
-
category: "decoration",
|
|
594
|
-
},
|
|
595
|
-
{
|
|
596
|
-
id: "iso-cloud",
|
|
597
|
-
category: "decoration",
|
|
598
|
-
},
|
|
599
|
-
]);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/** Default cell size in pixels */
|
|
603
|
-
const DEFAULT_CELL_SIZE = 64;
|
|
604
|
-
/**
|
|
605
|
-
* Calculate raw scene-space coordinates from isometric grid position.
|
|
606
|
-
* Layout is applied separately by subtracting resolved bounds and adding padding.
|
|
607
|
-
*/
|
|
608
|
-
function projectToRaw(gridX, gridY, cellSize) {
|
|
609
|
-
return {
|
|
610
|
-
rawX: cellSize * (gridX - gridY) * 0.5,
|
|
611
|
-
rawY: cellSize * (gridX + gridY) * 0.25,
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
/**
|
|
615
|
-
* Calculate screen coordinates from raw isometric projection and resolved bounds.
|
|
616
|
-
*/
|
|
617
|
-
function projectToScreen(gridX, gridY, cellSize, boundsMinX = 0, boundsMinY = 0, paddingX = 0, paddingY = 0) {
|
|
618
|
-
const { rawX, rawY } = projectToRaw(gridX, gridY, cellSize);
|
|
619
|
-
return {
|
|
620
|
-
screenX: rawX - boundsMinX + paddingX,
|
|
621
|
-
screenY: rawY - boundsMinY + paddingY,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
/** Calculate the screen size for an element based on its grid size. */
|
|
625
|
-
function calculateVisualSize(gridSize, cellSize) {
|
|
626
|
-
return cellSize * gridSize;
|
|
627
|
-
}
|
|
628
|
-
/** Calculate the element transform string for positioning and scaling. */
|
|
629
|
-
function calculateTransform(screenX, screenY, visualSize, cellSize) {
|
|
630
|
-
const scale = visualSize / cellSize;
|
|
631
|
-
return `translate(${screenX}px, ${screenY}px) scale(${scale})`;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/** Build built-in entry, exit, ambient, and reduced-motion CSS keyframes. */
|
|
635
|
-
function buildKeyframeCSS() {
|
|
636
|
-
return [
|
|
637
|
-
"@keyframes iso-anim-fade-in{from{opacity:0}to{opacity:1}}",
|
|
638
|
-
"@keyframes iso-anim-fade-in-grow{from{opacity:0;scale:.01}to{opacity:1;scale:1}}",
|
|
639
|
-
"@keyframes iso-anim-fall-in{from{opacity:0;translate:0 -40px}to{opacity:1;translate:0 0}}",
|
|
640
|
-
"@keyframes iso-anim-rise-from-ground{from{opacity:0;translate:0 40px}to{opacity:1;translate:0 0}}",
|
|
641
|
-
"@keyframes iso-anim-slide-in-left{from{opacity:0;translate:-60px 0}to{opacity:1;translate:0 0}}",
|
|
642
|
-
"@keyframes iso-anim-slide-in-right{from{opacity:0;translate:60px 0}to{opacity:1;translate:0 0}}",
|
|
643
|
-
"@keyframes iso-anim-flip-in{from{opacity:1;scale:0 1}to{opacity:1;scale:1 1}}",
|
|
644
|
-
"@keyframes iso-anim-fade-out{from{opacity:1}to{opacity:0}}",
|
|
645
|
-
"@keyframes iso-anim-fade-out-shrink{from{opacity:1;scale:1}to{opacity:0;scale:.01}}",
|
|
646
|
-
"@keyframes iso-anim-fall-through-ground{from{opacity:1;translate:0 0}to{opacity:0;translate:0 40px}}",
|
|
647
|
-
"@keyframes iso-anim-rise-away{from{opacity:1;translate:0 0}to{opacity:0;translate:0 -40px}}",
|
|
648
|
-
"@keyframes iso-anim-slide-out-left{from{opacity:1;translate:0 0}to{opacity:0;translate:-60px 0}}",
|
|
649
|
-
"@keyframes iso-anim-slide-out-right{from{opacity:1;translate:0 0}to{opacity:0;translate:60px 0}}",
|
|
650
|
-
"@keyframes iso-anim-flip-out{from{opacity:1;scale:1 1}to{opacity:0;scale:0 1}}",
|
|
651
|
-
"@keyframes iso-anim-pulse{0%,100%{opacity:.7}50%{opacity:1}}",
|
|
652
|
-
"@keyframes iso-anim-float{0%,100%{translate:0 0}50%{translate:0 -6px}}",
|
|
653
|
-
"@keyframes iso-anim-shake{0%,25%,75%,100%{translate:0 0}50%{translate:3px 0}}",
|
|
654
|
-
"@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))}}",
|
|
655
|
-
"@keyframes iso-anim-spin{from{rotate:0deg}to{rotate:360deg}}",
|
|
656
|
-
"@keyframes iso-anim-blink{0%,49%{opacity:1}50%,100%{opacity:0}}",
|
|
657
|
-
"@keyframes iso-connector-flow-route{from{stroke-dashoffset:0}to{stroke-dashoffset:calc(-1px * var(--iso-flow-distance,20))}}",
|
|
658
|
-
"@keyframes iso-connector-flow-reverse{from{stroke-dashoffset:0}to{stroke-dashoffset:calc(1px * var(--iso-flow-distance,20))}}",
|
|
659
|
-
".iso-connector-pattern-dashed .iso-connector-shaft,.iso-connector-pattern-dotted .iso-connector-shaft{--iso-flow-distance:20}",
|
|
660
|
-
".iso-connector-direction-route .iso-connector-shaft.iso-ambient-flow{animation:iso-connector-flow-route 900ms linear infinite}",
|
|
661
|
-
".iso-connector-direction-reverse .iso-connector-shaft.iso-ambient-flow{animation:iso-connector-flow-reverse 900ms linear infinite}",
|
|
662
|
-
"@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}}",
|
|
663
|
-
].join("\n");
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const NS$1 = "http://www.w3.org/2000/svg";
|
|
667
|
-
function createAssetResolver(bundle) {
|
|
668
|
-
const embedded = new Map();
|
|
669
|
-
const bundleAssets = bundle?.assets;
|
|
670
|
-
if (bundleAssets && typeof bundleAssets === "object") {
|
|
671
|
-
for (const [name, asset] of Object.entries(bundleAssets)) {
|
|
672
|
-
if (asset && typeof asset === "object" && typeof asset.url === "string") {
|
|
673
|
-
embedded.set(name, { url: asset.url, anchor: asset.anchor });
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return (name) => embedded.get(name);
|
|
678
|
-
}
|
|
679
|
-
function createAssetNode(asset, assetName, cellSize) {
|
|
680
|
-
return createUrlAssetNode(asset.url, assetName, cellSize, asset.anchor);
|
|
681
|
-
}
|
|
682
|
-
function createTextAssetNode(textContent, assetName, cellSize) {
|
|
683
|
-
if (!textContent?.value) {
|
|
684
|
-
throw new RenderError("TEXT_CONTENT_MISSING", `Text content is missing for built-in asset: ${assetName}`, {
|
|
685
|
-
asset: assetName,
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
const group = document.createElementNS(NS$1, "g");
|
|
689
|
-
const text = document.createElementNS(NS$1, "text");
|
|
690
|
-
const align = textContent.align ?? "middle";
|
|
691
|
-
const fontSize = textContent.fontSize ?? 12;
|
|
692
|
-
const lineHeight = textContent.lineHeight ?? 1.2;
|
|
693
|
-
const anchorX = textAnchorX(align, cellSize);
|
|
694
|
-
text.setAttribute("x", String(anchorX));
|
|
695
|
-
text.setAttribute("y", String(-cellSize));
|
|
696
|
-
text.setAttribute("text-anchor", align);
|
|
697
|
-
text.setAttribute("dominant-baseline", "text-before-edge");
|
|
698
|
-
text.setAttribute("font-family", "Arial, Helvetica, sans-serif");
|
|
699
|
-
text.setAttribute("font-size", String(fontSize));
|
|
700
|
-
text.setAttribute("font-weight", String(textContent.fontWeight ?? 700));
|
|
701
|
-
text.setAttribute("fill", textContent.fill ?? "currentColor");
|
|
702
|
-
const lines = normalizeTextLines(textContent.value);
|
|
703
|
-
for (const [index, line] of lines.entries()) {
|
|
704
|
-
const tspan = document.createElementNS(NS$1, "tspan");
|
|
705
|
-
tspan.setAttribute("x", String(anchorX));
|
|
706
|
-
tspan.setAttribute("dy", index === 0 ? "0" : String(fontSize * lineHeight));
|
|
707
|
-
tspan.textContent = line;
|
|
708
|
-
text.appendChild(tspan);
|
|
709
|
-
}
|
|
710
|
-
group.appendChild(text);
|
|
711
|
-
return group;
|
|
712
|
-
}
|
|
713
|
-
function createPrimitiveAssetNode(assetName, primitive, cellSize) {
|
|
714
|
-
const group = document.createElementNS(NS$1, "g");
|
|
715
|
-
switch (assetName) {
|
|
716
|
-
case "rectangle":
|
|
717
|
-
appendProjectedPolygon(group, rectanglePoints(), primitive?.rectangle, cellSize);
|
|
718
|
-
return group;
|
|
719
|
-
case "polygon":
|
|
720
|
-
appendProjectedPolygon(group, primitive?.polygon?.points, primitive?.polygon, cellSize);
|
|
721
|
-
return group;
|
|
722
|
-
case "line":
|
|
723
|
-
appendProjectedPolyline(group, primitive?.line, primitive?.line, cellSize);
|
|
724
|
-
return group;
|
|
725
|
-
case "circle":
|
|
726
|
-
appendCircle(group, primitive?.circle, cellSize);
|
|
727
|
-
return group;
|
|
728
|
-
default:
|
|
729
|
-
throw new RenderError("PRIMITIVE_ASSET_UNKNOWN", `Unknown built-in primitive asset: ${assetName}`, {
|
|
730
|
-
asset: assetName,
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
function textAnchorX(align, cellSize) {
|
|
735
|
-
if (align === "start")
|
|
736
|
-
return -cellSize / 2;
|
|
737
|
-
if (align === "end")
|
|
738
|
-
return cellSize / 2;
|
|
739
|
-
return 0;
|
|
740
|
-
}
|
|
741
|
-
function normalizeTextLines(value) {
|
|
742
|
-
const normalized = value.replace(/\r\n?/g, "\n").replace(/\n$/, "");
|
|
743
|
-
return normalized.split("\n");
|
|
744
|
-
}
|
|
745
|
-
function rectanglePoints() {
|
|
746
|
-
return [
|
|
747
|
-
[0, 0],
|
|
748
|
-
[1, 0],
|
|
749
|
-
[1, 1],
|
|
750
|
-
[0, 1],
|
|
751
|
-
];
|
|
752
|
-
}
|
|
753
|
-
function appendProjectedPolygon(group, points, style, cellSize) {
|
|
754
|
-
if (!points)
|
|
755
|
-
return;
|
|
756
|
-
const polygon = document.createElementNS(NS$1, "polygon");
|
|
757
|
-
polygon.setAttribute("points", points.map((point) => projectLocalPoint(point, cellSize)).join(" "));
|
|
758
|
-
applyPrimitiveStyle(polygon, style, { fill: "currentColor", stroke: "none" });
|
|
759
|
-
group.appendChild(polygon);
|
|
760
|
-
}
|
|
761
|
-
function appendProjectedPolyline(group, line, style, cellSize) {
|
|
762
|
-
if (!line?.points)
|
|
763
|
-
return;
|
|
764
|
-
const polyline = document.createElementNS(NS$1, "polyline");
|
|
765
|
-
polyline.setAttribute("points", line.points.map((point) => projectLocalPoint(point, cellSize)).join(" "));
|
|
766
|
-
polyline.setAttribute("fill", "none");
|
|
767
|
-
polyline.setAttribute("stroke-linecap", line.lineCap ?? "round");
|
|
768
|
-
polyline.setAttribute("stroke-linejoin", line.lineJoin ?? "round");
|
|
769
|
-
applyPrimitiveStyle(polyline, style, {
|
|
770
|
-
fill: "none",
|
|
771
|
-
stroke: "currentColor",
|
|
772
|
-
});
|
|
773
|
-
group.appendChild(polyline);
|
|
774
|
-
}
|
|
775
|
-
function appendCircle(group, style, cellSize) {
|
|
776
|
-
const center = projectLocalPoint([0.5, 0.5], cellSize);
|
|
777
|
-
const circle = document.createElementNS(NS$1, "circle");
|
|
778
|
-
const [cx, cy] = center.split(",").map(Number);
|
|
779
|
-
circle.setAttribute("cx", String(cx));
|
|
780
|
-
circle.setAttribute("cy", String(cy));
|
|
781
|
-
circle.setAttribute("r", String(cellSize * 0.2));
|
|
782
|
-
applyPrimitiveStyle(circle, style, {
|
|
783
|
-
fill: "currentColor",
|
|
784
|
-
stroke: "none",
|
|
785
|
-
});
|
|
786
|
-
group.appendChild(circle);
|
|
787
|
-
}
|
|
788
|
-
function projectLocalPoint(point, cellSize) {
|
|
789
|
-
const projected = projectToRaw(point[0], point[1], cellSize);
|
|
790
|
-
const anchor = projectToRaw(1, 1, cellSize);
|
|
791
|
-
return `${projected.rawX - anchor.rawX},${projected.rawY - anchor.rawY}`;
|
|
792
|
-
}
|
|
793
|
-
function applyPrimitiveStyle(node, style, defaults) {
|
|
794
|
-
node.setAttribute("fill", style?.fill ?? defaults.fill);
|
|
795
|
-
node.setAttribute("stroke", style?.stroke ?? defaults.stroke);
|
|
796
|
-
node.setAttribute("stroke-width", String(style?.strokeWidth ?? 0));
|
|
797
|
-
node.setAttribute("opacity", String(style?.opacity ?? 1));
|
|
798
|
-
if (style?.dash)
|
|
799
|
-
node.setAttribute("stroke-dasharray", style.dash.join(" "));
|
|
800
|
-
}
|
|
801
|
-
function createUrlAssetNode(url, assetName, cellSize, anchor) {
|
|
802
|
-
if (!isSafeAssetUrl(url)) {
|
|
803
|
-
throw new RenderError("INVALID_ASSET_URL", `Asset URL is unsafe: ${assetName}`, { asset: assetName });
|
|
804
|
-
}
|
|
805
|
-
const group = document.createElementNS(NS$1, "g");
|
|
806
|
-
const image = document.createElementNS(NS$1, "image");
|
|
807
|
-
const resolvedUrl = resolveBrowserAssetUrl(url);
|
|
808
|
-
const [anchorX, anchorY] = anchor ?? [0.5, 1];
|
|
809
|
-
image.setAttribute("href", resolvedUrl);
|
|
810
|
-
image.setAttributeNS("http://www.w3.org/1999/xlink", "href", resolvedUrl);
|
|
811
|
-
image.setAttribute("x", String(-cellSize * anchorX));
|
|
812
|
-
image.setAttribute("y", String(-cellSize * anchorY));
|
|
813
|
-
image.setAttribute("width", String(cellSize));
|
|
814
|
-
image.setAttribute("height", String(cellSize));
|
|
815
|
-
image.setAttribute("preserveAspectRatio", "xMidYMax meet");
|
|
816
|
-
group.appendChild(image);
|
|
817
|
-
return group;
|
|
818
|
-
}
|
|
819
|
-
function resolveBrowserAssetUrl(url) {
|
|
820
|
-
try {
|
|
821
|
-
const baseURI = document.baseURI;
|
|
822
|
-
return typeof baseURI === "string" ? new URL(url, baseURI).href : url;
|
|
823
|
-
}
|
|
824
|
-
catch {
|
|
825
|
-
return url;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
function isSafeAssetUrl(url) {
|
|
829
|
-
const normalized = url.trim().toLowerCase();
|
|
830
|
-
return normalized.length > 0 && !normalized.startsWith("javascript:");
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const CSS_CUSTOM_PROPERTY_PATTERN = /^--[a-zA-Z0-9-_]+$/;
|
|
834
|
-
function applyThemeToElement(element, themeVars) {
|
|
835
|
-
for (const [name, value] of Object.entries(themeVars)) {
|
|
836
|
-
if (!CSS_CUSTOM_PROPERTY_PATTERN.test(name)) {
|
|
837
|
-
throw new RenderError("INVALID_THEME_VAR", `Invalid CSS custom property name: ${name}`);
|
|
838
|
-
}
|
|
839
|
-
element.style.setProperty(name, value);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
const NS = "http://www.w3.org/2000/svg";
|
|
844
|
-
const BUILT_IN_TEXT_ASSET_ID = "text";
|
|
845
|
-
const BUILT_IN_PRIMITIVE_ASSET_IDS = new Set(["rectangle", "circle", "polygon", "line"]);
|
|
846
|
-
const DEFAULT_CONNECTOR_DASH = {
|
|
847
|
-
dashed: [12, 8],
|
|
848
|
-
dotted: [0, 8],
|
|
849
|
-
};
|
|
850
|
-
const ENDPOINT_RADIUS_GRID = 0.14;
|
|
851
|
-
const ARROW_LENGTH_GRID = 0.35;
|
|
852
|
-
const ARROW_WIDTH_GRID = 0.28;
|
|
853
|
-
const BAR_WIDTH_GRID = 0.4;
|
|
854
|
-
// ── Public API ────────────────────────────────────────────────────────────
|
|
855
|
-
/** Build the SVG DOM for a compiled runtime bundle and mount it into a container. */
|
|
856
|
-
function buildSceneDOM(container, bundle, config) {
|
|
857
|
-
const layout = resolveSceneLayout(bundle);
|
|
858
|
-
const initialStop = bundle.scenes[0];
|
|
859
|
-
const allElements = collectElementDefinitions(bundle);
|
|
860
|
-
const allConnectors = collectConnectorDefinitions(bundle);
|
|
861
|
-
const initialById = new Map((initialStop?.elements ?? []).map((element) => [element.id, element]));
|
|
862
|
-
const initialConnectorsById = new Map((initialStop?.connectors ?? []).map((connector) => [connector.id, connector]));
|
|
863
|
-
const svg = createRootSvg(layout, config?.label, bundle.className);
|
|
864
|
-
const assetResolver = createAssetResolver(bundle);
|
|
865
|
-
svg.appendChild(createCssDefs());
|
|
866
|
-
applyThemeToElement(svg, {
|
|
867
|
-
...(resolveTheme(bundle.theme) ?? {}),
|
|
868
|
-
...(bundle.themeVars ?? {}),
|
|
869
|
-
...(config?.themeVars ?? {}),
|
|
870
|
-
});
|
|
871
|
-
const sortedLayers = sortLayers(bundle.layers);
|
|
872
|
-
const layerMap = createLayerMap(sortedLayers);
|
|
873
|
-
if (bundle.floor.visible) {
|
|
874
|
-
svg.appendChild(createFloorGrid(bundle, layout));
|
|
875
|
-
}
|
|
876
|
-
const connectorMap = new Map();
|
|
877
|
-
for (const def of allConnectors) {
|
|
878
|
-
const initial = initialConnectorsById.get(def.id) ?? {
|
|
879
|
-
...def,
|
|
880
|
-
presence: "removed",
|
|
881
|
-
};
|
|
882
|
-
const state = createConnectorInstance(initial, layout);
|
|
883
|
-
if (initial.presence === "removed") {
|
|
884
|
-
state.isHidden = true;
|
|
885
|
-
hideElementAfterExit(state.node);
|
|
886
|
-
}
|
|
887
|
-
applyConnectorAmbientClasses(state, initial.ambient ?? []);
|
|
888
|
-
svg.appendChild(state.node);
|
|
889
|
-
connectorMap.set(def.id, state);
|
|
890
|
-
}
|
|
891
|
-
const depthGroup = document.createElementNS(NS, "g");
|
|
892
|
-
depthGroup.classList.add("iso-depth-layer");
|
|
893
|
-
depthGroup.setAttribute("data-layer", "depth");
|
|
894
|
-
svg.appendChild(depthGroup);
|
|
895
|
-
const labelGroup = document.createElementNS(NS, "g");
|
|
896
|
-
labelGroup.classList.add("iso-layer", "iso-layer-labels");
|
|
897
|
-
labelGroup.setAttribute("data-layer", "labels");
|
|
898
|
-
svg.appendChild(labelGroup);
|
|
899
|
-
const elementMap = new Map();
|
|
900
|
-
const sortedElements = sortElementsForPerspective(allElements);
|
|
901
|
-
for (const def of sortedElements) {
|
|
902
|
-
const declaredLayer = layerMap.get(def.layer);
|
|
903
|
-
if (!declaredLayer) {
|
|
904
|
-
throw new RenderError("MISSING_LAYER", `Unknown layer for "${def.id}": ${def.layer}`);
|
|
905
|
-
}
|
|
906
|
-
const initial = initialById.get(def.id) ?? {
|
|
907
|
-
...def,
|
|
908
|
-
presence: "removed",
|
|
909
|
-
};
|
|
910
|
-
const instance = createElementInstance(initial, layout, assetResolver);
|
|
911
|
-
instance.node.classList.add(`iso-layer-${def.layer}`);
|
|
912
|
-
instance.node.setAttribute("data-layer", def.layer);
|
|
913
|
-
if (initial.presence === "removed") {
|
|
914
|
-
instance.isHidden = true;
|
|
915
|
-
hideElementAfterExit(instance.node);
|
|
916
|
-
}
|
|
917
|
-
applyAmbientClasses(instance, initial.ambient ?? []);
|
|
918
|
-
const parent = isTextAsset(def.asset) ? labelGroup : depthGroup;
|
|
919
|
-
parent.appendChild(instance.node);
|
|
920
|
-
elementMap.set(def.id, instance);
|
|
921
|
-
}
|
|
922
|
-
svg._layerMap = layerMap;
|
|
923
|
-
svg._elementMap = elementMap;
|
|
924
|
-
svg._connectorMap = connectorMap;
|
|
925
|
-
container.appendChild(svg);
|
|
926
|
-
return svg;
|
|
927
|
-
}
|
|
928
|
-
/** Update transforms and ambient classes for a live set of interpolated runtime values. */
|
|
929
|
-
function updateElementTransforms(svg, elements, connectors = []) {
|
|
930
|
-
const layout = svg._layout;
|
|
931
|
-
if (!layout)
|
|
932
|
-
return;
|
|
933
|
-
const map = svg._elementMap;
|
|
934
|
-
if (map) {
|
|
935
|
-
for (const def of elements) {
|
|
936
|
-
const state = map.get(def.id);
|
|
937
|
-
if (!state)
|
|
938
|
-
continue;
|
|
939
|
-
updateGeneratedElementContent(state.node, def, layout);
|
|
940
|
-
applyElementTransform(state.node, def, layout);
|
|
941
|
-
applyAmbientClasses(state, def.ambient ?? []);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
const connectorMap = svg._connectorMap;
|
|
945
|
-
if (!connectorMap)
|
|
946
|
-
return;
|
|
947
|
-
for (const def of connectors) {
|
|
948
|
-
const state = connectorMap.get(def.id);
|
|
949
|
-
if (!state)
|
|
950
|
-
continue;
|
|
951
|
-
applyConnectorState(state, def, layout);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
/** Read the internal ElementState for an element by its id. */
|
|
955
|
-
function getElementState(svg, id) {
|
|
956
|
-
return svg._elementMap?.get(id);
|
|
957
|
-
}
|
|
958
|
-
/** Read the internal ConnectorState for a connector by its id. */
|
|
959
|
-
function getConnectorState(svg, id) {
|
|
960
|
-
return svg._connectorMap?.get(id);
|
|
961
|
-
}
|
|
962
|
-
function getResolvedViewBox(bundle) {
|
|
963
|
-
return resolveSceneLayout(bundle).viewBox;
|
|
964
|
-
}
|
|
965
|
-
// ── Lifecycle helpers ─────────────────────────────────────────────────────
|
|
966
|
-
/** Hide an element after its exit animation completes. */
|
|
967
|
-
function hideElementAfterExit(node) {
|
|
968
|
-
node.style.visibility = "hidden";
|
|
969
|
-
node.style.pointerEvents = "none";
|
|
970
|
-
}
|
|
971
|
-
/** Show an element on re-addition. */
|
|
972
|
-
function unhideElementOnReadd(node) {
|
|
973
|
-
node.style.visibility = "visible";
|
|
974
|
-
node.style.pointerEvents = "auto";
|
|
975
|
-
}
|
|
976
|
-
// ── Layout helpers ────────────────────────────────────────────────────────
|
|
977
|
-
function resolveSceneLayout(bundle) {
|
|
978
|
-
const cellSize = bundle.grid.cellSize;
|
|
979
|
-
const padding = bundle.layout.padding;
|
|
980
|
-
const contentBounds = calculateContentBounds(bundle, cellSize);
|
|
981
|
-
const floorBounds = calculateFloorBounds(bundle, cellSize);
|
|
982
|
-
const selectedBounds = selectBounds(bundle.layout.bounds, contentBounds, floorBounds);
|
|
983
|
-
const width = selectedBounds.maxX - selectedBounds.minX + padding.x * 2;
|
|
984
|
-
const height = selectedBounds.maxY - selectedBounds.minY + padding.y * 2;
|
|
985
|
-
const viewBox = {
|
|
986
|
-
minX: 0,
|
|
987
|
-
minY: 0,
|
|
988
|
-
width: roundDimension(width || cellSize),
|
|
989
|
-
height: roundDimension(height || cellSize),
|
|
990
|
-
};
|
|
991
|
-
return {
|
|
992
|
-
cellSize,
|
|
993
|
-
padding,
|
|
994
|
-
contentBounds,
|
|
995
|
-
floorBounds,
|
|
996
|
-
selectedBounds,
|
|
997
|
-
viewBox,
|
|
998
|
-
};
|
|
999
|
-
}
|
|
1000
|
-
function calculateContentBounds(bundle, cellSize) {
|
|
1001
|
-
let bounds = emptyBounds();
|
|
1002
|
-
for (const stop of bundle.scenes) {
|
|
1003
|
-
for (const element of stop.elements ?? []) {
|
|
1004
|
-
if (element.presence === "removed")
|
|
1005
|
-
continue;
|
|
1006
|
-
const { rawX, rawY } = projectToRaw(element.pos[0] + element.size, element.pos[1] + element.size, cellSize);
|
|
1007
|
-
const visualSize = calculateVisualSize(element.size, cellSize);
|
|
1008
|
-
const [anchorX, anchorY] = assetAnchorForBounds(bundle, element);
|
|
1009
|
-
bounds = includeBounds(bounds, {
|
|
1010
|
-
minX: rawX - visualSize * anchorX,
|
|
1011
|
-
minY: rawY - visualSize * anchorY,
|
|
1012
|
-
maxX: rawX + visualSize * (1 - anchorX),
|
|
1013
|
-
maxY: rawY + visualSize * (1 - anchorY),
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
for (const connector of stop.connectors ?? []) {
|
|
1017
|
-
if (connector.presence === "removed")
|
|
1018
|
-
continue;
|
|
1019
|
-
bounds = includeBounds(bounds, calculateConnectorBounds(connector, cellSize));
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
return normalizeBounds(bounds, cellSize);
|
|
1023
|
-
}
|
|
1024
|
-
function assetAnchorForBounds(bundle, element) {
|
|
1025
|
-
return bundle.assets?.[element.asset]?.anchor ?? [0.5, 1];
|
|
1026
|
-
}
|
|
1027
|
-
function calculateConnectorBounds(connector, cellSize) {
|
|
1028
|
-
let bounds = emptyBounds();
|
|
1029
|
-
for (const [x, y] of connector.route) {
|
|
1030
|
-
const { rawX, rawY } = projectToRaw(x, y, cellSize);
|
|
1031
|
-
bounds = includePoint(bounds, rawX, rawY);
|
|
1032
|
-
}
|
|
1033
|
-
const endpointPadding = Math.max(ARROW_LENGTH_GRID, BAR_WIDTH_GRID, ENDPOINT_RADIUS_GRID * 2) * cellSize;
|
|
1034
|
-
const strokePadding = connector.style.strokeWidth / 2 + (connector.style.outlineWidth ?? 0);
|
|
1035
|
-
const padding = endpointPadding + strokePadding;
|
|
1036
|
-
return {
|
|
1037
|
-
minX: bounds.minX - padding,
|
|
1038
|
-
minY: bounds.minY - padding,
|
|
1039
|
-
maxX: bounds.maxX + padding,
|
|
1040
|
-
maxY: bounds.maxY + padding,
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
function calculateFloorBounds(bundle, cellSize) {
|
|
1044
|
-
const origin = bundle.floor.origin;
|
|
1045
|
-
const [width, height] = bundle.floor.size;
|
|
1046
|
-
const points = [
|
|
1047
|
-
origin,
|
|
1048
|
-
[origin[0] + width, origin[1]],
|
|
1049
|
-
[origin[0], origin[1] + height],
|
|
1050
|
-
[origin[0] + width, origin[1] + height],
|
|
1051
|
-
];
|
|
1052
|
-
let bounds = emptyBounds();
|
|
1053
|
-
for (const [x, y] of points) {
|
|
1054
|
-
const { rawX, rawY } = projectToRaw(x, y, cellSize);
|
|
1055
|
-
bounds = includePoint(bounds, rawX, rawY);
|
|
1056
|
-
}
|
|
1057
|
-
return normalizeBounds(bounds, cellSize);
|
|
1058
|
-
}
|
|
1059
|
-
function selectBounds(mode, content, floor) {
|
|
1060
|
-
if (mode === "content")
|
|
1061
|
-
return content;
|
|
1062
|
-
if (mode === "floor")
|
|
1063
|
-
return floor;
|
|
1064
|
-
return includeBounds(content, floor);
|
|
1065
|
-
}
|
|
1066
|
-
function emptyBounds() {
|
|
1067
|
-
return {
|
|
1068
|
-
minX: Number.POSITIVE_INFINITY,
|
|
1069
|
-
minY: Number.POSITIVE_INFINITY,
|
|
1070
|
-
maxX: Number.NEGATIVE_INFINITY,
|
|
1071
|
-
maxY: Number.NEGATIVE_INFINITY,
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
function normalizeBounds(bounds, cellSize) {
|
|
1075
|
-
if (Number.isFinite(bounds.minX))
|
|
1076
|
-
return bounds;
|
|
1077
|
-
return { minX: 0, minY: 0, maxX: cellSize, maxY: cellSize };
|
|
1078
|
-
}
|
|
1079
|
-
function includePoint(bounds, x, y) {
|
|
1080
|
-
return {
|
|
1081
|
-
minX: Math.min(bounds.minX, x),
|
|
1082
|
-
minY: Math.min(bounds.minY, y),
|
|
1083
|
-
maxX: Math.max(bounds.maxX, x),
|
|
1084
|
-
maxY: Math.max(bounds.maxY, y),
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
function includeBounds(bounds, next) {
|
|
1088
|
-
return {
|
|
1089
|
-
minX: Math.min(bounds.minX, next.minX),
|
|
1090
|
-
minY: Math.min(bounds.minY, next.minY),
|
|
1091
|
-
maxX: Math.max(bounds.maxX, next.maxX),
|
|
1092
|
-
maxY: Math.max(bounds.maxY, next.maxY),
|
|
1093
|
-
};
|
|
1094
|
-
}
|
|
1095
|
-
function roundDimension(value) {
|
|
1096
|
-
return Math.round(value * 1000) / 1000;
|
|
1097
|
-
}
|
|
1098
|
-
// ── Private helpers ───────────────────────────────────────────────────────
|
|
1099
|
-
function createRootSvg(layout, label, className) {
|
|
1100
|
-
const svg = document.createElementNS(NS, "svg");
|
|
1101
|
-
svg.classList.add("iso-scene");
|
|
1102
|
-
for (const token of className?.trim().split(/\s+/) ?? []) {
|
|
1103
|
-
if (token)
|
|
1104
|
-
svg.classList.add(token);
|
|
1105
|
-
}
|
|
1106
|
-
svg.setAttribute("width", "100%");
|
|
1107
|
-
svg.setAttribute("height", "100%");
|
|
1108
|
-
svg.setAttribute("viewBox", `${layout.viewBox.minX} ${layout.viewBox.minY} ${layout.viewBox.width} ${layout.viewBox.height}`);
|
|
1109
|
-
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1110
|
-
svg.style.width = "100%";
|
|
1111
|
-
svg.style.height = "100%";
|
|
1112
|
-
svg.style.display = "block";
|
|
1113
|
-
if (label) {
|
|
1114
|
-
svg.setAttribute("role", "img");
|
|
1115
|
-
svg.setAttribute("aria-label", label);
|
|
1116
|
-
}
|
|
1117
|
-
else {
|
|
1118
|
-
svg.setAttribute("aria-hidden", "true");
|
|
1119
|
-
}
|
|
1120
|
-
svg._layout = layout;
|
|
1121
|
-
svg._viewBoxW = layout.viewBox.width;
|
|
1122
|
-
svg._viewBoxH = layout.viewBox.height;
|
|
1123
|
-
return svg;
|
|
1124
|
-
}
|
|
1125
|
-
function createCssDefs() {
|
|
1126
|
-
const styleEl = document.createElementNS(NS, "style");
|
|
1127
|
-
styleEl.textContent = buildKeyframeCSS();
|
|
1128
|
-
return styleEl;
|
|
1129
|
-
}
|
|
1130
|
-
function createFloorGrid(bundle, layout) {
|
|
1131
|
-
const group = document.createElementNS(NS, "g");
|
|
1132
|
-
group.classList.add("iso-floor-grid", `iso-layer-${bundle.floor.layer}`);
|
|
1133
|
-
group.setAttribute("data-layer", bundle.floor.layer);
|
|
1134
|
-
const [originX, originY] = bundle.floor.origin;
|
|
1135
|
-
const [columns, rows] = bundle.floor.size;
|
|
1136
|
-
const corners = [
|
|
1137
|
-
projectGridPoint(originX, originY, layout),
|
|
1138
|
-
projectGridPoint(originX + columns, originY, layout),
|
|
1139
|
-
projectGridPoint(originX + columns, originY + rows, layout),
|
|
1140
|
-
projectGridPoint(originX, originY + rows, layout),
|
|
1141
|
-
];
|
|
1142
|
-
const slab = document.createElementNS(NS, "polygon");
|
|
1143
|
-
slab.classList.add("iso-floor-slab");
|
|
1144
|
-
slab.setAttribute("points", corners.map(pointToString).join(" "));
|
|
1145
|
-
slab.setAttribute("fill", "#dbe6f4");
|
|
1146
|
-
slab.setAttribute("fill-opacity", "0.22");
|
|
1147
|
-
slab.setAttribute("stroke", "#b9c9df");
|
|
1148
|
-
slab.setAttribute("stroke-width", "1");
|
|
1149
|
-
group.appendChild(slab);
|
|
1150
|
-
for (let x = 0; x <= columns; x++) {
|
|
1151
|
-
group.appendChild(createFloorLine(projectGridPoint(originX + x, originY, layout), projectGridPoint(originX + x, originY + rows, layout)));
|
|
1152
|
-
}
|
|
1153
|
-
for (let y = 0; y <= rows; y++) {
|
|
1154
|
-
group.appendChild(createFloorLine(projectGridPoint(originX, originY + y, layout), projectGridPoint(originX + columns, originY + y, layout)));
|
|
1155
|
-
}
|
|
1156
|
-
return group;
|
|
1157
|
-
}
|
|
1158
|
-
function createFloorLine(start, end) {
|
|
1159
|
-
const line = document.createElementNS(NS, "line");
|
|
1160
|
-
line.setAttribute("x1", String(start.x));
|
|
1161
|
-
line.setAttribute("y1", String(start.y));
|
|
1162
|
-
line.setAttribute("x2", String(end.x));
|
|
1163
|
-
line.setAttribute("y2", String(end.y));
|
|
1164
|
-
line.setAttribute("stroke", "#2563eb");
|
|
1165
|
-
line.setAttribute("stroke-width", "1");
|
|
1166
|
-
line.setAttribute("stroke-dasharray", "5 5");
|
|
1167
|
-
line.setAttribute("stroke-opacity", "0.2");
|
|
1168
|
-
return line;
|
|
1169
|
-
}
|
|
1170
|
-
function projectGridPoint(x, y, layout) {
|
|
1171
|
-
const screen = projectToScreen(x, y, layout.cellSize, layout.selectedBounds.minX, layout.selectedBounds.minY, layout.padding.x, layout.padding.y);
|
|
1172
|
-
return { x: screen.screenX, y: screen.screenY };
|
|
1173
|
-
}
|
|
1174
|
-
function pointToString(point) {
|
|
1175
|
-
return `${point.x},${point.y}`;
|
|
1176
|
-
}
|
|
1177
|
-
function createLayerMap(layers) {
|
|
1178
|
-
return new Map(layers.map((layer) => [layer.name, layer]));
|
|
1179
|
-
}
|
|
1180
|
-
function sortLayers(layers) {
|
|
1181
|
-
return [...layers]
|
|
1182
|
-
.map((layer, index) => ({ name: layer.name, order: layer.order ?? index }))
|
|
1183
|
-
.sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
|
|
1184
|
-
}
|
|
1185
|
-
function sortElementsForPerspective(elements) {
|
|
1186
|
-
return elements.slice().sort((a, b) => {
|
|
1187
|
-
const bucket = renderBucket(a) - renderBucket(b);
|
|
1188
|
-
if (bucket !== 0)
|
|
1189
|
-
return bucket;
|
|
1190
|
-
const depth = a.pos[0] + a.pos[1] - (b.pos[0] + b.pos[1]);
|
|
1191
|
-
if (depth !== 0)
|
|
1192
|
-
return depth;
|
|
1193
|
-
return a.id.localeCompare(b.id);
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
function renderBucket(element) {
|
|
1197
|
-
if (isPrimitiveAsset(element.asset))
|
|
1198
|
-
return 0;
|
|
1199
|
-
if (isTextAsset(element.asset))
|
|
1200
|
-
return 2;
|
|
1201
|
-
return 1;
|
|
1202
|
-
}
|
|
1203
|
-
function collectElementDefinitions(bundle) {
|
|
1204
|
-
const byId = new Map();
|
|
1205
|
-
for (const stop of bundle.scenes) {
|
|
1206
|
-
for (const element of stop.elements ?? []) {
|
|
1207
|
-
if (!byId.has(element.id) || byId.get(element.id)?.presence === "removed") {
|
|
1208
|
-
byId.set(element.id, element);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
return [...byId.values()];
|
|
1213
|
-
}
|
|
1214
|
-
function collectConnectorDefinitions(bundle) {
|
|
1215
|
-
const byId = new Map();
|
|
1216
|
-
for (const stop of bundle.scenes) {
|
|
1217
|
-
for (const connector of stop.connectors ?? []) {
|
|
1218
|
-
if (!byId.has(connector.id) || byId.get(connector.id)?.presence === "removed") {
|
|
1219
|
-
byId.set(connector.id, connector);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
return [...byId.values()];
|
|
1224
|
-
}
|
|
1225
|
-
/** Create a single element SVG instance applying entry animation. */
|
|
1226
|
-
function createElementInstance(def, layout, resolveAsset) {
|
|
1227
|
-
const node = def.asset === BUILT_IN_TEXT_ASSET_ID
|
|
1228
|
-
? createTextAssetNode(def.text, def.asset, layout.cellSize)
|
|
1229
|
-
: isPrimitiveAsset(def.asset)
|
|
1230
|
-
? createPrimitiveAssetNode(def.asset, def.primitive, layout.cellSize)
|
|
1231
|
-
: createResolvedAssetNode(def, resolveAsset, layout.cellSize);
|
|
1232
|
-
node.classList.add("iso-element", `iso-element-${def.id}`);
|
|
1233
|
-
node.setAttribute("data-id", def.id);
|
|
1234
|
-
node.setAttribute("data-asset", def.asset);
|
|
1235
|
-
node.style.overflow = "visible";
|
|
1236
|
-
node.style.pointerEvents = "auto";
|
|
1237
|
-
applyElementTransform(node, def, layout);
|
|
1238
|
-
const entryAnim = def.enter;
|
|
1239
|
-
if (entryAnim && entryAnim !== "none" && def.presence !== "removed") {
|
|
1240
|
-
const keyName = `iso-anim-${entryAnim}`;
|
|
1241
|
-
animateElement(node, keyName, "enter");
|
|
1242
|
-
node.addEventListener("animationend", () => {
|
|
1243
|
-
node.style.animation = "";
|
|
1244
|
-
}, { once: true });
|
|
1245
|
-
return { node, isHidden: false, entryKey: entryAnim, ambient: new Set() };
|
|
1246
|
-
}
|
|
1247
|
-
return { node, isHidden: false, ambient: new Set() };
|
|
1248
|
-
}
|
|
1249
|
-
function isTextAsset(assetId) {
|
|
1250
|
-
return assetId === BUILT_IN_TEXT_ASSET_ID;
|
|
1251
|
-
}
|
|
1252
|
-
function isPrimitiveAsset(assetId) {
|
|
1253
|
-
return BUILT_IN_PRIMITIVE_ASSET_IDS.has(assetId);
|
|
1254
|
-
}
|
|
1255
|
-
function createResolvedAssetNode(def, resolveAsset, cellSize) {
|
|
1256
|
-
const asset = resolveAsset(def.asset);
|
|
1257
|
-
if (!asset) {
|
|
1258
|
-
throw new RenderError("ASSET_NOT_FOUND", `Asset not found: ${def.asset}`, {
|
|
1259
|
-
asset: def.asset,
|
|
1260
|
-
elementId: def.id,
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
return createAssetNode(asset, def.asset, cellSize);
|
|
1264
|
-
}
|
|
1265
|
-
function updateGeneratedElementContent(node, def, layout) {
|
|
1266
|
-
if (!isTextAsset(def.asset) && !isPrimitiveAsset(def.asset))
|
|
1267
|
-
return;
|
|
1268
|
-
const replacement = isTextAsset(def.asset)
|
|
1269
|
-
? createTextAssetNode(def.text, def.asset, layout.cellSize)
|
|
1270
|
-
: createPrimitiveAssetNode(def.asset, def.primitive, layout.cellSize);
|
|
1271
|
-
clearChildren(node);
|
|
1272
|
-
while (replacement.firstChild) {
|
|
1273
|
-
const child = replacement.firstChild;
|
|
1274
|
-
replacement.removeChild(child);
|
|
1275
|
-
node.appendChild(child);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
function createConnectorInstance(def, layout) {
|
|
1279
|
-
const node = document.createElementNS(NS, "g");
|
|
1280
|
-
const shaft = document.createElementNS(NS, "path");
|
|
1281
|
-
const state = { node, shaft, isHidden: false, ambient: new Set() };
|
|
1282
|
-
applyConnectorState(state, def, layout);
|
|
1283
|
-
const entryAnim = def.enter;
|
|
1284
|
-
if (entryAnim && entryAnim !== "none" && def.presence !== "removed") {
|
|
1285
|
-
animateElement(node, `iso-anim-${entryAnim}`, "enter");
|
|
1286
|
-
node.addEventListener("animationend", () => {
|
|
1287
|
-
node.style.animation = "";
|
|
1288
|
-
}, { once: true });
|
|
1289
|
-
}
|
|
1290
|
-
return state;
|
|
1291
|
-
}
|
|
1292
|
-
function applyConnectorState(state, def, layout) {
|
|
1293
|
-
applyConnectorGroupAttrs(state.node, def);
|
|
1294
|
-
clearChildren(state.node);
|
|
1295
|
-
const d = routePath(def.route, layout);
|
|
1296
|
-
if (shouldRenderOutline(def)) {
|
|
1297
|
-
const outline = document.createElementNS(NS, "path");
|
|
1298
|
-
outline.classList.add("iso-connector-outline");
|
|
1299
|
-
applyConnectorPathAttrs(outline, def, d, {
|
|
1300
|
-
stroke: def.style.outline ?? def.style.stroke,
|
|
1301
|
-
strokeWidth: def.style.strokeWidth + def.style.outlineWidth * 2,
|
|
1302
|
-
includeDash: false,
|
|
1303
|
-
});
|
|
1304
|
-
state.node.appendChild(outline);
|
|
1305
|
-
}
|
|
1306
|
-
state.shaft = document.createElementNS(NS, "path");
|
|
1307
|
-
state.shaft.classList.add("iso-connector-shaft");
|
|
1308
|
-
applyConnectorPathAttrs(state.shaft, def, d, {
|
|
1309
|
-
stroke: def.style.stroke,
|
|
1310
|
-
strokeWidth: def.style.strokeWidth,
|
|
1311
|
-
includeDash: true,
|
|
1312
|
-
});
|
|
1313
|
-
state.node.appendChild(state.shaft);
|
|
1314
|
-
if (def.style.variant === "road" && def.style.lane === "center-dashed") {
|
|
1315
|
-
const lane = document.createElementNS(NS, "path");
|
|
1316
|
-
lane.classList.add("iso-connector-lane");
|
|
1317
|
-
applyConnectorPathAttrs(lane, def, d, {
|
|
1318
|
-
stroke: "#ffffff",
|
|
1319
|
-
strokeWidth: Math.max(1, def.style.strokeWidth * 0.12),
|
|
1320
|
-
includeDash: false,
|
|
1321
|
-
});
|
|
1322
|
-
lane.setAttribute("stroke-dasharray", "8 8");
|
|
1323
|
-
state.node.appendChild(lane);
|
|
1324
|
-
}
|
|
1325
|
-
appendEndpoint(state.node, def, "start", layout);
|
|
1326
|
-
appendEndpoint(state.node, def, "end", layout);
|
|
1327
|
-
applyConnectorAmbientClasses(state, def.ambient ?? []);
|
|
1328
|
-
}
|
|
1329
|
-
function clearChildren(node) {
|
|
1330
|
-
while (node.firstChild)
|
|
1331
|
-
node.removeChild(node.firstChild);
|
|
1332
|
-
}
|
|
1333
|
-
function applyConnectorGroupAttrs(node, def) {
|
|
1334
|
-
node.setAttribute("class", [
|
|
1335
|
-
"iso-connector",
|
|
1336
|
-
`iso-connector-${def.id}`,
|
|
1337
|
-
`iso-connector-variant-${def.style.variant}`,
|
|
1338
|
-
`iso-connector-pattern-${def.style.pattern}`,
|
|
1339
|
-
`iso-connector-direction-${def.direction}`,
|
|
1340
|
-
`iso-layer-${def.layer}`,
|
|
1341
|
-
].join(" "));
|
|
1342
|
-
node.setAttribute("data-id", def.id);
|
|
1343
|
-
node.setAttribute("data-layer", def.layer);
|
|
1344
|
-
node.style.overflow = "visible";
|
|
1345
|
-
node.style.pointerEvents = "auto";
|
|
1346
|
-
}
|
|
1347
|
-
function shouldRenderOutline(def) {
|
|
1348
|
-
return Boolean(def.style.outline && def.style.outlineWidth > 0);
|
|
1349
|
-
}
|
|
1350
|
-
function applyConnectorPathAttrs(path, def, d, options) {
|
|
1351
|
-
path.setAttribute("d", d);
|
|
1352
|
-
path.setAttribute("fill", "none");
|
|
1353
|
-
path.setAttribute("stroke", options.stroke);
|
|
1354
|
-
path.setAttribute("stroke-width", String(options.strokeWidth));
|
|
1355
|
-
path.setAttribute("stroke-linecap", "round");
|
|
1356
|
-
path.setAttribute("stroke-linejoin", "round");
|
|
1357
|
-
path.setAttribute("opacity", String(def.style.opacity));
|
|
1358
|
-
if (options.includeDash && def.style.pattern !== "solid") {
|
|
1359
|
-
const dash = def.style.dash ?? DEFAULT_CONNECTOR_DASH[def.style.pattern];
|
|
1360
|
-
path.setAttribute("stroke-dasharray", dash.join(" "));
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
function routePath(route, layout) {
|
|
1364
|
-
return route
|
|
1365
|
-
.map((point, index) => {
|
|
1366
|
-
const projected = projectGridPoint(point[0], point[1], layout);
|
|
1367
|
-
return `${index === 0 ? "M" : "L"} ${projected.x} ${projected.y}`;
|
|
1368
|
-
})
|
|
1369
|
-
.join(" ");
|
|
1370
|
-
}
|
|
1371
|
-
function appendEndpoint(group, def, kind, layout) {
|
|
1372
|
-
const endpoint = def[kind];
|
|
1373
|
-
if (endpoint === "none" || def.route.length < 2)
|
|
1374
|
-
return;
|
|
1375
|
-
const node = createEndpointNode(endpoint, def, kind, layout);
|
|
1376
|
-
node.classList.add(`iso-connector-${kind}`);
|
|
1377
|
-
group.appendChild(node);
|
|
1378
|
-
}
|
|
1379
|
-
function createEndpointNode(endpoint, def, kind, layout) {
|
|
1380
|
-
switch (endpoint) {
|
|
1381
|
-
case "arrow":
|
|
1382
|
-
return createArrowEndpoint(def, kind, layout);
|
|
1383
|
-
case "dot":
|
|
1384
|
-
return createCircleEndpoint(def, kind, layout, true);
|
|
1385
|
-
case "circle":
|
|
1386
|
-
return createCircleEndpoint(def, kind, layout, false);
|
|
1387
|
-
case "diamond":
|
|
1388
|
-
return createDiamondEndpoint(def, kind, layout);
|
|
1389
|
-
case "bar":
|
|
1390
|
-
return createBarEndpoint(def, kind, layout);
|
|
1391
|
-
case "none":
|
|
1392
|
-
throw new RenderError("CONNECTOR_ENDPOINT_NONE", "Cannot create geometry for endpoint none");
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
function createArrowEndpoint(def, kind, layout) {
|
|
1396
|
-
const tip = endpointPoint(def, kind);
|
|
1397
|
-
const direction = endpointDirection(def, kind);
|
|
1398
|
-
const perpendicular = [-direction[1], direction[0]];
|
|
1399
|
-
const base = [tip[0] - direction[0] * ARROW_LENGTH_GRID, tip[1] - direction[1] * ARROW_LENGTH_GRID];
|
|
1400
|
-
const halfWidth = ARROW_WIDTH_GRID / 2;
|
|
1401
|
-
const points = [
|
|
1402
|
-
tip,
|
|
1403
|
-
[base[0] + perpendicular[0] * halfWidth, base[1] + perpendicular[1] * halfWidth],
|
|
1404
|
-
[base[0] - perpendicular[0] * halfWidth, base[1] - perpendicular[1] * halfWidth],
|
|
1405
|
-
].map((point) => projectGridPoint(point[0], point[1], layout));
|
|
1406
|
-
const polygon = document.createElementNS(NS, "polygon");
|
|
1407
|
-
polygon.setAttribute("points", points.map(pointToString).join(" "));
|
|
1408
|
-
polygon.setAttribute("fill", def.style.stroke);
|
|
1409
|
-
polygon.setAttribute("opacity", String(def.style.opacity));
|
|
1410
|
-
return polygon;
|
|
1411
|
-
}
|
|
1412
|
-
function createCircleEndpoint(def, kind, layout, filled) {
|
|
1413
|
-
const point = endpointPoint(def, kind);
|
|
1414
|
-
const projected = projectGridPoint(point[0], point[1], layout);
|
|
1415
|
-
const circle = document.createElementNS(NS, "circle");
|
|
1416
|
-
circle.setAttribute("cx", String(projected.x));
|
|
1417
|
-
circle.setAttribute("cy", String(projected.y));
|
|
1418
|
-
circle.setAttribute("r", String(ENDPOINT_RADIUS_GRID * layout.cellSize));
|
|
1419
|
-
circle.setAttribute("stroke", def.style.stroke);
|
|
1420
|
-
circle.setAttribute("stroke-width", String(Math.max(1, def.style.strokeWidth)));
|
|
1421
|
-
circle.setAttribute("opacity", String(def.style.opacity));
|
|
1422
|
-
circle.setAttribute("fill", filled ? def.style.stroke : "none");
|
|
1423
|
-
return circle;
|
|
1424
|
-
}
|
|
1425
|
-
function createDiamondEndpoint(def, kind, layout) {
|
|
1426
|
-
const center = endpointPoint(def, kind);
|
|
1427
|
-
const radius = ENDPOINT_RADIUS_GRID;
|
|
1428
|
-
const points = [
|
|
1429
|
-
[center[0], center[1] - radius],
|
|
1430
|
-
[center[0] + radius, center[1]],
|
|
1431
|
-
[center[0], center[1] + radius],
|
|
1432
|
-
[center[0] - radius, center[1]],
|
|
1433
|
-
];
|
|
1434
|
-
const polygon = document.createElementNS(NS, "polygon");
|
|
1435
|
-
polygon.setAttribute("points", points
|
|
1436
|
-
.map((point) => projectGridPoint(point[0], point[1], layout))
|
|
1437
|
-
.map(pointToString)
|
|
1438
|
-
.join(" "));
|
|
1439
|
-
polygon.setAttribute("fill", def.style.stroke);
|
|
1440
|
-
polygon.setAttribute("opacity", String(def.style.opacity));
|
|
1441
|
-
return polygon;
|
|
1442
|
-
}
|
|
1443
|
-
function createBarEndpoint(def, kind, layout) {
|
|
1444
|
-
const center = endpointPoint(def, kind);
|
|
1445
|
-
const direction = endpointDirection(def, kind);
|
|
1446
|
-
const perpendicular = [-direction[1], direction[0]];
|
|
1447
|
-
const half = BAR_WIDTH_GRID / 2;
|
|
1448
|
-
const a = projectGridPoint(center[0] + perpendicular[0] * half, center[1] + perpendicular[1] * half, layout);
|
|
1449
|
-
const b = projectGridPoint(center[0] - perpendicular[0] * half, center[1] - perpendicular[1] * half, layout);
|
|
1450
|
-
const line = document.createElementNS(NS, "line");
|
|
1451
|
-
line.setAttribute("x1", String(a.x));
|
|
1452
|
-
line.setAttribute("y1", String(a.y));
|
|
1453
|
-
line.setAttribute("x2", String(b.x));
|
|
1454
|
-
line.setAttribute("y2", String(b.y));
|
|
1455
|
-
line.setAttribute("stroke", def.style.stroke);
|
|
1456
|
-
line.setAttribute("stroke-width", String(Math.max(1, def.style.strokeWidth)));
|
|
1457
|
-
line.setAttribute("stroke-linecap", "round");
|
|
1458
|
-
line.setAttribute("opacity", String(def.style.opacity));
|
|
1459
|
-
return line;
|
|
1460
|
-
}
|
|
1461
|
-
function endpointPoint(def, kind) {
|
|
1462
|
-
return kind === "start" ? def.route[0] : def.route[def.route.length - 1];
|
|
1463
|
-
}
|
|
1464
|
-
function endpointDirection(def, kind) {
|
|
1465
|
-
const point = kind === "start"
|
|
1466
|
-
? vectorBetween(def.route[0], def.route[1])
|
|
1467
|
-
: vectorBetween(def.route[def.route.length - 2], def.route[def.route.length - 1]);
|
|
1468
|
-
const effective = def.direction === "reverse" ? [-point[0], -point[1]] : point;
|
|
1469
|
-
return normalizeVector(effective);
|
|
1470
|
-
}
|
|
1471
|
-
function vectorBetween(start, end) {
|
|
1472
|
-
return [end[0] - start[0], end[1] - start[1]];
|
|
1473
|
-
}
|
|
1474
|
-
function normalizeVector(vector) {
|
|
1475
|
-
const length = Math.hypot(vector[0], vector[1]);
|
|
1476
|
-
if (length === 0)
|
|
1477
|
-
return [1, 0];
|
|
1478
|
-
return [vector[0] / length, vector[1] / length];
|
|
1479
|
-
}
|
|
1480
|
-
function applyElementTransform(node, def, layout) {
|
|
1481
|
-
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);
|
|
1482
|
-
const visualSize = calculateVisualSize(def.size, layout.cellSize);
|
|
1483
|
-
const scale = visualSize / layout.cellSize;
|
|
1484
|
-
node.setAttribute("transform", `translate(${screen.screenX} ${screen.screenY}) scale(${scale})`);
|
|
1485
|
-
}
|
|
1486
|
-
function applyAmbientClasses(state, ambient) {
|
|
1487
|
-
const next = new Set((ambient ?? []).map((item) => item.name));
|
|
1488
|
-
for (const name of state.ambient) {
|
|
1489
|
-
if (!next.has(name))
|
|
1490
|
-
state.node.classList.remove(`iso-ambient-${name}`);
|
|
1491
|
-
}
|
|
1492
|
-
for (const name of next) {
|
|
1493
|
-
if (!state.ambient.has(name))
|
|
1494
|
-
state.node.classList.add(`iso-ambient-${name}`);
|
|
1495
|
-
}
|
|
1496
|
-
state.ambient = next;
|
|
1497
|
-
}
|
|
1498
|
-
function applyConnectorAmbientClasses(state, ambient) {
|
|
1499
|
-
const next = new Set((ambient ?? []).map((item) => item.name));
|
|
1500
|
-
for (const name of state.ambient) {
|
|
1501
|
-
if (!next.has(name))
|
|
1502
|
-
state.shaft.classList.remove(`iso-ambient-${name}`);
|
|
1503
|
-
}
|
|
1504
|
-
for (const name of next) {
|
|
1505
|
-
state.shaft.classList.add(`iso-ambient-${name}`);
|
|
1506
|
-
}
|
|
1507
|
-
state.ambient = next;
|
|
1508
|
-
}
|
|
1509
|
-
/** Apply a CSS keyframe animation to an element. */
|
|
1510
|
-
function animateElement(node, keyframeName, type = "enter") {
|
|
1511
|
-
node.style.opacity = "1";
|
|
1512
|
-
node.style.animation = "none";
|
|
1513
|
-
node.getBoundingClientRect();
|
|
1514
|
-
const duration = type === "exit" ? "var(--iso-anim-duration-exit, 300ms)" : "var(--iso-anim-duration-enter, 400ms)";
|
|
1515
|
-
const easing = type === "exit" ? "var(--iso-anim-easing-exit, ease-in)" : "var(--iso-anim-easing-enter, ease-out)";
|
|
1516
|
-
node.style.animation = `${keyframeName} ${duration} ${easing} forwards`;
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
498
|
/**
|
|
1520
499
|
* Linear easing (no interpolation).
|
|
1521
500
|
*/
|
|
@@ -1586,6 +565,8 @@ class AnimationController {
|
|
|
1586
565
|
_pendingProgress = null;
|
|
1587
566
|
_destroyed = false;
|
|
1588
567
|
_listeners = new Map();
|
|
568
|
+
_cameraAnim = null;
|
|
569
|
+
_cameraState = null;
|
|
1589
570
|
// Scroll tracking state
|
|
1590
571
|
_minScroll = 0;
|
|
1591
572
|
_maxScroll = 0;
|
|
@@ -1637,6 +618,11 @@ class AnimationController {
|
|
|
1637
618
|
this._paused = false;
|
|
1638
619
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
1639
620
|
this._sceneElement = runtime.sceneElement ?? config.sceneElement ?? null;
|
|
621
|
+
this._cameraState = {
|
|
622
|
+
viewBox: getResolvedViewBox(bundle),
|
|
623
|
+
target: { type: "reset" },
|
|
624
|
+
isZoomed: false,
|
|
625
|
+
};
|
|
1640
626
|
this._engine.init(bundle);
|
|
1641
627
|
this._bindScroll();
|
|
1642
628
|
}
|
|
@@ -1653,6 +639,7 @@ class AnimationController {
|
|
|
1653
639
|
return;
|
|
1654
640
|
}
|
|
1655
641
|
this._progress = clamped;
|
|
642
|
+
this._cancelCameraAnimation();
|
|
1656
643
|
if (this._paused)
|
|
1657
644
|
return;
|
|
1658
645
|
this._scheduleProgressForward(clamped);
|
|
@@ -1720,6 +707,47 @@ class AnimationController {
|
|
|
1720
707
|
this._assertNotDestroyed();
|
|
1721
708
|
return this._paused;
|
|
1722
709
|
}
|
|
710
|
+
zoomToElement(id, options = {}) {
|
|
711
|
+
this._assertNotDestroyed();
|
|
712
|
+
const svg = this._getSceneSvg();
|
|
713
|
+
if (!this._bundle || !svg) {
|
|
714
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "zoomToElement() requires an initialized scene SVG");
|
|
715
|
+
}
|
|
716
|
+
const elementBounds = getCurrentElementBounds(svg, id);
|
|
717
|
+
if (!elementBounds) {
|
|
718
|
+
throw new ControllerError("CAMERA_TARGET_NOT_FOUND", `Camera target element "${id}" was not found`, {
|
|
719
|
+
elementId: id,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
this._applyCameraDestination(expandViewBox(elementBounds, this._resolveCameraPadding(options)), { type: "element", id }, options);
|
|
723
|
+
}
|
|
724
|
+
zoomToArea(area, options = {}) {
|
|
725
|
+
this._assertNotDestroyed();
|
|
726
|
+
if (!this._bundle || !this._getSceneSvg()) {
|
|
727
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "zoomToArea() requires an initialized scene SVG");
|
|
728
|
+
}
|
|
729
|
+
this._assertValidCameraArea(area);
|
|
730
|
+
const bounds = getGridAreaBounds(this._bundle, area);
|
|
731
|
+
this._applyCameraDestination(expandViewBox(bounds, this._resolveCameraPadding(options)), {
|
|
732
|
+
type: "area",
|
|
733
|
+
at: [...area.at],
|
|
734
|
+
size: [...area.size],
|
|
735
|
+
}, options);
|
|
736
|
+
}
|
|
737
|
+
resetZoom(options = {}) {
|
|
738
|
+
this._assertNotDestroyed();
|
|
739
|
+
if (!this._bundle || !this._getSceneSvg()) {
|
|
740
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "resetZoom() requires an initialized scene SVG");
|
|
741
|
+
}
|
|
742
|
+
this._applyCameraDestination(getResolvedViewBox(this._bundle), { type: "reset" }, options);
|
|
743
|
+
}
|
|
744
|
+
getCameraState() {
|
|
745
|
+
this._assertNotDestroyed();
|
|
746
|
+
if (!this._cameraState) {
|
|
747
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera state is not initialized");
|
|
748
|
+
}
|
|
749
|
+
return copyCameraState(this._cameraState);
|
|
750
|
+
}
|
|
1723
751
|
/**
|
|
1724
752
|
* Destroy controller and clean up all listeners and resources.
|
|
1725
753
|
*/
|
|
@@ -1728,12 +756,14 @@ class AnimationController {
|
|
|
1728
756
|
this._unbindScroll();
|
|
1729
757
|
this._cancelFrame();
|
|
1730
758
|
this._cancelTransition();
|
|
759
|
+
this._cancelCameraAnimation();
|
|
1731
760
|
if (this._ownsEngine)
|
|
1732
761
|
this._engine.destroy();
|
|
1733
762
|
this._listeners.clear();
|
|
1734
763
|
this._bundle = null;
|
|
1735
764
|
this._container = null;
|
|
1736
765
|
this._pendingProgress = null;
|
|
766
|
+
this._cameraState = null;
|
|
1737
767
|
this._destroyed = true;
|
|
1738
768
|
}
|
|
1739
769
|
// ── Event system ───────────────────────────────────────────────────────
|
|
@@ -1823,6 +853,121 @@ class AnimationController {
|
|
|
1823
853
|
updateElementTransforms(svg, updates, connectors);
|
|
1824
854
|
this._applyLifecycleChanges(updates);
|
|
1825
855
|
this._applyConnectorLifecycleChanges(connectors);
|
|
856
|
+
this._applyCameraForProgress(this._progress);
|
|
857
|
+
}
|
|
858
|
+
_applyCameraForProgress(progress) {
|
|
859
|
+
const svg = this._getSceneSvg();
|
|
860
|
+
if (!this._bundle || !svg)
|
|
861
|
+
return;
|
|
862
|
+
const sceneStops = this.scenes;
|
|
863
|
+
if (sceneStops.length === 0)
|
|
864
|
+
return;
|
|
865
|
+
const { previous, next } = surroundingStops(sceneStops, progress);
|
|
866
|
+
const previousCamera = this._effectiveCameraAt(previous.index);
|
|
867
|
+
const nextCamera = this._effectiveCameraAt(next.index);
|
|
868
|
+
const previousViewBox = this._resolveCameraViewBox(previousCamera);
|
|
869
|
+
const nextViewBox = this._resolveCameraViewBox(nextCamera);
|
|
870
|
+
const range = next.scene.progress - previous.scene.progress;
|
|
871
|
+
const rawT = range <= 0 ? 0 : (progress - previous.scene.progress) / range;
|
|
872
|
+
const t = resolveCameraEasing(nextCamera.easing)(Math.max(0, Math.min(1, rawT)));
|
|
873
|
+
const viewBox = lerpViewBox(previousViewBox, nextViewBox, t);
|
|
874
|
+
const target = t < 1 ? previousCamera.target : nextCamera.target;
|
|
875
|
+
this._setCameraViewBox(svg, viewBox, target);
|
|
876
|
+
}
|
|
877
|
+
_effectiveCameraAt(index) {
|
|
878
|
+
for (let i = index; i >= 0; i--) {
|
|
879
|
+
const camera = this.scenes[i]?.camera;
|
|
880
|
+
if (camera) {
|
|
881
|
+
return {
|
|
882
|
+
target: camera.target,
|
|
883
|
+
padding: camera.padding,
|
|
884
|
+
duration: camera.duration,
|
|
885
|
+
easing: camera.easing,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return { target: { type: "reset" } };
|
|
890
|
+
}
|
|
891
|
+
_resolveCameraViewBox(camera) {
|
|
892
|
+
if (!this._bundle)
|
|
893
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized bundle");
|
|
894
|
+
if (camera.target.type === "reset")
|
|
895
|
+
return getResolvedViewBox(this._bundle);
|
|
896
|
+
if (camera.target.type === "area") {
|
|
897
|
+
return expandViewBox(getGridAreaBounds(this._bundle, { at: camera.target.at, size: camera.target.size }), camera.padding ?? 32);
|
|
898
|
+
}
|
|
899
|
+
const svg = this._getSceneSvg();
|
|
900
|
+
if (!svg)
|
|
901
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized scene SVG");
|
|
902
|
+
const bounds = getCurrentElementBounds(svg, camera.target.id);
|
|
903
|
+
if (!bounds) {
|
|
904
|
+
throw new ControllerError("CAMERA_TARGET_NOT_FOUND", `Camera target element "${camera.target.id}" was not found`, {
|
|
905
|
+
elementId: camera.target.id,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return expandViewBox(bounds, camera.padding ?? 32);
|
|
909
|
+
}
|
|
910
|
+
_applyCameraDestination(viewBox, target, options) {
|
|
911
|
+
const svg = this._getSceneSvg();
|
|
912
|
+
if (!svg)
|
|
913
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized scene SVG");
|
|
914
|
+
this._cancelCameraAnimation();
|
|
915
|
+
const duration = this._resolveCameraDuration(options);
|
|
916
|
+
if (duration === 0) {
|
|
917
|
+
this._setCameraViewBox(svg, viewBox, target);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const from = currentSvgViewBox(svg) ?? this._cameraState?.viewBox ?? viewBox;
|
|
921
|
+
const easing = resolveCameraEasing(options.easing ?? this._config.transitionEasing);
|
|
922
|
+
const start = performance.now();
|
|
923
|
+
const step = (now) => {
|
|
924
|
+
const t = Math.min((now - start) / duration, 1);
|
|
925
|
+
this._setCameraViewBox(svg, lerpViewBox(from, viewBox, easing(t)), target);
|
|
926
|
+
if (t < 1) {
|
|
927
|
+
this._cameraAnim = requestAnimationFrame(step);
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
this._cameraAnim = null;
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
this._cameraAnim = requestAnimationFrame(step);
|
|
934
|
+
}
|
|
935
|
+
_setCameraViewBox(svg, viewBox, target) {
|
|
936
|
+
applySceneViewBox(svg, viewBox);
|
|
937
|
+
const full = this._bundle ? getResolvedViewBox(this._bundle) : viewBox;
|
|
938
|
+
this._cameraState = {
|
|
939
|
+
viewBox,
|
|
940
|
+
target,
|
|
941
|
+
isZoomed: !sameViewBox(viewBox, full),
|
|
942
|
+
};
|
|
943
|
+
this._emit("camera-change", this.getCameraState());
|
|
944
|
+
}
|
|
945
|
+
_getSceneSvg() {
|
|
946
|
+
return (this._sceneElement ?? this._container?.querySelector("svg") ?? null);
|
|
947
|
+
}
|
|
948
|
+
_resolveCameraPadding(options) {
|
|
949
|
+
const padding = options.padding ?? 32;
|
|
950
|
+
if (!Number.isFinite(padding) || padding < 0 || padding > 2048) {
|
|
951
|
+
throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera padding must be between 0 and 2048");
|
|
952
|
+
}
|
|
953
|
+
return padding;
|
|
954
|
+
}
|
|
955
|
+
_resolveCameraDuration(options) {
|
|
956
|
+
const duration = options.duration ?? this._config.transitionDuration;
|
|
957
|
+
if (!Number.isInteger(duration) || duration < 0 || duration > 10000) {
|
|
958
|
+
throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera duration must be an integer from 0 to 10000");
|
|
959
|
+
}
|
|
960
|
+
return duration;
|
|
961
|
+
}
|
|
962
|
+
_assertValidCameraArea(area) {
|
|
963
|
+
if (!Array.isArray(area.at) ||
|
|
964
|
+
area.at.length !== 2 ||
|
|
965
|
+
!area.at.every((value) => Number.isFinite(value) && value >= 0) ||
|
|
966
|
+
!Array.isArray(area.size) ||
|
|
967
|
+
area.size.length !== 2 ||
|
|
968
|
+
!area.size.every((value) => Number.isFinite(value) && value > 0)) {
|
|
969
|
+
throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera area must use non-negative at and positive size");
|
|
970
|
+
}
|
|
1826
971
|
}
|
|
1827
972
|
_applyLifecycleChanges(elements) {
|
|
1828
973
|
for (const elDef of elements) {
|
|
@@ -2002,6 +1147,12 @@ class AnimationController {
|
|
|
2002
1147
|
this._transitionAnim = null;
|
|
2003
1148
|
}
|
|
2004
1149
|
}
|
|
1150
|
+
_cancelCameraAnimation() {
|
|
1151
|
+
if (this._cameraAnim !== null) {
|
|
1152
|
+
cancelAnimationFrame(this._cameraAnim);
|
|
1153
|
+
this._cameraAnim = null;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
2005
1156
|
// ── Scroll binding ─────────────────────────────────────────────────────
|
|
2006
1157
|
_bindScroll() {
|
|
2007
1158
|
const container = this._config.container;
|
|
@@ -2186,9 +1337,70 @@ function oppositeEntryAnimation(exit) {
|
|
|
2186
1337
|
return "none";
|
|
2187
1338
|
}
|
|
2188
1339
|
}
|
|
1340
|
+
function surroundingStops(scenes, progress) {
|
|
1341
|
+
let previousIndex = 0;
|
|
1342
|
+
let nextIndex = scenes.length - 1;
|
|
1343
|
+
for (let i = 0; i < scenes.length; i++) {
|
|
1344
|
+
if (scenes[i].progress <= progress)
|
|
1345
|
+
previousIndex = i;
|
|
1346
|
+
if (scenes[i].progress >= progress) {
|
|
1347
|
+
nextIndex = i;
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
previous: { scene: scenes[previousIndex], index: previousIndex },
|
|
1353
|
+
next: { scene: scenes[nextIndex], index: nextIndex },
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function expandViewBox(viewBox, padding) {
|
|
1357
|
+
return {
|
|
1358
|
+
minX: roundCameraNumber(viewBox.minX - padding),
|
|
1359
|
+
minY: roundCameraNumber(viewBox.minY - padding),
|
|
1360
|
+
width: roundCameraNumber(Math.max(1, viewBox.width + padding * 2)),
|
|
1361
|
+
height: roundCameraNumber(Math.max(1, viewBox.height + padding * 2)),
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function lerpViewBox(from, to, t) {
|
|
1365
|
+
return {
|
|
1366
|
+
minX: roundCameraNumber(from.minX + (to.minX - from.minX) * t),
|
|
1367
|
+
minY: roundCameraNumber(from.minY + (to.minY - from.minY) * t),
|
|
1368
|
+
width: roundCameraNumber(from.width + (to.width - from.width) * t),
|
|
1369
|
+
height: roundCameraNumber(from.height + (to.height - from.height) * t),
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function sameViewBox(a, b) {
|
|
1373
|
+
return a.minX === b.minX && a.minY === b.minY && a.width === b.width && a.height === b.height;
|
|
1374
|
+
}
|
|
1375
|
+
function currentSvgViewBox(svg) {
|
|
1376
|
+
const raw = svg.getAttribute("viewBox");
|
|
1377
|
+
if (!raw)
|
|
1378
|
+
return undefined;
|
|
1379
|
+
const parts = raw.trim().split(/\s+/).map(Number);
|
|
1380
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part)))
|
|
1381
|
+
return undefined;
|
|
1382
|
+
return { minX: parts[0], minY: parts[1], width: parts[2], height: parts[3] };
|
|
1383
|
+
}
|
|
1384
|
+
function resolveCameraEasing(easing) {
|
|
1385
|
+
return resolveEasing(easing === "ease-in-out" ? "easeInOutCubic" : easing === "ease-out" ? "easeOutCubic" : "linear");
|
|
1386
|
+
}
|
|
1387
|
+
function copyCameraState(state) {
|
|
1388
|
+
return {
|
|
1389
|
+
viewBox: { ...state.viewBox },
|
|
1390
|
+
target: state.target?.type === "area"
|
|
1391
|
+
? { type: "area", at: [...state.target.at], size: [...state.target.size] }
|
|
1392
|
+
: state.target
|
|
1393
|
+
? { ...state.target }
|
|
1394
|
+
: undefined,
|
|
1395
|
+
isZoomed: state.isZoomed,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function roundCameraNumber(value) {
|
|
1399
|
+
return Math.round(value * 1000) / 1000;
|
|
1400
|
+
}
|
|
2189
1401
|
|
|
2190
1402
|
const RUNTIME_BUNDLE_FORMAT = "isostate-runtime-bundle";
|
|
2191
|
-
const RUNTIME_VERSION = "0.
|
|
1403
|
+
const RUNTIME_VERSION = "0.3.0";
|
|
2192
1404
|
const HEX_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
2193
1405
|
/** Mount a compiled runtime bundle into an HTML element. */
|
|
2194
1406
|
function mountScene(target, bundle, options = {}) {
|
|
@@ -2278,6 +1490,10 @@ function getResolvedConfig(bundle, options = {}) {
|
|
|
2278
1490
|
align: [...bundle.layout.align],
|
|
2279
1491
|
},
|
|
2280
1492
|
viewBox: getResolvedViewBox(bundle),
|
|
1493
|
+
camera: {
|
|
1494
|
+
viewBox: getResolvedViewBox(bundle),
|
|
1495
|
+
isZoomed: false,
|
|
1496
|
+
},
|
|
2281
1497
|
theme: bundle.theme,
|
|
2282
1498
|
themeVars: getResolvedThemeVars(bundle, options.themeVars),
|
|
2283
1499
|
scenes: bundle.scenes.map((scene) => ({
|
|
@@ -2321,71 +1537,6 @@ function normalizeValue(value) {
|
|
|
2321
1537
|
function isPlainObject(value) {
|
|
2322
1538
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2323
1539
|
}
|
|
2324
|
-
function sha256(input) {
|
|
2325
|
-
const bytes = utf8Bytes(input);
|
|
2326
|
-
const bitLength = bytes.length * 8;
|
|
2327
|
-
bytes.push(0x80);
|
|
2328
|
-
while (bytes.length % 64 !== 56)
|
|
2329
|
-
bytes.push(0);
|
|
2330
|
-
for (let i = 7; i >= 0; i--) {
|
|
2331
|
-
bytes.push(Math.floor(bitLength / 2 ** (i * 8)) & 0xff);
|
|
2332
|
-
}
|
|
2333
|
-
const h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
2334
|
-
const k = [
|
|
2335
|
-
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
|
|
2336
|
-
0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
2337
|
-
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
|
|
2338
|
-
0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
2339
|
-
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
|
|
2340
|
-
0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
2341
|
-
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
|
|
2342
|
-
0xc67178f2,
|
|
2343
|
-
];
|
|
2344
|
-
const w = new Array(64);
|
|
2345
|
-
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
2346
|
-
for (let i = 0; i < 16; i++) {
|
|
2347
|
-
const j = offset + i * 4;
|
|
2348
|
-
w[i] = ((bytes[j] << 24) | (bytes[j + 1] << 16) | (bytes[j + 2] << 8) | bytes[j + 3]) >>> 0;
|
|
2349
|
-
}
|
|
2350
|
-
for (let i = 16; i < 64; i++) {
|
|
2351
|
-
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
2352
|
-
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
2353
|
-
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
2354
|
-
}
|
|
2355
|
-
let [a, b, c, d, e, f, g, hh] = h;
|
|
2356
|
-
for (let i = 0; i < 64; i++) {
|
|
2357
|
-
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
2358
|
-
const ch = (e & f) ^ (~e & g);
|
|
2359
|
-
const temp1 = (hh + s1 + ch + k[i] + w[i]) >>> 0;
|
|
2360
|
-
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
2361
|
-
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
2362
|
-
const temp2 = (s0 + maj) >>> 0;
|
|
2363
|
-
hh = g;
|
|
2364
|
-
g = f;
|
|
2365
|
-
f = e;
|
|
2366
|
-
e = (d + temp1) >>> 0;
|
|
2367
|
-
d = c;
|
|
2368
|
-
c = b;
|
|
2369
|
-
b = a;
|
|
2370
|
-
a = (temp1 + temp2) >>> 0;
|
|
2371
|
-
}
|
|
2372
|
-
h[0] = (h[0] + a) >>> 0;
|
|
2373
|
-
h[1] = (h[1] + b) >>> 0;
|
|
2374
|
-
h[2] = (h[2] + c) >>> 0;
|
|
2375
|
-
h[3] = (h[3] + d) >>> 0;
|
|
2376
|
-
h[4] = (h[4] + e) >>> 0;
|
|
2377
|
-
h[5] = (h[5] + f) >>> 0;
|
|
2378
|
-
h[6] = (h[6] + g) >>> 0;
|
|
2379
|
-
h[7] = (h[7] + hh) >>> 0;
|
|
2380
|
-
}
|
|
2381
|
-
return h.map((value) => value.toString(16).padStart(8, "0")).join("");
|
|
2382
|
-
}
|
|
2383
|
-
function utf8Bytes(input) {
|
|
2384
|
-
return Array.from(new TextEncoder().encode(input));
|
|
2385
|
-
}
|
|
2386
|
-
function rotr(value, bits) {
|
|
2387
|
-
return (value >>> bits) | (value << (32 - bits));
|
|
2388
|
-
}
|
|
2389
1540
|
|
|
2390
|
-
export { AnimationController as A,
|
|
2391
|
-
//# sourceMappingURL=index-
|
|
1541
|
+
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-B6HlPnov.js.map
|