js-draw 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -174,7 +174,7 @@ export declare class Editor {
174
174
  * editor.dispatchNoAnnounce(editor.viewport.zoomTo(someRectangle), addToHistory);
175
175
  * ```
176
176
  */
177
- dispatchNoAnnounce(command: Command, addToHistory?: boolean): void;
177
+ dispatchNoAnnounce(command: Command, addToHistory?: boolean): void | Promise<void>;
178
178
  /**
179
179
  * Apply a large transformation in chunks.
180
180
  * If `apply` is `false`, the commands are unapplied.
@@ -186,8 +186,15 @@ export declare class Editor {
186
186
  asyncUnapplyCommands(commands: Command[], chunkSize: number): Promise<void>;
187
187
  private announceUndoCallback;
188
188
  private announceRedoCallback;
189
+ private nextRerenderListeners;
189
190
  private rerenderQueued;
190
- queueRerender(): void;
191
+ /**
192
+ * Schedule a re-render for some time in the near future. Does not schedule an additional
193
+ * re-render if a re-render is already queued.
194
+ *
195
+ * @returns a promise that resolves when
196
+ */
197
+ queueRerender(): Promise<void>;
191
198
  rerender(showImageBounds?: boolean): void;
192
199
  drawWetInk(...path: RenderablePathSpec[]): void;
193
200
  clearWetInk(): void;
@@ -84,6 +84,8 @@ export class Editor {
84
84
  this.announceRedoCallback = (command) => {
85
85
  this.announceForAccessibility(this.localization.redoAnnouncement(command.description(this, this.localization)));
86
86
  };
87
+ // Listeners to be called once at the end of the next re-render.
88
+ this.nextRerenderListeners = [];
87
89
  this.rerenderQueued = false;
88
90
  this.localization = Object.assign(Object.assign({}, getLocalizationTable()), settings.localization);
89
91
  // Fill default settings.
@@ -474,13 +476,9 @@ export class Editor {
474
476
  }
475
477
  /** `apply` a command. `command` will be announced for accessibility. */
476
478
  dispatch(command, addToHistory = true) {
477
- if (addToHistory) {
478
- const apply = false; // Don't double-apply
479
- this.history.push(command, apply);
480
- }
481
- const applyResult = command.apply(this);
479
+ const dispatchResult = this.dispatchNoAnnounce(command, addToHistory);
482
480
  this.announceForAccessibility(command.description(this, this.localization));
483
- return applyResult;
481
+ return dispatchResult;
484
482
  }
485
483
  /**
486
484
  * Dispatches a command without announcing it. By default, does not add to history.
@@ -499,11 +497,10 @@ export class Editor {
499
497
  */
500
498
  dispatchNoAnnounce(command, addToHistory = false) {
501
499
  if (addToHistory) {
502
- this.history.push(command);
503
- }
504
- else {
505
- command.apply(this);
500
+ const apply = false; // Don't double-apply
501
+ this.history.push(command, apply);
506
502
  }
503
+ return command.apply(this);
507
504
  }
508
505
  /**
509
506
  * Apply a large transformation in chunks.
@@ -546,16 +543,27 @@ export class Editor {
546
543
  asyncUnapplyCommands(commands, chunkSize) {
547
544
  return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
548
545
  }
549
- // Schedule a re-render for some time in the near future. Does not schedule an additional
550
- // re-render if a re-render is already queued.
546
+ /**
547
+ * Schedule a re-render for some time in the near future. Does not schedule an additional
548
+ * re-render if a re-render is already queued.
549
+ *
550
+ * @returns a promise that resolves when
551
+ */
551
552
  queueRerender() {
552
553
  if (!this.rerenderQueued) {
553
554
  this.rerenderQueued = true;
554
555
  requestAnimationFrame(() => {
555
- this.rerender();
556
- this.rerenderQueued = false;
556
+ // If .rerender was called manually, we might not need to
557
+ // re-render.
558
+ if (this.rerenderQueued) {
559
+ this.rerender();
560
+ this.rerenderQueued = false;
561
+ }
557
562
  });
558
563
  }
564
+ return new Promise(resolve => {
565
+ this.nextRerenderListeners.push(() => resolve());
566
+ });
559
567
  }
560
568
  rerender(showImageBounds = true) {
561
569
  this.display.startRerender();
@@ -572,6 +580,8 @@ export class Editor {
572
580
  renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
573
581
  }
574
582
  this.rerenderQueued = false;
583
+ this.nextRerenderListeners.forEach(listener => listener());
584
+ this.nextRerenderListeners = [];
575
585
  }
576
586
  drawWetInk(...path) {
577
587
  for (const part of path) {
@@ -2,7 +2,7 @@ var _a;
2
2
  import AbstractComponent from './components/AbstractComponent';
3
3
  import Rect2 from './math/Rect2';
4
4
  import SerializableCommand from './commands/SerializableCommand';
5
- // @internal
5
+ // @internal Sort by z-index, low to high
6
6
  export const sortLeavesByZIndex = (leaves) => {
7
7
  leaves.sort((a, b) => a.getContent().getZIndex() - b.getContent().getZIndex());
8
8
  };
@@ -4,8 +4,8 @@ import Editor from '../Editor';
4
4
  export default class Eraser extends BaseTool {
5
5
  private editor;
6
6
  private lastPoint;
7
- private command;
8
7
  private toRemove;
8
+ private partialCommands;
9
9
  constructor(editor: Editor, description: string);
10
10
  onPointerDown(event: PointerEvt): boolean;
11
11
  onPointerMove(event: PointerEvt): void;
@@ -6,7 +6,8 @@ export default class Eraser extends BaseTool {
6
6
  constructor(editor, description) {
7
7
  super(editor.notifier, description);
8
8
  this.editor = editor;
9
- this.command = null;
9
+ // Commands that each remove one element
10
+ this.partialCommands = [];
10
11
  }
11
12
  onPointerDown(event) {
12
13
  if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
@@ -17,35 +18,34 @@ export default class Eraser extends BaseTool {
17
18
  return false;
18
19
  }
19
20
  onPointerMove(event) {
20
- var _a;
21
21
  const currentPoint = event.current.canvasPos;
22
22
  if (currentPoint.minus(this.lastPoint).magnitude() === 0) {
23
23
  return;
24
24
  }
25
25
  const line = new LineSegment2(this.lastPoint, currentPoint);
26
26
  const region = line.bbox;
27
- // Remove any intersecting elements.
28
- this.toRemove.push(...this.editor.image
29
- .getElementsIntersectingRegion(region).filter(component => {
27
+ const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
30
28
  return component.intersects(line);
31
- }));
32
- (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
33
- this.command = new Erase(this.toRemove);
34
- this.command.apply(this.editor);
29
+ });
30
+ // Remove any intersecting elements.
31
+ this.toRemove.push(...intersectingElems);
32
+ // Create new Erase commands for the now-to-be-erased elements and apply them.
33
+ const newPartialCommands = intersectingElems.map(elem => new Erase([elem]));
34
+ newPartialCommands.forEach(cmd => cmd.apply(this.editor));
35
+ this.partialCommands.push(...newPartialCommands);
35
36
  this.lastPoint = currentPoint;
36
37
  }
37
38
  onPointerUp(_event) {
38
- var _a;
39
- if (this.command && this.toRemove.length > 0) {
40
- (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
41
- // Dispatch the command to make it undo-able
42
- this.editor.dispatch(this.command);
39
+ if (this.toRemove.length > 0) {
40
+ // Undo commands for each individual component and unite into a single command.
41
+ this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
42
+ this.partialCommands = [];
43
+ const command = new Erase(this.toRemove);
44
+ this.editor.dispatch(command); // dispatch: Makes undo-able.
43
45
  }
44
- this.command = null;
45
46
  }
46
47
  onGestureCancel() {
47
- var _a;
48
- (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
49
- this.command = null;
48
+ this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
49
+ this.partialCommands = [];
50
50
  }
51
51
  }
@@ -14,7 +14,6 @@ export default class Selection {
14
14
  private originalRegion;
15
15
  private transformers;
16
16
  private transform;
17
- private transformCommands;
18
17
  private selectedElems;
19
18
  private container;
20
19
  private backgroundElem;
@@ -28,7 +27,6 @@ export default class Selection {
28
27
  get preTransformedScreenRegionRotation(): number;
29
28
  get screenRegion(): Rect2;
30
29
  get screenRegionRotation(): number;
31
- private computeTransformCommands;
32
30
  setTransform(transform: Mat33, preview?: boolean): void;
33
31
  finalizeTransform(): void;
34
32
  private static ApplyTransformationCommand;
@@ -38,13 +36,14 @@ export default class Selection {
38
36
  getMinCanvasSize(): number;
39
37
  getSelectedItemCount(): number;
40
38
  updateUI(): void;
39
+ private addRemoveSelectionFromImage;
41
40
  private targetHandle;
42
41
  private backgroundDragging;
43
42
  onDragStart(pointer: Pointer, target: EventTarget): boolean;
44
43
  onDragUpdate(pointer: Pointer): void;
45
44
  onDragEnd(): void;
46
45
  onDragCancel(): void;
47
- scrollTo(): void;
46
+ scrollTo(): Promise<void>;
48
47
  deleteSelectedObjects(): Command;
49
48
  duplicateSelectedObjects(): Command;
50
49
  addTo(elem: HTMLElement): void;
@@ -22,13 +22,14 @@ import Erase from '../../commands/Erase';
22
22
  import Duplicate from '../../commands/Duplicate';
23
23
  import { DragTransformer, ResizeTransformer, RotateTransformer } from './TransformMode';
24
24
  import { ResizeMode } from './types';
25
+ import EditorImage from '../../EditorImage';
25
26
  const updateChunkSize = 100;
27
+ const maxPreviewElemCount = 500;
26
28
  // @internal
27
29
  export default class Selection {
28
30
  constructor(startPoint, editor) {
29
31
  this.editor = editor;
30
32
  this.transform = Mat33.identity;
31
- this.transformCommands = [];
32
33
  this.selectedElems = [];
33
34
  this.hasParent = true;
34
35
  this.targetHandle = null;
@@ -89,28 +90,19 @@ export default class Selection {
89
90
  get screenRegionRotation() {
90
91
  return this.regionRotation + this.editor.viewport.getRotationAngle();
91
92
  }
92
- computeTransformCommands() {
93
- return this.selectedElems.map(elem => {
94
- return elem.transformBy(this.transform);
95
- });
96
- }
97
93
  // Applies, previews, but doesn't finalize the given transformation.
98
94
  setTransform(transform, preview = true) {
99
95
  this.transform = transform;
100
96
  if (preview && this.hasParent) {
101
- this.previewTransformCmds();
102
97
  this.scrollTo();
98
+ this.previewTransformCmds();
103
99
  }
104
100
  }
105
101
  // Applies the current transformation to the selection
106
102
  finalizeTransform() {
107
- this.transformCommands.forEach(cmd => {
108
- cmd.unapply(this.editor);
109
- });
110
103
  const fullTransform = this.transform;
111
104
  const selectedElems = this.selectedElems;
112
105
  // Reset for the next drag
113
- this.transformCommands = [];
114
106
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
115
107
  this.transform = Mat33.identity;
116
108
  // Make the commands undo-able
@@ -119,13 +111,19 @@ export default class Selection {
119
111
  // Preview the effects of the current transformation on the selection
120
112
  previewTransformCmds() {
121
113
  // Don't render what we're moving if it's likely to be slow.
122
- if (this.selectedElems.length > updateChunkSize) {
114
+ if (this.selectedElems.length > maxPreviewElemCount) {
123
115
  this.updateUI();
124
116
  return;
125
117
  }
126
- this.transformCommands.forEach(cmd => cmd.unapply(this.editor));
127
- this.transformCommands = this.computeTransformCommands();
128
- this.transformCommands.forEach(cmd => cmd.apply(this.editor));
118
+ const wetInkRenderer = this.editor.display.getWetInkRenderer();
119
+ wetInkRenderer.clear();
120
+ wetInkRenderer.pushTransform(this.transform);
121
+ const viewportVisibleRect = this.editor.viewport.visibleRect;
122
+ const visibleRect = viewportVisibleRect.transformedBoundingBox(this.transform.inverse());
123
+ for (const elem of this.selectedElems) {
124
+ elem.render(wetInkRenderer, visibleRect);
125
+ }
126
+ wetInkRenderer.popTransform();
129
127
  this.updateUI();
130
128
  }
131
129
  // Find the objects corresponding to this in the document,
@@ -208,7 +206,39 @@ export default class Selection {
208
206
  handle.updatePosition();
209
207
  }
210
208
  }
209
+ // Add/remove the contents of this' seleciton from the editor.
210
+ // Used to prevent previewed content from looking like duplicate content
211
+ // while dragging.
212
+ //
213
+ // Does nothing if a large number of elements are selected (and so modifying
214
+ // the editor image is likely to be slow.)
215
+ //
216
+ // If removed from the image, selected elements are drawn as wet ink.
217
+ addRemoveSelectionFromImage(inImage) {
218
+ return __awaiter(this, void 0, void 0, function* () {
219
+ // Don't hide elements if doing so will be slow.
220
+ if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
221
+ return;
222
+ }
223
+ for (const elem of this.selectedElems) {
224
+ const parent = this.editor.image.findParent(elem);
225
+ if (!inImage) {
226
+ parent === null || parent === void 0 ? void 0 : parent.remove();
227
+ }
228
+ // If we're making things visible and the selected object wasn't previously
229
+ // visible,
230
+ else if (!parent) {
231
+ EditorImage.addElement(elem).apply(this.editor);
232
+ }
233
+ }
234
+ yield this.editor.queueRerender();
235
+ if (!inImage) {
236
+ this.previewTransformCmds();
237
+ }
238
+ });
239
+ }
211
240
  onDragStart(pointer, target) {
241
+ void this.addRemoveSelectionFromImage(false);
212
242
  for (const handle of this.handles) {
213
243
  if (handle.isTarget(target)) {
214
244
  handle.handleDragStart(pointer);
@@ -230,7 +260,6 @@ export default class Selection {
230
260
  if (this.targetHandle) {
231
261
  this.targetHandle.handleDragUpdate(pointer);
232
262
  }
233
- this.updateUI();
234
263
  }
235
264
  onDragEnd() {
236
265
  if (this.backgroundDragging) {
@@ -239,6 +268,7 @@ export default class Selection {
239
268
  else if (this.targetHandle) {
240
269
  this.targetHandle.handleDragEnd();
241
270
  }
271
+ this.addRemoveSelectionFromImage(true);
242
272
  this.backgroundDragging = false;
243
273
  this.targetHandle = null;
244
274
  this.updateUI();
@@ -247,19 +277,26 @@ export default class Selection {
247
277
  this.backgroundDragging = false;
248
278
  this.targetHandle = null;
249
279
  this.setTransform(Mat33.identity);
280
+ this.addRemoveSelectionFromImage(true);
250
281
  }
251
282
  // Scroll the viewport to this. Does not zoom
252
283
  scrollTo() {
253
- if (this.selectedElems.length === 0) {
254
- return;
255
- }
256
- const screenRect = new Rect2(0, 0, this.editor.display.width, this.editor.display.height);
257
- if (!screenRect.containsPoint(this.screenRegion.center)) {
258
- const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
259
- const screenDelta = this.screenRegion.center.minus(closestPoint);
260
- const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
261
- this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(-1))), false);
262
- }
284
+ return __awaiter(this, void 0, void 0, function* () {
285
+ if (this.selectedElems.length === 0) {
286
+ return;
287
+ }
288
+ const screenRect = new Rect2(0, 0, this.editor.display.width, this.editor.display.height);
289
+ if (!screenRect.containsPoint(this.screenRegion.center)) {
290
+ const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
291
+ const screenDelta = this.screenRegion.center.minus(closestPoint);
292
+ const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
293
+ yield this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(-1))), false);
294
+ // Re-renders clear wet ink, so we need to re-draw the preview
295
+ // after the full re-render.
296
+ yield this.editor.queueRerender();
297
+ this.previewTransformCmds();
298
+ }
299
+ });
263
300
  }
264
301
  deleteSelectedObjects() {
265
302
  return new Erase(this.selectedElems);
@@ -311,6 +311,15 @@ export default class SelectionTool extends BaseTool {
311
311
  setSelection(objects) {
312
312
  // Only select selectable objects.
313
313
  objects = objects.filter(obj => obj.isSelectable());
314
+ // Sort by z-index
315
+ objects.sort((a, b) => a.getZIndex() - b.getZIndex());
316
+ // Remove duplicates
317
+ objects = objects.filter((current, idx) => {
318
+ if (idx > 0) {
319
+ return current !== objects[idx - 1];
320
+ }
321
+ return true;
322
+ });
314
323
  let bbox = null;
315
324
  for (const object of objects) {
316
325
  if (bbox) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
package/src/Editor.ts CHANGED
@@ -626,15 +626,10 @@ export class Editor {
626
626
 
627
627
  /** `apply` a command. `command` will be announced for accessibility. */
628
628
  public dispatch(command: Command, addToHistory: boolean = true) {
629
- if (addToHistory) {
630
- const apply = false; // Don't double-apply
631
- this.history.push(command, apply);
632
- }
633
-
634
- const applyResult = command.apply(this);
629
+ const dispatchResult = this.dispatchNoAnnounce(command, addToHistory);
635
630
  this.announceForAccessibility(command.description(this, this.localization));
636
631
 
637
- return applyResult;
632
+ return dispatchResult;
638
633
  }
639
634
 
640
635
  /**
@@ -654,10 +649,11 @@ export class Editor {
654
649
  */
655
650
  public dispatchNoAnnounce(command: Command, addToHistory: boolean = false) {
656
651
  if (addToHistory) {
657
- this.history.push(command);
658
- } else {
659
- command.apply(this);
652
+ const apply = false; // Don't double-apply
653
+ this.history.push(command, apply);
660
654
  }
655
+
656
+ return command.apply(this);
661
657
  }
662
658
 
663
659
  /**
@@ -714,17 +710,32 @@ export class Editor {
714
710
  this.announceForAccessibility(this.localization.redoAnnouncement(command.description(this, this.localization)));
715
711
  };
716
712
 
713
+ // Listeners to be called once at the end of the next re-render.
714
+ private nextRerenderListeners: Array<()=> void> = [];
717
715
  private rerenderQueued: boolean = false;
718
- // Schedule a re-render for some time in the near future. Does not schedule an additional
719
- // re-render if a re-render is already queued.
720
- public queueRerender() {
716
+
717
+ /**
718
+ * Schedule a re-render for some time in the near future. Does not schedule an additional
719
+ * re-render if a re-render is already queued.
720
+ *
721
+ * @returns a promise that resolves when
722
+ */
723
+ public queueRerender(): Promise<void> {
721
724
  if (!this.rerenderQueued) {
722
725
  this.rerenderQueued = true;
723
726
  requestAnimationFrame(() => {
724
- this.rerender();
725
- this.rerenderQueued = false;
727
+ // If .rerender was called manually, we might not need to
728
+ // re-render.
729
+ if (this.rerenderQueued) {
730
+ this.rerender();
731
+ this.rerenderQueued = false;
732
+ }
726
733
  });
727
734
  }
735
+
736
+ return new Promise(resolve => {
737
+ this.nextRerenderListeners.push(() => resolve());
738
+ });
728
739
  }
729
740
 
730
741
  public rerender(showImageBounds: boolean = true) {
@@ -751,6 +762,8 @@ export class Editor {
751
762
  }
752
763
 
753
764
  this.rerenderQueued = false;
765
+ this.nextRerenderListeners.forEach(listener => listener());
766
+ this.nextRerenderListeners = [];
754
767
  }
755
768
 
756
769
  public drawWetInk(...path: RenderablePathSpec[]) {
@@ -7,7 +7,7 @@ import { EditorLocalization } from './localization';
7
7
  import RenderingCache from './rendering/caching/RenderingCache';
8
8
  import SerializableCommand from './commands/SerializableCommand';
9
9
 
10
- // @internal
10
+ // @internal Sort by z-index, low to high
11
11
  export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
12
12
  leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
13
13
  };
@@ -9,9 +9,11 @@ import { PointerDevice } from '../Pointer';
9
9
 
10
10
  export default class Eraser extends BaseTool {
11
11
  private lastPoint: Point2;
12
- private command: Erase|null = null;
13
12
  private toRemove: AbstractComponent[];
14
13
 
14
+ // Commands that each remove one element
15
+ private partialCommands: Erase[] = [];
16
+
15
17
  public constructor(private editor: Editor, description: string) {
16
18
  super(editor.notifier, description);
17
19
  }
@@ -35,31 +37,35 @@ export default class Eraser extends BaseTool {
35
37
  const line = new LineSegment2(this.lastPoint, currentPoint);
36
38
  const region = line.bbox;
37
39
 
40
+ const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
41
+ return component.intersects(line);
42
+ });
43
+
38
44
  // Remove any intersecting elements.
39
- this.toRemove.push(...this.editor.image
40
- .getElementsIntersectingRegion(region).filter(component => {
41
- return component.intersects(line);
42
- }));
45
+ this.toRemove.push(...intersectingElems);
46
+
47
+ // Create new Erase commands for the now-to-be-erased elements and apply them.
48
+ const newPartialCommands = intersectingElems.map(elem => new Erase([ elem ]));
49
+ newPartialCommands.forEach(cmd => cmd.apply(this.editor));
43
50
 
44
- this.command?.unapply(this.editor);
45
- this.command = new Erase(this.toRemove);
46
- this.command.apply(this.editor);
51
+ this.partialCommands.push(...newPartialCommands);
47
52
 
48
53
  this.lastPoint = currentPoint;
49
54
  }
50
55
 
51
56
  public onPointerUp(_event: PointerEvt): void {
52
- if (this.command && this.toRemove.length > 0) {
53
- this.command?.unapply(this.editor);
57
+ if (this.toRemove.length > 0) {
58
+ // Undo commands for each individual component and unite into a single command.
59
+ this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
60
+ this.partialCommands = [];
54
61
 
55
- // Dispatch the command to make it undo-able
56
- this.editor.dispatch(this.command);
62
+ const command = new Erase(this.toRemove);
63
+ this.editor.dispatch(command); // dispatch: Makes undo-able.
57
64
  }
58
- this.command = null;
59
65
  }
60
66
 
61
67
  public onGestureCancel(): void {
62
- this.command?.unapply(this.editor);
63
- this.command = null;
68
+ this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
69
+ this.partialCommands = [];
64
70
  }
65
71
  }