@tsdraw/react 0.8.2 → 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 +152 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +153 -67
- package/dist/index.js.map +1 -1
- package/dist/tsdraw.css +46 -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, 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
|
|
@@ -290,12 +290,11 @@ function getCanvasCursor(currentTool, state) {
|
|
|
290
290
|
if (state.isRotatingSelection) return "grabbing";
|
|
291
291
|
if (state.isResizingSelection) return "nwse-resize";
|
|
292
292
|
if (state.isMovingSelection) return "grabbing";
|
|
293
|
+
if (state.isHoveringSelectionBounds) return "move";
|
|
293
294
|
return "default";
|
|
294
295
|
}
|
|
295
296
|
return state.showToolOverlay ? "none" : "crosshair";
|
|
296
297
|
}
|
|
297
|
-
|
|
298
|
-
// src/canvas/touchInteractions.ts
|
|
299
298
|
var TAP_MAX_DURATION_MS = 100;
|
|
300
299
|
var DOUBLE_TAP_INTERVAL_MS = 100;
|
|
301
300
|
var TAP_MOVE_TOLERANCE = 14;
|
|
@@ -317,17 +316,28 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
317
316
|
mode: "not-sure",
|
|
318
317
|
previousCenter: { x: 0, y: 0 },
|
|
319
318
|
initialCenter: { x: 0, y: 0 },
|
|
320
|
-
previousDistance: 1,
|
|
321
319
|
initialDistance: 1,
|
|
322
|
-
|
|
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;
|
|
330
|
-
touchCameraState.
|
|
340
|
+
touchCameraState.initialZoom = 1;
|
|
331
341
|
};
|
|
332
342
|
const maybeHandleTouchTapGesture = () => {
|
|
333
343
|
if (activeTouchPoints.size > 0) return;
|
|
@@ -358,14 +368,12 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
358
368
|
const second = points[1];
|
|
359
369
|
const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
|
360
370
|
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
371
|
touchCameraState.active = true;
|
|
363
372
|
touchCameraState.mode = "not-sure";
|
|
364
373
|
touchCameraState.previousCenter = center;
|
|
365
374
|
touchCameraState.initialCenter = center;
|
|
366
|
-
touchCameraState.previousDistance = Math.max(1, distance);
|
|
367
375
|
touchCameraState.initialDistance = Math.max(1, distance);
|
|
368
|
-
touchCameraState.
|
|
376
|
+
touchCameraState.initialZoom = editor.getZoomLevel();
|
|
369
377
|
};
|
|
370
378
|
const updateTouchCameraGesture = () => {
|
|
371
379
|
if (!touchCameraState.active) return false;
|
|
@@ -378,7 +386,6 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
378
386
|
const second = points[1];
|
|
379
387
|
const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
|
380
388
|
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
389
|
const centerDx = center.x - touchCameraState.previousCenter.x;
|
|
383
390
|
const centerDy = center.y - touchCameraState.previousCenter.y;
|
|
384
391
|
const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
|
|
@@ -390,20 +397,27 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
390
397
|
const canvasRect = canvas.getBoundingClientRect();
|
|
391
398
|
const centerOnCanvasX = center.x - canvasRect.left;
|
|
392
399
|
const centerOnCanvasY = center.y - canvasRect.top;
|
|
393
|
-
editor.panBy(centerDx, centerDy);
|
|
394
400
|
if (touchCameraState.mode === "zooming") {
|
|
395
|
-
const
|
|
396
|
-
editor.
|
|
397
|
-
|
|
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);
|
|
398
413
|
}
|
|
399
414
|
touchCameraState.previousCenter = center;
|
|
400
|
-
touchCameraState.previousDistance = distance;
|
|
401
|
-
touchCameraState.previousAngle = angle;
|
|
402
415
|
handlers.refreshView();
|
|
403
416
|
return true;
|
|
404
417
|
};
|
|
405
418
|
const handlePointerDown = (event) => {
|
|
406
419
|
if (!isTouchPointer(event)) return false;
|
|
420
|
+
stopFingerPanSlide();
|
|
407
421
|
activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
408
422
|
if (!touchTapState.active) {
|
|
409
423
|
touchTapState.active = true;
|
|
@@ -416,9 +430,16 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
416
430
|
}
|
|
417
431
|
touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
418
432
|
if (activeTouchPoints.size === 2) {
|
|
433
|
+
endFingerPan();
|
|
419
434
|
beginTouchCameraGesture();
|
|
420
435
|
return true;
|
|
421
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
|
+
}
|
|
422
443
|
return false;
|
|
423
444
|
};
|
|
424
445
|
const handlePointerMove = (event) => {
|
|
@@ -429,16 +450,34 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
429
450
|
const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
|
|
430
451
|
if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
|
|
431
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
|
+
}
|
|
432
459
|
return updateTouchCameraGesture();
|
|
433
460
|
};
|
|
434
461
|
const handlePointerUpOrCancel = (event) => {
|
|
435
462
|
if (!isTouchPointer(event)) return false;
|
|
436
463
|
const wasCameraGestureActive = touchCameraState.active;
|
|
464
|
+
const wasFingerPan = fingerPanPointerId === event.pointerId;
|
|
465
|
+
const releasedPanSession = wasFingerPan ? fingerPanSession : null;
|
|
437
466
|
activeTouchPoints.delete(event.pointerId);
|
|
438
467
|
touchTapState.startPoints.delete(event.pointerId);
|
|
439
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
|
+
}
|
|
440
479
|
maybeHandleTouchTapGesture();
|
|
441
|
-
return wasCameraGestureActive;
|
|
480
|
+
return wasCameraGestureActive || wasFingerPan;
|
|
442
481
|
};
|
|
443
482
|
let gestureLastScale = 1;
|
|
444
483
|
let gestureActive = false;
|
|
@@ -472,6 +511,8 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
472
511
|
touchTapState.active = false;
|
|
473
512
|
touchTapState.startPoints.clear();
|
|
474
513
|
endTouchCameraGesture();
|
|
514
|
+
endFingerPan();
|
|
515
|
+
stopFingerPanSlide();
|
|
475
516
|
};
|
|
476
517
|
return {
|
|
477
518
|
handlePointerDown,
|
|
@@ -480,6 +521,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
480
521
|
handleGestureEvent,
|
|
481
522
|
reset,
|
|
482
523
|
isCameraGestureActive: () => touchCameraState.active,
|
|
524
|
+
isFingerPanActive: () => fingerPanPointerId !== null,
|
|
483
525
|
isTrackpadZoomActive: () => gestureActive
|
|
484
526
|
};
|
|
485
527
|
}
|
|
@@ -679,6 +721,18 @@ function toScreenRect(editor, bounds) {
|
|
|
679
721
|
function resolveDrawColor(colorStyle, theme) {
|
|
680
722
|
return resolveThemeColor(colorStyle, theme);
|
|
681
723
|
}
|
|
724
|
+
function getHandlePagePoint(bounds, handle) {
|
|
725
|
+
switch (handle) {
|
|
726
|
+
case "nw":
|
|
727
|
+
return { x: bounds.minX, y: bounds.minY };
|
|
728
|
+
case "ne":
|
|
729
|
+
return { x: bounds.maxX, y: bounds.minY };
|
|
730
|
+
case "sw":
|
|
731
|
+
return { x: bounds.minX, y: bounds.maxY };
|
|
732
|
+
case "se":
|
|
733
|
+
return { x: bounds.maxX, y: bounds.maxY };
|
|
734
|
+
}
|
|
735
|
+
}
|
|
682
736
|
var ZOOM_WHEEL_CAP = 10;
|
|
683
737
|
function useTsdrawCanvasController(options = {}) {
|
|
684
738
|
const onMountRef = useRef(options.onMount);
|
|
@@ -696,11 +750,13 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
696
750
|
const schedulePersistRef = useRef(null);
|
|
697
751
|
const isPointerActiveRef = useRef(false);
|
|
698
752
|
const pendingRemoteDocumentRef = useRef(null);
|
|
753
|
+
const activeCameraSlideRef = useRef(null);
|
|
699
754
|
const selectionRotationRef = useRef(0);
|
|
700
755
|
const resizeRef = useRef({
|
|
701
756
|
handle: null,
|
|
702
757
|
startBounds: null,
|
|
703
|
-
startShapes: /* @__PURE__ */ new Map()
|
|
758
|
+
startShapes: /* @__PURE__ */ new Map(),
|
|
759
|
+
cursorHandleOffset: { x: 0, y: 0 }
|
|
704
760
|
});
|
|
705
761
|
const rotateRef = useRef({
|
|
706
762
|
center: null,
|
|
@@ -787,33 +843,38 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
787
843
|
setIsResizingSelection(false);
|
|
788
844
|
setIsRotatingSelection(false);
|
|
789
845
|
selectDragRef.current.mode = "none";
|
|
790
|
-
resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
|
|
791
|
-
rotateRef.current = {
|
|
792
|
-
center: null,
|
|
793
|
-
startAngle: 0,
|
|
794
|
-
startSelectionRotationDeg: selectionRotationRef.current,
|
|
795
|
-
startShapes: /* @__PURE__ */ new Map()
|
|
796
|
-
};
|
|
846
|
+
resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map(), cursorHandleOffset: { x: 0, y: 0 } };
|
|
847
|
+
rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map() };
|
|
797
848
|
}, []);
|
|
798
849
|
const handleResizePointerDown = useCallback(
|
|
799
850
|
(e, handle) => {
|
|
800
851
|
e.preventDefault();
|
|
801
852
|
e.stopPropagation();
|
|
802
853
|
const editor = editorRef.current;
|
|
803
|
-
|
|
854
|
+
const canvas = canvasRef.current;
|
|
855
|
+
if (!editor || !canvas || selectedShapeIdsRef.current.length === 0) return;
|
|
804
856
|
const bounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
|
|
805
857
|
if (!bounds) return;
|
|
858
|
+
const handlePagePoint = getHandlePagePoint(bounds, handle);
|
|
859
|
+
const pointerPage = getPagePointFromClient(editor, e.clientX, e.clientY);
|
|
860
|
+
const cursorOffset = {
|
|
861
|
+
x: pointerPage.x - handlePagePoint.x,
|
|
862
|
+
y: pointerPage.y - handlePagePoint.y
|
|
863
|
+
};
|
|
806
864
|
resizeRef.current = {
|
|
807
865
|
handle,
|
|
808
866
|
startBounds: bounds,
|
|
809
|
-
startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current)
|
|
867
|
+
startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current),
|
|
868
|
+
cursorHandleOffset: cursorOffset
|
|
810
869
|
};
|
|
870
|
+
isPointerActiveRef.current = true;
|
|
871
|
+
activePointerIdsRef.current.add(e.pointerId);
|
|
872
|
+
canvas.setPointerCapture(e.pointerId);
|
|
811
873
|
editor.beginHistoryEntry();
|
|
812
874
|
selectDragRef.current.mode = "resize";
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
selectDragRef.current.
|
|
816
|
-
selectDragRef.current.currentPage = p;
|
|
875
|
+
editor.input.pointerDown(handlePagePoint.x, handlePagePoint.y, 0.5, false);
|
|
876
|
+
selectDragRef.current.startPage = handlePagePoint;
|
|
877
|
+
selectDragRef.current.currentPage = handlePagePoint;
|
|
817
878
|
lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
|
|
818
879
|
setIsResizingSelection(true);
|
|
819
880
|
},
|
|
@@ -824,7 +885,8 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
824
885
|
e.preventDefault();
|
|
825
886
|
e.stopPropagation();
|
|
826
887
|
const editor = editorRef.current;
|
|
827
|
-
|
|
888
|
+
const canvas = canvasRef.current;
|
|
889
|
+
if (!editor || !canvas || selectedShapeIdsRef.current.length === 0) return;
|
|
828
890
|
const bounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
|
|
829
891
|
if (!bounds) return;
|
|
830
892
|
const center = {
|
|
@@ -838,6 +900,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
838
900
|
startSelectionRotationDeg: selectionRotationRef.current,
|
|
839
901
|
startShapes: buildTransformSnapshots(editor, selectedShapeIdsRef.current)
|
|
840
902
|
};
|
|
903
|
+
isPointerActiveRef.current = true;
|
|
904
|
+
activePointerIdsRef.current.add(e.pointerId);
|
|
905
|
+
canvas.setPointerCapture(e.pointerId);
|
|
841
906
|
editor.beginHistoryEntry();
|
|
842
907
|
selectDragRef.current.mode = "rotate";
|
|
843
908
|
editor.input.pointerDown(p.x, p.y, 0.5, false);
|
|
@@ -967,7 +1032,6 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
967
1032
|
setDrawDash(nextDrawStyle.dash);
|
|
968
1033
|
setDrawFill(nextDrawStyle.fill);
|
|
969
1034
|
setDrawSize(nextDrawStyle.size);
|
|
970
|
-
setSelectionRotationDeg(0);
|
|
971
1035
|
render();
|
|
972
1036
|
refreshSelectionBounds(editor, nextSelectionIds);
|
|
973
1037
|
ignorePersistenceChanges = false;
|
|
@@ -983,7 +1047,6 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
983
1047
|
const applyDocumentChangeResult = (changed) => {
|
|
984
1048
|
if (!changed) return false;
|
|
985
1049
|
reconcileSelectionAfterDocumentLoad();
|
|
986
|
-
setSelectionRotationDeg(0);
|
|
987
1050
|
render();
|
|
988
1051
|
syncHistoryState();
|
|
989
1052
|
return true;
|
|
@@ -1010,8 +1073,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1010
1073
|
setSelectedShapeIds([]);
|
|
1011
1074
|
selectedShapeIdsRef.current = [];
|
|
1012
1075
|
setSelectionBounds(null);
|
|
1013
|
-
|
|
1014
|
-
setSelectionRotationDeg(0);
|
|
1076
|
+
resetSelectUi();
|
|
1015
1077
|
render();
|
|
1016
1078
|
syncHistoryState();
|
|
1017
1079
|
return true;
|
|
@@ -1022,12 +1084,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1022
1084
|
lastPointerClientRef.current = null;
|
|
1023
1085
|
editor.input.pointerUp();
|
|
1024
1086
|
if (currentToolRef.current === "select") {
|
|
1025
|
-
|
|
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";
|
|
1087
|
+
resetSelectUi();
|
|
1031
1088
|
} else {
|
|
1032
1089
|
editor.tools.pointerUp();
|
|
1033
1090
|
}
|
|
@@ -1042,12 +1099,19 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1042
1099
|
refreshSelectionBounds(editor);
|
|
1043
1100
|
},
|
|
1044
1101
|
runUndo: () => applyDocumentChangeResult(editor.undo()),
|
|
1045
|
-
runRedo: () => applyDocumentChangeResult(editor.redo())
|
|
1102
|
+
runRedo: () => applyDocumentChangeResult(editor.redo()),
|
|
1103
|
+
isPenModeActive: () => penModeRef.current
|
|
1046
1104
|
});
|
|
1047
|
-
const isDrawingTool = (tool) => tool !== "select" && tool !== "hand";
|
|
1048
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
|
+
};
|
|
1049
1112
|
const handlePointerDown = (e) => {
|
|
1050
1113
|
if (!canvas.contains(e.target)) return;
|
|
1114
|
+
stopActiveSlide();
|
|
1051
1115
|
if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1052
1116
|
penDetectedRef.current = true;
|
|
1053
1117
|
penModeRef.current = true;
|
|
@@ -1055,15 +1119,15 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1055
1119
|
lastPointerDownWithRef.current = e.pointerType;
|
|
1056
1120
|
activePointerIdsRef.current.add(e.pointerId);
|
|
1057
1121
|
const startedCameraGesture = touchInteractions.handlePointerDown(e);
|
|
1058
|
-
if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
|
|
1122
|
+
if (startedCameraGesture || touchInteractions.isCameraGestureActive() || touchInteractions.isFingerPanActive()) {
|
|
1059
1123
|
e.preventDefault();
|
|
1060
1124
|
if (!canvas.hasPointerCapture(e.pointerId)) {
|
|
1061
1125
|
canvas.setPointerCapture(e.pointerId);
|
|
1062
1126
|
}
|
|
1063
1127
|
return;
|
|
1064
1128
|
}
|
|
1065
|
-
const
|
|
1066
|
-
if (
|
|
1129
|
+
const isTouchBlockedByPenMode = penModeRef.current && e.pointerType === "touch";
|
|
1130
|
+
if (isTouchBlockedByPenMode) {
|
|
1067
1131
|
return;
|
|
1068
1132
|
}
|
|
1069
1133
|
if (activePointerIdsRef.current.size > 1) {
|
|
@@ -1081,7 +1145,13 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1081
1145
|
if (currentToolRef.current === "select") {
|
|
1082
1146
|
const hit = getTopShapeAtPoint(editor, { x, y });
|
|
1083
1147
|
const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
|
|
1084
|
-
|
|
1148
|
+
const isInsideSelectionBounds = (() => {
|
|
1149
|
+
if (selectedShapeIdsRef.current.length === 0) return false;
|
|
1150
|
+
const pageBounds = getSelectionBoundsPage(editor, selectedShapeIdsRef.current);
|
|
1151
|
+
if (!pageBounds) return false;
|
|
1152
|
+
return x >= pageBounds.minX && x <= pageBounds.maxX && y >= pageBounds.minY && y <= pageBounds.maxY;
|
|
1153
|
+
})();
|
|
1154
|
+
if (isHitSelected || isInsideSelectionBounds) {
|
|
1085
1155
|
selectDragRef.current = {
|
|
1086
1156
|
mode: "move",
|
|
1087
1157
|
startPage: { x, y },
|
|
@@ -1112,7 +1182,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1112
1182
|
}
|
|
1113
1183
|
editor.input.pointerDown(x, y, pressure, isPen);
|
|
1114
1184
|
editor.input.setModifiers(first.shiftKey, first.ctrlKey, first.metaKey);
|
|
1115
|
-
editor.tools.pointerDown({ point: { x, y, z: pressure } });
|
|
1185
|
+
editor.tools.pointerDown({ point: { x, y, z: pressure }, screenX: e.clientX, screenY: e.clientY });
|
|
1116
1186
|
render();
|
|
1117
1187
|
refreshSelectionBounds(editor);
|
|
1118
1188
|
};
|
|
@@ -1125,7 +1195,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1125
1195
|
e.preventDefault();
|
|
1126
1196
|
return;
|
|
1127
1197
|
}
|
|
1128
|
-
if (penModeRef.current && e.pointerType === "touch" &&
|
|
1198
|
+
if (penModeRef.current && e.pointerType === "touch" && !isPointerActiveRef.current) return;
|
|
1129
1199
|
if (activePointerIdsRef.current.size > 1) return;
|
|
1130
1200
|
updatePointerPreview(e.clientX, e.clientY);
|
|
1131
1201
|
const prevClient = lastPointerClientRef.current;
|
|
@@ -1152,9 +1222,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1152
1222
|
return;
|
|
1153
1223
|
}
|
|
1154
1224
|
if (mode === "resize") {
|
|
1155
|
-
const { handle, startBounds, startShapes } = resizeRef.current;
|
|
1225
|
+
const { handle, startBounds, startShapes, cursorHandleOffset } = resizeRef.current;
|
|
1156
1226
|
if (!handle || !startBounds) return;
|
|
1157
|
-
applyResize(editor, handle, startBounds, startShapes, { x: px, y: py }, e.shiftKey);
|
|
1227
|
+
applyResize(editor, handle, startBounds, startShapes, { x: px - cursorHandleOffset.x, y: py - cursorHandleOffset.y }, e.shiftKey);
|
|
1158
1228
|
render();
|
|
1159
1229
|
refreshSelectionBounds(editor);
|
|
1160
1230
|
return;
|
|
@@ -1174,13 +1244,12 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1174
1244
|
const nextIds = selectDragRef.current.additive ? Array.from(/* @__PURE__ */ new Set([...selectDragRef.current.initialSelection, ...ids])) : ids;
|
|
1175
1245
|
setSelectedShapeIds(nextIds);
|
|
1176
1246
|
selectedShapeIdsRef.current = nextIds;
|
|
1177
|
-
setSelectionRotationDeg(0);
|
|
1178
1247
|
refreshSelectionBounds(editor, nextIds);
|
|
1179
1248
|
return;
|
|
1180
1249
|
}
|
|
1181
1250
|
}
|
|
1182
1251
|
editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
|
|
1183
|
-
editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy });
|
|
1252
|
+
editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy, screenX: e.clientX, screenY: e.clientY });
|
|
1184
1253
|
render();
|
|
1185
1254
|
refreshSelectionBounds(editor);
|
|
1186
1255
|
};
|
|
@@ -1204,12 +1273,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1204
1273
|
setIsRotatingSelection(false);
|
|
1205
1274
|
selectDragRef.current.mode = "none";
|
|
1206
1275
|
setSelectionRotationDeg(0);
|
|
1207
|
-
rotateRef.current = {
|
|
1208
|
-
center: null,
|
|
1209
|
-
startAngle: 0,
|
|
1210
|
-
startSelectionRotationDeg: selectionRotationRef.current,
|
|
1211
|
-
startShapes: /* @__PURE__ */ new Map()
|
|
1212
|
-
};
|
|
1276
|
+
rotateRef.current = { center: null, startAngle: 0, startSelectionRotationDeg: 0, startShapes: /* @__PURE__ */ new Map() };
|
|
1213
1277
|
render();
|
|
1214
1278
|
refreshSelectionBounds(editor);
|
|
1215
1279
|
editor.endHistoryEntry();
|
|
@@ -1218,7 +1282,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1218
1282
|
if (drag.mode === "resize") {
|
|
1219
1283
|
setIsResizingSelection(false);
|
|
1220
1284
|
selectDragRef.current.mode = "none";
|
|
1221
|
-
resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
|
|
1285
|
+
resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map(), cursorHandleOffset: { x: 0, y: 0 } };
|
|
1222
1286
|
render();
|
|
1223
1287
|
refreshSelectionBounds(editor);
|
|
1224
1288
|
editor.endHistoryEntry();
|
|
@@ -1251,7 +1315,6 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1251
1315
|
}
|
|
1252
1316
|
setSelectedShapeIds(ids);
|
|
1253
1317
|
selectedShapeIdsRef.current = ids;
|
|
1254
|
-
setSelectionRotationDeg(0);
|
|
1255
1318
|
setSelectionBrush(null);
|
|
1256
1319
|
selectDragRef.current.mode = "none";
|
|
1257
1320
|
render();
|
|
@@ -1265,9 +1328,26 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1265
1328
|
return;
|
|
1266
1329
|
}
|
|
1267
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
|
+
}
|
|
1268
1338
|
editor.tools.pointerUp();
|
|
1269
1339
|
render();
|
|
1270
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
|
+
}
|
|
1271
1351
|
if (pendingRemoteDocumentRef.current) {
|
|
1272
1352
|
const pendingRemoteDocument = pendingRemoteDocumentRef.current;
|
|
1273
1353
|
pendingRemoteDocumentRef.current = null;
|
|
@@ -1434,11 +1514,17 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1434
1514
|
resize();
|
|
1435
1515
|
const ro = new ResizeObserver(resize);
|
|
1436
1516
|
ro.observe(container);
|
|
1517
|
+
const handlePointerLeaveViewport = (e) => {
|
|
1518
|
+
if (e.relatedTarget === null) {
|
|
1519
|
+
setIsPointerInsideCanvas(false);
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1437
1522
|
canvas.addEventListener("pointerdown", handlePointerDown);
|
|
1438
1523
|
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
1439
1524
|
document.addEventListener("gesturestart", handleGestureEvent);
|
|
1440
1525
|
document.addEventListener("gesturechange", handleGestureEvent);
|
|
1441
1526
|
document.addEventListener("gestureend", handleGestureEvent);
|
|
1527
|
+
document.documentElement.addEventListener("pointerleave", handlePointerLeaveViewport);
|
|
1442
1528
|
window.addEventListener("pointermove", handlePointerMove);
|
|
1443
1529
|
window.addEventListener("pointerup", handlePointerUp);
|
|
1444
1530
|
window.addEventListener("pointercancel", handlePointerCancel);
|
|
@@ -1462,7 +1548,6 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1462
1548
|
const changed = editor.undo();
|
|
1463
1549
|
if (!changed) return false;
|
|
1464
1550
|
reconcileSelectionAfterDocumentLoad();
|
|
1465
|
-
setSelectionRotationDeg(0);
|
|
1466
1551
|
render();
|
|
1467
1552
|
syncHistoryState();
|
|
1468
1553
|
return true;
|
|
@@ -1471,7 +1556,6 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1471
1556
|
const changed = editor.redo();
|
|
1472
1557
|
if (!changed) return false;
|
|
1473
1558
|
reconcileSelectionAfterDocumentLoad();
|
|
1474
|
-
setSelectionRotationDeg(0);
|
|
1475
1559
|
render();
|
|
1476
1560
|
syncHistoryState();
|
|
1477
1561
|
return true;
|
|
@@ -1499,6 +1583,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1499
1583
|
document.removeEventListener("gesturestart", handleGestureEvent);
|
|
1500
1584
|
document.removeEventListener("gesturechange", handleGestureEvent);
|
|
1501
1585
|
document.removeEventListener("gestureend", handleGestureEvent);
|
|
1586
|
+
document.documentElement.removeEventListener("pointerleave", handlePointerLeaveViewport);
|
|
1502
1587
|
window.removeEventListener("pointermove", handlePointerMove);
|
|
1503
1588
|
window.removeEventListener("pointerup", handlePointerUp);
|
|
1504
1589
|
window.removeEventListener("pointercancel", handlePointerCancel);
|
|
@@ -1507,6 +1592,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1507
1592
|
isPointerActiveRef.current = false;
|
|
1508
1593
|
activePointerIdsRef.current.clear();
|
|
1509
1594
|
pendingRemoteDocumentRef.current = null;
|
|
1595
|
+
stopActiveSlide();
|
|
1510
1596
|
touchInteractions.reset();
|
|
1511
1597
|
persistenceChannel?.close();
|
|
1512
1598
|
void persistenceDb?.close();
|
|
@@ -1563,7 +1649,6 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1563
1649
|
selectedShapeIdsRef.current = nextSelectedShapeIds;
|
|
1564
1650
|
setSelectedShapeIds(nextSelectedShapeIds);
|
|
1565
1651
|
}
|
|
1566
|
-
setSelectionRotationDeg(0);
|
|
1567
1652
|
render();
|
|
1568
1653
|
setCanUndo(editor.canUndo());
|
|
1569
1654
|
setCanRedo(editor.canRedo());
|
|
@@ -1579,17 +1664,18 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1579
1664
|
selectedShapeIdsRef.current = nextSelectedShapeIds;
|
|
1580
1665
|
setSelectedShapeIds(nextSelectedShapeIds);
|
|
1581
1666
|
}
|
|
1582
|
-
setSelectionRotationDeg(0);
|
|
1583
1667
|
render();
|
|
1584
1668
|
setCanUndo(editor.canUndo());
|
|
1585
1669
|
setCanRedo(editor.canRedo());
|
|
1586
1670
|
return true;
|
|
1587
1671
|
}, [render]);
|
|
1672
|
+
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;
|
|
1588
1673
|
const showToolOverlay = isPointerInsideCanvas && (currentTool === "pen" || currentTool === "eraser");
|
|
1589
1674
|
const canvasCursor = getCanvasCursor(currentTool, {
|
|
1590
1675
|
isMovingSelection,
|
|
1591
1676
|
isResizingSelection,
|
|
1592
1677
|
isRotatingSelection,
|
|
1678
|
+
isHoveringSelectionBounds,
|
|
1593
1679
|
showToolOverlay
|
|
1594
1680
|
});
|
|
1595
1681
|
const cursorContext = {
|