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
@@ -99,30 +99,33 @@ class TextComponent extends AbstractComponent {
|
|
99
99
|
let bbox = null;
|
100
100
|
const cursor = new TextComponent.TextCursor(this.transform, this.style);
|
101
101
|
for (const textObject of this.textObjects) {
|
102
|
-
const transform = cursor.update(textObject);
|
102
|
+
const transform = cursor.update(textObject).transform;
|
103
103
|
const currentBBox = this.computeUntransformedBBoxOfPart(textObject).transformedBoundingBox(transform);
|
104
104
|
bbox ??= currentBBox;
|
105
105
|
bbox = bbox.union(currentBBox);
|
106
106
|
}
|
107
107
|
this.contentBBox = bbox ?? Rect2.empty;
|
108
108
|
}
|
109
|
-
renderInternal(canvas) {
|
109
|
+
renderInternal(canvas, visibleRect) {
|
110
110
|
const cursor = new TextComponent.TextCursor(this.transform, this.style);
|
111
111
|
for (const textObject of this.textObjects) {
|
112
|
-
const transform = cursor.update(textObject);
|
112
|
+
const { transform, bbox } = cursor.update(textObject);
|
113
|
+
if (visibleRect && !visibleRect.intersects(bbox)) {
|
114
|
+
continue;
|
115
|
+
}
|
113
116
|
if (typeof textObject === 'string') {
|
114
117
|
canvas.drawText(textObject, transform, this.style);
|
115
118
|
}
|
116
119
|
else {
|
117
120
|
canvas.pushTransform(transform);
|
118
|
-
textObject.renderInternal(canvas);
|
121
|
+
textObject.renderInternal(canvas, visibleRect?.transformedBoundingBox(transform.inverse()));
|
119
122
|
canvas.popTransform();
|
120
123
|
}
|
121
124
|
}
|
122
125
|
}
|
123
|
-
render(canvas,
|
126
|
+
render(canvas, visibleRect) {
|
124
127
|
canvas.startObject(this.contentBBox);
|
125
|
-
this.renderInternal(canvas);
|
128
|
+
this.renderInternal(canvas, visibleRect);
|
126
129
|
canvas.endObject(this.getLoadSaveData());
|
127
130
|
}
|
128
131
|
getProportionalRenderingTime() {
|
@@ -132,7 +135,7 @@ class TextComponent extends AbstractComponent {
|
|
132
135
|
const cursor = new TextComponent.TextCursor(this.transform, this.style);
|
133
136
|
for (const subObject of this.textObjects) {
|
134
137
|
// Convert canvas space to internal space relative to the current object.
|
135
|
-
const invTransform = cursor.update(subObject).inverse();
|
138
|
+
const invTransform = cursor.update(subObject).transform.inverse();
|
136
139
|
const transformedLine = lineSegment.transformedBy(invTransform);
|
137
140
|
if (typeof subObject === 'string') {
|
138
141
|
const textBBox = TextComponent.getTextDimens(subObject, this.style);
|
@@ -310,11 +313,11 @@ TextComponent.TextCursor = class {
|
|
310
313
|
this.transform = Mat33.identity;
|
311
314
|
}
|
312
315
|
/**
|
313
|
-
* Based on previous calls to `update`, returns the transformation
|
314
|
-
* the
|
315
|
-
* constructor
|
316
|
+
* Based on previous calls to `update`, returns the transformation and bounding box (relative
|
317
|
+
* to the parent element, or if none, the canvas) of the given `element`. Note that
|
318
|
+
* this is computed in part using the `parentTransform` provivded to this cursor's constructor.
|
316
319
|
*
|
317
|
-
*
|
320
|
+
* Warning: There may be edge cases here that are not taken into account.
|
318
321
|
*/
|
319
322
|
update(elem) {
|
320
323
|
let elementTransform = Mat33.identity;
|
@@ -353,7 +356,11 @@ TextComponent.TextCursor = class {
|
|
353
356
|
// Update this.transform so that future calls to update return correct values.
|
354
357
|
const endShiftTransform = Mat33.translation(Vec2.of(textSize.width, 0));
|
355
358
|
this.transform = elementTransform.rightMul(elemInternalTransform).rightMul(endShiftTransform);
|
356
|
-
|
359
|
+
const transform = this.parentTransform.rightMul(elementTransform);
|
360
|
+
return {
|
361
|
+
transform,
|
362
|
+
bbox: textSize.transformedBoundingBox(transform),
|
363
|
+
};
|
357
364
|
}
|
358
365
|
};
|
359
366
|
export default TextComponent;
|
@@ -182,11 +182,11 @@ class EditorImage {
|
|
182
182
|
* @see {@link Display.flatten}
|
183
183
|
*/
|
184
184
|
static addElement(elem, applyByFlattening = false) {
|
185
|
-
return new
|
185
|
+
return new _a.AddElementCommand(elem, applyByFlattening);
|
186
186
|
}
|
187
187
|
/** @see EditorImage.addElement */
|
188
188
|
addElement(elem, applyByFlattening) {
|
189
|
-
return
|
189
|
+
return _a.addElement(elem, applyByFlattening);
|
190
190
|
}
|
191
191
|
/**
|
192
192
|
* @returns a `Viewport` for rendering the image when importing/exporting.
|
@@ -205,7 +205,7 @@ class EditorImage {
|
|
205
205
|
* autoresize (if it was previously enabled).
|
206
206
|
*/
|
207
207
|
setImportExportRect(imageRect) {
|
208
|
-
return
|
208
|
+
return _a.SetImportExportRectCommand.of(this, imageRect, false);
|
209
209
|
}
|
210
210
|
getAutoresizeEnabled() {
|
211
211
|
return this.shouldAutoresizeExportViewport;
|
@@ -216,7 +216,7 @@ class EditorImage {
|
|
216
216
|
return Command.empty;
|
217
217
|
}
|
218
218
|
const newBBox = this.root.getBBox();
|
219
|
-
return
|
219
|
+
return _a.SetImportExportRectCommand.of(this, newBBox, autoresize);
|
220
220
|
}
|
221
221
|
setAutoresizeEnabledDirectly(shouldAutoresize) {
|
222
222
|
if (shouldAutoresize !== this.shouldAutoresizeExportViewport) {
|
@@ -322,7 +322,7 @@ EditorImage.AddElementCommand = (_b = class extends SerializableCommand {
|
|
322
322
|
const id = json.elemData.id;
|
323
323
|
const foundElem = editor.image.lookupElement(id);
|
324
324
|
const elem = foundElem ?? AbstractComponent.deserialize(json.elemData);
|
325
|
-
const result = new
|
325
|
+
const result = new _a.AddElementCommand(elem);
|
326
326
|
result.serializedElem = json.elemData;
|
327
327
|
return result;
|
328
328
|
});
|
@@ -331,7 +331,7 @@ EditorImage.AddElementCommand = (_b = class extends SerializableCommand {
|
|
331
331
|
// Handles resizing the background import/export region of the image.
|
332
332
|
EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand {
|
333
333
|
constructor(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize) {
|
334
|
-
super(
|
334
|
+
super(_a.SetImportExportRectCommand.commandId);
|
335
335
|
this.originalSize = originalSize;
|
336
336
|
this.originalTransform = originalTransform;
|
337
337
|
this.originalAutoresize = originalAutoresize;
|
@@ -344,7 +344,7 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
|
|
344
344
|
const originalSize = importExportViewport.visibleRect.size;
|
345
345
|
const originalTransform = importExportViewport.canvasToScreenTransform;
|
346
346
|
const originalAutoresize = image.getAutoresizeEnabled();
|
347
|
-
return new
|
347
|
+
return new _a.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, newExportRect, newAutoresize);
|
348
348
|
}
|
349
349
|
apply(editor) {
|
350
350
|
editor.image.setAutoresizeEnabledDirectly(this.newAutoresize);
|
@@ -405,7 +405,7 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
|
|
405
405
|
const finalRect = new Rect2(json.newRegion.x, json.newRegion.y, json.newRegion.w, json.newRegion.h);
|
406
406
|
const autoresize = json.autoresize ?? false;
|
407
407
|
const originalAutoresize = json.originalAutoresize ?? false;
|
408
|
-
return new
|
408
|
+
return new _a.SetImportExportRectCommand(originalSize, originalTransform, originalAutoresize, finalRect, autoresize);
|
409
409
|
});
|
410
410
|
})(),
|
411
411
|
_c);
|
@@ -10,5 +10,7 @@ export interface EditorLocalization extends ToolbarLocalization, ToolLocalizatio
|
|
10
10
|
doneLoading: string;
|
11
11
|
loading: (percentage: number) => string;
|
12
12
|
imageEditor: string;
|
13
|
+
softwareLibraries: string;
|
14
|
+
developerInformation: string;
|
13
15
|
}
|
14
16
|
export declare const defaultEditorLocalization: EditorLocalization;
|
@@ -19,4 +19,6 @@ export const defaultEditorLocalization = {
|
|
19
19
|
doneLoading: 'Done loading',
|
20
20
|
undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`,
|
21
21
|
redoAnnouncement: (commandDescription) => `Redid ${commandDescription}`,
|
22
|
+
softwareLibraries: 'Libraries',
|
23
|
+
developerInformation: 'Developer information',
|
22
24
|
};
|
@@ -21,11 +21,26 @@ export const visualEquivalent = (renderablePath, visibleRect) => {
|
|
21
21
|
const path = pathFromRenderable(renderablePath);
|
22
22
|
const strokeWidth = renderablePath.style.stroke?.width ?? 0;
|
23
23
|
const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
|
24
|
+
const styledPathBBox = path.bbox.grownBy(strokeWidth);
|
25
|
+
// Are we close enough to the path that it fills the entire screen?
|
26
|
+
if (onlyStroked
|
27
|
+
&& renderablePath.style.stroke
|
28
|
+
&& strokeWidth > visibleRect.maxDimension
|
29
|
+
&& styledPathBBox.containsRect(visibleRect)) {
|
30
|
+
const strokeRadius = strokeWidth / 2;
|
31
|
+
// Do a fast, but with many false negatives, check.
|
32
|
+
for (const point of path.startEndPoints()) {
|
33
|
+
// If within the strokeRadius of any point
|
34
|
+
if (visibleRect.isWithinRadiusOf(strokeRadius, point)) {
|
35
|
+
return pathToRenderable(Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color });
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
24
39
|
// Scale the expanded rect --- the visual equivalent is only close for huge strokes.
|
25
40
|
const expandedRect = visibleRect.grownBy(strokeWidth)
|
26
41
|
.transformedBoundingBox(Mat33.scaling2D(4, visibleRect.center));
|
27
42
|
// TODO: Handle simplifying very small paths.
|
28
|
-
if (expandedRect.containsRect(
|
43
|
+
if (expandedRect.containsRect(styledPathBBox)) {
|
29
44
|
return renderablePath;
|
30
45
|
}
|
31
46
|
const parts = [];
|
@@ -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
|
}
|
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
11
|
};
|
12
|
-
var _BaseWidget_instances, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
|
12
|
+
var _BaseWidget_instances, _a, _BaseWidget_hasDropdown, _BaseWidget_disabledDueToReadOnlyEditor, _BaseWidget_tags, _BaseWidget_removeEditorListeners, _BaseWidget_addEditorListeners;
|
13
13
|
import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler.mjs';
|
14
14
|
import { keyPressEventFromHTMLEvent, keyUpEventFromHTMLEvent } from '../../inputEvents.mjs';
|
15
15
|
import { toolbarCSSPrefix } from '../constants.mjs';
|
@@ -422,13 +422,13 @@ class BaseWidget {
|
|
422
422
|
}
|
423
423
|
}
|
424
424
|
}
|
425
|
-
_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() {
|
425
|
+
_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() {
|
426
426
|
__classPrivateFieldGet(this, _BaseWidget_removeEditorListeners, "f")?.call(this);
|
427
427
|
const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
|
428
428
|
let removeKeyPressListener = null;
|
429
429
|
// If the onKeyPress function has been extended and the editor is configured to send keypress events to
|
430
430
|
// toolbar widgets,
|
431
|
-
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !==
|
431
|
+
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== _a.prototype.onKeyPress) {
|
432
432
|
const keyPressListener = (event) => this.onKeyPress(event);
|
433
433
|
const handler = toolbarShortcutHandlers[0];
|
434
434
|
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;
|
@@ -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,27 +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;
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
143
|
+
this.scrollTo();
|
144
|
+
// Make the commands undo-able.
|
145
|
+
// Don't check for non-empty transforms because this breaks changing the
|
146
|
+
// z-index of the just-transformed commands.
|
147
|
+
//
|
148
|
+
// TODO: Check whether the selectedElems are already all toplevel.
|
149
|
+
const transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform));
|
136
150
|
// Clear renderings of any in-progress transformations
|
137
151
|
const wetInkRenderer = this.editor.display.getWetInkRenderer();
|
138
152
|
wetInkRenderer.clear();
|
153
|
+
return transformPromise;
|
139
154
|
}
|
140
155
|
// Preview the effects of the current transformation on the selection
|
141
156
|
previewTransformCmds() {
|
157
|
+
if (this.selectedElems.length === 0) {
|
158
|
+
return;
|
159
|
+
}
|
142
160
|
// Don't render what we're moving if it's likely to be slow.
|
143
161
|
if (this.selectedElems.length > maxPreviewElemCount) {
|
144
162
|
this.updateUI();
|
@@ -219,28 +237,29 @@ class Selection {
|
|
219
237
|
if (!this.hasParent) {
|
220
238
|
return;
|
221
239
|
}
|
240
|
+
const screenRegion = this.getScreenRegion();
|
222
241
|
// marginLeft, marginTop: Display relative to the top left of the selection overlay.
|
223
242
|
// left, top don't work for this.
|
224
|
-
this.backgroundElem.style.marginLeft = `${
|
225
|
-
this.backgroundElem.style.marginTop = `${
|
226
|
-
this.backgroundElem.style.width = `${
|
227
|
-
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`;
|
228
247
|
const rotationDeg = this.screenRegionRotation * 180 / Math.PI;
|
229
248
|
this.backgroundElem.style.transform = `rotate(${rotationDeg}deg)`;
|
230
249
|
this.backgroundElem.style.transformOrigin = 'center';
|
231
250
|
// If closer to perpendicular, apply different CSS
|
232
251
|
const perpendicularClassName = `${cssPrefix}rotated-near-perpendicular`;
|
233
252
|
if (Math.abs(Math.sin(this.screenRegionRotation)) > 0.5) {
|
234
|
-
this.
|
253
|
+
this.innerContainer.classList.add(perpendicularClassName);
|
235
254
|
}
|
236
255
|
else {
|
237
|
-
this.
|
256
|
+
this.innerContainer.classList.remove(perpendicularClassName);
|
238
257
|
}
|
239
258
|
for (const handle of this.handles) {
|
240
259
|
handle.updatePosition();
|
241
260
|
}
|
242
261
|
}
|
243
|
-
// Add/remove the contents of this
|
262
|
+
// Add/remove the contents of this seleciton from the editor.
|
244
263
|
// Used to prevent previewed content from looking like duplicate content
|
245
264
|
// while dragging.
|
246
265
|
//
|
@@ -248,6 +267,9 @@ class Selection {
|
|
248
267
|
// the editor image is likely to be slow.)
|
249
268
|
//
|
250
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.
|
251
273
|
addRemoveSelectionFromImage(inImage) {
|
252
274
|
// Don't hide elements if doing so will be slow.
|
253
275
|
if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
|
@@ -290,17 +312,18 @@ class Selection {
|
|
290
312
|
document.getSelection()?.removeAllRanges();
|
291
313
|
this.targetHandle = null;
|
292
314
|
let result = false;
|
315
|
+
this.backgroundDragging = false;
|
316
|
+
if (this.region.containsPoint(pointer.canvasPos)) {
|
317
|
+
this.backgroundDragging = true;
|
318
|
+
result = true;
|
319
|
+
}
|
293
320
|
for (const handle of this.handles) {
|
294
321
|
if (handle.containsPoint(pointer.canvasPos)) {
|
295
322
|
this.targetHandle = handle;
|
323
|
+
this.backgroundDragging = false;
|
296
324
|
result = true;
|
297
325
|
}
|
298
326
|
}
|
299
|
-
this.backgroundDragging = false;
|
300
|
-
if (this.region.containsPoint(pointer.canvasPos)) {
|
301
|
-
this.backgroundDragging = true;
|
302
|
-
result = true;
|
303
|
-
}
|
304
327
|
if (result) {
|
305
328
|
this.removeDeletedElemsFromSelection();
|
306
329
|
this.addRemoveSelectionFromImage(false);
|
@@ -338,23 +361,29 @@ class Selection {
|
|
338
361
|
this.targetHandle = null;
|
339
362
|
this.setTransform(Mat33.identity);
|
340
363
|
this.addRemoveSelectionFromImage(true);
|
364
|
+
this.updateUI();
|
341
365
|
}
|
342
366
|
// Scroll the viewport to this. Does not zoom
|
343
|
-
|
367
|
+
scrollTo() {
|
344
368
|
if (this.selectedElems.length === 0) {
|
345
|
-
return;
|
369
|
+
return false;
|
346
370
|
}
|
347
|
-
const
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
const
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
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;
|
357
385
|
}
|
386
|
+
return false;
|
358
387
|
}
|
359
388
|
deleteSelectedObjects() {
|
360
389
|
if (this.backgroundDragging || this.targetHandle) {
|
@@ -382,7 +411,7 @@ class Selection {
|
|
382
411
|
if (wasTransforming) {
|
383
412
|
// Don't update the selection's focus when redoing/undoing
|
384
413
|
const selectionToUpdate = null;
|
385
|
-
tmpApplyCommand = new
|
414
|
+
tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
|
386
415
|
// Transform to ensure that the duplicates are in the correct location
|
387
416
|
await tmpApplyCommand.apply(this.editor);
|
388
417
|
// Show items again
|
@@ -399,10 +428,10 @@ class Selection {
|
|
399
428
|
return duplicateCommand;
|
400
429
|
}
|
401
430
|
addTo(elem) {
|
402
|
-
if (this.
|
403
|
-
this.
|
431
|
+
if (this.outerContainer.parentElement) {
|
432
|
+
this.outerContainer.remove();
|
404
433
|
}
|
405
|
-
elem.appendChild(this.
|
434
|
+
elem.appendChild(this.outerContainer);
|
406
435
|
this.hasParent = true;
|
407
436
|
}
|
408
437
|
setToPoint(point) {
|
@@ -411,8 +440,8 @@ class Selection {
|
|
411
440
|
this.updateUI();
|
412
441
|
}
|
413
442
|
cancelSelection() {
|
414
|
-
if (this.
|
415
|
-
this.
|
443
|
+
if (this.outerContainer.parentElement) {
|
444
|
+
this.outerContainer.remove();
|
416
445
|
}
|
417
446
|
this.originalRegion = Rect2.empty;
|
418
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;
|