@viamrobotics/motion-tools 1.9.1 → 1.11.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 (136) hide show
  1. package/dist/HoverUpdater.svelte.d.ts +16 -0
  2. package/dist/HoverUpdater.svelte.js +78 -0
  3. package/dist/WorldObject.svelte.d.ts +27 -0
  4. package/dist/WorldObject.svelte.js +8 -55
  5. package/dist/{draw → buf/draw}/v1/drawing_pb.d.ts +6 -0
  6. package/dist/{draw → buf/draw}/v1/drawing_pb.js +7 -0
  7. package/dist/buf/draw/v1/service_connect.d.ts +122 -0
  8. package/dist/buf/draw/v1/service_connect.js +126 -0
  9. package/dist/buf/draw/v1/service_pb.d.ts +382 -0
  10. package/dist/buf/draw/v1/service_pb.js +612 -0
  11. package/dist/components/App.svelte +28 -30
  12. package/dist/components/Arrows/Arrows.svelte +16 -3
  13. package/dist/components/FileDrop/file-dropper.d.ts +1 -1
  14. package/dist/components/FileDrop/snapshot-dropper.js +1 -1
  15. package/dist/components/FileDrop/useFileDrop.svelte.d.ts +2 -1
  16. package/dist/components/Frame.svelte +1 -1
  17. package/dist/components/Geometry.svelte +113 -71
  18. package/dist/components/Geometry.svelte.d.ts +6 -7
  19. package/dist/components/MeasureTool/MeasurePoint.svelte +3 -3
  20. package/dist/components/MeasureTool/MeasureTool.svelte +6 -6
  21. package/dist/components/SceneProviders.svelte +4 -0
  22. package/dist/components/Snapshot.svelte +1 -1
  23. package/dist/components/Snapshot.svelte.d.ts +1 -1
  24. package/dist/components/hover/HoveredEntities.svelte +23 -0
  25. package/dist/components/hover/HoveredEntity.svelte +15 -0
  26. package/dist/components/hover/HoveredEntity.svelte.d.ts +3 -0
  27. package/dist/components/hover/HoveredEntityTooltip.svelte +70 -0
  28. package/dist/components/{HoveredEntityTooltip.svelte.d.ts → hover/HoveredEntityTooltip.svelte.d.ts} +2 -2
  29. package/dist/components/hover/LinkedHoveredEntity.svelte +55 -0
  30. package/dist/components/hover/LinkedHoveredEntity.svelte.d.ts +9 -0
  31. package/dist/components/overlay/AddRelationship.svelte +131 -0
  32. package/dist/components/overlay/AddRelationship.svelte.d.ts +7 -0
  33. package/dist/components/overlay/Details.svelte +55 -2
  34. package/dist/components/overlay/FloatingPanel.svelte +78 -0
  35. package/dist/components/overlay/FloatingPanel.svelte.d.ts +13 -0
  36. package/dist/components/overlay/{left-pane/RefreshRate.svelte → RefreshRate.svelte} +1 -1
  37. package/dist/components/overlay/ToggleGroup.svelte +22 -26
  38. package/dist/components/overlay/ToggleGroup.svelte.d.ts +6 -7
  39. package/dist/components/overlay/left-pane/TreeContainer.svelte +0 -4
  40. package/dist/components/overlay/settings/Settings.svelte +330 -0
  41. package/dist/components/overlay/settings/Tabs.svelte +54 -0
  42. package/dist/components/overlay/settings/Tabs.svelte.d.ts +12 -0
  43. package/dist/components/overlay/widgets/Camera.svelte +20 -12
  44. package/dist/components/xr/ArmTeleop.svelte +469 -0
  45. package/dist/components/xr/ArmTeleop.svelte.d.ts +10 -0
  46. package/dist/components/xr/CameraFeed.svelte +191 -47
  47. package/dist/components/xr/CameraFeed.svelte.d.ts +7 -0
  48. package/dist/components/xr/Controllers.svelte +45 -38
  49. package/dist/components/xr/Controllers.svelte.d.ts +2 -17
  50. package/dist/components/xr/Hands.svelte +2 -4
  51. package/dist/components/xr/JointLimitsWidget.svelte +209 -0
  52. package/dist/components/xr/JointLimitsWidget.svelte.d.ts +13 -0
  53. package/dist/components/xr/OriginMarker.svelte +1 -15
  54. package/dist/components/xr/XR.svelte +78 -5
  55. package/dist/components/xr/XRConfigPanel.svelte +449 -0
  56. package/dist/components/xr/XRConfigPanel.svelte.d.ts +11 -0
  57. package/dist/components/xr/XRControllerSettings.svelte +240 -0
  58. package/dist/components/xr/XRControllerSettings.svelte.d.ts +3 -0
  59. package/dist/components/xr/XRToast.svelte +215 -0
  60. package/dist/components/xr/XRToast.svelte.d.ts +3 -0
  61. package/dist/components/xr/math.d.ts +14 -0
  62. package/dist/components/xr/math.js +26 -0
  63. package/dist/components/xr/toasts.svelte.d.ts +20 -0
  64. package/dist/components/xr/toasts.svelte.js +32 -0
  65. package/dist/components/xr/useOrigin.svelte.d.ts +2 -2
  66. package/dist/components/xr/useOrigin.svelte.js +4 -4
  67. package/dist/ecs/index.d.ts +1 -0
  68. package/dist/ecs/index.js +1 -0
  69. package/dist/ecs/relations.d.ts +7 -0
  70. package/dist/ecs/relations.js +7 -0
  71. package/dist/ecs/traits.d.ts +15 -1
  72. package/dist/ecs/traits.js +19 -5
  73. package/dist/ecs/useTrait.svelte.d.ts +3 -3
  74. package/dist/frame.d.ts +0 -3
  75. package/dist/hooks/useArmKinematics.svelte.d.ts +12 -0
  76. package/dist/hooks/useArmKinematics.svelte.js +31 -0
  77. package/dist/hooks/useGeometries.svelte.js +47 -36
  78. package/dist/hooks/useLinked.svelte.d.ts +7 -0
  79. package/dist/hooks/useLinked.svelte.js +35 -0
  80. package/dist/hooks/useObjectEvents.svelte.js +52 -16
  81. package/dist/hooks/usePartConfig.svelte.d.ts +0 -35
  82. package/dist/hooks/usePartConfig.svelte.js +2 -2
  83. package/dist/hooks/usePointcloudObjects.svelte.js +45 -64
  84. package/dist/hooks/usePointclouds.svelte.js +13 -9
  85. package/dist/hooks/usePose.svelte.js +5 -2
  86. package/dist/hooks/useResourceByName.svelte.d.ts +7 -0
  87. package/dist/hooks/useResourceByName.svelte.js +2 -2
  88. package/dist/hooks/useSettings.svelte.d.ts +14 -0
  89. package/dist/hooks/useSettings.svelte.js +10 -0
  90. package/dist/hooks/useWorldState.svelte.d.ts +0 -8
  91. package/dist/lib.d.ts +1 -3
  92. package/dist/lib.js +1 -3
  93. package/dist/snapshot.d.ts +2 -2
  94. package/dist/snapshot.js +2 -2
  95. package/dist/three/InstancedArrows/raycast.d.ts +2 -4
  96. package/dist/three/InstancedArrows/raycast.js +5 -5
  97. package/dist/transform.js +1 -0
  98. package/package.json +7 -5
  99. package/dist/assert.d.ts +0 -14
  100. package/dist/assert.js +0 -21
  101. package/dist/components/BatchedGeometry.svelte +0 -0
  102. package/dist/components/BatchedGeometry.svelte.d.ts +0 -26
  103. package/dist/components/Detections.svelte +0 -41
  104. package/dist/components/Detections.svelte.d.ts +0 -3
  105. package/dist/components/DetectionsPlane.svelte +0 -23
  106. package/dist/components/DetectionsPlane.svelte.d.ts +0 -21
  107. package/dist/components/Geometry2.svelte +0 -211
  108. package/dist/components/Geometry2.svelte.d.ts +0 -19
  109. package/dist/components/HoveredEntities.svelte +0 -19
  110. package/dist/components/HoveredEntityTooltip.svelte +0 -242
  111. package/dist/components/overlay/left-pane/Settings.svelte +0 -221
  112. package/dist/components/overlay/left-pane/Widgets.svelte +0 -65
  113. package/dist/components/overlay/left-pane/Widgets.svelte.d.ts +0 -3
  114. package/dist/entries.d.ts +0 -1
  115. package/dist/entries.js +0 -3
  116. package/dist/hooks/index.d.ts +0 -0
  117. package/dist/hooks/index.js +0 -1
  118. package/dist/test.d.ts +0 -1
  119. package/dist/test.js +0 -1
  120. package/dist/three/BoxHelper.d.ts +0 -50
  121. package/dist/three/BoxHelper.js +0 -134
  122. /package/dist/{common → buf/common}/v1/common_pb.d.ts +0 -0
  123. /package/dist/{common → buf/common}/v1/common_pb.js +0 -0
  124. /package/dist/{draw → buf/draw}/v1/metadata_pb.d.ts +0 -0
  125. /package/dist/{draw → buf/draw}/v1/metadata_pb.js +0 -0
  126. /package/dist/{draw → buf/draw}/v1/scene_pb.d.ts +0 -0
  127. /package/dist/{draw → buf/draw}/v1/scene_pb.js +0 -0
  128. /package/dist/{draw → buf/draw}/v1/snapshot_pb.d.ts +0 -0
  129. /package/dist/{draw → buf/draw}/v1/snapshot_pb.js +0 -0
  130. /package/dist/{draw → buf/draw}/v1/transforms_pb.d.ts +0 -0
  131. /package/dist/{draw → buf/draw}/v1/transforms_pb.js +0 -0
  132. /package/dist/components/{HoveredEntities.svelte.d.ts → hover/HoveredEntities.svelte.d.ts} +0 -0
  133. /package/dist/components/overlay/{left-pane/RefreshRate.svelte.d.ts → RefreshRate.svelte.d.ts} +0 -0
  134. /package/dist/components/overlay/{left-pane → settings}/Settings.svelte.d.ts +0 -0
  135. /package/dist/components/{BentPlaneGeometry.svelte → xr/BentPlaneGeometry.svelte} +0 -0
  136. /package/dist/components/{BentPlaneGeometry.svelte.d.ts → xr/BentPlaneGeometry.svelte.d.ts} +0 -0
@@ -1,16 +1,23 @@
1
1
  <script lang="ts">
2
2
  import { T, useTask } from '@threlte/core'
3
3
  import { createStreamClient } from '@viamrobotics/svelte-sdk'
4
- import BentPlaneGeometry from '../BentPlaneGeometry.svelte'
5
- import { useHeadset } from '@threlte/xr'
6
- import { Euler, Group, Mesh, Vector3, Quaternion, VideoTexture } from 'three'
4
+ import { VideoTexture } from 'three'
7
5
  import { usePartID } from '../../hooks/usePartID.svelte'
6
+ import BentPlaneGeometry from './BentPlaneGeometry.svelte'
8
7
 
9
8
  interface CameraFeedProps {
10
9
  resourceName: string
10
+ offset?: { x?: number; y?: number; z?: number }
11
+ scale?: number
12
+ enableProfiling?: boolean
11
13
  }
12
14
 
13
- let { resourceName }: CameraFeedProps = $props()
15
+ let {
16
+ resourceName,
17
+ offset = {},
18
+ scale = 0.7,
19
+ enableProfiling = false,
20
+ }: CameraFeedProps = $props()
14
21
 
15
22
  const partID = usePartID()
16
23
  const streamClient = createStreamClient(
@@ -21,63 +28,200 @@
21
28
  let video = document.createElement('video')
22
29
  let aspect = $state(1)
23
30
  let ready = $state(false)
31
+ let texture = $state<VideoTexture | null>(null)
24
32
 
25
- video.addEventListener('canplaythrough', () => {
26
- aspect = video.videoWidth / video.videoHeight
27
- video.play()
28
- })
33
+ // ===== LATENCY PROFILING =====
34
+ interface LatencyMetrics {
35
+ streamConnectTime?: number
36
+ videoReadyTime?: number
37
+ firstFrameTime?: number
38
+ captureToPresent?: number // Camera capture → browser decode (from metadata)
39
+ presentToRender?: number // Browser decode → Three.js texture update
40
+ totalLatency?: number // End-to-end
41
+ fps?: number
42
+ }
43
+ let metrics = $state<LatencyMetrics>({})
44
+ let frameCount = $state(0)
45
+ let lastFrameTime = 0
46
+ let fpsFrames: number[] = []
47
+ let videoFrameCallbackId: number | null = null
48
+
49
+ // Critical: video must autoplay and be muted for streams to work
50
+ video.autoplay = true
51
+ video.muted = true
52
+ video.playsInline = true
53
+
54
+ // Low-latency settings for teleoperation
55
+ // @ts-expect-error - latencyHint is not in standard types but supported by browsers
56
+ video.latencyHint = 0 // Minimize latency
57
+ video.disableRemotePlayback = true
29
58
 
30
59
  $effect.pre(() => {
31
- video.srcObject = streamClient.mediaStream
32
- ready = true
33
- })
60
+ const mediaStream = streamClient.mediaStream
61
+ if (!mediaStream) {
62
+ ready = false
63
+ texture?.dispose()
64
+ texture = null
65
+ return
66
+ }
67
+
68
+ // PROFILING: Stream connected
69
+ const streamConnectTime = performance.now()
70
+ if (enableProfiling) {
71
+ metrics.streamConnectTime = streamConnectTime
72
+ }
73
+
74
+ video.srcObject = mediaStream
34
75
 
35
- const headset = useHeadset()
76
+ // Wait for video to be ready before creating texture
77
+ const onReady = () => {
78
+ const videoReadyTime = performance.now()
79
+ aspect = video.videoWidth / video.videoHeight
36
80
 
37
- let group = new Group()
38
- let mesh = new Mesh()
39
- let euler = new Euler()
40
- let quaternion = new Quaternion()
41
- let direction = new Vector3()
81
+ if (!texture) {
82
+ texture = new VideoTexture(video)
83
+ }
42
84
 
43
- const { start, stop } = useTask(
44
- (delta) => {
45
- group.position.lerp(headset.position, delta * 5)
85
+ // Force play to ensure stream is active
86
+ video.play().catch((e) => console.warn('Video play failed:', e))
87
+ ready = true
46
88
 
47
- headset.getWorldDirection(direction)
48
- euler.set(0, Math.atan2(direction.x, direction.z), 0)
49
- quaternion.setFromEuler(euler)
50
- group.quaternion.slerp(quaternion, delta * 5)
89
+ // PROFILING: Video ready
90
+ if (enableProfiling) {
91
+ metrics.videoReadyTime = videoReadyTime
92
+ const setupLatency = videoReadyTime - streamConnectTime
93
+ console.log(
94
+ `[🎥 ${resourceName}] Ready: ${video.videoWidth}x${video.videoHeight} (setup: ${setupLatency.toFixed(0)}ms)`
95
+ )
96
+ }
51
97
 
52
- mesh.lookAt(headset.position)
53
- },
54
- {
55
- autoStart: false,
98
+ // Start frame-by-frame profiling using requestVideoFrameCallback
99
+ startFrameProfiling()
100
+ }
101
+
102
+ const onMetadata = () => {
103
+ if (video.readyState >= video.HAVE_METADATA) {
104
+ onReady()
105
+ }
56
106
  }
57
- )
58
107
 
59
- $effect(() => {
60
- if (ready) {
61
- start()
108
+ if (video.readyState >= video.HAVE_METADATA) {
109
+ onReady()
62
110
  } else {
63
- stop()
111
+ video.addEventListener('loadedmetadata', onMetadata, { once: true })
112
+ }
113
+
114
+ // Cleanup when component unmounts
115
+ return () => {
116
+ ready = false
117
+ video.pause()
118
+ video.srcObject = null
119
+ texture?.dispose()
120
+ texture = null
121
+ stopFrameProfiling()
122
+ // Don't stop tracks - let the streamClient manage them
64
123
  }
65
124
  })
66
125
 
67
- const texture = new VideoTexture(video)
126
+ // Frame-by-frame profiling using requestVideoFrameCallback
127
+ function startFrameProfiling() {
128
+ if (!enableProfiling) {
129
+ // If profiling disabled, use simple useTask approach
130
+ return
131
+ }
132
+
133
+ stopFrameProfiling() // Clear any existing callback
134
+
135
+ const updateFrame = (now: number, metadata: VideoFrameCallbackMetadata) => {
136
+ if (!texture || !ready) {
137
+ stopFrameProfiling()
138
+ return
139
+ }
140
+
141
+ frameCount++
142
+
143
+ // Update texture
144
+ texture.needsUpdate = true
145
+
146
+ // Calculate latency metrics from metadata
147
+ if (metadata) {
148
+ // All times in metadata are in microseconds, need to convert to ms
149
+ // 'now' parameter is DOMHighResTimeStamp in milliseconds
150
+ const captureTime = metadata.captureTime || metadata.mediaTime
151
+ const presentationTime = metadata.presentationTime || metadata.expectedDisplayTime
152
+
153
+ if (captureTime) {
154
+ // The times might be in different epochs, so we can only reliably calculate
155
+ // the difference between capture and presentation
156
+ if (presentationTime) {
157
+ // Encoding + Network + Decoding time
158
+ const captureToPresentMs = (presentationTime - captureTime) / 1000
159
+ metrics.captureToPresent = captureToPresentMs
160
+
161
+ // Time since video element presented the frame to when we render it
162
+ // This should be very small (< 16ms ideally)
163
+ const presentMsRelative = presentationTime / 1000
164
+ const timeSincePresentation = now - presentMsRelative
165
+
166
+ // Only use this if the time domains seem aligned (value is reasonable)
167
+ if (Math.abs(timeSincePresentation) < 1000) {
168
+ metrics.presentToRender = timeSincePresentation
169
+ metrics.totalLatency = captureToPresentMs + timeSincePresentation
170
+ } else {
171
+ // Time domains don't align - just use capture to present as approximation
172
+ metrics.presentToRender = undefined
173
+ metrics.totalLatency = captureToPresentMs
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ // Calculate FPS
180
+ if (lastFrameTime > 0) {
181
+ const frameDelta = now - lastFrameTime
182
+ fpsFrames.push(1000 / frameDelta)
183
+ if (fpsFrames.length > 30) fpsFrames.shift() // Keep last 30 frames
184
+ metrics.fps = fpsFrames.reduce((a, b) => a + b, 0) / fpsFrames.length
185
+ }
186
+ lastFrameTime = now
187
+
188
+ // Log key metrics every 60 frames
189
+ if (enableProfiling && frameCount % 60 === 0) {
190
+ const latency = metrics.totalLatency ? `${metrics.totalLatency.toFixed(1)}ms` : 'N/A'
191
+ const fps = metrics.fps ? `${metrics.fps.toFixed(1)}fps` : 'N/A'
192
+ const resolution = metadata ? `${metadata.width}x${metadata.height}` : 'N/A'
193
+ console.log(`[🎥 ${resourceName}] ${resolution} @ ${fps} | Latency: ${latency}`)
194
+ }
195
+
196
+ // Schedule next frame
197
+ videoFrameCallbackId = video.requestVideoFrameCallback(updateFrame)
198
+ }
199
+
200
+ // Start the callback loop
201
+ videoFrameCallbackId = video.requestVideoFrameCallback(updateFrame)
202
+ }
203
+
204
+ function stopFrameProfiling() {
205
+ if (videoFrameCallbackId !== null) {
206
+ video.cancelVideoFrameCallback(videoFrameCallbackId)
207
+ videoFrameCallbackId = null
208
+ }
209
+ }
210
+
211
+ // Fallback: If profiling is disabled, use simple useTask
212
+ useTask(() => {
213
+ if (!enableProfiling && texture && ready) {
214
+ texture.needsUpdate = true
215
+ }
216
+ })
68
217
  </script>
69
218
 
70
- {#if ready}
71
- <T is={group}>
72
- <T.Group>
73
- <T
74
- is={mesh}
75
- position={[0, 0, -1.5]}
76
- scale={0.7}
77
- >
78
- <BentPlaneGeometry args={[0.1, aspect, 1, 20, 20]} />
79
- <T.MeshBasicMaterial map={texture} />
80
- </T>
81
- </T.Group>
82
- </T>
219
+ {#if ready && texture}
220
+ <T.Mesh
221
+ position={[offset.x ?? 0, offset.y ?? 0, offset.z ?? -1.5]}
222
+ {scale}
223
+ >
224
+ <BentPlaneGeometry args={[0.1, aspect, 1, 20, 20]} />
225
+ <T.MeshBasicMaterial map={texture} />
226
+ </T.Mesh>
83
227
  {/if}
@@ -1,5 +1,12 @@
1
1
  interface CameraFeedProps {
2
2
  resourceName: string;
3
+ offset?: {
4
+ x?: number;
5
+ y?: number;
6
+ z?: number;
7
+ };
8
+ scale?: number;
9
+ enableProfiling?: boolean;
3
10
  }
4
11
  declare const CameraFeed: import("svelte").Component<CameraFeedProps, {}, "">;
5
12
  type CameraFeed = ReturnType<typeof CameraFeed>;
@@ -1,45 +1,26 @@
1
1
  <script lang="ts">
2
2
  import { Controller } from '@threlte/xr'
3
- // import { useGamepad } from '@threlte/extras'
4
-
5
- // import { BaseClient } from '@viamrobotics/sdk'
6
-
7
3
  import { RigidBody } from '@threlte/rapier'
8
4
  import HandCollider from './HandCollider.svelte'
9
- // import { usePartID } from '../../hooks/usePartID.svelte'
10
- // import { useResourceNames, useRobotClient } from '@viamrobotics/svelte-sdk'
11
-
12
- // const gamepadLeft = useGamepad({ xr: true, hand: 'left' })
13
-
14
- // const partID = usePartID()
15
- // const resources = useResourceNames(() => partID.current)
16
- // const robotClient = useRobotClient(() => partID.current)
17
- // const resource = $derived(resources.current.find((r) => r.subtype === 'base'))
18
- // const baseClient = $derived(
19
- // robotClient.current && resource ? new BaseClient(robotClient.current, resource.name) : undefined
20
- // )
21
-
22
- // const linear = { x: 0, y: 0, z: 0 }
23
- // const angular = { x: 0, y: 0, z: 0 }
24
-
25
- // gamepadLeft.squeeze.on('change', (event) => {
26
- // linear.y = -event.value
27
- // baseClient?.setPower(linear, angular)
28
- // })
29
-
30
- // gamepadLeft.trigger.on('change', (event) => {
31
- // if (typeof event.value === 'number') {
32
- // linear.y = event.value
33
- // baseClient?.setPower(linear, angular)
34
- // }
35
- // })
36
-
37
- // gamepadLeft.thumbstick.on('change', (event) => {
38
- // if (typeof event.value === 'object') {
39
- // angular.z = event.value.x
40
- // baseClient?.setPower(linear, angular)
41
- // }
42
- // })
5
+ import ArmTeleop from './ArmTeleop.svelte'
6
+ import { useSettings } from '../../hooks/useSettings.svelte'
7
+
8
+ const settings = useSettings()
9
+
10
+ // Get controller config from settings
11
+ const config = $derived(settings.current.xrController)
12
+
13
+ // Left controller configuration
14
+ const leftArmName = $derived(config.left.armName)
15
+ const leftGripperName = $derived(config.left.gripperName)
16
+ const leftScaleFactor = $derived(config.left.scaleFactor)
17
+ const leftRotationEnabled = $derived(config.left.rotationEnabled)
18
+
19
+ // Right controller configuration
20
+ const rightArmName = $derived(config.right.armName)
21
+ const rightGripperName = $derived(config.right.gripperName)
22
+ const rightScaleFactor = $derived(config.right.scaleFactor)
23
+ const rightRotationEnabled = $derived(config.right.rotationEnabled)
43
24
  </script>
44
25
 
45
26
  <Controller left>
@@ -57,3 +38,29 @@
57
38
  </RigidBody>
58
39
  {/snippet}
59
40
  </Controller>
41
+
42
+ <!-- Left Controller Arm Teleop -->
43
+ {#if leftArmName}
44
+ {#key `${leftArmName}-${leftGripperName}-${leftScaleFactor}-${leftRotationEnabled}`}
45
+ <ArmTeleop
46
+ armName={leftArmName}
47
+ gripperName={leftGripperName}
48
+ scaleFactor={leftScaleFactor}
49
+ rotationEnabled={leftRotationEnabled}
50
+ hand="left"
51
+ />
52
+ {/key}
53
+ {/if}
54
+
55
+ <!-- Right Controller Arm Teleop -->
56
+ {#if rightArmName}
57
+ {#key `${rightArmName}-${rightGripperName}-${rightScaleFactor}-${rightRotationEnabled}`}
58
+ <ArmTeleop
59
+ armName={rightArmName}
60
+ gripperName={rightGripperName}
61
+ scaleFactor={rightScaleFactor}
62
+ rotationEnabled={rightRotationEnabled}
63
+ hand="right"
64
+ />
65
+ {/key}
66
+ {/if}
@@ -1,18 +1,3 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const Controllers: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type Controllers = InstanceType<typeof Controllers>;
1
+ declare const Controllers: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Controllers = ReturnType<typeof Controllers>;
18
3
  export default Controllers;
@@ -1,14 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { Hand } from '@threlte/xr'
3
3
 
4
- console.log('hands')
5
-
6
4
  const onpinchstart = () => {
7
- console.log('start')
5
+ // Pinch started
8
6
  }
9
7
 
10
8
  const onpinchend = () => {
11
- console.log('end')
9
+ // Pinch ended
12
10
  }
13
11
  </script>
14
12
 
@@ -0,0 +1,209 @@
1
+ <script lang="ts">
2
+ import { T } from '@threlte/core'
3
+ import { CanvasTexture, PlaneGeometry } from 'three'
4
+ import { useArmClient } from '../../hooks/useArmClient.svelte'
5
+ import { useArmKinematics } from '../../hooks/useArmKinematics.svelte'
6
+
7
+ interface JointLimitsWidgetProps {
8
+ armName: string
9
+ offset?: { x?: number; y?: number; z?: number }
10
+ scale?: number
11
+ rotationY?: number
12
+ }
13
+
14
+ let {
15
+ armName,
16
+ offset = {},
17
+ scale = 0.6,
18
+ rotationY = -15 * (Math.PI / 180),
19
+ }: JointLimitsWidgetProps = $props()
20
+
21
+ const armClient = useArmClient()
22
+ const armKinematics = useArmKinematics()
23
+
24
+ interface JointLimitData {
25
+ jointId: string
26
+ currentPosition: number
27
+ min: number
28
+ max: number
29
+ percentage: number
30
+ status: 'safe' | 'caution' | 'danger'
31
+ }
32
+
33
+ // Get joint limits and current positions for this arm
34
+ const jointLimits = $derived(armKinematics.kinematics[armName])
35
+ const currentPositions = $derived(armClient.currentPositions[armName])
36
+
37
+ // Combine limits and positions into display data
38
+ const jointData = $derived.by((): JointLimitData[] | undefined => {
39
+ if (!jointLimits || !currentPositions) return undefined
40
+
41
+ return jointLimits.map((limit, index) => {
42
+ const current = currentPositions[index] ?? 0
43
+ const range = limit.max - limit.min
44
+ const percentage = range !== 0 ? ((current - limit.min) / range) * 100 : 50
45
+
46
+ let status: 'safe' | 'caution' | 'danger'
47
+ if (percentage < 10 || percentage > 90) {
48
+ status = 'danger'
49
+ } else if (percentage < 20 || percentage > 80) {
50
+ status = 'caution'
51
+ } else {
52
+ status = 'safe'
53
+ }
54
+
55
+ return {
56
+ jointId: limit.id,
57
+ currentPosition: current,
58
+ min: limit.min,
59
+ max: limit.max,
60
+ percentage,
61
+ status,
62
+ }
63
+ })
64
+ })
65
+
66
+ // Canvas setup
67
+ const CANVAS_WIDTH = 800
68
+ const HEADER_HEIGHT = 80
69
+ const ROW_HEIGHT = 120
70
+ let canvasHeight = $derived(HEADER_HEIGHT + (jointData?.length ?? 0) * ROW_HEIGHT)
71
+
72
+ let canvas: HTMLCanvasElement | undefined = $state()
73
+ let texture: CanvasTexture | undefined = $state()
74
+ let geometry: PlaneGeometry | undefined = $state()
75
+
76
+ // Initialize canvas
77
+ $effect(() => {
78
+ if (!canvas && jointData && jointData.length > 0) {
79
+ canvas = document.createElement('canvas')
80
+ canvas.width = CANVAS_WIDTH
81
+ canvas.height = canvasHeight
82
+ texture = new CanvasTexture(canvas)
83
+
84
+ // Calculate aspect ratio for plane geometry
85
+ const aspect = CANVAS_WIDTH / canvasHeight
86
+ // Width in 3D space, height based on aspect
87
+ geometry = new PlaneGeometry(1.2, 1.2 / aspect)
88
+ }
89
+ })
90
+
91
+ // Update canvas height when number of joints changes
92
+ $effect(() => {
93
+ if (canvas && jointData) {
94
+ const newHeight = HEADER_HEIGHT + jointData.length * ROW_HEIGHT
95
+ if (canvas.height !== newHeight) {
96
+ canvas.height = newHeight
97
+
98
+ // Update geometry for new aspect ratio
99
+ const aspect = CANVAS_WIDTH / newHeight
100
+ geometry?.dispose()
101
+ geometry = new PlaneGeometry(1.2, 1.2 / aspect)
102
+ }
103
+ }
104
+ })
105
+
106
+ // Render header with arm name
107
+ function renderHeader(ctx: CanvasRenderingContext2D, width: number) {
108
+ // Header background
109
+ ctx.fillStyle = '#0a0a0a'
110
+ ctx.fillRect(0, 0, width, HEADER_HEIGHT)
111
+
112
+ // Arm name
113
+ ctx.fillStyle = '#ffffff'
114
+ ctx.font = 'bold 36px monospace'
115
+ ctx.textBaseline = 'middle'
116
+ ctx.fillText(armName, 20, HEADER_HEIGHT / 2)
117
+
118
+ // Separator line
119
+ ctx.strokeStyle = '#444444'
120
+ ctx.lineWidth = 4
121
+ ctx.beginPath()
122
+ ctx.moveTo(0, HEADER_HEIGHT)
123
+ ctx.lineTo(width, HEADER_HEIGHT)
124
+ ctx.stroke()
125
+ }
126
+
127
+ // Render joint data to canvas
128
+ function renderJointLimits(
129
+ ctx: CanvasRenderingContext2D,
130
+ joints: JointLimitData[],
131
+ width: number,
132
+ height: number
133
+ ) {
134
+ const rowHeight = (height - HEADER_HEIGHT) / joints.length
135
+
136
+ joints.forEach((joint, index) => {
137
+ const y = HEADER_HEIGHT + index * rowHeight
138
+
139
+ // Background row
140
+ ctx.fillStyle = index % 2 === 0 ? '#1a1a1a' : '#222222'
141
+ ctx.fillRect(0, y, width, rowHeight)
142
+
143
+ // Joint label
144
+ ctx.fillStyle = '#ffffff'
145
+ ctx.font = 'bold 32px monospace'
146
+ ctx.textBaseline = 'middle'
147
+ ctx.fillText(joint.jointId, 20, y + rowHeight / 2)
148
+
149
+ // Progress bar dimensions
150
+ const barX = 240
151
+ const barY = y + (rowHeight - 60) / 2
152
+ const barWidth = 360
153
+ const barHeight = 60
154
+
155
+ // Progress bar background
156
+ ctx.fillStyle = '#333333'
157
+ ctx.fillRect(barX, barY, barWidth, barHeight)
158
+
159
+ // Progress bar fill (colored by status)
160
+ const fillWidth = barWidth * (joint.percentage / 100)
161
+ ctx.fillStyle =
162
+ joint.status === 'danger' ? '#ff4444' : joint.status === 'caution' ? '#ffaa00' : '#44ff44'
163
+ ctx.fillRect(barX, barY, fillWidth, barHeight)
164
+
165
+ // Progress bar border
166
+ ctx.strokeStyle = '#666666'
167
+ ctx.lineWidth = 4
168
+ ctx.strokeRect(barX, barY, barWidth, barHeight)
169
+
170
+ // Current value text
171
+ ctx.fillStyle = '#ffffff'
172
+ ctx.font = '28px monospace'
173
+ ctx.fillText(`${joint.currentPosition.toFixed(1)}°`, barX + barWidth + 20, y + rowHeight / 2)
174
+ })
175
+ }
176
+
177
+ // Update canvas when joint data changes
178
+ $effect(() => {
179
+ if (canvas && jointData && jointData.length > 0) {
180
+ const ctx = canvas.getContext('2d')
181
+ if (!ctx) return
182
+
183
+ // Clear canvas
184
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
185
+
186
+ // Render header with arm name
187
+ renderHeader(ctx, canvas.width)
188
+
189
+ // Render joint limits
190
+ renderJointLimits(ctx, jointData, canvas.width, canvas.height)
191
+
192
+ // Mark texture for update
193
+ if (texture) {
194
+ texture.needsUpdate = true
195
+ }
196
+ }
197
+ })
198
+ </script>
199
+
200
+ {#if texture && geometry && jointData && jointData.length > 0}
201
+ <T.Mesh
202
+ position={[offset.x ?? 0, offset.y ?? 1.5, offset.z ?? -2.5]}
203
+ rotation.y={rotationY}
204
+ {scale}
205
+ >
206
+ <T is={geometry} />
207
+ <T.MeshBasicMaterial map={texture} />
208
+ </T.Mesh>
209
+ {/if}
@@ -0,0 +1,13 @@
1
+ interface JointLimitsWidgetProps {
2
+ armName: string;
3
+ offset?: {
4
+ x?: number;
5
+ y?: number;
6
+ z?: number;
7
+ };
8
+ scale?: number;
9
+ rotationY?: number;
10
+ }
11
+ declare const JointLimitsWidget: import("svelte").Component<JointLimitsWidgetProps, {}, "">;
12
+ type JointLimitsWidget = ReturnType<typeof JointLimitsWidget>;
13
+ export default JointLimitsWidget;
@@ -36,7 +36,6 @@
36
36
  const right = useController('right')
37
37
 
38
38
  const leftPad = useGamepad({ xr: true, hand: 'left' })
39
- const rightPad = useGamepad({ xr: true, hand: 'right' })
40
39
 
41
40
  leftPad.trigger.on('down', () => {
42
41
  const grip = $left?.grip
@@ -51,26 +50,13 @@
51
50
  })
52
51
  leftPad.trigger.on('up', () => (dragging = false))
53
52
 
54
- rightPad.trigger.on('down', () => {
55
- const grip = $right?.grip
56
-
57
- if (!grip) {
58
- return
59
- }
60
-
61
- rotating = true
62
- rotateDown.copy($right?.grip.position)
63
- currentDistance = euler.z
64
- })
65
- rightPad.trigger.on('up', () => (rotating = false))
66
-
67
53
  const dragTask = useTask(
68
54
  () => {
69
55
  if (!$left || !rigidBody) return
70
56
 
71
57
  position.copy($left.grip.position).sub(offset)
72
58
 
73
- origin.set(position)
59
+ origin.set([position.x, position.y, position.z])
74
60
 
75
61
  rigidBody.setNextKinematicTranslation({ x: position.x, y: position.y, z: position.z })
76
62
  },