js-draw 0.1.1 → 0.1.4

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 (86) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +21 -12
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +2 -1
  5. package/dist/src/Editor.js +24 -6
  6. package/dist/src/EditorImage.js +3 -0
  7. package/dist/src/Pointer.d.ts +3 -2
  8. package/dist/src/Pointer.js +12 -3
  9. package/dist/src/SVGLoader.d.ts +11 -0
  10. package/dist/src/SVGLoader.js +113 -4
  11. package/dist/src/Viewport.d.ts +1 -1
  12. package/dist/src/Viewport.js +12 -2
  13. package/dist/src/components/AbstractComponent.d.ts +6 -0
  14. package/dist/src/components/AbstractComponent.js +11 -0
  15. package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
  16. package/dist/src/components/Stroke.js +1 -1
  17. package/dist/src/components/Text.d.ts +30 -0
  18. package/dist/src/components/Text.js +111 -0
  19. package/dist/src/components/localization.d.ts +1 -0
  20. package/dist/src/components/localization.js +1 -0
  21. package/dist/src/geometry/Mat33.d.ts +1 -0
  22. package/dist/src/geometry/Mat33.js +30 -0
  23. package/dist/src/geometry/Path.js +105 -67
  24. package/dist/src/geometry/Rect2.d.ts +2 -0
  25. package/dist/src/geometry/Rect2.js +6 -0
  26. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
  27. package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
  28. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
  29. package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
  30. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
  31. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  32. package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
  33. package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
  34. package/dist/src/testing/loadExpectExtensions.js +1 -4
  35. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  36. package/dist/src/toolbar/HTMLToolbar.js +242 -154
  37. package/dist/src/toolbar/icons.d.ts +12 -0
  38. package/dist/src/toolbar/icons.js +198 -0
  39. package/dist/src/toolbar/localization.d.ts +5 -1
  40. package/dist/src/toolbar/localization.js +5 -1
  41. package/dist/src/toolbar/types.d.ts +4 -0
  42. package/dist/src/tools/PanZoom.d.ts +9 -6
  43. package/dist/src/tools/PanZoom.js +30 -21
  44. package/dist/src/tools/Pen.js +8 -3
  45. package/dist/src/tools/SelectionTool.js +1 -1
  46. package/dist/src/tools/TextTool.d.ts +30 -0
  47. package/dist/src/tools/TextTool.js +173 -0
  48. package/dist/src/tools/ToolController.d.ts +5 -5
  49. package/dist/src/tools/ToolController.js +10 -9
  50. package/dist/src/tools/localization.d.ts +3 -0
  51. package/dist/src/tools/localization.js +3 -0
  52. package/dist-test/test-dist-bundle.html +8 -1
  53. package/package.json +1 -1
  54. package/src/Editor.css +2 -0
  55. package/src/Editor.ts +26 -7
  56. package/src/EditorImage.ts +4 -0
  57. package/src/Pointer.ts +13 -4
  58. package/src/SVGLoader.ts +146 -5
  59. package/src/Viewport.ts +15 -3
  60. package/src/components/AbstractComponent.ts +16 -1
  61. package/src/components/SVGGlobalAttributesObject.ts +0 -1
  62. package/src/components/Stroke.ts +1 -1
  63. package/src/components/Text.ts +140 -0
  64. package/src/components/localization.ts +2 -0
  65. package/src/geometry/Mat33.test.ts +44 -0
  66. package/src/geometry/Mat33.ts +41 -0
  67. package/src/geometry/Path.fromString.test.ts +94 -4
  68. package/src/geometry/Path.toString.test.ts +7 -3
  69. package/src/geometry/Path.ts +110 -68
  70. package/src/geometry/Rect2.ts +8 -0
  71. package/src/rendering/renderers/AbstractRenderer.ts +18 -1
  72. package/src/rendering/renderers/CanvasRenderer.ts +34 -10
  73. package/src/rendering/renderers/DummyRenderer.ts +8 -0
  74. package/src/rendering/renderers/SVGRenderer.ts +57 -10
  75. package/src/testing/loadExpectExtensions.ts +1 -4
  76. package/src/toolbar/HTMLToolbar.ts +294 -170
  77. package/src/toolbar/icons.ts +227 -0
  78. package/src/toolbar/localization.ts +11 -2
  79. package/src/toolbar/toolbar.css +27 -11
  80. package/src/toolbar/types.ts +5 -0
  81. package/src/tools/PanZoom.ts +37 -27
  82. package/src/tools/Pen.ts +7 -3
  83. package/src/tools/SelectionTool.ts +1 -1
  84. package/src/tools/TextTool.ts +225 -0
  85. package/src/tools/ToolController.ts +7 -5
  86. package/src/tools/localization.ts +7 -0
@@ -186,5 +186,35 @@ export default class Mat33 {
186
186
  // Translate such that [center] goes to (0, 0)
187
187
  return result.rightMul(Mat33.translation(center.times(-1)));
188
188
  }
189
+ // Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
190
+ static fromCSSMatrix(cssString) {
191
+ if (cssString === '' || cssString === 'none') {
192
+ return Mat33.identity;
193
+ }
194
+ const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
195
+ const numberSepExp = '[, \\t\\n]';
196
+ const regExpSource = `^\\s*matrix\\s*\\(${[
197
+ // According to MDN, matrix(a,b,c,d,e,f) has form:
198
+ // ⎡ a c e ⎤
199
+ // ⎢ b d f ⎥
200
+ // ⎣ 0 0 1 ⎦
201
+ numberExp, numberExp, numberExp,
202
+ numberExp, numberExp, numberExp, // b, d, f
203
+ ].join(`${numberSepExp}+`)}${numberSepExp}*\\)\\s*$`;
204
+ const matrixExp = new RegExp(regExpSource, 'i');
205
+ const match = matrixExp.exec(cssString);
206
+ if (!match) {
207
+ throw new Error(`Unsupported transformation: ${cssString}`);
208
+ }
209
+ const matrixData = match.slice(1).map(entry => parseFloat(entry));
210
+ const a = matrixData[0];
211
+ const b = matrixData[1];
212
+ const c = matrixData[2];
213
+ const d = matrixData[3];
214
+ const e = matrixData[4];
215
+ const f = matrixData[5];
216
+ const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
217
+ return transform;
218
+ }
189
219
  }
190
220
  Mat33.identity = new Mat33(1, 0, 0, 0, 1, 0, 0, 0, 1);
@@ -220,6 +220,7 @@ export default class Path {
220
220
  const lastDigit = parseInt(text.charAt(text.length - 1), 10);
221
221
  const postDecimal = parseInt(roundingDownMatch[3], 10);
222
222
  const preDecimal = parseInt(roundingDownMatch[2], 10);
223
+ const origPostDecimalString = roundingDownMatch[3];
223
224
  let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
224
225
  let carry = 0;
225
226
  if (newPostDecimal.length > postDecimal.toString().length) {
@@ -227,11 +228,17 @@ export default class Path {
227
228
  newPostDecimal = newPostDecimal.substring(1);
228
229
  carry = 1;
229
230
  }
231
+ // parseInt(...).toString() removes leading zeroes. Add them back.
232
+ while (newPostDecimal.length < origPostDecimalString.length) {
233
+ newPostDecimal = carry.toString(10) + newPostDecimal;
234
+ carry = 0;
235
+ }
230
236
  text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
231
237
  }
232
238
  text = text.replace(fixRoundingUpExp, '$1');
233
239
  // Remove trailing zeroes
234
- text = text.replace(/([.][^0]*)0+$/, '$1');
240
+ text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
241
+ text = text.replace(/[.]0+$/, '.');
235
242
  // Remove trailing period
236
243
  return text.replace(/[.]$/, '');
237
244
  };
@@ -275,10 +282,12 @@ export default class Path {
275
282
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
276
283
  // and
277
284
  // https://www.w3.org/TR/SVG2/paths.html
285
+ var _a;
278
286
  // Remove linebreaks
279
287
  pathString = pathString.split('\n').join(' ');
280
288
  let lastPos = Vec2.zero;
281
289
  let firstPos = null;
290
+ let startPos = null;
282
291
  let isFirstCommand = true;
283
292
  const commands = [];
284
293
  const moveTo = (point) => {
@@ -317,15 +326,61 @@ export default class Path {
317
326
  endPoint,
318
327
  });
319
328
  };
329
+ const commandArgCounts = {
330
+ 'm': 1,
331
+ 'l': 1,
332
+ 'c': 3,
333
+ 'q': 2,
334
+ 'z': 0,
335
+ 'h': 1,
336
+ 'v': 1,
337
+ };
320
338
  // Each command: Command character followed by anything that isn't a command character
321
- const commandExp = /([MmZzLlHhVvCcSsQqTtAa])\s*([^a-zA-Z]*)/g;
339
+ const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
322
340
  let current;
323
341
  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) => {
342
+ const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(part => part.length > 0).reduce((accumualtor, current) => {
343
+ // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
344
+ // expressions. As such, we need an alternative.
345
+ // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
346
+ // we need special cases:
347
+ current = current.replace(/([^eE])[-]/g, '$1 -');
348
+ const parts = current.split(' -');
349
+ if (parts[0] !== '') {
350
+ accumualtor.push(parts[0]);
351
+ }
352
+ accumualtor.push(...parts.slice(1).map(part => `-${part}`));
353
+ return accumualtor;
354
+ }, []);
355
+ let numericArgs = argParts.map(arg => parseFloat(arg));
356
+ let commandChar = current[1].toLowerCase();
357
+ let uppercaseCommand = current[1] !== commandChar;
358
+ // Convert commands that don't take points into commands that do.
359
+ if (commandChar === 'v' || commandChar === 'h') {
360
+ numericArgs = numericArgs.reduce((accumulator, current) => {
361
+ if (commandChar === 'v') {
362
+ return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
363
+ }
364
+ else {
365
+ return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
366
+ }
367
+ }, []);
368
+ commandChar = 'l';
369
+ }
370
+ else if (commandChar === 'z') {
371
+ if (firstPos) {
372
+ numericArgs = [firstPos.x, firstPos.y];
373
+ firstPos = lastPos;
374
+ }
375
+ else {
376
+ continue;
377
+ }
378
+ // 'z' always acts like an uppercase lineTo(startPos)
379
+ uppercaseCommand = true;
380
+ commandChar = 'l';
381
+ }
382
+ const commandArgCount = (_a = commandArgCounts[commandChar]) !== null && _a !== void 0 ? _a : 0;
383
+ const allArgs = numericArgs.reduce((accumulator, current, index, parts) => {
329
384
  if (index % 2 !== 0) {
330
385
  const currentAsFloat = current;
331
386
  const prevAsFloat = parts[index - 1];
@@ -334,76 +389,59 @@ export default class Path {
334
389
  else {
335
390
  return accumulator;
336
391
  }
337
- }, []).map((coordinate) => {
392
+ }, []).map((coordinate, index) => {
338
393
  // Lowercase commands are relative, uppercase commands use absolute
339
394
  // positioning
395
+ let newPos;
340
396
  if (uppercaseCommand) {
341
- lastPos = coordinate;
342
- return coordinate;
397
+ newPos = coordinate;
343
398
  }
344
399
  else {
345
- lastPos = lastPos.plus(coordinate);
346
- return lastPos;
400
+ newPos = lastPos.plus(coordinate);
401
+ }
402
+ if ((index + 1) % commandArgCount === 0) {
403
+ lastPos = newPos;
347
404
  }
405
+ return newPos;
348
406
  });
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}`);
407
+ if (allArgs.length % commandArgCount !== 0) {
408
+ throw new Error([
409
+ `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
410
+ `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
411
+ `Command: ${current[0]}`,
412
+ ].join('\n'));
396
413
  }
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());
414
+ for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
415
+ const args = allArgs.slice(argPos, argPos + commandArgCount);
416
+ switch (commandChar.toLowerCase()) {
417
+ case 'm':
418
+ if (argPos === 0) {
419
+ moveTo(args[0]);
420
+ }
421
+ else {
422
+ lineTo(args[0]);
423
+ }
424
+ break;
425
+ case 'l':
426
+ lineTo(args[0]);
427
+ break;
428
+ case 'c':
429
+ cubicBezierTo(args[0], args[1], args[2]);
430
+ break;
431
+ case 'q':
432
+ quadraticBeierTo(args[0], args[1]);
433
+ break;
434
+ default:
435
+ throw new Error(`Unknown path command ${commandChar}`);
436
+ }
437
+ isFirstCommand = false;
401
438
  }
402
- if (args.length > 0) {
403
- firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = args[0]);
439
+ if (allArgs.length > 0) {
440
+ firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = allArgs[0]);
441
+ startPos !== null && startPos !== void 0 ? startPos : (startPos = firstPos);
442
+ lastPos = allArgs[allArgs.length - 1];
404
443
  }
405
- isFirstCommand = false;
406
444
  }
407
- return new Path(firstPos !== null && firstPos !== void 0 ? firstPos : Vec2.zero, commands);
445
+ return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
408
446
  }
409
447
  }
@@ -34,6 +34,8 @@ export default class Rect2 {
34
34
  get maxDimension(): number;
35
35
  get topRight(): import("./Vec3").default;
36
36
  get bottomLeft(): import("./Vec3").default;
37
+ get width(): number;
38
+ get height(): number;
37
39
  getEdges(): LineSegment2[];
38
40
  transformedBoundingBox(affineTransform: Mat33): Rect2;
39
41
  /** @return true iff this is equal to [other] ± fuzz */
@@ -126,6 +126,12 @@ export default class Rect2 {
126
126
  get bottomLeft() {
127
127
  return this.topLeft.plus(Vec2.of(0, this.h));
128
128
  }
129
+ get width() {
130
+ return this.w;
131
+ }
132
+ get height() {
133
+ return this.h;
134
+ }
129
135
  // Returns edges in the order
130
136
  // [ rightEdge, topEdge, leftEdge, bottomEdge ]
131
137
  getEdges() {
@@ -1,4 +1,6 @@
1
1
  import Color4 from '../../Color4';
2
+ import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
+ import { TextStyle } from '../../components/Text';
2
4
  import Mat33 from '../../geometry/Mat33';
3
5
  import { PathCommand } from '../../geometry/Path';
4
6
  import Rect2 from '../../geometry/Rect2';
@@ -19,6 +21,7 @@ export interface RenderablePathSpec {
19
21
  export default abstract class AbstractRenderer {
20
22
  private viewport;
21
23
  private selfTransform;
24
+ private transformStack;
22
25
  protected constructor(viewport: Viewport);
23
26
  protected getViewport(): Viewport;
24
27
  abstract displaySize(): Vec2;
@@ -29,6 +32,7 @@ export default abstract class AbstractRenderer {
29
32
  protected abstract moveTo(point: Point2): void;
30
33
  protected abstract traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
31
34
  protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
35
+ abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
32
36
  abstract isTooSmallToRender(rect: Rect2): boolean;
33
37
  setDraftMode(_draftMode: boolean): void;
34
38
  protected objectLevel: number;
@@ -37,12 +41,14 @@ export default abstract class AbstractRenderer {
37
41
  drawPath(path: RenderablePathSpec): void;
38
42
  drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
39
43
  startObject(_boundingBox: Rect2, _clip?: boolean): void;
40
- endObject(): void;
44
+ endObject(_loaderData?: LoadSaveDataTable): void;
41
45
  protected getNestingLevel(): number;
42
46
  abstract drawPoints(...points: Point2[]): void;
43
47
  canRenderFromWithoutDataLoss(_other: AbstractRenderer): boolean;
44
48
  renderFromOtherOfSameType(_renderTo: Mat33, other: AbstractRenderer): void;
45
49
  setTransform(transform: Mat33 | null): void;
50
+ pushTransform(transform: Mat33): void;
51
+ popTransform(): void;
46
52
  getCanvasToScreenTransform(): Mat33;
47
53
  canvasToScreen(vec: Vec2): Vec2;
48
54
  getSizeOfCanvasPixelOnScreen(): number;
@@ -11,6 +11,7 @@ export default class AbstractRenderer {
11
11
  this.viewport = viewport;
12
12
  // If null, this' transformation is linked to the Viewport
13
13
  this.selfTransform = null;
14
+ this.transformStack = [];
14
15
  this.objectLevel = 0;
15
16
  this.currentPaths = null;
16
17
  }
@@ -79,7 +80,7 @@ export default class AbstractRenderer {
79
80
  this.currentPaths = [];
80
81
  this.objectLevel++;
81
82
  }
82
- endObject() {
83
+ endObject(_loaderData) {
83
84
  // Render the paths all at once
84
85
  this.flushPath();
85
86
  this.currentPaths = null;
@@ -104,6 +105,17 @@ export default class AbstractRenderer {
104
105
  setTransform(transform) {
105
106
  this.selfTransform = transform;
106
107
  }
108
+ pushTransform(transform) {
109
+ this.transformStack.push(this.selfTransform);
110
+ this.setTransform(this.getCanvasToScreenTransform().rightMul(transform));
111
+ }
112
+ popTransform() {
113
+ var _a;
114
+ if (this.transformStack.length === 0) {
115
+ throw new Error('Unable to pop more transforms than have been pushed!');
116
+ }
117
+ this.setTransform((_a = this.transformStack.pop()) !== null && _a !== void 0 ? _a : null);
118
+ }
107
119
  // Get the matrix that transforms a vector on the canvas to a vector on this'
108
120
  // rendering target.
109
121
  getCanvasToScreenTransform() {
@@ -1,3 +1,4 @@
1
+ import { TextStyle } from '../../components/Text';
1
2
  import Mat33 from '../../geometry/Mat33';
2
3
  import Rect2 from '../../geometry/Rect2';
3
4
  import { Point2, Vec2 } from '../../geometry/Vec2';
@@ -12,6 +13,7 @@ export default class CanvasRenderer extends AbstractRenderer {
12
13
  private minRenderSizeAnyDimen;
13
14
  private minRenderSizeBothDimens;
14
15
  constructor(ctx: CanvasRenderingContext2D, viewport: Viewport);
16
+ private transformBy;
15
17
  canRenderFromWithoutDataLoss(other: AbstractRenderer): boolean;
16
18
  renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void;
17
19
  setDraftMode(draftMode: boolean): void;
@@ -24,6 +26,7 @@ export default class CanvasRenderer extends AbstractRenderer {
24
26
  protected traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
25
27
  protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
26
28
  drawPath(path: RenderablePathSpec): void;
29
+ drawText(text: string, transform: Mat33, style: TextStyle): void;
27
30
  private clipLevels;
28
31
  startObject(boundingBox: Rect2, clip: boolean): void;
29
32
  endObject(): void;
@@ -1,4 +1,5 @@
1
1
  import Color4 from '../../Color4';
2
+ import Text from '../../components/Text';
2
3
  import { Vec2 } from '../../geometry/Vec2';
3
4
  import AbstractRenderer from './AbstractRenderer';
4
5
  export default class CanvasRenderer extends AbstractRenderer {
@@ -10,6 +11,16 @@ export default class CanvasRenderer extends AbstractRenderer {
10
11
  this.clipLevels = [];
11
12
  this.setDraftMode(false);
12
13
  }
14
+ transformBy(transformBy) {
15
+ // From MDN, transform(a,b,c,d,e,f)
16
+ // takes input such that
17
+ // ⎡ a c e ⎤
18
+ // ⎢ b d f ⎥ transforms content drawn to [ctx].
19
+ // ⎣ 0 0 1 ⎦
20
+ this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
21
+ transformBy.a2, transformBy.b2, // c, d
22
+ transformBy.a3, transformBy.b3);
23
+ }
13
24
  canRenderFromWithoutDataLoss(other) {
14
25
  return other instanceof CanvasRenderer;
15
26
  }
@@ -19,14 +30,7 @@ export default class CanvasRenderer extends AbstractRenderer {
19
30
  }
20
31
  transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
21
32
  this.ctx.save();
22
- // From MDN, transform(a,b,c,d,e,f)
23
- // takes input such that
24
- // ⎡ a c e ⎤
25
- // ⎢ b d f ⎥ transforms content drawn to [ctx].
26
- // ⎣ 0 0 1 ⎦
27
- this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
28
- transformBy.a2, transformBy.b2, // c, d
29
- transformBy.a3, transformBy.b3);
33
+ this.transformBy(transformBy);
30
34
  this.ctx.drawImage(other.ctx.canvas, 0, 0);
31
35
  this.ctx.restore();
32
36
  }
@@ -105,6 +109,22 @@ export default class CanvasRenderer extends AbstractRenderer {
105
109
  }
106
110
  super.drawPath(path);
107
111
  }
112
+ drawText(text, transform, style) {
113
+ this.ctx.save();
114
+ transform = this.getCanvasToScreenTransform().rightMul(transform);
115
+ this.transformBy(transform);
116
+ Text.applyTextStyles(this.ctx, style);
117
+ if (style.renderingStyle.fill.a !== 0) {
118
+ this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
119
+ this.ctx.fillText(text, 0, 0);
120
+ }
121
+ if (style.renderingStyle.stroke) {
122
+ this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
123
+ this.ctx.lineWidth = style.renderingStyle.stroke.width;
124
+ this.ctx.strokeText(text, 0, 0);
125
+ }
126
+ this.ctx.restore();
127
+ }
108
128
  startObject(boundingBox, clip) {
109
129
  if (this.isTooSmallToRender(boundingBox)) {
110
130
  this.ignoreObjectsAboveLevel = this.getNestingLevel();
@@ -1,3 +1,4 @@
1
+ import { TextStyle } from '../../components/Text';
1
2
  import Mat33 from '../../geometry/Mat33';
2
3
  import Rect2 from '../../geometry/Rect2';
3
4
  import { Point2, Vec2 } from '../../geometry/Vec2';
@@ -10,6 +11,7 @@ export default class DummyRenderer extends AbstractRenderer {
10
11
  lastFillStyle: RenderingStyle | null;
11
12
  lastPoint: Point2 | null;
12
13
  objectNestingLevel: number;
14
+ lastText: string | null;
13
15
  pointBuffer: Point2[];
14
16
  constructor(viewport: Viewport);
15
17
  displaySize(): Vec2;
@@ -21,6 +23,7 @@ export default class DummyRenderer extends AbstractRenderer {
21
23
  protected traceCubicBezierCurve(p1: Vec3, p2: Vec3, p3: Vec3): void;
22
24
  protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
23
25
  drawPoints(..._points: Vec3[]): void;
26
+ drawText(text: string, _transform: Mat33, _style: TextStyle): void;
24
27
  startObject(boundingBox: Rect2, _clip: boolean): void;
25
28
  endObject(): void;
26
29
  isTooSmallToRender(_rect: Rect2): boolean;
@@ -10,6 +10,7 @@ export default class DummyRenderer extends AbstractRenderer {
10
10
  this.lastFillStyle = null;
11
11
  this.lastPoint = null;
12
12
  this.objectNestingLevel = 0;
13
+ this.lastText = null;
13
14
  // List of points drawn since the last clear.
14
15
  this.pointBuffer = [];
15
16
  }
@@ -28,6 +29,7 @@ export default class DummyRenderer extends AbstractRenderer {
28
29
  this.clearedCount++;
29
30
  this.renderedPathCount = 0;
30
31
  this.pointBuffer = [];
32
+ this.lastText = null;
31
33
  // Ensure all objects finished rendering
32
34
  if (this.objectNestingLevel > 0) {
33
35
  throw new Error(`Within an object while clearing! Nesting level: ${this.objectNestingLevel}`);
@@ -68,6 +70,9 @@ export default class DummyRenderer extends AbstractRenderer {
68
70
  // drawPoints is intended for debugging.
69
71
  // As such, it is unlikely to be the target of automated tests.
70
72
  }
73
+ drawText(text, _transform, _style) {
74
+ this.lastText = text;
75
+ }
71
76
  startObject(boundingBox, _clip) {
72
77
  super.startObject(boundingBox);
73
78
  this.objectNestingLevel += 1;
@@ -1,3 +1,6 @@
1
+ import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
+ import { TextStyle } from '../../components/Text';
3
+ import Mat33 from '../../geometry/Mat33';
1
4
  import Rect2 from '../../geometry/Rect2';
2
5
  import { Point2, Vec2 } from '../../geometry/Vec2';
3
6
  import Viewport from '../../Viewport';
@@ -9,7 +12,7 @@ export default class SVGRenderer extends AbstractRenderer {
9
12
  private lastPathStyle;
10
13
  private lastPath;
11
14
  private lastPathStart;
12
- private mainGroup;
15
+ private objectElems;
13
16
  private overwrittenAttrs;
14
17
  constructor(elem: SVGSVGElement, viewport: Viewport);
15
18
  setRootSVGAttribute(name: string, value: string | null): void;
@@ -18,8 +21,9 @@ export default class SVGRenderer extends AbstractRenderer {
18
21
  protected beginPath(startPoint: Point2): void;
19
22
  protected endPath(style: RenderingStyle): void;
20
23
  private addPathToSVG;
24
+ drawText(text: string, transform: Mat33, style: TextStyle): void;
21
25
  startObject(boundingBox: Rect2): void;
22
- endObject(): void;
26
+ endObject(loaderData?: LoadSaveDataTable): void;
23
27
  protected lineTo(point: Point2): void;
24
28
  protected moveTo(point: Point2): void;
25
29
  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, svgStyleAttributesDataKey } 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,31 @@ 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);
88
+ }
89
+ drawText(text, transform, style) {
90
+ var _a, _b, _c;
91
+ transform = this.getCanvasToScreenTransform().rightMul(transform);
92
+ const textElem = document.createElementNS(svgNameSpace, 'text');
93
+ textElem.appendChild(document.createTextNode(text));
94
+ textElem.style.transform = `matrix(
95
+ ${transform.a1}, ${transform.b1},
96
+ ${transform.a2}, ${transform.b2},
97
+ ${transform.a3}, ${transform.b3}
98
+ )`;
99
+ textElem.style.fontFamily = style.fontFamily;
100
+ textElem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
101
+ textElem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
102
+ textElem.style.fontSize = style.size + 'px';
103
+ textElem.style.fill = style.renderingStyle.fill.toHexString();
104
+ if (style.renderingStyle.stroke) {
105
+ const strokeStyle = style.renderingStyle.stroke;
106
+ textElem.style.stroke = strokeStyle.color.toHexString();
107
+ textElem.style.strokeWidth = strokeStyle.width + 'px';
108
+ }
109
+ this.elem.appendChild(textElem);
110
+ (_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
87
111
  }
88
112
  startObject(boundingBox) {
89
113
  super.startObject(boundingBox);
@@ -91,11 +115,30 @@ export default class SVGRenderer extends AbstractRenderer {
91
115
  this.lastPath = null;
92
116
  this.lastPathStart = null;
93
117
  this.lastPathStyle = null;
118
+ this.objectElems = [];
94
119
  }
95
- endObject() {
96
- super.endObject();
120
+ endObject(loaderData) {
121
+ var _a;
122
+ super.endObject(loaderData);
97
123
  // Don't extend paths across objects
98
124
  this.addPathToSVG();
125
+ if (loaderData) {
126
+ // Restore any attributes unsupported by the app.
127
+ for (const elem of (_a = this.objectElems) !== null && _a !== void 0 ? _a : []) {
128
+ const attrs = loaderData[svgAttributesDataKey];
129
+ const styleAttrs = loaderData[svgStyleAttributesDataKey];
130
+ if (attrs) {
131
+ for (const [attr, value] of attrs) {
132
+ elem.setAttribute(attr, value);
133
+ }
134
+ }
135
+ if (styleAttrs) {
136
+ for (const attr of styleAttrs) {
137
+ elem.style.setProperty(attr.key, attr.value, attr.priority);
138
+ }
139
+ }
140
+ }
141
+ }
99
142
  }
100
143
  lineTo(point) {
101
144
  point = this.canvasToScreen(point);
@@ -137,7 +180,7 @@ export default class SVGRenderer extends AbstractRenderer {
137
180
  elem.setAttribute('cx', `${point.x}`);
138
181
  elem.setAttribute('cy', `${point.y}`);
139
182
  elem.setAttribute('r', '15');
140
- this.mainGroup.appendChild(elem);
183
+ this.elem.appendChild(elem);
141
184
  });
142
185
  }
143
186
  // Renders a **copy** of the given element.
@@ -15,10 +15,7 @@ export const loadExpectExtensions = () => {
15
15
  return {
16
16
  pass,
17
17
  message: () => {
18
- if (pass) {
19
- return `Expected ${expected} not to .eq ${actual}. Options(${eqArgs})`;
20
- }
21
- return `Expected ${expected} to .eq ${actual}. Options(${eqArgs})`;
18
+ return `Expected ${pass ? '!' : ''}(${actual}).eq(${expected}). Options(${eqArgs})`;
22
19
  },
23
20
  };
24
21
  },
@@ -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;