js-draw 1.21.2 → 1.21.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (183) hide show
  1. package/dist/bundle.js +1 -1
  2. package/dist/cjs/tools/BaseTool.d.ts +61 -0
  3. package/dist/cjs/tools/BaseTool.js +179 -0
  4. package/dist/cjs/tools/Eraser.d.ts +60 -0
  5. package/dist/cjs/tools/Eraser.js +299 -0
  6. package/dist/cjs/tools/Eraser.test.d.ts +1 -0
  7. package/dist/cjs/tools/FindTool.d.ts +21 -0
  8. package/dist/cjs/tools/FindTool.js +137 -0
  9. package/dist/cjs/tools/FindTool.test.d.ts +1 -0
  10. package/dist/cjs/tools/InputFilter/ContextMenuRecognizer.d.ts +17 -0
  11. package/dist/cjs/tools/InputFilter/ContextMenuRecognizer.js +105 -0
  12. package/dist/cjs/tools/InputFilter/ContextMenuRecognizer.test.d.ts +1 -0
  13. package/dist/cjs/tools/InputFilter/FunctionMapper.d.ts +12 -0
  14. package/dist/cjs/tools/InputFilter/FunctionMapper.js +21 -0
  15. package/dist/cjs/tools/InputFilter/InputMapper.d.ts +23 -0
  16. package/dist/cjs/tools/InputFilter/InputMapper.js +38 -0
  17. package/dist/cjs/tools/InputFilter/InputPipeline.d.ts +15 -0
  18. package/dist/cjs/tools/InputFilter/InputPipeline.js +54 -0
  19. package/dist/cjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  20. package/dist/cjs/tools/InputFilter/InputStabilizer.d.ts +29 -0
  21. package/dist/cjs/tools/InputFilter/InputStabilizer.js +181 -0
  22. package/dist/cjs/tools/InputFilter/StrokeKeyboardControl.d.ts +21 -0
  23. package/dist/cjs/tools/InputFilter/StrokeKeyboardControl.js +84 -0
  24. package/dist/cjs/tools/PanZoom.d.ts +125 -0
  25. package/dist/cjs/tools/PanZoom.js +517 -0
  26. package/dist/cjs/tools/PanZoom.test.d.ts +1 -0
  27. package/dist/cjs/tools/PasteHandler.d.ts +23 -0
  28. package/dist/cjs/tools/PasteHandler.js +109 -0
  29. package/dist/cjs/tools/Pen.d.ts +54 -0
  30. package/dist/cjs/tools/Pen.js +335 -0
  31. package/dist/cjs/tools/Pen.test.d.ts +1 -0
  32. package/dist/cjs/tools/PipetteTool.d.ts +28 -0
  33. package/dist/cjs/tools/PipetteTool.js +69 -0
  34. package/dist/cjs/tools/ScrollbarTool.d.ts +18 -0
  35. package/dist/cjs/tools/ScrollbarTool.js +85 -0
  36. package/dist/cjs/tools/SelectionTool/SelectAllShortcutHandler.d.ts +9 -0
  37. package/dist/cjs/tools/SelectionTool/SelectAllShortcutHandler.js +32 -0
  38. package/dist/cjs/tools/SelectionTool/Selection.d.ts +72 -0
  39. package/dist/cjs/tools/SelectionTool/Selection.js +634 -0
  40. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +62 -0
  41. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +141 -0
  42. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +32 -0
  43. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.js +86 -0
  44. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +42 -0
  45. package/dist/cjs/tools/SelectionTool/SelectionTool.js +500 -0
  46. package/dist/cjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  47. package/dist/cjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  48. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  49. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
  50. package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +42 -0
  51. package/dist/cjs/tools/SelectionTool/TransformMode.js +155 -0
  52. package/dist/cjs/tools/SelectionTool/keybindings.d.ts +15 -0
  53. package/dist/cjs/tools/SelectionTool/keybindings.js +38 -0
  54. package/dist/cjs/tools/SelectionTool/types.d.ts +35 -0
  55. package/dist/cjs/tools/SelectionTool/types.js +14 -0
  56. package/dist/cjs/tools/SelectionTool/util/makeClipboardErrorHandlers.d.ts +6 -0
  57. package/dist/cjs/tools/SelectionTool/util/makeClipboardErrorHandlers.js +50 -0
  58. package/dist/cjs/tools/SelectionTool/util/showSelectionContextMenu.d.ts +5 -0
  59. package/dist/cjs/tools/SelectionTool/util/showSelectionContextMenu.js +43 -0
  60. package/dist/cjs/tools/SoundUITool.d.ts +26 -0
  61. package/dist/cjs/tools/SoundUITool.js +171 -0
  62. package/dist/cjs/tools/TextTool.d.ts +36 -0
  63. package/dist/cjs/tools/TextTool.js +285 -0
  64. package/dist/cjs/tools/TextTool.test.d.ts +1 -0
  65. package/dist/cjs/tools/ToolController.d.ts +73 -0
  66. package/dist/cjs/tools/ToolController.js +304 -0
  67. package/dist/cjs/tools/ToolController.test.d.ts +1 -0
  68. package/dist/cjs/tools/ToolEnabledGroup.d.ts +6 -0
  69. package/dist/cjs/tools/ToolEnabledGroup.js +13 -0
  70. package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +16 -0
  71. package/dist/cjs/tools/ToolSwitcherShortcut.js +40 -0
  72. package/dist/cjs/tools/ToolbarShortcutHandler.d.ts +12 -0
  73. package/dist/cjs/tools/ToolbarShortcutHandler.js +34 -0
  74. package/dist/cjs/tools/UndoRedoShortcut.d.ts +8 -0
  75. package/dist/cjs/tools/UndoRedoShortcut.js +27 -0
  76. package/dist/cjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  77. package/dist/cjs/tools/keybindings.d.ts +16 -0
  78. package/dist/cjs/tools/keybindings.js +58 -0
  79. package/dist/cjs/tools/lib.d.ts +14 -0
  80. package/dist/cjs/tools/lib.js +36 -0
  81. package/dist/cjs/tools/localization.d.ts +43 -0
  82. package/dist/cjs/tools/localization.js +45 -0
  83. package/dist/cjs/tools/util/StationaryPenDetector.d.ts +25 -0
  84. package/dist/cjs/tools/util/StationaryPenDetector.js +107 -0
  85. package/dist/cjs/tools/util/createMenuOverlay.d.ts +10 -0
  86. package/dist/cjs/tools/util/createMenuOverlay.js +126 -0
  87. package/dist/cjs/tools/util/createMenuOverlay.test.d.ts +1 -0
  88. package/dist/cjs/version.js +1 -1
  89. package/dist/mjs/tools/BaseTool.d.ts +61 -0
  90. package/dist/mjs/tools/BaseTool.mjs +177 -0
  91. package/dist/mjs/tools/Eraser.d.ts +60 -0
  92. package/dist/mjs/tools/Eraser.mjs +292 -0
  93. package/dist/mjs/tools/Eraser.test.d.ts +1 -0
  94. package/dist/mjs/tools/FindTool.d.ts +21 -0
  95. package/dist/mjs/tools/FindTool.mjs +131 -0
  96. package/dist/mjs/tools/FindTool.test.d.ts +1 -0
  97. package/dist/mjs/tools/InputFilter/ContextMenuRecognizer.d.ts +17 -0
  98. package/dist/mjs/tools/InputFilter/ContextMenuRecognizer.mjs +76 -0
  99. package/dist/mjs/tools/InputFilter/ContextMenuRecognizer.test.d.ts +1 -0
  100. package/dist/mjs/tools/InputFilter/FunctionMapper.d.ts +12 -0
  101. package/dist/mjs/tools/InputFilter/FunctionMapper.mjs +15 -0
  102. package/dist/mjs/tools/InputFilter/InputMapper.d.ts +23 -0
  103. package/dist/mjs/tools/InputFilter/InputMapper.mjs +36 -0
  104. package/dist/mjs/tools/InputFilter/InputPipeline.d.ts +15 -0
  105. package/dist/mjs/tools/InputFilter/InputPipeline.mjs +49 -0
  106. package/dist/mjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  107. package/dist/mjs/tools/InputFilter/InputStabilizer.d.ts +29 -0
  108. package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +175 -0
  109. package/dist/mjs/tools/InputFilter/StrokeKeyboardControl.d.ts +21 -0
  110. package/dist/mjs/tools/InputFilter/StrokeKeyboardControl.mjs +78 -0
  111. package/dist/mjs/tools/PanZoom.d.ts +125 -0
  112. package/dist/mjs/tools/PanZoom.mjs +510 -0
  113. package/dist/mjs/tools/PanZoom.test.d.ts +1 -0
  114. package/dist/mjs/tools/PasteHandler.d.ts +23 -0
  115. package/dist/mjs/tools/PasteHandler.mjs +103 -0
  116. package/dist/mjs/tools/Pen.d.ts +54 -0
  117. package/dist/mjs/tools/Pen.mjs +306 -0
  118. package/dist/mjs/tools/Pen.test.d.ts +1 -0
  119. package/dist/mjs/tools/PipetteTool.d.ts +28 -0
  120. package/dist/mjs/tools/PipetteTool.mjs +63 -0
  121. package/dist/mjs/tools/ScrollbarTool.d.ts +18 -0
  122. package/dist/mjs/tools/ScrollbarTool.mjs +79 -0
  123. package/dist/mjs/tools/SelectionTool/SelectAllShortcutHandler.d.ts +9 -0
  124. package/dist/mjs/tools/SelectionTool/SelectAllShortcutHandler.mjs +26 -0
  125. package/dist/mjs/tools/SelectionTool/Selection.d.ts +72 -0
  126. package/dist/mjs/tools/SelectionTool/Selection.mjs +606 -0
  127. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +62 -0
  128. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +137 -0
  129. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +32 -0
  130. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.mjs +83 -0
  131. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +42 -0
  132. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +493 -0
  133. package/dist/mjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  134. package/dist/mjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  135. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  136. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
  137. package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +42 -0
  138. package/dist/mjs/tools/SelectionTool/TransformMode.mjs +146 -0
  139. package/dist/mjs/tools/SelectionTool/keybindings.d.ts +15 -0
  140. package/dist/mjs/tools/SelectionTool/keybindings.mjs +32 -0
  141. package/dist/mjs/tools/SelectionTool/types.d.ts +35 -0
  142. package/dist/mjs/tools/SelectionTool/types.mjs +11 -0
  143. package/dist/mjs/tools/SelectionTool/util/makeClipboardErrorHandlers.d.ts +6 -0
  144. package/dist/mjs/tools/SelectionTool/util/makeClipboardErrorHandlers.mjs +45 -0
  145. package/dist/mjs/tools/SelectionTool/util/showSelectionContextMenu.d.ts +5 -0
  146. package/dist/mjs/tools/SelectionTool/util/showSelectionContextMenu.mjs +38 -0
  147. package/dist/mjs/tools/SoundUITool.d.ts +26 -0
  148. package/dist/mjs/tools/SoundUITool.mjs +165 -0
  149. package/dist/mjs/tools/TextTool.d.ts +36 -0
  150. package/dist/mjs/tools/TextTool.mjs +279 -0
  151. package/dist/mjs/tools/TextTool.test.d.ts +1 -0
  152. package/dist/mjs/tools/ToolController.d.ts +73 -0
  153. package/dist/mjs/tools/ToolController.mjs +275 -0
  154. package/dist/mjs/tools/ToolController.test.d.ts +1 -0
  155. package/dist/mjs/tools/ToolEnabledGroup.d.ts +6 -0
  156. package/dist/mjs/tools/ToolEnabledGroup.mjs +10 -0
  157. package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +16 -0
  158. package/dist/mjs/tools/ToolSwitcherShortcut.mjs +34 -0
  159. package/dist/mjs/tools/ToolbarShortcutHandler.d.ts +12 -0
  160. package/dist/mjs/tools/ToolbarShortcutHandler.mjs +28 -0
  161. package/dist/mjs/tools/UndoRedoShortcut.d.ts +8 -0
  162. package/dist/mjs/tools/UndoRedoShortcut.mjs +21 -0
  163. package/dist/mjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  164. package/dist/mjs/tools/keybindings.d.ts +16 -0
  165. package/dist/mjs/tools/keybindings.mjs +38 -0
  166. package/dist/mjs/tools/lib.d.ts +14 -0
  167. package/dist/mjs/tools/lib.mjs +14 -0
  168. package/dist/mjs/tools/localization.d.ts +43 -0
  169. package/dist/mjs/tools/localization.mjs +42 -0
  170. package/dist/mjs/tools/util/StationaryPenDetector.d.ts +25 -0
  171. package/dist/mjs/tools/util/StationaryPenDetector.mjs +103 -0
  172. package/dist/mjs/tools/util/createMenuOverlay.d.ts +10 -0
  173. package/dist/mjs/tools/util/createMenuOverlay.mjs +121 -0
  174. package/dist/mjs/tools/util/createMenuOverlay.test.d.ts +1 -0
  175. package/dist/mjs/version.mjs +1 -1
  176. package/package.json +4 -4
  177. package/src/tools/FindTool.css +7 -0
  178. package/src/tools/ScrollbarTool.scss +57 -0
  179. package/src/tools/SelectionTool/SelectionTool.scss +165 -0
  180. package/src/tools/SelectionTool/util/makeClipboardErrorHandlers.scss +15 -0
  181. package/src/tools/SoundUITool.scss +22 -0
  182. package/src/tools/tools.scss +6 -0
  183. package/src/tools/util/createMenuOverlay.scss +67 -0
@@ -0,0 +1,493 @@
1
+ import { Mat33, Vec2 } from '@js-draw/math';
2
+ import { EditorEventType } from '../../types.mjs';
3
+ import Viewport from '../../Viewport.mjs';
4
+ import BaseTool from '../BaseTool.mjs';
5
+ import CanvasRenderer from '../../rendering/renderers/CanvasRenderer.mjs';
6
+ import SVGRenderer from '../../rendering/renderers/SVGRenderer.mjs';
7
+ import Selection from './Selection.mjs';
8
+ import TextComponent from '../../components/TextComponent.mjs';
9
+ import { duplicateSelectionShortcut, translateLeftSelectionShortcutId, translateRightSelectionShortcutId, selectAllKeyboardShortcut, sendToBackSelectionShortcut, snapToGridKeyboardShortcutId, translateDownSelectionShortcutId, translateUpSelectionShortcutId, rotateClockwiseSelectionShortcutId, rotateCounterClockwiseSelectionShortcutId, stretchXSelectionShortcutId, shrinkXSelectionShortcutId, shrinkYSelectionShortcutId, stretchYSelectionShortcutId, stretchXYSelectionShortcutId, shrinkXYSelectionShortcutId } from '../keybindings.mjs';
10
+ import ToPointerAutoscroller from './ToPointerAutoscroller.mjs';
11
+ import showSelectionContextMenu from './util/showSelectionContextMenu.mjs';
12
+ export const cssPrefix = 'selection-tool-';
13
+ // Allows users to select/transform portions of the `EditorImage`.
14
+ // With respect to `extend`ing, `SelectionTool` is not stable.
15
+ export default class SelectionTool extends BaseTool {
16
+ constructor(editor, description) {
17
+ super(editor.notifier, description);
18
+ this.editor = editor;
19
+ // True if clearing and recreating the selectionBox has been deferred. This is used to prevent the selection
20
+ // from vanishing on pointerdown events that are intended to form other gestures (e.g. long press) that would
21
+ // ultimately restore the selection.
22
+ this.rebuildSelectionScheduled = false;
23
+ this.startPoint = null; // canvas position
24
+ this.expandingSelectionBox = false;
25
+ this.shiftKeyPressed = false;
26
+ this.snapToGrid = false;
27
+ this.lastPointer = null;
28
+ this.showContextMenu = async (canvasAnchor, preferSelectionMenu = true) => {
29
+ await showSelectionContextMenu(this.selectionBox, this.editor, canvasAnchor, preferSelectionMenu, () => this.clearSelection());
30
+ };
31
+ this.selectionBoxHandlingEvt = false;
32
+ this.lastSelectedObjects = [];
33
+ // Whether the last keypress corresponded to an action that didn't transform the
34
+ // selection (and thus does not need to be finalized on onKeyUp).
35
+ this.hasUnfinalizedTransformFromKeyPress = false;
36
+ this.autoscroller = new ToPointerAutoscroller(editor.viewport, (scrollBy) => {
37
+ editor.dispatch(Viewport.transformBy(Mat33.translation(scrollBy)), false);
38
+ // Update the selection box/content to match the new viewport.
39
+ if (this.lastPointer) {
40
+ // The viewport has changed -- ensure that the screen and canvas positions
41
+ // of the pointer are both correct
42
+ const updatedPointer = this.lastPointer.withScreenPosition(this.lastPointer.screenPos, editor.viewport);
43
+ this.onMainPointerUpdated(updatedPointer);
44
+ }
45
+ });
46
+ this.handleOverlay = document.createElement('div');
47
+ editor.createHTMLOverlay(this.handleOverlay);
48
+ this.handleOverlay.style.display = 'none';
49
+ this.handleOverlay.classList.add('handleOverlay');
50
+ editor.notifier.on(EditorEventType.ViewportChanged, _data => {
51
+ // The selection box could be using the wet ink display if its transformation
52
+ // hasn't been finalized yet. Clear before updating the UI.
53
+ this.editor.clearWetInk();
54
+ // If not currently selecting, ensure that the selection box
55
+ // is large enough.
56
+ if (!this.expandingSelectionBox) {
57
+ this.selectionBox?.padRegion();
58
+ }
59
+ this.selectionBox?.updateUI();
60
+ });
61
+ this.editor.handleKeyEventsFrom(this.handleOverlay);
62
+ this.editor.handlePointerEventsFrom(this.handleOverlay);
63
+ }
64
+ makeSelectionBox(selectionStartPos) {
65
+ this.prevSelectionBox = this.selectionBox;
66
+ this.selectionBox = new Selection(selectionStartPos, this.editor, this.showContextMenu);
67
+ if (!this.expandingSelectionBox) {
68
+ // Remove any previous selection rects
69
+ this.prevSelectionBox?.cancelSelection();
70
+ }
71
+ this.selectionBox.addTo(this.handleOverlay);
72
+ }
73
+ snapSelectionToGrid() {
74
+ if (!this.selectionBox)
75
+ throw new Error('No selection to snap!');
76
+ // Snap the top left corner of what we have selected.
77
+ const topLeftOfBBox = this.selectionBox.computeTightBoundingBox().topLeft;
78
+ const snappedTopLeft = this.editor.viewport.snapToGrid(topLeftOfBBox);
79
+ const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
80
+ const oldTransform = this.selectionBox.getTransform();
81
+ this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDelta)));
82
+ this.selectionBox.finalizeTransform();
83
+ }
84
+ onContextMenu(event) {
85
+ const canShowSelectionMenu = this.selectionBox?.getScreenRegion()?.containsPoint(event.screenPos);
86
+ void this.showContextMenu(event.canvasPos, canShowSelectionMenu);
87
+ return true;
88
+ }
89
+ onPointerDown({ allPointers, current }) {
90
+ const snapToGrid = this.snapToGrid;
91
+ if (snapToGrid) {
92
+ current = current.snappedToGrid(this.editor.viewport);
93
+ }
94
+ // Don't rely on .isPrimary -- it's buggy in Firefox. See https://github.com/personalizedrefrigerator/js-draw/issues/71
95
+ if (allPointers.length === 1) {
96
+ this.startPoint = current.canvasPos;
97
+ let transforming = false;
98
+ if (this.selectionBox) {
99
+ if (snapToGrid) {
100
+ this.snapSelectionToGrid();
101
+ }
102
+ const dragStartResult = this.selectionBox.onDragStart(current);
103
+ if (dragStartResult) {
104
+ transforming = true;
105
+ this.selectionBoxHandlingEvt = true;
106
+ this.expandingSelectionBox = false;
107
+ }
108
+ }
109
+ if (!transforming) {
110
+ // Shift key: Combine the new and old selection boxes at the end of the gesture.
111
+ this.expandingSelectionBox = this.shiftKeyPressed;
112
+ this.rebuildSelectionScheduled = true;
113
+ }
114
+ else {
115
+ // Only autoscroll if we're transforming an existing selection
116
+ this.autoscroller.start();
117
+ }
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+ onPointerMove(event) {
123
+ this.onMainPointerUpdated(event.current);
124
+ }
125
+ onMainPointerUpdated(currentPointer) {
126
+ this.lastPointer = currentPointer;
127
+ if (this.rebuildSelectionScheduled) {
128
+ this.rebuildSelectionScheduled = false;
129
+ this.makeSelectionBox(this.startPoint ?? currentPointer.canvasPos);
130
+ this.selectionBox?.setHandlesVisible(false);
131
+ }
132
+ if (!this.selectionBox)
133
+ return;
134
+ this.autoscroller.onPointerMove(currentPointer.screenPos);
135
+ if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
136
+ const screenPos = this.editor.viewport.canvasToScreen(this.startPoint);
137
+ currentPointer = currentPointer.lockedToXYAxesScreen(screenPos, this.editor.viewport);
138
+ }
139
+ if (this.snapToGrid) {
140
+ currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
141
+ }
142
+ if (this.selectionBoxHandlingEvt) {
143
+ this.selectionBox.onDragUpdate(currentPointer);
144
+ }
145
+ else {
146
+ this.selectionBox.setToPoint(currentPointer.canvasPos);
147
+ }
148
+ }
149
+ onPointerUp(event) {
150
+ this.onMainPointerUpdated(event.current);
151
+ this.autoscroller.stop();
152
+ if (!this.selectionBox)
153
+ return;
154
+ this.selectionBox.setHandlesVisible(true);
155
+ // Were we expanding the previous selection?
156
+ if (this.expandingSelectionBox && this.prevSelectionBox) {
157
+ // If so, finish expanding.
158
+ this.expandingSelectionBox = false;
159
+ this.selectionBox.resolveToObjects();
160
+ this.setSelection([
161
+ ...this.selectionBox.getSelectedObjects(),
162
+ ...this.prevSelectionBox.getSelectedObjects(),
163
+ ]);
164
+ }
165
+ else {
166
+ if (!this.selectionBoxHandlingEvt) {
167
+ // Expand/shrink the selection rectangle, if applicable
168
+ this.selectionBox.resolveToObjects();
169
+ this.onSelectionUpdated();
170
+ }
171
+ else {
172
+ this.selectionBox.onDragEnd();
173
+ }
174
+ this.selectionBoxHandlingEvt = false;
175
+ this.lastPointer = null;
176
+ }
177
+ }
178
+ onGestureCancel() {
179
+ this.autoscroller.stop();
180
+ if (this.selectionBoxHandlingEvt) {
181
+ this.selectionBox?.onDragCancel();
182
+ }
183
+ else if (!this.rebuildSelectionScheduled) {
184
+ // Revert to the previous selection, if any.
185
+ this.selectionBox?.cancelSelection();
186
+ this.selectionBox = this.prevSelectionBox;
187
+ this.selectionBox?.addTo(this.handleOverlay);
188
+ this.selectionBox?.recomputeRegion();
189
+ this.prevSelectionBox = null;
190
+ }
191
+ this.rebuildSelectionScheduled = false;
192
+ this.expandingSelectionBox = false;
193
+ this.lastPointer = null;
194
+ this.selectionBoxHandlingEvt = false;
195
+ }
196
+ onSelectionUpdated() {
197
+ const selectedItemCount = this.selectionBox?.getSelectedItemCount() ?? 0;
198
+ const selectedObjects = this.selectionBox?.getSelectedObjects() ?? [];
199
+ const hasDifferentSelection = this.lastSelectedObjects.length !== selectedItemCount
200
+ || selectedObjects.some((obj, i) => this.lastSelectedObjects[i] !== obj);
201
+ if (hasDifferentSelection) {
202
+ this.lastSelectedObjects = selectedObjects;
203
+ // Note that the selection has changed
204
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
205
+ kind: EditorEventType.ToolUpdated,
206
+ tool: this,
207
+ });
208
+ // Only fire the SelectionUpdated event if the selection really updated.
209
+ this.editor.notifier.dispatch(EditorEventType.SelectionUpdated, {
210
+ kind: EditorEventType.SelectionUpdated,
211
+ selectedComponents: selectedObjects,
212
+ tool: this,
213
+ });
214
+ if (selectedItemCount > 0) {
215
+ this.editor.announceForAccessibility(this.editor.localization.selectedElements(selectedItemCount));
216
+ this.zoomToSelection();
217
+ }
218
+ }
219
+ if (selectedItemCount === 0 && this.selectionBox) {
220
+ this.selectionBox.cancelSelection();
221
+ this.prevSelectionBox = this.selectionBox;
222
+ this.selectionBox = null;
223
+ }
224
+ }
225
+ zoomToSelection() {
226
+ if (this.selectionBox) {
227
+ const selectionRect = this.selectionBox.region;
228
+ this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false);
229
+ }
230
+ }
231
+ onKeyPress(event) {
232
+ const shortcucts = this.editor.shortcuts;
233
+ if (shortcucts.matchesShortcut(snapToGridKeyboardShortcutId, event)) {
234
+ this.snapToGrid = true;
235
+ return true;
236
+ }
237
+ if (this.selectionBox && (shortcucts.matchesShortcut(duplicateSelectionShortcut, event)
238
+ || shortcucts.matchesShortcut(sendToBackSelectionShortcut, event))) {
239
+ // Handle duplication on key up — we don't want to accidentally duplicate
240
+ // many times.
241
+ return true;
242
+ }
243
+ else if (shortcucts.matchesShortcut(selectAllKeyboardShortcut, event)) {
244
+ this.setSelection(this.editor.image.getAllElements());
245
+ return true;
246
+ }
247
+ else if (event.ctrlKey) {
248
+ // Don't transform the selection with, for example, ctrl+i.
249
+ // Pass it to another tool, if apliccable.
250
+ return false;
251
+ }
252
+ else if (event.shiftKey || event.key === 'Shift') {
253
+ this.shiftKeyPressed = true;
254
+ if (event.key === 'Shift') {
255
+ return true;
256
+ }
257
+ }
258
+ let rotationSteps = 0;
259
+ let xTranslateSteps = 0;
260
+ let yTranslateSteps = 0;
261
+ let xScaleSteps = 0;
262
+ let yScaleSteps = 0;
263
+ if (shortcucts.matchesShortcut(translateLeftSelectionShortcutId, event)) {
264
+ xTranslateSteps -= 1;
265
+ }
266
+ else if (shortcucts.matchesShortcut(translateRightSelectionShortcutId, event)) {
267
+ xTranslateSteps += 1;
268
+ }
269
+ else if (shortcucts.matchesShortcut(translateUpSelectionShortcutId, event)) {
270
+ yTranslateSteps -= 1;
271
+ }
272
+ else if (shortcucts.matchesShortcut(translateDownSelectionShortcutId, event)) {
273
+ yTranslateSteps += 1;
274
+ }
275
+ else if (shortcucts.matchesShortcut(rotateClockwiseSelectionShortcutId, event)) {
276
+ rotationSteps += 1;
277
+ }
278
+ else if (shortcucts.matchesShortcut(rotateCounterClockwiseSelectionShortcutId, event)) {
279
+ rotationSteps -= 1;
280
+ }
281
+ else if (shortcucts.matchesShortcut(shrinkXSelectionShortcutId, event)) {
282
+ xScaleSteps -= 1;
283
+ }
284
+ else if (shortcucts.matchesShortcut(stretchXSelectionShortcutId, event)) {
285
+ xScaleSteps += 1;
286
+ }
287
+ else if (shortcucts.matchesShortcut(shrinkYSelectionShortcutId, event)) {
288
+ yScaleSteps -= 1;
289
+ }
290
+ else if (shortcucts.matchesShortcut(stretchYSelectionShortcutId, event)) {
291
+ yScaleSteps += 1;
292
+ }
293
+ else if (shortcucts.matchesShortcut(shrinkXYSelectionShortcutId, event)) {
294
+ xScaleSteps -= 1;
295
+ yScaleSteps -= 1;
296
+ }
297
+ else if (shortcucts.matchesShortcut(stretchXYSelectionShortcutId, event)) {
298
+ xScaleSteps += 1;
299
+ yScaleSteps += 1;
300
+ }
301
+ let handled = xTranslateSteps !== 0
302
+ || yTranslateSteps !== 0
303
+ || rotationSteps !== 0
304
+ || xScaleSteps !== 0
305
+ || yScaleSteps !== 0;
306
+ if (!this.selectionBox) {
307
+ handled = false;
308
+ }
309
+ else if (handled) {
310
+ const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas();
311
+ const rotateStepSize = Math.PI / 8;
312
+ const scaleStepSize = 5 / 4;
313
+ const region = this.selectionBox.region;
314
+ const scaleFactor = Vec2.of(scaleStepSize ** xScaleSteps, scaleStepSize ** yScaleSteps);
315
+ const rotationMat = Mat33.zRotation(rotationSteps * rotateStepSize);
316
+ const roundedRotationMatrix = rotationMat.mapEntries(component => Viewport.roundScaleRatio(component));
317
+ const regionCenter = this.editor.viewport.roundPoint(region.center);
318
+ const transform = Mat33.scaling2D(scaleFactor, this.editor.viewport.roundPoint(region.topLeft)).rightMul(Mat33.translation(regionCenter).rightMul(roundedRotationMatrix).rightMul(Mat33.translation(regionCenter.times(-1)))).rightMul(Mat33.translation(this.editor.viewport.roundPoint(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))));
319
+ const oldTransform = this.selectionBox.getTransform();
320
+ this.selectionBox.setTransform(oldTransform.rightMul(transform));
321
+ this.selectionBox.scrollTo();
322
+ // The transformation needs to be finalized at some point (on key up)
323
+ this.hasUnfinalizedTransformFromKeyPress = true;
324
+ }
325
+ if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
326
+ this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
327
+ this.clearSelection();
328
+ handled = true;
329
+ }
330
+ return handled;
331
+ }
332
+ onKeyUp(evt) {
333
+ const shortcucts = this.editor.shortcuts;
334
+ if (shortcucts.matchesShortcut(snapToGridKeyboardShortcutId, evt)) {
335
+ this.snapToGrid = false;
336
+ return true;
337
+ }
338
+ if (shortcucts.matchesShortcut(selectAllKeyboardShortcut, evt)) {
339
+ // Selected all in onKeyDown. Don't finalizeTransform.
340
+ return true;
341
+ }
342
+ if (this.selectionBox && shortcucts.matchesShortcut(duplicateSelectionShortcut, evt)) {
343
+ // Finalize duplicating the selection
344
+ this.selectionBox.duplicateSelectedObjects().then(command => {
345
+ this.editor.dispatch(command);
346
+ });
347
+ return true;
348
+ }
349
+ if (this.selectionBox && shortcucts.matchesShortcut(sendToBackSelectionShortcut, evt)) {
350
+ const sendToBackCommand = this.selectionBox.sendToBack();
351
+ if (sendToBackCommand) {
352
+ this.editor.dispatch(sendToBackCommand);
353
+ }
354
+ return true;
355
+ }
356
+ // Here, we check if shiftKey === false because, as of this writing,
357
+ // evt.shiftKey is an optional property. Being falsey could just mean
358
+ // that it wasn't set.
359
+ if (evt.shiftKey === false) {
360
+ this.shiftKeyPressed = false;
361
+ // Don't return immediately -- event may be otherwise handled
362
+ }
363
+ // Also check for key === 'Shift' (for the case where shiftKey is undefined)
364
+ if (evt.key === 'Shift') {
365
+ this.shiftKeyPressed = false;
366
+ return true;
367
+ }
368
+ // If we don't need to finalize the transform
369
+ if (!this.hasUnfinalizedTransformFromKeyPress) {
370
+ return true;
371
+ }
372
+ if (this.selectionBox) {
373
+ this.selectionBox.finalizeTransform();
374
+ this.hasUnfinalizedTransformFromKeyPress = false;
375
+ return true;
376
+ }
377
+ return false;
378
+ }
379
+ onCopy(event) {
380
+ if (!this.selectionBox) {
381
+ return false;
382
+ }
383
+ const selectedElems = this.selectionBox.getSelectedObjects();
384
+ const bbox = this.selectionBox.region;
385
+ if (selectedElems.length === 0) {
386
+ return false;
387
+ }
388
+ const exportViewport = new Viewport(() => { });
389
+ const selectionScreenSize = this.selectionBox.getScreenRegion().size.times(this.editor.display.getDevicePixelRatio());
390
+ // Update the viewport to have screen size roughly equal to the size of the selection box
391
+ let scaleFactor = selectionScreenSize.maximumEntryMagnitude() / (bbox.size.maximumEntryMagnitude() || 1);
392
+ // Round to a nearby power of two
393
+ scaleFactor = Math.pow(2, Math.ceil(Math.log2(scaleFactor)));
394
+ exportViewport.updateScreenSize(bbox.size.times(scaleFactor));
395
+ exportViewport.resetTransform(Mat33.scaling2D(scaleFactor)
396
+ // Move the selection onto the screen
397
+ .rightMul(Mat33.translation(bbox.topLeft.times(-1))));
398
+ const { element: svgExportElem, renderer: svgRenderer } = SVGRenderer.fromViewport(exportViewport, { sanitize: true, useViewBoxForPositioning: true });
399
+ const { element: canvas, renderer: canvasRenderer } = CanvasRenderer.fromViewport(exportViewport, { maxCanvasDimen: 4096 });
400
+ const text = [];
401
+ for (const elem of selectedElems) {
402
+ elem.render(svgRenderer);
403
+ elem.render(canvasRenderer);
404
+ if (elem instanceof TextComponent) {
405
+ text.push(elem.getText());
406
+ }
407
+ }
408
+ event.setData('image/svg+xml', svgExportElem.outerHTML);
409
+ event.setData('text/html', svgExportElem.outerHTML);
410
+ event.setData('image/png', new Promise((resolve, reject) => {
411
+ canvas.toBlob((blob) => {
412
+ if (blob) {
413
+ resolve(blob);
414
+ }
415
+ else {
416
+ reject(new Error('Failed to convert canvas to blob.'));
417
+ }
418
+ }, 'image/png');
419
+ }));
420
+ if (text.length > 0) {
421
+ event.setData('text/plain', text.join('\n'));
422
+ }
423
+ return true;
424
+ }
425
+ setEnabled(enabled) {
426
+ const wasEnabled = this.isEnabled();
427
+ super.setEnabled(enabled);
428
+ if (wasEnabled === enabled) {
429
+ return;
430
+ }
431
+ // Clear the selection
432
+ this.selectionBox?.cancelSelection();
433
+ this.onSelectionUpdated();
434
+ this.handleOverlay.replaceChildren();
435
+ this.selectionBox = null;
436
+ this.shiftKeyPressed = false;
437
+ this.snapToGrid = false;
438
+ this.handleOverlay.style.display = enabled ? 'block' : 'none';
439
+ if (enabled) {
440
+ this.handleOverlay.tabIndex = 0;
441
+ this.handleOverlay.setAttribute('aria-label', this.editor.localization.selectionToolKeyboardShortcuts);
442
+ }
443
+ else {
444
+ this.handleOverlay.tabIndex = -1;
445
+ }
446
+ }
447
+ // Get the object responsible for displaying this' selection.
448
+ // @internal
449
+ getSelection() {
450
+ return this.selectionBox;
451
+ }
452
+ getSelectedObjects() {
453
+ return this.selectionBox?.getSelectedObjects() ?? [];
454
+ }
455
+ // Select the given `objects`. Any non-selectable objects in `objects` are ignored.
456
+ setSelection(objects) {
457
+ // Only select selectable objects.
458
+ objects = objects.filter(obj => obj.isSelectable());
459
+ // Sort by z-index
460
+ objects.sort((a, b) => a.getZIndex() - b.getZIndex());
461
+ // Remove duplicates
462
+ objects = objects.filter((current, idx) => {
463
+ if (idx > 0) {
464
+ return current !== objects[idx - 1];
465
+ }
466
+ return true;
467
+ });
468
+ let bbox = null;
469
+ for (const object of objects) {
470
+ if (bbox) {
471
+ bbox = bbox.union(object.getBBox());
472
+ }
473
+ else {
474
+ bbox = object.getBBox();
475
+ }
476
+ }
477
+ if (!bbox) {
478
+ return;
479
+ }
480
+ this.clearSelection();
481
+ if (!this.selectionBox) {
482
+ this.makeSelectionBox(bbox.topLeft);
483
+ }
484
+ this.selectionBox.setSelectedObjects(objects, bbox);
485
+ this.onSelectionUpdated();
486
+ }
487
+ clearSelection() {
488
+ this.handleOverlay.replaceChildren();
489
+ this.prevSelectionBox = this.selectionBox;
490
+ this.selectionBox = null;
491
+ this.onSelectionUpdated();
492
+ }
493
+ }
@@ -0,0 +1,23 @@
1
+ import { Point2, Vec2 } from '@js-draw/math';
2
+ import Viewport from '../../Viewport';
3
+ type ScrollByCallback = (delta: Vec2) => void;
4
+ /**
5
+ * Automatically scrolls the viewport such that the user's pointer is visible.
6
+ */
7
+ export default class ToPointerAutoscroller {
8
+ private viewport;
9
+ private scrollByCanvasDelta;
10
+ private started;
11
+ private updateLoopId;
12
+ private updateLoopRunning;
13
+ private targetPoint;
14
+ private scrollRate;
15
+ constructor(viewport: Viewport, scrollByCanvasDelta: ScrollByCallback);
16
+ private getScrollForPoint;
17
+ start(): void;
18
+ onPointerMove(pointerScreenPosition: Point2): void;
19
+ stop(): void;
20
+ private startUpdateLoop;
21
+ private stopUpdateLoop;
22
+ }
23
+ export {};
@@ -0,0 +1,77 @@
1
+ import { Rect2, Vec2 } from '@js-draw/math';
2
+ import untilNextAnimationFrame from '../../util/untilNextAnimationFrame.mjs';
3
+ /**
4
+ * Automatically scrolls the viewport such that the user's pointer is visible.
5
+ */
6
+ export default class ToPointerAutoscroller {
7
+ constructor(viewport, scrollByCanvasDelta) {
8
+ this.viewport = viewport;
9
+ this.scrollByCanvasDelta = scrollByCanvasDelta;
10
+ this.started = false;
11
+ this.updateLoopId = 0;
12
+ this.updateLoopRunning = false;
13
+ this.targetPoint = null;
14
+ this.scrollRate = 1000; // px/s
15
+ }
16
+ getScrollForPoint(screenPoint) {
17
+ const screenSize = this.viewport.getScreenRectSize();
18
+ const screenRect = new Rect2(0, 0, screenSize.x, screenSize.y);
19
+ // Starts autoscrolling when the cursor is **outside of** this region
20
+ const marginSize = 44;
21
+ const autoscrollBoundary = screenRect.grownBy(-marginSize);
22
+ if (autoscrollBoundary.containsPoint(screenPoint)) {
23
+ return Vec2.zero;
24
+ }
25
+ const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
26
+ const distToEdge = closestEdgePoint.distanceTo(screenPoint);
27
+ const toEdge = closestEdgePoint.minus(screenPoint);
28
+ // Go faster for points further away from the boundary.
29
+ const maximumScaleFactor = 1.25;
30
+ const scaleFactor = Math.min(distToEdge / marginSize, maximumScaleFactor);
31
+ return toEdge.normalizedOrZero().times(scaleFactor);
32
+ }
33
+ start() {
34
+ this.started = true;
35
+ }
36
+ onPointerMove(pointerScreenPosition) {
37
+ if (!this.started) {
38
+ return;
39
+ }
40
+ if (this.getScrollForPoint(pointerScreenPosition) === Vec2.zero) {
41
+ this.stopUpdateLoop();
42
+ }
43
+ else {
44
+ this.targetPoint = pointerScreenPosition;
45
+ this.startUpdateLoop();
46
+ }
47
+ }
48
+ stop() {
49
+ this.targetPoint = null;
50
+ this.started = false;
51
+ this.stopUpdateLoop();
52
+ }
53
+ startUpdateLoop() {
54
+ if (this.updateLoopRunning) {
55
+ return;
56
+ }
57
+ (async () => {
58
+ this.updateLoopId++;
59
+ const currentUpdateLoopId = this.updateLoopId;
60
+ let lastUpdateTime = performance.now();
61
+ while (this.updateLoopId === currentUpdateLoopId && this.targetPoint) {
62
+ this.updateLoopRunning = true;
63
+ const currentTime = performance.now();
64
+ const deltaTimeMs = currentTime - lastUpdateTime;
65
+ const scrollDirection = this.getScrollForPoint(this.targetPoint);
66
+ const screenScrollAmount = scrollDirection.times(this.scrollRate * deltaTimeMs / 1000);
67
+ this.scrollByCanvasDelta(this.viewport.screenToCanvasTransform.transformVec3(screenScrollAmount));
68
+ lastUpdateTime = currentTime;
69
+ await untilNextAnimationFrame();
70
+ }
71
+ this.updateLoopRunning = false;
72
+ })();
73
+ }
74
+ stopUpdateLoop() {
75
+ this.updateLoopId++;
76
+ }
77
+ }
@@ -0,0 +1,42 @@
1
+ import Editor from '../../Editor';
2
+ import { Vec3 } from '@js-draw/math';
3
+ import Selection from './Selection';
4
+ import { ResizeMode } from './types';
5
+ export declare class DragTransformer {
6
+ private readonly editor;
7
+ private selection;
8
+ private dragStartPoint;
9
+ constructor(editor: Editor, selection: Selection);
10
+ onDragStart(startPoint: Vec3): void;
11
+ onDragUpdate(canvasPos: Vec3): void;
12
+ onDragEnd(): void | Promise<void>;
13
+ }
14
+ export declare class ResizeTransformer {
15
+ private readonly editor;
16
+ private selection;
17
+ private mode;
18
+ private dragStartPoint;
19
+ private transformOrigin;
20
+ private scaleRate;
21
+ constructor(editor: Editor, selection: Selection);
22
+ onDragStart(startPoint: Vec3, mode: ResizeMode): void;
23
+ private computeOriginAndScaleRate;
24
+ onDragUpdate(canvasPos: Vec3): void;
25
+ onDragEnd(): void | Promise<void>;
26
+ }
27
+ export declare class RotateTransformer {
28
+ private readonly editor;
29
+ private selection;
30
+ private startAngle;
31
+ private targetRotation;
32
+ private maximumDistFromStart;
33
+ private startPoint;
34
+ private startTime;
35
+ constructor(editor: Editor, selection: Selection);
36
+ private getAngle;
37
+ private roundAngle;
38
+ onDragStart(startPoint: Vec3): void;
39
+ private setRotationTo;
40
+ onDragUpdate(canvasPos: Vec3): void;
41
+ onDragEnd(): void | Promise<void>;
42
+ }