js-draw 0.12.0 → 0.13.1

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 (73) hide show
  1. package/CHANGELOG.md +14 -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/toolbar/makeColorInput.js +8 -1
  31. package/dist/src/tools/PanZoom.d.ts +3 -1
  32. package/dist/src/tools/PanZoom.js +31 -6
  33. package/dist/src/tools/PasteHandler.d.ts +11 -4
  34. package/dist/src/tools/PasteHandler.js +12 -5
  35. package/dist/src/tools/Pen.d.ts +7 -2
  36. package/dist/src/tools/Pen.js +39 -6
  37. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -0
  38. package/dist/src/tools/SelectionTool/SelectionHandle.js +6 -0
  39. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -1
  40. package/dist/src/tools/SelectionTool/SelectionTool.js +53 -15
  41. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  42. package/dist/src/tools/ToolSwitcherShortcut.js +9 -3
  43. package/dist/src/tools/UndoRedoShortcut.js +2 -4
  44. package/package.json +2 -2
  45. package/src/Color4.test.ts +11 -0
  46. package/src/Color4.ts +22 -0
  47. package/src/Editor.ts +36 -22
  48. package/src/EditorImage.ts +12 -0
  49. package/src/Pointer.ts +19 -0
  50. package/src/SVGLoader.ts +6 -1
  51. package/src/Viewport.ts +50 -11
  52. package/src/commands/invertCommand.ts +1 -1
  53. package/src/components/AbstractComponent.ts +33 -2
  54. package/src/lib.ts +6 -3
  55. package/src/math/Mat33.ts +1 -1
  56. package/src/rendering/Display.ts +12 -15
  57. package/src/rendering/RenderingStyle.ts +1 -1
  58. package/src/rendering/lib.ts +4 -0
  59. package/src/rendering/renderers/DummyRenderer.ts +2 -3
  60. package/src/rendering/renderers/SVGRenderer.ts +4 -0
  61. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -1
  62. package/src/toolbar/HTMLToolbar.ts +1 -1
  63. package/src/toolbar/IconProvider.ts +98 -31
  64. package/src/toolbar/makeColorInput.ts +9 -1
  65. package/src/tools/PanZoom.ts +37 -7
  66. package/src/tools/PasteHandler.ts +12 -6
  67. package/src/tools/Pen.test.ts +44 -1
  68. package/src/tools/Pen.ts +53 -8
  69. package/src/tools/SelectionTool/SelectionHandle.ts +9 -0
  70. package/src/tools/SelectionTool/SelectionTool.ts +67 -15
  71. package/src/tools/ToolSwitcherShortcut.ts +10 -5
  72. package/src/tools/UndoRedoShortcut.ts +2 -5
  73. 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;
@@ -9,7 +9,7 @@ export const makeColorInput = (editor, onColorChange) => {
9
9
  colorInput.classList.add('coloris_input');
10
10
  colorInputContainer.classList.add('color-input-container');
11
11
  colorInputContainer.appendChild(colorInput);
12
- addPipetteTool(editor, colorInputContainer, (color) => {
12
+ const pipetteController = addPipetteTool(editor, colorInputContainer, (color) => {
13
13
  colorInput.value = color.toHexString();
14
14
  onInputEnd();
15
15
  // Update the color preview, if it exists (may be managed by Coloris).
@@ -41,6 +41,7 @@ export const makeColorInput = (editor, onColorChange) => {
41
41
  kind: EditorEventType.ColorPickerToggled,
42
42
  open: true,
43
43
  });
44
+ pipetteController.cancel();
44
45
  });
45
46
  colorInput.addEventListener('close', () => {
46
47
  editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
@@ -102,5 +103,11 @@ const addPipetteTool = (editor, container, onColorChange) => {
102
103
  }
103
104
  };
104
105
  container.appendChild(pipetteButton);
106
+ return {
107
+ // Cancel a pipette color selection if one is in progress.
108
+ cancel: () => {
109
+ endColorSelectMode();
110
+ },
111
+ };
105
112
  };
106
113
  export default makeColorInput;
@@ -21,11 +21,12 @@ export default class PanZoom extends BaseTool {
21
21
  private editor;
22
22
  private mode;
23
23
  private transform;
24
- private lastAngle;
25
24
  private lastDist;
26
25
  private lastScreenCenter;
27
26
  private lastTimestamp;
28
27
  private lastPointerDownTimestamp;
28
+ private initialTouchAngle;
29
+ private initialViewportRotation;
29
30
  private inertialScroller;
30
31
  private velocity;
31
32
  constructor(editor: Editor, mode: PanZoomMode, description: string);
@@ -34,6 +35,7 @@ export default class PanZoom extends BaseTool {
34
35
  onPointerDown({ allPointers: pointers, current: currentPointer }: PointerEvt): boolean;
35
36
  private updateVelocity;
36
37
  private getCenterDelta;
38
+ private toSnappedRotationDelta;
37
39
  private handleTwoFingerMove;
38
40
  private handleOneFingerMove;
39
41
  onPointerMove({ allPointers }: PointerEvt): void;
@@ -78,6 +78,8 @@ export default class PanZoom extends BaseTool {
78
78
  this.mode = mode;
79
79
  this.transform = null;
80
80
  this.lastPointerDownTimestamp = 0;
81
+ this.initialTouchAngle = 0;
82
+ this.initialViewportRotation = 0;
81
83
  this.inertialScroller = null;
82
84
  this.velocity = null;
83
85
  }
@@ -104,9 +106,10 @@ export default class PanZoom extends BaseTool {
104
106
  const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
105
107
  if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
106
108
  const { screenCenter, angle, dist } = this.computePinchData(pointers[0], pointers[1]);
107
- this.lastAngle = angle;
108
109
  this.lastDist = dist;
109
110
  this.lastScreenCenter = screenCenter;
111
+ this.initialTouchAngle = angle;
112
+ this.initialViewportRotation = this.editor.viewport.getRotationAngle();
110
113
  handlingGesture = true;
111
114
  }
112
115
  else if (pointers.length === 1 && ((this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
@@ -149,20 +152,42 @@ export default class PanZoom extends BaseTool {
149
152
  const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenCenter.minus(this.lastScreenCenter));
150
153
  return delta;
151
154
  }
155
+ // Snaps `angle` to common desired rotations. For example, if `touchAngle` corresponds
156
+ // to a viewport rotation of 90.1 degrees, this function returns a rotation delta that,
157
+ // when applied to the viewport, rotates the viewport to 90.0 degrees.
158
+ //
159
+ // Returns a snapped rotation delta that, when applied to the viewport, rotates the viewport,
160
+ // from its position on the last touchDown event, by `touchAngle - initialTouchAngle`.
161
+ toSnappedRotationDelta(touchAngle) {
162
+ const deltaAngle = touchAngle - this.initialTouchAngle;
163
+ let fullRotation = deltaAngle + this.initialViewportRotation;
164
+ const snapToMultipleOf = Math.PI / 2;
165
+ const roundedFullRotation = Math.round(fullRotation / snapToMultipleOf) * snapToMultipleOf;
166
+ // The maximum angle for which we snap the given angle to a multiple of
167
+ // `snapToMultipleOf`.
168
+ const maxSnapAngle = 0.07;
169
+ // Snap the rotation
170
+ if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
171
+ fullRotation = roundedFullRotation;
172
+ }
173
+ return fullRotation - this.editor.viewport.getRotationAngle();
174
+ }
152
175
  handleTwoFingerMove(allPointers) {
153
176
  const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
154
177
  const delta = this.getCenterDelta(screenCenter);
155
- let rotation = angle - this.lastAngle;
178
+ let deltaRotation;
156
179
  if (this.isRotationLocked()) {
157
- rotation = 0;
180
+ deltaRotation = 0;
181
+ }
182
+ else {
183
+ deltaRotation = this.toSnappedRotationDelta(angle);
158
184
  }
159
185
  this.updateVelocity(screenCenter);
160
186
  const transformUpdate = Mat33.translation(delta)
161
187
  .rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
162
- .rightMul(Mat33.zRotation(rotation, canvasCenter));
188
+ .rightMul(Mat33.zRotation(deltaRotation, canvasCenter));
163
189
  this.lastScreenCenter = screenCenter;
164
190
  this.lastDist = dist;
165
- this.lastAngle = angle;
166
191
  this.transform = Viewport.transformBy(this.transform.transform.rightMul(transformUpdate));
167
192
  }
168
193
  handleOneFingerMove(pointer) {
@@ -266,7 +291,7 @@ export default class PanZoom extends BaseTool {
266
291
  const toCanvas = this.editor.viewport.screenToCanvasTransform;
267
292
  // Transform without including translation
268
293
  const translation = toCanvas.transformVec3(Vec3.of(-delta.x, -delta.y, 0));
269
- const pinchZoomScaleFactor = 1.04;
294
+ const pinchZoomScaleFactor = 1.03;
270
295
  const transformUpdate = Mat33.scaling2D(Math.max(0.25, Math.min(Math.pow(pinchZoomScaleFactor, -delta.z), 4)), canvasPos).rightMul(Mat33.translation(translation));
271
296
  this.updateTransform(transformUpdate, true);
272
297
  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;