js-draw 1.6.0 → 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 +4 -6
- 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/image/EditorImage.js +8 -8
- 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/toolbar/widgets/BaseWidget.js +3 -3
- package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
- package/dist/cjs/tools/SelectionTool/Selection.js +81 -52
- 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/image/EditorImage.mjs +8 -8
- 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/toolbar/widgets/BaseWidget.mjs +3 -3
- package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
- package/dist/mjs/tools/SelectionTool/Selection.mjs +81 -52
- 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/docs/img/readme-images/js-draw.png +0 -0
- package/package.json +6 -6
- package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -26,11 +26,26 @@ const visualEquivalent = (renderablePath, visibleRect) => {
|
|
26
26
|
const path = (0, exports.pathFromRenderable)(renderablePath);
|
27
27
|
const strokeWidth = renderablePath.style.stroke?.width ?? 0;
|
28
28
|
const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
|
29
|
+
const styledPathBBox = path.bbox.grownBy(strokeWidth);
|
30
|
+
// Are we close enough to the path that it fills the entire screen?
|
31
|
+
if (onlyStroked
|
32
|
+
&& renderablePath.style.stroke
|
33
|
+
&& strokeWidth > visibleRect.maxDimension
|
34
|
+
&& styledPathBBox.containsRect(visibleRect)) {
|
35
|
+
const strokeRadius = strokeWidth / 2;
|
36
|
+
// Do a fast, but with many false negatives, check.
|
37
|
+
for (const point of path.startEndPoints()) {
|
38
|
+
// If within the strokeRadius of any point
|
39
|
+
if (visibleRect.isWithinRadiusOf(strokeRadius, point)) {
|
40
|
+
return (0, exports.pathToRenderable)(math_1.Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color });
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
29
44
|
// Scale the expanded rect --- the visual equivalent is only close for huge strokes.
|
30
45
|
const expandedRect = visibleRect.grownBy(strokeWidth)
|
31
46
|
.transformedBoundingBox(math_1.Mat33.scaling2D(4, visibleRect.center));
|
32
47
|
// TODO: Handle simplifying very small paths.
|
33
|
-
if (expandedRect.containsRect(
|
48
|
+
if (expandedRect.containsRect(styledPathBBox)) {
|
34
49
|
return renderablePath;
|
35
50
|
}
|
36
51
|
const parts = [];
|
@@ -46,5 +46,23 @@ class CacheRecordManager {
|
|
46
46
|
this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
|
47
47
|
return this.cacheRecords[0];
|
48
48
|
}
|
49
|
+
// Returns information to (hopefully) help debug performance issues
|
50
|
+
getDebugInfo() {
|
51
|
+
let numberAllocd = 0;
|
52
|
+
let averageReassignedCount = 0;
|
53
|
+
for (const cacheRecord of this.cacheRecords) {
|
54
|
+
averageReassignedCount += cacheRecord.allocCount;
|
55
|
+
if (cacheRecord.isAllocd()) {
|
56
|
+
numberAllocd++;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
averageReassignedCount /= Math.max(this.cacheRecords.length, 0);
|
60
|
+
const debugInfo = [
|
61
|
+
`${this.cacheRecords.length} cache records (max ${this.maxCanvases})`,
|
62
|
+
`${numberAllocd} assigned to screen regions`,
|
63
|
+
`Average number of times reassigned: ${Math.round(averageReassignedCount * 100) / 100}`,
|
64
|
+
];
|
65
|
+
return debugInfo.join('\n');
|
66
|
+
}
|
49
67
|
}
|
50
68
|
exports.CacheRecordManager = CacheRecordManager;
|
@@ -149,9 +149,10 @@ class CanvasRenderer extends AbstractRenderer_1.default {
|
|
149
149
|
return;
|
150
150
|
}
|
151
151
|
// If part of a huge object, it might be worth trimming the path
|
152
|
-
|
152
|
+
const visibleRect = this.getViewport().visibleRect;
|
153
|
+
if (this.currentObjectBBox?.containsRect(visibleRect)) {
|
153
154
|
// Try to trim/remove parts of the path outside of the bounding box.
|
154
|
-
path = (0, RenderablePathSpec_1.visualEquivalent)(path,
|
155
|
+
path = (0, RenderablePathSpec_1.visualEquivalent)(path, visibleRect);
|
155
156
|
}
|
156
157
|
super.drawPath(path);
|
157
158
|
}
|
@@ -13,7 +13,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
13
13
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
14
14
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
15
15
|
};
|
16
|
-
var _BaseWidget_instances, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
|
16
|
+
var _BaseWidget_instances, _a, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
18
18
|
exports.ToolbarWidgetTag = void 0;
|
19
19
|
const ToolbarShortcutHandler_1 = __importDefault(require("../../tools/ToolbarShortcutHandler"));
|
@@ -428,13 +428,13 @@ class BaseWidget {
|
|
428
428
|
}
|
429
429
|
}
|
430
430
|
}
|
431
|
-
_BaseWidget_hasDropdown = new WeakMap(), _BaseWidget_disabledDueToReadOnlyEditor = new WeakMap(), _BaseWidget_tags = new WeakMap(), _BaseWidget_removeEditorListeners = new WeakMap(), _BaseWidget_instances = new WeakSet(), _BaseWidget_addEditorListeners = function _BaseWidget_addEditorListeners() {
|
431
|
+
_a = BaseWidget, _BaseWidget_hasDropdown = new WeakMap(), _BaseWidget_disabledDueToReadOnlyEditor = new WeakMap(), _BaseWidget_tags = new WeakMap(), _BaseWidget_removeEditorListeners = new WeakMap(), _BaseWidget_instances = new WeakSet(), _BaseWidget_addEditorListeners = function _BaseWidget_addEditorListeners() {
|
432
432
|
__classPrivateFieldGet(this, _BaseWidget_removeEditorListeners, "f")?.call(this);
|
433
433
|
const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler_1.default);
|
434
434
|
let removeKeyPressListener = null;
|
435
435
|
// If the onKeyPress function has been extended and the editor is configured to send keypress events to
|
436
436
|
// toolbar widgets,
|
437
|
-
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !==
|
437
|
+
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== _a.prototype.onKeyPress) {
|
438
438
|
const keyPressListener = (event) => this.onKeyPress(event);
|
439
439
|
const handler = toolbarShortcutHandlers[0];
|
440
440
|
handler.registerListener(keyPressListener);
|
@@ -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;
|
@@ -64,22 +64,34 @@ class Selection {
|
|
64
64
|
resize: new TransformMode_1.ResizeTransformer(editor, this),
|
65
65
|
rotate: new TransformMode_1.RotateTransformer(editor, this),
|
66
66
|
};
|
67
|
-
|
67
|
+
// We need two containers for some CSS to apply (the outer container
|
68
|
+
// needs zero height, the inner needs to prevent the selection background
|
69
|
+
// from being visible outside of the editor).
|
70
|
+
this.outerContainer = document.createElement('div');
|
71
|
+
this.outerContainer.classList.add(`${SelectionTool_1.cssPrefix}selection-outer-container`);
|
72
|
+
this.innerContainer = document.createElement('div');
|
73
|
+
this.innerContainer.classList.add(`${SelectionTool_1.cssPrefix}selection-inner-container`);
|
68
74
|
this.backgroundElem = document.createElement('div');
|
69
75
|
this.backgroundElem.classList.add(`${SelectionTool_1.cssPrefix}selection-background`);
|
70
|
-
this.
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
}
|
76
|
+
this.innerContainer.appendChild(this.backgroundElem);
|
77
|
+
this.outerContainer.appendChild(this.innerContainer);
|
78
|
+
const makeResizeHandle = (mode, side) => {
|
79
|
+
const modeToAction = {
|
80
|
+
[types_1.ResizeMode.Both]: SelectionHandle_1.HandleAction.ResizeXY,
|
81
|
+
[types_1.ResizeMode.HorizontalOnly]: SelectionHandle_1.HandleAction.ResizeX,
|
82
|
+
[types_1.ResizeMode.VerticalOnly]: SelectionHandle_1.HandleAction.ResizeY,
|
83
|
+
};
|
84
|
+
return new SelectionHandle_1.default({
|
85
|
+
action: modeToAction[mode],
|
86
|
+
side,
|
87
|
+
}, this, this.editor.viewport, (startPoint) => this.transformers.resize.onDragStart(startPoint, mode), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
|
88
|
+
};
|
89
|
+
const resizeHorizontalHandles = [
|
90
|
+
makeResizeHandle(types_1.ResizeMode.HorizontalOnly, math_1.Vec2.of(0, 0.5)),
|
91
|
+
makeResizeHandle(types_1.ResizeMode.HorizontalOnly, math_1.Vec2.of(1, 0.5)),
|
92
|
+
];
|
93
|
+
const resizeVerticalHandle = makeResizeHandle(types_1.ResizeMode.VerticalOnly, math_1.Vec2.of(0.5, 1));
|
94
|
+
const resizeBothHandle = makeResizeHandle(types_1.ResizeMode.Both, math_1.Vec2.of(1, 1));
|
83
95
|
const rotationHandle = new SelectionHandle_1.default({
|
84
96
|
action: SelectionHandle_1.HandleAction.Rotate,
|
85
97
|
side: math_1.Vec2.of(0.5, 0),
|
@@ -87,7 +99,7 @@ class Selection {
|
|
87
99
|
}, this, this.editor.viewport, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
|
88
100
|
this.handles = [
|
89
101
|
resizeBothHandle,
|
90
|
-
|
102
|
+
...resizeHorizontalHandles,
|
91
103
|
resizeVerticalHandle,
|
92
104
|
rotationHandle,
|
93
105
|
];
|
@@ -133,7 +145,7 @@ class Selection {
|
|
133
145
|
get preTransformedScreenRegionRotation() {
|
134
146
|
return this.editor.viewport.getRotationAngle();
|
135
147
|
}
|
136
|
-
|
148
|
+
getScreenRegion() {
|
137
149
|
const toScreen = this.editor.viewport.canvasToScreenTransform;
|
138
150
|
const scaleFactor = this.editor.viewport.getScaleFactor();
|
139
151
|
const screenCenter = toScreen.transformVec2(this.region.center);
|
@@ -146,27 +158,33 @@ class Selection {
|
|
146
158
|
setTransform(transform, preview = true) {
|
147
159
|
this.transform = transform;
|
148
160
|
if (preview && this.hasParent) {
|
149
|
-
this.scrollTo();
|
150
161
|
this.previewTransformCmds();
|
151
162
|
}
|
152
163
|
}
|
153
164
|
// Applies the current transformation to the selection
|
154
|
-
|
165
|
+
finalizeTransform() {
|
155
166
|
const fullTransform = this.transform;
|
156
167
|
const selectedElems = this.selectedElems;
|
157
168
|
// Reset for the next drag
|
158
169
|
this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
|
159
170
|
this.transform = math_1.Mat33.identity;
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
171
|
+
this.scrollTo();
|
172
|
+
// Make the commands undo-able.
|
173
|
+
// Don't check for non-empty transforms because this breaks changing the
|
174
|
+
// 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));
|
164
178
|
// Clear renderings of any in-progress transformations
|
165
179
|
const wetInkRenderer = this.editor.display.getWetInkRenderer();
|
166
180
|
wetInkRenderer.clear();
|
181
|
+
return transformPromise;
|
167
182
|
}
|
168
183
|
// Preview the effects of the current transformation on the selection
|
169
184
|
previewTransformCmds() {
|
185
|
+
if (this.selectedElems.length === 0) {
|
186
|
+
return;
|
187
|
+
}
|
170
188
|
// Don't render what we're moving if it's likely to be slow.
|
171
189
|
if (this.selectedElems.length > maxPreviewElemCount) {
|
172
190
|
this.updateUI();
|
@@ -247,28 +265,29 @@ class Selection {
|
|
247
265
|
if (!this.hasParent) {
|
248
266
|
return;
|
249
267
|
}
|
268
|
+
const screenRegion = this.getScreenRegion();
|
250
269
|
// marginLeft, marginTop: Display relative to the top left of the selection overlay.
|
251
270
|
// left, top don't work for this.
|
252
|
-
this.backgroundElem.style.marginLeft = `${
|
253
|
-
this.backgroundElem.style.marginTop = `${
|
254
|
-
this.backgroundElem.style.width = `${
|
255
|
-
this.backgroundElem.style.height = `${
|
271
|
+
this.backgroundElem.style.marginLeft = `${screenRegion.topLeft.x}px`;
|
272
|
+
this.backgroundElem.style.marginTop = `${screenRegion.topLeft.y}px`;
|
273
|
+
this.backgroundElem.style.width = `${screenRegion.width}px`;
|
274
|
+
this.backgroundElem.style.height = `${screenRegion.height}px`;
|
256
275
|
const rotationDeg = this.screenRegionRotation * 180 / Math.PI;
|
257
276
|
this.backgroundElem.style.transform = `rotate(${rotationDeg}deg)`;
|
258
277
|
this.backgroundElem.style.transformOrigin = 'center';
|
259
278
|
// If closer to perpendicular, apply different CSS
|
260
279
|
const perpendicularClassName = `${SelectionTool_1.cssPrefix}rotated-near-perpendicular`;
|
261
280
|
if (Math.abs(Math.sin(this.screenRegionRotation)) > 0.5) {
|
262
|
-
this.
|
281
|
+
this.innerContainer.classList.add(perpendicularClassName);
|
263
282
|
}
|
264
283
|
else {
|
265
|
-
this.
|
284
|
+
this.innerContainer.classList.remove(perpendicularClassName);
|
266
285
|
}
|
267
286
|
for (const handle of this.handles) {
|
268
287
|
handle.updatePosition();
|
269
288
|
}
|
270
289
|
}
|
271
|
-
// Add/remove the contents of this
|
290
|
+
// Add/remove the contents of this seleciton from the editor.
|
272
291
|
// Used to prevent previewed content from looking like duplicate content
|
273
292
|
// while dragging.
|
274
293
|
//
|
@@ -276,6 +295,9 @@ class Selection {
|
|
276
295
|
// the editor image is likely to be slow.)
|
277
296
|
//
|
278
297
|
// If removed from the image, selected elements are drawn as wet ink.
|
298
|
+
//
|
299
|
+
// [inImage] should be `true` if the selected elements should be added to the
|
300
|
+
// main image, `false` if they should be removed.
|
279
301
|
addRemoveSelectionFromImage(inImage) {
|
280
302
|
// Don't hide elements if doing so will be slow.
|
281
303
|
if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
|
@@ -318,17 +340,18 @@ class Selection {
|
|
318
340
|
document.getSelection()?.removeAllRanges();
|
319
341
|
this.targetHandle = null;
|
320
342
|
let result = false;
|
343
|
+
this.backgroundDragging = false;
|
344
|
+
if (this.region.containsPoint(pointer.canvasPos)) {
|
345
|
+
this.backgroundDragging = true;
|
346
|
+
result = true;
|
347
|
+
}
|
321
348
|
for (const handle of this.handles) {
|
322
349
|
if (handle.containsPoint(pointer.canvasPos)) {
|
323
350
|
this.targetHandle = handle;
|
351
|
+
this.backgroundDragging = false;
|
324
352
|
result = true;
|
325
353
|
}
|
326
354
|
}
|
327
|
-
this.backgroundDragging = false;
|
328
|
-
if (this.region.containsPoint(pointer.canvasPos)) {
|
329
|
-
this.backgroundDragging = true;
|
330
|
-
result = true;
|
331
|
-
}
|
332
355
|
if (result) {
|
333
356
|
this.removeDeletedElemsFromSelection();
|
334
357
|
this.addRemoveSelectionFromImage(false);
|
@@ -366,23 +389,29 @@ class Selection {
|
|
366
389
|
this.targetHandle = null;
|
367
390
|
this.setTransform(math_1.Mat33.identity);
|
368
391
|
this.addRemoveSelectionFromImage(true);
|
392
|
+
this.updateUI();
|
369
393
|
}
|
370
394
|
// Scroll the viewport to this. Does not zoom
|
371
|
-
|
395
|
+
scrollTo() {
|
372
396
|
if (this.selectedElems.length === 0) {
|
373
|
-
return;
|
397
|
+
return false;
|
374
398
|
}
|
375
|
-
const
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
const
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
this.
|
399
|
+
const screenSize = this.editor.viewport.getScreenRectSize();
|
400
|
+
const screenRect = new math_1.Rect2(0, 0, screenSize.x, screenSize.y);
|
401
|
+
const selectionScreenRegion = this.getScreenRegion();
|
402
|
+
if (!screenRect.containsPoint(selectionScreenRegion.center)) {
|
403
|
+
const targetPointScreen = selectionScreenRegion.center;
|
404
|
+
const closestPointScreen = screenRect.getClosestPointOnBoundaryTo(targetPointScreen);
|
405
|
+
const closestPointCanvas = this.editor.viewport.screenToCanvas(closestPointScreen);
|
406
|
+
const targetPointCanvas = this.region.center;
|
407
|
+
const delta = closestPointCanvas.minus(targetPointCanvas);
|
408
|
+
this.editor.dispatchNoAnnounce(Viewport_1.default.transformBy(math_1.Mat33.translation(delta.times(0.5))), false);
|
409
|
+
this.editor.queueRerender().then(() => {
|
410
|
+
this.previewTransformCmds();
|
411
|
+
});
|
412
|
+
return true;
|
385
413
|
}
|
414
|
+
return false;
|
386
415
|
}
|
387
416
|
deleteSelectedObjects() {
|
388
417
|
if (this.backgroundDragging || this.targetHandle) {
|
@@ -410,7 +439,7 @@ class Selection {
|
|
410
439
|
if (wasTransforming) {
|
411
440
|
// Don't update the selection's focus when redoing/undoing
|
412
441
|
const selectionToUpdate = null;
|
413
|
-
tmpApplyCommand = new
|
442
|
+
tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
|
414
443
|
// Transform to ensure that the duplicates are in the correct location
|
415
444
|
await tmpApplyCommand.apply(this.editor);
|
416
445
|
// Show items again
|
@@ -427,10 +456,10 @@ class Selection {
|
|
427
456
|
return duplicateCommand;
|
428
457
|
}
|
429
458
|
addTo(elem) {
|
430
|
-
if (this.
|
431
|
-
this.
|
459
|
+
if (this.outerContainer.parentElement) {
|
460
|
+
this.outerContainer.remove();
|
432
461
|
}
|
433
|
-
elem.appendChild(this.
|
462
|
+
elem.appendChild(this.outerContainer);
|
434
463
|
this.hasParent = true;
|
435
464
|
}
|
436
465
|
setToPoint(point) {
|
@@ -439,8 +468,8 @@ class Selection {
|
|
439
468
|
this.updateUI();
|
440
469
|
}
|
441
470
|
cancelSelection() {
|
442
|
-
if (this.
|
443
|
-
this.
|
471
|
+
if (this.outerContainer.parentElement) {
|
472
|
+
this.outerContainer.remove();
|
444
473
|
}
|
445
474
|
this.originalRegion = math_1.Rect2.empty;
|
446
475
|
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
|
}
|
@@ -16,6 +16,7 @@ var HandleAction;
|
|
16
16
|
HandleAction["ResizeX"] = "resize-x";
|
17
17
|
HandleAction["ResizeY"] = "resize-y";
|
18
18
|
})(HandleAction || (exports.HandleAction = HandleAction = {}));
|
19
|
+
// The *interactable* handle size. The visual size will be slightly smaller.
|
19
20
|
exports.handleSize = 30;
|
20
21
|
class SelectionHandle {
|
21
22
|
constructor(presentation, parent, viewport, onDragStart, onDragUpdate, onDragEnd) {
|
@@ -28,10 +29,14 @@ class SelectionHandle {
|
|
28
29
|
this.dragLastPos = null;
|
29
30
|
this.element = document.createElement('div');
|
30
31
|
this.element.classList.add(`${SelectionTool_1.cssPrefix}handle`, `${SelectionTool_1.cssPrefix}${presentation.action}`);
|
32
|
+
// Create a slightly smaller content/background element.
|
33
|
+
const visibleContent = document.createElement('div');
|
34
|
+
visibleContent.classList.add(`${SelectionTool_1.cssPrefix}content`);
|
35
|
+
this.element.appendChild(visibleContent);
|
31
36
|
this.parentSide = presentation.side;
|
32
37
|
const icon = presentation.icon;
|
33
38
|
if (icon) {
|
34
|
-
|
39
|
+
visibleContent.appendChild(icon);
|
35
40
|
icon.classList.add('icon');
|
36
41
|
}
|
37
42
|
if (presentation.action === HandleAction.Rotate) {
|
@@ -64,7 +69,7 @@ class SelectionHandle {
|
|
64
69
|
* selection box.
|
65
70
|
*/
|
66
71
|
getBBoxParentCoords() {
|
67
|
-
const parentRect = this.parent.
|
72
|
+
const parentRect = this.parent.getScreenRegion();
|
68
73
|
const size = math_1.Vec2.of(exports.handleSize, exports.handleSize);
|
69
74
|
const topLeft = parentRect.size.scale(this.parentSide)
|
70
75
|
// Center
|
@@ -118,7 +123,7 @@ class SelectionHandle {
|
|
118
123
|
if (!this.dragLastPos) {
|
119
124
|
return;
|
120
125
|
}
|
121
|
-
this.onDragEnd();
|
126
|
+
return this.onDragEnd();
|
122
127
|
}
|
123
128
|
setSnapToGrid(snap) {
|
124
129
|
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;
|
@@ -12,6 +12,7 @@ const SVGRenderer_1 = __importDefault(require("../../rendering/renderers/SVGRend
|
|
12
12
|
const Selection_1 = __importDefault(require("./Selection"));
|
13
13
|
const TextComponent_1 = __importDefault(require("../../components/TextComponent"));
|
14
14
|
const keybindings_1 = require("../keybindings");
|
15
|
+
const ToPointerAutoscroller_1 = __importDefault(require("./ToPointerAutoscroller"));
|
15
16
|
exports.cssPrefix = 'selection-tool-';
|
16
17
|
// Allows users to select/transform portions of the `EditorImage`.
|
17
18
|
// With respect to `extend`ing, `SelectionTool` is not stable.
|
@@ -23,8 +24,19 @@ class SelectionTool extends BaseTool_1.default {
|
|
23
24
|
this.expandingSelectionBox = false;
|
24
25
|
this.shiftKeyPressed = false;
|
25
26
|
this.snapToGrid = false;
|
27
|
+
this.lastPointer = null;
|
26
28
|
this.selectionBoxHandlingEvt = false;
|
27
29
|
this.lastSelectedObjects = [];
|
30
|
+
this.autoscroller = new ToPointerAutoscroller_1.default(editor.viewport, (scrollBy) => {
|
31
|
+
editor.dispatch(Viewport_1.default.transformBy(math_1.Mat33.translation(scrollBy)), false);
|
32
|
+
// Update the selection box/content to match the new viewport.
|
33
|
+
if (this.lastPointer) {
|
34
|
+
// The viewport has changed -- ensure that the screen and canvas positions
|
35
|
+
// of the pointer are both correct
|
36
|
+
const updatedPointer = this.lastPointer.withScreenPosition(this.lastPointer.screenPos, editor.viewport);
|
37
|
+
this.onMainPointerUpdated(updatedPointer);
|
38
|
+
}
|
39
|
+
});
|
28
40
|
this.handleOverlay = document.createElement('div');
|
29
41
|
editor.createHTMLOverlay(this.handleOverlay);
|
30
42
|
this.handleOverlay.style.display = 'none';
|
@@ -87,14 +99,22 @@ class SelectionTool extends BaseTool_1.default {
|
|
87
99
|
this.expandingSelectionBox = this.shiftKeyPressed;
|
88
100
|
this.makeSelectionBox(current.canvasPos);
|
89
101
|
}
|
102
|
+
else {
|
103
|
+
// Only autoscroll if we're transforming an existing selection
|
104
|
+
this.autoscroller.start();
|
105
|
+
}
|
90
106
|
return true;
|
91
107
|
}
|
92
108
|
return false;
|
93
109
|
}
|
94
110
|
onPointerMove(event) {
|
111
|
+
this.onMainPointerUpdated(event.current);
|
112
|
+
}
|
113
|
+
onMainPointerUpdated(currentPointer) {
|
114
|
+
this.lastPointer = currentPointer;
|
95
115
|
if (!this.selectionBox)
|
96
116
|
return;
|
97
|
-
|
117
|
+
this.autoscroller.onPointerMove(currentPointer.screenPos);
|
98
118
|
if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
|
99
119
|
const screenPos = this.editor.viewport.canvasToScreen(this.startPoint);
|
100
120
|
currentPointer = currentPointer.lockedToXYAxesScreen(screenPos, this.editor.viewport);
|
@@ -109,21 +129,8 @@ class SelectionTool extends BaseTool_1.default {
|
|
109
129
|
this.selectionBox.setToPoint(currentPointer.canvasPos);
|
110
130
|
}
|
111
131
|
}
|
112
|
-
// Called after a gestureCancel and a pointerUp
|
113
|
-
onGestureEnd() {
|
114
|
-
if (!this.selectionBox)
|
115
|
-
return;
|
116
|
-
if (!this.selectionBoxHandlingEvt) {
|
117
|
-
// Expand/shrink the selection rectangle, if applicable
|
118
|
-
this.selectionBox.resolveToObjects();
|
119
|
-
this.onSelectionUpdated();
|
120
|
-
}
|
121
|
-
else {
|
122
|
-
this.selectionBox.onDragEnd();
|
123
|
-
}
|
124
|
-
this.selectionBoxHandlingEvt = false;
|
125
|
-
}
|
126
132
|
onPointerUp(event) {
|
133
|
+
this.autoscroller.stop();
|
127
134
|
if (!this.selectionBox)
|
128
135
|
return;
|
129
136
|
let currentPointer = event.current;
|
@@ -142,10 +149,20 @@ class SelectionTool extends BaseTool_1.default {
|
|
142
149
|
]);
|
143
150
|
}
|
144
151
|
else {
|
145
|
-
this.
|
152
|
+
if (!this.selectionBoxHandlingEvt) {
|
153
|
+
// Expand/shrink the selection rectangle, if applicable
|
154
|
+
this.selectionBox.resolveToObjects();
|
155
|
+
this.onSelectionUpdated();
|
156
|
+
}
|
157
|
+
else {
|
158
|
+
this.selectionBox.onDragEnd();
|
159
|
+
}
|
160
|
+
this.selectionBoxHandlingEvt = false;
|
161
|
+
this.lastPointer = null;
|
146
162
|
}
|
147
163
|
}
|
148
164
|
onGestureCancel() {
|
165
|
+
this.autoscroller.stop();
|
149
166
|
if (this.selectionBoxHandlingEvt) {
|
150
167
|
this.selectionBox?.onDragCancel();
|
151
168
|
}
|
@@ -158,6 +175,8 @@ class SelectionTool extends BaseTool_1.default {
|
|
158
175
|
this.prevSelectionBox = null;
|
159
176
|
}
|
160
177
|
this.expandingSelectionBox = false;
|
178
|
+
this.lastPointer = null;
|
179
|
+
this.selectionBoxHandlingEvt = false;
|
161
180
|
}
|
162
181
|
onSelectionUpdated() {
|
163
182
|
const selectedItemCount = this.selectionBox?.getSelectedItemCount() ?? 0;
|
@@ -283,6 +302,7 @@ class SelectionTool extends BaseTool_1.default {
|
|
283
302
|
const transform = math_1.Mat33.scaling2D(scaleFactor, this.editor.viewport.roundPoint(region.topLeft)).rightMul(math_1.Mat33.translation(regionCenter).rightMul(roundedRotationMatrix).rightMul(math_1.Mat33.translation(regionCenter.times(-1)))).rightMul(math_1.Mat33.translation(this.editor.viewport.roundPoint(math_1.Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))));
|
284
303
|
const oldTransform = this.selectionBox.getTransform();
|
285
304
|
this.selectionBox.setTransform(oldTransform.rightMul(transform));
|
305
|
+
this.selectionBox.scrollTo();
|
286
306
|
}
|
287
307
|
if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
|
288
308
|
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 {};
|