@tsdraw/react 0.9.4 → 0.9.5

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
@@ -10,6 +10,7 @@ function SelectionOverlay({
10
10
  selectionBrush,
11
11
  selectionBounds,
12
12
  selectionRotationDeg,
13
+ vertexHandleScreenPositions,
13
14
  currentTool,
14
15
  selectedCount,
15
16
  onRotatePointerDown,
@@ -37,7 +38,8 @@ function SelectionOverlay({
37
38
  top: selectionBounds.top,
38
39
  width: selectionBounds.width,
39
40
  height: selectionBounds.height,
40
- transform: `rotate(${selectionRotationDeg}deg)`
41
+ transform: `rotate(${selectionRotationDeg}deg)`,
42
+ transformOrigin: "center center"
41
43
  },
42
44
  children: [
43
45
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-selection-bounds" }),
@@ -95,7 +97,15 @@ function SelectionOverlay({
95
97
  ] })
96
98
  ]
97
99
  }
98
- )
100
+ ),
101
+ currentTool === "select" && vertexHandleScreenPositions.map((pos, index) => /* @__PURE__ */ jsxRuntime.jsx(
102
+ "div",
103
+ {
104
+ className: "tsdraw-vertex-handle",
105
+ style: { position: "absolute", left: pos.left, top: pos.top, transform: "translate(-50%, -50%)", pointerEvents: "none", zIndex: 95 }
106
+ },
107
+ `vertex-${index.toString()}`
108
+ ))
99
109
  ] });
100
110
  }
101
111
  function parseAnchor(anchor) {
@@ -454,6 +464,7 @@ function getCanvasCursor(currentTool, state) {
454
464
  if (state.isRotatingSelection) return "grabbing";
455
465
  if (state.isResizingSelection) return "nwse-resize";
456
466
  if (state.isMovingSelection) return "grabbing";
467
+ if (state.isDraggingVertex) return "grabbing";
457
468
  if (state.isHoveringSelectionBounds) return "move";
458
469
  return "default";
459
470
  }
@@ -896,6 +907,16 @@ function toScreenRect(editor, bounds) {
896
907
  height: Math.max(0, maxY - minY)
897
908
  };
898
909
  }
910
+ function selectionCenteredOnRotation(editor, startBoundsPage, centerPage) {
911
+ const startScreen = toScreenRect(editor, startBoundsPage);
912
+ const centerScreen = core.pageToScreen(editor.viewport, centerPage.x, centerPage.y);
913
+ return {
914
+ left: centerScreen.x - startScreen.width / 2,
915
+ top: centerScreen.y - startScreen.height / 2,
916
+ width: startScreen.width,
917
+ height: startScreen.height
918
+ };
919
+ }
899
920
  function resolveDrawColor(colorStyle, theme) {
900
921
  return core.resolveThemeColor(colorStyle, theme);
901
922
  }
@@ -956,7 +977,8 @@ function useTsdrawCanvasController(options = {}) {
956
977
  center: null,
957
978
  startAngle: 0,
958
979
  startSelectionRotationDeg: 0,
959
- startShapes: /* @__PURE__ */ new Map()
980
+ startShapes: /* @__PURE__ */ new Map(),
981
+ startBoundsPage: null
960
982
  });
961
983
  const selectDragRef = react.useRef({
962
984
  mode: "none",
@@ -978,6 +1000,8 @@ function useTsdrawCanvasController(options = {}) {
978
1000
  const [isMovingSelection, setIsMovingSelection] = react.useState(false);
979
1001
  const [isResizingSelection, setIsResizingSelection] = react.useState(false);
980
1002
  const [isRotatingSelection, setIsRotatingSelection] = react.useState(false);
1003
+ const [isDraggingVertex, setIsDraggingVertex] = react.useState(false);
1004
+ const [vertexHandleScreenPositions, setVertexHandleScreenPositions] = react.useState([]);
981
1005
  const [canUndo, setCanUndo] = react.useState(false);
982
1006
  const [canRedo, setCanRedo] = react.useState(false);
983
1007
  const [isPersistenceReady, setIsPersistenceReady] = react.useState(!options.persistenceKey);
@@ -1071,9 +1095,12 @@ function useTsdrawCanvasController(options = {}) {
1071
1095
  setIsMovingSelection(false);
1072
1096
  setIsResizingSelection(false);
1073
1097
  setIsRotatingSelection(false);
1098
+ setIsDraggingVertex(false);
1074
1099
  selectDragRef.current.mode = "none";
1100
+ selectDragRef.current.vertexRefs = void 0;
1101
+ selectDragRef.current.vertexSnapshots = void 0;
1075
1102
  resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map(), cursorHandleOffset: { x: 0, y: 0 } };
1076
- rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map() };
1103
+ rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map(), startBoundsPage: null };
1077
1104
  }, []);
1078
1105
  const handleResizePointerDown = react.useCallback(
1079
1106
  (e, handle) => {
@@ -1127,8 +1154,10 @@ function useTsdrawCanvasController(options = {}) {
1127
1154
  center,
1128
1155
  startAngle: Math.atan2(p.y - center.y, p.x - center.x),
1129
1156
  startSelectionRotationDeg: selectionRotationRef.current,
1130
- startShapes: core.buildTransformSnapshots(editor, selectedShapeIdsRef.current)
1157
+ startShapes: core.buildTransformSnapshots(editor, selectedShapeIdsRef.current),
1158
+ startBoundsPage: bounds
1131
1159
  };
1160
+ setSelectionBounds(selectionCenteredOnRotation(editor, bounds, center));
1132
1161
  isPointerActiveRef.current = true;
1133
1162
  activePointerIdsRef.current.add(e.pointerId);
1134
1163
  canvas.setPointerCapture(e.pointerId);
@@ -1394,6 +1423,30 @@ function useTsdrawCanvasController(options = {}) {
1394
1423
  const pressure = (first.pressure ?? 0.5) * pressureSensitivity;
1395
1424
  const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
1396
1425
  if (currentToolRef.current === "select") {
1426
+ if (!readOnlyRef.current) {
1427
+ const zoom = editor.viewport.zoom;
1428
+ const marginPage = 12 / zoom;
1429
+ const clusterTolerance = 20 / zoom;
1430
+ const vertexHit = core.findVertexHit(editor, { x, y }, marginPage, clusterTolerance);
1431
+ if (vertexHit) {
1432
+ const involvedIds = [...new Set(vertexHit.refs.map((r) => r.shapeId))];
1433
+ selectDragRef.current = {
1434
+ mode: "vertex",
1435
+ startPage: { x, y },
1436
+ currentPage: { x, y },
1437
+ startPositions: /* @__PURE__ */ new Map(),
1438
+ additive: false,
1439
+ initialSelection: [...selectedShapeIdsRef.current],
1440
+ vertexRefs: vertexHit.refs,
1441
+ vertexSnapshots: vertexHit.snapshots
1442
+ };
1443
+ setIsDraggingVertex(true);
1444
+ setSelectedShapeIds(involvedIds);
1445
+ selectedShapeIdsRef.current = involvedIds;
1446
+ refreshSelectionBounds(editor, involvedIds);
1447
+ return;
1448
+ }
1449
+ }
1397
1450
  const hit = core.getTopShapeAtPoint(editor, { x, y });
1398
1451
  const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
1399
1452
  const isInsideSelectionBounds = (() => {
@@ -1409,7 +1462,8 @@ function useTsdrawCanvasController(options = {}) {
1409
1462
  currentPage: { x, y },
1410
1463
  startPositions: core.buildStartPositions(editor, selectedShapeIdsRef.current),
1411
1464
  additive: false,
1412
- initialSelection: [...selectedShapeIdsRef.current]
1465
+ initialSelection: [...selectedShapeIdsRef.current],
1466
+ moveFromEmptyInsideBounds: isInsideSelectionBounds && !hit
1413
1467
  };
1414
1468
  setIsMovingSelection(true);
1415
1469
  return;
@@ -1465,13 +1519,14 @@ function useTsdrawCanvasController(options = {}) {
1465
1519
  const mode = selectDragRef.current.mode;
1466
1520
  const { x: px, y: py } = editor.input.getCurrentPagePoint();
1467
1521
  if (mode === "rotate") {
1468
- const { center, startAngle, startSelectionRotationDeg, startShapes } = rotateRef.current;
1469
- if (!center) return;
1522
+ const { center, startAngle, startSelectionRotationDeg, startShapes, startBoundsPage } = rotateRef.current;
1523
+ if (!center || !startBoundsPage) return;
1470
1524
  const angle = Math.atan2(py - center.y, px - center.x);
1471
1525
  const delta = angle - startAngle;
1472
1526
  setSelectionRotationDeg(startSelectionRotationDeg + delta * 180 / Math.PI);
1473
1527
  core.applyRotation(editor, startShapes, center, delta);
1474
1528
  render();
1529
+ setSelectionBounds(selectionCenteredOnRotation(editor, startBoundsPage, center));
1475
1530
  return;
1476
1531
  }
1477
1532
  if (mode === "resize") {
@@ -1489,6 +1544,20 @@ function useTsdrawCanvasController(options = {}) {
1489
1544
  refreshSelectionBounds(editor);
1490
1545
  return;
1491
1546
  }
1547
+ if (mode === "vertex") {
1548
+ const drag = selectDragRef.current;
1549
+ const refs = drag.vertexRefs;
1550
+ const snapshots = drag.vertexSnapshots;
1551
+ if (refs && snapshots) {
1552
+ core.applyVertexDrag(editor, snapshots, refs, {
1553
+ x: px - drag.startPage.x,
1554
+ y: py - drag.startPage.y
1555
+ });
1556
+ render();
1557
+ refreshSelectionBounds(editor);
1558
+ }
1559
+ return;
1560
+ }
1492
1561
  if (mode === "marquee") {
1493
1562
  selectDragRef.current.currentPage = { x: px, y: py };
1494
1563
  const pageRect = core.normalizeSelectionBounds(selectDragRef.current.startPage, selectDragRef.current.currentPage);
@@ -1526,7 +1595,7 @@ function useTsdrawCanvasController(options = {}) {
1526
1595
  setIsRotatingSelection(false);
1527
1596
  selectDragRef.current.mode = "none";
1528
1597
  setSelectionRotationDeg(0);
1529
- rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map() };
1598
+ rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map(), startBoundsPage: null };
1530
1599
  render();
1531
1600
  refreshSelectionBounds(editor);
1532
1601
  editor.endHistoryEntry();
@@ -1544,6 +1613,24 @@ function useTsdrawCanvasController(options = {}) {
1544
1613
  if (drag.mode === "move") {
1545
1614
  setIsMovingSelection(false);
1546
1615
  selectDragRef.current.mode = "none";
1616
+ const distSq = (x - drag.startPage.x) ** 2 + (y - drag.startPage.y) ** 2;
1617
+ if (drag.moveFromEmptyInsideBounds && distSq < 4) {
1618
+ setSelectedShapeIds([]);
1619
+ selectedShapeIdsRef.current = [];
1620
+ setSelectionBounds(null);
1621
+ } else {
1622
+ refreshSelectionBounds(editor);
1623
+ }
1624
+ selectDragRef.current.moveFromEmptyInsideBounds = void 0;
1625
+ render();
1626
+ editor.endHistoryEntry();
1627
+ return;
1628
+ }
1629
+ if (drag.mode === "vertex") {
1630
+ setIsDraggingVertex(false);
1631
+ selectDragRef.current.mode = "none";
1632
+ selectDragRef.current.vertexRefs = void 0;
1633
+ selectDragRef.current.vertexSnapshots = void 0;
1547
1634
  render();
1548
1635
  refreshSelectionBounds(editor);
1549
1636
  editor.endHistoryEntry();
@@ -1622,9 +1709,21 @@ function useTsdrawCanvasController(options = {}) {
1622
1709
  editor.input.pointerUp();
1623
1710
  if (currentToolRef.current === "select") {
1624
1711
  const drag = selectDragRef.current;
1625
- if (drag.mode === "rotate") setIsRotatingSelection(false);
1712
+ if (drag.mode === "rotate") {
1713
+ setIsRotatingSelection(false);
1714
+ setSelectionRotationDeg(0);
1715
+ rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map(), startBoundsPage: null };
1716
+ }
1626
1717
  if (drag.mode === "resize") setIsResizingSelection(false);
1627
- if (drag.mode === "move") setIsMovingSelection(false);
1718
+ if (drag.mode === "move") {
1719
+ setIsMovingSelection(false);
1720
+ selectDragRef.current.moveFromEmptyInsideBounds = void 0;
1721
+ }
1722
+ if (drag.mode === "vertex") {
1723
+ setIsDraggingVertex(false);
1724
+ selectDragRef.current.vertexRefs = void 0;
1725
+ selectDragRef.current.vertexSnapshots = void 0;
1726
+ }
1628
1727
  if (drag.mode === "marquee") setSelectionBrush(null);
1629
1728
  if (drag.mode !== "none") {
1630
1729
  selectDragRef.current.mode = "none";
@@ -1961,10 +2060,25 @@ function useTsdrawCanvasController(options = {}) {
1961
2060
  }, [render]);
1962
2061
  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;
1963
2062
  const showToolOverlay = isPointerInsideCanvas && (currentTool === "pen" || currentTool === "eraser");
2063
+ react.useEffect(() => {
2064
+ const editor = editorRef.current;
2065
+ if (!editor || currentTool !== "select" || selectedShapeIds.length === 0) {
2066
+ setVertexHandleScreenPositions([]);
2067
+ return;
2068
+ }
2069
+ const pagePts = core.getVertexHandlePagePositions(editor, selectedShapeIds);
2070
+ setVertexHandleScreenPositions(
2071
+ pagePts.map((pg) => {
2072
+ const s = core.pageToScreen(editor.viewport, pg.x, pg.y);
2073
+ return { left: s.x, top: s.y };
2074
+ })
2075
+ );
2076
+ }, [currentTool, selectedShapeIds, selectionBounds, selectionRotationDeg, isPersistenceReady]);
1964
2077
  const canvasCursor = getCanvasCursor(currentTool, {
1965
2078
  isMovingSelection,
1966
2079
  isResizingSelection,
1967
2080
  isRotatingSelection,
2081
+ isDraggingVertex,
1968
2082
  isHoveringSelectionBounds,
1969
2083
  showToolOverlay
1970
2084
  });
@@ -1974,7 +2088,8 @@ function useTsdrawCanvasController(options = {}) {
1974
2088
  showToolOverlay,
1975
2089
  isMovingSelection,
1976
2090
  isResizingSelection,
1977
- isRotatingSelection
2091
+ isRotatingSelection,
2092
+ isDraggingVertex
1978
2093
  };
1979
2094
  const toolOverlay = {
1980
2095
  visible: showToolOverlay,
@@ -1997,6 +2112,7 @@ function useTsdrawCanvasController(options = {}) {
1997
2112
  selectionBrush,
1998
2113
  selectionBounds,
1999
2114
  selectionRotationDeg,
2115
+ vertexHandleScreenPositions,
2000
2116
  canvasCursor,
2001
2117
  cursorContext,
2002
2118
  toolOverlay,
@@ -2107,6 +2223,7 @@ function Tsdraw(props) {
2107
2223
  selectionBrush,
2108
2224
  selectionBounds,
2109
2225
  selectionRotationDeg,
2226
+ vertexHandleScreenPositions,
2110
2227
  canvasCursor: defaultCanvasCursor,
2111
2228
  cursorContext,
2112
2229
  toolOverlay,
@@ -2288,6 +2405,7 @@ function Tsdraw(props) {
2288
2405
  selectionBrush,
2289
2406
  selectionBounds,
2290
2407
  selectionRotationDeg,
2408
+ vertexHandleScreenPositions,
2291
2409
  currentTool,
2292
2410
  selectedCount: selectedShapeIds.length,
2293
2411
  onRotatePointerDown: handleRotatePointerDown,