js-draw 0.15.1 → 0.15.2

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 (82) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.d.ts +1 -1
  4. package/dist/src/Color4.js +5 -1
  5. package/dist/src/Editor.d.ts +0 -2
  6. package/dist/src/Editor.js +15 -30
  7. package/dist/src/EditorImage.d.ts +25 -0
  8. package/dist/src/EditorImage.js +57 -2
  9. package/dist/src/EventDispatcher.d.ts +4 -3
  10. package/dist/src/SVGLoader.d.ts +1 -0
  11. package/dist/src/SVGLoader.js +15 -1
  12. package/dist/src/Viewport.d.ts +3 -3
  13. package/dist/src/Viewport.js +4 -8
  14. package/dist/src/components/AbstractComponent.d.ts +5 -1
  15. package/dist/src/components/AbstractComponent.js +10 -2
  16. package/dist/src/components/ImageBackground.d.ts +41 -0
  17. package/dist/src/components/ImageBackground.js +132 -0
  18. package/dist/src/components/ImageComponent.js +2 -0
  19. package/dist/src/components/builders/ArrowBuilder.d.ts +3 -1
  20. package/dist/src/components/builders/ArrowBuilder.js +43 -40
  21. package/dist/src/components/builders/LineBuilder.d.ts +3 -1
  22. package/dist/src/components/builders/LineBuilder.js +25 -28
  23. package/dist/src/components/builders/RectangleBuilder.js +1 -1
  24. package/dist/src/components/lib.d.ts +2 -1
  25. package/dist/src/components/lib.js +2 -1
  26. package/dist/src/components/localization.d.ts +2 -0
  27. package/dist/src/components/localization.js +2 -0
  28. package/dist/src/math/Mat33.js +43 -5
  29. package/dist/src/math/Path.d.ts +5 -0
  30. package/dist/src/math/Path.js +80 -28
  31. package/dist/src/math/Vec3.js +1 -1
  32. package/dist/src/rendering/Display.js +1 -1
  33. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
  34. package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
  35. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  36. package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
  37. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  38. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  39. package/dist/src/testing/sendTouchEvent.d.ts +6 -0
  40. package/dist/src/testing/sendTouchEvent.js +26 -0
  41. package/dist/src/toolbar/IconProvider.js +1 -2
  42. package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
  43. package/dist/src/tools/Eraser.js +5 -2
  44. package/dist/src/tools/PanZoom.js +12 -0
  45. package/dist/src/tools/SelectionTool/Selection.js +1 -1
  46. package/dist/src/tools/SelectionTool/SelectionTool.js +5 -1
  47. package/package.json +1 -1
  48. package/src/Color4.test.ts +6 -0
  49. package/src/Color4.ts +6 -1
  50. package/src/Editor.ts +15 -36
  51. package/src/EditorImage.ts +74 -2
  52. package/src/EventDispatcher.ts +4 -1
  53. package/src/SVGLoader.ts +12 -1
  54. package/src/Viewport.ts +4 -7
  55. package/src/components/AbstractComponent.ts +11 -1
  56. package/src/components/ImageBackground.ts +167 -0
  57. package/src/components/ImageComponent.ts +2 -0
  58. package/src/components/builders/ArrowBuilder.ts +44 -41
  59. package/src/components/builders/LineBuilder.ts +26 -28
  60. package/src/components/builders/RectangleBuilder.ts +1 -1
  61. package/src/components/lib.ts +2 -0
  62. package/src/components/localization.ts +4 -0
  63. package/src/math/Mat33.test.ts +20 -1
  64. package/src/math/Mat33.ts +47 -5
  65. package/src/math/Path.ts +87 -28
  66. package/src/math/Vec3.test.ts +4 -0
  67. package/src/math/Vec3.ts +1 -1
  68. package/src/rendering/Display.ts +1 -1
  69. package/src/rendering/renderers/AbstractRenderer.ts +20 -3
  70. package/src/rendering/renderers/CanvasRenderer.ts +16 -3
  71. package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
  72. package/src/rendering/renderers/SVGRenderer.ts +8 -1
  73. package/src/testing/sendTouchEvent.ts +43 -0
  74. package/src/toolbar/IconProvider.ts +1 -2
  75. package/src/toolbar/widgets/HandToolWidget.ts +1 -1
  76. package/src/tools/Eraser.test.ts +24 -1
  77. package/src/tools/Eraser.ts +6 -2
  78. package/src/tools/PanZoom.test.ts +267 -23
  79. package/src/tools/PanZoom.ts +15 -1
  80. package/src/tools/SelectionTool/Selection.ts +1 -1
  81. package/src/tools/SelectionTool/SelectionTool.ts +6 -1
  82. package/src/types.ts +1 -0
@@ -1,3 +1,4 @@
1
+ import Color4 from '../../Color4';
1
2
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
3
  import Mat33 from '../../math/Mat33';
3
4
  import Path, { PathCommand, PathCommandType } from '../../math/Path';
@@ -122,21 +123,37 @@ export default abstract class AbstractRenderer {
122
123
  }
123
124
  }
124
125
 
125
- // Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
126
+ // Strokes a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
126
127
  // This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
127
128
  public drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle) {
128
129
  const path = Path.fromRect(rect, lineWidth);
129
130
  this.drawPath(path.toRenderable(lineFill));
130
131
  }
131
132
 
132
- // Note the start/end of an object with the given bounding box.
133
+ // Fills a rectangle.
134
+ public fillRect(rect: Rect2, fill: Color4) {
135
+ const path = Path.fromRect(rect);
136
+ this.drawPath(path.toRenderable({ fill }));
137
+ }
138
+
139
+ // Note the start of an object with the given bounding box.
133
140
  // Renderers are not required to support [clip]
134
141
  public startObject(_boundingBox: Rect2, _clip?: boolean) {
135
142
  this.currentPaths = [];
136
143
  this.objectLevel ++;
137
144
  }
138
145
 
139
- public endObject(_loaderData?: LoadSaveDataTable) {
146
+ /**
147
+ * Notes the end of an object.
148
+ * @param _loaderData - a map from strings to JSON-ifyable objects
149
+ * and contains properties attached to the object by whatever loader loaded the image. This
150
+ * is used to preserve attributes not supported by js-draw when loading/saving an image.
151
+ * Renderers may ignore this.
152
+ *
153
+ * @param _objectTags - a list of labels (e.g. `className`s) to be attached to the object.
154
+ * Renderers may ignore this.
155
+ */
156
+ public endObject(_loaderData?: LoadSaveDataTable, _objectTags?: string[]) {
140
157
  // Render the paths all at once
141
158
  this.flushPath();
142
159
  this.currentPaths = null;
@@ -1,6 +1,7 @@
1
1
  import Color4 from '../../Color4';
2
2
  import TextComponent from '../../components/TextComponent';
3
3
  import Mat33 from '../../math/Mat33';
4
+ import Path from '../../math/Path';
4
5
  import Rect2 from '../../math/Rect2';
5
6
  import { Point2, Vec2 } from '../../math/Vec2';
6
7
  import Vec3 from '../../math/Vec3';
@@ -12,6 +13,7 @@ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './Abstrac
12
13
  export default class CanvasRenderer extends AbstractRenderer {
13
14
  private ignoreObjectsAboveLevel: number|null = null;
14
15
  private ignoringObject: boolean = false;
16
+ private currentObjectBBox: Rect2|null = null;
15
17
 
16
18
  // Minimum square distance of a control point from the line between the end points
17
19
  // for the curve not to be drawn as a line.
@@ -65,8 +67,8 @@ export default class CanvasRenderer extends AbstractRenderer {
65
67
  this.minRenderSizeAnyDimen = 0.5;
66
68
  } else {
67
69
  this.minSquareCurveApproxDist = 0.5;
68
- this.minRenderSizeBothDimens = 0.3;
69
- this.minRenderSizeAnyDimen = 1e-5;
70
+ this.minRenderSizeBothDimens = 0.2;
71
+ this.minRenderSizeAnyDimen = 1e-6;
70
72
  }
71
73
  }
72
74
 
@@ -149,6 +151,15 @@ export default class CanvasRenderer extends AbstractRenderer {
149
151
  return;
150
152
  }
151
153
 
154
+ // If part of a huge object, it might be worth trimming the path
155
+ if (this.currentObjectBBox?.containsRect(this.getViewport().visibleRect)) {
156
+ // Try to trim/remove parts of the path outside of the bounding box.
157
+ path = Path.visualEquivalent(
158
+ path,
159
+ this.getViewport().visibleRect
160
+ );
161
+ }
162
+
152
163
  super.drawPath(path);
153
164
  }
154
165
 
@@ -181,13 +192,14 @@ export default class CanvasRenderer extends AbstractRenderer {
181
192
  }
182
193
 
183
194
  private clipLevels: number[] = [];
184
- public startObject(boundingBox: Rect2, clip: boolean) {
195
+ public startObject(boundingBox: Rect2, clip?: boolean) {
185
196
  if (this.isTooSmallToRender(boundingBox)) {
186
197
  this.ignoreObjectsAboveLevel = this.getNestingLevel();
187
198
  this.ignoringObject = true;
188
199
  }
189
200
 
190
201
  super.startObject(boundingBox);
202
+ this.currentObjectBBox = boundingBox;
191
203
 
192
204
  if (!this.ignoringObject && clip) {
193
205
  this.clipLevels.push(this.objectLevel);
@@ -209,6 +221,7 @@ export default class CanvasRenderer extends AbstractRenderer {
209
221
  }
210
222
  }
211
223
 
224
+ this.currentObjectBBox = null;
212
225
  super.endObject();
213
226
 
214
227
  // If exiting an object with a too-small-to-draw bounding box,
@@ -1,12 +1,11 @@
1
1
 
2
- import EventDispatcher from '../../EventDispatcher';
3
2
  import Mat33 from '../../math/Mat33';
4
3
  import { Vec2 } from '../../math/Vec2';
5
4
  import Viewport from '../../Viewport';
6
5
  import DummyRenderer from './DummyRenderer';
7
6
 
8
7
  const makeRenderer = (): [DummyRenderer, Viewport] => {
9
- const viewport = new Viewport(new EventDispatcher());
8
+ const viewport = new Viewport(() => {});
10
9
  return [ new DummyRenderer(viewport), viewport ];
11
10
  };
12
11
 
@@ -248,7 +248,7 @@ export default class SVGRenderer extends AbstractRenderer {
248
248
  this.objectElems = [];
249
249
  }
250
250
 
251
- public endObject(loaderData?: LoadSaveDataTable) {
251
+ public endObject(loaderData?: LoadSaveDataTable, elemClassNames?: string[]) {
252
252
  super.endObject(loaderData);
253
253
 
254
254
  // Don't extend paths across objects
@@ -273,6 +273,13 @@ export default class SVGRenderer extends AbstractRenderer {
273
273
  }
274
274
  }
275
275
  }
276
+
277
+ // Add class names to the object, if given.
278
+ if (elemClassNames) {
279
+ for (const elem of this.objectElems ?? []) {
280
+ elem.classList.add(...elemClassNames);
281
+ }
282
+ }
276
283
  }
277
284
 
278
285
  // Not implemented -- use drawPath instead.
@@ -0,0 +1,43 @@
1
+ import Editor from '../Editor';
2
+ import { Vec2 } from '../math/Vec2';
3
+ import Pointer, { PointerDevice } from '../Pointer';
4
+ import { InputEvtType } from '../types';
5
+
6
+
7
+ const sendTouchEvent = (
8
+ editor: Editor,
9
+ eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
10
+ screenPos: Vec2,
11
+ allOtherPointers?: Pointer[]
12
+ ) => {
13
+ const canvasPos = editor.viewport.screenToCanvas(screenPos);
14
+
15
+ let ptrId = 0;
16
+ let maxPtrId = 0;
17
+
18
+ // Get a unique ID for the main pointer
19
+ // (try to use id=0, but don't use it if it's already in use).
20
+ for (const pointer of allOtherPointers ?? []) {
21
+ maxPtrId = Math.max(pointer.id, maxPtrId);
22
+ if (pointer.id === ptrId) {
23
+ ptrId = maxPtrId + 1;
24
+ }
25
+ }
26
+
27
+ const mainPointer = Pointer.ofCanvasPoint(
28
+ canvasPos, eventType !== InputEvtType.PointerUpEvt, editor.viewport, ptrId, PointerDevice.Touch
29
+ );
30
+
31
+ editor.toolController.dispatchInputEvent({
32
+ kind: eventType,
33
+ allPointers: [
34
+ ...(allOtherPointers ?? []),
35
+ mainPointer,
36
+ ],
37
+ current: mainPointer,
38
+ });
39
+
40
+ return mainPointer;
41
+ };
42
+
43
+ export default sendTouchEvent;
@@ -1,6 +1,5 @@
1
1
  import Color4 from '../Color4';
2
2
  import { ComponentBuilderFactory } from '../components/builders/types';
3
- import EventDispatcher from '../EventDispatcher';
4
3
  import { Vec2 } from '../math/Vec2';
5
4
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
5
  import TextStyle from '../rendering/TextRenderingStyle';
@@ -542,7 +541,7 @@ export default class IconProvider {
542
541
  time: nowTime,
543
542
  };
544
543
 
545
- const viewport = new Viewport(new EventDispatcher());
544
+ const viewport = new Viewport(() => {});
546
545
  const builder = factory(startPoint, viewport);
547
546
  builder.addPoint(endPoint);
548
547
 
@@ -94,7 +94,7 @@ class ZoomWidget extends BaseWidget {
94
94
  }
95
95
 
96
96
  protected fillDropdown(dropdown: HTMLElement): boolean {
97
- dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
97
+ dropdown.replaceChildren(makeZoomControl(this.localizationTable, this.editor));
98
98
  return true;
99
99
  }
100
100
  }
@@ -1,5 +1,6 @@
1
+ import UnknownSVGObject from '../components/UnknownSVGObject';
1
2
  import Editor from '../Editor';
2
- import { Rect2, StrokeComponent } from '../lib';
3
+ import { EditorImage, Rect2, StrokeComponent } from '../lib';
3
4
  import { Vec2 } from '../math/Vec2';
4
5
  import createEditor from '../testing/createEditor';
5
6
  import { InputEvtType } from '../types';
@@ -76,4 +77,26 @@ describe('Eraser', () => {
76
77
 
77
78
  expect(getAllStrokes(editor)).toHaveLength(0);
78
79
  });
80
+
81
+ it('should not erase unselectable objects', () => {
82
+ const editor = createEditor();
83
+ const unerasableObj = new UnknownSVGObject(document.createElementNS('http://www.w3.org/2000/svg', 'arc'));
84
+
85
+ // Add to the image
86
+ expect(editor.image.getAllElements()).toHaveLength(0);
87
+ editor.dispatch(EditorImage.addElement(unerasableObj));
88
+ expect(editor.image.getAllElements()).toHaveLength(1);
89
+
90
+
91
+ const eraser = selectEraser(editor);
92
+ eraser.setThickness(100);
93
+
94
+ // Try to erase it.
95
+ editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
96
+ jest.advanceTimersByTime(100);
97
+ editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(3, 0));
98
+
99
+ // Should not have been erased
100
+ expect(editor.image.getAllElements()).toHaveLength(1);
101
+ });
79
102
  });
@@ -67,11 +67,15 @@ export default class Eraser extends BaseTool {
67
67
  return component.intersects(line) || component.intersectsRect(eraserRect);
68
68
  });
69
69
 
70
+ // Only erase components that could be selected (and thus interacted with)
71
+ // by the user.
72
+ const toErase = intersectingElems.filter(elem => elem.isSelectable());
73
+
70
74
  // Remove any intersecting elements.
71
- this.toRemove.push(...intersectingElems);
75
+ this.toRemove.push(...toErase);
72
76
 
73
77
  // Create new Erase commands for the now-to-be-erased elements and apply them.
74
- const newPartialCommands = intersectingElems.map(elem => new Erase([ elem ]));
78
+ const newPartialCommands = toErase.map(elem => new Erase([ elem ]));
75
79
  newPartialCommands.forEach(cmd => cmd.apply(this.editor));
76
80
 
77
81
  this.partialCommands.push(...newPartialCommands);
@@ -1,10 +1,11 @@
1
1
 
2
2
  import Editor from '../Editor';
3
- import { Mat33, Pointer, PointerDevice, Vec2 } from '../lib';
3
+ import { Mat33, Vec2 } from '../lib';
4
4
  import createEditor from '../testing/createEditor';
5
+ import sendTouchEvent from '../testing/sendTouchEvent';
5
6
  import { InputEvtType } from '../types';
6
7
  import waitForTimeout from '../util/waitForTimeout';
7
- import PanZoom from './PanZoom';
8
+ import PanZoom, { PanZoomMode } from './PanZoom';
8
9
 
9
10
  const selectPanZom = (editor: Editor): PanZoom => {
10
11
  const primaryTools = editor.toolController.getPrimaryTools();
@@ -13,27 +14,6 @@ const selectPanZom = (editor: Editor): PanZoom => {
13
14
  return panZoom;
14
15
  };
15
16
 
16
- const sendTouchEvent = (
17
- editor: Editor,
18
- eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
19
- screenPos: Vec2,
20
- ) => {
21
- const canvasPos = editor.viewport.screenToCanvas(screenPos);
22
-
23
- const ptrId = 0;
24
- const mainPointer = Pointer.ofCanvasPoint(
25
- canvasPos, eventType !== InputEvtType.PointerUpEvt, editor.viewport, ptrId, PointerDevice.Touch
26
- );
27
-
28
- editor.toolController.dispatchInputEvent({
29
- kind: eventType,
30
- allPointers: [
31
- mainPointer,
32
- ],
33
- current: mainPointer,
34
- });
35
- };
36
-
37
17
  describe('PanZoom', () => {
38
18
  it('touch and drag should pan, then inertial scroll', async () => {
39
19
  const editor = createEditor();
@@ -63,4 +43,268 @@ describe('PanZoom', () => {
63
43
  const afterDelayTranslation = editor.viewport.canvasToScreen(Vec2.zero);
64
44
  expect(afterDelayTranslation.minus(updatedTranslation).magnitude()).toBeGreaterThan(0);
65
45
  });
46
+
47
+ it('should scale the view based on distance between two touches', () => {
48
+ const editor = createEditor();
49
+ selectPanZom(editor);
50
+ editor.viewport.resetTransform(Mat33.identity);
51
+
52
+ let firstPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
53
+ let secondPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(100, 0), [ firstPointer ]);
54
+
55
+ let expectedScale = 1;
56
+ expect(editor.viewport.getScaleFactor()).toBe(expectedScale);
57
+ expect(editor.viewport.canvasToScreen(Vec2.zero)).objEq(Vec2.zero);
58
+
59
+ const maxIterations = 10;
60
+ for (let i = 0; i < maxIterations; i++) {
61
+ jest.advanceTimersByTime(100);
62
+
63
+ const point1 = Vec2.of(-i * 5, 0);
64
+ const point2 = Vec2.of(i * 5 + 100, 0);
65
+
66
+ const eventType = InputEvtType.PointerMoveEvt;
67
+
68
+ firstPointer = sendTouchEvent(editor, eventType, point1, [ secondPointer ]);
69
+ secondPointer = sendTouchEvent(editor, eventType, point2, [ firstPointer ]);
70
+ expectedScale = point1.minus(point2).magnitude() / 100;
71
+ Vec2.zero;
72
+ if (i === maxIterations - 1) {
73
+ jest.advanceTimersByTime(10);
74
+
75
+ sendTouchEvent(editor, InputEvtType.PointerUpEvt, point1, [ secondPointer ]);
76
+ sendTouchEvent(editor, InputEvtType.PointerUpEvt, point2);
77
+ }
78
+
79
+ jest.advanceTimersByTime(100);
80
+
81
+ expect(editor.viewport.getRotationAngle()).toBe(0);
82
+ expect(editor.viewport.getScaleFactor()).toBeCloseTo(expectedScale);
83
+
84
+ // Center of touches should remain roughly center
85
+ // (One touch is updating before the other, so there will be some leftwards drift)
86
+ const translation = editor.viewport.canvasToScreen(Vec2.zero).minus(Vec2.zero);
87
+ expect(translation.magnitude()).toBeLessThanOrEqual(i * 10);
88
+ }
89
+ });
90
+
91
+ it('should zoom in to point within current screen', () => {
92
+ // This sequence of touches and initial zoom was found to cause issues in
93
+ // 4abe27ff8e7913155828f98dee77b09c57c51d30 and before.
94
+ const initialCanvasTransform = new Mat33(832845.8900685566,0,-62599136.228663616,0,832845.8900685566,-86864630.94239436,0,0,1);
95
+ const touchData = [
96
+ { touch1Point: {'x':776.41796875,'y':161.8515625}, touch2Point: {'x':794.09765625,'y':321.3984375}, },
97
+ { touch1Point: {'x':776.41796875,'y':159.7578125}, touch2Point: {'x':794.09765625,'y':321.3984375}, },
98
+ { touch1Point: {'x':776.41796875,'y':159.7578125}, touch2Point: {'x':794.09765625,'y':318.2578125}, },
99
+ { touch1Point: {'x':777.45703125,'y':157.6640625}, touch2Point: {'x':794.09765625,'y':318.2578125}, },
100
+ { touch1Point: {'x':777.45703125,'y':157.6640625}, touch2Point: {'x':794.09765625,'y':316.16796875}, },
101
+ { touch1Point: {'x':777.9765625,'y':155.57421875}, touch2Point: {'x':794.09765625,'y':316.16796875}, },
102
+ { touch1Point: {'x':777.9765625,'y':155.57421875}, touch2Point: {'x':795.13671875,'y':313.02734375}, },
103
+ { touch1Point: {'x':778.49609375,'y':153.48046875}, touch2Point: {'x':795.13671875,'y':313.02734375}, },
104
+ { touch1Point: {'x':778.49609375,'y':153.48046875}, touch2Point: {'x':795.65625,'y':307.796875}, },
105
+ { touch1Point: {'x':779.015625,'y':150.34375}, touch2Point: {'x':795.65625,'y':307.796875}, },
106
+ { touch1Point: {'x':779.015625,'y':150.34375}, touch2Point: {'x':796.69921875,'y':301.51953125}, },
107
+ { touch1Point: {'x':779.53515625,'y':146.15625}, touch2Point: {'x':796.69921875,'y':301.51953125}, },
108
+ { touch1Point: {'x':779.53515625,'y':146.15625}, touch2Point: {'x':797.73828125,'y':295.2421875}, },
109
+ { touch1Point: {'x':780.05859375,'y':141.97265625}, touch2Point: {'x':797.73828125,'y':295.2421875}, },
110
+ { touch1Point: {'x':780.05859375,'y':141.97265625}, touch2Point: {'x':799.296875,'y':288.44140625}, },
111
+ { touch1Point: {'x':780.578125,'y':136.7421875}, touch2Point: {'x':799.296875,'y':288.44140625}, },
112
+ { touch1Point: {'x':780.578125,'y':136.7421875}, touch2Point: {'x':800.859375,'y':280.59765625}, },
113
+ { touch1Point: {'x':781.09765625,'y':131.51171875}, touch2Point: {'x':800.859375,'y':280.59765625}, },
114
+ { touch1Point: {'x':781.09765625,'y':131.51171875}, touch2Point: {'x':801.8984375,'y':273.796875}, },
115
+ { touch1Point: {'x':781.6171875,'y':126.27734375}, touch2Point: {'x':801.8984375,'y':273.796875}, },
116
+ { touch1Point: {'x':781.6171875,'y':126.27734375}, touch2Point: {'x':802.41796875,'y':267.51953125}, },
117
+ { touch1Point: {'x':782.13671875,'y':120}, touch2Point: {'x':802.41796875,'y':267.51953125}, },
118
+ { touch1Point: {'x':782.13671875,'y':120}, touch2Point: {'x':802.9375,'y':261.2421875}, },
119
+ { touch1Point: {'x':782.65625,'y':113.72265625}, touch2Point: {'x':802.9375,'y':261.2421875}, },
120
+ { touch1Point: {'x':782.65625,'y':113.72265625}, touch2Point: {'x':803.45703125,'y':254.44140625}, },
121
+ { touch1Point: {'x':783.17578125,'y':107.96875}, touch2Point: {'x':803.45703125,'y':254.44140625}, },
122
+ { touch1Point: {'x':783.17578125,'y':107.96875}, touch2Point: {'x':803.9765625,'y':249.2109375}, },
123
+ { touch1Point: {'x':783.6953125,'y':102.21484375}, touch2Point: {'x':803.9765625,'y':249.2109375}, },
124
+ { touch1Point: {'x':783.6953125,'y':102.21484375}, touch2Point: {'x':803.9765625,'y':244.5}, },
125
+ { touch1Point: {'x':784.21875,'y':96.984375}, touch2Point: {'x':803.9765625,'y':244.5}, },
126
+ { touch1Point: {'x':784.21875,'y':96.984375}, touch2Point: {'x':803.9765625,'y':240.31640625}, },
127
+ { touch1Point: {'x':784.73828125,'y':92.80078125}, touch2Point: {'x':803.9765625,'y':240.31640625}, },
128
+ { touch1Point: {'x':784.73828125,'y':92.80078125}, touch2Point: {'x':803.9765625,'y':235.0859375}, },
129
+ { touch1Point: {'x':785.2578125,'y':89.13671875}, touch2Point: {'x':803.9765625,'y':235.0859375}, },
130
+ { touch1Point: {'x':785.2578125,'y':89.13671875}, touch2Point: {'x':803.9765625,'y':229.85546875}, },
131
+ { touch1Point: {'x':785.2578125,'y':86.5234375}, touch2Point: {'x':803.9765625,'y':229.85546875}, },
132
+ { touch1Point: {'x':785.2578125,'y':86.5234375}, touch2Point: {'x':803.9765625,'y':224.625}, },
133
+ { touch1Point: {'x':785.2578125,'y':83.90625}, touch2Point: {'x':803.9765625,'y':224.625}, },
134
+ { touch1Point: {'x':785.2578125,'y':83.90625}, touch2Point: {'x':803.9765625,'y':220.4375}, },
135
+ { touch1Point: {'x':785.2578125,'y':81.29296875}, touch2Point: {'x':803.9765625,'y':220.4375}, },
136
+ { touch1Point: {'x':785.2578125,'y':81.29296875}, touch2Point: {'x':803.9765625,'y':216.77734375}, },
137
+ { touch1Point: {'x':785.2578125,'y':78.67578125}, touch2Point: {'x':803.9765625,'y':216.77734375}, },
138
+ { touch1Point: {'x':785.2578125,'y':78.67578125}, touch2Point: {'x':803.9765625,'y':212.58984375}, },
139
+ { touch1Point: {'x':785.2578125,'y':76.05859375}, touch2Point: {'x':803.9765625,'y':212.58984375}, },
140
+ { touch1Point: {'x':785.2578125,'y':76.05859375}, touch2Point: {'x':803.9765625,'y':208.40625}, },
141
+ { touch1Point: {'x':785.2578125,'y':76.05859375}, touch2Point: {'x':803.45703125,'y':204.74609375}, },
142
+ { touch1Point: {'x':785.2578125,'y':72.921875}, touch2Point: {'x':803.45703125,'y':204.74609375}, },
143
+ { touch1Point: {'x':785.2578125,'y':72.921875}, touch2Point: {'x':802.9375,'y':201.08203125}, },
144
+ { touch1Point: {'x':785.2578125,'y':70.828125}, touch2Point: {'x':802.9375,'y':201.08203125}, },
145
+ { touch1Point: {'x':785.2578125,'y':70.828125}, touch2Point: {'x':802.41796875,'y':198.46875}, },
146
+ { touch1Point: {'x':785.2578125,'y':68.73828125}, touch2Point: {'x':802.41796875,'y':198.46875}, },
147
+ { touch1Point: {'x':785.2578125,'y':68.73828125}, touch2Point: {'x':801.8984375,'y':195.8515625}, },
148
+ { touch1Point: {'x':785.2578125,'y':66.64453125}, touch2Point: {'x':801.8984375,'y':195.8515625}, },
149
+ { touch1Point: {'x':785.2578125,'y':66.64453125}, touch2Point: {'x':801.37890625,'y':193.23828125}, },
150
+ { touch1Point: {'x':785.2578125,'y':64.55078125}, touch2Point: {'x':801.37890625,'y':193.23828125}, },
151
+ { touch1Point: {'x':785.2578125,'y':61.9375}, touch2Point: {'x':801.37890625,'y':193.23828125}, },
152
+ { touch1Point: {'x':785.2578125,'y':61.9375}, touch2Point: {'x':801.37890625,'y':190.09765625}, },
153
+ { touch1Point: {'x':785.2578125,'y':59.84375}, touch2Point: {'x':801.37890625,'y':190.09765625}, },
154
+ { touch1Point: {'x':785.77734375,'y':57.23046875}, touch2Point: {'x':801.37890625,'y':190.09765625}, },
155
+ { touch1Point: {'x':785.77734375,'y':57.23046875}, touch2Point: {'x':800.859375,'y':187.484375}, },
156
+ { touch1Point: {'x':785.77734375,'y':55.13671875}, touch2Point: {'x':800.859375,'y':187.484375}, },
157
+ { touch1Point: {'x':785.77734375,'y':55.13671875}, touch2Point: {'x':800.859375,'y':185.390625}, },
158
+ { touch1Point: {'x':785.77734375,'y':51.99609375}, touch2Point: {'x':800.859375,'y':185.390625}, },
159
+ { touch1Point: {'x':785.77734375,'y':51.99609375}, touch2Point: {'x':800.859375,'y':183.296875}, },
160
+ { touch1Point: {'x':785.77734375,'y':49.90625}, touch2Point: {'x':800.859375,'y':183.296875}, },
161
+ { touch1Point: {'x':785.77734375,'y':49.90625}, touch2Point: {'x':800.859375,'y':181.20703125}, },
162
+ { touch1Point: {'x':785.77734375,'y':47.8125}, touch2Point: {'x':800.859375,'y':181.20703125}, },
163
+ { touch1Point: {'x':771.21484375,'y':187.484375}, touch2Point: {'x':753.53515625,'y':321.921875}, },
164
+ { touch1Point: {'x':768.09765625,'y':185.390625}, touch2Point: {'x':753.53515625,'y':321.921875}, },
165
+ { touch1Point: {'x':763.9375,'y':183.296875}, touch2Point: {'x':753.53515625,'y':321.921875}, },
166
+ { touch1Point: {'x':760.81640625,'y':182.25}, touch2Point: {'x':753.53515625,'y':321.921875}, },
167
+ { touch1Point: {'x':760.81640625,'y':182.25}, touch2Point: {'x':756.13671875,'y':321.921875}, },
168
+ { touch1Point: {'x':757.6953125,'y':181.20703125}, touch2Point: {'x':756.13671875,'y':321.921875}, },
169
+ { touch1Point: {'x':755.09375,'y':180.16015625}, touch2Point: {'x':756.13671875,'y':321.921875}, },
170
+ { touch1Point: {'x':755.09375,'y':180.16015625}, touch2Point: {'x':758.21484375,'y':321.921875}, },
171
+ { touch1Point: {'x':753.015625,'y':179.63671875}, touch2Point: {'x':758.21484375,'y':321.921875}, },
172
+ { touch1Point: {'x':750.93359375,'y':179.11328125}, touch2Point: {'x':758.21484375,'y':321.921875}, },
173
+ { touch1Point: {'x':750.93359375,'y':179.11328125}, touch2Point: {'x':760.296875,'y':321.921875}, },
174
+ { touch1Point: {'x':748.85546875,'y':178.58984375}, touch2Point: {'x':760.296875,'y':321.921875}, },
175
+ { touch1Point: {'x':748.85546875,'y':178.58984375}, touch2Point: {'x':763.4140625,'y':321.3984375}, },
176
+ { touch1Point: {'x':745.734375,'y':177.54296875}, touch2Point: {'x':763.4140625,'y':321.3984375}, },
177
+ { touch1Point: {'x':745.734375,'y':177.54296875}, touch2Point: {'x':766.53515625,'y':320.3515625}, },
178
+ { touch1Point: {'x':742.09375,'y':176.49609375}, touch2Point: {'x':766.53515625,'y':320.3515625}, },
179
+ { touch1Point: {'x':742.09375,'y':176.49609375}, touch2Point: {'x':768.6171875,'y':319.3046875}, },
180
+ { touch1Point: {'x':740.015625,'y':175.97265625}, touch2Point: {'x':768.6171875,'y':319.3046875}, },
181
+ { touch1Point: {'x':737.93359375,'y':175.44921875}, touch2Point: {'x':768.6171875,'y':319.3046875}, },
182
+ { touch1Point: {'x':737.93359375,'y':175.44921875}, touch2Point: {'x':772.2578125,'y':317.21484375}, },
183
+ { touch1Point: {'x':735.33203125,'y':174.9296875}, touch2Point: {'x':772.2578125,'y':317.21484375}, },
184
+ { touch1Point: {'x':733.25390625,'y':174.40625}, touch2Point: {'x':772.2578125,'y':317.21484375}, },
185
+ { touch1Point: {'x':733.25390625,'y':174.40625}, touch2Point: {'x':775.375,'y':315.12109375}, },
186
+ { touch1Point: {'x':731.171875,'y':173.8828125}, touch2Point: {'x':775.375,'y':315.12109375}, },
187
+ { touch1Point: {'x':729.09375,'y':173.8828125}, touch2Point: {'x':775.375,'y':315.12109375}, },
188
+ { touch1Point: {'x':729.09375,'y':173.8828125}, touch2Point: {'x':778.49609375,'y':313.02734375}, },
189
+ { touch1Point: {'x':727.01171875,'y':173.8828125}, touch2Point: {'x':778.49609375,'y':313.02734375}, },
190
+ { touch1Point: {'x':727.01171875,'y':173.8828125}, touch2Point: {'x':782.13671875,'y':310.9375}, },
191
+ { touch1Point: {'x':723.89453125,'y':173.8828125}, touch2Point: {'x':782.13671875,'y':310.9375}, },
192
+ { touch1Point: {'x':723.89453125,'y':173.8828125}, touch2Point: {'x':784.21875,'y':309.890625}, },
193
+ { touch1Point: {'x':723.89453125,'y':173.8828125}, touch2Point: {'x':786.296875,'y':308.84375}, },
194
+ { touch1Point: {'x':721.8125,'y':173.8828125}, touch2Point: {'x':786.296875,'y':308.84375}, },
195
+ { touch1Point: {'x':721.8125,'y':173.8828125}, touch2Point: {'x':788.37890625,'y':307.796875}, },
196
+ { touch1Point: {'x':721.8125,'y':173.8828125}, touch2Point: {'x':790.45703125,'y':306.75}, },
197
+ { touch1Point: {'x':719.734375,'y':173.8828125}, touch2Point: {'x':790.45703125,'y':306.75}, },
198
+ { touch1Point: {'x':719.734375,'y':173.8828125}, touch2Point: {'x':792.5390625,'y':305.703125}, },
199
+ { touch1Point: {'x':719.734375,'y':173.8828125}, touch2Point: {'x':795.65625,'y':303.61328125}, },
200
+ { touch1Point: {'x':717.65234375,'y':173.8828125}, touch2Point: {'x':795.65625,'y':303.61328125}, },
201
+ { touch1Point: {'x':717.65234375,'y':173.8828125}, touch2Point: {'x':798.77734375,'y':302.56640625}, },
202
+ { touch1Point: {'x':717.65234375,'y':173.8828125}, touch2Point: {'x':801.8984375,'y':300.47265625}, },
203
+ { touch1Point: {'x':715.5703125,'y':173.8828125}, touch2Point: {'x':801.8984375,'y':300.47265625}, },
204
+ { touch1Point: {'x':715.5703125,'y':173.8828125}, touch2Point: {'x':805.01953125,'y':297.859375}, },
205
+ { touch1Point: {'x':715.5703125,'y':173.8828125}, touch2Point: {'x':806.578125,'y':296.2890625}, },
206
+ { touch1Point: {'x':713.4921875,'y':173.8828125}, touch2Point: {'x':806.578125,'y':296.2890625}, },
207
+ { touch1Point: {'x':713.4921875,'y':173.8828125}, touch2Point: {'x':809.1796875,'y':294.1953125}, },
208
+ { touch1Point: {'x':711.41015625,'y':173.359375}, touch2Point: {'x':809.1796875,'y':294.1953125}, },
209
+ { touch1Point: {'x':711.41015625,'y':173.359375}, touch2Point: {'x':811.2578125,'y':292.10546875}, },
210
+ { touch1Point: {'x':711.41015625,'y':173.359375}, touch2Point: {'x':813.859375,'y':290.01171875}, },
211
+ { touch1Point: {'x':709.33203125,'y':173.359375}, touch2Point: {'x':813.859375,'y':290.01171875}, },
212
+ { touch1Point: {'x':709.33203125,'y':173.359375}, touch2Point: {'x':817.5,'y':287.91796875}, },
213
+ { touch1Point: {'x':707.25,'y':173.359375}, touch2Point: {'x':817.5,'y':287.91796875}, },
214
+ { touch1Point: {'x':707.25,'y':173.359375}, touch2Point: {'x':820.62109375,'y':285.828125}, },
215
+ { touch1Point: {'x':705.171875,'y':173.359375}, touch2Point: {'x':820.62109375,'y':285.828125}, },
216
+ { touch1Point: {'x':705.171875,'y':173.359375}, touch2Point: {'x':823.73828125,'y':283.734375}, },
217
+ { touch1Point: {'x':705.171875,'y':173.359375}, touch2Point: {'x':825.8203125,'y':282.6875}, },
218
+ { touch1Point: {'x':703.08984375,'y':173.8828125}, touch2Point: {'x':825.8203125,'y':282.6875}, },
219
+ { touch1Point: {'x':703.08984375,'y':173.8828125}, touch2Point: {'x':827.8984375,'y':281.640625}, },
220
+ { touch1Point: {'x':703.08984375,'y':173.8828125}, touch2Point: {'x':829.98046875,'y':280.59765625}, },
221
+ { touch1Point: {'x':700.4921875,'y':174.9296875}, touch2Point: {'x':829.98046875,'y':280.59765625}, },
222
+ { touch1Point: {'x':700.4921875,'y':174.9296875}, touch2Point: {'x':833.1015625,'y':278.50390625}, },
223
+ { touch1Point: {'x':697.890625,'y':175.97265625}, touch2Point: {'x':833.1015625,'y':278.50390625}, },
224
+ { touch1Point: {'x':697.890625,'y':175.97265625}, touch2Point: {'x':836.22265625,'y':277.45703125}, },
225
+ { touch1Point: {'x':694.76953125,'y':177.54296875}, touch2Point: {'x':836.22265625,'y':277.45703125}, },
226
+ { touch1Point: {'x':694.76953125,'y':177.54296875}, touch2Point: {'x':838.8203125,'y':276.41015625}, },
227
+ { touch1Point: {'x':691.6484375,'y':180.16015625}, touch2Point: {'x':838.8203125,'y':276.41015625}, },
228
+ { touch1Point: {'x':690.08984375,'y':181.7265625}, touch2Point: {'x':838.8203125,'y':276.41015625}, },
229
+ { touch1Point: {'x':688.53125,'y':183.296875}, touch2Point: {'x':838.8203125,'y':276.41015625}, },
230
+ { touch1Point: {'x':688.53125,'y':183.296875}, touch2Point: {'x':840.3828125,'y':274.83984375}, },
231
+ { touch1Point: {'x':686.96875,'y':185.390625}, touch2Point: {'x':840.3828125,'y':274.83984375}, },
232
+ { touch1Point: {'x':685.41015625,'y':188.00390625}, touch2Point: {'x':840.3828125,'y':274.83984375}, },
233
+ { touch1Point: {'x':683.8515625,'y':190.62109375}, touch2Point: {'x':840.3828125,'y':274.83984375}, },
234
+ { touch1Point: {'x':683.8515625,'y':190.62109375}, touch2Point: {'x':841.94140625,'y':273.2734375}, },
235
+ { touch1Point: {'x':682.2890625,'y':194.28125}, touch2Point: {'x':841.94140625,'y':273.2734375}, },
236
+ { touch1Point: {'x':680.73046875,'y':197.9453125}, touch2Point: {'x':841.94140625,'y':273.2734375}, },
237
+ { touch1Point: {'x':679.16796875,'y':201.60546875}, touch2Point: {'x':841.94140625,'y':273.2734375}, },
238
+ { touch1Point: {'x':677.609375,'y':205.79296875}, touch2Point: {'x':841.94140625,'y':273.2734375}, },
239
+ { touch1Point: {'x':677.609375,'y':205.79296875}, touch2Point: {'x':844.0234375,'y':272.2265625}, },
240
+ { touch1Point: {'x':676.5703125,'y':209.453125}, touch2Point: {'x':844.0234375,'y':272.2265625}, },
241
+ { touch1Point: {'x':675.52734375,'y':212.58984375}, touch2Point: {'x':844.0234375,'y':272.2265625}, },
242
+ { touch1Point: {'x':674.48828125,'y':214.68359375}, touch2Point: {'x':844.0234375,'y':272.2265625}, },
243
+ { touch1Point: {'x':673.44921875,'y':216.77734375}, touch2Point: {'x':844.0234375,'y':272.2265625}, },
244
+ { touch1Point: {'x':672.9296875,'y':218.8671875}, touch2Point: {'x':844.0234375,'y':272.2265625}, },
245
+ { touch1Point: {'x':671.890625,'y':221.484375}, touch2Point: {'x':844.0234375,'y':272.2265625}, },
246
+ { touch1Point: {'x':671.890625,'y':221.484375}, touch2Point: {'x':844.0234375,'y':270.1328125}, },
247
+ { touch1Point: {'x':671.890625,'y':223.578125}, touch2Point: {'x':844.0234375,'y':270.1328125}, },
248
+ { touch1Point: {'x':671.890625,'y':223.578125}, touch2Point: {'x':844.0234375,'y':267.51953125}, },
249
+ { touch1Point: {'x':671.890625,'y':225.66796875}, touch2Point: {'x':844.0234375,'y':267.51953125}, },
250
+ { touch1Point: {'x':671.890625,'y':225.66796875}, touch2Point: {'x':844.0234375,'y':265.42578125}, },
251
+ { touch1Point: {'x':671.890625,'y':225.66796875}, touch2Point: {'x':844.54296875,'y':263.33203125}, },
252
+ { touch1Point: {'x':671.890625,'y':227.76171875}, touch2Point: {'x':844.54296875,'y':263.33203125}, },
253
+ { touch1Point: {'x':671.890625,'y':227.76171875}, touch2Point: {'x':844.54296875,'y':261.2421875}, },
254
+ { touch1Point: {'x':672.41015625,'y':229.85546875}, touch2Point: {'x':844.54296875,'y':261.2421875}, },
255
+ { touch1Point: {'x':672.41015625,'y':229.85546875}, touch2Point: {'x':844.54296875,'y':259.1484375}, },
256
+ { touch1Point: {'x':673.44921875,'y':231.9453125}, touch2Point: {'x':844.54296875,'y':259.1484375}, },
257
+ { touch1Point: {'x':673.44921875,'y':231.9453125}, touch2Point: {'x':844.54296875,'y':256.0078125}, },
258
+ { touch1Point: {'x':675.0078125,'y':234.5625}, touch2Point: {'x':844.54296875,'y':256.0078125}, },
259
+ { touch1Point: {'x':675.0078125,'y':234.5625}, touch2Point: {'x':844.54296875,'y':253.91796875}, },
260
+ { touch1Point: {'x':676.05078125,'y':236.65625}, touch2Point: {'x':844.54296875,'y':253.91796875}, },
261
+ { touch1Point: {'x':676.05078125,'y':236.65625}, touch2Point: {'x':844.54296875,'y':250.77734375}, },
262
+ { touch1Point: {'x':677.609375,'y':238.74609375}, touch2Point: {'x':844.54296875,'y':250.77734375}, },
263
+ { touch1Point: {'x':677.609375,'y':238.74609375}, touch2Point: {'x':844.54296875,'y':247.640625}, },
264
+ { touch1Point: {'x':678.6484375,'y':240.83984375}, touch2Point: {'x':844.54296875,'y':247.640625}, },
265
+ { touch1Point: {'x':678.6484375,'y':240.83984375}, touch2Point: {'x':844.54296875,'y':244.5}, },
266
+ { touch1Point: {'x':678.6484375,'y':240.83984375}, touch2Point: {'x':844.54296875,'y':241.36328125}, },
267
+ { touch1Point: {'x':680.73046875,'y':242.41015625}, touch2Point: {'x':844.54296875,'y':241.36328125}, },
268
+ { touch1Point: {'x':680.73046875,'y':242.41015625}, touch2Point: {'x':844.54296875,'y':238.74609375}, },
269
+ { touch1Point: {'x':682.2890625,'y':243.9765625}, touch2Point: {'x':844.54296875,'y':238.74609375}, },
270
+ { touch1Point: {'x':682.2890625,'y':243.9765625}, touch2Point: {'x':844.54296875,'y':236.65625}, },
271
+ { touch1Point: {'x':683.8515625,'y':245.546875}, touch2Point: {'x':844.54296875,'y':236.65625}, },
272
+ { touch1Point: {'x':683.8515625,'y':245.546875}, touch2Point: {'x':844.54296875,'y':234.5625}, },
273
+ { touch1Point: {'x':685.9296875,'y':246.0703125}, touch2Point: {'x':844.54296875,'y':234.5625}, },
274
+ { touch1Point: {'x':685.9296875,'y':246.0703125}, touch2Point: {'x':844.54296875,'y':232.46875}, },
275
+ ].map(touchPoints => {
276
+ return [
277
+ Vec2.ofXY(touchPoints.touch1Point),
278
+ Vec2.ofXY(touchPoints.touch2Point),
279
+ ];
280
+ });
281
+
282
+
283
+ const editor = createEditor();
284
+ selectPanZom(editor);
285
+ editor.viewport.resetTransform(initialCanvasTransform);
286
+
287
+ editor.toolController.getMatchingTools(PanZoom).forEach(tool => {
288
+ tool.setModeEnabled(PanZoomMode.RotationLocked, false);
289
+ });
290
+
291
+ let lastVisibleRect = editor.viewport.visibleRect;
292
+
293
+
294
+ let firstPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, touchData[0][0]);
295
+ let secondPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, touchData[0][1], [ firstPointer ]);
296
+
297
+ for (const [touch1Point, touch2Point] of touchData) {
298
+ jest.advanceTimersByTime(10);
299
+
300
+ firstPointer = sendTouchEvent(editor, InputEvtType.PointerMoveEvt, touch1Point, [ secondPointer ]);
301
+ secondPointer = sendTouchEvent(editor, InputEvtType.PointerMoveEvt, touch2Point, [ firstPointer ]);
302
+
303
+ const rectCenterDelta = editor.viewport.visibleRect.center.minus(lastVisibleRect.center);
304
+
305
+ expect(rectCenterDelta.magnitude()).toBeLessThan(lastVisibleRect.w);
306
+
307
+ lastVisibleRect = editor.viewport.visibleRect;
308
+ }
309
+ });
66
310
  });
@@ -106,6 +106,13 @@ export default class PanZoom extends BaseTool {
106
106
 
107
107
  // Returns information about the pointers in a gesture
108
108
  public computePinchData(p1: Pointer, p2: Pointer): PinchData {
109
+ // Swap the pointers to ensure consistent ordering.
110
+ if (p1.id < p2.id) {
111
+ const tmp = p1;
112
+ p1 = p2;
113
+ p2 = tmp;
114
+ }
115
+
109
116
  const screenBetween = p2.screenPos.minus(p1.screenPos);
110
117
  const angle = screenBetween.angle();
111
118
  const dist = screenBetween.magnitude();
@@ -211,6 +218,13 @@ export default class PanZoom extends BaseTool {
211
218
  // Snap the rotation
212
219
  if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
213
220
  fullRotation = roundedFullRotation;
221
+
222
+ // Work around a rotation/matrix multiply bug.
223
+ // (See commit after 4abe27ff8e7913155828f98dee77b09c57c51d30).
224
+ // TODO: Fix the underlying issue and remove this.
225
+ if (fullRotation !== 0) {
226
+ fullRotation += 0.0001;
227
+ }
214
228
  }
215
229
 
216
230
  return fullRotation - this.editor.viewport.getRotationAngle();
@@ -228,12 +242,12 @@ export default class PanZoom extends BaseTool {
228
242
  deltaRotation = this.toSnappedRotationDelta(angle);
229
243
  }
230
244
 
231
-
232
245
  this.updateVelocity(screenCenter);
233
246
 
234
247
  const transformUpdate = Mat33.translation(delta)
235
248
  .rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
236
249
  .rightMul(Mat33.zRotation(deltaRotation, canvasCenter));
250
+
237
251
  this.lastScreenCenter = screenCenter;
238
252
  this.lastDist = dist;
239
253
  this.transform = Viewport.transformBy(
@@ -324,7 +324,7 @@ export default class Selection {
324
324
  }
325
325
 
326
326
  this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
327
- return elem.intersectsRect(this.region);
327
+ return elem.intersectsRect(this.region) && elem.isSelectable();
328
328
  });
329
329
 
330
330
  if (singleItemSelectionMode && this.selectedElems.length > 0) {