js-draw 1.26.0 → 1.27.1

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.
Files changed (67) hide show
  1. package/dist/Editor.css +1 -1
  2. package/dist/bundle.js +42 -37
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +0 -2
  5. package/dist/cjs/components/AbstractComponent.d.ts +15 -0
  6. package/dist/cjs/components/AbstractComponent.js +16 -0
  7. package/dist/cjs/components/Stroke.d.ts +1 -0
  8. package/dist/cjs/components/Stroke.js +7 -0
  9. package/dist/cjs/toolbar/IconProvider.d.ts +2 -1
  10. package/dist/cjs/toolbar/IconProvider.js +18 -8
  11. package/dist/cjs/toolbar/localization.d.ts +2 -0
  12. package/dist/cjs/toolbar/localization.js +2 -0
  13. package/dist/cjs/toolbar/widgets/SelectionToolWidget.d.ts +7 -0
  14. package/dist/cjs/toolbar/widgets/SelectionToolWidget.js +109 -28
  15. package/dist/cjs/toolbar/widgets/components/makeButtonGrid.d.ts +17 -0
  16. package/dist/cjs/toolbar/widgets/components/makeButtonGrid.js +40 -0
  17. package/dist/cjs/tools/SelectionTool/Selection.d.ts +2 -3
  18. package/dist/cjs/tools/SelectionTool/Selection.js +19 -40
  19. package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.d.ts +17 -0
  20. package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.js +67 -0
  21. package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.d.ts +13 -0
  22. package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.js +33 -0
  23. package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.d.ts +15 -0
  24. package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.js +39 -0
  25. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +10 -2
  26. package/dist/cjs/tools/SelectionTool/SelectionTool.js +68 -55
  27. package/dist/cjs/tools/SelectionTool/types.d.ts +4 -0
  28. package/dist/cjs/tools/SelectionTool/types.js +6 -1
  29. package/dist/cjs/tools/lib.d.ts +1 -1
  30. package/dist/cjs/tools/lib.js +2 -1
  31. package/dist/cjs/util/ReactiveValue.js +2 -6
  32. package/dist/cjs/version.js +1 -1
  33. package/dist/mjs/Editor.d.ts +0 -2
  34. package/dist/mjs/components/AbstractComponent.d.ts +15 -0
  35. package/dist/mjs/components/AbstractComponent.mjs +16 -0
  36. package/dist/mjs/components/Stroke.d.ts +1 -0
  37. package/dist/mjs/components/Stroke.mjs +7 -0
  38. package/dist/mjs/toolbar/IconProvider.d.ts +2 -1
  39. package/dist/mjs/toolbar/IconProvider.mjs +18 -8
  40. package/dist/mjs/toolbar/localization.d.ts +2 -0
  41. package/dist/mjs/toolbar/localization.mjs +2 -0
  42. package/dist/mjs/toolbar/widgets/SelectionToolWidget.d.ts +7 -0
  43. package/dist/mjs/toolbar/widgets/SelectionToolWidget.mjs +109 -28
  44. package/dist/mjs/toolbar/widgets/components/makeButtonGrid.d.ts +17 -0
  45. package/dist/mjs/toolbar/widgets/components/makeButtonGrid.mjs +35 -0
  46. package/dist/mjs/tools/SelectionTool/Selection.d.ts +2 -3
  47. package/dist/mjs/tools/SelectionTool/Selection.mjs +19 -40
  48. package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.d.ts +17 -0
  49. package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.mjs +61 -0
  50. package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.d.ts +13 -0
  51. package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.mjs +27 -0
  52. package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.d.ts +15 -0
  53. package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.mjs +36 -0
  54. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +10 -2
  55. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +68 -55
  56. package/dist/mjs/tools/SelectionTool/types.d.ts +4 -0
  57. package/dist/mjs/tools/SelectionTool/types.mjs +5 -0
  58. package/dist/mjs/tools/lib.d.ts +1 -1
  59. package/dist/mjs/tools/lib.mjs +1 -1
  60. package/dist/mjs/util/ReactiveValue.mjs +2 -6
  61. package/dist/mjs/version.mjs +1 -1
  62. package/package.json +4 -4
  63. package/src/toolbar/EdgeToolbar.scss +6 -1
  64. package/src/toolbar/widgets/components/components.scss +1 -0
  65. package/src/toolbar/widgets/components/makeButtonGrid.scss +25 -0
  66. package/src/tools/SelectionTool/SelectionTool.scss +1 -0
  67. package/src/tools/util/createMenuOverlay.scss +3 -2
@@ -1,13 +1,16 @@
1
1
  import { Color4 } from '@js-draw/math';
2
2
  import { isRestylableComponent } from '../../components/RestylableComponent.mjs';
3
3
  import uniteCommands from '../../commands/uniteCommands.mjs';
4
+ import { SelectionMode } from '../../tools/SelectionTool/SelectionTool.mjs';
4
5
  import { EditorEventType } from '../../types.mjs';
5
6
  import makeColorInput from './components/makeColorInput.mjs';
6
- import ActionButtonWidget from './ActionButtonWidget.mjs';
7
7
  import BaseToolWidget from './BaseToolWidget.mjs';
8
8
  import { resizeImageToSelectionKeyboardShortcut } from './keybindings.mjs';
9
9
  import makeSeparator from './components/makeSeparator.mjs';
10
10
  import { toolbarCSSPrefix } from '../constants.mjs';
11
+ import BaseWidget from './BaseWidget.mjs';
12
+ import makeButtonGrid from './components/makeButtonGrid.mjs';
13
+ import { MutableReactiveValue } from '../../util/ReactiveValue.mjs';
11
14
  const makeFormatMenu = (editor, selectionTool, localizationTable) => {
12
15
  const container = document.createElement('div');
13
16
  container.classList.add('selection-format-menu', `${toolbarCSSPrefix}spacedList`, `${toolbarCSSPrefix}indentedList`);
@@ -63,48 +66,63 @@ const makeFormatMenu = (editor, selectionTool, localizationTable) => {
63
66
  },
64
67
  };
65
68
  };
69
+ class LassoSelectToggle extends BaseWidget {
70
+ constructor(editor, tool, localizationTable) {
71
+ super(editor, 'selection-mode-toggle', localizationTable);
72
+ this.tool = tool;
73
+ editor.notifier.on(EditorEventType.ToolUpdated, (toolEvt) => {
74
+ if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === tool) {
75
+ this.setSelected(tool.modeValue.get() === SelectionMode.Lasso);
76
+ }
77
+ });
78
+ this.setSelected(false);
79
+ }
80
+ shouldAutoDisableInReadOnlyEditor() {
81
+ return false;
82
+ }
83
+ setModeFlag(enabled) {
84
+ this.tool.modeValue.set(enabled ? SelectionMode.Lasso : SelectionMode.Rectangle);
85
+ }
86
+ handleClick() {
87
+ this.setModeFlag(!this.isSelected());
88
+ }
89
+ getTitle() {
90
+ return this.localizationTable.selectionTool__lassoSelect;
91
+ }
92
+ createIcon() {
93
+ return this.editor.icons.makeSelectionIcon(SelectionMode.Lasso);
94
+ }
95
+ fillDropdown(_dropdown) {
96
+ return false;
97
+ }
98
+ getHelpText() {
99
+ return this.localizationTable.selectionTool__lassoSelect__help;
100
+ }
101
+ }
66
102
  export default class SelectionToolWidget extends BaseToolWidget {
67
103
  constructor(editor, tool, localization) {
68
104
  super(editor, tool, 'selection-tool-widget', localization);
69
105
  this.tool = tool;
70
106
  this.updateFormatMenu = () => { };
71
- const resizeButton = new ActionButtonWidget(editor, 'resize-btn', () => editor.icons.makeResizeImageToSelectionIcon(), this.localizationTable.resizeImageToSelection, () => {
72
- this.resizeImageToSelection();
73
- }, localization);
74
- resizeButton.setHelpText(this.localizationTable.selectionDropdown__resizeToHelpText);
75
- const deleteButton = new ActionButtonWidget(editor, 'delete-btn', () => editor.icons.makeDeleteSelectionIcon(), this.localizationTable.deleteSelection, () => {
76
- const selection = this.tool.getSelection();
77
- this.editor.dispatch(selection.deleteSelectedObjects());
78
- this.tool.clearSelection();
79
- }, localization);
80
- deleteButton.setHelpText(this.localizationTable.selectionDropdown__deleteHelpText);
81
- const duplicateButton = new ActionButtonWidget(editor, 'duplicate-btn', () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, async () => {
107
+ this.addSubWidget(new LassoSelectToggle(editor, tool, this.localizationTable));
108
+ const hasSelection = () => {
82
109
  const selection = this.tool.getSelection();
83
- this.editor.dispatch(await selection.duplicateSelectedObjects());
84
- this.setDropdownVisible(false);
85
- }, localization);
86
- duplicateButton.setHelpText(this.localizationTable.selectionDropdown__duplicateHelpText);
87
- this.addSubWidget(resizeButton);
88
- this.addSubWidget(deleteButton);
89
- this.addSubWidget(duplicateButton);
90
- const updateDisabled = (disabled) => {
91
- resizeButton.setDisabled(disabled);
92
- deleteButton.setDisabled(disabled);
93
- duplicateButton.setDisabled(disabled);
110
+ return !!selection && selection.getSelectedItemCount() > 0;
94
111
  };
95
- updateDisabled(true);
112
+ this.hasSelectionValue = MutableReactiveValue.fromInitialValue(hasSelection());
96
113
  // Enable/disable actions based on whether items are selected
97
114
  this.editor.notifier.on(EditorEventType.ToolUpdated, (toolEvt) => {
98
115
  if (toolEvt.kind !== EditorEventType.ToolUpdated) {
99
116
  throw new Error('Invalid event type!');
100
117
  }
101
118
  if (toolEvt.tool === this.tool) {
102
- const selection = this.tool.getSelection();
103
- const hasSelection = selection && selection.getSelectedItemCount() > 0;
104
- updateDisabled(!hasSelection);
119
+ this.hasSelectionValue.set(hasSelection());
105
120
  this.updateFormatMenu();
106
121
  }
107
122
  });
123
+ tool.modeValue.onUpdate(() => {
124
+ this.updateIcon();
125
+ });
108
126
  }
109
127
  resizeImageToSelection() {
110
128
  const selection = this.tool.getSelection();
@@ -130,16 +148,66 @@ export default class SelectionToolWidget extends BaseToolWidget {
130
148
  return this.localizationTable.select;
131
149
  }
132
150
  createIcon() {
133
- return this.editor.icons.makeSelectionIcon();
151
+ return this.editor.icons.makeSelectionIcon(this.tool.modeValue.get());
134
152
  }
135
153
  getHelpText() {
136
154
  return this.localizationTable.selectionDropdown__baseHelpText;
137
155
  }
156
+ createSelectionActions(helpDisplay) {
157
+ const icons = this.editor.icons;
158
+ const grid = makeButtonGrid([
159
+ {
160
+ icon: () => icons.makeDeleteSelectionIcon(),
161
+ label: this.localizationTable.deleteSelection,
162
+ onCreated: (button) => {
163
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__deleteHelpText);
164
+ },
165
+ onClick: () => {
166
+ const selection = this.tool.getSelection();
167
+ this.editor.dispatch(selection.deleteSelectedObjects());
168
+ this.tool.clearSelection();
169
+ },
170
+ enabled: this.hasSelectionValue,
171
+ },
172
+ {
173
+ icon: () => icons.makeDuplicateSelectionIcon(),
174
+ label: this.localizationTable.duplicateSelection,
175
+ onCreated: (button) => {
176
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__duplicateHelpText);
177
+ },
178
+ onClick: async () => {
179
+ const selection = this.tool.getSelection();
180
+ const command = await selection?.duplicateSelectedObjects();
181
+ if (command) {
182
+ this.editor.dispatch(command);
183
+ }
184
+ },
185
+ enabled: this.hasSelectionValue,
186
+ },
187
+ {
188
+ icon: () => icons.makeResizeImageToSelectionIcon(),
189
+ label: this.localizationTable.resizeImageToSelection,
190
+ onCreated: (button) => {
191
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__resizeToHelpText);
192
+ },
193
+ onClick: () => {
194
+ this.resizeImageToSelection();
195
+ },
196
+ enabled: this.hasSelectionValue,
197
+ },
198
+ ], 3);
199
+ return { container: grid.container };
200
+ }
138
201
  fillDropdown(dropdown, helpDisplay) {
139
202
  super.fillDropdown(dropdown, helpDisplay);
140
203
  const controlsContainer = document.createElement('div');
141
204
  controlsContainer.classList.add(`${toolbarCSSPrefix}nonbutton-controls-main-list`);
142
205
  dropdown.appendChild(controlsContainer);
206
+ // Actions (duplicate, delete, etc.)
207
+ makeSeparator().addTo(controlsContainer);
208
+ const actions = this.createSelectionActions(helpDisplay);
209
+ controlsContainer.appendChild(actions.container);
210
+ // Formatting
143
211
  makeSeparator(this.localizationTable.reformatSelection).addTo(controlsContainer);
144
212
  const formatMenu = makeFormatMenu(this.editor, this.tool, this.localizationTable);
145
213
  formatMenu.addTo(controlsContainer);
@@ -150,4 +218,17 @@ export default class SelectionToolWidget extends BaseToolWidget {
150
218
  formatMenu.update();
151
219
  return true;
152
220
  }
221
+ serializeState() {
222
+ return {
223
+ ...super.serializeState(),
224
+ selectionMode: this.tool.modeValue.get(),
225
+ };
226
+ }
227
+ deserializeFrom(state) {
228
+ super.deserializeFrom(state);
229
+ const isValidSelectionMode = Object.values(SelectionMode).includes(state.selectionMode);
230
+ if (isValidSelectionMode) {
231
+ this.tool.modeValue.set(state.selectionMode);
232
+ }
233
+ }
153
234
  }
@@ -0,0 +1,17 @@
1
+ import { ReactiveValue } from '../../../util/ReactiveValue';
2
+ import { IconElemType } from '../../IconProvider';
3
+ interface Button {
4
+ icon: () => IconElemType;
5
+ label: string;
6
+ onClick: () => void;
7
+ onCreated?: (button: HTMLElement) => void;
8
+ enabled?: ReactiveValue<boolean>;
9
+ }
10
+ /**
11
+ * Creates HTML `button` elements from `buttonSpecs` and displays them in a
12
+ * grid with `columnCount` columns.
13
+ */
14
+ declare const makeButtonGrid: (buttonSpecs: Button[], columnCount: number) => {
15
+ container: HTMLDivElement;
16
+ };
17
+ export default makeButtonGrid;
@@ -0,0 +1,35 @@
1
+ import addLongPressOrHoverCssClasses from '../../../util/addLongPressOrHoverCssClasses.mjs';
2
+ /**
3
+ * Creates HTML `button` elements from `buttonSpecs` and displays them in a
4
+ * grid with `columnCount` columns.
5
+ */
6
+ const makeButtonGrid = (buttonSpecs, columnCount) => {
7
+ const container = document.createElement('div');
8
+ container.classList.add('toolbar-button-grid');
9
+ container.style.setProperty('--column-count', `${columnCount}`);
10
+ const makeButton = (buttonSpec) => {
11
+ const buttonElement = document.createElement('button');
12
+ buttonElement.classList.add('button');
13
+ const iconElement = buttonSpec.icon();
14
+ iconElement.classList.add('icon');
15
+ const labelElement = document.createElement('label');
16
+ labelElement.textContent = buttonSpec.label;
17
+ labelElement.classList.add('button-label-text');
18
+ buttonElement.onclick = buttonSpec.onClick;
19
+ if (buttonSpec.enabled) {
20
+ buttonSpec.enabled.onUpdateAndNow((enabled) => {
21
+ buttonElement.disabled = !enabled;
22
+ });
23
+ }
24
+ buttonElement.replaceChildren(iconElement, labelElement);
25
+ container.appendChild(buttonElement);
26
+ addLongPressOrHoverCssClasses(buttonElement);
27
+ buttonSpec.onCreated?.(buttonElement);
28
+ return buttonElement;
29
+ };
30
+ buttonSpecs.map(makeButton);
31
+ return {
32
+ container,
33
+ };
34
+ };
35
+ export default makeButtonGrid;
@@ -20,7 +20,7 @@ export default class Selection {
20
20
  private innerContainer;
21
21
  private backgroundElem;
22
22
  private hasParent;
23
- constructor(startPoint: Point2, editor: Editor, showContextMenu: (anchor: Point2) => void);
23
+ constructor(selectedElems: AbstractComponent[], editor: Editor, showContextMenu: (anchor: Point2) => void);
24
24
  getBackgroundElem(): HTMLElement;
25
25
  getTransform(): Mat33;
26
26
  get preTransformRegion(): Rect2;
@@ -43,7 +43,6 @@ export default class Selection {
43
43
  sendToBack(): SerializableCommand | null;
44
44
  private static ApplyTransformationCommand;
45
45
  private previewTransformCmds;
46
- resolveToObjects(): boolean;
47
46
  recomputeRegion(): boolean;
48
47
  padRegion(): void;
49
48
  getMinCanvasSize(): number;
@@ -63,10 +62,10 @@ export default class Selection {
63
62
  private selectionDuplicatedAnimationTimeout;
64
63
  private runSelectionDuplicatedAnimation;
65
64
  duplicateSelectedObjects(): Promise<Command>;
65
+ snapSelectedObjectsToGrid(): void;
66
66
  setHandlesVisible(showHandles: boolean): void;
67
67
  addTo(elem: HTMLElement): void;
68
68
  setToPoint(point: Point2): void;
69
69
  cancelSelection(): void;
70
- setSelectedObjects(objects: AbstractComponent[], bbox: Rect2): void;
71
70
  getSelectedObjects(): AbstractComponent[];
72
71
  }
@@ -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),
@@ -83,6 +85,7 @@ class Selection {
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;
@@ -479,6 +458,16 @@ class Selection {
479
458
  }
480
459
  return command;
481
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();
470
+ }
482
471
  setHandlesVisible(showHandles) {
483
472
  if (!showHandles) {
484
473
  this.innerContainer.classList.add('-hide-handles');
@@ -507,16 +496,6 @@ class Selection {
507
496
  this.selectionTightBoundingBox = null;
508
497
  this.hasParent = false;
509
498
  }
510
- setSelectedObjects(objects, bbox) {
511
- this.addRemoveSelectionFromImage(true);
512
- this.originalRegion = bbox;
513
- this.selectionTightBoundingBox = bbox;
514
- this.selectedElems = objects.filter((object) => object.isSelectable());
515
- // Enforce increasing z-index invariant
516
- this.selectedElems.sort((a, b) => a.getZIndex() - b.getZIndex());
517
- this.padRegion();
518
- this.updateUI();
519
- }
520
499
  getSelectedObjects() {
521
500
  return [...this.selectedElems];
522
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
+ }
@@ -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
  }