js-draw 0.11.2 → 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/.github/workflows/github-pages.yml +2 -0
- package/CHANGELOG.md +12 -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/EditorImage.js +1 -11
- 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/math/Rect2.d.ts +1 -0
- package/dist/src/math/Rect2.js +20 -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 +38 -9
- 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 +21 -6
- package/dist/src/tools/ToolController.js +3 -3
- 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/EditorImage.ts +1 -11
- package/src/SVGLoader.ts +14 -14
- package/src/components/AbstractComponent.ts +19 -0
- package/src/math/Rect2.test.ts +22 -0
- package/src/math/Rect2.ts +26 -0
- package/src/toolbar/HTMLToolbar.ts +81 -5
- package/src/toolbar/IconProvider.ts +39 -9
- 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 +26 -8
- package/src/tools/ToolController.ts +3 -3
- package/src/types.ts +2 -2
@@ -4,11 +4,20 @@ import Editor from '../Editor';
|
|
4
4
|
export default class Eraser extends BaseTool {
|
5
5
|
private editor;
|
6
6
|
private lastPoint;
|
7
|
+
private isFirstEraseEvt;
|
7
8
|
private toRemove;
|
9
|
+
private thickness;
|
8
10
|
private partialCommands;
|
9
11
|
constructor(editor: Editor, description: string);
|
12
|
+
private clearPreview;
|
13
|
+
private getSizeOnCanvas;
|
14
|
+
private drawPreviewAt;
|
15
|
+
private getEraserRect;
|
16
|
+
private eraseTo;
|
10
17
|
onPointerDown(event: PointerEvt): boolean;
|
11
18
|
onPointerMove(event: PointerEvt): void;
|
12
|
-
onPointerUp(
|
19
|
+
onPointerUp(event: PointerEvt): void;
|
13
20
|
onGestureCancel(): void;
|
21
|
+
getThickness(): number;
|
22
|
+
setThickness(thickness: number): void;
|
14
23
|
}
|
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') {
|
@@ -63,11 +63,20 @@ export default class TextTool extends BaseTool {
|
|
63
63
|
// Estimate
|
64
64
|
return style.size * 2 / 3;
|
65
65
|
}
|
66
|
-
|
66
|
+
// Take input from this' textInputElem and add it to the EditorImage.
|
67
|
+
// If [removeInput], the HTML input element is removed. Otherwise, its value
|
68
|
+
// is cleared.
|
69
|
+
flushInput(removeInput = true) {
|
67
70
|
if (this.textInputElem && this.textTargetPosition) {
|
68
71
|
const content = this.textInputElem.value.trimEnd();
|
69
|
-
this.textInputElem.
|
70
|
-
|
72
|
+
this.textInputElem.value = '';
|
73
|
+
if (removeInput) {
|
74
|
+
// In some browsers, .remove() triggers a .blur event (synchronously).
|
75
|
+
// Clear this.textInputElem before removal
|
76
|
+
const input = this.textInputElem;
|
77
|
+
this.textInputElem = null;
|
78
|
+
input.remove();
|
79
|
+
}
|
71
80
|
if (content === '') {
|
72
81
|
return;
|
73
82
|
}
|
@@ -134,9 +143,15 @@ export default class TextTool extends BaseTool {
|
|
134
143
|
}
|
135
144
|
};
|
136
145
|
this.textInputElem.onblur = () => {
|
137
|
-
//
|
138
|
-
//
|
139
|
-
|
146
|
+
// Delay removing the input -- flushInput may be called within a blur()
|
147
|
+
// event handler
|
148
|
+
const removeInput = false;
|
149
|
+
const input = this.textInputElem;
|
150
|
+
this.flushInput(removeInput);
|
151
|
+
this.textInputElem = null;
|
152
|
+
setTimeout(() => {
|
153
|
+
input === null || input === void 0 ? void 0 : input.remove();
|
154
|
+
}, 0);
|
140
155
|
};
|
141
156
|
this.textInputElem.onkeyup = (evt) => {
|
142
157
|
var _a, _b;
|
@@ -21,13 +21,13 @@ export default class ToolController {
|
|
21
21
|
this.primaryToolGroup = primaryToolGroup;
|
22
22
|
const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
|
23
23
|
const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom);
|
24
|
-
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness:
|
24
|
+
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 8 });
|
25
25
|
const primaryTools = [
|
26
26
|
// Three pens
|
27
27
|
primaryPenTool,
|
28
28
|
new Pen(editor, localization.penTool(2), { color: Color4.clay, thickness: 4 }),
|
29
|
-
// Highlighter-like pen with width=
|
30
|
-
new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness:
|
29
|
+
// Highlighter-like pen with width=40
|
30
|
+
new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 40 }, makePressureSensitiveFreehandLineBuilder),
|
31
31
|
new Eraser(editor, localization.eraserTool),
|
32
32
|
new SelectionTool(editor, localization.selectionTool),
|
33
33
|
new TextTool(editor, localization.textTool, localization),
|
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/EditorImage.ts
CHANGED
@@ -320,17 +320,7 @@ export class ImageNode {
|
|
320
320
|
if (this.content !== null) {
|
321
321
|
this.bbox = this.content.getBBox();
|
322
322
|
} else {
|
323
|
-
this.bbox = Rect2.
|
324
|
-
let isFirst = true;
|
325
|
-
|
326
|
-
for (const child of this.children) {
|
327
|
-
if (isFirst) {
|
328
|
-
this.bbox = child.getBBox();
|
329
|
-
isFirst = false;
|
330
|
-
} else {
|
331
|
-
this.bbox = this.bbox.union(child.getBBox());
|
332
|
-
}
|
333
|
-
}
|
323
|
+
this.bbox = Rect2.union(...this.children.map(child => child.getBBox()));
|
334
324
|
}
|
335
325
|
|
336
326
|
if (bubbleUp && !oldBBox.eq(this.bbox)) {
|
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
|
|
package/src/math/Rect2.test.ts
CHANGED
@@ -46,6 +46,28 @@ describe('Rect2', () => {
|
|
46
46
|
expect(Rect2.empty.union(Rect2.empty)).objEq(Rect2.empty);
|
47
47
|
});
|
48
48
|
|
49
|
+
it('should handle empty unions', () => {
|
50
|
+
expect(Rect2.union()).toStrictEqual(Rect2.empty);
|
51
|
+
});
|
52
|
+
|
53
|
+
it('should correctly union multiple rectangles', () => {
|
54
|
+
expect(Rect2.union(new Rect2(0, 0, 1, 1), new Rect2(1, 1, 2, 2))).objEq(
|
55
|
+
new Rect2(0, 0, 3, 3)
|
56
|
+
);
|
57
|
+
|
58
|
+
expect(
|
59
|
+
Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, 1, 2, 2), new Rect2(1, 10, 1, 0.1))
|
60
|
+
).objEq(
|
61
|
+
new Rect2(-1, 0, 4, 10.1)
|
62
|
+
);
|
63
|
+
|
64
|
+
expect(
|
65
|
+
Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, -11.1, 2, 2), new Rect2(1, 10, 1, 0.1))
|
66
|
+
).objEq(
|
67
|
+
new Rect2(-1, -11.1, 4, 21.2)
|
68
|
+
);
|
69
|
+
});
|
70
|
+
|
49
71
|
it('should contain points that are within a rectangle', () => {
|
50
72
|
expect(new Rect2(-1, -1, 2, 2).containsPoint(Vec2.zero)).toBe(true);
|
51
73
|
expect(new Rect2(-1, -1, 0, 0).containsPoint(Vec2.zero)).toBe(false);
|
package/src/math/Rect2.ts
CHANGED
@@ -278,6 +278,32 @@ export default class Rect2 {
|
|
278
278
|
);
|
279
279
|
}
|
280
280
|
|
281
|
+
// @returns a rectangle that contains all of the given rectangles, the bounding box
|
282
|
+
// of the given rectangles.
|
283
|
+
public static union(...rects: Rect2[]): Rect2 {
|
284
|
+
if (rects.length === 0) {
|
285
|
+
return Rect2.empty;
|
286
|
+
}
|
287
|
+
|
288
|
+
const firstRect = rects[0];
|
289
|
+
let minX: number = firstRect.topLeft.x;
|
290
|
+
let minY: number = firstRect.topLeft.y;
|
291
|
+
let maxX: number = firstRect.bottomRight.x;
|
292
|
+
let maxY: number = firstRect.bottomRight.y;
|
293
|
+
|
294
|
+
for (let i = 1; i < rects.length; i++) {
|
295
|
+
const rect = rects[i];
|
296
|
+
minX = Math.min(minX, rect.topLeft.x);
|
297
|
+
minY = Math.min(minY, rect.topLeft.y);
|
298
|
+
maxX = Math.max(maxX, rect.bottomRight.x);
|
299
|
+
maxY = Math.max(maxY, rect.bottomRight.y);
|
300
|
+
}
|
301
|
+
|
302
|
+
return new Rect2(
|
303
|
+
minX, minY, maxX - minX, maxY - minY,
|
304
|
+
);
|
305
|
+
}
|
306
|
+
|
281
307
|
public static of(template: RectTemplate) {
|
282
308
|
const width = template.width ?? template.w ?? 0;
|
283
309
|
const height = template.height ?? template.h ?? 0;
|