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.
- package/README.md +70 -10
- package/dist/bundle.js +2 -2
- package/dist/cjs/Editor.d.ts +18 -20
- package/dist/cjs/Editor.js +5 -2
- package/dist/cjs/components/AbstractComponent.d.ts +17 -5
- package/dist/cjs/components/AbstractComponent.js +15 -15
- package/dist/cjs/components/Stroke.d.ts +4 -1
- package/dist/cjs/components/Stroke.js +158 -2
- package/dist/cjs/components/builders/PolylineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PolylineBuilder.js +9 -2
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
- package/dist/cjs/image/EditorImage.js +1 -1
- package/dist/cjs/localizations/de.js +1 -1
- package/dist/cjs/localizations/es.js +1 -1
- package/dist/cjs/testing/createEditor.d.ts +2 -2
- package/dist/cjs/testing/createEditor.js +2 -2
- package/dist/cjs/toolbar/IconProvider.d.ts +3 -1
- package/dist/cjs/toolbar/IconProvider.js +15 -3
- package/dist/cjs/toolbar/localization.d.ts +6 -1
- package/dist/cjs/toolbar/localization.js +7 -2
- package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +10 -3
- package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
- package/dist/cjs/tools/Eraser.d.ts +24 -4
- package/dist/cjs/tools/Eraser.js +107 -20
- package/dist/cjs/tools/PasteHandler.js +0 -1
- package/dist/cjs/tools/lib.d.ts +1 -4
- package/dist/cjs/tools/lib.js +2 -4
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +18 -20
- package/dist/mjs/Editor.mjs +5 -2
- package/dist/mjs/components/AbstractComponent.d.ts +17 -5
- package/dist/mjs/components/AbstractComponent.mjs +15 -15
- package/dist/mjs/components/Stroke.d.ts +4 -1
- package/dist/mjs/components/Stroke.mjs +159 -3
- package/dist/mjs/components/builders/PolylineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PolylineBuilder.mjs +10 -3
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
- package/dist/mjs/image/EditorImage.mjs +1 -1
- package/dist/mjs/localizations/de.mjs +1 -1
- package/dist/mjs/localizations/es.mjs +1 -1
- package/dist/mjs/testing/createEditor.d.ts +2 -2
- package/dist/mjs/testing/createEditor.mjs +2 -2
- package/dist/mjs/toolbar/IconProvider.d.ts +3 -1
- package/dist/mjs/toolbar/IconProvider.mjs +15 -3
- package/dist/mjs/toolbar/localization.d.ts +6 -1
- package/dist/mjs/toolbar/localization.mjs +7 -2
- package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +10 -3
- package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
- package/dist/mjs/tools/Eraser.d.ts +24 -4
- package/dist/mjs/tools/Eraser.mjs +107 -21
- package/dist/mjs/tools/PasteHandler.mjs +0 -1
- package/dist/mjs/tools/lib.d.ts +1 -4
- package/dist/mjs/tools/lib.mjs +1 -4
- package/dist/mjs/version.mjs +1 -1
- package/package.json +3 -3
@@ -1,5 +1,4 @@
|
|
1
|
-
import {
|
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.
|
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
|
154
|
-
if (!
|
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
|
152
|
+
return curve.p2.minus(curve.p1).normalized();
|
169
153
|
};
|
170
154
|
const getEnterDirection = (curve) => {
|
171
|
-
return
|
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.
|
183
|
-
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.
|
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
|
-
|
223
|
+
const connector = {
|
240
224
|
kind: PathCommandType.LineTo,
|
241
225
|
point: startPoint,
|
242
226
|
};
|
243
|
-
this.
|
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
|
-
|
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
|
274
|
-
let startVec =
|
275
|
-
let endVec =
|
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
|
-
|
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 =
|
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
|
327
|
-
const lowerCurve = new
|
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 (
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
35
|
-
|
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.
|
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
|
13
|
-
|
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
|
-
|
50
|
+
getModeValue(): MutableReactiveValue<EraserMode>;
|
31
51
|
}
|