js-draw 0.1.11 → 0.2.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/.eslintrc.js +1 -0
- package/.firebaserc +5 -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 +13 -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 +133 -4
- package/dist/src/Editor.js +124 -27
- package/dist/src/EditorImage.d.ts +8 -3
- package/dist/src/EditorImage.js +42 -26
- package/dist/src/EventDispatcher.d.ts +18 -0
- package/dist/src/EventDispatcher.js +19 -4
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +4 -3
- package/dist/src/SVGLoader.d.ts +1 -1
- package/dist/src/SVGLoader.js +14 -6
- package/dist/src/UndoRedoHistory.js +15 -2
- package/dist/src/Viewport.d.ts +8 -25
- package/dist/src/Viewport.js +18 -10
- package/dist/src/bundle/bundled.d.ts +1 -2
- package/dist/src/bundle/bundled.js +1 -2
- package/dist/src/commands/Command.d.ts +2 -2
- package/dist/src/commands/Command.js +4 -4
- package/dist/src/commands/Duplicate.d.ts +2 -2
- package/dist/src/commands/Duplicate.js +4 -5
- package/dist/src/commands/Erase.d.ts +2 -2
- package/dist/src/commands/Erase.js +7 -6
- 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 +2 -1
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/components/AbstractComponent.d.ts +16 -11
- package/dist/src/components/AbstractComponent.js +28 -17
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +4 -4
- package/dist/src/components/SVGGlobalAttributesObject.js +8 -2
- package/dist/src/components/Stroke.d.ts +16 -6
- package/dist/src/components/Stroke.js +12 -9
- package/dist/src/components/Text.d.ts +5 -5
- package/dist/src/components/Text.js +9 -9
- package/dist/src/components/UnknownSVGObject.d.ts +4 -4
- package/dist/src/components/UnknownSVGObject.js +7 -2
- package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
- package/dist/src/components/builders/ArrowBuilder.js +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
- package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
- package/dist/src/components/builders/LineBuilder.d.ts +1 -1
- package/dist/src/components/builders/LineBuilder.js +1 -1
- package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
- package/dist/src/components/builders/RectangleBuilder.js +3 -3
- package/dist/src/components/builders/types.d.ts +1 -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/localization.d.ts +1 -0
- package/dist/src/localization.js +5 -1
- package/dist/src/localizations/es.js +1 -1
- package/dist/src/{geometry → math}/LineSegment2.d.ts +0 -0
- package/dist/src/{geometry → math}/LineSegment2.js +0 -0
- package/dist/src/math/Mat33.d.ts +78 -0
- package/dist/src/{geometry → math}/Mat33.js +48 -20
- package/dist/src/{geometry → math}/Path.d.ts +2 -1
- package/dist/src/{geometry → math}/Path.js +59 -52
- package/dist/src/{geometry → math}/Rect2.d.ts +2 -2
- package/dist/src/{geometry → math}/Rect2.js +0 -0
- package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
- package/dist/src/{geometry → math}/Vec2.js +0 -0
- package/dist/src/math/Vec3.d.ts +96 -0
- package/dist/src/{geometry → math}/Vec3.js +63 -15
- package/dist/src/math/lib.d.ts +7 -0
- package/dist/src/math/lib.js +7 -0
- package/dist/src/math/rounding.d.ts +3 -0
- package/dist/src/math/rounding.js +121 -0
- package/dist/src/rendering/Display.d.ts +47 -1
- package/dist/src/rendering/Display.js +60 -15
- package/dist/src/rendering/caching/CacheRecord.d.ts +3 -2
- package/dist/src/rendering/caching/CacheRecord.js +4 -1
- package/dist/src/rendering/caching/CacheRecordManager.d.ts +5 -4
- 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 +10 -11
- package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
- package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
- package/dist/src/rendering/caching/testUtils.js +1 -1
- package/dist/src/rendering/caching/types.d.ts +2 -4
- package/dist/src/rendering/localization.d.ts +2 -0
- package/dist/src/rendering/localization.js +2 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
- package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
- package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
- package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
- package/dist/src/toolbar/HTMLToolbar.js +1 -0
- package/dist/src/toolbar/icons.d.ts +3 -0
- package/dist/src/toolbar/icons.js +142 -132
- package/dist/src/toolbar/localization.d.ts +2 -1
- package/dist/src/toolbar/localization.js +2 -1
- package/dist/src/toolbar/makeColorInput.js +3 -2
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
- package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +2 -0
- package/dist/src/toolbar/widgets/HandToolWidget.js +3 -3
- package/dist/src/toolbar/widgets/PenWidget.js +1 -0
- package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
- package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
- package/dist/src/tools/Eraser.js +1 -1
- package/dist/src/tools/PanZoom.d.ts +1 -1
- package/dist/src/tools/PanZoom.js +24 -14
- 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.d.ts +3 -3
- package/dist/src/tools/SelectionTool.js +51 -28
- package/dist/src/tools/TextTool.js +1 -1
- package/dist/src/types.d.ts +21 -10
- 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 +181 -37
- package/src/EditorImage.test.ts +2 -4
- package/src/EditorImage.ts +46 -28
- package/src/EventDispatcher.ts +21 -6
- package/src/Pointer.ts +4 -3
- package/src/SVGLoader.ts +14 -6
- package/src/UndoRedoHistory.ts +18 -2
- package/src/Viewport.ts +23 -18
- package/src/bundle/bundled.ts +1 -2
- package/src/commands/Command.ts +5 -5
- package/src/commands/Duplicate.ts +4 -5
- package/src/commands/Erase.ts +7 -6
- 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 +3 -1
- package/src/components/AbstractComponent.ts +35 -24
- package/src/components/SVGGlobalAttributesObject.ts +11 -4
- package/src/components/Stroke.test.ts +4 -6
- package/src/components/Stroke.ts +15 -11
- package/src/components/Text.test.ts +2 -2
- package/src/components/Text.ts +9 -10
- package/src/components/UnknownSVGObject.ts +10 -4
- package/src/components/builders/ArrowBuilder.ts +2 -2
- package/src/components/builders/FreehandLineBuilder.ts +190 -80
- package/src/components/builders/LineBuilder.ts +2 -2
- package/src/components/builders/RectangleBuilder.ts +3 -3
- package/src/components/builders/types.ts +1 -1
- package/src/components/lib.ts +9 -0
- package/src/lib.ts +28 -0
- package/src/localization.ts +6 -0
- package/src/localizations/es.ts +2 -1
- package/src/{geometry → math}/LineSegment2.test.ts +0 -0
- package/src/{geometry → math}/LineSegment2.ts +0 -0
- package/src/{geometry → math}/Mat33.test.ts +0 -0
- package/src/{geometry → math}/Mat33.ts +48 -20
- package/src/{geometry → math}/Path.fromString.test.ts +0 -0
- package/src/{geometry → math}/Path.test.ts +0 -0
- package/src/{geometry → math}/Path.toString.test.ts +11 -2
- package/src/{geometry → math}/Path.ts +61 -58
- package/src/{geometry → math}/Rect2.test.ts +0 -0
- package/src/{geometry → math}/Rect2.ts +2 -2
- package/src/{geometry → math}/Vec2.test.ts +0 -0
- package/src/{geometry → math}/Vec2.ts +0 -0
- package/src/{geometry → math}/Vec3.test.ts +0 -0
- package/src/{geometry → math}/Vec3.ts +64 -16
- package/src/math/lib.ts +15 -0
- package/src/math/rounding.test.ts +40 -0
- package/src/math/rounding.ts +147 -0
- package/src/rendering/Display.ts +63 -15
- package/src/rendering/caching/CacheRecord.test.ts +3 -3
- package/src/rendering/caching/CacheRecord.ts +6 -2
- package/src/rendering/caching/CacheRecordManager.ts +34 -8
- package/src/rendering/caching/RenderingCache.test.ts +3 -3
- package/src/rendering/caching/RenderingCache.ts +11 -16
- package/src/rendering/caching/RenderingCacheNode.ts +23 -7
- package/src/rendering/caching/testUtils.ts +1 -1
- package/src/rendering/caching/types.ts +2 -7
- package/src/rendering/localization.ts +4 -0
- package/src/rendering/renderers/AbstractRenderer.ts +4 -4
- package/src/rendering/renderers/CanvasRenderer.ts +5 -5
- package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
- package/src/rendering/renderers/DummyRenderer.ts +4 -4
- package/src/rendering/renderers/SVGRenderer.ts +10 -4
- package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
- package/src/toolbar/HTMLToolbar.ts +1 -0
- package/src/toolbar/icons.ts +157 -137
- package/src/toolbar/localization.ts +4 -2
- package/src/toolbar/makeColorInput.ts +3 -2
- package/src/toolbar/toolbar.css +1 -1
- package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
- package/src/toolbar/widgets/BaseWidget.ts +2 -0
- package/src/toolbar/widgets/HandToolWidget.ts +3 -3
- package/src/toolbar/widgets/PenWidget.ts +2 -0
- package/src/toolbar/widgets/SelectionWidget.ts +46 -41
- package/src/tools/Eraser.ts +2 -2
- package/src/tools/PanZoom.ts +28 -17
- package/src/tools/Pen.ts +11 -2
- package/src/tools/PipetteTool.ts +2 -0
- package/src/tools/SelectionTool.test.ts +2 -4
- package/src/tools/SelectionTool.ts +52 -24
- package/src/tools/TextTool.ts +2 -2
- package/src/tools/UndoRedoShortcut.test.ts +1 -1
- package/src/types.ts +23 -7
- package/tsconfig.json +4 -1
- package/typedoc.json +20 -0
- package/dist/src/geometry/Mat33.d.ts +0 -32
- package/dist/src/geometry/Vec3.d.ts +0 -34
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
@@ -1,19 +1,23 @@
|
|
1
1
|
import Command from './commands/Command';
|
2
2
|
import { CommandLocalization } from './commands/localization';
|
3
3
|
import Editor from './Editor';
|
4
|
-
import Mat33 from './
|
5
|
-
import Rect2 from './
|
6
|
-
import { Point2, Vec2 } from './
|
7
|
-
import Vec3 from './
|
4
|
+
import Mat33 from './math/Mat33';
|
5
|
+
import Rect2 from './math/Rect2';
|
6
|
+
import { Point2, Vec2 } from './math/Vec2';
|
7
|
+
import Vec3 from './math/Vec3';
|
8
8
|
import { StrokeDataPoint } from './types';
|
9
9
|
import { EditorEventType, EditorNotifier } from './types';
|
10
10
|
|
11
11
|
// Returns the base type of some type of point/number
|
12
12
|
type PointDataType<T extends Point2|StrokeDataPoint|number> = T extends Point2 ? Point2 : number;
|
13
13
|
|
14
|
+
export abstract class ViewportTransform extends Command {
|
15
|
+
public abstract readonly transform: Mat33;
|
16
|
+
}
|
17
|
+
|
14
18
|
export class Viewport {
|
15
19
|
// Command that translates/scales the viewport.
|
16
|
-
|
20
|
+
private static ViewportTransform = class extends ViewportTransform {
|
17
21
|
readonly #inverseTransform: Mat33;
|
18
22
|
|
19
23
|
public constructor(public readonly transform: Mat33) {
|
@@ -33,14 +37,14 @@ export class Viewport {
|
|
33
37
|
editor.queueRerender();
|
34
38
|
}
|
35
39
|
|
36
|
-
public description(localizationTable: CommandLocalization): string {
|
40
|
+
public description(editor: Editor, localizationTable: CommandLocalization): string {
|
37
41
|
const result: string[] = [];
|
38
42
|
|
39
43
|
// Describe the transformation's affect on the viewport (note that transformation transforms
|
40
44
|
// the **elements** within the viewport). Assumes the transformation only does rotation/scale/translation.
|
41
|
-
const origVec =
|
45
|
+
const origVec = editor.viewport.visibleRect.center;
|
42
46
|
const linearTransformedVec = this.transform.transformVec3(Vec2.unitX);
|
43
|
-
const affineTransformedVec = this.transform.transformVec2(
|
47
|
+
const affineTransformedVec = this.transform.transformVec2(origVec);
|
44
48
|
|
45
49
|
const scale = linearTransformedVec.magnitude();
|
46
50
|
const rotation = 180 / Math.PI * linearTransformedVec.angle();
|
@@ -48,8 +52,7 @@ export class Viewport {
|
|
48
52
|
|
49
53
|
if (scale > 1.2) {
|
50
54
|
result.push(localizationTable.zoomedIn);
|
51
|
-
}
|
52
|
-
else if (scale < 0.8) {
|
55
|
+
} else if (scale < 0.8) {
|
53
56
|
result.push(localizationTable.zoomedOut);
|
54
57
|
}
|
55
58
|
|
@@ -64,7 +67,7 @@ export class Viewport {
|
|
64
67
|
result.push(localizationTable.movedRight);
|
65
68
|
}
|
66
69
|
|
67
|
-
if (translation.y < minTranslation) {
|
70
|
+
if (translation.y < -minTranslation) {
|
68
71
|
result.push(localizationTable.movedDown);
|
69
72
|
} else if (translation.y > minTranslation) {
|
70
73
|
result.push(localizationTable.movedUp);
|
@@ -78,11 +81,13 @@ export class Viewport {
|
|
78
81
|
private inverseTransform: Mat33;
|
79
82
|
private screenRect: Rect2;
|
80
83
|
|
84
|
+
// @internal
|
81
85
|
public constructor(private notifier: EditorNotifier) {
|
82
86
|
this.resetTransform(Mat33.identity);
|
83
87
|
this.screenRect = Rect2.empty;
|
84
88
|
}
|
85
89
|
|
90
|
+
// @internal
|
86
91
|
public updateScreenSize(screenSize: Vec2) {
|
87
92
|
this.screenRect = this.screenRect.resizedTo(screenSize);
|
88
93
|
}
|
@@ -100,7 +105,11 @@ export class Viewport {
|
|
100
105
|
return this.transform.transformVec2(canvasPoint);
|
101
106
|
}
|
102
107
|
|
103
|
-
|
108
|
+
public static transformBy(transform: Mat33): ViewportTransform {
|
109
|
+
return new Viewport.ViewportTransform(transform);
|
110
|
+
}
|
111
|
+
|
112
|
+
// Updates the transformation directly. Using `transformBy` is preferred.
|
104
113
|
// [newTransform] should map from canvas coordinates to screen coordinates.
|
105
114
|
public resetTransform(newTransform: Mat33 = Mat33.identity) {
|
106
115
|
const oldTransform = this.transform;
|
@@ -131,6 +140,7 @@ export class Viewport {
|
|
131
140
|
return this.transform.transformVec3(Vec3.unitX).magnitude();
|
132
141
|
}
|
133
142
|
|
143
|
+
// Returns the size of one screen pixel in canvas units.
|
134
144
|
public getSizeOfPixelOnCanvas(): number {
|
135
145
|
return 1/this.getScaleFactor();
|
136
146
|
}
|
@@ -140,7 +150,7 @@ export class Viewport {
|
|
140
150
|
return this.transform.transformVec3(Vec3.unitX).angle();
|
141
151
|
}
|
142
152
|
|
143
|
-
// Rounds the given
|
153
|
+
// Rounds the given `point` to a multiple of 10 such that it is within `tolerance` of
|
144
154
|
// its original location. This is useful for preparing data for base-10 conversion.
|
145
155
|
public static roundPoint<T extends Point2|number>(
|
146
156
|
point: T, tolerance: number,
|
@@ -231,9 +241,4 @@ export class Viewport {
|
|
231
241
|
}
|
232
242
|
}
|
233
243
|
|
234
|
-
export namespace Viewport { // eslint-disable-line
|
235
|
-
// Needed to allow accessing as a type. See https://stackoverflow.com/a/68201883
|
236
|
-
export type ViewportTransform = typeof Viewport.ViewportTransform.prototype;
|
237
|
-
}
|
238
|
-
|
239
244
|
export default Viewport;
|
package/src/bundle/bundled.ts
CHANGED
package/src/commands/Command.ts
CHANGED
@@ -8,7 +8,7 @@ export abstract class Command {
|
|
8
8
|
// Called when the command is being deleted
|
9
9
|
public onDrop(_editor: Editor) { }
|
10
10
|
|
11
|
-
public abstract description(localizationTable: EditorLocalization): string;
|
11
|
+
public abstract description(editor: Editor, localizationTable: EditorLocalization): string;
|
12
12
|
|
13
13
|
public static union(a: Command, b: Command): Command {
|
14
14
|
return new class extends Command {
|
@@ -22,9 +22,9 @@ export abstract class Command {
|
|
22
22
|
a.unapply(editor);
|
23
23
|
}
|
24
24
|
|
25
|
-
public description(localizationTable: EditorLocalization) {
|
26
|
-
const aDescription = a.description(localizationTable);
|
27
|
-
const bDescription = b.description(localizationTable);
|
25
|
+
public description(editor: Editor, localizationTable: EditorLocalization) {
|
26
|
+
const aDescription = a.description(editor, localizationTable);
|
27
|
+
const bDescription = b.description(editor, localizationTable);
|
28
28
|
|
29
29
|
if (aDescription === bDescription) {
|
30
30
|
return aDescription;
|
@@ -36,7 +36,7 @@ export abstract class Command {
|
|
36
36
|
}
|
37
37
|
|
38
38
|
public static readonly empty = new class extends Command {
|
39
|
-
public description(_localizationTable: EditorLocalization) { return ''; }
|
39
|
+
public description(_editor: Editor, _localizationTable: EditorLocalization) { return ''; }
|
40
40
|
public apply(_editor: Editor) { }
|
41
41
|
public unapply(_editor: Editor) { }
|
42
42
|
};
|
@@ -24,7 +24,7 @@ export default class Duplicate extends SerializableCommand {
|
|
24
24
|
this.reverse.apply(editor);
|
25
25
|
}
|
26
26
|
|
27
|
-
public description(localizationTable: EditorLocalization): string {
|
27
|
+
public description(_editor: Editor, localizationTable: EditorLocalization): string {
|
28
28
|
if (this.duplicates.length === 0) {
|
29
29
|
return localizationTable.duplicatedNoElements;
|
30
30
|
}
|
@@ -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
@@ -49,7 +49,7 @@ export default class Erase extends SerializableCommand {
|
|
49
49
|
}
|
50
50
|
}
|
51
51
|
|
52
|
-
public description(localizationTable: EditorLocalization): string {
|
52
|
+
public description(_editor: Editor, localizationTable: EditorLocalization): string {
|
53
53
|
if (this.toRemove.length === 0) {
|
54
54
|
return localizationTable.erasedNoElements;
|
55
55
|
}
|
@@ -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
|
+
};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import Rect2 from '../
|
1
|
+
import Rect2 from '../math/Rect2';
|
2
2
|
|
3
3
|
export interface CommandLocalization {
|
4
4
|
movedLeft: string;
|
@@ -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,17 +1,16 @@
|
|
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';
|
5
|
-
import LineSegment2 from '../
|
6
|
-
import Mat33 from '../
|
7
|
-
import Rect2 from '../
|
4
|
+
import LineSegment2 from '../math/LineSegment2';
|
5
|
+
import Mat33 from '../math/Mat33';
|
6
|
+
import Rect2 from '../math/Rect2';
|
8
7
|
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
|
|
@@ -128,13 +129,12 @@ export default abstract class AbstractComponent {
|
|
128
129
|
editor.queueRerender();
|
129
130
|
}
|
130
131
|
|
131
|
-
public description(localizationTable: EditorLocalization) {
|
132
|
+
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
132
133
|
return localizationTable.transformedElements(1);
|
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,6 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
//
|
2
|
+
// Used by `SVGLoader`s to store unrecognised global attributes
|
3
|
+
// (e.g. unrecognised XML namespace declarations).
|
4
|
+
// @internal
|
5
|
+
// @packageDocumentation
|
6
|
+
//
|
7
|
+
|
8
|
+
import LineSegment2 from '../math/LineSegment2';
|
9
|
+
import Mat33 from '../math/Mat33';
|
10
|
+
import Rect2 from '../math/Rect2';
|
4
11
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
12
|
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
6
13
|
import AbstractComponent from './AbstractComponent';
|
@@ -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
|
|
@@ -1,12 +1,10 @@
|
|
1
|
-
/* @jest-environment jsdom */
|
2
|
-
|
3
1
|
import Color4 from '../Color4';
|
4
|
-
import Path from '../
|
5
|
-
import { Vec2 } from '../
|
2
|
+
import Path from '../math/Path';
|
3
|
+
import { Vec2 } from '../math/Vec2';
|
6
4
|
import Stroke from './Stroke';
|
7
5
|
import { loadExpectExtensions } from '../testing/loadExpectExtensions';
|
8
6
|
import createEditor from '../testing/createEditor';
|
9
|
-
import Mat33 from '../
|
7
|
+
import Mat33 from '../math/Mat33';
|
10
8
|
|
11
9
|
loadExpectExtensions();
|
12
10
|
|
@@ -60,7 +58,7 @@ describe('Stroke', () => {
|
|
60
58
|
});
|
61
59
|
|
62
60
|
it('strokes should deserialize from JSON data', () => {
|
63
|
-
const deserialized = Stroke.
|
61
|
+
const deserialized = Stroke.deserializeFromJSON(`[
|
64
62
|
{
|
65
63
|
"style": { "fill": "#f00" },
|
66
64
|
"path": "m0,0 l10,10z"
|
package/src/components/Stroke.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
import LineSegment2 from '../
|
2
|
-
import Mat33 from '../
|
3
|
-
import Path from '../
|
4
|
-
import Rect2 from '../
|
1
|
+
import LineSegment2 from '../math/LineSegment2';
|
2
|
+
import Mat33 from '../math/Mat33';
|
3
|
+
import Path from '../math/Path';
|
4
|
+
import Rect2 from '../math/Rect2';
|
5
5
|
import AbstractRenderer, { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
|
6
6
|
import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
7
7
|
import AbstractComponent from './AbstractComponent';
|
@@ -111,19 +111,23 @@ export default class Stroke extends AbstractComponent {
|
|
111
111
|
return new Stroke(this.parts);
|
112
112
|
}
|
113
113
|
|
114
|
-
protected
|
115
|
-
return
|
114
|
+
protected serializeToJSON() {
|
115
|
+
return this.parts.map(part => {
|
116
116
|
return {
|
117
117
|
style: styleToJSON(part.style),
|
118
118
|
path: part.path.serialize(),
|
119
119
|
};
|
120
|
-
})
|
120
|
+
});
|
121
121
|
}
|
122
122
|
|
123
|
-
|
124
|
-
|
123
|
+
/** @internal */
|
124
|
+
public static deserializeFromJSON(json: any): Stroke {
|
125
|
+
if (typeof json === 'string') {
|
126
|
+
json = JSON.parse(json);
|
127
|
+
}
|
128
|
+
|
125
129
|
if (typeof json !== 'object' || typeof json.length !== 'number') {
|
126
|
-
throw new Error(`${
|
130
|
+
throw new Error(`${json} is missing required field, parts, or parts is of the wrong type.`);
|
127
131
|
}
|
128
132
|
|
129
133
|
const pathSpec: RenderablePathSpec[] = json.map((part: any) => {
|
@@ -134,4 +138,4 @@ export default class Stroke extends AbstractComponent {
|
|
134
138
|
}
|
135
139
|
}
|
136
140
|
|
137
|
-
AbstractComponent.registerComponent('stroke', Stroke.
|
141
|
+
AbstractComponent.registerComponent('stroke', Stroke.deserializeFromJSON);
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
|
-
import Mat33 from '../
|
3
|
-
import Rect2 from '../
|
2
|
+
import Mat33 from '../math/Mat33';
|
3
|
+
import Rect2 from '../math/Rect2';
|
4
4
|
import AbstractComponent from './AbstractComponent';
|
5
5
|
import Text, { TextStyle } from './Text';
|
6
6
|
import { loadExpectExtensions } from '../testing/loadExpectExtensions';
|