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