js-draw 0.3.0 → 0.3.2
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/.github/ISSUE_TEMPLATE/translation.md +4 -1
- package/CHANGELOG.md +15 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +4 -1
- package/dist/src/Editor.js +117 -2
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/SVGLoader.d.ts +4 -1
- package/dist/src/SVGLoader.js +78 -33
- package/dist/src/UndoRedoHistory.d.ts +1 -0
- package/dist/src/UndoRedoHistory.js +6 -0
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +12 -4
- package/dist/src/commands/lib.d.ts +2 -1
- package/dist/src/commands/lib.js +2 -1
- package/dist/src/commands/localization.d.ts +1 -0
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/commands/uniteCommands.d.ts +4 -0
- package/dist/src/commands/uniteCommands.js +105 -0
- package/dist/src/components/AbstractComponent.d.ts +2 -0
- package/dist/src/components/AbstractComponent.js +41 -5
- package/dist/src/components/ImageComponent.d.ts +27 -0
- package/dist/src/components/ImageComponent.js +129 -0
- package/dist/src/components/Stroke.js +11 -6
- package/dist/src/components/builders/FreehandLineBuilder.js +7 -7
- package/dist/src/components/lib.d.ts +4 -2
- package/dist/src/components/lib.js +4 -2
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/math/LineSegment2.d.ts +4 -0
- package/dist/src/math/LineSegment2.js +9 -0
- package/dist/src/math/Path.d.ts +5 -1
- package/dist/src/math/Path.js +89 -7
- package/dist/src/math/Rect2.js +1 -1
- package/dist/src/math/Triangle.d.ts +11 -0
- package/dist/src/math/Triangle.js +19 -0
- package/dist/src/rendering/Display.js +2 -2
- package/dist/src/rendering/localization.d.ts +3 -0
- package/dist/src/rendering/localization.js +3 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +9 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +14 -12
- package/dist/src/rendering/renderers/SVGRenderer.js +71 -87
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
- package/dist/src/toolbar/HTMLToolbar.js +1 -0
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
- package/dist/src/tools/BaseTool.d.ts +4 -1
- package/dist/src/tools/BaseTool.js +12 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +142 -0
- package/dist/src/tools/Pen.d.ts +2 -1
- package/dist/src/tools/Pen.js +16 -0
- package/dist/src/tools/SelectionTool.d.ts +7 -1
- package/dist/src/tools/SelectionTool.js +63 -5
- package/dist/src/tools/ToolController.d.ts +1 -0
- package/dist/src/tools/ToolController.js +45 -29
- package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
- package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
- package/dist/src/tools/lib.d.ts +2 -0
- package/dist/src/tools/lib.js +2 -0
- package/dist/src/tools/localization.d.ts +4 -0
- package/dist/src/tools/localization.js +4 -0
- package/dist/src/types.d.ts +21 -4
- package/dist/src/types.js +3 -0
- package/package.json +2 -2
- package/src/Editor.ts +131 -2
- package/src/EditorImage.ts +7 -1
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +13 -4
- package/src/commands/lib.ts +2 -0
- package/src/commands/localization.ts +2 -0
- package/src/commands/uniteCommands.test.ts +23 -0
- package/src/commands/uniteCommands.ts +121 -0
- package/src/components/AbstractComponent.ts +55 -9
- package/src/components/ImageComponent.ts +153 -0
- package/src/components/Stroke.test.ts +5 -0
- package/src/components/Stroke.ts +13 -7
- package/src/components/builders/FreehandLineBuilder.ts +7 -7
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +13 -0
- package/src/math/Path.test.ts +53 -0
- package/src/math/Path.toString.test.ts +4 -2
- package/src/math/Path.ts +109 -11
- package/src/math/Rect2.ts +1 -1
- package/src/math/Triangle.ts +29 -0
- package/src/rendering/Display.ts +2 -2
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +17 -0
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +76 -101
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/toolbar/HTMLToolbar.ts +1 -1
- package/src/toolbar/types.ts +1 -1
- package/src/toolbar/widgets/BaseWidget.ts +27 -1
- package/src/tools/BaseTool.ts +17 -1
- package/src/tools/PasteHandler.ts +156 -0
- package/src/tools/Pen.ts +20 -1
- package/src/tools/SelectionTool.ts +80 -8
- package/src/tools/ToolController.ts +60 -46
- package/src/tools/ToolSwitcherShortcut.ts +34 -0
- package/src/tools/lib.ts +2 -0
- package/src/tools/localization.ts +10 -0
- package/src/types.ts +29 -3
package/src/toolbar/types.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
|
-
import { InputEvtType } from '../../types';
|
2
|
+
import { EditorEventType, InputEvtType } from '../../types';
|
3
3
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
4
4
|
import { makeDropdownIcon } from '../icons';
|
5
5
|
import { ToolbarLocalization } from '../localization';
|
@@ -14,6 +14,7 @@ export default abstract class BaseWidget {
|
|
14
14
|
#hasDropdown: boolean;
|
15
15
|
private disabled: boolean = false;
|
16
16
|
private subWidgets: BaseWidget[] = [];
|
17
|
+
private toplevel: boolean = true;
|
17
18
|
|
18
19
|
public constructor(
|
19
20
|
protected editor: Editor,
|
@@ -46,6 +47,7 @@ export default abstract class BaseWidget {
|
|
46
47
|
|
47
48
|
for (const widget of this.subWidgets) {
|
48
49
|
widget.addTo(dropdown);
|
50
|
+
widget.setIsToplevel(false);
|
49
51
|
}
|
50
52
|
return true;
|
51
53
|
}
|
@@ -103,6 +105,7 @@ export default abstract class BaseWidget {
|
|
103
105
|
}
|
104
106
|
|
105
107
|
// Adds this to [parent]. This can only be called once for each ToolbarWidget.
|
108
|
+
// @internal
|
106
109
|
public addTo(parent: HTMLElement) {
|
107
110
|
this.label.innerText = this.getTitle();
|
108
111
|
|
@@ -119,6 +122,19 @@ export default abstract class BaseWidget {
|
|
119
122
|
this.dropdownIcon = this.createDropdownIcon();
|
120
123
|
this.button.appendChild(this.dropdownIcon);
|
121
124
|
this.container.appendChild(this.dropdownContainer);
|
125
|
+
|
126
|
+
this.editor.notifier.on(EditorEventType.ToolbarDropdownShown, (evt) => {
|
127
|
+
if (
|
128
|
+
evt.kind === EditorEventType.ToolbarDropdownShown
|
129
|
+
&& evt.parentWidget !== this
|
130
|
+
|
131
|
+
// Don't hide if a submenu wash shown (it might be a submenu of
|
132
|
+
// the current menu).
|
133
|
+
&& evt.parentWidget.toplevel
|
134
|
+
) {
|
135
|
+
this.setDropdownVisible(false);
|
136
|
+
}
|
137
|
+
});
|
122
138
|
}
|
123
139
|
|
124
140
|
this.setDropdownVisible(false);
|
@@ -171,6 +187,11 @@ export default abstract class BaseWidget {
|
|
171
187
|
this.editor.announceForAccessibility(
|
172
188
|
this.localizationTable.dropdownShown(this.getTitle())
|
173
189
|
);
|
190
|
+
|
191
|
+
this.editor.notifier.dispatch(EditorEventType.ToolbarDropdownShown, {
|
192
|
+
kind: EditorEventType.ToolbarDropdownShown,
|
193
|
+
parentWidget: this,
|
194
|
+
});
|
174
195
|
} else {
|
175
196
|
this.dropdownContainer.classList.add('hidden');
|
176
197
|
this.container.classList.remove('dropdownVisible');
|
@@ -195,6 +216,11 @@ export default abstract class BaseWidget {
|
|
195
216
|
}
|
196
217
|
}
|
197
218
|
|
219
|
+
/** Set whether the widget is contained within another. @internal */
|
220
|
+
protected setIsToplevel(toplevel: boolean) {
|
221
|
+
this.toplevel = toplevel;
|
222
|
+
}
|
223
|
+
|
198
224
|
protected isDropdownVisible(): boolean {
|
199
225
|
return !this.dropdownContainer.classList.contains('hidden');
|
200
226
|
}
|
package/src/tools/BaseTool.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types';
|
1
|
+
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent, PasteEvent, CopyEvent } from '../types';
|
2
2
|
import ToolEnabledGroup from './ToolEnabledGroup';
|
3
3
|
|
4
4
|
export default abstract class BaseTool implements PointerEvtListener {
|
@@ -17,6 +17,14 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
17
17
|
return false;
|
18
18
|
}
|
19
19
|
|
20
|
+
public onCopy(_event: CopyEvent): boolean {
|
21
|
+
return false;
|
22
|
+
}
|
23
|
+
|
24
|
+
public onPaste(_event: PasteEvent): boolean {
|
25
|
+
return false;
|
26
|
+
}
|
27
|
+
|
20
28
|
public onKeyPress(_event: KeyPressEvent): boolean {
|
21
29
|
return false;
|
22
30
|
}
|
@@ -56,5 +64,13 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
56
64
|
|
57
65
|
this.group = group;
|
58
66
|
}
|
67
|
+
|
68
|
+
public getToolGroup(): ToolEnabledGroup|null {
|
69
|
+
if (this.group) {
|
70
|
+
return this.group;
|
71
|
+
}
|
72
|
+
|
73
|
+
return null;
|
74
|
+
}
|
59
75
|
}
|
60
76
|
|
@@ -0,0 +1,156 @@
|
|
1
|
+
/**
|
2
|
+
* A tool that handles paste events.
|
3
|
+
* @packageDocumentation
|
4
|
+
*/
|
5
|
+
|
6
|
+
import Editor from '../Editor';
|
7
|
+
import { AbstractComponent, TextComponent } from '../components/lib';
|
8
|
+
import { Command, uniteCommands } from '../commands/lib';
|
9
|
+
import SVGLoader from '../SVGLoader';
|
10
|
+
import { PasteEvent } from '../types';
|
11
|
+
import { Mat33, Rect2, Vec2 } from '../math/lib';
|
12
|
+
import BaseTool from './BaseTool';
|
13
|
+
import EditorImage from '../EditorImage';
|
14
|
+
import SelectionTool from './SelectionTool';
|
15
|
+
import TextTool from './TextTool';
|
16
|
+
import Color4 from '../Color4';
|
17
|
+
import { TextStyle } from '../components/Text';
|
18
|
+
import ImageComponent from '../components/ImageComponent';
|
19
|
+
|
20
|
+
// { @inheritDoc PasteHandler! }
|
21
|
+
export default class PasteHandler extends BaseTool {
|
22
|
+
public constructor(private editor: Editor) {
|
23
|
+
super(editor.notifier, editor.localization.pasteHandler);
|
24
|
+
}
|
25
|
+
|
26
|
+
public onPaste(event: PasteEvent): boolean {
|
27
|
+
const mime = event.mime.toLowerCase();
|
28
|
+
|
29
|
+
if (mime === 'image/svg+xml') {
|
30
|
+
void this.doSVGPaste(event.data);
|
31
|
+
return true;
|
32
|
+
}
|
33
|
+
else if (mime === 'text/plain') {
|
34
|
+
void this.doTextPaste(event.data);
|
35
|
+
return true;
|
36
|
+
}
|
37
|
+
else if (mime === 'image/png' || mime === 'image/jpeg') {
|
38
|
+
void this.doImagePaste(event.data);
|
39
|
+
return true;
|
40
|
+
}
|
41
|
+
|
42
|
+
return false;
|
43
|
+
}
|
44
|
+
|
45
|
+
private async addComponentsFromPaste(components: AbstractComponent[]) {
|
46
|
+
let bbox: Rect2|null = null;
|
47
|
+
for (const component of components) {
|
48
|
+
if (bbox) {
|
49
|
+
bbox = bbox.union(component.getBBox());
|
50
|
+
} else {
|
51
|
+
bbox = component.getBBox();
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
if (!bbox) {
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
|
59
|
+
// Find a transform that scales/moves bbox onto the screen.
|
60
|
+
const visibleRect = this.editor.viewport.visibleRect;
|
61
|
+
const scaleRatioX = visibleRect.width / bbox.width;
|
62
|
+
const scaleRatioY = visibleRect.height / bbox.height;
|
63
|
+
|
64
|
+
let scaleRatio = scaleRatioX;
|
65
|
+
if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
|
66
|
+
scaleRatio = scaleRatioY;
|
67
|
+
}
|
68
|
+
scaleRatio *= 2 / 3;
|
69
|
+
|
70
|
+
const transfm = Mat33.translation(
|
71
|
+
visibleRect.center.minus(bbox.center)
|
72
|
+
).rightMul(
|
73
|
+
Mat33.scaling2D(scaleRatio, bbox.center)
|
74
|
+
);
|
75
|
+
|
76
|
+
const commands: Command[] = [];
|
77
|
+
for (const component of components) {
|
78
|
+
// To allow deserialization, we need to add first, then transform.
|
79
|
+
commands.push(EditorImage.addElement(component));
|
80
|
+
commands.push(component.transformBy(transfm));
|
81
|
+
}
|
82
|
+
|
83
|
+
const applyChunkSize = 100;
|
84
|
+
this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
|
85
|
+
|
86
|
+
for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
|
87
|
+
selectionTool.setEnabled(true);
|
88
|
+
selectionTool.setSelection(components);
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
private async doSVGPaste(data: string) {
|
93
|
+
const sanitize = true;
|
94
|
+
const loader = SVGLoader.fromString(data, sanitize);
|
95
|
+
|
96
|
+
const components: AbstractComponent[] = [];
|
97
|
+
|
98
|
+
await loader.start((component) => {
|
99
|
+
components.push(component);
|
100
|
+
},
|
101
|
+
(_countProcessed: number, _totalToProcess: number) => null);
|
102
|
+
|
103
|
+
await this.addComponentsFromPaste(components);
|
104
|
+
}
|
105
|
+
|
106
|
+
private async doTextPaste(text: string) {
|
107
|
+
const textTools = this.editor.toolController.getMatchingTools(TextTool);
|
108
|
+
|
109
|
+
textTools.sort((a, b) => {
|
110
|
+
if (!a.isEnabled() && b.isEnabled()) {
|
111
|
+
return -1;
|
112
|
+
}
|
113
|
+
|
114
|
+
if (!b.isEnabled() && a.isEnabled()) {
|
115
|
+
return 1;
|
116
|
+
}
|
117
|
+
|
118
|
+
return 0;
|
119
|
+
});
|
120
|
+
|
121
|
+
const defaultTextStyle: TextStyle = { size: 12, fontFamily: 'sans', renderingStyle: { fill: Color4.red } };
|
122
|
+
const pastedTextStyle: TextStyle = textTools[0]?.getTextStyle() ?? defaultTextStyle;
|
123
|
+
|
124
|
+
const lines = text.split('\n');
|
125
|
+
let lastComponent: TextComponent|null = null;
|
126
|
+
const components: TextComponent[] = [];
|
127
|
+
|
128
|
+
for (const line of lines) {
|
129
|
+
let position = Vec2.zero;
|
130
|
+
if (lastComponent) {
|
131
|
+
const lineMargin = Math.floor(pastedTextStyle.size);
|
132
|
+
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
|
133
|
+
}
|
134
|
+
|
135
|
+
const component = new TextComponent([ line ], Mat33.translation(position), pastedTextStyle);
|
136
|
+
components.push(component);
|
137
|
+
lastComponent = component;
|
138
|
+
}
|
139
|
+
|
140
|
+
if (components.length === 1) {
|
141
|
+
await this.addComponentsFromPaste([ components[0] ]);
|
142
|
+
} else {
|
143
|
+
// Wrap the existing `TextComponent`s --- dragging one component should drag all.
|
144
|
+
await this.addComponentsFromPaste([
|
145
|
+
new TextComponent(components, Mat33.identity, pastedTextStyle)
|
146
|
+
]);
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
private async doImagePaste(dataURL: string) {
|
151
|
+
const image = new Image();
|
152
|
+
image.src = dataURL;
|
153
|
+
const component = await ImageComponent.fromImage(image, Mat33.identity);
|
154
|
+
await this.addComponentsFromPaste([ component ]);
|
155
|
+
}
|
156
|
+
}
|
package/src/tools/Pen.ts
CHANGED
@@ -3,7 +3,7 @@ import Editor from '../Editor';
|
|
3
3
|
import EditorImage from '../EditorImage';
|
4
4
|
import Pointer, { PointerDevice } from '../Pointer';
|
5
5
|
import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
|
6
|
-
import { EditorEventType, PointerEvt, StrokeDataPoint } from '../types';
|
6
|
+
import { EditorEventType, KeyPressEvent, PointerEvt, StrokeDataPoint } from '../types';
|
7
7
|
import BaseTool from './BaseTool';
|
8
8
|
import { ComponentBuilder, ComponentBuilderFactory } from '../components/builders/types';
|
9
9
|
|
@@ -159,4 +159,23 @@ export default class Pen extends BaseTool {
|
|
159
159
|
public getThickness() { return this.style.thickness; }
|
160
160
|
public getColor() { return this.style.color; }
|
161
161
|
public getStrokeFactory() { return this.builderFactory; }
|
162
|
+
|
163
|
+
public onKeyPress({ key }: KeyPressEvent): boolean {
|
164
|
+
key = key.toLowerCase();
|
165
|
+
|
166
|
+
let newThickness: number|undefined;
|
167
|
+
if (key === '-' || key === '_') {
|
168
|
+
newThickness = this.getThickness() * 2/3;
|
169
|
+
} else if (key === '+' || key === '=') {
|
170
|
+
newThickness = this.getThickness() * 3/2;
|
171
|
+
}
|
172
|
+
|
173
|
+
if (newThickness !== undefined) {
|
174
|
+
newThickness = Math.min(Math.max(1, newThickness), 128);
|
175
|
+
this.setThickness(newThickness);
|
176
|
+
return true;
|
177
|
+
}
|
178
|
+
|
179
|
+
return false;
|
180
|
+
}
|
162
181
|
}
|
@@ -11,10 +11,11 @@ import Mat33 from '../math/Mat33';
|
|
11
11
|
import Rect2 from '../math/Rect2';
|
12
12
|
import { Point2, Vec2 } from '../math/Vec2';
|
13
13
|
import { EditorLocalization } from '../localization';
|
14
|
-
import { EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
|
14
|
+
import { CopyEvent, EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
|
15
15
|
import Viewport from '../Viewport';
|
16
16
|
import BaseTool from './BaseTool';
|
17
17
|
import SerializableCommand from '../commands/SerializableCommand';
|
18
|
+
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
18
19
|
|
19
20
|
const handleScreenSize = 30;
|
20
21
|
const styles = `
|
@@ -381,6 +382,16 @@ class Selection {
|
|
381
382
|
this.region = Rect2.empty;
|
382
383
|
}
|
383
384
|
|
385
|
+
public setSelectedObjects(objects: AbstractComponent[], bbox: Rect2) {
|
386
|
+
this.region = bbox;
|
387
|
+
this.selectedElems = objects;
|
388
|
+
this.updateUI();
|
389
|
+
}
|
390
|
+
|
391
|
+
public getSelectedObjects(): AbstractComponent[] {
|
392
|
+
return this.selectedElems;
|
393
|
+
}
|
394
|
+
|
384
395
|
// Find the objects corresponding to this in the document,
|
385
396
|
// select them.
|
386
397
|
// Returns false iff nothing was selected.
|
@@ -528,15 +539,19 @@ export default class SelectionTool extends BaseTool {
|
|
528
539
|
this.editor.handleKeyEventsFrom(this.handleOverlay);
|
529
540
|
}
|
530
541
|
|
542
|
+
private makeSelectionBox(selectionStartPos: Point2) {
|
543
|
+
this.prevSelectionBox = this.selectionBox;
|
544
|
+
this.selectionBox = new Selection(
|
545
|
+
selectionStartPos, this.editor
|
546
|
+
);
|
547
|
+
// Remove any previous selection rects
|
548
|
+
this.handleOverlay.replaceChildren();
|
549
|
+
this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
|
550
|
+
}
|
551
|
+
|
531
552
|
public onPointerDown(event: PointerEvt): boolean {
|
532
553
|
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
533
|
-
this.
|
534
|
-
this.selectionBox = new Selection(
|
535
|
-
event.current.canvasPos, this.editor
|
536
|
-
);
|
537
|
-
// Remove any previous selection rects
|
538
|
-
this.handleOverlay.replaceChildren();
|
539
|
-
this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
|
554
|
+
this.makeSelectionBox(event.current.canvasPos);
|
540
555
|
|
541
556
|
return true;
|
542
557
|
}
|
@@ -679,6 +694,12 @@ export default class SelectionTool extends BaseTool {
|
|
679
694
|
this.selectionBox.transformPreview(transform);
|
680
695
|
}
|
681
696
|
|
697
|
+
if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
|
698
|
+
this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
|
699
|
+
this.clearSelection();
|
700
|
+
handled = true;
|
701
|
+
}
|
702
|
+
|
682
703
|
return handled;
|
683
704
|
}
|
684
705
|
|
@@ -690,6 +711,35 @@ export default class SelectionTool extends BaseTool {
|
|
690
711
|
return false;
|
691
712
|
}
|
692
713
|
|
714
|
+
public onCopy(event: CopyEvent): boolean {
|
715
|
+
if (!this.selectionBox) {
|
716
|
+
return false;
|
717
|
+
}
|
718
|
+
|
719
|
+
const selectedElems = this.selectionBox.getSelectedObjects();
|
720
|
+
const bbox = this.selectionBox.region;
|
721
|
+
if (selectedElems.length === 0) {
|
722
|
+
return false;
|
723
|
+
}
|
724
|
+
|
725
|
+
const exportViewport = new Viewport(this.editor.notifier);
|
726
|
+
exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
|
727
|
+
exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
|
728
|
+
|
729
|
+
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
730
|
+
const exportElem = document.createElementNS(svgNameSpace, 'svg');
|
731
|
+
|
732
|
+
const sanitize = true;
|
733
|
+
const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
|
734
|
+
|
735
|
+
for (const elem of selectedElems) {
|
736
|
+
elem.render(renderer);
|
737
|
+
}
|
738
|
+
|
739
|
+
event.setData('image/svg+xml', exportElem.outerHTML);
|
740
|
+
return true;
|
741
|
+
}
|
742
|
+
|
693
743
|
public setEnabled(enabled: boolean) {
|
694
744
|
super.setEnabled(enabled);
|
695
745
|
|
@@ -712,6 +762,28 @@ export default class SelectionTool extends BaseTool {
|
|
712
762
|
return this.selectionBox;
|
713
763
|
}
|
714
764
|
|
765
|
+
public setSelection(objects: AbstractComponent[]) {
|
766
|
+
let bbox: Rect2|null = null;
|
767
|
+
for (const object of objects) {
|
768
|
+
if (bbox) {
|
769
|
+
bbox = bbox.union(object.getBBox());
|
770
|
+
} else {
|
771
|
+
bbox = object.getBBox();
|
772
|
+
}
|
773
|
+
}
|
774
|
+
|
775
|
+
if (!bbox) {
|
776
|
+
return;
|
777
|
+
}
|
778
|
+
|
779
|
+
this.clearSelection();
|
780
|
+
if (!this.selectionBox) {
|
781
|
+
this.makeSelectionBox(bbox.topLeft);
|
782
|
+
}
|
783
|
+
|
784
|
+
this.selectionBox!.setSelectedObjects(objects, bbox);
|
785
|
+
}
|
786
|
+
|
715
787
|
public clearSelection() {
|
716
788
|
this.handleOverlay.replaceChildren();
|
717
789
|
this.prevSelectionBox = this.selectionBox;
|
@@ -11,31 +11,32 @@ import { ToolLocalization } from './localization';
|
|
11
11
|
import UndoRedoShortcut from './UndoRedoShortcut';
|
12
12
|
import TextTool from './TextTool';
|
13
13
|
import PipetteTool from './PipetteTool';
|
14
|
+
import ToolSwitcherShortcut from './ToolSwitcherShortcut';
|
15
|
+
import PasteHandler from './PasteHandler';
|
14
16
|
|
15
17
|
export default class ToolController {
|
16
18
|
private tools: BaseTool[];
|
17
19
|
private activeTool: BaseTool|null = null;
|
18
20
|
private primaryToolGroup: ToolEnabledGroup;
|
19
|
-
|
21
|
+
|
20
22
|
/** @internal */
|
21
23
|
public constructor(editor: Editor, localization: ToolLocalization) {
|
22
24
|
const primaryToolGroup = new ToolEnabledGroup();
|
23
25
|
this.primaryToolGroup = primaryToolGroup;
|
24
|
-
|
26
|
+
|
25
27
|
const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
|
26
28
|
const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom);
|
27
29
|
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
|
28
30
|
const primaryTools = [
|
29
|
-
new SelectionTool(editor, localization.selectionTool),
|
30
|
-
new Eraser(editor, localization.eraserTool),
|
31
|
-
|
32
31
|
// Three pens
|
33
32
|
primaryPenTool,
|
34
33
|
new Pen(editor, localization.penTool(2), { color: Color4.clay, thickness: 4 }),
|
35
|
-
|
34
|
+
|
36
35
|
// Highlighter-like pen with width=64
|
37
36
|
new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
|
38
|
-
|
37
|
+
|
38
|
+
new Eraser(editor, localization.eraserTool),
|
39
|
+
new SelectionTool(editor, localization.selectionTool),
|
39
40
|
new TextTool(editor, localization.textTool, localization),
|
40
41
|
];
|
41
42
|
this.tools = [
|
@@ -44,11 +45,13 @@ export default class ToolController {
|
|
44
45
|
...primaryTools,
|
45
46
|
keyboardPanZoomTool,
|
46
47
|
new UndoRedoShortcut(editor),
|
48
|
+
new ToolSwitcherShortcut(editor),
|
49
|
+
new PasteHandler(editor),
|
47
50
|
];
|
48
51
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
|
49
52
|
panZoomTool.setEnabled(true);
|
50
53
|
primaryPenTool.setEnabled(true);
|
51
|
-
|
54
|
+
|
52
55
|
editor.notifier.on(EditorEventType.ToolEnabled, event => {
|
53
56
|
if (event.kind === EditorEventType.ToolEnabled) {
|
54
57
|
editor.announceForAccessibility(localization.toolEnabledAnnouncement(event.tool.description));
|
@@ -59,17 +62,17 @@ export default class ToolController {
|
|
59
62
|
editor.announceForAccessibility(localization.toolDisabledAnnouncement(event.tool.description));
|
60
63
|
}
|
61
64
|
});
|
62
|
-
|
65
|
+
|
63
66
|
this.activeTool = null;
|
64
67
|
}
|
65
|
-
|
68
|
+
|
66
69
|
// Replaces the current set of tools with `tools`. This should only be done before
|
67
70
|
// the creation of the app's toolbar (if using `HTMLToolbar`).
|
68
71
|
public setTools(tools: BaseTool[], primaryToolGroup?: ToolEnabledGroup) {
|
69
72
|
this.tools = tools;
|
70
73
|
this.primaryToolGroup = primaryToolGroup ?? new ToolEnabledGroup();
|
71
74
|
}
|
72
|
-
|
75
|
+
|
73
76
|
// Add a tool that acts like one of the primary tools (only one primary tool can be enabled at a time).
|
74
77
|
// This should be called before creating the app's toolbar.
|
75
78
|
public addPrimaryTool(tool: BaseTool) {
|
@@ -77,16 +80,22 @@ export default class ToolController {
|
|
77
80
|
if (tool.isEnabled()) {
|
78
81
|
this.primaryToolGroup.notifyEnabled(tool);
|
79
82
|
}
|
80
|
-
|
83
|
+
|
81
84
|
this.addTool(tool);
|
82
85
|
}
|
83
|
-
|
86
|
+
|
87
|
+
public getPrimaryTools(): BaseTool[] {
|
88
|
+
return this.tools.filter(tool => {
|
89
|
+
return tool.getToolGroup() === this.primaryToolGroup;
|
90
|
+
});
|
91
|
+
}
|
92
|
+
|
84
93
|
// Add a tool to the end of this' tool list (the added tool receives events after tools already added to this).
|
85
94
|
// This should be called before creating the app's toolbar.
|
86
95
|
public addTool(tool: BaseTool) {
|
87
96
|
this.tools.push(tool);
|
88
97
|
}
|
89
|
-
|
98
|
+
|
90
99
|
// Returns true if the event was handled
|
91
100
|
public dispatchInputEvent(event: InputEvt): boolean {
|
92
101
|
let handled = false;
|
@@ -96,7 +105,7 @@ export default class ToolController {
|
|
96
105
|
if (this.activeTool !== tool) {
|
97
106
|
this.activeTool?.onGestureCancel();
|
98
107
|
}
|
99
|
-
|
108
|
+
|
100
109
|
this.activeTool = tool;
|
101
110
|
handled = true;
|
102
111
|
break;
|
@@ -106,49 +115,54 @@ export default class ToolController {
|
|
106
115
|
this.activeTool?.onPointerUp(event);
|
107
116
|
this.activeTool = null;
|
108
117
|
handled = true;
|
109
|
-
} else if (
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
118
|
+
} else if (event.kind === InputEvtType.PointerMoveEvt) {
|
119
|
+
if (this.activeTool !== null) {
|
120
|
+
this.activeTool.onPointerMove(event);
|
121
|
+
handled = true;
|
122
|
+
}
|
123
|
+
} else if (event.kind === InputEvtType.GestureCancelEvt) {
|
124
|
+
if (this.activeTool !== null) {
|
125
|
+
this.activeTool.onGestureCancel();
|
126
|
+
this.activeTool = null;
|
127
|
+
}
|
128
|
+
} else {
|
129
|
+
let allCasesHandledGuard: never;
|
130
|
+
|
115
131
|
for (const tool of this.tools) {
|
116
132
|
if (!tool.isEnabled()) {
|
117
133
|
continue;
|
118
134
|
}
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
135
|
+
|
136
|
+
switch (event.kind) {
|
137
|
+
case InputEvtType.KeyPressEvent:
|
138
|
+
handled = tool.onKeyPress(event);
|
139
|
+
break;
|
140
|
+
case InputEvtType.KeyUpEvent:
|
141
|
+
handled = tool.onKeyUp(event);
|
142
|
+
break;
|
143
|
+
case InputEvtType.WheelEvt:
|
144
|
+
handled = tool.onWheel(event);
|
145
|
+
break;
|
146
|
+
case InputEvtType.CopyEvent:
|
147
|
+
handled = tool.onCopy(event);
|
148
|
+
break;
|
149
|
+
case InputEvtType.PasteEvent:
|
150
|
+
handled = tool.onPaste(event);
|
151
|
+
break;
|
152
|
+
default:
|
153
|
+
allCasesHandledGuard = event;
|
154
|
+
return allCasesHandledGuard;
|
155
|
+
}
|
124
156
|
|
125
157
|
if (handled) {
|
126
158
|
break;
|
127
159
|
}
|
128
160
|
}
|
129
|
-
} else if (this.activeTool !== null) {
|
130
|
-
let allCasesHandledGuard: never;
|
131
|
-
|
132
|
-
switch (event.kind) {
|
133
|
-
case InputEvtType.PointerMoveEvt:
|
134
|
-
this.activeTool.onPointerMove(event);
|
135
|
-
break;
|
136
|
-
case InputEvtType.GestureCancelEvt:
|
137
|
-
this.activeTool.onGestureCancel();
|
138
|
-
this.activeTool = null;
|
139
|
-
break;
|
140
|
-
default:
|
141
|
-
allCasesHandledGuard = event;
|
142
|
-
return allCasesHandledGuard;
|
143
|
-
}
|
144
|
-
handled = true;
|
145
|
-
} else {
|
146
|
-
handled = false;
|
147
161
|
}
|
148
|
-
|
162
|
+
|
149
163
|
return handled;
|
150
164
|
}
|
151
|
-
|
165
|
+
|
152
166
|
public getMatchingTools<Type extends BaseTool>(type: new (...args: any[])=>Type): Type[] {
|
153
167
|
return this.tools.filter(tool => tool instanceof type) as Type[];
|
154
168
|
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
// Handles ctrl+1, ctrl+2, ctrl+3, ..., shortcuts for switching tools.
|
2
|
+
// @packageDocumentation
|
3
|
+
|
4
|
+
import Editor from '../Editor';
|
5
|
+
import { KeyPressEvent } from '../types';
|
6
|
+
import BaseTool from './BaseTool';
|
7
|
+
|
8
|
+
// {@inheritDoc ToolSwitcherShortcut!}
|
9
|
+
export default class ToolSwitcherShortcut extends BaseTool {
|
10
|
+
public constructor(private editor: Editor) {
|
11
|
+
super(editor.notifier, editor.localization.changeTool);
|
12
|
+
}
|
13
|
+
|
14
|
+
public onKeyPress({ key }: KeyPressEvent): boolean {
|
15
|
+
const toolController = this.editor.toolController;
|
16
|
+
const primaryTools = toolController.getPrimaryTools();
|
17
|
+
|
18
|
+
// Map keys 0-9 to primary tools.
|
19
|
+
const keyMatch = /^[0-9]$/.exec(key);
|
20
|
+
|
21
|
+
let targetTool: BaseTool|undefined;
|
22
|
+
if (keyMatch) {
|
23
|
+
const targetIdx = parseInt(keyMatch[0], 10) - 1;
|
24
|
+
targetTool = primaryTools[targetIdx];
|
25
|
+
}
|
26
|
+
|
27
|
+
if (targetTool) {
|
28
|
+
targetTool.setEnabled(true);
|
29
|
+
return true;
|
30
|
+
}
|
31
|
+
|
32
|
+
return false;
|
33
|
+
}
|
34
|
+
}
|
package/src/tools/lib.ts
CHANGED
@@ -8,10 +8,12 @@ export { default as ToolController } from './ToolController';
|
|
8
8
|
export { default as ToolEnabledGroup } from './ToolEnabledGroup';
|
9
9
|
|
10
10
|
export { default as UndoRedoShortcut } from './UndoRedoShortcut';
|
11
|
+
export { default as ToolSwitcherShortcut } from './ToolSwitcherShortcut';
|
11
12
|
export { default as PanZoomTool, PanZoomMode } from './PanZoom';
|
12
13
|
|
13
14
|
export { default as PenTool, PenStyle } from './Pen';
|
14
15
|
export { default as TextTool } from './TextTool';
|
15
16
|
export { default as SelectionTool } from './SelectionTool';
|
16
17
|
export { default as EraserTool } from './Eraser';
|
18
|
+
export { default as PasteHandler } from './PasteHandler';
|
17
19
|
|