@tsdraw/react 0.6.2 → 0.8.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 +563 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -4
- package/dist/index.d.ts +27 -4
- package/dist/index.js +565 -109
- package/dist/index.js.map +1 -1
- package/dist/tsdraw.css +39 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, getTopShapeAtPoint, buildStartPositions, applyRotation, applyResize, applyMove, normalizeSelectionBounds, getShapesInBounds, isSelectTool } from '@tsdraw/core';
|
|
4
|
-
import { IconPointer, IconPencil, IconEraser, IconHandStop, IconArrowBackUp, IconArrowForwardUp } from '@tabler/icons-react';
|
|
3
|
+
import { DEFAULT_COLORS, getSelectionBoundsPage, buildTransformSnapshots, Editor, STROKE_WIDTHS, ERASER_MARGIN, resolveThemeColor, pageToScreen, getTopShapeAtPoint, buildStartPositions, applyRotation, applyResize, applyMove, normalizeSelectionBounds, getShapesInBounds, isSelectTool } from '@tsdraw/core';
|
|
4
|
+
import { IconPointer, IconPencil, IconSquare, IconCircle, IconEraser, IconHandStop, IconArrowBackUp, IconArrowForwardUp } from '@tabler/icons-react';
|
|
5
5
|
|
|
6
6
|
// src/components/TsdrawCanvas.tsx
|
|
7
7
|
function SelectionOverlay({
|
|
@@ -98,66 +98,106 @@ function SelectionOverlay({
|
|
|
98
98
|
}
|
|
99
99
|
var STYLE_COLORS = Object.entries(DEFAULT_COLORS).filter(([key]) => key !== "white").map(([value]) => ({ value }));
|
|
100
100
|
var STYLE_DASHES = ["draw", "solid", "dashed", "dotted"];
|
|
101
|
+
var STYLE_FILLS = ["none", "blank", "semi", "solid"];
|
|
101
102
|
var STYLE_SIZES = ["s", "m", "l", "xl"];
|
|
102
103
|
function StylePanel({
|
|
103
104
|
visible,
|
|
105
|
+
parts,
|
|
106
|
+
customParts,
|
|
104
107
|
style,
|
|
105
108
|
theme,
|
|
106
109
|
drawColor,
|
|
107
110
|
drawDash,
|
|
111
|
+
drawFill,
|
|
108
112
|
drawSize,
|
|
109
113
|
onColorSelect,
|
|
110
114
|
onDashSelect,
|
|
115
|
+
onFillSelect,
|
|
111
116
|
onSizeSelect
|
|
112
117
|
}) {
|
|
113
|
-
if (!visible) return null;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
118
|
+
if (!visible || parts.length === 0) return null;
|
|
119
|
+
const context = {
|
|
120
|
+
drawColor,
|
|
121
|
+
drawDash,
|
|
122
|
+
drawFill,
|
|
123
|
+
drawSize,
|
|
124
|
+
onColorSelect,
|
|
125
|
+
onDashSelect,
|
|
126
|
+
onFillSelect,
|
|
127
|
+
onSizeSelect
|
|
128
|
+
};
|
|
129
|
+
const customPartMap = new Map((customParts ?? []).map((customPart) => [customPart.id, customPart]));
|
|
130
|
+
return /* @__PURE__ */ jsx("div", { className: "tsdraw-style-panel", style, "aria-label": "Draw style panel", children: parts.map((part) => {
|
|
131
|
+
if (part === "colors") {
|
|
132
|
+
return /* @__PURE__ */ jsx("div", { className: "tsdraw-style-colors", children: STYLE_COLORS.map((item) => /* @__PURE__ */ jsx(
|
|
133
|
+
"button",
|
|
134
|
+
{
|
|
135
|
+
type: "button",
|
|
136
|
+
className: "tsdraw-style-color",
|
|
137
|
+
"data-active": drawColor === item.value ? "true" : void 0,
|
|
138
|
+
"aria-label": `Color ${item.value}`,
|
|
139
|
+
title: item.value,
|
|
140
|
+
onClick: () => onColorSelect(item.value),
|
|
141
|
+
children: /* @__PURE__ */ jsx(
|
|
142
|
+
"span",
|
|
143
|
+
{
|
|
144
|
+
className: "tsdraw-style-color-dot",
|
|
145
|
+
style: { background: resolveThemeColor(item.value, theme) }
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
},
|
|
149
|
+
item.value
|
|
150
|
+
)) }, part);
|
|
151
|
+
}
|
|
152
|
+
if (part === "dashes") {
|
|
153
|
+
return /* @__PURE__ */ jsx("div", { className: "tsdraw-style-section", children: STYLE_DASHES.map((dash) => /* @__PURE__ */ jsx(
|
|
154
|
+
"button",
|
|
155
|
+
{
|
|
156
|
+
type: "button",
|
|
157
|
+
className: "tsdraw-style-row",
|
|
158
|
+
"data-active": drawDash === dash ? "true" : void 0,
|
|
159
|
+
"aria-label": `Stroke ${dash}`,
|
|
160
|
+
title: dash,
|
|
161
|
+
onClick: () => onDashSelect(dash),
|
|
162
|
+
children: /* @__PURE__ */ jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsx("span", { className: `tsdraw-style-preview-line tsdraw-style-preview-line--${dash}` }) })
|
|
163
|
+
},
|
|
164
|
+
dash
|
|
165
|
+
)) }, part);
|
|
166
|
+
}
|
|
167
|
+
if (part === "fills") {
|
|
168
|
+
return /* @__PURE__ */ jsx("div", { className: "tsdraw-style-section", children: STYLE_FILLS.map((fill) => /* @__PURE__ */ jsx(
|
|
169
|
+
"button",
|
|
170
|
+
{
|
|
171
|
+
type: "button",
|
|
172
|
+
className: "tsdraw-style-row",
|
|
173
|
+
"data-active": drawFill === fill ? "true" : void 0,
|
|
174
|
+
"aria-label": `Fill ${fill}`,
|
|
175
|
+
title: fill,
|
|
176
|
+
onClick: () => onFillSelect(fill),
|
|
177
|
+
children: /* @__PURE__ */ jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsx("span", { className: `tsdraw-style-fill tsdraw-style-fill--${fill}` }) })
|
|
178
|
+
},
|
|
179
|
+
fill
|
|
180
|
+
)) }, part);
|
|
181
|
+
}
|
|
182
|
+
if (part === "sizes") {
|
|
183
|
+
return /* @__PURE__ */ jsx("div", { className: "tsdraw-style-section", children: STYLE_SIZES.map((size) => /* @__PURE__ */ jsx(
|
|
184
|
+
"button",
|
|
185
|
+
{
|
|
186
|
+
type: "button",
|
|
187
|
+
className: "tsdraw-style-row",
|
|
188
|
+
"data-active": drawSize === size ? "true" : void 0,
|
|
189
|
+
"aria-label": `Thickness ${size}`,
|
|
190
|
+
title: size,
|
|
191
|
+
onClick: () => onSizeSelect(size),
|
|
192
|
+
children: /* @__PURE__ */ jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsx("span", { className: `tsdraw-style-size tsdraw-style-size--${size}` }) })
|
|
193
|
+
},
|
|
194
|
+
size
|
|
195
|
+
)) }, part);
|
|
196
|
+
}
|
|
197
|
+
const customPart = customPartMap.get(part);
|
|
198
|
+
if (!customPart) return null;
|
|
199
|
+
return /* @__PURE__ */ jsx("div", { className: "tsdraw-style-section tsdraw-style-section--custom", children: customPart.render(context) }, part);
|
|
200
|
+
}) });
|
|
161
201
|
}
|
|
162
202
|
function ToolOverlay({
|
|
163
203
|
visible,
|
|
@@ -197,6 +237,8 @@ function ToolOverlay({
|
|
|
197
237
|
function getDefaultToolbarIcon(toolId, isActive) {
|
|
198
238
|
if (toolId === "select") return /* @__PURE__ */ jsx(IconPointer, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
|
|
199
239
|
if (toolId === "pen") return /* @__PURE__ */ jsx(IconPencil, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
|
|
240
|
+
if (toolId === "square") return /* @__PURE__ */ jsx(IconSquare, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
|
|
241
|
+
if (toolId === "circle") return /* @__PURE__ */ jsx(IconCircle, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
|
|
200
242
|
if (toolId === "eraser") return /* @__PURE__ */ jsx(IconEraser, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
|
|
201
243
|
if (toolId === "hand") return /* @__PURE__ */ jsx(IconHandStop, { size: 16, stroke: isActive ? 1 : 1.8, fill: isActive ? "currentColor" : "none", style: isActive ? { stroke: "#000000" } : void 0 });
|
|
202
244
|
return null;
|
|
@@ -253,6 +295,249 @@ function getCanvasCursor(currentTool, state) {
|
|
|
253
295
|
return state.showToolOverlay ? "none" : "crosshair";
|
|
254
296
|
}
|
|
255
297
|
|
|
298
|
+
// src/canvas/touchInteractions.ts
|
|
299
|
+
var TAP_MAX_DURATION_MS = 100;
|
|
300
|
+
var DOUBLE_TAP_INTERVAL_MS = 100;
|
|
301
|
+
var TAP_MOVE_TOLERANCE = 14;
|
|
302
|
+
var PINCH_MODE_ZOOM_DISTANCE = 24;
|
|
303
|
+
var PINCH_MODE_PAN_DISTANCE = 16;
|
|
304
|
+
var PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE = 64;
|
|
305
|
+
function createTouchInteractionController(editor, canvas, handlers) {
|
|
306
|
+
const activeTouchPoints = /* @__PURE__ */ new Map();
|
|
307
|
+
const touchTapState = {
|
|
308
|
+
active: false,
|
|
309
|
+
startTime: 0,
|
|
310
|
+
maxTouchCount: 0,
|
|
311
|
+
moved: false,
|
|
312
|
+
startPoints: /* @__PURE__ */ new Map(),
|
|
313
|
+
lastTapAtByCount: {}
|
|
314
|
+
};
|
|
315
|
+
const touchCameraState = {
|
|
316
|
+
active: false,
|
|
317
|
+
mode: "not-sure",
|
|
318
|
+
previousCenter: { x: 0, y: 0 },
|
|
319
|
+
initialCenter: { x: 0, y: 0 },
|
|
320
|
+
previousDistance: 1,
|
|
321
|
+
initialDistance: 1,
|
|
322
|
+
previousAngle: 0
|
|
323
|
+
};
|
|
324
|
+
const isTouchPointer = (event) => event.pointerType === "touch";
|
|
325
|
+
const endTouchCameraGesture = () => {
|
|
326
|
+
touchCameraState.active = false;
|
|
327
|
+
touchCameraState.mode = "not-sure";
|
|
328
|
+
touchCameraState.previousDistance = 1;
|
|
329
|
+
touchCameraState.initialDistance = 1;
|
|
330
|
+
touchCameraState.previousAngle = 0;
|
|
331
|
+
};
|
|
332
|
+
const maybeHandleTouchTapGesture = () => {
|
|
333
|
+
if (activeTouchPoints.size > 0) return;
|
|
334
|
+
if (!touchTapState.active) return;
|
|
335
|
+
const elapsed = performance.now() - touchTapState.startTime;
|
|
336
|
+
if (!touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
|
|
337
|
+
const fingerCount = touchTapState.maxTouchCount;
|
|
338
|
+
const now = performance.now();
|
|
339
|
+
const previousTapTime = touchTapState.lastTapAtByCount[fingerCount] ?? 0;
|
|
340
|
+
const isDoubleTap = previousTapTime > 0 && now - previousTapTime <= DOUBLE_TAP_INTERVAL_MS;
|
|
341
|
+
if (isDoubleTap) {
|
|
342
|
+
touchTapState.lastTapAtByCount[fingerCount] = 0;
|
|
343
|
+
if (fingerCount === 2) {
|
|
344
|
+
if (handlers.runUndo()) handlers.refreshView();
|
|
345
|
+
} else if (handlers.runRedo()) handlers.refreshView();
|
|
346
|
+
} else touchTapState.lastTapAtByCount[fingerCount] = now;
|
|
347
|
+
}
|
|
348
|
+
touchTapState.active = false;
|
|
349
|
+
touchTapState.startPoints.clear();
|
|
350
|
+
touchTapState.maxTouchCount = 0;
|
|
351
|
+
touchTapState.moved = false;
|
|
352
|
+
};
|
|
353
|
+
const beginTouchCameraGesture = () => {
|
|
354
|
+
const points = [...activeTouchPoints.values()];
|
|
355
|
+
if (points.length !== 2) return;
|
|
356
|
+
handlers.cancelActivePointerInteraction();
|
|
357
|
+
const first = points[0];
|
|
358
|
+
const second = points[1];
|
|
359
|
+
const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
|
360
|
+
const distance = Math.hypot(second.x - first.x, second.y - first.y);
|
|
361
|
+
const angle = Math.atan2(second.y - first.y, second.x - first.x);
|
|
362
|
+
touchCameraState.active = true;
|
|
363
|
+
touchCameraState.mode = "not-sure";
|
|
364
|
+
touchCameraState.previousCenter = center;
|
|
365
|
+
touchCameraState.initialCenter = center;
|
|
366
|
+
touchCameraState.previousDistance = Math.max(1, distance);
|
|
367
|
+
touchCameraState.initialDistance = Math.max(1, distance);
|
|
368
|
+
touchCameraState.previousAngle = angle;
|
|
369
|
+
};
|
|
370
|
+
const updateTouchCameraGesture = () => {
|
|
371
|
+
if (!touchCameraState.active) return false;
|
|
372
|
+
const points = [...activeTouchPoints.values()];
|
|
373
|
+
if (points.length !== 2) {
|
|
374
|
+
endTouchCameraGesture();
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const first = points[0];
|
|
378
|
+
const second = points[1];
|
|
379
|
+
const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
|
380
|
+
const distance = Math.max(1, Math.hypot(second.x - first.x, second.y - first.y));
|
|
381
|
+
const angle = Math.atan2(second.y - first.y, second.x - first.x);
|
|
382
|
+
const centerDx = center.x - touchCameraState.previousCenter.x;
|
|
383
|
+
const centerDy = center.y - touchCameraState.previousCenter.y;
|
|
384
|
+
const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
|
|
385
|
+
const originDistance = Math.hypot(center.x - touchCameraState.initialCenter.x, center.y - touchCameraState.initialCenter.y);
|
|
386
|
+
if (touchCameraState.mode === "not-sure") {
|
|
387
|
+
if (touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
388
|
+
else if (originDistance > PINCH_MODE_PAN_DISTANCE) touchCameraState.mode = "panning";
|
|
389
|
+
} else if (touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
|
|
390
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
391
|
+
const centerOnCanvasX = center.x - canvasRect.left;
|
|
392
|
+
const centerOnCanvasY = center.y - canvasRect.top;
|
|
393
|
+
editor.panBy(centerDx, centerDy);
|
|
394
|
+
if (touchCameraState.mode === "zooming") {
|
|
395
|
+
const zoomFactor = distance / touchCameraState.previousDistance;
|
|
396
|
+
editor.zoomAt(zoomFactor, centerOnCanvasX, centerOnCanvasY);
|
|
397
|
+
editor.rotateAt(angle - touchCameraState.previousAngle, centerOnCanvasX, centerOnCanvasY);
|
|
398
|
+
}
|
|
399
|
+
touchCameraState.previousCenter = center;
|
|
400
|
+
touchCameraState.previousDistance = distance;
|
|
401
|
+
touchCameraState.previousAngle = angle;
|
|
402
|
+
handlers.refreshView();
|
|
403
|
+
return true;
|
|
404
|
+
};
|
|
405
|
+
const handlePointerDown = (event) => {
|
|
406
|
+
if (!isTouchPointer(event)) return false;
|
|
407
|
+
activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
408
|
+
if (!touchTapState.active) {
|
|
409
|
+
touchTapState.active = true;
|
|
410
|
+
touchTapState.startTime = performance.now();
|
|
411
|
+
touchTapState.maxTouchCount = activeTouchPoints.size;
|
|
412
|
+
touchTapState.moved = false;
|
|
413
|
+
touchTapState.startPoints.clear();
|
|
414
|
+
} else {
|
|
415
|
+
touchTapState.maxTouchCount = Math.max(touchTapState.maxTouchCount, activeTouchPoints.size);
|
|
416
|
+
}
|
|
417
|
+
touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
418
|
+
if (activeTouchPoints.size === 2) {
|
|
419
|
+
beginTouchCameraGesture();
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
};
|
|
424
|
+
const handlePointerMove = (event) => {
|
|
425
|
+
if (!isTouchPointer(event)) return false;
|
|
426
|
+
if (activeTouchPoints.has(event.pointerId)) activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
427
|
+
const tapStart = touchTapState.startPoints.get(event.pointerId);
|
|
428
|
+
if (tapStart) {
|
|
429
|
+
const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
|
|
430
|
+
if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
|
|
431
|
+
}
|
|
432
|
+
return updateTouchCameraGesture();
|
|
433
|
+
};
|
|
434
|
+
const handlePointerUpOrCancel = (event) => {
|
|
435
|
+
if (!isTouchPointer(event)) return false;
|
|
436
|
+
const wasCameraGestureActive = touchCameraState.active;
|
|
437
|
+
activeTouchPoints.delete(event.pointerId);
|
|
438
|
+
touchTapState.startPoints.delete(event.pointerId);
|
|
439
|
+
if (activeTouchPoints.size < 2) endTouchCameraGesture();
|
|
440
|
+
maybeHandleTouchTapGesture();
|
|
441
|
+
return wasCameraGestureActive;
|
|
442
|
+
};
|
|
443
|
+
let gestureLastScale = 1;
|
|
444
|
+
let gestureActive = false;
|
|
445
|
+
const handleGestureEvent = (event, container) => {
|
|
446
|
+
if (!container.contains(event.target)) return;
|
|
447
|
+
event.preventDefault();
|
|
448
|
+
const gestureEvent = event;
|
|
449
|
+
if (gestureEvent.scale == null) return;
|
|
450
|
+
if (event.type === "gesturestart") {
|
|
451
|
+
gestureLastScale = gestureEvent.scale;
|
|
452
|
+
gestureActive = true;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (event.type === "gestureend") {
|
|
456
|
+
gestureActive = false;
|
|
457
|
+
gestureLastScale = 1;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (event.type === "gesturechange" && gestureActive) {
|
|
461
|
+
const zoomFactor = gestureEvent.scale / gestureLastScale;
|
|
462
|
+
gestureLastScale = gestureEvent.scale;
|
|
463
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
464
|
+
const cx = (gestureEvent.clientX ?? canvasRect.left + canvasRect.width / 2) - canvasRect.left;
|
|
465
|
+
const cy = (gestureEvent.clientY ?? canvasRect.top + canvasRect.height / 2) - canvasRect.top;
|
|
466
|
+
editor.zoomAt(zoomFactor, cx, cy);
|
|
467
|
+
handlers.refreshView();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const reset = () => {
|
|
471
|
+
activeTouchPoints.clear();
|
|
472
|
+
touchTapState.active = false;
|
|
473
|
+
touchTapState.startPoints.clear();
|
|
474
|
+
endTouchCameraGesture();
|
|
475
|
+
};
|
|
476
|
+
return {
|
|
477
|
+
handlePointerDown,
|
|
478
|
+
handlePointerMove,
|
|
479
|
+
handlePointerUpOrCancel,
|
|
480
|
+
handleGestureEvent,
|
|
481
|
+
reset,
|
|
482
|
+
isCameraGestureActive: () => touchCameraState.active,
|
|
483
|
+
isTrackpadZoomActive: () => gestureActive
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/canvas/keyboardShortcuts.ts
|
|
488
|
+
var TOOL_SHORTCUTS = {
|
|
489
|
+
v: "select",
|
|
490
|
+
h: "hand",
|
|
491
|
+
e: "eraser",
|
|
492
|
+
p: "pen",
|
|
493
|
+
b: "pen",
|
|
494
|
+
d: "pen",
|
|
495
|
+
x: "pen",
|
|
496
|
+
r: "square",
|
|
497
|
+
o: "circle",
|
|
498
|
+
c: "circle"
|
|
499
|
+
};
|
|
500
|
+
function isEditableTarget(eventTarget) {
|
|
501
|
+
const element = eventTarget;
|
|
502
|
+
if (!element) return false;
|
|
503
|
+
if (element.isContentEditable) return true;
|
|
504
|
+
const tagName = element.tagName;
|
|
505
|
+
return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
|
|
506
|
+
}
|
|
507
|
+
function handleKeyboardShortcutKeyDown(event, handlers) {
|
|
508
|
+
if (isEditableTarget(event.target)) return;
|
|
509
|
+
const loweredKey = event.key.toLowerCase();
|
|
510
|
+
const isMetaPressed = event.metaKey || event.ctrlKey;
|
|
511
|
+
if (isMetaPressed && (loweredKey === "z" || loweredKey === "y")) {
|
|
512
|
+
const shouldRedo = loweredKey === "y" || loweredKey === "z" && event.shiftKey;
|
|
513
|
+
if (handlers.runHistoryShortcut(shouldRedo)) {
|
|
514
|
+
event.preventDefault();
|
|
515
|
+
event.stopPropagation();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (!isMetaPressed && !event.altKey) {
|
|
520
|
+
const nextToolId = TOOL_SHORTCUTS[loweredKey];
|
|
521
|
+
if (nextToolId && handlers.isToolAvailable(nextToolId)) {
|
|
522
|
+
handlers.setToolFromShortcut(nextToolId);
|
|
523
|
+
event.preventDefault();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (event.key === "Delete" || event.key === "Backspace") {
|
|
528
|
+
if (handlers.deleteSelection()) {
|
|
529
|
+
event.preventDefault();
|
|
530
|
+
event.stopPropagation();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
handlers.dispatchKeyDown(event);
|
|
535
|
+
}
|
|
536
|
+
function handleKeyboardShortcutKeyUp(event, handlers) {
|
|
537
|
+
if (isEditableTarget(event.target)) return;
|
|
538
|
+
handlers.dispatchKeyUp(event);
|
|
539
|
+
}
|
|
540
|
+
|
|
256
541
|
// src/persistence/localIndexedDb.ts
|
|
257
542
|
var DATABASE_PREFIX = "tsdraw_v1_";
|
|
258
543
|
var DATABASE_VERSION = 2;
|
|
@@ -376,25 +661,35 @@ function getOrCreateSessionId() {
|
|
|
376
661
|
|
|
377
662
|
// src/canvas/useTsdrawCanvasController.ts
|
|
378
663
|
function toScreenRect(editor, bounds) {
|
|
379
|
-
const
|
|
664
|
+
const topLeft = pageToScreen(editor.viewport, bounds.minX, bounds.minY);
|
|
665
|
+
const topRight = pageToScreen(editor.viewport, bounds.maxX, bounds.minY);
|
|
666
|
+
const bottomLeft = pageToScreen(editor.viewport, bounds.minX, bounds.maxY);
|
|
667
|
+
const bottomRight = pageToScreen(editor.viewport, bounds.maxX, bounds.maxY);
|
|
668
|
+
const minX = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
|
|
669
|
+
const minY = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
|
|
670
|
+
const maxX = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
|
|
671
|
+
const maxY = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
|
|
380
672
|
return {
|
|
381
|
-
left:
|
|
382
|
-
top:
|
|
383
|
-
width: (
|
|
384
|
-
height: (
|
|
673
|
+
left: minX,
|
|
674
|
+
top: minY,
|
|
675
|
+
width: Math.max(0, maxX - minX),
|
|
676
|
+
height: Math.max(0, maxY - minY)
|
|
385
677
|
};
|
|
386
678
|
}
|
|
387
679
|
function resolveDrawColor(colorStyle, theme) {
|
|
388
680
|
return resolveThemeColor(colorStyle, theme);
|
|
389
681
|
}
|
|
682
|
+
var ZOOM_WHEEL_CAP = 10;
|
|
390
683
|
function useTsdrawCanvasController(options = {}) {
|
|
391
|
-
const stylePanelToolIds = options.stylePanelToolIds ?? ["pen"];
|
|
392
|
-
const stylePanelToolIdsRef = useRef(stylePanelToolIds);
|
|
393
684
|
const onMountRef = useRef(options.onMount);
|
|
394
685
|
const containerRef = useRef(null);
|
|
395
686
|
const canvasRef = useRef(null);
|
|
396
687
|
const editorRef = useRef(null);
|
|
397
688
|
const dprRef = useRef(1);
|
|
689
|
+
const penDetectedRef = useRef(false);
|
|
690
|
+
const penModeRef = useRef(false);
|
|
691
|
+
const lastPointerDownWithRef = useRef("mouse");
|
|
692
|
+
const activePointerIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
398
693
|
const lastPointerClientRef = useRef(null);
|
|
399
694
|
const currentToolRef = useRef(options.initialTool ?? "pen");
|
|
400
695
|
const selectedShapeIdsRef = useRef([]);
|
|
@@ -424,6 +719,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
424
719
|
const [currentTool, setCurrentToolState] = useState(options.initialTool ?? "pen");
|
|
425
720
|
const [drawColor, setDrawColor] = useState("black");
|
|
426
721
|
const [drawDash, setDrawDash] = useState("draw");
|
|
722
|
+
const [drawFill, setDrawFill] = useState("none");
|
|
427
723
|
const [drawSize, setDrawSize] = useState("m");
|
|
428
724
|
const [selectedShapeIds, setSelectedShapeIds] = useState([]);
|
|
429
725
|
const [selectionBrush, setSelectionBrush] = useState(null);
|
|
@@ -440,9 +736,6 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
440
736
|
useEffect(() => {
|
|
441
737
|
currentToolRef.current = currentTool;
|
|
442
738
|
}, [currentTool]);
|
|
443
|
-
useEffect(() => {
|
|
444
|
-
stylePanelToolIdsRef.current = stylePanelToolIds;
|
|
445
|
-
}, [stylePanelToolIds]);
|
|
446
739
|
useEffect(() => {
|
|
447
740
|
onMountRef.current = options.onMount;
|
|
448
741
|
}, [options.onMount]);
|
|
@@ -454,7 +747,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
454
747
|
}, [selectionRotationDeg]);
|
|
455
748
|
useEffect(() => {
|
|
456
749
|
schedulePersistRef.current?.();
|
|
457
|
-
}, [selectedShapeIds, currentTool, drawColor, drawDash, drawSize]);
|
|
750
|
+
}, [selectedShapeIds, currentTool, drawColor, drawDash, drawFill, drawSize]);
|
|
458
751
|
const render = useCallback(() => {
|
|
459
752
|
const canvas = canvasRef.current;
|
|
460
753
|
const editor = editorRef.current;
|
|
@@ -590,6 +883,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
590
883
|
const initialStyle = editor.getCurrentDrawStyle();
|
|
591
884
|
setDrawColor(initialStyle.color);
|
|
592
885
|
setDrawDash(initialStyle.dash);
|
|
886
|
+
setDrawFill(initialStyle.fill);
|
|
593
887
|
setDrawSize(initialStyle.size);
|
|
594
888
|
const resize = () => {
|
|
595
889
|
const dpr = window.devicePixelRatio ?? 1;
|
|
@@ -652,9 +946,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
652
946
|
}
|
|
653
947
|
refreshSelectionBounds(editor, nextSelectedShapeIds);
|
|
654
948
|
};
|
|
655
|
-
const applyRemoteDocumentSnapshot = (
|
|
949
|
+
const applyRemoteDocumentSnapshot = (document2) => {
|
|
656
950
|
ignorePersistenceChanges = true;
|
|
657
|
-
editor.loadDocumentSnapshot(
|
|
951
|
+
editor.loadDocumentSnapshot(document2);
|
|
658
952
|
editor.clearRedoHistory();
|
|
659
953
|
reconcileSelectionAfterDocumentLoad();
|
|
660
954
|
render();
|
|
@@ -671,6 +965,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
671
965
|
const nextDrawStyle = editor.getCurrentDrawStyle();
|
|
672
966
|
setDrawColor(nextDrawStyle.color);
|
|
673
967
|
setDrawDash(nextDrawStyle.dash);
|
|
968
|
+
setDrawFill(nextDrawStyle.fill);
|
|
674
969
|
setDrawSize(nextDrawStyle.size);
|
|
675
970
|
setSelectionRotationDeg(0);
|
|
676
971
|
render();
|
|
@@ -685,8 +980,95 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
685
980
|
const coalesced = e.getCoalescedEvents?.();
|
|
686
981
|
return coalesced && coalesced.length > 0 ? coalesced : [e];
|
|
687
982
|
};
|
|
983
|
+
const applyDocumentChangeResult = (changed) => {
|
|
984
|
+
if (!changed) return false;
|
|
985
|
+
reconcileSelectionAfterDocumentLoad();
|
|
986
|
+
setSelectionRotationDeg(0);
|
|
987
|
+
render();
|
|
988
|
+
syncHistoryState();
|
|
989
|
+
return true;
|
|
990
|
+
};
|
|
991
|
+
const normalizeWheelDelta = (event) => {
|
|
992
|
+
let deltaX = event.deltaX;
|
|
993
|
+
let deltaY = event.deltaY;
|
|
994
|
+
let deltaZoom = 0;
|
|
995
|
+
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
996
|
+
const clamped = Math.abs(deltaY) > ZOOM_WHEEL_CAP ? ZOOM_WHEEL_CAP * Math.sign(deltaY) : deltaY;
|
|
997
|
+
deltaZoom = -clamped / 100;
|
|
998
|
+
} else if (event.shiftKey && !navigator.userAgent.includes("Mac") && !navigator.userAgent.includes("iPhone") && !navigator.userAgent.includes("iPad")) {
|
|
999
|
+
deltaX = deltaY;
|
|
1000
|
+
deltaY = 0;
|
|
1001
|
+
}
|
|
1002
|
+
return { x: -deltaX, y: -deltaY, z: deltaZoom };
|
|
1003
|
+
};
|
|
1004
|
+
const deleteCurrentSelection = () => {
|
|
1005
|
+
const selectedIds = selectedShapeIdsRef.current;
|
|
1006
|
+
if (selectedIds.length === 0) return false;
|
|
1007
|
+
editor.beginHistoryEntry();
|
|
1008
|
+
editor.deleteShapes(selectedIds);
|
|
1009
|
+
editor.endHistoryEntry();
|
|
1010
|
+
setSelectedShapeIds([]);
|
|
1011
|
+
selectedShapeIdsRef.current = [];
|
|
1012
|
+
setSelectionBounds(null);
|
|
1013
|
+
setSelectionBrush(null);
|
|
1014
|
+
setSelectionRotationDeg(0);
|
|
1015
|
+
render();
|
|
1016
|
+
syncHistoryState();
|
|
1017
|
+
return true;
|
|
1018
|
+
};
|
|
1019
|
+
const cancelActivePointerInteraction = () => {
|
|
1020
|
+
if (!isPointerActiveRef.current) return;
|
|
1021
|
+
isPointerActiveRef.current = false;
|
|
1022
|
+
lastPointerClientRef.current = null;
|
|
1023
|
+
editor.input.pointerUp();
|
|
1024
|
+
if (currentToolRef.current === "select") {
|
|
1025
|
+
const dragMode = selectDragRef.current.mode;
|
|
1026
|
+
if (dragMode === "rotate") setIsRotatingSelection(false);
|
|
1027
|
+
if (dragMode === "resize") setIsResizingSelection(false);
|
|
1028
|
+
if (dragMode === "move") setIsMovingSelection(false);
|
|
1029
|
+
if (dragMode === "marquee") setSelectionBrush(null);
|
|
1030
|
+
selectDragRef.current.mode = "none";
|
|
1031
|
+
} else {
|
|
1032
|
+
editor.tools.pointerUp();
|
|
1033
|
+
}
|
|
1034
|
+
editor.endHistoryEntry();
|
|
1035
|
+
render();
|
|
1036
|
+
refreshSelectionBounds(editor);
|
|
1037
|
+
};
|
|
1038
|
+
const touchInteractions = createTouchInteractionController(editor, canvas, {
|
|
1039
|
+
cancelActivePointerInteraction,
|
|
1040
|
+
refreshView: () => {
|
|
1041
|
+
render();
|
|
1042
|
+
refreshSelectionBounds(editor);
|
|
1043
|
+
},
|
|
1044
|
+
runUndo: () => applyDocumentChangeResult(editor.undo()),
|
|
1045
|
+
runRedo: () => applyDocumentChangeResult(editor.redo())
|
|
1046
|
+
});
|
|
1047
|
+
const isDrawingTool = (tool) => tool !== "select" && tool !== "hand";
|
|
1048
|
+
const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
|
|
688
1049
|
const handlePointerDown = (e) => {
|
|
689
1050
|
if (!canvas.contains(e.target)) return;
|
|
1051
|
+
if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1052
|
+
penDetectedRef.current = true;
|
|
1053
|
+
penModeRef.current = true;
|
|
1054
|
+
}
|
|
1055
|
+
lastPointerDownWithRef.current = e.pointerType;
|
|
1056
|
+
activePointerIdsRef.current.add(e.pointerId);
|
|
1057
|
+
const startedCameraGesture = touchInteractions.handlePointerDown(e);
|
|
1058
|
+
if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
if (!canvas.hasPointerCapture(e.pointerId)) {
|
|
1061
|
+
canvas.setPointerCapture(e.pointerId);
|
|
1062
|
+
}
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const allowPointerDown = !penModeRef.current || e.pointerType !== "touch" || !isDrawingTool(currentToolRef.current);
|
|
1066
|
+
if (!allowPointerDown) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (activePointerIdsRef.current.size > 1) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
690
1072
|
isPointerActiveRef.current = true;
|
|
691
1073
|
editor.beginHistoryEntry();
|
|
692
1074
|
canvas.setPointerCapture(e.pointerId);
|
|
@@ -695,7 +1077,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
695
1077
|
const first = sampleEvents(e)[0];
|
|
696
1078
|
const { x, y } = getPagePoint(first);
|
|
697
1079
|
const pressure = first.pressure ?? 0.5;
|
|
698
|
-
const isPen = first.pointerType === "pen" || first.
|
|
1080
|
+
const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
|
|
699
1081
|
if (currentToolRef.current === "select") {
|
|
700
1082
|
const hit = getTopShapeAtPoint(editor, { x, y });
|
|
701
1083
|
const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
|
|
@@ -735,6 +1117,16 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
735
1117
|
refreshSelectionBounds(editor);
|
|
736
1118
|
};
|
|
737
1119
|
const handlePointerMove = (e) => {
|
|
1120
|
+
if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
|
|
1121
|
+
penDetectedRef.current = true;
|
|
1122
|
+
penModeRef.current = true;
|
|
1123
|
+
}
|
|
1124
|
+
if (touchInteractions.handlePointerMove(e)) {
|
|
1125
|
+
e.preventDefault();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (penModeRef.current && e.pointerType === "touch" && isDrawingTool(currentToolRef.current) && !isPointerActiveRef.current) return;
|
|
1129
|
+
if (activePointerIdsRef.current.size > 1) return;
|
|
738
1130
|
updatePointerPreview(e.clientX, e.clientY);
|
|
739
1131
|
const prevClient = lastPointerClientRef.current;
|
|
740
1132
|
const dx = prevClient ? e.clientX - prevClient.x : 0;
|
|
@@ -743,7 +1135,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
743
1135
|
for (const sample of sampleEvents(e)) {
|
|
744
1136
|
const { x, y } = getPagePoint(sample);
|
|
745
1137
|
const pressure = sample.pressure ?? 0.5;
|
|
746
|
-
const isPen = sample.pointerType === "pen" || sample.
|
|
1138
|
+
const isPen = sample.pointerType === "pen" || hasRealPressure(sample.pressure);
|
|
747
1139
|
editor.input.pointerMove(x, y, pressure, isPen);
|
|
748
1140
|
}
|
|
749
1141
|
if (currentToolRef.current === "select") {
|
|
@@ -793,6 +1185,13 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
793
1185
|
refreshSelectionBounds(editor);
|
|
794
1186
|
};
|
|
795
1187
|
const handlePointerUp = (e) => {
|
|
1188
|
+
activePointerIdsRef.current.delete(e.pointerId);
|
|
1189
|
+
const hadTouchCameraGesture = touchInteractions.handlePointerUpOrCancel(e);
|
|
1190
|
+
if (hadTouchCameraGesture || touchInteractions.isCameraGestureActive()) {
|
|
1191
|
+
e.preventDefault();
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (!isPointerActiveRef.current) return;
|
|
796
1195
|
isPointerActiveRef.current = false;
|
|
797
1196
|
lastPointerClientRef.current = null;
|
|
798
1197
|
updatePointerPreview(e.clientX, e.clientY);
|
|
@@ -876,7 +1275,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
876
1275
|
}
|
|
877
1276
|
editor.endHistoryEntry();
|
|
878
1277
|
};
|
|
879
|
-
const handlePointerCancel = () => {
|
|
1278
|
+
const handlePointerCancel = (e) => {
|
|
1279
|
+
activePointerIdsRef.current.delete(e.pointerId);
|
|
1280
|
+
const hadTouchCameraGesture = touchInteractions.handlePointerUpOrCancel(e);
|
|
1281
|
+
if (hadTouchCameraGesture || touchInteractions.isCameraGestureActive()) return;
|
|
880
1282
|
if (!isPointerActiveRef.current) return;
|
|
881
1283
|
isPointerActiveRef.current = false;
|
|
882
1284
|
lastPointerClientRef.current = null;
|
|
@@ -905,31 +1307,58 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
905
1307
|
applyRemoteDocumentSnapshot(pending);
|
|
906
1308
|
}
|
|
907
1309
|
};
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
render();
|
|
921
|
-
syncHistoryState();
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
1310
|
+
const handleWheel = (e) => {
|
|
1311
|
+
if (!container.contains(e.target)) return;
|
|
1312
|
+
e.preventDefault();
|
|
1313
|
+
if (touchInteractions.isTrackpadZoomActive()) return;
|
|
1314
|
+
const delta = normalizeWheelDelta(e);
|
|
1315
|
+
if (delta.z !== 0) {
|
|
1316
|
+
const rect = canvas.getBoundingClientRect();
|
|
1317
|
+
const pointX = e.clientX - rect.left;
|
|
1318
|
+
const pointY = e.clientY - rect.top;
|
|
1319
|
+
editor.zoomAt(Math.exp(delta.z), pointX, pointY);
|
|
1320
|
+
} else {
|
|
1321
|
+
editor.panBy(delta.x, delta.y);
|
|
924
1322
|
}
|
|
925
|
-
editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
|
|
926
|
-
editor.tools.keyDown({ key: e.key });
|
|
927
1323
|
render();
|
|
1324
|
+
refreshSelectionBounds(editor);
|
|
1325
|
+
};
|
|
1326
|
+
const handleGestureEvent = (e) => {
|
|
1327
|
+
touchInteractions.handleGestureEvent(e, container);
|
|
1328
|
+
};
|
|
1329
|
+
const handleKeyDown = (e) => {
|
|
1330
|
+
handleKeyboardShortcutKeyDown(e, {
|
|
1331
|
+
isToolAvailable: (tool) => editor.tools.hasTool(tool),
|
|
1332
|
+
setToolFromShortcut: (tool) => {
|
|
1333
|
+
editor.setCurrentTool(tool);
|
|
1334
|
+
setCurrentToolState(tool);
|
|
1335
|
+
currentToolRef.current = tool;
|
|
1336
|
+
if (tool !== "select") resetSelectUi();
|
|
1337
|
+
render();
|
|
1338
|
+
},
|
|
1339
|
+
runHistoryShortcut: (shouldRedo) => applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo()),
|
|
1340
|
+
deleteSelection: () => currentToolRef.current === "select" ? deleteCurrentSelection() : false,
|
|
1341
|
+
dispatchKeyDown: (event) => {
|
|
1342
|
+
editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
|
|
1343
|
+
editor.tools.keyDown({ key: event.key });
|
|
1344
|
+
render();
|
|
1345
|
+
},
|
|
1346
|
+
dispatchKeyUp: () => void 0
|
|
1347
|
+
});
|
|
928
1348
|
};
|
|
929
1349
|
const handleKeyUp = (e) => {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1350
|
+
handleKeyboardShortcutKeyUp(e, {
|
|
1351
|
+
isToolAvailable: () => false,
|
|
1352
|
+
setToolFromShortcut: () => void 0,
|
|
1353
|
+
runHistoryShortcut: () => false,
|
|
1354
|
+
deleteSelection: () => false,
|
|
1355
|
+
dispatchKeyDown: () => void 0,
|
|
1356
|
+
dispatchKeyUp: (event) => {
|
|
1357
|
+
editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
|
|
1358
|
+
editor.tools.keyUp({ key: event.key });
|
|
1359
|
+
render();
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
933
1362
|
};
|
|
934
1363
|
const initializePersistence = async () => {
|
|
935
1364
|
if (!persistenceKey) {
|
|
@@ -1006,6 +1435,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1006
1435
|
const ro = new ResizeObserver(resize);
|
|
1007
1436
|
ro.observe(container);
|
|
1008
1437
|
canvas.addEventListener("pointerdown", handlePointerDown);
|
|
1438
|
+
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
1439
|
+
document.addEventListener("gesturestart", handleGestureEvent);
|
|
1440
|
+
document.addEventListener("gesturechange", handleGestureEvent);
|
|
1441
|
+
document.addEventListener("gestureend", handleGestureEvent);
|
|
1009
1442
|
window.addEventListener("pointermove", handlePointerMove);
|
|
1010
1443
|
window.addEventListener("pointerup", handlePointerUp);
|
|
1011
1444
|
window.addEventListener("pointercancel", handlePointerCancel);
|
|
@@ -1049,6 +1482,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1049
1482
|
editor.setCurrentDrawStyle(partial);
|
|
1050
1483
|
if (partial.color) setDrawColor(partial.color);
|
|
1051
1484
|
if (partial.dash) setDrawDash(partial.dash);
|
|
1485
|
+
if (partial.fill) setDrawFill(partial.fill);
|
|
1052
1486
|
if (partial.size) setDrawSize(partial.size);
|
|
1053
1487
|
render();
|
|
1054
1488
|
}
|
|
@@ -1061,13 +1495,19 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1061
1495
|
disposeMount?.();
|
|
1062
1496
|
ro.disconnect();
|
|
1063
1497
|
canvas.removeEventListener("pointerdown", handlePointerDown);
|
|
1498
|
+
container.removeEventListener("wheel", handleWheel);
|
|
1499
|
+
document.removeEventListener("gesturestart", handleGestureEvent);
|
|
1500
|
+
document.removeEventListener("gesturechange", handleGestureEvent);
|
|
1501
|
+
document.removeEventListener("gestureend", handleGestureEvent);
|
|
1064
1502
|
window.removeEventListener("pointermove", handlePointerMove);
|
|
1065
1503
|
window.removeEventListener("pointerup", handlePointerUp);
|
|
1066
1504
|
window.removeEventListener("pointercancel", handlePointerCancel);
|
|
1067
1505
|
window.removeEventListener("keydown", handleKeyDown);
|
|
1068
1506
|
window.removeEventListener("keyup", handleKeyUp);
|
|
1069
1507
|
isPointerActiveRef.current = false;
|
|
1508
|
+
activePointerIdsRef.current.clear();
|
|
1070
1509
|
pendingRemoteDocumentRef.current = null;
|
|
1510
|
+
touchInteractions.reset();
|
|
1071
1511
|
persistenceChannel?.close();
|
|
1072
1512
|
void persistenceDb?.close();
|
|
1073
1513
|
editorRef.current = null;
|
|
@@ -1078,6 +1518,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1078
1518
|
options.persistenceKey,
|
|
1079
1519
|
options.toolDefinitions,
|
|
1080
1520
|
refreshSelectionBounds,
|
|
1521
|
+
resetSelectUi,
|
|
1081
1522
|
render,
|
|
1082
1523
|
updatePointerPreview
|
|
1083
1524
|
]);
|
|
@@ -1106,6 +1547,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1106
1547
|
editor.setCurrentDrawStyle(partial);
|
|
1107
1548
|
if (partial.color) setDrawColor(partial.color);
|
|
1108
1549
|
if (partial.dash) setDrawDash(partial.dash);
|
|
1550
|
+
if (partial.fill) setDrawFill(partial.fill);
|
|
1109
1551
|
if (partial.size) setDrawSize(partial.size);
|
|
1110
1552
|
render();
|
|
1111
1553
|
},
|
|
@@ -1173,6 +1615,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1173
1615
|
currentTool,
|
|
1174
1616
|
drawColor,
|
|
1175
1617
|
drawDash,
|
|
1618
|
+
drawFill,
|
|
1176
1619
|
drawSize,
|
|
1177
1620
|
selectedShapeIds,
|
|
1178
1621
|
selectionBrush,
|
|
@@ -1182,7 +1625,6 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1182
1625
|
cursorContext,
|
|
1183
1626
|
toolOverlay,
|
|
1184
1627
|
isPersistenceReady,
|
|
1185
|
-
showStylePanel: stylePanelToolIdsRef.current.includes(currentTool),
|
|
1186
1628
|
canUndo,
|
|
1187
1629
|
canRedo,
|
|
1188
1630
|
undo,
|
|
@@ -1193,12 +1635,21 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1193
1635
|
handleRotatePointerDown
|
|
1194
1636
|
};
|
|
1195
1637
|
}
|
|
1196
|
-
var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser"]];
|
|
1638
|
+
var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser", "square", "circle"]];
|
|
1197
1639
|
var EMPTY_CUSTOM_TOOLS = [];
|
|
1198
1640
|
var EMPTY_CUSTOM_ELEMENTS = [];
|
|
1641
|
+
var EMPTY_STYLE_PANEL_PARTS = [];
|
|
1642
|
+
var EMPTY_STYLE_PANEL_CUSTOM_PARTS = [];
|
|
1643
|
+
var DEFAULT_STYLE_PANEL_PARTS_BY_TOOL = {
|
|
1644
|
+
pen: ["colors", "dashes", "sizes"],
|
|
1645
|
+
square: ["colors", "dashes", "fills", "sizes"],
|
|
1646
|
+
circle: ["colors", "dashes", "fills", "sizes"]
|
|
1647
|
+
};
|
|
1199
1648
|
var DEFAULT_TOOL_LABELS = {
|
|
1200
1649
|
select: "Select",
|
|
1201
1650
|
pen: "Pen",
|
|
1651
|
+
square: "Rectangle",
|
|
1652
|
+
circle: "Ellipse",
|
|
1202
1653
|
eraser: "Eraser",
|
|
1203
1654
|
hand: "Hand"
|
|
1204
1655
|
};
|
|
@@ -1270,21 +1721,6 @@ function Tsdraw(props) {
|
|
|
1270
1721
|
() => customTools.filter((customTool) => toolbarToolIds.has(customTool.id)).map((customTool) => customTool.definition),
|
|
1271
1722
|
[customTools, toolbarToolIds]
|
|
1272
1723
|
);
|
|
1273
|
-
const stylePanelToolIds = useMemo(
|
|
1274
|
-
() => {
|
|
1275
|
-
const nextToolIds = /* @__PURE__ */ new Set();
|
|
1276
|
-
if (toolbarToolIds.has("pen")) {
|
|
1277
|
-
nextToolIds.add("pen");
|
|
1278
|
-
}
|
|
1279
|
-
for (const customTool of customTools) {
|
|
1280
|
-
if ((customTool.showStylePanel ?? false) && toolbarToolIds.has(customTool.id)) {
|
|
1281
|
-
nextToolIds.add(customTool.id);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
return [...nextToolIds];
|
|
1285
|
-
},
|
|
1286
|
-
[customTools, toolbarToolIds]
|
|
1287
|
-
);
|
|
1288
1724
|
const firstToolbarTool = useMemo(() => {
|
|
1289
1725
|
for (const toolbarPart of toolbarPartIds) {
|
|
1290
1726
|
for (const item of toolbarPart) {
|
|
@@ -1313,6 +1749,7 @@ function Tsdraw(props) {
|
|
|
1313
1749
|
currentTool,
|
|
1314
1750
|
drawColor,
|
|
1315
1751
|
drawDash,
|
|
1752
|
+
drawFill,
|
|
1316
1753
|
drawSize,
|
|
1317
1754
|
selectedShapeIds,
|
|
1318
1755
|
selectionBrush,
|
|
@@ -1322,7 +1759,6 @@ function Tsdraw(props) {
|
|
|
1322
1759
|
cursorContext,
|
|
1323
1760
|
toolOverlay,
|
|
1324
1761
|
isPersistenceReady,
|
|
1325
|
-
showStylePanel,
|
|
1326
1762
|
canUndo,
|
|
1327
1763
|
canRedo,
|
|
1328
1764
|
undo,
|
|
@@ -1336,11 +1772,12 @@ function Tsdraw(props) {
|
|
|
1336
1772
|
initialTool,
|
|
1337
1773
|
theme: resolvedTheme,
|
|
1338
1774
|
persistenceKey: props.persistenceKey,
|
|
1339
|
-
stylePanelToolIds,
|
|
1340
1775
|
onMount: props.onMount
|
|
1341
1776
|
});
|
|
1342
1777
|
const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 14);
|
|
1343
1778
|
const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8, 8);
|
|
1779
|
+
const isToolbarHidden = props.uiOptions?.toolbar?.hide === true;
|
|
1780
|
+
const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true;
|
|
1344
1781
|
const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
|
|
1345
1782
|
const defaultToolOverlay = /* @__PURE__ */ jsx(
|
|
1346
1783
|
ToolOverlay,
|
|
@@ -1362,9 +1799,24 @@ function Tsdraw(props) {
|
|
|
1362
1799
|
const onDashSelect = useCallback((dash) => {
|
|
1363
1800
|
applyDrawStyle({ dash });
|
|
1364
1801
|
}, [applyDrawStyle]);
|
|
1802
|
+
const onFillSelect = useCallback((fill) => {
|
|
1803
|
+
applyDrawStyle({ fill });
|
|
1804
|
+
}, [applyDrawStyle]);
|
|
1365
1805
|
const onSizeSelect = useCallback((size) => {
|
|
1366
1806
|
applyDrawStyle({ size });
|
|
1367
1807
|
}, [applyDrawStyle]);
|
|
1808
|
+
const activeCustomTool = customToolMap.get(currentTool);
|
|
1809
|
+
const stylePanelParts = useMemo(
|
|
1810
|
+
() => {
|
|
1811
|
+
const fromCustomTool = activeCustomTool?.stylePanel?.parts;
|
|
1812
|
+
if (fromCustomTool && fromCustomTool.length > 0) return fromCustomTool;
|
|
1813
|
+
if (activeCustomTool?.stylePanel?.customParts && activeCustomTool.stylePanel.customParts.length > 0) return activeCustomTool.stylePanel.customParts.map((customPart) => customPart.id);
|
|
1814
|
+
if (currentTool in DEFAULT_STYLE_PANEL_PARTS_BY_TOOL) return DEFAULT_STYLE_PANEL_PARTS_BY_TOOL[currentTool] ?? EMPTY_STYLE_PANEL_PARTS;
|
|
1815
|
+
return EMPTY_STYLE_PANEL_PARTS;
|
|
1816
|
+
},
|
|
1817
|
+
[activeCustomTool, currentTool]
|
|
1818
|
+
);
|
|
1819
|
+
const stylePanelCustomParts = activeCustomTool?.stylePanel?.customParts ?? EMPTY_STYLE_PANEL_CUSTOM_PARTS;
|
|
1368
1820
|
const toolbarParts = useMemo(
|
|
1369
1821
|
() => toolbarPartIds.map((toolbarPart, partIndex) => {
|
|
1370
1822
|
const items = toolbarPart.map((item) => {
|
|
@@ -1453,14 +1905,18 @@ function Tsdraw(props) {
|
|
|
1453
1905
|
/* @__PURE__ */ jsx(
|
|
1454
1906
|
StylePanel,
|
|
1455
1907
|
{
|
|
1456
|
-
visible: isPersistenceReady &&
|
|
1908
|
+
visible: !isStylePanelHidden && isPersistenceReady && stylePanelParts.length > 0,
|
|
1909
|
+
parts: stylePanelParts,
|
|
1910
|
+
customParts: stylePanelCustomParts,
|
|
1457
1911
|
style: stylePanelPlacementStyle,
|
|
1458
1912
|
theme: resolvedTheme,
|
|
1459
1913
|
drawColor,
|
|
1460
1914
|
drawDash,
|
|
1915
|
+
drawFill,
|
|
1461
1916
|
drawSize,
|
|
1462
1917
|
onColorSelect,
|
|
1463
1918
|
onDashSelect,
|
|
1919
|
+
onFillSelect,
|
|
1464
1920
|
onSizeSelect
|
|
1465
1921
|
}
|
|
1466
1922
|
),
|
|
@@ -1477,7 +1933,7 @@ function Tsdraw(props) {
|
|
|
1477
1933
|
},
|
|
1478
1934
|
customElement.id
|
|
1479
1935
|
)),
|
|
1480
|
-
/* @__PURE__ */ jsx(
|
|
1936
|
+
!isToolbarHidden ? /* @__PURE__ */ jsx(
|
|
1481
1937
|
Toolbar,
|
|
1482
1938
|
{
|
|
1483
1939
|
parts: toolbarParts,
|
|
@@ -1486,7 +1942,7 @@ function Tsdraw(props) {
|
|
|
1486
1942
|
onToolChange: setTool,
|
|
1487
1943
|
disabled: !isPersistenceReady
|
|
1488
1944
|
}
|
|
1489
|
-
)
|
|
1945
|
+
) : null
|
|
1490
1946
|
]
|
|
1491
1947
|
}
|
|
1492
1948
|
);
|