f1ow 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ };