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.
Files changed (82) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +1 -0
  2. package/CHANGELOG.md +12 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.js +3 -0
  5. package/dist/src/SVGLoader.js +5 -7
  6. package/dist/src/components/Stroke.js +10 -3
  7. package/dist/src/components/builders/FreehandLineBuilder.d.ts +10 -23
  8. package/dist/src/components/builders/FreehandLineBuilder.js +70 -396
  9. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +36 -0
  10. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +339 -0
  11. package/dist/src/components/lib.d.ts +2 -0
  12. package/dist/src/components/lib.js +2 -0
  13. package/dist/src/components/util/StrokeSmoother.d.ts +35 -0
  14. package/dist/src/components/util/StrokeSmoother.js +206 -0
  15. package/dist/src/math/Mat33.d.ts +2 -0
  16. package/dist/src/math/Mat33.js +4 -0
  17. package/dist/src/math/Path.d.ts +2 -0
  18. package/dist/src/math/Path.js +44 -0
  19. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -0
  20. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
  21. package/dist/src/rendering/renderers/SVGRenderer.js +20 -0
  22. package/dist/src/toolbar/HTMLToolbar.d.ts +3 -0
  23. package/dist/src/toolbar/HTMLToolbar.js +24 -0
  24. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  25. package/dist/src/toolbar/IconProvider.js +43 -1
  26. package/dist/src/toolbar/localization.d.ts +2 -0
  27. package/dist/src/toolbar/localization.js +2 -0
  28. package/dist/src/toolbar/makeColorInput.d.ts +2 -1
  29. package/dist/src/toolbar/makeColorInput.js +13 -2
  30. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +1 -1
  31. package/dist/src/toolbar/widgets/ActionButtonWidget.js +2 -2
  32. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -2
  33. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -3
  34. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -2
  35. package/dist/src/toolbar/widgets/BaseWidget.js +67 -6
  36. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +4 -0
  37. package/dist/src/toolbar/widgets/EraserToolWidget.js +3 -0
  38. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +3 -1
  39. package/dist/src/toolbar/widgets/HandToolWidget.js +22 -13
  40. package/dist/src/toolbar/widgets/PenToolWidget.d.ts +7 -1
  41. package/dist/src/toolbar/widgets/PenToolWidget.js +83 -12
  42. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  43. package/dist/src/toolbar/widgets/SelectionToolWidget.js +7 -7
  44. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +4 -1
  45. package/dist/src/toolbar/widgets/TextToolWidget.js +17 -3
  46. package/dist/src/tools/PanZoom.d.ts +4 -1
  47. package/dist/src/tools/PanZoom.js +24 -1
  48. package/dist/src/tools/Pen.d.ts +2 -2
  49. package/dist/src/tools/Pen.js +2 -2
  50. package/dist/src/tools/SelectionTool/Selection.d.ts +1 -0
  51. package/dist/src/tools/SelectionTool/Selection.js +8 -1
  52. package/dist/src/tools/ToolController.js +2 -1
  53. package/package.json +1 -1
  54. package/src/Color4.ts +2 -0
  55. package/src/SVGLoader.ts +8 -8
  56. package/src/components/Stroke.ts +16 -3
  57. package/src/components/builders/FreehandLineBuilder.ts +54 -495
  58. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +454 -0
  59. package/src/components/lib.ts +3 -1
  60. package/src/components/util/StrokeSmoother.ts +290 -0
  61. package/src/math/Mat33.ts +5 -0
  62. package/src/math/Path.test.ts +49 -0
  63. package/src/math/Path.ts +51 -0
  64. package/src/rendering/renderers/CanvasRenderer.ts +2 -0
  65. package/src/rendering/renderers/SVGRenderer.ts +24 -0
  66. package/src/toolbar/HTMLToolbar.ts +33 -0
  67. package/src/toolbar/IconProvider.ts +49 -1
  68. package/src/toolbar/localization.ts +4 -0
  69. package/src/toolbar/makeColorInput.ts +21 -3
  70. package/src/toolbar/widgets/ActionButtonWidget.ts +6 -3
  71. package/src/toolbar/widgets/BaseToolWidget.ts +4 -3
  72. package/src/toolbar/widgets/BaseWidget.ts +83 -5
  73. package/src/toolbar/widgets/EraserToolWidget.ts +11 -0
  74. package/src/toolbar/widgets/HandToolWidget.ts +48 -17
  75. package/src/toolbar/widgets/PenToolWidget.ts +110 -13
  76. package/src/toolbar/widgets/SelectionToolWidget.ts +8 -5
  77. package/src/toolbar/widgets/TextToolWidget.ts +29 -4
  78. package/src/tools/PanZoom.ts +28 -1
  79. package/src/tools/Pen.test.ts +2 -2
  80. package/src/tools/Pen.ts +1 -1
  81. package/src/tools/SelectionTool/Selection.ts +10 -1
  82. 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
- protected localizationTable: ToolbarLocalization,
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
- private subWidgets: BaseWidget[] = [];
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 localizationTable: ToolbarLocalization,
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 widget of this.subWidgets) {
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
- this.subWidgets.push(widget);
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: ToolbarLocalization) {
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, localizationTable: ToolbarLocalization,
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
- const mode = this.tool.getMode();
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
- this.touchPanningWidget = new HandModeWidget(
178
- editor, localizationTable,
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
- this.addSubWidget(this.touchPanningWidget);
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: ToolbarLocalization
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: localization.freehandPen,
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: localization.arrowPen,
53
+ name: this.localizationTable.arrowPen,
54
+ id: 'arrow',
55
+
40
56
  factory: makeArrowBuilder,
41
57
  },
42
58
  {
43
- name: localization.linePen,
59
+ name: this.localizationTable.linePen,
60
+ id: 'line',
61
+
44
62
  factory: makeLineBuilder,
45
63
  },
46
64
  {
47
- name: localization.filledRectanglePen,
65
+ name: this.localizationTable.filledRectanglePen,
66
+ id: 'filled-rectangle',
67
+
48
68
  factory: makeFilledRectangleBuilder,
49
69
  },
50
70
  {
51
- name: localization.outlinedRectanglePen,
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
- colorInput.value = this.tool.getColor().toHexString();
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
- if (penType.factory === this.tool.getStrokeFactory()) {
161
- objectTypeSelect.value = i.toString();
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: ToolbarLocalization
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, localization,
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, localization,
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, localization,
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: ToolbarLocalization) {
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
- colorInput.value = style.renderingStyle.fill.toHexString();
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
  }
@@ -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(angle - this.lastAngle, canvasCenter));
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;
@@ -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), 8); // ± 8
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
+ });