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.
- 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
|
}
|