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.
- package/README.md +9 -4
- package/dist/components/Canvas/ConnectionPoints.d.ts +2 -0
- package/dist/components/Canvas/TextHtmlOverlay.d.ts +12 -0
- package/dist/components/shapes/TextLabel.d.ts +1 -1
- package/dist/components/shapes/TextShape.d.ts +1 -0
- package/dist/f1ow-collaboration.js +3804 -0
- package/dist/f1ow.js +4580 -3179
- package/dist/f1ow.umd.cjs +5874 -3946
- package/dist/hooks/useFlowAnimation.d.ts +8 -0
- package/dist/lib/FlowCanvasProps.d.ts +31 -1
- package/dist/lib/collaboration.d.ts +10 -0
- package/dist/lib/index.d.ts +7 -10
- package/dist/store/CanvasStoreContext.d.ts +14 -0
- package/dist/store/useCanvasStore.d.ts +32 -0
- package/dist/syncBridge-CveP4QyQ.js +428 -0
- package/dist/types/index.d.ts +97 -1
- package/dist/utils/connection.d.ts +30 -2
- package/dist/utils/editable.d.ts +2 -0
- package/dist/utils/elbow.d.ts +41 -8
- package/dist/utils/labelMetrics.d.ts +21 -0
- package/dist/utils/markdown.d.ts +14 -0
- package/dist/utils/markdownEditing.d.ts +1 -0
- package/dist/utils/textBinding.d.ts +18 -0
- package/dist/utils/textStyleTargets.d.ts +6 -0
- package/dist/yjsProvider-mWrSFiNG.js +100 -0
- package/package.json +121 -107
|
@@ -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';
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -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
|
+
};
|