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.
- package/dist/Editor.css +1 -1
- package/dist/bundle.js +42 -37
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +0 -2
- package/dist/cjs/components/AbstractComponent.d.ts +15 -0
- package/dist/cjs/components/AbstractComponent.js +16 -0
- package/dist/cjs/components/Stroke.d.ts +1 -0
- package/dist/cjs/components/Stroke.js +7 -0
- package/dist/cjs/toolbar/IconProvider.d.ts +2 -1
- package/dist/cjs/toolbar/IconProvider.js +18 -8
- package/dist/cjs/toolbar/localization.d.ts +2 -0
- package/dist/cjs/toolbar/localization.js +2 -0
- package/dist/cjs/toolbar/widgets/SelectionToolWidget.d.ts +7 -0
- package/dist/cjs/toolbar/widgets/SelectionToolWidget.js +109 -28
- package/dist/cjs/toolbar/widgets/components/makeButtonGrid.d.ts +17 -0
- package/dist/cjs/toolbar/widgets/components/makeButtonGrid.js +40 -0
- package/dist/cjs/tools/SelectionTool/Selection.d.ts +2 -3
- package/dist/cjs/tools/SelectionTool/Selection.js +19 -40
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.d.ts +17 -0
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.js +67 -0
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.d.ts +13 -0
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.js +33 -0
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.d.ts +15 -0
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.js +39 -0
- package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +10 -2
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +68 -55
- package/dist/cjs/tools/SelectionTool/types.d.ts +4 -0
- package/dist/cjs/tools/SelectionTool/types.js +6 -1
- package/dist/cjs/tools/lib.d.ts +1 -1
- package/dist/cjs/tools/lib.js +2 -1
- package/dist/cjs/util/ReactiveValue.js +2 -6
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +0 -2
- package/dist/mjs/components/AbstractComponent.d.ts +15 -0
- package/dist/mjs/components/AbstractComponent.mjs +16 -0
- package/dist/mjs/components/Stroke.d.ts +1 -0
- package/dist/mjs/components/Stroke.mjs +7 -0
- package/dist/mjs/toolbar/IconProvider.d.ts +2 -1
- package/dist/mjs/toolbar/IconProvider.mjs +18 -8
- package/dist/mjs/toolbar/localization.d.ts +2 -0
- package/dist/mjs/toolbar/localization.mjs +2 -0
- package/dist/mjs/toolbar/widgets/SelectionToolWidget.d.ts +7 -0
- package/dist/mjs/toolbar/widgets/SelectionToolWidget.mjs +109 -28
- package/dist/mjs/toolbar/widgets/components/makeButtonGrid.d.ts +17 -0
- package/dist/mjs/toolbar/widgets/components/makeButtonGrid.mjs +35 -0
- package/dist/mjs/tools/SelectionTool/Selection.d.ts +2 -3
- package/dist/mjs/tools/SelectionTool/Selection.mjs +19 -40
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.d.ts +17 -0
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.mjs +61 -0
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.d.ts +13 -0
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.mjs +27 -0
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.d.ts +15 -0
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.mjs +36 -0
- package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +10 -2
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +68 -55
- package/dist/mjs/tools/SelectionTool/types.d.ts +4 -0
- package/dist/mjs/tools/SelectionTool/types.mjs +5 -0
- package/dist/mjs/tools/lib.d.ts +1 -1
- package/dist/mjs/tools/lib.mjs +1 -1
- package/dist/mjs/util/ReactiveValue.mjs +2 -6
- package/dist/mjs/version.mjs +1 -1
- package/package.json +4 -4
- package/src/toolbar/EdgeToolbar.scss +6 -1
- package/src/toolbar/widgets/components/components.scss +1 -0
- package/src/toolbar/widgets/components/makeButtonGrid.scss +25 -0
- package/src/tools/SelectionTool/SelectionTool.scss +1 -0
- 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
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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(
|
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
|
-
|
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
|
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
|
}
|