js-draw 0.1.1 → 0.1.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 (52) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.js +4 -0
  4. package/dist/src/EditorImage.js +3 -0
  5. package/dist/src/Pointer.d.ts +3 -2
  6. package/dist/src/Pointer.js +12 -3
  7. package/dist/src/SVGLoader.d.ts +3 -0
  8. package/dist/src/SVGLoader.js +11 -1
  9. package/dist/src/Viewport.js +10 -0
  10. package/dist/src/components/AbstractComponent.d.ts +6 -0
  11. package/dist/src/components/AbstractComponent.js +11 -0
  12. package/dist/src/components/Stroke.js +1 -1
  13. package/dist/src/geometry/Path.js +97 -66
  14. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +2 -1
  15. package/dist/src/rendering/renderers/AbstractRenderer.js +1 -1
  16. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -2
  17. package/dist/src/rendering/renderers/SVGRenderer.js +21 -7
  18. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  19. package/dist/src/toolbar/HTMLToolbar.js +165 -154
  20. package/dist/src/toolbar/icons.d.ts +10 -0
  21. package/dist/src/toolbar/icons.js +180 -0
  22. package/dist/src/toolbar/localization.d.ts +4 -1
  23. package/dist/src/toolbar/localization.js +4 -1
  24. package/dist/src/toolbar/types.d.ts +4 -0
  25. package/dist/src/tools/PanZoom.d.ts +9 -6
  26. package/dist/src/tools/PanZoom.js +30 -21
  27. package/dist/src/tools/Pen.js +8 -3
  28. package/dist/src/tools/ToolController.d.ts +5 -6
  29. package/dist/src/tools/ToolController.js +8 -10
  30. package/dist/src/tools/localization.d.ts +1 -0
  31. package/dist/src/tools/localization.js +1 -0
  32. package/package.json +1 -1
  33. package/src/Editor.ts +4 -0
  34. package/src/EditorImage.ts +4 -0
  35. package/src/Pointer.ts +13 -4
  36. package/src/SVGLoader.ts +25 -2
  37. package/src/Viewport.ts +13 -1
  38. package/src/components/AbstractComponent.ts +16 -1
  39. package/src/components/Stroke.ts +1 -1
  40. package/src/geometry/Path.fromString.test.ts +94 -4
  41. package/src/geometry/Path.ts +99 -67
  42. package/src/rendering/renderers/AbstractRenderer.ts +2 -1
  43. package/src/rendering/renderers/SVGRenderer.ts +22 -10
  44. package/src/toolbar/HTMLToolbar.ts +199 -170
  45. package/src/toolbar/icons.ts +203 -0
  46. package/src/toolbar/localization.ts +9 -2
  47. package/src/toolbar/toolbar.css +21 -8
  48. package/src/toolbar/types.ts +5 -0
  49. package/src/tools/PanZoom.ts +37 -27
  50. package/src/tools/Pen.ts +7 -3
  51. package/src/tools/ToolController.ts +3 -5
  52. package/src/tools/localization.ts +2 -0
@@ -115,6 +115,10 @@ export class Editor {
115
115
  // May be required to prevent text selection on iOS/Safari:
116
116
  // See https://stackoverflow.com/a/70992717/17055750
117
117
  this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
118
+ this.renderingRegion.addEventListener('contextmenu', evt => {
119
+ // Don't show a context menu
120
+ evt.preventDefault();
121
+ });
118
122
  this.renderingRegion.addEventListener('pointerdown', evt => {
119
123
  const pointer = Pointer.ofEvent(evt, true, this.viewport);
120
124
  pointers[pointer.id] = pointer;
@@ -62,6 +62,9 @@ EditorImage.AddElementCommand = (_a = class {
62
62
  _applyByFlattening.set(this, false);
63
63
  __classPrivateFieldSet(this, _element, element, "f");
64
64
  __classPrivateFieldSet(this, _applyByFlattening, applyByFlattening, "f");
65
+ if (isNaN(__classPrivateFieldGet(this, _element, "f").getBBox().area)) {
66
+ throw new Error('Elements in the image cannot have NaN bounding boxes');
67
+ }
65
68
  }
66
69
  apply(editor) {
67
70
  editor.image.addElement(__classPrivateFieldGet(this, _element, "f"));
@@ -4,8 +4,9 @@ export declare enum PointerDevice {
4
4
  Pen = 0,
5
5
  Eraser = 1,
6
6
  Touch = 2,
7
- Mouse = 3,
8
- Other = 4
7
+ PrimaryButtonMouse = 3,
8
+ RightButtonMouse = 4,
9
+ Other = 5
9
10
  }
10
11
  export default class Pointer {
11
12
  readonly screenPos: Point2;
@@ -4,8 +4,9 @@ export var PointerDevice;
4
4
  PointerDevice[PointerDevice["Pen"] = 0] = "Pen";
5
5
  PointerDevice[PointerDevice["Eraser"] = 1] = "Eraser";
6
6
  PointerDevice[PointerDevice["Touch"] = 2] = "Touch";
7
- PointerDevice[PointerDevice["Mouse"] = 3] = "Mouse";
8
- PointerDevice[PointerDevice["Other"] = 4] = "Other";
7
+ PointerDevice[PointerDevice["PrimaryButtonMouse"] = 3] = "PrimaryButtonMouse";
8
+ PointerDevice[PointerDevice["RightButtonMouse"] = 4] = "RightButtonMouse";
9
+ PointerDevice[PointerDevice["Other"] = 5] = "Other";
9
10
  })(PointerDevice || (PointerDevice = {}));
10
11
  // Provides a snapshot containing information about a pointer. A Pointer
11
12
  // object is immutable --- it will not be updated when the pointer's information changes.
@@ -34,7 +35,7 @@ export default class Pointer {
34
35
  var _a, _b;
35
36
  const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
36
37
  const pointerTypeToDevice = {
37
- 'mouse': PointerDevice.Mouse,
38
+ 'mouse': PointerDevice.PrimaryButtonMouse,
38
39
  'pen': PointerDevice.Pen,
39
40
  'touch': PointerDevice.Touch,
40
41
  };
@@ -45,6 +46,14 @@ export default class Pointer {
45
46
  }
46
47
  const timeStamp = (new Date()).getTime();
47
48
  const canvasPos = viewport.roundPoint(viewport.screenToCanvas(screenPos));
49
+ if (device === PointerDevice.PrimaryButtonMouse) {
50
+ if (evt.buttons & 0x2) {
51
+ device = PointerDevice.RightButtonMouse;
52
+ }
53
+ else if (!(evt.buttons & 0x1)) {
54
+ device = PointerDevice.Other;
55
+ }
56
+ }
48
57
  return new Pointer(screenPos, canvasPos, (_b = evt.pressure) !== null && _b !== void 0 ? _b : null, evt.isPrimary, isDown, device, evt.pointerId, timeStamp);
49
58
  }
50
59
  // Create a new Pointer from a point on the canvas.
@@ -1,6 +1,8 @@
1
1
  import Rect2 from './geometry/Rect2';
2
2
  import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
3
3
  export declare const defaultSVGViewRect: Rect2;
4
+ export declare const svgAttributesDataKey = "svgAttrs";
5
+ export declare type SVGLoaderUnknownAttribute = [string, string];
4
6
  export default class SVGLoader implements ImageLoader {
5
7
  private source;
6
8
  private onFinish?;
@@ -13,6 +15,7 @@ export default class SVGLoader implements ImageLoader {
13
15
  private constructor();
14
16
  private getStyle;
15
17
  private strokeDataFromElem;
18
+ private attachUnrecognisedAttrs;
16
19
  private addPath;
17
20
  private addUnknownNode;
18
21
  private updateViewBox;
@@ -15,6 +15,8 @@ import Path from './geometry/Path';
15
15
  import Rect2 from './geometry/Rect2';
16
16
  // Size of a loaded image if no size is specified.
17
17
  export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
18
+ // Key to retrieve unrecognised attributes from an AbstractComponent
19
+ export const svgAttributesDataKey = 'svgAttrs';
18
20
  export default class SVGLoader {
19
21
  constructor(source, onFinish) {
20
22
  this.source = source;
@@ -81,6 +83,14 @@ export default class SVGLoader {
81
83
  }
82
84
  return result;
83
85
  }
86
+ attachUnrecognisedAttrs(elem, node, supportedAttrs) {
87
+ for (const attr of node.getAttributeNames()) {
88
+ if (supportedAttrs.has(attr)) {
89
+ continue;
90
+ }
91
+ elem.attachLoadSaveData(svgAttributesDataKey, [attr, node.getAttribute(attr)]);
92
+ }
93
+ }
84
94
  // Adds a stroke with a single path
85
95
  addPath(node) {
86
96
  var _a;
@@ -88,6 +98,7 @@ export default class SVGLoader {
88
98
  try {
89
99
  const strokeData = this.strokeDataFromElem(node);
90
100
  elem = new Stroke(strokeData);
101
+ this.attachUnrecognisedAttrs(elem, node, new Set(['stroke', 'fill', 'stroke-width', 'd']));
91
102
  }
92
103
  catch (e) {
93
104
  console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
@@ -189,7 +200,6 @@ export default class SVGLoader {
189
200
  sandbox.remove();
190
201
  throw new Error('SVG loading iframe is not sandboxed.');
191
202
  }
192
- // Try running JavaScript within the iframe
193
203
  const sandboxDoc = (_b = (_a = sandbox.contentWindow) === null || _a === void 0 ? void 0 : _a.document) !== null && _b !== void 0 ? _b : sandbox.contentDocument;
194
204
  if (sandboxDoc == null)
195
205
  throw new Error('Unable to open a sandboxed iframe!');
@@ -87,6 +87,12 @@ export class Viewport {
87
87
  // Returns null if no transformation is necessary
88
88
  zoomTo(toMakeVisible) {
89
89
  let transform = Mat33.identity;
90
+ if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
91
+ throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
92
+ }
93
+ if (isNaN(toMakeVisible.size.magnitude())) {
94
+ throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
95
+ }
90
96
  // Try to move the selection within the center 2/3rds of the viewport.
91
97
  const recomputeTargetRect = () => {
92
98
  // transform transforms objects on the canvas. As such, we need to invert it
@@ -115,6 +121,10 @@ export class Viewport {
115
121
  const viewportContentTransform = visibleRectTransform.inverse();
116
122
  transform = transform.rightMul(viewportContentTransform);
117
123
  }
124
+ if (!transform.invertable()) {
125
+ console.warn('Unable to zoom to ', toMakeVisible, '! Computed transform', transform, 'is singular.');
126
+ transform = Mat33.identity;
127
+ }
118
128
  return new Viewport.ViewportTransform(transform);
119
129
  }
120
130
  }
@@ -4,12 +4,17 @@ import Mat33 from '../geometry/Mat33';
4
4
  import Rect2 from '../geometry/Rect2';
5
5
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
6
6
  import { ImageComponentLocalization } from './localization';
7
+ declare type LoadSaveData = unknown;
8
+ export declare type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
7
9
  export default abstract class AbstractComponent {
8
10
  protected lastChangedTime: number;
9
11
  protected abstract contentBBox: Rect2;
10
12
  private zIndex;
11
13
  private static zIndexCounter;
12
14
  protected constructor();
15
+ private loadSaveData;
16
+ attachLoadSaveData(key: string, data: LoadSaveData): void;
17
+ getLoadSaveData(): LoadSaveDataTable;
13
18
  getZIndex(): number;
14
19
  getBBox(): Rect2;
15
20
  abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
@@ -18,3 +23,4 @@ export default abstract class AbstractComponent {
18
23
  transformBy(affineTransfm: Mat33): Command;
19
24
  abstract description(localizationTable: ImageComponentLocalization): string;
20
25
  }
26
+ export {};
@@ -1,9 +1,20 @@
1
1
  import EditorImage from '../EditorImage';
2
2
  export default class AbstractComponent {
3
3
  constructor() {
4
+ // Get and manage data attached by a loader.
5
+ this.loadSaveData = {};
4
6
  this.lastChangedTime = (new Date()).getTime();
5
7
  this.zIndex = AbstractComponent.zIndexCounter++;
6
8
  }
9
+ attachLoadSaveData(key, data) {
10
+ if (!this.loadSaveData[key]) {
11
+ this.loadSaveData[key] = [];
12
+ }
13
+ this.loadSaveData[key].push(data);
14
+ }
15
+ getLoadSaveData() {
16
+ return this.loadSaveData;
17
+ }
7
18
  getZIndex() {
8
19
  return this.zIndex;
9
20
  }
@@ -41,7 +41,7 @@ export default class Stroke extends AbstractComponent {
41
41
  canvas.drawPath(part);
42
42
  }
43
43
  }
44
- canvas.endObject();
44
+ canvas.endObject(this.getLoadSaveData());
45
45
  }
46
46
  // Grows the bounding box for a given stroke part based on that part's style.
47
47
  bboxForPart(origBBox, style) {
@@ -275,10 +275,12 @@ export default class Path {
275
275
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
276
276
  // and
277
277
  // https://www.w3.org/TR/SVG2/paths.html
278
+ var _a;
278
279
  // Remove linebreaks
279
280
  pathString = pathString.split('\n').join(' ');
280
281
  let lastPos = Vec2.zero;
281
282
  let firstPos = null;
283
+ let startPos = null;
282
284
  let isFirstCommand = true;
283
285
  const commands = [];
284
286
  const moveTo = (point) => {
@@ -317,15 +319,61 @@ export default class Path {
317
319
  endPoint,
318
320
  });
319
321
  };
322
+ const commandArgCounts = {
323
+ 'm': 1,
324
+ 'l': 1,
325
+ 'c': 3,
326
+ 'q': 2,
327
+ 'z': 0,
328
+ 'h': 1,
329
+ 'v': 1,
330
+ };
320
331
  // Each command: Command character followed by anything that isn't a command character
321
- const commandExp = /([MmZzLlHhVvCcSsQqTtAa])\s*([^a-zA-Z]*)/g;
332
+ const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
322
333
  let current;
323
334
  while ((current = commandExp.exec(pathString)) !== null) {
324
- const argParts = current[2].trim().split(/[^0-9.-]/).filter(part => part.length > 0);
325
- const numericArgs = argParts.map(arg => parseFloat(arg));
326
- const commandChar = current[1];
327
- const uppercaseCommand = commandChar !== commandChar.toLowerCase();
328
- const args = numericArgs.reduce((accumulator, current, index, parts) => {
335
+ const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(part => part.length > 0).reduce((accumualtor, current) => {
336
+ // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
337
+ // expressions. As such, we need an alternative.
338
+ // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
339
+ // we need special cases:
340
+ current = current.replace(/([^eE])[-]/g, '$1 -');
341
+ const parts = current.split(' -');
342
+ if (parts[0] !== '') {
343
+ accumualtor.push(parts[0]);
344
+ }
345
+ accumualtor.push(...parts.slice(1).map(part => `-${part}`));
346
+ return accumualtor;
347
+ }, []);
348
+ let numericArgs = argParts.map(arg => parseFloat(arg));
349
+ let commandChar = current[1].toLowerCase();
350
+ let uppercaseCommand = current[1] !== commandChar;
351
+ // Convert commands that don't take points into commands that do.
352
+ if (commandChar === 'v' || commandChar === 'h') {
353
+ numericArgs = numericArgs.reduce((accumulator, current) => {
354
+ if (commandChar === 'v') {
355
+ return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
356
+ }
357
+ else {
358
+ return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
359
+ }
360
+ }, []);
361
+ commandChar = 'l';
362
+ }
363
+ else if (commandChar === 'z') {
364
+ if (firstPos) {
365
+ numericArgs = [firstPos.x, firstPos.y];
366
+ firstPos = lastPos;
367
+ }
368
+ else {
369
+ continue;
370
+ }
371
+ // 'z' always acts like an uppercase lineTo(startPos)
372
+ uppercaseCommand = true;
373
+ commandChar = 'l';
374
+ }
375
+ const commandArgCount = (_a = commandArgCounts[commandChar]) !== null && _a !== void 0 ? _a : 0;
376
+ const allArgs = numericArgs.reduce((accumulator, current, index, parts) => {
329
377
  if (index % 2 !== 0) {
330
378
  const currentAsFloat = current;
331
379
  const prevAsFloat = parts[index - 1];
@@ -334,76 +382,59 @@ export default class Path {
334
382
  else {
335
383
  return accumulator;
336
384
  }
337
- }, []).map((coordinate) => {
385
+ }, []).map((coordinate, index) => {
338
386
  // Lowercase commands are relative, uppercase commands use absolute
339
387
  // positioning
388
+ let newPos;
340
389
  if (uppercaseCommand) {
341
- lastPos = coordinate;
342
- return coordinate;
390
+ newPos = coordinate;
343
391
  }
344
392
  else {
345
- lastPos = lastPos.plus(coordinate);
346
- return lastPos;
393
+ newPos = lastPos.plus(coordinate);
394
+ }
395
+ if ((index + 1) % commandArgCount === 0) {
396
+ lastPos = newPos;
347
397
  }
398
+ return newPos;
348
399
  });
349
- let expectedPointArgCount;
350
- switch (commandChar.toLowerCase()) {
351
- case 'm':
352
- expectedPointArgCount = 1;
353
- moveTo(args[0]);
354
- break;
355
- case 'l':
356
- expectedPointArgCount = 1;
357
- lineTo(args[0]);
358
- break;
359
- case 'z':
360
- expectedPointArgCount = 0;
361
- // firstPos can be null if the stroke data is just 'z'.
362
- if (firstPos) {
363
- lineTo(firstPos);
364
- }
365
- break;
366
- case 'c':
367
- expectedPointArgCount = 3;
368
- cubicBezierTo(args[0], args[1], args[2]);
369
- break;
370
- case 'q':
371
- expectedPointArgCount = 2;
372
- quadraticBeierTo(args[0], args[1]);
373
- break;
374
- // Horizontal line
375
- case 'h':
376
- expectedPointArgCount = 0;
377
- if (uppercaseCommand) {
378
- lineTo(Vec2.of(numericArgs[0], lastPos.y));
379
- }
380
- else {
381
- lineTo(lastPos.plus(Vec2.of(numericArgs[0], 0)));
382
- }
383
- break;
384
- // Vertical line
385
- case 'v':
386
- expectedPointArgCount = 0;
387
- if (uppercaseCommand) {
388
- lineTo(Vec2.of(lastPos.x, numericArgs[1]));
389
- }
390
- else {
391
- lineTo(lastPos.plus(Vec2.of(0, numericArgs[1])));
392
- }
393
- break;
394
- default:
395
- throw new Error(`Unknown path command ${commandChar}`);
400
+ if (allArgs.length % commandArgCount !== 0) {
401
+ throw new Error([
402
+ `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
403
+ `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
404
+ `Command: ${current[0]}`,
405
+ ].join('\n'));
396
406
  }
397
- if (args.length !== expectedPointArgCount) {
398
- throw new Error(`
399
- Incorrect number of arguments: got ${JSON.stringify(args)} with a length of ${args.length} ≠ ${expectedPointArgCount}.
400
- `.trim());
407
+ for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
408
+ const args = allArgs.slice(argPos, argPos + commandArgCount);
409
+ switch (commandChar.toLowerCase()) {
410
+ case 'm':
411
+ if (argPos === 0) {
412
+ moveTo(args[0]);
413
+ }
414
+ else {
415
+ lineTo(args[0]);
416
+ }
417
+ break;
418
+ case 'l':
419
+ lineTo(args[0]);
420
+ break;
421
+ case 'c':
422
+ cubicBezierTo(args[0], args[1], args[2]);
423
+ break;
424
+ case 'q':
425
+ quadraticBeierTo(args[0], args[1]);
426
+ break;
427
+ default:
428
+ throw new Error(`Unknown path command ${commandChar}`);
429
+ }
430
+ isFirstCommand = false;
401
431
  }
402
- if (args.length > 0) {
403
- firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = args[0]);
432
+ if (allArgs.length > 0) {
433
+ firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = allArgs[0]);
434
+ startPos !== null && startPos !== void 0 ? startPos : (startPos = firstPos);
435
+ lastPos = allArgs[allArgs.length - 1];
404
436
  }
405
- isFirstCommand = false;
406
437
  }
407
- return new Path(firstPos !== null && firstPos !== void 0 ? firstPos : Vec2.zero, commands);
438
+ return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
408
439
  }
409
440
  }
@@ -1,4 +1,5 @@
1
1
  import Color4 from '../../Color4';
2
+ import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
3
  import Mat33 from '../../geometry/Mat33';
3
4
  import { PathCommand } from '../../geometry/Path';
4
5
  import Rect2 from '../../geometry/Rect2';
@@ -37,7 +38,7 @@ export default abstract class AbstractRenderer {
37
38
  drawPath(path: RenderablePathSpec): void;
38
39
  drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
39
40
  startObject(_boundingBox: Rect2, _clip?: boolean): void;
40
- endObject(): void;
41
+ endObject(_loaderData?: LoadSaveDataTable): void;
41
42
  protected getNestingLevel(): number;
42
43
  abstract drawPoints(...points: Point2[]): void;
43
44
  canRenderFromWithoutDataLoss(_other: AbstractRenderer): boolean;
@@ -79,7 +79,7 @@ export default class AbstractRenderer {
79
79
  this.currentPaths = [];
80
80
  this.objectLevel++;
81
81
  }
82
- endObject() {
82
+ endObject(_loaderData) {
83
83
  // Render the paths all at once
84
84
  this.flushPath();
85
85
  this.currentPaths = null;
@@ -1,3 +1,4 @@
1
+ import { LoadSaveDataTable } from '../../components/AbstractComponent';
1
2
  import Rect2 from '../../geometry/Rect2';
2
3
  import { Point2, Vec2 } from '../../geometry/Vec2';
3
4
  import Viewport from '../../Viewport';
@@ -9,7 +10,7 @@ export default class SVGRenderer extends AbstractRenderer {
9
10
  private lastPathStyle;
10
11
  private lastPath;
11
12
  private lastPathStart;
12
- private mainGroup;
13
+ private objectElems;
13
14
  private overwrittenAttrs;
14
15
  constructor(elem: SVGSVGElement, viewport: Viewport);
15
16
  setRootSVGAttribute(name: string, value: string | null): void;
@@ -19,7 +20,7 @@ export default class SVGRenderer extends AbstractRenderer {
19
20
  protected endPath(style: RenderingStyle): void;
20
21
  private addPathToSVG;
21
22
  startObject(boundingBox: Rect2): void;
22
- endObject(): void;
23
+ endObject(loaderData?: LoadSaveDataTable): void;
23
24
  protected lineTo(point: Point2): void;
24
25
  protected moveTo(point: Point2): void;
25
26
  protected traceCubicBezierCurve(controlPoint1: Point2, controlPoint2: Point2, endPoint: Point2): void;
@@ -1,11 +1,13 @@
1
1
  import Path, { PathCommandType } from '../../geometry/Path';
2
2
  import { Vec2 } from '../../geometry/Vec2';
3
+ import { svgAttributesDataKey } from '../../SVGLoader';
3
4
  import AbstractRenderer from './AbstractRenderer';
4
5
  const svgNameSpace = 'http://www.w3.org/2000/svg';
5
6
  export default class SVGRenderer extends AbstractRenderer {
6
7
  constructor(elem, viewport) {
7
8
  super(viewport);
8
9
  this.elem = elem;
10
+ this.objectElems = null;
9
11
  this.overwrittenAttrs = {};
10
12
  this.clear();
11
13
  }
@@ -26,7 +28,6 @@ export default class SVGRenderer extends AbstractRenderer {
26
28
  return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
27
29
  }
28
30
  clear() {
29
- this.mainGroup = document.createElementNS(svgNameSpace, 'g');
30
31
  // Restore all alltributes
31
32
  for (const attrName in this.overwrittenAttrs) {
32
33
  const value = this.overwrittenAttrs[attrName];
@@ -38,8 +39,6 @@ export default class SVGRenderer extends AbstractRenderer {
38
39
  }
39
40
  }
40
41
  this.overwrittenAttrs = {};
41
- // Remove all children
42
- this.elem.replaceChildren(this.mainGroup);
43
42
  }
44
43
  beginPath(startPoint) {
45
44
  var _a;
@@ -72,6 +71,7 @@ export default class SVGRenderer extends AbstractRenderer {
72
71
  }
73
72
  // Push [this.fullPath] to the SVG
74
73
  addPathToSVG() {
74
+ var _a;
75
75
  if (!this.lastPathStyle || !this.lastPath) {
76
76
  return;
77
77
  }
@@ -83,7 +83,8 @@ export default class SVGRenderer extends AbstractRenderer {
83
83
  pathElem.setAttribute('stroke', style.stroke.color.toHexString());
84
84
  pathElem.setAttribute('stroke-width', style.stroke.width.toString());
85
85
  }
86
- this.mainGroup.appendChild(pathElem);
86
+ this.elem.appendChild(pathElem);
87
+ (_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(pathElem);
87
88
  }
88
89
  startObject(boundingBox) {
89
90
  super.startObject(boundingBox);
@@ -91,11 +92,24 @@ export default class SVGRenderer extends AbstractRenderer {
91
92
  this.lastPath = null;
92
93
  this.lastPathStart = null;
93
94
  this.lastPathStyle = null;
95
+ this.objectElems = [];
94
96
  }
95
- endObject() {
96
- super.endObject();
97
+ endObject(loaderData) {
98
+ var _a;
99
+ super.endObject(loaderData);
97
100
  // Don't extend paths across objects
98
101
  this.addPathToSVG();
102
+ if (loaderData) {
103
+ // Restore any attributes unsupported by the app.
104
+ for (const elem of (_a = this.objectElems) !== null && _a !== void 0 ? _a : []) {
105
+ const attrs = loaderData[svgAttributesDataKey];
106
+ if (attrs) {
107
+ for (const [attr, value] of attrs) {
108
+ elem.setAttribute(attr, value);
109
+ }
110
+ }
111
+ }
112
+ }
99
113
  }
100
114
  lineTo(point) {
101
115
  point = this.canvasToScreen(point);
@@ -137,7 +151,7 @@ export default class SVGRenderer extends AbstractRenderer {
137
151
  elem.setAttribute('cx', `${point.x}`);
138
152
  elem.setAttribute('cy', `${point.y}`);
139
153
  elem.setAttribute('r', '15');
140
- this.mainGroup.appendChild(elem);
154
+ this.elem.appendChild(elem);
141
155
  });
142
156
  }
143
157
  // Renders a **copy** of the given element.
@@ -1,5 +1,6 @@
1
1
  import Editor from '../Editor';
2
2
  import { ToolbarLocalization } from './localization';
3
+ import { ActionButtonIcon } from './types';
3
4
  export default class HTMLToolbar {
4
5
  private editor;
5
6
  private localizationTable;
@@ -7,7 +8,7 @@ export default class HTMLToolbar {
7
8
  private penTypes;
8
9
  constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
9
10
  setupColorPickers(): void;
10
- addActionButton(text: string, command: () => void, parent?: Element): HTMLButtonElement;
11
+ addActionButton(title: string | ActionButtonIcon, command: () => void, parent?: Element): HTMLButtonElement;
11
12
  private addUndoRedoButtons;
12
13
  addDefaultToolWidgets(): void;
13
14
  addDefaultActionButtons(): void;