js-draw 1.16.1 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. package/README.md +70 -10
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +75 -7
  5. package/dist/cjs/Editor.js +93 -90
  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/AbstractComponent.d.ts +17 -5
  13. package/dist/cjs/components/AbstractComponent.js +15 -15
  14. package/dist/cjs/components/Stroke.d.ts +4 -1
  15. package/dist/cjs/components/Stroke.js +158 -2
  16. package/dist/cjs/components/TextComponent.d.ts +36 -1
  17. package/dist/cjs/components/TextComponent.js +39 -1
  18. package/dist/cjs/components/builders/ArrowBuilder.js +1 -1
  19. package/dist/cjs/components/builders/PolylineBuilder.d.ts +35 -0
  20. package/dist/cjs/components/builders/PolylineBuilder.js +122 -0
  21. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
  22. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
  23. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +1 -1
  24. package/dist/cjs/components/lib.d.ts +1 -0
  25. package/dist/cjs/components/lib.js +3 -1
  26. package/dist/cjs/components/util/StrokeSmoother.js +4 -4
  27. package/dist/cjs/image/EditorImage.d.ts +4 -1
  28. package/dist/cjs/image/EditorImage.js +5 -2
  29. package/dist/cjs/inputEvents.d.ts +11 -1
  30. package/dist/cjs/localizations/comments.d.ts +3 -0
  31. package/dist/cjs/localizations/comments.js +3 -0
  32. package/dist/cjs/localizations/de.js +1 -3
  33. package/dist/cjs/localizations/es.js +3 -3
  34. package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  35. package/dist/cjs/rendering/renderers/CanvasRenderer.js +16 -0
  36. package/dist/cjs/rendering/renderers/SVGRenderer.js +1 -1
  37. package/dist/cjs/testing/createEditor.d.ts +2 -2
  38. package/dist/cjs/testing/createEditor.js +2 -2
  39. package/dist/cjs/toolbar/IconProvider.d.ts +9 -4
  40. package/dist/cjs/toolbar/IconProvider.js +21 -7
  41. package/dist/cjs/toolbar/localization.d.ts +6 -1
  42. package/dist/cjs/toolbar/localization.js +7 -2
  43. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
  44. package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
  45. package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
  46. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  47. package/dist/cjs/toolbar/widgets/PenToolWidget.js +17 -4
  48. package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
  49. package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
  50. package/dist/cjs/tools/Eraser.d.ts +24 -4
  51. package/dist/cjs/tools/Eraser.js +108 -21
  52. package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
  53. package/dist/cjs/tools/PasteHandler.js +35 -10
  54. package/dist/cjs/tools/Pen.js +2 -2
  55. package/dist/cjs/tools/SelectionTool/SelectionTool.js +23 -4
  56. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +1 -1
  57. package/dist/cjs/tools/ToolController.d.ts +17 -1
  58. package/dist/cjs/tools/ToolController.js +21 -8
  59. package/dist/cjs/tools/lib.d.ts +1 -4
  60. package/dist/cjs/tools/lib.js +2 -4
  61. package/dist/cjs/tools/localization.d.ts +2 -2
  62. package/dist/cjs/tools/localization.js +2 -2
  63. package/dist/cjs/util/ClipboardHandler.d.ts +27 -0
  64. package/dist/cjs/util/ClipboardHandler.js +205 -0
  65. package/dist/cjs/util/ClipboardHandler.test.d.ts +1 -0
  66. package/dist/cjs/version.d.ts +5 -0
  67. package/dist/cjs/version.js +6 -1
  68. package/dist/mjs/Editor.d.ts +75 -7
  69. package/dist/mjs/Editor.mjs +93 -90
  70. package/dist/mjs/Pointer.d.ts +2 -1
  71. package/dist/mjs/Pointer.mjs +9 -2
  72. package/dist/mjs/commands/localization.d.ts +1 -0
  73. package/dist/mjs/commands/localization.mjs +1 -0
  74. package/dist/mjs/commands/uniteCommands.d.ts +5 -1
  75. package/dist/mjs/commands/uniteCommands.mjs +33 -7
  76. package/dist/mjs/components/AbstractComponent.d.ts +17 -5
  77. package/dist/mjs/components/AbstractComponent.mjs +15 -15
  78. package/dist/mjs/components/Stroke.d.ts +4 -1
  79. package/dist/mjs/components/Stroke.mjs +159 -3
  80. package/dist/mjs/components/TextComponent.d.ts +36 -1
  81. package/dist/mjs/components/TextComponent.mjs +40 -2
  82. package/dist/mjs/components/builders/ArrowBuilder.mjs +1 -1
  83. package/dist/mjs/components/builders/PolylineBuilder.d.ts +35 -0
  84. package/dist/mjs/components/builders/PolylineBuilder.mjs +115 -0
  85. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
  86. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
  87. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +1 -1
  88. package/dist/mjs/components/lib.d.ts +1 -0
  89. package/dist/mjs/components/lib.mjs +1 -0
  90. package/dist/mjs/components/util/StrokeSmoother.mjs +4 -4
  91. package/dist/mjs/image/EditorImage.d.ts +4 -1
  92. package/dist/mjs/image/EditorImage.mjs +5 -2
  93. package/dist/mjs/inputEvents.d.ts +11 -1
  94. package/dist/mjs/localizations/comments.d.ts +3 -0
  95. package/dist/mjs/localizations/comments.mjs +3 -0
  96. package/dist/mjs/localizations/de.mjs +1 -3
  97. package/dist/mjs/localizations/es.mjs +3 -3
  98. package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  99. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +16 -0
  100. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +1 -1
  101. package/dist/mjs/testing/createEditor.d.ts +2 -2
  102. package/dist/mjs/testing/createEditor.mjs +2 -2
  103. package/dist/mjs/toolbar/IconProvider.d.ts +9 -4
  104. package/dist/mjs/toolbar/IconProvider.mjs +21 -7
  105. package/dist/mjs/toolbar/localization.d.ts +6 -1
  106. package/dist/mjs/toolbar/localization.mjs +7 -2
  107. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
  108. package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
  109. package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
  110. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  111. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +17 -4
  112. package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
  113. package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
  114. package/dist/mjs/tools/Eraser.d.ts +24 -4
  115. package/dist/mjs/tools/Eraser.mjs +108 -22
  116. package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
  117. package/dist/mjs/tools/PasteHandler.mjs +35 -10
  118. package/dist/mjs/tools/Pen.mjs +2 -2
  119. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +23 -4
  120. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +1 -1
  121. package/dist/mjs/tools/ToolController.d.ts +17 -1
  122. package/dist/mjs/tools/ToolController.mjs +21 -8
  123. package/dist/mjs/tools/lib.d.ts +1 -4
  124. package/dist/mjs/tools/lib.mjs +1 -4
  125. package/dist/mjs/tools/localization.d.ts +2 -2
  126. package/dist/mjs/tools/localization.mjs +2 -2
  127. package/dist/mjs/util/ClipboardHandler.d.ts +27 -0
  128. package/dist/mjs/util/ClipboardHandler.mjs +200 -0
  129. package/dist/mjs/util/ClipboardHandler.test.d.ts +1 -0
  130. package/dist/mjs/version.d.ts +5 -0
  131. package/dist/mjs/version.mjs +6 -1
  132. package/package.json +6 -6
@@ -30,7 +30,8 @@ exports.defaultToolbarLocalization = {
30
30
  save: 'Save',
31
31
  undo: 'Undo',
32
32
  redo: 'Redo',
33
- selectPenTip: 'Pen tip',
33
+ fullStrokeEraser: 'Full stroke eraser',
34
+ selectPenType: 'Pen type',
34
35
  selectShape: 'Shape',
35
36
  pickColorFromScreen: 'Pick color from screen',
36
37
  clickToPickColorAnnouncement: 'Click on the screen to pick a color',
@@ -48,6 +49,7 @@ exports.defaultToolbarLocalization = {
48
49
  strokeAutocorrect: 'Autocorrect',
49
50
  touchPanning: 'Touchscreen panning',
50
51
  roundedTipPen: 'Round',
52
+ roundedTipPen2: 'Polyline',
51
53
  flatTipPen: 'Flat',
52
54
  arrowPen: 'Arrow',
53
55
  linePen: 'Line',
@@ -62,7 +64,7 @@ exports.defaultToolbarLocalization = {
62
64
  penDropdown__baseHelpText: 'This tool draws shapes or freehand lines.',
63
65
  penDropdown__colorHelpText: 'Changes the pen\'s color',
64
66
  penDropdown__thicknessHelpText: 'Changes the thickness of strokes drawn by the pen.',
65
- penDropdown__penTypeHelpText: 'Changes the pen style.\n\nEither a “pen tip” style or “shape” can be chosen. Choosing a “pen tip” style draws freehand lines. Choosing a “shape” draws shapes.',
67
+ penDropdown__penTypeHelpText: 'Changes the pen style.\n\nEither a “pen” style or “shape” can be chosen. Choosing a “pen” style draws freehand lines. Choosing a “shape” draws shapes.',
66
68
  penDropdown__autocorrectHelpText: 'Converts approximate freehand lines and rectangles to perfect ones.\n\nThe pen must be held stationary at the end of a stroke to trigger a correction.',
67
69
  penDropdown__stabilizationHelpText: 'Draws smoother strokes.\n\nThis also adds a short delay between the mouse/stylus and the stroke.',
68
70
  handDropdown__baseHelpText: 'This tool is responsible for scrolling, rotating, and zooming the editor.',
@@ -72,6 +74,9 @@ exports.defaultToolbarLocalization = {
72
74
  handDropdown__zoomDisplayHelpText: 'Shows the current zoom level. 100% shows the image at its actual size.',
73
75
  handDropdown__touchPanningHelpText: 'When enabled, touch gestures move the image rather than select or draw.',
74
76
  handDropdown__lockRotationHelpText: 'When enabled, prevents touch gestures from rotating the screen.',
77
+ eraserDropdown__baseHelpText: 'This tool removes strokes, images, and text under the cursor.',
78
+ eraserDropdown__thicknessHelpText: 'Changes the size of the eraser.',
79
+ eraserDropdown__fullStrokeEraserHelpText: 'When in full-stroke mode, entire shapes are erased.\n\nWhen not in full-stroke mode, shapes can be partially erased.',
75
80
  selectionDropdown__baseHelpText: 'Selects content and manipulates the selection',
76
81
  selectionDropdown__resizeToHelpText: 'Crops the drawing to the size of what\'s currently selected.\n\nIf auto-resize is enabled, it will be disabled.',
77
82
  selectionDropdown__deleteHelpText: 'Erases selected items.',
@@ -194,7 +194,30 @@ class DocumentPropertiesWidget extends BaseWidget_1.default {
194
194
  row.replaceChildren(label, input);
195
195
  return {
196
196
  setValue: (value) => {
197
- input.value = value.toString();
197
+ // Slightly improve the case where the user tries to change the
198
+ // first digit of a dimension like 600.
199
+ //
200
+ // As changing the value also gives the image zero size (which is unsupported,
201
+ // .setValue is called immediately). We work around this by trying to select
202
+ // the added/changed digits.
203
+ //
204
+ // See https://github.com/personalizedrefrigerator/js-draw/issues/58.
205
+ if (document.activeElement === input && input.value.match(/^0*$/)) {
206
+ // We need to switch to type="text" and back to type="number" because
207
+ // number inputs don't support selection.
208
+ //
209
+ // See https://stackoverflow.com/q/22381837
210
+ const originalValue = input.value;
211
+ input.type = 'text';
212
+ input.value = value.toString();
213
+ // Select the added digits
214
+ const lengthToSelect = Math.max(1, input.value.length - originalValue.length);
215
+ input.setSelectionRange(0, lengthToSelect);
216
+ input.type = 'number';
217
+ }
218
+ else {
219
+ input.value = value.toString();
220
+ }
198
221
  },
199
222
  setIsAutomaticSize: (automatic) => {
200
223
  input.disabled = automatic;
@@ -1,15 +1,20 @@
1
1
  import Editor from '../../Editor';
2
2
  import Eraser from '../../tools/Eraser';
3
3
  import { ToolbarLocalization } from '../localization';
4
+ import HelpDisplay from '../utils/HelpDisplay';
4
5
  import BaseToolWidget from './BaseToolWidget';
5
6
  import { SavedToolbuttonState } from './BaseWidget';
6
7
  export default class EraserToolWidget extends BaseToolWidget {
7
8
  private tool;
8
9
  private updateInputs;
9
10
  constructor(editor: Editor, tool: Eraser, localizationTable?: ToolbarLocalization);
11
+ protected getHelpText(): string;
10
12
  protected getTitle(): string;
13
+ private makeIconForType;
11
14
  protected createIcon(): Element;
12
- protected fillDropdown(dropdown: HTMLElement): boolean;
15
+ private static idCounter;
16
+ private makeEraserTypeSelector;
17
+ protected fillDropdown(dropdown: HTMLElement, helpDisplay?: HelpDisplay): boolean;
13
18
  serializeState(): SavedToolbuttonState;
14
19
  deserializeFrom(state: SavedToolbuttonState): void;
15
20
  }
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const Eraser_1 = require("../../tools/Eraser");
6
7
  const types_1 = require("../../types");
7
8
  const constants_1 = require("../constants");
8
9
  const BaseToolWidget_1 = __importDefault(require("./BaseToolWidget"));
@@ -19,26 +20,57 @@ class EraserToolWidget extends BaseToolWidget_1.default {
19
20
  }
20
21
  });
21
22
  }
23
+ getHelpText() {
24
+ return this.localizationTable.eraserDropdown__baseHelpText;
25
+ }
22
26
  getTitle() {
23
27
  return this.localizationTable.eraser;
24
28
  }
29
+ makeIconForType(mode) {
30
+ return this.editor.icons.makeEraserIcon(this.tool.getThickness(), mode);
31
+ }
25
32
  createIcon() {
26
- return this.editor.icons.makeEraserIcon(this.tool.getThickness());
33
+ return this.makeIconForType(this.tool.getModeValue().get());
34
+ }
35
+ makeEraserTypeSelector(helpDisplay) {
36
+ const container = document.createElement('div');
37
+ const labelElement = document.createElement('label');
38
+ const checkboxElement = document.createElement('input');
39
+ checkboxElement.id = `${constants_1.toolbarCSSPrefix}eraserToolWidget-${EraserToolWidget.idCounter++}`;
40
+ labelElement.htmlFor = checkboxElement.id;
41
+ labelElement.innerText = this.localizationTable.fullStrokeEraser;
42
+ checkboxElement.type = 'checkbox';
43
+ checkboxElement.oninput = () => {
44
+ this.tool.getModeValue().set(checkboxElement.checked ? Eraser_1.EraserMode.FullStroke : Eraser_1.EraserMode.PartialStroke);
45
+ };
46
+ const updateValue = () => {
47
+ checkboxElement.checked = this.tool.getModeValue().get() === Eraser_1.EraserMode.FullStroke;
48
+ };
49
+ container.replaceChildren(labelElement, checkboxElement);
50
+ helpDisplay?.registerTextHelpForElement(container, this.localizationTable.eraserDropdown__fullStrokeEraserHelpText);
51
+ return {
52
+ addTo: (parent) => {
53
+ parent.appendChild(container);
54
+ },
55
+ updateValue,
56
+ };
27
57
  }
28
- fillDropdown(dropdown) {
58
+ fillDropdown(dropdown, helpDisplay) {
29
59
  const container = document.createElement('div');
30
60
  container.classList.add(`${constants_1.toolbarCSSPrefix}spacedList`, `${constants_1.toolbarCSSPrefix}nonbutton-controls-main-list`);
31
61
  const thicknessSlider = (0, makeThicknessSlider_1.default)(this.editor, thickness => {
32
62
  this.tool.setThickness(thickness);
33
63
  });
34
64
  thicknessSlider.setBounds(10, 55);
65
+ helpDisplay?.registerTextHelpForElement(thicknessSlider.container, this.localizationTable.eraserDropdown__thicknessHelpText);
66
+ const modeSelector = this.makeEraserTypeSelector(helpDisplay);
35
67
  this.updateInputs = () => {
36
68
  thicknessSlider.setValue(this.tool.getThickness());
69
+ modeSelector.updateValue();
37
70
  };
38
71
  this.updateInputs();
39
- const spacer = document.createElement('div');
40
- spacer.style.height = '5px';
41
- container.replaceChildren(thicknessSlider.container, spacer);
72
+ container.replaceChildren(thicknessSlider.container);
73
+ modeSelector.addTo(container);
42
74
  dropdown.replaceChildren(container);
43
75
  return true;
44
76
  }
@@ -46,6 +78,7 @@ class EraserToolWidget extends BaseToolWidget_1.default {
46
78
  return {
47
79
  ...super.serializeState(),
48
80
  thickness: this.tool.getThickness(),
81
+ mode: this.tool.getModeValue().get(),
49
82
  };
50
83
  }
51
84
  deserializeFrom(state) {
@@ -57,6 +90,13 @@ class EraserToolWidget extends BaseToolWidget_1.default {
57
90
  }
58
91
  this.tool.setThickness(parsedThickness);
59
92
  }
93
+ if (state.mode) {
94
+ const mode = state.mode;
95
+ if (Object.values(Eraser_1.EraserMode).includes(mode)) {
96
+ this.tool.getModeValue().set(mode);
97
+ }
98
+ }
60
99
  }
61
100
  }
101
+ EraserToolWidget.idCounter = 0;
62
102
  exports.default = EraserToolWidget;
@@ -15,7 +15,7 @@ export interface PenTypeRecord {
15
15
  export default class PenToolWidget extends BaseToolWidget {
16
16
  private tool;
17
17
  private updateInputs;
18
- protected penTypes: PenTypeRecord[];
18
+ protected penTypes: Readonly<PenTypeRecord>[];
19
19
  protected shapelikeIDs: string[];
20
20
  private static idCounter;
21
21
  constructor(editor: Editor, tool: Pen, localization?: ToolbarLocalization);
@@ -17,6 +17,7 @@ const keybindings_1 = require("./keybindings");
17
17
  const constants_1 = require("../constants");
18
18
  const makeThicknessSlider_1 = __importDefault(require("./components/makeThicknessSlider"));
19
19
  const makeGridSelector_1 = __importDefault(require("./components/makeGridSelector"));
20
+ const PolylineBuilder_1 = require("../../components/builders/PolylineBuilder");
20
21
  class PenToolWidget extends BaseToolWidget_1.default {
21
22
  constructor(editor, tool, localization) {
22
23
  super(editor, tool, 'pen', localization);
@@ -24,8 +25,12 @@ class PenToolWidget extends BaseToolWidget_1.default {
24
25
  this.updateInputs = () => { };
25
26
  // Pen types that correspond to
26
27
  this.shapelikeIDs = ['pressure-sensitive-pen', 'freehand-pen'];
28
+ // Additional client-specified pens.
29
+ const additionalPens = editor.getCurrentSettings().pens?.additionalPenTypes ?? [];
30
+ const filterPens = editor.getCurrentSettings().pens?.filterPenTypes ?? (() => true);
27
31
  // Default pen types
28
32
  this.penTypes = [
33
+ // Non-shape pens
29
34
  {
30
35
  name: this.localizationTable.flatTipPen,
31
36
  id: 'pressure-sensitive-pen',
@@ -36,6 +41,13 @@ class PenToolWidget extends BaseToolWidget_1.default {
36
41
  id: 'freehand-pen',
37
42
  factory: FreehandLineBuilder_1.makeFreehandLineBuilder,
38
43
  },
44
+ {
45
+ name: this.localizationTable.roundedTipPen2,
46
+ id: 'polyline-pen',
47
+ factory: PolylineBuilder_1.makePolylineBuilder,
48
+ },
49
+ ...(additionalPens.filter(pen => !pen.isShapeBuilder)),
50
+ // Shape pens
39
51
  {
40
52
  name: this.localizationTable.arrowPen,
41
53
  id: 'arrow',
@@ -65,8 +77,9 @@ class PenToolWidget extends BaseToolWidget_1.default {
65
77
  id: 'outlined-circle',
66
78
  isShapeBuilder: true,
67
79
  factory: CircleBuilder_1.makeOutlinedCircleBuilder,
68
- }
69
- ];
80
+ },
81
+ ...(additionalPens.filter(pen => pen.isShapeBuilder)),
82
+ ].filter(filterPens);
70
83
  this.editor.notifier.on(types_1.EditorEventType.ToolUpdated, toolEvt => {
71
84
  if (toolEvt.kind !== types_1.EditorEventType.ToolUpdated) {
72
85
  throw new Error('Invalid event type!');
@@ -110,7 +123,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
110
123
  style.factory = record.factory;
111
124
  }
112
125
  const strokeFactory = record?.factory;
113
- if (!strokeFactory || strokeFactory === FreehandLineBuilder_1.makeFreehandLineBuilder || strokeFactory === PressureSensitiveFreehandLineBuilder_1.makePressureSensitiveFreehandLineBuilder) {
126
+ if (!strokeFactory || strokeFactory === FreehandLineBuilder_1.makeFreehandLineBuilder || strokeFactory === PressureSensitiveFreehandLineBuilder_1.makePressureSensitiveFreehandLineBuilder || strokeFactory === PolylineBuilder_1.makePolylineBuilder) {
114
127
  return this.editor.icons.makePenIcon(style);
115
128
  }
116
129
  else {
@@ -130,7 +143,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
130
143
  isShapeBuilder: penType.isShapeBuilder ?? false,
131
144
  };
132
145
  });
133
- const penSelector = (0, makeGridSelector_1.default)(this.localizationTable.selectPenTip, this.getCurrentPenTypeIdx(), allChoices.filter(choice => !choice.isShapeBuilder));
146
+ const penSelector = (0, makeGridSelector_1.default)(this.localizationTable.selectPenType, this.getCurrentPenTypeIdx(), allChoices.filter(choice => !choice.isShapeBuilder));
134
147
  const shapeSelector = (0, makeGridSelector_1.default)(this.localizationTable.selectShape, this.getCurrentPenTypeIdx(), allChoices.filter(choice => choice.isShapeBuilder));
135
148
  const onSelectorUpdate = (newPenTypeIndex) => {
136
149
  this.tool.setStrokeFactory(this.penTypes[newPenTypeIndex].factory);
@@ -0,0 +1 @@
1
+ export {};
@@ -9,7 +9,7 @@ const KeyboardShortcutManager_1 = __importDefault(require("../../shortcuts/Keybo
9
9
  exports.resizeImageToSelectionKeyboardShortcut = 'jsdraw.toolbar.SelectionTool.resizeImageToSelection';
10
10
  KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.resizeImageToSelectionKeyboardShortcut, ['ctrlOrMeta+r'], 'Resize image to selection');
11
11
  // Pen tool
12
- exports.selectStrokeTypeKeyboardShortcutIds = [1, 2, 3, 4, 5, 6, 7].map(id => `jsdraw.toolbar.PenTool.select-pen-${id}`);
12
+ exports.selectStrokeTypeKeyboardShortcutIds = [1, 2, 3, 4, 5, 6, 7, 8, 9].map(id => `jsdraw.toolbar.PenTool.select-pen-${id}`);
13
13
  for (let i = 0; i < exports.selectStrokeTypeKeyboardShortcutIds.length; i++) {
14
14
  const id = exports.selectStrokeTypeKeyboardShortcutIds[i];
15
15
  KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(id, [`CtrlOrMeta+Digit${(i + 1)}`], 'Select pen style ' + (i + 1));
@@ -2,30 +2,50 @@ import { KeyPressEvent, PointerEvt } from '../inputEvents';
2
2
  import BaseTool from './BaseTool';
3
3
  import Editor from '../Editor';
4
4
  import { MutableReactiveValue } from '../util/ReactiveValue';
5
+ export declare enum EraserMode {
6
+ PartialStroke = "partial-stroke",
7
+ FullStroke = "full-stroke"
8
+ }
9
+ export interface InitialEraserOptions {
10
+ thickness?: number;
11
+ mode?: EraserMode;
12
+ }
5
13
  export default class Eraser extends BaseTool {
6
14
  private editor;
7
15
  private lastPoint;
8
16
  private isFirstEraseEvt;
9
- private toRemove;
10
17
  private thickness;
11
18
  private thicknessValue;
12
- private partialCommands;
13
- constructor(editor: Editor, description: string);
19
+ private modeValue;
20
+ private toRemove;
21
+ private toAdd;
22
+ private eraseCommands;
23
+ private addCommands;
24
+ constructor(editor: Editor, description: string, options?: InitialEraserOptions);
14
25
  private clearPreview;
15
26
  private getSizeOnCanvas;
16
27
  private drawPreviewAt;
28
+ /**
29
+ * @returns the eraser rectangle in canvas coordinates.
30
+ *
31
+ * For now, all erasers are rectangles or points.
32
+ */
17
33
  private getEraserRect;
34
+ /** Erases in a line from the last point to the current. */
18
35
  private eraseTo;
19
36
  onPointerDown(event: PointerEvt): boolean;
20
37
  onPointerMove(event: PointerEvt): void;
21
38
  onPointerUp(event: PointerEvt): void;
22
39
  onGestureCancel(): void;
23
40
  onKeyPress(event: KeyPressEvent): boolean;
41
+ /** Returns the side-length of the tip of this eraser. */
24
42
  getThickness(): number;
43
+ /** Sets the side-length of this' tip. */
44
+ setThickness(thickness: number): void;
25
45
  /**
26
46
  * Returns a {@link MutableReactiveValue} that can be used to watch
27
47
  * this tool's thickness.
28
48
  */
29
49
  getThicknessValue(): MutableReactiveValue<number>;
30
- setThickness(thickness: number): void;
50
+ getModeValue(): MutableReactiveValue<EraserMode>;
31
51
  }
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.EraserMode = void 0;
6
7
  const types_1 = require("../types");
7
8
  const BaseTool_1 = __importDefault(require("./BaseTool"));
8
9
  const math_1 = require("@js-draw/math");
@@ -10,15 +11,24 @@ const Erase_1 = __importDefault(require("../commands/Erase"));
10
11
  const Pointer_1 = require("../Pointer");
11
12
  const keybindings_1 = require("./keybindings");
12
13
  const ReactiveValue_1 = require("../util/ReactiveValue");
14
+ const EditorImage_1 = __importDefault(require("../image/EditorImage"));
15
+ const uniteCommands_1 = __importDefault(require("../commands/uniteCommands"));
16
+ const RenderablePathSpec_1 = require("../rendering/RenderablePathSpec");
17
+ var EraserMode;
18
+ (function (EraserMode) {
19
+ EraserMode["PartialStroke"] = "partial-stroke";
20
+ EraserMode["FullStroke"] = "full-stroke";
21
+ })(EraserMode || (exports.EraserMode = EraserMode = {}));
13
22
  class Eraser extends BaseTool_1.default {
14
- constructor(editor, description) {
23
+ constructor(editor, description, options) {
15
24
  super(editor.notifier, description);
16
25
  this.editor = editor;
17
26
  this.lastPoint = null;
18
27
  this.isFirstEraseEvt = true;
19
- this.thickness = 10;
20
28
  // Commands that each remove one element
21
- this.partialCommands = [];
29
+ this.eraseCommands = [];
30
+ this.addCommands = [];
31
+ this.thickness = options?.thickness ?? 10;
22
32
  this.thicknessValue = ReactiveValue_1.ReactiveValue.fromInitialValue(this.thickness);
23
33
  this.thicknessValue.onUpdate(value => {
24
34
  this.thickness = value;
@@ -27,6 +37,13 @@ class Eraser extends BaseTool_1.default {
27
37
  tool: this,
28
38
  });
29
39
  });
40
+ this.modeValue = ReactiveValue_1.ReactiveValue.fromInitialValue(options?.mode ?? EraserMode.FullStroke);
41
+ this.modeValue.onUpdate(_value => {
42
+ this.editor.notifier.dispatch(types_1.EditorEventType.ToolUpdated, {
43
+ kind: types_1.EditorEventType.ToolUpdated,
44
+ tool: this,
45
+ });
46
+ });
30
47
  }
31
48
  clearPreview() {
32
49
  this.editor.clearWetInk();
@@ -39,18 +56,26 @@ class Eraser extends BaseTool_1.default {
39
56
  const size = this.getSizeOnCanvas();
40
57
  const renderer = this.editor.display.getWetInkRenderer();
41
58
  const rect = this.getEraserRect(point);
59
+ const rect2 = this.getEraserRect(this.lastPoint ?? point);
42
60
  const fill = {
43
- fill: math_1.Color4.gray,
61
+ fill: math_1.Color4.transparent,
62
+ stroke: { width: size / 10, color: math_1.Color4.gray },
44
63
  };
45
- renderer.drawRect(rect, size / 4, fill);
64
+ renderer.drawPath((0, RenderablePathSpec_1.pathToRenderable)(math_1.Path.fromConvexHullOf([...rect.corners, ...rect2.corners]), fill));
46
65
  }
66
+ /**
67
+ * @returns the eraser rectangle in canvas coordinates.
68
+ *
69
+ * For now, all erasers are rectangles or points.
70
+ */
47
71
  getEraserRect(centerPoint) {
48
72
  const size = this.getSizeOnCanvas();
49
73
  const halfSize = math_1.Vec2.of(size / 2, size / 2);
50
74
  return math_1.Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
51
75
  }
76
+ /** Erases in a line from the last point to the current. */
52
77
  eraseTo(currentPoint) {
53
- if (!this.isFirstEraseEvt && currentPoint.minus(this.lastPoint).magnitude() === 0) {
78
+ if (!this.isFirstEraseEvt && currentPoint.distanceTo(this.lastPoint) === 0) {
54
79
  return;
55
80
  }
56
81
  this.isFirstEraseEvt = false;
@@ -65,13 +90,55 @@ class Eraser extends BaseTool_1.default {
65
90
  });
66
91
  // Only erase components that could be selected (and thus interacted with)
67
92
  // by the user.
68
- const toErase = intersectingElems.filter(elem => elem.isSelectable());
69
- // Remove any intersecting elements.
70
- this.toRemove.push(...toErase);
71
- // Create new Erase commands for the now-to-be-erased elements and apply them.
72
- const newPartialCommands = toErase.map(elem => new Erase_1.default([elem]));
73
- newPartialCommands.forEach(cmd => cmd.apply(this.editor));
74
- this.partialCommands.push(...newPartialCommands);
93
+ const eraseableElems = intersectingElems.filter(elem => elem.isSelectable());
94
+ if (this.modeValue.get() === EraserMode.FullStroke) {
95
+ // Remove any intersecting elements.
96
+ this.toRemove.push(...eraseableElems);
97
+ // Create new Erase commands for the now-to-be-erased elements and apply them.
98
+ const newPartialCommands = eraseableElems.map(elem => new Erase_1.default([elem]));
99
+ newPartialCommands.forEach(cmd => cmd.apply(this.editor));
100
+ this.eraseCommands.push(...newPartialCommands);
101
+ }
102
+ else {
103
+ const toErase = [];
104
+ const toAdd = [];
105
+ for (const targetElem of eraseableElems) {
106
+ toErase.push(targetElem);
107
+ // Completely delete items that can't be divided.
108
+ if (!targetElem.withRegionErased) {
109
+ continue;
110
+ }
111
+ // Completely delete items that are completely or almost completely
112
+ // contained within the eraser.
113
+ const grownRect = eraserRect.grownBy(eraserRect.maxDimension / 3);
114
+ if (grownRect.containsRect(targetElem.getExactBBox())) {
115
+ continue;
116
+ }
117
+ // Join the current and previous rectangles so that points between events are also
118
+ // erased.
119
+ const erasePath = math_1.Path.fromConvexHullOf([
120
+ ...eraserRect.corners, ...this.getEraserRect(this.lastPoint ?? currentPoint).corners
121
+ ].map(p => this.editor.viewport.roundPoint(p)));
122
+ toAdd.push(...targetElem.withRegionErased(erasePath, this.editor.viewport));
123
+ }
124
+ const eraseCommand = new Erase_1.default(toErase);
125
+ const newAddCommands = toAdd.map(elem => EditorImage_1.default.addElement(elem));
126
+ eraseCommand.apply(this.editor);
127
+ newAddCommands.forEach(command => command.apply(this.editor));
128
+ const finalToErase = [];
129
+ for (const item of toErase) {
130
+ if (this.toAdd.includes(item)) {
131
+ this.toAdd = this.toAdd.filter(i => i !== item);
132
+ }
133
+ else {
134
+ finalToErase.push(item);
135
+ }
136
+ }
137
+ this.toRemove.push(...finalToErase);
138
+ this.toAdd.push(...toAdd);
139
+ this.eraseCommands.push(new Erase_1.default(finalToErase));
140
+ this.addCommands.push(...newAddCommands);
141
+ }
75
142
  this.drawPreviewAt(currentPoint);
76
143
  this.lastPoint = currentPoint;
77
144
  }
@@ -79,6 +146,7 @@ class Eraser extends BaseTool_1.default {
79
146
  if (event.allPointers.length === 1 || event.current.device === Pointer_1.PointerDevice.Eraser) {
80
147
  this.lastPoint = event.current.canvasPos;
81
148
  this.toRemove = [];
149
+ this.toAdd = [];
82
150
  this.isFirstEraseEvt = true;
83
151
  this.drawPreviewAt(event.current.canvasPos);
84
152
  return true;
@@ -91,18 +159,32 @@ class Eraser extends BaseTool_1.default {
91
159
  }
92
160
  onPointerUp(event) {
93
161
  this.eraseTo(event.current.canvasPos);
94
- if (this.toRemove.length > 0) {
162
+ const commands = [];
163
+ if (this.addCommands.length > 0) {
164
+ this.addCommands.forEach(cmd => cmd.unapply(this.editor));
165
+ commands.push(...this.toAdd.map(a => EditorImage_1.default.addElement(a)));
166
+ this.addCommands = [];
167
+ }
168
+ if (this.eraseCommands.length > 0) {
95
169
  // Undo commands for each individual component and unite into a single command.
96
- this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
97
- this.partialCommands = [];
170
+ this.eraseCommands.forEach(cmd => cmd.unapply(this.editor));
171
+ this.eraseCommands = [];
98
172
  const command = new Erase_1.default(this.toRemove);
99
- this.editor.dispatch(command); // dispatch: Makes undo-able.
173
+ commands.push(command);
174
+ }
175
+ if (commands.length === 1) {
176
+ this.editor.dispatch(commands[0]); // dispatch: Makes undo-able.
177
+ }
178
+ else {
179
+ this.editor.dispatch((0, uniteCommands_1.default)(commands));
100
180
  }
101
181
  this.clearPreview();
102
182
  }
103
183
  onGestureCancel() {
104
- this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
105
- this.partialCommands = [];
184
+ this.addCommands.forEach(cmd => cmd.unapply(this.editor));
185
+ this.eraseCommands.forEach(cmd => cmd.unapply(this.editor));
186
+ this.eraseCommands = [];
187
+ this.addCommands = [];
106
188
  this.clearPreview();
107
189
  }
108
190
  onKeyPress(event) {
@@ -121,9 +203,14 @@ class Eraser extends BaseTool_1.default {
121
203
  }
122
204
  return false;
123
205
  }
206
+ /** Returns the side-length of the tip of this eraser. */
124
207
  getThickness() {
125
208
  return this.thickness;
126
209
  }
210
+ /** Sets the side-length of this' tip. */
211
+ setThickness(thickness) {
212
+ this.thicknessValue.set(thickness);
213
+ }
127
214
  /**
128
215
  * Returns a {@link MutableReactiveValue} that can be used to watch
129
216
  * this tool's thickness.
@@ -131,8 +218,8 @@ class Eraser extends BaseTool_1.default {
131
218
  getThicknessValue() {
132
219
  return this.thicknessValue;
133
220
  }
134
- setThickness(thickness) {
135
- this.thicknessValue.set(thickness);
221
+ getModeValue() {
222
+ return this.modeValue;
136
223
  }
137
224
  }
138
225
  exports.default = Eraser;
@@ -13,10 +13,10 @@ var StabilizerType;
13
13
  })(StabilizerType || (StabilizerType = {}));
14
14
  const defaultOptions = {
15
15
  kind: StabilizerType.IntertialStabilizer,
16
- mass: 0.4,
17
- springConstant: 100.0,
16
+ mass: 0.4, // kg
17
+ springConstant: 100.0, // N/m
18
18
  frictionCoefficient: 0.28,
19
- maxPointDist: 10,
19
+ maxPointDist: 10, // screen units
20
20
  inertiaFraction: 0.75,
21
21
  minSimilarityToFinalize: 0.0,
22
22
  velocityDecayFactor: 0.1,
@@ -28,8 +28,28 @@ class PasteHandler extends BaseTool_1.default {
28
28
  // @internal
29
29
  onPaste(event) {
30
30
  const mime = event.mime.toLowerCase();
31
- if (mime === 'image/svg+xml') {
32
- void this.doSVGPaste(event.data);
31
+ const svgData = (() => {
32
+ if (mime === 'image/svg+xml') {
33
+ return event.data;
34
+ }
35
+ if (mime !== 'text/html') {
36
+ return false;
37
+ }
38
+ // text/html is sometimes handlable SVG data. Use a hueristic
39
+ // to determine if this is the case:
40
+ // We use [^] and not . so that newlines are included.
41
+ const match = event.data.match(/^[^]{0,200}<svg.*/i); // [^]{0,200} <- Allow for metadata near start
42
+ if (!match) {
43
+ return false;
44
+ }
45
+ // Extract the SVG element from the pasted data
46
+ let svgEnd = event.data.toLowerCase().lastIndexOf('</svg>');
47
+ if (svgEnd === -1)
48
+ svgEnd = event.data.length;
49
+ return event.data.substring(event.data.search(/<svg/i), svgEnd);
50
+ })();
51
+ if (svgData) {
52
+ void this.doSVGPaste(svgData);
33
53
  return true;
34
54
  }
35
55
  else if (mime === 'text/plain') {
@@ -43,16 +63,21 @@ class PasteHandler extends BaseTool_1.default {
43
63
  return false;
44
64
  }
45
65
  async addComponentsFromPaste(components) {
46
- await this.editor.addAndCenterComponents(components);
66
+ await this.editor.addAndCenterComponents(components, true, this.editor.localization.pasted(components.length));
47
67
  }
48
68
  async doSVGPaste(data) {
49
- const sanitize = true;
50
- const loader = SVGLoader_1.default.fromString(data, sanitize);
51
- const components = [];
52
- await loader.start((component) => {
53
- components.push(component);
54
- }, (_countProcessed, _totalToProcess) => null);
55
- await this.addComponentsFromPaste(components);
69
+ this.editor.showLoadingWarning(0);
70
+ try {
71
+ const loader = SVGLoader_1.default.fromString(data, true);
72
+ const components = [];
73
+ await loader.start((component) => {
74
+ components.push(component);
75
+ }, (_countProcessed, _totalToProcess) => null);
76
+ await this.addComponentsFromPaste(components);
77
+ }
78
+ finally {
79
+ this.editor.hideLoadingWarning();
80
+ }
56
81
  }
57
82
  async doTextPaste(text) {
58
83
  const textTools = this.editor.toolController.getMatchingTools(TextTool_1.default);
@@ -102,8 +102,8 @@ class Pen extends BaseTool_1.default {
102
102
  this.currentDeviceType = current.device;
103
103
  if (this.shapeAutocompletionEnabled) {
104
104
  const stationaryDetectionConfig = {
105
- maxSpeed: 8.5,
106
- maxRadius: 11,
105
+ maxSpeed: 8.5, // screenPx/s
106
+ maxRadius: 11, // screenPx
107
107
  minTimeSeconds: 0.5, // s
108
108
  };
109
109
  this.stationaryDetector = new StationaryPenDetector_1.default(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));