js-draw 0.1.12 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +1 -0
- package/.firebaserc +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +34 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/ISSUE_TEMPLATE/translation.md +96 -0
- package/.github/workflows/firebase-hosting-merge.yml +25 -0
- package/.github/workflows/firebase-hosting-pull-request.yml +22 -0
- package/.github/workflows/github-pages.yml +52 -0
- package/CHANGELOG.md +9 -0
- package/README.md +11 -6
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +19 -0
- package/dist/src/Color4.js +24 -3
- package/dist/src/Editor.d.ts +129 -2
- package/dist/src/Editor.js +94 -17
- package/dist/src/EditorImage.d.ts +7 -2
- package/dist/src/EditorImage.js +41 -25
- package/dist/src/EventDispatcher.d.ts +18 -0
- package/dist/src/EventDispatcher.js +19 -4
- package/dist/src/Pointer.js +3 -2
- package/dist/src/UndoRedoHistory.js +15 -2
- package/dist/src/Viewport.js +4 -1
- package/dist/src/bundle/bundled.d.ts +1 -2
- package/dist/src/bundle/bundled.js +1 -2
- package/dist/src/commands/Duplicate.d.ts +1 -1
- package/dist/src/commands/Duplicate.js +3 -4
- package/dist/src/commands/Erase.d.ts +1 -1
- package/dist/src/commands/Erase.js +6 -5
- package/dist/src/commands/SerializableCommand.d.ts +4 -5
- package/dist/src/commands/SerializableCommand.js +12 -4
- package/dist/src/commands/invertCommand.d.ts +4 -0
- package/dist/src/commands/invertCommand.js +44 -0
- package/dist/src/commands/lib.d.ts +6 -0
- package/dist/src/commands/lib.js +6 -0
- package/dist/src/commands/localization.d.ts +1 -0
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/components/AbstractComponent.d.ts +13 -8
- package/dist/src/components/AbstractComponent.js +26 -15
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
- package/dist/src/components/SVGGlobalAttributesObject.js +7 -1
- package/dist/src/components/Stroke.d.ts +12 -2
- package/dist/src/components/Stroke.js +10 -7
- package/dist/src/components/Text.d.ts +2 -2
- package/dist/src/components/Text.js +6 -6
- package/dist/src/components/UnknownSVGObject.d.ts +1 -1
- package/dist/src/components/UnknownSVGObject.js +6 -1
- package/dist/src/components/lib.d.ts +4 -0
- package/dist/src/components/lib.js +4 -0
- package/dist/src/lib.d.ts +25 -0
- package/dist/src/lib.js +25 -0
- package/dist/src/localizations/de.d.ts +3 -0
- package/dist/src/localizations/de.js +4 -0
- package/dist/src/localizations/getLocalizationTable.js +2 -0
- package/dist/src/math/Mat33.d.ts +47 -1
- package/dist/src/math/Mat33.js +48 -20
- package/dist/src/math/Path.js +3 -3
- package/dist/src/math/Rect2.d.ts +2 -2
- package/dist/src/math/Vec3.d.ts +62 -0
- package/dist/src/math/Vec3.js +62 -14
- package/dist/src/math/lib.d.ts +7 -0
- package/dist/src/math/lib.js +7 -0
- package/dist/src/math/rounding.js +1 -0
- package/dist/src/rendering/Display.d.ts +44 -0
- package/dist/src/rendering/Display.js +45 -6
- package/dist/src/rendering/caching/CacheRecord.d.ts +1 -0
- package/dist/src/rendering/caching/CacheRecord.js +3 -0
- package/dist/src/rendering/caching/CacheRecordManager.d.ts +4 -3
- package/dist/src/rendering/caching/CacheRecordManager.js +16 -4
- package/dist/src/rendering/caching/RenderingCache.d.ts +2 -3
- package/dist/src/rendering/caching/RenderingCache.js +9 -10
- package/dist/src/rendering/caching/types.d.ts +1 -3
- package/dist/src/rendering/renderers/CanvasRenderer.js +1 -1
- package/dist/src/toolbar/HTMLToolbar.js +1 -0
- package/dist/src/toolbar/makeColorInput.js +1 -1
- package/dist/src/toolbar/widgets/PenWidget.js +1 -0
- package/dist/src/tools/Pen.d.ts +1 -2
- package/dist/src/tools/Pen.js +8 -1
- package/dist/src/tools/PipetteTool.js +1 -0
- package/dist/src/tools/SelectionTool.js +45 -22
- package/dist/src/types.d.ts +17 -6
- package/dist/src/types.js +7 -5
- package/firebase.json +16 -0
- package/package.json +118 -101
- package/src/Color4.ts +23 -2
- package/src/Editor.ts +147 -25
- package/src/EditorImage.ts +45 -27
- package/src/EventDispatcher.ts +21 -6
- package/src/Pointer.ts +3 -2
- package/src/UndoRedoHistory.ts +18 -2
- package/src/Viewport.ts +5 -2
- package/src/bundle/bundled.ts +1 -2
- package/src/commands/Duplicate.ts +3 -4
- package/src/commands/Erase.ts +6 -5
- package/src/commands/SerializableCommand.ts +17 -9
- package/src/commands/invertCommand.ts +51 -0
- package/src/commands/lib.ts +14 -0
- package/src/commands/localization.ts +2 -0
- package/src/components/AbstractComponent.ts +31 -20
- package/src/components/SVGGlobalAttributesObject.ts +8 -1
- package/src/components/Stroke.test.ts +1 -1
- package/src/components/Stroke.ts +11 -7
- package/src/components/Text.ts +6 -7
- package/src/components/UnknownSVGObject.ts +7 -1
- package/src/components/lib.ts +9 -0
- package/src/lib.ts +28 -0
- package/src/localizations/de.ts +98 -0
- package/src/localizations/getLocalizationTable.ts +2 -0
- package/src/math/Mat33.ts +48 -20
- package/src/math/Path.ts +3 -3
- package/src/math/Rect2.ts +2 -2
- package/src/math/Vec3.ts +62 -14
- package/src/math/lib.ts +15 -0
- package/src/math/rounding.ts +2 -0
- package/src/rendering/Display.ts +46 -6
- package/src/rendering/caching/CacheRecord.test.ts +1 -1
- package/src/rendering/caching/CacheRecord.ts +4 -0
- package/src/rendering/caching/CacheRecordManager.ts +33 -7
- package/src/rendering/caching/RenderingCache.ts +10 -15
- package/src/rendering/caching/types.ts +1 -6
- package/src/rendering/renderers/CanvasRenderer.ts +1 -1
- package/src/styles.js +4 -0
- package/src/toolbar/HTMLToolbar.ts +1 -0
- package/src/toolbar/makeColorInput.ts +1 -1
- package/src/toolbar/toolbar.css +8 -2
- package/src/toolbar/widgets/PenWidget.ts +2 -0
- package/src/tools/PanZoom.ts +0 -1
- package/src/tools/Pen.ts +11 -2
- package/src/tools/PipetteTool.ts +2 -0
- package/src/tools/SelectionTool.ts +46 -18
- package/src/types.ts +19 -3
- package/tsconfig.json +4 -1
- package/typedoc.json +20 -0
package/src/EditorImage.ts
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
import Editor from './Editor';
|
2
2
|
import AbstractRenderer from './rendering/renderers/AbstractRenderer';
|
3
|
-
import Command from './commands/Command';
|
4
3
|
import Viewport from './Viewport';
|
5
4
|
import AbstractComponent from './components/AbstractComponent';
|
6
5
|
import Rect2 from './math/Rect2';
|
@@ -8,6 +7,7 @@ import { EditorLocalization } from './localization';
|
|
8
7
|
import RenderingCache from './rendering/caching/RenderingCache';
|
9
8
|
import SerializableCommand from './commands/SerializableCommand';
|
10
9
|
|
10
|
+
// @internal
|
11
11
|
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
12
12
|
leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
|
13
13
|
};
|
@@ -17,6 +17,7 @@ export default class EditorImage {
|
|
17
17
|
private root: ImageNode;
|
18
18
|
private componentsById: Record<string, AbstractComponent>;
|
19
19
|
|
20
|
+
// @internal
|
20
21
|
public constructor() {
|
21
22
|
this.root = new ImageNode();
|
22
23
|
this.componentsById = {};
|
@@ -33,15 +34,17 @@ export default class EditorImage {
|
|
33
34
|
return null;
|
34
35
|
}
|
35
36
|
|
37
|
+
/** @internal */
|
36
38
|
public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
|
37
39
|
cache.render(screenRenderer, this.root, viewport);
|
38
40
|
}
|
39
41
|
|
42
|
+
/** @internal */
|
40
43
|
public render(renderer: AbstractRenderer, viewport: Viewport) {
|
41
44
|
this.root.render(renderer, viewport.visibleRect);
|
42
45
|
}
|
43
46
|
|
44
|
-
|
47
|
+
/** Renders all nodes, even ones not within the viewport. @internal */
|
45
48
|
public renderAll(renderer: AbstractRenderer) {
|
46
49
|
const leaves = this.root.getLeaves();
|
47
50
|
sortLeavesByZIndex(leaves);
|
@@ -58,6 +61,7 @@ export default class EditorImage {
|
|
58
61
|
return leaves.map(leaf => leaf.getContent()!);
|
59
62
|
}
|
60
63
|
|
64
|
+
/** @internal */
|
61
65
|
public onDestroyElement(elem: AbstractComponent) {
|
62
66
|
delete this.componentsById[elem.getId()];
|
63
67
|
}
|
@@ -71,7 +75,7 @@ export default class EditorImage {
|
|
71
75
|
return this.root.addLeaf(elem);
|
72
76
|
}
|
73
77
|
|
74
|
-
public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false):
|
78
|
+
public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): SerializableCommand {
|
75
79
|
return new EditorImage.AddElementCommand(elem, applyByFlattening);
|
76
80
|
}
|
77
81
|
|
@@ -108,20 +112,21 @@ export default class EditorImage {
|
|
108
112
|
editor.queueRerender();
|
109
113
|
}
|
110
114
|
|
111
|
-
public description(
|
115
|
+
public description(_editor: Editor, localization: EditorLocalization) {
|
112
116
|
return localization.addElementAction(this.element.description(localization));
|
113
117
|
}
|
114
118
|
|
115
|
-
protected
|
116
|
-
return
|
119
|
+
protected serializeToJSON() {
|
120
|
+
return {
|
117
121
|
elemData: this.element.serialize(),
|
118
|
-
}
|
122
|
+
};
|
119
123
|
}
|
120
124
|
|
121
125
|
static {
|
122
|
-
SerializableCommand.register('add-element', (
|
123
|
-
const
|
124
|
-
const
|
126
|
+
SerializableCommand.register('add-element', (json: any, editor: Editor) => {
|
127
|
+
const id = json.elemData.id;
|
128
|
+
const foundElem = editor.image.lookupElement(id);
|
129
|
+
const elem = foundElem ?? AbstractComponent.deserialize(json.elemData);
|
125
130
|
return new EditorImage.AddElementCommand(elem);
|
126
131
|
});
|
127
132
|
}
|
@@ -130,7 +135,7 @@ export default class EditorImage {
|
|
130
135
|
|
131
136
|
type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
|
132
137
|
|
133
|
-
|
138
|
+
/** Part of the Editor's image. @internal */
|
134
139
|
export class ImageNode {
|
135
140
|
private content: AbstractComponent|null;
|
136
141
|
private bbox: Rect2;
|
@@ -182,19 +187,29 @@ export class ImageNode {
|
|
182
187
|
// Returns a list of `ImageNode`s with content (and thus no children).
|
183
188
|
public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
|
184
189
|
const result: ImageNode[] = [];
|
190
|
+
let current: ImageNode|undefined;
|
191
|
+
const workList: ImageNode[] = [];
|
185
192
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
}
|
193
|
+
workList.push(this);
|
194
|
+
const toNext = () => {
|
195
|
+
current = undefined;
|
190
196
|
|
191
|
-
|
192
|
-
|
193
|
-
|
197
|
+
const next = workList.pop();
|
198
|
+
if (next && !isTooSmall?.(next.bbox)) {
|
199
|
+
current = next;
|
200
|
+
|
201
|
+
if (current.content !== null && current.getBBox().intersection(region)) {
|
202
|
+
result.push(current);
|
203
|
+
}
|
194
204
|
|
195
|
-
|
196
|
-
|
197
|
-
|
205
|
+
workList.push(
|
206
|
+
...current.getChildrenIntersectingRegion(region)
|
207
|
+
);
|
208
|
+
}
|
209
|
+
};
|
210
|
+
|
211
|
+
while (workList.length > 0) {
|
212
|
+
toNext();
|
198
213
|
}
|
199
214
|
|
200
215
|
return result;
|
@@ -239,15 +254,18 @@ export class ImageNode {
|
|
239
254
|
// share a parent.
|
240
255
|
const leafBBox = leaf.getBBox();
|
241
256
|
if (leafBBox.containsRect(this.getBBox())) {
|
242
|
-
// Create a node for this' children and for the new content..
|
243
257
|
const nodeForNewLeaf = new ImageNode(this);
|
244
|
-
const nodeForChildren = new ImageNode(this);
|
245
258
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
259
|
+
if (this.children.length < this.targetChildCount) {
|
260
|
+
this.children.push(nodeForNewLeaf);
|
261
|
+
} else {
|
262
|
+
const nodeForChildren = new ImageNode(this);
|
250
263
|
|
264
|
+
nodeForChildren.children = this.children;
|
265
|
+
this.children = [nodeForNewLeaf, nodeForChildren];
|
266
|
+
nodeForChildren.recomputeBBox(true);
|
267
|
+
nodeForChildren.updateParents();
|
268
|
+
}
|
251
269
|
return nodeForNewLeaf.addLeaf(leaf);
|
252
270
|
}
|
253
271
|
|
package/src/EventDispatcher.ts
CHANGED
@@ -1,13 +1,28 @@
|
|
1
|
-
|
1
|
+
/**
|
2
|
+
* Handles notifying listeners of events.
|
3
|
+
*
|
4
|
+
* `EventKeyType` is used to distinguish events (e.g. a `ClickEvent` vs a `TouchEvent`)
|
5
|
+
* while `EventMessageType` is the type of the data sent with an event (can be `void`).
|
6
|
+
*
|
7
|
+
* @example
|
8
|
+
* ```
|
9
|
+
* const dispatcher = new EventDispatcher<'event1'|'event2'|'event3', void>();
|
10
|
+
* dispatcher.on('event1', () => {
|
11
|
+
* console.log('Event 1 triggered.');
|
12
|
+
* });
|
13
|
+
* dispatcher.dispatch('event1');
|
14
|
+
* ```
|
15
|
+
*
|
16
|
+
* @packageDocumentation
|
17
|
+
*/
|
18
|
+
|
19
|
+
// Code shared with Joplin (js-draw was originally intended to be part of Joplin).
|
2
20
|
|
3
21
|
type Listener<Value> = (data: Value)=> void;
|
4
22
|
type CallbackHandler<EventType> = (data: EventType)=> void;
|
5
23
|
|
6
|
-
//
|
7
|
-
// while EventMessageType is the type of the data sent with an event (can be `void`)
|
24
|
+
// { @inheritDoc EventDispatcher! }
|
8
25
|
export default class EventDispatcher<EventKeyType extends string|symbol|number, EventMessageType> {
|
9
|
-
// Partial marks all fields as optional. To initialize with an empty object, this is required.
|
10
|
-
// See https://stackoverflow.com/a/64526384
|
11
26
|
private listeners: Partial<Record<EventKeyType, Array<Listener<EventMessageType>>>>;
|
12
27
|
public constructor() {
|
13
28
|
this.listeners = {};
|
@@ -38,7 +53,7 @@ export default class EventDispatcher<EventKeyType extends string|symbol|number,
|
|
38
53
|
};
|
39
54
|
}
|
40
55
|
|
41
|
-
|
56
|
+
/** Removes an event listener. This is equivalent to calling `.remove()` on the object returned by `.on`. */
|
42
57
|
public off(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>) {
|
43
58
|
const listeners = this.listeners[eventName];
|
44
59
|
if (!listeners) return;
|
package/src/Pointer.ts
CHANGED
@@ -11,7 +11,7 @@ export enum PointerDevice {
|
|
11
11
|
}
|
12
12
|
|
13
13
|
// Provides a snapshot containing information about a pointer. A Pointer
|
14
|
-
// object is immutable
|
14
|
+
// object is immutable — it will not be updated when the pointer's information changes.
|
15
15
|
export default class Pointer {
|
16
16
|
private constructor(
|
17
17
|
// The (x, y) position of the pointer relative to the top-left corner
|
@@ -31,11 +31,12 @@ export default class Pointer {
|
|
31
31
|
// Unique ID for the pointer
|
32
32
|
public readonly id: number,
|
33
33
|
|
34
|
-
// Numeric timestamp (milliseconds, as from (new Date).getTime())
|
34
|
+
// Numeric timestamp (milliseconds, as from `(new Date).getTime()`)
|
35
35
|
public readonly timeStamp: number,
|
36
36
|
) {
|
37
37
|
}
|
38
38
|
|
39
|
+
// Creates a Pointer from a DOM event.
|
39
40
|
public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer {
|
40
41
|
const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
|
41
42
|
|
package/src/UndoRedoHistory.ts
CHANGED
@@ -9,6 +9,7 @@ class UndoRedoHistory {
|
|
9
9
|
private undoStack: Command[];
|
10
10
|
private redoStack: Command[];
|
11
11
|
|
12
|
+
// @internal
|
12
13
|
public constructor(
|
13
14
|
private readonly editor: Editor,
|
14
15
|
private announceRedoCallback: AnnounceRedoCallback,
|
@@ -37,7 +38,12 @@ class UndoRedoHistory {
|
|
37
38
|
elem.onDrop(this.editor);
|
38
39
|
}
|
39
40
|
this.redoStack = [];
|
41
|
+
|
40
42
|
this.fireUpdateEvent();
|
43
|
+
this.editor.notifier.dispatch(EditorEventType.CommandDone, {
|
44
|
+
kind: EditorEventType.CommandDone,
|
45
|
+
command,
|
46
|
+
});
|
41
47
|
}
|
42
48
|
|
43
49
|
// Remove the last command from this' undo stack and apply it.
|
@@ -47,8 +53,13 @@ class UndoRedoHistory {
|
|
47
53
|
this.redoStack.push(command);
|
48
54
|
command.unapply(this.editor);
|
49
55
|
this.announceUndoCallback(command);
|
56
|
+
|
57
|
+
this.fireUpdateEvent();
|
58
|
+
this.editor.notifier.dispatch(EditorEventType.CommandUndone, {
|
59
|
+
kind: EditorEventType.CommandUndone,
|
60
|
+
command,
|
61
|
+
});
|
50
62
|
}
|
51
|
-
this.fireUpdateEvent();
|
52
63
|
}
|
53
64
|
|
54
65
|
public redo() {
|
@@ -57,8 +68,13 @@ class UndoRedoHistory {
|
|
57
68
|
this.undoStack.push(command);
|
58
69
|
command.apply(this.editor);
|
59
70
|
this.announceRedoCallback(command);
|
71
|
+
|
72
|
+
this.fireUpdateEvent();
|
73
|
+
this.editor.notifier.dispatch(EditorEventType.CommandDone, {
|
74
|
+
kind: EditorEventType.CommandDone,
|
75
|
+
command,
|
76
|
+
});
|
60
77
|
}
|
61
|
-
this.fireUpdateEvent();
|
62
78
|
}
|
63
79
|
|
64
80
|
public get undoStackSize(): number {
|
package/src/Viewport.ts
CHANGED
@@ -81,11 +81,13 @@ export class Viewport {
|
|
81
81
|
private inverseTransform: Mat33;
|
82
82
|
private screenRect: Rect2;
|
83
83
|
|
84
|
+
// @internal
|
84
85
|
public constructor(private notifier: EditorNotifier) {
|
85
86
|
this.resetTransform(Mat33.identity);
|
86
87
|
this.screenRect = Rect2.empty;
|
87
88
|
}
|
88
89
|
|
90
|
+
// @internal
|
89
91
|
public updateScreenSize(screenSize: Vec2) {
|
90
92
|
this.screenRect = this.screenRect.resizedTo(screenSize);
|
91
93
|
}
|
@@ -107,7 +109,7 @@ export class Viewport {
|
|
107
109
|
return new Viewport.ViewportTransform(transform);
|
108
110
|
}
|
109
111
|
|
110
|
-
// Updates the transformation directly. Using transformBy is preferred.
|
112
|
+
// Updates the transformation directly. Using `transformBy` is preferred.
|
111
113
|
// [newTransform] should map from canvas coordinates to screen coordinates.
|
112
114
|
public resetTransform(newTransform: Mat33 = Mat33.identity) {
|
113
115
|
const oldTransform = this.transform;
|
@@ -138,6 +140,7 @@ export class Viewport {
|
|
138
140
|
return this.transform.transformVec3(Vec3.unitX).magnitude();
|
139
141
|
}
|
140
142
|
|
143
|
+
// Returns the size of one screen pixel in canvas units.
|
141
144
|
public getSizeOfPixelOnCanvas(): number {
|
142
145
|
return 1/this.getScaleFactor();
|
143
146
|
}
|
@@ -147,7 +150,7 @@ export class Viewport {
|
|
147
150
|
return this.transform.transformVec3(Vec3.unitX).angle();
|
148
151
|
}
|
149
152
|
|
150
|
-
// Rounds the given
|
153
|
+
// Rounds the given `point` to a multiple of 10 such that it is within `tolerance` of
|
151
154
|
// its original location. This is useful for preparing data for base-10 conversion.
|
152
155
|
public static roundPoint<T extends Point2|number>(
|
153
156
|
point: T, tolerance: number,
|
package/src/bundle/bundled.ts
CHANGED
@@ -35,13 +35,12 @@ export default class Duplicate extends SerializableCommand {
|
|
35
35
|
);
|
36
36
|
}
|
37
37
|
|
38
|
-
protected
|
39
|
-
return
|
38
|
+
protected serializeToJSON() {
|
39
|
+
return this.toDuplicate.map(elem => elem.getId());
|
40
40
|
}
|
41
41
|
|
42
42
|
static {
|
43
|
-
SerializableCommand.register('duplicate', (
|
44
|
-
const json = JSON.parse(data);
|
43
|
+
SerializableCommand.register('duplicate', (json: any, editor: Editor) => {
|
45
44
|
const elems = json.map((id: string) => editor.image.lookupElement(id));
|
46
45
|
return new Duplicate(elems);
|
47
46
|
});
|
package/src/commands/Erase.ts
CHANGED
@@ -58,15 +58,16 @@ export default class Erase extends SerializableCommand {
|
|
58
58
|
return localizationTable.eraseAction(description, this.toRemove.length);
|
59
59
|
}
|
60
60
|
|
61
|
-
protected
|
61
|
+
protected serializeToJSON() {
|
62
62
|
const elemIds = this.toRemove.map(elem => elem.getId());
|
63
|
-
return
|
63
|
+
return elemIds;
|
64
64
|
}
|
65
65
|
|
66
66
|
static {
|
67
|
-
SerializableCommand.register('erase', (
|
68
|
-
const
|
69
|
-
|
67
|
+
SerializableCommand.register('erase', (json: any, editor) => {
|
68
|
+
const elems = json
|
69
|
+
.map((elemId: string) => editor.image.lookupElement(elemId))
|
70
|
+
.filter((elem: AbstractComponent|null) => elem !== null);
|
70
71
|
return new Erase(elems);
|
71
72
|
});
|
72
73
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
2
|
import Command from './Command';
|
3
3
|
|
4
|
-
type DeserializationCallback = (data: string, editor: Editor) => SerializableCommand;
|
4
|
+
export type DeserializationCallback = (data: Record<string, any>|any[], editor: Editor) => SerializableCommand;
|
5
5
|
|
6
6
|
export default abstract class SerializableCommand extends Command {
|
7
7
|
public constructor(private commandTypeId: string) {
|
@@ -14,27 +14,35 @@ export default abstract class SerializableCommand extends Command {
|
|
14
14
|
}
|
15
15
|
}
|
16
16
|
|
17
|
-
protected abstract
|
17
|
+
protected abstract serializeToJSON(): string|Record<string, any>|any[];
|
18
18
|
private static deserializationCallbacks: Record<string, DeserializationCallback> = {};
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
// Convert this command to an object that can be passed to `JSON.stringify`.
|
21
|
+
//
|
22
|
+
// Do not rely on the stability of the optupt of this function — it can change
|
23
|
+
// form without a major version increase.
|
24
|
+
public serialize(): Record<string|symbol, any> {
|
25
|
+
return {
|
26
|
+
data: this.serializeToJSON(),
|
23
27
|
commandType: this.commandTypeId,
|
24
|
-
}
|
28
|
+
};
|
25
29
|
}
|
26
30
|
|
27
|
-
|
28
|
-
|
31
|
+
// Convert a `string` containing JSON data (or the output of `JSON.parse`) into a
|
32
|
+
// `Command`.
|
33
|
+
public static deserialize(data: string|Record<string, any>, editor: Editor): SerializableCommand {
|
34
|
+
const json = typeof data === 'string' ? JSON.parse(data) : data;
|
29
35
|
const commandType = json.commandType as string;
|
30
36
|
|
31
37
|
if (!(commandType in SerializableCommand.deserializationCallbacks)) {
|
32
38
|
throw new Error(`Unrecognised command type ${commandType}!`);
|
33
39
|
}
|
34
40
|
|
35
|
-
return SerializableCommand.deserializationCallbacks[commandType](json.data
|
41
|
+
return SerializableCommand.deserializationCallbacks[commandType](json.data, editor);
|
36
42
|
}
|
37
43
|
|
44
|
+
// Register a deserialization callback. This must be called at least once for every subclass of
|
45
|
+
// `SerializableCommand`.
|
38
46
|
public static register(commandTypeId: string, deserialize: DeserializationCallback) {
|
39
47
|
SerializableCommand.deserializationCallbacks[commandTypeId] = deserialize;
|
40
48
|
}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { EditorLocalization } from '../localization';
|
3
|
+
import Command from './Command';
|
4
|
+
import SerializableCommand from './SerializableCommand';
|
5
|
+
|
6
|
+
// Returns a command taht does the opposite of the given command --- `result.apply()` calls
|
7
|
+
// `command.unapply()` and `result.unapply()` calls `command.apply()`.
|
8
|
+
const invertCommand = <T extends Command> (command: T): T extends SerializableCommand ? SerializableCommand : Command => {
|
9
|
+
if (command instanceof SerializableCommand) {
|
10
|
+
// SerializableCommand that does the inverse of [command]
|
11
|
+
return new class extends SerializableCommand {
|
12
|
+
protected serializeToJSON() {
|
13
|
+
return command.serialize();
|
14
|
+
}
|
15
|
+
public apply(editor: Editor): void {
|
16
|
+
command.unapply(editor);
|
17
|
+
}
|
18
|
+
public unapply(editor: Editor): void {
|
19
|
+
command.unapply(editor);
|
20
|
+
}
|
21
|
+
public description(editor: Editor, localizationTable: EditorLocalization): string {
|
22
|
+
return localizationTable.inverseOf(command.description(editor, localizationTable));
|
23
|
+
}
|
24
|
+
}('inverse');
|
25
|
+
} else {
|
26
|
+
// Command that does the inverse of [command].
|
27
|
+
const result = new class extends Command {
|
28
|
+
public apply(editor: Editor) {
|
29
|
+
command.unapply(editor);
|
30
|
+
}
|
31
|
+
|
32
|
+
public unapply(editor: Editor) {
|
33
|
+
command.apply(editor);
|
34
|
+
}
|
35
|
+
|
36
|
+
public description(editor: Editor, localizationTable: EditorLocalization) {
|
37
|
+
return localizationTable.inverseOf(command.description(editor, localizationTable));
|
38
|
+
}
|
39
|
+
};
|
40
|
+
|
41
|
+
// We know that T does not extend SerializableCommand, and thus returning a Command
|
42
|
+
// is appropriate.
|
43
|
+
return result as any;
|
44
|
+
}
|
45
|
+
};
|
46
|
+
|
47
|
+
SerializableCommand.register('inverse', (data, editor) => {
|
48
|
+
return invertCommand(SerializableCommand.deserialize(data, editor));
|
49
|
+
});
|
50
|
+
|
51
|
+
export default invertCommand;
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import Command from './Command';
|
2
|
+
import Duplicate from './Duplicate';
|
3
|
+
import Erase from './Erase';
|
4
|
+
import invertCommand from './invertCommand';
|
5
|
+
import SerializableCommand from './SerializableCommand';
|
6
|
+
|
7
|
+
export {
|
8
|
+
Command,
|
9
|
+
Duplicate,
|
10
|
+
Erase,
|
11
|
+
SerializableCommand,
|
12
|
+
|
13
|
+
invertCommand,
|
14
|
+
};
|
@@ -17,6 +17,7 @@ export interface CommandLocalization {
|
|
17
17
|
addElementAction: (elemDescription: string) => string;
|
18
18
|
eraseAction: (elemDescription: string, numElems: number) => string;
|
19
19
|
duplicateAction: (elemDescription: string, count: number)=> string;
|
20
|
+
inverseOf: (actionDescription: string)=> string;
|
20
21
|
|
21
22
|
selectedElements: (count: number)=>string;
|
22
23
|
}
|
@@ -28,6 +29,7 @@ export const defaultCommandLocalization: CommandLocalization = {
|
|
28
29
|
addElementAction: (componentDescription: string) => `Added ${componentDescription}`,
|
29
30
|
eraseAction: (componentDescription: string, numElems: number) => `Erased ${numElems} ${componentDescription}`,
|
30
31
|
duplicateAction: (componentDescription: string, numElems: number) => `Duplicated ${numElems} ${componentDescription}`,
|
32
|
+
inverseOf: (actionDescription: string) => `Inverse of ${actionDescription}`,
|
31
33
|
elements: 'Elements',
|
32
34
|
erasedNoElements: 'Erased nothing',
|
33
35
|
duplicatedNoElements: 'Duplicated nothing',
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import Command from '../commands/Command';
|
2
1
|
import SerializableCommand from '../commands/SerializableCommand';
|
3
2
|
import Editor from '../Editor';
|
4
3
|
import EditorImage from '../EditorImage';
|
@@ -9,9 +8,9 @@ import { EditorLocalization } from '../localization';
|
|
9
8
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
10
9
|
import { ImageComponentLocalization } from './localization';
|
11
10
|
|
12
|
-
type LoadSaveData = (string[]|Record<symbol, string|number>);
|
11
|
+
export type LoadSaveData = (string[]|Record<symbol, string|number>);
|
13
12
|
export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
|
14
|
-
type DeserializeCallback = (data: string)=>AbstractComponent;
|
13
|
+
export type DeserializeCallback = (data: string)=>AbstractComponent;
|
15
14
|
type ComponentId = string;
|
16
15
|
|
17
16
|
export default abstract class AbstractComponent {
|
@@ -38,6 +37,8 @@ export default abstract class AbstractComponent {
|
|
38
37
|
}
|
39
38
|
}
|
40
39
|
|
40
|
+
// Returns a unique ID for this element.
|
41
|
+
// @see { @link EditorImage!default.lookupElement }
|
41
42
|
public getId() {
|
42
43
|
return this.id;
|
43
44
|
}
|
@@ -77,14 +78,14 @@ export default abstract class AbstractComponent {
|
|
77
78
|
public abstract intersects(lineSegment: LineSegment2): boolean;
|
78
79
|
|
79
80
|
// Return null iff this object cannot be safely serialized/deserialized.
|
80
|
-
protected abstract
|
81
|
+
protected abstract serializeToJSON(): any[]|Record<string, any>|number|string|null;
|
81
82
|
|
82
83
|
// Private helper for transformBy: Apply the given transformation to all points of this.
|
83
84
|
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
84
85
|
|
85
86
|
// Returns a command that, when applied, transforms this by [affineTransfm] and
|
86
87
|
// updates the editor.
|
87
|
-
public transformBy(affineTransfm: Mat33):
|
88
|
+
public transformBy(affineTransfm: Mat33): SerializableCommand {
|
88
89
|
return new AbstractComponent.TransformElementCommand(affineTransfm, this);
|
89
90
|
}
|
90
91
|
|
@@ -133,8 +134,7 @@ export default abstract class AbstractComponent {
|
|
133
134
|
}
|
134
135
|
|
135
136
|
static {
|
136
|
-
SerializableCommand.register('transform-element', (
|
137
|
-
const json = JSON.parse(data);
|
137
|
+
SerializableCommand.register('transform-element', (json: any, editor: Editor) => {
|
138
138
|
const elem = editor.image.lookupElement(json.id);
|
139
139
|
|
140
140
|
if (!elem) {
|
@@ -154,11 +154,11 @@ export default abstract class AbstractComponent {
|
|
154
154
|
});
|
155
155
|
}
|
156
156
|
|
157
|
-
protected
|
158
|
-
return
|
157
|
+
protected serializeToJSON() {
|
158
|
+
return {
|
159
159
|
id: this.component.getId(),
|
160
160
|
transfm: this.affineTransfm.toArray(),
|
161
|
-
}
|
161
|
+
};
|
162
162
|
}
|
163
163
|
};
|
164
164
|
|
@@ -178,26 +178,33 @@ export default abstract class AbstractComponent {
|
|
178
178
|
return clone;
|
179
179
|
}
|
180
180
|
|
181
|
+
// Convert the component to an object that can be passed to
|
182
|
+
// `JSON.stringify`.
|
183
|
+
//
|
184
|
+
// Do not rely on the output of this function to take a particular form —
|
185
|
+
// this function's output can change form without a major version increase.
|
181
186
|
public serialize() {
|
182
|
-
const data = this.
|
187
|
+
const data = this.serializeToJSON();
|
183
188
|
|
184
189
|
if (data === null) {
|
185
190
|
throw new Error(`${this} cannot be serialized.`);
|
186
191
|
}
|
187
192
|
|
188
|
-
return
|
193
|
+
return {
|
189
194
|
name: this.componentKind,
|
190
195
|
zIndex: this.zIndex,
|
191
196
|
id: this.id,
|
192
197
|
loadSaveData: this.loadSaveData,
|
193
198
|
data,
|
194
|
-
}
|
199
|
+
};
|
195
200
|
}
|
196
201
|
|
197
|
-
// Returns true if
|
202
|
+
// Returns true if `data` is not deserializable. May return false even if [data]
|
198
203
|
// is not deserializable.
|
199
|
-
private static isNotDeserializable(
|
200
|
-
|
204
|
+
private static isNotDeserializable(json: any|string) {
|
205
|
+
if (typeof json === 'string') {
|
206
|
+
json = JSON.parse(json);
|
207
|
+
}
|
201
208
|
|
202
209
|
if (typeof json !== 'object') {
|
203
210
|
return true;
|
@@ -214,12 +221,16 @@ export default abstract class AbstractComponent {
|
|
214
221
|
return false;
|
215
222
|
}
|
216
223
|
|
217
|
-
|
218
|
-
|
219
|
-
|
224
|
+
// Convert a string or an object produced by `JSON.parse` into an `AbstractComponent`.
|
225
|
+
public static deserialize(json: string|any): AbstractComponent {
|
226
|
+
if (typeof json === 'string') {
|
227
|
+
json = JSON.parse(json);
|
228
|
+
}
|
229
|
+
|
230
|
+
if (AbstractComponent.isNotDeserializable(json)) {
|
231
|
+
throw new Error(`Element with data ${json} cannot be deserialized.`);
|
220
232
|
}
|
221
233
|
|
222
|
-
const json = JSON.parse(data);
|
223
234
|
const instance = this.deserializationCallbacks[json.name]!(json.data);
|
224
235
|
instance.zIndex = json.zIndex;
|
225
236
|
instance.id = json.id;
|
@@ -1,3 +1,10 @@
|
|
1
|
+
//
|
2
|
+
// Used by `SVGLoader`s to store unrecognised global attributes
|
3
|
+
// (e.g. unrecognised XML namespace declarations).
|
4
|
+
// @internal
|
5
|
+
// @packageDocumentation
|
6
|
+
//
|
7
|
+
|
1
8
|
import LineSegment2 from '../math/LineSegment2';
|
2
9
|
import Mat33 from '../math/Mat33';
|
3
10
|
import Rect2 from '../math/Rect2';
|
@@ -44,7 +51,7 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
|
|
44
51
|
return localization.svgObject;
|
45
52
|
}
|
46
53
|
|
47
|
-
protected
|
54
|
+
protected serializeToJSON(): string | null {
|
48
55
|
return JSON.stringify(this.attrs);
|
49
56
|
}
|
50
57
|
|
@@ -58,7 +58,7 @@ describe('Stroke', () => {
|
|
58
58
|
});
|
59
59
|
|
60
60
|
it('strokes should deserialize from JSON data', () => {
|
61
|
-
const deserialized = Stroke.
|
61
|
+
const deserialized = Stroke.deserializeFromJSON(`[
|
62
62
|
{
|
63
63
|
"style": { "fill": "#f00" },
|
64
64
|
"path": "m0,0 l10,10z"
|