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
@@ -152,27 +152,51 @@ class PenToolWidget extends BaseToolWidget_1.default {
152
152
  },
153
153
  };
154
154
  }
155
- setInputStabilizationEnabled(enabled) {
156
- this.tool.setHasStabilization(enabled);
157
- }
158
- createStabilizationOption() {
159
- const stabilizationOption = document.createElement('div');
160
- const stabilizationCheckbox = document.createElement('input');
161
- const stabilizationLabel = document.createElement('label');
162
- stabilizationLabel.innerText = this.localizationTable.inputStabilization;
163
- stabilizationCheckbox.type = 'checkbox';
164
- stabilizationCheckbox.id = `${constants_1.toolbarCSSPrefix}-penInputStabilizationCheckbox-${PenToolWidget.idCounter++}`;
165
- stabilizationLabel.htmlFor = stabilizationCheckbox.id;
166
- stabilizationOption.replaceChildren(stabilizationLabel, stabilizationCheckbox);
167
- stabilizationCheckbox.oninput = () => {
168
- this.setInputStabilizationEnabled(stabilizationCheckbox.checked);
155
+ createStrokeCorrectionOptions() {
156
+ const container = document.createElement('div');
157
+ container.classList.add('action-button-row', `${constants_1.toolbarCSSPrefix}-pen-tool-toggle-buttons`);
158
+ const addToggleButton = (labelText, icon) => {
159
+ const button = document.createElement('button');
160
+ button.classList.add(`${constants_1.toolbarCSSPrefix}-toggle-button`);
161
+ const iconElement = icon.cloneNode(true);
162
+ iconElement.classList.add('icon');
163
+ const label = document.createElement('span');
164
+ label.innerText = labelText;
165
+ button.replaceChildren(iconElement, label);
166
+ button.setAttribute('role', 'switch');
167
+ container.appendChild(button);
168
+ let checked = false;
169
+ let onChangeListener = (_checked) => { };
170
+ const result = {
171
+ setChecked(newChecked) {
172
+ checked = newChecked;
173
+ button.setAttribute('aria-checked', `${checked}`);
174
+ onChangeListener(checked);
175
+ },
176
+ setOnInputListener(listener) {
177
+ onChangeListener = listener;
178
+ },
179
+ };
180
+ button.onclick = () => {
181
+ result.setChecked(!checked);
182
+ };
183
+ return result;
169
184
  };
185
+ const stabilizationOption = addToggleButton(this.localizationTable.inputStabilization, this.editor.icons.makeStrokeSmoothingIcon());
186
+ stabilizationOption.setOnInputListener(enabled => {
187
+ this.tool.setHasStabilization(enabled);
188
+ });
189
+ const autocorrectOption = addToggleButton(this.localizationTable.strokeAutocorrect, this.editor.icons.makeShapeAutocorrectIcon());
190
+ autocorrectOption.setOnInputListener(enabled => {
191
+ this.tool.setStrokeAutocorrectEnabled(enabled);
192
+ });
170
193
  return {
171
194
  update: () => {
172
- stabilizationCheckbox.checked = !!this.tool.getInputMapper();
195
+ stabilizationOption.setChecked(!!this.tool.getInputMapper());
196
+ autocorrectOption.setChecked(this.tool.getStrokeAutocorrectionEnabled());
173
197
  },
174
198
  addTo: (parent) => {
175
- parent.appendChild(stabilizationOption);
199
+ parent.appendChild(container);
176
200
  }
177
201
  };
178
202
  }
@@ -194,20 +218,22 @@ class PenToolWidget extends BaseToolWidget_1.default {
194
218
  colorLabel.setAttribute('for', colorInput.id);
195
219
  colorRow.appendChild(colorLabel);
196
220
  colorRow.appendChild(colorInputContainer);
197
- const stabilizationOption = this.createStabilizationOption();
221
+ const toggleButtonRow = this.createStrokeCorrectionOptions();
198
222
  this.updateInputs = () => {
199
223
  setColorInputValue(this.tool.getColor());
200
224
  setThickness(this.tool.getThickness());
201
225
  penTypeSelect.updateIcons();
202
226
  // Update the selected stroke factory.
203
227
  penTypeSelect.setValue(this.getCurrentPenTypeIdx());
204
- stabilizationOption.update();
228
+ toggleButtonRow.update();
205
229
  };
206
230
  this.updateInputs();
207
231
  container.replaceChildren(colorRow, thicknessRow);
208
232
  penTypeSelect.addTo(container);
209
- stabilizationOption.addTo(container);
210
233
  dropdown.replaceChildren(container);
234
+ // Add the toggle button row *outside* of the main content (use different
235
+ // spacing with respect to the sides of the container).
236
+ toggleButtonRow.addTo(dropdown);
211
237
  return true;
212
238
  }
213
239
  onKeyPress(event) {
@@ -237,6 +263,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
237
263
  thickness: this.tool.getThickness(),
238
264
  strokeFactoryId: this.getCurrentPenType()?.id,
239
265
  inputStabilization: !!this.tool.getInputMapper(),
266
+ strokeAutocorrect: this.tool.getStrokeAutocorrectionEnabled(),
240
267
  };
241
268
  }
242
269
  deserializeFrom(state) {
@@ -267,7 +294,10 @@ class PenToolWidget extends BaseToolWidget_1.default {
267
294
  }
268
295
  }
269
296
  if (state.inputStabilization !== undefined) {
270
- this.setInputStabilizationEnabled(!!state.inputStabilization);
297
+ this.tool.setHasStabilization(!!state.inputStabilization);
298
+ }
299
+ if (state.strokeAutocorrect !== undefined) {
300
+ this.tool.setStrokeAutocorrectEnabled(!!state.strokeAutocorrect);
271
301
  }
272
302
  }
273
303
  }
@@ -1,3 +1,4 @@
1
1
  export declare const resizeImageToSelectionKeyboardShortcut = "jsdraw.toolbar.SelectionTool.resizeImageToSelection";
2
2
  export declare const selectStrokeTypeKeyboardShortcutIds: string[];
3
3
  export declare const saveKeyboardShortcut = "jsdraw.toolbar.SaveActionWidget.save";
4
+ export declare const exitKeyboardShortcut = "jsdraw.toolbar.ExitActionWidget.exit";
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.saveKeyboardShortcut = exports.selectStrokeTypeKeyboardShortcutIds = exports.resizeImageToSelectionKeyboardShortcut = void 0;
6
+ exports.exitKeyboardShortcut = exports.saveKeyboardShortcut = exports.selectStrokeTypeKeyboardShortcutIds = exports.resizeImageToSelectionKeyboardShortcut = void 0;
7
7
  const KeyboardShortcutManager_1 = __importDefault(require("../../shortcuts/KeyboardShortcutManager"));
8
8
  // Selection
9
9
  exports.resizeImageToSelectionKeyboardShortcut = 'jsdraw.toolbar.SelectionTool.resizeImageToSelection';
@@ -17,3 +17,6 @@ for (let i = 0; i < exports.selectStrokeTypeKeyboardShortcutIds.length; i++) {
17
17
  // Save
18
18
  exports.saveKeyboardShortcut = 'jsdraw.toolbar.SaveActionWidget.save';
19
19
  KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.saveKeyboardShortcut, ['ctrlOrMeta+KeyS'], 'Save');
20
+ // Exit
21
+ exports.exitKeyboardShortcut = 'jsdraw.toolbar.ExitActionWidget.exit';
22
+ KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.exitKeyboardShortcut, ['Alt+KeyQ'], 'Exit');
@@ -1,4 +1,4 @@
1
- import ReactiveValue from 'js-draw/src/util/ReactiveValue';
1
+ import ReactiveValue from '../../../util/ReactiveValue';
2
2
  /**
3
3
  * A class that manages whether/what content is shown for a widget.
4
4
  *
@@ -19,6 +19,11 @@ export default class Pen extends BaseTool {
19
19
  private currentDeviceType;
20
20
  private styleValue;
21
21
  private style;
22
+ private shapeAutocompletionEnabled;
23
+ private autocorrectedShape;
24
+ private lastAutocorrectedShape;
25
+ private removedAutocorrectedShapeTime;
26
+ private stationaryDetector;
22
27
  constructor(editor: Editor, description: string, style: Partial<PenStyle>);
23
28
  private getPressureMultiplier;
24
29
  protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
@@ -30,12 +35,16 @@ export default class Pen extends BaseTool {
30
35
  onPointerMove({ current }: PointerEvt): void;
31
36
  onPointerUp({ current }: PointerEvt): boolean;
32
37
  onGestureCancel(): void;
38
+ private removedAutocorrectedShapeRecently;
39
+ private autocorrectShape;
33
40
  private finalizeStroke;
34
41
  private noteUpdated;
35
42
  setColor(color: Color4): void;
36
43
  setThickness(thickness: number): void;
37
44
  setStrokeFactory(factory: ComponentBuilderFactory): void;
38
45
  setHasStabilization(hasStabilization: boolean): void;
46
+ setStrokeAutocorrectEnabled(enabled: boolean): void;
47
+ getStrokeAutocorrectionEnabled(): boolean;
39
48
  getThickness(): number;
40
49
  getColor(): Color4;
41
50
  getStrokeFactory(): ComponentBuilderFactory;
@@ -13,6 +13,7 @@ const keybindings_1 = require("./keybindings");
13
13
  const keybindings_2 = require("./keybindings");
14
14
  const InputStabilizer_1 = __importDefault(require("./InputFilter/InputStabilizer"));
15
15
  const ReactiveValue_1 = require("../util/ReactiveValue");
16
+ const StationaryPenDetector_1 = __importDefault(require("./util/StationaryPenDetector"));
16
17
  class Pen extends BaseTool_1.default {
17
18
  constructor(editor, description, style) {
18
19
  super(editor.notifier, description);
@@ -21,6 +22,11 @@ class Pen extends BaseTool_1.default {
21
22
  this.lastPoint = null;
22
23
  this.startPoint = null;
23
24
  this.currentDeviceType = null;
25
+ this.shapeAutocompletionEnabled = false;
26
+ this.autocorrectedShape = null;
27
+ this.lastAutocorrectedShape = null;
28
+ this.removedAutocorrectedShapeTime = 0;
29
+ this.stationaryDetector = null;
24
30
  this.styleValue = ReactiveValue_1.ReactiveValue.fromInitialValue({
25
31
  factory: FreehandLineBuilder_1.makeFreehandLineBuilder,
26
32
  color: math_1.Color4.blue,
@@ -58,7 +64,14 @@ class Pen extends BaseTool_1.default {
58
64
  // Displays the stroke that is currently being built with the display's `wetInkRenderer`.
59
65
  previewStroke() {
60
66
  this.editor.clearWetInk();
61
- this.builder?.preview(this.editor.display.getWetInkRenderer());
67
+ const wetInkRenderer = this.editor.display.getWetInkRenderer();
68
+ if (this.autocorrectedShape) {
69
+ const visibleRect = this.editor.viewport.visibleRect;
70
+ this.autocorrectedShape.render(wetInkRenderer, visibleRect);
71
+ }
72
+ else {
73
+ this.builder?.preview(wetInkRenderer);
74
+ }
62
75
  }
63
76
  // Throws if no stroke builder exists.
64
77
  addPointToStroke(point) {
@@ -87,6 +100,19 @@ class Pen extends BaseTool_1.default {
87
100
  this.startPoint = this.toStrokePoint(current);
88
101
  this.builder = this.style.factory(this.startPoint, this.editor.viewport);
89
102
  this.currentDeviceType = current.device;
103
+ if (this.shapeAutocompletionEnabled) {
104
+ const stationaryDetectionConfig = {
105
+ maxSpeed: 5,
106
+ maxRadius: 10,
107
+ minTimeSeconds: 0.5, // s
108
+ };
109
+ this.stationaryDetector = new StationaryPenDetector_1.default(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
110
+ }
111
+ else {
112
+ this.stationaryDetector = null;
113
+ }
114
+ this.lastAutocorrectedShape = null;
115
+ this.removedAutocorrectedShapeTime = 0;
90
116
  return true;
91
117
  }
92
118
  return false;
@@ -114,7 +140,14 @@ class Pen extends BaseTool_1.default {
114
140
  return;
115
141
  if (current.device !== this.currentDeviceType)
116
142
  return;
117
- this.addPointToStroke(this.toStrokePoint(current));
143
+ const isStationary = this.stationaryDetector?.onPointerMove(current);
144
+ if (!isStationary) {
145
+ this.addPointToStroke(this.toStrokePoint(current));
146
+ if (this.autocorrectedShape) {
147
+ this.removedAutocorrectedShapeTime = performance.now();
148
+ this.autocorrectedShape = null;
149
+ }
150
+ }
118
151
  }
119
152
  onPointerUp({ current }) {
120
153
  if (!this.builder)
@@ -124,6 +157,7 @@ class Pen extends BaseTool_1.default {
124
157
  // device type.
125
158
  return true;
126
159
  }
160
+ this.stationaryDetector?.onPointerUp(current);
127
161
  // onPointerUp events can have zero pressure. Use the last pressure instead.
128
162
  const currentPoint = this.toStrokePoint(current);
129
163
  const strokePoint = {
@@ -139,10 +173,42 @@ class Pen extends BaseTool_1.default {
139
173
  onGestureCancel() {
140
174
  this.builder = null;
141
175
  this.editor.clearWetInk();
176
+ this.stationaryDetector?.destroy();
177
+ this.stationaryDetector = null;
178
+ }
179
+ removedAutocorrectedShapeRecently() {
180
+ return this.removedAutocorrectedShapeTime > performance.now() - 320;
181
+ }
182
+ async autocorrectShape(_lastPointer) {
183
+ if (!this.builder || !this.builder.autocorrectShape)
184
+ return;
185
+ if (!this.shapeAutocompletionEnabled)
186
+ return;
187
+ // If already corrected, do nothing
188
+ if (this.autocorrectedShape)
189
+ return;
190
+ // Activate stroke fitting
191
+ const correctedShape = await this.builder.autocorrectShape();
192
+ if (!this.builder || !correctedShape) {
193
+ return;
194
+ }
195
+ // Don't complete to empty shapes.
196
+ const bboxArea = correctedShape.getBBox().area;
197
+ if (bboxArea === 0 || !isFinite(bboxArea)) {
198
+ return;
199
+ }
200
+ this.autocorrectedShape = correctedShape;
201
+ this.lastAutocorrectedShape = correctedShape;
202
+ this.previewStroke();
142
203
  }
143
204
  finalizeStroke() {
144
205
  if (this.builder) {
145
- const stroke = this.builder.build();
206
+ // If autocorrectedShape was cleared recently enough, it was
207
+ // probably by mistake. Reset it.
208
+ if (this.lastAutocorrectedShape && this.removedAutocorrectedShapeRecently()) {
209
+ this.autocorrectedShape = this.lastAutocorrectedShape;
210
+ }
211
+ const stroke = this.autocorrectedShape ?? this.builder.build();
146
212
  this.previewStroke();
147
213
  if (stroke.getBBox().area > 0) {
148
214
  const canFlatten = true;
@@ -155,7 +221,11 @@ class Pen extends BaseTool_1.default {
155
221
  }
156
222
  this.builder = null;
157
223
  this.lastPoint = null;
224
+ this.autocorrectedShape = null;
225
+ this.lastAutocorrectedShape = null;
158
226
  this.editor.clearWetInk();
227
+ this.stationaryDetector?.destroy();
228
+ this.stationaryDetector = null;
159
229
  }
160
230
  noteUpdated() {
161
231
  this.editor.notifier.dispatch(types_1.EditorEventType.ToolUpdated, {
@@ -201,6 +271,15 @@ class Pen extends BaseTool_1.default {
201
271
  }
202
272
  this.noteUpdated();
203
273
  }
274
+ setStrokeAutocorrectEnabled(enabled) {
275
+ if (enabled !== this.shapeAutocompletionEnabled) {
276
+ this.shapeAutocompletionEnabled = enabled;
277
+ this.noteUpdated();
278
+ }
279
+ }
280
+ getStrokeAutocorrectionEnabled() {
281
+ return this.shapeAutocompletionEnabled;
282
+ }
204
283
  getThickness() { return this.style.thickness; }
205
284
  getColor() { return this.style.color; }
206
285
  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;
@@ -41,6 +41,7 @@ const Duplicate_1 = __importDefault(require("../../commands/Duplicate"));
41
41
  const TransformMode_1 = require("./TransformMode");
42
42
  const types_1 = require("./types");
43
43
  const EditorImage_1 = __importDefault(require("../../image/EditorImage"));
44
+ const uniteCommands_1 = __importDefault(require("../../commands/uniteCommands"));
44
45
  const updateChunkSize = 100;
45
46
  const maxPreviewElemCount = 500;
46
47
  // @internal
@@ -51,6 +52,7 @@ class Selection {
51
52
  // @see getTightBoundingBox
52
53
  this.selectionTightBoundingBox = null;
53
54
  this.transform = math_1.Mat33.identity;
55
+ // invariant: sorted by increasing z-index
54
56
  this.selectedElems = [];
55
57
  this.hasParent = true;
56
58
  // Maps IDs to whether we removed the component from the image
@@ -161,6 +163,16 @@ class Selection {
161
163
  this.previewTransformCmds();
162
164
  }
163
165
  }
166
+ getDeltaZIndexToMoveSelectionToTop() {
167
+ if (this.selectedElems.length === 0) {
168
+ return 0;
169
+ }
170
+ const selectedBottommostZIndex = this.selectedElems[0].getZIndex();
171
+ const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.region);
172
+ const topMostVisibleZIndex = visibleObjects[visibleObjects.length - 1]?.getZIndex() ?? selectedBottommostZIndex;
173
+ const deltaZIndex = (topMostVisibleZIndex + 1) - selectedBottommostZIndex;
174
+ return deltaZIndex;
175
+ }
164
176
  // Applies the current transformation to the selection
165
177
  finalizeTransform() {
166
178
  const fullTransform = this.transform;
@@ -169,17 +181,35 @@ class Selection {
169
181
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
170
182
  this.transform = math_1.Mat33.identity;
171
183
  this.scrollTo();
184
+ let transformPromise = undefined;
172
185
  // Make the commands undo-able.
173
186
  // Don't check for non-empty transforms because this breaks changing the
174
187
  // z-index of the just-transformed commands.
175
- //
176
- // TODO: Check whether the selectedElems are already all toplevel.
177
- const transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
188
+ if (this.selectedElems.length > 0) {
189
+ const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
190
+ transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform, deltaZIndex));
191
+ }
178
192
  // Clear renderings of any in-progress transformations
179
193
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
180
194
  wetInkRenderer.clear();
181
195
  return transformPromise;
182
196
  }
197
+ /** Sends all selected elements to the bottom of the visible image. */
198
+ sendToBack() {
199
+ const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.editor.viewport.visibleRect);
200
+ // VisibleObjects and selectedElems should both be sorted by z-index
201
+ const lowestVisibleZIndex = visibleObjects[0]?.getZIndex() ?? 0;
202
+ const highestSelectedZIndex = this.selectedElems[this.selectedElems.length - 1]?.getZIndex() ?? 0;
203
+ const targetHighestZIndex = lowestVisibleZIndex - 1;
204
+ const deltaZIndex = targetHighestZIndex - highestSelectedZIndex;
205
+ if (deltaZIndex !== 0) {
206
+ const commands = this.selectedElems.map(elem => {
207
+ return elem.setZIndex(elem.getZIndex() + deltaZIndex);
208
+ });
209
+ return (0, uniteCommands_1.default)(commands, updateChunkSize);
210
+ }
211
+ return null;
212
+ }
183
213
  // Preview the effects of the current transformation on the selection
184
214
  previewTransformCmds() {
185
215
  if (this.selectedElems.length === 0) {
@@ -193,7 +223,7 @@ class Selection {
193
223
  const wetInkRenderer = this.editor.display.getWetInkRenderer();
194
224
  wetInkRenderer.clear();
195
225
  wetInkRenderer.pushTransform(this.transform);
196
- const viewportVisibleRect = this.editor.viewport.visibleRect;
226
+ const viewportVisibleRect = this.editor.viewport.visibleRect.union(this.region);
197
227
  const visibleRect = viewportVisibleRect.transformedBoundingBox(this.transform.inverse());
198
228
  for (const elem of this.selectedElems) {
199
229
  elem.render(wetInkRenderer, visibleRect);
@@ -439,7 +469,8 @@ class Selection {
439
469
  if (wasTransforming) {
440
470
  // Don't update the selection's focus when redoing/undoing
441
471
  const selectionToUpdate = null;
442
- tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
472
+ const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
473
+ tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform, deltaZIndex);
443
474
  // Transform to ensure that the duplicates are in the correct location
444
475
  await tmpApplyCommand.apply(this.editor);
445
476
  // Show items again
@@ -480,6 +511,8 @@ class Selection {
480
511
  this.originalRegion = bbox;
481
512
  this.selectionTightBoundingBox = bbox;
482
513
  this.selectedElems = objects.filter(object => object.isSelectable());
514
+ // Enforce increasing z-index invariant
515
+ this.selectedElems.sort((a, b) => a.getZIndex() - b.getZIndex());
483
516
  this.padRegion();
484
517
  this.updateUI();
485
518
  }
@@ -493,7 +526,8 @@ _a = Selection;
493
526
  // The selection box is lost when serializing/deserializing. No need to store box rotation
494
527
  const fullTransform = new math_1.Mat33(...json.transform);
495
528
  const elemIds = (json.elems ?? []);
496
- return new _a.ApplyTransformationCommand(null, elemIds, fullTransform);
529
+ const deltaZIndex = parseInt(json.deltaZIndex ?? 0);
530
+ return new _a.ApplyTransformationCommand(null, elemIds, fullTransform, deltaZIndex);
497
531
  });
498
532
  })();
499
533
  Selection.ApplyTransformationCommand = class extends SerializableCommand_1.default {
@@ -501,10 +535,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
501
535
  // If a `string[]`, selectedElems is a list of element IDs.
502
536
  selectedElems,
503
537
  // Full transformation used to transform elements.
504
- fullTransform) {
538
+ fullTransform, deltaZIndex) {
505
539
  super('selection-tool-transform');
506
540
  this.selection = selection;
507
541
  this.fullTransform = fullTransform;
542
+ this.deltaZIndex = deltaZIndex;
508
543
  const isIDList = (arr) => {
509
544
  return typeof arr[0] === 'string';
510
545
  };
@@ -515,11 +550,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
515
550
  else {
516
551
  this.selectedElemIds = selectedElems.map(elem => elem.getId());
517
552
  this.transformCommands = selectedElems.map(elem => {
518
- return elem.transformBy(this.fullTransform);
553
+ return elem.setZIndexAndTransformBy(this.fullTransform, elem.getZIndex() + deltaZIndex);
519
554
  });
520
555
  }
521
556
  }
522
- resolveToElems(editor) {
557
+ resolveToElems(editor, isUndoing) {
523
558
  if (this.transformCommands) {
524
559
  return;
525
560
  }
@@ -528,11 +563,19 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
528
563
  if (!elem) {
529
564
  throw new Error(`Unable to find element with ID, ${id}.`);
530
565
  }
531
- return elem.transformBy(this.fullTransform);
566
+ let originalZIndex = elem.getZIndex();
567
+ let targetZIndex = elem.getZIndex() + this.deltaZIndex;
568
+ // If the command has already been applied, the element should currently
569
+ // have the target z-index.
570
+ if (isUndoing) {
571
+ targetZIndex = elem.getZIndex();
572
+ originalZIndex = elem.getZIndex() - this.deltaZIndex;
573
+ }
574
+ return elem.setZIndexAndTransformBy(this.fullTransform, targetZIndex, originalZIndex);
532
575
  });
533
576
  }
534
577
  async apply(editor) {
535
- this.resolveToElems(editor);
578
+ this.resolveToElems(editor, false);
536
579
  this.selection?.setTransform(this.fullTransform, false);
537
580
  this.selection?.updateUI();
538
581
  await editor.asyncApplyCommands(this.transformCommands, updateChunkSize);
@@ -541,7 +584,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
541
584
  this.selection?.updateUI();
542
585
  }
543
586
  async unapply(editor) {
544
- this.resolveToElems(editor);
587
+ this.resolveToElems(editor, true);
545
588
  this.selection?.setTransform(this.fullTransform.inverse(), false);
546
589
  this.selection?.updateUI();
547
590
  await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
@@ -553,6 +596,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
553
596
  return {
554
597
  elems: this.selectedElemIds,
555
598
  transform: this.fullTransform.toArray(),
599
+ deltaZIndex: this.deltaZIndex,
556
600
  };
557
601
  }
558
602
  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;
@@ -27,6 +27,9 @@ class SelectionTool extends BaseTool_1.default {
27
27
  this.lastPointer = null;
28
28
  this.selectionBoxHandlingEvt = false;
29
29
  this.lastSelectedObjects = [];
30
+ // Whether the last keypress corresponded to an action that didn't transform the
31
+ // selection (and thus does not need to be finalized on onKeyUp).
32
+ this.hasUnfinalizedTransformFromKeyPress = false;
30
33
  this.autoscroller = new ToPointerAutoscroller_1.default(editor.viewport, (scrollBy) => {
31
34
  editor.dispatch(Viewport_1.default.transformBy(math_1.Mat33.translation(scrollBy)), false);
32
35
  // Update the selection box/content to match the new viewport.
@@ -219,7 +222,8 @@ class SelectionTool extends BaseTool_1.default {
219
222
  this.snapToGrid = true;
220
223
  return true;
221
224
  }
222
- if (this.selectionBox && shortcucts.matchesShortcut(keybindings_1.duplicateSelectionShortcut, event)) {
225
+ if (this.selectionBox && (shortcucts.matchesShortcut(keybindings_1.duplicateSelectionShortcut, event)
226
+ || shortcucts.matchesShortcut(keybindings_1.sendToBackSelectionShortcut, event))) {
223
227
  // Handle duplication on key up — we don't want to accidentally duplicate
224
228
  // many times.
225
229
  return true;
@@ -303,6 +307,8 @@ class SelectionTool extends BaseTool_1.default {
303
307
  const oldTransform = this.selectionBox.getTransform();
304
308
  this.selectionBox.setTransform(oldTransform.rightMul(transform));
305
309
  this.selectionBox.scrollTo();
310
+ // The transformation needs to be finalized at some point (on key up)
311
+ this.hasUnfinalizedTransformFromKeyPress = true;
306
312
  }
307
313
  if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
308
314
  this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
@@ -328,12 +334,24 @@ class SelectionTool extends BaseTool_1.default {
328
334
  });
329
335
  return true;
330
336
  }
337
+ if (this.selectionBox && shortcucts.matchesShortcut(keybindings_1.sendToBackSelectionShortcut, evt)) {
338
+ const sendToBackCommand = this.selectionBox.sendToBack();
339
+ if (sendToBackCommand) {
340
+ this.editor.dispatch(sendToBackCommand);
341
+ }
342
+ return true;
343
+ }
331
344
  if (evt.key === 'Shift') {
332
345
  this.shiftKeyPressed = false;
333
346
  return true;
334
347
  }
348
+ // If we don't need to finalize the transform
349
+ if (!this.hasUnfinalizedTransformFromKeyPress) {
350
+ return true;
351
+ }
335
352
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
336
353
  this.selectionBox.finalizeTransform();
354
+ this.hasUnfinalizedTransformFromKeyPress = false;
337
355
  return true;
338
356
  }
339
357
  return false;
@@ -44,6 +44,10 @@ class TextTool extends BaseTool_1.default {
44
44
  .${overlayCSSClass} {
45
45
  height: 0;
46
46
  overflow: visible;
47
+
48
+ /* Allows absolutely-positioned textareas to scroll with
49
+ the containing overlay. */
50
+ position: relative;
47
51
  }
48
52
 
49
53
  .${overlayCSSClass} textarea {
@@ -126,7 +130,7 @@ class TextTool extends BaseTool_1.default {
126
130
  this.textInputElem.style.fontWeight = this.textStyle.fontWeight ?? '';
127
131
  this.textInputElem.style.fontSize = `${this.textStyle.size}px`;
128
132
  this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
129
- this.textInputElem.style.position = 'relative';
133
+ this.textInputElem.style.position = 'absolute';
130
134
  this.textInputElem.style.left = `${textScreenPos.x}px`;
131
135
  this.textInputElem.style.top = `${textScreenPos.y}px`;
132
136
  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;
@@ -10,7 +10,6 @@ const BaseTool_1 = __importDefault(require("./BaseTool"));
10
10
  *
11
11
  * This is in the default set of {@link ToolController} tools.
12
12
  *
13
- * @deprecated This may be replaced in the future.
14
13
  */
15
14
  class ToolSwitcherShortcut extends BaseTool_1.default {
16
15
  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";
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.duplicateSelectionShortcut = exports.selectAllKeyboardShortcut = exports.zoomOutKeyboardShortcutId = exports.zoomInKeyboardShortcutId = exports.rotateCounterClockwiseKeyboardShortcutId = exports.rotateClockwiseKeyboardShortcutId = exports.moveDownKeyboardShortcutId = exports.moveUpKeyboardShortcutId = exports.moveRightKeyboardShortcutId = exports.moveLeftKeyboardShortcutId = exports.toggleFindVisibleShortcutId = exports.lineLockKeyboardShortcutId = exports.snapToGridKeyboardShortcutId = exports.decreaseSizeKeyboardShortcutId = exports.increaseSizeKeyboardShortcutId = exports.redoKeyboardShortcutId = exports.undoKeyboardShortcutId = void 0;
6
+ exports.sendToBackSelectionShortcut = exports.duplicateSelectionShortcut = exports.selectAllKeyboardShortcut = exports.zoomOutKeyboardShortcutId = exports.zoomInKeyboardShortcutId = exports.rotateCounterClockwiseKeyboardShortcutId = exports.rotateClockwiseKeyboardShortcutId = exports.moveDownKeyboardShortcutId = exports.moveUpKeyboardShortcutId = exports.moveRightKeyboardShortcutId = exports.moveLeftKeyboardShortcutId = exports.toggleFindVisibleShortcutId = exports.lineLockKeyboardShortcutId = exports.snapToGridKeyboardShortcutId = exports.decreaseSizeKeyboardShortcutId = exports.increaseSizeKeyboardShortcutId = exports.redoKeyboardShortcutId = exports.undoKeyboardShortcutId = void 0;
7
7
  const KeyboardShortcutManager_1 = __importDefault(require("../shortcuts/KeyboardShortcutManager"));
8
8
  // This file contains user-overridable tool-realted keybindings.
9
9
  // Undo/redo
@@ -45,3 +45,5 @@ exports.selectAllKeyboardShortcut = 'jsdraw.tools.SelectionTool.selectAll';
45
45
  KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.selectAllKeyboardShortcut, ['CtrlOrMeta+KeyA'], 'Select all');
46
46
  exports.duplicateSelectionShortcut = 'jsdraw.tools.SelectionTool.duplicateSelection';
47
47
  KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.duplicateSelectionShortcut, ['CtrlOrMeta+KeyD'], 'Duplicate selection');
48
+ exports.sendToBackSelectionShortcut = 'jsdraw.tools.SelectionTool.sendToBack';
49
+ KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.sendToBackSelectionShortcut, ['End'], 'Send to back');