js-draw 0.3.1 → 0.4.0

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