js-draw 0.17.1 → 0.17.3

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.
@@ -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 Command from './commands/Command';
12
- import Mat33 from './math/Mat33';
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;
@@ -72,7 +72,7 @@ describe('TextComponent', () => {
72
72
  expect(text.getStyle().color).objEq(Color4.green);
73
73
  });
74
74
 
75
- it('restyling the duplicate of a TextComponent should preserve the original\'s style', () => {
75
+ it('calling forceStyle on the duplicate of a TextComponent should preserve the original\'s style', () => {
76
76
  const originalStyle: TextStyle = {
77
77
  size: 11,
78
78
  fontFamily: 'sans-serif',
@@ -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,7 +170,7 @@ 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
175
  this.style = {
176
176
  ...this.style,
@@ -197,12 +197,7 @@ export default class TextComponent extends AbstractComponent implements Restylea
197
197
 
198
198
  // See this.getStyle
199
199
  public getTextStyle() {
200
- return {
201
- ...this.style,
202
- renderingStyle: {
203
- ...this.style.renderingStyle,
204
- },
205
- };
200
+ return cloneTextStyle(this.style);
206
201
  }
207
202
 
208
203
  public getBaselinePos() {
@@ -219,7 +214,14 @@ export default class TextComponent extends AbstractComponent implements Restylea
219
214
  }
220
215
 
221
216
  protected createClone(): AbstractComponent {
222
- return new TextComponent(this.textObjects, this.transform, this.style);
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);
223
225
  }
224
226
 
225
227
  public getText() {
@@ -8,7 +8,7 @@ export { default as AbstractComponent } from './AbstractComponent';
8
8
  import Stroke from './Stroke';
9
9
  import TextComponent from './TextComponent';
10
10
  import ImageComponent from './ImageComponent';
11
- import RestyleableComponent, { createRestyleComponentCommand } from './RestylableComponent';
11
+ import RestyleableComponent, { createRestyleComponentCommand, isRestylableComponent } from './RestylableComponent';
12
12
  import ImageBackground from './ImageBackground';
13
13
 
14
14
  export {
@@ -16,6 +16,7 @@ export {
16
16
  TextComponent as Text,
17
17
  RestyleableComponent,
18
18
  createRestyleComponentCommand,
19
+ isRestylableComponent,
19
20
 
20
21
  TextComponent,
21
22
  Stroke as StrokeComponent,
@@ -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);
@@ -1,5 +1,4 @@
1
1
  import Color4 from '../../Color4';
2
- import ImageBackground from '../../components/ImageBackground';
3
2
  import Editor from '../../Editor';
4
3
  import { EditorImageEventType } from '../../EditorImage';
5
4
  import Rect2 from '../../math/Rect2';
@@ -56,31 +55,12 @@ export default class DocumentPropertiesWidget extends BaseWidget {
56
55
  }
57
56
  }
58
57
 
59
- private getBackgroundElem() {
60
- const backgroundComponents = [];
61
-
62
- for (const component of this.editor.image.getBackgroundComponents()) {
63
- if (component instanceof ImageBackground) {
64
- backgroundComponents.push(component);
65
- }
66
- }
67
-
68
- if (backgroundComponents.length === 0) {
69
- return null;
70
- }
71
-
72
- // Return the last background component in the list — the component with highest z-index.
73
- return backgroundComponents[backgroundComponents.length - 1];
74
- }
75
-
76
58
  private setBackgroundColor(color: Color4) {
77
59
  this.editor.dispatch(this.editor.setBackgroundColor(color));
78
60
  }
79
61
 
80
62
  private getBackgroundColor() {
81
- const background = this.getBackgroundElem();
82
-
83
- return background?.getStyle()?.color ?? Color4.transparent;
63
+ return this.editor.estimateBackgroundColor();
84
64
  }
85
65
 
86
66
  private updateImportExportRectSize(size: { width?: number, height?: number }) {
@@ -49,7 +49,8 @@ export default class FindTool extends BaseTool {
49
49
  }
50
50
 
51
51
  if (matchIdx < matches.length) {
52
- this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true));
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
  );
@@ -1,7 +1,55 @@
1
1
 
2
- // Compile-time assertion that a branch of code is unreachable.
3
- // See https://stackoverflow.com/a/39419171/17055750
4
- // @internal
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
+ };