brilliantsole 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +16 -10
  2. package/assets/3d/anchor.glb +0 -0
  3. package/assets/3d/coin.glb +0 -0
  4. package/assets/3d/glasses.glb +0 -0
  5. package/assets/3d/rightHand.glb +0 -0
  6. package/assets/audio/bounceMedium.wav +0 -0
  7. package/assets/audio/bounceStrong.wav +0 -0
  8. package/assets/audio/bounceWeak.wav +0 -0
  9. package/assets/audio/coin.wav +0 -0
  10. package/assets/audio/getUp.wav +0 -0
  11. package/assets/audio/grab.wav +0 -0
  12. package/assets/audio/kick.wav +0 -0
  13. package/assets/audio/platterFadeIn old.wav +0 -0
  14. package/assets/audio/platterFadeIn.wav +0 -0
  15. package/assets/audio/platterFadeOut.wav +0 -0
  16. package/assets/audio/punch.wav +0 -0
  17. package/assets/audio/punchSqueak.wav +0 -0
  18. package/assets/audio/purr.wav +0 -0
  19. package/assets/audio/purrFadeOut.wav +0 -0
  20. package/assets/audio/release.wav +0 -0
  21. package/assets/audio/splat.wav +0 -0
  22. package/assets/audio/stomp.wav +0 -0
  23. package/assets/images/ukaton-pressure-0.svg +9 -0
  24. package/assets/images/ukaton-pressure-1.svg +9 -0
  25. package/assets/images/ukaton-pressure-10.svg +9 -0
  26. package/assets/images/ukaton-pressure-11.svg +9 -0
  27. package/assets/images/ukaton-pressure-12.svg +9 -0
  28. package/assets/images/ukaton-pressure-13.svg +9 -0
  29. package/assets/images/ukaton-pressure-14.svg +9 -0
  30. package/assets/images/ukaton-pressure-15.svg +9 -0
  31. package/assets/images/ukaton-pressure-2.svg +9 -0
  32. package/assets/images/ukaton-pressure-3.svg +9 -0
  33. package/assets/images/ukaton-pressure-4.svg +9 -0
  34. package/assets/images/ukaton-pressure-5.svg +9 -0
  35. package/assets/images/ukaton-pressure-6.svg +9 -0
  36. package/assets/images/ukaton-pressure-7.svg +9 -0
  37. package/assets/images/ukaton-pressure-8.svg +9 -0
  38. package/assets/images/ukaton-pressure-9.svg +9 -0
  39. package/assets/images/ukaton-right-insole.svg +798 -0
  40. package/build/brilliantsole.cjs +2870 -882
  41. package/build/brilliantsole.cjs.map +1 -1
  42. package/build/brilliantsole.js +2477 -782
  43. package/build/brilliantsole.js.map +1 -1
  44. package/build/brilliantsole.ls.js +2260 -592
  45. package/build/brilliantsole.ls.js.map +1 -1
  46. package/build/brilliantsole.min.js +1 -1
  47. package/build/brilliantsole.min.js.map +1 -1
  48. package/build/brilliantsole.module.d.ts +302 -116
  49. package/build/brilliantsole.module.js +2468 -782
  50. package/build/brilliantsole.module.js.map +1 -1
  51. package/build/brilliantsole.module.min.d.ts +302 -116
  52. package/build/brilliantsole.module.min.js +1 -1
  53. package/build/brilliantsole.module.min.js.map +1 -1
  54. package/build/brilliantsole.node.module.d.ts +295 -113
  55. package/build/brilliantsole.node.module.js +2860 -882
  56. package/build/brilliantsole.node.module.js.map +1 -1
  57. package/build/dts/BS-output.d.ts +10 -0
  58. package/build/dts/BS.d.ts +21 -9
  59. package/build/dts/CameraManager.d.ts +72 -0
  60. package/build/dts/Device.d.ts +53 -16
  61. package/build/dts/DeviceInformationManager.d.ts +4 -4
  62. package/build/dts/DeviceManager.d.ts +3 -0
  63. package/build/dts/FileTransferManager.d.ts +18 -8
  64. package/build/dts/InformationManager.d.ts +8 -5
  65. package/build/dts/TfliteManager.d.ts +22 -2
  66. package/build/dts/WifiManager.d.ts +61 -0
  67. package/build/dts/connection/BaseConnectionManager.d.ts +37 -3
  68. package/build/dts/connection/ClientConnectionManager.d.ts +11 -2
  69. package/build/dts/connection/bluetooth/BluetoothConnectionManager.d.ts +1 -0
  70. package/build/dts/connection/bluetooth/NobleConnectionManager.d.ts +3 -1
  71. package/build/dts/connection/bluetooth/WebBluetoothConnectionManager.d.ts +2 -0
  72. package/build/dts/connection/bluetooth/bluetoothUUIDs.d.ts +2 -2
  73. package/build/dts/connection/udp/UDPConnectionManager.d.ts +28 -0
  74. package/build/dts/connection/webSocket/WebSocketConnectionManager.d.ts +25 -0
  75. package/build/dts/devicePair/DevicePair.d.ts +14 -10
  76. package/build/dts/devicePair/DevicePairPressureSensorDataManager.d.ts +8 -4
  77. package/build/dts/devicePair/DevicePairSensorDataManager.d.ts +2 -2
  78. package/build/dts/scanner/BaseScanner.d.ts +4 -1
  79. package/build/dts/scanner/NobleScanner.d.ts +2 -1
  80. package/build/dts/sensor/MotionSensorDataManager.d.ts +5 -2
  81. package/build/dts/sensor/SensorDataManager.d.ts +5 -4
  82. package/build/dts/server/BaseClient.d.ts +6 -3
  83. package/build/dts/server/ServerUtils.d.ts +1 -1
  84. package/build/dts/server/websocket/WebSocketUtils.d.ts +1 -1
  85. package/build/dts/utils/CenterOfPressureHelper.d.ts +2 -2
  86. package/build/dts/utils/Console.d.ts +2 -0
  87. package/build/dts/utils/MathUtils.d.ts +2 -0
  88. package/build/dts/utils/ThrottleUtils.d.ts +2 -0
  89. package/build/dts/vibration/VibrationManager.d.ts +19 -2
  90. package/build/index.d.ts +299 -113
  91. package/build/index.node.d.ts +292 -110
  92. package/examples/3d/scene.html +19 -5
  93. package/examples/3d/script.js +90 -17
  94. package/examples/3d-generic/index.html +144 -0
  95. package/examples/3d-generic/script.js +266 -0
  96. package/examples/balance/script.js +2 -1
  97. package/examples/basic/index.html +232 -18
  98. package/examples/basic/script.js +746 -106
  99. package/examples/bottango/index.html +11 -1
  100. package/examples/bottango/script.js +2 -2
  101. package/examples/center-of-pressure/index.html +114 -114
  102. package/examples/center-of-pressure/script.js +1 -1
  103. package/examples/device-pair/index.html +58 -58
  104. package/examples/device-pair/script.js +12 -8
  105. package/examples/edge-impulse/script.js +135 -44
  106. package/examples/edge-impulse-test/README.md +11 -0
  107. package/examples/edge-impulse-test/edge-impulse-standalone.js +7228 -0
  108. package/examples/edge-impulse-test/edge-impulse-standalone.wasm +0 -0
  109. package/examples/edge-impulse-test/index.html +75 -0
  110. package/examples/edge-impulse-test/run-impulse.js +135 -0
  111. package/examples/edge-impulse-test/script.js +200 -0
  112. package/examples/gloves/edge-impulse-standalone.js +7228 -0
  113. package/examples/gloves/edge-impulse-standalone.wasm +0 -0
  114. package/examples/gloves/index.html +119 -0
  115. package/examples/gloves/run-impulse.js +135 -0
  116. package/examples/gloves/scene.html +124 -0
  117. package/examples/gloves/script.js +931 -0
  118. package/examples/graph/index.html +11 -1
  119. package/examples/graph/script.js +94 -37
  120. package/examples/pressure/index.html +180 -12
  121. package/examples/pressure/script.js +144 -7
  122. package/examples/punch/index.html +135 -0
  123. package/examples/punch/punch.tflite +0 -0
  124. package/examples/punch/script.js +169 -0
  125. package/examples/recording/index.html +191 -183
  126. package/examples/server/index.html +109 -23
  127. package/examples/server/script.js +322 -111
  128. package/examples/ukaton-firmware-update/index.html +20 -0
  129. package/examples/ukaton-firmware-update/manifest.json +11 -0
  130. package/examples/ukaton-firmware-update/merged-firmware.bin +0 -0
  131. package/examples/utils/aframe/aframe-master.min.js +2 -0
  132. package/examples/utils/aframe/bs-vibration.js +150 -0
  133. package/examples/utils/aframe/force-pushable.js +80 -0
  134. package/examples/utils/aframe/grabbable-anchor.js +46 -0
  135. package/examples/utils/aframe/grabbable-listener.js +31 -0
  136. package/examples/utils/aframe/grabbable-physics-body.js +190 -0
  137. package/examples/utils/aframe/grow-shrink.js +25 -0
  138. package/examples/utils/aframe/hand-punch.js +119 -0
  139. package/examples/utils/aframe/my-obb-collider.js +293 -0
  140. package/examples/utils/aframe/occlude-hand-tracking-controls.js +47 -0
  141. package/examples/utils/aframe/occlude-mesh.js +42 -0
  142. package/examples/utils/aframe/palm-up-detector.js +47 -0
  143. package/examples/utils/aframe/shadow-material.js +20 -0
  144. package/examples/utils/aframe/soft-shadow-light.js +9 -0
  145. package/examples/webxr/script.js +3 -3
  146. package/examples/webxr-2/assets/3d/soccerBall.glb +0 -0
  147. package/examples/webxr-2/assets/audio/shellBounce.wav +0 -0
  148. package/examples/webxr-2/assets/audio/shellHit.wav +0 -0
  149. package/examples/webxr-2/assets/audio/shellKick.wav +0 -0
  150. package/examples/webxr-2/assets/audio/soccerBounce.wav +0 -0
  151. package/examples/webxr-2/assets/audio/soccerKick.mp3 +0 -0
  152. package/examples/webxr-2/assets/images/shellTexture.png +0 -0
  153. package/examples/webxr-2/components/bs-ankle.js +337 -0
  154. package/examples/webxr-2/components/coin.js +84 -0
  155. package/examples/webxr-2/components/custom-wrap.js +17 -0
  156. package/examples/webxr-2/components/goomba.js +3250 -0
  157. package/examples/webxr-2/components/init-shell-material.js +215 -0
  158. package/examples/webxr-2/components/platter.js +172 -0
  159. package/examples/webxr-2/components/shell.js +374 -0
  160. package/examples/webxr-2/components/soccer-ball.js +250 -0
  161. package/examples/webxr-2/components/squashed-goomba.js +249 -0
  162. package/examples/webxr-2/edge-impulse-standalone.js +7228 -0
  163. package/examples/webxr-2/edge-impulse-standalone.wasm +0 -0
  164. package/examples/webxr-2/index.html +996 -0
  165. package/examples/webxr-2/kick.tflite +0 -0
  166. package/examples/webxr-2/kick2.tflite +0 -0
  167. package/examples/webxr-2/run-impulse.js +135 -0
  168. package/examples/webxr-2/script.js +384 -0
  169. package/package.json +2 -1
  170. package/src/.prettierrc +4 -0
  171. package/src/BS.ts +66 -9
  172. package/src/CameraManager.ts +499 -0
  173. package/src/Device.ts +620 -92
  174. package/src/DeviceInformationManager.ts +22 -11
  175. package/src/DeviceManager.ts +94 -25
  176. package/src/FileTransferManager.ts +146 -21
  177. package/src/FirmwareManager.ts +1 -1
  178. package/src/InformationManager.ts +62 -20
  179. package/src/TfliteManager.ts +172 -26
  180. package/src/WifiManager.ts +323 -0
  181. package/src/connection/BaseConnectionManager.ts +145 -30
  182. package/src/connection/ClientConnectionManager.ts +47 -11
  183. package/src/connection/bluetooth/BluetoothConnectionManager.ts +14 -3
  184. package/src/connection/bluetooth/NobleConnectionManager.ts +155 -42
  185. package/src/connection/bluetooth/WebBluetoothConnectionManager.ts +104 -35
  186. package/src/connection/bluetooth/bluetoothUUIDs.ts +40 -13
  187. package/src/connection/udp/UDPConnectionManager.ts +356 -0
  188. package/src/connection/websocket/WebSocketConnectionManager.ts +282 -0
  189. package/src/devicePair/DevicePair.ts +145 -49
  190. package/src/devicePair/DevicePairPressureSensorDataManager.ts +72 -24
  191. package/src/devicePair/DevicePairSensorDataManager.ts +5 -5
  192. package/src/scanner/BaseScanner.ts +49 -11
  193. package/src/scanner/NobleScanner.ts +81 -17
  194. package/src/sensor/BarometerSensorDataManager.ts +1 -1
  195. package/src/sensor/MotionSensorDataManager.ts +22 -7
  196. package/src/sensor/PressureSensorDataManager.ts +47 -13
  197. package/src/sensor/SensorConfigurationManager.ts +75 -24
  198. package/src/sensor/SensorDataManager.ts +107 -26
  199. package/src/server/BaseClient.ts +192 -37
  200. package/src/server/BaseServer.ts +201 -43
  201. package/src/server/ServerUtils.ts +39 -9
  202. package/src/server/udp/UDPServer.ts +74 -23
  203. package/src/server/udp/UDPUtils.ts +9 -2
  204. package/src/server/websocket/WebSocketClient.ts +30 -9
  205. package/src/server/websocket/WebSocketServer.ts +1 -1
  206. package/src/server/websocket/WebSocketUtils.ts +4 -2
  207. package/src/utils/CenterOfPressureHelper.ts +5 -5
  208. package/src/utils/Console.ts +62 -9
  209. package/src/utils/MathUtils.ts +31 -1
  210. package/src/utils/ParseUtils.ts +25 -6
  211. package/src/utils/ThrottleUtils.ts +62 -0
  212. package/src/utils/Timer.ts +1 -1
  213. package/src/utils/checksum.ts +1 -1
  214. package/src/utils/mcumgr.js +1 -1
  215. package/src/vibration/VibrationManager.ts +166 -40
@@ -0,0 +1,931 @@
1
+ import * as BS from "../../build/brilliantsole.module.js";
2
+ window.BS = BS;
3
+ console.log({ BS });
4
+ //BS.setAllConsoleLevelFlags({ log: false });
5
+
6
+ /** @typedef {import("../utils/three/three.module.min").Vector2} TVector2 */
7
+ /** @typedef {import("../utils/three/three.module.min").Vector3} TVector3 */
8
+ /** @typedef {import("../utils/three/three.module.min").Quaternion} TQuaternion */
9
+ /** @typedef {import("../utils/three/three.module.min").Euler} TEuler */
10
+
11
+ // GET DEVICES
12
+
13
+ /** @type {HTMLTemplateElement} */
14
+ const availableDeviceTemplate = document.getElementById(
15
+ "availableDeviceTemplate"
16
+ );
17
+ const availableDevicesContainer = document.getElementById("availableDevices");
18
+ /** @param {BS.Device[]} availableDevices */
19
+ function onAvailableDevices(availableDevices) {
20
+ availableDevicesContainer.innerHTML = "";
21
+ if (availableDevices.length == 0) {
22
+ availableDevicesContainer.innerText = "no devices available";
23
+ } else {
24
+ availableDevices.forEach((availableDevice) => {
25
+ let availableDeviceContainer = availableDeviceTemplate.content
26
+ .cloneNode(true)
27
+ .querySelector(".availableDevice");
28
+ availableDeviceContainer.querySelector(".name").innerText =
29
+ availableDevice.name;
30
+ availableDeviceContainer.querySelector(".type").innerText =
31
+ availableDevice.type;
32
+
33
+ /** @type {HTMLButtonElement} */
34
+ const toggleConnectionButton =
35
+ availableDeviceContainer.querySelector(".toggleConnection");
36
+ toggleConnectionButton.addEventListener("click", () => {
37
+ availableDevice.toggleConnection();
38
+ });
39
+ const onConnectionStatusUpdate = () => {
40
+ switch (availableDevice.connectionStatus) {
41
+ case "connected":
42
+ case "notConnected":
43
+ toggleConnectionButton.disabled = false;
44
+ toggleConnectionButton.innerText = availableDevice.isConnected
45
+ ? "disconnect"
46
+ : "connect";
47
+ break;
48
+ case "connecting":
49
+ case "disconnecting":
50
+ toggleConnectionButton.disabled = true;
51
+ toggleConnectionButton.innerText = availableDevice.connectionStatus;
52
+ break;
53
+ }
54
+ };
55
+ availableDevice.addEventListener("connectionStatus", () =>
56
+ onConnectionStatusUpdate()
57
+ );
58
+ onConnectionStatusUpdate();
59
+ availableDevicesContainer.appendChild(availableDeviceContainer);
60
+ });
61
+ }
62
+ }
63
+ async function getDevices() {
64
+ const availableDevices = await BS.DeviceManager.GetDevices();
65
+ if (!availableDevices) {
66
+ return;
67
+ }
68
+ onAvailableDevices(availableDevices);
69
+ }
70
+
71
+ BS.DeviceManager.AddEventListener("availableDevices", (event) => {
72
+ const devices = event.message.availableDevices;
73
+ onAvailableDevices(devices);
74
+ });
75
+ getDevices();
76
+
77
+ // ADD DEVICE
78
+
79
+ /** @type {HTMLButtonElement} */
80
+ const addDeviceButton = document.getElementById("addDevice");
81
+ addDeviceButton.addEventListener("click", () => {
82
+ BS.Device.Connect();
83
+ });
84
+
85
+ const devicePair = BS.DevicePair.gloves;
86
+ devicePair.addEventListener("isConnected", () => {
87
+ addDeviceButton.disabled = devicePair.isConnected;
88
+ });
89
+
90
+ // 3D VISUALIZATION
91
+
92
+ const glovesContainer = document.getElementById("gloves");
93
+ /** @type {HTMLTemplateElement} */
94
+ const gloveTemplate = document.getElementById("gloveTemplate");
95
+
96
+ window.sensorRate = 20;
97
+ window.interpolationSmoothing = 0.4;
98
+ window.positionScalar = 0.1;
99
+
100
+ devicePair.sides.forEach((side) => {
101
+ /** @type {HTMLElement} */
102
+ const gloveContainer = gloveTemplate.content
103
+ .cloneNode(true)
104
+ .querySelector(".glove");
105
+ gloveContainer.classList.add(side);
106
+ gloveContainer.dataset.side = side;
107
+ /** @type {HTMLIFrameElement} */
108
+ const iframe = gloveContainer.querySelector("iframe");
109
+ iframe.addEventListener("load", () => {
110
+ onIFrameLoaded(gloveContainer);
111
+ });
112
+ glovesContainer.appendChild(gloveContainer);
113
+ });
114
+
115
+ /** @param {HTMLElement} gloveContainer */
116
+ function onIFrameLoaded(gloveContainer) {
117
+ /** @type {BS.Side} */
118
+ const side = gloveContainer.dataset.side;
119
+ /** @type {HTMLIFrameElement} */
120
+ const iframe = gloveContainer.querySelector("iframe");
121
+ const scene = iframe.contentDocument.querySelector("a-scene");
122
+ const targetEntity = scene.querySelector(".target");
123
+ const targetPositionEntity = targetEntity.querySelector(".position");
124
+ const targetRotationEntity = targetEntity.querySelector(".rotation");
125
+ const gloveEntity = targetEntity.querySelector(".glove");
126
+ const pressureEntities = Array.from(
127
+ targetEntity.querySelectorAll("[data-pressure]")
128
+ )
129
+ .sort((a, b) => a.dataset.pressure - b.dataset.pressure)
130
+ .map((entity) => entity.querySelector("a-sphere"));
131
+ pressureEntities.forEach((entity) => entity.setAttribute("opacity", "0.0"));
132
+ scene.addEventListener("loaded", () => {
133
+ if (side == "left") {
134
+ targetEntity.object3D.scale.x *= -1;
135
+ }
136
+ });
137
+
138
+ /** @type {HTMLButtonElement} */
139
+ const toggleConnectionButton =
140
+ gloveContainer.querySelector(".toggleConnection");
141
+ toggleConnectionButton.addEventListener("click", () => {
142
+ devicePair[side].toggleConnection();
143
+ });
144
+ devicePair.addEventListener("deviceIsConnected", (event) => {
145
+ const device = event.message.device;
146
+ if (device.side != side) {
147
+ return;
148
+ }
149
+
150
+ if (device.isConnected) {
151
+ toggleConnectionButton.disabled = false;
152
+ }
153
+ toggleConnectionButton.innerText = device.isConnected
154
+ ? "disconnect"
155
+ : "reconnect";
156
+ });
157
+
158
+ devicePair.addEventListener("deviceConnectionStatus", (event) => {
159
+ const device = event.message.device;
160
+ if (device.side != side) {
161
+ return;
162
+ }
163
+
164
+ switch (device.connectionStatus) {
165
+ case "connected":
166
+ case "notConnected":
167
+ toggleConnectionButton.disabled = false;
168
+ toggleConnectionButton.innerText = device.isConnected
169
+ ? "disconnect"
170
+ : "reconnect";
171
+ break;
172
+ case "connecting":
173
+ case "disconnecting":
174
+ toggleConnectionButton.disabled = true;
175
+ toggleConnectionButton.innerText = device.connectionStatus;
176
+ break;
177
+ }
178
+ });
179
+
180
+ /** @type {HTMLSelectElement} */
181
+ const orientationSelect = gloveContainer.querySelector(".orientation");
182
+ orientationSelect.addEventListener("input", () => {
183
+ /** @type {BS.SensorConfiguration} */
184
+ const configuration = {
185
+ gameRotation: 0,
186
+ rotation: 0,
187
+ gyroscope: 0,
188
+ orientation: 0,
189
+ };
190
+
191
+ switch (orientationSelect.value) {
192
+ case "none":
193
+ break;
194
+ case "gameRotation":
195
+ configuration.gameRotation = pinchSensorRate;
196
+ break;
197
+ case "rotation":
198
+ configuration.rotation = pinchSensorRate;
199
+ break;
200
+ case "orientation":
201
+ configuration.orientation = pinchSensorRate;
202
+ break;
203
+ case "gyroscope":
204
+ configuration.gyroscope = pinchSensorRate;
205
+ break;
206
+ default:
207
+ console.error(
208
+ `uncaught orientationSelect value "${orientationSelect.value}"`
209
+ );
210
+ break;
211
+ }
212
+
213
+ devicePair[side]?.setSensorConfiguration(configuration);
214
+ });
215
+
216
+ /** @type {HTMLButtonElement} */
217
+ const resetOrientationButton =
218
+ gloveContainer.querySelector(".resetOrientation");
219
+ resetOrientationButton.addEventListener("click", () => {
220
+ resetOrientation();
221
+ });
222
+ devicePair.addEventListener("deviceIsConnected", (event) => {
223
+ const device = event.message.device;
224
+ if (device.side != side) {
225
+ return;
226
+ }
227
+
228
+ resetOrientationButton.disabled = !device.isConnected;
229
+ });
230
+
231
+ /** @type {HTMLSelectElement} */
232
+ const positionSelect = gloveContainer.querySelector(".position");
233
+ positionSelect.addEventListener("input", () => {
234
+ /** @type {BS.SensorConfiguration} */
235
+ const configuration = {
236
+ acceleration: 0,
237
+ gravity: 0,
238
+ linearAcceleration: 0,
239
+ };
240
+
241
+ switch (positionSelect.value) {
242
+ case "none":
243
+ break;
244
+ case "acceleration":
245
+ configuration.acceleration = pinchSensorRate;
246
+ break;
247
+ case "gravity":
248
+ configuration.gravity = pinchSensorRate;
249
+ break;
250
+ case "linearAcceleration":
251
+ configuration.linearAcceleration = pinchSensorRate;
252
+ break;
253
+ default:
254
+ console.error(
255
+ `uncaught positionSelect value "${positionSelect.value}"`
256
+ );
257
+ break;
258
+ }
259
+
260
+ console.log({ configuration });
261
+
262
+ devicePair[side]?.setSensorConfiguration(configuration);
263
+ });
264
+ devicePair.addEventListener("deviceIsConnected", (event) => {
265
+ const device = event.message.device;
266
+ if (device.side != side) {
267
+ return;
268
+ }
269
+
270
+ orientationSelect.disabled = !device.isConnected;
271
+ positionSelect.disabled = !device.isConnected;
272
+ });
273
+
274
+ devicePair.addEventListener("deviceGetSensorConfiguration", (event) => {
275
+ const device = event.message.device;
276
+ if (device.side != side) {
277
+ return;
278
+ }
279
+
280
+ let newOrientationSelectValue = "none";
281
+ let newPositionSelectValue = "none";
282
+
283
+ for (const key in device.sensorConfiguration) {
284
+ /** @type {BS.SensorType} */
285
+ const sensorType = key;
286
+ if (device.sensorConfiguration[sensorType] > 0) {
287
+ switch (sensorType) {
288
+ case "gameRotation":
289
+ case "rotation":
290
+ case "orientation":
291
+ case "gyroscope":
292
+ newOrientationSelectValue = sensorType;
293
+ break;
294
+ case "acceleration":
295
+ case "gravity":
296
+ case "linearAcceleration":
297
+ newPositionSelectValue = sensorType;
298
+ break;
299
+ }
300
+ }
301
+ }
302
+
303
+ orientationSelect.value = newOrientationSelectValue;
304
+ positionSelect.value = newPositionSelectValue;
305
+ });
306
+
307
+ /** @type {TVector3} */
308
+ const _position = new THREE.Vector3();
309
+
310
+ /** @param {BS.Vector3} position */
311
+ const updatePosition = (position) => {
312
+ _position.copy(position).multiplyScalar(window.positionScalar);
313
+ targetPositionEntity.object3D.position.lerp(
314
+ _position,
315
+ window.interpolationSmoothing
316
+ );
317
+ };
318
+
319
+ devicePair.addEventListener("deviceAcceleration", (event) => {
320
+ const device = event.message.device;
321
+ if (device.side != side) {
322
+ return;
323
+ }
324
+ const acceleration = event.message.acceleration;
325
+ updatePosition(acceleration);
326
+ });
327
+ devicePair.addEventListener("deviceGravity", (event) => {
328
+ const device = event.message.device;
329
+ if (device.side != side) {
330
+ return;
331
+ }
332
+
333
+ const gravity = event.message.gravity;
334
+ updatePosition(gravity);
335
+ });
336
+ devicePair.addEventListener("deviceLinearAcceleration", (event) => {
337
+ const device = event.message.device;
338
+ if (device.side != side) {
339
+ return;
340
+ }
341
+
342
+ const linearAcceleration = event.message.linearAcceleration;
343
+ updatePosition(linearAcceleration);
344
+ });
345
+
346
+ /** @type {TQuaternion} */
347
+ const offsetQuaternion = new THREE.Quaternion();
348
+ const resetOrientation = () => {
349
+ offsetQuaternion.copy(_quaternion).invert();
350
+ };
351
+
352
+ /** @type {TQuaternion} */
353
+ const _quaternion = new THREE.Quaternion();
354
+ /** @type {TQuaternion} */
355
+ const targetQuaternion = new THREE.Quaternion();
356
+ /**
357
+ * @param {BS.Quaternion} quaternion
358
+ * @param {boolean} applyOffset
359
+ */
360
+ const updateQuaternion = (quaternion, applyOffset = false) => {
361
+ _quaternion.copy(quaternion);
362
+ targetQuaternion.copy(_quaternion);
363
+ if (applyOffset) {
364
+ targetQuaternion.premultiply(offsetQuaternion);
365
+ }
366
+ targetRotationEntity.object3D.quaternion.slerp(
367
+ targetQuaternion,
368
+ window.interpolationSmoothing
369
+ );
370
+ };
371
+ devicePair.addEventListener("deviceGameRotation", (event) => {
372
+ const device = event.message.device;
373
+ if (device.side != side) {
374
+ return;
375
+ }
376
+
377
+ const gameRotation = event.message.gameRotation;
378
+ updateQuaternion(gameRotation, true);
379
+ });
380
+ devicePair.addEventListener("deviceRotation", (event) => {
381
+ const device = event.message.device;
382
+ if (device.side != side) {
383
+ return;
384
+ }
385
+
386
+ const rotation = event.message.rotation;
387
+ updateQuaternion(rotation, true);
388
+ });
389
+
390
+ /** @type {TVector3} */
391
+ const orientationVector3 = new THREE.Vector3();
392
+ /** @type {TEuler} */
393
+ const orientationEuler = new THREE.Euler(0, 0, 0, "YXZ");
394
+ /** @type {TQuaternion} */
395
+ const orientationQuaternion = new THREE.Quaternion();
396
+ devicePair.addEventListener("deviceOrientation", (event) => {
397
+ const device = event.message.device;
398
+ if (device.side != side) {
399
+ return;
400
+ }
401
+
402
+ const orientation = event.message.orientation;
403
+ orientationVector3
404
+ .set(orientation.pitch, orientation.heading, orientation.roll)
405
+ .multiplyScalar(Math.PI / 180);
406
+ orientationEuler.setFromVector3(orientationVector3);
407
+ orientationQuaternion.setFromEuler(orientationEuler);
408
+ updateQuaternion(orientationQuaternion);
409
+ });
410
+
411
+ /** @type {TVector3} */
412
+ const gyroscopeVector3 = new THREE.Vector3();
413
+ /** @type {TEuler} */
414
+ const gyroscopeEuler = new THREE.Euler();
415
+ /** @type {TQuaternion} */
416
+ const gyroscopeQuaternion = new THREE.Quaternion();
417
+ devicePair.addEventListener("deviceGyroscope", (event) => {
418
+ const device = event.message.device;
419
+ if (device.side != side) {
420
+ return;
421
+ }
422
+
423
+ const gyroscope = event.message.gyroscope;
424
+ gyroscopeVector3.copy(gyroscope).multiplyScalar(Math.PI / 180);
425
+ gyroscopeEuler.setFromVector3(gyroscopeVector3);
426
+ gyroscopeQuaternion.setFromEuler(gyroscopeEuler);
427
+ updateQuaternion(gyroscopeQuaternion);
428
+ });
429
+
430
+ // PRESSURE
431
+
432
+ /** @type {HTMLButtonElement} */
433
+ const togglePressureButton = gloveContainer.querySelector(".togglePressure");
434
+ togglePressureButton.addEventListener("click", () => {
435
+ togglePressure();
436
+ });
437
+ const setIsPressureEnabled = (newIsPressureEnabled) => {
438
+ /** @type {BS.SensorConfiguration} */
439
+ const configuration = { pressure: newIsPressureEnabled ? 20 : 0 };
440
+ devicePair[side]?.setSensorConfiguration(configuration);
441
+ };
442
+ const togglePressure = () => {
443
+ const isPressureEnabled =
444
+ devicePair[side].sensorConfiguration.pressure != 0;
445
+ setIsPressureEnabled(!isPressureEnabled);
446
+ };
447
+ devicePair.addEventListener("deviceGetSensorConfiguration", (event) => {
448
+ if (event.message.side != side) {
449
+ return;
450
+ }
451
+ const { sensorConfiguration } = event.message;
452
+ togglePressureButton.innerText =
453
+ sensorConfiguration.pressure == 0
454
+ ? "enable pressure"
455
+ : "disable pressure";
456
+ });
457
+ /** @type {HTMLButtonElement} */
458
+ const resetPressureButton = gloveContainer.querySelector(".resetPressure");
459
+ resetPressureButton.addEventListener("click", () => {
460
+ devicePair[side].resetPressureRange();
461
+ });
462
+ devicePair.addEventListener("deviceIsConnected", (event) => {
463
+ const device = event.message.device;
464
+ if (device.side != side) {
465
+ return;
466
+ }
467
+
468
+ togglePressureButton.disabled = !device.isConnected;
469
+ resetPressureButton.disabled = !device.isConnected;
470
+ });
471
+
472
+ devicePair.addEventListener("devicePressure", (event) => {
473
+ if (event.message.side != side) {
474
+ return;
475
+ }
476
+ const { pressure } = event.message;
477
+ pressure.sensors.forEach((sensor, index) => {
478
+ pressureEntities[index]?.setAttribute("opacity", sensor.normalizedValue);
479
+ });
480
+ });
481
+
482
+ // CURSOR MODE
483
+ let isCursorEnabled = false;
484
+ /** @type {HTMLButtonElement} */
485
+ const toggleCursorButton = gloveContainer.querySelector(".toggleCursor");
486
+ toggleCursorButton.addEventListener("click", () => {
487
+ setIsCursorEnabled(!isCursorEnabled);
488
+ });
489
+ /** @type {HTMLButtonElement} */
490
+ const resetCursorButton = gloveContainer.querySelector(".resetCursor");
491
+ resetCursorButton.addEventListener("click", () => {
492
+ cursorIntersectableEntities.forEach((entity) => {
493
+ entity.removeAttribute("dynamic-body");
494
+ entity.object3D.position.set(0, 1, -20);
495
+ entity.setAttribute("dynamic-body", "");
496
+ });
497
+ });
498
+
499
+ let checkCursorIntersectableEntitiesIntervalId;
500
+ const checkCursorIntersectableEntitiesInterval = 1000;
501
+ const xThreshold = 4.5;
502
+ const checkCursorIntersectableEntities = () => {
503
+ cursorIntersectableEntities.forEach((entity) => {
504
+ const position = entity.object3D.position;
505
+ if (!isCursorDown && Math.abs(position.x) > xThreshold) {
506
+ entity.removeAttribute("dynamic-body");
507
+ entity.object3D.position.set(0, 1, -20);
508
+ entity.setAttribute("dynamic-body", "");
509
+ }
510
+ });
511
+ };
512
+ const setIsCursorEnabled = (newIsCursorEnabled) => {
513
+ isCursorEnabled = newIsCursorEnabled;
514
+ toggleCursorButton.innerText = isCursorEnabled
515
+ ? "disable cursor"
516
+ : "enable cursor";
517
+
518
+ if (devicePair[side]?.isConnected) {
519
+ const device = devicePair[side];
520
+ if (device.isUkaton) {
521
+ if (isCursorEnabled) {
522
+ device.setSensorConfiguration(pinchSensorConfiguration);
523
+ } else {
524
+ device.clearSensorConfiguration();
525
+ }
526
+ } else {
527
+ if (isCursorEnabled) {
528
+ orientationSelect.value = "gyroscope";
529
+ } else {
530
+ orientationSelect.value = "none";
531
+ }
532
+ orientationSelect.dispatchEvent(new Event("input"));
533
+ setIsPressureEnabled(isCursorEnabled);
534
+ }
535
+
536
+ if (devicePair[side]?.isUkaton) {
537
+ if (isCursorEnabled) {
538
+ positionSelect.value = "linearAcceleration";
539
+ } else {
540
+ positionSelect.value = "none";
541
+ }
542
+ positionSelect.dispatchEvent(new Event("input"));
543
+ } else {
544
+ setIsPressureEnabled(isCursorEnabled);
545
+ }
546
+ }
547
+ onCursorIsEnabled();
548
+
549
+ if (isCursorEnabled) {
550
+ checkCursorIntersectableEntitiesIntervalId = setInterval(
551
+ () => checkCursorIntersectableEntities(),
552
+ checkCursorIntersectableEntitiesInterval
553
+ );
554
+ } else {
555
+ clearInterval(checkCursorIntersectableEntitiesIntervalId);
556
+ }
557
+ };
558
+ window.addEventListener("pinch", () => {
559
+ if (isCursorDown || intersectedEntities[0]) {
560
+ setIsCursorDown(!isCursorDown);
561
+ devicePair[side]?.triggerVibration([
562
+ {
563
+ type: "waveformEffect",
564
+ locations: ["rear"],
565
+ segments: [{ effect: "buzz100" }],
566
+ },
567
+ ]);
568
+ }
569
+ });
570
+
571
+ const onCursorIsEnabled = () => {
572
+ if (isCursorEnabled) {
573
+ targetEntity.setAttribute("visible", "false");
574
+ cursorExample.setAttribute("visible", "true");
575
+ //cameraEntity.setAttribute("orbit-controls", { enabled: false });
576
+ cameraEntity.setAttribute("camera", { active: false });
577
+ cursorCameraEntity.setAttribute("camera", { active: true });
578
+ } else {
579
+ targetEntity.setAttribute("visible", "true");
580
+ cursorExample.setAttribute("visible", "false");
581
+ cursorCameraEntity.setAttribute("camera", { active: false });
582
+ cameraEntity.setAttribute("camera", { active: true });
583
+ //cameraEntity.setAttribute("orbit-controls", { enabled: true });
584
+ }
585
+ };
586
+
587
+ devicePair.addEventListener("deviceIsConnected", (event) => {
588
+ const device = event.message.device;
589
+ if (device.side != side) {
590
+ return;
591
+ }
592
+ //toggleCursorButton.disabled = !device.isConnected;
593
+ });
594
+
595
+ const cameraEntity = scene.querySelector(".camera");
596
+ const cursorCameraEntity = scene.querySelector(".cursorCamera");
597
+ const cursorEntity = scene.querySelector(".cursor");
598
+ const cursorHandleEntity = scene.querySelector(".cursorHandle");
599
+ const cursorMeshEntity = cursorEntity.querySelector(".mesh");
600
+ const cursorExample = scene.querySelector(".cursorExample");
601
+ devicePair.addEventListener("deviceGyroscope", (event) => {
602
+ if (event.message.side != side) {
603
+ return;
604
+ }
605
+ if (!isCursorEnabled) {
606
+ return;
607
+ }
608
+ const { gyroscope } = event.message;
609
+ const { x: yawDegrees, y: pitchDegrees, z: rollDegrees } = gyroscope;
610
+ // console.log({ yawDegrees, pitchDegrees, rollDegrees });
611
+ setCursorPosition(yawDegrees * 0.002, pitchDegrees * 0.003, true);
612
+ });
613
+ const cursorRaycaster = new THREE.Raycaster();
614
+ const cursor2DPosition = new THREE.Vector2();
615
+ const cursor3DPosition = new THREE.Vector3();
616
+ const setCursorPosition = (x, y, isOffset = false) => {
617
+ if (isOffset) {
618
+ cursor2DPosition.x += x;
619
+ cursor2DPosition.y += y;
620
+ } else {
621
+ cursor2DPosition.set(x, y);
622
+ }
623
+ cursor2DPosition.clampScalar(-1, 1);
624
+ updateCursorEntity();
625
+ if (!isCursorDown) {
626
+ intersectEntities();
627
+ }
628
+ if (isCursorDown && draggingEntity) {
629
+ dragEntity();
630
+ }
631
+ };
632
+ const updateCursorEntity = () => {
633
+ cursorRaycaster.setFromCamera(
634
+ cursor2DPosition,
635
+ cursorCameraEntity.object3D.children[0]
636
+ );
637
+ cursorRaycaster.ray.at(1, cursor3DPosition);
638
+ cursorEntity.object3D.position.copy(cursor3DPosition);
639
+ };
640
+ const cursorIntersectableEntities = Array.from(
641
+ scene.querySelectorAll(".intersectable")
642
+ );
643
+
644
+ const dragEntityPosition = new THREE.Vector3();
645
+ const dragEntity = () => {
646
+ cursorRaycaster.ray.at(20, dragEntityPosition);
647
+ //draggingEntity.object3D.position.copy(dragEntityPosition);
648
+ cursorHandleEntity.object3D.position.copy(dragEntityPosition);
649
+ };
650
+
651
+ let intersectedEntities = [];
652
+ const intersectEntities = () => {
653
+ intersectedEntities.length = 0;
654
+ cursorIntersectableEntities.forEach((entity) => {
655
+ const intersections = cursorRaycaster.intersectObject(
656
+ entity.object3D,
657
+ true
658
+ );
659
+ const intersection = intersections[0];
660
+ if (intersection) {
661
+ intersectedEntities.push(entity);
662
+ entity.setAttribute("color", "lightgreen");
663
+ } else {
664
+ entity.setAttribute("color", entity.dataset.color);
665
+ }
666
+ });
667
+ };
668
+
669
+ let isCursorDown = false;
670
+ scene.addEventListener("mousemove", (event) => {
671
+ const canvas = scene.canvas;
672
+ const rect = canvas.getBoundingClientRect();
673
+ const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
674
+ const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
675
+ setCursorPosition(x, y);
676
+ });
677
+ scene.addEventListener("mousedown", () => {
678
+ setIsCursorDown(true);
679
+ });
680
+ scene.addEventListener("mouseup", () => {
681
+ setIsCursorDown(false);
682
+ });
683
+ let draggingEntity;
684
+ const setIsCursorDown = (newIsCursorDown) => {
685
+ isCursorDown = newIsCursorDown;
686
+ cursorMeshEntity.setAttribute(
687
+ "color",
688
+ isCursorDown ? "black" : cursorMeshEntity.dataset.color
689
+ );
690
+ if (isCursorDown && intersectedEntities[0]) {
691
+ draggingEntity = intersectedEntities[0];
692
+ console.log("dragging entity");
693
+ draggingEntity.setAttribute("color", "green");
694
+ cursorHandleEntity.setAttribute("static-body", "");
695
+ draggingEntity.setAttribute(
696
+ "constraint",
697
+ "target: .cursorHandle; collideConnected: false; type: pointToPoint;"
698
+ );
699
+ }
700
+ if (!isCursorDown && draggingEntity) {
701
+ console.log("removing draggingEntity");
702
+ draggingEntity.setAttribute("color", draggingEntity.dataset.color);
703
+
704
+ draggingEntity.removeAttribute("constraint");
705
+ cursorHandleEntity.removeAttribute("static-body");
706
+ draggingEntity = undefined;
707
+ }
708
+ };
709
+ devicePair.addEventListener("devicePressure", (event) => {
710
+ if (event.message.side != side) {
711
+ return;
712
+ }
713
+ const { pressure } = event.message;
714
+ const pinchPressure = pressure.sensors[4];
715
+ const isPinching = pinchPressure.normalizedValue > 0.5;
716
+ setIsCursorDown(isPinching);
717
+ });
718
+ onCursorIsEnabled();
719
+ }
720
+
721
+ // SERVER
722
+
723
+ const websocketClient = new BS.WebSocketClient();
724
+ /** @type {HTMLButtonElement} */
725
+ const toggleServerConnectionButton = document.getElementById(
726
+ "toggleServerConnection"
727
+ );
728
+ toggleServerConnectionButton.addEventListener("click", () => {
729
+ websocketClient.toggleConnection();
730
+ });
731
+ websocketClient.addEventListener("isConnected", () => {
732
+ toggleServerConnectionButton.innerText = websocketClient.isConnected
733
+ ? "disconnect from server"
734
+ : "connect to server";
735
+ });
736
+ websocketClient.addEventListener("connectionStatus", () => {
737
+ let disabled;
738
+ switch (websocketClient.connectionStatus) {
739
+ case "notConnected":
740
+ case "connected":
741
+ disabled = false;
742
+ break;
743
+ case "connecting":
744
+ case "disconnecting":
745
+ disabled = true;
746
+ break;
747
+ }
748
+ toggleServerConnectionButton.disabled = disabled;
749
+ });
750
+
751
+ // PINCH CONFIG
752
+
753
+ const pinchSensorRate = 20;
754
+ /** @type {BS.TfliteSensorType[]} */
755
+ const pinchSensorTypes = ["linearAcceleration", "gyroscope"];
756
+ /** @type {BS.SensorConfiguration} */
757
+ const pinchSensorConfiguration = {};
758
+ pinchSensorTypes.forEach((sensorType) => {
759
+ pinchSensorConfiguration[sensorType] = pinchSensorRate;
760
+ });
761
+
762
+ /** @param {BS.DeviceEventMap["sensorData"]} event */
763
+ const onDeviceSensorData = (event) => {
764
+ let data = [];
765
+ switch (event.message.sensorType) {
766
+ case "pressure":
767
+ data = event.message.pressure.sensors.map((sensor) => sensor.rawValue);
768
+ break;
769
+ case "linearAcceleration":
770
+ {
771
+ const { x, y, z } = event.message.linearAcceleration;
772
+ data = [x, y, z];
773
+ }
774
+ break;
775
+ case "gyroscope":
776
+ {
777
+ const { x, y, z } = event.message.gyroscope;
778
+ data = [x, y, z];
779
+ }
780
+ break;
781
+ case "magnetometer":
782
+ {
783
+ const { x, y, z } = event.message.magnetometer;
784
+ data = [x, y, z];
785
+ }
786
+ break;
787
+ }
788
+ data = data.map(
789
+ (value) => value * pinchSensorScalars[event.message.sensorType]
790
+ );
791
+ appendData(event.message.timestamp, event.message.sensorType, data);
792
+ };
793
+ devicePair.addEventListener("deviceIsConnected", (event) => {
794
+ const { device, isConnected, side } = event.message;
795
+ if (side != "right") {
796
+ return;
797
+ }
798
+ if (isConnected) {
799
+ device.addEventListener("sensorData", onDeviceSensorData);
800
+ } else {
801
+ device.removeEventListener("sensorData", onDeviceSensorData);
802
+ }
803
+ });
804
+
805
+ const pinchSensorScalars = {
806
+ pressure: 1 / (2 ** 16 - 1),
807
+ linearAcceleration: 1 / 4,
808
+ gyroscope: 1 / 720,
809
+ magnetometer: 1 / 2500,
810
+ };
811
+
812
+ // MODEL
813
+
814
+ let classifier;
815
+ async function loadClassifier() {
816
+ if (classifier) {
817
+ return;
818
+ }
819
+ classifier = new EdgeImpulseClassifier();
820
+ await classifier.init();
821
+
822
+ let project = classifier.getProjectInfo();
823
+ console.log("loaded classifier", project);
824
+
825
+ window.classifier = classifier;
826
+ }
827
+
828
+ devicePair.addEventListener("deviceIsConnected", (event) => {
829
+ if (event.message.device.isUkaton) {
830
+ loadClassifier();
831
+ }
832
+ });
833
+
834
+ /** @param {number[]} features */
835
+ function classify(features) {
836
+ try {
837
+ let res = classifier.classify(features);
838
+ // console.log(res);
839
+ const didPinch = res.results[1].value > 0.5;
840
+ if (didPinch) {
841
+ console.log("pinch");
842
+ window.dispatchEvent(new Event("pinch"));
843
+ lastTimeGestureRecognized = Date.now();
844
+ }
845
+ } catch (ex) {
846
+ console.error("Failed to classify", ex);
847
+ }
848
+ }
849
+
850
+ // PINCH MODEL BUFFER
851
+ const time = 600; // ms
852
+ const numberOfSamples = time / pinchSensorRate;
853
+ const numberOfFeaturesInEachSensorType = {};
854
+ BS.TfliteSensorTypes.forEach((sensorType) => {
855
+ switch (sensorType) {
856
+ case "pressure":
857
+ numberOfFeaturesInEachSensorType[sensorType] = 8; // change to 16 for ukaton
858
+ break;
859
+ case "linearAcceleration":
860
+ case "gyroscope":
861
+ case "magnetometer":
862
+ numberOfFeaturesInEachSensorType[sensorType] = 3;
863
+ break;
864
+ }
865
+ });
866
+ let numberOfFeaturesInOneSample = 0;
867
+ pinchSensorTypes.forEach((sensorType) => {
868
+ numberOfFeaturesInOneSample += numberOfFeaturesInEachSensorType[sensorType];
869
+ });
870
+ const numberOfFeatures = numberOfFeaturesInOneSample * numberOfSamples;
871
+ console.log({
872
+ time,
873
+ numberOfSamples,
874
+ numberOfFeaturesInOneSample,
875
+ numberOfFeatures,
876
+ });
877
+ const samples = [];
878
+ let pendingSample;
879
+ let lastTimeClassified = 0;
880
+ let lastTimeGestureRecognized = 0;
881
+ let classificationDelay = 0;
882
+ let gestureDelay = 1000;
883
+ let isClassifying = false;
884
+ /**
885
+ * @param {number} timestamp
886
+ * @param {BS.TfliteSensorType} sensorType
887
+ * @param {number[]} data
888
+ */
889
+ function appendData(timestamp, sensorType, data) {
890
+ //console.log({ timestamp, sensorType, data });
891
+ if (!pendingSample || timestamp != pendingSample.timestamp) {
892
+ pendingSample = { timestamp };
893
+ //console.log("pendingSample", pendingSample);
894
+ }
895
+ pendingSample[sensorType] = data;
896
+ const gotAllSensorSamples = pinchSensorTypes.every(
897
+ (sensorType) => sensorType in pendingSample
898
+ );
899
+ if (gotAllSensorSamples) {
900
+ //console.log("got all samples");
901
+ samples.push(pendingSample);
902
+ pendingSample = undefined;
903
+ }
904
+
905
+ while (samples.length > numberOfSamples) {
906
+ samples.shift();
907
+ }
908
+
909
+ if (!isClassifying && samples.length == numberOfSamples) {
910
+ const now = Date.now();
911
+ if (
912
+ now - lastTimeGestureRecognized < gestureDelay ||
913
+ now - lastTimeClassified < classificationDelay
914
+ ) {
915
+ return;
916
+ }
917
+ const features = [];
918
+ samples.forEach((sample) => {
919
+ const _features = [];
920
+ pinchSensorTypes.forEach((sensorType) => {
921
+ _features.push(...sample[sensorType]);
922
+ features.push(..._features);
923
+ });
924
+ });
925
+ isClassifying = true;
926
+ //console.log("classifying", features);
927
+ classify(features);
928
+ isClassifying = false;
929
+ lastTimeClassified = now;
930
+ }
931
+ }