js-draw 0.15.1 → 0.16.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 +56 -0
- package/CHANGELOG.md +13 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +1 -1
- package/dist/src/Color4.js +5 -1
- package/dist/src/Editor.d.ts +11 -2
- package/dist/src/Editor.js +66 -33
- package/dist/src/EditorImage.d.ts +28 -3
- package/dist/src/EditorImage.js +109 -18
- package/dist/src/EventDispatcher.d.ts +4 -3
- package/dist/src/SVGLoader.d.ts +1 -0
- package/dist/src/SVGLoader.js +15 -1
- package/dist/src/Viewport.d.ts +8 -3
- package/dist/src/Viewport.js +15 -8
- package/dist/src/components/AbstractComponent.d.ts +6 -1
- package/dist/src/components/AbstractComponent.js +15 -2
- package/dist/src/components/ImageBackground.d.ts +42 -0
- package/dist/src/components/ImageBackground.js +139 -0
- package/dist/src/components/ImageComponent.js +2 -0
- package/dist/src/components/builders/ArrowBuilder.d.ts +3 -1
- package/dist/src/components/builders/ArrowBuilder.js +43 -40
- package/dist/src/components/builders/LineBuilder.d.ts +3 -1
- package/dist/src/components/builders/LineBuilder.js +25 -28
- package/dist/src/components/builders/RectangleBuilder.js +1 -1
- package/dist/src/components/lib.d.ts +2 -1
- package/dist/src/components/lib.js +2 -1
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/localizations/es.js +1 -1
- package/dist/src/math/Mat33.js +43 -5
- package/dist/src/math/Path.d.ts +5 -0
- package/dist/src/math/Path.js +80 -28
- package/dist/src/math/Vec3.js +1 -1
- package/dist/src/rendering/Display.js +1 -1
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
- package/dist/src/testing/sendTouchEvent.d.ts +6 -0
- package/dist/src/testing/sendTouchEvent.js +26 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +25 -2
- package/dist/src/toolbar/HTMLToolbar.js +127 -15
- package/dist/src/toolbar/IconProvider.d.ts +2 -0
- package/dist/src/toolbar/IconProvider.js +45 -2
- package/dist/src/toolbar/localization.d.ts +5 -0
- package/dist/src/toolbar/localization.js +5 -0
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
- package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
- package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
- package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
- package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
- package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
- package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
- package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
- package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
- package/dist/src/toolbar/widgets/OverflowWidget.js +65 -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.js +5 -2
- package/dist/src/tools/PanZoom.js +12 -0
- package/dist/src/tools/PasteHandler.js +2 -2
- package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
- package/dist/src/tools/SelectionTool/Selection.js +3 -2
- package/dist/src/tools/SelectionTool/SelectionTool.js +5 -1
- package/package.json +1 -1
- package/src/Color4.test.ts +6 -0
- package/src/Color4.ts +6 -1
- package/src/Editor.loadFrom.test.ts +24 -0
- package/src/Editor.ts +73 -39
- package/src/EditorImage.ts +136 -21
- package/src/EventDispatcher.ts +4 -1
- package/src/SVGLoader.ts +12 -1
- package/src/Viewport.ts +17 -7
- package/src/components/AbstractComponent.ts +17 -1
- package/src/components/ImageBackground.test.ts +35 -0
- package/src/components/ImageBackground.ts +176 -0
- package/src/components/ImageComponent.ts +2 -0
- package/src/components/builders/ArrowBuilder.ts +44 -41
- package/src/components/builders/LineBuilder.ts +26 -28
- package/src/components/builders/RectangleBuilder.ts +1 -1
- package/src/components/lib.ts +2 -0
- package/src/components/localization.ts +4 -0
- package/src/localizations/es.ts +8 -0
- package/src/math/Mat33.test.ts +47 -3
- package/src/math/Mat33.ts +47 -5
- package/src/math/Path.ts +87 -28
- package/src/math/Vec3.test.ts +4 -0
- package/src/math/Vec3.ts +1 -1
- package/src/rendering/Display.ts +1 -1
- package/src/rendering/renderers/AbstractRenderer.ts +20 -3
- package/src/rendering/renderers/CanvasRenderer.ts +17 -4
- package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
- package/src/rendering/renderers/SVGRenderer.ts +8 -1
- package/src/testing/sendTouchEvent.ts +43 -0
- package/src/toolbar/HTMLToolbar.ts +164 -16
- package/src/toolbar/IconProvider.ts +47 -2
- package/src/toolbar/localization.ts +10 -0
- package/src/toolbar/toolbar.css +2 -0
- package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
- package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
- package/src/toolbar/widgets/BaseWidget.ts +34 -2
- package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
- package/src/toolbar/widgets/HandToolWidget.ts +1 -1
- package/src/toolbar/widgets/OverflowWidget.css +9 -0
- package/src/toolbar/widgets/OverflowWidget.ts +83 -0
- package/src/toolbar/widgets/lib.ts +2 -1
- package/src/tools/Eraser.test.ts +24 -1
- package/src/tools/Eraser.ts +6 -2
- package/src/tools/PanZoom.test.ts +267 -23
- package/src/tools/PanZoom.ts +15 -1
- package/src/tools/PasteHandler.ts +3 -2
- package/src/tools/SelectionTool/Selection.ts +3 -2
- package/src/tools/SelectionTool/SelectionTool.ts +6 -1
- package/src/types.ts +1 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
import Color4 from './Color4';
|
2
|
+
import { imageBackgroundCSSClassName } from './components/ImageBackground';
|
3
|
+
import { RestyleableComponent } from './lib';
|
4
|
+
import SVGLoader from './SVGLoader';
|
5
|
+
import createEditor from './testing/createEditor';
|
6
|
+
|
7
|
+
describe('Editor.loadFrom', () => {
|
8
|
+
it('should remove existing BackgroundComponents when loading new BackgroundComponents', async () => {
|
9
|
+
const editor = createEditor();
|
10
|
+
await editor.dispatch(editor.setBackgroundColor(Color4.red));
|
11
|
+
|
12
|
+
let backgroundComponents = editor.image.getBackgroundComponents();
|
13
|
+
expect(backgroundComponents).toHaveLength(1);
|
14
|
+
expect((backgroundComponents[0] as RestyleableComponent).getStyle().color).objEq(Color4.red);
|
15
|
+
|
16
|
+
await editor.loadFrom(SVGLoader.fromString(`<svg viewBox='0 0 100 100'>
|
17
|
+
<path class='${imageBackgroundCSSClassName}' d='m0,0 L100,0 L100,100 L0,100 z' fill='#000'/>
|
18
|
+
</svg>`, true));
|
19
|
+
|
20
|
+
backgroundComponents = editor.image.getBackgroundComponents();
|
21
|
+
expect(backgroundComponents).toHaveLength(1);
|
22
|
+
expect((backgroundComponents[0] as RestyleableComponent).getStyle().color).objEq(Color4.black);
|
23
|
+
});
|
24
|
+
});
|
package/src/Editor.ts
CHANGED
@@ -26,6 +26,8 @@ import fileToBase64 from './util/fileToBase64';
|
|
26
26
|
import uniteCommands from './commands/uniteCommands';
|
27
27
|
import SelectionTool from './tools/SelectionTool/SelectionTool';
|
28
28
|
import AbstractComponent from './components/AbstractComponent';
|
29
|
+
import Erase from './commands/Erase';
|
30
|
+
import ImageBackground, { BackgroundType } from './components/ImageBackground';
|
29
31
|
|
30
32
|
type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
|
31
33
|
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
|
@@ -109,9 +111,6 @@ export class Editor {
|
|
109
111
|
*/
|
110
112
|
public readonly image: EditorImage;
|
111
113
|
|
112
|
-
/** Viewport for the exported/imported image. */
|
113
|
-
private importExportViewport: Viewport;
|
114
|
-
|
115
114
|
/**
|
116
115
|
* Allows transforming the view and querying information about
|
117
116
|
* what is currently visible.
|
@@ -215,8 +214,13 @@ export class Editor {
|
|
215
214
|
this.renderingRegion.setAttribute('alt', '');
|
216
215
|
|
217
216
|
this.notifier = new EventDispatcher();
|
218
|
-
this.
|
219
|
-
|
217
|
+
this.viewport = new Viewport((oldTransform, newTransform) => {
|
218
|
+
this.notifier.dispatch(EditorEventType.ViewportChanged, {
|
219
|
+
kind: EditorEventType.ViewportChanged,
|
220
|
+
newTransform,
|
221
|
+
oldTransform,
|
222
|
+
});
|
223
|
+
});
|
220
224
|
this.display = new Display(this, this.settings.renderingMode, this.renderingRegion);
|
221
225
|
this.image = new EditorImage();
|
222
226
|
this.history = new UndoRedoHistory(this, this.announceRedoCallback, this.announceUndoCallback);
|
@@ -224,9 +228,6 @@ export class Editor {
|
|
224
228
|
|
225
229
|
parent.appendChild(this.container);
|
226
230
|
|
227
|
-
// Default to a 500x500 image
|
228
|
-
this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
|
229
|
-
|
230
231
|
this.viewport.updateScreenSize(
|
231
232
|
Vec2.of(this.display.width, this.display.height)
|
232
233
|
);
|
@@ -356,9 +357,10 @@ export class Editor {
|
|
356
357
|
this.viewport.updateScreenSize(
|
357
358
|
Vec2.of(this.display.width, this.display.height)
|
358
359
|
);
|
360
|
+
this.queueRerender();
|
359
361
|
});
|
360
362
|
|
361
|
-
|
363
|
+
const handleResize = () => {
|
362
364
|
this.notifier.dispatch(EditorEventType.DisplayResized, {
|
363
365
|
kind: EditorEventType.DisplayResized,
|
364
366
|
newSize: Vec2.of(
|
@@ -366,8 +368,14 @@ export class Editor {
|
|
366
368
|
this.display.height
|
367
369
|
),
|
368
370
|
});
|
369
|
-
|
370
|
-
|
371
|
+
};
|
372
|
+
|
373
|
+
if ('ResizeObserver' in (window as any)) {
|
374
|
+
const resizeObserver = new ResizeObserver(handleResize);
|
375
|
+
resizeObserver.observe(this.container);
|
376
|
+
} else {
|
377
|
+
addEventListener('resize', handleResize);
|
378
|
+
}
|
371
379
|
|
372
380
|
this.accessibilityControlArea.addEventListener('input', () => {
|
373
381
|
this.accessibilityControlArea.value = '';
|
@@ -773,7 +781,7 @@ export class Editor {
|
|
773
781
|
const exportRectFill = { fill: Color4.fromHex('#44444455') };
|
774
782
|
const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
|
775
783
|
renderer.drawRect(
|
776
|
-
this.
|
784
|
+
this.getImportExportRect(),
|
777
785
|
exportRectStrokeWidth,
|
778
786
|
exportRectFill
|
779
787
|
);
|
@@ -920,13 +928,14 @@ export class Editor {
|
|
920
928
|
public toDataURL(format: 'image/png'|'image/jpeg'|'image/webp' = 'image/png'): string {
|
921
929
|
const canvas = document.createElement('canvas');
|
922
930
|
|
923
|
-
const
|
931
|
+
const importExportViewport = this.image.getImportExportViewport();
|
932
|
+
const resolution = importExportViewport.getScreenRectSize();
|
924
933
|
|
925
934
|
canvas.width = resolution.x;
|
926
935
|
canvas.height = resolution.y;
|
927
936
|
|
928
937
|
const ctx = canvas.getContext('2d')!;
|
929
|
-
const renderer = new CanvasRenderer(ctx,
|
938
|
+
const renderer = new CanvasRenderer(ctx, importExportViewport);
|
930
939
|
|
931
940
|
this.image.renderAll(renderer);
|
932
941
|
|
@@ -935,12 +944,12 @@ export class Editor {
|
|
935
944
|
}
|
936
945
|
|
937
946
|
public toSVG(): SVGElement {
|
938
|
-
const importExportViewport = this.
|
947
|
+
const importExportViewport = this.image.getImportExportViewport().getTemporaryClone();
|
939
948
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
940
949
|
const result = document.createElementNS(svgNameSpace, 'svg');
|
941
950
|
const renderer = new SVGRenderer(result, importExportViewport);
|
942
951
|
|
943
|
-
const origTransform =
|
952
|
+
const origTransform = importExportViewport.canvasToScreenTransform;
|
944
953
|
// Render with (0,0) at (0,0) — we'll handle translation with
|
945
954
|
// the viewBox property.
|
946
955
|
importExportViewport.resetTransform(Mat33.identity);
|
@@ -966,10 +975,18 @@ export class Editor {
|
|
966
975
|
return result;
|
967
976
|
}
|
968
977
|
|
978
|
+
/**
|
979
|
+
* Load editor data from an `ImageLoader` (e.g. an {@link SVGLoader}).
|
980
|
+
*
|
981
|
+
* @see loadFromSVG
|
982
|
+
*/
|
969
983
|
public async loadFrom(loader: ImageLoader) {
|
970
984
|
this.showLoadingWarning(0);
|
971
985
|
this.display.setDraftMode(true);
|
972
986
|
|
987
|
+
const originalBackgrounds = this.image.getBackgroundComponents();
|
988
|
+
const eraseBackgroundCommand = new Erase(originalBackgrounds);
|
989
|
+
|
973
990
|
await loader.start(async (component) => {
|
974
991
|
await this.dispatchNoAnnounce(EditorImage.addElement(component));
|
975
992
|
}, (countProcessed: number, totalToProcess: number) => {
|
@@ -984,41 +1001,58 @@ export class Editor {
|
|
984
1001
|
this.dispatchNoAnnounce(this.setImportExportRect(importExportRect), false);
|
985
1002
|
this.dispatchNoAnnounce(this.viewport.zoomTo(importExportRect), false);
|
986
1003
|
});
|
1004
|
+
|
1005
|
+
// Ensure that we don't have multiple overlapping BackgroundComponents. Remove
|
1006
|
+
// old BackgroundComponents.
|
1007
|
+
// Overlapping BackgroundComponents may cause changing the background color to
|
1008
|
+
// not work properly.
|
1009
|
+
if (this.image.getBackgroundComponents().length !== originalBackgrounds.length) {
|
1010
|
+
await this.dispatchNoAnnounce(eraseBackgroundCommand);
|
1011
|
+
}
|
1012
|
+
|
987
1013
|
this.hideLoadingWarning();
|
988
1014
|
|
989
1015
|
this.display.setDraftMode(false);
|
990
1016
|
this.queueRerender();
|
991
1017
|
}
|
992
1018
|
|
1019
|
+
private getTopmostBackgroundComponent(): ImageBackground|null {
|
1020
|
+
let background: ImageBackground|null = null;
|
1021
|
+
|
1022
|
+
// Find a background component, if one exists.
|
1023
|
+
// Use the last (topmost) background component if there are multiple.
|
1024
|
+
for (const component of this.image.getBackgroundComponents()) {
|
1025
|
+
if (component instanceof ImageBackground) {
|
1026
|
+
background = component;
|
1027
|
+
}
|
1028
|
+
}
|
1029
|
+
|
1030
|
+
return background;
|
1031
|
+
}
|
1032
|
+
|
1033
|
+
/**
|
1034
|
+
* Set the background color of the image.
|
1035
|
+
*/
|
1036
|
+
public setBackgroundColor(color: Color4): Command {
|
1037
|
+
let background = this.getTopmostBackgroundComponent();
|
1038
|
+
|
1039
|
+
if (!background) {
|
1040
|
+
const backgroundType = color.eq(Color4.transparent) ? BackgroundType.None : BackgroundType.SolidColor;
|
1041
|
+
background = new ImageBackground(backgroundType, color);
|
1042
|
+
return this.image.addElement(background);
|
1043
|
+
} else {
|
1044
|
+
return background.updateStyle({ color });
|
1045
|
+
}
|
1046
|
+
}
|
1047
|
+
|
993
1048
|
// Returns the size of the visible region of the output SVG
|
994
1049
|
public getImportExportRect(): Rect2 {
|
995
|
-
return this.
|
1050
|
+
return this.image.getImportExportViewport().visibleRect;
|
996
1051
|
}
|
997
1052
|
|
998
1053
|
// Resize the output SVG to match `imageRect`.
|
999
1054
|
public setImportExportRect(imageRect: Rect2): Command {
|
1000
|
-
|
1001
|
-
const origTransform = this.importExportViewport.canvasToScreenTransform;
|
1002
|
-
|
1003
|
-
return new class extends Command {
|
1004
|
-
public apply(editor: Editor) {
|
1005
|
-
const viewport = editor.importExportViewport;
|
1006
|
-
viewport.updateScreenSize(imageRect.size);
|
1007
|
-
viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
|
1008
|
-
editor.queueRerender();
|
1009
|
-
}
|
1010
|
-
|
1011
|
-
public unapply(editor: Editor) {
|
1012
|
-
const viewport = editor.importExportViewport;
|
1013
|
-
viewport.updateScreenSize(origSize);
|
1014
|
-
viewport.resetTransform(origTransform);
|
1015
|
-
editor.queueRerender();
|
1016
|
-
}
|
1017
|
-
|
1018
|
-
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
1019
|
-
return localizationTable.resizeOutputCommand(imageRect);
|
1020
|
-
}
|
1021
|
-
};
|
1055
|
+
return this.image.setImportExportRect(imageRect);
|
1022
1056
|
}
|
1023
1057
|
|
1024
1058
|
/**
|
package/src/EditorImage.ts
CHANGED
@@ -6,32 +6,105 @@ import Rect2 from './math/Rect2';
|
|
6
6
|
import { EditorLocalization } from './localization';
|
7
7
|
import RenderingCache from './rendering/caching/RenderingCache';
|
8
8
|
import SerializableCommand from './commands/SerializableCommand';
|
9
|
+
import EventDispatcher from './EventDispatcher';
|
10
|
+
import { Vec2 } from './math/Vec2';
|
11
|
+
import Command from './commands/Command';
|
12
|
+
import Mat33 from './math/Mat33';
|
9
13
|
|
10
14
|
// @internal Sort by z-index, low to high
|
11
15
|
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
12
16
|
leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
|
13
17
|
};
|
14
18
|
|
19
|
+
export enum EditorImageEventType {
|
20
|
+
ExportViewportChanged
|
21
|
+
}
|
22
|
+
|
23
|
+
export type EditorImageNotifier = EventDispatcher<EditorImageEventType, { image: EditorImage }>;
|
24
|
+
|
15
25
|
// Handles lookup/storage of elements in the image
|
16
26
|
export default class EditorImage {
|
17
27
|
private root: ImageNode;
|
28
|
+
private background: ImageNode;
|
18
29
|
private componentsById: Record<string, AbstractComponent>;
|
19
30
|
|
31
|
+
/** Viewport for the exported/imported image. */
|
32
|
+
private importExportViewport: Viewport;
|
33
|
+
|
34
|
+
// @internal
|
35
|
+
public readonly notifier: EditorImageNotifier;
|
36
|
+
|
20
37
|
// @internal
|
21
38
|
public constructor() {
|
22
39
|
this.root = new ImageNode();
|
40
|
+
this.background = new ImageNode();
|
23
41
|
this.componentsById = {};
|
42
|
+
|
43
|
+
this.notifier = new EventDispatcher();
|
44
|
+
this.importExportViewport = new Viewport(() => {
|
45
|
+
this.notifier.dispatch(EditorImageEventType.ExportViewportChanged, {
|
46
|
+
image: this,
|
47
|
+
});
|
48
|
+
});
|
49
|
+
|
50
|
+
// Default to a 500x500 image
|
51
|
+
this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
|
24
52
|
}
|
25
53
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
54
|
+
/**
|
55
|
+
* @returns a `Viewport` for rendering the image when importing/exporting.
|
56
|
+
*/
|
57
|
+
public getImportExportViewport() {
|
58
|
+
return this.importExportViewport;
|
59
|
+
}
|
60
|
+
|
61
|
+
public setImportExportRect(imageRect: Rect2): Command {
|
62
|
+
const importExportViewport = this.getImportExportViewport();
|
63
|
+
const origSize = importExportViewport.visibleRect.size;
|
64
|
+
const origTransform = importExportViewport.canvasToScreenTransform;
|
65
|
+
|
66
|
+
return new class extends Command {
|
67
|
+
public apply(editor: Editor) {
|
68
|
+
const viewport = editor.image.getImportExportViewport();
|
69
|
+
viewport.updateScreenSize(imageRect.size);
|
70
|
+
viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
|
71
|
+
editor.queueRerender();
|
72
|
+
}
|
73
|
+
|
74
|
+
public unapply(editor: Editor) {
|
75
|
+
const viewport = editor.image.getImportExportViewport();
|
76
|
+
viewport.updateScreenSize(origSize);
|
77
|
+
viewport.resetTransform(origTransform);
|
78
|
+
editor.queueRerender();
|
79
|
+
}
|
80
|
+
|
81
|
+
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
82
|
+
return localizationTable.resizeOutputCommand(imageRect);
|
83
|
+
}
|
84
|
+
};
|
85
|
+
}
|
86
|
+
|
87
|
+
// Returns all components that make up the background of this image. These
|
88
|
+
// components are rendered below all other components.
|
89
|
+
public getBackgroundComponents(): AbstractComponent[] {
|
90
|
+
const result = [];
|
91
|
+
|
92
|
+
const leaves = this.background.getLeaves();
|
93
|
+
sortLeavesByZIndex(leaves);
|
94
|
+
|
95
|
+
for (const leaf of leaves) {
|
96
|
+
const content = leaf.getContent();
|
97
|
+
|
98
|
+
if (content) {
|
99
|
+
result.push(content);
|
32
100
|
}
|
33
101
|
}
|
34
|
-
return
|
102
|
+
return result;
|
103
|
+
}
|
104
|
+
|
105
|
+
// Returns the parent of the given element, if it exists.
|
106
|
+
public findParent(elem: AbstractComponent): ImageNode|null {
|
107
|
+
return this.background.getChildWithContent(elem) ?? this.root.getChildWithContent(elem);
|
35
108
|
}
|
36
109
|
|
37
110
|
// Forces a re-render of `elem` when the image is next re-rendered as a whole.
|
@@ -49,22 +122,22 @@ export default class EditorImage {
|
|
49
122
|
|
50
123
|
/** @internal */
|
51
124
|
public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
|
125
|
+
this.background.render(screenRenderer, viewport.visibleRect);
|
52
126
|
cache.render(screenRenderer, this.root, viewport);
|
53
127
|
}
|
54
128
|
|
55
|
-
/**
|
56
|
-
|
57
|
-
|
129
|
+
/**
|
130
|
+
* Renders all nodes visible from `viewport` (or all nodes if `viewport = null`)
|
131
|
+
* @internal
|
132
|
+
*/
|
133
|
+
public render(renderer: AbstractRenderer, viewport: Viewport|null) {
|
134
|
+
this.background.render(renderer, viewport?.visibleRect);
|
135
|
+
this.root.render(renderer, viewport?.visibleRect);
|
58
136
|
}
|
59
137
|
|
60
138
|
/** Renders all nodes, even ones not within the viewport. @internal */
|
61
139
|
public renderAll(renderer: AbstractRenderer) {
|
62
|
-
|
63
|
-
sortLeavesByZIndex(leaves);
|
64
|
-
|
65
|
-
for (const leaf of leaves) {
|
66
|
-
leaf.getContent()!.render(renderer, leaf.getBBox());
|
67
|
-
}
|
140
|
+
this.render(renderer, null);
|
68
141
|
}
|
69
142
|
|
70
143
|
/** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
|
@@ -98,8 +171,26 @@ export default class EditorImage {
|
|
98
171
|
}
|
99
172
|
|
100
173
|
private addElementDirectly(elem: AbstractComponent): ImageNode {
|
174
|
+
elem.onAddToImage(this);
|
175
|
+
|
101
176
|
this.componentsById[elem.getId()] = elem;
|
102
|
-
|
177
|
+
|
178
|
+
// If a background component, add to the background. Else,
|
179
|
+
// add to the normal component tree.
|
180
|
+
const parentTree = elem.isBackground() ? this.background : this.root;
|
181
|
+
return parentTree.addLeaf(elem);
|
182
|
+
}
|
183
|
+
|
184
|
+
private removeElementDirectly(element: AbstractComponent) {
|
185
|
+
const container = this.findParent(element);
|
186
|
+
container?.remove();
|
187
|
+
|
188
|
+
if (container) {
|
189
|
+
this.onDestroyElement(element);
|
190
|
+
return true;
|
191
|
+
}
|
192
|
+
|
193
|
+
return false;
|
103
194
|
}
|
104
195
|
|
105
196
|
/**
|
@@ -113,6 +204,11 @@ export default class EditorImage {
|
|
113
204
|
return new EditorImage.AddElementCommand(elem, applyByFlattening);
|
114
205
|
}
|
115
206
|
|
207
|
+
/** @see EditorImage.addElement */
|
208
|
+
public addElement(elem: AbstractComponent, applyByFlattening?: boolean) {
|
209
|
+
return EditorImage.addElement(elem, applyByFlattening);
|
210
|
+
}
|
211
|
+
|
116
212
|
// A Command that can access private [EditorImage] functionality
|
117
213
|
private static AddElementCommand = class extends SerializableCommand {
|
118
214
|
private serializedElem: any;
|
@@ -147,8 +243,7 @@ export default class EditorImage {
|
|
147
243
|
}
|
148
244
|
|
149
245
|
public unapply(editor: Editor) {
|
150
|
-
|
151
|
-
container?.remove();
|
246
|
+
editor.image.removeElementDirectly(this.element);
|
152
247
|
editor.queueRerender();
|
153
248
|
}
|
154
249
|
|
@@ -255,6 +350,19 @@ export class ImageNode {
|
|
255
350
|
return result;
|
256
351
|
}
|
257
352
|
|
353
|
+
// Returns the child of this with the target content or `null` if no
|
354
|
+
// such child exists.
|
355
|
+
public getChildWithContent(target: AbstractComponent): ImageNode|null {
|
356
|
+
const candidates = this.getLeavesIntersectingRegion(target.getBBox());
|
357
|
+
for (const candidate of candidates) {
|
358
|
+
if (candidate.getContent() === target) {
|
359
|
+
return candidate;
|
360
|
+
}
|
361
|
+
}
|
362
|
+
|
363
|
+
return null;
|
364
|
+
}
|
365
|
+
|
258
366
|
// Returns a list of leaves with this as an ancestor.
|
259
367
|
// Like getLeavesInRegion, but does not check whether ancestors are in a given rectangle
|
260
368
|
public getLeaves(): ImageNode[] {
|
@@ -388,6 +496,8 @@ export class ImageNode {
|
|
388
496
|
|
389
497
|
// Remove this node and all of its children
|
390
498
|
public remove() {
|
499
|
+
this.content?.onRemoveFromImage();
|
500
|
+
|
391
501
|
if (!this.parent) {
|
392
502
|
this.content = null;
|
393
503
|
this.children = [];
|
@@ -417,8 +527,13 @@ export class ImageNode {
|
|
417
527
|
this.children = [];
|
418
528
|
}
|
419
529
|
|
420
|
-
public render(renderer: AbstractRenderer, visibleRect
|
421
|
-
|
530
|
+
public render(renderer: AbstractRenderer, visibleRect?: Rect2) {
|
531
|
+
let leaves;
|
532
|
+
if (visibleRect) {
|
533
|
+
leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
|
534
|
+
} else {
|
535
|
+
leaves = this.getLeaves();
|
536
|
+
}
|
422
537
|
sortLeavesByZIndex(leaves);
|
423
538
|
|
424
539
|
for (const leaf of leaves) {
|
package/src/EventDispatcher.ts
CHANGED
@@ -20,6 +20,9 @@
|
|
20
20
|
|
21
21
|
type Listener<Value> = (data: Value)=> void;
|
22
22
|
type CallbackHandler<EventType> = (data: EventType)=> void;
|
23
|
+
export interface DispatcherEventListener {
|
24
|
+
remove: ()=>void;
|
25
|
+
}
|
23
26
|
|
24
27
|
// { @inheritDoc EventDispatcher! }
|
25
28
|
export default class EventDispatcher<EventKeyType extends string|symbol|number, EventMessageType> {
|
@@ -38,7 +41,7 @@ export default class EventDispatcher<EventKeyType extends string|symbol|number,
|
|
38
41
|
}
|
39
42
|
}
|
40
43
|
|
41
|
-
public on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>) {
|
44
|
+
public on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): DispatcherEventListener {
|
42
45
|
if (!this.listeners[eventName]) this.listeners[eventName] = [];
|
43
46
|
this.listeners[eventName]!.push(callback);
|
44
47
|
|
package/src/SVGLoader.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import Color4 from './Color4';
|
2
2
|
import AbstractComponent from './components/AbstractComponent';
|
3
|
+
import ImageBackground, { BackgroundType, imageBackgroundCSSClassName } from './components/ImageBackground';
|
3
4
|
import ImageComponent from './components/ImageComponent';
|
4
5
|
import Stroke from './components/Stroke';
|
5
6
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
@@ -186,6 +187,12 @@ export default class SVGLoader implements ImageLoader {
|
|
186
187
|
await this.onAddComponent?.(elem);
|
187
188
|
}
|
188
189
|
|
190
|
+
private async addBackground(node: SVGPathElement) {
|
191
|
+
const fill = Color4.fromString(node.getAttribute('fill') ?? node.style.fill ?? 'black');
|
192
|
+
const elem = new ImageBackground(BackgroundType.SolidColor, fill);
|
193
|
+
await this.onAddComponent?.(elem);
|
194
|
+
}
|
195
|
+
|
189
196
|
// If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
|
190
197
|
// to prevent storing duplicate transform information when saving the component.
|
191
198
|
private getTransform(elem: SVGElement, supportedAttrs?: string[], computedStyles?: CSSStyleDeclaration): Mat33 {
|
@@ -359,7 +366,11 @@ export default class SVGLoader implements ImageLoader {
|
|
359
366
|
// Continue -- visit the node's children.
|
360
367
|
break;
|
361
368
|
case 'path':
|
362
|
-
|
369
|
+
if (node.classList.contains(imageBackgroundCSSClassName)) {
|
370
|
+
await this.addBackground(node as SVGPathElement);
|
371
|
+
} else {
|
372
|
+
await this.addPath(node as SVGPathElement);
|
373
|
+
}
|
363
374
|
break;
|
364
375
|
case 'text':
|
365
376
|
await this.addText(node as SVGTextElement);
|
package/src/Viewport.ts
CHANGED
@@ -6,7 +6,6 @@ import Rect2 from './math/Rect2';
|
|
6
6
|
import { Point2, Vec2 } from './math/Vec2';
|
7
7
|
import Vec3 from './math/Vec3';
|
8
8
|
import { StrokeDataPoint } from './types';
|
9
|
-
import { EditorEventType, EditorNotifier } from './types';
|
10
9
|
|
11
10
|
// Returns the base type of some type of point/number
|
12
11
|
type PointDataType<T extends Point2|StrokeDataPoint|number> = T extends Point2 ? Point2 : number;
|
@@ -15,6 +14,8 @@ export abstract class ViewportTransform extends Command {
|
|
15
14
|
public abstract readonly transform: Mat33;
|
16
15
|
}
|
17
16
|
|
17
|
+
type TransformChangeCallback = (oldTransform: Mat33, newTransform: Mat33)=> void;
|
18
|
+
|
18
19
|
export class Viewport {
|
19
20
|
// Command that translates/scales the viewport.
|
20
21
|
private static ViewportTransform = class extends ViewportTransform {
|
@@ -82,11 +83,24 @@ export class Viewport {
|
|
82
83
|
private screenRect: Rect2;
|
83
84
|
|
84
85
|
// @internal
|
85
|
-
public constructor(private
|
86
|
+
public constructor(private onTransformChangeCallback: TransformChangeCallback) {
|
86
87
|
this.resetTransform(Mat33.identity);
|
87
88
|
this.screenRect = Rect2.empty;
|
88
89
|
}
|
89
90
|
|
91
|
+
/**
|
92
|
+
* @returns a temporary copy of `this` that does not notify when modified. This is
|
93
|
+
* useful when rendering with a temporarily different viewport.
|
94
|
+
*/
|
95
|
+
public getTemporaryClone(): Viewport {
|
96
|
+
const result = new Viewport(() => {});
|
97
|
+
result.transform = this.transform;
|
98
|
+
result.inverseTransform = this.inverseTransform;
|
99
|
+
result.screenRect = this.screenRect;
|
100
|
+
|
101
|
+
return result;
|
102
|
+
}
|
103
|
+
|
90
104
|
// @internal
|
91
105
|
public updateScreenSize(screenSize: Vec2) {
|
92
106
|
this.screenRect = this.screenRect.resizedTo(screenSize);
|
@@ -120,11 +134,7 @@ export class Viewport {
|
|
120
134
|
const oldTransform = this.transform;
|
121
135
|
this.transform = newTransform;
|
122
136
|
this.inverseTransform = newTransform.inverse();
|
123
|
-
this.
|
124
|
-
kind: EditorEventType.ViewportChanged,
|
125
|
-
newTransform,
|
126
|
-
oldTransform,
|
127
|
-
});
|
137
|
+
this.onTransformChangeCallback?.(oldTransform, newTransform);
|
128
138
|
}
|
129
139
|
|
130
140
|
public get screenToCanvasTransform(): Mat33 {
|
@@ -37,9 +37,15 @@ export default abstract class AbstractComponent {
|
|
37
37
|
protected constructor(
|
38
38
|
// A unique identifier for the type of component
|
39
39
|
private readonly componentKind: string,
|
40
|
+
initialZIndex?: number,
|
40
41
|
) {
|
41
42
|
this.lastChangedTime = (new Date()).getTime();
|
42
|
-
|
43
|
+
|
44
|
+
if (initialZIndex !== undefined) {
|
45
|
+
this.zIndex = initialZIndex;
|
46
|
+
} else {
|
47
|
+
this.zIndex = AbstractComponent.zIndexCounter++;
|
48
|
+
}
|
43
49
|
|
44
50
|
// Create a unique ID.
|
45
51
|
this.id = `${new Date().getTime()}-${Math.random()}`;
|
@@ -96,6 +102,10 @@ export default abstract class AbstractComponent {
|
|
96
102
|
return this.contentBBox;
|
97
103
|
}
|
98
104
|
|
105
|
+
/** Called when this component is added to the given image. */
|
106
|
+
public onAddToImage(_image: EditorImage): void { }
|
107
|
+
public onRemoveFromImage(): void { }
|
108
|
+
|
99
109
|
public abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
100
110
|
|
101
111
|
/** @return true if `lineSegment` intersects this component. */
|
@@ -146,6 +156,12 @@ export default abstract class AbstractComponent {
|
|
146
156
|
return true;
|
147
157
|
}
|
148
158
|
|
159
|
+
// @returns true iff this component should be added to the background, rather than the
|
160
|
+
// foreground of the image.
|
161
|
+
public isBackground(): boolean {
|
162
|
+
return false;
|
163
|
+
}
|
164
|
+
|
149
165
|
// @returns an approximation of the proportional time it takes to render this component.
|
150
166
|
// This is intended to be a rough estimate, but, for example, a stroke with two points sould have
|
151
167
|
// a renderingWeight approximately twice that of a stroke with one point.
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import Color4 from '../Color4';
|
2
|
+
import { Path, Rect2 } from '../math/lib';
|
3
|
+
import createEditor from '../testing/createEditor';
|
4
|
+
import ImageBackground, { BackgroundType, imageBackgroundCSSClassName } from './ImageBackground';
|
5
|
+
|
6
|
+
describe('ImageBackground', () => {
|
7
|
+
it('should render to fill exported SVG', () => {
|
8
|
+
const editor = createEditor();
|
9
|
+
const background = new ImageBackground(BackgroundType.SolidColor, Color4.green);
|
10
|
+
editor.image.addElement(
|
11
|
+
background
|
12
|
+
).apply(editor);
|
13
|
+
|
14
|
+
const expectedImportExportRect = new Rect2(-10, 10, 15, 20);
|
15
|
+
editor.setImportExportRect(expectedImportExportRect).apply(editor);
|
16
|
+
expect(editor.getImportExportRect()).objEq(expectedImportExportRect);
|
17
|
+
|
18
|
+
expect(background.getBBox()).objEq(expectedImportExportRect);
|
19
|
+
|
20
|
+
const rendered = editor.toSVG();
|
21
|
+
const renderedBackground = rendered.querySelector(`.${imageBackgroundCSSClassName}`);
|
22
|
+
|
23
|
+
if (renderedBackground === null) {
|
24
|
+
throw new Error('ImageBackground did not render in exported SVG');
|
25
|
+
}
|
26
|
+
|
27
|
+
expect(renderedBackground.tagName.toLowerCase()).toBe('path');
|
28
|
+
|
29
|
+
const pathString = renderedBackground.getAttribute('d')!;
|
30
|
+
expect(pathString).not.toBeNull();
|
31
|
+
|
32
|
+
const path = Path.fromString(pathString);
|
33
|
+
expect(path.bbox).objEq(editor.getImportExportRect());
|
34
|
+
});
|
35
|
+
});
|