@tsdraw/react 0.9.3 → 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
  }
@@ -911,6 +932,11 @@ function getHandlePagePoint(bounds, handle) {
911
932
  return { x: bounds.maxX, y: bounds.maxY };
912
933
  }
913
934
  }
935
+ function resolveAutoShapeOption(input) {
936
+ if (input === false) return { enabled: false };
937
+ if (input === true || input == null) return { enabled: true };
938
+ return { enabled: input.enabled ?? true, ...input };
939
+ }
914
940
  var ZOOM_WHEEL_CAP = 10;
915
941
  var VIEW_ONLY_TOOLS = /* @__PURE__ */ new Set(["select", "hand"]);
916
942
  function useTsdrawCanvasController(options = {}) {
@@ -922,6 +948,7 @@ function useTsdrawCanvasController(options = {}) {
922
948
  const touchOptionsRef = react.useRef(options.touchOptions);
923
949
  const keyboardShortcutsRef = react.useRef(options.keyboardShortcuts);
924
950
  const penOptionsRef = react.useRef(options.penOptions);
951
+ const autoShapeRef = react.useRef(options.autoShape);
925
952
  const backgroundRef = react.useRef(options.background);
926
953
  const readOnlyRef = react.useRef(options.readOnly ?? false);
927
954
  const containerRef = react.useRef(null);
@@ -950,7 +977,8 @@ function useTsdrawCanvasController(options = {}) {
950
977
  center: null,
951
978
  startAngle: 0,
952
979
  startSelectionRotationDeg: 0,
953
- startShapes: /* @__PURE__ */ new Map()
980
+ startShapes: /* @__PURE__ */ new Map(),
981
+ startBoundsPage: null
954
982
  });
955
983
  const selectDragRef = react.useRef({
956
984
  mode: "none",
@@ -972,6 +1000,8 @@ function useTsdrawCanvasController(options = {}) {
972
1000
  const [isMovingSelection, setIsMovingSelection] = react.useState(false);
973
1001
  const [isResizingSelection, setIsResizingSelection] = react.useState(false);
974
1002
  const [isRotatingSelection, setIsRotatingSelection] = react.useState(false);
1003
+ const [isDraggingVertex, setIsDraggingVertex] = react.useState(false);
1004
+ const [vertexHandleScreenPositions, setVertexHandleScreenPositions] = react.useState([]);
975
1005
  const [canUndo, setCanUndo] = react.useState(false);
976
1006
  const [canRedo, setCanRedo] = react.useState(false);
977
1007
  const [isPersistenceReady, setIsPersistenceReady] = react.useState(!options.persistenceKey);
@@ -1004,6 +1034,11 @@ function useTsdrawCanvasController(options = {}) {
1004
1034
  react.useEffect(() => {
1005
1035
  penOptionsRef.current = options.penOptions;
1006
1036
  }, [options.penOptions]);
1037
+ react.useEffect(() => {
1038
+ autoShapeRef.current = options.autoShape;
1039
+ const editor = editorRef.current;
1040
+ if (editor) editor.setAutoShape(resolveAutoShapeOption(options.autoShape));
1041
+ }, [options.autoShape]);
1007
1042
  react.useEffect(() => {
1008
1043
  backgroundRef.current = options.background;
1009
1044
  }, [options.background]);
@@ -1060,9 +1095,12 @@ function useTsdrawCanvasController(options = {}) {
1060
1095
  setIsMovingSelection(false);
1061
1096
  setIsResizingSelection(false);
1062
1097
  setIsRotatingSelection(false);
1098
+ setIsDraggingVertex(false);
1063
1099
  selectDragRef.current.mode = "none";
1100
+ selectDragRef.current.vertexRefs = void 0;
1101
+ selectDragRef.current.vertexSnapshots = void 0;
1064
1102
  resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map(), cursorHandleOffset: { x: 0, y: 0 } };
1065
- 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 };
1066
1104
  }, []);
1067
1105
  const handleResizePointerDown = react.useCallback(
1068
1106
  (e, handle) => {
@@ -1116,8 +1154,10 @@ function useTsdrawCanvasController(options = {}) {
1116
1154
  center,
1117
1155
  startAngle: Math.atan2(p.y - center.y, p.x - center.x),
1118
1156
  startSelectionRotationDeg: selectionRotationRef.current,
1119
- startShapes: core.buildTransformSnapshots(editor, selectedShapeIdsRef.current)
1157
+ startShapes: core.buildTransformSnapshots(editor, selectedShapeIdsRef.current),
1158
+ startBoundsPage: bounds
1120
1159
  };
1160
+ setSelectionBounds(selectionCenteredOnRotation(editor, bounds, center));
1121
1161
  isPointerActiveRef.current = true;
1122
1162
  activePointerIdsRef.current.add(e.pointerId);
1123
1163
  canvas.setPointerCapture(e.pointerId);
@@ -1142,7 +1182,8 @@ function useTsdrawCanvasController(options = {}) {
1142
1182
  const editor = new core.Editor({
1143
1183
  toolDefinitions: options.toolDefinitions,
1144
1184
  initialToolId: initialTool,
1145
- zoomRange: cameraOpts?.zoomRange
1185
+ zoomRange: cameraOpts?.zoomRange,
1186
+ autoShape: resolveAutoShapeOption(autoShapeRef.current)
1146
1187
  });
1147
1188
  editor.renderer.setTheme(options.theme ?? "light");
1148
1189
  if (!editor.tools.hasTool(initialTool)) {
@@ -1382,6 +1423,30 @@ function useTsdrawCanvasController(options = {}) {
1382
1423
  const pressure = (first.pressure ?? 0.5) * pressureSensitivity;
1383
1424
  const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
1384
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
+ }
1385
1450
  const hit = core.getTopShapeAtPoint(editor, { x, y });
1386
1451
  const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
1387
1452
  const isInsideSelectionBounds = (() => {
@@ -1397,7 +1462,8 @@ function useTsdrawCanvasController(options = {}) {
1397
1462
  currentPage: { x, y },
1398
1463
  startPositions: core.buildStartPositions(editor, selectedShapeIdsRef.current),
1399
1464
  additive: false,
1400
- initialSelection: [...selectedShapeIdsRef.current]
1465
+ initialSelection: [...selectedShapeIdsRef.current],
1466
+ moveFromEmptyInsideBounds: isInsideSelectionBounds && !hit
1401
1467
  };
1402
1468
  setIsMovingSelection(true);
1403
1469
  return;
@@ -1453,13 +1519,14 @@ function useTsdrawCanvasController(options = {}) {
1453
1519
  const mode = selectDragRef.current.mode;
1454
1520
  const { x: px, y: py } = editor.input.getCurrentPagePoint();
1455
1521
  if (mode === "rotate") {
1456
- const { center, startAngle, startSelectionRotationDeg, startShapes } = rotateRef.current;
1457
- if (!center) return;
1522
+ const { center, startAngle, startSelectionRotationDeg, startShapes, startBoundsPage } = rotateRef.current;
1523
+ if (!center || !startBoundsPage) return;
1458
1524
  const angle = Math.atan2(py - center.y, px - center.x);
1459
1525
  const delta = angle - startAngle;
1460
1526
  setSelectionRotationDeg(startSelectionRotationDeg + delta * 180 / Math.PI);
1461
1527
  core.applyRotation(editor, startShapes, center, delta);
1462
1528
  render();
1529
+ setSelectionBounds(selectionCenteredOnRotation(editor, startBoundsPage, center));
1463
1530
  return;
1464
1531
  }
1465
1532
  if (mode === "resize") {
@@ -1477,6 +1544,20 @@ function useTsdrawCanvasController(options = {}) {
1477
1544
  refreshSelectionBounds(editor);
1478
1545
  return;
1479
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
+ }
1480
1561
  if (mode === "marquee") {
1481
1562
  selectDragRef.current.currentPage = { x: px, y: py };
1482
1563
  const pageRect = core.normalizeSelectionBounds(selectDragRef.current.startPage, selectDragRef.current.currentPage);
@@ -1514,7 +1595,7 @@ function useTsdrawCanvasController(options = {}) {
1514
1595
  setIsRotatingSelection(false);
1515
1596
  selectDragRef.current.mode = "none";
1516
1597
  setSelectionRotationDeg(0);
1517
- 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 };
1518
1599
  render();
1519
1600
  refreshSelectionBounds(editor);
1520
1601
  editor.endHistoryEntry();
@@ -1532,6 +1613,24 @@ function useTsdrawCanvasController(options = {}) {
1532
1613
  if (drag.mode === "move") {
1533
1614
  setIsMovingSelection(false);
1534
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;
1535
1634
  render();
1536
1635
  refreshSelectionBounds(editor);
1537
1636
  editor.endHistoryEntry();
@@ -1610,9 +1709,21 @@ function useTsdrawCanvasController(options = {}) {
1610
1709
  editor.input.pointerUp();
1611
1710
  if (currentToolRef.current === "select") {
1612
1711
  const drag = selectDragRef.current;
1613
- 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
+ }
1614
1717
  if (drag.mode === "resize") setIsResizingSelection(false);
1615
- 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
+ }
1616
1727
  if (drag.mode === "marquee") setSelectionBrush(null);
1617
1728
  if (drag.mode !== "none") {
1618
1729
  selectDragRef.current.mode = "none";
@@ -1949,10 +2060,25 @@ function useTsdrawCanvasController(options = {}) {
1949
2060
  }, [render]);
1950
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;
1951
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]);
1952
2077
  const canvasCursor = getCanvasCursor(currentTool, {
1953
2078
  isMovingSelection,
1954
2079
  isResizingSelection,
1955
2080
  isRotatingSelection,
2081
+ isDraggingVertex,
1956
2082
  isHoveringSelectionBounds,
1957
2083
  showToolOverlay
1958
2084
  });
@@ -1962,7 +2088,8 @@ function useTsdrawCanvasController(options = {}) {
1962
2088
  showToolOverlay,
1963
2089
  isMovingSelection,
1964
2090
  isResizingSelection,
1965
- isRotatingSelection
2091
+ isRotatingSelection,
2092
+ isDraggingVertex
1966
2093
  };
1967
2094
  const toolOverlay = {
1968
2095
  visible: showToolOverlay,
@@ -1985,6 +2112,7 @@ function useTsdrawCanvasController(options = {}) {
1985
2112
  selectionBrush,
1986
2113
  selectionBounds,
1987
2114
  selectionRotationDeg,
2115
+ vertexHandleScreenPositions,
1988
2116
  canvasCursor,
1989
2117
  cursorContext,
1990
2118
  toolOverlay,
@@ -2095,6 +2223,7 @@ function Tsdraw(props) {
2095
2223
  selectionBrush,
2096
2224
  selectionBounds,
2097
2225
  selectionRotationDeg,
2226
+ vertexHandleScreenPositions,
2098
2227
  canvasCursor: defaultCanvasCursor,
2099
2228
  cursorContext,
2100
2229
  toolOverlay,
@@ -2117,6 +2246,7 @@ function Tsdraw(props) {
2117
2246
  touchOptions: props.touchOptions,
2118
2247
  keyboardShortcuts: props.keyboardShortcuts,
2119
2248
  penOptions: props.penOptions,
2249
+ autoShape: props.autoShape,
2120
2250
  background: props.background,
2121
2251
  readOnly: props.readOnly,
2122
2252
  autoFocus: props.autoFocus,
@@ -2275,6 +2405,7 @@ function Tsdraw(props) {
2275
2405
  selectionBrush,
2276
2406
  selectionBounds,
2277
2407
  selectionRotationDeg,
2408
+ vertexHandleScreenPositions,
2278
2409
  currentTool,
2279
2410
  selectedCount: selectedShapeIds.length,
2280
2411
  onRotatePointerDown: handleRotatePointerDown,