js-draw 0.11.2 → 0.12.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 (49) hide show
  1. package/.github/workflows/github-pages.yml +2 -0
  2. package/CHANGELOG.md +12 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.d.ts +1 -0
  5. package/dist/src/Color4.js +1 -0
  6. package/dist/src/Editor.js +4 -5
  7. package/dist/src/EditorImage.js +1 -11
  8. package/dist/src/SVGLoader.js +43 -35
  9. package/dist/src/components/AbstractComponent.d.ts +1 -0
  10. package/dist/src/components/AbstractComponent.js +15 -0
  11. package/dist/src/math/Rect2.d.ts +1 -0
  12. package/dist/src/math/Rect2.js +20 -0
  13. package/dist/src/toolbar/HTMLToolbar.d.ts +51 -0
  14. package/dist/src/toolbar/HTMLToolbar.js +63 -5
  15. package/dist/src/toolbar/IconProvider.d.ts +1 -1
  16. package/dist/src/toolbar/IconProvider.js +38 -9
  17. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +8 -1
  18. package/dist/src/toolbar/widgets/EraserToolWidget.js +45 -4
  19. package/dist/src/toolbar/widgets/PenToolWidget.js +2 -2
  20. package/dist/src/toolbar/widgets/SelectionToolWidget.js +12 -3
  21. package/dist/src/tools/Eraser.d.ts +10 -1
  22. package/dist/src/tools/Eraser.js +65 -13
  23. package/dist/src/tools/SelectionTool/Selection.d.ts +4 -1
  24. package/dist/src/tools/SelectionTool/Selection.js +64 -27
  25. package/dist/src/tools/SelectionTool/SelectionTool.js +3 -1
  26. package/dist/src/tools/TextTool.js +21 -6
  27. package/dist/src/tools/ToolController.js +3 -3
  28. package/dist/src/types.d.ts +2 -2
  29. package/package.json +1 -1
  30. package/src/Color4.ts +1 -0
  31. package/src/Editor.ts +3 -4
  32. package/src/EditorImage.ts +1 -11
  33. package/src/SVGLoader.ts +14 -14
  34. package/src/components/AbstractComponent.ts +19 -0
  35. package/src/math/Rect2.test.ts +22 -0
  36. package/src/math/Rect2.ts +26 -0
  37. package/src/toolbar/HTMLToolbar.ts +81 -5
  38. package/src/toolbar/IconProvider.ts +39 -9
  39. package/src/toolbar/widgets/EraserToolWidget.ts +64 -5
  40. package/src/toolbar/widgets/PenToolWidget.ts +2 -2
  41. package/src/toolbar/widgets/SelectionToolWidget.ts +2 -2
  42. package/src/tools/Eraser.test.ts +79 -0
  43. package/src/tools/Eraser.ts +81 -17
  44. package/src/tools/SelectionTool/Selection.ts +73 -23
  45. package/src/tools/SelectionTool/SelectionTool.test.ts +138 -21
  46. package/src/tools/SelectionTool/SelectionTool.ts +3 -1
  47. package/src/tools/TextTool.ts +26 -8
  48. package/src/tools/ToolController.ts +3 -3
  49. package/src/types.ts +2 -2
@@ -39,5 +39,6 @@ export default class Color4 {
39
39
  static yellow: Color4;
40
40
  static clay: Color4;
41
41
  static black: Color4;
42
+ static gray: Color4;
42
43
  static white: Color4;
43
44
  }
@@ -146,4 +146,5 @@ Color4.purple = Color4.ofRGB(0.5, 0.2, 0.5);
146
146
  Color4.yellow = Color4.ofRGB(1, 1, 0.1);
147
147
  Color4.clay = Color4.ofRGB(0.8, 0.4, 0.2);
148
148
  Color4.black = Color4.ofRGB(0, 0, 0);
149
+ Color4.gray = Color4.ofRGB(0.5, 0.5, 0.5);
149
150
  Color4.white = Color4.ofRGB(1, 1, 1);
@@ -188,8 +188,7 @@ export class Editor {
188
188
  addToolbar(defaultLayout = true) {
189
189
  const toolbar = new HTMLToolbar(this, this.container, this.localization);
190
190
  if (defaultLayout) {
191
- toolbar.addDefaultToolWidgets();
192
- toolbar.addDefaultActionButtons();
191
+ toolbar.addDefaults();
193
192
  }
194
193
  return toolbar;
195
194
  }
@@ -718,9 +717,9 @@ export class Editor {
718
717
  return __awaiter(this, void 0, void 0, function* () {
719
718
  this.showLoadingWarning(0);
720
719
  this.display.setDraftMode(true);
721
- yield loader.start((component) => {
722
- this.dispatchNoAnnounce(EditorImage.addElement(component));
723
- }, (countProcessed, totalToProcess) => {
720
+ yield loader.start((component) => __awaiter(this, void 0, void 0, function* () {
721
+ yield this.dispatchNoAnnounce(EditorImage.addElement(component));
722
+ }), (countProcessed, totalToProcess) => {
724
723
  if (countProcessed % 500 === 0) {
725
724
  this.showLoadingWarning(countProcessed / totalToProcess);
726
725
  this.rerender();
@@ -244,17 +244,7 @@ export class ImageNode {
244
244
  this.bbox = this.content.getBBox();
245
245
  }
246
246
  else {
247
- this.bbox = Rect2.empty;
248
- let isFirst = true;
249
- for (const child of this.children) {
250
- if (isFirst) {
251
- this.bbox = child.getBBox();
252
- isFirst = false;
253
- }
254
- else {
255
- this.bbox = this.bbox.union(child.getBBox());
256
- }
257
- }
247
+ this.bbox = Rect2.union(...this.children.map(child => child.getBBox()));
258
248
  }
259
249
  if (bubbleUp && !oldBBox.eq(this.bbox)) {
260
250
  (_a = this.parent) === null || _a === void 0 ? void 0 : _a.recomputeBBox(true);
@@ -125,22 +125,24 @@ export default class SVGLoader {
125
125
  // Adds a stroke with a single path
126
126
  addPath(node) {
127
127
  var _a;
128
- let elem;
129
- try {
130
- const strokeData = this.strokeDataFromElem(node);
131
- elem = new Stroke(strokeData);
132
- this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStrokeFillStyleAttrs, 'd']), new Set(supportedStrokeFillStyleAttrs));
133
- }
134
- catch (e) {
135
- console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
136
- if (this.storeUnknown) {
137
- elem = new UnknownSVGObject(node);
128
+ return __awaiter(this, void 0, void 0, function* () {
129
+ let elem;
130
+ try {
131
+ const strokeData = this.strokeDataFromElem(node);
132
+ elem = new Stroke(strokeData);
133
+ this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStrokeFillStyleAttrs, 'd']), new Set(supportedStrokeFillStyleAttrs));
138
134
  }
139
- else {
140
- return;
135
+ catch (e) {
136
+ console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
137
+ if (this.storeUnknown) {
138
+ elem = new UnknownSVGObject(node);
139
+ }
140
+ else {
141
+ return;
142
+ }
141
143
  }
142
- }
143
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
144
+ yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem));
145
+ });
144
146
  }
145
147
  // If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
146
148
  // to prevent storing duplicate transform information when saving the component.
@@ -223,14 +225,16 @@ export default class SVGLoader {
223
225
  }
224
226
  addText(elem) {
225
227
  var _a;
226
- try {
227
- const textElem = this.makeText(elem);
228
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
229
- }
230
- catch (e) {
231
- console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
232
- this.addUnknownNode(elem);
233
- }
228
+ return __awaiter(this, void 0, void 0, function* () {
229
+ try {
230
+ const textElem = this.makeText(elem);
231
+ yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem));
232
+ }
233
+ catch (e) {
234
+ console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
235
+ this.addUnknownNode(elem);
236
+ }
237
+ });
234
238
  }
235
239
  addImage(elem) {
236
240
  var _a, _b, _c;
@@ -243,20 +247,22 @@ export default class SVGLoader {
243
247
  const transform = this.getTransform(elem, supportedAttrs);
244
248
  const imageElem = yield ImageComponent.fromImage(image, transform);
245
249
  this.attachUnrecognisedAttrs(imageElem, elem, new Set(supportedAttrs), new Set(['transform']));
246
- (_c = this.onAddComponent) === null || _c === void 0 ? void 0 : _c.call(this, imageElem);
250
+ yield ((_c = this.onAddComponent) === null || _c === void 0 ? void 0 : _c.call(this, imageElem));
247
251
  }
248
252
  catch (e) {
249
253
  console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
250
- this.addUnknownNode(elem);
254
+ yield this.addUnknownNode(elem);
251
255
  }
252
256
  });
253
257
  }
254
258
  addUnknownNode(node) {
255
259
  var _a;
256
- if (this.storeUnknown) {
257
- const component = new UnknownSVGObject(node);
258
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
259
- }
260
+ return __awaiter(this, void 0, void 0, function* () {
261
+ if (this.storeUnknown) {
262
+ const component = new UnknownSVGObject(node);
263
+ yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component));
264
+ }
265
+ });
260
266
  }
261
267
  updateViewBox(node) {
262
268
  var _a;
@@ -278,9 +284,11 @@ export default class SVGLoader {
278
284
  }
279
285
  updateSVGAttrs(node) {
280
286
  var _a;
281
- if (this.storeUnknown) {
282
- (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
283
- }
287
+ return __awaiter(this, void 0, void 0, function* () {
288
+ if (this.storeUnknown) {
289
+ yield ((_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node))));
290
+ }
291
+ });
284
292
  }
285
293
  visit(node) {
286
294
  var _a;
@@ -292,10 +300,10 @@ export default class SVGLoader {
292
300
  // Continue -- visit the node's children.
293
301
  break;
294
302
  case 'path':
295
- this.addPath(node);
303
+ yield this.addPath(node);
296
304
  break;
297
305
  case 'text':
298
- this.addText(node);
306
+ yield this.addText(node);
299
307
  visitChildren = false;
300
308
  break;
301
309
  case 'image':
@@ -308,14 +316,14 @@ export default class SVGLoader {
308
316
  this.updateSVGAttrs(node);
309
317
  break;
310
318
  case 'style':
311
- this.addUnknownNode(node);
319
+ yield this.addUnknownNode(node);
312
320
  break;
313
321
  default:
314
322
  console.warn('Unknown SVG element,', node);
315
323
  if (!(node instanceof SVGElement)) {
316
324
  console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
317
325
  }
318
- this.addUnknownNode(node);
326
+ yield this.addUnknownNode(node);
319
327
  return;
320
328
  }
321
329
  if (visitChildren) {
@@ -25,6 +25,7 @@ export default abstract class AbstractComponent {
25
25
  getBBox(): Rect2;
26
26
  abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
27
27
  abstract intersects(lineSegment: LineSegment2): boolean;
28
+ intersectsRect(rect: Rect2): boolean;
28
29
  protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
29
30
  protected abstract applyTransformation(affineTransfm: Mat33): void;
30
31
  transformBy(affineTransfm: Mat33): SerializableCommand;
@@ -43,6 +43,21 @@ export default class AbstractComponent {
43
43
  getBBox() {
44
44
  return this.contentBBox;
45
45
  }
46
+ intersectsRect(rect) {
47
+ // If this component intersects rect,
48
+ // it is either contained entirely within rect or intersects one of rect's edges.
49
+ // If contained within,
50
+ if (rect.containsRect(this.getBBox())) {
51
+ return true;
52
+ }
53
+ // Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
54
+ // As such, test with more lines than just the rect's edges.
55
+ const testLines = [];
56
+ for (const subregion of rect.divideIntoGrid(2, 2)) {
57
+ testLines.push(...subregion.getEdges());
58
+ }
59
+ return testLines.some(edge => this.intersects(edge));
60
+ }
46
61
  // Returns a command that, when applied, transforms this by [affineTransfm] and
47
62
  // updates the editor.
48
63
  transformBy(affineTransfm) {
@@ -45,6 +45,7 @@ export default class Rect2 {
45
45
  toString(): string;
46
46
  static fromCorners(corner1: Point2, corner2: Point2): Rect2;
47
47
  static bboxOf(points: Point2[], margin?: number): Rect2;
48
+ static union(...rects: Rect2[]): Rect2;
48
49
  static of(template: RectTemplate): Rect2;
49
50
  static empty: Rect2;
50
51
  static unitSquare: Rect2;
@@ -197,6 +197,26 @@ export default class Rect2 {
197
197
  }
198
198
  return Rect2.fromCorners(Vec2.of(minX - margin, minY - margin), Vec2.of(maxX + margin, maxY + margin));
199
199
  }
200
+ // @returns a rectangle that contains all of the given rectangles, the bounding box
201
+ // of the given rectangles.
202
+ static union(...rects) {
203
+ if (rects.length === 0) {
204
+ return Rect2.empty;
205
+ }
206
+ const firstRect = rects[0];
207
+ let minX = firstRect.topLeft.x;
208
+ let minY = firstRect.topLeft.y;
209
+ let maxX = firstRect.bottomRight.x;
210
+ let maxY = firstRect.bottomRight.y;
211
+ for (let i = 1; i < rects.length; i++) {
212
+ const rect = rects[i];
213
+ minX = Math.min(minX, rect.topLeft.x);
214
+ minY = Math.min(minY, rect.topLeft.y);
215
+ maxX = Math.max(maxX, rect.bottomRight.x);
216
+ maxY = Math.max(maxY, rect.bottomRight.y);
217
+ }
218
+ return new Rect2(minX, minY, maxX - minX, maxY - minY);
219
+ }
200
220
  static of(template) {
201
221
  var _a, _b, _c, _d;
202
222
  const width = (_b = (_a = template.width) !== null && _a !== void 0 ? _a : template.w) !== null && _b !== void 0 ? _b : 0;
@@ -3,6 +3,11 @@ import { ToolbarLocalization } from './localization';
3
3
  import { ActionButtonIcon } from './types';
4
4
  import BaseWidget from './widgets/BaseWidget';
5
5
  export declare const toolbarCSSPrefix = "toolbar-";
6
+ interface SpacerOptions {
7
+ grow: number;
8
+ minSize: string;
9
+ maxSize: string;
10
+ }
6
11
  export default class HTMLToolbar {
7
12
  private editor;
8
13
  private localizationTable;
@@ -13,8 +18,45 @@ export default class HTMLToolbar {
13
18
  /** @internal */
14
19
  constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
15
20
  setupColorPickers(): void;
21
+ /**
22
+ * Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
23
+ * (i.e. its `addTo` method should not have been called).
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const toolbar = editor.addToolbar();
28
+ * const insertImageWidget = new InsertImageWidget(editor);
29
+ * toolbar.addWidget(insertImageWidget);
30
+ * ```
31
+ */
16
32
  addWidget(widget: BaseWidget): void;
33
+ /**
34
+ * Adds a spacer.
35
+ *
36
+ * @example
37
+ * Adding a save button that moves to the very right edge of the toolbar
38
+ * while keeping the other buttons centered:
39
+ * ```ts
40
+ * const toolbar = editor.addToolbar(false);
41
+ *
42
+ * toolbar.addSpacer({ grow: 1 });
43
+ * toolbar.addDefaults();
44
+ * toolbar.addSpacer({ grow: 1 });
45
+ *
46
+ * toolbar.addActionButton({
47
+ * label: 'Save',
48
+ * icon: editor.icons.makeSaveIcon(),
49
+ * }, () => {
50
+ * saveCallback();
51
+ * });
52
+ * ```
53
+ */
54
+ addSpacer(options?: Partial<SpacerOptions>): void;
17
55
  serializeState(): string;
56
+ /**
57
+ * Deserialize toolbar widgets from the given state.
58
+ * Assumes that toolbar widgets are in the same order as when state was serialized.
59
+ */
18
60
  deserializeState(state: string): void;
19
61
  /**
20
62
  * Adds an action button with `title` to this toolbar (or to the given `parent` element).
@@ -25,4 +67,13 @@ export default class HTMLToolbar {
25
67
  addUndoRedoButtons(): void;
26
68
  addDefaultToolWidgets(): void;
27
69
  addDefaultActionButtons(): void;
70
+ /**
71
+ * Adds both the default tool widgets and action buttons. Equivalent to
72
+ * ```ts
73
+ * toolbar.addDefaultToolWidgets();
74
+ * toolbar.addDefaultActionButtons();
75
+ * ```
76
+ */
77
+ addDefaults(): void;
28
78
  }
79
+ export {};
@@ -12,7 +12,8 @@ import EraserWidget from './widgets/EraserToolWidget';
12
12
  import SelectionToolWidget from './widgets/SelectionToolWidget';
13
13
  import TextToolWidget from './widgets/TextToolWidget';
14
14
  import HandToolWidget from './widgets/HandToolWidget';
15
- import { ActionButtonWidget, InsertImageWidget } from './lib';
15
+ import ActionButtonWidget from './widgets/ActionButtonWidget';
16
+ import InsertImageWidget from './widgets/InsertImageWidget';
16
17
  export const toolbarCSSPrefix = 'toolbar-';
17
18
  export default class HTMLToolbar {
18
19
  /** @internal */
@@ -94,8 +95,17 @@ export default class HTMLToolbar {
94
95
  }
95
96
  });
96
97
  }
97
- // Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
98
- // (i.e. its `addTo` method should not have been called).
98
+ /**
99
+ * Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
100
+ * (i.e. its `addTo` method should not have been called).
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const toolbar = editor.addToolbar();
105
+ * const insertImageWidget = new InsertImageWidget(editor);
106
+ * toolbar.addWidget(insertImageWidget);
107
+ * ```
108
+ */
99
109
  addWidget(widget) {
100
110
  // Prevent name collisions
101
111
  const id = widget.getUniqueIdIn(this.widgets);
@@ -105,6 +115,41 @@ export default class HTMLToolbar {
105
115
  widget.addTo(this.container);
106
116
  this.setupColorPickers();
107
117
  }
118
+ /**
119
+ * Adds a spacer.
120
+ *
121
+ * @example
122
+ * Adding a save button that moves to the very right edge of the toolbar
123
+ * while keeping the other buttons centered:
124
+ * ```ts
125
+ * const toolbar = editor.addToolbar(false);
126
+ *
127
+ * toolbar.addSpacer({ grow: 1 });
128
+ * toolbar.addDefaults();
129
+ * toolbar.addSpacer({ grow: 1 });
130
+ *
131
+ * toolbar.addActionButton({
132
+ * label: 'Save',
133
+ * icon: editor.icons.makeSaveIcon(),
134
+ * }, () => {
135
+ * saveCallback();
136
+ * });
137
+ * ```
138
+ */
139
+ addSpacer(options = {}) {
140
+ const spacer = document.createElement('div');
141
+ spacer.classList.add(`${toolbarCSSPrefix}spacer`);
142
+ if (options.grow) {
143
+ spacer.style.flexGrow = `${options.grow}`;
144
+ }
145
+ if (options.minSize) {
146
+ spacer.style.minWidth = options.minSize;
147
+ }
148
+ if (options.maxSize) {
149
+ spacer.style.maxWidth = options.maxSize;
150
+ }
151
+ this.container.appendChild(spacer);
152
+ }
108
153
  serializeState() {
109
154
  const result = {};
110
155
  for (const widgetId in this.widgets) {
@@ -112,8 +157,10 @@ export default class HTMLToolbar {
112
157
  }
113
158
  return JSON.stringify(result);
114
159
  }
115
- // Deserialize toolbar widgets from the given state.
116
- // Assumes that toolbar widgets are in the same order as when state was serialized.
160
+ /**
161
+ * Deserialize toolbar widgets from the given state.
162
+ * Assumes that toolbar widgets are in the same order as when state was serialized.
163
+ */
117
164
  deserializeState(state) {
118
165
  const data = JSON.parse(state);
119
166
  for (const widgetId in data) {
@@ -188,5 +235,16 @@ export default class HTMLToolbar {
188
235
  addDefaultActionButtons() {
189
236
  this.addUndoRedoButtons();
190
237
  }
238
+ /**
239
+ * Adds both the default tool widgets and action buttons. Equivalent to
240
+ * ```ts
241
+ * toolbar.addDefaultToolWidgets();
242
+ * toolbar.addDefaultActionButtons();
243
+ * ```
244
+ */
245
+ addDefaults() {
246
+ this.addDefaultToolWidgets();
247
+ this.addDefaultActionButtons();
248
+ }
191
249
  }
192
250
  HTMLToolbar.colorisStarted = false;
@@ -7,7 +7,7 @@ export default class IconProvider {
7
7
  makeUndoIcon(): IconType;
8
8
  makeRedoIcon(mirror?: boolean): IconType;
9
9
  makeDropdownIcon(): IconType;
10
- makeEraserIcon(): IconType;
10
+ makeEraserIcon(eraserSize?: number): IconType;
11
11
  makeSelectionIcon(): IconType;
12
12
  /**
13
13
  * @param pathData - SVG path data (e.g. `m10,10l30,30z`)
@@ -67,19 +67,46 @@ export default class IconProvider {
67
67
  icon.setAttribute('viewBox', '0 0 100 100');
68
68
  return icon;
69
69
  }
70
- makeEraserIcon() {
70
+ makeEraserIcon(eraserSize) {
71
71
  const icon = document.createElementNS(svgNamespace, 'svg');
72
- // Draw an eraser-like shape
72
+ eraserSize !== null && eraserSize !== void 0 ? eraserSize : (eraserSize = 10);
73
+ const scaledSize = eraserSize / 4;
74
+ const eraserColor = '#ff70af';
75
+ // Draw an eraser-like shape. Created with Inkscape
73
76
  icon.innerHTML = `
74
77
  <g>
75
- <rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
78
+ <path
79
+ style="fill:${eraserColor}"
80
+ stroke="black"
81
+ transform="rotate(41.35)"
82
+ d="M 52.5 27
83
+ C 50 28.9 48.9 31.7 48.9 34.8
84
+ L 48.9 39.8
85
+ C 48.9 45.3 53.4 49.8 58.9 49.8
86
+ L 103.9 49.8
87
+ C 105.8 49.8 107.6 49.2 109.1 48.3
88
+ L 110.2 ${scaledSize + 49.5} L 159.7 ${scaledSize + 5}
89
+ L 157.7 ${-scaledSize + 5.2} L 112.4 ${49.5 - scaledSize}
90
+ C 113.4 43.5 113.9 41.7 113.9 39.8
91
+ L 113.9 34.8
92
+ C 113.9 29.3 109.4 24.8 103.9 24.8
93
+ L 58.9 24.8
94
+ C 56.5 24.8 54.3 25.7 52.5 27
95
+ z "
96
+ id="path438" />
97
+
76
98
  <rect
77
- x=10 y=10 width=80 height=50
99
+ stroke="#cc8077"
78
100
  ${iconColorFill}
79
- />
101
+ id="rect218"
102
+ width="65"
103
+ height="75"
104
+ x="48.9"
105
+ y="-38.7"
106
+ transform="rotate(41.35)" />
80
107
  </g>
81
108
  `;
82
- icon.setAttribute('viewBox', '0 0 100 100');
109
+ icon.setAttribute('viewBox', '0 0 120 120');
83
110
  return icon;
84
111
  }
85
112
  makeSelectionIcon() {
@@ -353,17 +380,19 @@ export default class IconProvider {
353
380
  return icon;
354
381
  }
355
382
  makeIconFromFactory(pen, factory) {
356
- const toolThickness = pen.getThickness();
383
+ // Increase the thickness we use to generate the icon less with larger actual thicknesses.
384
+ // We want the icon to be recognisable with a large range of thicknesses.
385
+ const thickness = Math.sqrt(pen.getThickness()) * 3;
357
386
  const nowTime = (new Date()).getTime();
358
387
  const startPoint = {
359
388
  pos: Vec2.of(10, 10),
360
- width: toolThickness / 5,
389
+ width: thickness,
361
390
  color: pen.getColor(),
362
391
  time: nowTime - 100,
363
392
  };
364
393
  const endPoint = {
365
394
  pos: Vec2.of(90, 90),
366
- width: toolThickness / 5,
395
+ width: thickness,
367
396
  color: pen.getColor(),
368
397
  time: nowTime,
369
398
  };
@@ -2,9 +2,16 @@ import Editor from '../../Editor';
2
2
  import Eraser from '../../tools/Eraser';
3
3
  import { ToolbarLocalization } from '../localization';
4
4
  import BaseToolWidget from './BaseToolWidget';
5
+ import { SavedToolbuttonState } from './BaseWidget';
5
6
  export default class EraserToolWidget extends BaseToolWidget {
7
+ private tool;
8
+ private thicknessInput;
6
9
  constructor(editor: Editor, tool: Eraser, localizationTable?: ToolbarLocalization);
7
10
  protected getTitle(): string;
8
11
  protected createIcon(): Element;
9
- protected fillDropdown(_dropdown: HTMLElement): boolean;
12
+ private updateInputs;
13
+ private static nextThicknessInputId;
14
+ protected fillDropdown(dropdown: HTMLElement): boolean;
15
+ serializeState(): SavedToolbuttonState;
16
+ deserializeFrom(state: SavedToolbuttonState): void;
10
17
  }
@@ -1,16 +1,57 @@
1
+ import { EditorEventType } from '../../types';
2
+ import { toolbarCSSPrefix } from '../HTMLToolbar';
1
3
  import BaseToolWidget from './BaseToolWidget';
2
4
  export default class EraserToolWidget extends BaseToolWidget {
3
5
  constructor(editor, tool, localizationTable) {
4
6
  super(editor, tool, 'eraser-tool-widget', localizationTable);
7
+ this.tool = tool;
8
+ this.thicknessInput = null;
9
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
10
+ if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === this.tool) {
11
+ this.updateInputs();
12
+ this.updateIcon();
13
+ }
14
+ });
5
15
  }
6
16
  getTitle() {
7
17
  return this.localizationTable.eraser;
8
18
  }
9
19
  createIcon() {
10
- return this.editor.icons.makeEraserIcon();
20
+ return this.editor.icons.makeEraserIcon(this.tool.getThickness());
11
21
  }
12
- fillDropdown(_dropdown) {
13
- // No dropdown associated with the eraser
14
- return false;
22
+ updateInputs() {
23
+ if (this.thicknessInput) {
24
+ this.thicknessInput.value = `${this.tool.getThickness()}`;
25
+ }
26
+ }
27
+ fillDropdown(dropdown) {
28
+ const thicknessLabel = document.createElement('label');
29
+ this.thicknessInput = document.createElement('input');
30
+ this.thicknessInput.type = 'range';
31
+ this.thicknessInput.min = '4';
32
+ this.thicknessInput.max = '40';
33
+ this.thicknessInput.oninput = () => {
34
+ this.tool.setThickness(parseFloat(this.thicknessInput.value));
35
+ };
36
+ this.thicknessInput.id = `${toolbarCSSPrefix}eraserThicknessInput${EraserToolWidget.nextThicknessInputId++}`;
37
+ thicknessLabel.innerText = this.localizationTable.thicknessLabel;
38
+ thicknessLabel.htmlFor = this.thicknessInput.id;
39
+ this.updateInputs();
40
+ dropdown.replaceChildren(thicknessLabel, this.thicknessInput);
41
+ return true;
42
+ }
43
+ serializeState() {
44
+ return Object.assign(Object.assign({}, super.serializeState()), { thickness: this.tool.getThickness() });
45
+ }
46
+ deserializeFrom(state) {
47
+ super.deserializeFrom(state);
48
+ if (state.thickness) {
49
+ const parsedThickness = parseFloat(state.thickness);
50
+ if (typeof parsedThickness !== 'number' || !isFinite(parsedThickness)) {
51
+ throw new Error(`Deserializing property ${parsedThickness} is not a number or is not finite.`);
52
+ }
53
+ this.tool.setThickness(parsedThickness);
54
+ }
15
55
  }
16
56
  }
57
+ EraserToolWidget.nextThicknessInputId = 0;
@@ -105,8 +105,8 @@ export default class PenToolWidget extends BaseToolWidget {
105
105
  const objectSelectLabel = document.createElement('label');
106
106
  const objectTypeSelect = document.createElement('select');
107
107
  // Give inputs IDs so we can label them with a <label for=...>Label text</label>
108
- thicknessInput.id = `${toolbarCSSPrefix}thicknessInput${PenToolWidget.idCounter++}`;
109
- objectTypeSelect.id = `${toolbarCSSPrefix}builderSelect${PenToolWidget.idCounter++}`;
108
+ thicknessInput.id = `${toolbarCSSPrefix}penThicknessInput${PenToolWidget.idCounter++}`;
109
+ objectTypeSelect.id = `${toolbarCSSPrefix}penBuilderSelect${PenToolWidget.idCounter++}`;
110
110
  thicknessLabel.innerText = this.localizationTable.thicknessLabel;
111
111
  thicknessLabel.setAttribute('for', thicknessInput.id);
112
112
  objectSelectLabel.innerText = this.localizationTable.selectObjectType;
@@ -1,3 +1,12 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { EditorEventType } from '../../types';
2
11
  import ActionButtonWidget from './ActionButtonWidget';
3
12
  import BaseToolWidget from './BaseToolWidget';
@@ -13,10 +22,10 @@ export default class SelectionToolWidget extends BaseToolWidget {
13
22
  this.editor.dispatch(selection.deleteSelectedObjects());
14
23
  this.tool.clearSelection();
15
24
  }, localization);
16
- const duplicateButton = new ActionButtonWidget(editor, 'duplicate-btn', () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => {
25
+ const duplicateButton = new ActionButtonWidget(editor, 'duplicate-btn', () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => __awaiter(this, void 0, void 0, function* () {
17
26
  const selection = this.tool.getSelection();
18
- this.editor.dispatch(selection.duplicateSelectedObjects());
19
- }, localization);
27
+ this.editor.dispatch(yield selection.duplicateSelectedObjects());
28
+ }), localization);
20
29
  this.addSubWidget(resizeButton);
21
30
  this.addSubWidget(deleteButton);
22
31
  this.addSubWidget(duplicateButton);