js-draw 0.5.0 → 0.6.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 (73) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +8 -5
  4. package/dist/src/Editor.js +4 -1
  5. package/dist/src/EditorImage.d.ts +3 -0
  6. package/dist/src/EditorImage.js +7 -0
  7. package/dist/src/SVGLoader.js +5 -6
  8. package/dist/src/components/AbstractComponent.d.ts +1 -0
  9. package/dist/src/components/AbstractComponent.js +4 -0
  10. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -0
  11. package/dist/src/components/SVGGlobalAttributesObject.js +3 -0
  12. package/dist/src/components/Text.d.ts +3 -5
  13. package/dist/src/components/Text.js +19 -10
  14. package/dist/src/components/UnknownSVGObject.d.ts +1 -0
  15. package/dist/src/components/UnknownSVGObject.js +3 -0
  16. package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
  17. package/dist/src/testing/beforeEachFile.js +4 -0
  18. package/dist/src/toolbar/HTMLToolbar.js +2 -3
  19. package/dist/src/toolbar/IconProvider.d.ts +24 -0
  20. package/dist/src/toolbar/IconProvider.js +415 -0
  21. package/dist/src/toolbar/lib.d.ts +1 -1
  22. package/dist/src/toolbar/lib.js +1 -2
  23. package/dist/src/toolbar/localization.d.ts +0 -1
  24. package/dist/src/toolbar/localization.js +0 -1
  25. package/dist/src/toolbar/makeColorInput.js +1 -2
  26. package/dist/src/toolbar/widgets/BaseWidget.js +1 -2
  27. package/dist/src/toolbar/widgets/EraserToolWidget.js +1 -2
  28. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +5 -3
  29. package/dist/src/toolbar/widgets/HandToolWidget.js +35 -12
  30. package/dist/src/toolbar/widgets/PenToolWidget.js +2 -3
  31. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +3 -0
  32. package/dist/src/toolbar/widgets/SelectionToolWidget.js +20 -7
  33. package/dist/src/toolbar/widgets/TextToolWidget.js +1 -2
  34. package/dist/src/tools/PanZoom.d.ts +1 -1
  35. package/dist/src/tools/PanZoom.js +4 -1
  36. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -0
  37. package/dist/src/tools/SelectionTool/SelectionTool.js +66 -3
  38. package/dist/src/tools/ToolController.js +1 -0
  39. package/dist/src/tools/localization.d.ts +1 -0
  40. package/dist/src/tools/localization.js +1 -0
  41. package/package.json +1 -1
  42. package/src/Editor.ts +11 -5
  43. package/src/EditorImage.ts +9 -0
  44. package/src/SVGLoader.test.ts +37 -0
  45. package/src/SVGLoader.ts +5 -6
  46. package/src/components/AbstractComponent.ts +5 -0
  47. package/src/components/SVGGlobalAttributesObject.ts +4 -0
  48. package/src/components/Text.test.ts +1 -16
  49. package/src/components/Text.ts +21 -11
  50. package/src/components/UnknownSVGObject.ts +4 -0
  51. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  52. package/src/testing/beforeEachFile.ts +6 -1
  53. package/src/toolbar/HTMLToolbar.ts +2 -3
  54. package/src/toolbar/IconProvider.ts +476 -0
  55. package/src/toolbar/lib.ts +1 -1
  56. package/src/toolbar/localization.ts +0 -2
  57. package/src/toolbar/makeColorInput.ts +1 -2
  58. package/src/toolbar/widgets/BaseWidget.ts +1 -2
  59. package/src/toolbar/widgets/EraserToolWidget.ts +1 -2
  60. package/src/toolbar/widgets/HandToolWidget.ts +42 -20
  61. package/src/toolbar/widgets/PenToolWidget.ts +2 -3
  62. package/src/toolbar/widgets/SelectionToolWidget.ts +24 -8
  63. package/src/toolbar/widgets/TextToolWidget.ts +1 -2
  64. package/src/tools/PanZoom.ts +4 -1
  65. package/src/tools/SelectionTool/SelectionTool.css +1 -0
  66. package/src/tools/SelectionTool/SelectionTool.test.ts +40 -0
  67. package/src/tools/SelectionTool/SelectionTool.ts +73 -4
  68. package/src/tools/ToolController.ts +1 -0
  69. package/src/tools/localization.ts +4 -0
  70. package/typedoc.json +5 -1
  71. package/dist/src/toolbar/icons.d.ts +0 -20
  72. package/dist/src/toolbar/icons.js +0 -385
  73. package/src/toolbar/icons.ts +0 -443
@@ -1,6 +1,5 @@
1
1
  import { EditorEventType } from '../../types';
2
2
  import { toolbarCSSPrefix } from '../HTMLToolbar';
3
- import { makeTextIcon } from '../icons';
4
3
  import makeColorInput from '../makeColorInput';
5
4
  import BaseToolWidget from './BaseToolWidget';
6
5
  export default class TextToolWidget extends BaseToolWidget {
@@ -21,7 +20,7 @@ export default class TextToolWidget extends BaseToolWidget {
21
20
  }
22
21
  createIcon() {
23
22
  const textStyle = this.tool.getTextStyle();
24
- return makeTextIcon(textStyle);
23
+ return this.editor.icons.makeTextIcon(textStyle);
25
24
  }
26
25
  fillDropdown(dropdown) {
27
26
  const fontRow = document.createElement('div');
@@ -35,7 +35,7 @@ export default class PanZoom extends BaseTool {
35
35
  onGestureCancel(): void;
36
36
  private updateTransform;
37
37
  onWheel({ delta, screenPos }: WheelEvt): boolean;
38
- onKeyPress({ key }: KeyPressEvent): boolean;
38
+ onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean;
39
39
  setMode(mode: PanZoomMode): void;
40
40
  getMode(): PanZoomMode;
41
41
  }
@@ -134,10 +134,13 @@ export default class PanZoom extends BaseTool {
134
134
  this.updateTransform(transformUpdate, true);
135
135
  return true;
136
136
  }
137
- onKeyPress({ key }) {
137
+ onKeyPress({ key, ctrlKey, altKey }) {
138
138
  if (!(this.mode & PanZoomMode.Keyboard)) {
139
139
  return false;
140
140
  }
141
+ if (ctrlKey || altKey) {
142
+ return false;
143
+ }
141
144
  // No need to keep the same the transform for keyboard events.
142
145
  this.transform = Viewport.transformBy(Mat33.identity);
143
146
  let translation = Vec2.zero;
@@ -10,6 +10,8 @@ export default class SelectionTool extends BaseTool {
10
10
  private prevSelectionBox;
11
11
  private selectionBox;
12
12
  private lastEvtTarget;
13
+ private expandingSelectionBox;
14
+ private shiftKeyPressed;
13
15
  constructor(editor: Editor, description: string);
14
16
  private makeSelectionBox;
15
17
  private selectionBoxHandlingEvt;
@@ -26,6 +28,7 @@ export default class SelectionTool extends BaseTool {
26
28
  onCopy(event: CopyEvent): boolean;
27
29
  setEnabled(enabled: boolean): void;
28
30
  getSelection(): Selection | null;
31
+ getSelectedObjects(): AbstractComponent[];
29
32
  setSelection(objects: AbstractComponent[]): void;
30
33
  clearSelection(): void;
31
34
  }
@@ -16,6 +16,8 @@ export default class SelectionTool extends BaseTool {
16
16
  super(editor.notifier, description);
17
17
  this.editor = editor;
18
18
  this.lastEvtTarget = null;
19
+ this.expandingSelectionBox = false;
20
+ this.shiftKeyPressed = false;
19
21
  this.selectionBoxHandlingEvt = false;
20
22
  this.handleOverlay = document.createElement('div');
21
23
  editor.createHTMLOverlay(this.handleOverlay);
@@ -34,10 +36,13 @@ export default class SelectionTool extends BaseTool {
34
36
  });
35
37
  }
36
38
  makeSelectionBox(selectionStartPos) {
39
+ var _a;
37
40
  this.prevSelectionBox = this.selectionBox;
38
41
  this.selectionBox = new Selection(selectionStartPos, this.editor);
39
- // Remove any previous selection rects
40
- this.handleOverlay.replaceChildren();
42
+ if (!this.expandingSelectionBox) {
43
+ // Remove any previous selection rects
44
+ (_a = this.prevSelectionBox) === null || _a === void 0 ? void 0 : _a.cancelSelection();
45
+ }
41
46
  this.selectionBox.addTo(this.handleOverlay);
42
47
  }
43
48
  onPointerDown(event) {
@@ -45,8 +50,11 @@ export default class SelectionTool extends BaseTool {
45
50
  if (event.allPointers.length === 1 && event.current.isPrimary) {
46
51
  if (this.lastEvtTarget && ((_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.onDragStart(event.current, this.lastEvtTarget))) {
47
52
  this.selectionBoxHandlingEvt = true;
53
+ this.expandingSelectionBox = false;
48
54
  }
49
55
  else {
56
+ // Shift key: Combine the new and old selection boxes at the end of the gesture.
57
+ this.expandingSelectionBox = this.shiftKeyPressed;
50
58
  this.makeSelectionBox(event.current.canvasPos);
51
59
  }
52
60
  return true;
@@ -81,6 +89,7 @@ export default class SelectionTool extends BaseTool {
81
89
  this.selectionBox = null;
82
90
  }
83
91
  }
92
+ // Called after a gestureCancel and a pointerUp
84
93
  onGestureEnd() {
85
94
  this.lastEvtTarget = null;
86
95
  if (!this.selectionBox)
@@ -105,7 +114,19 @@ export default class SelectionTool extends BaseTool {
105
114
  if (!this.selectionBox)
106
115
  return;
107
116
  this.selectionBox.setToPoint(event.current.canvasPos);
108
- this.onGestureEnd();
117
+ // Were we expanding the previous selection?
118
+ if (this.expandingSelectionBox && this.prevSelectionBox) {
119
+ // If so, finish expanding.
120
+ this.expandingSelectionBox = false;
121
+ this.selectionBox.resolveToObjects();
122
+ this.setSelection([
123
+ ...this.selectionBox.getSelectedObjects(),
124
+ ...this.prevSelectionBox.getSelectedObjects(),
125
+ ]);
126
+ }
127
+ else {
128
+ this.onGestureEnd();
129
+ }
109
130
  }
110
131
  onGestureCancel() {
111
132
  var _a, _b, _c;
@@ -118,8 +139,28 @@ export default class SelectionTool extends BaseTool {
118
139
  this.selectionBox = this.prevSelectionBox;
119
140
  (_c = this.selectionBox) === null || _c === void 0 ? void 0 : _c.addTo(this.handleOverlay);
120
141
  }
142
+ this.expandingSelectionBox = false;
121
143
  }
122
144
  onKeyPress(event) {
145
+ if (this.selectionBox && event.ctrlKey && event.key === 'd') {
146
+ // Handle duplication on key up — we don't want to accidentally duplicate
147
+ // many times.
148
+ return true;
149
+ }
150
+ else if (event.key === 'a' && event.ctrlKey) {
151
+ // Handle ctrl+A on key up.
152
+ // Return early to prevent 'a' from moving the selection/view.
153
+ return true;
154
+ }
155
+ else if (event.ctrlKey) {
156
+ // Don't transform the selection with, for example, ctrl+i.
157
+ // Pass it to another tool, if apliccable.
158
+ return false;
159
+ }
160
+ else if (event.key === 'Shift') {
161
+ this.shiftKeyPressed = true;
162
+ return true;
163
+ }
123
164
  let rotationSteps = 0;
124
165
  let xTranslateSteps = 0;
125
166
  let yTranslateSteps = 0;
@@ -194,6 +235,20 @@ export default class SelectionTool extends BaseTool {
194
235
  return handled;
195
236
  }
196
237
  onKeyUp(evt) {
238
+ if (evt.key === 'Shift') {
239
+ this.shiftKeyPressed = false;
240
+ return true;
241
+ }
242
+ else if (evt.ctrlKey) {
243
+ if (this.selectionBox && evt.key === 'd') {
244
+ this.editor.dispatch(this.selectionBox.duplicateSelectedObjects());
245
+ return true;
246
+ }
247
+ else if (evt.key === 'a') {
248
+ this.setSelection(this.editor.image.getAllElements());
249
+ return true;
250
+ }
251
+ }
197
252
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
198
253
  this.selectionBox.finalizeTransform();
199
254
  return true;
@@ -244,10 +299,18 @@ export default class SelectionTool extends BaseTool {
244
299
  }
245
300
  }
246
301
  // Get the object responsible for displaying this' selection.
302
+ // @internal
247
303
  getSelection() {
248
304
  return this.selectionBox;
249
305
  }
306
+ getSelectedObjects() {
307
+ var _a, _b;
308
+ return (_b = (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.getSelectedObjects()) !== null && _b !== void 0 ? _b : [];
309
+ }
310
+ // Select the given `objects`. Any non-selectable objects in `objects` are ignored.
250
311
  setSelection(objects) {
312
+ // Only select selectable objects.
313
+ objects = objects.filter(obj => obj.isSelectable());
251
314
  let bbox = null;
252
315
  for (const object of objects) {
253
316
  if (bbox) {
@@ -29,6 +29,7 @@ export default class ToolController {
29
29
  new Eraser(editor, localization.eraserTool),
30
30
  new SelectionTool(editor, localization.selectionTool),
31
31
  new TextTool(editor, localization.textTool, localization),
32
+ new PanZoom(editor, PanZoomMode.SinglePointerGestures, localization.anyDevicePanning)
32
33
  ];
33
34
  this.tools = [
34
35
  new PipetteTool(editor, localization.pipetteTool),
@@ -12,6 +12,7 @@ export interface ToolLocalization {
12
12
  enterTextToInsert: string;
13
13
  changeTool: string;
14
14
  pasteHandler: string;
15
+ anyDevicePanning: string;
15
16
  copied: (count: number, description: string) => string;
16
17
  pasted: (count: number, description: string) => string;
17
18
  toolEnabledAnnouncement: (toolName: string) => string;
@@ -12,6 +12,7 @@ export const defaultToolLocalization = {
12
12
  enterTextToInsert: 'Text to insert',
13
13
  changeTool: 'Change tool',
14
14
  pasteHandler: 'Copy paste handler',
15
+ anyDevicePanning: 'Any device panning',
15
16
  copied: (count, description) => `Copied ${count} ${description}`,
16
17
  pasted: (count, description) => `Pasted ${count} ${description}`,
17
18
  toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
package/src/Editor.ts CHANGED
@@ -37,6 +37,7 @@ import Mat33 from './math/Mat33';
37
37
  import Rect2 from './math/Rect2';
38
38
  import { EditorLocalization } from './localization';
39
39
  import getLocalizationTable from './localizations/getLocalizationTable';
40
+ import IconProvider from './toolbar/IconProvider';
40
41
 
41
42
  type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
42
43
  type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
@@ -58,6 +59,8 @@ export interface EditorSettings {
58
59
  /** Minimum zoom fraction (e.g. 0.5 → 50% zoom). */
59
60
  minZoom: number,
60
61
  maxZoom: number,
62
+
63
+ iconProvider: IconProvider,
61
64
  }
62
65
 
63
66
  // { @inheritDoc Editor! }
@@ -101,22 +104,23 @@ export class Editor {
101
104
  * editor.dispatch(addElementCommand);
102
105
  * ```
103
106
  */
104
- public image: EditorImage;
107
+ public readonly image: EditorImage;
105
108
 
106
109
  /** Viewport for the exported/imported image. */
107
110
  private importExportViewport: Viewport;
108
111
 
109
112
  /** @internal */
110
- public localization: EditorLocalization;
113
+ public readonly localization: EditorLocalization;
111
114
 
112
- public viewport: Viewport;
113
- public toolController: ToolController;
115
+ public readonly icons: IconProvider;
116
+ public readonly viewport: Viewport;
117
+ public readonly toolController: ToolController;
114
118
 
115
119
  /**
116
120
  * Global event dispatcher/subscriber.
117
121
  * @see {@link types.EditorEventType}
118
122
  */
119
- public notifier: EditorNotifier;
123
+ public readonly notifier: EditorNotifier;
120
124
 
121
125
  private loadingWarning: HTMLElement;
122
126
  private accessibilityAnnounceArea: HTMLElement;
@@ -164,7 +168,9 @@ export class Editor {
164
168
  localization: this.localization,
165
169
  minZoom: settings.minZoom ?? 2e-10,
166
170
  maxZoom: settings.maxZoom ?? 1e12,
171
+ iconProvider: settings.iconProvider ?? new IconProvider(),
167
172
  };
173
+ this.icons = this.settings.iconProvider;
168
174
 
169
175
  this.container = document.createElement('div');
170
176
  this.renderingRegion = document.createElement('div');
@@ -54,6 +54,15 @@ export default class EditorImage {
54
54
  }
55
55
  }
56
56
 
57
+ /** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
58
+ public getAllElements() {
59
+ const leaves = this.root.getLeaves();
60
+ sortLeavesByZIndex(leaves);
61
+
62
+ return leaves.map(leaf => leaf.getContent()!);
63
+ }
64
+
65
+ /** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */
57
66
  public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] {
58
67
  const leaves = this.root.getLeavesIntersectingRegion(region);
59
68
  sortLeavesByZIndex(leaves);
@@ -0,0 +1,37 @@
1
+ import { Rect2, TextComponent, Vec2 } from './lib';
2
+ import SVGLoader from './SVGLoader';
3
+ import createEditor from './testing/createEditor';
4
+
5
+ describe('SVGLoader', () => {
6
+ it('should correctly load x/y-positioned text nodes', async () => {
7
+ const editor = createEditor();
8
+ await editor.loadFrom(SVGLoader.fromString(`
9
+ <svg>
10
+ <text>Testing...</text>
11
+ <text y=100>Test 2...</text>
12
+ <text x=100>Test 3...</text>
13
+ <text x=100 y=100>Test 3...</text>
14
+
15
+ <!-- Transform matrix: translate by (100,0) -->
16
+ <text style='transform: matrix(1,0,0,1,100,0);'>Test 3...</text>
17
+ </svg>
18
+ `, true));
19
+ const elems = editor.image
20
+ .getElementsIntersectingRegion(new Rect2(-1000, -1000, 10000, 10000))
21
+ .filter(elem => elem instanceof TextComponent);
22
+ expect(elems).toHaveLength(5);
23
+ const topLefts = elems.map(elem => elem.getBBox().topLeft);
24
+
25
+ // Top-left of Testing... should be (0, 0) ± 10 pixels (objects are aligned based on baseline)
26
+ expect(topLefts[0]).objEq(Vec2.of(0, 0), 10);
27
+
28
+ expect(topLefts[1].y - topLefts[0].y).toBe(100);
29
+ expect(topLefts[1].x - topLefts[0].x).toBe(0);
30
+
31
+ expect(topLefts[2].y - topLefts[0].y).toBe(0);
32
+ expect(topLefts[2].x - topLefts[0].x).toBe(100);
33
+
34
+ expect(topLefts[4].x - topLefts[0].x).toBe(100);
35
+ expect(topLefts[4].y - topLefts[0].y).toBe(0);
36
+ });
37
+ });
package/src/SVGLoader.ts CHANGED
@@ -124,7 +124,7 @@ export default class SVGLoader implements ImageLoader {
124
124
  );
125
125
  }
126
126
 
127
- if (supportedStyleAttrs) {
127
+ if (supportedStyleAttrs && node.style) {
128
128
  for (const attr of node.style) {
129
129
  if (attr === '' || !attr) {
130
130
  continue;
@@ -198,9 +198,9 @@ export default class SVGLoader implements ImageLoader {
198
198
 
199
199
  const elemX = elem.getAttribute('x');
200
200
  const elemY = elem.getAttribute('y');
201
- if (elemX && elemY) {
202
- const x = parseFloat(elemX);
203
- const y = parseFloat(elemY);
201
+ if (elemX || elemY) {
202
+ const x = parseFloat(elemX ?? '0');
203
+ const y = parseFloat(elemY ?? '0');
204
204
  if (!isNaN(x) && !isNaN(y)) {
205
205
  supportedAttrs?.push('x', 'y');
206
206
  transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
@@ -245,7 +245,7 @@ export default class SVGLoader implements ImageLoader {
245
245
  size: fontSize,
246
246
  fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
247
247
  renderingStyle: {
248
- fill: Color4.fromString(computedStyles.fill)
248
+ fill: Color4.fromString(computedStyles.fill || elem.style.fill || '#000')
249
249
  },
250
250
  };
251
251
 
@@ -406,7 +406,6 @@ export default class SVGLoader implements ImageLoader {
406
406
  this.onFinish?.();
407
407
  }
408
408
 
409
- // TODO: Handling unsafe data! Tripple-check that this is secure!
410
409
  // @param sanitize - if `true`, don't store unknown attributes.
411
410
  public static fromString(text: string, sanitize: boolean = false): SVGLoader {
412
411
  const sandbox = document.createElement('iframe');
@@ -89,6 +89,11 @@ export default abstract class AbstractComponent {
89
89
  return new AbstractComponent.TransformElementCommand(affineTransfm, this);
90
90
  }
91
91
 
92
+ // @returns true iff this component can be selected (e.g. by the selection tool.)
93
+ public isSelectable(): boolean {
94
+ return true;
95
+ }
96
+
92
97
  private static transformElementCommandId = 'transform-element';
93
98
 
94
99
  private static UnresolvedTransformElementCommand = class extends SerializableCommand {
@@ -43,6 +43,10 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
43
43
  protected applyTransformation(_affineTransfm: Mat33): void {
44
44
  }
45
45
 
46
+ public isSelectable() {
47
+ return false;
48
+ }
49
+
46
50
  protected createClone() {
47
51
  return new SVGGlobalAttributesObject(this.attrs);
48
52
  }
@@ -1,21 +1,8 @@
1
1
  import Color4 from '../Color4';
2
2
  import Mat33 from '../math/Mat33';
3
- import Rect2 from '../math/Rect2';
4
3
  import AbstractComponent from './AbstractComponent';
5
4
  import Text, { TextStyle } from './Text';
6
5
 
7
- const estimateTextBounds = (text: string, style: TextStyle): Rect2 => {
8
- const widthEst = text.length * style.size;
9
- const heightEst = style.size;
10
-
11
- // Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should
12
- // be above (0, 0).
13
- return new Rect2(0, -heightEst * 2/3, widthEst, heightEst);
14
- };
15
-
16
- // Don't use the default Canvas-based text bounding code. The canvas-based code may not work
17
- // with jsdom.
18
- AbstractComponent.registerComponent('text', (data: string) => Text.deserializeFromString(data, estimateTextBounds));
19
6
 
20
7
  describe('Text', () => {
21
8
  it('should be serializable', () => {
@@ -24,9 +11,7 @@ describe('Text', () => {
24
11
  fontFamily: 'serif',
25
12
  renderingStyle: { fill: Color4.black },
26
13
  };
27
- const text = new Text(
28
- [ 'Foo' ], Mat33.identity, style, estimateTextBounds
29
- );
14
+ const text = new Text([ 'Foo' ], Mat33.identity, style);
30
15
  const serialized = text.serialize();
31
16
  const deserialized = AbstractComponent.deserialize(serialized) as Text;
32
17
  expect(deserialized.getBBox()).objEq(text.getBBox());
@@ -14,8 +14,6 @@ export interface TextStyle {
14
14
  renderingStyle: RenderingStyle;
15
15
  }
16
16
 
17
- type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2;
18
-
19
17
  const componentTypeId = 'text';
20
18
  export default class Text extends AbstractComponent {
21
19
  protected contentBBox: Rect2;
@@ -24,10 +22,6 @@ export default class Text extends AbstractComponent {
24
22
  protected readonly textObjects: Array<string|Text>,
25
23
  private transform: Mat33,
26
24
  private readonly style: TextStyle,
27
-
28
- // If not given, an HtmlCanvasElement is used to determine text boundaries.
29
- // @internal
30
- private readonly getTextDimens: GetTextDimensCallback = Text.getTextDimens,
31
25
  ) {
32
26
  super(componentTypeId);
33
27
  this.recomputeBBox();
@@ -47,9 +41,25 @@ export default class Text extends AbstractComponent {
47
41
  ctx.textAlign = 'left';
48
42
  }
49
43
 
50
- private static textMeasuringCtx: CanvasRenderingContext2D;
44
+ private static textMeasuringCtx: CanvasRenderingContext2D|null = null;
45
+
46
+ // Roughly estimate the bounding box of `text`. Use if no CanvasRenderingContext2D is available.
47
+ private static estimateTextDimens(text: string, style: TextStyle): Rect2 {
48
+ const widthEst = text.length * style.size;
49
+ const heightEst = style.size;
50
+
51
+ // Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should
52
+ // be above (0, 0).
53
+ return new Rect2(0, -heightEst * 2/3, widthEst, heightEst);
54
+ }
55
+
56
+ // Returns the bounding box of `text`. This is approximate if no Canvas is available.
51
57
  private static getTextDimens(text: string, style: TextStyle): Rect2 {
52
- Text.textMeasuringCtx ??= document.createElement('canvas').getContext('2d')!;
58
+ Text.textMeasuringCtx ??= document.createElement('canvas').getContext('2d') ?? null;
59
+ if (!Text.textMeasuringCtx) {
60
+ return this.estimateTextDimens(text, style);
61
+ }
62
+
53
63
  const ctx = Text.textMeasuringCtx;
54
64
  Text.applyTextStyles(ctx, style);
55
65
 
@@ -63,7 +73,7 @@ export default class Text extends AbstractComponent {
63
73
 
64
74
  private computeBBoxOfPart(part: string|Text) {
65
75
  if (typeof part === 'string') {
66
- const textBBox = this.getTextDimens(part, this.style);
76
+ const textBBox = Text.getTextDimens(part, this.style);
67
77
  return textBBox.transformedBoundingBox(this.transform);
68
78
  } else {
69
79
  const bbox = part.contentBBox.transformedBoundingBox(this.transform);
@@ -178,7 +188,7 @@ export default class Text extends AbstractComponent {
178
188
  };
179
189
  }
180
190
 
181
- public static deserializeFromString(json: any, getTextDimens: GetTextDimensCallback = Text.getTextDimens): Text {
191
+ public static deserializeFromString(json: any): Text {
182
192
  const style: TextStyle = {
183
193
  renderingStyle: styleFromJSON(json.style.renderingStyle),
184
194
  size: json.style.size,
@@ -203,7 +213,7 @@ export default class Text extends AbstractComponent {
203
213
  const transformData = json.transform as Mat33Array;
204
214
  const transform = new Mat33(...transformData);
205
215
 
206
- return new Text(textObjects, transform, style, getTextDimens);
216
+ return new Text(textObjects, transform, style);
207
217
  }
208
218
  }
209
219
 
@@ -37,6 +37,10 @@ export default class UnknownSVGObject extends AbstractComponent {
37
37
  protected applyTransformation(_affineTransfm: Mat33): void {
38
38
  }
39
39
 
40
+ public isSelectable() {
41
+ return false;
42
+ }
43
+
40
44
  protected createClone(): AbstractComponent {
41
45
  return new UnknownSVGObject(this.svgObject.cloneNode(true) as SVGElement);
42
46
  }
@@ -415,8 +415,8 @@ export default class FreehandLineBuilder implements ComponentBuilder {
415
415
 
416
416
  let enteringVec = this.lastExitingVec;
417
417
  if (!enteringVec) {
418
- let sampleIdx = Math.ceil(this.buffer.length / 3);
419
- if (sampleIdx === 0) {
418
+ let sampleIdx = Math.ceil(this.buffer.length / 2);
419
+ if (sampleIdx === 0 || sampleIdx >= this.buffer.length) {
420
420
  sampleIdx = this.buffer.length - 1;
421
421
  }
422
422
 
@@ -1,3 +1,8 @@
1
1
  import loadExpectExtensions from './loadExpectExtensions';
2
2
  loadExpectExtensions();
3
- jest.useFakeTimers();
3
+ jest.useFakeTimers();
4
+
5
+ // jsdom doesn't support HTMLCanvasElement#getContext — it logs an error
6
+ // to the console. Make it return null so we can handle a non-existent Canvas
7
+ // at runtime (e.g. use something else, if available).
8
+ HTMLCanvasElement.prototype.getContext = () => null;
@@ -5,7 +5,6 @@ import { coloris, init as colorisInit } from '@melloware/coloris';
5
5
  import Color4 from '../Color4';
6
6
  import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
7
7
  import { ActionButtonIcon } from './types';
8
- import { makeRedoIcon, makeUndoIcon } from './icons';
9
8
  import SelectionTool from '../tools/SelectionTool/SelectionTool';
10
9
  import PanZoomTool from '../tools/PanZoom';
11
10
  import TextTool from '../tools/TextTool';
@@ -156,13 +155,13 @@ export default class HTMLToolbar {
156
155
 
157
156
  const undoButton = this.addActionButton({
158
157
  label: this.localizationTable.undo,
159
- icon: makeUndoIcon()
158
+ icon: this.editor.icons.makeUndoIcon()
160
159
  }, () => {
161
160
  this.editor.history.undo();
162
161
  }, undoRedoGroup);
163
162
  const redoButton = this.addActionButton({
164
163
  label: this.localizationTable.redo,
165
- icon: makeRedoIcon(),
164
+ icon: this.editor.icons.makeRedoIcon(),
166
165
  }, () => {
167
166
  this.editor.history.redo();
168
167
  }, undoRedoGroup);