f1ow 1.0.0 → 1.1.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.
@@ -0,0 +1,8 @@
1
+ import { default as Konva } from 'konva';
2
+ /**
3
+ * Animate dashOffset on a Konva node to create a "flowing" dash effect.
4
+ * @param nodeRef - React ref to a Konva Line or Shape node
5
+ * @param enabled - whether animation is active
6
+ * @param speed - pixels per second (default 40)
7
+ */
8
+ export declare function useFlowAnimation(nodeRef: React.RefObject<Konva.Line | Konva.Shape | null>, enabled: boolean, speed?: number): void;
@@ -1,8 +1,9 @@
1
- import { CanvasElement, ElementStyle, ToolType } from '../types';
1
+ import { CanvasElement, ElementStyle, ToolType, ConnectionConfig } from '../types';
2
2
  import { ContextMenuItem } from '../components/ContextMenu/ContextMenu';
3
3
  import { CollaborationConfig } from '../collaboration/types';
4
4
  import { CustomElementConfig } from '../utils/elementRegistry';
5
5
  import { RenderAnnotationFn } from '../components/Canvas/AnnotationsOverlay';
6
+ import { CanvasStore } from '../store/useCanvasStore';
6
7
  export type { ContextMenuItem };
7
8
  /** Context passed to custom context menu renderers */
8
9
  export interface ContextMenuContext {
@@ -122,6 +123,19 @@ export interface FlowCanvasProps {
122
123
  * Pass a `CollaborationConfig` to connect, or `undefined`/`null` to disable.
123
124
  */
124
125
  collaboration?: CollaborationConfig | null;
126
+ /**
127
+ * Optional canvas store instance produced by `createCanvasStore()`.
128
+ * When supplied, this `<FlowCanvas>` and its descendant React
129
+ * subscribers (Toolbar, StylePanel, overlays) read state from this
130
+ * isolated store instead of the module-level singleton, allowing
131
+ * multiple canvases to coexist on the same page without cross-talk.
132
+ *
133
+ * Note: tools, keyboard shortcuts, and the collaboration sync bridge
134
+ * still read from the singleton via `getState()`. Until that wiring
135
+ * is migrated, those subsystems target the singleton even when this
136
+ * prop is supplied.
137
+ */
138
+ store?: CanvasStore;
125
139
  /**
126
140
  * Register custom element types for this canvas instance.
127
141
  *
@@ -143,6 +157,22 @@ export interface FlowCanvasProps {
143
157
  * ```
144
158
  */
145
159
  customElementTypes?: CustomElementConfig[];
160
+ /**
161
+ * Configure the connection/binding system behavior.
162
+ * Controls snap thresholds, port visibility, default line styles, and more.
163
+ *
164
+ * @example
165
+ * ```tsx
166
+ * <FlowCanvas
167
+ * connectionConfig={{
168
+ * enablePorts: true,
169
+ * snapThreshold: 20,
170
+ * defaultLineType: 'elbow',
171
+ * }}
172
+ * />
173
+ * ```
174
+ */
175
+ connectionConfig?: ConnectionConfig;
146
176
  /**
147
177
  * Configure Web Workers for background processing (elbow routing, SVG export).
148
178
  *
@@ -0,0 +1,10 @@
1
+ export type { CollaborationUser, AwarenessState, CollaborationConfig, ConnectionStatus, CollaborationEvent, } from '../collaboration/types';
2
+ export { createCollaborationProvider, destroyCollaborationProvider, getYDoc, getYProvider, getYElements, isCollaborationActive, onStatusChange, updateAwareness, getRemoteAwareness, } from '../collaboration/yjsProvider';
3
+ export { startSync, stopSync } from '../collaboration/syncBridge';
4
+ export { elementToYMap, yMapToElement, SYNC_FIELDS, STYLE_FIELDS } from '../collaboration/syncBridgeCodec';
5
+ export { CollaborationManager } from '../collaboration/CollaborationManager';
6
+ export { SyncWorkerAdapter } from '../collaboration/syncWorker';
7
+ export type { WorkerInMessage, WorkerOutMessage, SyncWorkerCallbacks } from '../collaboration/syncWorker';
8
+ export { useCollaboration } from '../collaboration/useCollaboration';
9
+ export type { UseCollaborationReturn } from '../collaboration/useCollaboration';
10
+ export { default as CursorOverlay } from '../collaboration/CursorOverlay';
@@ -2,20 +2,23 @@ export { default as FlowCanvas } from './FlowCanvas';
2
2
  export type { FlowCanvasProps, FlowCanvasRef, FlowCanvasTheme, ContextMenuItem, ContextMenuContext, } from './FlowCanvasProps';
3
3
  export { DEFAULT_THEME } from './FlowCanvasProps';
4
4
  export type { AnnotationContext, AnnotationScreenBounds, RenderAnnotationFn, } from '../components/Canvas/AnnotationsOverlay';
5
- export type { CanvasElement, RectangleElement, EllipseElement, DiamondElement, LineElement, ArrowElement, FreeDrawElement, TextElement, ImageElement, BaseElement, ElementStyle, ElementType, ToolType, Point, ViewportState, ConnectionAnchor, BoundElement, Binding, SnapTarget, Arrowhead, LineType, TextAlign, VerticalAlign, ImageScaleMode, ImageCrop, ElementMeta, CanvasOperation, } from '../types';
6
- export { useCanvasStore } from '../store/useCanvasStore';
5
+ export type { CanvasElement, RectangleElement, EllipseElement, DiamondElement, LineElement, ArrowElement, FreeDrawElement, TextElement, ImageElement, BaseElement, ElementStyle, ElementType, ToolType, Point, ViewportState, ConnectionAnchor, BoundElement, Binding, SnapTarget, Arrowhead, LineType, TextAlign, VerticalAlign, ImageScaleMode, ImageCrop, ElementMeta, CanvasOperation, AnchorId, Port, SnapMode, LineGradient, LineTaper, LineStyleExtension, ConnectionConfig, } from '../types';
6
+ export { useCanvasStore, createCanvasStore } from '../store/useCanvasStore';
7
+ export type { CanvasStore } from '../store/useCanvasStore';
8
+ export { CanvasStoreProvider, useCanvasStoreInstance } from '../store/CanvasStoreContext';
9
+ export type { CanvasStoreProviderProps } from '../store/CanvasStoreContext';
7
10
  export { DEFAULT_STYLE, STROKE_COLORS, FILL_COLORS, STROKE_WIDTHS, TOOLS, ARROWHEAD_TYPES, LINE_TYPES, ROUGHNESS_CONFIGS } from '../constants';
8
11
  export { generateId } from '../utils/id';
9
12
  export { distance, normalizeRect, rotatePoint, isPointInRect, getDiamondPoints, getStrokeDash } from '../utils/geometry';
10
13
  export { exportToDataURL, downloadPNG, exportToJSON, downloadJSON, exportToSVG, downloadSVG } from '../utils/export';
11
14
  export { drawArrowhead, arrowheadSize, flatToPoints } from '../utils/arrowheads';
12
15
  export { computeCurveControlPoint, quadBezierAt, quadBezierTangent, curveArrowPrev, CURVE_RATIO } from '../utils/curve';
16
+ export { anchorToFixedPoint, fixedPointToAnchor, resolvePort, resolveBindingPoint, createBindingFromSnap, isBindingStale, findNearestSnapTarget, recomputeBoundPoints, computeFixedPoint, getEdgePointFromFixedPoint, isConnectable, getConnectionPoints, getEdgePoint, getAnchorPosition, findConnectorsForElement, addBoundElement, removeBoundElement, syncBoundElements, } from '../utils/connection';
13
17
  export { LABEL_PADDING_H, LABEL_PADDING_V, LABEL_CORNER, LABEL_LINE_HEIGHT, LABEL_MIN_WIDTH, measureLabelText, computePillSize } from '../utils/labelMetrics';
14
18
  export { elementRegistry, registerCustomElement } from '../utils/elementRegistry';
15
19
  export type { CustomElementConfig, ValidationResult } from '../utils/elementRegistry';
16
20
  export { computeElbowPoints, computeElbowRoute, simplifyElbowPath, clearElbowRouteCache, directionFromFixedPoint, directionFromPoints, directionFromShapeToPoint, directionFromEdgePoint, getElbowPreferredDirection, } from '../utils/elbow';
17
21
  export type { Direction } from '../utils/elbow';
18
- export { getConnectionPoints, getEdgePoint, getEdgePointFromFixedPoint, computeFixedPoint, getAnchorPosition, findNearestSnapTarget, isConnectable, recomputeBoundPoints, findConnectorsForElement, addBoundElement, removeBoundElement, syncBoundElements, } from '../utils/connection';
19
22
  export { fileToDataURL, loadImage, computeImageElementDimensions, createImageElement, getImageFilesFromDataTransfer, extractImageDataFromClipboard, clipboardHasImage, resolveImageSource, openImageFilePicker, } from '../utils/image';
20
23
  export { getVisibleBounds, getElementAABB, aabbOverlaps, cullToViewport, buildElementMap, cloneElementsForHistory, rafThrottle, batchElementUpdates, } from '../utils/performance';
21
24
  export type { AABB } from '../utils/performance';
@@ -41,19 +44,13 @@ export { generateKeyBetween, generateNKeysBetween, isValidFractionalIndex, compa
41
44
  export { OperationLog, opAdd, opDelete, opMove, opResize, opStyle, opRotate, opReorder, opUpdatePoints, opSetText, opBatch, applyOperation, detectOperations, } from '../utils/crdtPrep';
42
45
  export type { OperationEntry } from '../utils/crdtPrep';
43
46
  export type { CollaborationUser, AwarenessState, CollaborationConfig, ConnectionStatus, CollaborationEvent, } from '../collaboration/types';
44
- export { createCollaborationProvider, destroyCollaborationProvider, getYDoc, getYProvider, getYElements, isCollaborationActive, onStatusChange, updateAwareness, getRemoteAwareness, } from '../collaboration/yjsProvider';
45
- export { startSync, stopSync } from '../collaboration/syncBridge';
46
- export { elementToYMap, yMapToElement, SYNC_FIELDS, STYLE_FIELDS } from '../collaboration/syncBridgeCodec';
47
- export { CollaborationManager } from '../collaboration/CollaborationManager';
48
- export { SyncWorkerAdapter } from '../collaboration/syncWorker';
49
- export type { WorkerInMessage, WorkerOutMessage, SyncWorkerCallbacks } from '../collaboration/syncWorker';
50
47
  export { useCollaboration } from '../collaboration/useCollaboration';
51
48
  export type { UseCollaborationReturn } from '../collaboration/useCollaboration';
52
49
  export { default as CursorOverlay } from '../collaboration/CursorOverlay';
53
50
  export { TileCache, tileKey } from '../rendering/tileCache';
54
51
  export type { TileCoord } from '../rendering/tileCache';
55
52
  export { TileRenderer, TILE_SIZE, discreteZoom, worldTileSize, getVisibleTiles, tileBounds, getElementTiles, } from '../rendering/tileRenderer';
56
- export type { TileDrawFn, TileRendererOptions } from '../rendering/tileRenderer';
53
+ export type { TileDrawFn, TileRendererOptions, TileSpatialQuery } from '../rendering/tileRenderer';
57
54
  export { useTileRenderer } from '../rendering/useTileRenderer';
58
55
  export type { UseTileRendererOptions, UseTileRendererReturn } from '../rendering/useTileRenderer';
59
56
  export { TextureAtlas } from '../webgl/textureAtlas';
@@ -0,0 +1,14 @@
1
+ import { ReactNode } from 'react';
2
+ import { CanvasStore } from './useCanvasStore';
3
+ export interface CanvasStoreProviderProps {
4
+ /** Store instance produced by `createCanvasStore()`. */
5
+ store: CanvasStore;
6
+ children: ReactNode;
7
+ }
8
+ export declare function CanvasStoreProvider({ store, children }: CanvasStoreProviderProps): import("react/jsx-runtime").JSX.Element;
9
+ /**
10
+ * Resolve the canvas store for the current React subtree. Falls back to
11
+ * the singleton `useCanvasStore` when no provider is mounted, so existing
12
+ * single-instance apps work without changes.
13
+ */
14
+ export declare function useCanvasStoreInstance(): CanvasStore;
@@ -15,11 +15,17 @@ interface ElementDiff {
15
15
  */
16
16
  interface HistoryEntry {
17
17
  diffs: ElementDiff[];
18
+ /** Element order before this entry, used to restore z-order changes. */
19
+ beforeOrder?: string[];
20
+ /** Element order after this entry, used to redo z-order changes. */
21
+ afterOrder?: string[];
18
22
  /** Optional named mark/checkpoint for grouping */
19
23
  mark?: string;
20
24
  /** Timestamp for squash heuristics */
21
25
  timestamp: number;
22
26
  }
27
+ type AlignMode = 'left' | 'centerH' | 'right' | 'top' | 'centerV' | 'bottom';
28
+ type FlipAxis = 'horizontal' | 'vertical';
23
29
  interface CanvasState {
24
30
  elements: CanvasElement[];
25
31
  selectedIds: string[];
@@ -35,6 +41,8 @@ interface CanvasState {
35
41
  historyIndex: number;
36
42
  /** Baseline snapshot for computing diffs against current state */
37
43
  _historyBaseline: Map<string, CanvasElement>;
44
+ /** Baseline element order for z-order history. */
45
+ _historyOrderBaseline: string[];
38
46
  /** Whether history recording is temporarily paused */
39
47
  _historyPaused: boolean;
40
48
  showGrid: boolean;
@@ -55,6 +63,9 @@ interface CanvasState {
55
63
  sendToBack: (ids: string[]) => void;
56
64
  bringForward: (ids: string[]) => void;
57
65
  sendBackward: (ids: string[]) => void;
66
+ alignElements: (ids: string[], mode: AlignMode) => void;
67
+ rotateElements: (ids: string[], deltaDegrees: number) => void;
68
+ flipElements: (ids: string[], axis: FlipAxis) => void;
58
69
  toggleLockElements: (ids: string[]) => void;
59
70
  groupElements: (ids: string[]) => void;
60
71
  ungroupElements: (ids: string[]) => void;
@@ -123,5 +134,26 @@ interface CanvasState {
123
134
  canRedo: () => boolean;
124
135
  toggleGrid: () => void;
125
136
  }
137
+ /**
138
+ * Factory: create a fresh canvas store instance.
139
+ *
140
+ * Each call returns an independent Zustand store with its own elements,
141
+ * selection, viewport, and history. Use this with `CanvasStoreProvider`
142
+ * to render multiple `<FlowCanvas>` instances side-by-side without state
143
+ * cross-talk on the React subscriber side.
144
+ *
145
+ * Note: the module-level `useCanvasStore` singleton remains exported for
146
+ * backward compatibility and is still used by tools, hooks, and the
147
+ * collaboration sync bridge that read state via `getState()`. Full
148
+ * multi-instance isolation across those subsystems is a follow-up phase.
149
+ */
150
+ export type CanvasStore = ReturnType<typeof createCanvasStore>;
151
+ export declare function createCanvasStore(): import('zustand').UseBoundStore<import('zustand').StoreApi<CanvasState>>;
152
+ /**
153
+ * Default singleton store. Used by tools, hooks, and the collaboration sync
154
+ * bridge that read state via `getState()`. Most apps render a single
155
+ * `<FlowCanvas>` per page, in which case this singleton is what the
156
+ * `CanvasStoreProvider` exposes.
157
+ */
126
158
  export declare const useCanvasStore: import('zustand').UseBoundStore<import('zustand').StoreApi<CanvasState>>;
127
159
  export {};
@@ -0,0 +1,428 @@
1
+ import * as Y from "yjs";
2
+ import { useCanvasStore } from "./f1ow.js";
3
+ import { getYElements, getYDoc } from "./yjsProvider-mWrSFiNG.js";
4
+ const SYNC_FIELDS = [
5
+ "id",
6
+ "type",
7
+ "x",
8
+ "y",
9
+ "width",
10
+ "height",
11
+ "rotation",
12
+ "isLocked",
13
+ "isVisible",
14
+ "sortOrder",
15
+ "version"
16
+ ];
17
+ const STYLE_FIELDS = [
18
+ "strokeColor",
19
+ "fillColor",
20
+ "strokeWidth",
21
+ "opacity",
22
+ "strokeStyle",
23
+ "roughness",
24
+ "fontSize",
25
+ "fontFamily"
26
+ ];
27
+ function elementToYMap(el, yMap) {
28
+ const elRecord = el;
29
+ for (const field of SYNC_FIELDS) {
30
+ const value = elRecord[field];
31
+ if (value !== void 0) {
32
+ yMap.set(field, value);
33
+ }
34
+ }
35
+ if (el.style) {
36
+ for (const sf of STYLE_FIELDS) {
37
+ yMap.set(`style.${sf}`, el.style[sf]);
38
+ }
39
+ }
40
+ if (el.boundElements) {
41
+ yMap.set("boundElements", JSON.stringify(el.boundElements));
42
+ } else {
43
+ yMap.set("boundElements", null);
44
+ }
45
+ if (el.ports) {
46
+ yMap.set("ports", JSON.stringify(el.ports));
47
+ }
48
+ if ("lineStyle" in el && el.lineStyle) {
49
+ yMap.set("lineStyle", JSON.stringify(el.lineStyle));
50
+ }
51
+ if (el.ports) {
52
+ yMap.set("ports", JSON.stringify(el.ports));
53
+ }
54
+ if ("lineStyle" in el && el.lineStyle) {
55
+ yMap.set("lineStyle", JSON.stringify(el.lineStyle));
56
+ }
57
+ if (el.groupIds) {
58
+ yMap.set("groupIds", JSON.stringify(el.groupIds));
59
+ }
60
+ switch (el.type) {
61
+ case "rectangle":
62
+ yMap.set("cornerRadius", el.cornerRadius);
63
+ break;
64
+ case "line":
65
+ case "arrow":
66
+ yMap.set("points", JSON.stringify(el.points));
67
+ yMap.set("lineType", el.lineType);
68
+ if (el.curvature !== void 0) yMap.set("curvature", el.curvature);
69
+ yMap.set("startBinding", el.startBinding ? JSON.stringify(el.startBinding) : null);
70
+ yMap.set("endBinding", el.endBinding ? JSON.stringify(el.endBinding) : null);
71
+ if (el.type === "arrow") {
72
+ yMap.set("startArrowhead", el.startArrowhead);
73
+ yMap.set("endArrowhead", el.endArrowhead);
74
+ }
75
+ break;
76
+ case "freedraw":
77
+ yMap.set("points", JSON.stringify(el.points));
78
+ break;
79
+ case "text":
80
+ yMap.set("text", el.text);
81
+ yMap.set("containerId", el.containerId);
82
+ yMap.set("textAlign", el.textAlign);
83
+ yMap.set("verticalAlign", el.verticalAlign);
84
+ break;
85
+ case "image":
86
+ yMap.set("src", el.src);
87
+ yMap.set("naturalWidth", el.naturalWidth);
88
+ yMap.set("naturalHeight", el.naturalHeight);
89
+ yMap.set("scaleMode", el.scaleMode);
90
+ yMap.set("crop", el.crop ? JSON.stringify(el.crop) : null);
91
+ yMap.set("cornerRadius", el.cornerRadius);
92
+ yMap.set("alt", el.alt);
93
+ break;
94
+ }
95
+ }
96
+ function yMapToElement(yMap) {
97
+ const type = yMap.get("type");
98
+ const id = yMap.get("id");
99
+ if (!type || !id) return null;
100
+ const style = {};
101
+ for (const sf of STYLE_FIELDS) {
102
+ const val = yMap.get(`style.${sf}`);
103
+ if (val !== void 0) {
104
+ style[sf] = val;
105
+ }
106
+ }
107
+ const base = {
108
+ id,
109
+ type,
110
+ x: yMap.get("x") ?? 0,
111
+ y: yMap.get("y") ?? 0,
112
+ width: yMap.get("width") ?? 100,
113
+ height: yMap.get("height") ?? 100,
114
+ rotation: yMap.get("rotation") ?? 0,
115
+ isLocked: yMap.get("isLocked") ?? false,
116
+ isVisible: yMap.get("isVisible") ?? true,
117
+ version: yMap.get("version") ?? 0,
118
+ style,
119
+ boundElements: safeParseJSON(yMap.get("boundElements")) ?? null,
120
+ groupIds: safeParseJSON(yMap.get("groupIds")) ?? void 0,
121
+ sortOrder: yMap.get("sortOrder") ?? void 0,
122
+ ports: safeParseJSON(yMap.get("ports")) ?? void 0
123
+ };
124
+ switch (type) {
125
+ case "rectangle":
126
+ base.cornerRadius = yMap.get("cornerRadius") ?? 0;
127
+ break;
128
+ case "line":
129
+ case "arrow":
130
+ base.points = safeParseJSON(yMap.get("points")) ?? [0, 0, 100, 0];
131
+ base.lineType = yMap.get("lineType") ?? "sharp";
132
+ base.curvature = yMap.get("curvature") ?? void 0;
133
+ base.startBinding = safeParseJSON(yMap.get("startBinding"));
134
+ base.endBinding = safeParseJSON(yMap.get("endBinding"));
135
+ if (type === "arrow") {
136
+ base.startArrowhead = yMap.get("startArrowhead") ?? null;
137
+ base.endArrowhead = yMap.get("endArrowhead") ?? "arrow";
138
+ }
139
+ {
140
+ const ls = safeParseJSON(yMap.get("lineStyle"));
141
+ if (ls) base.lineStyle = ls;
142
+ }
143
+ break;
144
+ case "freedraw":
145
+ base.points = safeParseJSON(yMap.get("points")) ?? [];
146
+ break;
147
+ case "text":
148
+ base.text = yMap.get("text") ?? "";
149
+ base.containerId = yMap.get("containerId") ?? null;
150
+ base.textAlign = yMap.get("textAlign") ?? "center";
151
+ base.verticalAlign = yMap.get("verticalAlign") ?? "middle";
152
+ break;
153
+ case "image":
154
+ base.src = yMap.get("src") ?? "";
155
+ base.naturalWidth = yMap.get("naturalWidth") ?? 0;
156
+ base.naturalHeight = yMap.get("naturalHeight") ?? 0;
157
+ base.scaleMode = yMap.get("scaleMode") ?? "fit";
158
+ base.crop = safeParseJSON(yMap.get("crop")) ?? null;
159
+ base.cornerRadius = yMap.get("cornerRadius") ?? 0;
160
+ base.alt = yMap.get("alt") ?? "";
161
+ break;
162
+ }
163
+ return base;
164
+ }
165
+ function safeParseJSON(json) {
166
+ if (json == null) return null;
167
+ try {
168
+ return JSON.parse(json);
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ let _isApplyingRemote = false;
174
+ let _isApplyingLocal = false;
175
+ let _unsubscribe = null;
176
+ let _yObserverCleanup = null;
177
+ let _syncTimer = null;
178
+ let _lastElements = [];
179
+ function startSync(debounceMs = 50) {
180
+ const doc = getYDoc();
181
+ const yElements = getYElements();
182
+ if (!doc || !yElements) {
183
+ console.warn("[SyncBridge] Cannot start sync — no Yjs doc");
184
+ return;
185
+ }
186
+ stopSync();
187
+ if (yElements.size > 0) {
188
+ _isApplyingRemote = true;
189
+ const elements = yMapCollectionToElements(yElements);
190
+ useCanvasStore.getState().setElements(elements);
191
+ _lastElements = elements;
192
+ _isApplyingRemote = false;
193
+ } else {
194
+ const localElements = useCanvasStore.getState().elements;
195
+ if (localElements.length > 0) {
196
+ _isApplyingLocal = true;
197
+ doc.transact(() => {
198
+ for (const el of localElements) {
199
+ const yMap = new Y.Map();
200
+ elementToYMap(el, yMap);
201
+ yElements.set(el.id, yMap);
202
+ }
203
+ }, "local-init");
204
+ _isApplyingLocal = false;
205
+ }
206
+ _lastElements = localElements;
207
+ }
208
+ const yObserver = (events, transaction) => {
209
+ if (transaction.origin === "local-sync" || transaction.origin === "local-init") return;
210
+ if (_isApplyingLocal) return;
211
+ _isApplyingRemote = true;
212
+ const store = useCanvasStore.getState();
213
+ let elements = [..._lastElements];
214
+ let changed = false;
215
+ for (const [key, change] of events.keys) {
216
+ if (change.action === "add" || change.action === "update") {
217
+ const yMap = yElements.get(key);
218
+ if (yMap) {
219
+ const el = yMapToElement(yMap);
220
+ if (el) {
221
+ const idx = elements.findIndex((e) => e.id === key);
222
+ if (idx >= 0) {
223
+ elements[idx] = el;
224
+ } else {
225
+ elements.push(el);
226
+ }
227
+ changed = true;
228
+ }
229
+ }
230
+ } else if (change.action === "delete") {
231
+ elements = elements.filter((e) => e.id !== key);
232
+ changed = true;
233
+ }
234
+ }
235
+ if (changed) {
236
+ elements.sort((a, b) => {
237
+ if (a.sortOrder && b.sortOrder) {
238
+ return a.sortOrder < b.sortOrder ? -1 : a.sortOrder > b.sortOrder ? 1 : 0;
239
+ }
240
+ return 0;
241
+ });
242
+ store.setElements(elements);
243
+ _lastElements = elements;
244
+ }
245
+ _isApplyingRemote = false;
246
+ };
247
+ let _deepObserverTimer = null;
248
+ const _dirtyElementIds = /* @__PURE__ */ new Set();
249
+ const deepObserver = (events) => {
250
+ if (_isApplyingLocal) return;
251
+ for (const event of events) {
252
+ let target = event.target;
253
+ while (target && !(target instanceof Y.Map && target.parent === yElements)) {
254
+ target = target.parent;
255
+ }
256
+ if (target instanceof Y.Map) {
257
+ const id = target.get("id");
258
+ if (id) _dirtyElementIds.add(id);
259
+ }
260
+ }
261
+ if (_deepObserverTimer) clearTimeout(_deepObserverTimer);
262
+ _deepObserverTimer = setTimeout(() => {
263
+ if (_dirtyElementIds.size === 0 || _isApplyingLocal) return;
264
+ _isApplyingRemote = true;
265
+ let elements = [..._lastElements];
266
+ let changed = false;
267
+ for (const id of _dirtyElementIds) {
268
+ const yMap = yElements.get(id);
269
+ if (!yMap) continue;
270
+ const el = yMapToElement(yMap);
271
+ if (!el) continue;
272
+ const idx = elements.findIndex((e) => e.id === id);
273
+ if (idx >= 0) {
274
+ elements[idx] = el;
275
+ changed = true;
276
+ }
277
+ }
278
+ _dirtyElementIds.clear();
279
+ if (changed) {
280
+ useCanvasStore.getState().setElements(elements);
281
+ _lastElements = elements;
282
+ }
283
+ _isApplyingRemote = false;
284
+ }, 16);
285
+ };
286
+ yElements.observe(yObserver);
287
+ yElements.observeDeep(deepObserver);
288
+ _yObserverCleanup = () => {
289
+ yElements.unobserve(yObserver);
290
+ yElements.unobserveDeep(deepObserver);
291
+ if (_deepObserverTimer) clearTimeout(_deepObserverTimer);
292
+ _dirtyElementIds.clear();
293
+ };
294
+ _unsubscribe = useCanvasStore.subscribe(
295
+ (state) => {
296
+ if (_isApplyingRemote) return;
297
+ if (state.elements === _lastElements) return;
298
+ if (_syncTimer) clearTimeout(_syncTimer);
299
+ _syncTimer = setTimeout(() => {
300
+ syncLocalToYjs(state.elements, yElements, doc);
301
+ }, debounceMs);
302
+ }
303
+ );
304
+ }
305
+ function stopSync() {
306
+ if (_unsubscribe) {
307
+ _unsubscribe();
308
+ _unsubscribe = null;
309
+ }
310
+ if (_yObserverCleanup) {
311
+ _yObserverCleanup();
312
+ _yObserverCleanup = null;
313
+ }
314
+ if (_syncTimer) {
315
+ clearTimeout(_syncTimer);
316
+ _syncTimer = null;
317
+ }
318
+ _lastElements = [];
319
+ }
320
+ function syncLocalToYjs(elements, yElements, doc) {
321
+ _isApplyingLocal = true;
322
+ _lastElements = elements;
323
+ const localMap = /* @__PURE__ */ new Map();
324
+ for (const el of elements) {
325
+ localMap.set(el.id, el);
326
+ }
327
+ doc.transact(() => {
328
+ for (const [id] of yElements.entries()) {
329
+ if (!localMap.has(id)) {
330
+ yElements.delete(id);
331
+ }
332
+ }
333
+ for (const el of elements) {
334
+ let yMap = yElements.get(el.id);
335
+ if (!yMap) {
336
+ yMap = new Y.Map();
337
+ elementToYMap(el, yMap);
338
+ yElements.set(el.id, yMap);
339
+ } else {
340
+ updateYMapFromElement(el, yMap);
341
+ }
342
+ }
343
+ }, "local-sync");
344
+ _isApplyingLocal = false;
345
+ }
346
+ function updateYMapFromElement(el, yMap) {
347
+ const elRecord = el;
348
+ for (const field of SYNC_FIELDS) {
349
+ const value = elRecord[field];
350
+ if (value !== yMap.get(field)) {
351
+ yMap.set(field, value);
352
+ }
353
+ }
354
+ if (el.style) {
355
+ for (const sf of STYLE_FIELDS) {
356
+ const val = el.style[sf];
357
+ if (val !== yMap.get(`style.${sf}`)) {
358
+ yMap.set(`style.${sf}`, val);
359
+ }
360
+ }
361
+ }
362
+ const beJson = el.boundElements ? JSON.stringify(el.boundElements) : null;
363
+ if (beJson !== yMap.get("boundElements")) {
364
+ yMap.set("boundElements", beJson);
365
+ }
366
+ switch (el.type) {
367
+ case "rectangle":
368
+ if (el.cornerRadius !== yMap.get("cornerRadius")) {
369
+ yMap.set("cornerRadius", el.cornerRadius);
370
+ }
371
+ break;
372
+ case "line":
373
+ case "arrow": {
374
+ const ptsJson = JSON.stringify(el.points);
375
+ if (ptsJson !== yMap.get("points")) yMap.set("points", ptsJson);
376
+ if (el.lineType !== yMap.get("lineType")) yMap.set("lineType", el.lineType);
377
+ if (el.curvature !== yMap.get("curvature")) yMap.set("curvature", el.curvature);
378
+ const sbJson = el.startBinding ? JSON.stringify(el.startBinding) : null;
379
+ if (sbJson !== yMap.get("startBinding")) yMap.set("startBinding", sbJson);
380
+ const ebJson = el.endBinding ? JSON.stringify(el.endBinding) : null;
381
+ if (ebJson !== yMap.get("endBinding")) yMap.set("endBinding", ebJson);
382
+ if (el.type === "arrow") {
383
+ if (el.startArrowhead !== yMap.get("startArrowhead")) yMap.set("startArrowhead", el.startArrowhead);
384
+ if (el.endArrowhead !== yMap.get("endArrowhead")) yMap.set("endArrowhead", el.endArrowhead);
385
+ }
386
+ break;
387
+ }
388
+ case "freedraw": {
389
+ const fpJson = JSON.stringify(el.points);
390
+ if (fpJson !== yMap.get("points")) yMap.set("points", fpJson);
391
+ break;
392
+ }
393
+ case "text":
394
+ if (el.text !== yMap.get("text")) yMap.set("text", el.text);
395
+ if (el.containerId !== yMap.get("containerId")) yMap.set("containerId", el.containerId);
396
+ if (el.textAlign !== yMap.get("textAlign")) yMap.set("textAlign", el.textAlign);
397
+ if (el.verticalAlign !== yMap.get("verticalAlign")) yMap.set("verticalAlign", el.verticalAlign);
398
+ break;
399
+ case "image":
400
+ if (el.src !== yMap.get("src")) yMap.set("src", el.src);
401
+ if (el.naturalWidth !== yMap.get("naturalWidth")) yMap.set("naturalWidth", el.naturalWidth);
402
+ if (el.naturalHeight !== yMap.get("naturalHeight")) yMap.set("naturalHeight", el.naturalHeight);
403
+ if (el.scaleMode !== yMap.get("scaleMode")) yMap.set("scaleMode", el.scaleMode);
404
+ const cropJson = el.crop ? JSON.stringify(el.crop) : null;
405
+ if (cropJson !== yMap.get("crop")) yMap.set("crop", cropJson);
406
+ if (el.cornerRadius !== yMap.get("cornerRadius")) yMap.set("cornerRadius", el.cornerRadius);
407
+ if (el.alt !== yMap.get("alt")) yMap.set("alt", el.alt);
408
+ break;
409
+ }
410
+ }
411
+ function yMapCollectionToElements(yElements) {
412
+ const elements = [];
413
+ for (const [, yMap] of yElements.entries()) {
414
+ const el = yMapToElement(yMap);
415
+ if (el) elements.push(el);
416
+ }
417
+ elements.sort((a, b) => {
418
+ if (a.sortOrder && b.sortOrder) {
419
+ return a.sortOrder < b.sortOrder ? -1 : a.sortOrder > b.sortOrder ? 1 : 0;
420
+ }
421
+ return 0;
422
+ });
423
+ return elements;
424
+ }
425
+ export {
426
+ startSync,
427
+ stopSync
428
+ };