@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.
- package/dist/HoverUpdater.svelte.d.ts +0 -3
- package/dist/HoverUpdater.svelte.js +8 -50
- 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 +3 -2
- 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/Focus.svelte +1 -8
- package/dist/components/Frame.svelte +1 -1
- package/dist/components/Geometry.svelte +112 -71
- package/dist/components/Geometry.svelte.d.ts +6 -7
- package/dist/components/Lasso/Lasso.svelte +153 -0
- package/dist/components/Lasso/Lasso.svelte.d.ts +6 -0
- package/dist/components/Lasso/Line.svelte +44 -0
- package/dist/components/Lasso/Line.svelte.d.ts +11 -0
- package/dist/components/PointerMissBox.svelte +0 -1
- package/dist/components/Points.svelte +1 -1
- package/dist/components/Scene.svelte +3 -6
- package/dist/components/SceneProviders.svelte +2 -0
- package/dist/components/Snapshot.svelte +1 -1
- package/dist/components/Snapshot.svelte.d.ts +1 -1
- package/dist/components/hover/HoveredEntityTooltip.svelte +2 -1
- package/dist/components/overlay/Details.svelte +20 -0
- package/dist/components/overlay/left-pane/TreeContainer.svelte +0 -2
- package/dist/components/overlay/settings/Settings.svelte +51 -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 +194 -47
- package/dist/components/xr/CameraFeed.svelte.d.ts +8 -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 +212 -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 +86 -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/traits.d.ts +9 -0
- package/dist/ecs/traits.js +9 -0
- 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 +46 -35
- package/dist/hooks/useObjectEvents.svelte.js +24 -7
- package/dist/hooks/usePartConfig.svelte.d.ts +0 -35
- package/dist/hooks/usePartConfig.svelte.js +2 -2
- package/dist/hooks/usePointcloudObjects.svelte.js +44 -63
- package/dist/hooks/usePointclouds.svelte.js +10 -6
- package/dist/hooks/usePose.svelte.js +4 -1
- 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/plugins/bvh.svelte.d.ts +8 -0
- package/dist/plugins/bvh.svelte.js +69 -0
- package/dist/ply.d.ts +1 -1
- package/dist/ply.js +5 -0
- 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/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/{BentPlaneGeometry.svelte → xr/BentPlaneGeometry.svelte} +0 -0
- /package/dist/components/{BentPlaneGeometry.svelte.d.ts → xr/BentPlaneGeometry.svelte.d.ts} +0 -0
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import { useSelectedEntity } from '../../../hooks/useSelection.svelte'
|
|
5
5
|
import { provideTreeExpandedContext } from './useExpanded.svelte'
|
|
6
6
|
import Logs from './Logs.svelte'
|
|
7
|
-
import Widgets from './Widgets.svelte'
|
|
8
7
|
import AddFrames from './AddFrames.svelte'
|
|
9
8
|
import { useEnvironment } from '../../../hooks/useEnvironment.svelte'
|
|
10
9
|
import { usePartID } from '../../../hooks/usePartID.svelte'
|
|
@@ -85,5 +84,4 @@
|
|
|
85
84
|
{/if}
|
|
86
85
|
|
|
87
86
|
<Logs />
|
|
88
|
-
<Widgets />
|
|
89
87
|
</div>
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import Tabs from './Tabs.svelte'
|
|
16
16
|
import { PersistedState } from 'runed'
|
|
17
17
|
import ToggleGroup from '../ToggleGroup.svelte'
|
|
18
|
+
import XRControllerSettings from '../../xr/XRControllerSettings.svelte'
|
|
18
19
|
|
|
19
20
|
const { invalidate } = useThrelte()
|
|
20
21
|
const partID = usePartID()
|
|
@@ -36,6 +37,10 @@
|
|
|
36
37
|
invalidate()
|
|
37
38
|
})
|
|
38
39
|
|
|
40
|
+
const currentRobotCameraWidgets = $derived(
|
|
41
|
+
settings.current.openCameraWidgets[partID.current] || []
|
|
42
|
+
)
|
|
43
|
+
|
|
39
44
|
const isOpen = new PersistedState('settings-is-open', false)
|
|
40
45
|
const activeTab = new PersistedState('settings-active-tab', 'Connection')
|
|
41
46
|
</script>
|
|
@@ -258,6 +263,50 @@
|
|
|
258
263
|
</div>
|
|
259
264
|
{/snippet}
|
|
260
265
|
|
|
266
|
+
{#snippet XR()}
|
|
267
|
+
<div class="flex flex-col gap-2.5 text-xs">
|
|
268
|
+
<XRControllerSettings />
|
|
269
|
+
</div>
|
|
270
|
+
{/snippet}
|
|
271
|
+
|
|
272
|
+
{#snippet Widgets()}
|
|
273
|
+
<div class="text-gray-9 flex flex-col gap-1 text-xs">
|
|
274
|
+
<label class="flex items-center justify-between gap-2 py-1">
|
|
275
|
+
Arm positions
|
|
276
|
+
<Switch bind:on={settings.current.enableArmPositionsWidget} />
|
|
277
|
+
</label>
|
|
278
|
+
|
|
279
|
+
{@render SectionTitle('Camera widgets')}
|
|
280
|
+
|
|
281
|
+
{#each cameras.current as camera (camera)}
|
|
282
|
+
{@const isWidgetOpen = currentRobotCameraWidgets.includes(camera.name)}
|
|
283
|
+
<div class="flex items-center justify-between gap-2 py-0.5">
|
|
284
|
+
<span class="min-w-0 truncate">{camera.name}</span>
|
|
285
|
+
<Switch
|
|
286
|
+
on={isWidgetOpen}
|
|
287
|
+
on:change={(event) => {
|
|
288
|
+
if (event.detail) {
|
|
289
|
+
settings.current.openCameraWidgets = {
|
|
290
|
+
...settings.current.openCameraWidgets,
|
|
291
|
+
[partID.current]: [...currentRobotCameraWidgets, camera.name],
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
settings.current.openCameraWidgets = {
|
|
295
|
+
...settings.current.openCameraWidgets,
|
|
296
|
+
[partID.current]: currentRobotCameraWidgets.filter(
|
|
297
|
+
(widget) => widget !== camera.name
|
|
298
|
+
),
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
{:else}
|
|
305
|
+
No cameras detected
|
|
306
|
+
{/each}
|
|
307
|
+
</div>
|
|
308
|
+
{/snippet}
|
|
309
|
+
|
|
261
310
|
<FloatingPanel
|
|
262
311
|
title="Settings"
|
|
263
312
|
bind:isOpen={isOpen.current}
|
|
@@ -270,7 +319,9 @@
|
|
|
270
319
|
{ label: 'Scene', content: Scene },
|
|
271
320
|
{ label: 'Pointclouds', content: Pointclouds },
|
|
272
321
|
{ label: 'Vision', content: Vision },
|
|
322
|
+
{ label: 'Widgets', content: Widgets },
|
|
273
323
|
{ label: 'Stats', content: Stats },
|
|
324
|
+
...('xr' in navigator ? [{ label: 'VR / AR', content: XR }] : []),
|
|
274
325
|
]}
|
|
275
326
|
onValueChange={(value) => {
|
|
276
327
|
activeTab.current = value
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { draggable } from '@neodrag/svelte'
|
|
3
3
|
import { Icon, Select } from '@viamrobotics/prime-core'
|
|
4
|
-
import { CameraStream, useRobotClient } from '@viamrobotics/svelte-sdk'
|
|
5
|
-
import { StreamClient } from '@viamrobotics/sdk'
|
|
4
|
+
import { CameraStream, useRobotClient, useConnectionStatus } from '@viamrobotics/svelte-sdk'
|
|
5
|
+
import { StreamClient, MachineConnectionEvent } from '@viamrobotics/sdk'
|
|
6
6
|
import { useSettings } from '../../../hooks/useSettings.svelte'
|
|
7
7
|
import { usePartID } from '../../../hooks/usePartID.svelte'
|
|
8
8
|
import { useEnvironment } from '../../../hooks/useEnvironment.svelte'
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const settings = useSettings()
|
|
18
18
|
const partID = usePartID()
|
|
19
19
|
const client = useRobotClient(() => partID.current)
|
|
20
|
+
const connectionStatus = useConnectionStatus(() => partID.current)
|
|
20
21
|
const environment = useEnvironment()
|
|
21
22
|
|
|
22
23
|
let dragElement = $state.raw<HTMLElement>()
|
|
@@ -70,13 +71,18 @@
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
//
|
|
74
|
-
let streamClient = $derived(
|
|
74
|
+
// Only create StreamClient when connection is fully established
|
|
75
|
+
let streamClient = $derived(
|
|
76
|
+
client.current && connectionStatus.current === MachineConnectionEvent.CONNECTED
|
|
77
|
+
? new StreamClient(client.current)
|
|
78
|
+
: undefined
|
|
79
|
+
)
|
|
75
80
|
|
|
76
81
|
$effect(() => {
|
|
77
82
|
if (streamClient) {
|
|
78
83
|
isLoading = true
|
|
79
84
|
error = undefined
|
|
85
|
+
|
|
80
86
|
streamClient
|
|
81
87
|
.getOptions(name)
|
|
82
88
|
.then((options) => {
|
|
@@ -164,14 +170,16 @@
|
|
|
164
170
|
class="relative min-h-0 w-full flex-1 overflow-hidden bg-black [&_img]:h-full [&_img]:w-full [&_img]:object-fill [&_video]:h-full [&_video]:w-full [&_video]:object-fill"
|
|
165
171
|
style:aspect-ratio={aspectRatio}
|
|
166
172
|
>
|
|
167
|
-
{#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
{#if connectionStatus.current === MachineConnectionEvent.CONNECTED}
|
|
174
|
+
{#key environment.current.viewerMode === 'monitor'}
|
|
175
|
+
<CameraStream
|
|
176
|
+
{name}
|
|
177
|
+
partID={partID.current}
|
|
178
|
+
onloadedmetadata={onMediaLoad}
|
|
179
|
+
onload={onMediaLoad}
|
|
180
|
+
/>
|
|
181
|
+
{/key}
|
|
182
|
+
{/if}
|
|
175
183
|
|
|
176
184
|
<!-- FPS Pill -->
|
|
177
185
|
{#if fps > 0}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useTask, T } from '@threlte/core'
|
|
3
|
+
import { useController, useXR, type XRController } from '@threlte/xr'
|
|
4
|
+
import { Vector3, Quaternion } from 'three'
|
|
5
|
+
import { createResourceClient } from '@viamrobotics/svelte-sdk'
|
|
6
|
+
import { ArmClient, GripperClient } from '@viamrobotics/sdk'
|
|
7
|
+
import * as VIAM from '@viamrobotics/sdk'
|
|
8
|
+
import { usePartID } from '../../hooks/usePartID.svelte'
|
|
9
|
+
import {
|
|
10
|
+
getFrameTransformationQuaternion,
|
|
11
|
+
calculatePositionTarget,
|
|
12
|
+
} from './math'
|
|
13
|
+
import { OrientationVector } from '../../three/OrientationVector'
|
|
14
|
+
import { xrToast } from './toasts.svelte'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
armName: string
|
|
18
|
+
gripperName?: string
|
|
19
|
+
scaleFactor?: number
|
|
20
|
+
hand?: 'left' | 'right'
|
|
21
|
+
rotationEnabled?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
armName,
|
|
26
|
+
gripperName,
|
|
27
|
+
scaleFactor = 1.0,
|
|
28
|
+
hand = 'right',
|
|
29
|
+
rotationEnabled = true,
|
|
30
|
+
}: Props = $props()
|
|
31
|
+
|
|
32
|
+
const partID = usePartID()
|
|
33
|
+
|
|
34
|
+
// Capture initial prop values — parent uses {#key} to force remount on changes.
|
|
35
|
+
// Wrapped in an IIFE to avoid Svelte's state_referenced_locally warning.
|
|
36
|
+
const { initialHand, initialGripperName } = (() => ({
|
|
37
|
+
initialHand: hand,
|
|
38
|
+
initialGripperName: gripperName,
|
|
39
|
+
}))()
|
|
40
|
+
|
|
41
|
+
// Create Viam Arm Client
|
|
42
|
+
const armClient = createResourceClient(
|
|
43
|
+
ArmClient,
|
|
44
|
+
() => partID.current,
|
|
45
|
+
() => armName
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Create Viam Gripper Client (optional)
|
|
49
|
+
const gripperClient = initialGripperName
|
|
50
|
+
? createResourceClient(
|
|
51
|
+
GripperClient,
|
|
52
|
+
() => partID.current,
|
|
53
|
+
() => initialGripperName
|
|
54
|
+
)
|
|
55
|
+
: undefined
|
|
56
|
+
|
|
57
|
+
// Get XR Context for Raw Input
|
|
58
|
+
const { session } = useXR()
|
|
59
|
+
const controller = useController(initialHand)
|
|
60
|
+
|
|
61
|
+
let isControlling = $state(false)
|
|
62
|
+
let wasPressed = false // Frame-to-frame state for edge detection (squeeze button)
|
|
63
|
+
let wasTriggerPressed = false // Frame-to-frame state for trigger
|
|
64
|
+
let wasBPressed = false // Frame-to-frame state for B button
|
|
65
|
+
let isSending = false
|
|
66
|
+
let isReturning = false // Prevent control during return to saved pose
|
|
67
|
+
let gripperStopTimeout: ReturnType<typeof setTimeout> | null = null
|
|
68
|
+
|
|
69
|
+
// Stack to store saved poses - can return to previous positions
|
|
70
|
+
let poseStack: VIAM.Pose[] = []
|
|
71
|
+
|
|
72
|
+
// Reference States
|
|
73
|
+
let controllerRefPos = new Vector3()
|
|
74
|
+
// The Controller's rotation at start, converted to Robot Frame
|
|
75
|
+
let controllerRefRotRobot = new Quaternion()
|
|
76
|
+
|
|
77
|
+
// Robot Reference (Viam Checkpoint)
|
|
78
|
+
let robotRefPos = { x: 0, y: 0, z: 0 }
|
|
79
|
+
let robotRefQuat = new Quaternion()
|
|
80
|
+
let robotRefOV = new OrientationVector() // Keep default radians - setUnits breaks toQuaternion!
|
|
81
|
+
|
|
82
|
+
// Offset from controller orientation to arm orientation
|
|
83
|
+
// This maintains the relationship: armRot = controllerRot * offset
|
|
84
|
+
let controllerToArmOffset = new Quaternion()
|
|
85
|
+
|
|
86
|
+
// Transformation Frame
|
|
87
|
+
const qTransform = getFrameTransformationQuaternion()
|
|
88
|
+
|
|
89
|
+
// Throttling
|
|
90
|
+
let lastCommandTime = 0
|
|
91
|
+
let errorTimeout = 0
|
|
92
|
+
let lastErrorHapticTime = 0
|
|
93
|
+
const COMMAND_INTERVAL = 11 // ms (90Hz)
|
|
94
|
+
const ERROR_COOLDOWN = 1000 // ms
|
|
95
|
+
const ERROR_HAPTIC_INTERVAL = 200 // ms between error haptic pulses
|
|
96
|
+
let lastErrorToastTime = 0
|
|
97
|
+
const ERROR_TOAST_COOLDOWN = 3000 // ms - don't spam error toasts
|
|
98
|
+
|
|
99
|
+
// Haptic Feedback Helper
|
|
100
|
+
function triggerHapticFeedback(intensity: number = 0.5, duration: number = 100) {
|
|
101
|
+
const currentSession = $session
|
|
102
|
+
if (!currentSession) return
|
|
103
|
+
|
|
104
|
+
const inputSource = Array.from(currentSession.inputSources).find(
|
|
105
|
+
(s) => s.handedness === initialHand
|
|
106
|
+
)
|
|
107
|
+
if (!inputSource?.gamepad?.hapticActuators?.length) return
|
|
108
|
+
|
|
109
|
+
const actuator = inputSource.gamepad.hapticActuators[0]
|
|
110
|
+
if ('pulse' in actuator) {
|
|
111
|
+
actuator
|
|
112
|
+
.pulse(intensity, duration)
|
|
113
|
+
.catch((e) => console.warn('[ArmTeleop] Haptic pulse failed:', e))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function showArmErrorToast(error: unknown) {
|
|
118
|
+
const now = Date.now()
|
|
119
|
+
if (now - lastErrorToastTime < ERROR_TOAST_COOLDOWN) return
|
|
120
|
+
lastErrorToastTime = now
|
|
121
|
+
|
|
122
|
+
const msg = String(error).toLowerCase()
|
|
123
|
+
if (
|
|
124
|
+
msg.includes('motion') &&
|
|
125
|
+
(msg.includes('not found') ||
|
|
126
|
+
msg.includes('not registered') ||
|
|
127
|
+
msg.includes('not configured'))
|
|
128
|
+
) {
|
|
129
|
+
xrToast.danger('Motion service not registered')
|
|
130
|
+
} else {
|
|
131
|
+
xrToast.warning('Position not reachable (IK error)')
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ghost Visualization State
|
|
136
|
+
let ghostPos = new Vector3()
|
|
137
|
+
let ghostRot = new Quaternion()
|
|
138
|
+
let ghostPosArray = $state<[number, number, number]>([0, 0, 0])
|
|
139
|
+
let ghostRotArray = $state<[number, number, number, number]>([0, 0, 0, 1])
|
|
140
|
+
|
|
141
|
+
useTask(() => {
|
|
142
|
+
// 1. Get Input Source
|
|
143
|
+
const currentSession = $session
|
|
144
|
+
if (!currentSession || !controller.current) return
|
|
145
|
+
|
|
146
|
+
const inputSource = Array.from(currentSession.inputSources).find(
|
|
147
|
+
(s) => s.handedness === initialHand
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if (!inputSource || !inputSource.gamepad) return
|
|
151
|
+
|
|
152
|
+
// 2. Poll Buttons
|
|
153
|
+
// Trigger (button 0) - Gripper control
|
|
154
|
+
// Squeeze/Grip (button 1) - Arm control
|
|
155
|
+
// B button (button 5 on Quest controllers) - Return to saved pose
|
|
156
|
+
const trigger = inputSource.gamepad.buttons[0]
|
|
157
|
+
const squeeze = inputSource.gamepad.buttons[1]
|
|
158
|
+
const bButton = inputSource.gamepad.buttons[5]
|
|
159
|
+
const isPressed = squeeze && squeeze.pressed
|
|
160
|
+
const isTriggerPressed = trigger && trigger.pressed
|
|
161
|
+
const isBPressed = bButton && bButton.pressed
|
|
162
|
+
|
|
163
|
+
// 3. Edge Detection & State Machine - ARM CONTROL (Squeeze)
|
|
164
|
+
if (isPressed && !wasPressed) {
|
|
165
|
+
// Rising Edge: Start Control
|
|
166
|
+
if (armClient.current) {
|
|
167
|
+
handleStartControl(controller.current)
|
|
168
|
+
}
|
|
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
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 4. Edge Detection - GRIPPER CONTROL (Trigger)
|
|
181
|
+
if (gripperClient?.current) {
|
|
182
|
+
if (isTriggerPressed && !wasTriggerPressed) {
|
|
183
|
+
// Trigger pressed: Grab/close gripper
|
|
184
|
+
// Clear any pending stop timeout
|
|
185
|
+
if (gripperStopTimeout) {
|
|
186
|
+
clearTimeout(gripperStopTimeout)
|
|
187
|
+
gripperStopTimeout = null
|
|
188
|
+
}
|
|
189
|
+
gripperClient.current.grab().catch((e) => console.warn('Gripper grab failed:', e))
|
|
190
|
+
} else if (!isTriggerPressed && wasTriggerPressed) {
|
|
191
|
+
// Trigger released: Open gripper, then stop after 1 second
|
|
192
|
+
// Clear any pending stop timeout
|
|
193
|
+
if (gripperStopTimeout) {
|
|
194
|
+
clearTimeout(gripperStopTimeout)
|
|
195
|
+
gripperStopTimeout = null
|
|
196
|
+
}
|
|
197
|
+
gripperClient.current.open().catch((e) => console.warn('Gripper open failed:', e))
|
|
198
|
+
|
|
199
|
+
// Schedule stop after 1 second
|
|
200
|
+
gripperStopTimeout = setTimeout(() => {
|
|
201
|
+
gripperClient?.current?.stop().catch((e) => console.warn('Gripper stop failed:', e))
|
|
202
|
+
gripperStopTimeout = null
|
|
203
|
+
}, 1000)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 5. Edge Detection - RETURN TO SAVED POSE (B Button)
|
|
208
|
+
if (isBPressed && !wasBPressed) {
|
|
209
|
+
if (poseStack.length > 0) {
|
|
210
|
+
handleReturnToPose()
|
|
211
|
+
} else {
|
|
212
|
+
xrToast.warning('No saved positions to return to')
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
wasPressed = isPressed
|
|
217
|
+
wasTriggerPressed = isTriggerPressed
|
|
218
|
+
wasBPressed = isBPressed
|
|
219
|
+
|
|
220
|
+
// 6. Control Loop (skip if returning to saved pose)
|
|
221
|
+
if (isControlling && armClient.current && !isReturning) {
|
|
222
|
+
handleControlFrame(controller.current)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Helper to transform XR Quaternion to Robot Frame: T * q * inv(T)
|
|
227
|
+
function transformToRobotFrame(q: Quaternion, transform: Quaternion) {
|
|
228
|
+
const transformInv = transform.clone().invert()
|
|
229
|
+
return transform.clone().multiply(q).multiply(transformInv)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function handleStartControl(c: XRController) {
|
|
233
|
+
try {
|
|
234
|
+
const currentPose = await armClient.current!.getEndPosition()
|
|
235
|
+
|
|
236
|
+
if (!currentPose) {
|
|
237
|
+
console.warn('[ArmTeleop] Could not get end position')
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const { x, y, z, oX, oY, oZ, theta } = currentPose
|
|
242
|
+
|
|
243
|
+
robotRefPos = { x, y, z }
|
|
244
|
+
robotRefOV.set(oX, oY, oZ, (theta * Math.PI) / 180) // SDK returns degrees, convert to radians
|
|
245
|
+
robotRefQuat = robotRefOV.toQuaternion(new Quaternion()).normalize()
|
|
246
|
+
|
|
247
|
+
// Save this pose to the stack for quick return
|
|
248
|
+
poseStack.push({ x, y, z, oX, oY, oZ, theta })
|
|
249
|
+
|
|
250
|
+
// Use grip space for tracking
|
|
251
|
+
const grip = c.grip
|
|
252
|
+
if (!grip) {
|
|
253
|
+
console.error('[ArmTeleop] No grip space found on controller')
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
controllerRefPos.copy(grip.position)
|
|
258
|
+
|
|
259
|
+
// 1. Capture Reference and Transform to Robot Frame straight away
|
|
260
|
+
// Matches Dart: referenceRotationQuaternionViamPhone
|
|
261
|
+
controllerRefRotRobot = transformToRobotFrame(grip.quaternion, qTransform).normalize()
|
|
262
|
+
|
|
263
|
+
// 2. Compute offset from controller orientation to arm orientation
|
|
264
|
+
// This maintains: armRot = controllerRot * offset
|
|
265
|
+
// So: offset = inverse(controllerRot) * armRot
|
|
266
|
+
controllerToArmOffset = controllerRefRotRobot
|
|
267
|
+
.clone()
|
|
268
|
+
.invert()
|
|
269
|
+
.multiply(robotRefQuat)
|
|
270
|
+
.normalize()
|
|
271
|
+
|
|
272
|
+
errorTimeout = 0
|
|
273
|
+
|
|
274
|
+
isControlling = true
|
|
275
|
+
|
|
276
|
+
// Haptic feedback: short pulse on teleop start
|
|
277
|
+
triggerHapticFeedback(0.5, 100)
|
|
278
|
+
} catch (e) {
|
|
279
|
+
console.error('[ArmTeleop] Failed to start teleop:', e)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function handleStopControl() {
|
|
284
|
+
try {
|
|
285
|
+
await armClient.current!.getEndPosition()
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.error('[ArmTeleop] Failed to get final position:', e)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function handleControlFrame(c: XRController) {
|
|
292
|
+
const now = Date.now()
|
|
293
|
+
|
|
294
|
+
const grip = c.grip
|
|
295
|
+
if (!grip) return
|
|
296
|
+
|
|
297
|
+
const currentControllerPos = grip.position
|
|
298
|
+
const currentControllerRot = grip.quaternion
|
|
299
|
+
|
|
300
|
+
// Calculate Delta XR for visualizer
|
|
301
|
+
const deltaXR = currentControllerPos.clone().sub(controllerRefPos)
|
|
302
|
+
|
|
303
|
+
// --- Position Step ---
|
|
304
|
+
const targetPos = calculatePositionTarget(
|
|
305
|
+
currentControllerPos,
|
|
306
|
+
controllerRefPos,
|
|
307
|
+
robotRefPos,
|
|
308
|
+
qTransform,
|
|
309
|
+
scaleFactor
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
// --- Rotation Step ---
|
|
313
|
+
let targetOV
|
|
314
|
+
if (rotationEnabled) {
|
|
315
|
+
// ABSOLUTE ROTATION: Transform controller orientation to robot frame, then apply offset
|
|
316
|
+
// 1. Transform XR Frame → Robot Frame using sandwich transform: T * q * T^-1
|
|
317
|
+
const currentRotRobot = transformToRobotFrame(currentControllerRot, qTransform).normalize()
|
|
318
|
+
|
|
319
|
+
// 2. Apply offset to maintain initial controller→arm relationship
|
|
320
|
+
// targetArmRot = currentControllerRot * offset
|
|
321
|
+
const targetArmRotQuat = currentRotRobot.clone().multiply(controllerToArmOffset).normalize()
|
|
322
|
+
|
|
323
|
+
// 3. Convert to Viam OrientationVector using proper Dart-matching algorithm
|
|
324
|
+
// Keep radians - conversion to degrees happens when sending to backend
|
|
325
|
+
targetOV = new OrientationVector().setFromQuaternion(targetArmRotQuat)
|
|
326
|
+
|
|
327
|
+
// Update Ghost Rotation for visualizer
|
|
328
|
+
ghostRot.copy(currentControllerRot)
|
|
329
|
+
} else {
|
|
330
|
+
// Keep orientation fixed to start - use original OV
|
|
331
|
+
targetOV = robotRefOV
|
|
332
|
+
ghostRot.copy(grip.quaternion) // Just track hand
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- Update Ghost Visualizer ---
|
|
336
|
+
ghostPos.copy(controllerRefPos).add(deltaXR.multiplyScalar(scaleFactor))
|
|
337
|
+
|
|
338
|
+
/*
|
|
339
|
+
Ghost Rotation is handled above for simplicity.
|
|
340
|
+
Strictly, visualizer should probably show the Robot Frame rotation mapped back to XR,
|
|
341
|
+
but showing raw controller rotation is better for "feeling" where your hand is.
|
|
342
|
+
*/
|
|
343
|
+
|
|
344
|
+
ghostPosArray = ghostPos.toArray()
|
|
345
|
+
ghostRotArray = ghostRot.toArray()
|
|
346
|
+
|
|
347
|
+
// --- Send Command ---
|
|
348
|
+
if (now - lastCommandTime < COMMAND_INTERVAL) return
|
|
349
|
+
if (isSending) return
|
|
350
|
+
|
|
351
|
+
// If in error state, provide haptic feedback and skip sending
|
|
352
|
+
if (now < errorTimeout) {
|
|
353
|
+
// Buzz controller to indicate IK constraint error (throttled)
|
|
354
|
+
if (now - lastErrorHapticTime > ERROR_HAPTIC_INTERVAL) {
|
|
355
|
+
triggerHapticFeedback(0.7, 150) // Stronger, longer pulse for errors
|
|
356
|
+
lastErrorHapticTime = now
|
|
357
|
+
}
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
lastCommandTime = now
|
|
362
|
+
isSending = true
|
|
363
|
+
|
|
364
|
+
if (isNaN(targetPos.x) || isNaN(targetOV.th)) {
|
|
365
|
+
console.warn('Teleop Safety: NaN detected', targetPos, targetOV)
|
|
366
|
+
isSending = false
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const command = {
|
|
371
|
+
servo_cartesian: {
|
|
372
|
+
x: targetPos.x,
|
|
373
|
+
y: targetPos.y,
|
|
374
|
+
z: targetPos.z,
|
|
375
|
+
o_x: targetOV.x,
|
|
376
|
+
o_y: targetOV.y,
|
|
377
|
+
o_z: targetOV.z,
|
|
378
|
+
theta: (targetOV.th * 180) / Math.PI, // Convert radians to degrees for backend
|
|
379
|
+
speed: 7,
|
|
380
|
+
acceleration: 10,
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let USE_UFACTORY_IK = false
|
|
385
|
+
if (USE_UFACTORY_IK) {
|
|
386
|
+
const client = armClient.current
|
|
387
|
+
if (client) {
|
|
388
|
+
client
|
|
389
|
+
.doCommand(VIAM.Struct.fromJson(command))
|
|
390
|
+
.catch((e) => {
|
|
391
|
+
console.warn('Move failed:', e)
|
|
392
|
+
errorTimeout = Date.now() + ERROR_COOLDOWN
|
|
393
|
+
triggerHapticFeedback(0.8, 200)
|
|
394
|
+
lastErrorHapticTime = Date.now()
|
|
395
|
+
showArmErrorToast(e)
|
|
396
|
+
})
|
|
397
|
+
.finally(() => {
|
|
398
|
+
isSending = false
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
armClient
|
|
403
|
+
.current!.moveToPosition({
|
|
404
|
+
x: targetPos.x,
|
|
405
|
+
y: targetPos.y,
|
|
406
|
+
z: targetPos.z,
|
|
407
|
+
oX: targetOV.x,
|
|
408
|
+
oY: targetOV.y,
|
|
409
|
+
oZ: targetOV.z,
|
|
410
|
+
theta: (targetOV.th * 180) / Math.PI,
|
|
411
|
+
})
|
|
412
|
+
.catch((e) => {
|
|
413
|
+
console.warn('Move failed:', e)
|
|
414
|
+
errorTimeout = Date.now() + ERROR_COOLDOWN
|
|
415
|
+
triggerHapticFeedback(0.8, 200)
|
|
416
|
+
lastErrorHapticTime = Date.now()
|
|
417
|
+
showArmErrorToast(e)
|
|
418
|
+
})
|
|
419
|
+
.finally(() => {
|
|
420
|
+
isSending = false
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function handleReturnToPose() {
|
|
426
|
+
if (!armClient.current || poseStack.length === 0) return
|
|
427
|
+
|
|
428
|
+
// Pop the last saved pose
|
|
429
|
+
const savedPose = poseStack.pop()!
|
|
430
|
+
|
|
431
|
+
isReturning = true
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
// Use moveToPosition to return to the saved pose
|
|
435
|
+
await armClient.current.moveToPosition(savedPose)
|
|
436
|
+
xrToast.success('Returned to saved position')
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.error('[ArmTeleop] Failed to return to saved pose:', e)
|
|
439
|
+
xrToast.danger('Failed to return to position')
|
|
440
|
+
} finally {
|
|
441
|
+
isReturning = false
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
</script>
|
|
445
|
+
|
|
446
|
+
{#if isControlling}
|
|
447
|
+
<!-- Ghost Marker (Target Position in XR Space) -->
|
|
448
|
+
<T.Mesh
|
|
449
|
+
position={ghostPosArray}
|
|
450
|
+
quaternion={ghostRotArray}
|
|
451
|
+
>
|
|
452
|
+
<T.BoxGeometry args={[0.05, 0.05, 0.1]} />
|
|
453
|
+
<T.MeshBasicMaterial
|
|
454
|
+
color="hotpink"
|
|
455
|
+
wireframe
|
|
456
|
+
/>
|
|
457
|
+
<T.AxesHelper args={[0.2]} />
|
|
458
|
+
</T.Mesh>
|
|
459
|
+
|
|
460
|
+
<!-- Original Reference Marker -->
|
|
461
|
+
<T.Mesh position={controllerRefPos.toArray()}>
|
|
462
|
+
<T.SphereGeometry args={[0.02]} />
|
|
463
|
+
<T.MeshBasicMaterial
|
|
464
|
+
color="gray"
|
|
465
|
+
opacity={0.5}
|
|
466
|
+
transparent
|
|
467
|
+
/>
|
|
468
|
+
</T.Mesh>
|
|
469
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
armName: string;
|
|
3
|
+
gripperName?: string;
|
|
4
|
+
scaleFactor?: number;
|
|
5
|
+
hand?: 'left' | 'right';
|
|
6
|
+
rotationEnabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare const ArmTeleop: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ArmTeleop = ReturnType<typeof ArmTeleop>;
|
|
10
|
+
export default ArmTeleop;
|