js-draw 1.25.0 → 1.27.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. package/LICENSE +1 -1
  2. package/dist/Editor.css +1 -1935
  3. package/dist/bundle.js +478 -4
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +0 -2
  6. package/dist/cjs/Editor.js +1 -1
  7. package/dist/cjs/bundle/bundled.js +2 -1
  8. package/dist/cjs/components/AbstractComponent.d.ts +15 -0
  9. package/dist/cjs/components/AbstractComponent.js +16 -0
  10. package/dist/cjs/components/Stroke.d.ts +1 -0
  11. package/dist/cjs/components/Stroke.js +7 -0
  12. package/dist/cjs/image/EditorImage.d.ts +2 -1
  13. package/dist/cjs/image/EditorImage.js +21 -6
  14. package/dist/cjs/toolbar/AbstractToolbar.js +9 -2
  15. package/dist/cjs/toolbar/IconProvider.d.ts +2 -1
  16. package/dist/cjs/toolbar/IconProvider.js +18 -8
  17. package/dist/cjs/toolbar/localization.d.ts +2 -0
  18. package/dist/cjs/toolbar/localization.js +2 -0
  19. package/dist/cjs/toolbar/widgets/BaseWidget.js +6 -1
  20. package/dist/cjs/toolbar/widgets/HandToolWidget.js +3 -3
  21. package/dist/cjs/toolbar/widgets/SelectionToolWidget.d.ts +7 -0
  22. package/dist/cjs/toolbar/widgets/SelectionToolWidget.js +109 -28
  23. package/dist/cjs/toolbar/widgets/components/makeButtonGrid.d.ts +17 -0
  24. package/dist/cjs/toolbar/widgets/components/makeButtonGrid.js +40 -0
  25. package/dist/cjs/tools/SelectionTool/Selection.d.ts +2 -3
  26. package/dist/cjs/tools/SelectionTool/Selection.js +30 -46
  27. package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.d.ts +17 -0
  28. package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.js +67 -0
  29. package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.d.ts +13 -0
  30. package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.js +33 -0
  31. package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.d.ts +15 -0
  32. package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.js +39 -0
  33. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
  34. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.js +13 -4
  35. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +10 -2
  36. package/dist/cjs/tools/SelectionTool/SelectionTool.js +68 -55
  37. package/dist/cjs/tools/SelectionTool/types.d.ts +4 -0
  38. package/dist/cjs/tools/SelectionTool/types.js +6 -1
  39. package/dist/cjs/tools/TextTool.js +5 -2
  40. package/dist/cjs/tools/lib.d.ts +1 -1
  41. package/dist/cjs/tools/lib.js +2 -1
  42. package/dist/cjs/util/ReactiveValue.js +2 -6
  43. package/dist/cjs/util/assertions.d.ts +7 -6
  44. package/dist/cjs/util/assertions.js +35 -29
  45. package/dist/cjs/version.js +1 -1
  46. package/dist/mjs/Editor.d.ts +0 -2
  47. package/dist/mjs/Editor.mjs +1 -1
  48. package/dist/mjs/bundle/bundled.mjs +2 -1
  49. package/dist/mjs/components/AbstractComponent.d.ts +15 -0
  50. package/dist/mjs/components/AbstractComponent.mjs +16 -0
  51. package/dist/mjs/components/Stroke.d.ts +1 -0
  52. package/dist/mjs/components/Stroke.mjs +7 -0
  53. package/dist/mjs/image/EditorImage.d.ts +2 -1
  54. package/dist/mjs/image/EditorImage.mjs +21 -6
  55. package/dist/mjs/toolbar/AbstractToolbar.mjs +9 -2
  56. package/dist/mjs/toolbar/IconProvider.d.ts +2 -1
  57. package/dist/mjs/toolbar/IconProvider.mjs +18 -8
  58. package/dist/mjs/toolbar/localization.d.ts +2 -0
  59. package/dist/mjs/toolbar/localization.mjs +2 -0
  60. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +6 -1
  61. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +3 -3
  62. package/dist/mjs/toolbar/widgets/SelectionToolWidget.d.ts +7 -0
  63. package/dist/mjs/toolbar/widgets/SelectionToolWidget.mjs +109 -28
  64. package/dist/mjs/toolbar/widgets/components/makeButtonGrid.d.ts +17 -0
  65. package/dist/mjs/toolbar/widgets/components/makeButtonGrid.mjs +35 -0
  66. package/dist/mjs/tools/SelectionTool/Selection.d.ts +2 -3
  67. package/dist/mjs/tools/SelectionTool/Selection.mjs +30 -46
  68. package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.d.ts +17 -0
  69. package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.mjs +61 -0
  70. package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.d.ts +13 -0
  71. package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.mjs +27 -0
  72. package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.d.ts +15 -0
  73. package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.mjs +36 -0
  74. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
  75. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.mjs +13 -4
  76. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +10 -2
  77. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +68 -55
  78. package/dist/mjs/tools/SelectionTool/types.d.ts +4 -0
  79. package/dist/mjs/tools/SelectionTool/types.mjs +5 -0
  80. package/dist/mjs/tools/TextTool.mjs +5 -2
  81. package/dist/mjs/tools/lib.d.ts +1 -1
  82. package/dist/mjs/tools/lib.mjs +1 -1
  83. package/dist/mjs/util/ReactiveValue.mjs +2 -6
  84. package/dist/mjs/util/assertions.d.ts +7 -6
  85. package/dist/mjs/util/assertions.mjs +28 -24
  86. package/dist/mjs/version.mjs +1 -1
  87. package/package.json +4 -4
  88. package/src/toolbar/EdgeToolbar.scss +6 -1
  89. package/src/toolbar/widgets/components/components.scss +1 -0
  90. package/src/toolbar/widgets/components/makeButtonGrid.scss +25 -0
  91. package/src/tools/SelectionTool/SelectionTool.scss +12 -1
  92. package/src/tools/util/createMenuOverlay.scss +5 -3
@@ -19,7 +19,7 @@ const updateChunkSize = 100;
19
19
  const maxPreviewElemCount = 500;
20
20
  // @internal
21
21
  class Selection {
22
- constructor(startPoint, editor, showContextMenu) {
22
+ constructor(selectedElems, editor, showContextMenu) {
23
23
  this.editor = editor;
24
24
  // The last-computed bounding box of selected content
25
25
  // @see getTightBoundingBox
@@ -33,7 +33,9 @@ class Selection {
33
33
  this.activeHandle = null;
34
34
  this.backgroundDragging = false;
35
35
  this.selectionDuplicatedAnimationTimeout = null;
36
- this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
36
+ selectedElems = [...selectedElems];
37
+ this.selectedElems = selectedElems;
38
+ this.originalRegion = Rect2.empty;
37
39
  this.transformers = {
38
40
  drag: new DragTransformer(editor, this),
39
41
  resize: new ResizeTransformer(editor, this),
@@ -72,17 +74,18 @@ class Selection {
72
74
  side: Vec2.of(0.5, 0),
73
75
  icon: this.editor.icons.makeRotateIcon(),
74
76
  }, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
75
- const menuToggleButton = new SelectionMenuShortcut(this, this.editor.viewport, showContextMenu, this.editor.localization);
77
+ const menuToggleButton = new SelectionMenuShortcut(this, this.editor.viewport, this.editor.icons.makeOverflowIcon(), showContextMenu, this.editor.localization);
76
78
  this.childwidgets = [
79
+ menuToggleButton,
77
80
  resizeBothHandle,
78
81
  ...resizeHorizontalHandles,
79
82
  resizeVerticalHandle,
80
83
  rotationHandle,
81
- menuToggleButton,
82
84
  ];
83
85
  for (const widget of this.childwidgets) {
84
86
  widget.addTo(this.backgroundElem);
85
87
  }
88
+ this.recomputeRegion();
86
89
  this.updateUI();
87
90
  }
88
91
  // @internal Intended for unit tests
@@ -205,34 +208,6 @@ class Selection {
205
208
  wetInkRenderer.popTransform();
206
209
  this.updateUI();
207
210
  }
208
- // Find the objects corresponding to this in the document,
209
- // select them.
210
- // Returns false iff nothing was selected.
211
- resolveToObjects() {
212
- let singleItemSelectionMode = false;
213
- this.transform = Mat33.identity;
214
- // Grow the rectangle, if necessary
215
- if (this.region.w === 0 || this.region.h === 0) {
216
- const padding = this.editor.viewport.visibleRect.maxDimension / 200;
217
- this.originalRegion = Rect2.bboxOf(this.region.corners, padding);
218
- // Only select one item if the rectangle was very small.
219
- singleItemSelectionMode = true;
220
- }
221
- this.selectedElems = this.editor.image
222
- .getElementsIntersectingRegion(this.region)
223
- .filter((elem) => {
224
- return elem.intersectsRect(this.region) && elem.isSelectable();
225
- });
226
- if (singleItemSelectionMode && this.selectedElems.length > 0) {
227
- this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
228
- }
229
- // Find the bounding box of all selected elements.
230
- if (!this.recomputeRegion()) {
231
- return false;
232
- }
233
- this.updateUI();
234
- return true;
235
- }
236
211
  // Recompute this' region from the selected elements.
237
212
  // Returns false if the selection is empty.
238
213
  recomputeRegion() {
@@ -354,6 +329,10 @@ class Selection {
354
329
  });
355
330
  }
356
331
  onDragStart(pointer) {
332
+ // If empty, it isn't possible to drag
333
+ if (this.selectedElems.length === 0) {
334
+ return false;
335
+ }
357
336
  // Clear the HTML selection (prevent HTML drag and drop being triggered by this drag)
358
337
  document.getSelection()?.removeAllRanges();
359
338
  this.activeHandle = null;
@@ -454,6 +433,7 @@ class Selection {
454
433
  if (!wasTransforming) {
455
434
  this.runSelectionDuplicatedAnimation();
456
435
  }
436
+ let command;
457
437
  if (wasTransforming) {
458
438
  // Don't update the selection's focus when redoing/undoing
459
439
  const selectionToUpdate = null;
@@ -463,16 +443,30 @@ class Selection {
463
443
  await tmpApplyCommand.apply(this.editor);
464
444
  // Show items again
465
445
  this.addRemoveSelectionFromImage(true);
466
- }
467
- const duplicateCommand = new Duplicate(this.selectedElems);
468
- if (wasTransforming) {
446
+ // With the transformation applied, create the duplicates
447
+ command = uniteCommands(this.selectedElems.map((elem) => {
448
+ return EditorImage.addElement(elem.clone());
449
+ }));
469
450
  // Move the selected objects back to the correct location.
470
451
  await tmpApplyCommand?.unapply(this.editor);
471
452
  this.addRemoveSelectionFromImage(false);
472
453
  this.previewTransformCmds();
473
454
  this.updateUI();
474
455
  }
475
- return duplicateCommand;
456
+ else {
457
+ command = new Duplicate(this.selectedElems);
458
+ }
459
+ return command;
460
+ }
461
+ snapSelectedObjectsToGrid() {
462
+ const viewport = this.editor.viewport;
463
+ // Snap the top left corner of what we have selected.
464
+ const topLeftOfBBox = this.computeTightBoundingBox().topLeft;
465
+ const snappedTopLeft = viewport.snapToGrid(topLeftOfBBox);
466
+ const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
467
+ const oldTransform = this.getTransform();
468
+ this.setTransform(oldTransform.rightMul(Mat33.translation(snapDelta)));
469
+ this.finalizeTransform();
476
470
  }
477
471
  setHandlesVisible(showHandles) {
478
472
  if (!showHandles) {
@@ -502,16 +496,6 @@ class Selection {
502
496
  this.selectionTightBoundingBox = null;
503
497
  this.hasParent = false;
504
498
  }
505
- setSelectedObjects(objects, bbox) {
506
- this.addRemoveSelectionFromImage(true);
507
- this.originalRegion = bbox;
508
- this.selectionTightBoundingBox = bbox;
509
- this.selectedElems = objects.filter((object) => object.isSelectable());
510
- // Enforce increasing z-index invariant
511
- this.selectedElems.sort((a, b) => a.getZIndex() - b.getZIndex());
512
- this.padRegion();
513
- this.updateUI();
514
- }
515
499
  getSelectedObjects() {
516
500
  return [...this.selectedElems];
517
501
  }
@@ -0,0 +1,17 @@
1
+ import { Path, Point2 } from '@js-draw/math';
2
+ import Viewport from '../../../Viewport';
3
+ import EditorImage from '../../../image/EditorImage';
4
+ import AbstractComponent from '../../../components/AbstractComponent';
5
+ import SelectionBuilder from './SelectionBuilder';
6
+ /**
7
+ * Creates lasso selections.
8
+ */
9
+ export default class LassoSelectionBuilder extends SelectionBuilder {
10
+ private viewport;
11
+ private boundaryPoints;
12
+ private lastPoint;
13
+ constructor(startPoint: Point2, viewport: Viewport);
14
+ onPointerMove(canvasPoint: Point2): void;
15
+ previewPath(): Path;
16
+ resolveInternal(image: EditorImage): AbstractComponent[];
17
+ }
@@ -0,0 +1,61 @@
1
+ import { Path } from '@js-draw/math';
2
+ import { PathCommandType } from '@js-draw/math';
3
+ import SelectionBuilder from './SelectionBuilder.mjs';
4
+ /**
5
+ * Creates lasso selections.
6
+ */
7
+ export default class LassoSelectionBuilder extends SelectionBuilder {
8
+ constructor(startPoint, viewport) {
9
+ super();
10
+ this.viewport = viewport;
11
+ this.boundaryPoints = [];
12
+ this.boundaryPoints.push(startPoint);
13
+ this.lastPoint = startPoint;
14
+ }
15
+ onPointerMove(canvasPoint) {
16
+ const lastBoundaryPoint = this.boundaryPoints[this.boundaryPoints.length - 1];
17
+ const minBoundaryDist = this.viewport.getSizeOfPixelOnCanvas() * 8;
18
+ if (lastBoundaryPoint.distanceTo(canvasPoint) >= minBoundaryDist) {
19
+ this.boundaryPoints.push(canvasPoint);
20
+ }
21
+ this.lastPoint = canvasPoint;
22
+ }
23
+ previewPath() {
24
+ const pathCommands = this.boundaryPoints.map((point) => {
25
+ return { kind: PathCommandType.LineTo, point };
26
+ });
27
+ pathCommands.push({
28
+ kind: PathCommandType.LineTo,
29
+ point: this.lastPoint,
30
+ });
31
+ return new Path(this.boundaryPoints[0], pathCommands).asClosed();
32
+ }
33
+ resolveInternal(image) {
34
+ const path = this.previewPath();
35
+ const lines = path.polylineApproximation();
36
+ const candidates = image.getElementsIntersectingRegion(path.bbox);
37
+ const componentIsInSelection = (component) => {
38
+ if (path.closedContainsRect(component.getExactBBox())) {
39
+ return true;
40
+ }
41
+ let hasKeyPoint = false;
42
+ for (const point of component.keyPoints()) {
43
+ if (path.closedContainsPoint(point)) {
44
+ hasKeyPoint = true;
45
+ break;
46
+ }
47
+ }
48
+ if (!hasKeyPoint) {
49
+ return false;
50
+ }
51
+ // Only select if completely contained within the lasso
52
+ for (const line of lines) {
53
+ if (component.intersects(line)) {
54
+ return false;
55
+ }
56
+ }
57
+ return true;
58
+ };
59
+ return candidates.filter(componentIsInSelection);
60
+ }
61
+ }
@@ -0,0 +1,13 @@
1
+ import { Path, Point2 } from '@js-draw/math';
2
+ import EditorImage from '../../../image/EditorImage';
3
+ import SelectionBuilder from './SelectionBuilder';
4
+ /**
5
+ * Creates rectangle selections
6
+ */
7
+ export default class RectSelectionBuilder extends SelectionBuilder {
8
+ private rect;
9
+ constructor(startPoint: Point2);
10
+ onPointerMove(canvasPoint: Point2): void;
11
+ previewPath(): Path;
12
+ resolveInternal(image: EditorImage): import("../../../lib").AbstractComponent[];
13
+ }
@@ -0,0 +1,27 @@
1
+ import { Path, Rect2 } from '@js-draw/math';
2
+ import SelectionBuilder from './SelectionBuilder.mjs';
3
+ /**
4
+ * Creates rectangle selections
5
+ */
6
+ export default class RectSelectionBuilder extends SelectionBuilder {
7
+ constructor(startPoint) {
8
+ super();
9
+ this.rect = Rect2.fromCorners(startPoint, startPoint);
10
+ }
11
+ onPointerMove(canvasPoint) {
12
+ this.rect = this.rect.grownToPoint(canvasPoint);
13
+ }
14
+ previewPath() {
15
+ return Path.fromRect(this.rect);
16
+ }
17
+ resolveInternal(image) {
18
+ return image.getElementsIntersectingRegion(this.rect).filter((element) => {
19
+ // Filter out the case where the selection rectangle is completely contained
20
+ // within the element (and does not intersect it).
21
+ // This is useful, for example, if a very large stroke is used as the background
22
+ // for another drawing. This prevents the very large stroke from being selected
23
+ // unless the selection touches one of its edges.
24
+ return element.intersectsRect(this.rect);
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,15 @@
1
+ import { Color4, Path, Point2 } from '@js-draw/math';
2
+ import AbstractRenderer from '../../../rendering/renderers/AbstractRenderer';
3
+ import EditorImage from '../../../image/EditorImage';
4
+ import AbstractComponent from '../../../components/AbstractComponent';
5
+ import Viewport from '../../../Viewport';
6
+ export default abstract class SelectionBuilder {
7
+ abstract onPointerMove(canvasPoint: Point2): void;
8
+ abstract previewPath(): Path;
9
+ /** Returns the components currently in the selection bounds. Used by {@link resolve}. */
10
+ protected abstract resolveInternal(image: EditorImage): AbstractComponent[];
11
+ /** Renders a preview of the selection bounds */
12
+ render(renderer: AbstractRenderer, color: Color4): void;
13
+ /** Converts the selection preview into a set of selected elements */
14
+ resolve(image: EditorImage, viewport: Viewport): AbstractComponent[];
15
+ }
@@ -0,0 +1,36 @@
1
+ import { pathToRenderable } from '../../../rendering/RenderablePathSpec.mjs';
2
+ export default class SelectionBuilder {
3
+ /** Renders a preview of the selection bounds */
4
+ render(renderer, color) {
5
+ renderer.drawPath(pathToRenderable(this.previewPath(), { fill: color }));
6
+ }
7
+ /** Converts the selection preview into a set of selected elements */
8
+ resolve(image, viewport) {
9
+ const path = this.previewPath();
10
+ const filterComponents = (components) => {
11
+ return components.filter((component) => {
12
+ return component.isSelectable();
13
+ });
14
+ };
15
+ let components;
16
+ // If the bounding box is very small, search for items **near** the bounding box,
17
+ // rather than in the bounding box.
18
+ const clickSize = viewport.getSizeOfPixelOnCanvas() * 3;
19
+ const isClick = path.bbox.maxDimension <= clickSize;
20
+ if (isClick) {
21
+ const searchRegionSize = viewport.visibleRect.maxDimension / 200;
22
+ const minSizeBox = path.bbox.grownBy(searchRegionSize);
23
+ components = image.getElementsIntersectingRegion(minSizeBox).filter((component) => {
24
+ return minSizeBox.containsRect(component.getBBox()) || component.intersectsRect(minSizeBox);
25
+ });
26
+ components = filterComponents(components);
27
+ if (components.length > 1) {
28
+ components = [components[0]];
29
+ }
30
+ }
31
+ else {
32
+ components = filterComponents(this.resolveInternal(image));
33
+ }
34
+ return components;
35
+ }
36
+ }
@@ -11,10 +11,12 @@ type OnShowContextMenu = (anchor: Point2) => void;
11
11
  export default class SelectionMenuShortcut implements SelectionBoxChild {
12
12
  private readonly parent;
13
13
  private readonly viewport;
14
+ private readonly icon;
14
15
  private localization;
15
16
  private element;
17
+ private button;
16
18
  private onClick;
17
- constructor(parent: Selection, viewport: Viewport, showContextMenu: OnShowContextMenu, localization: ToolLocalization);
19
+ constructor(parent: Selection, viewport: Viewport, icon: Element, showContextMenu: OnShowContextMenu, localization: ToolLocalization);
18
20
  private initUI;
19
21
  addTo(container: HTMLElement): void;
20
22
  remove(): void;
@@ -2,15 +2,17 @@ import { Rect2, Vec2 } from '@js-draw/math';
2
2
  import { cssPrefix } from './SelectionTool.mjs';
3
3
  const verticalOffset = 40;
4
4
  export default class SelectionMenuShortcut {
5
- constructor(parent, viewport, showContextMenu, localization) {
5
+ constructor(parent, viewport, icon, showContextMenu, localization) {
6
6
  this.parent = parent;
7
7
  this.viewport = viewport;
8
+ this.icon = icon;
8
9
  this.localization = localization;
9
10
  this.lastDragPointer = null;
10
11
  this.element = document.createElement('div');
11
12
  this.element.classList.add(`${cssPrefix}handle`, `${cssPrefix}selection-menu`);
12
13
  this.element.style.setProperty('--vertical-offset', `${verticalOffset}px`);
13
14
  this.onClick = () => {
15
+ this.button?.focus({ preventScroll: true });
14
16
  const anchor = this.getBBoxCanvasCoords().center;
15
17
  showContextMenu(anchor);
16
18
  };
@@ -19,16 +21,22 @@ export default class SelectionMenuShortcut {
19
21
  }
20
22
  initUI() {
21
23
  const button = document.createElement('button');
22
- button.textContent = '...';
24
+ this.icon.classList.add('icon');
25
+ button.replaceChildren(this.icon);
23
26
  button.ariaLabel = this.localization.selectionMenu__show;
24
27
  button.title = button.ariaLabel;
28
+ this.button = button;
25
29
  // To prevent editor event handlers from conflicting with those for the button,
26
30
  // don't register a [click] handler. An onclick handler can be fired incorrectly
27
31
  // in this case (in Chrome) after onClick is fired in onDragEnd, leading to a double
28
32
  // on-click action.
29
33
  button.onkeydown = (event) => {
30
- if (event.key === 'Enter')
34
+ if (event.key === 'Enter') {
35
+ // .preventDefault prevents [Enter] from activating the first item in the
36
+ // selection menu.
37
+ event.preventDefault();
31
38
  this.onClick();
39
+ }
32
40
  };
33
41
  this.element.appendChild(button);
34
42
  // Update the bounding box of this in response to the new button.
@@ -58,7 +66,8 @@ export default class SelectionMenuShortcut {
58
66
  const contentCanvasSize = this.getElementScreenSize().times(toCanvasScale);
59
67
  const handleSizeCanvas = verticalOffset / this.viewport.getScaleFactor();
60
68
  const topLeft = Vec2.of(parentCanvasRect.x, parentCanvasRect.y - handleSizeCanvas);
61
- return new Rect2(topLeft.x, topLeft.y, contentCanvasSize.x, contentCanvasSize.y);
69
+ const minSize = Vec2.of(48, 48).times(toCanvasScale);
70
+ return new Rect2(topLeft.x, topLeft.y, contentCanvasSize.x, contentCanvasSize.y).grownToSize(minSize);
62
71
  }
63
72
  updatePosition() {
64
73
  const bbox = this.getBBoxParentCoords();
@@ -3,13 +3,18 @@ import Editor from '../../Editor';
3
3
  import { ContextMenuEvt, CopyEvent, KeyPressEvent, KeyUpEvent, PointerEvt } from '../../inputEvents';
4
4
  import BaseTool from '../BaseTool';
5
5
  import Selection from './Selection';
6
+ import { MutableReactiveValue } from '../../util/ReactiveValue';
7
+ import { SelectionMode } from './types';
6
8
  export declare const cssPrefix = "selection-tool-";
9
+ export { SelectionMode };
7
10
  export default class SelectionTool extends BaseTool {
8
11
  private editor;
12
+ readonly modeValue: MutableReactiveValue<SelectionMode>;
13
+ private selectionBuilder;
9
14
  private handleOverlay;
10
15
  private prevSelectionBox;
11
16
  private selectionBox;
12
- private rebuildSelectionScheduled;
17
+ private removeSelectionScheduled;
13
18
  private startPoint;
14
19
  private expandingSelectionBox;
15
20
  private shiftKeyPressed;
@@ -17,8 +22,8 @@ export default class SelectionTool extends BaseTool {
17
22
  private lastPointer;
18
23
  private autoscroller;
19
24
  constructor(editor: Editor, description: string);
25
+ private getSelectionColor;
20
26
  private makeSelectionBox;
21
- private snapSelectionToGrid;
22
27
  private showContextMenu;
23
28
  onContextMenu(event: ContextMenuEvt): boolean;
24
29
  private selectionBoxHandlingEvt;
@@ -36,7 +41,10 @@ export default class SelectionTool extends BaseTool {
36
41
  onCopy(event: CopyEvent): boolean;
37
42
  setEnabled(enabled: boolean): void;
38
43
  getSelection(): Selection | null;
44
+ /** @returns true if the selection is currently being created by the user. */
45
+ isSelecting(): boolean;
39
46
  getSelectedObjects(): AbstractComponent[];
40
47
  setSelection(objects: AbstractComponent[]): void;
48
+ private clearSelectionNoUpdateEvent;
41
49
  clearSelection(): void;
42
50
  }
@@ -1,4 +1,4 @@
1
- import { Mat33, Vec2 } from '@js-draw/math';
1
+ import { Mat33, Vec2, Color4 } from '@js-draw/math';
2
2
  import { EditorEventType } from '../../types.mjs';
3
3
  import Viewport from '../../Viewport.mjs';
4
4
  import BaseTool from '../BaseTool.mjs';
@@ -9,7 +9,12 @@ import TextComponent from '../../components/TextComponent.mjs';
9
9
  import { duplicateSelectionShortcut, translateLeftSelectionShortcutId, translateRightSelectionShortcutId, selectAllKeyboardShortcut, sendToBackSelectionShortcut, snapToGridKeyboardShortcutId, translateDownSelectionShortcutId, translateUpSelectionShortcutId, rotateClockwiseSelectionShortcutId, rotateCounterClockwiseSelectionShortcutId, stretchXSelectionShortcutId, shrinkXSelectionShortcutId, shrinkYSelectionShortcutId, stretchYSelectionShortcutId, stretchXYSelectionShortcutId, shrinkXYSelectionShortcutId, } from '../keybindings.mjs';
10
10
  import ToPointerAutoscroller from './ToPointerAutoscroller.mjs';
11
11
  import showSelectionContextMenu from './util/showSelectionContextMenu.mjs';
12
+ import { MutableReactiveValue } from '../../util/ReactiveValue.mjs';
13
+ import { SelectionMode } from './types.mjs';
14
+ import LassoSelectionBuilder from './SelectionBuilders/LassoSelectionBuilder.mjs';
15
+ import RectSelectionBuilder from './SelectionBuilders/RectSelectionBuilder.mjs';
12
16
  export const cssPrefix = 'selection-tool-';
17
+ export { SelectionMode };
13
18
  // Allows users to select/transform portions of the `EditorImage`.
14
19
  // With respect to `extend`ing, `SelectionTool` is not stable.
15
20
  export default class SelectionTool extends BaseTool {
@@ -19,7 +24,7 @@ export default class SelectionTool extends BaseTool {
19
24
  // True if clearing and recreating the selectionBox has been deferred. This is used to prevent the selection
20
25
  // from vanishing on pointerdown events that are intended to form other gestures (e.g. long press) that would
21
26
  // ultimately restore the selection.
22
- this.rebuildSelectionScheduled = false;
27
+ this.removeSelectionScheduled = false;
23
28
  this.startPoint = null; // canvas position
24
29
  this.expandingSelectionBox = false;
25
30
  this.shiftKeyPressed = false;
@@ -33,6 +38,13 @@ export default class SelectionTool extends BaseTool {
33
38
  // Whether the last keypress corresponded to an action that didn't transform the
34
39
  // selection (and thus does not need to be finalized on onKeyUp).
35
40
  this.hasUnfinalizedTransformFromKeyPress = false;
41
+ this.modeValue = MutableReactiveValue.fromInitialValue(SelectionMode.Rectangle);
42
+ this.modeValue.onUpdate(() => {
43
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
44
+ kind: EditorEventType.ToolUpdated,
45
+ tool: this,
46
+ });
47
+ });
36
48
  this.autoscroller = new ToPointerAutoscroller(editor.viewport, (scrollBy) => {
37
49
  editor.dispatch(Viewport.transformBy(Mat33.translation(scrollBy)), false);
38
50
  // Update the selection box/content to match the new viewport.
@@ -61,26 +73,19 @@ export default class SelectionTool extends BaseTool {
61
73
  this.editor.handleKeyEventsFrom(this.handleOverlay);
62
74
  this.editor.handlePointerEventsFrom(this.handleOverlay);
63
75
  }
64
- makeSelectionBox(selectionStartPos) {
76
+ getSelectionColor() {
77
+ const colorString = getComputedStyle(this.handleOverlay).getPropertyValue('--selection-background-color');
78
+ return Color4.fromString(colorString).withAlpha(0.5);
79
+ }
80
+ makeSelectionBox(selectedObjects) {
65
81
  this.prevSelectionBox = this.selectionBox;
66
- this.selectionBox = new Selection(selectionStartPos, this.editor, this.showContextMenu);
82
+ this.selectionBox = new Selection(selectedObjects, this.editor, this.showContextMenu);
67
83
  if (!this.expandingSelectionBox) {
68
84
  // Remove any previous selection rects
69
85
  this.prevSelectionBox?.cancelSelection();
70
86
  }
71
87
  this.selectionBox.addTo(this.handleOverlay);
72
88
  }
73
- snapSelectionToGrid() {
74
- if (!this.selectionBox)
75
- throw new Error('No selection to snap!');
76
- // Snap the top left corner of what we have selected.
77
- const topLeftOfBBox = this.selectionBox.computeTightBoundingBox().topLeft;
78
- const snappedTopLeft = this.editor.viewport.snapToGrid(topLeftOfBBox);
79
- const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
80
- const oldTransform = this.selectionBox.getTransform();
81
- this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDelta)));
82
- this.selectionBox.finalizeTransform();
83
- }
84
89
  onContextMenu(event) {
85
90
  const canShowSelectionMenu = this.selectionBox
86
91
  ?.getScreenRegion()
@@ -99,7 +104,7 @@ export default class SelectionTool extends BaseTool {
99
104
  let transforming = false;
100
105
  if (this.selectionBox) {
101
106
  if (snapToGrid) {
102
- this.snapSelectionToGrid();
107
+ this.selectionBox.snapSelectedObjectsToGrid();
103
108
  }
104
109
  const dragStartResult = this.selectionBox.onDragStart(current);
105
110
  if (dragStartResult) {
@@ -111,7 +116,13 @@ export default class SelectionTool extends BaseTool {
111
116
  if (!transforming) {
112
117
  // Shift key: Combine the new and old selection boxes at the end of the gesture.
113
118
  this.expandingSelectionBox = this.shiftKeyPressed;
114
- this.rebuildSelectionScheduled = true;
119
+ this.removeSelectionScheduled = !this.expandingSelectionBox;
120
+ if (this.modeValue.get() === SelectionMode.Lasso) {
121
+ this.selectionBuilder = new LassoSelectionBuilder(current.canvasPos, this.editor.viewport);
122
+ }
123
+ else {
124
+ this.selectionBuilder = new RectSelectionBuilder(current.canvasPos);
125
+ }
115
126
  }
116
127
  else {
117
128
  // Only autoscroll if we're transforming an existing selection
@@ -126,13 +137,12 @@ export default class SelectionTool extends BaseTool {
126
137
  }
127
138
  onMainPointerUpdated(currentPointer) {
128
139
  this.lastPointer = currentPointer;
129
- if (this.rebuildSelectionScheduled) {
130
- this.rebuildSelectionScheduled = false;
131
- this.makeSelectionBox(this.startPoint ?? currentPointer.canvasPos);
132
- this.selectionBox?.setHandlesVisible(false);
140
+ if (this.removeSelectionScheduled) {
141
+ this.removeSelectionScheduled = false;
142
+ this.handleOverlay.replaceChildren();
143
+ this.prevSelectionBox = this.selectionBox;
144
+ this.selectionBox = null;
133
145
  }
134
- if (!this.selectionBox)
135
- return;
136
146
  this.autoscroller.onPointerMove(currentPointer.screenPos);
137
147
  if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
138
148
  const screenPos = this.editor.viewport.canvasToScreen(this.startPoint);
@@ -142,47 +152,46 @@ export default class SelectionTool extends BaseTool {
142
152
  currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
143
153
  }
144
154
  if (this.selectionBoxHandlingEvt) {
145
- this.selectionBox.onDragUpdate(currentPointer);
155
+ this.selectionBox?.onDragUpdate(currentPointer);
146
156
  }
147
157
  else {
148
- this.selectionBox.setToPoint(currentPointer.canvasPos);
158
+ this.selectionBuilder?.onPointerMove(currentPointer.canvasPos);
159
+ this.editor.clearWetInk();
160
+ this.selectionBuilder?.render(this.editor.display.getWetInkRenderer(), this.getSelectionColor());
149
161
  }
150
162
  }
151
163
  onPointerUp(event) {
152
164
  this.onMainPointerUpdated(event.current);
153
165
  this.autoscroller.stop();
154
- if (!this.selectionBox)
155
- return;
156
- this.selectionBox.setHandlesVisible(true);
157
- // Were we expanding the previous selection?
158
- if (this.expandingSelectionBox && this.prevSelectionBox) {
159
- // If so, finish expanding.
160
- this.expandingSelectionBox = false;
161
- this.selectionBox.resolveToObjects();
162
- this.setSelection([
163
- ...this.selectionBox.getSelectedObjects(),
164
- ...this.prevSelectionBox.getSelectedObjects(),
165
- ]);
166
+ if (this.selectionBoxHandlingEvt) {
167
+ this.selectionBox?.onDragEnd();
166
168
  }
167
- else {
168
- if (!this.selectionBoxHandlingEvt) {
169
- // Expand/shrink the selection rectangle, if applicable
170
- this.selectionBox.resolveToObjects();
171
- this.onSelectionUpdated();
169
+ else if (this.selectionBuilder) {
170
+ const newSelection = this.selectionBuilder.resolve(this.editor.image, this.editor.viewport);
171
+ this.selectionBuilder = null;
172
+ this.editor.clearWetInk();
173
+ if (this.expandingSelectionBox && this.selectionBox) {
174
+ this.setSelection([...this.selectionBox.getSelectedObjects(), ...newSelection]);
172
175
  }
173
176
  else {
174
- this.selectionBox.onDragEnd();
177
+ this.setSelection(newSelection);
175
178
  }
176
- this.selectionBoxHandlingEvt = false;
177
- this.lastPointer = null;
178
179
  }
180
+ this.expandingSelectionBox = false;
181
+ this.removeSelectionScheduled = false;
182
+ this.selectionBoxHandlingEvt = false;
183
+ this.lastPointer = null;
179
184
  }
180
185
  onGestureCancel() {
186
+ if (this.selectionBuilder) {
187
+ this.selectionBuilder = null;
188
+ this.editor.clearWetInk();
189
+ }
181
190
  this.autoscroller.stop();
182
191
  if (this.selectionBoxHandlingEvt) {
183
192
  this.selectionBox?.onDragCancel();
184
193
  }
185
- else if (!this.rebuildSelectionScheduled) {
194
+ else if (!this.removeSelectionScheduled) {
186
195
  // Revert to the previous selection, if any.
187
196
  this.selectionBox?.cancelSelection();
188
197
  this.selectionBox = this.prevSelectionBox;
@@ -190,7 +199,7 @@ export default class SelectionTool extends BaseTool {
190
199
  this.selectionBox?.recomputeRegion();
191
200
  this.prevSelectionBox = null;
192
201
  }
193
- this.rebuildSelectionScheduled = false;
202
+ this.removeSelectionScheduled = false;
194
203
  this.expandingSelectionBox = false;
195
204
  this.lastPointer = null;
196
205
  this.selectionBoxHandlingEvt = false;
@@ -458,6 +467,10 @@ export default class SelectionTool extends BaseTool {
458
467
  getSelection() {
459
468
  return this.selectionBox;
460
469
  }
470
+ /** @returns true if the selection is currently being created by the user. */
471
+ isSelecting() {
472
+ return !!this.selectionBuilder;
473
+ }
461
474
  getSelectedObjects() {
462
475
  return this.selectionBox?.getSelectedObjects() ?? [];
463
476
  }
@@ -483,20 +496,20 @@ export default class SelectionTool extends BaseTool {
483
496
  bbox = object.getBBox();
484
497
  }
485
498
  }
486
- if (!bbox) {
487
- return;
488
- }
489
- this.clearSelection();
490
- if (!this.selectionBox) {
491
- this.makeSelectionBox(bbox.topLeft);
499
+ this.clearSelectionNoUpdateEvent();
500
+ if (bbox) {
501
+ this.makeSelectionBox(objects);
492
502
  }
493
- this.selectionBox.setSelectedObjects(objects, bbox);
494
503
  this.onSelectionUpdated();
495
504
  }
496
- clearSelection() {
505
+ // Equivalent to .clearSelection, but does not dispatch an update event
506
+ clearSelectionNoUpdateEvent() {
497
507
  this.handleOverlay.replaceChildren();
498
508
  this.prevSelectionBox = this.selectionBox;
499
509
  this.selectionBox = null;
510
+ }
511
+ clearSelection() {
512
+ this.clearSelectionNoUpdateEvent();
500
513
  this.onSelectionUpdated();
501
514
  }
502
515
  }