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
@@ -4,14 +4,21 @@ import { KeyPressEvent } from '../../inputEvents';
4
4
  import { ToolbarLocalization } from '../localization';
5
5
  import BaseToolWidget from './BaseToolWidget';
6
6
  import HelpDisplay from '../utils/HelpDisplay';
7
+ import { SavedToolbuttonState } from './BaseWidget';
7
8
  export default class SelectionToolWidget extends BaseToolWidget {
8
9
  private tool;
9
10
  private updateFormatMenu;
11
+ private hasSelectionValue;
10
12
  constructor(editor: Editor, tool: SelectionTool, localization?: ToolbarLocalization);
11
13
  private resizeImageToSelection;
12
14
  protected onKeyPress(event: KeyPressEvent): boolean;
13
15
  protected getTitle(): string;
14
16
  protected createIcon(): Element;
15
17
  protected getHelpText(): string;
18
+ protected createSelectionActions(helpDisplay?: HelpDisplay): {
19
+ container: HTMLDivElement;
20
+ };
16
21
  protected fillDropdown(dropdown: HTMLElement, helpDisplay?: HelpDisplay): boolean;
22
+ serializeState(): SavedToolbuttonState;
23
+ deserializeFrom(state: SavedToolbuttonState): void;
17
24
  }
@@ -6,13 +6,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const math_1 = require("@js-draw/math");
7
7
  const RestylableComponent_1 = require("../../components/RestylableComponent");
8
8
  const uniteCommands_1 = __importDefault(require("../../commands/uniteCommands"));
9
+ const SelectionTool_1 = require("../../tools/SelectionTool/SelectionTool");
9
10
  const types_1 = require("../../types");
10
11
  const makeColorInput_1 = __importDefault(require("./components/makeColorInput"));
11
- const ActionButtonWidget_1 = __importDefault(require("./ActionButtonWidget"));
12
12
  const BaseToolWidget_1 = __importDefault(require("./BaseToolWidget"));
13
13
  const keybindings_1 = require("./keybindings");
14
14
  const makeSeparator_1 = __importDefault(require("./components/makeSeparator"));
15
15
  const constants_1 = require("../constants");
16
+ const BaseWidget_1 = __importDefault(require("./BaseWidget"));
17
+ const makeButtonGrid_1 = __importDefault(require("./components/makeButtonGrid"));
18
+ const ReactiveValue_1 = require("../../util/ReactiveValue");
16
19
  const makeFormatMenu = (editor, selectionTool, localizationTable) => {
17
20
  const container = document.createElement('div');
18
21
  container.classList.add('selection-format-menu', `${constants_1.toolbarCSSPrefix}spacedList`, `${constants_1.toolbarCSSPrefix}indentedList`);
@@ -68,48 +71,63 @@ const makeFormatMenu = (editor, selectionTool, localizationTable) => {
68
71
  },
69
72
  };
70
73
  };
74
+ class LassoSelectToggle extends BaseWidget_1.default {
75
+ constructor(editor, tool, localizationTable) {
76
+ super(editor, 'selection-mode-toggle', localizationTable);
77
+ this.tool = tool;
78
+ editor.notifier.on(types_1.EditorEventType.ToolUpdated, (toolEvt) => {
79
+ if (toolEvt.kind === types_1.EditorEventType.ToolUpdated && toolEvt.tool === tool) {
80
+ this.setSelected(tool.modeValue.get() === SelectionTool_1.SelectionMode.Lasso);
81
+ }
82
+ });
83
+ this.setSelected(false);
84
+ }
85
+ shouldAutoDisableInReadOnlyEditor() {
86
+ return false;
87
+ }
88
+ setModeFlag(enabled) {
89
+ this.tool.modeValue.set(enabled ? SelectionTool_1.SelectionMode.Lasso : SelectionTool_1.SelectionMode.Rectangle);
90
+ }
91
+ handleClick() {
92
+ this.setModeFlag(!this.isSelected());
93
+ }
94
+ getTitle() {
95
+ return this.localizationTable.selectionTool__lassoSelect;
96
+ }
97
+ createIcon() {
98
+ return this.editor.icons.makeSelectionIcon(SelectionTool_1.SelectionMode.Lasso);
99
+ }
100
+ fillDropdown(_dropdown) {
101
+ return false;
102
+ }
103
+ getHelpText() {
104
+ return this.localizationTable.selectionTool__lassoSelect__help;
105
+ }
106
+ }
71
107
  class SelectionToolWidget extends BaseToolWidget_1.default {
72
108
  constructor(editor, tool, localization) {
73
109
  super(editor, tool, 'selection-tool-widget', localization);
74
110
  this.tool = tool;
75
111
  this.updateFormatMenu = () => { };
76
- const resizeButton = new ActionButtonWidget_1.default(editor, 'resize-btn', () => editor.icons.makeResizeImageToSelectionIcon(), this.localizationTable.resizeImageToSelection, () => {
77
- this.resizeImageToSelection();
78
- }, localization);
79
- resizeButton.setHelpText(this.localizationTable.selectionDropdown__resizeToHelpText);
80
- const deleteButton = new ActionButtonWidget_1.default(editor, 'delete-btn', () => editor.icons.makeDeleteSelectionIcon(), this.localizationTable.deleteSelection, () => {
81
- const selection = this.tool.getSelection();
82
- this.editor.dispatch(selection.deleteSelectedObjects());
83
- this.tool.clearSelection();
84
- }, localization);
85
- deleteButton.setHelpText(this.localizationTable.selectionDropdown__deleteHelpText);
86
- const duplicateButton = new ActionButtonWidget_1.default(editor, 'duplicate-btn', () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, async () => {
112
+ this.addSubWidget(new LassoSelectToggle(editor, tool, this.localizationTable));
113
+ const hasSelection = () => {
87
114
  const selection = this.tool.getSelection();
88
- this.editor.dispatch(await selection.duplicateSelectedObjects());
89
- this.setDropdownVisible(false);
90
- }, localization);
91
- duplicateButton.setHelpText(this.localizationTable.selectionDropdown__duplicateHelpText);
92
- this.addSubWidget(resizeButton);
93
- this.addSubWidget(deleteButton);
94
- this.addSubWidget(duplicateButton);
95
- const updateDisabled = (disabled) => {
96
- resizeButton.setDisabled(disabled);
97
- deleteButton.setDisabled(disabled);
98
- duplicateButton.setDisabled(disabled);
115
+ return !!selection && selection.getSelectedItemCount() > 0;
99
116
  };
100
- updateDisabled(true);
117
+ this.hasSelectionValue = ReactiveValue_1.MutableReactiveValue.fromInitialValue(hasSelection());
101
118
  // Enable/disable actions based on whether items are selected
102
119
  this.editor.notifier.on(types_1.EditorEventType.ToolUpdated, (toolEvt) => {
103
120
  if (toolEvt.kind !== types_1.EditorEventType.ToolUpdated) {
104
121
  throw new Error('Invalid event type!');
105
122
  }
106
123
  if (toolEvt.tool === this.tool) {
107
- const selection = this.tool.getSelection();
108
- const hasSelection = selection && selection.getSelectedItemCount() > 0;
109
- updateDisabled(!hasSelection);
124
+ this.hasSelectionValue.set(hasSelection());
110
125
  this.updateFormatMenu();
111
126
  }
112
127
  });
128
+ tool.modeValue.onUpdate(() => {
129
+ this.updateIcon();
130
+ });
113
131
  }
114
132
  resizeImageToSelection() {
115
133
  const selection = this.tool.getSelection();
@@ -135,16 +153,66 @@ class SelectionToolWidget extends BaseToolWidget_1.default {
135
153
  return this.localizationTable.select;
136
154
  }
137
155
  createIcon() {
138
- return this.editor.icons.makeSelectionIcon();
156
+ return this.editor.icons.makeSelectionIcon(this.tool.modeValue.get());
139
157
  }
140
158
  getHelpText() {
141
159
  return this.localizationTable.selectionDropdown__baseHelpText;
142
160
  }
161
+ createSelectionActions(helpDisplay) {
162
+ const icons = this.editor.icons;
163
+ const grid = (0, makeButtonGrid_1.default)([
164
+ {
165
+ icon: () => icons.makeDeleteSelectionIcon(),
166
+ label: this.localizationTable.deleteSelection,
167
+ onCreated: (button) => {
168
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__deleteHelpText);
169
+ },
170
+ onClick: () => {
171
+ const selection = this.tool.getSelection();
172
+ this.editor.dispatch(selection.deleteSelectedObjects());
173
+ this.tool.clearSelection();
174
+ },
175
+ enabled: this.hasSelectionValue,
176
+ },
177
+ {
178
+ icon: () => icons.makeDuplicateSelectionIcon(),
179
+ label: this.localizationTable.duplicateSelection,
180
+ onCreated: (button) => {
181
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__duplicateHelpText);
182
+ },
183
+ onClick: async () => {
184
+ const selection = this.tool.getSelection();
185
+ const command = await selection?.duplicateSelectedObjects();
186
+ if (command) {
187
+ this.editor.dispatch(command);
188
+ }
189
+ },
190
+ enabled: this.hasSelectionValue,
191
+ },
192
+ {
193
+ icon: () => icons.makeResizeImageToSelectionIcon(),
194
+ label: this.localizationTable.resizeImageToSelection,
195
+ onCreated: (button) => {
196
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__resizeToHelpText);
197
+ },
198
+ onClick: () => {
199
+ this.resizeImageToSelection();
200
+ },
201
+ enabled: this.hasSelectionValue,
202
+ },
203
+ ], 3);
204
+ return { container: grid.container };
205
+ }
143
206
  fillDropdown(dropdown, helpDisplay) {
144
207
  super.fillDropdown(dropdown, helpDisplay);
145
208
  const controlsContainer = document.createElement('div');
146
209
  controlsContainer.classList.add(`${constants_1.toolbarCSSPrefix}nonbutton-controls-main-list`);
147
210
  dropdown.appendChild(controlsContainer);
211
+ // Actions (duplicate, delete, etc.)
212
+ (0, makeSeparator_1.default)().addTo(controlsContainer);
213
+ const actions = this.createSelectionActions(helpDisplay);
214
+ controlsContainer.appendChild(actions.container);
215
+ // Formatting
148
216
  (0, makeSeparator_1.default)(this.localizationTable.reformatSelection).addTo(controlsContainer);
149
217
  const formatMenu = makeFormatMenu(this.editor, this.tool, this.localizationTable);
150
218
  formatMenu.addTo(controlsContainer);
@@ -155,5 +223,18 @@ class SelectionToolWidget extends BaseToolWidget_1.default {
155
223
  formatMenu.update();
156
224
  return true;
157
225
  }
226
+ serializeState() {
227
+ return {
228
+ ...super.serializeState(),
229
+ selectionMode: this.tool.modeValue.get(),
230
+ };
231
+ }
232
+ deserializeFrom(state) {
233
+ super.deserializeFrom(state);
234
+ const isValidSelectionMode = Object.values(SelectionTool_1.SelectionMode).includes(state.selectionMode);
235
+ if (isValidSelectionMode) {
236
+ this.tool.modeValue.set(state.selectionMode);
237
+ }
238
+ }
158
239
  }
159
240
  exports.default = SelectionToolWidget;
@@ -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,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const addLongPressOrHoverCssClasses_1 = __importDefault(require("../../../util/addLongPressOrHoverCssClasses"));
7
+ /**
8
+ * Creates HTML `button` elements from `buttonSpecs` and displays them in a
9
+ * grid with `columnCount` columns.
10
+ */
11
+ const makeButtonGrid = (buttonSpecs, columnCount) => {
12
+ const container = document.createElement('div');
13
+ container.classList.add('toolbar-button-grid');
14
+ container.style.setProperty('--column-count', `${columnCount}`);
15
+ const makeButton = (buttonSpec) => {
16
+ const buttonElement = document.createElement('button');
17
+ buttonElement.classList.add('button');
18
+ const iconElement = buttonSpec.icon();
19
+ iconElement.classList.add('icon');
20
+ const labelElement = document.createElement('label');
21
+ labelElement.textContent = buttonSpec.label;
22
+ labelElement.classList.add('button-label-text');
23
+ buttonElement.onclick = buttonSpec.onClick;
24
+ if (buttonSpec.enabled) {
25
+ buttonSpec.enabled.onUpdateAndNow((enabled) => {
26
+ buttonElement.disabled = !enabled;
27
+ });
28
+ }
29
+ buttonElement.replaceChildren(iconElement, labelElement);
30
+ container.appendChild(buttonElement);
31
+ (0, addLongPressOrHoverCssClasses_1.default)(buttonElement);
32
+ buttonSpec.onCreated?.(buttonElement);
33
+ return buttonElement;
34
+ };
35
+ buttonSpecs.map(makeButton);
36
+ return {
37
+ container,
38
+ };
39
+ };
40
+ exports.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
  }
@@ -57,7 +57,7 @@ const updateChunkSize = 100;
57
57
  const maxPreviewElemCount = 500;
58
58
  // @internal
59
59
  class Selection {
60
- constructor(startPoint, editor, showContextMenu) {
60
+ constructor(selectedElems, editor, showContextMenu) {
61
61
  this.editor = editor;
62
62
  // The last-computed bounding box of selected content
63
63
  // @see getTightBoundingBox
@@ -71,7 +71,9 @@ class Selection {
71
71
  this.activeHandle = null;
72
72
  this.backgroundDragging = false;
73
73
  this.selectionDuplicatedAnimationTimeout = null;
74
- this.originalRegion = new math_1.Rect2(startPoint.x, startPoint.y, 0, 0);
74
+ selectedElems = [...selectedElems];
75
+ this.selectedElems = selectedElems;
76
+ this.originalRegion = math_1.Rect2.empty;
75
77
  this.transformers = {
76
78
  drag: new TransformMode_1.DragTransformer(editor, this),
77
79
  resize: new TransformMode_1.ResizeTransformer(editor, this),
@@ -110,17 +112,18 @@ class Selection {
110
112
  side: math_1.Vec2.of(0.5, 0),
111
113
  icon: this.editor.icons.makeRotateIcon(),
112
114
  }, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
113
- const menuToggleButton = new SelectionMenuShortcut_1.default(this, this.editor.viewport, showContextMenu, this.editor.localization);
115
+ const menuToggleButton = new SelectionMenuShortcut_1.default(this, this.editor.viewport, this.editor.icons.makeOverflowIcon(), showContextMenu, this.editor.localization);
114
116
  this.childwidgets = [
117
+ menuToggleButton,
115
118
  resizeBothHandle,
116
119
  ...resizeHorizontalHandles,
117
120
  resizeVerticalHandle,
118
121
  rotationHandle,
119
- menuToggleButton,
120
122
  ];
121
123
  for (const widget of this.childwidgets) {
122
124
  widget.addTo(this.backgroundElem);
123
125
  }
126
+ this.recomputeRegion();
124
127
  this.updateUI();
125
128
  }
126
129
  // @internal Intended for unit tests
@@ -243,34 +246,6 @@ class Selection {
243
246
  wetInkRenderer.popTransform();
244
247
  this.updateUI();
245
248
  }
246
- // Find the objects corresponding to this in the document,
247
- // select them.
248
- // Returns false iff nothing was selected.
249
- resolveToObjects() {
250
- let singleItemSelectionMode = false;
251
- this.transform = math_1.Mat33.identity;
252
- // Grow the rectangle, if necessary
253
- if (this.region.w === 0 || this.region.h === 0) {
254
- const padding = this.editor.viewport.visibleRect.maxDimension / 200;
255
- this.originalRegion = math_1.Rect2.bboxOf(this.region.corners, padding);
256
- // Only select one item if the rectangle was very small.
257
- singleItemSelectionMode = true;
258
- }
259
- this.selectedElems = this.editor.image
260
- .getElementsIntersectingRegion(this.region)
261
- .filter((elem) => {
262
- return elem.intersectsRect(this.region) && elem.isSelectable();
263
- });
264
- if (singleItemSelectionMode && this.selectedElems.length > 0) {
265
- this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
266
- }
267
- // Find the bounding box of all selected elements.
268
- if (!this.recomputeRegion()) {
269
- return false;
270
- }
271
- this.updateUI();
272
- return true;
273
- }
274
249
  // Recompute this' region from the selected elements.
275
250
  // Returns false if the selection is empty.
276
251
  recomputeRegion() {
@@ -392,6 +367,10 @@ class Selection {
392
367
  });
393
368
  }
394
369
  onDragStart(pointer) {
370
+ // If empty, it isn't possible to drag
371
+ if (this.selectedElems.length === 0) {
372
+ return false;
373
+ }
395
374
  // Clear the HTML selection (prevent HTML drag and drop being triggered by this drag)
396
375
  document.getSelection()?.removeAllRanges();
397
376
  this.activeHandle = null;
@@ -492,6 +471,7 @@ class Selection {
492
471
  if (!wasTransforming) {
493
472
  this.runSelectionDuplicatedAnimation();
494
473
  }
474
+ let command;
495
475
  if (wasTransforming) {
496
476
  // Don't update the selection's focus when redoing/undoing
497
477
  const selectionToUpdate = null;
@@ -501,16 +481,30 @@ class Selection {
501
481
  await tmpApplyCommand.apply(this.editor);
502
482
  // Show items again
503
483
  this.addRemoveSelectionFromImage(true);
504
- }
505
- const duplicateCommand = new Duplicate_1.default(this.selectedElems);
506
- if (wasTransforming) {
484
+ // With the transformation applied, create the duplicates
485
+ command = (0, uniteCommands_1.default)(this.selectedElems.map((elem) => {
486
+ return EditorImage_1.default.addElement(elem.clone());
487
+ }));
507
488
  // Move the selected objects back to the correct location.
508
489
  await tmpApplyCommand?.unapply(this.editor);
509
490
  this.addRemoveSelectionFromImage(false);
510
491
  this.previewTransformCmds();
511
492
  this.updateUI();
512
493
  }
513
- return duplicateCommand;
494
+ else {
495
+ command = new Duplicate_1.default(this.selectedElems);
496
+ }
497
+ return command;
498
+ }
499
+ snapSelectedObjectsToGrid() {
500
+ const viewport = this.editor.viewport;
501
+ // Snap the top left corner of what we have selected.
502
+ const topLeftOfBBox = this.computeTightBoundingBox().topLeft;
503
+ const snappedTopLeft = viewport.snapToGrid(topLeftOfBBox);
504
+ const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
505
+ const oldTransform = this.getTransform();
506
+ this.setTransform(oldTransform.rightMul(math_1.Mat33.translation(snapDelta)));
507
+ this.finalizeTransform();
514
508
  }
515
509
  setHandlesVisible(showHandles) {
516
510
  if (!showHandles) {
@@ -540,16 +534,6 @@ class Selection {
540
534
  this.selectionTightBoundingBox = null;
541
535
  this.hasParent = false;
542
536
  }
543
- setSelectedObjects(objects, bbox) {
544
- this.addRemoveSelectionFromImage(true);
545
- this.originalRegion = bbox;
546
- this.selectionTightBoundingBox = bbox;
547
- this.selectedElems = objects.filter((object) => object.isSelectable());
548
- // Enforce increasing z-index invariant
549
- this.selectedElems.sort((a, b) => a.getZIndex() - b.getZIndex());
550
- this.padRegion();
551
- this.updateUI();
552
- }
553
537
  getSelectedObjects() {
554
538
  return [...this.selectedElems];
555
539
  }
@@ -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,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const math_1 = require("@js-draw/math");
7
+ const math_2 = require("@js-draw/math");
8
+ const SelectionBuilder_1 = __importDefault(require("./SelectionBuilder"));
9
+ /**
10
+ * Creates lasso selections.
11
+ */
12
+ class LassoSelectionBuilder extends SelectionBuilder_1.default {
13
+ constructor(startPoint, viewport) {
14
+ super();
15
+ this.viewport = viewport;
16
+ this.boundaryPoints = [];
17
+ this.boundaryPoints.push(startPoint);
18
+ this.lastPoint = startPoint;
19
+ }
20
+ onPointerMove(canvasPoint) {
21
+ const lastBoundaryPoint = this.boundaryPoints[this.boundaryPoints.length - 1];
22
+ const minBoundaryDist = this.viewport.getSizeOfPixelOnCanvas() * 8;
23
+ if (lastBoundaryPoint.distanceTo(canvasPoint) >= minBoundaryDist) {
24
+ this.boundaryPoints.push(canvasPoint);
25
+ }
26
+ this.lastPoint = canvasPoint;
27
+ }
28
+ previewPath() {
29
+ const pathCommands = this.boundaryPoints.map((point) => {
30
+ return { kind: math_2.PathCommandType.LineTo, point };
31
+ });
32
+ pathCommands.push({
33
+ kind: math_2.PathCommandType.LineTo,
34
+ point: this.lastPoint,
35
+ });
36
+ return new math_1.Path(this.boundaryPoints[0], pathCommands).asClosed();
37
+ }
38
+ resolveInternal(image) {
39
+ const path = this.previewPath();
40
+ const lines = path.polylineApproximation();
41
+ const candidates = image.getElementsIntersectingRegion(path.bbox);
42
+ const componentIsInSelection = (component) => {
43
+ if (path.closedContainsRect(component.getExactBBox())) {
44
+ return true;
45
+ }
46
+ let hasKeyPoint = false;
47
+ for (const point of component.keyPoints()) {
48
+ if (path.closedContainsPoint(point)) {
49
+ hasKeyPoint = true;
50
+ break;
51
+ }
52
+ }
53
+ if (!hasKeyPoint) {
54
+ return false;
55
+ }
56
+ // Only select if completely contained within the lasso
57
+ for (const line of lines) {
58
+ if (component.intersects(line)) {
59
+ return false;
60
+ }
61
+ }
62
+ return true;
63
+ };
64
+ return candidates.filter(componentIsInSelection);
65
+ }
66
+ }
67
+ exports.default = LassoSelectionBuilder;
@@ -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,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const math_1 = require("@js-draw/math");
7
+ const SelectionBuilder_1 = __importDefault(require("./SelectionBuilder"));
8
+ /**
9
+ * Creates rectangle selections
10
+ */
11
+ class RectSelectionBuilder extends SelectionBuilder_1.default {
12
+ constructor(startPoint) {
13
+ super();
14
+ this.rect = math_1.Rect2.fromCorners(startPoint, startPoint);
15
+ }
16
+ onPointerMove(canvasPoint) {
17
+ this.rect = this.rect.grownToPoint(canvasPoint);
18
+ }
19
+ previewPath() {
20
+ return math_1.Path.fromRect(this.rect);
21
+ }
22
+ resolveInternal(image) {
23
+ return image.getElementsIntersectingRegion(this.rect).filter((element) => {
24
+ // Filter out the case where the selection rectangle is completely contained
25
+ // within the element (and does not intersect it).
26
+ // This is useful, for example, if a very large stroke is used as the background
27
+ // for another drawing. This prevents the very large stroke from being selected
28
+ // unless the selection touches one of its edges.
29
+ return element.intersectsRect(this.rect);
30
+ });
31
+ }
32
+ }
33
+ exports.default = RectSelectionBuilder;
@@ -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,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const RenderablePathSpec_1 = require("../../../rendering/RenderablePathSpec");
4
+ class SelectionBuilder {
5
+ /** Renders a preview of the selection bounds */
6
+ render(renderer, color) {
7
+ renderer.drawPath((0, RenderablePathSpec_1.pathToRenderable)(this.previewPath(), { fill: color }));
8
+ }
9
+ /** Converts the selection preview into a set of selected elements */
10
+ resolve(image, viewport) {
11
+ const path = this.previewPath();
12
+ const filterComponents = (components) => {
13
+ return components.filter((component) => {
14
+ return component.isSelectable();
15
+ });
16
+ };
17
+ let components;
18
+ // If the bounding box is very small, search for items **near** the bounding box,
19
+ // rather than in the bounding box.
20
+ const clickSize = viewport.getSizeOfPixelOnCanvas() * 3;
21
+ const isClick = path.bbox.maxDimension <= clickSize;
22
+ if (isClick) {
23
+ const searchRegionSize = viewport.visibleRect.maxDimension / 200;
24
+ const minSizeBox = path.bbox.grownBy(searchRegionSize);
25
+ components = image.getElementsIntersectingRegion(minSizeBox).filter((component) => {
26
+ return minSizeBox.containsRect(component.getBBox()) || component.intersectsRect(minSizeBox);
27
+ });
28
+ components = filterComponents(components);
29
+ if (components.length > 1) {
30
+ components = [components[0]];
31
+ }
32
+ }
33
+ else {
34
+ components = filterComponents(this.resolveInternal(image));
35
+ }
36
+ return components;
37
+ }
38
+ }
39
+ exports.default = SelectionBuilder;
@@ -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;