@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
@@ -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
- // Create a single StreamClient instance per robot client
74
- let streamClient = $derived(client.current ? new StreamClient(client.current) : undefined)
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
- {#key environment.current.viewerMode === 'monitor'}
168
- <CameraStream
169
- {name}
170
- partID={partID.current}
171
- onloadedmetadata={onMediaLoad}
172
- onload={onMediaLoad}
173
- />
174
- {/key}
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;