@tsdraw/react 0.8.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +154 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +155 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode, CSSProperties } from 'react';
|
|
3
|
-
import { ColorStyle, DashStyle, FillStyle, SizeStyle,
|
|
3
|
+
import { ZoomRange, ToolId, ColorStyle, DashStyle, FillStyle, SizeStyle, Editor, ToolDefinition, TsdrawBackgroundOptions, TsdrawEditorSnapshot, TsdrawDocumentSnapshot, Viewport } from '@tsdraw/core';
|
|
4
|
+
export { TsdrawBackgroundCustom, TsdrawBackgroundOptions, TsdrawBackgroundPreset, TsdrawBackgroundType } from '@tsdraw/core';
|
|
5
|
+
|
|
6
|
+
interface TsdrawCameraOptions {
|
|
7
|
+
panSpeed?: number;
|
|
8
|
+
zoomSpeed?: number;
|
|
9
|
+
zoomRange?: ZoomRange;
|
|
10
|
+
wheelBehavior?: 'pan' | 'zoom' | 'none';
|
|
11
|
+
slideEnabled?: boolean;
|
|
12
|
+
slideFriction?: number;
|
|
13
|
+
locked?: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface TsdrawTouchOptions {
|
|
16
|
+
pinchToZoom?: boolean;
|
|
17
|
+
fingerPanInPenMode?: boolean;
|
|
18
|
+
tapUndoRedo?: boolean;
|
|
19
|
+
trackpadGestures?: boolean;
|
|
20
|
+
}
|
|
21
|
+
interface TsdrawKeyboardShortcutOptions {
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
toolShortcuts?: Record<string, ToolId>;
|
|
24
|
+
overrideDefaults?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface TsdrawPenOptions {
|
|
27
|
+
pressureSensitivity?: number;
|
|
28
|
+
autoDetect?: boolean;
|
|
29
|
+
}
|
|
4
30
|
|
|
5
31
|
type TsdrawStylePanelPartItem = 'colors' | 'dashes' | 'fills' | 'sizes' | (string & {});
|
|
6
32
|
interface TsdrawStylePanelRenderContext {
|
|
@@ -123,6 +149,17 @@ interface TsdrawProps {
|
|
|
123
149
|
initialToolId?: ToolId;
|
|
124
150
|
uiOptions?: TsdrawUiOptions;
|
|
125
151
|
onMount?: (api: TsdrawMountApi) => void | (() => void);
|
|
152
|
+
cameraOptions?: TsdrawCameraOptions;
|
|
153
|
+
touchOptions?: TsdrawTouchOptions;
|
|
154
|
+
keyboardShortcuts?: TsdrawKeyboardShortcutOptions;
|
|
155
|
+
penOptions?: TsdrawPenOptions;
|
|
156
|
+
background?: TsdrawBackgroundOptions;
|
|
157
|
+
readOnly?: boolean;
|
|
158
|
+
autoFocus?: boolean;
|
|
159
|
+
snapshot?: TsdrawEditorSnapshot;
|
|
160
|
+
onChange?: (snapshot: TsdrawDocumentSnapshot) => void;
|
|
161
|
+
onCameraChange?: (viewport: Viewport) => void;
|
|
162
|
+
onToolChange?: (toolId: ToolId) => void;
|
|
126
163
|
}
|
|
127
164
|
type TsdrawCanvasProps = TsdrawProps;
|
|
128
165
|
declare function Tsdraw(props: TsdrawProps): react_jsx_runtime.JSX.Element;
|
|
@@ -148,4 +185,4 @@ interface ToolbarPart {
|
|
|
148
185
|
}
|
|
149
186
|
declare function getDefaultToolbarIcon(toolId: ToolId, isActive: boolean): ReactNode;
|
|
150
187
|
|
|
151
|
-
export { type ToolbarActionItem, type ToolbarPart, type ToolbarPartItem, type ToolbarRenderItem, type ToolbarToolItem, Tsdraw, TsdrawCanvas, type TsdrawCanvasProps, type TsdrawCursorContext, type TsdrawCustomElement, type TsdrawCustomElementRenderArgs, type TsdrawCustomTool, type TsdrawMountApi, type TsdrawProps, type TsdrawStylePanelCustomPart, type TsdrawStylePanelPartItem, type TsdrawStylePanelRenderContext, type TsdrawToolOverlayState, type TsdrawToolbarBuiltInAction, type TsdrawUiOptions, type TsdrawUiPlacement, type UiAnchor, getDefaultToolbarIcon };
|
|
188
|
+
export { type ToolbarActionItem, type ToolbarPart, type ToolbarPartItem, type ToolbarRenderItem, type ToolbarToolItem, Tsdraw, type TsdrawCameraOptions, TsdrawCanvas, type TsdrawCanvasProps, type TsdrawCursorContext, type TsdrawCustomElement, type TsdrawCustomElementRenderArgs, type TsdrawCustomTool, type TsdrawKeyboardShortcutOptions, type TsdrawMountApi, type TsdrawPenOptions, type TsdrawProps, type TsdrawStylePanelCustomPart, type TsdrawStylePanelPartItem, type TsdrawStylePanelRenderContext, type TsdrawToolOverlayState, type TsdrawToolbarBuiltInAction, type TsdrawTouchOptions, type TsdrawUiOptions, type TsdrawUiPlacement, type UiAnchor, getDefaultToolbarIcon };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode, CSSProperties } from 'react';
|
|
3
|
-
import { ColorStyle, DashStyle, FillStyle, SizeStyle,
|
|
3
|
+
import { ZoomRange, ToolId, ColorStyle, DashStyle, FillStyle, SizeStyle, Editor, ToolDefinition, TsdrawBackgroundOptions, TsdrawEditorSnapshot, TsdrawDocumentSnapshot, Viewport } from '@tsdraw/core';
|
|
4
|
+
export { TsdrawBackgroundCustom, TsdrawBackgroundOptions, TsdrawBackgroundPreset, TsdrawBackgroundType } from '@tsdraw/core';
|
|
5
|
+
|
|
6
|
+
interface TsdrawCameraOptions {
|
|
7
|
+
panSpeed?: number;
|
|
8
|
+
zoomSpeed?: number;
|
|
9
|
+
zoomRange?: ZoomRange;
|
|
10
|
+
wheelBehavior?: 'pan' | 'zoom' | 'none';
|
|
11
|
+
slideEnabled?: boolean;
|
|
12
|
+
slideFriction?: number;
|
|
13
|
+
locked?: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface TsdrawTouchOptions {
|
|
16
|
+
pinchToZoom?: boolean;
|
|
17
|
+
fingerPanInPenMode?: boolean;
|
|
18
|
+
tapUndoRedo?: boolean;
|
|
19
|
+
trackpadGestures?: boolean;
|
|
20
|
+
}
|
|
21
|
+
interface TsdrawKeyboardShortcutOptions {
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
toolShortcuts?: Record<string, ToolId>;
|
|
24
|
+
overrideDefaults?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface TsdrawPenOptions {
|
|
27
|
+
pressureSensitivity?: number;
|
|
28
|
+
autoDetect?: boolean;
|
|
29
|
+
}
|
|
4
30
|
|
|
5
31
|
type TsdrawStylePanelPartItem = 'colors' | 'dashes' | 'fills' | 'sizes' | (string & {});
|
|
6
32
|
interface TsdrawStylePanelRenderContext {
|
|
@@ -123,6 +149,17 @@ interface TsdrawProps {
|
|
|
123
149
|
initialToolId?: ToolId;
|
|
124
150
|
uiOptions?: TsdrawUiOptions;
|
|
125
151
|
onMount?: (api: TsdrawMountApi) => void | (() => void);
|
|
152
|
+
cameraOptions?: TsdrawCameraOptions;
|
|
153
|
+
touchOptions?: TsdrawTouchOptions;
|
|
154
|
+
keyboardShortcuts?: TsdrawKeyboardShortcutOptions;
|
|
155
|
+
penOptions?: TsdrawPenOptions;
|
|
156
|
+
background?: TsdrawBackgroundOptions;
|
|
157
|
+
readOnly?: boolean;
|
|
158
|
+
autoFocus?: boolean;
|
|
159
|
+
snapshot?: TsdrawEditorSnapshot;
|
|
160
|
+
onChange?: (snapshot: TsdrawDocumentSnapshot) => void;
|
|
161
|
+
onCameraChange?: (viewport: Viewport) => void;
|
|
162
|
+
onToolChange?: (toolId: ToolId) => void;
|
|
126
163
|
}
|
|
127
164
|
type TsdrawCanvasProps = TsdrawProps;
|
|
128
165
|
declare function Tsdraw(props: TsdrawProps): react_jsx_runtime.JSX.Element;
|
|
@@ -148,4 +185,4 @@ interface ToolbarPart {
|
|
|
148
185
|
}
|
|
149
186
|
declare function getDefaultToolbarIcon(toolId: ToolId, isActive: boolean): ReactNode;
|
|
150
187
|
|
|
151
|
-
export { type ToolbarActionItem, type ToolbarPart, type ToolbarPartItem, type ToolbarRenderItem, type ToolbarToolItem, Tsdraw, TsdrawCanvas, type TsdrawCanvasProps, type TsdrawCursorContext, type TsdrawCustomElement, type TsdrawCustomElementRenderArgs, type TsdrawCustomTool, type TsdrawMountApi, type TsdrawProps, type TsdrawStylePanelCustomPart, type TsdrawStylePanelPartItem, type TsdrawStylePanelRenderContext, type TsdrawToolOverlayState, type TsdrawToolbarBuiltInAction, type TsdrawUiOptions, type TsdrawUiPlacement, type UiAnchor, getDefaultToolbarIcon };
|
|
188
|
+
export { type ToolbarActionItem, type ToolbarPart, type ToolbarPartItem, type ToolbarRenderItem, type ToolbarToolItem, Tsdraw, type TsdrawCameraOptions, TsdrawCanvas, type TsdrawCanvasProps, type TsdrawCursorContext, type TsdrawCustomElement, type TsdrawCustomElementRenderArgs, type TsdrawCustomTool, type TsdrawKeyboardShortcutOptions, type TsdrawMountApi, type TsdrawPenOptions, type TsdrawProps, type TsdrawStylePanelCustomPart, type TsdrawStylePanelPartItem, type TsdrawStylePanelRenderContext, type TsdrawToolOverlayState, type TsdrawToolbarBuiltInAction, type TsdrawTouchOptions, type TsdrawUiOptions, type TsdrawUiPlacement, type UiAnchor, getDefaultToolbarIcon };
|
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, HandDraggingState, startCameraSlide, isSelectTool, beginCameraPan, moveCameraPan } from '@tsdraw/core';
|
|
3
|
+
import { DEFAULT_COLORS, renderCanvasBackground, 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
|
|
@@ -301,7 +301,11 @@ var TAP_MOVE_TOLERANCE = 14;
|
|
|
301
301
|
var PINCH_MODE_ZOOM_DISTANCE = 24;
|
|
302
302
|
var PINCH_MODE_PAN_DISTANCE = 16;
|
|
303
303
|
var PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE = 64;
|
|
304
|
-
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;
|
|
305
309
|
const activeTouchPoints = /* @__PURE__ */ new Map();
|
|
306
310
|
const touchTapState = {
|
|
307
311
|
active: false,
|
|
@@ -343,7 +347,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
343
347
|
if (activeTouchPoints.size > 0) return;
|
|
344
348
|
if (!touchTapState.active) return;
|
|
345
349
|
const elapsed = performance.now() - touchTapState.startTime;
|
|
346
|
-
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)) {
|
|
347
351
|
const fingerCount = touchTapState.maxTouchCount;
|
|
348
352
|
const now = performance.now();
|
|
349
353
|
const previousTapTime = touchTapState.lastTapAtByCount[fingerCount] ?? 0;
|
|
@@ -391,9 +395,9 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
391
395
|
const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
|
|
392
396
|
const originDistance = Math.hypot(center.x - touchCameraState.initialCenter.x, center.y - touchCameraState.initialCenter.y);
|
|
393
397
|
if (touchCameraState.mode === "not-sure") {
|
|
394
|
-
if (touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
398
|
+
if (allowPinchZoom && touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
395
399
|
else if (originDistance > PINCH_MODE_PAN_DISTANCE) touchCameraState.mode = "panning";
|
|
396
|
-
} 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";
|
|
397
401
|
const canvasRect = canvas.getBoundingClientRect();
|
|
398
402
|
const centerOnCanvasX = center.x - canvasRect.left;
|
|
399
403
|
const centerOnCanvasY = center.y - canvasRect.top;
|
|
@@ -434,7 +438,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
434
438
|
beginTouchCameraGesture();
|
|
435
439
|
return true;
|
|
436
440
|
}
|
|
437
|
-
if (handlers.isPenModeActive() && activeTouchPoints.size === 1) {
|
|
441
|
+
if (allowFingerPan && handlers.isPenModeActive() && activeTouchPoints.size === 1) {
|
|
438
442
|
handlers.cancelActivePointerInteraction();
|
|
439
443
|
fingerPanPointerId = event.pointerId;
|
|
440
444
|
fingerPanSession = beginCameraPan(editor.viewport, event.clientX, event.clientY);
|
|
@@ -469,11 +473,15 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
469
473
|
if (wasFingerPan) {
|
|
470
474
|
endFingerPan();
|
|
471
475
|
if (releasedPanSession) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
}
|
|
477
485
|
}
|
|
478
486
|
}
|
|
479
487
|
maybeHandleTouchTapGesture();
|
|
@@ -484,6 +492,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
484
492
|
const handleGestureEvent = (event, container) => {
|
|
485
493
|
if (!container.contains(event.target)) return;
|
|
486
494
|
event.preventDefault();
|
|
495
|
+
if (!allowTrackpadGestures) return;
|
|
487
496
|
const gestureEvent = event;
|
|
488
497
|
if (gestureEvent.scale == null) return;
|
|
489
498
|
if (event.type === "gesturestart") {
|
|
@@ -527,7 +536,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
|
|
|
527
536
|
}
|
|
528
537
|
|
|
529
538
|
// src/canvas/keyboardShortcuts.ts
|
|
530
|
-
var
|
|
539
|
+
var DEFAULT_TOOL_SHORTCUTS = {
|
|
531
540
|
v: "select",
|
|
532
541
|
h: "hand",
|
|
533
542
|
e: "eraser",
|
|
@@ -539,6 +548,11 @@ var TOOL_SHORTCUTS = {
|
|
|
539
548
|
o: "circle",
|
|
540
549
|
c: "circle"
|
|
541
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
|
+
}
|
|
542
556
|
function isEditableTarget(eventTarget) {
|
|
543
557
|
const element = eventTarget;
|
|
544
558
|
if (!element) return false;
|
|
@@ -546,7 +560,7 @@ function isEditableTarget(eventTarget) {
|
|
|
546
560
|
const tagName = element.tagName;
|
|
547
561
|
return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
|
|
548
562
|
}
|
|
549
|
-
function handleKeyboardShortcutKeyDown(event, handlers) {
|
|
563
|
+
function handleKeyboardShortcutKeyDown(event, handlers, toolShortcutMap = DEFAULT_TOOL_SHORTCUTS) {
|
|
550
564
|
if (isEditableTarget(event.target)) return;
|
|
551
565
|
const loweredKey = event.key.toLowerCase();
|
|
552
566
|
const isMetaPressed = event.metaKey || event.ctrlKey;
|
|
@@ -559,7 +573,7 @@ function handleKeyboardShortcutKeyDown(event, handlers) {
|
|
|
559
573
|
}
|
|
560
574
|
}
|
|
561
575
|
if (!isMetaPressed && !event.altKey) {
|
|
562
|
-
const nextToolId =
|
|
576
|
+
const nextToolId = toolShortcutMap[loweredKey];
|
|
563
577
|
if (nextToolId && handlers.isToolAvailable(nextToolId)) {
|
|
564
578
|
handlers.setToolFromShortcut(nextToolId);
|
|
565
579
|
event.preventDefault();
|
|
@@ -734,8 +748,18 @@ function getHandlePagePoint(bounds, handle) {
|
|
|
734
748
|
}
|
|
735
749
|
}
|
|
736
750
|
var ZOOM_WHEEL_CAP = 10;
|
|
751
|
+
var VIEW_ONLY_TOOLS = /* @__PURE__ */ new Set(["select", "hand"]);
|
|
737
752
|
function useTsdrawCanvasController(options = {}) {
|
|
738
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 backgroundRef = useRef(options.background);
|
|
762
|
+
const readOnlyRef = useRef(options.readOnly ?? false);
|
|
739
763
|
const containerRef = useRef(null);
|
|
740
764
|
const canvasRef = useRef(null);
|
|
741
765
|
const editorRef = useRef(null);
|
|
@@ -795,6 +819,33 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
795
819
|
useEffect(() => {
|
|
796
820
|
onMountRef.current = options.onMount;
|
|
797
821
|
}, [options.onMount]);
|
|
822
|
+
useEffect(() => {
|
|
823
|
+
onChangeRef.current = options.onChange;
|
|
824
|
+
}, [options.onChange]);
|
|
825
|
+
useEffect(() => {
|
|
826
|
+
onCameraChangeRef.current = options.onCameraChange;
|
|
827
|
+
}, [options.onCameraChange]);
|
|
828
|
+
useEffect(() => {
|
|
829
|
+
onToolChangeRef.current = options.onToolChange;
|
|
830
|
+
}, [options.onToolChange]);
|
|
831
|
+
useEffect(() => {
|
|
832
|
+
cameraOptionsRef.current = options.cameraOptions;
|
|
833
|
+
}, [options.cameraOptions]);
|
|
834
|
+
useEffect(() => {
|
|
835
|
+
touchOptionsRef.current = options.touchOptions;
|
|
836
|
+
}, [options.touchOptions]);
|
|
837
|
+
useEffect(() => {
|
|
838
|
+
keyboardShortcutsRef.current = options.keyboardShortcuts;
|
|
839
|
+
}, [options.keyboardShortcuts]);
|
|
840
|
+
useEffect(() => {
|
|
841
|
+
penOptionsRef.current = options.penOptions;
|
|
842
|
+
}, [options.penOptions]);
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
backgroundRef.current = options.background;
|
|
845
|
+
}, [options.background]);
|
|
846
|
+
useEffect(() => {
|
|
847
|
+
readOnlyRef.current = options.readOnly ?? false;
|
|
848
|
+
}, [options.readOnly]);
|
|
798
849
|
useEffect(() => {
|
|
799
850
|
selectedShapeIdsRef.current = selectedShapeIds;
|
|
800
851
|
}, [selectedShapeIds]);
|
|
@@ -811,8 +862,11 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
811
862
|
const ctx = canvas.getContext("2d");
|
|
812
863
|
if (!ctx) return;
|
|
813
864
|
const dpr = dprRef.current || 1;
|
|
865
|
+
const logicalWidth = canvas.width / dpr;
|
|
866
|
+
const logicalHeight = canvas.height / dpr;
|
|
814
867
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
815
|
-
ctx.clearRect(0, 0,
|
|
868
|
+
ctx.clearRect(0, 0, logicalWidth, logicalHeight);
|
|
869
|
+
renderCanvasBackground(ctx, editor.viewport, logicalWidth, logicalHeight, backgroundRef.current, editor.renderer.theme);
|
|
816
870
|
editor.render(ctx);
|
|
817
871
|
}, []);
|
|
818
872
|
const refreshSelectionBounds = useCallback((editor, ids = selectedShapeIdsRef.current) => {
|
|
@@ -918,14 +972,21 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
918
972
|
const canvas = canvasRef.current;
|
|
919
973
|
if (!container || !canvas) return;
|
|
920
974
|
const initialTool = options.initialTool ?? "pen";
|
|
975
|
+
const cameraOpts = cameraOptionsRef.current;
|
|
976
|
+
const touchOpts = touchOptionsRef.current;
|
|
977
|
+
const toolShortcutMap = resolveToolShortcuts(keyboardShortcutsRef.current);
|
|
921
978
|
const editor = new Editor({
|
|
922
979
|
toolDefinitions: options.toolDefinitions,
|
|
923
|
-
initialToolId: initialTool
|
|
980
|
+
initialToolId: initialTool,
|
|
981
|
+
zoomRange: cameraOpts?.zoomRange
|
|
924
982
|
});
|
|
925
983
|
editor.renderer.setTheme(options.theme ?? "light");
|
|
926
984
|
if (!editor.tools.hasTool(initialTool)) {
|
|
927
985
|
editor.setCurrentTool("pen");
|
|
928
986
|
}
|
|
987
|
+
if (options.snapshot) {
|
|
988
|
+
editor.loadPersistenceSnapshot(options.snapshot);
|
|
989
|
+
}
|
|
929
990
|
let disposed = false;
|
|
930
991
|
let ignorePersistenceChanges = false;
|
|
931
992
|
let disposeMount;
|
|
@@ -1092,16 +1153,24 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1092
1153
|
render();
|
|
1093
1154
|
refreshSelectionBounds(editor);
|
|
1094
1155
|
};
|
|
1156
|
+
const emitCameraChange = () => {
|
|
1157
|
+
onCameraChangeRef.current?.({ ...editor.viewport });
|
|
1158
|
+
};
|
|
1095
1159
|
const touchInteractions = createTouchInteractionController(editor, canvas, {
|
|
1096
1160
|
cancelActivePointerInteraction,
|
|
1097
1161
|
refreshView: () => {
|
|
1098
1162
|
render();
|
|
1099
1163
|
refreshSelectionBounds(editor);
|
|
1164
|
+
emitCameraChange();
|
|
1100
1165
|
},
|
|
1101
1166
|
runUndo: () => applyDocumentChangeResult(editor.undo()),
|
|
1102
1167
|
runRedo: () => applyDocumentChangeResult(editor.redo()),
|
|
1103
|
-
isPenModeActive: () => penModeRef.current
|
|
1104
|
-
|
|
1168
|
+
isPenModeActive: () => penModeRef.current,
|
|
1169
|
+
getSlideOptions: () => ({
|
|
1170
|
+
enabled: cameraOptionsRef.current?.slideEnabled !== false,
|
|
1171
|
+
slideOptions: { friction: cameraOptionsRef.current?.slideFriction }
|
|
1172
|
+
})
|
|
1173
|
+
}, touchOpts);
|
|
1105
1174
|
const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
|
|
1106
1175
|
const stopActiveSlide = () => {
|
|
1107
1176
|
if (activeCameraSlideRef.current) {
|
|
@@ -1111,8 +1180,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1111
1180
|
};
|
|
1112
1181
|
const handlePointerDown = (e) => {
|
|
1113
1182
|
if (!canvas.contains(e.target)) return;
|
|
1183
|
+
if (cameraOptionsRef.current?.locked && e.pointerType !== "pen") return;
|
|
1114
1184
|
stopActiveSlide();
|
|
1115
|
-
|
|
1185
|
+
const penAutoDetect = penOptionsRef.current?.autoDetect !== false;
|
|
1186
|
+
if (penAutoDetect && !penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1116
1187
|
penDetectedRef.current = true;
|
|
1117
1188
|
penModeRef.current = true;
|
|
1118
1189
|
}
|
|
@@ -1133,6 +1204,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1133
1204
|
if (activePointerIdsRef.current.size > 1) {
|
|
1134
1205
|
return;
|
|
1135
1206
|
}
|
|
1207
|
+
if (readOnlyRef.current && !VIEW_ONLY_TOOLS.has(currentToolRef.current)) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1136
1210
|
isPointerActiveRef.current = true;
|
|
1137
1211
|
editor.beginHistoryEntry();
|
|
1138
1212
|
canvas.setPointerCapture(e.pointerId);
|
|
@@ -1140,7 +1214,8 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1140
1214
|
updatePointerPreview(e.clientX, e.clientY);
|
|
1141
1215
|
const first = sampleEvents(e)[0];
|
|
1142
1216
|
const { x, y } = getPagePoint(first);
|
|
1143
|
-
const
|
|
1217
|
+
const pressureSensitivity = penOptionsRef.current?.pressureSensitivity ?? 1;
|
|
1218
|
+
const pressure = (first.pressure ?? 0.5) * pressureSensitivity;
|
|
1144
1219
|
const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
|
|
1145
1220
|
if (currentToolRef.current === "select") {
|
|
1146
1221
|
const hit = getTopShapeAtPoint(editor, { x, y });
|
|
@@ -1187,7 +1262,8 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1187
1262
|
refreshSelectionBounds(editor);
|
|
1188
1263
|
};
|
|
1189
1264
|
const handlePointerMove = (e) => {
|
|
1190
|
-
|
|
1265
|
+
const penAutoDetectOnMove = penOptionsRef.current?.autoDetect !== false;
|
|
1266
|
+
if (penAutoDetectOnMove && !penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1191
1267
|
penDetectedRef.current = true;
|
|
1192
1268
|
penModeRef.current = true;
|
|
1193
1269
|
}
|
|
@@ -1202,9 +1278,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1202
1278
|
const dx = prevClient ? e.clientX - prevClient.x : 0;
|
|
1203
1279
|
const dy = prevClient ? e.clientY - prevClient.y : 0;
|
|
1204
1280
|
lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
|
|
1281
|
+
const movePressureSensitivity = penOptionsRef.current?.pressureSensitivity ?? 1;
|
|
1205
1282
|
for (const sample of sampleEvents(e)) {
|
|
1206
1283
|
const { x, y } = getPagePoint(sample);
|
|
1207
|
-
const pressure = sample.pressure ?? 0.5;
|
|
1284
|
+
const pressure = (sample.pressure ?? 0.5) * movePressureSensitivity;
|
|
1208
1285
|
const isPen = sample.pointerType === "pen" || hasRealPressure(sample.pressure);
|
|
1209
1286
|
editor.input.pointerMove(x, y, pressure, isPen);
|
|
1210
1287
|
}
|
|
@@ -1338,14 +1415,18 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1338
1415
|
editor.tools.pointerUp();
|
|
1339
1416
|
render();
|
|
1340
1417
|
refreshSelectionBounds(editor);
|
|
1341
|
-
if (handPanSession) {
|
|
1418
|
+
if (handPanSession && cameraOptionsRef.current?.slideEnabled !== false) {
|
|
1342
1419
|
activeCameraSlideRef.current = startCameraSlide(
|
|
1343
1420
|
handPanSession,
|
|
1344
|
-
(slideDx, slideDy) =>
|
|
1421
|
+
(slideDx, slideDy) => {
|
|
1422
|
+
editor.panBy(slideDx, slideDy);
|
|
1423
|
+
emitCameraChange();
|
|
1424
|
+
},
|
|
1345
1425
|
() => {
|
|
1346
1426
|
render();
|
|
1347
1427
|
refreshSelectionBounds(editor);
|
|
1348
|
-
}
|
|
1428
|
+
},
|
|
1429
|
+
{ friction: cameraOptionsRef.current?.slideFriction }
|
|
1349
1430
|
);
|
|
1350
1431
|
}
|
|
1351
1432
|
if (pendingRemoteDocumentRef.current) {
|
|
@@ -1390,41 +1471,59 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1390
1471
|
const handleWheel = (e) => {
|
|
1391
1472
|
if (!container.contains(e.target)) return;
|
|
1392
1473
|
e.preventDefault();
|
|
1474
|
+
const camOpts = cameraOptionsRef.current;
|
|
1475
|
+
if (camOpts?.locked) return;
|
|
1476
|
+
if (camOpts?.wheelBehavior === "none") return;
|
|
1393
1477
|
if (touchInteractions.isTrackpadZoomActive()) return;
|
|
1394
1478
|
const delta = normalizeWheelDelta(e);
|
|
1479
|
+
const panMultiplier = camOpts?.panSpeed ?? 1;
|
|
1480
|
+
const zoomMultiplier = camOpts?.zoomSpeed ?? 1;
|
|
1395
1481
|
if (delta.z !== 0) {
|
|
1396
1482
|
const rect = canvas.getBoundingClientRect();
|
|
1397
1483
|
const pointX = e.clientX - rect.left;
|
|
1398
1484
|
const pointY = e.clientY - rect.top;
|
|
1399
|
-
editor.zoomAt(Math.exp(delta.z), pointX, pointY);
|
|
1485
|
+
editor.zoomAt(Math.exp(delta.z * zoomMultiplier), pointX, pointY);
|
|
1400
1486
|
} else {
|
|
1401
|
-
editor.panBy(delta.x, delta.y);
|
|
1487
|
+
editor.panBy(delta.x * panMultiplier, delta.y * panMultiplier);
|
|
1402
1488
|
}
|
|
1403
1489
|
render();
|
|
1404
1490
|
refreshSelectionBounds(editor);
|
|
1491
|
+
emitCameraChange();
|
|
1405
1492
|
};
|
|
1406
1493
|
const handleGestureEvent = (e) => {
|
|
1407
1494
|
touchInteractions.handleGestureEvent(e, container);
|
|
1408
1495
|
};
|
|
1409
1496
|
const handleKeyDown = (e) => {
|
|
1497
|
+
if (keyboardShortcutsRef.current?.enabled === false) return;
|
|
1498
|
+
const isReadOnly = readOnlyRef.current;
|
|
1410
1499
|
handleKeyboardShortcutKeyDown(e, {
|
|
1411
|
-
isToolAvailable: (tool) =>
|
|
1500
|
+
isToolAvailable: (tool) => {
|
|
1501
|
+
if (isReadOnly && !VIEW_ONLY_TOOLS.has(tool)) return false;
|
|
1502
|
+
return editor.tools.hasTool(tool);
|
|
1503
|
+
},
|
|
1412
1504
|
setToolFromShortcut: (tool) => {
|
|
1413
1505
|
editor.setCurrentTool(tool);
|
|
1414
1506
|
setCurrentToolState(tool);
|
|
1415
1507
|
currentToolRef.current = tool;
|
|
1416
1508
|
if (tool !== "select") resetSelectUi();
|
|
1417
1509
|
render();
|
|
1510
|
+
onToolChangeRef.current?.(tool);
|
|
1511
|
+
},
|
|
1512
|
+
runHistoryShortcut: (shouldRedo) => {
|
|
1513
|
+
if (isReadOnly) return false;
|
|
1514
|
+
return applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo());
|
|
1515
|
+
},
|
|
1516
|
+
deleteSelection: () => {
|
|
1517
|
+
if (isReadOnly) return false;
|
|
1518
|
+
return currentToolRef.current === "select" ? deleteCurrentSelection() : false;
|
|
1418
1519
|
},
|
|
1419
|
-
runHistoryShortcut: (shouldRedo) => applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo()),
|
|
1420
|
-
deleteSelection: () => currentToolRef.current === "select" ? deleteCurrentSelection() : false,
|
|
1421
1520
|
dispatchKeyDown: (event) => {
|
|
1422
1521
|
editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
|
|
1423
1522
|
editor.tools.keyDown({ key: event.key });
|
|
1424
1523
|
render();
|
|
1425
1524
|
},
|
|
1426
1525
|
dispatchKeyUp: () => void 0
|
|
1427
|
-
});
|
|
1526
|
+
}, toolShortcutMap);
|
|
1428
1527
|
};
|
|
1429
1528
|
const handleKeyUp = (e) => {
|
|
1430
1529
|
handleKeyboardShortcutKeyUp(e, {
|
|
@@ -1505,6 +1604,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1505
1604
|
const cleanupEditorListener = editor.listen(() => {
|
|
1506
1605
|
if (ignorePersistenceChanges) return;
|
|
1507
1606
|
schedulePersist();
|
|
1607
|
+
onChangeRef.current?.(editor.getDocumentSnapshot());
|
|
1508
1608
|
});
|
|
1509
1609
|
const cleanupHistoryListener = editor.listenHistory(() => {
|
|
1510
1610
|
syncHistoryState();
|
|
@@ -1571,6 +1671,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1571
1671
|
render();
|
|
1572
1672
|
}
|
|
1573
1673
|
});
|
|
1674
|
+
if (options.autoFocus !== false) {
|
|
1675
|
+
container.focus({ preventScroll: true });
|
|
1676
|
+
}
|
|
1574
1677
|
return () => {
|
|
1575
1678
|
disposed = true;
|
|
1576
1679
|
schedulePersistRef.current = null;
|
|
@@ -1614,15 +1717,21 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1614
1717
|
editor.renderer.setTheme(options.theme ?? "light");
|
|
1615
1718
|
render();
|
|
1616
1719
|
}, [options.theme, render]);
|
|
1720
|
+
useEffect(() => {
|
|
1721
|
+
if (!editorRef.current) return;
|
|
1722
|
+
render();
|
|
1723
|
+
}, [options.background, render]);
|
|
1617
1724
|
const setTool = useCallback(
|
|
1618
1725
|
(tool) => {
|
|
1619
1726
|
const editor = editorRef.current;
|
|
1620
1727
|
if (!editor) return;
|
|
1621
1728
|
if (!editor.tools.hasTool(tool)) return;
|
|
1729
|
+
if (readOnlyRef.current && !VIEW_ONLY_TOOLS.has(tool)) return;
|
|
1622
1730
|
editor.setCurrentTool(tool);
|
|
1623
1731
|
setCurrentToolState(tool);
|
|
1624
1732
|
currentToolRef.current = tool;
|
|
1625
1733
|
if (tool !== "select") resetSelectUi();
|
|
1734
|
+
onToolChangeRef.current?.(tool);
|
|
1626
1735
|
},
|
|
1627
1736
|
[resetSelectUi]
|
|
1628
1737
|
);
|
|
@@ -1858,12 +1967,23 @@ function Tsdraw(props) {
|
|
|
1858
1967
|
initialTool,
|
|
1859
1968
|
theme: resolvedTheme,
|
|
1860
1969
|
persistenceKey: props.persistenceKey,
|
|
1861
|
-
onMount: props.onMount
|
|
1970
|
+
onMount: props.onMount,
|
|
1971
|
+
cameraOptions: props.cameraOptions,
|
|
1972
|
+
touchOptions: props.touchOptions,
|
|
1973
|
+
keyboardShortcuts: props.keyboardShortcuts,
|
|
1974
|
+
penOptions: props.penOptions,
|
|
1975
|
+
background: props.background,
|
|
1976
|
+
readOnly: props.readOnly,
|
|
1977
|
+
autoFocus: props.autoFocus,
|
|
1978
|
+
snapshot: props.snapshot,
|
|
1979
|
+
onChange: props.onChange,
|
|
1980
|
+
onCameraChange: props.onCameraChange,
|
|
1981
|
+
onToolChange: props.onToolChange
|
|
1862
1982
|
});
|
|
1863
1983
|
const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 14);
|
|
1864
1984
|
const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8, 8);
|
|
1865
1985
|
const isToolbarHidden = props.uiOptions?.toolbar?.hide === true;
|
|
1866
|
-
const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true;
|
|
1986
|
+
const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true || props.readOnly === true;
|
|
1867
1987
|
const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
|
|
1868
1988
|
const defaultToolOverlay = /* @__PURE__ */ jsx(
|
|
1869
1989
|
ToolOverlay,
|
|
@@ -1952,12 +2072,14 @@ function Tsdraw(props) {
|
|
|
1952
2072
|
"div",
|
|
1953
2073
|
{
|
|
1954
2074
|
ref: containerRef,
|
|
2075
|
+
tabIndex: 0,
|
|
1955
2076
|
className: `tsdraw tsdraw-${resolvedTheme}mode ${props.className ?? ""}`,
|
|
1956
2077
|
style: {
|
|
1957
2078
|
width: props.width ?? "100%",
|
|
1958
2079
|
height: props.height ?? "100%",
|
|
1959
2080
|
position: "relative",
|
|
1960
2081
|
overflow: "hidden",
|
|
2082
|
+
outline: "none",
|
|
1961
2083
|
...props.style
|
|
1962
2084
|
},
|
|
1963
2085
|
children: [
|