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
@@ -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');
|
@@ -82,20 +82,48 @@ export default class IconProvider {
|
|
82
82
|
return icon;
|
83
83
|
}
|
84
84
|
|
85
|
-
public makeEraserIcon(): IconType {
|
85
|
+
public makeEraserIcon(eraserSize?: number): IconType {
|
86
86
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
87
|
+
eraserSize ??= 10;
|
88
|
+
|
89
|
+
const scaledSize = eraserSize / 4;
|
90
|
+
const eraserColor = '#ff70af';
|
87
91
|
|
88
|
-
// Draw an eraser-like shape
|
92
|
+
// Draw an eraser-like shape. Created with Inkscape
|
89
93
|
icon.innerHTML = `
|
90
94
|
<g>
|
91
|
-
<
|
95
|
+
<path
|
96
|
+
style="fill:${eraserColor}"
|
97
|
+
stroke="black"
|
98
|
+
transform="rotate(41.35)"
|
99
|
+
d="M 52.5 27
|
100
|
+
C 50 28.9 48.9 31.7 48.9 34.8
|
101
|
+
L 48.9 39.8
|
102
|
+
C 48.9 45.3 53.4 49.8 58.9 49.8
|
103
|
+
L 103.9 49.8
|
104
|
+
C 105.8 49.8 107.6 49.2 109.1 48.3
|
105
|
+
L 110.2 ${scaledSize + 49.5} L 159.7 ${scaledSize + 5}
|
106
|
+
L 157.7 ${-scaledSize + 5.2} L 112.4 ${49.5 - scaledSize}
|
107
|
+
C 113.4 43.5 113.9 41.7 113.9 39.8
|
108
|
+
L 113.9 34.8
|
109
|
+
C 113.9 29.3 109.4 24.8 103.9 24.8
|
110
|
+
L 58.9 24.8
|
111
|
+
C 56.5 24.8 54.3 25.7 52.5 27
|
112
|
+
z "
|
113
|
+
id="path438" />
|
114
|
+
|
92
115
|
<rect
|
93
|
-
|
116
|
+
stroke="#cc8077"
|
94
117
|
${iconColorFill}
|
95
|
-
|
118
|
+
id="rect218"
|
119
|
+
width="65"
|
120
|
+
height="75"
|
121
|
+
x="48.9"
|
122
|
+
y="-38.7"
|
123
|
+
transform="rotate(41.35)" />
|
96
124
|
</g>
|
97
125
|
`;
|
98
|
-
icon.setAttribute('viewBox', '0 0
|
126
|
+
icon.setAttribute('viewBox', '0 0 120 120');
|
99
127
|
return icon;
|
100
128
|
}
|
101
129
|
|
@@ -355,52 +383,119 @@ export default class IconProvider {
|
|
355
383
|
return icon;
|
356
384
|
}
|
357
385
|
|
358
|
-
public makePenIcon(
|
386
|
+
public makePenIcon(strokeSize: number, color: string|Color4, rounded?: boolean): IconType {
|
359
387
|
if (color instanceof Color4) {
|
360
388
|
color = color.toHexString();
|
361
389
|
}
|
362
390
|
|
363
391
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
364
392
|
icon.setAttribute('viewBox', '0 0 100 100');
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
393
|
+
const tipThickness = strokeSize / 2;
|
394
|
+
|
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
|
+
`;
|
409
|
+
|
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
|
+
`;
|
371
415
|
|
372
|
-
let
|
373
|
-
let
|
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';
|
374
419
|
|
375
|
-
if (
|
376
|
-
|
377
|
-
|
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';
|
378
424
|
}
|
379
425
|
|
380
|
-
const
|
381
|
-
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`;
|
382
427
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
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 = `
|
389
465
|
<path
|
390
|
-
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
|
391
466
|
${iconColorStrokeFill}
|
467
|
+
d="${gripMainPath}"
|
392
468
|
/>
|
393
|
-
|
394
|
-
|
395
|
-
<!-- Checkerboard background for slightly transparent pens -->
|
396
|
-
<path d='${backgroundStrokeTipPath}' fill='${checkerboardPatternRef}'/>
|
397
|
-
|
398
|
-
<!-- Actual pen tip -->
|
469
|
+
|
470
|
+
<!-- shadows -->
|
399
471
|
<path
|
400
|
-
|
401
|
-
|
402
|
-
stroke='${color}'
|
472
|
+
fill="rgba(150, 150, 150, 0.3)"
|
473
|
+
d="${gripShadow1Path}"
|
403
474
|
/>
|
475
|
+
<path
|
476
|
+
fill="rgba(100, 100, 100, 0.2)"
|
477
|
+
d="${gripShadow2Path}"
|
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}
|
404
499
|
</g>
|
405
500
|
`;
|
406
501
|
return icon;
|
@@ -1,26 +1,85 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
2
|
import Eraser from '../../tools/Eraser';
|
3
|
+
import { EditorEventType } from '../../types';
|
4
|
+
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
3
5
|
import { ToolbarLocalization } from '../localization';
|
4
6
|
import BaseToolWidget from './BaseToolWidget';
|
7
|
+
import { SavedToolbuttonState } from './BaseWidget';
|
5
8
|
|
6
9
|
export default class EraserToolWidget extends BaseToolWidget {
|
10
|
+
private thicknessInput: HTMLInputElement|null = null;
|
7
11
|
public constructor(
|
8
12
|
editor: Editor,
|
9
|
-
tool: Eraser,
|
13
|
+
private tool: Eraser,
|
10
14
|
localizationTable?: ToolbarLocalization
|
11
15
|
) {
|
12
16
|
super(editor, tool, 'eraser-tool-widget', localizationTable);
|
17
|
+
|
18
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
|
19
|
+
if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === this.tool) {
|
20
|
+
this.updateInputs();
|
21
|
+
this.updateIcon();
|
22
|
+
}
|
23
|
+
});
|
13
24
|
}
|
14
25
|
|
15
26
|
protected getTitle(): string {
|
16
27
|
return this.localizationTable.eraser;
|
17
28
|
}
|
29
|
+
|
18
30
|
protected createIcon(): Element {
|
19
|
-
return this.editor.icons.makeEraserIcon();
|
31
|
+
return this.editor.icons.makeEraserIcon(this.tool.getThickness());
|
32
|
+
}
|
33
|
+
|
34
|
+
private updateInputs() {
|
35
|
+
if (this.thicknessInput) {
|
36
|
+
this.thicknessInput.value = `${this.tool.getThickness()}`;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
private static nextThicknessInputId = 0;
|
41
|
+
|
42
|
+
protected fillDropdown(dropdown: HTMLElement): boolean {
|
43
|
+
const thicknessLabel = document.createElement('label');
|
44
|
+
this.thicknessInput = document.createElement('input');
|
45
|
+
|
46
|
+
this.thicknessInput.type = 'range';
|
47
|
+
this.thicknessInput.min = '4';
|
48
|
+
this.thicknessInput.max = '40';
|
49
|
+
this.thicknessInput.oninput = () => {
|
50
|
+
this.tool.setThickness(parseFloat(this.thicknessInput!.value));
|
51
|
+
};
|
52
|
+
this.thicknessInput.id = `${toolbarCSSPrefix}eraserThicknessInput${EraserToolWidget.nextThicknessInputId++}`;
|
53
|
+
|
54
|
+
thicknessLabel.innerText = this.localizationTable.thicknessLabel;
|
55
|
+
thicknessLabel.htmlFor = this.thicknessInput.id;
|
56
|
+
|
57
|
+
this.updateInputs();
|
58
|
+
dropdown.replaceChildren(thicknessLabel, this.thicknessInput);
|
59
|
+
return true;
|
60
|
+
}
|
61
|
+
|
62
|
+
public serializeState(): SavedToolbuttonState {
|
63
|
+
return {
|
64
|
+
...super.serializeState(),
|
65
|
+
|
66
|
+
thickness: this.tool.getThickness(),
|
67
|
+
};
|
20
68
|
}
|
21
69
|
|
22
|
-
|
23
|
-
|
24
|
-
|
70
|
+
public deserializeFrom(state: SavedToolbuttonState) {
|
71
|
+
super.deserializeFrom(state);
|
72
|
+
|
73
|
+
if (state.thickness) {
|
74
|
+
const parsedThickness = parseFloat(state.thickness);
|
75
|
+
|
76
|
+
if (typeof parsedThickness !== 'number' || !isFinite(parsedThickness)) {
|
77
|
+
throw new Error(
|
78
|
+
`Deserializing property ${parsedThickness} is not a number or is not finite.`
|
79
|
+
);
|
80
|
+
}
|
81
|
+
|
82
|
+
this.tool.setThickness(parsedThickness);
|
83
|
+
}
|
25
84
|
}
|
26
85
|
}
|
@@ -145,8 +145,8 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
145
145
|
const objectTypeSelect = document.createElement('select');
|
146
146
|
|
147
147
|
// Give inputs IDs so we can label them with a <label for=...>Label text</label>
|
148
|
-
thicknessInput.id = `${toolbarCSSPrefix}
|
149
|
-
objectTypeSelect.id = `${toolbarCSSPrefix}
|
148
|
+
thicknessInput.id = `${toolbarCSSPrefix}penThicknessInput${PenToolWidget.idCounter++}`;
|
149
|
+
objectTypeSelect.id = `${toolbarCSSPrefix}penBuilderSelect${PenToolWidget.idCounter++}`;
|
150
150
|
|
151
151
|
thicknessLabel.innerText = this.localizationTable.thicknessLabel;
|
152
152
|
thicknessLabel.setAttribute('for', thicknessInput.id);
|
@@ -35,9 +35,9 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
35
35
|
editor, 'duplicate-btn',
|
36
36
|
() => editor.icons.makeDuplicateSelectionIcon(),
|
37
37
|
this.localizationTable.duplicateSelection,
|
38
|
-
() => {
|
38
|
+
async () => {
|
39
39
|
const selection = this.tool.getSelection();
|
40
|
-
this.editor.dispatch(selection!.duplicateSelectedObjects());
|
40
|
+
this.editor.dispatch(await selection!.duplicateSelectedObjects());
|
41
41
|
},
|
42
42
|
localization,
|
43
43
|
);
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { Rect2, StrokeComponent } from '../lib';
|
3
|
+
import { Vec2 } from '../math/Vec2';
|
4
|
+
import createEditor from '../testing/createEditor';
|
5
|
+
import { InputEvtType } from '../types';
|
6
|
+
import Eraser from './Eraser';
|
7
|
+
|
8
|
+
const selectEraser = (editor: Editor) => {
|
9
|
+
const tools = editor.toolController;
|
10
|
+
const eraser = tools.getMatchingTools(Eraser)[0];
|
11
|
+
eraser.setEnabled(true);
|
12
|
+
|
13
|
+
return eraser;
|
14
|
+
};
|
15
|
+
|
16
|
+
const getAllStrokes = (editor: Editor) => {
|
17
|
+
return editor.image.getAllElements().filter(elem => elem instanceof StrokeComponent);
|
18
|
+
};
|
19
|
+
|
20
|
+
describe('Eraser', () => {
|
21
|
+
it('should erase object between locations of events', () => {
|
22
|
+
const editor = createEditor();
|
23
|
+
|
24
|
+
// Draw a line
|
25
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
26
|
+
jest.advanceTimersByTime(100);
|
27
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(200, 200));
|
28
|
+
|
29
|
+
// Should have drawn a line
|
30
|
+
const strokes = getAllStrokes(editor);
|
31
|
+
expect(strokes).toHaveLength(1);
|
32
|
+
expect(strokes[0].getBBox().area).toBeGreaterThanOrEqual(200 * 200);
|
33
|
+
|
34
|
+
selectEraser(editor);
|
35
|
+
|
36
|
+
// Erase the line.
|
37
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 0));
|
38
|
+
jest.advanceTimersByTime(400);
|
39
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(0, 200));
|
40
|
+
|
41
|
+
// Should have erased the line
|
42
|
+
expect(getAllStrokes(editor)).toHaveLength(0);
|
43
|
+
});
|
44
|
+
|
45
|
+
it('should erase objects within eraser.thickness of an event when not zoomed', async () => {
|
46
|
+
const editor = createEditor();
|
47
|
+
|
48
|
+
await editor.loadFromSVG(`
|
49
|
+
<svg>
|
50
|
+
<path d='m0,0 l2,0 l0,2 l-2,0 z' fill="#ff0000"/>
|
51
|
+
<path d='m50,50 l2,0 l0,2 l-2,0 z' fill="#ff0000"/>
|
52
|
+
</svg>
|
53
|
+
`, true);
|
54
|
+
|
55
|
+
editor.viewport.resetTransform();
|
56
|
+
|
57
|
+
const allStrokes = getAllStrokes(editor);
|
58
|
+
expect(allStrokes).toHaveLength(2);
|
59
|
+
expect(allStrokes[0].getBBox()).objEq(new Rect2(0, 0, 2, 2));
|
60
|
+
expect(allStrokes[1].getBBox()).objEq(new Rect2(50, 50, 2, 2));
|
61
|
+
|
62
|
+
const eraser = selectEraser(editor);
|
63
|
+
eraser.setThickness(10);
|
64
|
+
|
65
|
+
// Erase the first stroke
|
66
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(3, 0));
|
67
|
+
jest.advanceTimersByTime(100);
|
68
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(3, 0));
|
69
|
+
|
70
|
+
expect(getAllStrokes(editor)).toHaveLength(1);
|
71
|
+
|
72
|
+
// Erase the remaining stroke
|
73
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(47, 47));
|
74
|
+
jest.advanceTimersByTime(100);
|
75
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(47, 47));
|
76
|
+
|
77
|
+
expect(getAllStrokes(editor)).toHaveLength(0);
|
78
|
+
});
|
79
|
+
});
|
package/src/tools/Eraser.ts
CHANGED
@@ -1,15 +1,20 @@
|
|
1
|
-
import { PointerEvt } from '../types';
|
1
|
+
import { EditorEventType, PointerEvt } from '../types';
|
2
2
|
import BaseTool from './BaseTool';
|
3
3
|
import Editor from '../Editor';
|
4
|
-
import { Point2 } from '../math/Vec2';
|
4
|
+
import { Point2, Vec2 } from '../math/Vec2';
|
5
5
|
import LineSegment2 from '../math/LineSegment2';
|
6
6
|
import Erase from '../commands/Erase';
|
7
7
|
import AbstractComponent from '../components/AbstractComponent';
|
8
8
|
import { PointerDevice } from '../Pointer';
|
9
|
+
import Color4 from '../Color4';
|
10
|
+
import Rect2 from '../math/Rect2';
|
11
|
+
import RenderingStyle from '../rendering/RenderingStyle';
|
9
12
|
|
10
13
|
export default class Eraser extends BaseTool {
|
11
|
-
private lastPoint: Point2;
|
14
|
+
private lastPoint: Point2|null = null;
|
15
|
+
private isFirstEraseEvt: boolean = true;
|
12
16
|
private toRemove: AbstractComponent[];
|
17
|
+
private thickness: number = 10;
|
13
18
|
|
14
19
|
// Commands that each remove one element
|
15
20
|
private partialCommands: Erase[] = [];
|
@@ -18,27 +23,48 @@ export default class Eraser extends BaseTool {
|
|
18
23
|
super(editor.notifier, description);
|
19
24
|
}
|
20
25
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
this.toRemove = [];
|
25
|
-
return true;
|
26
|
-
}
|
26
|
+
private clearPreview() {
|
27
|
+
this.editor.clearWetInk();
|
28
|
+
}
|
27
29
|
|
28
|
-
|
30
|
+
private getSizeOnCanvas() {
|
31
|
+
return this.thickness / this.editor.viewport.getScaleFactor();
|
29
32
|
}
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
+
private drawPreviewAt(point: Point2) {
|
35
|
+
this.clearPreview();
|
36
|
+
|
37
|
+
const size = this.getSizeOnCanvas();
|
38
|
+
|
39
|
+
const renderer = this.editor.display.getWetInkRenderer();
|
40
|
+
const rect = this.getEraserRect(point);
|
41
|
+
const fill: RenderingStyle = {
|
42
|
+
fill: Color4.gray,
|
43
|
+
};
|
44
|
+
renderer.drawRect(rect, size / 4, fill);
|
45
|
+
}
|
46
|
+
|
47
|
+
private getEraserRect(centerPoint: Point2) {
|
48
|
+
const size = this.getSizeOnCanvas();
|
49
|
+
const halfSize = Vec2.of(size / 2, size / 2);
|
50
|
+
return Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
|
51
|
+
}
|
52
|
+
|
53
|
+
private eraseTo(currentPoint: Point2) {
|
54
|
+
if (!this.isFirstEraseEvt && currentPoint.minus(this.lastPoint!).magnitude() === 0) {
|
34
55
|
return;
|
35
56
|
}
|
57
|
+
this.isFirstEraseEvt = false;
|
36
58
|
|
37
|
-
|
38
|
-
|
59
|
+
// Currently only objects within eraserRect or that intersect a straight line
|
60
|
+
// from the center of the current rect to the previous are erased. TODO: Erase
|
61
|
+
// all objects as if there were pointerMove events between the two points.
|
62
|
+
const eraserRect = this.getEraserRect(currentPoint);
|
63
|
+
const line = new LineSegment2(this.lastPoint!, currentPoint);
|
64
|
+
const region = Rect2.union(line.bbox, eraserRect);
|
39
65
|
|
40
66
|
const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
|
41
|
-
return component.intersects(line);
|
67
|
+
return component.intersects(line) || component.intersectsRect(eraserRect);
|
42
68
|
});
|
43
69
|
|
44
70
|
// Remove any intersecting elements.
|
@@ -50,10 +76,32 @@ export default class Eraser extends BaseTool {
|
|
50
76
|
|
51
77
|
this.partialCommands.push(...newPartialCommands);
|
52
78
|
|
79
|
+
this.drawPreviewAt(currentPoint);
|
53
80
|
this.lastPoint = currentPoint;
|
54
81
|
}
|
55
82
|
|
56
|
-
public
|
83
|
+
public onPointerDown(event: PointerEvt): boolean {
|
84
|
+
if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
|
85
|
+
this.lastPoint = event.current.canvasPos;
|
86
|
+
this.toRemove = [];
|
87
|
+
this.isFirstEraseEvt = true;
|
88
|
+
|
89
|
+
this.drawPreviewAt(event.current.canvasPos);
|
90
|
+
return true;
|
91
|
+
}
|
92
|
+
|
93
|
+
return false;
|
94
|
+
}
|
95
|
+
|
96
|
+
public onPointerMove(event: PointerEvt): void {
|
97
|
+
const currentPoint = event.current.canvasPos;
|
98
|
+
|
99
|
+
this.eraseTo(currentPoint);
|
100
|
+
}
|
101
|
+
|
102
|
+
public onPointerUp(event: PointerEvt): void {
|
103
|
+
this.eraseTo(event.current.canvasPos);
|
104
|
+
|
57
105
|
if (this.toRemove.length > 0) {
|
58
106
|
// Undo commands for each individual component and unite into a single command.
|
59
107
|
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
@@ -62,10 +110,26 @@ export default class Eraser extends BaseTool {
|
|
62
110
|
const command = new Erase(this.toRemove);
|
63
111
|
this.editor.dispatch(command); // dispatch: Makes undo-able.
|
64
112
|
}
|
113
|
+
|
114
|
+
this.clearPreview();
|
65
115
|
}
|
66
116
|
|
67
117
|
public onGestureCancel(): void {
|
68
118
|
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
69
119
|
this.partialCommands = [];
|
120
|
+
this.clearPreview();
|
121
|
+
}
|
122
|
+
|
123
|
+
public getThickness() {
|
124
|
+
return this.thickness;
|
125
|
+
}
|
126
|
+
|
127
|
+
public setThickness(thickness: number) {
|
128
|
+
this.thickness = thickness;
|
129
|
+
|
130
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
131
|
+
kind: EditorEventType.ToolUpdated,
|
132
|
+
tool: this,
|
133
|
+
});
|
70
134
|
}
|
71
135
|
}
|
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
|
});
|