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