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