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.
Files changed (67) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/bundle.js +3 -3
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/src/Pointer.d.ts +3 -1
  5. package/dist/cjs/src/Pointer.js +27 -2
  6. package/dist/cjs/src/commands/Duplicate.d.ts +24 -0
  7. package/dist/cjs/src/commands/Duplicate.js +26 -0
  8. package/dist/cjs/src/commands/Erase.d.ts +20 -0
  9. package/dist/cjs/src/commands/Erase.js +20 -0
  10. package/dist/cjs/src/commands/invertCommand.js +6 -0
  11. package/dist/cjs/src/commands/uniteCommands.js +14 -13
  12. package/dist/cjs/src/components/BackgroundComponent.js +9 -2
  13. package/dist/cjs/src/components/ImageComponent.d.ts +6 -6
  14. package/dist/cjs/src/components/ImageComponent.js +17 -12
  15. package/dist/cjs/src/components/lib.d.ts +1 -1
  16. package/dist/cjs/src/components/lib.js +2 -2
  17. package/dist/cjs/src/components/localization.d.ts +1 -0
  18. package/dist/cjs/src/components/localization.js +1 -0
  19. package/dist/cjs/src/math/Vec2.d.ts +20 -0
  20. package/dist/cjs/src/math/Vec2.js +20 -0
  21. package/dist/cjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
  22. package/dist/cjs/src/rendering/renderers/AbstractRenderer.js +21 -3
  23. package/dist/cjs/src/rendering/renderers/CanvasRenderer.js +12 -7
  24. package/dist/cjs/src/toolbar/localization.d.ts +1 -0
  25. package/dist/cjs/src/toolbar/localization.js +1 -0
  26. package/dist/cjs/src/toolbar/makeColorInput.js +8 -0
  27. package/dist/cjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
  28. package/dist/cjs/src/toolbar/widgets/BaseWidget.js +29 -6
  29. package/dist/cjs/src/tools/Pen.d.ts +4 -0
  30. package/dist/cjs/src/tools/Pen.js +24 -1
  31. package/dist/cjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
  32. package/dist/cjs/src/tools/SelectionTool/SelectionTool.js +8 -0
  33. package/dist/cjs/src/util/waitForAll.d.ts +6 -0
  34. package/dist/cjs/src/util/waitForAll.js +17 -0
  35. package/dist/mjs/src/Pointer.d.ts +3 -1
  36. package/dist/mjs/src/Pointer.mjs +27 -2
  37. package/dist/mjs/src/commands/Duplicate.d.ts +24 -0
  38. package/dist/mjs/src/commands/Duplicate.mjs +26 -0
  39. package/dist/mjs/src/commands/Erase.d.ts +20 -0
  40. package/dist/mjs/src/commands/Erase.mjs +20 -0
  41. package/dist/mjs/src/commands/invertCommand.mjs +6 -0
  42. package/dist/mjs/src/commands/uniteCommands.mjs +14 -13
  43. package/dist/mjs/src/components/BackgroundComponent.mjs +9 -2
  44. package/dist/mjs/src/components/ImageComponent.d.ts +6 -6
  45. package/dist/mjs/src/components/ImageComponent.mjs +17 -12
  46. package/dist/mjs/src/components/lib.d.ts +1 -1
  47. package/dist/mjs/src/components/lib.mjs +3 -1
  48. package/dist/mjs/src/components/localization.d.ts +1 -0
  49. package/dist/mjs/src/components/localization.mjs +1 -0
  50. package/dist/mjs/src/math/Vec2.d.ts +20 -0
  51. package/dist/mjs/src/math/Vec2.mjs +20 -0
  52. package/dist/mjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
  53. package/dist/mjs/src/rendering/renderers/AbstractRenderer.mjs +21 -3
  54. package/dist/mjs/src/rendering/renderers/CanvasRenderer.mjs +12 -7
  55. package/dist/mjs/src/toolbar/localization.d.ts +1 -0
  56. package/dist/mjs/src/toolbar/localization.mjs +1 -0
  57. package/dist/mjs/src/toolbar/makeColorInput.mjs +8 -0
  58. package/dist/mjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
  59. package/dist/mjs/src/toolbar/widgets/BaseWidget.mjs +29 -6
  60. package/dist/mjs/src/tools/Pen.d.ts +4 -0
  61. package/dist/mjs/src/tools/Pen.mjs +24 -1
  62. package/dist/mjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
  63. package/dist/mjs/src/tools/SelectionTool/SelectionTool.mjs +8 -0
  64. package/dist/mjs/src/util/waitForAll.d.ts +6 -0
  65. package/dist/mjs/src/util/waitForAll.mjs +15 -0
  66. package/package.json +12 -12
  67. 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
- // Fills a rectangle.
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
- // Note the start of an object with the given bounding box.
104
- // Renderers are not required to support [clip]
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
- this.clipLevels.push(this.objectLevel);
184
- this.ctx.save();
185
- this.ctx.beginPath();
186
- for (const corner of boundingBox.corners) {
187
- const screenCorner = this.canvasToScreen(corner);
188
- this.ctx.lineTo(screenCorner.x, screenCorner.y);
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 @@ export interface ToolbarLocalization {
26
26
  duplicateSelection: string;
27
27
  pickColorFromScreen: string;
28
28
  clickToPickColorAnnouncement: string;
29
+ colorSelectionCanceledAnnouncement: string;
29
30
  reformatSelection: string;
30
31
  undo: string;
31
32
  redo: string;
@@ -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.ariaSelected = 'true';
227
+ this.button.setAttribute('aria-checked', 'true');
222
228
  }
223
229
  else {
224
230
  this.container.classList.remove('selected');
225
- this.button.ariaSelected = 'false';
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
- this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
266
- this.dropdownContainer.style.transform = 'translate(-100%, 0)';
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.builder = this.builderFactory(this.toStrokePoint(current), this.editor.viewport);
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
  }
@@ -10,6 +10,7 @@ export default class SelectionTool extends BaseTool {
10
10
  private prevSelectionBox;
11
11
  private selectionBox;
12
12
  private lastEvtTarget;
13
+ private startPoint;
13
14
  private expandingSelectionBox;
14
15
  private shiftKeyPressed;
15
16
  private ctrlKeyPressed;
@@ -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,6 @@
1
+ /**
2
+ * Resolves when all given promises have resolved. If no promises are given,
3
+ * does not return a Promise.
4
+ */
5
+ declare const waitForAll: (results: (Promise<void> | void)[]) => Promise<void> | void;
6
+ export default waitForAll;
@@ -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
  }
@@ -36,8 +36,33 @@ export default class Pointer {
36
36
  // this.
37
37
  snappedToGrid(viewport) {
38
38
  const snappedCanvasPos = viewport.snapToGrid(this.canvasPos);
39
- const snappedScreenPos = viewport.canvasToScreen(snappedCanvasPos);
40
- return new Pointer(snappedScreenPos, snappedCanvasPos, this.pressure, this.isPrimary, this.down, this.device, this.id, this.timeStamp);
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
  }