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.
- package/dist/Editor.css +48 -1
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +41 -0
- package/dist/cjs/Editor.js +9 -0
- package/dist/cjs/Pointer.js +1 -1
- package/dist/cjs/commands/Erase.d.ts +22 -2
- package/dist/cjs/commands/Erase.js +22 -2
- package/dist/cjs/commands/invertCommand.js +5 -0
- package/dist/cjs/commands/uniteCommands.d.ts +36 -0
- package/dist/cjs/commands/uniteCommands.js +36 -0
- package/dist/cjs/components/AbstractComponent.d.ts +8 -0
- package/dist/cjs/components/AbstractComponent.js +28 -8
- package/dist/cjs/components/ImageComponent.d.ts +12 -0
- package/dist/cjs/components/ImageComponent.js +16 -9
- package/dist/cjs/components/Stroke.d.ts +16 -2
- package/dist/cjs/components/Stroke.js +17 -1
- package/dist/cjs/components/builders/ArrowBuilder.js +3 -3
- package/dist/cjs/components/builders/CircleBuilder.js +3 -3
- package/dist/cjs/components/builders/FreehandLineBuilder.js +3 -3
- package/dist/cjs/components/builders/LineBuilder.js +3 -3
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +3 -3
- package/dist/cjs/components/builders/RectangleBuilder.js +5 -6
- package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
- package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +168 -0
- package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
- package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.js +46 -0
- package/dist/cjs/components/builders/types.d.ts +12 -0
- package/dist/cjs/image/EditorImage.d.ts +32 -1
- package/dist/cjs/image/EditorImage.js +32 -1
- package/dist/cjs/rendering/RenderablePathSpec.d.ts +5 -1
- package/dist/cjs/rendering/RenderablePathSpec.js +4 -0
- package/dist/cjs/toolbar/AbstractToolbar.d.ts +18 -2
- package/dist/cjs/toolbar/AbstractToolbar.js +46 -30
- package/dist/cjs/toolbar/IconProvider.d.ts +2 -0
- package/dist/cjs/toolbar/IconProvider.js +17 -0
- package/dist/cjs/toolbar/localization.d.ts +3 -0
- package/dist/cjs/toolbar/localization.js +4 -1
- package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -1
- package/dist/cjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
- package/dist/cjs/toolbar/widgets/ExitActionWidget.js +32 -0
- package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
- package/dist/cjs/toolbar/widgets/HandToolWidget.js +24 -13
- package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
- package/dist/cjs/toolbar/widgets/InsertImageWidget.js +102 -22
- package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +50 -20
- package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
- package/dist/cjs/toolbar/widgets/layout/types.d.ts +1 -1
- package/dist/cjs/tools/Pen.d.ts +9 -0
- package/dist/cjs/tools/Pen.js +82 -3
- package/dist/cjs/tools/SelectionTool/Selection.d.ts +4 -0
- package/dist/cjs/tools/SelectionTool/Selection.js +56 -12
- package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +19 -1
- package/dist/cjs/tools/TextTool.js +5 -1
- package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +0 -1
- package/dist/cjs/tools/ToolSwitcherShortcut.js +0 -1
- package/dist/cjs/tools/keybindings.d.ts +1 -0
- package/dist/cjs/tools/keybindings.js +3 -1
- package/dist/cjs/tools/util/StationaryPenDetector.d.ts +22 -0
- package/dist/cjs/tools/util/StationaryPenDetector.js +95 -0
- package/dist/cjs/util/ReactiveValue.d.ts +2 -0
- package/dist/cjs/util/ReactiveValue.js +2 -0
- package/dist/cjs/util/lib.d.ts +1 -0
- package/dist/cjs/util/lib.js +4 -1
- package/dist/cjs/util/waitForImageLoaded.d.ts +2 -0
- package/dist/cjs/util/waitForImageLoaded.js +12 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +41 -0
- package/dist/mjs/Editor.mjs +9 -0
- package/dist/mjs/Pointer.mjs +1 -1
- package/dist/mjs/commands/Erase.d.ts +22 -2
- package/dist/mjs/commands/Erase.mjs +22 -2
- package/dist/mjs/commands/invertCommand.mjs +5 -0
- package/dist/mjs/commands/uniteCommands.d.ts +36 -0
- package/dist/mjs/commands/uniteCommands.mjs +36 -0
- package/dist/mjs/components/AbstractComponent.d.ts +8 -0
- package/dist/mjs/components/AbstractComponent.mjs +28 -8
- package/dist/mjs/components/ImageComponent.d.ts +12 -0
- package/dist/mjs/components/ImageComponent.mjs +16 -9
- package/dist/mjs/components/Stroke.d.ts +16 -2
- package/dist/mjs/components/Stroke.mjs +17 -1
- package/dist/mjs/components/builders/ArrowBuilder.mjs +3 -2
- package/dist/mjs/components/builders/CircleBuilder.mjs +3 -2
- package/dist/mjs/components/builders/FreehandLineBuilder.mjs +3 -2
- package/dist/mjs/components/builders/LineBuilder.mjs +3 -2
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +3 -2
- package/dist/mjs/components/builders/RectangleBuilder.mjs +5 -4
- package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
- package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +166 -0
- package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
- package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.mjs +44 -0
- package/dist/mjs/components/builders/types.d.ts +12 -0
- package/dist/mjs/image/EditorImage.d.ts +32 -1
- package/dist/mjs/image/EditorImage.mjs +32 -1
- package/dist/mjs/rendering/RenderablePathSpec.d.ts +5 -1
- package/dist/mjs/rendering/RenderablePathSpec.mjs +4 -0
- package/dist/mjs/toolbar/AbstractToolbar.d.ts +18 -2
- package/dist/mjs/toolbar/AbstractToolbar.mjs +46 -30
- package/dist/mjs/toolbar/IconProvider.d.ts +2 -0
- package/dist/mjs/toolbar/IconProvider.mjs +17 -0
- package/dist/mjs/toolbar/localization.d.ts +3 -0
- package/dist/mjs/toolbar/localization.mjs +4 -1
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -1
- package/dist/mjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
- package/dist/mjs/toolbar/widgets/ExitActionWidget.mjs +27 -0
- package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
- package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +24 -13
- package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
- package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +102 -22
- package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +50 -20
- package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
- package/dist/mjs/toolbar/widgets/layout/types.d.ts +1 -1
- package/dist/mjs/tools/Pen.d.ts +9 -0
- package/dist/mjs/tools/Pen.mjs +82 -3
- package/dist/mjs/tools/SelectionTool/Selection.d.ts +4 -0
- package/dist/mjs/tools/SelectionTool/Selection.mjs +56 -12
- package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +20 -2
- package/dist/mjs/tools/TextTool.mjs +5 -1
- package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +0 -1
- package/dist/mjs/tools/ToolSwitcherShortcut.mjs +0 -1
- package/dist/mjs/tools/keybindings.d.ts +1 -0
- package/dist/mjs/tools/keybindings.mjs +2 -0
- package/dist/mjs/tools/util/StationaryPenDetector.d.ts +22 -0
- package/dist/mjs/tools/util/StationaryPenDetector.mjs +92 -0
- package/dist/mjs/util/ReactiveValue.d.ts +2 -0
- package/dist/mjs/util/ReactiveValue.mjs +2 -0
- package/dist/mjs/util/lib.d.ts +1 -0
- package/dist/mjs/util/lib.mjs +1 -0
- package/dist/mjs/util/waitForImageLoaded.d.ts +2 -0
- package/dist/mjs/util/waitForImageLoaded.mjs +10 -0
- package/dist/mjs/version.mjs +1 -1
- package/package.json +3 -3
- package/src/Editor.scss +7 -0
- package/src/toolbar/AbstractToolbar.scss +20 -0
- package/src/toolbar/toolbar.scss +1 -1
- package/src/toolbar/widgets/InsertImageWidget.scss +6 -1
- package/src/toolbar/widgets/PenToolWidget.scss +33 -0
- package/src/tools/SelectionTool/SelectionTool.scss +6 -0
- package/src/toolbar/widgets/PenToolWidget.css +0 -2
package/dist/mjs/tools/Pen.mjs
CHANGED
@@ -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.
|
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.
|
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
|
-
|
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
|
-
|
149
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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 = '
|
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';
|
@@ -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`. */
|
package/dist/mjs/util/lib.d.ts
CHANGED
package/dist/mjs/util/lib.mjs
CHANGED
@@ -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;
|
package/dist/mjs/version.mjs
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "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.
|
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": "
|
89
|
+
"gitHead": "01fc3dc7bdbc9f456705bf08d9c30b4549122d97"
|
90
90
|
}
|