@canvas-harness/react 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1675 @@
1
+ 'use strict';
2
+
3
+ var core = require('@canvas-harness/core');
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/Canvas.tsx
8
+ var CanvasContext = react.createContext(null);
9
+ function CanvasProvider({ store, children }) {
10
+ return /* @__PURE__ */ jsxRuntime.jsx(CanvasContext.Provider, { value: store, children });
11
+ }
12
+ function useCanvasStore() {
13
+ const store = react.useContext(CanvasContext);
14
+ if (!store) {
15
+ throw new Error(
16
+ "useCanvasStore() must be used inside <CanvasProvider>. Wrap your tree with <CanvasProvider store={store}>."
17
+ );
18
+ }
19
+ return store;
20
+ }
21
+ function EditorMount({
22
+ store,
23
+ factory = core.createDefaultTextareaEditor
24
+ }) {
25
+ const hostRef = react.useRef(null);
26
+ react.useEffect(() => {
27
+ const host = hostRef.current;
28
+ if (!host) return;
29
+ let activeAdapter = null;
30
+ let currentEditingKey = null;
31
+ const teardown = () => {
32
+ if (activeAdapter) {
33
+ activeAdapter.destroy();
34
+ activeAdapter = null;
35
+ }
36
+ currentEditingKey = null;
37
+ };
38
+ const onInteraction = () => {
39
+ const state = store.getInteractionState();
40
+ const target = state.mode === "editing" ? state.editingTarget : null;
41
+ const key = target ? `${target.kind}:${target.id}` : null;
42
+ if (key === currentEditingKey) return;
43
+ teardown();
44
+ if (!target) return;
45
+ let editorNode = null;
46
+ if (target.kind === "node") {
47
+ editorNode = store.getNode(target.id) ?? null;
48
+ } else {
49
+ const edge = store.getEdge(target.id);
50
+ const geom = store.getEdgeGeometry(target.id);
51
+ if (edge && geom) editorNode = synthesizeLabelNode(edge, geom);
52
+ }
53
+ if (!editorNode) return;
54
+ currentEditingKey = key;
55
+ activeAdapter = factory({
56
+ node: editorNode,
57
+ container: host,
58
+ camera: store.getCamera(),
59
+ dpr: window.devicePixelRatio || 1,
60
+ onCommit: (text) => {
61
+ store.commitEdit(text);
62
+ },
63
+ onCancel: () => {
64
+ store.cancelEdit();
65
+ }
66
+ });
67
+ };
68
+ onInteraction();
69
+ const unsub = store.subscribe("interaction", onInteraction);
70
+ return () => {
71
+ unsub();
72
+ teardown();
73
+ };
74
+ }, [store, factory]);
75
+ return /* @__PURE__ */ jsxRuntime.jsx(
76
+ "div",
77
+ {
78
+ ref: hostRef,
79
+ style: {
80
+ position: "absolute",
81
+ inset: 0,
82
+ pointerEvents: "none"
83
+ // Children (the textarea) re-enable pointer events themselves.
84
+ }
85
+ }
86
+ );
87
+ }
88
+ var synthesizeLabelNode = (edge, geom) => {
89
+ const labelStyle = { fontFamily: "handwriting", ...edge.style };
90
+ const bounds = core.edgeLabelBoundsWorld(edge, geom);
91
+ if (!bounds) {
92
+ if (geom.samples.length < 2) return null;
93
+ const mid = geom.samples[Math.floor(geom.samples.length / 2)];
94
+ return {
95
+ id: core.asNodeId(`__edge-label:${edge.id}`),
96
+ type: "text",
97
+ x: mid.x - 60,
98
+ y: mid.y - 12,
99
+ w: 120,
100
+ h: 24,
101
+ angle: 0,
102
+ z: 0,
103
+ groups: [],
104
+ content: "",
105
+ style: labelStyle
106
+ };
107
+ }
108
+ return {
109
+ id: core.asNodeId(`__edge-label:${edge.id}`),
110
+ type: "text",
111
+ x: bounds.x,
112
+ y: bounds.y,
113
+ w: bounds.w,
114
+ h: bounds.h,
115
+ angle: 0,
116
+ z: 0,
117
+ groups: [],
118
+ content: edge.content ?? "",
119
+ style: { ...labelStyle, autoFit: false }
120
+ // labels don't autofit height
121
+ };
122
+ };
123
+ var CLICK_MAX_PIXELS = 4;
124
+ var useArrowTool = (ref, store, enabled, defaults) => {
125
+ const defaultsRef = react.useRef(defaults);
126
+ defaultsRef.current = defaults;
127
+ react.useEffect(() => {
128
+ if (!enabled) return;
129
+ const el = ref.current;
130
+ if (!el) return;
131
+ let pointerDownAt = null;
132
+ let active = false;
133
+ let sourceEnd = null;
134
+ const screenFromEvent = (e) => {
135
+ const rect = el.getBoundingClientRect();
136
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
137
+ };
138
+ const worldFromEvent = (e) => core.screenToWorld(screenFromEvent(e), store.getCamera());
139
+ const endFromWorldPoint = (world) => {
140
+ const hit = core.hitTestPoint(store, world, store.getCamera().z);
141
+ if (hit && hit.kind === "body") {
142
+ const node = store.getNode(hit.nodeId);
143
+ const localOffset = core.projectToNodeBoundary(world, node);
144
+ return { end: { nodeId: node.id, localOffset }, nodeId: node.id };
145
+ }
146
+ return { end: { worldPoint: world }, nodeId: null };
147
+ };
148
+ const followingEnd = (world) => {
149
+ const hit = core.hitTestPoint(store, world, store.getCamera().z);
150
+ if (hit && hit.kind === "body") {
151
+ const node = store.getNode(hit.nodeId);
152
+ const local = core.worldToNodeLocal(world, node);
153
+ const clamped = {
154
+ x: Math.max(0, Math.min(node.w, local.x)),
155
+ y: Math.max(0, Math.min(node.h, local.y))
156
+ };
157
+ return { end: { nodeId: node.id, localOffset: clamped }, nodeId: node.id };
158
+ }
159
+ return { end: { worldPoint: world }, nodeId: null };
160
+ };
161
+ const onPointerDown = (e) => {
162
+ if (e.button !== 0) return;
163
+ pointerDownAt = screenFromEvent(e);
164
+ const world = worldFromEvent(e);
165
+ const { end } = endFromWorldPoint(world);
166
+ sourceEnd = end;
167
+ el.setPointerCapture(e.pointerId);
168
+ e.preventDefault();
169
+ };
170
+ const onPointerMove = (e) => {
171
+ if (!pointerDownAt || !sourceEnd) return;
172
+ const screen = screenFromEvent(e);
173
+ const dx = screen.x - pointerDownAt.x;
174
+ const dy = screen.y - pointerDownAt.y;
175
+ if (!active && Math.abs(dx) < CLICK_MAX_PIXELS && Math.abs(dy) < CLICK_MAX_PIXELS) return;
176
+ active = true;
177
+ const world = worldFromEvent(e);
178
+ const { end: target, nodeId: snapTargetNodeId } = followingEnd(world);
179
+ store.setInteractionState({
180
+ mode: "creating-edge",
181
+ draftEdge: {
182
+ source: sourceEnd,
183
+ target,
184
+ reconnectingId: null,
185
+ snapTargetNodeId
186
+ }
187
+ });
188
+ };
189
+ const onPointerUp = (e) => {
190
+ if (!pointerDownAt) return;
191
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
192
+ const wasActive = active;
193
+ if (wasActive && sourceEnd) {
194
+ const world = worldFromEvent(e);
195
+ const { end: target } = endFromWorldPoint(world);
196
+ const d = defaultsRef.current;
197
+ store.addEdge({
198
+ id: core.asEdgeId(store.generateId()),
199
+ source: sourceEnd,
200
+ target,
201
+ pathStyle: d?.pathStyle ?? "bezier",
202
+ z: 0,
203
+ groups: [],
204
+ ...d?.style ? { style: d.style } : {}
205
+ });
206
+ }
207
+ store.resetInteractionState();
208
+ pointerDownAt = null;
209
+ active = false;
210
+ sourceEnd = null;
211
+ };
212
+ const onPointerCancel = (e) => {
213
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
214
+ store.resetInteractionState();
215
+ pointerDownAt = null;
216
+ active = false;
217
+ sourceEnd = null;
218
+ };
219
+ el.addEventListener("pointerdown", onPointerDown);
220
+ el.addEventListener("pointermove", onPointerMove);
221
+ el.addEventListener("pointerup", onPointerUp);
222
+ el.addEventListener("pointercancel", onPointerCancel);
223
+ return () => {
224
+ el.removeEventListener("pointerdown", onPointerDown);
225
+ el.removeEventListener("pointermove", onPointerMove);
226
+ el.removeEventListener("pointerup", onPointerUp);
227
+ el.removeEventListener("pointercancel", onPointerCancel);
228
+ };
229
+ }, [ref, store, enabled]);
230
+ };
231
+ var CLICK_MAX_PIXELS2 = 4;
232
+ var LONG_PRESS_MS = 500;
233
+ var LONG_PRESS_MAX_MOVE_PX = 10;
234
+ var useInteractionGesture = (ref, store, tool) => {
235
+ react.useEffect(() => {
236
+ const el = ref.current;
237
+ if (!el) return;
238
+ if (tool !== "select") return;
239
+ let pointerDownAt = null;
240
+ let activeGesture = "idle";
241
+ let resizeHandle = null;
242
+ let dragOriginals = [];
243
+ let marqueeStartWorld = null;
244
+ let marqueeShift = false;
245
+ let reconnectEdgeId = null;
246
+ let reconnectEnd = null;
247
+ let midpointEdgeId = null;
248
+ let rotateNodeId = null;
249
+ let rotateOriginAngle = 0;
250
+ let rotatePointerStartAngle = 0;
251
+ const palm = core.createPalmRejectionState();
252
+ let longPressTimer = null;
253
+ const clearLongPress = () => {
254
+ if (longPressTimer !== null) {
255
+ clearTimeout(longPressTimer);
256
+ longPressTimer = null;
257
+ }
258
+ };
259
+ const screenFromEvent = (e) => {
260
+ const rect = el.getBoundingClientRect();
261
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
262
+ };
263
+ const worldFromEvent = (e) => core.screenToWorld(screenFromEvent(e), store.getCamera());
264
+ const snapshotOriginals = (ids) => {
265
+ const result = [];
266
+ for (const id of ids) {
267
+ const n = store.getNode(id);
268
+ if (n) result.push({ id, x: n.x, y: n.y, w: n.w, h: n.h, angle: n.angle });
269
+ }
270
+ return result;
271
+ };
272
+ const beginDrag = (ids) => {
273
+ dragOriginals = snapshotOriginals(ids);
274
+ store.setInteractionState({
275
+ mode: "dragging",
276
+ draggedIds: ids,
277
+ dragOriginals,
278
+ dragDelta: { x: 0, y: 0 }
279
+ });
280
+ };
281
+ const beginResize = (id, handle) => {
282
+ dragOriginals = snapshotOriginals([id]);
283
+ store.setInteractionState({
284
+ mode: "resizing",
285
+ draggedIds: [id],
286
+ dragOriginals,
287
+ resizeHandle: handle,
288
+ resizeLockAspect: false,
289
+ resizeFromCenter: false
290
+ });
291
+ };
292
+ const pointerAngleFromCenter = (node, world) => {
293
+ const cx = node.x + node.w / 2;
294
+ const cy = node.y + node.h / 2;
295
+ return Math.atan2(world.y - cy, world.x - cx);
296
+ };
297
+ const beginRotate = (id, worldAtStart) => {
298
+ const node = store.getNode(id);
299
+ if (!node) return;
300
+ rotateNodeId = id;
301
+ rotateOriginAngle = node.angle;
302
+ rotatePointerStartAngle = pointerAngleFromCenter(node, worldAtStart);
303
+ store.setInteractionState({
304
+ mode: "rotating",
305
+ draggedIds: [id]
306
+ });
307
+ };
308
+ const ROTATE_SNAP_RAD = 15 * Math.PI / 180;
309
+ const updateRotate = (worldPoint, shift) => {
310
+ if (!rotateNodeId) return;
311
+ const node = store.getNode(rotateNodeId);
312
+ if (!node) return;
313
+ const pointerAngle = pointerAngleFromCenter(node, worldPoint);
314
+ const delta = pointerAngle - rotatePointerStartAngle;
315
+ let next = rotateOriginAngle + delta;
316
+ if (shift) next = Math.round(next / ROTATE_SNAP_RAD) * ROTATE_SNAP_RAD;
317
+ store.updateNode(rotateNodeId, { angle: next });
318
+ };
319
+ const commitRotate = () => {
320
+ rotateNodeId = null;
321
+ store.resetInteractionState();
322
+ };
323
+ const updateDrag = (delta) => {
324
+ store.setInteractionState({ dragDelta: delta });
325
+ };
326
+ const updateResize = (worldPoint, modifiers) => {
327
+ const orig = dragOriginals[0];
328
+ if (!orig || !resizeHandle) return;
329
+ const next = computeResizeGeometry(orig, resizeHandle, worldPoint, modifiers);
330
+ store.updateNode(orig.id, next);
331
+ store.setInteractionState({
332
+ resizeLockAspect: modifiers.shift,
333
+ resizeFromCenter: modifiers.alt
334
+ });
335
+ };
336
+ const commitDrag = () => {
337
+ const interaction = store.getInteractionState();
338
+ const delta = interaction.dragDelta;
339
+ if (delta.x !== 0 || delta.y !== 0) {
340
+ store.batch(() => {
341
+ for (const orig of dragOriginals) {
342
+ store.updateNode(orig.id, { x: orig.x + delta.x, y: orig.y + delta.y });
343
+ }
344
+ });
345
+ }
346
+ store.resetInteractionState();
347
+ };
348
+ const commitResize = () => {
349
+ const selected = store.getSelection();
350
+ for (const id of selected) {
351
+ const node = store.getNode(id);
352
+ if (!node) continue;
353
+ if (!core.shouldAutoFit(node)) continue;
354
+ const fitted = core.computeAutoFitHeight(node);
355
+ if (fitted > node.h) store.updateNode(node.id, { h: fitted });
356
+ }
357
+ store.resetInteractionState();
358
+ };
359
+ const beginMarquee = (start, shift) => {
360
+ marqueeStartWorld = start;
361
+ marqueeShift = shift;
362
+ store.setInteractionState({
363
+ mode: "marqueeing",
364
+ marqueeRect: { x: start.x, y: start.y, w: 0, h: 0 },
365
+ marqueeAdditive: shift
366
+ });
367
+ };
368
+ const updateMarquee = (current) => {
369
+ if (!marqueeStartWorld) return;
370
+ const rect = {
371
+ x: Math.min(marqueeStartWorld.x, current.x),
372
+ y: Math.min(marqueeStartWorld.y, current.y),
373
+ w: Math.abs(current.x - marqueeStartWorld.x),
374
+ h: Math.abs(current.y - marqueeStartWorld.y)
375
+ };
376
+ store.setInteractionState({ marqueeRect: rect });
377
+ };
378
+ const commitMarquee = () => {
379
+ const interaction = store.getInteractionState();
380
+ const rect = interaction.marqueeRect;
381
+ if (rect && (rect.w > 0 || rect.h > 0)) {
382
+ const hits = core.marqueeNodes(store, rect);
383
+ if (marqueeShift) {
384
+ const existing = new Set(store.getSelection());
385
+ for (const id of hits) existing.add(id);
386
+ store.setSelection([...existing]);
387
+ } else {
388
+ store.setSelection(hits);
389
+ }
390
+ }
391
+ store.resetInteractionState();
392
+ };
393
+ const onPointerDown = (e) => {
394
+ if (e.button !== 0) return;
395
+ if (e.pointerType === "pen") core.notePenActive(palm);
396
+ else if (e.pointerType === "touch" && core.shouldRejectTouch(palm, Date.now())) return;
397
+ pointerDownAt = screenFromEvent(e);
398
+ const world = worldFromEvent(e);
399
+ const camera = store.getCamera();
400
+ const selection = store.getSelection();
401
+ const selectedNodeIds = /* @__PURE__ */ new Set();
402
+ const selectedEdgeIds = /* @__PURE__ */ new Set();
403
+ for (const id of selection) {
404
+ if (store.getNode(id)) selectedNodeIds.add(id);
405
+ else if (store.getEdge(id)) selectedEdgeIds.add(id);
406
+ }
407
+ const hit = core.hitTestAny(store, world, camera.z, selectedNodeIds, selectedEdgeIds);
408
+ if (hit?.kind === "rotate-handle") {
409
+ activeGesture = "rotate";
410
+ beginRotate(hit.nodeId, world);
411
+ el.setPointerCapture(e.pointerId);
412
+ e.preventDefault();
413
+ return;
414
+ }
415
+ if (hit?.kind === "resize-handle") {
416
+ resizeHandle = hit.handle;
417
+ activeGesture = "resize";
418
+ beginResize(hit.nodeId, hit.handle);
419
+ el.setPointerCapture(e.pointerId);
420
+ e.preventDefault();
421
+ return;
422
+ }
423
+ if (hit?.kind === "midpoint-handle") {
424
+ midpointEdgeId = hit.edgeId;
425
+ activeGesture = "edge-midpoint";
426
+ el.setPointerCapture(e.pointerId);
427
+ e.preventDefault();
428
+ return;
429
+ }
430
+ if (hit?.kind === "source-handle" || hit?.kind === "target-handle") {
431
+ reconnectEdgeId = hit.edgeId;
432
+ reconnectEnd = hit.kind === "source-handle" ? "source" : "target";
433
+ activeGesture = "reconnect-edge";
434
+ const edge = store.getEdge(hit.edgeId);
435
+ if (edge) {
436
+ store.setInteractionState({
437
+ mode: "reconnecting-edge",
438
+ draftEdge: {
439
+ source: edge.source,
440
+ target: edge.target,
441
+ reconnectingId: hit.edgeId,
442
+ snapTargetNodeId: null
443
+ }
444
+ });
445
+ }
446
+ el.setPointerCapture(e.pointerId);
447
+ e.preventDefault();
448
+ return;
449
+ }
450
+ if (hit?.kind === "body" && "nodeId" in hit) {
451
+ const alreadySelected = selectedNodeIds.has(hit.nodeId);
452
+ if (e.shiftKey) {
453
+ const next = new Set(selectedNodeIds);
454
+ if (alreadySelected) next.delete(hit.nodeId);
455
+ else next.add(hit.nodeId);
456
+ store.setSelection([...next]);
457
+ } else if (!alreadySelected) {
458
+ store.setSelection([hit.nodeId]);
459
+ }
460
+ activeGesture = "click-pending";
461
+ el.setPointerCapture(e.pointerId);
462
+ if (e.pointerType === "touch") {
463
+ const targetIds = e.shiftKey ? [...selectedNodeIds, hit.nodeId] : [hit.nodeId];
464
+ clearLongPress();
465
+ longPressTimer = setTimeout(() => {
466
+ longPressTimer = null;
467
+ if (activeGesture !== "click-pending") return;
468
+ activeGesture = "drag";
469
+ beginDrag(targetIds);
470
+ }, LONG_PRESS_MS);
471
+ }
472
+ e.preventDefault();
473
+ return;
474
+ }
475
+ if (hit?.kind === "body" && "edgeId" in hit) {
476
+ if (e.shiftKey) {
477
+ const next = new Set(selectedEdgeIds);
478
+ if (selectedEdgeIds.has(hit.edgeId)) next.delete(hit.edgeId);
479
+ else next.add(hit.edgeId);
480
+ store.setSelection([...selectedNodeIds, ...next]);
481
+ } else {
482
+ store.setSelection([hit.edgeId]);
483
+ }
484
+ activeGesture = "click-pending";
485
+ el.setPointerCapture(e.pointerId);
486
+ e.preventDefault();
487
+ return;
488
+ }
489
+ if (!e.shiftKey) store.setSelection([]);
490
+ activeGesture = "click-pending";
491
+ el.setPointerCapture(e.pointerId);
492
+ };
493
+ const onPointerMove = (e) => {
494
+ if (!pointerDownAt) return;
495
+ const screen = screenFromEvent(e);
496
+ const dx = screen.x - pointerDownAt.x;
497
+ const dy = screen.y - pointerDownAt.y;
498
+ if (longPressTimer !== null && (Math.abs(dx) > LONG_PRESS_MAX_MOVE_PX || Math.abs(dy) > LONG_PRESS_MAX_MOVE_PX)) {
499
+ clearLongPress();
500
+ }
501
+ if (activeGesture === "click-pending") {
502
+ if (Math.abs(dx) < CLICK_MAX_PIXELS2 && Math.abs(dy) < CLICK_MAX_PIXELS2) return;
503
+ const startWorld = core.screenToWorld(pointerDownAt, store.getCamera());
504
+ const camera = store.getCamera();
505
+ const selectedIds = new Set(
506
+ store.getSelection().filter((id) => store.getNode(id))
507
+ );
508
+ const hit = core.hitTestAny(store, startWorld, camera.z, selectedIds, /* @__PURE__ */ new Set());
509
+ if (hit?.kind === "body" && "nodeId" in hit && selectedIds.has(hit.nodeId)) {
510
+ activeGesture = "drag";
511
+ beginDrag([...selectedIds]);
512
+ } else {
513
+ activeGesture = "marquee";
514
+ beginMarquee(startWorld, e.shiftKey);
515
+ }
516
+ }
517
+ if (activeGesture === "drag") {
518
+ const camera = store.getCamera();
519
+ updateDrag({ x: dx / camera.z, y: dy / camera.z });
520
+ } else if (activeGesture === "resize") {
521
+ const world = worldFromEvent(e);
522
+ updateResize(world, { shift: e.shiftKey, alt: e.altKey });
523
+ } else if (activeGesture === "rotate") {
524
+ updateRotate(worldFromEvent(e), e.shiftKey);
525
+ } else if (activeGesture === "marquee") {
526
+ updateMarquee(worldFromEvent(e));
527
+ } else if (activeGesture === "reconnect-edge" && reconnectEdgeId && reconnectEnd) {
528
+ updateReconnect(worldFromEvent(e));
529
+ } else if (activeGesture === "edge-midpoint" && midpointEdgeId) {
530
+ updateEdgeMidpoint(worldFromEvent(e));
531
+ }
532
+ };
533
+ const updateEdgeMidpoint = (world) => {
534
+ if (!midpointEdgeId) return;
535
+ const geom = store.getEdgeGeometry(midpointEdgeId);
536
+ if (!geom) return;
537
+ const { c1, c2 } = core.midpointToCubicControls(geom.source, world, geom.target);
538
+ store.updateEdge(midpointEdgeId, { control: [c1, c2] });
539
+ };
540
+ const updateReconnect = (world) => {
541
+ if (!reconnectEdgeId || !reconnectEnd) return;
542
+ const edge = store.getEdge(reconnectEdgeId);
543
+ if (!edge) return;
544
+ const camera = store.getCamera();
545
+ const newEnd = followingEnd(world, camera.z);
546
+ const draftSource = reconnectEnd === "source" ? newEnd.end : edge.source;
547
+ const draftTarget = reconnectEnd === "target" ? newEnd.end : edge.target;
548
+ store.setInteractionState({
549
+ mode: "reconnecting-edge",
550
+ draftEdge: {
551
+ source: draftSource,
552
+ target: draftTarget,
553
+ reconnectingId: reconnectEdgeId,
554
+ snapTargetNodeId: newEnd.nodeId
555
+ }
556
+ });
557
+ };
558
+ const followingEnd = (world, cameraZ) => {
559
+ const hit = core.hitTestAny(store, world, cameraZ);
560
+ if (hit?.kind === "body" && "nodeId" in hit) {
561
+ const node = store.getNode(hit.nodeId);
562
+ if (node) {
563
+ const local = core.worldToNodeLocal(world, node);
564
+ const clamped = {
565
+ x: Math.max(0, Math.min(node.w, local.x)),
566
+ y: Math.max(0, Math.min(node.h, local.y))
567
+ };
568
+ return { end: { nodeId: node.id, localOffset: clamped }, nodeId: node.id };
569
+ }
570
+ }
571
+ return { end: { worldPoint: world }, nodeId: null };
572
+ };
573
+ const commitReconnect = (e) => {
574
+ if (!reconnectEdgeId || !reconnectEnd) return;
575
+ const world = worldFromEvent(e);
576
+ const hit = core.hitTestAny(store, world, store.getCamera().z);
577
+ let newEnd;
578
+ if (hit?.kind === "body" && "nodeId" in hit) {
579
+ const node = store.getNode(hit.nodeId);
580
+ if (node) {
581
+ const localOffset = core.projectToNodeBoundary(world, node);
582
+ newEnd = { nodeId: node.id, localOffset };
583
+ } else {
584
+ newEnd = { worldPoint: world };
585
+ }
586
+ } else {
587
+ newEnd = { worldPoint: world };
588
+ }
589
+ store.updateEdge(
590
+ reconnectEdgeId,
591
+ reconnectEnd === "source" ? { source: newEnd } : { target: newEnd }
592
+ );
593
+ store.resetInteractionState();
594
+ };
595
+ const onPointerUp = (e) => {
596
+ if (e.pointerType === "pen") core.notePenInactive(palm, Date.now());
597
+ clearLongPress();
598
+ if (!pointerDownAt) return;
599
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
600
+ switch (activeGesture) {
601
+ case "drag":
602
+ commitDrag();
603
+ break;
604
+ case "resize":
605
+ commitResize();
606
+ break;
607
+ case "rotate":
608
+ commitRotate();
609
+ break;
610
+ case "marquee":
611
+ commitMarquee();
612
+ break;
613
+ case "reconnect-edge":
614
+ commitReconnect(e);
615
+ break;
616
+ }
617
+ pointerDownAt = null;
618
+ activeGesture = "idle";
619
+ resizeHandle = null;
620
+ dragOriginals = [];
621
+ marqueeStartWorld = null;
622
+ reconnectEdgeId = null;
623
+ midpointEdgeId = null;
624
+ reconnectEnd = null;
625
+ };
626
+ const onKeyDown = (e) => {
627
+ const target = e.target;
628
+ if (target && (target.tagName === "TEXTAREA" || target.tagName === "INPUT" || target.isContentEditable))
629
+ return;
630
+ if (e.key === "Escape") {
631
+ store.setSelection([]);
632
+ store.resetInteractionState();
633
+ }
634
+ if ((e.key === "Delete" || e.key === "Backspace") && store.getSelection().length > 0) {
635
+ const ids = store.getSelection();
636
+ store.batch(() => {
637
+ for (const id of ids) {
638
+ if (store.getNode(id)) store.removeNode(id);
639
+ else if (store.getEdge(id)) store.removeEdge(id);
640
+ }
641
+ });
642
+ store.setSelection([]);
643
+ }
644
+ };
645
+ el.addEventListener("pointerdown", onPointerDown);
646
+ el.addEventListener("pointermove", onPointerMove);
647
+ el.addEventListener("pointerup", onPointerUp);
648
+ el.addEventListener("pointercancel", onPointerUp);
649
+ window.addEventListener("keydown", onKeyDown);
650
+ return () => {
651
+ el.removeEventListener("pointerdown", onPointerDown);
652
+ el.removeEventListener("pointermove", onPointerMove);
653
+ el.removeEventListener("pointerup", onPointerUp);
654
+ el.removeEventListener("pointercancel", onPointerUp);
655
+ window.removeEventListener("keydown", onKeyDown);
656
+ };
657
+ }, [ref, store, tool]);
658
+ };
659
+ var computeResizeGeometry = (orig, handle, pointer, modifiers) => {
660
+ let x = orig.x;
661
+ let y = orig.y;
662
+ let w = orig.w;
663
+ let h = orig.h;
664
+ const rightFixed = handle === "nw" || handle === "w" || handle === "sw";
665
+ const leftFixed = handle === "ne" || handle === "e" || handle === "se";
666
+ const bottomFixed = handle === "nw" || handle === "n" || handle === "ne";
667
+ const topFixed = handle === "sw" || handle === "s" || handle === "se";
668
+ if (rightFixed) {
669
+ const right = orig.x + orig.w;
670
+ w = Math.max(1, right - pointer.x);
671
+ x = right - w;
672
+ } else if (leftFixed) {
673
+ w = Math.max(1, pointer.x - orig.x);
674
+ }
675
+ if (bottomFixed) {
676
+ const bottom = orig.y + orig.h;
677
+ h = Math.max(1, bottom - pointer.y);
678
+ y = bottom - h;
679
+ } else if (topFixed) {
680
+ h = Math.max(1, pointer.y - orig.y);
681
+ }
682
+ if (modifiers.shift) {
683
+ const targetAspect = orig.w / orig.h;
684
+ const currentAspect = w / h;
685
+ if (currentAspect > targetAspect) {
686
+ w = h * targetAspect;
687
+ if (rightFixed) x = orig.x + orig.w - w;
688
+ } else {
689
+ h = w / targetAspect;
690
+ if (bottomFixed) y = orig.y + orig.h - h;
691
+ }
692
+ }
693
+ if (modifiers.alt) {
694
+ const cx = orig.x + orig.w / 2;
695
+ const cy = orig.y + orig.h / 2;
696
+ if (handle !== "n" && handle !== "s") {
697
+ w = Math.max(1, w * 2 - orig.w + (orig.w - Math.abs(orig.w - w * 0)));
698
+ const newW = Math.abs(pointer.x - cx) * 2;
699
+ w = Math.max(1, newW);
700
+ x = cx - w / 2;
701
+ }
702
+ if (handle !== "e" && handle !== "w") {
703
+ const newH = Math.abs(pointer.y - cy) * 2;
704
+ h = Math.max(1, newH);
705
+ y = cy - h / 2;
706
+ }
707
+ }
708
+ return { x, y, w, h };
709
+ };
710
+ var useOverlayHost = () => {
711
+ const [mountedIds, setMountedIds] = react.useState([]);
712
+ react.useEffect(() => {
713
+ }, []);
714
+ return { mountedIds, setMountedIds };
715
+ };
716
+ var usePanZoom = (ref, store) => {
717
+ react.useEffect(() => {
718
+ const el = ref.current;
719
+ if (!el) return;
720
+ let panning = false;
721
+ let panActivatedBySpace = false;
722
+ let lastX = 0;
723
+ let lastY = 0;
724
+ let pendingDx = 0;
725
+ let pendingDy = 0;
726
+ let pendingZoomFactor = 1;
727
+ let pendingZoomAnchor = null;
728
+ let scheduled = false;
729
+ let rafId = 0;
730
+ const MOTION_RESET_MS = 150;
731
+ let motionEndDeadline = 0;
732
+ let motionEndPolling = false;
733
+ const setMotion = (mode) => {
734
+ const current = store.getInteractionState().mode;
735
+ if (mode === null) {
736
+ if (current === "panning" || current === "zooming") {
737
+ store.setInteractionState({ mode: "idle" });
738
+ }
739
+ return;
740
+ }
741
+ if (current !== "idle" && current !== "panning" && current !== "zooming") return;
742
+ if (current !== mode) store.setInteractionState({ mode });
743
+ };
744
+ const pollMotionEnd = () => {
745
+ if (performance.now() >= motionEndDeadline) {
746
+ motionEndPolling = false;
747
+ setMotion(null);
748
+ return;
749
+ }
750
+ requestAnimationFrame(pollMotionEnd);
751
+ };
752
+ const pulseMotion = (mode) => {
753
+ motionEndDeadline = performance.now() + MOTION_RESET_MS;
754
+ const current = store.getInteractionState().mode;
755
+ if (current !== mode) setMotion(mode);
756
+ if (!motionEndPolling) {
757
+ motionEndPolling = true;
758
+ requestAnimationFrame(pollMotionEnd);
759
+ }
760
+ };
761
+ const activeTouches = /* @__PURE__ */ new Map();
762
+ let lastPinchDistance = 0;
763
+ let lastPinchMidpoint = null;
764
+ const palm = core.createPalmRejectionState();
765
+ const flushPending = () => {
766
+ scheduled = false;
767
+ rafId = 0;
768
+ if (pendingZoomFactor !== 1 && pendingZoomAnchor) {
769
+ const camera = store.getCamera();
770
+ store.setCamera(
771
+ core.zoomAtScreenPoint(camera, core.clampZoom(camera.z * pendingZoomFactor), pendingZoomAnchor)
772
+ );
773
+ pendingZoomFactor = 1;
774
+ pendingZoomAnchor = null;
775
+ }
776
+ if (pendingDx !== 0 || pendingDy !== 0) {
777
+ const camera = store.getCamera();
778
+ store.setCamera(core.panByScreen(camera, { x: pendingDx, y: pendingDy }));
779
+ pendingDx = 0;
780
+ pendingDy = 0;
781
+ }
782
+ };
783
+ const schedule = () => {
784
+ if (scheduled) return;
785
+ scheduled = true;
786
+ rafId = requestAnimationFrame(flushPending);
787
+ };
788
+ const isEditing = () => store.getInteractionState().mode === "editing";
789
+ const screenFromClient = (clientX, clientY) => {
790
+ const rect = el.getBoundingClientRect();
791
+ return { x: clientX - rect.left, y: clientY - rect.top };
792
+ };
793
+ const updatePointerInfo = (e) => {
794
+ const { x: sx, y: sy } = screenFromClient(e.clientX, e.clientY);
795
+ const camera = store.getCamera();
796
+ store.setInteractionState({
797
+ pointer: {
798
+ worldX: sx / camera.z + camera.x,
799
+ worldY: sy / camera.z + camera.y,
800
+ screenX: sx,
801
+ screenY: sy,
802
+ pointerType: e.pointerType,
803
+ pressure: e.pointerType === "pen" ? e.pressure : void 0
804
+ }
805
+ });
806
+ };
807
+ const onWheel = (e) => {
808
+ if (isEditing()) return;
809
+ e.preventDefault();
810
+ if (e.ctrlKey || e.metaKey) {
811
+ const factor = Math.exp(-e.deltaY * 0.01);
812
+ pendingZoomFactor *= factor;
813
+ pendingZoomAnchor = screenFromClient(e.clientX, e.clientY);
814
+ pulseMotion("zooming");
815
+ } else {
816
+ pendingDx += -e.deltaX;
817
+ pendingDy += -e.deltaY;
818
+ pulseMotion("panning");
819
+ }
820
+ schedule();
821
+ };
822
+ const resetPinchTracking = () => {
823
+ lastPinchDistance = 0;
824
+ lastPinchMidpoint = null;
825
+ };
826
+ const recomputePinchSeed = () => {
827
+ const pts = [...activeTouches.values()];
828
+ if (pts.length !== 2) {
829
+ resetPinchTracking();
830
+ return;
831
+ }
832
+ const [a, b] = pts;
833
+ lastPinchDistance = Math.hypot(a.x - b.x, a.y - b.y);
834
+ lastPinchMidpoint = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
835
+ };
836
+ const onPointerDown = (e) => {
837
+ if (isEditing()) return;
838
+ if (e.pointerType === "pen") {
839
+ core.notePenActive(palm);
840
+ } else if (e.pointerType === "touch" && core.shouldRejectTouch(palm, Date.now())) {
841
+ return;
842
+ }
843
+ if (e.pointerType === "touch") {
844
+ const pt = screenFromClient(e.clientX, e.clientY);
845
+ activeTouches.set(e.pointerId, { id: e.pointerId, x: pt.x, y: pt.y });
846
+ if (activeTouches.size === 2) {
847
+ recomputePinchSeed();
848
+ el.setPointerCapture(e.pointerId);
849
+ setMotion("zooming");
850
+ e.preventDefault();
851
+ }
852
+ return;
853
+ }
854
+ if (e.button === 1 || e.button === 0 && panActivatedBySpace) {
855
+ panning = true;
856
+ lastX = e.clientX;
857
+ lastY = e.clientY;
858
+ el.setPointerCapture(e.pointerId);
859
+ setMotion("panning");
860
+ e.preventDefault();
861
+ }
862
+ };
863
+ const onPointerMove = (e) => {
864
+ updatePointerInfo(e);
865
+ if (e.pointerType === "touch" && activeTouches.has(e.pointerId)) {
866
+ const pt = screenFromClient(e.clientX, e.clientY);
867
+ activeTouches.set(e.pointerId, { id: e.pointerId, x: pt.x, y: pt.y });
868
+ if (activeTouches.size === 2 && lastPinchMidpoint && lastPinchDistance > 0) {
869
+ const pts = [...activeTouches.values()];
870
+ const [a, b] = pts;
871
+ const dist = Math.hypot(a.x - b.x, a.y - b.y);
872
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
873
+ const factor = dist / lastPinchDistance;
874
+ if (Number.isFinite(factor) && factor > 0) {
875
+ pendingZoomFactor *= factor;
876
+ pendingZoomAnchor = mid;
877
+ }
878
+ pendingDx += mid.x - lastPinchMidpoint.x;
879
+ pendingDy += mid.y - lastPinchMidpoint.y;
880
+ lastPinchDistance = dist;
881
+ lastPinchMidpoint = mid;
882
+ schedule();
883
+ }
884
+ return;
885
+ }
886
+ if (!panning) return;
887
+ const dx = e.clientX - lastX;
888
+ const dy = e.clientY - lastY;
889
+ lastX = e.clientX;
890
+ lastY = e.clientY;
891
+ pendingDx += dx;
892
+ pendingDy += dy;
893
+ schedule();
894
+ };
895
+ const onPointerUp = (e) => {
896
+ if (e.pointerType === "pen") core.notePenInactive(palm, Date.now());
897
+ if (e.pointerType === "touch" && activeTouches.has(e.pointerId)) {
898
+ activeTouches.delete(e.pointerId);
899
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
900
+ if (activeTouches.size === 2) recomputePinchSeed();
901
+ else {
902
+ resetPinchTracking();
903
+ setMotion(null);
904
+ }
905
+ return;
906
+ }
907
+ if (!panning) return;
908
+ panning = false;
909
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
910
+ setMotion(null);
911
+ };
912
+ const onPointerCancel = (e) => {
913
+ if (e.pointerType === "touch") {
914
+ activeTouches.delete(e.pointerId);
915
+ if (activeTouches.size < 2) {
916
+ resetPinchTracking();
917
+ setMotion(null);
918
+ }
919
+ }
920
+ if (panning) {
921
+ panning = false;
922
+ setMotion(null);
923
+ }
924
+ if (e.pointerType === "pen") core.notePenInactive(palm, Date.now());
925
+ };
926
+ const onKeyDown = (e) => {
927
+ if (e.code === "Space") panActivatedBySpace = true;
928
+ };
929
+ const onKeyUp = (e) => {
930
+ if (e.code === "Space") panActivatedBySpace = false;
931
+ };
932
+ el.addEventListener("wheel", onWheel, { passive: false });
933
+ el.addEventListener("pointerdown", onPointerDown);
934
+ el.addEventListener("pointermove", onPointerMove);
935
+ el.addEventListener("pointerup", onPointerUp);
936
+ el.addEventListener("pointercancel", onPointerCancel);
937
+ window.addEventListener("keydown", onKeyDown);
938
+ window.addEventListener("keyup", onKeyUp);
939
+ return () => {
940
+ el.removeEventListener("wheel", onWheel);
941
+ el.removeEventListener("pointerdown", onPointerDown);
942
+ el.removeEventListener("pointermove", onPointerMove);
943
+ el.removeEventListener("pointerup", onPointerUp);
944
+ el.removeEventListener("pointercancel", onPointerCancel);
945
+ window.removeEventListener("keydown", onKeyDown);
946
+ window.removeEventListener("keyup", onKeyUp);
947
+ if (rafId !== 0) cancelAnimationFrame(rafId);
948
+ };
949
+ }, [ref, store]);
950
+ };
951
+ var useResizeObserver = (ref) => {
952
+ const [size, setSize] = react.useState({ w: 0, h: 0 });
953
+ react.useEffect(() => {
954
+ const el = ref.current;
955
+ if (!el) return;
956
+ const ro = new ResizeObserver((entries) => {
957
+ const entry = entries[0];
958
+ if (!entry) return;
959
+ const { width, height } = entry.contentRect;
960
+ setSize({ w: Math.round(width), h: Math.round(height) });
961
+ });
962
+ ro.observe(el);
963
+ return () => ro.disconnect();
964
+ }, [ref]);
965
+ return size;
966
+ };
967
+ function Canvas(props) {
968
+ if (props.store) {
969
+ return /* @__PURE__ */ jsxRuntime.jsx(CanvasProvider, { store: props.store, children: /* @__PURE__ */ jsxRuntime.jsx(CanvasSurface, { ...props }) });
970
+ }
971
+ return /* @__PURE__ */ jsxRuntime.jsx(CanvasSurface, { ...props });
972
+ }
973
+ var DRAG_CREATE_MIN_SIZE_PX = 5;
974
+ function CanvasSurface({
975
+ tool,
976
+ theme,
977
+ editorAdapter,
978
+ onRenderer,
979
+ onClick,
980
+ onDoubleClick,
981
+ onCreateDrag,
982
+ arrowDefaults,
983
+ background,
984
+ renderCustomNodeView,
985
+ children
986
+ }) {
987
+ const store = useCanvasStore();
988
+ const wrapRef = react.useRef(null);
989
+ const staticRef = react.useRef(null);
990
+ const interactiveRef = react.useRef(null);
991
+ const overlayRef = react.useRef(null);
992
+ const rendererRef = react.useRef(null);
993
+ const toolRef = react.useRef(tool);
994
+ toolRef.current = tool;
995
+ const { w, h } = useResizeObserver(wrapRef);
996
+ usePanZoom(wrapRef, store);
997
+ useInteractionGesture(wrapRef, store, tool);
998
+ useArrowTool(wrapRef, store, tool === "arrow", arrowDefaults);
999
+ const { mountedIds, setMountedIds } = useOverlayHost();
1000
+ const [camera, setCamera] = react.useState(() => store.getCamera());
1001
+ react.useEffect(() => store.subscribe("camera", (c) => setCamera({ ...c })), [store]);
1002
+ react.useEffect(() => {
1003
+ if (!staticRef.current || !interactiveRef.current || w === 0 || h === 0) return;
1004
+ if (rendererRef.current) {
1005
+ rendererRef.current.setSize(w, h);
1006
+ return;
1007
+ }
1008
+ const r = core.createRenderer({
1009
+ store,
1010
+ staticCanvas: staticRef.current,
1011
+ interactiveCanvas: interactiveRef.current,
1012
+ theme,
1013
+ width: w,
1014
+ height: h,
1015
+ background,
1016
+ onOverlayChange: (ids) => setMountedIds(ids)
1017
+ });
1018
+ r.start();
1019
+ rendererRef.current = r;
1020
+ onRenderer?.(r);
1021
+ return () => {
1022
+ r.dispose();
1023
+ rendererRef.current = null;
1024
+ };
1025
+ }, [store, theme, w, h, onRenderer, setMountedIds]);
1026
+ react.useEffect(() => {
1027
+ rendererRef.current?.setBackground(background);
1028
+ }, [background]);
1029
+ react.useEffect(() => {
1030
+ const el = wrapRef.current;
1031
+ if (!el) return;
1032
+ const dispatch = (e, cb) => {
1033
+ if (!cb) return;
1034
+ const rect = el.getBoundingClientRect();
1035
+ const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
1036
+ const world = core.screenToWorld(screen, store.getCamera());
1037
+ cb({ screen, world, tool: toolRef.current, native: e });
1038
+ };
1039
+ const onClickHandler = (e) => dispatch(e, onClick);
1040
+ const onDoubleClickHandler = (e) => {
1041
+ if (toolRef.current === "select") {
1042
+ const rect = el.getBoundingClientRect();
1043
+ const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
1044
+ const camera2 = store.getCamera();
1045
+ const world = core.screenToWorld(screen, camera2);
1046
+ const hit = core.hitTestAny(store, world, camera2.z);
1047
+ if (hit && hit.kind === "body" && "nodeId" in hit) {
1048
+ store.beginEdit(hit.nodeId);
1049
+ } else if (hit && hit.kind === "body" && "edgeId" in hit) {
1050
+ store.beginEdit(hit.edgeId);
1051
+ } else if (hit && hit.kind === "label") {
1052
+ store.beginEdit(hit.edgeId);
1053
+ } else if (hit && hit.kind === "midpoint-handle") {
1054
+ store.updateEdge(hit.edgeId, { control: void 0 });
1055
+ }
1056
+ }
1057
+ dispatch(e, onDoubleClick);
1058
+ };
1059
+ el.addEventListener("click", onClickHandler);
1060
+ el.addEventListener("dblclick", onDoubleClickHandler);
1061
+ return () => {
1062
+ el.removeEventListener("click", onClickHandler);
1063
+ el.removeEventListener("dblclick", onDoubleClickHandler);
1064
+ };
1065
+ }, [store, onClick, onDoubleClick]);
1066
+ react.useEffect(() => {
1067
+ const el = wrapRef.current;
1068
+ if (!el || !onCreateDrag) return;
1069
+ let startWorld = null;
1070
+ let startScreen = null;
1071
+ let activePointerId = null;
1072
+ let committed = false;
1073
+ const justCommittedRef = { current: false };
1074
+ const screenFromEvent = (e) => {
1075
+ const rect = el.getBoundingClientRect();
1076
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
1077
+ };
1078
+ const worldFromEvent = (e) => core.screenToWorld(screenFromEvent(e), store.getCamera());
1079
+ const isShapeTool = (t) => t !== "select" && t !== "arrow" && t !== "text";
1080
+ const onPointerDown = (e) => {
1081
+ if (e.button !== 0) return;
1082
+ if (!isShapeTool(toolRef.current)) return;
1083
+ if (store.getInteractionState().mode === "editing") return;
1084
+ const camera2 = store.getCamera();
1085
+ const world = core.screenToWorld(screenFromEvent(e), camera2);
1086
+ if (core.hitTestAny(store, world, camera2.z)) return;
1087
+ startWorld = world;
1088
+ startScreen = screenFromEvent(e);
1089
+ activePointerId = e.pointerId;
1090
+ committed = false;
1091
+ el.setPointerCapture(e.pointerId);
1092
+ };
1093
+ const onPointerMove = (e) => {
1094
+ if (startWorld === null || startScreen === null) return;
1095
+ if (e.pointerId !== activePointerId) return;
1096
+ const screen = screenFromEvent(e);
1097
+ const dx = screen.x - startScreen.x;
1098
+ const dy = screen.y - startScreen.y;
1099
+ if (!committed && Math.abs(dx) < DRAG_CREATE_MIN_SIZE_PX && Math.abs(dy) < DRAG_CREATE_MIN_SIZE_PX) {
1100
+ return;
1101
+ }
1102
+ if (!committed) committed = true;
1103
+ const world = worldFromEvent(e);
1104
+ const rect = {
1105
+ x: Math.min(startWorld.x, world.x),
1106
+ y: Math.min(startWorld.y, world.y),
1107
+ w: Math.abs(world.x - startWorld.x),
1108
+ h: Math.abs(world.y - startWorld.y)
1109
+ };
1110
+ store.setInteractionState({
1111
+ mode: "creating-shape",
1112
+ createDraftRect: rect,
1113
+ createTool: toolRef.current
1114
+ });
1115
+ };
1116
+ const onPointerUp = (e) => {
1117
+ if (activePointerId !== e.pointerId) return;
1118
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
1119
+ const wasCommitted = committed;
1120
+ activePointerId = null;
1121
+ if (!wasCommitted || !startWorld) {
1122
+ startWorld = null;
1123
+ startScreen = null;
1124
+ return;
1125
+ }
1126
+ const world = worldFromEvent(e);
1127
+ const rect = {
1128
+ x: Math.min(startWorld.x, world.x),
1129
+ y: Math.min(startWorld.y, world.y),
1130
+ w: Math.abs(world.x - startWorld.x),
1131
+ h: Math.abs(world.y - startWorld.y)
1132
+ };
1133
+ startWorld = null;
1134
+ startScreen = null;
1135
+ store.resetInteractionState();
1136
+ justCommittedRef.current = true;
1137
+ setTimeout(() => {
1138
+ justCommittedRef.current = false;
1139
+ }, 0);
1140
+ onCreateDrag({ rect, tool: toolRef.current, native: e });
1141
+ };
1142
+ const onClickCapture = (e) => {
1143
+ if (justCommittedRef.current) {
1144
+ e.stopPropagation();
1145
+ e.preventDefault();
1146
+ }
1147
+ };
1148
+ el.addEventListener("pointerdown", onPointerDown);
1149
+ el.addEventListener("pointermove", onPointerMove);
1150
+ el.addEventListener("pointerup", onPointerUp);
1151
+ el.addEventListener("pointercancel", onPointerUp);
1152
+ el.addEventListener("click", onClickCapture, true);
1153
+ return () => {
1154
+ el.removeEventListener("pointerdown", onPointerDown);
1155
+ el.removeEventListener("pointermove", onPointerMove);
1156
+ el.removeEventListener("pointerup", onPointerUp);
1157
+ el.removeEventListener("pointercancel", onPointerUp);
1158
+ el.removeEventListener("click", onClickCapture, true);
1159
+ };
1160
+ }, [store, onCreateDrag]);
1161
+ react.useEffect(() => {
1162
+ const onKey = (e) => {
1163
+ const target = e.target;
1164
+ if (target && (target.tagName === "TEXTAREA" || target.tagName === "INPUT")) return;
1165
+ const meta = e.metaKey || e.ctrlKey;
1166
+ if (!meta) return;
1167
+ if (e.key === "c" || e.key === "C") {
1168
+ e.preventDefault();
1169
+ void core.copy(store);
1170
+ } else if (e.key === "x" || e.key === "X") {
1171
+ e.preventDefault();
1172
+ void core.cut(store);
1173
+ } else if (e.key === "v" || e.key === "V") {
1174
+ e.preventDefault();
1175
+ void core.paste(store);
1176
+ } else if (e.key === "]") {
1177
+ const selection = store.getSelection();
1178
+ if (selection.length === 0) return;
1179
+ e.preventDefault();
1180
+ if (e.shiftKey) store.bringToFront(selection);
1181
+ else store.bringForward(selection);
1182
+ } else if (e.key === "[") {
1183
+ const selection = store.getSelection();
1184
+ if (selection.length === 0) return;
1185
+ e.preventDefault();
1186
+ if (e.shiftKey) store.sendToBack(selection);
1187
+ else store.sendBackward(selection);
1188
+ }
1189
+ };
1190
+ window.addEventListener("keydown", onKey);
1191
+ return () => window.removeEventListener("keydown", onKey);
1192
+ }, [store]);
1193
+ const overlayTransform = `translate(${-camera.x * camera.z}px, ${-camera.y * camera.z}px) scale(${camera.z})`;
1194
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1195
+ "div",
1196
+ {
1197
+ ref: wrapRef,
1198
+ "data-canvas-host": "",
1199
+ style: {
1200
+ position: "absolute",
1201
+ inset: 0,
1202
+ background: "#f8fafc",
1203
+ overflow: "hidden",
1204
+ cursor: tool === "select" ? "default" : "crosshair",
1205
+ touchAction: "none"
1206
+ },
1207
+ children: [
1208
+ /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: staticRef, style: { position: "absolute", inset: 0, pointerEvents: "none" } }),
1209
+ /* @__PURE__ */ jsxRuntime.jsx(
1210
+ "div",
1211
+ {
1212
+ ref: overlayRef,
1213
+ style: {
1214
+ position: "absolute",
1215
+ inset: 0,
1216
+ transformOrigin: "0 0",
1217
+ transform: overlayTransform,
1218
+ pointerEvents: "none"
1219
+ },
1220
+ children: renderCustomNodeView ? mountedIds.map((id) => /* @__PURE__ */ jsxRuntime.jsx(OverlayItem, { id, children: renderCustomNodeView(id) }, id)) : null
1221
+ }
1222
+ ),
1223
+ /* @__PURE__ */ jsxRuntime.jsx(
1224
+ "canvas",
1225
+ {
1226
+ ref: interactiveRef,
1227
+ style: { position: "absolute", inset: 0, pointerEvents: "none" }
1228
+ }
1229
+ ),
1230
+ /* @__PURE__ */ jsxRuntime.jsx(EditorMount, { store, factory: editorAdapter }),
1231
+ children
1232
+ ]
1233
+ }
1234
+ );
1235
+ }
1236
+ function OverlayItem({ id, children }) {
1237
+ const store = useCanvasStore();
1238
+ const [node, setNode] = react.useState(() => store.getNode(id));
1239
+ react.useEffect(() => {
1240
+ return store.subscribe("change", () => setNode(store.getNode(id)));
1241
+ }, [id, store]);
1242
+ if (!node) return null;
1243
+ return /* @__PURE__ */ jsxRuntime.jsx(
1244
+ "div",
1245
+ {
1246
+ style: {
1247
+ position: "absolute",
1248
+ left: node.x,
1249
+ top: node.y,
1250
+ width: node.w,
1251
+ height: node.h,
1252
+ transform: node.angle !== 0 ? `rotate(${node.angle}rad)` : void 0,
1253
+ transformOrigin: "center"
1254
+ },
1255
+ children
1256
+ }
1257
+ );
1258
+ }
1259
+ var POSITION_STYLES = {
1260
+ "top-left": { top: 12, left: 12 },
1261
+ "top-right": { top: 12, right: 12 },
1262
+ "bottom-left": { bottom: 12, left: 12 },
1263
+ "bottom-right": { bottom: 12, right: 12 }
1264
+ };
1265
+ function Minimap({
1266
+ width = 200,
1267
+ height = 150,
1268
+ maxNodes = core.DEFAULT_MINIMAP_MAX_NODES,
1269
+ position = "bottom-right",
1270
+ style,
1271
+ viewportColor = "#3b82f6",
1272
+ backgroundColor = "#ffffff",
1273
+ borderColor = "#cbd5e1",
1274
+ defaultNodeColor = "#94a3b8"
1275
+ }) {
1276
+ const store = useCanvasStore();
1277
+ const containerRef = react.useRef(null);
1278
+ const canvasRef = react.useRef(null);
1279
+ const cacheRef = react.useRef(null);
1280
+ const cachedBoundsRef = react.useRef(null);
1281
+ const dirtyRef = react.useRef(true);
1282
+ const rafRef = react.useRef(0);
1283
+ const [overCap, setOverCap] = react.useState(false);
1284
+ react.useEffect(() => {
1285
+ const c = document.createElement("canvas");
1286
+ const dpr = window.devicePixelRatio || 1;
1287
+ c.width = Math.ceil(width * dpr);
1288
+ c.height = Math.ceil(height * dpr);
1289
+ cacheRef.current = c;
1290
+ dirtyRef.current = true;
1291
+ }, [width, height]);
1292
+ const schedule = () => {
1293
+ if (rafRef.current !== 0) return;
1294
+ rafRef.current = requestAnimationFrame(() => {
1295
+ rafRef.current = 0;
1296
+ repaint();
1297
+ });
1298
+ };
1299
+ const repaint = () => {
1300
+ const canvas = canvasRef.current;
1301
+ if (!canvas) return;
1302
+ const ctx = canvas.getContext("2d");
1303
+ if (!ctx) return;
1304
+ const dpr = window.devicePixelRatio || 1;
1305
+ if (dirtyRef.current) {
1306
+ const cache = cacheRef.current;
1307
+ if (cache) {
1308
+ const cctx = cache.getContext("2d");
1309
+ if (cctx) {
1310
+ cctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1311
+ cctx.clearRect(0, 0, width, height);
1312
+ cctx.fillStyle = backgroundColor;
1313
+ cctx.fillRect(0, 0, width, height);
1314
+ const ok = core.renderMinimapContent(cctx, store, width, height, {
1315
+ maxNodes,
1316
+ defaultNodeColor
1317
+ });
1318
+ cachedBoundsRef.current = ok ? core.sceneBounds(store) : null;
1319
+ setOverCap(!ok && store.getNodeCount() > maxNodes);
1320
+ }
1321
+ }
1322
+ dirtyRef.current = false;
1323
+ }
1324
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1325
+ ctx.clearRect(0, 0, width, height);
1326
+ if (cacheRef.current) {
1327
+ ctx.drawImage(cacheRef.current, 0, 0, width, height);
1328
+ } else {
1329
+ ctx.fillStyle = backgroundColor;
1330
+ ctx.fillRect(0, 0, width, height);
1331
+ }
1332
+ const bounds = cachedBoundsRef.current;
1333
+ if (bounds && bounds.w > 0 && bounds.h > 0) {
1334
+ const camera = store.getCamera();
1335
+ const wrap = containerRef.current?.closest("[data-canvas-host]");
1336
+ const screenW = wrap?.clientWidth ?? window.innerWidth;
1337
+ const screenH = wrap?.clientHeight ?? window.innerHeight;
1338
+ core.drawMinimapViewport(
1339
+ ctx,
1340
+ core.worldViewportFromCamera(camera, screenW, screenH),
1341
+ bounds,
1342
+ width,
1343
+ height,
1344
+ viewportColor
1345
+ );
1346
+ }
1347
+ };
1348
+ react.useEffect(() => {
1349
+ const onChange = () => {
1350
+ dirtyRef.current = true;
1351
+ schedule();
1352
+ };
1353
+ const onCamera = () => {
1354
+ schedule();
1355
+ };
1356
+ const unsubChange = store.subscribe("change", onChange);
1357
+ const unsubCamera = store.subscribe("camera", onCamera);
1358
+ schedule();
1359
+ return () => {
1360
+ unsubChange();
1361
+ unsubCamera();
1362
+ if (rafRef.current !== 0) {
1363
+ cancelAnimationFrame(rafRef.current);
1364
+ rafRef.current = 0;
1365
+ }
1366
+ };
1367
+ }, [store]);
1368
+ react.useEffect(() => {
1369
+ const canvas = canvasRef.current;
1370
+ if (!canvas) return;
1371
+ let dragging = false;
1372
+ const move = (e) => {
1373
+ const rect = canvas.getBoundingClientRect();
1374
+ const worldCenter = core.minimapScreenToWorld(
1375
+ store,
1376
+ e.clientX - rect.left,
1377
+ e.clientY - rect.top,
1378
+ width,
1379
+ height
1380
+ );
1381
+ if (!worldCenter) return;
1382
+ const wrap = containerRef.current?.closest("[data-canvas-host]");
1383
+ const screenW = wrap?.clientWidth ?? window.innerWidth;
1384
+ const screenH = wrap?.clientHeight ?? window.innerHeight;
1385
+ const camera = store.getCamera();
1386
+ store.setCamera({
1387
+ x: worldCenter.x - screenW / camera.z / 2,
1388
+ y: worldCenter.y - screenH / camera.z / 2
1389
+ });
1390
+ };
1391
+ const down = (e) => {
1392
+ if (e.button !== 0) return;
1393
+ dragging = true;
1394
+ canvas.setPointerCapture(e.pointerId);
1395
+ move(e);
1396
+ };
1397
+ const drag = (e) => {
1398
+ if (!dragging) return;
1399
+ move(e);
1400
+ };
1401
+ const up = (e) => {
1402
+ if (!dragging) return;
1403
+ dragging = false;
1404
+ if (canvas.hasPointerCapture(e.pointerId)) canvas.releasePointerCapture(e.pointerId);
1405
+ };
1406
+ canvas.addEventListener("pointerdown", down);
1407
+ canvas.addEventListener("pointermove", drag);
1408
+ canvas.addEventListener("pointerup", up);
1409
+ canvas.addEventListener("pointercancel", up);
1410
+ return () => {
1411
+ canvas.removeEventListener("pointerdown", down);
1412
+ canvas.removeEventListener("pointermove", drag);
1413
+ canvas.removeEventListener("pointerup", up);
1414
+ canvas.removeEventListener("pointercancel", up);
1415
+ };
1416
+ }, [store, width, height]);
1417
+ const containerStyle = style ?? {
1418
+ position: "absolute",
1419
+ ...POSITION_STYLES[position],
1420
+ width,
1421
+ height,
1422
+ background: backgroundColor,
1423
+ border: `1px solid ${borderColor}`,
1424
+ borderRadius: 6,
1425
+ boxShadow: "0 1px 3px rgba(0,0,0,.08)",
1426
+ overflow: "hidden",
1427
+ zIndex: 10
1428
+ };
1429
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, style: containerStyle, children: [
1430
+ /* @__PURE__ */ jsxRuntime.jsx(
1431
+ "canvas",
1432
+ {
1433
+ ref: canvasRef,
1434
+ width: Math.ceil(
1435
+ width * (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1)
1436
+ ),
1437
+ height: Math.ceil(
1438
+ height * (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1)
1439
+ ),
1440
+ style: { width, height, display: "block", cursor: "crosshair" }
1441
+ }
1442
+ ),
1443
+ overCap && /* @__PURE__ */ jsxRuntime.jsxs(
1444
+ "div",
1445
+ {
1446
+ style: {
1447
+ position: "absolute",
1448
+ inset: 0,
1449
+ display: "flex",
1450
+ alignItems: "center",
1451
+ justifyContent: "center",
1452
+ color: "#64748b",
1453
+ fontFamily: "system-ui, -apple-system, sans-serif",
1454
+ fontSize: 11,
1455
+ textAlign: "center",
1456
+ padding: 8,
1457
+ pointerEvents: "none"
1458
+ },
1459
+ children: [
1460
+ "Minimap disabled",
1461
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
1462
+ "(",
1463
+ store.getNodeCount(),
1464
+ " > ",
1465
+ maxNodes,
1466
+ ")"
1467
+ ]
1468
+ }
1469
+ )
1470
+ ] });
1471
+ }
1472
+ function useNode(id) {
1473
+ const store = useCanvasStore();
1474
+ return react.useSyncExternalStore(
1475
+ (cb) => {
1476
+ return store.subscribe("change", (batch) => {
1477
+ for (const op of batch.ops) {
1478
+ if ("node" in op && op.node.id === id) {
1479
+ cb();
1480
+ return;
1481
+ }
1482
+ if ("id" in op && op.id === id) {
1483
+ cb();
1484
+ return;
1485
+ }
1486
+ }
1487
+ });
1488
+ },
1489
+ () => store.getNode(id)
1490
+ );
1491
+ }
1492
+ function useNodes(predicate) {
1493
+ const store = useCanvasStore();
1494
+ const [nodes, setNodes] = react.useState(() => {
1495
+ const all = store.getAllNodes();
1496
+ return predicate ? all.filter(predicate) : all;
1497
+ });
1498
+ react.useEffect(() => {
1499
+ const recompute = () => {
1500
+ const all = store.getAllNodes();
1501
+ setNodes(predicate ? all.filter(predicate) : all);
1502
+ };
1503
+ return store.subscribe("change", recompute);
1504
+ }, [store, predicate]);
1505
+ return nodes;
1506
+ }
1507
+ function useEdge(id) {
1508
+ const store = useCanvasStore();
1509
+ return react.useSyncExternalStore(
1510
+ (cb) => store.subscribe("change", (batch) => {
1511
+ for (const op of batch.ops) {
1512
+ if ("edge" in op && op.edge.id === id) {
1513
+ cb();
1514
+ return;
1515
+ }
1516
+ if ("id" in op && op.id === id) {
1517
+ cb();
1518
+ return;
1519
+ }
1520
+ }
1521
+ }),
1522
+ () => store.getEdge(id)
1523
+ );
1524
+ }
1525
+ function useEdges(predicate) {
1526
+ const store = useCanvasStore();
1527
+ const [edges, setEdges] = react.useState(() => {
1528
+ const all = store.getAllEdges();
1529
+ return predicate ? all.filter(predicate) : all;
1530
+ });
1531
+ react.useEffect(() => {
1532
+ const recompute = () => {
1533
+ const all = store.getAllEdges();
1534
+ setEdges(predicate ? all.filter(predicate) : all);
1535
+ };
1536
+ return store.subscribe("change", recompute);
1537
+ }, [store, predicate]);
1538
+ return edges;
1539
+ }
1540
+ function useSelection() {
1541
+ const store = useCanvasStore();
1542
+ return react.useSyncExternalStore(
1543
+ (cb) => store.subscribe("selection", cb),
1544
+ () => store.getSelection()
1545
+ );
1546
+ }
1547
+ function useCamera() {
1548
+ const store = useCanvasStore();
1549
+ return react.useSyncExternalStore(
1550
+ (cb) => store.subscribe("camera", cb),
1551
+ () => store.getCamera()
1552
+ );
1553
+ }
1554
+ function useInteractionState() {
1555
+ const store = useCanvasStore();
1556
+ return react.useSyncExternalStore(
1557
+ (cb) => store.subscribe("interaction", cb),
1558
+ () => store.getInteractionState()
1559
+ );
1560
+ }
1561
+ function useInteractionMode() {
1562
+ const store = useCanvasStore();
1563
+ return react.useSyncExternalStore(
1564
+ (cb) => {
1565
+ let lastMode = store.getInteractionState().mode;
1566
+ return store.subscribe("interaction", (state) => {
1567
+ if (state.mode !== lastMode) {
1568
+ lastMode = state.mode;
1569
+ cb();
1570
+ }
1571
+ });
1572
+ },
1573
+ () => store.getInteractionState().mode
1574
+ );
1575
+ }
1576
+ function useCursor() {
1577
+ const store = useCanvasStore();
1578
+ return react.useSyncExternalStore(
1579
+ (cb) => store.subscribe("interaction", cb),
1580
+ () => store.getInteractionState().pointer
1581
+ );
1582
+ }
1583
+ function useIsMoving() {
1584
+ const mode = useInteractionMode();
1585
+ return mode === "panning" || mode === "zooming" || mode === "dragging" || mode === "resizing" || mode === "rotating";
1586
+ }
1587
+ var EMPTY_DRAGGED = [];
1588
+ function useDraggedIds() {
1589
+ const store = useCanvasStore();
1590
+ return react.useSyncExternalStore(
1591
+ (cb) => store.subscribe("interaction", cb),
1592
+ () => {
1593
+ const state = store.getInteractionState();
1594
+ return state.draggedIds.length === 0 ? EMPTY_DRAGGED : state.draggedIds;
1595
+ }
1596
+ );
1597
+ }
1598
+ function useIsPenActive() {
1599
+ const cursor = useCursor();
1600
+ return cursor?.pointerType === "pen";
1601
+ }
1602
+ function useLocalPresence() {
1603
+ const store = useCanvasStore();
1604
+ return react.useSyncExternalStore(
1605
+ (cb) => store.subscribe("presence", (e) => {
1606
+ if ("removed" in e && e.removed) return;
1607
+ if (e.state.clientId === store.clientId) cb();
1608
+ }),
1609
+ () => store.presence.getLocal()
1610
+ );
1611
+ }
1612
+ function usePresence(clientId) {
1613
+ const store = useCanvasStore();
1614
+ const [, force] = react.useState(0);
1615
+ react.useEffect(() => {
1616
+ if (clientId !== void 0) return;
1617
+ return store.subscribe("presence", (e) => {
1618
+ if ("removed" in e && e.removed) return force((n) => n + 1);
1619
+ if (e.state.clientId !== store.clientId) force((n) => n + 1);
1620
+ });
1621
+ }, [store, clientId]);
1622
+ const [snap, setSnap] = react.useState(
1623
+ () => clientId === void 0 ? void 0 : store.presence.get(clientId)
1624
+ );
1625
+ react.useEffect(() => {
1626
+ if (clientId === void 0) return;
1627
+ return store.subscribe("presence", (e) => {
1628
+ if ("removed" in e && e.removed && e.clientId === clientId) setSnap(void 0);
1629
+ else if (!("removed" in e) && e.state.clientId === clientId) setSnap(e.state);
1630
+ });
1631
+ }, [store, clientId]);
1632
+ if (clientId !== void 0) return snap;
1633
+ return store.presence.getAll();
1634
+ }
1635
+ function useCanUndo() {
1636
+ const store = useCanvasStore();
1637
+ return react.useSyncExternalStore(
1638
+ (cb) => store.subscribe("change", cb),
1639
+ () => store.canUndo()
1640
+ );
1641
+ }
1642
+ function useCanRedo() {
1643
+ const store = useCanvasStore();
1644
+ return react.useSyncExternalStore(
1645
+ (cb) => store.subscribe("change", cb),
1646
+ () => store.canRedo()
1647
+ );
1648
+ }
1649
+
1650
+ // src/index.ts
1651
+ var VERSION = "0.0.0";
1652
+
1653
+ exports.Canvas = Canvas;
1654
+ exports.CanvasProvider = CanvasProvider;
1655
+ exports.Minimap = Minimap;
1656
+ exports.VERSION = VERSION;
1657
+ exports.useCamera = useCamera;
1658
+ exports.useCanRedo = useCanRedo;
1659
+ exports.useCanUndo = useCanUndo;
1660
+ exports.useCanvasStore = useCanvasStore;
1661
+ exports.useCursor = useCursor;
1662
+ exports.useDraggedIds = useDraggedIds;
1663
+ exports.useEdge = useEdge;
1664
+ exports.useEdges = useEdges;
1665
+ exports.useInteractionMode = useInteractionMode;
1666
+ exports.useInteractionState = useInteractionState;
1667
+ exports.useIsMoving = useIsMoving;
1668
+ exports.useIsPenActive = useIsPenActive;
1669
+ exports.useLocalPresence = useLocalPresence;
1670
+ exports.useNode = useNode;
1671
+ exports.useNodes = useNodes;
1672
+ exports.usePresence = usePresence;
1673
+ exports.useSelection = useSelection;
1674
+ //# sourceMappingURL=index.cjs.map
1675
+ //# sourceMappingURL=index.cjs.map