@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
@@ -35,13 +35,13 @@ const BUILTIN_THEMES = {
35
35
  * Returns undefined if the theme is not found.
36
36
  */
37
37
  function resolveTheme(name) {
38
- return BUILTIN_THEMES[name];
38
+ return Object.hasOwn(BUILTIN_THEMES, name) ? BUILTIN_THEMES[name] : undefined;
39
39
  }
40
40
  /**
41
41
  * Compose a new theme by extending an existing one with overrides.
42
42
  */
43
43
  function composeTheme(baseName, overrides) {
44
- const base = BUILTIN_THEMES[baseName];
44
+ const base = resolveTheme(baseName);
45
45
  if (!base) {
46
46
  return { name: baseName, vars: { ...overrides } };
47
47
  }
@@ -73,6 +73,7 @@ class AssetRegistryImpl {
73
73
  this._assets.delete(id);
74
74
  }
75
75
  }
76
+ /** Create a registry populated with the given asset definitions. */
76
77
  function createAssetRegistry(assets = []) {
77
78
  const registry = new AssetRegistryImpl();
78
79
  for (const asset of assets) {
@@ -161,12 +162,19 @@ function buildKeyframeCSS() {
161
162
  "@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))}}",
162
163
  "@keyframes iso-anim-spin{from{rotate:0deg}to{rotate:360deg}}",
163
164
  "@keyframes iso-anim-blink{0%,49%{opacity:1}50%,100%{opacity:0}}",
165
+ ".iso-ambient-pulse{animation:iso-anim-pulse 2s ease-in-out infinite}",
166
+ ".iso-ambient-float{animation:iso-anim-float 3s ease-in-out infinite}",
167
+ ".iso-ambient-shake{animation:iso-anim-shake 0.5s ease-in-out infinite}",
168
+ ".iso-ambient-glow{animation:iso-anim-glow 2s ease-in-out infinite}",
169
+ ".iso-ambient-spin{animation:iso-anim-spin 4s linear infinite}",
170
+ ".iso-ambient-blink{animation:iso-anim-blink 1.5s step-end infinite}",
164
171
  "@keyframes iso-connector-flow-route{from{stroke-dashoffset:0}to{stroke-dashoffset:calc(-1px * var(--iso-flow-distance,20))}}",
165
172
  "@keyframes iso-connector-flow-reverse{from{stroke-dashoffset:0}to{stroke-dashoffset:calc(1px * var(--iso-flow-distance,20))}}",
166
173
  ".iso-connector-pattern-dashed .iso-connector-shaft,.iso-connector-pattern-dotted .iso-connector-shaft{--iso-flow-distance:20}",
167
174
  ".iso-connector-direction-route .iso-connector-shaft.iso-ambient-flow{animation:iso-connector-flow-route 900ms linear infinite}",
168
175
  ".iso-connector-direction-reverse .iso-connector-shaft.iso-ambient-flow{animation:iso-connector-flow-reverse 900ms linear infinite}",
169
176
  "@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}}",
177
+ ".iso-interactive g[data-id]{cursor:pointer}",
170
178
  ].join("\n");
171
179
  }
172
180
 
@@ -374,6 +382,10 @@ function isSafeAssetUrl(url) {
374
382
  }
375
383
 
376
384
  const CSS_CUSTOM_PROPERTY_PATTERN = /^--[a-zA-Z0-9-_]+$/;
385
+ /**
386
+ * Apply theme CSS custom properties to an element's inline style.
387
+ * Throws `RenderError` (`INVALID_THEME_VAR`) if a key is not a valid `--name` custom property.
388
+ */
377
389
  function applyThemeToElement(element, themeVars) {
378
390
  for (const [name, value] of Object.entries(themeVars)) {
379
391
  if (!CSS_CUSTOM_PROPERTY_PATTERN.test(name)) {
@@ -466,6 +478,7 @@ function buildSceneDOM(container, bundle, config) {
466
478
  svg._layerMap = layerMap;
467
479
  svg._elementMap = elementMap;
468
480
  svg._connectorMap = connectorMap;
481
+ svg._depthGroup = depthGroup;
469
482
  container.appendChild(svg);
470
483
  return svg;
471
484
  }
@@ -480,11 +493,17 @@ function updateElementTransforms(svg, elements, connectors = []) {
480
493
  const state = map.get(def.id);
481
494
  if (!state)
482
495
  continue;
496
+ const previous = state.current;
483
497
  state.current = def;
484
- updateGeneratedElementContent(state.node, def, layout);
498
+ if (!hasEqualGeneratedContent(previous, def)) {
499
+ updateGeneratedElementContent(state.node, def, layout);
500
+ }
485
501
  applyElementTransform(state.node, def, layout);
486
502
  applyAmbientClasses(state, def.ambient ?? []);
487
503
  }
504
+ if (svg._depthGroup) {
505
+ reconcileDepthOrder(svg._depthGroup, map);
506
+ }
488
507
  }
489
508
  const connectorMap = svg._connectorMap;
490
509
  if (!connectorMap)
@@ -798,11 +817,45 @@ function sortElementsForPerspective(elements) {
798
817
  });
799
818
  }
800
819
  function renderBucket(element) {
801
- if (isPrimitiveAsset(element.asset))
802
- return 0;
803
820
  if (isTextAsset(element.asset))
804
- return 2;
805
- return 1;
821
+ return 1;
822
+ return 0;
823
+ }
824
+ /**
825
+ * Re-sort the depth group's live child nodes to match the current
826
+ * `(x + y)` + id perspective order. Only elements whose relative order
827
+ * changed are moved so in-flight CSS animation state on unaffected
828
+ * elements is preserved. Text elements live in the label group and are
829
+ * never part of this reconciliation.
830
+ */
831
+ function reconcileDepthOrder(depthGroup, elementMap) {
832
+ const current = [];
833
+ for (const state of elementMap.values()) {
834
+ if (state.node.parentElement !== depthGroup)
835
+ continue;
836
+ current.push(state.current);
837
+ }
838
+ if (current.length < 2)
839
+ return;
840
+ const desiredOrder = sortElementsForPerspective(current);
841
+ const existingOrder = [...depthGroup.childNodes];
842
+ let orderMatches = existingOrder.length === desiredOrder.length;
843
+ if (orderMatches) {
844
+ for (let index = 0; index < desiredOrder.length; index++) {
845
+ const expectedId = desiredOrder[index]?.id;
846
+ if (existingOrder[index]?.getAttribute("data-id") !== expectedId) {
847
+ orderMatches = false;
848
+ break;
849
+ }
850
+ }
851
+ }
852
+ if (orderMatches)
853
+ return;
854
+ for (const def of desiredOrder) {
855
+ const state = elementMap.get(def.id);
856
+ if (state)
857
+ depthGroup.appendChild(state.node);
858
+ }
806
859
  }
807
860
  function collectElementDefinitions(bundle) {
808
861
  const byId = new Map();
@@ -879,6 +932,41 @@ function updateGeneratedElementContent(node, def, layout) {
879
932
  node.appendChild(child);
880
933
  }
881
934
  }
935
+ /**
936
+ * Compare the generated-content payload (`text` or `primitive`) of two
937
+ * element states for structural equality. Used to skip destructive
938
+ * regeneration of text/primitive DOM content on frame updates where the
939
+ * asset, id, and content payload are unchanged from the previous frame.
940
+ */
941
+ function hasEqualGeneratedContent(previous, next) {
942
+ if (!previous)
943
+ return false;
944
+ if (previous.asset !== next.asset)
945
+ return false;
946
+ if (!isTextAsset(next.asset) && !isPrimitiveAsset(next.asset))
947
+ return true;
948
+ if (isTextAsset(next.asset))
949
+ return deepEqual(previous.text, next.text);
950
+ return deepEqual(previous.primitive, next.primitive);
951
+ }
952
+ function deepEqual(a, b) {
953
+ if (a === b)
954
+ return true;
955
+ if (typeof a !== typeof b)
956
+ return false;
957
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object")
958
+ return false;
959
+ if (Array.isArray(a) || Array.isArray(b)) {
960
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
961
+ return false;
962
+ return a.every((item, index) => deepEqual(item, b[index]));
963
+ }
964
+ const aKeys = Object.keys(a);
965
+ const bKeys = Object.keys(b);
966
+ if (aKeys.length !== bKeys.length)
967
+ return false;
968
+ return aKeys.every((key) => deepEqual(a[key], b[key]));
969
+ }
882
970
  function createConnectorInstance(def, layout) {
883
971
  const node = document.createElementNS(NS, "g");
884
972
  const shaft = document.createElementNS(NS, "path");
@@ -894,55 +982,145 @@ function createConnectorInstance(def, layout) {
894
982
  return state;
895
983
  }
896
984
  function applyConnectorState(state, def, layout) {
897
- applyConnectorGroupAttrs(state.node, def);
898
- clearChildren(state.node);
899
- const d = routePath(def.route, layout);
900
- if (shouldRenderOutline(def)) {
901
- const outline = document.createElementNS(NS, "path");
902
- outline.classList.add("iso-connector-outline");
903
- applyConnectorPathAttrs(outline, def, d, {
985
+ const previous = state.previous;
986
+ const routeChanged = !previous || !routesEqual(previous.route, def.route);
987
+ const styleChanged = !previous || !connectorStylesEqual(previous.style, def.style);
988
+ const shapeChanged = !previous ||
989
+ shouldRenderOutline(previous) !== shouldRenderOutline(def) ||
990
+ previous.style.variant !== def.style.variant ||
991
+ previous.style.lane !== def.style.lane ||
992
+ previous.start !== def.start ||
993
+ previous.end !== def.end ||
994
+ previous.route.length !== def.route.length;
995
+ applyConnectorGroupAttrs(state.node, def, previous);
996
+ const geometryChanged = routeChanged || shapeChanged || styleChanged;
997
+ const d = geometryChanged ? routePath(def.route, layout) : (state.shaft.getAttribute("d") ?? "");
998
+ if (shapeChanged) {
999
+ if (state.outline)
1000
+ state.node.removeChild(state.outline);
1001
+ state.outline = undefined;
1002
+ if (shouldRenderOutline(def)) {
1003
+ const outline = document.createElementNS(NS, "path");
1004
+ outline.classList.add("iso-connector-outline");
1005
+ applyConnectorPathAttrs(outline, def, d, {
1006
+ stroke: def.style.outline ?? def.style.stroke,
1007
+ strokeWidth: def.style.strokeWidth + def.style.outlineWidth * 2,
1008
+ includeDash: false,
1009
+ });
1010
+ state.node.insertBefore(outline, state.shaft);
1011
+ state.outline = outline;
1012
+ }
1013
+ }
1014
+ else if (state.outline && geometryChanged) {
1015
+ applyConnectorPathAttrs(state.outline, def, d, {
904
1016
  stroke: def.style.outline ?? def.style.stroke,
905
1017
  strokeWidth: def.style.strokeWidth + def.style.outlineWidth * 2,
906
1018
  includeDash: false,
907
1019
  });
908
- state.node.appendChild(outline);
909
1020
  }
910
- state.shaft = document.createElementNS(NS, "path");
1021
+ if (!state.shaft.parentNode)
1022
+ state.node.appendChild(state.shaft);
911
1023
  state.shaft.classList.add("iso-connector-shaft");
912
- applyConnectorPathAttrs(state.shaft, def, d, {
913
- stroke: def.style.stroke,
914
- strokeWidth: def.style.strokeWidth,
915
- includeDash: true,
916
- });
917
- state.node.appendChild(state.shaft);
918
- if (def.style.variant === "road" && def.style.lane === "center-dashed") {
919
- const lane = document.createElementNS(NS, "path");
920
- lane.classList.add("iso-connector-lane");
921
- applyConnectorPathAttrs(lane, def, d, {
1024
+ if (geometryChanged) {
1025
+ applyConnectorPathAttrs(state.shaft, def, d, {
1026
+ stroke: def.style.stroke,
1027
+ strokeWidth: def.style.strokeWidth,
1028
+ includeDash: true,
1029
+ });
1030
+ }
1031
+ if (shapeChanged) {
1032
+ if (state.lane)
1033
+ state.node.removeChild(state.lane);
1034
+ state.lane = undefined;
1035
+ if (def.style.variant === "road" && def.style.lane === "center-dashed") {
1036
+ const lane = document.createElementNS(NS, "path");
1037
+ lane.classList.add("iso-connector-lane");
1038
+ applyConnectorPathAttrs(lane, def, d, {
1039
+ stroke: "#ffffff",
1040
+ strokeWidth: Math.max(1, def.style.strokeWidth * 0.12),
1041
+ includeDash: false,
1042
+ });
1043
+ lane.setAttribute("stroke-dasharray", "8 8");
1044
+ state.node.appendChild(lane);
1045
+ state.lane = lane;
1046
+ }
1047
+ }
1048
+ else if (state.lane && geometryChanged) {
1049
+ applyConnectorPathAttrs(state.lane, def, d, {
922
1050
  stroke: "#ffffff",
923
1051
  strokeWidth: Math.max(1, def.style.strokeWidth * 0.12),
924
1052
  includeDash: false,
925
1053
  });
926
- lane.setAttribute("stroke-dasharray", "8 8");
927
- state.node.appendChild(lane);
1054
+ state.lane.setAttribute("stroke-dasharray", "8 8");
1055
+ }
1056
+ if (geometryChanged) {
1057
+ if (state.startEndpoint)
1058
+ state.node.removeChild(state.startEndpoint);
1059
+ if (state.endEndpoint)
1060
+ state.node.removeChild(state.endEndpoint);
1061
+ state.startEndpoint = appendEndpoint(state.node, def, "start", layout);
1062
+ state.endEndpoint = appendEndpoint(state.node, def, "end", layout);
928
1063
  }
929
- appendEndpoint(state.node, def, "start", layout);
930
- appendEndpoint(state.node, def, "end", layout);
931
1064
  applyConnectorAmbientClasses(state, def.ambient ?? []);
1065
+ state.previous = def;
932
1066
  }
933
1067
  function clearChildren(node) {
934
1068
  while (node.firstChild)
935
1069
  node.removeChild(node.firstChild);
936
1070
  }
937
- function applyConnectorGroupAttrs(node, def) {
938
- node.setAttribute("class", [
1071
+ function routesEqual(a, b) {
1072
+ if (a === b)
1073
+ return true;
1074
+ if (a.length !== b.length)
1075
+ return false;
1076
+ return a.every((point, index) => {
1077
+ const other = b[index];
1078
+ return other !== undefined && point[0] === other[0] && point[1] === other[1];
1079
+ });
1080
+ }
1081
+ function connectorStylesEqual(a, b) {
1082
+ if (a === b)
1083
+ return true;
1084
+ return (a.variant === b.variant &&
1085
+ a.pattern === b.pattern &&
1086
+ a.stroke === b.stroke &&
1087
+ a.strokeWidth === b.strokeWidth &&
1088
+ a.opacity === b.opacity &&
1089
+ a.outline === b.outline &&
1090
+ a.outlineWidth === b.outlineWidth &&
1091
+ a.lane === b.lane &&
1092
+ (a.dash ?? []).join(",") === (b.dash ?? []).join(","));
1093
+ }
1094
+ function applyConnectorGroupAttrs(node, def, previous) {
1095
+ const nextClasses = [
939
1096
  "iso-connector",
940
1097
  `iso-connector-${def.id}`,
941
1098
  `iso-connector-variant-${def.style.variant}`,
942
1099
  `iso-connector-pattern-${def.style.pattern}`,
943
1100
  `iso-connector-direction-${def.direction}`,
944
1101
  `iso-layer-${def.layer}`,
945
- ].join(" "));
1102
+ ];
1103
+ if (!previous) {
1104
+ node.classList.add(...nextClasses);
1105
+ }
1106
+ else {
1107
+ const previousClasses = [
1108
+ "iso-connector",
1109
+ `iso-connector-${previous.id}`,
1110
+ `iso-connector-variant-${previous.style.variant}`,
1111
+ `iso-connector-pattern-${previous.style.pattern}`,
1112
+ `iso-connector-direction-${previous.direction}`,
1113
+ `iso-layer-${previous.layer}`,
1114
+ ];
1115
+ for (const name of previousClasses) {
1116
+ if (!nextClasses.includes(name))
1117
+ node.classList.remove(name);
1118
+ }
1119
+ for (const name of nextClasses) {
1120
+ if (!previousClasses.includes(name))
1121
+ node.classList.add(name);
1122
+ }
1123
+ }
946
1124
  node.setAttribute("data-id", def.id);
947
1125
  node.setAttribute("data-layer", def.layer);
948
1126
  node.style.overflow = "visible";
@@ -975,10 +1153,11 @@ function routePath(route, layout) {
975
1153
  function appendEndpoint(group, def, kind, layout) {
976
1154
  const endpoint = def[kind];
977
1155
  if (endpoint === "none" || def.route.length < 2)
978
- return;
1156
+ return undefined;
979
1157
  const node = createEndpointNode(endpoint, def, kind, layout);
980
1158
  node.classList.add(`iso-connector-${kind}`);
981
1159
  group.appendChild(node);
1160
+ return node;
982
1161
  }
983
1162
  function createEndpointNode(endpoint, def, kind, layout) {
984
1163
  switch (endpoint) {
@@ -1120,5 +1299,5 @@ function animateElement(node, keyframeName, type = "enter") {
1120
1299
  node.style.animation = `${keyframeName} ${duration} ${easing} forwards`;
1121
1300
  }
1122
1301
 
1123
- export { AssetRegistryImpl as A, DEFAULT_CELL_SIZE as D, animateElement as a, applySceneViewBox as b, applyThemeToElement as c, buildSceneDOM as d, calculateTransform as e, calculateVisualSize as f, composeTheme as g, createAssetRegistry as h, createDefaultRegistry as i, getConnectorState as j, getCurrentElementBounds as k, getElementState as l, getGridAreaBounds as m, getResolvedProjectionLayout as n, getResolvedViewBox as o, hideElementAfterExit as p, projectToRaw as q, projectToScreen as r, resolveTheme as s, updateElementTransforms as t, unhideElementOnReadd as u };
1124
- //# sourceMappingURL=rendering-engine-BUUV3Qq-.js.map
1302
+ export { AssetRegistryImpl as A, DEFAULT_CELL_SIZE as D, getResolvedViewBox as a, getCurrentElementBounds as b, getElementState as c, getConnectorState as d, updateElementTransforms as e, projectToScreen as f, getResolvedProjectionLayout as g, hideElementAfterExit as h, applyThemeToElement as i, buildSceneDOM as j, calculateTransform as k, calculateVisualSize as l, composeTheme as m, createAssetRegistry as n, createDefaultRegistry as o, projectToRaw as p, getGridAreaBounds as q, resolveTheme as r, applySceneViewBox as s, animateElement as t, unhideElementOnReadd as u };
1303
+ //# sourceMappingURL=rendering-engine-nkqYx0qc.js.map