js-draw 0.3.1 → 0.4.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/.github/ISSUE_TEMPLATE/translation.md +4 -1
- package/CHANGELOG.md +16 -0
- package/README.md +1 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +15 -1
- package/dist/src/Editor.js +221 -78
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +8 -3
- 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 +2 -0
- package/dist/src/Viewport.js +26 -5
- 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/builders/FreehandLineBuilder.js +2 -2
- 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/language/assertions.d.ts +1 -0
- package/dist/src/language/assertions.js +5 -0
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +3 -0
- package/dist/src/math/Mat33.d.ts +38 -2
- package/dist/src/math/Mat33.js +30 -1
- package/dist/src/math/Path.d.ts +1 -1
- package/dist/src/math/Path.js +10 -8
- package/dist/src/math/Vec3.d.ts +11 -1
- package/dist/src/math/Vec3.js +15 -0
- package/dist/src/math/rounding.d.ts +1 -0
- package/dist/src/math/rounding.js +13 -6
- 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 +7 -0
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -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 +5 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/toolbar/HTMLToolbar.js +5 -4
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/tools/BaseTool.d.ts +3 -1
- package/dist/src/tools/BaseTool.js +6 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +144 -0
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
- package/dist/src/tools/SelectionTool/Selection.js +337 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +276 -0
- package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
- package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
- package/dist/src/tools/SelectionTool/types.d.ts +9 -0
- package/dist/src/tools/SelectionTool/types.js +11 -0
- package/dist/src/tools/ToolController.js +37 -28
- package/dist/src/tools/lib.d.ts +2 -1
- package/dist/src/tools/lib.js +2 -1
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist/src/types.d.ts +14 -3
- package/dist/src/types.js +2 -0
- package/package.json +1 -1
- package/src/Editor.css +1 -0
- package/src/Editor.ts +275 -109
- package/src/EditorImage.ts +7 -1
- package/src/Pointer.ts +8 -3
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +30 -6
- 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 +53 -11
- package/src/components/ImageComponent.ts +149 -0
- package/src/components/Text.ts +2 -6
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/language/assertions.ts +6 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +5 -0
- package/src/math/Mat33.test.ts +14 -0
- package/src/math/Mat33.ts +43 -2
- package/src/math/Path.toString.test.ts +12 -1
- package/src/math/Path.ts +11 -9
- package/src/math/Vec3.ts +22 -1
- package/src/math/rounding.test.ts +30 -5
- package/src/math/rounding.ts +16 -7
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +19 -2
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +50 -21
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/BaseTool.ts +9 -1
- package/src/tools/PasteHandler.ts +159 -0
- package/src/tools/Pen.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +455 -0
- package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
- package/src/tools/SelectionTool/SelectionTool.css +22 -0
- package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
- package/src/tools/SelectionTool/SelectionTool.ts +335 -0
- package/src/tools/SelectionTool/TransformMode.ts +114 -0
- package/src/tools/SelectionTool/types.ts +11 -0
- package/src/tools/ToolController.ts +52 -45
- package/src/tools/lib.ts +2 -1
- package/src/tools/localization.ts +8 -0
- package/src/types.ts +17 -3
- package/dist/src/tools/SelectionTool.d.ts +0 -59
- package/dist/src/tools/SelectionTool.js +0 -589
- package/src/tools/SelectionTool.ts +0 -725
@@ -0,0 +1,335 @@
|
|
1
|
+
// Allows users to select/transform portions of the `EditorImage`.
|
2
|
+
// With respect to `extend`ing, `SelectionTool` is not stable.
|
3
|
+
// @packageDocumentation
|
4
|
+
|
5
|
+
import AbstractComponent from '../../components/AbstractComponent';
|
6
|
+
import Editor from '../../Editor';
|
7
|
+
import Mat33 from '../../math/Mat33';
|
8
|
+
import Rect2 from '../../math/Rect2';
|
9
|
+
import { Point2, Vec2 } from '../../math/Vec2';
|
10
|
+
import { CopyEvent, EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt } from '../../types';
|
11
|
+
import Viewport from '../../Viewport';
|
12
|
+
import BaseTool from '../BaseTool';
|
13
|
+
import SVGRenderer from '../../rendering/renderers/SVGRenderer';
|
14
|
+
import Selection from './Selection';
|
15
|
+
|
16
|
+
export const cssPrefix = 'selection-tool-';
|
17
|
+
|
18
|
+
// {@inheritDoc SelectionTool!}
|
19
|
+
export default class SelectionTool extends BaseTool {
|
20
|
+
private handleOverlay: HTMLElement;
|
21
|
+
private prevSelectionBox: Selection|null;
|
22
|
+
private selectionBox: Selection|null;
|
23
|
+
private lastEvtTarget: EventTarget|null = null;
|
24
|
+
|
25
|
+
public constructor(private editor: Editor, description: string) {
|
26
|
+
super(editor.notifier, description);
|
27
|
+
|
28
|
+
this.handleOverlay = document.createElement('div');
|
29
|
+
editor.createHTMLOverlay(this.handleOverlay);
|
30
|
+
|
31
|
+
this.handleOverlay.style.display = 'none';
|
32
|
+
this.handleOverlay.classList.add('handleOverlay');
|
33
|
+
|
34
|
+
editor.notifier.on(EditorEventType.ViewportChanged, _data => {
|
35
|
+
this.selectionBox?.updateUI();
|
36
|
+
});
|
37
|
+
|
38
|
+
this.editor.handleKeyEventsFrom(this.handleOverlay);
|
39
|
+
this.editor.handlePointerEventsFrom(this.handleOverlay, (eventName, htmlEvent: PointerEvent) => {
|
40
|
+
if (eventName === 'pointerdown') {
|
41
|
+
this.lastEvtTarget = htmlEvent.target;
|
42
|
+
}
|
43
|
+
return true;
|
44
|
+
});
|
45
|
+
}
|
46
|
+
|
47
|
+
private makeSelectionBox(selectionStartPos: Point2) {
|
48
|
+
this.prevSelectionBox = this.selectionBox;
|
49
|
+
this.selectionBox = new Selection(
|
50
|
+
selectionStartPos, this.editor
|
51
|
+
);
|
52
|
+
// Remove any previous selection rects
|
53
|
+
this.handleOverlay.replaceChildren();
|
54
|
+
this.selectionBox.addTo(this.handleOverlay);
|
55
|
+
}
|
56
|
+
|
57
|
+
private selectionBoxHandlingEvt: boolean = false;
|
58
|
+
public onPointerDown(event: PointerEvt): boolean {
|
59
|
+
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
60
|
+
if (this.lastEvtTarget && this.selectionBox?.onDragStart(event.current, this.lastEvtTarget)) {
|
61
|
+
this.selectionBoxHandlingEvt = true;
|
62
|
+
} else {
|
63
|
+
this.makeSelectionBox(event.current.canvasPos);
|
64
|
+
}
|
65
|
+
|
66
|
+
return true;
|
67
|
+
}
|
68
|
+
return false;
|
69
|
+
}
|
70
|
+
|
71
|
+
public onPointerMove(event: PointerEvt): void {
|
72
|
+
if (!this.selectionBox) return;
|
73
|
+
|
74
|
+
if (this.selectionBoxHandlingEvt) {
|
75
|
+
this.selectionBox.onDragUpdate(event.current);
|
76
|
+
} else {
|
77
|
+
this.selectionBox!.setToPoint(event.current.canvasPos);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
private onSelectionUpdated() {
|
82
|
+
// Note that the selection has changed
|
83
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
84
|
+
kind: EditorEventType.ToolUpdated,
|
85
|
+
tool: this,
|
86
|
+
});
|
87
|
+
|
88
|
+
const selectedItemCount = this.selectionBox?.getSelectedItemCount() ?? 0;
|
89
|
+
if (selectedItemCount > 0) {
|
90
|
+
this.editor.announceForAccessibility(
|
91
|
+
this.editor.localization.selectedElements(selectedItemCount)
|
92
|
+
);
|
93
|
+
this.zoomToSelection();
|
94
|
+
} else if (this.selectionBox) {
|
95
|
+
this.selectionBox.cancelSelection();
|
96
|
+
this.prevSelectionBox = this.selectionBox;
|
97
|
+
this.selectionBox = null;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
private onGestureEnd() {
|
102
|
+
this.lastEvtTarget = null;
|
103
|
+
|
104
|
+
if (!this.selectionBox) return;
|
105
|
+
|
106
|
+
if (!this.selectionBoxHandlingEvt) {
|
107
|
+
// Expand/shrink the selection rectangle, if applicable
|
108
|
+
this.selectionBox.resolveToObjects();
|
109
|
+
this.onSelectionUpdated();
|
110
|
+
} else {
|
111
|
+
this.selectionBox.onDragEnd();
|
112
|
+
}
|
113
|
+
|
114
|
+
|
115
|
+
this.selectionBoxHandlingEvt = false;
|
116
|
+
}
|
117
|
+
|
118
|
+
private zoomToSelection() {
|
119
|
+
if (this.selectionBox) {
|
120
|
+
const selectionRect = this.selectionBox.region;
|
121
|
+
this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false);
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
public onPointerUp(event: PointerEvt): void {
|
126
|
+
if (!this.selectionBox) return;
|
127
|
+
|
128
|
+
this.selectionBox.setToPoint(event.current.canvasPos);
|
129
|
+
this.onGestureEnd();
|
130
|
+
}
|
131
|
+
|
132
|
+
public onGestureCancel(): void {
|
133
|
+
if (this.selectionBoxHandlingEvt) {
|
134
|
+
this.selectionBox?.onDragCancel();
|
135
|
+
} else {
|
136
|
+
// Revert to the previous selection, if any.
|
137
|
+
this.selectionBox?.cancelSelection();
|
138
|
+
this.selectionBox = this.prevSelectionBox;
|
139
|
+
this.selectionBox?.addTo(this.handleOverlay);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
private static handleableKeys = [
|
144
|
+
'a', 'h', 'ArrowLeft',
|
145
|
+
'd', 'l', 'ArrowRight',
|
146
|
+
'q', 'k', 'ArrowUp',
|
147
|
+
'e', 'j', 'ArrowDown',
|
148
|
+
'r', 'R',
|
149
|
+
'i', 'I', 'o', 'O',
|
150
|
+
];
|
151
|
+
public onKeyPress(event: KeyPressEvent): boolean {
|
152
|
+
let rotationSteps = 0;
|
153
|
+
let xTranslateSteps = 0;
|
154
|
+
let yTranslateSteps = 0;
|
155
|
+
let xScaleSteps = 0;
|
156
|
+
let yScaleSteps = 0;
|
157
|
+
|
158
|
+
switch (event.key) {
|
159
|
+
case 'a':
|
160
|
+
case 'h':
|
161
|
+
case 'ArrowLeft':
|
162
|
+
xTranslateSteps -= 1;
|
163
|
+
break;
|
164
|
+
case 'd':
|
165
|
+
case 'l':
|
166
|
+
case 'ArrowRight':
|
167
|
+
xTranslateSteps += 1;
|
168
|
+
break;
|
169
|
+
case 'q':
|
170
|
+
case 'k':
|
171
|
+
case 'ArrowUp':
|
172
|
+
yTranslateSteps -= 1;
|
173
|
+
break;
|
174
|
+
case 'e':
|
175
|
+
case 'j':
|
176
|
+
case 'ArrowDown':
|
177
|
+
yTranslateSteps += 1;
|
178
|
+
break;
|
179
|
+
case 'r':
|
180
|
+
rotationSteps += 1;
|
181
|
+
break;
|
182
|
+
case 'R':
|
183
|
+
rotationSteps -= 1;
|
184
|
+
break;
|
185
|
+
case 'i':
|
186
|
+
xScaleSteps -= 1;
|
187
|
+
break;
|
188
|
+
case 'I':
|
189
|
+
xScaleSteps += 1;
|
190
|
+
break;
|
191
|
+
case 'o':
|
192
|
+
yScaleSteps -= 1;
|
193
|
+
break;
|
194
|
+
case 'O':
|
195
|
+
yScaleSteps += 1;
|
196
|
+
break;
|
197
|
+
}
|
198
|
+
|
199
|
+
let handled = xTranslateSteps !== 0
|
200
|
+
|| yTranslateSteps !== 0
|
201
|
+
|| rotationSteps !== 0
|
202
|
+
|| xScaleSteps !== 0
|
203
|
+
|| yScaleSteps !== 0;
|
204
|
+
|
205
|
+
if (!this.selectionBox) {
|
206
|
+
handled = false;
|
207
|
+
} else if (handled) {
|
208
|
+
const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas();
|
209
|
+
const rotateStepSize = Math.PI / 8;
|
210
|
+
const scaleStepSize = 5 / 4;
|
211
|
+
|
212
|
+
const region = this.selectionBox.region;
|
213
|
+
const scaleFactor = Vec2.of(scaleStepSize ** xScaleSteps, scaleStepSize ** yScaleSteps);
|
214
|
+
|
215
|
+
const rotationMat = Mat33.zRotation(
|
216
|
+
rotationSteps * rotateStepSize
|
217
|
+
);
|
218
|
+
const roundedRotationMatrix = rotationMat.mapEntries(component => Viewport.roundScaleRatio(component));
|
219
|
+
const regionCenter = this.editor.viewport.roundPoint(region.center);
|
220
|
+
|
221
|
+
const transform = Mat33.scaling2D(
|
222
|
+
scaleFactor,
|
223
|
+
this.editor.viewport.roundPoint(region.topLeft)
|
224
|
+
).rightMul(
|
225
|
+
Mat33.translation(regionCenter).rightMul(
|
226
|
+
roundedRotationMatrix
|
227
|
+
).rightMul(
|
228
|
+
Mat33.translation(regionCenter.times(-1))
|
229
|
+
)
|
230
|
+
).rightMul(Mat33.translation(
|
231
|
+
this.editor.viewport.roundPoint(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))
|
232
|
+
));
|
233
|
+
const oldTransform = this.selectionBox.getTransform();
|
234
|
+
this.selectionBox.setTransform(oldTransform.rightMul(transform));
|
235
|
+
}
|
236
|
+
|
237
|
+
if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
|
238
|
+
this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
|
239
|
+
this.clearSelection();
|
240
|
+
handled = true;
|
241
|
+
}
|
242
|
+
|
243
|
+
return handled;
|
244
|
+
}
|
245
|
+
|
246
|
+
public onKeyUp(evt: KeyUpEvent) {
|
247
|
+
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
248
|
+
this.selectionBox.finalizeTransform();
|
249
|
+
return true;
|
250
|
+
}
|
251
|
+
return false;
|
252
|
+
}
|
253
|
+
|
254
|
+
public onCopy(event: CopyEvent): boolean {
|
255
|
+
if (!this.selectionBox) {
|
256
|
+
return false;
|
257
|
+
}
|
258
|
+
|
259
|
+
const selectedElems = this.selectionBox.getSelectedObjects();
|
260
|
+
const bbox = this.selectionBox.region;
|
261
|
+
if (selectedElems.length === 0) {
|
262
|
+
return false;
|
263
|
+
}
|
264
|
+
|
265
|
+
const exportViewport = new Viewport(this.editor.notifier);
|
266
|
+
exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
|
267
|
+
exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
|
268
|
+
|
269
|
+
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
270
|
+
const exportElem = document.createElementNS(svgNameSpace, 'svg');
|
271
|
+
|
272
|
+
const sanitize = true;
|
273
|
+
const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
|
274
|
+
|
275
|
+
for (const elem of selectedElems) {
|
276
|
+
elem.render(renderer);
|
277
|
+
}
|
278
|
+
|
279
|
+
event.setData('image/svg+xml', exportElem.outerHTML);
|
280
|
+
return true;
|
281
|
+
}
|
282
|
+
|
283
|
+
public setEnabled(enabled: boolean) {
|
284
|
+
super.setEnabled(enabled);
|
285
|
+
|
286
|
+
// Clear the selection
|
287
|
+
this.handleOverlay.replaceChildren();
|
288
|
+
this.selectionBox = null;
|
289
|
+
|
290
|
+
this.handleOverlay.style.display = enabled ? 'block' : 'none';
|
291
|
+
|
292
|
+
if (enabled) {
|
293
|
+
this.handleOverlay.tabIndex = 0;
|
294
|
+
this.handleOverlay.setAttribute('aria-label', this.editor.localization.selectionToolKeyboardShortcuts);
|
295
|
+
} else {
|
296
|
+
this.handleOverlay.tabIndex = -1;
|
297
|
+
}
|
298
|
+
}
|
299
|
+
|
300
|
+
// Get the object responsible for displaying this' selection.
|
301
|
+
public getSelection(): Selection|null {
|
302
|
+
return this.selectionBox;
|
303
|
+
}
|
304
|
+
|
305
|
+
public setSelection(objects: AbstractComponent[]) {
|
306
|
+
let bbox: Rect2|null = null;
|
307
|
+
for (const object of objects) {
|
308
|
+
if (bbox) {
|
309
|
+
bbox = bbox.union(object.getBBox());
|
310
|
+
} else {
|
311
|
+
bbox = object.getBBox();
|
312
|
+
}
|
313
|
+
}
|
314
|
+
|
315
|
+
if (!bbox) {
|
316
|
+
return;
|
317
|
+
}
|
318
|
+
|
319
|
+
this.clearSelection();
|
320
|
+
if (!this.selectionBox) {
|
321
|
+
this.makeSelectionBox(bbox.topLeft);
|
322
|
+
}
|
323
|
+
|
324
|
+
this.selectionBox!.setSelectedObjects(objects, bbox);
|
325
|
+
this.onSelectionUpdated();
|
326
|
+
}
|
327
|
+
|
328
|
+
public clearSelection() {
|
329
|
+
this.handleOverlay.replaceChildren();
|
330
|
+
this.prevSelectionBox = this.selectionBox;
|
331
|
+
this.selectionBox = null;
|
332
|
+
|
333
|
+
this.onSelectionUpdated();
|
334
|
+
}
|
335
|
+
}
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import Editor from '../../Editor';
|
2
|
+
import Mat33 from '../../math/Mat33';
|
3
|
+
import { Point2, Vec2 } from '../../math/Vec2';
|
4
|
+
import Vec3 from '../../math/Vec3';
|
5
|
+
import Viewport from '../../Viewport';
|
6
|
+
import Selection from './Selection';
|
7
|
+
import { ResizeMode } from './types';
|
8
|
+
|
9
|
+
export class DragTransformer {
|
10
|
+
private dragStartPoint: Point2;
|
11
|
+
public constructor(private readonly editor: Editor, private selection: Selection) { }
|
12
|
+
|
13
|
+
public onDragStart(startPoint: Vec3) {
|
14
|
+
this.selection.setTransform(Mat33.identity);
|
15
|
+
this.dragStartPoint = startPoint;
|
16
|
+
}
|
17
|
+
public onDragUpdate(canvasPos: Vec3) {
|
18
|
+
const delta = this.editor.viewport.roundPoint(canvasPos.minus(this.dragStartPoint));
|
19
|
+
this.selection.setTransform(Mat33.translation(
|
20
|
+
delta
|
21
|
+
));
|
22
|
+
}
|
23
|
+
public onDragEnd() {
|
24
|
+
this.selection.finalizeTransform();
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
export class ResizeTransformer {
|
29
|
+
private mode: ResizeMode = ResizeMode.Both;
|
30
|
+
private dragStartPoint: Point2;
|
31
|
+
public constructor(private readonly editor: Editor, private selection: Selection) { }
|
32
|
+
|
33
|
+
public onDragStart(startPoint: Vec3, mode: ResizeMode) {
|
34
|
+
this.selection.setTransform(Mat33.identity);
|
35
|
+
this.mode = mode;
|
36
|
+
this.dragStartPoint = startPoint;
|
37
|
+
}
|
38
|
+
public onDragUpdate(canvasPos: Vec3) {
|
39
|
+
const canvasDelta = canvasPos.minus(this.dragStartPoint);
|
40
|
+
|
41
|
+
const origWidth = this.selection.preTransformRegion.width;
|
42
|
+
const origHeight = this.selection.preTransformRegion.height;
|
43
|
+
|
44
|
+
let scale = Vec2.of(1, 1);
|
45
|
+
if (this.mode === ResizeMode.HorizontalOnly) {
|
46
|
+
const newWidth = origWidth + canvasDelta.x;
|
47
|
+
scale = Vec2.of(newWidth / origWidth, scale.y);
|
48
|
+
}
|
49
|
+
|
50
|
+
if (this.mode === ResizeMode.VerticalOnly) {
|
51
|
+
const newHeight = origHeight + canvasDelta.y;
|
52
|
+
scale = Vec2.of(scale.x, newHeight / origHeight);
|
53
|
+
}
|
54
|
+
|
55
|
+
if (this.mode === ResizeMode.Both) {
|
56
|
+
const delta = Math.abs(canvasDelta.x) > Math.abs(canvasDelta.y) ? canvasDelta.x : canvasDelta.y;
|
57
|
+
const newWidth = origWidth + delta;
|
58
|
+
scale = Vec2.of(newWidth / origWidth, newWidth / origWidth);
|
59
|
+
}
|
60
|
+
|
61
|
+
// Round: If this isn't done, scaling can create numbers with long decimal representations.
|
62
|
+
// long decimal representations => large file sizes.
|
63
|
+
scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
|
64
|
+
|
65
|
+
if (scale.x > 0 && scale.y > 0) {
|
66
|
+
const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
|
67
|
+
this.selection.setTransform(Mat33.scaling2D(scale, origin));
|
68
|
+
}
|
69
|
+
}
|
70
|
+
public onDragEnd() {
|
71
|
+
this.selection.finalizeTransform();
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
export class RotateTransformer {
|
76
|
+
private startAngle: number = 0;
|
77
|
+
public constructor(private readonly editor: Editor, private selection: Selection) { }
|
78
|
+
|
79
|
+
private getAngle(canvasPoint: Point2) {
|
80
|
+
const selectionCenter = this.selection.preTransformRegion.center;
|
81
|
+
const offset = canvasPoint.minus(selectionCenter);
|
82
|
+
return offset.angle();
|
83
|
+
}
|
84
|
+
|
85
|
+
private roundAngle(angle: number) {
|
86
|
+
// Round angles to the nearest 16th of a turn
|
87
|
+
const roundingFactor = 16 / 2 / Math.PI;
|
88
|
+
return Math.round(angle * roundingFactor) / roundingFactor;
|
89
|
+
}
|
90
|
+
|
91
|
+
public onDragStart(startPoint: Vec3) {
|
92
|
+
this.selection.setTransform(Mat33.identity);
|
93
|
+
this.startAngle = this.getAngle(startPoint);
|
94
|
+
}
|
95
|
+
|
96
|
+
public onDragUpdate(canvasPos: Vec3) {
|
97
|
+
const targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
|
98
|
+
|
99
|
+
// Transform in canvas space
|
100
|
+
const canvasSelCenter = this.editor.viewport.roundPoint(this.selection.preTransformRegion.center);
|
101
|
+
const unrounded = Mat33.zRotation(targetRotation);
|
102
|
+
const roundedRotationTransform = unrounded.mapEntries(entry => Viewport.roundScaleRatio(entry));
|
103
|
+
|
104
|
+
const fullRoundedTransform = Mat33
|
105
|
+
.translation(canvasSelCenter)
|
106
|
+
.rightMul(roundedRotationTransform)
|
107
|
+
.rightMul(Mat33.translation(canvasSelCenter.times(-1)));
|
108
|
+
|
109
|
+
this.selection.setTransform(fullRoundedTransform);
|
110
|
+
}
|
111
|
+
public onDragEnd() {
|
112
|
+
this.selection.finalizeTransform();
|
113
|
+
}
|
114
|
+
}
|
@@ -5,24 +5,25 @@ import PanZoom, { PanZoomMode } from './PanZoom';
|
|
5
5
|
import Pen from './Pen';
|
6
6
|
import ToolEnabledGroup from './ToolEnabledGroup';
|
7
7
|
import Eraser from './Eraser';
|
8
|
-
import SelectionTool from './SelectionTool';
|
8
|
+
import SelectionTool from './SelectionTool/SelectionTool';
|
9
9
|
import Color4 from '../Color4';
|
10
10
|
import { ToolLocalization } from './localization';
|
11
11
|
import UndoRedoShortcut from './UndoRedoShortcut';
|
12
12
|
import TextTool from './TextTool';
|
13
13
|
import PipetteTool from './PipetteTool';
|
14
14
|
import ToolSwitcherShortcut from './ToolSwitcherShortcut';
|
15
|
+
import PasteHandler from './PasteHandler';
|
15
16
|
|
16
17
|
export default class ToolController {
|
17
18
|
private tools: BaseTool[];
|
18
19
|
private activeTool: BaseTool|null = null;
|
19
20
|
private primaryToolGroup: ToolEnabledGroup;
|
20
|
-
|
21
|
+
|
21
22
|
/** @internal */
|
22
23
|
public constructor(editor: Editor, localization: ToolLocalization) {
|
23
24
|
const primaryToolGroup = new ToolEnabledGroup();
|
24
25
|
this.primaryToolGroup = primaryToolGroup;
|
25
|
-
|
26
|
+
|
26
27
|
const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
|
27
28
|
const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom);
|
28
29
|
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
|
@@ -30,10 +31,10 @@ export default class ToolController {
|
|
30
31
|
// Three pens
|
31
32
|
primaryPenTool,
|
32
33
|
new Pen(editor, localization.penTool(2), { color: Color4.clay, thickness: 4 }),
|
33
|
-
|
34
|
+
|
34
35
|
// Highlighter-like pen with width=64
|
35
36
|
new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
|
36
|
-
|
37
|
+
|
37
38
|
new Eraser(editor, localization.eraserTool),
|
38
39
|
new SelectionTool(editor, localization.selectionTool),
|
39
40
|
new TextTool(editor, localization.textTool, localization),
|
@@ -45,11 +46,12 @@ export default class ToolController {
|
|
45
46
|
keyboardPanZoomTool,
|
46
47
|
new UndoRedoShortcut(editor),
|
47
48
|
new ToolSwitcherShortcut(editor),
|
49
|
+
new PasteHandler(editor),
|
48
50
|
];
|
49
51
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
|
50
52
|
panZoomTool.setEnabled(true);
|
51
53
|
primaryPenTool.setEnabled(true);
|
52
|
-
|
54
|
+
|
53
55
|
editor.notifier.on(EditorEventType.ToolEnabled, event => {
|
54
56
|
if (event.kind === EditorEventType.ToolEnabled) {
|
55
57
|
editor.announceForAccessibility(localization.toolEnabledAnnouncement(event.tool.description));
|
@@ -60,17 +62,17 @@ export default class ToolController {
|
|
60
62
|
editor.announceForAccessibility(localization.toolDisabledAnnouncement(event.tool.description));
|
61
63
|
}
|
62
64
|
});
|
63
|
-
|
65
|
+
|
64
66
|
this.activeTool = null;
|
65
67
|
}
|
66
|
-
|
68
|
+
|
67
69
|
// Replaces the current set of tools with `tools`. This should only be done before
|
68
70
|
// the creation of the app's toolbar (if using `HTMLToolbar`).
|
69
71
|
public setTools(tools: BaseTool[], primaryToolGroup?: ToolEnabledGroup) {
|
70
72
|
this.tools = tools;
|
71
73
|
this.primaryToolGroup = primaryToolGroup ?? new ToolEnabledGroup();
|
72
74
|
}
|
73
|
-
|
75
|
+
|
74
76
|
// Add a tool that acts like one of the primary tools (only one primary tool can be enabled at a time).
|
75
77
|
// This should be called before creating the app's toolbar.
|
76
78
|
public addPrimaryTool(tool: BaseTool) {
|
@@ -78,22 +80,22 @@ export default class ToolController {
|
|
78
80
|
if (tool.isEnabled()) {
|
79
81
|
this.primaryToolGroup.notifyEnabled(tool);
|
80
82
|
}
|
81
|
-
|
83
|
+
|
82
84
|
this.addTool(tool);
|
83
85
|
}
|
84
|
-
|
86
|
+
|
85
87
|
public getPrimaryTools(): BaseTool[] {
|
86
88
|
return this.tools.filter(tool => {
|
87
89
|
return tool.getToolGroup() === this.primaryToolGroup;
|
88
90
|
});
|
89
91
|
}
|
90
|
-
|
92
|
+
|
91
93
|
// Add a tool to the end of this' tool list (the added tool receives events after tools already added to this).
|
92
94
|
// This should be called before creating the app's toolbar.
|
93
95
|
public addTool(tool: BaseTool) {
|
94
96
|
this.tools.push(tool);
|
95
97
|
}
|
96
|
-
|
98
|
+
|
97
99
|
// Returns true if the event was handled
|
98
100
|
public dispatchInputEvent(event: InputEvt): boolean {
|
99
101
|
let handled = false;
|
@@ -103,7 +105,7 @@ export default class ToolController {
|
|
103
105
|
if (this.activeTool !== tool) {
|
104
106
|
this.activeTool?.onGestureCancel();
|
105
107
|
}
|
106
|
-
|
108
|
+
|
107
109
|
this.activeTool = tool;
|
108
110
|
handled = true;
|
109
111
|
break;
|
@@ -113,49 +115,54 @@ export default class ToolController {
|
|
113
115
|
this.activeTool?.onPointerUp(event);
|
114
116
|
this.activeTool = null;
|
115
117
|
handled = true;
|
116
|
-
} else if (
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
+
|
122
131
|
for (const tool of this.tools) {
|
123
132
|
if (!tool.isEnabled()) {
|
124
133
|
continue;
|
125
134
|
}
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
+
}
|
131
156
|
|
132
157
|
if (handled) {
|
133
158
|
break;
|
134
159
|
}
|
135
160
|
}
|
136
|
-
} else if (this.activeTool !== null) {
|
137
|
-
let allCasesHandledGuard: never;
|
138
|
-
|
139
|
-
switch (event.kind) {
|
140
|
-
case InputEvtType.PointerMoveEvt:
|
141
|
-
this.activeTool.onPointerMove(event);
|
142
|
-
break;
|
143
|
-
case InputEvtType.GestureCancelEvt:
|
144
|
-
this.activeTool.onGestureCancel();
|
145
|
-
this.activeTool = null;
|
146
|
-
break;
|
147
|
-
default:
|
148
|
-
allCasesHandledGuard = event;
|
149
|
-
return allCasesHandledGuard;
|
150
|
-
}
|
151
|
-
handled = true;
|
152
|
-
} else {
|
153
|
-
handled = false;
|
154
161
|
}
|
155
|
-
|
162
|
+
|
156
163
|
return handled;
|
157
164
|
}
|
158
|
-
|
165
|
+
|
159
166
|
public getMatchingTools<Type extends BaseTool>(type: new (...args: any[])=>Type): Type[] {
|
160
167
|
return this.tools.filter(tool => tool instanceof type) as Type[];
|
161
168
|
}
|
package/src/tools/lib.ts
CHANGED
@@ -13,6 +13,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
|
|
13
13
|
|
14
14
|
export { default as PenTool, PenStyle } from './Pen';
|
15
15
|
export { default as TextTool } from './TextTool';
|
16
|
-
export { default as SelectionTool } from './SelectionTool';
|
16
|
+
export { default as SelectionTool } from './SelectionTool/SelectionTool';
|
17
17
|
export { default as EraserTool } from './Eraser';
|
18
|
+
export { default as PasteHandler } from './PasteHandler';
|
18
19
|
|
@@ -13,6 +13,10 @@ export interface ToolLocalization {
|
|
13
13
|
textTool: string;
|
14
14
|
enterTextToInsert: string;
|
15
15
|
changeTool: string;
|
16
|
+
pasteHandler: string;
|
17
|
+
|
18
|
+
copied: (count: number, description: string) => string;
|
19
|
+
pasted: (count: number, description: string) => string;
|
16
20
|
|
17
21
|
toolEnabledAnnouncement: (toolName: string) => string;
|
18
22
|
toolDisabledAnnouncement: (toolName: string) => string;
|
@@ -32,6 +36,10 @@ export const defaultToolLocalization: ToolLocalization = {
|
|
32
36
|
textTool: 'Text',
|
33
37
|
enterTextToInsert: 'Text to insert',
|
34
38
|
changeTool: 'Change tool',
|
39
|
+
pasteHandler: 'Copy paste handler',
|
40
|
+
|
41
|
+
copied: (count: number, description: string) => `Copied ${count} ${description}`,
|
42
|
+
pasted: (count: number, description: string) => `Pasted ${count} ${description}`,
|
35
43
|
|
36
44
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
37
45
|
toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
|