@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.
- package/dist/HoverUpdater.svelte.d.ts +16 -0
- package/dist/HoverUpdater.svelte.js +78 -0
- package/dist/WorldObject.svelte.d.ts +27 -0
- package/dist/WorldObject.svelte.js +8 -55
- package/dist/{draw → buf/draw}/v1/drawing_pb.d.ts +6 -0
- package/dist/{draw → buf/draw}/v1/drawing_pb.js +7 -0
- package/dist/buf/draw/v1/service_connect.d.ts +122 -0
- package/dist/buf/draw/v1/service_connect.js +126 -0
- package/dist/buf/draw/v1/service_pb.d.ts +382 -0
- package/dist/buf/draw/v1/service_pb.js +612 -0
- package/dist/components/App.svelte +28 -30
- package/dist/components/Arrows/Arrows.svelte +16 -3
- package/dist/components/FileDrop/file-dropper.d.ts +1 -1
- package/dist/components/FileDrop/snapshot-dropper.js +1 -1
- package/dist/components/FileDrop/useFileDrop.svelte.d.ts +2 -1
- package/dist/components/Frame.svelte +1 -1
- package/dist/components/Geometry.svelte +113 -71
- package/dist/components/Geometry.svelte.d.ts +6 -7
- package/dist/components/MeasureTool/MeasurePoint.svelte +3 -3
- package/dist/components/MeasureTool/MeasureTool.svelte +6 -6
- package/dist/components/SceneProviders.svelte +4 -0
- package/dist/components/Snapshot.svelte +1 -1
- package/dist/components/Snapshot.svelte.d.ts +1 -1
- package/dist/components/hover/HoveredEntities.svelte +23 -0
- package/dist/components/hover/HoveredEntity.svelte +15 -0
- package/dist/components/hover/HoveredEntity.svelte.d.ts +3 -0
- package/dist/components/hover/HoveredEntityTooltip.svelte +70 -0
- package/dist/components/{HoveredEntityTooltip.svelte.d.ts → hover/HoveredEntityTooltip.svelte.d.ts} +2 -2
- package/dist/components/hover/LinkedHoveredEntity.svelte +55 -0
- package/dist/components/hover/LinkedHoveredEntity.svelte.d.ts +9 -0
- package/dist/components/overlay/AddRelationship.svelte +131 -0
- package/dist/components/overlay/AddRelationship.svelte.d.ts +7 -0
- package/dist/components/overlay/Details.svelte +55 -2
- package/dist/components/overlay/FloatingPanel.svelte +78 -0
- package/dist/components/overlay/FloatingPanel.svelte.d.ts +13 -0
- package/dist/components/overlay/{left-pane/RefreshRate.svelte → RefreshRate.svelte} +1 -1
- package/dist/components/overlay/ToggleGroup.svelte +22 -26
- package/dist/components/overlay/ToggleGroup.svelte.d.ts +6 -7
- package/dist/components/overlay/left-pane/TreeContainer.svelte +0 -4
- package/dist/components/overlay/settings/Settings.svelte +330 -0
- package/dist/components/overlay/settings/Tabs.svelte +54 -0
- package/dist/components/overlay/settings/Tabs.svelte.d.ts +12 -0
- package/dist/components/overlay/widgets/Camera.svelte +20 -12
- package/dist/components/xr/ArmTeleop.svelte +469 -0
- package/dist/components/xr/ArmTeleop.svelte.d.ts +10 -0
- package/dist/components/xr/CameraFeed.svelte +191 -47
- package/dist/components/xr/CameraFeed.svelte.d.ts +7 -0
- package/dist/components/xr/Controllers.svelte +45 -38
- package/dist/components/xr/Controllers.svelte.d.ts +2 -17
- package/dist/components/xr/Hands.svelte +2 -4
- package/dist/components/xr/JointLimitsWidget.svelte +209 -0
- package/dist/components/xr/JointLimitsWidget.svelte.d.ts +13 -0
- package/dist/components/xr/OriginMarker.svelte +1 -15
- package/dist/components/xr/XR.svelte +78 -5
- package/dist/components/xr/XRConfigPanel.svelte +449 -0
- package/dist/components/xr/XRConfigPanel.svelte.d.ts +11 -0
- package/dist/components/xr/XRControllerSettings.svelte +240 -0
- package/dist/components/xr/XRControllerSettings.svelte.d.ts +3 -0
- package/dist/components/xr/XRToast.svelte +215 -0
- package/dist/components/xr/XRToast.svelte.d.ts +3 -0
- package/dist/components/xr/math.d.ts +14 -0
- package/dist/components/xr/math.js +26 -0
- package/dist/components/xr/toasts.svelte.d.ts +20 -0
- package/dist/components/xr/toasts.svelte.js +32 -0
- package/dist/components/xr/useOrigin.svelte.d.ts +2 -2
- package/dist/components/xr/useOrigin.svelte.js +4 -4
- package/dist/ecs/index.d.ts +1 -0
- package/dist/ecs/index.js +1 -0
- package/dist/ecs/relations.d.ts +7 -0
- package/dist/ecs/relations.js +7 -0
- package/dist/ecs/traits.d.ts +15 -1
- package/dist/ecs/traits.js +19 -5
- package/dist/ecs/useTrait.svelte.d.ts +3 -3
- package/dist/frame.d.ts +0 -3
- package/dist/hooks/useArmKinematics.svelte.d.ts +12 -0
- package/dist/hooks/useArmKinematics.svelte.js +31 -0
- package/dist/hooks/useGeometries.svelte.js +47 -36
- package/dist/hooks/useLinked.svelte.d.ts +7 -0
- package/dist/hooks/useLinked.svelte.js +35 -0
- package/dist/hooks/useObjectEvents.svelte.js +52 -16
- package/dist/hooks/usePartConfig.svelte.d.ts +0 -35
- package/dist/hooks/usePartConfig.svelte.js +2 -2
- package/dist/hooks/usePointcloudObjects.svelte.js +45 -64
- package/dist/hooks/usePointclouds.svelte.js +13 -9
- package/dist/hooks/usePose.svelte.js +5 -2
- package/dist/hooks/useResourceByName.svelte.d.ts +7 -0
- package/dist/hooks/useResourceByName.svelte.js +2 -2
- package/dist/hooks/useSettings.svelte.d.ts +14 -0
- package/dist/hooks/useSettings.svelte.js +10 -0
- package/dist/hooks/useWorldState.svelte.d.ts +0 -8
- package/dist/lib.d.ts +1 -3
- package/dist/lib.js +1 -3
- package/dist/snapshot.d.ts +2 -2
- package/dist/snapshot.js +2 -2
- package/dist/three/InstancedArrows/raycast.d.ts +2 -4
- package/dist/three/InstancedArrows/raycast.js +5 -5
- package/dist/transform.js +1 -0
- package/package.json +7 -5
- package/dist/assert.d.ts +0 -14
- package/dist/assert.js +0 -21
- package/dist/components/BatchedGeometry.svelte +0 -0
- package/dist/components/BatchedGeometry.svelte.d.ts +0 -26
- package/dist/components/Detections.svelte +0 -41
- package/dist/components/Detections.svelte.d.ts +0 -3
- package/dist/components/DetectionsPlane.svelte +0 -23
- package/dist/components/DetectionsPlane.svelte.d.ts +0 -21
- package/dist/components/Geometry2.svelte +0 -211
- package/dist/components/Geometry2.svelte.d.ts +0 -19
- package/dist/components/HoveredEntities.svelte +0 -19
- package/dist/components/HoveredEntityTooltip.svelte +0 -242
- package/dist/components/overlay/left-pane/Settings.svelte +0 -221
- package/dist/components/overlay/left-pane/Widgets.svelte +0 -65
- package/dist/components/overlay/left-pane/Widgets.svelte.d.ts +0 -3
- package/dist/entries.d.ts +0 -1
- package/dist/entries.js +0 -3
- package/dist/hooks/index.d.ts +0 -0
- package/dist/hooks/index.js +0 -1
- package/dist/test.d.ts +0 -1
- package/dist/test.js +0 -1
- package/dist/three/BoxHelper.d.ts +0 -50
- package/dist/three/BoxHelper.js +0 -134
- /package/dist/{common → buf/common}/v1/common_pb.d.ts +0 -0
- /package/dist/{common → buf/common}/v1/common_pb.js +0 -0
- /package/dist/{draw → buf/draw}/v1/metadata_pb.d.ts +0 -0
- /package/dist/{draw → buf/draw}/v1/metadata_pb.js +0 -0
- /package/dist/{draw → buf/draw}/v1/scene_pb.d.ts +0 -0
- /package/dist/{draw → buf/draw}/v1/scene_pb.js +0 -0
- /package/dist/{draw → buf/draw}/v1/snapshot_pb.d.ts +0 -0
- /package/dist/{draw → buf/draw}/v1/snapshot_pb.js +0 -0
- /package/dist/{draw → buf/draw}/v1/transforms_pb.d.ts +0 -0
- /package/dist/{draw → buf/draw}/v1/transforms_pb.js +0 -0
- /package/dist/components/{HoveredEntities.svelte.d.ts → hover/HoveredEntities.svelte.d.ts} +0 -0
- /package/dist/components/overlay/{left-pane/RefreshRate.svelte.d.ts → RefreshRate.svelte.d.ts} +0 -0
- /package/dist/components/overlay/{left-pane → settings}/Settings.svelte.d.ts +0 -0
- /package/dist/components/{BentPlaneGeometry.svelte → xr/BentPlaneGeometry.svelte} +0 -0
- /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
|
|
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 {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let quaternion = new Quaternion()
|
|
41
|
-
let direction = new Vector3()
|
|
81
|
+
if (!texture) {
|
|
82
|
+
texture = new VideoTexture(video)
|
|
83
|
+
}
|
|
42
84
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
start()
|
|
108
|
+
if (video.readyState >= video.HAVE_METADATA) {
|
|
109
|
+
onReady()
|
|
62
110
|
} else {
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
5
|
+
// Pinch started
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
const onpinchend = () => {
|
|
11
|
-
|
|
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
|
},
|