js-draw 0.9.1 → 0.9.3

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 (90) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +742 -0
  2. package/CHANGELOG.md +7 -0
  3. package/build_tools/buildTranslationTemplate.ts +119 -0
  4. package/dist/build_tools/buildTranslationTemplate.d.ts +1 -0
  5. package/dist/build_tools/buildTranslationTemplate.js +93 -0
  6. package/dist/bundle.js +1 -1
  7. package/dist/src/Editor.d.ts +2 -3
  8. package/dist/src/Editor.js +7 -15
  9. package/dist/src/EditorImage.d.ts +1 -1
  10. package/dist/src/EventDispatcher.d.ts +1 -1
  11. package/dist/src/SVGLoader.d.ts +2 -2
  12. package/dist/src/SVGLoader.js +1 -1
  13. package/dist/src/UndoRedoHistory.d.ts +2 -2
  14. package/dist/src/Viewport.d.ts +1 -1
  15. package/dist/src/Viewport.js +1 -3
  16. package/dist/src/commands/Command.d.ts +2 -2
  17. package/dist/src/commands/SerializableCommand.d.ts +1 -1
  18. package/dist/src/commands/uniteCommands.js +19 -10
  19. package/dist/src/components/AbstractComponent.d.ts +3 -3
  20. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
  21. package/dist/src/components/{Text.d.ts → TextComponent.d.ts} +0 -0
  22. package/dist/src/components/{Text.js → TextComponent.js} +0 -0
  23. package/dist/src/components/builders/FreehandLineBuilder.d.ts +2 -0
  24. package/dist/src/components/builders/FreehandLineBuilder.js +10 -2
  25. package/dist/src/components/builders/types.d.ts +1 -1
  26. package/dist/src/components/lib.d.ts +1 -1
  27. package/dist/src/components/lib.js +1 -1
  28. package/dist/src/components/util/StrokeSmoother.d.ts +1 -1
  29. package/dist/src/math/Mat33.d.ts +1 -1
  30. package/dist/src/math/Path.d.ts +1 -1
  31. package/dist/src/math/Vec2.d.ts +2 -2
  32. package/dist/src/rendering/caching/testUtils.d.ts +1 -1
  33. package/dist/src/rendering/caching/types.d.ts +2 -2
  34. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -1
  35. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +1 -1
  36. package/dist/src/rendering/renderers/CanvasRenderer.js +1 -1
  37. package/dist/src/rendering/renderers/DummyRenderer.d.ts +1 -1
  38. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  39. package/dist/src/rendering/renderers/SVGRenderer.js +1 -1
  40. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +1 -1
  41. package/dist/src/rendering/renderers/TextOnlyRenderer.js +2 -2
  42. package/dist/src/toolbar/IconProvider.d.ts +2 -2
  43. package/dist/src/toolbar/localization.d.ts +1 -0
  44. package/dist/src/toolbar/localization.js +1 -0
  45. package/dist/src/toolbar/makeColorInput.d.ts +2 -2
  46. package/dist/src/toolbar/widgets/BaseWidget.d.ts +1 -1
  47. package/dist/src/toolbar/widgets/TextToolWidget.js +23 -2
  48. package/dist/src/tools/BaseTool.js +4 -4
  49. package/dist/src/tools/FindTool.d.ts +21 -0
  50. package/dist/src/tools/FindTool.js +113 -0
  51. package/dist/src/tools/PipetteTool.d.ts +1 -1
  52. package/dist/src/tools/SelectionTool/Selection.js +42 -11
  53. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -3
  54. package/dist/src/tools/SelectionTool/SelectionTool.js +1 -1
  55. package/dist/src/tools/TextTool.d.ts +1 -1
  56. package/dist/src/tools/TextTool.js +9 -3
  57. package/dist/src/tools/ToolController.js +2 -0
  58. package/dist/src/tools/ToolbarShortcutHandler.d.ts +1 -1
  59. package/dist/src/tools/localization.d.ts +6 -0
  60. package/dist/src/tools/localization.js +6 -0
  61. package/dist/src/types.d.ts +8 -8
  62. package/package.json +17 -16
  63. package/src/Editor.css +1 -0
  64. package/src/Editor.toSVG.test.ts +1 -1
  65. package/src/Editor.ts +12 -22
  66. package/src/SVGLoader.ts +1 -1
  67. package/src/Viewport.ts +1 -3
  68. package/src/commands/Command.ts +2 -2
  69. package/src/commands/uniteCommands.ts +21 -10
  70. package/src/components/{Text.test.ts → TextComponent.test.ts} +1 -1
  71. package/src/components/{Text.ts → TextComponent.ts} +0 -0
  72. package/src/components/builders/FreehandLineBuilder.ts +12 -2
  73. package/src/components/lib.ts +1 -1
  74. package/src/rendering/renderers/AbstractRenderer.ts +1 -1
  75. package/src/rendering/renderers/CanvasRenderer.ts +1 -1
  76. package/src/rendering/renderers/DummyRenderer.ts +1 -1
  77. package/src/rendering/renderers/SVGRenderer.ts +2 -2
  78. package/src/rendering/renderers/TextOnlyRenderer.ts +3 -3
  79. package/src/toolbar/IconProvider.ts +1 -1
  80. package/src/toolbar/localization.ts +2 -0
  81. package/src/toolbar/widgets/TextToolWidget.ts +29 -1
  82. package/src/tools/FindTool.css +7 -0
  83. package/src/tools/FindTool.ts +151 -0
  84. package/src/tools/PasteHandler.ts +1 -1
  85. package/src/tools/SelectionTool/Selection.ts +51 -10
  86. package/src/tools/SelectionTool/SelectionTool.ts +1 -1
  87. package/src/tools/TextTool.ts +11 -3
  88. package/src/tools/ToolController.ts +2 -0
  89. package/src/tools/localization.ts +14 -0
  90. package/.github/ISSUE_TEMPLATE/translation.md +0 -100
@@ -55,7 +55,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
55
55
  fill: Color4.transparent,
56
56
  stroke: {
57
57
  color: this.startPoint.color,
58
- width: this.averageWidth,
58
+ width: this.roundDistance(this.averageWidth),
59
59
  }
60
60
  };
61
61
  }
@@ -107,16 +107,26 @@ export default class FreehandLineBuilder implements ComponentBuilder {
107
107
  return this.previewStroke()!;
108
108
  }
109
109
 
110
- private roundPoint(point: Point2): Point2 {
110
+ private getMinFit(): number {
111
111
  let minFit = Math.min(this.minFitAllowed, this.averageWidth / 2);
112
112
 
113
113
  if (minFit < 1e-10) {
114
114
  minFit = this.minFitAllowed;
115
115
  }
116
+
117
+ return minFit;
118
+ }
116
119
 
120
+ private roundPoint(point: Point2): Point2 {
121
+ const minFit = this.getMinFit();
117
122
  return Viewport.roundPoint(point, minFit);
118
123
  }
119
124
 
125
+ private roundDistance(dist: number): number {
126
+ const minFit = this.getMinFit();
127
+ return Viewport.roundPoint(dist, minFit);
128
+ }
129
+
120
130
  private curveToPathCommands(curve: Curve|null): PathCommand[] {
121
131
  // Case where no points have been added
122
132
  if (!curve) {
@@ -6,7 +6,7 @@ export { default as StrokeSmoother, Curve as StrokeSmootherCurve } from './util/
6
6
  export * from './AbstractComponent';
7
7
  export { default as AbstractComponent } from './AbstractComponent';
8
8
  import Stroke from './Stroke';
9
- import TextComponent from './Text';
9
+ import TextComponent from './TextComponent';
10
10
  import ImageComponent from './ImageComponent';
11
11
 
12
12
  export {
@@ -1,5 +1,5 @@
1
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
- import { TextStyle } from '../../components/Text';
2
+ import { TextStyle } from '../../components/TextComponent';
3
3
  import Mat33 from '../../math/Mat33';
4
4
  import Path, { PathCommand, PathCommandType } from '../../math/Path';
5
5
  import Rect2 from '../../math/Rect2';
@@ -1,5 +1,5 @@
1
1
  import Color4 from '../../Color4';
2
- import TextComponent, { TextStyle } from '../../components/Text';
2
+ import TextComponent, { TextStyle } from '../../components/TextComponent';
3
3
  import Mat33 from '../../math/Mat33';
4
4
  import Rect2 from '../../math/Rect2';
5
5
  import { Point2, Vec2 } from '../../math/Vec2';
@@ -1,6 +1,6 @@
1
1
  // Renderer that outputs nothing. Useful for automated tests.
2
2
 
3
- import { TextStyle } from '../../components/Text';
3
+ import { TextStyle } from '../../components/TextComponent';
4
4
  import Mat33 from '../../math/Mat33';
5
5
  import Rect2 from '../../math/Rect2';
6
6
  import { Point2, Vec2 } from '../../math/Vec2';
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
- import { TextStyle } from '../../components/Text';
3
+ import { TextStyle } from '../../components/TextComponent';
4
4
  import Mat33 from '../../math/Mat33';
5
5
  import Path from '../../math/Path';
6
6
  import Rect2 from '../../math/Rect2';
@@ -102,7 +102,7 @@ export default class SVGRenderer extends AbstractRenderer {
102
102
 
103
103
  if (style.stroke) {
104
104
  pathElem.setAttribute('stroke', style.stroke.color.toHexString());
105
- pathElem.setAttribute('stroke-width', style.stroke.width.toString());
105
+ pathElem.setAttribute('stroke-width', toRoundedString(style.stroke.width));
106
106
  }
107
107
 
108
108
  this.elem.appendChild(pathElem);
@@ -1,4 +1,4 @@
1
- import { TextStyle } from '../../components/Text';
1
+ import { TextStyle } from '../../components/TextComponent';
2
2
  import Mat33 from '../../math/Mat33';
3
3
  import Rect2 from '../../math/Rect2';
4
4
  import { Vec2 } from '../../math/Vec2';
@@ -34,8 +34,8 @@ export default class TextOnlyRenderer extends AbstractRenderer {
34
34
  public getDescription(): string {
35
35
  return [
36
36
  this.localizationTable.pathNodeCount(this.pathCount),
37
- ...(this.textNodeCount > 0 ? this.localizationTable.textNodeCount(this.textNodeCount) : []),
38
- ...(this.imageNodeCount > 0 ? this.localizationTable.imageNodeCount(this.imageNodeCount) : []),
37
+ ...(this.textNodeCount > 0 ? [this.localizationTable.textNodeCount(this.textNodeCount)] : []),
38
+ ...(this.imageNodeCount > 0 ? [this.localizationTable.imageNodeCount(this.imageNodeCount)] : []),
39
39
  ...this.descriptionBuilder
40
40
  ].join('\n');
41
41
  }
@@ -1,6 +1,6 @@
1
1
  import Color4 from '../Color4';
2
2
  import { ComponentBuilderFactory } from '../components/builders/types';
3
- import { TextStyle } from '../components/Text';
3
+ import { TextStyle } from '../components/TextComponent';
4
4
  import EventDispatcher from '../EventDispatcher';
5
5
  import { Vec2 } from '../math/Vec2';
6
6
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
@@ -2,6 +2,7 @@
2
2
 
3
3
  export interface ToolbarLocalization {
4
4
  fontLabel: string;
5
+ textSize: string;
5
6
  touchPanning: string;
6
7
  lockRotation: string;
7
8
  outlinedRectanglePen: string;
@@ -44,6 +45,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
44
45
  thicknessLabel: 'Thickness: ',
45
46
  colorLabel: 'Color: ',
46
47
  fontLabel: 'Font: ',
48
+ textSize: 'Size: ',
47
49
  resizeImageToSelection: 'Resize image to selection',
48
50
  deleteSelection: 'Delete selection',
49
51
  duplicateSelection: 'Duplicate selection',
@@ -34,10 +34,14 @@ export default class TextToolWidget extends BaseToolWidget {
34
34
  protected fillDropdown(dropdown: HTMLElement): boolean {
35
35
  const fontRow = document.createElement('div');
36
36
  const colorRow = document.createElement('div');
37
+ const sizeRow = document.createElement('div');
37
38
 
38
39
  const fontInput = document.createElement('select');
39
40
  const fontLabel = document.createElement('label');
40
41
 
42
+ const sizeInput = document.createElement('input');
43
+ const sizeLabel = document.createElement('label');
44
+
41
45
  const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
42
46
  this.tool.setColor(color);
43
47
  });
@@ -52,12 +56,20 @@ export default class TextToolWidget extends BaseToolWidget {
52
56
  fontsInInput.add(fontName);
53
57
  };
54
58
 
59
+ sizeInput.setAttribute('type', 'number');
60
+ sizeInput.min = '1';
61
+ sizeInput.max = '128';
62
+
55
63
  fontLabel.innerText = this.localizationTable.fontLabel;
56
64
  colorLabel.innerText = this.localizationTable.colorLabel;
65
+ sizeLabel.innerText = this.localizationTable.textSize;
57
66
 
58
67
  colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
59
68
  colorLabel.setAttribute('for', colorInput.id);
60
69
 
70
+ sizeInput.id = `${toolbarCSSPrefix}-text-size-input-${TextToolWidget.idCounter++}`;
71
+ sizeLabel.setAttribute('for', sizeInput.id);
72
+
61
73
  addFontToInput('monospace');
62
74
  addFontToInput('serif');
63
75
  addFontToInput('sans-serif');
@@ -68,12 +80,22 @@ export default class TextToolWidget extends BaseToolWidget {
68
80
  this.tool.setFontFamily(fontInput.value);
69
81
  };
70
82
 
83
+ sizeInput.onchange = () => {
84
+ const size = parseInt(sizeInput.value);
85
+ if (!isNaN(size) && size > 0) {
86
+ this.tool.setFontSize(size);
87
+ }
88
+ };
89
+
71
90
  colorRow.appendChild(colorLabel);
72
91
  colorRow.appendChild(colorInputContainer);
73
92
 
74
93
  fontRow.appendChild(fontLabel);
75
94
  fontRow.appendChild(fontInput);
76
95
 
96
+ sizeRow.appendChild(sizeLabel);
97
+ sizeRow.appendChild(sizeInput);
98
+
77
99
  this.updateDropdownInputs = () => {
78
100
  const style = this.tool.getTextStyle();
79
101
  setColorInputValue(style.renderingStyle.fill);
@@ -82,10 +104,11 @@ export default class TextToolWidget extends BaseToolWidget {
82
104
  addFontToInput(style.fontFamily);
83
105
  }
84
106
  fontInput.value = style.fontFamily;
107
+ sizeInput.value = `${style.size}`;
85
108
  };
86
109
  this.updateDropdownInputs();
87
110
 
88
- dropdown.replaceChildren(colorRow, fontRow);
111
+ dropdown.replaceChildren(colorRow, sizeRow, fontRow);
89
112
  return true;
90
113
  }
91
114
 
@@ -96,6 +119,7 @@ export default class TextToolWidget extends BaseToolWidget {
96
119
  ...super.serializeState(),
97
120
 
98
121
  fontFamily: textStyle.fontFamily,
122
+ textSize: textStyle.size,
99
123
  color: textStyle.renderingStyle.fill.toHexString(),
100
124
  };
101
125
  }
@@ -109,6 +133,10 @@ export default class TextToolWidget extends BaseToolWidget {
109
133
  this.tool.setColor(Color4.fromHex(state.color));
110
134
  }
111
135
 
136
+ if (state.textSize && typeof(state.textSize) === 'number') {
137
+ this.tool.setFontSize(state.textSize);
138
+ }
139
+
112
140
  super.deserializeFrom(state);
113
141
  }
114
142
  }
@@ -0,0 +1,7 @@
1
+
2
+ .find-tool-overlay {
3
+ /* Show at the bottom of the screen. */
4
+ order: -1;
5
+
6
+ position: absolute;
7
+ }
@@ -0,0 +1,151 @@
1
+ // Displays a find dialog that allows the user to search for and focus text.
2
+ //
3
+ // @packageDocumentation
4
+
5
+ import Editor from '../Editor';
6
+ import TextComponent from '../components/TextComponent';
7
+ import Rect2 from '../math/Rect2';
8
+ import { KeyPressEvent } from '../types';
9
+ import BaseTool from './BaseTool';
10
+
11
+ export const cssPrefix = 'find-tool';
12
+
13
+ export default class FindTool extends BaseTool {
14
+ private overlay: HTMLElement;
15
+ private searchInput: HTMLInputElement;
16
+ private currentMatchIdx: number = 0;
17
+
18
+ public constructor(private editor: Editor) {
19
+ super(editor.notifier, editor.localization.findLabel);
20
+
21
+ this.overlay = document.createElement('div');
22
+ this.fillOverlay();
23
+ editor.createHTMLOverlay(this.overlay);
24
+
25
+ this.overlay.style.display = 'none';
26
+ this.overlay.classList.add(`${cssPrefix}-overlay`);
27
+ }
28
+
29
+ private getMatches(searchFor: string): Rect2[] {
30
+ searchFor = searchFor.toLocaleLowerCase();
31
+ const allTextComponents = this.editor.image.getAllElements()
32
+ .filter(
33
+ elem => elem instanceof TextComponent
34
+ ) as TextComponent[];
35
+
36
+ const matches = allTextComponents.filter(
37
+ text => text.getText().toLocaleLowerCase().indexOf(searchFor) !== -1
38
+ );
39
+
40
+ return matches.map(match => match.getBBox());
41
+ }
42
+
43
+ private focusCurrentMatch() {
44
+ const matches = this.getMatches(this.searchInput.value);
45
+ let matchIdx = this.currentMatchIdx % matches.length;
46
+
47
+ if (matchIdx < 0) {
48
+ matchIdx = matches.length + matchIdx;
49
+ }
50
+
51
+ if (matchIdx < matches.length) {
52
+ this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true));
53
+ this.editor.announceForAccessibility(
54
+ this.editor.localization.focusedFoundText(matchIdx + 1, matches.length)
55
+ );
56
+ }
57
+ }
58
+
59
+ private toNextMatch() {
60
+ this.currentMatchIdx ++;
61
+ this.focusCurrentMatch();
62
+ }
63
+
64
+ private toPrevMatch() {
65
+ this.currentMatchIdx --;
66
+ this.focusCurrentMatch();
67
+ }
68
+
69
+ private fillOverlay() {
70
+ const label = document.createElement('label');
71
+ this.searchInput = document.createElement('input');
72
+ const nextBtn = document.createElement('button');
73
+ const closeBtn = document.createElement('button');
74
+
75
+ // Math.random() ensures that the ID is unique (to allow us to refer to it
76
+ // with an htmlFor).
77
+ this.searchInput.setAttribute('id', `${cssPrefix}-searchInput-${Math.random()}`);
78
+ label.htmlFor = this.searchInput.getAttribute('id')!;
79
+
80
+ label.innerText = this.editor.localization.findLabel;
81
+ nextBtn.innerText = this.editor.localization.toNextMatch;
82
+ closeBtn.innerText = this.editor.localization.closeFindDialog;
83
+
84
+ this.searchInput.onkeydown = (ev: KeyboardEvent) => {
85
+ if (ev.key === 'Enter') {
86
+ if (ev.shiftKey) {
87
+ this.toPrevMatch();
88
+ } else {
89
+ this.toNextMatch();
90
+ }
91
+ }
92
+ else if (ev.key === 'Escape') {
93
+ this.setVisible(false);
94
+ }
95
+ else if (ev.key === 'f' && ev.ctrlKey) {
96
+ ev.preventDefault();
97
+ this.toggleVisible();
98
+ }
99
+ };
100
+
101
+ nextBtn.onclick = () => {
102
+ this.toNextMatch();
103
+ };
104
+
105
+ closeBtn.onclick = () => {
106
+ this.setVisible(false);
107
+ };
108
+
109
+ this.overlay.replaceChildren(label, this.searchInput, nextBtn, closeBtn);
110
+ }
111
+
112
+ private isVisible() {
113
+ return this.overlay.style.display !== 'none';
114
+ }
115
+
116
+ private setVisible(visible: boolean) {
117
+ if (visible !== this.isVisible()) {
118
+ this.overlay.style.display = visible ? 'block' : 'none';
119
+
120
+ if (visible) {
121
+ this.searchInput.focus();
122
+ this.editor.announceForAccessibility(this.editor.localization.findDialogShown);
123
+ } else {
124
+ this.editor.focus();
125
+ this.editor.announceForAccessibility(this.editor.localization.findDialogHidden);
126
+ }
127
+ }
128
+ }
129
+
130
+ private toggleVisible() {
131
+ this.setVisible(!this.isVisible());
132
+ }
133
+
134
+ public onKeyPress(event: KeyPressEvent): boolean {
135
+ if (event.ctrlKey && event.key === 'f') {
136
+ this.toggleVisible();
137
+
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ public setEnabled(enabled: boolean) {
145
+ super.setEnabled(enabled);
146
+
147
+ if (enabled) {
148
+ this.setVisible(false);
149
+ }
150
+ }
151
+ }
@@ -14,7 +14,7 @@ import EditorImage from '../EditorImage';
14
14
  import SelectionTool from './SelectionTool/SelectionTool';
15
15
  import TextTool from './TextTool';
16
16
  import Color4 from '../Color4';
17
- import { TextStyle } from '../components/Text';
17
+ import { TextStyle } from '../components/TextComponent';
18
18
  import ImageComponent from '../components/ImageComponent';
19
19
  import Viewport from '../Viewport';
20
20
 
@@ -170,7 +170,7 @@ export default class Selection {
170
170
  });
171
171
 
172
172
  const fullTransform = this.transform;
173
- const currentTransfmCommands = this.computeTransformCommands();
173
+ const selectedElems = this.selectedElems;
174
174
 
175
175
  // Reset for the next drag
176
176
  this.transformCommands = [];
@@ -179,43 +179,84 @@ export default class Selection {
179
179
 
180
180
  // Make the commands undo-able
181
181
  this.editor.dispatch(new Selection.ApplyTransformationCommand(
182
- this, currentTransfmCommands, fullTransform
182
+ this, selectedElems, fullTransform
183
183
  ));
184
184
  }
185
185
 
186
186
  static {
187
- SerializableCommand.register('selection-tool-transform', (json: any, editor) => {
187
+ SerializableCommand.register('selection-tool-transform', (json: any, _editor) => {
188
188
  // The selection box is lost when serializing/deserializing. No need to store box rotation
189
189
  const fullTransform: Mat33 = new Mat33(...(json.transform as Mat33Array));
190
- const commands = (json.commands as any[]).map(data => SerializableCommand.deserialize(data, editor));
190
+ const elemIds: string[] = (json.elems as any[] ?? []);
191
191
 
192
- return new this.ApplyTransformationCommand(null, commands, fullTransform);
192
+ return new this.ApplyTransformationCommand(null, elemIds, fullTransform);
193
193
  });
194
194
  }
195
195
 
196
196
  private static ApplyTransformationCommand = class extends SerializableCommand {
197
+ private transformCommands: Command[];
198
+ private selectedElemIds: string[];
199
+
197
200
  public constructor(
198
201
  private selection: Selection|null,
199
- private currentTransfmCommands: SerializableCommand[],
202
+
203
+ // If a `string[]`, selectedElems is a list of element IDs.
204
+ selectedElems: AbstractComponent[]|string[],
205
+
206
+ // Full transformation used to transform elements.
200
207
  private fullTransform: Mat33,
201
208
  ) {
202
209
  super('selection-tool-transform');
210
+
211
+ const isIDList = (arr: AbstractComponent[]|string[]): arr is string[] => {
212
+ return typeof arr[0] === 'string';
213
+ };
214
+
215
+ // If a list of element IDs,
216
+ if (isIDList(selectedElems)) {
217
+ this.selectedElemIds = selectedElems as string[];
218
+ } else {
219
+ this.selectedElemIds = (selectedElems as AbstractComponent[]).map(elem => elem.getId());
220
+ this.transformCommands = selectedElems.map(elem => {
221
+ return elem.transformBy(this.fullTransform);
222
+ });
223
+ }
224
+ }
225
+
226
+ private resolveToElems(editor: Editor) {
227
+ if (this.transformCommands) {
228
+ return;
229
+ }
230
+
231
+ this.transformCommands = this.selectedElemIds.map(id => {
232
+ const elem = editor.image.lookupElement(id);
233
+
234
+ if (!elem) {
235
+ throw new Error(`Unable to find element with ID, ${id}.`);
236
+ }
237
+
238
+ return elem.transformBy(this.fullTransform);
239
+ });
203
240
  }
204
241
 
205
242
  public async apply(editor: Editor) {
243
+ this.resolveToElems(editor);
244
+
206
245
  this.selection?.setTransform(this.fullTransform, false);
207
246
  this.selection?.updateUI();
208
- await editor.asyncApplyCommands(this.currentTransfmCommands, updateChunkSize);
247
+ await editor.asyncApplyCommands(this.transformCommands, updateChunkSize);
209
248
  this.selection?.setTransform(Mat33.identity, false);
210
249
  this.selection?.recomputeRegion();
211
250
  this.selection?.updateUI();
212
251
  }
213
252
 
214
253
  public async unapply(editor: Editor) {
254
+ this.resolveToElems(editor);
255
+
215
256
  this.selection?.setTransform(this.fullTransform.inverse(), false);
216
257
  this.selection?.updateUI();
217
258
 
218
- await editor.asyncUnapplyCommands(this.currentTransfmCommands, updateChunkSize);
259
+ await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize);
219
260
  this.selection?.setTransform(Mat33.identity);
220
261
  this.selection?.recomputeRegion();
221
262
  this.selection?.updateUI();
@@ -223,13 +264,13 @@ export default class Selection {
223
264
 
224
265
  protected serializeToJSON() {
225
266
  return {
226
- commands: this.currentTransfmCommands.map(command => command.serialize()),
267
+ elems: this.selectedElemIds,
227
268
  transform: this.fullTransform.toArray(),
228
269
  };
229
270
  }
230
271
 
231
272
  public description(_editor: Editor, localizationTable: EditorLocalization) {
232
- return localizationTable.transformedElements(this.currentTransfmCommands.length);
273
+ return localizationTable.transformedElements(this.selectedElemIds.length);
233
274
  }
234
275
  };
235
276
 
@@ -12,7 +12,7 @@ import Viewport from '../../Viewport';
12
12
  import BaseTool from '../BaseTool';
13
13
  import SVGRenderer from '../../rendering/renderers/SVGRenderer';
14
14
  import Selection from './Selection';
15
- import TextComponent from '../../components/Text';
15
+ import TextComponent from '../../components/TextComponent';
16
16
 
17
17
  export const cssPrefix = 'selection-tool-';
18
18
 
@@ -1,5 +1,5 @@
1
1
  import Color4 from '../Color4';
2
- import TextComponent, { TextStyle } from '../components/Text';
2
+ import TextComponent, { TextStyle } from '../components/TextComponent';
3
3
  import Editor from '../Editor';
4
4
  import EditorImage from '../EditorImage';
5
5
  import Rect2 from '../math/Rect2';
@@ -135,8 +135,11 @@ export default class TextTool extends BaseTool {
135
135
  this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
136
136
  this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
137
137
 
138
+ // Get the ascent based on the font, using a character that is tall in most fonts.
139
+ const tallCharacter = '⎢';
140
+ const ascent = this.getTextAscent(tallCharacter, this.textStyle);
141
+
138
142
  const rotation = this.textRotation + viewport.getRotationAngle();
139
- const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
140
143
  const scale: Mat33 = this.getTextScaleMatrix();
141
144
  this.textInputElem.style.transform = `${scale.toCSSMatrix()} rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
142
145
  this.textInputElem.style.transformOrigin = 'top left';
@@ -208,7 +211,12 @@ export default class TextTool extends BaseTool {
208
211
  const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
209
212
  const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
210
213
  const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
211
- const targetTextNodes = targetNodes.filter(node => node instanceof TextComponent) as TextComponent[];
214
+ let targetTextNodes = targetNodes.filter(node => node instanceof TextComponent) as TextComponent[];
215
+
216
+ // Don't try to edit text nodes that contain the viewport (this allows us
217
+ // to zoom in on text nodes and add text on top of them.)
218
+ const visibleRect = this.editor.viewport.visibleRect;
219
+ targetTextNodes = targetTextNodes.filter(node => !node.getBBox().containsRect(visibleRect));
212
220
 
213
221
  // End any TextNodes we're currently editing.
214
222
  this.flushInput();
@@ -15,6 +15,7 @@ import ToolSwitcherShortcut from './ToolSwitcherShortcut';
15
15
  import PasteHandler from './PasteHandler';
16
16
  import ToolbarShortcutHandler from './ToolbarShortcutHandler';
17
17
  import { makePressureSensitiveFreehandLineBuilder } from '../components/builders/PressureSensitiveFreehandLineBuilder';
18
+ import FindTool from './FindTool';
18
19
 
19
20
  export default class ToolController {
20
21
  private tools: BaseTool[];
@@ -50,6 +51,7 @@ export default class ToolController {
50
51
  new UndoRedoShortcut(editor),
51
52
  new ToolbarShortcutHandler(editor),
52
53
  new ToolSwitcherShortcut(editor),
54
+ new FindTool(editor),
53
55
  new PasteHandler(editor),
54
56
  ];
55
57
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
@@ -15,6 +15,13 @@ export interface ToolLocalization {
15
15
  changeTool: string;
16
16
  pasteHandler: string;
17
17
 
18
+ findLabel: string;
19
+ toNextMatch: string;
20
+ closeFindDialog: string;
21
+ findDialogShown: string;
22
+ findDialogHidden: string;
23
+ focusedFoundText: (currentMatchNumber: number, totalMatches: number)=> string;
24
+
18
25
  anyDevicePanning: string;
19
26
 
20
27
  copied: (count: number, description: string) => string;
@@ -40,6 +47,13 @@ export const defaultToolLocalization: ToolLocalization = {
40
47
  changeTool: 'Change tool',
41
48
  pasteHandler: 'Copy paste handler',
42
49
 
50
+ findLabel: 'Find',
51
+ toNextMatch: 'Next',
52
+ closeFindDialog: 'Close',
53
+ findDialogShown: 'Find dialog shown',
54
+ findDialogHidden: 'Find dialog hidden',
55
+ focusedFoundText: (matchIdx: number, totalMatches: number) => `Viewing match ${matchIdx} of ${totalMatches}`,
56
+
43
57
  anyDevicePanning: 'Any device panning',
44
58
 
45
59
  copied: (count: number, description: string) => `Copied ${count} ${description}`,