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
@@ -15,6 +15,9 @@ type FromViewportOptions = {
15
15
  */
16
16
  useViewBoxForPositioning?: boolean;
17
17
  };
18
+ type DrawWithSVGParentContext = {
19
+ sanitize: boolean;
20
+ };
18
21
  /**
19
22
  * Renders onto an `SVGElement`.
20
23
  *
@@ -57,7 +60,22 @@ export default class SVGRenderer extends AbstractRenderer {
57
60
  protected traceCubicBezierCurve(_controlPoint1: Point2, _controlPoint2: Point2, _endPoint: Point2): void;
58
61
  protected traceQuadraticBezierCurve(_controlPoint: Point2, _endPoint: Point2): void;
59
62
  drawPoints(...points: Point2[]): void;
63
+ /**
64
+ * Adds a **copy** of the given element directly to the container
65
+ * SVG element, **without applying transforms**.
66
+ *
67
+ * If `sanitize` is enabled, this does nothing.
68
+ */
60
69
  drawSVGElem(elem: SVGElement): void;
70
+ /**
71
+ * Allows rendering directly to the underlying SVG element. Rendered
72
+ * content is added to a `<g>` element that's passed as `parent` to `callback`.
73
+ *
74
+ * **Note**: Unlike {@link drawSVGElem}, this method can be used even if `sanitize` is `true`.
75
+ * In this case, it's the responsibility of `callback` to ensure that everything added
76
+ * to `parent` is safe to render.
77
+ */
78
+ drawWithSVGParent(callback: (parent: SVGGElement, context: DrawWithSVGParentContext) => void): void;
61
79
  isTooSmallToRender(_rect: Rect2): boolean;
62
80
  /**
63
81
  * Creates a new SVG element and `SVGRenerer` with `width`, `height`, `viewBox`,
@@ -347,7 +347,12 @@ class SVGRenderer extends AbstractRenderer_1.default {
347
347
  this.elem.appendChild(elem);
348
348
  });
349
349
  }
350
- // Renders a **copy** of the given element.
350
+ /**
351
+ * Adds a **copy** of the given element directly to the container
352
+ * SVG element, **without applying transforms**.
353
+ *
354
+ * If `sanitize` is enabled, this does nothing.
355
+ */
351
356
  drawSVGElem(elem) {
352
357
  if (this.sanitize) {
353
358
  return;
@@ -361,6 +366,21 @@ class SVGRenderer extends AbstractRenderer_1.default {
361
366
  this.elem.appendChild(elemToDraw);
362
367
  this.objectElems?.push(elemToDraw);
363
368
  }
369
+ /**
370
+ * Allows rendering directly to the underlying SVG element. Rendered
371
+ * content is added to a `<g>` element that's passed as `parent` to `callback`.
372
+ *
373
+ * **Note**: Unlike {@link drawSVGElem}, this method can be used even if `sanitize` is `true`.
374
+ * In this case, it's the responsibility of `callback` to ensure that everything added
375
+ * to `parent` is safe to render.
376
+ */
377
+ drawWithSVGParent(callback) {
378
+ const parent = document.createElementNS(svgNameSpace, 'g');
379
+ this.transformFrom(math_1.Mat33.identity, parent, true);
380
+ callback(parent, { sanitize: this.sanitize });
381
+ this.elem.appendChild(parent);
382
+ this.objectElems?.push(parent);
383
+ }
364
384
  isTooSmallToRender(_rect) {
365
385
  return false;
366
386
  }
@@ -139,6 +139,8 @@ const createHelpPage = (helpItems, onItemClick, onBackgroundClick, context) => {
139
139
  clonedElement.style.margin = '0';
140
140
  const clonedElementContainer = document.createElement('div');
141
141
  clonedElementContainer.classList.add('cloned-element-container');
142
+ clonedElementContainer.role = 'group';
143
+ clonedElementContainer.ariaLabel = context.localization.helpControlsAccessibilityLabel;
142
144
  clonedElementContainer.style.position = 'absolute';
143
145
  clonedElementContainer.style.left = `${targetBBox.topLeft.x}px`;
144
146
  clonedElementContainer.style.top = `${targetBBox.topLeft.y}px`;
@@ -157,11 +159,11 @@ const createHelpPage = (helpItems, onItemClick, onBackgroundClick, context) => {
157
159
  };
158
160
  const onItemChange = () => {
159
161
  const helpTextElement = document.createElement('div');
160
- helpTextElement.innerText = currentItem?.helpText ?? '';
162
+ helpTextElement.textContent = currentItem?.helpText ?? '';
161
163
  // For tests
162
164
  helpTextElement.classList.add('current-item-help');
163
165
  const navigationHelpElement = document.createElement('div');
164
- navigationHelpElement.innerText = context.localization.helpScreenNavigationHelp;
166
+ navigationHelpElement.textContent = context.localization.helpScreenNavigationHelp;
165
167
  navigationHelpElement.classList.add('navigation-help');
166
168
  textLabel.replaceChildren(helpTextElement, ...(currentItemIndex === 0 ? [navigationHelpElement] : []));
167
169
  updateClonedElementStates();
@@ -278,8 +280,8 @@ class HelpDisplay {
278
280
  navigationButtonContainer.classList.add('navigation-buttons');
279
281
  const nextButton = document.createElement('button');
280
282
  const previousButton = document.createElement('button');
281
- nextButton.innerText = this.context.localization.next;
282
- previousButton.innerText = this.context.localization.previous;
283
+ nextButton.textContent = this.context.localization.next;
284
+ previousButton.textContent = this.context.localization.previous;
283
285
  nextButton.classList.add('next');
284
286
  previousButton.classList.add('previous');
285
287
  const updateButtonVisibility = () => {
@@ -1,6 +1,7 @@
1
1
  export interface ToolbarUtilsLocalization {
2
2
  help: string;
3
3
  helpScreenNavigationHelp: string;
4
+ helpControlsAccessibilityLabel: string;
4
5
  helpHidden: string;
5
6
  next: string;
6
7
  previous: string;
@@ -8,4 +8,5 @@ exports.defaultToolbarUtilsLocalization = {
8
8
  previous: 'Previous',
9
9
  close: 'Close',
10
10
  helpScreenNavigationHelp: 'Click on a control for more information.',
11
+ helpControlsAccessibilityLabel: 'Controls: Activate a control to show help.',
11
12
  };
@@ -100,7 +100,7 @@ class DocumentPropertiesWidget extends BaseWidget_1.default {
100
100
  setBackgroundType(backgroundType) {
101
101
  const prevBackgroundColor = this.editor.estimateBackgroundColor();
102
102
  const newBackground = new BackgroundComponent_1.default(backgroundType, prevBackgroundColor);
103
- const addBackgroundCommand = this.editor.image.addElement(newBackground);
103
+ const addBackgroundCommand = this.editor.image.addComponent(newBackground);
104
104
  return (0, uniteCommands_1.default)([this.removeBackgroundComponents(), addBackgroundCommand]);
105
105
  }
106
106
  /** Returns the type of the topmost background component */
@@ -275,7 +275,7 @@ class InsertImageWidget extends BaseWidget_1.default {
275
275
  const widthAdjustTransform = math_1.Mat33.scaling2D(originalWidth / newWidth);
276
276
  const commands = [];
277
277
  for (const component of newComponents) {
278
- commands.push(EditorImage_1.default.addElement(component), component.transformBy(originalTransform.rightMul(widthAdjustTransform)), component.setZIndex(editingImage.getZIndex()));
278
+ commands.push(EditorImage_1.default.addComponent(component), component.transformBy(originalTransform.rightMul(widthAdjustTransform)), component.setZIndex(editingImage.getZIndex()));
279
279
  }
280
280
  this.editor.dispatch((0, uniteCommands_1.default)([...commands, eraseCommand]));
281
281
  selectionTools[0]?.setSelection(newComponents);
@@ -22,7 +22,7 @@ labelText, defaultId, choices) => {
22
22
  outerContainer.classList.add(`${constants_1.toolbarCSSPrefix}grid-selector`);
23
23
  const selectedValue = ReactiveValue_1.MutableReactiveValue.fromInitialValue(defaultId);
24
24
  const menuContainer = document.createElement('div');
25
- menuContainer.setAttribute('role', 'menu');
25
+ menuContainer.role = 'group';
26
26
  menuContainer.id = `${constants_1.toolbarCSSPrefix}-grid-select-id-${idCounter++}`;
27
27
  (0, stopPropagationOfScrollingWheelEvents_1.default)(menuContainer);
28
28
  const label = document.createElement('label');
@@ -145,7 +145,7 @@ class Eraser extends BaseTool_1.default {
145
145
  const line = new math_1.LineSegment2(this.lastPoint, currentPoint);
146
146
  const region = math_1.Rect2.union(line.bbox, eraserRect);
147
147
  const intersectingElems = this.editor.image
148
- .getElementsIntersectingRegion(region)
148
+ .getComponentsIntersecting(region)
149
149
  .filter((component) => {
150
150
  return component.intersects(line) || component.intersectsRect(eraserRect);
151
151
  });
@@ -184,7 +184,7 @@ class Eraser extends BaseTool_1.default {
184
184
  toAdd.push(...targetElem.withRegionErased(erasePath, this.editor.viewport));
185
185
  }
186
186
  const eraseCommand = new Erase_1.default(toErase);
187
- const newAddCommands = toAdd.map((elem) => EditorImage_1.default.addElement(elem));
187
+ const newAddCommands = toAdd.map((elem) => EditorImage_1.default.addComponent(elem));
188
188
  eraseCommand.apply(this.editor);
189
189
  newAddCommands.forEach((command) => command.apply(this.editor));
190
190
  const finalToErase = [];
@@ -240,7 +240,7 @@ class Eraser extends BaseTool_1.default {
240
240
  this.toRemove = this.toRemove.filter((other) => other !== item);
241
241
  }
242
242
  }
243
- commands.push(...[...this.toAdd].map((a) => EditorImage_1.default.addElement(a)));
243
+ commands.push(...[...this.toAdd].map((a) => EditorImage_1.default.addComponent(a)));
244
244
  this.addCommands = [];
245
245
  }
246
246
  if (this.eraseCommands.length > 0) {
@@ -27,7 +27,7 @@ class FindTool extends BaseTool_1.default {
27
27
  }
28
28
  getMatches(searchFor) {
29
29
  const lowerSearchFor = searchFor.toLocaleLowerCase();
30
- const matchingComponents = this.editor.image.getAllElements().filter((component) => {
30
+ const matchingComponents = this.editor.image.getAllComponents().filter((component) => {
31
31
  let text = '';
32
32
  if (component instanceof TextComponent_1.default) {
33
33
  text = component.getText();
@@ -76,7 +76,10 @@ class PasteHandler extends BaseTool_1.default {
76
76
  async doSVGPaste(data) {
77
77
  this.editor.showLoadingWarning(0);
78
78
  try {
79
- const loader = SVGLoader_1.default.fromString(data, true);
79
+ const loader = SVGLoader_1.default.fromString(data, {
80
+ sanitize: true,
81
+ plugins: this.editor.getCurrentSettings().svg?.loaderPlugins ?? [],
82
+ });
80
83
  const components = [];
81
84
  await loader.start((component) => {
82
85
  components.push(component);
@@ -251,7 +251,7 @@ class Pen extends BaseTool_1.default {
251
251
  this.editor.announceForAccessibility(this.editor.localization.autocorrectedTo(stroke.description(this.editor.localization)));
252
252
  }
253
253
  const canFlatten = true;
254
- const action = EditorImage_1.default.addElement(stroke, canFlatten);
254
+ const action = EditorImage_1.default.addComponent(stroke, canFlatten);
255
255
  this.editor.dispatch(action);
256
256
  }
257
257
  else {
@@ -22,7 +22,7 @@ class SelectAllShortcutHandler extends BaseTool_1.default {
22
22
  if (selectionTools.length > 0) {
23
23
  const selectionTool = selectionTools[0];
24
24
  selectionTool.setEnabled(true);
25
- selectionTool.setSelection(this.editor.image.getAllElements());
25
+ selectionTool.setSelection(this.editor.image.getAllComponents());
26
26
  return true;
27
27
  }
28
28
  }
@@ -53,6 +53,8 @@ const types_1 = require("./types");
53
53
  const EditorImage_1 = __importDefault(require("../../image/EditorImage"));
54
54
  const uniteCommands_1 = __importDefault(require("../../commands/uniteCommands"));
55
55
  const SelectionMenuShortcut_1 = __importDefault(require("./SelectionMenuShortcut"));
56
+ const assertions_1 = require("../../util/assertions");
57
+ const describeTransformation_1 = __importDefault(require("../../util/describeTransformation"));
56
58
  const updateChunkSize = 100;
57
59
  const maxPreviewElemCount = 500;
58
60
  // @internal
@@ -186,7 +188,7 @@ class Selection {
186
188
  return 0;
187
189
  }
188
190
  const selectedBottommostZIndex = this.selectedElems[0].getZIndex();
189
- const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.region);
191
+ const visibleObjects = this.editor.image.getComponentsIntersecting(this.region);
190
192
  const topMostVisibleZIndex = visibleObjects[visibleObjects.length - 1]?.getZIndex() ?? selectedBottommostZIndex;
191
193
  const deltaZIndex = topMostVisibleZIndex + 1 - selectedBottommostZIndex;
192
194
  return deltaZIndex;
@@ -205,13 +207,13 @@ class Selection {
205
207
  // z-index of the just-transformed commands.
206
208
  if (this.selectedElems.length > 0) {
207
209
  const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
208
- transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, fullTransform, deltaZIndex));
210
+ transformPromise = this.editor.dispatch(new _a.ApplyTransformationCommand(this, selectedElems, this.originalRegion.center, fullTransform, deltaZIndex));
209
211
  }
210
212
  return transformPromise;
211
213
  }
212
214
  /** Sends all selected elements to the bottom of the visible image. */
213
215
  sendToBack() {
214
- const visibleObjects = this.editor.image.getElementsIntersectingRegion(this.editor.viewport.visibleRect);
216
+ const visibleObjects = this.editor.image.getComponentsIntersecting(this.editor.viewport.visibleRect);
215
217
  // VisibleObjects and selectedElems should both be sorted by z-index
216
218
  const lowestVisibleZIndex = visibleObjects[0]?.getZIndex() ?? 0;
217
219
  const highestSelectedZIndex = this.selectedElems[this.selectedElems.length - 1]?.getZIndex() ?? 0;
@@ -338,7 +340,7 @@ class Selection {
338
340
  // If we're making things visible and the selected object wasn't previously
339
341
  // visible,
340
342
  else if (!parent && this.removedFromImage[elem.getId()]) {
341
- EditorImage_1.default.addElement(elem).apply(this.editor);
343
+ EditorImage_1.default.addComponent(elem).apply(this.editor);
342
344
  this.removedFromImage[elem.getId()] = false;
343
345
  delete this.removedFromImage[elem.getId()];
344
346
  }
@@ -476,14 +478,14 @@ class Selection {
476
478
  // Don't update the selection's focus when redoing/undoing
477
479
  const selectionToUpdate = null;
478
480
  const deltaZIndex = this.getDeltaZIndexToMoveSelectionToTop();
479
- tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.transform, deltaZIndex);
481
+ tmpApplyCommand = new _a.ApplyTransformationCommand(selectionToUpdate, this.selectedElems, this.region.center, this.transform, deltaZIndex);
480
482
  // Transform to ensure that the duplicates are in the correct location
481
483
  await tmpApplyCommand.apply(this.editor);
482
484
  // Show items again
483
485
  this.addRemoveSelectionFromImage(true);
484
486
  // With the transformation applied, create the duplicates
485
487
  command = (0, uniteCommands_1.default)(this.selectedElems.map((elem) => {
486
- return EditorImage_1.default.addElement(elem.clone());
488
+ return EditorImage_1.default.addComponent(elem.clone());
487
489
  }));
488
490
  // Move the selected objects back to the correct location.
489
491
  await tmpApplyCommand?.unapply(this.editor);
@@ -541,21 +543,31 @@ class Selection {
541
543
  _a = Selection;
542
544
  (() => {
543
545
  SerializableCommand_1.default.register('selection-tool-transform', (json, _editor) => {
546
+ const rawTransformArray = json.transform;
547
+ const rawCenterArray = json.selectionCenter ?? [0, 0];
548
+ const rawElementIds = json.elems ?? [];
549
+ (0, assertions_1.assertIsNumberArray)(rawTransformArray);
550
+ (0, assertions_1.assertIsNumberArray)(rawCenterArray);
551
+ (0, assertions_1.assertIsStringArray)(rawElementIds);
544
552
  // The selection box is lost when serializing/deserializing. No need to store box rotation
545
- const fullTransform = new math_1.Mat33(...json.transform);
546
- const elemIds = json.elems ?? [];
553
+ const fullTransform = new math_1.Mat33(...rawTransformArray);
554
+ const elemIds = rawElementIds;
547
555
  const deltaZIndex = parseInt(json.deltaZIndex ?? 0);
548
- return new _a.ApplyTransformationCommand(null, elemIds, fullTransform, deltaZIndex);
556
+ const center = math_1.Vec2.of(rawCenterArray[0] ?? 0, rawCenterArray[1] ?? 0);
557
+ return new _a.ApplyTransformationCommand(null, elemIds, center, fullTransform, deltaZIndex);
549
558
  });
550
559
  })();
551
560
  Selection.ApplyTransformationCommand = class extends SerializableCommand_1.default {
552
561
  constructor(selection,
553
562
  // If a `string[]`, selectedElems is a list of element IDs.
554
563
  selectedElems,
564
+ // Information used to describe the transformation
565
+ selectionCenter,
555
566
  // Full transformation used to transform elements.
556
567
  fullTransform, deltaZIndex) {
557
568
  super('selection-tool-transform');
558
569
  this.selection = selection;
570
+ this.selectionCenter = selectionCenter;
559
571
  this.fullTransform = fullTransform;
560
572
  this.deltaZIndex = deltaZIndex;
561
573
  const isIDList = (arr) => {
@@ -623,10 +635,11 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand_1.defau
623
635
  elems: this.selectedElemIds,
624
636
  transform: this.fullTransform.toArray(),
625
637
  deltaZIndex: this.deltaZIndex,
638
+ selectionCenter: this.selectionCenter.asArray(),
626
639
  };
627
640
  }
628
641
  description(_editor, localizationTable) {
629
- return localizationTable.transformedElements(this.selectedElemIds.length);
642
+ return localizationTable.transformedElements(this.selectedElemIds.length, (0, describeTransformation_1.default)(this.selectionCenter, this.fullTransform, false, localizationTable));
630
643
  }
631
644
  };
632
645
  exports.default = Selection;
@@ -38,7 +38,7 @@ class LassoSelectionBuilder extends SelectionBuilder_1.default {
38
38
  resolveInternal(image) {
39
39
  const path = this.previewPath();
40
40
  const lines = path.polylineApproximation();
41
- const candidates = image.getElementsIntersectingRegion(path.bbox);
41
+ const candidates = image.getComponentsIntersecting(path.bbox);
42
42
  const componentIsInSelection = (component) => {
43
43
  if (path.closedContainsRect(component.getExactBBox())) {
44
44
  return true;
@@ -20,7 +20,7 @@ class RectSelectionBuilder extends SelectionBuilder_1.default {
20
20
  return math_1.Path.fromRect(this.rect);
21
21
  }
22
22
  resolveInternal(image) {
23
- return image.getElementsIntersectingRegion(this.rect).filter((element) => {
23
+ return image.getComponentsIntersecting(this.rect).filter((element) => {
24
24
  // Filter out the case where the selection rectangle is completely contained
25
25
  // within the element (and does not intersect it).
26
26
  // This is useful, for example, if a very large stroke is used as the background
@@ -22,7 +22,7 @@ class SelectionBuilder {
22
22
  if (isClick) {
23
23
  const searchRegionSize = viewport.visibleRect.maxDimension / 200;
24
24
  const minSizeBox = path.bbox.grownBy(searchRegionSize);
25
- components = image.getElementsIntersectingRegion(minSizeBox).filter((component) => {
25
+ components = image.getComponentsIntersecting(minSizeBox).filter((component) => {
26
26
  return minSizeBox.containsRect(component.getBBox()) || component.intersectsRect(minSizeBox);
27
27
  });
28
28
  components = filterComponents(components);
@@ -259,7 +259,7 @@ class SelectionTool extends BaseTool_1.default {
259
259
  return true;
260
260
  }
261
261
  else if (shortcucts.matchesShortcut(keybindings_1.selectAllKeyboardShortcut, event)) {
262
- this.setSelection(this.editor.image.getAllElements());
262
+ this.setSelection(this.editor.image.getAllComponents());
263
263
  return true;
264
264
  }
265
265
  else if (event.ctrlKey) {
@@ -462,7 +462,8 @@ class SelectionTool extends BaseTool_1.default {
462
462
  this.handleOverlay.style.display = enabled ? 'block' : 'none';
463
463
  if (enabled) {
464
464
  this.handleOverlay.tabIndex = 0;
465
- this.handleOverlay.setAttribute('aria-label', this.editor.localization.selectionToolKeyboardShortcuts);
465
+ this.handleOverlay.role = 'group';
466
+ this.handleOverlay.ariaLabel = this.editor.localization.selectionToolKeyboardShortcuts;
466
467
  }
467
468
  else {
468
469
  this.handleOverlay.tabIndex = -1;
@@ -156,7 +156,7 @@ class SoundUITool extends BaseTool_1.default {
156
156
  this.soundFeedback?.setColor(this.editor.display.getColorAt(current.screenPos) ?? math_1.Color4.black);
157
157
  const pointerMotionLine = new math_1.LineSegment2(this.lastPointerPos, current.canvasPos);
158
158
  const collisions = this.editor.image
159
- .getElementsIntersectingRegion(pointerMotionLine.bbox)
159
+ .getComponentsIntersecting(pointerMotionLine.bbox)
160
160
  .filter((component) => component.intersects(pointerMotionLine));
161
161
  this.lastPointerPos = current.canvasPos;
162
162
  if (collisions.length > 0) {
@@ -103,7 +103,7 @@ class TextTool extends BaseTool_1.default {
103
103
  const scrollCorrectionCanvas = this.editor.viewport.screenToCanvasTransform.transformVec3(scrollCorrectionScreen);
104
104
  const scrollTransform = math_1.Mat33.translation(scrollCorrectionCanvas);
105
105
  const textComponent = TextComponent_1.default.fromLines(content.split('\n'), scrollTransform.rightMul(this.contentTransform.get()), this.textStyle);
106
- const action = EditorImage_1.default.addElement(textComponent);
106
+ const action = EditorImage_1.default.addComponent(textComponent);
107
107
  if (this.removeExistingCommand) {
108
108
  // Unapply so that `removeExistingCommand` can be added to the undo stack.
109
109
  this.removeExistingCommand.unapply(this.editor);
@@ -213,7 +213,7 @@ class TextTool extends BaseTool_1.default {
213
213
  const canvasPos = current.canvasPos;
214
214
  const halfTestRegionSize = math_1.Vec2.of(4, 4).times(this.editor.viewport.getSizeOfPixelOnCanvas());
215
215
  const testRegion = math_1.Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
216
- const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
216
+ const targetNodes = this.editor.image.getComponentsIntersecting(testRegion);
217
217
  let targetTextNodes = targetNodes.filter((node) => node instanceof TextComponent_1.default);
218
218
  // Don't try to edit text nodes that contain the viewport (this allows us
219
219
  // to zoom in on text nodes and add text on top of them.)
@@ -15,11 +15,17 @@ export declare function assertUnreachable(key: never): never;
15
15
  * ```
16
16
  */
17
17
  export declare function assertIsNumber(value: unknown, allowNaN?: boolean): asserts value is number;
18
+ /** Throws an `Error` if the given `value` is not a `string`. */
19
+ export declare function assertIsString(value: unknown): asserts value is string;
18
20
  export declare function assertIsArray(values: unknown): asserts values is unknown[];
19
21
  /**
20
22
  * Throws if any of `values` is not of type number.
21
23
  */
22
24
  export declare function assertIsNumberArray(values: unknown, allowNaN?: boolean): asserts values is number[];
25
+ /**
26
+ * Throws if any of `values` is not of type `string`.
27
+ */
28
+ export declare function assertIsStringArray(values: unknown): asserts values is string[];
23
29
  /**
24
30
  * Throws an exception if `typeof value` is not a boolean.
25
31
  */
@@ -4,8 +4,10 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.assertUnreachable = assertUnreachable;
6
6
  exports.assertIsNumber = assertIsNumber;
7
+ exports.assertIsString = assertIsString;
7
8
  exports.assertIsArray = assertIsArray;
8
9
  exports.assertIsNumberArray = assertIsNumberArray;
10
+ exports.assertIsStringArray = assertIsStringArray;
9
11
  exports.assertIsBoolean = assertIsBoolean;
10
12
  exports.assertTruthy = assertTruthy;
11
13
  exports.assertIsObject = assertIsObject;
@@ -33,6 +35,12 @@ function assertIsNumber(value, allowNaN = false) {
33
35
  throw new Error('Given value is not a number');
34
36
  }
35
37
  }
38
+ /** Throws an `Error` if the given `value` is not a `string`. */
39
+ function assertIsString(value) {
40
+ if (typeof value !== 'string') {
41
+ throw new Error('Given value is not a string');
42
+ }
43
+ }
36
44
  function assertIsArray(values) {
37
45
  if (!Array.isArray(values)) {
38
46
  throw new Error('Asserting isArray: Given entity is not an array');
@@ -48,6 +56,16 @@ function assertIsNumberArray(values, allowNaN = false) {
48
56
  assertIsNumber(value, allowNaN);
49
57
  }
50
58
  }
59
+ /**
60
+ * Throws if any of `values` is not of type `string`.
61
+ */
62
+ function assertIsStringArray(values) {
63
+ assertIsArray(values);
64
+ assertIsNumber(values.length);
65
+ for (const value of values) {
66
+ assertIsString(value);
67
+ }
68
+ }
51
69
  /**
52
70
  * Throws an exception if `typeof value` is not a boolean.
53
71
  */
@@ -0,0 +1,12 @@
1
+ import { Mat33, Vec2 } from '@js-draw/math';
2
+ interface Descriptions {
3
+ zoomedIn: string;
4
+ zoomedOut: string;
5
+ movedLeft: string;
6
+ movedRight: string;
7
+ movedUp: string;
8
+ movedDown: string;
9
+ rotatedBy: (deg: number) => string;
10
+ }
11
+ declare const describeTransformation: (origin: Vec2, transform: Mat33, invertDirections: boolean, localizationTable: Descriptions) => string;
12
+ export default describeTransformation;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const math_1 = require("@js-draw/math");
4
+ const describeTransformation = (
5
+ // The location of the object before being transformed
6
+ origin,
7
+ // The transformation
8
+ transform,
9
+ // If true, moving the object right, for example, reads as "moved left"
10
+ invertDirections, localizationTable) => {
11
+ // Describe the transformation's affect on the viewport (note that transformation transforms
12
+ // the **elements** within the viewport). Assumes the transformation only does rotation/scale/translation.
13
+ const linearTransformedVec = transform.transformVec3(math_1.Vec2.unitX);
14
+ const affineTransformedVec = transform.transformVec2(origin);
15
+ const scale = linearTransformedVec.magnitude();
16
+ const clockwiseRotation = -(180 / Math.PI) * linearTransformedVec.angle();
17
+ const translation = affineTransformedVec.minus(origin);
18
+ const result = [];
19
+ if (scale > 1.2) {
20
+ result.push(localizationTable.zoomedIn);
21
+ }
22
+ else if (scale < 0.8) {
23
+ result.push(localizationTable.zoomedOut);
24
+ }
25
+ if (Math.floor(Math.abs(clockwiseRotation)) > 0) {
26
+ const roundedRotation = Math.round(invertDirections ? -clockwiseRotation : clockwiseRotation);
27
+ result.push(localizationTable.rotatedBy(roundedRotation));
28
+ }
29
+ const minTranslation = 1e-4;
30
+ if (translation.x > minTranslation) {
31
+ result.push(invertDirections ? localizationTable.movedLeft : localizationTable.movedRight);
32
+ }
33
+ else if (translation.x < -minTranslation) {
34
+ result.push(invertDirections ? localizationTable.movedRight : localizationTable.movedLeft);
35
+ }
36
+ if (translation.y < -minTranslation) {
37
+ result.push(invertDirections ? localizationTable.movedDown : localizationTable.movedUp);
38
+ }
39
+ else if (translation.y > minTranslation) {
40
+ result.push(invertDirections ? localizationTable.movedUp : localizationTable.movedDown);
41
+ }
42
+ return result.join('; ');
43
+ };
44
+ exports.default = describeTransformation;
@@ -7,5 +7,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  */
8
8
  exports.default = {
9
9
  // Note: Auto-updated by prebuild.js:
10
- number: '1.27.2',
10
+ number: '1.28.0',
11
11
  };
@@ -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
  /**