@tsdraw/react 0.8.2 → 0.8.4

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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
- import { DEFAULT_COLORS, getSelectionBoundsPage, buildTransformSnapshots, Editor, STROKE_WIDTHS, ERASER_MARGIN, resolveThemeColor, pageToScreen, getTopShapeAtPoint, buildStartPositions, applyRotation, applyResize, applyMove, normalizeSelectionBounds, getShapesInBounds, isSelectTool } from '@tsdraw/core';
3
+ import { DEFAULT_COLORS, getSelectionBoundsPage, buildTransformSnapshots, Editor, STROKE_WIDTHS, ERASER_MARGIN, resolveThemeColor, pageToScreen, getTopShapeAtPoint, buildStartPositions, applyRotation, applyResize, applyMove, normalizeSelectionBounds, getShapesInBounds, HandDraggingState, startCameraSlide, isSelectTool, beginCameraPan, moveCameraPan } from '@tsdraw/core';
4
4
  import { IconPointer, IconPencil, IconSquare, IconCircle, IconEraser, IconHandStop, IconArrowBackUp, IconArrowForwardUp } from '@tabler/icons-react';
5
5
 
6
6
  // src/components/TsdrawCanvas.tsx
@@ -290,12 +290,11 @@ function getCanvasCursor(currentTool, state) {
290
290
  if (state.isRotatingSelection) return "grabbing";
291
291
  if (state.isResizingSelection) return "nwse-resize";
292
292
  if (state.isMovingSelection) return "grabbing";
293
+ if (state.isHoveringSelectionBounds) return "move";
293
294
  return "default";
294
295
  }
295
296
  return state.showToolOverlay ? "none" : "crosshair";
296
297
  }
297
-
298
- // src/canvas/touchInteractions.ts
299
298
  var TAP_MAX_DURATION_MS = 100;
300
299
  var DOUBLE_TAP_INTERVAL_MS = 100;
301
300
  var TAP_MOVE_TOLERANCE = 14;
@@ -317,17 +316,28 @@ function createTouchInteractionController(editor, canvas, handlers) {
317
316
  mode: "not-sure",
318
317
  previousCenter: { x: 0, y: 0 },
319
318
  initialCenter: { x: 0, y: 0 },
320
- previousDistance: 1,
321
319
  initialDistance: 1,
322
- previousAngle: 0
320
+ initialZoom: 1
323
321
  };
322
+ let fingerPanPointerId = null;
323
+ let fingerPanSession = null;
324
+ let fingerPanSlide = null;
324
325
  const isTouchPointer = (event) => event.pointerType === "touch";
326
+ const stopFingerPanSlide = () => {
327
+ if (fingerPanSlide) {
328
+ fingerPanSlide.stop();
329
+ fingerPanSlide = null;
330
+ }
331
+ };
332
+ const endFingerPan = () => {
333
+ fingerPanPointerId = null;
334
+ fingerPanSession = null;
335
+ };
325
336
  const endTouchCameraGesture = () => {
326
337
  touchCameraState.active = false;
327
338
  touchCameraState.mode = "not-sure";
328
- touchCameraState.previousDistance = 1;
329
339
  touchCameraState.initialDistance = 1;
330
- touchCameraState.previousAngle = 0;
340
+ touchCameraState.initialZoom = 1;
331
341
  };
332
342
  const maybeHandleTouchTapGesture = () => {
333
343
  if (activeTouchPoints.size > 0) return;
@@ -358,14 +368,12 @@ function createTouchInteractionController(editor, canvas, handlers) {
358
368
  const second = points[1];
359
369
  const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
360
370
  const distance = Math.hypot(second.x - first.x, second.y - first.y);
361
- const angle = Math.atan2(second.y - first.y, second.x - first.x);
362
371
  touchCameraState.active = true;
363
372
  touchCameraState.mode = "not-sure";
364
373
  touchCameraState.previousCenter = center;
365
374
  touchCameraState.initialCenter = center;
366
- touchCameraState.previousDistance = Math.max(1, distance);
367
375
  touchCameraState.initialDistance = Math.max(1, distance);
368
- touchCameraState.previousAngle = angle;
376
+ touchCameraState.initialZoom = editor.getZoomLevel();
369
377
  };
370
378
  const updateTouchCameraGesture = () => {
371
379
  if (!touchCameraState.active) return false;
@@ -378,7 +386,6 @@ function createTouchInteractionController(editor, canvas, handlers) {
378
386
  const second = points[1];
379
387
  const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
380
388
  const distance = Math.max(1, Math.hypot(second.x - first.x, second.y - first.y));
381
- const angle = Math.atan2(second.y - first.y, second.x - first.x);
382
389
  const centerDx = center.x - touchCameraState.previousCenter.x;
383
390
  const centerDy = center.y - touchCameraState.previousCenter.y;
384
391
  const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
@@ -390,20 +397,27 @@ function createTouchInteractionController(editor, canvas, handlers) {
390
397
  const canvasRect = canvas.getBoundingClientRect();
391
398
  const centerOnCanvasX = center.x - canvasRect.left;
392
399
  const centerOnCanvasY = center.y - canvasRect.top;
393
- editor.panBy(centerDx, centerDy);
394
400
  if (touchCameraState.mode === "zooming") {
395
- const zoomFactor = distance / touchCameraState.previousDistance;
396
- editor.zoomAt(zoomFactor, centerOnCanvasX, centerOnCanvasY);
397
- editor.rotateAt(angle - touchCameraState.previousAngle, centerOnCanvasX, centerOnCanvasY);
401
+ const targetZoom = Math.max(0.1, Math.min(4, touchCameraState.initialZoom * (distance / touchCameraState.initialDistance)));
402
+ const pannedX = editor.viewport.x + centerDx;
403
+ const pannedY = editor.viewport.y + centerDy;
404
+ const pageAtCenterX = (centerOnCanvasX - pannedX) / editor.viewport.zoom;
405
+ const pageAtCenterY = (centerOnCanvasY - pannedY) / editor.viewport.zoom;
406
+ editor.setViewport({
407
+ x: centerOnCanvasX - pageAtCenterX * targetZoom,
408
+ y: centerOnCanvasY - pageAtCenterY * targetZoom,
409
+ zoom: targetZoom
410
+ });
411
+ } else {
412
+ editor.panBy(centerDx, centerDy);
398
413
  }
399
414
  touchCameraState.previousCenter = center;
400
- touchCameraState.previousDistance = distance;
401
- touchCameraState.previousAngle = angle;
402
415
  handlers.refreshView();
403
416
  return true;
404
417
  };
405
418
  const handlePointerDown = (event) => {
406
419
  if (!isTouchPointer(event)) return false;
420
+ stopFingerPanSlide();
407
421
  activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
408
422
  if (!touchTapState.active) {
409
423
  touchTapState.active = true;
@@ -416,9 +430,16 @@ function createTouchInteractionController(editor, canvas, handlers) {
416
430
  }
417
431
  touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
418
432
  if (activeTouchPoints.size === 2) {
433
+ endFingerPan();
419
434
  beginTouchCameraGesture();
420
435
  return true;
421
436
  }
437
+ if (handlers.isPenModeActive() && activeTouchPoints.size === 1) {
438
+ handlers.cancelActivePointerInteraction();
439
+ fingerPanPointerId = event.pointerId;
440
+ fingerPanSession = beginCameraPan(editor.viewport, event.clientX, event.clientY);
441
+ return true;
442
+ }
422
443
  return false;
423
444
  };
424
445
  const handlePointerMove = (event) => {
@@ -429,16 +450,34 @@ function createTouchInteractionController(editor, canvas, handlers) {
429
450
  const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
430
451
  if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
431
452
  }
453
+ if (fingerPanPointerId === event.pointerId && fingerPanSession) {
454
+ const target = moveCameraPan(fingerPanSession, event.clientX, event.clientY);
455
+ editor.setViewport({ x: target.x, y: target.y });
456
+ handlers.refreshView();
457
+ return true;
458
+ }
432
459
  return updateTouchCameraGesture();
433
460
  };
434
461
  const handlePointerUpOrCancel = (event) => {
435
462
  if (!isTouchPointer(event)) return false;
436
463
  const wasCameraGestureActive = touchCameraState.active;
464
+ const wasFingerPan = fingerPanPointerId === event.pointerId;
465
+ const releasedPanSession = wasFingerPan ? fingerPanSession : null;
437
466
  activeTouchPoints.delete(event.pointerId);
438
467
  touchTapState.startPoints.delete(event.pointerId);
439
468
  if (activeTouchPoints.size < 2) endTouchCameraGesture();
469
+ if (wasFingerPan) {
470
+ endFingerPan();
471
+ if (releasedPanSession) {
472
+ fingerPanSlide = startCameraSlide(
473
+ releasedPanSession,
474
+ (dx, dy) => editor.panBy(dx, dy),
475
+ () => handlers.refreshView()
476
+ );
477
+ }
478
+ }
440
479
  maybeHandleTouchTapGesture();
441
- return wasCameraGestureActive;
480
+ return wasCameraGestureActive || wasFingerPan;
442
481
  };
443
482
  let gestureLastScale = 1;
444
483
  let gestureActive = false;
@@ -472,6 +511,8 @@ function createTouchInteractionController(editor, canvas, handlers) {
472
511
  touchTapState.active = false;
473
512
  touchTapState.startPoints.clear();
474
513
  endTouchCameraGesture();
514
+ endFingerPan();
515
+ stopFingerPanSlide();
475
516
  };
476
517
  return {
477
518
  handlePointerDown,
@@ -480,6 +521,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
480
521
  handleGestureEvent,
481
522
  reset,
482
523
  isCameraGestureActive: () => touchCameraState.active,
524
+ isFingerPanActive: () => fingerPanPointerId !== null,
483
525
  isTrackpadZoomActive: () => gestureActive
484
526
  };
485
527
  }
@@ -679,6 +721,18 @@ function toScreenRect(editor, bounds) {
679
721
  function resolveDrawColor(colorStyle, theme) {
680
722
  return resolveThemeColor(colorStyle, theme);
681
723
  }
724
+ function getHandlePagePoint(bounds, handle) {
725
+ switch (handle) {
726
+ case "nw":
727
+ return { x: bounds.minX, y: bounds.minY };
728
+ case "ne":
729
+ return { x: bounds.maxX, y: bounds.minY };
730
+ case "sw":
731
+ return { x: bounds.minX, y: bounds.maxY };
732
+ case "se":
733
+ return { x: bounds.maxX, y: bounds.maxY };
734
+ }
735
+ }
682
736
  var ZOOM_WHEEL_CAP = 10;
683
737
  function useTsdrawCanvasController(options = {}) {
684
738
  const onMountRef = useRef(options.onMount);
@@ -696,11 +750,13 @@ function useTsdrawCanvasController(options = {}) {
696
750
  const schedulePersistRef = useRef(null);
697
751
  const isPointerActiveRef = useRef(false);
698
752
  const pendingRemoteDocumentRef = useRef(null);
753
+ const activeCameraSlideRef = useRef(null);
699
754
  const selectionRotationRef = useRef(0);
700
755
  const resizeRef = useRef({
701
756
  handle: null,
702
757
  startBounds: null,
703
- startShapes: /* @__PURE__ */ new Map()
758
+ startShapes: /* @__PURE__ */ new Map(),
759
+ cursorHandleOffset: { x: 0, y: 0 }
704
760
  });
705
761
  const rotateRef = useRef({
706
762
  center: null,
@@ -787,33 +843,38 @@ function useTsdrawCanvasController(options = {}) {
787
843
  setIsResizingSelection(false);
788
844
  setIsRotatingSelection(false);
789
845
  selectDragRef.current.mode = "none";
790
- resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
791
- rotateRef.current = {
792
- center: null,
793
- startAngle: 0,
794
- startSelectionRotationDeg: selectionRotationRef.current,
795
- startShapes: /* @__PURE__ */ new Map()
796
- };
846
+ resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map(), cursorHandleOffset: { x: 0, y: 0 } };
847
+ rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map() };
797
848
  }, []);
798
849
  const handleResizePointerDown = useCallback(
799
850
  (e, handle) => {
800
851
  e.preventDefault();
801
852
  e.stopPropagation();
802
853
  const editor = editorRef.current;
803
- if (!editor || selectedShapeIdsRef.current.length === 0) return;
854
+ const canvas = canvasRef.current;
855
+ if (!editor || !canvas || selectedShapeIdsRef.current.length === 0) return;
804
856
  const bounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
805
857
  if (!bounds) return;
858
+ const handlePagePoint = getHandlePagePoint(bounds, handle);
859
+ const pointerPage = getPagePointFromClient(editor, e.clientX, e.clientY);
860
+ const cursorOffset = {
861
+ x: pointerPage.x - handlePagePoint.x,
862
+ y: pointerPage.y - handlePagePoint.y
863
+ };
806
864
  resizeRef.current = {
807
865
  handle,
808
866
  startBounds: bounds,
809
- startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current)
867
+ startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current),
868
+ cursorHandleOffset: cursorOffset
810
869
  };
870
+ isPointerActiveRef.current = true;
871
+ activePointerIdsRef.current.add(e.pointerId);
872
+ canvas.setPointerCapture(e.pointerId);
811
873
  editor.beginHistoryEntry();
812
874
  selectDragRef.current.mode = "resize";
813
- const p = getPagePointFromClient(editor, e.clientX, e.clientY);
814
- editor.input.pointerDown(p.x, p.y, 0.5, false);
815
- selectDragRef.current.startPage = p;
816
- selectDragRef.current.currentPage = p;
875
+ editor.input.pointerDown(handlePagePoint.x, handlePagePoint.y, 0.5, false);
876
+ selectDragRef.current.startPage = handlePagePoint;
877
+ selectDragRef.current.currentPage = handlePagePoint;
817
878
  lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
818
879
  setIsResizingSelection(true);
819
880
  },
@@ -824,7 +885,8 @@ function useTsdrawCanvasController(options = {}) {
824
885
  e.preventDefault();
825
886
  e.stopPropagation();
826
887
  const editor = editorRef.current;
827
- if (!editor || selectedShapeIdsRef.current.length === 0) return;
888
+ const canvas = canvasRef.current;
889
+ if (!editor || !canvas || selectedShapeIdsRef.current.length === 0) return;
828
890
  const bounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
829
891
  if (!bounds) return;
830
892
  const center = {
@@ -838,6 +900,9 @@ function useTsdrawCanvasController(options = {}) {
838
900
  startSelectionRotationDeg: selectionRotationRef.current,
839
901
  startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current)
840
902
  };
903
+ isPointerActiveRef.current = true;
904
+ activePointerIdsRef.current.add(e.pointerId);
905
+ canvas.setPointerCapture(e.pointerId);
841
906
  editor.beginHistoryEntry();
842
907
  selectDragRef.current.mode = "rotate";
843
908
  editor.input.pointerDown(p.x, p.y, 0.5, false);
@@ -967,7 +1032,6 @@ function useTsdrawCanvasController(options = {}) {
967
1032
  setDrawDash(nextDrawStyle.dash);
968
1033
  setDrawFill(nextDrawStyle.fill);
969
1034
  setDrawSize(nextDrawStyle.size);
970
- setSelectionRotationDeg(0);
971
1035
  render();
972
1036
  refreshSelectionBounds(editor, nextSelectionIds);
973
1037
  ignorePersistenceChanges = false;
@@ -983,7 +1047,6 @@ function useTsdrawCanvasController(options = {}) {
983
1047
  const applyDocumentChangeResult = (changed) => {
984
1048
  if (!changed) return false;
985
1049
  reconcileSelectionAfterDocumentLoad();
986
- setSelectionRotationDeg(0);
987
1050
  render();
988
1051
  syncHistoryState();
989
1052
  return true;
@@ -1010,8 +1073,7 @@ function useTsdrawCanvasController(options = {}) {
1010
1073
  setSelectedShapeIds([]);
1011
1074
  selectedShapeIdsRef.current = [];
1012
1075
  setSelectionBounds(null);
1013
- setSelectionBrush(null);
1014
- setSelectionRotationDeg(0);
1076
+ resetSelectUi();
1015
1077
  render();
1016
1078
  syncHistoryState();
1017
1079
  return true;
@@ -1022,12 +1084,7 @@ function useTsdrawCanvasController(options = {}) {
1022
1084
  lastPointerClientRef.current = null;
1023
1085
  editor.input.pointerUp();
1024
1086
  if (currentToolRef.current === "select") {
1025
- const dragMode = selectDragRef.current.mode;
1026
- if (dragMode === "rotate") setIsRotatingSelection(false);
1027
- if (dragMode === "resize") setIsResizingSelection(false);
1028
- if (dragMode === "move") setIsMovingSelection(false);
1029
- if (dragMode === "marquee") setSelectionBrush(null);
1030
- selectDragRef.current.mode = "none";
1087
+ resetSelectUi();
1031
1088
  } else {
1032
1089
  editor.tools.pointerUp();
1033
1090
  }
@@ -1042,12 +1099,19 @@ function useTsdrawCanvasController(options = {}) {
1042
1099
  refreshSelectionBounds(editor);
1043
1100
  },
1044
1101
  runUndo: () => applyDocumentChangeResult(editor.undo()),
1045
- runRedo: () => applyDocumentChangeResult(editor.redo())
1102
+ runRedo: () => applyDocumentChangeResult(editor.redo()),
1103
+ isPenModeActive: () => penModeRef.current
1046
1104
  });
1047
- const isDrawingTool = (tool) => tool !== "select" && tool !== "hand";
1048
1105
  const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
1106
+ const stopActiveSlide = () => {
1107
+ if (activeCameraSlideRef.current) {
1108
+ activeCameraSlideRef.current.stop();
1109
+ activeCameraSlideRef.current = null;
1110
+ }
1111
+ };
1049
1112
  const handlePointerDown = (e) => {
1050
1113
  if (!canvas.contains(e.target)) return;
1114
+ stopActiveSlide();
1051
1115
  if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1052
1116
  penDetectedRef.current = true;
1053
1117
  penModeRef.current = true;
@@ -1055,15 +1119,15 @@ function useTsdrawCanvasController(options = {}) {
1055
1119
  lastPointerDownWithRef.current = e.pointerType;
1056
1120
  activePointerIdsRef.current.add(e.pointerId);
1057
1121
  const startedCameraGesture = touchInteractions.handlePointerDown(e);
1058
- if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
1122
+ if (startedCameraGesture || touchInteractions.isCameraGestureActive() || touchInteractions.isFingerPanActive()) {
1059
1123
  e.preventDefault();
1060
1124
  if (!canvas.hasPointerCapture(e.pointerId)) {
1061
1125
  canvas.setPointerCapture(e.pointerId);
1062
1126
  }
1063
1127
  return;
1064
1128
  }
1065
- const allowPointerDown = !penModeRef.current || e.pointerType !== "touch" || !isDrawingTool(currentToolRef.current);
1066
- if (!allowPointerDown) {
1129
+ const isTouchBlockedByPenMode = penModeRef.current && e.pointerType === "touch";
1130
+ if (isTouchBlockedByPenMode) {
1067
1131
  return;
1068
1132
  }
1069
1133
  if (activePointerIdsRef.current.size > 1) {
@@ -1081,7 +1145,13 @@ function useTsdrawCanvasController(options = {}) {
1081
1145
  if (currentToolRef.current === "select") {
1082
1146
  const hit = getTopShapeAtPoint(editor, { x, y });
1083
1147
  const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
1084
- if (isHitSelected) {
1148
+ const isInsideSelectionBounds = (() => {
1149
+ if (selectedShapeIdsRef.current.length === 0) return false;
1150
+ const pageBounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
1151
+ if (!pageBounds) return false;
1152
+ return x >= pageBounds.minX && x <= pageBounds.maxX && y >= pageBounds.minY && y <= pageBounds.maxY;
1153
+ })();
1154
+ if (isHitSelected || isInsideSelectionBounds) {
1085
1155
  selectDragRef.current = {
1086
1156
  mode: "move",
1087
1157
  startPage: { x, y },
@@ -1112,7 +1182,7 @@ function useTsdrawCanvasController(options = {}) {
1112
1182
  }
1113
1183
  editor.input.pointerDown(x, y, pressure, isPen);
1114
1184
  editor.input.setModifiers(first.shiftKey, first.ctrlKey, first.metaKey);
1115
- editor.tools.pointerDown({ point: { x, y, z: pressure } });
1185
+ editor.tools.pointerDown({ point: { x, y, z: pressure }, screenX: e.clientX, screenY: e.clientY });
1116
1186
  render();
1117
1187
  refreshSelectionBounds(editor);
1118
1188
  };
@@ -1125,7 +1195,7 @@ function useTsdrawCanvasController(options = {}) {
1125
1195
  e.preventDefault();
1126
1196
  return;
1127
1197
  }
1128
- if (penModeRef.current && e.pointerType === "touch" && isDrawingTool(currentToolRef.current) && !isPointerActiveRef.current) return;
1198
+ if (penModeRef.current && e.pointerType === "touch" && !isPointerActiveRef.current) return;
1129
1199
  if (activePointerIdsRef.current.size > 1) return;
1130
1200
  updatePointerPreview(e.clientX, e.clientY);
1131
1201
  const prevClient = lastPointerClientRef.current;
@@ -1152,9 +1222,9 @@ function useTsdrawCanvasController(options = {}) {
1152
1222
  return;
1153
1223
  }
1154
1224
  if (mode === "resize") {
1155
- const { handle, startBounds, startShapes } = resizeRef.current;
1225
+ const { handle, startBounds, startShapes, cursorHandleOffset } = resizeRef.current;
1156
1226
  if (!handle || !startBounds) return;
1157
- applyResize(editor, handle, startBounds, startShapes, { x: px, y: py }, e.shiftKey);
1227
+ applyResize(editor, handle, startBounds, startShapes, { x: px - cursorHandleOffset.x, y: py - cursorHandleOffset.y }, e.shiftKey);
1158
1228
  render();
1159
1229
  refreshSelectionBounds(editor);
1160
1230
  return;
@@ -1174,13 +1244,12 @@ function useTsdrawCanvasController(options = {}) {
1174
1244
  const nextIds = selectDragRef.current.additive ? Array.from(/* @__PURE__ */ new Set([...selectDragRef.current.initialSelection, ...ids])) : ids;
1175
1245
  setSelectedShapeIds(nextIds);
1176
1246
  selectedShapeIdsRef.current = nextIds;
1177
- setSelectionRotationDeg(0);
1178
1247
  refreshSelectionBounds(editor, nextIds);
1179
1248
  return;
1180
1249
  }
1181
1250
  }
1182
1251
  editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
1183
- editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy });
1252
+ editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy, screenX: e.clientX, screenY: e.clientY });
1184
1253
  render();
1185
1254
  refreshSelectionBounds(editor);
1186
1255
  };
@@ -1204,12 +1273,7 @@ function useTsdrawCanvasController(options = {}) {
1204
1273
  setIsRotatingSelection(false);
1205
1274
  selectDragRef.current.mode = "none";
1206
1275
  setSelectionRotationDeg(0);
1207
- rotateRef.current = {
1208
- center: null,
1209
- startAngle: 0,
1210
- startSelectionRotationDeg: selectionRotationRef.current,
1211
- startShapes: /* @__PURE__ */ new Map()
1212
- };
1276
+ rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map() };
1213
1277
  render();
1214
1278
  refreshSelectionBounds(editor);
1215
1279
  editor.endHistoryEntry();
@@ -1218,7 +1282,7 @@ function useTsdrawCanvasController(options = {}) {
1218
1282
  if (drag.mode === "resize") {
1219
1283
  setIsResizingSelection(false);
1220
1284
  selectDragRef.current.mode = "none";
1221
- resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
1285
+ resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map(), cursorHandleOffset: { x: 0, y: 0 } };
1222
1286
  render();
1223
1287
  refreshSelectionBounds(editor);
1224
1288
  editor.endHistoryEntry();
@@ -1251,7 +1315,6 @@ function useTsdrawCanvasController(options = {}) {
1251
1315
  }
1252
1316
  setSelectedShapeIds(ids);
1253
1317
  selectedShapeIdsRef.current = ids;
1254
- setSelectionRotationDeg(0);
1255
1318
  setSelectionBrush(null);
1256
1319
  selectDragRef.current.mode = "none";
1257
1320
  render();
@@ -1265,9 +1328,26 @@ function useTsdrawCanvasController(options = {}) {
1265
1328
  return;
1266
1329
  }
1267
1330
  }
1331
+ let handPanSession = null;
1332
+ if (currentToolRef.current === "hand") {
1333
+ const currentState = editor.tools.getCurrentState();
1334
+ if (currentState instanceof HandDraggingState) {
1335
+ handPanSession = currentState.getPanSession();
1336
+ }
1337
+ }
1268
1338
  editor.tools.pointerUp();
1269
1339
  render();
1270
1340
  refreshSelectionBounds(editor);
1341
+ if (handPanSession) {
1342
+ activeCameraSlideRef.current = startCameraSlide(
1343
+ handPanSession,
1344
+ (slideDx, slideDy) => editor.panBy(slideDx, slideDy),
1345
+ () => {
1346
+ render();
1347
+ refreshSelectionBounds(editor);
1348
+ }
1349
+ );
1350
+ }
1271
1351
  if (pendingRemoteDocumentRef.current) {
1272
1352
  const pendingRemoteDocument = pendingRemoteDocumentRef.current;
1273
1353
  pendingRemoteDocumentRef.current = null;
@@ -1434,11 +1514,17 @@ function useTsdrawCanvasController(options = {}) {
1434
1514
  resize();
1435
1515
  const ro = new ResizeObserver(resize);
1436
1516
  ro.observe(container);
1517
+ const handlePointerLeaveViewport = (e) => {
1518
+ if (e.relatedTarget === null) {
1519
+ setIsPointerInsideCanvas(false);
1520
+ }
1521
+ };
1437
1522
  canvas.addEventListener("pointerdown", handlePointerDown);
1438
1523
  container.addEventListener("wheel", handleWheel, { passive: false });
1439
1524
  document.addEventListener("gesturestart", handleGestureEvent);
1440
1525
  document.addEventListener("gesturechange", handleGestureEvent);
1441
1526
  document.addEventListener("gestureend", handleGestureEvent);
1527
+ document.documentElement.addEventListener("pointerleave", handlePointerLeaveViewport);
1442
1528
  window.addEventListener("pointermove", handlePointerMove);
1443
1529
  window.addEventListener("pointerup", handlePointerUp);
1444
1530
  window.addEventListener("pointercancel", handlePointerCancel);
@@ -1462,7 +1548,6 @@ function useTsdrawCanvasController(options = {}) {
1462
1548
  const changed = editor.undo();
1463
1549
  if (!changed) return false;
1464
1550
  reconcileSelectionAfterDocumentLoad();
1465
- setSelectionRotationDeg(0);
1466
1551
  render();
1467
1552
  syncHistoryState();
1468
1553
  return true;
@@ -1471,7 +1556,6 @@ function useTsdrawCanvasController(options = {}) {
1471
1556
  const changed = editor.redo();
1472
1557
  if (!changed) return false;
1473
1558
  reconcileSelectionAfterDocumentLoad();
1474
- setSelectionRotationDeg(0);
1475
1559
  render();
1476
1560
  syncHistoryState();
1477
1561
  return true;
@@ -1499,6 +1583,7 @@ function useTsdrawCanvasController(options = {}) {
1499
1583
  document.removeEventListener("gesturestart", handleGestureEvent);
1500
1584
  document.removeEventListener("gesturechange", handleGestureEvent);
1501
1585
  document.removeEventListener("gestureend", handleGestureEvent);
1586
+ document.documentElement.removeEventListener("pointerleave", handlePointerLeaveViewport);
1502
1587
  window.removeEventListener("pointermove", handlePointerMove);
1503
1588
  window.removeEventListener("pointerup", handlePointerUp);
1504
1589
  window.removeEventListener("pointercancel", handlePointerCancel);
@@ -1507,6 +1592,7 @@ function useTsdrawCanvasController(options = {}) {
1507
1592
  isPointerActiveRef.current = false;
1508
1593
  activePointerIdsRef.current.clear();
1509
1594
  pendingRemoteDocumentRef.current = null;
1595
+ stopActiveSlide();
1510
1596
  touchInteractions.reset();
1511
1597
  persistenceChannel?.close();
1512
1598
  void persistenceDb?.close();
@@ -1563,7 +1649,6 @@ function useTsdrawCanvasController(options = {}) {
1563
1649
  selectedShapeIdsRef.current = nextSelectedShapeIds;
1564
1650
  setSelectedShapeIds(nextSelectedShapeIds);
1565
1651
  }
1566
- setSelectionRotationDeg(0);
1567
1652
  render();
1568
1653
  setCanUndo(editor.canUndo());
1569
1654
  setCanRedo(editor.canRedo());
@@ -1579,17 +1664,18 @@ function useTsdrawCanvasController(options = {}) {
1579
1664
  selectedShapeIdsRef.current = nextSelectedShapeIds;
1580
1665
  setSelectedShapeIds(nextSelectedShapeIds);
1581
1666
  }
1582
- setSelectionRotationDeg(0);
1583
1667
  render();
1584
1668
  setCanUndo(editor.canUndo());
1585
1669
  setCanRedo(editor.canRedo());
1586
1670
  return true;
1587
1671
  }, [render]);
1672
+ const isHoveringSelectionBounds = isPointerInsideCanvas && currentTool === "select" && selectedShapeIds.length > 0 && selectionBounds != null && pointerScreenPoint.x >= selectionBounds.left && pointerScreenPoint.x <= selectionBounds.left + selectionBounds.width && pointerScreenPoint.y >= selectionBounds.top && pointerScreenPoint.y <= selectionBounds.top + selectionBounds.height;
1588
1673
  const showToolOverlay = isPointerInsideCanvas && (currentTool === "pen" || currentTool === "eraser");
1589
1674
  const canvasCursor = getCanvasCursor(currentTool, {
1590
1675
  isMovingSelection,
1591
1676
  isResizingSelection,
1592
1677
  isRotatingSelection,
1678
+ isHoveringSelectionBounds,
1593
1679
  showToolOverlay
1594
1680
  });
1595
1681
  const cursorContext = {