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,3804 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import * as Y from "yjs";
|
|
5
|
+
import { WebsocketProvider } from "y-websocket";
|
|
6
|
+
import { create } from "zustand";
|
|
7
|
+
import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
|
8
|
+
import { jsx, Fragment, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { Group, Rect, Line, Text } from "react-konva";
|
|
10
|
+
let _doc = null;
|
|
11
|
+
let _provider = null;
|
|
12
|
+
let _config = null;
|
|
13
|
+
let _statusListeners = /* @__PURE__ */ new Set();
|
|
14
|
+
function createCollaborationProvider(config) {
|
|
15
|
+
if (_provider) {
|
|
16
|
+
destroyCollaborationProvider();
|
|
17
|
+
}
|
|
18
|
+
_config = config;
|
|
19
|
+
_doc = new Y.Doc();
|
|
20
|
+
_provider = new WebsocketProvider(
|
|
21
|
+
config.serverUrl,
|
|
22
|
+
config.roomName,
|
|
23
|
+
_doc,
|
|
24
|
+
{
|
|
25
|
+
connect: true,
|
|
26
|
+
params: config.authToken ? { token: config.authToken } : void 0
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
_provider.awareness.setLocalState({
|
|
30
|
+
user: config.user,
|
|
31
|
+
cursor: null,
|
|
32
|
+
selectedIds: []
|
|
33
|
+
});
|
|
34
|
+
_provider.on("status", (event) => {
|
|
35
|
+
const status = event.status;
|
|
36
|
+
for (const listener of _statusListeners) {
|
|
37
|
+
listener(status);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return { doc: _doc, provider: _provider };
|
|
41
|
+
}
|
|
42
|
+
function destroyCollaborationProvider() {
|
|
43
|
+
if (_provider) {
|
|
44
|
+
_provider.awareness.setLocalState(null);
|
|
45
|
+
_provider.disconnect();
|
|
46
|
+
_provider.destroy();
|
|
47
|
+
_provider = null;
|
|
48
|
+
}
|
|
49
|
+
if (_doc) {
|
|
50
|
+
_doc.destroy();
|
|
51
|
+
_doc = null;
|
|
52
|
+
}
|
|
53
|
+
_config = null;
|
|
54
|
+
}
|
|
55
|
+
function getYDoc() {
|
|
56
|
+
return _doc;
|
|
57
|
+
}
|
|
58
|
+
function getYProvider() {
|
|
59
|
+
return _provider;
|
|
60
|
+
}
|
|
61
|
+
function getYElements() {
|
|
62
|
+
return _doc == null ? void 0 : _doc.getMap("elements");
|
|
63
|
+
}
|
|
64
|
+
function getCollaborationConfig() {
|
|
65
|
+
return _config;
|
|
66
|
+
}
|
|
67
|
+
function isCollaborationActive() {
|
|
68
|
+
return _provider !== null && _provider.wsconnected;
|
|
69
|
+
}
|
|
70
|
+
function onStatusChange(listener) {
|
|
71
|
+
_statusListeners.add(listener);
|
|
72
|
+
return () => {
|
|
73
|
+
_statusListeners.delete(listener);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function updateAwareness(update) {
|
|
77
|
+
if (!_provider) return;
|
|
78
|
+
const current = _provider.awareness.getLocalState();
|
|
79
|
+
_provider.awareness.setLocalState({
|
|
80
|
+
...current,
|
|
81
|
+
...update
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function getRemoteAwareness() {
|
|
85
|
+
if (!_provider) return /* @__PURE__ */ new Map();
|
|
86
|
+
const all = _provider.awareness.getStates();
|
|
87
|
+
const localId = _provider.awareness.clientID;
|
|
88
|
+
const remote = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const [clientId, state] of all) {
|
|
90
|
+
if (clientId !== localId && state && state.user) {
|
|
91
|
+
remote.set(clientId, state);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return remote;
|
|
95
|
+
}
|
|
96
|
+
const yjsProvider = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
97
|
+
__proto__: null,
|
|
98
|
+
createCollaborationProvider,
|
|
99
|
+
destroyCollaborationProvider,
|
|
100
|
+
getCollaborationConfig,
|
|
101
|
+
getRemoteAwareness,
|
|
102
|
+
getYDoc,
|
|
103
|
+
getYElements,
|
|
104
|
+
getYProvider,
|
|
105
|
+
isCollaborationActive,
|
|
106
|
+
onStatusChange,
|
|
107
|
+
updateAwareness
|
|
108
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
109
|
+
const DEFAULT_STYLE = {
|
|
110
|
+
strokeColor: "#1e1e1e",
|
|
111
|
+
fillColor: "transparent",
|
|
112
|
+
strokeWidth: 2,
|
|
113
|
+
opacity: 1,
|
|
114
|
+
strokeStyle: "solid",
|
|
115
|
+
roughness: 0,
|
|
116
|
+
fontSize: 20,
|
|
117
|
+
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
|
118
|
+
freehandStyle: "standard"
|
|
119
|
+
};
|
|
120
|
+
const MIN_ZOOM = 0.1;
|
|
121
|
+
const MAX_ZOOM = 5;
|
|
122
|
+
const SHAPE_MARGIN = 20;
|
|
123
|
+
const EXIT_FACE_MARGIN = 25;
|
|
124
|
+
const MIN_STUB_LENGTH = 36;
|
|
125
|
+
const BOUNDS_MARGIN = 40;
|
|
126
|
+
const BEND_PENALTY = 1e4;
|
|
127
|
+
const AMBIGUOUS_AXIS_DELTA = 24;
|
|
128
|
+
const AMBIGUOUS_RATIO = 1.5;
|
|
129
|
+
function dirVec(dir) {
|
|
130
|
+
switch (dir) {
|
|
131
|
+
case "up":
|
|
132
|
+
return { x: 0, y: -1 };
|
|
133
|
+
case "down":
|
|
134
|
+
return { x: 0, y: 1 };
|
|
135
|
+
case "left":
|
|
136
|
+
return { x: -1, y: 0 };
|
|
137
|
+
case "right":
|
|
138
|
+
return { x: 1, y: 0 };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function isVerticalDir(dir) {
|
|
142
|
+
return dir === "up" || dir === "down";
|
|
143
|
+
}
|
|
144
|
+
function horizontalDir(dx) {
|
|
145
|
+
return dx >= 0 ? "right" : "left";
|
|
146
|
+
}
|
|
147
|
+
function verticalDir(dy) {
|
|
148
|
+
return dy >= 0 ? "down" : "up";
|
|
149
|
+
}
|
|
150
|
+
function alternateAxisDir(primary, dx, dy) {
|
|
151
|
+
return isVerticalDir(primary) ? horizontalDir(dx) : verticalDir(dy);
|
|
152
|
+
}
|
|
153
|
+
function getElbowDirectionPreference(shape, targetPoint, targetShape, targetBinding) {
|
|
154
|
+
const cx = shape.x + shape.width / 2;
|
|
155
|
+
const cy = shape.y + shape.height / 2;
|
|
156
|
+
const dx = targetPoint.x - cx;
|
|
157
|
+
const dy = targetPoint.y - cy;
|
|
158
|
+
const hDir = horizontalDir(dx);
|
|
159
|
+
const vDir = verticalDir(dy);
|
|
160
|
+
if (targetShape) {
|
|
161
|
+
const shapeRight = shape.x + shape.width;
|
|
162
|
+
const shapeBottom = shape.y + shape.height;
|
|
163
|
+
const targetRight = targetShape.x + targetShape.width;
|
|
164
|
+
const targetBottom = targetShape.y + targetShape.height;
|
|
165
|
+
const gapX = shapeRight < targetShape.x ? targetShape.x - shapeRight : targetRight < shape.x ? shape.x - targetRight : 0;
|
|
166
|
+
const gapY = shapeBottom < targetShape.y ? targetShape.y - shapeBottom : targetBottom < shape.y ? shape.y - targetBottom : 0;
|
|
167
|
+
const targetBindingHasAxis = Boolean(
|
|
168
|
+
(targetBinding == null ? void 0 : targetBinding.isPrecise) && !(targetBinding.fixedPoint[0] === 0.5 && targetBinding.fixedPoint[1] === 0.5)
|
|
169
|
+
);
|
|
170
|
+
if (targetBindingHasAxis && gapX > 0 && gapY > 0) {
|
|
171
|
+
const targetDir = directionFromFixedPoint(targetBinding.fixedPoint);
|
|
172
|
+
return targetDir === "left" || targetDir === "right" ? { primary: hDir } : { primary: vDir };
|
|
173
|
+
}
|
|
174
|
+
if (gapX > 0 && gapY === 0) {
|
|
175
|
+
return { primary: hDir };
|
|
176
|
+
}
|
|
177
|
+
if (gapY > 0 && gapX === 0) {
|
|
178
|
+
return { primary: vDir };
|
|
179
|
+
}
|
|
180
|
+
if (gapX > 0 && gapY > 0) {
|
|
181
|
+
const primary = gapX < gapY ? hDir : vDir;
|
|
182
|
+
const alternate = alternateAxisDir(primary, dx, dy);
|
|
183
|
+
return Math.abs(gapX - gapY) <= AMBIGUOUS_AXIS_DELTA ? { primary, alternate } : { primary };
|
|
184
|
+
}
|
|
185
|
+
const overlapX = Math.max(0, Math.min(shapeRight, targetRight) - Math.max(shape.x, targetShape.x));
|
|
186
|
+
const overlapY = Math.max(0, Math.min(shapeBottom, targetBottom) - Math.max(shape.y, targetShape.y));
|
|
187
|
+
if (overlapX > overlapY) {
|
|
188
|
+
return Math.abs(overlapX - overlapY) <= AMBIGUOUS_AXIS_DELTA ? { primary: vDir, alternate: hDir } : { primary: vDir };
|
|
189
|
+
}
|
|
190
|
+
if (overlapY > overlapX) {
|
|
191
|
+
return Math.abs(overlapY - overlapX) <= AMBIGUOUS_AXIS_DELTA ? { primary: hDir, alternate: vDir } : { primary: hDir };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const hw = (shape.width || 1) / 2;
|
|
195
|
+
const hh = (shape.height || 1) / 2;
|
|
196
|
+
const normDx = Math.abs(dx) / hw;
|
|
197
|
+
const normDy = Math.abs(dy) / hh;
|
|
198
|
+
if (normDx > normDy * 3) {
|
|
199
|
+
return { primary: hDir };
|
|
200
|
+
}
|
|
201
|
+
if (normDy > normDx * 3) {
|
|
202
|
+
return { primary: vDir };
|
|
203
|
+
}
|
|
204
|
+
const dominant = Math.max(normDx, normDy) / Math.max(1e-6, Math.min(normDx, normDy));
|
|
205
|
+
return dominant <= AMBIGUOUS_RATIO ? { primary: vDir, alternate: hDir } : { primary: vDir };
|
|
206
|
+
}
|
|
207
|
+
function directionFromFixedPoint(fp) {
|
|
208
|
+
const [fx, fy] = fp;
|
|
209
|
+
const dTop = fy;
|
|
210
|
+
const dBottom = 1 - fy;
|
|
211
|
+
const dLeft = fx;
|
|
212
|
+
const dRight = 1 - fx;
|
|
213
|
+
const min = Math.min(dTop, dBottom, dLeft, dRight);
|
|
214
|
+
if (min === dTop) return "up";
|
|
215
|
+
if (min === dBottom) return "down";
|
|
216
|
+
if (min === dLeft) return "left";
|
|
217
|
+
return "right";
|
|
218
|
+
}
|
|
219
|
+
function directionFromPoints(from, to) {
|
|
220
|
+
const dx = to.x - from.x;
|
|
221
|
+
const dy = to.y - from.y;
|
|
222
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
223
|
+
return dx >= 0 ? "right" : "left";
|
|
224
|
+
}
|
|
225
|
+
return dy >= 0 ? "down" : "up";
|
|
226
|
+
}
|
|
227
|
+
function directionFromShapeToPoint(shape, targetPoint) {
|
|
228
|
+
const cx = shape.x + shape.width / 2;
|
|
229
|
+
const cy = shape.y + shape.height / 2;
|
|
230
|
+
const dx = targetPoint.x - cx;
|
|
231
|
+
const dy = targetPoint.y - cy;
|
|
232
|
+
const hw = shape.width / 2 || 1;
|
|
233
|
+
const hh = shape.height / 2 || 1;
|
|
234
|
+
const normDx = dx / hw;
|
|
235
|
+
const normDy = dy / hh;
|
|
236
|
+
if (Math.abs(normDx) >= Math.abs(normDy)) {
|
|
237
|
+
return dx >= 0 ? "right" : "left";
|
|
238
|
+
}
|
|
239
|
+
return dy >= 0 ? "down" : "up";
|
|
240
|
+
}
|
|
241
|
+
function rectRight(r) {
|
|
242
|
+
return r.left + r.width;
|
|
243
|
+
}
|
|
244
|
+
function rectBottom(r) {
|
|
245
|
+
return r.top + r.height;
|
|
246
|
+
}
|
|
247
|
+
function rectContains(r, p) {
|
|
248
|
+
return p.x > r.left && p.x < rectRight(r) && p.y > r.top && p.y < rectBottom(r);
|
|
249
|
+
}
|
|
250
|
+
function rectInflate(r, h, v) {
|
|
251
|
+
return {
|
|
252
|
+
left: r.left - h,
|
|
253
|
+
top: r.top - v,
|
|
254
|
+
width: r.width + h * 2,
|
|
255
|
+
height: r.height + v * 2
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function inflateExcludingFace(r, margin, face) {
|
|
259
|
+
const exitM = Math.min(margin, EXIT_FACE_MARGIN);
|
|
260
|
+
const l = face === "left" ? exitM : margin;
|
|
261
|
+
const ri = face === "right" ? exitM : margin;
|
|
262
|
+
const t = face === "up" ? exitM : margin;
|
|
263
|
+
const b = face === "down" ? exitM : margin;
|
|
264
|
+
return {
|
|
265
|
+
left: r.left - l,
|
|
266
|
+
top: r.top - t,
|
|
267
|
+
width: r.width + l + ri,
|
|
268
|
+
height: r.height + t + b
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function inflateExcludingFaces(r, margin, faces) {
|
|
272
|
+
const faceSet = new Set(faces);
|
|
273
|
+
const exitM = Math.min(margin, EXIT_FACE_MARGIN);
|
|
274
|
+
const l = faceSet.has("left") ? exitM : margin;
|
|
275
|
+
const ri = faceSet.has("right") ? exitM : margin;
|
|
276
|
+
const t = faceSet.has("up") ? exitM : margin;
|
|
277
|
+
const b = faceSet.has("down") ? exitM : margin;
|
|
278
|
+
return {
|
|
279
|
+
left: r.left - l,
|
|
280
|
+
top: r.top - t,
|
|
281
|
+
width: r.width + l + ri,
|
|
282
|
+
height: r.height + t + b
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function rectUnion(a, b) {
|
|
286
|
+
const left = Math.min(a.left, b.left);
|
|
287
|
+
const top = Math.min(a.top, b.top);
|
|
288
|
+
const right = Math.max(rectRight(a), rectRight(b));
|
|
289
|
+
const bottom = Math.max(rectBottom(a), rectBottom(b));
|
|
290
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
291
|
+
}
|
|
292
|
+
function bboxToRect(b) {
|
|
293
|
+
return { left: b.x, top: b.y, width: b.width, height: b.height };
|
|
294
|
+
}
|
|
295
|
+
function getShapeBBox(el) {
|
|
296
|
+
const rotation = el.rotation || 0;
|
|
297
|
+
if (rotation === 0) {
|
|
298
|
+
return { x: el.x, y: el.y, width: el.width, height: el.height };
|
|
299
|
+
}
|
|
300
|
+
const cx = el.x + el.width / 2;
|
|
301
|
+
const cy = el.y + el.height / 2;
|
|
302
|
+
const hw = el.width / 2;
|
|
303
|
+
const hh = el.height / 2;
|
|
304
|
+
const rad = rotation * Math.PI / 180;
|
|
305
|
+
const cos = Math.cos(rad);
|
|
306
|
+
const sin = Math.sin(rad);
|
|
307
|
+
const corners = [
|
|
308
|
+
{ x: -hw, y: -hh },
|
|
309
|
+
{ x: hw, y: -hh },
|
|
310
|
+
{ x: hw, y: hh },
|
|
311
|
+
{ x: -hw, y: hh }
|
|
312
|
+
].map((c) => ({
|
|
313
|
+
x: cx + c.x * cos - c.y * sin,
|
|
314
|
+
y: cy + c.x * sin + c.y * cos
|
|
315
|
+
}));
|
|
316
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
317
|
+
for (const c of corners) {
|
|
318
|
+
if (c.x < minX) minX = c.x;
|
|
319
|
+
if (c.y < minY) minY = c.y;
|
|
320
|
+
if (c.x > maxX) maxX = c.x;
|
|
321
|
+
if (c.y > maxY) maxY = c.y;
|
|
322
|
+
}
|
|
323
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
324
|
+
}
|
|
325
|
+
function oppositeDir(d) {
|
|
326
|
+
switch (d) {
|
|
327
|
+
case "left":
|
|
328
|
+
return "right";
|
|
329
|
+
case "right":
|
|
330
|
+
return "left";
|
|
331
|
+
case "up":
|
|
332
|
+
return "down";
|
|
333
|
+
case "down":
|
|
334
|
+
return "up";
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function getMoveDir(from, to) {
|
|
338
|
+
if (to.x < from.x) return "left";
|
|
339
|
+
if (to.x > from.x) return "right";
|
|
340
|
+
if (to.y < from.y) return "up";
|
|
341
|
+
if (to.y > from.y) return "down";
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
function isBend(a, b) {
|
|
345
|
+
if (a === null || b === null) return false;
|
|
346
|
+
const axisA = a === "left" || a === "right" ? "h" : "v";
|
|
347
|
+
const axisB = b === "left" || b === "right" ? "h" : "v";
|
|
348
|
+
return axisA !== axisB;
|
|
349
|
+
}
|
|
350
|
+
class PathNode {
|
|
351
|
+
constructor(pt) {
|
|
352
|
+
__publicField(this, "adjacent", /* @__PURE__ */ new Map());
|
|
353
|
+
this.pt = pt;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
class PathGraph {
|
|
357
|
+
constructor() {
|
|
358
|
+
__publicField(this, "idx", {});
|
|
359
|
+
}
|
|
360
|
+
add(p) {
|
|
361
|
+
const xs = String(p.x), ys = String(p.y);
|
|
362
|
+
if (!(xs in this.idx)) this.idx[xs] = {};
|
|
363
|
+
if (!(ys in this.idx[xs])) this.idx[xs][ys] = new PathNode(p);
|
|
364
|
+
}
|
|
365
|
+
get(p) {
|
|
366
|
+
var _a;
|
|
367
|
+
const xs = String(p.x), ys = String(p.y);
|
|
368
|
+
return ((_a = this.idx[xs]) == null ? void 0 : _a[ys]) ?? null;
|
|
369
|
+
}
|
|
370
|
+
has(p) {
|
|
371
|
+
return this.get(p) !== null;
|
|
372
|
+
}
|
|
373
|
+
/** Create a bidirectional edge between two points */
|
|
374
|
+
connect(a, b) {
|
|
375
|
+
const na = this.get(a), nb = this.get(b);
|
|
376
|
+
if (!na || !nb) return;
|
|
377
|
+
const d = Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
|
|
378
|
+
na.adjacent.set(nb, d);
|
|
379
|
+
nb.adjacent.set(na, d);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
class MinHeap {
|
|
383
|
+
constructor() {
|
|
384
|
+
__publicField(this, "data", []);
|
|
385
|
+
}
|
|
386
|
+
get size() {
|
|
387
|
+
return this.data.length;
|
|
388
|
+
}
|
|
389
|
+
push(item, priority) {
|
|
390
|
+
this.data.push({ item, priority });
|
|
391
|
+
this.bubbleUp(this.data.length - 1);
|
|
392
|
+
}
|
|
393
|
+
pop() {
|
|
394
|
+
if (this.data.length === 0) return void 0;
|
|
395
|
+
const top = this.data[0];
|
|
396
|
+
const end = this.data.pop();
|
|
397
|
+
if (this.data.length > 0) {
|
|
398
|
+
this.data[0] = end;
|
|
399
|
+
this.sinkDown(0);
|
|
400
|
+
}
|
|
401
|
+
return top.item;
|
|
402
|
+
}
|
|
403
|
+
bubbleUp(i) {
|
|
404
|
+
while (i > 0) {
|
|
405
|
+
const parent = i - 1 >> 1;
|
|
406
|
+
if (this.data[parent].priority <= this.data[i].priority) break;
|
|
407
|
+
[this.data[parent], this.data[i]] = [this.data[i], this.data[parent]];
|
|
408
|
+
i = parent;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
sinkDown(i) {
|
|
412
|
+
const len = this.data.length;
|
|
413
|
+
while (true) {
|
|
414
|
+
let s = i;
|
|
415
|
+
const l = 2 * i + 1, r = 2 * i + 2;
|
|
416
|
+
if (l < len && this.data[l].priority < this.data[s].priority) s = l;
|
|
417
|
+
if (r < len && this.data[r].priority < this.data[s].priority) s = r;
|
|
418
|
+
if (s === i) break;
|
|
419
|
+
[this.data[s], this.data[i]] = [this.data[i], this.data[s]];
|
|
420
|
+
i = s;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function astarSearch(graph, origin, destination) {
|
|
425
|
+
const srcNode = graph.get(origin);
|
|
426
|
+
const destNode = graph.get(destination);
|
|
427
|
+
if (!srcNode || !destNode) return null;
|
|
428
|
+
const h = (p) => Math.abs(p.x - destination.x) + Math.abs(p.y - destination.y);
|
|
429
|
+
const skey = (node, dir) => `${node.pt.x},${node.pt.y},${dir ?? "n"}`;
|
|
430
|
+
const bestG = /* @__PURE__ */ new Map();
|
|
431
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
432
|
+
const open = new MinHeap();
|
|
433
|
+
const startState = { node: srcNode, dir: null, g: 0 };
|
|
434
|
+
const startKey = skey(srcNode, null);
|
|
435
|
+
bestG.set(startKey, 0);
|
|
436
|
+
parentOf.set(startKey, null);
|
|
437
|
+
open.push(startState, h(srcNode.pt));
|
|
438
|
+
while (open.size > 0) {
|
|
439
|
+
const cur = open.pop();
|
|
440
|
+
const curKey = skey(cur.node, cur.dir);
|
|
441
|
+
if (cur.g > (bestG.get(curKey) ?? Infinity)) continue;
|
|
442
|
+
if (cur.node === destNode) {
|
|
443
|
+
const path = [];
|
|
444
|
+
let state = cur;
|
|
445
|
+
while (state) {
|
|
446
|
+
path.push(state.node.pt);
|
|
447
|
+
const pk = skey(state.node, state.dir);
|
|
448
|
+
state = parentOf.get(pk);
|
|
449
|
+
if (state === null || state === void 0) break;
|
|
450
|
+
}
|
|
451
|
+
if (path[path.length - 1] !== srcNode.pt) {
|
|
452
|
+
path.push(srcNode.pt);
|
|
453
|
+
}
|
|
454
|
+
path.reverse();
|
|
455
|
+
return path;
|
|
456
|
+
}
|
|
457
|
+
for (const [adj, weight] of cur.node.adjacent) {
|
|
458
|
+
const dir = getMoveDir(cur.node.pt, adj.pt);
|
|
459
|
+
if (dir === null) continue;
|
|
460
|
+
const isBackward = cur.dir !== null && dir === oppositeDir(cur.dir);
|
|
461
|
+
const turning = isBend(cur.dir, dir);
|
|
462
|
+
const penalty = isBackward ? BEND_PENALTY * 3 : turning ? BEND_PENALTY : 0;
|
|
463
|
+
const newG = cur.g + weight + penalty;
|
|
464
|
+
const adjKey = skey(adj, dir);
|
|
465
|
+
if (newG < (bestG.get(adjKey) ?? Infinity)) {
|
|
466
|
+
bestG.set(adjKey, newG);
|
|
467
|
+
parentOf.set(adjKey, cur);
|
|
468
|
+
open.push(
|
|
469
|
+
{ node: adj, dir, g: newG },
|
|
470
|
+
newG + h(adj.pt)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
function deduplicatePoints(points) {
|
|
478
|
+
const map = /* @__PURE__ */ new Map();
|
|
479
|
+
const result = [];
|
|
480
|
+
for (const p of points) {
|
|
481
|
+
let ys = map.get(p.x);
|
|
482
|
+
if (!ys) {
|
|
483
|
+
ys = /* @__PURE__ */ new Set();
|
|
484
|
+
map.set(p.x, ys);
|
|
485
|
+
}
|
|
486
|
+
if (!ys.has(p.y)) {
|
|
487
|
+
ys.add(p.y);
|
|
488
|
+
result.push(p);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
function generateGridSpots(verticals, horizontals, bounds, obstacles) {
|
|
494
|
+
const bL = bounds.left, bR = rectRight(bounds), bT = bounds.top, bB = rectBottom(bounds);
|
|
495
|
+
const allXs = [.../* @__PURE__ */ new Set([bL, ...verticals.filter((v) => v >= bL && v <= bR), bR])].sort((a, b) => a - b);
|
|
496
|
+
const allYs = [.../* @__PURE__ */ new Set([bT, ...horizontals.filter((h) => h >= bT && h <= bB), bB])].sort((a, b) => a - b);
|
|
497
|
+
const insideObstacle = (p) => obstacles.some((o) => rectContains(o, p));
|
|
498
|
+
const points = [];
|
|
499
|
+
for (const y of allYs) {
|
|
500
|
+
for (const x of allXs) {
|
|
501
|
+
const p = { x, y };
|
|
502
|
+
if (!insideObstacle(p)) {
|
|
503
|
+
points.push(p);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return points;
|
|
508
|
+
}
|
|
509
|
+
function segmentCrossesObstacle(a, b, obstacles) {
|
|
510
|
+
if (a.y === b.y) {
|
|
511
|
+
const y = a.y;
|
|
512
|
+
const x1 = Math.min(a.x, b.x);
|
|
513
|
+
const x2 = Math.max(a.x, b.x);
|
|
514
|
+
for (const obs of obstacles) {
|
|
515
|
+
if (obs.top < y && y < rectBottom(obs) && obs.left < x2 && rectRight(obs) > x1) {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} else if (a.x === b.x) {
|
|
520
|
+
const x = a.x;
|
|
521
|
+
const y1 = Math.min(a.y, b.y);
|
|
522
|
+
const y2 = Math.max(a.y, b.y);
|
|
523
|
+
for (const obs of obstacles) {
|
|
524
|
+
if (obs.left < x && x < rectRight(obs) && obs.top < y2 && rectBottom(obs) > y1) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
function buildGraph(spots, obstacles) {
|
|
532
|
+
const graph = new PathGraph();
|
|
533
|
+
const xSet = /* @__PURE__ */ new Set();
|
|
534
|
+
const ySet = /* @__PURE__ */ new Set();
|
|
535
|
+
for (const p of spots) {
|
|
536
|
+
graph.add(p);
|
|
537
|
+
xSet.add(p.x);
|
|
538
|
+
ySet.add(p.y);
|
|
539
|
+
}
|
|
540
|
+
const hotXs = [...xSet].sort((a, b) => a - b);
|
|
541
|
+
const hotYs = [...ySet].sort((a, b) => a - b);
|
|
542
|
+
for (let i = 0; i < hotYs.length; i++) {
|
|
543
|
+
for (let j = 0; j < hotXs.length; j++) {
|
|
544
|
+
const cur = { x: hotXs[j], y: hotYs[i] };
|
|
545
|
+
if (!graph.has(cur)) continue;
|
|
546
|
+
for (let k = j - 1; k >= 0; k--) {
|
|
547
|
+
const left = { x: hotXs[k], y: hotYs[i] };
|
|
548
|
+
if (graph.has(left)) {
|
|
549
|
+
if (!segmentCrossesObstacle(left, cur, obstacles)) {
|
|
550
|
+
graph.connect(left, cur);
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
556
|
+
const up = { x: hotXs[j], y: hotYs[k] };
|
|
557
|
+
if (graph.has(up)) {
|
|
558
|
+
if (!segmentCrossesObstacle(up, cur, obstacles)) {
|
|
559
|
+
graph.connect(up, cur);
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return graph;
|
|
567
|
+
}
|
|
568
|
+
function simplifyPointPath(points) {
|
|
569
|
+
if (points.length <= 2) return points;
|
|
570
|
+
const result = [points[0]];
|
|
571
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
572
|
+
const prev = points[i - 1], cur = points[i], next = points[i + 1];
|
|
573
|
+
const collinearX = prev.x === cur.x && cur.x === next.x;
|
|
574
|
+
const collinearY = prev.y === cur.y && cur.y === next.y;
|
|
575
|
+
if (collinearX || collinearY) continue;
|
|
576
|
+
if (prev.x === cur.x && prev.y === cur.y) continue;
|
|
577
|
+
result.push(cur);
|
|
578
|
+
}
|
|
579
|
+
result.push(points[points.length - 1]);
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
function countBends(path) {
|
|
583
|
+
if (path.length <= 2) return 0;
|
|
584
|
+
let bends = 0;
|
|
585
|
+
for (let i = 1; i < path.length - 1; i++) {
|
|
586
|
+
const prev = path[i - 1], cur = path[i], next = path[i + 1];
|
|
587
|
+
const sameX = prev.x === cur.x && cur.x === next.x;
|
|
588
|
+
const sameY = prev.y === cur.y && cur.y === next.y;
|
|
589
|
+
if (!sameX && !sameY) bends++;
|
|
590
|
+
}
|
|
591
|
+
return bends;
|
|
592
|
+
}
|
|
593
|
+
function totalPathLength(path) {
|
|
594
|
+
let len = 0;
|
|
595
|
+
for (let i = 1; i < path.length; i++) {
|
|
596
|
+
len += Math.abs(path[i].x - path[i - 1].x) + Math.abs(path[i].y - path[i - 1].y);
|
|
597
|
+
}
|
|
598
|
+
return len;
|
|
599
|
+
}
|
|
600
|
+
function pickBestRoute(candidates) {
|
|
601
|
+
let best = candidates[0];
|
|
602
|
+
let bestBends = countBends(best);
|
|
603
|
+
let bestLen = totalPathLength(best);
|
|
604
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
605
|
+
const bends = countBends(candidates[i]);
|
|
606
|
+
const len = totalPathLength(candidates[i]);
|
|
607
|
+
if (bends < bestBends || bends === bestBends && len < bestLen) {
|
|
608
|
+
best = candidates[i];
|
|
609
|
+
bestBends = bends;
|
|
610
|
+
bestLen = len;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return best;
|
|
614
|
+
}
|
|
615
|
+
function segmentLength(a, b) {
|
|
616
|
+
return Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
|
|
617
|
+
}
|
|
618
|
+
function interiorShortSegmentPenalty(path) {
|
|
619
|
+
if (path.length <= 3) return 0;
|
|
620
|
+
const threshold = MIN_STUB_LENGTH * 0.75;
|
|
621
|
+
let penalty = 0;
|
|
622
|
+
for (let i = 1; i < path.length - 2; i++) {
|
|
623
|
+
const len = segmentLength(path[i], path[i + 1]);
|
|
624
|
+
if (len < threshold) {
|
|
625
|
+
penalty += threshold - len;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return penalty;
|
|
629
|
+
}
|
|
630
|
+
function segmentRectClearance(a, b, rect) {
|
|
631
|
+
if (a.x === b.x) {
|
|
632
|
+
const x = a.x;
|
|
633
|
+
const minY = Math.min(a.y, b.y);
|
|
634
|
+
const maxY = Math.max(a.y, b.y);
|
|
635
|
+
const dx2 = x < rect.left ? rect.left - x : x > rectRight(rect) ? x - rectRight(rect) : 0;
|
|
636
|
+
const dy2 = maxY < rect.top ? rect.top - maxY : minY > rectBottom(rect) ? minY - rectBottom(rect) : 0;
|
|
637
|
+
return Math.hypot(dx2, dy2);
|
|
638
|
+
}
|
|
639
|
+
const y = a.y;
|
|
640
|
+
const minX = Math.min(a.x, b.x);
|
|
641
|
+
const maxX = Math.max(a.x, b.x);
|
|
642
|
+
const dy = y < rect.top ? rect.top - y : y > rectBottom(rect) ? y - rectBottom(rect) : 0;
|
|
643
|
+
const dx = maxX < rect.left ? rect.left - maxX : minX > rectRight(rect) ? minX - rectRight(rect) : 0;
|
|
644
|
+
return Math.hypot(dx, dy);
|
|
645
|
+
}
|
|
646
|
+
function routeInteriorClearance(path, obstacles) {
|
|
647
|
+
if (path.length <= 3 || obstacles.length === 0) return Number.POSITIVE_INFINITY;
|
|
648
|
+
const rects = obstacles.map(bboxToRect);
|
|
649
|
+
let best = Number.POSITIVE_INFINITY;
|
|
650
|
+
for (let i = 1; i < path.length - 2; i++) {
|
|
651
|
+
for (const rect of rects) {
|
|
652
|
+
best = Math.min(best, segmentRectClearance(path[i], path[i + 1], rect));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return best;
|
|
656
|
+
}
|
|
657
|
+
function buildDirectionPairCandidates(startPref, endPref) {
|
|
658
|
+
const startDirs = startPref.alternate ? [startPref.primary, startPref.alternate] : [startPref.primary];
|
|
659
|
+
const endDirs = endPref.alternate ? [endPref.primary, endPref.alternate] : [endPref.primary];
|
|
660
|
+
const unique = /* @__PURE__ */ new Map();
|
|
661
|
+
for (let i = 0; i < startDirs.length; i++) {
|
|
662
|
+
for (let j = 0; j < endDirs.length; j++) {
|
|
663
|
+
const candidate = {
|
|
664
|
+
startDir: startDirs[i],
|
|
665
|
+
endDir: endDirs[j],
|
|
666
|
+
preferencePenalty: i + j
|
|
667
|
+
};
|
|
668
|
+
const key = `${candidate.startDir}|${candidate.endDir}`;
|
|
669
|
+
const existing = unique.get(key);
|
|
670
|
+
if (!existing || candidate.preferencePenalty < existing.preferencePenalty) {
|
|
671
|
+
unique.set(key, candidate);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return [...unique.values()].sort((a, b) => a.preferencePenalty - b.preferencePenalty);
|
|
676
|
+
}
|
|
677
|
+
function isBetterScoredElbowRoute(candidate, current) {
|
|
678
|
+
if (candidate.bends !== current.bends) {
|
|
679
|
+
return candidate.bends < current.bends;
|
|
680
|
+
}
|
|
681
|
+
if (Math.abs(candidate.shortSegmentPenalty - current.shortSegmentPenalty) > 0.5) {
|
|
682
|
+
return candidate.shortSegmentPenalty < current.shortSegmentPenalty;
|
|
683
|
+
}
|
|
684
|
+
if (Math.abs(candidate.clearance - current.clearance) > 1) {
|
|
685
|
+
return candidate.clearance > current.clearance;
|
|
686
|
+
}
|
|
687
|
+
if (Math.abs(candidate.length - current.length) > 0.5) {
|
|
688
|
+
return candidate.length < current.length;
|
|
689
|
+
}
|
|
690
|
+
return candidate.preferencePenalty < current.preferencePenalty;
|
|
691
|
+
}
|
|
692
|
+
function selectElbowDirectionPair(args) {
|
|
693
|
+
const {
|
|
694
|
+
startWorld,
|
|
695
|
+
endWorld,
|
|
696
|
+
startBinding,
|
|
697
|
+
endBinding,
|
|
698
|
+
startShape,
|
|
699
|
+
endShape,
|
|
700
|
+
intermediateObstacles = [],
|
|
701
|
+
minStubLength
|
|
702
|
+
} = args;
|
|
703
|
+
const isCenterBinding = (binding) => !binding.isPrecise || binding.fixedPoint[0] === 0.5 && binding.fixedPoint[1] === 0.5;
|
|
704
|
+
const startPref = startBinding && isCenterBinding(startBinding) ? getElbowDirectionPreference(startShape ?? { x: startWorld.x, y: startWorld.y, width: 1, height: 1 }, endWorld, endShape, endBinding) : { primary: startBinding ? directionFromFixedPoint(startBinding.fixedPoint) : directionFromPoints(startWorld, endWorld) };
|
|
705
|
+
const endPref = endBinding && isCenterBinding(endBinding) ? getElbowDirectionPreference(endShape ?? { x: endWorld.x, y: endWorld.y, width: 1, height: 1 }, startWorld, startShape, startBinding) : { primary: endBinding ? directionFromFixedPoint(endBinding.fixedPoint) : directionFromPoints(endWorld, startWorld) };
|
|
706
|
+
const candidates = buildDirectionPairCandidates(startPref, endPref);
|
|
707
|
+
if (candidates.length === 1) {
|
|
708
|
+
return { startDir: candidates[0].startDir, endDir: candidates[0].endDir };
|
|
709
|
+
}
|
|
710
|
+
const clearanceObstacles = [
|
|
711
|
+
...startShape ? [startShape] : [],
|
|
712
|
+
...endShape ? [endShape] : [],
|
|
713
|
+
...intermediateObstacles
|
|
714
|
+
];
|
|
715
|
+
let best = null;
|
|
716
|
+
for (const candidate of candidates) {
|
|
717
|
+
const path = computeElbowRoute(
|
|
718
|
+
startWorld,
|
|
719
|
+
endWorld,
|
|
720
|
+
candidate.startDir,
|
|
721
|
+
candidate.endDir,
|
|
722
|
+
startShape,
|
|
723
|
+
endShape,
|
|
724
|
+
minStubLength,
|
|
725
|
+
intermediateObstacles
|
|
726
|
+
);
|
|
727
|
+
const scored = {
|
|
728
|
+
...candidate,
|
|
729
|
+
path,
|
|
730
|
+
bends: countBends(path),
|
|
731
|
+
length: totalPathLength(path),
|
|
732
|
+
shortSegmentPenalty: interiorShortSegmentPenalty(path),
|
|
733
|
+
clearance: routeInteriorClearance(path, clearanceObstacles)
|
|
734
|
+
};
|
|
735
|
+
if (!best || isBetterScoredElbowRoute(scored, best)) {
|
|
736
|
+
best = scored;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return best ? { startDir: best.startDir, endDir: best.endDir } : { startDir: startPref.primary, endDir: endPref.primary };
|
|
740
|
+
}
|
|
741
|
+
function findRouteWithObstacles(start, end, startDir, endDir, obstacles, margin) {
|
|
742
|
+
let bounds = {
|
|
743
|
+
left: Math.min(start.x, end.x),
|
|
744
|
+
top: Math.min(start.y, end.y),
|
|
745
|
+
width: Math.abs(end.x - start.x) || 1,
|
|
746
|
+
height: Math.abs(end.y - start.y) || 1
|
|
747
|
+
};
|
|
748
|
+
for (const obs of obstacles) {
|
|
749
|
+
bounds = rectUnion(bounds, obs);
|
|
750
|
+
}
|
|
751
|
+
bounds = rectInflate(bounds, BOUNDS_MARGIN, BOUNDS_MARGIN);
|
|
752
|
+
const verticals = [];
|
|
753
|
+
const horizontals = [];
|
|
754
|
+
for (const obs of obstacles) {
|
|
755
|
+
verticals.push(obs.left, rectRight(obs));
|
|
756
|
+
horizontals.push(obs.top, rectBottom(obs));
|
|
757
|
+
verticals.push(obs.left + obs.width / 2);
|
|
758
|
+
horizontals.push(obs.top + obs.height / 2);
|
|
759
|
+
}
|
|
760
|
+
verticals.push(start.x, end.x);
|
|
761
|
+
horizontals.push(start.y, end.y);
|
|
762
|
+
const sv = dirVec(startDir);
|
|
763
|
+
const ev = dirVec(endDir);
|
|
764
|
+
let origin = {
|
|
765
|
+
x: start.x + sv.x * margin,
|
|
766
|
+
y: start.y + sv.y * margin
|
|
767
|
+
};
|
|
768
|
+
let destination = {
|
|
769
|
+
x: end.x + ev.x * margin,
|
|
770
|
+
y: end.y + ev.y * margin
|
|
771
|
+
};
|
|
772
|
+
origin = clearAntennaPoint(origin, startDir, obstacles);
|
|
773
|
+
destination = clearAntennaPoint(destination, endDir, obstacles);
|
|
774
|
+
verticals.push(origin.x, destination.x);
|
|
775
|
+
horizontals.push(origin.y, destination.y);
|
|
776
|
+
verticals.push((start.x + end.x) / 2);
|
|
777
|
+
horizontals.push((start.y + end.y) / 2);
|
|
778
|
+
const gridSpots = generateGridSpots(verticals, horizontals, bounds, obstacles);
|
|
779
|
+
const allSpots = deduplicatePoints([origin, destination, ...gridSpots]);
|
|
780
|
+
const graph = buildGraph(allSpots, obstacles);
|
|
781
|
+
const pathPoints = astarSearch(graph, origin, destination);
|
|
782
|
+
if (pathPoints) {
|
|
783
|
+
const fullPath = [start, ...pathPoints, end];
|
|
784
|
+
return simplifyPointPath(fullPath);
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
function computeElbowRoute(start, end, startDir, endDir, startBBox, endBBox, minStubLength, intermediateObstacles) {
|
|
789
|
+
if (start.x === end.x && start.y === end.y) {
|
|
790
|
+
return [start, end];
|
|
791
|
+
}
|
|
792
|
+
const shapeA = startBBox ? bboxToRect(startBBox) : { left: start.x, top: start.y, width: 0, height: 0 };
|
|
793
|
+
const shapeB = endBBox ? bboxToRect(endBBox) : { left: end.x, top: end.y, width: 0, height: 0 };
|
|
794
|
+
const inflationMargin = SHAPE_MARGIN;
|
|
795
|
+
const antennaMargin = Math.max(MIN_STUB_LENGTH, minStubLength ?? 0);
|
|
796
|
+
const intermediateRects = (intermediateObstacles ?? []).map(
|
|
797
|
+
(bbox) => rectInflate(bboxToRect(bbox), inflationMargin, inflationMargin)
|
|
798
|
+
);
|
|
799
|
+
const inflA_std = startBBox ? inflateExcludingFace(shapeA, inflationMargin, startDir) : rectInflate(shapeA, inflationMargin, inflationMargin);
|
|
800
|
+
const inflB_std = endBBox ? inflateExcludingFace(shapeB, inflationMargin, endDir) : rectInflate(shapeB, inflationMargin, inflationMargin);
|
|
801
|
+
const facesA = [startDir];
|
|
802
|
+
const facesB = [endDir];
|
|
803
|
+
let hasRelaxedConfig = false;
|
|
804
|
+
if (startBBox) {
|
|
805
|
+
const faceTowardEnd = directionFromShapeToPoint(startBBox, end);
|
|
806
|
+
if (faceTowardEnd !== startDir) {
|
|
807
|
+
facesA.push(faceTowardEnd);
|
|
808
|
+
hasRelaxedConfig = true;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (endBBox) {
|
|
812
|
+
const faceTowardStart = directionFromShapeToPoint(endBBox, start);
|
|
813
|
+
if (faceTowardStart !== endDir) {
|
|
814
|
+
facesB.push(faceTowardStart);
|
|
815
|
+
hasRelaxedConfig = true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const inflA_rlx = startBBox ? inflateExcludingFaces(shapeA, inflationMargin, facesA) : rectInflate(shapeA, inflationMargin, inflationMargin);
|
|
819
|
+
const inflB_rlx = endBBox ? inflateExcludingFaces(shapeB, inflationMargin, facesB) : rectInflate(shapeB, inflationMargin, inflationMargin);
|
|
820
|
+
const candidates = [];
|
|
821
|
+
const route1 = findRouteWithObstacles(
|
|
822
|
+
start,
|
|
823
|
+
end,
|
|
824
|
+
startDir,
|
|
825
|
+
endDir,
|
|
826
|
+
[inflA_std, inflB_std, ...intermediateRects],
|
|
827
|
+
antennaMargin
|
|
828
|
+
);
|
|
829
|
+
if (route1) candidates.push(route1);
|
|
830
|
+
if (hasRelaxedConfig) {
|
|
831
|
+
const route2 = findRouteWithObstacles(
|
|
832
|
+
start,
|
|
833
|
+
end,
|
|
834
|
+
startDir,
|
|
835
|
+
endDir,
|
|
836
|
+
[inflA_rlx, inflB_rlx, ...intermediateRects],
|
|
837
|
+
antennaMargin
|
|
838
|
+
);
|
|
839
|
+
if (route2) candidates.push(route2);
|
|
840
|
+
}
|
|
841
|
+
if (candidates.length > 0) {
|
|
842
|
+
return pickBestRoute(candidates);
|
|
843
|
+
}
|
|
844
|
+
if (intermediateRects.length > 0) {
|
|
845
|
+
const routeFallback = findRouteWithObstacles(
|
|
846
|
+
start,
|
|
847
|
+
end,
|
|
848
|
+
startDir,
|
|
849
|
+
endDir,
|
|
850
|
+
[inflA_std, inflB_std],
|
|
851
|
+
antennaMargin
|
|
852
|
+
);
|
|
853
|
+
if (routeFallback) return routeFallback;
|
|
854
|
+
}
|
|
855
|
+
if (intermediateRects.length > 0) {
|
|
856
|
+
const halfMargin = Math.max(4, inflationMargin / 2);
|
|
857
|
+
const halfIntermediateRects = (intermediateObstacles ?? []).map(
|
|
858
|
+
(bbox) => rectInflate(bboxToRect(bbox), halfMargin, halfMargin)
|
|
859
|
+
);
|
|
860
|
+
const inflA_half = startBBox ? inflateExcludingFace(shapeA, halfMargin, startDir) : rectInflate(shapeA, halfMargin, halfMargin);
|
|
861
|
+
const inflB_half = endBBox ? inflateExcludingFace(shapeB, halfMargin, endDir) : rectInflate(shapeB, halfMargin, halfMargin);
|
|
862
|
+
const routeRelaxed = findRouteWithObstacles(
|
|
863
|
+
start,
|
|
864
|
+
end,
|
|
865
|
+
startDir,
|
|
866
|
+
endDir,
|
|
867
|
+
[inflA_half, inflB_half, ...halfIntermediateRects],
|
|
868
|
+
antennaMargin
|
|
869
|
+
);
|
|
870
|
+
if (routeRelaxed) {
|
|
871
|
+
return routeRelaxed;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return simplifyPointPath(fallbackRoute(start, end, startDir, endDir, antennaMargin));
|
|
875
|
+
}
|
|
876
|
+
function clearAntennaPoint(pt, dir, obstacles) {
|
|
877
|
+
let { x, y } = pt;
|
|
878
|
+
for (let pass = 0; pass < obstacles.length; pass++) {
|
|
879
|
+
let allClear = true;
|
|
880
|
+
for (const obs of obstacles) {
|
|
881
|
+
if (x <= obs.left || x >= rectRight(obs) || y <= obs.top || y >= rectBottom(obs)) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
allClear = false;
|
|
885
|
+
switch (dir) {
|
|
886
|
+
case "left":
|
|
887
|
+
x = obs.left - 1;
|
|
888
|
+
break;
|
|
889
|
+
case "right":
|
|
890
|
+
x = rectRight(obs) + 1;
|
|
891
|
+
break;
|
|
892
|
+
case "up":
|
|
893
|
+
y = obs.top - 1;
|
|
894
|
+
break;
|
|
895
|
+
case "down":
|
|
896
|
+
y = rectBottom(obs) + 1;
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (allClear) break;
|
|
901
|
+
}
|
|
902
|
+
return { x, y };
|
|
903
|
+
}
|
|
904
|
+
function fallbackRoute(start, end, startDir, endDir, margin) {
|
|
905
|
+
const stub = Math.max(margin, SHAPE_MARGIN);
|
|
906
|
+
const sv = dirVec(startDir);
|
|
907
|
+
const ev = dirVec(endDir);
|
|
908
|
+
const s1 = { x: start.x + sv.x * stub, y: start.y + sv.y * stub };
|
|
909
|
+
const e1 = { x: end.x + ev.x * stub, y: end.y + ev.y * stub };
|
|
910
|
+
if (isVerticalDir(startDir) === isVerticalDir(endDir)) {
|
|
911
|
+
if (isVerticalDir(startDir)) {
|
|
912
|
+
const midY = (s1.y + e1.y) / 2;
|
|
913
|
+
return [start, s1, { x: s1.x, y: midY }, { x: e1.x, y: midY }, e1, end];
|
|
914
|
+
} else {
|
|
915
|
+
const midX = (s1.x + e1.x) / 2;
|
|
916
|
+
return [start, s1, { x: midX, y: s1.y }, { x: midX, y: e1.y }, e1, end];
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
if (isVerticalDir(startDir)) {
|
|
920
|
+
return [start, s1, { x: s1.x, y: e1.y }, e1, end];
|
|
921
|
+
} else {
|
|
922
|
+
return [start, s1, { x: e1.x, y: s1.y }, e1, end];
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const OBSTACLE_TYPES = /* @__PURE__ */ new Set(["rectangle", "ellipse", "diamond", "text", "image"]);
|
|
927
|
+
function collectElbowIntermediateObstacles(startWorld, endWorld, allElements, excludeIds) {
|
|
928
|
+
const obstacles = [];
|
|
929
|
+
const routeMinX = Math.min(startWorld.x, endWorld.x) - BOUNDS_MARGIN * 3;
|
|
930
|
+
const routeMaxX = Math.max(startWorld.x, endWorld.x) + BOUNDS_MARGIN * 3;
|
|
931
|
+
const routeMinY = Math.min(startWorld.y, endWorld.y) - BOUNDS_MARGIN * 3;
|
|
932
|
+
const routeMaxY = Math.max(startWorld.y, endWorld.y) + BOUNDS_MARGIN * 3;
|
|
933
|
+
for (const el of allElements) {
|
|
934
|
+
if (!OBSTACLE_TYPES.has(el.type)) continue;
|
|
935
|
+
if (excludeIds.has(el.id)) continue;
|
|
936
|
+
if (!el.isVisible) continue;
|
|
937
|
+
const bbox = getShapeBBox(el);
|
|
938
|
+
if (bbox.x + bbox.width < routeMinX || bbox.x > routeMaxX || bbox.y + bbox.height < routeMinY || bbox.y > routeMaxY) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
obstacles.push(bbox);
|
|
942
|
+
}
|
|
943
|
+
return obstacles;
|
|
944
|
+
}
|
|
945
|
+
const CURVE_RATIO = 0.2;
|
|
946
|
+
function computeCurveControlPoint(start, end, ratio = CURVE_RATIO) {
|
|
947
|
+
const mx = (start.x + end.x) / 2;
|
|
948
|
+
const my = (start.y + end.y) / 2;
|
|
949
|
+
const dx = end.x - start.x;
|
|
950
|
+
const dy = end.y - start.y;
|
|
951
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
952
|
+
if (len === 0) return { x: mx, y: my };
|
|
953
|
+
const nx = -dy / len;
|
|
954
|
+
const ny = dx / len;
|
|
955
|
+
const offset = len * ratio;
|
|
956
|
+
return { x: mx + nx * offset, y: my + ny * offset };
|
|
957
|
+
}
|
|
958
|
+
function quadBezierAt(p0, cp, p2, t) {
|
|
959
|
+
const mt = 1 - t;
|
|
960
|
+
return {
|
|
961
|
+
x: mt * mt * p0.x + 2 * mt * t * cp.x + t * t * p2.x,
|
|
962
|
+
y: mt * mt * p0.y + 2 * mt * t * cp.y + t * t * p2.y
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function toRad(degrees) {
|
|
966
|
+
return degrees * Math.PI / 180;
|
|
967
|
+
}
|
|
968
|
+
function rotatePoint(px, py, angle) {
|
|
969
|
+
if (angle === 0) return { x: px, y: py };
|
|
970
|
+
const cos = Math.cos(angle);
|
|
971
|
+
const sin = Math.sin(angle);
|
|
972
|
+
return { x: px * cos - py * sin, y: px * sin + py * cos };
|
|
973
|
+
}
|
|
974
|
+
function worldToLocal(el, wp) {
|
|
975
|
+
const cx = el.x + el.width / 2;
|
|
976
|
+
const cy = el.y + el.height / 2;
|
|
977
|
+
const dx = wp.x - cx;
|
|
978
|
+
const dy = wp.y - cy;
|
|
979
|
+
const angle = toRad(el.rotation || 0);
|
|
980
|
+
if (angle === 0) return { x: dx, y: dy };
|
|
981
|
+
return rotatePoint(dx, dy, -angle);
|
|
982
|
+
}
|
|
983
|
+
function localToWorld(el, lp) {
|
|
984
|
+
const cx = el.x + el.width / 2;
|
|
985
|
+
const cy = el.y + el.height / 2;
|
|
986
|
+
const angle = toRad(el.rotation || 0);
|
|
987
|
+
const rotated = rotatePoint(lp.x, lp.y, angle);
|
|
988
|
+
return { x: cx + rotated.x, y: cy + rotated.y };
|
|
989
|
+
}
|
|
990
|
+
function anchorToFixedPoint(anchor) {
|
|
991
|
+
switch (anchor) {
|
|
992
|
+
case "n":
|
|
993
|
+
return [0.5, 0];
|
|
994
|
+
case "s":
|
|
995
|
+
return [0.5, 1];
|
|
996
|
+
case "e":
|
|
997
|
+
return [1, 0.5];
|
|
998
|
+
case "w":
|
|
999
|
+
return [0, 0.5];
|
|
1000
|
+
case "ne":
|
|
1001
|
+
return [1, 0];
|
|
1002
|
+
case "nw":
|
|
1003
|
+
return [0, 0];
|
|
1004
|
+
case "se":
|
|
1005
|
+
return [1, 1];
|
|
1006
|
+
case "sw":
|
|
1007
|
+
return [0, 1];
|
|
1008
|
+
case "center":
|
|
1009
|
+
return [0.5, 0.5];
|
|
1010
|
+
case "auto":
|
|
1011
|
+
return [0.5, 0.5];
|
|
1012
|
+
// caller should resolve via nearest
|
|
1013
|
+
default:
|
|
1014
|
+
return [0.5, 0.5];
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function resolvePort(el, portId) {
|
|
1018
|
+
if (!el.ports) return null;
|
|
1019
|
+
const port = el.ports.find((p) => p.id === portId);
|
|
1020
|
+
return port ? port.ratio : null;
|
|
1021
|
+
}
|
|
1022
|
+
function resolveBindingPoint(binding, element) {
|
|
1023
|
+
if (binding.portId) {
|
|
1024
|
+
const portFp = resolvePort(element, binding.portId);
|
|
1025
|
+
if (portFp) return { fixedPoint: portFp, snapMode: "port" };
|
|
1026
|
+
}
|
|
1027
|
+
if (binding.anchor && binding.anchor !== "auto") {
|
|
1028
|
+
return { fixedPoint: anchorToFixedPoint(binding.anchor), snapMode: "anchor" };
|
|
1029
|
+
}
|
|
1030
|
+
const mode = binding.fixedPoint[0] === 0.5 && binding.fixedPoint[1] === 0.5 ? "center" : "edge";
|
|
1031
|
+
return { fixedPoint: binding.fixedPoint, snapMode: mode };
|
|
1032
|
+
}
|
|
1033
|
+
function getEdgePointFromFixedPoint(el, fixedPoint, gap = 0, toward) {
|
|
1034
|
+
const localTargetX = (fixedPoint[0] - 0.5) * el.width;
|
|
1035
|
+
const localTargetY = (fixedPoint[1] - 0.5) * el.height;
|
|
1036
|
+
if (localTargetX === 0 && localTargetY === 0) {
|
|
1037
|
+
if (toward) {
|
|
1038
|
+
return getEdgePoint(el, toward, gap);
|
|
1039
|
+
}
|
|
1040
|
+
return localToWorld(el, { x: 0, y: 0 });
|
|
1041
|
+
}
|
|
1042
|
+
const worldTarget = localToWorld(el, { x: localTargetX, y: localTargetY });
|
|
1043
|
+
return getEdgePoint(el, worldTarget, gap);
|
|
1044
|
+
}
|
|
1045
|
+
function getEdgePoint(el, toward, gap = 0) {
|
|
1046
|
+
const local = worldToLocal(el, toward);
|
|
1047
|
+
const lx = local.x;
|
|
1048
|
+
const ly = local.y;
|
|
1049
|
+
if (lx === 0 && ly === 0) {
|
|
1050
|
+
if (el.width >= el.height) {
|
|
1051
|
+
return localToWorld(el, { x: el.width / 2 + gap, y: 0 });
|
|
1052
|
+
}
|
|
1053
|
+
return localToWorld(el, { x: 0, y: -(el.height / 2 + gap) });
|
|
1054
|
+
}
|
|
1055
|
+
let edgeLocal;
|
|
1056
|
+
switch (el.type) {
|
|
1057
|
+
case "ellipse": {
|
|
1058
|
+
const a = el.width / 2;
|
|
1059
|
+
const b = el.height / 2;
|
|
1060
|
+
const angle = Math.atan2(ly, lx);
|
|
1061
|
+
edgeLocal = {
|
|
1062
|
+
x: (a + gap) * Math.cos(angle),
|
|
1063
|
+
y: (b + gap) * Math.sin(angle)
|
|
1064
|
+
};
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case "diamond": {
|
|
1068
|
+
const hw = el.width / 2;
|
|
1069
|
+
const hh = el.height / 2;
|
|
1070
|
+
const angle = Math.atan2(ly, lx);
|
|
1071
|
+
const cosA = Math.cos(angle);
|
|
1072
|
+
const sinA = Math.sin(angle);
|
|
1073
|
+
const absCos = Math.abs(cosA);
|
|
1074
|
+
const absSin = Math.abs(sinA);
|
|
1075
|
+
const t = 1 / (absCos / hw + absSin / hh);
|
|
1076
|
+
const ex = t * cosA;
|
|
1077
|
+
const ey = t * sinA;
|
|
1078
|
+
if (gap === 0) {
|
|
1079
|
+
edgeLocal = { x: ex, y: ey };
|
|
1080
|
+
} else {
|
|
1081
|
+
const nx = (cosA >= 0 ? 1 : -1) / hw;
|
|
1082
|
+
const ny = (sinA >= 0 ? 1 : -1) / hh;
|
|
1083
|
+
const nLen = Math.sqrt(nx * nx + ny * ny);
|
|
1084
|
+
edgeLocal = {
|
|
1085
|
+
x: ex + nx / nLen * gap,
|
|
1086
|
+
y: ey + ny / nLen * gap
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
default: {
|
|
1092
|
+
const hw = el.width / 2;
|
|
1093
|
+
const hh = el.height / 2;
|
|
1094
|
+
const angle = Math.atan2(ly, lx);
|
|
1095
|
+
const absTanAngle = Math.abs(Math.tan(angle));
|
|
1096
|
+
let ex, ey;
|
|
1097
|
+
if (absTanAngle <= hh / hw) {
|
|
1098
|
+
ex = lx > 0 ? hw : -hw;
|
|
1099
|
+
ey = ex * Math.tan(angle);
|
|
1100
|
+
} else {
|
|
1101
|
+
ey = ly > 0 ? hh : -hh;
|
|
1102
|
+
ex = ey / Math.tan(angle);
|
|
1103
|
+
}
|
|
1104
|
+
if (gap === 0) {
|
|
1105
|
+
edgeLocal = { x: ex, y: ey };
|
|
1106
|
+
} else {
|
|
1107
|
+
if (absTanAngle <= hh / hw) {
|
|
1108
|
+
edgeLocal = { x: ex + (lx > 0 ? gap : -gap), y: ey };
|
|
1109
|
+
} else {
|
|
1110
|
+
edgeLocal = { x: ex, y: ey + (ly > 0 ? gap : -gap) };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return localToWorld(el, edgeLocal);
|
|
1117
|
+
}
|
|
1118
|
+
function recomputeBoundPoints(connector, allElements) {
|
|
1119
|
+
const { startBinding, endBinding } = connector;
|
|
1120
|
+
if (!startBinding && !endBinding) return null;
|
|
1121
|
+
const elementMap = new Map(allElements.map((el) => [el.id, el]));
|
|
1122
|
+
const pts = connector.points;
|
|
1123
|
+
const pointCount = pts.length / 2;
|
|
1124
|
+
let startPt = {
|
|
1125
|
+
x: connector.x + pts[0],
|
|
1126
|
+
y: connector.y + pts[1]
|
|
1127
|
+
};
|
|
1128
|
+
let endPt = {
|
|
1129
|
+
x: connector.x + pts[pts.length - 2],
|
|
1130
|
+
y: connector.y + pts[pts.length - 1]
|
|
1131
|
+
};
|
|
1132
|
+
const startEl = startBinding ? elementMap.get(startBinding.elementId) : void 0;
|
|
1133
|
+
const endEl = endBinding ? elementMap.get(endBinding.elementId) : void 0;
|
|
1134
|
+
const isElbow = connector.lineType === "elbow";
|
|
1135
|
+
const getAnchorDir = (binding, el) => {
|
|
1136
|
+
if (binding.isPrecise) {
|
|
1137
|
+
const { fixedPoint: resolvedFp } = resolveBindingPoint(binding, el);
|
|
1138
|
+
return getEdgePointFromFixedPoint(el, resolvedFp, 0);
|
|
1139
|
+
}
|
|
1140
|
+
return { x: el.x + el.width / 2, y: el.y + el.height / 2 };
|
|
1141
|
+
};
|
|
1142
|
+
const getPreciseEdgePoint = (binding, el, toward) => {
|
|
1143
|
+
const { fixedPoint: resolvedFp } = resolveBindingPoint(binding, el);
|
|
1144
|
+
return getEdgePointFromFixedPoint(el, resolvedFp, binding.gap, toward);
|
|
1145
|
+
};
|
|
1146
|
+
const directionStartRef = startBinding && startEl ? startBinding.isPrecise ? getPreciseEdgePoint(startBinding, startEl, endPt) : { x: startEl.x + startEl.width / 2, y: startEl.y + startEl.height / 2 } : startPt;
|
|
1147
|
+
const directionEndRef = endBinding && endEl ? endBinding.isPrecise ? getPreciseEdgePoint(endBinding, endEl, startPt) : { x: endEl.x + endEl.width / 2, y: endEl.y + endEl.height / 2 } : endPt;
|
|
1148
|
+
const elbowDirectionPair = isElbow ? (() => {
|
|
1149
|
+
const excludeIds = /* @__PURE__ */ new Set();
|
|
1150
|
+
if (startBinding == null ? void 0 : startBinding.elementId) excludeIds.add(startBinding.elementId);
|
|
1151
|
+
if (endBinding == null ? void 0 : endBinding.elementId) excludeIds.add(endBinding.elementId);
|
|
1152
|
+
return selectElbowDirectionPair({
|
|
1153
|
+
startWorld: directionStartRef,
|
|
1154
|
+
endWorld: directionEndRef,
|
|
1155
|
+
startBinding,
|
|
1156
|
+
endBinding,
|
|
1157
|
+
startShape: startEl ?? null,
|
|
1158
|
+
endShape: endEl ?? null,
|
|
1159
|
+
intermediateObstacles: collectElbowIntermediateObstacles(directionStartRef, directionEndRef, allElements, excludeIds)
|
|
1160
|
+
});
|
|
1161
|
+
})() : null;
|
|
1162
|
+
const getElbowFaceEdgePoint = (el, gap, preferredDir) => {
|
|
1163
|
+
const cx = el.x + el.width / 2;
|
|
1164
|
+
const cy = el.y + el.height / 2;
|
|
1165
|
+
const far = Math.max(el.width, el.height) * 10;
|
|
1166
|
+
let target;
|
|
1167
|
+
switch (preferredDir) {
|
|
1168
|
+
case "up":
|
|
1169
|
+
target = { x: cx, y: cy - far };
|
|
1170
|
+
break;
|
|
1171
|
+
case "down":
|
|
1172
|
+
target = { x: cx, y: cy + far };
|
|
1173
|
+
break;
|
|
1174
|
+
case "left":
|
|
1175
|
+
target = { x: cx - far, y: cy };
|
|
1176
|
+
break;
|
|
1177
|
+
case "right":
|
|
1178
|
+
target = { x: cx + far, y: cy };
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
return getEdgePoint(el, target, gap);
|
|
1182
|
+
};
|
|
1183
|
+
const getCenterEdgePoint = isElbow ? (el, _toward, gap, preferredDir) => getElbowFaceEdgePoint(el, gap, preferredDir) : getEdgePoint;
|
|
1184
|
+
if (startBinding && startEl && endBinding && endEl) {
|
|
1185
|
+
if (startBinding.isPrecise) {
|
|
1186
|
+
startPt = getPreciseEdgePoint(startBinding, startEl, endPt);
|
|
1187
|
+
} else {
|
|
1188
|
+
const endAnchor = getAnchorDir(endBinding, endEl);
|
|
1189
|
+
startPt = getCenterEdgePoint(startEl, endAnchor, startBinding.gap, (elbowDirectionPair == null ? void 0 : elbowDirectionPair.startDir) ?? "right");
|
|
1190
|
+
}
|
|
1191
|
+
if (endBinding.isPrecise) {
|
|
1192
|
+
endPt = getPreciseEdgePoint(endBinding, endEl, startPt);
|
|
1193
|
+
} else {
|
|
1194
|
+
const startAnchor = getAnchorDir(startBinding, startEl);
|
|
1195
|
+
endPt = getCenterEdgePoint(endEl, startAnchor, endBinding.gap, (elbowDirectionPair == null ? void 0 : elbowDirectionPair.endDir) ?? "left");
|
|
1196
|
+
}
|
|
1197
|
+
if (startBinding.isPrecise) {
|
|
1198
|
+
startPt = getPreciseEdgePoint(startBinding, startEl, endPt);
|
|
1199
|
+
} else if (!startBinding.isPrecise) {
|
|
1200
|
+
startPt = getCenterEdgePoint(startEl, endPt, startBinding.gap, (elbowDirectionPair == null ? void 0 : elbowDirectionPair.startDir) ?? "right");
|
|
1201
|
+
}
|
|
1202
|
+
if (endBinding.isPrecise) {
|
|
1203
|
+
endPt = getPreciseEdgePoint(endBinding, endEl, startPt);
|
|
1204
|
+
} else if (!endBinding.isPrecise) {
|
|
1205
|
+
endPt = getCenterEdgePoint(endEl, startPt, endBinding.gap, (elbowDirectionPair == null ? void 0 : elbowDirectionPair.endDir) ?? "left");
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
if (startBinding && startEl) {
|
|
1209
|
+
if (startBinding.isPrecise) {
|
|
1210
|
+
startPt = getPreciseEdgePoint(startBinding, startEl, endPt);
|
|
1211
|
+
} else {
|
|
1212
|
+
startPt = getCenterEdgePoint(startEl, endPt, startBinding.gap, (elbowDirectionPair == null ? void 0 : elbowDirectionPair.startDir) ?? "right");
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (endBinding && endEl) {
|
|
1216
|
+
if (endBinding.isPrecise) {
|
|
1217
|
+
endPt = getPreciseEdgePoint(endBinding, endEl, startPt);
|
|
1218
|
+
} else {
|
|
1219
|
+
endPt = getCenterEdgePoint(endEl, startPt, endBinding.gap, (elbowDirectionPair == null ? void 0 : elbowDirectionPair.endDir) ?? "left");
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const newPoints = [0, 0];
|
|
1224
|
+
for (let i = 1; i < pointCount - 1; i++) {
|
|
1225
|
+
const worldX = connector.x + pts[i * 2];
|
|
1226
|
+
const worldY = connector.y + pts[i * 2 + 1];
|
|
1227
|
+
newPoints.push(worldX - startPt.x, worldY - startPt.y);
|
|
1228
|
+
}
|
|
1229
|
+
newPoints.push(endPt.x - startPt.x, endPt.y - startPt.y);
|
|
1230
|
+
const dx = endPt.x - startPt.x;
|
|
1231
|
+
const dy = endPt.y - startPt.y;
|
|
1232
|
+
return {
|
|
1233
|
+
x: startPt.x,
|
|
1234
|
+
y: startPt.y,
|
|
1235
|
+
points: newPoints,
|
|
1236
|
+
width: Math.abs(dx),
|
|
1237
|
+
height: Math.abs(dy)
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function findConnectorsForElement(elementId, elements) {
|
|
1241
|
+
return elements.filter((el) => {
|
|
1242
|
+
var _a, _b;
|
|
1243
|
+
if (el.type !== "line" && el.type !== "arrow") return false;
|
|
1244
|
+
const c = el;
|
|
1245
|
+
return ((_a = c.startBinding) == null ? void 0 : _a.elementId) === elementId || ((_b = c.endBinding) == null ? void 0 : _b.elementId) === elementId;
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
function clearBindingsForDeletedElements(deletedIds, elements) {
|
|
1249
|
+
return elements.map((el) => {
|
|
1250
|
+
let changed = false;
|
|
1251
|
+
if (el.boundElements && el.boundElements.length > 0) {
|
|
1252
|
+
const filtered = el.boundElements.filter((be) => !deletedIds.has(be.id));
|
|
1253
|
+
if (filtered.length !== el.boundElements.length) {
|
|
1254
|
+
el = { ...el, boundElements: filtered.length > 0 ? filtered : null };
|
|
1255
|
+
changed = true;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (el.type !== "line" && el.type !== "arrow") return el;
|
|
1259
|
+
const c = el;
|
|
1260
|
+
let startBinding = c.startBinding;
|
|
1261
|
+
let endBinding = c.endBinding;
|
|
1262
|
+
if (startBinding && deletedIds.has(startBinding.elementId)) {
|
|
1263
|
+
startBinding = null;
|
|
1264
|
+
changed = true;
|
|
1265
|
+
}
|
|
1266
|
+
if (endBinding && deletedIds.has(endBinding.elementId)) {
|
|
1267
|
+
endBinding = null;
|
|
1268
|
+
changed = true;
|
|
1269
|
+
}
|
|
1270
|
+
return changed ? { ...el, startBinding, endBinding } : el;
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
function computeConnectorLabelPosition(connector, textWidth, textHeight) {
|
|
1274
|
+
const pts = connector.points;
|
|
1275
|
+
const startPt = { x: pts[0], y: pts[1] };
|
|
1276
|
+
const endPt = { x: pts[pts.length - 2], y: pts[pts.length - 1] };
|
|
1277
|
+
let midX;
|
|
1278
|
+
let midY;
|
|
1279
|
+
if (connector.lineType === "curved") {
|
|
1280
|
+
const curvature = connector.curvature ?? CURVE_RATIO;
|
|
1281
|
+
const cp = computeCurveControlPoint(startPt, endPt, curvature);
|
|
1282
|
+
const mid = quadBezierAt(startPt, cp, endPt, 0.5);
|
|
1283
|
+
midX = connector.x + mid.x;
|
|
1284
|
+
midY = connector.y + mid.y;
|
|
1285
|
+
} else if (connector.lineType === "elbow" && pts.length >= 4) {
|
|
1286
|
+
const segCount = pts.length / 2 - 1;
|
|
1287
|
+
let totalLen = 0;
|
|
1288
|
+
for (let i = 0; i < segCount; i++) {
|
|
1289
|
+
const dx = pts[(i + 1) * 2] - pts[i * 2];
|
|
1290
|
+
const dy = pts[(i + 1) * 2 + 1] - pts[i * 2 + 1];
|
|
1291
|
+
totalLen += Math.sqrt(dx * dx + dy * dy);
|
|
1292
|
+
}
|
|
1293
|
+
const half = totalLen / 2;
|
|
1294
|
+
let walked = 0;
|
|
1295
|
+
midX = connector.x + (startPt.x + endPt.x) / 2;
|
|
1296
|
+
midY = connector.y + (startPt.y + endPt.y) / 2;
|
|
1297
|
+
for (let i = 0; i < segCount; i++) {
|
|
1298
|
+
const ax = pts[i * 2], ay = pts[i * 2 + 1];
|
|
1299
|
+
const bx = pts[(i + 1) * 2], by = pts[(i + 1) * 2 + 1];
|
|
1300
|
+
const dx = bx - ax, dy = by - ay;
|
|
1301
|
+
const segLen = Math.sqrt(dx * dx + dy * dy);
|
|
1302
|
+
if (walked + segLen >= half && segLen > 0) {
|
|
1303
|
+
const t = (half - walked) / segLen;
|
|
1304
|
+
midX = connector.x + ax + dx * t;
|
|
1305
|
+
midY = connector.y + ay + dy * t;
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
walked += segLen;
|
|
1309
|
+
}
|
|
1310
|
+
} else {
|
|
1311
|
+
midX = connector.x + (startPt.x + endPt.x) / 2;
|
|
1312
|
+
midY = connector.y + (startPt.y + endPt.y) / 2;
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
x: midX - textWidth / 2,
|
|
1316
|
+
y: midY - textHeight / 2
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function syncConnectorLabels(connectorIds, elMap) {
|
|
1320
|
+
const updates = [];
|
|
1321
|
+
for (const connId of connectorIds) {
|
|
1322
|
+
const conn = elMap.get(connId);
|
|
1323
|
+
if (!conn || conn.type !== "arrow" && conn.type !== "line") continue;
|
|
1324
|
+
if (!conn.boundElements) continue;
|
|
1325
|
+
const connector = conn;
|
|
1326
|
+
for (const be of conn.boundElements) {
|
|
1327
|
+
if (be.type !== "text") continue;
|
|
1328
|
+
const txt = elMap.get(be.id);
|
|
1329
|
+
if (!txt) continue;
|
|
1330
|
+
const textW = Math.max(10, txt.width || 60);
|
|
1331
|
+
const textH = txt.height || 30;
|
|
1332
|
+
const pos = computeConnectorLabelPosition(connector, textW, textH);
|
|
1333
|
+
updates.push({ id: txt.id, updates: { x: pos.x, y: pos.y } });
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return updates;
|
|
1337
|
+
}
|
|
1338
|
+
const BOUND_TEXT_PADDING = 4;
|
|
1339
|
+
const CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
1340
|
+
"rectangle",
|
|
1341
|
+
"ellipse",
|
|
1342
|
+
"diamond",
|
|
1343
|
+
"image"
|
|
1344
|
+
]);
|
|
1345
|
+
function computeBoundTextPosition(container, text) {
|
|
1346
|
+
const tw = Math.max(20, container.width - BOUND_TEXT_PADDING * 2);
|
|
1347
|
+
let ty;
|
|
1348
|
+
if (text.verticalAlign === "top") {
|
|
1349
|
+
ty = container.y + BOUND_TEXT_PADDING;
|
|
1350
|
+
} else if (text.verticalAlign === "bottom") {
|
|
1351
|
+
ty = container.y + container.height - text.height - BOUND_TEXT_PADDING;
|
|
1352
|
+
} else {
|
|
1353
|
+
ty = container.y + (container.height - text.height) / 2;
|
|
1354
|
+
}
|
|
1355
|
+
return { x: container.x + BOUND_TEXT_PADDING, y: ty, width: tw };
|
|
1356
|
+
}
|
|
1357
|
+
function syncAfterDrag(movedIds, elements, skipIds) {
|
|
1358
|
+
const elMap = /* @__PURE__ */ new Map();
|
|
1359
|
+
for (const el of elements) elMap.set(el.id, el);
|
|
1360
|
+
const updates = [];
|
|
1361
|
+
const processedConnectors = /* @__PURE__ */ new Set();
|
|
1362
|
+
for (const id of movedIds) {
|
|
1363
|
+
const connectors = findConnectorsForElement(id, elements);
|
|
1364
|
+
for (const conn of connectors) {
|
|
1365
|
+
if (processedConnectors.has(conn.id)) continue;
|
|
1366
|
+
processedConnectors.add(conn.id);
|
|
1367
|
+
const freshConn = elMap.get(conn.id);
|
|
1368
|
+
if (!freshConn) continue;
|
|
1369
|
+
const recomputed = recomputeBoundPoints(freshConn, elements);
|
|
1370
|
+
if (recomputed) updates.push({ id: freshConn.id, updates: recomputed });
|
|
1371
|
+
}
|
|
1372
|
+
const el = elMap.get(id);
|
|
1373
|
+
if ((el == null ? void 0 : el.boundElements) && CONTAINER_TYPES.has(el.type)) {
|
|
1374
|
+
for (const be of el.boundElements) {
|
|
1375
|
+
if (be.type !== "text") continue;
|
|
1376
|
+
const txt = elMap.get(be.id);
|
|
1377
|
+
if (!txt) continue;
|
|
1378
|
+
updates.push({ id: be.id, updates: computeBoundTextPosition(el, txt) });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (processedConnectors.size > 0 && updates.length > 0) {
|
|
1383
|
+
const tempMap = new Map(elMap);
|
|
1384
|
+
for (const u of updates) {
|
|
1385
|
+
const existing = tempMap.get(u.id);
|
|
1386
|
+
if (existing) {
|
|
1387
|
+
tempMap.set(u.id, { ...existing, ...u.updates });
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const labelUpdates = syncConnectorLabels(
|
|
1391
|
+
processedConnectors,
|
|
1392
|
+
tempMap
|
|
1393
|
+
);
|
|
1394
|
+
for (const lu of labelUpdates) {
|
|
1395
|
+
updates.push(lu);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return { updates, processedConnectorIds: processedConnectors };
|
|
1399
|
+
}
|
|
1400
|
+
let urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
1401
|
+
let nanoid = (size = 21) => {
|
|
1402
|
+
let id = "";
|
|
1403
|
+
let bytes = crypto.getRandomValues(new Uint8Array(size |= 0));
|
|
1404
|
+
while (size--) {
|
|
1405
|
+
id += urlAlphabet[bytes[size] & 63];
|
|
1406
|
+
}
|
|
1407
|
+
return id;
|
|
1408
|
+
};
|
|
1409
|
+
function generateId() {
|
|
1410
|
+
return nanoid(12);
|
|
1411
|
+
}
|
|
1412
|
+
const BUILTIN_TYPES = /* @__PURE__ */ new Set([
|
|
1413
|
+
"rectangle",
|
|
1414
|
+
"ellipse",
|
|
1415
|
+
"diamond",
|
|
1416
|
+
"line",
|
|
1417
|
+
"arrow",
|
|
1418
|
+
"freedraw",
|
|
1419
|
+
"text",
|
|
1420
|
+
"image"
|
|
1421
|
+
]);
|
|
1422
|
+
const VALID_TEXT_ALIGNS = /* @__PURE__ */ new Set(["left", "center", "right"]);
|
|
1423
|
+
const VALID_VERTICAL_ALIGNS = /* @__PURE__ */ new Set(["top", "middle", "bottom"]);
|
|
1424
|
+
const VALID_IMAGE_SCALES = /* @__PURE__ */ new Set(["fit", "fill", "stretch"]);
|
|
1425
|
+
const VALID_LINE_TYPES = /* @__PURE__ */ new Set(["sharp", "curved", "elbow"]);
|
|
1426
|
+
class ElementRegistryClass {
|
|
1427
|
+
constructor() {
|
|
1428
|
+
__publicField(this, "customs", /* @__PURE__ */ new Map());
|
|
1429
|
+
}
|
|
1430
|
+
// ── Registration ──────────────────────────────────────────
|
|
1431
|
+
/**
|
|
1432
|
+
* Register a custom element type.
|
|
1433
|
+
*
|
|
1434
|
+
* @throws If the type already exists and `allowOverride` is not `true`.
|
|
1435
|
+
*/
|
|
1436
|
+
register(config) {
|
|
1437
|
+
if (BUILTIN_TYPES.has(config.type) && !config.allowOverride) {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`[f1ow] Cannot register custom element type "${config.type}" — it conflicts with a built-in type. Set allowOverride: true if you intentionally want to replace the built-in validator.`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
if (this.customs.has(config.type) && !config.allowOverride) {
|
|
1443
|
+
throw new Error(
|
|
1444
|
+
`[f1ow] Element type "${config.type}" is already registered. Set allowOverride: true to replace the existing registration.`
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
this.customs.set(config.type, config);
|
|
1448
|
+
}
|
|
1449
|
+
// ── Queries ───────────────────────────────────────────────
|
|
1450
|
+
/** Returns `true` if the type is known (built-in or custom). */
|
|
1451
|
+
isRegistered(type) {
|
|
1452
|
+
return BUILTIN_TYPES.has(type) || this.customs.has(type);
|
|
1453
|
+
}
|
|
1454
|
+
/** Retrieve the custom config for a type (undefined for built-in types). */
|
|
1455
|
+
getCustomConfig(type) {
|
|
1456
|
+
return this.customs.get(type);
|
|
1457
|
+
}
|
|
1458
|
+
/** All registered type names, built-in first then custom. */
|
|
1459
|
+
getRegisteredTypes() {
|
|
1460
|
+
return [...BUILTIN_TYPES, ...this.customs.keys()];
|
|
1461
|
+
}
|
|
1462
|
+
// ── Validation ────────────────────────────────────────────
|
|
1463
|
+
/**
|
|
1464
|
+
* Validate a full element before it enters the canvas store.
|
|
1465
|
+
*
|
|
1466
|
+
* Checks:
|
|
1467
|
+
* 1. Non-null object with required base fields (id, type, x, y, w, h, rotation, style)
|
|
1468
|
+
* 2. `type` is a known/registered type
|
|
1469
|
+
* 3. Type-specific field checks for all 8 built-in types
|
|
1470
|
+
* 4. Custom `validate` callback (custom types only)
|
|
1471
|
+
*/
|
|
1472
|
+
validateElement(element) {
|
|
1473
|
+
if (!element || typeof element !== "object" || Array.isArray(element)) {
|
|
1474
|
+
return { valid: false, error: "Element must be a non-null object" };
|
|
1475
|
+
}
|
|
1476
|
+
const el = element;
|
|
1477
|
+
if (typeof el.id !== "string" || el.id.trim() === "") {
|
|
1478
|
+
return { valid: false, error: "Element id must be a non-empty string" };
|
|
1479
|
+
}
|
|
1480
|
+
const id = el.id;
|
|
1481
|
+
if (typeof el.type !== "string" || !this.isRegistered(el.type)) {
|
|
1482
|
+
return {
|
|
1483
|
+
valid: false,
|
|
1484
|
+
error: `Unknown element type "${el.type}". Valid types: ${this.getRegisteredTypes().join(", ")}`
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
if (typeof el.x !== "number" || !isFinite(el.x)) {
|
|
1488
|
+
return { valid: false, error: `"${id}": x must be a finite number` };
|
|
1489
|
+
}
|
|
1490
|
+
if (typeof el.y !== "number" || !isFinite(el.y)) {
|
|
1491
|
+
return { valid: false, error: `"${id}": y must be a finite number` };
|
|
1492
|
+
}
|
|
1493
|
+
if (typeof el.width !== "number" || !isFinite(el.width) || el.width < 0) {
|
|
1494
|
+
return { valid: false, error: `"${id}": width must be a non-negative finite number` };
|
|
1495
|
+
}
|
|
1496
|
+
if (typeof el.height !== "number" || !isFinite(el.height) || el.height < 0) {
|
|
1497
|
+
return { valid: false, error: `"${id}": height must be a non-negative finite number` };
|
|
1498
|
+
}
|
|
1499
|
+
if (typeof el.rotation !== "number" || !isFinite(el.rotation)) {
|
|
1500
|
+
return { valid: false, error: `"${id}": rotation must be a finite number` };
|
|
1501
|
+
}
|
|
1502
|
+
const styleCheck = this._validateStyle(id, el.style);
|
|
1503
|
+
if (!styleCheck.valid) return styleCheck;
|
|
1504
|
+
if (BUILTIN_TYPES.has(el.type)) {
|
|
1505
|
+
const typeCheck = this._validateBuiltinFields(el);
|
|
1506
|
+
if (!typeCheck.valid) return typeCheck;
|
|
1507
|
+
}
|
|
1508
|
+
const customCfg = this.customs.get(el.type);
|
|
1509
|
+
if (customCfg == null ? void 0 : customCfg.validate) {
|
|
1510
|
+
let result;
|
|
1511
|
+
try {
|
|
1512
|
+
result = customCfg.validate(el);
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
const name = customCfg.displayName ?? customCfg.type;
|
|
1515
|
+
return {
|
|
1516
|
+
valid: false,
|
|
1517
|
+
error: `Custom element "${name}" validator threw for "${id}": ${err instanceof Error ? err.message : String(err)}`
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
if (result !== true) {
|
|
1521
|
+
const name = customCfg.displayName ?? customCfg.type;
|
|
1522
|
+
return {
|
|
1523
|
+
valid: false,
|
|
1524
|
+
error: `Custom element "${name}" validation failed for "${id}": ${result}`
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return { valid: true };
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Validate a partial update before it is applied to an existing element.
|
|
1532
|
+
*
|
|
1533
|
+
* Prevents overwriting immutable fields (`id` and `type`), and checks
|
|
1534
|
+
* numeric fields for finiteness.
|
|
1535
|
+
*/
|
|
1536
|
+
validateUpdate(updates) {
|
|
1537
|
+
if ("id" in updates) {
|
|
1538
|
+
return { valid: false, error: "Cannot overwrite element id via updateElement" };
|
|
1539
|
+
}
|
|
1540
|
+
if ("type" in updates) {
|
|
1541
|
+
return {
|
|
1542
|
+
valid: false,
|
|
1543
|
+
error: "Cannot overwrite element type via updateElement — use convertElementType instead"
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
for (const key of ["x", "y", "width", "height", "rotation"]) {
|
|
1547
|
+
if (key in updates) {
|
|
1548
|
+
const v = updates[key];
|
|
1549
|
+
if (typeof v !== "number" || !isFinite(v)) {
|
|
1550
|
+
return { valid: false, error: `Update field "${key}" must be a finite number, got: ${v}` };
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return { valid: true };
|
|
1555
|
+
}
|
|
1556
|
+
// ── Defaults ──────────────────────────────────────────────
|
|
1557
|
+
/**
|
|
1558
|
+
* Merge custom `defaults` into the element.
|
|
1559
|
+
* Existing fields on the element take priority — defaults only fill gaps.
|
|
1560
|
+
* Only active when the element's type has a custom config with `defaults`.
|
|
1561
|
+
*/
|
|
1562
|
+
applyDefaults(element) {
|
|
1563
|
+
const type = element.type;
|
|
1564
|
+
if (!type) return element;
|
|
1565
|
+
const cfg = this.customs.get(type);
|
|
1566
|
+
if (!(cfg == null ? void 0 : cfg.defaults)) return element;
|
|
1567
|
+
return { ...cfg.defaults, ...element };
|
|
1568
|
+
}
|
|
1569
|
+
// ── Private helpers ───────────────────────────────────────
|
|
1570
|
+
_validateStyle(id, style) {
|
|
1571
|
+
if (!style || typeof style !== "object" || Array.isArray(style)) {
|
|
1572
|
+
return { valid: false, error: `"${id}": missing or invalid style object` };
|
|
1573
|
+
}
|
|
1574
|
+
const s = style;
|
|
1575
|
+
if (typeof s.strokeColor !== "string") {
|
|
1576
|
+
return { valid: false, error: `"${id}": style.strokeColor must be a string` };
|
|
1577
|
+
}
|
|
1578
|
+
if (typeof s.fillColor !== "string") {
|
|
1579
|
+
return { valid: false, error: `"${id}": style.fillColor must be a string` };
|
|
1580
|
+
}
|
|
1581
|
+
if (typeof s.strokeWidth !== "number" || !isFinite(s.strokeWidth) || s.strokeWidth < 0) {
|
|
1582
|
+
return { valid: false, error: `"${id}": style.strokeWidth must be a non-negative number` };
|
|
1583
|
+
}
|
|
1584
|
+
if (typeof s.opacity !== "number" || !isFinite(s.opacity) || s.opacity < 0 || s.opacity > 1) {
|
|
1585
|
+
return { valid: false, error: `"${id}": style.opacity must be between 0 and 1` };
|
|
1586
|
+
}
|
|
1587
|
+
if (typeof s.fontSize !== "number" || !isFinite(s.fontSize) || s.fontSize <= 0) {
|
|
1588
|
+
return { valid: false, error: `"${id}": style.fontSize must be a positive number` };
|
|
1589
|
+
}
|
|
1590
|
+
return { valid: true };
|
|
1591
|
+
}
|
|
1592
|
+
_validateBuiltinFields(el) {
|
|
1593
|
+
const type = el.type;
|
|
1594
|
+
const id = el.id;
|
|
1595
|
+
switch (type) {
|
|
1596
|
+
case "rectangle":
|
|
1597
|
+
if (typeof el.cornerRadius !== "number" || !isFinite(el.cornerRadius) || el.cornerRadius < 0) {
|
|
1598
|
+
return {
|
|
1599
|
+
valid: false,
|
|
1600
|
+
error: `rectangle "${id}": cornerRadius must be a non-negative number`
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
break;
|
|
1604
|
+
case "line":
|
|
1605
|
+
case "arrow": {
|
|
1606
|
+
if (!Array.isArray(el.points) || el.points.length < 4 || el.points.some((p) => typeof p !== "number" || !isFinite(p))) {
|
|
1607
|
+
return {
|
|
1608
|
+
valid: false,
|
|
1609
|
+
error: `${type} "${id}": points must be an array of at least 4 finite numbers [x1,y1,x2,y2,...]`
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
if (typeof el.lineType !== "string" || !VALID_LINE_TYPES.has(el.lineType)) {
|
|
1613
|
+
return {
|
|
1614
|
+
valid: false,
|
|
1615
|
+
error: `${type} "${id}": lineType must be one of ${[...VALID_LINE_TYPES].join(" | ")}`
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
break;
|
|
1619
|
+
}
|
|
1620
|
+
case "freedraw": {
|
|
1621
|
+
if (!Array.isArray(el.points) || el.points.length < 2 || el.points.some((p) => typeof p !== "number" || !isFinite(p))) {
|
|
1622
|
+
return {
|
|
1623
|
+
valid: false,
|
|
1624
|
+
error: `freedraw "${id}": points must be an array of at least 2 finite numbers`
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
break;
|
|
1628
|
+
}
|
|
1629
|
+
case "text": {
|
|
1630
|
+
if (typeof el.text !== "string") {
|
|
1631
|
+
return { valid: false, error: `text "${id}": text must be a string` };
|
|
1632
|
+
}
|
|
1633
|
+
if (!VALID_TEXT_ALIGNS.has(el.textAlign)) {
|
|
1634
|
+
return {
|
|
1635
|
+
valid: false,
|
|
1636
|
+
error: `text "${id}": textAlign must be one of ${[...VALID_TEXT_ALIGNS].join(" | ")}`
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
if (!VALID_VERTICAL_ALIGNS.has(el.verticalAlign)) {
|
|
1640
|
+
return {
|
|
1641
|
+
valid: false,
|
|
1642
|
+
error: `text "${id}": verticalAlign must be one of ${[...VALID_VERTICAL_ALIGNS].join(" | ")}`
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1647
|
+
case "image": {
|
|
1648
|
+
if (typeof el.src !== "string" || el.src.trim() === "") {
|
|
1649
|
+
return { valid: false, error: `image "${id}": src must be a non-empty string` };
|
|
1650
|
+
}
|
|
1651
|
+
if (!VALID_IMAGE_SCALES.has(el.scaleMode)) {
|
|
1652
|
+
return {
|
|
1653
|
+
valid: false,
|
|
1654
|
+
error: `image "${id}": scaleMode must be one of ${[...VALID_IMAGE_SCALES].join(" | ")}`
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return { valid: true };
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
const elementRegistry = new ElementRegistryClass();
|
|
1664
|
+
function deepCloneElement(el) {
|
|
1665
|
+
const clone = { ...el };
|
|
1666
|
+
if (clone.style) clone.style = { ...clone.style };
|
|
1667
|
+
if (clone.boundElements) clone.boundElements = clone.boundElements.map((be) => ({ ...be }));
|
|
1668
|
+
if (clone.startBinding) clone.startBinding = { ...clone.startBinding };
|
|
1669
|
+
if (clone.endBinding) clone.endBinding = { ...clone.endBinding };
|
|
1670
|
+
if (clone.groupIds) clone.groupIds = [...clone.groupIds];
|
|
1671
|
+
if (Array.isArray(clone.points)) {
|
|
1672
|
+
clone.points = [...clone.points];
|
|
1673
|
+
}
|
|
1674
|
+
if (Array.isArray(clone.pressures)) {
|
|
1675
|
+
clone.pressures = [...clone.pressures];
|
|
1676
|
+
}
|
|
1677
|
+
if (clone.crop) clone.crop = { ...clone.crop };
|
|
1678
|
+
return clone;
|
|
1679
|
+
}
|
|
1680
|
+
function cloneAndRemapElements(originals, allElements, offset = 20) {
|
|
1681
|
+
const originalIds = new Set(originals.map((el) => el.id));
|
|
1682
|
+
const extraTextIds = /* @__PURE__ */ new Set();
|
|
1683
|
+
for (const el of originals) {
|
|
1684
|
+
if (el.boundElements) {
|
|
1685
|
+
for (const be of el.boundElements) {
|
|
1686
|
+
if (be.type === "text" && !originalIds.has(be.id)) {
|
|
1687
|
+
extraTextIds.add(be.id);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const toDuplicate = [
|
|
1693
|
+
...originals,
|
|
1694
|
+
...allElements.filter((el) => extraTextIds.has(el.id))
|
|
1695
|
+
];
|
|
1696
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1697
|
+
for (const el of toDuplicate) {
|
|
1698
|
+
idMap.set(el.id, generateId());
|
|
1699
|
+
}
|
|
1700
|
+
const groupIdMap = /* @__PURE__ */ new Map();
|
|
1701
|
+
for (const el of toDuplicate) {
|
|
1702
|
+
if (el.groupIds) {
|
|
1703
|
+
for (const gid of el.groupIds) {
|
|
1704
|
+
if (!groupIdMap.has(gid)) {
|
|
1705
|
+
groupIdMap.set(gid, generateId());
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
const clones = toDuplicate.map((el) => {
|
|
1711
|
+
const newId = idMap.get(el.id);
|
|
1712
|
+
const dup = deepCloneElement(el);
|
|
1713
|
+
dup.id = newId;
|
|
1714
|
+
dup.x = el.x + offset;
|
|
1715
|
+
dup.y = el.y + offset;
|
|
1716
|
+
if (dup.containerId && idMap.has(dup.containerId)) {
|
|
1717
|
+
dup.containerId = idMap.get(dup.containerId);
|
|
1718
|
+
} else if (dup.containerId) {
|
|
1719
|
+
dup.containerId = null;
|
|
1720
|
+
}
|
|
1721
|
+
if (dup.boundElements) {
|
|
1722
|
+
dup.boundElements = dup.boundElements.map(
|
|
1723
|
+
(be) => idMap.has(be.id) ? { ...be, id: idMap.get(be.id) } : null
|
|
1724
|
+
).filter(Boolean);
|
|
1725
|
+
if (dup.boundElements.length === 0) dup.boundElements = null;
|
|
1726
|
+
}
|
|
1727
|
+
if (dup.startBinding && idMap.has(dup.startBinding.elementId)) {
|
|
1728
|
+
dup.startBinding = { ...dup.startBinding, elementId: idMap.get(dup.startBinding.elementId) };
|
|
1729
|
+
} else if (dup.startBinding) {
|
|
1730
|
+
dup.startBinding = null;
|
|
1731
|
+
}
|
|
1732
|
+
if (dup.endBinding && idMap.has(dup.endBinding.elementId)) {
|
|
1733
|
+
dup.endBinding = { ...dup.endBinding, elementId: idMap.get(dup.endBinding.elementId) };
|
|
1734
|
+
} else if (dup.endBinding) {
|
|
1735
|
+
dup.endBinding = null;
|
|
1736
|
+
}
|
|
1737
|
+
if (dup.groupIds && dup.groupIds.length > 0) {
|
|
1738
|
+
dup.groupIds = dup.groupIds.map((gid) => groupIdMap.get(gid) ?? gid);
|
|
1739
|
+
}
|
|
1740
|
+
return dup;
|
|
1741
|
+
});
|
|
1742
|
+
const selectedCloneIds = clones.filter((c) => {
|
|
1743
|
+
var _a;
|
|
1744
|
+
const origId = (_a = [...idMap.entries()].find(([, v]) => v === c.id)) == null ? void 0 : _a[0];
|
|
1745
|
+
return origId ? originalIds.has(origId) : false;
|
|
1746
|
+
}).map((c) => c.id);
|
|
1747
|
+
return { clones, idMap, selectedCloneIds };
|
|
1748
|
+
}
|
|
1749
|
+
function getElementAABB(el) {
|
|
1750
|
+
if ((el.type === "line" || el.type === "arrow") && "points" in el) {
|
|
1751
|
+
const pts = el.points;
|
|
1752
|
+
let minX = 0, maxX = 0, minY = 0, maxY = 0;
|
|
1753
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
1754
|
+
const px = pts[i];
|
|
1755
|
+
const py = pts[i + 1];
|
|
1756
|
+
if (px < minX) minX = px;
|
|
1757
|
+
if (px > maxX) maxX = px;
|
|
1758
|
+
if (py < minY) minY = py;
|
|
1759
|
+
if (py > maxY) maxY = py;
|
|
1760
|
+
}
|
|
1761
|
+
return {
|
|
1762
|
+
minX: el.x + minX,
|
|
1763
|
+
minY: el.y + minY,
|
|
1764
|
+
maxX: el.x + maxX,
|
|
1765
|
+
maxY: el.y + maxY
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
if (el.type === "freedraw" && "points" in el) {
|
|
1769
|
+
if (el.isComplete === false) {
|
|
1770
|
+
const pts = el.points;
|
|
1771
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1772
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
1773
|
+
const px = pts[i];
|
|
1774
|
+
const py = pts[i + 1];
|
|
1775
|
+
if (px < minX) minX = px;
|
|
1776
|
+
if (px > maxX) maxX = px;
|
|
1777
|
+
if (py < minY) minY = py;
|
|
1778
|
+
if (py > maxY) maxY = py;
|
|
1779
|
+
}
|
|
1780
|
+
if (minX === Infinity) {
|
|
1781
|
+
return { minX: el.x, minY: el.y, maxX: el.x + 1, maxY: el.y + 1 };
|
|
1782
|
+
}
|
|
1783
|
+
return { minX, minY, maxX, maxY };
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
minX: el.x,
|
|
1787
|
+
minY: el.y,
|
|
1788
|
+
maxX: el.x + el.width,
|
|
1789
|
+
maxY: el.y + el.height
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
return {
|
|
1793
|
+
minX: el.x,
|
|
1794
|
+
minY: el.y,
|
|
1795
|
+
maxX: el.x + el.width,
|
|
1796
|
+
maxY: el.y + el.height
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const ZOOM_STEPS = [0.1, 0.25, 0.33, 0.5, 0.67, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
|
|
1800
|
+
const DEFAULT_ANIMATION_DURATION = 280;
|
|
1801
|
+
function zoomAtPoint({ viewport, point, targetScale }) {
|
|
1802
|
+
const clampedScale = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, targetScale));
|
|
1803
|
+
const wx = (point.x - viewport.x) / viewport.scale;
|
|
1804
|
+
const wy = (point.y - viewport.y) / viewport.scale;
|
|
1805
|
+
return {
|
|
1806
|
+
scale: clampedScale,
|
|
1807
|
+
x: point.x - wx * clampedScale,
|
|
1808
|
+
y: point.y - wy * clampedScale
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
function getNextZoomStep(currentScale) {
|
|
1812
|
+
for (const step of ZOOM_STEPS) {
|
|
1813
|
+
if (step > currentScale + 0.01) return step;
|
|
1814
|
+
}
|
|
1815
|
+
return MAX_ZOOM;
|
|
1816
|
+
}
|
|
1817
|
+
function getPrevZoomStep(currentScale) {
|
|
1818
|
+
for (let i = ZOOM_STEPS.length - 1; i >= 0; i--) {
|
|
1819
|
+
if (ZOOM_STEPS[i] < currentScale - 0.01) return ZOOM_STEPS[i];
|
|
1820
|
+
}
|
|
1821
|
+
return MIN_ZOOM;
|
|
1822
|
+
}
|
|
1823
|
+
function getElementsBounds(elements) {
|
|
1824
|
+
if (elements.length === 0) return null;
|
|
1825
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1826
|
+
for (const el of elements) {
|
|
1827
|
+
const bb = getElementAABB(el);
|
|
1828
|
+
if (bb.minX < minX) minX = bb.minX;
|
|
1829
|
+
if (bb.minY < minY) minY = bb.minY;
|
|
1830
|
+
if (bb.maxX > maxX) maxX = bb.maxX;
|
|
1831
|
+
if (bb.maxY > maxY) maxY = bb.maxY;
|
|
1832
|
+
}
|
|
1833
|
+
return { minX, minY, maxX, maxY };
|
|
1834
|
+
}
|
|
1835
|
+
function computeZoomToFit(bounds, stageWidth, stageHeight, options = {}) {
|
|
1836
|
+
const padding = options.padding ?? 50;
|
|
1837
|
+
const maxZoom = options.maxZoom ?? 2;
|
|
1838
|
+
const bbW = bounds.maxX - bounds.minX;
|
|
1839
|
+
const bbH = bounds.maxY - bounds.minY;
|
|
1840
|
+
if (bbW === 0 && bbH === 0) {
|
|
1841
|
+
return {
|
|
1842
|
+
scale: 1,
|
|
1843
|
+
x: stageWidth / 2 - bounds.minX,
|
|
1844
|
+
y: stageHeight / 2 - bounds.minY
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const scaleX = (stageWidth - padding * 2) / (bbW || 1);
|
|
1848
|
+
const scaleY = (stageHeight - padding * 2) / (bbH || 1);
|
|
1849
|
+
const scale = Math.min(Math.min(scaleX, scaleY), maxZoom);
|
|
1850
|
+
const clampedScale = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, scale));
|
|
1851
|
+
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
1852
|
+
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
1853
|
+
return {
|
|
1854
|
+
scale: clampedScale,
|
|
1855
|
+
x: stageWidth / 2 - centerX * clampedScale,
|
|
1856
|
+
y: stageHeight / 2 - centerY * clampedScale
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
function easeOutCubic(t) {
|
|
1860
|
+
return 1 - Math.pow(1 - t, 3);
|
|
1861
|
+
}
|
|
1862
|
+
let activeAnimationId = null;
|
|
1863
|
+
function animateViewport(from, to, setViewport, duration = DEFAULT_ANIMATION_DURATION) {
|
|
1864
|
+
if (activeAnimationId !== null) {
|
|
1865
|
+
cancelAnimationFrame(activeAnimationId);
|
|
1866
|
+
activeAnimationId = null;
|
|
1867
|
+
}
|
|
1868
|
+
const startTime = performance.now();
|
|
1869
|
+
const tick = (now) => {
|
|
1870
|
+
const elapsed = now - startTime;
|
|
1871
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
1872
|
+
const eased = easeOutCubic(progress);
|
|
1873
|
+
const interpolated = {
|
|
1874
|
+
x: from.x + (to.x - from.x) * eased,
|
|
1875
|
+
y: from.y + (to.y - from.y) * eased,
|
|
1876
|
+
scale: from.scale + (to.scale - from.scale) * eased
|
|
1877
|
+
};
|
|
1878
|
+
setViewport(interpolated);
|
|
1879
|
+
if (progress < 1) {
|
|
1880
|
+
activeAnimationId = requestAnimationFrame(tick);
|
|
1881
|
+
} else {
|
|
1882
|
+
activeAnimationId = null;
|
|
1883
|
+
setViewport(to);
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
activeAnimationId = requestAnimationFrame(tick);
|
|
1887
|
+
return () => {
|
|
1888
|
+
if (activeAnimationId !== null) {
|
|
1889
|
+
cancelAnimationFrame(activeAnimationId);
|
|
1890
|
+
activeAnimationId = null;
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
function cancelViewportAnimation() {
|
|
1895
|
+
if (activeAnimationId !== null) {
|
|
1896
|
+
cancelAnimationFrame(activeAnimationId);
|
|
1897
|
+
activeAnimationId = null;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
const GEOMETRY_KEYS = /* @__PURE__ */ new Set([
|
|
1901
|
+
"x",
|
|
1902
|
+
"y",
|
|
1903
|
+
"width",
|
|
1904
|
+
"height",
|
|
1905
|
+
"rotation",
|
|
1906
|
+
"ports",
|
|
1907
|
+
"points",
|
|
1908
|
+
"cornerRadius",
|
|
1909
|
+
"radiusX",
|
|
1910
|
+
"radiusY"
|
|
1911
|
+
]);
|
|
1912
|
+
function expandWithBoundChildren(ids, elements) {
|
|
1913
|
+
const idSet = new Set(ids);
|
|
1914
|
+
for (const el of elements) {
|
|
1915
|
+
if (el.type === "text" && el.containerId && idSet.has(el.containerId)) {
|
|
1916
|
+
idSet.add(el.id);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return idSet;
|
|
1920
|
+
}
|
|
1921
|
+
const MAX_HISTORY = 100;
|
|
1922
|
+
function cloneElement(el) {
|
|
1923
|
+
if (el.type === "image" && "src" in el) {
|
|
1924
|
+
const { src, ...rest } = el;
|
|
1925
|
+
const cloned = structuredClone(rest);
|
|
1926
|
+
cloned.src = src;
|
|
1927
|
+
return cloned;
|
|
1928
|
+
}
|
|
1929
|
+
return structuredClone(el);
|
|
1930
|
+
}
|
|
1931
|
+
function orderOf(elements) {
|
|
1932
|
+
return elements.map((el) => el.id);
|
|
1933
|
+
}
|
|
1934
|
+
function sameOrder(a, b) {
|
|
1935
|
+
return a.length === b.length && a.every((id, index) => id === b[index]);
|
|
1936
|
+
}
|
|
1937
|
+
function restoreOrder(elements, order) {
|
|
1938
|
+
const byId = new Map(elements.map((el) => [el.id, el]));
|
|
1939
|
+
const ordered = [];
|
|
1940
|
+
for (const id of order) {
|
|
1941
|
+
const el = byId.get(id);
|
|
1942
|
+
if (el) {
|
|
1943
|
+
ordered.push(el);
|
|
1944
|
+
byId.delete(id);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
for (const el of elements) {
|
|
1948
|
+
if (byId.has(el.id)) ordered.push(el);
|
|
1949
|
+
}
|
|
1950
|
+
return ordered;
|
|
1951
|
+
}
|
|
1952
|
+
function validSelectedIds(selectedIds, elements) {
|
|
1953
|
+
const existingIds = new Set(elements.map((el) => el.id));
|
|
1954
|
+
return selectedIds.filter((id) => existingIds.has(id));
|
|
1955
|
+
}
|
|
1956
|
+
function selectedEditableElements(ids, elements) {
|
|
1957
|
+
const idSet = new Set(ids);
|
|
1958
|
+
return elements.filter((el) => idSet.has(el.id) && !el.isLocked);
|
|
1959
|
+
}
|
|
1960
|
+
function elementBounds(el) {
|
|
1961
|
+
if ((el.type === "line" || el.type === "arrow" || el.type === "freedraw") && el.points.length >= 2) {
|
|
1962
|
+
let minLocalX = Infinity;
|
|
1963
|
+
let minLocalY = Infinity;
|
|
1964
|
+
let maxLocalX = -Infinity;
|
|
1965
|
+
let maxLocalY = -Infinity;
|
|
1966
|
+
for (let i = 0; i < el.points.length; i += 2) {
|
|
1967
|
+
minLocalX = Math.min(minLocalX, el.points[i]);
|
|
1968
|
+
minLocalY = Math.min(minLocalY, el.points[i + 1]);
|
|
1969
|
+
maxLocalX = Math.max(maxLocalX, el.points[i]);
|
|
1970
|
+
maxLocalY = Math.max(maxLocalY, el.points[i + 1]);
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
minX: el.x + minLocalX,
|
|
1974
|
+
minY: el.y + minLocalY,
|
|
1975
|
+
maxX: el.x + maxLocalX,
|
|
1976
|
+
maxY: el.y + maxLocalY
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
return {
|
|
1980
|
+
minX: el.x,
|
|
1981
|
+
minY: el.y,
|
|
1982
|
+
maxX: el.x + el.width,
|
|
1983
|
+
maxY: el.y + el.height
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
function boundsOf(elements) {
|
|
1987
|
+
if (elements.length === 0) return null;
|
|
1988
|
+
let minX = Infinity;
|
|
1989
|
+
let minY = Infinity;
|
|
1990
|
+
let maxX = -Infinity;
|
|
1991
|
+
let maxY = -Infinity;
|
|
1992
|
+
for (const el of elements) {
|
|
1993
|
+
const bounds = elementBounds(el);
|
|
1994
|
+
minX = Math.min(minX, bounds.minX);
|
|
1995
|
+
minY = Math.min(minY, bounds.minY);
|
|
1996
|
+
maxX = Math.max(maxX, bounds.maxX);
|
|
1997
|
+
maxY = Math.max(maxY, bounds.maxY);
|
|
1998
|
+
}
|
|
1999
|
+
return { minX, minY, maxX, maxY };
|
|
2000
|
+
}
|
|
2001
|
+
function normalizeRotation(rotation) {
|
|
2002
|
+
return (rotation % 360 + 360) % 360;
|
|
2003
|
+
}
|
|
2004
|
+
function flipPoints(points, min, max, axisOffset) {
|
|
2005
|
+
if (!points) return void 0;
|
|
2006
|
+
return points.map((value, index) => index % 2 === axisOffset ? min + max - value : value);
|
|
2007
|
+
}
|
|
2008
|
+
function syncMovedElements(ids, get) {
|
|
2009
|
+
const state = get();
|
|
2010
|
+
const sync = syncAfterDrag(ids, state.elements);
|
|
2011
|
+
if (sync.updates.length > 0) {
|
|
2012
|
+
state.batchUpdateElements(sync.updates);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
function createCanvasStore() {
|
|
2016
|
+
return create((set, get) => ({
|
|
2017
|
+
// ─── Initial State ────────────────────────────────────────
|
|
2018
|
+
elements: [],
|
|
2019
|
+
selectedIds: [],
|
|
2020
|
+
activeTool: "select",
|
|
2021
|
+
currentStyle: { ...DEFAULT_STYLE },
|
|
2022
|
+
currentLineType: "sharp",
|
|
2023
|
+
currentStartArrowhead: null,
|
|
2024
|
+
currentEndArrowhead: "arrow",
|
|
2025
|
+
viewport: { x: 0, y: 0, scale: 1 },
|
|
2026
|
+
isDrawing: false,
|
|
2027
|
+
drawStart: null,
|
|
2028
|
+
history: [],
|
|
2029
|
+
historyIndex: -1,
|
|
2030
|
+
_historyBaseline: /* @__PURE__ */ new Map(),
|
|
2031
|
+
_historyOrderBaseline: [],
|
|
2032
|
+
_historyPaused: false,
|
|
2033
|
+
showGrid: false,
|
|
2034
|
+
// ─── Element Actions ──────────────────────────────────────
|
|
2035
|
+
addElement: (element) => {
|
|
2036
|
+
const validation = elementRegistry.validateElement(element);
|
|
2037
|
+
if (!validation.valid) {
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
const finalElement = elementRegistry.applyDefaults(element);
|
|
2041
|
+
set((state) => ({
|
|
2042
|
+
elements: [...state.elements, finalElement]
|
|
2043
|
+
}));
|
|
2044
|
+
get().pushHistory();
|
|
2045
|
+
},
|
|
2046
|
+
updateElement: (id, updates) => {
|
|
2047
|
+
const updateValidation = elementRegistry.validateUpdate(updates);
|
|
2048
|
+
if (!updateValidation.valid) {
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const hasGeometryChange = Object.keys(updates).some((k) => GEOMETRY_KEYS.has(k));
|
|
2052
|
+
set((state) => {
|
|
2053
|
+
const elements = state.elements;
|
|
2054
|
+
const idx = elements.findIndex((el) => el.id === id);
|
|
2055
|
+
if (idx === -1) return state;
|
|
2056
|
+
const base = elements[idx];
|
|
2057
|
+
const versionBump = hasGeometryChange ? { version: (base.version ?? 0) + 1 } : {};
|
|
2058
|
+
const updated = { ...base, ...updates, ...versionBump };
|
|
2059
|
+
if (updated === base) return state;
|
|
2060
|
+
const next = elements.slice();
|
|
2061
|
+
next[idx] = updated;
|
|
2062
|
+
return { elements: next };
|
|
2063
|
+
});
|
|
2064
|
+
},
|
|
2065
|
+
batchUpdateElements: (batchUpdates) => {
|
|
2066
|
+
if (batchUpdates.length === 0) return;
|
|
2067
|
+
if (batchUpdates.length === 1) {
|
|
2068
|
+
get().updateElement(batchUpdates[0].id, batchUpdates[0].updates);
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const validUpdates = batchUpdates.filter(({ id, updates }) => {
|
|
2072
|
+
const v = elementRegistry.validateUpdate(updates);
|
|
2073
|
+
if (!v.valid) {
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
return true;
|
|
2077
|
+
});
|
|
2078
|
+
if (validUpdates.length === 0) return;
|
|
2079
|
+
set((state) => {
|
|
2080
|
+
const elements = state.elements;
|
|
2081
|
+
const idxMap = /* @__PURE__ */ new Map();
|
|
2082
|
+
for (let i = 0; i < elements.length; i++) {
|
|
2083
|
+
idxMap.set(elements[i].id, i);
|
|
2084
|
+
}
|
|
2085
|
+
let next = null;
|
|
2086
|
+
for (const { id, updates } of validUpdates) {
|
|
2087
|
+
const idx = idxMap.get(id);
|
|
2088
|
+
if (idx === void 0) continue;
|
|
2089
|
+
const src = next ? next[idx] : elements[idx];
|
|
2090
|
+
const hasGeometryChange = Object.keys(updates).some((k) => GEOMETRY_KEYS.has(k));
|
|
2091
|
+
const versionBump = hasGeometryChange ? { version: (src.version ?? 0) + 1 } : {};
|
|
2092
|
+
const updated = { ...src, ...updates, ...versionBump };
|
|
2093
|
+
if (updated === src) continue;
|
|
2094
|
+
if (!next) next = elements.slice();
|
|
2095
|
+
next[idx] = updated;
|
|
2096
|
+
}
|
|
2097
|
+
return next ? { elements: next } : state;
|
|
2098
|
+
});
|
|
2099
|
+
},
|
|
2100
|
+
deleteElements: (ids) => {
|
|
2101
|
+
const deletedSet = new Set(ids);
|
|
2102
|
+
const { elements: current } = get();
|
|
2103
|
+
for (const el of current) {
|
|
2104
|
+
if (deletedSet.has(el.id) && el.boundElements) {
|
|
2105
|
+
for (const be of el.boundElements) {
|
|
2106
|
+
if (be.type === "text") {
|
|
2107
|
+
deletedSet.add(be.id);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
for (const el of current) {
|
|
2113
|
+
if (el.type === "text" && "containerId" in el && el.containerId && deletedSet.has(el.containerId)) {
|
|
2114
|
+
deletedSet.add(el.id);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
set((state) => {
|
|
2118
|
+
const remaining = state.elements.filter((el) => !deletedSet.has(el.id));
|
|
2119
|
+
const cleaned = clearBindingsForDeletedElements(deletedSet, remaining);
|
|
2120
|
+
return {
|
|
2121
|
+
elements: cleaned,
|
|
2122
|
+
selectedIds: state.selectedIds.filter((id) => !deletedSet.has(id))
|
|
2123
|
+
};
|
|
2124
|
+
});
|
|
2125
|
+
get().pushHistory();
|
|
2126
|
+
},
|
|
2127
|
+
setElements: (elements) => {
|
|
2128
|
+
if (elements === get().elements) return;
|
|
2129
|
+
const valid = elements.filter((el) => {
|
|
2130
|
+
const result = elementRegistry.validateElement(el);
|
|
2131
|
+
if (!result.valid && false) ;
|
|
2132
|
+
return result.valid;
|
|
2133
|
+
});
|
|
2134
|
+
const baseline = /* @__PURE__ */ new Map();
|
|
2135
|
+
for (const el of valid) {
|
|
2136
|
+
baseline.set(el.id, el);
|
|
2137
|
+
}
|
|
2138
|
+
set({ elements: valid, _historyBaseline: baseline, _historyOrderBaseline: orderOf(valid) });
|
|
2139
|
+
},
|
|
2140
|
+
duplicateElements: (ids) => {
|
|
2141
|
+
const { elements } = get();
|
|
2142
|
+
const originals = elements.filter((el) => ids.includes(el.id));
|
|
2143
|
+
const { clones, selectedCloneIds } = cloneAndRemapElements(originals, elements);
|
|
2144
|
+
set((state) => ({
|
|
2145
|
+
elements: [...state.elements, ...clones],
|
|
2146
|
+
selectedIds: selectedCloneIds.length > 0 ? selectedCloneIds : clones.map((d) => d.id)
|
|
2147
|
+
}));
|
|
2148
|
+
get().pushHistory();
|
|
2149
|
+
},
|
|
2150
|
+
convertElementType: (ids, targetType) => {
|
|
2151
|
+
const shapeTypes = /* @__PURE__ */ new Set(["rectangle", "ellipse", "diamond"]);
|
|
2152
|
+
if (!shapeTypes.has(targetType)) return;
|
|
2153
|
+
set((state) => ({
|
|
2154
|
+
elements: state.elements.map((el) => {
|
|
2155
|
+
if (!ids.includes(el.id)) return el;
|
|
2156
|
+
if (!shapeTypes.has(el.type)) return el;
|
|
2157
|
+
if (el.type === targetType) return el;
|
|
2158
|
+
const { cornerRadius: _cr, ...sharedFields } = el;
|
|
2159
|
+
const base = { ...sharedFields, type: targetType };
|
|
2160
|
+
if (targetType === "rectangle") {
|
|
2161
|
+
return { ...base, cornerRadius: 0 };
|
|
2162
|
+
}
|
|
2163
|
+
return base;
|
|
2164
|
+
})
|
|
2165
|
+
}));
|
|
2166
|
+
get().pushHistory();
|
|
2167
|
+
},
|
|
2168
|
+
bringToFront: (ids) => {
|
|
2169
|
+
set((state) => {
|
|
2170
|
+
const fullIds = expandWithBoundChildren(ids, state.elements);
|
|
2171
|
+
const others = state.elements.filter((el) => !fullIds.has(el.id));
|
|
2172
|
+
const targets = state.elements.filter((el) => fullIds.has(el.id));
|
|
2173
|
+
return { elements: [...others, ...targets] };
|
|
2174
|
+
});
|
|
2175
|
+
get().pushHistory();
|
|
2176
|
+
},
|
|
2177
|
+
sendToBack: (ids) => {
|
|
2178
|
+
set((state) => {
|
|
2179
|
+
const fullIds = expandWithBoundChildren(ids, state.elements);
|
|
2180
|
+
const others = state.elements.filter((el) => !fullIds.has(el.id));
|
|
2181
|
+
const targets = state.elements.filter((el) => fullIds.has(el.id));
|
|
2182
|
+
return { elements: [...targets, ...others] };
|
|
2183
|
+
});
|
|
2184
|
+
get().pushHistory();
|
|
2185
|
+
},
|
|
2186
|
+
bringForward: (ids) => {
|
|
2187
|
+
set((state) => {
|
|
2188
|
+
const elems = [...state.elements];
|
|
2189
|
+
const idSet = expandWithBoundChildren(ids, state.elements);
|
|
2190
|
+
for (let i = elems.length - 2; i >= 0; i--) {
|
|
2191
|
+
if (idSet.has(elems[i].id) && !idSet.has(elems[i + 1].id)) {
|
|
2192
|
+
[elems[i], elems[i + 1]] = [elems[i + 1], elems[i]];
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
return { elements: elems };
|
|
2196
|
+
});
|
|
2197
|
+
get().pushHistory();
|
|
2198
|
+
},
|
|
2199
|
+
sendBackward: (ids) => {
|
|
2200
|
+
set((state) => {
|
|
2201
|
+
const elems = [...state.elements];
|
|
2202
|
+
const idSet = expandWithBoundChildren(ids, state.elements);
|
|
2203
|
+
for (let i = 1; i < elems.length; i++) {
|
|
2204
|
+
if (idSet.has(elems[i].id) && !idSet.has(elems[i - 1].id)) {
|
|
2205
|
+
[elems[i], elems[i - 1]] = [elems[i - 1], elems[i]];
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return { elements: elems };
|
|
2209
|
+
});
|
|
2210
|
+
get().pushHistory();
|
|
2211
|
+
},
|
|
2212
|
+
// ─── Transforms ──────────────────────────────────────────
|
|
2213
|
+
alignElements: (ids, mode) => {
|
|
2214
|
+
const movedIds = [];
|
|
2215
|
+
set((state) => {
|
|
2216
|
+
const targets = selectedEditableElements(ids, state.elements);
|
|
2217
|
+
if (targets.length < 2) return state;
|
|
2218
|
+
const bounds = boundsOf(targets);
|
|
2219
|
+
if (!bounds) return state;
|
|
2220
|
+
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
2221
|
+
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
2222
|
+
const targetIds = new Set(targets.map((el) => el.id));
|
|
2223
|
+
const targetBounds = new Map(targets.map((el) => [el.id, elementBounds(el)]));
|
|
2224
|
+
let changed = false;
|
|
2225
|
+
const elements = state.elements.map((el) => {
|
|
2226
|
+
if (!targetIds.has(el.id)) return el;
|
|
2227
|
+
const elBounds = targetBounds.get(el.id);
|
|
2228
|
+
if (!elBounds) return el;
|
|
2229
|
+
let x = el.x;
|
|
2230
|
+
let y = el.y;
|
|
2231
|
+
if (mode === "left") x += bounds.minX - elBounds.minX;
|
|
2232
|
+
if (mode === "centerH") x += centerX - (elBounds.minX + elBounds.maxX) / 2;
|
|
2233
|
+
if (mode === "right") x += bounds.maxX - elBounds.maxX;
|
|
2234
|
+
if (mode === "top") y += bounds.minY - elBounds.minY;
|
|
2235
|
+
if (mode === "centerV") y += centerY - (elBounds.minY + elBounds.maxY) / 2;
|
|
2236
|
+
if (mode === "bottom") y += bounds.maxY - elBounds.maxY;
|
|
2237
|
+
if (x === el.x && y === el.y) return el;
|
|
2238
|
+
changed = true;
|
|
2239
|
+
movedIds.push(el.id);
|
|
2240
|
+
return { ...el, x, y, version: (el.version ?? 0) + 1 };
|
|
2241
|
+
});
|
|
2242
|
+
return changed ? { elements } : state;
|
|
2243
|
+
});
|
|
2244
|
+
if (movedIds.length > 0) {
|
|
2245
|
+
syncMovedElements(movedIds, get);
|
|
2246
|
+
get().pushHistory();
|
|
2247
|
+
}
|
|
2248
|
+
},
|
|
2249
|
+
rotateElements: (ids, deltaDegrees) => {
|
|
2250
|
+
const movedIds = [];
|
|
2251
|
+
set((state) => {
|
|
2252
|
+
const idSet = new Set(ids);
|
|
2253
|
+
let changed = false;
|
|
2254
|
+
const elements = state.elements.map((el) => {
|
|
2255
|
+
if (!idSet.has(el.id) || el.isLocked) return el;
|
|
2256
|
+
const rotation = normalizeRotation((el.rotation ?? 0) + deltaDegrees);
|
|
2257
|
+
if (rotation === el.rotation) return el;
|
|
2258
|
+
changed = true;
|
|
2259
|
+
movedIds.push(el.id);
|
|
2260
|
+
return { ...el, rotation, version: (el.version ?? 0) + 1 };
|
|
2261
|
+
});
|
|
2262
|
+
return changed ? { elements } : state;
|
|
2263
|
+
});
|
|
2264
|
+
if (movedIds.length > 0) {
|
|
2265
|
+
syncMovedElements(movedIds, get);
|
|
2266
|
+
get().pushHistory();
|
|
2267
|
+
}
|
|
2268
|
+
},
|
|
2269
|
+
flipElements: (ids, axis) => {
|
|
2270
|
+
const movedIds = [];
|
|
2271
|
+
set((state) => {
|
|
2272
|
+
const targets = selectedEditableElements(ids, state.elements);
|
|
2273
|
+
if (targets.length === 0) return state;
|
|
2274
|
+
const bounds = boundsOf(targets);
|
|
2275
|
+
if (!bounds) return state;
|
|
2276
|
+
const targetIds = new Set(targets.map((el) => el.id));
|
|
2277
|
+
const targetBounds = new Map(targets.map((el) => [el.id, elementBounds(el)]));
|
|
2278
|
+
let changed = false;
|
|
2279
|
+
const elements = state.elements.map((el) => {
|
|
2280
|
+
if (!targetIds.has(el.id)) return el;
|
|
2281
|
+
const elBounds = targetBounds.get(el.id);
|
|
2282
|
+
if (!elBounds) return el;
|
|
2283
|
+
const updates = {};
|
|
2284
|
+
if (axis === "horizontal") {
|
|
2285
|
+
updates.x = el.x + bounds.minX + bounds.maxX - elBounds.minX - elBounds.maxX;
|
|
2286
|
+
if (el.type === "line" || el.type === "arrow" || el.type === "freedraw") {
|
|
2287
|
+
updates.points = flipPoints(el.points, elBounds.minX - el.x, elBounds.maxX - el.x, 0);
|
|
2288
|
+
}
|
|
2289
|
+
} else {
|
|
2290
|
+
updates.y = el.y + bounds.minY + bounds.maxY - elBounds.minY - elBounds.maxY;
|
|
2291
|
+
if (el.type === "line" || el.type === "arrow" || el.type === "freedraw") {
|
|
2292
|
+
updates.points = flipPoints(el.points, elBounds.minY - el.y, elBounds.maxY - el.y, 1);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
changed = true;
|
|
2296
|
+
movedIds.push(el.id);
|
|
2297
|
+
return { ...el, ...updates, version: (el.version ?? 0) + 1 };
|
|
2298
|
+
});
|
|
2299
|
+
return changed ? { elements } : state;
|
|
2300
|
+
});
|
|
2301
|
+
if (movedIds.length > 0) {
|
|
2302
|
+
syncMovedElements(movedIds, get);
|
|
2303
|
+
get().pushHistory();
|
|
2304
|
+
}
|
|
2305
|
+
},
|
|
2306
|
+
// ─── Lock ─────────────────────────────────────────────────
|
|
2307
|
+
toggleLockElements: (ids) => {
|
|
2308
|
+
set((state) => ({
|
|
2309
|
+
elements: state.elements.map(
|
|
2310
|
+
(el) => ids.includes(el.id) ? { ...el, isLocked: !el.isLocked } : el
|
|
2311
|
+
)
|
|
2312
|
+
}));
|
|
2313
|
+
get().pushHistory();
|
|
2314
|
+
},
|
|
2315
|
+
// ─── Grouping ─────────────────────────────────────────────
|
|
2316
|
+
groupElements: (ids) => {
|
|
2317
|
+
if (ids.length < 2) return;
|
|
2318
|
+
const groupId = generateId();
|
|
2319
|
+
const idSet = new Set(ids);
|
|
2320
|
+
const { elements } = get();
|
|
2321
|
+
for (const el of elements) {
|
|
2322
|
+
if (idSet.has(el.id) && el.boundElements) {
|
|
2323
|
+
for (const be of el.boundElements) {
|
|
2324
|
+
if (be.type === "text") {
|
|
2325
|
+
idSet.add(be.id);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
set((state) => ({
|
|
2331
|
+
elements: state.elements.map(
|
|
2332
|
+
(el) => idSet.has(el.id) ? { ...el, groupIds: [...el.groupIds ?? [], groupId] } : el
|
|
2333
|
+
)
|
|
2334
|
+
}));
|
|
2335
|
+
get().pushHistory();
|
|
2336
|
+
},
|
|
2337
|
+
ungroupElements: (ids) => {
|
|
2338
|
+
var _a;
|
|
2339
|
+
const { elements } = get();
|
|
2340
|
+
const selected = elements.filter((el) => ids.includes(el.id));
|
|
2341
|
+
const groupIdsToRemove = /* @__PURE__ */ new Set();
|
|
2342
|
+
for (const el of selected) {
|
|
2343
|
+
if ((_a = el.groupIds) == null ? void 0 : _a.length) {
|
|
2344
|
+
groupIdsToRemove.add(el.groupIds[el.groupIds.length - 1]);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
if (groupIdsToRemove.size === 0) return;
|
|
2348
|
+
set((state) => ({
|
|
2349
|
+
elements: state.elements.map((el) => {
|
|
2350
|
+
var _a2;
|
|
2351
|
+
if (!((_a2 = el.groupIds) == null ? void 0 : _a2.length)) return el;
|
|
2352
|
+
const filtered = el.groupIds.filter((g) => !groupIdsToRemove.has(g));
|
|
2353
|
+
return {
|
|
2354
|
+
...el,
|
|
2355
|
+
groupIds: filtered.length > 0 ? filtered : void 0
|
|
2356
|
+
};
|
|
2357
|
+
})
|
|
2358
|
+
}));
|
|
2359
|
+
get().pushHistory();
|
|
2360
|
+
},
|
|
2361
|
+
// ─── Selection ────────────────────────────────────────────
|
|
2362
|
+
setSelectedIds: (ids) => set({ selectedIds: ids }),
|
|
2363
|
+
clearSelection: () => set({ selectedIds: [] }),
|
|
2364
|
+
// ─── Tool ─────────────────────────────────────────────────
|
|
2365
|
+
setActiveTool: (tool) => set((state) => ({
|
|
2366
|
+
activeTool: tool,
|
|
2367
|
+
// Keep selection when switching back to 'select' (e.g. after creating an element)
|
|
2368
|
+
selectedIds: tool === "select" ? state.selectedIds : []
|
|
2369
|
+
})),
|
|
2370
|
+
setCurrentStyle: (style) => set((state) => ({
|
|
2371
|
+
currentStyle: { ...state.currentStyle, ...style }
|
|
2372
|
+
})),
|
|
2373
|
+
setCurrentLineType: (lineType) => set({ currentLineType: lineType }),
|
|
2374
|
+
setCurrentStartArrowhead: (arrowhead) => set({ currentStartArrowhead: arrowhead }),
|
|
2375
|
+
setCurrentEndArrowhead: (arrowhead) => set({ currentEndArrowhead: arrowhead }),
|
|
2376
|
+
// ─── Viewport ─────────────────────────────────────────────
|
|
2377
|
+
setViewport: (viewport) => {
|
|
2378
|
+
cancelViewportAnimation();
|
|
2379
|
+
set((state) => ({
|
|
2380
|
+
viewport: { ...state.viewport, ...viewport }
|
|
2381
|
+
}));
|
|
2382
|
+
},
|
|
2383
|
+
zoomIn: (center, options) => {
|
|
2384
|
+
const { viewport } = get();
|
|
2385
|
+
const targetScale = getNextZoomStep(viewport.scale);
|
|
2386
|
+
const pt = center ?? { x: 400, y: 300 };
|
|
2387
|
+
const target = zoomAtPoint({ viewport, point: pt, targetScale });
|
|
2388
|
+
if (options == null ? void 0 : options.animate) {
|
|
2389
|
+
animateViewport(viewport, target, (v) => set((s) => ({ viewport: { ...s.viewport, ...v } })));
|
|
2390
|
+
} else {
|
|
2391
|
+
cancelViewportAnimation();
|
|
2392
|
+
set({ viewport: target });
|
|
2393
|
+
}
|
|
2394
|
+
},
|
|
2395
|
+
zoomOut: (center, options) => {
|
|
2396
|
+
const { viewport } = get();
|
|
2397
|
+
const targetScale = getPrevZoomStep(viewport.scale);
|
|
2398
|
+
const pt = center ?? { x: 400, y: 300 };
|
|
2399
|
+
const target = zoomAtPoint({ viewport, point: pt, targetScale });
|
|
2400
|
+
if (options == null ? void 0 : options.animate) {
|
|
2401
|
+
animateViewport(viewport, target, (v) => set((s) => ({ viewport: { ...s.viewport, ...v } })));
|
|
2402
|
+
} else {
|
|
2403
|
+
cancelViewportAnimation();
|
|
2404
|
+
set({ viewport: target });
|
|
2405
|
+
}
|
|
2406
|
+
},
|
|
2407
|
+
resetZoom: (options) => {
|
|
2408
|
+
const { viewport } = get();
|
|
2409
|
+
const target = { x: 0, y: 0, scale: 1 };
|
|
2410
|
+
if (options == null ? void 0 : options.animate) {
|
|
2411
|
+
animateViewport(viewport, target, (v) => set((s) => ({ viewport: { ...s.viewport, ...v } })));
|
|
2412
|
+
} else {
|
|
2413
|
+
cancelViewportAnimation();
|
|
2414
|
+
set({ viewport: target });
|
|
2415
|
+
}
|
|
2416
|
+
},
|
|
2417
|
+
zoomToFit: (stageWidth, stageHeight, ids, options) => {
|
|
2418
|
+
const { elements, viewport } = get();
|
|
2419
|
+
const targets = ids ? elements.filter((e) => ids.includes(e.id)) : elements;
|
|
2420
|
+
const bounds = getElementsBounds(targets);
|
|
2421
|
+
if (!bounds) return;
|
|
2422
|
+
const target = computeZoomToFit(bounds, stageWidth, stageHeight, {
|
|
2423
|
+
padding: options == null ? void 0 : options.padding,
|
|
2424
|
+
maxZoom: options == null ? void 0 : options.maxZoom
|
|
2425
|
+
});
|
|
2426
|
+
if (options == null ? void 0 : options.animate) {
|
|
2427
|
+
animateViewport(viewport, target, (v) => set((s) => ({ viewport: { ...s.viewport, ...v } })));
|
|
2428
|
+
} else {
|
|
2429
|
+
cancelViewportAnimation();
|
|
2430
|
+
set({ viewport: target });
|
|
2431
|
+
}
|
|
2432
|
+
},
|
|
2433
|
+
zoomToSelection: (stageWidth, stageHeight, options) => {
|
|
2434
|
+
const { elements, selectedIds, viewport } = get();
|
|
2435
|
+
if (selectedIds.length === 0) return;
|
|
2436
|
+
const targets = elements.filter((e) => selectedIds.includes(e.id));
|
|
2437
|
+
const bounds = getElementsBounds(targets);
|
|
2438
|
+
if (!bounds) return;
|
|
2439
|
+
const target = computeZoomToFit(bounds, stageWidth, stageHeight, {
|
|
2440
|
+
padding: (options == null ? void 0 : options.padding) ?? 80,
|
|
2441
|
+
maxZoom: (options == null ? void 0 : options.maxZoom) ?? 2
|
|
2442
|
+
});
|
|
2443
|
+
if (options == null ? void 0 : options.animate) {
|
|
2444
|
+
animateViewport(viewport, target, (v) => set((s) => ({ viewport: { ...s.viewport, ...v } })));
|
|
2445
|
+
} else {
|
|
2446
|
+
cancelViewportAnimation();
|
|
2447
|
+
set({ viewport: target });
|
|
2448
|
+
}
|
|
2449
|
+
},
|
|
2450
|
+
// ─── Drawing ──────────────────────────────────────────────
|
|
2451
|
+
setIsDrawing: (isDrawing) => set({ isDrawing }),
|
|
2452
|
+
setDrawStart: (point) => set({ drawStart: point }),
|
|
2453
|
+
// ─── History (Diff-based) ───────────────────────────────────
|
|
2454
|
+
pushHistory: (mark) => {
|
|
2455
|
+
const { elements, _historyBaseline, _historyOrderBaseline, _historyPaused } = get();
|
|
2456
|
+
if (_historyPaused) return;
|
|
2457
|
+
const diffs = [];
|
|
2458
|
+
const currentMap = /* @__PURE__ */ new Map();
|
|
2459
|
+
for (const el of elements) {
|
|
2460
|
+
currentMap.set(el.id, el);
|
|
2461
|
+
}
|
|
2462
|
+
const currentOrder = orderOf(elements);
|
|
2463
|
+
const orderChanged = !sameOrder(_historyOrderBaseline, currentOrder);
|
|
2464
|
+
for (const el of elements) {
|
|
2465
|
+
const baseline = _historyBaseline.get(el.id);
|
|
2466
|
+
if (!baseline) {
|
|
2467
|
+
diffs.push({ type: "add", elementId: el.id, after: cloneElement(el) });
|
|
2468
|
+
} else if (baseline !== el) {
|
|
2469
|
+
diffs.push({
|
|
2470
|
+
type: "modify",
|
|
2471
|
+
elementId: el.id,
|
|
2472
|
+
before: cloneElement(baseline),
|
|
2473
|
+
after: cloneElement(el)
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
for (const [id, baseline] of _historyBaseline) {
|
|
2478
|
+
if (!currentMap.has(id)) {
|
|
2479
|
+
diffs.push({ type: "delete", elementId: id, before: cloneElement(baseline) });
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
if (diffs.length === 0 && !orderChanged) return;
|
|
2483
|
+
set((state) => {
|
|
2484
|
+
const newHistory = state.history.slice(0, state.historyIndex + 1);
|
|
2485
|
+
newHistory.push({
|
|
2486
|
+
diffs,
|
|
2487
|
+
beforeOrder: orderChanged ? _historyOrderBaseline : void 0,
|
|
2488
|
+
afterOrder: orderChanged ? currentOrder : void 0,
|
|
2489
|
+
mark,
|
|
2490
|
+
timestamp: Date.now()
|
|
2491
|
+
});
|
|
2492
|
+
if (newHistory.length > MAX_HISTORY) {
|
|
2493
|
+
newHistory.shift();
|
|
2494
|
+
}
|
|
2495
|
+
return {
|
|
2496
|
+
history: newHistory,
|
|
2497
|
+
historyIndex: newHistory.length - 1,
|
|
2498
|
+
// Update baseline to current state
|
|
2499
|
+
_historyBaseline: new Map(currentMap),
|
|
2500
|
+
_historyOrderBaseline: currentOrder
|
|
2501
|
+
};
|
|
2502
|
+
});
|
|
2503
|
+
},
|
|
2504
|
+
undo: () => {
|
|
2505
|
+
const { historyIndex, history } = get();
|
|
2506
|
+
if (historyIndex < 0) return;
|
|
2507
|
+
const entry = history[historyIndex];
|
|
2508
|
+
set((state) => {
|
|
2509
|
+
let elements = [...state.elements];
|
|
2510
|
+
for (let i = entry.diffs.length - 1; i >= 0; i--) {
|
|
2511
|
+
const diff = entry.diffs[i];
|
|
2512
|
+
switch (diff.type) {
|
|
2513
|
+
case "add":
|
|
2514
|
+
elements = elements.filter((el) => el.id !== diff.elementId);
|
|
2515
|
+
break;
|
|
2516
|
+
case "modify":
|
|
2517
|
+
elements = elements.map(
|
|
2518
|
+
(el) => el.id === diff.elementId ? cloneElement(diff.before) : el
|
|
2519
|
+
);
|
|
2520
|
+
break;
|
|
2521
|
+
case "delete":
|
|
2522
|
+
elements.push(cloneElement(diff.before));
|
|
2523
|
+
break;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
if (entry.beforeOrder) {
|
|
2527
|
+
elements = restoreOrder(elements, entry.beforeOrder);
|
|
2528
|
+
}
|
|
2529
|
+
const newBaseline = /* @__PURE__ */ new Map();
|
|
2530
|
+
for (const el of elements) {
|
|
2531
|
+
newBaseline.set(el.id, el);
|
|
2532
|
+
}
|
|
2533
|
+
return {
|
|
2534
|
+
historyIndex: historyIndex - 1,
|
|
2535
|
+
elements,
|
|
2536
|
+
selectedIds: validSelectedIds(state.selectedIds, elements),
|
|
2537
|
+
_historyBaseline: newBaseline,
|
|
2538
|
+
_historyOrderBaseline: orderOf(elements)
|
|
2539
|
+
};
|
|
2540
|
+
});
|
|
2541
|
+
},
|
|
2542
|
+
redo: () => {
|
|
2543
|
+
const { historyIndex, history } = get();
|
|
2544
|
+
if (historyIndex >= history.length - 1) return;
|
|
2545
|
+
const newIndex = historyIndex + 1;
|
|
2546
|
+
const entry = history[newIndex];
|
|
2547
|
+
set((state) => {
|
|
2548
|
+
let elements = [...state.elements];
|
|
2549
|
+
for (const diff of entry.diffs) {
|
|
2550
|
+
switch (diff.type) {
|
|
2551
|
+
case "add":
|
|
2552
|
+
elements.push(cloneElement(diff.after));
|
|
2553
|
+
break;
|
|
2554
|
+
case "modify":
|
|
2555
|
+
elements = elements.map(
|
|
2556
|
+
(el) => el.id === diff.elementId ? cloneElement(diff.after) : el
|
|
2557
|
+
);
|
|
2558
|
+
break;
|
|
2559
|
+
case "delete":
|
|
2560
|
+
elements = elements.filter((el) => el.id !== diff.elementId);
|
|
2561
|
+
break;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
if (entry.afterOrder) {
|
|
2565
|
+
elements = restoreOrder(elements, entry.afterOrder);
|
|
2566
|
+
}
|
|
2567
|
+
const newBaseline = /* @__PURE__ */ new Map();
|
|
2568
|
+
for (const el of elements) {
|
|
2569
|
+
newBaseline.set(el.id, el);
|
|
2570
|
+
}
|
|
2571
|
+
return {
|
|
2572
|
+
historyIndex: newIndex,
|
|
2573
|
+
elements,
|
|
2574
|
+
selectedIds: validSelectedIds(state.selectedIds, elements),
|
|
2575
|
+
_historyBaseline: newBaseline,
|
|
2576
|
+
_historyOrderBaseline: orderOf(elements)
|
|
2577
|
+
};
|
|
2578
|
+
});
|
|
2579
|
+
},
|
|
2580
|
+
squashHistory: (count = 2) => {
|
|
2581
|
+
set((state) => {
|
|
2582
|
+
var _a, _b;
|
|
2583
|
+
if (state.history.length < count) return state;
|
|
2584
|
+
const startIdx = Math.max(0, state.history.length - count);
|
|
2585
|
+
const toSquash = state.history.slice(startIdx);
|
|
2586
|
+
const netDiffs = /* @__PURE__ */ new Map();
|
|
2587
|
+
for (const entry of toSquash) {
|
|
2588
|
+
for (const diff of entry.diffs) {
|
|
2589
|
+
const existing = netDiffs.get(diff.elementId);
|
|
2590
|
+
if (!existing) {
|
|
2591
|
+
netDiffs.set(diff.elementId, { ...diff });
|
|
2592
|
+
} else {
|
|
2593
|
+
if (diff.type === "delete") {
|
|
2594
|
+
if (existing.type === "add") {
|
|
2595
|
+
netDiffs.delete(diff.elementId);
|
|
2596
|
+
} else {
|
|
2597
|
+
netDiffs.set(diff.elementId, {
|
|
2598
|
+
type: "delete",
|
|
2599
|
+
elementId: diff.elementId,
|
|
2600
|
+
before: existing.before
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
} else if (diff.type === "modify") {
|
|
2604
|
+
netDiffs.set(diff.elementId, {
|
|
2605
|
+
type: existing.type === "add" ? "add" : "modify",
|
|
2606
|
+
elementId: diff.elementId,
|
|
2607
|
+
before: existing.before,
|
|
2608
|
+
after: diff.after
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
const squashed = {
|
|
2615
|
+
diffs: Array.from(netDiffs.values()),
|
|
2616
|
+
beforeOrder: (_a = toSquash.find((entry) => entry.beforeOrder)) == null ? void 0 : _a.beforeOrder,
|
|
2617
|
+
afterOrder: (_b = [...toSquash].reverse().find((entry) => entry.afterOrder)) == null ? void 0 : _b.afterOrder,
|
|
2618
|
+
mark: toSquash[toSquash.length - 1].mark,
|
|
2619
|
+
timestamp: Date.now()
|
|
2620
|
+
};
|
|
2621
|
+
const newHistory = [...state.history.slice(0, startIdx), squashed];
|
|
2622
|
+
return {
|
|
2623
|
+
history: newHistory,
|
|
2624
|
+
historyIndex: newHistory.length - 1
|
|
2625
|
+
};
|
|
2626
|
+
});
|
|
2627
|
+
},
|
|
2628
|
+
pauseHistory: () => set({ _historyPaused: true }),
|
|
2629
|
+
resumeHistory: () => set({ _historyPaused: false }),
|
|
2630
|
+
canUndo: () => get().historyIndex >= 0,
|
|
2631
|
+
canRedo: () => {
|
|
2632
|
+
const { historyIndex, history } = get();
|
|
2633
|
+
return historyIndex < history.length - 1;
|
|
2634
|
+
},
|
|
2635
|
+
// ─── Grid ─────────────────────────────────────────────────
|
|
2636
|
+
toggleGrid: () => set((state) => ({ showGrid: !state.showGrid }))
|
|
2637
|
+
}));
|
|
2638
|
+
}
|
|
2639
|
+
const useCanvasStore = createCanvasStore();
|
|
2640
|
+
const SYNC_FIELDS = [
|
|
2641
|
+
"id",
|
|
2642
|
+
"type",
|
|
2643
|
+
"x",
|
|
2644
|
+
"y",
|
|
2645
|
+
"width",
|
|
2646
|
+
"height",
|
|
2647
|
+
"rotation",
|
|
2648
|
+
"isLocked",
|
|
2649
|
+
"isVisible",
|
|
2650
|
+
"sortOrder",
|
|
2651
|
+
"version"
|
|
2652
|
+
];
|
|
2653
|
+
const STYLE_FIELDS = [
|
|
2654
|
+
"strokeColor",
|
|
2655
|
+
"fillColor",
|
|
2656
|
+
"strokeWidth",
|
|
2657
|
+
"opacity",
|
|
2658
|
+
"strokeStyle",
|
|
2659
|
+
"roughness",
|
|
2660
|
+
"fontSize",
|
|
2661
|
+
"fontFamily"
|
|
2662
|
+
];
|
|
2663
|
+
function elementToYMap(el, yMap) {
|
|
2664
|
+
const elRecord = el;
|
|
2665
|
+
for (const field of SYNC_FIELDS) {
|
|
2666
|
+
const value = elRecord[field];
|
|
2667
|
+
if (value !== void 0) {
|
|
2668
|
+
yMap.set(field, value);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (el.style) {
|
|
2672
|
+
for (const sf of STYLE_FIELDS) {
|
|
2673
|
+
yMap.set(`style.${sf}`, el.style[sf]);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
if (el.boundElements) {
|
|
2677
|
+
yMap.set("boundElements", JSON.stringify(el.boundElements));
|
|
2678
|
+
} else {
|
|
2679
|
+
yMap.set("boundElements", null);
|
|
2680
|
+
}
|
|
2681
|
+
if (el.ports) {
|
|
2682
|
+
yMap.set("ports", JSON.stringify(el.ports));
|
|
2683
|
+
}
|
|
2684
|
+
if ("lineStyle" in el && el.lineStyle) {
|
|
2685
|
+
yMap.set("lineStyle", JSON.stringify(el.lineStyle));
|
|
2686
|
+
}
|
|
2687
|
+
if (el.ports) {
|
|
2688
|
+
yMap.set("ports", JSON.stringify(el.ports));
|
|
2689
|
+
}
|
|
2690
|
+
if ("lineStyle" in el && el.lineStyle) {
|
|
2691
|
+
yMap.set("lineStyle", JSON.stringify(el.lineStyle));
|
|
2692
|
+
}
|
|
2693
|
+
if (el.groupIds) {
|
|
2694
|
+
yMap.set("groupIds", JSON.stringify(el.groupIds));
|
|
2695
|
+
}
|
|
2696
|
+
switch (el.type) {
|
|
2697
|
+
case "rectangle":
|
|
2698
|
+
yMap.set("cornerRadius", el.cornerRadius);
|
|
2699
|
+
break;
|
|
2700
|
+
case "line":
|
|
2701
|
+
case "arrow":
|
|
2702
|
+
yMap.set("points", JSON.stringify(el.points));
|
|
2703
|
+
yMap.set("lineType", el.lineType);
|
|
2704
|
+
if (el.curvature !== void 0) yMap.set("curvature", el.curvature);
|
|
2705
|
+
yMap.set("startBinding", el.startBinding ? JSON.stringify(el.startBinding) : null);
|
|
2706
|
+
yMap.set("endBinding", el.endBinding ? JSON.stringify(el.endBinding) : null);
|
|
2707
|
+
if (el.type === "arrow") {
|
|
2708
|
+
yMap.set("startArrowhead", el.startArrowhead);
|
|
2709
|
+
yMap.set("endArrowhead", el.endArrowhead);
|
|
2710
|
+
}
|
|
2711
|
+
break;
|
|
2712
|
+
case "freedraw":
|
|
2713
|
+
yMap.set("points", JSON.stringify(el.points));
|
|
2714
|
+
break;
|
|
2715
|
+
case "text":
|
|
2716
|
+
yMap.set("text", el.text);
|
|
2717
|
+
yMap.set("containerId", el.containerId);
|
|
2718
|
+
yMap.set("textAlign", el.textAlign);
|
|
2719
|
+
yMap.set("verticalAlign", el.verticalAlign);
|
|
2720
|
+
break;
|
|
2721
|
+
case "image":
|
|
2722
|
+
yMap.set("src", el.src);
|
|
2723
|
+
yMap.set("naturalWidth", el.naturalWidth);
|
|
2724
|
+
yMap.set("naturalHeight", el.naturalHeight);
|
|
2725
|
+
yMap.set("scaleMode", el.scaleMode);
|
|
2726
|
+
yMap.set("crop", el.crop ? JSON.stringify(el.crop) : null);
|
|
2727
|
+
yMap.set("cornerRadius", el.cornerRadius);
|
|
2728
|
+
yMap.set("alt", el.alt);
|
|
2729
|
+
break;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
function yMapToElement(yMap) {
|
|
2733
|
+
const type = yMap.get("type");
|
|
2734
|
+
const id = yMap.get("id");
|
|
2735
|
+
if (!type || !id) return null;
|
|
2736
|
+
const style = {};
|
|
2737
|
+
for (const sf of STYLE_FIELDS) {
|
|
2738
|
+
const val = yMap.get(`style.${sf}`);
|
|
2739
|
+
if (val !== void 0) {
|
|
2740
|
+
style[sf] = val;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
const base = {
|
|
2744
|
+
id,
|
|
2745
|
+
type,
|
|
2746
|
+
x: yMap.get("x") ?? 0,
|
|
2747
|
+
y: yMap.get("y") ?? 0,
|
|
2748
|
+
width: yMap.get("width") ?? 100,
|
|
2749
|
+
height: yMap.get("height") ?? 100,
|
|
2750
|
+
rotation: yMap.get("rotation") ?? 0,
|
|
2751
|
+
isLocked: yMap.get("isLocked") ?? false,
|
|
2752
|
+
isVisible: yMap.get("isVisible") ?? true,
|
|
2753
|
+
version: yMap.get("version") ?? 0,
|
|
2754
|
+
style,
|
|
2755
|
+
boundElements: safeParseJSON(yMap.get("boundElements")) ?? null,
|
|
2756
|
+
groupIds: safeParseJSON(yMap.get("groupIds")) ?? void 0,
|
|
2757
|
+
sortOrder: yMap.get("sortOrder") ?? void 0,
|
|
2758
|
+
ports: safeParseJSON(yMap.get("ports")) ?? void 0
|
|
2759
|
+
};
|
|
2760
|
+
switch (type) {
|
|
2761
|
+
case "rectangle":
|
|
2762
|
+
base.cornerRadius = yMap.get("cornerRadius") ?? 0;
|
|
2763
|
+
break;
|
|
2764
|
+
case "line":
|
|
2765
|
+
case "arrow":
|
|
2766
|
+
base.points = safeParseJSON(yMap.get("points")) ?? [0, 0, 100, 0];
|
|
2767
|
+
base.lineType = yMap.get("lineType") ?? "sharp";
|
|
2768
|
+
base.curvature = yMap.get("curvature") ?? void 0;
|
|
2769
|
+
base.startBinding = safeParseJSON(yMap.get("startBinding"));
|
|
2770
|
+
base.endBinding = safeParseJSON(yMap.get("endBinding"));
|
|
2771
|
+
if (type === "arrow") {
|
|
2772
|
+
base.startArrowhead = yMap.get("startArrowhead") ?? null;
|
|
2773
|
+
base.endArrowhead = yMap.get("endArrowhead") ?? "arrow";
|
|
2774
|
+
}
|
|
2775
|
+
{
|
|
2776
|
+
const ls = safeParseJSON(yMap.get("lineStyle"));
|
|
2777
|
+
if (ls) base.lineStyle = ls;
|
|
2778
|
+
}
|
|
2779
|
+
break;
|
|
2780
|
+
case "freedraw":
|
|
2781
|
+
base.points = safeParseJSON(yMap.get("points")) ?? [];
|
|
2782
|
+
break;
|
|
2783
|
+
case "text":
|
|
2784
|
+
base.text = yMap.get("text") ?? "";
|
|
2785
|
+
base.containerId = yMap.get("containerId") ?? null;
|
|
2786
|
+
base.textAlign = yMap.get("textAlign") ?? "center";
|
|
2787
|
+
base.verticalAlign = yMap.get("verticalAlign") ?? "middle";
|
|
2788
|
+
break;
|
|
2789
|
+
case "image":
|
|
2790
|
+
base.src = yMap.get("src") ?? "";
|
|
2791
|
+
base.naturalWidth = yMap.get("naturalWidth") ?? 0;
|
|
2792
|
+
base.naturalHeight = yMap.get("naturalHeight") ?? 0;
|
|
2793
|
+
base.scaleMode = yMap.get("scaleMode") ?? "fit";
|
|
2794
|
+
base.crop = safeParseJSON(yMap.get("crop")) ?? null;
|
|
2795
|
+
base.cornerRadius = yMap.get("cornerRadius") ?? 0;
|
|
2796
|
+
base.alt = yMap.get("alt") ?? "";
|
|
2797
|
+
break;
|
|
2798
|
+
}
|
|
2799
|
+
return base;
|
|
2800
|
+
}
|
|
2801
|
+
function safeParseJSON(json) {
|
|
2802
|
+
if (json == null) return null;
|
|
2803
|
+
try {
|
|
2804
|
+
return JSON.parse(json);
|
|
2805
|
+
} catch {
|
|
2806
|
+
return null;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
let _isApplyingRemote = false;
|
|
2810
|
+
let _isApplyingLocal = false;
|
|
2811
|
+
let _unsubscribe = null;
|
|
2812
|
+
let _yObserverCleanup = null;
|
|
2813
|
+
let _syncTimer = null;
|
|
2814
|
+
let _lastElements = [];
|
|
2815
|
+
function startSync(debounceMs = 50) {
|
|
2816
|
+
const doc = getYDoc();
|
|
2817
|
+
const yElements = getYElements();
|
|
2818
|
+
if (!doc || !yElements) {
|
|
2819
|
+
console.warn("[SyncBridge] Cannot start sync — no Yjs doc");
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
stopSync();
|
|
2823
|
+
if (yElements.size > 0) {
|
|
2824
|
+
_isApplyingRemote = true;
|
|
2825
|
+
const elements = yMapCollectionToElements(yElements);
|
|
2826
|
+
useCanvasStore.getState().setElements(elements);
|
|
2827
|
+
_lastElements = elements;
|
|
2828
|
+
_isApplyingRemote = false;
|
|
2829
|
+
} else {
|
|
2830
|
+
const localElements = useCanvasStore.getState().elements;
|
|
2831
|
+
if (localElements.length > 0) {
|
|
2832
|
+
_isApplyingLocal = true;
|
|
2833
|
+
doc.transact(() => {
|
|
2834
|
+
for (const el of localElements) {
|
|
2835
|
+
const yMap = new Y.Map();
|
|
2836
|
+
elementToYMap(el, yMap);
|
|
2837
|
+
yElements.set(el.id, yMap);
|
|
2838
|
+
}
|
|
2839
|
+
}, "local-init");
|
|
2840
|
+
_isApplyingLocal = false;
|
|
2841
|
+
}
|
|
2842
|
+
_lastElements = localElements;
|
|
2843
|
+
}
|
|
2844
|
+
const yObserver = (events, transaction) => {
|
|
2845
|
+
if (transaction.origin === "local-sync" || transaction.origin === "local-init") return;
|
|
2846
|
+
if (_isApplyingLocal) return;
|
|
2847
|
+
_isApplyingRemote = true;
|
|
2848
|
+
const store = useCanvasStore.getState();
|
|
2849
|
+
let elements = [..._lastElements];
|
|
2850
|
+
let changed = false;
|
|
2851
|
+
for (const [key, change] of events.keys) {
|
|
2852
|
+
if (change.action === "add" || change.action === "update") {
|
|
2853
|
+
const yMap = yElements.get(key);
|
|
2854
|
+
if (yMap) {
|
|
2855
|
+
const el = yMapToElement(yMap);
|
|
2856
|
+
if (el) {
|
|
2857
|
+
const idx = elements.findIndex((e) => e.id === key);
|
|
2858
|
+
if (idx >= 0) {
|
|
2859
|
+
elements[idx] = el;
|
|
2860
|
+
} else {
|
|
2861
|
+
elements.push(el);
|
|
2862
|
+
}
|
|
2863
|
+
changed = true;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
} else if (change.action === "delete") {
|
|
2867
|
+
elements = elements.filter((e) => e.id !== key);
|
|
2868
|
+
changed = true;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
if (changed) {
|
|
2872
|
+
elements.sort((a, b) => {
|
|
2873
|
+
if (a.sortOrder && b.sortOrder) {
|
|
2874
|
+
return a.sortOrder < b.sortOrder ? -1 : a.sortOrder > b.sortOrder ? 1 : 0;
|
|
2875
|
+
}
|
|
2876
|
+
return 0;
|
|
2877
|
+
});
|
|
2878
|
+
store.setElements(elements);
|
|
2879
|
+
_lastElements = elements;
|
|
2880
|
+
}
|
|
2881
|
+
_isApplyingRemote = false;
|
|
2882
|
+
};
|
|
2883
|
+
let _deepObserverTimer = null;
|
|
2884
|
+
const _dirtyElementIds = /* @__PURE__ */ new Set();
|
|
2885
|
+
const deepObserver = (events) => {
|
|
2886
|
+
if (_isApplyingLocal) return;
|
|
2887
|
+
for (const event of events) {
|
|
2888
|
+
let target = event.target;
|
|
2889
|
+
while (target && !(target instanceof Y.Map && target.parent === yElements)) {
|
|
2890
|
+
target = target.parent;
|
|
2891
|
+
}
|
|
2892
|
+
if (target instanceof Y.Map) {
|
|
2893
|
+
const id = target.get("id");
|
|
2894
|
+
if (id) _dirtyElementIds.add(id);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (_deepObserverTimer) clearTimeout(_deepObserverTimer);
|
|
2898
|
+
_deepObserverTimer = setTimeout(() => {
|
|
2899
|
+
if (_dirtyElementIds.size === 0 || _isApplyingLocal) return;
|
|
2900
|
+
_isApplyingRemote = true;
|
|
2901
|
+
let elements = [..._lastElements];
|
|
2902
|
+
let changed = false;
|
|
2903
|
+
for (const id of _dirtyElementIds) {
|
|
2904
|
+
const yMap = yElements.get(id);
|
|
2905
|
+
if (!yMap) continue;
|
|
2906
|
+
const el = yMapToElement(yMap);
|
|
2907
|
+
if (!el) continue;
|
|
2908
|
+
const idx = elements.findIndex((e) => e.id === id);
|
|
2909
|
+
if (idx >= 0) {
|
|
2910
|
+
elements[idx] = el;
|
|
2911
|
+
changed = true;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
_dirtyElementIds.clear();
|
|
2915
|
+
if (changed) {
|
|
2916
|
+
useCanvasStore.getState().setElements(elements);
|
|
2917
|
+
_lastElements = elements;
|
|
2918
|
+
}
|
|
2919
|
+
_isApplyingRemote = false;
|
|
2920
|
+
}, 16);
|
|
2921
|
+
};
|
|
2922
|
+
yElements.observe(yObserver);
|
|
2923
|
+
yElements.observeDeep(deepObserver);
|
|
2924
|
+
_yObserverCleanup = () => {
|
|
2925
|
+
yElements.unobserve(yObserver);
|
|
2926
|
+
yElements.unobserveDeep(deepObserver);
|
|
2927
|
+
if (_deepObserverTimer) clearTimeout(_deepObserverTimer);
|
|
2928
|
+
_dirtyElementIds.clear();
|
|
2929
|
+
};
|
|
2930
|
+
_unsubscribe = useCanvasStore.subscribe(
|
|
2931
|
+
(state) => {
|
|
2932
|
+
if (_isApplyingRemote) return;
|
|
2933
|
+
if (state.elements === _lastElements) return;
|
|
2934
|
+
if (_syncTimer) clearTimeout(_syncTimer);
|
|
2935
|
+
_syncTimer = setTimeout(() => {
|
|
2936
|
+
syncLocalToYjs(state.elements, yElements, doc);
|
|
2937
|
+
}, debounceMs);
|
|
2938
|
+
}
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2941
|
+
function stopSync() {
|
|
2942
|
+
if (_unsubscribe) {
|
|
2943
|
+
_unsubscribe();
|
|
2944
|
+
_unsubscribe = null;
|
|
2945
|
+
}
|
|
2946
|
+
if (_yObserverCleanup) {
|
|
2947
|
+
_yObserverCleanup();
|
|
2948
|
+
_yObserverCleanup = null;
|
|
2949
|
+
}
|
|
2950
|
+
if (_syncTimer) {
|
|
2951
|
+
clearTimeout(_syncTimer);
|
|
2952
|
+
_syncTimer = null;
|
|
2953
|
+
}
|
|
2954
|
+
_lastElements = [];
|
|
2955
|
+
}
|
|
2956
|
+
function syncLocalToYjs(elements, yElements, doc) {
|
|
2957
|
+
_isApplyingLocal = true;
|
|
2958
|
+
_lastElements = elements;
|
|
2959
|
+
const localMap = /* @__PURE__ */ new Map();
|
|
2960
|
+
for (const el of elements) {
|
|
2961
|
+
localMap.set(el.id, el);
|
|
2962
|
+
}
|
|
2963
|
+
doc.transact(() => {
|
|
2964
|
+
for (const [id] of yElements.entries()) {
|
|
2965
|
+
if (!localMap.has(id)) {
|
|
2966
|
+
yElements.delete(id);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
for (const el of elements) {
|
|
2970
|
+
let yMap = yElements.get(el.id);
|
|
2971
|
+
if (!yMap) {
|
|
2972
|
+
yMap = new Y.Map();
|
|
2973
|
+
elementToYMap(el, yMap);
|
|
2974
|
+
yElements.set(el.id, yMap);
|
|
2975
|
+
} else {
|
|
2976
|
+
updateYMapFromElement(el, yMap);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}, "local-sync");
|
|
2980
|
+
_isApplyingLocal = false;
|
|
2981
|
+
}
|
|
2982
|
+
function updateYMapFromElement(el, yMap) {
|
|
2983
|
+
const elRecord = el;
|
|
2984
|
+
for (const field of SYNC_FIELDS) {
|
|
2985
|
+
const value = elRecord[field];
|
|
2986
|
+
if (value !== yMap.get(field)) {
|
|
2987
|
+
yMap.set(field, value);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
if (el.style) {
|
|
2991
|
+
for (const sf of STYLE_FIELDS) {
|
|
2992
|
+
const val = el.style[sf];
|
|
2993
|
+
if (val !== yMap.get(`style.${sf}`)) {
|
|
2994
|
+
yMap.set(`style.${sf}`, val);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
const beJson = el.boundElements ? JSON.stringify(el.boundElements) : null;
|
|
2999
|
+
if (beJson !== yMap.get("boundElements")) {
|
|
3000
|
+
yMap.set("boundElements", beJson);
|
|
3001
|
+
}
|
|
3002
|
+
switch (el.type) {
|
|
3003
|
+
case "rectangle":
|
|
3004
|
+
if (el.cornerRadius !== yMap.get("cornerRadius")) {
|
|
3005
|
+
yMap.set("cornerRadius", el.cornerRadius);
|
|
3006
|
+
}
|
|
3007
|
+
break;
|
|
3008
|
+
case "line":
|
|
3009
|
+
case "arrow": {
|
|
3010
|
+
const ptsJson = JSON.stringify(el.points);
|
|
3011
|
+
if (ptsJson !== yMap.get("points")) yMap.set("points", ptsJson);
|
|
3012
|
+
if (el.lineType !== yMap.get("lineType")) yMap.set("lineType", el.lineType);
|
|
3013
|
+
if (el.curvature !== yMap.get("curvature")) yMap.set("curvature", el.curvature);
|
|
3014
|
+
const sbJson = el.startBinding ? JSON.stringify(el.startBinding) : null;
|
|
3015
|
+
if (sbJson !== yMap.get("startBinding")) yMap.set("startBinding", sbJson);
|
|
3016
|
+
const ebJson = el.endBinding ? JSON.stringify(el.endBinding) : null;
|
|
3017
|
+
if (ebJson !== yMap.get("endBinding")) yMap.set("endBinding", ebJson);
|
|
3018
|
+
if (el.type === "arrow") {
|
|
3019
|
+
if (el.startArrowhead !== yMap.get("startArrowhead")) yMap.set("startArrowhead", el.startArrowhead);
|
|
3020
|
+
if (el.endArrowhead !== yMap.get("endArrowhead")) yMap.set("endArrowhead", el.endArrowhead);
|
|
3021
|
+
}
|
|
3022
|
+
break;
|
|
3023
|
+
}
|
|
3024
|
+
case "freedraw": {
|
|
3025
|
+
const fpJson = JSON.stringify(el.points);
|
|
3026
|
+
if (fpJson !== yMap.get("points")) yMap.set("points", fpJson);
|
|
3027
|
+
break;
|
|
3028
|
+
}
|
|
3029
|
+
case "text":
|
|
3030
|
+
if (el.text !== yMap.get("text")) yMap.set("text", el.text);
|
|
3031
|
+
if (el.containerId !== yMap.get("containerId")) yMap.set("containerId", el.containerId);
|
|
3032
|
+
if (el.textAlign !== yMap.get("textAlign")) yMap.set("textAlign", el.textAlign);
|
|
3033
|
+
if (el.verticalAlign !== yMap.get("verticalAlign")) yMap.set("verticalAlign", el.verticalAlign);
|
|
3034
|
+
break;
|
|
3035
|
+
case "image":
|
|
3036
|
+
if (el.src !== yMap.get("src")) yMap.set("src", el.src);
|
|
3037
|
+
if (el.naturalWidth !== yMap.get("naturalWidth")) yMap.set("naturalWidth", el.naturalWidth);
|
|
3038
|
+
if (el.naturalHeight !== yMap.get("naturalHeight")) yMap.set("naturalHeight", el.naturalHeight);
|
|
3039
|
+
if (el.scaleMode !== yMap.get("scaleMode")) yMap.set("scaleMode", el.scaleMode);
|
|
3040
|
+
const cropJson = el.crop ? JSON.stringify(el.crop) : null;
|
|
3041
|
+
if (cropJson !== yMap.get("crop")) yMap.set("crop", cropJson);
|
|
3042
|
+
if (el.cornerRadius !== yMap.get("cornerRadius")) yMap.set("cornerRadius", el.cornerRadius);
|
|
3043
|
+
if (el.alt !== yMap.get("alt")) yMap.set("alt", el.alt);
|
|
3044
|
+
break;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
function yMapCollectionToElements(yElements) {
|
|
3048
|
+
const elements = [];
|
|
3049
|
+
for (const [, yMap] of yElements.entries()) {
|
|
3050
|
+
const el = yMapToElement(yMap);
|
|
3051
|
+
if (el) elements.push(el);
|
|
3052
|
+
}
|
|
3053
|
+
elements.sort((a, b) => {
|
|
3054
|
+
if (a.sortOrder && b.sortOrder) {
|
|
3055
|
+
return a.sortOrder < b.sortOrder ? -1 : a.sortOrder > b.sortOrder ? 1 : 0;
|
|
3056
|
+
}
|
|
3057
|
+
return 0;
|
|
3058
|
+
});
|
|
3059
|
+
return elements;
|
|
3060
|
+
}
|
|
3061
|
+
const syncBridge = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
3062
|
+
__proto__: null,
|
|
3063
|
+
startSync,
|
|
3064
|
+
stopSync
|
|
3065
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
3066
|
+
class CollaborationManager {
|
|
3067
|
+
constructor() {
|
|
3068
|
+
// Provider state
|
|
3069
|
+
__publicField(this, "_doc", null);
|
|
3070
|
+
__publicField(this, "_provider", null);
|
|
3071
|
+
__publicField(this, "_config", null);
|
|
3072
|
+
// Sync state
|
|
3073
|
+
__publicField(this, "_isApplyingRemote", false);
|
|
3074
|
+
__publicField(this, "_isApplyingLocal", false);
|
|
3075
|
+
__publicField(this, "_lastElements", []);
|
|
3076
|
+
__publicField(this, "_syncTimer", null);
|
|
3077
|
+
__publicField(this, "_deepTimer", null);
|
|
3078
|
+
__publicField(this, "_dirtyIds", /* @__PURE__ */ new Set());
|
|
3079
|
+
__publicField(this, "_storeUnsub", null);
|
|
3080
|
+
__publicField(this, "_yObserverCleanup", null);
|
|
3081
|
+
// Status listeners
|
|
3082
|
+
__publicField(this, "_statusListeners", /* @__PURE__ */ new Set());
|
|
3083
|
+
}
|
|
3084
|
+
// ─── Provider Lifecycle ───────────────────────────────────
|
|
3085
|
+
get doc() {
|
|
3086
|
+
return this._doc;
|
|
3087
|
+
}
|
|
3088
|
+
get provider() {
|
|
3089
|
+
return this._provider;
|
|
3090
|
+
}
|
|
3091
|
+
get config() {
|
|
3092
|
+
return this._config;
|
|
3093
|
+
}
|
|
3094
|
+
get isActive() {
|
|
3095
|
+
return this._provider !== null && this._provider.wsconnected;
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Connect to a collaboration room.
|
|
3099
|
+
* If already connected, disconnects first.
|
|
3100
|
+
*/
|
|
3101
|
+
connect(config) {
|
|
3102
|
+
this.dispose();
|
|
3103
|
+
this._config = config;
|
|
3104
|
+
this._doc = new Y.Doc();
|
|
3105
|
+
this._provider = new WebsocketProvider(
|
|
3106
|
+
config.serverUrl,
|
|
3107
|
+
config.roomName,
|
|
3108
|
+
this._doc,
|
|
3109
|
+
{
|
|
3110
|
+
connect: true,
|
|
3111
|
+
params: config.authToken ? { token: config.authToken } : void 0
|
|
3112
|
+
}
|
|
3113
|
+
);
|
|
3114
|
+
this._provider.awareness.setLocalState({
|
|
3115
|
+
user: config.user,
|
|
3116
|
+
cursor: null,
|
|
3117
|
+
selectedIds: []
|
|
3118
|
+
});
|
|
3119
|
+
this._provider.on("status", (event) => {
|
|
3120
|
+
const status = event.status;
|
|
3121
|
+
for (const listener of this._statusListeners) {
|
|
3122
|
+
listener(status);
|
|
3123
|
+
}
|
|
3124
|
+
});
|
|
3125
|
+
return { doc: this._doc, provider: this._provider };
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* Get the shared Y.Map for elements.
|
|
3129
|
+
*/
|
|
3130
|
+
getYElements() {
|
|
3131
|
+
var _a;
|
|
3132
|
+
return (_a = this._doc) == null ? void 0 : _a.getMap("elements");
|
|
3133
|
+
}
|
|
3134
|
+
// ─── Awareness ────────────────────────────────────────────
|
|
3135
|
+
updateAwareness(update) {
|
|
3136
|
+
if (!this._provider) return;
|
|
3137
|
+
const current = this._provider.awareness.getLocalState();
|
|
3138
|
+
this._provider.awareness.setLocalState({ ...current, ...update });
|
|
3139
|
+
}
|
|
3140
|
+
getRemoteAwareness() {
|
|
3141
|
+
if (!this._provider) return /* @__PURE__ */ new Map();
|
|
3142
|
+
const all = this._provider.awareness.getStates();
|
|
3143
|
+
const localId = this._provider.awareness.clientID;
|
|
3144
|
+
const remote = /* @__PURE__ */ new Map();
|
|
3145
|
+
for (const [clientId, state] of all) {
|
|
3146
|
+
if (clientId !== localId && state && state.user) {
|
|
3147
|
+
remote.set(clientId, state);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
return remote;
|
|
3151
|
+
}
|
|
3152
|
+
// ─── Status Listeners ─────────────────────────────────────
|
|
3153
|
+
onStatusChange(listener) {
|
|
3154
|
+
this._statusListeners.add(listener);
|
|
3155
|
+
return () => {
|
|
3156
|
+
this._statusListeners.delete(listener);
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
// ─── Sync Bridge ─────────────────────────────────────────
|
|
3160
|
+
/**
|
|
3161
|
+
* Start bidirectional sync between Yjs and the provided store.
|
|
3162
|
+
*/
|
|
3163
|
+
startSync(store, debounceMs = 50) {
|
|
3164
|
+
const doc = this._doc;
|
|
3165
|
+
const yElements = this.getYElements();
|
|
3166
|
+
if (!doc || !yElements) {
|
|
3167
|
+
console.warn("[CollaborationManager] Cannot start sync — not connected");
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
this.stopSync();
|
|
3171
|
+
if (yElements.size > 0) {
|
|
3172
|
+
this._isApplyingRemote = true;
|
|
3173
|
+
const elements = this._yMapCollectionToElements(yElements);
|
|
3174
|
+
store.setElements(elements);
|
|
3175
|
+
this._lastElements = elements;
|
|
3176
|
+
this._isApplyingRemote = false;
|
|
3177
|
+
} else {
|
|
3178
|
+
const localElements = store.getState().elements;
|
|
3179
|
+
if (localElements.length > 0) {
|
|
3180
|
+
this._isApplyingLocal = true;
|
|
3181
|
+
doc.transact(() => {
|
|
3182
|
+
for (const el of localElements) {
|
|
3183
|
+
const yMap = new Y.Map();
|
|
3184
|
+
elementToYMap(el, yMap);
|
|
3185
|
+
yElements.set(el.id, yMap);
|
|
3186
|
+
}
|
|
3187
|
+
}, "local-init");
|
|
3188
|
+
this._isApplyingLocal = false;
|
|
3189
|
+
}
|
|
3190
|
+
this._lastElements = localElements;
|
|
3191
|
+
}
|
|
3192
|
+
const yObserver = (events, transaction) => {
|
|
3193
|
+
if (transaction.origin === "local-sync" || transaction.origin === "local-init") return;
|
|
3194
|
+
if (this._isApplyingLocal) return;
|
|
3195
|
+
this._isApplyingRemote = true;
|
|
3196
|
+
let elements = [...this._lastElements];
|
|
3197
|
+
let changed = false;
|
|
3198
|
+
for (const [key, change] of events.keys) {
|
|
3199
|
+
if (change.action === "add" || change.action === "update") {
|
|
3200
|
+
const yMap = yElements.get(key);
|
|
3201
|
+
if (yMap) {
|
|
3202
|
+
const el = yMapToElement(yMap);
|
|
3203
|
+
if (el) {
|
|
3204
|
+
const idx = elements.findIndex((e) => e.id === key);
|
|
3205
|
+
if (idx >= 0) elements[idx] = el;
|
|
3206
|
+
else elements.push(el);
|
|
3207
|
+
changed = true;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
} else if (change.action === "delete") {
|
|
3211
|
+
elements = elements.filter((e) => e.id !== key);
|
|
3212
|
+
changed = true;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
if (changed) {
|
|
3216
|
+
elements.sort((a, b) => {
|
|
3217
|
+
if (a.sortOrder && b.sortOrder) {
|
|
3218
|
+
return a.sortOrder < b.sortOrder ? -1 : a.sortOrder > b.sortOrder ? 1 : 0;
|
|
3219
|
+
}
|
|
3220
|
+
return 0;
|
|
3221
|
+
});
|
|
3222
|
+
store.setElements(elements);
|
|
3223
|
+
this._lastElements = elements;
|
|
3224
|
+
}
|
|
3225
|
+
this._isApplyingRemote = false;
|
|
3226
|
+
};
|
|
3227
|
+
const deepObserver = (events) => {
|
|
3228
|
+
if (this._isApplyingLocal) return;
|
|
3229
|
+
for (const event of events) {
|
|
3230
|
+
let target = event.target;
|
|
3231
|
+
while (target && !(target instanceof Y.Map && target.parent === yElements)) {
|
|
3232
|
+
target = target.parent;
|
|
3233
|
+
}
|
|
3234
|
+
if (target instanceof Y.Map) {
|
|
3235
|
+
const id = target.get("id");
|
|
3236
|
+
if (id) this._dirtyIds.add(id);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
if (this._deepTimer) clearTimeout(this._deepTimer);
|
|
3240
|
+
this._deepTimer = setTimeout(() => {
|
|
3241
|
+
if (this._dirtyIds.size === 0 || this._isApplyingLocal) return;
|
|
3242
|
+
this._isApplyingRemote = true;
|
|
3243
|
+
let elements = [...this._lastElements];
|
|
3244
|
+
let changed = false;
|
|
3245
|
+
for (const id of this._dirtyIds) {
|
|
3246
|
+
const yMap = yElements.get(id);
|
|
3247
|
+
if (!yMap) continue;
|
|
3248
|
+
const el = yMapToElement(yMap);
|
|
3249
|
+
if (!el) continue;
|
|
3250
|
+
const idx = elements.findIndex((e) => e.id === id);
|
|
3251
|
+
if (idx >= 0) {
|
|
3252
|
+
elements[idx] = el;
|
|
3253
|
+
changed = true;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
this._dirtyIds.clear();
|
|
3257
|
+
if (changed) {
|
|
3258
|
+
store.setElements(elements);
|
|
3259
|
+
this._lastElements = elements;
|
|
3260
|
+
}
|
|
3261
|
+
this._isApplyingRemote = false;
|
|
3262
|
+
}, 16);
|
|
3263
|
+
};
|
|
3264
|
+
yElements.observe(yObserver);
|
|
3265
|
+
yElements.observeDeep(deepObserver);
|
|
3266
|
+
this._yObserverCleanup = () => {
|
|
3267
|
+
yElements.unobserve(yObserver);
|
|
3268
|
+
yElements.unobserveDeep(deepObserver);
|
|
3269
|
+
if (this._deepTimer) clearTimeout(this._deepTimer);
|
|
3270
|
+
this._dirtyIds.clear();
|
|
3271
|
+
};
|
|
3272
|
+
this._storeUnsub = store.subscribe((state) => {
|
|
3273
|
+
if (this._isApplyingRemote) return;
|
|
3274
|
+
if (state.elements === this._lastElements) return;
|
|
3275
|
+
if (this._syncTimer) clearTimeout(this._syncTimer);
|
|
3276
|
+
this._syncTimer = setTimeout(() => {
|
|
3277
|
+
this._syncLocalToYjs(state.elements, yElements, doc);
|
|
3278
|
+
}, debounceMs);
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
stopSync() {
|
|
3282
|
+
var _a, _b;
|
|
3283
|
+
(_a = this._storeUnsub) == null ? void 0 : _a.call(this);
|
|
3284
|
+
this._storeUnsub = null;
|
|
3285
|
+
(_b = this._yObserverCleanup) == null ? void 0 : _b.call(this);
|
|
3286
|
+
this._yObserverCleanup = null;
|
|
3287
|
+
if (this._syncTimer) {
|
|
3288
|
+
clearTimeout(this._syncTimer);
|
|
3289
|
+
this._syncTimer = null;
|
|
3290
|
+
}
|
|
3291
|
+
this._lastElements = [];
|
|
3292
|
+
}
|
|
3293
|
+
// ─── Dispose ──────────────────────────────────────────────
|
|
3294
|
+
dispose() {
|
|
3295
|
+
this.stopSync();
|
|
3296
|
+
if (this._provider) {
|
|
3297
|
+
this._provider.awareness.setLocalState(null);
|
|
3298
|
+
this._provider.disconnect();
|
|
3299
|
+
this._provider.destroy();
|
|
3300
|
+
this._provider = null;
|
|
3301
|
+
}
|
|
3302
|
+
if (this._doc) {
|
|
3303
|
+
this._doc.destroy();
|
|
3304
|
+
this._doc = null;
|
|
3305
|
+
}
|
|
3306
|
+
this._config = null;
|
|
3307
|
+
this._statusListeners.clear();
|
|
3308
|
+
}
|
|
3309
|
+
// ─── Private ──────────────────────────────────────────────
|
|
3310
|
+
_syncLocalToYjs(elements, yElements, doc) {
|
|
3311
|
+
this._isApplyingLocal = true;
|
|
3312
|
+
this._lastElements = elements;
|
|
3313
|
+
const localMap = /* @__PURE__ */ new Map();
|
|
3314
|
+
for (const el of elements) localMap.set(el.id, el);
|
|
3315
|
+
doc.transact(() => {
|
|
3316
|
+
for (const [id] of yElements.entries()) {
|
|
3317
|
+
if (!localMap.has(id)) yElements.delete(id);
|
|
3318
|
+
}
|
|
3319
|
+
for (const el of elements) {
|
|
3320
|
+
let yMap = yElements.get(el.id);
|
|
3321
|
+
if (!yMap) {
|
|
3322
|
+
yMap = new Y.Map();
|
|
3323
|
+
elementToYMap(el, yMap);
|
|
3324
|
+
yElements.set(el.id, yMap);
|
|
3325
|
+
} else {
|
|
3326
|
+
this._updateYMapFromElement(el, yMap);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
}, "local-sync");
|
|
3330
|
+
this._isApplyingLocal = false;
|
|
3331
|
+
}
|
|
3332
|
+
_updateYMapFromElement(el, yMap) {
|
|
3333
|
+
const SYNC_FIELDS2 = [
|
|
3334
|
+
"id",
|
|
3335
|
+
"type",
|
|
3336
|
+
"x",
|
|
3337
|
+
"y",
|
|
3338
|
+
"width",
|
|
3339
|
+
"height",
|
|
3340
|
+
"rotation",
|
|
3341
|
+
"isLocked",
|
|
3342
|
+
"isVisible",
|
|
3343
|
+
"sortOrder"
|
|
3344
|
+
];
|
|
3345
|
+
const STYLE_FIELDS2 = [
|
|
3346
|
+
"strokeColor",
|
|
3347
|
+
"fillColor",
|
|
3348
|
+
"strokeWidth",
|
|
3349
|
+
"opacity",
|
|
3350
|
+
"strokeStyle",
|
|
3351
|
+
"roughness",
|
|
3352
|
+
"fontSize",
|
|
3353
|
+
"fontFamily"
|
|
3354
|
+
];
|
|
3355
|
+
const elRecord = el;
|
|
3356
|
+
for (const field of SYNC_FIELDS2) {
|
|
3357
|
+
const value = elRecord[field];
|
|
3358
|
+
if (value !== yMap.get(field)) yMap.set(field, value);
|
|
3359
|
+
}
|
|
3360
|
+
if (el.style) {
|
|
3361
|
+
for (const sf of STYLE_FIELDS2) {
|
|
3362
|
+
const val = el.style[sf];
|
|
3363
|
+
if (val !== yMap.get(`style.${sf}`)) yMap.set(`style.${sf}`, val);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
const beJson = el.boundElements ? JSON.stringify(el.boundElements) : null;
|
|
3367
|
+
if (beJson !== yMap.get("boundElements")) yMap.set("boundElements", beJson);
|
|
3368
|
+
switch (el.type) {
|
|
3369
|
+
case "rectangle":
|
|
3370
|
+
if (el.cornerRadius !== yMap.get("cornerRadius")) yMap.set("cornerRadius", el.cornerRadius);
|
|
3371
|
+
break;
|
|
3372
|
+
case "line":
|
|
3373
|
+
case "arrow": {
|
|
3374
|
+
const ptsJson = JSON.stringify(el.points);
|
|
3375
|
+
if (ptsJson !== yMap.get("points")) yMap.set("points", ptsJson);
|
|
3376
|
+
if (el.lineType !== yMap.get("lineType")) yMap.set("lineType", el.lineType);
|
|
3377
|
+
if (el.curvature !== yMap.get("curvature")) yMap.set("curvature", el.curvature);
|
|
3378
|
+
const sbJson = el.startBinding ? JSON.stringify(el.startBinding) : null;
|
|
3379
|
+
if (sbJson !== yMap.get("startBinding")) yMap.set("startBinding", sbJson);
|
|
3380
|
+
const ebJson = el.endBinding ? JSON.stringify(el.endBinding) : null;
|
|
3381
|
+
if (ebJson !== yMap.get("endBinding")) yMap.set("endBinding", ebJson);
|
|
3382
|
+
if (el.type === "arrow") {
|
|
3383
|
+
if (el.startArrowhead !== yMap.get("startArrowhead")) yMap.set("startArrowhead", el.startArrowhead);
|
|
3384
|
+
if (el.endArrowhead !== yMap.get("endArrowhead")) yMap.set("endArrowhead", el.endArrowhead);
|
|
3385
|
+
}
|
|
3386
|
+
break;
|
|
3387
|
+
}
|
|
3388
|
+
case "freedraw": {
|
|
3389
|
+
const fpJson = JSON.stringify(el.points);
|
|
3390
|
+
if (fpJson !== yMap.get("points")) yMap.set("points", fpJson);
|
|
3391
|
+
break;
|
|
3392
|
+
}
|
|
3393
|
+
case "text":
|
|
3394
|
+
if (el.text !== yMap.get("text")) yMap.set("text", el.text);
|
|
3395
|
+
if (el.containerId !== yMap.get("containerId")) yMap.set("containerId", el.containerId);
|
|
3396
|
+
if (el.textAlign !== yMap.get("textAlign")) yMap.set("textAlign", el.textAlign);
|
|
3397
|
+
if (el.verticalAlign !== yMap.get("verticalAlign")) yMap.set("verticalAlign", el.verticalAlign);
|
|
3398
|
+
break;
|
|
3399
|
+
case "image":
|
|
3400
|
+
if (el.src !== yMap.get("src")) yMap.set("src", el.src);
|
|
3401
|
+
if (el.naturalWidth !== yMap.get("naturalWidth")) yMap.set("naturalWidth", el.naturalWidth);
|
|
3402
|
+
if (el.naturalHeight !== yMap.get("naturalHeight")) yMap.set("naturalHeight", el.naturalHeight);
|
|
3403
|
+
if (el.scaleMode !== yMap.get("scaleMode")) yMap.set("scaleMode", el.scaleMode);
|
|
3404
|
+
const cropJson = el.crop ? JSON.stringify(el.crop) : null;
|
|
3405
|
+
if (cropJson !== yMap.get("crop")) yMap.set("crop", cropJson);
|
|
3406
|
+
if (el.cornerRadius !== yMap.get("cornerRadius")) yMap.set("cornerRadius", el.cornerRadius);
|
|
3407
|
+
if (el.alt !== yMap.get("alt")) yMap.set("alt", el.alt);
|
|
3408
|
+
break;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
_yMapCollectionToElements(yElements) {
|
|
3412
|
+
const elements = [];
|
|
3413
|
+
for (const [, yMap] of yElements.entries()) {
|
|
3414
|
+
const el = yMapToElement(yMap);
|
|
3415
|
+
if (el) elements.push(el);
|
|
3416
|
+
}
|
|
3417
|
+
elements.sort((a, b) => {
|
|
3418
|
+
if (a.sortOrder && b.sortOrder) {
|
|
3419
|
+
return a.sortOrder < b.sortOrder ? -1 : a.sortOrder > b.sortOrder ? 1 : 0;
|
|
3420
|
+
}
|
|
3421
|
+
return 0;
|
|
3422
|
+
});
|
|
3423
|
+
return elements;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
function dataUrlToBlobUrl(dataUrl) {
|
|
3427
|
+
const commaIdx = dataUrl.indexOf(",");
|
|
3428
|
+
if (commaIdx === -1) throw new Error("Invalid data URL");
|
|
3429
|
+
const meta = dataUrl.slice(5, commaIdx);
|
|
3430
|
+
const isBase64 = meta.endsWith(";base64");
|
|
3431
|
+
const encoded = dataUrl.slice(commaIdx + 1);
|
|
3432
|
+
let bytes;
|
|
3433
|
+
if (isBase64) {
|
|
3434
|
+
const binary = atob(encoded);
|
|
3435
|
+
bytes = new Uint8Array(binary.length);
|
|
3436
|
+
for (let i = 0; i < binary.length; i++) {
|
|
3437
|
+
bytes[i] = binary.charCodeAt(i);
|
|
3438
|
+
}
|
|
3439
|
+
} else {
|
|
3440
|
+
const text = decodeURIComponent(encoded);
|
|
3441
|
+
bytes = new TextEncoder().encode(text);
|
|
3442
|
+
}
|
|
3443
|
+
const blob = new Blob([bytes], { type: "application/javascript" });
|
|
3444
|
+
return URL.createObjectURL(blob);
|
|
3445
|
+
}
|
|
3446
|
+
function createWorker(viteWorkerUrl, config) {
|
|
3447
|
+
if (typeof Worker === "undefined") return null;
|
|
3448
|
+
try {
|
|
3449
|
+
const raw = typeof viteWorkerUrl === "function" ? viteWorkerUrl() : viteWorkerUrl;
|
|
3450
|
+
const urlString = raw instanceof URL ? raw.href : String(raw);
|
|
3451
|
+
if (urlString.startsWith("data:")) {
|
|
3452
|
+
const blobUrl = dataUrlToBlobUrl(urlString);
|
|
3453
|
+
return new Worker(blobUrl);
|
|
3454
|
+
}
|
|
3455
|
+
return new Worker(raw instanceof URL ? raw : urlString, { type: "module" });
|
|
3456
|
+
} catch (err) {
|
|
3457
|
+
console.warn("[workerFactory] Worker creation failed, using sync fallback:", err);
|
|
3458
|
+
return null;
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
class SyncWorkerAdapter {
|
|
3462
|
+
constructor(callbacks) {
|
|
3463
|
+
__publicField(this, "_worker", null);
|
|
3464
|
+
__publicField(this, "_callbacks");
|
|
3465
|
+
this._callbacks = callbacks;
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Initialize and connect the sync worker.
|
|
3469
|
+
* Uses Vite's `?worker` import syntax for bundling.
|
|
3470
|
+
*/
|
|
3471
|
+
connect(config, syncDebounceMs) {
|
|
3472
|
+
this.dispose();
|
|
3473
|
+
this._worker = createWorker(
|
|
3474
|
+
() => "__worker_stub__"
|
|
3475
|
+
);
|
|
3476
|
+
if (!this._worker) {
|
|
3477
|
+
console.warn("[SyncWorkerBridge] Worker creation failed, collaboration disabled");
|
|
3478
|
+
this._callbacks.onStatus("disconnected");
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
this._worker.onmessage = (e) => {
|
|
3482
|
+
const msg = e.data;
|
|
3483
|
+
switch (msg.type) {
|
|
3484
|
+
case "status":
|
|
3485
|
+
this._callbacks.onStatus(msg.status);
|
|
3486
|
+
break;
|
|
3487
|
+
case "remote-update":
|
|
3488
|
+
this._callbacks.onRemoteUpdate(msg.elements);
|
|
3489
|
+
break;
|
|
3490
|
+
case "peers":
|
|
3491
|
+
this._callbacks.onPeers(msg.peers);
|
|
3492
|
+
break;
|
|
3493
|
+
case "error":
|
|
3494
|
+
this._callbacks.onError(msg.message);
|
|
3495
|
+
break;
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
this._worker.onerror = (err) => {
|
|
3499
|
+
this._callbacks.onError(`Worker error: ${err.message}`);
|
|
3500
|
+
};
|
|
3501
|
+
this._post({ type: "connect", config, syncDebounceMs });
|
|
3502
|
+
}
|
|
3503
|
+
disconnect() {
|
|
3504
|
+
this._post({ type: "disconnect" });
|
|
3505
|
+
}
|
|
3506
|
+
sendLocalUpdate(elements) {
|
|
3507
|
+
this._post({ type: "local-update", elements });
|
|
3508
|
+
}
|
|
3509
|
+
sendAwareness(update) {
|
|
3510
|
+
this._post({
|
|
3511
|
+
type: "awareness",
|
|
3512
|
+
cursor: update.cursor ?? null,
|
|
3513
|
+
selectedIds: update.selectedIds,
|
|
3514
|
+
activeTool: update.activeTool
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
dispose() {
|
|
3518
|
+
if (this._worker) {
|
|
3519
|
+
this._worker.terminate();
|
|
3520
|
+
this._worker = null;
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
_post(msg) {
|
|
3524
|
+
var _a;
|
|
3525
|
+
(_a = this._worker) == null ? void 0 : _a.postMessage(msg);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
let _modulesPromise = null;
|
|
3529
|
+
function loadCollabModules() {
|
|
3530
|
+
if (!_modulesPromise) {
|
|
3531
|
+
_modulesPromise = Promise.all([
|
|
3532
|
+
Promise.resolve().then(() => yjsProvider),
|
|
3533
|
+
Promise.resolve().then(() => syncBridge)
|
|
3534
|
+
]).then(([yjsProvider2, syncBridge2]) => ({ yjsProvider: yjsProvider2, syncBridge: syncBridge2 }));
|
|
3535
|
+
}
|
|
3536
|
+
return _modulesPromise;
|
|
3537
|
+
}
|
|
3538
|
+
function useCollaboration(config) {
|
|
3539
|
+
const [connectionStatus, setConnectionStatus] = useState("disconnected");
|
|
3540
|
+
const [peers, setPeers] = useState([]);
|
|
3541
|
+
const configRef = useRef(config);
|
|
3542
|
+
configRef.current = config;
|
|
3543
|
+
const throttleRef = useRef((config == null ? void 0 : config.awarenessThrottleMs) ?? 100);
|
|
3544
|
+
const lastCursorUpdateRef = useRef(0);
|
|
3545
|
+
const modulesRef = useRef(null);
|
|
3546
|
+
useEffect(() => {
|
|
3547
|
+
if (!config) {
|
|
3548
|
+
const loaded = modulesRef.current;
|
|
3549
|
+
if (loaded) {
|
|
3550
|
+
loaded.yjsProvider.destroyCollaborationProvider();
|
|
3551
|
+
loaded.syncBridge.stopSync();
|
|
3552
|
+
}
|
|
3553
|
+
setConnectionStatus("disconnected");
|
|
3554
|
+
setPeers([]);
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
let cancelled = false;
|
|
3558
|
+
let cleanup = null;
|
|
3559
|
+
loadCollabModules().then((mods) => {
|
|
3560
|
+
if (cancelled) return;
|
|
3561
|
+
modulesRef.current = mods;
|
|
3562
|
+
const { createCollaborationProvider: createCollaborationProvider2, onStatusChange: onStatusChange2, getRemoteAwareness: getRemoteAwareness2, updateAwareness: updateAwareness2 } = mods.yjsProvider;
|
|
3563
|
+
const { startSync: startSync2, stopSync: stopSync2 } = mods.syncBridge;
|
|
3564
|
+
const { provider } = createCollaborationProvider2(config);
|
|
3565
|
+
startSync2(config.syncDebounceMs ?? 50);
|
|
3566
|
+
const unsubStatus = onStatusChange2(setConnectionStatus);
|
|
3567
|
+
const awarenessHandler = () => {
|
|
3568
|
+
const remote = getRemoteAwareness2();
|
|
3569
|
+
setPeers(Array.from(remote.values()));
|
|
3570
|
+
};
|
|
3571
|
+
provider.awareness.on("change", awarenessHandler);
|
|
3572
|
+
const unsubStore = useCanvasStore.subscribe((state, prevState) => {
|
|
3573
|
+
if (state.selectedIds !== prevState.selectedIds) {
|
|
3574
|
+
updateAwareness2({ selectedIds: state.selectedIds });
|
|
3575
|
+
}
|
|
3576
|
+
if (state.activeTool !== prevState.activeTool) {
|
|
3577
|
+
updateAwareness2({ activeTool: state.activeTool });
|
|
3578
|
+
}
|
|
3579
|
+
});
|
|
3580
|
+
cleanup = () => {
|
|
3581
|
+
unsubStatus();
|
|
3582
|
+
unsubStore();
|
|
3583
|
+
provider.awareness.off("change", awarenessHandler);
|
|
3584
|
+
stopSync2();
|
|
3585
|
+
mods.yjsProvider.destroyCollaborationProvider();
|
|
3586
|
+
};
|
|
3587
|
+
}).catch((err) => {
|
|
3588
|
+
console.error(
|
|
3589
|
+
"[f1ow] Failed to load collaboration modules. Ensure `yjs` and `y-websocket` are installed when collaboration is enabled.",
|
|
3590
|
+
err
|
|
3591
|
+
);
|
|
3592
|
+
setConnectionStatus("disconnected");
|
|
3593
|
+
});
|
|
3594
|
+
return () => {
|
|
3595
|
+
cancelled = true;
|
|
3596
|
+
if (cleanup) cleanup();
|
|
3597
|
+
setConnectionStatus("disconnected");
|
|
3598
|
+
setPeers([]);
|
|
3599
|
+
};
|
|
3600
|
+
}, [
|
|
3601
|
+
// Re-create if server/room/user changes
|
|
3602
|
+
config == null ? void 0 : config.serverUrl,
|
|
3603
|
+
config == null ? void 0 : config.roomName,
|
|
3604
|
+
config == null ? void 0 : config.user.id,
|
|
3605
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3606
|
+
config == null ? void 0 : config.syncDebounceMs
|
|
3607
|
+
]);
|
|
3608
|
+
const updateCursor = useCallback((position) => {
|
|
3609
|
+
var _a;
|
|
3610
|
+
const now = Date.now();
|
|
3611
|
+
if (now - lastCursorUpdateRef.current < throttleRef.current) return;
|
|
3612
|
+
lastCursorUpdateRef.current = now;
|
|
3613
|
+
(_a = modulesRef.current) == null ? void 0 : _a.yjsProvider.updateAwareness({ cursor: position });
|
|
3614
|
+
}, []);
|
|
3615
|
+
const disconnect = useCallback(() => {
|
|
3616
|
+
var _a;
|
|
3617
|
+
const provider = (_a = modulesRef.current) == null ? void 0 : _a.yjsProvider.getYProvider();
|
|
3618
|
+
if (provider) provider.disconnect();
|
|
3619
|
+
}, []);
|
|
3620
|
+
const reconnect = useCallback(() => {
|
|
3621
|
+
var _a;
|
|
3622
|
+
const provider = (_a = modulesRef.current) == null ? void 0 : _a.yjsProvider.getYProvider();
|
|
3623
|
+
if (provider) provider.connect();
|
|
3624
|
+
}, []);
|
|
3625
|
+
return {
|
|
3626
|
+
isConnected: connectionStatus === "connected",
|
|
3627
|
+
connectionStatus,
|
|
3628
|
+
peers,
|
|
3629
|
+
disconnect,
|
|
3630
|
+
reconnect,
|
|
3631
|
+
updateCursor
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
const CURSOR_POINTS = [0, 0, 0, 18, 4.5, 14.5, 9, 20, 12, 18, 7.5, 12.5, 14, 12.5, 0, 0];
|
|
3635
|
+
const LABEL_FONT_SIZE = 12;
|
|
3636
|
+
const LABEL_FONT_FAMILY = "system-ui, -apple-system, sans-serif";
|
|
3637
|
+
const LABEL_PADDING_X = 6;
|
|
3638
|
+
const LABEL_PADDING_Y = 4;
|
|
3639
|
+
const LABEL_OFFSET_X = 14;
|
|
3640
|
+
const LABEL_OFFSET_Y = 18;
|
|
3641
|
+
const _measureCanvas = {
|
|
3642
|
+
ctx: null,
|
|
3643
|
+
cache: /* @__PURE__ */ new Map()
|
|
3644
|
+
};
|
|
3645
|
+
function measureTextWidth(text) {
|
|
3646
|
+
const cached = _measureCanvas.cache.get(text);
|
|
3647
|
+
if (cached !== void 0) return cached;
|
|
3648
|
+
if (!_measureCanvas.ctx) {
|
|
3649
|
+
try {
|
|
3650
|
+
const canvas = document.createElement("canvas");
|
|
3651
|
+
_measureCanvas.ctx = canvas.getContext("2d");
|
|
3652
|
+
} catch {
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
let width;
|
|
3656
|
+
if (_measureCanvas.ctx) {
|
|
3657
|
+
_measureCanvas.ctx.font = `${LABEL_FONT_SIZE}px ${LABEL_FONT_FAMILY}`;
|
|
3658
|
+
width = _measureCanvas.ctx.measureText(text).width;
|
|
3659
|
+
} else {
|
|
3660
|
+
width = text.length * 7;
|
|
3661
|
+
}
|
|
3662
|
+
if (_measureCanvas.cache.size > 200) {
|
|
3663
|
+
_measureCanvas.cache.clear();
|
|
3664
|
+
}
|
|
3665
|
+
_measureCanvas.cache.set(text, width);
|
|
3666
|
+
return width;
|
|
3667
|
+
}
|
|
3668
|
+
function isInViewport(wx, wy, viewport, stageW, stageH, margin) {
|
|
3669
|
+
const sx = (wx - viewport.x) * viewport.scale;
|
|
3670
|
+
const sy = (wy - viewport.y) * viewport.scale;
|
|
3671
|
+
return sx >= -margin && sx <= stageW + margin && sy >= -margin && sy <= stageH + margin;
|
|
3672
|
+
}
|
|
3673
|
+
function isElementInViewport(el, viewport, stageW, stageH, margin) {
|
|
3674
|
+
const cx = el.x + el.width / 2;
|
|
3675
|
+
const cy = el.y + el.height / 2;
|
|
3676
|
+
const halfDiag = Math.sqrt(el.width * el.width + el.height * el.height) / 2;
|
|
3677
|
+
return isInViewport(cx, cy, viewport, stageW, stageH, margin + halfDiag * viewport.scale);
|
|
3678
|
+
}
|
|
3679
|
+
const CursorOverlay = ({
|
|
3680
|
+
peers,
|
|
3681
|
+
viewport,
|
|
3682
|
+
stageWidth,
|
|
3683
|
+
stageHeight,
|
|
3684
|
+
elements
|
|
3685
|
+
}) => {
|
|
3686
|
+
const elementMap = useMemo(() => {
|
|
3687
|
+
const map = /* @__PURE__ */ new Map();
|
|
3688
|
+
for (const el of elements) map.set(el.id, el);
|
|
3689
|
+
return map;
|
|
3690
|
+
}, [elements]);
|
|
3691
|
+
const invScale = 1 / viewport.scale;
|
|
3692
|
+
const CULL_MARGIN = 150;
|
|
3693
|
+
return /* @__PURE__ */ jsx(Fragment, { children: peers.map((peer) => {
|
|
3694
|
+
if (!peer.cursor && peer.selectedIds.length === 0) return null;
|
|
3695
|
+
const { user } = peer;
|
|
3696
|
+
return /* @__PURE__ */ jsxs(Group, { children: [
|
|
3697
|
+
peer.selectedIds.map((selId) => {
|
|
3698
|
+
const el = elementMap.get(selId);
|
|
3699
|
+
if (!el) return null;
|
|
3700
|
+
if (!isElementInViewport(el, viewport, stageWidth, stageHeight, CULL_MARGIN)) {
|
|
3701
|
+
return null;
|
|
3702
|
+
}
|
|
3703
|
+
const pad = 3 * invScale;
|
|
3704
|
+
return /* @__PURE__ */ jsx(
|
|
3705
|
+
Rect,
|
|
3706
|
+
{
|
|
3707
|
+
x: el.x + el.width / 2,
|
|
3708
|
+
y: el.y + el.height / 2,
|
|
3709
|
+
offsetX: el.width / 2 + pad,
|
|
3710
|
+
offsetY: el.height / 2 + pad,
|
|
3711
|
+
width: el.width + pad * 2,
|
|
3712
|
+
height: el.height + pad * 2,
|
|
3713
|
+
rotation: el.rotation ?? 0,
|
|
3714
|
+
stroke: user.color,
|
|
3715
|
+
strokeWidth: 2 * invScale,
|
|
3716
|
+
dash: [6 * invScale, 4 * invScale],
|
|
3717
|
+
cornerRadius: 3 * invScale,
|
|
3718
|
+
listening: false,
|
|
3719
|
+
perfectDrawEnabled: false
|
|
3720
|
+
},
|
|
3721
|
+
selId
|
|
3722
|
+
);
|
|
3723
|
+
}),
|
|
3724
|
+
peer.cursor && isInViewport(
|
|
3725
|
+
peer.cursor.x,
|
|
3726
|
+
peer.cursor.y,
|
|
3727
|
+
viewport,
|
|
3728
|
+
stageWidth,
|
|
3729
|
+
stageHeight,
|
|
3730
|
+
CULL_MARGIN
|
|
3731
|
+
) && /* @__PURE__ */ jsxs(
|
|
3732
|
+
Group,
|
|
3733
|
+
{
|
|
3734
|
+
x: peer.cursor.x,
|
|
3735
|
+
y: peer.cursor.y,
|
|
3736
|
+
scaleX: invScale,
|
|
3737
|
+
scaleY: invScale,
|
|
3738
|
+
listening: false,
|
|
3739
|
+
children: [
|
|
3740
|
+
/* @__PURE__ */ jsx(
|
|
3741
|
+
Line,
|
|
3742
|
+
{
|
|
3743
|
+
points: CURSOR_POINTS,
|
|
3744
|
+
fill: user.color,
|
|
3745
|
+
stroke: "#ffffff",
|
|
3746
|
+
strokeWidth: 1,
|
|
3747
|
+
closed: true,
|
|
3748
|
+
perfectDrawEnabled: false
|
|
3749
|
+
}
|
|
3750
|
+
),
|
|
3751
|
+
/* @__PURE__ */ jsx(
|
|
3752
|
+
Rect,
|
|
3753
|
+
{
|
|
3754
|
+
x: LABEL_OFFSET_X,
|
|
3755
|
+
y: LABEL_OFFSET_Y,
|
|
3756
|
+
width: measureTextWidth(user.name) + LABEL_PADDING_X * 2,
|
|
3757
|
+
height: LABEL_FONT_SIZE + LABEL_PADDING_Y * 2,
|
|
3758
|
+
fill: user.color,
|
|
3759
|
+
cornerRadius: 4,
|
|
3760
|
+
listening: false,
|
|
3761
|
+
perfectDrawEnabled: false
|
|
3762
|
+
}
|
|
3763
|
+
),
|
|
3764
|
+
/* @__PURE__ */ jsx(
|
|
3765
|
+
Text,
|
|
3766
|
+
{
|
|
3767
|
+
x: LABEL_OFFSET_X + LABEL_PADDING_X,
|
|
3768
|
+
y: LABEL_OFFSET_Y + LABEL_PADDING_Y,
|
|
3769
|
+
text: user.name,
|
|
3770
|
+
fontSize: LABEL_FONT_SIZE,
|
|
3771
|
+
fontFamily: LABEL_FONT_FAMILY,
|
|
3772
|
+
fill: "#ffffff",
|
|
3773
|
+
listening: false,
|
|
3774
|
+
perfectDrawEnabled: false
|
|
3775
|
+
}
|
|
3776
|
+
)
|
|
3777
|
+
]
|
|
3778
|
+
}
|
|
3779
|
+
)
|
|
3780
|
+
] }, user.id);
|
|
3781
|
+
}) });
|
|
3782
|
+
};
|
|
3783
|
+
const CursorOverlay_default = React.memo(CursorOverlay);
|
|
3784
|
+
export {
|
|
3785
|
+
CollaborationManager,
|
|
3786
|
+
CursorOverlay_default as CursorOverlay,
|
|
3787
|
+
STYLE_FIELDS,
|
|
3788
|
+
SYNC_FIELDS,
|
|
3789
|
+
SyncWorkerAdapter,
|
|
3790
|
+
createCollaborationProvider,
|
|
3791
|
+
destroyCollaborationProvider,
|
|
3792
|
+
elementToYMap,
|
|
3793
|
+
getRemoteAwareness,
|
|
3794
|
+
getYDoc,
|
|
3795
|
+
getYElements,
|
|
3796
|
+
getYProvider,
|
|
3797
|
+
isCollaborationActive,
|
|
3798
|
+
onStatusChange,
|
|
3799
|
+
startSync,
|
|
3800
|
+
stopSync,
|
|
3801
|
+
updateAwareness,
|
|
3802
|
+
useCollaboration,
|
|
3803
|
+
yMapToElement
|
|
3804
|
+
};
|