js-draw 0.7.2 → 0.9.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/.github/ISSUE_TEMPLATE/translation.md +1 -0
- package/CHANGELOG.md +12 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.js +3 -0
- package/dist/src/SVGLoader.js +5 -7
- package/dist/src/components/Stroke.js +10 -3
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +10 -23
- package/dist/src/components/builders/FreehandLineBuilder.js +70 -396
- package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +36 -0
- package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +339 -0
- package/dist/src/components/lib.d.ts +2 -0
- package/dist/src/components/lib.js +2 -0
- package/dist/src/components/util/StrokeSmoother.d.ts +35 -0
- package/dist/src/components/util/StrokeSmoother.js +206 -0
- package/dist/src/math/Mat33.d.ts +2 -0
- package/dist/src/math/Mat33.js +4 -0
- package/dist/src/math/Path.d.ts +2 -0
- package/dist/src/math/Path.js +44 -0
- package/dist/src/rendering/renderers/CanvasRenderer.js +2 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
- package/dist/src/rendering/renderers/SVGRenderer.js +20 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +3 -0
- package/dist/src/toolbar/HTMLToolbar.js +24 -0
- package/dist/src/toolbar/IconProvider.d.ts +1 -0
- package/dist/src/toolbar/IconProvider.js +43 -1
- package/dist/src/toolbar/localization.d.ts +2 -0
- package/dist/src/toolbar/localization.js +2 -0
- package/dist/src/toolbar/makeColorInput.d.ts +2 -1
- package/dist/src/toolbar/makeColorInput.js +13 -2
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +1 -1
- package/dist/src/toolbar/widgets/ActionButtonWidget.js +2 -2
- package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -2
- package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -3
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -2
- package/dist/src/toolbar/widgets/BaseWidget.js +67 -6
- package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +4 -0
- package/dist/src/toolbar/widgets/EraserToolWidget.js +3 -0
- package/dist/src/toolbar/widgets/HandToolWidget.d.ts +3 -1
- package/dist/src/toolbar/widgets/HandToolWidget.js +22 -13
- package/dist/src/toolbar/widgets/PenToolWidget.d.ts +7 -1
- package/dist/src/toolbar/widgets/PenToolWidget.js +83 -12
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +7 -7
- package/dist/src/toolbar/widgets/TextToolWidget.d.ts +4 -1
- package/dist/src/toolbar/widgets/TextToolWidget.js +17 -3
- package/dist/src/tools/PanZoom.d.ts +4 -1
- package/dist/src/tools/PanZoom.js +24 -1
- package/dist/src/tools/Pen.d.ts +2 -2
- package/dist/src/tools/Pen.js +2 -2
- package/dist/src/tools/SelectionTool/Selection.d.ts +1 -0
- package/dist/src/tools/SelectionTool/Selection.js +8 -1
- package/dist/src/tools/ToolController.js +2 -1
- package/package.json +1 -1
- package/src/Color4.ts +2 -0
- package/src/SVGLoader.ts +8 -8
- package/src/components/Stroke.ts +16 -3
- package/src/components/builders/FreehandLineBuilder.ts +54 -495
- package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +454 -0
- package/src/components/lib.ts +3 -1
- package/src/components/util/StrokeSmoother.ts +290 -0
- package/src/math/Mat33.ts +5 -0
- package/src/math/Path.test.ts +49 -0
- package/src/math/Path.ts +51 -0
- package/src/rendering/renderers/CanvasRenderer.ts +2 -0
- package/src/rendering/renderers/SVGRenderer.ts +24 -0
- package/src/toolbar/HTMLToolbar.ts +33 -0
- package/src/toolbar/IconProvider.ts +49 -1
- package/src/toolbar/localization.ts +4 -0
- package/src/toolbar/makeColorInput.ts +21 -3
- package/src/toolbar/widgets/ActionButtonWidget.ts +6 -3
- package/src/toolbar/widgets/BaseToolWidget.ts +4 -3
- package/src/toolbar/widgets/BaseWidget.ts +83 -5
- package/src/toolbar/widgets/EraserToolWidget.ts +11 -0
- package/src/toolbar/widgets/HandToolWidget.ts +48 -17
- package/src/toolbar/widgets/PenToolWidget.ts +110 -13
- package/src/toolbar/widgets/SelectionToolWidget.ts +8 -5
- package/src/toolbar/widgets/TextToolWidget.ts +29 -4
- package/src/tools/PanZoom.ts +28 -1
- package/src/tools/Pen.test.ts +2 -2
- package/src/tools/Pen.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +10 -1
- package/src/tools/ToolController.ts +2 -1
@@ -8,9 +8,10 @@ export default abstract class BaseToolWidget extends BaseWidget {
|
|
8
8
|
public constructor(
|
9
9
|
protected editor: Editor,
|
10
10
|
protected targetTool: BaseTool,
|
11
|
-
|
11
|
+
id: string,
|
12
|
+
localizationTable?: ToolbarLocalization,
|
12
13
|
) {
|
13
|
-
super(editor, localizationTable);
|
14
|
+
super(editor, id, localizationTable);
|
14
15
|
|
15
16
|
editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
|
16
17
|
if (toolEvt.kind !== EditorEventType.ToolEnabled) {
|
@@ -50,4 +51,4 @@ export default abstract class BaseToolWidget extends BaseWidget {
|
|
50
51
|
super.addTo(parent);
|
51
52
|
this.setSelected(this.targetTool.isEnabled());
|
52
53
|
}
|
53
|
-
}
|
54
|
+
}
|
@@ -4,6 +4,8 @@ import { EditorEventType, InputEvtType, KeyPressEvent } from '../../types';
|
|
4
4
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
5
5
|
import { ToolbarLocalization } from '../localization';
|
6
6
|
|
7
|
+
export type SavedToolbuttonState = Record<string, any>;
|
8
|
+
|
7
9
|
export default abstract class BaseWidget {
|
8
10
|
protected readonly container: HTMLElement;
|
9
11
|
private button: HTMLElement;
|
@@ -13,13 +15,20 @@ export default abstract class BaseWidget {
|
|
13
15
|
private label: HTMLLabelElement;
|
14
16
|
#hasDropdown: boolean;
|
15
17
|
private disabled: boolean = false;
|
16
|
-
|
18
|
+
|
19
|
+
// Maps subWidget IDs to subWidgets.
|
20
|
+
private subWidgets: Record<string, BaseWidget> = {};
|
21
|
+
|
17
22
|
private toplevel: boolean = true;
|
23
|
+
protected readonly localizationTable: ToolbarLocalization;
|
18
24
|
|
19
25
|
public constructor(
|
20
26
|
protected editor: Editor,
|
21
|
-
protected
|
27
|
+
protected id: string,
|
28
|
+
localizationTable?: ToolbarLocalization,
|
22
29
|
) {
|
30
|
+
this.localizationTable = localizationTable ?? editor.localization;
|
31
|
+
|
23
32
|
this.icon = null;
|
24
33
|
this.container = document.createElement('div');
|
25
34
|
this.container.classList.add(`${toolbarCSSPrefix}toolContainer`);
|
@@ -43,17 +52,44 @@ export default abstract class BaseWidget {
|
|
43
52
|
}
|
44
53
|
}
|
45
54
|
|
55
|
+
public getId(): string {
|
56
|
+
return this.id;
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Returns the ID of this widget in `container`. Adds a suffix to this' ID
|
61
|
+
* if an item in `container` already has this' ID.
|
62
|
+
*
|
63
|
+
* For example, if `this` has ID `foo` and if
|
64
|
+
* `container = { 'foo': somethingNotThis, 'foo-1': somethingElseNotThis }`, this method
|
65
|
+
* returns `foo-2` because elements with IDs `foo` and `foo-1` are already present in
|
66
|
+
* `container`.
|
67
|
+
*/
|
68
|
+
public getUniqueIdIn(container: Record<string, BaseWidget>): string {
|
69
|
+
let id = this.getId();
|
70
|
+
let idCounter = 0;
|
71
|
+
|
72
|
+
while (id in container && container[id] !== this) {
|
73
|
+
id = this.getId() + '-' + idCounter.toString();
|
74
|
+
idCounter ++;
|
75
|
+
}
|
76
|
+
|
77
|
+
return id;
|
78
|
+
}
|
79
|
+
|
46
80
|
protected abstract getTitle(): string;
|
47
81
|
protected abstract createIcon(): Element;
|
48
82
|
|
49
83
|
// Add content to the widget's associated dropdown menu.
|
50
84
|
// Returns true if such a menu should be created, false otherwise.
|
51
85
|
protected fillDropdown(dropdown: HTMLElement): boolean {
|
52
|
-
if (this.subWidgets.length === 0) {
|
86
|
+
if (Object.keys(this.subWidgets).length === 0) {
|
53
87
|
return false;
|
54
88
|
}
|
55
89
|
|
56
|
-
for (const
|
90
|
+
for (const widgetId in this.subWidgets) {
|
91
|
+
const widget = this.subWidgets[widgetId];
|
92
|
+
|
57
93
|
widget.addTo(dropdown);
|
58
94
|
widget.setIsToplevel(false);
|
59
95
|
}
|
@@ -118,7 +154,10 @@ export default abstract class BaseWidget {
|
|
118
154
|
|
119
155
|
// Add a widget to this' dropdown. Must be called before this.addTo.
|
120
156
|
protected addSubWidget(widget: BaseWidget) {
|
121
|
-
|
157
|
+
// Generate a unique ID for the widget.
|
158
|
+
const id = widget.getUniqueIdIn(this.subWidgets);
|
159
|
+
|
160
|
+
this.subWidgets[id] = widget;
|
122
161
|
}
|
123
162
|
|
124
163
|
// Adds this to [parent]. This can only be called once for each ToolbarWidget.
|
@@ -251,4 +290,43 @@ export default abstract class BaseWidget {
|
|
251
290
|
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
|
252
291
|
return icon;
|
253
292
|
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Serialize state associated with this widget.
|
296
|
+
* Override this method to allow saving/restoring from state on application load.
|
297
|
+
*
|
298
|
+
* Overriders should call `super` and include the output of `super.serializeState` in
|
299
|
+
* the output dictionary.
|
300
|
+
*
|
301
|
+
* Clients should not rely on the output from `saveState` being in any particular
|
302
|
+
* format.
|
303
|
+
*/
|
304
|
+
public serializeState(): SavedToolbuttonState {
|
305
|
+
const subwidgetState: Record<string, any> = {};
|
306
|
+
|
307
|
+
// Save all subwidget state.
|
308
|
+
for (const subwidgetId in this.subWidgets) {
|
309
|
+
subwidgetState[subwidgetId] = this.subWidgets[subwidgetId].serializeState();
|
310
|
+
}
|
311
|
+
|
312
|
+
return {
|
313
|
+
subwidgetState,
|
314
|
+
};
|
315
|
+
}
|
316
|
+
|
317
|
+
/**
|
318
|
+
* Restore widget state from serialized data. See also `saveState`.
|
319
|
+
*
|
320
|
+
* Overriders must call `super`.
|
321
|
+
*/
|
322
|
+
public deserializeFrom(state: SavedToolbuttonState): void {
|
323
|
+
if (state.subwidgetState) {
|
324
|
+
// Deserialize all subwidgets.
|
325
|
+
for (const subwidgetId in state.subwidgetState) {
|
326
|
+
if (subwidgetId in this.subWidgets) {
|
327
|
+
this.subWidgets[subwidgetId].deserializeFrom(state.subwidgetState[subwidgetId]);
|
328
|
+
}
|
329
|
+
}
|
330
|
+
}
|
331
|
+
}
|
254
332
|
}
|
@@ -1,6 +1,17 @@
|
|
1
|
+
import Editor from '../../Editor';
|
2
|
+
import Eraser from '../../tools/Eraser';
|
3
|
+
import { ToolbarLocalization } from '../localization';
|
1
4
|
import BaseToolWidget from './BaseToolWidget';
|
2
5
|
|
3
6
|
export default class EraserToolWidget extends BaseToolWidget {
|
7
|
+
public constructor(
|
8
|
+
editor: Editor,
|
9
|
+
tool: Eraser,
|
10
|
+
localizationTable?: ToolbarLocalization
|
11
|
+
) {
|
12
|
+
super(editor, tool, 'eraser-tool-widget', localizationTable);
|
13
|
+
}
|
14
|
+
|
4
15
|
protected getTitle(): string {
|
5
16
|
return this.localizationTable.eraser;
|
6
17
|
}
|
@@ -7,7 +7,7 @@ import Viewport from '../../Viewport';
|
|
7
7
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
8
8
|
import { ToolbarLocalization } from '../localization';
|
9
9
|
import BaseToolWidget from './BaseToolWidget';
|
10
|
-
import BaseWidget from './BaseWidget';
|
10
|
+
import BaseWidget, { SavedToolbuttonState } from './BaseWidget';
|
11
11
|
|
12
12
|
const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor) => {
|
13
13
|
const zoomLevelRow = document.createElement('div');
|
@@ -74,8 +74,8 @@ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor)
|
|
74
74
|
};
|
75
75
|
|
76
76
|
class ZoomWidget extends BaseWidget {
|
77
|
-
public constructor(editor: Editor, localizationTable
|
78
|
-
super(editor, localizationTable);
|
77
|
+
public constructor(editor: Editor, localizationTable?: ToolbarLocalization) {
|
78
|
+
super(editor, 'zoom-widget', localizationTable);
|
79
79
|
|
80
80
|
// Make it possible to open the dropdown, even if this widget isn't selected.
|
81
81
|
this.container.classList.add('dropdownShowable');
|
@@ -101,12 +101,14 @@ class ZoomWidget extends BaseWidget {
|
|
101
101
|
|
102
102
|
class HandModeWidget extends BaseWidget {
|
103
103
|
public constructor(
|
104
|
-
editor: Editor,
|
104
|
+
editor: Editor,
|
105
105
|
|
106
106
|
protected tool: PanZoom, protected flag: PanZoomMode, protected makeIcon: ()=> Element,
|
107
107
|
private title: string,
|
108
|
+
|
109
|
+
localizationTable?: ToolbarLocalization,
|
108
110
|
) {
|
109
|
-
super(editor, localizationTable);
|
111
|
+
super(editor, `pan-mode-${flag}`, localizationTable);
|
110
112
|
|
111
113
|
editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
|
112
114
|
if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === tool) {
|
@@ -122,12 +124,7 @@ class HandModeWidget extends BaseWidget {
|
|
122
124
|
}
|
123
125
|
|
124
126
|
private setModeFlag(enabled: boolean) {
|
125
|
-
|
126
|
-
if (enabled) {
|
127
|
-
this.tool.setMode(mode | this.flag);
|
128
|
-
} else {
|
129
|
-
this.tool.setMode(mode & ~this.flag);
|
130
|
-
}
|
127
|
+
this.tool.setModeEnabled(this.flag, enabled);
|
131
128
|
}
|
132
129
|
|
133
130
|
protected handleClick() {
|
@@ -148,7 +145,6 @@ class HandModeWidget extends BaseWidget {
|
|
148
145
|
}
|
149
146
|
|
150
147
|
export default class HandToolWidget extends BaseToolWidget {
|
151
|
-
private touchPanningWidget: HandModeWidget;
|
152
148
|
private allowTogglingBaseTool: boolean;
|
153
149
|
|
154
150
|
public constructor(
|
@@ -162,7 +158,7 @@ export default class HandToolWidget extends BaseToolWidget {
|
|
162
158
|
) {
|
163
159
|
const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
|
164
160
|
const tool = primaryHandTool ?? overridePanZoomTool;
|
165
|
-
super(editor, tool, localizationTable);
|
161
|
+
super(editor, tool, 'hand-tool-widget', localizationTable);
|
166
162
|
|
167
163
|
// Only allow toggling a hand tool if we're using the primary hand tool and not the override
|
168
164
|
// hand tool for this button.
|
@@ -174,16 +170,29 @@ export default class HandToolWidget extends BaseToolWidget {
|
|
174
170
|
}
|
175
171
|
|
176
172
|
// Controls for the overriding hand tool.
|
177
|
-
|
178
|
-
editor,
|
173
|
+
const touchPanningWidget = new HandModeWidget(
|
174
|
+
editor,
|
179
175
|
|
180
176
|
overridePanZoomTool, PanZoomMode.OneFingerTouchGestures,
|
181
177
|
() => this.editor.icons.makeTouchPanningIcon(),
|
182
178
|
|
183
|
-
localizationTable.touchPanning
|
179
|
+
localizationTable.touchPanning,
|
180
|
+
|
181
|
+
localizationTable,
|
184
182
|
);
|
183
|
+
|
184
|
+
const rotationLockWidget = new HandModeWidget(
|
185
|
+
editor,
|
185
186
|
|
186
|
-
|
187
|
+
overridePanZoomTool, PanZoomMode.RotationLocked,
|
188
|
+
() => this.editor.icons.makeRotationLockIcon(),
|
189
|
+
|
190
|
+
localizationTable.lockRotation,
|
191
|
+
localizationTable,
|
192
|
+
);
|
193
|
+
|
194
|
+
this.addSubWidget(touchPanningWidget);
|
195
|
+
this.addSubWidget(rotationLockWidget);
|
187
196
|
this.addSubWidget(
|
188
197
|
new ZoomWidget(editor, localizationTable)
|
189
198
|
);
|
@@ -216,4 +225,26 @@ export default class HandToolWidget extends BaseToolWidget {
|
|
216
225
|
super.setSelected(selected);
|
217
226
|
}
|
218
227
|
}
|
228
|
+
|
229
|
+
public serializeState(): SavedToolbuttonState {
|
230
|
+
const toolMode = this.overridePanZoomTool.getMode();
|
231
|
+
|
232
|
+
return {
|
233
|
+
...super.serializeState(),
|
234
|
+
touchPanning: toolMode & PanZoomMode.OneFingerTouchGestures,
|
235
|
+
rotationLocked: toolMode & PanZoomMode.RotationLocked,
|
236
|
+
};
|
237
|
+
}
|
238
|
+
|
239
|
+
public deserializeFrom(state: SavedToolbuttonState): void {
|
240
|
+
if (state.touchPanning !== undefined) {
|
241
|
+
this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, state.touchPanning);
|
242
|
+
}
|
243
|
+
|
244
|
+
if (state.rotationLocked !== undefined) {
|
245
|
+
this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, state.rotationLocked);
|
246
|
+
}
|
247
|
+
|
248
|
+
super.deserializeFrom(state);
|
249
|
+
}
|
219
250
|
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';
|
2
2
|
import { makeFreehandLineBuilder } from '../../components/builders/FreehandLineBuilder';
|
3
|
+
import { makePressureSensitiveFreehandLineBuilder } from '../../components/builders/PressureSensitiveFreehandLineBuilder';
|
3
4
|
import { makeLineBuilder } from '../../components/builders/LineBuilder';
|
4
5
|
import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../components/builders/RectangleBuilder';
|
5
6
|
import { ComponentBuilderFactory } from '../../components/builders/types';
|
@@ -10,12 +11,17 @@ import { toolbarCSSPrefix } from '../HTMLToolbar';
|
|
10
11
|
import { ToolbarLocalization } from '../localization';
|
11
12
|
import makeColorInput from '../makeColorInput';
|
12
13
|
import BaseToolWidget from './BaseToolWidget';
|
14
|
+
import Color4 from '../../Color4';
|
15
|
+
import { SavedToolbuttonState } from './BaseWidget';
|
13
16
|
|
14
17
|
|
15
18
|
export interface PenTypeRecord {
|
16
19
|
// Description of the factory (e.g. 'Freehand line')
|
17
20
|
name: string;
|
18
21
|
|
22
|
+
// A unique ID for the facotory (e.g. 'chisel-tip-pen')
|
23
|
+
id: string;
|
24
|
+
|
19
25
|
// Creates an `AbstractComponent` from pen input.
|
20
26
|
factory: ComponentBuilderFactory;
|
21
27
|
}
|
@@ -25,30 +31,46 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
25
31
|
protected penTypes: PenTypeRecord[];
|
26
32
|
|
27
33
|
public constructor(
|
28
|
-
editor: Editor, private tool: Pen, localization
|
34
|
+
editor: Editor, private tool: Pen, localization?: ToolbarLocalization
|
29
35
|
) {
|
30
|
-
super(editor, tool, localization);
|
36
|
+
super(editor, tool, 'pen', localization);
|
31
37
|
|
32
38
|
// Default pen types
|
33
39
|
this.penTypes = [
|
34
40
|
{
|
35
|
-
name:
|
41
|
+
name: this.localizationTable.pressureSensitiveFreehandPen,
|
42
|
+
id: 'pressure-sensitive-pen',
|
43
|
+
|
44
|
+
factory: makePressureSensitiveFreehandLineBuilder,
|
45
|
+
},
|
46
|
+
{
|
47
|
+
name: this.localizationTable.freehandPen,
|
48
|
+
id: 'freehand-pen',
|
49
|
+
|
36
50
|
factory: makeFreehandLineBuilder,
|
37
51
|
},
|
38
52
|
{
|
39
|
-
name:
|
53
|
+
name: this.localizationTable.arrowPen,
|
54
|
+
id: 'arrow',
|
55
|
+
|
40
56
|
factory: makeArrowBuilder,
|
41
57
|
},
|
42
58
|
{
|
43
|
-
name:
|
59
|
+
name: this.localizationTable.linePen,
|
60
|
+
id: 'line',
|
61
|
+
|
44
62
|
factory: makeLineBuilder,
|
45
63
|
},
|
46
64
|
{
|
47
|
-
name:
|
65
|
+
name: this.localizationTable.filledRectanglePen,
|
66
|
+
id: 'filled-rectangle',
|
67
|
+
|
48
68
|
factory: makeFilledRectangleBuilder,
|
49
69
|
},
|
50
70
|
{
|
51
|
-
name:
|
71
|
+
name: this.localizationTable.outlinedRectanglePen,
|
72
|
+
id: 'outlined-rectangle',
|
73
|
+
|
52
74
|
factory: makeOutlinedRectangleBuilder,
|
53
75
|
},
|
54
76
|
];
|
@@ -70,9 +92,33 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
70
92
|
return this.targetTool.description;
|
71
93
|
}
|
72
94
|
|
95
|
+
// Return the index of this tool's stroke factory in the list of
|
96
|
+
// all stroke factories.
|
97
|
+
//
|
98
|
+
// Returns -1 if the stroke factory is not in the list of all stroke factories.
|
99
|
+
private getCurrentPenTypeIdx(): number {
|
100
|
+
const currentFactory = this.tool.getStrokeFactory();
|
101
|
+
|
102
|
+
for (let i = 0; i < this.penTypes.length; i ++) {
|
103
|
+
if (this.penTypes[i].factory === currentFactory) {
|
104
|
+
return i;
|
105
|
+
}
|
106
|
+
}
|
107
|
+
return -1;
|
108
|
+
}
|
109
|
+
|
110
|
+
private getCurrentPenType(): PenTypeRecord|null {
|
111
|
+
for (const penType of this.penTypes) {
|
112
|
+
if (penType.factory === this.tool.getStrokeFactory()) {
|
113
|
+
return penType;
|
114
|
+
}
|
115
|
+
}
|
116
|
+
return null;
|
117
|
+
}
|
118
|
+
|
73
119
|
protected createIcon(): Element {
|
74
120
|
const strokeFactory = this.tool.getStrokeFactory();
|
75
|
-
if (strokeFactory === makeFreehandLineBuilder) {
|
121
|
+
if (strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
|
76
122
|
// Use a square-root scale to prevent the pen's tip from overflowing.
|
77
123
|
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
78
124
|
const color = this.tool.getColor();
|
@@ -133,7 +179,7 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
133
179
|
|
134
180
|
const colorRow = document.createElement('div');
|
135
181
|
const colorLabel = document.createElement('label');
|
136
|
-
const [ colorInput, colorInputContainer ] = makeColorInput(this.editor, color => {
|
182
|
+
const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
|
137
183
|
this.tool.setColor(color);
|
138
184
|
});
|
139
185
|
|
@@ -145,9 +191,10 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
145
191
|
colorRow.appendChild(colorInputContainer);
|
146
192
|
|
147
193
|
this.updateInputs = () => {
|
148
|
-
|
194
|
+
setColorInputValue(this.tool.getColor());
|
149
195
|
thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
|
150
196
|
|
197
|
+
// Update the list of stroke factories
|
151
198
|
objectTypeSelect.replaceChildren();
|
152
199
|
for (let i = 0; i < this.penTypes.length; i ++) {
|
153
200
|
const penType = this.penTypes[i];
|
@@ -156,10 +203,14 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
156
203
|
option.innerText = penType.name;
|
157
204
|
|
158
205
|
objectTypeSelect.appendChild(option);
|
206
|
+
}
|
159
207
|
|
160
|
-
|
161
|
-
|
162
|
-
|
208
|
+
// Update the selected stroke factory.
|
209
|
+
const strokeFactoryIdx = this.getCurrentPenTypeIdx();
|
210
|
+
if (strokeFactoryIdx === -1) {
|
211
|
+
objectTypeSelect.value = '';
|
212
|
+
} else {
|
213
|
+
objectTypeSelect.value = strokeFactoryIdx.toString();
|
163
214
|
}
|
164
215
|
};
|
165
216
|
this.updateInputs();
|
@@ -185,4 +236,50 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
185
236
|
|
186
237
|
return false;
|
187
238
|
}
|
239
|
+
|
240
|
+
public serializeState(): SavedToolbuttonState {
|
241
|
+
return {
|
242
|
+
...super.serializeState(),
|
243
|
+
|
244
|
+
color: this.tool.getColor().toHexString(),
|
245
|
+
thickness: this.tool.getThickness(),
|
246
|
+
strokeFactoryId: this.getCurrentPenType()?.id,
|
247
|
+
};
|
248
|
+
}
|
249
|
+
|
250
|
+
public deserializeFrom(state: SavedToolbuttonState) {
|
251
|
+
super.deserializeFrom(state);
|
252
|
+
|
253
|
+
const verifyPropertyType = (propertyName: string, expectedType: 'string'|'number'|'object') => {
|
254
|
+
const actualType = typeof(state[propertyName]);
|
255
|
+
if (actualType !== expectedType) {
|
256
|
+
throw new Error(
|
257
|
+
`Deserializing property ${propertyName}: Invalid type. Expected ${expectedType},` +
|
258
|
+
` was ${actualType}.`
|
259
|
+
);
|
260
|
+
}
|
261
|
+
};
|
262
|
+
|
263
|
+
if (state.color) {
|
264
|
+
verifyPropertyType('color', 'string');
|
265
|
+
this.tool.setColor(Color4.fromHex(state.color));
|
266
|
+
}
|
267
|
+
|
268
|
+
if (state.thickness) {
|
269
|
+
verifyPropertyType('thickness', 'number');
|
270
|
+
this.tool.setThickness(state.thickness);
|
271
|
+
}
|
272
|
+
|
273
|
+
if (state.strokeFactoryId) {
|
274
|
+
verifyPropertyType('strokeFactoryId', 'string');
|
275
|
+
|
276
|
+
const factoryId: string = state.strokeFactoryId;
|
277
|
+
for (const penType of this.penTypes) {
|
278
|
+
if (factoryId === penType.id) {
|
279
|
+
this.tool.setStrokeFactory(penType.factory);
|
280
|
+
break;
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}
|
284
|
+
}
|
188
285
|
}
|
@@ -7,20 +7,21 @@ import BaseToolWidget from './BaseToolWidget';
|
|
7
7
|
|
8
8
|
export default class SelectionToolWidget extends BaseToolWidget {
|
9
9
|
public constructor(
|
10
|
-
editor: Editor, private tool: SelectionTool, localization
|
10
|
+
editor: Editor, private tool: SelectionTool, localization?: ToolbarLocalization
|
11
11
|
) {
|
12
|
-
super(editor, tool, localization);
|
12
|
+
super(editor, tool, 'selection-tool-widget', localization);
|
13
13
|
|
14
14
|
const resizeButton = new ActionButtonWidget(
|
15
|
-
editor,
|
15
|
+
editor, 'resize-btn',
|
16
16
|
() => editor.icons.makeResizeViewportIcon(),
|
17
17
|
this.localizationTable.resizeImageToSelection,
|
18
18
|
() => {
|
19
19
|
this.resizeImageToSelection();
|
20
20
|
},
|
21
|
+
localization,
|
21
22
|
);
|
22
23
|
const deleteButton = new ActionButtonWidget(
|
23
|
-
editor,
|
24
|
+
editor, 'delete-btn',
|
24
25
|
() => editor.icons.makeDeleteSelectionIcon(),
|
25
26
|
this.localizationTable.deleteSelection,
|
26
27
|
() => {
|
@@ -28,15 +29,17 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
28
29
|
this.editor.dispatch(selection!.deleteSelectedObjects());
|
29
30
|
this.tool.clearSelection();
|
30
31
|
},
|
32
|
+
localization,
|
31
33
|
);
|
32
34
|
const duplicateButton = new ActionButtonWidget(
|
33
|
-
editor,
|
35
|
+
editor, 'duplicate-btn',
|
34
36
|
() => editor.icons.makeDuplicateSelectionIcon(),
|
35
37
|
this.localizationTable.duplicateSelection,
|
36
38
|
() => {
|
37
39
|
const selection = this.tool.getSelection();
|
38
40
|
this.editor.dispatch(selection!.duplicateSelectedObjects());
|
39
41
|
},
|
42
|
+
localization,
|
40
43
|
);
|
41
44
|
|
42
45
|
this.addSubWidget(resizeButton);
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import Color4 from '../../Color4';
|
1
2
|
import Editor from '../../Editor';
|
2
3
|
import TextTool from '../../tools/TextTool';
|
3
4
|
import { EditorEventType } from '../../types';
|
@@ -5,11 +6,12 @@ import { toolbarCSSPrefix } from '../HTMLToolbar';
|
|
5
6
|
import { ToolbarLocalization } from '../localization';
|
6
7
|
import makeColorInput from '../makeColorInput';
|
7
8
|
import BaseToolWidget from './BaseToolWidget';
|
9
|
+
import { SavedToolbuttonState } from './BaseWidget';
|
8
10
|
|
9
11
|
export default class TextToolWidget extends BaseToolWidget {
|
10
12
|
private updateDropdownInputs: (()=>void)|null = null;
|
11
|
-
public constructor(editor: Editor, private tool: TextTool, localization
|
12
|
-
super(editor, tool, localization);
|
13
|
+
public constructor(editor: Editor, private tool: TextTool, localization?: ToolbarLocalization) {
|
14
|
+
super(editor, tool, 'text-tool-widget', localization);
|
13
15
|
|
14
16
|
editor.notifier.on(EditorEventType.ToolUpdated, evt => {
|
15
17
|
if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
|
@@ -36,7 +38,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
36
38
|
const fontInput = document.createElement('select');
|
37
39
|
const fontLabel = document.createElement('label');
|
38
40
|
|
39
|
-
const [ colorInput, colorInputContainer ] = makeColorInput(this.editor, color => {
|
41
|
+
const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
|
40
42
|
this.tool.setColor(color);
|
41
43
|
});
|
42
44
|
const colorLabel = document.createElement('label');
|
@@ -74,7 +76,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
74
76
|
|
75
77
|
this.updateDropdownInputs = () => {
|
76
78
|
const style = this.tool.getTextStyle();
|
77
|
-
|
79
|
+
setColorInputValue(style.renderingStyle.fill);
|
78
80
|
|
79
81
|
if (!fontsInInput.has(style.fontFamily)) {
|
80
82
|
addFontToInput(style.fontFamily);
|
@@ -86,4 +88,27 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
86
88
|
dropdown.replaceChildren(colorRow, fontRow);
|
87
89
|
return true;
|
88
90
|
}
|
91
|
+
|
92
|
+
public serializeState(): SavedToolbuttonState {
|
93
|
+
const textStyle = this.tool.getTextStyle();
|
94
|
+
|
95
|
+
return {
|
96
|
+
...super.serializeState(),
|
97
|
+
|
98
|
+
fontFamily: textStyle.fontFamily,
|
99
|
+
color: textStyle.renderingStyle.fill.toHexString(),
|
100
|
+
};
|
101
|
+
}
|
102
|
+
|
103
|
+
public deserializeFrom(state: SavedToolbuttonState) {
|
104
|
+
if (state.fontFamily && typeof(state.fontFamily) === 'string') {
|
105
|
+
this.tool.setFontFamily(state.fontFamily);
|
106
|
+
}
|
107
|
+
|
108
|
+
if (state.color && typeof(state.color) === 'string') {
|
109
|
+
this.tool.setColor(Color4.fromHex(state.color));
|
110
|
+
}
|
111
|
+
|
112
|
+
super.deserializeFrom(state);
|
113
|
+
}
|
89
114
|
}
|
package/src/tools/PanZoom.ts
CHANGED
@@ -21,6 +21,8 @@ export enum PanZoomMode {
|
|
21
21
|
RightClickDrags = 0x1 << 2,
|
22
22
|
SinglePointerGestures = 0x1 << 3,
|
23
23
|
Keyboard = 0x1 << 4,
|
24
|
+
|
25
|
+
RotationLocked = 0x1 << 5,
|
24
26
|
}
|
25
27
|
|
26
28
|
export default class PanZoom extends BaseTool {
|
@@ -90,10 +92,15 @@ export default class PanZoom extends BaseTool {
|
|
90
92
|
const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
|
91
93
|
|
92
94
|
const delta = this.getCenterDelta(screenCenter);
|
95
|
+
let rotation = angle - this.lastAngle;
|
96
|
+
|
97
|
+
if (this.isRotationLocked()) {
|
98
|
+
rotation = 0;
|
99
|
+
}
|
93
100
|
|
94
101
|
const transformUpdate = Mat33.translation(delta)
|
95
102
|
.rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
|
96
|
-
.rightMul(Mat33.zRotation(
|
103
|
+
.rightMul(Mat33.zRotation(rotation, canvasCenter));
|
97
104
|
this.lastScreenCenter = screenCenter;
|
98
105
|
this.lastDist = dist;
|
99
106
|
this.lastAngle = angle;
|
@@ -250,6 +257,10 @@ export default class PanZoom extends BaseTool {
|
|
250
257
|
rotation += 0.0001;
|
251
258
|
}
|
252
259
|
|
260
|
+
if (this.isRotationLocked()) {
|
261
|
+
rotation = 0;
|
262
|
+
}
|
263
|
+
|
253
264
|
const toCanvas = this.editor.viewport.screenToCanvasTransform;
|
254
265
|
|
255
266
|
// Transform without translating (treat toCanvas as a linear instead of
|
@@ -270,6 +281,22 @@ export default class PanZoom extends BaseTool {
|
|
270
281
|
return true;
|
271
282
|
}
|
272
283
|
|
284
|
+
private isRotationLocked(): boolean {
|
285
|
+
return !!(this.mode & PanZoomMode.RotationLocked);
|
286
|
+
}
|
287
|
+
|
288
|
+
// Sets whether the given `mode` is enabled. `mode` should be a single
|
289
|
+
// mode from the `PanZoomMode` enum.
|
290
|
+
public setModeEnabled(mode: PanZoomMode, enabled: boolean) {
|
291
|
+
let newMode = this.mode;
|
292
|
+
if (enabled) {
|
293
|
+
newMode |= mode;
|
294
|
+
} else {
|
295
|
+
newMode &= ~mode;
|
296
|
+
}
|
297
|
+
this.setMode(newMode);
|
298
|
+
}
|
299
|
+
|
273
300
|
public setMode(mode: PanZoomMode) {
|
274
301
|
if (mode !== this.mode) {
|
275
302
|
this.mode = mode;
|
package/src/tools/Pen.test.ts
CHANGED
@@ -144,7 +144,7 @@ describe('Pen', () => {
|
|
144
144
|
const elems = editor.image.getElementsIntersectingRegion(new Rect2(0, 0, 1000, 1000));
|
145
145
|
expect(elems).toHaveLength(1);
|
146
146
|
|
147
|
-
expect(elems[0].getBBox().topLeft).objEq(Vec2.of(420, 24),
|
147
|
+
expect(elems[0].getBBox().topLeft).objEq(Vec2.of(420, 24), 32); // ± 32
|
148
148
|
expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(420, 340), 25); // ± 25
|
149
149
|
});
|
150
|
-
});
|
150
|
+
});
|