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