js-draw 1.27.1 → 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 (125) hide show
  1. package/README.md +1 -1
  2. package/build-config.json +2 -1
  3. package/dist/Editor.css +1 -1
  4. package/dist/bundle.js +28 -28
  5. package/dist/bundledStyles.js +1 -1
  6. package/dist/cjs/Editor.d.ts +7 -2
  7. package/dist/cjs/Editor.js +11 -5
  8. package/dist/cjs/SVGLoader/SVGLoader.d.ts +21 -0
  9. package/dist/cjs/SVGLoader/SVGLoader.js +74 -47
  10. package/dist/cjs/SVGLoader/SVGLoader.plugins.test.d.ts +1 -0
  11. package/dist/cjs/Viewport.js +2 -32
  12. package/dist/cjs/commands/Duplicate.d.ts +7 -4
  13. package/dist/cjs/commands/Duplicate.js +48 -7
  14. package/dist/cjs/commands/Duplicate.test.d.ts +1 -0
  15. package/dist/cjs/commands/Erase.d.ts +1 -1
  16. package/dist/cjs/commands/Erase.js +2 -2
  17. package/dist/cjs/commands/localization.d.ts +2 -2
  18. package/dist/cjs/commands/localization.js +2 -2
  19. package/dist/cjs/components/AbstractComponent.d.ts +7 -0
  20. package/dist/cjs/components/AbstractComponent.js +16 -2
  21. package/dist/cjs/components/Stroke.d.ts +21 -1
  22. package/dist/cjs/components/Stroke.js +29 -0
  23. package/dist/cjs/components/TextComponent.d.ts +2 -2
  24. package/dist/cjs/components/TextComponent.js +2 -2
  25. package/dist/cjs/components/builders/PolylineBuilder.js +1 -1
  26. package/dist/cjs/image/EditorImage.d.ts +17 -9
  27. package/dist/cjs/image/EditorImage.js +33 -17
  28. package/dist/cjs/lib.d.ts +1 -1
  29. package/dist/cjs/localizations/de.js +2 -2
  30. package/dist/cjs/rendering/RenderingStyle.d.ts +7 -6
  31. package/dist/cjs/rendering/lib.d.ts +1 -1
  32. package/dist/cjs/rendering/renderers/AbstractRenderer.js +4 -0
  33. package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +9 -0
  34. package/dist/cjs/rendering/renderers/CanvasRenderer.js +14 -0
  35. package/dist/cjs/rendering/renderers/SVGRenderer.d.ts +18 -0
  36. package/dist/cjs/rendering/renderers/SVGRenderer.js +21 -1
  37. package/dist/cjs/toolbar/AbstractToolbar.d.ts +2 -2
  38. package/dist/cjs/toolbar/AbstractToolbar.js +2 -3
  39. package/dist/cjs/toolbar/DropdownToolbar.d.ts +1 -1
  40. package/dist/cjs/toolbar/DropdownToolbar.js +2 -3
  41. package/dist/cjs/toolbar/DropdownToolbar.test.d.ts +1 -0
  42. package/dist/cjs/toolbar/utils/HelpDisplay.js +6 -4
  43. package/dist/cjs/toolbar/utils/localization.d.ts +1 -0
  44. package/dist/cjs/toolbar/utils/localization.js +1 -0
  45. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +1 -1
  46. package/dist/cjs/toolbar/widgets/InsertImageWidget/InsertImageWidget.js +1 -1
  47. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +1 -1
  48. package/dist/cjs/tools/Eraser.js +3 -3
  49. package/dist/cjs/tools/FindTool.js +1 -1
  50. package/dist/cjs/tools/PasteHandler.js +4 -1
  51. package/dist/cjs/tools/Pen.js +1 -1
  52. package/dist/cjs/tools/SelectionTool/SelectAllShortcutHandler.js +1 -1
  53. package/dist/cjs/tools/SelectionTool/Selection.js +23 -10
  54. package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.js +1 -1
  55. package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.js +1 -1
  56. package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.js +1 -1
  57. package/dist/cjs/tools/SelectionTool/SelectionTool.js +3 -2
  58. package/dist/cjs/tools/SoundUITool.js +1 -1
  59. package/dist/cjs/tools/TextTool.js +2 -2
  60. package/dist/cjs/util/assertions.d.ts +6 -0
  61. package/dist/cjs/util/assertions.js +18 -0
  62. package/dist/cjs/util/describeTransformation.d.ts +12 -0
  63. package/dist/cjs/util/describeTransformation.js +44 -0
  64. package/dist/cjs/version.js +2 -1
  65. package/dist/mjs/Editor.d.ts +7 -2
  66. package/dist/mjs/Editor.mjs +11 -5
  67. package/dist/mjs/SVGLoader/SVGLoader.d.ts +21 -0
  68. package/dist/mjs/SVGLoader/SVGLoader.mjs +74 -47
  69. package/dist/mjs/SVGLoader/SVGLoader.plugins.test.d.ts +1 -0
  70. package/dist/mjs/Viewport.mjs +2 -32
  71. package/dist/mjs/commands/Duplicate.d.ts +7 -4
  72. package/dist/mjs/commands/Duplicate.mjs +48 -7
  73. package/dist/mjs/commands/Duplicate.test.d.ts +1 -0
  74. package/dist/mjs/commands/Erase.d.ts +1 -1
  75. package/dist/mjs/commands/Erase.mjs +2 -2
  76. package/dist/mjs/commands/localization.d.ts +2 -2
  77. package/dist/mjs/commands/localization.mjs +2 -2
  78. package/dist/mjs/components/AbstractComponent.d.ts +7 -0
  79. package/dist/mjs/components/AbstractComponent.mjs +17 -3
  80. package/dist/mjs/components/Stroke.d.ts +21 -1
  81. package/dist/mjs/components/Stroke.mjs +31 -2
  82. package/dist/mjs/components/TextComponent.d.ts +2 -2
  83. package/dist/mjs/components/TextComponent.mjs +2 -2
  84. package/dist/mjs/components/builders/PolylineBuilder.mjs +1 -1
  85. package/dist/mjs/image/EditorImage.d.ts +17 -9
  86. package/dist/mjs/image/EditorImage.mjs +33 -17
  87. package/dist/mjs/lib.d.ts +1 -1
  88. package/dist/mjs/localizations/de.mjs +2 -2
  89. package/dist/mjs/rendering/RenderingStyle.d.ts +7 -6
  90. package/dist/mjs/rendering/lib.d.ts +1 -1
  91. package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +4 -0
  92. package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +9 -0
  93. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +14 -0
  94. package/dist/mjs/rendering/renderers/SVGRenderer.d.ts +18 -0
  95. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +21 -1
  96. package/dist/mjs/toolbar/AbstractToolbar.d.ts +2 -2
  97. package/dist/mjs/toolbar/AbstractToolbar.mjs +2 -3
  98. package/dist/mjs/toolbar/DropdownToolbar.d.ts +1 -1
  99. package/dist/mjs/toolbar/DropdownToolbar.mjs +2 -3
  100. package/dist/mjs/toolbar/DropdownToolbar.test.d.ts +1 -0
  101. package/dist/mjs/toolbar/utils/HelpDisplay.mjs +6 -4
  102. package/dist/mjs/toolbar/utils/localization.d.ts +1 -0
  103. package/dist/mjs/toolbar/utils/localization.mjs +1 -0
  104. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +1 -1
  105. package/dist/mjs/toolbar/widgets/InsertImageWidget/InsertImageWidget.mjs +1 -1
  106. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +1 -1
  107. package/dist/mjs/tools/Eraser.mjs +3 -3
  108. package/dist/mjs/tools/FindTool.mjs +1 -1
  109. package/dist/mjs/tools/PasteHandler.mjs +4 -1
  110. package/dist/mjs/tools/Pen.mjs +1 -1
  111. package/dist/mjs/tools/SelectionTool/SelectAllShortcutHandler.mjs +1 -1
  112. package/dist/mjs/tools/SelectionTool/Selection.mjs +23 -10
  113. package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.mjs +1 -1
  114. package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.mjs +1 -1
  115. package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.mjs +1 -1
  116. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +3 -2
  117. package/dist/mjs/tools/SoundUITool.mjs +1 -1
  118. package/dist/mjs/tools/TextTool.mjs +2 -2
  119. package/dist/mjs/util/assertions.d.ts +6 -0
  120. package/dist/mjs/util/assertions.mjs +16 -0
  121. package/dist/mjs/util/describeTransformation.d.ts +12 -0
  122. package/dist/mjs/util/describeTransformation.mjs +42 -0
  123. package/dist/mjs/version.mjs +2 -1
  124. package/package.json +4 -4
  125. package/src/toolbar/utils/HelpDisplay.scss +7 -1
@@ -7,6 +7,7 @@ import UndoRedoHistory from './UndoRedoHistory';
7
7
  import Viewport from './Viewport';
8
8
  import { Point2, Vec2, Color4, Mat33, Rect2 } from '@js-draw/math';
9
9
  import Display, { RenderingMode } from './rendering/Display';
10
+ import { SVGLoaderPlugin } from './SVGLoader/SVGLoader';
10
11
  import Pointer from './Pointer';
11
12
  import { EditorLocalization } from './localization';
12
13
  import IconProvider from './toolbar/IconProvider';
@@ -131,6 +132,10 @@ export interface EditorSettings {
131
132
  /** Called to write data to the clipboard. Keys in `data` are MIME types. Values are the data associated with that type. */
132
133
  write(data: Map<string, Blob | Promise<Blob> | string>): void | Promise<void>;
133
134
  } | null;
135
+ svg: {
136
+ /** Plugins that create custom components while loading with {@link Editor.loadFromSVG}. */
137
+ loaderPlugins?: SVGLoaderPlugin[];
138
+ } | null;
134
139
  }
135
140
  /**
136
141
  * The main entrypoint for the full editor.
@@ -184,10 +189,10 @@ export declare class Editor {
184
189
  * const stroke = new Stroke([
185
190
  * pathToRenderable(Path.fromString('M0,0 L100,100 L300,30 z'), { fill: Color4.red }),
186
191
  * ]);
187
- * const addElementCommand = editor.image.addElement(stroke);
192
+ * const addComponentCommand = editor.image.addComponent(stroke);
188
193
  *
189
194
  * // Add the stroke to the editor
190
- * editor.dispatch(addElementCommand);
195
+ * editor.dispatch(addComponentCommand);
191
196
  * ```
192
197
  */
193
198
  readonly image: EditorImage;
@@ -120,6 +120,9 @@ export class Editor {
120
120
  image: {
121
121
  showImagePicker: settings.image?.showImagePicker ?? undefined,
122
122
  },
123
+ svg: {
124
+ loaderPlugins: settings.svg?.loaderPlugins ?? [],
125
+ },
123
126
  clipboardApi: settings.clipboardApi ?? null,
124
127
  };
125
128
  // Validate settings
@@ -1019,7 +1022,7 @@ export class Editor {
1019
1022
  const commands = [];
1020
1023
  for (const component of components) {
1021
1024
  // To allow deserialization, we need to add first, then transform.
1022
- commands.push(EditorImage.addElement(component));
1025
+ commands.push(EditorImage.addComponent(component));
1023
1026
  commands.push(component.transformBy(transfm));
1024
1027
  }
1025
1028
  const applyChunkSize = 100;
@@ -1099,7 +1102,7 @@ export class Editor {
1099
1102
  const originalBackgrounds = this.image.getBackgroundComponents();
1100
1103
  const eraseBackgroundCommand = new Erase(originalBackgrounds);
1101
1104
  await loader.start(async (component) => {
1102
- await this.dispatchNoAnnounce(EditorImage.addElement(component));
1105
+ await this.dispatchNoAnnounce(EditorImage.addComponent(component));
1103
1106
  }, (countProcessed, totalToProcess) => {
1104
1107
  if (countProcessed % 500 === 0) {
1105
1108
  this.showLoadingWarning(countProcessed / totalToProcess);
@@ -1173,7 +1176,7 @@ export class Editor {
1173
1176
  const fillsScreen = style.autoresize ?? originalFillsScreen;
1174
1177
  if (backgroundType !== BackgroundType.None) {
1175
1178
  const newBackground = new BackgroundComponent(backgroundType, backgroundColor);
1176
- commands.push(EditorImage.addElement(newBackground));
1179
+ commands.push(EditorImage.addComponent(newBackground));
1177
1180
  }
1178
1181
  if (fillsScreen !== originalFillsScreen) {
1179
1182
  commands.push(this.image.setAutoresizeEnabled(fillsScreen));
@@ -1199,7 +1202,7 @@ export class Editor {
1199
1202
  ? BackgroundType.None
1200
1203
  : BackgroundType.SolidColor;
1201
1204
  background = new BackgroundComponent(backgroundType, color);
1202
- return this.image.addElement(background);
1205
+ return this.image.addComponent(background);
1203
1206
  }
1204
1207
  else {
1205
1208
  return background.updateStyle({ color });
@@ -1248,7 +1251,10 @@ export class Editor {
1248
1251
  * ```
1249
1252
  */
1250
1253
  async loadFromSVG(svgData, sanitize = false) {
1251
- const loader = SVGLoader.fromString(svgData, sanitize);
1254
+ const loader = SVGLoader.fromString(svgData, {
1255
+ sanitize,
1256
+ plugins: this.getCurrentSettings().svg?.loaderPlugins,
1257
+ });
1252
1258
  await this.loadFrom(loader);
1253
1259
  }
1254
1260
  /**
@@ -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;