js-draw 1.6.1 → 1.7.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 (60) hide show
  1. package/README.md +0 -2
  2. package/dist/Editor.css +30 -4
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +5 -0
  6. package/dist/cjs/Editor.js +53 -70
  7. package/dist/cjs/components/BackgroundComponent.js +6 -1
  8. package/dist/cjs/components/TextComponent.d.ts +1 -1
  9. package/dist/cjs/components/TextComponent.js +19 -12
  10. package/dist/cjs/localization.d.ts +2 -0
  11. package/dist/cjs/localization.js +2 -0
  12. package/dist/cjs/localizations/comments.js +1 -0
  13. package/dist/cjs/rendering/RenderablePathSpec.js +16 -1
  14. package/dist/cjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  15. package/dist/cjs/rendering/caching/CacheRecordManager.js +18 -0
  16. package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
  17. package/dist/cjs/rendering/caching/RenderingCache.js +3 -0
  18. package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -2
  19. package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
  20. package/dist/cjs/tools/SelectionTool/Selection.js +75 -48
  21. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  22. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +8 -3
  23. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  24. package/dist/cjs/tools/SelectionTool/SelectionTool.js +36 -16
  25. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  26. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
  27. package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  28. package/dist/cjs/tools/SelectionTool/TransformMode.js +52 -9
  29. package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  30. package/dist/cjs/util/listenForKeyboardEventsFrom.js +142 -0
  31. package/dist/cjs/version.js +1 -1
  32. package/dist/mjs/Editor.d.ts +5 -0
  33. package/dist/mjs/Editor.mjs +53 -70
  34. package/dist/mjs/components/BackgroundComponent.mjs +6 -1
  35. package/dist/mjs/components/TextComponent.d.ts +1 -1
  36. package/dist/mjs/components/TextComponent.mjs +19 -12
  37. package/dist/mjs/localization.d.ts +2 -0
  38. package/dist/mjs/localization.mjs +2 -0
  39. package/dist/mjs/localizations/comments.mjs +1 -0
  40. package/dist/mjs/rendering/RenderablePathSpec.mjs +16 -1
  41. package/dist/mjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  42. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +18 -0
  43. package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
  44. package/dist/mjs/rendering/caching/RenderingCache.mjs +3 -0
  45. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -2
  46. package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
  47. package/dist/mjs/tools/SelectionTool/Selection.mjs +75 -48
  48. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  49. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +8 -3
  50. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  51. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -16
  52. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  53. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
  54. package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  55. package/dist/mjs/tools/SelectionTool/TransformMode.mjs +52 -9
  56. package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  57. package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +140 -0
  58. package/dist/mjs/version.mjs +1 -1
  59. package/package.json +4 -4
  60. package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -149,9 +149,10 @@ class CanvasRenderer extends AbstractRenderer_1.default {
149
149
  return;
150
150
  }
151
151
  // If part of a huge object, it might be worth trimming the path
152
- if (this.currentObjectBBox?.containsRect(this.getViewport().visibleRect)) {
152
+ const visibleRect = this.getViewport().visibleRect;
153
+ if (this.currentObjectBBox?.containsRect(visibleRect)) {
153
154
  // Try to trim/remove parts of the path outside of the bounding box.
154
- path = (0, RenderablePathSpec_1.visualEquivalent)(path, this.getViewport().visibleRect);
155
+ path = (0, RenderablePathSpec_1.visualEquivalent)(path, visibleRect);
155
156
  }
156
157
  super.drawPath(path);
157
158
  }
@@ -15,7 +15,8 @@ export default class Selection {
15
15
  private transformers;
16
16
  private transform;
17
17
  private selectedElems;
18
- private container;
18
+ private outerContainer;
19
+ private innerContainer;
19
20
  private backgroundElem;
20
21
  private hasParent;
21
22
  constructor(startPoint: Point2, editor: Editor);
@@ -32,10 +33,10 @@ export default class Selection {
32
33
  get regionRotation(): number;
33
34
  get preTransformedScreenRegion(): Rect2;
34
35
  get preTransformedScreenRegionRotation(): number;
35
- get screenRegion(): Rect2;
36
+ getScreenRegion(): Rect2;
36
37
  get screenRegionRotation(): number;
37
38
  setTransform(transform: Mat33, preview?: boolean): void;
38
- finalizeTransform(): Promise<void>;
39
+ finalizeTransform(): void | Promise<void>;
39
40
  private static ApplyTransformationCommand;
40
41
  private previewTransformCmds;
41
42
  resolveToObjects(): boolean;
@@ -53,7 +54,7 @@ export default class Selection {
53
54
  onDragUpdate(pointer: Pointer): void;
54
55
  onDragEnd(): void;
55
56
  onDragCancel(): void;
56
- scrollTo(): Promise<void>;
57
+ scrollTo(): boolean;
57
58
  deleteSelectedObjects(): Command;
58
59
  private selectionDuplicatedAnimationTimeout;
59
60
  private runSelectionDuplicatedAnimation;
@@ -64,22 +64,34 @@ class Selection {
64
64
  resize: new TransformMode_1.ResizeTransformer(editor, this),
65
65
  rotate: new TransformMode_1.RotateTransformer(editor, this),
66
66
  };
67
- this.container = document.createElement('div');
67
+ // We need two containers for some CSS to apply (the outer container
68
+ // needs zero height, the inner needs to prevent the selection background
69
+ // from being visible outside of the editor).
70
+ this.outerContainer = document.createElement('div');
71
+ this.outerContainer.classList.add(`${SelectionTool_1.cssPrefix}selection-outer-container`);
72
+ this.innerContainer = document.createElement('div');
73
+ this.innerContainer.classList.add(`${SelectionTool_1.cssPrefix}selection-inner-container`);
68
74
  this.backgroundElem = document.createElement('div');
69
75
  this.backgroundElem.classList.add(`${SelectionTool_1.cssPrefix}selection-background`);
70
- this.container.appendChild(this.backgroundElem);
71
- const resizeHorizontalHandle = new SelectionHandle_1.default({
72
- action: SelectionHandle_1.HandleAction.ResizeX,
73
- side: math_1.Vec2.of(1, 0.5),
74
- }, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, types_1.ResizeMode.HorizontalOnly), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
75
- const resizeVerticalHandle = new SelectionHandle_1.default({
76
- action: SelectionHandle_1.HandleAction.ResizeY,
77
- side: math_1.Vec2.of(0.5, 1),
78
- }, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, types_1.ResizeMode.VerticalOnly), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
79
- const resizeBothHandle = new SelectionHandle_1.default({
80
- action: SelectionHandle_1.HandleAction.ResizeXY,
81
- side: math_1.Vec2.of(1, 1),
82
- }, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, types_1.ResizeMode.Both), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
76
+ this.innerContainer.appendChild(this.backgroundElem);
77
+ this.outerContainer.appendChild(this.innerContainer);
78
+ const makeResizeHandle = (mode, side) => {
79
+ const modeToAction = {
80
+ [types_1.ResizeMode.Both]: SelectionHandle_1.HandleAction.ResizeXY,
81
+ [types_1.ResizeMode.HorizontalOnly]: SelectionHandle_1.HandleAction.ResizeX,
82
+ [types_1.ResizeMode.VerticalOnly]: SelectionHandle_1.HandleAction.ResizeY,
83
+ };
84
+ return new SelectionHandle_1.default({
85
+ action: modeToAction[mode],
86
+ side,
87
+ }, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, mode), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
88
+ };
89
+ const resizeHorizontalHandles = [
90
+ makeResizeHandle(types_1.ResizeMode.HorizontalOnly, math_1.Vec2.of(0, 0.5)),
91
+ makeResizeHandle(types_1.ResizeMode.HorizontalOnly, math_1.Vec2.of(1, 0.5)),
92
+ ];
93
+ const resizeVerticalHandle = makeResizeHandle(types_1.ResizeMode.VerticalOnly, math_1.Vec2.of(0.5, 1));
94
+ const resizeBothHandle = makeResizeHandle(types_1.ResizeMode.Both, math_1.Vec2.of(1, 1));
83
95
  const rotationHandle = new SelectionHandle_1.default({
84
96
  action: SelectionHandle_1.HandleAction.Rotate,
85
97
  side: math_1.Vec2.of(0.5, 0),
@@ -87,7 +99,7 @@ class Selection {
87
99
  }, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
88
100
  this.handles = [
89
101
  resizeBothHandle,
90
- resizeHorizontalHandle,
102
+ ...resizeHorizontalHandles,
91
103
  resizeVerticalHandle,
92
104
  rotationHandle,
93
105
  ];
@@ -133,7 +145,7 @@ class Selection {
133
145
  get preTransformedScreenRegionRotation() {
134
146
  return this.editor.viewport.getRotationAngle();
135
147
  }
136
- get screenRegion() {
148
+ getScreenRegion() {
137
149
  const toScreen = this.editor.viewport.canvasToScreenTransform;
138
150
  const scaleFactor = this.editor.viewport.getScaleFactor();
139
151
  const screenCenter = toScreen.transformVec2(this.region.center);
@@ -146,29 +158,33 @@ class Selection {
146
158
  setTransform(transform, preview = true) {
147
159
  this.transform = transform;
148
160
  if (preview && this.hasParent) {
149
- this.scrollTo();
150
161
  this.previewTransformCmds();
151
162
  }
152
163
  }
153
164
  // Applies the current transformation to the selection
154
- async finalizeTransform() {
165
+ finalizeTransform() {
155
166
  const fullTransform = this.transform;
156
167
  const selectedElems = this.selectedElems;
157
168
  // Reset for the next drag
158
169
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
159
170
  this.transform = math_1.Mat33.identity;
171
+ this.scrollTo();
160
172
  // Make the commands undo-able.
161
173
  // Don't check for non-empty transforms because this breaks changing the
162
174
  // z-index of the just-transformed commands.
163
175
  //
164
176
  // TODO: Check whether the selectedElems are already all toplevel.
165
- await this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
177
+ const transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
166
178
  // Clear renderings of any in-progress transformations
167
179
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
168
180
  wetInkRenderer.clear();
181
+ return transformPromise;
169
182
  }
170
183
  // Preview the effects of the current transformation on the selection
171
184
  previewTransformCmds() {
185
+ if (this.selectedElems.length === 0) {
186
+ return;
187
+ }
172
188
  // Don't render what we're moving if it's likely to be slow.
173
189
  if (this.selectedElems.length > maxPreviewElemCount) {
174
190
  this.updateUI();
@@ -249,28 +265,29 @@ class Selection {
249
265
  if (!this.hasParent) {
250
266
  return;
251
267
  }
268
+ const screenRegion = this.getScreenRegion();
252
269
  // marginLeft, marginTop: Display relative to the top left of the selection overlay.
253
270
  // left, top don't work for this.
254
- this.backgroundElem.style.marginLeft = `${this.screenRegion.topLeft.x}px`;
255
- this.backgroundElem.style.marginTop = `${this.screenRegion.topLeft.y}px`;
256
- this.backgroundElem.style.width = `${this.screenRegion.width}px`;
257
- this.backgroundElem.style.height = `${this.screenRegion.height}px`;
271
+ this.backgroundElem.style.marginLeft = `${screenRegion.topLeft.x}px`;
272
+ this.backgroundElem.style.marginTop = `${screenRegion.topLeft.y}px`;
273
+ this.backgroundElem.style.width = `${screenRegion.width}px`;
274
+ this.backgroundElem.style.height = `${screenRegion.height}px`;
258
275
  const rotationDeg = this.screenRegionRotation * 180 / Math.PI;
259
276
  this.backgroundElem.style.transform = `rotate(${rotationDeg}deg)`;
260
277
  this.backgroundElem.style.transformOrigin = 'center';
261
278
  // If closer to perpendicular, apply different CSS
262
279
  const perpendicularClassName = `${SelectionTool_1.cssPrefix}rotated-near-perpendicular`;
263
280
  if (Math.abs(Math.sin(this.screenRegionRotation)) > 0.5) {
264
- this.container.classList.add(perpendicularClassName);
281
+ this.innerContainer.classList.add(perpendicularClassName);
265
282
  }
266
283
  else {
267
- this.container.classList.remove(perpendicularClassName);
284
+ this.innerContainer.classList.remove(perpendicularClassName);
268
285
  }
269
286
  for (const handle of this.handles) {
270
287
  handle.updatePosition();
271
288
  }
272
289
  }
273
- // Add/remove the contents of this' seleciton from the editor.
290
+ // Add/remove the contents of this seleciton from the editor.
274
291
  // Used to prevent previewed content from looking like duplicate content
275
292
  // while dragging.
276
293
  //
@@ -278,6 +295,9 @@ class Selection {
278
295
  // the editor image is likely to be slow.)
279
296
  //
280
297
  // If removed from the image, selected elements are drawn as wet ink.
298
+ //
299
+ // [inImage] should be `true` if the selected elements should be added to the
300
+ // main image, `false` if they should be removed.
281
301
  addRemoveSelectionFromImage(inImage) {
282
302
  // Don't hide elements if doing so will be slow.
283
303
  if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
@@ -320,17 +340,18 @@ class Selection {
320
340
  document.getSelection()?.removeAllRanges();
321
341
  this.targetHandle = null;
322
342
  let result = false;
343
+ this.backgroundDragging = false;
344
+ if (this.region.containsPoint(pointer.canvasPos)) {
345
+ this.backgroundDragging = true;
346
+ result = true;
347
+ }
323
348
  for (const handle of this.handles) {
324
349
  if (handle.containsPoint(pointer.canvasPos)) {
325
350
  this.targetHandle = handle;
351
+ this.backgroundDragging = false;
326
352
  result = true;
327
353
  }
328
354
  }
329
- this.backgroundDragging = false;
330
- if (this.region.containsPoint(pointer.canvasPos)) {
331
- this.backgroundDragging = true;
332
- result = true;
333
- }
334
355
  if (result) {
335
356
  this.removeDeletedElemsFromSelection();
336
357
  this.addRemoveSelectionFromImage(false);
@@ -368,23 +389,29 @@ class Selection {
368
389
  this.targetHandle = null;
369
390
  this.setTransform(math_1.Mat33.identity);
370
391
  this.addRemoveSelectionFromImage(true);
392
+ this.updateUI();
371
393
  }
372
394
  // Scroll the viewport to this. Does not zoom
373
- async scrollTo() {
395
+ scrollTo() {
374
396
  if (this.selectedElems.length === 0) {
375
- return;
397
+ return false;
376
398
  }
377
- const screenRect = new math_1.Rect2(0, 0, this.editor.display.width, this.editor.display.height);
378
- if (!screenRect.containsPoint(this.screenRegion.center)) {
379
- const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
380
- const screenDelta = this.screenRegion.center.minus(closestPoint);
381
- const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
382
- await this.editor.dispatchNoAnnounce(Viewport_1.default.transformBy(math_1.Mat33.translation(delta.times(-1))), false);
383
- // Re-renders clear wet ink, so we need to re-draw the preview
384
- // after the full re-render.
385
- await this.editor.queueRerender();
386
- this.previewTransformCmds();
399
+ const screenSize = this.editor.viewport.getScreenRectSize();
400
+ const screenRect = new math_1.Rect2(0, 0, screenSize.x, screenSize.y);
401
+ const selectionScreenRegion = this.getScreenRegion();
402
+ if (!screenRect.containsPoint(selectionScreenRegion.center)) {
403
+ const targetPointScreen = selectionScreenRegion.center;
404
+ const closestPointScreen = screenRect.getClosestPointOnBoundaryTo(targetPointScreen);
405
+ const closestPointCanvas = this.editor.viewport.screenToCanvas(closestPointScreen);
406
+ const targetPointCanvas = this.region.center;
407
+ const delta = closestPointCanvas.minus(targetPointCanvas);
408
+ this.editor.dispatchNoAnnounce(Viewport_1.default.transformBy(math_1.Mat33.translation(delta.times(0.5))), false);
409
+ this.editor.queueRerender().then(() => {
410
+ this.previewTransformCmds();
411
+ });
412
+ return true;
387
413
  }
414
+ return false;
388
415
  }
389
416
  deleteSelectedObjects() {
390
417
  if (this.backgroundDragging || this.targetHandle) {
@@ -429,10 +456,10 @@ class Selection {
429
456
  return duplicateCommand;
430
457
  }
431
458
  addTo(elem) {
432
- if (this.container.parentElement) {
433
- this.container.remove();
459
+ if (this.outerContainer.parentElement) {
460
+ this.outerContainer.remove();
434
461
  }
435
- elem.appendChild(this.container);
462
+ elem.appendChild(this.outerContainer);
436
463
  this.hasParent = true;
437
464
  }
438
465
  setToPoint(point) {
@@ -441,8 +468,8 @@ class Selection {
441
468
  this.updateUI();
442
469
  }
443
470
  cancelSelection() {
444
- if (this.container.parentElement) {
445
- this.container.remove();
471
+ if (this.outerContainer.parentElement) {
472
+ this.outerContainer.remove();
446
473
  }
447
474
  this.originalRegion = math_1.Rect2.empty;
448
475
  this.selectionTightBoundingBox = null;
@@ -16,7 +16,7 @@ export interface HandlePresentation {
16
16
  export declare const handleSize = 30;
17
17
  export type DragStartCallback = (startPoint: Point2) => void;
18
18
  export type DragUpdateCallback = (canvasPoint: Point2) => void;
19
- export type DragEndCallback = () => void;
19
+ export type DragEndCallback = () => Promise<void> | void;
20
20
  export default class SelectionHandle {
21
21
  readonly presentation: HandlePresentation;
22
22
  private readonly parent;
@@ -50,7 +50,7 @@ export default class SelectionHandle {
50
50
  private dragLastPos;
51
51
  handleDragStart(pointer: Pointer): void;
52
52
  handleDragUpdate(pointer: Pointer): void;
53
- handleDragEnd(): void;
53
+ handleDragEnd(): void | Promise<void>;
54
54
  setSnapToGrid(snap: boolean): void;
55
55
  isSnappingToGrid(): boolean;
56
56
  }
@@ -16,6 +16,7 @@ var HandleAction;
16
16
  HandleAction["ResizeX"] = "resize-x";
17
17
  HandleAction["ResizeY"] = "resize-y";
18
18
  })(HandleAction || (exports.HandleAction = HandleAction = {}));
19
+ // The *interactable* handle size. The visual size will be slightly smaller.
19
20
  exports.handleSize = 30;
20
21
  class SelectionHandle {
21
22
  constructor(presentation, parent, viewport, onDragStart, onDragUpdate, onDragEnd) {
@@ -28,10 +29,14 @@ class SelectionHandle {
28
29
  this.dragLastPos = null;
29
30
  this.element = document.createElement('div');
30
31
  this.element.classList.add(`${SelectionTool_1.cssPrefix}handle`, `${SelectionTool_1.cssPrefix}${presentation.action}`);
32
+ // Create a slightly smaller content/background element.
33
+ const visibleContent = document.createElement('div');
34
+ visibleContent.classList.add(`${SelectionTool_1.cssPrefix}content`);
35
+ this.element.appendChild(visibleContent);
31
36
  this.parentSide = presentation.side;
32
37
  const icon = presentation.icon;
33
38
  if (icon) {
34
- this.element.appendChild(icon);
39
+ visibleContent.appendChild(icon);
35
40
  icon.classList.add('icon');
36
41
  }
37
42
  if (presentation.action === HandleAction.Rotate) {
@@ -64,7 +69,7 @@ class SelectionHandle {
64
69
  * selection box.
65
70
  */
66
71
  getBBoxParentCoords() {
67
- const parentRect = this.parent.screenRegion;
72
+ const parentRect = this.parent.getScreenRegion();
68
73
  const size = math_1.Vec2.of(exports.handleSize, exports.handleSize);
69
74
  const topLeft = parentRect.size.scale(this.parentSide)
70
75
  // Center
@@ -118,7 +123,7 @@ class SelectionHandle {
118
123
  if (!this.dragLastPos) {
119
124
  return;
120
125
  }
121
- this.onDragEnd();
126
+ return this.onDragEnd();
122
127
  }
123
128
  setSnapToGrid(snap) {
124
129
  this.snapToGrid = snap;
@@ -13,13 +13,15 @@ export default class SelectionTool extends BaseTool {
13
13
  private expandingSelectionBox;
14
14
  private shiftKeyPressed;
15
15
  private snapToGrid;
16
+ private lastPointer;
17
+ private autoscroller;
16
18
  constructor(editor: Editor, description: string);
17
19
  private makeSelectionBox;
18
20
  private snapSelectionToGrid;
19
21
  private selectionBoxHandlingEvt;
20
22
  onPointerDown({ allPointers, current }: PointerEvt): boolean;
21
23
  onPointerMove(event: PointerEvt): void;
22
- private onGestureEnd;
24
+ private onMainPointerUpdated;
23
25
  onPointerUp(event: PointerEvt): void;
24
26
  onGestureCancel(): void;
25
27
  private lastSelectedObjects;
@@ -12,6 +12,7 @@ const SVGRenderer_1 = __importDefault(require("../../rendering/renderers/SVGRend
12
12
  const Selection_1 = __importDefault(require("./Selection"));
13
13
  const TextComponent_1 = __importDefault(require("../../components/TextComponent"));
14
14
  const keybindings_1 = require("../keybindings");
15
+ const ToPointerAutoscroller_1 = __importDefault(require("./ToPointerAutoscroller"));
15
16
  exports.cssPrefix = 'selection-tool-';
16
17
  // Allows users to select/transform portions of the `EditorImage`.
17
18
  // With respect to `extend`ing, `SelectionTool` is not stable.
@@ -23,8 +24,19 @@ class SelectionTool extends BaseTool_1.default {
23
24
  this.expandingSelectionBox = false;
24
25
  this.shiftKeyPressed = false;
25
26
  this.snapToGrid = false;
27
+ this.lastPointer = null;
26
28
  this.selectionBoxHandlingEvt = false;
27
29
  this.lastSelectedObjects = [];
30
+ this.autoscroller = new ToPointerAutoscroller_1.default(editor.viewport, (scrollBy) => {
31
+ editor.dispatch(Viewport_1.default.transformBy(math_1.Mat33.translation(scrollBy)), false);
32
+ // Update the selection box/content to match the new viewport.
33
+ if (this.lastPointer) {
34
+ // The viewport has changed -- ensure that the screen and canvas positions
35
+ // of the pointer are both correct
36
+ const updatedPointer = this.lastPointer.withScreenPosition(this.lastPointer.screenPos, editor.viewport);
37
+ this.onMainPointerUpdated(updatedPointer);
38
+ }
39
+ });
28
40
  this.handleOverlay = document.createElement('div');
29
41
  editor.createHTMLOverlay(this.handleOverlay);
30
42
  this.handleOverlay.style.display = 'none';
@@ -87,14 +99,22 @@ class SelectionTool extends BaseTool_1.default {
87
99
  this.expandingSelectionBox = this.shiftKeyPressed;
88
100
  this.makeSelectionBox(current.canvasPos);
89
101
  }
102
+ else {
103
+ // Only autoscroll if we're transforming an existing selection
104
+ this.autoscroller.start();
105
+ }
90
106
  return true;
91
107
  }
92
108
  return false;
93
109
  }
94
110
  onPointerMove(event) {
111
+ this.onMainPointerUpdated(event.current);
112
+ }
113
+ onMainPointerUpdated(currentPointer) {
114
+ this.lastPointer = currentPointer;
95
115
  if (!this.selectionBox)
96
116
  return;
97
- let currentPointer = event.current;
117
+ this.autoscroller.onPointerMove(currentPointer.screenPos);
98
118
  if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
99
119
  const screenPos = this.editor.viewport.canvasToScreen(this.startPoint);
100
120
  currentPointer = currentPointer.lockedToXYAxesScreen(screenPos, this.editor.viewport);
@@ -109,21 +129,8 @@ class SelectionTool extends BaseTool_1.default {
109
129
  this.selectionBox.setToPoint(currentPointer.canvasPos);
110
130
  }
111
131
  }
112
- // Called after a gestureCancel and a pointerUp
113
- onGestureEnd() {
114
- if (!this.selectionBox)
115
- return;
116
- if (!this.selectionBoxHandlingEvt) {
117
- // Expand/shrink the selection rectangle, if applicable
118
- this.selectionBox.resolveToObjects();
119
- this.onSelectionUpdated();
120
- }
121
- else {
122
- this.selectionBox.onDragEnd();
123
- }
124
- this.selectionBoxHandlingEvt = false;
125
- }
126
132
  onPointerUp(event) {
133
+ this.autoscroller.stop();
127
134
  if (!this.selectionBox)
128
135
  return;
129
136
  let currentPointer = event.current;
@@ -142,10 +149,20 @@ class SelectionTool extends BaseTool_1.default {
142
149
  ]);
143
150
  }
144
151
  else {
145
- this.onGestureEnd();
152
+ if (!this.selectionBoxHandlingEvt) {
153
+ // Expand/shrink the selection rectangle, if applicable
154
+ this.selectionBox.resolveToObjects();
155
+ this.onSelectionUpdated();
156
+ }
157
+ else {
158
+ this.selectionBox.onDragEnd();
159
+ }
160
+ this.selectionBoxHandlingEvt = false;
161
+ this.lastPointer = null;
146
162
  }
147
163
  }
148
164
  onGestureCancel() {
165
+ this.autoscroller.stop();
149
166
  if (this.selectionBoxHandlingEvt) {
150
167
  this.selectionBox?.onDragCancel();
151
168
  }
@@ -158,6 +175,8 @@ class SelectionTool extends BaseTool_1.default {
158
175
  this.prevSelectionBox = null;
159
176
  }
160
177
  this.expandingSelectionBox = false;
178
+ this.lastPointer = null;
179
+ this.selectionBoxHandlingEvt = false;
161
180
  }
162
181
  onSelectionUpdated() {
163
182
  const selectedItemCount = this.selectionBox?.getSelectedItemCount() ?? 0;
@@ -283,6 +302,7 @@ class SelectionTool extends BaseTool_1.default {
283
302
  const transform = math_1.Mat33.scaling2D(scaleFactor, this.editor.viewport.roundPoint(region.topLeft)).rightMul(math_1.Mat33.translation(regionCenter).rightMul(roundedRotationMatrix).rightMul(math_1.Mat33.translation(regionCenter.times(-1)))).rightMul(math_1.Mat33.translation(this.editor.viewport.roundPoint(math_1.Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))));
284
303
  const oldTransform = this.selectionBox.getTransform();
285
304
  this.selectionBox.setTransform(oldTransform.rightMul(transform));
305
+ this.selectionBox.scrollTo();
286
306
  }
287
307
  if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
288
308
  this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
@@ -0,0 +1,23 @@
1
+ import { Point2, Vec2 } from '@js-draw/math';
2
+ import Viewport from '../../Viewport';
3
+ type ScrollByCallback = (delta: Vec2) => void;
4
+ /**
5
+ * Automatically scrolls the viewport such that the user's pointer is visible.
6
+ */
7
+ export default class ToPointerAutoscroller {
8
+ private viewport;
9
+ private scrollByCanvasDelta;
10
+ private started;
11
+ private updateLoopId;
12
+ private updateLoopRunning;
13
+ private targetPoint;
14
+ private scrollRate;
15
+ constructor(viewport: Viewport, scrollByCanvasDelta: ScrollByCallback);
16
+ private getScrollForPoint;
17
+ start(): void;
18
+ onPointerMove(pointerScreenPosition: Point2): void;
19
+ stop(): void;
20
+ private startUpdateLoop;
21
+ private stopUpdateLoop;
22
+ }
23
+ export {};
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const math_1 = require("@js-draw/math");
7
+ const untilNextAnimationFrame_1 = __importDefault(require("../../util/untilNextAnimationFrame"));
8
+ /**
9
+ * Automatically scrolls the viewport such that the user's pointer is visible.
10
+ */
11
+ class ToPointerAutoscroller {
12
+ constructor(viewport, scrollByCanvasDelta) {
13
+ this.viewport = viewport;
14
+ this.scrollByCanvasDelta = scrollByCanvasDelta;
15
+ this.started = false;
16
+ this.updateLoopId = 0;
17
+ this.updateLoopRunning = false;
18
+ this.targetPoint = null;
19
+ this.scrollRate = 1000; // px/s
20
+ }
21
+ getScrollForPoint(screenPoint) {
22
+ const screenSize = this.viewport.getScreenRectSize();
23
+ const screenRect = new math_1.Rect2(0, 0, screenSize.x, screenSize.y);
24
+ // Starts autoscrolling when the cursor is **outside of** this region
25
+ const marginSize = 44;
26
+ const autoscrollBoundary = screenRect.grownBy(-marginSize);
27
+ if (autoscrollBoundary.containsPoint(screenPoint)) {
28
+ return math_1.Vec2.zero;
29
+ }
30
+ const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
31
+ const distToEdge = closestEdgePoint.minus(screenPoint).magnitude();
32
+ const toEdge = closestEdgePoint.minus(screenPoint);
33
+ // Go faster for points further away from the boundary.
34
+ const maximumScaleFactor = 1.25;
35
+ const scaleFactor = Math.min(distToEdge / marginSize, maximumScaleFactor);
36
+ return toEdge.normalizedOrZero().times(scaleFactor);
37
+ }
38
+ start() {
39
+ this.started = true;
40
+ }
41
+ onPointerMove(pointerScreenPosition) {
42
+ if (!this.started) {
43
+ return;
44
+ }
45
+ if (this.getScrollForPoint(pointerScreenPosition) === math_1.Vec2.zero) {
46
+ this.stopUpdateLoop();
47
+ }
48
+ else {
49
+ this.targetPoint = pointerScreenPosition;
50
+ this.startUpdateLoop();
51
+ }
52
+ }
53
+ stop() {
54
+ this.targetPoint = null;
55
+ this.started = false;
56
+ this.stopUpdateLoop();
57
+ }
58
+ startUpdateLoop() {
59
+ if (this.updateLoopRunning) {
60
+ return;
61
+ }
62
+ (async () => {
63
+ this.updateLoopId++;
64
+ const currentUpdateLoopId = this.updateLoopId;
65
+ let lastUpdateTime = performance.now();
66
+ while (this.updateLoopId === currentUpdateLoopId && this.targetPoint) {
67
+ this.updateLoopRunning = true;
68
+ const currentTime = performance.now();
69
+ const deltaTimeMs = currentTime - lastUpdateTime;
70
+ const scrollDirection = this.getScrollForPoint(this.targetPoint);
71
+ const screenScrollAmount = scrollDirection.times(this.scrollRate * deltaTimeMs / 1000);
72
+ this.scrollByCanvasDelta(this.viewport.screenToCanvasTransform.transformVec3(screenScrollAmount));
73
+ lastUpdateTime = currentTime;
74
+ await (0, untilNextAnimationFrame_1.default)();
75
+ }
76
+ this.updateLoopRunning = false;
77
+ })();
78
+ }
79
+ stopUpdateLoop() {
80
+ this.updateLoopId++;
81
+ }
82
+ }
83
+ exports.default = ToPointerAutoscroller;
@@ -9,26 +9,33 @@ export declare class DragTransformer {
9
9
  constructor(editor: Editor, selection: Selection);
10
10
  onDragStart(startPoint: Vec3): void;
11
11
  onDragUpdate(canvasPos: Vec3): void;
12
- onDragEnd(): void;
12
+ onDragEnd(): void | Promise<void>;
13
13
  }
14
14
  export declare class ResizeTransformer {
15
15
  private readonly editor;
16
16
  private selection;
17
17
  private mode;
18
18
  private dragStartPoint;
19
+ private transformOrigin;
20
+ private scaleRate;
19
21
  constructor(editor: Editor, selection: Selection);
20
22
  onDragStart(startPoint: Vec3, mode: ResizeMode): void;
23
+ private computeOriginAndScaleRate;
21
24
  onDragUpdate(canvasPos: Vec3): void;
22
- onDragEnd(): void;
25
+ onDragEnd(): void | Promise<void>;
23
26
  }
24
27
  export declare class RotateTransformer {
25
28
  private readonly editor;
26
29
  private selection;
27
30
  private startAngle;
31
+ private targetRotation;
32
+ private maximumDistFromStart;
33
+ private startPoint;
28
34
  constructor(editor: Editor, selection: Selection);
29
35
  private getAngle;
30
36
  private roundAngle;
31
37
  onDragStart(startPoint: Vec3): void;
38
+ private setRotationTo;
32
39
  onDragUpdate(canvasPos: Vec3): void;
33
- onDragEnd(): void;
40
+ onDragEnd(): void | Promise<void>;
34
41
  }