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
@@ -99,30 +99,33 @@ class TextComponent extends AbstractComponent {
99
99
  let bbox = null;
100
100
  const cursor = new TextComponent.TextCursor(this.transform, this.style);
101
101
  for (const textObject of this.textObjects) {
102
- const transform = cursor.update(textObject);
102
+ const transform = cursor.update(textObject).transform;
103
103
  const currentBBox = this.computeUntransformedBBoxOfPart(textObject).transformedBoundingBox(transform);
104
104
  bbox ??= currentBBox;
105
105
  bbox = bbox.union(currentBBox);
106
106
  }
107
107
  this.contentBBox = bbox ?? Rect2.empty;
108
108
  }
109
- renderInternal(canvas) {
109
+ renderInternal(canvas, visibleRect) {
110
110
  const cursor = new TextComponent.TextCursor(this.transform, this.style);
111
111
  for (const textObject of this.textObjects) {
112
- const transform = cursor.update(textObject);
112
+ const { transform, bbox } = cursor.update(textObject);
113
+ if (visibleRect && !visibleRect.intersects(bbox)) {
114
+ continue;
115
+ }
113
116
  if (typeof textObject === 'string') {
114
117
  canvas.drawText(textObject, transform, this.style);
115
118
  }
116
119
  else {
117
120
  canvas.pushTransform(transform);
118
- textObject.renderInternal(canvas);
121
+ textObject.renderInternal(canvas, visibleRect?.transformedBoundingBox(transform.inverse()));
119
122
  canvas.popTransform();
120
123
  }
121
124
  }
122
125
  }
123
- render(canvas, _visibleRect) {
126
+ render(canvas, visibleRect) {
124
127
  canvas.startObject(this.contentBBox);
125
- this.renderInternal(canvas);
128
+ this.renderInternal(canvas, visibleRect);
126
129
  canvas.endObject(this.getLoadSaveData());
127
130
  }
128
131
  getProportionalRenderingTime() {
@@ -132,7 +135,7 @@ class TextComponent extends AbstractComponent {
132
135
  const cursor = new TextComponent.TextCursor(this.transform, this.style);
133
136
  for (const subObject of this.textObjects) {
134
137
  // Convert canvas space to internal space relative to the current object.
135
- const invTransform = cursor.update(subObject).inverse();
138
+ const invTransform = cursor.update(subObject).transform.inverse();
136
139
  const transformedLine = lineSegment.transformedBy(invTransform);
137
140
  if (typeof subObject === 'string') {
138
141
  const textBBox = TextComponent.getTextDimens(subObject, this.style);
@@ -310,11 +313,11 @@ TextComponent.TextCursor = class {
310
313
  this.transform = Mat33.identity;
311
314
  }
312
315
  /**
313
- * Based on previous calls to `update`, returns the transformation of
314
- * the given `element` (including the parentTransform given to this cursor's
315
- * constructor).
316
+ * Based on previous calls to `update`, returns the transformation and bounding box (relative
317
+ * to the parent element, or if none, the canvas) of the given `element`. Note that
318
+ * this is computed in part using the `parentTransform` provivded to this cursor's constructor.
316
319
  *
317
- * The result does not take into account
320
+ * Warning: There may be edge cases here that are not taken into account.
318
321
  */
319
322
  update(elem) {
320
323
  let elementTransform = Mat33.identity;
@@ -353,7 +356,11 @@ TextComponent.TextCursor = class {
353
356
  // Update this.transform so that future calls to update return correct values.
354
357
  const endShiftTransform = Mat33.translation(Vec2.of(textSize.width, 0));
355
358
  this.transform = elementTransform.rightMul(elemInternalTransform).rightMul(endShiftTransform);
356
- return this.parentTransform.rightMul(elementTransform);
359
+ const transform = this.parentTransform.rightMul(elementTransform);
360
+ return {
361
+ transform,
362
+ bbox: textSize.transformedBoundingBox(transform),
363
+ };
357
364
  }
358
365
  };
359
366
  export default TextComponent;
@@ -182,11 +182,11 @@ class EditorImage {
182
182
  * @see {@link Display.flatten}
183
183
  */
184
184
  static addElement(elem, applyByFlattening = false) {
185
- return new EditorImage.AddElementCommand(elem, applyByFlattening);
185
+ return new _a.AddElementCommand(elem, applyByFlattening);
186
186
  }
187
187
  /** @see EditorImage.addElement */
188
188
  addElement(elem, applyByFlattening) {
189
- return EditorImage.addElement(elem, applyByFlattening);
189
+ return _a.addElement(elem, applyByFlattening);
190
190
  }
191
191
  /**
192
192
  * @returns a `Viewport` for rendering the image when importing/exporting.
@@ -205,7 +205,7 @@ class EditorImage {
205
205
  * autoresize (if it was previously enabled).
206
206
  */
207
207
  setImportExportRect(imageRect) {
208
- return EditorImage.SetImportExportRectCommand.of(this, imageRect, false);
208
+ return _a.SetImportExportRectCommand.of(this, imageRect, false);
209
209
  }
210
210
  getAutoresizeEnabled() {
211
211
  return this.shouldAutoresizeExportViewport;
@@ -216,7 +216,7 @@ class EditorImage {
216
216
  return Command.empty;
217
217
  }
218
218
  const newBBox = this.root.getBBox();
219
- return EditorImage.SetImportExportRectCommand.of(this, newBBox, autoresize);
219
+ return _a.SetImportExportRectCommand.of(this, newBBox, autoresize);
220
220
  }
221
221
  setAutoresizeEnabledDirectly(shouldAutoresize) {
222
222
  if (shouldAutoresize !== this.shouldAutoresizeExportViewport) {
@@ -322,7 +322,7 @@ EditorImage.AddElementCommand = (_b = class extends SerializableCommand {
322
322
  const id = json.elemData.id;
323
323
  const foundElem = editor.image.lookupElement(id);
324
324
  const elem = foundElem ?? AbstractComponent.deserialize(json.elemData);
325
- const result = new EditorImage.AddElementCommand(elem);
325
+ const result = new _a.AddElementCommand(elem);
326
326
  result.serializedElem = json.elemData;
327
327
  return result;
328
328
  });
@@ -331,7 +331,7 @@ EditorImage.AddElementCommand = (_b = class extends SerializableCommand {
331
331
  // Handles resizing the background import/export region of the image.
332
332
  EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand {
333
333
  constructor(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize) {
334
- super(EditorImage.SetImportExportRectCommand.commandId);
334
+ super(_a.SetImportExportRectCommand.commandId);
335
335
  this.originalSize = originalSize;
336
336
  this.originalTransform = originalTransform;
337
337
  this.originalAutoresize = originalAutoresize;
@@ -344,7 +344,7 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
344
344
  const originalSize = importExportViewport.visibleRect.size;
345
345
  const originalTransform = importExportViewport.canvasToScreenTransform;
346
346
  const originalAutoresize = image.getAutoresizeEnabled();
347
- return new EditorImage.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize);
347
+ return new _a.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize);
348
348
  }
349
349
  apply(editor) {
350
350
  editor.image.setAutoresizeEnabledDirectly(this.newAutoresize);
@@ -405,7 +405,7 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
405
405
  const finalRect = new Rect2(json.newRegion.x, json.newRegion.y, json.newRegion.w, json.newRegion.h);
406
406
  const autoresize = json.autoresize ?? false;
407
407
  const originalAutoresize = json.originalAutoresize ?? false;
408
- return new EditorImage.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, finalRect, autoresize);
408
+ return new _a.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, finalRect, autoresize);
409
409
  });
410
410
  })(),
411
411
  _c);
@@ -10,5 +10,7 @@ export interface EditorLocalization extends ToolbarLocalization, ToolLocalizatio
10
10
  doneLoading: string;
11
11
  loading: (percentage: number) => string;
12
12
  imageEditor: string;
13
+ softwareLibraries: string;
14
+ developerInformation: string;
13
15
  }
14
16
  export declare const defaultEditorLocalization: EditorLocalization;
@@ -19,4 +19,6 @@ export const defaultEditorLocalization = {
19
19
  doneLoading: 'Done loading',
20
20
  undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`,
21
21
  redoAnnouncement: (commandDescription) => `Redid ${commandDescription}`,
22
+ softwareLibraries: 'Libraries',
23
+ developerInformation: 'Developer information',
22
24
  };
@@ -2,6 +2,7 @@
2
2
  * Comments to help translators create translations.
3
3
  */
4
4
  const comments = {
5
+ pen: 'Likely unused',
5
6
  dragAndDropHereOrBrowse: 'Uses {{curly braces}} to denote bold text',
6
7
  closeSidebar: 'Currently used as an accessibilty label',
7
8
  };
@@ -21,11 +21,26 @@ export const visualEquivalent = (renderablePath, visibleRect) => {
21
21
  const path = pathFromRenderable(renderablePath);
22
22
  const strokeWidth = renderablePath.style.stroke?.width ?? 0;
23
23
  const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
24
+ const styledPathBBox = path.bbox.grownBy(strokeWidth);
25
+ // Are we close enough to the path that it fills the entire screen?
26
+ if (onlyStroked
27
+ && renderablePath.style.stroke
28
+ && strokeWidth > visibleRect.maxDimension
29
+ && styledPathBBox.containsRect(visibleRect)) {
30
+ const strokeRadius = strokeWidth / 2;
31
+ // Do a fast, but with many false negatives, check.
32
+ for (const point of path.startEndPoints()) {
33
+ // If within the strokeRadius of any point
34
+ if (visibleRect.isWithinRadiusOf(strokeRadius, point)) {
35
+ return pathToRenderable(Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color });
36
+ }
37
+ }
38
+ }
24
39
  // Scale the expanded rect --- the visual equivalent is only close for huge strokes.
25
40
  const expandedRect = visibleRect.grownBy(strokeWidth)
26
41
  .transformedBoundingBox(Mat33.scaling2D(4, visibleRect.center));
27
42
  // TODO: Handle simplifying very small paths.
28
- if (expandedRect.containsRect(path.bbox.grownBy(strokeWidth))) {
43
+ if (expandedRect.containsRect(styledPathBBox)) {
29
44
  return renderablePath;
30
45
  }
31
46
  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
  }
@@ -40,4 +40,22 @@ export class CacheRecordManager {
40
40
  this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
41
41
  return this.cacheRecords[0];
42
42
  }
43
+ // Returns information to (hopefully) help debug performance issues
44
+ getDebugInfo() {
45
+ let numberAllocd = 0;
46
+ let averageReassignedCount = 0;
47
+ for (const cacheRecord of this.cacheRecords) {
48
+ averageReassignedCount += cacheRecord.allocCount;
49
+ if (cacheRecord.isAllocd()) {
50
+ numberAllocd++;
51
+ }
52
+ }
53
+ averageReassignedCount /= Math.max(this.cacheRecords.length, 0);
54
+ const debugInfo = [
55
+ `${this.cacheRecords.length} cache records (max ${this.maxCanvases})`,
56
+ `${numberAllocd} assigned to screen regions`,
57
+ `Average number of times reassigned: ${Math.round(averageReassignedCount * 100) / 100}`,
58
+ ];
59
+ return debugInfo.join('\n');
60
+ }
43
61
  }
@@ -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
  }
@@ -41,4 +41,7 @@ export default class RenderingCache {
41
41
  image.render(screenRenderer, visibleRect);
42
42
  }
43
43
  }
44
+ getDebugInfo() {
45
+ return this.recordManager.getDebugInfo();
46
+ }
44
47
  }
@@ -144,9 +144,10 @@ export default class CanvasRenderer extends AbstractRenderer {
144
144
  return;
145
145
  }
146
146
  // If part of a huge object, it might be worth trimming the path
147
- if (this.currentObjectBBox?.containsRect(this.getViewport().visibleRect)) {
147
+ const visibleRect = this.getViewport().visibleRect;
148
+ if (this.currentObjectBBox?.containsRect(visibleRect)) {
148
149
  // Try to trim/remove parts of the path outside of the bounding box.
149
- path = visualEquivalent(path, this.getViewport().visibleRect);
150
+ path = visualEquivalent(path, visibleRect);
150
151
  }
151
152
  super.drawPath(path);
152
153
  }
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _BaseWidget_instances, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
12
+ var _BaseWidget_instances, _a, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
13
13
  import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler.mjs';
14
14
  import { keyPressEventFromHTMLEvent, keyUpEventFromHTMLEvent } from '../../inputEvents.mjs';
15
15
  import { toolbarCSSPrefix } from '../constants.mjs';
@@ -422,13 +422,13 @@ class BaseWidget {
422
422
  }
423
423
  }
424
424
  }
425
- _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() {
425
+ _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() {
426
426
  __classPrivateFieldGet(this, _BaseWidget_removeEditorListeners, "f")?.call(this);
427
427
  const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
428
428
  let removeKeyPressListener = null;
429
429
  // If the onKeyPress function has been extended and the editor is configured to send keypress events to
430
430
  // toolbar widgets,
431
- if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
431
+ if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== _a.prototype.onKeyPress) {
432
432
  const keyPressListener = (event) => this.onKeyPress(event);
433
433
  const handler = toolbarShortcutHandlers[0];
434
434
  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;
@@ -36,22 +36,34 @@ class Selection {
36
36
  resize: new ResizeTransformer(editor, this),
37
37
  rotate: new RotateTransformer(editor, this),
38
38
  };
39
- this.container = document.createElement('div');
39
+ // We need two containers for some CSS to apply (the outer container
40
+ // needs zero height, the inner needs to prevent the selection background
41
+ // from being visible outside of the editor).
42
+ this.outerContainer = document.createElement('div');
43
+ this.outerContainer.classList.add(`${cssPrefix}selection-outer-container`);
44
+ this.innerContainer = document.createElement('div');
45
+ this.innerContainer.classList.add(`${cssPrefix}selection-inner-container`);
40
46
  this.backgroundElem = document.createElement('div');
41
47
  this.backgroundElem.classList.add(`${cssPrefix}selection-background`);
42
- this.container.appendChild(this.backgroundElem);
43
- const resizeHorizontalHandle = new SelectionHandle({
44
- action: HandleAction.ResizeX,
45
- side: Vec2.of(1, 0.5),
46
- }, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.HorizontalOnly), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
47
- const resizeVerticalHandle = new SelectionHandle({
48
- action: HandleAction.ResizeY,
49
- side: Vec2.of(0.5, 1),
50
- }, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.VerticalOnly), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
51
- const resizeBothHandle = new SelectionHandle({
52
- action: HandleAction.ResizeXY,
53
- side: Vec2.of(1, 1),
54
- }, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.Both), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
48
+ this.innerContainer.appendChild(this.backgroundElem);
49
+ this.outerContainer.appendChild(this.innerContainer);
50
+ const makeResizeHandle = (mode, side) => {
51
+ const modeToAction = {
52
+ [ResizeMode.Both]: HandleAction.ResizeXY,
53
+ [ResizeMode.HorizontalOnly]: HandleAction.ResizeX,
54
+ [ResizeMode.VerticalOnly]: HandleAction.ResizeY,
55
+ };
56
+ return new SelectionHandle({
57
+ action: modeToAction[mode],
58
+ side,
59
+ }, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, mode), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
60
+ };
61
+ const resizeHorizontalHandles = [
62
+ makeResizeHandle(ResizeMode.HorizontalOnly, Vec2.of(0, 0.5)),
63
+ makeResizeHandle(ResizeMode.HorizontalOnly, Vec2.of(1, 0.5)),
64
+ ];
65
+ const resizeVerticalHandle = makeResizeHandle(ResizeMode.VerticalOnly, Vec2.of(0.5, 1));
66
+ const resizeBothHandle = makeResizeHandle(ResizeMode.Both, Vec2.of(1, 1));
55
67
  const rotationHandle = new SelectionHandle({
56
68
  action: HandleAction.Rotate,
57
69
  side: Vec2.of(0.5, 0),
@@ -59,7 +71,7 @@ class Selection {
59
71
  }, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
60
72
  this.handles = [
61
73
  resizeBothHandle,
62
- resizeHorizontalHandle,
74
+ ...resizeHorizontalHandles,
63
75
  resizeVerticalHandle,
64
76
  rotationHandle,
65
77
  ];
@@ -105,7 +117,7 @@ class Selection {
105
117
  get preTransformedScreenRegionRotation() {
106
118
  return this.editor.viewport.getRotationAngle();
107
119
  }
108
- get screenRegion() {
120
+ getScreenRegion() {
109
121
  const toScreen = this.editor.viewport.canvasToScreenTransform;
110
122
  const scaleFactor = this.editor.viewport.getScaleFactor();
111
123
  const screenCenter = toScreen.transformVec2(this.region.center);
@@ -118,27 +130,33 @@ class Selection {
118
130
  setTransform(transform, preview = true) {
119
131
  this.transform = transform;
120
132
  if (preview && this.hasParent) {
121
- this.scrollTo();
122
133
  this.previewTransformCmds();
123
134
  }
124
135
  }
125
136
  // Applies the current transformation to the selection
126
- async finalizeTransform() {
137
+ finalizeTransform() {
127
138
  const fullTransform = this.transform;
128
139
  const selectedElems = this.selectedElems;
129
140
  // Reset for the next drag
130
141
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
131
142
  this.transform = Mat33.identity;
132
- // Make the commands undo-able, but only if the transform is non-empty.
133
- if (!fullTransform.eq(Mat33.identity)) {
134
- await this.editor.dispatch(new Selection.ApplyTransformationCommand(this, selectedElems, fullTransform));
135
- }
143
+ this.scrollTo();
144
+ // Make the commands undo-able.
145
+ // Don't check for non-empty transforms because this breaks changing the
146
+ // z-index of the just-transformed commands.
147
+ //
148
+ // TODO: Check whether the selectedElems are already all toplevel.
149
+ const transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
136
150
  // Clear renderings of any in-progress transformations
137
151
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
138
152
  wetInkRenderer.clear();
153
+ return transformPromise;
139
154
  }
140
155
  // Preview the effects of the current transformation on the selection
141
156
  previewTransformCmds() {
157
+ if (this.selectedElems.length === 0) {
158
+ return;
159
+ }
142
160
  // Don't render what we're moving if it's likely to be slow.
143
161
  if (this.selectedElems.length > maxPreviewElemCount) {
144
162
  this.updateUI();
@@ -219,28 +237,29 @@ class Selection {
219
237
  if (!this.hasParent) {
220
238
  return;
221
239
  }
240
+ const screenRegion = this.getScreenRegion();
222
241
  // marginLeft, marginTop: Display relative to the top left of the selection overlay.
223
242
  // left, top don't work for this.
224
- this.backgroundElem.style.marginLeft = `${this.screenRegion.topLeft.x}px`;
225
- this.backgroundElem.style.marginTop = `${this.screenRegion.topLeft.y}px`;
226
- this.backgroundElem.style.width = `${this.screenRegion.width}px`;
227
- this.backgroundElem.style.height = `${this.screenRegion.height}px`;
243
+ this.backgroundElem.style.marginLeft = `${screenRegion.topLeft.x}px`;
244
+ this.backgroundElem.style.marginTop = `${screenRegion.topLeft.y}px`;
245
+ this.backgroundElem.style.width = `${screenRegion.width}px`;
246
+ this.backgroundElem.style.height = `${screenRegion.height}px`;
228
247
  const rotationDeg = this.screenRegionRotation * 180 / Math.PI;
229
248
  this.backgroundElem.style.transform = `rotate(${rotationDeg}deg)`;
230
249
  this.backgroundElem.style.transformOrigin = 'center';
231
250
  // If closer to perpendicular, apply different CSS
232
251
  const perpendicularClassName = `${cssPrefix}rotated-near-perpendicular`;
233
252
  if (Math.abs(Math.sin(this.screenRegionRotation)) > 0.5) {
234
- this.container.classList.add(perpendicularClassName);
253
+ this.innerContainer.classList.add(perpendicularClassName);
235
254
  }
236
255
  else {
237
- this.container.classList.remove(perpendicularClassName);
256
+ this.innerContainer.classList.remove(perpendicularClassName);
238
257
  }
239
258
  for (const handle of this.handles) {
240
259
  handle.updatePosition();
241
260
  }
242
261
  }
243
- // Add/remove the contents of this' seleciton from the editor.
262
+ // Add/remove the contents of this seleciton from the editor.
244
263
  // Used to prevent previewed content from looking like duplicate content
245
264
  // while dragging.
246
265
  //
@@ -248,6 +267,9 @@ class Selection {
248
267
  // the editor image is likely to be slow.)
249
268
  //
250
269
  // If removed from the image, selected elements are drawn as wet ink.
270
+ //
271
+ // [inImage] should be `true` if the selected elements should be added to the
272
+ // main image, `false` if they should be removed.
251
273
  addRemoveSelectionFromImage(inImage) {
252
274
  // Don't hide elements if doing so will be slow.
253
275
  if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
@@ -290,17 +312,18 @@ class Selection {
290
312
  document.getSelection()?.removeAllRanges();
291
313
  this.targetHandle = null;
292
314
  let result = false;
315
+ this.backgroundDragging = false;
316
+ if (this.region.containsPoint(pointer.canvasPos)) {
317
+ this.backgroundDragging = true;
318
+ result = true;
319
+ }
293
320
  for (const handle of this.handles) {
294
321
  if (handle.containsPoint(pointer.canvasPos)) {
295
322
  this.targetHandle = handle;
323
+ this.backgroundDragging = false;
296
324
  result = true;
297
325
  }
298
326
  }
299
- this.backgroundDragging = false;
300
- if (this.region.containsPoint(pointer.canvasPos)) {
301
- this.backgroundDragging = true;
302
- result = true;
303
- }
304
327
  if (result) {
305
328
  this.removeDeletedElemsFromSelection();
306
329
  this.addRemoveSelectionFromImage(false);
@@ -338,23 +361,29 @@ class Selection {
338
361
  this.targetHandle = null;
339
362
  this.setTransform(Mat33.identity);
340
363
  this.addRemoveSelectionFromImage(true);
364
+ this.updateUI();
341
365
  }
342
366
  // Scroll the viewport to this. Does not zoom
343
- async scrollTo() {
367
+ scrollTo() {
344
368
  if (this.selectedElems.length === 0) {
345
- return;
369
+ return false;
346
370
  }
347
- const screenRect = new Rect2(0, 0, this.editor.display.width, this.editor.display.height);
348
- if (!screenRect.containsPoint(this.screenRegion.center)) {
349
- const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
350
- const screenDelta = this.screenRegion.center.minus(closestPoint);
351
- const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
352
- await this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(-1))), false);
353
- // Re-renders clear wet ink, so we need to re-draw the preview
354
- // after the full re-render.
355
- await this.editor.queueRerender();
356
- this.previewTransformCmds();
371
+ const screenSize = this.editor.viewport.getScreenRectSize();
372
+ const screenRect = new Rect2(0, 0, screenSize.x, screenSize.y);
373
+ const selectionScreenRegion = this.getScreenRegion();
374
+ if (!screenRect.containsPoint(selectionScreenRegion.center)) {
375
+ const targetPointScreen = selectionScreenRegion.center;
376
+ const closestPointScreen = screenRect.getClosestPointOnBoundaryTo(targetPointScreen);
377
+ const closestPointCanvas = this.editor.viewport.screenToCanvas(closestPointScreen);
378
+ const targetPointCanvas = this.region.center;
379
+ const delta = closestPointCanvas.minus(targetPointCanvas);
380
+ this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(0.5))), false);
381
+ this.editor.queueRerender().then(() => {
382
+ this.previewTransformCmds();
383
+ });
384
+ return true;
357
385
  }
386
+ return false;
358
387
  }
359
388
  deleteSelectedObjects() {
360
389
  if (this.backgroundDragging || this.targetHandle) {
@@ -382,7 +411,7 @@ class Selection {
382
411
  if (wasTransforming) {
383
412
  // Don't update the selection's focus when redoing/undoing
384
413
  const selectionToUpdate = null;
385
- tmpApplyCommand = new Selection.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
414
+ tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
386
415
  // Transform to ensure that the duplicates are in the correct location
387
416
  await tmpApplyCommand.apply(this.editor);
388
417
  // Show items again
@@ -399,10 +428,10 @@ class Selection {
399
428
  return duplicateCommand;
400
429
  }
401
430
  addTo(elem) {
402
- if (this.container.parentElement) {
403
- this.container.remove();
431
+ if (this.outerContainer.parentElement) {
432
+ this.outerContainer.remove();
404
433
  }
405
- elem.appendChild(this.container);
434
+ elem.appendChild(this.outerContainer);
406
435
  this.hasParent = true;
407
436
  }
408
437
  setToPoint(point) {
@@ -411,8 +440,8 @@ class Selection {
411
440
  this.updateUI();
412
441
  }
413
442
  cancelSelection() {
414
- if (this.container.parentElement) {
415
- this.container.remove();
443
+ if (this.outerContainer.parentElement) {
444
+ this.outerContainer.remove();
416
445
  }
417
446
  this.originalRegion = Rect2.empty;
418
447
  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
  }
@@ -13,6 +13,7 @@ export var HandleAction;
13
13
  HandleAction["ResizeX"] = "resize-x";
14
14
  HandleAction["ResizeY"] = "resize-y";
15
15
  })(HandleAction || (HandleAction = {}));
16
+ // The *interactable* handle size. The visual size will be slightly smaller.
16
17
  export const handleSize = 30;
17
18
  export default class SelectionHandle {
18
19
  constructor(presentation, parent, viewport, onDragStart, onDragUpdate, onDragEnd) {
@@ -25,10 +26,14 @@ export default class SelectionHandle {
25
26
  this.dragLastPos = null;
26
27
  this.element = document.createElement('div');
27
28
  this.element.classList.add(`${cssPrefix}handle`, `${cssPrefix}${presentation.action}`);
29
+ // Create a slightly smaller content/background element.
30
+ const visibleContent = document.createElement('div');
31
+ visibleContent.classList.add(`${cssPrefix}content`);
32
+ this.element.appendChild(visibleContent);
28
33
  this.parentSide = presentation.side;
29
34
  const icon = presentation.icon;
30
35
  if (icon) {
31
- this.element.appendChild(icon);
36
+ visibleContent.appendChild(icon);
32
37
  icon.classList.add('icon');
33
38
  }
34
39
  if (presentation.action === HandleAction.Rotate) {
@@ -61,7 +66,7 @@ export default class SelectionHandle {
61
66
  * selection box.
62
67
  */
63
68
  getBBoxParentCoords() {
64
- const parentRect = this.parent.screenRegion;
69
+ const parentRect = this.parent.getScreenRegion();
65
70
  const size = Vec2.of(handleSize, handleSize);
66
71
  const topLeft = parentRect.size.scale(this.parentSide)
67
72
  // Center
@@ -115,7 +120,7 @@ export default class SelectionHandle {
115
120
  if (!this.dragLastPos) {
116
121
  return;
117
122
  }
118
- this.onDragEnd();
123
+ return this.onDragEnd();
119
124
  }
120
125
  setSnapToGrid(snap) {
121
126
  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;