js-draw 0.3.1 → 0.4.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.md +4 -1
- package/CHANGELOG.md +16 -0
- package/README.md +1 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +15 -1
- package/dist/src/Editor.js +221 -78
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +8 -3
- package/dist/src/SVGLoader.d.ts +4 -1
- package/dist/src/SVGLoader.js +78 -33
- package/dist/src/UndoRedoHistory.d.ts +1 -0
- package/dist/src/UndoRedoHistory.js +6 -0
- package/dist/src/Viewport.d.ts +2 -0
- package/dist/src/Viewport.js +26 -5
- package/dist/src/commands/lib.d.ts +2 -1
- package/dist/src/commands/lib.js +2 -1
- package/dist/src/commands/localization.d.ts +1 -0
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/commands/uniteCommands.d.ts +4 -0
- package/dist/src/commands/uniteCommands.js +105 -0
- package/dist/src/components/AbstractComponent.d.ts +2 -0
- package/dist/src/components/AbstractComponent.js +41 -5
- package/dist/src/components/ImageComponent.d.ts +27 -0
- package/dist/src/components/ImageComponent.js +129 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
- package/dist/src/components/lib.d.ts +4 -2
- package/dist/src/components/lib.js +4 -2
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/language/assertions.d.ts +1 -0
- package/dist/src/language/assertions.js +5 -0
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +3 -0
- package/dist/src/math/Mat33.d.ts +38 -2
- package/dist/src/math/Mat33.js +30 -1
- package/dist/src/math/Path.d.ts +1 -1
- package/dist/src/math/Path.js +10 -8
- package/dist/src/math/Vec3.d.ts +11 -1
- package/dist/src/math/Vec3.js +15 -0
- package/dist/src/math/rounding.d.ts +1 -0
- package/dist/src/math/rounding.js +13 -6
- package/dist/src/rendering/localization.d.ts +3 -0
- package/dist/src/rendering/localization.js +3 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/toolbar/HTMLToolbar.js +5 -4
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/tools/BaseTool.d.ts +3 -1
- package/dist/src/tools/BaseTool.js +6 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +144 -0
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
- package/dist/src/tools/SelectionTool/Selection.js +337 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +276 -0
- package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
- package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
- package/dist/src/tools/SelectionTool/types.d.ts +9 -0
- package/dist/src/tools/SelectionTool/types.js +11 -0
- package/dist/src/tools/ToolController.js +37 -28
- package/dist/src/tools/lib.d.ts +2 -1
- package/dist/src/tools/lib.js +2 -1
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist/src/types.d.ts +14 -3
- package/dist/src/types.js +2 -0
- package/package.json +1 -1
- package/src/Editor.css +1 -0
- package/src/Editor.ts +275 -109
- package/src/EditorImage.ts +7 -1
- package/src/Pointer.ts +8 -3
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +30 -6
- package/src/commands/lib.ts +2 -0
- package/src/commands/localization.ts +2 -0
- package/src/commands/uniteCommands.test.ts +23 -0
- package/src/commands/uniteCommands.ts +121 -0
- package/src/components/AbstractComponent.ts +53 -11
- package/src/components/ImageComponent.ts +149 -0
- package/src/components/Text.ts +2 -6
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/language/assertions.ts +6 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +5 -0
- package/src/math/Mat33.test.ts +14 -0
- package/src/math/Mat33.ts +43 -2
- package/src/math/Path.toString.test.ts +12 -1
- package/src/math/Path.ts +11 -9
- package/src/math/Vec3.ts +22 -1
- package/src/math/rounding.test.ts +30 -5
- package/src/math/rounding.ts +16 -7
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +19 -2
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +50 -21
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/BaseTool.ts +9 -1
- package/src/tools/PasteHandler.ts +159 -0
- package/src/tools/Pen.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +455 -0
- package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
- package/src/tools/SelectionTool/SelectionTool.css +22 -0
- package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
- package/src/tools/SelectionTool/SelectionTool.ts +335 -0
- package/src/tools/SelectionTool/TransformMode.ts +114 -0
- package/src/tools/SelectionTool/types.ts +11 -0
- package/src/tools/ToolController.ts +52 -45
- package/src/tools/lib.ts +2 -1
- package/src/tools/localization.ts +8 -0
- package/src/types.ts +17 -3
- package/dist/src/tools/SelectionTool.d.ts +0 -59
- package/dist/src/tools/SelectionTool.js +0 -589
- package/src/tools/SelectionTool.ts +0 -725
@@ -5,6 +5,7 @@ class UndoRedoHistory {
|
|
5
5
|
this.editor = editor;
|
6
6
|
this.announceRedoCallback = announceRedoCallback;
|
7
7
|
this.announceUndoCallback = announceUndoCallback;
|
8
|
+
this.maxUndoRedoStackSize = 700;
|
8
9
|
this.undoStack = [];
|
9
10
|
this.redoStack = [];
|
10
11
|
}
|
@@ -25,6 +26,11 @@ class UndoRedoHistory {
|
|
25
26
|
elem.onDrop(this.editor);
|
26
27
|
}
|
27
28
|
this.redoStack = [];
|
29
|
+
if (this.undoStack.length > this.maxUndoRedoStackSize) {
|
30
|
+
const removeAtOnceCount = 10;
|
31
|
+
const removedElements = this.undoStack.splice(0, removeAtOnceCount);
|
32
|
+
removedElements.forEach(elem => elem.onDrop(this.editor));
|
33
|
+
}
|
28
34
|
this.fireUpdateEvent();
|
29
35
|
this.editor.notifier.dispatch(EditorEventType.CommandDone, {
|
30
36
|
kind: EditorEventType.CommandDone,
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -29,6 +29,8 @@ export declare class Viewport {
|
|
29
29
|
getRotationAngle(): number;
|
30
30
|
static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
|
31
31
|
roundPoint(point: Point2): Point2;
|
32
|
+
static roundScaleRatio(scaleRatio: number, roundAmount?: number): number;
|
33
|
+
computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Mat33;
|
32
34
|
zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;
|
33
35
|
}
|
34
36
|
export default Viewport;
|
package/dist/src/Viewport.js
CHANGED
@@ -29,6 +29,7 @@ export class Viewport {
|
|
29
29
|
updateScreenSize(screenSize) {
|
30
30
|
this.screenRect = this.screenRect.resizedTo(screenSize);
|
31
31
|
}
|
32
|
+
// Get the screen's visible region transformed into canvas space.
|
32
33
|
get visibleRect() {
|
33
34
|
return this.screenRect.transformedBoundingBox(this.inverseTransform);
|
34
35
|
}
|
@@ -72,7 +73,8 @@ export class Viewport {
|
|
72
73
|
getSizeOfPixelOnCanvas() {
|
73
74
|
return 1 / this.getScaleFactor();
|
74
75
|
}
|
75
|
-
// Returns the angle of the canvas in radians
|
76
|
+
// Returns the angle of the canvas in radians.
|
77
|
+
// This is the angle by which the canvas is rotated relative to the screen.
|
76
78
|
getRotationAngle() {
|
77
79
|
return this.transform.transformVec3(Vec3.unitX).angle();
|
78
80
|
}
|
@@ -93,10 +95,20 @@ export class Viewport {
|
|
93
95
|
roundPoint(point) {
|
94
96
|
return Viewport.roundPoint(point, 1 / this.getScaleFactor());
|
95
97
|
}
|
96
|
-
//
|
97
|
-
//
|
98
|
-
|
99
|
-
|
98
|
+
// `roundAmount`: An integer >= 0, larger numbers cause less rounding. Smaller numbers cause more
|
99
|
+
// (as such `roundAmount = 0` does the most rounding).
|
100
|
+
static roundScaleRatio(scaleRatio, roundAmount = 1) {
|
101
|
+
if (Math.abs(scaleRatio) <= 1e-12) {
|
102
|
+
return 0;
|
103
|
+
}
|
104
|
+
// Represent as k 10ⁿ for some n, k ∈ ℤ.
|
105
|
+
const decimalComponent = Math.pow(10, Math.floor(Math.log10(Math.abs(scaleRatio))));
|
106
|
+
const roundAnountFactor = Math.pow(2, roundAmount);
|
107
|
+
scaleRatio = Math.round(scaleRatio / decimalComponent * roundAnountFactor) / roundAnountFactor * decimalComponent;
|
108
|
+
return scaleRatio;
|
109
|
+
}
|
110
|
+
// Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.
|
111
|
+
computeZoomToTransform(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
|
100
112
|
let transform = Mat33.identity;
|
101
113
|
if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
|
102
114
|
throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
|
@@ -136,6 +148,15 @@ export class Viewport {
|
|
136
148
|
console.warn('Unable to zoom to ', toMakeVisible, '! Computed transform', transform, 'is singular.');
|
137
149
|
transform = Mat33.identity;
|
138
150
|
}
|
151
|
+
return transform;
|
152
|
+
}
|
153
|
+
// Returns a Command that transforms the view such that [rect] is visible, and perhaps
|
154
|
+
// centered in the viewport.
|
155
|
+
// Returns null if no transformation is necessary
|
156
|
+
//
|
157
|
+
// @see {@link computeZoomToTransform}
|
158
|
+
zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
|
159
|
+
const transform = this.computeZoomToTransform(toMakeVisible, allowZoomIn, allowZoomOut);
|
139
160
|
return new Viewport.ViewportTransform(transform);
|
140
161
|
}
|
141
162
|
}
|
@@ -3,4 +3,5 @@ import Duplicate from './Duplicate';
|
|
3
3
|
import Erase from './Erase';
|
4
4
|
import invertCommand from './invertCommand';
|
5
5
|
import SerializableCommand from './SerializableCommand';
|
6
|
-
|
6
|
+
import uniteCommands from './uniteCommands';
|
7
|
+
export { Command, Duplicate, Erase, SerializableCommand, invertCommand, uniteCommands, };
|
package/dist/src/commands/lib.js
CHANGED
@@ -3,4 +3,5 @@ import Duplicate from './Duplicate';
|
|
3
3
|
import Erase from './Erase';
|
4
4
|
import invertCommand from './invertCommand';
|
5
5
|
import SerializableCommand from './SerializableCommand';
|
6
|
-
|
6
|
+
import uniteCommands from './uniteCommands';
|
7
|
+
export { Command, Duplicate, Erase, SerializableCommand, invertCommand, uniteCommands, };
|
@@ -17,6 +17,7 @@ export interface CommandLocalization {
|
|
17
17
|
eraseAction: (elemDescription: string, numElems: number) => string;
|
18
18
|
duplicateAction: (elemDescription: string, count: number) => string;
|
19
19
|
inverseOf: (actionDescription: string) => string;
|
20
|
+
unionOf: (actionDescription: string, actionCount: number) => string;
|
20
21
|
selectedElements: (count: number) => string;
|
21
22
|
}
|
22
23
|
export declare const defaultCommandLocalization: CommandLocalization;
|
@@ -5,6 +5,7 @@ export const defaultCommandLocalization = {
|
|
5
5
|
addElementAction: (componentDescription) => `Added ${componentDescription}`,
|
6
6
|
eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
|
7
7
|
duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
|
8
|
+
unionOf: (actionDescription, actionCount) => `Union: ${actionCount} ${actionDescription}`,
|
8
9
|
inverseOf: (actionDescription) => `Inverse of ${actionDescription}`,
|
9
10
|
elements: 'Elements',
|
10
11
|
erasedNoElements: 'Erased nothing',
|
@@ -0,0 +1,4 @@
|
|
1
|
+
import Command from './Command';
|
2
|
+
import SerializableCommand from './SerializableCommand';
|
3
|
+
declare const uniteCommands: <T extends Command>(commands: T[], applyChunkSize?: number) => T extends SerializableCommand ? SerializableCommand : Command;
|
4
|
+
export default uniteCommands;
|
@@ -0,0 +1,105 @@
|
|
1
|
+
import Command from './Command';
|
2
|
+
import SerializableCommand from './SerializableCommand';
|
3
|
+
class NonSerializableUnion extends Command {
|
4
|
+
constructor(commands, applyChunkSize) {
|
5
|
+
super();
|
6
|
+
this.commands = commands;
|
7
|
+
this.applyChunkSize = applyChunkSize;
|
8
|
+
}
|
9
|
+
apply(editor) {
|
10
|
+
if (this.applyChunkSize === undefined) {
|
11
|
+
for (const command of this.commands) {
|
12
|
+
command.apply(editor);
|
13
|
+
}
|
14
|
+
}
|
15
|
+
else {
|
16
|
+
editor.asyncApplyCommands(this.commands, this.applyChunkSize);
|
17
|
+
}
|
18
|
+
}
|
19
|
+
unapply(editor) {
|
20
|
+
if (this.applyChunkSize === undefined) {
|
21
|
+
for (const command of this.commands) {
|
22
|
+
command.unapply(editor);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
else {
|
26
|
+
editor.asyncUnapplyCommands(this.commands, this.applyChunkSize);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
description(editor, localizationTable) {
|
30
|
+
const descriptions = [];
|
31
|
+
let lastDescription = null;
|
32
|
+
let duplicateDescriptionCount = 0;
|
33
|
+
for (const part of this.commands) {
|
34
|
+
const description = part.description(editor, localizationTable);
|
35
|
+
if (description !== lastDescription && lastDescription !== null) {
|
36
|
+
descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
|
37
|
+
lastDescription = null;
|
38
|
+
duplicateDescriptionCount = 0;
|
39
|
+
}
|
40
|
+
duplicateDescriptionCount++;
|
41
|
+
lastDescription !== null && lastDescription !== void 0 ? lastDescription : (lastDescription = description);
|
42
|
+
}
|
43
|
+
if (duplicateDescriptionCount > 1) {
|
44
|
+
descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
|
45
|
+
}
|
46
|
+
else if (duplicateDescriptionCount === 1) {
|
47
|
+
descriptions.push(lastDescription);
|
48
|
+
}
|
49
|
+
return descriptions.join(', ');
|
50
|
+
}
|
51
|
+
}
|
52
|
+
class SerializableUnion extends SerializableCommand {
|
53
|
+
constructor(commands, applyChunkSize) {
|
54
|
+
super('union');
|
55
|
+
this.commands = commands;
|
56
|
+
this.applyChunkSize = applyChunkSize;
|
57
|
+
this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize);
|
58
|
+
}
|
59
|
+
serializeToJSON() {
|
60
|
+
return {
|
61
|
+
applyChunkSize: this.applyChunkSize,
|
62
|
+
data: this.commands.map(command => command.serialize()),
|
63
|
+
};
|
64
|
+
}
|
65
|
+
apply(editor) {
|
66
|
+
this.nonserializableCommand.apply(editor);
|
67
|
+
}
|
68
|
+
unapply(editor) {
|
69
|
+
this.nonserializableCommand.unapply(editor);
|
70
|
+
}
|
71
|
+
description(editor, localizationTable) {
|
72
|
+
return this.nonserializableCommand.description(editor, localizationTable);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
const uniteCommands = (commands, applyChunkSize) => {
|
76
|
+
let allSerializable = true;
|
77
|
+
for (const command of commands) {
|
78
|
+
if (!(command instanceof SerializableCommand)) {
|
79
|
+
allSerializable = false;
|
80
|
+
break;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
if (!allSerializable) {
|
84
|
+
return new NonSerializableUnion(commands, applyChunkSize);
|
85
|
+
}
|
86
|
+
else {
|
87
|
+
const castedCommands = commands;
|
88
|
+
return new SerializableUnion(castedCommands, applyChunkSize);
|
89
|
+
}
|
90
|
+
};
|
91
|
+
SerializableCommand.register('union', (data, editor) => {
|
92
|
+
if (typeof data.data.length !== 'number') {
|
93
|
+
throw new Error('Unions of commands must serialize to lists of serialization data.');
|
94
|
+
}
|
95
|
+
const applyChunkSize = data.applyChunkSize;
|
96
|
+
if (typeof applyChunkSize !== 'number' && applyChunkSize !== undefined) {
|
97
|
+
throw new Error('serialized applyChunkSize is neither undefined nor a number.');
|
98
|
+
}
|
99
|
+
const commands = [];
|
100
|
+
for (const part of data.data) {
|
101
|
+
commands.push(SerializableCommand.deserialize(part, editor));
|
102
|
+
}
|
103
|
+
return uniteCommands(commands, applyChunkSize);
|
104
|
+
});
|
105
|
+
export default uniteCommands;
|
@@ -28,6 +28,8 @@ export default abstract class AbstractComponent {
|
|
28
28
|
protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
|
29
29
|
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
30
30
|
transformBy(affineTransfm: Mat33): SerializableCommand;
|
31
|
+
private static transformElementCommandId;
|
32
|
+
private static UnresolvedTransformElementCommand;
|
31
33
|
private static TransformElementCommand;
|
32
34
|
abstract description(localizationTable: ImageComponentLocalization): string;
|
33
35
|
protected abstract createClone(): AbstractComponent;
|
@@ -113,9 +113,45 @@ export default class AbstractComponent {
|
|
113
113
|
// Topmost z-index
|
114
114
|
AbstractComponent.zIndexCounter = 0;
|
115
115
|
AbstractComponent.deserializationCallbacks = {};
|
116
|
+
AbstractComponent.transformElementCommandId = 'transform-element';
|
117
|
+
AbstractComponent.UnresolvedTransformElementCommand = class extends SerializableCommand {
|
118
|
+
constructor(affineTransfm, componentID) {
|
119
|
+
super(AbstractComponent.transformElementCommandId);
|
120
|
+
this.affineTransfm = affineTransfm;
|
121
|
+
this.componentID = componentID;
|
122
|
+
this.command = null;
|
123
|
+
}
|
124
|
+
resolveCommand(editor) {
|
125
|
+
if (this.command) {
|
126
|
+
return;
|
127
|
+
}
|
128
|
+
const component = editor.image.lookupElement(this.componentID);
|
129
|
+
if (!component) {
|
130
|
+
throw new Error(`Unable to resolve component with ID ${this.componentID}`);
|
131
|
+
}
|
132
|
+
this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component);
|
133
|
+
}
|
134
|
+
apply(editor) {
|
135
|
+
this.resolveCommand(editor);
|
136
|
+
this.command.apply(editor);
|
137
|
+
}
|
138
|
+
unapply(editor) {
|
139
|
+
this.resolveCommand(editor);
|
140
|
+
this.command.unapply(editor);
|
141
|
+
}
|
142
|
+
description(_editor, localizationTable) {
|
143
|
+
return localizationTable.transformedElements(1);
|
144
|
+
}
|
145
|
+
serializeToJSON() {
|
146
|
+
return {
|
147
|
+
id: this.componentID,
|
148
|
+
transfm: this.affineTransfm.toArray(),
|
149
|
+
};
|
150
|
+
}
|
151
|
+
};
|
116
152
|
AbstractComponent.TransformElementCommand = (_a = class extends SerializableCommand {
|
117
153
|
constructor(affineTransfm, component) {
|
118
|
-
super(
|
154
|
+
super(AbstractComponent.transformElementCommandId);
|
119
155
|
this.affineTransfm = affineTransfm;
|
120
156
|
this.component = component;
|
121
157
|
this.origZIndex = component.zIndex;
|
@@ -155,13 +191,13 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
155
191
|
}
|
156
192
|
},
|
157
193
|
(() => {
|
158
|
-
SerializableCommand.register(
|
194
|
+
SerializableCommand.register(AbstractComponent.transformElementCommandId, (json, editor) => {
|
159
195
|
const elem = editor.image.lookupElement(json.id);
|
196
|
+
const transform = new Mat33(...json.transfm);
|
160
197
|
if (!elem) {
|
161
|
-
|
198
|
+
return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
|
162
199
|
}
|
163
|
-
|
164
|
-
return new AbstractComponent.TransformElementCommand(new Mat33(...transform), elem);
|
200
|
+
return new AbstractComponent.TransformElementCommand(transform, elem);
|
165
201
|
});
|
166
202
|
})(),
|
167
203
|
_a);
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import LineSegment2 from '../math/LineSegment2';
|
2
|
+
import Mat33, { Mat33Array } from '../math/Mat33';
|
3
|
+
import Rect2 from '../math/Rect2';
|
4
|
+
import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';
|
5
|
+
import AbstractComponent from './AbstractComponent';
|
6
|
+
import { ImageComponentLocalization } from './localization';
|
7
|
+
export default class ImageComponent extends AbstractComponent {
|
8
|
+
protected contentBBox: Rect2;
|
9
|
+
private image;
|
10
|
+
constructor(image: RenderableImage);
|
11
|
+
private getImageRect;
|
12
|
+
private recomputeBBox;
|
13
|
+
static fromImage(elem: HTMLImageElement, transform: Mat33): Promise<ImageComponent>;
|
14
|
+
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
15
|
+
intersects(lineSegment: LineSegment2): boolean;
|
16
|
+
protected serializeToJSON(): {
|
17
|
+
src: string;
|
18
|
+
label: string | undefined;
|
19
|
+
width: number;
|
20
|
+
height: number;
|
21
|
+
transform: Mat33Array;
|
22
|
+
};
|
23
|
+
protected applyTransformation(affineTransfm: Mat33): void;
|
24
|
+
description(localizationTable: ImageComponentLocalization): string;
|
25
|
+
protected createClone(): AbstractComponent;
|
26
|
+
static deserializeFromJSON(data: any): ImageComponent;
|
27
|
+
}
|
@@ -0,0 +1,129 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
10
|
+
import Mat33 from '../math/Mat33';
|
11
|
+
import Rect2 from '../math/Rect2';
|
12
|
+
import AbstractComponent from './AbstractComponent';
|
13
|
+
// Represents a raster image.
|
14
|
+
export default class ImageComponent extends AbstractComponent {
|
15
|
+
constructor(image) {
|
16
|
+
var _a, _b, _c;
|
17
|
+
super('image-component');
|
18
|
+
this.image = Object.assign(Object.assign({}, image), { label: (_c = (_b = (_a = image.label) !== null && _a !== void 0 ? _a : image.image.getAttribute('alt')) !== null && _b !== void 0 ? _b : image.image.getAttribute('aria-label')) !== null && _c !== void 0 ? _c : undefined });
|
19
|
+
const isHTMLImageElem = (elem) => {
|
20
|
+
return elem.getAttribute('src') !== undefined;
|
21
|
+
};
|
22
|
+
if (isHTMLImageElem(image.image) && !image.image.complete) {
|
23
|
+
image.image.onload = () => this.recomputeBBox();
|
24
|
+
}
|
25
|
+
this.recomputeBBox();
|
26
|
+
}
|
27
|
+
getImageRect() {
|
28
|
+
return new Rect2(0, 0, this.image.image.width, this.image.image.height);
|
29
|
+
}
|
30
|
+
recomputeBBox() {
|
31
|
+
this.contentBBox = this.getImageRect();
|
32
|
+
this.contentBBox = this.contentBBox.transformedBoundingBox(this.image.transform);
|
33
|
+
}
|
34
|
+
// Load from an image. Waits for the image to load if incomplete.
|
35
|
+
static fromImage(elem, transform) {
|
36
|
+
var _a;
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
38
|
+
if (!elem.complete) {
|
39
|
+
yield new Promise((resolve, reject) => {
|
40
|
+
elem.onload = resolve;
|
41
|
+
elem.onerror = reject;
|
42
|
+
elem.onabort = reject;
|
43
|
+
});
|
44
|
+
}
|
45
|
+
let width, height;
|
46
|
+
if (typeof elem.width === 'number' && typeof elem.height === 'number'
|
47
|
+
&& elem.width !== 0 && elem.height !== 0) {
|
48
|
+
width = elem.width;
|
49
|
+
height = elem.height;
|
50
|
+
}
|
51
|
+
else {
|
52
|
+
width = elem.clientWidth;
|
53
|
+
height = elem.clientHeight;
|
54
|
+
}
|
55
|
+
let image;
|
56
|
+
let url = (_a = elem.src) !== null && _a !== void 0 ? _a : '';
|
57
|
+
if (!url.startsWith('data:image/')) {
|
58
|
+
// Convert to a data URL:
|
59
|
+
const canvas = document.createElement('canvas');
|
60
|
+
canvas.width = width;
|
61
|
+
canvas.height = height;
|
62
|
+
const ctx = canvas.getContext('2d');
|
63
|
+
ctx.drawImage(elem, 0, 0, canvas.width, canvas.height);
|
64
|
+
url = canvas.toDataURL();
|
65
|
+
image = canvas;
|
66
|
+
}
|
67
|
+
else {
|
68
|
+
image = new Image();
|
69
|
+
image.src = url;
|
70
|
+
image.width = width;
|
71
|
+
image.height = height;
|
72
|
+
}
|
73
|
+
return new ImageComponent({
|
74
|
+
image,
|
75
|
+
base64Url: url,
|
76
|
+
transform: transform,
|
77
|
+
});
|
78
|
+
});
|
79
|
+
}
|
80
|
+
render(canvas, _visibleRect) {
|
81
|
+
canvas.drawImage(this.image);
|
82
|
+
}
|
83
|
+
intersects(lineSegment) {
|
84
|
+
const rect = this.getImageRect();
|
85
|
+
const edges = rect.getEdges().map(edge => edge.transformedBy(this.image.transform));
|
86
|
+
for (const edge of edges) {
|
87
|
+
if (edge.intersects(lineSegment)) {
|
88
|
+
return true;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
return false;
|
92
|
+
}
|
93
|
+
serializeToJSON() {
|
94
|
+
return {
|
95
|
+
src: this.image.base64Url,
|
96
|
+
label: this.image.label,
|
97
|
+
// Store the width and height for bounding box computations while the image is loading.
|
98
|
+
width: this.image.image.width,
|
99
|
+
height: this.image.image.height,
|
100
|
+
transform: this.image.transform.toArray(),
|
101
|
+
};
|
102
|
+
}
|
103
|
+
applyTransformation(affineTransfm) {
|
104
|
+
this.image.transform = affineTransfm.rightMul(this.image.transform);
|
105
|
+
this.recomputeBBox();
|
106
|
+
}
|
107
|
+
description(localizationTable) {
|
108
|
+
return this.image.label ? localizationTable.imageNode(this.image.label) : localizationTable.unlabeledImageNode;
|
109
|
+
}
|
110
|
+
createClone() {
|
111
|
+
return new ImageComponent(Object.assign({}, this.image));
|
112
|
+
}
|
113
|
+
static deserializeFromJSON(data) {
|
114
|
+
if (!(typeof data.src === 'string')) {
|
115
|
+
throw new Error(`${data} has invalid format! Expected src property.`);
|
116
|
+
}
|
117
|
+
const image = new Image();
|
118
|
+
image.src = data.src;
|
119
|
+
image.width = data.width;
|
120
|
+
image.height = data.height;
|
121
|
+
return new ImageComponent({
|
122
|
+
image: image,
|
123
|
+
base64Url: image.src,
|
124
|
+
label: data.label,
|
125
|
+
transform: new Mat33(...data.transform),
|
126
|
+
});
|
127
|
+
}
|
128
|
+
}
|
129
|
+
AbstractComponent.registerComponent('image-component', ImageComponent.deserializeFromJSON);
|
@@ -237,9 +237,9 @@ export default class FreehandLineBuilder {
|
|
237
237
|
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
238
238
|
return upperBoundary.intersects(lowerBoundary).length > 0;
|
239
239
|
};
|
240
|
-
// If the boundaries have
|
240
|
+
// If the boundaries have intersections, increasing the half vector's length could fix this.
|
241
241
|
if (boundariesIntersect()) {
|
242
|
-
halfVec = halfVec.times(
|
242
|
+
halfVec = halfVec.times(1.1);
|
243
243
|
}
|
244
244
|
// Each starts at startPt ± startVec
|
245
245
|
const lowerCurve = {
|
@@ -1,6 +1,8 @@
|
|
1
1
|
export * from './builders/types';
|
2
2
|
export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
|
3
|
-
|
3
|
+
export * from './AbstractComponent';
|
4
|
+
export { default as AbstractComponent } from './AbstractComponent';
|
4
5
|
import Stroke from './Stroke';
|
5
6
|
import Text from './Text';
|
6
|
-
|
7
|
+
import ImageComponent from './ImageComponent';
|
8
|
+
export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
|
@@ -1,6 +1,8 @@
|
|
1
1
|
export * from './builders/types';
|
2
2
|
export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
|
3
|
-
|
3
|
+
export * from './AbstractComponent';
|
4
|
+
export { default as AbstractComponent } from './AbstractComponent';
|
4
5
|
import Stroke from './Stroke';
|
5
6
|
import Text from './Text';
|
6
|
-
|
7
|
+
import ImageComponent from './ImageComponent';
|
8
|
+
export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const assertUnreachable: (key: never) => never;
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import Mat33 from './Mat33';
|
1
2
|
import Rect2 from './Rect2';
|
2
3
|
import { Vec2, Point2 } from './Vec2';
|
3
4
|
interface IntersectionResult {
|
@@ -17,6 +18,7 @@ export default class LineSegment2 {
|
|
17
18
|
intersection(other: LineSegment2): IntersectionResult | null;
|
18
19
|
intersects(other: LineSegment2): boolean;
|
19
20
|
closestPointTo(target: Point2): import("./Vec3").default;
|
21
|
+
transformedBy(affineTransfm: Mat33): LineSegment2;
|
20
22
|
toString(): string;
|
21
23
|
}
|
22
24
|
export {};
|
@@ -116,6 +116,9 @@ export default class LineSegment2 {
|
|
116
116
|
return this.p1;
|
117
117
|
}
|
118
118
|
}
|
119
|
+
transformedBy(affineTransfm) {
|
120
|
+
return new LineSegment2(affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2));
|
121
|
+
}
|
119
122
|
toString() {
|
120
123
|
return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
|
121
124
|
}
|
package/dist/src/math/Mat33.d.ts
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
import { Point2, Vec2 } from './Vec2';
|
2
2
|
import Vec3 from './Vec3';
|
3
|
+
export declare type Mat33Array = [
|
4
|
+
number,
|
5
|
+
number,
|
6
|
+
number,
|
7
|
+
number,
|
8
|
+
number,
|
9
|
+
number,
|
10
|
+
number,
|
11
|
+
number,
|
12
|
+
number
|
13
|
+
];
|
3
14
|
/**
|
4
15
|
* Represents a three dimensional linear transformation or
|
5
16
|
* a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
|
@@ -68,11 +79,36 @@ export default class Mat33 {
|
|
68
79
|
* ...
|
69
80
|
* ```
|
70
81
|
*/
|
71
|
-
toArray():
|
82
|
+
toArray(): Mat33Array;
|
83
|
+
/**
|
84
|
+
* @example
|
85
|
+
* ```
|
86
|
+
* new Mat33(
|
87
|
+
* 1, 2, 3,
|
88
|
+
* 4, 5, 6,
|
89
|
+
* 7, 8, 9,
|
90
|
+
* ).mapEntries(component => component - 1);
|
91
|
+
* // → ⎡ 0, 1, 2 ⎤
|
92
|
+
* // ⎢ 3, 4, 5 ⎥
|
93
|
+
* // ⎣ 6, 7, 8 ⎦
|
94
|
+
* ```
|
95
|
+
*/
|
96
|
+
mapEntries(mapping: (component: number) => number): Mat33;
|
72
97
|
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
|
73
98
|
static translation(amount: Vec2): Mat33;
|
74
99
|
static zRotation(radians: number, center?: Point2): Mat33;
|
75
100
|
static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
|
76
|
-
/**
|
101
|
+
/** @see {@link !fromCSSMatrix} */
|
102
|
+
toCSSMatrix(): string;
|
103
|
+
/**
|
104
|
+
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
|
105
|
+
*
|
106
|
+
* Note that such a matrix has the form,
|
107
|
+
* ```
|
108
|
+
* ⎡ a c e ⎤
|
109
|
+
* ⎢ b d f ⎥
|
110
|
+
* ⎣ 0 0 1 ⎦
|
111
|
+
* ```
|
112
|
+
*/
|
77
113
|
static fromCSSMatrix(cssString: string): Mat33;
|
78
114
|
}
|
package/dist/src/math/Mat33.js
CHANGED
@@ -183,6 +183,22 @@ export default class Mat33 {
|
|
183
183
|
this.c1, this.c2, this.c3,
|
184
184
|
];
|
185
185
|
}
|
186
|
+
/**
|
187
|
+
* @example
|
188
|
+
* ```
|
189
|
+
* new Mat33(
|
190
|
+
* 1, 2, 3,
|
191
|
+
* 4, 5, 6,
|
192
|
+
* 7, 8, 9,
|
193
|
+
* ).mapEntries(component => component - 1);
|
194
|
+
* // → ⎡ 0, 1, 2 ⎤
|
195
|
+
* // ⎢ 3, 4, 5 ⎥
|
196
|
+
* // ⎣ 6, 7, 8 ⎦
|
197
|
+
* ```
|
198
|
+
*/
|
199
|
+
mapEntries(mapping) {
|
200
|
+
return new Mat33(mapping(this.a1), mapping(this.a2), mapping(this.a3), mapping(this.b1), mapping(this.b2), mapping(this.b3), mapping(this.c1), mapping(this.c2), mapping(this.c3));
|
201
|
+
}
|
186
202
|
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
|
187
203
|
static translation(amount) {
|
188
204
|
// When transforming Vec2s by a 3x3 matrix, we give the input
|
@@ -214,7 +230,20 @@ export default class Mat33 {
|
|
214
230
|
// Translate such that [center] goes to (0, 0)
|
215
231
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
216
232
|
}
|
217
|
-
/**
|
233
|
+
/** @see {@link !fromCSSMatrix} */
|
234
|
+
toCSSMatrix() {
|
235
|
+
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
|
236
|
+
}
|
237
|
+
/**
|
238
|
+
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
|
239
|
+
*
|
240
|
+
* Note that such a matrix has the form,
|
241
|
+
* ```
|
242
|
+
* ⎡ a c e ⎤
|
243
|
+
* ⎢ b d f ⎥
|
244
|
+
* ⎣ 0 0 1 ⎦
|
245
|
+
* ```
|
246
|
+
*/
|
218
247
|
static fromCSSMatrix(cssString) {
|
219
248
|
if (cssString === '' || cssString === 'none') {
|
220
249
|
return Mat33.identity;
|