@tsdraw/react 0.8.3 → 0.8.5
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 +224 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +225 -41
- package/dist/index.js.map +1 -1
- 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
|
|
@@ -295,15 +295,17 @@ function getCanvasCursor(currentTool, state) {
|
|
|
295
295
|
}
|
|
296
296
|
return state.showToolOverlay ? "none" : "crosshair";
|
|
297
297
|
}
|
|
298
|
-
|
|
299
|
-
// src/canvas/touchInteractions.ts
|
|
300
298
|
var TAP_MAX_DURATION_MS = 100;
|
|
301
299
|
var DOUBLE_TAP_INTERVAL_MS = 100;
|
|
302
300
|
var TAP_MOVE_TOLERANCE = 14;
|
|
303
301
|
var PINCH_MODE_ZOOM_DISTANCE = 24;
|
|
304
302
|
var PINCH_MODE_PAN_DISTANCE = 16;
|
|
305
303
|
var PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE = 64;
|
|
306
|
-
function createTouchInteractionController(editor, canvas, handlers) {
|
|
304
|
+
function createTouchInteractionController(editor, canvas, handlers, touchOptions) {
|
|
305
|
+
const allowPinchZoom = touchOptions?.pinchToZoom !== false;
|
|
306
|
+
const allowFingerPan = touchOptions?.fingerPanInPenMode !== false;
|
|
307
|
+
const allowTapUndoRedo = touchOptions?.tapUndoRedo !== false;
|
|
308
|
+
const allowTrackpadGestures = touchOptions?.trackpadGestures !== false;
|
|
307
309
|
const activeTouchPoints = /* @__PURE__ */ new Map();
|
|
308
310
|
const touchTapState = {
|
|
309
311
|
active: false,
|
|
@@ -318,21 +320,34 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
318
320
|
mode: "not-sure",
|
|
319
321
|
previousCenter: { x: 0, y: 0 },
|
|
320
322
|
initialCenter: { x: 0, y: 0 },
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
initialDistance: 1,
|
|
324
|
+
initialZoom: 1
|
|
323
325
|
};
|
|
326
|
+
let fingerPanPointerId = null;
|
|
327
|
+
let fingerPanSession = null;
|
|
328
|
+
let fingerPanSlide = null;
|
|
324
329
|
const isTouchPointer = (event) => event.pointerType === "touch";
|
|
330
|
+
const stopFingerPanSlide = () => {
|
|
331
|
+
if (fingerPanSlide) {
|
|
332
|
+
fingerPanSlide.stop();
|
|
333
|
+
fingerPanSlide = null;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const endFingerPan = () => {
|
|
337
|
+
fingerPanPointerId = null;
|
|
338
|
+
fingerPanSession = null;
|
|
339
|
+
};
|
|
325
340
|
const endTouchCameraGesture = () => {
|
|
326
341
|
touchCameraState.active = false;
|
|
327
342
|
touchCameraState.mode = "not-sure";
|
|
328
|
-
touchCameraState.previousDistance = 1;
|
|
329
343
|
touchCameraState.initialDistance = 1;
|
|
344
|
+
touchCameraState.initialZoom = 1;
|
|
330
345
|
};
|
|
331
346
|
const maybeHandleTouchTapGesture = () => {
|
|
332
347
|
if (activeTouchPoints.size > 0) return;
|
|
333
348
|
if (!touchTapState.active) return;
|
|
334
349
|
const elapsed = performance.now() - touchTapState.startTime;
|
|
335
|
-
if (!touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
|
|
350
|
+
if (allowTapUndoRedo && !touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
|
|
336
351
|
const fingerCount = touchTapState.maxTouchCount;
|
|
337
352
|
const now = performance.now();
|
|
338
353
|
const previousTapTime = touchTapState.lastTapAtByCount[fingerCount] ?? 0;
|
|
@@ -361,8 +376,8 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
361
376
|
touchCameraState.mode = "not-sure";
|
|
362
377
|
touchCameraState.previousCenter = center;
|
|
363
378
|
touchCameraState.initialCenter = center;
|
|
364
|
-
touchCameraState.previousDistance = Math.max(1, distance);
|
|
365
379
|
touchCameraState.initialDistance = Math.max(1, distance);
|
|
380
|
+
touchCameraState.initialZoom = editor.getZoomLevel();
|
|
366
381
|
};
|
|
367
382
|
const updateTouchCameraGesture = () => {
|
|
368
383
|
if (!touchCameraState.active) return false;
|
|
@@ -380,24 +395,33 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
380
395
|
const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
|
|
381
396
|
const originDistance = Math.hypot(center.x - touchCameraState.initialCenter.x, center.y - touchCameraState.initialCenter.y);
|
|
382
397
|
if (touchCameraState.mode === "not-sure") {
|
|
383
|
-
if (touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
398
|
+
if (allowPinchZoom && touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
384
399
|
else if (originDistance > PINCH_MODE_PAN_DISTANCE) touchCameraState.mode = "panning";
|
|
385
|
-
} else if (touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
400
|
+
} else if (allowPinchZoom && touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
386
401
|
const canvasRect = canvas.getBoundingClientRect();
|
|
387
402
|
const centerOnCanvasX = center.x - canvasRect.left;
|
|
388
403
|
const centerOnCanvasY = center.y - canvasRect.top;
|
|
389
|
-
editor.panBy(centerDx, centerDy);
|
|
390
404
|
if (touchCameraState.mode === "zooming") {
|
|
391
|
-
const
|
|
392
|
-
editor.
|
|
405
|
+
const targetZoom = Math.max(0.1, Math.min(4, touchCameraState.initialZoom * (distance / touchCameraState.initialDistance)));
|
|
406
|
+
const pannedX = editor.viewport.x + centerDx;
|
|
407
|
+
const pannedY = editor.viewport.y + centerDy;
|
|
408
|
+
const pageAtCenterX = (centerOnCanvasX - pannedX) / editor.viewport.zoom;
|
|
409
|
+
const pageAtCenterY = (centerOnCanvasY - pannedY) / editor.viewport.zoom;
|
|
410
|
+
editor.setViewport({
|
|
411
|
+
x: centerOnCanvasX - pageAtCenterX * targetZoom,
|
|
412
|
+
y: centerOnCanvasY - pageAtCenterY * targetZoom,
|
|
413
|
+
zoom: targetZoom
|
|
414
|
+
});
|
|
415
|
+
} else {
|
|
416
|
+
editor.panBy(centerDx, centerDy);
|
|
393
417
|
}
|
|
394
418
|
touchCameraState.previousCenter = center;
|
|
395
|
-
touchCameraState.previousDistance = distance;
|
|
396
419
|
handlers.refreshView();
|
|
397
420
|
return true;
|
|
398
421
|
};
|
|
399
422
|
const handlePointerDown = (event) => {
|
|
400
423
|
if (!isTouchPointer(event)) return false;
|
|
424
|
+
stopFingerPanSlide();
|
|
401
425
|
activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
402
426
|
if (!touchTapState.active) {
|
|
403
427
|
touchTapState.active = true;
|
|
@@ -410,9 +434,16 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
410
434
|
}
|
|
411
435
|
touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
412
436
|
if (activeTouchPoints.size === 2) {
|
|
437
|
+
endFingerPan();
|
|
413
438
|
beginTouchCameraGesture();
|
|
414
439
|
return true;
|
|
415
440
|
}
|
|
441
|
+
if (allowFingerPan && handlers.isPenModeActive() && activeTouchPoints.size === 1) {
|
|
442
|
+
handlers.cancelActivePointerInteraction();
|
|
443
|
+
fingerPanPointerId = event.pointerId;
|
|
444
|
+
fingerPanSession = beginCameraPan(editor.viewport, event.clientX, event.clientY);
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
416
447
|
return false;
|
|
417
448
|
};
|
|
418
449
|
const handlePointerMove = (event) => {
|
|
@@ -423,22 +454,45 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
423
454
|
const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
|
|
424
455
|
if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
|
|
425
456
|
}
|
|
457
|
+
if (fingerPanPointerId === event.pointerId && fingerPanSession) {
|
|
458
|
+
const target = moveCameraPan(fingerPanSession, event.clientX, event.clientY);
|
|
459
|
+
editor.setViewport({ x: target.x, y: target.y });
|
|
460
|
+
handlers.refreshView();
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
426
463
|
return updateTouchCameraGesture();
|
|
427
464
|
};
|
|
428
465
|
const handlePointerUpOrCancel = (event) => {
|
|
429
466
|
if (!isTouchPointer(event)) return false;
|
|
430
467
|
const wasCameraGestureActive = touchCameraState.active;
|
|
468
|
+
const wasFingerPan = fingerPanPointerId === event.pointerId;
|
|
469
|
+
const releasedPanSession = wasFingerPan ? fingerPanSession : null;
|
|
431
470
|
activeTouchPoints.delete(event.pointerId);
|
|
432
471
|
touchTapState.startPoints.delete(event.pointerId);
|
|
433
472
|
if (activeTouchPoints.size < 2) endTouchCameraGesture();
|
|
473
|
+
if (wasFingerPan) {
|
|
474
|
+
endFingerPan();
|
|
475
|
+
if (releasedPanSession) {
|
|
476
|
+
const slideConfig = handlers.getSlideOptions();
|
|
477
|
+
if (slideConfig.enabled) {
|
|
478
|
+
fingerPanSlide = startCameraSlide(
|
|
479
|
+
releasedPanSession,
|
|
480
|
+
(dx, dy) => editor.panBy(dx, dy),
|
|
481
|
+
() => handlers.refreshView(),
|
|
482
|
+
slideConfig.slideOptions
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
434
487
|
maybeHandleTouchTapGesture();
|
|
435
|
-
return wasCameraGestureActive;
|
|
488
|
+
return wasCameraGestureActive || wasFingerPan;
|
|
436
489
|
};
|
|
437
490
|
let gestureLastScale = 1;
|
|
438
491
|
let gestureActive = false;
|
|
439
492
|
const handleGestureEvent = (event, container) => {
|
|
440
493
|
if (!container.contains(event.target)) return;
|
|
441
494
|
event.preventDefault();
|
|
495
|
+
if (!allowTrackpadGestures) return;
|
|
442
496
|
const gestureEvent = event;
|
|
443
497
|
if (gestureEvent.scale == null) return;
|
|
444
498
|
if (event.type === "gesturestart") {
|
|
@@ -466,6 +520,8 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
466
520
|
touchTapState.active = false;
|
|
467
521
|
touchTapState.startPoints.clear();
|
|
468
522
|
endTouchCameraGesture();
|
|
523
|
+
endFingerPan();
|
|
524
|
+
stopFingerPanSlide();
|
|
469
525
|
};
|
|
470
526
|
return {
|
|
471
527
|
handlePointerDown,
|
|
@@ -474,12 +530,13 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
474
530
|
handleGestureEvent,
|
|
475
531
|
reset,
|
|
476
532
|
isCameraGestureActive: () => touchCameraState.active,
|
|
533
|
+
isFingerPanActive: () => fingerPanPointerId !== null,
|
|
477
534
|
isTrackpadZoomActive: () => gestureActive
|
|
478
535
|
};
|
|
479
536
|
}
|
|
480
537
|
|
|
481
538
|
// src/canvas/keyboardShortcuts.ts
|
|
482
|
-
var
|
|
539
|
+
var DEFAULT_TOOL_SHORTCUTS = {
|
|
483
540
|
v: "select",
|
|
484
541
|
h: "hand",
|
|
485
542
|
e: "eraser",
|
|
@@ -491,6 +548,11 @@ var TOOL_SHORTCUTS = {
|
|
|
491
548
|
o: "circle",
|
|
492
549
|
c: "circle"
|
|
493
550
|
};
|
|
551
|
+
function resolveToolShortcuts(shortcutOptions) {
|
|
552
|
+
if (!shortcutOptions?.toolShortcuts) return DEFAULT_TOOL_SHORTCUTS;
|
|
553
|
+
if (shortcutOptions.overrideDefaults) return { ...shortcutOptions.toolShortcuts };
|
|
554
|
+
return { ...DEFAULT_TOOL_SHORTCUTS, ...shortcutOptions.toolShortcuts };
|
|
555
|
+
}
|
|
494
556
|
function isEditableTarget(eventTarget) {
|
|
495
557
|
const element = eventTarget;
|
|
496
558
|
if (!element) return false;
|
|
@@ -498,7 +560,7 @@ function isEditableTarget(eventTarget) {
|
|
|
498
560
|
const tagName = element.tagName;
|
|
499
561
|
return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
|
|
500
562
|
}
|
|
501
|
-
function handleKeyboardShortcutKeyDown(event, handlers) {
|
|
563
|
+
function handleKeyboardShortcutKeyDown(event, handlers, toolShortcutMap = DEFAULT_TOOL_SHORTCUTS) {
|
|
502
564
|
if (isEditableTarget(event.target)) return;
|
|
503
565
|
const loweredKey = event.key.toLowerCase();
|
|
504
566
|
const isMetaPressed = event.metaKey || event.ctrlKey;
|
|
@@ -511,7 +573,7 @@ function handleKeyboardShortcutKeyDown(event, handlers) {
|
|
|
511
573
|
}
|
|
512
574
|
}
|
|
513
575
|
if (!isMetaPressed && !event.altKey) {
|
|
514
|
-
const nextToolId =
|
|
576
|
+
const nextToolId = toolShortcutMap[loweredKey];
|
|
515
577
|
if (nextToolId && handlers.isToolAvailable(nextToolId)) {
|
|
516
578
|
handlers.setToolFromShortcut(nextToolId);
|
|
517
579
|
event.preventDefault();
|
|
@@ -686,8 +748,17 @@ function getHandlePagePoint(bounds, handle) {
|
|
|
686
748
|
}
|
|
687
749
|
}
|
|
688
750
|
var ZOOM_WHEEL_CAP = 10;
|
|
751
|
+
var VIEW_ONLY_TOOLS = /* @__PURE__ */ new Set(["select", "hand"]);
|
|
689
752
|
function useTsdrawCanvasController(options = {}) {
|
|
690
753
|
const onMountRef = useRef(options.onMount);
|
|
754
|
+
const onChangeRef = useRef(options.onChange);
|
|
755
|
+
const onCameraChangeRef = useRef(options.onCameraChange);
|
|
756
|
+
const onToolChangeRef = useRef(options.onToolChange);
|
|
757
|
+
const cameraOptionsRef = useRef(options.cameraOptions);
|
|
758
|
+
const touchOptionsRef = useRef(options.touchOptions);
|
|
759
|
+
const keyboardShortcutsRef = useRef(options.keyboardShortcuts);
|
|
760
|
+
const penOptionsRef = useRef(options.penOptions);
|
|
761
|
+
const readOnlyRef = useRef(options.readOnly ?? false);
|
|
691
762
|
const containerRef = useRef(null);
|
|
692
763
|
const canvasRef = useRef(null);
|
|
693
764
|
const editorRef = useRef(null);
|
|
@@ -702,6 +773,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
702
773
|
const schedulePersistRef = useRef(null);
|
|
703
774
|
const isPointerActiveRef = useRef(false);
|
|
704
775
|
const pendingRemoteDocumentRef = useRef(null);
|
|
776
|
+
const activeCameraSlideRef = useRef(null);
|
|
705
777
|
const selectionRotationRef = useRef(0);
|
|
706
778
|
const resizeRef = useRef({
|
|
707
779
|
handle: null,
|
|
@@ -746,6 +818,30 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
746
818
|
useEffect(() => {
|
|
747
819
|
onMountRef.current = options.onMount;
|
|
748
820
|
}, [options.onMount]);
|
|
821
|
+
useEffect(() => {
|
|
822
|
+
onChangeRef.current = options.onChange;
|
|
823
|
+
}, [options.onChange]);
|
|
824
|
+
useEffect(() => {
|
|
825
|
+
onCameraChangeRef.current = options.onCameraChange;
|
|
826
|
+
}, [options.onCameraChange]);
|
|
827
|
+
useEffect(() => {
|
|
828
|
+
onToolChangeRef.current = options.onToolChange;
|
|
829
|
+
}, [options.onToolChange]);
|
|
830
|
+
useEffect(() => {
|
|
831
|
+
cameraOptionsRef.current = options.cameraOptions;
|
|
832
|
+
}, [options.cameraOptions]);
|
|
833
|
+
useEffect(() => {
|
|
834
|
+
touchOptionsRef.current = options.touchOptions;
|
|
835
|
+
}, [options.touchOptions]);
|
|
836
|
+
useEffect(() => {
|
|
837
|
+
keyboardShortcutsRef.current = options.keyboardShortcuts;
|
|
838
|
+
}, [options.keyboardShortcuts]);
|
|
839
|
+
useEffect(() => {
|
|
840
|
+
penOptionsRef.current = options.penOptions;
|
|
841
|
+
}, [options.penOptions]);
|
|
842
|
+
useEffect(() => {
|
|
843
|
+
readOnlyRef.current = options.readOnly ?? false;
|
|
844
|
+
}, [options.readOnly]);
|
|
749
845
|
useEffect(() => {
|
|
750
846
|
selectedShapeIdsRef.current = selectedShapeIds;
|
|
751
847
|
}, [selectedShapeIds]);
|
|
@@ -869,14 +965,21 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
869
965
|
const canvas = canvasRef.current;
|
|
870
966
|
if (!container || !canvas) return;
|
|
871
967
|
const initialTool = options.initialTool ?? "pen";
|
|
968
|
+
const cameraOpts = cameraOptionsRef.current;
|
|
969
|
+
const touchOpts = touchOptionsRef.current;
|
|
970
|
+
const toolShortcutMap = resolveToolShortcuts(keyboardShortcutsRef.current);
|
|
872
971
|
const editor = new Editor({
|
|
873
972
|
toolDefinitions: options.toolDefinitions,
|
|
874
|
-
initialToolId: initialTool
|
|
973
|
+
initialToolId: initialTool,
|
|
974
|
+
zoomRange: cameraOpts?.zoomRange
|
|
875
975
|
});
|
|
876
976
|
editor.renderer.setTheme(options.theme ?? "light");
|
|
877
977
|
if (!editor.tools.hasTool(initialTool)) {
|
|
878
978
|
editor.setCurrentTool("pen");
|
|
879
979
|
}
|
|
980
|
+
if (options.snapshot) {
|
|
981
|
+
editor.loadPersistenceSnapshot(options.snapshot);
|
|
982
|
+
}
|
|
880
983
|
let disposed = false;
|
|
881
984
|
let ignorePersistenceChanges = false;
|
|
882
985
|
let disposeMount;
|
|
@@ -1043,40 +1146,60 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1043
1146
|
render();
|
|
1044
1147
|
refreshSelectionBounds(editor);
|
|
1045
1148
|
};
|
|
1149
|
+
const emitCameraChange = () => {
|
|
1150
|
+
onCameraChangeRef.current?.({ ...editor.viewport });
|
|
1151
|
+
};
|
|
1046
1152
|
const touchInteractions = createTouchInteractionController(editor, canvas, {
|
|
1047
1153
|
cancelActivePointerInteraction,
|
|
1048
1154
|
refreshView: () => {
|
|
1049
1155
|
render();
|
|
1050
1156
|
refreshSelectionBounds(editor);
|
|
1157
|
+
emitCameraChange();
|
|
1051
1158
|
},
|
|
1052
1159
|
runUndo: () => applyDocumentChangeResult(editor.undo()),
|
|
1053
|
-
runRedo: () => applyDocumentChangeResult(editor.redo())
|
|
1054
|
-
|
|
1055
|
-
|
|
1160
|
+
runRedo: () => applyDocumentChangeResult(editor.redo()),
|
|
1161
|
+
isPenModeActive: () => penModeRef.current,
|
|
1162
|
+
getSlideOptions: () => ({
|
|
1163
|
+
enabled: cameraOptionsRef.current?.slideEnabled !== false,
|
|
1164
|
+
slideOptions: { friction: cameraOptionsRef.current?.slideFriction }
|
|
1165
|
+
})
|
|
1166
|
+
}, touchOpts);
|
|
1056
1167
|
const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
|
|
1168
|
+
const stopActiveSlide = () => {
|
|
1169
|
+
if (activeCameraSlideRef.current) {
|
|
1170
|
+
activeCameraSlideRef.current.stop();
|
|
1171
|
+
activeCameraSlideRef.current = null;
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1057
1174
|
const handlePointerDown = (e) => {
|
|
1058
1175
|
if (!canvas.contains(e.target)) return;
|
|
1059
|
-
if (
|
|
1176
|
+
if (cameraOptionsRef.current?.locked && e.pointerType !== "pen") return;
|
|
1177
|
+
stopActiveSlide();
|
|
1178
|
+
const penAutoDetect = penOptionsRef.current?.autoDetect !== false;
|
|
1179
|
+
if (penAutoDetect && !penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1060
1180
|
penDetectedRef.current = true;
|
|
1061
1181
|
penModeRef.current = true;
|
|
1062
1182
|
}
|
|
1063
1183
|
lastPointerDownWithRef.current = e.pointerType;
|
|
1064
1184
|
activePointerIdsRef.current.add(e.pointerId);
|
|
1065
1185
|
const startedCameraGesture = touchInteractions.handlePointerDown(e);
|
|
1066
|
-
if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
|
|
1186
|
+
if (startedCameraGesture || touchInteractions.isCameraGestureActive() || touchInteractions.isFingerPanActive()) {
|
|
1067
1187
|
e.preventDefault();
|
|
1068
1188
|
if (!canvas.hasPointerCapture(e.pointerId)) {
|
|
1069
1189
|
canvas.setPointerCapture(e.pointerId);
|
|
1070
1190
|
}
|
|
1071
1191
|
return;
|
|
1072
1192
|
}
|
|
1073
|
-
const
|
|
1074
|
-
if (
|
|
1193
|
+
const isTouchBlockedByPenMode = penModeRef.current && e.pointerType === "touch";
|
|
1194
|
+
if (isTouchBlockedByPenMode) {
|
|
1075
1195
|
return;
|
|
1076
1196
|
}
|
|
1077
1197
|
if (activePointerIdsRef.current.size > 1) {
|
|
1078
1198
|
return;
|
|
1079
1199
|
}
|
|
1200
|
+
if (readOnlyRef.current && !VIEW_ONLY_TOOLS.has(currentToolRef.current)) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1080
1203
|
isPointerActiveRef.current = true;
|
|
1081
1204
|
editor.beginHistoryEntry();
|
|
1082
1205
|
canvas.setPointerCapture(e.pointerId);
|
|
@@ -1084,7 +1207,8 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1084
1207
|
updatePointerPreview(e.clientX, e.clientY);
|
|
1085
1208
|
const first = sampleEvents(e)[0];
|
|
1086
1209
|
const { x, y } = getPagePoint(first);
|
|
1087
|
-
const
|
|
1210
|
+
const pressureSensitivity = penOptionsRef.current?.pressureSensitivity ?? 1;
|
|
1211
|
+
const pressure = (first.pressure ?? 0.5) * pressureSensitivity;
|
|
1088
1212
|
const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
|
|
1089
1213
|
if (currentToolRef.current === "select") {
|
|
1090
1214
|
const hit = getTopShapeAtPoint(editor, { x, y });
|
|
@@ -1126,12 +1250,13 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1126
1250
|
}
|
|
1127
1251
|
editor.input.pointerDown(x, y, pressure, isPen);
|
|
1128
1252
|
editor.input.setModifiers(first.shiftKey, first.ctrlKey, first.metaKey);
|
|
1129
|
-
editor.tools.pointerDown({ point: { x, y, z: pressure } });
|
|
1253
|
+
editor.tools.pointerDown({ point: { x, y, z: pressure }, screenX: e.clientX, screenY: e.clientY });
|
|
1130
1254
|
render();
|
|
1131
1255
|
refreshSelectionBounds(editor);
|
|
1132
1256
|
};
|
|
1133
1257
|
const handlePointerMove = (e) => {
|
|
1134
|
-
|
|
1258
|
+
const penAutoDetectOnMove = penOptionsRef.current?.autoDetect !== false;
|
|
1259
|
+
if (penAutoDetectOnMove && !penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1135
1260
|
penDetectedRef.current = true;
|
|
1136
1261
|
penModeRef.current = true;
|
|
1137
1262
|
}
|
|
@@ -1139,16 +1264,17 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1139
1264
|
e.preventDefault();
|
|
1140
1265
|
return;
|
|
1141
1266
|
}
|
|
1142
|
-
if (penModeRef.current && e.pointerType === "touch" &&
|
|
1267
|
+
if (penModeRef.current && e.pointerType === "touch" && !isPointerActiveRef.current) return;
|
|
1143
1268
|
if (activePointerIdsRef.current.size > 1) return;
|
|
1144
1269
|
updatePointerPreview(e.clientX, e.clientY);
|
|
1145
1270
|
const prevClient = lastPointerClientRef.current;
|
|
1146
1271
|
const dx = prevClient ? e.clientX - prevClient.x : 0;
|
|
1147
1272
|
const dy = prevClient ? e.clientY - prevClient.y : 0;
|
|
1148
1273
|
lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
|
|
1274
|
+
const movePressureSensitivity = penOptionsRef.current?.pressureSensitivity ?? 1;
|
|
1149
1275
|
for (const sample of sampleEvents(e)) {
|
|
1150
1276
|
const { x, y } = getPagePoint(sample);
|
|
1151
|
-
const pressure = sample.pressure ?? 0.5;
|
|
1277
|
+
const pressure = (sample.pressure ?? 0.5) * movePressureSensitivity;
|
|
1152
1278
|
const isPen = sample.pointerType === "pen" || hasRealPressure(sample.pressure);
|
|
1153
1279
|
editor.input.pointerMove(x, y, pressure, isPen);
|
|
1154
1280
|
}
|
|
@@ -1193,7 +1319,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1193
1319
|
}
|
|
1194
1320
|
}
|
|
1195
1321
|
editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
|
|
1196
|
-
editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy });
|
|
1322
|
+
editor.tools.pointerMove({ screenDeltaX: dx, screenDeltaY: dy, screenX: e.clientX, screenY: e.clientY });
|
|
1197
1323
|
render();
|
|
1198
1324
|
refreshSelectionBounds(editor);
|
|
1199
1325
|
};
|
|
@@ -1272,9 +1398,30 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1272
1398
|
return;
|
|
1273
1399
|
}
|
|
1274
1400
|
}
|
|
1401
|
+
let handPanSession = null;
|
|
1402
|
+
if (currentToolRef.current === "hand") {
|
|
1403
|
+
const currentState = editor.tools.getCurrentState();
|
|
1404
|
+
if (currentState instanceof HandDraggingState) {
|
|
1405
|
+
handPanSession = currentState.getPanSession();
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1275
1408
|
editor.tools.pointerUp();
|
|
1276
1409
|
render();
|
|
1277
1410
|
refreshSelectionBounds(editor);
|
|
1411
|
+
if (handPanSession && cameraOptionsRef.current?.slideEnabled !== false) {
|
|
1412
|
+
activeCameraSlideRef.current = startCameraSlide(
|
|
1413
|
+
handPanSession,
|
|
1414
|
+
(slideDx, slideDy) => {
|
|
1415
|
+
editor.panBy(slideDx, slideDy);
|
|
1416
|
+
emitCameraChange();
|
|
1417
|
+
},
|
|
1418
|
+
() => {
|
|
1419
|
+
render();
|
|
1420
|
+
refreshSelectionBounds(editor);
|
|
1421
|
+
},
|
|
1422
|
+
{ friction: cameraOptionsRef.current?.slideFriction }
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1278
1425
|
if (pendingRemoteDocumentRef.current) {
|
|
1279
1426
|
const pendingRemoteDocument = pendingRemoteDocumentRef.current;
|
|
1280
1427
|
pendingRemoteDocumentRef.current = null;
|
|
@@ -1317,41 +1464,59 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1317
1464
|
const handleWheel = (e) => {
|
|
1318
1465
|
if (!container.contains(e.target)) return;
|
|
1319
1466
|
e.preventDefault();
|
|
1467
|
+
const camOpts = cameraOptionsRef.current;
|
|
1468
|
+
if (camOpts?.locked) return;
|
|
1469
|
+
if (camOpts?.wheelBehavior === "none") return;
|
|
1320
1470
|
if (touchInteractions.isTrackpadZoomActive()) return;
|
|
1321
1471
|
const delta = normalizeWheelDelta(e);
|
|
1472
|
+
const panMultiplier = camOpts?.panSpeed ?? 1;
|
|
1473
|
+
const zoomMultiplier = camOpts?.zoomSpeed ?? 1;
|
|
1322
1474
|
if (delta.z !== 0) {
|
|
1323
1475
|
const rect = canvas.getBoundingClientRect();
|
|
1324
1476
|
const pointX = e.clientX - rect.left;
|
|
1325
1477
|
const pointY = e.clientY - rect.top;
|
|
1326
|
-
editor.zoomAt(Math.exp(delta.z), pointX, pointY);
|
|
1478
|
+
editor.zoomAt(Math.exp(delta.z * zoomMultiplier), pointX, pointY);
|
|
1327
1479
|
} else {
|
|
1328
|
-
editor.panBy(delta.x, delta.y);
|
|
1480
|
+
editor.panBy(delta.x * panMultiplier, delta.y * panMultiplier);
|
|
1329
1481
|
}
|
|
1330
1482
|
render();
|
|
1331
1483
|
refreshSelectionBounds(editor);
|
|
1484
|
+
emitCameraChange();
|
|
1332
1485
|
};
|
|
1333
1486
|
const handleGestureEvent = (e) => {
|
|
1334
1487
|
touchInteractions.handleGestureEvent(e, container);
|
|
1335
1488
|
};
|
|
1336
1489
|
const handleKeyDown = (e) => {
|
|
1490
|
+
if (keyboardShortcutsRef.current?.enabled === false) return;
|
|
1491
|
+
const isReadOnly = readOnlyRef.current;
|
|
1337
1492
|
handleKeyboardShortcutKeyDown(e, {
|
|
1338
|
-
isToolAvailable: (tool) =>
|
|
1493
|
+
isToolAvailable: (tool) => {
|
|
1494
|
+
if (isReadOnly && !VIEW_ONLY_TOOLS.has(tool)) return false;
|
|
1495
|
+
return editor.tools.hasTool(tool);
|
|
1496
|
+
},
|
|
1339
1497
|
setToolFromShortcut: (tool) => {
|
|
1340
1498
|
editor.setCurrentTool(tool);
|
|
1341
1499
|
setCurrentToolState(tool);
|
|
1342
1500
|
currentToolRef.current = tool;
|
|
1343
1501
|
if (tool !== "select") resetSelectUi();
|
|
1344
1502
|
render();
|
|
1503
|
+
onToolChangeRef.current?.(tool);
|
|
1504
|
+
},
|
|
1505
|
+
runHistoryShortcut: (shouldRedo) => {
|
|
1506
|
+
if (isReadOnly) return false;
|
|
1507
|
+
return applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo());
|
|
1508
|
+
},
|
|
1509
|
+
deleteSelection: () => {
|
|
1510
|
+
if (isReadOnly) return false;
|
|
1511
|
+
return currentToolRef.current === "select" ? deleteCurrentSelection() : false;
|
|
1345
1512
|
},
|
|
1346
|
-
runHistoryShortcut: (shouldRedo) => applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo()),
|
|
1347
|
-
deleteSelection: () => currentToolRef.current === "select" ? deleteCurrentSelection() : false,
|
|
1348
1513
|
dispatchKeyDown: (event) => {
|
|
1349
1514
|
editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
|
|
1350
1515
|
editor.tools.keyDown({ key: event.key });
|
|
1351
1516
|
render();
|
|
1352
1517
|
},
|
|
1353
1518
|
dispatchKeyUp: () => void 0
|
|
1354
|
-
});
|
|
1519
|
+
}, toolShortcutMap);
|
|
1355
1520
|
};
|
|
1356
1521
|
const handleKeyUp = (e) => {
|
|
1357
1522
|
handleKeyboardShortcutKeyUp(e, {
|
|
@@ -1432,6 +1597,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1432
1597
|
const cleanupEditorListener = editor.listen(() => {
|
|
1433
1598
|
if (ignorePersistenceChanges) return;
|
|
1434
1599
|
schedulePersist();
|
|
1600
|
+
onChangeRef.current?.(editor.getDocumentSnapshot());
|
|
1435
1601
|
});
|
|
1436
1602
|
const cleanupHistoryListener = editor.listenHistory(() => {
|
|
1437
1603
|
syncHistoryState();
|
|
@@ -1498,6 +1664,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1498
1664
|
render();
|
|
1499
1665
|
}
|
|
1500
1666
|
});
|
|
1667
|
+
if (options.autoFocus !== false) {
|
|
1668
|
+
container.focus({ preventScroll: true });
|
|
1669
|
+
}
|
|
1501
1670
|
return () => {
|
|
1502
1671
|
disposed = true;
|
|
1503
1672
|
schedulePersistRef.current = null;
|
|
@@ -1519,6 +1688,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1519
1688
|
isPointerActiveRef.current = false;
|
|
1520
1689
|
activePointerIdsRef.current.clear();
|
|
1521
1690
|
pendingRemoteDocumentRef.current = null;
|
|
1691
|
+
stopActiveSlide();
|
|
1522
1692
|
touchInteractions.reset();
|
|
1523
1693
|
persistenceChannel?.close();
|
|
1524
1694
|
void persistenceDb?.close();
|
|
@@ -1545,10 +1715,12 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1545
1715
|
const editor = editorRef.current;
|
|
1546
1716
|
if (!editor) return;
|
|
1547
1717
|
if (!editor.tools.hasTool(tool)) return;
|
|
1718
|
+
if (readOnlyRef.current && !VIEW_ONLY_TOOLS.has(tool)) return;
|
|
1548
1719
|
editor.setCurrentTool(tool);
|
|
1549
1720
|
setCurrentToolState(tool);
|
|
1550
1721
|
currentToolRef.current = tool;
|
|
1551
1722
|
if (tool !== "select") resetSelectUi();
|
|
1723
|
+
onToolChangeRef.current?.(tool);
|
|
1552
1724
|
},
|
|
1553
1725
|
[resetSelectUi]
|
|
1554
1726
|
);
|
|
@@ -1784,12 +1956,22 @@ function Tsdraw(props) {
|
|
|
1784
1956
|
initialTool,
|
|
1785
1957
|
theme: resolvedTheme,
|
|
1786
1958
|
persistenceKey: props.persistenceKey,
|
|
1787
|
-
onMount: props.onMount
|
|
1959
|
+
onMount: props.onMount,
|
|
1960
|
+
cameraOptions: props.cameraOptions,
|
|
1961
|
+
touchOptions: props.touchOptions,
|
|
1962
|
+
keyboardShortcuts: props.keyboardShortcuts,
|
|
1963
|
+
penOptions: props.penOptions,
|
|
1964
|
+
readOnly: props.readOnly,
|
|
1965
|
+
autoFocus: props.autoFocus,
|
|
1966
|
+
snapshot: props.snapshot,
|
|
1967
|
+
onChange: props.onChange,
|
|
1968
|
+
onCameraChange: props.onCameraChange,
|
|
1969
|
+
onToolChange: props.onToolChange
|
|
1788
1970
|
});
|
|
1789
1971
|
const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 14);
|
|
1790
1972
|
const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8, 8);
|
|
1791
1973
|
const isToolbarHidden = props.uiOptions?.toolbar?.hide === true;
|
|
1792
|
-
const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true;
|
|
1974
|
+
const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true || props.readOnly === true;
|
|
1793
1975
|
const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
|
|
1794
1976
|
const defaultToolOverlay = /* @__PURE__ */ jsx(
|
|
1795
1977
|
ToolOverlay,
|
|
@@ -1878,12 +2060,14 @@ function Tsdraw(props) {
|
|
|
1878
2060
|
"div",
|
|
1879
2061
|
{
|
|
1880
2062
|
ref: containerRef,
|
|
2063
|
+
tabIndex: 0,
|
|
1881
2064
|
className: `tsdraw tsdraw-${resolvedTheme}mode ${props.className ?? ""}`,
|
|
1882
2065
|
style: {
|
|
1883
2066
|
width: props.width ?? "100%",
|
|
1884
2067
|
height: props.height ?? "100%",
|
|
1885
2068
|
position: "relative",
|
|
1886
2069
|
overflow: "hidden",
|
|
2070
|
+
outline: "none",
|
|
1887
2071
|
...props.style
|
|
1888
2072
|
},
|
|
1889
2073
|
children: [
|