js-draw 0.3.1 → 0.3.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.
Files changed (84) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +4 -1
  2. package/CHANGELOG.md +8 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +4 -1
  5. package/dist/src/Editor.js +117 -2
  6. package/dist/src/EditorImage.js +4 -1
  7. package/dist/src/SVGLoader.d.ts +4 -1
  8. package/dist/src/SVGLoader.js +78 -33
  9. package/dist/src/UndoRedoHistory.d.ts +1 -0
  10. package/dist/src/UndoRedoHistory.js +6 -0
  11. package/dist/src/Viewport.d.ts +1 -0
  12. package/dist/src/Viewport.js +12 -4
  13. package/dist/src/commands/lib.d.ts +2 -1
  14. package/dist/src/commands/lib.js +2 -1
  15. package/dist/src/commands/localization.d.ts +1 -0
  16. package/dist/src/commands/localization.js +1 -0
  17. package/dist/src/commands/uniteCommands.d.ts +4 -0
  18. package/dist/src/commands/uniteCommands.js +105 -0
  19. package/dist/src/components/AbstractComponent.d.ts +2 -0
  20. package/dist/src/components/AbstractComponent.js +41 -5
  21. package/dist/src/components/ImageComponent.d.ts +27 -0
  22. package/dist/src/components/ImageComponent.js +129 -0
  23. package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
  24. package/dist/src/components/lib.d.ts +4 -2
  25. package/dist/src/components/lib.js +4 -2
  26. package/dist/src/components/localization.d.ts +2 -0
  27. package/dist/src/components/localization.js +2 -0
  28. package/dist/src/math/LineSegment2.d.ts +2 -0
  29. package/dist/src/math/LineSegment2.js +3 -0
  30. package/dist/src/rendering/localization.d.ts +3 -0
  31. package/dist/src/rendering/localization.js +3 -0
  32. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
  33. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  34. package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
  35. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
  36. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  37. package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
  38. package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
  39. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
  40. package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
  41. package/dist/src/tools/BaseTool.d.ts +3 -1
  42. package/dist/src/tools/BaseTool.js +6 -0
  43. package/dist/src/tools/PasteHandler.d.ts +16 -0
  44. package/dist/src/tools/PasteHandler.js +142 -0
  45. package/dist/src/tools/SelectionTool.d.ts +7 -1
  46. package/dist/src/tools/SelectionTool.js +63 -5
  47. package/dist/src/tools/ToolController.js +36 -27
  48. package/dist/src/tools/lib.d.ts +1 -0
  49. package/dist/src/tools/lib.js +1 -0
  50. package/dist/src/tools/localization.d.ts +3 -0
  51. package/dist/src/tools/localization.js +3 -0
  52. package/dist/src/types.d.ts +13 -2
  53. package/dist/src/types.js +2 -0
  54. package/package.json +1 -1
  55. package/src/Editor.ts +131 -2
  56. package/src/EditorImage.ts +7 -1
  57. package/src/SVGLoader.ts +90 -36
  58. package/src/UndoRedoHistory.test.ts +33 -0
  59. package/src/UndoRedoHistory.ts +8 -0
  60. package/src/Viewport.ts +13 -4
  61. package/src/commands/lib.ts +2 -0
  62. package/src/commands/localization.ts +2 -0
  63. package/src/commands/uniteCommands.test.ts +23 -0
  64. package/src/commands/uniteCommands.ts +121 -0
  65. package/src/components/AbstractComponent.ts +55 -9
  66. package/src/components/ImageComponent.ts +153 -0
  67. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  68. package/src/components/lib.ts +7 -2
  69. package/src/components/localization.ts +4 -0
  70. package/src/math/LineSegment2.test.ts +9 -0
  71. package/src/math/LineSegment2.ts +5 -0
  72. package/src/rendering/localization.ts +6 -0
  73. package/src/rendering/renderers/AbstractRenderer.ts +16 -0
  74. package/src/rendering/renderers/CanvasRenderer.ts +10 -1
  75. package/src/rendering/renderers/DummyRenderer.ts +6 -1
  76. package/src/rendering/renderers/SVGRenderer.ts +50 -21
  77. package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
  78. package/src/tools/BaseTool.ts +9 -1
  79. package/src/tools/PasteHandler.ts +156 -0
  80. package/src/tools/SelectionTool.ts +80 -8
  81. package/src/tools/ToolController.ts +51 -44
  82. package/src/tools/lib.ts +1 -0
  83. package/src/tools/localization.ts +8 -0
  84. package/src/types.ts +16 -2
@@ -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 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: number[];
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
  };
@@ -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,7 +1,10 @@
1
1
  export interface TextRendererLocalization {
2
2
  pathNodeCount(pathCount: number): string;
3
3
  textNodeCount(nodeCount: number): string;
4
+ imageNodeCount(nodeCount: number): string;
4
5
  textNode(content: string): string;
6
+ unlabeledImageNode: string;
7
+ imageNode(label: string): string;
5
8
  rerenderAsText: string;
6
9
  }
7
10
  export declare const defaultTextRendererLocalization: TextRendererLocalization;
@@ -1,6 +1,9 @@
1
1
  export const defaultTextRendererLocalization = {
2
2
  pathNodeCount: (count) => `There are ${count} visible path objects.`,
3
3
  textNodeCount: (count) => `There are ${count} visible text nodes.`,
4
+ imageNodeCount: (nodeCount) => `There are ${nodeCount} visible image nodes.`,
4
5
  textNode: (content) => `Text: ${content}`,
6
+ imageNode: (label) => `Image: ${label}`,
7
+ unlabeledImageNode: 'Unlabeled image',
5
8
  rerenderAsText: 'Re-render as text',
6
9
  };
@@ -12,6 +12,12 @@ export interface RenderablePathSpec {
12
12
  style: RenderingStyle;
13
13
  path?: Path;
14
14
  }
15
+ export interface RenderableImage {
16
+ transform: Mat33;
17
+ image: HTMLImageElement | HTMLCanvasElement;
18
+ base64Url: string;
19
+ label?: string;
20
+ }
15
21
  export default abstract class AbstractRenderer {
16
22
  private viewport;
17
23
  private selfTransform;
@@ -27,6 +33,7 @@ export default abstract class AbstractRenderer {
27
33
  protected abstract traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
28
34
  protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
29
35
  abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
36
+ abstract drawImage(image: RenderableImage): void;
30
37
  abstract isTooSmallToRender(rect: Rect2): boolean;
31
38
  setDraftMode(_draftMode: boolean): void;
32
39
  protected objectLevel: number;
@@ -5,7 +5,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
5
5
  import Vec3 from '../../math/Vec3';
6
6
  import Viewport from '../../Viewport';
7
7
  import RenderingStyle from '../RenderingStyle';
8
- import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
8
+ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
9
9
  export default class CanvasRenderer extends AbstractRenderer {
10
10
  private ctx;
11
11
  private ignoreObjectsAboveLevel;
@@ -28,6 +28,7 @@ export default class CanvasRenderer extends AbstractRenderer {
28
28
  protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
29
29
  drawPath(path: RenderablePathSpec): void;
30
30
  drawText(text: string, transform: Mat33, style: TextStyle): void;
31
+ drawImage(image: RenderableImage): void;
31
32
  private clipLevels;
32
33
  startObject(boundingBox: Rect2, clip: boolean): void;
33
34
  endObject(): void;
@@ -125,6 +125,13 @@ export default class CanvasRenderer extends AbstractRenderer {
125
125
  }
126
126
  this.ctx.restore();
127
127
  }
128
+ drawImage(image) {
129
+ this.ctx.save();
130
+ const transform = this.getCanvasToScreenTransform().rightMul(image.transform);
131
+ this.transformBy(transform);
132
+ this.ctx.drawImage(image.image, 0, 0);
133
+ this.ctx.restore();
134
+ }
128
135
  startObject(boundingBox, clip) {
129
136
  if (this.isTooSmallToRender(boundingBox)) {
130
137
  this.ignoreObjectsAboveLevel = this.getNestingLevel();
@@ -5,7 +5,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
5
5
  import Vec3 from '../../math/Vec3';
6
6
  import Viewport from '../../Viewport';
7
7
  import RenderingStyle from '../RenderingStyle';
8
- import AbstractRenderer from './AbstractRenderer';
8
+ import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
9
9
  export default class DummyRenderer extends AbstractRenderer {
10
10
  clearedCount: number;
11
11
  renderedPathCount: number;
@@ -13,6 +13,7 @@ export default class DummyRenderer extends AbstractRenderer {
13
13
  lastPoint: Point2 | null;
14
14
  objectNestingLevel: number;
15
15
  lastText: string | null;
16
+ lastImage: RenderableImage | null;
16
17
  pointBuffer: Point2[];
17
18
  constructor(viewport: Viewport);
18
19
  displaySize(): Vec2;
@@ -25,6 +26,7 @@ export default class DummyRenderer extends AbstractRenderer {
25
26
  protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
26
27
  drawPoints(..._points: Vec3[]): void;
27
28
  drawText(text: string, _transform: Mat33, _style: TextStyle): void;
29
+ drawImage(image: RenderableImage): void;
28
30
  startObject(boundingBox: Rect2, _clip: boolean): void;
29
31
  endObject(): void;
30
32
  isTooSmallToRender(_rect: Rect2): boolean;
@@ -11,6 +11,7 @@ export default class DummyRenderer extends AbstractRenderer {
11
11
  this.lastPoint = null;
12
12
  this.objectNestingLevel = 0;
13
13
  this.lastText = null;
14
+ this.lastImage = null;
14
15
  // List of points drawn since the last clear.
15
16
  this.pointBuffer = [];
16
17
  }
@@ -30,6 +31,7 @@ export default class DummyRenderer extends AbstractRenderer {
30
31
  this.renderedPathCount = 0;
31
32
  this.pointBuffer = [];
32
33
  this.lastText = null;
34
+ this.lastImage = null;
33
35
  // Ensure all objects finished rendering
34
36
  if (this.objectNestingLevel > 0) {
35
37
  throw new Error(`Within an object while clearing! Nesting level: ${this.objectNestingLevel}`);
@@ -73,6 +75,9 @@ export default class DummyRenderer extends AbstractRenderer {
73
75
  drawText(text, _transform, _style) {
74
76
  this.lastText = text;
75
77
  }
78
+ drawImage(image) {
79
+ this.lastImage = image;
80
+ }
76
81
  startObject(boundingBox, _clip) {
77
82
  super.startObject(boundingBox);
78
83
  this.objectNestingLevel += 1;
@@ -5,20 +5,23 @@ import Rect2 from '../../math/Rect2';
5
5
  import { Point2, Vec2 } from '../../math/Vec2';
6
6
  import Viewport from '../../Viewport';
7
7
  import RenderingStyle from '../RenderingStyle';
8
- import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
8
+ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
9
9
  export default class SVGRenderer extends AbstractRenderer {
10
10
  private elem;
11
+ private sanitize;
11
12
  private lastPathStyle;
12
13
  private lastPathString;
13
14
  private objectElems;
14
15
  private overwrittenAttrs;
15
- constructor(elem: SVGSVGElement, viewport: Viewport);
16
+ constructor(elem: SVGSVGElement, viewport: Viewport, sanitize?: boolean);
16
17
  setRootSVGAttribute(name: string, value: string | null): void;
17
18
  displaySize(): Vec2;
18
19
  clear(): void;
19
20
  private addPathToSVG;
20
21
  drawPath(pathSpec: RenderablePathSpec): void;
22
+ private transformFrom;
21
23
  drawText(text: string, transform: Mat33, style: TextStyle): void;
24
+ drawImage(image: RenderableImage): void;
22
25
  startObject(boundingBox: Rect2): void;
23
26
  endObject(loaderData?: LoadSaveDataTable): void;
24
27
  private unimplementedMessage;
@@ -6,9 +6,11 @@ import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader
6
6
  import AbstractRenderer from './AbstractRenderer';
7
7
  const svgNameSpace = 'http://www.w3.org/2000/svg';
8
8
  export default class SVGRenderer extends AbstractRenderer {
9
- constructor(elem, viewport) {
9
+ // Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
10
+ constructor(elem, viewport, sanitize = false) {
10
11
  super(viewport);
11
12
  this.elem = elem;
13
+ this.sanitize = sanitize;
12
14
  this.lastPathStyle = null;
13
15
  this.lastPathString = [];
14
16
  this.objectElems = null;
@@ -17,6 +19,9 @@ export default class SVGRenderer extends AbstractRenderer {
17
19
  }
18
20
  // Sets an attribute on the root SVG element.
19
21
  setRootSVGAttribute(name, value) {
22
+ if (this.sanitize) {
23
+ return;
24
+ }
20
25
  // Make the original value of the attribute restorable on clear
21
26
  if (!(name in this.overwrittenAttrs)) {
22
27
  this.overwrittenAttrs[name] = this.elem.getAttribute(name);
@@ -32,18 +37,20 @@ export default class SVGRenderer extends AbstractRenderer {
32
37
  return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
33
38
  }
34
39
  clear() {
35
- // Restore all alltributes
36
- for (const attrName in this.overwrittenAttrs) {
37
- const value = this.overwrittenAttrs[attrName];
38
- if (value) {
39
- this.elem.setAttribute(attrName, value);
40
- }
41
- else {
42
- this.elem.removeAttribute(attrName);
40
+ this.lastPathString = [];
41
+ if (!this.sanitize) {
42
+ // Restore all all attributes
43
+ for (const attrName in this.overwrittenAttrs) {
44
+ const value = this.overwrittenAttrs[attrName];
45
+ if (value) {
46
+ this.elem.setAttribute(attrName, value);
47
+ }
48
+ else {
49
+ this.elem.removeAttribute(attrName);
50
+ }
43
51
  }
52
+ this.overwrittenAttrs = {};
44
53
  }
45
- this.overwrittenAttrs = {};
46
- this.lastPathString = [];
47
54
  }
48
55
  // Push [this.fullPath] to the SVG
49
56
  addPathToSVG() {
@@ -74,25 +81,29 @@ export default class SVGRenderer extends AbstractRenderer {
74
81
  }
75
82
  this.lastPathString.push(path.toString());
76
83
  }
77
- drawText(text, transform, style) {
78
- var _a, _b, _c;
79
- transform = this.getCanvasToScreenTransform().rightMul(transform);
84
+ // Apply [elemTransform] to [elem].
85
+ transformFrom(elemTransform, elem) {
86
+ let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
80
87
  const translation = transform.transformVec2(Vec2.zero);
81
88
  transform = transform.rightMul(Mat33.translation(translation.times(-1)));
82
- const textElem = document.createElementNS(svgNameSpace, 'text');
83
- textElem.appendChild(document.createTextNode(text));
84
- textElem.style.transform = `matrix(
89
+ elem.style.transform = `matrix(
85
90
  ${transform.a1}, ${transform.b1},
86
91
  ${transform.a2}, ${transform.b2},
87
92
  ${transform.a3}, ${transform.b3}
88
93
  )`;
94
+ elem.setAttribute('x', `${toRoundedString(translation.x)}`);
95
+ elem.setAttribute('y', `${toRoundedString(translation.y)}`);
96
+ }
97
+ drawText(text, transform, style) {
98
+ var _a, _b, _c;
99
+ const textElem = document.createElementNS(svgNameSpace, 'text');
100
+ textElem.appendChild(document.createTextNode(text));
101
+ this.transformFrom(transform, textElem);
89
102
  textElem.style.fontFamily = style.fontFamily;
90
103
  textElem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
91
104
  textElem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
92
105
  textElem.style.fontSize = style.size + 'px';
93
106
  textElem.style.fill = style.renderingStyle.fill.toHexString();
94
- textElem.setAttribute('x', `${toRoundedString(translation.x)}`);
95
- textElem.setAttribute('y', `${toRoundedString(translation.y)}`);
96
107
  if (style.renderingStyle.stroke) {
97
108
  const strokeStyle = style.renderingStyle.stroke;
98
109
  textElem.style.stroke = strokeStyle.color.toHexString();
@@ -101,6 +112,17 @@ export default class SVGRenderer extends AbstractRenderer {
101
112
  this.elem.appendChild(textElem);
102
113
  (_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
103
114
  }
115
+ drawImage(image) {
116
+ var _a, _b, _c, _d, _e;
117
+ const svgImgElem = document.createElementNS(svgNameSpace, 'image');
118
+ svgImgElem.setAttribute('href', image.base64Url);
119
+ svgImgElem.setAttribute('width', (_a = image.image.getAttribute('width')) !== null && _a !== void 0 ? _a : '');
120
+ svgImgElem.setAttribute('height', (_b = image.image.getAttribute('height')) !== null && _b !== void 0 ? _b : '');
121
+ svgImgElem.setAttribute('aria-label', (_d = (_c = image.image.getAttribute('aria-label')) !== null && _c !== void 0 ? _c : image.image.getAttribute('alt')) !== null && _d !== void 0 ? _d : '');
122
+ this.transformFrom(image.transform, svgImgElem);
123
+ this.elem.appendChild(svgImgElem);
124
+ (_e = this.objectElems) === null || _e === void 0 ? void 0 : _e.push(svgImgElem);
125
+ }
104
126
  startObject(boundingBox) {
105
127
  super.startObject(boundingBox);
106
128
  // Only accumulate a path within an object
@@ -113,7 +135,7 @@ export default class SVGRenderer extends AbstractRenderer {
113
135
  super.endObject(loaderData);
114
136
  // Don't extend paths across objects
115
137
  this.addPathToSVG();
116
- if (loaderData) {
138
+ if (loaderData && !this.sanitize) {
117
139
  // Restore any attributes unsupported by the app.
118
140
  for (const elem of (_a = this.objectElems) !== null && _a !== void 0 ? _a : []) {
119
141
  const attrs = loaderData[svgAttributesDataKey];
@@ -150,6 +172,9 @@ export default class SVGRenderer extends AbstractRenderer {
150
172
  }
151
173
  // Renders a **copy** of the given element.
152
174
  drawSVGElem(elem) {
175
+ if (this.sanitize) {
176
+ return;
177
+ }
153
178
  this.elem.appendChild(elem.cloneNode(true));
154
179
  }
155
180
  isTooSmallToRender(_rect) {
@@ -5,12 +5,13 @@ import Vec3 from '../../math/Vec3';
5
5
  import Viewport from '../../Viewport';
6
6
  import { TextRendererLocalization } from '../localization';
7
7
  import RenderingStyle from '../RenderingStyle';
8
- import AbstractRenderer from './AbstractRenderer';
8
+ import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
9
9
  export default class TextOnlyRenderer extends AbstractRenderer {
10
10
  private localizationTable;
11
11
  private descriptionBuilder;
12
12
  private pathCount;
13
13
  private textNodeCount;
14
+ private imageNodeCount;
14
15
  constructor(viewport: Viewport, localizationTable: TextRendererLocalization);
15
16
  displaySize(): Vec3;
16
17
  clear(): void;
@@ -22,6 +23,7 @@ export default class TextOnlyRenderer extends AbstractRenderer {
22
23
  protected traceCubicBezierCurve(_p1: Vec3, _p2: Vec3, _p3: Vec3): void;
23
24
  protected traceQuadraticBezierCurve(_controlPoint: Vec3, _endPoint: Vec3): void;
24
25
  drawText(text: string, _transform: Mat33, _style: TextStyle): void;
26
+ drawImage(image: RenderableImage): void;
25
27
  isTooSmallToRender(rect: Rect2): boolean;
26
28
  drawPoints(..._points: Vec3[]): void;
27
29
  }
@@ -8,6 +8,7 @@ export default class TextOnlyRenderer extends AbstractRenderer {
8
8
  this.descriptionBuilder = [];
9
9
  this.pathCount = 0;
10
10
  this.textNodeCount = 0;
11
+ this.imageNodeCount = 0;
11
12
  }
12
13
  displaySize() {
13
14
  // We don't have a graphical display, export a reasonable size.
@@ -21,7 +22,8 @@ export default class TextOnlyRenderer extends AbstractRenderer {
21
22
  getDescription() {
22
23
  return [
23
24
  this.localizationTable.pathNodeCount(this.pathCount),
24
- this.localizationTable.textNodeCount(this.textNodeCount),
25
+ ...(this.textNodeCount > 0 ? this.localizationTable.textNodeCount(this.textNodeCount) : []),
26
+ ...(this.imageNodeCount > 0 ? this.localizationTable.imageNodeCount(this.imageNodeCount) : []),
25
27
  ...this.descriptionBuilder
26
28
  ].join('\n');
27
29
  }
@@ -42,6 +44,11 @@ export default class TextOnlyRenderer extends AbstractRenderer {
42
44
  this.descriptionBuilder.push(this.localizationTable.textNode(text));
43
45
  this.textNodeCount++;
44
46
  }
47
+ drawImage(image) {
48
+ const label = image.label ? this.localizationTable.imageNode(image.label) : this.localizationTable.unlabeledImageNode;
49
+ this.descriptionBuilder.push(label);
50
+ this.imageNodeCount++;
51
+ }
45
52
  isTooSmallToRender(rect) {
46
53
  return rect.maxDimension < 15 / this.getSizeOfCanvasPixelOnScreen();
47
54
  }
@@ -1,4 +1,4 @@
1
- import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, KeyPressEvent, KeyUpEvent } from '../types';
1
+ import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, KeyPressEvent, KeyUpEvent, PasteEvent, CopyEvent } from '../types';
2
2
  import ToolEnabledGroup from './ToolEnabledGroup';
3
3
  export default abstract class BaseTool implements PointerEvtListener {
4
4
  private notifier;
@@ -11,6 +11,8 @@ export default abstract class BaseTool implements PointerEvtListener {
11
11
  onGestureCancel(): void;
12
12
  protected constructor(notifier: EditorNotifier, description: string);
13
13
  onWheel(_event: WheelEvt): boolean;
14
+ onCopy(_event: CopyEvent): boolean;
15
+ onPaste(_event: PasteEvent): boolean;
14
16
  onKeyPress(_event: KeyPressEvent): boolean;
15
17
  onKeyUp(_event: KeyUpEvent): boolean;
16
18
  setEnabled(enabled: boolean): void;