js-draw 1.9.1 → 1.11.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 (145) hide show
  1. package/dist/Editor.css +48 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +41 -0
  5. package/dist/cjs/Editor.js +9 -0
  6. package/dist/cjs/Pointer.js +1 -1
  7. package/dist/cjs/commands/Erase.d.ts +22 -2
  8. package/dist/cjs/commands/Erase.js +22 -2
  9. package/dist/cjs/commands/invertCommand.js +5 -0
  10. package/dist/cjs/commands/uniteCommands.d.ts +36 -0
  11. package/dist/cjs/commands/uniteCommands.js +36 -0
  12. package/dist/cjs/components/AbstractComponent.d.ts +8 -0
  13. package/dist/cjs/components/AbstractComponent.js +28 -8
  14. package/dist/cjs/components/ImageComponent.d.ts +12 -0
  15. package/dist/cjs/components/ImageComponent.js +16 -9
  16. package/dist/cjs/components/Stroke.d.ts +16 -2
  17. package/dist/cjs/components/Stroke.js +17 -1
  18. package/dist/cjs/components/builders/ArrowBuilder.js +3 -3
  19. package/dist/cjs/components/builders/CircleBuilder.js +3 -3
  20. package/dist/cjs/components/builders/FreehandLineBuilder.js +3 -3
  21. package/dist/cjs/components/builders/LineBuilder.js +3 -3
  22. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +3 -3
  23. package/dist/cjs/components/builders/RectangleBuilder.js +5 -6
  24. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  25. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +168 -0
  26. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  27. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.js +46 -0
  28. package/dist/cjs/components/builders/types.d.ts +12 -0
  29. package/dist/cjs/image/EditorImage.d.ts +32 -1
  30. package/dist/cjs/image/EditorImage.js +32 -1
  31. package/dist/cjs/rendering/RenderablePathSpec.d.ts +5 -1
  32. package/dist/cjs/rendering/RenderablePathSpec.js +4 -0
  33. package/dist/cjs/toolbar/AbstractToolbar.d.ts +18 -2
  34. package/dist/cjs/toolbar/AbstractToolbar.js +46 -30
  35. package/dist/cjs/toolbar/IconProvider.d.ts +2 -0
  36. package/dist/cjs/toolbar/IconProvider.js +17 -0
  37. package/dist/cjs/toolbar/localization.d.ts +3 -0
  38. package/dist/cjs/toolbar/localization.js +4 -1
  39. package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -1
  40. package/dist/cjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  41. package/dist/cjs/toolbar/widgets/ExitActionWidget.js +32 -0
  42. package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  43. package/dist/cjs/toolbar/widgets/HandToolWidget.js +24 -13
  44. package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  45. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +102 -22
  46. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  47. package/dist/cjs/toolbar/widgets/PenToolWidget.js +50 -20
  48. package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
  49. package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
  50. package/dist/cjs/toolbar/widgets/layout/types.d.ts +1 -1
  51. package/dist/cjs/tools/Pen.d.ts +9 -0
  52. package/dist/cjs/tools/Pen.js +82 -3
  53. package/dist/cjs/tools/SelectionTool/Selection.d.ts +4 -0
  54. package/dist/cjs/tools/SelectionTool/Selection.js +56 -12
  55. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  56. package/dist/cjs/tools/SelectionTool/SelectionTool.js +19 -1
  57. package/dist/cjs/tools/TextTool.js +5 -1
  58. package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  59. package/dist/cjs/tools/ToolSwitcherShortcut.js +0 -1
  60. package/dist/cjs/tools/keybindings.d.ts +1 -0
  61. package/dist/cjs/tools/keybindings.js +3 -1
  62. package/dist/cjs/tools/util/StationaryPenDetector.d.ts +22 -0
  63. package/dist/cjs/tools/util/StationaryPenDetector.js +95 -0
  64. package/dist/cjs/util/ReactiveValue.d.ts +2 -0
  65. package/dist/cjs/util/ReactiveValue.js +2 -0
  66. package/dist/cjs/util/lib.d.ts +1 -0
  67. package/dist/cjs/util/lib.js +4 -1
  68. package/dist/cjs/util/waitForImageLoaded.d.ts +2 -0
  69. package/dist/cjs/util/waitForImageLoaded.js +12 -0
  70. package/dist/cjs/version.js +1 -1
  71. package/dist/mjs/Editor.d.ts +41 -0
  72. package/dist/mjs/Editor.mjs +9 -0
  73. package/dist/mjs/Pointer.mjs +1 -1
  74. package/dist/mjs/commands/Erase.d.ts +22 -2
  75. package/dist/mjs/commands/Erase.mjs +22 -2
  76. package/dist/mjs/commands/invertCommand.mjs +5 -0
  77. package/dist/mjs/commands/uniteCommands.d.ts +36 -0
  78. package/dist/mjs/commands/uniteCommands.mjs +36 -0
  79. package/dist/mjs/components/AbstractComponent.d.ts +8 -0
  80. package/dist/mjs/components/AbstractComponent.mjs +28 -8
  81. package/dist/mjs/components/ImageComponent.d.ts +12 -0
  82. package/dist/mjs/components/ImageComponent.mjs +16 -9
  83. package/dist/mjs/components/Stroke.d.ts +16 -2
  84. package/dist/mjs/components/Stroke.mjs +17 -1
  85. package/dist/mjs/components/builders/ArrowBuilder.mjs +3 -2
  86. package/dist/mjs/components/builders/CircleBuilder.mjs +3 -2
  87. package/dist/mjs/components/builders/FreehandLineBuilder.mjs +3 -2
  88. package/dist/mjs/components/builders/LineBuilder.mjs +3 -2
  89. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +3 -2
  90. package/dist/mjs/components/builders/RectangleBuilder.mjs +5 -4
  91. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  92. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +166 -0
  93. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  94. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.mjs +44 -0
  95. package/dist/mjs/components/builders/types.d.ts +12 -0
  96. package/dist/mjs/image/EditorImage.d.ts +32 -1
  97. package/dist/mjs/image/EditorImage.mjs +32 -1
  98. package/dist/mjs/rendering/RenderablePathSpec.d.ts +5 -1
  99. package/dist/mjs/rendering/RenderablePathSpec.mjs +4 -0
  100. package/dist/mjs/toolbar/AbstractToolbar.d.ts +18 -2
  101. package/dist/mjs/toolbar/AbstractToolbar.mjs +46 -30
  102. package/dist/mjs/toolbar/IconProvider.d.ts +2 -0
  103. package/dist/mjs/toolbar/IconProvider.mjs +17 -0
  104. package/dist/mjs/toolbar/localization.d.ts +3 -0
  105. package/dist/mjs/toolbar/localization.mjs +4 -1
  106. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -1
  107. package/dist/mjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  108. package/dist/mjs/toolbar/widgets/ExitActionWidget.mjs +27 -0
  109. package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  110. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +24 -13
  111. package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  112. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +102 -22
  113. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  114. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +50 -20
  115. package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
  116. package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
  117. package/dist/mjs/toolbar/widgets/layout/types.d.ts +1 -1
  118. package/dist/mjs/tools/Pen.d.ts +9 -0
  119. package/dist/mjs/tools/Pen.mjs +82 -3
  120. package/dist/mjs/tools/SelectionTool/Selection.d.ts +4 -0
  121. package/dist/mjs/tools/SelectionTool/Selection.mjs +56 -12
  122. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  123. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +20 -2
  124. package/dist/mjs/tools/TextTool.mjs +5 -1
  125. package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  126. package/dist/mjs/tools/ToolSwitcherShortcut.mjs +0 -1
  127. package/dist/mjs/tools/keybindings.d.ts +1 -0
  128. package/dist/mjs/tools/keybindings.mjs +2 -0
  129. package/dist/mjs/tools/util/StationaryPenDetector.d.ts +22 -0
  130. package/dist/mjs/tools/util/StationaryPenDetector.mjs +92 -0
  131. package/dist/mjs/util/ReactiveValue.d.ts +2 -0
  132. package/dist/mjs/util/ReactiveValue.mjs +2 -0
  133. package/dist/mjs/util/lib.d.ts +1 -0
  134. package/dist/mjs/util/lib.mjs +1 -0
  135. package/dist/mjs/util/waitForImageLoaded.d.ts +2 -0
  136. package/dist/mjs/util/waitForImageLoaded.mjs +10 -0
  137. package/dist/mjs/version.mjs +1 -1
  138. package/package.json +3 -3
  139. package/src/Editor.scss +7 -0
  140. package/src/toolbar/AbstractToolbar.scss +20 -0
  141. package/src/toolbar/toolbar.scss +1 -1
  142. package/src/toolbar/widgets/InsertImageWidget.scss +6 -1
  143. package/src/toolbar/widgets/PenToolWidget.scss +33 -0
  144. package/src/tools/SelectionTool/SelectionTool.scss +6 -0
  145. package/src/toolbar/widgets/PenToolWidget.css +0 -2
@@ -8,6 +8,7 @@ import { undoKeyboardShortcutId } from './keybindings.mjs';
8
8
  import { decreaseSizeKeyboardShortcutId, increaseSizeKeyboardShortcutId } from './keybindings.mjs';
9
9
  import InputStabilizer from './InputFilter/InputStabilizer.mjs';
10
10
  import { ReactiveValue } from '../util/ReactiveValue.mjs';
11
+ import StationaryPenDetector from './util/StationaryPenDetector.mjs';
11
12
  export default class Pen extends BaseTool {
12
13
  constructor(editor, description, style) {
13
14
  super(editor.notifier, description);
@@ -16,6 +17,11 @@ export default class Pen extends BaseTool {
16
17
  this.lastPoint = null;
17
18
  this.startPoint = null;
18
19
  this.currentDeviceType = null;
20
+ this.shapeAutocompletionEnabled = false;
21
+ this.autocorrectedShape = null;
22
+ this.lastAutocorrectedShape = null;
23
+ this.removedAutocorrectedShapeTime = 0;
24
+ this.stationaryDetector = null;
19
25
  this.styleValue = ReactiveValue.fromInitialValue({
20
26
  factory: makeFreehandLineBuilder,
21
27
  color: Color4.blue,
@@ -53,7 +59,14 @@ export default class Pen extends BaseTool {
53
59
  // Displays the stroke that is currently being built with the display's `wetInkRenderer`.
54
60
  previewStroke() {
55
61
  this.editor.clearWetInk();
56
- this.builder?.preview(this.editor.display.getWetInkRenderer());
62
+ const wetInkRenderer = this.editor.display.getWetInkRenderer();
63
+ if (this.autocorrectedShape) {
64
+ const visibleRect = this.editor.viewport.visibleRect;
65
+ this.autocorrectedShape.render(wetInkRenderer, visibleRect);
66
+ }
67
+ else {
68
+ this.builder?.preview(wetInkRenderer);
69
+ }
57
70
  }
58
71
  // Throws if no stroke builder exists.
59
72
  addPointToStroke(point) {
@@ -82,6 +95,19 @@ export default class Pen extends BaseTool {
82
95
  this.startPoint = this.toStrokePoint(current);
83
96
  this.builder = this.style.factory(this.startPoint, this.editor.viewport);
84
97
  this.currentDeviceType = current.device;
98
+ if (this.shapeAutocompletionEnabled) {
99
+ const stationaryDetectionConfig = {
100
+ maxSpeed: 5,
101
+ maxRadius: 10,
102
+ minTimeSeconds: 0.5, // s
103
+ };
104
+ this.stationaryDetector = new StationaryPenDetector(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
105
+ }
106
+ else {
107
+ this.stationaryDetector = null;
108
+ }
109
+ this.lastAutocorrectedShape = null;
110
+ this.removedAutocorrectedShapeTime = 0;
85
111
  return true;
86
112
  }
87
113
  return false;
@@ -109,7 +135,14 @@ export default class Pen extends BaseTool {
109
135
  return;
110
136
  if (current.device !== this.currentDeviceType)
111
137
  return;
112
- this.addPointToStroke(this.toStrokePoint(current));
138
+ const isStationary = this.stationaryDetector?.onPointerMove(current);
139
+ if (!isStationary) {
140
+ this.addPointToStroke(this.toStrokePoint(current));
141
+ if (this.autocorrectedShape) {
142
+ this.removedAutocorrectedShapeTime = performance.now();
143
+ this.autocorrectedShape = null;
144
+ }
145
+ }
113
146
  }
114
147
  onPointerUp({ current }) {
115
148
  if (!this.builder)
@@ -119,6 +152,7 @@ export default class Pen extends BaseTool {
119
152
  // device type.
120
153
  return true;
121
154
  }
155
+ this.stationaryDetector?.onPointerUp(current);
122
156
  // onPointerUp events can have zero pressure. Use the last pressure instead.
123
157
  const currentPoint = this.toStrokePoint(current);
124
158
  const strokePoint = {
@@ -134,10 +168,42 @@ export default class Pen extends BaseTool {
134
168
  onGestureCancel() {
135
169
  this.builder = null;
136
170
  this.editor.clearWetInk();
171
+ this.stationaryDetector?.destroy();
172
+ this.stationaryDetector = null;
173
+ }
174
+ removedAutocorrectedShapeRecently() {
175
+ return this.removedAutocorrectedShapeTime > performance.now() - 320;
176
+ }
177
+ async autocorrectShape(_lastPointer) {
178
+ if (!this.builder || !this.builder.autocorrectShape)
179
+ return;
180
+ if (!this.shapeAutocompletionEnabled)
181
+ return;
182
+ // If already corrected, do nothing
183
+ if (this.autocorrectedShape)
184
+ return;
185
+ // Activate stroke fitting
186
+ const correctedShape = await this.builder.autocorrectShape();
187
+ if (!this.builder || !correctedShape) {
188
+ return;
189
+ }
190
+ // Don't complete to empty shapes.
191
+ const bboxArea = correctedShape.getBBox().area;
192
+ if (bboxArea === 0 || !isFinite(bboxArea)) {
193
+ return;
194
+ }
195
+ this.autocorrectedShape = correctedShape;
196
+ this.lastAutocorrectedShape = correctedShape;
197
+ this.previewStroke();
137
198
  }
138
199
  finalizeStroke() {
139
200
  if (this.builder) {
140
- const stroke = this.builder.build();
201
+ // If autocorrectedShape was cleared recently enough, it was
202
+ // probably by mistake. Reset it.
203
+ if (this.lastAutocorrectedShape && this.removedAutocorrectedShapeRecently()) {
204
+ this.autocorrectedShape = this.lastAutocorrectedShape;
205
+ }
206
+ const stroke = this.autocorrectedShape ?? this.builder.build();
141
207
  this.previewStroke();
142
208
  if (stroke.getBBox().area > 0) {
143
209
  const canFlatten = true;
@@ -150,7 +216,11 @@ export default class Pen extends BaseTool {
150
216
  }
151
217
  this.builder = null;
152
218
  this.lastPoint = null;
219
+ this.autocorrectedShape = null;
220
+ this.lastAutocorrectedShape = null;
153
221
  this.editor.clearWetInk();
222
+ this.stationaryDetector?.destroy();
223
+ this.stationaryDetector = null;
154
224
  }
155
225
  noteUpdated() {
156
226
  this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
@@ -196,6 +266,15 @@ export default class Pen extends BaseTool {
196
266
  }
197
267
  this.noteUpdated();
198
268
  }
269
+ setStrokeAutocorrectEnabled(enabled) {
270
+ if (enabled !== this.shapeAutocompletionEnabled) {
271
+ this.shapeAutocompletionEnabled = enabled;
272
+ this.noteUpdated();
273
+ }
274
+ }
275
+ getStrokeAutocorrectionEnabled() {
276
+ return this.shapeAutocompletionEnabled;
277
+ }
199
278
  getThickness() { return this.style.thickness; }
200
279
  getColor() { return this.style.color; }
201
280
  getStrokeFactory() { return this.style.factory; }
@@ -2,6 +2,7 @@
2
2
  * @internal
3
3
  * @packageDocumentation
4
4
  */
5
+ import SerializableCommand from '../../commands/SerializableCommand';
5
6
  import Editor from '../../Editor';
6
7
  import { Mat33, Rect2, Point2 } from '@js-draw/math';
7
8
  import Pointer from '../../Pointer';
@@ -36,7 +37,10 @@ export default class Selection {
36
37
  getScreenRegion(): Rect2;
37
38
  get screenRegionRotation(): number;
38
39
  setTransform(transform: Mat33, preview?: boolean): void;
40
+ private getDeltaZIndexToMoveSelectionToTop;
39
41
  finalizeTransform(): void | Promise<void>;
42
+ /** Sends all selected elements to the bottom of the visible image. */
43
+ sendToBack(): SerializableCommand | null;
40
44
  private static ApplyTransformationCommand;
41
45
  private previewTransformCmds;
42
46
  resolveToObjects(): boolean;
@@ -13,6 +13,7 @@ import Duplicate from '../../commands/Duplicate.mjs';
13
13
  import { DragTransformer, ResizeTransformer, RotateTransformer } from './TransformMode.mjs';
14
14
  import { ResizeMode } from './types.mjs';
15
15
  import EditorImage from '../../image/EditorImage.mjs';
16
+ import uniteCommands from '../../commands/uniteCommands.mjs';
16
17
  const updateChunkSize = 100;
17
18
  const maxPreviewElemCount = 500;
18
19
  // @internal
@@ -23,6 +24,7 @@ class Selection {
23
24
  // @see getTightBoundingBox
24
25
  this.selectionTightBoundingBox = null;
25
26
  this.transform = Mat33.identity;
27
+ // invariant: sorted by increasing z-index
26
28
  this.selectedElems = [];
27
29
  this.hasParent = true;
28
30
  // Maps IDs to whether we removed the component from the image
@@ -133,6 +135,16 @@ class Selection {
133
135
  this.previewTransformCmds();
134
136
  }
135
137
  }
138
+ getDeltaZIndexToMoveSelectionToTop() {
139
+ if (this.selectedElems.length === 0) {
140
+ return 0;
141
+ }
142
+ const selectedBottommostZIndex = this.selectedElems[0].getZIndex();
143
+ const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.region);
144
+ const topMostVisibleZIndex = visibleObjects[visibleObjects.length - 1]?.getZIndex() ?? selectedBottommostZIndex;
145
+ const deltaZIndex = (topMostVisibleZIndex + 1) - selectedBottommostZIndex;
146
+ return deltaZIndex;
147
+ }
136
148
  // Applies the current transformation to the selection
137
149
  finalizeTransform() {
138
150
  const fullTransform = this.transform;
@@ -141,17 +153,35 @@ class Selection {
141
153
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
142
154
  this.transform = Mat33.identity;
143
155
  this.scrollTo();
156
+ let transformPromise = undefined;
144
157
  // Make the commands undo-able.
145
158
  // Don't check for non-empty transforms because this breaks changing the
146
159
  // 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));
160
+ if (this.selectedElems.length > 0) {
161
+ const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
162
+ transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform, deltaZIndex));
163
+ }
150
164
  // Clear renderings of any in-progress transformations
151
165
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
152
166
  wetInkRenderer.clear();
153
167
  return transformPromise;
154
168
  }
169
+ /** Sends all selected elements to the bottom of the visible image. */
170
+ sendToBack() {
171
+ const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.editor.viewport.visibleRect);
172
+ // VisibleObjects and selectedElems should both be sorted by z-index
173
+ const lowestVisibleZIndex = visibleObjects[0]?.getZIndex() ?? 0;
174
+ const highestSelectedZIndex = this.selectedElems[this.selectedElems.length - 1]?.getZIndex() ?? 0;
175
+ const targetHighestZIndex = lowestVisibleZIndex - 1;
176
+ const deltaZIndex = targetHighestZIndex - highestSelectedZIndex;
177
+ if (deltaZIndex !== 0) {
178
+ const commands = this.selectedElems.map(elem => {
179
+ return elem.setZIndex(elem.getZIndex() + deltaZIndex);
180
+ });
181
+ return uniteCommands(commands, updateChunkSize);
182
+ }
183
+ return null;
184
+ }
155
185
  // Preview the effects of the current transformation on the selection
156
186
  previewTransformCmds() {
157
187
  if (this.selectedElems.length === 0) {
@@ -165,7 +195,7 @@ class Selection {
165
195
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
166
196
  wetInkRenderer.clear();
167
197
  wetInkRenderer.pushTransform(this.transform);
168
- const viewportVisibleRect = this.editor.viewport.visibleRect;
198
+ const viewportVisibleRect = this.editor.viewport.visibleRect.union(this.region);
169
199
  const visibleRect = viewportVisibleRect.transformedBoundingBox(this.transform.inverse());
170
200
  for (const elem of this.selectedElems) {
171
201
  elem.render(wetInkRenderer, visibleRect);
@@ -411,7 +441,8 @@ class Selection {
411
441
  if (wasTransforming) {
412
442
  // Don't update the selection's focus when redoing/undoing
413
443
  const selectionToUpdate = null;
414
- tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
444
+ const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
445
+ tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform, deltaZIndex);
415
446
  // Transform to ensure that the duplicates are in the correct location
416
447
  await tmpApplyCommand.apply(this.editor);
417
448
  // Show items again
@@ -452,6 +483,8 @@ class Selection {
452
483
  this.originalRegion = bbox;
453
484
  this.selectionTightBoundingBox = bbox;
454
485
  this.selectedElems = objects.filter(object => object.isSelectable());
486
+ // Enforce increasing z-index invariant
487
+ this.selectedElems.sort((a, b) => a.getZIndex() - b.getZIndex());
455
488
  this.padRegion();
456
489
  this.updateUI();
457
490
  }
@@ -465,7 +498,8 @@ _a = Selection;
465
498
  // The selection box is lost when serializing/deserializing. No need to store box rotation
466
499
  const fullTransform = new Mat33(...json.transform);
467
500
  const elemIds = (json.elems ?? []);
468
- return new _a.ApplyTransformationCommand(null, elemIds, fullTransform);
501
+ const deltaZIndex = parseInt(json.deltaZIndex ?? 0);
502
+ return new _a.ApplyTransformationCommand(null, elemIds, fullTransform, deltaZIndex);
469
503
  });
470
504
  })();
471
505
  Selection.ApplyTransformationCommand = class extends SerializableCommand {
@@ -473,10 +507,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
473
507
  // If a `string[]`, selectedElems is a list of element IDs.
474
508
  selectedElems,
475
509
  // Full transformation used to transform elements.
476
- fullTransform) {
510
+ fullTransform, deltaZIndex) {
477
511
  super('selection-tool-transform');
478
512
  this.selection = selection;
479
513
  this.fullTransform = fullTransform;
514
+ this.deltaZIndex = deltaZIndex;
480
515
  const isIDList = (arr) => {
481
516
  return typeof arr[0] === 'string';
482
517
  };
@@ -487,11 +522,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
487
522
  else {
488
523
  this.selectedElemIds = selectedElems.map(elem => elem.getId());
489
524
  this.transformCommands = selectedElems.map(elem => {
490
- return elem.transformBy(this.fullTransform);
525
+ return elem.setZIndexAndTransformBy(this.fullTransform, elem.getZIndex() + deltaZIndex);
491
526
  });
492
527
  }
493
528
  }
494
- resolveToElems(editor) {
529
+ resolveToElems(editor, isUndoing) {
495
530
  if (this.transformCommands) {
496
531
  return;
497
532
  }
@@ -500,11 +535,19 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
500
535
  if (!elem) {
501
536
  throw new Error(`Unable to find element with ID, ${id}.`);
502
537
  }
503
- return elem.transformBy(this.fullTransform);
538
+ let originalZIndex = elem.getZIndex();
539
+ let targetZIndex = elem.getZIndex() + this.deltaZIndex;
540
+ // If the command has already been applied, the element should currently
541
+ // have the target z-index.
542
+ if (isUndoing) {
543
+ targetZIndex = elem.getZIndex();
544
+ originalZIndex = elem.getZIndex() - this.deltaZIndex;
545
+ }
546
+ return elem.setZIndexAndTransformBy(this.fullTransform, targetZIndex, originalZIndex);
504
547
  });
505
548
  }
506
549
  async apply(editor) {
507
- this.resolveToElems(editor);
550
+ this.resolveToElems(editor, false);
508
551
  this.selection?.setTransform(this.fullTransform, false);
509
552
  this.selection?.updateUI();
510
553
  await editor.asyncApplyCommands(this.transformCommands, updateChunkSize);
@@ -513,7 +556,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
513
556
  this.selection?.updateUI();
514
557
  }
515
558
  async unapply(editor) {
516
- this.resolveToElems(editor);
559
+ this.resolveToElems(editor, true);
517
560
  this.selection?.setTransform(this.fullTransform.inverse(), false);
518
561
  this.selection?.updateUI();
519
562
  await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
@@ -525,6 +568,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
525
568
  return {
526
569
  elems: this.selectedElemIds,
527
570
  transform: this.fullTransform.toArray(),
571
+ deltaZIndex: this.deltaZIndex,
528
572
  };
529
573
  }
530
574
  description(_editor, localizationTable) {
@@ -28,6 +28,7 @@ export default class SelectionTool extends BaseTool {
28
28
  private onSelectionUpdated;
29
29
  private zoomToSelection;
30
30
  private static handleableKeys;
31
+ private hasUnfinalizedTransformFromKeyPress;
31
32
  onKeyPress(event: KeyPressEvent): boolean;
32
33
  onKeyUp(evt: KeyUpEvent): boolean;
33
34
  onCopy(event: CopyEvent): boolean;
@@ -5,7 +5,7 @@ import BaseTool from '../BaseTool.mjs';
5
5
  import SVGRenderer from '../../rendering/renderers/SVGRenderer.mjs';
6
6
  import Selection from './Selection.mjs';
7
7
  import TextComponent from '../../components/TextComponent.mjs';
8
- import { duplicateSelectionShortcut, selectAllKeyboardShortcut, snapToGridKeyboardShortcutId } from '../keybindings.mjs';
8
+ import { duplicateSelectionShortcut, selectAllKeyboardShortcut, sendToBackSelectionShortcut, snapToGridKeyboardShortcutId } from '../keybindings.mjs';
9
9
  import ToPointerAutoscroller from './ToPointerAutoscroller.mjs';
10
10
  export const cssPrefix = 'selection-tool-';
11
11
  // Allows users to select/transform portions of the `EditorImage`.
@@ -21,6 +21,9 @@ class SelectionTool extends BaseTool {
21
21
  this.lastPointer = null;
22
22
  this.selectionBoxHandlingEvt = false;
23
23
  this.lastSelectedObjects = [];
24
+ // Whether the last keypress corresponded to an action that didn't transform the
25
+ // selection (and thus does not need to be finalized on onKeyUp).
26
+ this.hasUnfinalizedTransformFromKeyPress = false;
24
27
  this.autoscroller = new ToPointerAutoscroller(editor.viewport, (scrollBy) => {
25
28
  editor.dispatch(Viewport.transformBy(Mat33.translation(scrollBy)), false);
26
29
  // Update the selection box/content to match the new viewport.
@@ -213,7 +216,8 @@ class SelectionTool extends BaseTool {
213
216
  this.snapToGrid = true;
214
217
  return true;
215
218
  }
216
- if (this.selectionBox && shortcucts.matchesShortcut(duplicateSelectionShortcut, event)) {
219
+ if (this.selectionBox && (shortcucts.matchesShortcut(duplicateSelectionShortcut, event)
220
+ || shortcucts.matchesShortcut(sendToBackSelectionShortcut, event))) {
217
221
  // Handle duplication on key up — we don't want to accidentally duplicate
218
222
  // many times.
219
223
  return true;
@@ -297,6 +301,8 @@ class SelectionTool extends BaseTool {
297
301
  const oldTransform = this.selectionBox.getTransform();
298
302
  this.selectionBox.setTransform(oldTransform.rightMul(transform));
299
303
  this.selectionBox.scrollTo();
304
+ // The transformation needs to be finalized at some point (on key up)
305
+ this.hasUnfinalizedTransformFromKeyPress = true;
300
306
  }
301
307
  if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
302
308
  this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
@@ -322,12 +328,24 @@ class SelectionTool extends BaseTool {
322
328
  });
323
329
  return true;
324
330
  }
331
+ if (this.selectionBox && shortcucts.matchesShortcut(sendToBackSelectionShortcut, evt)) {
332
+ const sendToBackCommand = this.selectionBox.sendToBack();
333
+ if (sendToBackCommand) {
334
+ this.editor.dispatch(sendToBackCommand);
335
+ }
336
+ return true;
337
+ }
325
338
  if (evt.key === 'Shift') {
326
339
  this.shiftKeyPressed = false;
327
340
  return true;
328
341
  }
342
+ // If we don't need to finalize the transform
343
+ if (!this.hasUnfinalizedTransformFromKeyPress) {
344
+ return true;
345
+ }
329
346
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
330
347
  this.selectionBox.finalizeTransform();
348
+ this.hasUnfinalizedTransformFromKeyPress = false;
331
349
  return true;
332
350
  }
333
351
  return false;
@@ -39,6 +39,10 @@ export default class TextTool extends BaseTool {
39
39
  .${overlayCSSClass} {
40
40
  height: 0;
41
41
  overflow: visible;
42
+
43
+ /* Allows absolutely-positioned textareas to scroll with
44
+ the containing overlay. */
45
+ position: relative;
42
46
  }
43
47
 
44
48
  .${overlayCSSClass} textarea {
@@ -121,7 +125,7 @@ export default class TextTool extends BaseTool {
121
125
  this.textInputElem.style.fontWeight = this.textStyle.fontWeight ?? '';
122
126
  this.textInputElem.style.fontSize = `${this.textStyle.size}px`;
123
127
  this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
124
- this.textInputElem.style.position = 'relative';
128
+ this.textInputElem.style.position = 'absolute';
125
129
  this.textInputElem.style.left = `${textScreenPos.x}px`;
126
130
  this.textInputElem.style.top = `${textScreenPos.y}px`;
127
131
  this.textInputElem.style.margin = '0';
@@ -7,7 +7,6 @@ import BaseTool from './BaseTool';
7
7
  *
8
8
  * This is in the default set of {@link ToolController} tools.
9
9
  *
10
- * @deprecated This may be replaced in the future.
11
10
  */
12
11
  export default class ToolSwitcherShortcut extends BaseTool {
13
12
  private editor;
@@ -5,7 +5,6 @@ import BaseTool from './BaseTool.mjs';
5
5
  *
6
6
  * This is in the default set of {@link ToolController} tools.
7
7
  *
8
- * @deprecated This may be replaced in the future.
9
8
  */
10
9
  export default class ToolSwitcherShortcut extends BaseTool {
11
10
  constructor(editor) {
@@ -15,3 +15,4 @@ export declare const zoomInKeyboardShortcutId = "jsdraw.tools.PanZoom.zoomIn";
15
15
  export declare const zoomOutKeyboardShortcutId = "jsdraw.tools.PanZoom.zoomOut";
16
16
  export declare const selectAllKeyboardShortcut = "jsdraw.tools.SelectionTool.selectAll";
17
17
  export declare const duplicateSelectionShortcut = "jsdraw.tools.SelectionTool.duplicateSelection";
18
+ export declare const sendToBackSelectionShortcut = "jsdraw.tools.SelectionTool.sendToBack";
@@ -39,3 +39,5 @@ export const selectAllKeyboardShortcut = 'jsdraw.tools.SelectionTool.selectAll';
39
39
  KeyboardShortcutManager.registerDefaultKeyboardShortcut(selectAllKeyboardShortcut, ['CtrlOrMeta+KeyA'], 'Select all');
40
40
  export const duplicateSelectionShortcut = 'jsdraw.tools.SelectionTool.duplicateSelection';
41
41
  KeyboardShortcutManager.registerDefaultKeyboardShortcut(duplicateSelectionShortcut, ['CtrlOrMeta+KeyD'], 'Duplicate selection');
42
+ export const sendToBackSelectionShortcut = 'jsdraw.tools.SelectionTool.sendToBack';
43
+ KeyboardShortcutManager.registerDefaultKeyboardShortcut(sendToBackSelectionShortcut, ['End'], 'Send to back');
@@ -0,0 +1,22 @@
1
+ import Pointer from '../../Pointer';
2
+ interface Config {
3
+ maxSpeed: number;
4
+ minTimeSeconds: number;
5
+ maxRadius: number;
6
+ }
7
+ type OnStationaryCallback = (lastPointer: Pointer) => void;
8
+ export default class StationaryPenDetector {
9
+ private config;
10
+ private onStationary;
11
+ private stationaryStartPointer;
12
+ private lastPointer;
13
+ private averageVelocity;
14
+ private timeout;
15
+ constructor(startPointer: Pointer, config: Config, onStationary: OnStationaryCallback);
16
+ onPointerMove(currentPointer: Pointer): boolean | undefined;
17
+ onPointerUp(pointer: Pointer): void;
18
+ destroy(): void;
19
+ private cancelStationaryTimeout;
20
+ private setStationaryTimeout;
21
+ }
22
+ export {};
@@ -0,0 +1,92 @@
1
+ import { Vec2 } from '@js-draw/math';
2
+ export default class StationaryPenDetector {
3
+ // Only handles one pen. As such, `startPointer` should be the same device/finger
4
+ // as `updatedPointer` in `onPointerMove`.
5
+ //
6
+ // A new `StationaryPenDetector` should be created for each gesture.
7
+ constructor(startPointer, config, onStationary) {
8
+ this.config = config;
9
+ this.onStationary = onStationary;
10
+ this.timeout = null;
11
+ this.stationaryStartPointer = startPointer;
12
+ this.lastPointer = startPointer;
13
+ this.averageVelocity = Vec2.zero;
14
+ }
15
+ // Returns true if stationary
16
+ onPointerMove(currentPointer) {
17
+ if (!this.stationaryStartPointer) {
18
+ // Destoroyed
19
+ return;
20
+ }
21
+ if (currentPointer.id !== this.stationaryStartPointer.id) {
22
+ return false;
23
+ }
24
+ // dx: "Δx" Displacement from last.
25
+ const dxFromLast = currentPointer.screenPos.minus(this.lastPointer.screenPos);
26
+ const dxFromStationaryStart = currentPointer.screenPos.minus(this.stationaryStartPointer.screenPos);
27
+ // dt: Delta time:
28
+ // /1000: Convert to s.
29
+ let dtFromLast = (currentPointer.timeStamp - this.lastPointer.timeStamp) / 1000; // s
30
+ // Don't divide by zero
31
+ if (dtFromLast === 0) {
32
+ dtFromLast = 1;
33
+ }
34
+ const currentVelocity = dxFromLast.times(1 / dtFromLast); // px/s
35
+ // Slight smoothing of the velocity to prevent input jitter from affecting the
36
+ // velocity too significantly.
37
+ this.averageVelocity = this.averageVelocity.lerp(currentVelocity, 0.5); // px/s
38
+ const dtFromStart = currentPointer.timeStamp - this.stationaryStartPointer.timeStamp; // ms
39
+ // If not stationary
40
+ if (dxFromStationaryStart.length() > this.config.maxRadius
41
+ || this.averageVelocity.length() > this.config.maxSpeed
42
+ || dtFromStart < this.config.minTimeSeconds) {
43
+ this.stationaryStartPointer = currentPointer;
44
+ this.lastPointer = currentPointer;
45
+ this.setStationaryTimeout(this.config.minTimeSeconds * 1000);
46
+ return false;
47
+ }
48
+ const stationaryTimeoutMs = this.config.minTimeSeconds * 1000 - dtFromStart;
49
+ this.lastPointer = currentPointer;
50
+ return stationaryTimeoutMs <= 0;
51
+ }
52
+ onPointerUp(pointer) {
53
+ if (pointer.id !== this.stationaryStartPointer?.id) {
54
+ this.cancelStationaryTimeout();
55
+ }
56
+ }
57
+ destroy() {
58
+ this.cancelStationaryTimeout();
59
+ this.stationaryStartPointer = null;
60
+ }
61
+ cancelStationaryTimeout() {
62
+ if (this.timeout !== null) {
63
+ clearTimeout(this.timeout);
64
+ this.timeout = null;
65
+ }
66
+ }
67
+ setStationaryTimeout(timeoutMs) {
68
+ if (this.timeout !== null) {
69
+ return;
70
+ }
71
+ if (timeoutMs <= 0) {
72
+ this.onStationary(this.lastPointer);
73
+ }
74
+ else {
75
+ this.timeout = setTimeout(() => {
76
+ this.timeout = null;
77
+ if (!this.stationaryStartPointer) {
78
+ // Destroyed
79
+ return;
80
+ }
81
+ const timeSinceStationaryStart = performance.now() - this.stationaryStartPointer.timeStamp;
82
+ const timeRemaining = this.config.minTimeSeconds * 1000 - timeSinceStationaryStart;
83
+ if (timeRemaining <= 0) {
84
+ this.onStationary(this.lastPointer);
85
+ }
86
+ else {
87
+ this.setStationaryTimeout(timeRemaining);
88
+ }
89
+ }, timeoutMs);
90
+ }
91
+ }
92
+ }
@@ -13,6 +13,8 @@ type UpdateCallback<T> = (value: T) => void;
13
13
  *
14
14
  * Static methods in the `ReactiveValue` and `MutableReactiveValue` classes are
15
15
  * constructors (e.g. `fromImmutable`).
16
+ *
17
+ * Avoid extending this class from an external library, as that may not be stable.
16
18
  */
17
19
  export declare abstract class ReactiveValue<T> {
18
20
  /**
@@ -31,6 +31,8 @@ const noOpSetUpdateListener = () => {
31
31
  *
32
32
  * Static methods in the `ReactiveValue` and `MutableReactiveValue` classes are
33
33
  * constructors (e.g. `fromImmutable`).
34
+ *
35
+ * Avoid extending this class from an external library, as that may not be stable.
34
36
  */
35
37
  export class ReactiveValue {
36
38
  /** Creates a `ReactiveValue` with an initial value, `initialValue`. */
@@ -1 +1,2 @@
1
1
  export { default as adjustEditorThemeForContrast } from './adjustEditorThemeForContrast';
2
+ export { ReactiveValue, MutableReactiveValue } from './ReactiveValue';
@@ -1 +1,2 @@
1
1
  export { default as adjustEditorThemeForContrast } from './adjustEditorThemeForContrast.mjs';
2
+ export { ReactiveValue, MutableReactiveValue } from './ReactiveValue.mjs';
@@ -0,0 +1,2 @@
1
+ declare const waitForImageLoad: (image: HTMLImageElement) => Promise<void>;
2
+ export default waitForImageLoad;
@@ -0,0 +1,10 @@
1
+ const waitForImageLoad = async (image) => {
2
+ if (!image.complete) {
3
+ await new Promise((resolve, reject) => {
4
+ image.onload = event => resolve(event);
5
+ image.onerror = event => reject(event);
6
+ image.onabort = event => reject(event);
7
+ });
8
+ }
9
+ };
10
+ export default waitForImageLoad;
@@ -1,3 +1,3 @@
1
1
  export default {
2
- number: '1.9.1',
2
+ number: '1.11.0',
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.9.1",
3
+ "version": "1.11.0",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -64,7 +64,7 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.9.0",
67
+ "@js-draw/math": "^1.10.0",
68
68
  "@melloware/coloris": "0.21.0"
69
69
  },
70
70
  "devDependencies": {
@@ -86,5 +86,5 @@
86
86
  "freehand",
87
87
  "svg"
88
88
  ],
89
- "gitHead": "b4bae7b437b2ce4ba8378a062b8e3959dca0f26e"
89
+ "gitHead": "01fc3dc7bdbc9f456705bf08d9c30b4549122d97"
90
90
  }