@viamrobotics/motion-tools 1.13.0 → 1.14.0

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 (102) hide show
  1. package/dist/FrameConfigUpdater.svelte.js +1 -1
  2. package/dist/attribute.js +10 -2
  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/buffer.d.ts +56 -7
  8. package/dist/buffer.js +70 -12
  9. package/dist/color.js +3 -3
  10. package/dist/components/App.svelte +1 -5
  11. package/dist/components/Camera.svelte +1 -7
  12. package/dist/components/Camera.svelte.d.ts +0 -1
  13. package/dist/components/CameraControls.svelte +2 -2
  14. package/dist/components/{Arrows → Entities/Arrows}/ArrowGroups.svelte +3 -3
  15. package/dist/components/{Arrows → Entities/Arrows}/Arrows.svelte +6 -6
  16. package/dist/components/{Arrows → Entities/Arrows}/Arrows.svelte.d.ts +1 -1
  17. package/dist/components/{Entities.svelte → Entities/Entities.svelte} +7 -3
  18. package/dist/components/Entities/Frame.svelte +86 -0
  19. package/dist/components/{Frame.svelte.d.ts → Entities/Frame.svelte.d.ts} +1 -0
  20. package/dist/components/{GLTF.svelte → Entities/GLTF.svelte} +5 -5
  21. package/dist/components/Entities/Geometry.svelte +75 -0
  22. package/dist/components/Entities/Geometry.svelte.d.ts +10 -0
  23. package/dist/components/{Label.svelte → Entities/Label.svelte} +1 -1
  24. package/dist/components/Entities/Line.svelte +90 -0
  25. package/dist/components/Entities/Mesh.svelte +130 -0
  26. package/dist/components/Entities/Mesh.svelte.d.ts +4 -0
  27. package/dist/components/{Points.svelte → Entities/Points.svelte} +5 -5
  28. package/dist/components/{Pose.svelte → Entities/Pose.svelte} +3 -3
  29. package/dist/{hooks/useObjectEvents.svelte.d.ts → components/Entities/hooks/useEntityEvents.svelte.d.ts} +1 -1
  30. package/dist/{hooks/useObjectEvents.svelte.js → components/Entities/hooks/useEntityEvents.svelte.js} +6 -6
  31. package/dist/components/FileDrop/file-names.js +6 -3
  32. package/dist/components/FileDrop/snapshot-dropper.js +8 -4
  33. package/dist/components/FileDrop/useFileDrop.svelte.js +9 -6
  34. package/dist/components/Lasso/Lasso.svelte +4 -4
  35. package/dist/components/PCD.svelte +14 -6
  36. package/dist/components/PCD.svelte.d.ts +2 -0
  37. package/dist/components/Scene.svelte +1 -3
  38. package/dist/components/Selected.svelte +2 -0
  39. package/dist/components/StaticGeometries.svelte +1 -1
  40. package/dist/components/overlay/AddRelationship.svelte +1 -1
  41. package/dist/components/overlay/LiveUpdatesBanner.svelte +4 -6
  42. package/dist/components/overlay/left-pane/buildTree.js +15 -0
  43. package/dist/components/overlay/settings/Settings.svelte +11 -13
  44. package/dist/components/overlay/widgets/Camera.svelte +11 -9
  45. package/dist/components/xr/ArmTeleop.svelte +33 -33
  46. package/dist/components/xr/CameraFeed.svelte +21 -23
  47. package/dist/components/xr/JointLimitsWidget.svelte +19 -5
  48. package/dist/components/xr/XRConfigPanel.svelte +17 -16
  49. package/dist/components/xr/XRControllerSettings.svelte +5 -4
  50. package/dist/components/xr/XRToast.svelte +11 -6
  51. package/dist/ecs/relations.d.ts +1 -0
  52. package/dist/ecs/relations.js +1 -0
  53. package/dist/ecs/traits.d.ts +2 -19
  54. package/dist/ecs/traits.js +33 -6
  55. package/dist/ecs/useQuery.svelte.js +3 -3
  56. package/dist/format.js +1 -1
  57. package/dist/hooks/use3DModels.svelte.js +36 -38
  58. package/dist/hooks/useConfigFrames.svelte.js +2 -7
  59. package/dist/hooks/useDrawAPI.svelte.js +1 -1
  60. package/dist/hooks/useFramelessComponents.svelte.js +1 -1
  61. package/dist/hooks/useFrames.svelte.js +62 -56
  62. package/dist/hooks/useGeometries.svelte.js +59 -36
  63. package/dist/hooks/useLinked.svelte.js +5 -4
  64. package/dist/hooks/usePartConfig.svelte.js +16 -17
  65. package/dist/hooks/usePointcloudObjects.svelte.js +107 -62
  66. package/dist/hooks/usePointclouds.svelte.js +50 -32
  67. package/dist/hooks/usePose.svelte.js +3 -7
  68. package/dist/hooks/useResizable.svelte.js +4 -3
  69. package/dist/hooks/useSettings.svelte.js +2 -2
  70. package/dist/hooks/useWeblabs.svelte.js +3 -2
  71. package/dist/hooks/useWorldState.svelte.js +31 -28
  72. package/dist/loaders/pcd/index.js +2 -1
  73. package/dist/loaders/pcd/worker.inline.d.ts +1 -1
  74. package/dist/loaders/pcd/worker.inline.js +1 -1
  75. package/dist/loaders/pcd/worker.js +1 -1
  76. package/dist/metadata.d.ts +22 -0
  77. package/dist/metadata.js +66 -0
  78. package/dist/snapshot.d.ts +20 -0
  79. package/dist/snapshot.js +72 -28
  80. package/dist/three/InstancedArrows/InstancedArrows.js +1 -1
  81. package/dist/three/InstancedArrows/box.js +1 -1
  82. package/dist/three/InstancedArrows/raycast.js +12 -12
  83. package/dist/three/OBBHelper.d.ts +3 -2
  84. package/dist/three/OBBHelper.js +17 -5
  85. package/package.json +19 -11
  86. package/dist/WorldObject.svelte.d.ts +0 -27
  87. package/dist/WorldObject.svelte.js +0 -114
  88. package/dist/components/Frame.svelte +0 -89
  89. package/dist/components/Geometry.svelte +0 -211
  90. package/dist/components/Geometry.svelte.d.ts +0 -19
  91. package/dist/components/Line.svelte +0 -43
  92. /package/dist/components/{Arrows → Entities/Arrows}/ArrowGroups.svelte.d.ts +0 -0
  93. /package/dist/components/{Entities.svelte.d.ts → Entities/Entities.svelte.d.ts} +0 -0
  94. /package/dist/components/{GLTF.svelte.d.ts → Entities/GLTF.svelte.d.ts} +0 -0
  95. /package/dist/components/{Label.svelte.d.ts → Entities/Label.svelte.d.ts} +0 -0
  96. /package/dist/components/{Line.svelte.d.ts → Entities/Line.svelte.d.ts} +0 -0
  97. /package/dist/components/{LineDots.svelte → Entities/LineDots.svelte} +0 -0
  98. /package/dist/components/{LineDots.svelte.d.ts → Entities/LineDots.svelte.d.ts} +0 -0
  99. /package/dist/components/{LineGeometry.svelte → Entities/LineGeometry.svelte} +0 -0
  100. /package/dist/components/{LineGeometry.svelte.d.ts → Entities/LineGeometry.svelte.d.ts} +0 -0
  101. /package/dist/components/{Points.svelte.d.ts → Entities/Points.svelte.d.ts} +0 -0
  102. /package/dist/components/{Pose.svelte.d.ts → Entities/Pose.svelte.d.ts} +0 -0
@@ -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
 
@@ -132,7 +132,8 @@
132
132
  const iconCenterX = accentBarWidth + 30
133
133
  const textStartX = accentBarWidth + 56
134
134
 
135
- toasts.forEach((toast, index) => {
135
+ let index = 0
136
+ for (const toast of toasts) {
136
137
  const y = index * (TOAST_HEIGHT + TOAST_GAP)
137
138
  const style = VARIANT_STYLES[toast.variant]
138
139
 
@@ -165,7 +166,9 @@
165
166
  ctx.font = '28px sans-serif'
166
167
  ctx.textBaseline = 'middle'
167
168
  ctx.fillText(toast.message, textStartX, y + TOAST_HEIGHT / 2)
168
- })
169
+
170
+ index += 1
171
+ }
169
172
 
170
173
  texture.needsUpdate = true
171
174
  }
@@ -189,12 +192,14 @@
189
192
  renderToasts(toasts)
190
193
  })
191
194
 
195
+ const dispose = () => {
196
+ texture.dispose()
197
+ untrack(() => geometry?.dispose())
198
+ }
199
+
192
200
  // Cleanup
193
201
  $effect(() => {
194
- return () => {
195
- texture.dispose()
196
- untrack(() => geometry?.dispose())
197
- }
202
+ return dispose
198
203
  })
199
204
  </script>
200
205
 
@@ -1,3 +1,4 @@
1
+ export declare const ChildOf: import("koota").Relation<import("koota").Trait<Record<string, never>>>;
1
2
  export declare const SubEntityLinkType: {
2
3
  readonly HoverLink: "HoverLink";
3
4
  };
@@ -1,4 +1,5 @@
1
1
  import { relation } from 'koota';
2
+ export const ChildOf = relation({ exclusive: true });
2
3
  export const SubEntityLinkType = {
3
4
  HoverLink: 'HoverLink',
4
5
  };
@@ -1,4 +1,5 @@
1
1
  import type { GLTF as ThreeGltf } from 'three/examples/jsm/loaders/GLTFLoader.js';
2
+ import { type Entity } from 'koota';
2
3
  import { BufferGeometry as ThreeBufferGeometry } from 'three';
3
4
  import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
4
5
  export declare const Name: import("koota").Trait<() => string>;
@@ -178,22 +179,4 @@ export declare const Geometry: (geometry: ViamGeometry) => import("koota").Trait
178
179
  }>, Partial<{
179
180
  r: number;
180
181
  }>] | [import("koota").Trait<() => ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>>, ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>];
181
- export declare const updateGeometry: (geometry: ViamGeometry) => (ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap> | import("koota").Trait<() => ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>>)[] | ({
182
- x: number;
183
- y: number;
184
- z: number;
185
- } | import("koota").Trait<{
186
- x: number;
187
- y: number;
188
- z: number;
189
- }>)[] | ({
190
- r: number;
191
- l: number;
192
- } | import("koota").Trait<{
193
- l: number;
194
- r: number;
195
- }>)[] | ({
196
- r: number;
197
- } | import("koota").Trait<{
198
- r: number;
199
- }>)[];
182
+ export declare const updateGeometryTrait: (entity: Entity, geometry?: ViamGeometry) => void;
@@ -126,18 +126,45 @@ export const Geometry = (geometry) => {
126
126
  }
127
127
  return ReferenceFrame;
128
128
  };
129
- export const updateGeometry = (geometry) => {
129
+ export const updateGeometryTrait = (entity, geometry) => {
130
+ if (!geometry) {
131
+ entity.remove(Box, Capsule, Sphere, BufferGeometry);
132
+ return;
133
+ }
130
134
  if (geometry.geometryType.case === 'box') {
131
- return [Box, createBox(geometry.geometryType.value)];
135
+ if (entity.has(Box)) {
136
+ entity.set(Box, createBox(geometry.geometryType.value));
137
+ }
138
+ else {
139
+ entity.remove(Capsule, Sphere, BufferGeometry);
140
+ entity.add(Box(createBox(geometry.geometryType.value)));
141
+ }
132
142
  }
133
143
  else if (geometry.geometryType.case === 'capsule') {
134
- return [Capsule, createCapsule(geometry.geometryType.value)];
144
+ if (entity.has(Capsule)) {
145
+ entity.set(Capsule, createCapsule(geometry.geometryType.value));
146
+ }
147
+ else {
148
+ entity.remove(Box, Sphere, BufferGeometry);
149
+ entity.add(Capsule(createCapsule(geometry.geometryType.value)));
150
+ }
135
151
  }
136
152
  else if (geometry.geometryType.case === 'sphere') {
137
- return [Sphere, createSphere(geometry.geometryType.value)];
153
+ if (entity.has(Sphere)) {
154
+ entity.set(Sphere, createSphere(geometry.geometryType.value));
155
+ }
156
+ else {
157
+ entity.remove(Box, Capsule, BufferGeometry);
158
+ entity.add(Sphere(createSphere(geometry.geometryType.value)));
159
+ }
138
160
  }
139
161
  else if (geometry.geometryType.case === 'mesh') {
140
- return [BufferGeometry, parsePlyInput(geometry.geometryType.value.mesh)];
162
+ if (entity.has(BufferGeometry)) {
163
+ entity.set(BufferGeometry, parsePlyInput(geometry.geometryType.value.mesh));
164
+ }
165
+ else {
166
+ entity.remove(Box, Sphere, Capsule);
167
+ entity.add(BufferGeometry(parsePlyInput(geometry.geometryType.value.mesh)));
168
+ }
141
169
  }
142
- return [];
143
170
  };
@@ -31,11 +31,11 @@ export function useQuery(...parameters) {
31
31
  };
32
32
  });
33
33
  });
34
+ const handler = () => {
35
+ version += 1;
36
+ };
34
37
  // Force reattaching event listeners when the world is reset.
35
38
  $effect(() => {
36
- const handler = () => {
37
- version += 1;
38
- };
39
39
  world[internal].resetSubscriptions.add(handler);
40
40
  return () => {
41
41
  world[internal].resetSubscriptions.delete(handler);
package/dist/format.js CHANGED
@@ -2,7 +2,7 @@ export const formatNumeric = (value, decimals = 2) => {
2
2
  if (value === undefined) {
3
3
  return '––';
4
4
  }
5
- if (isNaN(value)) {
5
+ if (Number.isNaN(value)) {
6
6
  return 'NaN';
7
7
  }
8
8
  if (value === Number.POSITIVE_INFINITY) {
@@ -18,48 +18,46 @@ export const provide3DModels = (partID) => {
18
18
  const clients = $derived(armClients.filter((client) => {
19
19
  return arms.current.some((arm) => arm.name === client.current?.name);
20
20
  }));
21
- $effect(() => {
22
- const fetch3DModels = async () => {
23
- const next = {};
24
- for (const client of clients) {
25
- if (!client.current)
21
+ const fetch3DModels = async () => {
22
+ const next = {};
23
+ for (const client of clients) {
24
+ if (!client.current)
25
+ continue;
26
+ try {
27
+ const geometries = await client.current.getGeometries();
28
+ if (geometries.length === 0) {
26
29
  continue;
27
- try {
28
- const geometries = await client.current.getGeometries();
29
- if (geometries.length === 0) {
30
- continue;
31
- }
32
- const geometryLabel = geometries[0].label;
33
- const prefix = geometryLabel.split(':')[0];
34
- const models = await client.current.get3DModels();
35
- if (!(prefix in next)) {
36
- next[prefix] = {};
37
- }
38
- for (const [id, model] of Object.entries(models)) {
39
- const arrayBuffer = model.mesh.buffer.slice(model.mesh.byteOffset, model.mesh.byteOffset + model.mesh.byteLength);
40
- const gltfModel = await gltfLoader.parseAsync(arrayBuffer, '');
41
- next[prefix][id] = gltfModel.scene;
42
- gltfModel.scene.traverse((object) => {
43
- if (isInstanceOf(object, 'Mesh')) {
44
- const { material } = object;
45
- if (isInstanceOf(material, 'MeshStandardMaterial')) {
46
- material.roughness = 0.3;
47
- material.metalness = 0.1;
48
- }
49
- }
50
- });
51
- }
52
- current = next;
53
30
  }
54
- catch (error) {
55
- // some arms may not implement this api yet
56
- console.warn(`${client.current.name} returned an error: ${error} when getting 3D models`);
31
+ const geometryLabel = geometries[0].label;
32
+ const prefix = geometryLabel.split(':')[0];
33
+ const models = await client.current.get3DModels();
34
+ if (!(prefix in next)) {
35
+ next[prefix] = {};
36
+ }
37
+ for (const [id, model] of Object.entries(models)) {
38
+ const arrayBuffer = model.mesh.buffer.slice(model.mesh.byteOffset, model.mesh.byteOffset + model.mesh.byteLength);
39
+ const gltfModel = await gltfLoader.parseAsync(arrayBuffer, '');
40
+ next[prefix][id] = gltfModel.scene;
41
+ gltfModel.scene.traverse((object) => {
42
+ if (isInstanceOf(object, 'Mesh')) {
43
+ const { material } = object;
44
+ if (isInstanceOf(material, 'MeshStandardMaterial')) {
45
+ material.roughness = 0.3;
46
+ material.metalness = 0.1;
47
+ }
48
+ }
49
+ });
57
50
  }
51
+ current = next;
58
52
  }
59
- };
60
- const shouldFetchModels = settings.current.isLoaded &&
61
- (settings.current.renderArmModels === 'model' ||
62
- settings.current.renderArmModels === 'colliders+model');
53
+ catch (error) {
54
+ // some arms may not implement this api yet
55
+ console.warn(`${client.current.name} returned an error: ${error} when getting 3D models`);
56
+ }
57
+ }
58
+ };
59
+ $effect(() => {
60
+ const shouldFetchModels = settings.current.isLoaded && settings.current.renderArmModels.includes('model');
63
61
  if (shouldFetchModels) {
64
62
  fetch3DModels();
65
63
  }
@@ -8,12 +8,7 @@ export const provideConfigFrames = () => {
8
8
  const environment = useEnvironment();
9
9
  const partConfig = usePartConfig();
10
10
  $effect(() => {
11
- if (partConfig.isDirty) {
12
- environment.current.viewerMode = 'edit';
13
- }
14
- else {
15
- environment.current.viewerMode = 'monitor';
16
- }
11
+ environment.current.viewerMode = partConfig.isDirty ? 'edit' : 'monitor';
17
12
  });
18
13
  const [configFrames, configUnsetFrameNames] = $derived.by(() => {
19
14
  const { components } = partConfig.current;
@@ -74,7 +69,7 @@ export const provideConfigFrames = () => {
74
69
  }
75
70
  }
76
71
  }
77
- return Array.from(validFrames);
72
+ return [...validFrames];
78
73
  };
79
74
  const unsetFrames = $derived([...new Set([...configUnsetFrameNames, ...fragmentUnsetFrameNames])]);
80
75
  setContext(key, {