@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.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
- return /* @__PURE__ */ jsxs("div", { className: "tsdraw-style-panel", style, "aria-label": "Draw style panel", children: [
115
- /* @__PURE__ */ jsx("div", { className: "tsdraw-style-colors", children: STYLE_COLORS.map((item) => /* @__PURE__ */ jsx(
116
- "button",
117
- {
118
- type: "button",
119
- className: "tsdraw-style-color",
120
- "data-active": drawColor === item.value ? "true" : void 0,
121
- "aria-label": `Color ${item.value}`,
122
- title: item.value,
123
- onClick: () => onColorSelect(item.value),
124
- children: /* @__PURE__ */ jsx(
125
- "span",
126
- {
127
- className: "tsdraw-style-color-dot",
128
- style: { background: resolveThemeColor(item.value, theme) }
129
- }
130
- )
131
- },
132
- item.value
133
- )) }),
134
- /* @__PURE__ */ jsx("div", { className: "tsdraw-style-section", children: STYLE_DASHES.map((dash) => /* @__PURE__ */ jsx(
135
- "button",
136
- {
137
- type: "button",
138
- className: "tsdraw-style-row",
139
- "data-active": drawDash === dash ? "true" : void 0,
140
- "aria-label": `Stroke ${dash}`,
141
- title: dash,
142
- onClick: () => onDashSelect(dash),
143
- children: /* @__PURE__ */ jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsx("span", { className: `tsdraw-style-preview-line tsdraw-style-preview-line--${dash}` }) })
144
- },
145
- dash
146
- )) }),
147
- /* @__PURE__ */ jsx("div", { className: "tsdraw-style-section", children: STYLE_SIZES.map((size) => /* @__PURE__ */ jsx(
148
- "button",
149
- {
150
- type: "button",
151
- className: "tsdraw-style-row",
152
- "data-active": drawSize === size ? "true" : void 0,
153
- "aria-label": `Thickness ${size}`,
154
- title: size,
155
- onClick: () => onSizeSelect(size),
156
- children: /* @__PURE__ */ jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsx("span", { className: `tsdraw-style-size tsdraw-style-size--${size}` }) })
157
- },
158
- size
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 { x, y, zoom } = editor.viewport;
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: bounds.minX * zoom + x,
382
- top: bounds.minY * zoom + y,
383
- width: (bounds.maxX - bounds.minX) * zoom,
384
- height: (bounds.maxY - bounds.minY) * zoom
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 = (document) => {
949
+ const applyRemoteDocumentSnapshot = (document2) => {
656
950
  ignorePersistenceChanges = true;
657
- editor.loadDocumentSnapshot(document);
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.pointerType === "touch";
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.pointerType === "touch";
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 handleKeyDown = (e) => {
909
- const isMetaPressed = e.metaKey || e.ctrlKey;
910
- const loweredKey = e.key.toLowerCase();
911
- const isUndoOrRedoKey = loweredKey === "z" || loweredKey === "y";
912
- if (isMetaPressed && isUndoOrRedoKey) {
913
- const shouldRedo = loweredKey === "y" || loweredKey === "z" && e.shiftKey;
914
- const changed = shouldRedo ? editor.redo() : editor.undo();
915
- if (changed) {
916
- e.preventDefault();
917
- e.stopPropagation();
918
- reconcileSelectionAfterDocumentLoad();
919
- setSelectionRotationDeg(0);
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
- editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
931
- editor.tools.keyUp({ key: e.key });
932
- render();
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 && showStylePanel,
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
  );