js-draw 1.17.0 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
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
  }