js-draw 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +70 -10
  2. package/dist/bundle.js +2 -2
  3. package/dist/cjs/Editor.d.ts +18 -20
  4. package/dist/cjs/Editor.js +5 -2
  5. package/dist/cjs/components/AbstractComponent.d.ts +17 -5
  6. package/dist/cjs/components/AbstractComponent.js +15 -15
  7. package/dist/cjs/components/Stroke.d.ts +4 -1
  8. package/dist/cjs/components/Stroke.js +158 -2
  9. package/dist/cjs/components/builders/PolylineBuilder.d.ts +1 -1
  10. package/dist/cjs/components/builders/PolylineBuilder.js +9 -2
  11. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
  12. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
  13. package/dist/cjs/image/EditorImage.js +1 -1
  14. package/dist/cjs/localizations/de.js +1 -1
  15. package/dist/cjs/localizations/es.js +1 -1
  16. package/dist/cjs/testing/createEditor.d.ts +2 -2
  17. package/dist/cjs/testing/createEditor.js +2 -2
  18. package/dist/cjs/toolbar/IconProvider.d.ts +3 -1
  19. package/dist/cjs/toolbar/IconProvider.js +15 -3
  20. package/dist/cjs/toolbar/localization.d.ts +6 -1
  21. package/dist/cjs/toolbar/localization.js +7 -2
  22. package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
  23. package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
  24. package/dist/cjs/toolbar/widgets/PenToolWidget.js +10 -3
  25. package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
  26. package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
  27. package/dist/cjs/tools/Eraser.d.ts +24 -4
  28. package/dist/cjs/tools/Eraser.js +107 -20
  29. package/dist/cjs/tools/PasteHandler.js +0 -1
  30. package/dist/cjs/tools/lib.d.ts +1 -4
  31. package/dist/cjs/tools/lib.js +2 -4
  32. package/dist/cjs/version.js +1 -1
  33. package/dist/mjs/Editor.d.ts +18 -20
  34. package/dist/mjs/Editor.mjs +5 -2
  35. package/dist/mjs/components/AbstractComponent.d.ts +17 -5
  36. package/dist/mjs/components/AbstractComponent.mjs +15 -15
  37. package/dist/mjs/components/Stroke.d.ts +4 -1
  38. package/dist/mjs/components/Stroke.mjs +159 -3
  39. package/dist/mjs/components/builders/PolylineBuilder.d.ts +1 -1
  40. package/dist/mjs/components/builders/PolylineBuilder.mjs +10 -3
  41. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
  42. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
  43. package/dist/mjs/image/EditorImage.mjs +1 -1
  44. package/dist/mjs/localizations/de.mjs +1 -1
  45. package/dist/mjs/localizations/es.mjs +1 -1
  46. package/dist/mjs/testing/createEditor.d.ts +2 -2
  47. package/dist/mjs/testing/createEditor.mjs +2 -2
  48. package/dist/mjs/toolbar/IconProvider.d.ts +3 -1
  49. package/dist/mjs/toolbar/IconProvider.mjs +15 -3
  50. package/dist/mjs/toolbar/localization.d.ts +6 -1
  51. package/dist/mjs/toolbar/localization.mjs +7 -2
  52. package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
  53. package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
  54. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +10 -3
  55. package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
  56. package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
  57. package/dist/mjs/tools/Eraser.d.ts +24 -4
  58. package/dist/mjs/tools/Eraser.mjs +107 -21
  59. package/dist/mjs/tools/PasteHandler.mjs +0 -1
  60. package/dist/mjs/tools/lib.d.ts +1 -4
  61. package/dist/mjs/tools/lib.mjs +1 -4
  62. package/dist/mjs/version.mjs +1 -1
  63. package/package.json +3 -3
@@ -660,7 +660,7 @@ class ImageNode {
660
660
  this.bbox = math_1.Rect2.union(...this.children.map(child => child.getBBox()));
661
661
  }
662
662
  if (bubbleUp && !oldBBox.eq(this.bbox)) {
663
- if (!this.bbox.containsRect(oldBBox)) {
663
+ if (this.bbox.containsRect(oldBBox)) {
664
664
  this.parent?.unionBBoxWith(this.bbox);
665
665
  }
666
666
  else {
@@ -29,7 +29,7 @@ const localization = {
29
29
  selectionToolKeyboardShortcuts: 'Auswahl-Werkzeug: Verwende die Pfeiltasten, um ausgewählte Elemente zu verschieben und ‚i‘ und ‚o‘, um ihre Größe zu ändern.',
30
30
  touchPanning: 'Ansicht mit Touchscreen verschieben',
31
31
  anyDevicePanning: 'Ansicht mit jedem Eingabegerät verschieben',
32
- selectPenTip: 'Objekt-Typ: ',
32
+ selectPenType: 'Objekt-Typ: ',
33
33
  roundedTipPen: 'Freihand',
34
34
  flatTipPen: 'Stift (druckempfindlich)',
35
35
  arrowPen: 'Pfeil',
@@ -24,7 +24,7 @@ const localization = {
24
24
  save: 'Guardar',
25
25
  undo: 'Deshace',
26
26
  redo: 'Rehace',
27
- selectPenTip: 'Punta',
27
+ selectPenType: 'Punta',
28
28
  selectShape: 'Forma',
29
29
  pickColorFromScreen: 'Selecciona un color de la pantalla',
30
30
  clickToPickColorAnnouncement: 'Haga un clic en la pantalla para seleccionar un color',
@@ -1,4 +1,4 @@
1
- import Editor from '../Editor';
1
+ import Editor, { EditorSettings } from '../Editor';
2
2
  /** Creates an editor. Should only be used in test files. */
3
- declare const _default: () => Editor;
3
+ declare const _default: (settings?: Partial<EditorSettings>) => Editor;
4
4
  export default _default;
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const Display_1 = require("../rendering/Display");
7
7
  const Editor_1 = __importDefault(require("../Editor"));
8
8
  /** Creates an editor. Should only be used in test files. */
9
- exports.default = () => {
9
+ exports.default = (settings) => {
10
10
  if (jest === undefined) {
11
11
  throw new Error('Files in the testing/ folder should only be used in tests!');
12
12
  }
13
- return new Editor_1.default(document.body, { renderingMode: Display_1.RenderingMode.DummyRenderer });
13
+ return new Editor_1.default(document.body, { renderingMode: Display_1.RenderingMode.DummyRenderer, ...settings });
14
14
  };
@@ -1,6 +1,7 @@
1
1
  import { Color4 } from '@js-draw/math';
2
2
  import TextRenderingStyle from '../rendering/TextRenderingStyle';
3
3
  import { PenStyle } from '../tools/Pen';
4
+ import { EraserMode } from '../tools/Eraser';
4
5
  export type IconElemType = HTMLImageElement | SVGElement;
5
6
  /**
6
7
  * Provides icons that can be used in the toolbar and other locations.
@@ -39,7 +40,7 @@ export default class IconProvider {
39
40
  makeUndoIcon(): IconElemType;
40
41
  makeRedoIcon(): IconElemType;
41
42
  makeDropdownIcon(): IconElemType;
42
- makeEraserIcon(eraserSize?: number): IconElemType;
43
+ makeEraserIcon(eraserSize?: number, mode?: EraserMode): IconElemType;
43
44
  makeSelectionIcon(): IconElemType;
44
45
  makeRotateIcon(): IconElemType;
45
46
  makeHandToolIcon(): IconElemType;
@@ -88,6 +89,7 @@ export default class IconProvider {
88
89
  * @returns true if the given `penStyle` is known to match a rounded tip type of pen.
89
90
  */
90
91
  protected isRoundedTipPen(penStyle: PenStyle): boolean;
92
+ protected isPolylinePen(penStyle: PenStyle): boolean;
91
93
  /** Must be overridden by icon packs that need attribution. */
92
94
  licenseInfo(): string | null;
93
95
  }
@@ -13,6 +13,8 @@ const math_1 = require("@js-draw/math");
13
13
  const SVGRenderer_1 = __importDefault(require("../rendering/renderers/SVGRenderer"));
14
14
  const Viewport_1 = __importDefault(require("../Viewport"));
15
15
  const FreehandLineBuilder_1 = require("../components/builders/FreehandLineBuilder");
16
+ const PolylineBuilder_1 = require("../components/builders/PolylineBuilder");
17
+ const Eraser_1 = require("../tools/Eraser");
16
18
  const svgNamespace = 'http://www.w3.org/2000/svg';
17
19
  const iconColorFill = `
18
20
  style='fill: var(--icon-color);'
@@ -117,16 +119,23 @@ class IconProvider {
117
119
  icon.setAttribute('viewBox', '-10 -10 110 110');
118
120
  return icon;
119
121
  }
120
- makeEraserIcon(eraserSize) {
122
+ makeEraserIcon(eraserSize, mode) {
121
123
  const icon = document.createElementNS(svgNamespace, 'svg');
122
124
  eraserSize ??= 10;
123
125
  const scaledSize = eraserSize / 4;
124
126
  const eraserColor = '#ff70af';
125
127
  // Draw an eraser-like shape. Created with Inkscape
126
128
  icon.innerHTML = `
129
+ <defs>
130
+ <linearGradient id="dash-pattern">
131
+ <stop offset="80%" stop-color="${eraserColor}"/>
132
+ <stop offset="85%" stop-color="white"/>
133
+ <stop offset="90%" stop-color="${eraserColor}"/>
134
+ </linearGradient>
135
+ </defs>
127
136
  <g>
128
137
  <path
129
- style="fill:${eraserColor}"
138
+ style="fill:${mode === Eraser_1.EraserMode.PartialStroke ? 'url(#dash-pattern)' : eraserColor}"
130
139
  stroke="black"
131
140
  transform="rotate(41.35)"
132
141
  d="M 52.5 27
@@ -840,7 +849,10 @@ class IconProvider {
840
849
  * @returns true if the given `penStyle` is known to match a rounded tip type of pen.
841
850
  */
842
851
  isRoundedTipPen(penStyle) {
843
- return penStyle.factory === FreehandLineBuilder_1.makeFreehandLineBuilder;
852
+ return penStyle.factory === FreehandLineBuilder_1.makeFreehandLineBuilder || penStyle.factory === PolylineBuilder_1.makePolylineBuilder;
853
+ }
854
+ isPolylinePen(penStyle) {
855
+ return penStyle.factory === PolylineBuilder_1.makePolylineBuilder;
844
856
  }
845
857
  /** Must be overridden by icon packs that need attribution. */
846
858
  licenseInfo() { return null; }
@@ -18,8 +18,9 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
18
18
  cancel: string;
19
19
  submit: string;
20
20
  roundedTipPen: string;
21
+ roundedTipPen2: string;
21
22
  flatTipPen: string;
22
- selectPenTip: string;
23
+ selectPenType: string;
23
24
  selectShape: string;
24
25
  colorLabel: string;
25
26
  pen: string;
@@ -30,6 +31,7 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
30
31
  resizeImageToSelection: string;
31
32
  deleteSelection: string;
32
33
  duplicateSelection: string;
34
+ fullStrokeEraser: string;
33
35
  pickColorFromScreen: string;
34
36
  clickToPickColorAnnouncement: string;
35
37
  colorSelectionCanceledAnnouncement: string;
@@ -66,7 +68,10 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
66
68
  handDropdown__zoomOutHelpText: string;
67
69
  handDropdown__resetViewHelpText: string;
68
70
  handDropdown__touchPanningHelpText: string;
71
+ eraserDropdown__baseHelpText: string;
72
+ eraserDropdown__fullStrokeEraserHelpText: string;
69
73
  handDropdown__lockRotationHelpText: string;
74
+ eraserDropdown__thicknessHelpText: string;
70
75
  selectionDropdown__baseHelpText: string;
71
76
  selectionDropdown__resizeToHelpText: string;
72
77
  selectionDropdown__deleteHelpText: string;
@@ -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.',
@@ -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;
@@ -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);
@@ -26,6 +27,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
26
27
  this.shapelikeIDs = ['pressure-sensitive-pen', 'freehand-pen'];
27
28
  // Additional client-specified pens.
28
29
  const additionalPens = editor.getCurrentSettings().pens?.additionalPenTypes ?? [];
30
+ const filterPens = editor.getCurrentSettings().pens?.filterPenTypes ?? (() => true);
29
31
  // Default pen types
30
32
  this.penTypes = [
31
33
  // Non-shape pens
@@ -39,6 +41,11 @@ class PenToolWidget extends BaseToolWidget_1.default {
39
41
  id: 'freehand-pen',
40
42
  factory: FreehandLineBuilder_1.makeFreehandLineBuilder,
41
43
  },
44
+ {
45
+ name: this.localizationTable.roundedTipPen2,
46
+ id: 'polyline-pen',
47
+ factory: PolylineBuilder_1.makePolylineBuilder,
48
+ },
42
49
  ...(additionalPens.filter(pen => !pen.isShapeBuilder)),
43
50
  // Shape pens
44
51
  {
@@ -72,7 +79,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
72
79
  factory: CircleBuilder_1.makeOutlinedCircleBuilder,
73
80
  },
74
81
  ...(additionalPens.filter(pen => pen.isShapeBuilder)),
75
- ];
82
+ ].filter(filterPens);
76
83
  this.editor.notifier.on(types_1.EditorEventType.ToolUpdated, toolEvt => {
77
84
  if (toolEvt.kind !== types_1.EditorEventType.ToolUpdated) {
78
85
  throw new Error('Invalid event type!');
@@ -116,7 +123,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
116
123
  style.factory = record.factory;
117
124
  }
118
125
  const strokeFactory = record?.factory;
119
- 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) {
120
127
  return this.editor.icons.makePenIcon(style);
121
128
  }
122
129
  else {
@@ -136,7 +143,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
136
143
  isShapeBuilder: penType.isShapeBuilder ?? false,
137
144
  };
138
145
  });
139
- 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));
140
147
  const shapeSelector = (0, makeGridSelector_1.default)(this.localizationTable.selectShape, this.getCurrentPenTypeIdx(), allChoices.filter(choice => choice.isShapeBuilder));
141
148
  const onSelectorUpdate = (newPenTypeIndex) => {
142
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,16 +56,24 @@ 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
78
  if (!this.isFirstEraseEvt && currentPoint.distanceTo(this.lastPoint) === 0) {
54
79
  return;
@@ -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;
@@ -49,7 +49,6 @@ class PasteHandler extends BaseTool_1.default {
49
49
  return event.data.substring(event.data.search(/<svg/i), svgEnd);
50
50
  })();
51
51
  if (svgData) {
52
- console.log('svgpaste', svgData);
53
52
  void this.doSVGPaste(svgData);
54
53
  return true;
55
54
  }