js-draw 1.27.2 → 1.28.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 (112) hide show
  1. package/README.md +1 -1
  2. package/dist/Editor.css +1 -1
  3. package/dist/bundle.js +28 -28
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +7 -2
  6. package/dist/cjs/Editor.js +11 -5
  7. package/dist/cjs/SVGLoader/SVGLoader.d.ts +21 -0
  8. package/dist/cjs/SVGLoader/SVGLoader.js +74 -47
  9. package/dist/cjs/SVGLoader/SVGLoader.plugins.test.d.ts +1 -0
  10. package/dist/cjs/Viewport.js +2 -32
  11. package/dist/cjs/commands/Duplicate.d.ts +7 -4
  12. package/dist/cjs/commands/Duplicate.js +48 -7
  13. package/dist/cjs/commands/Duplicate.test.d.ts +1 -0
  14. package/dist/cjs/commands/Erase.d.ts +1 -1
  15. package/dist/cjs/commands/Erase.js +2 -2
  16. package/dist/cjs/commands/localization.d.ts +2 -2
  17. package/dist/cjs/commands/localization.js +2 -2
  18. package/dist/cjs/components/AbstractComponent.d.ts +7 -0
  19. package/dist/cjs/components/AbstractComponent.js +16 -2
  20. package/dist/cjs/components/Stroke.d.ts +21 -1
  21. package/dist/cjs/components/Stroke.js +29 -0
  22. package/dist/cjs/components/TextComponent.d.ts +2 -2
  23. package/dist/cjs/components/TextComponent.js +2 -2
  24. package/dist/cjs/image/EditorImage.d.ts +17 -9
  25. package/dist/cjs/image/EditorImage.js +33 -17
  26. package/dist/cjs/lib.d.ts +1 -1
  27. package/dist/cjs/localizations/de.js +2 -2
  28. package/dist/cjs/rendering/RenderingStyle.d.ts +7 -6
  29. package/dist/cjs/rendering/lib.d.ts +1 -1
  30. package/dist/cjs/rendering/renderers/AbstractRenderer.js +4 -0
  31. package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +9 -0
  32. package/dist/cjs/rendering/renderers/CanvasRenderer.js +14 -0
  33. package/dist/cjs/rendering/renderers/SVGRenderer.d.ts +18 -0
  34. package/dist/cjs/rendering/renderers/SVGRenderer.js +21 -1
  35. package/dist/cjs/toolbar/utils/HelpDisplay.js +6 -4
  36. package/dist/cjs/toolbar/utils/localization.d.ts +1 -0
  37. package/dist/cjs/toolbar/utils/localization.js +1 -0
  38. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +1 -1
  39. package/dist/cjs/toolbar/widgets/InsertImageWidget/InsertImageWidget.js +1 -1
  40. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +1 -1
  41. package/dist/cjs/tools/Eraser.js +3 -3
  42. package/dist/cjs/tools/FindTool.js +1 -1
  43. package/dist/cjs/tools/PasteHandler.js +4 -1
  44. package/dist/cjs/tools/Pen.js +1 -1
  45. package/dist/cjs/tools/SelectionTool/SelectAllShortcutHandler.js +1 -1
  46. package/dist/cjs/tools/SelectionTool/Selection.js +23 -10
  47. package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.js +1 -1
  48. package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.js +1 -1
  49. package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.js +1 -1
  50. package/dist/cjs/tools/SelectionTool/SelectionTool.js +3 -2
  51. package/dist/cjs/tools/SoundUITool.js +1 -1
  52. package/dist/cjs/tools/TextTool.js +2 -2
  53. package/dist/cjs/util/assertions.d.ts +6 -0
  54. package/dist/cjs/util/assertions.js +18 -0
  55. package/dist/cjs/util/describeTransformation.d.ts +12 -0
  56. package/dist/cjs/util/describeTransformation.js +44 -0
  57. package/dist/cjs/version.js +1 -1
  58. package/dist/mjs/Editor.d.ts +7 -2
  59. package/dist/mjs/Editor.mjs +11 -5
  60. package/dist/mjs/SVGLoader/SVGLoader.d.ts +21 -0
  61. package/dist/mjs/SVGLoader/SVGLoader.mjs +74 -47
  62. package/dist/mjs/SVGLoader/SVGLoader.plugins.test.d.ts +1 -0
  63. package/dist/mjs/Viewport.mjs +2 -32
  64. package/dist/mjs/commands/Duplicate.d.ts +7 -4
  65. package/dist/mjs/commands/Duplicate.mjs +48 -7
  66. package/dist/mjs/commands/Duplicate.test.d.ts +1 -0
  67. package/dist/mjs/commands/Erase.d.ts +1 -1
  68. package/dist/mjs/commands/Erase.mjs +2 -2
  69. package/dist/mjs/commands/localization.d.ts +2 -2
  70. package/dist/mjs/commands/localization.mjs +2 -2
  71. package/dist/mjs/components/AbstractComponent.d.ts +7 -0
  72. package/dist/mjs/components/AbstractComponent.mjs +17 -3
  73. package/dist/mjs/components/Stroke.d.ts +21 -1
  74. package/dist/mjs/components/Stroke.mjs +31 -2
  75. package/dist/mjs/components/TextComponent.d.ts +2 -2
  76. package/dist/mjs/components/TextComponent.mjs +2 -2
  77. package/dist/mjs/image/EditorImage.d.ts +17 -9
  78. package/dist/mjs/image/EditorImage.mjs +33 -17
  79. package/dist/mjs/lib.d.ts +1 -1
  80. package/dist/mjs/localizations/de.mjs +2 -2
  81. package/dist/mjs/rendering/RenderingStyle.d.ts +7 -6
  82. package/dist/mjs/rendering/lib.d.ts +1 -1
  83. package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +4 -0
  84. package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +9 -0
  85. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +14 -0
  86. package/dist/mjs/rendering/renderers/SVGRenderer.d.ts +18 -0
  87. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +21 -1
  88. package/dist/mjs/toolbar/utils/HelpDisplay.mjs +6 -4
  89. package/dist/mjs/toolbar/utils/localization.d.ts +1 -0
  90. package/dist/mjs/toolbar/utils/localization.mjs +1 -0
  91. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +1 -1
  92. package/dist/mjs/toolbar/widgets/InsertImageWidget/InsertImageWidget.mjs +1 -1
  93. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +1 -1
  94. package/dist/mjs/tools/Eraser.mjs +3 -3
  95. package/dist/mjs/tools/FindTool.mjs +1 -1
  96. package/dist/mjs/tools/PasteHandler.mjs +4 -1
  97. package/dist/mjs/tools/Pen.mjs +1 -1
  98. package/dist/mjs/tools/SelectionTool/SelectAllShortcutHandler.mjs +1 -1
  99. package/dist/mjs/tools/SelectionTool/Selection.mjs +23 -10
  100. package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.mjs +1 -1
  101. package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.mjs +1 -1
  102. package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.mjs +1 -1
  103. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +3 -2
  104. package/dist/mjs/tools/SoundUITool.mjs +1 -1
  105. package/dist/mjs/tools/TextTool.mjs +2 -2
  106. package/dist/mjs/util/assertions.d.ts +6 -0
  107. package/dist/mjs/util/assertions.mjs +16 -0
  108. package/dist/mjs/util/describeTransformation.d.ts +12 -0
  109. package/dist/mjs/util/describeTransformation.mjs +42 -0
  110. package/dist/mjs/version.mjs +1 -1
  111. package/package.json +4 -4
  112. package/src/toolbar/utils/HelpDisplay.scss +7 -1
@@ -11,6 +11,25 @@ export type SVGLoaderUnknownStyleAttribute = {
11
11
  value: string;
12
12
  priority?: string;
13
13
  };
14
+ export interface SVGLoaderControl {
15
+ /** Call this to add a component to the editor. */
16
+ addComponent: ComponentAddedListener;
17
+ }
18
+ /**
19
+ * Loads custom components from an SVG image.
20
+ * @see SVGLoader.fromString
21
+ */
22
+ export interface SVGLoaderPlugin {
23
+ /**
24
+ * Called when the {@link SVGLoader} encounters a `node`.
25
+ *
26
+ * Call `loader.addComponent` to add new components to the image.
27
+ *
28
+ * Returning `true` prevents the {@link SVGLoader} from doing further
29
+ * processing on the node.
30
+ */
31
+ visit(node: Element, loader: SVGLoaderControl): Promise<boolean>;
32
+ }
14
33
  export declare enum SVGLoaderLoadMethod {
15
34
  IFrame = "iframe",
16
35
  DOMParser = "domparser"
@@ -18,6 +37,7 @@ export declare enum SVGLoaderLoadMethod {
18
37
  export interface SVGLoaderOptions {
19
38
  sanitize?: boolean;
20
39
  disableUnknownObjectWarnings?: boolean;
40
+ plugins?: SVGLoaderPlugin[];
21
41
  loadMethod?: SVGLoaderLoadMethod;
22
42
  }
23
43
  export default class SVGLoader implements ImageLoader {
@@ -31,6 +51,7 @@ export default class SVGLoader implements ImageLoader {
31
51
  private rootViewBox;
32
52
  private readonly storeUnknown;
33
53
  private readonly disableUnknownObjectWarnings;
54
+ private readonly plugins;
34
55
  private constructor();
35
56
  private getStyle;
36
57
  private strokeDataFromElem;
@@ -39,6 +39,7 @@ export default class SVGLoader {
39
39
  this.totalToProcess = 0;
40
40
  this.containerGroupIDs = [];
41
41
  this.encounteredIDs = [];
42
+ this.plugins = options.plugins ?? [];
42
43
  this.storeUnknown = !(options.sanitize ?? false);
43
44
  this.disableUnknownObjectWarnings = !!options.disableUnknownObjectWarnings;
44
45
  }
@@ -434,56 +435,78 @@ export default class SVGLoader {
434
435
  async visit(node) {
435
436
  this.totalToProcess += node.childElementCount;
436
437
  let visitChildren = true;
437
- switch (node.tagName.toLowerCase()) {
438
- case 'g':
439
- if (node.classList.contains(imageBackgroundCSSClassName)) {
440
- await this.addBackground(node);
438
+ const visitPlugin = async () => {
439
+ for (const plugin of this.plugins) {
440
+ const processed = await plugin.visit(node, {
441
+ addComponent: (component) => {
442
+ return this.onAddComponent?.(component);
443
+ },
444
+ });
445
+ if (processed) {
441
446
  visitChildren = false;
447
+ return true;
442
448
  }
443
- else {
444
- await this.startGroup(node);
445
- }
446
- // Otherwise, continue -- visit the node's children.
447
- break;
448
- case 'path':
449
- if (node.classList.contains(imageBackgroundCSSClassName)) {
450
- await this.addBackground(node);
451
- }
452
- else {
453
- await this.addPath(node);
454
- }
455
- break;
456
- case 'text':
457
- await this.addText(node);
458
- visitChildren = false;
459
- break;
460
- case 'image':
461
- await this.addImage(node);
462
- // Images should not have children.
463
- visitChildren = false;
464
- break;
465
- case 'svg':
466
- this.updateViewBox(node);
467
- this.updateSVGAttrs(node);
468
- break;
469
- case 'style':
470
- // Keeping unnecessary style sheets can cause the browser to keep all
471
- // SVG elements *referenced* by the style sheet in some browsers.
472
- //
473
- // Only keep the style sheet if it won't be discarded on save.
474
- if (node.getAttribute('id') !== renderedStylesheetId) {
475
- await this.addUnknownNode(node);
476
- }
477
- break;
478
- default:
479
- if (!this.disableUnknownObjectWarnings) {
480
- console.warn('Unknown SVG element,', node, node.tagName);
481
- if (!(node instanceof SVGElement)) {
482
- console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
449
+ }
450
+ return false;
451
+ };
452
+ const visitBuiltIn = async () => {
453
+ switch (node.tagName.toLowerCase()) {
454
+ case 'g':
455
+ if (node.classList.contains(imageBackgroundCSSClassName)) {
456
+ await this.addBackground(node);
457
+ visitChildren = false;
483
458
  }
484
- }
485
- await this.addUnknownNode(node);
486
- return;
459
+ else {
460
+ await this.startGroup(node);
461
+ }
462
+ // Otherwise, continue -- visit the node's children.
463
+ break;
464
+ case 'path':
465
+ if (node.classList.contains(imageBackgroundCSSClassName)) {
466
+ await this.addBackground(node);
467
+ }
468
+ else {
469
+ await this.addPath(node);
470
+ }
471
+ break;
472
+ case 'text':
473
+ await this.addText(node);
474
+ visitChildren = false;
475
+ break;
476
+ case 'image':
477
+ await this.addImage(node);
478
+ // Images should not have children.
479
+ visitChildren = false;
480
+ break;
481
+ case 'svg':
482
+ this.updateViewBox(node);
483
+ this.updateSVGAttrs(node);
484
+ break;
485
+ case 'style':
486
+ // Keeping unnecessary style sheets can cause the browser to keep all
487
+ // SVG elements *referenced* by the style sheet in some browsers.
488
+ //
489
+ // Only keep the style sheet if it won't be discarded on save.
490
+ if (node.getAttribute('id') !== renderedStylesheetId) {
491
+ await this.addUnknownNode(node);
492
+ }
493
+ break;
494
+ default:
495
+ if (!this.disableUnknownObjectWarnings) {
496
+ console.warn('Unknown SVG element,', node, node.tagName);
497
+ if (!(node instanceof SVGElement)) {
498
+ console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
499
+ }
500
+ }
501
+ await this.addUnknownNode(node);
502
+ return;
503
+ }
504
+ };
505
+ if (await visitPlugin()) {
506
+ visitChildren = false;
507
+ }
508
+ else {
509
+ await visitBuiltIn();
487
510
  }
488
511
  if (visitChildren) {
489
512
  for (const child of node.children) {
@@ -602,17 +625,21 @@ export default class SVGLoader {
602
625
  // Handle options
603
626
  let sanitize;
604
627
  let disableUnknownObjectWarnings;
628
+ let plugins;
605
629
  if (typeof options === 'boolean') {
606
630
  sanitize = options;
607
631
  disableUnknownObjectWarnings = false;
632
+ plugins = [];
608
633
  }
609
634
  else {
610
635
  sanitize = options.sanitize ?? false;
611
636
  disableUnknownObjectWarnings = options.disableUnknownObjectWarnings ?? false;
637
+ plugins = options.plugins;
612
638
  }
613
639
  return new SVGLoader(svgElem, cleanUp, {
614
640
  sanitize,
615
641
  disableUnknownObjectWarnings,
642
+ plugins,
616
643
  });
617
644
  }
618
645
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -12,6 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
12
12
  var _inverseTransform, _a;
13
13
  import Command from './commands/Command.mjs';
14
14
  import { Mat33, Rect2, Vec2, Vec3 } from '@js-draw/math';
15
+ import describeTransformation from './util/describeTransformation.mjs';
15
16
  export class ViewportTransform extends Command {
16
17
  }
17
18
  export class Viewport {
@@ -234,38 +235,7 @@ Viewport.ViewportTransform = (_a = class extends ViewportTransform {
234
235
  editor.queueRerender();
235
236
  }
236
237
  description(editor, localizationTable) {
237
- const result = [];
238
- // Describe the transformation's affect on the viewport (note that transformation transforms
239
- // the **elements** within the viewport). Assumes the transformation only does rotation/scale/translation.
240
- const origVec = editor.viewport.visibleRect.center;
241
- const linearTransformedVec = this.transform.transformVec3(Vec2.unitX);
242
- const affineTransformedVec = this.transform.transformVec2(origVec);
243
- const scale = linearTransformedVec.magnitude();
244
- const rotation = (180 / Math.PI) * linearTransformedVec.angle();
245
- const translation = affineTransformedVec.minus(origVec);
246
- if (scale > 1.2) {
247
- result.push(localizationTable.zoomedIn);
248
- }
249
- else if (scale < 0.8) {
250
- result.push(localizationTable.zoomedOut);
251
- }
252
- if (Math.floor(Math.abs(rotation)) > 0) {
253
- result.push(localizationTable.rotatedBy(Math.round(rotation)));
254
- }
255
- const minTranslation = 1e-4;
256
- if (translation.x > minTranslation) {
257
- result.push(localizationTable.movedLeft);
258
- }
259
- else if (translation.x < -minTranslation) {
260
- result.push(localizationTable.movedRight);
261
- }
262
- if (translation.y < -minTranslation) {
263
- result.push(localizationTable.movedDown);
264
- }
265
- else if (translation.y > minTranslation) {
266
- result.push(localizationTable.movedUp);
267
- }
268
- return result.join('; ');
238
+ return describeTransformation(editor.viewport.visibleRect.center, this.transform, true, localizationTable);
269
239
  }
270
240
  },
271
241
  _inverseTransform = new WeakMap(),
@@ -12,7 +12,7 @@ import SerializableCommand from './SerializableCommand';
12
12
  *
13
13
  * // Find all elements intersecting the rectangle with top left (0,0) and
14
14
  * // (width,height)=(100,100).
15
- * const elems = editor.image.getElementsIntersectingRegion(
15
+ * const elems = editor.image.getComponentsIntersecting(
16
16
  * new Rect2(0, 0, 100, 100)
17
17
  * );
18
18
  *
@@ -23,16 +23,19 @@ import SerializableCommand from './SerializableCommand';
23
23
  * editor.dispatch(duplicateElems);
24
24
  * ```
25
25
  *
26
- * @see {@link Editor.dispatch} {@link EditorImage.getElementsIntersectingRegion}
26
+ * @see {@link Editor.dispatch} {@link EditorImage.getComponentsIntersecting}
27
27
  */
28
28
  export default class Duplicate extends SerializableCommand {
29
29
  private toDuplicate;
30
30
  private duplicates;
31
31
  private reverse;
32
- constructor(toDuplicate: AbstractComponent[]);
32
+ constructor(toDuplicate: AbstractComponent[], idsForDuplicates?: string[]);
33
33
  apply(editor: Editor): void;
34
34
  unapply(editor: Editor): void;
35
35
  onDrop(editor: Editor): void;
36
36
  description(_editor: Editor, localizationTable: EditorLocalization): string;
37
- protected serializeToJSON(): string[];
37
+ protected serializeToJSON(): {
38
+ originalIds: string[];
39
+ cloneIds: string[];
40
+ };
38
41
  }
@@ -1,4 +1,5 @@
1
1
  import describeComponentList from '../components/util/describeComponentList.mjs';
2
+ import { assertIsStringArray } from '../util/assertions.mjs';
2
3
  import Erase from './Erase.mjs';
3
4
  import SerializableCommand from './SerializableCommand.mjs';
4
5
  /**
@@ -11,7 +12,7 @@ import SerializableCommand from './SerializableCommand.mjs';
11
12
  *
12
13
  * // Find all elements intersecting the rectangle with top left (0,0) and
13
14
  * // (width,height)=(100,100).
14
- * const elems = editor.image.getElementsIntersectingRegion(
15
+ * const elems = editor.image.getComponentsIntersecting(
15
16
  * new Rect2(0, 0, 100, 100)
16
17
  * );
17
18
  *
@@ -22,13 +23,24 @@ import SerializableCommand from './SerializableCommand.mjs';
22
23
  * editor.dispatch(duplicateElems);
23
24
  * ```
24
25
  *
25
- * @see {@link Editor.dispatch} {@link EditorImage.getElementsIntersectingRegion}
26
+ * @see {@link Editor.dispatch} {@link EditorImage.getComponentsIntersecting}
26
27
  */
27
28
  class Duplicate extends SerializableCommand {
28
- constructor(toDuplicate) {
29
+ constructor(toDuplicate,
30
+ // @internal -- IDs given to the duplicate elements
31
+ idsForDuplicates) {
29
32
  super('duplicate');
30
33
  this.toDuplicate = toDuplicate;
31
- this.duplicates = toDuplicate.map((elem) => elem.clone());
34
+ this.duplicates = toDuplicate.map((elem, idx) => {
35
+ // For collaborative editing, it's important for the clones to have
36
+ // the same IDs as the originals
37
+ if (idsForDuplicates && idsForDuplicates[idx]) {
38
+ return elem.cloneWithId(idsForDuplicates[idx]);
39
+ }
40
+ else {
41
+ return elem.clone();
42
+ }
43
+ });
32
44
  this.reverse = new Erase(this.duplicates);
33
45
  }
34
46
  apply(editor) {
@@ -47,13 +59,42 @@ class Duplicate extends SerializableCommand {
47
59
  return localizationTable.duplicateAction(describeComponentList(localizationTable, this.duplicates) ?? localizationTable.elements, this.duplicates.length);
48
60
  }
49
61
  serializeToJSON() {
50
- return this.toDuplicate.map((elem) => elem.getId());
62
+ return {
63
+ originalIds: this.toDuplicate.map((elem) => elem.getId()),
64
+ cloneIds: this.duplicates.map((elem) => elem.getId()),
65
+ };
51
66
  }
52
67
  }
53
68
  (() => {
54
69
  SerializableCommand.register('duplicate', (json, editor) => {
55
- const elems = json.map((id) => editor.image.lookupElement(id));
56
- return new Duplicate(elems);
70
+ let originalIds;
71
+ let cloneIds;
72
+ // Compatibility with older editors
73
+ if (Array.isArray(json)) {
74
+ originalIds = json;
75
+ cloneIds = [];
76
+ }
77
+ else {
78
+ originalIds = json.originalIds;
79
+ cloneIds = json.cloneIds;
80
+ }
81
+ assertIsStringArray(originalIds);
82
+ assertIsStringArray(cloneIds);
83
+ // Resolve to elements -- only keep the elements that can be found in the image.
84
+ const resolvedElements = [];
85
+ const filteredCloneIds = [];
86
+ for (let i = 0; i < originalIds.length; i++) {
87
+ const originalId = originalIds[i];
88
+ const foundElement = editor.image.lookupElement(originalId);
89
+ if (!foundElement) {
90
+ console.warn('Duplicate command: Could not find element with ID', originalId);
91
+ }
92
+ else {
93
+ filteredCloneIds.push(cloneIds[i]);
94
+ resolvedElements.push(foundElement);
95
+ }
96
+ }
97
+ return new Duplicate(resolvedElements, filteredCloneIds);
57
98
  });
58
99
  })();
59
100
  export default Duplicate;
@@ -0,0 +1 @@
1
+ export {};
@@ -31,7 +31,7 @@ import SerializableCommand from './SerializableCommand';
31
31
  *
32
32
  * // Find all elements intersecting the rectangle with top left (-10,-30) and
33
33
  * // (width,height)=(50,100).
34
- * const elems = editor.image.getElementsIntersectingRegion(
34
+ * const elems = editor.image.getComponentsIntersecting(
35
35
  * new Rect2(-10, -30, 50, 100)
36
36
  * );
37
37
  *
@@ -31,7 +31,7 @@ import SerializableCommand from './SerializableCommand.mjs';
31
31
  *
32
32
  * // Find all elements intersecting the rectangle with top left (-10,-30) and
33
33
  * // (width,height)=(50,100).
34
- * const elems = editor.image.getElementsIntersectingRegion(
34
+ * const elems = editor.image.getComponentsIntersecting(
35
35
  * new Rect2(-10, -30, 50, 100)
36
36
  * );
37
37
  *
@@ -63,7 +63,7 @@ class Erase extends SerializableCommand {
63
63
  unapply(editor) {
64
64
  for (const part of this.toRemove) {
65
65
  if (!editor.image.findParent(part)) {
66
- EditorImage.addElement(part).apply(editor);
66
+ EditorImage.addComponent(part).apply(editor);
67
67
  }
68
68
  }
69
69
  this.applied = false;
@@ -11,11 +11,11 @@ export interface CommandLocalization {
11
11
  duplicatedNoElements: string;
12
12
  elements: string;
13
13
  updatedViewport: string;
14
- transformedElements: (elemCount: number) => string;
14
+ transformedElements: (elemCount: number, transformDescription: string) => string;
15
15
  resizeOutputCommand: (newSize: Rect2) => string;
16
16
  enabledAutoresizeOutputCommand: string;
17
17
  disabledAutoresizeOutputCommand: string;
18
- addElementAction: (elemDescription: string) => string;
18
+ addComponentAction: (elemDescription: string) => string;
19
19
  eraseAction: (elemDescription: string, numElems: number) => string;
20
20
  duplicateAction: (elemDescription: string, count: number) => string;
21
21
  inverseOf: (actionDescription: string) => string;
@@ -1,10 +1,10 @@
1
1
  export const defaultCommandLocalization = {
2
2
  updatedViewport: 'Transformed Viewport',
3
- transformedElements: (elemCount) => `Transformed ${elemCount} element${elemCount === 1 ? '' : 's'}`,
3
+ transformedElements: (elemCount, action) => `Transformed ${elemCount} element${elemCount === 1 ? '' : 's'} (${action})`,
4
4
  resizeOutputCommand: (newSize) => `Resized image to ${newSize.w}x${newSize.h}`,
5
5
  enabledAutoresizeOutputCommand: 'Enabled output autoresize',
6
6
  disabledAutoresizeOutputCommand: 'Disabled output autoresize',
7
- addElementAction: (componentDescription) => `Added ${componentDescription}`,
7
+ addComponentAction: (componentDescription) => `Added ${componentDescription}`,
8
8
  eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
9
9
  duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
10
10
  unionOf: (actionDescription, actionCount) => `Union: ${actionCount} ${actionDescription}`,
@@ -158,6 +158,13 @@ export default abstract class AbstractComponent {
158
158
  abstract description(localizationTable: ImageComponentLocalization): string;
159
159
  protected abstract createClone(): AbstractComponent;
160
160
  clone(): AbstractComponent;
161
+ /**
162
+ * Creates a copy of this component with a particular `id`.
163
+ * This is used internally by {@link Duplicate} when deserializing.
164
+ *
165
+ * @internal -- users of the library shouldn't need this.
166
+ */
167
+ cloneWithId(cloneId: string): AbstractComponent;
161
168
  /**
162
169
  * **Optional method**: Divides this component into sections roughly along the given path,
163
170
  * removing parts that are roughly within `shape`.
@@ -5,8 +5,10 @@ var __setFunctionName = (this && this.__setFunctionName) || function (f, name, p
5
5
  var _a;
6
6
  import SerializableCommand from '../commands/SerializableCommand.mjs';
7
7
  import EditorImage from '../image/EditorImage.mjs';
8
- import { Mat33 } from '@js-draw/math';
8
+ import { Mat33, Vec2 } from '@js-draw/math';
9
9
  import UnresolvedSerializableCommand from '../commands/UnresolvedCommand.mjs';
10
+ import describeTransformation from '../util/describeTransformation.mjs';
11
+ import { assertIsString } from '../util/assertions.mjs';
10
12
  export var ComponentSizingMode;
11
13
  (function (ComponentSizingMode) {
12
14
  /** The default. The compnent gets its size from its bounding box. */
@@ -201,6 +203,17 @@ class AbstractComponent {
201
203
  }
202
204
  return clone;
203
205
  }
206
+ /**
207
+ * Creates a copy of this component with a particular `id`.
208
+ * This is used internally by {@link Duplicate} when deserializing.
209
+ *
210
+ * @internal -- users of the library shouldn't need this.
211
+ */
212
+ cloneWithId(cloneId) {
213
+ const clone = this.clone();
214
+ clone.id = cloneId;
215
+ return clone;
216
+ }
204
217
  // Convert the component to an object that can be passed to
205
218
  // `JSON.stringify`.
206
219
  //
@@ -244,6 +257,7 @@ class AbstractComponent {
244
257
  if (AbstractComponent.isNotDeserializable(json)) {
245
258
  throw new Error(`Element with data ${json} cannot be deserialized.`);
246
259
  }
260
+ assertIsString(json.id);
247
261
  const instance = this.deserializationCallbacks[json.name](json.data);
248
262
  instance.id = json.id;
249
263
  if (isFinite(json.zIndex)) {
@@ -307,7 +321,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
307
321
  }
308
322
  // Add the element back to the document.
309
323
  if (hadParent) {
310
- EditorImage.addElement(this.component).apply(editor);
324
+ EditorImage.addComponent(this.component).apply(editor);
311
325
  }
312
326
  }
313
327
  apply(editor) {
@@ -321,7 +335,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
321
335
  editor.queueRerender();
322
336
  }
323
337
  description(_editor, localizationTable) {
324
- return localizationTable.transformedElements(1);
338
+ return localizationTable.transformedElements(1, describeTransformation(Vec2.zero, this.affineTransfm, false, localizationTable));
325
339
  }
326
340
  serializeToJSON() {
327
341
  return {
@@ -1,7 +1,8 @@
1
1
  import SerializableCommand from '../commands/SerializableCommand';
2
- import { Mat33, Path, Rect2, LineSegment2 } from '@js-draw/math';
2
+ import { Mat33, Path, Rect2, LineSegment2, Color4 } from '@js-draw/math';
3
3
  import Editor from '../Editor';
4
4
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import { StrokeStyle } from '../rendering/RenderingStyle';
5
6
  import AbstractComponent from './AbstractComponent';
6
7
  import { ImageComponentLocalization } from './localization';
7
8
  import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
@@ -48,6 +49,25 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
48
49
  * ```
49
50
  */
50
51
  constructor(parts: RenderablePathSpec[], initialZIndex?: number);
52
+ /**
53
+ * Creates a new `Stroke` from a {@link Path} and `style`. Strokes created
54
+ * with this method have transparent fill.
55
+ *
56
+ * Example:
57
+ * ```ts,runnable
58
+ * import { Editor, Stroke, Color4 } from 'js-draw';
59
+ * const editor = new Editor(document.body);
60
+ * ---visible---
61
+ * const stroke = Stroke.fromStroked('m0,0 l10,10', { width: 10, color: Color4.red });
62
+ * editor.dispatch(editor.image.addComponent(stroke));
63
+ * ```
64
+ * Notice that `path` can be a string that specifies an SVG path
65
+ *
66
+ * @see fromFilled
67
+ */
68
+ static fromStroked(path: Path | string, style: StrokeStyle): Stroke;
69
+ /** @see fromStroked */
70
+ static fromFilled(path: Path | string, fill: Color4): Stroke;
51
71
  getStyle(): ComponentStyle;
52
72
  updateStyle(style: ComponentStyle): SerializableCommand;
53
73
  forceStyle(style: ComponentStyle, editor: Editor | null): void;
@@ -1,5 +1,5 @@
1
- import { Path, Rect2, PathCommandType, comparePathIndices, stepPathIndexBy, } from '@js-draw/math';
2
- import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle.mjs';
1
+ import { Path, Rect2, PathCommandType, comparePathIndices, stepPathIndexBy, Color4, } from '@js-draw/math';
2
+ import { styleFromJSON, styleToJSON, } from '../rendering/RenderingStyle.mjs';
3
3
  import AbstractComponent from './AbstractComponent.mjs';
4
4
  import { createRestyleComponentCommand, } from './RestylableComponent.mjs';
5
5
  import { pathFromRenderable, pathToRenderable, simplifyPathToFullScreenOrEmpty, } from '../rendering/RenderablePathSpec.mjs';
@@ -69,6 +69,35 @@ export default class Stroke extends AbstractComponent {
69
69
  }
70
70
  this.contentBBox ??= Rect2.empty;
71
71
  }
72
+ /**
73
+ * Creates a new `Stroke` from a {@link Path} and `style`. Strokes created
74
+ * with this method have transparent fill.
75
+ *
76
+ * Example:
77
+ * ```ts,runnable
78
+ * import { Editor, Stroke, Color4 } from 'js-draw';
79
+ * const editor = new Editor(document.body);
80
+ * ---visible---
81
+ * const stroke = Stroke.fromStroked('m0,0 l10,10', { width: 10, color: Color4.red });
82
+ * editor.dispatch(editor.image.addComponent(stroke));
83
+ * ```
84
+ * Notice that `path` can be a string that specifies an SVG path
85
+ *
86
+ * @see fromFilled
87
+ */
88
+ static fromStroked(path, style) {
89
+ if (typeof path === 'string') {
90
+ path = Path.fromString(path);
91
+ }
92
+ return new Stroke([pathToRenderable(path, { fill: Color4.transparent, stroke: style })]);
93
+ }
94
+ /** @see fromStroked */
95
+ static fromFilled(path, fill) {
96
+ if (typeof path === 'string') {
97
+ path = Path.fromString(path);
98
+ }
99
+ return new Stroke([pathToRenderable(path, { fill })]);
100
+ }
72
101
  getStyle() {
73
102
  if (this.parts.length === 0) {
74
103
  return {};
@@ -38,7 +38,7 @@ type TextElement = TextComponent | string;
38
38
  * };
39
39
  *
40
40
  * editor.dispatch(
41
- * editor.image.addElement(new TextComponent(['Hello, world'], positioning1, style)),
41
+ * editor.image.addComponent(new TextComponent(['Hello, world'], positioning1, style)),
42
42
  * );
43
43
  *
44
44
  *
@@ -49,7 +49,7 @@ type TextElement = TextComponent | string;
49
49
  * // is placed directly after 'Test'.
50
50
  * const positioning2 = Mat33.translation(Vec2.of(10, 50));
51
51
  * editor.dispatch(
52
- * editor.image.addElement(
52
+ * editor.image.addComponent(
53
53
  * new TextComponent([ new TextComponent(['Test'], positioning1, style), '[Test]' ], positioning2, style)
54
54
  * ),
55
55
  * );
@@ -40,7 +40,7 @@ const defaultTextStyle = {
40
40
  * };
41
41
  *
42
42
  * editor.dispatch(
43
- * editor.image.addElement(new TextComponent(['Hello, world'], positioning1, style)),
43
+ * editor.image.addComponent(new TextComponent(['Hello, world'], positioning1, style)),
44
44
  * );
45
45
  *
46
46
  *
@@ -51,7 +51,7 @@ const defaultTextStyle = {
51
51
  * // is placed directly after 'Test'.
52
52
  * const positioning2 = Mat33.translation(Vec2.of(10, 50));
53
53
  * editor.dispatch(
54
- * editor.image.addElement(
54
+ * editor.image.addComponent(
55
55
  * new TextComponent([ new TextComponent(['Test'], positioning1, style), '[Test]' ], positioning2, style)
56
56
  * ),
57
57
  * );