@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.cjs CHANGED
@@ -297,8 +297,6 @@ function getCanvasCursor(currentTool, state) {
297
297
  }
298
298
  return state.showToolOverlay ? "none" : "crosshair";
299
299
  }
300
-
301
- // src/canvas/touchInteractions.ts
302
300
  var TAP_MAX_DURATION_MS = 100;
303
301
  var DOUBLE_TAP_INTERVAL_MS = 100;
304
302
  var TAP_MOVE_TOLERANCE = 14;
@@ -320,15 +318,28 @@ function createTouchInteractionController(editor, canvas, handlers) {
320
318
  mode: "not-sure",
321
319
  previousCenter: { x: 0, y: 0 },
322
320
  initialCenter: { x: 0, y: 0 },
323
- previousDistance: 1,
324
- initialDistance: 1
321
+ initialDistance: 1,
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;
342
+ touchCameraState.initialZoom = 1;
332
343
  };
333
344
  const maybeHandleTouchTapGesture = () => {
334
345
  if (activeTouchPoints.size > 0) return;
@@ -363,8 +374,8 @@ function createTouchInteractionController(editor, canvas, handlers) {
363
374
  touchCameraState.mode = "not-sure";
364
375
  touchCameraState.previousCenter = center;
365
376
  touchCameraState.initialCenter = center;
366
- touchCameraState.previousDistance = Math.max(1, distance);
367
377
  touchCameraState.initialDistance = Math.max(1, distance);
378
+ touchCameraState.initialZoom = editor.getZoomLevel();
368
379
  };
369
380
  const updateTouchCameraGesture = () => {
370
381
  if (!touchCameraState.active) return false;
@@ -388,18 +399,27 @@ function createTouchInteractionController(editor, canvas, handlers) {
388
399
  const canvasRect = canvas.getBoundingClientRect();
389
400
  const centerOnCanvasX = center.x - canvasRect.left;
390
401
  const centerOnCanvasY = center.y - canvasRect.top;
391
- editor.panBy(centerDx, centerDy);
392
402
  if (touchCameraState.mode === "zooming") {
393
- const zoomFactor = distance / touchCameraState.previousDistance;
394
- editor.zoomAt(zoomFactor, 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);
395
415
  }
396
416
  touchCameraState.previousCenter = center;
397
- touchCameraState.previousDistance = distance;
398
417
  handlers.refreshView();
399
418
  return true;
400
419
  };
401
420
  const handlePointerDown = (event) => {
402
421
  if (!isTouchPointer(event)) return false;
422
+ stopFingerPanSlide();
403
423
  activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
404
424
  if (!touchTapState.active) {
405
425
  touchTapState.active = true;
@@ -412,9 +432,16 @@ function createTouchInteractionController(editor, canvas, handlers) {
412
432
  }
413
433
  touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
414
434
  if (activeTouchPoints.size === 2) {
435
+ endFingerPan();
415
436
  beginTouchCameraGesture();
416
437
  return true;
417
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
+ }
418
445
  return false;
419
446
  };
420
447
  const handlePointerMove = (event) => {
@@ -425,16 +452,34 @@ function createTouchInteractionController(editor, canvas, handlers) {
425
452
  const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
426
453
  if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
427
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
+ }
428
461
  return updateTouchCameraGesture();
429
462
  };
430
463
  const handlePointerUpOrCancel = (event) => {
431
464
  if (!isTouchPointer(event)) return false;
432
465
  const wasCameraGestureActive = touchCameraState.active;
466
+ const wasFingerPan = fingerPanPointerId === event.pointerId;
467
+ const releasedPanSession = wasFingerPan ? fingerPanSession : null;
433
468
  activeTouchPoints.delete(event.pointerId);
434
469
  touchTapState.startPoints.delete(event.pointerId);
435
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
+ }
436
481
  maybeHandleTouchTapGesture();
437
- return wasCameraGestureActive;
482
+ return wasCameraGestureActive || wasFingerPan;
438
483
  };
439
484
  let gestureLastScale = 1;
440
485
  let gestureActive = false;
@@ -468,6 +513,8 @@ function createTouchInteractionController(editor, canvas, handlers) {
468
513
  touchTapState.active = false;
469
514
  touchTapState.startPoints.clear();
470
515
  endTouchCameraGesture();
516
+ endFingerPan();
517
+ stopFingerPanSlide();
471
518
  };
472
519
  return {
473
520
  handlePointerDown,
@@ -476,6 +523,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
476
523
  handleGestureEvent,
477
524
  reset,
478
525
  isCameraGestureActive: () => touchCameraState.active,
526
+ isFingerPanActive: () => fingerPanPointerId !== null,
479
527
  isTrackpadZoomActive: () => gestureActive
480
528
  };
481
529
  }
@@ -704,6 +752,7 @@ function useTsdrawCanvasController(options = {}) {
704
752
  const schedulePersistRef = react.useRef(null);
705
753
  const isPointerActiveRef = react.useRef(false);
706
754
  const pendingRemoteDocumentRef = react.useRef(null);
755
+ const activeCameraSlideRef = react.useRef(null);
707
756
  const selectionRotationRef = react.useRef(0);
708
757
  const resizeRef = react.useRef({
709
758
  handle: null,
@@ -1052,12 +1101,19 @@ function useTsdrawCanvasController(options = {}) {
1052
1101
  refreshSelectionBounds(editor);
1053
1102
  },
1054
1103
  runUndo: () => applyDocumentChangeResult(editor.undo()),
1055
- runRedo: () => applyDocumentChangeResult(editor.redo())
1104
+ runRedo: () => applyDocumentChangeResult(editor.redo()),
1105
+ isPenModeActive: () => penModeRef.current
1056
1106
  });
1057
- const isDrawingTool = (tool) => tool !== "select" && tool !== "hand";
1058
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
+ };
1059
1114
  const handlePointerDown = (e) => {
1060
1115
  if (!canvas.contains(e.target)) return;
1116
+ stopActiveSlide();
1061
1117
  if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1062
1118
  penDetectedRef.current = true;
1063
1119
  penModeRef.current = true;
@@ -1065,15 +1121,15 @@ function useTsdrawCanvasController(options = {}) {
1065
1121
  lastPointerDownWithRef.current = e.pointerType;
1066
1122
  activePointerIdsRef.current.add(e.pointerId);
1067
1123
  const startedCameraGesture = touchInteractions.handlePointerDown(e);
1068
- if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
1124
+ if (startedCameraGesture || touchInteractions.isCameraGestureActive() || touchInteractions.isFingerPanActive()) {
1069
1125
  e.preventDefault();
1070
1126
  if (!canvas.hasPointerCapture(e.pointerId)) {
1071
1127
  canvas.setPointerCapture(e.pointerId);
1072
1128
  }
1073
1129
  return;
1074
1130
  }
1075
- const allowPointerDown = !penModeRef.current || e.pointerType !== "touch" || !isDrawingTool(currentToolRef.current);
1076
- if (!allowPointerDown) {
1131
+ const isTouchBlockedByPenMode = penModeRef.current && e.pointerType === "touch";
1132
+ if (isTouchBlockedByPenMode) {
1077
1133
  return;
1078
1134
  }
1079
1135
  if (activePointerIdsRef.current.size > 1) {
@@ -1128,7 +1184,7 @@ function useTsdrawCanvasController(options = {}) {
1128
1184
  }
1129
1185
  editor.input.pointerDown(x, y, pressure, isPen);
1130
1186
  editor.input.setModifiers(first.shiftKey, first.ctrlKey, first.metaKey);
1131
- editor.tools.pointerDown({ point: { x, y, z: pressure } });
1187
+ editor.tools.pointerDown({ point: { x, y, z: pressure }, screenX: e.clientX, screenY: e.clientY });
1132
1188
  render();
1133
1189
  refreshSelectionBounds(editor);
1134
1190
  };
@@ -1141,7 +1197,7 @@ function useTsdrawCanvasController(options = {}) {
1141
1197
  e.preventDefault();
1142
1198
  return;
1143
1199
  }
1144
- if (penModeRef.current && e.pointerType === "touch" && isDrawingTool(currentToolRef.current) && !isPointerActiveRef.current) return;
1200
+ if (penModeRef.current && e.pointerType === "touch" && !isPointerActiveRef.current) return;
1145
1201
  if (activePointerIdsRef.current.size > 1) return;
1146
1202
  updatePointerPreview(e.clientX, e.clientY);
1147
1203
  const prevClient = lastPointerClientRef.current;
@@ -1195,7 +1251,7 @@ function useTsdrawCanvasController(options = {}) {
1195
1251
  }
1196
1252
  }
1197
1253
  editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
1198
- editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy });
1254
+ editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy, screenX: e.clientX, screenY: e.clientY });
1199
1255
  render();
1200
1256
  refreshSelectionBounds(editor);
1201
1257
  };
@@ -1274,9 +1330,26 @@ function useTsdrawCanvasController(options = {}) {
1274
1330
  return;
1275
1331
  }
1276
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
+ }
1277
1340
  editor.tools.pointerUp();
1278
1341
  render();
1279
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
+ }
1280
1353
  if (pendingRemoteDocumentRef.current) {
1281
1354
  const pendingRemoteDocument = pendingRemoteDocumentRef.current;
1282
1355
  pendingRemoteDocumentRef.current = null;
@@ -1521,6 +1594,7 @@ function useTsdrawCanvasController(options = {}) {
1521
1594
  isPointerActiveRef.current = false;
1522
1595
  activePointerIdsRef.current.clear();
1523
1596
  pendingRemoteDocumentRef.current = null;
1597
+ stopActiveSlide();
1524
1598
  touchInteractions.reset();
1525
1599
  persistenceChannel?.close();
1526
1600
  void persistenceDb?.close();