@viamrobotics/motion-tools 0.9.4 → 0.9.5

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.
@@ -1,4 +1,4 @@
1
- import type { Geometry, Pose } from '@viamrobotics/sdk';
1
+ import type { Geometry, Pose, TransformWithUUID } from '@viamrobotics/sdk';
2
2
  import { BatchedMesh, Box3, Object3D, Vector3, type ColorRepresentation } from 'three';
3
3
  export type PointsGeometry = {
4
4
  case: 'points';
@@ -34,3 +34,22 @@ export declare class WorldObject<T extends Geometries = Geometries> {
34
34
  metadata: Metadata;
35
35
  constructor(name: string, pose?: Pose, parent?: string, geometry?: T, metadata?: Metadata);
36
36
  }
37
+ export declare const fromTransform: (transform: TransformWithUUID) => WorldObject<{
38
+ case: undefined;
39
+ value?: undefined;
40
+ } | {
41
+ case: "sphere";
42
+ value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Sphere>;
43
+ } | {
44
+ case: "box";
45
+ value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").RectangularPrism>;
46
+ } | {
47
+ case: "capsule";
48
+ value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Capsule>;
49
+ } | {
50
+ case: "mesh";
51
+ value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Mesh>;
52
+ } | {
53
+ case: "pointcloud";
54
+ value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").PointCloud>;
55
+ }>;
@@ -16,3 +16,8 @@ export class WorldObject {
16
16
  this.metadata = metadata ?? {};
17
17
  }
18
18
  }
19
+ export const fromTransform = (transform) => {
20
+ const metadata = { ...transform.metadata?.fields };
21
+ const worldObject = new WorldObject(transform.referenceFrame, transform.poseInObserverFrame?.pose, transform.poseInObserverFrame?.referenceFrame, transform.physicalObject?.geometryType, metadata);
22
+ return worldObject;
23
+ };
@@ -66,6 +66,7 @@
66
66
  is={line}
67
67
  {...rest}
68
68
  raycast={() => null}
69
+ bvh={{ enabled: false }}
69
70
  >
70
71
  <T is={geometry} />
71
72
  <T
@@ -1,44 +1,59 @@
1
- <script
2
- module
3
- lang="ts"
4
- >
1
+ <script lang="ts">
5
2
  import { T, type Props as ThrelteProps } from '@threlte/core'
6
- import { CanvasTexture, type Sprite, type ColorRepresentation } from 'three'
7
-
8
- const size = 128
9
- const canvas = new OffscreenCanvas(size, size)
10
- const ctx = canvas.getContext('2d')
3
+ import type { ColorRepresentation, Vector3Tuple, Group } from 'three'
4
+ import { HTML } from '@threlte/extras'
11
5
 
12
- if (ctx) {
13
- ctx.clearRect(0, 0, size, size)
14
- ctx.beginPath()
15
- ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2)
16
- ctx.fillStyle = 'white'
17
- ctx.fill()
18
- }
19
-
20
- const map = new CanvasTexture(canvas)
21
- </script>
22
-
23
- <script lang="ts">
24
- interface Props extends ThrelteProps<typeof Sprite> {
6
+ interface Props extends ThrelteProps<typeof Group> {
7
+ position: Vector3Tuple
25
8
  color?: ColorRepresentation
26
9
  opacity?: number
27
10
  }
28
11
 
29
- let { color, opacity = 1, ref = $bindable(), ...rest }: Props = $props()
12
+ let { position, color, opacity = 1, ref = $bindable(), ...rest }: Props = $props()
30
13
  </script>
31
14
 
32
- <T.Sprite
15
+ <T.Group
33
16
  bind:ref
34
- scale={0.05}
35
17
  {...rest}
18
+ {position}
36
19
  >
37
- <T.SpriteMaterial
38
- transparent
39
- depthTest={false}
40
- {map}
41
- {opacity}
42
- color={color ?? 'black'}
43
- />
44
- </T.Sprite>
20
+ <T.Mesh
21
+ bvh={{ enabled: false }}
22
+ raycast={() => null}
23
+ scale={0.01}
24
+ renderOrder={1}
25
+ >
26
+ <T.SphereGeometry />
27
+ <T.MeshBasicMaterial
28
+ color={color ?? 'black'}
29
+ transparent
30
+ depthTest={false}
31
+ {opacity}
32
+ />
33
+ </T.Mesh>
34
+
35
+ <HTML
36
+ class="pointer-events-none mb-2 w-16 -translate-x-1/2 -translate-y-[calc(100%+10px)] border border-black bg-white px-1 py-0.5 text-xs text-wrap"
37
+ >
38
+ <div class="flex justify-between">
39
+ <span class="text-subtle-2">x</span>
40
+ <div>
41
+ {position[0].toFixed(2)}<span class="text-subtle-2">m</span>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="flex justify-between">
46
+ <span class="text-subtle-2">y</span>
47
+ <div>
48
+ {position[1].toFixed(2)}<span class="text-subtle-2">m</span>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="flex justify-between">
53
+ <span class="text-subtle-2">z</span>
54
+ <div>
55
+ {position[2].toFixed(2)}<span class="text-subtle-2">m</span>
56
+ </div>
57
+ </div>
58
+ </HTML>
59
+ </T.Group>
@@ -1,6 +1,7 @@
1
1
  import { type Props as ThrelteProps } from '@threlte/core';
2
- import { type Sprite, type ColorRepresentation } from 'three';
3
- interface Props extends ThrelteProps<typeof Sprite> {
2
+ import type { ColorRepresentation, Vector3Tuple, Group } from 'three';
3
+ interface Props extends ThrelteProps<typeof Group> {
4
+ position: Vector3Tuple;
4
5
  color?: ColorRepresentation;
5
6
  opacity?: number;
6
7
  }
@@ -2,7 +2,10 @@
2
2
  import { T } from '@threlte/core'
3
3
  import { TrackballControls, Gizmo } from '@threlte/extras'
4
4
  import { Box3, type Object3D, Vector3 } from 'three'
5
+ import { TrackballControls as ThreeTrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
5
6
  import Camera from './Camera.svelte'
7
+ import Portal from './portal/Portal.svelte'
8
+ import Button from './dashboard/Button.svelte'
6
9
 
7
10
  interface Props {
8
11
  object3d: Object3D
@@ -16,6 +19,8 @@
16
19
  let center = $state.raw<[number, number, number]>([0, 0, 0])
17
20
  let size = $state.raw<[number, number, number]>([0, 0, 0])
18
21
 
22
+ let controls = $state.raw<ThreeTrackballControls>()
23
+
19
24
  $effect.pre(() => {
20
25
  box.setFromObject(object3d)
21
26
  size = box.getSize(vec).toArray()
@@ -23,11 +28,39 @@
23
28
  })
24
29
  </script>
25
30
 
31
+ <Portal id="dashboard">
32
+ <fieldset>
33
+ <Button
34
+ active
35
+ icon="camera-outline"
36
+ description="Reset camera"
37
+ onclick={() => {
38
+ controls?.reset()
39
+ }}
40
+ />
41
+ </fieldset>
42
+ </Portal>
43
+
26
44
  <Camera position={[size[0] + 1, size[0] + 1, size[0] + 1]}>
27
- <TrackballControls target={center}>
45
+ <TrackballControls
46
+ bind:ref={controls}
47
+ target={center}
48
+ >
28
49
  <Gizmo />
29
50
  </TrackballControls>
30
51
  </Camera>
31
52
 
32
- <T is={object3d} />
33
- <T.BoxHelper args={[object3d, 'red']} />
53
+ <T
54
+ is={object3d}
55
+ bvh={{
56
+ enabled: object3d.type === 'Points',
57
+ maxDepth: 40,
58
+ maxLeafTris: 20,
59
+ }}
60
+ />
61
+
62
+ <T.BoxHelper
63
+ args={[object3d, 'red']}
64
+ bvh={{ enabled: false }}
65
+ raycast={() => null}
66
+ />
@@ -62,6 +62,7 @@
62
62
  {name}
63
63
  {uuid}
64
64
  {...rest}
65
+ bvh={{ enabled: false }}
65
66
  >
66
67
  {#if geometry?.case === 'mesh'}
67
68
  {@const mesh = geometry.value.mesh as Uint8Array<ArrayBuffer>}
@@ -112,7 +113,10 @@
112
113
  />
113
114
 
114
115
  {#if geo}
115
- <T.LineSegments raycast={() => null}>
116
+ <T.LineSegments
117
+ raycast={() => null}
118
+ bvh={{ enabled: false }}
119
+ >
116
120
  <T.EdgesGeometry args={[geo, 0]} />
117
121
  <T.LineBasicMaterial color={darkenColor(color, 10)} />
118
122
  </T.LineSegments>
@@ -1,41 +1,39 @@
1
1
  <script lang="ts">
2
2
  import { untrack } from 'svelte'
3
- import { Raycaster, Vector2, Vector3, type Intersection } from 'three'
4
- import { T, useThrelte, useTask } from '@threlte/core'
5
- import { HTML, MeshLineGeometry, MeshLineMaterial, useInteractivity } from '@threlte/extras'
3
+ import { Vector3, type Intersection } from 'three'
4
+ import { T } from '@threlte/core'
5
+ import { HTML, MeshLineGeometry, MeshLineMaterial } from '@threlte/extras'
6
6
  import { useSettings } from '../hooks/useSettings.svelte'
7
7
  import Button from './dashboard/Button.svelte'
8
8
  import Portal from './portal/Portal.svelte'
9
9
  import DotSprite from './DotSprite.svelte'
10
+ import { useMouseRaycaster } from '../hooks/useMouseRaycaster.svelte'
11
+ import { useFocused } from '../hooks/useSelection.svelte'
10
12
 
13
+ const focus = useFocused()
11
14
  const settings = useSettings()
12
- const { camera } = useThrelte()
13
- const interactivity = useInteractivity()
14
- const raycaster = new Raycaster()
15
15
 
16
16
  const htmlPosition = new Vector3()
17
- const pointerDown = new Vector2()
18
- const pointerUp = new Vector2()
19
17
 
20
18
  let step: 'idle' | 'p1' | 'p2' = 'idle'
21
19
 
22
- let intersection: Intersection | undefined
20
+ let intersection = $state.raw<Intersection>()
23
21
  let p1 = $state.raw<Vector3>()
24
22
  let p2 = $state.raw<Vector3>()
25
23
 
26
24
  const enabled = $derived(settings.current.enableMeasure)
27
25
 
28
- const onpointerdown = (event: PointerEvent) => {
29
- pointerDown.set(event.clientX, event.clientY)
30
- }
31
-
32
- const onpointerup = (event: PointerEvent) => {
33
- pointerUp.set(event.clientX, event.clientY)
26
+ const { onclick, onmove, raycaster } = useMouseRaycaster(() => ({
27
+ enabled,
28
+ }))
29
+ raycaster.firstHitOnly = true
30
+ raycaster.params.Points.threshold = 0.005
34
31
 
35
- if (pointerDown.distanceToSquared(pointerUp) > 0.1) {
36
- return
37
- }
32
+ onmove((event) => {
33
+ intersection = event.intersections[0]
34
+ })
38
35
 
36
+ onclick(() => {
39
37
  if (step === 'idle' && intersection) {
40
38
  p1 = intersection.point.clone()
41
39
  step = 'p1'
@@ -47,38 +45,18 @@
47
45
  p2 = undefined
48
46
  step = 'idle'
49
47
  }
50
- }
51
-
52
- const { start, stop } = useTask(
53
- () => {
54
- if (interactivity.hovered.size === 0) {
55
- return
56
- }
57
-
58
- for (const [, event] of interactivity.hovered) {
59
- raycaster.setFromCamera(interactivity.pointer.current, camera.current)
60
- intersection = raycaster.intersectObject(event.object)[0]
61
- }
62
- },
63
- { autoStart: false }
64
- )
65
-
66
- $effect(() => {
67
- if (!enabled) {
68
- untrack(() => {
69
- p1 = undefined
70
- p2 = undefined
71
- step = 'idle'
72
- })
73
- }
74
48
  })
75
49
 
50
+ const clear = () => {
51
+ p1 = undefined
52
+ p2 = undefined
53
+ step = 'idle'
54
+ }
55
+
76
56
  $effect(() => {
77
- if (enabled) {
78
- start()
79
- } else {
80
- stop()
81
- }
57
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
58
+ ;(focus.current, enabled)
59
+ untrack(() => clear())
82
60
  })
83
61
  </script>
84
62
 
@@ -96,12 +74,11 @@
96
74
  </fieldset>
97
75
  </Portal>
98
76
 
99
- <svelte:window
100
- onpointerdown={enabled ? onpointerdown : undefined}
101
- onpointerup={enabled ? onpointerup : undefined}
102
- />
103
-
104
77
  {#if enabled}
78
+ {#if intersection}
79
+ <DotSprite position={intersection?.point.toArray()} />
80
+ {/if}
81
+
105
82
  {#if p1}
106
83
  <DotSprite position={p1.toArray()} />
107
84
  {/if}
@@ -111,12 +88,18 @@
111
88
  {/if}
112
89
 
113
90
  {#if p1 && p2}
114
- <T.Mesh>
91
+ <T.Mesh
92
+ raycast={() => null}
93
+ bvh={{ enabled: false }}
94
+ renderOrder={1}
95
+ >
115
96
  <MeshLineGeometry points={[p1, p2]} />
116
97
  <MeshLineMaterial
117
- width={0.015}
98
+ width={2.5}
118
99
  depthTest={false}
119
100
  color="black"
101
+ attenuate={false}
102
+ transparent
120
103
  />
121
104
  </T.Mesh>
122
105
  <HTML
@@ -124,7 +107,7 @@
124
107
  position={htmlPosition.lerpVectors(p1, p2, 0.5).toArray()}
125
108
  >
126
109
  <div class="border border-black bg-white px-1 py-0.5 text-xs">
127
- {p1.distanceTo(p2).toFixed(2)}m
110
+ {p1.distanceTo(p2).toFixed(2)}<span class="text-subtle-2">m</span>
128
111
  </div>
129
112
  </HTML>
130
113
  {/if}
@@ -6,11 +6,9 @@
6
6
  PointsMaterial,
7
7
  OrthographicCamera,
8
8
  } from 'three'
9
-
10
9
  import { T, useTask, useThrelte } from '@threlte/core'
11
10
  import type { WorldObject } from '../WorldObject'
12
11
  import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
13
- import { meshBounds } from '@threlte/extras'
14
12
  import { poseToObject3d } from '../transform'
15
13
  import { useSettings } from '../hooks/useSettings.svelte'
16
14
  import type { Snippet } from 'svelte'
@@ -85,8 +83,8 @@
85
83
  is={points}
86
84
  name={object.name}
87
85
  uuid={object.uuid}
88
- raycast={meshBounds}
89
86
  {...events}
87
+ bvh={{ maxDepth: 40, maxLeafTris: 20 }}
90
88
  >
91
89
  <T is={geometry} />
92
90
  <T is={material} />
@@ -1,19 +1,25 @@
1
1
  <script lang="ts">
2
- import { BackSide, Vector3 } from 'three'
2
+ import { BackSide, Mesh, Vector3 } from 'three'
3
3
  import { T, useThrelte } from '@threlte/core'
4
4
  import { MeshDiscardMaterial } from '@threlte/extras'
5
5
  import { useSelected } from '../hooks/useSelection.svelte'
6
6
  import { useTransformControls } from '../hooks/useControls.svelte'
7
+ import { useSettings } from '../hooks/useSettings.svelte'
7
8
 
8
9
  const { camera } = useThrelte()
10
+ const settings = useSettings()
9
11
  const selected = useSelected()
10
12
  const transformControls = useTransformControls()
11
13
  const cameraDown = new Vector3()
12
14
 
15
+ const enabled = $derived(!settings.current.enableMeasure)
16
+
13
17
  const size = 1_000
14
18
  </script>
15
19
 
16
20
  <T.Mesh
21
+ raycast={enabled ? Mesh.prototype.raycast : () => null}
22
+ bvh={{ enabled: false }}
17
23
  onpointerdown={() => {
18
24
  cameraDown.copy(camera.current.position)
19
25
  }}
@@ -1,18 +1,3 @@
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 PointerMissBox: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type PointerMissBox = InstanceType<typeof PointerMissBox>;
1
+ declare const PointerMissBox: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type PointerMissBox = ReturnType<typeof PointerMissBox>;
18
3
  export default PointerMissBox;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { Vector3 } from 'three'
3
3
  import { T } from '@threlte/core'
4
- import { Grid, interactivity, PerfMonitor } from '@threlte/extras'
4
+ import { Grid, interactivity, PerfMonitor, bvh } from '@threlte/extras'
5
5
  import { PortalTarget } from './portal'
6
6
  import WorldObjects from './WorldObjects.svelte'
7
7
  import Selected from './Selected.svelte'
@@ -24,7 +24,11 @@
24
24
 
25
25
  let { children }: Props = $props()
26
26
 
27
- interactivity({
27
+ const settings = useSettings()
28
+ const focusedObject3d = useFocusedObject3d()
29
+ const origin = useOrigin()
30
+
31
+ const { raycaster, enabled } = interactivity({
28
32
  filter: (items) => {
29
33
  const item = items.find((item) => {
30
34
  return item.object.visible === undefined || item.object.visible === true
@@ -33,12 +37,14 @@
33
37
  return item ? [item] : []
34
38
  },
35
39
  })
40
+ $effect.pre(() => {
41
+ enabled.set(!settings.current.enableMeasure)
42
+ })
43
+ raycaster.firstHitOnly = true
36
44
 
37
- const settings = useSettings()
38
- const focusedObject3d = useFocusedObject3d()
39
- const origin = useOrigin()
45
+ bvh(() => ({ helper: false }))
40
46
 
41
- const object3d = $derived(focusedObject3d.current)
47
+ const focusedObject = $derived(focusedObject3d.current)
42
48
 
43
49
  const { isPresenting } = useXR()
44
50
  </script>
@@ -52,8 +58,11 @@
52
58
  rotation.x={$isPresenting ? -Math.PI / 2 : 0}
53
59
  rotation.z={origin.rotation}
54
60
  >
55
- {#if object3d}
56
- <Focus {object3d} />
61
+ <PointerMissBox />
62
+ <MeasureTool />
63
+
64
+ {#if focusedObject}
65
+ <Focus object3d={focusedObject} />
57
66
  {:else}
58
67
  {#if !$isPresenting}
59
68
  <Camera position={[3, 3, 3]}>
@@ -61,18 +70,13 @@
61
70
  </Camera>
62
71
  {/if}
63
72
 
64
- <PortalTarget id="world" />
65
-
66
- <MeasureTool />
67
73
  <StaticGeometries />
68
-
69
- <WorldObjects />
70
- <PointerMissBox />
71
-
72
74
  <Selected />
73
75
 
74
76
  {#if !$isPresenting && settings.current.grid}
75
77
  <Grid
78
+ raycast={() => null}
79
+ bvh={{ enabled: false }}
76
80
  plane="xy"
77
81
  sectionColor="#333"
78
82
  infiniteGrid
@@ -84,6 +88,11 @@
84
88
  {/if}
85
89
  {/if}
86
90
 
91
+ <T.Group attach={focusedObject ? false : undefined}>
92
+ <PortalTarget id="world" />
93
+ <WorldObjects />
94
+ </T.Group>
95
+
87
96
  {@render children?.()}
88
97
 
89
98
  <T.DirectionalLight position={[3, 3, 3]} />
@@ -37,6 +37,7 @@
37
37
  providePointclouds(() => partID.current)
38
38
  provideMotionClient(() => partID.current)
39
39
  provideObjects()
40
+
40
41
  const { focus } = provideSelection()
41
42
  </script>
42
43
 
@@ -53,4 +53,5 @@
53
53
  <T
54
54
  is={box}
55
55
  raycast={() => null}
56
+ bvh={{ enabled: false }}
56
57
  />
@@ -9,6 +9,7 @@
9
9
  import Settings from './Settings.svelte'
10
10
  import Logs from './Logs.svelte'
11
11
  import { useDraggable } from '../../hooks/useDraggable.svelte'
12
+ import { useWorldStates } from '../../hooks/useWorldState.svelte'
12
13
 
13
14
  const { ...rest } = $props()
14
15
 
@@ -17,6 +18,7 @@
17
18
  const selected = useSelected()
18
19
  const objects = useObjects()
19
20
  const draggable = useDraggable('treeview')
21
+ const worldStates = useWorldStates()
20
22
 
21
23
  let rootNode = $state<TreeNode>({
22
24
  id: 'world',
@@ -25,7 +27,7 @@
25
27
  href: '/',
26
28
  })
27
29
 
28
- const nodes = $derived(buildTreeNodes(objects.current))
30
+ const nodes = $derived(buildTreeNodes(objects.current, worldStates.current))
29
31
 
30
32
  $effect.pre(() => {
31
33
  if (!isEqual(rootNode.children, nodes)) {
@@ -8,4 +8,7 @@ export interface TreeNode {
8
8
  /**
9
9
  * Creates a tree representing parent child / relationships from a set of frames.
10
10
  */
11
- export declare const buildTreeNodes: (objects: WorldObject[]) => TreeNode[];
11
+ export declare const buildTreeNodes: (objects: WorldObject[], worldStates: {
12
+ name: string;
13
+ objects: WorldObject[];
14
+ }[]) => TreeNode[];
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Creates a tree representing parent child / relationships from a set of frames.
3
3
  */
4
- export const buildTreeNodes = (objects) => {
4
+ export const buildTreeNodes = (objects, worldStates) => {
5
5
  const nodeMap = new Map();
6
6
  const rootNodes = [];
7
7
  for (const object of objects) {
@@ -25,5 +25,27 @@ export const buildTreeNodes = (objects) => {
25
25
  }
26
26
  }
27
27
  }
28
+ for (const worldState of worldStates) {
29
+ const node = {
30
+ name: worldState.name,
31
+ id: worldState.name,
32
+ children: [],
33
+ href: `/world-state/${worldState.name}`,
34
+ };
35
+ console.log('worldState', worldState);
36
+ for (const object of worldState.objects) {
37
+ const child = {
38
+ name: object.name,
39
+ id: object.uuid,
40
+ children: [],
41
+ href: `/world-state/${worldState.name}/${object.name}`,
42
+ };
43
+ nodeMap.set(object.name, child);
44
+ node.children?.push(child);
45
+ console.log('child', child);
46
+ }
47
+ nodeMap.set(worldState.name, node);
48
+ rootNodes.push(node);
49
+ }
28
50
  return rootNodes;
29
51
  };
@@ -11,11 +11,14 @@
11
11
  import Pointcloud from './Pointcloud.svelte'
12
12
  import Model from './WorldObject.svelte'
13
13
  import Label from './Label.svelte'
14
+ import { useWorldStates } from '../hooks/useWorldState.svelte'
15
+ import WorldState from './WorldState.svelte'
14
16
 
15
17
  const points = usePointClouds()
16
18
  const drawAPI = useDrawAPI()
17
19
  const frames = useFrames()
18
20
  const geometries = useGeometries()
21
+ const worldStates = useWorldStates()
19
22
  </script>
20
23
 
21
24
  {#each frames.current as object (object.uuid)}
@@ -65,6 +68,10 @@
65
68
  </Portal>
66
69
  {/each}
67
70
 
71
+ {#each worldStates.current as { name, objects } (name)}
72
+ <WorldState {objects} />
73
+ {/each}
74
+
68
75
  {#each points.current as object (object.uuid)}
69
76
  <Portal id={object.referenceFrame}>
70
77
  <Pointcloud {object}>
@@ -81,11 +88,14 @@
81
88
  </Portal>
82
89
  {/each}
83
90
 
84
- <T
85
- name={drawAPI.object3ds.batchedArrow.object3d.name}
86
- is={drawAPI.object3ds.batchedArrow.object3d}
87
- dispose={false}
88
- />
91
+ {#if drawAPI.poses.length > 0}
92
+ <T
93
+ name={drawAPI.object3ds.batchedArrow.object3d.name}
94
+ is={drawAPI.object3ds.batchedArrow.object3d}
95
+ dispose={false}
96
+ bvh={{ enabled: false }}
97
+ />
98
+ {/if}
89
99
 
90
100
  {#each drawAPI.meshes as object (object.uuid)}
91
101
  <Portal id={object.referenceFrame}>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import Frame from './Frame.svelte'
3
+ import Label from './Label.svelte'
4
+ import Portal from './portal/Portal.svelte'
5
+ import PortalTarget from './portal/PortalTarget.svelte'
6
+ import { WorldObject } from '../WorldObject'
7
+
8
+ interface Props {
9
+ objects: WorldObject[]
10
+ }
11
+
12
+ let { objects }: Props = $props()
13
+ </script>
14
+
15
+ {#each objects as object (object.uuid)}
16
+ <Portal id={object.referenceFrame}>
17
+ <Frame
18
+ uuid={object.uuid}
19
+ name={object.name}
20
+ pose={object.pose}
21
+ geometry={object.geometry}
22
+ metadata={object.metadata}
23
+ >
24
+ <PortalTarget id={object.name} />
25
+ <Label text={object.name} />
26
+ </Frame>
27
+ </Portal>
28
+ {/each}
@@ -0,0 +1,7 @@
1
+ import { WorldObject } from '../WorldObject';
2
+ interface Props {
3
+ objects: WorldObject[];
4
+ }
5
+ declare const WorldState: import("svelte").Component<Props, {}, "">;
6
+ type WorldState = ReturnType<typeof WorldState>;
7
+ export default WorldState;
@@ -0,0 +1,17 @@
1
+ import { Raycaster, type Intersection } from 'three';
2
+ type EventNames = 'click' | 'move' | 'pointerenter' | 'pointerleave';
3
+ interface RaycastEvent<T extends EventNames> {
4
+ type: T;
5
+ intersections: Intersection[];
6
+ }
7
+ type Callback<T extends EventNames> = (event: RaycastEvent<T>) => void;
8
+ export declare const useMouseRaycaster: (getOptions?: () => {
9
+ enabled: boolean;
10
+ }) => {
11
+ raycaster: Raycaster;
12
+ onclick: (cb: Callback<"click">) => void;
13
+ onmove: (cb: Callback<"move">) => void;
14
+ onpointerenter: (cb: Callback<"pointerenter">) => void;
15
+ onpointerleave: (cb: Callback<"pointerleave">) => void;
16
+ };
17
+ export {};
@@ -0,0 +1,108 @@
1
+ import { Vector2, Raycaster, EventDispatcher } from 'three';
2
+ import { useThrelte } from '@threlte/core';
3
+ const pointerDown = new Vector2();
4
+ const pointerUp = new Vector2();
5
+ const pointerMove = new Vector2();
6
+ export const useMouseRaycaster = (getOptions) => {
7
+ let intersections = [];
8
+ const options = $derived({
9
+ enabled: true,
10
+ ...getOptions?.(),
11
+ });
12
+ const eventDispatcher = new EventDispatcher();
13
+ const raycaster = new Raycaster();
14
+ const { camera, dom, scene } = useThrelte();
15
+ const getNormalizedCoordinates = (event, vec) => {
16
+ const rect = dom.getBoundingClientRect();
17
+ /*
18
+ * Calculate pointer position in normalized device coordinates
19
+ * (-1 to +1) for both components
20
+ */
21
+ vec.x = ((event.clientX - rect.x) / dom.clientWidth) * 2 - 1;
22
+ vec.y = -(((event.clientY - rect.y) / dom.clientHeight) * 2) + 1;
23
+ };
24
+ const onPointerDown = (event) => {
25
+ getNormalizedCoordinates(event, pointerDown);
26
+ };
27
+ const onPointerUp = (event) => {
28
+ if (camera.current === undefined) {
29
+ return;
30
+ }
31
+ getNormalizedCoordinates(event, pointerUp);
32
+ if (pointerDown.sub(pointerUp).lengthSq() > 0.001) {
33
+ return;
34
+ }
35
+ // Update the picking ray with the camera and pointer position
36
+ raycaster.setFromCamera(pointerUp, camera.current);
37
+ const currentIntersections = raycaster.intersectObjects(scene.children, true);
38
+ eventDispatcher.dispatchEvent({ type: 'click', intersections: currentIntersections });
39
+ };
40
+ const onPointerMove = (event) => {
41
+ if (camera.current === undefined) {
42
+ return;
43
+ }
44
+ getNormalizedCoordinates(event, pointerMove);
45
+ raycaster.setFromCamera(pointerMove, camera.current);
46
+ const currentIntersections = raycaster.intersectObjects(scene.children, true);
47
+ const enterIntersections = [];
48
+ const leaveIntersections = [];
49
+ for (const a of currentIntersections) {
50
+ if (!intersections.some((b) => b.object.uuid === a.object.uuid)) {
51
+ enterIntersections.push(a);
52
+ }
53
+ }
54
+ for (const a of intersections) {
55
+ if (!currentIntersections.some((b) => b.object.uuid === a.object.uuid)) {
56
+ leaveIntersections.push(a);
57
+ }
58
+ }
59
+ if (enterIntersections.length > 0) {
60
+ eventDispatcher.dispatchEvent({ type: 'pointerenter', intersections: enterIntersections });
61
+ }
62
+ if (leaveIntersections.length > 0) {
63
+ eventDispatcher.dispatchEvent({ type: 'pointerleave', intersections: leaveIntersections });
64
+ }
65
+ eventDispatcher.dispatchEvent({ type: 'move', intersections: currentIntersections });
66
+ intersections = currentIntersections;
67
+ };
68
+ $effect(() => {
69
+ if (!options.enabled) {
70
+ return;
71
+ }
72
+ dom.addEventListener('pointermove', onPointerMove, { passive: true });
73
+ dom.addEventListener('pointerdown', onPointerDown, { passive: true });
74
+ dom.addEventListener('pointerup', onPointerUp, { passive: true });
75
+ return () => {
76
+ dom.removeEventListener('pointerdown', onPointerDown);
77
+ dom.removeEventListener('pointerup', onPointerUp);
78
+ dom.removeEventListener('pointermove', onPointerMove);
79
+ };
80
+ });
81
+ return {
82
+ raycaster,
83
+ onclick: (cb) => {
84
+ $effect(() => {
85
+ eventDispatcher.addEventListener('click', cb);
86
+ return () => eventDispatcher.removeEventListener('click', cb);
87
+ });
88
+ },
89
+ onmove: (cb) => {
90
+ $effect(() => {
91
+ eventDispatcher.addEventListener('move', cb);
92
+ return () => eventDispatcher.removeEventListener('move', cb);
93
+ });
94
+ },
95
+ onpointerenter: (cb) => {
96
+ $effect(() => {
97
+ eventDispatcher.addEventListener('pointerenter', cb);
98
+ return () => eventDispatcher.removeEventListener('pointerenter', cb);
99
+ });
100
+ },
101
+ onpointerleave: (cb) => {
102
+ $effect(() => {
103
+ eventDispatcher.addEventListener('pointerleave', cb);
104
+ return () => eventDispatcher.removeEventListener('pointerleave', cb);
105
+ });
106
+ },
107
+ };
108
+ };
@@ -2,17 +2,12 @@ import { useCursor } from '@threlte/extras';
2
2
  import { useFocused, useSelected } from './useSelection.svelte';
3
3
  import { useVisibility } from './useVisibility.svelte';
4
4
  import { Vector2 } from 'three';
5
- import { useSettings } from './useSettings.svelte';
6
5
  export const useObjectEvents = (uuid) => {
7
- const settings = useSettings();
8
6
  const selected = useSelected();
9
7
  const focused = useFocused();
10
8
  const visibility = useVisibility();
11
9
  const down = new Vector2();
12
- const measureCursor = useCursor('crosshair');
13
- const hoverCursor = useCursor();
14
- const measuring = $derived(settings.current.enableMeasure);
15
- const cursor = $derived(measuring ? measureCursor : hoverCursor);
10
+ const cursor = useCursor();
16
11
  return {
17
12
  get visible() {
18
13
  return visibility.get(uuid());
@@ -27,9 +22,6 @@ export const useObjectEvents = (uuid) => {
27
22
  },
28
23
  ondblclick: (event) => {
29
24
  event.stopPropagation();
30
- if (measuring) {
31
- return;
32
- }
33
25
  focused.set(uuid());
34
26
  },
35
27
  onpointerdown: (event) => {
@@ -37,9 +29,6 @@ export const useObjectEvents = (uuid) => {
37
29
  },
38
30
  onclick: (event) => {
39
31
  event.stopPropagation();
40
- if (measuring) {
41
- return;
42
- }
43
32
  if (down.distanceToSquared(event.pointer) < 0.1) {
44
33
  selected.set(uuid());
45
34
  }
@@ -1,4 +1,4 @@
1
- import { useThrelte } from '@threlte/core';
1
+ import { isInstanceOf, useThrelte } from '@threlte/core';
2
2
  import { getContext, setContext } from 'svelte';
3
3
  import { Matrix4, Object3D } from 'three';
4
4
  import { useObjects } from './useObjects.svelte';
@@ -56,7 +56,17 @@ export const provideSelection = () => {
56
56
  setContext(focusedObjectKey, focusedObjectContext);
57
57
  const { scene } = useThrelte();
58
58
  const uuid = $derived(focusedObject?.uuid);
59
- const focusedObject3d = $derived(uuid ? scene.getObjectByProperty('uuid', uuid)?.clone() : undefined);
59
+ const focusedObject3d = $derived.by(() => {
60
+ if (!uuid)
61
+ return;
62
+ const object = scene.getObjectByProperty('uuid', uuid)?.clone();
63
+ object?.traverse((child) => {
64
+ if (isInstanceOf(child, 'LineSegments')) {
65
+ child.raycast = () => null;
66
+ }
67
+ });
68
+ return object;
69
+ });
60
70
  setContext(focusedObject3dKey, {
61
71
  get current() {
62
72
  return focusedObject3d;
@@ -0,0 +1,19 @@
1
+ import { type TransformChangeEvent } from '@viamrobotics/sdk';
2
+ import { WorldObject } from '../WorldObject';
3
+ export declare const useWorldStates: () => {
4
+ readonly names: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").ResourceName>[];
5
+ readonly current: {
6
+ readonly name: string;
7
+ readonly objects: WorldObject<import("../WorldObject").Geometries>[];
8
+ readonly listUUIDs: import("@tanstack/svelte-query").QueryObserverResult<string[]>;
9
+ readonly getTransforms: import("@tanstack/svelte-query").QueryObserverResult<import("@viamrobotics/sdk").TransformWithUUID>[] | undefined;
10
+ readonly changeStream: import("@tanstack/svelte-query").DefinedQueryObserverResult<TransformChangeEvent[], Error> | import("@tanstack/svelte-query").QueryObserverLoadingErrorResult<TransformChangeEvent[], Error> | import("@tanstack/svelte-query").QueryObserverLoadingResult<TransformChangeEvent[], Error> | import("@tanstack/svelte-query").QueryObserverPendingResult<TransformChangeEvent[], Error> | import("@tanstack/svelte-query").QueryObserverPlaceholderResult<TransformChangeEvent[], Error>;
11
+ }[];
12
+ };
13
+ export declare const useWorldState: (partID: () => string, resourceName: () => string) => {
14
+ readonly name: string;
15
+ readonly objects: WorldObject<import("../WorldObject").Geometries>[];
16
+ readonly listUUIDs: import("@tanstack/svelte-query").QueryObserverResult<string[]>;
17
+ readonly getTransforms: import("@tanstack/svelte-query").QueryObserverResult<import("@viamrobotics/sdk").TransformWithUUID>[] | undefined;
18
+ readonly changeStream: import("@tanstack/svelte-query").DefinedQueryObserverResult<TransformChangeEvent[], Error> | import("@tanstack/svelte-query").QueryObserverLoadingErrorResult<TransformChangeEvent[], Error> | import("@tanstack/svelte-query").QueryObserverLoadingResult<TransformChangeEvent[], Error> | import("@tanstack/svelte-query").QueryObserverPendingResult<TransformChangeEvent[], Error> | import("@tanstack/svelte-query").QueryObserverPlaceholderResult<TransformChangeEvent[], Error>;
19
+ };
@@ -0,0 +1,97 @@
1
+ import { toPath, getInUnsafe, mutInUnsafe } from '@thi.ng/paths';
2
+ import { WorldStateStoreClient, TransformChangeType, } from '@viamrobotics/sdk';
3
+ import { createResourceClient, createResourceQuery, createResourceStream, useResourceNames, } from '@viamrobotics/svelte-sdk';
4
+ import { fromTransform, WorldObject } from '../WorldObject';
5
+ import { usePartID } from './usePartID.svelte';
6
+ export const useWorldStates = () => {
7
+ const partID = usePartID();
8
+ const resourceNames = useResourceNames(() => partID.current, 'world_state_store');
9
+ const current = $derived.by(() => resourceNames.current.map(({ name }) => useWorldState(() => partID.current, () => name)));
10
+ return {
11
+ get names() {
12
+ return resourceNames.current;
13
+ },
14
+ get current() {
15
+ return current;
16
+ },
17
+ };
18
+ };
19
+ export const useWorldState = (partID, resourceName) => {
20
+ const client = createResourceClient(WorldStateStoreClient, partID, resourceName);
21
+ let initialized = false;
22
+ const listUUIDs = createResourceQuery(client, 'listUUIDs');
23
+ const getTransforms = $derived(listUUIDs.current.data?.map((uuid) => {
24
+ return createResourceQuery(client, 'getTransform', () => [uuid], () => ({ refetchInterval: false }));
25
+ }));
26
+ const changeStream = createResourceStream(client, 'streamTransformChanges');
27
+ const worldObjects = $state({});
28
+ const initializeCurrent = (objects) => {
29
+ for (const object of objects) {
30
+ worldObjects[object.uuid] = object;
31
+ }
32
+ initialized = true;
33
+ };
34
+ $effect(() => {
35
+ if (!getTransforms) {
36
+ return;
37
+ }
38
+ if (initialized) {
39
+ return;
40
+ }
41
+ const queries = getTransforms.map((query) => query.current);
42
+ if (queries.some((query) => query?.isLoading)) {
43
+ return;
44
+ }
45
+ const objects = [];
46
+ for (const transform of queries.flatMap((query) => query.data) ?? []) {
47
+ if (transform === undefined) {
48
+ continue;
49
+ }
50
+ objects.push(fromTransform(transform));
51
+ }
52
+ initializeCurrent(objects);
53
+ });
54
+ const processChangeEvent = async (event) => {
55
+ if (event.transform === undefined) {
56
+ return;
57
+ }
58
+ switch (event.changeType) {
59
+ case TransformChangeType.ADDED:
60
+ worldObjects[event.transform.uuidString] = fromTransform(event.transform);
61
+ break;
62
+ case TransformChangeType.UPDATED:
63
+ for (const path of event.updatedFields?.paths ?? []) {
64
+ // Type inference is tough here, so we use unsafe APIs
65
+ const paths = toPath(path);
66
+ const next = getInUnsafe(event.transform, paths);
67
+ mutInUnsafe(worldObjects[event.transform.uuidString], paths, next);
68
+ }
69
+ break;
70
+ case TransformChangeType.REMOVED:
71
+ delete worldObjects[event.transform.uuidString];
72
+ break;
73
+ }
74
+ };
75
+ $effect(() => {
76
+ for (const event of changeStream.current?.data ?? []) {
77
+ void processChangeEvent(event);
78
+ }
79
+ });
80
+ return {
81
+ get name() {
82
+ return resourceName();
83
+ },
84
+ get objects() {
85
+ return Object.values(worldObjects);
86
+ },
87
+ get listUUIDs() {
88
+ return listUUIDs.current;
89
+ },
90
+ get getTransforms() {
91
+ return getTransforms?.map((query) => query.current);
92
+ },
93
+ get changeStream() {
94
+ return changeStream.current;
95
+ },
96
+ };
97
+ };
package/package.json CHANGED
@@ -1,71 +1,73 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "devDependencies": {
8
- "@ag-grid-community/client-side-row-model": "32.3.8",
9
- "@ag-grid-community/core": "32.3.8",
10
- "@ag-grid-community/styles": "32.3.8",
11
- "@changesets/cli": "2.29.5",
12
- "@dimforge/rapier3d-compat": "0.18.0",
13
- "@eslint/compat": "1.3.1",
14
- "@eslint/js": "9.32.0",
15
- "@playwright/test": "1.54.2",
16
- "@sentry/sveltekit": "10.1.0",
17
- "@skeletonlabs/skeleton": "3.1.7",
18
- "@skeletonlabs/skeleton-svelte": "1.3.1",
19
- "@sveltejs/adapter-static": "3.0.8",
20
- "@sveltejs/kit": "2.27.1",
21
- "@sveltejs/package": "2.4.0",
22
- "@sveltejs/vite-plugin-svelte": "6.1.0",
8
+ "@ag-grid-community/client-side-row-model": "32.3.9",
9
+ "@ag-grid-community/core": "32.3.9",
10
+ "@ag-grid-community/styles": "32.3.9",
11
+ "@changesets/cli": "2.29.6",
12
+ "@dimforge/rapier3d-compat": "0.18.2",
13
+ "@eslint/compat": "1.3.2",
14
+ "@eslint/js": "9.34.0",
15
+ "@playwright/test": "1.55.0",
16
+ "@sentry/sveltekit": "10.10.0",
17
+ "@skeletonlabs/skeleton": "3.2.0",
18
+ "@skeletonlabs/skeleton-svelte": "1.5.1",
19
+ "@sveltejs/adapter-static": "3.0.9",
20
+ "@sveltejs/kit": "2.37.0",
21
+ "@sveltejs/package": "2.5.0",
22
+ "@sveltejs/vite-plugin-svelte": "6.1.4",
23
23
  "@tailwindcss/forms": "0.5.10",
24
- "@tailwindcss/vite": "4.1.11",
25
- "@tanstack/svelte-query": "5.83.1",
26
- "@tanstack/svelte-query-devtools": "5.84.0",
27
- "@testing-library/jest-dom": "6.6.4",
24
+ "@tailwindcss/vite": "4.1.13",
25
+ "@tanstack/svelte-query": "5.86.0",
26
+ "@tanstack/svelte-query-devtools": "5.86.0",
27
+ "@testing-library/jest-dom": "6.8.0",
28
28
  "@testing-library/svelte": "5.2.8",
29
- "@threlte/core": "8.1.4",
30
- "@threlte/extras": "9.4.4",
29
+ "@thi.ng/paths": "5.2.21",
30
+ "@threlte/core": "8.1.5",
31
+ "@threlte/extras": "9.5.4",
31
32
  "@threlte/rapier": "3.1.5",
32
33
  "@threlte/xr": "1.0.8",
33
- "@types/bun": "1.2.19",
34
+ "@types/bun": "1.2.21",
34
35
  "@types/lodash-es": "4.17.12",
35
36
  "@types/three": "0.179.0",
36
- "@typescript-eslint/eslint-plugin": "8.39.0",
37
- "@typescript-eslint/parser": "8.39.0",
37
+ "@typescript-eslint/eslint-plugin": "8.42.0",
38
+ "@typescript-eslint/parser": "8.42.0",
38
39
  "@viamrobotics/prime-core": "0.1.5",
39
- "@viamrobotics/sdk": "0.46.0",
40
- "@viamrobotics/svelte-sdk": "0.4.5",
40
+ "@viamrobotics/sdk": "0.50.0",
41
+ "@viamrobotics/svelte-sdk": "0.6.0",
41
42
  "@vitejs/plugin-basic-ssl": "2.1.0",
42
- "@zag-js/svelte": "1.21.1",
43
- "@zag-js/tree-view": "1.21.1",
43
+ "@zag-js/svelte": "1.22.1",
44
+ "@zag-js/tree-view": "1.22.1",
44
45
  "camera-controls": "3.1.0",
45
- "eslint": "9.32.0",
46
+ "eslint": "9.34.0",
46
47
  "eslint-config-prettier": "10.1.8",
47
- "eslint-plugin-svelte": "3.11.0",
48
+ "eslint-plugin-svelte": "3.12.1",
48
49
  "globals": "16.3.0",
49
50
  "idb-keyval": "6.2.2",
50
51
  "jsdom": "26.1.0",
51
52
  "lodash-es": "4.17.21",
52
- "lucide-svelte": "0.536.0",
53
+ "lucide-svelte": "0.542.0",
53
54
  "prettier": "3.6.2",
54
55
  "prettier-plugin-svelte": "3.4.0",
55
56
  "prettier-plugin-tailwindcss": "0.6.14",
56
57
  "publint": "0.3.12",
57
58
  "runed": "0.31.1",
58
- "svelte": "5.37.3",
59
+ "svelte": "5.38.7",
59
60
  "svelte-check": "4.3.1",
60
61
  "svelte-virtuallists": "1.4.2",
61
- "tailwindcss": "4.1.11",
62
- "three": "0.179.1",
63
- "threlte-uikit": "1.2.0",
64
- "tsx": "4.20.3",
62
+ "tailwindcss": "4.1.13",
63
+ "three": "0.179.0",
64
+ "three-mesh-bvh": "^0.9.1",
65
+ "threlte-uikit": "1.2.1",
66
+ "tsx": "4.20.5",
65
67
  "typescript": "5.9.2",
66
- "typescript-eslint": "8.39.0",
67
- "vite": "6.3.5",
68
- "vite-plugin-devtools-json": "0.4.1",
68
+ "typescript-eslint": "8.42.0",
69
+ "vite": "7.1.4",
70
+ "vite-plugin-devtools-json": "1.0.0",
69
71
  "vite-plugin-mkcert": "1.17.8",
70
72
  "vitest": "3.2.4"
71
73
  },
File without changes
@@ -1,26 +0,0 @@
1
- export default Labels;
2
- type Labels = SvelteComponent<{
3
- [x: string]: never;
4
- }, {
5
- [evt: string]: CustomEvent<any>;
6
- }, {}> & {
7
- $$bindings?: string | undefined;
8
- };
9
- declare const Labels: $$__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
- }