@viamrobotics/motion-tools 1.10.0 → 1.11.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 (119) hide show
  1. package/dist/HoverUpdater.svelte.d.ts +0 -3
  2. package/dist/HoverUpdater.svelte.js +8 -50
  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 +3 -2
  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/Focus.svelte +1 -8
  17. package/dist/components/Frame.svelte +1 -1
  18. package/dist/components/Geometry.svelte +112 -71
  19. package/dist/components/Geometry.svelte.d.ts +6 -7
  20. package/dist/components/Lasso/Lasso.svelte +153 -0
  21. package/dist/components/Lasso/Lasso.svelte.d.ts +6 -0
  22. package/dist/components/Lasso/Line.svelte +44 -0
  23. package/dist/components/Lasso/Line.svelte.d.ts +11 -0
  24. package/dist/components/PointerMissBox.svelte +0 -1
  25. package/dist/components/Points.svelte +1 -1
  26. package/dist/components/Scene.svelte +3 -6
  27. package/dist/components/SceneProviders.svelte +2 -0
  28. package/dist/components/Snapshot.svelte +1 -1
  29. package/dist/components/Snapshot.svelte.d.ts +1 -1
  30. package/dist/components/hover/HoveredEntityTooltip.svelte +2 -1
  31. package/dist/components/overlay/Details.svelte +20 -0
  32. package/dist/components/overlay/left-pane/TreeContainer.svelte +0 -2
  33. package/dist/components/overlay/settings/Settings.svelte +51 -0
  34. package/dist/components/overlay/widgets/Camera.svelte +20 -12
  35. package/dist/components/xr/ArmTeleop.svelte +469 -0
  36. package/dist/components/xr/ArmTeleop.svelte.d.ts +10 -0
  37. package/dist/components/xr/CameraFeed.svelte +194 -47
  38. package/dist/components/xr/CameraFeed.svelte.d.ts +8 -0
  39. package/dist/components/xr/Controllers.svelte +45 -38
  40. package/dist/components/xr/Controllers.svelte.d.ts +2 -17
  41. package/dist/components/xr/Hands.svelte +2 -4
  42. package/dist/components/xr/JointLimitsWidget.svelte +212 -0
  43. package/dist/components/xr/JointLimitsWidget.svelte.d.ts +13 -0
  44. package/dist/components/xr/OriginMarker.svelte +1 -15
  45. package/dist/components/xr/XR.svelte +86 -5
  46. package/dist/components/xr/XRConfigPanel.svelte +449 -0
  47. package/dist/components/xr/XRConfigPanel.svelte.d.ts +11 -0
  48. package/dist/components/xr/XRControllerSettings.svelte +240 -0
  49. package/dist/components/xr/XRControllerSettings.svelte.d.ts +3 -0
  50. package/dist/components/xr/XRToast.svelte +215 -0
  51. package/dist/components/xr/XRToast.svelte.d.ts +3 -0
  52. package/dist/components/xr/math.d.ts +14 -0
  53. package/dist/components/xr/math.js +26 -0
  54. package/dist/components/xr/toasts.svelte.d.ts +20 -0
  55. package/dist/components/xr/toasts.svelte.js +32 -0
  56. package/dist/components/xr/useOrigin.svelte.d.ts +2 -2
  57. package/dist/components/xr/useOrigin.svelte.js +4 -4
  58. package/dist/ecs/traits.d.ts +9 -0
  59. package/dist/ecs/traits.js +9 -0
  60. package/dist/ecs/useTrait.svelte.d.ts +3 -3
  61. package/dist/frame.d.ts +0 -3
  62. package/dist/hooks/useArmKinematics.svelte.d.ts +12 -0
  63. package/dist/hooks/useArmKinematics.svelte.js +31 -0
  64. package/dist/hooks/useGeometries.svelte.js +46 -35
  65. package/dist/hooks/useObjectEvents.svelte.js +24 -7
  66. package/dist/hooks/usePartConfig.svelte.d.ts +0 -35
  67. package/dist/hooks/usePartConfig.svelte.js +2 -2
  68. package/dist/hooks/usePointcloudObjects.svelte.js +44 -63
  69. package/dist/hooks/usePointclouds.svelte.js +10 -6
  70. package/dist/hooks/usePose.svelte.js +4 -1
  71. package/dist/hooks/useResourceByName.svelte.d.ts +7 -0
  72. package/dist/hooks/useResourceByName.svelte.js +2 -2
  73. package/dist/hooks/useSettings.svelte.d.ts +14 -0
  74. package/dist/hooks/useSettings.svelte.js +10 -0
  75. package/dist/hooks/useWorldState.svelte.d.ts +0 -8
  76. package/dist/lib.d.ts +1 -3
  77. package/dist/lib.js +1 -3
  78. package/dist/plugins/bvh.svelte.d.ts +8 -0
  79. package/dist/plugins/bvh.svelte.js +69 -0
  80. package/dist/ply.d.ts +1 -1
  81. package/dist/ply.js +5 -0
  82. package/dist/snapshot.d.ts +2 -2
  83. package/dist/snapshot.js +2 -2
  84. package/dist/three/InstancedArrows/raycast.d.ts +2 -4
  85. package/dist/three/InstancedArrows/raycast.js +5 -5
  86. package/dist/transform.js +1 -0
  87. package/package.json +7 -5
  88. package/dist/assert.d.ts +0 -14
  89. package/dist/assert.js +0 -21
  90. package/dist/components/BatchedGeometry.svelte +0 -0
  91. package/dist/components/BatchedGeometry.svelte.d.ts +0 -26
  92. package/dist/components/Detections.svelte +0 -41
  93. package/dist/components/Detections.svelte.d.ts +0 -3
  94. package/dist/components/DetectionsPlane.svelte +0 -23
  95. package/dist/components/DetectionsPlane.svelte.d.ts +0 -21
  96. package/dist/components/Geometry2.svelte +0 -211
  97. package/dist/components/Geometry2.svelte.d.ts +0 -19
  98. package/dist/components/overlay/left-pane/Widgets.svelte +0 -65
  99. package/dist/components/overlay/left-pane/Widgets.svelte.d.ts +0 -3
  100. package/dist/entries.d.ts +0 -1
  101. package/dist/entries.js +0 -3
  102. package/dist/hooks/index.d.ts +0 -0
  103. package/dist/hooks/index.js +0 -1
  104. package/dist/test.d.ts +0 -1
  105. package/dist/test.js +0 -1
  106. package/dist/three/BoxHelper.d.ts +0 -50
  107. package/dist/three/BoxHelper.js +0 -134
  108. /package/dist/{common → buf/common}/v1/common_pb.d.ts +0 -0
  109. /package/dist/{common → buf/common}/v1/common_pb.js +0 -0
  110. /package/dist/{draw → buf/draw}/v1/metadata_pb.d.ts +0 -0
  111. /package/dist/{draw → buf/draw}/v1/metadata_pb.js +0 -0
  112. /package/dist/{draw → buf/draw}/v1/scene_pb.d.ts +0 -0
  113. /package/dist/{draw → buf/draw}/v1/scene_pb.js +0 -0
  114. /package/dist/{draw → buf/draw}/v1/snapshot_pb.d.ts +0 -0
  115. /package/dist/{draw → buf/draw}/v1/snapshot_pb.js +0 -0
  116. /package/dist/{draw → buf/draw}/v1/transforms_pb.d.ts +0 -0
  117. /package/dist/{draw → buf/draw}/v1/transforms_pb.js +0 -0
  118. /package/dist/components/{BentPlaneGeometry.svelte → xr/BentPlaneGeometry.svelte} +0 -0
  119. /package/dist/components/{BentPlaneGeometry.svelte.d.ts → xr/BentPlaneGeometry.svelte.d.ts} +0 -0
@@ -1,16 +1,25 @@
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
13
+ onAspectChange?: (aspect: number) => void
11
14
  }
12
15
 
13
- let { resourceName }: CameraFeedProps = $props()
16
+ let {
17
+ resourceName,
18
+ offset = {},
19
+ scale = 0.7,
20
+ enableProfiling = false,
21
+ onAspectChange,
22
+ }: CameraFeedProps = $props()
14
23
 
15
24
  const partID = usePartID()
16
25
  const streamClient = createStreamClient(
@@ -21,63 +30,201 @@
21
30
  let video = document.createElement('video')
22
31
  let aspect = $state(1)
23
32
  let ready = $state(false)
33
+ let texture = $state<VideoTexture | null>(null)
24
34
 
25
- video.addEventListener('canplaythrough', () => {
26
- aspect = video.videoWidth / video.videoHeight
27
- video.play()
28
- })
35
+ // ===== LATENCY PROFILING =====
36
+ interface LatencyMetrics {
37
+ streamConnectTime?: number
38
+ videoReadyTime?: number
39
+ firstFrameTime?: number
40
+ captureToPresent?: number // Camera capture → browser decode (from metadata)
41
+ presentToRender?: number // Browser decode → Three.js texture update
42
+ totalLatency?: number // End-to-end
43
+ fps?: number
44
+ }
45
+ let metrics = $state<LatencyMetrics>({})
46
+ let frameCount = $state(0)
47
+ let lastFrameTime = 0
48
+ let fpsFrames: number[] = []
49
+ let videoFrameCallbackId: number | null = null
50
+
51
+ // Critical: video must autoplay and be muted for streams to work
52
+ video.autoplay = true
53
+ video.muted = true
54
+ video.playsInline = true
55
+
56
+ // Low-latency settings for teleoperation
57
+ // @ts-expect-error - latencyHint is not in standard types but supported by browsers
58
+ video.latencyHint = 0 // Minimize latency
59
+ video.disableRemotePlayback = true
29
60
 
30
61
  $effect.pre(() => {
31
- video.srcObject = streamClient.mediaStream
32
- ready = true
33
- })
62
+ const mediaStream = streamClient.mediaStream
63
+ if (!mediaStream) {
64
+ ready = false
65
+ texture?.dispose()
66
+ texture = null
67
+ return
68
+ }
69
+
70
+ // PROFILING: Stream connected
71
+ const streamConnectTime = performance.now()
72
+ if (enableProfiling) {
73
+ metrics.streamConnectTime = streamConnectTime
74
+ }
75
+
76
+ video.srcObject = mediaStream
34
77
 
35
- const headset = useHeadset()
78
+ // Wait for video to be ready before creating texture
79
+ const onReady = () => {
80
+ const videoReadyTime = performance.now()
81
+ aspect = video.videoWidth / video.videoHeight
82
+ onAspectChange?.(aspect)
36
83
 
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()
84
+ if (!texture) {
85
+ texture = new VideoTexture(video)
86
+ }
42
87
 
43
- const { start, stop } = useTask(
44
- (delta) => {
45
- group.position.lerp(headset.position, delta * 5)
88
+ // Force play to ensure stream is active
89
+ video.play().catch((e) => console.warn('Video play failed:', e))
90
+ ready = true
46
91
 
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)
92
+ // PROFILING: Video ready
93
+ if (enableProfiling) {
94
+ metrics.videoReadyTime = videoReadyTime
95
+ const setupLatency = videoReadyTime - streamConnectTime
96
+ console.log(
97
+ `[🎥 ${resourceName}] Ready: ${video.videoWidth}x${video.videoHeight} (setup: ${setupLatency.toFixed(0)}ms)`
98
+ )
99
+ }
51
100
 
52
- mesh.lookAt(headset.position)
53
- },
54
- {
55
- autoStart: false,
101
+ // Start frame-by-frame profiling using requestVideoFrameCallback
102
+ startFrameProfiling()
103
+ }
104
+
105
+ const onMetadata = () => {
106
+ if (video.readyState >= video.HAVE_METADATA) {
107
+ onReady()
108
+ }
56
109
  }
57
- )
58
110
 
59
- $effect(() => {
60
- if (ready) {
61
- start()
111
+ if (video.readyState >= video.HAVE_METADATA) {
112
+ onReady()
62
113
  } else {
63
- stop()
114
+ video.addEventListener('loadedmetadata', onMetadata, { once: true })
115
+ }
116
+
117
+ // Cleanup when component unmounts
118
+ return () => {
119
+ ready = false
120
+ video.pause()
121
+ video.srcObject = null
122
+ texture?.dispose()
123
+ texture = null
124
+ stopFrameProfiling()
125
+ // Don't stop tracks - let the streamClient manage them
64
126
  }
65
127
  })
66
128
 
67
- const texture = new VideoTexture(video)
129
+ // Frame-by-frame profiling using requestVideoFrameCallback
130
+ function startFrameProfiling() {
131
+ if (!enableProfiling) {
132
+ // If profiling disabled, use simple useTask approach
133
+ return
134
+ }
135
+
136
+ stopFrameProfiling() // Clear any existing callback
137
+
138
+ const updateFrame = (now: number, metadata: VideoFrameCallbackMetadata) => {
139
+ if (!texture || !ready) {
140
+ stopFrameProfiling()
141
+ return
142
+ }
143
+
144
+ frameCount++
145
+
146
+ // Update texture
147
+ texture.needsUpdate = true
148
+
149
+ // Calculate latency metrics from metadata
150
+ if (metadata) {
151
+ // All times in metadata are in microseconds, need to convert to ms
152
+ // 'now' parameter is DOMHighResTimeStamp in milliseconds
153
+ const captureTime = metadata.captureTime || metadata.mediaTime
154
+ const presentationTime = metadata.presentationTime || metadata.expectedDisplayTime
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
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ // Calculate FPS
183
+ if (lastFrameTime > 0) {
184
+ const frameDelta = now - lastFrameTime
185
+ fpsFrames.push(1000 / frameDelta)
186
+ if (fpsFrames.length > 30) fpsFrames.shift() // Keep last 30 frames
187
+ metrics.fps = fpsFrames.reduce((a, b) => a + b, 0) / fpsFrames.length
188
+ }
189
+ lastFrameTime = now
190
+
191
+ // Log key metrics every 60 frames
192
+ if (enableProfiling && frameCount % 60 === 0) {
193
+ const latency = metrics.totalLatency ? `${metrics.totalLatency.toFixed(1)}ms` : 'N/A'
194
+ const fps = metrics.fps ? `${metrics.fps.toFixed(1)}fps` : 'N/A'
195
+ const resolution = metadata ? `${metadata.width}x${metadata.height}` : 'N/A'
196
+ console.log(`[🎥 ${resourceName}] ${resolution} @ ${fps} | Latency: ${latency}`)
197
+ }
198
+
199
+ // Schedule next frame
200
+ videoFrameCallbackId = video.requestVideoFrameCallback(updateFrame)
201
+ }
202
+
203
+ // Start the callback loop
204
+ videoFrameCallbackId = video.requestVideoFrameCallback(updateFrame)
205
+ }
206
+
207
+ function stopFrameProfiling() {
208
+ if (videoFrameCallbackId !== null) {
209
+ video.cancelVideoFrameCallback(videoFrameCallbackId)
210
+ videoFrameCallbackId = null
211
+ }
212
+ }
213
+
214
+ // Fallback: If profiling is disabled, use simple useTask
215
+ useTask(() => {
216
+ if (!enableProfiling && texture && ready) {
217
+ texture.needsUpdate = true
218
+ }
219
+ })
68
220
  </script>
69
221
 
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>
222
+ {#if ready && texture}
223
+ <T.Mesh
224
+ position={[offset.x ?? 0, offset.y ?? 0, offset.z ?? -1.5]}
225
+ {scale}
226
+ >
227
+ <BentPlaneGeometry args={[0.1, aspect, 1, 20, 20]} />
228
+ <T.MeshBasicMaterial map={texture} />
229
+ </T.Mesh>
83
230
  {/if}
@@ -1,5 +1,13 @@
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;
10
+ onAspectChange?: (aspect: number) => void;
3
11
  }
4
12
  declare const CameraFeed: import("svelte").Component<CameraFeedProps, {}, "">;
5
13
  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,212 @@
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 { armName, offset = {}, scale = 0.6, rotationY = 0 }: JointLimitsWidgetProps = $props()
15
+
16
+ const armClient = useArmClient()
17
+ const armKinematics = useArmKinematics()
18
+
19
+ interface JointLimitData {
20
+ jointId: string
21
+ currentPosition: number
22
+ min: number
23
+ max: number
24
+ percentage: number
25
+ status: 'safe' | 'caution' | 'danger'
26
+ }
27
+
28
+ // Get joint limits and current positions for this arm
29
+ const jointLimits = $derived(armKinematics.kinematics[armName])
30
+ const currentPositions = $derived(armClient.currentPositions[armName])
31
+
32
+ // Combine limits and positions into display data
33
+ const jointData = $derived.by((): JointLimitData[] | undefined => {
34
+ if (!jointLimits || !currentPositions) return undefined
35
+
36
+ return jointLimits.map((limit, index) => {
37
+ const current = currentPositions[index] ?? 0
38
+ const range = limit.max - limit.min
39
+ const percentage = range !== 0 ? ((current - limit.min) / range) * 100 : 50
40
+
41
+ let status: 'safe' | 'caution' | 'danger'
42
+ if (percentage < 10 || percentage > 90) {
43
+ status = 'danger'
44
+ } else if (percentage < 20 || percentage > 80) {
45
+ status = 'caution'
46
+ } else {
47
+ status = 'safe'
48
+ }
49
+
50
+ return {
51
+ jointId: limit.id,
52
+ currentPosition: current,
53
+ min: limit.min,
54
+ max: limit.max,
55
+ percentage,
56
+ status,
57
+ }
58
+ })
59
+ })
60
+
61
+ // Canvas setup — use 2x resolution for sharper XR text
62
+ const RESOLUTION_SCALE = 4
63
+ const CANVAS_WIDTH = 800 * RESOLUTION_SCALE
64
+ const HEADER_HEIGHT = 80 * RESOLUTION_SCALE
65
+ const ROW_HEIGHT = 120 * RESOLUTION_SCALE
66
+ let canvasHeight = $derived(HEADER_HEIGHT + (jointData?.length ?? 0) * ROW_HEIGHT)
67
+
68
+ let canvas: HTMLCanvasElement | undefined = $state()
69
+ let texture: CanvasTexture | undefined = $state()
70
+ let geometry: PlaneGeometry | undefined = $state()
71
+
72
+ // Initialize canvas
73
+ $effect(() => {
74
+ if (!canvas && jointData && jointData.length > 0) {
75
+ canvas = document.createElement('canvas')
76
+ canvas.width = CANVAS_WIDTH
77
+ canvas.height = canvasHeight
78
+ texture = new CanvasTexture(canvas)
79
+
80
+ // Calculate aspect ratio for plane geometry
81
+ const aspect = CANVAS_WIDTH / canvasHeight
82
+ // Width in 3D space, height based on aspect
83
+ geometry = new PlaneGeometry(1.2, 1.2 / aspect)
84
+ }
85
+ })
86
+
87
+ // Update canvas height when number of joints changes
88
+ $effect(() => {
89
+ if (canvas && jointData) {
90
+ const newHeight = HEADER_HEIGHT + jointData.length * ROW_HEIGHT
91
+ if (canvas.height !== newHeight) {
92
+ canvas.height = newHeight
93
+
94
+ // Update geometry for new aspect ratio
95
+ const aspect = CANVAS_WIDTH / newHeight
96
+ geometry?.dispose()
97
+ geometry = new PlaneGeometry(1.2, 1.2 / aspect)
98
+ }
99
+ }
100
+ })
101
+
102
+ // Render header with arm name
103
+ function renderHeader(ctx: CanvasRenderingContext2D, width: number) {
104
+ const s = RESOLUTION_SCALE
105
+
106
+ // Header background
107
+ ctx.fillStyle = '#0a0a0a'
108
+ ctx.fillRect(0, 0, width, HEADER_HEIGHT)
109
+
110
+ // Arm name
111
+ ctx.fillStyle = '#ffffff'
112
+ ctx.font = `bold ${36 * s}px monospace`
113
+ ctx.textBaseline = 'middle'
114
+ ctx.fillText(armName, 20 * s, HEADER_HEIGHT / 2)
115
+
116
+ // Separator line
117
+ ctx.strokeStyle = '#444444'
118
+ ctx.lineWidth = 4 * s
119
+ ctx.beginPath()
120
+ ctx.moveTo(0, HEADER_HEIGHT)
121
+ ctx.lineTo(width, HEADER_HEIGHT)
122
+ ctx.stroke()
123
+ }
124
+
125
+ // Render joint data to canvas
126
+ function renderJointLimits(
127
+ ctx: CanvasRenderingContext2D,
128
+ joints: JointLimitData[],
129
+ width: number,
130
+ height: number
131
+ ) {
132
+ const s = RESOLUTION_SCALE
133
+ const rowHeight = (height - HEADER_HEIGHT) / joints.length
134
+
135
+ joints.forEach((joint, index) => {
136
+ const y = HEADER_HEIGHT + index * rowHeight
137
+
138
+ // Background row
139
+ ctx.fillStyle = index % 2 === 0 ? '#1a1a1a' : '#222222'
140
+ ctx.fillRect(0, y, width, rowHeight)
141
+
142
+ // Joint label
143
+ ctx.fillStyle = '#ffffff'
144
+ ctx.font = `bold ${32 * s}px monospace`
145
+ ctx.textBaseline = 'middle'
146
+ ctx.fillText(joint.jointId, 20 * s, y + rowHeight / 2)
147
+
148
+ // Progress bar dimensions
149
+ const barX = 240 * s
150
+ const barY = y + (rowHeight - 60 * s) / 2
151
+ const barWidth = 360 * s
152
+ const barHeight = 60 * s
153
+
154
+ // Progress bar background
155
+ ctx.fillStyle = '#333333'
156
+ ctx.fillRect(barX, barY, barWidth, barHeight)
157
+
158
+ // Progress bar fill (colored by status)
159
+ const fillWidth = barWidth * (joint.percentage / 100)
160
+ ctx.fillStyle =
161
+ joint.status === 'danger' ? '#ff4444' : joint.status === 'caution' ? '#ffaa00' : '#44ff44'
162
+ ctx.fillRect(barX, barY, fillWidth, barHeight)
163
+
164
+ // Progress bar border
165
+ ctx.strokeStyle = '#666666'
166
+ ctx.lineWidth = 4 * s
167
+ ctx.strokeRect(barX, barY, barWidth, barHeight)
168
+
169
+ // Current value text
170
+ ctx.fillStyle = '#ffffff'
171
+ ctx.font = `${28 * s}px monospace`
172
+ ctx.fillText(
173
+ `${joint.currentPosition.toFixed(1)}°`,
174
+ barX + barWidth + 20 * s,
175
+ y + rowHeight / 2
176
+ )
177
+ })
178
+ }
179
+
180
+ // Update canvas when joint data changes
181
+ $effect(() => {
182
+ if (canvas && jointData && jointData.length > 0) {
183
+ const ctx = canvas.getContext('2d')
184
+ if (!ctx) return
185
+
186
+ // Clear canvas
187
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
188
+
189
+ // Render header with arm name
190
+ renderHeader(ctx, canvas.width)
191
+
192
+ // Render joint limits
193
+ renderJointLimits(ctx, jointData, canvas.width, canvas.height)
194
+
195
+ // Mark texture for update
196
+ if (texture) {
197
+ texture.needsUpdate = true
198
+ }
199
+ }
200
+ })
201
+ </script>
202
+
203
+ {#if texture && geometry && jointData && jointData.length > 0}
204
+ <T.Mesh
205
+ position={[offset.x ?? 0, offset.y ?? 1.5, offset.z ?? -2.5]}
206
+ rotation.y={rotationY}
207
+ {scale}
208
+ >
209
+ <T is={geometry} />
210
+ <T.MeshBasicMaterial map={texture} />
211
+ </T.Mesh>
212
+ {/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
  },