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.
Files changed (91) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.d.ts +13 -0
  4. package/dist/src/Color4.js +17 -0
  5. package/dist/src/Editor.d.ts +33 -18
  6. package/dist/src/Editor.js +26 -24
  7. package/dist/src/EditorImage.d.ts +12 -0
  8. package/dist/src/EditorImage.js +12 -0
  9. package/dist/src/Pointer.d.ts +1 -0
  10. package/dist/src/Pointer.js +8 -0
  11. package/dist/src/SVGLoader.d.ts +5 -0
  12. package/dist/src/SVGLoader.js +49 -36
  13. package/dist/src/Viewport.d.ts +30 -1
  14. package/dist/src/Viewport.js +39 -9
  15. package/dist/src/commands/invertCommand.js +1 -1
  16. package/dist/src/components/AbstractComponent.d.ts +20 -0
  17. package/dist/src/components/AbstractComponent.js +32 -2
  18. package/dist/src/lib.d.ts +6 -3
  19. package/dist/src/lib.js +4 -1
  20. package/dist/src/math/Mat33.d.ts +1 -1
  21. package/dist/src/math/Mat33.js +1 -1
  22. package/dist/src/rendering/Display.d.ts +9 -11
  23. package/dist/src/rendering/Display.js +12 -14
  24. package/dist/src/rendering/lib.d.ts +3 -0
  25. package/dist/src/rendering/lib.js +3 -0
  26. package/dist/src/rendering/renderers/DummyRenderer.js +2 -2
  27. package/dist/src/rendering/renderers/SVGRenderer.js +4 -0
  28. package/dist/src/toolbar/HTMLToolbar.d.ts +51 -0
  29. package/dist/src/toolbar/HTMLToolbar.js +63 -5
  30. package/dist/src/toolbar/IconProvider.d.ts +2 -2
  31. package/dist/src/toolbar/IconProvider.js +123 -35
  32. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +8 -1
  33. package/dist/src/toolbar/widgets/EraserToolWidget.js +45 -4
  34. package/dist/src/toolbar/widgets/PenToolWidget.js +2 -2
  35. package/dist/src/toolbar/widgets/SelectionToolWidget.js +12 -3
  36. package/dist/src/tools/Eraser.d.ts +10 -1
  37. package/dist/src/tools/Eraser.js +65 -13
  38. package/dist/src/tools/PanZoom.js +1 -1
  39. package/dist/src/tools/PasteHandler.d.ts +11 -4
  40. package/dist/src/tools/PasteHandler.js +12 -5
  41. package/dist/src/tools/Pen.d.ts +7 -2
  42. package/dist/src/tools/Pen.js +39 -6
  43. package/dist/src/tools/SelectionTool/Selection.d.ts +4 -1
  44. package/dist/src/tools/SelectionTool/Selection.js +64 -27
  45. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -0
  46. package/dist/src/tools/SelectionTool/SelectionHandle.js +6 -0
  47. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -1
  48. package/dist/src/tools/SelectionTool/SelectionTool.js +56 -16
  49. package/dist/src/tools/TextTool.js +10 -6
  50. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  51. package/dist/src/tools/ToolSwitcherShortcut.js +9 -3
  52. package/dist/src/tools/UndoRedoShortcut.js +2 -4
  53. package/dist/src/types.d.ts +2 -2
  54. package/package.json +2 -2
  55. package/src/Color4.test.ts +11 -0
  56. package/src/Color4.ts +23 -0
  57. package/src/Editor.ts +39 -26
  58. package/src/EditorImage.ts +12 -0
  59. package/src/Pointer.ts +19 -0
  60. package/src/SVGLoader.ts +20 -15
  61. package/src/Viewport.ts +50 -11
  62. package/src/commands/invertCommand.ts +1 -1
  63. package/src/components/AbstractComponent.ts +52 -2
  64. package/src/lib.ts +6 -3
  65. package/src/math/Mat33.ts +1 -1
  66. package/src/rendering/Display.ts +12 -15
  67. package/src/rendering/RenderingStyle.ts +1 -1
  68. package/src/rendering/lib.ts +4 -0
  69. package/src/rendering/renderers/DummyRenderer.ts +2 -3
  70. package/src/rendering/renderers/SVGRenderer.ts +4 -0
  71. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -1
  72. package/src/toolbar/HTMLToolbar.ts +81 -5
  73. package/src/toolbar/IconProvider.ts +132 -37
  74. package/src/toolbar/widgets/EraserToolWidget.ts +64 -5
  75. package/src/toolbar/widgets/PenToolWidget.ts +2 -2
  76. package/src/toolbar/widgets/SelectionToolWidget.ts +2 -2
  77. package/src/tools/Eraser.test.ts +79 -0
  78. package/src/tools/Eraser.ts +81 -17
  79. package/src/tools/PanZoom.ts +1 -1
  80. package/src/tools/PasteHandler.ts +12 -6
  81. package/src/tools/Pen.test.ts +44 -1
  82. package/src/tools/Pen.ts +53 -8
  83. package/src/tools/SelectionTool/Selection.ts +73 -23
  84. package/src/tools/SelectionTool/SelectionHandle.ts +9 -0
  85. package/src/tools/SelectionTool/SelectionTool.test.ts +138 -21
  86. package/src/tools/SelectionTool/SelectionTool.ts +70 -16
  87. package/src/tools/TextTool.ts +14 -8
  88. package/src/tools/ToolSwitcherShortcut.ts +10 -5
  89. package/src/tools/UndoRedoShortcut.ts +2 -5
  90. package/src/types.ts +2 -2
  91. 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
- fillDropdown(_dropdown) {
13
- // No dropdown associated with the eraser
14
- return false;
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}thicknessInput${PenToolWidget.idCounter++}`;
109
- objectTypeSelect.id = `${toolbarCSSPrefix}builderSelect${PenToolWidget.idCounter++}`;
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(_event: PointerEvt): void;
19
+ onPointerUp(event: PointerEvt): void;
13
20
  onGestureCancel(): void;
21
+ getThickness(): number;
22
+ setThickness(thickness: number): void;
14
23
  }
@@ -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
- onPointerDown(event) {
13
- if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
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
- onPointerMove(event) {
21
- const currentPoint = event.current.canvasPos;
22
- if (currentPoint.minus(this.lastPoint).magnitude() === 0) {
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
- onPointerUp(_event) {
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.04;
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
- // { @inheritDoc PasteHandler! }
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') {
@@ -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
- onKeyPress({ key }: KeyPressEvent): boolean;
35
+ setEnabled(enabled: boolean): void;
36
+ private isSnappingToGrid;
37
+ onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
38
+ onKeyUp({ key }: KeyUpEvent): boolean;
34
39
  }
@@ -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: pointer.canvasPos,
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 (this.builder && current.isPrimary) {
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
- onKeyPress({ key }) {
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
- if (this.region.containsRect(elem.getBBox())) {
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
- return __awaiter(this, void 0, void 0, function* () {
219
- // Don't hide elements if doing so will be slow.
220
- if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
221
- return;
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
- for (const elem of this.selectedElems) {
224
- const parent = this.editor.image.findParent(elem);
225
- if (!inImage) {
226
- parent === null || parent === void 0 ? void 0 : parent.remove();
227
- }
228
- // If we're making things visible and the selected object wasn't previously
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
- yield this.editor.queueRerender();
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
- void this.addRemoveSelectionFromImage(false);
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 new Duplicate(this.selectedElems);
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
  }
@@ -72,4 +72,10 @@ export default class SelectionHandle {
72
72
  }
73
73
  this.onDragEnd();
74
74
  }
75
+ setSnapToGrid(snap) {
76
+ this.snapToGrid = snap;
77
+ }
78
+ isSnappingToGrid() {
79
+ return this.snapToGrid;
80
+ }
75
81
  }
@@ -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(event: PointerEvt): boolean;
20
+ onPointerDown({ allPointers, current }: PointerEvt): boolean;
19
21
  onPointerMove(event: PointerEvt): void;
20
22
  private onSelectionUpdated;
21
23
  private onGestureEnd;