js-draw 0.11.3 → 0.12.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/CHANGELOG.md +6 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +1 -0
- package/dist/src/Color4.js +1 -0
- package/dist/src/Editor.js +4 -5
- package/dist/src/SVGLoader.js +43 -35
- package/dist/src/components/AbstractComponent.d.ts +1 -0
- package/dist/src/components/AbstractComponent.js +15 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +51 -0
- package/dist/src/toolbar/HTMLToolbar.js +63 -5
- package/dist/src/toolbar/IconProvider.d.ts +1 -1
- package/dist/src/toolbar/IconProvider.js +33 -6
- package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +8 -1
- package/dist/src/toolbar/widgets/EraserToolWidget.js +45 -4
- package/dist/src/toolbar/widgets/PenToolWidget.js +2 -2
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +12 -3
- package/dist/src/tools/Eraser.d.ts +10 -1
- package/dist/src/tools/Eraser.js +65 -13
- package/dist/src/tools/SelectionTool/Selection.d.ts +4 -1
- package/dist/src/tools/SelectionTool/Selection.js +64 -27
- package/dist/src/tools/SelectionTool/SelectionTool.js +3 -1
- package/dist/src/tools/TextTool.js +10 -6
- package/dist/src/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/Color4.ts +1 -0
- package/src/Editor.ts +3 -4
- package/src/SVGLoader.ts +14 -14
- package/src/components/AbstractComponent.ts +19 -0
- package/src/toolbar/HTMLToolbar.ts +81 -5
- package/src/toolbar/IconProvider.ts +34 -6
- package/src/toolbar/widgets/EraserToolWidget.ts +64 -5
- package/src/toolbar/widgets/PenToolWidget.ts +2 -2
- package/src/toolbar/widgets/SelectionToolWidget.ts +2 -2
- package/src/tools/Eraser.test.ts +79 -0
- package/src/tools/Eraser.ts +81 -17
- package/src/tools/SelectionTool/Selection.ts +73 -23
- package/src/tools/SelectionTool/SelectionTool.test.ts +138 -21
- package/src/tools/SelectionTool/SelectionTool.ts +3 -1
- package/src/tools/TextTool.ts +14 -8
- package/src/types.ts +2 -2
package/dist/src/tools/Eraser.js
CHANGED
@@ -1,31 +1,55 @@
|
|
1
|
+
import { EditorEventType } from '../types';
|
1
2
|
import BaseTool from './BaseTool';
|
3
|
+
import { Vec2 } from '../math/Vec2';
|
2
4
|
import LineSegment2 from '../math/LineSegment2';
|
3
5
|
import Erase from '../commands/Erase';
|
4
6
|
import { PointerDevice } from '../Pointer';
|
7
|
+
import Color4 from '../Color4';
|
8
|
+
import Rect2 from '../math/Rect2';
|
5
9
|
export default class Eraser extends BaseTool {
|
6
10
|
constructor(editor, description) {
|
7
11
|
super(editor.notifier, description);
|
8
12
|
this.editor = editor;
|
13
|
+
this.lastPoint = null;
|
14
|
+
this.isFirstEraseEvt = true;
|
15
|
+
this.thickness = 10;
|
9
16
|
// Commands that each remove one element
|
10
17
|
this.partialCommands = [];
|
11
18
|
}
|
12
|
-
|
13
|
-
|
14
|
-
this.lastPoint = event.current.canvasPos;
|
15
|
-
this.toRemove = [];
|
16
|
-
return true;
|
17
|
-
}
|
18
|
-
return false;
|
19
|
+
clearPreview() {
|
20
|
+
this.editor.clearWetInk();
|
19
21
|
}
|
20
|
-
|
21
|
-
|
22
|
-
|
22
|
+
getSizeOnCanvas() {
|
23
|
+
return this.thickness / this.editor.viewport.getScaleFactor();
|
24
|
+
}
|
25
|
+
drawPreviewAt(point) {
|
26
|
+
this.clearPreview();
|
27
|
+
const size = this.getSizeOnCanvas();
|
28
|
+
const renderer = this.editor.display.getWetInkRenderer();
|
29
|
+
const rect = this.getEraserRect(point);
|
30
|
+
const fill = {
|
31
|
+
fill: Color4.gray,
|
32
|
+
};
|
33
|
+
renderer.drawRect(rect, size / 4, fill);
|
34
|
+
}
|
35
|
+
getEraserRect(centerPoint) {
|
36
|
+
const size = this.getSizeOnCanvas();
|
37
|
+
const halfSize = Vec2.of(size / 2, size / 2);
|
38
|
+
return Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
|
39
|
+
}
|
40
|
+
eraseTo(currentPoint) {
|
41
|
+
if (!this.isFirstEraseEvt && currentPoint.minus(this.lastPoint).magnitude() === 0) {
|
23
42
|
return;
|
24
43
|
}
|
44
|
+
this.isFirstEraseEvt = false;
|
45
|
+
// Currently only objects within eraserRect or that intersect a straight line
|
46
|
+
// from the center of the current rect to the previous are erased. TODO: Erase
|
47
|
+
// all objects as if there were pointerMove events between the two points.
|
48
|
+
const eraserRect = this.getEraserRect(currentPoint);
|
25
49
|
const line = new LineSegment2(this.lastPoint, currentPoint);
|
26
|
-
const region = line.bbox;
|
50
|
+
const region = Rect2.union(line.bbox, eraserRect);
|
27
51
|
const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
|
28
|
-
return component.intersects(line);
|
52
|
+
return component.intersects(line) || component.intersectsRect(eraserRect);
|
29
53
|
});
|
30
54
|
// Remove any intersecting elements.
|
31
55
|
this.toRemove.push(...intersectingElems);
|
@@ -33,9 +57,25 @@ export default class Eraser extends BaseTool {
|
|
33
57
|
const newPartialCommands = intersectingElems.map(elem => new Erase([elem]));
|
34
58
|
newPartialCommands.forEach(cmd => cmd.apply(this.editor));
|
35
59
|
this.partialCommands.push(...newPartialCommands);
|
60
|
+
this.drawPreviewAt(currentPoint);
|
36
61
|
this.lastPoint = currentPoint;
|
37
62
|
}
|
38
|
-
|
63
|
+
onPointerDown(event) {
|
64
|
+
if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
|
65
|
+
this.lastPoint = event.current.canvasPos;
|
66
|
+
this.toRemove = [];
|
67
|
+
this.isFirstEraseEvt = true;
|
68
|
+
this.drawPreviewAt(event.current.canvasPos);
|
69
|
+
return true;
|
70
|
+
}
|
71
|
+
return false;
|
72
|
+
}
|
73
|
+
onPointerMove(event) {
|
74
|
+
const currentPoint = event.current.canvasPos;
|
75
|
+
this.eraseTo(currentPoint);
|
76
|
+
}
|
77
|
+
onPointerUp(event) {
|
78
|
+
this.eraseTo(event.current.canvasPos);
|
39
79
|
if (this.toRemove.length > 0) {
|
40
80
|
// Undo commands for each individual component and unite into a single command.
|
41
81
|
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
@@ -43,9 +83,21 @@ export default class Eraser extends BaseTool {
|
|
43
83
|
const command = new Erase(this.toRemove);
|
44
84
|
this.editor.dispatch(command); // dispatch: Makes undo-able.
|
45
85
|
}
|
86
|
+
this.clearPreview();
|
46
87
|
}
|
47
88
|
onGestureCancel() {
|
48
89
|
this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
|
49
90
|
this.partialCommands = [];
|
91
|
+
this.clearPreview();
|
92
|
+
}
|
93
|
+
getThickness() {
|
94
|
+
return this.thickness;
|
95
|
+
}
|
96
|
+
setThickness(thickness) {
|
97
|
+
this.thickness = thickness;
|
98
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
99
|
+
kind: EditorEventType.ToolUpdated,
|
100
|
+
tool: this,
|
101
|
+
});
|
50
102
|
}
|
51
103
|
}
|
@@ -19,6 +19,7 @@ export default class Selection {
|
|
19
19
|
private backgroundElem;
|
20
20
|
private hasParent;
|
21
21
|
constructor(startPoint: Point2, editor: Editor);
|
22
|
+
getBackgroundElem(): HTMLElement;
|
22
23
|
getTransform(): Mat33;
|
23
24
|
get preTransformRegion(): Rect2;
|
24
25
|
get region(): Rect2;
|
@@ -36,7 +37,9 @@ export default class Selection {
|
|
36
37
|
getMinCanvasSize(): number;
|
37
38
|
getSelectedItemCount(): number;
|
38
39
|
updateUI(): void;
|
40
|
+
private removedFromImage;
|
39
41
|
private addRemoveSelectionFromImage;
|
42
|
+
private removeDeletedElemsFromSelection;
|
40
43
|
private targetHandle;
|
41
44
|
private backgroundDragging;
|
42
45
|
onDragStart(pointer: Pointer, target: EventTarget): boolean;
|
@@ -45,7 +48,7 @@ export default class Selection {
|
|
45
48
|
onDragCancel(): void;
|
46
49
|
scrollTo(): Promise<void>;
|
47
50
|
deleteSelectedObjects(): Command;
|
48
|
-
duplicateSelectedObjects(): Command
|
51
|
+
duplicateSelectedObjects(): Promise<Command>;
|
49
52
|
addTo(elem: HTMLElement): void;
|
50
53
|
setToPoint(point: Point2): void;
|
51
54
|
cancelSelection(): void;
|
@@ -32,6 +32,8 @@ export default class Selection {
|
|
32
32
|
this.transform = Mat33.identity;
|
33
33
|
this.selectedElems = [];
|
34
34
|
this.hasParent = true;
|
35
|
+
// Maps IDs to whether we removed the component from the image
|
36
|
+
this.removedFromImage = {};
|
35
37
|
this.targetHandle = null;
|
36
38
|
this.backgroundDragging = false;
|
37
39
|
this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
|
@@ -58,6 +60,10 @@ export default class Selection {
|
|
58
60
|
handle.addTo(this.backgroundElem);
|
59
61
|
}
|
60
62
|
}
|
63
|
+
// @internal Intended for unit tests
|
64
|
+
getBackgroundElem() {
|
65
|
+
return this.backgroundElem;
|
66
|
+
}
|
61
67
|
getTransform() {
|
62
68
|
return this.transform;
|
63
69
|
}
|
@@ -140,16 +146,7 @@ export default class Selection {
|
|
140
146
|
singleItemSelectionMode = true;
|
141
147
|
}
|
142
148
|
this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
|
143
|
-
|
144
|
-
return true;
|
145
|
-
}
|
146
|
-
// Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
|
147
|
-
// As such, test with more lines than just this' edges.
|
148
|
-
const testLines = [];
|
149
|
-
for (const subregion of this.region.divideIntoGrid(2, 2)) {
|
150
|
-
testLines.push(...subregion.getEdges());
|
151
|
-
}
|
152
|
-
return testLines.some(edge => elem.intersects(edge));
|
149
|
+
return elem.intersectsRect(this.region);
|
153
150
|
});
|
154
151
|
if (singleItemSelectionMode && this.selectedElems.length > 0) {
|
155
152
|
this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
|
@@ -215,30 +212,45 @@ export default class Selection {
|
|
215
212
|
//
|
216
213
|
// If removed from the image, selected elements are drawn as wet ink.
|
217
214
|
addRemoveSelectionFromImage(inImage) {
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
215
|
+
// Don't hide elements if doing so will be slow.
|
216
|
+
if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
|
217
|
+
return;
|
218
|
+
}
|
219
|
+
for (const elem of this.selectedElems) {
|
220
|
+
const parent = this.editor.image.findParent(elem);
|
221
|
+
if (!inImage && parent) {
|
222
|
+
this.removedFromImage[elem.getId()] = true;
|
223
|
+
parent.remove();
|
222
224
|
}
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
// visible,
|
230
|
-
else if (!parent) {
|
231
|
-
EditorImage.addElement(elem).apply(this.editor);
|
232
|
-
}
|
225
|
+
// If we're making things visible and the selected object wasn't previously
|
226
|
+
// visible,
|
227
|
+
else if (!parent && this.removedFromImage[elem.getId()]) {
|
228
|
+
EditorImage.addElement(elem).apply(this.editor);
|
229
|
+
this.removedFromImage[elem.getId()] = false;
|
230
|
+
delete this.removedFromImage[elem.getId()];
|
233
231
|
}
|
234
|
-
|
232
|
+
}
|
233
|
+
// Don't await queueRerender. If we're running in a test, the re-render might never
|
234
|
+
// happen.
|
235
|
+
this.editor.queueRerender().then(() => {
|
235
236
|
if (!inImage) {
|
236
237
|
this.previewTransformCmds();
|
237
238
|
}
|
238
239
|
});
|
239
240
|
}
|
241
|
+
removeDeletedElemsFromSelection() {
|
242
|
+
// Remove any deleted elements from the selection.
|
243
|
+
this.selectedElems = this.selectedElems.filter(elem => {
|
244
|
+
const hasParent = !!this.editor.image.findParent(elem);
|
245
|
+
// If we removed the element and haven't added it back yet, don't remove it
|
246
|
+
// from the selection.
|
247
|
+
const weRemoved = this.removedFromImage[elem.getId()];
|
248
|
+
return hasParent || weRemoved;
|
249
|
+
});
|
250
|
+
}
|
240
251
|
onDragStart(pointer, target) {
|
241
|
-
|
252
|
+
this.removeDeletedElemsFromSelection();
|
253
|
+
this.addRemoveSelectionFromImage(false);
|
242
254
|
for (const handle of this.handles) {
|
243
255
|
if (handle.isTarget(target)) {
|
244
256
|
handle.handleDragStart(pointer);
|
@@ -299,10 +311,34 @@ export default class Selection {
|
|
299
311
|
});
|
300
312
|
}
|
301
313
|
deleteSelectedObjects() {
|
314
|
+
if (this.backgroundDragging || this.targetHandle) {
|
315
|
+
this.onDragEnd();
|
316
|
+
}
|
302
317
|
return new Erase(this.selectedElems);
|
303
318
|
}
|
304
319
|
duplicateSelectedObjects() {
|
305
|
-
return
|
320
|
+
return __awaiter(this, void 0, void 0, function* () {
|
321
|
+
const wasTransforming = this.backgroundDragging || this.targetHandle;
|
322
|
+
let tmpApplyCommand = null;
|
323
|
+
if (wasTransforming) {
|
324
|
+
// Don't update the selection's focus when redoing/undoing
|
325
|
+
const selectionToUpdate = null;
|
326
|
+
tmpApplyCommand = new Selection.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform);
|
327
|
+
// Transform to ensure that the duplicates are in the correct location
|
328
|
+
yield tmpApplyCommand.apply(this.editor);
|
329
|
+
// Show items again
|
330
|
+
this.addRemoveSelectionFromImage(true);
|
331
|
+
}
|
332
|
+
const duplicateCommand = new Duplicate(this.selectedElems);
|
333
|
+
if (wasTransforming) {
|
334
|
+
// Move the selected objects back to the correct location.
|
335
|
+
yield (tmpApplyCommand === null || tmpApplyCommand === void 0 ? void 0 : tmpApplyCommand.unapply(this.editor));
|
336
|
+
this.addRemoveSelectionFromImage(false);
|
337
|
+
this.previewTransformCmds();
|
338
|
+
this.updateUI();
|
339
|
+
}
|
340
|
+
return duplicateCommand;
|
341
|
+
});
|
306
342
|
}
|
307
343
|
addTo(elem) {
|
308
344
|
if (this.container.parentElement) {
|
@@ -323,6 +359,7 @@ export default class Selection {
|
|
323
359
|
this.hasParent = false;
|
324
360
|
}
|
325
361
|
setSelectedObjects(objects, bbox) {
|
362
|
+
this.addRemoveSelectionFromImage(true);
|
326
363
|
this.originalRegion = bbox;
|
327
364
|
this.selectedElems = objects.filter(object => object.isSelectable());
|
328
365
|
this.updateUI();
|
@@ -241,7 +241,9 @@ export default class SelectionTool extends BaseTool {
|
|
241
241
|
}
|
242
242
|
else if (evt.ctrlKey) {
|
243
243
|
if (this.selectionBox && evt.key === 'd') {
|
244
|
-
this.
|
244
|
+
this.selectionBox.duplicateSelectedObjects().then(command => {
|
245
|
+
this.editor.dispatch(command);
|
246
|
+
});
|
245
247
|
return true;
|
246
248
|
}
|
247
249
|
else if (evt.key === 'a') {
|
@@ -69,12 +69,13 @@ export default class TextTool extends BaseTool {
|
|
69
69
|
flushInput(removeInput = true) {
|
70
70
|
if (this.textInputElem && this.textTargetPosition) {
|
71
71
|
const content = this.textInputElem.value.trimEnd();
|
72
|
+
this.textInputElem.value = '';
|
72
73
|
if (removeInput) {
|
73
|
-
|
74
|
+
// In some browsers, .remove() triggers a .blur event (synchronously).
|
75
|
+
// Clear this.textInputElem before removal
|
76
|
+
const input = this.textInputElem;
|
74
77
|
this.textInputElem = null;
|
75
|
-
|
76
|
-
else {
|
77
|
-
this.textInputElem.value = '';
|
78
|
+
input.remove();
|
78
79
|
}
|
79
80
|
if (content === '') {
|
80
81
|
return;
|
@@ -145,9 +146,12 @@ export default class TextTool extends BaseTool {
|
|
145
146
|
// Delay removing the input -- flushInput may be called within a blur()
|
146
147
|
// event handler
|
147
148
|
const removeInput = false;
|
148
|
-
this.flushInput(removeInput);
|
149
149
|
const input = this.textInputElem;
|
150
|
-
|
150
|
+
this.flushInput(removeInput);
|
151
|
+
this.textInputElem = null;
|
152
|
+
setTimeout(() => {
|
153
|
+
input === null || input === void 0 ? void 0 : input.remove();
|
154
|
+
}, 0);
|
151
155
|
};
|
152
156
|
this.textInputElem.onkeyup = (evt) => {
|
153
157
|
var _a, _b;
|
package/dist/src/types.d.ts
CHANGED
@@ -129,8 +129,8 @@ export interface ToolbarDropdownShownEvent {
|
|
129
129
|
readonly parentWidget: BaseWidget;
|
130
130
|
}
|
131
131
|
export type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent | ColorPickerToggled | ColorPickerColorSelected | ToolbarDropdownShownEvent;
|
132
|
-
export type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
|
133
|
-
export type ComponentAddedListener = (component: AbstractComponent) => void;
|
132
|
+
export type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null | void;
|
133
|
+
export type ComponentAddedListener = (component: AbstractComponent) => Promise<void> | void;
|
134
134
|
export type OnDetermineExportRectListener = (exportRect: Rect2) => void;
|
135
135
|
export interface ImageLoader {
|
136
136
|
start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener): Promise<void>;
|
package/package.json
CHANGED
package/src/Color4.ts
CHANGED
@@ -176,5 +176,6 @@ export default class Color4 {
|
|
176
176
|
public static yellow = Color4.ofRGB(1, 1, 0.1);
|
177
177
|
public static clay = Color4.ofRGB(0.8, 0.4, 0.2);
|
178
178
|
public static black = Color4.ofRGB(0, 0, 0);
|
179
|
+
public static gray = Color4.ofRGB(0.5, 0.5, 0.5);
|
179
180
|
public static white = Color4.ofRGB(1, 1, 1);
|
180
181
|
}
|
package/src/Editor.ts
CHANGED
@@ -292,8 +292,7 @@ export class Editor {
|
|
292
292
|
const toolbar = new HTMLToolbar(this, this.container, this.localization);
|
293
293
|
|
294
294
|
if (defaultLayout) {
|
295
|
-
toolbar.
|
296
|
-
toolbar.addDefaultActionButtons();
|
295
|
+
toolbar.addDefaults();
|
297
296
|
}
|
298
297
|
|
299
298
|
return toolbar;
|
@@ -946,8 +945,8 @@ export class Editor {
|
|
946
945
|
this.showLoadingWarning(0);
|
947
946
|
this.display.setDraftMode(true);
|
948
947
|
|
949
|
-
await loader.start((component) => {
|
950
|
-
this.dispatchNoAnnounce(EditorImage.addElement(component));
|
948
|
+
await loader.start(async (component) => {
|
949
|
+
await this.dispatchNoAnnounce(EditorImage.addElement(component));
|
951
950
|
}, (countProcessed: number, totalToProcess: number) => {
|
952
951
|
if (countProcessed % 500 === 0) {
|
953
952
|
this.showLoadingWarning(countProcessed / totalToProcess);
|
package/src/SVGLoader.ts
CHANGED
@@ -156,7 +156,7 @@ export default class SVGLoader implements ImageLoader {
|
|
156
156
|
}
|
157
157
|
|
158
158
|
// Adds a stroke with a single path
|
159
|
-
private addPath(node: SVGPathElement) {
|
159
|
+
private async addPath(node: SVGPathElement) {
|
160
160
|
let elem: AbstractComponent;
|
161
161
|
try {
|
162
162
|
const strokeData = this.strokeDataFromElem(node);
|
@@ -181,7 +181,7 @@ export default class SVGLoader implements ImageLoader {
|
|
181
181
|
return;
|
182
182
|
}
|
183
183
|
}
|
184
|
-
this.onAddComponent?.(elem);
|
184
|
+
await this.onAddComponent?.(elem);
|
185
185
|
}
|
186
186
|
|
187
187
|
// If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
|
@@ -274,10 +274,10 @@ export default class SVGLoader implements ImageLoader {
|
|
274
274
|
return result;
|
275
275
|
}
|
276
276
|
|
277
|
-
private addText(elem: SVGTextElement|SVGTSpanElement) {
|
277
|
+
private async addText(elem: SVGTextElement|SVGTSpanElement) {
|
278
278
|
try {
|
279
279
|
const textElem = this.makeText(elem);
|
280
|
-
this.onAddComponent?.(textElem);
|
280
|
+
await this.onAddComponent?.(textElem);
|
281
281
|
} catch (e) {
|
282
282
|
console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
|
283
283
|
this.addUnknownNode(elem);
|
@@ -300,17 +300,17 @@ export default class SVGLoader implements ImageLoader {
|
|
300
300
|
new Set([ 'transform' ])
|
301
301
|
);
|
302
302
|
|
303
|
-
this.onAddComponent?.(imageElem);
|
303
|
+
await this.onAddComponent?.(imageElem);
|
304
304
|
} catch (e) {
|
305
305
|
console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
|
306
|
-
this.addUnknownNode(elem);
|
306
|
+
await this.addUnknownNode(elem);
|
307
307
|
}
|
308
308
|
}
|
309
309
|
|
310
|
-
private addUnknownNode(node: SVGElement) {
|
310
|
+
private async addUnknownNode(node: SVGElement) {
|
311
311
|
if (this.storeUnknown) {
|
312
312
|
const component = new UnknownSVGObject(node);
|
313
|
-
this.onAddComponent?.(component);
|
313
|
+
await this.onAddComponent?.(component);
|
314
314
|
}
|
315
315
|
}
|
316
316
|
|
@@ -335,9 +335,9 @@ export default class SVGLoader implements ImageLoader {
|
|
335
335
|
this.onDetermineExportRect?.(this.rootViewBox);
|
336
336
|
}
|
337
337
|
|
338
|
-
private updateSVGAttrs(node: SVGSVGElement) {
|
338
|
+
private async updateSVGAttrs(node: SVGSVGElement) {
|
339
339
|
if (this.storeUnknown) {
|
340
|
-
this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
340
|
+
await this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
341
341
|
}
|
342
342
|
}
|
343
343
|
|
@@ -350,10 +350,10 @@ export default class SVGLoader implements ImageLoader {
|
|
350
350
|
// Continue -- visit the node's children.
|
351
351
|
break;
|
352
352
|
case 'path':
|
353
|
-
this.addPath(node as SVGPathElement);
|
353
|
+
await this.addPath(node as SVGPathElement);
|
354
354
|
break;
|
355
355
|
case 'text':
|
356
|
-
this.addText(node as SVGTextElement);
|
356
|
+
await this.addText(node as SVGTextElement);
|
357
357
|
visitChildren = false;
|
358
358
|
break;
|
359
359
|
case 'image':
|
@@ -367,7 +367,7 @@ export default class SVGLoader implements ImageLoader {
|
|
367
367
|
this.updateSVGAttrs(node as SVGSVGElement);
|
368
368
|
break;
|
369
369
|
case 'style':
|
370
|
-
this.addUnknownNode(node as SVGStyleElement);
|
370
|
+
await this.addUnknownNode(node as SVGStyleElement);
|
371
371
|
break;
|
372
372
|
default:
|
373
373
|
console.warn('Unknown SVG element,', node);
|
@@ -377,7 +377,7 @@ export default class SVGLoader implements ImageLoader {
|
|
377
377
|
);
|
378
378
|
}
|
379
379
|
|
380
|
-
this.addUnknownNode(node as SVGElement);
|
380
|
+
await this.addUnknownNode(node as SVGElement);
|
381
381
|
return;
|
382
382
|
}
|
383
383
|
|
@@ -78,6 +78,25 @@ export default abstract class AbstractComponent {
|
|
78
78
|
public abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
79
79
|
public abstract intersects(lineSegment: LineSegment2): boolean;
|
80
80
|
|
81
|
+
public intersectsRect(rect: Rect2): boolean {
|
82
|
+
// If this component intersects rect,
|
83
|
+
// it is either contained entirely within rect or intersects one of rect's edges.
|
84
|
+
|
85
|
+
// If contained within,
|
86
|
+
if (rect.containsRect(this.getBBox())) {
|
87
|
+
return true;
|
88
|
+
}
|
89
|
+
|
90
|
+
// Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
|
91
|
+
// As such, test with more lines than just the rect's edges.
|
92
|
+
const testLines = [];
|
93
|
+
for (const subregion of rect.divideIntoGrid(2, 2)) {
|
94
|
+
testLines.push(...subregion.getEdges());
|
95
|
+
}
|
96
|
+
|
97
|
+
return testLines.some(edge => this.intersects(edge));
|
98
|
+
}
|
99
|
+
|
81
100
|
// Return null iff this object cannot be safely serialized/deserialized.
|
82
101
|
protected abstract serializeToJSON(): any[]|Record<string, any>|number|string|null;
|
83
102
|
|
@@ -16,12 +16,25 @@ import SelectionToolWidget from './widgets/SelectionToolWidget';
|
|
16
16
|
import TextToolWidget from './widgets/TextToolWidget';
|
17
17
|
import HandToolWidget from './widgets/HandToolWidget';
|
18
18
|
import BaseWidget from './widgets/BaseWidget';
|
19
|
-
import
|
19
|
+
import ActionButtonWidget from './widgets/ActionButtonWidget';
|
20
|
+
import InsertImageWidget from './widgets/InsertImageWidget';
|
20
21
|
|
21
22
|
export const toolbarCSSPrefix = 'toolbar-';
|
22
23
|
|
23
24
|
type UpdateColorisCallback = ()=>void;
|
24
25
|
|
26
|
+
interface SpacerOptions {
|
27
|
+
// Defaults to 0. If a non-zero number, determines the rate at which the
|
28
|
+
// spacer should grow (like flexGrow).
|
29
|
+
grow: number;
|
30
|
+
|
31
|
+
// Minimum size (e.g. "23px")
|
32
|
+
minSize: string;
|
33
|
+
|
34
|
+
// Maximum size (e.g. "50px")
|
35
|
+
maxSize: string;
|
36
|
+
}
|
37
|
+
|
25
38
|
export default class HTMLToolbar {
|
26
39
|
private container: HTMLElement;
|
27
40
|
|
@@ -121,8 +134,17 @@ export default class HTMLToolbar {
|
|
121
134
|
});
|
122
135
|
}
|
123
136
|
|
124
|
-
|
125
|
-
|
137
|
+
/**
|
138
|
+
* Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
|
139
|
+
* (i.e. its `addTo` method should not have been called).
|
140
|
+
*
|
141
|
+
* @example
|
142
|
+
* ```ts
|
143
|
+
* const toolbar = editor.addToolbar();
|
144
|
+
* const insertImageWidget = new InsertImageWidget(editor);
|
145
|
+
* toolbar.addWidget(insertImageWidget);
|
146
|
+
* ```
|
147
|
+
*/
|
126
148
|
public addWidget(widget: BaseWidget) {
|
127
149
|
// Prevent name collisions
|
128
150
|
const id = widget.getUniqueIdIn(this.widgets);
|
@@ -135,6 +157,46 @@ export default class HTMLToolbar {
|
|
135
157
|
this.setupColorPickers();
|
136
158
|
}
|
137
159
|
|
160
|
+
/**
|
161
|
+
* Adds a spacer.
|
162
|
+
*
|
163
|
+
* @example
|
164
|
+
* Adding a save button that moves to the very right edge of the toolbar
|
165
|
+
* while keeping the other buttons centered:
|
166
|
+
* ```ts
|
167
|
+
* const toolbar = editor.addToolbar(false);
|
168
|
+
*
|
169
|
+
* toolbar.addSpacer({ grow: 1 });
|
170
|
+
* toolbar.addDefaults();
|
171
|
+
* toolbar.addSpacer({ grow: 1 });
|
172
|
+
*
|
173
|
+
* toolbar.addActionButton({
|
174
|
+
* label: 'Save',
|
175
|
+
* icon: editor.icons.makeSaveIcon(),
|
176
|
+
* }, () => {
|
177
|
+
* saveCallback();
|
178
|
+
* });
|
179
|
+
* ```
|
180
|
+
*/
|
181
|
+
public addSpacer(options: Partial<SpacerOptions> = {}) {
|
182
|
+
const spacer = document.createElement('div');
|
183
|
+
spacer.classList.add(`${toolbarCSSPrefix}spacer`);
|
184
|
+
|
185
|
+
if (options.grow) {
|
186
|
+
spacer.style.flexGrow = `${options.grow}`;
|
187
|
+
}
|
188
|
+
|
189
|
+
if (options.minSize) {
|
190
|
+
spacer.style.minWidth = options.minSize;
|
191
|
+
}
|
192
|
+
|
193
|
+
if (options.maxSize) {
|
194
|
+
spacer.style.maxWidth = options.maxSize;
|
195
|
+
}
|
196
|
+
|
197
|
+
this.container.appendChild(spacer);
|
198
|
+
}
|
199
|
+
|
138
200
|
public serializeState(): string {
|
139
201
|
const result: Record<string, any> = {};
|
140
202
|
|
@@ -145,8 +207,10 @@ export default class HTMLToolbar {
|
|
145
207
|
return JSON.stringify(result);
|
146
208
|
}
|
147
209
|
|
148
|
-
|
149
|
-
|
210
|
+
/**
|
211
|
+
* Deserialize toolbar widgets from the given state.
|
212
|
+
* Assumes that toolbar widgets are in the same order as when state was serialized.
|
213
|
+
*/
|
150
214
|
public deserializeState(state: string) {
|
151
215
|
const data = JSON.parse(state);
|
152
216
|
|
@@ -247,4 +311,16 @@ export default class HTMLToolbar {
|
|
247
311
|
public addDefaultActionButtons() {
|
248
312
|
this.addUndoRedoButtons();
|
249
313
|
}
|
314
|
+
|
315
|
+
/**
|
316
|
+
* Adds both the default tool widgets and action buttons. Equivalent to
|
317
|
+
* ```ts
|
318
|
+
* toolbar.addDefaultToolWidgets();
|
319
|
+
* toolbar.addDefaultActionButtons();
|
320
|
+
* ```
|
321
|
+
*/
|
322
|
+
public addDefaults() {
|
323
|
+
this.addDefaultToolWidgets();
|
324
|
+
this.addDefaultActionButtons();
|
325
|
+
}
|
250
326
|
}
|