js-draw 1.16.0 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. package/dist/Editor.css +11 -0
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +79 -6
  5. package/dist/cjs/Editor.js +114 -91
  6. package/dist/cjs/Pointer.d.ts +2 -1
  7. package/dist/cjs/Pointer.js +9 -2
  8. package/dist/cjs/commands/localization.d.ts +1 -0
  9. package/dist/cjs/commands/localization.js +1 -0
  10. package/dist/cjs/commands/uniteCommands.d.ts +5 -1
  11. package/dist/cjs/commands/uniteCommands.js +33 -7
  12. package/dist/cjs/components/TextComponent.d.ts +36 -1
  13. package/dist/cjs/components/TextComponent.js +39 -1
  14. package/dist/cjs/components/builders/ArrowBuilder.js +1 -1
  15. package/dist/cjs/components/builders/PolylineBuilder.d.ts +35 -0
  16. package/dist/cjs/components/builders/PolylineBuilder.js +115 -0
  17. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
  18. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +1 -1
  19. package/dist/cjs/components/lib.d.ts +1 -0
  20. package/dist/cjs/components/lib.js +3 -1
  21. package/dist/cjs/components/util/StrokeSmoother.js +4 -4
  22. package/dist/cjs/image/EditorImage.d.ts +4 -1
  23. package/dist/cjs/image/EditorImage.js +4 -1
  24. package/dist/cjs/inputEvents.d.ts +11 -1
  25. package/dist/cjs/localizations/comments.d.ts +3 -0
  26. package/dist/cjs/localizations/comments.js +3 -0
  27. package/dist/cjs/localizations/de.js +0 -2
  28. package/dist/cjs/localizations/es.js +2 -2
  29. package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  30. package/dist/cjs/rendering/renderers/CanvasRenderer.js +16 -0
  31. package/dist/cjs/rendering/renderers/SVGRenderer.js +1 -1
  32. package/dist/cjs/toolbar/IconProvider.d.ts +6 -3
  33. package/dist/cjs/toolbar/IconProvider.js +6 -4
  34. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
  35. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  36. package/dist/cjs/toolbar/widgets/PenToolWidget.js +7 -1
  37. package/dist/cjs/tools/Eraser.js +1 -1
  38. package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
  39. package/dist/cjs/tools/PasteHandler.js +40 -10
  40. package/dist/cjs/tools/Pen.js +2 -2
  41. package/dist/cjs/tools/SelectionTool/SelectionTool.js +23 -4
  42. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +1 -1
  43. package/dist/cjs/tools/ToolController.d.ts +17 -1
  44. package/dist/cjs/tools/ToolController.js +21 -8
  45. package/dist/cjs/tools/localization.d.ts +2 -2
  46. package/dist/cjs/tools/localization.js +2 -2
  47. package/dist/cjs/util/ClipboardHandler.d.ts +27 -0
  48. package/dist/cjs/util/ClipboardHandler.js +205 -0
  49. package/dist/cjs/util/ClipboardHandler.test.d.ts +1 -0
  50. package/dist/cjs/version.d.ts +5 -0
  51. package/dist/cjs/version.js +6 -1
  52. package/dist/mjs/Editor.d.ts +79 -6
  53. package/dist/mjs/Editor.mjs +114 -91
  54. package/dist/mjs/Pointer.d.ts +2 -1
  55. package/dist/mjs/Pointer.mjs +9 -2
  56. package/dist/mjs/commands/localization.d.ts +1 -0
  57. package/dist/mjs/commands/localization.mjs +1 -0
  58. package/dist/mjs/commands/uniteCommands.d.ts +5 -1
  59. package/dist/mjs/commands/uniteCommands.mjs +33 -7
  60. package/dist/mjs/components/TextComponent.d.ts +36 -1
  61. package/dist/mjs/components/TextComponent.mjs +40 -2
  62. package/dist/mjs/components/builders/ArrowBuilder.mjs +1 -1
  63. package/dist/mjs/components/builders/PolylineBuilder.d.ts +35 -0
  64. package/dist/mjs/components/builders/PolylineBuilder.mjs +108 -0
  65. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +1 -1
  66. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +1 -1
  67. package/dist/mjs/components/lib.d.ts +1 -0
  68. package/dist/mjs/components/lib.mjs +1 -0
  69. package/dist/mjs/components/util/StrokeSmoother.mjs +4 -4
  70. package/dist/mjs/image/EditorImage.d.ts +4 -1
  71. package/dist/mjs/image/EditorImage.mjs +4 -1
  72. package/dist/mjs/inputEvents.d.ts +11 -1
  73. package/dist/mjs/localizations/comments.d.ts +3 -0
  74. package/dist/mjs/localizations/comments.mjs +3 -0
  75. package/dist/mjs/localizations/de.mjs +0 -2
  76. package/dist/mjs/localizations/es.mjs +2 -2
  77. package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  78. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +16 -0
  79. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +1 -1
  80. package/dist/mjs/toolbar/IconProvider.d.ts +6 -3
  81. package/dist/mjs/toolbar/IconProvider.mjs +6 -4
  82. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
  83. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  84. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +7 -1
  85. package/dist/mjs/tools/Eraser.mjs +1 -1
  86. package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
  87. package/dist/mjs/tools/PasteHandler.mjs +40 -10
  88. package/dist/mjs/tools/Pen.mjs +2 -2
  89. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +23 -4
  90. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +1 -1
  91. package/dist/mjs/tools/ToolController.d.ts +17 -1
  92. package/dist/mjs/tools/ToolController.mjs +21 -8
  93. package/dist/mjs/tools/localization.d.ts +2 -2
  94. package/dist/mjs/tools/localization.mjs +2 -2
  95. package/dist/mjs/util/ClipboardHandler.d.ts +27 -0
  96. package/dist/mjs/util/ClipboardHandler.mjs +200 -0
  97. package/dist/mjs/util/ClipboardHandler.test.d.ts +1 -0
  98. package/dist/mjs/version.d.ts +5 -0
  99. package/dist/mjs/version.mjs +6 -1
  100. package/package.json +6 -6
  101. package/src/Editor.scss +10 -0
  102. package/src/toolbar/EdgeToolbar.scss +2 -0
  103. package/src/tools/SoundUITool.scss +4 -1
@@ -13,7 +13,6 @@ import getLocalizationTable from './localizations/getLocalizationTable.mjs';
13
13
  import IconProvider from './toolbar/IconProvider.mjs';
14
14
  import CanvasRenderer from './rendering/renderers/CanvasRenderer.mjs';
15
15
  import untilNextAnimationFrame from './util/untilNextAnimationFrame.mjs';
16
- import fileToBase64Url from './util/fileToBase64Url.mjs';
17
16
  import uniteCommands from './commands/uniteCommands.mjs';
18
17
  import SelectionTool from './tools/SelectionTool/SelectionTool.mjs';
19
18
  import Erase from './commands/Erase.mjs';
@@ -29,6 +28,7 @@ import { editorImageToSVGSync, editorImageToSVGAsync } from './image/export/ed
29
28
  import { MutableReactiveValue } from './util/ReactiveValue.mjs';
30
29
  import listenForKeyboardEventsFrom from './util/listenForKeyboardEventsFrom.mjs';
31
30
  import mitLicenseAttribution from './util/mitLicenseAttribution.mjs';
31
+ import ClipboardHandler from './util/ClipboardHandler.mjs';
32
32
  /**
33
33
  * The main entrypoint for the full editor.
34
34
  *
@@ -109,6 +109,7 @@ export class Editor {
109
109
  iconProvider: settings.iconProvider ?? new IconProvider(),
110
110
  notices: [],
111
111
  appInfo: settings.appInfo ? { ...settings.appInfo } : null,
112
+ pens: { additionalPenTypes: settings.pens?.additionalPenTypes ?? [], },
112
113
  };
113
114
  // Validate settings
114
115
  if (this.settings.minZoom > this.settings.maxZoom) {
@@ -189,6 +190,16 @@ export class Editor {
189
190
  }
190
191
  });
191
192
  }
193
+ /**
194
+ * @returns a shallow copy of the current settings of the editor.
195
+ *
196
+ * Do not modify.
197
+ */
198
+ getCurrentSettings() {
199
+ return {
200
+ ...this.settings,
201
+ };
202
+ }
192
203
  /**
193
204
  * @returns a reference to the editor's container.
194
205
  *
@@ -238,6 +249,24 @@ export class Editor {
238
249
  this.handlePointerEventsFrom(this.renderingRegion);
239
250
  this.handleKeyEventsFrom(this.renderingRegion);
240
251
  this.handlePointerEventsFrom(this.accessibilityAnnounceArea);
252
+ // Prevent selected text from control areas from being dragged.
253
+ // See https://github.com/personalizedrefrigerator/joplin-plugin-freehand-drawing/issues/8
254
+ const preventSelectionOf = [
255
+ this.renderingRegion,
256
+ this.accessibilityAnnounceArea,
257
+ this.accessibilityControlArea,
258
+ this.loadingWarning,
259
+ ];
260
+ for (const element of preventSelectionOf) {
261
+ element.addEventListener('drag', event => {
262
+ event.preventDefault();
263
+ return false;
264
+ });
265
+ element.addEventListener('dragstart', event => {
266
+ event.preventDefault();
267
+ return false;
268
+ });
269
+ }
241
270
  this.container.addEventListener('wheel', evt => {
242
271
  this.handleHTMLWheelEvent(evt);
243
272
  });
@@ -257,19 +286,12 @@ export class Editor {
257
286
  this.accessibilityControlArea.addEventListener('input', () => {
258
287
  this.accessibilityControlArea.value = '';
259
288
  });
260
- document.addEventListener('copy', evt => {
289
+ const copyHandler = new ClipboardHandler(this);
290
+ document.addEventListener('copy', async (evt) => {
261
291
  if (!this.isEventSink(document.querySelector(':focus'))) {
262
292
  return;
263
293
  }
264
- const clipboardData = evt.clipboardData;
265
- if (this.toolController.dispatchInputEvent({
266
- kind: InputEvtType.CopyEvent,
267
- setData: (mime, data) => {
268
- clipboardData?.setData(mime, data);
269
- },
270
- })) {
271
- evt.preventDefault();
272
- }
294
+ copyHandler.copy(evt);
273
295
  });
274
296
  document.addEventListener('paste', evt => {
275
297
  this.handlePaste(evt);
@@ -337,11 +359,21 @@ export class Editor {
337
359
  * (e.g. with synthetic events). @internal
338
360
  */
339
361
  setPointerCapture(target, pointerId) {
340
- target.setPointerCapture(pointerId);
362
+ try {
363
+ target.setPointerCapture(pointerId);
364
+ }
365
+ catch (error) {
366
+ console.warn('Failed to setPointerCapture', error);
367
+ }
341
368
  }
342
369
  /** Can be overridden in a testing environment to handle synthetic events. @internal */
343
370
  releasePointerCapture(target, pointerId) {
344
- target.releasePointerCapture(pointerId);
371
+ try {
372
+ target.releasePointerCapture(pointerId);
373
+ }
374
+ catch (error) {
375
+ console.warn('Failed to releasePointerCapture', error);
376
+ }
345
377
  }
346
378
  /**
347
379
  * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
@@ -367,7 +399,7 @@ export class Editor {
367
399
  if (pointer.down) {
368
400
  const prevData = this.pointers[pointer.id];
369
401
  if (prevData) {
370
- const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
402
+ const distanceMoved = pointer.screenPos.distanceTo(prevData.screenPos);
371
403
  // If the pointer moved less than two pixels, don't send a new event.
372
404
  if (distanceMoved < 2) {
373
405
  return false;
@@ -415,71 +447,18 @@ export class Editor {
415
447
  }
416
448
  return false;
417
449
  }
450
+ /** @internal */
451
+ async handleDrop(evt) {
452
+ evt.preventDefault();
453
+ this.handlePaste(evt);
454
+ }
455
+ /** @internal */
418
456
  async handlePaste(evt) {
419
457
  const target = document.querySelector(':focus') ?? evt.target;
420
458
  if (!this.isEventSink(target)) {
421
459
  return;
422
460
  }
423
- const clipboardData = evt.dataTransfer ?? evt.clipboardData;
424
- if (!clipboardData) {
425
- return;
426
- }
427
- // Handle SVG files (prefer to PNG/JPEG)
428
- for (const file of clipboardData.files) {
429
- if (file.type.toLowerCase() === 'image/svg+xml') {
430
- const text = await file.text();
431
- if (this.toolController.dispatchInputEvent({
432
- kind: InputEvtType.PasteEvent,
433
- mime: file.type,
434
- data: text,
435
- })) {
436
- evt.preventDefault();
437
- return;
438
- }
439
- }
440
- }
441
- // Handle image files.
442
- for (const file of clipboardData.files) {
443
- const fileType = file.type.toLowerCase();
444
- if (fileType === 'image/png' || fileType === 'image/jpg') {
445
- this.showLoadingWarning(0);
446
- const onprogress = (evt) => {
447
- this.showLoadingWarning(evt.loaded / evt.total);
448
- };
449
- try {
450
- const data = await fileToBase64Url(file, { onprogress });
451
- if (data && this.toolController.dispatchInputEvent({
452
- kind: InputEvtType.PasteEvent,
453
- mime: fileType,
454
- data: data,
455
- })) {
456
- evt.preventDefault();
457
- this.hideLoadingWarning();
458
- return;
459
- }
460
- }
461
- catch (e) {
462
- console.error('Error reading image:', e);
463
- }
464
- this.hideLoadingWarning();
465
- }
466
- }
467
- // Supported MIMEs for text data, in order of preference
468
- const supportedMIMEs = [
469
- 'image/svg+xml',
470
- 'text/plain',
471
- ];
472
- for (const mime of supportedMIMEs) {
473
- const data = clipboardData.getData(mime);
474
- if (data && this.toolController.dispatchInputEvent({
475
- kind: InputEvtType.PasteEvent,
476
- mime,
477
- data,
478
- })) {
479
- evt.preventDefault();
480
- return;
481
- }
482
- }
461
+ return await new ClipboardHandler(this).paste(evt);
483
462
  }
484
463
  /**
485
464
  * Forward pointer events from `elem` to this editor. Such that right-click/right-click drag
@@ -590,7 +569,7 @@ export class Editor {
590
569
  const eventBuffer = gestureData[pointerId].eventBuffer;
591
570
  // Skip if the pointer hasn't moved enough to not be a "click".
592
571
  const strokeStartThreshold = 10;
593
- const isWithinClickThreshold = gestureStartPos && currentPos.minus(gestureStartPos).magnitude() < strokeStartThreshold;
572
+ const isWithinClickThreshold = gestureStartPos && currentPos.distanceTo(gestureStartPos) < strokeStartThreshold;
594
573
  if (isWithinClickThreshold && !gestureData[pointerId].hasMovedSignificantly) {
595
574
  eventBuffer.push([eventName, event]);
596
575
  sendToEditor = false;
@@ -677,8 +656,7 @@ export class Editor {
677
656
  evt.preventDefault();
678
657
  };
679
658
  elem.ondrop = evt => {
680
- evt.preventDefault();
681
- this.handlePaste(evt);
659
+ this.handleDrop(evt);
682
660
  };
683
661
  this.eventListenerTargets.push(elem);
684
662
  }
@@ -705,7 +683,8 @@ export class Editor {
705
683
  /** `apply` a command. `command` will be announced for accessibility. */
706
684
  dispatch(command, addToHistory = true) {
707
685
  const dispatchResult = this.dispatchNoAnnounce(command, addToHistory);
708
- this.announceForAccessibility(command.description(this, this.localization));
686
+ const commandDescription = command.description(this, this.localization);
687
+ this.announceForAccessibility(commandDescription);
709
688
  return dispatchResult;
710
689
  }
711
690
  /**
@@ -919,8 +898,11 @@ export class Editor {
919
898
  * This is a convenience method that creates **and applies** a single command.
920
899
  *
921
900
  * If `selectComponents` is true (the default), the components are selected.
901
+ *
902
+ * `actionDescription`, if given, should be a screenreader-friendly description of the
903
+ * reason components were added (e.g. "pasted").
922
904
  */
923
- async addAndCenterComponents(components, selectComponents = true) {
905
+ async addAndCenterComponents(components, selectComponents = true, actionDescription) {
924
906
  let bbox = null;
925
907
  for (const component of components) {
926
908
  if (bbox) {
@@ -951,7 +933,7 @@ export class Editor {
951
933
  commands.push(component.transformBy(transfm));
952
934
  }
953
935
  const applyChunkSize = 100;
954
- await this.dispatch(uniteCommands(commands, applyChunkSize), true);
936
+ await this.dispatch(uniteCommands(commands, { applyChunkSize, description: actionDescription }), true);
955
937
  if (selectComponents) {
956
938
  for (const selectionTool of this.toolController.getMatchingTools(SelectionTool)) {
957
939
  selectionTool.setEnabled(true);
@@ -971,17 +953,7 @@ export class Editor {
971
953
  * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
972
954
  */
973
955
  toDataURL(format = 'image/png', outputSize) {
974
- const canvas = document.createElement('canvas');
975
- const importExportViewport = this.image.getImportExportViewport();
976
- const exportRectSize = importExportViewport.getScreenRectSize();
977
- const resolution = outputSize ?? exportRectSize;
978
- canvas.width = resolution.x;
979
- canvas.height = resolution.y;
980
- const ctx = canvas.getContext('2d');
981
- // Scale to ensure that the entire output is visible.
982
- const scaleFactor = Math.min(resolution.x / exportRectSize.x, resolution.y / exportRectSize.y);
983
- ctx.scale(scaleFactor, scaleFactor);
984
- const renderer = new CanvasRenderer(ctx, importExportViewport);
956
+ const { element: canvas, renderer } = CanvasRenderer.fromViewport(this.image.getImportExportViewport(), { canvasSize: outputSize });
985
957
  this.image.renderAll(renderer);
986
958
  const dataURL = canvas.toDataURL(format);
987
959
  return dataURL;
@@ -1074,8 +1046,59 @@ export class Editor {
1074
1046
  }
1075
1047
  return background;
1076
1048
  }
1049
+ /**
1050
+ * This is a convenience method for adding or updating the {@link BackgroundComponent}
1051
+ * and {@link EditorImage.setAutoresizeEnabled} for the current image.
1052
+ *
1053
+ * If there are multiple {@link BackgroundComponent}s in the image, this only modifies
1054
+ * the topmost such element.
1055
+ *
1056
+ * **Example**:
1057
+ * ```ts,runnable
1058
+ * import { Editor, Color4, BackgroundComponentBackgroundType } from 'js-draw';
1059
+ * const editor = new Editor(document.body);
1060
+ * editor.dispatch(editor.setBackgroundStyle({
1061
+ * color: Color4.orange,
1062
+ * type: BackgroundComponentBackgroundType.Grid,
1063
+ * autoresize: true,
1064
+ * }));
1065
+ * ```
1066
+ *
1067
+ * To change the background size, see {@link EditorImage.setImportExportRect}.
1068
+ */
1069
+ setBackgroundStyle(style) {
1070
+ const originalBackground = this.getTopmostBackgroundComponent();
1071
+ const commands = [];
1072
+ if (originalBackground) {
1073
+ commands.push(new Erase([originalBackground]));
1074
+ }
1075
+ const originalType = originalBackground?.getBackgroundType?.() ?? BackgroundType.None;
1076
+ const originalColor = originalBackground?.getStyle?.().color ?? Color4.transparent;
1077
+ const originalFillsScreen = this.image.getAutoresizeEnabled();
1078
+ const defaultType = (style.color && originalType === BackgroundType.None ? BackgroundType.SolidColor : originalType);
1079
+ const backgroundType = style.type ?? defaultType;
1080
+ const backgroundColor = style.color ?? originalColor;
1081
+ const fillsScreen = style.autoresize ?? originalFillsScreen;
1082
+ if (backgroundType !== BackgroundType.None) {
1083
+ const newBackground = new BackgroundComponent(backgroundType, backgroundColor);
1084
+ commands.push(EditorImage.addElement(newBackground));
1085
+ }
1086
+ if (fillsScreen !== originalFillsScreen) {
1087
+ commands.push(this.image.setAutoresizeEnabled(fillsScreen));
1088
+ // Avoid 0x0 backgrounds
1089
+ if (!fillsScreen && this.image.getImportExportRect().maxDimension === 0) {
1090
+ commands.push(this.image.setImportExportRect(this.image.getImportExportRect().resizedTo(Vec2.of(500, 500))));
1091
+ }
1092
+ }
1093
+ return uniteCommands(commands);
1094
+ }
1077
1095
  /**
1078
1096
  * Set the background color of the image.
1097
+ *
1098
+ * This is a convenience method for adding or updating the {@link BackgroundComponent}
1099
+ * for the current image.
1100
+ *
1101
+ * @see {@link setBackgroundStyle}
1079
1102
  */
1080
1103
  setBackgroundColor(color) {
1081
1104
  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
  }
@@ -110,9 +110,16 @@ export default class Pointer {
110
110
  }
111
111
  // Create a new Pointer from a point on the canvas.
112
112
  // Intended for unit tests.
113
- static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null) {
113
+ static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null, timeStamp = null) {
114
114
  const screenPos = viewport.canvasToScreen(canvasPos);
115
- const timeStamp = performance.now();
115
+ timeStamp ??= performance.now();
116
+ return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
117
+ }
118
+ // Create a new Pointer from a point on the screen.
119
+ // Intended for unit tests.
120
+ static ofScreenPoint(screenPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null, timeStamp = null) {
121
+ const canvasPos = viewport.screenToCanvas(screenPos);
122
+ timeStamp ??= performance.now();
116
123
  return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
117
124
  }
118
125
  }
@@ -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;
@@ -19,5 +19,6 @@ export const defaultCommandLocalization = {
19
19
  movedRight: 'Moved right',
20
20
  zoomedOut: 'Zoomed out',
21
21
  zoomedIn: 'Zoomed in',
22
+ andNMoreCommands: (count) => `And ${count} more commands.`,
22
23
  selectedElements: (count) => `Selected ${count} element${count === 1 ? '' : 's'}`,
23
24
  };
@@ -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;
@@ -2,10 +2,11 @@ import waitForAll from '../util/waitForAll.mjs';
2
2
  import Command from './Command.mjs';
3
3
  import SerializableCommand from './SerializableCommand.mjs';
4
4
  class NonSerializableUnion extends Command {
5
- constructor(commands, applyChunkSize) {
5
+ constructor(commands, applyChunkSize, descriptionOverride) {
6
6
  super();
7
7
  this.commands = commands;
8
8
  this.applyChunkSize = applyChunkSize;
9
+ this.descriptionOverride = descriptionOverride;
9
10
  }
10
11
  apply(editor) {
11
12
  if (this.applyChunkSize === undefined) {
@@ -31,9 +32,13 @@ class NonSerializableUnion extends Command {
31
32
  this.commands.forEach(command => command.onDrop(editor));
32
33
  }
33
34
  description(editor, localizationTable) {
35
+ if (this.descriptionOverride) {
36
+ return this.descriptionOverride;
37
+ }
34
38
  const descriptions = [];
35
39
  let lastDescription = null;
36
40
  let duplicateDescriptionCount = 0;
41
+ let handledCommandCount = 0;
37
42
  for (const part of this.commands) {
38
43
  const description = part.description(editor, localizationTable);
39
44
  if (description !== lastDescription && lastDescription !== null) {
@@ -42,7 +47,13 @@ class NonSerializableUnion extends Command {
42
47
  duplicateDescriptionCount = 0;
43
48
  }
44
49
  duplicateDescriptionCount++;
50
+ handledCommandCount++;
45
51
  lastDescription ??= description;
52
+ // Long descriptions aren't very useful to the user.
53
+ const maxDescriptionLength = 12;
54
+ if (descriptions.length > maxDescriptionLength) {
55
+ break;
56
+ }
46
57
  }
47
58
  if (duplicateDescriptionCount > 1) {
48
59
  descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
@@ -50,15 +61,19 @@ class NonSerializableUnion extends Command {
50
61
  else if (duplicateDescriptionCount === 1) {
51
62
  descriptions.push(lastDescription);
52
63
  }
64
+ if (handledCommandCount < this.commands.length) {
65
+ descriptions.push(localizationTable.andNMoreCommands(this.commands.length - handledCommandCount));
66
+ }
53
67
  return descriptions.join(', ');
54
68
  }
55
69
  }
56
70
  class SerializableUnion extends SerializableCommand {
57
- constructor(commands, applyChunkSize) {
71
+ constructor(commands, applyChunkSize, descriptionOverride) {
58
72
  super('union');
59
73
  this.commands = commands;
60
74
  this.applyChunkSize = applyChunkSize;
61
- this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize);
75
+ this.descriptionOverride = descriptionOverride;
76
+ this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize, descriptionOverride);
62
77
  }
63
78
  serializeToJSON() {
64
79
  if (this.serializedData) {
@@ -67,6 +82,7 @@ class SerializableUnion extends SerializableCommand {
67
82
  return {
68
83
  applyChunkSize: this.applyChunkSize,
69
84
  data: this.commands.map(command => command.serialize()),
85
+ description: this.descriptionOverride,
70
86
  };
71
87
  }
72
88
  apply(editor) {
@@ -120,7 +136,7 @@ class SerializableUnion extends SerializableCommand {
120
136
  * // applying them shouldn't be done all at once (which would block the UI).
121
137
  * ```
122
138
  */
123
- const uniteCommands = (commands, applyChunkSize) => {
139
+ const uniteCommands = (commands, options) => {
124
140
  let allSerializable = true;
125
141
  for (const command of commands) {
126
142
  if (!(command instanceof SerializableCommand)) {
@@ -128,12 +144,21 @@ const uniteCommands = (commands, applyChunkSize) => {
128
144
  break;
129
145
  }
130
146
  }
147
+ let applyChunkSize;
148
+ let description;
149
+ if (typeof options === 'number') {
150
+ applyChunkSize = options;
151
+ }
152
+ else {
153
+ applyChunkSize = options?.applyChunkSize;
154
+ description = options?.description;
155
+ }
131
156
  if (!allSerializable) {
132
- return new NonSerializableUnion(commands, applyChunkSize);
157
+ return new NonSerializableUnion(commands, applyChunkSize, description);
133
158
  }
134
159
  else {
135
160
  const castedCommands = commands;
136
- return new SerializableUnion(castedCommands, applyChunkSize);
161
+ return new SerializableUnion(castedCommands, applyChunkSize, description);
137
162
  }
138
163
  };
139
164
  SerializableCommand.register('union', (data, editor) => {
@@ -144,10 +169,11 @@ SerializableCommand.register('union', (data, editor) => {
144
169
  if (typeof applyChunkSize !== 'number' && applyChunkSize !== undefined) {
145
170
  throw new Error('serialized applyChunkSize is neither undefined nor a number.');
146
171
  }
172
+ const description = typeof data.description === 'string' ? data.description : undefined;
147
173
  const commands = [];
148
174
  for (const part of data.data) {
149
175
  commands.push(SerializableCommand.deserialize(part, editor));
150
176
  }
151
- return uniteCommands(commands, applyChunkSize);
177
+ return uniteCommands(commands, { applyChunkSize, description });
152
178
  });
153
179
  export 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;
@@ -1,4 +1,4 @@
1
- import { Vec2, Rect2, Mat33 } from '@js-draw/math';
1
+ import { Vec2, Rect2, Mat33, Color4 } from '@js-draw/math';
2
2
  import { cloneTextStyle, textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle.mjs';
3
3
  import AbstractComponent from './AbstractComponent.mjs';
4
4
  import { createRestyleComponentCommand } from './RestylableComponent.mjs';
@@ -14,8 +14,46 @@ export var TextTransformMode;
14
14
  /**Relatively positioned in the Y direction, absolutely positioned in the X direction. */
15
15
  TextTransformMode[TextTransformMode["RELATIVE_Y_ABSOLUTE_X"] = 3] = "RELATIVE_Y_ABSOLUTE_X";
16
16
  })(TextTransformMode || (TextTransformMode = {}));
17
+ const defaultTextStyle = {
18
+ fontFamily: 'sans', size: 12, renderingStyle: { fill: Color4.purple },
19
+ };
17
20
  /**
18
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
+ * ```
19
57
  */
20
58
  class TextComponent extends AbstractComponent {
21
59
  /**
@@ -25,7 +63,7 @@ class TextComponent extends AbstractComponent {
25
63
  */
26
64
  constructor(textObjects,
27
65
  // Transformation relative to this component's parent element.
28
- transform, style,
66
+ transform, style = defaultTextStyle,
29
67
  // @internal
30
68
  transformMode = TextTransformMode.ABSOLUTE_XY) {
31
69
  super(componentTypeId);
@@ -21,7 +21,7 @@ export default class ArrowBuilder {
21
21
  const lineStartPoint = this.startPoint.pos;
22
22
  const endPoint = this.endPoint.pos;
23
23
  const toEnd = endPoint.minus(lineStartPoint).normalized();
24
- const arrowLength = endPoint.minus(lineStartPoint).length();
24
+ const arrowLength = endPoint.distanceTo(lineStartPoint);
25
25
  // Ensure that the arrow tip is smaller than the arrow.
26
26
  const arrowTipSize = Math.min(this.getLineWidth(), arrowLength / 2);
27
27
  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
+ }