js-draw 1.16.1 → 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/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +75 -7
- package/dist/cjs/Editor.js +93 -90
- package/dist/cjs/Pointer.d.ts +2 -1
- package/dist/cjs/Pointer.js +9 -2
- package/dist/cjs/commands/localization.d.ts +1 -0
- package/dist/cjs/commands/localization.js +1 -0
- package/dist/cjs/commands/uniteCommands.d.ts +5 -1
- package/dist/cjs/commands/uniteCommands.js +33 -7
- 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/TextComponent.d.ts +36 -1
- package/dist/cjs/components/TextComponent.js +39 -1
- package/dist/cjs/components/builders/ArrowBuilder.js +1 -1
- package/dist/cjs/components/builders/PolylineBuilder.d.ts +35 -0
- package/dist/cjs/components/builders/PolylineBuilder.js +122 -0
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
- package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +1 -1
- package/dist/cjs/components/lib.d.ts +1 -0
- package/dist/cjs/components/lib.js +3 -1
- package/dist/cjs/components/util/StrokeSmoother.js +4 -4
- package/dist/cjs/image/EditorImage.d.ts +4 -1
- package/dist/cjs/image/EditorImage.js +5 -2
- package/dist/cjs/inputEvents.d.ts +11 -1
- package/dist/cjs/localizations/comments.d.ts +3 -0
- package/dist/cjs/localizations/comments.js +3 -0
- package/dist/cjs/localizations/de.js +1 -3
- package/dist/cjs/localizations/es.js +3 -3
- package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +16 -0
- package/dist/cjs/rendering/renderers/SVGRenderer.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 +9 -4
- package/dist/cjs/toolbar/IconProvider.js +21 -7
- package/dist/cjs/toolbar/localization.d.ts +6 -1
- package/dist/cjs/toolbar/localization.js +7 -2
- package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
- 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.d.ts +1 -1
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +17 -4
- 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 +108 -21
- package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
- package/dist/cjs/tools/PasteHandler.js +35 -10
- package/dist/cjs/tools/Pen.js +2 -2
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +23 -4
- package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +1 -1
- package/dist/cjs/tools/ToolController.d.ts +17 -1
- package/dist/cjs/tools/ToolController.js +21 -8
- package/dist/cjs/tools/lib.d.ts +1 -4
- package/dist/cjs/tools/lib.js +2 -4
- package/dist/cjs/tools/localization.d.ts +2 -2
- package/dist/cjs/tools/localization.js +2 -2
- package/dist/cjs/util/ClipboardHandler.d.ts +27 -0
- package/dist/cjs/util/ClipboardHandler.js +205 -0
- package/dist/cjs/util/ClipboardHandler.test.d.ts +1 -0
- package/dist/cjs/version.d.ts +5 -0
- package/dist/cjs/version.js +6 -1
- package/dist/mjs/Editor.d.ts +75 -7
- package/dist/mjs/Editor.mjs +93 -90
- package/dist/mjs/Pointer.d.ts +2 -1
- package/dist/mjs/Pointer.mjs +9 -2
- package/dist/mjs/commands/localization.d.ts +1 -0
- package/dist/mjs/commands/localization.mjs +1 -0
- package/dist/mjs/commands/uniteCommands.d.ts +5 -1
- package/dist/mjs/commands/uniteCommands.mjs +33 -7
- 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/TextComponent.d.ts +36 -1
- package/dist/mjs/components/TextComponent.mjs +40 -2
- package/dist/mjs/components/builders/ArrowBuilder.mjs +1 -1
- package/dist/mjs/components/builders/PolylineBuilder.d.ts +35 -0
- package/dist/mjs/components/builders/PolylineBuilder.mjs +115 -0
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
- package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +1 -1
- package/dist/mjs/components/lib.d.ts +1 -0
- package/dist/mjs/components/lib.mjs +1 -0
- package/dist/mjs/components/util/StrokeSmoother.mjs +4 -4
- package/dist/mjs/image/EditorImage.d.ts +4 -1
- package/dist/mjs/image/EditorImage.mjs +5 -2
- package/dist/mjs/inputEvents.d.ts +11 -1
- package/dist/mjs/localizations/comments.d.ts +3 -0
- package/dist/mjs/localizations/comments.mjs +3 -0
- package/dist/mjs/localizations/de.mjs +1 -3
- package/dist/mjs/localizations/es.mjs +3 -3
- package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +16 -0
- package/dist/mjs/rendering/renderers/SVGRenderer.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 +9 -4
- package/dist/mjs/toolbar/IconProvider.mjs +21 -7
- package/dist/mjs/toolbar/localization.d.ts +6 -1
- package/dist/mjs/toolbar/localization.mjs +7 -2
- package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
- 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.d.ts +1 -1
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +17 -4
- 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 +108 -22
- package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
- package/dist/mjs/tools/PasteHandler.mjs +35 -10
- package/dist/mjs/tools/Pen.mjs +2 -2
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +23 -4
- package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +1 -1
- package/dist/mjs/tools/ToolController.d.ts +17 -1
- package/dist/mjs/tools/ToolController.mjs +21 -8
- package/dist/mjs/tools/lib.d.ts +1 -4
- package/dist/mjs/tools/lib.mjs +1 -4
- package/dist/mjs/tools/localization.d.ts +2 -2
- package/dist/mjs/tools/localization.mjs +2 -2
- package/dist/mjs/util/ClipboardHandler.d.ts +27 -0
- package/dist/mjs/util/ClipboardHandler.mjs +200 -0
- package/dist/mjs/util/ClipboardHandler.test.d.ts +1 -0
- package/dist/mjs/version.d.ts +5 -0
- package/dist/mjs/version.mjs +6 -1
- package/package.json +6 -6
@@ -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.',
|
@@ -166,7 +166,30 @@ class DocumentPropertiesWidget extends BaseWidget {
|
|
166
166
|
row.replaceChildren(label, input);
|
167
167
|
return {
|
168
168
|
setValue: (value) => {
|
169
|
-
|
169
|
+
// Slightly improve the case where the user tries to change the
|
170
|
+
// first digit of a dimension like 600.
|
171
|
+
//
|
172
|
+
// As changing the value also gives the image zero size (which is unsupported,
|
173
|
+
// .setValue is called immediately). We work around this by trying to select
|
174
|
+
// the added/changed digits.
|
175
|
+
//
|
176
|
+
// See https://github.com/personalizedrefrigerator/js-draw/issues/58.
|
177
|
+
if (document.activeElement === input && input.value.match(/^0*$/)) {
|
178
|
+
// We need to switch to type="text" and back to type="number" because
|
179
|
+
// number inputs don't support selection.
|
180
|
+
//
|
181
|
+
// See https://stackoverflow.com/q/22381837
|
182
|
+
const originalValue = input.value;
|
183
|
+
input.type = 'text';
|
184
|
+
input.value = value.toString();
|
185
|
+
// Select the added digits
|
186
|
+
const lengthToSelect = Math.max(1, input.value.length - originalValue.length);
|
187
|
+
input.setSelectionRange(0, lengthToSelect);
|
188
|
+
input.type = 'number';
|
189
|
+
}
|
190
|
+
else {
|
191
|
+
input.value = value.toString();
|
192
|
+
}
|
170
193
|
},
|
171
194
|
setIsAutomaticSize: (automatic) => {
|
172
195
|
input.disabled = automatic;
|
@@ -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;
|
@@ -15,7 +15,7 @@ export interface PenTypeRecord {
|
|
15
15
|
export default class PenToolWidget extends BaseToolWidget {
|
16
16
|
private tool;
|
17
17
|
private updateInputs;
|
18
|
-
protected penTypes: PenTypeRecord[];
|
18
|
+
protected penTypes: Readonly<PenTypeRecord>[];
|
19
19
|
protected shapelikeIDs: string[];
|
20
20
|
private static idCounter;
|
21
21
|
constructor(editor: Editor, tool: Pen, localization?: ToolbarLocalization);
|
@@ -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);
|
@@ -19,8 +20,12 @@ class PenToolWidget extends BaseToolWidget {
|
|
19
20
|
this.updateInputs = () => { };
|
20
21
|
// Pen types that correspond to
|
21
22
|
this.shapelikeIDs = ['pressure-sensitive-pen', 'freehand-pen'];
|
23
|
+
// Additional client-specified pens.
|
24
|
+
const additionalPens = editor.getCurrentSettings().pens?.additionalPenTypes ?? [];
|
25
|
+
const filterPens = editor.getCurrentSettings().pens?.filterPenTypes ?? (() => true);
|
22
26
|
// Default pen types
|
23
27
|
this.penTypes = [
|
28
|
+
// Non-shape pens
|
24
29
|
{
|
25
30
|
name: this.localizationTable.flatTipPen,
|
26
31
|
id: 'pressure-sensitive-pen',
|
@@ -31,6 +36,13 @@ class PenToolWidget extends BaseToolWidget {
|
|
31
36
|
id: 'freehand-pen',
|
32
37
|
factory: makeFreehandLineBuilder,
|
33
38
|
},
|
39
|
+
{
|
40
|
+
name: this.localizationTable.roundedTipPen2,
|
41
|
+
id: 'polyline-pen',
|
42
|
+
factory: makePolylineBuilder,
|
43
|
+
},
|
44
|
+
...(additionalPens.filter(pen => !pen.isShapeBuilder)),
|
45
|
+
// Shape pens
|
34
46
|
{
|
35
47
|
name: this.localizationTable.arrowPen,
|
36
48
|
id: 'arrow',
|
@@ -60,8 +72,9 @@ class PenToolWidget extends BaseToolWidget {
|
|
60
72
|
id: 'outlined-circle',
|
61
73
|
isShapeBuilder: true,
|
62
74
|
factory: makeOutlinedCircleBuilder,
|
63
|
-
}
|
64
|
-
|
75
|
+
},
|
76
|
+
...(additionalPens.filter(pen => pen.isShapeBuilder)),
|
77
|
+
].filter(filterPens);
|
65
78
|
this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
|
66
79
|
if (toolEvt.kind !== EditorEventType.ToolUpdated) {
|
67
80
|
throw new Error('Invalid event type!');
|
@@ -105,7 +118,7 @@ class PenToolWidget extends BaseToolWidget {
|
|
105
118
|
style.factory = record.factory;
|
106
119
|
}
|
107
120
|
const strokeFactory = record?.factory;
|
108
|
-
if (!strokeFactory || strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
|
121
|
+
if (!strokeFactory || strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder || strokeFactory === makePolylineBuilder) {
|
109
122
|
return this.editor.icons.makePenIcon(style);
|
110
123
|
}
|
111
124
|
else {
|
@@ -125,7 +138,7 @@ class PenToolWidget extends BaseToolWidget {
|
|
125
138
|
isShapeBuilder: penType.isShapeBuilder ?? false,
|
126
139
|
};
|
127
140
|
});
|
128
|
-
const penSelector = makeGridSelector(this.localizationTable.
|
141
|
+
const penSelector = makeGridSelector(this.localizationTable.selectPenType, this.getCurrentPenTypeIdx(), allChoices.filter(choice => !choice.isShapeBuilder));
|
129
142
|
const shapeSelector = makeGridSelector(this.localizationTable.selectShape, this.getCurrentPenTypeIdx(), allChoices.filter(choice => choice.isShapeBuilder));
|
130
143
|
const onSelectorUpdate = (newPenTypeIndex) => {
|
131
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
|
}
|
@@ -1,19 +1,28 @@
|
|
1
1
|
import { EditorEventType } from '../types.mjs';
|
2
2
|
import BaseTool from './BaseTool.mjs';
|
3
|
-
import { Vec2, LineSegment2, Color4, Rect2 } from '@js-draw/math';
|
3
|
+
import { Vec2, LineSegment2, Color4, Rect2, Path } from '@js-draw/math';
|
4
4
|
import Erase from '../commands/Erase.mjs';
|
5
5
|
import { PointerDevice } from '../Pointer.mjs';
|
6
6
|
import { decreaseSizeKeyboardShortcutId, increaseSizeKeyboardShortcutId } from './keybindings.mjs';
|
7
7
|
import { ReactiveValue } from '../util/ReactiveValue.mjs';
|
8
|
+
import EditorImage from '../image/EditorImage.mjs';
|
9
|
+
import uniteCommands from '../commands/uniteCommands.mjs';
|
10
|
+
import { pathToRenderable } from '../rendering/RenderablePathSpec.mjs';
|
11
|
+
export var EraserMode;
|
12
|
+
(function (EraserMode) {
|
13
|
+
EraserMode["PartialStroke"] = "partial-stroke";
|
14
|
+
EraserMode["FullStroke"] = "full-stroke";
|
15
|
+
})(EraserMode || (EraserMode = {}));
|
8
16
|
export default class Eraser extends BaseTool {
|
9
|
-
constructor(editor, description) {
|
17
|
+
constructor(editor, description, options) {
|
10
18
|
super(editor.notifier, description);
|
11
19
|
this.editor = editor;
|
12
20
|
this.lastPoint = null;
|
13
21
|
this.isFirstEraseEvt = true;
|
14
|
-
this.thickness = 10;
|
15
22
|
// Commands that each remove one element
|
16
|
-
this.
|
23
|
+
this.eraseCommands = [];
|
24
|
+
this.addCommands = [];
|
25
|
+
this.thickness = options?.thickness ?? 10;
|
17
26
|
this.thicknessValue = ReactiveValue.fromInitialValue(this.thickness);
|
18
27
|
this.thicknessValue.onUpdate(value => {
|
19
28
|
this.thickness = value;
|
@@ -22,6 +31,13 @@ export default class Eraser extends BaseTool {
|
|
22
31
|
tool: this,
|
23
32
|
});
|
24
33
|
});
|
34
|
+
this.modeValue = ReactiveValue.fromInitialValue(options?.mode ?? EraserMode.FullStroke);
|
35
|
+
this.modeValue.onUpdate(_value => {
|
36
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
37
|
+
kind: EditorEventType.ToolUpdated,
|
38
|
+
tool: this,
|
39
|
+
});
|
40
|
+
});
|
25
41
|
}
|
26
42
|
clearPreview() {
|
27
43
|
this.editor.clearWetInk();
|
@@ -34,18 +50,26 @@ export default class Eraser extends BaseTool {
|
|
34
50
|
const size = this.getSizeOnCanvas();
|
35
51
|
const renderer = this.editor.display.getWetInkRenderer();
|
36
52
|
const rect = this.getEraserRect(point);
|
53
|
+
const rect2 = this.getEraserRect(this.lastPoint ?? point);
|
37
54
|
const fill = {
|
38
|
-
fill: Color4.
|
55
|
+
fill: Color4.transparent,
|
56
|
+
stroke: { width: size / 10, color: Color4.gray },
|
39
57
|
};
|
40
|
-
renderer.
|
58
|
+
renderer.drawPath(pathToRenderable(Path.fromConvexHullOf([...rect.corners, ...rect2.corners]), fill));
|
41
59
|
}
|
60
|
+
/**
|
61
|
+
* @returns the eraser rectangle in canvas coordinates.
|
62
|
+
*
|
63
|
+
* For now, all erasers are rectangles or points.
|
64
|
+
*/
|
42
65
|
getEraserRect(centerPoint) {
|
43
66
|
const size = this.getSizeOnCanvas();
|
44
67
|
const halfSize = Vec2.of(size / 2, size / 2);
|
45
68
|
return Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
|
46
69
|
}
|
70
|
+
/** Erases in a line from the last point to the current. */
|
47
71
|
eraseTo(currentPoint) {
|
48
|
-
if (!this.isFirstEraseEvt && currentPoint.
|
72
|
+
if (!this.isFirstEraseEvt && currentPoint.distanceTo(this.lastPoint) === 0) {
|
49
73
|
return;
|
50
74
|
}
|
51
75
|
this.isFirstEraseEvt = false;
|
@@ -60,13 +84,55 @@ export default class Eraser extends BaseTool {
|
|
60
84
|
});
|
61
85
|
// Only erase components that could be selected (and thus interacted with)
|
62
86
|
// by the user.
|
63
|
-
const
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
87
|
+
const eraseableElems = intersectingElems.filter(elem => elem.isSelectable());
|
88
|
+
if (this.modeValue.get() === EraserMode.FullStroke) {
|
89
|
+
// Remove any intersecting elements.
|
90
|
+
this.toRemove.push(...eraseableElems);
|
91
|
+
// Create new Erase commands for the now-to-be-erased elements and apply them.
|
92
|
+
const newPartialCommands = eraseableElems.map(elem => new Erase([elem]));
|
93
|
+
newPartialCommands.forEach(cmd => cmd.apply(this.editor));
|
94
|
+
this.eraseCommands.push(...newPartialCommands);
|
95
|
+
}
|
96
|
+
else {
|
97
|
+
const toErase = [];
|
98
|
+
const toAdd = [];
|
99
|
+
for (const targetElem of eraseableElems) {
|
100
|
+
toErase.push(targetElem);
|
101
|
+
// Completely delete items that can't be divided.
|
102
|
+
if (!targetElem.withRegionErased) {
|
103
|
+
continue;
|
104
|
+
}
|
105
|
+
// Completely delete items that are completely or almost completely
|
106
|
+
// contained within the eraser.
|
107
|
+
const grownRect = eraserRect.grownBy(eraserRect.maxDimension / 3);
|
108
|
+
if (grownRect.containsRect(targetElem.getExactBBox())) {
|
109
|
+
continue;
|
110
|
+
}
|
111
|
+
// Join the current and previous rectangles so that points between events are also
|
112
|
+
// erased.
|
113
|
+
const erasePath = Path.fromConvexHullOf([
|
114
|
+
...eraserRect.corners, ...this.getEraserRect(this.lastPoint ?? currentPoint).corners
|
115
|
+
].map(p => this.editor.viewport.roundPoint(p)));
|
116
|
+
toAdd.push(...targetElem.withRegionErased(erasePath, this.editor.viewport));
|
117
|
+
}
|
118
|
+
const eraseCommand = new Erase(toErase);
|
119
|
+
const newAddCommands = toAdd.map(elem => EditorImage.addElement(elem));
|
120
|
+
eraseCommand.apply(this.editor);
|
121
|
+
newAddCommands.forEach(command => command.apply(this.editor));
|
122
|
+
const finalToErase = [];
|
123
|
+
for (const item of toErase) {
|
124
|
+
if (this.toAdd.includes(item)) {
|
125
|
+
this.toAdd = this.toAdd.filter(i => i !== item);
|
126
|
+
}
|
127
|
+
else {
|
128
|
+
finalToErase.push(item);
|
129
|
+
}
|
130
|
+
}
|
131
|
+
this.toRemove.push(...finalToErase);
|
132
|
+
this.toAdd.push(...toAdd);
|
133
|
+
this.eraseCommands.push(new Erase(finalToErase));
|
134
|
+
this.addCommands.push(...newAddCommands);
|
135
|
+
}
|
70
136
|
this.drawPreviewAt(currentPoint);
|
71
137
|
this.lastPoint = currentPoint;
|
72
138
|
}
|
@@ -74,6 +140,7 @@ export default class Eraser extends BaseTool {
|
|
74
140
|
if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
|
75
141
|
this.lastPoint = event.current.canvasPos;
|
76
142
|
this.toRemove = [];
|
143
|
+
this.toAdd = [];
|
77
144
|
this.isFirstEraseEvt = true;
|
78
145
|
this.drawPreviewAt(event.current.canvasPos);
|
79
146
|
return true;
|
@@ -86,18 +153,32 @@ export default class Eraser extends BaseTool {
|
|
86
153
|
}
|
87
154
|
onPointerUp(event) {
|
88
155
|
this.eraseTo(event.current.canvasPos);
|
89
|
-
|
156
|
+
const commands = [];
|
157
|
+
if (this.addCommands.length > 0) {
|
158
|
+
this.addCommands.forEach(cmd => cmd.unapply(this.editor));
|
159
|
+
commands.push(...this.toAdd.map(a => EditorImage.addElement(a)));
|
160
|
+
this.addCommands = [];
|
161
|
+
}
|
162
|
+
if (this.eraseCommands.length > 0) {
|
90
163
|
// Undo commands for each individual component and unite into a single command.
|
91
|
-
this.
|
92
|
-
this.
|
164
|
+
this.eraseCommands.forEach(cmd => cmd.unapply(this.editor));
|
165
|
+
this.eraseCommands = [];
|
93
166
|
const command = new Erase(this.toRemove);
|
94
|
-
|
167
|
+
commands.push(command);
|
168
|
+
}
|
169
|
+
if (commands.length === 1) {
|
170
|
+
this.editor.dispatch(commands[0]); // dispatch: Makes undo-able.
|
171
|
+
}
|
172
|
+
else {
|
173
|
+
this.editor.dispatch(uniteCommands(commands));
|
95
174
|
}
|
96
175
|
this.clearPreview();
|
97
176
|
}
|
98
177
|
onGestureCancel() {
|
99
|
-
this.
|
100
|
-
this.
|
178
|
+
this.addCommands.forEach(cmd => cmd.unapply(this.editor));
|
179
|
+
this.eraseCommands.forEach(cmd => cmd.unapply(this.editor));
|
180
|
+
this.eraseCommands = [];
|
181
|
+
this.addCommands = [];
|
101
182
|
this.clearPreview();
|
102
183
|
}
|
103
184
|
onKeyPress(event) {
|
@@ -116,9 +197,14 @@ export default class Eraser extends BaseTool {
|
|
116
197
|
}
|
117
198
|
return false;
|
118
199
|
}
|
200
|
+
/** Returns the side-length of the tip of this eraser. */
|
119
201
|
getThickness() {
|
120
202
|
return this.thickness;
|
121
203
|
}
|
204
|
+
/** Sets the side-length of this' tip. */
|
205
|
+
setThickness(thickness) {
|
206
|
+
this.thicknessValue.set(thickness);
|
207
|
+
}
|
122
208
|
/**
|
123
209
|
* Returns a {@link MutableReactiveValue} that can be used to watch
|
124
210
|
* this tool's thickness.
|
@@ -126,7 +212,7 @@ export default class Eraser extends BaseTool {
|
|
126
212
|
getThicknessValue() {
|
127
213
|
return this.thicknessValue;
|
128
214
|
}
|
129
|
-
|
130
|
-
this.
|
215
|
+
getModeValue() {
|
216
|
+
return this.modeValue;
|
131
217
|
}
|
132
218
|
}
|
@@ -8,10 +8,10 @@ var StabilizerType;
|
|
8
8
|
})(StabilizerType || (StabilizerType = {}));
|
9
9
|
const defaultOptions = {
|
10
10
|
kind: StabilizerType.IntertialStabilizer,
|
11
|
-
mass: 0.4,
|
12
|
-
springConstant: 100.0,
|
11
|
+
mass: 0.4, // kg
|
12
|
+
springConstant: 100.0, // N/m
|
13
13
|
frictionCoefficient: 0.28,
|
14
|
-
maxPointDist: 10,
|
14
|
+
maxPointDist: 10, // screen units
|
15
15
|
inertiaFraction: 0.75,
|
16
16
|
minSimilarityToFinalize: 0.0,
|
17
17
|
velocityDecayFactor: 0.1,
|
@@ -23,8 +23,28 @@ export default class PasteHandler extends BaseTool {
|
|
23
23
|
// @internal
|
24
24
|
onPaste(event) {
|
25
25
|
const mime = event.mime.toLowerCase();
|
26
|
-
|
27
|
-
|
26
|
+
const svgData = (() => {
|
27
|
+
if (mime === 'image/svg+xml') {
|
28
|
+
return event.data;
|
29
|
+
}
|
30
|
+
if (mime !== 'text/html') {
|
31
|
+
return false;
|
32
|
+
}
|
33
|
+
// text/html is sometimes handlable SVG data. Use a hueristic
|
34
|
+
// to determine if this is the case:
|
35
|
+
// We use [^] and not . so that newlines are included.
|
36
|
+
const match = event.data.match(/^[^]{0,200}<svg.*/i); // [^]{0,200} <- Allow for metadata near start
|
37
|
+
if (!match) {
|
38
|
+
return false;
|
39
|
+
}
|
40
|
+
// Extract the SVG element from the pasted data
|
41
|
+
let svgEnd = event.data.toLowerCase().lastIndexOf('</svg>');
|
42
|
+
if (svgEnd === -1)
|
43
|
+
svgEnd = event.data.length;
|
44
|
+
return event.data.substring(event.data.search(/<svg/i), svgEnd);
|
45
|
+
})();
|
46
|
+
if (svgData) {
|
47
|
+
void this.doSVGPaste(svgData);
|
28
48
|
return true;
|
29
49
|
}
|
30
50
|
else if (mime === 'text/plain') {
|
@@ -38,16 +58,21 @@ export default class PasteHandler extends BaseTool {
|
|
38
58
|
return false;
|
39
59
|
}
|
40
60
|
async addComponentsFromPaste(components) {
|
41
|
-
await this.editor.addAndCenterComponents(components);
|
61
|
+
await this.editor.addAndCenterComponents(components, true, this.editor.localization.pasted(components.length));
|
42
62
|
}
|
43
63
|
async doSVGPaste(data) {
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
64
|
+
this.editor.showLoadingWarning(0);
|
65
|
+
try {
|
66
|
+
const loader = SVGLoader.fromString(data, true);
|
67
|
+
const components = [];
|
68
|
+
await loader.start((component) => {
|
69
|
+
components.push(component);
|
70
|
+
}, (_countProcessed, _totalToProcess) => null);
|
71
|
+
await this.addComponentsFromPaste(components);
|
72
|
+
}
|
73
|
+
finally {
|
74
|
+
this.editor.hideLoadingWarning();
|
75
|
+
}
|
51
76
|
}
|
52
77
|
async doTextPaste(text) {
|
53
78
|
const textTools = this.editor.toolController.getMatchingTools(TextTool);
|
package/dist/mjs/tools/Pen.mjs
CHANGED
@@ -97,8 +97,8 @@ export default class Pen extends BaseTool {
|
|
97
97
|
this.currentDeviceType = current.device;
|
98
98
|
if (this.shapeAutocompletionEnabled) {
|
99
99
|
const stationaryDetectionConfig = {
|
100
|
-
maxSpeed: 8.5,
|
101
|
-
maxRadius: 11,
|
100
|
+
maxSpeed: 8.5, // screenPx/s
|
101
|
+
maxRadius: 11, // screenPx
|
102
102
|
minTimeSeconds: 0.5, // s
|
103
103
|
};
|
104
104
|
this.stationaryDetector = new StationaryPenDetector(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
|