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