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
@@ -1,5 +1,4 @@
1
- import { Bezier } from 'bezier-js';
2
- import { Vec2, Rect2, PathCommandType } from '@js-draw/math';
1
+ import { Vec2, Rect2, PathCommandType, QuadraticBezier } from '@js-draw/math';
3
2
  import Stroke from '../Stroke.mjs';
4
3
  import Viewport from '../../Viewport.mjs';
5
4
  import { StrokeSmoother } from '../util/StrokeSmoother.mjs';
@@ -25,6 +24,7 @@ export default class PressureSensitiveFreehandLineBuilder {
25
24
  this.isFirstSegment = true;
26
25
  this.pathStartConnector = null;
27
26
  this.mostRecentConnector = null;
27
+ this.nextCurveStartConnector = null;
28
28
  this.lastUpperBezier = null;
29
29
  this.lastLowerBezier = null;
30
30
  this.parts = [];
@@ -42,18 +42,18 @@ export default class PressureSensitiveFreehandLineBuilder {
42
42
  fill: this.startPoint.color ?? null,
43
43
  };
44
44
  }
45
- previewCurrentPath() {
45
+ previewCurrentPath(extendWithLatest = true) {
46
46
  const upperPath = this.upperSegments.slice();
47
47
  const lowerPath = this.lowerSegments.slice();
48
48
  let lowerToUpperCap;
49
49
  let pathStartConnector;
50
50
  const currentCurve = this.curveFitter.preview();
51
- if (currentCurve) {
51
+ if (currentCurve && extendWithLatest) {
52
52
  const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand } = this.segmentToPath(currentCurve);
53
53
  upperPath.push(upperCurveCommand);
54
54
  lowerPath.push(lowerCurveCommand);
55
55
  lowerToUpperCap = lowerToUpperConnector;
56
- pathStartConnector = this.pathStartConnector ?? upperToLowerConnector;
56
+ pathStartConnector = this.pathStartConnector ?? [upperToLowerConnector];
57
57
  }
58
58
  else {
59
59
  if (this.mostRecentConnector === null || this.pathStartConnector === null) {
@@ -94,7 +94,7 @@ export default class PressureSensitiveFreehandLineBuilder {
94
94
  // __/ __/
95
95
  // /___ /
96
96
  // •
97
- pathStartConnector,
97
+ ...pathStartConnector,
98
98
  // Move back to the start point:
99
99
  // •
100
100
  // __/ __/
@@ -111,13 +111,6 @@ export default class PressureSensitiveFreehandLineBuilder {
111
111
  }
112
112
  return null;
113
113
  }
114
- previewStroke() {
115
- const pathPreview = this.previewFullPath();
116
- if (pathPreview) {
117
- return new Stroke(pathPreview);
118
- }
119
- return null;
120
- }
121
114
  preview(renderer) {
122
115
  const paths = this.previewFullPath();
123
116
  if (paths) {
@@ -135,7 +128,7 @@ export default class PressureSensitiveFreehandLineBuilder {
135
128
  // Ensure we have something.
136
129
  this.addCurve(null);
137
130
  }
138
- return this.previewStroke();
131
+ return new Stroke(this.previewFullPath());
139
132
  }
140
133
  roundPoint(point) {
141
134
  let minFit = Math.min(this.minFitAllowed, this.curveStartWidth / 3);
@@ -150,25 +143,16 @@ export default class PressureSensitiveFreehandLineBuilder {
150
143
  return false;
151
144
  }
152
145
  const getIntersection = (curve1, curve2) => {
153
- const intersection = curve1.intersects(curve2);
154
- if (!intersection || intersection.length === 0) {
146
+ const intersections = curve1.intersectsBezier(curve2);
147
+ if (!intersections.length)
155
148
  return null;
156
- }
157
- // From http://pomax.github.io/bezierjs/#intersect-curve,
158
- // .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
159
- const firstTPair = intersection[0];
160
- const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
161
- if (!match) {
162
- throw new Error(`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`);
163
- }
164
- const t = parseFloat(match[1]);
165
- return Vec2.ofXY(curve1.get(t));
149
+ return intersections[0].point;
166
150
  };
167
151
  const getExitDirection = (curve) => {
168
- return Vec2.ofXY(curve.points[2]).minus(Vec2.ofXY(curve.points[1])).normalized();
152
+ return curve.p2.minus(curve.p1).normalized();
169
153
  };
170
154
  const getEnterDirection = (curve) => {
171
- return Vec2.ofXY(curve.points[1]).minus(Vec2.ofXY(curve.points[0])).normalized();
155
+ return curve.p1.minus(curve.p0).normalized();
172
156
  };
173
157
  // Prevent
174
158
  // /
@@ -179,8 +163,8 @@ export default class PressureSensitiveFreehandLineBuilder {
179
163
  // where the next stroke and the previous stroke are in different directions.
180
164
  //
181
165
  // Are the exit/enter directions of the previous and current curves in different enough directions?
182
- if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.3
183
- || getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.3
166
+ if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.35
167
+ || getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.35
184
168
  // Also handle if the curves exit/enter directions differ
185
169
  || getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
186
170
  || getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
@@ -236,32 +220,37 @@ export default class PressureSensitiveFreehandLineBuilder {
236
220
  controlPoint: center.plus(Vec2.of(width, -width)),
237
221
  endPoint: center.plus(Vec2.of(width, 0)),
238
222
  });
239
- this.pathStartConnector = {
223
+ const connector = {
240
224
  kind: PathCommandType.LineTo,
241
225
  point: startPoint,
242
226
  };
243
- this.mostRecentConnector = this.pathStartConnector;
227
+ this.pathStartConnector = [connector];
228
+ this.mostRecentConnector = connector;
244
229
  return;
245
230
  }
246
- const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, } = this.segmentToPath(curve);
247
- const shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
231
+ const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, nextCurveStartConnector, } = this.segmentToPath(curve);
232
+ let shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
248
233
  if (shouldStartNew) {
249
- const part = this.previewCurrentPath();
234
+ const part = this.previewCurrentPath(false);
250
235
  if (part) {
251
236
  this.parts.push(part);
252
237
  this.upperSegments = [];
253
238
  this.lowerSegments = [];
254
239
  }
240
+ else {
241
+ shouldStartNew = false;
242
+ }
255
243
  }
256
244
  if (this.isFirstSegment || shouldStartNew) {
257
245
  // We draw the upper path (reversed), then the lower path, so we need the
258
246
  // upperToLowerConnector to join the two paths.
259
- this.pathStartConnector = upperToLowerConnector;
247
+ this.pathStartConnector = this.nextCurveStartConnector ?? [upperToLowerConnector];
260
248
  this.isFirstSegment = false;
261
249
  }
262
250
  // With the most recent connector, we're joining the end of the lowerPath to the most recent
263
251
  // upperPath:
264
252
  this.mostRecentConnector = lowerToUpperConnector;
253
+ this.nextCurveStartConnector = nextCurveStartConnector;
265
254
  this.lowerSegments.push(lowerCurveCommand);
266
255
  this.upperSegments.push(upperCurveCommand);
267
256
  this.lastLowerBezier = lowerCurve;
@@ -270,9 +259,9 @@ export default class PressureSensitiveFreehandLineBuilder {
270
259
  }
271
260
  // Returns [upper curve, connector, lower curve]
272
261
  segmentToPath(curve) {
273
- const bezier = new Bezier(curve.startPoint.xy, curve.controlPoint.xy, curve.endPoint.xy);
274
- let startVec = Vec2.ofXY(bezier.normal(0)).normalized();
275
- let endVec = Vec2.ofXY(bezier.normal(1)).normalized();
262
+ const bezier = new QuadraticBezier(curve.startPoint, curve.controlPoint, curve.endPoint);
263
+ let startVec = bezier.normal(0);
264
+ let endVec = bezier.normal(1);
276
265
  startVec = startVec.times(curve.startWidth / 2);
277
266
  endVec = endVec.times(curve.endWidth / 2);
278
267
  if (!isFinite(startVec.magnitude())) {
@@ -283,18 +272,9 @@ export default class PressureSensitiveFreehandLineBuilder {
283
272
  const endPt = curve.endPoint;
284
273
  const controlPoint = curve.controlPoint;
285
274
  // Approximate the normal at the location of the control point
286
- let projectionT = bezier.project(controlPoint.xy).t;
287
- if (!projectionT) {
288
- if (startPt.squareDistanceTo(controlPoint) < endPt.squareDistanceTo(controlPoint)) {
289
- projectionT = 0.1;
290
- }
291
- else {
292
- projectionT = 0.9;
293
- }
294
- }
275
+ const projectionT = bezier.nearestPointTo(controlPoint).parameterValue;
295
276
  const halfVecT = projectionT;
296
- const halfVec = Vec2.ofXY(bezier.normal(halfVecT))
297
- .normalized().times(curve.startWidth / 2 * halfVecT
277
+ const halfVec = bezier.normal(halfVecT).times(curve.startWidth / 2 * halfVecT
298
278
  + curve.endWidth / 2 * (1 - halfVecT));
299
279
  // Each starts at startPt ± startVec
300
280
  const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
@@ -318,16 +298,29 @@ export default class PressureSensitiveFreehandLineBuilder {
318
298
  kind: PathCommandType.LineTo,
319
299
  point: upperCurveStartPoint,
320
300
  };
301
+ // The segment to be used to start the next path (to insert to connect the start of its
302
+ // lower and the end of its upper).
303
+ const nextCurveStartConnector = [
304
+ {
305
+ kind: PathCommandType.LineTo,
306
+ point: upperCurveStartPoint,
307
+ },
308
+ {
309
+ kind: PathCommandType.LineTo,
310
+ point: lowerCurveEndPoint,
311
+ },
312
+ ];
321
313
  const upperCurveCommand = {
322
314
  kind: PathCommandType.QuadraticBezierTo,
323
315
  controlPoint: upperCurveControlPoint,
324
316
  endPoint: upperCurveEndPoint,
325
317
  };
326
- const upperCurve = new Bezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
327
- const lowerCurve = new Bezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
318
+ const upperCurve = new QuadraticBezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
319
+ const lowerCurve = new QuadraticBezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
328
320
  return {
329
321
  upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
330
322
  upperCurve, lowerCurve,
323
+ nextCurveStartConnector,
331
324
  };
332
325
  }
333
326
  addPoint(newPoint) {
@@ -629,7 +629,7 @@ export class ImageNode {
629
629
  this.bbox = Rect2.union(...this.children.map(child => child.getBBox()));
630
630
  }
631
631
  if (bubbleUp && !oldBBox.eq(this.bbox)) {
632
- if (!this.bbox.containsRect(oldBBox)) {
632
+ if (this.bbox.containsRect(oldBBox)) {
633
633
  this.parent?.unionBBoxWith(this.bbox);
634
634
  }
635
635
  else {
@@ -27,7 +27,7 @@ const localization = {
27
27
  selectionToolKeyboardShortcuts: 'Auswahl-Werkzeug: Verwende die Pfeiltasten, um ausgewählte Elemente zu verschieben und ‚i‘ und ‚o‘, um ihre Größe zu ändern.',
28
28
  touchPanning: 'Ansicht mit Touchscreen verschieben',
29
29
  anyDevicePanning: 'Ansicht mit jedem Eingabegerät verschieben',
30
- selectPenTip: 'Objekt-Typ: ',
30
+ selectPenType: 'Objekt-Typ: ',
31
31
  roundedTipPen: 'Freihand',
32
32
  flatTipPen: 'Stift (druckempfindlich)',
33
33
  arrowPen: 'Pfeil',
@@ -22,7 +22,7 @@ const localization = {
22
22
  save: 'Guardar',
23
23
  undo: 'Deshace',
24
24
  redo: 'Rehace',
25
- selectPenTip: 'Punta',
25
+ selectPenType: 'Punta',
26
26
  selectShape: 'Forma',
27
27
  pickColorFromScreen: 'Selecciona un color de la pantalla',
28
28
  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;
@@ -1,9 +1,9 @@
1
1
  import { RenderingMode } from '../rendering/Display.mjs';
2
2
  import Editor from '../Editor.mjs';
3
3
  /** Creates an editor. Should only be used in test files. */
4
- export default () => {
4
+ export default (settings) => {
5
5
  if (jest === undefined) {
6
6
  throw new Error('Files in the testing/ folder should only be used in tests!');
7
7
  }
8
- return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
8
+ return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer, ...settings });
9
9
  };
@@ -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
  }
@@ -8,6 +8,8 @@ import { Vec2, Color4 } from '@js-draw/math';
8
8
  import SVGRenderer from '../rendering/renderers/SVGRenderer.mjs';
9
9
  import Viewport from '../Viewport.mjs';
10
10
  import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder.mjs';
11
+ import { makePolylineBuilder } from '../components/builders/PolylineBuilder.mjs';
12
+ import { EraserMode } from '../tools/Eraser.mjs';
11
13
  const svgNamespace = 'http://www.w3.org/2000/svg';
12
14
  const iconColorFill = `
13
15
  style='fill: var(--icon-color);'
@@ -112,16 +114,23 @@ class IconProvider {
112
114
  icon.setAttribute('viewBox', '-10 -10 110 110');
113
115
  return icon;
114
116
  }
115
- makeEraserIcon(eraserSize) {
117
+ makeEraserIcon(eraserSize, mode) {
116
118
  const icon = document.createElementNS(svgNamespace, 'svg');
117
119
  eraserSize ??= 10;
118
120
  const scaledSize = eraserSize / 4;
119
121
  const eraserColor = '#ff70af';
120
122
  // Draw an eraser-like shape. Created with Inkscape
121
123
  icon.innerHTML = `
124
+ <defs>
125
+ <linearGradient id="dash-pattern">
126
+ <stop offset="80%" stop-color="${eraserColor}"/>
127
+ <stop offset="85%" stop-color="white"/>
128
+ <stop offset="90%" stop-color="${eraserColor}"/>
129
+ </linearGradient>
130
+ </defs>
122
131
  <g>
123
132
  <path
124
- style="fill:${eraserColor}"
133
+ style="fill:${mode === EraserMode.PartialStroke ? 'url(#dash-pattern)' : eraserColor}"
125
134
  stroke="black"
126
135
  transform="rotate(41.35)"
127
136
  d="M 52.5 27
@@ -835,7 +844,10 @@ class IconProvider {
835
844
  * @returns true if the given `penStyle` is known to match a rounded tip type of pen.
836
845
  */
837
846
  isRoundedTipPen(penStyle) {
838
- return penStyle.factory === makeFreehandLineBuilder;
847
+ return penStyle.factory === makeFreehandLineBuilder || penStyle.factory === makePolylineBuilder;
848
+ }
849
+ isPolylinePen(penStyle) {
850
+ return penStyle.factory === makePolylineBuilder;
839
851
  }
840
852
  /** Must be overridden by icon packs that need attribution. */
841
853
  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;
@@ -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.',
@@ -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;
@@ -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);
@@ -21,6 +22,7 @@ class PenToolWidget extends BaseToolWidget {
21
22
  this.shapelikeIDs = ['pressure-sensitive-pen', 'freehand-pen'];
22
23
  // Additional client-specified pens.
23
24
  const additionalPens = editor.getCurrentSettings().pens?.additionalPenTypes ?? [];
25
+ const filterPens = editor.getCurrentSettings().pens?.filterPenTypes ?? (() => true);
24
26
  // Default pen types
25
27
  this.penTypes = [
26
28
  // Non-shape pens
@@ -34,6 +36,11 @@ class PenToolWidget extends BaseToolWidget {
34
36
  id: 'freehand-pen',
35
37
  factory: makeFreehandLineBuilder,
36
38
  },
39
+ {
40
+ name: this.localizationTable.roundedTipPen2,
41
+ id: 'polyline-pen',
42
+ factory: makePolylineBuilder,
43
+ },
37
44
  ...(additionalPens.filter(pen => !pen.isShapeBuilder)),
38
45
  // Shape pens
39
46
  {
@@ -67,7 +74,7 @@ class PenToolWidget extends BaseToolWidget {
67
74
  factory: makeOutlinedCircleBuilder,
68
75
  },
69
76
  ...(additionalPens.filter(pen => pen.isShapeBuilder)),
70
- ];
77
+ ].filter(filterPens);
71
78
  this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
72
79
  if (toolEvt.kind !== EditorEventType.ToolUpdated) {
73
80
  throw new Error('Invalid event type!');
@@ -111,7 +118,7 @@ class PenToolWidget extends BaseToolWidget {
111
118
  style.factory = record.factory;
112
119
  }
113
120
  const strokeFactory = record?.factory;
114
- if (!strokeFactory || strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
121
+ if (!strokeFactory || strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder || strokeFactory === makePolylineBuilder) {
115
122
  return this.editor.icons.makePenIcon(style);
116
123
  }
117
124
  else {
@@ -131,7 +138,7 @@ class PenToolWidget extends BaseToolWidget {
131
138
  isShapeBuilder: penType.isShapeBuilder ?? false,
132
139
  };
133
140
  });
134
- 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));
135
142
  const shapeSelector = makeGridSelector(this.localizationTable.selectShape, this.getCurrentPenTypeIdx(), allChoices.filter(choice => choice.isShapeBuilder));
136
143
  const onSelectorUpdate = (newPenTypeIndex) => {
137
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
  }