@viamrobotics/motion-tools 1.32.0 → 1.33.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 (132) hide show
  1. package/dist/components/App.svelte +17 -11
  2. package/dist/components/App.svelte.d.ts +14 -7
  3. package/dist/components/Entities/Entities.svelte +18 -25
  4. package/dist/components/Entities/Entities.svelte.d.ts +2 -17
  5. package/dist/components/Entities/Label.svelte +79 -13
  6. package/dist/components/Entities/Label.svelte.d.ts +2 -1
  7. package/dist/components/Entities/Labels.svelte +36 -0
  8. package/dist/components/Entities/Labels.svelte.d.ts +3 -0
  9. package/dist/components/Entities/LineDots.svelte +8 -3
  10. package/dist/components/Entities/labelLayout/applyTeleports.d.ts +9 -0
  11. package/dist/components/Entities/labelLayout/applyTeleports.js +39 -0
  12. package/dist/components/Entities/labelLayout/buildNeighborhood.d.ts +8 -0
  13. package/dist/components/Entities/labelLayout/buildNeighborhood.js +26 -0
  14. package/dist/components/Entities/labelLayout/cameraHash.d.ts +8 -0
  15. package/dist/components/Entities/labelLayout/cameraHash.js +25 -0
  16. package/dist/components/Entities/labelLayout/cost.d.ts +44 -0
  17. package/dist/components/Entities/labelLayout/cost.js +126 -0
  18. package/dist/components/Entities/labelLayout/createLabelLayout.d.ts +27 -0
  19. package/dist/components/Entities/labelLayout/createLabelLayout.js +194 -0
  20. package/dist/components/Entities/labelLayout/geometry.d.ts +20 -0
  21. package/dist/components/Entities/labelLayout/geometry.js +151 -0
  22. package/dist/components/Entities/labelLayout/labelStore.svelte.d.ts +17 -0
  23. package/dist/components/Entities/labelLayout/labelStore.svelte.js +28 -0
  24. package/dist/components/Entities/labelLayout/measure.d.ts +13 -0
  25. package/dist/components/Entities/labelLayout/measure.js +42 -0
  26. package/dist/components/Entities/labelLayout/slots.d.ts +11 -0
  27. package/dist/components/Entities/labelLayout/slots.js +47 -0
  28. package/dist/components/Entities/labelLayout/solve.d.ts +11 -0
  29. package/dist/components/Entities/labelLayout/solve.js +93 -0
  30. package/dist/components/Entities/labelLayout/spatialHash.d.ts +15 -0
  31. package/dist/components/Entities/labelLayout/spatialHash.js +53 -0
  32. package/dist/components/Entities/labelLayout/types.d.ts +105 -0
  33. package/dist/components/Entities/labelLayout/types.js +19 -0
  34. package/dist/components/Entities/labelLayout/writeBack.d.ts +20 -0
  35. package/dist/components/Entities/labelLayout/writeBack.js +51 -0
  36. package/dist/components/Scene.svelte +42 -48
  37. package/dist/components/SceneProviders.svelte +0 -3
  38. package/dist/components/SelectedTransformControls.svelte +65 -47
  39. package/dist/components/overlay/Details.svelte +198 -224
  40. package/dist/components/overlay/Details.svelte.d.ts +1 -1
  41. package/dist/components/overlay/Popover.svelte +6 -4
  42. package/dist/components/overlay/Popover.svelte.d.ts +6 -2
  43. package/dist/components/overlay/dashboard/Button.svelte +7 -2
  44. package/dist/components/overlay/dashboard/Button.svelte.d.ts +2 -1
  45. package/dist/components/overlay/details/AxesHelperDetails.svelte +32 -0
  46. package/dist/components/overlay/details/AxesHelperDetails.svelte.d.ts +7 -0
  47. package/dist/components/overlay/details/ColorDetails.svelte +35 -0
  48. package/dist/components/overlay/details/ColorDetails.svelte.d.ts +7 -0
  49. package/dist/components/overlay/details/GeometryDetails.svelte +104 -0
  50. package/dist/components/overlay/details/GeometryDetails.svelte.d.ts +7 -0
  51. package/dist/components/overlay/details/LineDetails/LineDetails.svelte +196 -0
  52. package/dist/components/overlay/details/LineDetails/LineDetails.svelte.d.ts +7 -0
  53. package/dist/components/overlay/details/LineDetails/linePositions.d.ts +3 -0
  54. package/dist/components/overlay/details/LineDetails/linePositions.js +30 -0
  55. package/dist/components/overlay/details/OpacityDetails.svelte +44 -0
  56. package/dist/components/overlay/details/OpacityDetails.svelte.d.ts +7 -0
  57. package/dist/components/overlay/details/PoseDetails.svelte +189 -0
  58. package/dist/components/overlay/details/PoseDetails.svelte.d.ts +14 -0
  59. package/dist/components/overlay/settings/ConnectionSettings.svelte +42 -0
  60. package/dist/components/overlay/settings/ConnectionSettings.svelte.d.ts +18 -0
  61. package/dist/components/overlay/settings/DebugSettings.svelte +13 -0
  62. package/dist/components/{xr/frame-configure/Controllers.svelte.d.ts → overlay/settings/DebugSettings.svelte.d.ts} +3 -3
  63. package/dist/components/overlay/settings/PointcloudSettings.svelte +61 -0
  64. package/dist/components/overlay/settings/PointcloudSettings.svelte.d.ts +3 -0
  65. package/dist/components/overlay/settings/SceneSettings.svelte +110 -0
  66. package/dist/components/overlay/settings/SceneSettings.svelte.d.ts +18 -0
  67. package/dist/components/overlay/settings/Settings.svelte +27 -312
  68. package/dist/components/overlay/settings/Settings.svelte.d.ts +8 -1
  69. package/dist/components/overlay/settings/Tabs.svelte +5 -3
  70. package/dist/components/overlay/settings/Tabs.svelte.d.ts +3 -3
  71. package/dist/components/overlay/settings/VisionSettings.svelte +31 -0
  72. package/dist/components/overlay/settings/VisionSettings.svelte.d.ts +3 -0
  73. package/dist/components/overlay/settings/WeblabSettings.svelte +27 -0
  74. package/dist/components/overlay/settings/WeblabSettings.svelte.d.ts +18 -0
  75. package/dist/components/overlay/settings/WidgetSettings.svelte +49 -0
  76. package/dist/components/overlay/settings/WidgetSettings.svelte.d.ts +3 -0
  77. package/dist/components/overlay/widgets/FramePov.svelte +1 -12
  78. package/dist/ecs/traits.d.ts +1 -1
  79. package/dist/ecs/traits.js +1 -1
  80. package/dist/hooks/useWorldState.svelte.js +39 -50
  81. package/dist/{components/xr → plugins/XR}/ArmTeleop.svelte +3 -5
  82. package/dist/plugins/XR/DebugPanel.svelte +29 -0
  83. package/dist/plugins/XR/DebugPanel.svelte.d.ts +3 -0
  84. package/dist/plugins/XR/OriginMarker.svelte +341 -0
  85. package/dist/plugins/XR/PendingEditsPanel.svelte +60 -0
  86. package/dist/plugins/XR/PendingEditsPanel.svelte.d.ts +18 -0
  87. package/dist/plugins/XR/WristDisplay.svelte +60 -0
  88. package/dist/plugins/XR/WristDisplay.svelte.d.ts +19 -0
  89. package/dist/{components/xr → plugins/XR}/XR.svelte +69 -23
  90. package/dist/plugins/XR/XRPlugins.svelte +9 -0
  91. package/dist/plugins/XR/XRPlugins.svelte.d.ts +26 -0
  92. package/dist/plugins/XR/XRSettings.svelte +240 -0
  93. package/dist/plugins/XR/XRSettings.svelte.d.ts +3 -0
  94. package/dist/{components/xr → plugins/XR}/XRToast.svelte +6 -9
  95. package/dist/plugins/XR/debug.svelte.d.ts +7 -0
  96. package/dist/plugins/XR/debug.svelte.js +13 -0
  97. package/dist/plugins/XR/frame-configure/Controllers.svelte +413 -0
  98. package/dist/plugins/XR/teleop/Controllers.svelte.d.ts +3 -0
  99. package/dist/{components/xr → plugins/XR}/useAnchors.svelte.d.ts +4 -0
  100. package/dist/{components/xr → plugins/XR}/useAnchors.svelte.js +22 -0
  101. package/dist/plugins/XR/useOrigin.svelte.d.ts +24 -0
  102. package/dist/plugins/XR/useOrigin.svelte.js +50 -0
  103. package/dist/plugins/index.d.ts +2 -0
  104. package/dist/plugins/index.js +2 -0
  105. package/dist/three/OBBHelper.js +1 -0
  106. package/package.json +3 -1
  107. package/dist/components/xr/OriginMarker.svelte +0 -151
  108. package/dist/components/xr/XRControllerSettings.svelte +0 -242
  109. package/dist/components/xr/XRControllerSettings.svelte.d.ts +0 -3
  110. package/dist/components/xr/frame-configure/Controllers.svelte +0 -6
  111. package/dist/components/xr/useOrigin.svelte.d.ts +0 -9
  112. package/dist/components/xr/useOrigin.svelte.js +0 -27
  113. /package/dist/{components/xr → plugins/XR}/ArmTeleop.svelte.d.ts +0 -0
  114. /package/dist/{components/xr → plugins/XR}/BentPlaneGeometry.svelte +0 -0
  115. /package/dist/{components/xr → plugins/XR}/BentPlaneGeometry.svelte.d.ts +0 -0
  116. /package/dist/{components/xr → plugins/XR}/CameraFeed.svelte +0 -0
  117. /package/dist/{components/xr → plugins/XR}/CameraFeed.svelte.d.ts +0 -0
  118. /package/dist/{components/xr → plugins/XR}/JointLimitsWidget.svelte +0 -0
  119. /package/dist/{components/xr → plugins/XR}/JointLimitsWidget.svelte.d.ts +0 -0
  120. /package/dist/{components/xr → plugins/XR}/OriginMarker.svelte.d.ts +0 -0
  121. /package/dist/{components/xr → plugins/XR}/PointDistance.svelte +0 -0
  122. /package/dist/{components/xr → plugins/XR}/PointDistance.svelte.d.ts +0 -0
  123. /package/dist/{components/xr → plugins/XR}/XR.svelte.d.ts +0 -0
  124. /package/dist/{components/xr → plugins/XR}/XRConfigPanel.svelte +0 -0
  125. /package/dist/{components/xr → plugins/XR}/XRConfigPanel.svelte.d.ts +0 -0
  126. /package/dist/{components/xr → plugins/XR}/XRToast.svelte.d.ts +0 -0
  127. /package/dist/{components/xr/teleop → plugins/XR/frame-configure}/Controllers.svelte.d.ts +0 -0
  128. /package/dist/{components/xr → plugins/XR}/math.d.ts +0 -0
  129. /package/dist/{components/xr → plugins/XR}/math.js +0 -0
  130. /package/dist/{components/xr → plugins/XR}/teleop/Controllers.svelte +0 -0
  131. /package/dist/{components/xr → plugins/XR}/toasts.svelte.d.ts +0 -0
  132. /package/dist/{components/xr → plugins/XR}/toasts.svelte.js +0 -0
@@ -0,0 +1,341 @@
1
+ <script lang="ts">
2
+ import { useTask, useThrelte } from '@threlte/core'
3
+ import { useGamepad } from '@threlte/extras'
4
+ import { Hand, useController, useHand, useHeadset, useXR } from '@threlte/xr'
5
+ import { useDebounce } from 'runed'
6
+ import { Euler, Quaternion, Vector3 } from 'three'
7
+
8
+ import { usePartID } from '../../hooks/usePartID.svelte'
9
+
10
+ import { useAnchors } from './useAnchors.svelte'
11
+ import { useOrigin } from './useOrigin.svelte'
12
+
13
+ const origin = useOrigin()
14
+ const anchors = useAnchors()
15
+ const partID = usePartID()
16
+ const headset = useHeadset()
17
+
18
+ const storageKey = $derived(`xr-origin-anchor:${partID.current}`)
19
+
20
+ const DEFAULT_ORIGIN: [number, number, number] = [-1, -1, 0]
21
+ const COMMIT_DEBOUNCE_MS = 500
22
+
23
+ const leftPad = useGamepad({ xr: true, hand: 'left' })
24
+ const rightPad = useGamepad({ xr: true, hand: 'right' })
25
+ const leftController = useController('left')
26
+ const rightController = useController('right')
27
+
28
+ const THUMBSTICK_SPEED = 0.05
29
+
30
+ // Head-relative translation basis. Recomputed per thumbstick tick so
31
+ // stick-forward always moves content away from the viewer, regardless of
32
+ // how the scene was rotated or which way the user is physically facing.
33
+ const headForward = new Vector3()
34
+ const headRight = new Vector3()
35
+
36
+ // The anchor that currently represents the persisted origin. Kept so we can
37
+ // delete it when a new calibration is committed.
38
+ let persistedAnchor: XRAnchor | undefined
39
+
40
+ // The restored anchor we're waiting to localize on session start. Once the
41
+ // device reports it as tracked we snap origin to its pose and clear this.
42
+ let pendingRestore = $state.raw<XRAnchor | undefined>(undefined)
43
+
44
+ const restoreQuat = new Quaternion()
45
+ const restoreEuler = new Euler(0, 0, 0, 'ZYX')
46
+
47
+ const commitVec = new Vector3()
48
+ const commitQuat = new Quaternion()
49
+
50
+ const commit = useDebounce(async () => {
51
+ // origin.position/rotation define the composed XR reference space's
52
+ // offset from zUp, so an anchor at identity in the current (composed)
53
+ // space IS the anchor at origin's pose in zUp. commitVec/commitQuat
54
+ // are left at their default-constructed zero/identity values.
55
+ const anchor = await anchors.createAnchor(commitVec, commitQuat)
56
+ if (!anchor) return
57
+
58
+ const uuid = await anchors.persist(anchor)
59
+ if (!uuid) {
60
+ anchor.delete()
61
+ return
62
+ }
63
+
64
+ const prev = localStorage.getItem(storageKey)
65
+ if (prev && prev !== uuid) {
66
+ anchors.remove(prev).catch(() => {})
67
+ }
68
+
69
+ if (persistedAnchor && persistedAnchor !== anchor) {
70
+ persistedAnchor.delete()
71
+ }
72
+
73
+ persistedAnchor = anchor
74
+ localStorage.setItem(storageKey, uuid)
75
+ }, COMMIT_DEBOUNCE_MS)
76
+
77
+ origin.registerCommit(() => commit())
78
+
79
+ leftPad.thumbstick.on('change', ({ value }) => {
80
+ // While the grip is held, the left controller drives fine rotation;
81
+ // ignore the thumbstick so the two inputs don't fight each other.
82
+ if (leftPad.squeeze.pressed || typeof value === 'number') return
83
+
84
+ const { x: vx, y: vy } = value
85
+ const [x, y, z] = origin.position
86
+
87
+ origin.set([x, y, z + vy * THUMBSTICK_SPEED], origin.rotation + vx * THUMBSTICK_SPEED)
88
+ commit()
89
+ })
90
+
91
+ rightPad.thumbstick.on('change', ({ value }) => {
92
+ if (rightPad.squeeze.pressed || typeof value === 'number') return
93
+
94
+ const { x: vx, y: vy } = value
95
+ const [x, y, z] = origin.position
96
+
97
+ headset.getWorldDirection(headForward)
98
+
99
+ // Flatten onto the XY (ground) plane so pitch doesn't bleed into horizontal motion.
100
+ headForward.z = 0
101
+ if (headForward.lengthSq() < 1e-6) {
102
+ // Viewer gaze was purely vertical (or pose not yet reported) — fall back to scene +Y.
103
+ headForward.set(0, 1, 0)
104
+ } else {
105
+ headForward.normalize()
106
+ }
107
+ // headForward is in the composed XR reference space; rotate into zUp
108
+ // so the stick direction maps to the user's physical gaze regardless
109
+ // of the current origin rotation.
110
+ origin.toZUpDir(headForward)
111
+ headRight.set(headForward.y, -headForward.x, 0)
112
+
113
+ const deltaX = headRight.x * vx * THUMBSTICK_SPEED + headForward.x * vy * THUMBSTICK_SPEED
114
+ const deltaY = headRight.y * vx * THUMBSTICK_SPEED + headForward.y * vy * THUMBSTICK_SPEED
115
+
116
+ origin.set([x + deltaX, y + deltaY, z], origin.rotation)
117
+ commit()
118
+ })
119
+
120
+ // Fine calibration: hold the grip, then move/rotate the controller to nudge
121
+ // the origin 1:1 with the controller delta. Extract world-Z yaw from the
122
+ // controller quaternion for rotation so pitch/roll don't leak in.
123
+ const quatYaw = (q: Quaternion) =>
124
+ Math.atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z))
125
+
126
+ const fineTranslateStart = new Vector3()
127
+ const fineTranslateOriginStart = new Vector3()
128
+ const fineTranslateCurrent = new Vector3()
129
+ let fineTranslating = $state(false)
130
+
131
+ rightPad.squeeze.on('change', () => {
132
+ const ray = rightController.current?.targetRay
133
+ if (rightPad.squeeze.pressed && ray) {
134
+ // Save start position in zUp so the delta stays stable as the
135
+ // composed reference space recomposes each tick on origin changes.
136
+ origin.toZUpPos(fineTranslateStart, ray.position)
137
+ const [ox, oy, oz] = origin.position
138
+ fineTranslateOriginStart.set(ox, oy, oz)
139
+ fineTranslating = true
140
+ } else {
141
+ fineTranslating = false
142
+ }
143
+ })
144
+
145
+ useTask(
146
+ () => {
147
+ const ray = rightController.current?.targetRay
148
+ if (!ray) return
149
+ origin.toZUpPos(fineTranslateCurrent, ray.position)
150
+ origin.set(
151
+ [
152
+ fineTranslateOriginStart.x + fineTranslateCurrent.x - fineTranslateStart.x,
153
+ fineTranslateOriginStart.y + fineTranslateCurrent.y - fineTranslateStart.y,
154
+ fineTranslateOriginStart.z + fineTranslateCurrent.z - fineTranslateStart.z,
155
+ ],
156
+ origin.rotation
157
+ )
158
+ commit()
159
+ },
160
+ { running: () => fineTranslating }
161
+ )
162
+
163
+ let fineRotateStartYaw = 0
164
+ let fineRotateOriginStart = 0
165
+ let fineRotating = $state(false)
166
+
167
+ leftPad.squeeze.on('change', () => {
168
+ const ray = leftController.current?.targetRay
169
+ if (leftPad.squeeze.pressed && ray) {
170
+ // Controller yaw in composed = yaw_zUp − origin.rotation; convert
171
+ // to zUp so the delta stays stable while origin.rotation updates.
172
+ fineRotateStartYaw = quatYaw(ray.quaternion) + origin.rotation
173
+ fineRotateOriginStart = origin.rotation
174
+ fineRotating = true
175
+ } else {
176
+ fineRotating = false
177
+ }
178
+ })
179
+
180
+ useTask(
181
+ () => {
182
+ const ray = leftController.current?.targetRay
183
+ if (!ray) return
184
+ const yawZup = quatYaw(ray.quaternion) + origin.rotation
185
+ origin.set(origin.position, fineRotateOriginStart + yawZup - fineRotateStartYaw)
186
+ commit()
187
+ },
188
+ { running: () => fineRotating }
189
+ )
190
+
191
+ let startLeftPinchTranslation = new Vector3()
192
+ let leftPinchTranslation = new Vector3()
193
+ let startRightPinchTranslation = new Vector3()
194
+ let rightPinchCurrent = new Vector3()
195
+ let startRightPinchRotation = 0
196
+
197
+ const leftHand = useHand('left')
198
+ const rightHand = useHand('right')
199
+
200
+ let translating = $state(false)
201
+ let rotating = $state(false)
202
+
203
+ const { renderer } = useThrelte()
204
+ const { isPresenting } = useXR()
205
+
206
+ // Session start: try to restore a persisted anchor for this part. If none
207
+ // exists or restore fails, fall back to a default offset so the origin is
208
+ // visible in front of the user.
209
+ $effect(() => {
210
+ if (!$isPresenting) {
211
+ pendingRestore = undefined
212
+ persistedAnchor = undefined
213
+ return
214
+ }
215
+
216
+ const uuid = localStorage.getItem(storageKey)
217
+ if (!uuid) {
218
+ origin.set(DEFAULT_ORIGIN, 0)
219
+ return
220
+ }
221
+
222
+ let cancelled = false
223
+ anchors
224
+ .restore(uuid)
225
+ .then((anchor) => {
226
+ if (cancelled) return
227
+ if (anchor) {
228
+ persistedAnchor = anchor
229
+ pendingRestore = anchor
230
+ } else {
231
+ localStorage.removeItem(storageKey)
232
+ origin.set(DEFAULT_ORIGIN, 0)
233
+ }
234
+ })
235
+ .catch(() => {
236
+ if (cancelled) return
237
+ localStorage.removeItem(storageKey)
238
+ origin.set(DEFAULT_ORIGIN, 0)
239
+ })
240
+
241
+ return () => {
242
+ cancelled = true
243
+ }
244
+ })
245
+
246
+ // Once the restored anchor has localized, snap origin to its pose. This
247
+ // runs only while a restore is pending so it won't fight user input.
248
+ useTask(
249
+ () => {
250
+ const anchor = pendingRestore
251
+ if (!anchor) return
252
+
253
+ const pose = anchors.getAnchorPose(anchor)
254
+ if (!pose) return
255
+
256
+ const { position: p, orientation: o } = pose.transform
257
+ restoreQuat.set(o.x, o.y, o.z, o.w)
258
+ restoreEuler.setFromQuaternion(restoreQuat, 'ZYX')
259
+ origin.set([p.x, p.y, p.z], restoreEuler.z)
260
+ pendingRestore = undefined
261
+ },
262
+ {
263
+ running: () => pendingRestore !== undefined,
264
+ }
265
+ )
266
+
267
+ $effect(() => {
268
+ if (!$isPresenting) {
269
+ return
270
+ }
271
+ renderer.xr.getHand(0).addEventListener('pinchstart', () => {
272
+ const p = leftHand.current?.targetRay.position
273
+ if (p) {
274
+ translating = true
275
+ // Pinch start position in zUp; delta is cumulative from here.
276
+ origin.toZUpPos(startLeftPinchTranslation, p)
277
+ }
278
+ })
279
+ })
280
+
281
+ useTask(
282
+ () => {
283
+ const p = leftHand.current?.targetRay.position
284
+ if (p && translating) {
285
+ origin.toZUpPos(leftPinchTranslation, p)
286
+ origin.set(
287
+ [
288
+ leftPinchTranslation.x - startLeftPinchTranslation.x,
289
+ leftPinchTranslation.y - startLeftPinchTranslation.y,
290
+ leftPinchTranslation.z - startLeftPinchTranslation.z,
291
+ ],
292
+ origin.rotation
293
+ )
294
+ commit()
295
+ }
296
+ },
297
+ {
298
+ running: () => translating,
299
+ }
300
+ )
301
+
302
+ useTask(
303
+ () => {
304
+ const p = rightHand.current?.targetRay.position
305
+ if (p && rotating) {
306
+ origin.toZUpPos(rightPinchCurrent, p)
307
+ const deltaX = rightPinchCurrent.x - startRightPinchTranslation.x
308
+ origin.set(origin.position, startRightPinchRotation + deltaX)
309
+ commit()
310
+ }
311
+ },
312
+ {
313
+ running: () => rotating,
314
+ }
315
+ )
316
+ </script>
317
+
318
+ <Hand
319
+ left
320
+ onpinchstart={() => {
321
+ const p = leftHand.current?.targetRay.position
322
+ if (p) {
323
+ translating = true
324
+ origin.toZUpPos(startLeftPinchTranslation, p)
325
+ }
326
+ }}
327
+ onpinchend={() => (translating = false)}
328
+ />
329
+
330
+ <Hand
331
+ right
332
+ onpinchstart={() => {
333
+ const p = rightHand.current?.targetRay.position
334
+ if (p) {
335
+ rotating = true
336
+ origin.toZUpPos(startRightPinchTranslation, p)
337
+ startRightPinchRotation = origin.rotation
338
+ }
339
+ }}
340
+ onpinchend={() => (rotating = false)}
341
+ />
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { Text } from 'threlte-uikit'
3
+ import { Button, ButtonLabel, Panel } from 'threlte-uikit/horizon'
4
+
5
+ import { usePartConfig } from '../../hooks/usePartConfig.svelte'
6
+
7
+ import WristDisplay from './WristDisplay.svelte'
8
+
9
+ const partConfig = usePartConfig()
10
+ </script>
11
+
12
+ {#if partConfig.isDirty}
13
+ <WristDisplay position={[0, 0.005, 0.1]}>
14
+ <Panel
15
+ flexDirection="column"
16
+ padding={16}
17
+ gap={12}
18
+ backgroundColor="#111"
19
+ borderRadius={16}
20
+ minWidth={420}
21
+ >
22
+ <Text
23
+ text="Pending frame edits"
24
+ fontSize={18}
25
+ color="#ffffff"
26
+ />
27
+ <Panel
28
+ flexDirection="row"
29
+ gap={8}
30
+ >
31
+ <Button
32
+ variant="tertiary"
33
+ size="sm"
34
+ onclick={() => partConfig.discardChanges()}
35
+ >
36
+ <ButtonLabel>
37
+ <Text
38
+ text="Discard"
39
+ fontSize={14}
40
+ color="#ffffff"
41
+ />
42
+ </ButtonLabel>
43
+ </Button>
44
+ <Button
45
+ variant="primary"
46
+ size="sm"
47
+ onclick={() => partConfig.save()}
48
+ >
49
+ <ButtonLabel>
50
+ <Text
51
+ text="Save"
52
+ fontSize={14}
53
+ color="#ffffff"
54
+ />
55
+ </ButtonLabel>
56
+ </Button>
57
+ </Panel>
58
+ </Panel>
59
+ </WristDisplay>
60
+ {/if}
@@ -0,0 +1,18 @@
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 PendingEditsPanel: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type PendingEditsPanel = InstanceType<typeof PendingEditsPanel>;
18
+ export default PendingEditsPanel;
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+
4
+ import { T } from '@threlte/core'
5
+ import { useController, useHandJoint } from '@threlte/xr'
6
+ import { fromStore } from 'svelte/store'
7
+ import { Group, type Vector3Tuple } from 'three'
8
+ import { provideDefaultProperties } from 'threlte-uikit'
9
+
10
+ interface Props {
11
+ /** Offset from the wrist in the wrist-local frame, meters. */
12
+ position?: Vector3Tuple
13
+ /**
14
+ * Rotation in the wrist-local frame. The default orients uikit content
15
+ * (panels face their own +Z) onto the dorsal side of the wrist, so the
16
+ * user sees it when turning their palm down, like a smartwatch.
17
+ */
18
+ rotation?: Vector3Tuple
19
+ /** Uniform scale for the wrist group. Smaller than the HUD default
20
+ * because panels live at arm's length instead of ~1 m away. */
21
+ scale?: number
22
+ children?: Snippet
23
+ }
24
+
25
+ const {
26
+ position = [0, 0.005, 0.08],
27
+ rotation = [-Math.PI / 2, 0, 0],
28
+ scale = 0.03,
29
+ children,
30
+ }: Props = $props()
31
+
32
+ // Draw uikit content on top of the scene (real-world depth, selection OBB,
33
+ // etc.), matching the old HUD behavior.
34
+ provideDefaultProperties(() => ({
35
+ depthTest: false,
36
+ renderOrder: 999,
37
+ }))
38
+
39
+ const leftController = fromStore(useController('left'))
40
+ const leftWrist = fromStore(useHandJoint('left', 'wrist'))
41
+
42
+ // Prefer the hand wrist joint when hand tracking is active; fall back to
43
+ // the controller grip. Both are three.js Groups updated per frame by
44
+ // WebXR, so attaching as a child follows the wrist automatically.
45
+ const parent = $derived(leftWrist.current ?? leftController.current?.grip)
46
+
47
+ const group = new Group()
48
+ </script>
49
+
50
+ {#if parent}
51
+ <T
52
+ is={group}
53
+ attach={parent}
54
+ {position}
55
+ {rotation}
56
+ {scale}
57
+ >
58
+ {@render children?.()}
59
+ </T>
60
+ {/if}
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type Vector3Tuple } from 'three';
3
+ interface Props {
4
+ /** Offset from the wrist in the wrist-local frame, meters. */
5
+ position?: Vector3Tuple;
6
+ /**
7
+ * Rotation in the wrist-local frame. The default orients uikit content
8
+ * (panels face their own +Z) onto the dorsal side of the wrist, so the
9
+ * user sees it when turning their palm down, like a smartwatch.
10
+ */
11
+ rotation?: Vector3Tuple;
12
+ /** Uniform scale for the wrist group. Smaller than the HUD default
13
+ * because panels live at arm's length instead of ~1 m away. */
14
+ scale?: number;
15
+ children?: Snippet;
16
+ }
17
+ declare const WristDisplay: import("svelte").Component<Props, {}, "">;
18
+ type WristDisplay = ReturnType<typeof WristDisplay>;
19
+ export default WristDisplay;
@@ -8,24 +8,29 @@
8
8
  import { useSettings } from '../../hooks/useSettings.svelte'
9
9
 
10
10
  import CameraFeed from './CameraFeed.svelte'
11
+ import DebugPanel from './DebugPanel.svelte'
11
12
  import FrameConfigureControllers from './frame-configure/Controllers.svelte'
12
13
  import JointLimitsWidget from './JointLimitsWidget.svelte'
13
14
  import OriginMarker from './OriginMarker.svelte'
15
+ import PendingEditsPanel from './PendingEditsPanel.svelte'
14
16
  import TeleopControllers from './teleop/Controllers.svelte'
15
17
  import { provideAnchors } from './useAnchors.svelte'
16
- import { useOrigin } from './useOrigin.svelte'
18
+ import { provideOrigin } from './useOrigin.svelte'
19
+ import XRPlugins from './XRPlugins.svelte'
17
20
  import XRToast from './XRToast.svelte'
18
21
 
19
22
  const { ...rest } = $props()
20
23
 
21
- const { isPresenting } = useXR()
22
- const settings = useSettings()
23
- const origin = useOrigin()
24
+ const origin = provideOrigin()
24
25
  provideAnchors()
25
- const enableXR = $derived(settings.current.enableXR)
26
26
 
27
+ const { renderer } = useThrelte()
28
+ const { isPresenting } = useXR()
29
+ const settings = useSettings()
27
30
  const partID = usePartID()
28
31
 
32
+ const enableXR = $derived(settings.current.enableXR)
33
+
29
34
  // Get all enabled camera widgets for the current part
30
35
  const enabledCameras = $derived.by(() => {
31
36
  const openWidgets = settings.current.openCameraWidgets
@@ -48,34 +53,57 @@
48
53
  const leftArmName = $derived(controllerConfig.left.armName)
49
54
  const rightArmName = $derived(controllerConfig.right.armName)
50
55
 
51
- const { renderer } = useThrelte()
56
+ // Compose the XR reference space from:
57
+ // 1) a -π/2 rotation around X to switch from WebXR's Y-up to Viam's Z-up
58
+ // 2) the scene origin (position + yaw) so the origin's pose lives at the
59
+ // composed space's identity. With this, controllers, camera, and scene
60
+ // content all share one frame — no separate origin group needed.
61
+ let baseRefSpace: XRReferenceSpace | undefined
52
62
 
53
- // Move into Viam's coordinate system. This basically accomplishes
54
- // the same thing as setting z up in the Camera component.
55
63
  $effect(() => {
56
- if ($isPresenting) {
57
- const q = new Quaternion().setFromAxisAngle({ x: 1, y: 0, z: 0 }, -Math.PI / 2)
58
-
59
- // after the XR session has started and a reference space exists:
60
- const baseRefSpace = renderer.xr.getReferenceSpace()
61
- if (baseRefSpace) {
62
- const rotatedRefSpace = baseRefSpace.getOffsetReferenceSpace(
63
- new XRRigidTransform({ x: 0, y: 0, z: 0, w: 1 }, { x: q.x, y: q.y, z: q.z, w: q.w })
64
- )
64
+ if (!$isPresenting) {
65
+ baseRefSpace = undefined
66
+ return
67
+ }
65
68
 
66
- renderer.xr.setReferenceSpace(rotatedRefSpace)
67
- }
69
+ if (!baseRefSpace) {
70
+ const current = renderer.xr.getReferenceSpace()
71
+ if (!current) return
72
+ baseRefSpace = current
68
73
  }
74
+
75
+ const [ox, oy, oz] = origin.position
76
+ const oRot = origin.rotation
77
+
78
+ const zUpQ = new Quaternion().setFromAxisAngle({ x: 1, y: 0, z: 0 }, -Math.PI / 2)
79
+ const originQ = new Quaternion().setFromAxisAngle({ x: 0, y: 0, z: 1 }, oRot)
80
+
81
+ const composed = baseRefSpace
82
+ .getOffsetReferenceSpace(
83
+ new XRRigidTransform(
84
+ { x: 0, y: 0, z: 0, w: 1 },
85
+ { x: zUpQ.x, y: zUpQ.y, z: zUpQ.z, w: zUpQ.w }
86
+ )
87
+ )
88
+ .getOffsetReferenceSpace(
89
+ new XRRigidTransform(
90
+ { x: ox, y: oy, z: oz },
91
+ { x: originQ.x, y: originQ.y, z: originQ.z, w: originQ.w }
92
+ )
93
+ )
94
+
95
+ renderer.xr.setReferenceSpace(composed)
69
96
  })
70
97
  </script>
71
98
 
99
+ {#if $isPresenting}
100
+ <XRPlugins />
101
+ {/if}
102
+
72
103
  {#if enableXR}
73
104
  <XR
74
- onsessionstart={() => {
75
- origin.set([-1, -1, 0])
76
- }}
77
105
  onsessionend={() => {
78
- origin.set([0, 0, 0])
106
+ origin.set([0, 0, 0], 0)
79
107
  }}
80
108
  >
81
109
  <!-- Render camera feeds only when presenting to avoid conflicting with overlay Camera widgets -->
@@ -115,6 +143,8 @@
115
143
  <!-- <XRConfigPanel offset={{ x: 0, y: 2.5, z: -2.5 }} scale={0.7} /> -->
116
144
 
117
145
  <XRToast />
146
+ <DebugPanel />
147
+ <PendingEditsPanel />
118
148
 
119
149
  {#if settings.current.xrMode === 'arm-teleop'}
120
150
  <TeleopControllers />
@@ -127,6 +157,22 @@
127
157
 
128
158
  <XRButton
129
159
  mode="immersive-ar"
160
+ sessionInit={{
161
+ optionalFeatures: [
162
+ 'local-floor',
163
+ 'bounded-floor',
164
+ 'anchors',
165
+ // Required for cross-session persistence — enables
166
+ // `anchor.requestPersistentHandle()` and
167
+ // `session.restorePersistentAnchor()` on Quest Browser.
168
+ 'persistent-anchors',
169
+ 'plane-detection',
170
+ 'hand-tracking',
171
+ 'layers',
172
+ 'hit-test',
173
+ ],
174
+ }}
175
+ style="color: #555; border-color: #ccc; backdrop-filter: blur(4px); background: rgba(255,255,255,0.5)"
130
176
  {...rest}
131
177
  />
132
178
  {/if}
@@ -0,0 +1,9 @@
1
+ <script>
2
+ import { pointerControls, touchControls } from '@threlte/xr'
3
+
4
+ pointerControls('left')
5
+ pointerControls('right')
6
+
7
+ touchControls('left')
8
+ touchControls('right')
9
+ </script>
@@ -0,0 +1,26 @@
1
+ export default XRPlugins;
2
+ type XRPlugins = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const XRPlugins: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ 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> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }