js-draw 0.1.9 → 0.1.12

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 (193) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +15 -3
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +7 -2
  5. package/dist/src/Editor.js +74 -25
  6. package/dist/src/EditorImage.d.ts +1 -1
  7. package/dist/src/EditorImage.js +2 -2
  8. package/dist/src/Pointer.d.ts +1 -1
  9. package/dist/src/Pointer.js +1 -1
  10. package/dist/src/SVGLoader.d.ts +1 -1
  11. package/dist/src/SVGLoader.js +14 -6
  12. package/dist/src/UndoRedoHistory.js +3 -0
  13. package/dist/src/Viewport.d.ts +8 -25
  14. package/dist/src/Viewport.js +17 -10
  15. package/dist/src/bundle/bundled.d.ts +2 -1
  16. package/dist/src/bundle/bundled.js +2 -1
  17. package/dist/src/commands/Command.d.ts +2 -2
  18. package/dist/src/commands/Command.js +4 -4
  19. package/dist/src/commands/Duplicate.d.ts +1 -1
  20. package/dist/src/commands/Duplicate.js +1 -1
  21. package/dist/src/commands/Erase.d.ts +1 -1
  22. package/dist/src/commands/Erase.js +1 -1
  23. package/dist/src/commands/localization.d.ts +1 -1
  24. package/dist/src/components/AbstractComponent.d.ts +3 -3
  25. package/dist/src/components/AbstractComponent.js +2 -2
  26. package/dist/src/components/SVGGlobalAttributesObject.d.ts +3 -3
  27. package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
  28. package/dist/src/components/Stroke.d.ts +4 -4
  29. package/dist/src/components/Stroke.js +2 -2
  30. package/dist/src/components/Text.d.ts +3 -3
  31. package/dist/src/components/Text.js +3 -3
  32. package/dist/src/components/UnknownSVGObject.d.ts +3 -3
  33. package/dist/src/components/UnknownSVGObject.js +1 -1
  34. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  35. package/dist/src/components/builders/ArrowBuilder.js +1 -1
  36. package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
  37. package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
  38. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  39. package/dist/src/components/builders/LineBuilder.js +1 -1
  40. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  41. package/dist/src/components/builders/RectangleBuilder.js +3 -3
  42. package/dist/src/components/builders/types.d.ts +1 -1
  43. package/dist/src/localization.d.ts +1 -0
  44. package/dist/src/localization.js +5 -1
  45. package/dist/src/localizations/en.d.ts +3 -0
  46. package/dist/src/localizations/en.js +4 -0
  47. package/dist/src/localizations/es.d.ts +3 -0
  48. package/dist/src/localizations/es.js +18 -0
  49. package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
  50. package/dist/src/localizations/getLocalizationTable.js +43 -0
  51. package/dist/src/{geometry → math}/LineSegment2.d.ts +1 -0
  52. package/dist/src/{geometry → math}/LineSegment2.js +16 -0
  53. package/dist/src/{geometry → math}/Mat33.d.ts +0 -0
  54. package/dist/src/{geometry → math}/Mat33.js +0 -0
  55. package/dist/src/{geometry → math}/Path.d.ts +2 -1
  56. package/dist/src/{geometry → math}/Path.js +58 -51
  57. package/dist/src/{geometry → math}/Rect2.d.ts +1 -0
  58. package/dist/src/{geometry → math}/Rect2.js +16 -0
  59. package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
  60. package/dist/src/{geometry → math}/Vec2.js +0 -0
  61. package/dist/src/{geometry → math}/Vec3.d.ts +1 -1
  62. package/dist/src/{geometry → math}/Vec3.js +1 -1
  63. package/dist/src/math/rounding.d.ts +3 -0
  64. package/dist/src/math/rounding.js +120 -0
  65. package/dist/src/rendering/Display.d.ts +3 -1
  66. package/dist/src/rendering/Display.js +16 -10
  67. package/dist/src/rendering/caching/CacheRecord.d.ts +2 -2
  68. package/dist/src/rendering/caching/CacheRecord.js +1 -1
  69. package/dist/src/rendering/caching/CacheRecordManager.d.ts +1 -1
  70. package/dist/src/rendering/caching/RenderingCache.js +1 -1
  71. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
  72. package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
  73. package/dist/src/rendering/caching/testUtils.js +1 -1
  74. package/dist/src/rendering/caching/types.d.ts +1 -1
  75. package/dist/src/rendering/localization.d.ts +2 -0
  76. package/dist/src/rendering/localization.js +2 -0
  77. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
  78. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
  79. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
  80. package/dist/src/rendering/renderers/CanvasRenderer.js +1 -1
  81. package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
  82. package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
  83. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
  84. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  85. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
  86. package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
  87. package/dist/src/toolbar/HTMLToolbar.js +5 -7
  88. package/dist/src/toolbar/icons.d.ts +3 -0
  89. package/dist/src/toolbar/icons.js +152 -142
  90. package/dist/src/toolbar/localization.d.ts +4 -1
  91. package/dist/src/toolbar/localization.js +4 -1
  92. package/dist/src/toolbar/makeColorInput.js +2 -1
  93. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
  94. package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
  95. package/dist/src/toolbar/widgets/BaseWidget.js +31 -0
  96. package/dist/src/toolbar/widgets/HandToolWidget.js +10 -3
  97. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
  98. package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
  99. package/dist/src/tools/BaseTool.d.ts +2 -1
  100. package/dist/src/tools/BaseTool.js +3 -0
  101. package/dist/src/tools/Eraser.js +1 -1
  102. package/dist/src/tools/PanZoom.d.ts +3 -2
  103. package/dist/src/tools/PanZoom.js +32 -16
  104. package/dist/src/tools/SelectionTool.d.ts +11 -4
  105. package/dist/src/tools/SelectionTool.js +135 -23
  106. package/dist/src/tools/TextTool.js +1 -1
  107. package/dist/src/tools/ToolController.js +6 -2
  108. package/dist/src/tools/localization.d.ts +1 -0
  109. package/dist/src/tools/localization.js +1 -0
  110. package/dist/src/types.d.ts +13 -6
  111. package/dist/src/types.js +1 -0
  112. package/package.json +9 -1
  113. package/src/Editor.ts +86 -24
  114. package/src/EditorImage.test.ts +2 -4
  115. package/src/EditorImage.ts +2 -2
  116. package/src/Pointer.ts +1 -1
  117. package/src/SVGLoader.ts +14 -6
  118. package/src/UndoRedoHistory.ts +4 -0
  119. package/src/Viewport.ts +21 -17
  120. package/src/bundle/bundled.ts +2 -1
  121. package/src/commands/Command.ts +5 -5
  122. package/src/commands/Duplicate.ts +1 -1
  123. package/src/commands/Erase.ts +1 -1
  124. package/src/commands/localization.ts +1 -1
  125. package/src/components/AbstractComponent.ts +4 -4
  126. package/src/components/SVGGlobalAttributesObject.ts +3 -3
  127. package/src/components/Stroke.test.ts +3 -5
  128. package/src/components/Stroke.ts +4 -4
  129. package/src/components/Text.test.ts +2 -2
  130. package/src/components/Text.ts +3 -3
  131. package/src/components/UnknownSVGObject.ts +3 -3
  132. package/src/components/builders/ArrowBuilder.ts +2 -2
  133. package/src/components/builders/FreehandLineBuilder.ts +190 -80
  134. package/src/components/builders/LineBuilder.ts +2 -2
  135. package/src/components/builders/RectangleBuilder.ts +3 -3
  136. package/src/components/builders/types.ts +1 -1
  137. package/src/localization.ts +6 -0
  138. package/src/localizations/en.ts +8 -0
  139. package/src/localizations/es.ts +63 -0
  140. package/src/localizations/getLocalizationTable.test.ts +27 -0
  141. package/src/localizations/getLocalizationTable.ts +53 -0
  142. package/src/{geometry → math}/LineSegment2.test.ts +15 -0
  143. package/src/{geometry → math}/LineSegment2.ts +20 -0
  144. package/src/{geometry → math}/Mat33.test.ts +0 -0
  145. package/src/{geometry → math}/Mat33.ts +0 -0
  146. package/src/{geometry → math}/Path.fromString.test.ts +0 -0
  147. package/src/{geometry → math}/Path.test.ts +0 -0
  148. package/src/{geometry → math}/Path.toString.test.ts +11 -2
  149. package/src/{geometry → math}/Path.ts +60 -57
  150. package/src/{geometry → math}/Rect2.test.ts +20 -7
  151. package/src/{geometry → math}/Rect2.ts +19 -1
  152. package/src/{geometry → math}/Vec2.test.ts +0 -0
  153. package/src/{geometry → math}/Vec2.ts +0 -0
  154. package/src/{geometry → math}/Vec3.test.ts +0 -0
  155. package/src/{geometry → math}/Vec3.ts +2 -2
  156. package/src/math/rounding.test.ts +40 -0
  157. package/src/math/rounding.ts +145 -0
  158. package/src/rendering/Display.ts +18 -10
  159. package/src/rendering/caching/CacheRecord.test.ts +2 -2
  160. package/src/rendering/caching/CacheRecord.ts +2 -2
  161. package/src/rendering/caching/CacheRecordManager.ts +1 -1
  162. package/src/rendering/caching/RenderingCache.test.ts +3 -3
  163. package/src/rendering/caching/RenderingCache.ts +1 -1
  164. package/src/rendering/caching/RenderingCacheNode.ts +23 -7
  165. package/src/rendering/caching/testUtils.ts +1 -1
  166. package/src/rendering/caching/types.ts +1 -1
  167. package/src/rendering/localization.ts +4 -0
  168. package/src/rendering/renderers/AbstractRenderer.ts +4 -4
  169. package/src/rendering/renderers/CanvasRenderer.ts +4 -4
  170. package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
  171. package/src/rendering/renderers/DummyRenderer.ts +4 -4
  172. package/src/rendering/renderers/SVGRenderer.ts +10 -4
  173. package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
  174. package/src/toolbar/HTMLToolbar.ts +5 -8
  175. package/src/toolbar/icons.ts +167 -147
  176. package/src/toolbar/localization.ts +8 -2
  177. package/src/toolbar/makeColorInput.ts +2 -1
  178. package/src/toolbar/toolbar.css +7 -3
  179. package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
  180. package/src/toolbar/widgets/BaseWidget.ts +36 -0
  181. package/src/toolbar/widgets/HandToolWidget.ts +14 -3
  182. package/src/toolbar/widgets/SelectionWidget.ts +46 -41
  183. package/src/tools/BaseTool.ts +5 -1
  184. package/src/tools/Eraser.ts +2 -2
  185. package/src/tools/PanZoom.ts +39 -18
  186. package/src/tools/SelectionTool.test.ts +26 -5
  187. package/src/tools/SelectionTool.ts +162 -27
  188. package/src/tools/TextTool.ts +2 -2
  189. package/src/tools/ToolController.ts +6 -2
  190. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  191. package/src/tools/localization.ts +2 -0
  192. package/src/types.ts +14 -5
  193. package/dist-test/test-dist-bundle.html +0 -42
@@ -19,10 +19,13 @@ export interface ToolbarLocalization {
19
19
  resizeImageToSelection: string;
20
20
  deleteSelection: string;
21
21
  duplicateSelection: string;
22
- pickColorFronScreen: string;
22
+ pickColorFromScreen: string;
23
+ clickToPickColorAnnouncement: string;
23
24
  undo: string;
24
25
  redo: string;
25
26
  zoom: string;
27
+ resetView: string;
28
+ selectionToolKeyboardShortcuts: string;
26
29
 
27
30
  dropdownShown: (toolName: string)=> string;
28
31
  dropdownHidden: (toolName: string)=> string;
@@ -36,6 +39,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
36
39
  select: 'Select',
37
40
  handTool: 'Pan',
38
41
  zoom: 'Zoom',
42
+ resetView: 'Reset view',
39
43
  thicknessLabel: 'Thickness: ',
40
44
  colorLabel: 'Color: ',
41
45
  fontLabel: 'Font: ',
@@ -45,7 +49,9 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
45
49
  undo: 'Undo',
46
50
  redo: 'Redo',
47
51
  selectObjectType: 'Object type: ',
48
- pickColorFronScreen: 'Pick color from screen',
52
+ pickColorFromScreen: 'Pick color from screen',
53
+ clickToPickColorAnnouncement: 'Click on the screen to pick a color',
54
+ selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
49
55
 
50
56
  touchPanning: 'Touchscreen panning',
51
57
  anyDevicePanning: 'Any device panning',
@@ -69,7 +69,7 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
69
69
  const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: OnColorChangeListener) => {
70
70
  const pipetteButton = document.createElement('button');
71
71
  pipetteButton.classList.add('pipetteButton');
72
- pipetteButton.title = editor.localization.pickColorFronScreen;
72
+ pipetteButton.title = editor.localization.pickColorFromScreen;
73
73
  pipetteButton.setAttribute('alt', pipetteButton.title);
74
74
 
75
75
  const updatePipetteIcon = (color?: Color4) => {
@@ -111,6 +111,7 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
111
111
  );
112
112
  if (pipetteTool) {
113
113
  pipetteButton.classList.add('active');
114
+ editor.announceForAccessibility(editor.localization.clickToPickColorAnnouncement);
114
115
  }
115
116
  };
116
117
 
@@ -1,5 +1,7 @@
1
1
  .toolbar-root {
2
2
  background-color: var(--primary-background-color);
3
+ --icon-color: var(--primary-foreground-color);
4
+
3
5
 
4
6
  border: 1px solid var(--secondary-background-color);
5
7
  border-radius: 2px;
@@ -38,6 +40,7 @@
38
40
  text-align: center;
39
41
  border-radius: 6px;
40
42
 
43
+ --icon-color: var(--primary-foreground-color);
41
44
  background-color: var(--primary-background-color);
42
45
  color: var(--primary-foreground-color);
43
46
  border: none;
@@ -80,9 +83,10 @@
80
83
  min-height: 30px;
81
84
  }
82
85
 
83
- .toolbar-toolContainer.selected .toolbar-button {
86
+ .toolbar-toolContainer.selected > .toolbar-button {
84
87
  background-color: var(--secondary-background-color);
85
88
  color: var(--secondary-foreground-color);
89
+ --icon-color: var(--secondary-foreground-color);
86
90
  }
87
91
 
88
92
  .toolbar-toolContainer:not(.selected):not(.dropdownShowable) > .toolbar-button > .toolbar-showHideDropdownIcon {
@@ -151,8 +155,7 @@
151
155
  }
152
156
 
153
157
  .toolbar-root .toolbar-zoomLevelEditor button {
154
- width: min-content;
155
- height: min-content;
158
+ min-width: 48px;
156
159
  }
157
160
 
158
161
  .color-input-container {
@@ -173,4 +176,5 @@
173
176
 
174
177
  .color-input-container .pipetteButton.active {
175
178
  background-color: var(--secondary-background-color);
179
+ --icon-color: var(--secondary-foreground-color);
176
180
  }
@@ -0,0 +1,31 @@
1
+ import Editor from '../../Editor';
2
+ import { ToolbarLocalization } from '../localization';
3
+ import BaseWidget from './BaseWidget';
4
+
5
+ export default class ActionButtonWidget extends BaseWidget {
6
+ public constructor(
7
+ editor: Editor, localizationTable: ToolbarLocalization,
8
+ protected makeIcon: ()=> Element,
9
+ protected title: string,
10
+
11
+ protected clickAction: ()=>void,
12
+ ) {
13
+ super(editor, localizationTable);
14
+ }
15
+
16
+ protected handleClick() {
17
+ this.clickAction();
18
+ }
19
+
20
+ protected getTitle(): string {
21
+ return this.title;
22
+ }
23
+
24
+ protected createIcon(): Element {
25
+ return this.makeIcon();
26
+ }
27
+
28
+ protected fillDropdown(_dropdown: HTMLElement): boolean {
29
+ return false;
30
+ }
31
+ }
@@ -1,4 +1,5 @@
1
1
  import Editor from '../../Editor';
2
+ import { InputEvtType } from '../../types';
2
3
  import { toolbarCSSPrefix } from '../HTMLToolbar';
3
4
  import { makeDropdownIcon } from '../icons';
4
5
  import { ToolbarLocalization } from '../localization';
@@ -50,6 +51,39 @@ export default abstract class BaseWidget {
50
51
  }
51
52
 
52
53
  protected setupActionBtnClickListener(button: HTMLElement) {
54
+ const clickTriggers = { Enter: true, ' ': true, };
55
+ button.onkeydown = (evt) => {
56
+ let handled = false;
57
+
58
+ if (evt.key in clickTriggers) {
59
+ if (!this.disabled) {
60
+ this.handleClick();
61
+ handled = true;
62
+ }
63
+ }
64
+
65
+ // If we didn't do anything with the event, send it to the editor.
66
+ if (!handled) {
67
+ this.editor.toolController.dispatchInputEvent({
68
+ kind: InputEvtType.KeyPressEvent,
69
+ key: evt.key,
70
+ ctrlKey: evt.ctrlKey,
71
+ });
72
+ }
73
+ };
74
+
75
+ button.onkeyup = evt => {
76
+ if (evt.key in clickTriggers) {
77
+ return;
78
+ }
79
+
80
+ this.editor.toolController.dispatchInputEvent({
81
+ kind: InputEvtType.KeyUpEvent,
82
+ key: evt.key,
83
+ ctrlKey: evt.ctrlKey,
84
+ });
85
+ };
86
+
53
87
  button.onclick = () => {
54
88
  if (!this.disabled) {
55
89
  this.handleClick();
@@ -103,8 +137,10 @@ export default abstract class BaseWidget {
103
137
  this.disabled = disabled;
104
138
  if (this.disabled) {
105
139
  this.button.classList.add('disabled');
140
+ this.button.setAttribute('aria-disabled', 'true');
106
141
  } else {
107
142
  this.button.classList.remove('disabled');
143
+ this.button.removeAttribute('aria-disabled');
108
144
  }
109
145
  }
110
146
 
@@ -1,5 +1,5 @@
1
1
  import Editor from '../../Editor';
2
- import Mat33 from '../../geometry/Mat33';
2
+ import Mat33 from '../../math/Mat33';
3
3
  import PanZoom, { PanZoomMode } from '../../tools/PanZoom';
4
4
  import { EditorEventType } from '../../types';
5
5
  import Viewport from '../../Viewport';
@@ -14,10 +14,12 @@ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor)
14
14
 
15
15
  const increaseButton = document.createElement('button');
16
16
  const decreaseButton = document.createElement('button');
17
+ const resetViewButton = document.createElement('button');
17
18
  const zoomLevelDisplay = document.createElement('span');
18
19
  increaseButton.innerText = '+';
19
20
  decreaseButton.innerText = '-';
20
- zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
21
+ resetViewButton.innerText = localizationTable.resetView;
22
+ zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton, resetViewButton);
21
23
 
22
24
  zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
23
25
  zoomLevelDisplay.classList.add('zoomDisplay');
@@ -42,13 +44,16 @@ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor)
42
44
  editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
43
45
  if (event.kind === EditorEventType.ViewportChanged) {
44
46
  updateZoomDisplay();
47
+
48
+ // Can't reset if already reset.
49
+ resetViewButton.disabled = event.newTransform.eq(Mat33.identity);
45
50
  }
46
51
  });
47
52
 
48
53
  const zoomBy = (factor: number) => {
49
54
  const screenCenter = editor.viewport.visibleRect.center;
50
55
  const transformUpdate = Mat33.scaling2D(factor, screenCenter);
51
- editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
56
+ editor.dispatch(Viewport.transformBy(transformUpdate), false);
52
57
  };
53
58
 
54
59
  increaseButton.onclick = () => {
@@ -59,6 +64,12 @@ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor)
59
64
  zoomBy(4.0/5);
60
65
  };
61
66
 
67
+ resetViewButton.onclick = () => {
68
+ editor.dispatch(Viewport.transformBy(
69
+ editor.viewport.canvasToScreenTransform.inverse()
70
+ ), true);
71
+ };
72
+
62
73
  return zoomLevelRow;
63
74
  };
64
75
 
@@ -1,8 +1,9 @@
1
1
  import Editor from '../../Editor';
2
2
  import SelectionTool from '../../tools/SelectionTool';
3
3
  import { EditorEventType } from '../../types';
4
- import { makeSelectionIcon } from '../icons';
4
+ import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons';
5
5
  import { ToolbarLocalization } from '../localization';
6
+ import ActionButtonWidget from './ActionButtonWidget';
6
7
  import BaseToolWidget from './BaseToolWidget';
7
8
 
8
9
  export class SelectionWidget extends BaseToolWidget {
@@ -10,44 +11,46 @@ export class SelectionWidget extends BaseToolWidget {
10
11
  editor: Editor, private tool: SelectionTool, localization: ToolbarLocalization
11
12
  ) {
12
13
  super(editor, tool, localization);
13
- }
14
-
15
- protected getTitle(): string {
16
- return this.localizationTable.select;
17
- }
18
-
19
- protected createIcon(): Element {
20
- return makeSelectionIcon();
21
- }
22
14
 
23
- protected fillDropdown(dropdown: HTMLElement): boolean {
24
- const container = document.createElement('div');
25
- const resizeButton = document.createElement('button');
26
- const duplicateButton = document.createElement('button');
27
- const deleteButton = document.createElement('button');
28
-
29
- resizeButton.innerText = this.localizationTable.resizeImageToSelection;
30
- resizeButton.disabled = true;
31
- deleteButton.innerText = this.localizationTable.deleteSelection;
32
- deleteButton.disabled = true;
33
- duplicateButton.innerText = this.localizationTable.duplicateSelection;
34
- duplicateButton.disabled = true;
35
-
36
- resizeButton.onclick = () => {
37
- const selection = this.tool.getSelection();
38
- this.editor.dispatch(this.editor.setImportExportRect(selection!.region));
39
- };
15
+ const resizeButton = new ActionButtonWidget(
16
+ editor, localization,
17
+ makeResizeViewportIcon,
18
+ this.localizationTable.resizeImageToSelection,
19
+ () => {
20
+ const selection = this.tool.getSelection();
21
+ this.editor.dispatch(this.editor.setImportExportRect(selection!.region));
22
+ },
23
+ );
24
+ const deleteButton = new ActionButtonWidget(
25
+ editor, localization,
26
+ makeDeleteSelectionIcon,
27
+ this.localizationTable.deleteSelection,
28
+ () => {
29
+ const selection = this.tool.getSelection();
30
+ this.editor.dispatch(selection!.deleteSelectedObjects());
31
+ this.tool.clearSelection();
32
+ },
33
+ );
34
+ const duplicateButton = new ActionButtonWidget(
35
+ editor, localization,
36
+ makeDuplicateSelectionIcon,
37
+ this.localizationTable.duplicateSelection,
38
+ () => {
39
+ const selection = this.tool.getSelection();
40
+ this.editor.dispatch(selection!.duplicateSelectedObjects());
41
+ },
42
+ );
40
43
 
41
- deleteButton.onclick = () => {
42
- const selection = this.tool.getSelection();
43
- this.editor.dispatch(selection!.deleteSelectedObjects());
44
- this.tool.clearSelection();
45
- };
44
+ this.addSubWidget(resizeButton);
45
+ this.addSubWidget(deleteButton);
46
+ this.addSubWidget(duplicateButton);
46
47
 
47
- duplicateButton.onclick = () => {
48
- const selection = this.tool.getSelection();
49
- this.editor.dispatch(selection!.duplicateSelectedObjects());
48
+ const updateDisabled = (disabled: boolean) => {
49
+ resizeButton.setDisabled(disabled);
50
+ deleteButton.setDisabled(disabled);
51
+ duplicateButton.setDisabled(disabled);
50
52
  };
53
+ updateDisabled(true);
51
54
 
52
55
  // Enable/disable actions based on whether items are selected
53
56
  this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
@@ -59,14 +62,16 @@ export class SelectionWidget extends BaseToolWidget {
59
62
  const selection = this.tool.getSelection();
60
63
  const hasSelection = selection && selection.region.area > 0;
61
64
 
62
- resizeButton.disabled = !hasSelection;
63
- deleteButton.disabled = resizeButton.disabled;
64
- duplicateButton.disabled = resizeButton.disabled;
65
+ updateDisabled(!hasSelection);
65
66
  }
66
67
  });
68
+ }
67
69
 
68
- container.replaceChildren(resizeButton, duplicateButton, deleteButton);
69
- dropdown.appendChild(container);
70
- return true;
70
+ protected getTitle(): string {
71
+ return this.localizationTable.select;
72
+ }
73
+
74
+ protected createIcon(): Element {
75
+ return makeSelectionIcon();
71
76
  }
72
77
  }
@@ -1,4 +1,4 @@
1
- import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent } from '../types';
1
+ import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types';
2
2
  import { ToolType } from './ToolController';
3
3
  import ToolEnabledGroup from './ToolEnabledGroup';
4
4
 
@@ -24,6 +24,10 @@ export default abstract class BaseTool implements PointerEvtListener {
24
24
  return false;
25
25
  }
26
26
 
27
+ public onKeyUp(_event: KeyUpEvent): boolean {
28
+ return false;
29
+ }
30
+
27
31
  public setEnabled(enabled: boolean) {
28
32
  this.enabled = enabled;
29
33
 
@@ -1,8 +1,8 @@
1
1
  import { PointerEvt } from '../types';
2
2
  import BaseTool from './BaseTool';
3
3
  import Editor from '../Editor';
4
- import { Point2 } from '../geometry/Vec2';
5
- import LineSegment2 from '../geometry/LineSegment2';
4
+ import { Point2 } from '../math/Vec2';
5
+ import LineSegment2 from '../math/LineSegment2';
6
6
  import Erase from '../commands/Erase';
7
7
  import { ToolType } from './ToolController';
8
8
  import AbstractComponent from '../components/AbstractComponent';
@@ -1,11 +1,11 @@
1
1
 
2
2
  import { Editor } from '../Editor';
3
- import Mat33 from '../geometry/Mat33';
4
- import { Point2, Vec2 } from '../geometry/Vec2';
5
- import Vec3 from '../geometry/Vec3';
3
+ import Mat33 from '../math/Mat33';
4
+ import { Point2, Vec2 } from '../math/Vec2';
5
+ import Vec3 from '../math/Vec3';
6
6
  import Pointer, { PointerDevice } from '../Pointer';
7
7
  import { EditorEventType, KeyPressEvent, PointerEvt, WheelEvt } from '../types';
8
- import { Viewport } from '../Viewport';
8
+ import { Viewport, ViewportTransform } from '../Viewport';
9
9
  import BaseTool from './BaseTool';
10
10
  import { ToolType } from './ToolController';
11
11
 
@@ -16,16 +16,18 @@ interface PinchData {
16
16
  dist: number;
17
17
  }
18
18
 
19
+
19
20
  export enum PanZoomMode {
20
21
  OneFingerTouchGestures = 0x1,
21
22
  TwoFingerTouchGestures = 0x1 << 1,
22
23
  RightClickDrags = 0x1 << 2,
23
24
  SinglePointerGestures = 0x1 << 3,
25
+ Keyboard = 0x1 << 4,
24
26
  }
25
27
 
26
28
  export default class PanZoom extends BaseTool {
27
29
  public readonly kind: ToolType.PanZoom = ToolType.PanZoom;
28
- private transform: Viewport.ViewportTransform|null = null;
30
+ private transform: ViewportTransform|null = null;
29
31
 
30
32
  private lastAngle: number;
31
33
  private lastDist: number;
@@ -72,7 +74,7 @@ export default class PanZoom extends BaseTool {
72
74
  }
73
75
 
74
76
  if (handlingGesture) {
75
- this.transform ??= new Viewport.ViewportTransform(Mat33.identity);
77
+ this.transform ??= Viewport.transformBy(Mat33.identity);
76
78
  this.editor.display.setDraftMode(true);
77
79
  }
78
80
 
@@ -98,14 +100,14 @@ export default class PanZoom extends BaseTool {
98
100
  this.lastScreenCenter = screenCenter;
99
101
  this.lastDist = dist;
100
102
  this.lastAngle = angle;
101
- this.transform = new Viewport.ViewportTransform(
103
+ this.transform = Viewport.transformBy(
102
104
  this.transform!.transform.rightMul(transformUpdate)
103
105
  );
104
106
  }
105
107
 
106
108
  private handleOneFingerMove(pointer: Pointer) {
107
109
  const delta = this.getCenterDelta(pointer.screenPos);
108
- this.transform = new Viewport.ViewportTransform(
110
+ this.transform = Viewport.transformBy(
109
111
  this.transform!.transform.rightMul(
110
112
  Mat33.translation(delta)
111
113
  )
@@ -114,7 +116,7 @@ export default class PanZoom extends BaseTool {
114
116
  }
115
117
 
116
118
  public onPointerMove({ allPointers }: PointerEvt): void {
117
- this.transform ??= new Viewport.ViewportTransform(Mat33.identity);
119
+ this.transform ??= Viewport.transformBy(Mat33.identity);
118
120
 
119
121
  const lastTransform = this.transform;
120
122
  if (allPointers.length === 2) {
@@ -144,21 +146,25 @@ export default class PanZoom extends BaseTool {
144
146
 
145
147
  // Applies [transformUpdate] to the editor. This stacks on top of the
146
148
  // current transformation, if it exists.
147
- private updateTransform(transformUpdate: Mat33) {
149
+ private updateTransform(transformUpdate: Mat33, announce: boolean = false) {
148
150
  let newTransform = transformUpdate;
149
151
  if (this.transform) {
150
152
  newTransform = this.transform.transform.rightMul(transformUpdate);
151
153
  }
152
154
 
153
155
  this.transform?.unapply(this.editor);
154
- this.transform = new Viewport.ViewportTransform(newTransform);
156
+ this.transform = Viewport.transformBy(newTransform);
155
157
  this.transform.apply(this.editor);
158
+
159
+ if (announce) {
160
+ this.editor.announceForAccessibility(this.transform.description(this.editor, this.editor.localization));
161
+ }
156
162
  }
157
163
 
158
164
  public onWheel({ delta, screenPos }: WheelEvt): boolean {
159
- if (this.transform === null) {
160
- this.transform = new Viewport.ViewportTransform(Mat33.identity);
161
- }
165
+ // Reset the transformation -- wheel events are individual events, so we don't
166
+ // need to unapply/reapply.
167
+ this.transform = Viewport.transformBy(Mat33.identity);
162
168
 
163
169
  const canvasPos = this.editor.viewport.screenToCanvas(screenPos);
164
170
  const toCanvas = this.editor.viewport.screenToCanvasTransform;
@@ -170,16 +176,23 @@ export default class PanZoom extends BaseTool {
170
176
  );
171
177
  const pinchZoomScaleFactor = 1.04;
172
178
  const transformUpdate = Mat33.scaling2D(
173
- Math.pow(pinchZoomScaleFactor, -delta.z), canvasPos
179
+ Math.max(0.25, Math.min(Math.pow(pinchZoomScaleFactor, -delta.z), 4)), canvasPos
174
180
  ).rightMul(
175
181
  Mat33.translation(translation)
176
182
  );
177
- this.updateTransform(transformUpdate);
183
+ this.updateTransform(transformUpdate, true);
178
184
 
179
185
  return true;
180
186
  }
181
187
 
182
188
  public onKeyPress({ key }: KeyPressEvent): boolean {
189
+ if (!(this.mode & PanZoomMode.Keyboard)) {
190
+ return false;
191
+ }
192
+
193
+ // No need to keep the same the transform for keyboard events.
194
+ this.transform = Viewport.transformBy(Mat33.identity);
195
+
183
196
  let translation = Vec2.zero;
184
197
  let scale = 1;
185
198
  let rotation = 0;
@@ -196,10 +209,12 @@ export default class PanZoom extends BaseTool {
196
209
  case 'ArrowRight':
197
210
  translation = Vec2.of(1, 0);
198
211
  break;
212
+ case 'q':
199
213
  case 'k':
200
214
  case 'ArrowUp':
201
215
  translation = Vec2.of(0, -1);
202
216
  break;
217
+ case 'e':
203
218
  case 'j':
204
219
  case 'ArrowDown':
205
220
  translation = Vec2.of(0, 1);
@@ -222,13 +237,19 @@ export default class PanZoom extends BaseTool {
222
237
 
223
238
  // For each keypress,
224
239
  translation = translation.times(30); // Move at most 30 units
225
- rotation *= Math.PI / 8; // Rotate at most a sixteenth of a rotation
240
+ rotation *= Math.PI / 8; // Rotate at least a sixteenth of a rotation
226
241
 
227
242
  // Transform the canvas, not the viewport:
228
243
  translation = translation.times(-1);
229
244
  rotation = rotation * -1;
230
245
  scale = 1 / scale;
231
246
 
247
+ // Work around an issue that seems to be related to rotation matricies losing precision on inversion.
248
+ // TODO: Figure out why and implement a better solution.
249
+ if (rotation !== 0) {
250
+ rotation += 0.0001;
251
+ }
252
+
232
253
  const toCanvas = this.editor.viewport.screenToCanvasTransform;
233
254
 
234
255
  // Transform without translating (treat toCanvas as a linear instead of
@@ -244,7 +265,7 @@ export default class PanZoom extends BaseTool {
244
265
  )).rightMul(Mat33.translation(
245
266
  translation
246
267
  ));
247
- this.updateTransform(transformUpdate);
268
+ this.updateTransform(transformUpdate, true);
248
269
 
249
270
  return true;
250
271
  }
@@ -1,11 +1,9 @@
1
- /* @jest-environment jsdom */
2
-
3
1
  import Color4 from '../Color4';
4
2
  import Stroke from '../components/Stroke';
5
3
  import Editor from '../Editor';
6
4
  import EditorImage from '../EditorImage';
7
- import Path from '../geometry/Path';
8
- import { Vec2 } from '../geometry/Vec2';
5
+ import Path from '../math/Path';
6
+ import { Vec2 } from '../math/Vec2';
9
7
  import { InputEvtType } from '../types';
10
8
  import SelectionTool from './SelectionTool';
11
9
  import { ToolType } from './ToolController';
@@ -66,7 +64,7 @@ describe('SelectionTool', () => {
66
64
 
67
65
  // Drag the object
68
66
  selection!.handleBackgroundDrag(Vec2.of(5, 5));
69
- selection!.finishDragging();
67
+ selection!.finalizeTransform();
70
68
 
71
69
  expect(testStroke.getBBox().topLeft).toMatchObject({
72
70
  x: 5,
@@ -80,4 +78,27 @@ describe('SelectionTool', () => {
80
78
  y: 0,
81
79
  });
82
80
  });
81
+
82
+ it('moving the selection with a keyboard should move the view to keep the selection in view', () => {
83
+ const { addTestStrokeCommand } = createSquareStroke();
84
+ const editor = createEditor();
85
+ editor.dispatch(addTestStrokeCommand);
86
+
87
+ // Select the stroke
88
+ const selectionTool = getSelectionTool(editor);
89
+ selectionTool.setEnabled(true);
90
+ editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
91
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
92
+ editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
93
+
94
+ const selection = selectionTool.getSelection();
95
+ if (selection === null) {
96
+ // Throw to allow TypeScript's non-null checker to understand that selection
97
+ // must be non-null after this.
98
+ throw new Error('Selection should be non-null.');
99
+ }
100
+
101
+ selection.handleBackgroundDrag(Vec2.of(0, -1000));
102
+ expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true);
103
+ });
83
104
  });