js-draw 0.3.0 → 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 (113) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +4 -1
  2. package/CHANGELOG.md +15 -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/Stroke.js +11 -6
  24. package/dist/src/components/builders/FreehandLineBuilder.js +7 -7
  25. package/dist/src/components/lib.d.ts +4 -2
  26. package/dist/src/components/lib.js +4 -2
  27. package/dist/src/components/localization.d.ts +2 -0
  28. package/dist/src/components/localization.js +2 -0
  29. package/dist/src/math/LineSegment2.d.ts +4 -0
  30. package/dist/src/math/LineSegment2.js +9 -0
  31. package/dist/src/math/Path.d.ts +5 -1
  32. package/dist/src/math/Path.js +89 -7
  33. package/dist/src/math/Rect2.js +1 -1
  34. package/dist/src/math/Triangle.d.ts +11 -0
  35. package/dist/src/math/Triangle.js +19 -0
  36. package/dist/src/rendering/Display.js +2 -2
  37. package/dist/src/rendering/localization.d.ts +3 -0
  38. package/dist/src/rendering/localization.js +3 -0
  39. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +9 -1
  40. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  41. package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
  42. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
  43. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  44. package/dist/src/rendering/renderers/SVGRenderer.d.ts +14 -12
  45. package/dist/src/rendering/renderers/SVGRenderer.js +71 -87
  46. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
  47. package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
  48. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
  49. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  50. package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
  51. package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
  52. package/dist/src/tools/BaseTool.d.ts +4 -1
  53. package/dist/src/tools/BaseTool.js +12 -0
  54. package/dist/src/tools/PasteHandler.d.ts +16 -0
  55. package/dist/src/tools/PasteHandler.js +142 -0
  56. package/dist/src/tools/Pen.d.ts +2 -1
  57. package/dist/src/tools/Pen.js +16 -0
  58. package/dist/src/tools/SelectionTool.d.ts +7 -1
  59. package/dist/src/tools/SelectionTool.js +63 -5
  60. package/dist/src/tools/ToolController.d.ts +1 -0
  61. package/dist/src/tools/ToolController.js +45 -29
  62. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  63. package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
  64. package/dist/src/tools/lib.d.ts +2 -0
  65. package/dist/src/tools/lib.js +2 -0
  66. package/dist/src/tools/localization.d.ts +4 -0
  67. package/dist/src/tools/localization.js +4 -0
  68. package/dist/src/types.d.ts +21 -4
  69. package/dist/src/types.js +3 -0
  70. package/package.json +2 -2
  71. package/src/Editor.ts +131 -2
  72. package/src/EditorImage.ts +7 -1
  73. package/src/SVGLoader.ts +90 -36
  74. package/src/UndoRedoHistory.test.ts +33 -0
  75. package/src/UndoRedoHistory.ts +8 -0
  76. package/src/Viewport.ts +13 -4
  77. package/src/commands/lib.ts +2 -0
  78. package/src/commands/localization.ts +2 -0
  79. package/src/commands/uniteCommands.test.ts +23 -0
  80. package/src/commands/uniteCommands.ts +121 -0
  81. package/src/components/AbstractComponent.ts +55 -9
  82. package/src/components/ImageComponent.ts +153 -0
  83. package/src/components/Stroke.test.ts +5 -0
  84. package/src/components/Stroke.ts +13 -7
  85. package/src/components/builders/FreehandLineBuilder.ts +7 -7
  86. package/src/components/lib.ts +7 -2
  87. package/src/components/localization.ts +4 -0
  88. package/src/math/LineSegment2.test.ts +9 -0
  89. package/src/math/LineSegment2.ts +13 -0
  90. package/src/math/Path.test.ts +53 -0
  91. package/src/math/Path.toString.test.ts +4 -2
  92. package/src/math/Path.ts +109 -11
  93. package/src/math/Rect2.ts +1 -1
  94. package/src/math/Triangle.ts +29 -0
  95. package/src/rendering/Display.ts +2 -2
  96. package/src/rendering/localization.ts +6 -0
  97. package/src/rendering/renderers/AbstractRenderer.ts +17 -0
  98. package/src/rendering/renderers/CanvasRenderer.ts +10 -1
  99. package/src/rendering/renderers/DummyRenderer.ts +6 -1
  100. package/src/rendering/renderers/SVGRenderer.ts +76 -101
  101. package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
  102. package/src/toolbar/HTMLToolbar.ts +1 -1
  103. package/src/toolbar/types.ts +1 -1
  104. package/src/toolbar/widgets/BaseWidget.ts +27 -1
  105. package/src/tools/BaseTool.ts +17 -1
  106. package/src/tools/PasteHandler.ts +156 -0
  107. package/src/tools/Pen.ts +20 -1
  108. package/src/tools/SelectionTool.ts +80 -8
  109. package/src/tools/ToolController.ts +60 -46
  110. package/src/tools/ToolSwitcherShortcut.ts +34 -0
  111. package/src/tools/lib.ts +2 -0
  112. package/src/tools/localization.ts +10 -0
  113. package/src/types.ts +29 -3
package/src/math/Path.ts CHANGED
@@ -47,12 +47,9 @@ interface IntersectionResult {
47
47
 
48
48
  type GeometryArrayType = Array<LineSegment2|Bezier>;
49
49
  export default class Path {
50
- private cachedGeometry: GeometryArrayType|null;
51
50
  public readonly bbox: Rect2;
52
51
 
53
52
  public constructor(public readonly startPoint: Point2, public readonly parts: PathCommand[]) {
54
- this.cachedGeometry = null;
55
-
56
53
  // Initial bounding box contains one point: the start point.
57
54
  this.bbox = Rect2.bboxOf([startPoint]);
58
55
 
@@ -63,6 +60,8 @@ export default class Path {
63
60
  }
64
61
  }
65
62
 
63
+ private cachedGeometry: GeometryArrayType|null = null;
64
+
66
65
  // Lazy-loads and returns this path's geometry
67
66
  public get geometry(): Array<LineSegment2|Bezier> {
68
67
  if (this.cachedGeometry) {
@@ -106,6 +105,41 @@ export default class Path {
106
105
  return this.cachedGeometry;
107
106
  }
108
107
 
108
+ private cachedPolylineApproximation: LineSegment2[]|null = null;
109
+
110
+ // Approximates this path with a group of line segments.
111
+ public polylineApproximation(): LineSegment2[] {
112
+ if (this.cachedPolylineApproximation) {
113
+ return this.cachedPolylineApproximation;
114
+ }
115
+
116
+ const points: Point2[] = [];
117
+
118
+ for (const part of this.parts) {
119
+ switch (part.kind) {
120
+ case PathCommandType.CubicBezierTo:
121
+ points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
122
+ break;
123
+ case PathCommandType.QuadraticBezierTo:
124
+ points.push(part.controlPoint, part.endPoint);
125
+ break;
126
+ case PathCommandType.MoveTo:
127
+ case PathCommandType.LineTo:
128
+ points.push(part.point);
129
+ break;
130
+ }
131
+ }
132
+
133
+ const result: LineSegment2[] = [];
134
+ let prevPoint = this.startPoint;
135
+ for (const point of points) {
136
+ result.push(new LineSegment2(prevPoint, point));
137
+ prevPoint = point;
138
+ }
139
+
140
+ return result;
141
+ }
142
+
109
143
  public static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2 {
110
144
  const points = [startPoint];
111
145
  let exhaustivenessCheck: never;
@@ -129,6 +163,10 @@ export default class Path {
129
163
  }
130
164
 
131
165
  public intersection(line: LineSegment2): IntersectionResult[] {
166
+ if (!line.bbox.intersects(this.bbox)) {
167
+ return [];
168
+ }
169
+
132
170
  const result: IntersectionResult[] = [];
133
171
  for (const part of this.geometry) {
134
172
  if (part instanceof LineSegment2) {
@@ -229,6 +267,55 @@ export default class Path {
229
267
  ]);
230
268
  }
231
269
 
270
+ // Treats this as a closed path and returns true if part of `rect` is roughly within
271
+ // this path's interior.
272
+ //
273
+ // Note: Assumes that this is a closed, non-self-intersecting path.
274
+ public closedRoughlyIntersects(rect: Rect2): boolean {
275
+ if (rect.containsRect(this.bbox)) {
276
+ return true;
277
+ }
278
+
279
+ // Choose a point outside of the path.
280
+ const startPt = this.bbox.topLeft.minus(Vec2.of(1, 1));
281
+ const testPts = rect.corners;
282
+ const polygon = this.polylineApproximation();
283
+
284
+ for (const point of testPts) {
285
+ const testLine = new LineSegment2(point, startPt);
286
+
287
+ let intersectionCount = 0;
288
+ for (const line of polygon) {
289
+ if (line.intersects(testLine)) {
290
+ intersectionCount ++;
291
+ }
292
+ }
293
+
294
+ // Odd? The point is within the polygon!
295
+ if (intersectionCount % 2 === 1) {
296
+ return true;
297
+ }
298
+ }
299
+
300
+ // Grow the rectangle for possible additional precision.
301
+ const grownRect = rect.grownBy(Math.min(rect.size.x, rect.size.y));
302
+ const edges = [];
303
+ for (const subrect of grownRect.divideIntoGrid(4, 4)) {
304
+ edges.push(...subrect.getEdges());
305
+ }
306
+
307
+ for (const edge of edges) {
308
+ for (const line of polygon) {
309
+ if (edge.intersects(line)) {
310
+ return true;
311
+ }
312
+ }
313
+ }
314
+
315
+ // Even? Probably no intersection.
316
+ return false;
317
+ }
318
+
232
319
  // Returns a path that outlines [rect]. If [lineWidth] is not given, the resultant path is
233
320
  // the outline of [rect]. Otherwise, the resultant path represents a line of width [lineWidth]
234
321
  // that traces [rect].
@@ -273,6 +360,10 @@ export default class Path {
273
360
  }
274
361
 
275
362
  public static fromRenderable(renderable: RenderablePathSpec): Path {
363
+ if (renderable.path) {
364
+ return renderable.path;
365
+ }
366
+
276
367
  return new Path(renderable.startPoint, renderable.commands);
277
368
  }
278
369
 
@@ -281,18 +372,23 @@ export default class Path {
281
372
  startPoint: this.startPoint,
282
373
  style: fill,
283
374
  commands: this.parts,
375
+ path: this,
284
376
  };
285
377
  }
286
378
 
379
+ private cachedStringVersion: string|null = null;
380
+
287
381
  public toString(): string {
382
+ if (this.cachedStringVersion) {
383
+ return this.cachedStringVersion;
384
+ }
385
+
288
386
  // Hueristic: Try to determine whether converting absolute to relative commands is worth it.
289
- // If we're near (0, 0), it probably isn't worth it and if bounding boxes are large,
290
- // it also probably isn't worth it.
291
- const makeRelativeCommands =
292
- Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.size.x) < 2
293
- && Math.abs(this.bbox.topLeft.y) > 10 && Math.abs(this.bbox.size.y) < 2;
387
+ const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
294
388
 
295
- return Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
389
+ const result = Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
390
+ this.cachedStringVersion = result;
391
+ return result;
296
392
  }
297
393
 
298
394
  public serialize(): string {
@@ -301,7 +397,7 @@ export default class Path {
301
397
 
302
398
  // @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
303
399
  // conversions can lead to smaller output strings, but also take time.
304
- public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands: boolean = true): string {
400
+ public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands?: boolean): string {
305
401
  const result: string[] = [];
306
402
 
307
403
  let prevPoint: Point2|undefined;
@@ -561,7 +657,9 @@ export default class Path {
561
657
  }
562
658
  }
563
659
 
564
- return new Path(startPos ?? Vec2.zero, commands);
660
+ const result = new Path(startPos ?? Vec2.zero, commands);
661
+ result.cachedStringVersion = pathString;
662
+ return result;
565
663
  }
566
664
 
567
665
  public static empty: Path = new Path(Vec2.zero, []);
package/src/math/Rect2.ts CHANGED
@@ -12,7 +12,7 @@ export interface RectTemplate {
12
12
  height?: number;
13
13
  }
14
14
 
15
- // invariant: w > 0, h > 0.
15
+ // invariant: w 0, h 0.
16
16
  export default class Rect2 {
17
17
  // Derived state:
18
18
 
@@ -0,0 +1,29 @@
1
+ import Mat33 from './Mat33';
2
+ import Vec3 from './Vec3';
3
+
4
+ export default class Triangle {
5
+ public constructor(
6
+ public readonly vertex1: Vec3,
7
+ public readonly vertex2: Vec3,
8
+ public readonly vertex3: Vec3,
9
+ ) {}
10
+
11
+ public map(mapping: (vertex: Vec3)=>Vec3): Triangle {
12
+ return new Triangle(
13
+ mapping(this.vertex1),
14
+ mapping(this.vertex2),
15
+ mapping(this.vertex3),
16
+ );
17
+ }
18
+
19
+ // Transform, treating this as composed of 2D points.
20
+ public transformed2DBy(affineTransform: Mat33) {
21
+ return this.map(affineTransform.transformVec2);
22
+ }
23
+
24
+ // Transforms this by a linear transform --- verticies are treated as
25
+ // 3D points.
26
+ public transformedBy(linearTransform: Mat33) {
27
+ return this.map(linearTransform.transformVec3);
28
+ }
29
+ }
@@ -76,9 +76,9 @@ export default class Display {
76
76
  return this.dryInkRenderer.canRenderFromWithoutDataLoss(renderer);
77
77
  },
78
78
  blockResolution: cacheBlockResolution,
79
- cacheSize: 600 * 600 * 4 * 120,
79
+ cacheSize: 600 * 600 * 4 * 90,
80
80
  maxScale: 1.4,
81
- minComponentsPerCache: 45,
81
+ minComponentsPerCache: 20,
82
82
  minComponentsToUseCache: 105,
83
83
  });
84
84
 
@@ -2,13 +2,19 @@
2
2
  export interface TextRendererLocalization {
3
3
  pathNodeCount(pathCount: number): string;
4
4
  textNodeCount(nodeCount: number): string;
5
+ imageNodeCount(nodeCount: number): string;
5
6
  textNode(content: string): string;
7
+ unlabeledImageNode: string;
8
+ imageNode(label: string): string;
6
9
  rerenderAsText: string;
7
10
  }
8
11
 
9
12
  export const defaultTextRendererLocalization: TextRendererLocalization = {
10
13
  pathNodeCount: (count: number) => `There are ${count} visible path objects.`,
11
14
  textNodeCount: (count: number) => `There are ${count} visible text nodes.`,
15
+ imageNodeCount: (nodeCount: number) => `There are ${nodeCount} visible image nodes.`,
12
16
  textNode: (content: string) => `Text: ${content}`,
17
+ imageNode: (label: string) => `Image: ${label}`,
18
+ unlabeledImageNode: 'Unlabeled image',
13
19
  rerenderAsText: 'Re-render as text',
14
20
  };
@@ -11,6 +11,22 @@ export interface RenderablePathSpec {
11
11
  startPoint: Point2;
12
12
  commands: PathCommand[];
13
13
  style: RenderingStyle;
14
+ path?: Path;
15
+ }
16
+
17
+ export interface RenderableImage {
18
+ transform: Mat33;
19
+
20
+ // An Image or HTMLCanvasElement. If an Image, it must be loaded from the same origin as this
21
+ // (and should have `src=this.base64Url`).
22
+ image: HTMLImageElement|HTMLCanvasElement;
23
+
24
+ // All images that can be drawn **must** have a base64 URL in the form
25
+ // data:image/[format];base64,[data here]
26
+ // If `image` is an Image, this should be equivalent to `image.src`.
27
+ base64Url: string;
28
+
29
+ label?: string;
14
30
  }
15
31
 
16
32
  export default abstract class AbstractRenderer {
@@ -40,6 +56,7 @@ export default abstract class AbstractRenderer {
40
56
  controlPoint: Point2, endPoint: Point2,
41
57
  ): void;
42
58
  public abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
59
+ public abstract drawImage(image: RenderableImage): void;
43
60
 
44
61
  // Returns true iff the given rectangle is so small, rendering anything within
45
62
  // it has no effect on the image.
@@ -6,7 +6,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
6
6
  import Vec3 from '../../math/Vec3';
7
7
  import Viewport from '../../Viewport';
8
8
  import RenderingStyle from '../RenderingStyle';
9
- import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
9
+ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
10
10
 
11
11
  export default class CanvasRenderer extends AbstractRenderer {
12
12
  private ignoreObjectsAboveLevel: number|null = null;
@@ -168,6 +168,15 @@ export default class CanvasRenderer extends AbstractRenderer {
168
168
  this.ctx.restore();
169
169
  }
170
170
 
171
+ public drawImage(image: RenderableImage) {
172
+ this.ctx.save();
173
+ const transform = this.getCanvasToScreenTransform().rightMul(image.transform);
174
+ this.transformBy(transform);
175
+
176
+ this.ctx.drawImage(image.image, 0, 0);
177
+ this.ctx.restore();
178
+ }
179
+
171
180
  private clipLevels: number[] = [];
172
181
  public startObject(boundingBox: Rect2, clip: boolean) {
173
182
  if (this.isTooSmallToRender(boundingBox)) {
@@ -7,7 +7,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
7
7
  import Vec3 from '../../math/Vec3';
8
8
  import Viewport from '../../Viewport';
9
9
  import RenderingStyle from '../RenderingStyle';
10
- import AbstractRenderer from './AbstractRenderer';
10
+ import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
11
11
 
12
12
  export default class DummyRenderer extends AbstractRenderer {
13
13
  // Variables that track the state of what's been rendered
@@ -17,6 +17,7 @@ export default class DummyRenderer extends AbstractRenderer {
17
17
  public lastPoint: Point2|null = null;
18
18
  public objectNestingLevel: number = 0;
19
19
  public lastText: string|null = null;
20
+ public lastImage: RenderableImage|null = null;
20
21
 
21
22
  // List of points drawn since the last clear.
22
23
  public pointBuffer: Point2[] = [];
@@ -44,6 +45,7 @@ export default class DummyRenderer extends AbstractRenderer {
44
45
  this.renderedPathCount = 0;
45
46
  this.pointBuffer = [];
46
47
  this.lastText = null;
48
+ this.lastImage = null;
47
49
 
48
50
  // Ensure all objects finished rendering
49
51
  if (this.objectNestingLevel > 0) {
@@ -96,6 +98,9 @@ export default class DummyRenderer extends AbstractRenderer {
96
98
  public drawText(text: string, _transform: Mat33, _style: TextStyle): void {
97
99
  this.lastText = text;
98
100
  }
101
+ public drawImage(image: RenderableImage) {
102
+ this.lastImage = image;
103
+ }
99
104
 
100
105
  public startObject(boundingBox: Rect2, _clip: boolean) {
101
106
  super.startObject(boundingBox);
@@ -2,34 +2,35 @@
2
2
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
3
  import { TextStyle } from '../../components/Text';
4
4
  import Mat33 from '../../math/Mat33';
5
- import Path, { PathCommand, PathCommandType } from '../../math/Path';
5
+ import Path from '../../math/Path';
6
6
  import Rect2 from '../../math/Rect2';
7
7
  import { toRoundedString } from '../../math/rounding';
8
8
  import { Point2, Vec2 } from '../../math/Vec2';
9
9
  import { svgAttributesDataKey, SVGLoaderUnknownAttribute, SVGLoaderUnknownStyleAttribute, svgStyleAttributesDataKey } from '../../SVGLoader';
10
10
  import Viewport from '../../Viewport';
11
11
  import RenderingStyle from '../RenderingStyle';
12
- import AbstractRenderer from './AbstractRenderer';
12
+ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
13
13
 
14
14
  const svgNameSpace = 'http://www.w3.org/2000/svg';
15
15
  export default class SVGRenderer extends AbstractRenderer {
16
- private currentPath: PathCommand[]|null;
17
- private pathStart: Point2|null;
18
-
19
- private lastPathStyle: RenderingStyle|null;
20
- private lastPath: PathCommand[]|null;
21
- private lastPathStart: Point2|null;
16
+ private lastPathStyle: RenderingStyle|null = null;
17
+ private lastPathString: string[] = [];
22
18
  private objectElems: SVGElement[]|null = null;
23
19
 
24
20
  private overwrittenAttrs: Record<string, string|null> = {};
25
21
 
26
- public constructor(private elem: SVGSVGElement, viewport: Viewport) {
22
+ // Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
23
+ public constructor(private elem: SVGSVGElement, viewport: Viewport, private sanitize: boolean = false) {
27
24
  super(viewport);
28
25
  this.clear();
29
26
  }
30
27
 
31
28
  // Sets an attribute on the root SVG element.
32
29
  public setRootSVGAttribute(name: string, value: string|null) {
30
+ if (this.sanitize) {
31
+ return;
32
+ }
33
+
33
34
  // Make the original value of the attribute restorable on clear
34
35
  if (!(name in this.overwrittenAttrs)) {
35
36
  this.overwrittenAttrs[name] = this.elem.getAttribute(name);
@@ -47,57 +48,31 @@ export default class SVGRenderer extends AbstractRenderer {
47
48
  }
48
49
 
49
50
  public clear() {
50
- // Restore all alltributes
51
- for (const attrName in this.overwrittenAttrs) {
52
- const value = this.overwrittenAttrs[attrName];
53
-
54
- if (value) {
55
- this.elem.setAttribute(attrName, value);
56
- } else {
57
- this.elem.removeAttribute(attrName);
58
- }
59
- }
60
- this.overwrittenAttrs = {};
61
- }
51
+ this.lastPathString = [];
62
52
 
63
- protected beginPath(startPoint: Point2) {
64
- this.currentPath = [];
65
- this.pathStart = this.canvasToScreen(startPoint);
66
- this.lastPathStart ??= this.pathStart;
67
- }
68
-
69
- protected endPath(style: RenderingStyle) {
70
- if (this.currentPath == null) {
71
- throw new Error('No path exists to end! Make sure beginPath was called!');
72
- }
73
-
74
- // Try to extend the previous path, if possible
75
- if (style.fill.eq(this.lastPathStyle?.fill) && this.lastPath != null) {
76
- this.lastPath.push({
77
- kind: PathCommandType.MoveTo,
78
- point: this.pathStart!,
79
- }, ...this.currentPath);
80
- this.pathStart = null;
81
- this.currentPath = null;
82
- } else {
83
- this.addPathToSVG();
84
- this.lastPathStart = this.pathStart;
85
- this.lastPathStyle = style;
86
- this.lastPath = this.currentPath;
53
+ if (!this.sanitize) {
54
+ // Restore all all attributes
55
+ for (const attrName in this.overwrittenAttrs) {
56
+ const value = this.overwrittenAttrs[attrName];
87
57
 
88
- this.pathStart = null;
89
- this.currentPath = null;
58
+ if (value) {
59
+ this.elem.setAttribute(attrName, value);
60
+ } else {
61
+ this.elem.removeAttribute(attrName);
62
+ }
63
+ }
64
+ this.overwrittenAttrs = {};
90
65
  }
91
66
  }
92
67
 
93
68
  // Push [this.fullPath] to the SVG
94
69
  private addPathToSVG() {
95
- if (!this.lastPathStyle || !this.lastPath) {
70
+ if (!this.lastPathStyle || this.lastPathString.length === 0) {
96
71
  return;
97
72
  }
98
73
 
99
74
  const pathElem = document.createElementNS(svgNameSpace, 'path');
100
- pathElem.setAttribute('d', Path.toString(this.lastPathStart!, this.lastPath));
75
+ pathElem.setAttribute('d', this.lastPathString.join(' '));
101
76
 
102
77
  const style = this.lastPathStyle;
103
78
  pathElem.setAttribute('fill', style.fill.toHexString());
@@ -111,26 +86,44 @@ export default class SVGRenderer extends AbstractRenderer {
111
86
  this.objectElems?.push(pathElem);
112
87
  }
113
88
 
114
- public drawText(text: string, transform: Mat33, style: TextStyle): void {
115
- transform = this.getCanvasToScreenTransform().rightMul(transform);
89
+ public drawPath(pathSpec: RenderablePathSpec) {
90
+ const style = pathSpec.style;
91
+ const path = Path.fromRenderable(pathSpec);
116
92
 
93
+ // Try to extend the previous path, if possible
94
+ if (!style.fill.eq(this.lastPathStyle?.fill) || this.lastPathString.length === 0) {
95
+ this.addPathToSVG();
96
+ this.lastPathStyle = style;
97
+ this.lastPathString = [];
98
+ }
99
+ this.lastPathString.push(path.toString());
100
+ }
101
+
102
+ // Apply [elemTransform] to [elem].
103
+ private transformFrom(elemTransform: Mat33, elem: SVGElement) {
104
+ let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
117
105
  const translation = transform.transformVec2(Vec2.zero);
118
106
  transform = transform.rightMul(Mat33.translation(translation.times(-1)));
119
107
 
120
- const textElem = document.createElementNS(svgNameSpace, 'text');
121
- textElem.appendChild(document.createTextNode(text));
122
- textElem.style.transform = `matrix(
108
+ elem.style.transform = `matrix(
123
109
  ${transform.a1}, ${transform.b1},
124
110
  ${transform.a2}, ${transform.b2},
125
111
  ${transform.a3}, ${transform.b3}
126
112
  )`;
113
+ elem.setAttribute('x', `${toRoundedString(translation.x)}`);
114
+ elem.setAttribute('y', `${toRoundedString(translation.y)}`);
115
+ }
116
+
117
+ public drawText(text: string, transform: Mat33, style: TextStyle): void {
118
+ const textElem = document.createElementNS(svgNameSpace, 'text');
119
+ textElem.appendChild(document.createTextNode(text));
120
+ this.transformFrom(transform, textElem);
121
+
127
122
  textElem.style.fontFamily = style.fontFamily;
128
123
  textElem.style.fontVariant = style.fontVariant ?? '';
129
124
  textElem.style.fontWeight = style.fontWeight ?? '';
130
125
  textElem.style.fontSize = style.size + 'px';
131
126
  textElem.style.fill = style.renderingStyle.fill.toHexString();
132
- textElem.setAttribute('x', `${toRoundedString(translation.x)}`);
133
- textElem.setAttribute('y', `${toRoundedString(translation.y)}`);
134
127
 
135
128
  if (style.renderingStyle.stroke) {
136
129
  const strokeStyle = style.renderingStyle.stroke;
@@ -142,12 +135,23 @@ export default class SVGRenderer extends AbstractRenderer {
142
135
  this.objectElems?.push(textElem);
143
136
  }
144
137
 
138
+ public drawImage(image: RenderableImage) {
139
+ const svgImgElem = document.createElementNS(svgNameSpace, 'image');
140
+ svgImgElem.setAttribute('href', image.base64Url);
141
+ svgImgElem.setAttribute('width', image.image.getAttribute('width') ?? '');
142
+ svgImgElem.setAttribute('height', image.image.getAttribute('height') ?? '');
143
+ svgImgElem.setAttribute('aria-label', image.image.getAttribute('aria-label') ?? image.image.getAttribute('alt') ?? '');
144
+ this.transformFrom(image.transform, svgImgElem);
145
+
146
+ this.elem.appendChild(svgImgElem);
147
+ this.objectElems?.push(svgImgElem);
148
+ }
149
+
145
150
  public startObject(boundingBox: Rect2) {
146
151
  super.startObject(boundingBox);
147
152
 
148
153
  // Only accumulate a path within an object
149
- this.lastPath = null;
150
- this.lastPathStart = null;
154
+ this.lastPathString = [];
151
155
  this.lastPathStyle = null;
152
156
  this.objectElems = [];
153
157
  }
@@ -158,7 +162,7 @@ export default class SVGRenderer extends AbstractRenderer {
158
162
  // Don't extend paths across objects
159
163
  this.addPathToSVG();
160
164
 
161
- if (loaderData) {
165
+ if (loaderData && !this.sanitize) {
162
166
  // Restore any attributes unsupported by the app.
163
167
  for (const elem of this.objectElems ?? []) {
164
168
  const attrs = loaderData[svgAttributesDataKey] as SVGLoaderUnknownAttribute[]|undefined;
@@ -179,49 +183,16 @@ export default class SVGRenderer extends AbstractRenderer {
179
183
  }
180
184
  }
181
185
 
182
- protected lineTo(point: Point2) {
183
- point = this.canvasToScreen(point);
184
-
185
- this.currentPath!.push({
186
- kind: PathCommandType.LineTo,
187
- point,
188
- });
189
- }
190
-
191
- protected moveTo(point: Point2) {
192
- point = this.canvasToScreen(point);
193
-
194
- this.currentPath!.push({
195
- kind: PathCommandType.MoveTo,
196
- point,
197
- });
198
- }
199
-
186
+ // Not implemented -- use drawPath instead.
187
+ private unimplementedMessage() { throw new Error('Not implemenented!'); }
188
+ protected beginPath(_startPoint: Point2) { this.unimplementedMessage(); }
189
+ protected endPath(_style: RenderingStyle) { this.unimplementedMessage(); }
190
+ protected lineTo(_point: Point2) { this.unimplementedMessage(); }
191
+ protected moveTo(_point: Point2) { this.unimplementedMessage(); }
200
192
  protected traceCubicBezierCurve(
201
- controlPoint1: Point2, controlPoint2: Point2, endPoint: Point2
202
- ) {
203
- controlPoint1 = this.canvasToScreen(controlPoint1);
204
- controlPoint2 = this.canvasToScreen(controlPoint2);
205
- endPoint = this.canvasToScreen(endPoint);
206
-
207
- this.currentPath!.push({
208
- kind: PathCommandType.CubicBezierTo,
209
- controlPoint1,
210
- controlPoint2,
211
- endPoint,
212
- });
213
- }
214
-
215
- protected traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2) {
216
- controlPoint = this.canvasToScreen(controlPoint);
217
- endPoint = this.canvasToScreen(endPoint);
218
-
219
- this.currentPath!.push({
220
- kind: PathCommandType.QuadraticBezierTo,
221
- controlPoint,
222
- endPoint,
223
- });
224
- }
193
+ _controlPoint1: Point2, _controlPoint2: Point2, _endPoint: Point2
194
+ ) { this.unimplementedMessage(); }
195
+ protected traceQuadraticBezierCurve(_controlPoint: Point2, _endPoint: Point2) { this.unimplementedMessage(); }
225
196
 
226
197
  public drawPoints(...points: Point2[]) {
227
198
  points.map(point => {
@@ -235,6 +206,10 @@ export default class SVGRenderer extends AbstractRenderer {
235
206
 
236
207
  // Renders a **copy** of the given element.
237
208
  public drawSVGElem(elem: SVGElement) {
209
+ if (this.sanitize) {
210
+ return;
211
+ }
212
+
238
213
  this.elem.appendChild(elem.cloneNode(true));
239
214
  }
240
215
 
@@ -6,7 +6,7 @@ import Vec3 from '../../math/Vec3';
6
6
  import Viewport from '../../Viewport';
7
7
  import { TextRendererLocalization } from '../localization';
8
8
  import RenderingStyle from '../RenderingStyle';
9
- import AbstractRenderer from './AbstractRenderer';
9
+ import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
10
10
 
11
11
  // Outputs a description of what was rendered.
12
12
 
@@ -14,6 +14,7 @@ export default class TextOnlyRenderer extends AbstractRenderer {
14
14
  private descriptionBuilder: string[] = [];
15
15
  private pathCount: number = 0;
16
16
  private textNodeCount: number = 0;
17
+ private imageNodeCount: number = 0;
17
18
 
18
19
  public constructor(viewport: Viewport, private localizationTable: TextRendererLocalization) {
19
20
  super(viewport);
@@ -33,7 +34,8 @@ export default class TextOnlyRenderer extends AbstractRenderer {
33
34
  public getDescription(): string {
34
35
  return [
35
36
  this.localizationTable.pathNodeCount(this.pathCount),
36
- this.localizationTable.textNodeCount(this.textNodeCount),
37
+ ...(this.textNodeCount > 0 ? this.localizationTable.textNodeCount(this.textNodeCount) : []),
38
+ ...(this.imageNodeCount > 0 ? this.localizationTable.imageNodeCount(this.imageNodeCount) : []),
37
39
  ...this.descriptionBuilder
38
40
  ].join('\n');
39
41
  }
@@ -55,6 +57,12 @@ export default class TextOnlyRenderer extends AbstractRenderer {
55
57
  this.descriptionBuilder.push(this.localizationTable.textNode(text));
56
58
  this.textNodeCount ++;
57
59
  }
60
+ public drawImage(image: RenderableImage) {
61
+ const label = image.label ? this.localizationTable.imageNode(image.label) : this.localizationTable.unlabeledImageNode;
62
+
63
+ this.descriptionBuilder.push(label);
64
+ this.imageNodeCount ++;
65
+ }
58
66
  public isTooSmallToRender(rect: Rect2): boolean {
59
67
  return rect.maxDimension < 15 / this.getSizeOfCanvasPixelOnScreen();
60
68
  }
@@ -17,7 +17,6 @@ import HandToolWidget from './widgets/HandToolWidget';
17
17
  import BaseWidget from './widgets/BaseWidget';
18
18
  import { EraserTool, PenTool } from '../tools/lib';
19
19
 
20
-
21
20
  export const toolbarCSSPrefix = 'toolbar-';
22
21
 
23
22
  type UpdateColorisCallback = ()=>void;
@@ -28,6 +27,7 @@ export default class HTMLToolbar {
28
27
  private static colorisStarted: boolean = false;
29
28
  private updateColoris: UpdateColorisCallback|null = null;
30
29
 
30
+ /** @internal */
31
31
  public constructor(
32
32
  private editor: Editor, parent: HTMLElement,
33
33
  private localizationTable: ToolbarLocalization = defaultToolbarLocalization,