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
@@ -371,6 +371,7 @@ export default class Path {
371
371
 
372
372
  let lastPos: Point2 = Vec2.zero;
373
373
  let firstPos: Point2|null = null;
374
+ let startPos: Point2|null = null;
374
375
  let isFirstCommand: boolean = true;
375
376
  const commands: PathCommand[] = [];
376
377
 
@@ -413,19 +414,67 @@ export default class Path {
413
414
  endPoint,
414
415
  });
415
416
  };
417
+ const commandArgCounts: Record<string, number> = {
418
+ 'm': 1,
419
+ 'l': 1,
420
+ 'c': 3,
421
+ 'q': 2,
422
+ 'z': 0,
423
+ 'h': 1,
424
+ 'v': 1,
425
+ };
416
426
 
417
427
  // Each command: Command character followed by anything that isn't a command character
418
- const commandExp = /([MmZzLlHhVvCcSsQqTtAa])\s*([^a-zA-Z]*)/g;
428
+ const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
419
429
  let current;
420
430
  while ((current = commandExp.exec(pathString)) !== null) {
421
- const argParts = current[2].trim().split(/[^0-9.-]/).filter(
431
+ const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(
422
432
  part => part.length > 0
423
- );
424
- const numericArgs = argParts.map(arg => parseFloat(arg));
433
+ ).reduce((accumualtor: string[], current: string): string[] => {
434
+ // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
435
+ // expressions. As such, we need an alternative.
436
+ // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
437
+ // we need special cases:
438
+ current = current.replace(/([^eE])[-]/g, '$1 -');
439
+ const parts = current.split(' -');
440
+ if (parts[0] !== '') {
441
+ accumualtor.push(parts[0]);
442
+ }
443
+ accumualtor.push(...parts.slice(1).map(part => `-${part}`));
444
+ return accumualtor;
445
+ }, []);
446
+
447
+ let numericArgs = argParts.map(arg => parseFloat(arg));
448
+
449
+ let commandChar = current[1].toLowerCase();
450
+ let uppercaseCommand = current[1] !== commandChar;
451
+
452
+ // Convert commands that don't take points into commands that do.
453
+ if (commandChar === 'v' || commandChar === 'h') {
454
+ numericArgs = numericArgs.reduce((accumulator: number[], current: number): number[] => {
455
+ if (commandChar === 'v') {
456
+ return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
457
+ } else {
458
+ return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
459
+ }
460
+ }, []);
461
+ commandChar = 'l';
462
+ } else if (commandChar === 'z') {
463
+ if (firstPos) {
464
+ numericArgs = [ firstPos.x, firstPos.y ];
465
+ firstPos = lastPos;
466
+ } else {
467
+ continue;
468
+ }
469
+
470
+ // 'z' always acts like an uppercase lineTo(startPos)
471
+ uppercaseCommand = true;
472
+ commandChar = 'l';
473
+ }
474
+
425
475
 
426
- const commandChar = current[1];
427
- const uppercaseCommand = commandChar !== commandChar.toLowerCase();
428
- const args = numericArgs.reduce((
476
+ const commandArgCount: number = commandArgCounts[commandChar] ?? 0;
477
+ const allArgs = numericArgs.reduce((
429
478
  accumulator: Point2[], current, index, parts
430
479
  ): Point2[] => {
431
480
  if (index % 2 !== 0) {
@@ -435,82 +484,65 @@ export default class Path {
435
484
  } else {
436
485
  return accumulator;
437
486
  }
438
- }, []).map((coordinate: Vec2): Point2 => {
487
+ }, []).map((coordinate, index): Point2 => {
439
488
  // Lowercase commands are relative, uppercase commands use absolute
440
489
  // positioning
490
+ let newPos;
441
491
  if (uppercaseCommand) {
442
- lastPos = coordinate;
443
- return coordinate;
492
+ newPos = coordinate;
444
493
  } else {
445
- lastPos = lastPos.plus(coordinate);
446
- return lastPos;
494
+ newPos = lastPos.plus(coordinate);
447
495
  }
448
- });
449
-
450
- let expectedPointArgCount;
451
496
 
452
- switch (commandChar.toLowerCase()) {
453
- case 'm':
454
- expectedPointArgCount = 1;
455
- moveTo(args[0]);
456
- break;
457
- case 'l':
458
- expectedPointArgCount = 1;
459
- lineTo(args[0]);
460
- break;
461
- case 'z':
462
- expectedPointArgCount = 0;
463
- // firstPos can be null if the stroke data is just 'z'.
464
- if (firstPos) {
465
- lineTo(firstPos);
497
+ if ((index + 1) % commandArgCount === 0) {
498
+ lastPos = newPos;
466
499
  }
467
- break;
468
- case 'c':
469
- expectedPointArgCount = 3;
470
- cubicBezierTo(args[0], args[1], args[2]);
471
- break;
472
- case 'q':
473
- expectedPointArgCount = 2;
474
- quadraticBeierTo(args[0], args[1]);
475
- break;
476
500
 
477
- // Horizontal line
478
- case 'h':
479
- expectedPointArgCount = 0;
480
-
481
- if (uppercaseCommand) {
482
- lineTo(Vec2.of(numericArgs[0], lastPos.y));
483
- } else {
484
- lineTo(lastPos.plus(Vec2.of(numericArgs[0], 0)));
485
- }
486
- break;
501
+ return newPos;
502
+ });
503
+
504
+ if (allArgs.length % commandArgCount !== 0) {
505
+ throw new Error([
506
+ `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
507
+ `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
508
+ `Command: ${current[0]}`,
509
+ ].join('\n'));
510
+ }
487
511
 
488
- // Vertical line
489
- case 'v':
490
- expectedPointArgCount = 0;
512
+ for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
513
+ const args = allArgs.slice(argPos, argPos + commandArgCount);
491
514
 
492
- if (uppercaseCommand) {
493
- lineTo(Vec2.of(lastPos.x, numericArgs[1]));
494
- } else {
495
- lineTo(lastPos.plus(Vec2.of(0, numericArgs[1])));
515
+ switch (commandChar.toLowerCase()) {
516
+ case 'm':
517
+ if (argPos === 0) {
518
+ moveTo(args[0]);
519
+ } else {
520
+ lineTo(args[0]);
521
+ }
522
+ break;
523
+ case 'l':
524
+ lineTo(args[0]);
525
+ break;
526
+ case 'c':
527
+ cubicBezierTo(args[0], args[1], args[2]);
528
+ break;
529
+ case 'q':
530
+ quadraticBeierTo(args[0], args[1]);
531
+ break;
532
+ default:
533
+ throw new Error(`Unknown path command ${commandChar}`);
496
534
  }
497
- break;
498
- default:
499
- throw new Error(`Unknown path command ${commandChar}`);
500
- }
501
535
 
502
- if (args.length !== expectedPointArgCount) {
503
- throw new Error(`
504
- Incorrect number of arguments: got ${JSON.stringify(args)} with a length of ${args.length} ≠ ${expectedPointArgCount}.
505
- `.trim());
536
+ isFirstCommand = false;
506
537
  }
507
538
 
508
- if (args.length > 0) {
509
- firstPos ??= args[0];
539
+ if (allArgs.length > 0) {
540
+ firstPos ??= allArgs[0];
541
+ startPos ??= firstPos;
542
+ lastPos = allArgs[allArgs.length - 1];
510
543
  }
511
- isFirstCommand = false;
512
544
  }
513
545
 
514
- return new Path(firstPos ?? Vec2.zero, commands);
546
+ return new Path(startPos ?? Vec2.zero, commands);
515
547
  }
516
548
  }
@@ -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 Path, { PathCommand, PathCommandType } from '../../geometry/Path';
4
5
  import Rect2 from '../../geometry/Rect2';
@@ -128,7 +129,7 @@ export default abstract class AbstractRenderer {
128
129
  this.objectLevel ++;
129
130
  }
130
131
 
131
- public endObject() {
132
+ public endObject(_loaderData?: LoadSaveDataTable) {
132
133
  // Render the paths all at once
133
134
  this.flushPath();
134
135
  this.currentPaths = null;
@@ -1,7 +1,9 @@
1
1
 
2
+ import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
3
  import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
3
4
  import Rect2 from '../../geometry/Rect2';
4
5
  import { Point2, Vec2 } from '../../geometry/Vec2';
6
+ import { svgAttributesDataKey, SVGLoaderUnknownAttribute } from '../../SVGLoader';
5
7
  import Viewport from '../../Viewport';
6
8
  import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
7
9
 
@@ -13,8 +15,8 @@ export default class SVGRenderer extends AbstractRenderer {
13
15
  private lastPathStyle: RenderingStyle|null;
14
16
  private lastPath: PathCommand[]|null;
15
17
  private lastPathStart: Point2|null;
18
+ private objectElems: SVGElement[]|null = null;
16
19
 
17
- private mainGroup: SVGGElement;
18
20
  private overwrittenAttrs: Record<string, string|null> = {};
19
21
 
20
22
  public constructor(private elem: SVGSVGElement, viewport: Viewport) {
@@ -41,8 +43,6 @@ export default class SVGRenderer extends AbstractRenderer {
41
43
  }
42
44
 
43
45
  public clear() {
44
- this.mainGroup = document.createElementNS(svgNameSpace, 'g');
45
-
46
46
  // Restore all alltributes
47
47
  for (const attrName in this.overwrittenAttrs) {
48
48
  const value = this.overwrittenAttrs[attrName];
@@ -54,9 +54,6 @@ export default class SVGRenderer extends AbstractRenderer {
54
54
  }
55
55
  }
56
56
  this.overwrittenAttrs = {};
57
-
58
- // Remove all children
59
- this.elem.replaceChildren(this.mainGroup);
60
57
  }
61
58
 
62
59
  protected beginPath(startPoint: Point2) {
@@ -106,7 +103,8 @@ export default class SVGRenderer extends AbstractRenderer {
106
103
  pathElem.setAttribute('stroke-width', style.stroke.width.toString());
107
104
  }
108
105
 
109
- this.mainGroup.appendChild(pathElem);
106
+ this.elem.appendChild(pathElem);
107
+ this.objectElems?.push(pathElem);
110
108
  }
111
109
 
112
110
  public startObject(boundingBox: Rect2) {
@@ -116,13 +114,27 @@ export default class SVGRenderer extends AbstractRenderer {
116
114
  this.lastPath = null;
117
115
  this.lastPathStart = null;
118
116
  this.lastPathStyle = null;
117
+ this.objectElems = [];
119
118
  }
120
119
 
121
- public endObject() {
122
- super.endObject();
120
+ public endObject(loaderData?: LoadSaveDataTable) {
121
+ super.endObject(loaderData);
123
122
 
124
123
  // Don't extend paths across objects
125
124
  this.addPathToSVG();
125
+
126
+ if (loaderData) {
127
+ // Restore any attributes unsupported by the app.
128
+ for (const elem of this.objectElems ?? []) {
129
+ const attrs = loaderData[svgAttributesDataKey] as SVGLoaderUnknownAttribute[]|undefined;
130
+
131
+ if (attrs) {
132
+ for (const [ attr, value ] of attrs) {
133
+ elem.setAttribute(attr, value);
134
+ }
135
+ }
136
+ }
137
+ }
126
138
  }
127
139
 
128
140
  protected lineTo(point: Point2) {
@@ -175,7 +187,7 @@ export default class SVGRenderer extends AbstractRenderer {
175
187
  elem.setAttribute('cx', `${point.x}`);
176
188
  elem.setAttribute('cy', `${point.y}`);
177
189
  elem.setAttribute('r', '15');
178
- this.mainGroup.appendChild(elem);
190
+ this.elem.appendChild(elem);
179
191
  });
180
192
  }
181
193