@tsdraw/react 0.8.3 → 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
@@ -295,8 +295,6 @@ function getCanvasCursor(currentTool, state) {
295
295
  }
296
296
  return state.showToolOverlay ? "none" : "crosshair";
297
297
  }
298
-
299
- // src/canvas/touchInteractions.ts
300
298
  var TAP_MAX_DURATION_MS = 100;
301
299
  var DOUBLE_TAP_INTERVAL_MS = 100;
302
300
  var TAP_MOVE_TOLERANCE = 14;
@@ -318,15 +316,28 @@ function createTouchInteractionController(editor, canvas, handlers) {
318
316
  mode: "not-sure",
319
317
  previousCenter: { x: 0, y: 0 },
320
318
  initialCenter: { x: 0, y: 0 },
321
- previousDistance: 1,
322
- initialDistance: 1
319
+ initialDistance: 1,
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;
340
+ touchCameraState.initialZoom = 1;
330
341
  };
331
342
  const maybeHandleTouchTapGesture = () => {
332
343
  if (activeTouchPoints.size > 0) return;
@@ -361,8 +372,8 @@ function createTouchInteractionController(editor, canvas, handlers) {
361
372
  touchCameraState.mode = "not-sure";
362
373
  touchCameraState.previousCenter = center;
363
374
  touchCameraState.initialCenter = center;
364
- touchCameraState.previousDistance = Math.max(1, distance);
365
375
  touchCameraState.initialDistance = Math.max(1, distance);
376
+ touchCameraState.initialZoom = editor.getZoomLevel();
366
377
  };
367
378
  const updateTouchCameraGesture = () => {
368
379
  if (!touchCameraState.active) return false;
@@ -386,18 +397,27 @@ function createTouchInteractionController(editor, canvas, handlers) {
386
397
  const canvasRect = canvas.getBoundingClientRect();
387
398
  const centerOnCanvasX = center.x - canvasRect.left;
388
399
  const centerOnCanvasY = center.y - canvasRect.top;
389
- editor.panBy(centerDx, centerDy);
390
400
  if (touchCameraState.mode === "zooming") {
391
- const zoomFactor = distance / touchCameraState.previousDistance;
392
- editor.zoomAt(zoomFactor, 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);
393
413
  }
394
414
  touchCameraState.previousCenter = center;
395
- touchCameraState.previousDistance = distance;
396
415
  handlers.refreshView();
397
416
  return true;
398
417
  };
399
418
  const handlePointerDown = (event) => {
400
419
  if (!isTouchPointer(event)) return false;
420
+ stopFingerPanSlide();
401
421
  activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
402
422
  if (!touchTapState.active) {
403
423
  touchTapState.active = true;
@@ -410,9 +430,16 @@ function createTouchInteractionController(editor, canvas, handlers) {
410
430
  }
411
431
  touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
412
432
  if (activeTouchPoints.size === 2) {
433
+ endFingerPan();
413
434
  beginTouchCameraGesture();
414
435
  return true;
415
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
+ }
416
443
  return false;
417
444
  };
418
445
  const handlePointerMove = (event) => {
@@ -423,16 +450,34 @@ function createTouchInteractionController(editor, canvas, handlers) {
423
450
  const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
424
451
  if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
425
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
+ }
426
459
  return updateTouchCameraGesture();
427
460
  };
428
461
  const handlePointerUpOrCancel = (event) => {
429
462
  if (!isTouchPointer(event)) return false;
430
463
  const wasCameraGestureActive = touchCameraState.active;
464
+ const wasFingerPan = fingerPanPointerId === event.pointerId;
465
+ const releasedPanSession = wasFingerPan ? fingerPanSession : null;
431
466
  activeTouchPoints.delete(event.pointerId);
432
467
  touchTapState.startPoints.delete(event.pointerId);
433
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
+ }
434
479
  maybeHandleTouchTapGesture();
435
- return wasCameraGestureActive;
480
+ return wasCameraGestureActive || wasFingerPan;
436
481
  };
437
482
  let gestureLastScale = 1;
438
483
  let gestureActive = false;
@@ -466,6 +511,8 @@ function createTouchInteractionController(editor, canvas, handlers) {
466
511
  touchTapState.active = false;
467
512
  touchTapState.startPoints.clear();
468
513
  endTouchCameraGesture();
514
+ endFingerPan();
515
+ stopFingerPanSlide();
469
516
  };
470
517
  return {
471
518
  handlePointerDown,
@@ -474,6 +521,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
474
521
  handleGestureEvent,
475
522
  reset,
476
523
  isCameraGestureActive: () => touchCameraState.active,
524
+ isFingerPanActive: () => fingerPanPointerId !== null,
477
525
  isTrackpadZoomActive: () => gestureActive
478
526
  };
479
527
  }
@@ -702,6 +750,7 @@ function useTsdrawCanvasController(options = {}) {
702
750
  const schedulePersistRef = useRef(null);
703
751
  const isPointerActiveRef = useRef(false);
704
752
  const pendingRemoteDocumentRef = useRef(null);
753
+ const activeCameraSlideRef = useRef(null);
705
754
  const selectionRotationRef = useRef(0);
706
755
  const resizeRef = useRef({
707
756
  handle: null,
@@ -1050,12 +1099,19 @@ function useTsdrawCanvasController(options = {}) {
1050
1099
  refreshSelectionBounds(editor);
1051
1100
  },
1052
1101
  runUndo: () => applyDocumentChangeResult(editor.undo()),
1053
- runRedo: () => applyDocumentChangeResult(editor.redo())
1102
+ runRedo: () => applyDocumentChangeResult(editor.redo()),
1103
+ isPenModeActive: () => penModeRef.current
1054
1104
  });
1055
- const isDrawingTool = (tool) => tool !== "select" && tool !== "hand";
1056
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
+ };
1057
1112
  const handlePointerDown = (e) => {
1058
1113
  if (!canvas.contains(e.target)) return;
1114
+ stopActiveSlide();
1059
1115
  if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1060
1116
  penDetectedRef.current = true;
1061
1117
  penModeRef.current = true;
@@ -1063,15 +1119,15 @@ function useTsdrawCanvasController(options = {}) {
1063
1119
  lastPointerDownWithRef.current = e.pointerType;
1064
1120
  activePointerIdsRef.current.add(e.pointerId);
1065
1121
  const startedCameraGesture = touchInteractions.handlePointerDown(e);
1066
- if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
1122
+ if (startedCameraGesture || touchInteractions.isCameraGestureActive() || touchInteractions.isFingerPanActive()) {
1067
1123
  e.preventDefault();
1068
1124
  if (!canvas.hasPointerCapture(e.pointerId)) {
1069
1125
  canvas.setPointerCapture(e.pointerId);
1070
1126
  }
1071
1127
  return;
1072
1128
  }
1073
- const allowPointerDown = !penModeRef.current || e.pointerType !== "touch" || !isDrawingTool(currentToolRef.current);
1074
- if (!allowPointerDown) {
1129
+ const isTouchBlockedByPenMode = penModeRef.current && e.pointerType === "touch";
1130
+ if (isTouchBlockedByPenMode) {
1075
1131
  return;
1076
1132
  }
1077
1133
  if (activePointerIdsRef.current.size > 1) {
@@ -1126,7 +1182,7 @@ function useTsdrawCanvasController(options = {}) {
1126
1182
  }
1127
1183
  editor.input.pointerDown(x, y, pressure, isPen);
1128
1184
  editor.input.setModifiers(first.shiftKey, first.ctrlKey, first.metaKey);
1129
- editor.tools.pointerDown({ point: { x, y, z: pressure } });
1185
+ editor.tools.pointerDown({ point: { x, y, z: pressure }, screenX: e.clientX, screenY: e.clientY });
1130
1186
  render();
1131
1187
  refreshSelectionBounds(editor);
1132
1188
  };
@@ -1139,7 +1195,7 @@ function useTsdrawCanvasController(options = {}) {
1139
1195
  e.preventDefault();
1140
1196
  return;
1141
1197
  }
1142
- if (penModeRef.current && e.pointerType === "touch" && isDrawingTool(currentToolRef.current) && !isPointerActiveRef.current) return;
1198
+ if (penModeRef.current && e.pointerType === "touch" && !isPointerActiveRef.current) return;
1143
1199
  if (activePointerIdsRef.current.size > 1) return;
1144
1200
  updatePointerPreview(e.clientX, e.clientY);
1145
1201
  const prevClient = lastPointerClientRef.current;
@@ -1193,7 +1249,7 @@ function useTsdrawCanvasController(options = {}) {
1193
1249
  }
1194
1250
  }
1195
1251
  editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
1196
- editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy });
1252
+ editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy, screenX: e.clientX, screenY: e.clientY });
1197
1253
  render();
1198
1254
  refreshSelectionBounds(editor);
1199
1255
  };
@@ -1272,9 +1328,26 @@ function useTsdrawCanvasController(options = {}) {
1272
1328
  return;
1273
1329
  }
1274
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
+ }
1275
1338
  editor.tools.pointerUp();
1276
1339
  render();
1277
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
+ }
1278
1351
  if (pendingRemoteDocumentRef.current) {
1279
1352
  const pendingRemoteDocument = pendingRemoteDocumentRef.current;
1280
1353
  pendingRemoteDocumentRef.current = null;
@@ -1519,6 +1592,7 @@ function useTsdrawCanvasController(options = {}) {
1519
1592
  isPointerActiveRef.current = false;
1520
1593
  activePointerIdsRef.current.clear();
1521
1594
  pendingRemoteDocumentRef.current = null;
1595
+ stopActiveSlide();
1522
1596
  touchInteractions.reset();
1523
1597
  persistenceChannel?.close();
1524
1598
  void persistenceDb?.close();