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