js-draw 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/dist/Editor.css +30 -4
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +5 -0
- package/dist/cjs/Editor.js +53 -70
- package/dist/cjs/components/BackgroundComponent.js +6 -1
- package/dist/cjs/components/TextComponent.d.ts +1 -1
- package/dist/cjs/components/TextComponent.js +19 -12
- package/dist/cjs/localization.d.ts +2 -0
- package/dist/cjs/localization.js +2 -0
- package/dist/cjs/localizations/comments.js +1 -0
- package/dist/cjs/rendering/RenderablePathSpec.js +16 -1
- package/dist/cjs/rendering/caching/CacheRecordManager.d.ts +1 -0
- package/dist/cjs/rendering/caching/CacheRecordManager.js +18 -0
- package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
- package/dist/cjs/rendering/caching/RenderingCache.js +3 -0
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -2
- package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
- package/dist/cjs/tools/SelectionTool/Selection.js +75 -48
- package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
- package/dist/cjs/tools/SelectionTool/SelectionHandle.js +8 -3
- package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +36 -16
- package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
- package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
- package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +10 -3
- package/dist/cjs/tools/SelectionTool/TransformMode.js +52 -9
- package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
- package/dist/cjs/util/listenForKeyboardEventsFrom.js +142 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +5 -0
- package/dist/mjs/Editor.mjs +53 -70
- package/dist/mjs/components/BackgroundComponent.mjs +6 -1
- package/dist/mjs/components/TextComponent.d.ts +1 -1
- package/dist/mjs/components/TextComponent.mjs +19 -12
- package/dist/mjs/localization.d.ts +2 -0
- package/dist/mjs/localization.mjs +2 -0
- package/dist/mjs/localizations/comments.mjs +1 -0
- package/dist/mjs/rendering/RenderablePathSpec.mjs +16 -1
- package/dist/mjs/rendering/caching/CacheRecordManager.d.ts +1 -0
- package/dist/mjs/rendering/caching/CacheRecordManager.mjs +18 -0
- package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
- package/dist/mjs/rendering/caching/RenderingCache.mjs +3 -0
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -2
- package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
- package/dist/mjs/tools/SelectionTool/Selection.mjs +75 -48
- package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
- package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +8 -3
- package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -16
- package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
- package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
- package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +10 -3
- package/dist/mjs/tools/SelectionTool/TransformMode.mjs +52 -9
- package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
- package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +140 -0
- package/dist/mjs/version.mjs +1 -1
- package/package.json +4 -4
- package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -40,4 +40,22 @@ export class CacheRecordManager {
|
|
40
40
|
this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
|
41
41
|
return this.cacheRecords[0];
|
42
42
|
}
|
43
|
+
// Returns information to (hopefully) help debug performance issues
|
44
|
+
getDebugInfo() {
|
45
|
+
let numberAllocd = 0;
|
46
|
+
let averageReassignedCount = 0;
|
47
|
+
for (const cacheRecord of this.cacheRecords) {
|
48
|
+
averageReassignedCount += cacheRecord.allocCount;
|
49
|
+
if (cacheRecord.isAllocd()) {
|
50
|
+
numberAllocd++;
|
51
|
+
}
|
52
|
+
}
|
53
|
+
averageReassignedCount /= Math.max(this.cacheRecords.length, 0);
|
54
|
+
const debugInfo = [
|
55
|
+
`${this.cacheRecords.length} cache records (max ${this.maxCanvases})`,
|
56
|
+
`${numberAllocd} assigned to screen regions`,
|
57
|
+
`Average number of times reassigned: ${Math.round(averageReassignedCount * 100) / 100}`,
|
58
|
+
];
|
59
|
+
return debugInfo.join('\n');
|
60
|
+
}
|
43
61
|
}
|
@@ -144,9 +144,10 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
144
144
|
return;
|
145
145
|
}
|
146
146
|
// If part of a huge object, it might be worth trimming the path
|
147
|
-
|
147
|
+
const visibleRect = this.getViewport().visibleRect;
|
148
|
+
if (this.currentObjectBBox?.containsRect(visibleRect)) {
|
148
149
|
// Try to trim/remove parts of the path outside of the bounding box.
|
149
|
-
path = visualEquivalent(path,
|
150
|
+
path = visualEquivalent(path, visibleRect);
|
150
151
|
}
|
151
152
|
super.drawPath(path);
|
152
153
|
}
|
@@ -15,7 +15,8 @@ export default class Selection {
|
|
15
15
|
private transformers;
|
16
16
|
private transform;
|
17
17
|
private selectedElems;
|
18
|
-
private
|
18
|
+
private outerContainer;
|
19
|
+
private innerContainer;
|
19
20
|
private backgroundElem;
|
20
21
|
private hasParent;
|
21
22
|
constructor(startPoint: Point2, editor: Editor);
|
@@ -32,10 +33,10 @@ export default class Selection {
|
|
32
33
|
get regionRotation(): number;
|
33
34
|
get preTransformedScreenRegion(): Rect2;
|
34
35
|
get preTransformedScreenRegionRotation(): number;
|
35
|
-
|
36
|
+
getScreenRegion(): Rect2;
|
36
37
|
get screenRegionRotation(): number;
|
37
38
|
setTransform(transform: Mat33, preview?: boolean): void;
|
38
|
-
finalizeTransform(): Promise<void>;
|
39
|
+
finalizeTransform(): void | Promise<void>;
|
39
40
|
private static ApplyTransformationCommand;
|
40
41
|
private previewTransformCmds;
|
41
42
|
resolveToObjects(): boolean;
|
@@ -53,7 +54,7 @@ export default class Selection {
|
|
53
54
|
onDragUpdate(pointer: Pointer): void;
|
54
55
|
onDragEnd(): void;
|
55
56
|
onDragCancel(): void;
|
56
|
-
scrollTo():
|
57
|
+
scrollTo(): boolean;
|
57
58
|
deleteSelectedObjects(): Command;
|
58
59
|
private selectionDuplicatedAnimationTimeout;
|
59
60
|
private runSelectionDuplicatedAnimation;
|
@@ -36,22 +36,34 @@ class Selection {
|
|
36
36
|
resize: new ResizeTransformer(editor, this),
|
37
37
|
rotate: new RotateTransformer(editor, this),
|
38
38
|
};
|
39
|
-
|
39
|
+
// We need two containers for some CSS to apply (the outer container
|
40
|
+
// needs zero height, the inner needs to prevent the selection background
|
41
|
+
// from being visible outside of the editor).
|
42
|
+
this.outerContainer = document.createElement('div');
|
43
|
+
this.outerContainer.classList.add(`${cssPrefix}selection-outer-container`);
|
44
|
+
this.innerContainer = document.createElement('div');
|
45
|
+
this.innerContainer.classList.add(`${cssPrefix}selection-inner-container`);
|
40
46
|
this.backgroundElem = document.createElement('div');
|
41
47
|
this.backgroundElem.classList.add(`${cssPrefix}selection-background`);
|
42
|
-
this.
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
}
|
48
|
+
this.innerContainer.appendChild(this.backgroundElem);
|
49
|
+
this.outerContainer.appendChild(this.innerContainer);
|
50
|
+
const makeResizeHandle = (mode, side) => {
|
51
|
+
const modeToAction = {
|
52
|
+
[ResizeMode.Both]: HandleAction.ResizeXY,
|
53
|
+
[ResizeMode.HorizontalOnly]: HandleAction.ResizeX,
|
54
|
+
[ResizeMode.VerticalOnly]: HandleAction.ResizeY,
|
55
|
+
};
|
56
|
+
return new SelectionHandle({
|
57
|
+
action: modeToAction[mode],
|
58
|
+
side,
|
59
|
+
}, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, mode), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
|
60
|
+
};
|
61
|
+
const resizeHorizontalHandles = [
|
62
|
+
makeResizeHandle(ResizeMode.HorizontalOnly, Vec2.of(0, 0.5)),
|
63
|
+
makeResizeHandle(ResizeMode.HorizontalOnly, Vec2.of(1, 0.5)),
|
64
|
+
];
|
65
|
+
const resizeVerticalHandle = makeResizeHandle(ResizeMode.VerticalOnly, Vec2.of(0.5, 1));
|
66
|
+
const resizeBothHandle = makeResizeHandle(ResizeMode.Both, Vec2.of(1, 1));
|
55
67
|
const rotationHandle = new SelectionHandle({
|
56
68
|
action: HandleAction.Rotate,
|
57
69
|
side: Vec2.of(0.5, 0),
|
@@ -59,7 +71,7 @@ class Selection {
|
|
59
71
|
}, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
|
60
72
|
this.handles = [
|
61
73
|
resizeBothHandle,
|
62
|
-
|
74
|
+
...resizeHorizontalHandles,
|
63
75
|
resizeVerticalHandle,
|
64
76
|
rotationHandle,
|
65
77
|
];
|
@@ -105,7 +117,7 @@ class Selection {
|
|
105
117
|
get preTransformedScreenRegionRotation() {
|
106
118
|
return this.editor.viewport.getRotationAngle();
|
107
119
|
}
|
108
|
-
|
120
|
+
getScreenRegion() {
|
109
121
|
const toScreen = this.editor.viewport.canvasToScreenTransform;
|
110
122
|
const scaleFactor = this.editor.viewport.getScaleFactor();
|
111
123
|
const screenCenter = toScreen.transformVec2(this.region.center);
|
@@ -118,29 +130,33 @@ class Selection {
|
|
118
130
|
setTransform(transform, preview = true) {
|
119
131
|
this.transform = transform;
|
120
132
|
if (preview && this.hasParent) {
|
121
|
-
this.scrollTo();
|
122
133
|
this.previewTransformCmds();
|
123
134
|
}
|
124
135
|
}
|
125
136
|
// Applies the current transformation to the selection
|
126
|
-
|
137
|
+
finalizeTransform() {
|
127
138
|
const fullTransform = this.transform;
|
128
139
|
const selectedElems = this.selectedElems;
|
129
140
|
// Reset for the next drag
|
130
141
|
this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
|
131
142
|
this.transform = Mat33.identity;
|
143
|
+
this.scrollTo();
|
132
144
|
// Make the commands undo-able.
|
133
145
|
// Don't check for non-empty transforms because this breaks changing the
|
134
146
|
// z-index of the just-transformed commands.
|
135
147
|
//
|
136
148
|
// TODO: Check whether the selectedElems are already all toplevel.
|
137
|
-
|
149
|
+
const transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
|
138
150
|
// Clear renderings of any in-progress transformations
|
139
151
|
const wetInkRenderer = this.editor.display.getWetInkRenderer();
|
140
152
|
wetInkRenderer.clear();
|
153
|
+
return transformPromise;
|
141
154
|
}
|
142
155
|
// Preview the effects of the current transformation on the selection
|
143
156
|
previewTransformCmds() {
|
157
|
+
if (this.selectedElems.length === 0) {
|
158
|
+
return;
|
159
|
+
}
|
144
160
|
// Don't render what we're moving if it's likely to be slow.
|
145
161
|
if (this.selectedElems.length > maxPreviewElemCount) {
|
146
162
|
this.updateUI();
|
@@ -221,28 +237,29 @@ class Selection {
|
|
221
237
|
if (!this.hasParent) {
|
222
238
|
return;
|
223
239
|
}
|
240
|
+
const screenRegion = this.getScreenRegion();
|
224
241
|
// marginLeft, marginTop: Display relative to the top left of the selection overlay.
|
225
242
|
// left, top don't work for this.
|
226
|
-
this.backgroundElem.style.marginLeft = `${
|
227
|
-
this.backgroundElem.style.marginTop = `${
|
228
|
-
this.backgroundElem.style.width = `${
|
229
|
-
this.backgroundElem.style.height = `${
|
243
|
+
this.backgroundElem.style.marginLeft = `${screenRegion.topLeft.x}px`;
|
244
|
+
this.backgroundElem.style.marginTop = `${screenRegion.topLeft.y}px`;
|
245
|
+
this.backgroundElem.style.width = `${screenRegion.width}px`;
|
246
|
+
this.backgroundElem.style.height = `${screenRegion.height}px`;
|
230
247
|
const rotationDeg = this.screenRegionRotation * 180 / Math.PI;
|
231
248
|
this.backgroundElem.style.transform = `rotate(${rotationDeg}deg)`;
|
232
249
|
this.backgroundElem.style.transformOrigin = 'center';
|
233
250
|
// If closer to perpendicular, apply different CSS
|
234
251
|
const perpendicularClassName = `${cssPrefix}rotated-near-perpendicular`;
|
235
252
|
if (Math.abs(Math.sin(this.screenRegionRotation)) > 0.5) {
|
236
|
-
this.
|
253
|
+
this.innerContainer.classList.add(perpendicularClassName);
|
237
254
|
}
|
238
255
|
else {
|
239
|
-
this.
|
256
|
+
this.innerContainer.classList.remove(perpendicularClassName);
|
240
257
|
}
|
241
258
|
for (const handle of this.handles) {
|
242
259
|
handle.updatePosition();
|
243
260
|
}
|
244
261
|
}
|
245
|
-
// Add/remove the contents of this
|
262
|
+
// Add/remove the contents of this seleciton from the editor.
|
246
263
|
// Used to prevent previewed content from looking like duplicate content
|
247
264
|
// while dragging.
|
248
265
|
//
|
@@ -250,6 +267,9 @@ class Selection {
|
|
250
267
|
// the editor image is likely to be slow.)
|
251
268
|
//
|
252
269
|
// If removed from the image, selected elements are drawn as wet ink.
|
270
|
+
//
|
271
|
+
// [inImage] should be `true` if the selected elements should be added to the
|
272
|
+
// main image, `false` if they should be removed.
|
253
273
|
addRemoveSelectionFromImage(inImage) {
|
254
274
|
// Don't hide elements if doing so will be slow.
|
255
275
|
if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
|
@@ -292,17 +312,18 @@ class Selection {
|
|
292
312
|
document.getSelection()?.removeAllRanges();
|
293
313
|
this.targetHandle = null;
|
294
314
|
let result = false;
|
315
|
+
this.backgroundDragging = false;
|
316
|
+
if (this.region.containsPoint(pointer.canvasPos)) {
|
317
|
+
this.backgroundDragging = true;
|
318
|
+
result = true;
|
319
|
+
}
|
295
320
|
for (const handle of this.handles) {
|
296
321
|
if (handle.containsPoint(pointer.canvasPos)) {
|
297
322
|
this.targetHandle = handle;
|
323
|
+
this.backgroundDragging = false;
|
298
324
|
result = true;
|
299
325
|
}
|
300
326
|
}
|
301
|
-
this.backgroundDragging = false;
|
302
|
-
if (this.region.containsPoint(pointer.canvasPos)) {
|
303
|
-
this.backgroundDragging = true;
|
304
|
-
result = true;
|
305
|
-
}
|
306
327
|
if (result) {
|
307
328
|
this.removeDeletedElemsFromSelection();
|
308
329
|
this.addRemoveSelectionFromImage(false);
|
@@ -340,23 +361,29 @@ class Selection {
|
|
340
361
|
this.targetHandle = null;
|
341
362
|
this.setTransform(Mat33.identity);
|
342
363
|
this.addRemoveSelectionFromImage(true);
|
364
|
+
this.updateUI();
|
343
365
|
}
|
344
366
|
// Scroll the viewport to this. Does not zoom
|
345
|
-
|
367
|
+
scrollTo() {
|
346
368
|
if (this.selectedElems.length === 0) {
|
347
|
-
return;
|
369
|
+
return false;
|
348
370
|
}
|
349
|
-
const
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
const
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
this.
|
371
|
+
const screenSize = this.editor.viewport.getScreenRectSize();
|
372
|
+
const screenRect = new Rect2(0, 0, screenSize.x, screenSize.y);
|
373
|
+
const selectionScreenRegion = this.getScreenRegion();
|
374
|
+
if (!screenRect.containsPoint(selectionScreenRegion.center)) {
|
375
|
+
const targetPointScreen = selectionScreenRegion.center;
|
376
|
+
const closestPointScreen = screenRect.getClosestPointOnBoundaryTo(targetPointScreen);
|
377
|
+
const closestPointCanvas = this.editor.viewport.screenToCanvas(closestPointScreen);
|
378
|
+
const targetPointCanvas = this.region.center;
|
379
|
+
const delta = closestPointCanvas.minus(targetPointCanvas);
|
380
|
+
this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(0.5))), false);
|
381
|
+
this.editor.queueRerender().then(() => {
|
382
|
+
this.previewTransformCmds();
|
383
|
+
});
|
384
|
+
return true;
|
359
385
|
}
|
386
|
+
return false;
|
360
387
|
}
|
361
388
|
deleteSelectedObjects() {
|
362
389
|
if (this.backgroundDragging || this.targetHandle) {
|
@@ -401,10 +428,10 @@ class Selection {
|
|
401
428
|
return duplicateCommand;
|
402
429
|
}
|
403
430
|
addTo(elem) {
|
404
|
-
if (this.
|
405
|
-
this.
|
431
|
+
if (this.outerContainer.parentElement) {
|
432
|
+
this.outerContainer.remove();
|
406
433
|
}
|
407
|
-
elem.appendChild(this.
|
434
|
+
elem.appendChild(this.outerContainer);
|
408
435
|
this.hasParent = true;
|
409
436
|
}
|
410
437
|
setToPoint(point) {
|
@@ -413,8 +440,8 @@ class Selection {
|
|
413
440
|
this.updateUI();
|
414
441
|
}
|
415
442
|
cancelSelection() {
|
416
|
-
if (this.
|
417
|
-
this.
|
443
|
+
if (this.outerContainer.parentElement) {
|
444
|
+
this.outerContainer.remove();
|
418
445
|
}
|
419
446
|
this.originalRegion = Rect2.empty;
|
420
447
|
this.selectionTightBoundingBox = null;
|
@@ -16,7 +16,7 @@ export interface HandlePresentation {
|
|
16
16
|
export declare const handleSize = 30;
|
17
17
|
export type DragStartCallback = (startPoint: Point2) => void;
|
18
18
|
export type DragUpdateCallback = (canvasPoint: Point2) => void;
|
19
|
-
export type DragEndCallback = () => void;
|
19
|
+
export type DragEndCallback = () => Promise<void> | void;
|
20
20
|
export default class SelectionHandle {
|
21
21
|
readonly presentation: HandlePresentation;
|
22
22
|
private readonly parent;
|
@@ -50,7 +50,7 @@ export default class SelectionHandle {
|
|
50
50
|
private dragLastPos;
|
51
51
|
handleDragStart(pointer: Pointer): void;
|
52
52
|
handleDragUpdate(pointer: Pointer): void;
|
53
|
-
handleDragEnd(): void
|
53
|
+
handleDragEnd(): void | Promise<void>;
|
54
54
|
setSnapToGrid(snap: boolean): void;
|
55
55
|
isSnappingToGrid(): boolean;
|
56
56
|
}
|
@@ -13,6 +13,7 @@ export var HandleAction;
|
|
13
13
|
HandleAction["ResizeX"] = "resize-x";
|
14
14
|
HandleAction["ResizeY"] = "resize-y";
|
15
15
|
})(HandleAction || (HandleAction = {}));
|
16
|
+
// The *interactable* handle size. The visual size will be slightly smaller.
|
16
17
|
export const handleSize = 30;
|
17
18
|
export default class SelectionHandle {
|
18
19
|
constructor(presentation, parent, viewport, onDragStart, onDragUpdate, onDragEnd) {
|
@@ -25,10 +26,14 @@ export default class SelectionHandle {
|
|
25
26
|
this.dragLastPos = null;
|
26
27
|
this.element = document.createElement('div');
|
27
28
|
this.element.classList.add(`${cssPrefix}handle`, `${cssPrefix}${presentation.action}`);
|
29
|
+
// Create a slightly smaller content/background element.
|
30
|
+
const visibleContent = document.createElement('div');
|
31
|
+
visibleContent.classList.add(`${cssPrefix}content`);
|
32
|
+
this.element.appendChild(visibleContent);
|
28
33
|
this.parentSide = presentation.side;
|
29
34
|
const icon = presentation.icon;
|
30
35
|
if (icon) {
|
31
|
-
|
36
|
+
visibleContent.appendChild(icon);
|
32
37
|
icon.classList.add('icon');
|
33
38
|
}
|
34
39
|
if (presentation.action === HandleAction.Rotate) {
|
@@ -61,7 +66,7 @@ export default class SelectionHandle {
|
|
61
66
|
* selection box.
|
62
67
|
*/
|
63
68
|
getBBoxParentCoords() {
|
64
|
-
const parentRect = this.parent.
|
69
|
+
const parentRect = this.parent.getScreenRegion();
|
65
70
|
const size = Vec2.of(handleSize, handleSize);
|
66
71
|
const topLeft = parentRect.size.scale(this.parentSide)
|
67
72
|
// Center
|
@@ -115,7 +120,7 @@ export default class SelectionHandle {
|
|
115
120
|
if (!this.dragLastPos) {
|
116
121
|
return;
|
117
122
|
}
|
118
|
-
this.onDragEnd();
|
123
|
+
return this.onDragEnd();
|
119
124
|
}
|
120
125
|
setSnapToGrid(snap) {
|
121
126
|
this.snapToGrid = snap;
|
@@ -13,13 +13,15 @@ export default class SelectionTool extends BaseTool {
|
|
13
13
|
private expandingSelectionBox;
|
14
14
|
private shiftKeyPressed;
|
15
15
|
private snapToGrid;
|
16
|
+
private lastPointer;
|
17
|
+
private autoscroller;
|
16
18
|
constructor(editor: Editor, description: string);
|
17
19
|
private makeSelectionBox;
|
18
20
|
private snapSelectionToGrid;
|
19
21
|
private selectionBoxHandlingEvt;
|
20
22
|
onPointerDown({ allPointers, current }: PointerEvt): boolean;
|
21
23
|
onPointerMove(event: PointerEvt): void;
|
22
|
-
private
|
24
|
+
private onMainPointerUpdated;
|
23
25
|
onPointerUp(event: PointerEvt): void;
|
24
26
|
onGestureCancel(): void;
|
25
27
|
private lastSelectedObjects;
|
@@ -6,6 +6,7 @@ import SVGRenderer from '../../rendering/renderers/SVGRenderer.mjs';
|
|
6
6
|
import Selection from './Selection.mjs';
|
7
7
|
import TextComponent from '../../components/TextComponent.mjs';
|
8
8
|
import { duplicateSelectionShortcut, selectAllKeyboardShortcut, snapToGridKeyboardShortcutId } from '../keybindings.mjs';
|
9
|
+
import ToPointerAutoscroller from './ToPointerAutoscroller.mjs';
|
9
10
|
export const cssPrefix = 'selection-tool-';
|
10
11
|
// Allows users to select/transform portions of the `EditorImage`.
|
11
12
|
// With respect to `extend`ing, `SelectionTool` is not stable.
|
@@ -17,8 +18,19 @@ class SelectionTool extends BaseTool {
|
|
17
18
|
this.expandingSelectionBox = false;
|
18
19
|
this.shiftKeyPressed = false;
|
19
20
|
this.snapToGrid = false;
|
21
|
+
this.lastPointer = null;
|
20
22
|
this.selectionBoxHandlingEvt = false;
|
21
23
|
this.lastSelectedObjects = [];
|
24
|
+
this.autoscroller = new ToPointerAutoscroller(editor.viewport, (scrollBy) => {
|
25
|
+
editor.dispatch(Viewport.transformBy(Mat33.translation(scrollBy)), false);
|
26
|
+
// Update the selection box/content to match the new viewport.
|
27
|
+
if (this.lastPointer) {
|
28
|
+
// The viewport has changed -- ensure that the screen and canvas positions
|
29
|
+
// of the pointer are both correct
|
30
|
+
const updatedPointer = this.lastPointer.withScreenPosition(this.lastPointer.screenPos, editor.viewport);
|
31
|
+
this.onMainPointerUpdated(updatedPointer);
|
32
|
+
}
|
33
|
+
});
|
22
34
|
this.handleOverlay = document.createElement('div');
|
23
35
|
editor.createHTMLOverlay(this.handleOverlay);
|
24
36
|
this.handleOverlay.style.display = 'none';
|
@@ -81,14 +93,22 @@ class SelectionTool extends BaseTool {
|
|
81
93
|
this.expandingSelectionBox = this.shiftKeyPressed;
|
82
94
|
this.makeSelectionBox(current.canvasPos);
|
83
95
|
}
|
96
|
+
else {
|
97
|
+
// Only autoscroll if we're transforming an existing selection
|
98
|
+
this.autoscroller.start();
|
99
|
+
}
|
84
100
|
return true;
|
85
101
|
}
|
86
102
|
return false;
|
87
103
|
}
|
88
104
|
onPointerMove(event) {
|
105
|
+
this.onMainPointerUpdated(event.current);
|
106
|
+
}
|
107
|
+
onMainPointerUpdated(currentPointer) {
|
108
|
+
this.lastPointer = currentPointer;
|
89
109
|
if (!this.selectionBox)
|
90
110
|
return;
|
91
|
-
|
111
|
+
this.autoscroller.onPointerMove(currentPointer.screenPos);
|
92
112
|
if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
|
93
113
|
const screenPos = this.editor.viewport.canvasToScreen(this.startPoint);
|
94
114
|
currentPointer = currentPointer.lockedToXYAxesScreen(screenPos, this.editor.viewport);
|
@@ -103,21 +123,8 @@ class SelectionTool extends BaseTool {
|
|
103
123
|
this.selectionBox.setToPoint(currentPointer.canvasPos);
|
104
124
|
}
|
105
125
|
}
|
106
|
-
// Called after a gestureCancel and a pointerUp
|
107
|
-
onGestureEnd() {
|
108
|
-
if (!this.selectionBox)
|
109
|
-
return;
|
110
|
-
if (!this.selectionBoxHandlingEvt) {
|
111
|
-
// Expand/shrink the selection rectangle, if applicable
|
112
|
-
this.selectionBox.resolveToObjects();
|
113
|
-
this.onSelectionUpdated();
|
114
|
-
}
|
115
|
-
else {
|
116
|
-
this.selectionBox.onDragEnd();
|
117
|
-
}
|
118
|
-
this.selectionBoxHandlingEvt = false;
|
119
|
-
}
|
120
126
|
onPointerUp(event) {
|
127
|
+
this.autoscroller.stop();
|
121
128
|
if (!this.selectionBox)
|
122
129
|
return;
|
123
130
|
let currentPointer = event.current;
|
@@ -136,10 +143,20 @@ class SelectionTool extends BaseTool {
|
|
136
143
|
]);
|
137
144
|
}
|
138
145
|
else {
|
139
|
-
this.
|
146
|
+
if (!this.selectionBoxHandlingEvt) {
|
147
|
+
// Expand/shrink the selection rectangle, if applicable
|
148
|
+
this.selectionBox.resolveToObjects();
|
149
|
+
this.onSelectionUpdated();
|
150
|
+
}
|
151
|
+
else {
|
152
|
+
this.selectionBox.onDragEnd();
|
153
|
+
}
|
154
|
+
this.selectionBoxHandlingEvt = false;
|
155
|
+
this.lastPointer = null;
|
140
156
|
}
|
141
157
|
}
|
142
158
|
onGestureCancel() {
|
159
|
+
this.autoscroller.stop();
|
143
160
|
if (this.selectionBoxHandlingEvt) {
|
144
161
|
this.selectionBox?.onDragCancel();
|
145
162
|
}
|
@@ -152,6 +169,8 @@ class SelectionTool extends BaseTool {
|
|
152
169
|
this.prevSelectionBox = null;
|
153
170
|
}
|
154
171
|
this.expandingSelectionBox = false;
|
172
|
+
this.lastPointer = null;
|
173
|
+
this.selectionBoxHandlingEvt = false;
|
155
174
|
}
|
156
175
|
onSelectionUpdated() {
|
157
176
|
const selectedItemCount = this.selectionBox?.getSelectedItemCount() ?? 0;
|
@@ -277,6 +296,7 @@ class SelectionTool extends BaseTool {
|
|
277
296
|
const transform = Mat33.scaling2D(scaleFactor, this.editor.viewport.roundPoint(region.topLeft)).rightMul(Mat33.translation(regionCenter).rightMul(roundedRotationMatrix).rightMul(Mat33.translation(regionCenter.times(-1)))).rightMul(Mat33.translation(this.editor.viewport.roundPoint(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))));
|
278
297
|
const oldTransform = this.selectionBox.getTransform();
|
279
298
|
this.selectionBox.setTransform(oldTransform.rightMul(transform));
|
299
|
+
this.selectionBox.scrollTo();
|
280
300
|
}
|
281
301
|
if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
|
282
302
|
this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { Point2, Vec2 } from '@js-draw/math';
|
2
|
+
import Viewport from '../../Viewport';
|
3
|
+
type ScrollByCallback = (delta: Vec2) => void;
|
4
|
+
/**
|
5
|
+
* Automatically scrolls the viewport such that the user's pointer is visible.
|
6
|
+
*/
|
7
|
+
export default class ToPointerAutoscroller {
|
8
|
+
private viewport;
|
9
|
+
private scrollByCanvasDelta;
|
10
|
+
private started;
|
11
|
+
private updateLoopId;
|
12
|
+
private updateLoopRunning;
|
13
|
+
private targetPoint;
|
14
|
+
private scrollRate;
|
15
|
+
constructor(viewport: Viewport, scrollByCanvasDelta: ScrollByCallback);
|
16
|
+
private getScrollForPoint;
|
17
|
+
start(): void;
|
18
|
+
onPointerMove(pointerScreenPosition: Point2): void;
|
19
|
+
stop(): void;
|
20
|
+
private startUpdateLoop;
|
21
|
+
private stopUpdateLoop;
|
22
|
+
}
|
23
|
+
export {};
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import { Rect2, Vec2 } from '@js-draw/math';
|
2
|
+
import untilNextAnimationFrame from '../../util/untilNextAnimationFrame.mjs';
|
3
|
+
/**
|
4
|
+
* Automatically scrolls the viewport such that the user's pointer is visible.
|
5
|
+
*/
|
6
|
+
export default class ToPointerAutoscroller {
|
7
|
+
constructor(viewport, scrollByCanvasDelta) {
|
8
|
+
this.viewport = viewport;
|
9
|
+
this.scrollByCanvasDelta = scrollByCanvasDelta;
|
10
|
+
this.started = false;
|
11
|
+
this.updateLoopId = 0;
|
12
|
+
this.updateLoopRunning = false;
|
13
|
+
this.targetPoint = null;
|
14
|
+
this.scrollRate = 1000; // px/s
|
15
|
+
}
|
16
|
+
getScrollForPoint(screenPoint) {
|
17
|
+
const screenSize = this.viewport.getScreenRectSize();
|
18
|
+
const screenRect = new Rect2(0, 0, screenSize.x, screenSize.y);
|
19
|
+
// Starts autoscrolling when the cursor is **outside of** this region
|
20
|
+
const marginSize = 44;
|
21
|
+
const autoscrollBoundary = screenRect.grownBy(-marginSize);
|
22
|
+
if (autoscrollBoundary.containsPoint(screenPoint)) {
|
23
|
+
return Vec2.zero;
|
24
|
+
}
|
25
|
+
const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
|
26
|
+
const distToEdge = closestEdgePoint.minus(screenPoint).magnitude();
|
27
|
+
const toEdge = closestEdgePoint.minus(screenPoint);
|
28
|
+
// Go faster for points further away from the boundary.
|
29
|
+
const maximumScaleFactor = 1.25;
|
30
|
+
const scaleFactor = Math.min(distToEdge / marginSize, maximumScaleFactor);
|
31
|
+
return toEdge.normalizedOrZero().times(scaleFactor);
|
32
|
+
}
|
33
|
+
start() {
|
34
|
+
this.started = true;
|
35
|
+
}
|
36
|
+
onPointerMove(pointerScreenPosition) {
|
37
|
+
if (!this.started) {
|
38
|
+
return;
|
39
|
+
}
|
40
|
+
if (this.getScrollForPoint(pointerScreenPosition) === Vec2.zero) {
|
41
|
+
this.stopUpdateLoop();
|
42
|
+
}
|
43
|
+
else {
|
44
|
+
this.targetPoint = pointerScreenPosition;
|
45
|
+
this.startUpdateLoop();
|
46
|
+
}
|
47
|
+
}
|
48
|
+
stop() {
|
49
|
+
this.targetPoint = null;
|
50
|
+
this.started = false;
|
51
|
+
this.stopUpdateLoop();
|
52
|
+
}
|
53
|
+
startUpdateLoop() {
|
54
|
+
if (this.updateLoopRunning) {
|
55
|
+
return;
|
56
|
+
}
|
57
|
+
(async () => {
|
58
|
+
this.updateLoopId++;
|
59
|
+
const currentUpdateLoopId = this.updateLoopId;
|
60
|
+
let lastUpdateTime = performance.now();
|
61
|
+
while (this.updateLoopId === currentUpdateLoopId && this.targetPoint) {
|
62
|
+
this.updateLoopRunning = true;
|
63
|
+
const currentTime = performance.now();
|
64
|
+
const deltaTimeMs = currentTime - lastUpdateTime;
|
65
|
+
const scrollDirection = this.getScrollForPoint(this.targetPoint);
|
66
|
+
const screenScrollAmount = scrollDirection.times(this.scrollRate * deltaTimeMs / 1000);
|
67
|
+
this.scrollByCanvasDelta(this.viewport.screenToCanvasTransform.transformVec3(screenScrollAmount));
|
68
|
+
lastUpdateTime = currentTime;
|
69
|
+
await untilNextAnimationFrame();
|
70
|
+
}
|
71
|
+
this.updateLoopRunning = false;
|
72
|
+
})();
|
73
|
+
}
|
74
|
+
stopUpdateLoop() {
|
75
|
+
this.updateLoopId++;
|
76
|
+
}
|
77
|
+
}
|
@@ -9,26 +9,33 @@ export declare class DragTransformer {
|
|
9
9
|
constructor(editor: Editor, selection: Selection);
|
10
10
|
onDragStart(startPoint: Vec3): void;
|
11
11
|
onDragUpdate(canvasPos: Vec3): void;
|
12
|
-
onDragEnd(): void
|
12
|
+
onDragEnd(): void | Promise<void>;
|
13
13
|
}
|
14
14
|
export declare class ResizeTransformer {
|
15
15
|
private readonly editor;
|
16
16
|
private selection;
|
17
17
|
private mode;
|
18
18
|
private dragStartPoint;
|
19
|
+
private transformOrigin;
|
20
|
+
private scaleRate;
|
19
21
|
constructor(editor: Editor, selection: Selection);
|
20
22
|
onDragStart(startPoint: Vec3, mode: ResizeMode): void;
|
23
|
+
private computeOriginAndScaleRate;
|
21
24
|
onDragUpdate(canvasPos: Vec3): void;
|
22
|
-
onDragEnd(): void
|
25
|
+
onDragEnd(): void | Promise<void>;
|
23
26
|
}
|
24
27
|
export declare class RotateTransformer {
|
25
28
|
private readonly editor;
|
26
29
|
private selection;
|
27
30
|
private startAngle;
|
31
|
+
private targetRotation;
|
32
|
+
private maximumDistFromStart;
|
33
|
+
private startPoint;
|
28
34
|
constructor(editor: Editor, selection: Selection);
|
29
35
|
private getAngle;
|
30
36
|
private roundAngle;
|
31
37
|
onDragStart(startPoint: Vec3): void;
|
38
|
+
private setRotationTo;
|
32
39
|
onDragUpdate(canvasPos: Vec3): void;
|
33
|
-
onDragEnd(): void
|
40
|
+
onDragEnd(): void | Promise<void>;
|
34
41
|
}
|