js-draw 0.10.3 → 0.11.1
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/.github/ISSUE_TEMPLATE/translation.yml +72 -0
- package/CHANGELOG.md +9 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +12 -3
- package/dist/src/Editor.js +72 -25
- package/dist/src/EditorImage.js +1 -1
- package/dist/src/SVGLoader.js +3 -2
- package/dist/src/components/AbstractComponent.d.ts +1 -0
- package/dist/src/components/AbstractComponent.js +15 -6
- package/dist/src/components/ImageComponent.d.ts +3 -0
- package/dist/src/components/ImageComponent.js +12 -1
- package/dist/src/localizations/es.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +9 -5
- package/dist/src/toolbar/HTMLToolbar.js +2 -1
- package/dist/src/toolbar/IconProvider.d.ts +1 -0
- package/dist/src/toolbar/IconProvider.js +7 -0
- package/dist/src/toolbar/localization.d.ts +8 -0
- package/dist/src/toolbar/localization.js +8 -0
- package/dist/src/toolbar/widgets/InsertImageWidget.d.ts +19 -0
- package/dist/src/toolbar/widgets/InsertImageWidget.js +169 -0
- package/dist/src/toolbar/widgets/lib.d.ts +1 -0
- package/dist/src/toolbar/widgets/lib.js +1 -0
- package/dist/src/tools/Eraser.d.ts +1 -1
- package/dist/src/tools/Eraser.js +16 -18
- package/dist/src/tools/PanZoom.js +10 -0
- package/dist/src/tools/PasteHandler.js +1 -39
- package/dist/src/tools/SelectionTool/Selection.d.ts +2 -3
- package/dist/src/tools/SelectionTool/Selection.js +63 -26
- package/dist/src/tools/SelectionTool/SelectionTool.js +9 -0
- package/dist/src/util/fileToBase64.d.ts +3 -0
- package/dist/src/util/fileToBase64.js +13 -0
- package/dist/src/util/waitForTimeout.d.ts +2 -0
- package/dist/src/util/waitForTimeout.js +7 -0
- package/package.json +1 -1
- package/src/Editor.ts +90 -27
- package/src/EditorImage.ts +1 -1
- package/src/SVGLoader.ts +1 -0
- package/src/components/AbstractComponent.ts +18 -4
- package/src/components/ImageComponent.ts +15 -0
- package/src/localizations/es.ts +3 -0
- package/src/rendering/renderers/SVGRenderer.ts +6 -1
- package/src/toolbar/HTMLToolbar.ts +3 -1
- package/src/toolbar/IconProvider.ts +8 -0
- package/src/toolbar/localization.ts +19 -1
- package/src/toolbar/toolbar.css +2 -0
- package/src/toolbar/widgets/InsertImageWidget.css +44 -0
- package/src/toolbar/widgets/InsertImageWidget.ts +222 -0
- package/src/toolbar/widgets/lib.ts +2 -0
- package/src/tools/Eraser.ts +19 -15
- package/src/tools/PanZoom.test.ts +65 -0
- package/src/tools/PanZoom.ts +12 -0
- package/src/tools/PasteHandler.ts +2 -51
- package/src/tools/SelectionTool/Selection.ts +62 -22
- package/src/tools/SelectionTool/SelectionTool.ts +12 -0
- package/src/util/fileToBase64.ts +18 -0
- package/src/util/waitForTimeout.ts +9 -0
@@ -0,0 +1,169 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
10
|
+
import ImageComponent from '../../components/ImageComponent';
|
11
|
+
import Erase from '../../commands/Erase';
|
12
|
+
import EditorImage from '../../EditorImage';
|
13
|
+
import { SelectionTool, uniteCommands } from '../../lib';
|
14
|
+
import Mat33 from '../../math/Mat33';
|
15
|
+
import fileToBase64 from '../../util/fileToBase64';
|
16
|
+
import ActionButtonWidget from './ActionButtonWidget';
|
17
|
+
export default class InsertImageWidget extends ActionButtonWidget {
|
18
|
+
constructor(editor, localization) {
|
19
|
+
localization !== null && localization !== void 0 ? localization : (localization = editor.localization);
|
20
|
+
super(editor, 'insert-image-widget', () => editor.icons.makeInsertImageIcon(), localization.image, () => this.onClicked());
|
21
|
+
this.imageSelectionOverlay = document.createElement('div');
|
22
|
+
this.imageSelectionOverlay.classList.add('toolbar-image-selection-overlay');
|
23
|
+
this.fillOverlay();
|
24
|
+
this.editor.createHTMLOverlay(this.imageSelectionOverlay);
|
25
|
+
this.imageSelectionOverlay.style.display = 'none';
|
26
|
+
}
|
27
|
+
fillOverlay() {
|
28
|
+
const container = document.createElement('div');
|
29
|
+
const chooseImageRow = document.createElement('div');
|
30
|
+
const altTextRow = document.createElement('div');
|
31
|
+
this.imagePreview = document.createElement('img');
|
32
|
+
this.statusView = document.createElement('div');
|
33
|
+
const actionButtonRow = document.createElement('div');
|
34
|
+
actionButtonRow.classList.add('action-button-row');
|
35
|
+
this.submitButton = document.createElement('button');
|
36
|
+
const cancelButton = document.createElement('button');
|
37
|
+
this.imageFileInput = document.createElement('input');
|
38
|
+
this.imageAltTextInput = document.createElement('input');
|
39
|
+
const imageFileInputLabel = document.createElement('label');
|
40
|
+
const imageAltTextLabel = document.createElement('label');
|
41
|
+
const fileInputId = `insert-image-file-input-${InsertImageWidget.nextInputId++}`;
|
42
|
+
const altTextInputId = `insert-image-alt-text-input-${InsertImageWidget.nextInputId++}`;
|
43
|
+
this.imageFileInput.setAttribute('id', fileInputId);
|
44
|
+
this.imageAltTextInput.setAttribute('id', altTextInputId);
|
45
|
+
imageAltTextLabel.htmlFor = altTextInputId;
|
46
|
+
imageFileInputLabel.htmlFor = fileInputId;
|
47
|
+
this.imageFileInput.accept = 'image/*';
|
48
|
+
imageAltTextLabel.innerText = this.localizationTable.inputAltText;
|
49
|
+
imageFileInputLabel.innerText = this.localizationTable.chooseFile;
|
50
|
+
this.imageFileInput.type = 'file';
|
51
|
+
this.imageAltTextInput.type = 'text';
|
52
|
+
this.statusView.setAttribute('aria-live', 'polite');
|
53
|
+
cancelButton.innerText = this.localizationTable.cancel;
|
54
|
+
this.submitButton.innerText = this.localizationTable.submit;
|
55
|
+
this.imageFileInput.onchange = () => __awaiter(this, void 0, void 0, function* () {
|
56
|
+
if (this.imageFileInput.value === '' || !this.imageFileInput.files || !this.imageFileInput.files[0]) {
|
57
|
+
this.imagePreview.style.display = 'none';
|
58
|
+
this.submitButton.disabled = true;
|
59
|
+
return;
|
60
|
+
}
|
61
|
+
this.imagePreview.style.display = 'block';
|
62
|
+
const image = this.imageFileInput.files[0];
|
63
|
+
let data = null;
|
64
|
+
try {
|
65
|
+
data = yield fileToBase64(image);
|
66
|
+
}
|
67
|
+
catch (e) {
|
68
|
+
this.statusView.innerText = this.localizationTable.imageLoadError(e);
|
69
|
+
}
|
70
|
+
this.imageBase64URL = data;
|
71
|
+
if (data) {
|
72
|
+
this.imagePreview.src = data;
|
73
|
+
this.submitButton.disabled = false;
|
74
|
+
this.updateImageSizeDisplay();
|
75
|
+
}
|
76
|
+
else {
|
77
|
+
this.submitButton.disabled = true;
|
78
|
+
this.statusView.innerText = '';
|
79
|
+
}
|
80
|
+
});
|
81
|
+
cancelButton.onclick = () => {
|
82
|
+
this.hideDialog();
|
83
|
+
};
|
84
|
+
this.imageSelectionOverlay.onclick = (evt) => {
|
85
|
+
// If clicking on the backdrop
|
86
|
+
if (evt.target === this.imageSelectionOverlay) {
|
87
|
+
this.hideDialog();
|
88
|
+
}
|
89
|
+
};
|
90
|
+
chooseImageRow.replaceChildren(imageFileInputLabel, this.imageFileInput);
|
91
|
+
altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
|
92
|
+
actionButtonRow.replaceChildren(cancelButton, this.submitButton);
|
93
|
+
container.replaceChildren(chooseImageRow, altTextRow, this.imagePreview, this.statusView, actionButtonRow);
|
94
|
+
this.imageSelectionOverlay.replaceChildren(container);
|
95
|
+
}
|
96
|
+
hideDialog() {
|
97
|
+
this.imageSelectionOverlay.style.display = 'none';
|
98
|
+
}
|
99
|
+
updateImageSizeDisplay() {
|
100
|
+
var _a;
|
101
|
+
const imageData = (_a = this.imageBase64URL) !== null && _a !== void 0 ? _a : '';
|
102
|
+
const sizeInKiB = imageData.length / 1024;
|
103
|
+
const sizeInMiB = sizeInKiB / 1024;
|
104
|
+
let units = 'KiB';
|
105
|
+
let size = sizeInKiB;
|
106
|
+
if (sizeInMiB >= 1) {
|
107
|
+
size = sizeInMiB;
|
108
|
+
units = 'MiB';
|
109
|
+
}
|
110
|
+
this.statusView.innerText = this.localizationTable.imageSize(Math.round(size), units);
|
111
|
+
}
|
112
|
+
clearInputs() {
|
113
|
+
this.imageFileInput.value = '';
|
114
|
+
this.imageAltTextInput.value = '';
|
115
|
+
this.imagePreview.style.display = 'none';
|
116
|
+
this.submitButton.disabled = true;
|
117
|
+
this.statusView.innerText = '';
|
118
|
+
}
|
119
|
+
onClicked() {
|
120
|
+
var _a;
|
121
|
+
this.imageSelectionOverlay.style.display = '';
|
122
|
+
this.clearInputs();
|
123
|
+
this.imageFileInput.focus();
|
124
|
+
const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
|
125
|
+
const selectedObjects = selectionTools.map(tool => tool.getSelectedObjects()).flat();
|
126
|
+
let editingImage = null;
|
127
|
+
if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
|
128
|
+
editingImage = selectedObjects[0];
|
129
|
+
this.imageAltTextInput.value = (_a = editingImage.getAltText()) !== null && _a !== void 0 ? _a : '';
|
130
|
+
this.imagePreview.style.display = 'block';
|
131
|
+
this.submitButton.disabled = false;
|
132
|
+
this.imageBase64URL = editingImage.getURL();
|
133
|
+
this.imagePreview.src = this.imageBase64URL;
|
134
|
+
this.updateImageSizeDisplay();
|
135
|
+
}
|
136
|
+
else {
|
137
|
+
selectionTools.forEach(tool => tool.clearSelection());
|
138
|
+
}
|
139
|
+
this.submitButton.onclick = () => __awaiter(this, void 0, void 0, function* () {
|
140
|
+
var _b;
|
141
|
+
if (!this.imageBase64URL) {
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
const image = new Image();
|
145
|
+
image.src = this.imageBase64URL;
|
146
|
+
image.setAttribute('alt', this.imageAltTextInput.value);
|
147
|
+
const component = yield ImageComponent.fromImage(image, Mat33.identity);
|
148
|
+
if (component.getBBox().area === 0) {
|
149
|
+
this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
|
150
|
+
return;
|
151
|
+
}
|
152
|
+
this.imageSelectionOverlay.style.display = 'none';
|
153
|
+
if (editingImage) {
|
154
|
+
const eraseCommand = new Erase([editingImage]);
|
155
|
+
yield this.editor.dispatch(uniteCommands([
|
156
|
+
EditorImage.addElement(component),
|
157
|
+
component.transformBy(editingImage.getTransformation()),
|
158
|
+
component.setZIndex(editingImage.getZIndex()),
|
159
|
+
eraseCommand,
|
160
|
+
]));
|
161
|
+
(_b = selectionTools[0]) === null || _b === void 0 ? void 0 : _b.setSelection([component]);
|
162
|
+
}
|
163
|
+
else {
|
164
|
+
yield this.editor.addAndCenterComponents([component]);
|
165
|
+
}
|
166
|
+
});
|
167
|
+
}
|
168
|
+
}
|
169
|
+
InsertImageWidget.nextInputId = 0;
|
@@ -6,3 +6,4 @@ export { default as TextToolWidget } from './TextToolWidget';
|
|
6
6
|
export { default as HandToolWidget } from './HandToolWidget';
|
7
7
|
export { default as SelectionToolWidget } from './SelectionToolWidget';
|
8
8
|
export { default as EraserToolWidget } from './EraserToolWidget';
|
9
|
+
export { default as InsertImageWidget } from './InsertImageWidget';
|
@@ -6,3 +6,4 @@ export { default as TextToolWidget } from './TextToolWidget';
|
|
6
6
|
export { default as HandToolWidget } from './HandToolWidget';
|
7
7
|
export { default as SelectionToolWidget } from './SelectionToolWidget';
|
8
8
|
export { default as EraserToolWidget } from './EraserToolWidget';
|
9
|
+
export { default as InsertImageWidget } from './InsertImageWidget';
|
@@ -4,8 +4,8 @@ import Editor from '../Editor';
|
|
4
4
|
export default class Eraser extends BaseTool {
|
5
5
|
private editor;
|
6
6
|
private lastPoint;
|
7
|
-
private command;
|
8
7
|
private toRemove;
|
8
|
+
private partialCommands;
|
9
9
|
constructor(editor: Editor, description: string);
|
10
10
|
onPointerDown(event: PointerEvt): boolean;
|
11
11
|
onPointerMove(event: PointerEvt): void;
|
package/dist/src/tools/Eraser.js
CHANGED
@@ -6,7 +6,8 @@ export default class Eraser extends BaseTool {
|
|
6
6
|
constructor(editor, description) {
|
7
7
|
super(editor.notifier, description);
|
8
8
|
this.editor = editor;
|
9
|
-
|
9
|
+
// Commands that each remove one element
|
10
|
+
this.partialCommands = [];
|
10
11
|
}
|
11
12
|
onPointerDown(event) {
|
12
13
|
if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
|
@@ -17,35 +18,32 @@ export default class Eraser extends BaseTool {
|
|
17
18
|
return false;
|
18
19
|
}
|
19
20
|
onPointerMove(event) {
|
20
|
-
var _a;
|
21
21
|
const currentPoint = event.current.canvasPos;
|
22
22
|
if (currentPoint.minus(this.lastPoint).magnitude() === 0) {
|
23
23
|
return;
|
24
24
|
}
|
25
25
|
const line = new LineSegment2(this.lastPoint, currentPoint);
|
26
26
|
const region = line.bbox;
|
27
|
-
|
28
|
-
this.toRemove.push(...this.editor.image
|
29
|
-
.getElementsIntersectingRegion(region).filter(component => {
|
27
|
+
const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
|
30
28
|
return component.intersects(line);
|
31
|
-
})
|
32
|
-
|
33
|
-
this.
|
34
|
-
|
29
|
+
});
|
30
|
+
// Remove any intersecting elements.
|
31
|
+
this.toRemove.push(...intersectingElems);
|
32
|
+
// Create new Erase commands for the now-to-be-erased elements and apply them.
|
33
|
+
const newPartialCommands = intersectingElems.map(elem => new Erase([elem]));
|
34
|
+
newPartialCommands.forEach(cmd => cmd.apply(this.editor));
|
35
|
+
this.partialCommands.push(...newPartialCommands);
|
35
36
|
this.lastPoint = currentPoint;
|
36
37
|
}
|
37
38
|
onPointerUp(_event) {
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
this.editor.dispatch(
|
39
|
+
if (this.toRemove.length > 0) {
|
40
|
+
// Undo commands for each individual component and unite into a single command.
|
41
|
+
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
42
|
+
const command = new Erase(this.toRemove);
|
43
|
+
this.editor.dispatch(command); // dispatch: Makes undo-able.
|
43
44
|
}
|
44
|
-
this.command = null;
|
45
45
|
}
|
46
46
|
onGestureCancel() {
|
47
|
-
|
48
|
-
(_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
|
49
|
-
this.command = null;
|
47
|
+
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
50
48
|
}
|
51
49
|
}
|
@@ -202,10 +202,20 @@ export default class PanZoom extends BaseTool {
|
|
202
202
|
&& this.velocity !== null
|
203
203
|
&& event.current.timeStamp - this.lastPointerDownTimestamp > minInertialScrollDt;
|
204
204
|
if (shouldInertialScroll && this.velocity !== null) {
|
205
|
+
const oldVelocity = this.velocity;
|
205
206
|
// If the user drags the screen, then stops, then lifts the pointer,
|
206
207
|
// we want the final velocity to reflect the stop at the end (so the velocity
|
207
208
|
// should be near zero). Handle this:
|
208
209
|
this.updateVelocity(event.current.screenPos);
|
210
|
+
// Work around an input issue. Some devices that disable the touchscreen when a stylus
|
211
|
+
// comes near the screen fire a touch-end event at the position of the stylus when a
|
212
|
+
// touch gesture is canceled. Because the stylus is often far away from the last touch,
|
213
|
+
// this causes a great displacement between the second-to-last (from the touchscreen) and
|
214
|
+
// last (from the pen that is now near the screen) events. Only allow velocity to decrease
|
215
|
+
// to work around this:
|
216
|
+
if (oldVelocity.magnitude() < this.velocity.magnitude()) {
|
217
|
+
this.velocity = oldVelocity;
|
218
|
+
}
|
209
219
|
// Cancel any ongoing inertial scrolling.
|
210
220
|
(_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
|
211
221
|
this.inertialScroller = new InertialScroller(this.velocity, (scrollDelta) => {
|
@@ -12,16 +12,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
12
12
|
});
|
13
13
|
};
|
14
14
|
import { TextComponent } from '../components/lib';
|
15
|
-
import { uniteCommands } from '../commands/lib';
|
16
15
|
import SVGLoader from '../SVGLoader';
|
17
16
|
import { Mat33 } from '../math/lib';
|
18
17
|
import BaseTool from './BaseTool';
|
19
|
-
import EditorImage from '../EditorImage';
|
20
|
-
import SelectionTool from './SelectionTool/SelectionTool';
|
21
18
|
import TextTool from './TextTool';
|
22
19
|
import Color4 from '../Color4';
|
23
20
|
import ImageComponent from '../components/ImageComponent';
|
24
|
-
import Viewport from '../Viewport';
|
25
21
|
// { @inheritDoc PasteHandler! }
|
26
22
|
export default class PasteHandler extends BaseTool {
|
27
23
|
constructor(editor) {
|
@@ -46,41 +42,7 @@ export default class PasteHandler extends BaseTool {
|
|
46
42
|
}
|
47
43
|
addComponentsFromPaste(components) {
|
48
44
|
return __awaiter(this, void 0, void 0, function* () {
|
49
|
-
|
50
|
-
for (const component of components) {
|
51
|
-
if (bbox) {
|
52
|
-
bbox = bbox.union(component.getBBox());
|
53
|
-
}
|
54
|
-
else {
|
55
|
-
bbox = component.getBBox();
|
56
|
-
}
|
57
|
-
}
|
58
|
-
if (!bbox) {
|
59
|
-
return;
|
60
|
-
}
|
61
|
-
// Find a transform that scales/moves bbox onto the screen.
|
62
|
-
const visibleRect = this.editor.viewport.visibleRect;
|
63
|
-
const scaleRatioX = visibleRect.width / bbox.width;
|
64
|
-
const scaleRatioY = visibleRect.height / bbox.height;
|
65
|
-
let scaleRatio = scaleRatioX;
|
66
|
-
if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
|
67
|
-
scaleRatio = scaleRatioY;
|
68
|
-
}
|
69
|
-
scaleRatio *= 2 / 3;
|
70
|
-
scaleRatio = Viewport.roundScaleRatio(scaleRatio);
|
71
|
-
const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));
|
72
|
-
const commands = [];
|
73
|
-
for (const component of components) {
|
74
|
-
// To allow deserialization, we need to add first, then transform.
|
75
|
-
commands.push(EditorImage.addElement(component));
|
76
|
-
commands.push(component.transformBy(transfm));
|
77
|
-
}
|
78
|
-
const applyChunkSize = 100;
|
79
|
-
this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
|
80
|
-
for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
|
81
|
-
selectionTool.setEnabled(true);
|
82
|
-
selectionTool.setSelection(components);
|
83
|
-
}
|
45
|
+
yield this.editor.addAndCenterComponents(components);
|
84
46
|
});
|
85
47
|
}
|
86
48
|
doSVGPaste(data) {
|
@@ -14,7 +14,6 @@ export default class Selection {
|
|
14
14
|
private originalRegion;
|
15
15
|
private transformers;
|
16
16
|
private transform;
|
17
|
-
private transformCommands;
|
18
17
|
private selectedElems;
|
19
18
|
private container;
|
20
19
|
private backgroundElem;
|
@@ -28,7 +27,6 @@ export default class Selection {
|
|
28
27
|
get preTransformedScreenRegionRotation(): number;
|
29
28
|
get screenRegion(): Rect2;
|
30
29
|
get screenRegionRotation(): number;
|
31
|
-
private computeTransformCommands;
|
32
30
|
setTransform(transform: Mat33, preview?: boolean): void;
|
33
31
|
finalizeTransform(): void;
|
34
32
|
private static ApplyTransformationCommand;
|
@@ -38,13 +36,14 @@ export default class Selection {
|
|
38
36
|
getMinCanvasSize(): number;
|
39
37
|
getSelectedItemCount(): number;
|
40
38
|
updateUI(): void;
|
39
|
+
private addRemoveSelectionFromImage;
|
41
40
|
private targetHandle;
|
42
41
|
private backgroundDragging;
|
43
42
|
onDragStart(pointer: Pointer, target: EventTarget): boolean;
|
44
43
|
onDragUpdate(pointer: Pointer): void;
|
45
44
|
onDragEnd(): void;
|
46
45
|
onDragCancel(): void;
|
47
|
-
scrollTo(): void
|
46
|
+
scrollTo(): Promise<void>;
|
48
47
|
deleteSelectedObjects(): Command;
|
49
48
|
duplicateSelectedObjects(): Command;
|
50
49
|
addTo(elem: HTMLElement): void;
|
@@ -22,13 +22,14 @@ import Erase from '../../commands/Erase';
|
|
22
22
|
import Duplicate from '../../commands/Duplicate';
|
23
23
|
import { DragTransformer, ResizeTransformer, RotateTransformer } from './TransformMode';
|
24
24
|
import { ResizeMode } from './types';
|
25
|
+
import EditorImage from '../../EditorImage';
|
25
26
|
const updateChunkSize = 100;
|
27
|
+
const maxPreviewElemCount = 500;
|
26
28
|
// @internal
|
27
29
|
export default class Selection {
|
28
30
|
constructor(startPoint, editor) {
|
29
31
|
this.editor = editor;
|
30
32
|
this.transform = Mat33.identity;
|
31
|
-
this.transformCommands = [];
|
32
33
|
this.selectedElems = [];
|
33
34
|
this.hasParent = true;
|
34
35
|
this.targetHandle = null;
|
@@ -89,28 +90,19 @@ export default class Selection {
|
|
89
90
|
get screenRegionRotation() {
|
90
91
|
return this.regionRotation + this.editor.viewport.getRotationAngle();
|
91
92
|
}
|
92
|
-
computeTransformCommands() {
|
93
|
-
return this.selectedElems.map(elem => {
|
94
|
-
return elem.transformBy(this.transform);
|
95
|
-
});
|
96
|
-
}
|
97
93
|
// Applies, previews, but doesn't finalize the given transformation.
|
98
94
|
setTransform(transform, preview = true) {
|
99
95
|
this.transform = transform;
|
100
96
|
if (preview && this.hasParent) {
|
101
|
-
this.previewTransformCmds();
|
102
97
|
this.scrollTo();
|
98
|
+
this.previewTransformCmds();
|
103
99
|
}
|
104
100
|
}
|
105
101
|
// Applies the current transformation to the selection
|
106
102
|
finalizeTransform() {
|
107
|
-
this.transformCommands.forEach(cmd => {
|
108
|
-
cmd.unapply(this.editor);
|
109
|
-
});
|
110
103
|
const fullTransform = this.transform;
|
111
104
|
const selectedElems = this.selectedElems;
|
112
105
|
// Reset for the next drag
|
113
|
-
this.transformCommands = [];
|
114
106
|
this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
|
115
107
|
this.transform = Mat33.identity;
|
116
108
|
// Make the commands undo-able
|
@@ -119,13 +111,19 @@ export default class Selection {
|
|
119
111
|
// Preview the effects of the current transformation on the selection
|
120
112
|
previewTransformCmds() {
|
121
113
|
// Don't render what we're moving if it's likely to be slow.
|
122
|
-
if (this.selectedElems.length >
|
114
|
+
if (this.selectedElems.length > maxPreviewElemCount) {
|
123
115
|
this.updateUI();
|
124
116
|
return;
|
125
117
|
}
|
126
|
-
this.
|
127
|
-
|
128
|
-
|
118
|
+
const wetInkRenderer = this.editor.display.getWetInkRenderer();
|
119
|
+
wetInkRenderer.clear();
|
120
|
+
wetInkRenderer.pushTransform(this.transform);
|
121
|
+
const viewportVisibleRect = this.editor.viewport.visibleRect;
|
122
|
+
const visibleRect = viewportVisibleRect.transformedBoundingBox(this.transform.inverse());
|
123
|
+
for (const elem of this.selectedElems) {
|
124
|
+
elem.render(wetInkRenderer, visibleRect);
|
125
|
+
}
|
126
|
+
wetInkRenderer.popTransform();
|
129
127
|
this.updateUI();
|
130
128
|
}
|
131
129
|
// Find the objects corresponding to this in the document,
|
@@ -208,7 +206,39 @@ export default class Selection {
|
|
208
206
|
handle.updatePosition();
|
209
207
|
}
|
210
208
|
}
|
209
|
+
// Add/remove the contents of this' seleciton from the editor.
|
210
|
+
// Used to prevent previewed content from looking like duplicate content
|
211
|
+
// while dragging.
|
212
|
+
//
|
213
|
+
// Does nothing if a large number of elements are selected (and so modifying
|
214
|
+
// the editor image is likely to be slow.)
|
215
|
+
//
|
216
|
+
// If removed from the image, selected elements are drawn as wet ink.
|
217
|
+
addRemoveSelectionFromImage(inImage) {
|
218
|
+
return __awaiter(this, void 0, void 0, function* () {
|
219
|
+
// Don't hide elements if doing so will be slow.
|
220
|
+
if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
|
221
|
+
return;
|
222
|
+
}
|
223
|
+
for (const elem of this.selectedElems) {
|
224
|
+
const parent = this.editor.image.findParent(elem);
|
225
|
+
if (!inImage) {
|
226
|
+
parent === null || parent === void 0 ? void 0 : parent.remove();
|
227
|
+
}
|
228
|
+
// If we're making things visible and the selected object wasn't previously
|
229
|
+
// visible,
|
230
|
+
else if (!parent) {
|
231
|
+
EditorImage.addElement(elem).apply(this.editor);
|
232
|
+
}
|
233
|
+
}
|
234
|
+
yield this.editor.queueRerender();
|
235
|
+
if (!inImage) {
|
236
|
+
this.previewTransformCmds();
|
237
|
+
}
|
238
|
+
});
|
239
|
+
}
|
211
240
|
onDragStart(pointer, target) {
|
241
|
+
void this.addRemoveSelectionFromImage(false);
|
212
242
|
for (const handle of this.handles) {
|
213
243
|
if (handle.isTarget(target)) {
|
214
244
|
handle.handleDragStart(pointer);
|
@@ -230,7 +260,6 @@ export default class Selection {
|
|
230
260
|
if (this.targetHandle) {
|
231
261
|
this.targetHandle.handleDragUpdate(pointer);
|
232
262
|
}
|
233
|
-
this.updateUI();
|
234
263
|
}
|
235
264
|
onDragEnd() {
|
236
265
|
if (this.backgroundDragging) {
|
@@ -239,6 +268,7 @@ export default class Selection {
|
|
239
268
|
else if (this.targetHandle) {
|
240
269
|
this.targetHandle.handleDragEnd();
|
241
270
|
}
|
271
|
+
this.addRemoveSelectionFromImage(true);
|
242
272
|
this.backgroundDragging = false;
|
243
273
|
this.targetHandle = null;
|
244
274
|
this.updateUI();
|
@@ -247,19 +277,26 @@ export default class Selection {
|
|
247
277
|
this.backgroundDragging = false;
|
248
278
|
this.targetHandle = null;
|
249
279
|
this.setTransform(Mat33.identity);
|
280
|
+
this.addRemoveSelectionFromImage(true);
|
250
281
|
}
|
251
282
|
// Scroll the viewport to this. Does not zoom
|
252
283
|
scrollTo() {
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
284
|
+
return __awaiter(this, void 0, void 0, function* () {
|
285
|
+
if (this.selectedElems.length === 0) {
|
286
|
+
return;
|
287
|
+
}
|
288
|
+
const screenRect = new Rect2(0, 0, this.editor.display.width, this.editor.display.height);
|
289
|
+
if (!screenRect.containsPoint(this.screenRegion.center)) {
|
290
|
+
const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
|
291
|
+
const screenDelta = this.screenRegion.center.minus(closestPoint);
|
292
|
+
const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
|
293
|
+
yield this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(-1))), false);
|
294
|
+
// Re-renders clear wet ink, so we need to re-draw the preview
|
295
|
+
// after the full re-render.
|
296
|
+
yield this.editor.queueRerender();
|
297
|
+
this.previewTransformCmds();
|
298
|
+
}
|
299
|
+
});
|
263
300
|
}
|
264
301
|
deleteSelectedObjects() {
|
265
302
|
return new Erase(this.selectedElems);
|
@@ -311,6 +311,15 @@ export default class SelectionTool extends BaseTool {
|
|
311
311
|
setSelection(objects) {
|
312
312
|
// Only select selectable objects.
|
313
313
|
objects = objects.filter(obj => obj.isSelectable());
|
314
|
+
// Sort by z-index
|
315
|
+
objects.sort((a, b) => a.getZIndex() - b.getZIndex());
|
316
|
+
// Remove duplicates
|
317
|
+
objects = objects.filter((current, idx) => {
|
318
|
+
if (idx > 0) {
|
319
|
+
return current !== objects[idx - 1];
|
320
|
+
}
|
321
|
+
return true;
|
322
|
+
});
|
314
323
|
let bbox = null;
|
315
324
|
for (const object of objects) {
|
316
325
|
if (bbox) {
|
@@ -0,0 +1,13 @@
|
|
1
|
+
const fileToBase64 = (file, onprogress) => {
|
2
|
+
const reader = new FileReader();
|
3
|
+
return new Promise((resolve, reject) => {
|
4
|
+
reader.onload = () => resolve(reader.result);
|
5
|
+
reader.onerror = reject;
|
6
|
+
reader.onabort = reject;
|
7
|
+
reader.onprogress = (evt) => {
|
8
|
+
onprogress === null || onprogress === void 0 ? void 0 : onprogress(evt);
|
9
|
+
};
|
10
|
+
reader.readAsDataURL(file);
|
11
|
+
});
|
12
|
+
};
|
13
|
+
export default fileToBase64;
|
package/package.json
CHANGED