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
@@ -55,7 +55,7 @@ export default class Path {
55
55
  static fromRenderable(renderable: RenderablePathSpec): Path;
56
56
  toRenderable(fill: RenderingStyle): RenderablePathSpec;
57
57
  private cachedStringVersion;
58
- toString(): string;
58
+ toString(useNonAbsCommands?: boolean): string;
59
59
  serialize(): string;
60
60
  static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands?: boolean): string;
61
61
  static fromString(pathString: string): Path;
@@ -282,13 +282,15 @@ export default class Path {
282
282
  path: this,
283
283
  };
284
284
  }
285
- toString() {
285
+ toString(useNonAbsCommands) {
286
286
  if (this.cachedStringVersion) {
287
287
  return this.cachedStringVersion;
288
288
  }
289
- // Hueristic: Try to determine whether converting absolute to relative commands is worth it.
290
- const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
291
- const result = Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
289
+ if (useNonAbsCommands === undefined) {
290
+ // Hueristic: Try to determine whether converting absolute to relative commands is worth it.
291
+ useNonAbsCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
292
+ }
293
+ const result = Path.toString(this.startPoint, this.parts, !useNonAbsCommands);
292
294
  this.cachedStringVersion = result;
293
295
  return result;
294
296
  }
@@ -307,10 +309,12 @@ export default class Path {
307
309
  const roundedPrevX = prevPoint ? toRoundedString(prevPoint.x) : '';
308
310
  const roundedPrevY = prevPoint ? toRoundedString(prevPoint.y) : '';
309
311
  for (const point of points) {
312
+ const xComponent = toRoundedString(point.x);
313
+ const yComponent = toRoundedString(point.y);
310
314
  // Relative commands are often shorter as strings than absolute commands.
311
315
  if (!makeAbsCommand) {
312
- const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint.x, roundedPrevX, roundedPrevY);
313
- const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint.y, roundedPrevX, roundedPrevY);
316
+ const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint.x, xComponent, roundedPrevX, roundedPrevY);
317
+ const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint.y, yComponent, roundedPrevX, roundedPrevY);
314
318
  // No need for an additional separator if it starts with a '-'
315
319
  if (yComponentRelative.charAt(0) === '-') {
316
320
  relativeCommandParts.push(`${xComponentRelative}${yComponentRelative}`);
@@ -320,8 +324,6 @@ export default class Path {
320
324
  }
321
325
  }
322
326
  else {
323
- const xComponent = toRoundedString(point.x);
324
- const yComponent = toRoundedString(point.y);
325
327
  absoluteCommandParts.push(`${xComponent},${yComponent}`);
326
328
  }
327
329
  }
@@ -38,6 +38,16 @@ export default class Vec3 {
38
38
  minus(v: Vec3): Vec3;
39
39
  dot(other: Vec3): number;
40
40
  cross(other: Vec3): Vec3;
41
+ /**
42
+ * If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
43
+ * if `other is a `number`, returns the result of scalar multiplication.
44
+ *
45
+ * @example
46
+ * ```
47
+ * Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
48
+ * ```
49
+ */
50
+ scale(other: Vec3 | number): Vec3;
41
51
  /**
42
52
  * Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
43
53
  * 90 degrees counter-clockwise.
@@ -73,7 +83,7 @@ export default class Vec3 {
73
83
  * ```
74
84
  */
75
85
  map(fn: (component: number, index: number) => number): Vec3;
76
- asArray(): number[];
86
+ asArray(): [number, number, number];
77
87
  /**
78
88
  * [fuzz] The maximum difference between two components for this and [other]
79
89
  * to be considered equal.
@@ -76,6 +76,21 @@ export default class Vec3 {
76
76
  // | x2 y2 z2|
77
77
  return Vec3.of(this.y * other.z - other.y * this.z, other.x * this.z - this.x * other.z, this.x * other.y - other.x * this.y);
78
78
  }
79
+ /**
80
+ * If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
81
+ * if `other is a `number`, returns the result of scalar multiplication.
82
+ *
83
+ * @example
84
+ * ```
85
+ * Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
86
+ * ```
87
+ */
88
+ scale(other) {
89
+ if (typeof other === 'number') {
90
+ return this.times(other);
91
+ }
92
+ return Vec3.of(this.x * other.x, this.y * other.y, this.z * other.z);
93
+ }
79
94
  /**
80
95
  * Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
81
96
  * 90 degrees counter-clockwise.
@@ -1,3 +1,4 @@
1
+ export declare const cleanUpNumber: (text: string) => string;
1
2
  export declare const toRoundedString: (num: number) => string;
2
3
  export declare const getLenAfterDecimal: (numberAsString: string) => number;
3
4
  export declare const toStringOfSamePrecision: (num: number, ...references: string[]) => string;
@@ -1,8 +1,14 @@
1
1
  // @packageDocumentation @internal
2
2
  // Clean up stringified numbers
3
- const cleanUpNumber = (text) => {
3
+ export const cleanUpNumber = (text) => {
4
4
  // Regular expression substitions can be somewhat expensive. Only do them
5
5
  // if necessary.
6
+ if (text.indexOf('e') > 0) {
7
+ // Round to zero.
8
+ if (text.match(/[eE][-]\d{2,}$/)) {
9
+ return '0';
10
+ }
11
+ }
6
12
  const lastChar = text.charAt(text.length - 1);
7
13
  if (lastChar === '0' || lastChar === '.') {
8
14
  // Remove trailing zeroes
@@ -10,23 +16,24 @@ const cleanUpNumber = (text) => {
10
16
  text = text.replace(/[.]0+$/, '.');
11
17
  // Remove trailing period
12
18
  text = text.replace(/[.]$/, '');
13
- if (text === '-0') {
14
- return '0';
15
- }
16
19
  }
17
20
  const firstChar = text.charAt(0);
18
21
  if (firstChar === '0' || firstChar === '-') {
19
22
  // Remove unnecessary leading zeroes.
20
23
  text = text.replace(/^(0+)[.]/, '.');
21
24
  text = text.replace(/^-(0+)[.]/, '-.');
25
+ text = text.replace(/^(-?)0+$/, '$10');
26
+ }
27
+ if (text === '-0') {
28
+ return '0';
22
29
  }
23
30
  return text;
24
31
  };
25
32
  export const toRoundedString = (num) => {
26
33
  // Try to remove rounding errors. If the number ends in at least three/four zeroes
27
34
  // (or nines) just one or two digits, it's probably a rounding error.
28
- const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,2}$/;
29
- const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,2}$/;
35
+ const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
36
+ const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
30
37
  let text = num.toString(10);
31
38
  if (text.indexOf('.') === -1) {
32
39
  return text;
@@ -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;
@@ -64,7 +64,8 @@ export default class AbstractRenderer {
64
64
  this.currentPaths.push(path);
65
65
  }
66
66
  }
67
- // Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]
67
+ // Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
68
+ // This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
68
69
  drawRect(rect, lineWidth, lineFill) {
69
70
  const path = Path.fromRect(rect, lineWidth);
70
71
  this.drawPath(path.toRenderable(lineFill));
@@ -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,17 +1,18 @@
1
1
  import { EditorEventType } from '../types';
2
2
  import { coloris, init as colorisInit } from '@melloware/coloris';
3
3
  import Color4 from '../Color4';
4
- import SelectionTool from '../tools/SelectionTool';
5
4
  import { defaultToolbarLocalization } from './localization';
6
5
  import { makeRedoIcon, makeUndoIcon } from './icons';
7
- import PanZoom from '../tools/PanZoom';
6
+ import SelectionTool from '../tools/SelectionTool/SelectionTool';
7
+ import PanZoomTool from '../tools/PanZoom';
8
8
  import TextTool from '../tools/TextTool';
9
+ import EraserTool from '../tools/Eraser';
10
+ import PenTool from '../tools/Pen';
9
11
  import PenToolWidget from './widgets/PenToolWidget';
10
12
  import EraserWidget from './widgets/EraserToolWidget';
11
13
  import SelectionToolWidget from './widgets/SelectionToolWidget';
12
14
  import TextToolWidget from './widgets/TextToolWidget';
13
15
  import HandToolWidget from './widgets/HandToolWidget';
14
- import { EraserTool, PenTool } from '../tools/lib';
15
16
  export const toolbarCSSPrefix = 'toolbar-';
16
17
  export default class HTMLToolbar {
17
18
  /** @internal */
@@ -158,7 +159,7 @@ export default class HTMLToolbar {
158
159
  for (const tool of toolController.getMatchingTools(TextTool)) {
159
160
  this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
160
161
  }
161
- const panZoomTool = toolController.getMatchingTools(PanZoom)[0];
162
+ const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
162
163
  if (panZoomTool) {
163
164
  this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
164
165
  }
@@ -1,5 +1,5 @@
1
1
  import Editor from '../../Editor';
2
- import SelectionTool from '../../tools/SelectionTool';
2
+ import SelectionTool from '../../tools/SelectionTool/SelectionTool';
3
3
  import { ToolbarLocalization } from '../localization';
4
4
  import BaseToolWidget from './BaseToolWidget';
5
5
  export default class SelectionToolWidget extends BaseToolWidget {
@@ -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;
@@ -13,6 +13,12 @@ export default class BaseTool {
13
13
  onWheel(_event) {
14
14
  return false;
15
15
  }
16
+ onCopy(_event) {
17
+ return false;
18
+ }
19
+ onPaste(_event) {
20
+ return false;
21
+ }
16
22
  onKeyPress(_event) {
17
23
  return false;
18
24
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A tool that handles paste events.
3
+ * @packageDocumentation
4
+ */
5
+ import Editor from '../Editor';
6
+ import { PasteEvent } from '../types';
7
+ import BaseTool from './BaseTool';
8
+ export default class PasteHandler extends BaseTool {
9
+ private editor;
10
+ constructor(editor: Editor);
11
+ onPaste(event: PasteEvent): boolean;
12
+ private addComponentsFromPaste;
13
+ private doSVGPaste;
14
+ private doTextPaste;
15
+ private doImagePaste;
16
+ }