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
@@ -1,16 +1,57 @@
|
|
1
|
+
import { EditorEventType } from '../../types';
|
2
|
+
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
1
3
|
import BaseToolWidget from './BaseToolWidget';
|
2
4
|
export default class EraserToolWidget extends BaseToolWidget {
|
3
5
|
constructor(editor, tool, localizationTable) {
|
4
6
|
super(editor, tool, 'eraser-tool-widget', localizationTable);
|
7
|
+
this.tool = tool;
|
8
|
+
this.thicknessInput = null;
|
9
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
|
10
|
+
if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === this.tool) {
|
11
|
+
this.updateInputs();
|
12
|
+
this.updateIcon();
|
13
|
+
}
|
14
|
+
});
|
5
15
|
}
|
6
16
|
getTitle() {
|
7
17
|
return this.localizationTable.eraser;
|
8
18
|
}
|
9
19
|
createIcon() {
|
10
|
-
return this.editor.icons.makeEraserIcon();
|
20
|
+
return this.editor.icons.makeEraserIcon(this.tool.getThickness());
|
11
21
|
}
|
12
|
-
|
13
|
-
|
14
|
-
|
22
|
+
updateInputs() {
|
23
|
+
if (this.thicknessInput) {
|
24
|
+
this.thicknessInput.value = `${this.tool.getThickness()}`;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
fillDropdown(dropdown) {
|
28
|
+
const thicknessLabel = document.createElement('label');
|
29
|
+
this.thicknessInput = document.createElement('input');
|
30
|
+
this.thicknessInput.type = 'range';
|
31
|
+
this.thicknessInput.min = '4';
|
32
|
+
this.thicknessInput.max = '40';
|
33
|
+
this.thicknessInput.oninput = () => {
|
34
|
+
this.tool.setThickness(parseFloat(this.thicknessInput.value));
|
35
|
+
};
|
36
|
+
this.thicknessInput.id = `${toolbarCSSPrefix}eraserThicknessInput${EraserToolWidget.nextThicknessInputId++}`;
|
37
|
+
thicknessLabel.innerText = this.localizationTable.thicknessLabel;
|
38
|
+
thicknessLabel.htmlFor = this.thicknessInput.id;
|
39
|
+
this.updateInputs();
|
40
|
+
dropdown.replaceChildren(thicknessLabel, this.thicknessInput);
|
41
|
+
return true;
|
42
|
+
}
|
43
|
+
serializeState() {
|
44
|
+
return Object.assign(Object.assign({}, super.serializeState()), { thickness: this.tool.getThickness() });
|
45
|
+
}
|
46
|
+
deserializeFrom(state) {
|
47
|
+
super.deserializeFrom(state);
|
48
|
+
if (state.thickness) {
|
49
|
+
const parsedThickness = parseFloat(state.thickness);
|
50
|
+
if (typeof parsedThickness !== 'number' || !isFinite(parsedThickness)) {
|
51
|
+
throw new Error(`Deserializing property ${parsedThickness} is not a number or is not finite.`);
|
52
|
+
}
|
53
|
+
this.tool.setThickness(parsedThickness);
|
54
|
+
}
|
15
55
|
}
|
16
56
|
}
|
57
|
+
EraserToolWidget.nextThicknessInputId = 0;
|
@@ -105,8 +105,8 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
105
105
|
const objectSelectLabel = document.createElement('label');
|
106
106
|
const objectTypeSelect = document.createElement('select');
|
107
107
|
// Give inputs IDs so we can label them with a <label for=...>Label text</label>
|
108
|
-
thicknessInput.id = `${toolbarCSSPrefix}
|
109
|
-
objectTypeSelect.id = `${toolbarCSSPrefix}
|
108
|
+
thicknessInput.id = `${toolbarCSSPrefix}penThicknessInput${PenToolWidget.idCounter++}`;
|
109
|
+
objectTypeSelect.id = `${toolbarCSSPrefix}penBuilderSelect${PenToolWidget.idCounter++}`;
|
110
110
|
thicknessLabel.innerText = this.localizationTable.thicknessLabel;
|
111
111
|
thicknessLabel.setAttribute('for', thicknessInput.id);
|
112
112
|
objectSelectLabel.innerText = this.localizationTable.selectObjectType;
|
@@ -1,3 +1,12 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
1
10
|
import { EditorEventType } from '../../types';
|
2
11
|
import ActionButtonWidget from './ActionButtonWidget';
|
3
12
|
import BaseToolWidget from './BaseToolWidget';
|
@@ -13,10 +22,10 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
13
22
|
this.editor.dispatch(selection.deleteSelectedObjects());
|
14
23
|
this.tool.clearSelection();
|
15
24
|
}, localization);
|
16
|
-
const duplicateButton = new ActionButtonWidget(editor, 'duplicate-btn', () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => {
|
25
|
+
const duplicateButton = new ActionButtonWidget(editor, 'duplicate-btn', () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => __awaiter(this, void 0, void 0, function* () {
|
17
26
|
const selection = this.tool.getSelection();
|
18
|
-
this.editor.dispatch(selection.duplicateSelectedObjects());
|
19
|
-
}, localization);
|
27
|
+
this.editor.dispatch(yield selection.duplicateSelectedObjects());
|
28
|
+
}), localization);
|
20
29
|
this.addSubWidget(resizeButton);
|
21
30
|
this.addSubWidget(deleteButton);
|
22
31
|
this.addSubWidget(duplicateButton);
|
@@ -4,11 +4,20 @@ import Editor from '../Editor';
|
|
4
4
|
export default class Eraser extends BaseTool {
|
5
5
|
private editor;
|
6
6
|
private lastPoint;
|
7
|
+
private isFirstEraseEvt;
|
7
8
|
private toRemove;
|
9
|
+
private thickness;
|
8
10
|
private partialCommands;
|
9
11
|
constructor(editor: Editor, description: string);
|
12
|
+
private clearPreview;
|
13
|
+
private getSizeOnCanvas;
|
14
|
+
private drawPreviewAt;
|
15
|
+
private getEraserRect;
|
16
|
+
private eraseTo;
|
10
17
|
onPointerDown(event: PointerEvt): boolean;
|
11
18
|
onPointerMove(event: PointerEvt): void;
|
12
|
-
onPointerUp(
|
19
|
+
onPointerUp(event: PointerEvt): void;
|
13
20
|
onGestureCancel(): void;
|
21
|
+
getThickness(): number;
|
22
|
+
setThickness(thickness: number): void;
|
14
23
|
}
|
package/dist/src/tools/Eraser.js
CHANGED
@@ -1,31 +1,55 @@
|
|
1
|
+
import { EditorEventType } from '../types';
|
1
2
|
import BaseTool from './BaseTool';
|
3
|
+
import { Vec2 } from '../math/Vec2';
|
2
4
|
import LineSegment2 from '../math/LineSegment2';
|
3
5
|
import Erase from '../commands/Erase';
|
4
6
|
import { PointerDevice } from '../Pointer';
|
7
|
+
import Color4 from '../Color4';
|
8
|
+
import Rect2 from '../math/Rect2';
|
5
9
|
export default class Eraser extends BaseTool {
|
6
10
|
constructor(editor, description) {
|
7
11
|
super(editor.notifier, description);
|
8
12
|
this.editor = editor;
|
13
|
+
this.lastPoint = null;
|
14
|
+
this.isFirstEraseEvt = true;
|
15
|
+
this.thickness = 10;
|
9
16
|
// Commands that each remove one element
|
10
17
|
this.partialCommands = [];
|
11
18
|
}
|
12
|
-
|
13
|
-
|
14
|
-
this.lastPoint = event.current.canvasPos;
|
15
|
-
this.toRemove = [];
|
16
|
-
return true;
|
17
|
-
}
|
18
|
-
return false;
|
19
|
+
clearPreview() {
|
20
|
+
this.editor.clearWetInk();
|
19
21
|
}
|
20
|
-
|
21
|
-
|
22
|
-
|
22
|
+
getSizeOnCanvas() {
|
23
|
+
return this.thickness / this.editor.viewport.getScaleFactor();
|
24
|
+
}
|
25
|
+
drawPreviewAt(point) {
|
26
|
+
this.clearPreview();
|
27
|
+
const size = this.getSizeOnCanvas();
|
28
|
+
const renderer = this.editor.display.getWetInkRenderer();
|
29
|
+
const rect = this.getEraserRect(point);
|
30
|
+
const fill = {
|
31
|
+
fill: Color4.gray,
|
32
|
+
};
|
33
|
+
renderer.drawRect(rect, size / 4, fill);
|
34
|
+
}
|
35
|
+
getEraserRect(centerPoint) {
|
36
|
+
const size = this.getSizeOnCanvas();
|
37
|
+
const halfSize = Vec2.of(size / 2, size / 2);
|
38
|
+
return Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
|
39
|
+
}
|
40
|
+
eraseTo(currentPoint) {
|
41
|
+
if (!this.isFirstEraseEvt && currentPoint.minus(this.lastPoint).magnitude() === 0) {
|
23
42
|
return;
|
24
43
|
}
|
44
|
+
this.isFirstEraseEvt = false;
|
45
|
+
// Currently only objects within eraserRect or that intersect a straight line
|
46
|
+
// from the center of the current rect to the previous are erased. TODO: Erase
|
47
|
+
// all objects as if there were pointerMove events between the two points.
|
48
|
+
const eraserRect = this.getEraserRect(currentPoint);
|
25
49
|
const line = new LineSegment2(this.lastPoint, currentPoint);
|
26
|
-
const region = line.bbox;
|
50
|
+
const region = Rect2.union(line.bbox, eraserRect);
|
27
51
|
const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
|
28
|
-
return component.intersects(line);
|
52
|
+
return component.intersects(line) || component.intersectsRect(eraserRect);
|
29
53
|
});
|
30
54
|
// Remove any intersecting elements.
|
31
55
|
this.toRemove.push(...intersectingElems);
|
@@ -33,9 +57,25 @@ export default class Eraser extends BaseTool {
|
|
33
57
|
const newPartialCommands = intersectingElems.map(elem => new Erase([elem]));
|
34
58
|
newPartialCommands.forEach(cmd => cmd.apply(this.editor));
|
35
59
|
this.partialCommands.push(...newPartialCommands);
|
60
|
+
this.drawPreviewAt(currentPoint);
|
36
61
|
this.lastPoint = currentPoint;
|
37
62
|
}
|
38
|
-
|
63
|
+
onPointerDown(event) {
|
64
|
+
if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
|
65
|
+
this.lastPoint = event.current.canvasPos;
|
66
|
+
this.toRemove = [];
|
67
|
+
this.isFirstEraseEvt = true;
|
68
|
+
this.drawPreviewAt(event.current.canvasPos);
|
69
|
+
return true;
|
70
|
+
}
|
71
|
+
return false;
|
72
|
+
}
|
73
|
+
onPointerMove(event) {
|
74
|
+
const currentPoint = event.current.canvasPos;
|
75
|
+
this.eraseTo(currentPoint);
|
76
|
+
}
|
77
|
+
onPointerUp(event) {
|
78
|
+
this.eraseTo(event.current.canvasPos);
|
39
79
|
if (this.toRemove.length > 0) {
|
40
80
|
// Undo commands for each individual component and unite into a single command.
|
41
81
|
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
@@ -43,9 +83,21 @@ export default class Eraser extends BaseTool {
|
|
43
83
|
const command = new Erase(this.toRemove);
|
44
84
|
this.editor.dispatch(command); // dispatch: Makes undo-able.
|
45
85
|
}
|
86
|
+
this.clearPreview();
|
46
87
|
}
|
47
88
|
onGestureCancel() {
|
48
89
|
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
49
90
|
this.partialCommands = [];
|
91
|
+
this.clearPreview();
|
92
|
+
}
|
93
|
+
getThickness() {
|
94
|
+
return this.thickness;
|
95
|
+
}
|
96
|
+
setThickness(thickness) {
|
97
|
+
this.thickness = thickness;
|
98
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
99
|
+
kind: EditorEventType.ToolUpdated,
|
100
|
+
tool: this,
|
101
|
+
});
|
50
102
|
}
|
51
103
|
}
|
@@ -266,7 +266,7 @@ export default class PanZoom extends BaseTool {
|
|
266
266
|
const toCanvas = this.editor.viewport.screenToCanvasTransform;
|
267
267
|
// Transform without including translation
|
268
268
|
const translation = toCanvas.transformVec3(Vec3.of(-delta.x, -delta.y, 0));
|
269
|
-
const pinchZoomScaleFactor = 1.
|
269
|
+
const pinchZoomScaleFactor = 1.03;
|
270
270
|
const transformUpdate = Mat33.scaling2D(Math.max(0.25, Math.min(Math.pow(pinchZoomScaleFactor, -delta.z), 4)), canvasPos).rightMul(Mat33.translation(translation));
|
271
271
|
this.updateTransform(transformUpdate, true);
|
272
272
|
return true;
|
@@ -1,10 +1,17 @@
|
|
1
|
-
/**
|
2
|
-
* A tool that handles paste events.
|
3
|
-
* @packageDocumentation
|
4
|
-
*/
|
5
1
|
import Editor from '../Editor';
|
6
2
|
import { PasteEvent } from '../types';
|
7
3
|
import BaseTool from './BaseTool';
|
4
|
+
/**
|
5
|
+
* A tool that handles paste events (e.g. as triggered by ctrl+V).
|
6
|
+
*
|
7
|
+
* @example
|
8
|
+
* While `ToolController` has a `PasteHandler` in its default list of tools,
|
9
|
+
* if a non-default set is being used, `PasteHandler` can be added as follows:
|
10
|
+
* ```ts
|
11
|
+
* const toolController = editor.toolController;
|
12
|
+
* toolController.addTool(new PasteHandler(editor));
|
13
|
+
* ```
|
14
|
+
*/
|
8
15
|
export default class PasteHandler extends BaseTool {
|
9
16
|
private editor;
|
10
17
|
constructor(editor: Editor);
|
@@ -1,7 +1,3 @@
|
|
1
|
-
/**
|
2
|
-
* A tool that handles paste events.
|
3
|
-
* @packageDocumentation
|
4
|
-
*/
|
5
1
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
6
2
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
7
3
|
return new (P || (P = Promise))(function (resolve, reject) {
|
@@ -18,12 +14,23 @@ import BaseTool from './BaseTool';
|
|
18
14
|
import TextTool from './TextTool';
|
19
15
|
import Color4 from '../Color4';
|
20
16
|
import ImageComponent from '../components/ImageComponent';
|
21
|
-
|
17
|
+
/**
|
18
|
+
* A tool that handles paste events (e.g. as triggered by ctrl+V).
|
19
|
+
*
|
20
|
+
* @example
|
21
|
+
* While `ToolController` has a `PasteHandler` in its default list of tools,
|
22
|
+
* if a non-default set is being used, `PasteHandler` can be added as follows:
|
23
|
+
* ```ts
|
24
|
+
* const toolController = editor.toolController;
|
25
|
+
* toolController.addTool(new PasteHandler(editor));
|
26
|
+
* ```
|
27
|
+
*/
|
22
28
|
export default class PasteHandler extends BaseTool {
|
23
29
|
constructor(editor) {
|
24
30
|
super(editor.notifier, editor.localization.pasteHandler);
|
25
31
|
this.editor = editor;
|
26
32
|
}
|
33
|
+
// @internal
|
27
34
|
onPaste(event) {
|
28
35
|
const mime = event.mime.toLowerCase();
|
29
36
|
if (mime === 'image/svg+xml') {
|
package/dist/src/tools/Pen.d.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
2
|
import Editor from '../Editor';
|
3
3
|
import Pointer from '../Pointer';
|
4
|
-
import { KeyPressEvent, PointerEvt, StrokeDataPoint } from '../types';
|
4
|
+
import { KeyPressEvent, KeyUpEvent, PointerEvt, StrokeDataPoint } from '../types';
|
5
5
|
import BaseTool from './BaseTool';
|
6
6
|
import { ComponentBuilder, ComponentBuilderFactory } from '../components/builders/types';
|
7
7
|
export interface PenStyle {
|
@@ -14,6 +14,7 @@ export default class Pen extends BaseTool {
|
|
14
14
|
private builderFactory;
|
15
15
|
protected builder: ComponentBuilder | null;
|
16
16
|
private lastPoint;
|
17
|
+
private ctrlKeyPressed;
|
17
18
|
constructor(editor: Editor, description: string, style: PenStyle, builderFactory?: ComponentBuilderFactory);
|
18
19
|
private getPressureMultiplier;
|
19
20
|
protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
|
@@ -23,6 +24,7 @@ export default class Pen extends BaseTool {
|
|
23
24
|
onPointerMove({ current }: PointerEvt): void;
|
24
25
|
onPointerUp({ current }: PointerEvt): void;
|
25
26
|
onGestureCancel(): void;
|
27
|
+
private finalizeStroke;
|
26
28
|
private noteUpdated;
|
27
29
|
setColor(color: Color4): void;
|
28
30
|
setThickness(thickness: number): void;
|
@@ -30,5 +32,8 @@ export default class Pen extends BaseTool {
|
|
30
32
|
getThickness(): number;
|
31
33
|
getColor(): Color4;
|
32
34
|
getStrokeFactory(): ComponentBuilderFactory;
|
33
|
-
|
35
|
+
setEnabled(enabled: boolean): void;
|
36
|
+
private isSnappingToGrid;
|
37
|
+
onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
|
38
|
+
onKeyUp({ key }: KeyUpEvent): boolean;
|
34
39
|
}
|
package/dist/src/tools/Pen.js
CHANGED
@@ -11,6 +11,7 @@ export default class Pen extends BaseTool {
|
|
11
11
|
this.builderFactory = builderFactory;
|
12
12
|
this.builder = null;
|
13
13
|
this.lastPoint = null;
|
14
|
+
this.ctrlKeyPressed = false;
|
14
15
|
}
|
15
16
|
getPressureMultiplier() {
|
16
17
|
return 1 / this.editor.viewport.getScaleFactor() * this.style.thickness;
|
@@ -18,6 +19,9 @@ export default class Pen extends BaseTool {
|
|
18
19
|
// Converts a `pointer` to a `StrokeDataPoint`.
|
19
20
|
toStrokePoint(pointer) {
|
20
21
|
var _a;
|
22
|
+
if (this.isSnappingToGrid()) {
|
23
|
+
pointer = pointer.snappedToGrid(this.editor.viewport);
|
24
|
+
}
|
21
25
|
const minPressure = 0.3;
|
22
26
|
let pressure = Math.max((_a = pointer.pressure) !== null && _a !== void 0 ? _a : 1.0, minPressure);
|
23
27
|
if (!isFinite(pressure)) {
|
@@ -27,8 +31,9 @@ export default class Pen extends BaseTool {
|
|
27
31
|
console.assert(isFinite(pointer.canvasPos.length()), 'Non-finite canvas position!');
|
28
32
|
console.assert(isFinite(pointer.screenPos.length()), 'Non-finite screen position!');
|
29
33
|
console.assert(isFinite(pointer.timeStamp), 'Non-finite timeStamp on pointer!');
|
34
|
+
const pos = pointer.canvasPos;
|
30
35
|
return {
|
31
|
-
pos
|
36
|
+
pos,
|
32
37
|
width: pressure * this.getPressureMultiplier(),
|
33
38
|
color: this.style.color,
|
34
39
|
time: pointer.timeStamp,
|
@@ -65,6 +70,8 @@ export default class Pen extends BaseTool {
|
|
65
70
|
return false;
|
66
71
|
}
|
67
72
|
onPointerMove({ current }) {
|
73
|
+
if (!this.builder)
|
74
|
+
return;
|
68
75
|
this.addPointToStroke(this.toStrokePoint(current));
|
69
76
|
}
|
70
77
|
onPointerUp({ current }) {
|
@@ -76,7 +83,15 @@ export default class Pen extends BaseTool {
|
|
76
83
|
const currentPoint = this.toStrokePoint(current);
|
77
84
|
const strokePoint = Object.assign(Object.assign({}, currentPoint), { width: (_b = (_a = this.lastPoint) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : currentPoint.width });
|
78
85
|
this.addPointToStroke(strokePoint);
|
79
|
-
if (
|
86
|
+
if (current.isPrimary) {
|
87
|
+
this.finalizeStroke();
|
88
|
+
}
|
89
|
+
}
|
90
|
+
onGestureCancel() {
|
91
|
+
this.editor.clearWetInk();
|
92
|
+
}
|
93
|
+
finalizeStroke() {
|
94
|
+
if (this.builder) {
|
80
95
|
const stroke = this.builder.build();
|
81
96
|
this.previewStroke();
|
82
97
|
if (stroke.getBBox().area > 0) {
|
@@ -91,9 +106,6 @@ export default class Pen extends BaseTool {
|
|
91
106
|
this.builder = null;
|
92
107
|
this.editor.clearWetInk();
|
93
108
|
}
|
94
|
-
onGestureCancel() {
|
95
|
-
this.editor.clearWetInk();
|
96
|
-
}
|
97
109
|
noteUpdated() {
|
98
110
|
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
99
111
|
kind: EditorEventType.ToolUpdated,
|
@@ -121,7 +133,12 @@ export default class Pen extends BaseTool {
|
|
121
133
|
getThickness() { return this.style.thickness; }
|
122
134
|
getColor() { return this.style.color; }
|
123
135
|
getStrokeFactory() { return this.builderFactory; }
|
124
|
-
|
136
|
+
setEnabled(enabled) {
|
137
|
+
super.setEnabled(enabled);
|
138
|
+
this.ctrlKeyPressed = false;
|
139
|
+
}
|
140
|
+
isSnappingToGrid() { return this.ctrlKeyPressed; }
|
141
|
+
onKeyPress({ key, ctrlKey }) {
|
125
142
|
key = key.toLowerCase();
|
126
143
|
let newThickness;
|
127
144
|
if (key === '-' || key === '_') {
|
@@ -135,6 +152,22 @@ export default class Pen extends BaseTool {
|
|
135
152
|
this.setThickness(newThickness);
|
136
153
|
return true;
|
137
154
|
}
|
155
|
+
if (key === 'control') {
|
156
|
+
this.ctrlKeyPressed = true;
|
157
|
+
return true;
|
158
|
+
}
|
159
|
+
// Ctrl+Z: End the stroke so that it can be undone/redone.
|
160
|
+
if (key === 'z' && ctrlKey && this.builder) {
|
161
|
+
this.finalizeStroke();
|
162
|
+
}
|
163
|
+
return false;
|
164
|
+
}
|
165
|
+
onKeyUp({ key }) {
|
166
|
+
key = key.toLowerCase();
|
167
|
+
if (key === 'control') {
|
168
|
+
this.ctrlKeyPressed = false;
|
169
|
+
return true;
|
170
|
+
}
|
138
171
|
return false;
|
139
172
|
}
|
140
173
|
}
|
@@ -19,6 +19,7 @@ export default class Selection {
|
|
19
19
|
private backgroundElem;
|
20
20
|
private hasParent;
|
21
21
|
constructor(startPoint: Point2, editor: Editor);
|
22
|
+
getBackgroundElem(): HTMLElement;
|
22
23
|
getTransform(): Mat33;
|
23
24
|
get preTransformRegion(): Rect2;
|
24
25
|
get region(): Rect2;
|
@@ -36,7 +37,9 @@ export default class Selection {
|
|
36
37
|
getMinCanvasSize(): number;
|
37
38
|
getSelectedItemCount(): number;
|
38
39
|
updateUI(): void;
|
40
|
+
private removedFromImage;
|
39
41
|
private addRemoveSelectionFromImage;
|
42
|
+
private removeDeletedElemsFromSelection;
|
40
43
|
private targetHandle;
|
41
44
|
private backgroundDragging;
|
42
45
|
onDragStart(pointer: Pointer, target: EventTarget): boolean;
|
@@ -45,7 +48,7 @@ export default class Selection {
|
|
45
48
|
onDragCancel(): void;
|
46
49
|
scrollTo(): Promise<void>;
|
47
50
|
deleteSelectedObjects(): Command;
|
48
|
-
duplicateSelectedObjects(): Command
|
51
|
+
duplicateSelectedObjects(): Promise<Command>;
|
49
52
|
addTo(elem: HTMLElement): void;
|
50
53
|
setToPoint(point: Point2): void;
|
51
54
|
cancelSelection(): void;
|
@@ -32,6 +32,8 @@ export default class Selection {
|
|
32
32
|
this.transform = Mat33.identity;
|
33
33
|
this.selectedElems = [];
|
34
34
|
this.hasParent = true;
|
35
|
+
// Maps IDs to whether we removed the component from the image
|
36
|
+
this.removedFromImage = {};
|
35
37
|
this.targetHandle = null;
|
36
38
|
this.backgroundDragging = false;
|
37
39
|
this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
|
@@ -58,6 +60,10 @@ export default class Selection {
|
|
58
60
|
handle.addTo(this.backgroundElem);
|
59
61
|
}
|
60
62
|
}
|
63
|
+
// @internal Intended for unit tests
|
64
|
+
getBackgroundElem() {
|
65
|
+
return this.backgroundElem;
|
66
|
+
}
|
61
67
|
getTransform() {
|
62
68
|
return this.transform;
|
63
69
|
}
|
@@ -140,16 +146,7 @@ export default class Selection {
|
|
140
146
|
singleItemSelectionMode = true;
|
141
147
|
}
|
142
148
|
this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
|
143
|
-
|
144
|
-
return true;
|
145
|
-
}
|
146
|
-
// Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
|
147
|
-
// As such, test with more lines than just this' edges.
|
148
|
-
const testLines = [];
|
149
|
-
for (const subregion of this.region.divideIntoGrid(2, 2)) {
|
150
|
-
testLines.push(...subregion.getEdges());
|
151
|
-
}
|
152
|
-
return testLines.some(edge => elem.intersects(edge));
|
149
|
+
return elem.intersectsRect(this.region);
|
153
150
|
});
|
154
151
|
if (singleItemSelectionMode && this.selectedElems.length > 0) {
|
155
152
|
this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
|
@@ -215,30 +212,45 @@ export default class Selection {
|
|
215
212
|
//
|
216
213
|
// If removed from the image, selected elements are drawn as wet ink.
|
217
214
|
addRemoveSelectionFromImage(inImage) {
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
215
|
+
// Don't hide elements if doing so will be slow.
|
216
|
+
if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
|
217
|
+
return;
|
218
|
+
}
|
219
|
+
for (const elem of this.selectedElems) {
|
220
|
+
const parent = this.editor.image.findParent(elem);
|
221
|
+
if (!inImage && parent) {
|
222
|
+
this.removedFromImage[elem.getId()] = true;
|
223
|
+
parent.remove();
|
222
224
|
}
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
// visible,
|
230
|
-
else if (!parent) {
|
231
|
-
EditorImage.addElement(elem).apply(this.editor);
|
232
|
-
}
|
225
|
+
// If we're making things visible and the selected object wasn't previously
|
226
|
+
// visible,
|
227
|
+
else if (!parent && this.removedFromImage[elem.getId()]) {
|
228
|
+
EditorImage.addElement(elem).apply(this.editor);
|
229
|
+
this.removedFromImage[elem.getId()] = false;
|
230
|
+
delete this.removedFromImage[elem.getId()];
|
233
231
|
}
|
234
|
-
|
232
|
+
}
|
233
|
+
// Don't await queueRerender. If we're running in a test, the re-render might never
|
234
|
+
// happen.
|
235
|
+
this.editor.queueRerender().then(() => {
|
235
236
|
if (!inImage) {
|
236
237
|
this.previewTransformCmds();
|
237
238
|
}
|
238
239
|
});
|
239
240
|
}
|
241
|
+
removeDeletedElemsFromSelection() {
|
242
|
+
// Remove any deleted elements from the selection.
|
243
|
+
this.selectedElems = this.selectedElems.filter(elem => {
|
244
|
+
const hasParent = !!this.editor.image.findParent(elem);
|
245
|
+
// If we removed the element and haven't added it back yet, don't remove it
|
246
|
+
// from the selection.
|
247
|
+
const weRemoved = this.removedFromImage[elem.getId()];
|
248
|
+
return hasParent || weRemoved;
|
249
|
+
});
|
250
|
+
}
|
240
251
|
onDragStart(pointer, target) {
|
241
|
-
|
252
|
+
this.removeDeletedElemsFromSelection();
|
253
|
+
this.addRemoveSelectionFromImage(false);
|
242
254
|
for (const handle of this.handles) {
|
243
255
|
if (handle.isTarget(target)) {
|
244
256
|
handle.handleDragStart(pointer);
|
@@ -299,10 +311,34 @@ export default class Selection {
|
|
299
311
|
});
|
300
312
|
}
|
301
313
|
deleteSelectedObjects() {
|
314
|
+
if (this.backgroundDragging || this.targetHandle) {
|
315
|
+
this.onDragEnd();
|
316
|
+
}
|
302
317
|
return new Erase(this.selectedElems);
|
303
318
|
}
|
304
319
|
duplicateSelectedObjects() {
|
305
|
-
return
|
320
|
+
return __awaiter(this, void 0, void 0, function* () {
|
321
|
+
const wasTransforming = this.backgroundDragging || this.targetHandle;
|
322
|
+
let tmpApplyCommand = null;
|
323
|
+
if (wasTransforming) {
|
324
|
+
// Don't update the selection's focus when redoing/undoing
|
325
|
+
const selectionToUpdate = null;
|
326
|
+
tmpApplyCommand = new Selection.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
|
327
|
+
// Transform to ensure that the duplicates are in the correct location
|
328
|
+
yield tmpApplyCommand.apply(this.editor);
|
329
|
+
// Show items again
|
330
|
+
this.addRemoveSelectionFromImage(true);
|
331
|
+
}
|
332
|
+
const duplicateCommand = new Duplicate(this.selectedElems);
|
333
|
+
if (wasTransforming) {
|
334
|
+
// Move the selected objects back to the correct location.
|
335
|
+
yield (tmpApplyCommand === null || tmpApplyCommand === void 0 ? void 0 : tmpApplyCommand.unapply(this.editor));
|
336
|
+
this.addRemoveSelectionFromImage(false);
|
337
|
+
this.previewTransformCmds();
|
338
|
+
this.updateUI();
|
339
|
+
}
|
340
|
+
return duplicateCommand;
|
341
|
+
});
|
306
342
|
}
|
307
343
|
addTo(elem) {
|
308
344
|
if (this.container.parentElement) {
|
@@ -323,6 +359,7 @@ export default class Selection {
|
|
323
359
|
this.hasParent = false;
|
324
360
|
}
|
325
361
|
setSelectedObjects(objects, bbox) {
|
362
|
+
this.addRemoveSelectionFromImage(true);
|
326
363
|
this.originalRegion = bbox;
|
327
364
|
this.selectedElems = objects.filter(object => object.isSelectable());
|
328
365
|
this.updateUI();
|
@@ -17,6 +17,7 @@ export default class SelectionHandle {
|
|
17
17
|
private readonly onDragUpdate;
|
18
18
|
private readonly onDragEnd;
|
19
19
|
private element;
|
20
|
+
private snapToGrid;
|
20
21
|
constructor(shape: HandleShape, parentSide: Vec2, parent: Selection, onDragStart: DragStartCallback, onDragUpdate: DragUpdateCallback, onDragEnd: DragEndCallback);
|
21
22
|
/**
|
22
23
|
* Adds this to `container`, where `conatiner` should be the background/selection
|
@@ -32,4 +33,6 @@ export default class SelectionHandle {
|
|
32
33
|
handleDragStart(pointer: Pointer): void;
|
33
34
|
handleDragUpdate(pointer: Pointer): void;
|
34
35
|
handleDragEnd(): void;
|
36
|
+
setSnapToGrid(snap: boolean): void;
|
37
|
+
isSnappingToGrid(): boolean;
|
35
38
|
}
|
@@ -12,10 +12,12 @@ export default class SelectionTool extends BaseTool {
|
|
12
12
|
private lastEvtTarget;
|
13
13
|
private expandingSelectionBox;
|
14
14
|
private shiftKeyPressed;
|
15
|
+
private ctrlKeyPressed;
|
15
16
|
constructor(editor: Editor, description: string);
|
16
17
|
private makeSelectionBox;
|
18
|
+
private snapSelectionToGrid;
|
17
19
|
private selectionBoxHandlingEvt;
|
18
|
-
onPointerDown(
|
20
|
+
onPointerDown({ allPointers, current }: PointerEvt): boolean;
|
19
21
|
onPointerMove(event: PointerEvt): void;
|
20
22
|
private onSelectionUpdated;
|
21
23
|
private onGestureEnd;
|