@wandelbots/nova-js 3.2.0-pr.dev-e2e-jogging-test.143.4f02caf → 3.2.0-pr.feat-v2.155.e91b019

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 (66) hide show
  1. package/README.md +60 -50
  2. package/dist/{AutoReconnectingWebsocket-BI1ckzP8.d.ts → AutoReconnectingWebsocket-CoU4ZyD2.d.cts} +1 -3
  3. package/dist/AutoReconnectingWebsocket-CoU4ZyD2.d.cts.map +1 -0
  4. package/dist/{AutoReconnectingWebsocket-Cr7f9016.d.cts → AutoReconnectingWebsocket-D0gTrkzu.d.ts} +1 -3
  5. package/dist/AutoReconnectingWebsocket-D0gTrkzu.d.ts.map +1 -0
  6. package/dist/{LoginWithAuth0-0g0wWRUC.js → LoginWithAuth0-CaX7yo7d.js} +58 -5
  7. package/dist/LoginWithAuth0-CaX7yo7d.js.map +1 -0
  8. package/dist/{LoginWithAuth0-C82OCyDy.cjs → LoginWithAuth0-DaPnTz2I.cjs} +99 -4
  9. package/dist/LoginWithAuth0-DaPnTz2I.cjs.map +1 -0
  10. package/dist/index.cjs +11 -10
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +11 -8
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.ts +11 -8
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -3
  17. package/dist/lib/v1/index.cjs +17 -19
  18. package/dist/lib/v1/index.cjs.map +1 -1
  19. package/dist/lib/v1/index.d.cts +4 -2
  20. package/dist/lib/v1/index.d.cts.map +1 -1
  21. package/dist/lib/v1/index.d.ts +4 -2
  22. package/dist/lib/v1/index.d.ts.map +1 -1
  23. package/dist/lib/v1/index.js +7 -10
  24. package/dist/lib/v1/index.js.map +1 -1
  25. package/dist/lib/v2/index.cjs +1087 -11
  26. package/dist/lib/v2/index.cjs.map +1 -1
  27. package/dist/lib/v2/index.d.cts +256 -7
  28. package/dist/lib/v2/index.d.cts.map +1 -1
  29. package/dist/lib/v2/index.d.ts +256 -7
  30. package/dist/lib/v2/index.d.ts.map +1 -1
  31. package/dist/lib/v2/index.js +1082 -12
  32. package/dist/lib/v2/index.js.map +1 -1
  33. package/dist/wandelscriptUtils-CO5GYRij.js +24 -0
  34. package/dist/wandelscriptUtils-CO5GYRij.js.map +1 -0
  35. package/dist/wandelscriptUtils-COHpTIme.d.cts +12 -0
  36. package/dist/wandelscriptUtils-COHpTIme.d.cts.map +1 -0
  37. package/dist/wandelscriptUtils-Cl3GBxOp.d.ts +12 -0
  38. package/dist/wandelscriptUtils-Cl3GBxOp.d.ts.map +1 -0
  39. package/dist/wandelscriptUtils-DwpJ4jCy.cjs +30 -0
  40. package/dist/wandelscriptUtils-DwpJ4jCy.cjs.map +1 -0
  41. package/package.json +2 -2
  42. package/src/LoginWithAuth0.ts +3 -3
  43. package/src/index.ts +1 -0
  44. package/src/lib/converters.ts +5 -23
  45. package/src/lib/v1/MotionStreamConnection.ts +1 -1
  46. package/src/lib/v1/NovaClient.ts +6 -0
  47. package/src/lib/v1/index.ts +6 -0
  48. package/src/lib/v1/mock/MockNovaInstance.ts +0 -1
  49. package/src/lib/v1/wandelscriptUtils.ts +22 -0
  50. package/src/lib/v2/ConnectedMotionGroup.ts +415 -0
  51. package/src/lib/v2/JoggerConnection.ts +647 -0
  52. package/src/lib/v2/MotionStreamConnection.ts +222 -0
  53. package/src/lib/v2/NovaClient.ts +43 -8
  54. package/src/lib/v2/index.ts +5 -0
  55. package/src/lib/v2/mock/MockNovaInstance.ts +385 -1
  56. package/src/lib/v2/motionStateUpdate.ts +76 -0
  57. package/src/lib/v2/types/vector3.ts +1 -0
  58. package/src/lib/v2/wandelscriptUtils.ts +27 -0
  59. package/dist/AutoReconnectingWebsocket-BI1ckzP8.d.ts.map +0 -1
  60. package/dist/AutoReconnectingWebsocket-Cr7f9016.d.cts.map +0 -1
  61. package/dist/LoginWithAuth0-0g0wWRUC.js.map +0 -1
  62. package/dist/LoginWithAuth0-C82OCyDy.cjs.map +0 -1
  63. package/dist/converters-DP2EIVv6.cjs +0 -108
  64. package/dist/converters-DP2EIVv6.cjs.map +0 -1
  65. package/dist/converters-DY6Lf7mb.js +0 -66
  66. package/dist/converters-DY6Lf7mb.js.map +0 -1
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./lib/availableStorage"
3
3
  export * from "./lib/converters"
4
4
  export * from "./lib/errorHandling"
5
5
  export * from "./LoginWithAuth0"
6
+ export * from "./lib/v1/wandelscriptUtils"
@@ -1,5 +1,3 @@
1
- import type { Pose } from "@wandelbots/nova-api/v1"
2
-
3
1
  /** Try to parse something as JSON; return undefined if we can't */
4
2
  // biome-ignore lint/suspicious/noExplicitAny: it's json
5
3
  export function tryParseJson(json: unknown): any {
@@ -39,27 +37,6 @@ export function degreesToRadians(degrees: number): number {
39
37
  return degrees * (Math.PI / 180)
40
38
  }
41
39
 
42
- /**
43
- * Convert a Pose object representing a motion group position
44
- * into a string which represents that pose in Wandelscript.
45
- */
46
- export function poseToWandelscriptString(
47
- pose: Pick<Pose, "position" | "orientation">,
48
- ) {
49
- const position = [pose.position.x, pose.position.y, pose.position.z]
50
- const orientation = [
51
- pose.orientation?.x ?? 0,
52
- pose.orientation?.y ?? 0,
53
- pose.orientation?.z ?? 0,
54
- ]
55
-
56
- const positionValues = position.map((v) => v.toFixed(1))
57
- // Rotation needs more precision since it's in radians
58
- const rotationValues = orientation.map((v) => v.toFixed(4))
59
-
60
- return `(${positionValues.concat(rotationValues).join(", ")})`
61
- }
62
-
63
40
  /**
64
41
  * Check for coordinate system id equivalence, accounting for the "world" default
65
42
  * on empty/undefined values.
@@ -73,3 +50,8 @@ export function isSameCoordinateSystem(
73
50
 
74
51
  return firstCoordSystem === secondCoordSystem
75
52
  }
53
+
54
+ /**
55
+ * Helpful const for converting {x, y, z} to [x, y, z] and vice versa
56
+ */
57
+ export const XYZ_TO_VECTOR = { x: 0, y: 1, z: 2 }
@@ -81,7 +81,7 @@ export class MotionStreamConnection {
81
81
 
82
82
  // Wait for the first message to get the initial state
83
83
  const firstMessage = await motionStateSocket.firstMessage()
84
- console.log("got first message", firstMessage)
84
+ console.log("got first message", tryParseJson(firstMessage.data))
85
85
  const initialMotionState = tryParseJson(firstMessage.data)
86
86
  ?.result as MotionGroupStateResponse
87
87
 
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @fileoverview
3
+ * @deprecated The nova v1 client is deprecated. Please use the v2 client from `@wandelbots/nova-js/v2` instead.
4
+ */
5
+
1
6
  import type { Configuration as BaseConfiguration } from "@wandelbots/nova-api/v1"
2
7
  import type { AxiosRequestConfig } from "axios"
3
8
  import axios, { isAxiosError } from "axios"
@@ -54,6 +59,7 @@ function permissiveInstanceUrlParse(url: string): string {
54
59
 
55
60
  /**
56
61
  * Client for connecting to a Nova instance and controlling robots.
62
+ * @deprecated The nova v1 client is deprecated. Please use the v2 client from `@wandelbots/nova-js/v2` instead.
57
63
  */
58
64
  export class NovaClient {
59
65
  readonly api: NovaCellAPIClient
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @fileoverview
3
+ * @deprecated The nova v1 client is deprecated. Please use the v2 client from `@wandelbots/nova-js/v2` instead.
4
+ */
5
+
1
6
  export * from "@wandelbots/nova-api/v1"
2
7
  export * from "./ConnectedMotionGroup"
3
8
  export * from "./getLatestTrajectories"
@@ -6,3 +11,4 @@ export * from "./MotionStreamConnection"
6
11
  export * from "./NovaCellAPIClient"
7
12
  export * from "./NovaClient"
8
13
  export * from "./ProgramStateConnection"
14
+ export * from "./wandelscriptUtils"
@@ -11,7 +11,6 @@ import * as pathToRegexp from "path-to-regexp"
11
11
  import type { AutoReconnectingWebsocket } from "../../AutoReconnectingWebsocket"
12
12
 
13
13
  /**
14
- * EXPERIMENTAL
15
14
  * Ultra-simplified mock Nova server for testing stuff
16
15
  */
17
16
  export class MockNovaInstance {
@@ -0,0 +1,22 @@
1
+ import type { Pose } from "@wandelbots/nova-api/v1"
2
+
3
+ /**
4
+ * Convert a Pose object representing a motion group position
5
+ * into a string which represents that pose in Wandelscript.
6
+ */
7
+ export function poseToWandelscriptString(
8
+ pose: Pick<Pose, "position" | "orientation">,
9
+ ) {
10
+ const position = [pose.position.x, pose.position.y, pose.position.z]
11
+ const orientation = [
12
+ pose.orientation?.x ?? 0,
13
+ pose.orientation?.y ?? 0,
14
+ pose.orientation?.z ?? 0,
15
+ ]
16
+
17
+ const positionValues = position.map((v) => v.toFixed(1))
18
+ // Rotation needs more precision since it's in radians
19
+ const rotationValues = orientation.map((v) => v.toFixed(4))
20
+
21
+ return `(${positionValues.concat(rotationValues).join(", ")})`
22
+ }
@@ -0,0 +1,415 @@
1
+ import type {
2
+ MotionGroupDescription,
3
+ MotionGroupState,
4
+ RobotControllerState,
5
+ OperationMode,
6
+ SafetyStateType,
7
+ } from "@wandelbots/nova-api/v2"
8
+ import { makeAutoObservable, runInAction } from "mobx"
9
+ import * as THREE from "three"
10
+ import type { AutoReconnectingWebsocket } from "../AutoReconnectingWebsocket"
11
+ import { tryParseJson } from "../converters"
12
+ import { jointValuesEqual, tcpMotionEqual } from "./motionStateUpdate"
13
+ import type { NovaClient } from "./NovaClient"
14
+ import type { Vector3Simple } from "./types/vector3"
15
+
16
+ const MOTION_DELTA_THRESHOLD = 0.0001
17
+
18
+ export type RobotTcpLike = {
19
+ id: string
20
+ readable_name: string
21
+ position: Vector3Simple
22
+ orientation: Vector3Simple
23
+ }
24
+
25
+ export type MotionGroupOption = {
26
+ selectionId: string
27
+ }
28
+
29
+ /**
30
+ * Store representing the current state of a connected motion group.
31
+ */
32
+ export class ConnectedMotionGroup {
33
+ static async connect(nova: NovaClient, motionGroupId: string) {
34
+ const [_motionGroupIndex, controllerId] = motionGroupId.split("@") as [
35
+ string,
36
+ string,
37
+ ]
38
+
39
+ const controller =
40
+ await nova.api.controller.getCurrentRobotControllerState(controllerId)
41
+ const motionGroup = controller?.motion_groups.find(
42
+ (mg) => mg.motion_group === motionGroupId,
43
+ )
44
+ if (!controller || !motionGroup) {
45
+ throw new Error(
46
+ `Controller ${controllerId} or motion group ${motionGroupId} not found`,
47
+ )
48
+ }
49
+
50
+ const motionStateSocket = nova.openReconnectingWebsocket(
51
+ `/controllers/${controllerId}/motion-groups/${motionGroupId}/state-stream`,
52
+ )
53
+
54
+ // Wait for the first message to get the initial state
55
+ const firstMessage = await motionStateSocket.firstMessage()
56
+ const initialMotionState = tryParseJson(firstMessage.data)
57
+ ?.result as MotionGroupState
58
+
59
+ if (!initialMotionState) {
60
+ throw new Error(
61
+ `Unable to parse initial motion state message ${firstMessage.data}`,
62
+ )
63
+ }
64
+
65
+ console.log(
66
+ `Connected motion state websocket to motion group ${motionGroup.motion_group}. Initial state:\n `,
67
+ initialMotionState,
68
+ )
69
+
70
+ // Check if robot is virtual or physical
71
+ const config = await nova.api.controller.getRobotController(
72
+ controller.controller,
73
+ )
74
+ const isVirtual = config.configuration.kind === "VirtualController"
75
+
76
+ // If there's a configured mounting, we need it to show the right
77
+ // position of the robot model
78
+ const description = await nova.api.motionGroup.getMotionGroupDescription(
79
+ controllerId,
80
+ motionGroup.motion_group,
81
+ )
82
+
83
+ // Find out what TCPs this motion group has (we need it for jogging)
84
+ // There are converted into a RobotTcpLike for easier use in the UI
85
+ const tcps: RobotTcpLike[] = Object.entries(description.tcps || {}).map(
86
+ ([id, tcp]) => ({
87
+ id,
88
+ readable_name: tcp.name,
89
+ position: tcp.pose.position as Vector3Simple,
90
+ orientation: tcp.pose.orientation as Vector3Simple,
91
+ }),
92
+ )
93
+
94
+ // Open the websocket to monitor controller state for e.g. e-stop
95
+ const controllerStateSocket = nova.openReconnectingWebsocket(
96
+ `/controllers/${controller.controller}/state-stream?response_rate=1000`,
97
+ )
98
+
99
+ // Wait for the first message to get the initial state
100
+ const firstControllerMessage = await controllerStateSocket.firstMessage()
101
+ const initialControllerState = tryParseJson(firstControllerMessage.data)
102
+ ?.result as RobotControllerState
103
+
104
+ if (!initialControllerState) {
105
+ throw new Error(
106
+ `Unable to parse initial controller state message ${firstControllerMessage.data}`,
107
+ )
108
+ }
109
+
110
+ console.log(
111
+ `Connected controller state websocket to controller ${controller.controller}. Initial state:\n `,
112
+ initialControllerState,
113
+ )
114
+
115
+ return new ConnectedMotionGroup(
116
+ nova,
117
+ controller,
118
+ motionGroup,
119
+ initialMotionState,
120
+ motionStateSocket,
121
+ isVirtual,
122
+ tcps,
123
+ description,
124
+ initialControllerState,
125
+ controllerStateSocket,
126
+ )
127
+ }
128
+
129
+ connectedJoggingSocket: WebSocket | null = null
130
+ // biome-ignore lint/suspicious/noExplicitAny: legacy code
131
+ planData: any | null // tmp
132
+ joggingVelocity: number = 10
133
+
134
+ // Not mobx-observable as this changes very fast; should be observed
135
+ // using animation frames
136
+ rapidlyChangingMotionState: MotionGroupState
137
+
138
+ // Response rate on the websocket should be a bit slower on this one since
139
+ // we don't use the motion data
140
+ controllerState: RobotControllerState
141
+
142
+ /**
143
+ * Reflects activation state of the motion group / robot servos. The
144
+ * movement controls in the UI should only be enabled in the "active" state
145
+ */
146
+ activationState: "inactive" | "activating" | "deactivating" | "active" =
147
+ "inactive"
148
+
149
+ constructor(
150
+ readonly nova: NovaClient,
151
+ readonly controller: RobotControllerState,
152
+ readonly motionGroup: MotionGroupState,
153
+ readonly initialMotionState: MotionGroupState,
154
+ readonly motionStateSocket: AutoReconnectingWebsocket,
155
+ readonly isVirtual: boolean,
156
+ readonly tcps: RobotTcpLike[],
157
+ readonly description: MotionGroupDescription,
158
+ readonly initialControllerState: RobotControllerState,
159
+ readonly controllerStateSocket: AutoReconnectingWebsocket,
160
+ ) {
161
+ this.rapidlyChangingMotionState = initialMotionState
162
+ this.controllerState = initialControllerState
163
+
164
+ // Track controller state updates (e.g. safety state and operation mode)
165
+ controllerStateSocket.addEventListener("message", (event) => {
166
+ const data = tryParseJson(event.data)?.result
167
+
168
+ if (!data) {
169
+ return
170
+ }
171
+
172
+ runInAction(() => {
173
+ this.controllerState = data
174
+ })
175
+ })
176
+
177
+ motionStateSocket.addEventListener("message", (event) => {
178
+ const latestMotionState = tryParseJson(event.data)?.result as
179
+ | MotionGroupState
180
+ | undefined
181
+
182
+ if (!latestMotionState) {
183
+ throw new Error(
184
+ `Failed to get motion state for ${this.motionGroupId}: ${event.data}`,
185
+ )
186
+ }
187
+
188
+ // handle motionState message
189
+ if (
190
+ !jointValuesEqual(
191
+ this.rapidlyChangingMotionState.joint_position,
192
+ latestMotionState.joint_position,
193
+ MOTION_DELTA_THRESHOLD,
194
+ )
195
+ ) {
196
+ runInAction(() => {
197
+ this.rapidlyChangingMotionState = latestMotionState
198
+ })
199
+ }
200
+
201
+ // handle tcpPose message
202
+ if (
203
+ !tcpMotionEqual(
204
+ this.rapidlyChangingMotionState,
205
+ latestMotionState,
206
+ MOTION_DELTA_THRESHOLD,
207
+ )
208
+ ) {
209
+ runInAction(() => {
210
+ this.rapidlyChangingMotionState.tcp_pose = latestMotionState.tcp_pose
211
+ })
212
+ }
213
+
214
+ // handle standstill changes
215
+ if (
216
+ this.rapidlyChangingMotionState.standstill !==
217
+ latestMotionState.standstill
218
+ ) {
219
+ runInAction(() => {
220
+ this.rapidlyChangingMotionState.standstill =
221
+ latestMotionState.standstill
222
+ })
223
+ }
224
+ })
225
+ makeAutoObservable(this)
226
+ }
227
+
228
+ get motionGroupId() {
229
+ return this.motionGroup.motion_group
230
+ }
231
+
232
+ get controllerId() {
233
+ return this.controller.controller
234
+ }
235
+
236
+ get modelFromController() {
237
+ return this.description.motion_group_model
238
+ }
239
+
240
+ get wandelscriptIdentifier() {
241
+ const num = this.motionGroupId.split("@")[0]
242
+ return `${this.controllerId.replaceAll("-", "_")}_${num}`
243
+ }
244
+
245
+ /** Jogging velocity in radians for rotation and joint movement */
246
+ get joggingVelocityRads() {
247
+ return (this.joggingVelocity * Math.PI) / 180
248
+ }
249
+
250
+ get joints() {
251
+ return this.initialMotionState.joint_position.map((_, i) => {
252
+ return {
253
+ index: i,
254
+ }
255
+ })
256
+ }
257
+
258
+ get dhParameters() {
259
+ return this.description.dh_parameters
260
+ }
261
+
262
+ get safetyZones() {
263
+ return this.description.safety_zones
264
+ }
265
+
266
+ /** Gets the robot mounting position offset in 3D viz coordinates */
267
+ get mountingPosition(): [number, number, number] {
268
+ if (!this.description.mounting) {
269
+ return [0, 0, 0]
270
+ }
271
+
272
+ return [
273
+ (this.description.mounting.position?.[0] || 0) / 1000,
274
+ (this.description.mounting.position?.[1] || 0) / 1000,
275
+ (this.description.mounting.position?.[2] || 0) / 1000,
276
+ ]
277
+ }
278
+
279
+ /** Gets the robot mounting position rotation in 3D viz coordinates */
280
+ get mountingQuaternion() {
281
+ const rotationVector = new THREE.Vector3(
282
+ this.description.mounting?.orientation?.[0] || 0,
283
+ this.description.mounting?.orientation?.[1] || 0,
284
+ this.description.mounting?.orientation?.[2] || 0,
285
+ )
286
+
287
+ const magnitude = rotationVector.length()
288
+ const axis = rotationVector.normalize()
289
+
290
+ return new THREE.Quaternion().setFromAxisAngle(axis, magnitude)
291
+ }
292
+
293
+ /**
294
+ * Whether the controller is currently in a safety state
295
+ * corresponding to an emergency stop
296
+ */
297
+ get isEstopActive() {
298
+ const estopStates: SafetyStateType[] = [
299
+ "SAFETY_STATE_ROBOT_EMERGENCY_STOP",
300
+ "SAFETY_STATE_DEVICE_EMERGENCY_STOP",
301
+ ]
302
+
303
+ return estopStates.includes(this.controllerState.safety_state)
304
+ }
305
+
306
+ /**
307
+ * Whether the controller is in a safety state
308
+ * that may be non-functional for robot pad purposes
309
+ */
310
+ get isMoveableSafetyState() {
311
+ const goodSafetyStates: SafetyStateType[] = [
312
+ "SAFETY_STATE_NORMAL",
313
+ "SAFETY_STATE_REDUCED",
314
+ ]
315
+
316
+ return goodSafetyStates.includes(this.controllerState.safety_state)
317
+ }
318
+
319
+ /**
320
+ * Whether the controller is in an operation mode that allows movement
321
+ */
322
+ get isMoveableOperationMode() {
323
+ const goodOperationModes: OperationMode[] = [
324
+ "OPERATION_MODE_AUTO",
325
+ "OPERATION_MODE_MANUAL",
326
+ "OPERATION_MODE_MANUAL_T1",
327
+ "OPERATION_MODE_MANUAL_T2",
328
+ ]
329
+
330
+ return goodOperationModes.includes(this.controllerState.operation_mode)
331
+ }
332
+
333
+ /**
334
+ * Whether the robot is currently active and can be moved, based on the
335
+ * safety state, operation mode and servo toggle activation state.
336
+ */
337
+ get canBeMoved() {
338
+ return (
339
+ this.isMoveableSafetyState &&
340
+ this.isMoveableOperationMode &&
341
+ this.activationState === "active"
342
+ )
343
+ }
344
+
345
+ async deactivate() {
346
+ if (this.activationState !== "active") {
347
+ console.error("Tried to deactivate while already deactivating")
348
+ return
349
+ }
350
+
351
+ runInAction(() => {
352
+ this.activationState = "deactivating"
353
+ })
354
+
355
+ try {
356
+ await this.nova.api.controller.setDefaultMode(
357
+ this.controllerId,
358
+ "ROBOT_SYSTEM_MODE_MONITOR",
359
+ )
360
+
361
+ runInAction(() => {
362
+ this.activationState = "inactive"
363
+ })
364
+ } catch (err) {
365
+ runInAction(() => {
366
+ this.activationState = "active"
367
+ })
368
+ throw err
369
+ }
370
+ }
371
+
372
+ async activate() {
373
+ if (this.activationState !== "inactive") {
374
+ console.error("Tried to activate while already activating")
375
+ return
376
+ }
377
+
378
+ runInAction(() => {
379
+ this.activationState = "activating"
380
+ })
381
+
382
+ try {
383
+ await this.nova.api.controller.setDefaultMode(
384
+ this.controllerId,
385
+ "ROBOT_SYSTEM_MODE_CONTROL",
386
+ )
387
+
388
+ runInAction(() => {
389
+ this.activationState = "active"
390
+ })
391
+ } catch (err) {
392
+ runInAction(() => {
393
+ this.activationState = "inactive"
394
+ })
395
+ throw err
396
+ }
397
+ }
398
+
399
+ toggleActivation() {
400
+ if (this.activationState === "inactive") {
401
+ this.activate()
402
+ } else if (this.activationState === "active") {
403
+ this.deactivate()
404
+ }
405
+ }
406
+
407
+ dispose() {
408
+ this.motionStateSocket.close()
409
+ if (this.connectedJoggingSocket) this.connectedJoggingSocket.close()
410
+ }
411
+
412
+ setJoggingVelocity(velocity: number) {
413
+ this.joggingVelocity = velocity
414
+ }
415
+ }