js-draw 0.12.0 → 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 +9 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +12 -0
- package/dist/src/Color4.js +16 -0
- package/dist/src/Editor.d.ts +33 -18
- package/dist/src/Editor.js +22 -19
- 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 +6 -1
- 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 +19 -0
- package/dist/src/components/AbstractComponent.js +17 -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/IconProvider.d.ts +1 -1
- package/dist/src/toolbar/IconProvider.js +90 -29
- 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/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 +53 -15
- 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/package.json +2 -2
- package/src/Color4.test.ts +11 -0
- package/src/Color4.ts +22 -0
- package/src/Editor.ts +36 -22
- package/src/EditorImage.ts +12 -0
- package/src/Pointer.ts +19 -0
- package/src/SVGLoader.ts +6 -1
- package/src/Viewport.ts +50 -11
- package/src/commands/invertCommand.ts +1 -1
- package/src/components/AbstractComponent.ts +33 -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 +1 -1
- package/src/toolbar/IconProvider.ts +98 -31
- 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/SelectionHandle.ts +9 -0
- package/src/tools/SelectionTool/SelectionTool.ts +67 -15
- package/src/tools/ToolSwitcherShortcut.ts +10 -5
- package/src/tools/UndoRedoShortcut.ts +2 -5
- package/typedoc.json +2 -2
@@ -43,7 +43,7 @@ export default class IconProvider {
|
|
43
43
|
return this.makeRedoIcon(true);
|
44
44
|
}
|
45
45
|
|
46
|
-
// @param mirror - reflect across the x-axis
|
46
|
+
// @param mirror - reflect across the x-axis. This parameter is internal.
|
47
47
|
// @returns a redo icon.
|
48
48
|
public makeRedoIcon(mirror: boolean = false): IconType {
|
49
49
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
@@ -383,52 +383,119 @@ export default class IconProvider {
|
|
383
383
|
return icon;
|
384
384
|
}
|
385
385
|
|
386
|
-
public makePenIcon(
|
386
|
+
public makePenIcon(strokeSize: number, color: string|Color4, rounded?: boolean): IconType {
|
387
387
|
if (color instanceof Color4) {
|
388
388
|
color = color.toHexString();
|
389
389
|
}
|
390
390
|
|
391
391
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
392
392
|
icon.setAttribute('viewBox', '0 0 100 100');
|
393
|
-
|
394
|
-
const halfThickness = tipThickness / 2;
|
395
|
-
|
396
|
-
// Draw a pen-like shape
|
397
|
-
const penTipLeft = 50 - halfThickness;
|
398
|
-
const penTipRight = 50 + halfThickness;
|
393
|
+
const tipThickness = strokeSize / 2;
|
399
394
|
|
400
|
-
|
401
|
-
|
395
|
+
const inkTipPath = `
|
396
|
+
M ${15 - tipThickness},${80 - tipThickness}
|
397
|
+
${15 - tipThickness},${80 + tipThickness}
|
398
|
+
30,83
|
399
|
+
15,65
|
400
|
+
Z
|
401
|
+
`;
|
402
|
+
const trailStartEndY = 80 + tipThickness;
|
403
|
+
const inkTrailPath = `
|
404
|
+
m ${15 - tipThickness * 1.1},${trailStartEndY}
|
405
|
+
c 35,10 55,15 60,30
|
406
|
+
l ${35 + tipThickness * 1.2},${-10 - tipThickness}
|
407
|
+
C 80.47,98.32 50.5,${90 + tipThickness} 20,${trailStartEndY} Z
|
408
|
+
`;
|
402
409
|
|
403
|
-
|
404
|
-
|
405
|
-
|
410
|
+
const colorBubblePath = `
|
411
|
+
M 72.45,35.67
|
412
|
+
A 10,15 41.8 0 1 55,40.2 10,15 41.8 0 1 57.55,22.3 10,15 41.8 0 1 75,17.8 10,15 41.8 0 1 72.5,35.67
|
413
|
+
Z
|
414
|
+
`;
|
415
|
+
|
416
|
+
let gripMainPath = 'M 85,-25 25,35 h 10 v 10 h 10 v 10 h 10 v 10 h 10 l -5,10 60,-60 z';
|
417
|
+
let gripShadow1Path = 'M 25,35 H 35 L 90,-15 85,-25 Z';
|
418
|
+
let gripShadow2Path = 'M 60,75 65,65 H 55 l 55,-55 10,5 z';
|
419
|
+
|
420
|
+
if (rounded) {
|
421
|
+
gripMainPath = 'M 85,-25 25,35 c 15,0 40,30 35,40 l 60,-60 z';
|
422
|
+
gripShadow1Path = 'm 25,35 c 3.92361,0.384473 7.644275,0.980572 10,3 l 55,-53 -5,-10 z';
|
423
|
+
gripShadow2Path = 'M 60,75 C 61,66 59,65 56,59 l 54,-54 10,10 z';
|
406
424
|
}
|
407
425
|
|
408
|
-
const
|
409
|
-
const backgroundStrokeTipPath = `M14,63 ${tipCenterBackgroundPath} L88,60 Z`;
|
426
|
+
const penTipPath = `M 25,35 ${10 - tipThickness / 4},${70 - tipThickness / 2} 20,75 25,85 60,75 70,55 45,25 Z`;
|
410
427
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
428
|
+
const pencilTipColor = Color4.fromHex('#f4d7d7');
|
429
|
+
const tipColor = pencilTipColor.mix(
|
430
|
+
Color4.fromString(color), tipThickness / 40 - 0.1
|
431
|
+
).toHexString();
|
432
|
+
|
433
|
+
const ink = `
|
434
|
+
<path
|
435
|
+
fill="${checkerboardPatternRef}"
|
436
|
+
d="${inkTipPath}"
|
437
|
+
/>
|
438
|
+
<path
|
439
|
+
fill="${checkerboardPatternRef}"
|
440
|
+
d="${inkTrailPath}"
|
441
|
+
/>
|
442
|
+
<path
|
443
|
+
fill="${color}"
|
444
|
+
d="${inkTipPath}"
|
445
|
+
/>
|
446
|
+
<path
|
447
|
+
fill="${color}"
|
448
|
+
d="${inkTrailPath}"
|
449
|
+
/>
|
450
|
+
`;
|
451
|
+
|
452
|
+
const penTip = `
|
453
|
+
<path
|
454
|
+
fill="${checkerboardPatternRef}"
|
455
|
+
d="${penTipPath}"
|
456
|
+
/>
|
457
|
+
<path
|
458
|
+
fill="${tipColor}"
|
459
|
+
stroke="${color}"
|
460
|
+
d="${penTipPath}"
|
461
|
+
/>
|
462
|
+
`;
|
463
|
+
|
464
|
+
const grip = `
|
417
465
|
<path
|
418
|
-
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
|
419
466
|
${iconColorStrokeFill}
|
467
|
+
d="${gripMainPath}"
|
468
|
+
/>
|
469
|
+
|
470
|
+
<!-- shadows -->
|
471
|
+
<path
|
472
|
+
fill="rgba(150, 150, 150, 0.3)"
|
473
|
+
d="${gripShadow1Path}"
|
420
474
|
/>
|
421
|
-
</g>
|
422
|
-
<g>
|
423
|
-
<!-- Checkerboard background for slightly transparent pens -->
|
424
|
-
<path d='${backgroundStrokeTipPath}' fill='${checkerboardPatternRef}'/>
|
425
|
-
|
426
|
-
<!-- Actual pen tip -->
|
427
475
|
<path
|
428
|
-
|
429
|
-
|
430
|
-
stroke='${color}'
|
476
|
+
fill="rgba(100, 100, 100, 0.2)"
|
477
|
+
d="${gripShadow2Path}"
|
431
478
|
/>
|
479
|
+
|
480
|
+
<!-- color bubble -->
|
481
|
+
<path
|
482
|
+
fill="${checkerboardPatternRef}"
|
483
|
+
d="${colorBubblePath}"
|
484
|
+
/>
|
485
|
+
<path
|
486
|
+
fill="${color}"
|
487
|
+
d="${colorBubblePath}"
|
488
|
+
/>
|
489
|
+
`;
|
490
|
+
|
491
|
+
icon.innerHTML = `
|
492
|
+
<defs>
|
493
|
+
${checkerboardPatternDef}
|
494
|
+
</defs>
|
495
|
+
<g>
|
496
|
+
${ink}
|
497
|
+
${penTip}
|
498
|
+
${grip}
|
432
499
|
</g>
|
433
500
|
`;
|
434
501
|
return icon;
|
package/src/tools/PanZoom.ts
CHANGED
@@ -338,7 +338,7 @@ export default class PanZoom extends BaseTool {
|
|
338
338
|
toCanvas.transformVec3(
|
339
339
|
Vec3.of(-delta.x, -delta.y, 0)
|
340
340
|
);
|
341
|
-
const pinchZoomScaleFactor = 1.
|
341
|
+
const pinchZoomScaleFactor = 1.03;
|
342
342
|
const transformUpdate = Mat33.scaling2D(
|
343
343
|
Math.max(0.25, Math.min(Math.pow(pinchZoomScaleFactor, -delta.z), 4)), canvasPos
|
344
344
|
).rightMul(
|
@@ -1,8 +1,3 @@
|
|
1
|
-
/**
|
2
|
-
* A tool that handles paste events.
|
3
|
-
* @packageDocumentation
|
4
|
-
*/
|
5
|
-
|
6
1
|
import Editor from '../Editor';
|
7
2
|
import { AbstractComponent, TextComponent } from '../components/lib';
|
8
3
|
import SVGLoader from '../SVGLoader';
|
@@ -14,12 +9,23 @@ import Color4 from '../Color4';
|
|
14
9
|
import { TextStyle } from '../components/TextComponent';
|
15
10
|
import ImageComponent from '../components/ImageComponent';
|
16
11
|
|
17
|
-
|
12
|
+
/**
|
13
|
+
* A tool that handles paste events (e.g. as triggered by ctrl+V).
|
14
|
+
*
|
15
|
+
* @example
|
16
|
+
* While `ToolController` has a `PasteHandler` in its default list of tools,
|
17
|
+
* if a non-default set is being used, `PasteHandler` can be added as follows:
|
18
|
+
* ```ts
|
19
|
+
* const toolController = editor.toolController;
|
20
|
+
* toolController.addTool(new PasteHandler(editor));
|
21
|
+
* ```
|
22
|
+
*/
|
18
23
|
export default class PasteHandler extends BaseTool {
|
19
24
|
public constructor(private editor: Editor) {
|
20
25
|
super(editor.notifier, editor.localization.pasteHandler);
|
21
26
|
}
|
22
27
|
|
28
|
+
// @internal
|
23
29
|
public onPaste(event: PasteEvent): boolean {
|
24
30
|
const mime = event.mime.toLowerCase();
|
25
31
|
|
package/src/tools/Pen.test.ts
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
|
2
|
-
import
|
2
|
+
import PenTool from './Pen';
|
3
3
|
import { Vec2 } from '../math/Vec2';
|
4
4
|
import createEditor from '../testing/createEditor';
|
5
5
|
import { InputEvtType } from '../types';
|
6
|
+
import Rect2 from '../math/Rect2';
|
7
|
+
import StrokeComponent from '../components/Stroke';
|
8
|
+
import Mat33 from '../math/Mat33';
|
9
|
+
import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
|
6
10
|
|
7
11
|
describe('Pen', () => {
|
8
12
|
it('should draw horizontal lines', () => {
|
@@ -147,4 +151,43 @@ describe('Pen', () => {
|
|
147
151
|
expect(elems[0].getBBox().topLeft).objEq(Vec2.of(420, 24), 32); // ± 32
|
148
152
|
expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(420, 340), 25); // ± 25
|
149
153
|
});
|
154
|
+
|
155
|
+
it('ctrl+z should finalize then undo the current stroke', async () => {
|
156
|
+
const editor = createEditor();
|
157
|
+
|
158
|
+
expect(editor.history.undoStackSize).toBe(0);
|
159
|
+
|
160
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(10, 10));
|
161
|
+
jest.advanceTimersByTime(100);
|
162
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(20, 10));
|
163
|
+
|
164
|
+
const ctrlKeyDown = true;
|
165
|
+
editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'z', ctrlKeyDown);
|
166
|
+
|
167
|
+
// Stroke should have been undone
|
168
|
+
expect(editor.history.redoStackSize).toBe(1);
|
169
|
+
|
170
|
+
// Lifting the pointer up shouldn't clear the redo stack.
|
171
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(420, 340));
|
172
|
+
expect(editor.history.redoStackSize).toBe(1);
|
173
|
+
});
|
174
|
+
|
175
|
+
it('holding ctrl should snap the stroke to grid', () => {
|
176
|
+
const editor = createEditor();
|
177
|
+
editor.viewport.resetTransform(Mat33.identity);
|
178
|
+
|
179
|
+
const penTool = editor.toolController.getMatchingTools(PenTool)[0];
|
180
|
+
penTool.setStrokeFactory(makeFreehandLineBuilder);
|
181
|
+
|
182
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0.1, 0.1));
|
183
|
+
jest.advanceTimersByTime(100);
|
184
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10.1, 10.1));
|
185
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(10.1, 10.1));
|
186
|
+
|
187
|
+
const allElems = editor.image.getAllElements();
|
188
|
+
expect(allElems).toHaveLength(1);
|
189
|
+
|
190
|
+
const firstStroke = allElems[0] as StrokeComponent;
|
191
|
+
expect(firstStroke.getPath().bbox).objEq(new Rect2(0, 0, 10, 10));
|
192
|
+
});
|
150
193
|
});
|
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
|
}
|
@@ -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
|
}
|
@@ -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,6 +334,11 @@ 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;
|
@@ -358,6 +407,9 @@ export default class SelectionTool extends BaseTool {
|
|
358
407
|
this.handleOverlay.replaceChildren();
|
359
408
|
this.selectionBox = null;
|
360
409
|
|
410
|
+
this.shiftKeyPressed = false;
|
411
|
+
this.ctrlKeyPressed = false;
|
412
|
+
|
361
413
|
this.handleOverlay.style.display = enabled ? 'block' : 'none';
|
362
414
|
|
363
415
|
if (enabled) {
|
@@ -1,16 +1,21 @@
|
|
1
|
-
// Handles ctrl+1, ctrl+2, ctrl+3, ..., shortcuts for switching tools.
|
2
|
-
// @packageDocumentation
|
3
|
-
|
4
1
|
import Editor from '../Editor';
|
5
2
|
import { KeyPressEvent } from '../types';
|
6
3
|
import BaseTool from './BaseTool';
|
7
4
|
|
8
|
-
|
5
|
+
/**
|
6
|
+
* Handles keyboard events used, by default, to select tools. By default,
|
7
|
+
* 1 maps to the first primary tool, 2 to the second primary tool, ... .
|
8
|
+
*
|
9
|
+
* This is in the default set of {@link ToolController} tools.
|
10
|
+
*
|
11
|
+
* @deprecated This may be replaced in the future.
|
12
|
+
*/
|
9
13
|
export default class ToolSwitcherShortcut extends BaseTool {
|
10
14
|
public constructor(private editor: Editor) {
|
11
15
|
super(editor.notifier, editor.localization.changeTool);
|
12
16
|
}
|
13
|
-
|
17
|
+
|
18
|
+
// @internal
|
14
19
|
public onKeyPress({ key }: KeyPressEvent): boolean {
|
15
20
|
const toolController = this.editor.toolController;
|
16
21
|
const primaryTools = toolController.getPrimaryTools();
|
@@ -1,17 +1,14 @@
|
|
1
|
-
// Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts.
|
2
|
-
// @packageDocumentation
|
3
|
-
|
4
1
|
import Editor from '../Editor';
|
5
2
|
import { KeyPressEvent } from '../types';
|
6
3
|
import BaseTool from './BaseTool';
|
7
4
|
|
8
|
-
//
|
5
|
+
// Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts.
|
9
6
|
export default class UndoRedoShortcut extends BaseTool {
|
10
7
|
public constructor(private editor: Editor) {
|
11
8
|
super(editor.notifier, editor.localization.undoRedoTool);
|
12
9
|
}
|
13
10
|
|
14
|
-
//
|
11
|
+
// @internal
|
15
12
|
public onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean {
|
16
13
|
if (ctrlKey) {
|
17
14
|
if (key === 'z') {
|