js-draw 0.17.0 → 0.17.2
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/CHANGELOG.md +14 -1
- package/dist/bundle.js +1 -1
- package/dist/src/EditorImage.d.ts +6 -6
- package/dist/src/EditorImage.js +83 -42
- package/dist/src/UndoRedoHistory.js +1 -1
- package/dist/src/components/TextComponent.d.ts +13 -1
- package/dist/src/components/TextComponent.js +13 -5
- package/dist/src/rendering/RenderingStyle.d.ts +7 -0
- package/dist/src/rendering/RenderingStyle.js +6 -0
- package/dist/src/rendering/TextRenderingStyle.d.ts +13 -0
- package/dist/src/rendering/TextRenderingStyle.js +4 -1
- package/dist/src/toolbar/HTMLToolbar.js +14 -3
- package/dist/src/tools/FindTool.js +2 -1
- package/dist/src/util/assertions.d.ts +22 -0
- package/dist/src/util/assertions.js +42 -3
- package/package.json +16 -16
- package/src/EditorImage.test.ts +33 -0
- package/src/EditorImage.ts +87 -35
- package/src/UndoRedoHistory.test.ts +1 -1
- package/src/UndoRedoHistory.ts +1 -1
- package/src/components/TextComponent.test.ts +26 -1
- package/src/components/TextComponent.ts +18 -8
- package/src/rendering/RenderingStyle.ts +9 -0
- package/src/rendering/TextRenderingStyle.ts +8 -1
- package/src/toolbar/HTMLToolbar.ts +17 -4
- package/src/tools/FindTool.ts +2 -1
- package/src/util/assertions.ts +51 -3
package/src/EditorImage.ts
CHANGED
@@ -8,8 +8,8 @@ import RenderingCache from './rendering/caching/RenderingCache';
|
|
8
8
|
import SerializableCommand from './commands/SerializableCommand';
|
9
9
|
import EventDispatcher from './EventDispatcher';
|
10
10
|
import { Vec2 } from './math/Vec2';
|
11
|
-
import
|
12
|
-
import
|
11
|
+
import Mat33, { Mat33Array } from './math/Mat33';
|
12
|
+
import { assertIsNumber, assertIsNumberArray } from './util/assertions';
|
13
13
|
|
14
14
|
// @internal Sort by z-index, low to high
|
15
15
|
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
@@ -51,39 +51,6 @@ export default class EditorImage {
|
|
51
51
|
this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
|
52
52
|
}
|
53
53
|
|
54
|
-
/**
|
55
|
-
* @returns a `Viewport` for rendering the image when importing/exporting.
|
56
|
-
*/
|
57
|
-
public getImportExportViewport() {
|
58
|
-
return this.importExportViewport;
|
59
|
-
}
|
60
|
-
|
61
|
-
public setImportExportRect(imageRect: Rect2): Command {
|
62
|
-
const importExportViewport = this.getImportExportViewport();
|
63
|
-
const origSize = importExportViewport.visibleRect.size;
|
64
|
-
const origTransform = importExportViewport.canvasToScreenTransform;
|
65
|
-
|
66
|
-
return new class extends Command {
|
67
|
-
public apply(editor: Editor) {
|
68
|
-
const viewport = editor.image.getImportExportViewport();
|
69
|
-
viewport.updateScreenSize(imageRect.size);
|
70
|
-
viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
|
71
|
-
editor.queueRerender();
|
72
|
-
}
|
73
|
-
|
74
|
-
public unapply(editor: Editor) {
|
75
|
-
const viewport = editor.image.getImportExportViewport();
|
76
|
-
viewport.updateScreenSize(origSize);
|
77
|
-
viewport.resetTransform(origTransform);
|
78
|
-
editor.queueRerender();
|
79
|
-
}
|
80
|
-
|
81
|
-
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
82
|
-
return localizationTable.resizeOutputCommand(imageRect);
|
83
|
-
}
|
84
|
-
};
|
85
|
-
}
|
86
|
-
|
87
54
|
// Returns all components that make up the background of this image. These
|
88
55
|
// components are rendered below all other components.
|
89
56
|
public getBackgroundComponents(): AbstractComponent[] {
|
@@ -271,6 +238,91 @@ export default class EditorImage {
|
|
271
238
|
});
|
272
239
|
}
|
273
240
|
};
|
241
|
+
|
242
|
+
|
243
|
+
|
244
|
+
/**
|
245
|
+
* @returns a `Viewport` for rendering the image when importing/exporting.
|
246
|
+
*/
|
247
|
+
public getImportExportViewport() {
|
248
|
+
return this.importExportViewport;
|
249
|
+
}
|
250
|
+
|
251
|
+
public setImportExportRect(imageRect: Rect2): SerializableCommand {
|
252
|
+
const importExportViewport = this.getImportExportViewport();
|
253
|
+
const origSize = importExportViewport.visibleRect.size;
|
254
|
+
const origTransform = importExportViewport.canvasToScreenTransform;
|
255
|
+
|
256
|
+
return new EditorImage.SetImportExportRectCommand(origSize, origTransform, imageRect);
|
257
|
+
}
|
258
|
+
|
259
|
+
// Handles resizing the background import/export region of the image.
|
260
|
+
private static SetImportExportRectCommand = class extends SerializableCommand {
|
261
|
+
private static commandId = 'set-import-export-rect';
|
262
|
+
|
263
|
+
public constructor(
|
264
|
+
private originalSize: Vec2,
|
265
|
+
private originalTransform: Mat33,
|
266
|
+
private finalRect: Rect2,
|
267
|
+
) {
|
268
|
+
super(EditorImage.SetImportExportRectCommand.commandId);
|
269
|
+
}
|
270
|
+
|
271
|
+
public apply(editor: Editor) {
|
272
|
+
const viewport = editor.image.getImportExportViewport();
|
273
|
+
viewport.updateScreenSize(this.finalRect.size);
|
274
|
+
viewport.resetTransform(Mat33.translation(this.finalRect.topLeft.times(-1)));
|
275
|
+
editor.queueRerender();
|
276
|
+
}
|
277
|
+
|
278
|
+
public unapply(editor: Editor) {
|
279
|
+
const viewport = editor.image.getImportExportViewport();
|
280
|
+
viewport.updateScreenSize(this.originalSize);
|
281
|
+
viewport.resetTransform(this.originalTransform);
|
282
|
+
editor.queueRerender();
|
283
|
+
}
|
284
|
+
|
285
|
+
public description(_editor: Editor, localization: EditorLocalization) {
|
286
|
+
return localization.resizeOutputCommand(this.finalRect);
|
287
|
+
}
|
288
|
+
|
289
|
+
protected serializeToJSON() {
|
290
|
+
return {
|
291
|
+
originalSize: this.originalSize.xy,
|
292
|
+
originalTransform: this.originalTransform.toArray(),
|
293
|
+
newRegion: {
|
294
|
+
x: this.finalRect.x,
|
295
|
+
y: this.finalRect.y,
|
296
|
+
w: this.finalRect.w,
|
297
|
+
h: this.finalRect.h,
|
298
|
+
},
|
299
|
+
};
|
300
|
+
}
|
301
|
+
|
302
|
+
static {
|
303
|
+
const commandId = this.commandId;
|
304
|
+
SerializableCommand.register(commandId, (json: any, _editor: Editor) => {
|
305
|
+
assertIsNumber(json.originalSize.x);
|
306
|
+
assertIsNumber(json.originalSize.y);
|
307
|
+
assertIsNumberArray(json.originalTransform);
|
308
|
+
assertIsNumberArray([
|
309
|
+
json.newRegion.x,
|
310
|
+
json.newRegion.y,
|
311
|
+
json.newRegion.w,
|
312
|
+
json.newRegion.h,
|
313
|
+
]);
|
314
|
+
|
315
|
+
const originalSize = Vec2.ofXY(json.originalSize);
|
316
|
+
const originalTransform = new Mat33(...(json.originalTransform as Mat33Array));
|
317
|
+
const finalRect = new Rect2(
|
318
|
+
json.newRegion.x, json.newRegion.y, json.newRegion.w, json.newRegion.h
|
319
|
+
);
|
320
|
+
return new EditorImage.SetImportExportRectCommand(
|
321
|
+
originalSize, originalTransform, finalRect
|
322
|
+
);
|
323
|
+
});
|
324
|
+
}
|
325
|
+
};
|
274
326
|
}
|
275
327
|
|
276
328
|
type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
|
package/src/UndoRedoHistory.ts
CHANGED
@@ -47,7 +47,7 @@ class UndoRedoHistory {
|
|
47
47
|
this.#redoStack = [];
|
48
48
|
|
49
49
|
if (this.#undoStack.length > this.maxUndoRedoStackSize) {
|
50
|
-
const removeAtOnceCount =
|
50
|
+
const removeAtOnceCount = Math.ceil(this.maxUndoRedoStackSize / 100);
|
51
51
|
const removedElements = this.#undoStack.splice(0, removeAtOnceCount);
|
52
52
|
removedElements.forEach(elem => elem.onDrop(this.editor));
|
53
53
|
}
|
@@ -71,4 +71,29 @@ describe('TextComponent', () => {
|
|
71
71
|
editor.history.undo();
|
72
72
|
expect(text.getStyle().color).objEq(Color4.green);
|
73
73
|
});
|
74
|
-
|
74
|
+
|
75
|
+
it('calling forceStyle on the duplicate of a TextComponent should preserve the original\'s style', () => {
|
76
|
+
const originalStyle: TextStyle = {
|
77
|
+
size: 11,
|
78
|
+
fontFamily: 'sans-serif',
|
79
|
+
renderingStyle: { fill: Color4.purple, },
|
80
|
+
};
|
81
|
+
|
82
|
+
const text1 = new TextComponent([ 'Test' ], Mat33.identity, originalStyle);
|
83
|
+
const text2 = text1.clone() as TextComponent;
|
84
|
+
|
85
|
+
text1.forceStyle({
|
86
|
+
color: Color4.red,
|
87
|
+
}, null);
|
88
|
+
|
89
|
+
expect(text2.getStyle().color).objEq(Color4.purple);
|
90
|
+
expect(text1.getStyle().color).objEq(Color4.red);
|
91
|
+
|
92
|
+
text2.forceStyle({
|
93
|
+
textStyle: originalStyle,
|
94
|
+
}, null);
|
95
|
+
|
96
|
+
expect(text1.getStyle().color).objEq(Color4.red);
|
97
|
+
expect(text2.getTextStyle()).toMatchObject(originalStyle);
|
98
|
+
});
|
99
|
+
});
|
@@ -5,7 +5,7 @@ import Rect2 from '../math/Rect2';
|
|
5
5
|
import Editor from '../Editor';
|
6
6
|
import { Vec2 } from '../math/Vec2';
|
7
7
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
8
|
-
import { TextStyle, textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
|
8
|
+
import { cloneTextStyle, TextStyle, textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
|
9
9
|
import AbstractComponent from './AbstractComponent';
|
10
10
|
import { ImageComponentLocalization } from './localization';
|
11
11
|
import RestyleableComponent, { ComponentStyle, createRestyleComponentCommand } from './RestylableComponent';
|
@@ -170,11 +170,14 @@ export default class TextComponent extends AbstractComponent implements Restylea
|
|
170
170
|
|
171
171
|
public forceStyle(style: ComponentStyle, editor: Editor|null): void {
|
172
172
|
if (style.textStyle) {
|
173
|
-
this.style = style.textStyle;
|
173
|
+
this.style = cloneTextStyle(style.textStyle);
|
174
174
|
} else if (style.color) {
|
175
|
-
this.style
|
176
|
-
...this.style
|
177
|
-
|
175
|
+
this.style = {
|
176
|
+
...this.style,
|
177
|
+
renderingStyle: {
|
178
|
+
...this.style.renderingStyle,
|
179
|
+
fill: style.color,
|
180
|
+
},
|
178
181
|
};
|
179
182
|
} else {
|
180
183
|
return;
|
@@ -194,7 +197,7 @@ export default class TextComponent extends AbstractComponent implements Restylea
|
|
194
197
|
|
195
198
|
// See this.getStyle
|
196
199
|
public getTextStyle() {
|
197
|
-
return this.style;
|
200
|
+
return cloneTextStyle(this.style);
|
198
201
|
}
|
199
202
|
|
200
203
|
public getBaselinePos() {
|
@@ -211,7 +214,14 @@ export default class TextComponent extends AbstractComponent implements Restylea
|
|
211
214
|
}
|
212
215
|
|
213
216
|
protected createClone(): AbstractComponent {
|
214
|
-
|
217
|
+
const clonedTextObjects = this.textObjects.map(obj => {
|
218
|
+
if (typeof obj === 'string') {
|
219
|
+
return obj;
|
220
|
+
} else {
|
221
|
+
return obj.createClone() as TextComponent;
|
222
|
+
}
|
223
|
+
});
|
224
|
+
return new TextComponent(clonedTextObjects, this.transform, this.style);
|
215
225
|
}
|
216
226
|
|
217
227
|
public getText() {
|
@@ -302,4 +312,4 @@ export default class TextComponent extends AbstractComponent implements Restylea
|
|
302
312
|
}
|
303
313
|
}
|
304
314
|
|
305
|
-
AbstractComponent.registerComponent(componentTypeId, (data: string) => TextComponent.deserializeFromString(data));
|
315
|
+
AbstractComponent.registerComponent(componentTypeId, (data: string) => TextComponent.deserializeFromString(data));
|
@@ -10,6 +10,15 @@ interface RenderingStyle {
|
|
10
10
|
|
11
11
|
export default RenderingStyle;
|
12
12
|
|
13
|
+
export const cloneStyle = (style: RenderingStyle) => {
|
14
|
+
return {
|
15
|
+
fill: style.fill,
|
16
|
+
stroke: style.stroke ? {
|
17
|
+
...style.stroke
|
18
|
+
} : undefined,
|
19
|
+
};
|
20
|
+
};
|
21
|
+
|
13
22
|
export const stylesEqual = (a: RenderingStyle, b: RenderingStyle): boolean => {
|
14
23
|
const result = a === b || (a.fill.eq(b.fill)
|
15
24
|
&& (a.stroke == undefined) === (b.stroke == undefined)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import RenderingStyle, { styleFromJSON, styleToJSON } from './RenderingStyle';
|
1
|
+
import RenderingStyle, { cloneStyle, styleFromJSON, styleToJSON } from './RenderingStyle';
|
2
2
|
|
3
3
|
export interface TextStyle {
|
4
4
|
size: number;
|
@@ -10,6 +10,13 @@ export interface TextStyle {
|
|
10
10
|
|
11
11
|
export default TextStyle;
|
12
12
|
|
13
|
+
export const cloneTextStyle = (style: TextStyle) => {
|
14
|
+
return {
|
15
|
+
...style,
|
16
|
+
renderingStyle: cloneStyle(style.renderingStyle),
|
17
|
+
};
|
18
|
+
};
|
19
|
+
|
13
20
|
export const textStyleFromJSON = (json: any) => {
|
14
21
|
if (typeof json === 'string') {
|
15
22
|
json = JSON.parse(json);
|
@@ -181,6 +181,18 @@ export default class HTMLToolbar {
|
|
181
181
|
return totalWidth;
|
182
182
|
};
|
183
183
|
|
184
|
+
// Returns true if there is enough empty space to move the first child
|
185
|
+
// from the overflow menu to the main menu.
|
186
|
+
const canRemoveFirstChildFromOverflow = (freeSpaceInMainMenu: number) => {
|
187
|
+
const overflowChildren = this.overflowWidget?.getChildWidgets() ?? [];
|
188
|
+
|
189
|
+
if (overflowChildren.length === 0) {
|
190
|
+
return false;
|
191
|
+
}
|
192
|
+
|
193
|
+
return overflowChildren[0].getButtonWidth() <= freeSpaceInMainMenu;
|
194
|
+
};
|
195
|
+
|
184
196
|
let overflowWidgetsWidth = getTotalWidth(this.overflowWidget.getChildWidgets());
|
185
197
|
let shownWidgetWidth = getTotalWidth(this.widgetList) - overflowWidgetsWidth;
|
186
198
|
let availableWidth = this.container.clientWidth * 0.87;
|
@@ -194,7 +206,8 @@ export default class HTMLToolbar {
|
|
194
206
|
|
195
207
|
let updatedChildren = false;
|
196
208
|
|
197
|
-
|
209
|
+
// If we can remove at least one child from the overflow menu,
|
210
|
+
if (canRemoveFirstChildFromOverflow(availableWidth - shownWidgetWidth)) {
|
198
211
|
// Move widgets to the main menu.
|
199
212
|
const overflowChildren = this.overflowWidget.clearChildren();
|
200
213
|
|
@@ -207,15 +220,12 @@ export default class HTMLToolbar {
|
|
207
220
|
}
|
208
221
|
}
|
209
222
|
|
210
|
-
this.overflowWidget.setHidden(true);
|
211
223
|
overflowWidgetsWidth = 0;
|
212
|
-
|
213
224
|
updatedChildren = true;
|
214
225
|
}
|
215
226
|
|
216
227
|
if (shownWidgetWidth >= availableWidth) {
|
217
228
|
// Move widgets to the overflow menu.
|
218
|
-
this.overflowWidget.setHidden(false);
|
219
229
|
|
220
230
|
// Start with the rightmost widget, move to the leftmost
|
221
231
|
for (
|
@@ -238,6 +248,9 @@ export default class HTMLToolbar {
|
|
238
248
|
updatedChildren = true;
|
239
249
|
}
|
240
250
|
|
251
|
+
// Hide/show the overflow widget.
|
252
|
+
this.overflowWidget.setHidden(this.overflowWidget.getChildWidgets().length === 0);
|
253
|
+
|
241
254
|
if (updatedChildren) {
|
242
255
|
this.setupColorPickers();
|
243
256
|
}
|
package/src/tools/FindTool.ts
CHANGED
@@ -49,7 +49,8 @@ export default class FindTool extends BaseTool {
|
|
49
49
|
}
|
50
50
|
|
51
51
|
if (matchIdx < matches.length) {
|
52
|
-
|
52
|
+
const undoable = false;
|
53
|
+
this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true), undoable);
|
53
54
|
this.editor.announceForAccessibility(
|
54
55
|
this.editor.localization.focusedFoundText(matchIdx + 1, matches.length)
|
55
56
|
);
|
package/src/util/assertions.ts
CHANGED
@@ -1,7 +1,55 @@
|
|
1
1
|
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
/**
|
3
|
+
* Compile-time assertion that a branch of code is unreachable.
|
4
|
+
* @internal
|
5
|
+
*/
|
5
6
|
export const assertUnreachable = (key: never): never => {
|
7
|
+
// See https://stackoverflow.com/a/39419171/17055750
|
6
8
|
throw new Error(`Should be unreachable. Key: ${key}.`);
|
7
9
|
};
|
10
|
+
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Throws an exception if the typeof given value is not a number or `value` is NaN.
|
14
|
+
*
|
15
|
+
* @example
|
16
|
+
* ```ts
|
17
|
+
* const foo: unknown = 3;
|
18
|
+
* assertIsNumber(foo);
|
19
|
+
*
|
20
|
+
* assertIsNumber('hello, world'); // throws an Error.
|
21
|
+
* ```
|
22
|
+
*
|
23
|
+
*
|
24
|
+
*/
|
25
|
+
export const assertIsNumber = (value: any, allowNaN: boolean = false): value is number => {
|
26
|
+
if (typeof value !== 'number' || (!allowNaN && isNaN(value))) {
|
27
|
+
throw new Error('Given value is not a number');
|
28
|
+
// return false;
|
29
|
+
}
|
30
|
+
|
31
|
+
return true;
|
32
|
+
};
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Throws if any of `values` is not of type number.
|
36
|
+
*/
|
37
|
+
export const assertIsNumberArray = (
|
38
|
+
values: any[], allowNaN: boolean = false
|
39
|
+
): values is number[] => {
|
40
|
+
if (typeof values !== 'object') {
|
41
|
+
throw new Error('Asserting isNumberArray: Given entity is not an array');
|
42
|
+
}
|
43
|
+
|
44
|
+
if (!assertIsNumber(values['length'])) {
|
45
|
+
return false;
|
46
|
+
}
|
47
|
+
|
48
|
+
for (const value of values) {
|
49
|
+
if (!assertIsNumber(value, allowNaN)) {
|
50
|
+
return false;
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
return true;
|
55
|
+
};
|