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