js-draw 0.12.0 → 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 (70) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.d.ts +12 -0
  4. package/dist/src/Color4.js +16 -0
  5. package/dist/src/Editor.d.ts +33 -18
  6. package/dist/src/Editor.js +22 -19
  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 +6 -1
  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 +19 -0
  17. package/dist/src/components/AbstractComponent.js +17 -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/IconProvider.d.ts +1 -1
  29. package/dist/src/toolbar/IconProvider.js +90 -29
  30. package/dist/src/tools/PanZoom.js +1 -1
  31. package/dist/src/tools/PasteHandler.d.ts +11 -4
  32. package/dist/src/tools/PasteHandler.js +12 -5
  33. package/dist/src/tools/Pen.d.ts +7 -2
  34. package/dist/src/tools/Pen.js +39 -6
  35. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -0
  36. package/dist/src/tools/SelectionTool/SelectionHandle.js +6 -0
  37. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -1
  38. package/dist/src/tools/SelectionTool/SelectionTool.js +53 -15
  39. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  40. package/dist/src/tools/ToolSwitcherShortcut.js +9 -3
  41. package/dist/src/tools/UndoRedoShortcut.js +2 -4
  42. package/package.json +2 -2
  43. package/src/Color4.test.ts +11 -0
  44. package/src/Color4.ts +22 -0
  45. package/src/Editor.ts +36 -22
  46. package/src/EditorImage.ts +12 -0
  47. package/src/Pointer.ts +19 -0
  48. package/src/SVGLoader.ts +6 -1
  49. package/src/Viewport.ts +50 -11
  50. package/src/commands/invertCommand.ts +1 -1
  51. package/src/components/AbstractComponent.ts +33 -2
  52. package/src/lib.ts +6 -3
  53. package/src/math/Mat33.ts +1 -1
  54. package/src/rendering/Display.ts +12 -15
  55. package/src/rendering/RenderingStyle.ts +1 -1
  56. package/src/rendering/lib.ts +4 -0
  57. package/src/rendering/renderers/DummyRenderer.ts +2 -3
  58. package/src/rendering/renderers/SVGRenderer.ts +4 -0
  59. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -1
  60. package/src/toolbar/HTMLToolbar.ts +1 -1
  61. package/src/toolbar/IconProvider.ts +98 -31
  62. package/src/tools/PanZoom.ts +1 -1
  63. package/src/tools/PasteHandler.ts +12 -6
  64. package/src/tools/Pen.test.ts +44 -1
  65. package/src/tools/Pen.ts +53 -8
  66. package/src/tools/SelectionTool/SelectionHandle.ts +9 -0
  67. package/src/tools/SelectionTool/SelectionTool.ts +67 -15
  68. package/src/tools/ToolSwitcherShortcut.ts +10 -5
  69. package/src/tools/UndoRedoShortcut.ts +2 -5
  70. package/typedoc.json +2 -2
@@ -1,17 +1,3 @@
1
- /**
2
- * Handles `HTMLCanvasElement`s (or other drawing surfaces if being used) used to display the editor's contents.
3
- *
4
- * @example
5
- * ```
6
- * const editor = new Editor(document.body);
7
- * const w = editor.display.width;
8
- * const h = editor.display.height;
9
- * const center = Vec2.of(w / 2, h / 2);
10
- * const colorAtCenter = editor.display.getColorAt(center);
11
- * ```
12
- *
13
- * @packageDocumentation
14
- */
15
1
  import CanvasRenderer from './renderers/CanvasRenderer';
16
2
  import { EditorEventType } from '../types';
17
3
  import DummyRenderer from './renderers/DummyRenderer';
@@ -25,6 +11,18 @@ export var RenderingMode;
25
11
  RenderingMode[RenderingMode["CanvasRenderer"] = 1] = "CanvasRenderer";
26
12
  // SVGRenderer is not supported by the main display
27
13
  })(RenderingMode || (RenderingMode = {}));
14
+ /**
15
+ * Handles `HTMLCanvasElement`s (or other drawing surfaces if being used) used to display the editor's contents.
16
+ *
17
+ * @example
18
+ * ```
19
+ * const editor = new Editor(document.body);
20
+ * const w = editor.display.width;
21
+ * const h = editor.display.height;
22
+ * const center = Vec2.of(w / 2, h / 2);
23
+ * const colorAtCenter = editor.display.getColorAt(center);
24
+ * ```
25
+ */
28
26
  export default class Display {
29
27
  /** @internal */
30
28
  constructor(editor, mode, parent) {
@@ -0,0 +1,3 @@
1
+ export { default as AbstractRenderer } from './renderers/AbstractRenderer';
2
+ export { default as DummyRenderer } from './renderers/DummyRenderer';
3
+ export { default as Display } from './Display';
@@ -0,0 +1,3 @@
1
+ export { default as AbstractRenderer } from './renderers/AbstractRenderer';
2
+ export { default as DummyRenderer } from './renderers/DummyRenderer';
3
+ export { default as Display } from './Display';
@@ -1,6 +1,6 @@
1
- // Renderer that outputs nothing. Useful for automated tests.
2
1
  import { Vec2 } from '../../math/Vec2';
3
2
  import AbstractRenderer from './AbstractRenderer';
3
+ // Renderer that outputs almost nothing. Useful for automated tests.
4
4
  export default class DummyRenderer extends AbstractRenderer {
5
5
  constructor(viewport) {
6
6
  super(viewport);
@@ -17,7 +17,7 @@ export default class DummyRenderer extends AbstractRenderer {
17
17
  }
18
18
  displaySize() {
19
19
  // Do we have a stored viewport size?
20
- const viewportSize = this.getViewport().getResolution();
20
+ const viewportSize = this.getViewport().getScreenRectSize();
21
21
  // Don't use a 0x0 viewport — DummyRenderer is often used
22
22
  // for tests that run without a display, so pretend we have a
23
23
  // reasonable-sized display.
@@ -31,6 +31,10 @@ export default class SVGRenderer extends AbstractRenderer {
31
31
  stroke-linecap: round;
32
32
  stroke-linejoin: round;
33
33
  }
34
+
35
+ text {
36
+ white-space: pre;
37
+ }
34
38
  `.replace(/\s+/g, '');
35
39
  styleSheet.setAttribute('id', renderedStylesheetId);
36
40
  this.elem.appendChild(styleSheet);
@@ -21,7 +21,7 @@ export default class IconProvider {
21
21
  makeRotationLockIcon(): IconType;
22
22
  makeInsertImageIcon(): IconType;
23
23
  makeTextIcon(textStyle: TextStyle): IconType;
24
- makePenIcon(tipThickness: number, color: string | Color4, roundedTip?: boolean): IconType;
24
+ makePenIcon(strokeSize: number, color: string | Color4, rounded?: boolean): IconType;
25
25
  makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
26
26
  makePipetteIcon(color?: Color4): IconType;
27
27
  makeResizeViewportIcon(): IconType;
@@ -30,7 +30,7 @@ export default class IconProvider {
30
30
  makeUndoIcon() {
31
31
  return this.makeRedoIcon(true);
32
32
  }
33
- // @param mirror - reflect across the x-axis @internal
33
+ // @param mirror - reflect across the x-axis. This parameter is internal.
34
34
  // @returns a redo icon.
35
35
  makeRedoIcon(mirror = false) {
36
36
  const icon = document.createElementNS(svgNamespace, 'svg');
@@ -336,45 +336,106 @@ export default class IconProvider {
336
336
  icon.appendChild(textNode);
337
337
  return icon;
338
338
  }
339
- makePenIcon(tipThickness, color, roundedTip) {
339
+ makePenIcon(strokeSize, color, rounded) {
340
340
  if (color instanceof Color4) {
341
341
  color = color.toHexString();
342
342
  }
343
343
  const icon = document.createElementNS(svgNamespace, 'svg');
344
344
  icon.setAttribute('viewBox', '0 0 100 100');
345
- const halfThickness = tipThickness / 2;
346
- // Draw a pen-like shape
347
- const penTipLeft = 50 - halfThickness;
348
- const penTipRight = 50 + halfThickness;
349
- let tipCenterPrimaryPath = `L${penTipLeft},95 L${penTipRight},90`;
350
- let tipCenterBackgroundPath = `L${penTipLeft},85 L${penTipRight},83`;
351
- if (roundedTip) {
352
- tipCenterPrimaryPath = `L${penTipLeft},95 q${halfThickness},10 ${2 * halfThickness},-5`;
353
- tipCenterBackgroundPath = `L${penTipLeft},87 q${halfThickness},10 ${2 * halfThickness},-3`;
345
+ const tipThickness = strokeSize / 2;
346
+ const inkTipPath = `
347
+ M ${15 - tipThickness},${80 - tipThickness}
348
+ ${15 - tipThickness},${80 + tipThickness}
349
+ 30,83
350
+ 15,65
351
+ Z
352
+ `;
353
+ const trailStartEndY = 80 + tipThickness;
354
+ const inkTrailPath = `
355
+ m ${15 - tipThickness * 1.1},${trailStartEndY}
356
+ c 35,10 55,15 60,30
357
+ l ${35 + tipThickness * 1.2},${-10 - tipThickness}
358
+ C 80.47,98.32 50.5,${90 + tipThickness} 20,${trailStartEndY} Z
359
+ `;
360
+ const colorBubblePath = `
361
+ M 72.45,35.67
362
+ A 10,15 41.8 0 1 55,40.2 10,15 41.8 0 1 57.55,22.3 10,15 41.8 0 1 75,17.8 10,15 41.8 0 1 72.5,35.67
363
+ Z
364
+ `;
365
+ let gripMainPath = 'M 85,-25 25,35 h 10 v 10 h 10 v 10 h 10 v 10 h 10 l -5,10 60,-60 z';
366
+ let gripShadow1Path = 'M 25,35 H 35 L 90,-15 85,-25 Z';
367
+ let gripShadow2Path = 'M 60,75 65,65 H 55 l 55,-55 10,5 z';
368
+ if (rounded) {
369
+ gripMainPath = 'M 85,-25 25,35 c 15,0 40,30 35,40 l 60,-60 z';
370
+ gripShadow1Path = 'm 25,35 c 3.92361,0.384473 7.644275,0.980572 10,3 l 55,-53 -5,-10 z';
371
+ gripShadow2Path = 'M 60,75 C 61,66 59,65 56,59 l 54,-54 10,10 z';
354
372
  }
355
- const primaryStrokeTipPath = `M14,63 ${tipCenterPrimaryPath} L88,60 Z`;
356
- const backgroundStrokeTipPath = `M14,63 ${tipCenterBackgroundPath} L88,60 Z`;
357
- icon.innerHTML = `
358
- <defs>
359
- ${checkerboardPatternDef}
360
- </defs>
361
- <g>
362
- <!-- Pen grip -->
373
+ const penTipPath = `M 25,35 ${10 - tipThickness / 4},${70 - tipThickness / 2} 20,75 25,85 60,75 70,55 45,25 Z`;
374
+ const pencilTipColor = Color4.fromHex('#f4d7d7');
375
+ const tipColor = pencilTipColor.mix(Color4.fromString(color), tipThickness / 40 - 0.1).toHexString();
376
+ const ink = `
377
+ <path
378
+ fill="${checkerboardPatternRef}"
379
+ d="${inkTipPath}"
380
+ />
381
+ <path
382
+ fill="${checkerboardPatternRef}"
383
+ d="${inkTrailPath}"
384
+ />
385
+ <path
386
+ fill="${color}"
387
+ d="${inkTipPath}"
388
+ />
389
+ <path
390
+ fill="${color}"
391
+ d="${inkTrailPath}"
392
+ />
393
+ `;
394
+ const penTip = `
395
+ <path
396
+ fill="${checkerboardPatternRef}"
397
+ d="${penTipPath}"
398
+ />
399
+ <path
400
+ fill="${tipColor}"
401
+ stroke="${color}"
402
+ d="${penTipPath}"
403
+ />
404
+ `;
405
+ const grip = `
363
406
  <path
364
- d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
365
407
  ${iconColorStrokeFill}
408
+ d="${gripMainPath}"
366
409
  />
367
- </g>
368
- <g>
369
- <!-- Checkerboard background for slightly transparent pens -->
370
- <path d='${backgroundStrokeTipPath}' fill='${checkerboardPatternRef}'/>
371
-
372
- <!-- Actual pen tip -->
410
+
411
+ <!-- shadows -->
412
+ <path
413
+ fill="rgba(150, 150, 150, 0.3)"
414
+ d="${gripShadow1Path}"
415
+ />
416
+ <path
417
+ fill="rgba(100, 100, 100, 0.2)"
418
+ d="${gripShadow2Path}"
419
+ />
420
+
421
+ <!-- color bubble -->
373
422
  <path
374
- d='${primaryStrokeTipPath}'
375
- fill='${color}'
376
- stroke='${color}'
423
+ fill="${checkerboardPatternRef}"
424
+ d="${colorBubblePath}"
377
425
  />
426
+ <path
427
+ fill="${color}"
428
+ d="${colorBubblePath}"
429
+ />
430
+ `;
431
+ icon.innerHTML = `
432
+ <defs>
433
+ ${checkerboardPatternDef}
434
+ </defs>
435
+ <g>
436
+ ${ink}
437
+ ${penTip}
438
+ ${grip}
378
439
  </g>
379
440
  `;
380
441
  return icon;
@@ -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
  }
@@ -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;
@@ -1,6 +1,3 @@
1
- // Allows users to select/transform portions of the `EditorImage`.
2
- // With respect to `extend`ing, `SelectionTool` is not stable.
3
- // @packageDocumentation
4
1
  import Mat33 from '../../math/Mat33';
5
2
  import { Vec2 } from '../../math/Vec2';
6
3
  import { EditorEventType } from '../../types';
@@ -10,7 +7,8 @@ import SVGRenderer from '../../rendering/renderers/SVGRenderer';
10
7
  import Selection from './Selection';
11
8
  import TextComponent from '../../components/TextComponent';
12
9
  export const cssPrefix = 'selection-tool-';
13
- // {@inheritDoc SelectionTool!}
10
+ // Allows users to select/transform portions of the `EditorImage`.
11
+ // With respect to `extend`ing, `SelectionTool` is not stable.
14
12
  export default class SelectionTool extends BaseTool {
15
13
  constructor(editor, description) {
16
14
  super(editor.notifier, description);
@@ -18,6 +16,7 @@ export default class SelectionTool extends BaseTool {
18
16
  this.lastEvtTarget = null;
19
17
  this.expandingSelectionBox = false;
20
18
  this.shiftKeyPressed = false;
19
+ this.ctrlKeyPressed = false;
21
20
  this.selectionBoxHandlingEvt = false;
22
21
  this.handleOverlay = document.createElement('div');
23
22
  editor.createHTMLOverlay(this.handleOverlay);
@@ -45,17 +44,37 @@ export default class SelectionTool extends BaseTool {
45
44
  }
46
45
  this.selectionBox.addTo(this.handleOverlay);
47
46
  }
48
- onPointerDown(event) {
49
- var _a;
50
- if (event.allPointers.length === 1 && event.current.isPrimary) {
51
- if (this.lastEvtTarget && ((_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.onDragStart(event.current, this.lastEvtTarget))) {
52
- this.selectionBoxHandlingEvt = true;
53
- this.expandingSelectionBox = false;
47
+ snapSelectionToGrid() {
48
+ if (!this.selectionBox)
49
+ throw new Error('No selection to snap!');
50
+ const topLeftOfBBox = this.selectionBox.region.topLeft;
51
+ const snapDistance = this.editor.viewport.snapToGrid(topLeftOfBBox).minus(topLeftOfBBox);
52
+ const oldTransform = this.selectionBox.getTransform();
53
+ this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDistance)));
54
+ this.selectionBox.finalizeTransform();
55
+ }
56
+ onPointerDown({ allPointers, current }) {
57
+ const snapToGrid = this.ctrlKeyPressed;
58
+ if (snapToGrid) {
59
+ current = current.snappedToGrid(this.editor.viewport);
60
+ }
61
+ if (allPointers.length === 1 && current.isPrimary) {
62
+ let transforming = false;
63
+ if (this.lastEvtTarget && this.selectionBox) {
64
+ if (snapToGrid) {
65
+ this.snapSelectionToGrid();
66
+ }
67
+ const dragStartResult = this.selectionBox.onDragStart(current, this.lastEvtTarget);
68
+ if (dragStartResult) {
69
+ transforming = true;
70
+ this.selectionBoxHandlingEvt = true;
71
+ this.expandingSelectionBox = false;
72
+ }
54
73
  }
55
- else {
74
+ if (!transforming) {
56
75
  // Shift key: Combine the new and old selection boxes at the end of the gesture.
57
76
  this.expandingSelectionBox = this.shiftKeyPressed;
58
- this.makeSelectionBox(event.current.canvasPos);
77
+ this.makeSelectionBox(current.canvasPos);
59
78
  }
60
79
  return true;
61
80
  }
@@ -64,11 +83,15 @@ export default class SelectionTool extends BaseTool {
64
83
  onPointerMove(event) {
65
84
  if (!this.selectionBox)
66
85
  return;
86
+ let currentPointer = event.current;
87
+ if (this.ctrlKeyPressed) {
88
+ currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
89
+ }
67
90
  if (this.selectionBoxHandlingEvt) {
68
- this.selectionBox.onDragUpdate(event.current);
91
+ this.selectionBox.onDragUpdate(currentPointer);
69
92
  }
70
93
  else {
71
- this.selectionBox.setToPoint(event.current.canvasPos);
94
+ this.selectionBox.setToPoint(currentPointer.canvasPos);
72
95
  }
73
96
  }
74
97
  onSelectionUpdated() {
@@ -113,7 +136,11 @@ export default class SelectionTool extends BaseTool {
113
136
  onPointerUp(event) {
114
137
  if (!this.selectionBox)
115
138
  return;
116
- this.selectionBox.setToPoint(event.current.canvasPos);
139
+ let currentPointer = event.current;
140
+ if (this.ctrlKeyPressed) {
141
+ currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
142
+ }
143
+ this.selectionBox.setToPoint(currentPointer.canvasPos);
117
144
  // Were we expanding the previous selection?
118
145
  if (this.expandingSelectionBox && this.prevSelectionBox) {
119
146
  // If so, finish expanding.
@@ -142,6 +169,10 @@ export default class SelectionTool extends BaseTool {
142
169
  this.expandingSelectionBox = false;
143
170
  }
144
171
  onKeyPress(event) {
172
+ if (event.key === 'Control') {
173
+ this.ctrlKeyPressed = true;
174
+ return true;
175
+ }
145
176
  if (this.selectionBox && event.ctrlKey && event.key === 'd') {
146
177
  // Handle duplication on key up — we don't want to accidentally duplicate
147
178
  // many times.
@@ -235,6 +266,10 @@ export default class SelectionTool extends BaseTool {
235
266
  return handled;
236
267
  }
237
268
  onKeyUp(evt) {
269
+ if (evt.key === 'Control') {
270
+ this.ctrlKeyPressed = false;
271
+ return true;
272
+ }
238
273
  if (evt.key === 'Shift') {
239
274
  this.shiftKeyPressed = false;
240
275
  return true;
@@ -291,6 +326,8 @@ export default class SelectionTool extends BaseTool {
291
326
  // Clear the selection
292
327
  this.handleOverlay.replaceChildren();
293
328
  this.selectionBox = null;
329
+ this.shiftKeyPressed = false;
330
+ this.ctrlKeyPressed = false;
294
331
  this.handleOverlay.style.display = enabled ? 'block' : 'none';
295
332
  if (enabled) {
296
333
  this.handleOverlay.tabIndex = 0;
@@ -355,4 +392,5 @@ SelectionTool.handleableKeys = [
355
392
  'e', 'j', 'ArrowDown',
356
393
  'r', 'R',
357
394
  'i', 'I', 'o', 'O',
395
+ 'Control',
358
396
  ];
@@ -1,6 +1,14 @@
1
1
  import Editor from '../Editor';
2
2
  import { KeyPressEvent } from '../types';
3
3
  import BaseTool from './BaseTool';
4
+ /**
5
+ * Handles keyboard events used, by default, to select tools. By default,
6
+ * 1 maps to the first primary tool, 2 to the second primary tool, ... .
7
+ *
8
+ * This is in the default set of {@link ToolController} tools.
9
+ *
10
+ * @deprecated This may be replaced in the future.
11
+ */
4
12
  export default class ToolSwitcherShortcut extends BaseTool {
5
13
  private editor;
6
14
  constructor(editor: Editor);
@@ -1,12 +1,18 @@
1
- // Handles ctrl+1, ctrl+2, ctrl+3, ..., shortcuts for switching tools.
2
- // @packageDocumentation
3
1
  import BaseTool from './BaseTool';
4
- // {@inheritDoc ToolSwitcherShortcut!}
2
+ /**
3
+ * Handles keyboard events used, by default, to select tools. By default,
4
+ * 1 maps to the first primary tool, 2 to the second primary tool, ... .
5
+ *
6
+ * This is in the default set of {@link ToolController} tools.
7
+ *
8
+ * @deprecated This may be replaced in the future.
9
+ */
5
10
  export default class ToolSwitcherShortcut extends BaseTool {
6
11
  constructor(editor) {
7
12
  super(editor.notifier, editor.localization.changeTool);
8
13
  this.editor = editor;
9
14
  }
15
+ // @internal
10
16
  onKeyPress({ key }) {
11
17
  const toolController = this.editor.toolController;
12
18
  const primaryTools = toolController.getPrimaryTools();
@@ -1,13 +1,11 @@
1
- // Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts.
2
- // @packageDocumentation
3
1
  import BaseTool from './BaseTool';
4
- // {@inheritDoc UndoRedoShortcut!}
2
+ // Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts.
5
3
  export default class UndoRedoShortcut extends BaseTool {
6
4
  constructor(editor) {
7
5
  super(editor.notifier, editor.localization.undoRedoTool);
8
6
  this.editor = editor;
9
7
  }
10
- // Activate undo/redo
8
+ // @internal
11
9
  onKeyPress({ key, ctrlKey }) {
12
10
  if (ctrlKey) {
13
11
  if (key === 'z') {