@pixerate/editor 0.19.0 → 0.20.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 (112) hide show
  1. package/README.md +91 -2
  2. package/dist/agent/index.cjs +625 -2
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +286 -2
  5. package/dist/agent/index.d.ts +286 -2
  6. package/dist/agent/index.js +624 -1
  7. package/dist/agent/index.js.map +1 -1
  8. package/dist/canvas/index.cjs +1240 -22
  9. package/dist/canvas/index.cjs.map +1 -1
  10. package/dist/canvas/index.d.cts +515 -3
  11. package/dist/canvas/index.d.ts +515 -3
  12. package/dist/canvas/index.js +1219 -1
  13. package/dist/canvas/index.js.map +1 -1
  14. package/dist/chunk-3TYMNULT.js +1367 -0
  15. package/dist/chunk-3TYMNULT.js.map +1 -0
  16. package/dist/chunk-4QPLEUL2.cjs +4480 -0
  17. package/dist/chunk-4QPLEUL2.cjs.map +1 -0
  18. package/dist/chunk-4SUNJZWB.cjs +553 -0
  19. package/dist/chunk-4SUNJZWB.cjs.map +1 -0
  20. package/dist/chunk-A7DKIA72.js +2657 -0
  21. package/dist/chunk-A7DKIA72.js.map +1 -0
  22. package/dist/chunk-AXV2ZW34.cjs +118 -0
  23. package/dist/chunk-AXV2ZW34.cjs.map +1 -0
  24. package/dist/chunk-B2ZRLN4S.js +118 -0
  25. package/dist/chunk-B2ZRLN4S.js.map +1 -0
  26. package/dist/chunk-CYFJCKC4.js +4480 -0
  27. package/dist/chunk-CYFJCKC4.js.map +1 -0
  28. package/dist/chunk-GVW4P7BG.js +2706 -0
  29. package/dist/chunk-GVW4P7BG.js.map +1 -0
  30. package/dist/chunk-JACER6TE.cjs +1 -0
  31. package/dist/chunk-JACER6TE.cjs.map +1 -0
  32. package/dist/chunk-JYRIOFEN.cjs +2657 -0
  33. package/dist/chunk-JYRIOFEN.cjs.map +1 -0
  34. package/dist/chunk-KYRZDWM4.js +299 -0
  35. package/dist/chunk-KYRZDWM4.js.map +1 -0
  36. package/dist/chunk-LA7PEGU3.cjs +299 -0
  37. package/dist/chunk-LA7PEGU3.cjs.map +1 -0
  38. package/dist/chunk-N7FG6CBO.js +1 -0
  39. package/dist/chunk-N7FG6CBO.js.map +1 -0
  40. package/dist/chunk-OYXSKQFR.js +553 -0
  41. package/dist/chunk-OYXSKQFR.js.map +1 -0
  42. package/dist/chunk-TVRC4KVT.cjs +1367 -0
  43. package/dist/chunk-TVRC4KVT.cjs.map +1 -0
  44. package/dist/chunk-U6NDQFOK.cjs +2706 -0
  45. package/dist/chunk-U6NDQFOK.cjs.map +1 -0
  46. package/dist/controller-OUdxPY-X.d.cts +754 -0
  47. package/dist/controller-OUdxPY-X.d.ts +754 -0
  48. package/dist/extensions/index.cjs +144 -3
  49. package/dist/extensions/index.cjs.map +1 -1
  50. package/dist/extensions/index.d.cts +9 -144
  51. package/dist/extensions/index.d.ts +9 -144
  52. package/dist/extensions/index.js +145 -4
  53. package/dist/grammar/index.cjs +31 -3
  54. package/dist/grammar/index.cjs.map +1 -1
  55. package/dist/grammar/index.d.cts +239 -19
  56. package/dist/grammar/index.d.ts +239 -19
  57. package/dist/grammar/index.js +31 -3
  58. package/dist/history/index.d.cts +2 -41
  59. package/dist/history/index.d.ts +2 -41
  60. package/dist/index-Ng1pG7JE.d.ts +899 -0
  61. package/dist/index-k3mFbPx_.d.cts +899 -0
  62. package/dist/index.cjs +457 -20
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +99 -12
  65. package/dist/index.d.ts +99 -12
  66. package/dist/index.js +457 -20
  67. package/dist/index.js.map +1 -1
  68. package/dist/registry-iAHbqkYt.d.cts +128 -0
  69. package/dist/registry-iAHbqkYt.d.ts +128 -0
  70. package/dist/serializers/index.cjs +26 -3
  71. package/dist/serializers/index.cjs.map +1 -1
  72. package/dist/serializers/index.d.cts +119 -6
  73. package/dist/serializers/index.d.ts +119 -6
  74. package/dist/serializers/index.js +24 -1
  75. package/dist/spreadsheet/index.cjs +122 -2
  76. package/dist/spreadsheet/index.cjs.map +1 -1
  77. package/dist/spreadsheet/index.d.cts +371 -313
  78. package/dist/spreadsheet/index.d.ts +371 -313
  79. package/dist/spreadsheet/index.js +127 -7
  80. package/dist/tokens-CyLZmIei.d.cts +76 -0
  81. package/dist/tokens-CyLZmIei.d.ts +76 -0
  82. package/dist/track-changes-G8JcWB8b.d.cts +306 -0
  83. package/dist/track-changes-G8JcWB8b.d.ts +306 -0
  84. package/dist/types-BZgcDsC5.d.cts +42 -0
  85. package/dist/types-BZgcDsC5.d.ts +42 -0
  86. package/dist/{types-JV3S8VeR.d.cts → types-DMe0vFB1.d.cts} +1 -1
  87. package/dist/{types-JV3S8VeR.d.ts → types-DMe0vFB1.d.ts} +1 -1
  88. package/package.json +1 -1
  89. package/dist/chunk-7FWEWE6T.js +0 -1888
  90. package/dist/chunk-7FWEWE6T.js.map +0 -1
  91. package/dist/chunk-AIZUKWTL.js +0 -1715
  92. package/dist/chunk-AIZUKWTL.js.map +0 -1
  93. package/dist/chunk-GLSIZKOX.js +0 -17
  94. package/dist/chunk-GLSIZKOX.js.map +0 -1
  95. package/dist/chunk-LDES3EOS.cjs +0 -1888
  96. package/dist/chunk-LDES3EOS.cjs.map +0 -1
  97. package/dist/chunk-LLUAKTLW.cjs +0 -532
  98. package/dist/chunk-LLUAKTLW.cjs.map +0 -1
  99. package/dist/chunk-MPPXH7U5.cjs +0 -336
  100. package/dist/chunk-MPPXH7U5.cjs.map +0 -1
  101. package/dist/chunk-O7LOPQMH.js +0 -336
  102. package/dist/chunk-O7LOPQMH.js.map +0 -1
  103. package/dist/chunk-QGVMBCT5.js +0 -532
  104. package/dist/chunk-QGVMBCT5.js.map +0 -1
  105. package/dist/chunk-SEDR75VH.cjs +0 -17
  106. package/dist/chunk-SEDR75VH.cjs.map +0 -1
  107. package/dist/chunk-YQCI4CCN.cjs +0 -1715
  108. package/dist/chunk-YQCI4CCN.cjs.map +0 -1
  109. package/dist/mention-Dcevh3ff.d.cts +0 -32
  110. package/dist/mention-Dcevh3ff.d.ts +0 -32
  111. package/dist/tokens-7kRTEtPb.d.cts +0 -39
  112. package/dist/tokens-7kRTEtPb.d.ts +0 -39
@@ -719,12 +719,1198 @@ var StreamingLayoutManager = class {
719
719
  }
720
720
  }
721
721
  };
722
+
723
+ // src/canvas/arrange.ts
724
+ var LOCKED_DATA_KEY = "locked";
725
+ var LOCKED_PREVIOUS_FLAGS_KEY = "lockedPrevious";
726
+ function parsePx(value) {
727
+ if (typeof value === "number" && Number.isFinite(value)) return value;
728
+ if (typeof value === "string") {
729
+ const match = /^\s*(-?\d+(?:\.\d+)?)(px)?\s*$/i.exec(value);
730
+ if (match) return Number(match[1]);
731
+ }
732
+ return void 0;
733
+ }
734
+ function readStyleSize(style) {
735
+ if (!style) return {};
736
+ if (typeof style === "object") {
737
+ return { width: parsePx(style.width), height: parsePx(style.height) };
738
+ }
739
+ const read = (prop) => {
740
+ const match = new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]+)`, "i").exec(style);
741
+ return match ? parsePx(match[1]) : void 0;
742
+ };
743
+ return { width: read("width"), height: read("height") };
744
+ }
745
+ function withStyleSize(style, size) {
746
+ if (!style) return style ?? void 0;
747
+ if (typeof style === "object") {
748
+ const next2 = { ...style };
749
+ if (size.width !== void 0 && "width" in next2) next2.width = size.width;
750
+ if (size.height !== void 0 && "height" in next2) next2.height = size.height;
751
+ return next2;
752
+ }
753
+ let next = style;
754
+ for (const prop of ["width", "height"]) {
755
+ const value = size[prop];
756
+ if (value === void 0) continue;
757
+ next = next.replace(
758
+ new RegExp(`((?:^|;)\\s*${prop}\\s*:\\s*)[^;]+`, "i"),
759
+ `$1${value}px`
760
+ );
761
+ }
762
+ return next;
763
+ }
764
+ function getNodeSize(node, options = {}) {
765
+ const styleSize = readStyleSize(node.style);
766
+ let width = node.measured?.width ?? node.width ?? styleSize.width;
767
+ let height = node.measured?.height ?? node.height ?? styleSize.height;
768
+ if (width === void 0 || height === void 0) {
769
+ const fallback = options.estimate === false ? { width: options.defaultWidth ?? 200, height: options.defaultHeight ?? 60 } : estimateNodeSize(node, options.defaultWidth, options.defaultHeight);
770
+ width ??= fallback.width;
771
+ height ??= fallback.height;
772
+ }
773
+ return { width, height };
774
+ }
775
+ function getAbsolutePosition(node, lookup) {
776
+ let x = node.position.x;
777
+ let y = node.position.y;
778
+ const seen = /* @__PURE__ */ new Set([node.id]);
779
+ let parentId = node.parentId;
780
+ while (parentId && !seen.has(parentId)) {
781
+ const parent = lookup.get(parentId);
782
+ if (!parent) break;
783
+ seen.add(parentId);
784
+ x += parent.position.x;
785
+ y += parent.position.y;
786
+ parentId = parent.parentId;
787
+ }
788
+ return { x, y };
789
+ }
790
+ function createNodeLookup(nodes) {
791
+ return new Map(nodes.map((n) => [n.id, n]));
792
+ }
793
+ function getNodeRect(node, lookup, options) {
794
+ const { x, y } = node.parentId && lookup ? getAbsolutePosition(node, lookup) : node.position;
795
+ return { x, y, ...getNodeSize(node, options) };
796
+ }
797
+ function isLocked(node) {
798
+ return node?.data?.[LOCKED_DATA_KEY] === true;
799
+ }
800
+ function lockNodes(nodes, ids) {
801
+ const idSet = new Set(ids);
802
+ return nodes.map((node) => {
803
+ if (!idSet.has(node.id) || isLocked(node)) return node;
804
+ const previous = {};
805
+ if (node.draggable !== void 0) previous.draggable = node.draggable;
806
+ if (node.deletable !== void 0) previous.deletable = node.deletable;
807
+ return {
808
+ ...node,
809
+ draggable: false,
810
+ deletable: false,
811
+ data: { ...node.data, [LOCKED_DATA_KEY]: true, [LOCKED_PREVIOUS_FLAGS_KEY]: previous }
812
+ };
813
+ });
814
+ }
815
+ function unlockNodes(nodes, ids) {
816
+ const idSet = new Set(ids);
817
+ return nodes.map((node) => {
818
+ if (!idSet.has(node.id) || !isLocked(node)) return node;
819
+ const {
820
+ [LOCKED_DATA_KEY]: _locked,
821
+ [LOCKED_PREVIOUS_FLAGS_KEY]: previous = {},
822
+ ...data
823
+ } = node.data;
824
+ const next = { ...node, data };
825
+ delete next.draggable;
826
+ delete next.deletable;
827
+ if (previous.draggable !== void 0) next.draggable = previous.draggable;
828
+ if (previous.deletable !== void 0) next.deletable = previous.deletable;
829
+ return next;
830
+ });
831
+ }
832
+ function toggleLockNodes(nodes, ids) {
833
+ const idSet = new Set(ids);
834
+ const anyUnlocked = nodes.some((n) => idSet.has(n.id) && !isLocked(n));
835
+ return anyUnlocked ? lockNodes(nodes, idSet) : unlockNodes(nodes, idSet);
836
+ }
837
+ function collect(nodes, ids, options) {
838
+ const lookup = createNodeLookup(nodes);
839
+ const items = [];
840
+ const seen = /* @__PURE__ */ new Set();
841
+ for (const id of ids) {
842
+ const node = lookup.get(id);
843
+ if (!node || seen.has(id)) continue;
844
+ seen.add(id);
845
+ items.push({ node, rect: getNodeRect(node, lookup, options), locked: isLocked(node) });
846
+ }
847
+ return { lookup, items };
848
+ }
849
+ function toPositionUpdates(lookup, targets) {
850
+ const updates = [];
851
+ for (const [id, abs] of targets) {
852
+ const node = lookup.get(id);
853
+ let position = abs;
854
+ if (node.parentId && lookup.has(node.parentId)) {
855
+ const parent = lookup.get(node.parentId);
856
+ const parentAbs = targets.get(parent.id) ?? getAbsolutePosition(parent, lookup);
857
+ position = { x: abs.x - parentAbs.x, y: abs.y - parentAbs.y };
858
+ }
859
+ if (position.x !== node.position.x || position.y !== node.position.y) {
860
+ updates.push({ id, position });
861
+ }
862
+ }
863
+ return updates;
864
+ }
865
+ function alignNodes(nodes, ids, edge, options = {}) {
866
+ const { lookup, items } = collect(nodes, ids, options);
867
+ const fixed = typeof options.to === "object" ? options.to : null;
868
+ if (items.length === 0 || !fixed && items.length < 2) return [];
869
+ const bounds = fixed ?? boundsOf(items.map((i) => i.rect));
870
+ const targets = /* @__PURE__ */ new Map();
871
+ for (const { node, rect, locked } of items) {
872
+ if (locked) continue;
873
+ let { x, y } = rect;
874
+ switch (edge) {
875
+ case "left":
876
+ x = bounds.x;
877
+ break;
878
+ case "center":
879
+ x = bounds.x + bounds.width / 2 - rect.width / 2;
880
+ break;
881
+ case "right":
882
+ x = bounds.x + bounds.width - rect.width;
883
+ break;
884
+ case "top":
885
+ y = bounds.y;
886
+ break;
887
+ case "middle":
888
+ y = bounds.y + bounds.height / 2 - rect.height / 2;
889
+ break;
890
+ case "bottom":
891
+ y = bounds.y + bounds.height - rect.height;
892
+ break;
893
+ }
894
+ targets.set(node.id, { x, y });
895
+ }
896
+ return toPositionUpdates(lookup, targets);
897
+ }
898
+ function distributeAxis(items, horizontal, mode, gapOverride, targets) {
899
+ const start = (r) => horizontal ? r.x : r.y;
900
+ const size = (r) => horizontal ? r.width : r.height;
901
+ const sorted = [...items].sort(
902
+ (a, b) => start(a.rect) + size(a.rect) / 2 - (start(b.rect) + size(b.rect) / 2)
903
+ );
904
+ const first = sorted[0].rect;
905
+ const last = sorted[sorted.length - 1].rect;
906
+ const n = sorted.length;
907
+ const set = (item, value) => {
908
+ if (item.locked) return;
909
+ const current = targets.get(item.node.id) ?? { x: item.rect.x, y: item.rect.y };
910
+ targets.set(item.node.id, horizontal ? { ...current, x: value } : { ...current, y: value });
911
+ };
912
+ if (mode === "centres") {
913
+ const c0 = start(first) + size(first) / 2;
914
+ const c1 = start(last) + size(last) / 2;
915
+ const step = (c1 - c0) / (n - 1);
916
+ sorted.forEach((item, i) => set(item, c0 + step * i - size(item.rect) / 2));
917
+ return;
918
+ }
919
+ const span = start(last) + size(last) - start(first);
920
+ const total = sorted.reduce((sum, item) => sum + size(item.rect), 0);
921
+ const gap = gapOverride ?? (span - total) / (n - 1);
922
+ let cursor = start(first);
923
+ for (const item of sorted) {
924
+ set(item, cursor);
925
+ cursor += size(item.rect) + gap;
926
+ }
927
+ }
928
+ function distributeNodes(nodes, ids, axis, mode = "gaps", options = {}) {
929
+ const { lookup, items } = collect(nodes, ids, options);
930
+ const minimum = options.gap !== void 0 && mode === "gaps" ? 2 : 3;
931
+ if (items.length < minimum) return [];
932
+ const targets = /* @__PURE__ */ new Map();
933
+ if (axis !== "vertical") distributeAxis(items, true, mode, options.gap, targets);
934
+ if (axis !== "horizontal") distributeAxis(items, false, mode, options.gap, targets);
935
+ return toPositionUpdates(lookup, targets);
936
+ }
937
+ function matchSize(nodes, ids, dimension, options = {}) {
938
+ const { items } = collect(nodes, ids, options);
939
+ if (items.length < 2) return [];
940
+ const reference = options.reference ?? "largest";
941
+ const pick = (dim) => {
942
+ if (typeof reference === "object") {
943
+ const item = items.find((i) => i.node.id === reference.id);
944
+ if (item) return item.rect[dim];
945
+ }
946
+ if (reference === "first") return items[0].rect[dim];
947
+ const values = items.map((i) => i.rect[dim]);
948
+ return reference === "smallest" ? Math.min(...values) : Math.max(...values);
949
+ };
950
+ const width = dimension !== "height" ? pick("width") : void 0;
951
+ const height = dimension !== "width" ? pick("height") : void 0;
952
+ const updates = [];
953
+ for (const { node, rect, locked } of items) {
954
+ if (locked) continue;
955
+ const update = { id: node.id };
956
+ if (width !== void 0 && width !== rect.width) update.width = width;
957
+ if (height !== void 0 && height !== rect.height) update.height = height;
958
+ if (update.width === void 0 && update.height === void 0) continue;
959
+ if (options.keepCentre) {
960
+ update.position = {
961
+ x: node.position.x - ((update.width ?? rect.width) - rect.width) / 2,
962
+ y: node.position.y - ((update.height ?? rect.height) - rect.height) / 2
963
+ };
964
+ }
965
+ updates.push(update);
966
+ }
967
+ return updates;
968
+ }
969
+ function reorderNodes(nodes, ids, operation, options = {}) {
970
+ const lookup = createNodeLookup(nodes);
971
+ const targetIds = new Set(
972
+ [...ids].filter((id) => lookup.has(id) && !isLocked(lookup.get(id)))
973
+ );
974
+ if (targetIds.size === 0) return [];
975
+ const groups = /* @__PURE__ */ new Map();
976
+ for (const node of nodes) {
977
+ const key = node.parentId ?? "";
978
+ if (!groups.has(key)) groups.set(key, []);
979
+ groups.get(key).push(node);
980
+ }
981
+ const updates = [];
982
+ for (const siblings of groups.values()) {
983
+ if (!siblings.some((n) => targetIds.has(n.id))) continue;
984
+ const order = siblings.map((node, index) => ({ node, index, z: typeof node.zIndex === "number" ? node.zIndex : 0 })).sort((a, b) => a.z - b.z || a.index - b.index).map((entry) => entry.node);
985
+ const isTarget = (n) => targetIds.has(n.id);
986
+ let next;
987
+ if (operation === "bringToFront") {
988
+ next = [...order.filter((n) => !isTarget(n)), ...order.filter(isTarget)];
989
+ } else if (operation === "sendToBack") {
990
+ next = [...order.filter(isTarget), ...order.filter((n) => !isTarget(n))];
991
+ } else {
992
+ next = [...order];
993
+ const forward = operation === "bringForward";
994
+ const rects = new Map(next.map((n) => [n.id, getNodeRect(n, lookup, options)]));
995
+ const intersects = (a, b) => a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
996
+ const indices = next.map((_, i) => i);
997
+ if (forward) indices.reverse();
998
+ for (const i of indices) {
999
+ const node = next[i];
1000
+ if (!isTarget(node)) continue;
1001
+ const step = forward ? 1 : -1;
1002
+ let j = i + step;
1003
+ let swapWith = -1;
1004
+ let firstNonTarget = -1;
1005
+ while (j >= 0 && j < next.length) {
1006
+ if (!isTarget(next[j])) {
1007
+ if (firstNonTarget === -1) firstNonTarget = j;
1008
+ if (intersects(rects.get(node.id), rects.get(next[j].id))) {
1009
+ swapWith = j;
1010
+ break;
1011
+ }
1012
+ }
1013
+ j += step;
1014
+ }
1015
+ if (swapWith === -1) swapWith = firstNonTarget;
1016
+ if (swapWith === -1) continue;
1017
+ next.splice(i, 1);
1018
+ next.splice(swapWith, 0, node);
1019
+ }
1020
+ }
1021
+ if (next.every((node, i) => node === order[i])) continue;
1022
+ next.forEach((node, rank) => {
1023
+ const current = typeof node.zIndex === "number" ? node.zIndex : 0;
1024
+ if (current !== rank) updates.push({ id: node.id, zIndex: rank });
1025
+ });
1026
+ }
1027
+ return updates;
1028
+ }
1029
+ function applyNodeUpdates(nodes, updates) {
1030
+ if (updates.length === 0) return nodes;
1031
+ const byId = /* @__PURE__ */ new Map();
1032
+ for (const update of updates) {
1033
+ byId.set(update.id, { ...byId.get(update.id), ...update });
1034
+ }
1035
+ return nodes.map((node) => {
1036
+ const update = byId.get(node.id);
1037
+ if (!update) return node;
1038
+ const next = { ...node };
1039
+ if (update.position) next.position = { ...update.position };
1040
+ if (update.zIndex !== void 0) next.zIndex = update.zIndex;
1041
+ if (update.width !== void 0 || update.height !== void 0) {
1042
+ if (update.width !== void 0) next.width = update.width;
1043
+ if (update.height !== void 0) next.height = update.height;
1044
+ if (node.style) next.style = withStyleSize(node.style, update);
1045
+ if (node.measured) {
1046
+ next.measured = {
1047
+ ...node.measured,
1048
+ ...update.width !== void 0 ? { width: update.width } : {},
1049
+ ...update.height !== void 0 ? { height: update.height } : {}
1050
+ };
1051
+ }
1052
+ }
1053
+ return next;
1054
+ });
1055
+ }
1056
+ function invertNodeUpdates(nodes, updates) {
1057
+ const lookup = createNodeLookup(nodes);
1058
+ const inverse = [];
1059
+ for (const update of updates) {
1060
+ const node = lookup.get(update.id);
1061
+ if (!node) continue;
1062
+ const back = { id: node.id };
1063
+ if (update.position) back.position = { ...node.position };
1064
+ if (update.zIndex !== void 0) back.zIndex = typeof node.zIndex === "number" ? node.zIndex : 0;
1065
+ if (update.width !== void 0 || update.height !== void 0) {
1066
+ const size = getNodeSize(node);
1067
+ if (update.width !== void 0) back.width = size.width;
1068
+ if (update.height !== void 0) back.height = size.height;
1069
+ }
1070
+ inverse.push(back);
1071
+ }
1072
+ return inverse;
1073
+ }
1074
+
1075
+ // src/canvas/guides.ts
1076
+ function buildAxis(entries) {
1077
+ entries.sort((a, b) => a.value - b.value);
1078
+ return {
1079
+ values: Float64Array.from(entries.map((e) => e.value)),
1080
+ rects: entries.map((e) => e.rect)
1081
+ };
1082
+ }
1083
+ function lowerBound(values, v) {
1084
+ let lo = 0;
1085
+ let hi = values.length;
1086
+ while (lo < hi) {
1087
+ const mid = lo + hi >>> 1;
1088
+ if (values[mid] < v) lo = mid + 1;
1089
+ else hi = mid;
1090
+ }
1091
+ return lo;
1092
+ }
1093
+ function createGuideIndex(rects) {
1094
+ const edgeX = [];
1095
+ const centreX = [];
1096
+ const edgeY = [];
1097
+ const centreY = [];
1098
+ for (const rect of rects) {
1099
+ edgeX.push({ value: rect.x, rect }, { value: rect.x + rect.width, rect });
1100
+ centreX.push({ value: rect.x + rect.width / 2, rect });
1101
+ edgeY.push({ value: rect.y, rect }, { value: rect.y + rect.height, rect });
1102
+ centreY.push({ value: rect.y + rect.height / 2, rect });
1103
+ }
1104
+ return {
1105
+ rects,
1106
+ edgeX: buildAxis(edgeX),
1107
+ centreX: buildAxis(centreX),
1108
+ edgeY: buildAxis(edgeY),
1109
+ centreY: buildAxis(centreY)
1110
+ };
1111
+ }
1112
+ function nearest(axis, v, tolerance) {
1113
+ const { values } = axis;
1114
+ let best = null;
1115
+ for (let i = lowerBound(values, v - tolerance); i < values.length && values[i] <= v + tolerance; i++) {
1116
+ const delta = values[i] - v;
1117
+ if (best === null || Math.abs(delta) < Math.abs(best)) best = delta;
1118
+ }
1119
+ return best;
1120
+ }
1121
+ function matches(axis, v, eps) {
1122
+ const { values, rects } = axis;
1123
+ const out = [];
1124
+ for (let i = lowerBound(values, v - eps); i < values.length && values[i] <= v + eps; i++) {
1125
+ out.push(rects[i]);
1126
+ }
1127
+ return out;
1128
+ }
1129
+ function spacingCandidate(rects, moving, horizontal, tolerance) {
1130
+ const start = (r) => horizontal ? r.x : r.y;
1131
+ const end = (r) => horizontal ? r.x + r.width : r.y + r.height;
1132
+ const crossStart = (r) => horizontal ? r.y : r.x;
1133
+ const crossEnd = (r) => horizontal ? r.y + r.height : r.x + r.width;
1134
+ const size = end(moving) - start(moving);
1135
+ const inLane = (r, lane2) => crossStart(r) < crossEnd(lane2) && crossEnd(r) > crossStart(lane2);
1136
+ const lane = rects.filter((r) => inLane(r, moving));
1137
+ if (lane.length === 0) return null;
1138
+ let before = null;
1139
+ let after = null;
1140
+ for (const r of lane) {
1141
+ if (end(r) <= start(moving) + tolerance && (!before || end(r) > end(before))) before = r;
1142
+ if (start(r) >= end(moving) - tolerance && (!after || start(r) < start(after))) after = r;
1143
+ }
1144
+ const neighbourOf = (r, forward) => {
1145
+ let best2 = null;
1146
+ for (const other of lane) {
1147
+ if (other === r || !inLane(other, r)) continue;
1148
+ if (forward ? start(other) >= end(r) : end(other) <= start(r)) {
1149
+ if (!best2 || (forward ? start(other) < start(best2) : end(other) > end(best2))) best2 = other;
1150
+ }
1151
+ }
1152
+ return best2;
1153
+ };
1154
+ const crossMid = (a, b) => (Math.max(crossStart(a), crossStart(b)) + Math.min(crossEnd(a), crossEnd(b))) / 2;
1155
+ const segment = (from, to, cross) => horizontal ? { orientation: "horizontal", kind: "spacing", x1: from, y1: cross, x2: to, y2: cross } : { orientation: "vertical", kind: "spacing", x1: cross, y1: from, x2: cross, y2: to };
1156
+ const candidates = [];
1157
+ if (before && after) {
1158
+ const b = before;
1159
+ const a = after;
1160
+ const pos = (end(b) + start(a) - size) / 2;
1161
+ if (pos >= end(b)) {
1162
+ candidates.push({
1163
+ pos,
1164
+ guides: (p) => [
1165
+ segment(end(b), p, crossMid(b, moving)),
1166
+ segment(p + size, start(a), crossMid(a, moving))
1167
+ ]
1168
+ });
1169
+ }
1170
+ }
1171
+ if (before) {
1172
+ const b = before;
1173
+ const prev = neighbourOf(b, false);
1174
+ if (prev) {
1175
+ const gap = start(b) - end(prev);
1176
+ candidates.push({
1177
+ pos: end(b) + gap,
1178
+ guides: (p) => [segment(end(prev), start(b), crossMid(prev, b)), segment(end(b), p, crossMid(b, moving))]
1179
+ });
1180
+ }
1181
+ }
1182
+ if (after) {
1183
+ const a = after;
1184
+ const next = neighbourOf(a, true);
1185
+ if (next) {
1186
+ const gap = start(next) - end(a);
1187
+ candidates.push({
1188
+ pos: start(a) - gap - size,
1189
+ guides: (p) => [segment(p + size, start(a), crossMid(a, moving)), segment(end(a), start(next), crossMid(a, next))]
1190
+ });
1191
+ }
1192
+ }
1193
+ let best = null;
1194
+ for (const c of candidates) {
1195
+ const delta = c.pos - start(moving);
1196
+ if (Math.abs(delta) <= tolerance && (!best || Math.abs(delta) < Math.abs(best.delta))) {
1197
+ best = { delta, guides: c.guides(c.pos) };
1198
+ }
1199
+ }
1200
+ return best;
1201
+ }
1202
+ function computeSnapGuides(moving, targets, options = {}) {
1203
+ const index = Array.isArray(targets) ? createGuideIndex(targets) : targets;
1204
+ const zoom = options.zoom && options.zoom > 0 ? options.zoom : 1;
1205
+ const tolerance = (options.threshold ?? 6) / zoom;
1206
+ const useEdges = options.edges !== false;
1207
+ const useCentres = options.centres !== false;
1208
+ const useSpacing = options.spacing !== false;
1209
+ const eps = 0.5 / zoom;
1210
+ const snapAxis = (horizontal) => {
1211
+ const start = horizontal ? moving.x : moving.y;
1212
+ const size = horizontal ? moving.width : moving.height;
1213
+ const edgeAxis = horizontal ? index.edgeX : index.edgeY;
1214
+ const centreAxis = horizontal ? index.centreX : index.centreY;
1215
+ let delta = null;
1216
+ const consider = (d) => {
1217
+ if (d !== null && (delta === null || Math.abs(d) < Math.abs(delta))) delta = d;
1218
+ };
1219
+ if (useEdges) {
1220
+ consider(nearest(edgeAxis, start, tolerance));
1221
+ consider(nearest(edgeAxis, start + size, tolerance));
1222
+ }
1223
+ if (useCentres) consider(nearest(centreAxis, start + size / 2, tolerance));
1224
+ const spacing = useSpacing ? spacingCandidate(index.rects, moving, horizontal, tolerance) : null;
1225
+ if (spacing && (delta === null || Math.abs(spacing.delta) < Math.abs(delta) - eps)) {
1226
+ return { delta: spacing.delta, spacing };
1227
+ }
1228
+ return { delta, spacing: null };
1229
+ };
1230
+ const sx = snapAxis(true);
1231
+ const sy = snapAxis(false);
1232
+ const dx = sx.delta ?? 0;
1233
+ const dy = sy.delta ?? 0;
1234
+ const snapped = { x: moving.x + dx, y: moving.y + dy, width: moving.width, height: moving.height };
1235
+ const guides = [];
1236
+ const alignmentGuides = (horizontal) => {
1237
+ const start = horizontal ? snapped.x : snapped.y;
1238
+ const size = horizontal ? snapped.width : snapped.height;
1239
+ const lines = [];
1240
+ if (useEdges) {
1241
+ const axis = horizontal ? index.edgeX : index.edgeY;
1242
+ lines.push({ value: start, kind: "edge", axis }, { value: start + size, kind: "edge", axis });
1243
+ }
1244
+ if (useCentres) {
1245
+ lines.push({ value: start + size / 2, kind: "centre", axis: horizontal ? index.centreX : index.centreY });
1246
+ }
1247
+ for (const line of lines) {
1248
+ const aligned = matches(line.axis, line.value, eps);
1249
+ if (aligned.length === 0) continue;
1250
+ const span = boundsOf([snapped, ...aligned]);
1251
+ guides.push(
1252
+ horizontal ? { orientation: "vertical", kind: line.kind, x1: line.value, y1: span.y, x2: line.value, y2: span.y + span.height } : { orientation: "horizontal", kind: line.kind, x1: span.x, y1: line.value, x2: span.x + span.width, y2: line.value }
1253
+ );
1254
+ }
1255
+ };
1256
+ if (sx.delta !== null) {
1257
+ if (sx.spacing) guides.push(...shiftGuides(sx.spacing.guides, 0, dy));
1258
+ else alignmentGuides(true);
1259
+ }
1260
+ if (sy.delta !== null) {
1261
+ if (sy.spacing) guides.push(...shiftGuides(sy.spacing.guides, dx, 0));
1262
+ else alignmentGuides(false);
1263
+ }
1264
+ return {
1265
+ position: { x: snapped.x, y: snapped.y },
1266
+ offset: { x: dx, y: dy },
1267
+ guides,
1268
+ snappedX: sx.delta !== null,
1269
+ snappedY: sy.delta !== null
1270
+ };
1271
+ }
1272
+ function shiftGuides(guides, dx, dy) {
1273
+ return guides.map(
1274
+ (g) => g.orientation === "horizontal" ? { ...g, y1: g.y1 + dy, y2: g.y2 + dy } : { ...g, x1: g.x1 + dx, x2: g.x2 + dx }
1275
+ );
1276
+ }
1277
+ function collectDescendantIds(nodes, ids) {
1278
+ const result = new Set(ids);
1279
+ let grew = true;
1280
+ while (grew) {
1281
+ grew = false;
1282
+ for (const node of nodes) {
1283
+ if (node.parentId && result.has(node.parentId) && !result.has(node.id)) {
1284
+ result.add(node.id);
1285
+ grew = true;
1286
+ }
1287
+ }
1288
+ }
1289
+ return result;
1290
+ }
1291
+ function createNodeGuideIndex(nodes, draggingIds, options = {}) {
1292
+ const exclude = collectDescendantIds(nodes, draggingIds);
1293
+ const lookup = createNodeLookup(nodes);
1294
+ const rects = [];
1295
+ for (const node of nodes) {
1296
+ if (exclude.has(node.id) || node.hidden === true) continue;
1297
+ if (options.filter && !options.filter(node)) continue;
1298
+ rects.push(getNodeRect(node, lookup, options));
1299
+ }
1300
+ return createGuideIndex(rects);
1301
+ }
1302
+ function snapNodeDrag(nodes, draggingIds, options = {}) {
1303
+ const ids = new Set(draggingIds);
1304
+ const lookup = createNodeLookup(nodes);
1305
+ const dragged = [...ids].map((id) => lookup.get(id)).filter((n) => !!n);
1306
+ const empty = {
1307
+ position: { x: 0, y: 0 },
1308
+ offset: { x: 0, y: 0 },
1309
+ guides: [],
1310
+ snappedX: false,
1311
+ snappedY: false,
1312
+ updates: []
1313
+ };
1314
+ if (dragged.length === 0) return empty;
1315
+ const bounds = boundsOf(dragged.map((n) => getNodeRect(n, lookup, options)));
1316
+ const index = options.index ?? createNodeGuideIndex(nodes, ids, options);
1317
+ const result = computeSnapGuides(bounds, index, options);
1318
+ const { x: dx, y: dy } = result.offset;
1319
+ const updates = [];
1320
+ if (dx !== 0 || dy !== 0) {
1321
+ for (const node of dragged) {
1322
+ if (node.parentId && ids.has(node.parentId)) continue;
1323
+ updates.push({ id: node.id, position: { x: node.position.x + dx, y: node.position.y + dy } });
1324
+ }
1325
+ }
1326
+ return { ...result, updates };
1327
+ }
1328
+
1329
+ // src/canvas/connectors.ts
1330
+ var CONNECTION_LINE_TYPES = [
1331
+ "default",
1332
+ "straight",
1333
+ "step",
1334
+ "smoothstep",
1335
+ "simplebezier"
1336
+ ];
1337
+ function positionToSide(position) {
1338
+ switch (position) {
1339
+ case "top":
1340
+ return "above";
1341
+ case "bottom":
1342
+ return "below";
1343
+ default:
1344
+ return position;
1345
+ }
1346
+ }
1347
+ function sideToPosition(side) {
1348
+ switch (side) {
1349
+ case "above":
1350
+ return "top";
1351
+ case "below":
1352
+ return "bottom";
1353
+ default:
1354
+ return side;
1355
+ }
1356
+ }
1357
+ function oppositePosition(position) {
1358
+ switch (position) {
1359
+ case "top":
1360
+ return "bottom";
1361
+ case "bottom":
1362
+ return "top";
1363
+ case "left":
1364
+ return "right";
1365
+ default:
1366
+ return "left";
1367
+ }
1368
+ }
1369
+ function handlePoint(rect, position) {
1370
+ switch (position) {
1371
+ case "top":
1372
+ return { x: rect.x + rect.width / 2, y: rect.y };
1373
+ case "bottom":
1374
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height };
1375
+ case "left":
1376
+ return { x: rect.x, y: rect.y + rect.height / 2 };
1377
+ default:
1378
+ return { x: rect.x + rect.width, y: rect.y + rect.height / 2 };
1379
+ }
1380
+ }
1381
+ function findConnectorSnap(pointer, targets, options = {}) {
1382
+ const zoom = options.zoom && options.zoom > 0 ? options.zoom : 1;
1383
+ const radius = (options.radius ?? 24) / zoom;
1384
+ const exclude = new Set(options.excludeIds ?? []);
1385
+ const all = ["top", "right", "bottom", "left"];
1386
+ let best = null;
1387
+ for (const target of targets) {
1388
+ if (exclude.has(target.id)) continue;
1389
+ const { rect } = target;
1390
+ if (pointer.x < rect.x - radius || pointer.x > rect.x + rect.width + radius || pointer.y < rect.y - radius || pointer.y > rect.y + rect.height + radius) {
1391
+ continue;
1392
+ }
1393
+ const inside = pointer.x >= rect.x && pointer.x <= rect.x + rect.width && pointer.y >= rect.y && pointer.y <= rect.y + rect.height;
1394
+ for (const position of target.positions ?? all) {
1395
+ const point = options.mode === "edge" ? closestPointOnSide(rect, position, pointer) : handlePoint(rect, position);
1396
+ const sidePoint = closestPointOnSide(rect, position, pointer);
1397
+ const rank = Math.hypot(pointer.x - sidePoint.x, pointer.y - sidePoint.y);
1398
+ const distance = Math.hypot(pointer.x - point.x, pointer.y - point.y);
1399
+ if (!inside && rank > radius) continue;
1400
+ const better = !best || inside && !best.inside || inside === best.inside && rank < best.rank;
1401
+ if (better) {
1402
+ best = { nodeId: target.id, position, side: positionToSide(position), point, distance, inside, rank };
1403
+ }
1404
+ }
1405
+ }
1406
+ if (!best) return null;
1407
+ const { inside: _inside, rank: _rank, ...snap } = best;
1408
+ return snap;
1409
+ }
1410
+ function closestPointOnSide(rect, position, p) {
1411
+ const clampX = Math.min(rect.x + rect.width, Math.max(rect.x, p.x));
1412
+ const clampY = Math.min(rect.y + rect.height, Math.max(rect.y, p.y));
1413
+ switch (position) {
1414
+ case "top":
1415
+ return { x: clampX, y: rect.y };
1416
+ case "bottom":
1417
+ return { x: clampX, y: rect.y + rect.height };
1418
+ case "left":
1419
+ return { x: rect.x, y: clampY };
1420
+ default:
1421
+ return { x: rect.x + rect.width, y: clampY };
1422
+ }
1423
+ }
1424
+ function nearestHandlePositions(source, target) {
1425
+ const a = centre(source);
1426
+ const b = centre(target);
1427
+ const dx = b.x - a.x;
1428
+ const dy = b.y - a.y;
1429
+ if (Math.abs(dx) >= Math.abs(dy)) {
1430
+ return dx >= 0 ? { source: "right", target: "left" } : { source: "left", target: "right" };
1431
+ }
1432
+ return dy >= 0 ? { source: "bottom", target: "top" } : { source: "top", target: "bottom" };
1433
+ }
1434
+
1435
+ // src/canvas/scenes.ts
1436
+ var fallbackCounter = 0;
1437
+ function defaultId() {
1438
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1439
+ return crypto.randomUUID();
1440
+ }
1441
+ fallbackCounter += 1;
1442
+ return `scene-${Date.now().toString(36)}-${fallbackCounter}`;
1443
+ }
1444
+ function isFiniteNumber(value) {
1445
+ return typeof value === "number" && Number.isFinite(value);
1446
+ }
1447
+ function isValidCanvasScene(value) {
1448
+ if (typeof value !== "object" || value === null) return false;
1449
+ const scene = value;
1450
+ const vp = scene.viewport;
1451
+ return typeof scene.id === "string" && typeof scene.name === "string" && typeof vp === "object" && vp !== null && isFiniteNumber(vp.x) && isFiniteNumber(vp.y) && isFiniteNumber(vp.zoom) && vp.zoom > 0 && (scene.nodeIds === void 0 || Array.isArray(scene.nodeIds) && scene.nodeIds.every((id) => typeof id === "string"));
1452
+ }
1453
+ function cloneScene(scene) {
1454
+ const copy = {
1455
+ id: scene.id,
1456
+ name: scene.name,
1457
+ viewport: { x: scene.viewport.x, y: scene.viewport.y, zoom: scene.viewport.zoom }
1458
+ };
1459
+ if (scene.nodeIds) copy.nodeIds = [...scene.nodeIds];
1460
+ return copy;
1461
+ }
1462
+ function parseCanvasScenes(input) {
1463
+ let value = input;
1464
+ if (typeof input === "string") {
1465
+ try {
1466
+ value = JSON.parse(input);
1467
+ } catch {
1468
+ return [];
1469
+ }
1470
+ }
1471
+ const list = Array.isArray(value) ? value : typeof value === "object" && value !== null && Array.isArray(value.scenes) ? value.scenes : [];
1472
+ const seen = /* @__PURE__ */ new Set();
1473
+ const scenes = [];
1474
+ for (const entry of list) {
1475
+ if (!isValidCanvasScene(entry) || seen.has(entry.id)) continue;
1476
+ seen.add(entry.id);
1477
+ scenes.push(cloneScene(entry));
1478
+ }
1479
+ return scenes;
1480
+ }
1481
+ var CanvasScenesStore = class {
1482
+ scenes;
1483
+ currentId = null;
1484
+ cached;
1485
+ listeners = /* @__PURE__ */ new Set();
1486
+ createId;
1487
+ onChange;
1488
+ wrap;
1489
+ constructor(options = {}) {
1490
+ this.scenes = parseCanvasScenes(options.initialScenes ?? []);
1491
+ this.createId = options.createId ?? defaultId;
1492
+ this.wrap = options.wrap ?? false;
1493
+ this.onChange = options.onChange;
1494
+ this.cached = this.snapshot();
1495
+ }
1496
+ snapshot() {
1497
+ const currentIndex = this.currentId ? this.scenes.findIndex((s) => s.id === this.currentId) : -1;
1498
+ return {
1499
+ scenes: this.scenes,
1500
+ currentId: currentIndex === -1 ? null : this.currentId,
1501
+ currentIndex
1502
+ };
1503
+ }
1504
+ commit(scenes = this.scenes) {
1505
+ this.scenes = scenes;
1506
+ if (this.currentId && !scenes.some((s) => s.id === this.currentId)) this.currentId = null;
1507
+ this.cached = this.snapshot();
1508
+ this.onChange?.(this.cached);
1509
+ for (const listener of this.listeners) listener(this.cached);
1510
+ }
1511
+ getState() {
1512
+ return this.cached;
1513
+ }
1514
+ getScene(id) {
1515
+ return this.scenes.find((s) => s.id === id);
1516
+ }
1517
+ subscribe(listener) {
1518
+ this.listeners.add(listener);
1519
+ return () => {
1520
+ this.listeners.delete(listener);
1521
+ };
1522
+ }
1523
+ /** First unused "Scene N" name. */
1524
+ nextDefaultName() {
1525
+ const names = new Set(this.scenes.map((s) => s.name));
1526
+ let n = this.scenes.length + 1;
1527
+ while (names.has(`Scene ${n}`)) n += 1;
1528
+ return `Scene ${n}`;
1529
+ }
1530
+ /**
1531
+ * Adds a scene (appended, or inserted at `index`) and makes it current.
1532
+ */
1533
+ add(input, index) {
1534
+ const scene = cloneScene({
1535
+ id: input.id && !this.getScene(input.id) ? input.id : this.createId(),
1536
+ name: input.name?.trim() || this.nextDefaultName(),
1537
+ viewport: input.viewport,
1538
+ nodeIds: input.nodeIds
1539
+ });
1540
+ if (!isValidCanvasScene(scene)) throw new Error("[CanvasScenesStore] Invalid scene viewport.");
1541
+ const scenes = [...this.scenes];
1542
+ const at = index === void 0 ? scenes.length : Math.max(0, Math.min(scenes.length, index));
1543
+ scenes.splice(at, 0, scene);
1544
+ this.currentId = scene.id;
1545
+ this.commit(scenes);
1546
+ return scene;
1547
+ }
1548
+ /** Patches a scene's name, viewport or node ids. Returns the updated scene. */
1549
+ update(id, patch) {
1550
+ const index = this.scenes.findIndex((s) => s.id === id);
1551
+ if (index === -1) return void 0;
1552
+ const current = this.scenes[index];
1553
+ const next = cloneScene({
1554
+ ...current,
1555
+ ...patch,
1556
+ name: patch.name !== void 0 ? patch.name.trim() || current.name : current.name,
1557
+ viewport: patch.viewport ?? current.viewport,
1558
+ id
1559
+ });
1560
+ if (!isValidCanvasScene(next)) throw new Error("[CanvasScenesStore] Invalid scene viewport.");
1561
+ const scenes = [...this.scenes];
1562
+ scenes[index] = next;
1563
+ this.commit(scenes);
1564
+ return next;
1565
+ }
1566
+ rename(id, name) {
1567
+ return this.update(id, { name });
1568
+ }
1569
+ /** Moves a scene to `toIndex` (clamped). */
1570
+ move(id, toIndex) {
1571
+ const from = this.scenes.findIndex((s) => s.id === id);
1572
+ if (from === -1) return;
1573
+ const to = Math.max(0, Math.min(this.scenes.length - 1, Math.trunc(toIndex)));
1574
+ if (from === to) return;
1575
+ const scenes = [...this.scenes];
1576
+ const [scene] = scenes.splice(from, 1);
1577
+ scenes.splice(to, 0, scene);
1578
+ this.commit(scenes);
1579
+ }
1580
+ /** Reorders scenes to match `ids`; unknown ids are ignored and missing scenes keep their relative order at the end. */
1581
+ reorder(ids) {
1582
+ const byId = new Map(this.scenes.map((s) => [s.id, s]));
1583
+ const ordered = [];
1584
+ for (const id of ids) {
1585
+ const scene = byId.get(id);
1586
+ if (scene) {
1587
+ ordered.push(scene);
1588
+ byId.delete(id);
1589
+ }
1590
+ }
1591
+ this.commit([...ordered, ...this.scenes.filter((s) => byId.has(s.id))]);
1592
+ }
1593
+ remove(id) {
1594
+ if (!this.getScene(id)) return;
1595
+ this.commit(this.scenes.filter((s) => s.id !== id));
1596
+ }
1597
+ clear() {
1598
+ this.currentId = null;
1599
+ this.commit([]);
1600
+ }
1601
+ /** Marks a scene as current and returns it. */
1602
+ goTo(id) {
1603
+ const scene = this.getScene(id);
1604
+ if (!scene) return void 0;
1605
+ if (this.currentId !== id) {
1606
+ this.currentId = id;
1607
+ this.commit();
1608
+ }
1609
+ return scene;
1610
+ }
1611
+ step(direction) {
1612
+ const count = this.scenes.length;
1613
+ if (count === 0) return void 0;
1614
+ const index = this.cached.currentIndex;
1615
+ let next;
1616
+ if (index === -1) next = direction === 1 ? 0 : count - 1;
1617
+ else next = index + direction;
1618
+ if (next < 0 || next >= count) {
1619
+ if (!this.wrap) return void 0;
1620
+ next = (next + count) % count;
1621
+ }
1622
+ return this.goTo(this.scenes[next].id);
1623
+ }
1624
+ /** Shows the next scene. Returns `undefined` at the end unless `wrap` is on. */
1625
+ next() {
1626
+ return this.step(1);
1627
+ }
1628
+ /** Shows the previous scene. Returns `undefined` at the start unless `wrap` is on. */
1629
+ previous() {
1630
+ return this.step(-1);
1631
+ }
1632
+ toJSON() {
1633
+ return { version: 1, scenes: this.scenes.map(cloneScene) };
1634
+ }
1635
+ serialize() {
1636
+ return JSON.stringify(this.toJSON());
1637
+ }
1638
+ /** Replaces all scenes from serialized data (see `parseCanvasScenes`). */
1639
+ load(input) {
1640
+ this.currentId = null;
1641
+ this.commit(parseCanvasScenes(input));
1642
+ }
1643
+ };
1644
+ function createCanvasScenesStore(options) {
1645
+ return new CanvasScenesStore(options);
1646
+ }
1647
+ function fitViewportToRect(rect, size, options = {}) {
1648
+ const { padding = 0.1, minZoom = 0.1, maxZoom = 2 } = options;
1649
+ const paddedWidth = Math.max(rect.width * (1 + padding * 2), 1);
1650
+ const paddedHeight = Math.max(rect.height * (1 + padding * 2), 1);
1651
+ const raw = Math.min(size.width / paddedWidth, size.height / paddedHeight);
1652
+ const zoom = Math.min(maxZoom, Math.max(minZoom, raw));
1653
+ const cx = rect.x + rect.width / 2;
1654
+ const cy = rect.y + rect.height / 2;
1655
+ return { x: size.width / 2 - cx * zoom, y: size.height / 2 - cy * zoom, zoom };
1656
+ }
1657
+ function fitViewportToNodes(nodes, size, ids, options = {}) {
1658
+ const lookup = createNodeLookup(nodes);
1659
+ const wanted = ids ? new Set(ids) : null;
1660
+ const rects = nodes.filter((n) => wanted ? wanted.has(n.id) : true).map((n) => getNodeRect(n, lookup, options));
1661
+ const bounds = boundsOf(rects);
1662
+ return bounds ? fitViewportToRect(bounds, size, options) : null;
1663
+ }
1664
+
1665
+ // src/canvas/arrangeShortcuts.ts
1666
+ var DEFAULT_ARRANGE_SHORTCUTS = [
1667
+ { action: "alignLeft", code: "KeyA", alt: true, label: "Alt+A" },
1668
+ { action: "alignCenter", code: "KeyH", alt: true, label: "Alt+H" },
1669
+ { action: "alignRight", code: "KeyD", alt: true, label: "Alt+D" },
1670
+ { action: "alignTop", code: "KeyW", alt: true, label: "Alt+W" },
1671
+ { action: "alignMiddle", code: "KeyV", alt: true, label: "Alt+V" },
1672
+ { action: "alignBottom", code: "KeyS", alt: true, label: "Alt+S" },
1673
+ { action: "distributeHorizontal", code: "KeyH", alt: true, shift: true, label: "Alt+Shift+H" },
1674
+ { action: "distributeVertical", code: "KeyV", alt: true, shift: true, label: "Alt+Shift+V" },
1675
+ { action: "lock", code: "KeyL", mod: true, label: "Mod+L" },
1676
+ { action: "unlock", code: "KeyL", mod: true, alt: true, label: "Mod+Alt+L" },
1677
+ { action: "bringForward", code: "BracketRight", mod: true, label: "Mod+]" },
1678
+ { action: "sendBackward", code: "BracketLeft", mod: true, label: "Mod+[" },
1679
+ { action: "bringToFront", code: "BracketRight", mod: true, alt: true, label: "Mod+Alt+]" },
1680
+ { action: "sendToBack", code: "BracketLeft", mod: true, alt: true, label: "Mod+Alt+[" },
1681
+ { action: "nextScene", code: "PageDown", label: "PageDown" },
1682
+ { action: "previousScene", code: "PageUp", label: "PageUp" }
1683
+ ];
1684
+ var KEY_TO_CODE = {
1685
+ "]": "BracketRight",
1686
+ "[": "BracketLeft",
1687
+ PageDown: "PageDown",
1688
+ PageUp: "PageUp"
1689
+ };
1690
+ function eventCode(event) {
1691
+ if (event.code) return event.code;
1692
+ if (KEY_TO_CODE[event.key]) return KEY_TO_CODE[event.key];
1693
+ if (/^[a-z]$/i.test(event.key)) return `Key${event.key.toUpperCase()}`;
1694
+ return event.key;
1695
+ }
1696
+ function matchArrangeShortcut(event, bindings = DEFAULT_ARRANGE_SHORTCUTS) {
1697
+ if (isEventFromTextInput(event)) return null;
1698
+ const code = eventCode(event);
1699
+ const mod = event.metaKey || event.ctrlKey;
1700
+ for (const binding of bindings) {
1701
+ if (binding.code === code && !!binding.mod === mod && !!binding.alt === event.altKey && !!binding.shift === event.shiftKey) {
1702
+ return binding.action;
1703
+ }
1704
+ }
1705
+ return null;
1706
+ }
1707
+
1708
+ // src/canvas/arrangeController.ts
1709
+ function createCanvasArrangeController(options) {
1710
+ let guides = [];
1711
+ let dragIndex = null;
1712
+ let dragKey = "";
1713
+ const selectedIds = (ids) => ids ? [...ids] : options.getSelectedIds?.() ?? options.getNodes().filter((n) => n.selected).map((n) => n.id);
1714
+ const setGuides = (next) => {
1715
+ if (next.length === 0 && guides.length === 0) return;
1716
+ guides = next;
1717
+ options.onGuidesChange?.(guides);
1718
+ };
1719
+ function apply(name, updates) {
1720
+ if (updates.length === 0) return updates;
1721
+ const inverse = invertNodeUpdates(options.getNodes(), updates);
1722
+ const run = (u) => options.setNodes(applyNodeUpdates(options.getNodes(), u));
1723
+ if (options.history) {
1724
+ options.history.execute({ name, execute: () => run(updates), undo: () => run(inverse) });
1725
+ } else {
1726
+ run(updates);
1727
+ }
1728
+ return updates;
1729
+ }
1730
+ function setLocked(name, ids, locked) {
1731
+ const nodes = options.getNodes();
1732
+ const set = new Set(ids);
1733
+ const changed = nodes.filter((n) => set.has(n.id) && isLocked(n) !== locked).map((n) => n.id);
1734
+ if (changed.length === 0) return changed;
1735
+ const lock = () => options.setNodes(lockNodes(options.getNodes(), changed));
1736
+ const unlock = () => options.setNodes(unlockNodes(options.getNodes(), changed));
1737
+ if (options.history) {
1738
+ options.history.execute({ name, execute: locked ? lock : unlock, undo: locked ? unlock : lock });
1739
+ } else if (locked) lock();
1740
+ else unlock();
1741
+ return changed;
1742
+ }
1743
+ const rect = () => options.rectOptions ?? {};
1744
+ const controller = {
1745
+ get guides() {
1746
+ return guides;
1747
+ },
1748
+ apply,
1749
+ align(edge, ids, alignOptions = {}) {
1750
+ return apply(`Align ${edge}`, alignNodes(options.getNodes(), selectedIds(ids), edge, { ...rect(), ...alignOptions }));
1751
+ },
1752
+ distribute(axis, mode = "gaps", ids, distributeOptions = {}) {
1753
+ return apply(
1754
+ `Distribute ${axis}`,
1755
+ distributeNodes(options.getNodes(), selectedIds(ids), axis, mode, { ...rect(), ...distributeOptions })
1756
+ );
1757
+ },
1758
+ matchSize(dimension, ids, sizeOptions = {}) {
1759
+ return apply(
1760
+ "Make Same Size",
1761
+ matchSize(options.getNodes(), selectedIds(ids), dimension, { ...rect(), ...sizeOptions })
1762
+ );
1763
+ },
1764
+ reorder(operation, ids) {
1765
+ return apply(operation, reorderNodes(options.getNodes(), selectedIds(ids), operation, rect()));
1766
+ },
1767
+ bringForward: (ids) => controller.reorder("bringForward", ids),
1768
+ sendBackward: (ids) => controller.reorder("sendBackward", ids),
1769
+ bringToFront: (ids) => controller.reorder("bringToFront", ids),
1770
+ sendToBack: (ids) => controller.reorder("sendToBack", ids),
1771
+ lock: (ids) => setLocked("Lock", selectedIds(ids), true),
1772
+ unlock: (ids) => setLocked("Unlock", selectedIds(ids), false),
1773
+ toggleLock(ids) {
1774
+ const list = selectedIds(ids);
1775
+ const set = new Set(list);
1776
+ const anyUnlocked = options.getNodes().some((n) => set.has(n.id) && !isLocked(n));
1777
+ return anyUnlocked ? controller.lock(list) : controller.unlock(list);
1778
+ },
1779
+ isLocked: (id) => isLocked(options.getNodes().find((n) => n.id === id)),
1780
+ /**
1781
+ * Runs a shortcut action. Scene actions are not handled here (see the scenes store);
1782
+ * returns `false` for them and for unknown actions.
1783
+ */
1784
+ runAction(action) {
1785
+ switch (action) {
1786
+ case "alignLeft":
1787
+ controller.align("left");
1788
+ return true;
1789
+ case "alignCenter":
1790
+ controller.align("center");
1791
+ return true;
1792
+ case "alignRight":
1793
+ controller.align("right");
1794
+ return true;
1795
+ case "alignTop":
1796
+ controller.align("top");
1797
+ return true;
1798
+ case "alignMiddle":
1799
+ controller.align("middle");
1800
+ return true;
1801
+ case "alignBottom":
1802
+ controller.align("bottom");
1803
+ return true;
1804
+ case "distributeHorizontal":
1805
+ controller.distribute("horizontal");
1806
+ return true;
1807
+ case "distributeVertical":
1808
+ controller.distribute("vertical");
1809
+ return true;
1810
+ case "lock":
1811
+ controller.lock();
1812
+ return true;
1813
+ case "unlock":
1814
+ controller.unlock();
1815
+ return true;
1816
+ case "bringForward":
1817
+ controller.bringForward();
1818
+ return true;
1819
+ case "sendBackward":
1820
+ controller.sendBackward();
1821
+ return true;
1822
+ case "bringToFront":
1823
+ controller.bringToFront();
1824
+ return true;
1825
+ case "sendToBack":
1826
+ controller.sendToBack();
1827
+ return true;
1828
+ default:
1829
+ return false;
1830
+ }
1831
+ },
1832
+ /** Caches the snap targets for a drag. Optional: `drag` builds them lazily. */
1833
+ beginDrag(ids) {
1834
+ const list = selectedIds(ids);
1835
+ dragKey = list.slice().sort().join("\0");
1836
+ dragIndex = createNodeGuideIndex(options.getNodes(), list, rect());
1837
+ },
1838
+ /**
1839
+ * Snaps the dragged nodes (as currently positioned in `getNodes()`) to the other
1840
+ * nodes, writes the snapped positions back through `setNodes` (not recorded in
1841
+ * history) and updates `guides`.
1842
+ */
1843
+ drag(ids) {
1844
+ if (options.snapping === false) return null;
1845
+ const list = selectedIds(ids);
1846
+ const key = list.slice().sort().join("\0");
1847
+ if (!dragIndex || key !== dragKey) controller.beginDrag(list);
1848
+ const result = snapNodeDrag(options.getNodes(), list, {
1849
+ ...rect(),
1850
+ ...options.guides,
1851
+ zoom: options.getZoom?.() ?? 1,
1852
+ index: dragIndex
1853
+ });
1854
+ if (result.updates.length > 0) {
1855
+ options.setNodes(applyNodeUpdates(options.getNodes(), result.updates));
1856
+ }
1857
+ setGuides(result.guides);
1858
+ return result;
1859
+ },
1860
+ /**
1861
+ * React Flow style: adjusts dragging `position` changes in an `onNodesChange` batch so
1862
+ * the dragged nodes snap, and updates `guides`. Returns the (possibly new) changes array.
1863
+ * A batch without a dragging position change ends the drag and clears the guides.
1864
+ */
1865
+ snapPositionChanges(changes) {
1866
+ const dragging = changes.filter((c) => c.type === "position" && c.position && c.dragging);
1867
+ if (dragging.length === 0) {
1868
+ if (changes.some((c) => c.type === "position" && c.dragging === false)) controller.endDrag();
1869
+ return changes;
1870
+ }
1871
+ if (options.snapping === false) return changes;
1872
+ const ids = dragging.map((c) => c.id);
1873
+ const key = ids.slice().sort().join("\0");
1874
+ if (!dragIndex || key !== dragKey) controller.beginDrag(ids);
1875
+ const proposed = new Map(dragging.map((c) => [c.id, c.position]));
1876
+ const nodes = options.getNodes().map((n) => proposed.has(n.id) ? { ...n, position: proposed.get(n.id) } : n);
1877
+ const result = snapNodeDrag(nodes, ids, {
1878
+ ...rect(),
1879
+ ...options.guides,
1880
+ zoom: options.getZoom?.() ?? 1,
1881
+ index: dragIndex
1882
+ });
1883
+ setGuides(result.guides);
1884
+ const { x: dx, y: dy } = result.offset;
1885
+ if (dx === 0 && dy === 0) return changes;
1886
+ const parents = new Map(nodes.map((n) => [n.id, n.parentId]));
1887
+ const shift = new Set(ids.filter((id) => !proposed.has(parents.get(id) ?? "")));
1888
+ return changes.map(
1889
+ (c) => c.type === "position" && c.position && c.dragging && shift.has(c.id) ? { ...c, position: { x: c.position.x + dx, y: c.position.y + dy } } : c
1890
+ );
1891
+ },
1892
+ /** Clears guides and the cached snap targets. */
1893
+ endDrag() {
1894
+ dragIndex = null;
1895
+ dragKey = "";
1896
+ setGuides([]);
1897
+ }
1898
+ };
1899
+ return controller;
1900
+ }
722
1901
  export {
1902
+ CONNECTION_LINE_TYPES,
1903
+ CanvasScenesStore,
1904
+ DEFAULT_ARRANGE_SHORTCUTS,
723
1905
  DEFAULT_NODE_HEIGHT,
724
1906
  DEFAULT_NODE_WIDTH,
725
1907
  DEFAULT_PLACEMENT_GAP,
1908
+ LOCKED_DATA_KEY,
1909
+ LOCKED_PREVIOUS_FLAGS_KEY,
726
1910
  ManualClock,
727
1911
  StreamingLayoutManager,
1912
+ alignNodes,
1913
+ applyNodeUpdates,
728
1914
  blendLayout,
729
1915
  boundsOf,
730
1916
  calculateDagreReflowDisplacement,
@@ -732,33 +1918,65 @@ export {
732
1918
  canvasHeight,
733
1919
  centerNodes,
734
1920
  centre,
1921
+ collectDescendantIds,
735
1922
  computeNodesBoundingBox,
1923
+ computeSnapGuides,
736
1924
  connectedOnly,
737
1925
  createBezierTrajectory,
1926
+ createCanvasArrangeController,
1927
+ createCanvasScenesStore,
738
1928
  createFanOutTrajectory,
1929
+ createGuideIndex,
1930
+ createNodeGuideIndex,
1931
+ createNodeLookup,
739
1932
  defaultEasing,
1933
+ distributeNodes,
740
1934
  ensureLayout,
741
1935
  estimateCanvasHeight,
742
1936
  estimateNodeSize,
1937
+ findConnectorSnap,
1938
+ fitViewportToNodes,
1939
+ fitViewportToRect,
743
1940
  flowSide,
1941
+ getAbsolutePosition,
744
1942
  getCenteredNodePosition,
745
1943
  getLayoutedNodes,
1944
+ getNodeRect,
1945
+ getNodeSize,
1946
+ handlePoint,
1947
+ invertNodeUpdates,
746
1948
  isEventFromTextInput,
1949
+ isLocked,
747
1950
  isValidCanvasEdge,
748
1951
  isValidCanvasNode,
1952
+ isValidCanvasScene,
749
1953
  isValidUrl,
750
1954
  layoutBounds,
751
1955
  linearPositionTrajectory,
752
1956
  linearTrajectory,
1957
+ lockNodes,
1958
+ matchArrangeShortcut,
1959
+ matchSize,
1960
+ nearestHandlePositions,
1961
+ oppositePosition,
753
1962
  overlaps,
754
1963
  pageHidden,
755
1964
  parseCanvasClipboardData,
1965
+ parseCanvasScenes,
756
1966
  placeBeside,
1967
+ positionToSide,
1968
+ readStyleSize,
757
1969
  realClock,
758
1970
  remapPastedNodes,
1971
+ reorderNodes,
759
1972
  repositionNodesTo,
760
1973
  runMultiNodeTransition,
761
1974
  serializeCanvasNodes,
762
- withStyleOpacity
1975
+ sideToPosition,
1976
+ snapNodeDrag,
1977
+ toggleLockNodes,
1978
+ unlockNodes,
1979
+ withStyleOpacity,
1980
+ withStyleSize
763
1981
  };
764
1982
  //# sourceMappingURL=index.js.map