js-draw 0.22.0 → 0.23.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 +14 -0
- package/dist/bundle.js +3 -3
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/src/Pointer.d.ts +3 -1
- package/dist/cjs/src/Pointer.js +27 -2
- package/dist/cjs/src/commands/Duplicate.d.ts +24 -0
- package/dist/cjs/src/commands/Duplicate.js +26 -0
- package/dist/cjs/src/commands/Erase.d.ts +20 -0
- package/dist/cjs/src/commands/Erase.js +20 -0
- package/dist/cjs/src/commands/invertCommand.js +6 -0
- package/dist/cjs/src/commands/uniteCommands.js +14 -13
- package/dist/cjs/src/components/BackgroundComponent.js +9 -2
- package/dist/cjs/src/components/ImageComponent.d.ts +6 -6
- package/dist/cjs/src/components/ImageComponent.js +17 -12
- package/dist/cjs/src/components/lib.d.ts +1 -1
- package/dist/cjs/src/components/lib.js +2 -2
- package/dist/cjs/src/components/localization.d.ts +1 -0
- package/dist/cjs/src/components/localization.js +1 -0
- package/dist/cjs/src/math/Vec2.d.ts +20 -0
- package/dist/cjs/src/math/Vec2.js +20 -0
- package/dist/cjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
- package/dist/cjs/src/rendering/renderers/AbstractRenderer.js +21 -3
- package/dist/cjs/src/rendering/renderers/CanvasRenderer.js +12 -7
- package/dist/cjs/src/toolbar/localization.d.ts +1 -0
- package/dist/cjs/src/toolbar/localization.js +1 -0
- package/dist/cjs/src/toolbar/makeColorInput.js +8 -0
- package/dist/cjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
- package/dist/cjs/src/toolbar/widgets/BaseWidget.js +29 -6
- package/dist/cjs/src/tools/Pen.d.ts +4 -0
- package/dist/cjs/src/tools/Pen.js +24 -1
- package/dist/cjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
- package/dist/cjs/src/tools/SelectionTool/SelectionTool.js +8 -0
- package/dist/cjs/src/util/waitForAll.d.ts +6 -0
- package/dist/cjs/src/util/waitForAll.js +17 -0
- package/dist/mjs/src/Pointer.d.ts +3 -1
- package/dist/mjs/src/Pointer.mjs +27 -2
- package/dist/mjs/src/commands/Duplicate.d.ts +24 -0
- package/dist/mjs/src/commands/Duplicate.mjs +26 -0
- package/dist/mjs/src/commands/Erase.d.ts +20 -0
- package/dist/mjs/src/commands/Erase.mjs +20 -0
- package/dist/mjs/src/commands/invertCommand.mjs +6 -0
- package/dist/mjs/src/commands/uniteCommands.mjs +14 -13
- package/dist/mjs/src/components/BackgroundComponent.mjs +9 -2
- package/dist/mjs/src/components/ImageComponent.d.ts +6 -6
- package/dist/mjs/src/components/ImageComponent.mjs +17 -12
- package/dist/mjs/src/components/lib.d.ts +1 -1
- package/dist/mjs/src/components/lib.mjs +3 -1
- package/dist/mjs/src/components/localization.d.ts +1 -0
- package/dist/mjs/src/components/localization.mjs +1 -0
- package/dist/mjs/src/math/Vec2.d.ts +20 -0
- package/dist/mjs/src/math/Vec2.mjs +20 -0
- package/dist/mjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
- package/dist/mjs/src/rendering/renderers/AbstractRenderer.mjs +21 -3
- package/dist/mjs/src/rendering/renderers/CanvasRenderer.mjs +12 -7
- package/dist/mjs/src/toolbar/localization.d.ts +1 -0
- package/dist/mjs/src/toolbar/localization.mjs +1 -0
- package/dist/mjs/src/toolbar/makeColorInput.mjs +8 -0
- package/dist/mjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
- package/dist/mjs/src/toolbar/widgets/BaseWidget.mjs +29 -6
- package/dist/mjs/src/tools/Pen.d.ts +4 -0
- package/dist/mjs/src/tools/Pen.mjs +24 -1
- package/dist/mjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
- package/dist/mjs/src/tools/SelectionTool/SelectionTool.mjs +8 -0
- package/dist/mjs/src/util/waitForAll.d.ts +6 -0
- package/dist/mjs/src/util/waitForAll.mjs +15 -0
- package/package.json +12 -12
- package/src/toolbar/toolbar.css +35 -1
@@ -7,13 +7,33 @@ exports.Vec2 = void 0;
|
|
7
7
|
const Vec3_1 = __importDefault(require("./Vec3"));
|
8
8
|
var Vec2;
|
9
9
|
(function (Vec2) {
|
10
|
+
/**
|
11
|
+
* Creates a `Vec2` from an x and y coordinate.
|
12
|
+
*
|
13
|
+
* For example,
|
14
|
+
* ```ts
|
15
|
+
* const v = Vec2.of(3, 4); // x=3, y=4.
|
16
|
+
* ```
|
17
|
+
*/
|
10
18
|
Vec2.of = (x, y) => {
|
11
19
|
return Vec3_1.default.of(x, y, 0);
|
12
20
|
};
|
21
|
+
/**
|
22
|
+
* Creates a `Vec2` from an object containing x and y coordinates.
|
23
|
+
*
|
24
|
+
* For example,
|
25
|
+
* ```ts
|
26
|
+
* const v1 = Vec2.ofXY({ x: 3, y: 4.5 });
|
27
|
+
* const v2 = Vec2.ofXY({ x: -123.4, y: 1 });
|
28
|
+
* ```
|
29
|
+
*/
|
13
30
|
Vec2.ofXY = ({ x, y }) => {
|
14
31
|
return Vec3_1.default.of(x, y, 0);
|
15
32
|
};
|
33
|
+
/** A vector of length 1 in the X direction (→). */
|
16
34
|
Vec2.unitX = Vec2.of(1, 0);
|
35
|
+
/** A vector of length 1 in the Y direction (↑). */
|
17
36
|
Vec2.unitY = Vec2.of(0, 1);
|
37
|
+
/** The zero vector: A vector with x=0, y=0. */
|
18
38
|
Vec2.zero = Vec2.of(0, 0);
|
19
39
|
})(Vec2 = exports.Vec2 || (exports.Vec2 = {}));
|
@@ -19,6 +19,11 @@ export interface RenderableImage {
|
|
19
19
|
base64Url: string;
|
20
20
|
label?: string;
|
21
21
|
}
|
22
|
+
/**
|
23
|
+
* Abstract base class for renderers.
|
24
|
+
*
|
25
|
+
* @see {@link EditorImage.render}
|
26
|
+
*/
|
22
27
|
export default abstract class AbstractRenderer {
|
23
28
|
private viewport;
|
24
29
|
private selfTransform;
|
@@ -40,9 +45,21 @@ export default abstract class AbstractRenderer {
|
|
40
45
|
protected objectLevel: number;
|
41
46
|
private currentPaths;
|
42
47
|
private flushPath;
|
48
|
+
/**
|
49
|
+
* Draws a styled path. If within an object started by {@link startObject},
|
50
|
+
* the resultant path may not be visible until {@link endObject} is called.
|
51
|
+
*/
|
43
52
|
drawPath(path: RenderablePathSpec): void;
|
44
53
|
drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
|
54
|
+
/** Draws a filled rectangle. */
|
45
55
|
fillRect(rect: Rect2, fill: Color4): void;
|
56
|
+
/**
|
57
|
+
* This should be called whenever a new object is being drawn.
|
58
|
+
*
|
59
|
+
* @param _boundingBox The bounding box of the object to be drawn.
|
60
|
+
* @param _clip Whether content outside `_boundingBox` should be drawn. Renderers
|
61
|
+
* that override this method are not required to support `_clip`.
|
62
|
+
*/
|
46
63
|
startObject(_boundingBox: Rect2, _clip?: boolean): void;
|
47
64
|
/**
|
48
65
|
* Notes the end of an object.
|
@@ -26,6 +26,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
const Path_1 = __importStar(require("../../math/Path"));
|
27
27
|
const Vec2_1 = require("../../math/Vec2");
|
28
28
|
const RenderingStyle_1 = require("../RenderingStyle");
|
29
|
+
/**
|
30
|
+
* Abstract base class for renderers.
|
31
|
+
*
|
32
|
+
* @see {@link EditorImage.render}
|
33
|
+
*/
|
29
34
|
class AbstractRenderer {
|
30
35
|
constructor(viewport) {
|
31
36
|
this.viewport = viewport;
|
@@ -74,7 +79,12 @@ class AbstractRenderer {
|
|
74
79
|
if (lastStyle) {
|
75
80
|
this.endPath(lastStyle);
|
76
81
|
}
|
82
|
+
this.currentPaths = [];
|
77
83
|
}
|
84
|
+
/**
|
85
|
+
* Draws a styled path. If within an object started by {@link startObject},
|
86
|
+
* the resultant path may not be visible until {@link endObject} is called.
|
87
|
+
*/
|
78
88
|
drawPath(path) {
|
79
89
|
// If we're being called outside of an object,
|
80
90
|
// we can't delay rendering
|
@@ -95,14 +105,22 @@ class AbstractRenderer {
|
|
95
105
|
const path = Path_1.default.fromRect(rect, lineWidth);
|
96
106
|
this.drawPath(path.toRenderable(lineFill));
|
97
107
|
}
|
98
|
-
|
108
|
+
/** Draws a filled rectangle. */
|
99
109
|
fillRect(rect, fill) {
|
100
110
|
const path = Path_1.default.fromRect(rect);
|
101
111
|
this.drawPath(path.toRenderable({ fill }));
|
102
112
|
}
|
103
|
-
|
104
|
-
|
113
|
+
/**
|
114
|
+
* This should be called whenever a new object is being drawn.
|
115
|
+
*
|
116
|
+
* @param _boundingBox The bounding box of the object to be drawn.
|
117
|
+
* @param _clip Whether content outside `_boundingBox` should be drawn. Renderers
|
118
|
+
* that override this method are not required to support `_clip`.
|
119
|
+
*/
|
105
120
|
startObject(_boundingBox, _clip) {
|
121
|
+
if (this.objectLevel > 0) {
|
122
|
+
this.flushPath();
|
123
|
+
}
|
106
124
|
this.currentPaths = [];
|
107
125
|
this.objectLevel++;
|
108
126
|
}
|
@@ -180,14 +180,19 @@ class CanvasRenderer extends AbstractRenderer_1.default {
|
|
180
180
|
super.startObject(boundingBox);
|
181
181
|
this.currentObjectBBox = boundingBox;
|
182
182
|
if (!this.ignoringObject && clip) {
|
183
|
-
|
184
|
-
|
185
|
-
this.
|
186
|
-
|
187
|
-
|
188
|
-
this.ctx.
|
183
|
+
// Don't clip if it would only remove content already trimmed by
|
184
|
+
// the edge of the screen.
|
185
|
+
const clippedIsOutsideScreen = boundingBox.containsRect(this.getViewport().visibleRect);
|
186
|
+
if (!clippedIsOutsideScreen) {
|
187
|
+
this.clipLevels.push(this.objectLevel);
|
188
|
+
this.ctx.save();
|
189
|
+
this.ctx.beginPath();
|
190
|
+
for (const corner of boundingBox.corners) {
|
191
|
+
const screenCorner = this.canvasToScreen(corner);
|
192
|
+
this.ctx.lineTo(screenCorner.x, screenCorner.y);
|
193
|
+
}
|
194
|
+
this.ctx.clip();
|
189
195
|
}
|
190
|
-
this.ctx.clip();
|
191
196
|
}
|
192
197
|
}
|
193
198
|
endObject() {
|
@@ -26,6 +26,7 @@ exports.defaultToolbarLocalization = {
|
|
26
26
|
selectPenType: 'Pen type: ',
|
27
27
|
pickColorFromScreen: 'Pick color from screen',
|
28
28
|
clickToPickColorAnnouncement: 'Click on the screen to pick a color',
|
29
|
+
colorSelectionCanceledAnnouncement: 'Color selection canceled',
|
29
30
|
selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
|
30
31
|
documentProperties: 'Page',
|
31
32
|
backgroundColor: 'Background Color: ',
|
@@ -48,6 +48,11 @@ const makeColorInput = (editor, onColorChange) => {
|
|
48
48
|
open: true,
|
49
49
|
});
|
50
50
|
pipetteController.cancel();
|
51
|
+
// Focus the Coloris color picker, if it exists.
|
52
|
+
// Don't focus the text input within the color picker, however,
|
53
|
+
// as this displays a keyboard on mobile devices.
|
54
|
+
const colorPickerElem = document.querySelector('#clr-picker #clr-hue-slider');
|
55
|
+
colorPickerElem === null || colorPickerElem === void 0 ? void 0 : colorPickerElem.focus();
|
51
56
|
});
|
52
57
|
colorInput.addEventListener('close', () => {
|
53
58
|
editor.notifier.dispatch(types_1.EditorEventType.ColorPickerToggled, {
|
@@ -55,6 +60,8 @@ const makeColorInput = (editor, onColorChange) => {
|
|
55
60
|
open: false,
|
56
61
|
});
|
57
62
|
onInputEnd();
|
63
|
+
// Restore focus to the input that opened the color picker
|
64
|
+
colorInput.focus();
|
58
65
|
});
|
59
66
|
const setColorInputValue = (color) => {
|
60
67
|
if (typeof color === 'object') {
|
@@ -101,6 +108,7 @@ const addPipetteTool = (editor, container, onColorChange) => {
|
|
101
108
|
// If already picking, cancel it.
|
102
109
|
if (pipetteButton.classList.contains('active')) {
|
103
110
|
endColorSelectMode();
|
111
|
+
editor.announceForAccessibility(editor.localization.colorSelectionCanceledAnnouncement);
|
104
112
|
return;
|
105
113
|
}
|
106
114
|
pipetteTool === null || pipetteTool === void 0 ? void 0 : pipetteTool.setColorListener(pipetteColorPreview, pipetteColorSelect);
|
@@ -46,6 +46,7 @@ export default abstract class BaseWidget {
|
|
46
46
|
protected updateIcon(): void;
|
47
47
|
setDisabled(disabled: boolean): void;
|
48
48
|
setSelected(selected: boolean): void;
|
49
|
+
private hideDropdownTimeout;
|
49
50
|
protected setDropdownVisible(visible: boolean): void;
|
50
51
|
canBeInOverflowMenu(): boolean;
|
51
52
|
getButtonWidth(): number;
|
@@ -28,6 +28,7 @@ class BaseWidget {
|
|
28
28
|
this.subWidgets = {};
|
29
29
|
this.toplevel = true;
|
30
30
|
this.toolbarWidgetToggleListener = null;
|
31
|
+
this.hideDropdownTimeout = null;
|
31
32
|
this.localizationTable = localizationTable !== null && localizationTable !== void 0 ? localizationTable : editor.localization;
|
32
33
|
this.icon = null;
|
33
34
|
this.container = document.createElement('div');
|
@@ -216,13 +217,18 @@ class BaseWidget {
|
|
216
217
|
if (currentlySelected === selected) {
|
217
218
|
return;
|
218
219
|
}
|
220
|
+
// Ensure that accessibility tools check and read the value of
|
221
|
+
// aria-checked.
|
222
|
+
// TODO: Ensure that 'role' is set to 'switch' by default for selectable
|
223
|
+
// buttons.
|
224
|
+
this.button.setAttribute('role', 'switch');
|
219
225
|
if (selected) {
|
220
226
|
this.container.classList.add('selected');
|
221
|
-
this.button.
|
227
|
+
this.button.setAttribute('aria-checked', 'true');
|
222
228
|
}
|
223
229
|
else {
|
224
230
|
this.container.classList.remove('selected');
|
225
|
-
this.button.
|
231
|
+
this.button.setAttribute('aria-checked', 'false');
|
226
232
|
}
|
227
233
|
}
|
228
234
|
setDropdownVisible(visible) {
|
@@ -230,6 +236,13 @@ class BaseWidget {
|
|
230
236
|
if (currentlyVisible === visible) {
|
231
237
|
return;
|
232
238
|
}
|
239
|
+
// If waiting to hide the dropdown, cancel it.
|
240
|
+
if (this.hideDropdownTimeout) {
|
241
|
+
clearTimeout(this.hideDropdownTimeout);
|
242
|
+
this.hideDropdownTimeout = null;
|
243
|
+
this.dropdownContainer.classList.remove('hiding');
|
244
|
+
}
|
245
|
+
const animationDuration = 150; // ms
|
233
246
|
if (visible) {
|
234
247
|
this.dropdownContainer.classList.remove('hidden');
|
235
248
|
this.container.classList.add('dropdownVisible');
|
@@ -240,10 +253,20 @@ class BaseWidget {
|
|
240
253
|
});
|
241
254
|
}
|
242
255
|
else {
|
243
|
-
this.dropdownContainer.classList.add('hidden');
|
244
256
|
this.container.classList.remove('dropdownVisible');
|
245
257
|
this.editor.announceForAccessibility(this.localizationTable.dropdownHidden(this.getTitle()));
|
258
|
+
this.dropdownContainer.classList.add('hiding');
|
259
|
+
// Hide the dropdown *slightly* before the animation finishes. This
|
260
|
+
// prevents flickering in some browsers.
|
261
|
+
const hideDelay = animationDuration * 0.95;
|
262
|
+
this.hideDropdownTimeout = setTimeout(() => {
|
263
|
+
this.dropdownContainer.classList.add('hidden');
|
264
|
+
this.dropdownContainer.classList.remove('hiding');
|
265
|
+
}, hideDelay);
|
246
266
|
}
|
267
|
+
// Animate
|
268
|
+
const animationName = `var(--dropdown-${visible ? 'show' : 'hide'}-animation)`;
|
269
|
+
this.dropdownContainer.style.animation = `${animationDuration}ms ease ${animationName}`;
|
247
270
|
this.repositionDropdown();
|
248
271
|
}
|
249
272
|
canBeInOverflowMenu() {
|
@@ -262,11 +285,11 @@ class BaseWidget {
|
|
262
285
|
const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
|
263
286
|
const screenWidth = document.body.clientWidth;
|
264
287
|
if (dropdownBBox.left > screenWidth / 2) {
|
265
|
-
|
266
|
-
|
288
|
+
// Use .translate so as not to conflict with CSS animating the
|
289
|
+
// transform property.
|
290
|
+
this.dropdownContainer.style.translate = `calc(${this.button.clientWidth + 'px'} - 100%) 0`;
|
267
291
|
}
|
268
292
|
else {
|
269
|
-
this.dropdownContainer.style.marginLeft = '';
|
270
293
|
this.dropdownContainer.style.transform = '';
|
271
294
|
}
|
272
295
|
}
|
@@ -14,9 +14,12 @@ export default class Pen extends BaseTool {
|
|
14
14
|
private builderFactory;
|
15
15
|
protected builder: ComponentBuilder | null;
|
16
16
|
private lastPoint;
|
17
|
+
private startPoint;
|
17
18
|
private ctrlKeyPressed;
|
19
|
+
private shiftKeyPressed;
|
18
20
|
constructor(editor: Editor, description: string, style: PenStyle, builderFactory?: ComponentBuilderFactory);
|
19
21
|
private getPressureMultiplier;
|
22
|
+
private xyAxesSnap;
|
20
23
|
protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
|
21
24
|
protected previewStroke(): void;
|
22
25
|
protected addPointToStroke(point: StrokeDataPoint): void;
|
@@ -34,6 +37,7 @@ export default class Pen extends BaseTool {
|
|
34
37
|
getStrokeFactory(): ComponentBuilderFactory;
|
35
38
|
setEnabled(enabled: boolean): void;
|
36
39
|
private isSnappingToGrid;
|
40
|
+
private isAngleLocked;
|
37
41
|
onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
|
38
42
|
onKeyUp({ key }: KeyUpEvent): boolean;
|
39
43
|
}
|
@@ -16,14 +16,26 @@ class Pen extends BaseTool_1.default {
|
|
16
16
|
this.builderFactory = builderFactory;
|
17
17
|
this.builder = null;
|
18
18
|
this.lastPoint = null;
|
19
|
+
this.startPoint = null;
|
19
20
|
this.ctrlKeyPressed = false;
|
21
|
+
this.shiftKeyPressed = false;
|
20
22
|
}
|
21
23
|
getPressureMultiplier() {
|
22
24
|
return 1 / this.editor.viewport.getScaleFactor() * this.style.thickness;
|
23
25
|
}
|
26
|
+
// Snap the given pointer to the nearer of the x/y axes.
|
27
|
+
xyAxesSnap(pointer) {
|
28
|
+
if (!this.startPoint) {
|
29
|
+
return pointer;
|
30
|
+
}
|
31
|
+
return pointer.lockedToXYAxes(this.startPoint.pos, this.editor.viewport);
|
32
|
+
}
|
24
33
|
// Converts a `pointer` to a `StrokeDataPoint`.
|
25
34
|
toStrokePoint(pointer) {
|
26
35
|
var _a;
|
36
|
+
if (this.isAngleLocked() && this.lastPoint) {
|
37
|
+
pointer = this.xyAxesSnap(pointer);
|
38
|
+
}
|
27
39
|
if (this.isSnappingToGrid()) {
|
28
40
|
pointer = pointer.snappedToGrid(this.editor.viewport);
|
29
41
|
}
|
@@ -69,7 +81,8 @@ class Pen extends BaseTool_1.default {
|
|
69
81
|
}
|
70
82
|
}
|
71
83
|
if ((allPointers.length === 1 && !isEraser) || anyDeviceIsStylus) {
|
72
|
-
this.
|
84
|
+
this.startPoint = this.toStrokePoint(current);
|
85
|
+
this.builder = this.builderFactory(this.startPoint, this.editor.viewport);
|
73
86
|
return true;
|
74
87
|
}
|
75
88
|
return false;
|
@@ -109,6 +122,7 @@ class Pen extends BaseTool_1.default {
|
|
109
122
|
}
|
110
123
|
}
|
111
124
|
this.builder = null;
|
125
|
+
this.lastPoint = null;
|
112
126
|
this.editor.clearWetInk();
|
113
127
|
}
|
114
128
|
noteUpdated() {
|
@@ -143,6 +157,7 @@ class Pen extends BaseTool_1.default {
|
|
143
157
|
this.ctrlKeyPressed = false;
|
144
158
|
}
|
145
159
|
isSnappingToGrid() { return this.ctrlKeyPressed; }
|
160
|
+
isAngleLocked() { return this.shiftKeyPressed; }
|
146
161
|
onKeyPress({ key, ctrlKey }) {
|
147
162
|
key = key.toLowerCase();
|
148
163
|
let newThickness;
|
@@ -161,6 +176,10 @@ class Pen extends BaseTool_1.default {
|
|
161
176
|
this.ctrlKeyPressed = true;
|
162
177
|
return true;
|
163
178
|
}
|
179
|
+
if (key === 'shift') {
|
180
|
+
this.shiftKeyPressed = true;
|
181
|
+
return true;
|
182
|
+
}
|
164
183
|
// Ctrl+Z: End the stroke so that it can be undone/redone.
|
165
184
|
if (key === 'z' && ctrlKey && this.builder) {
|
166
185
|
this.finalizeStroke();
|
@@ -173,6 +192,10 @@ class Pen extends BaseTool_1.default {
|
|
173
192
|
this.ctrlKeyPressed = false;
|
174
193
|
return true;
|
175
194
|
}
|
195
|
+
if (key === 'shift') {
|
196
|
+
this.shiftKeyPressed = false;
|
197
|
+
return true;
|
198
|
+
}
|
176
199
|
return false;
|
177
200
|
}
|
178
201
|
}
|
@@ -20,6 +20,7 @@ class SelectionTool extends BaseTool_1.default {
|
|
20
20
|
super(editor.notifier, description);
|
21
21
|
this.editor = editor;
|
22
22
|
this.lastEvtTarget = null;
|
23
|
+
this.startPoint = null; // canvas position
|
23
24
|
this.expandingSelectionBox = false;
|
24
25
|
this.shiftKeyPressed = false;
|
25
26
|
this.ctrlKeyPressed = false;
|
@@ -30,6 +31,9 @@ class SelectionTool extends BaseTool_1.default {
|
|
30
31
|
this.handleOverlay.classList.add('handleOverlay');
|
31
32
|
editor.notifier.on(types_1.EditorEventType.ViewportChanged, _data => {
|
32
33
|
var _a;
|
34
|
+
// The selection box could be using the wet ink display if its transformation
|
35
|
+
// hasn't been finalized yet. Clear before updating the UI.
|
36
|
+
this.editor.clearWetInk();
|
33
37
|
(_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.updateUI();
|
34
38
|
});
|
35
39
|
this.editor.handleKeyEventsFrom(this.handleOverlay);
|
@@ -67,6 +71,7 @@ class SelectionTool extends BaseTool_1.default {
|
|
67
71
|
current = current.snappedToGrid(this.editor.viewport);
|
68
72
|
}
|
69
73
|
if (allPointers.length === 1 && current.isPrimary) {
|
74
|
+
this.startPoint = current.canvasPos;
|
70
75
|
let transforming = false;
|
71
76
|
if (this.lastEvtTarget && this.selectionBox) {
|
72
77
|
if (snapToGrid) {
|
@@ -92,6 +97,9 @@ class SelectionTool extends BaseTool_1.default {
|
|
92
97
|
if (!this.selectionBox)
|
93
98
|
return;
|
94
99
|
let currentPointer = event.current;
|
100
|
+
if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
|
101
|
+
currentPointer = currentPointer.lockedToXYAxes(this.startPoint, this.editor.viewport);
|
102
|
+
}
|
95
103
|
if (this.ctrlKeyPressed) {
|
96
104
|
currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
|
97
105
|
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
/**
|
4
|
+
* Resolves when all given promises have resolved. If no promises are given,
|
5
|
+
* does not return a Promise.
|
6
|
+
*/
|
7
|
+
const waitForAll = (results) => {
|
8
|
+
// If any are Promises...
|
9
|
+
if (results.some(command => command && command['then'])) {
|
10
|
+
// Wait for all commands to finish.
|
11
|
+
return Promise.all(results)
|
12
|
+
// Ensure we return a Promise<void> and not a Promise<void[]>
|
13
|
+
.then(() => { });
|
14
|
+
}
|
15
|
+
return;
|
16
|
+
};
|
17
|
+
exports.default = waitForAll;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Point2 } from './math/Vec2';
|
1
|
+
import { Point2, Vec2 } from './math/Vec2';
|
2
2
|
import Viewport from './Viewport';
|
3
3
|
export declare enum PointerDevice {
|
4
4
|
Pen = 0,
|
@@ -19,6 +19,8 @@ export default class Pointer {
|
|
19
19
|
readonly timeStamp: number;
|
20
20
|
private constructor();
|
21
21
|
snappedToGrid(viewport: Viewport): Pointer;
|
22
|
+
lockedToXYAxes(originPoint: Vec2, viewport: Viewport): Pointer;
|
23
|
+
withCanvasPosition(canvasPos: Point2, viewport: Viewport): Pointer;
|
22
24
|
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer;
|
23
25
|
static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
|
24
26
|
}
|
package/dist/mjs/src/Pointer.mjs
CHANGED
@@ -36,8 +36,33 @@ export default class Pointer {
|
|
36
36
|
// this.
|
37
37
|
snappedToGrid(viewport) {
|
38
38
|
const snappedCanvasPos = viewport.snapToGrid(this.canvasPos);
|
39
|
-
|
40
|
-
|
39
|
+
return this.withCanvasPosition(snappedCanvasPos, viewport);
|
40
|
+
}
|
41
|
+
// Snap this pointer to the X or Y axis (whichever is closer), where (0,0)
|
42
|
+
// is considered to be at `originPoint`.
|
43
|
+
// @internal
|
44
|
+
lockedToXYAxes(originPoint, viewport) {
|
45
|
+
const current = this.canvasPos;
|
46
|
+
const currentFromStart = current.minus(originPoint);
|
47
|
+
// Determine whether the last point was closer to being on the
|
48
|
+
// x- or y- axis.
|
49
|
+
const projOntoXAxis = Vec2.unitX.times(currentFromStart.x);
|
50
|
+
const projOntoYAxis = Vec2.unitY.times(currentFromStart.y);
|
51
|
+
let pos;
|
52
|
+
if (currentFromStart.dot(projOntoXAxis) > currentFromStart.dot(projOntoYAxis)) {
|
53
|
+
pos = projOntoXAxis;
|
54
|
+
}
|
55
|
+
else {
|
56
|
+
pos = projOntoYAxis;
|
57
|
+
}
|
58
|
+
pos = pos.plus(originPoint);
|
59
|
+
return this.withCanvasPosition(pos, viewport);
|
60
|
+
}
|
61
|
+
// Returns a copy of this pointer with a new position. The screen position is determined
|
62
|
+
// by the given `canvasPos`.
|
63
|
+
withCanvasPosition(canvasPos, viewport) {
|
64
|
+
const screenPos = viewport.canvasToScreen(canvasPos);
|
65
|
+
return new Pointer(screenPos, canvasPos, this.pressure, this.isPrimary, this.down, this.device, this.id, this.timeStamp);
|
41
66
|
}
|
42
67
|
// Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
|
43
68
|
// considered the top left of `relativeTo`.
|
@@ -2,6 +2,29 @@ import AbstractComponent from '../components/AbstractComponent';
|
|
2
2
|
import Editor from '../Editor';
|
3
3
|
import { EditorLocalization } from '../localization';
|
4
4
|
import SerializableCommand from './SerializableCommand';
|
5
|
+
/**
|
6
|
+
* A command that duplicates the {@link AbstractComponent}s it's given. This command
|
7
|
+
* is the reverse of an {@link Erase} command.
|
8
|
+
*
|
9
|
+
* @example
|
10
|
+
* ```ts
|
11
|
+
* // Given some editor...
|
12
|
+
*
|
13
|
+
* // Find all elements intersecting the rectangle with top left (0,0) and
|
14
|
+
* // (width,height)=(100,100).
|
15
|
+
* const elems = editor.image.getElementsIntersectingRegion(
|
16
|
+
* new Rect2(0, 0, 100, 100)
|
17
|
+
* );
|
18
|
+
*
|
19
|
+
* // Create a command that, when applied, will duplicate the elements.
|
20
|
+
* const duplicateElems = new Duplicate(elems);
|
21
|
+
*
|
22
|
+
* // Apply the command (and make it undoable)
|
23
|
+
* editor.dispatch(duplicateElems);
|
24
|
+
* ```
|
25
|
+
*
|
26
|
+
* @see {@link Editor.dispatch} {@link EditorImage.getElementsIntersectingRegion}
|
27
|
+
*/
|
5
28
|
export default class Duplicate extends SerializableCommand {
|
6
29
|
private toDuplicate;
|
7
30
|
private duplicates;
|
@@ -9,6 +32,7 @@ export default class Duplicate extends SerializableCommand {
|
|
9
32
|
constructor(toDuplicate: AbstractComponent[]);
|
10
33
|
apply(editor: Editor): void;
|
11
34
|
unapply(editor: Editor): void;
|
35
|
+
onDrop(editor: Editor): void;
|
12
36
|
description(_editor: Editor, localizationTable: EditorLocalization): string;
|
13
37
|
protected serializeToJSON(): string[];
|
14
38
|
}
|
@@ -1,6 +1,29 @@
|
|
1
1
|
import describeComponentList from '../components/util/describeComponentList.mjs';
|
2
2
|
import Erase from './Erase.mjs';
|
3
3
|
import SerializableCommand from './SerializableCommand.mjs';
|
4
|
+
/**
|
5
|
+
* A command that duplicates the {@link AbstractComponent}s it's given. This command
|
6
|
+
* is the reverse of an {@link Erase} command.
|
7
|
+
*
|
8
|
+
* @example
|
9
|
+
* ```ts
|
10
|
+
* // Given some editor...
|
11
|
+
*
|
12
|
+
* // Find all elements intersecting the rectangle with top left (0,0) and
|
13
|
+
* // (width,height)=(100,100).
|
14
|
+
* const elems = editor.image.getElementsIntersectingRegion(
|
15
|
+
* new Rect2(0, 0, 100, 100)
|
16
|
+
* );
|
17
|
+
*
|
18
|
+
* // Create a command that, when applied, will duplicate the elements.
|
19
|
+
* const duplicateElems = new Duplicate(elems);
|
20
|
+
*
|
21
|
+
* // Apply the command (and make it undoable)
|
22
|
+
* editor.dispatch(duplicateElems);
|
23
|
+
* ```
|
24
|
+
*
|
25
|
+
* @see {@link Editor.dispatch} {@link EditorImage.getElementsIntersectingRegion}
|
26
|
+
*/
|
4
27
|
export default class Duplicate extends SerializableCommand {
|
5
28
|
constructor(toDuplicate) {
|
6
29
|
super('duplicate');
|
@@ -14,6 +37,9 @@ export default class Duplicate extends SerializableCommand {
|
|
14
37
|
unapply(editor) {
|
15
38
|
this.reverse.apply(editor);
|
16
39
|
}
|
40
|
+
onDrop(editor) {
|
41
|
+
this.reverse.onDrop(editor);
|
42
|
+
}
|
17
43
|
description(_editor, localizationTable) {
|
18
44
|
var _a;
|
19
45
|
if (this.duplicates.length === 0) {
|
@@ -2,6 +2,26 @@ import AbstractComponent from '../components/AbstractComponent';
|
|
2
2
|
import Editor from '../Editor';
|
3
3
|
import { EditorLocalization } from '../localization';
|
4
4
|
import SerializableCommand from './SerializableCommand';
|
5
|
+
/**
|
6
|
+
* Removes the given {@link AbstractComponent}s from the image.
|
7
|
+
*
|
8
|
+
* @example
|
9
|
+
* ```ts
|
10
|
+
* // Given some editor...
|
11
|
+
*
|
12
|
+
* // Find all elements intersecting the rectangle with top left (-10,-30) and
|
13
|
+
* // (width,height)=(50,100).
|
14
|
+
* const elems = editor.image.getElementsIntersectingRegion(
|
15
|
+
* new Rect2(-10, -30, 50, 100)
|
16
|
+
* );
|
17
|
+
*
|
18
|
+
* // Create a command that erases [elems] when applied
|
19
|
+
* const eraseElemsCmd = new Erase(elems);
|
20
|
+
*
|
21
|
+
* // Apply the command (and make it undoable)
|
22
|
+
* editor.dispatch(eraseElemsCmd);
|
23
|
+
* ```
|
24
|
+
*/
|
5
25
|
export default class Erase extends SerializableCommand {
|
6
26
|
private toRemove;
|
7
27
|
private applied;
|
@@ -1,6 +1,26 @@
|
|
1
1
|
import describeComponentList from '../components/util/describeComponentList.mjs';
|
2
2
|
import EditorImage from '../EditorImage.mjs';
|
3
3
|
import SerializableCommand from './SerializableCommand.mjs';
|
4
|
+
/**
|
5
|
+
* Removes the given {@link AbstractComponent}s from the image.
|
6
|
+
*
|
7
|
+
* @example
|
8
|
+
* ```ts
|
9
|
+
* // Given some editor...
|
10
|
+
*
|
11
|
+
* // Find all elements intersecting the rectangle with top left (-10,-30) and
|
12
|
+
* // (width,height)=(50,100).
|
13
|
+
* const elems = editor.image.getElementsIntersectingRegion(
|
14
|
+
* new Rect2(-10, -30, 50, 100)
|
15
|
+
* );
|
16
|
+
*
|
17
|
+
* // Create a command that erases [elems] when applied
|
18
|
+
* const eraseElemsCmd = new Erase(elems);
|
19
|
+
*
|
20
|
+
* // Apply the command (and make it undoable)
|
21
|
+
* editor.dispatch(eraseElemsCmd);
|
22
|
+
* ```
|
23
|
+
*/
|
4
24
|
export default class Erase extends SerializableCommand {
|
5
25
|
constructor(toRemove) {
|
6
26
|
super('erase');
|
@@ -15,6 +15,9 @@ const invertCommand = (command) => {
|
|
15
15
|
unapply(editor) {
|
16
16
|
command.unapply(editor);
|
17
17
|
}
|
18
|
+
onDrop(editor) {
|
19
|
+
command.onDrop(editor);
|
20
|
+
}
|
18
21
|
description(editor, localizationTable) {
|
19
22
|
return localizationTable.inverseOf(command.description(editor, localizationTable));
|
20
23
|
}
|
@@ -29,6 +32,9 @@ const invertCommand = (command) => {
|
|
29
32
|
unapply(editor) {
|
30
33
|
command.apply(editor);
|
31
34
|
}
|
35
|
+
onDrop(editor) {
|
36
|
+
command.onDrop(editor);
|
37
|
+
}
|
32
38
|
description(editor, localizationTable) {
|
33
39
|
return localizationTable.inverseOf(command.description(editor, localizationTable));
|
34
40
|
}
|