js-draw 1.6.0 → 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 (65) hide show
  1. package/README.md +4 -6
  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/image/EditorImage.js +8 -8
  11. package/dist/cjs/localization.d.ts +2 -0
  12. package/dist/cjs/localization.js +2 -0
  13. package/dist/cjs/localizations/comments.js +1 -0
  14. package/dist/cjs/rendering/RenderablePathSpec.js +16 -1
  15. package/dist/cjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  16. package/dist/cjs/rendering/caching/CacheRecordManager.js +18 -0
  17. package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
  18. package/dist/cjs/rendering/caching/RenderingCache.js +3 -0
  19. package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -2
  20. package/dist/cjs/toolbar/widgets/BaseWidget.js +3 -3
  21. package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
  22. package/dist/cjs/tools/SelectionTool/Selection.js +81 -52
  23. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  24. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +8 -3
  25. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  26. package/dist/cjs/tools/SelectionTool/SelectionTool.js +36 -16
  27. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  28. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
  29. package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  30. package/dist/cjs/tools/SelectionTool/TransformMode.js +52 -9
  31. package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  32. package/dist/cjs/util/listenForKeyboardEventsFrom.js +142 -0
  33. package/dist/cjs/version.js +1 -1
  34. package/dist/mjs/Editor.d.ts +5 -0
  35. package/dist/mjs/Editor.mjs +53 -70
  36. package/dist/mjs/components/BackgroundComponent.mjs +6 -1
  37. package/dist/mjs/components/TextComponent.d.ts +1 -1
  38. package/dist/mjs/components/TextComponent.mjs +19 -12
  39. package/dist/mjs/image/EditorImage.mjs +8 -8
  40. package/dist/mjs/localization.d.ts +2 -0
  41. package/dist/mjs/localization.mjs +2 -0
  42. package/dist/mjs/localizations/comments.mjs +1 -0
  43. package/dist/mjs/rendering/RenderablePathSpec.mjs +16 -1
  44. package/dist/mjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  45. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +18 -0
  46. package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
  47. package/dist/mjs/rendering/caching/RenderingCache.mjs +3 -0
  48. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -2
  49. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +3 -3
  50. package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
  51. package/dist/mjs/tools/SelectionTool/Selection.mjs +81 -52
  52. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  53. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +8 -3
  54. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  55. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -16
  56. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  57. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
  58. package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  59. package/dist/mjs/tools/SelectionTool/TransformMode.mjs +52 -9
  60. package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  61. package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +140 -0
  62. package/dist/mjs/version.mjs +1 -1
  63. package/docs/img/readme-images/js-draw.png +0 -0
  64. package/package.json +6 -6
  65. package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -26,11 +26,26 @@ const visualEquivalent = (renderablePath, visibleRect) => {
26
26
  const path = (0, exports.pathFromRenderable)(renderablePath);
27
27
  const strokeWidth = renderablePath.style.stroke?.width ?? 0;
28
28
  const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
29
+ const styledPathBBox = path.bbox.grownBy(strokeWidth);
30
+ // Are we close enough to the path that it fills the entire screen?
31
+ if (onlyStroked
32
+ && renderablePath.style.stroke
33
+ && strokeWidth > visibleRect.maxDimension
34
+ && styledPathBBox.containsRect(visibleRect)) {
35
+ const strokeRadius = strokeWidth / 2;
36
+ // Do a fast, but with many false negatives, check.
37
+ for (const point of path.startEndPoints()) {
38
+ // If within the strokeRadius of any point
39
+ if (visibleRect.isWithinRadiusOf(strokeRadius, point)) {
40
+ return (0, exports.pathToRenderable)(math_1.Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color });
41
+ }
42
+ }
43
+ }
29
44
  // Scale the expanded rect --- the visual equivalent is only close for huge strokes.
30
45
  const expandedRect = visibleRect.grownBy(strokeWidth)
31
46
  .transformedBoundingBox(math_1.Mat33.scaling2D(4, visibleRect.center));
32
47
  // TODO: Handle simplifying very small paths.
33
- if (expandedRect.containsRect(path.bbox.grownBy(strokeWidth))) {
48
+ if (expandedRect.containsRect(styledPathBBox)) {
34
49
  return renderablePath;
35
50
  }
36
51
  const parts = [];
@@ -9,4 +9,5 @@ export declare class CacheRecordManager {
9
9
  setSharedState(state: CacheState): void;
10
10
  allocCanvas(drawTo: Rect2, onDealloc: BeforeDeallocCallback): CacheRecord;
11
11
  private getLeastRecentlyUsedRecord;
12
+ getDebugInfo(): string;
12
13
  }
@@ -46,5 +46,23 @@ class CacheRecordManager {
46
46
  this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
47
47
  return this.cacheRecords[0];
48
48
  }
49
+ // Returns information to (hopefully) help debug performance issues
50
+ getDebugInfo() {
51
+ let numberAllocd = 0;
52
+ let averageReassignedCount = 0;
53
+ for (const cacheRecord of this.cacheRecords) {
54
+ averageReassignedCount += cacheRecord.allocCount;
55
+ if (cacheRecord.isAllocd()) {
56
+ numberAllocd++;
57
+ }
58
+ }
59
+ averageReassignedCount /= Math.max(this.cacheRecords.length, 0);
60
+ const debugInfo = [
61
+ `${this.cacheRecords.length} cache records (max ${this.maxCanvases})`,
62
+ `${numberAllocd} assigned to screen regions`,
63
+ `Average number of times reassigned: ${Math.round(averageReassignedCount * 100) / 100}`,
64
+ ];
65
+ return debugInfo.join('\n');
66
+ }
49
67
  }
50
68
  exports.CacheRecordManager = CacheRecordManager;
@@ -8,4 +8,5 @@ export default class RenderingCache {
8
8
  private rootNode;
9
9
  constructor(cacheProps: CacheProps);
10
10
  render(screenRenderer: AbstractRenderer, image: ImageNode, viewport: Viewport): void;
11
+ getDebugInfo(): string;
11
12
  }
@@ -46,5 +46,8 @@ class RenderingCache {
46
46
  image.render(screenRenderer, visibleRect);
47
47
  }
48
48
  }
49
+ getDebugInfo() {
50
+ return this.recordManager.getDebugInfo();
51
+ }
49
52
  }
50
53
  exports.default = RenderingCache;
@@ -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
  }
@@ -13,7 +13,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
13
13
  var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
15
15
  };
16
- var _BaseWidget_instances, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
16
+ var _BaseWidget_instances, _a, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.ToolbarWidgetTag = void 0;
19
19
  const ToolbarShortcutHandler_1 = __importDefault(require("../../tools/ToolbarShortcutHandler"));
@@ -428,13 +428,13 @@ class BaseWidget {
428
428
  }
429
429
  }
430
430
  }
431
- _BaseWidget_hasDropdown = new WeakMap(), _BaseWidget_disabledDueToReadOnlyEditor = new WeakMap(), _BaseWidget_tags = new WeakMap(), _BaseWidget_removeEditorListeners = new WeakMap(), _BaseWidget_instances = new WeakSet(), _BaseWidget_addEditorListeners = function _BaseWidget_addEditorListeners() {
431
+ _a = BaseWidget, _BaseWidget_hasDropdown = new WeakMap(), _BaseWidget_disabledDueToReadOnlyEditor = new WeakMap(), _BaseWidget_tags = new WeakMap(), _BaseWidget_removeEditorListeners = new WeakMap(), _BaseWidget_instances = new WeakSet(), _BaseWidget_addEditorListeners = function _BaseWidget_addEditorListeners() {
432
432
  __classPrivateFieldGet(this, _BaseWidget_removeEditorListeners, "f")?.call(this);
433
433
  const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler_1.default);
434
434
  let removeKeyPressListener = null;
435
435
  // If the onKeyPress function has been extended and the editor is configured to send keypress events to
436
436
  // toolbar widgets,
437
- if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
437
+ if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== _a.prototype.onKeyPress) {
438
438
  const keyPressListener = (event) => this.onKeyPress(event);
439
439
  const handler = toolbarShortcutHandlers[0];
440
440
  handler.registerListener(keyPressListener);
@@ -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,27 +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;
160
- // Make the commands undo-able, but only if the transform is non-empty.
161
- if (!fullTransform.eq(math_1.Mat33.identity)) {
162
- await this.editor.dispatch(new Selection.ApplyTransformationCommand(this, selectedElems, fullTransform));
163
- }
171
+ this.scrollTo();
172
+ // Make the commands undo-able.
173
+ // Don't check for non-empty transforms because this breaks changing the
174
+ // z-index of the just-transformed commands.
175
+ //
176
+ // TODO: Check whether the selectedElems are already all toplevel.
177
+ const transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
164
178
  // Clear renderings of any in-progress transformations
165
179
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
166
180
  wetInkRenderer.clear();
181
+ return transformPromise;
167
182
  }
168
183
  // Preview the effects of the current transformation on the selection
169
184
  previewTransformCmds() {
185
+ if (this.selectedElems.length === 0) {
186
+ return;
187
+ }
170
188
  // Don't render what we're moving if it's likely to be slow.
171
189
  if (this.selectedElems.length > maxPreviewElemCount) {
172
190
  this.updateUI();
@@ -247,28 +265,29 @@ class Selection {
247
265
  if (!this.hasParent) {
248
266
  return;
249
267
  }
268
+ const screenRegion = this.getScreenRegion();
250
269
  // marginLeft, marginTop: Display relative to the top left of the selection overlay.
251
270
  // left, top don't work for this.
252
- this.backgroundElem.style.marginLeft = `${this.screenRegion.topLeft.x}px`;
253
- this.backgroundElem.style.marginTop = `${this.screenRegion.topLeft.y}px`;
254
- this.backgroundElem.style.width = `${this.screenRegion.width}px`;
255
- 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`;
256
275
  const rotationDeg = this.screenRegionRotation * 180 / Math.PI;
257
276
  this.backgroundElem.style.transform = `rotate(${rotationDeg}deg)`;
258
277
  this.backgroundElem.style.transformOrigin = 'center';
259
278
  // If closer to perpendicular, apply different CSS
260
279
  const perpendicularClassName = `${SelectionTool_1.cssPrefix}rotated-near-perpendicular`;
261
280
  if (Math.abs(Math.sin(this.screenRegionRotation)) > 0.5) {
262
- this.container.classList.add(perpendicularClassName);
281
+ this.innerContainer.classList.add(perpendicularClassName);
263
282
  }
264
283
  else {
265
- this.container.classList.remove(perpendicularClassName);
284
+ this.innerContainer.classList.remove(perpendicularClassName);
266
285
  }
267
286
  for (const handle of this.handles) {
268
287
  handle.updatePosition();
269
288
  }
270
289
  }
271
- // Add/remove the contents of this' seleciton from the editor.
290
+ // Add/remove the contents of this seleciton from the editor.
272
291
  // Used to prevent previewed content from looking like duplicate content
273
292
  // while dragging.
274
293
  //
@@ -276,6 +295,9 @@ class Selection {
276
295
  // the editor image is likely to be slow.)
277
296
  //
278
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.
279
301
  addRemoveSelectionFromImage(inImage) {
280
302
  // Don't hide elements if doing so will be slow.
281
303
  if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
@@ -318,17 +340,18 @@ class Selection {
318
340
  document.getSelection()?.removeAllRanges();
319
341
  this.targetHandle = null;
320
342
  let result = false;
343
+ this.backgroundDragging = false;
344
+ if (this.region.containsPoint(pointer.canvasPos)) {
345
+ this.backgroundDragging = true;
346
+ result = true;
347
+ }
321
348
  for (const handle of this.handles) {
322
349
  if (handle.containsPoint(pointer.canvasPos)) {
323
350
  this.targetHandle = handle;
351
+ this.backgroundDragging = false;
324
352
  result = true;
325
353
  }
326
354
  }
327
- this.backgroundDragging = false;
328
- if (this.region.containsPoint(pointer.canvasPos)) {
329
- this.backgroundDragging = true;
330
- result = true;
331
- }
332
355
  if (result) {
333
356
  this.removeDeletedElemsFromSelection();
334
357
  this.addRemoveSelectionFromImage(false);
@@ -366,23 +389,29 @@ class Selection {
366
389
  this.targetHandle = null;
367
390
  this.setTransform(math_1.Mat33.identity);
368
391
  this.addRemoveSelectionFromImage(true);
392
+ this.updateUI();
369
393
  }
370
394
  // Scroll the viewport to this. Does not zoom
371
- async scrollTo() {
395
+ scrollTo() {
372
396
  if (this.selectedElems.length === 0) {
373
- return;
397
+ return false;
374
398
  }
375
- const screenRect = new math_1.Rect2(0, 0, this.editor.display.width, this.editor.display.height);
376
- if (!screenRect.containsPoint(this.screenRegion.center)) {
377
- const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
378
- const screenDelta = this.screenRegion.center.minus(closestPoint);
379
- const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
380
- await this.editor.dispatchNoAnnounce(Viewport_1.default.transformBy(math_1.Mat33.translation(delta.times(-1))), false);
381
- // Re-renders clear wet ink, so we need to re-draw the preview
382
- // after the full re-render.
383
- await this.editor.queueRerender();
384
- 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;
385
413
  }
414
+ return false;
386
415
  }
387
416
  deleteSelectedObjects() {
388
417
  if (this.backgroundDragging || this.targetHandle) {
@@ -410,7 +439,7 @@ class Selection {
410
439
  if (wasTransforming) {
411
440
  // Don't update the selection's focus when redoing/undoing
412
441
  const selectionToUpdate = null;
413
- tmpApplyCommand = new Selection.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
442
+ tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
414
443
  // Transform to ensure that the duplicates are in the correct location
415
444
  await tmpApplyCommand.apply(this.editor);
416
445
  // Show items again
@@ -427,10 +456,10 @@ class Selection {
427
456
  return duplicateCommand;
428
457
  }
429
458
  addTo(elem) {
430
- if (this.container.parentElement) {
431
- this.container.remove();
459
+ if (this.outerContainer.parentElement) {
460
+ this.outerContainer.remove();
432
461
  }
433
- elem.appendChild(this.container);
462
+ elem.appendChild(this.outerContainer);
434
463
  this.hasParent = true;
435
464
  }
436
465
  setToPoint(point) {
@@ -439,8 +468,8 @@ class Selection {
439
468
  this.updateUI();
440
469
  }
441
470
  cancelSelection() {
442
- if (this.container.parentElement) {
443
- this.container.remove();
471
+ if (this.outerContainer.parentElement) {
472
+ this.outerContainer.remove();
444
473
  }
445
474
  this.originalRegion = math_1.Rect2.empty;
446
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 {};