@viamrobotics/motion-tools 1.31.0 → 1.33.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.
Files changed (130) hide show
  1. package/dist/components/App.svelte +64 -53
  2. package/dist/components/App.svelte.d.ts +14 -7
  3. package/dist/components/Entities/Arrows/Arrows.svelte +4 -7
  4. package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +0 -1
  5. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +30 -16
  6. package/dist/components/InputBindings.svelte +0 -43
  7. package/dist/components/KeyboardBindings.svelte +38 -0
  8. package/dist/components/KeyboardBindings.svelte.d.ts +18 -0
  9. package/dist/components/PointerMissBox.svelte +6 -3
  10. package/dist/components/Scene.svelte +43 -61
  11. package/dist/components/SceneProviders.svelte +2 -7
  12. package/dist/components/SceneProviders.svelte.d.ts +1 -3
  13. package/dist/components/Selected.svelte +20 -27
  14. package/dist/components/SelectedTransformControls.svelte +8 -7
  15. package/dist/components/StaticGeometries.svelte +3 -5
  16. package/dist/components/hover/HoveredEntities.svelte +15 -14
  17. package/dist/components/hover/HoveredEntities.svelte.d.ts +17 -2
  18. package/dist/components/hover/HoveredEntity.svelte +8 -5
  19. package/dist/components/hover/HoveredEntity.svelte.d.ts +5 -1
  20. package/dist/components/hover/LinkedHoveredEntity.svelte +7 -11
  21. package/dist/components/hover/LinkedHoveredEntity.svelte.d.ts +1 -0
  22. package/dist/components/overlay/Details.svelte +22 -37
  23. package/dist/components/overlay/Details.svelte.d.ts +3 -1
  24. package/dist/components/overlay/controls/Controls.svelte +0 -2
  25. package/dist/components/overlay/dashboard/Button.svelte +5 -3
  26. package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
  27. package/dist/components/overlay/left-pane/Tree.svelte +13 -10
  28. package/dist/components/overlay/left-pane/TreeContainer.svelte +9 -4
  29. package/dist/components/overlay/left-pane/TreeNode.svelte +6 -4
  30. package/dist/components/overlay/settings/ConnectionSettings.svelte +42 -0
  31. package/dist/components/overlay/settings/ConnectionSettings.svelte.d.ts +18 -0
  32. package/dist/components/overlay/settings/DebugSettings.svelte +13 -0
  33. package/dist/components/{xr/frame-configure/Controllers.svelte.d.ts → overlay/settings/DebugSettings.svelte.d.ts} +3 -3
  34. package/dist/components/overlay/settings/PointcloudSettings.svelte +61 -0
  35. package/dist/components/overlay/settings/PointcloudSettings.svelte.d.ts +3 -0
  36. package/dist/components/overlay/settings/SceneSettings.svelte +110 -0
  37. package/dist/components/overlay/settings/SceneSettings.svelte.d.ts +18 -0
  38. package/dist/components/overlay/settings/Settings.svelte +27 -312
  39. package/dist/components/overlay/settings/Settings.svelte.d.ts +8 -1
  40. package/dist/components/overlay/settings/Tabs.svelte +5 -3
  41. package/dist/components/overlay/settings/Tabs.svelte.d.ts +3 -3
  42. package/dist/components/overlay/settings/VisionSettings.svelte +31 -0
  43. package/dist/components/overlay/settings/VisionSettings.svelte.d.ts +3 -0
  44. package/dist/components/overlay/settings/WeblabSettings.svelte +27 -0
  45. package/dist/components/overlay/settings/WeblabSettings.svelte.d.ts +18 -0
  46. package/dist/components/overlay/settings/WidgetSettings.svelte +49 -0
  47. package/dist/components/overlay/settings/WidgetSettings.svelte.d.ts +3 -0
  48. package/dist/components/overlay/widgets/FramePov.svelte +1 -12
  49. package/dist/draw.d.ts +1 -0
  50. package/dist/draw.js +1 -1
  51. package/dist/ecs/index.d.ts +1 -0
  52. package/dist/ecs/index.js +1 -0
  53. package/dist/ecs/traits.d.ts +22 -5
  54. package/dist/ecs/traits.js +33 -4
  55. package/dist/ecs/useTag.svelte.d.ts +5 -0
  56. package/dist/ecs/useTag.svelte.js +43 -0
  57. package/dist/hooks/useEnvironment.svelte.d.ts +1 -1
  58. package/dist/hooks/useLinked.svelte.js +7 -8
  59. package/dist/hooks/useMouseRaycaster.svelte.d.ts +4 -3
  60. package/dist/hooks/useMouseRaycaster.svelte.js +1 -0
  61. package/dist/hooks/useSettings.svelte.d.ts +1 -1
  62. package/dist/plugins/Focus/Focus.svelte +45 -0
  63. package/dist/plugins/Focus/Focus.svelte.d.ts +3 -0
  64. package/dist/plugins/Focus/FocusBox.svelte +75 -0
  65. package/dist/plugins/Focus/FocusBox.svelte.d.ts +3 -0
  66. package/dist/plugins/Focus/provideFocus.svelte.d.ts +1 -0
  67. package/dist/plugins/Focus/provideFocus.svelte.js +61 -0
  68. package/dist/{components → plugins}/MeasureTool/MeasureTool.svelte +6 -8
  69. package/dist/plugins/Selection/SelectionTool.svelte +10 -3
  70. package/dist/{components/xr → plugins/XR}/ArmTeleop.svelte +3 -5
  71. package/dist/plugins/XR/DebugPanel.svelte +29 -0
  72. package/dist/plugins/XR/DebugPanel.svelte.d.ts +3 -0
  73. package/dist/plugins/XR/OriginMarker.svelte +341 -0
  74. package/dist/plugins/XR/PendingEditsPanel.svelte +60 -0
  75. package/dist/plugins/XR/PendingEditsPanel.svelte.d.ts +18 -0
  76. package/dist/plugins/XR/WristDisplay.svelte +60 -0
  77. package/dist/plugins/XR/WristDisplay.svelte.d.ts +19 -0
  78. package/dist/{components/xr → plugins/XR}/XR.svelte +69 -23
  79. package/dist/plugins/XR/XRPlugins.svelte +9 -0
  80. package/dist/plugins/XR/XRPlugins.svelte.d.ts +26 -0
  81. package/dist/plugins/XR/XRSettings.svelte +240 -0
  82. package/dist/plugins/XR/XRSettings.svelte.d.ts +3 -0
  83. package/dist/{components/xr → plugins/XR}/XRToast.svelte +6 -9
  84. package/dist/plugins/XR/debug.svelte.d.ts +7 -0
  85. package/dist/plugins/XR/debug.svelte.js +13 -0
  86. package/dist/plugins/XR/frame-configure/Controllers.svelte +413 -0
  87. package/dist/plugins/XR/teleop/Controllers.svelte.d.ts +3 -0
  88. package/dist/{components/xr → plugins/XR}/useAnchors.svelte.d.ts +4 -0
  89. package/dist/{components/xr → plugins/XR}/useAnchors.svelte.js +22 -0
  90. package/dist/plugins/XR/useOrigin.svelte.d.ts +24 -0
  91. package/dist/plugins/XR/useOrigin.svelte.js +50 -0
  92. package/dist/plugins/index.d.ts +4 -0
  93. package/dist/plugins/index.js +4 -0
  94. package/dist/three/OBBHelper.js +1 -0
  95. package/dist/three/arrow.d.ts +2 -0
  96. package/dist/three/arrow.js +3 -1
  97. package/package.json +16 -4
  98. package/dist/components/Focus.svelte +0 -46
  99. package/dist/components/Focus.svelte.d.ts +0 -7
  100. package/dist/components/xr/OriginMarker.svelte +0 -151
  101. package/dist/components/xr/XRControllerSettings.svelte +0 -242
  102. package/dist/components/xr/XRControllerSettings.svelte.d.ts +0 -3
  103. package/dist/components/xr/frame-configure/Controllers.svelte +0 -6
  104. package/dist/components/xr/useOrigin.svelte.d.ts +0 -9
  105. package/dist/components/xr/useOrigin.svelte.js +0 -27
  106. package/dist/hooks/useSelection.svelte.d.ts +0 -33
  107. package/dist/hooks/useSelection.svelte.js +0 -94
  108. /package/dist/{components → plugins}/MeasureTool/MeasurePoint.svelte +0 -0
  109. /package/dist/{components → plugins}/MeasureTool/MeasurePoint.svelte.d.ts +0 -0
  110. /package/dist/{components → plugins}/MeasureTool/MeasureTool.svelte.d.ts +0 -0
  111. /package/dist/{components/xr → plugins/XR}/ArmTeleop.svelte.d.ts +0 -0
  112. /package/dist/{components/xr → plugins/XR}/BentPlaneGeometry.svelte +0 -0
  113. /package/dist/{components/xr → plugins/XR}/BentPlaneGeometry.svelte.d.ts +0 -0
  114. /package/dist/{components/xr → plugins/XR}/CameraFeed.svelte +0 -0
  115. /package/dist/{components/xr → plugins/XR}/CameraFeed.svelte.d.ts +0 -0
  116. /package/dist/{components/xr → plugins/XR}/JointLimitsWidget.svelte +0 -0
  117. /package/dist/{components/xr → plugins/XR}/JointLimitsWidget.svelte.d.ts +0 -0
  118. /package/dist/{components/xr → plugins/XR}/OriginMarker.svelte.d.ts +0 -0
  119. /package/dist/{components/xr → plugins/XR}/PointDistance.svelte +0 -0
  120. /package/dist/{components/xr → plugins/XR}/PointDistance.svelte.d.ts +0 -0
  121. /package/dist/{components/xr → plugins/XR}/XR.svelte.d.ts +0 -0
  122. /package/dist/{components/xr → plugins/XR}/XRConfigPanel.svelte +0 -0
  123. /package/dist/{components/xr → plugins/XR}/XRConfigPanel.svelte.d.ts +0 -0
  124. /package/dist/{components/xr → plugins/XR}/XRToast.svelte.d.ts +0 -0
  125. /package/dist/{components/xr/teleop → plugins/XR/frame-configure}/Controllers.svelte.d.ts +0 -0
  126. /package/dist/{components/xr → plugins/XR}/math.d.ts +0 -0
  127. /package/dist/{components/xr → plugins/XR}/math.js +0 -0
  128. /package/dist/{components/xr → plugins/XR}/teleop/Controllers.svelte +0 -0
  129. /package/dist/{components/xr → plugins/XR}/toasts.svelte.d.ts +0 -0
  130. /package/dist/{components/xr → plugins/XR}/toasts.svelte.js +0 -0
@@ -0,0 +1,413 @@
1
+ <script lang="ts">
2
+ import type { IntersectionEvent } from '@threlte/extras'
3
+ import type { XRTargetRaySpace } from 'three'
4
+
5
+ import { T, useTask, useThrelte } from '@threlte/core'
6
+ import { Controller, useController, useHeadset } from '@threlte/xr'
7
+ import { onDestroy } from 'svelte'
8
+ import { MathUtils, Vector3 } from 'three'
9
+ import { TransformControls } from 'three/addons/controls/TransformControls.js'
10
+ import { Text } from 'threlte-uikit'
11
+ import { Button, ButtonIcon, ButtonLabel, Panel } from 'threlte-uikit/horizon'
12
+ import { Icon, Locate, Move3d, Plus, Rotate3d, Scale3d } from 'threlte-uikit/lucide'
13
+
14
+ import { traits, useQuery, useTrait } from '../../../ecs'
15
+ import { FrameConfigUpdater } from '../../../FrameConfigUpdater.svelte'
16
+ import { useTransformControls } from '../../../hooks/useControls.svelte'
17
+ import { useFramelessComponents } from '../../../hooks/useFramelessComponents.svelte'
18
+ import { usePartConfig } from '../../../hooks/usePartConfig.svelte'
19
+ import { OrientationVector } from '../../../three/OrientationVector'
20
+
21
+ import { useOrigin } from '../useOrigin.svelte'
22
+ import WristDisplay from '../WristDisplay.svelte'
23
+
24
+ type Mode = 'translate' | 'rotate' | 'scale'
25
+ type Handedness = 'left' | 'right'
26
+
27
+ type BoxBase = { type: 'box'; x: number; y: number; z: number }
28
+ type SphereBase = { type: 'sphere'; r: number }
29
+ type CapsuleBase = { type: 'capsule'; r: number; l: number }
30
+ type GeometryBase = BoxBase | SphereBase | CapsuleBase
31
+
32
+ const { camera, renderer, scene } = useThrelte()
33
+
34
+ const partConfig = usePartConfig()
35
+ const transformControls = useTransformControls()
36
+ const framelessComponents = useFramelessComponents()
37
+ const leftController = useController('left')
38
+ const rightController = useController('right')
39
+ const headset = useHeadset()
40
+ const origin = useOrigin()
41
+ const selected = useQuery(traits.Selected)
42
+
43
+ const selectedEntity = $derived(selected.current[0])
44
+ const selectedObject3d = $derived(scene.getObjectByName(`${selectedEntity}`))
45
+ const framesAPI = useTrait(() => selectedEntity, traits.FramesAPI)
46
+
47
+ const resetForward = new Vector3()
48
+ const resetHead = new Vector3()
49
+ const resetOrigin = () => {
50
+ resetForward.set(0, 0, -1).applyQuaternion(headset.quaternion)
51
+ resetForward.z = 0
52
+ if (resetForward.lengthSq() < 1e-6) {
53
+ resetForward.set(0, 1, 0)
54
+ } else {
55
+ resetForward.normalize()
56
+ }
57
+ // headset.position/quaternion live in the composed XR reference space;
58
+ // convert into zUp before writing to origin.
59
+ origin.toZUpPos(resetHead, headset.position)
60
+ origin.toZUpDir(resetForward)
61
+ origin.set([resetHead.x + resetForward.x, resetHead.y + resetForward.y, 0], 0)
62
+ origin.commit()
63
+ }
64
+
65
+ const updater = new FrameConfigUpdater(partConfig.updateFrame, partConfig.deleteFrame)
66
+
67
+ const box = useTrait(() => selectedEntity, traits.Box)
68
+ const sphere = useTrait(() => selectedEntity, traits.Sphere)
69
+ const capsule = useTrait(() => selectedEntity, traits.Capsule)
70
+
71
+ const geometryType = $derived.by<'box' | 'sphere' | 'capsule' | undefined>(() => {
72
+ if (box.current) return 'box'
73
+ if (sphere.current) return 'sphere'
74
+ if (capsule.current) return 'capsule'
75
+ return undefined
76
+ })
77
+ const hasGeometry = $derived(geometryType !== undefined)
78
+
79
+ const controls = new TransformControls(camera.current, renderer.domElement)
80
+ controls.setSize(0.25)
81
+
82
+ const helper = controls.getHelper()
83
+ const raycaster = controls.getRaycaster()
84
+
85
+ let mode = $state<Mode>('translate')
86
+ let activeHandedness = $state.raw<Handedness>()
87
+ let attached = $state(false)
88
+ let addingFrame = $state(false)
89
+
90
+ // Snapshot of the geometry dims at drag start. Used in scale mode to compute
91
+ // `newDims = base * absoluteScaleFactor` each frame; cleared on drag end.
92
+ let geometryBase: GeometryBase | undefined
93
+
94
+ const getRay = (handedness: Handedness | undefined): XRTargetRaySpace | undefined => {
95
+ if (handedness === 'left') return leftController.current?.targetRay
96
+ if (handedness === 'right') return rightController.current?.targetRay
97
+ return undefined
98
+ }
99
+
100
+ const detectHandedness = (event: IntersectionEvent<PointerEvent>): Handedness | undefined => {
101
+ // nativeEvent is three's selectstart/selectend event whose `target` is the XRTargetRaySpace.
102
+ const target = (event.nativeEvent as { target?: unknown } | undefined)?.target
103
+ if (target && target === leftController.current?.targetRay) return 'left'
104
+ if (target && target === rightController.current?.targetRay) return 'right'
105
+ return undefined
106
+ }
107
+
108
+ $effect(() => {
109
+ controls.setMode(mode)
110
+ })
111
+
112
+ // Constrain which axes the gizmo exposes while in scale mode:
113
+ // box: all three (width/height/depth)
114
+ // capsule: X (radius) + Z (length); hide Y since capsules are radially symmetric
115
+ // sphere: X only; a single handle drives the radius
116
+ // Translate/rotate always show all three.
117
+ $effect(() => {
118
+ if (mode !== 'scale') {
119
+ controls.showX = true
120
+ controls.showY = true
121
+ controls.showZ = true
122
+ return
123
+ }
124
+ switch (geometryType) {
125
+ case 'box': {
126
+ controls.showX = true
127
+ controls.showY = true
128
+ controls.showZ = true
129
+ break
130
+ }
131
+ case 'capsule': {
132
+ controls.showX = true
133
+ controls.showY = false
134
+ controls.showZ = true
135
+ break
136
+ }
137
+ case 'sphere': {
138
+ controls.showX = true
139
+ controls.showY = false
140
+ controls.showZ = false
141
+ break
142
+ }
143
+ default: {
144
+ controls.showX = false
145
+ controls.showY = false
146
+ controls.showZ = false
147
+ }
148
+ }
149
+ })
150
+
151
+ // selectedObject3d resolves to the named Mesh from Mesh.svelte; the Group that
152
+ // carries the frame's pose is its parent (set up in Frame.svelte).
153
+ $effect(() => {
154
+ if (!framesAPI.current || !partConfig.hasEditPermissions) {
155
+ controls.detach()
156
+ attached = false
157
+ return
158
+ }
159
+
160
+ const target = selectedObject3d?.parent
161
+
162
+ if (target && target !== scene) {
163
+ controls.attach(target)
164
+ attached = true
165
+ } else {
166
+ controls.detach()
167
+ attached = false
168
+ }
169
+ })
170
+
171
+ // `Raycaster.setFromXRController` reads `ray.matrixWorld`. When hand-tracking
172
+ // is active in the session, `@threlte/xr` does not attach the controller's
173
+ // targetRay to the scene graph (see `Controller.svelte`'s
174
+ // `{#if !isHandTracking.current}`), so scene traversal never refreshes its
175
+ // matrixWorld. We have to call `updateMatrixWorld()` ourselves or the ray
176
+ // aims wherever the controller was last attached.
177
+ const setRaycasterFromRay = (ray: XRTargetRaySpace) => {
178
+ ray.updateMatrixWorld(true)
179
+ raycaster.setFromXRController(ray)
180
+ }
181
+
182
+ const onPointerDown = (event: IntersectionEvent<PointerEvent>) => {
183
+ activeHandedness = detectHandedness(event)
184
+ const ray = getRay(activeHandedness)
185
+ if (!ray) return
186
+ setRaycasterFromRay(ray)
187
+ controls.pointerHover(null)
188
+ controls.pointerDown(null)
189
+ }
190
+
191
+ const onPointerUp = () => {
192
+ const ray = getRay(activeHandedness)
193
+ if (ray) setRaycasterFromRay(ray)
194
+ controls.pointerUp(null)
195
+ activeHandedness = undefined
196
+ }
197
+
198
+ // Per-frame: while dragging, keep pointerMove firing even if the active ray
199
+ // wobbles off the gizmo. Otherwise, drive pointerHover for axis highlight.
200
+ useTask(() => {
201
+ if (!controls.object) return
202
+
203
+ if (controls.dragging) {
204
+ const ray = getRay(activeHandedness)
205
+ if (!ray) return
206
+ setRaycasterFromRay(ray)
207
+ controls.pointerMove(null)
208
+ return
209
+ }
210
+
211
+ const rays = [rightController.current?.targetRay, leftController.current?.targetRay]
212
+ for (const ray of rays) {
213
+ if (!ray) continue
214
+ setRaycasterFromRay(ray)
215
+ controls.pointerHover(null)
216
+ if (controls.axis !== null) return
217
+ }
218
+ })
219
+
220
+ const ov = new OrientationVector()
221
+
222
+ // Snapshot the current geometry dims when a scale-mode drag begins so every
223
+ // frame can compute `newDims = base * controls.object.scale` from a stable
224
+ // baseline, and clear the snapshot on drag end.
225
+ controls.addEventListener('mouseDown', () => {
226
+ transformControls.setActive(true)
227
+ if (mode !== 'scale') return
228
+ if (box.current) {
229
+ geometryBase = { type: 'box', x: box.current.x, y: box.current.y, z: box.current.z }
230
+ } else if (sphere.current) {
231
+ geometryBase = { type: 'sphere', r: sphere.current.r }
232
+ } else if (capsule.current) {
233
+ geometryBase = { type: 'capsule', r: capsule.current.r, l: capsule.current.l }
234
+ } else {
235
+ geometryBase = undefined
236
+ }
237
+ })
238
+
239
+ controls.addEventListener('mouseUp', () => {
240
+ transformControls.setActive(false)
241
+ geometryBase = undefined
242
+ })
243
+
244
+ controls.addEventListener('objectChange', () => {
245
+ const target = controls.object
246
+ if (!selectedEntity || !target) return
247
+
248
+ if (mode === 'translate') {
249
+ // three.js scene is in meters; FrameConfigUpdater stores mm.
250
+ updater.updateLocalPosition(selectedEntity, {
251
+ x: target.position.x * 1000,
252
+ y: target.position.y * 1000,
253
+ z: target.position.z * 1000,
254
+ })
255
+ } else if (mode === 'rotate') {
256
+ ov.setFromQuaternion(target.quaternion)
257
+ updater.updateLocalOrientation(selectedEntity, {
258
+ oX: ov.x,
259
+ oY: ov.y,
260
+ oZ: ov.z,
261
+ theta: MathUtils.radToDeg(ov.th),
262
+ })
263
+ } else if (geometryBase) {
264
+ // Scale mode: `target.scale` is the absolute factor since drag start
265
+ // because TC's internal `_scaleStart` was `(1,1,1)`. Apply it to the
266
+ // baseline dims, write back through the trait so the mesh regenerates
267
+ // at the new size, then snap the group's scale back to 1 so the next
268
+ // frame's regenerated geometry isn't re-scaled visually.
269
+ const s = target.scale
270
+ if (geometryBase.type === 'box') {
271
+ updater.updateGeometry(selectedEntity, {
272
+ type: 'box',
273
+ x: geometryBase.x * s.x,
274
+ y: geometryBase.y * s.y,
275
+ z: geometryBase.z * s.z,
276
+ })
277
+ } else if (geometryBase.type === 'sphere') {
278
+ updater.updateGeometry(selectedEntity, {
279
+ type: 'sphere',
280
+ r: geometryBase.r * s.x,
281
+ })
282
+ } else {
283
+ updater.updateGeometry(selectedEntity, {
284
+ type: 'capsule',
285
+ r: geometryBase.r * s.x,
286
+ l: geometryBase.l * s.z,
287
+ })
288
+ }
289
+ target.scale.set(1, 1, 1)
290
+ }
291
+ })
292
+
293
+ onDestroy(() => {
294
+ transformControls.setActive(false)
295
+ controls.detach()
296
+ controls.dispose()
297
+ })
298
+ </script>
299
+
300
+ <Controller left />
301
+ <Controller right />
302
+
303
+ <T
304
+ is={helper}
305
+ onpointerdown={onPointerDown as never}
306
+ onpointerup={onPointerUp as never}
307
+ />
308
+
309
+ {#if partConfig.hasEditPermissions}
310
+ <WristDisplay position={[0, 0.005, 0.04]}>
311
+ <Panel
312
+ flexDirection="row"
313
+ padding={8}
314
+ gap={8}
315
+ backgroundColor="#111"
316
+ borderRadius={16}
317
+ >
318
+ <Button
319
+ icon
320
+ size="sm"
321
+ variant="tertiary"
322
+ onclick={resetOrigin}
323
+ >
324
+ <ButtonIcon>
325
+ <Icon is={Locate} />
326
+ </ButtonIcon>
327
+ </Button>
328
+ <Button
329
+ icon
330
+ size="sm"
331
+ variant={addingFrame ? 'primary' : 'tertiary'}
332
+ onclick={() => (addingFrame = !addingFrame)}
333
+ >
334
+ <ButtonIcon>
335
+ <Icon is={Plus} />
336
+ </ButtonIcon>
337
+ </Button>
338
+ {#if attached}
339
+ <Button
340
+ icon
341
+ size="sm"
342
+ variant={mode === 'translate' ? 'primary' : 'tertiary'}
343
+ onclick={() => (mode = 'translate')}
344
+ >
345
+ <ButtonIcon>
346
+ <Icon is={Move3d} />
347
+ </ButtonIcon>
348
+ </Button>
349
+ <Button
350
+ icon
351
+ size="sm"
352
+ variant={mode === 'rotate' ? 'primary' : 'tertiary'}
353
+ onclick={() => (mode = 'rotate')}
354
+ >
355
+ <ButtonIcon>
356
+ <Icon is={Rotate3d} />
357
+ </ButtonIcon>
358
+ </Button>
359
+ <Button
360
+ icon
361
+ size="sm"
362
+ disabled={!hasGeometry}
363
+ variant={mode === 'scale' ? 'primary' : 'tertiary'}
364
+ onclick={() => (mode = 'scale')}
365
+ >
366
+ <ButtonIcon>
367
+ <Icon is={Scale3d} />
368
+ </ButtonIcon>
369
+ </Button>
370
+ {/if}
371
+ </Panel>
372
+ </WristDisplay>
373
+
374
+ {#if addingFrame}
375
+ <WristDisplay position={[0, 0.005, 0.14]}>
376
+ <Panel
377
+ flexDirection="column"
378
+ padding={12}
379
+ gap={6}
380
+ backgroundColor="#111"
381
+ borderRadius={16}
382
+ minWidth={320}
383
+ >
384
+ {#if framelessComponents.current.length > 0}
385
+ {#each framelessComponents.current as component (component)}
386
+ <Button
387
+ size="sm"
388
+ variant="tertiary"
389
+ onclick={() => {
390
+ partConfig.createFrame(component)
391
+ addingFrame = false
392
+ }}
393
+ >
394
+ <ButtonLabel>
395
+ <Text
396
+ text={component}
397
+ fontSize={14}
398
+ color="#ffffff"
399
+ />
400
+ </ButtonLabel>
401
+ </Button>
402
+ {/each}
403
+ {:else}
404
+ <Text
405
+ text="No components without frames"
406
+ fontSize={14}
407
+ color="#ffffff"
408
+ />
409
+ {/if}
410
+ </Panel>
411
+ </WristDisplay>
412
+ {/if}
413
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const Controllers: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Controllers = ReturnType<typeof Controllers>;
3
+ export default Controllers;
@@ -3,6 +3,10 @@ interface Context {
3
3
  createAnchor: (position: Vector3, orientation: Quaternion) => Promise<XRAnchor> | undefined;
4
4
  bindAnchorObject: (anchor: XRAnchor, object: Object3D) => void;
5
5
  unbindAnchorObject: (anchor: XRAnchor) => void;
6
+ persist: (anchor: XRAnchor) => Promise<string | undefined>;
7
+ restore: (uuid: string) => Promise<XRAnchor | undefined>;
8
+ remove: (uuid: string) => Promise<void>;
9
+ getAnchorPose: (anchor: XRAnchor) => XRPose | undefined;
6
10
  }
7
11
  export declare const provideAnchors: () => void;
8
12
  export declare const useAnchors: () => Context;
@@ -23,6 +23,24 @@ export const provideAnchors = () => {
23
23
  const unbindAnchorObject = (anchor) => {
24
24
  map.delete(anchor);
25
25
  };
26
+ const persist = async (anchor) => {
27
+ return anchor.requestPersistentHandle?.();
28
+ };
29
+ const restore = async (uuid) => {
30
+ const session = renderer.xr.getSession();
31
+ return session?.restorePersistentAnchor?.(uuid);
32
+ };
33
+ const remove = async (uuid) => {
34
+ const session = renderer.xr.getSession();
35
+ await session?.deletePersistentAnchor?.(uuid);
36
+ };
37
+ const getAnchorPose = (anchor) => {
38
+ const space = renderer.xr.getReferenceSpace();
39
+ const frame = renderer.xr.getFrame();
40
+ if (!space || !frame)
41
+ return;
42
+ return frame.getPose(anchor.anchorSpace, space);
43
+ };
26
44
  useTask(() => {
27
45
  const space = renderer.xr.getReferenceSpace();
28
46
  const frame = renderer.xr.getFrame();
@@ -48,6 +66,10 @@ export const provideAnchors = () => {
48
66
  createAnchor,
49
67
  bindAnchorObject,
50
68
  unbindAnchorObject,
69
+ persist,
70
+ restore,
71
+ remove,
72
+ getAnchorPose,
51
73
  });
52
74
  };
53
75
  export const useAnchors = () => {
@@ -0,0 +1,24 @@
1
+ import type { Vector3, Vector3Like, Vector3Tuple } from 'three';
2
+ interface Context {
3
+ position: Vector3Tuple;
4
+ rotation: number;
5
+ set: (pos?: Vector3Tuple, rot?: number) => void;
6
+ /** Request persistence of the current origin (creates/updates the XR anchor). */
7
+ commit: () => void;
8
+ /** Called once by OriginMarker to wire the commit implementation. */
9
+ registerCommit: (fn: () => void) => void;
10
+ /**
11
+ * Convert a position reported in the composed XR reference space
12
+ * (zUp offset by the current origin) into the zUp frame that
13
+ * `position`/`rotation` are stored in. Writes into `out` and returns it.
14
+ */
15
+ toZUpPos: (out: Vector3, p: Vector3Like) => Vector3;
16
+ /**
17
+ * Rotate a direction from the composed XR reference space into zUp
18
+ * (no translation). Mutates `v` in place and returns it.
19
+ */
20
+ toZUpDir: (v: Vector3) => Vector3;
21
+ }
22
+ export declare const provideOrigin: () => Context;
23
+ export declare const useOrigin: () => Context;
24
+ export {};
@@ -0,0 +1,50 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const key = Symbol('origin-context');
3
+ export const provideOrigin = () => {
4
+ const position = $state([0, 0, 0]);
5
+ let rotation = $state(0);
6
+ let commitFn = () => { };
7
+ const context = {
8
+ get position() {
9
+ return position;
10
+ },
11
+ get rotation() {
12
+ return rotation;
13
+ },
14
+ set(pos, rot) {
15
+ if (pos) {
16
+ position[0] = pos[0];
17
+ position[1] = pos[1];
18
+ position[2] = pos[2];
19
+ }
20
+ if (rot !== undefined) {
21
+ rotation = rot;
22
+ }
23
+ },
24
+ commit() {
25
+ commitFn();
26
+ },
27
+ registerCommit(fn) {
28
+ commitFn = fn;
29
+ },
30
+ toZUpPos(out, p) {
31
+ const cos = Math.cos(rotation);
32
+ const sin = Math.sin(rotation);
33
+ out.set(position[0] + cos * p.x - sin * p.y, position[1] + sin * p.x + cos * p.y, position[2] + p.z);
34
+ return out;
35
+ },
36
+ toZUpDir(v) {
37
+ const cos = Math.cos(rotation);
38
+ const sin = Math.sin(rotation);
39
+ const x = v.x;
40
+ v.x = cos * x - sin * v.y;
41
+ v.y = sin * x + cos * v.y;
42
+ return v;
43
+ },
44
+ };
45
+ setContext(key, context);
46
+ return context;
47
+ };
48
+ export const useOrigin = () => {
49
+ return getContext(key);
50
+ };
@@ -2,6 +2,10 @@ export { default as SelectionTool } from './Selection/SelectionTool.svelte';
2
2
  export * as selectionTraits from './Selection/traits';
3
3
  export * as selectionRelations from './Selection/relations';
4
4
  export { useSelectionPlugin } from './Selection/useSelectionPlugin.svelte';
5
+ export { default as MeasureTool } from './MeasureTool/MeasureTool.svelte';
5
6
  export { default as DrawService } from './DrawService/DrawService.svelte';
6
7
  export { default as Skybox } from './Skybox/Skybox.svelte';
7
8
  export { default as Debug } from './Debug/Debug.svelte';
9
+ export { default as Focus } from './Focus/Focus.svelte';
10
+ export { default as XR } from './XR/XR.svelte';
11
+ export { default as XRSettings } from './XR/XRSettings.svelte';
@@ -3,9 +3,13 @@ export { default as SelectionTool } from './Selection/SelectionTool.svelte';
3
3
  export * as selectionTraits from './Selection/traits';
4
4
  export * as selectionRelations from './Selection/relations';
5
5
  export { useSelectionPlugin } from './Selection/useSelectionPlugin.svelte';
6
+ export { default as MeasureTool } from './MeasureTool/MeasureTool.svelte';
6
7
  // DrawService
7
8
  export { default as DrawService } from './DrawService/DrawService.svelte';
8
9
  // Skybox
9
10
  export { default as Skybox } from './Skybox/Skybox.svelte';
10
11
  // Debug
11
12
  export { default as Debug } from './Debug/Debug.svelte';
13
+ export { default as Focus } from './Focus/Focus.svelte';
14
+ export { default as XR } from './XR/XR.svelte';
15
+ export { default as XRSettings } from './XR/XRSettings.svelte';
@@ -54,6 +54,7 @@ export class OBBHelper extends LineSegments2 {
54
54
  });
55
55
  super(geometry, material);
56
56
  this.matrixAutoUpdate = false;
57
+ this.matrixWorldAutoUpdate = false;
57
58
  this.frustumCulled = false;
58
59
  this.renderOrder = 999;
59
60
  }
@@ -1,4 +1,6 @@
1
1
  import { type BufferGeometry } from 'three';
2
+ /** Total length of the geometry produced by `createArrowGeometry`, in meters. */
3
+ export declare const ARROW_LENGTH = 0.1;
2
4
  /**
3
5
  * Returns one merged geometry for an arrow (box tail + cone head)
4
6
  *
@@ -1,12 +1,14 @@
1
1
  import { BoxGeometry, ConeGeometry } from 'three';
2
2
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
+ /** Total length of the geometry produced by `createArrowGeometry`, in meters. */
4
+ export const ARROW_LENGTH = 0.1;
3
5
  /**
4
6
  * Returns one merged geometry for an arrow (box tail + cone head)
5
7
  *
6
8
  * Arrow points along +Y with its base at y = 0
7
9
  */
8
10
  export const createArrowGeometry = () => {
9
- const length = 0.1;
11
+ const length = ARROW_LENGTH;
10
12
  const headLength = length * 0.3;
11
13
  const headWidth = headLength * 0.3;
12
14
  const tailLength = length - headLength;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.31.0",
3
+ "version": "1.33.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,6 +9,8 @@
9
9
  "@ag-grid-community/core": "32.3.9",
10
10
  "@ag-grid-community/styles": "32.3.9",
11
11
  "@changesets/cli": "2.29.6",
12
+ "@connectrpc/connect": "1.7.0",
13
+ "@connectrpc/connect-web": "1.7.0",
12
14
  "@dimforge/rapier3d-compat": "0.18.2",
13
15
  "@eslint/compat": "2.0.2",
14
16
  "@eslint/js": "10.0.1",
@@ -52,6 +54,7 @@
52
54
  "@zag-js/tree-view": "1.22.1",
53
55
  "camera-controls": "3.1.0",
54
56
  "concurrently": "^9.2.1",
57
+ "earcut": "^3.0.2",
55
58
  "esbuild": "^0.27.3",
56
59
  "eslint": "10.0.2",
57
60
  "eslint-config-prettier": "10.1.8",
@@ -87,6 +90,8 @@
87
90
  "@ag-grid-community/client-side-row-model": ">=32.3.0",
88
91
  "@ag-grid-community/core": ">=32.3.0",
89
92
  "@ag-grid-community/styles": ">=32.3.0",
93
+ "@connectrpc/connect": ">=1",
94
+ "@connectrpc/connect-web": ">=1",
90
95
  "@dimforge/rapier3d-compat": ">=0.17",
91
96
  "@tanstack/svelte-query-devtools": ">=6",
92
97
  "@threlte/core": ">=8",
@@ -105,6 +110,7 @@
105
110
  "@zag-js/toggle-group": ">=1",
106
111
  "@zag-js/tree-view": ">=1",
107
112
  "camera-controls": ">=3",
113
+ "earcut": ">=3",
108
114
  "idb-keyval": ">=6",
109
115
  "lucide-svelte": ">=0.511",
110
116
  "runed": ">=0.28",
@@ -113,8 +119,17 @@
113
119
  "svelte-virtuallists": ">=1"
114
120
  },
115
121
  "peerDependenciesMeta": {
122
+ "@connectrpc/connect": {
123
+ "optional": true
124
+ },
125
+ "@connectrpc/connect-web": {
126
+ "optional": true
127
+ },
116
128
  "@tanstack/svelte-query-devtools": {
117
129
  "optional": true
130
+ },
131
+ "earcut": {
132
+ "optional": true
118
133
  }
119
134
  },
120
135
  "engines": {
@@ -147,10 +162,7 @@
147
162
  ],
148
163
  "dependencies": {
149
164
  "@bufbuild/protobuf": "1.10.1",
150
- "@connectrpc/connect": "1.7.0",
151
- "@connectrpc/connect-web": "1.7.0",
152
165
  "@neodrag/svelte": "^2.3.3",
153
- "earcut": "^3.0.2",
154
166
  "filtrex": "^3.1.0",
155
167
  "koota": "0.6.5",
156
168
  "lodash-es": "4.18.1",