@tsdraw/react 0.7.0 → 0.8.0
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 +431 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +432 -32
- package/dist/index.js.map +1 -1
- package/dist/tsdraw.css +13 -10
- package/package.json +1 -1
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, 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, isSelectTool } 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,6 +295,249 @@ function getCanvasCursor(currentTool, state) {
|
|
|
295
295
|
return state.showToolOverlay ? "none" : "crosshair";
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// src/canvas/touchInteractions.ts
|
|
299
|
+
var TAP_MAX_DURATION_MS = 100;
|
|
300
|
+
var DOUBLE_TAP_INTERVAL_MS = 100;
|
|
301
|
+
var TAP_MOVE_TOLERANCE = 14;
|
|
302
|
+
var PINCH_MODE_ZOOM_DISTANCE = 24;
|
|
303
|
+
var PINCH_MODE_PAN_DISTANCE = 16;
|
|
304
|
+
var PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE = 64;
|
|
305
|
+
function createTouchInteractionController(editor, canvas, handlers) {
|
|
306
|
+
const activeTouchPoints = /* @__PURE__ */ new Map();
|
|
307
|
+
const touchTapState = {
|
|
308
|
+
active: false,
|
|
309
|
+
startTime: 0,
|
|
310
|
+
maxTouchCount: 0,
|
|
311
|
+
moved: false,
|
|
312
|
+
startPoints: /* @__PURE__ */ new Map(),
|
|
313
|
+
lastTapAtByCount: {}
|
|
314
|
+
};
|
|
315
|
+
const touchCameraState = {
|
|
316
|
+
active: false,
|
|
317
|
+
mode: "not-sure",
|
|
318
|
+
previousCenter: { x: 0, y: 0 },
|
|
319
|
+
initialCenter: { x: 0, y: 0 },
|
|
320
|
+
previousDistance: 1,
|
|
321
|
+
initialDistance: 1,
|
|
322
|
+
previousAngle: 0
|
|
323
|
+
};
|
|
324
|
+
const isTouchPointer = (event) => event.pointerType === "touch";
|
|
325
|
+
const endTouchCameraGesture = () => {
|
|
326
|
+
touchCameraState.active = false;
|
|
327
|
+
touchCameraState.mode = "not-sure";
|
|
328
|
+
touchCameraState.previousDistance = 1;
|
|
329
|
+
touchCameraState.initialDistance = 1;
|
|
330
|
+
touchCameraState.previousAngle = 0;
|
|
331
|
+
};
|
|
332
|
+
const maybeHandleTouchTapGesture = () => {
|
|
333
|
+
if (activeTouchPoints.size > 0) return;
|
|
334
|
+
if (!touchTapState.active) return;
|
|
335
|
+
const elapsed = performance.now() - touchTapState.startTime;
|
|
336
|
+
if (!touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
|
|
337
|
+
const fingerCount = touchTapState.maxTouchCount;
|
|
338
|
+
const now = performance.now();
|
|
339
|
+
const previousTapTime = touchTapState.lastTapAtByCount[fingerCount] ?? 0;
|
|
340
|
+
const isDoubleTap = previousTapTime > 0 && now - previousTapTime <= DOUBLE_TAP_INTERVAL_MS;
|
|
341
|
+
if (isDoubleTap) {
|
|
342
|
+
touchTapState.lastTapAtByCount[fingerCount] = 0;
|
|
343
|
+
if (fingerCount === 2) {
|
|
344
|
+
if (handlers.runUndo()) handlers.refreshView();
|
|
345
|
+
} else if (handlers.runRedo()) handlers.refreshView();
|
|
346
|
+
} else touchTapState.lastTapAtByCount[fingerCount] = now;
|
|
347
|
+
}
|
|
348
|
+
touchTapState.active = false;
|
|
349
|
+
touchTapState.startPoints.clear();
|
|
350
|
+
touchTapState.maxTouchCount = 0;
|
|
351
|
+
touchTapState.moved = false;
|
|
352
|
+
};
|
|
353
|
+
const beginTouchCameraGesture = () => {
|
|
354
|
+
const points = [...activeTouchPoints.values()];
|
|
355
|
+
if (points.length !== 2) return;
|
|
356
|
+
handlers.cancelActivePointerInteraction();
|
|
357
|
+
const first = points[0];
|
|
358
|
+
const second = points[1];
|
|
359
|
+
const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
|
360
|
+
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
|
+
touchCameraState.active = true;
|
|
363
|
+
touchCameraState.mode = "not-sure";
|
|
364
|
+
touchCameraState.previousCenter = center;
|
|
365
|
+
touchCameraState.initialCenter = center;
|
|
366
|
+
touchCameraState.previousDistance = Math.max(1, distance);
|
|
367
|
+
touchCameraState.initialDistance = Math.max(1, distance);
|
|
368
|
+
touchCameraState.previousAngle = angle;
|
|
369
|
+
};
|
|
370
|
+
const updateTouchCameraGesture = () => {
|
|
371
|
+
if (!touchCameraState.active) return false;
|
|
372
|
+
const points = [...activeTouchPoints.values()];
|
|
373
|
+
if (points.length !== 2) {
|
|
374
|
+
endTouchCameraGesture();
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const first = points[0];
|
|
378
|
+
const second = points[1];
|
|
379
|
+
const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
|
380
|
+
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
|
+
const centerDx = center.x - touchCameraState.previousCenter.x;
|
|
383
|
+
const centerDy = center.y - touchCameraState.previousCenter.y;
|
|
384
|
+
const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
|
|
385
|
+
const originDistance = Math.hypot(center.x - touchCameraState.initialCenter.x, center.y - touchCameraState.initialCenter.y);
|
|
386
|
+
if (touchCameraState.mode === "not-sure") {
|
|
387
|
+
if (touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
388
|
+
else if (originDistance > PINCH_MODE_PAN_DISTANCE) touchCameraState.mode = "panning";
|
|
389
|
+
} else if (touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
390
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
391
|
+
const centerOnCanvasX = center.x - canvasRect.left;
|
|
392
|
+
const centerOnCanvasY = center.y - canvasRect.top;
|
|
393
|
+
editor.panBy(centerDx, centerDy);
|
|
394
|
+
if (touchCameraState.mode === "zooming") {
|
|
395
|
+
const zoomFactor = distance / touchCameraState.previousDistance;
|
|
396
|
+
editor.zoomAt(zoomFactor, centerOnCanvasX, centerOnCanvasY);
|
|
397
|
+
editor.rotateAt(angle - touchCameraState.previousAngle, centerOnCanvasX, centerOnCanvasY);
|
|
398
|
+
}
|
|
399
|
+
touchCameraState.previousCenter = center;
|
|
400
|
+
touchCameraState.previousDistance = distance;
|
|
401
|
+
touchCameraState.previousAngle = angle;
|
|
402
|
+
handlers.refreshView();
|
|
403
|
+
return true;
|
|
404
|
+
};
|
|
405
|
+
const handlePointerDown = (event) => {
|
|
406
|
+
if (!isTouchPointer(event)) return false;
|
|
407
|
+
activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
408
|
+
if (!touchTapState.active) {
|
|
409
|
+
touchTapState.active = true;
|
|
410
|
+
touchTapState.startTime = performance.now();
|
|
411
|
+
touchTapState.maxTouchCount = activeTouchPoints.size;
|
|
412
|
+
touchTapState.moved = false;
|
|
413
|
+
touchTapState.startPoints.clear();
|
|
414
|
+
} else {
|
|
415
|
+
touchTapState.maxTouchCount = Math.max(touchTapState.maxTouchCount, activeTouchPoints.size);
|
|
416
|
+
}
|
|
417
|
+
touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
418
|
+
if (activeTouchPoints.size === 2) {
|
|
419
|
+
beginTouchCameraGesture();
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
};
|
|
424
|
+
const handlePointerMove = (event) => {
|
|
425
|
+
if (!isTouchPointer(event)) return false;
|
|
426
|
+
if (activeTouchPoints.has(event.pointerId)) activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
427
|
+
const tapStart = touchTapState.startPoints.get(event.pointerId);
|
|
428
|
+
if (tapStart) {
|
|
429
|
+
const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
|
|
430
|
+
if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
|
|
431
|
+
}
|
|
432
|
+
return updateTouchCameraGesture();
|
|
433
|
+
};
|
|
434
|
+
const handlePointerUpOrCancel = (event) => {
|
|
435
|
+
if (!isTouchPointer(event)) return false;
|
|
436
|
+
const wasCameraGestureActive = touchCameraState.active;
|
|
437
|
+
activeTouchPoints.delete(event.pointerId);
|
|
438
|
+
touchTapState.startPoints.delete(event.pointerId);
|
|
439
|
+
if (activeTouchPoints.size < 2) endTouchCameraGesture();
|
|
440
|
+
maybeHandleTouchTapGesture();
|
|
441
|
+
return wasCameraGestureActive;
|
|
442
|
+
};
|
|
443
|
+
let gestureLastScale = 1;
|
|
444
|
+
let gestureActive = false;
|
|
445
|
+
const handleGestureEvent = (event, container) => {
|
|
446
|
+
if (!container.contains(event.target)) return;
|
|
447
|
+
event.preventDefault();
|
|
448
|
+
const gestureEvent = event;
|
|
449
|
+
if (gestureEvent.scale == null) return;
|
|
450
|
+
if (event.type === "gesturestart") {
|
|
451
|
+
gestureLastScale = gestureEvent.scale;
|
|
452
|
+
gestureActive = true;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (event.type === "gestureend") {
|
|
456
|
+
gestureActive = false;
|
|
457
|
+
gestureLastScale = 1;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (event.type === "gesturechange" && gestureActive) {
|
|
461
|
+
const zoomFactor = gestureEvent.scale / gestureLastScale;
|
|
462
|
+
gestureLastScale = gestureEvent.scale;
|
|
463
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
464
|
+
const cx = (gestureEvent.clientX ?? canvasRect.left + canvasRect.width / 2) - canvasRect.left;
|
|
465
|
+
const cy = (gestureEvent.clientY ?? canvasRect.top + canvasRect.height / 2) - canvasRect.top;
|
|
466
|
+
editor.zoomAt(zoomFactor, cx, cy);
|
|
467
|
+
handlers.refreshView();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const reset = () => {
|
|
471
|
+
activeTouchPoints.clear();
|
|
472
|
+
touchTapState.active = false;
|
|
473
|
+
touchTapState.startPoints.clear();
|
|
474
|
+
endTouchCameraGesture();
|
|
475
|
+
};
|
|
476
|
+
return {
|
|
477
|
+
handlePointerDown,
|
|
478
|
+
handlePointerMove,
|
|
479
|
+
handlePointerUpOrCancel,
|
|
480
|
+
handleGestureEvent,
|
|
481
|
+
reset,
|
|
482
|
+
isCameraGestureActive: () => touchCameraState.active,
|
|
483
|
+
isTrackpadZoomActive: () => gestureActive
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/canvas/keyboardShortcuts.ts
|
|
488
|
+
var TOOL_SHORTCUTS = {
|
|
489
|
+
v: "select",
|
|
490
|
+
h: "hand",
|
|
491
|
+
e: "eraser",
|
|
492
|
+
p: "pen",
|
|
493
|
+
b: "pen",
|
|
494
|
+
d: "pen",
|
|
495
|
+
x: "pen",
|
|
496
|
+
r: "square",
|
|
497
|
+
o: "circle",
|
|
498
|
+
c: "circle"
|
|
499
|
+
};
|
|
500
|
+
function isEditableTarget(eventTarget) {
|
|
501
|
+
const element = eventTarget;
|
|
502
|
+
if (!element) return false;
|
|
503
|
+
if (element.isContentEditable) return true;
|
|
504
|
+
const tagName = element.tagName;
|
|
505
|
+
return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
|
|
506
|
+
}
|
|
507
|
+
function handleKeyboardShortcutKeyDown(event, handlers) {
|
|
508
|
+
if (isEditableTarget(event.target)) return;
|
|
509
|
+
const loweredKey = event.key.toLowerCase();
|
|
510
|
+
const isMetaPressed = event.metaKey || event.ctrlKey;
|
|
511
|
+
if (isMetaPressed && (loweredKey === "z" || loweredKey === "y")) {
|
|
512
|
+
const shouldRedo = loweredKey === "y" || loweredKey === "z" && event.shiftKey;
|
|
513
|
+
if (handlers.runHistoryShortcut(shouldRedo)) {
|
|
514
|
+
event.preventDefault();
|
|
515
|
+
event.stopPropagation();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (!isMetaPressed && !event.altKey) {
|
|
520
|
+
const nextToolId = TOOL_SHORTCUTS[loweredKey];
|
|
521
|
+
if (nextToolId && handlers.isToolAvailable(nextToolId)) {
|
|
522
|
+
handlers.setToolFromShortcut(nextToolId);
|
|
523
|
+
event.preventDefault();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (event.key === "Delete" || event.key === "Backspace") {
|
|
528
|
+
if (handlers.deleteSelection()) {
|
|
529
|
+
event.preventDefault();
|
|
530
|
+
event.stopPropagation();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
handlers.dispatchKeyDown(event);
|
|
535
|
+
}
|
|
536
|
+
function handleKeyboardShortcutKeyUp(event, handlers) {
|
|
537
|
+
if (isEditableTarget(event.target)) return;
|
|
538
|
+
handlers.dispatchKeyUp(event);
|
|
539
|
+
}
|
|
540
|
+
|
|
298
541
|
// src/persistence/localIndexedDb.ts
|
|
299
542
|
var DATABASE_PREFIX = "tsdraw_v1_";
|
|
300
543
|
var DATABASE_VERSION = 2;
|
|
@@ -418,23 +661,35 @@ function getOrCreateSessionId() {
|
|
|
418
661
|
|
|
419
662
|
// src/canvas/useTsdrawCanvasController.ts
|
|
420
663
|
function toScreenRect(editor, bounds) {
|
|
421
|
-
const
|
|
664
|
+
const topLeft = pageToScreen(editor.viewport, bounds.minX, bounds.minY);
|
|
665
|
+
const topRight = pageToScreen(editor.viewport, bounds.maxX, bounds.minY);
|
|
666
|
+
const bottomLeft = pageToScreen(editor.viewport, bounds.minX, bounds.maxY);
|
|
667
|
+
const bottomRight = pageToScreen(editor.viewport, bounds.maxX, bounds.maxY);
|
|
668
|
+
const minX = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
|
|
669
|
+
const minY = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
|
|
670
|
+
const maxX = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
|
|
671
|
+
const maxY = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
|
|
422
672
|
return {
|
|
423
|
-
left:
|
|
424
|
-
top:
|
|
425
|
-
width: (
|
|
426
|
-
height: (
|
|
673
|
+
left: minX,
|
|
674
|
+
top: minY,
|
|
675
|
+
width: Math.max(0, maxX - minX),
|
|
676
|
+
height: Math.max(0, maxY - minY)
|
|
427
677
|
};
|
|
428
678
|
}
|
|
429
679
|
function resolveDrawColor(colorStyle, theme) {
|
|
430
680
|
return resolveThemeColor(colorStyle, theme);
|
|
431
681
|
}
|
|
682
|
+
var ZOOM_WHEEL_CAP = 10;
|
|
432
683
|
function useTsdrawCanvasController(options = {}) {
|
|
433
684
|
const onMountRef = useRef(options.onMount);
|
|
434
685
|
const containerRef = useRef(null);
|
|
435
686
|
const canvasRef = useRef(null);
|
|
436
687
|
const editorRef = useRef(null);
|
|
437
688
|
const dprRef = useRef(1);
|
|
689
|
+
const penDetectedRef = useRef(false);
|
|
690
|
+
const penModeRef = useRef(false);
|
|
691
|
+
const lastPointerDownWithRef = useRef("mouse");
|
|
692
|
+
const activePointerIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
438
693
|
const lastPointerClientRef = useRef(null);
|
|
439
694
|
const currentToolRef = useRef(options.initialTool ?? "pen");
|
|
440
695
|
const selectedShapeIdsRef = useRef([]);
|
|
@@ -691,9 +946,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
691
946
|
}
|
|
692
947
|
refreshSelectionBounds(editor, nextSelectedShapeIds);
|
|
693
948
|
};
|
|
694
|
-
const applyRemoteDocumentSnapshot = (
|
|
949
|
+
const applyRemoteDocumentSnapshot = (document2) => {
|
|
695
950
|
ignorePersistenceChanges = true;
|
|
696
|
-
editor.loadDocumentSnapshot(
|
|
951
|
+
editor.loadDocumentSnapshot(document2);
|
|
697
952
|
editor.clearRedoHistory();
|
|
698
953
|
reconcileSelectionAfterDocumentLoad();
|
|
699
954
|
render();
|
|
@@ -725,8 +980,95 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
725
980
|
const coalesced = e.getCoalescedEvents?.();
|
|
726
981
|
return coalesced && coalesced.length > 0 ? coalesced : [e];
|
|
727
982
|
};
|
|
983
|
+
const applyDocumentChangeResult = (changed) => {
|
|
984
|
+
if (!changed) return false;
|
|
985
|
+
reconcileSelectionAfterDocumentLoad();
|
|
986
|
+
setSelectionRotationDeg(0);
|
|
987
|
+
render();
|
|
988
|
+
syncHistoryState();
|
|
989
|
+
return true;
|
|
990
|
+
};
|
|
991
|
+
const normalizeWheelDelta = (event) => {
|
|
992
|
+
let deltaX = event.deltaX;
|
|
993
|
+
let deltaY = event.deltaY;
|
|
994
|
+
let deltaZoom = 0;
|
|
995
|
+
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
996
|
+
const clamped = Math.abs(deltaY) > ZOOM_WHEEL_CAP ? ZOOM_WHEEL_CAP * Math.sign(deltaY) : deltaY;
|
|
997
|
+
deltaZoom = -clamped / 100;
|
|
998
|
+
} else if (event.shiftKey && !navigator.userAgent.includes("Mac") && !navigator.userAgent.includes("iPhone") && !navigator.userAgent.includes("iPad")) {
|
|
999
|
+
deltaX = deltaY;
|
|
1000
|
+
deltaY = 0;
|
|
1001
|
+
}
|
|
1002
|
+
return { x: -deltaX, y: -deltaY, z: deltaZoom };
|
|
1003
|
+
};
|
|
1004
|
+
const deleteCurrentSelection = () => {
|
|
1005
|
+
const selectedIds = selectedShapeIdsRef.current;
|
|
1006
|
+
if (selectedIds.length === 0) return false;
|
|
1007
|
+
editor.beginHistoryEntry();
|
|
1008
|
+
editor.deleteShapes(selectedIds);
|
|
1009
|
+
editor.endHistoryEntry();
|
|
1010
|
+
setSelectedShapeIds([]);
|
|
1011
|
+
selectedShapeIdsRef.current = [];
|
|
1012
|
+
setSelectionBounds(null);
|
|
1013
|
+
setSelectionBrush(null);
|
|
1014
|
+
setSelectionRotationDeg(0);
|
|
1015
|
+
render();
|
|
1016
|
+
syncHistoryState();
|
|
1017
|
+
return true;
|
|
1018
|
+
};
|
|
1019
|
+
const cancelActivePointerInteraction = () => {
|
|
1020
|
+
if (!isPointerActiveRef.current) return;
|
|
1021
|
+
isPointerActiveRef.current = false;
|
|
1022
|
+
lastPointerClientRef.current = null;
|
|
1023
|
+
editor.input.pointerUp();
|
|
1024
|
+
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";
|
|
1031
|
+
} else {
|
|
1032
|
+
editor.tools.pointerUp();
|
|
1033
|
+
}
|
|
1034
|
+
editor.endHistoryEntry();
|
|
1035
|
+
render();
|
|
1036
|
+
refreshSelectionBounds(editor);
|
|
1037
|
+
};
|
|
1038
|
+
const touchInteractions = createTouchInteractionController(editor, canvas, {
|
|
1039
|
+
cancelActivePointerInteraction,
|
|
1040
|
+
refreshView: () => {
|
|
1041
|
+
render();
|
|
1042
|
+
refreshSelectionBounds(editor);
|
|
1043
|
+
},
|
|
1044
|
+
runUndo: () => applyDocumentChangeResult(editor.undo()),
|
|
1045
|
+
runRedo: () => applyDocumentChangeResult(editor.redo())
|
|
1046
|
+
});
|
|
1047
|
+
const isDrawingTool = (tool) => tool !== "select" && tool !== "hand";
|
|
1048
|
+
const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
|
|
728
1049
|
const handlePointerDown = (e) => {
|
|
729
1050
|
if (!canvas.contains(e.target)) return;
|
|
1051
|
+
if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1052
|
+
penDetectedRef.current = true;
|
|
1053
|
+
penModeRef.current = true;
|
|
1054
|
+
}
|
|
1055
|
+
lastPointerDownWithRef.current = e.pointerType;
|
|
1056
|
+
activePointerIdsRef.current.add(e.pointerId);
|
|
1057
|
+
const startedCameraGesture = touchInteractions.handlePointerDown(e);
|
|
1058
|
+
if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
if (!canvas.hasPointerCapture(e.pointerId)) {
|
|
1061
|
+
canvas.setPointerCapture(e.pointerId);
|
|
1062
|
+
}
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const allowPointerDown = !penModeRef.current || e.pointerType !== "touch" || !isDrawingTool(currentToolRef.current);
|
|
1066
|
+
if (!allowPointerDown) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (activePointerIdsRef.current.size > 1) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
730
1072
|
isPointerActiveRef.current = true;
|
|
731
1073
|
editor.beginHistoryEntry();
|
|
732
1074
|
canvas.setPointerCapture(e.pointerId);
|
|
@@ -735,7 +1077,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
735
1077
|
const first = sampleEvents(e)[0];
|
|
736
1078
|
const { x, y } = getPagePoint(first);
|
|
737
1079
|
const pressure = first.pressure ?? 0.5;
|
|
738
|
-
const isPen = first.pointerType === "pen" || first.
|
|
1080
|
+
const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
|
|
739
1081
|
if (currentToolRef.current === "select") {
|
|
740
1082
|
const hit = getTopShapeAtPoint(editor, { x, y });
|
|
741
1083
|
const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
|
|
@@ -775,6 +1117,16 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
775
1117
|
refreshSelectionBounds(editor);
|
|
776
1118
|
};
|
|
777
1119
|
const handlePointerMove = (e) => {
|
|
1120
|
+
if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1121
|
+
penDetectedRef.current = true;
|
|
1122
|
+
penModeRef.current = true;
|
|
1123
|
+
}
|
|
1124
|
+
if (touchInteractions.handlePointerMove(e)) {
|
|
1125
|
+
e.preventDefault();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (penModeRef.current && e.pointerType === "touch" && isDrawingTool(currentToolRef.current) && !isPointerActiveRef.current) return;
|
|
1129
|
+
if (activePointerIdsRef.current.size > 1) return;
|
|
778
1130
|
updatePointerPreview(e.clientX, e.clientY);
|
|
779
1131
|
const prevClient = lastPointerClientRef.current;
|
|
780
1132
|
const dx = prevClient ? e.clientX - prevClient.x : 0;
|
|
@@ -783,7 +1135,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
783
1135
|
for (const sample of sampleEvents(e)) {
|
|
784
1136
|
const { x, y } = getPagePoint(sample);
|
|
785
1137
|
const pressure = sample.pressure ?? 0.5;
|
|
786
|
-
const isPen = sample.pointerType === "pen" || sample.
|
|
1138
|
+
const isPen = sample.pointerType === "pen" || hasRealPressure(sample.pressure);
|
|
787
1139
|
editor.input.pointerMove(x, y, pressure, isPen);
|
|
788
1140
|
}
|
|
789
1141
|
if (currentToolRef.current === "select") {
|
|
@@ -833,6 +1185,13 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
833
1185
|
refreshSelectionBounds(editor);
|
|
834
1186
|
};
|
|
835
1187
|
const handlePointerUp = (e) => {
|
|
1188
|
+
activePointerIdsRef.current.delete(e.pointerId);
|
|
1189
|
+
const hadTouchCameraGesture = touchInteractions.handlePointerUpOrCancel(e);
|
|
1190
|
+
if (hadTouchCameraGesture || touchInteractions.isCameraGestureActive()) {
|
|
1191
|
+
e.preventDefault();
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (!isPointerActiveRef.current) return;
|
|
836
1195
|
isPointerActiveRef.current = false;
|
|
837
1196
|
lastPointerClientRef.current = null;
|
|
838
1197
|
updatePointerPreview(e.clientX, e.clientY);
|
|
@@ -916,7 +1275,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
916
1275
|
}
|
|
917
1276
|
editor.endHistoryEntry();
|
|
918
1277
|
};
|
|
919
|
-
const handlePointerCancel = () => {
|
|
1278
|
+
const handlePointerCancel = (e) => {
|
|
1279
|
+
activePointerIdsRef.current.delete(e.pointerId);
|
|
1280
|
+
const hadTouchCameraGesture = touchInteractions.handlePointerUpOrCancel(e);
|
|
1281
|
+
if (hadTouchCameraGesture || touchInteractions.isCameraGestureActive()) return;
|
|
920
1282
|
if (!isPointerActiveRef.current) return;
|
|
921
1283
|
isPointerActiveRef.current = false;
|
|
922
1284
|
lastPointerClientRef.current = null;
|
|
@@ -945,31 +1307,58 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
945
1307
|
applyRemoteDocumentSnapshot(pending);
|
|
946
1308
|
}
|
|
947
1309
|
};
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
render();
|
|
961
|
-
syncHistoryState();
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
1310
|
+
const handleWheel = (e) => {
|
|
1311
|
+
if (!container.contains(e.target)) return;
|
|
1312
|
+
e.preventDefault();
|
|
1313
|
+
if (touchInteractions.isTrackpadZoomActive()) return;
|
|
1314
|
+
const delta = normalizeWheelDelta(e);
|
|
1315
|
+
if (delta.z !== 0) {
|
|
1316
|
+
const rect = canvas.getBoundingClientRect();
|
|
1317
|
+
const pointX = e.clientX - rect.left;
|
|
1318
|
+
const pointY = e.clientY - rect.top;
|
|
1319
|
+
editor.zoomAt(Math.exp(delta.z), pointX, pointY);
|
|
1320
|
+
} else {
|
|
1321
|
+
editor.panBy(delta.x, delta.y);
|
|
964
1322
|
}
|
|
965
|
-
editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
|
|
966
|
-
editor.tools.keyDown({ key: e.key });
|
|
967
1323
|
render();
|
|
1324
|
+
refreshSelectionBounds(editor);
|
|
1325
|
+
};
|
|
1326
|
+
const handleGestureEvent = (e) => {
|
|
1327
|
+
touchInteractions.handleGestureEvent(e, container);
|
|
1328
|
+
};
|
|
1329
|
+
const handleKeyDown = (e) => {
|
|
1330
|
+
handleKeyboardShortcutKeyDown(e, {
|
|
1331
|
+
isToolAvailable: (tool) => editor.tools.hasTool(tool),
|
|
1332
|
+
setToolFromShortcut: (tool) => {
|
|
1333
|
+
editor.setCurrentTool(tool);
|
|
1334
|
+
setCurrentToolState(tool);
|
|
1335
|
+
currentToolRef.current = tool;
|
|
1336
|
+
if (tool !== "select") resetSelectUi();
|
|
1337
|
+
render();
|
|
1338
|
+
},
|
|
1339
|
+
runHistoryShortcut: (shouldRedo) => applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo()),
|
|
1340
|
+
deleteSelection: () => currentToolRef.current === "select" ? deleteCurrentSelection() : false,
|
|
1341
|
+
dispatchKeyDown: (event) => {
|
|
1342
|
+
editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
|
|
1343
|
+
editor.tools.keyDown({ key: event.key });
|
|
1344
|
+
render();
|
|
1345
|
+
},
|
|
1346
|
+
dispatchKeyUp: () => void 0
|
|
1347
|
+
});
|
|
968
1348
|
};
|
|
969
1349
|
const handleKeyUp = (e) => {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1350
|
+
handleKeyboardShortcutKeyUp(e, {
|
|
1351
|
+
isToolAvailable: () => false,
|
|
1352
|
+
setToolFromShortcut: () => void 0,
|
|
1353
|
+
runHistoryShortcut: () => false,
|
|
1354
|
+
deleteSelection: () => false,
|
|
1355
|
+
dispatchKeyDown: () => void 0,
|
|
1356
|
+
dispatchKeyUp: (event) => {
|
|
1357
|
+
editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
|
|
1358
|
+
editor.tools.keyUp({ key: event.key });
|
|
1359
|
+
render();
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
973
1362
|
};
|
|
974
1363
|
const initializePersistence = async () => {
|
|
975
1364
|
if (!persistenceKey) {
|
|
@@ -1046,6 +1435,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1046
1435
|
const ro = new ResizeObserver(resize);
|
|
1047
1436
|
ro.observe(container);
|
|
1048
1437
|
canvas.addEventListener("pointerdown", handlePointerDown);
|
|
1438
|
+
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
1439
|
+
document.addEventListener("gesturestart", handleGestureEvent);
|
|
1440
|
+
document.addEventListener("gesturechange", handleGestureEvent);
|
|
1441
|
+
document.addEventListener("gestureend", handleGestureEvent);
|
|
1049
1442
|
window.addEventListener("pointermove", handlePointerMove);
|
|
1050
1443
|
window.addEventListener("pointerup", handlePointerUp);
|
|
1051
1444
|
window.addEventListener("pointercancel", handlePointerCancel);
|
|
@@ -1102,13 +1495,19 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1102
1495
|
disposeMount?.();
|
|
1103
1496
|
ro.disconnect();
|
|
1104
1497
|
canvas.removeEventListener("pointerdown", handlePointerDown);
|
|
1498
|
+
container.removeEventListener("wheel", handleWheel);
|
|
1499
|
+
document.removeEventListener("gesturestart", handleGestureEvent);
|
|
1500
|
+
document.removeEventListener("gesturechange", handleGestureEvent);
|
|
1501
|
+
document.removeEventListener("gestureend", handleGestureEvent);
|
|
1105
1502
|
window.removeEventListener("pointermove", handlePointerMove);
|
|
1106
1503
|
window.removeEventListener("pointerup", handlePointerUp);
|
|
1107
1504
|
window.removeEventListener("pointercancel", handlePointerCancel);
|
|
1108
1505
|
window.removeEventListener("keydown", handleKeyDown);
|
|
1109
1506
|
window.removeEventListener("keyup", handleKeyUp);
|
|
1110
1507
|
isPointerActiveRef.current = false;
|
|
1508
|
+
activePointerIdsRef.current.clear();
|
|
1111
1509
|
pendingRemoteDocumentRef.current = null;
|
|
1510
|
+
touchInteractions.reset();
|
|
1112
1511
|
persistenceChannel?.close();
|
|
1113
1512
|
void persistenceDb?.close();
|
|
1114
1513
|
editorRef.current = null;
|
|
@@ -1119,6 +1518,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1119
1518
|
options.persistenceKey,
|
|
1120
1519
|
options.toolDefinitions,
|
|
1121
1520
|
refreshSelectionBounds,
|
|
1521
|
+
resetSelectUi,
|
|
1122
1522
|
render,
|
|
1123
1523
|
updatePointerPreview
|
|
1124
1524
|
]);
|