js-draw 0.14.0 → 0.15.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/.github/ISSUE_TEMPLATE/translation.yml +24 -0
- package/CHANGELOG.md +14 -1
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +4 -0
- package/dist/src/Color4.js +22 -0
- package/dist/src/Editor.d.ts +2 -1
- package/dist/src/Editor.js +10 -1
- package/dist/src/EditorImage.d.ts +1 -0
- package/dist/src/EditorImage.js +11 -0
- package/dist/src/commands/UnresolvedCommand.d.ts +14 -0
- package/dist/src/commands/UnresolvedCommand.js +22 -0
- package/dist/src/commands/uniteCommands.js +4 -2
- package/dist/src/components/AbstractComponent.d.ts +0 -1
- package/dist/src/components/AbstractComponent.js +36 -50
- package/dist/src/components/RestylableComponent.d.ts +24 -0
- package/dist/src/components/RestylableComponent.js +80 -0
- package/dist/src/components/Stroke.d.ts +8 -1
- package/dist/src/components/Stroke.js +49 -1
- package/dist/src/components/TextComponent.d.ts +10 -10
- package/dist/src/components/TextComponent.js +46 -13
- package/dist/src/components/lib.d.ts +2 -1
- package/dist/src/components/lib.js +2 -1
- package/dist/src/components/localization.d.ts +1 -0
- package/dist/src/components/localization.js +1 -0
- package/dist/src/rendering/TextRenderingStyle.d.ts +23 -0
- package/dist/src/rendering/TextRenderingStyle.js +20 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +1 -1
- package/dist/src/toolbar/IconProvider.d.ts +2 -1
- package/dist/src/toolbar/IconProvider.js +10 -0
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +10 -4
- package/dist/src/toolbar/widgets/InsertImageWidget.js +2 -1
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +77 -1
- package/dist/src/tools/Pen.js +2 -2
- package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.d.ts +8 -0
- package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.js +22 -0
- package/dist/src/tools/SelectionTool/Selection.js +1 -1
- package/dist/src/tools/SelectionTool/SelectionTool.js +7 -10
- package/dist/src/tools/TextTool.d.ts +1 -1
- package/dist/src/tools/ToolController.js +2 -0
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/package.json +1 -1
- package/src/Color4.test.ts +4 -0
- package/src/Color4.ts +26 -0
- package/src/Editor.toSVG.test.ts +1 -1
- package/src/Editor.ts +12 -1
- package/src/EditorImage.ts +13 -0
- package/src/SVGLoader.ts +2 -1
- package/src/commands/UnresolvedCommand.ts +37 -0
- package/src/commands/uniteCommands.ts +5 -2
- package/src/components/AbstractComponent.transformBy.test.ts +22 -0
- package/src/components/AbstractComponent.ts +41 -59
- package/src/components/RestylableComponent.ts +142 -0
- package/src/components/Stroke.test.ts +68 -0
- package/src/components/Stroke.ts +68 -2
- package/src/components/TextComponent.test.ts +56 -2
- package/src/components/TextComponent.ts +63 -25
- package/src/components/lib.ts +4 -1
- package/src/components/localization.ts +3 -0
- package/src/math/Rect2.test.ts +18 -6
- package/src/rendering/TextRenderingStyle.ts +38 -0
- package/src/rendering/renderers/AbstractRenderer.ts +1 -1
- package/src/rendering/renderers/CanvasRenderer.ts +2 -1
- package/src/rendering/renderers/DummyRenderer.ts +1 -1
- package/src/rendering/renderers/SVGRenderer.ts +1 -1
- package/src/rendering/renderers/TextOnlyRenderer.ts +1 -1
- package/src/toolbar/IconProvider.ts +12 -1
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/toolbar.css +7 -0
- package/src/toolbar/widgets/BaseWidget.ts +12 -4
- package/src/toolbar/widgets/InsertImageWidget.ts +2 -1
- package/src/toolbar/widgets/SelectionToolWidget.ts +95 -1
- package/src/tools/PanZoom.test.ts +2 -1
- package/src/tools/PasteHandler.ts +1 -1
- package/src/tools/Pen.ts +2 -2
- package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +28 -0
- package/src/tools/SelectionTool/Selection.ts +1 -1
- package/src/tools/SelectionTool/SelectionTool.ts +6 -9
- package/src/tools/TextTool.ts +2 -1
- package/src/tools/ToolController.ts +2 -0
- package/src/tools/lib.ts +1 -0
- package/src/tools/localization.ts +2 -0
package/dist/src/Color4.d.ts
CHANGED
@@ -32,6 +32,10 @@ export default class Color4 {
|
|
32
32
|
* ```
|
33
33
|
*/
|
34
34
|
mix(other: Color4, fractionTo: number): Color4;
|
35
|
+
/**
|
36
|
+
* @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
|
37
|
+
*/
|
38
|
+
static average(colors: Color4[]): Color4;
|
35
39
|
private hexString;
|
36
40
|
/**
|
37
41
|
* @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
|
package/dist/src/Color4.js
CHANGED
@@ -121,6 +121,28 @@ export default class Color4 {
|
|
121
121
|
const fractionOfThis = 1 - fractionTo;
|
122
122
|
return new Color4(this.r * fractionOfThis + other.r * fractionTo, this.g * fractionOfThis + other.g * fractionTo, this.b * fractionOfThis + other.b * fractionTo, this.a * fractionOfThis + other.a * fractionTo);
|
123
123
|
}
|
124
|
+
/**
|
125
|
+
* @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
|
126
|
+
*/
|
127
|
+
static average(colors) {
|
128
|
+
let averageA = 0;
|
129
|
+
let averageR = 0;
|
130
|
+
let averageG = 0;
|
131
|
+
let averageB = 0;
|
132
|
+
for (const color of colors) {
|
133
|
+
averageA += color.a;
|
134
|
+
averageR += color.r;
|
135
|
+
averageG += color.g;
|
136
|
+
averageB += color.b;
|
137
|
+
}
|
138
|
+
if (colors.length > 0) {
|
139
|
+
averageA /= colors.length;
|
140
|
+
averageR /= colors.length;
|
141
|
+
averageG /= colors.length;
|
142
|
+
averageB /= colors.length;
|
143
|
+
}
|
144
|
+
return new Color4(averageR, averageG, averageB, averageA);
|
145
|
+
}
|
124
146
|
/**
|
125
147
|
* @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
|
126
148
|
*
|
package/dist/src/Editor.d.ts
CHANGED
@@ -192,7 +192,7 @@ export declare class Editor {
|
|
192
192
|
*/
|
193
193
|
asyncApplyOrUnapplyCommands(commands: Command[], apply: boolean, updateChunkSize: number): Promise<void>;
|
194
194
|
asyncApplyCommands(commands: Command[], chunkSize: number): Promise<void>;
|
195
|
-
asyncUnapplyCommands(commands: Command[], chunkSize: number): Promise<void>;
|
195
|
+
asyncUnapplyCommands(commands: Command[], chunkSize: number, unapplyInReverseOrder?: boolean): Promise<void>;
|
196
196
|
private announceUndoCallback;
|
197
197
|
private announceRedoCallback;
|
198
198
|
private nextRerenderListeners;
|
@@ -204,6 +204,7 @@ export declare class Editor {
|
|
204
204
|
* @returns a promise that resolves when
|
205
205
|
*/
|
206
206
|
queueRerender(): Promise<void>;
|
207
|
+
isRerenderQueued(): boolean;
|
207
208
|
rerender(showImageBounds?: boolean): void;
|
208
209
|
/**
|
209
210
|
* @see {@link Display.getWetInkRenderer} {@link Display.flatten}
|
package/dist/src/Editor.js
CHANGED
@@ -535,8 +535,13 @@ export class Editor {
|
|
535
535
|
asyncApplyCommands(commands, chunkSize) {
|
536
536
|
return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
|
537
537
|
}
|
538
|
+
// If `unapplyInReverseOrder`, commands are reversed before unapplying.
|
538
539
|
// @see {@link #asyncApplyOrUnapplyCommands }
|
539
|
-
asyncUnapplyCommands(commands, chunkSize) {
|
540
|
+
asyncUnapplyCommands(commands, chunkSize, unapplyInReverseOrder = false) {
|
541
|
+
if (unapplyInReverseOrder) {
|
542
|
+
commands = [...commands]; // copy
|
543
|
+
commands.reverse();
|
544
|
+
}
|
540
545
|
return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
|
541
546
|
}
|
542
547
|
/**
|
@@ -561,6 +566,10 @@ export class Editor {
|
|
561
566
|
this.nextRerenderListeners.push(() => resolve());
|
562
567
|
});
|
563
568
|
}
|
569
|
+
// @internal
|
570
|
+
isRerenderQueued() {
|
571
|
+
return this.rerenderQueued;
|
572
|
+
}
|
564
573
|
rerender(showImageBounds = true) {
|
565
574
|
this.display.startRerender();
|
566
575
|
// Don't render if the display has zero size.
|
@@ -10,6 +10,7 @@ export default class EditorImage {
|
|
10
10
|
private componentsById;
|
11
11
|
constructor();
|
12
12
|
findParent(elem: AbstractComponent): ImageNode | null;
|
13
|
+
queueRerenderOf(elem: AbstractComponent): void;
|
13
14
|
/** @internal */
|
14
15
|
renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport): void;
|
15
16
|
/** @internal */
|
package/dist/src/EditorImage.js
CHANGED
@@ -23,6 +23,17 @@ export default class EditorImage {
|
|
23
23
|
}
|
24
24
|
return null;
|
25
25
|
}
|
26
|
+
// Forces a re-render of `elem` when the image is next re-rendered as a whole.
|
27
|
+
// Does nothing if `elem` is not in this.
|
28
|
+
queueRerenderOf(elem) {
|
29
|
+
// TODO: Make more efficient (e.g. increase IDs of all parents,
|
30
|
+
// make cache take into account last modified time instead of IDs, etc.)
|
31
|
+
const parent = this.findParent(elem);
|
32
|
+
if (parent) {
|
33
|
+
parent.remove();
|
34
|
+
this.addElementDirectly(elem);
|
35
|
+
}
|
36
|
+
}
|
26
37
|
/** @internal */
|
27
38
|
renderWithCache(screenRenderer, cache, viewport) {
|
28
39
|
cache.render(screenRenderer, this.root, viewport);
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import EditorImage from '../EditorImage';
|
2
|
+
import AbstractComponent from '../components/AbstractComponent';
|
3
|
+
import SerializableCommand from './SerializableCommand';
|
4
|
+
export type ResolveFromComponentCallback = () => SerializableCommand;
|
5
|
+
/**
|
6
|
+
* A command that requires a component that may or may not be present in the editor when
|
7
|
+
* the command is created.
|
8
|
+
*/
|
9
|
+
export default abstract class UnresolvedSerializableCommand extends SerializableCommand {
|
10
|
+
protected component: AbstractComponent | null;
|
11
|
+
protected readonly componentID: string;
|
12
|
+
protected constructor(commandId: string, componentID: string, component?: AbstractComponent);
|
13
|
+
protected resolveComponent(image: EditorImage): void;
|
14
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import SerializableCommand from './SerializableCommand';
|
2
|
+
/**
|
3
|
+
* A command that requires a component that may or may not be present in the editor when
|
4
|
+
* the command is created.
|
5
|
+
*/
|
6
|
+
export default class UnresolvedSerializableCommand extends SerializableCommand {
|
7
|
+
constructor(commandId, componentID, component) {
|
8
|
+
super(commandId);
|
9
|
+
this.component = component !== null && component !== void 0 ? component : null;
|
10
|
+
this.componentID = componentID;
|
11
|
+
}
|
12
|
+
resolveComponent(image) {
|
13
|
+
if (this.component) {
|
14
|
+
return;
|
15
|
+
}
|
16
|
+
const component = image.lookupElement(this.componentID);
|
17
|
+
if (!component) {
|
18
|
+
throw new Error(`Unable to resolve component with ID ${this.componentID}`);
|
19
|
+
}
|
20
|
+
this.component = component;
|
21
|
+
}
|
22
|
+
}
|
@@ -27,12 +27,14 @@ class NonSerializableUnion extends Command {
|
|
27
27
|
}
|
28
28
|
}
|
29
29
|
unapply(editor) {
|
30
|
+
const commands = [...this.commands];
|
31
|
+
commands.reverse();
|
30
32
|
if (this.applyChunkSize === undefined) {
|
31
|
-
const results =
|
33
|
+
const results = commands.map(cmd => cmd.unapply(editor));
|
32
34
|
return NonSerializableUnion.waitForAll(results);
|
33
35
|
}
|
34
36
|
else {
|
35
|
-
return editor.asyncUnapplyCommands(
|
37
|
+
return editor.asyncUnapplyCommands(commands, this.applyChunkSize, false);
|
36
38
|
}
|
37
39
|
}
|
38
40
|
description(editor, localizationTable) {
|
@@ -48,7 +48,6 @@ export default abstract class AbstractComponent {
|
|
48
48
|
isSelectable(): boolean;
|
49
49
|
getProportionalRenderingTime(): number;
|
50
50
|
private static transformElementCommandId;
|
51
|
-
private static UnresolvedTransformElementCommand;
|
52
51
|
private static TransformElementCommand;
|
53
52
|
/**
|
54
53
|
* @return a description that could be read by a screen reader
|
@@ -2,6 +2,7 @@ var _a;
|
|
2
2
|
import SerializableCommand from '../commands/SerializableCommand';
|
3
3
|
import EditorImage from '../EditorImage';
|
4
4
|
import Mat33 from '../math/Mat33';
|
5
|
+
import UnresolvedSerializableCommand from '../commands/UnresolvedCommand';
|
5
6
|
/**
|
6
7
|
* A base class for everything that can be added to an {@link EditorImage}.
|
7
8
|
*/
|
@@ -75,11 +76,11 @@ export default class AbstractComponent {
|
|
75
76
|
// Returns a command that, when applied, transforms this by [affineTransfm] and
|
76
77
|
// updates the editor.
|
77
78
|
transformBy(affineTransfm) {
|
78
|
-
return new AbstractComponent.TransformElementCommand(affineTransfm, this);
|
79
|
+
return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this);
|
79
80
|
}
|
80
81
|
// Returns a command that updates this component's z-index.
|
81
82
|
setZIndex(newZIndex) {
|
82
|
-
return new AbstractComponent.TransformElementCommand(Mat33.identity, this, newZIndex);
|
83
|
+
return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex, this.getZIndex());
|
83
84
|
}
|
84
85
|
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
85
86
|
isSelectable() {
|
@@ -158,52 +159,35 @@ export default class AbstractComponent {
|
|
158
159
|
AbstractComponent.zIndexCounter = 0;
|
159
160
|
AbstractComponent.deserializationCallbacks = {};
|
160
161
|
AbstractComponent.transformElementCommandId = 'transform-element';
|
161
|
-
AbstractComponent.
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
this.command = null;
|
168
|
-
}
|
169
|
-
resolveCommand(editor) {
|
170
|
-
if (this.command) {
|
171
|
-
return;
|
172
|
-
}
|
173
|
-
const component = editor.image.lookupElement(this.componentID);
|
174
|
-
if (!component) {
|
175
|
-
throw new Error(`Unable to resolve component with ID ${this.componentID}`);
|
176
|
-
}
|
177
|
-
this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component, this.targetZIndex);
|
178
|
-
}
|
179
|
-
apply(editor) {
|
180
|
-
this.resolveCommand(editor);
|
181
|
-
this.command.apply(editor);
|
182
|
-
}
|
183
|
-
unapply(editor) {
|
184
|
-
this.resolveCommand(editor);
|
185
|
-
this.command.unapply(editor);
|
186
|
-
}
|
187
|
-
description(_editor, localizationTable) {
|
188
|
-
return localizationTable.transformedElements(1);
|
189
|
-
}
|
190
|
-
serializeToJSON() {
|
191
|
-
return {
|
192
|
-
id: this.componentID,
|
193
|
-
transfm: this.affineTransfm.toArray(),
|
194
|
-
targetZIndex: this.targetZIndex,
|
195
|
-
};
|
196
|
-
}
|
197
|
-
};
|
198
|
-
AbstractComponent.TransformElementCommand = (_a = class extends SerializableCommand {
|
199
|
-
constructor(affineTransfm, component, targetZIndex) {
|
200
|
-
super(AbstractComponent.transformElementCommandId);
|
162
|
+
AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerializableCommand {
|
163
|
+
// Construct a new TransformElementCommand. `component`, while optional, should
|
164
|
+
// be provided if available. If not provided, it will be fetched from the editor's
|
165
|
+
// document when the command is applied.
|
166
|
+
constructor(affineTransfm, componentID, component, targetZIndex, origZIndex) {
|
167
|
+
super(AbstractComponent.transformElementCommandId, componentID, component);
|
201
168
|
this.affineTransfm = affineTransfm;
|
202
|
-
this.
|
203
|
-
this.origZIndex = component.zIndex;
|
169
|
+
this.origZIndex = origZIndex;
|
204
170
|
this.targetZIndex = targetZIndex !== null && targetZIndex !== void 0 ? targetZIndex : AbstractComponent.zIndexCounter++;
|
171
|
+
// Ensure that we keep drawing on top even after changing the z-index.
|
172
|
+
if (this.targetZIndex >= AbstractComponent.zIndexCounter) {
|
173
|
+
AbstractComponent.zIndexCounter = this.targetZIndex + 1;
|
174
|
+
}
|
175
|
+
if (component && origZIndex === undefined) {
|
176
|
+
this.origZIndex = component.getZIndex();
|
177
|
+
}
|
178
|
+
}
|
179
|
+
resolveComponent(image) {
|
180
|
+
var _a;
|
181
|
+
if (this.component) {
|
182
|
+
return;
|
183
|
+
}
|
184
|
+
super.resolveComponent(image);
|
185
|
+
(_a = this.origZIndex) !== null && _a !== void 0 ? _a : (this.origZIndex = this.component.getZIndex());
|
205
186
|
}
|
206
187
|
updateTransform(editor, newTransfm) {
|
188
|
+
if (!this.component) {
|
189
|
+
throw new Error('this.component is undefined or null!');
|
190
|
+
}
|
207
191
|
// Any parent should have only one direct child.
|
208
192
|
const parent = editor.image.findParent(this.component);
|
209
193
|
let hadParent = false;
|
@@ -219,11 +203,13 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
219
203
|
}
|
220
204
|
}
|
221
205
|
apply(editor) {
|
206
|
+
this.resolveComponent(editor.image);
|
222
207
|
this.component.zIndex = this.targetZIndex;
|
223
208
|
this.updateTransform(editor, this.affineTransfm);
|
224
209
|
editor.queueRerender();
|
225
210
|
}
|
226
211
|
unapply(editor) {
|
212
|
+
this.resolveComponent(editor.image);
|
227
213
|
this.component.zIndex = this.origZIndex;
|
228
214
|
this.updateTransform(editor, this.affineTransfm.inverse());
|
229
215
|
editor.queueRerender();
|
@@ -233,21 +219,21 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
233
219
|
}
|
234
220
|
serializeToJSON() {
|
235
221
|
return {
|
236
|
-
id: this.
|
222
|
+
id: this.componentID,
|
237
223
|
transfm: this.affineTransfm.toArray(),
|
238
224
|
targetZIndex: this.targetZIndex,
|
225
|
+
origZIndex: this.origZIndex,
|
239
226
|
};
|
240
227
|
}
|
241
228
|
},
|
242
229
|
(() => {
|
243
230
|
SerializableCommand.register(AbstractComponent.transformElementCommandId, (json, editor) => {
|
244
|
-
|
231
|
+
var _a, _b;
|
232
|
+
const elem = (_a = editor.image.lookupElement(json.id)) !== null && _a !== void 0 ? _a : undefined;
|
245
233
|
const transform = new Mat33(...json.transfm);
|
246
234
|
const targetZIndex = json.targetZIndex;
|
247
|
-
|
248
|
-
|
249
|
-
}
|
250
|
-
return new AbstractComponent.TransformElementCommand(transform, elem, targetZIndex);
|
235
|
+
const origZIndex = (_b = json.origZIndex) !== null && _b !== void 0 ? _b : undefined;
|
236
|
+
return new AbstractComponent.TransformElementCommand(transform, json.id, elem, targetZIndex, origZIndex);
|
251
237
|
});
|
252
238
|
})(),
|
253
239
|
_a);
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import Color4 from '../Color4';
|
2
|
+
import SerializableCommand from '../commands/SerializableCommand';
|
3
|
+
import Editor from '../Editor';
|
4
|
+
import TextStyle from '../rendering/TextRenderingStyle';
|
5
|
+
import AbstractComponent from './AbstractComponent';
|
6
|
+
export interface ComponentStyle {
|
7
|
+
color?: Color4;
|
8
|
+
textStyle?: TextStyle;
|
9
|
+
}
|
10
|
+
export declare const createRestyleComponentCommand: (initialStyle: ComponentStyle, newStyle: ComponentStyle, component: RestyleableComponent) => SerializableCommand;
|
11
|
+
export declare const isRestylableComponent: (component: AbstractComponent) => component is RestyleableComponent;
|
12
|
+
export interface RestyleableComponent extends AbstractComponent {
|
13
|
+
getStyle(): ComponentStyle;
|
14
|
+
updateStyle(style: ComponentStyle): SerializableCommand;
|
15
|
+
/**
|
16
|
+
* Set the style of this component in a way that can't be undone/redone
|
17
|
+
* (does not create a command).
|
18
|
+
*
|
19
|
+
* Prefer `updateStyle(style).apply(editor)`.
|
20
|
+
*/
|
21
|
+
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
22
|
+
isRestylableComponent: true;
|
23
|
+
}
|
24
|
+
export default RestyleableComponent;
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import Color4 from '../Color4';
|
2
|
+
import SerializableCommand from '../commands/SerializableCommand';
|
3
|
+
import UnresolvedSerializableCommand from '../commands/UnresolvedCommand';
|
4
|
+
import { textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
|
5
|
+
const serializeComponentStyle = (style) => {
|
6
|
+
const result = {};
|
7
|
+
if (style.color) {
|
8
|
+
result.color = style.color.toHexString();
|
9
|
+
}
|
10
|
+
if (style.textStyle) {
|
11
|
+
result.textStyle = textStyleToJSON(style.textStyle);
|
12
|
+
}
|
13
|
+
return result;
|
14
|
+
};
|
15
|
+
const deserializeComponentStyle = (json) => {
|
16
|
+
const color = json.color ? Color4.fromHex(json.color) : undefined;
|
17
|
+
const textStyle = json.textStyle ? textStyleFromJSON(json.textStyle) : undefined;
|
18
|
+
return {
|
19
|
+
color,
|
20
|
+
textStyle,
|
21
|
+
};
|
22
|
+
};
|
23
|
+
// For internal use by Components implementing `updateStyle`:
|
24
|
+
export const createRestyleComponentCommand = (initialStyle, newStyle, component) => {
|
25
|
+
return new DefaultRestyleComponentCommand(initialStyle, newStyle, component.getId(), component);
|
26
|
+
};
|
27
|
+
// Returns true if `component` is a `RestylableComponent`.
|
28
|
+
export const isRestylableComponent = (component) => {
|
29
|
+
const hasMethods = 'getStyle' in component && 'updateStyle' in component && 'forceStyle' in component;
|
30
|
+
if (!hasMethods) {
|
31
|
+
return false;
|
32
|
+
}
|
33
|
+
if (!('isRestylableComponent' in component) || !component['isRestylableComponent']) {
|
34
|
+
return false;
|
35
|
+
}
|
36
|
+
return true;
|
37
|
+
};
|
38
|
+
const defaultRestyleComponentCommandId = 'default-restyle-element';
|
39
|
+
class DefaultRestyleComponentCommand extends UnresolvedSerializableCommand {
|
40
|
+
constructor(originalStyle, newStyle, componentID, component) {
|
41
|
+
super(defaultRestyleComponentCommandId, componentID, component);
|
42
|
+
this.originalStyle = originalStyle;
|
43
|
+
this.newStyle = newStyle;
|
44
|
+
}
|
45
|
+
getComponent(editor) {
|
46
|
+
this.resolveComponent(editor.image);
|
47
|
+
const component = this.component;
|
48
|
+
if (!component || !component['forceStyle'] || !component['updateStyle']) {
|
49
|
+
throw new Error('this.component is missing forceStyle and/or updateStyle methods!');
|
50
|
+
}
|
51
|
+
return component;
|
52
|
+
}
|
53
|
+
apply(editor) {
|
54
|
+
this.getComponent(editor).forceStyle(this.newStyle, editor);
|
55
|
+
}
|
56
|
+
unapply(editor) {
|
57
|
+
this.getComponent(editor).forceStyle(this.originalStyle, editor);
|
58
|
+
}
|
59
|
+
description(_editor, localizationTable) {
|
60
|
+
return localizationTable.restyledElements;
|
61
|
+
}
|
62
|
+
serializeToJSON() {
|
63
|
+
return {
|
64
|
+
id: this.componentID,
|
65
|
+
originalStyle: serializeComponentStyle(this.originalStyle),
|
66
|
+
newStyle: serializeComponentStyle(this.newStyle),
|
67
|
+
};
|
68
|
+
}
|
69
|
+
}
|
70
|
+
(() => {
|
71
|
+
SerializableCommand.register(defaultRestyleComponentCommandId, (json, _editor) => {
|
72
|
+
const origStyle = deserializeComponentStyle(json.originalStyle);
|
73
|
+
const newStyle = deserializeComponentStyle(json.newStyle);
|
74
|
+
const id = json.id;
|
75
|
+
if (typeof json.id !== 'string') {
|
76
|
+
throw new Error(`json.id is of type ${(typeof json.id)}, not string.`);
|
77
|
+
}
|
78
|
+
return new DefaultRestyleComponentCommand(origStyle, newStyle, id);
|
79
|
+
});
|
80
|
+
})();
|
@@ -1,15 +1,22 @@
|
|
1
|
+
import SerializableCommand from '../commands/SerializableCommand';
|
1
2
|
import LineSegment2 from '../math/LineSegment2';
|
2
3
|
import Mat33 from '../math/Mat33';
|
3
4
|
import Path from '../math/Path';
|
4
5
|
import Rect2 from '../math/Rect2';
|
6
|
+
import Editor from '../Editor';
|
5
7
|
import AbstractRenderer, { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
|
6
8
|
import AbstractComponent from './AbstractComponent';
|
7
9
|
import { ImageComponentLocalization } from './localization';
|
8
|
-
|
10
|
+
import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
|
11
|
+
export default class Stroke extends AbstractComponent implements RestyleableComponent {
|
9
12
|
private parts;
|
10
13
|
protected contentBBox: Rect2;
|
14
|
+
readonly isRestylableComponent: true;
|
11
15
|
private approximateRenderingTime;
|
12
16
|
constructor(parts: RenderablePathSpec[]);
|
17
|
+
getStyle(): ComponentStyle;
|
18
|
+
updateStyle(style: ComponentStyle): SerializableCommand;
|
19
|
+
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
13
20
|
intersects(line: LineSegment2): boolean;
|
14
21
|
render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
15
22
|
getProportionalRenderingTime(): number;
|
@@ -2,11 +2,15 @@ import Path from '../math/Path';
|
|
2
2
|
import Rect2 from '../math/Rect2';
|
3
3
|
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
4
4
|
import AbstractComponent from './AbstractComponent';
|
5
|
+
import { createRestyleComponentCommand } from './RestylableComponent';
|
5
6
|
export default class Stroke extends AbstractComponent {
|
6
|
-
// Creates a `Stroke` from the given `parts`.
|
7
|
+
// Creates a `Stroke` from the given `parts`. All parts should have the
|
8
|
+
// same color.
|
7
9
|
constructor(parts) {
|
8
10
|
var _a;
|
9
11
|
super('stroke');
|
12
|
+
// eslint-disable-next-line @typescript-eslint/prefer-as-const
|
13
|
+
this.isRestylableComponent = true;
|
10
14
|
this.approximateRenderingTime = 0;
|
11
15
|
this.parts = [];
|
12
16
|
for (const section of parts) {
|
@@ -29,6 +33,50 @@ export default class Stroke extends AbstractComponent {
|
|
29
33
|
}
|
30
34
|
(_a = this.contentBBox) !== null && _a !== void 0 ? _a : (this.contentBBox = Rect2.empty);
|
31
35
|
}
|
36
|
+
getStyle() {
|
37
|
+
if (this.parts.length === 0) {
|
38
|
+
return {};
|
39
|
+
}
|
40
|
+
const firstPart = this.parts[0];
|
41
|
+
if (firstPart.style.stroke === undefined
|
42
|
+
|| firstPart.style.stroke.width === 0) {
|
43
|
+
return {
|
44
|
+
color: firstPart.style.fill,
|
45
|
+
};
|
46
|
+
}
|
47
|
+
return {
|
48
|
+
color: firstPart.style.stroke.color,
|
49
|
+
};
|
50
|
+
}
|
51
|
+
updateStyle(style) {
|
52
|
+
return createRestyleComponentCommand(this.getStyle(), style, this);
|
53
|
+
}
|
54
|
+
forceStyle(style, editor) {
|
55
|
+
if (!style.color) {
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
this.parts = this.parts.map((part) => {
|
59
|
+
const newStyle = Object.assign(Object.assign({}, part.style), { stroke: part.style.stroke ? Object.assign({}, part.style.stroke) : undefined });
|
60
|
+
// Change the stroke color if a stroked shape. Else,
|
61
|
+
// change the fill.
|
62
|
+
if (newStyle.stroke && newStyle.stroke.width > 0) {
|
63
|
+
newStyle.stroke.color = style.color;
|
64
|
+
}
|
65
|
+
else {
|
66
|
+
newStyle.fill = style.color;
|
67
|
+
}
|
68
|
+
return {
|
69
|
+
path: part.path,
|
70
|
+
startPoint: part.startPoint,
|
71
|
+
commands: part.commands,
|
72
|
+
style: newStyle,
|
73
|
+
};
|
74
|
+
});
|
75
|
+
if (editor) {
|
76
|
+
editor.image.queueRerenderOf(this);
|
77
|
+
editor.queueRerender();
|
78
|
+
}
|
79
|
+
}
|
32
80
|
intersects(line) {
|
33
81
|
for (const part of this.parts) {
|
34
82
|
if (part.path.intersection(line).length > 0) {
|
@@ -1,22 +1,19 @@
|
|
1
|
+
import SerializableCommand from '../commands/SerializableCommand';
|
1
2
|
import LineSegment2 from '../math/LineSegment2';
|
2
3
|
import Mat33 from '../math/Mat33';
|
3
4
|
import Rect2 from '../math/Rect2';
|
5
|
+
import Editor from '../Editor';
|
4
6
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
|
-
import
|
7
|
+
import { TextStyle } from '../rendering/TextRenderingStyle';
|
6
8
|
import AbstractComponent from './AbstractComponent';
|
7
9
|
import { ImageComponentLocalization } from './localization';
|
8
|
-
|
9
|
-
|
10
|
-
fontFamily: string;
|
11
|
-
fontWeight?: string;
|
12
|
-
fontVariant?: string;
|
13
|
-
renderingStyle: RenderingStyle;
|
14
|
-
}
|
15
|
-
export default class TextComponent extends AbstractComponent {
|
10
|
+
import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
|
11
|
+
export default class TextComponent extends AbstractComponent implements RestyleableComponent {
|
16
12
|
protected readonly textObjects: Array<string | TextComponent>;
|
17
13
|
private transform;
|
18
14
|
private style;
|
19
15
|
protected contentBBox: Rect2;
|
16
|
+
readonly isRestylableComponent: true;
|
20
17
|
constructor(textObjects: Array<string | TextComponent>, transform: Mat33, style: TextStyle);
|
21
18
|
static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
|
22
19
|
private static textMeasuringCtx;
|
@@ -28,8 +25,11 @@ export default class TextComponent extends AbstractComponent {
|
|
28
25
|
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
29
26
|
getProportionalRenderingTime(): number;
|
30
27
|
intersects(lineSegment: LineSegment2): boolean;
|
31
|
-
|
28
|
+
getStyle(): ComponentStyle;
|
29
|
+
updateStyle(style: ComponentStyle): SerializableCommand;
|
30
|
+
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
32
31
|
getTextStyle(): TextStyle;
|
32
|
+
getBaselinePos(): import("../lib").Vec3;
|
33
33
|
getTransform(): Mat33;
|
34
34
|
protected applyTransformation(affineTransfm: Mat33): void;
|
35
35
|
protected createClone(): AbstractComponent;
|
@@ -2,8 +2,9 @@ import LineSegment2 from '../math/LineSegment2';
|
|
2
2
|
import Mat33 from '../math/Mat33';
|
3
3
|
import Rect2 from '../math/Rect2';
|
4
4
|
import { Vec2 } from '../math/Vec2';
|
5
|
-
import {
|
5
|
+
import { textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
|
6
6
|
import AbstractComponent from './AbstractComponent';
|
7
|
+
import { createRestyleComponentCommand } from './RestylableComponent';
|
7
8
|
const componentTypeId = 'text';
|
8
9
|
export default class TextComponent extends AbstractComponent {
|
9
10
|
constructor(textObjects, transform, style) {
|
@@ -11,6 +12,8 @@ export default class TextComponent extends AbstractComponent {
|
|
11
12
|
this.textObjects = textObjects;
|
12
13
|
this.transform = transform;
|
13
14
|
this.style = style;
|
15
|
+
// eslint-disable-next-line @typescript-eslint/prefer-as-const
|
16
|
+
this.isRestylableComponent = true;
|
14
17
|
this.recomputeBBox();
|
15
18
|
// If this has no direct children, choose a style representative of this' content
|
16
19
|
// (useful for estimating the style of the TextComponent).
|
@@ -117,12 +120,43 @@ export default class TextComponent extends AbstractComponent {
|
|
117
120
|
}
|
118
121
|
return false;
|
119
122
|
}
|
120
|
-
|
121
|
-
return
|
123
|
+
getStyle() {
|
124
|
+
return {
|
125
|
+
color: this.style.renderingStyle.fill,
|
126
|
+
// Make a copy
|
127
|
+
textStyle: Object.assign(Object.assign({}, this.style), { renderingStyle: Object.assign({}, this.style.renderingStyle) }),
|
128
|
+
};
|
129
|
+
}
|
130
|
+
updateStyle(style) {
|
131
|
+
return createRestyleComponentCommand(this.getStyle(), style, this);
|
132
|
+
}
|
133
|
+
forceStyle(style, editor) {
|
134
|
+
if (style.textStyle) {
|
135
|
+
this.style = style.textStyle;
|
136
|
+
}
|
137
|
+
else if (style.color) {
|
138
|
+
this.style.renderingStyle = Object.assign(Object.assign({}, this.style.renderingStyle), { fill: style.color });
|
139
|
+
}
|
140
|
+
else {
|
141
|
+
return;
|
142
|
+
}
|
143
|
+
for (const child of this.textObjects) {
|
144
|
+
if (child instanceof TextComponent) {
|
145
|
+
child.forceStyle(style, editor);
|
146
|
+
}
|
147
|
+
}
|
148
|
+
if (editor) {
|
149
|
+
editor.image.queueRerenderOf(this);
|
150
|
+
editor.queueRerender();
|
151
|
+
}
|
122
152
|
}
|
153
|
+
// See this.getStyle
|
123
154
|
getTextStyle() {
|
124
155
|
return this.style;
|
125
156
|
}
|
157
|
+
getBaselinePos() {
|
158
|
+
return this.transform.transformVec2(Vec2.zero);
|
159
|
+
}
|
126
160
|
getTransform() {
|
127
161
|
return this.transform;
|
128
162
|
}
|
@@ -148,9 +182,10 @@ export default class TextComponent extends AbstractComponent {
|
|
148
182
|
description(localizationTable) {
|
149
183
|
return localizationTable.text(this.getText());
|
150
184
|
}
|
185
|
+
// Do not rely on the output of `serializeToJSON` taking any particular format.
|
151
186
|
serializeToJSON() {
|
152
|
-
const serializableStyle =
|
153
|
-
const
|
187
|
+
const serializableStyle = textStyleToJSON(this.style);
|
188
|
+
const serializedTextObjects = this.textObjects.map(text => {
|
154
189
|
if (typeof text === 'string') {
|
155
190
|
return {
|
156
191
|
text,
|
@@ -163,19 +198,17 @@ export default class TextComponent extends AbstractComponent {
|
|
163
198
|
}
|
164
199
|
});
|
165
200
|
return {
|
166
|
-
textObjects,
|
201
|
+
textObjects: serializedTextObjects,
|
167
202
|
transform: this.transform.toArray(),
|
168
203
|
style: serializableStyle,
|
169
204
|
};
|
170
205
|
}
|
206
|
+
// @internal
|
171
207
|
static deserializeFromString(json) {
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
fontVariant: json.style.fontVariant,
|
177
|
-
fontFamily: json.style.fontFamily,
|
178
|
-
};
|
208
|
+
if (typeof json === 'string') {
|
209
|
+
json = JSON.parse(json);
|
210
|
+
}
|
211
|
+
const style = textStyleFromJSON(json.style);
|
179
212
|
const textObjects = json.textObjects.map((data) => {
|
180
213
|
var _a;
|
181
214
|
if (((_a = data.text) !== null && _a !== void 0 ? _a : null) !== null) {
|