@tsdraw/react 0.7.0 → 0.8.1

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 CHANGED
@@ -297,6 +297,249 @@ function getCanvasCursor(currentTool, state) {
297
297
  return state.showToolOverlay ? "none" : "crosshair";
298
298
  }
299
299
 
300
+ // src/canvas/touchInteractions.ts
301
+ var TAP_MAX_DURATION_MS = 100;
302
+ var DOUBLE_TAP_INTERVAL_MS = 100;
303
+ var TAP_MOVE_TOLERANCE = 14;
304
+ var PINCH_MODE_ZOOM_DISTANCE = 24;
305
+ var PINCH_MODE_PAN_DISTANCE = 16;
306
+ var PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE = 64;
307
+ function createTouchInteractionController(editor, canvas, handlers) {
308
+ const activeTouchPoints = /* @__PURE__ */ new Map();
309
+ const touchTapState = {
310
+ active: false,
311
+ startTime: 0,
312
+ maxTouchCount: 0,
313
+ moved: false,
314
+ startPoints: /* @__PURE__ */ new Map(),
315
+ lastTapAtByCount: {}
316
+ };
317
+ const touchCameraState = {
318
+ active: false,
319
+ mode: "not-sure",
320
+ previousCenter: { x: 0, y: 0 },
321
+ initialCenter: { x: 0, y: 0 },
322
+ previousDistance: 1,
323
+ initialDistance: 1,
324
+ previousAngle: 0
325
+ };
326
+ const isTouchPointer = (event) => event.pointerType === "touch";
327
+ const endTouchCameraGesture = () => {
328
+ touchCameraState.active = false;
329
+ touchCameraState.mode = "not-sure";
330
+ touchCameraState.previousDistance = 1;
331
+ touchCameraState.initialDistance = 1;
332
+ touchCameraState.previousAngle = 0;
333
+ };
334
+ const maybeHandleTouchTapGesture = () => {
335
+ if (activeTouchPoints.size > 0) return;
336
+ if (!touchTapState.active) return;
337
+ const elapsed = performance.now() - touchTapState.startTime;
338
+ if (!touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
339
+ const fingerCount = touchTapState.maxTouchCount;
340
+ const now = performance.now();
341
+ const previousTapTime = touchTapState.lastTapAtByCount[fingerCount] ?? 0;
342
+ const isDoubleTap = previousTapTime > 0 && now - previousTapTime <= DOUBLE_TAP_INTERVAL_MS;
343
+ if (isDoubleTap) {
344
+ touchTapState.lastTapAtByCount[fingerCount] = 0;
345
+ if (fingerCount === 2) {
346
+ if (handlers.runUndo()) handlers.refreshView();
347
+ } else if (handlers.runRedo()) handlers.refreshView();
348
+ } else touchTapState.lastTapAtByCount[fingerCount] = now;
349
+ }
350
+ touchTapState.active = false;
351
+ touchTapState.startPoints.clear();
352
+ touchTapState.maxTouchCount = 0;
353
+ touchTapState.moved = false;
354
+ };
355
+ const beginTouchCameraGesture = () => {
356
+ const points = [...activeTouchPoints.values()];
357
+ if (points.length !== 2) return;
358
+ handlers.cancelActivePointerInteraction();
359
+ const first = points[0];
360
+ const second = points[1];
361
+ const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
362
+ const distance = Math.hypot(second.x - first.x, second.y - first.y);
363
+ const angle = Math.atan2(second.y - first.y, second.x - first.x);
364
+ touchCameraState.active = true;
365
+ touchCameraState.mode = "not-sure";
366
+ touchCameraState.previousCenter = center;
367
+ touchCameraState.initialCenter = center;
368
+ touchCameraState.previousDistance = Math.max(1, distance);
369
+ touchCameraState.initialDistance = Math.max(1, distance);
370
+ touchCameraState.previousAngle = angle;
371
+ };
372
+ const updateTouchCameraGesture = () => {
373
+ if (!touchCameraState.active) return false;
374
+ const points = [...activeTouchPoints.values()];
375
+ if (points.length !== 2) {
376
+ endTouchCameraGesture();
377
+ return false;
378
+ }
379
+ const first = points[0];
380
+ const second = points[1];
381
+ const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
382
+ const distance = Math.max(1, Math.hypot(second.x - first.x, second.y - first.y));
383
+ const angle = Math.atan2(second.y - first.y, second.x - first.x);
384
+ const centerDx = center.x - touchCameraState.previousCenter.x;
385
+ const centerDy = center.y - touchCameraState.previousCenter.y;
386
+ const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
387
+ const originDistance = Math.hypot(center.x - touchCameraState.initialCenter.x, center.y - touchCameraState.initialCenter.y);
388
+ if (touchCameraState.mode === "not-sure") {
389
+ if (touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
390
+ else if (originDistance > PINCH_MODE_PAN_DISTANCE) touchCameraState.mode = "panning";
391
+ } else if (touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
392
+ const canvasRect = canvas.getBoundingClientRect();
393
+ const centerOnCanvasX = center.x - canvasRect.left;
394
+ const centerOnCanvasY = center.y - canvasRect.top;
395
+ editor.panBy(centerDx, centerDy);
396
+ if (touchCameraState.mode === "zooming") {
397
+ const zoomFactor = distance / touchCameraState.previousDistance;
398
+ editor.zoomAt(zoomFactor, centerOnCanvasX, centerOnCanvasY);
399
+ editor.rotateAt(angle - touchCameraState.previousAngle, centerOnCanvasX, centerOnCanvasY);
400
+ }
401
+ touchCameraState.previousCenter = center;
402
+ touchCameraState.previousDistance = distance;
403
+ touchCameraState.previousAngle = angle;
404
+ handlers.refreshView();
405
+ return true;
406
+ };
407
+ const handlePointerDown = (event) => {
408
+ if (!isTouchPointer(event)) return false;
409
+ activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
410
+ if (!touchTapState.active) {
411
+ touchTapState.active = true;
412
+ touchTapState.startTime = performance.now();
413
+ touchTapState.maxTouchCount = activeTouchPoints.size;
414
+ touchTapState.moved = false;
415
+ touchTapState.startPoints.clear();
416
+ } else {
417
+ touchTapState.maxTouchCount = Math.max(touchTapState.maxTouchCount, activeTouchPoints.size);
418
+ }
419
+ touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
420
+ if (activeTouchPoints.size === 2) {
421
+ beginTouchCameraGesture();
422
+ return true;
423
+ }
424
+ return false;
425
+ };
426
+ const handlePointerMove = (event) => {
427
+ if (!isTouchPointer(event)) return false;
428
+ if (activeTouchPoints.has(event.pointerId)) activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
429
+ const tapStart = touchTapState.startPoints.get(event.pointerId);
430
+ if (tapStart) {
431
+ const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
432
+ if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
433
+ }
434
+ return updateTouchCameraGesture();
435
+ };
436
+ const handlePointerUpOrCancel = (event) => {
437
+ if (!isTouchPointer(event)) return false;
438
+ const wasCameraGestureActive = touchCameraState.active;
439
+ activeTouchPoints.delete(event.pointerId);
440
+ touchTapState.startPoints.delete(event.pointerId);
441
+ if (activeTouchPoints.size < 2) endTouchCameraGesture();
442
+ maybeHandleTouchTapGesture();
443
+ return wasCameraGestureActive;
444
+ };
445
+ let gestureLastScale = 1;
446
+ let gestureActive = false;
447
+ const handleGestureEvent = (event, container) => {
448
+ if (!container.contains(event.target)) return;
449
+ event.preventDefault();
450
+ const gestureEvent = event;
451
+ if (gestureEvent.scale == null) return;
452
+ if (event.type === "gesturestart") {
453
+ gestureLastScale = gestureEvent.scale;
454
+ gestureActive = true;
455
+ return;
456
+ }
457
+ if (event.type === "gestureend") {
458
+ gestureActive = false;
459
+ gestureLastScale = 1;
460
+ return;
461
+ }
462
+ if (event.type === "gesturechange" && gestureActive) {
463
+ const zoomFactor = gestureEvent.scale / gestureLastScale;
464
+ gestureLastScale = gestureEvent.scale;
465
+ const canvasRect = canvas.getBoundingClientRect();
466
+ const cx = (gestureEvent.clientX ?? canvasRect.left + canvasRect.width / 2) - canvasRect.left;
467
+ const cy = (gestureEvent.clientY ?? canvasRect.top + canvasRect.height / 2) - canvasRect.top;
468
+ editor.zoomAt(zoomFactor, cx, cy);
469
+ handlers.refreshView();
470
+ }
471
+ };
472
+ const reset = () => {
473
+ activeTouchPoints.clear();
474
+ touchTapState.active = false;
475
+ touchTapState.startPoints.clear();
476
+ endTouchCameraGesture();
477
+ };
478
+ return {
479
+ handlePointerDown,
480
+ handlePointerMove,
481
+ handlePointerUpOrCancel,
482
+ handleGestureEvent,
483
+ reset,
484
+ isCameraGestureActive: () => touchCameraState.active,
485
+ isTrackpadZoomActive: () => gestureActive
486
+ };
487
+ }
488
+
489
+ // src/canvas/keyboardShortcuts.ts
490
+ var TOOL_SHORTCUTS = {
491
+ v: "select",
492
+ h: "hand",
493
+ e: "eraser",
494
+ p: "pen",
495
+ b: "pen",
496
+ d: "pen",
497
+ x: "pen",
498
+ r: "square",
499
+ o: "circle",
500
+ c: "circle"
501
+ };
502
+ function isEditableTarget(eventTarget) {
503
+ const element = eventTarget;
504
+ if (!element) return false;
505
+ if (element.isContentEditable) return true;
506
+ const tagName = element.tagName;
507
+ return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
508
+ }
509
+ function handleKeyboardShortcutKeyDown(event, handlers) {
510
+ if (isEditableTarget(event.target)) return;
511
+ const loweredKey = event.key.toLowerCase();
512
+ const isMetaPressed = event.metaKey || event.ctrlKey;
513
+ if (isMetaPressed && (loweredKey === "z" || loweredKey === "y")) {
514
+ const shouldRedo = loweredKey === "y" || loweredKey === "z" && event.shiftKey;
515
+ if (handlers.runHistoryShortcut(shouldRedo)) {
516
+ event.preventDefault();
517
+ event.stopPropagation();
518
+ return;
519
+ }
520
+ }
521
+ if (!isMetaPressed && !event.altKey) {
522
+ const nextToolId = TOOL_SHORTCUTS[loweredKey];
523
+ if (nextToolId && handlers.isToolAvailable(nextToolId)) {
524
+ handlers.setToolFromShortcut(nextToolId);
525
+ event.preventDefault();
526
+ return;
527
+ }
528
+ }
529
+ if (event.key === "Delete" || event.key === "Backspace") {
530
+ if (handlers.deleteSelection()) {
531
+ event.preventDefault();
532
+ event.stopPropagation();
533
+ return;
534
+ }
535
+ }
536
+ handlers.dispatchKeyDown(event);
537
+ }
538
+ function handleKeyboardShortcutKeyUp(event, handlers) {
539
+ if (isEditableTarget(event.target)) return;
540
+ handlers.dispatchKeyUp(event);
541
+ }
542
+
300
543
  // src/persistence/localIndexedDb.ts
301
544
  var DATABASE_PREFIX = "tsdraw_v1_";
302
545
  var DATABASE_VERSION = 2;
@@ -420,23 +663,35 @@ function getOrCreateSessionId() {
420
663
 
421
664
  // src/canvas/useTsdrawCanvasController.ts
422
665
  function toScreenRect(editor, bounds) {
423
- const { x, y, zoom } = editor.viewport;
666
+ const topLeft = core.pageToScreen(editor.viewport, bounds.minX, bounds.minY);
667
+ const topRight = core.pageToScreen(editor.viewport, bounds.maxX, bounds.minY);
668
+ const bottomLeft = core.pageToScreen(editor.viewport, bounds.minX, bounds.maxY);
669
+ const bottomRight = core.pageToScreen(editor.viewport, bounds.maxX, bounds.maxY);
670
+ const minX = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
671
+ const minY = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
672
+ const maxX = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
673
+ const maxY = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
424
674
  return {
425
- left: bounds.minX * zoom + x,
426
- top: bounds.minY * zoom + y,
427
- width: (bounds.maxX - bounds.minX) * zoom,
428
- height: (bounds.maxY - bounds.minY) * zoom
675
+ left: minX,
676
+ top: minY,
677
+ width: Math.max(0, maxX - minX),
678
+ height: Math.max(0, maxY - minY)
429
679
  };
430
680
  }
431
681
  function resolveDrawColor(colorStyle, theme) {
432
682
  return core.resolveThemeColor(colorStyle, theme);
433
683
  }
684
+ var ZOOM_WHEEL_CAP = 10;
434
685
  function useTsdrawCanvasController(options = {}) {
435
686
  const onMountRef = react.useRef(options.onMount);
436
687
  const containerRef = react.useRef(null);
437
688
  const canvasRef = react.useRef(null);
438
689
  const editorRef = react.useRef(null);
439
690
  const dprRef = react.useRef(1);
691
+ const penDetectedRef = react.useRef(false);
692
+ const penModeRef = react.useRef(false);
693
+ const lastPointerDownWithRef = react.useRef("mouse");
694
+ const activePointerIdsRef = react.useRef(/* @__PURE__ */ new Set());
440
695
  const lastPointerClientRef = react.useRef(null);
441
696
  const currentToolRef = react.useRef(options.initialTool ?? "pen");
442
697
  const selectedShapeIdsRef = react.useRef([]);
@@ -693,9 +948,9 @@ function useTsdrawCanvasController(options = {}) {
693
948
  }
694
949
  refreshSelectionBounds(editor, nextSelectedShapeIds);
695
950
  };
696
- const applyRemoteDocumentSnapshot = (document) => {
951
+ const applyRemoteDocumentSnapshot = (document2) => {
697
952
  ignorePersistenceChanges = true;
698
- editor.loadDocumentSnapshot(document);
953
+ editor.loadDocumentSnapshot(document2);
699
954
  editor.clearRedoHistory();
700
955
  reconcileSelectionAfterDocumentLoad();
701
956
  render();
@@ -727,8 +982,95 @@ function useTsdrawCanvasController(options = {}) {
727
982
  const coalesced = e.getCoalescedEvents?.();
728
983
  return coalesced && coalesced.length > 0 ? coalesced : [e];
729
984
  };
985
+ const applyDocumentChangeResult = (changed) => {
986
+ if (!changed) return false;
987
+ reconcileSelectionAfterDocumentLoad();
988
+ setSelectionRotationDeg(0);
989
+ render();
990
+ syncHistoryState();
991
+ return true;
992
+ };
993
+ const normalizeWheelDelta = (event) => {
994
+ let deltaX = event.deltaX;
995
+ let deltaY = event.deltaY;
996
+ let deltaZoom = 0;
997
+ if (event.ctrlKey || event.metaKey || event.altKey) {
998
+ const clamped = Math.abs(deltaY) > ZOOM_WHEEL_CAP ? ZOOM_WHEEL_CAP * Math.sign(deltaY) : deltaY;
999
+ deltaZoom = -clamped / 100;
1000
+ } else if (event.shiftKey && !navigator.userAgent.includes("Mac") && !navigator.userAgent.includes("iPhone") && !navigator.userAgent.includes("iPad")) {
1001
+ deltaX = deltaY;
1002
+ deltaY = 0;
1003
+ }
1004
+ return { x: -deltaX, y: -deltaY, z: deltaZoom };
1005
+ };
1006
+ const deleteCurrentSelection = () => {
1007
+ const selectedIds = selectedShapeIdsRef.current;
1008
+ if (selectedIds.length === 0) return false;
1009
+ editor.beginHistoryEntry();
1010
+ editor.deleteShapes(selectedIds);
1011
+ editor.endHistoryEntry();
1012
+ setSelectedShapeIds([]);
1013
+ selectedShapeIdsRef.current = [];
1014
+ setSelectionBounds(null);
1015
+ setSelectionBrush(null);
1016
+ setSelectionRotationDeg(0);
1017
+ render();
1018
+ syncHistoryState();
1019
+ return true;
1020
+ };
1021
+ const cancelActivePointerInteraction = () => {
1022
+ if (!isPointerActiveRef.current) return;
1023
+ isPointerActiveRef.current = false;
1024
+ lastPointerClientRef.current = null;
1025
+ editor.input.pointerUp();
1026
+ if (currentToolRef.current === "select") {
1027
+ const dragMode = selectDragRef.current.mode;
1028
+ if (dragMode === "rotate") setIsRotatingSelection(false);
1029
+ if (dragMode === "resize") setIsResizingSelection(false);
1030
+ if (dragMode === "move") setIsMovingSelection(false);
1031
+ if (dragMode === "marquee") setSelectionBrush(null);
1032
+ selectDragRef.current.mode = "none";
1033
+ } else {
1034
+ editor.tools.pointerUp();
1035
+ }
1036
+ editor.endHistoryEntry();
1037
+ render();
1038
+ refreshSelectionBounds(editor);
1039
+ };
1040
+ const touchInteractions = createTouchInteractionController(editor, canvas, {
1041
+ cancelActivePointerInteraction,
1042
+ refreshView: () => {
1043
+ render();
1044
+ refreshSelectionBounds(editor);
1045
+ },
1046
+ runUndo: () => applyDocumentChangeResult(editor.undo()),
1047
+ runRedo: () => applyDocumentChangeResult(editor.redo())
1048
+ });
1049
+ const isDrawingTool = (tool) => tool !== "select" && tool !== "hand";
1050
+ const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
730
1051
  const handlePointerDown = (e) => {
731
1052
  if (!canvas.contains(e.target)) return;
1053
+ if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1054
+ penDetectedRef.current = true;
1055
+ penModeRef.current = true;
1056
+ }
1057
+ lastPointerDownWithRef.current = e.pointerType;
1058
+ activePointerIdsRef.current.add(e.pointerId);
1059
+ const startedCameraGesture = touchInteractions.handlePointerDown(e);
1060
+ if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
1061
+ e.preventDefault();
1062
+ if (!canvas.hasPointerCapture(e.pointerId)) {
1063
+ canvas.setPointerCapture(e.pointerId);
1064
+ }
1065
+ return;
1066
+ }
1067
+ const allowPointerDown = !penModeRef.current || e.pointerType !== "touch" || !isDrawingTool(currentToolRef.current);
1068
+ if (!allowPointerDown) {
1069
+ return;
1070
+ }
1071
+ if (activePointerIdsRef.current.size > 1) {
1072
+ return;
1073
+ }
732
1074
  isPointerActiveRef.current = true;
733
1075
  editor.beginHistoryEntry();
734
1076
  canvas.setPointerCapture(e.pointerId);
@@ -737,7 +1079,7 @@ function useTsdrawCanvasController(options = {}) {
737
1079
  const first = sampleEvents(e)[0];
738
1080
  const { x, y } = getPagePoint(first);
739
1081
  const pressure = first.pressure ?? 0.5;
740
- const isPen = first.pointerType === "pen" || first.pointerType === "touch";
1082
+ const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
741
1083
  if (currentToolRef.current === "select") {
742
1084
  const hit = core.getTopShapeAtPoint(editor, { x, y });
743
1085
  const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
@@ -777,6 +1119,16 @@ function useTsdrawCanvasController(options = {}) {
777
1119
  refreshSelectionBounds(editor);
778
1120
  };
779
1121
  const handlePointerMove = (e) => {
1122
+ if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1123
+ penDetectedRef.current = true;
1124
+ penModeRef.current = true;
1125
+ }
1126
+ if (touchInteractions.handlePointerMove(e)) {
1127
+ e.preventDefault();
1128
+ return;
1129
+ }
1130
+ if (penModeRef.current && e.pointerType === "touch" && isDrawingTool(currentToolRef.current) && !isPointerActiveRef.current) return;
1131
+ if (activePointerIdsRef.current.size > 1) return;
780
1132
  updatePointerPreview(e.clientX, e.clientY);
781
1133
  const prevClient = lastPointerClientRef.current;
782
1134
  const dx = prevClient ? e.clientX - prevClient.x : 0;
@@ -785,7 +1137,7 @@ function useTsdrawCanvasController(options = {}) {
785
1137
  for (const sample of sampleEvents(e)) {
786
1138
  const { x, y } = getPagePoint(sample);
787
1139
  const pressure = sample.pressure ?? 0.5;
788
- const isPen = sample.pointerType === "pen" || sample.pointerType === "touch";
1140
+ const isPen = sample.pointerType === "pen" || hasRealPressure(sample.pressure);
789
1141
  editor.input.pointerMove(x, y, pressure, isPen);
790
1142
  }
791
1143
  if (currentToolRef.current === "select") {
@@ -835,6 +1187,13 @@ function useTsdrawCanvasController(options = {}) {
835
1187
  refreshSelectionBounds(editor);
836
1188
  };
837
1189
  const handlePointerUp = (e) => {
1190
+ activePointerIdsRef.current.delete(e.pointerId);
1191
+ const hadTouchCameraGesture = touchInteractions.handlePointerUpOrCancel(e);
1192
+ if (hadTouchCameraGesture || touchInteractions.isCameraGestureActive()) {
1193
+ e.preventDefault();
1194
+ return;
1195
+ }
1196
+ if (!isPointerActiveRef.current) return;
838
1197
  isPointerActiveRef.current = false;
839
1198
  lastPointerClientRef.current = null;
840
1199
  updatePointerPreview(e.clientX, e.clientY);
@@ -918,7 +1277,10 @@ function useTsdrawCanvasController(options = {}) {
918
1277
  }
919
1278
  editor.endHistoryEntry();
920
1279
  };
921
- const handlePointerCancel = () => {
1280
+ const handlePointerCancel = (e) => {
1281
+ activePointerIdsRef.current.delete(e.pointerId);
1282
+ const hadTouchCameraGesture = touchInteractions.handlePointerUpOrCancel(e);
1283
+ if (hadTouchCameraGesture || touchInteractions.isCameraGestureActive()) return;
922
1284
  if (!isPointerActiveRef.current) return;
923
1285
  isPointerActiveRef.current = false;
924
1286
  lastPointerClientRef.current = null;
@@ -947,31 +1309,58 @@ function useTsdrawCanvasController(options = {}) {
947
1309
  applyRemoteDocumentSnapshot(pending);
948
1310
  }
949
1311
  };
950
- const handleKeyDown = (e) => {
951
- const isMetaPressed = e.metaKey || e.ctrlKey;
952
- const loweredKey = e.key.toLowerCase();
953
- const isUndoOrRedoKey = loweredKey === "z" || loweredKey === "y";
954
- if (isMetaPressed && isUndoOrRedoKey) {
955
- const shouldRedo = loweredKey === "y" || loweredKey === "z" && e.shiftKey;
956
- const changed = shouldRedo ? editor.redo() : editor.undo();
957
- if (changed) {
958
- e.preventDefault();
959
- e.stopPropagation();
960
- reconcileSelectionAfterDocumentLoad();
961
- setSelectionRotationDeg(0);
962
- render();
963
- syncHistoryState();
964
- return;
965
- }
1312
+ const handleWheel = (e) => {
1313
+ if (!container.contains(e.target)) return;
1314
+ e.preventDefault();
1315
+ if (touchInteractions.isTrackpadZoomActive()) return;
1316
+ const delta = normalizeWheelDelta(e);
1317
+ if (delta.z !== 0) {
1318
+ const rect = canvas.getBoundingClientRect();
1319
+ const pointX = e.clientX - rect.left;
1320
+ const pointY = e.clientY - rect.top;
1321
+ editor.zoomAt(Math.exp(delta.z), pointX, pointY);
1322
+ } else {
1323
+ editor.panBy(delta.x, delta.y);
966
1324
  }
967
- editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
968
- editor.tools.keyDown({ key: e.key });
969
1325
  render();
1326
+ refreshSelectionBounds(editor);
1327
+ };
1328
+ const handleGestureEvent = (e) => {
1329
+ touchInteractions.handleGestureEvent(e, container);
1330
+ };
1331
+ const handleKeyDown = (e) => {
1332
+ handleKeyboardShortcutKeyDown(e, {
1333
+ isToolAvailable: (tool) => editor.tools.hasTool(tool),
1334
+ setToolFromShortcut: (tool) => {
1335
+ editor.setCurrentTool(tool);
1336
+ setCurrentToolState(tool);
1337
+ currentToolRef.current = tool;
1338
+ if (tool !== "select") resetSelectUi();
1339
+ render();
1340
+ },
1341
+ runHistoryShortcut: (shouldRedo) => applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo()),
1342
+ deleteSelection: () => currentToolRef.current === "select" ? deleteCurrentSelection() : false,
1343
+ dispatchKeyDown: (event) => {
1344
+ editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
1345
+ editor.tools.keyDown({ key: event.key });
1346
+ render();
1347
+ },
1348
+ dispatchKeyUp: () => void 0
1349
+ });
970
1350
  };
971
1351
  const handleKeyUp = (e) => {
972
- editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
973
- editor.tools.keyUp({ key: e.key });
974
- render();
1352
+ handleKeyboardShortcutKeyUp(e, {
1353
+ isToolAvailable: () => false,
1354
+ setToolFromShortcut: () => void 0,
1355
+ runHistoryShortcut: () => false,
1356
+ deleteSelection: () => false,
1357
+ dispatchKeyDown: () => void 0,
1358
+ dispatchKeyUp: (event) => {
1359
+ editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
1360
+ editor.tools.keyUp({ key: event.key });
1361
+ render();
1362
+ }
1363
+ });
975
1364
  };
976
1365
  const initializePersistence = async () => {
977
1366
  if (!persistenceKey) {
@@ -1048,6 +1437,10 @@ function useTsdrawCanvasController(options = {}) {
1048
1437
  const ro = new ResizeObserver(resize);
1049
1438
  ro.observe(container);
1050
1439
  canvas.addEventListener("pointerdown", handlePointerDown);
1440
+ container.addEventListener("wheel", handleWheel, { passive: false });
1441
+ document.addEventListener("gesturestart", handleGestureEvent);
1442
+ document.addEventListener("gesturechange", handleGestureEvent);
1443
+ document.addEventListener("gestureend", handleGestureEvent);
1051
1444
  window.addEventListener("pointermove", handlePointerMove);
1052
1445
  window.addEventListener("pointerup", handlePointerUp);
1053
1446
  window.addEventListener("pointercancel", handlePointerCancel);
@@ -1104,13 +1497,19 @@ function useTsdrawCanvasController(options = {}) {
1104
1497
  disposeMount?.();
1105
1498
  ro.disconnect();
1106
1499
  canvas.removeEventListener("pointerdown", handlePointerDown);
1500
+ container.removeEventListener("wheel", handleWheel);
1501
+ document.removeEventListener("gesturestart", handleGestureEvent);
1502
+ document.removeEventListener("gesturechange", handleGestureEvent);
1503
+ document.removeEventListener("gestureend", handleGestureEvent);
1107
1504
  window.removeEventListener("pointermove", handlePointerMove);
1108
1505
  window.removeEventListener("pointerup", handlePointerUp);
1109
1506
  window.removeEventListener("pointercancel", handlePointerCancel);
1110
1507
  window.removeEventListener("keydown", handleKeyDown);
1111
1508
  window.removeEventListener("keyup", handleKeyUp);
1112
1509
  isPointerActiveRef.current = false;
1510
+ activePointerIdsRef.current.clear();
1113
1511
  pendingRemoteDocumentRef.current = null;
1512
+ touchInteractions.reset();
1114
1513
  persistenceChannel?.close();
1115
1514
  void persistenceDb?.close();
1116
1515
  editorRef.current = null;
@@ -1121,6 +1520,7 @@ function useTsdrawCanvasController(options = {}) {
1121
1520
  options.persistenceKey,
1122
1521
  options.toolDefinitions,
1123
1522
  refreshSelectionBounds,
1523
+ resetSelectUi,
1124
1524
  render,
1125
1525
  updatePointerPreview
1126
1526
  ]);