js-draw 0.11.3 → 0.12.0
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/CHANGELOG.md +6 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +1 -0
- package/dist/src/Color4.js +1 -0
- package/dist/src/Editor.js +4 -5
- package/dist/src/SVGLoader.js +43 -35
- package/dist/src/components/AbstractComponent.d.ts +1 -0
- package/dist/src/components/AbstractComponent.js +15 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +51 -0
- package/dist/src/toolbar/HTMLToolbar.js +63 -5
- package/dist/src/toolbar/IconProvider.d.ts +1 -1
- package/dist/src/toolbar/IconProvider.js +33 -6
- package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +8 -1
- package/dist/src/toolbar/widgets/EraserToolWidget.js +45 -4
- package/dist/src/toolbar/widgets/PenToolWidget.js +2 -2
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +12 -3
- package/dist/src/tools/Eraser.d.ts +10 -1
- package/dist/src/tools/Eraser.js +65 -13
- package/dist/src/tools/SelectionTool/Selection.d.ts +4 -1
- package/dist/src/tools/SelectionTool/Selection.js +64 -27
- package/dist/src/tools/SelectionTool/SelectionTool.js +3 -1
- package/dist/src/tools/TextTool.js +10 -6
- package/dist/src/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/Color4.ts +1 -0
- package/src/Editor.ts +3 -4
- package/src/SVGLoader.ts +14 -14
- package/src/components/AbstractComponent.ts +19 -0
- package/src/toolbar/HTMLToolbar.ts +81 -5
- package/src/toolbar/IconProvider.ts +34 -6
- package/src/toolbar/widgets/EraserToolWidget.ts +64 -5
- package/src/toolbar/widgets/PenToolWidget.ts +2 -2
- package/src/toolbar/widgets/SelectionToolWidget.ts +2 -2
- package/src/tools/Eraser.test.ts +79 -0
- package/src/tools/Eraser.ts +81 -17
- package/src/tools/SelectionTool/Selection.ts +73 -23
- package/src/tools/SelectionTool/SelectionTool.test.ts +138 -21
- package/src/tools/SelectionTool/SelectionTool.ts +3 -1
- package/src/tools/TextTool.ts +14 -8
- package/src/types.ts +2 -2
@@ -82,20 +82,48 @@ export default class IconProvider {
|
|
82
82
|
return icon;
|
83
83
|
}
|
84
84
|
|
85
|
-
public makeEraserIcon(): IconType {
|
85
|
+
public makeEraserIcon(eraserSize?: number): IconType {
|
86
86
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
87
|
+
eraserSize ??= 10;
|
88
|
+
|
89
|
+
const scaledSize = eraserSize / 4;
|
90
|
+
const eraserColor = '#ff70af';
|
87
91
|
|
88
|
-
// Draw an eraser-like shape
|
92
|
+
// Draw an eraser-like shape. Created with Inkscape
|
89
93
|
icon.innerHTML = `
|
90
94
|
<g>
|
91
|
-
<
|
95
|
+
<path
|
96
|
+
style="fill:${eraserColor}"
|
97
|
+
stroke="black"
|
98
|
+
transform="rotate(41.35)"
|
99
|
+
d="M 52.5 27
|
100
|
+
C 50 28.9 48.9 31.7 48.9 34.8
|
101
|
+
L 48.9 39.8
|
102
|
+
C 48.9 45.3 53.4 49.8 58.9 49.8
|
103
|
+
L 103.9 49.8
|
104
|
+
C 105.8 49.8 107.6 49.2 109.1 48.3
|
105
|
+
L 110.2 ${scaledSize + 49.5} L 159.7 ${scaledSize + 5}
|
106
|
+
L 157.7 ${-scaledSize + 5.2} L 112.4 ${49.5 - scaledSize}
|
107
|
+
C 113.4 43.5 113.9 41.7 113.9 39.8
|
108
|
+
L 113.9 34.8
|
109
|
+
C 113.9 29.3 109.4 24.8 103.9 24.8
|
110
|
+
L 58.9 24.8
|
111
|
+
C 56.5 24.8 54.3 25.7 52.5 27
|
112
|
+
z "
|
113
|
+
id="path438" />
|
114
|
+
|
92
115
|
<rect
|
93
|
-
|
116
|
+
stroke="#cc8077"
|
94
117
|
${iconColorFill}
|
95
|
-
|
118
|
+
id="rect218"
|
119
|
+
width="65"
|
120
|
+
height="75"
|
121
|
+
x="48.9"
|
122
|
+
y="-38.7"
|
123
|
+
transform="rotate(41.35)" />
|
96
124
|
</g>
|
97
125
|
`;
|
98
|
-
icon.setAttribute('viewBox', '0 0
|
126
|
+
icon.setAttribute('viewBox', '0 0 120 120');
|
99
127
|
return icon;
|
100
128
|
}
|
101
129
|
|
@@ -1,26 +1,85 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
2
|
import Eraser from '../../tools/Eraser';
|
3
|
+
import { EditorEventType } from '../../types';
|
4
|
+
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
3
5
|
import { ToolbarLocalization } from '../localization';
|
4
6
|
import BaseToolWidget from './BaseToolWidget';
|
7
|
+
import { SavedToolbuttonState } from './BaseWidget';
|
5
8
|
|
6
9
|
export default class EraserToolWidget extends BaseToolWidget {
|
10
|
+
private thicknessInput: HTMLInputElement|null = null;
|
7
11
|
public constructor(
|
8
12
|
editor: Editor,
|
9
|
-
tool: Eraser,
|
13
|
+
private tool: Eraser,
|
10
14
|
localizationTable?: ToolbarLocalization
|
11
15
|
) {
|
12
16
|
super(editor, tool, 'eraser-tool-widget', localizationTable);
|
17
|
+
|
18
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
|
19
|
+
if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === this.tool) {
|
20
|
+
this.updateInputs();
|
21
|
+
this.updateIcon();
|
22
|
+
}
|
23
|
+
});
|
13
24
|
}
|
14
25
|
|
15
26
|
protected getTitle(): string {
|
16
27
|
return this.localizationTable.eraser;
|
17
28
|
}
|
29
|
+
|
18
30
|
protected createIcon(): Element {
|
19
|
-
return this.editor.icons.makeEraserIcon();
|
31
|
+
return this.editor.icons.makeEraserIcon(this.tool.getThickness());
|
32
|
+
}
|
33
|
+
|
34
|
+
private updateInputs() {
|
35
|
+
if (this.thicknessInput) {
|
36
|
+
this.thicknessInput.value = `${this.tool.getThickness()}`;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
private static nextThicknessInputId = 0;
|
41
|
+
|
42
|
+
protected fillDropdown(dropdown: HTMLElement): boolean {
|
43
|
+
const thicknessLabel = document.createElement('label');
|
44
|
+
this.thicknessInput = document.createElement('input');
|
45
|
+
|
46
|
+
this.thicknessInput.type = 'range';
|
47
|
+
this.thicknessInput.min = '4';
|
48
|
+
this.thicknessInput.max = '40';
|
49
|
+
this.thicknessInput.oninput = () => {
|
50
|
+
this.tool.setThickness(parseFloat(this.thicknessInput!.value));
|
51
|
+
};
|
52
|
+
this.thicknessInput.id = `${toolbarCSSPrefix}eraserThicknessInput${EraserToolWidget.nextThicknessInputId++}`;
|
53
|
+
|
54
|
+
thicknessLabel.innerText = this.localizationTable.thicknessLabel;
|
55
|
+
thicknessLabel.htmlFor = this.thicknessInput.id;
|
56
|
+
|
57
|
+
this.updateInputs();
|
58
|
+
dropdown.replaceChildren(thicknessLabel, this.thicknessInput);
|
59
|
+
return true;
|
60
|
+
}
|
61
|
+
|
62
|
+
public serializeState(): SavedToolbuttonState {
|
63
|
+
return {
|
64
|
+
...super.serializeState(),
|
65
|
+
|
66
|
+
thickness: this.tool.getThickness(),
|
67
|
+
};
|
20
68
|
}
|
21
69
|
|
22
|
-
|
23
|
-
|
24
|
-
|
70
|
+
public deserializeFrom(state: SavedToolbuttonState) {
|
71
|
+
super.deserializeFrom(state);
|
72
|
+
|
73
|
+
if (state.thickness) {
|
74
|
+
const parsedThickness = parseFloat(state.thickness);
|
75
|
+
|
76
|
+
if (typeof parsedThickness !== 'number' || !isFinite(parsedThickness)) {
|
77
|
+
throw new Error(
|
78
|
+
`Deserializing property ${parsedThickness} is not a number or is not finite.`
|
79
|
+
);
|
80
|
+
}
|
81
|
+
|
82
|
+
this.tool.setThickness(parsedThickness);
|
83
|
+
}
|
25
84
|
}
|
26
85
|
}
|
@@ -145,8 +145,8 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
145
145
|
const objectTypeSelect = document.createElement('select');
|
146
146
|
|
147
147
|
// Give inputs IDs so we can label them with a <label for=...>Label text</label>
|
148
|
-
thicknessInput.id = `${toolbarCSSPrefix}
|
149
|
-
objectTypeSelect.id = `${toolbarCSSPrefix}
|
148
|
+
thicknessInput.id = `${toolbarCSSPrefix}penThicknessInput${PenToolWidget.idCounter++}`;
|
149
|
+
objectTypeSelect.id = `${toolbarCSSPrefix}penBuilderSelect${PenToolWidget.idCounter++}`;
|
150
150
|
|
151
151
|
thicknessLabel.innerText = this.localizationTable.thicknessLabel;
|
152
152
|
thicknessLabel.setAttribute('for', thicknessInput.id);
|
@@ -35,9 +35,9 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
35
35
|
editor, 'duplicate-btn',
|
36
36
|
() => editor.icons.makeDuplicateSelectionIcon(),
|
37
37
|
this.localizationTable.duplicateSelection,
|
38
|
-
() => {
|
38
|
+
async () => {
|
39
39
|
const selection = this.tool.getSelection();
|
40
|
-
this.editor.dispatch(selection!.duplicateSelectedObjects());
|
40
|
+
this.editor.dispatch(await selection!.duplicateSelectedObjects());
|
41
41
|
},
|
42
42
|
localization,
|
43
43
|
);
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { Rect2, StrokeComponent } from '../lib';
|
3
|
+
import { Vec2 } from '../math/Vec2';
|
4
|
+
import createEditor from '../testing/createEditor';
|
5
|
+
import { InputEvtType } from '../types';
|
6
|
+
import Eraser from './Eraser';
|
7
|
+
|
8
|
+
const selectEraser = (editor: Editor) => {
|
9
|
+
const tools = editor.toolController;
|
10
|
+
const eraser = tools.getMatchingTools(Eraser)[0];
|
11
|
+
eraser.setEnabled(true);
|
12
|
+
|
13
|
+
return eraser;
|
14
|
+
};
|
15
|
+
|
16
|
+
const getAllStrokes = (editor: Editor) => {
|
17
|
+
return editor.image.getAllElements().filter(elem => elem instanceof StrokeComponent);
|
18
|
+
};
|
19
|
+
|
20
|
+
describe('Eraser', () => {
|
21
|
+
it('should erase object between locations of events', () => {
|
22
|
+
const editor = createEditor();
|
23
|
+
|
24
|
+
// Draw a line
|
25
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
26
|
+
jest.advanceTimersByTime(100);
|
27
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(200, 200));
|
28
|
+
|
29
|
+
// Should have drawn a line
|
30
|
+
const strokes = getAllStrokes(editor);
|
31
|
+
expect(strokes).toHaveLength(1);
|
32
|
+
expect(strokes[0].getBBox().area).toBeGreaterThanOrEqual(200 * 200);
|
33
|
+
|
34
|
+
selectEraser(editor);
|
35
|
+
|
36
|
+
// Erase the line.
|
37
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 0));
|
38
|
+
jest.advanceTimersByTime(400);
|
39
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(0, 200));
|
40
|
+
|
41
|
+
// Should have erased the line
|
42
|
+
expect(getAllStrokes(editor)).toHaveLength(0);
|
43
|
+
});
|
44
|
+
|
45
|
+
it('should erase objects within eraser.thickness of an event when not zoomed', async () => {
|
46
|
+
const editor = createEditor();
|
47
|
+
|
48
|
+
await editor.loadFromSVG(`
|
49
|
+
<svg>
|
50
|
+
<path d='m0,0 l2,0 l0,2 l-2,0 z' fill="#ff0000"/>
|
51
|
+
<path d='m50,50 l2,0 l0,2 l-2,0 z' fill="#ff0000"/>
|
52
|
+
</svg>
|
53
|
+
`, true);
|
54
|
+
|
55
|
+
editor.viewport.resetTransform();
|
56
|
+
|
57
|
+
const allStrokes = getAllStrokes(editor);
|
58
|
+
expect(allStrokes).toHaveLength(2);
|
59
|
+
expect(allStrokes[0].getBBox()).objEq(new Rect2(0, 0, 2, 2));
|
60
|
+
expect(allStrokes[1].getBBox()).objEq(new Rect2(50, 50, 2, 2));
|
61
|
+
|
62
|
+
const eraser = selectEraser(editor);
|
63
|
+
eraser.setThickness(10);
|
64
|
+
|
65
|
+
// Erase the first stroke
|
66
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(3, 0));
|
67
|
+
jest.advanceTimersByTime(100);
|
68
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(3, 0));
|
69
|
+
|
70
|
+
expect(getAllStrokes(editor)).toHaveLength(1);
|
71
|
+
|
72
|
+
// Erase the remaining stroke
|
73
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(47, 47));
|
74
|
+
jest.advanceTimersByTime(100);
|
75
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(47, 47));
|
76
|
+
|
77
|
+
expect(getAllStrokes(editor)).toHaveLength(0);
|
78
|
+
});
|
79
|
+
});
|
package/src/tools/Eraser.ts
CHANGED
@@ -1,15 +1,20 @@
|
|
1
|
-
import { PointerEvt } from '../types';
|
1
|
+
import { EditorEventType, PointerEvt } from '../types';
|
2
2
|
import BaseTool from './BaseTool';
|
3
3
|
import Editor from '../Editor';
|
4
|
-
import { Point2 } from '../math/Vec2';
|
4
|
+
import { Point2, Vec2 } from '../math/Vec2';
|
5
5
|
import LineSegment2 from '../math/LineSegment2';
|
6
6
|
import Erase from '../commands/Erase';
|
7
7
|
import AbstractComponent from '../components/AbstractComponent';
|
8
8
|
import { PointerDevice } from '../Pointer';
|
9
|
+
import Color4 from '../Color4';
|
10
|
+
import Rect2 from '../math/Rect2';
|
11
|
+
import RenderingStyle from '../rendering/RenderingStyle';
|
9
12
|
|
10
13
|
export default class Eraser extends BaseTool {
|
11
|
-
private lastPoint: Point2;
|
14
|
+
private lastPoint: Point2|null = null;
|
15
|
+
private isFirstEraseEvt: boolean = true;
|
12
16
|
private toRemove: AbstractComponent[];
|
17
|
+
private thickness: number = 10;
|
13
18
|
|
14
19
|
// Commands that each remove one element
|
15
20
|
private partialCommands: Erase[] = [];
|
@@ -18,27 +23,48 @@ export default class Eraser extends BaseTool {
|
|
18
23
|
super(editor.notifier, description);
|
19
24
|
}
|
20
25
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
this.toRemove = [];
|
25
|
-
return true;
|
26
|
-
}
|
26
|
+
private clearPreview() {
|
27
|
+
this.editor.clearWetInk();
|
28
|
+
}
|
27
29
|
|
28
|
-
|
30
|
+
private getSizeOnCanvas() {
|
31
|
+
return this.thickness / this.editor.viewport.getScaleFactor();
|
29
32
|
}
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
+
private drawPreviewAt(point: Point2) {
|
35
|
+
this.clearPreview();
|
36
|
+
|
37
|
+
const size = this.getSizeOnCanvas();
|
38
|
+
|
39
|
+
const renderer = this.editor.display.getWetInkRenderer();
|
40
|
+
const rect = this.getEraserRect(point);
|
41
|
+
const fill: RenderingStyle = {
|
42
|
+
fill: Color4.gray,
|
43
|
+
};
|
44
|
+
renderer.drawRect(rect, size / 4, fill);
|
45
|
+
}
|
46
|
+
|
47
|
+
private getEraserRect(centerPoint: Point2) {
|
48
|
+
const size = this.getSizeOnCanvas();
|
49
|
+
const halfSize = Vec2.of(size / 2, size / 2);
|
50
|
+
return Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
|
51
|
+
}
|
52
|
+
|
53
|
+
private eraseTo(currentPoint: Point2) {
|
54
|
+
if (!this.isFirstEraseEvt && currentPoint.minus(this.lastPoint!).magnitude() === 0) {
|
34
55
|
return;
|
35
56
|
}
|
57
|
+
this.isFirstEraseEvt = false;
|
36
58
|
|
37
|
-
|
38
|
-
|
59
|
+
// Currently only objects within eraserRect or that intersect a straight line
|
60
|
+
// from the center of the current rect to the previous are erased. TODO: Erase
|
61
|
+
// all objects as if there were pointerMove events between the two points.
|
62
|
+
const eraserRect = this.getEraserRect(currentPoint);
|
63
|
+
const line = new LineSegment2(this.lastPoint!, currentPoint);
|
64
|
+
const region = Rect2.union(line.bbox, eraserRect);
|
39
65
|
|
40
66
|
const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
|
41
|
-
return component.intersects(line);
|
67
|
+
return component.intersects(line) || component.intersectsRect(eraserRect);
|
42
68
|
});
|
43
69
|
|
44
70
|
// Remove any intersecting elements.
|
@@ -50,10 +76,32 @@ export default class Eraser extends BaseTool {
|
|
50
76
|
|
51
77
|
this.partialCommands.push(...newPartialCommands);
|
52
78
|
|
79
|
+
this.drawPreviewAt(currentPoint);
|
53
80
|
this.lastPoint = currentPoint;
|
54
81
|
}
|
55
82
|
|
56
|
-
public
|
83
|
+
public onPointerDown(event: PointerEvt): boolean {
|
84
|
+
if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
|
85
|
+
this.lastPoint = event.current.canvasPos;
|
86
|
+
this.toRemove = [];
|
87
|
+
this.isFirstEraseEvt = true;
|
88
|
+
|
89
|
+
this.drawPreviewAt(event.current.canvasPos);
|
90
|
+
return true;
|
91
|
+
}
|
92
|
+
|
93
|
+
return false;
|
94
|
+
}
|
95
|
+
|
96
|
+
public onPointerMove(event: PointerEvt): void {
|
97
|
+
const currentPoint = event.current.canvasPos;
|
98
|
+
|
99
|
+
this.eraseTo(currentPoint);
|
100
|
+
}
|
101
|
+
|
102
|
+
public onPointerUp(event: PointerEvt): void {
|
103
|
+
this.eraseTo(event.current.canvasPos);
|
104
|
+
|
57
105
|
if (this.toRemove.length > 0) {
|
58
106
|
// Undo commands for each individual component and unite into a single command.
|
59
107
|
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
@@ -62,10 +110,26 @@ export default class Eraser extends BaseTool {
|
|
62
110
|
const command = new Erase(this.toRemove);
|
63
111
|
this.editor.dispatch(command); // dispatch: Makes undo-able.
|
64
112
|
}
|
113
|
+
|
114
|
+
this.clearPreview();
|
65
115
|
}
|
66
116
|
|
67
117
|
public onGestureCancel(): void {
|
68
118
|
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
69
119
|
this.partialCommands = [];
|
120
|
+
this.clearPreview();
|
121
|
+
}
|
122
|
+
|
123
|
+
public getThickness() {
|
124
|
+
return this.thickness;
|
125
|
+
}
|
126
|
+
|
127
|
+
public setThickness(thickness: number) {
|
128
|
+
this.thickness = thickness;
|
129
|
+
|
130
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
131
|
+
kind: EditorEventType.ToolUpdated,
|
132
|
+
tool: this,
|
133
|
+
});
|
70
134
|
}
|
71
135
|
}
|
@@ -100,6 +100,11 @@ export default class Selection {
|
|
100
100
|
}
|
101
101
|
}
|
102
102
|
|
103
|
+
// @internal Intended for unit tests
|
104
|
+
public getBackgroundElem(): HTMLElement {
|
105
|
+
return this.backgroundElem;
|
106
|
+
}
|
107
|
+
|
103
108
|
public getTransform(): Mat33 {
|
104
109
|
return this.transform;
|
105
110
|
}
|
@@ -304,18 +309,7 @@ export default class Selection {
|
|
304
309
|
}
|
305
310
|
|
306
311
|
this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
|
307
|
-
|
308
|
-
return true;
|
309
|
-
}
|
310
|
-
|
311
|
-
// Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
|
312
|
-
// As such, test with more lines than just this' edges.
|
313
|
-
const testLines = [];
|
314
|
-
for (const subregion of this.region.divideIntoGrid(2, 2)) {
|
315
|
-
testLines.push(...subregion.getEdges());
|
316
|
-
}
|
317
|
-
|
318
|
-
return testLines.some(edge => elem.intersects(edge));
|
312
|
+
return elem.intersectsRect(this.region);
|
319
313
|
});
|
320
314
|
|
321
315
|
if (singleItemSelectionMode && this.selectedElems.length > 0) {
|
@@ -392,6 +386,9 @@ export default class Selection {
|
|
392
386
|
}
|
393
387
|
}
|
394
388
|
|
389
|
+
// Maps IDs to whether we removed the component from the image
|
390
|
+
private removedFromImage: Record<string, boolean> = {};
|
391
|
+
|
395
392
|
// Add/remove the contents of this' seleciton from the editor.
|
396
393
|
// Used to prevent previewed content from looking like duplicate content
|
397
394
|
// while dragging.
|
@@ -400,7 +397,7 @@ export default class Selection {
|
|
400
397
|
// the editor image is likely to be slow.)
|
401
398
|
//
|
402
399
|
// If removed from the image, selected elements are drawn as wet ink.
|
403
|
-
private
|
400
|
+
private addRemoveSelectionFromImage(inImage: boolean) {
|
404
401
|
// Don't hide elements if doing so will be slow.
|
405
402
|
if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
|
406
403
|
return;
|
@@ -409,26 +406,46 @@ export default class Selection {
|
|
409
406
|
for (const elem of this.selectedElems) {
|
410
407
|
const parent = this.editor.image.findParent(elem);
|
411
408
|
|
412
|
-
if (!inImage) {
|
413
|
-
|
409
|
+
if (!inImage && parent) {
|
410
|
+
this.removedFromImage[elem.getId()] = true;
|
411
|
+
parent.remove();
|
414
412
|
}
|
415
413
|
// If we're making things visible and the selected object wasn't previously
|
416
414
|
// visible,
|
417
|
-
else if (!parent) {
|
415
|
+
else if (!parent && this.removedFromImage[elem.getId()]) {
|
418
416
|
EditorImage.addElement(elem).apply(this.editor);
|
417
|
+
|
418
|
+
this.removedFromImage[elem.getId()] = false;
|
419
|
+
delete this.removedFromImage[elem.getId()];
|
419
420
|
}
|
420
421
|
}
|
421
422
|
|
422
|
-
await
|
423
|
-
|
424
|
-
|
425
|
-
|
423
|
+
// Don't await queueRerender. If we're running in a test, the re-render might never
|
424
|
+
// happen.
|
425
|
+
this.editor.queueRerender().then(() => {
|
426
|
+
if (!inImage) {
|
427
|
+
this.previewTransformCmds();
|
428
|
+
}
|
429
|
+
});
|
430
|
+
}
|
431
|
+
|
432
|
+
private removeDeletedElemsFromSelection() {
|
433
|
+
// Remove any deleted elements from the selection.
|
434
|
+
this.selectedElems = this.selectedElems.filter(elem => {
|
435
|
+
const hasParent = !!this.editor.image.findParent(elem);
|
436
|
+
|
437
|
+
// If we removed the element and haven't added it back yet, don't remove it
|
438
|
+
// from the selection.
|
439
|
+
const weRemoved = this.removedFromImage[elem.getId()];
|
440
|
+
return hasParent || weRemoved;
|
441
|
+
});
|
426
442
|
}
|
427
443
|
|
428
444
|
private targetHandle: SelectionHandle|null = null;
|
429
445
|
private backgroundDragging: boolean = false;
|
430
446
|
public onDragStart(pointer: Pointer, target: EventTarget): boolean {
|
431
|
-
|
447
|
+
this.removeDeletedElemsFromSelection();
|
448
|
+
this.addRemoveSelectionFromImage(false);
|
432
449
|
|
433
450
|
for (const handle of this.handles) {
|
434
451
|
if (handle.isTarget(target)) {
|
@@ -503,11 +520,43 @@ export default class Selection {
|
|
503
520
|
}
|
504
521
|
|
505
522
|
public deleteSelectedObjects(): Command {
|
523
|
+
if (this.backgroundDragging || this.targetHandle) {
|
524
|
+
this.onDragEnd();
|
525
|
+
}
|
526
|
+
|
506
527
|
return new Erase(this.selectedElems);
|
507
528
|
}
|
508
529
|
|
509
|
-
public duplicateSelectedObjects(): Command {
|
510
|
-
|
530
|
+
public async duplicateSelectedObjects(): Promise<Command> {
|
531
|
+
const wasTransforming = this.backgroundDragging || this.targetHandle;
|
532
|
+
let tmpApplyCommand: Command|null = null;
|
533
|
+
|
534
|
+
if (wasTransforming) {
|
535
|
+
// Don't update the selection's focus when redoing/undoing
|
536
|
+
const selectionToUpdate: Selection|null = null;
|
537
|
+
tmpApplyCommand = new Selection.ApplyTransformationCommand(
|
538
|
+
selectionToUpdate, this.selectedElems, this.transform
|
539
|
+
);
|
540
|
+
|
541
|
+
// Transform to ensure that the duplicates are in the correct location
|
542
|
+
await tmpApplyCommand.apply(this.editor);
|
543
|
+
|
544
|
+
// Show items again
|
545
|
+
this.addRemoveSelectionFromImage(true);
|
546
|
+
}
|
547
|
+
|
548
|
+
const duplicateCommand = new Duplicate(this.selectedElems);
|
549
|
+
|
550
|
+
if (wasTransforming) {
|
551
|
+
// Move the selected objects back to the correct location.
|
552
|
+
await tmpApplyCommand?.unapply(this.editor);
|
553
|
+
this.addRemoveSelectionFromImage(false);
|
554
|
+
|
555
|
+
this.previewTransformCmds();
|
556
|
+
this.updateUI();
|
557
|
+
}
|
558
|
+
|
559
|
+
return duplicateCommand;
|
511
560
|
}
|
512
561
|
|
513
562
|
public addTo(elem: HTMLElement) {
|
@@ -533,6 +582,7 @@ export default class Selection {
|
|
533
582
|
}
|
534
583
|
|
535
584
|
public setSelectedObjects(objects: AbstractComponent[], bbox: Rect2) {
|
585
|
+
this.addRemoveSelectionFromImage(true);
|
536
586
|
this.originalRegion = bbox;
|
537
587
|
this.selectedElems = objects.filter(object => object.isSelectable());
|
538
588
|
this.updateUI();
|