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
@@ -660,7 +660,7 @@ class ImageNode {
|
|
660
660
|
this.bbox = math_1.Rect2.union(...this.children.map(child => child.getBBox()));
|
661
661
|
}
|
662
662
|
if (bubbleUp && !oldBBox.eq(this.bbox)) {
|
663
|
-
if (
|
663
|
+
if (this.bbox.containsRect(oldBBox)) {
|
664
664
|
this.parent?.unionBBoxWith(this.bbox);
|
665
665
|
}
|
666
666
|
else {
|
@@ -29,7 +29,7 @@ const localization = {
|
|
29
29
|
selectionToolKeyboardShortcuts: 'Auswahl-Werkzeug: Verwende die Pfeiltasten, um ausgewählte Elemente zu verschieben und ‚i‘ und ‚o‘, um ihre Größe zu ändern.',
|
30
30
|
touchPanning: 'Ansicht mit Touchscreen verschieben',
|
31
31
|
anyDevicePanning: 'Ansicht mit jedem Eingabegerät verschieben',
|
32
|
-
|
32
|
+
selectPenType: 'Objekt-Typ: ',
|
33
33
|
roundedTipPen: 'Freihand',
|
34
34
|
flatTipPen: 'Stift (druckempfindlich)',
|
35
35
|
arrowPen: 'Pfeil',
|
@@ -24,7 +24,7 @@ const localization = {
|
|
24
24
|
save: 'Guardar',
|
25
25
|
undo: 'Deshace',
|
26
26
|
redo: 'Rehace',
|
27
|
-
|
27
|
+
selectPenType: 'Punta',
|
28
28
|
selectShape: 'Forma',
|
29
29
|
pickColorFromScreen: 'Selecciona un color de la pantalla',
|
30
30
|
clickToPickColorAnnouncement: 'Haga un clic en la pantalla para seleccionar un color',
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import Editor from '../Editor';
|
1
|
+
import Editor, { EditorSettings } from '../Editor';
|
2
2
|
/** Creates an editor. Should only be used in test files. */
|
3
|
-
declare const _default: () => Editor;
|
3
|
+
declare const _default: (settings?: Partial<EditorSettings>) => Editor;
|
4
4
|
export default _default;
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const Display_1 = require("../rendering/Display");
|
7
7
|
const Editor_1 = __importDefault(require("../Editor"));
|
8
8
|
/** Creates an editor. Should only be used in test files. */
|
9
|
-
exports.default = () => {
|
9
|
+
exports.default = (settings) => {
|
10
10
|
if (jest === undefined) {
|
11
11
|
throw new Error('Files in the testing/ folder should only be used in tests!');
|
12
12
|
}
|
13
|
-
return new Editor_1.default(document.body, { renderingMode: Display_1.RenderingMode.DummyRenderer });
|
13
|
+
return new Editor_1.default(document.body, { renderingMode: Display_1.RenderingMode.DummyRenderer, ...settings });
|
14
14
|
};
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { Color4 } from '@js-draw/math';
|
2
2
|
import TextRenderingStyle from '../rendering/TextRenderingStyle';
|
3
3
|
import { PenStyle } from '../tools/Pen';
|
4
|
+
import { EraserMode } from '../tools/Eraser';
|
4
5
|
export type IconElemType = HTMLImageElement | SVGElement;
|
5
6
|
/**
|
6
7
|
* Provides icons that can be used in the toolbar and other locations.
|
@@ -39,7 +40,7 @@ export default class IconProvider {
|
|
39
40
|
makeUndoIcon(): IconElemType;
|
40
41
|
makeRedoIcon(): IconElemType;
|
41
42
|
makeDropdownIcon(): IconElemType;
|
42
|
-
makeEraserIcon(eraserSize?: number): IconElemType;
|
43
|
+
makeEraserIcon(eraserSize?: number, mode?: EraserMode): IconElemType;
|
43
44
|
makeSelectionIcon(): IconElemType;
|
44
45
|
makeRotateIcon(): IconElemType;
|
45
46
|
makeHandToolIcon(): IconElemType;
|
@@ -88,6 +89,7 @@ export default class IconProvider {
|
|
88
89
|
* @returns true if the given `penStyle` is known to match a rounded tip type of pen.
|
89
90
|
*/
|
90
91
|
protected isRoundedTipPen(penStyle: PenStyle): boolean;
|
92
|
+
protected isPolylinePen(penStyle: PenStyle): boolean;
|
91
93
|
/** Must be overridden by icon packs that need attribution. */
|
92
94
|
licenseInfo(): string | null;
|
93
95
|
}
|
@@ -13,6 +13,8 @@ const math_1 = require("@js-draw/math");
|
|
13
13
|
const SVGRenderer_1 = __importDefault(require("../rendering/renderers/SVGRenderer"));
|
14
14
|
const Viewport_1 = __importDefault(require("../Viewport"));
|
15
15
|
const FreehandLineBuilder_1 = require("../components/builders/FreehandLineBuilder");
|
16
|
+
const PolylineBuilder_1 = require("../components/builders/PolylineBuilder");
|
17
|
+
const Eraser_1 = require("../tools/Eraser");
|
16
18
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
17
19
|
const iconColorFill = `
|
18
20
|
style='fill: var(--icon-color);'
|
@@ -117,16 +119,23 @@ class IconProvider {
|
|
117
119
|
icon.setAttribute('viewBox', '-10 -10 110 110');
|
118
120
|
return icon;
|
119
121
|
}
|
120
|
-
makeEraserIcon(eraserSize) {
|
122
|
+
makeEraserIcon(eraserSize, mode) {
|
121
123
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
122
124
|
eraserSize ??= 10;
|
123
125
|
const scaledSize = eraserSize / 4;
|
124
126
|
const eraserColor = '#ff70af';
|
125
127
|
// Draw an eraser-like shape. Created with Inkscape
|
126
128
|
icon.innerHTML = `
|
129
|
+
<defs>
|
130
|
+
<linearGradient id="dash-pattern">
|
131
|
+
<stop offset="80%" stop-color="${eraserColor}"/>
|
132
|
+
<stop offset="85%" stop-color="white"/>
|
133
|
+
<stop offset="90%" stop-color="${eraserColor}"/>
|
134
|
+
</linearGradient>
|
135
|
+
</defs>
|
127
136
|
<g>
|
128
137
|
<path
|
129
|
-
style="fill:${eraserColor}"
|
138
|
+
style="fill:${mode === Eraser_1.EraserMode.PartialStroke ? 'url(#dash-pattern)' : eraserColor}"
|
130
139
|
stroke="black"
|
131
140
|
transform="rotate(41.35)"
|
132
141
|
d="M 52.5 27
|
@@ -840,7 +849,10 @@ class IconProvider {
|
|
840
849
|
* @returns true if the given `penStyle` is known to match a rounded tip type of pen.
|
841
850
|
*/
|
842
851
|
isRoundedTipPen(penStyle) {
|
843
|
-
return penStyle.factory === FreehandLineBuilder_1.makeFreehandLineBuilder;
|
852
|
+
return penStyle.factory === FreehandLineBuilder_1.makeFreehandLineBuilder || penStyle.factory === PolylineBuilder_1.makePolylineBuilder;
|
853
|
+
}
|
854
|
+
isPolylinePen(penStyle) {
|
855
|
+
return penStyle.factory === PolylineBuilder_1.makePolylineBuilder;
|
844
856
|
}
|
845
857
|
/** Must be overridden by icon packs that need attribution. */
|
846
858
|
licenseInfo() { return null; }
|
@@ -18,8 +18,9 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
|
|
18
18
|
cancel: string;
|
19
19
|
submit: string;
|
20
20
|
roundedTipPen: string;
|
21
|
+
roundedTipPen2: string;
|
21
22
|
flatTipPen: string;
|
22
|
-
|
23
|
+
selectPenType: string;
|
23
24
|
selectShape: string;
|
24
25
|
colorLabel: string;
|
25
26
|
pen: string;
|
@@ -30,6 +31,7 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
|
|
30
31
|
resizeImageToSelection: string;
|
31
32
|
deleteSelection: string;
|
32
33
|
duplicateSelection: string;
|
34
|
+
fullStrokeEraser: string;
|
33
35
|
pickColorFromScreen: string;
|
34
36
|
clickToPickColorAnnouncement: string;
|
35
37
|
colorSelectionCanceledAnnouncement: string;
|
@@ -66,7 +68,10 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
|
|
66
68
|
handDropdown__zoomOutHelpText: string;
|
67
69
|
handDropdown__resetViewHelpText: string;
|
68
70
|
handDropdown__touchPanningHelpText: string;
|
71
|
+
eraserDropdown__baseHelpText: string;
|
72
|
+
eraserDropdown__fullStrokeEraserHelpText: string;
|
69
73
|
handDropdown__lockRotationHelpText: string;
|
74
|
+
eraserDropdown__thicknessHelpText: string;
|
70
75
|
selectionDropdown__baseHelpText: string;
|
71
76
|
selectionDropdown__resizeToHelpText: string;
|
72
77
|
selectionDropdown__deleteHelpText: string;
|
@@ -30,7 +30,8 @@ exports.defaultToolbarLocalization = {
|
|
30
30
|
save: 'Save',
|
31
31
|
undo: 'Undo',
|
32
32
|
redo: 'Redo',
|
33
|
-
|
33
|
+
fullStrokeEraser: 'Full stroke eraser',
|
34
|
+
selectPenType: 'Pen type',
|
34
35
|
selectShape: 'Shape',
|
35
36
|
pickColorFromScreen: 'Pick color from screen',
|
36
37
|
clickToPickColorAnnouncement: 'Click on the screen to pick a color',
|
@@ -48,6 +49,7 @@ exports.defaultToolbarLocalization = {
|
|
48
49
|
strokeAutocorrect: 'Autocorrect',
|
49
50
|
touchPanning: 'Touchscreen panning',
|
50
51
|
roundedTipPen: 'Round',
|
52
|
+
roundedTipPen2: 'Polyline',
|
51
53
|
flatTipPen: 'Flat',
|
52
54
|
arrowPen: 'Arrow',
|
53
55
|
linePen: 'Line',
|
@@ -62,7 +64,7 @@ exports.defaultToolbarLocalization = {
|
|
62
64
|
penDropdown__baseHelpText: 'This tool draws shapes or freehand lines.',
|
63
65
|
penDropdown__colorHelpText: 'Changes the pen\'s color',
|
64
66
|
penDropdown__thicknessHelpText: 'Changes the thickness of strokes drawn by the pen.',
|
65
|
-
penDropdown__penTypeHelpText: 'Changes the pen style.\n\nEither a “pen
|
67
|
+
penDropdown__penTypeHelpText: 'Changes the pen style.\n\nEither a “pen” style or “shape” can be chosen. Choosing a “pen” style draws freehand lines. Choosing a “shape” draws shapes.',
|
66
68
|
penDropdown__autocorrectHelpText: 'Converts approximate freehand lines and rectangles to perfect ones.\n\nThe pen must be held stationary at the end of a stroke to trigger a correction.',
|
67
69
|
penDropdown__stabilizationHelpText: 'Draws smoother strokes.\n\nThis also adds a short delay between the mouse/stylus and the stroke.',
|
68
70
|
handDropdown__baseHelpText: 'This tool is responsible for scrolling, rotating, and zooming the editor.',
|
@@ -72,6 +74,9 @@ exports.defaultToolbarLocalization = {
|
|
72
74
|
handDropdown__zoomDisplayHelpText: 'Shows the current zoom level. 100% shows the image at its actual size.',
|
73
75
|
handDropdown__touchPanningHelpText: 'When enabled, touch gestures move the image rather than select or draw.',
|
74
76
|
handDropdown__lockRotationHelpText: 'When enabled, prevents touch gestures from rotating the screen.',
|
77
|
+
eraserDropdown__baseHelpText: 'This tool removes strokes, images, and text under the cursor.',
|
78
|
+
eraserDropdown__thicknessHelpText: 'Changes the size of the eraser.',
|
79
|
+
eraserDropdown__fullStrokeEraserHelpText: 'When in full-stroke mode, entire shapes are erased.\n\nWhen not in full-stroke mode, shapes can be partially erased.',
|
75
80
|
selectionDropdown__baseHelpText: 'Selects content and manipulates the selection',
|
76
81
|
selectionDropdown__resizeToHelpText: 'Crops the drawing to the size of what\'s currently selected.\n\nIf auto-resize is enabled, it will be disabled.',
|
77
82
|
selectionDropdown__deleteHelpText: 'Erases selected items.',
|
@@ -1,15 +1,20 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
2
|
import Eraser from '../../tools/Eraser';
|
3
3
|
import { ToolbarLocalization } from '../localization';
|
4
|
+
import HelpDisplay from '../utils/HelpDisplay';
|
4
5
|
import BaseToolWidget from './BaseToolWidget';
|
5
6
|
import { SavedToolbuttonState } from './BaseWidget';
|
6
7
|
export default class EraserToolWidget extends BaseToolWidget {
|
7
8
|
private tool;
|
8
9
|
private updateInputs;
|
9
10
|
constructor(editor: Editor, tool: Eraser, localizationTable?: ToolbarLocalization);
|
11
|
+
protected getHelpText(): string;
|
10
12
|
protected getTitle(): string;
|
13
|
+
private makeIconForType;
|
11
14
|
protected createIcon(): Element;
|
12
|
-
|
15
|
+
private static idCounter;
|
16
|
+
private makeEraserTypeSelector;
|
17
|
+
protected fillDropdown(dropdown: HTMLElement, helpDisplay?: HelpDisplay): boolean;
|
13
18
|
serializeState(): SavedToolbuttonState;
|
14
19
|
deserializeFrom(state: SavedToolbuttonState): void;
|
15
20
|
}
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const Eraser_1 = require("../../tools/Eraser");
|
6
7
|
const types_1 = require("../../types");
|
7
8
|
const constants_1 = require("../constants");
|
8
9
|
const BaseToolWidget_1 = __importDefault(require("./BaseToolWidget"));
|
@@ -19,26 +20,57 @@ class EraserToolWidget extends BaseToolWidget_1.default {
|
|
19
20
|
}
|
20
21
|
});
|
21
22
|
}
|
23
|
+
getHelpText() {
|
24
|
+
return this.localizationTable.eraserDropdown__baseHelpText;
|
25
|
+
}
|
22
26
|
getTitle() {
|
23
27
|
return this.localizationTable.eraser;
|
24
28
|
}
|
29
|
+
makeIconForType(mode) {
|
30
|
+
return this.editor.icons.makeEraserIcon(this.tool.getThickness(), mode);
|
31
|
+
}
|
25
32
|
createIcon() {
|
26
|
-
return this.
|
33
|
+
return this.makeIconForType(this.tool.getModeValue().get());
|
34
|
+
}
|
35
|
+
makeEraserTypeSelector(helpDisplay) {
|
36
|
+
const container = document.createElement('div');
|
37
|
+
const labelElement = document.createElement('label');
|
38
|
+
const checkboxElement = document.createElement('input');
|
39
|
+
checkboxElement.id = `${constants_1.toolbarCSSPrefix}eraserToolWidget-${EraserToolWidget.idCounter++}`;
|
40
|
+
labelElement.htmlFor = checkboxElement.id;
|
41
|
+
labelElement.innerText = this.localizationTable.fullStrokeEraser;
|
42
|
+
checkboxElement.type = 'checkbox';
|
43
|
+
checkboxElement.oninput = () => {
|
44
|
+
this.tool.getModeValue().set(checkboxElement.checked ? Eraser_1.EraserMode.FullStroke : Eraser_1.EraserMode.PartialStroke);
|
45
|
+
};
|
46
|
+
const updateValue = () => {
|
47
|
+
checkboxElement.checked = this.tool.getModeValue().get() === Eraser_1.EraserMode.FullStroke;
|
48
|
+
};
|
49
|
+
container.replaceChildren(labelElement, checkboxElement);
|
50
|
+
helpDisplay?.registerTextHelpForElement(container, this.localizationTable.eraserDropdown__fullStrokeEraserHelpText);
|
51
|
+
return {
|
52
|
+
addTo: (parent) => {
|
53
|
+
parent.appendChild(container);
|
54
|
+
},
|
55
|
+
updateValue,
|
56
|
+
};
|
27
57
|
}
|
28
|
-
fillDropdown(dropdown) {
|
58
|
+
fillDropdown(dropdown, helpDisplay) {
|
29
59
|
const container = document.createElement('div');
|
30
60
|
container.classList.add(`${constants_1.toolbarCSSPrefix}spacedList`, `${constants_1.toolbarCSSPrefix}nonbutton-controls-main-list`);
|
31
61
|
const thicknessSlider = (0, makeThicknessSlider_1.default)(this.editor, thickness => {
|
32
62
|
this.tool.setThickness(thickness);
|
33
63
|
});
|
34
64
|
thicknessSlider.setBounds(10, 55);
|
65
|
+
helpDisplay?.registerTextHelpForElement(thicknessSlider.container, this.localizationTable.eraserDropdown__thicknessHelpText);
|
66
|
+
const modeSelector = this.makeEraserTypeSelector(helpDisplay);
|
35
67
|
this.updateInputs = () => {
|
36
68
|
thicknessSlider.setValue(this.tool.getThickness());
|
69
|
+
modeSelector.updateValue();
|
37
70
|
};
|
38
71
|
this.updateInputs();
|
39
|
-
|
40
|
-
|
41
|
-
container.replaceChildren(thicknessSlider.container, spacer);
|
72
|
+
container.replaceChildren(thicknessSlider.container);
|
73
|
+
modeSelector.addTo(container);
|
42
74
|
dropdown.replaceChildren(container);
|
43
75
|
return true;
|
44
76
|
}
|
@@ -46,6 +78,7 @@ class EraserToolWidget extends BaseToolWidget_1.default {
|
|
46
78
|
return {
|
47
79
|
...super.serializeState(),
|
48
80
|
thickness: this.tool.getThickness(),
|
81
|
+
mode: this.tool.getModeValue().get(),
|
49
82
|
};
|
50
83
|
}
|
51
84
|
deserializeFrom(state) {
|
@@ -57,6 +90,13 @@ class EraserToolWidget extends BaseToolWidget_1.default {
|
|
57
90
|
}
|
58
91
|
this.tool.setThickness(parsedThickness);
|
59
92
|
}
|
93
|
+
if (state.mode) {
|
94
|
+
const mode = state.mode;
|
95
|
+
if (Object.values(Eraser_1.EraserMode).includes(mode)) {
|
96
|
+
this.tool.getModeValue().set(mode);
|
97
|
+
}
|
98
|
+
}
|
60
99
|
}
|
61
100
|
}
|
101
|
+
EraserToolWidget.idCounter = 0;
|
62
102
|
exports.default = EraserToolWidget;
|
@@ -17,6 +17,7 @@ const keybindings_1 = require("./keybindings");
|
|
17
17
|
const constants_1 = require("../constants");
|
18
18
|
const makeThicknessSlider_1 = __importDefault(require("./components/makeThicknessSlider"));
|
19
19
|
const makeGridSelector_1 = __importDefault(require("./components/makeGridSelector"));
|
20
|
+
const PolylineBuilder_1 = require("../../components/builders/PolylineBuilder");
|
20
21
|
class PenToolWidget extends BaseToolWidget_1.default {
|
21
22
|
constructor(editor, tool, localization) {
|
22
23
|
super(editor, tool, 'pen', localization);
|
@@ -26,6 +27,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
26
27
|
this.shapelikeIDs = ['pressure-sensitive-pen', 'freehand-pen'];
|
27
28
|
// Additional client-specified pens.
|
28
29
|
const additionalPens = editor.getCurrentSettings().pens?.additionalPenTypes ?? [];
|
30
|
+
const filterPens = editor.getCurrentSettings().pens?.filterPenTypes ?? (() => true);
|
29
31
|
// Default pen types
|
30
32
|
this.penTypes = [
|
31
33
|
// Non-shape pens
|
@@ -39,6 +41,11 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
39
41
|
id: 'freehand-pen',
|
40
42
|
factory: FreehandLineBuilder_1.makeFreehandLineBuilder,
|
41
43
|
},
|
44
|
+
{
|
45
|
+
name: this.localizationTable.roundedTipPen2,
|
46
|
+
id: 'polyline-pen',
|
47
|
+
factory: PolylineBuilder_1.makePolylineBuilder,
|
48
|
+
},
|
42
49
|
...(additionalPens.filter(pen => !pen.isShapeBuilder)),
|
43
50
|
// Shape pens
|
44
51
|
{
|
@@ -72,7 +79,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
72
79
|
factory: CircleBuilder_1.makeOutlinedCircleBuilder,
|
73
80
|
},
|
74
81
|
...(additionalPens.filter(pen => pen.isShapeBuilder)),
|
75
|
-
];
|
82
|
+
].filter(filterPens);
|
76
83
|
this.editor.notifier.on(types_1.EditorEventType.ToolUpdated, toolEvt => {
|
77
84
|
if (toolEvt.kind !== types_1.EditorEventType.ToolUpdated) {
|
78
85
|
throw new Error('Invalid event type!');
|
@@ -116,7 +123,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
116
123
|
style.factory = record.factory;
|
117
124
|
}
|
118
125
|
const strokeFactory = record?.factory;
|
119
|
-
if (!strokeFactory || strokeFactory === FreehandLineBuilder_1.makeFreehandLineBuilder || strokeFactory === PressureSensitiveFreehandLineBuilder_1.makePressureSensitiveFreehandLineBuilder) {
|
126
|
+
if (!strokeFactory || strokeFactory === FreehandLineBuilder_1.makeFreehandLineBuilder || strokeFactory === PressureSensitiveFreehandLineBuilder_1.makePressureSensitiveFreehandLineBuilder || strokeFactory === PolylineBuilder_1.makePolylineBuilder) {
|
120
127
|
return this.editor.icons.makePenIcon(style);
|
121
128
|
}
|
122
129
|
else {
|
@@ -136,7 +143,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
136
143
|
isShapeBuilder: penType.isShapeBuilder ?? false,
|
137
144
|
};
|
138
145
|
});
|
139
|
-
const penSelector = (0, makeGridSelector_1.default)(this.localizationTable.
|
146
|
+
const penSelector = (0, makeGridSelector_1.default)(this.localizationTable.selectPenType, this.getCurrentPenTypeIdx(), allChoices.filter(choice => !choice.isShapeBuilder));
|
140
147
|
const shapeSelector = (0, makeGridSelector_1.default)(this.localizationTable.selectShape, this.getCurrentPenTypeIdx(), allChoices.filter(choice => choice.isShapeBuilder));
|
141
148
|
const onSelectorUpdate = (newPenTypeIndex) => {
|
142
149
|
this.tool.setStrokeFactory(this.penTypes[newPenTypeIndex].factory);
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -9,7 +9,7 @@ const KeyboardShortcutManager_1 = __importDefault(require("../../shortcuts/Keybo
|
|
9
9
|
exports.resizeImageToSelectionKeyboardShortcut = 'jsdraw.toolbar.SelectionTool.resizeImageToSelection';
|
10
10
|
KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(exports.resizeImageToSelectionKeyboardShortcut, ['ctrlOrMeta+r'], 'Resize image to selection');
|
11
11
|
// Pen tool
|
12
|
-
exports.selectStrokeTypeKeyboardShortcutIds = [1, 2, 3, 4, 5, 6, 7].map(id => `jsdraw.toolbar.PenTool.select-pen-${id}`);
|
12
|
+
exports.selectStrokeTypeKeyboardShortcutIds = [1, 2, 3, 4, 5, 6, 7, 8, 9].map(id => `jsdraw.toolbar.PenTool.select-pen-${id}`);
|
13
13
|
for (let i = 0; i < exports.selectStrokeTypeKeyboardShortcutIds.length; i++) {
|
14
14
|
const id = exports.selectStrokeTypeKeyboardShortcutIds[i];
|
15
15
|
KeyboardShortcutManager_1.default.registerDefaultKeyboardShortcut(id, [`CtrlOrMeta+Digit${(i + 1)}`], 'Select pen style ' + (i + 1));
|
@@ -2,30 +2,50 @@ import { KeyPressEvent, PointerEvt } from '../inputEvents';
|
|
2
2
|
import BaseTool from './BaseTool';
|
3
3
|
import Editor from '../Editor';
|
4
4
|
import { MutableReactiveValue } from '../util/ReactiveValue';
|
5
|
+
export declare enum EraserMode {
|
6
|
+
PartialStroke = "partial-stroke",
|
7
|
+
FullStroke = "full-stroke"
|
8
|
+
}
|
9
|
+
export interface InitialEraserOptions {
|
10
|
+
thickness?: number;
|
11
|
+
mode?: EraserMode;
|
12
|
+
}
|
5
13
|
export default class Eraser extends BaseTool {
|
6
14
|
private editor;
|
7
15
|
private lastPoint;
|
8
16
|
private isFirstEraseEvt;
|
9
|
-
private toRemove;
|
10
17
|
private thickness;
|
11
18
|
private thicknessValue;
|
12
|
-
private
|
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
|
}
|
package/dist/cjs/tools/Eraser.js
CHANGED
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.EraserMode = void 0;
|
6
7
|
const types_1 = require("../types");
|
7
8
|
const BaseTool_1 = __importDefault(require("./BaseTool"));
|
8
9
|
const math_1 = require("@js-draw/math");
|
@@ -10,15 +11,24 @@ const Erase_1 = __importDefault(require("../commands/Erase"));
|
|
10
11
|
const Pointer_1 = require("../Pointer");
|
11
12
|
const keybindings_1 = require("./keybindings");
|
12
13
|
const ReactiveValue_1 = require("../util/ReactiveValue");
|
14
|
+
const EditorImage_1 = __importDefault(require("../image/EditorImage"));
|
15
|
+
const uniteCommands_1 = __importDefault(require("../commands/uniteCommands"));
|
16
|
+
const RenderablePathSpec_1 = require("../rendering/RenderablePathSpec");
|
17
|
+
var EraserMode;
|
18
|
+
(function (EraserMode) {
|
19
|
+
EraserMode["PartialStroke"] = "partial-stroke";
|
20
|
+
EraserMode["FullStroke"] = "full-stroke";
|
21
|
+
})(EraserMode || (exports.EraserMode = EraserMode = {}));
|
13
22
|
class Eraser extends BaseTool_1.default {
|
14
|
-
constructor(editor, description) {
|
23
|
+
constructor(editor, description, options) {
|
15
24
|
super(editor.notifier, description);
|
16
25
|
this.editor = editor;
|
17
26
|
this.lastPoint = null;
|
18
27
|
this.isFirstEraseEvt = true;
|
19
|
-
this.thickness = 10;
|
20
28
|
// Commands that each remove one element
|
21
|
-
this.
|
29
|
+
this.eraseCommands = [];
|
30
|
+
this.addCommands = [];
|
31
|
+
this.thickness = options?.thickness ?? 10;
|
22
32
|
this.thicknessValue = ReactiveValue_1.ReactiveValue.fromInitialValue(this.thickness);
|
23
33
|
this.thicknessValue.onUpdate(value => {
|
24
34
|
this.thickness = value;
|
@@ -27,6 +37,13 @@ class Eraser extends BaseTool_1.default {
|
|
27
37
|
tool: this,
|
28
38
|
});
|
29
39
|
});
|
40
|
+
this.modeValue = ReactiveValue_1.ReactiveValue.fromInitialValue(options?.mode ?? EraserMode.FullStroke);
|
41
|
+
this.modeValue.onUpdate(_value => {
|
42
|
+
this.editor.notifier.dispatch(types_1.EditorEventType.ToolUpdated, {
|
43
|
+
kind: types_1.EditorEventType.ToolUpdated,
|
44
|
+
tool: this,
|
45
|
+
});
|
46
|
+
});
|
30
47
|
}
|
31
48
|
clearPreview() {
|
32
49
|
this.editor.clearWetInk();
|
@@ -39,16 +56,24 @@ class Eraser extends BaseTool_1.default {
|
|
39
56
|
const size = this.getSizeOnCanvas();
|
40
57
|
const renderer = this.editor.display.getWetInkRenderer();
|
41
58
|
const rect = this.getEraserRect(point);
|
59
|
+
const rect2 = this.getEraserRect(this.lastPoint ?? point);
|
42
60
|
const fill = {
|
43
|
-
fill: math_1.Color4.
|
61
|
+
fill: math_1.Color4.transparent,
|
62
|
+
stroke: { width: size / 10, color: math_1.Color4.gray },
|
44
63
|
};
|
45
|
-
renderer.
|
64
|
+
renderer.drawPath((0, RenderablePathSpec_1.pathToRenderable)(math_1.Path.fromConvexHullOf([...rect.corners, ...rect2.corners]), fill));
|
46
65
|
}
|
66
|
+
/**
|
67
|
+
* @returns the eraser rectangle in canvas coordinates.
|
68
|
+
*
|
69
|
+
* For now, all erasers are rectangles or points.
|
70
|
+
*/
|
47
71
|
getEraserRect(centerPoint) {
|
48
72
|
const size = this.getSizeOnCanvas();
|
49
73
|
const halfSize = math_1.Vec2.of(size / 2, size / 2);
|
50
74
|
return math_1.Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
|
51
75
|
}
|
76
|
+
/** Erases in a line from the last point to the current. */
|
52
77
|
eraseTo(currentPoint) {
|
53
78
|
if (!this.isFirstEraseEvt && currentPoint.distanceTo(this.lastPoint) === 0) {
|
54
79
|
return;
|
@@ -65,13 +90,55 @@ class Eraser extends BaseTool_1.default {
|
|
65
90
|
});
|
66
91
|
// Only erase components that could be selected (and thus interacted with)
|
67
92
|
// by the user.
|
68
|
-
const
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
93
|
+
const eraseableElems = intersectingElems.filter(elem => elem.isSelectable());
|
94
|
+
if (this.modeValue.get() === EraserMode.FullStroke) {
|
95
|
+
// Remove any intersecting elements.
|
96
|
+
this.toRemove.push(...eraseableElems);
|
97
|
+
// Create new Erase commands for the now-to-be-erased elements and apply them.
|
98
|
+
const newPartialCommands = eraseableElems.map(elem => new Erase_1.default([elem]));
|
99
|
+
newPartialCommands.forEach(cmd => cmd.apply(this.editor));
|
100
|
+
this.eraseCommands.push(...newPartialCommands);
|
101
|
+
}
|
102
|
+
else {
|
103
|
+
const toErase = [];
|
104
|
+
const toAdd = [];
|
105
|
+
for (const targetElem of eraseableElems) {
|
106
|
+
toErase.push(targetElem);
|
107
|
+
// Completely delete items that can't be divided.
|
108
|
+
if (!targetElem.withRegionErased) {
|
109
|
+
continue;
|
110
|
+
}
|
111
|
+
// Completely delete items that are completely or almost completely
|
112
|
+
// contained within the eraser.
|
113
|
+
const grownRect = eraserRect.grownBy(eraserRect.maxDimension / 3);
|
114
|
+
if (grownRect.containsRect(targetElem.getExactBBox())) {
|
115
|
+
continue;
|
116
|
+
}
|
117
|
+
// Join the current and previous rectangles so that points between events are also
|
118
|
+
// erased.
|
119
|
+
const erasePath = math_1.Path.fromConvexHullOf([
|
120
|
+
...eraserRect.corners, ...this.getEraserRect(this.lastPoint ?? currentPoint).corners
|
121
|
+
].map(p => this.editor.viewport.roundPoint(p)));
|
122
|
+
toAdd.push(...targetElem.withRegionErased(erasePath, this.editor.viewport));
|
123
|
+
}
|
124
|
+
const eraseCommand = new Erase_1.default(toErase);
|
125
|
+
const newAddCommands = toAdd.map(elem => EditorImage_1.default.addElement(elem));
|
126
|
+
eraseCommand.apply(this.editor);
|
127
|
+
newAddCommands.forEach(command => command.apply(this.editor));
|
128
|
+
const finalToErase = [];
|
129
|
+
for (const item of toErase) {
|
130
|
+
if (this.toAdd.includes(item)) {
|
131
|
+
this.toAdd = this.toAdd.filter(i => i !== item);
|
132
|
+
}
|
133
|
+
else {
|
134
|
+
finalToErase.push(item);
|
135
|
+
}
|
136
|
+
}
|
137
|
+
this.toRemove.push(...finalToErase);
|
138
|
+
this.toAdd.push(...toAdd);
|
139
|
+
this.eraseCommands.push(new Erase_1.default(finalToErase));
|
140
|
+
this.addCommands.push(...newAddCommands);
|
141
|
+
}
|
75
142
|
this.drawPreviewAt(currentPoint);
|
76
143
|
this.lastPoint = currentPoint;
|
77
144
|
}
|
@@ -79,6 +146,7 @@ class Eraser extends BaseTool_1.default {
|
|
79
146
|
if (event.allPointers.length === 1 || event.current.device === Pointer_1.PointerDevice.Eraser) {
|
80
147
|
this.lastPoint = event.current.canvasPos;
|
81
148
|
this.toRemove = [];
|
149
|
+
this.toAdd = [];
|
82
150
|
this.isFirstEraseEvt = true;
|
83
151
|
this.drawPreviewAt(event.current.canvasPos);
|
84
152
|
return true;
|
@@ -91,18 +159,32 @@ class Eraser extends BaseTool_1.default {
|
|
91
159
|
}
|
92
160
|
onPointerUp(event) {
|
93
161
|
this.eraseTo(event.current.canvasPos);
|
94
|
-
|
162
|
+
const commands = [];
|
163
|
+
if (this.addCommands.length > 0) {
|
164
|
+
this.addCommands.forEach(cmd => cmd.unapply(this.editor));
|
165
|
+
commands.push(...this.toAdd.map(a => EditorImage_1.default.addElement(a)));
|
166
|
+
this.addCommands = [];
|
167
|
+
}
|
168
|
+
if (this.eraseCommands.length > 0) {
|
95
169
|
// Undo commands for each individual component and unite into a single command.
|
96
|
-
this.
|
97
|
-
this.
|
170
|
+
this.eraseCommands.forEach(cmd => cmd.unapply(this.editor));
|
171
|
+
this.eraseCommands = [];
|
98
172
|
const command = new Erase_1.default(this.toRemove);
|
99
|
-
|
173
|
+
commands.push(command);
|
174
|
+
}
|
175
|
+
if (commands.length === 1) {
|
176
|
+
this.editor.dispatch(commands[0]); // dispatch: Makes undo-able.
|
177
|
+
}
|
178
|
+
else {
|
179
|
+
this.editor.dispatch((0, uniteCommands_1.default)(commands));
|
100
180
|
}
|
101
181
|
this.clearPreview();
|
102
182
|
}
|
103
183
|
onGestureCancel() {
|
104
|
-
this.
|
105
|
-
this.
|
184
|
+
this.addCommands.forEach(cmd => cmd.unapply(this.editor));
|
185
|
+
this.eraseCommands.forEach(cmd => cmd.unapply(this.editor));
|
186
|
+
this.eraseCommands = [];
|
187
|
+
this.addCommands = [];
|
106
188
|
this.clearPreview();
|
107
189
|
}
|
108
190
|
onKeyPress(event) {
|
@@ -121,9 +203,14 @@ class Eraser extends BaseTool_1.default {
|
|
121
203
|
}
|
122
204
|
return false;
|
123
205
|
}
|
206
|
+
/** Returns the side-length of the tip of this eraser. */
|
124
207
|
getThickness() {
|
125
208
|
return this.thickness;
|
126
209
|
}
|
210
|
+
/** Sets the side-length of this' tip. */
|
211
|
+
setThickness(thickness) {
|
212
|
+
this.thicknessValue.set(thickness);
|
213
|
+
}
|
127
214
|
/**
|
128
215
|
* Returns a {@link MutableReactiveValue} that can be used to watch
|
129
216
|
* this tool's thickness.
|
@@ -131,8 +218,8 @@ class Eraser extends BaseTool_1.default {
|
|
131
218
|
getThicknessValue() {
|
132
219
|
return this.thicknessValue;
|
133
220
|
}
|
134
|
-
|
135
|
-
this.
|
221
|
+
getModeValue() {
|
222
|
+
return this.modeValue;
|
136
223
|
}
|
137
224
|
}
|
138
225
|
exports.default = Eraser;
|