@lupinum/board-core 0.1.0 → 1.0.0-beta.1

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/index.js CHANGED
@@ -5,8 +5,14 @@ import {
5
5
  BoardInputError,
6
6
  BoardNotFoundError,
7
7
  assertInternalBoardPlugin,
8
- registerBoardInteractionAdapter
9
- } from "./chunk-5JZXHZWR.js";
8
+ collectJsonObjectExtras,
9
+ freezeClone,
10
+ freezeJsonObject,
11
+ readonlyMapView,
12
+ readonlySetView,
13
+ registerBoardInteractionAdapter,
14
+ sameArray
15
+ } from "./chunk-45V474TA.js";
10
16
 
11
17
  // src/math.ts
12
18
  function clamp(value, min, max) {
@@ -528,112 +534,6 @@ function snapPositionToEdges(bounds, otherEdges, threshold, excludeIds) {
528
534
  return { dx, dy, guides };
529
535
  }
530
536
 
531
- // src/helpers/clone.ts
532
- function freezeClone(value) {
533
- if (Array.isArray(value)) {
534
- for (const entry of value) {
535
- freezeClone(entry);
536
- }
537
- return Object.freeze(value);
538
- }
539
- if (value && typeof value === "object") {
540
- for (const child of Object.values(value)) {
541
- freezeClone(child);
542
- }
543
- return Object.freeze(value);
544
- }
545
- return value;
546
- }
547
- function sameArray(a, b) {
548
- if (a.length !== b.length) {
549
- return false;
550
- }
551
- return a.every((value, index) => value === b[index]);
552
- }
553
- function readonlyMapView(source) {
554
- let view;
555
- view = Object.freeze({
556
- get size() {
557
- return source.size;
558
- },
559
- get: (key) => source.get(key),
560
- has: (key) => source.has(key),
561
- entries: () => source.entries(),
562
- keys: () => source.keys(),
563
- values: () => source.values(),
564
- forEach: (callback, thisArg) => {
565
- source.forEach((value, key) => callback.call(thisArg, value, key, view));
566
- },
567
- [Symbol.iterator]: () => source[Symbol.iterator]()
568
- });
569
- return view;
570
- }
571
- function readonlySetView(source) {
572
- let view;
573
- view = Object.freeze({
574
- get size() {
575
- return source.size;
576
- },
577
- has: (value) => source.has(value),
578
- entries: () => source.entries(),
579
- keys: () => source.keys(),
580
- values: () => source.values(),
581
- forEach: (callback, thisArg) => {
582
- source.forEach((value) => callback.call(thisArg, value, value, view));
583
- },
584
- [Symbol.iterator]: () => source[Symbol.iterator](),
585
- union: (other) => {
586
- const result = new Set(source);
587
- for (const value of iteratorValues(other.keys())) result.add(value);
588
- return result;
589
- },
590
- intersection: (other) => {
591
- const result = /* @__PURE__ */ new Set();
592
- for (const value of source) {
593
- if (other.has(value)) result.add(value);
594
- }
595
- return result;
596
- },
597
- difference: (other) => {
598
- const result = /* @__PURE__ */ new Set();
599
- for (const value of source) {
600
- if (!other.has(value)) result.add(value);
601
- }
602
- return result;
603
- },
604
- symmetricDifference: (other) => {
605
- const result = new Set(source);
606
- for (const value of iteratorValues(other.keys())) {
607
- if (source.has(value)) result.delete(value);
608
- else result.add(value);
609
- }
610
- return result;
611
- },
612
- isSubsetOf: (other) => {
613
- for (const value of source) if (!other.has(value)) return false;
614
- return true;
615
- },
616
- isSupersetOf: (other) => {
617
- for (const value of iteratorValues(other.keys())) {
618
- if (!source.has(value)) return false;
619
- }
620
- return true;
621
- },
622
- isDisjointFrom: (other) => {
623
- for (const value of source) if (other.has(value)) return false;
624
- return true;
625
- }
626
- });
627
- return view;
628
- }
629
- function* iteratorValues(iterator) {
630
- while (true) {
631
- const result = iterator.next();
632
- if (result.done) return;
633
- yield result.value;
634
- }
635
- }
636
-
637
537
  // src/helpers/ids.ts
638
538
  function createNodeId() {
639
539
  return crypto.randomUUID();
@@ -1593,7 +1493,11 @@ function stagePersistentRoots(roots) {
1593
1493
  selection: new Set(roots.state.selection),
1594
1494
  interaction: cloneInteraction(roots.state.interaction),
1595
1495
  snapGuides: roots.state.snapGuides.map((guide) => ({ ...guide })),
1596
- nextZIndex: roots.state.nextZIndex
1496
+ nextZIndex: roots.state.nextZIndex,
1497
+ jsonCanvas: {
1498
+ document: roots.state.jsonCanvas.document,
1499
+ nodes: new Map(roots.state.jsonCanvas.nodes)
1500
+ }
1597
1501
  },
1598
1502
  grid: { ...roots.grid },
1599
1503
  pluginStates: new Map(
@@ -1639,6 +1543,29 @@ var JSON_CANVAS_SIDES = /* @__PURE__ */ new Set([
1639
1543
  ]);
1640
1544
  var JSON_CANVAS_EDGE_ENDS = /* @__PURE__ */ new Set(["none", "arrow"]);
1641
1545
  var JSON_CANVAS_BACKGROUND_STYLES = /* @__PURE__ */ new Set(["cover", "ratio", "repeat"]);
1546
+ var LEGACY_BOARD_METADATA_KEY = "x-vue-board";
1547
+ var DOCUMENT_FIELDS = /* @__PURE__ */ new Set([
1548
+ "nodes",
1549
+ "edges",
1550
+ "x-lupinum-board",
1551
+ LEGACY_BOARD_METADATA_KEY
1552
+ ]);
1553
+ var NODE_FIELDS = /* @__PURE__ */ new Set([
1554
+ "id",
1555
+ "type",
1556
+ "x",
1557
+ "y",
1558
+ "width",
1559
+ "height",
1560
+ "color",
1561
+ "text",
1562
+ "file",
1563
+ "subpath",
1564
+ "url",
1565
+ "label",
1566
+ "background",
1567
+ "backgroundStyle"
1568
+ ]);
1642
1569
  function isJsonCanvasNodeType(value) {
1643
1570
  return typeof value === "string" && JSON_CANVAS_NODE_TYPES.has(value);
1644
1571
  }
@@ -1716,8 +1643,9 @@ function jsonNodeToBoardNode(node, meta, index) {
1716
1643
  };
1717
1644
  return withNodeFields(base, node);
1718
1645
  }
1719
- function boardNodeToJsonNode(node) {
1646
+ function boardNodeToJsonNode(node, extras) {
1720
1647
  const base = {
1648
+ ...extras ?? {},
1721
1649
  id: node.id,
1722
1650
  type: node.type,
1723
1651
  x: node.x,
@@ -1759,9 +1687,10 @@ function mergeMetadata(base, patch) {
1759
1687
  };
1760
1688
  }
1761
1689
  function getDocumentMetadata(document) {
1762
- return document["x-vue-board"];
1690
+ const record = document;
1691
+ return document["x-lupinum-board"] ?? record[LEGACY_BOARD_METADATA_KEY];
1763
1692
  }
1764
- function toPersistedDocument(snapshot, featureDocuments) {
1693
+ function toPersistedDocument(snapshot, featureDocuments, passthrough) {
1765
1694
  let metadata = {
1766
1695
  camera: snapshot.camera,
1767
1696
  grid: snapshot.grid,
@@ -1787,13 +1716,18 @@ function toPersistedDocument(snapshot, featureDocuments) {
1787
1716
  metadata = mergeMetadata(metadata, getDocumentMetadata(featureDocument));
1788
1717
  }
1789
1718
  return {
1790
- nodes: snapshot.nodes.map((node) => boardNodeToJsonNode(node)),
1719
+ ...passthrough.document,
1720
+ nodes: snapshot.nodes.map(
1721
+ (node) => boardNodeToJsonNode(node, passthrough.nodes.get(node.id))
1722
+ ),
1791
1723
  ...edges !== void 0 ? { edges } : {},
1792
- "x-vue-board": metadata
1724
+ "x-lupinum-board": metadata
1793
1725
  };
1794
1726
  }
1795
1727
  function isRecord(value) {
1796
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1728
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1729
+ const prototype = Object.getPrototypeOf(value);
1730
+ return prototype === Object.prototype || prototype === null;
1797
1731
  }
1798
1732
  function assertRecord(value, message) {
1799
1733
  if (!isRecord(value)) {
@@ -1921,14 +1855,21 @@ function validateDocumentMetadata(metadata) {
1921
1855
  `Invalid board document: metadata for edge "${id}" has invalid data.`
1922
1856
  );
1923
1857
  }
1858
+ if (edge.data !== void 0) {
1859
+ freezeJsonObject(
1860
+ edge.data,
1861
+ `Invalid board document: metadata for edge "${id}" data`
1862
+ );
1863
+ }
1924
1864
  }
1925
1865
  }
1926
1866
  }
1927
1867
  function normalizeDocumentForImport(raw) {
1928
- const parsed = assertRecord(
1868
+ const parsedRecord = assertRecord(
1929
1869
  raw,
1930
1870
  "Invalid board document: document must be an object."
1931
1871
  );
1872
+ const parsed = parsedRecord;
1932
1873
  if (!Array.isArray(parsed.nodes)) {
1933
1874
  throw new BoardInputError("Invalid board document: missing nodes array.");
1934
1875
  }
@@ -1942,11 +1883,16 @@ function normalizeDocumentForImport(raw) {
1942
1883
  ]) {
1943
1884
  if (key in parsed) {
1944
1885
  throw new BoardInputError(
1945
- `Invalid board document: runtime field "${key}" belongs under x-vue-board.`
1886
+ `Invalid board document: runtime field "${key}" belongs under x-lupinum-board.`
1946
1887
  );
1947
1888
  }
1948
1889
  }
1949
- validateDocumentMetadata(getDocumentMetadata(parsed));
1890
+ const rawMetadata = getDocumentMetadata(parsed);
1891
+ validateDocumentMetadata(rawMetadata);
1892
+ const metadata = rawMetadata === void 0 ? void 0 : freezeJsonObject(
1893
+ rawMetadata,
1894
+ "Invalid board document: board metadata"
1895
+ );
1950
1896
  const seenNodes = /* @__PURE__ */ new Set();
1951
1897
  const nodes = parsed.nodes.map((node) => {
1952
1898
  if (!isRecord(node)) {
@@ -1969,7 +1915,10 @@ function normalizeDocumentForImport(raw) {
1969
1915
  `Invalid board document: node "${String(node.id)}" has unsupported backgroundStyle "${String(node.backgroundStyle)}".`
1970
1916
  );
1971
1917
  }
1972
- const normalized = { ...node };
1918
+ const normalized = freezeJsonObject(
1919
+ node,
1920
+ `Invalid board document: node "${String(node.id ?? "?")}"`
1921
+ );
1973
1922
  validateJsonCanvasNodeFields(normalized);
1974
1923
  if (seenNodes.has(normalized.id)) {
1975
1924
  throw new BoardInputError(
@@ -2037,13 +1986,41 @@ function normalizeDocumentForImport(raw) {
2037
1986
  `Invalid board document: edge "${id}" has invalid label.`
2038
1987
  );
2039
1988
  }
2040
- return { ...edge, id, fromNode, toNode };
1989
+ return freezeJsonObject(
1990
+ { ...edge, id, fromNode, toNode },
1991
+ `Invalid board document: edge "${id}"`
1992
+ );
2041
1993
  });
2042
1994
  }
2043
1995
  return {
1996
+ ...collectJsonObjectExtras(
1997
+ parsedRecord,
1998
+ DOCUMENT_FIELDS,
1999
+ "Invalid board document"
2000
+ ),
2044
2001
  nodes,
2045
2002
  ...edges !== void 0 ? { edges } : {},
2046
- ...getDocumentMetadata(parsed) !== void 0 ? { "x-vue-board": getDocumentMetadata(parsed) } : {}
2003
+ ...metadata !== void 0 ? { "x-lupinum-board": metadata } : {}
2004
+ };
2005
+ }
2006
+ function extractJsonCanvasPassthrough(document) {
2007
+ const record = document;
2008
+ return {
2009
+ document: collectJsonObjectExtras(
2010
+ record,
2011
+ DOCUMENT_FIELDS,
2012
+ "Invalid board document"
2013
+ ),
2014
+ nodes: new Map(
2015
+ document.nodes.map((node) => [
2016
+ node.id,
2017
+ collectJsonObjectExtras(
2018
+ node,
2019
+ NODE_FIELDS,
2020
+ `Invalid board document: node "${node.id}"`
2021
+ )
2022
+ ])
2023
+ )
2047
2024
  };
2048
2025
  }
2049
2026
  function materializeSnapshotNodes(snapshot) {
@@ -2163,6 +2140,110 @@ function createCameraSession(deps) {
2163
2140
  return { animateTo, cancelAnimations, computeFit };
2164
2141
  }
2165
2142
 
2143
+ // src/engine/public-engine.ts
2144
+ function createPublicEngine(engine) {
2145
+ return {
2146
+ plugins: engine.plugins,
2147
+ $camera: engine.$camera,
2148
+ $grid: engine.$grid,
2149
+ $nodes: engine.$nodes,
2150
+ $selection: engine.$selection,
2151
+ $interaction: engine.$interaction,
2152
+ $snapGuides: engine.$snapGuides,
2153
+ destroy: engine.destroy,
2154
+ batch: engine.batch,
2155
+ getState: engine.getState,
2156
+ getGridSettings: engine.getGridSettings,
2157
+ getViewportSize: engine.getViewportSize,
2158
+ updateGridSettings: engine.updateGridSettings,
2159
+ setViewportSize: engine.setViewportSize,
2160
+ on: engine.on,
2161
+ once: engine.once,
2162
+ off: engine.off,
2163
+ exportTrace: engine.exportTrace,
2164
+ addCommandGuard: engine.addCommandGuard,
2165
+ screenToWorld: engine.screenToWorld,
2166
+ worldToScreen: engine.worldToScreen,
2167
+ getVisibleBounds: engine.getVisibleBounds,
2168
+ getNode: engine.getNode,
2169
+ findNode: engine.findNode,
2170
+ hasNode: engine.hasNode,
2171
+ getNodeAt: engine.getNodeAt,
2172
+ getNodesInBounds: engine.getNodesInBounds,
2173
+ panBy: engine.panBy,
2174
+ panTo: engine.panTo,
2175
+ zoomAt: engine.zoomAt,
2176
+ zoomTo: engine.zoomTo,
2177
+ zoomToFit: engine.zoomToFit,
2178
+ zoomToNodes: engine.zoomToNodes,
2179
+ createNode: engine.createNode,
2180
+ updateNode: engine.updateNode,
2181
+ deleteNode: engine.deleteNode,
2182
+ moveNode: engine.moveNode,
2183
+ translateSelectedNodes: engine.translateSelectedNodes,
2184
+ resizeNode: engine.resizeNode,
2185
+ bringToFront: engine.bringToFront,
2186
+ sendToBack: engine.sendToBack,
2187
+ lockNode: engine.lockNode,
2188
+ unlockNode: engine.unlockNode,
2189
+ duplicateNodes: engine.duplicateNodes,
2190
+ copySelected: engine.copySelected,
2191
+ pasteClipboard: engine.pasteClipboard,
2192
+ pasteData: engine.pasteData,
2193
+ select: engine.select,
2194
+ selectAll: engine.selectAll,
2195
+ clearSelection: engine.clearSelection,
2196
+ deleteSelected: engine.deleteSelected,
2197
+ getSelection: engine.getSelection,
2198
+ beginTextEdit: engine.beginTextEdit,
2199
+ commitTextEdit: engine.commitTextEdit,
2200
+ cancelTextEdit: engine.cancelTextEdit,
2201
+ exportDocument: engine.exportDocument,
2202
+ loadDocument: engine.loadDocument
2203
+ };
2204
+ }
2205
+
2206
+ // src/engine/json-canvas-extras.ts
2207
+ function removeNodeExtras(current, id) {
2208
+ if (!current.nodes.has(id)) return current;
2209
+ const nodes = new Map(current.nodes);
2210
+ nodes.delete(id);
2211
+ return { ...current, nodes };
2212
+ }
2213
+ function copyNodeExtras(current, idMap, source = current.nodes) {
2214
+ const nodes = new Map(current.nodes);
2215
+ for (const [sourceId, targetId] of idMap) {
2216
+ const extras = source.get(sourceId);
2217
+ if (extras) nodes.set(targetId, extras);
2218
+ }
2219
+ return { ...current, nodes };
2220
+ }
2221
+ function replaceJsonCanvasExtras(incoming) {
2222
+ return {
2223
+ document: incoming.document,
2224
+ nodes: new Map(incoming.nodes)
2225
+ };
2226
+ }
2227
+ function mergeJsonCanvasExtras(current, incoming, idMap) {
2228
+ const document = Object.freeze({
2229
+ ...current.document,
2230
+ ...incoming.document
2231
+ });
2232
+ const nodes = new Map(current.nodes);
2233
+ for (const [id, extras] of incoming.nodes) {
2234
+ nodes.set(idMap.get(id) ?? id, extras);
2235
+ }
2236
+ return { document, nodes };
2237
+ }
2238
+ function selectNodeExtras(source, ids) {
2239
+ const selected = /* @__PURE__ */ new Map();
2240
+ for (const id of ids) {
2241
+ const extras = source.get(id);
2242
+ if (extras) selected.set(id, extras);
2243
+ }
2244
+ return selected;
2245
+ }
2246
+
2166
2247
  // src/engine/node-shape.ts
2167
2248
  function assertValidNodeGeometry(id, geometry) {
2168
2249
  if (!Number.isFinite(geometry.x) || !Number.isFinite(geometry.y) || !Number.isFinite(geometry.width) || !Number.isFinite(geometry.height) || geometry.width <= 0 || geometry.height <= 0) {
@@ -2368,6 +2449,7 @@ function createBoardEngine(options = {}) {
2368
2449
  const { emit, emitImmediate, on, once, off, reportUnhandledError } = eventBus;
2369
2450
  const commandGuards = createCommandGuardRegistry();
2370
2451
  const commitProjectors = /* @__PURE__ */ new Set();
2452
+ const projectedSubscribables = /* @__PURE__ */ new Set();
2371
2453
  const nodeDeletedHooks = /* @__PURE__ */ new Set();
2372
2454
  const pluginCleanups = /* @__PURE__ */ new Map();
2373
2455
  let pluginStates = /* @__PURE__ */ new Map();
@@ -2376,6 +2458,7 @@ function createBoardEngine(options = {}) {
2376
2458
  for (const hook of nodeDeletedHooks) hook(nodeId);
2377
2459
  }
2378
2460
  const clipboard = [];
2461
+ const clipboardPassthrough = /* @__PURE__ */ new Map();
2379
2462
  const plugins = {};
2380
2463
  let viewportSize = { ...DEFAULT_VIEWPORT_SIZE };
2381
2464
  let destroyed = false;
@@ -2414,11 +2497,13 @@ function createBoardEngine(options = {}) {
2414
2497
  selection: /* @__PURE__ */ new Set(),
2415
2498
  interaction: { mode: "idle" },
2416
2499
  snapGuides: [],
2417
- nextZIndex: 1
2500
+ nextZIndex: 1,
2501
+ jsonCanvas: { document: Object.freeze({}), nodes: /* @__PURE__ */ new Map() }
2418
2502
  };
2419
2503
  const initialDocument = options.initialDocument ? normalizeDocumentForImport(options.initialDocument) : null;
2420
2504
  if (initialDocument) {
2421
2505
  const initial = documentToSnapshot(initialDocument);
2506
+ state.jsonCanvas = extractJsonCanvasPassthrough(initialDocument);
2422
2507
  state.camera = { ...initial.camera };
2423
2508
  state.selection = /* @__PURE__ */ new Set();
2424
2509
  state.nextZIndex = initial.nextZIndex;
@@ -2518,6 +2603,7 @@ function createBoardEngine(options = {}) {
2518
2603
  const checkpoint = {
2519
2604
  roots,
2520
2605
  clipboard: [...clipboard],
2606
+ clipboardPassthrough: new Map(clipboardPassthrough),
2521
2607
  nodeOverrides: new Map(nodeOverrides),
2522
2608
  activeGestureHistoryRoot,
2523
2609
  activeBoxSelectionBefore
@@ -2533,6 +2619,10 @@ function createBoardEngine(options = {}) {
2533
2619
  grid = checkpoint.roots.grid;
2534
2620
  pluginStates = checkpoint.roots.pluginStates;
2535
2621
  clipboard.splice(0, clipboard.length, ...checkpoint.clipboard);
2622
+ clipboardPassthrough.clear();
2623
+ for (const [id, extras] of checkpoint.clipboardPassthrough) {
2624
+ clipboardPassthrough.set(id, extras);
2625
+ }
2536
2626
  nodeOverrides.clear();
2537
2627
  for (const [id, node] of checkpoint.nodeOverrides) {
2538
2628
  nodeOverrides.set(id, node);
@@ -2547,6 +2637,7 @@ function createBoardEngine(options = {}) {
2547
2637
  grid: freezeClone({ ...grid }),
2548
2638
  selection: state.selection,
2549
2639
  nextZIndex: state.nextZIndex,
2640
+ jsonCanvas: state.jsonCanvas,
2550
2641
  pluginSlices: new Map(
2551
2642
  Array.from(pluginStates, ([name, pluginState]) => [
2552
2643
  name,
@@ -2557,6 +2648,11 @@ function createBoardEngine(options = {}) {
2557
2648
  }
2558
2649
  function sameHistoryRoot(left, right) {
2559
2650
  if (left.nextZIndex !== right.nextZIndex) return false;
2651
+ if (left.jsonCanvas.document !== right.jsonCanvas.document) return false;
2652
+ if (left.jsonCanvas.nodes.size !== right.jsonCanvas.nodes.size) return false;
2653
+ for (const [id, extras] of left.jsonCanvas.nodes) {
2654
+ if (right.jsonCanvas.nodes.get(id) !== extras) return false;
2655
+ }
2560
2656
  if (left.grid.size !== right.grid.size || left.grid.majorEvery !== right.grid.majorEvery || left.grid.snap !== right.grid.snap || left.grid.edgeSnap !== right.grid.edgeSnap || left.grid.edgeSnapThreshold !== right.grid.edgeSnapThreshold || left.grid.pattern !== right.grid.pattern) {
2561
2657
  return false;
2562
2658
  }
@@ -2863,7 +2959,13 @@ function createBoardEngine(options = {}) {
2863
2959
  Array.from(state.selection.values()).filter((id) => state.nodes.has(id))
2864
2960
  );
2865
2961
  }
2866
- function restoreSnapshot(snapshot, mode) {
2962
+ function deleteNodePassthrough(id) {
2963
+ state.jsonCanvas = removeNodeExtras(state.jsonCanvas, id);
2964
+ }
2965
+ function copyNodePassthrough(idMap, source = state.jsonCanvas.nodes) {
2966
+ state.jsonCanvas = copyNodeExtras(state.jsonCanvas, idMap, source);
2967
+ }
2968
+ function restoreSnapshot(snapshot, mode, passthrough) {
2867
2969
  const snapshotNodes = materializeSnapshotNodes(snapshot);
2868
2970
  const idMap = /* @__PURE__ */ new Map();
2869
2971
  if (mode === "replace") {
@@ -2874,6 +2976,7 @@ function createBoardEngine(options = {}) {
2874
2976
  continue;
2875
2977
  }
2876
2978
  state.nodes.delete(id);
2979
+ deleteNodePassthrough(id);
2877
2980
  notifyNodeDeletedPlugins(prevNode.id);
2878
2981
  emit("node:deleted", id, materializeNode2(prevNode));
2879
2982
  }
@@ -2884,6 +2987,7 @@ function createBoardEngine(options = {}) {
2884
2987
  idMap.set(rawNode.id, node.id);
2885
2988
  emit("node:created", materializeNode2(node));
2886
2989
  }
2990
+ state.jsonCanvas = replaceJsonCanvasExtras(passthrough);
2887
2991
  state.selection = new Set(
2888
2992
  snapshot.selection.filter((id) => state.nodes.has(id))
2889
2993
  );
@@ -2904,6 +3008,11 @@ function createBoardEngine(options = {}) {
2904
3008
  state.nodes.set(id, { ...node, id, zIndex: state.nextZIndex++ });
2905
3009
  idMap.set(node.id, id);
2906
3010
  }
3011
+ state.jsonCanvas = mergeJsonCanvasExtras(
3012
+ state.jsonCanvas,
3013
+ passthrough,
3014
+ idMap
3015
+ );
2907
3016
  notifyNodesChanged();
2908
3017
  return idMap;
2909
3018
  }
@@ -2998,6 +3107,8 @@ function createBoardEngine(options = {}) {
2998
3107
  pluginStates.clear();
2999
3108
  pluginPersistence.clear();
3000
3109
  commitProjectors.clear();
3110
+ for (const projection of projectedSubscribables) projection.destroy();
3111
+ projectedSubscribables.clear();
3001
3112
  nodeDeletedHooks.clear();
3002
3113
  commandGuards.clear();
3003
3114
  eventBus.clear();
@@ -3011,7 +3122,7 @@ function createBoardEngine(options = {}) {
3011
3122
  },
3012
3123
  extend(key, value) {
3013
3124
  assertMutationAllowed();
3014
- plugins[key] = value;
3125
+ Reflect.set(plugins, key, value);
3015
3126
  },
3016
3127
  batch(fn) {
3017
3128
  assertCommandReady();
@@ -3120,6 +3231,27 @@ function createBoardEngine(options = {}) {
3120
3231
  commitProjectors.add(projector);
3121
3232
  return () => commitProjectors.delete(projector);
3122
3233
  },
3234
+ createCommitSubscribable(select, channel) {
3235
+ assertAlive();
3236
+ assertMutationAllowed();
3237
+ let selected = select();
3238
+ const projection = createSubscribable(
3239
+ selected,
3240
+ createBatchController(),
3241
+ (error) => reportUnhandledError(error, { source: "subscriber", channel })
3242
+ );
3243
+ const projector = () => {
3244
+ const next = select();
3245
+ if (Object.is(next, selected)) return () => void 0;
3246
+ return () => {
3247
+ selected = next;
3248
+ projection.set(next);
3249
+ };
3250
+ };
3251
+ commitProjectors.add(projector);
3252
+ projectedSubscribables.add(projection);
3253
+ return projection;
3254
+ },
3123
3255
  restoreHistoryRoot(root) {
3124
3256
  runCommand(
3125
3257
  "history:restore",
@@ -3132,6 +3264,10 @@ function createBoardEngine(options = {}) {
3132
3264
  Array.from(root.selection).filter((id) => state.nodes.has(id))
3133
3265
  );
3134
3266
  state.nextZIndex = root.nextZIndex;
3267
+ state.jsonCanvas = {
3268
+ document: root.jsonCanvas.document,
3269
+ nodes: new Map(root.jsonCanvas.nodes)
3270
+ };
3135
3271
  Object.assign(grid, root.grid);
3136
3272
  for (const [name, pluginState] of pluginStates) {
3137
3273
  if (root.pluginSlices.has(name)) {
@@ -3147,16 +3283,6 @@ function createBoardEngine(options = {}) {
3147
3283
  IGNORE_COMMAND
3148
3284
  );
3149
3285
  },
3150
- getPluginState() {
3151
- throw new Error(
3152
- "getPluginState is only available inside an internal plugin install() context."
3153
- );
3154
- },
3155
- updatePluginState(_update) {
3156
- throw new Error(
3157
- "updatePluginState is only available inside an internal plugin install() context."
3158
- );
3159
- },
3160
3286
  screenToWorld(point) {
3161
3287
  assertAlive();
3162
3288
  return screenToWorld(point, state.camera);
@@ -3344,6 +3470,7 @@ function createBoardEngine(options = {}) {
3344
3470
  continue;
3345
3471
  }
3346
3472
  state.nodes.delete(deleteId);
3473
+ deleteNodePassthrough(deleteId);
3347
3474
  notifyNodeDeletedPlugins(prevNode.id);
3348
3475
  emit("node:deleted", deleteId, materializeNode2(prevNode));
3349
3476
  }
@@ -3501,6 +3628,7 @@ function createBoardEngine(options = {}) {
3501
3628
  const forest = forestIdsFromSeeds(ids);
3502
3629
  const source = Array.from(forest).map((id) => state.nodes.get(id)).filter((node) => Boolean(node)).sort((a, b) => a.zIndex - b.zIndex);
3503
3630
  const duplicated = duplicateForest2(source, offset);
3631
+ copyNodePassthrough(duplicated.idMap);
3504
3632
  const created = duplicated.nodes;
3505
3633
  for (const node of created) {
3506
3634
  state.nodes.set(node.id, node);
@@ -3517,15 +3645,45 @@ function createBoardEngine(options = {}) {
3517
3645
  copySelected() {
3518
3646
  return runCommand("copySelected", [], () => {
3519
3647
  clipboard.length = 0;
3520
- for (const node of getCopyClosureNodes2()) {
3648
+ clipboardPassthrough.clear();
3649
+ const copied = getCopyClosureNodes2();
3650
+ for (const node of copied) {
3521
3651
  clipboard.push({ ...node });
3522
3652
  }
3653
+ for (const [id, extras] of selectNodeExtras(
3654
+ state.jsonCanvas.nodes,
3655
+ copied.map((node) => node.id)
3656
+ )) {
3657
+ clipboardPassthrough.set(id, extras);
3658
+ }
3523
3659
  return clipboard.map((node) => materializeNode2(node));
3524
3660
  });
3525
3661
  },
3526
3662
  pasteClipboard(offset = { x: grid.size, y: grid.size }) {
3527
3663
  return runCommand("pasteClipboard", [offset], () => {
3528
- const created = duplicateForest2(clipboard, offset).nodes;
3664
+ const duplicated = duplicateForest2(clipboard, offset);
3665
+ copyNodePassthrough(duplicated.idMap, clipboardPassthrough);
3666
+ const created = duplicated.nodes;
3667
+ for (const node of created) {
3668
+ state.nodes.set(node.id, node);
3669
+ emit("node:created", materializeNode2(node));
3670
+ }
3671
+ notifyNodesChanged();
3672
+ setSelection(created.map((node) => node.id));
3673
+ return created.map((node) => materializeNode2(node));
3674
+ });
3675
+ },
3676
+ pasteData(payload, offset = { x: grid.size, y: grid.size }) {
3677
+ return runCommand("pasteData", [payload, offset], () => {
3678
+ const inputs = options.clipboard?.deserialize(payload) ?? null;
3679
+ if (!inputs || inputs.length === 0) return null;
3680
+ const created = inputs.map(
3681
+ (input) => normalizeNode({
3682
+ ...input,
3683
+ x: input.x ?? offset.x,
3684
+ y: input.y ?? offset.y
3685
+ })
3686
+ );
3529
3687
  for (const node of created) {
3530
3688
  state.nodes.set(node.id, node);
3531
3689
  emit("node:created", materializeNode2(node));
@@ -3600,6 +3758,7 @@ function createBoardEngine(options = {}) {
3600
3758
  continue;
3601
3759
  }
3602
3760
  state.nodes.delete(deleteId);
3761
+ deleteNodePassthrough(deleteId);
3603
3762
  notifyNodeDeletedPlugins(prevNode.id);
3604
3763
  emit("node:deleted", deleteId, materializeNode2(prevNode));
3605
3764
  }
@@ -4038,7 +4197,8 @@ function createBoardEngine(options = {}) {
4038
4197
  );
4039
4198
  return toPersistedDocument(
4040
4199
  buildSnapshot(state, grid, buildPublicNodeMap(state)),
4041
- pluginDocuments
4200
+ pluginDocuments,
4201
+ state.jsonCanvas
4042
4202
  );
4043
4203
  },
4044
4204
  loadDocument(document, options2 = {}) {
@@ -4050,10 +4210,11 @@ function createBoardEngine(options = {}) {
4050
4210
  const normalized = normalizeDocumentForImport(document);
4051
4211
  assertCanRestoreDocument(normalized);
4052
4212
  const snapshot = documentToSnapshot(normalized);
4213
+ const passthrough = extractJsonCanvasPassthrough(normalized);
4053
4214
  nodeOverrides.clear();
4054
4215
  activeGestureHistoryRoot = null;
4055
4216
  activeBoxSelectionBefore = null;
4056
- const idMap = restoreSnapshot(snapshot, mode);
4217
+ const idMap = restoreSnapshot(snapshot, mode, passthrough);
4057
4218
  restorePluginDocuments(normalized, mode, idMap);
4058
4219
  },
4059
4220
  IGNORE_COMMAND
@@ -4075,29 +4236,7 @@ function createBoardEngine(options = {}) {
4075
4236
  );
4076
4237
  }
4077
4238
  validate("createBoardEngine");
4078
- const internalKeys = /* @__PURE__ */ new Set([
4079
- "emit",
4080
- "assertActive",
4081
- "isBatching",
4082
- "extend",
4083
- "runCommand",
4084
- "projectCommit",
4085
- "restoreHistoryRoot",
4086
- "getPluginState",
4087
- "updatePluginState",
4088
- "beginPan",
4089
- "beginNodeDrag",
4090
- "beginResize",
4091
- "beginBoxSelect",
4092
- "updatePointer",
4093
- "endInteraction",
4094
- "cancelInteraction",
4095
- "getUniformTranslationTargets",
4096
- "syncGroupZOrder"
4097
- ]);
4098
- const publicEngine = Object.fromEntries(
4099
- Object.entries(engine).filter(([key]) => !internalKeys.has(key))
4100
- );
4239
+ const publicEngine = createPublicEngine(engine);
4101
4240
  registerBoardInteractionAdapter(publicEngine, engine);
4102
4241
  return publicEngine;
4103
4242
  }