js-draw 1.21.1 → 1.21.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (187) hide show
  1. package/dist/Editor.css +4 -2
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +1 -1
  5. package/dist/cjs/tools/BaseTool.d.ts +61 -0
  6. package/dist/cjs/tools/BaseTool.js +179 -0
  7. package/dist/cjs/tools/Eraser.d.ts +60 -0
  8. package/dist/cjs/tools/Eraser.js +299 -0
  9. package/dist/cjs/tools/Eraser.test.d.ts +1 -0
  10. package/dist/cjs/tools/FindTool.d.ts +21 -0
  11. package/dist/cjs/tools/FindTool.js +137 -0
  12. package/dist/cjs/tools/FindTool.test.d.ts +1 -0
  13. package/dist/cjs/tools/InputFilter/ContextMenuRecognizer.d.ts +17 -0
  14. package/dist/cjs/tools/InputFilter/ContextMenuRecognizer.js +105 -0
  15. package/dist/cjs/tools/InputFilter/ContextMenuRecognizer.test.d.ts +1 -0
  16. package/dist/cjs/tools/InputFilter/FunctionMapper.d.ts +12 -0
  17. package/dist/cjs/tools/InputFilter/FunctionMapper.js +21 -0
  18. package/dist/cjs/tools/InputFilter/InputMapper.d.ts +23 -0
  19. package/dist/cjs/tools/InputFilter/InputMapper.js +38 -0
  20. package/dist/cjs/tools/InputFilter/InputPipeline.d.ts +15 -0
  21. package/dist/cjs/tools/InputFilter/InputPipeline.js +54 -0
  22. package/dist/cjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  23. package/dist/cjs/tools/InputFilter/InputStabilizer.d.ts +29 -0
  24. package/dist/cjs/tools/InputFilter/InputStabilizer.js +181 -0
  25. package/dist/cjs/tools/InputFilter/StrokeKeyboardControl.d.ts +21 -0
  26. package/dist/cjs/tools/InputFilter/StrokeKeyboardControl.js +84 -0
  27. package/dist/cjs/tools/PanZoom.d.ts +125 -0
  28. package/dist/cjs/tools/PanZoom.js +517 -0
  29. package/dist/cjs/tools/PanZoom.test.d.ts +1 -0
  30. package/dist/cjs/tools/PasteHandler.d.ts +23 -0
  31. package/dist/cjs/tools/PasteHandler.js +109 -0
  32. package/dist/cjs/tools/Pen.d.ts +54 -0
  33. package/dist/cjs/tools/Pen.js +335 -0
  34. package/dist/cjs/tools/Pen.test.d.ts +1 -0
  35. package/dist/cjs/tools/PipetteTool.d.ts +28 -0
  36. package/dist/cjs/tools/PipetteTool.js +69 -0
  37. package/dist/cjs/tools/ScrollbarTool.d.ts +18 -0
  38. package/dist/cjs/tools/ScrollbarTool.js +85 -0
  39. package/dist/cjs/tools/SelectionTool/SelectAllShortcutHandler.d.ts +9 -0
  40. package/dist/cjs/tools/SelectionTool/SelectAllShortcutHandler.js +32 -0
  41. package/dist/cjs/tools/SelectionTool/Selection.d.ts +72 -0
  42. package/dist/cjs/tools/SelectionTool/Selection.js +634 -0
  43. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +62 -0
  44. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +141 -0
  45. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +32 -0
  46. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.js +86 -0
  47. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +42 -0
  48. package/dist/cjs/tools/SelectionTool/SelectionTool.js +500 -0
  49. package/dist/cjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  50. package/dist/cjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  51. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  52. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
  53. package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +42 -0
  54. package/dist/cjs/tools/SelectionTool/TransformMode.js +155 -0
  55. package/dist/cjs/tools/SelectionTool/keybindings.d.ts +15 -0
  56. package/dist/cjs/tools/SelectionTool/keybindings.js +38 -0
  57. package/dist/cjs/tools/SelectionTool/types.d.ts +35 -0
  58. package/dist/cjs/tools/SelectionTool/types.js +14 -0
  59. package/dist/cjs/tools/SelectionTool/util/makeClipboardErrorHandlers.d.ts +6 -0
  60. package/dist/cjs/tools/SelectionTool/util/makeClipboardErrorHandlers.js +50 -0
  61. package/dist/cjs/tools/SelectionTool/util/showSelectionContextMenu.d.ts +5 -0
  62. package/dist/cjs/tools/SelectionTool/util/showSelectionContextMenu.js +43 -0
  63. package/dist/cjs/tools/SoundUITool.d.ts +26 -0
  64. package/dist/cjs/tools/SoundUITool.js +171 -0
  65. package/dist/cjs/tools/TextTool.d.ts +36 -0
  66. package/dist/cjs/tools/TextTool.js +285 -0
  67. package/dist/cjs/tools/TextTool.test.d.ts +1 -0
  68. package/dist/cjs/tools/ToolController.d.ts +73 -0
  69. package/dist/cjs/tools/ToolController.js +304 -0
  70. package/dist/cjs/tools/ToolController.test.d.ts +1 -0
  71. package/dist/cjs/tools/ToolEnabledGroup.d.ts +6 -0
  72. package/dist/cjs/tools/ToolEnabledGroup.js +13 -0
  73. package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +16 -0
  74. package/dist/cjs/tools/ToolSwitcherShortcut.js +40 -0
  75. package/dist/cjs/tools/ToolbarShortcutHandler.d.ts +12 -0
  76. package/dist/cjs/tools/ToolbarShortcutHandler.js +34 -0
  77. package/dist/cjs/tools/UndoRedoShortcut.d.ts +8 -0
  78. package/dist/cjs/tools/UndoRedoShortcut.js +27 -0
  79. package/dist/cjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  80. package/dist/cjs/tools/keybindings.d.ts +16 -0
  81. package/dist/cjs/tools/keybindings.js +58 -0
  82. package/dist/cjs/tools/lib.d.ts +14 -0
  83. package/dist/cjs/tools/lib.js +36 -0
  84. package/dist/cjs/tools/localization.d.ts +43 -0
  85. package/dist/cjs/tools/localization.js +45 -0
  86. package/dist/cjs/tools/util/StationaryPenDetector.d.ts +25 -0
  87. package/dist/cjs/tools/util/StationaryPenDetector.js +107 -0
  88. package/dist/cjs/tools/util/createMenuOverlay.d.ts +10 -0
  89. package/dist/cjs/tools/util/createMenuOverlay.js +126 -0
  90. package/dist/cjs/tools/util/createMenuOverlay.test.d.ts +1 -0
  91. package/dist/cjs/version.js +1 -1
  92. package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +1 -1
  93. package/dist/mjs/tools/BaseTool.d.ts +61 -0
  94. package/dist/mjs/tools/BaseTool.mjs +177 -0
  95. package/dist/mjs/tools/Eraser.d.ts +60 -0
  96. package/dist/mjs/tools/Eraser.mjs +292 -0
  97. package/dist/mjs/tools/Eraser.test.d.ts +1 -0
  98. package/dist/mjs/tools/FindTool.d.ts +21 -0
  99. package/dist/mjs/tools/FindTool.mjs +131 -0
  100. package/dist/mjs/tools/FindTool.test.d.ts +1 -0
  101. package/dist/mjs/tools/InputFilter/ContextMenuRecognizer.d.ts +17 -0
  102. package/dist/mjs/tools/InputFilter/ContextMenuRecognizer.mjs +76 -0
  103. package/dist/mjs/tools/InputFilter/ContextMenuRecognizer.test.d.ts +1 -0
  104. package/dist/mjs/tools/InputFilter/FunctionMapper.d.ts +12 -0
  105. package/dist/mjs/tools/InputFilter/FunctionMapper.mjs +15 -0
  106. package/dist/mjs/tools/InputFilter/InputMapper.d.ts +23 -0
  107. package/dist/mjs/tools/InputFilter/InputMapper.mjs +36 -0
  108. package/dist/mjs/tools/InputFilter/InputPipeline.d.ts +15 -0
  109. package/dist/mjs/tools/InputFilter/InputPipeline.mjs +49 -0
  110. package/dist/mjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  111. package/dist/mjs/tools/InputFilter/InputStabilizer.d.ts +29 -0
  112. package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +175 -0
  113. package/dist/mjs/tools/InputFilter/StrokeKeyboardControl.d.ts +21 -0
  114. package/dist/mjs/tools/InputFilter/StrokeKeyboardControl.mjs +78 -0
  115. package/dist/mjs/tools/PanZoom.d.ts +125 -0
  116. package/dist/mjs/tools/PanZoom.mjs +510 -0
  117. package/dist/mjs/tools/PanZoom.test.d.ts +1 -0
  118. package/dist/mjs/tools/PasteHandler.d.ts +23 -0
  119. package/dist/mjs/tools/PasteHandler.mjs +103 -0
  120. package/dist/mjs/tools/Pen.d.ts +54 -0
  121. package/dist/mjs/tools/Pen.mjs +306 -0
  122. package/dist/mjs/tools/Pen.test.d.ts +1 -0
  123. package/dist/mjs/tools/PipetteTool.d.ts +28 -0
  124. package/dist/mjs/tools/PipetteTool.mjs +63 -0
  125. package/dist/mjs/tools/ScrollbarTool.d.ts +18 -0
  126. package/dist/mjs/tools/ScrollbarTool.mjs +79 -0
  127. package/dist/mjs/tools/SelectionTool/SelectAllShortcutHandler.d.ts +9 -0
  128. package/dist/mjs/tools/SelectionTool/SelectAllShortcutHandler.mjs +26 -0
  129. package/dist/mjs/tools/SelectionTool/Selection.d.ts +72 -0
  130. package/dist/mjs/tools/SelectionTool/Selection.mjs +606 -0
  131. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +62 -0
  132. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +137 -0
  133. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +32 -0
  134. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.mjs +83 -0
  135. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +42 -0
  136. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +493 -0
  137. package/dist/mjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  138. package/dist/mjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  139. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  140. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
  141. package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +42 -0
  142. package/dist/mjs/tools/SelectionTool/TransformMode.mjs +146 -0
  143. package/dist/mjs/tools/SelectionTool/keybindings.d.ts +15 -0
  144. package/dist/mjs/tools/SelectionTool/keybindings.mjs +32 -0
  145. package/dist/mjs/tools/SelectionTool/types.d.ts +35 -0
  146. package/dist/mjs/tools/SelectionTool/types.mjs +11 -0
  147. package/dist/mjs/tools/SelectionTool/util/makeClipboardErrorHandlers.d.ts +6 -0
  148. package/dist/mjs/tools/SelectionTool/util/makeClipboardErrorHandlers.mjs +45 -0
  149. package/dist/mjs/tools/SelectionTool/util/showSelectionContextMenu.d.ts +5 -0
  150. package/dist/mjs/tools/SelectionTool/util/showSelectionContextMenu.mjs +38 -0
  151. package/dist/mjs/tools/SoundUITool.d.ts +26 -0
  152. package/dist/mjs/tools/SoundUITool.mjs +165 -0
  153. package/dist/mjs/tools/TextTool.d.ts +36 -0
  154. package/dist/mjs/tools/TextTool.mjs +279 -0
  155. package/dist/mjs/tools/TextTool.test.d.ts +1 -0
  156. package/dist/mjs/tools/ToolController.d.ts +73 -0
  157. package/dist/mjs/tools/ToolController.mjs +275 -0
  158. package/dist/mjs/tools/ToolController.test.d.ts +1 -0
  159. package/dist/mjs/tools/ToolEnabledGroup.d.ts +6 -0
  160. package/dist/mjs/tools/ToolEnabledGroup.mjs +10 -0
  161. package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +16 -0
  162. package/dist/mjs/tools/ToolSwitcherShortcut.mjs +34 -0
  163. package/dist/mjs/tools/ToolbarShortcutHandler.d.ts +12 -0
  164. package/dist/mjs/tools/ToolbarShortcutHandler.mjs +28 -0
  165. package/dist/mjs/tools/UndoRedoShortcut.d.ts +8 -0
  166. package/dist/mjs/tools/UndoRedoShortcut.mjs +21 -0
  167. package/dist/mjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  168. package/dist/mjs/tools/keybindings.d.ts +16 -0
  169. package/dist/mjs/tools/keybindings.mjs +38 -0
  170. package/dist/mjs/tools/lib.d.ts +14 -0
  171. package/dist/mjs/tools/lib.mjs +14 -0
  172. package/dist/mjs/tools/localization.d.ts +43 -0
  173. package/dist/mjs/tools/localization.mjs +42 -0
  174. package/dist/mjs/tools/util/StationaryPenDetector.d.ts +25 -0
  175. package/dist/mjs/tools/util/StationaryPenDetector.mjs +103 -0
  176. package/dist/mjs/tools/util/createMenuOverlay.d.ts +10 -0
  177. package/dist/mjs/tools/util/createMenuOverlay.mjs +121 -0
  178. package/dist/mjs/tools/util/createMenuOverlay.test.d.ts +1 -0
  179. package/dist/mjs/version.mjs +1 -1
  180. package/package.json +4 -4
  181. package/src/tools/FindTool.css +7 -0
  182. package/src/tools/ScrollbarTool.scss +57 -0
  183. package/src/tools/SelectionTool/SelectionTool.scss +165 -0
  184. package/src/tools/SelectionTool/util/makeClipboardErrorHandlers.scss +15 -0
  185. package/src/tools/SoundUITool.scss +22 -0
  186. package/src/tools/tools.scss +6 -0
  187. package/src/tools/util/createMenuOverlay.scss +67 -0
@@ -0,0 +1,510 @@
1
+ import { Mat33, Vec3, Vec2 } from '@js-draw/math';
2
+ import { PointerDevice } from '../Pointer.mjs';
3
+ import { EditorEventType } from '../types.mjs';
4
+ import untilNextAnimationFrame from '../util/untilNextAnimationFrame.mjs';
5
+ import { Viewport } from '../Viewport.mjs';
6
+ import BaseTool from './BaseTool.mjs';
7
+ import { moveDownKeyboardShortcutId, moveLeftKeyboardShortcutId, moveRightKeyboardShortcutId, moveUpKeyboardShortcutId, rotateClockwiseKeyboardShortcutId, rotateCounterClockwiseKeyboardShortcutId, zoomInKeyboardShortcutId, zoomOutKeyboardShortcutId } from './keybindings.mjs';
8
+ export var PanZoomMode;
9
+ (function (PanZoomMode) {
10
+ /** Touch gestures with a single pointer. Ignores non-touch gestures. */
11
+ PanZoomMode[PanZoomMode["OneFingerTouchGestures"] = 1] = "OneFingerTouchGestures";
12
+ /** Touch gestures with exactly two pointers. Ignores non-touch gestures. */
13
+ PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
14
+ PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
15
+ /** Single-pointer gestures of *any* type (including touch). */
16
+ PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
17
+ /** Keyboard navigation (e.g. LeftArrow to move left). */
18
+ PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard";
19
+ /** If provided, prevents **this** tool from rotating the viewport (other tools may still do so). */
20
+ PanZoomMode[PanZoomMode["RotationLocked"] = 32] = "RotationLocked";
21
+ })(PanZoomMode || (PanZoomMode = {}));
22
+ class InertialScroller {
23
+ constructor(initialVelocity, scrollBy, onComplete) {
24
+ this.initialVelocity = initialVelocity;
25
+ this.scrollBy = scrollBy;
26
+ this.onComplete = onComplete;
27
+ this.running = false;
28
+ this.start();
29
+ }
30
+ async start() {
31
+ if (this.running) {
32
+ return;
33
+ }
34
+ this.currentVelocity = this.initialVelocity;
35
+ let lastTime = performance.now();
36
+ this.running = true;
37
+ const maxSpeed = 5000; // units/s
38
+ const minSpeed = 200; // units/s
39
+ if (this.currentVelocity.magnitude() > maxSpeed) {
40
+ this.currentVelocity = this.currentVelocity.normalized().times(maxSpeed);
41
+ }
42
+ while (this.running && this.currentVelocity.magnitude() > minSpeed) {
43
+ const nowTime = performance.now();
44
+ const dt = (nowTime - lastTime) / 1000;
45
+ this.currentVelocity = this.currentVelocity.times(Math.pow(1 / 8, dt));
46
+ this.scrollBy(this.currentVelocity.times(dt));
47
+ await untilNextAnimationFrame();
48
+ lastTime = nowTime;
49
+ }
50
+ if (this.running) {
51
+ this.stop();
52
+ }
53
+ }
54
+ getCurrentVelocity() {
55
+ if (!this.running) {
56
+ return null;
57
+ }
58
+ return this.currentVelocity;
59
+ }
60
+ stop() {
61
+ if (this.running) {
62
+ this.running = false;
63
+ this.onComplete();
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * This tool moves the viewport in response to touchpad, touchscreen, mouse, and keyboard events.
69
+ *
70
+ * Which events are handled, and which are skipped, are determined by the tool's `mode`. For example,
71
+ * a `PanZoom` tool with `mode = PanZoomMode.TwoFingerTouchGestures|PanZoomMode.RightClickDrags` would
72
+ * respond to right-click drag events and two-finger touch gestures.
73
+ *
74
+ * @see {@link setModeEnabled}
75
+ */
76
+ export default class PanZoom extends BaseTool {
77
+ constructor(editor, mode, description) {
78
+ super(editor.notifier, description);
79
+ this.editor = editor;
80
+ this.mode = mode;
81
+ this.transform = null;
82
+ // Constants
83
+ // initialRotationSnapAngle is larger than afterRotationStartSnapAngle to
84
+ // make it more difficult to start rotating (and easier to continue rotating).
85
+ this.initialRotationSnapAngle = 0.22; // radians
86
+ this.afterRotationStartSnapAngle = 0.07; // radians
87
+ this.pinchZoomStartThreshold = 1.08; // scale factor
88
+ // Last timestamp at which a pointerdown event was received
89
+ this.lastPointerDownTimestamp = 0;
90
+ this.initialTouchAngle = 0;
91
+ this.initialViewportRotation = 0;
92
+ this.initialViewportScale = 0;
93
+ // Set to `true` only when scaling has started (if two fingers are down and have moved
94
+ // far enough).
95
+ this.isScaling = false;
96
+ this.isRotating = false;
97
+ this.inertialScroller = null;
98
+ this.velocity = null;
99
+ }
100
+ // The pan/zoom tool can be used in a read-only editor.
101
+ canReceiveInputInReadOnlyEditor() {
102
+ return true;
103
+ }
104
+ // Returns information about the pointers in a gesture
105
+ computePinchData(p1, p2) {
106
+ // Swap the pointers to ensure consistent ordering.
107
+ if (p1.id < p2.id) {
108
+ const tmp = p1;
109
+ p1 = p2;
110
+ p2 = tmp;
111
+ }
112
+ const screenBetween = p2.screenPos.minus(p1.screenPos);
113
+ const angle = screenBetween.angle();
114
+ const dist = screenBetween.magnitude();
115
+ const canvasCenter = p2.canvasPos.plus(p1.canvasPos).times(0.5);
116
+ const screenCenter = p2.screenPos.plus(p1.screenPos).times(0.5);
117
+ return { canvasCenter, screenCenter, angle, dist };
118
+ }
119
+ allPointersAreOfType(pointers, kind) {
120
+ return pointers.every(pointer => pointer.device === kind);
121
+ }
122
+ onPointerDown({ allPointers: pointers, current: currentPointer }) {
123
+ let handlingGesture = false;
124
+ const inertialScrollerVelocity = this.inertialScroller?.getCurrentVelocity() ?? Vec2.zero;
125
+ this.inertialScroller?.stop();
126
+ this.velocity = inertialScrollerVelocity;
127
+ this.lastPointerDownTimestamp = currentPointer.timeStamp;
128
+ const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
129
+ const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
130
+ if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
131
+ const { screenCenter, angle, dist } = this.computePinchData(pointers[0], pointers[1]);
132
+ this.lastTouchDist = dist;
133
+ this.startTouchDist = dist;
134
+ this.lastScreenCenter = screenCenter;
135
+ this.initialTouchAngle = angle;
136
+ this.initialViewportRotation = this.editor.viewport.getRotationAngle();
137
+ this.initialViewportScale = this.editor.viewport.getScaleFactor();
138
+ this.isScaling = false;
139
+ // We're initially rotated if `initialViewportRotation` isn't near a multiple of pi/2.
140
+ // In other words, if sin(2 initialViewportRotation) is near zero.
141
+ this.isRotating = Math.abs(Math.sin(this.initialViewportRotation * 2)) > 1e-3;
142
+ handlingGesture = true;
143
+ }
144
+ else if (pointers.length === 1 && ((this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
145
+ || (isRightClick && this.mode & PanZoomMode.RightClickDrags)
146
+ || (this.mode & PanZoomMode.SinglePointerGestures))) {
147
+ this.lastScreenCenter = pointers[0].screenPos;
148
+ this.isScaling = false;
149
+ handlingGesture = true;
150
+ }
151
+ if (handlingGesture) {
152
+ this.lastTimestamp = performance.now();
153
+ this.transform ??= Viewport.transformBy(Mat33.identity);
154
+ this.editor.display.setDraftMode(true);
155
+ }
156
+ return handlingGesture;
157
+ }
158
+ updateVelocity(currentCenter) {
159
+ const deltaPos = currentCenter.minus(this.lastScreenCenter);
160
+ let deltaTime = (performance.now() - this.lastTimestamp) / 1000;
161
+ // Ignore duplicate events, unless there has been enough time between them.
162
+ if (deltaPos.magnitude() === 0 && deltaTime < 0.1) {
163
+ return;
164
+ }
165
+ // We divide by deltaTime. Don't divide by zero.
166
+ if (deltaTime === 0) {
167
+ return;
168
+ }
169
+ // Don't divide by almost zero, either
170
+ deltaTime = Math.max(deltaTime, 0.01);
171
+ const currentVelocity = deltaPos.times(1 / deltaTime);
172
+ let smoothedVelocity = currentVelocity;
173
+ if (this.velocity) {
174
+ smoothedVelocity = this.velocity.lerp(currentVelocity, 0.5);
175
+ }
176
+ this.velocity = smoothedVelocity;
177
+ }
178
+ // Returns the change in position of the center of the given group of pointers.
179
+ // Assumes this.lastScreenCenter has been set appropriately.
180
+ getCenterDelta(screenCenter) {
181
+ // Use transformVec3 to avoid translating the delta
182
+ const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenCenter.minus(this.lastScreenCenter));
183
+ return delta;
184
+ }
185
+ // Snaps `angle` to common desired rotations. For example, if `touchAngle` corresponds
186
+ // to a viewport rotation of 90.1 degrees, this function returns a rotation delta that,
187
+ // when applied to the viewport, rotates the viewport to 90.0 degrees.
188
+ //
189
+ // Returns a snapped rotation delta that, when applied to the viewport, rotates the viewport,
190
+ // from its position on the last touchDown event, by `touchAngle - initialTouchAngle`.
191
+ toSnappedRotationDelta(touchAngle) {
192
+ const deltaAngle = touchAngle - this.initialTouchAngle;
193
+ let fullRotation = deltaAngle + this.initialViewportRotation;
194
+ const snapToMultipleOf = Math.PI / 2;
195
+ const roundedFullRotation = Math.round(fullRotation / snapToMultipleOf) * snapToMultipleOf;
196
+ // The maximum angle for which we snap the given angle to a multiple of
197
+ // `snapToMultipleOf`.
198
+ // Use a smaller snap angle if already rotated (to avoid pinch zoom gestures from
199
+ // starting rotation).
200
+ const maxSnapAngle = this.isRotating ? this.afterRotationStartSnapAngle : this.initialRotationSnapAngle;
201
+ // Snap the rotation
202
+ if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
203
+ fullRotation = roundedFullRotation;
204
+ // Work around a rotation/matrix multiply bug.
205
+ // (See commit after 4abe27ff8e7913155828f98dee77b09c57c51d30).
206
+ // TODO: Fix the underlying issue and remove this.
207
+ if (fullRotation !== 0) {
208
+ fullRotation += 0.0001;
209
+ }
210
+ }
211
+ return fullRotation - this.editor.viewport.getRotationAngle();
212
+ }
213
+ /**
214
+ * Given a scale update, `scaleFactor`, returns a new scale factor snapped
215
+ * to a power of two (if within some tolerance of that scale).
216
+ */
217
+ toSnappedScaleFactor(touchDist) {
218
+ // scaleFactor is applied to the current transformation of the viewport.
219
+ const newScale = this.initialViewportScale * touchDist / this.startTouchDist;
220
+ const currentScale = this.editor.viewport.getScaleFactor();
221
+ const logNewScale = Math.log(newScale) / Math.log(10);
222
+ const roundedLogNewScale = Math.round(logNewScale);
223
+ const logTolerance = 0.04;
224
+ if (Math.abs(roundedLogNewScale - logNewScale) < logTolerance) {
225
+ return Math.pow(10, roundedLogNewScale) / currentScale;
226
+ }
227
+ return touchDist / this.lastTouchDist;
228
+ }
229
+ handleTwoFingerMove(allPointers) {
230
+ const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
231
+ const delta = this.getCenterDelta(screenCenter);
232
+ let deltaRotation;
233
+ if (this.isRotationLocked()) {
234
+ deltaRotation = 0;
235
+ }
236
+ else {
237
+ deltaRotation = this.toSnappedRotationDelta(angle);
238
+ }
239
+ // If any rotation, make a note of this (affects rotation snap
240
+ // angles).
241
+ if (Math.abs(deltaRotation) > 1e-8) {
242
+ this.isRotating = true;
243
+ }
244
+ this.updateVelocity(screenCenter);
245
+ if (!this.isScaling) {
246
+ const initialScaleFactor = dist / this.startTouchDist;
247
+ // Only start scaling if scaling done so far exceeds some threshold.
248
+ const upperBound = this.pinchZoomStartThreshold;
249
+ const lowerBound = 1 / this.pinchZoomStartThreshold;
250
+ if (initialScaleFactor > upperBound || initialScaleFactor < lowerBound) {
251
+ this.isScaling = true;
252
+ }
253
+ }
254
+ let scaleFactor = 1;
255
+ if (this.isScaling) {
256
+ scaleFactor = this.toSnappedScaleFactor(dist);
257
+ // Don't set lastDist until we start scaling --
258
+ this.lastTouchDist = dist;
259
+ }
260
+ const transformUpdate = Mat33.translation(delta)
261
+ .rightMul(Mat33.scaling2D(scaleFactor, canvasCenter))
262
+ .rightMul(Mat33.zRotation(deltaRotation, canvasCenter));
263
+ this.lastScreenCenter = screenCenter;
264
+ this.transform = Viewport.transformBy(this.transform.transform.rightMul(transformUpdate));
265
+ return transformUpdate;
266
+ }
267
+ handleOneFingerMove(pointer) {
268
+ const delta = this.getCenterDelta(pointer.screenPos);
269
+ const transformUpdate = Mat33.translation(delta);
270
+ this.transform = Viewport.transformBy(this.transform.transform.rightMul(transformUpdate));
271
+ this.updateVelocity(pointer.screenPos);
272
+ this.lastScreenCenter = pointer.screenPos;
273
+ return transformUpdate;
274
+ }
275
+ onPointerMove({ allPointers }) {
276
+ this.transform ??= Viewport.transformBy(Mat33.identity);
277
+ let transformUpdate = Mat33.identity;
278
+ if (allPointers.length === 2) {
279
+ transformUpdate = this.handleTwoFingerMove(allPointers);
280
+ }
281
+ else if (allPointers.length === 1) {
282
+ transformUpdate = this.handleOneFingerMove(allPointers[0]);
283
+ }
284
+ Viewport.transformBy(transformUpdate).apply(this.editor);
285
+ this.lastTimestamp = performance.now();
286
+ }
287
+ onPointerUp(event) {
288
+ const onComplete = () => {
289
+ if (this.transform) {
290
+ this.transform.unapply(this.editor);
291
+ this.editor.dispatch(this.transform, false);
292
+ }
293
+ this.editor.display.setDraftMode(false);
294
+ this.transform = null;
295
+ this.velocity = Vec2.zero;
296
+ };
297
+ const minInertialScrollDt = 30;
298
+ const shouldInertialScroll = event.current.device === PointerDevice.Touch
299
+ && event.allPointers.length === 1
300
+ && this.velocity !== null
301
+ && event.current.timeStamp - this.lastPointerDownTimestamp > minInertialScrollDt;
302
+ if (shouldInertialScroll && this.velocity !== null) {
303
+ const oldVelocity = this.velocity;
304
+ // If the user drags the screen, then stops, then lifts the pointer,
305
+ // we want the final velocity to reflect the stop at the end (so the velocity
306
+ // should be near zero). Handle this:
307
+ this.updateVelocity(event.current.screenPos);
308
+ // Work around an input issue. Some devices that disable the touchscreen when a stylus
309
+ // comes near the screen fire a touch-end event at the position of the stylus when a
310
+ // touch gesture is canceled. Because the stylus is often far away from the last touch,
311
+ // this causes a great displacement between the second-to-last (from the touchscreen) and
312
+ // last (from the pen that is now near the screen) events. Only allow velocity to decrease
313
+ // to work around this:
314
+ if (oldVelocity.magnitude() < this.velocity.magnitude()) {
315
+ this.velocity = oldVelocity;
316
+ }
317
+ // Cancel any ongoing inertial scrolling.
318
+ this.inertialScroller?.stop();
319
+ this.inertialScroller = new InertialScroller(this.velocity, (scrollDelta) => {
320
+ if (!this.transform) {
321
+ return;
322
+ }
323
+ const canvasDelta = this.editor.viewport.screenToCanvasTransform.transformVec3(scrollDelta);
324
+ // Scroll by scrollDelta
325
+ this.transform.unapply(this.editor);
326
+ this.transform = Viewport.transformBy(this.transform.transform.rightMul(Mat33.translation(canvasDelta)));
327
+ this.transform.apply(this.editor);
328
+ }, onComplete);
329
+ }
330
+ else {
331
+ onComplete();
332
+ }
333
+ }
334
+ onGestureCancel() {
335
+ this.inertialScroller?.stop();
336
+ this.velocity = Vec2.zero;
337
+ this.transform?.unapply(this.editor);
338
+ this.editor.display.setDraftMode(false);
339
+ this.transform = null;
340
+ }
341
+ // Applies [transformUpdate] to the editor. This stacks on top of the
342
+ // current transformation, if it exists.
343
+ updateTransform(transformUpdate, announce = false) {
344
+ let newTransform = transformUpdate;
345
+ if (this.transform) {
346
+ newTransform = this.transform.transform.rightMul(transformUpdate);
347
+ }
348
+ this.transform?.unapply(this.editor);
349
+ this.transform = Viewport.transformBy(newTransform);
350
+ this.transform.apply(this.editor);
351
+ if (announce) {
352
+ this.editor.announceForAccessibility(this.transform.description(this.editor, this.editor.localization));
353
+ }
354
+ }
355
+ /**
356
+ * Updates the current transform and clears it. Use this method for events that are not part of
357
+ * a larger gesture (i.e. have no start and end event). For example, this would be used for `onwheel`
358
+ * events, but not for `onpointer` events.
359
+ */
360
+ applyAndFinalizeTransform(transformUpdate) {
361
+ this.updateTransform(transformUpdate, true);
362
+ this.transform = null;
363
+ }
364
+ onWheel({ delta, screenPos }) {
365
+ this.inertialScroller?.stop();
366
+ // Reset the transformation -- wheel events are individual events, so we don't
367
+ // need to unapply/reapply.
368
+ this.transform = Viewport.transformBy(Mat33.identity);
369
+ const canvasPos = this.editor.viewport.screenToCanvas(screenPos);
370
+ const toCanvas = this.editor.viewport.screenToCanvasTransform;
371
+ // Transform without including translation
372
+ const translation = toCanvas.transformVec3(Vec3.of(-delta.x, -delta.y, 0));
373
+ let pinchAmount = delta.z;
374
+ // Clamp the magnitude of pinchAmount
375
+ pinchAmount = Math.atan(pinchAmount / 2) * 2;
376
+ const pinchZoomScaleFactor = 1.04;
377
+ const transformUpdate = Mat33.scaling2D(Math.max(0.4, Math.min(Math.pow(pinchZoomScaleFactor, -pinchAmount), 4)), canvasPos).rightMul(Mat33.translation(translation));
378
+ this.applyAndFinalizeTransform(transformUpdate);
379
+ return true;
380
+ }
381
+ onKeyPress(event) {
382
+ this.inertialScroller?.stop();
383
+ if (!(this.mode & PanZoomMode.Keyboard)) {
384
+ return false;
385
+ }
386
+ // No need to keep the same the transform for keyboard events.
387
+ this.transform = Viewport.transformBy(Mat33.identity);
388
+ let translation = Vec2.zero;
389
+ let scale = 1;
390
+ let rotation = 0;
391
+ // Keyboard shortcut handling
392
+ const shortcucts = this.editor.shortcuts;
393
+ if (shortcucts.matchesShortcut(moveLeftKeyboardShortcutId, event)) {
394
+ translation = Vec2.of(-1, 0);
395
+ }
396
+ else if (shortcucts.matchesShortcut(moveRightKeyboardShortcutId, event)) {
397
+ translation = Vec2.of(1, 0);
398
+ }
399
+ else if (shortcucts.matchesShortcut(moveUpKeyboardShortcutId, event)) {
400
+ translation = Vec2.of(0, -1);
401
+ }
402
+ else if (shortcucts.matchesShortcut(moveDownKeyboardShortcutId, event)) {
403
+ translation = Vec2.of(0, 1);
404
+ }
405
+ else if (shortcucts.matchesShortcut(zoomInKeyboardShortcutId, event)) {
406
+ scale = 1 / 2;
407
+ }
408
+ else if (shortcucts.matchesShortcut(zoomOutKeyboardShortcutId, event)) {
409
+ scale = 2;
410
+ }
411
+ else if (shortcucts.matchesShortcut(rotateClockwiseKeyboardShortcutId, event)) {
412
+ rotation = 1;
413
+ }
414
+ else if (shortcucts.matchesShortcut(rotateCounterClockwiseKeyboardShortcutId, event)) {
415
+ rotation = -1;
416
+ }
417
+ else {
418
+ return false;
419
+ }
420
+ // For each keypress,
421
+ translation = translation.times(30); // Move at most 30 units
422
+ rotation *= Math.PI / 8; // Rotate at least a sixteenth of a rotation
423
+ // Transform the canvas, not the viewport:
424
+ translation = translation.times(-1);
425
+ rotation = rotation * -1;
426
+ scale = 1 / scale;
427
+ // Work around an issue that seems to be related to rotation matricies losing precision on inversion.
428
+ // TODO: Figure out why and implement a better solution.
429
+ if (rotation !== 0) {
430
+ rotation += 0.0001;
431
+ }
432
+ if (this.isRotationLocked()) {
433
+ rotation = 0;
434
+ }
435
+ const toCanvas = this.editor.viewport.screenToCanvasTransform;
436
+ // Transform without translating (treat toCanvas as a linear instead of
437
+ // an affine transformation).
438
+ translation = toCanvas.transformVec3(translation);
439
+ // Rotate/scale about the center of the canvas
440
+ const transformCenter = this.editor.viewport.visibleRect.center;
441
+ const transformUpdate = Mat33.scaling2D(scale, transformCenter).rightMul(Mat33.zRotation(rotation, transformCenter)).rightMul(Mat33.translation(translation));
442
+ this.applyAndFinalizeTransform(transformUpdate);
443
+ return true;
444
+ }
445
+ isRotationLocked() {
446
+ return !!(this.mode & PanZoomMode.RotationLocked);
447
+ }
448
+ /**
449
+ * Changes the types of gestures used by this pan/zoom tool.
450
+ *
451
+ * @see {@link PanZoomMode} {@link setMode}
452
+ *
453
+ * @example
454
+ * ```ts,runnable
455
+ * import { Editor, PanZoomTool, PanZoomMode } from 'js-draw';
456
+ *
457
+ * const editor = new Editor(document.body);
458
+ *
459
+ * // By default, there are multiple PanZoom tools that handle different events.
460
+ * // This gets all PanZoomTools.
461
+ * const panZoomToolList = editor.toolController.getMatchingTools(PanZoomTool);
462
+ *
463
+ * // The first PanZoomTool is the highest priority -- by default,
464
+ * // this tool is responsible for handling multi-finger touch gestures.
465
+ * //
466
+ * // Lower-priority PanZoomTools handle one-finger touch gestures and
467
+ * // key-presses.
468
+ * const panZoomTool = panZoomToolList[0];
469
+ *
470
+ * // Lock rotation for multi-finger touch gestures.
471
+ * panZoomTool.setModeEnabled(PanZoomMode.RotationLocked, true);
472
+ * ```
473
+ */
474
+ setModeEnabled(mode, enabled) {
475
+ let newMode = this.mode;
476
+ if (enabled) {
477
+ newMode |= mode;
478
+ }
479
+ else {
480
+ newMode &= ~mode;
481
+ }
482
+ this.setMode(newMode);
483
+ }
484
+ /**
485
+ * Sets all modes for this tool using a bitmask.
486
+ *
487
+ * @see {@link setModeEnabled}
488
+ *
489
+ * @example
490
+ * ```ts
491
+ * tool.setMode(PanZoomMode.RotationLocked|PanZoomMode.TwoFingerTouchGestures);
492
+ * ```
493
+ */
494
+ setMode(mode) {
495
+ if (mode !== this.mode) {
496
+ this.mode = mode;
497
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
498
+ kind: EditorEventType.ToolUpdated,
499
+ tool: this,
500
+ });
501
+ }
502
+ }
503
+ /**
504
+ * Returns a bitmask indicating the currently-enabled modes.
505
+ * @see {@link setModeEnabled}
506
+ */
507
+ getMode() {
508
+ return this.mode;
509
+ }
510
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import Editor from '../Editor';
2
+ import { PasteEvent } from '../inputEvents';
3
+ import BaseTool from './BaseTool';
4
+ /**
5
+ * A tool that handles paste events (e.g. as triggered by ctrl+V).
6
+ *
7
+ * @example
8
+ * While `ToolController` has a `PasteHandler` in its default list of tools,
9
+ * if a non-default set is being used, `PasteHandler` can be added as follows:
10
+ * ```ts
11
+ * const toolController = editor.toolController;
12
+ * toolController.addTool(new PasteHandler(editor));
13
+ * ```
14
+ */
15
+ export default class PasteHandler extends BaseTool {
16
+ private editor;
17
+ constructor(editor: Editor);
18
+ onPaste(event: PasteEvent): boolean;
19
+ private addComponentsFromPaste;
20
+ private doSVGPaste;
21
+ private doTextPaste;
22
+ private doImagePaste;
23
+ }
@@ -0,0 +1,103 @@
1
+ import TextComponent from '../components/TextComponent.mjs';
2
+ import SVGLoader from '../SVGLoader/SVGLoader.mjs';
3
+ import { Mat33, Color4 } from '@js-draw/math';
4
+ import BaseTool from './BaseTool.mjs';
5
+ import TextTool from './TextTool.mjs';
6
+ import ImageComponent from '../components/ImageComponent.mjs';
7
+ /**
8
+ * A tool that handles paste events (e.g. as triggered by ctrl+V).
9
+ *
10
+ * @example
11
+ * While `ToolController` has a `PasteHandler` in its default list of tools,
12
+ * if a non-default set is being used, `PasteHandler` can be added as follows:
13
+ * ```ts
14
+ * const toolController = editor.toolController;
15
+ * toolController.addTool(new PasteHandler(editor));
16
+ * ```
17
+ */
18
+ export default class PasteHandler extends BaseTool {
19
+ constructor(editor) {
20
+ super(editor.notifier, editor.localization.pasteHandler);
21
+ this.editor = editor;
22
+ }
23
+ // @internal
24
+ onPaste(event) {
25
+ const mime = event.mime.toLowerCase();
26
+ const svgData = (() => {
27
+ if (mime === 'image/svg+xml') {
28
+ return event.data;
29
+ }
30
+ if (mime !== 'text/html') {
31
+ return false;
32
+ }
33
+ // text/html is sometimes handlable SVG data. Use a hueristic
34
+ // to determine if this is the case:
35
+ // We use [^] and not . so that newlines are included.
36
+ const match = event.data.match(/^[^]{0,200}<svg.*/i); // [^]{0,200} <- Allow for metadata near start
37
+ if (!match) {
38
+ return false;
39
+ }
40
+ // Extract the SVG element from the pasted data
41
+ let svgEnd = event.data.toLowerCase().lastIndexOf('</svg>');
42
+ if (svgEnd === -1)
43
+ svgEnd = event.data.length;
44
+ return event.data.substring(event.data.search(/<svg/i), svgEnd);
45
+ })();
46
+ if (svgData) {
47
+ void this.doSVGPaste(svgData);
48
+ return true;
49
+ }
50
+ else if (mime === 'text/plain') {
51
+ void this.doTextPaste(event.data);
52
+ return true;
53
+ }
54
+ else if (mime === 'image/png' || mime === 'image/jpeg') {
55
+ void this.doImagePaste(event.data);
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ async addComponentsFromPaste(components) {
61
+ await this.editor.addAndCenterComponents(components, true, this.editor.localization.pasted(components.length));
62
+ }
63
+ async doSVGPaste(data) {
64
+ this.editor.showLoadingWarning(0);
65
+ try {
66
+ const loader = SVGLoader.fromString(data, true);
67
+ const components = [];
68
+ await loader.start((component) => {
69
+ components.push(component);
70
+ }, (_countProcessed, _totalToProcess) => null);
71
+ await this.addComponentsFromPaste(components);
72
+ }
73
+ finally {
74
+ this.editor.hideLoadingWarning();
75
+ }
76
+ }
77
+ async doTextPaste(text) {
78
+ const textTools = this.editor.toolController.getMatchingTools(TextTool);
79
+ textTools.sort((a, b) => {
80
+ if (!a.isEnabled() && b.isEnabled()) {
81
+ return -1;
82
+ }
83
+ if (!b.isEnabled() && a.isEnabled()) {
84
+ return 1;
85
+ }
86
+ return 0;
87
+ });
88
+ const defaultTextStyle = { size: 12, fontFamily: 'sans', renderingStyle: { fill: Color4.red } };
89
+ const pastedTextStyle = textTools[0]?.getTextStyle() ?? defaultTextStyle;
90
+ // Don't paste text that would be invisible.
91
+ if (text.trim() === '') {
92
+ return;
93
+ }
94
+ const lines = text.split('\n');
95
+ await this.addComponentsFromPaste([TextComponent.fromLines(lines, Mat33.identity, pastedTextStyle)]);
96
+ }
97
+ async doImagePaste(dataURL) {
98
+ const image = new Image();
99
+ image.src = dataURL;
100
+ const component = await ImageComponent.fromImage(image, Mat33.identity);
101
+ await this.addComponentsFromPaste([component]);
102
+ }
103
+ }