js-draw 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/translation.yml +8 -0
- package/CHANGELOG.md +15 -0
- package/README.md +1 -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 +14 -5
- package/dist/src/EditorImage.d.ts +1 -0
- package/dist/src/EditorImage.js +11 -0
- package/dist/src/SVGLoader.js +8 -2
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +6 -3
- 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 +30 -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/math/Path.js +10 -3
- 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 +30 -3
- package/dist/src/toolbar/IconProvider.js +37 -2
- 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.d.ts +6 -0
- package/dist/src/tools/SelectionTool/Selection.js +13 -4
- package/dist/src/tools/SelectionTool/SelectionTool.js +9 -12
- package/dist/src/tools/SelectionTool/TransformMode.js +1 -1
- 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 +16 -5
- package/src/EditorImage.ts +13 -0
- package/src/SVGLoader.ts +11 -3
- package/src/Viewport.ts +7 -3
- package/src/commands/UnresolvedCommand.ts +37 -0
- package/src/commands/uniteCommands.ts +5 -2
- package/src/components/AbstractComponent.ts +36 -61
- 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/Path.toString.test.ts +10 -0
- package/src/math/Path.ts +11 -3
- 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 +40 -7
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/toolbar.css +3 -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 +17 -6
- package/src/tools/SelectionTool/SelectionTool.ts +9 -13
- package/src/tools/SelectionTool/TransformMode.ts +1 -1
- 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
@@ -196,7 +196,7 @@ export class Editor {
|
|
196
196
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
197
197
|
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
198
198
|
// pinch-zooming.
|
199
|
-
if (!evt.ctrlKey) {
|
199
|
+
if (!evt.ctrlKey && !evt.metaKey) {
|
200
200
|
if (!this.settings.wheelEventsEnabled) {
|
201
201
|
return;
|
202
202
|
}
|
@@ -213,7 +213,7 @@ export class Editor {
|
|
213
213
|
else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
214
214
|
delta = delta.times(100);
|
215
215
|
}
|
216
|
-
if (evt.ctrlKey) {
|
216
|
+
if (evt.ctrlKey || evt.metaKey) {
|
217
217
|
delta = Vec3.of(0, 0, evt.deltaY);
|
218
218
|
}
|
219
219
|
// Ensure that `pos` is relative to `this.container`
|
@@ -441,7 +441,7 @@ export class Editor {
|
|
441
441
|
else if (this.toolController.dispatchInputEvent({
|
442
442
|
kind: InputEvtType.KeyPressEvent,
|
443
443
|
key: evt.key,
|
444
|
-
ctrlKey: evt.ctrlKey,
|
444
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
445
445
|
altKey: evt.altKey,
|
446
446
|
})) {
|
447
447
|
evt.preventDefault();
|
@@ -454,7 +454,7 @@ export class Editor {
|
|
454
454
|
if (this.toolController.dispatchInputEvent({
|
455
455
|
kind: InputEvtType.KeyUpEvent,
|
456
456
|
key: evt.key,
|
457
|
-
ctrlKey: evt.ctrlKey,
|
457
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
458
458
|
altKey: evt.altKey,
|
459
459
|
})) {
|
460
460
|
evt.preventDefault();
|
@@ -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);
|
package/dist/src/SVGLoader.js
CHANGED
@@ -202,7 +202,13 @@ export default class SVGLoader {
|
|
202
202
|
}
|
203
203
|
// Compute styles.
|
204
204
|
const computedStyles = window.getComputedStyle(elem);
|
205
|
-
const
|
205
|
+
const fontSizeExp = /^([-0-9.e]+)px/i;
|
206
|
+
// In some environments, computedStyles.fontSize can be increased by the system.
|
207
|
+
// Thus, to prevent text from growing on load/save, prefer .style.fontSize.
|
208
|
+
let fontSizeMatch = fontSizeExp.exec(elem.style.fontSize);
|
209
|
+
if (!fontSizeMatch) {
|
210
|
+
fontSizeMatch = fontSizeExp.exec(computedStyles.fontSize);
|
211
|
+
}
|
206
212
|
const supportedStyleAttrs = [
|
207
213
|
'fontFamily',
|
208
214
|
'transform',
|
@@ -390,7 +396,7 @@ export default class SVGLoader {
|
|
390
396
|
<meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
|
391
397
|
<meta charset='utf-8'/>
|
392
398
|
</head>
|
393
|
-
<body>
|
399
|
+
<body style='font-size: 12px;'>
|
394
400
|
<script>
|
395
401
|
console.error('JavaScript should not be able to run here!');
|
396
402
|
throw new Error(
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -44,6 +44,7 @@ export declare class Viewport {
|
|
44
44
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
45
45
|
*/
|
46
46
|
getScaleFactorToNearestPowerOfTen(): number;
|
47
|
+
private getScaleFactorToNearestPowerOf;
|
47
48
|
snapToGrid(canvasPos: Point2): Vec3;
|
48
49
|
/** Returns the size of one screen pixel in canvas units. */
|
49
50
|
getSizeOfPixelOnCanvas(): number;
|
package/dist/src/Viewport.js
CHANGED
@@ -84,13 +84,16 @@ export class Viewport {
|
|
84
84
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
85
85
|
*/
|
86
86
|
getScaleFactorToNearestPowerOfTen() {
|
87
|
+
return this.getScaleFactorToNearestPowerOf(10);
|
88
|
+
}
|
89
|
+
getScaleFactorToNearestPowerOf(powerOf) {
|
87
90
|
const scaleFactor = this.getScaleFactor();
|
88
|
-
return Math.pow(
|
91
|
+
return Math.pow(powerOf, Math.round(Math.log(scaleFactor) / Math.log(powerOf)));
|
89
92
|
}
|
90
93
|
snapToGrid(canvasPos) {
|
91
94
|
const snapCoordinate = (coordinate) => {
|
92
|
-
const scaleFactor = this.
|
93
|
-
const roundFactor = scaleFactor /
|
95
|
+
const scaleFactor = this.getScaleFactorToNearestPowerOf(2);
|
96
|
+
const roundFactor = scaleFactor / 50;
|
94
97
|
const snapped = Math.round(coordinate * roundFactor) / roundFactor;
|
95
98
|
return snapped;
|
96
99
|
};
|
@@ -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);
|
83
84
|
}
|
84
85
|
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
85
86
|
isSelectable() {
|
@@ -158,52 +159,31 @@ 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) {
|
167
|
+
super(AbstractComponent.transformElementCommandId, componentID, component);
|
201
168
|
this.affineTransfm = affineTransfm;
|
202
|
-
this.
|
203
|
-
this.origZIndex = component.zIndex;
|
169
|
+
this.origZIndex = null;
|
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
|
+
}
|
176
|
+
resolveComponent(image) {
|
177
|
+
if (this.component) {
|
178
|
+
return;
|
179
|
+
}
|
180
|
+
super.resolveComponent(image);
|
181
|
+
this.origZIndex = this.component.getZIndex();
|
205
182
|
}
|
206
183
|
updateTransform(editor, newTransfm) {
|
184
|
+
if (!this.component) {
|
185
|
+
throw new Error('this.component is undefined or null!');
|
186
|
+
}
|
207
187
|
// Any parent should have only one direct child.
|
208
188
|
const parent = editor.image.findParent(this.component);
|
209
189
|
let hadParent = false;
|
@@ -219,11 +199,13 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
219
199
|
}
|
220
200
|
}
|
221
201
|
apply(editor) {
|
202
|
+
this.resolveComponent(editor.image);
|
222
203
|
this.component.zIndex = this.targetZIndex;
|
223
204
|
this.updateTransform(editor, this.affineTransfm);
|
224
205
|
editor.queueRerender();
|
225
206
|
}
|
226
207
|
unapply(editor) {
|
208
|
+
this.resolveComponent(editor.image);
|
227
209
|
this.component.zIndex = this.origZIndex;
|
228
210
|
this.updateTransform(editor, this.affineTransfm.inverse());
|
229
211
|
editor.queueRerender();
|
@@ -233,7 +215,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
233
215
|
}
|
234
216
|
serializeToJSON() {
|
235
217
|
return {
|
236
|
-
id: this.
|
218
|
+
id: this.componentID,
|
237
219
|
transfm: this.affineTransfm.toArray(),
|
238
220
|
targetZIndex: this.targetZIndex,
|
239
221
|
};
|
@@ -241,13 +223,11 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
241
223
|
},
|
242
224
|
(() => {
|
243
225
|
SerializableCommand.register(AbstractComponent.transformElementCommandId, (json, editor) => {
|
244
|
-
|
226
|
+
var _a;
|
227
|
+
const elem = (_a = editor.image.lookupElement(json.id)) !== null && _a !== void 0 ? _a : undefined;
|
245
228
|
const transform = new Mat33(...json.transfm);
|
246
229
|
const targetZIndex = json.targetZIndex;
|
247
|
-
|
248
|
-
return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id, targetZIndex);
|
249
|
-
}
|
250
|
-
return new AbstractComponent.TransformElementCommand(transform, elem, targetZIndex);
|
230
|
+
return new AbstractComponent.TransformElementCommand(transform, json.id, elem, targetZIndex);
|
251
231
|
});
|
252
232
|
})(),
|
253
233
|
_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;
|