js-draw 0.11.3 → 0.13.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 +15 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +13 -0
- package/dist/src/Color4.js +17 -0
- package/dist/src/Editor.d.ts +33 -18
- package/dist/src/Editor.js +26 -24
- package/dist/src/EditorImage.d.ts +12 -0
- package/dist/src/EditorImage.js +12 -0
- package/dist/src/Pointer.d.ts +1 -0
- package/dist/src/Pointer.js +8 -0
- package/dist/src/SVGLoader.d.ts +5 -0
- package/dist/src/SVGLoader.js +49 -36
- package/dist/src/Viewport.d.ts +30 -1
- package/dist/src/Viewport.js +39 -9
- package/dist/src/commands/invertCommand.js +1 -1
- package/dist/src/components/AbstractComponent.d.ts +20 -0
- package/dist/src/components/AbstractComponent.js +32 -2
- package/dist/src/lib.d.ts +6 -3
- package/dist/src/lib.js +4 -1
- package/dist/src/math/Mat33.d.ts +1 -1
- package/dist/src/math/Mat33.js +1 -1
- package/dist/src/rendering/Display.d.ts +9 -11
- package/dist/src/rendering/Display.js +12 -14
- package/dist/src/rendering/lib.d.ts +3 -0
- package/dist/src/rendering/lib.js +3 -0
- package/dist/src/rendering/renderers/DummyRenderer.js +2 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +4 -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 +2 -2
- package/dist/src/toolbar/IconProvider.js +123 -35
- 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/PanZoom.js +1 -1
- package/dist/src/tools/PasteHandler.d.ts +11 -4
- package/dist/src/tools/PasteHandler.js +12 -5
- package/dist/src/tools/Pen.d.ts +7 -2
- package/dist/src/tools/Pen.js +39 -6
- package/dist/src/tools/SelectionTool/Selection.d.ts +4 -1
- package/dist/src/tools/SelectionTool/Selection.js +64 -27
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +6 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -1
- package/dist/src/tools/SelectionTool/SelectionTool.js +56 -16
- package/dist/src/tools/TextTool.js +10 -6
- package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
- package/dist/src/tools/ToolSwitcherShortcut.js +9 -3
- package/dist/src/tools/UndoRedoShortcut.js +2 -4
- package/dist/src/types.d.ts +2 -2
- package/package.json +2 -2
- package/src/Color4.test.ts +11 -0
- package/src/Color4.ts +23 -0
- package/src/Editor.ts +39 -26
- package/src/EditorImage.ts +12 -0
- package/src/Pointer.ts +19 -0
- package/src/SVGLoader.ts +20 -15
- package/src/Viewport.ts +50 -11
- package/src/commands/invertCommand.ts +1 -1
- package/src/components/AbstractComponent.ts +52 -2
- package/src/lib.ts +6 -3
- package/src/math/Mat33.ts +1 -1
- package/src/rendering/Display.ts +12 -15
- package/src/rendering/RenderingStyle.ts +1 -1
- package/src/rendering/lib.ts +4 -0
- package/src/rendering/renderers/DummyRenderer.ts +2 -3
- package/src/rendering/renderers/SVGRenderer.ts +4 -0
- package/src/rendering/renderers/TextOnlyRenderer.ts +0 -1
- package/src/toolbar/HTMLToolbar.ts +81 -5
- package/src/toolbar/IconProvider.ts +132 -37
- 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/PanZoom.ts +1 -1
- package/src/tools/PasteHandler.ts +12 -6
- package/src/tools/Pen.test.ts +44 -1
- package/src/tools/Pen.ts +53 -8
- package/src/tools/SelectionTool/Selection.ts +73 -23
- package/src/tools/SelectionTool/SelectionHandle.ts +9 -0
- package/src/tools/SelectionTool/SelectionTool.test.ts +138 -21
- package/src/tools/SelectionTool/SelectionTool.ts +70 -16
- package/src/tools/TextTool.ts +14 -8
- package/src/tools/ToolSwitcherShortcut.ts +10 -5
- package/src/tools/UndoRedoShortcut.ts +2 -5
- package/src/types.ts +2 -2
- package/typedoc.json +2 -2
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, KeyPressEvent, PointerEvt, StrokeDataPoint } from '../types';
|
6
|
+
import { EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt, StrokeDataPoint } from '../types';
|
7
7
|
import BaseTool from './BaseTool';
|
8
8
|
import { ComponentBuilder, ComponentBuilderFactory } from '../components/builders/types';
|
9
9
|
|
@@ -15,6 +15,7 @@ export interface PenStyle {
|
|
15
15
|
export default class Pen extends BaseTool {
|
16
16
|
protected builder: ComponentBuilder|null = null;
|
17
17
|
private lastPoint: StrokeDataPoint|null = null;
|
18
|
+
private ctrlKeyPressed: boolean = false;
|
18
19
|
|
19
20
|
public constructor(
|
20
21
|
private editor: Editor,
|
@@ -31,6 +32,10 @@ export default class Pen extends BaseTool {
|
|
31
32
|
|
32
33
|
// Converts a `pointer` to a `StrokeDataPoint`.
|
33
34
|
protected toStrokePoint(pointer: Pointer): StrokeDataPoint {
|
35
|
+
if (this.isSnappingToGrid()) {
|
36
|
+
pointer = pointer.snappedToGrid(this.editor.viewport);
|
37
|
+
}
|
38
|
+
|
34
39
|
const minPressure = 0.3;
|
35
40
|
let pressure = Math.max(pointer.pressure ?? 1.0, minPressure);
|
36
41
|
|
@@ -42,8 +47,10 @@ export default class Pen extends BaseTool {
|
|
42
47
|
console.assert(isFinite(pointer.screenPos.length()), 'Non-finite screen position!');
|
43
48
|
console.assert(isFinite(pointer.timeStamp), 'Non-finite timeStamp on pointer!');
|
44
49
|
|
50
|
+
const pos = pointer.canvasPos;
|
51
|
+
|
45
52
|
return {
|
46
|
-
pos
|
53
|
+
pos,
|
47
54
|
width: pressure * this.getPressureMultiplier(),
|
48
55
|
color: this.style.color,
|
49
56
|
time: pointer.timeStamp,
|
@@ -86,6 +93,8 @@ export default class Pen extends BaseTool {
|
|
86
93
|
}
|
87
94
|
|
88
95
|
public onPointerMove({ current }: PointerEvt): void {
|
96
|
+
if (!this.builder) return;
|
97
|
+
|
89
98
|
this.addPointToStroke(this.toStrokePoint(current));
|
90
99
|
}
|
91
100
|
|
@@ -102,7 +111,18 @@ export default class Pen extends BaseTool {
|
|
102
111
|
};
|
103
112
|
|
104
113
|
this.addPointToStroke(strokePoint);
|
105
|
-
|
114
|
+
|
115
|
+
if (current.isPrimary) {
|
116
|
+
this.finalizeStroke();
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
public onGestureCancel() {
|
121
|
+
this.editor.clearWetInk();
|
122
|
+
}
|
123
|
+
|
124
|
+
private finalizeStroke() {
|
125
|
+
if (this.builder) {
|
106
126
|
const stroke = this.builder.build();
|
107
127
|
this.previewStroke();
|
108
128
|
|
@@ -118,10 +138,6 @@ export default class Pen extends BaseTool {
|
|
118
138
|
this.editor.clearWetInk();
|
119
139
|
}
|
120
140
|
|
121
|
-
public onGestureCancel() {
|
122
|
-
this.editor.clearWetInk();
|
123
|
-
}
|
124
|
-
|
125
141
|
private noteUpdated() {
|
126
142
|
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
127
143
|
kind: EditorEventType.ToolUpdated,
|
@@ -160,7 +176,15 @@ export default class Pen extends BaseTool {
|
|
160
176
|
public getColor() { return this.style.color; }
|
161
177
|
public getStrokeFactory() { return this.builderFactory; }
|
162
178
|
|
163
|
-
public
|
179
|
+
public setEnabled(enabled: boolean): void {
|
180
|
+
super.setEnabled(enabled);
|
181
|
+
|
182
|
+
this.ctrlKeyPressed = false;
|
183
|
+
}
|
184
|
+
|
185
|
+
private isSnappingToGrid() { return this.ctrlKeyPressed; }
|
186
|
+
|
187
|
+
public onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean {
|
164
188
|
key = key.toLowerCase();
|
165
189
|
|
166
190
|
let newThickness: number|undefined;
|
@@ -176,6 +200,27 @@ export default class Pen extends BaseTool {
|
|
176
200
|
return true;
|
177
201
|
}
|
178
202
|
|
203
|
+
if (key === 'control') {
|
204
|
+
this.ctrlKeyPressed = true;
|
205
|
+
return true;
|
206
|
+
}
|
207
|
+
|
208
|
+
// Ctrl+Z: End the stroke so that it can be undone/redone.
|
209
|
+
if (key === 'z' && ctrlKey && this.builder) {
|
210
|
+
this.finalizeStroke();
|
211
|
+
}
|
212
|
+
|
213
|
+
return false;
|
214
|
+
}
|
215
|
+
|
216
|
+
public onKeyUp({ key }: KeyUpEvent): boolean {
|
217
|
+
key = key.toLowerCase();
|
218
|
+
|
219
|
+
if (key === 'control') {
|
220
|
+
this.ctrlKeyPressed = false;
|
221
|
+
return true;
|
222
|
+
}
|
223
|
+
|
179
224
|
return false;
|
180
225
|
}
|
181
226
|
}
|
@@ -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();
|
@@ -18,6 +18,7 @@ export type DragEndCallback = ()=> void;
|
|
18
18
|
|
19
19
|
export default class SelectionHandle {
|
20
20
|
private element: HTMLElement;
|
21
|
+
private snapToGrid: boolean;
|
21
22
|
|
22
23
|
// Bounding box in screen coordinates.
|
23
24
|
|
@@ -96,4 +97,12 @@ export default class SelectionHandle {
|
|
96
97
|
}
|
97
98
|
this.onDragEnd();
|
98
99
|
}
|
100
|
+
|
101
|
+
public setSnapToGrid(snap: boolean) {
|
102
|
+
this.snapToGrid = snap;
|
103
|
+
}
|
104
|
+
|
105
|
+
public isSnappingToGrid() {
|
106
|
+
return this.snapToGrid;
|
107
|
+
}
|
99
108
|
}
|
@@ -5,8 +5,11 @@ import EditorImage from '../../EditorImage';
|
|
5
5
|
import Path from '../../math/Path';
|
6
6
|
import { Vec2 } from '../../math/Vec2';
|
7
7
|
import { InputEvtType } from '../../types';
|
8
|
+
import Selection from './Selection';
|
8
9
|
import SelectionTool from './SelectionTool';
|
9
10
|
import createEditor from '../../testing/createEditor';
|
11
|
+
import Pointer from '../../Pointer';
|
12
|
+
import { Rect2 } from '../../lib';
|
10
13
|
|
11
14
|
const getSelectionTool = (editor: Editor): SelectionTool => {
|
12
15
|
return editor.toolController.getMatchingTools(SelectionTool)[0];
|
@@ -22,6 +25,33 @@ const createSquareStroke = (size: number = 1) => {
|
|
22
25
|
return { testStroke, addTestStrokeCommand };
|
23
26
|
};
|
24
27
|
|
28
|
+
const createEditorWithSingleObjectSelection = (objectSize: number = 50) => {
|
29
|
+
const { testStroke, addTestStrokeCommand } = createSquareStroke(objectSize);
|
30
|
+
const editor = createEditor();
|
31
|
+
editor.dispatch(addTestStrokeCommand);
|
32
|
+
|
33
|
+
// Select the object
|
34
|
+
const selectionTool = getSelectionTool(editor);
|
35
|
+
selectionTool.setEnabled(true);
|
36
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
37
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
|
38
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(5, 5));
|
39
|
+
|
40
|
+
return { editor, testStroke, selectionTool };
|
41
|
+
};
|
42
|
+
|
43
|
+
const dragSelection = (editor: Editor, selection: Selection, startPt: Vec2, endPt: Vec2) => {
|
44
|
+
const backgroundElem = selection.getBackgroundElem();
|
45
|
+
|
46
|
+
selection.onDragStart(Pointer.ofCanvasPoint(startPt, true, editor.viewport), backgroundElem);
|
47
|
+
jest.advanceTimersByTime(100);
|
48
|
+
|
49
|
+
selection.onDragUpdate(Pointer.ofCanvasPoint(endPt, true, editor.viewport));
|
50
|
+
jest.advanceTimersByTime(100);
|
51
|
+
|
52
|
+
selection.onDragEnd();
|
53
|
+
};
|
54
|
+
|
25
55
|
describe('SelectionTool', () => {
|
26
56
|
it('selection should shrink/grow to bounding box of selected objects', () => {
|
27
57
|
const { addTestStrokeCommand } = createSquareStroke();
|
@@ -47,17 +77,7 @@ describe('SelectionTool', () => {
|
|
47
77
|
});
|
48
78
|
|
49
79
|
it('sending keyboard events to the selected region should move selected items', () => {
|
50
|
-
const {
|
51
|
-
const editor = createEditor();
|
52
|
-
editor.dispatch(addTestStrokeCommand);
|
53
|
-
|
54
|
-
// Select the object
|
55
|
-
const selectionTool = getSelectionTool(editor);
|
56
|
-
selectionTool.setEnabled(true);
|
57
|
-
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
58
|
-
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
|
59
|
-
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(5, 5));
|
60
|
-
|
80
|
+
const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(50);
|
61
81
|
const selection = selectionTool.getSelection();
|
62
82
|
expect(selection).not.toBeNull();
|
63
83
|
|
@@ -78,16 +98,7 @@ describe('SelectionTool', () => {
|
|
78
98
|
});
|
79
99
|
|
80
100
|
it('moving the selection with a keyboard should move the view to keep the selection in view', () => {
|
81
|
-
const {
|
82
|
-
const editor = createEditor();
|
83
|
-
editor.dispatch(addTestStrokeCommand);
|
84
|
-
|
85
|
-
// Select the stroke
|
86
|
-
const selectionTool = getSelectionTool(editor);
|
87
|
-
selectionTool.setEnabled(true);
|
88
|
-
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
89
|
-
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
|
90
|
-
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
|
101
|
+
const { editor, selectionTool } = createEditorWithSingleObjectSelection(50);
|
91
102
|
|
92
103
|
const selection = selectionTool.getSelection();
|
93
104
|
if (selection === null) {
|
@@ -140,4 +151,110 @@ describe('SelectionTool', () => {
|
|
140
151
|
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(201, 201));
|
141
152
|
expect(selectionTool.getSelectedObjects()).toHaveLength(0);
|
142
153
|
});
|
154
|
+
|
155
|
+
it('should remove the selection from the document while dragging', () => {
|
156
|
+
const { editor, selectionTool } = createEditorWithSingleObjectSelection(50);
|
157
|
+
|
158
|
+
const selection = selectionTool.getSelection()!;
|
159
|
+
const backgroundElem = selection.getBackgroundElem();
|
160
|
+
selection.onDragStart(Pointer.ofCanvasPoint(Vec2.of(0, 0), true, editor.viewport), backgroundElem);
|
161
|
+
jest.advanceTimersByTime(100);
|
162
|
+
selection.onDragUpdate(Pointer.ofCanvasPoint(Vec2.of(20, 0), true, editor.viewport));
|
163
|
+
jest.advanceTimersByTime(100);
|
164
|
+
|
165
|
+
// Expect the selection to not be in the image while dragging
|
166
|
+
expect(editor.image.getAllElements()).toHaveLength(0);
|
167
|
+
|
168
|
+
selection.onDragEnd();
|
169
|
+
|
170
|
+
expect(editor.image.getAllElements()).toHaveLength(1);
|
171
|
+
});
|
172
|
+
|
173
|
+
it('should drag objects horizontally', () => {
|
174
|
+
const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(50);
|
175
|
+
|
176
|
+
expect(editor.image.findParent(testStroke)).not.toBeNull();
|
177
|
+
expect(testStroke.getBBox().topLeft).objEq(Vec2.of(0, 0));
|
178
|
+
|
179
|
+
const selection = selectionTool.getSelection()!;
|
180
|
+
dragSelection(editor, selection, Vec2.of(0, 0), Vec2.of(10, 0));
|
181
|
+
|
182
|
+
expect(editor.image.findParent(testStroke)).not.toBeNull();
|
183
|
+
expect(testStroke.getBBox().topLeft).objEq(Vec2.of(10, 0));
|
184
|
+
});
|
185
|
+
|
186
|
+
it('should round changes in objects positions when dragging', () => {
|
187
|
+
const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(50);
|
188
|
+
|
189
|
+
expect(editor.image.findParent(testStroke)).not.toBeNull();
|
190
|
+
expect(testStroke.getBBox().topLeft).objEq(Vec2.of(0, 0));
|
191
|
+
|
192
|
+
const selection = selectionTool.getSelection()!;
|
193
|
+
dragSelection(editor, selection, Vec2.of(0, 0), Vec2.of(9.999, 0));
|
194
|
+
|
195
|
+
expect(editor.image.findParent(testStroke)).not.toBeNull();
|
196
|
+
expect(testStroke.getBBox().topLeft).objEq(Vec2.of(10, 0));
|
197
|
+
});
|
198
|
+
|
199
|
+
it('dragCancel should return a selection to its original position', () => {
|
200
|
+
const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(150);
|
201
|
+
|
202
|
+
const selection = selectionTool.getSelection()!;
|
203
|
+
const dragBackground = selection.getBackgroundElem();
|
204
|
+
|
205
|
+
expect(testStroke.getBBox().topLeft).objEq(Vec2.zero);
|
206
|
+
|
207
|
+
selection.onDragStart(Pointer.ofCanvasPoint(Vec2.of(10, 0), true, editor.viewport), dragBackground);
|
208
|
+
jest.advanceTimersByTime(100);
|
209
|
+
selection.onDragUpdate(Pointer.ofCanvasPoint(Vec2.of(200, 10), true, editor.viewport));
|
210
|
+
jest.advanceTimersByTime(100);
|
211
|
+
selection.onDragCancel();
|
212
|
+
|
213
|
+
expect(testStroke.getBBox().topLeft).objEq(Vec2.zero);
|
214
|
+
expect(editor.image.findParent(testStroke)).not.toBeNull();
|
215
|
+
});
|
216
|
+
|
217
|
+
it('duplicateSelectedObjects should duplicate a selection while dragging', async () => {
|
218
|
+
const { editor, selectionTool, testStroke } = createEditorWithSingleObjectSelection(150);
|
219
|
+
|
220
|
+
const selection = selectionTool.getSelection()!;
|
221
|
+
const dragBackground = selection.getBackgroundElem();
|
222
|
+
|
223
|
+
selection.onDragStart(Pointer.ofCanvasPoint(Vec2.of(0, 0), true, editor.viewport), dragBackground);
|
224
|
+
jest.advanceTimersByTime(100);
|
225
|
+
selection.onDragUpdate(Pointer.ofCanvasPoint(Vec2.of(20, 0), true, editor.viewport));
|
226
|
+
|
227
|
+
// The selection should not be in the document while dragging
|
228
|
+
expect(editor.image.findParent(testStroke)).toBeNull();
|
229
|
+
|
230
|
+
await editor.dispatch(await selection.duplicateSelectedObjects());
|
231
|
+
jest.advanceTimersByTime(100);
|
232
|
+
|
233
|
+
// The duplicate stroke should be added to the document, but the original should not.
|
234
|
+
expect(editor.image.findParent(testStroke)).toBeNull();
|
235
|
+
|
236
|
+
const allObjectsInImage = editor.image.getAllElements();
|
237
|
+
expect(allObjectsInImage).toHaveLength(1);
|
238
|
+
|
239
|
+
const duplicateObject = allObjectsInImage[0];
|
240
|
+
|
241
|
+
// The duplicate stroke should be translated
|
242
|
+
expect(duplicateObject.getBBox()).objEq(new Rect2(20, 0, 150, 150));
|
243
|
+
|
244
|
+
// The duplicate stroke should be selected.
|
245
|
+
expect(selection.getSelectedObjects()).toHaveLength(1);
|
246
|
+
|
247
|
+
// The test stroke should not be added to the document
|
248
|
+
// (esp if we continue dragging)
|
249
|
+
selection.onDragUpdate(Pointer.ofCanvasPoint(Vec2.of(30, 10), true, editor.viewport));
|
250
|
+
jest.advanceTimersByTime(100);
|
251
|
+
|
252
|
+
expect(editor.image.findParent(testStroke)).toBeNull();
|
253
|
+
|
254
|
+
// The test stroke should be translated when we finish dragging.
|
255
|
+
selection.onDragEnd();
|
256
|
+
|
257
|
+
expect(editor.image.findParent(testStroke)).not.toBeNull();
|
258
|
+
expect(testStroke.getBBox()).objEq(new Rect2(30, 10, 150, 150));
|
259
|
+
});
|
143
260
|
});
|
@@ -1,7 +1,3 @@
|
|
1
|
-
// Allows users to select/transform portions of the `EditorImage`.
|
2
|
-
// With respect to `extend`ing, `SelectionTool` is not stable.
|
3
|
-
// @packageDocumentation
|
4
|
-
|
5
1
|
import AbstractComponent from '../../components/AbstractComponent';
|
6
2
|
import Editor from '../../Editor';
|
7
3
|
import Mat33 from '../../math/Mat33';
|
@@ -16,7 +12,8 @@ import TextComponent from '../../components/TextComponent';
|
|
16
12
|
|
17
13
|
export const cssPrefix = 'selection-tool-';
|
18
14
|
|
19
|
-
//
|
15
|
+
// Allows users to select/transform portions of the `EditorImage`.
|
16
|
+
// With respect to `extend`ing, `SelectionTool` is not stable.
|
20
17
|
export default class SelectionTool extends BaseTool {
|
21
18
|
private handleOverlay: HTMLElement;
|
22
19
|
private prevSelectionBox: Selection|null;
|
@@ -25,6 +22,7 @@ export default class SelectionTool extends BaseTool {
|
|
25
22
|
|
26
23
|
private expandingSelectionBox: boolean = false;
|
27
24
|
private shiftKeyPressed: boolean = false;
|
25
|
+
private ctrlKeyPressed: boolean = false;
|
28
26
|
|
29
27
|
public constructor(private editor: Editor, description: string) {
|
30
28
|
super(editor.notifier, description);
|
@@ -61,17 +59,47 @@ export default class SelectionTool extends BaseTool {
|
|
61
59
|
this.selectionBox.addTo(this.handleOverlay);
|
62
60
|
}
|
63
61
|
|
62
|
+
private snapSelectionToGrid() {
|
63
|
+
if (!this.selectionBox) throw new Error('No selection to snap!');
|
64
|
+
|
65
|
+
const topLeftOfBBox = this.selectionBox.region.topLeft;
|
66
|
+
const snapDistance =
|
67
|
+
this.editor.viewport.snapToGrid(topLeftOfBBox).minus(topLeftOfBBox);
|
68
|
+
|
69
|
+
const oldTransform = this.selectionBox.getTransform();
|
70
|
+
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDistance)));
|
71
|
+
this.selectionBox.finalizeTransform();
|
72
|
+
}
|
73
|
+
|
64
74
|
private selectionBoxHandlingEvt: boolean = false;
|
65
|
-
public onPointerDown(
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
75
|
+
public onPointerDown({ allPointers, current }: PointerEvt): boolean {
|
76
|
+
const snapToGrid = this.ctrlKeyPressed;
|
77
|
+
if (snapToGrid) {
|
78
|
+
current = current.snappedToGrid(this.editor.viewport);
|
79
|
+
}
|
80
|
+
|
81
|
+
if (allPointers.length === 1 && current.isPrimary) {
|
82
|
+
let transforming = false;
|
83
|
+
|
84
|
+
if (this.lastEvtTarget && this.selectionBox) {
|
85
|
+
if (snapToGrid) {
|
86
|
+
this.snapSelectionToGrid();
|
87
|
+
}
|
88
|
+
|
89
|
+
const dragStartResult = this.selectionBox.onDragStart(current, this.lastEvtTarget);
|
90
|
+
|
91
|
+
if (dragStartResult) {
|
92
|
+
transforming = true;
|
93
|
+
|
94
|
+
this.selectionBoxHandlingEvt = true;
|
95
|
+
this.expandingSelectionBox = false;
|
96
|
+
}
|
70
97
|
}
|
71
|
-
|
98
|
+
|
99
|
+
if (!transforming) {
|
72
100
|
// Shift key: Combine the new and old selection boxes at the end of the gesture.
|
73
101
|
this.expandingSelectionBox = this.shiftKeyPressed;
|
74
|
-
this.makeSelectionBox(
|
102
|
+
this.makeSelectionBox(current.canvasPos);
|
75
103
|
}
|
76
104
|
|
77
105
|
return true;
|
@@ -82,10 +110,15 @@ export default class SelectionTool extends BaseTool {
|
|
82
110
|
public onPointerMove(event: PointerEvt): void {
|
83
111
|
if (!this.selectionBox) return;
|
84
112
|
|
113
|
+
let currentPointer = event.current;
|
114
|
+
if (this.ctrlKeyPressed) {
|
115
|
+
currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
|
116
|
+
}
|
117
|
+
|
85
118
|
if (this.selectionBoxHandlingEvt) {
|
86
|
-
this.selectionBox.onDragUpdate(
|
119
|
+
this.selectionBox.onDragUpdate(currentPointer);
|
87
120
|
} else {
|
88
|
-
this.selectionBox!.setToPoint(
|
121
|
+
this.selectionBox!.setToPoint(currentPointer.canvasPos);
|
89
122
|
}
|
90
123
|
}
|
91
124
|
|
@@ -137,7 +170,12 @@ export default class SelectionTool extends BaseTool {
|
|
137
170
|
public onPointerUp(event: PointerEvt): void {
|
138
171
|
if (!this.selectionBox) return;
|
139
172
|
|
140
|
-
|
173
|
+
let currentPointer = event.current;
|
174
|
+
if (this.ctrlKeyPressed) {
|
175
|
+
currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
|
176
|
+
}
|
177
|
+
|
178
|
+
this.selectionBox.setToPoint(currentPointer.canvasPos);
|
141
179
|
|
142
180
|
// Were we expanding the previous selection?
|
143
181
|
if (this.expandingSelectionBox && this.prevSelectionBox) {
|
@@ -173,8 +211,14 @@ export default class SelectionTool extends BaseTool {
|
|
173
211
|
'e', 'j', 'ArrowDown',
|
174
212
|
'r', 'R',
|
175
213
|
'i', 'I', 'o', 'O',
|
214
|
+
'Control',
|
176
215
|
];
|
177
216
|
public onKeyPress(event: KeyPressEvent): boolean {
|
217
|
+
if (event.key === 'Control') {
|
218
|
+
this.ctrlKeyPressed = true;
|
219
|
+
return true;
|
220
|
+
}
|
221
|
+
|
178
222
|
if (this.selectionBox && event.ctrlKey && event.key === 'd') {
|
179
223
|
// Handle duplication on key up — we don't want to accidentally duplicate
|
180
224
|
// many times.
|
@@ -290,13 +334,20 @@ export default class SelectionTool extends BaseTool {
|
|
290
334
|
}
|
291
335
|
|
292
336
|
public onKeyUp(evt: KeyUpEvent) {
|
337
|
+
if (evt.key === 'Control') {
|
338
|
+
this.ctrlKeyPressed = false;
|
339
|
+
return true;
|
340
|
+
}
|
341
|
+
|
293
342
|
if (evt.key === 'Shift') {
|
294
343
|
this.shiftKeyPressed = false;
|
295
344
|
return true;
|
296
345
|
}
|
297
346
|
else if (evt.ctrlKey) {
|
298
347
|
if (this.selectionBox && evt.key === 'd') {
|
299
|
-
this.
|
348
|
+
this.selectionBox.duplicateSelectedObjects().then(command => {
|
349
|
+
this.editor.dispatch(command);
|
350
|
+
});
|
300
351
|
return true;
|
301
352
|
}
|
302
353
|
else if (evt.key === 'a') {
|
@@ -356,6 +407,9 @@ export default class SelectionTool extends BaseTool {
|
|
356
407
|
this.handleOverlay.replaceChildren();
|
357
408
|
this.selectionBox = null;
|
358
409
|
|
410
|
+
this.shiftKeyPressed = false;
|
411
|
+
this.ctrlKeyPressed = false;
|
412
|
+
|
359
413
|
this.handleOverlay.style.display = enabled ? 'block' : 'none';
|
360
414
|
|
361
415
|
if (enabled) {
|