js-draw 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/translation.yml +72 -0
- package/CHANGELOG.md +7 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +3 -1
- package/dist/src/Editor.js +52 -15
- 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/PanZoom.d.ts +2 -1
- package/dist/src/tools/PanZoom.js +43 -17
- package/dist/src/tools/PasteHandler.js +1 -39
- 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 +11 -11
- package/src/Editor.ts +66 -16
- 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/PanZoom.test.ts +65 -0
- package/src/tools/PanZoom.ts +46 -14
- package/src/tools/PasteHandler.ts +2 -51
- package/src/util/fileToBase64.ts +18 -0
- package/src/util/waitForTimeout.ts +9 -0
@@ -0,0 +1,222 @@
|
|
1
|
+
import ImageComponent from '../../components/ImageComponent';
|
2
|
+
import Editor from '../../Editor';
|
3
|
+
import Erase from '../../commands/Erase';
|
4
|
+
import EditorImage from '../../EditorImage';
|
5
|
+
import { SelectionTool, uniteCommands } from '../../lib';
|
6
|
+
import Mat33 from '../../math/Mat33';
|
7
|
+
import fileToBase64 from '../../util/fileToBase64';
|
8
|
+
import { ToolbarLocalization } from '../localization';
|
9
|
+
import ActionButtonWidget from './ActionButtonWidget';
|
10
|
+
|
11
|
+
export default class InsertImageWidget extends ActionButtonWidget {
|
12
|
+
private imageSelectionOverlay: HTMLElement;
|
13
|
+
private imagePreview: HTMLImageElement;
|
14
|
+
private imageFileInput: HTMLInputElement;
|
15
|
+
private imageAltTextInput: HTMLInputElement;
|
16
|
+
private statusView: HTMLElement;
|
17
|
+
private imageBase64URL: string|null;
|
18
|
+
private submitButton: HTMLButtonElement;
|
19
|
+
|
20
|
+
public constructor(editor: Editor, localization?: ToolbarLocalization) {
|
21
|
+
localization ??= editor.localization;
|
22
|
+
|
23
|
+
super(editor,
|
24
|
+
'insert-image-widget',
|
25
|
+
() => editor.icons.makeInsertImageIcon(),
|
26
|
+
localization.image,
|
27
|
+
() => this.onClicked()
|
28
|
+
);
|
29
|
+
|
30
|
+
this.imageSelectionOverlay = document.createElement('div');
|
31
|
+
this.imageSelectionOverlay.classList.add('toolbar-image-selection-overlay');
|
32
|
+
this.fillOverlay();
|
33
|
+
|
34
|
+
this.editor.createHTMLOverlay(this.imageSelectionOverlay);
|
35
|
+
this.imageSelectionOverlay.style.display = 'none';
|
36
|
+
}
|
37
|
+
|
38
|
+
private static nextInputId = 0;
|
39
|
+
|
40
|
+
private fillOverlay() {
|
41
|
+
const container = document.createElement('div');
|
42
|
+
|
43
|
+
const chooseImageRow = document.createElement('div');
|
44
|
+
const altTextRow = document.createElement('div');
|
45
|
+
this.imagePreview = document.createElement('img');
|
46
|
+
this.statusView = document.createElement('div');
|
47
|
+
const actionButtonRow = document.createElement('div');
|
48
|
+
|
49
|
+
actionButtonRow.classList.add('action-button-row');
|
50
|
+
|
51
|
+
this.submitButton = document.createElement('button');
|
52
|
+
const cancelButton = document.createElement('button');
|
53
|
+
|
54
|
+
this.imageFileInput = document.createElement('input');
|
55
|
+
this.imageAltTextInput = document.createElement('input');
|
56
|
+
const imageFileInputLabel = document.createElement('label');
|
57
|
+
const imageAltTextLabel = document.createElement('label');
|
58
|
+
|
59
|
+
const fileInputId = `insert-image-file-input-${InsertImageWidget.nextInputId ++}`;
|
60
|
+
const altTextInputId = `insert-image-alt-text-input-${InsertImageWidget.nextInputId++}`;
|
61
|
+
|
62
|
+
this.imageFileInput.setAttribute('id', fileInputId);
|
63
|
+
this.imageAltTextInput.setAttribute('id', altTextInputId);
|
64
|
+
imageAltTextLabel.htmlFor = altTextInputId;
|
65
|
+
imageFileInputLabel.htmlFor = fileInputId;
|
66
|
+
|
67
|
+
this.imageFileInput.accept = 'image/*';
|
68
|
+
|
69
|
+
imageAltTextLabel.innerText = this.localizationTable.inputAltText;
|
70
|
+
imageFileInputLabel.innerText = this.localizationTable.chooseFile;
|
71
|
+
|
72
|
+
this.imageFileInput.type = 'file';
|
73
|
+
this.imageAltTextInput.type = 'text';
|
74
|
+
|
75
|
+
this.statusView.setAttribute('aria-live', 'polite');
|
76
|
+
|
77
|
+
cancelButton.innerText = this.localizationTable.cancel;
|
78
|
+
this.submitButton.innerText = this.localizationTable.submit;
|
79
|
+
|
80
|
+
this.imageFileInput.onchange = async () => {
|
81
|
+
if (this.imageFileInput.value === '' || !this.imageFileInput.files || !this.imageFileInput.files[0]) {
|
82
|
+
this.imagePreview.style.display = 'none';
|
83
|
+
this.submitButton.disabled = true;
|
84
|
+
return;
|
85
|
+
}
|
86
|
+
|
87
|
+
this.imagePreview.style.display = 'block';
|
88
|
+
|
89
|
+
const image = this.imageFileInput.files[0];
|
90
|
+
|
91
|
+
let data: string|null = null;
|
92
|
+
|
93
|
+
try {
|
94
|
+
data = await fileToBase64(image);
|
95
|
+
} catch(e) {
|
96
|
+
this.statusView.innerText = this.localizationTable.imageLoadError(e);
|
97
|
+
}
|
98
|
+
|
99
|
+
this.imageBase64URL = data;
|
100
|
+
|
101
|
+
if (data) {
|
102
|
+
this.imagePreview.src = data;
|
103
|
+
this.submitButton.disabled = false;
|
104
|
+
this.updateImageSizeDisplay();
|
105
|
+
} else {
|
106
|
+
this.submitButton.disabled = true;
|
107
|
+
this.statusView.innerText = '';
|
108
|
+
}
|
109
|
+
};
|
110
|
+
|
111
|
+
cancelButton.onclick = () => {
|
112
|
+
this.hideDialog();
|
113
|
+
};
|
114
|
+
|
115
|
+
this.imageSelectionOverlay.onclick = (evt: MouseEvent) => {
|
116
|
+
// If clicking on the backdrop
|
117
|
+
if (evt.target === this.imageSelectionOverlay) {
|
118
|
+
this.hideDialog();
|
119
|
+
}
|
120
|
+
};
|
121
|
+
|
122
|
+
chooseImageRow.replaceChildren(imageFileInputLabel, this.imageFileInput);
|
123
|
+
altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
|
124
|
+
actionButtonRow.replaceChildren(cancelButton, this.submitButton);
|
125
|
+
|
126
|
+
container.replaceChildren(
|
127
|
+
chooseImageRow, altTextRow, this.imagePreview, this.statusView, actionButtonRow
|
128
|
+
);
|
129
|
+
|
130
|
+
this.imageSelectionOverlay.replaceChildren(container);
|
131
|
+
}
|
132
|
+
|
133
|
+
private hideDialog() {
|
134
|
+
this.imageSelectionOverlay.style.display = 'none';
|
135
|
+
}
|
136
|
+
|
137
|
+
private updateImageSizeDisplay() {
|
138
|
+
const imageData = this.imageBase64URL ?? '';
|
139
|
+
|
140
|
+
const sizeInKiB = imageData.length / 1024;
|
141
|
+
const sizeInMiB = sizeInKiB / 1024;
|
142
|
+
|
143
|
+
let units = 'KiB';
|
144
|
+
let size = sizeInKiB;
|
145
|
+
|
146
|
+
if (sizeInMiB >= 1) {
|
147
|
+
size = sizeInMiB;
|
148
|
+
units = 'MiB';
|
149
|
+
}
|
150
|
+
|
151
|
+
this.statusView.innerText = this.localizationTable.imageSize(
|
152
|
+
Math.round(size), units
|
153
|
+
);
|
154
|
+
}
|
155
|
+
|
156
|
+
private clearInputs() {
|
157
|
+
this.imageFileInput.value = '';
|
158
|
+
this.imageAltTextInput.value = '';
|
159
|
+
this.imagePreview.style.display = 'none';
|
160
|
+
this.submitButton.disabled = true;
|
161
|
+
this.statusView.innerText = '';
|
162
|
+
}
|
163
|
+
|
164
|
+
private onClicked() {
|
165
|
+
this.imageSelectionOverlay.style.display = '';
|
166
|
+
this.clearInputs();
|
167
|
+
this.imageFileInput.focus();
|
168
|
+
|
169
|
+
const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
|
170
|
+
const selectedObjects = selectionTools.map(tool => tool.getSelectedObjects()).flat();
|
171
|
+
|
172
|
+
let editingImage: ImageComponent|null = null;
|
173
|
+
if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
|
174
|
+
editingImage = selectedObjects[0];
|
175
|
+
|
176
|
+
this.imageAltTextInput.value = editingImage.getAltText() ?? '';
|
177
|
+
this.imagePreview.style.display = 'block';
|
178
|
+
this.submitButton.disabled = false;
|
179
|
+
|
180
|
+
this.imageBase64URL = editingImage.getURL();
|
181
|
+
this.imagePreview.src = this.imageBase64URL;
|
182
|
+
|
183
|
+
this.updateImageSizeDisplay();
|
184
|
+
} else {
|
185
|
+
selectionTools.forEach(tool => tool.clearSelection());
|
186
|
+
}
|
187
|
+
|
188
|
+
this.submitButton.onclick = async () => {
|
189
|
+
if (!this.imageBase64URL) {
|
190
|
+
return;
|
191
|
+
}
|
192
|
+
|
193
|
+
const image = new Image();
|
194
|
+
image.src = this.imageBase64URL;
|
195
|
+
image.setAttribute('alt', this.imageAltTextInput.value);
|
196
|
+
|
197
|
+
const component = await ImageComponent.fromImage(image, Mat33.identity);
|
198
|
+
|
199
|
+
if (component.getBBox().area === 0) {
|
200
|
+
this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
|
201
|
+
return;
|
202
|
+
}
|
203
|
+
|
204
|
+
this.imageSelectionOverlay.style.display = 'none';
|
205
|
+
|
206
|
+
if (editingImage) {
|
207
|
+
const eraseCommand = new Erase([ editingImage ]);
|
208
|
+
|
209
|
+
await this.editor.dispatch(uniteCommands([
|
210
|
+
EditorImage.addElement(component),
|
211
|
+
component.transformBy(editingImage.getTransformation()),
|
212
|
+
component.setZIndex(editingImage.getZIndex()),
|
213
|
+
eraseCommand,
|
214
|
+
]));
|
215
|
+
|
216
|
+
selectionTools[0]?.setSelection([ component ]);
|
217
|
+
} else {
|
218
|
+
await this.editor.addAndCenterComponents([ component ]);
|
219
|
+
}
|
220
|
+
};
|
221
|
+
}
|
222
|
+
}
|
@@ -8,3 +8,5 @@ export { default as TextToolWidget } from './TextToolWidget';
|
|
8
8
|
export { default as HandToolWidget } from './HandToolWidget';
|
9
9
|
export { default as SelectionToolWidget } from './SelectionToolWidget';
|
10
10
|
export { default as EraserToolWidget } from './EraserToolWidget';
|
11
|
+
|
12
|
+
export { default as InsertImageWidget } from './InsertImageWidget';
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
import Editor from '../Editor';
|
3
|
+
import { Mat33, Pointer, PointerDevice, Vec2 } from '../lib';
|
4
|
+
import createEditor from '../testing/createEditor';
|
5
|
+
import { InputEvtType } from '../types';
|
6
|
+
import waitForTimeout from '../util/waitForTimeout';
|
7
|
+
import PanZoom from './PanZoom';
|
8
|
+
|
9
|
+
const selectPanZom = (editor: Editor): PanZoom => {
|
10
|
+
const primaryTools = editor.toolController.getPrimaryTools();
|
11
|
+
const panZoom = primaryTools.filter(tool => tool instanceof PanZoom)[0] as PanZoom;
|
12
|
+
panZoom.setEnabled(true);
|
13
|
+
return panZoom;
|
14
|
+
};
|
15
|
+
|
16
|
+
const sendTouchEvent = (
|
17
|
+
editor: Editor,
|
18
|
+
eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
|
19
|
+
screenPos: Vec2,
|
20
|
+
) => {
|
21
|
+
const canvasPos = editor.viewport.screenToCanvas(screenPos);
|
22
|
+
|
23
|
+
const ptrId = 0;
|
24
|
+
const mainPointer = Pointer.ofCanvasPoint(
|
25
|
+
canvasPos, eventType !== InputEvtType.PointerUpEvt, editor.viewport, ptrId, PointerDevice.Touch
|
26
|
+
);
|
27
|
+
|
28
|
+
editor.toolController.dispatchInputEvent({
|
29
|
+
kind: eventType,
|
30
|
+
allPointers: [
|
31
|
+
mainPointer,
|
32
|
+
],
|
33
|
+
current: mainPointer,
|
34
|
+
});
|
35
|
+
};
|
36
|
+
|
37
|
+
describe('PanZoom', () => {
|
38
|
+
it('touch and drag should pan, then inertial scroll', async () => {
|
39
|
+
const editor = createEditor();
|
40
|
+
selectPanZom(editor);
|
41
|
+
editor.viewport.resetTransform(Mat33.identity);
|
42
|
+
|
43
|
+
const origTranslation = editor.viewport.canvasToScreen(Vec2.zero);
|
44
|
+
|
45
|
+
sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
46
|
+
for (let i = 1; i <= 10; i++) {
|
47
|
+
jest.advanceTimersByTime(10);
|
48
|
+
sendTouchEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(i * 10, 0));
|
49
|
+
}
|
50
|
+
|
51
|
+
// Use real timers -- we need to be able to start the inertial scroller.
|
52
|
+
jest.useRealTimers();
|
53
|
+
sendTouchEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(100, 0));
|
54
|
+
|
55
|
+
const updatedTranslation = editor.viewport.canvasToScreen(Vec2.zero);
|
56
|
+
expect(updatedTranslation.minus(origTranslation).magnitude()).toBe(100);
|
57
|
+
|
58
|
+
await waitForTimeout(600); // ms
|
59
|
+
jest.useFakeTimers();
|
60
|
+
|
61
|
+
// Should inertial scroll
|
62
|
+
const afterDelayTranslation = editor.viewport.canvasToScreen(Vec2.zero);
|
63
|
+
expect(afterDelayTranslation.minus(updatedTranslation).magnitude()).toBeGreaterThan(0);
|
64
|
+
});
|
65
|
+
});
|
package/src/tools/PanZoom.ts
CHANGED
@@ -30,6 +30,7 @@ type ScrollByCallback = (delta: Vec2) => void;
|
|
30
30
|
|
31
31
|
class InertialScroller {
|
32
32
|
private running: boolean = false;
|
33
|
+
private currentVelocity: Vec2;
|
33
34
|
|
34
35
|
public constructor(
|
35
36
|
private initialVelocity: Vec2,
|
@@ -44,22 +45,22 @@ class InertialScroller {
|
|
44
45
|
return;
|
45
46
|
}
|
46
47
|
|
47
|
-
|
48
|
+
this.currentVelocity = this.initialVelocity;
|
48
49
|
let lastTime = (new Date()).getTime();
|
49
50
|
this.running = true;
|
50
51
|
|
51
|
-
const maxSpeed =
|
52
|
+
const maxSpeed = 5000; // units/s
|
52
53
|
const minSpeed = 200; // units/s
|
53
|
-
if (currentVelocity.magnitude() > maxSpeed) {
|
54
|
-
currentVelocity = currentVelocity.normalized().times(maxSpeed);
|
54
|
+
if (this.currentVelocity.magnitude() > maxSpeed) {
|
55
|
+
this.currentVelocity = this.currentVelocity.normalized().times(maxSpeed);
|
55
56
|
}
|
56
57
|
|
57
|
-
while (this.running && currentVelocity.magnitude() > minSpeed) {
|
58
|
+
while (this.running && this.currentVelocity.magnitude() > minSpeed) {
|
58
59
|
const nowTime = (new Date()).getTime();
|
59
60
|
const dt = (nowTime - lastTime) / 1000;
|
60
61
|
|
61
|
-
currentVelocity = currentVelocity.times(Math.pow(1/8, dt));
|
62
|
-
this.scrollBy(currentVelocity.times(dt));
|
62
|
+
this.currentVelocity = this.currentVelocity.times(Math.pow(1/8, dt));
|
63
|
+
this.scrollBy(this.currentVelocity.times(dt));
|
63
64
|
|
64
65
|
await untilNextAnimationFrame();
|
65
66
|
lastTime = nowTime;
|
@@ -70,6 +71,14 @@ class InertialScroller {
|
|
70
71
|
}
|
71
72
|
}
|
72
73
|
|
74
|
+
public getCurrentVelocity(): Vec2|null {
|
75
|
+
if (!this.running) {
|
76
|
+
return null;
|
77
|
+
}
|
78
|
+
|
79
|
+
return this.currentVelocity;
|
80
|
+
}
|
81
|
+
|
73
82
|
public stop(): void {
|
74
83
|
if (this.running) {
|
75
84
|
this.running = false;
|
@@ -85,6 +94,7 @@ export default class PanZoom extends BaseTool {
|
|
85
94
|
private lastDist: number;
|
86
95
|
private lastScreenCenter: Point2;
|
87
96
|
private lastTimestamp: number;
|
97
|
+
private lastPointerDownTimestamp: number = 0;
|
88
98
|
|
89
99
|
private inertialScroller: InertialScroller|null = null;
|
90
100
|
private velocity: Vec2|null = null;
|
@@ -108,10 +118,14 @@ export default class PanZoom extends BaseTool {
|
|
108
118
|
return pointers.every(pointer => pointer.device === kind);
|
109
119
|
}
|
110
120
|
|
111
|
-
public onPointerDown({ allPointers: pointers }: PointerEvt): boolean {
|
121
|
+
public onPointerDown({ allPointers: pointers, current: currentPointer }: PointerEvt): boolean {
|
112
122
|
let handlingGesture = false;
|
113
123
|
|
124
|
+
const inertialScrollerVelocity = this.inertialScroller?.getCurrentVelocity() ?? Vec2.zero;
|
114
125
|
this.inertialScroller?.stop();
|
126
|
+
this.velocity = inertialScrollerVelocity;
|
127
|
+
|
128
|
+
this.lastPointerDownTimestamp = currentPointer.timeStamp;
|
115
129
|
|
116
130
|
const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
|
117
131
|
const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
|
@@ -142,17 +156,19 @@ export default class PanZoom extends BaseTool {
|
|
142
156
|
|
143
157
|
private updateVelocity(currentCenter: Point2) {
|
144
158
|
const deltaPos = currentCenter.minus(this.lastScreenCenter);
|
145
|
-
|
159
|
+
let deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
|
146
160
|
|
161
|
+
// Ignore duplicate events, unless there has been enough time between them.
|
162
|
+
if (deltaPos.magnitude() === 0 && deltaTime < 0.1) {
|
163
|
+
return;
|
164
|
+
}
|
147
165
|
// We divide by deltaTime. Don't divide by zero.
|
148
166
|
if (deltaTime === 0) {
|
149
167
|
return;
|
150
168
|
}
|
151
169
|
|
152
|
-
//
|
153
|
-
|
154
|
-
return;
|
155
|
-
}
|
170
|
+
// Don't divide by almost zero, either
|
171
|
+
deltaTime = Math.max(deltaTime, 0.01);
|
156
172
|
|
157
173
|
const currentVelocity = deltaPos.times(1 / deltaTime);
|
158
174
|
let smoothedVelocity = currentVelocity;
|
@@ -233,15 +249,31 @@ export default class PanZoom extends BaseTool {
|
|
233
249
|
this.velocity = Vec2.zero;
|
234
250
|
};
|
235
251
|
|
252
|
+
const minInertialScrollDt = 30;
|
236
253
|
const shouldInertialScroll =
|
237
|
-
event.current.device === PointerDevice.Touch
|
254
|
+
event.current.device === PointerDevice.Touch
|
255
|
+
&& event.allPointers.length === 1
|
256
|
+
&& this.velocity !== null
|
257
|
+
&& event.current.timeStamp - this.lastPointerDownTimestamp > minInertialScrollDt;
|
238
258
|
|
239
259
|
if (shouldInertialScroll && this.velocity !== null) {
|
260
|
+
const oldVelocity = this.velocity;
|
261
|
+
|
240
262
|
// If the user drags the screen, then stops, then lifts the pointer,
|
241
263
|
// we want the final velocity to reflect the stop at the end (so the velocity
|
242
264
|
// should be near zero). Handle this:
|
243
265
|
this.updateVelocity(event.current.screenPos);
|
244
266
|
|
267
|
+
// Work around an input issue. Some devices that disable the touchscreen when a stylus
|
268
|
+
// comes near the screen fire a touch-end event at the position of the stylus when a
|
269
|
+
// touch gesture is canceled. Because the stylus is often far away from the last touch,
|
270
|
+
// this causes a great displacement between the second-to-last (from the touchscreen) and
|
271
|
+
// last (from the pen that is now near the screen) events. Only allow velocity to decrease
|
272
|
+
// to work around this:
|
273
|
+
if (oldVelocity.magnitude() < this.velocity.magnitude()) {
|
274
|
+
this.velocity = oldVelocity;
|
275
|
+
}
|
276
|
+
|
245
277
|
// Cancel any ongoing inertial scrolling.
|
246
278
|
this.inertialScroller?.stop();
|
247
279
|
|
@@ -5,18 +5,14 @@
|
|
5
5
|
|
6
6
|
import Editor from '../Editor';
|
7
7
|
import { AbstractComponent, TextComponent } from '../components/lib';
|
8
|
-
import { Command, uniteCommands } from '../commands/lib';
|
9
8
|
import SVGLoader from '../SVGLoader';
|
10
9
|
import { PasteEvent } from '../types';
|
11
|
-
import { Mat33
|
10
|
+
import { Mat33 } from '../math/lib';
|
12
11
|
import BaseTool from './BaseTool';
|
13
|
-
import EditorImage from '../EditorImage';
|
14
|
-
import SelectionTool from './SelectionTool/SelectionTool';
|
15
12
|
import TextTool from './TextTool';
|
16
13
|
import Color4 from '../Color4';
|
17
14
|
import { TextStyle } from '../components/TextComponent';
|
18
15
|
import ImageComponent from '../components/ImageComponent';
|
19
|
-
import Viewport from '../Viewport';
|
20
16
|
|
21
17
|
// { @inheritDoc PasteHandler! }
|
22
18
|
export default class PasteHandler extends BaseTool {
|
@@ -44,52 +40,7 @@ export default class PasteHandler extends BaseTool {
|
|
44
40
|
}
|
45
41
|
|
46
42
|
private async addComponentsFromPaste(components: AbstractComponent[]) {
|
47
|
-
|
48
|
-
for (const component of components) {
|
49
|
-
if (bbox) {
|
50
|
-
bbox = bbox.union(component.getBBox());
|
51
|
-
} else {
|
52
|
-
bbox = component.getBBox();
|
53
|
-
}
|
54
|
-
}
|
55
|
-
|
56
|
-
if (!bbox) {
|
57
|
-
return;
|
58
|
-
}
|
59
|
-
|
60
|
-
// Find a transform that scales/moves bbox onto the screen.
|
61
|
-
const visibleRect = this.editor.viewport.visibleRect;
|
62
|
-
const scaleRatioX = visibleRect.width / bbox.width;
|
63
|
-
const scaleRatioY = visibleRect.height / bbox.height;
|
64
|
-
|
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
|
-
|
71
|
-
scaleRatio = Viewport.roundScaleRatio(scaleRatio);
|
72
|
-
|
73
|
-
const transfm = Mat33.translation(
|
74
|
-
visibleRect.center.minus(bbox.center)
|
75
|
-
).rightMul(
|
76
|
-
Mat33.scaling2D(scaleRatio, bbox.center)
|
77
|
-
);
|
78
|
-
|
79
|
-
const commands: Command[] = [];
|
80
|
-
for (const component of components) {
|
81
|
-
// To allow deserialization, we need to add first, then transform.
|
82
|
-
commands.push(EditorImage.addElement(component));
|
83
|
-
commands.push(component.transformBy(transfm));
|
84
|
-
}
|
85
|
-
|
86
|
-
const applyChunkSize = 100;
|
87
|
-
this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
|
88
|
-
|
89
|
-
for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
|
90
|
-
selectionTool.setEnabled(true);
|
91
|
-
selectionTool.setSelection(components);
|
92
|
-
}
|
43
|
+
await this.editor.addAndCenterComponents(components);
|
93
44
|
}
|
94
45
|
|
95
46
|
private async doSVGPaste(data: string) {
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
type ProgressListener = (evt: ProgressEvent<FileReader>)=> void;
|
3
|
+
const fileToBase64 = (file: File, onprogress?: ProgressListener): Promise<string|null> => {
|
4
|
+
const reader = new FileReader();
|
5
|
+
|
6
|
+
return new Promise((resolve: (result: string|null)=>void, reject) => {
|
7
|
+
reader.onload = () => resolve(reader.result as string|null);
|
8
|
+
reader.onerror = reject;
|
9
|
+
reader.onabort = reject;
|
10
|
+
reader.onprogress = (evt) => {
|
11
|
+
onprogress?.(evt);
|
12
|
+
};
|
13
|
+
|
14
|
+
reader.readAsDataURL(file);
|
15
|
+
});
|
16
|
+
};
|
17
|
+
|
18
|
+
export default fileToBase64;
|