@tsdraw/react 0.8.1 → 0.8.3

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
@@ -290,6 +290,7 @@ 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";
@@ -318,8 +319,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
318
319
  previousCenter: { x: 0, y: 0 },
319
320
  initialCenter: { x: 0, y: 0 },
320
321
  previousDistance: 1,
321
- initialDistance: 1,
322
- previousAngle: 0
322
+ initialDistance: 1
323
323
  };
324
324
  const isTouchPointer = (event) => event.pointerType === "touch";
325
325
  const endTouchCameraGesture = () => {
@@ -327,7 +327,6 @@ function createTouchInteractionController(editor, canvas, handlers) {
327
327
  touchCameraState.mode = "not-sure";
328
328
  touchCameraState.previousDistance = 1;
329
329
  touchCameraState.initialDistance = 1;
330
- touchCameraState.previousAngle = 0;
331
330
  };
332
331
  const maybeHandleTouchTapGesture = () => {
333
332
  if (activeTouchPoints.size > 0) return;
@@ -358,14 +357,12 @@ function createTouchInteractionController(editor, canvas, handlers) {
358
357
  const second = points[1];
359
358
  const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
360
359
  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
360
  touchCameraState.active = true;
363
361
  touchCameraState.mode = "not-sure";
364
362
  touchCameraState.previousCenter = center;
365
363
  touchCameraState.initialCenter = center;
366
364
  touchCameraState.previousDistance = Math.max(1, distance);
367
365
  touchCameraState.initialDistance = Math.max(1, distance);
368
- touchCameraState.previousAngle = angle;
369
366
  };
370
367
  const updateTouchCameraGesture = () => {
371
368
  if (!touchCameraState.active) return false;
@@ -378,7 +375,6 @@ function createTouchInteractionController(editor, canvas, handlers) {
378
375
  const second = points[1];
379
376
  const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
380
377
  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
378
  const centerDx = center.x - touchCameraState.previousCenter.x;
383
379
  const centerDy = center.y - touchCameraState.previousCenter.y;
384
380
  const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
@@ -394,11 +390,9 @@ function createTouchInteractionController(editor, canvas, handlers) {
394
390
  if (touchCameraState.mode === "zooming") {
395
391
  const zoomFactor = distance / touchCameraState.previousDistance;
396
392
  editor.zoomAt(zoomFactor, centerOnCanvasX, centerOnCanvasY);
397
- editor.rotateAt(angle - touchCameraState.previousAngle, centerOnCanvasX, centerOnCanvasY);
398
393
  }
399
394
  touchCameraState.previousCenter = center;
400
395
  touchCameraState.previousDistance = distance;
401
- touchCameraState.previousAngle = angle;
402
396
  handlers.refreshView();
403
397
  return true;
404
398
  };
@@ -679,6 +673,18 @@ function toScreenRect(editor, bounds) {
679
673
  function resolveDrawColor(colorStyle, theme) {
680
674
  return resolveThemeColor(colorStyle, theme);
681
675
  }
676
+ function getHandlePagePoint(bounds, handle) {
677
+ switch (handle) {
678
+ case "nw":
679
+ return { x: bounds.minX, y: bounds.minY };
680
+ case "ne":
681
+ return { x: bounds.maxX, y: bounds.minY };
682
+ case "sw":
683
+ return { x: bounds.minX, y: bounds.maxY };
684
+ case "se":
685
+ return { x: bounds.maxX, y: bounds.maxY };
686
+ }
687
+ }
682
688
  var ZOOM_WHEEL_CAP = 10;
683
689
  function useTsdrawCanvasController(options = {}) {
684
690
  const onMountRef = useRef(options.onMount);
@@ -700,7 +706,8 @@ function useTsdrawCanvasController(options = {}) {
700
706
  const resizeRef = useRef({
701
707
  handle: null,
702
708
  startBounds: null,
703
- startShapes: /* @__PURE__ */ new Map()
709
+ startShapes: /* @__PURE__ */ new Map(),
710
+ cursorHandleOffset: { x: 0, y: 0 }
704
711
  });
705
712
  const rotateRef = useRef({
706
713
  center: null,
@@ -787,33 +794,38 @@ function useTsdrawCanvasController(options = {}) {
787
794
  setIsResizingSelection(false);
788
795
  setIsRotatingSelection(false);
789
796
  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
- };
797
+ resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map(), cursorHandleOffset: { x: 0, y: 0 } };
798
+ rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map() };
797
799
  }, []);
798
800
  const handleResizePointerDown = useCallback(
799
801
  (e, handle) => {
800
802
  e.preventDefault();
801
803
  e.stopPropagation();
802
804
  const editor = editorRef.current;
803
- if (!editor || selectedShapeIdsRef.current.length === 0) return;
805
+ const canvas = canvasRef.current;
806
+ if (!editor || !canvas || selectedShapeIdsRef.current.length === 0) return;
804
807
  const bounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
805
808
  if (!bounds) return;
809
+ const handlePagePoint = getHandlePagePoint(bounds, handle);
810
+ const pointerPage = getPagePointFromClient(editor, e.clientX, e.clientY);
811
+ const cursorOffset = {
812
+ x: pointerPage.x - handlePagePoint.x,
813
+ y: pointerPage.y - handlePagePoint.y
814
+ };
806
815
  resizeRef.current = {
807
816
  handle,
808
817
  startBounds: bounds,
809
- startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current)
818
+ startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current),
819
+ cursorHandleOffset: cursorOffset
810
820
  };
821
+ isPointerActiveRef.current = true;
822
+ activePointerIdsRef.current.add(e.pointerId);
823
+ canvas.setPointerCapture(e.pointerId);
811
824
  editor.beginHistoryEntry();
812
825
  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;
826
+ editor.input.pointerDown(handlePagePoint.x, handlePagePoint.y, 0.5, false);
827
+ selectDragRef.current.startPage = handlePagePoint;
828
+ selectDragRef.current.currentPage = handlePagePoint;
817
829
  lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
818
830
  setIsResizingSelection(true);
819
831
  },
@@ -824,7 +836,8 @@ function useTsdrawCanvasController(options = {}) {
824
836
  e.preventDefault();
825
837
  e.stopPropagation();
826
838
  const editor = editorRef.current;
827
- if (!editor || selectedShapeIdsRef.current.length === 0) return;
839
+ const canvas = canvasRef.current;
840
+ if (!editor || !canvas || selectedShapeIdsRef.current.length === 0) return;
828
841
  const bounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
829
842
  if (!bounds) return;
830
843
  const center = {
@@ -838,6 +851,9 @@ function useTsdrawCanvasController(options = {}) {
838
851
  startSelectionRotationDeg: selectionRotationRef.current,
839
852
  startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current)
840
853
  };
854
+ isPointerActiveRef.current = true;
855
+ activePointerIdsRef.current.add(e.pointerId);
856
+ canvas.setPointerCapture(e.pointerId);
841
857
  editor.beginHistoryEntry();
842
858
  selectDragRef.current.mode = "rotate";
843
859
  editor.input.pointerDown(p.x, p.y, 0.5, false);
@@ -967,7 +983,6 @@ function useTsdrawCanvasController(options = {}) {
967
983
  setDrawDash(nextDrawStyle.dash);
968
984
  setDrawFill(nextDrawStyle.fill);
969
985
  setDrawSize(nextDrawStyle.size);
970
- setSelectionRotationDeg(0);
971
986
  render();
972
987
  refreshSelectionBounds(editor, nextSelectionIds);
973
988
  ignorePersistenceChanges = false;
@@ -983,7 +998,6 @@ function useTsdrawCanvasController(options = {}) {
983
998
  const applyDocumentChangeResult = (changed) => {
984
999
  if (!changed) return false;
985
1000
  reconcileSelectionAfterDocumentLoad();
986
- setSelectionRotationDeg(0);
987
1001
  render();
988
1002
  syncHistoryState();
989
1003
  return true;
@@ -1010,8 +1024,7 @@ function useTsdrawCanvasController(options = {}) {
1010
1024
  setSelectedShapeIds([]);
1011
1025
  selectedShapeIdsRef.current = [];
1012
1026
  setSelectionBounds(null);
1013
- setSelectionBrush(null);
1014
- setSelectionRotationDeg(0);
1027
+ resetSelectUi();
1015
1028
  render();
1016
1029
  syncHistoryState();
1017
1030
  return true;
@@ -1022,12 +1035,7 @@ function useTsdrawCanvasController(options = {}) {
1022
1035
  lastPointerClientRef.current = null;
1023
1036
  editor.input.pointerUp();
1024
1037
  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";
1038
+ resetSelectUi();
1031
1039
  } else {
1032
1040
  editor.tools.pointerUp();
1033
1041
  }
@@ -1081,7 +1089,13 @@ function useTsdrawCanvasController(options = {}) {
1081
1089
  if (currentToolRef.current === "select") {
1082
1090
  const hit = getTopShapeAtPoint(editor, { x, y });
1083
1091
  const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
1084
- if (isHitSelected) {
1092
+ const isInsideSelectionBounds = (() => {
1093
+ if (selectedShapeIdsRef.current.length === 0) return false;
1094
+ const pageBounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
1095
+ if (!pageBounds) return false;
1096
+ return x >= pageBounds.minX && x <= pageBounds.maxX && y >= pageBounds.minY && y <= pageBounds.maxY;
1097
+ })();
1098
+ if (isHitSelected || isInsideSelectionBounds) {
1085
1099
  selectDragRef.current = {
1086
1100
  mode: "move",
1087
1101
  startPage: { x, y },
@@ -1152,9 +1166,9 @@ function useTsdrawCanvasController(options = {}) {
1152
1166
  return;
1153
1167
  }
1154
1168
  if (mode === "resize") {
1155
- const { handle, startBounds, startShapes } = resizeRef.current;
1169
+ const { handle, startBounds, startShapes, cursorHandleOffset } = resizeRef.current;
1156
1170
  if (!handle || !startBounds) return;
1157
- applyResize(editor, handle, startBounds, startShapes, { x: px, y: py }, e.shiftKey);
1171
+ applyResize(editor, handle, startBounds, startShapes, { x: px - cursorHandleOffset.x, y: py - cursorHandleOffset.y }, e.shiftKey);
1158
1172
  render();
1159
1173
  refreshSelectionBounds(editor);
1160
1174
  return;
@@ -1174,7 +1188,6 @@ function useTsdrawCanvasController(options = {}) {
1174
1188
  const nextIds = selectDragRef.current.additive ? Array.from(/* @__PURE__ */ new Set([...selectDragRef.current.initialSelection, ...ids])) : ids;
1175
1189
  setSelectedShapeIds(nextIds);
1176
1190
  selectedShapeIdsRef.current = nextIds;
1177
- setSelectionRotationDeg(0);
1178
1191
  refreshSelectionBounds(editor, nextIds);
1179
1192
  return;
1180
1193
  }
@@ -1204,12 +1217,7 @@ function useTsdrawCanvasController(options = {}) {
1204
1217
  setIsRotatingSelection(false);
1205
1218
  selectDragRef.current.mode = "none";
1206
1219
  setSelectionRotationDeg(0);
1207
- rotateRef.current = {
1208
- center: null,
1209
- startAngle: 0,
1210
- startSelectionRotationDeg: selectionRotationRef.current,
1211
- startShapes: /* @__PURE__ */ new Map()
1212
- };
1220
+ rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map() };
1213
1221
  render();
1214
1222
  refreshSelectionBounds(editor);
1215
1223
  editor.endHistoryEntry();
@@ -1218,7 +1226,7 @@ function useTsdrawCanvasController(options = {}) {
1218
1226
  if (drag.mode === "resize") {
1219
1227
  setIsResizingSelection(false);
1220
1228
  selectDragRef.current.mode = "none";
1221
- resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
1229
+ resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map(), cursorHandleOffset: { x: 0, y: 0 } };
1222
1230
  render();
1223
1231
  refreshSelectionBounds(editor);
1224
1232
  editor.endHistoryEntry();
@@ -1251,7 +1259,6 @@ function useTsdrawCanvasController(options = {}) {
1251
1259
  }
1252
1260
  setSelectedShapeIds(ids);
1253
1261
  selectedShapeIdsRef.current = ids;
1254
- setSelectionRotationDeg(0);
1255
1262
  setSelectionBrush(null);
1256
1263
  selectDragRef.current.mode = "none";
1257
1264
  render();
@@ -1434,11 +1441,17 @@ function useTsdrawCanvasController(options = {}) {
1434
1441
  resize();
1435
1442
  const ro = new ResizeObserver(resize);
1436
1443
  ro.observe(container);
1444
+ const handlePointerLeaveViewport = (e) => {
1445
+ if (e.relatedTarget === null) {
1446
+ setIsPointerInsideCanvas(false);
1447
+ }
1448
+ };
1437
1449
  canvas.addEventListener("pointerdown", handlePointerDown);
1438
1450
  container.addEventListener("wheel", handleWheel, { passive: false });
1439
1451
  document.addEventListener("gesturestart", handleGestureEvent);
1440
1452
  document.addEventListener("gesturechange", handleGestureEvent);
1441
1453
  document.addEventListener("gestureend", handleGestureEvent);
1454
+ document.documentElement.addEventListener("pointerleave", handlePointerLeaveViewport);
1442
1455
  window.addEventListener("pointermove", handlePointerMove);
1443
1456
  window.addEventListener("pointerup", handlePointerUp);
1444
1457
  window.addEventListener("pointercancel", handlePointerCancel);
@@ -1462,7 +1475,6 @@ function useTsdrawCanvasController(options = {}) {
1462
1475
  const changed = editor.undo();
1463
1476
  if (!changed) return false;
1464
1477
  reconcileSelectionAfterDocumentLoad();
1465
- setSelectionRotationDeg(0);
1466
1478
  render();
1467
1479
  syncHistoryState();
1468
1480
  return true;
@@ -1471,7 +1483,6 @@ function useTsdrawCanvasController(options = {}) {
1471
1483
  const changed = editor.redo();
1472
1484
  if (!changed) return false;
1473
1485
  reconcileSelectionAfterDocumentLoad();
1474
- setSelectionRotationDeg(0);
1475
1486
  render();
1476
1487
  syncHistoryState();
1477
1488
  return true;
@@ -1499,6 +1510,7 @@ function useTsdrawCanvasController(options = {}) {
1499
1510
  document.removeEventListener("gesturestart", handleGestureEvent);
1500
1511
  document.removeEventListener("gesturechange", handleGestureEvent);
1501
1512
  document.removeEventListener("gestureend", handleGestureEvent);
1513
+ document.documentElement.removeEventListener("pointerleave", handlePointerLeaveViewport);
1502
1514
  window.removeEventListener("pointermove", handlePointerMove);
1503
1515
  window.removeEventListener("pointerup", handlePointerUp);
1504
1516
  window.removeEventListener("pointercancel", handlePointerCancel);
@@ -1563,7 +1575,6 @@ function useTsdrawCanvasController(options = {}) {
1563
1575
  selectedShapeIdsRef.current = nextSelectedShapeIds;
1564
1576
  setSelectedShapeIds(nextSelectedShapeIds);
1565
1577
  }
1566
- setSelectionRotationDeg(0);
1567
1578
  render();
1568
1579
  setCanUndo(editor.canUndo());
1569
1580
  setCanRedo(editor.canRedo());
@@ -1579,17 +1590,18 @@ function useTsdrawCanvasController(options = {}) {
1579
1590
  selectedShapeIdsRef.current = nextSelectedShapeIds;
1580
1591
  setSelectedShapeIds(nextSelectedShapeIds);
1581
1592
  }
1582
- setSelectionRotationDeg(0);
1583
1593
  render();
1584
1594
  setCanUndo(editor.canUndo());
1585
1595
  setCanRedo(editor.canRedo());
1586
1596
  return true;
1587
1597
  }, [render]);
1598
+ 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
1599
  const showToolOverlay = isPointerInsideCanvas && (currentTool === "pen" || currentTool === "eraser");
1589
1600
  const canvasCursor = getCanvasCursor(currentTool, {
1590
1601
  isMovingSelection,
1591
1602
  isResizingSelection,
1592
1603
  isRotatingSelection,
1604
+ isHoveringSelectionBounds,
1593
1605
  showToolOverlay
1594
1606
  });
1595
1607
  const cursorContext = {