js-draw 1.16.1 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. package/dist/bundle.js +2 -2
  2. package/dist/bundledStyles.js +1 -1
  3. package/dist/cjs/Editor.d.ts +76 -6
  4. package/dist/cjs/Editor.js +89 -89
  5. package/dist/cjs/Pointer.d.ts +2 -1
  6. package/dist/cjs/Pointer.js +9 -2
  7. package/dist/cjs/commands/localization.d.ts +1 -0
  8. package/dist/cjs/commands/localization.js +1 -0
  9. package/dist/cjs/commands/uniteCommands.d.ts +5 -1
  10. package/dist/cjs/commands/uniteCommands.js +33 -7
  11. package/dist/cjs/components/TextComponent.d.ts +36 -1
  12. package/dist/cjs/components/TextComponent.js +39 -1
  13. package/dist/cjs/components/builders/ArrowBuilder.js +1 -1
  14. package/dist/cjs/components/builders/PolylineBuilder.d.ts +35 -0
  15. package/dist/cjs/components/builders/PolylineBuilder.js +115 -0
  16. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
  17. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +1 -1
  18. package/dist/cjs/components/lib.d.ts +1 -0
  19. package/dist/cjs/components/lib.js +3 -1
  20. package/dist/cjs/components/util/StrokeSmoother.js +4 -4
  21. package/dist/cjs/image/EditorImage.d.ts +4 -1
  22. package/dist/cjs/image/EditorImage.js +4 -1
  23. package/dist/cjs/inputEvents.d.ts +11 -1
  24. package/dist/cjs/localizations/comments.d.ts +3 -0
  25. package/dist/cjs/localizations/comments.js +3 -0
  26. package/dist/cjs/localizations/de.js +0 -2
  27. package/dist/cjs/localizations/es.js +2 -2
  28. package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  29. package/dist/cjs/rendering/renderers/CanvasRenderer.js +16 -0
  30. package/dist/cjs/rendering/renderers/SVGRenderer.js +1 -1
  31. package/dist/cjs/toolbar/IconProvider.d.ts +6 -3
  32. package/dist/cjs/toolbar/IconProvider.js +6 -4
  33. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
  34. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  35. package/dist/cjs/toolbar/widgets/PenToolWidget.js +7 -1
  36. package/dist/cjs/tools/Eraser.js +1 -1
  37. package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
  38. package/dist/cjs/tools/PasteHandler.js +36 -10
  39. package/dist/cjs/tools/Pen.js +2 -2
  40. package/dist/cjs/tools/SelectionTool/SelectionTool.js +23 -4
  41. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +1 -1
  42. package/dist/cjs/tools/ToolController.d.ts +17 -1
  43. package/dist/cjs/tools/ToolController.js +21 -8
  44. package/dist/cjs/tools/localization.d.ts +2 -2
  45. package/dist/cjs/tools/localization.js +2 -2
  46. package/dist/cjs/util/ClipboardHandler.d.ts +27 -0
  47. package/dist/cjs/util/ClipboardHandler.js +205 -0
  48. package/dist/cjs/util/ClipboardHandler.test.d.ts +1 -0
  49. package/dist/cjs/version.d.ts +5 -0
  50. package/dist/cjs/version.js +6 -1
  51. package/dist/mjs/Editor.d.ts +76 -6
  52. package/dist/mjs/Editor.mjs +89 -89
  53. package/dist/mjs/Pointer.d.ts +2 -1
  54. package/dist/mjs/Pointer.mjs +9 -2
  55. package/dist/mjs/commands/localization.d.ts +1 -0
  56. package/dist/mjs/commands/localization.mjs +1 -0
  57. package/dist/mjs/commands/uniteCommands.d.ts +5 -1
  58. package/dist/mjs/commands/uniteCommands.mjs +33 -7
  59. package/dist/mjs/components/TextComponent.d.ts +36 -1
  60. package/dist/mjs/components/TextComponent.mjs +40 -2
  61. package/dist/mjs/components/builders/ArrowBuilder.mjs +1 -1
  62. package/dist/mjs/components/builders/PolylineBuilder.d.ts +35 -0
  63. package/dist/mjs/components/builders/PolylineBuilder.mjs +108 -0
  64. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +1 -1
  65. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +1 -1
  66. package/dist/mjs/components/lib.d.ts +1 -0
  67. package/dist/mjs/components/lib.mjs +1 -0
  68. package/dist/mjs/components/util/StrokeSmoother.mjs +4 -4
  69. package/dist/mjs/image/EditorImage.d.ts +4 -1
  70. package/dist/mjs/image/EditorImage.mjs +4 -1
  71. package/dist/mjs/inputEvents.d.ts +11 -1
  72. package/dist/mjs/localizations/comments.d.ts +3 -0
  73. package/dist/mjs/localizations/comments.mjs +3 -0
  74. package/dist/mjs/localizations/de.mjs +0 -2
  75. package/dist/mjs/localizations/es.mjs +2 -2
  76. package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  77. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +16 -0
  78. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +1 -1
  79. package/dist/mjs/toolbar/IconProvider.d.ts +6 -3
  80. package/dist/mjs/toolbar/IconProvider.mjs +6 -4
  81. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
  82. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  83. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +7 -1
  84. package/dist/mjs/tools/Eraser.mjs +1 -1
  85. package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
  86. package/dist/mjs/tools/PasteHandler.mjs +36 -10
  87. package/dist/mjs/tools/Pen.mjs +2 -2
  88. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +23 -4
  89. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +1 -1
  90. package/dist/mjs/tools/ToolController.d.ts +17 -1
  91. package/dist/mjs/tools/ToolController.mjs +21 -8
  92. package/dist/mjs/tools/localization.d.ts +2 -2
  93. package/dist/mjs/tools/localization.mjs +2 -2
  94. package/dist/mjs/util/ClipboardHandler.d.ts +27 -0
  95. package/dist/mjs/util/ClipboardHandler.mjs +200 -0
  96. package/dist/mjs/util/ClipboardHandler.test.d.ts +1 -0
  97. package/dist/mjs/version.d.ts +5 -0
  98. package/dist/mjs/version.mjs +6 -1
  99. package/package.json +6 -6
@@ -42,7 +42,6 @@ const getLocalizationTable_1 = __importDefault(require("./localizations/getLocal
42
42
  const IconProvider_1 = __importDefault(require("./toolbar/IconProvider"));
43
43
  const CanvasRenderer_1 = __importDefault(require("./rendering/renderers/CanvasRenderer"));
44
44
  const untilNextAnimationFrame_1 = __importDefault(require("./util/untilNextAnimationFrame"));
45
- const fileToBase64Url_1 = __importDefault(require("./util/fileToBase64Url"));
46
45
  const uniteCommands_1 = __importDefault(require("./commands/uniteCommands"));
47
46
  const SelectionTool_1 = __importDefault(require("./tools/SelectionTool/SelectionTool"));
48
47
  const Erase_1 = __importDefault(require("./commands/Erase"));
@@ -58,6 +57,7 @@ const editorImageToSVG_1 = require("./image/export/editorImageToSVG");
58
57
  const ReactiveValue_1 = require("./util/ReactiveValue");
59
58
  const listenForKeyboardEventsFrom_1 = __importDefault(require("./util/listenForKeyboardEventsFrom"));
60
59
  const mitLicenseAttribution_1 = __importDefault(require("./util/mitLicenseAttribution"));
60
+ const ClipboardHandler_1 = __importDefault(require("./util/ClipboardHandler"));
61
61
  /**
62
62
  * The main entrypoint for the full editor.
63
63
  *
@@ -138,6 +138,7 @@ class Editor {
138
138
  iconProvider: settings.iconProvider ?? new IconProvider_1.default(),
139
139
  notices: [],
140
140
  appInfo: settings.appInfo ? { ...settings.appInfo } : null,
141
+ pens: { additionalPenTypes: settings.pens?.additionalPenTypes ?? [], },
141
142
  };
142
143
  // Validate settings
143
144
  if (this.settings.minZoom > this.settings.maxZoom) {
@@ -218,6 +219,16 @@ class Editor {
218
219
  }
219
220
  });
220
221
  }
222
+ /**
223
+ * @returns a shallow copy of the current settings of the editor.
224
+ *
225
+ * Do not modify.
226
+ */
227
+ getCurrentSettings() {
228
+ return {
229
+ ...this.settings,
230
+ };
231
+ }
221
232
  /**
222
233
  * @returns a reference to the editor's container.
223
234
  *
@@ -304,19 +315,12 @@ class Editor {
304
315
  this.accessibilityControlArea.addEventListener('input', () => {
305
316
  this.accessibilityControlArea.value = '';
306
317
  });
307
- document.addEventListener('copy', evt => {
318
+ const copyHandler = new ClipboardHandler_1.default(this);
319
+ document.addEventListener('copy', async (evt) => {
308
320
  if (!this.isEventSink(document.querySelector(':focus'))) {
309
321
  return;
310
322
  }
311
- const clipboardData = evt.clipboardData;
312
- if (this.toolController.dispatchInputEvent({
313
- kind: inputEvents_1.InputEvtType.CopyEvent,
314
- setData: (mime, data) => {
315
- clipboardData?.setData(mime, data);
316
- },
317
- })) {
318
- evt.preventDefault();
319
- }
323
+ copyHandler.copy(evt);
320
324
  });
321
325
  document.addEventListener('paste', evt => {
322
326
  this.handlePaste(evt);
@@ -384,11 +388,21 @@ class Editor {
384
388
  * (e.g. with synthetic events). @internal
385
389
  */
386
390
  setPointerCapture(target, pointerId) {
387
- target.setPointerCapture(pointerId);
391
+ try {
392
+ target.setPointerCapture(pointerId);
393
+ }
394
+ catch (error) {
395
+ console.warn('Failed to setPointerCapture', error);
396
+ }
388
397
  }
389
398
  /** Can be overridden in a testing environment to handle synthetic events. @internal */
390
399
  releasePointerCapture(target, pointerId) {
391
- target.releasePointerCapture(pointerId);
400
+ try {
401
+ target.releasePointerCapture(pointerId);
402
+ }
403
+ catch (error) {
404
+ console.warn('Failed to releasePointerCapture', error);
405
+ }
392
406
  }
393
407
  /**
394
408
  * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
@@ -414,7 +428,7 @@ class Editor {
414
428
  if (pointer.down) {
415
429
  const prevData = this.pointers[pointer.id];
416
430
  if (prevData) {
417
- const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
431
+ const distanceMoved = pointer.screenPos.distanceTo(prevData.screenPos);
418
432
  // If the pointer moved less than two pixels, don't send a new event.
419
433
  if (distanceMoved < 2) {
420
434
  return false;
@@ -473,66 +487,7 @@ class Editor {
473
487
  if (!this.isEventSink(target)) {
474
488
  return;
475
489
  }
476
- const clipboardData = evt.dataTransfer ?? evt.clipboardData;
477
- if (!clipboardData) {
478
- return;
479
- }
480
- // Handle SVG files (prefer to PNG/JPEG)
481
- for (const file of clipboardData.files) {
482
- if (file.type.toLowerCase() === 'image/svg+xml') {
483
- const text = await file.text();
484
- if (this.toolController.dispatchInputEvent({
485
- kind: inputEvents_1.InputEvtType.PasteEvent,
486
- mime: file.type,
487
- data: text,
488
- })) {
489
- evt.preventDefault();
490
- return;
491
- }
492
- }
493
- }
494
- // Handle image files.
495
- for (const file of clipboardData.files) {
496
- const fileType = file.type.toLowerCase();
497
- if (fileType === 'image/png' || fileType === 'image/jpg') {
498
- this.showLoadingWarning(0);
499
- const onprogress = (evt) => {
500
- this.showLoadingWarning(evt.loaded / evt.total);
501
- };
502
- try {
503
- const data = await (0, fileToBase64Url_1.default)(file, { onprogress });
504
- if (data && this.toolController.dispatchInputEvent({
505
- kind: inputEvents_1.InputEvtType.PasteEvent,
506
- mime: fileType,
507
- data: data,
508
- })) {
509
- evt.preventDefault();
510
- this.hideLoadingWarning();
511
- return;
512
- }
513
- }
514
- catch (e) {
515
- console.error('Error reading image:', e);
516
- }
517
- this.hideLoadingWarning();
518
- }
519
- }
520
- // Supported MIMEs for text data, in order of preference
521
- const supportedMIMEs = [
522
- 'image/svg+xml',
523
- 'text/plain',
524
- ];
525
- for (const mime of supportedMIMEs) {
526
- const data = clipboardData.getData(mime);
527
- if (data && this.toolController.dispatchInputEvent({
528
- kind: inputEvents_1.InputEvtType.PasteEvent,
529
- mime,
530
- data,
531
- })) {
532
- evt.preventDefault();
533
- return;
534
- }
535
- }
490
+ return await new ClipboardHandler_1.default(this).paste(evt);
536
491
  }
537
492
  /**
538
493
  * Forward pointer events from `elem` to this editor. Such that right-click/right-click drag
@@ -643,7 +598,7 @@ class Editor {
643
598
  const eventBuffer = gestureData[pointerId].eventBuffer;
644
599
  // Skip if the pointer hasn't moved enough to not be a "click".
645
600
  const strokeStartThreshold = 10;
646
- const isWithinClickThreshold = gestureStartPos && currentPos.minus(gestureStartPos).magnitude() < strokeStartThreshold;
601
+ const isWithinClickThreshold = gestureStartPos && currentPos.distanceTo(gestureStartPos) < strokeStartThreshold;
647
602
  if (isWithinClickThreshold && !gestureData[pointerId].hasMovedSignificantly) {
648
603
  eventBuffer.push([eventName, event]);
649
604
  sendToEditor = false;
@@ -757,7 +712,8 @@ class Editor {
757
712
  /** `apply` a command. `command` will be announced for accessibility. */
758
713
  dispatch(command, addToHistory = true) {
759
714
  const dispatchResult = this.dispatchNoAnnounce(command, addToHistory);
760
- this.announceForAccessibility(command.description(this, this.localization));
715
+ const commandDescription = command.description(this, this.localization);
716
+ this.announceForAccessibility(commandDescription);
761
717
  return dispatchResult;
762
718
  }
763
719
  /**
@@ -971,8 +927,11 @@ class Editor {
971
927
  * This is a convenience method that creates **and applies** a single command.
972
928
  *
973
929
  * If `selectComponents` is true (the default), the components are selected.
930
+ *
931
+ * `actionDescription`, if given, should be a screenreader-friendly description of the
932
+ * reason components were added (e.g. "pasted").
974
933
  */
975
- async addAndCenterComponents(components, selectComponents = true) {
934
+ async addAndCenterComponents(components, selectComponents = true, actionDescription) {
976
935
  let bbox = null;
977
936
  for (const component of components) {
978
937
  if (bbox) {
@@ -1003,7 +962,7 @@ class Editor {
1003
962
  commands.push(component.transformBy(transfm));
1004
963
  }
1005
964
  const applyChunkSize = 100;
1006
- await this.dispatch((0, uniteCommands_1.default)(commands, applyChunkSize), true);
965
+ await this.dispatch((0, uniteCommands_1.default)(commands, { applyChunkSize, description: actionDescription }), true);
1007
966
  if (selectComponents) {
1008
967
  for (const selectionTool of this.toolController.getMatchingTools(SelectionTool_1.default)) {
1009
968
  selectionTool.setEnabled(true);
@@ -1023,17 +982,7 @@ class Editor {
1023
982
  * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
1024
983
  */
1025
984
  toDataURL(format = 'image/png', outputSize) {
1026
- const canvas = document.createElement('canvas');
1027
- const importExportViewport = this.image.getImportExportViewport();
1028
- const exportRectSize = importExportViewport.getScreenRectSize();
1029
- const resolution = outputSize ?? exportRectSize;
1030
- canvas.width = resolution.x;
1031
- canvas.height = resolution.y;
1032
- const ctx = canvas.getContext('2d');
1033
- // Scale to ensure that the entire output is visible.
1034
- const scaleFactor = Math.min(resolution.x / exportRectSize.x, resolution.y / exportRectSize.y);
1035
- ctx.scale(scaleFactor, scaleFactor);
1036
- const renderer = new CanvasRenderer_1.default(ctx, importExportViewport);
985
+ const { element: canvas, renderer } = CanvasRenderer_1.default.fromViewport(this.image.getImportExportViewport(), { canvasSize: outputSize });
1037
986
  this.image.renderAll(renderer);
1038
987
  const dataURL = canvas.toDataURL(format);
1039
988
  return dataURL;
@@ -1126,8 +1075,59 @@ class Editor {
1126
1075
  }
1127
1076
  return background;
1128
1077
  }
1078
+ /**
1079
+ * This is a convenience method for adding or updating the {@link BackgroundComponent}
1080
+ * and {@link EditorImage.setAutoresizeEnabled} for the current image.
1081
+ *
1082
+ * If there are multiple {@link BackgroundComponent}s in the image, this only modifies
1083
+ * the topmost such element.
1084
+ *
1085
+ * **Example**:
1086
+ * ```ts,runnable
1087
+ * import { Editor, Color4, BackgroundComponentBackgroundType } from 'js-draw';
1088
+ * const editor = new Editor(document.body);
1089
+ * editor.dispatch(editor.setBackgroundStyle({
1090
+ * color: Color4.orange,
1091
+ * type: BackgroundComponentBackgroundType.Grid,
1092
+ * autoresize: true,
1093
+ * }));
1094
+ * ```
1095
+ *
1096
+ * To change the background size, see {@link EditorImage.setImportExportRect}.
1097
+ */
1098
+ setBackgroundStyle(style) {
1099
+ const originalBackground = this.getTopmostBackgroundComponent();
1100
+ const commands = [];
1101
+ if (originalBackground) {
1102
+ commands.push(new Erase_1.default([originalBackground]));
1103
+ }
1104
+ const originalType = originalBackground?.getBackgroundType?.() ?? BackgroundComponent_1.BackgroundType.None;
1105
+ const originalColor = originalBackground?.getStyle?.().color ?? math_1.Color4.transparent;
1106
+ const originalFillsScreen = this.image.getAutoresizeEnabled();
1107
+ const defaultType = (style.color && originalType === BackgroundComponent_1.BackgroundType.None ? BackgroundComponent_1.BackgroundType.SolidColor : originalType);
1108
+ const backgroundType = style.type ?? defaultType;
1109
+ const backgroundColor = style.color ?? originalColor;
1110
+ const fillsScreen = style.autoresize ?? originalFillsScreen;
1111
+ if (backgroundType !== BackgroundComponent_1.BackgroundType.None) {
1112
+ const newBackground = new BackgroundComponent_1.default(backgroundType, backgroundColor);
1113
+ commands.push(EditorImage_1.default.addElement(newBackground));
1114
+ }
1115
+ if (fillsScreen !== originalFillsScreen) {
1116
+ commands.push(this.image.setAutoresizeEnabled(fillsScreen));
1117
+ // Avoid 0x0 backgrounds
1118
+ if (!fillsScreen && this.image.getImportExportRect().maxDimension === 0) {
1119
+ commands.push(this.image.setImportExportRect(this.image.getImportExportRect().resizedTo(math_1.Vec2.of(500, 500))));
1120
+ }
1121
+ }
1122
+ return (0, uniteCommands_1.default)(commands);
1123
+ }
1129
1124
  /**
1130
1125
  * Set the background color of the image.
1126
+ *
1127
+ * This is a convenience method for adding or updating the {@link BackgroundComponent}
1128
+ * for the current image.
1129
+ *
1130
+ * @see {@link setBackgroundStyle}
1131
1131
  */
1132
1132
  setBackgroundColor(color) {
1133
1133
  let background = this.getTopmostBackgroundComponent();
@@ -35,5 +35,6 @@ export default class Pointer {
35
35
  */
36
36
  withCanvasPosition(canvasPos: Point2, viewport: Viewport): Pointer;
37
37
  static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer;
38
- static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
38
+ static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null, timeStamp?: number | null): Pointer;
39
+ static ofScreenPoint(screenPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null, timeStamp?: number | null): Pointer;
39
40
  }
@@ -113,9 +113,16 @@ class Pointer {
113
113
  }
114
114
  // Create a new Pointer from a point on the canvas.
115
115
  // Intended for unit tests.
116
- static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null) {
116
+ static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null, timeStamp = null) {
117
117
  const screenPos = viewport.canvasToScreen(canvasPos);
118
- const timeStamp = performance.now();
118
+ timeStamp ??= performance.now();
119
+ return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
120
+ }
121
+ // Create a new Pointer from a point on the screen.
122
+ // Intended for unit tests.
123
+ static ofScreenPoint(screenPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null, timeStamp = null) {
124
+ const canvasPos = viewport.screenToCanvas(screenPos);
125
+ timeStamp ??= performance.now();
119
126
  return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
120
127
  }
121
128
  }
@@ -20,6 +20,7 @@ export interface CommandLocalization {
20
20
  duplicateAction: (elemDescription: string, count: number) => string;
21
21
  inverseOf: (actionDescription: string) => string;
22
22
  unionOf: (actionDescription: string, actionCount: number) => string;
23
+ andNMoreCommands: (count: number) => string;
23
24
  selectedElements: (count: number) => string;
24
25
  }
25
26
  export declare const defaultCommandLocalization: CommandLocalization;
@@ -22,5 +22,6 @@ exports.defaultCommandLocalization = {
22
22
  movedRight: 'Moved right',
23
23
  zoomedOut: 'Zoomed out',
24
24
  zoomedIn: 'Zoomed in',
25
+ andNMoreCommands: (count) => `And ${count} more commands.`,
25
26
  selectedElements: (count) => `Selected ${count} element${count === 1 ? '' : 's'}`,
26
27
  };
@@ -1,5 +1,9 @@
1
1
  import Command from './Command';
2
2
  import SerializableCommand from './SerializableCommand';
3
+ export interface UniteCommandsOptions {
4
+ applyChunkSize?: number;
5
+ description?: string;
6
+ }
3
7
  /**
4
8
  * Creates a single command from `commands`. This is useful when undoing should undo *all* commands
5
9
  * in `commands` at once, rather than one at a time.
@@ -36,5 +40,5 @@ import SerializableCommand from './SerializableCommand';
36
40
  * // applying them shouldn't be done all at once (which would block the UI).
37
41
  * ```
38
42
  */
39
- declare const uniteCommands: <T extends Command>(commands: T[], applyChunkSize?: number) => T extends SerializableCommand ? SerializableCommand : Command;
43
+ declare const uniteCommands: <T extends Command>(commands: T[], options?: UniteCommandsOptions | number) => T extends SerializableCommand ? SerializableCommand : Command;
40
44
  export default uniteCommands;
@@ -7,10 +7,11 @@ const waitForAll_1 = __importDefault(require("../util/waitForAll"));
7
7
  const Command_1 = __importDefault(require("./Command"));
8
8
  const SerializableCommand_1 = __importDefault(require("./SerializableCommand"));
9
9
  class NonSerializableUnion extends Command_1.default {
10
- constructor(commands, applyChunkSize) {
10
+ constructor(commands, applyChunkSize, descriptionOverride) {
11
11
  super();
12
12
  this.commands = commands;
13
13
  this.applyChunkSize = applyChunkSize;
14
+ this.descriptionOverride = descriptionOverride;
14
15
  }
15
16
  apply(editor) {
16
17
  if (this.applyChunkSize === undefined) {
@@ -36,9 +37,13 @@ class NonSerializableUnion extends Command_1.default {
36
37
  this.commands.forEach(command => command.onDrop(editor));
37
38
  }
38
39
  description(editor, localizationTable) {
40
+ if (this.descriptionOverride) {
41
+ return this.descriptionOverride;
42
+ }
39
43
  const descriptions = [];
40
44
  let lastDescription = null;
41
45
  let duplicateDescriptionCount = 0;
46
+ let handledCommandCount = 0;
42
47
  for (const part of this.commands) {
43
48
  const description = part.description(editor, localizationTable);
44
49
  if (description !== lastDescription && lastDescription !== null) {
@@ -47,7 +52,13 @@ class NonSerializableUnion extends Command_1.default {
47
52
  duplicateDescriptionCount = 0;
48
53
  }
49
54
  duplicateDescriptionCount++;
55
+ handledCommandCount++;
50
56
  lastDescription ??= description;
57
+ // Long descriptions aren't very useful to the user.
58
+ const maxDescriptionLength = 12;
59
+ if (descriptions.length > maxDescriptionLength) {
60
+ break;
61
+ }
51
62
  }
52
63
  if (duplicateDescriptionCount > 1) {
53
64
  descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
@@ -55,15 +66,19 @@ class NonSerializableUnion extends Command_1.default {
55
66
  else if (duplicateDescriptionCount === 1) {
56
67
  descriptions.push(lastDescription);
57
68
  }
69
+ if (handledCommandCount < this.commands.length) {
70
+ descriptions.push(localizationTable.andNMoreCommands(this.commands.length - handledCommandCount));
71
+ }
58
72
  return descriptions.join(', ');
59
73
  }
60
74
  }
61
75
  class SerializableUnion extends SerializableCommand_1.default {
62
- constructor(commands, applyChunkSize) {
76
+ constructor(commands, applyChunkSize, descriptionOverride) {
63
77
  super('union');
64
78
  this.commands = commands;
65
79
  this.applyChunkSize = applyChunkSize;
66
- this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize);
80
+ this.descriptionOverride = descriptionOverride;
81
+ this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize, descriptionOverride);
67
82
  }
68
83
  serializeToJSON() {
69
84
  if (this.serializedData) {
@@ -72,6 +87,7 @@ class SerializableUnion extends SerializableCommand_1.default {
72
87
  return {
73
88
  applyChunkSize: this.applyChunkSize,
74
89
  data: this.commands.map(command => command.serialize()),
90
+ description: this.descriptionOverride,
75
91
  };
76
92
  }
77
93
  apply(editor) {
@@ -125,7 +141,7 @@ class SerializableUnion extends SerializableCommand_1.default {
125
141
  * // applying them shouldn't be done all at once (which would block the UI).
126
142
  * ```
127
143
  */
128
- const uniteCommands = (commands, applyChunkSize) => {
144
+ const uniteCommands = (commands, options) => {
129
145
  let allSerializable = true;
130
146
  for (const command of commands) {
131
147
  if (!(command instanceof SerializableCommand_1.default)) {
@@ -133,12 +149,21 @@ const uniteCommands = (commands, applyChunkSize) => {
133
149
  break;
134
150
  }
135
151
  }
152
+ let applyChunkSize;
153
+ let description;
154
+ if (typeof options === 'number') {
155
+ applyChunkSize = options;
156
+ }
157
+ else {
158
+ applyChunkSize = options?.applyChunkSize;
159
+ description = options?.description;
160
+ }
136
161
  if (!allSerializable) {
137
- return new NonSerializableUnion(commands, applyChunkSize);
162
+ return new NonSerializableUnion(commands, applyChunkSize, description);
138
163
  }
139
164
  else {
140
165
  const castedCommands = commands;
141
- return new SerializableUnion(castedCommands, applyChunkSize);
166
+ return new SerializableUnion(castedCommands, applyChunkSize, description);
142
167
  }
143
168
  };
144
169
  SerializableCommand_1.default.register('union', (data, editor) => {
@@ -149,10 +174,11 @@ SerializableCommand_1.default.register('union', (data, editor) => {
149
174
  if (typeof applyChunkSize !== 'number' && applyChunkSize !== undefined) {
150
175
  throw new Error('serialized applyChunkSize is neither undefined nor a number.');
151
176
  }
177
+ const description = typeof data.description === 'string' ? data.description : undefined;
152
178
  const commands = [];
153
179
  for (const part of data.data) {
154
180
  commands.push(SerializableCommand_1.default.deserialize(part, editor));
155
181
  }
156
- return uniteCommands(commands, applyChunkSize);
182
+ return uniteCommands(commands, { applyChunkSize, description });
157
183
  });
158
184
  exports.default = uniteCommands;
@@ -19,6 +19,41 @@ export declare enum TextTransformMode {
19
19
  type TextElement = TextComponent | string;
20
20
  /**
21
21
  * Displays text.
22
+ *
23
+ * A `TextComponent` is a collection of `TextElement`s (`string`s or {@link TextComponent}s).
24
+ *
25
+ * **Example**:
26
+ *
27
+ * ```ts,runnable
28
+ * import { Editor, TextComponent, Mat33, Vec2, Color4, TextRenderingStyle } from 'js-draw';
29
+ * const editor = new Editor(document.body);
30
+ * editor.dispatch(editor.setBackgroundStyle({ color: Color4.black, autoresize: true ));
31
+ * ---visible---
32
+ * /// Adding a simple TextComponent
33
+ * ///------------------------------
34
+ *
35
+ * const positioning1 = Mat33.translation(Vec2.of(10, 10));
36
+ * const style: TextRenderingStyle = {
37
+ * fontFamily: 'sans', size: 12, renderingStyle: { fill: Color4.green },
38
+ * };
39
+ *
40
+ * editor.dispatch(
41
+ * editor.image.addElement(new TextComponent(['Hello, world'], positioning1, style)),
42
+ * );
43
+ *
44
+ *
45
+ * /// Adding nested TextComponents
46
+ * ///-----------------------------
47
+ *
48
+ * // Add another TextComponent that contains text and a TextComponent. Observe that '[Test]'
49
+ * // is placed directly after 'Test'.
50
+ * const positioning2 = Mat33.translation(Vec2.of(10, 50));
51
+ * editor.dispatch(
52
+ * editor.image.addElement(
53
+ * new TextComponent([ new TextComponent(['Test'], positioning1, style), '[Test]' ], positioning2, style)
54
+ * ),
55
+ * );
56
+ * ```
22
57
  */
23
58
  export default class TextComponent extends AbstractComponent implements RestyleableComponent {
24
59
  protected readonly textObjects: Array<TextElement>;
@@ -32,7 +67,7 @@ export default class TextComponent extends AbstractComponent implements Restylea
32
67
  *
33
68
  * @see {@link fromLines}
34
69
  */
35
- constructor(textObjects: Array<TextElement>, transform: Mat33, style: TextRenderingStyle, transformMode?: TextTransformMode);
70
+ constructor(textObjects: Array<TextElement>, transform: Mat33, style?: TextRenderingStyle, transformMode?: TextTransformMode);
36
71
  static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextRenderingStyle): void;
37
72
  private static textMeasuringCtx;
38
73
  private static estimateTextDimens;
@@ -20,8 +20,46 @@ var TextTransformMode;
20
20
  /**Relatively positioned in the Y direction, absolutely positioned in the X direction. */
21
21
  TextTransformMode[TextTransformMode["RELATIVE_Y_ABSOLUTE_X"] = 3] = "RELATIVE_Y_ABSOLUTE_X";
22
22
  })(TextTransformMode || (exports.TextTransformMode = TextTransformMode = {}));
23
+ const defaultTextStyle = {
24
+ fontFamily: 'sans', size: 12, renderingStyle: { fill: math_1.Color4.purple },
25
+ };
23
26
  /**
24
27
  * Displays text.
28
+ *
29
+ * A `TextComponent` is a collection of `TextElement`s (`string`s or {@link TextComponent}s).
30
+ *
31
+ * **Example**:
32
+ *
33
+ * ```ts,runnable
34
+ * import { Editor, TextComponent, Mat33, Vec2, Color4, TextRenderingStyle } from 'js-draw';
35
+ * const editor = new Editor(document.body);
36
+ * editor.dispatch(editor.setBackgroundStyle({ color: Color4.black, autoresize: true ));
37
+ * ---visible---
38
+ * /// Adding a simple TextComponent
39
+ * ///------------------------------
40
+ *
41
+ * const positioning1 = Mat33.translation(Vec2.of(10, 10));
42
+ * const style: TextRenderingStyle = {
43
+ * fontFamily: 'sans', size: 12, renderingStyle: { fill: Color4.green },
44
+ * };
45
+ *
46
+ * editor.dispatch(
47
+ * editor.image.addElement(new TextComponent(['Hello, world'], positioning1, style)),
48
+ * );
49
+ *
50
+ *
51
+ * /// Adding nested TextComponents
52
+ * ///-----------------------------
53
+ *
54
+ * // Add another TextComponent that contains text and a TextComponent. Observe that '[Test]'
55
+ * // is placed directly after 'Test'.
56
+ * const positioning2 = Mat33.translation(Vec2.of(10, 50));
57
+ * editor.dispatch(
58
+ * editor.image.addElement(
59
+ * new TextComponent([ new TextComponent(['Test'], positioning1, style), '[Test]' ], positioning2, style)
60
+ * ),
61
+ * );
62
+ * ```
25
63
  */
26
64
  class TextComponent extends AbstractComponent_1.default {
27
65
  /**
@@ -31,7 +69,7 @@ class TextComponent extends AbstractComponent_1.default {
31
69
  */
32
70
  constructor(textObjects,
33
71
  // Transformation relative to this component's parent element.
34
- transform, style,
72
+ transform, style = defaultTextStyle,
35
73
  // @internal
36
74
  transformMode = TextTransformMode.ABSOLUTE_XY) {
37
75
  super(componentTypeId);
@@ -27,7 +27,7 @@ class ArrowBuilder {
27
27
  const lineStartPoint = this.startPoint.pos;
28
28
  const endPoint = this.endPoint.pos;
29
29
  const toEnd = endPoint.minus(lineStartPoint).normalized();
30
- const arrowLength = endPoint.minus(lineStartPoint).length();
30
+ const arrowLength = endPoint.distanceTo(lineStartPoint);
31
31
  // Ensure that the arrow tip is smaller than the arrow.
32
32
  const arrowTipSize = Math.min(this.getLineWidth(), arrowLength / 2);
33
33
  const startSize = this.startPoint.width / 2;
@@ -0,0 +1,35 @@
1
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
2
+ import RenderablePathSpec from '../../rendering/RenderablePathSpec';
3
+ import { Rect2 } from '@js-draw/math';
4
+ import Stroke from '../Stroke';
5
+ import Viewport from '../../Viewport';
6
+ import { StrokeDataPoint } from '../../types';
7
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
8
+ import RenderingStyle from '../../rendering/RenderingStyle';
9
+ /**
10
+ * Creates strokes from line segments rather than Bézier curves.
11
+ *
12
+ * @beta Output behavior may change significantly between versions. For now, intended for debugging.
13
+ */
14
+ export declare const makePolylineBuilder: ComponentBuilderFactory;
15
+ export default class PolylineBuilder implements ComponentBuilder {
16
+ private minFitAllowed;
17
+ private viewport;
18
+ private parts;
19
+ private bbox;
20
+ private averageWidth;
21
+ private widthAverageNumSamples;
22
+ private lastPoint;
23
+ private startPoint;
24
+ constructor(startPoint: StrokeDataPoint, minFitAllowed: number, viewport: Viewport);
25
+ getBBox(): Rect2;
26
+ protected getRenderingStyle(): RenderingStyle;
27
+ protected previewCurrentPath(): RenderablePathSpec;
28
+ protected previewFullPath(): RenderablePathSpec[];
29
+ preview(renderer: AbstractRenderer): void;
30
+ build(): Stroke;
31
+ private getMinFit;
32
+ private roundPoint;
33
+ private roundDistance;
34
+ addPoint(newPoint: StrokeDataPoint): void;
35
+ }