@viamrobotics/motion-tools 1.13.0 → 1.13.1

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 (83) hide show
  1. package/dist/FrameConfigUpdater.svelte.js +1 -1
  2. package/dist/WorldObject.svelte.js +26 -13
  3. package/dist/buf/draw/v1/service_connect.d.ts +14 -25
  4. package/dist/buf/draw/v1/service_connect.js +14 -25
  5. package/dist/buf/draw/v1/service_pb.d.ts +61 -76
  6. package/dist/buf/draw/v1/service_pb.js +58 -111
  7. package/dist/color.js +3 -3
  8. package/dist/components/App.svelte +1 -5
  9. package/dist/components/Camera.svelte +1 -7
  10. package/dist/components/Camera.svelte.d.ts +0 -1
  11. package/dist/components/CameraControls.svelte +2 -2
  12. package/dist/components/{Arrows → Entities/Arrows}/ArrowGroups.svelte +3 -3
  13. package/dist/components/{Arrows → Entities/Arrows}/Arrows.svelte +6 -6
  14. package/dist/components/{Arrows → Entities/Arrows}/Arrows.svelte.d.ts +1 -1
  15. package/dist/components/{Entities.svelte → Entities/Entities.svelte} +5 -1
  16. package/dist/components/{Frame.svelte → Entities/Frame.svelte} +8 -8
  17. package/dist/components/{GLTF.svelte → Entities/GLTF.svelte} +5 -5
  18. package/dist/components/{Geometry.svelte → Entities/Geometry.svelte} +8 -13
  19. package/dist/components/{Label.svelte → Entities/Label.svelte} +1 -1
  20. package/dist/components/{Line.svelte → Entities/Line.svelte} +2 -2
  21. package/dist/components/{Points.svelte → Entities/Points.svelte} +5 -5
  22. package/dist/components/{Pose.svelte → Entities/Pose.svelte} +3 -3
  23. package/dist/{hooks/useObjectEvents.svelte.d.ts → components/Entities/hooks/useEntityEvents.svelte.d.ts} +1 -1
  24. package/dist/{hooks/useObjectEvents.svelte.js → components/Entities/hooks/useEntityEvents.svelte.js} +6 -6
  25. package/dist/components/FileDrop/file-names.js +6 -3
  26. package/dist/components/FileDrop/snapshot-dropper.js +8 -4
  27. package/dist/components/FileDrop/useFileDrop.svelte.js +9 -6
  28. package/dist/components/Lasso/Lasso.svelte +4 -4
  29. package/dist/components/PCD.svelte +14 -6
  30. package/dist/components/PCD.svelte.d.ts +2 -0
  31. package/dist/components/Scene.svelte +1 -3
  32. package/dist/components/StaticGeometries.svelte +1 -1
  33. package/dist/components/overlay/AddRelationship.svelte +1 -1
  34. package/dist/components/overlay/LiveUpdatesBanner.svelte +4 -6
  35. package/dist/components/overlay/settings/Settings.svelte +11 -13
  36. package/dist/components/overlay/widgets/Camera.svelte +11 -9
  37. package/dist/components/xr/ArmTeleop.svelte +33 -33
  38. package/dist/components/xr/CameraFeed.svelte +21 -23
  39. package/dist/components/xr/JointLimitsWidget.svelte +19 -5
  40. package/dist/components/xr/XRConfigPanel.svelte +17 -16
  41. package/dist/components/xr/XRControllerSettings.svelte +5 -4
  42. package/dist/components/xr/XRToast.svelte +11 -6
  43. package/dist/ecs/relations.d.ts +1 -0
  44. package/dist/ecs/relations.js +1 -0
  45. package/dist/ecs/useQuery.svelte.js +3 -3
  46. package/dist/format.js +1 -1
  47. package/dist/hooks/use3DModels.svelte.js +35 -35
  48. package/dist/hooks/useConfigFrames.svelte.js +2 -7
  49. package/dist/hooks/useDrawAPI.svelte.js +1 -1
  50. package/dist/hooks/useFramelessComponents.svelte.js +1 -1
  51. package/dist/hooks/useFrames.svelte.js +63 -56
  52. package/dist/hooks/useGeometries.svelte.js +1 -1
  53. package/dist/hooks/useLinked.svelte.js +5 -4
  54. package/dist/hooks/usePartConfig.svelte.js +16 -17
  55. package/dist/hooks/usePointcloudObjects.svelte.js +4 -4
  56. package/dist/hooks/usePointclouds.svelte.js +2 -4
  57. package/dist/hooks/usePose.svelte.js +3 -7
  58. package/dist/hooks/useResizable.svelte.js +4 -3
  59. package/dist/hooks/useSettings.svelte.js +2 -2
  60. package/dist/hooks/useWeblabs.svelte.js +3 -2
  61. package/dist/hooks/useWorldState.svelte.js +6 -3
  62. package/dist/loaders/pcd/index.js +2 -1
  63. package/dist/loaders/pcd/worker.inline.d.ts +1 -1
  64. package/dist/loaders/pcd/worker.inline.js +1 -1
  65. package/dist/loaders/pcd/worker.js +1 -1
  66. package/dist/snapshot.js +7 -5
  67. package/dist/three/InstancedArrows/InstancedArrows.js +1 -1
  68. package/dist/three/InstancedArrows/box.js +1 -1
  69. package/dist/three/InstancedArrows/raycast.js +12 -12
  70. package/package.json +19 -11
  71. /package/dist/components/{Arrows → Entities/Arrows}/ArrowGroups.svelte.d.ts +0 -0
  72. /package/dist/components/{Entities.svelte.d.ts → Entities/Entities.svelte.d.ts} +0 -0
  73. /package/dist/components/{Frame.svelte.d.ts → Entities/Frame.svelte.d.ts} +0 -0
  74. /package/dist/components/{GLTF.svelte.d.ts → Entities/GLTF.svelte.d.ts} +0 -0
  75. /package/dist/components/{Geometry.svelte.d.ts → Entities/Geometry.svelte.d.ts} +0 -0
  76. /package/dist/components/{Label.svelte.d.ts → Entities/Label.svelte.d.ts} +0 -0
  77. /package/dist/components/{Line.svelte.d.ts → Entities/Line.svelte.d.ts} +0 -0
  78. /package/dist/components/{LineDots.svelte → Entities/LineDots.svelte} +0 -0
  79. /package/dist/components/{LineDots.svelte.d.ts → Entities/LineDots.svelte.d.ts} +0 -0
  80. /package/dist/components/{LineGeometry.svelte → Entities/LineGeometry.svelte} +0 -0
  81. /package/dist/components/{LineGeometry.svelte.d.ts → Entities/LineGeometry.svelte.d.ts} +0 -0
  82. /package/dist/components/{Points.svelte.d.ts → Entities/Points.svelte.d.ts} +0 -0
  83. /package/dist/components/{Pose.svelte.d.ts → Entities/Pose.svelte.d.ts} +0 -0
@@ -81,16 +81,20 @@ const decodeGzip = async (params) => {
81
81
  };
82
82
  export const snapshotDropper = async (params) => {
83
83
  switch (params.extension) {
84
- case 'json':
84
+ case 'json': {
85
85
  return decodeJson(params);
86
- case 'pb':
86
+ }
87
+ case 'pb': {
87
88
  return decodeBinary(params);
88
- case 'pb.gz':
89
+ }
90
+ case 'pb.gz': {
89
91
  return decodeGzip(params);
90
- default:
92
+ }
93
+ default: {
91
94
  return {
92
95
  success: false,
93
96
  error: new FileDropperError(`Only ${Extensions.JSON}, ${Extensions.PB} and ${Extensions.PB_GZ} snapshot files are supported.`),
94
97
  };
98
+ }
95
99
  }
96
100
  };
@@ -4,14 +4,17 @@ import { plyDropper } from './ply-dropper';
4
4
  import { snapshotDropper } from './snapshot-dropper';
5
5
  const createFileDropper = (extension, prefix) => {
6
6
  switch (prefix) {
7
- case Prefixes.Snapshot:
7
+ case Prefixes.Snapshot: {
8
8
  return snapshotDropper;
9
+ }
9
10
  }
10
11
  switch (extension) {
11
- case Extensions.PCD:
12
+ case Extensions.PCD: {
12
13
  return pcdDropper;
13
- case Extensions.PLY:
14
+ }
15
+ case Extensions.PLY: {
14
16
  return plyDropper;
17
+ }
15
18
  }
16
19
  return undefined;
17
20
  };
@@ -74,11 +77,11 @@ export const useFileDrop = (onSuccess, onError) => {
74
77
  prefix,
75
78
  content,
76
79
  });
77
- if (!result.success) {
78
- handleError(result.error.message);
80
+ if (result.success) {
81
+ onSuccess(result);
79
82
  }
80
83
  else {
81
- onSuccess(result);
84
+ handleError(result.error.message);
82
85
  }
83
86
  });
84
87
  readFile(file, reader, extension);
@@ -245,16 +245,16 @@
245
245
  }
246
246
 
247
247
  $effect(() => {
248
- window.addEventListener('keydown', onkeydown)
249
- window.addEventListener('keyup', onkeyup)
248
+ globalThis.addEventListener('keydown', onkeydown)
249
+ globalThis.addEventListener('keyup', onkeyup)
250
250
  dom.addEventListener('pointerdown', onpointerdown)
251
251
  dom.addEventListener('pointermove', onpointermove)
252
252
  dom.addEventListener('pointerup', onpointerup)
253
253
  dom.addEventListener('pointerleave', onpointerleave)
254
254
 
255
255
  return () => {
256
- window.removeEventListener('keydown', onkeydown)
257
- window.removeEventListener('keyup', onkeyup)
256
+ globalThis.removeEventListener('keydown', onkeydown)
257
+ globalThis.removeEventListener('keyup', onkeyup)
258
258
  dom.removeEventListener('pointerdown', onpointerdown)
259
259
  dom.removeEventListener('pointermove', onpointermove)
260
260
  dom.removeEventListener('pointerup', onpointerup)
@@ -2,13 +2,15 @@
2
2
  import { parsePcdInWorker } from '../lib'
3
3
  import { traits, useWorld } from '../ecs'
4
4
  import { createBufferGeometry } from '../attribute'
5
- import type { Entity } from 'koota'
5
+ import type { ConfigurableTrait, Entity } from 'koota'
6
6
 
7
7
  interface Props {
8
8
  data: Uint8Array
9
+ name?: string
10
+ renderOrder?: number
9
11
  }
10
12
 
11
- let { data }: Props = $props()
13
+ let { data, name, renderOrder }: Props = $props()
12
14
 
13
15
  const world = useWorld()
14
16
 
@@ -18,11 +20,17 @@
18
20
  parsePcdInWorker(data).then(({ positions, colors }) => {
19
21
  const geometry = createBufferGeometry(positions, colors)
20
22
 
21
- entity = world.spawn(
22
- traits.Name('Random points'),
23
+ const entityTraits: ConfigurableTrait[] = [
24
+ traits.Name(name ?? 'Random points'),
23
25
  traits.Points,
24
- traits.BufferGeometry(geometry)
25
- )
26
+ traits.BufferGeometry(geometry),
27
+ ]
28
+
29
+ if (renderOrder) {
30
+ entityTraits.push(traits.RenderOrder(renderOrder))
31
+ }
32
+
33
+ entity = world.spawn(...entityTraits)
26
34
  })
27
35
 
28
36
  return () => {
@@ -1,5 +1,7 @@
1
1
  interface Props {
2
2
  data: Uint8Array;
3
+ name?: string;
4
+ renderOrder?: number;
3
5
  }
4
6
  declare const PCD: import("svelte").Component<Props, {}, "">;
5
7
  type PCD = ReturnType<typeof PCD>;
@@ -2,7 +2,7 @@
2
2
  import { ShaderMaterial, Vector3 } from 'three'
3
3
  import { T } from '@threlte/core'
4
4
  import { Environment, Grid, interactivity, PerfMonitor, PortalTarget } from '@threlte/extras'
5
- import Entities from './Entities.svelte'
5
+ import Entities from './Entities/Entities.svelte'
6
6
  import Selected from './Selected.svelte'
7
7
  import Focus from './Focus.svelte'
8
8
  import StaticGeometries from './StaticGeometries.svelte'
@@ -17,7 +17,6 @@
17
17
  import MeasureTool from './MeasureTool/MeasureTool.svelte'
18
18
  import PointerMissBox from './PointerMissBox.svelte'
19
19
  import BatchedArrows from './BatchedArrows.svelte'
20
- import Arrows from './Arrows/ArrowGroups.svelte'
21
20
  import hdrImage from '../assets/ferndale_studio_11_1k.hdr'
22
21
 
23
22
  interface Props {
@@ -103,7 +102,6 @@
103
102
 
104
103
  <Entities />
105
104
  <BatchedArrows />
106
- <Arrows />
107
105
  </T.Group>
108
106
 
109
107
  {@render children?.()}
@@ -12,7 +12,7 @@
12
12
  import { PressedKeys } from 'runed'
13
13
  import { quaternionToPose, vector3ToPose } from '../transform'
14
14
  import { Quaternion, Vector3 } from 'three'
15
- import Frame from './Frame.svelte'
15
+ import Frame from './Entities/Frame.svelte'
16
16
  import { useSettings } from '../hooks/useSettings.svelte'
17
17
  import { useWorld, traits } from '../ecs'
18
18
  import type { Entity } from 'koota'
@@ -17,7 +17,7 @@
17
17
  return allEntities.current
18
18
  .map((e: Entity) => e.get(traits.Name))
19
19
  .filter((n: string | undefined): n is string => n !== undefined && n !== currentEntityName)
20
- .sort()
20
+ .toSorted()
21
21
  })
22
22
 
23
23
  let showRelationshipOptions = $state(false)
@@ -13,12 +13,10 @@
13
13
 
14
14
  <svelte:window
15
15
  onkeydown={(event) => {
16
- if (event.metaKey) {
17
- if (event.key.toLowerCase() === 's') {
18
- event.preventDefault()
19
- event.stopImmediatePropagation()
20
- partConfig.save()
21
- }
16
+ if (event.metaKey && event.key.toLowerCase() === 's') {
17
+ event.preventDefault()
18
+ event.stopImmediatePropagation()
19
+ partConfig.save()
22
20
  }
23
21
  }}
24
22
  />
@@ -285,19 +285,17 @@
285
285
  <Switch
286
286
  on={isWidgetOpen}
287
287
  on:change={(event) => {
288
- if (event.detail) {
289
- settings.current.openCameraWidgets = {
290
- ...settings.current.openCameraWidgets,
291
- [partID.current]: [...currentRobotCameraWidgets, camera.name],
292
- }
293
- } else {
294
- settings.current.openCameraWidgets = {
295
- ...settings.current.openCameraWidgets,
296
- [partID.current]: currentRobotCameraWidgets.filter(
297
- (widget) => widget !== camera.name
298
- ),
299
- }
300
- }
288
+ settings.current.openCameraWidgets = event.detail
289
+ ? {
290
+ ...settings.current.openCameraWidgets,
291
+ [partID.current]: [...currentRobotCameraWidgets, camera.name],
292
+ }
293
+ : {
294
+ ...settings.current.openCameraWidgets,
295
+ [partID.current]: currentRobotCameraWidgets.filter(
296
+ (widget) => widget !== camera.name
297
+ ),
298
+ }
301
299
  }}
302
300
  />
303
301
  </div>
@@ -31,12 +31,14 @@
31
31
  let fpsInterval: ReturnType<typeof setInterval> | undefined
32
32
  let fpsCounterActive = false
33
33
 
34
+ const cleanup = () => {
35
+ if (fpsInterval) clearInterval(fpsInterval)
36
+ fpsCounterActive = false
37
+ }
38
+
34
39
  // Cleanup on destroy
35
40
  $effect(() => {
36
- return () => {
37
- if (fpsInterval) clearInterval(fpsInterval)
38
- fpsCounterActive = false
39
- }
41
+ return cleanup
40
42
  })
41
43
 
42
44
  const onMediaLoad = (e: Event) => {
@@ -89,8 +91,8 @@
89
91
  resolutions = options.map((opt) => ({ width: opt.width, height: opt.height }))
90
92
  isLoading = false
91
93
  })
92
- .catch((e) => {
93
- error = e instanceof Error ? e.message : 'Failed to get stream options'
94
+ .catch((error_) => {
95
+ error = error_ instanceof Error ? error_.message : 'Failed to get stream options'
94
96
  isLoading = false
95
97
  })
96
98
  }
@@ -101,13 +103,13 @@
101
103
  if (!target.value || !streamClient) return
102
104
 
103
105
  const [w, h] = target.value.split('x').map(Number)
104
- if (isNaN(w) || isNaN(h)) return
106
+ if (Number.isNaN(w) || Number.isNaN(h)) return
105
107
 
106
108
  try {
107
109
  await streamClient.setOptions(name, w, h)
108
110
  error = undefined
109
- } catch (err) {
110
- error = err instanceof Error ? err.message : 'Failed to set resolution'
111
+ } catch (error_) {
112
+ error = error_ instanceof Error ? error_.message : 'Failed to set resolution'
111
113
  }
112
114
  }
113
115
  </script>
@@ -24,7 +24,7 @@
24
24
  let {
25
25
  armName,
26
26
  gripperName,
27
- scaleFactor = 1.0,
27
+ scaleFactor = 1,
28
28
  hand = 'right',
29
29
  rotationEnabled = true,
30
30
  }: Props = $props()
@@ -33,6 +33,7 @@
33
33
 
34
34
  // Capture initial prop values — parent uses {#key} to force remount on changes.
35
35
  // Wrapped in an IIFE to avoid Svelte's state_referenced_locally warning.
36
+ // eslint-disable-next-line unicorn/no-unreadable-iife
36
37
  const { initialHand, initialGripperName } = (() => ({
37
38
  initialHand: hand,
38
39
  initialGripperName: gripperName,
@@ -101,16 +102,14 @@
101
102
  const currentSession = $session
102
103
  if (!currentSession) return
103
104
 
104
- const inputSource = Array.from(currentSession.inputSources).find(
105
- (s) => s.handedness === initialHand
106
- )
105
+ const inputSource = [...currentSession.inputSources].find((s) => s.handedness === initialHand)
107
106
  if (!inputSource?.gamepad?.hapticActuators?.length) return
108
107
 
109
108
  const actuator = inputSource.gamepad.hapticActuators[0]
110
109
  if ('pulse' in actuator) {
111
110
  actuator
112
111
  .pulse(intensity, duration)
113
- .catch((e) => console.warn('[ArmTeleop] Haptic pulse failed:', e))
112
+ .catch((error) => console.warn('[ArmTeleop] Haptic pulse failed:', error))
114
113
  }
115
114
  }
116
115
 
@@ -143,9 +142,7 @@
143
142
  const currentSession = $session
144
143
  if (!currentSession || !controller.current) return
145
144
 
146
- const inputSource = Array.from(currentSession.inputSources).find(
147
- (s) => s.handedness === initialHand
148
- )
145
+ const inputSource = [...currentSession.inputSources].find((s) => s.handedness === initialHand)
149
146
 
150
147
  if (!inputSource || !inputSource.gamepad) return
151
148
 
@@ -166,15 +163,16 @@
166
163
  if (armClient.current) {
167
164
  handleStartControl(controller.current)
168
165
  }
169
- } else if (!isPressed && wasPressed) {
170
- // Falling Edge: Stop Control
171
- if (isControlling) {
172
- isControlling = false
173
- // Haptic feedback: short pulse on teleop end
174
- triggerHapticFeedback(0.3, 80)
175
- // Log final position
176
- handleStopControl()
177
- }
166
+ } else if (
167
+ !isPressed &&
168
+ wasPressed && // Falling Edge: Stop Control
169
+ isControlling
170
+ ) {
171
+ isControlling = false
172
+ // Haptic feedback: short pulse on teleop end
173
+ triggerHapticFeedback(0.3, 80)
174
+ // Log final position
175
+ handleStopControl()
178
176
  }
179
177
 
180
178
  // 4. Edge Detection - GRIPPER CONTROL (Trigger)
@@ -186,7 +184,7 @@
186
184
  clearTimeout(gripperStopTimeout)
187
185
  gripperStopTimeout = null
188
186
  }
189
- gripperClient.current.grab().catch((e) => console.warn('Gripper grab failed:', e))
187
+ gripperClient.current.grab().catch((error) => console.warn('Gripper grab failed:', error))
190
188
  } else if (!isTriggerPressed && wasTriggerPressed) {
191
189
  // Trigger released: Open gripper, then stop after 1 second
192
190
  // Clear any pending stop timeout
@@ -194,11 +192,13 @@
194
192
  clearTimeout(gripperStopTimeout)
195
193
  gripperStopTimeout = null
196
194
  }
197
- gripperClient.current.open().catch((e) => console.warn('Gripper open failed:', e))
195
+ gripperClient.current.open().catch((error) => console.warn('Gripper open failed:', error))
198
196
 
199
197
  // Schedule stop after 1 second
200
198
  gripperStopTimeout = setTimeout(() => {
201
- gripperClient?.current?.stop().catch((e) => console.warn('Gripper stop failed:', e))
199
+ gripperClient?.current
200
+ ?.stop()
201
+ .catch((error) => console.warn('Gripper stop failed:', error))
202
202
  gripperStopTimeout = null
203
203
  }, 1000)
204
204
  }
@@ -275,16 +275,16 @@
275
275
 
276
276
  // Haptic feedback: short pulse on teleop start
277
277
  triggerHapticFeedback(0.5, 100)
278
- } catch (e) {
279
- console.error('[ArmTeleop] Failed to start teleop:', e)
278
+ } catch (error) {
279
+ console.error('[ArmTeleop] Failed to start teleop:', error)
280
280
  }
281
281
  }
282
282
 
283
283
  async function handleStopControl() {
284
284
  try {
285
285
  await armClient.current!.getEndPosition()
286
- } catch (e) {
287
- console.error('[ArmTeleop] Failed to get final position:', e)
286
+ } catch (error) {
287
+ console.error('[ArmTeleop] Failed to get final position:', error)
288
288
  }
289
289
  }
290
290
 
@@ -361,7 +361,7 @@
361
361
  lastCommandTime = now
362
362
  isSending = true
363
363
 
364
- if (isNaN(targetPos.x) || isNaN(targetOV.th)) {
364
+ if (Number.isNaN(targetPos.x) || Number.isNaN(targetOV.th)) {
365
365
  console.warn('Teleop Safety: NaN detected', targetPos, targetOV)
366
366
  isSending = false
367
367
  return
@@ -387,12 +387,12 @@
387
387
  if (client) {
388
388
  client
389
389
  .doCommand(VIAM.Struct.fromJson(command))
390
- .catch((e) => {
391
- console.warn('Move failed:', e)
390
+ .catch((error) => {
391
+ console.warn('Move failed:', error)
392
392
  errorTimeout = Date.now() + ERROR_COOLDOWN
393
393
  triggerHapticFeedback(0.8, 200)
394
394
  lastErrorHapticTime = Date.now()
395
- showArmErrorToast(e)
395
+ showArmErrorToast(error)
396
396
  })
397
397
  .finally(() => {
398
398
  isSending = false
@@ -409,12 +409,12 @@
409
409
  oZ: targetOV.z,
410
410
  theta: (targetOV.th * 180) / Math.PI,
411
411
  })
412
- .catch((e) => {
413
- console.warn('Move failed:', e)
412
+ .catch((error) => {
413
+ console.warn('Move failed:', error)
414
414
  errorTimeout = Date.now() + ERROR_COOLDOWN
415
415
  triggerHapticFeedback(0.8, 200)
416
416
  lastErrorHapticTime = Date.now()
417
- showArmErrorToast(e)
417
+ showArmErrorToast(error)
418
418
  })
419
419
  .finally(() => {
420
420
  isSending = false
@@ -434,8 +434,8 @@
434
434
  // Use moveToPosition to return to the saved pose
435
435
  await armClient.current.moveToPosition(savedPose)
436
436
  xrToast.success('Returned to saved position')
437
- } catch (e) {
438
- console.error('[ArmTeleop] Failed to return to saved pose:', e)
437
+ } catch (error) {
438
+ console.error('[ArmTeleop] Failed to return to saved pose:', error)
439
439
  xrToast.danger('Failed to return to position')
440
440
  } finally {
441
441
  isReturning = false
@@ -86,7 +86,7 @@
86
86
  }
87
87
 
88
88
  // Force play to ensure stream is active
89
- video.play().catch((e) => console.warn('Video play failed:', e))
89
+ video.play().catch((error) => console.warn('Video play failed:', error))
90
90
  ready = true
91
91
 
92
92
  // PROFILING: Video ready
@@ -153,28 +153,26 @@
153
153
  const captureTime = metadata.captureTime || metadata.mediaTime
154
154
  const presentationTime = metadata.presentationTime || metadata.expectedDisplayTime
155
155
 
156
- if (captureTime) {
157
- // The times might be in different epochs, so we can only reliably calculate
158
- // the difference between capture and presentation
159
- if (presentationTime) {
160
- // Encoding + Network + Decoding time
161
- const captureToPresentMs = (presentationTime - captureTime) / 1000
162
- metrics.captureToPresent = captureToPresentMs
163
-
164
- // Time since video element presented the frame to when we render it
165
- // This should be very small (< 16ms ideally)
166
- const presentMsRelative = presentationTime / 1000
167
- const timeSincePresentation = now - presentMsRelative
168
-
169
- // Only use this if the time domains seem aligned (value is reasonable)
170
- if (Math.abs(timeSincePresentation) < 1000) {
171
- metrics.presentToRender = timeSincePresentation
172
- metrics.totalLatency = captureToPresentMs + timeSincePresentation
173
- } else {
174
- // Time domains don't align - just use capture to present as approximation
175
- metrics.presentToRender = undefined
176
- metrics.totalLatency = captureToPresentMs
177
- }
156
+ // The times might be in different epochs, so we can only reliably calculate
157
+ // the difference between capture and presentation
158
+ if (captureTime && presentationTime) {
159
+ // Encoding + Network + Decoding time
160
+ const captureToPresentMs = (presentationTime - captureTime) / 1000
161
+ metrics.captureToPresent = captureToPresentMs
162
+
163
+ // Time since video element presented the frame to when we render it
164
+ // This should be very small (< 16ms ideally)
165
+ const presentMsRelative = presentationTime / 1000
166
+ const timeSincePresentation = now - presentMsRelative
167
+
168
+ // Only use this if the time domains seem aligned (value is reasonable)
169
+ if (Math.abs(timeSincePresentation) < 1000) {
170
+ metrics.presentToRender = timeSincePresentation
171
+ metrics.totalLatency = captureToPresentMs + timeSincePresentation
172
+ } else {
173
+ // Time domains don't align - just use capture to present as approximation
174
+ metrics.presentToRender = undefined
175
+ metrics.totalLatency = captureToPresentMs
178
176
  }
179
177
  }
180
178
  }
@@ -36,7 +36,7 @@
36
36
  return jointLimits.map((limit, index) => {
37
37
  const current = currentPositions[index] ?? 0
38
38
  const range = limit.max - limit.min
39
- const percentage = range !== 0 ? ((current - limit.min) / range) * 100 : 50
39
+ const percentage = range === 0 ? 50 : ((current - limit.min) / range) * 100
40
40
 
41
41
  let status: 'safe' | 'caution' | 'danger'
42
42
  if (percentage < 10 || percentage > 90) {
@@ -122,6 +122,18 @@
122
122
  ctx.stroke()
123
123
  }
124
124
 
125
+ const getJointColor = (status: 'safe' | 'caution' | 'danger') => {
126
+ if (status === 'danger') {
127
+ return '#ff4444'
128
+ }
129
+
130
+ if (status === 'caution') {
131
+ return '#ffaa00'
132
+ }
133
+
134
+ return '#44ff44'
135
+ }
136
+
125
137
  // Render joint data to canvas
126
138
  function renderJointLimits(
127
139
  ctx: CanvasRenderingContext2D,
@@ -132,7 +144,8 @@
132
144
  const s = RESOLUTION_SCALE
133
145
  const rowHeight = (height - HEADER_HEIGHT) / joints.length
134
146
 
135
- joints.forEach((joint, index) => {
147
+ let index = 0
148
+ for (const joint of joints) {
136
149
  const y = HEADER_HEIGHT + index * rowHeight
137
150
 
138
151
  // Background row
@@ -157,8 +170,7 @@
157
170
 
158
171
  // Progress bar fill (colored by status)
159
172
  const fillWidth = barWidth * (joint.percentage / 100)
160
- ctx.fillStyle =
161
- joint.status === 'danger' ? '#ff4444' : joint.status === 'caution' ? '#ffaa00' : '#44ff44'
173
+ ctx.fillStyle = getJointColor(joint.status)
162
174
  ctx.fillRect(barX, barY, fillWidth, barHeight)
163
175
 
164
176
  // Progress bar border
@@ -174,7 +186,9 @@
174
186
  barX + barWidth + 20 * s,
175
187
  y + rowHeight / 2
176
188
  )
177
- })
189
+
190
+ index += 1
191
+ }
178
192
  }
179
193
 
180
194
  // Update canvas when joint data changes
@@ -21,8 +21,8 @@
21
21
  let resources: ReturnType<typeof useResourceNames> | undefined
22
22
  try {
23
23
  resources = useResourceNames(() => partID.current)
24
- } catch (e) {
25
- console.warn('Failed to get resources, robot may not be connected yet:', e)
24
+ } catch (error) {
25
+ console.warn('Failed to get resources, robot may not be connected yet:', error)
26
26
  }
27
27
 
28
28
  // Get available arms and grippers
@@ -41,9 +41,9 @@
41
41
  const currentConfig = $derived(settings.current.xrController[selectedHand])
42
42
 
43
43
  // Local form state (editable) — synced from currentConfig via effect
44
- let formArmName = $state<string | undefined>(undefined)
45
- let formGripperName = $state<string | undefined>(undefined)
46
- let formScaleFactor = $state<number>(1.0)
44
+ let formArmName = $state<string>()
45
+ let formGripperName = $state<string>()
46
+ let formScaleFactor = $state<number>(1)
47
47
  let formRotationEnabled = $state<boolean>(true)
48
48
 
49
49
  // Sync form state when selected hand or config changes
@@ -59,9 +59,9 @@
59
59
  const CANVAS_WIDTH = 600
60
60
  const CANVAS_HEIGHT = 500
61
61
 
62
- let canvas: HTMLCanvasElement | undefined = $state()
63
- let texture: CanvasTexture | undefined = $state()
64
- let geometry: PlaneGeometry | undefined = $state()
62
+ let canvas = $state<HTMLCanvasElement>()
63
+ let texture = $state<CanvasTexture>()
64
+ let geometry = $state<PlaneGeometry>()
65
65
 
66
66
  // Initialize canvas
67
67
  $effect(() => {
@@ -90,14 +90,14 @@
90
90
  let uiElements: UIElement[] = []
91
91
 
92
92
  // Mesh ref for raycasting
93
- let meshRef = $state<Mesh | undefined>()
93
+ let meshRef = $state<Mesh>()
94
94
 
95
95
  // Controller interaction
96
96
  const rightController = useController('right')
97
97
  const leftController = useController('left')
98
98
 
99
99
  // Interaction state
100
- let hoveredElement = $state<UIElement | undefined>()
100
+ let hoveredElement = $state<UIElement>()
101
101
  let lastButtonPressed = $state(false)
102
102
 
103
103
  // Handle click on UI element
@@ -340,7 +340,7 @@
340
340
  ctx.fillRect(sliderX, sliderY, sliderWidth, sliderHeight)
341
341
 
342
342
  // Slider thumb
343
- const thumbPos = ((formScaleFactor - 0.1) / (3.0 - 0.1)) * sliderWidth
343
+ const thumbPos = ((formScaleFactor - 0.1) / (3 - 0.1)) * sliderWidth
344
344
  ctx.fillStyle = '#4CAF50'
345
345
  ctx.beginPath()
346
346
  ctx.arc(sliderX + thumbPos, sliderY + sliderHeight / 2, 12, 0, Math.PI * 2)
@@ -428,12 +428,13 @@
428
428
  }
429
429
  })
430
430
 
431
- // Clean up on unmount
431
+ const cleanup = () => {
432
+ texture?.dispose()
433
+ geometry?.dispose()
434
+ }
435
+
432
436
  $effect(() => {
433
- return () => {
434
- texture?.dispose()
435
- geometry?.dispose()
436
- }
437
+ return cleanup
437
438
  })
438
439
  </script>
439
440
 
@@ -105,8 +105,8 @@
105
105
  max="3.0"
106
106
  step="0.1"
107
107
  value={config.left.scaleFactor}
108
- style="--value: {((config.left.scaleFactor - 0.1) / (3.0 - 0.1)) * 100}%"
109
- oninput={(e) => updateConfig('left', 'scaleFactor', parseFloat(e.currentTarget.value))}
108
+ style="--value: {((config.left.scaleFactor - 0.1) / (3 - 0.1)) * 100}%"
109
+ oninput={(e) => updateConfig('left', 'scaleFactor', Number.parseFloat(e.currentTarget.value))}
110
110
  />
111
111
  </label>
112
112
 
@@ -166,8 +166,9 @@
166
166
  max="3.0"
167
167
  step="0.1"
168
168
  value={config.right.scaleFactor}
169
- style="--value: {((config.right.scaleFactor - 0.1) / (3.0 - 0.1)) * 100}%"
170
- oninput={(e) => updateConfig('right', 'scaleFactor', parseFloat(e.currentTarget.value))}
169
+ style="--value: {((config.right.scaleFactor - 0.1) / (3 - 0.1)) * 100}%"
170
+ oninput={(e) =>
171
+ updateConfig('right', 'scaleFactor', Number.parseFloat(e.currentTarget.value))}
171
172
  />
172
173
  </label>
173
174