@viamrobotics/motion-tools 0.18.2 → 0.19.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.
@@ -0,0 +1,10 @@
1
+ <script>
2
+ import { Canvas } from '@threlte/core'
3
+ let { child, ...rest } = $props()
4
+
5
+ const Component = $derived(child)
6
+ </script>
7
+
8
+ <Canvas>
9
+ <Component {...rest} />
10
+ </Canvas>
@@ -0,0 +1,11 @@
1
+ export default MockCanvas;
2
+ type MockCanvas = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const MockCanvas: import("svelte").Component<{
7
+ child: any;
8
+ } & Record<string, any>, {}, "">;
9
+ type $$ComponentProps = {
10
+ child: any;
11
+ } & Record<string, any>;
@@ -1,11 +1,13 @@
1
1
  <script lang="ts">
2
- import { T, useTask } from '@threlte/core'
2
+ import { T, useTask, useThrelte } from '@threlte/core'
3
3
  import { useSelectedObject, useSelectedObject3d } from '../hooks/useSelection.svelte'
4
4
  import { OBBHelper } from '../three/OBBHelper'
5
5
  import { OBB } from 'three/addons/math/OBB.js'
6
6
 
7
7
  const obb = new OBB()
8
8
  const obbHelper = new OBBHelper()
9
+
10
+ const { invalidate } = useThrelte()
9
11
  const selected = useSelectedObject()
10
12
  const selectedObject3d = useSelectedObject3d()
11
13
 
@@ -27,6 +29,7 @@
27
29
  if (selected.current.metadata.batched) {
28
30
  selected.current.metadata.getBoundingBoxAt?.(obb)
29
31
  obbHelper.setFromOBB(obb)
32
+ invalidate()
30
33
  return
31
34
  }
32
35
 
@@ -34,6 +37,7 @@
34
37
  selectedObject3d.current?.getWorldPosition(clone.position)
35
38
  selectedObject3d.current?.getWorldQuaternion(clone.quaternion)
36
39
  obbHelper.setFromObject(clone)
40
+ invalidate()
37
41
  }
38
42
  },
39
43
  {
@@ -48,8 +52,11 @@
48
52
  obbHelper.visible = true
49
53
  } else {
50
54
  stop()
55
+
51
56
  obbHelper.visible = false
52
57
  }
58
+
59
+ invalidate()
53
60
  })
54
61
  </script>
55
62
 
@@ -1,37 +1,8 @@
1
1
  <script lang="ts">
2
- import { useFrames } from '../../hooks/useFrames.svelte'
3
- import { useGeometries } from '../../hooks/useGeometries.svelte'
4
2
  import { useLogs } from '../../hooks/useLogs.svelte'
5
- import { usePointClouds } from '../../hooks/usePointclouds.svelte'
6
3
  import Drawer from './Drawer.svelte'
7
4
 
8
- const frames = useFrames()
9
- const geometries = useGeometries()
10
- const pointclouds = usePointClouds()
11
5
  const logs = useLogs()
12
-
13
- $effect(() => {
14
- if (frames.error) {
15
- const message = `Frames: ${frames.error.message}`
16
- logs.add(message, 'error')
17
- }
18
- })
19
-
20
- $effect(() => {
21
- if (geometries.errors.length > 0) {
22
- for (const error of geometries.errors) {
23
- logs.add(`Geometries: ${error.message}`, 'error')
24
- }
25
- }
26
- })
27
-
28
- $effect(() => {
29
- if (pointclouds.errors.length > 0) {
30
- for (const error of pointclouds.errors) {
31
- logs.add(`Pointclouds: ${error.message}`, 'error')
32
- }
33
- }
34
- })
35
6
  </script>
36
7
 
37
8
  <Drawer name="Logs">
@@ -1,3 +1,18 @@
1
- declare const Logs: import("svelte").Component<Record<string, never>, {}, "">;
2
- type Logs = ReturnType<typeof Logs>;
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 Logs: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type Logs = InstanceType<typeof Logs>;
3
18
  export default Logs;
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { Select, Switch, Input } from '@viamrobotics/prime-core'
3
- import { useQueryClient } from '@tanstack/svelte-query'
4
3
  import RefreshRate from '../RefreshRate.svelte'
5
4
  import { useMotionClient } from '../../hooks/useMotionClient.svelte'
6
5
  import Drawer from './Drawer.svelte'
@@ -10,13 +9,31 @@
10
9
  import { RefreshRates, useMachineSettings } from '../../hooks/useMachineSettings.svelte'
11
10
  import WeblabActive from '../weblab/WeblabActive.svelte'
12
11
  import { WEBLABS_EXPERIMENTS } from '../../hooks/useWeblabs.svelte'
12
+ import { useGeometries } from '../../hooks/useGeometries.svelte'
13
+ import { usePointClouds } from '../../hooks/usePointclouds.svelte'
14
+ import { useThrelte } from '@threlte/core'
15
+ import { useRefetchPoses } from '../../hooks/useRefetchPoses'
13
16
 
14
- const queryClient = useQueryClient()
17
+ const { invalidate } = useThrelte()
15
18
  const partID = usePartID()
16
19
  const cameras = useResourceNames(() => partID.current, 'camera')
17
20
  const settings = useSettings()
18
21
  const { disabledCameras } = useMachineSettings()
19
22
  const motionClient = useMotionClient()
23
+ const geometries = useGeometries()
24
+ const pointclouds = usePointClouds()
25
+
26
+ const { refetchPoses } = useRefetchPoses()
27
+
28
+ // Invalidate the renderer for any settings change
29
+ $effect(() => {
30
+ for (const key in settings.current) {
31
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
32
+ settings.current[key as keyof typeof settings.current]
33
+ }
34
+
35
+ invalidate()
36
+ })
20
37
  </script>
21
38
 
22
39
  <Drawer
@@ -31,14 +48,15 @@
31
48
  label="Poses"
32
49
  allowLive
33
50
  onManualRefetch={() => {
34
- queryClient.refetchQueries({ queryKey: ['getPose', 'getGeometries'], exact: false })
51
+ refetchPoses()
52
+ geometries.refetch()
35
53
  }}
36
54
  />
37
55
  <RefreshRate
38
56
  id={RefreshRates.pointclouds}
39
57
  label="Pointclouds"
40
58
  onManualRefetch={() => {
41
- queryClient.refetchQueries({ queryKey: ['getPointCloud'], exact: false })
59
+ pointclouds.refetch()
42
60
  }}
43
61
  />
44
62
  <div>
@@ -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 Settings: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type Settings = InstanceType<typeof Settings>;
1
+ declare const Settings: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Settings = ReturnType<typeof Settings>;
18
3
  export default Settings;
@@ -1,4 +1,4 @@
1
- import { ArmClient, ArmJointPositions } from '@viamrobotics/sdk';
1
+ import { ArmClient } from '@viamrobotics/sdk';
2
2
  import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
3
  import { getContext, setContext } from 'svelte';
4
4
  const key = Symbol('arm-client-context');
@@ -7,20 +7,8 @@ export const provideArmClient = (partID) => {
7
7
  const options = { refetchInterval: 500 };
8
8
  const names = $derived(arms.current.map((arm) => arm.name));
9
9
  const clients = $derived(arms.current.map((arm) => createResourceClient(ArmClient, partID, () => arm.name)));
10
- const jointPositionsQueries = $derived.by(() => {
11
- const results = {};
12
- for (const client of clients) {
13
- if (!client.current)
14
- continue;
15
- const query = createResourceQuery(client, 'getJointPositions', options);
16
- results[client.current.name] = query;
17
- }
18
- return results;
19
- });
20
- const currentPositions = $derived(Object.fromEntries(Object.entries(jointPositionsQueries).map(([name, query]) => [
21
- name,
22
- query.current.data?.values,
23
- ])));
10
+ const jointPositionsQueries = $derived(clients.map((client) => [client.current?.name, createResourceQuery(client, 'getJointPositions', options)]));
11
+ const currentPositions = $derived(Object.fromEntries(jointPositionsQueries.map(([name, query]) => [name, query.data?.values])));
24
12
  setContext(key, {
25
13
  get names() {
26
14
  return names;
@@ -10,17 +10,17 @@ import { createPose, createPoseFromFrame } from '../transform';
10
10
  import { useCameraControls } from './useControls.svelte';
11
11
  import { useThrelte } from '@threlte/core';
12
12
  import { OrientationVector } from '../three/OrientationVector';
13
+ import { useLogs } from './useLogs.svelte';
13
14
  const axis = new Vector3();
14
15
  const quaternion = new Quaternion();
15
16
  const ov = new OrientationVector();
16
17
  const key = Symbol('draw-api-context-key');
17
18
  const tryParse = (json) => {
18
19
  try {
19
- return JSON.parse(json);
20
+ return [null, JSON.parse(json)];
20
21
  }
21
22
  catch (error) {
22
- console.warn('Failed to parse JSON:', error);
23
- return;
23
+ return [error, null];
24
24
  }
25
25
  };
26
26
  /**
@@ -55,6 +55,7 @@ class Float32Reader {
55
55
  }
56
56
  }
57
57
  export const provideDrawAPI = () => {
58
+ const logs = useLogs();
58
59
  const cameraControls = useCameraControls();
59
60
  const { invalidate } = useThrelte();
60
61
  let pointsIndex = 0;
@@ -380,23 +381,27 @@ export const provideDrawAPI = () => {
380
381
  const scheduleReconnect = () => {
381
382
  setTimeout(() => {
382
383
  reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay);
383
- console.log(`Reconnecting in ${reconnectDelay / 1000} seconds...`);
384
+ logs.add(`Reconnecting to drawing server in ${reconnectDelay / 1000} seconds...`, 'warn');
384
385
  connect();
385
386
  }, reconnectDelay);
386
387
  };
387
388
  const onOpen = () => {
388
389
  connectionStatus = 'open';
389
390
  reconnectDelay = 1000;
390
- console.log(`Connected to websocket server at ${BACKEND_IP}:${BUN_SERVER_PORT}`);
391
+ logs.add(`Connected to drawing server at ${BACKEND_IP}:${BUN_SERVER_PORT}`);
391
392
  };
392
393
  const onClose = () => {
393
394
  connectionStatus = 'closed';
394
- console.log('Disconnected from websocket server');
395
+ logs.add('Disconnected from drawing server', 'warn');
395
396
  scheduleReconnect();
396
397
  };
397
398
  const onError = (event) => {
398
- console.log('Websocket error', JSON.stringify(event));
399
+ const stringified = JSON.stringify(event);
399
400
  ws.close();
401
+ if (stringified === '{"isTrusted":true}') {
402
+ return;
403
+ }
404
+ logs.add(`Drawing server error: ${JSON.stringify(event)}`, 'error');
400
405
  };
401
406
  const onMessage = async (event) => {
402
407
  if (typeof event.data === 'object' && 'arrayBuffer' in event.data) {
@@ -418,7 +423,10 @@ export const provideDrawAPI = () => {
418
423
  return drawGLTF(reader.buffer);
419
424
  }
420
425
  }
421
- const data = tryParse(event.data);
426
+ const [error, data] = tryParse(event.data);
427
+ if (error) {
428
+ logs.add(`Failed to parse JSON from drawing server: ${JSON.stringify(error)}`, 'error');
429
+ }
422
430
  if (!data)
423
431
  return;
424
432
  if ('setCameraPose' in data) {
@@ -1,8 +1,6 @@
1
1
  import { WorldObject } from '../WorldObject.svelte';
2
2
  interface FramesContext {
3
3
  current: WorldObject[];
4
- error?: Error;
5
- fetching: boolean;
6
4
  getParentFrameOptions: (componentName: string) => string[];
7
5
  }
8
6
  export declare const provideFrames: (partID: () => string) => void;
@@ -22,9 +22,16 @@ export const provideFrames = (partID) => {
22
22
  const { updateUUIDs } = usePersistentUUIDs();
23
23
  $effect.pre(() => {
24
24
  if (revision) {
25
- untrack(() => query.current).refetch();
25
+ untrack(() => query.refetch());
26
+ }
27
+ });
28
+ $effect(() => {
29
+ if (query.isFetching) {
30
+ logs.add('Fetching frames...');
31
+ }
32
+ else if (query.error) {
33
+ logs.add(`Frames: ${query.error.message}`, 'error');
26
34
  }
27
- logs.add('Fetching frames...');
28
35
  });
29
36
  $effect.pre(() => {
30
37
  if (partConfig.isDirty) {
@@ -36,7 +43,7 @@ export const provideFrames = (partID) => {
36
43
  });
37
44
  const machineFrames = $derived.by(() => {
38
45
  const objects = {};
39
- for (const { frame } of query.current.data ?? []) {
46
+ for (const { frame } of query.data ?? []) {
40
47
  if (frame === undefined) {
41
48
  continue;
42
49
  }
@@ -136,8 +143,6 @@ export const provideFrames = (partID) => {
136
143
  updateUUIDs(results);
137
144
  return results;
138
145
  });
139
- const error = $derived(query.current.error ?? undefined);
140
- const fetching = $derived(query.current.isFetching);
141
146
  const getParentFrameOptions = (componentName) => {
142
147
  const validFrames = new Set(current.map((frame) => frame.name));
143
148
  validFrames.add('world');
@@ -159,12 +164,6 @@ export const provideFrames = (partID) => {
159
164
  get current() {
160
165
  return current;
161
166
  },
162
- get error() {
163
- return error;
164
- },
165
- get fetching() {
166
- return fetching;
167
- },
168
167
  });
169
168
  };
170
169
  export const useFrames = () => {
@@ -1,7 +1,7 @@
1
1
  import { WorldObject } from '../WorldObject.svelte';
2
2
  interface Context {
3
3
  current: WorldObject[];
4
- errors: Error[];
4
+ refetch: () => void;
5
5
  }
6
6
  export declare const provideGeometries: (partID: () => string) => void;
7
7
  export declare const useGeometries: () => Context;
@@ -1,65 +1,59 @@
1
- import { ArmClient, CameraClient, GantryClient, Geometry, GripperClient } from '@viamrobotics/sdk';
2
- import { createQueries, queryOptions } from '@tanstack/svelte-query';
3
- import { createResourceClient, useResourceNames } from '@viamrobotics/svelte-sdk';
1
+ import { ArmClient, CameraClient, GantryClient, GripperClient } from '@viamrobotics/sdk';
2
+ import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
4
3
  import { setContext, getContext } from 'svelte';
5
- import { fromStore, toStore } from 'svelte/store';
6
4
  import { useMachineSettings, RefreshRates } from './useMachineSettings.svelte';
7
5
  import { WorldObject } from '../WorldObject.svelte';
8
6
  import { usePersistentUUIDs } from './usePersistentUUIDs.svelte';
9
7
  import { useLogs } from './useLogs.svelte';
10
8
  import { resourceColors } from '../color';
11
9
  import { Color } from 'three';
12
- import { useFrames } from './useFrames.svelte';
13
10
  import { RefetchRates } from '../components/RefreshRate.svelte';
11
+ import { useResourceByName } from './useResourceByName.svelte';
14
12
  const key = Symbol('geometries-context');
15
13
  export const provideGeometries = (partID) => {
16
- const frames = useFrames();
17
- const resourceNames = useResourceNames(partID);
14
+ const logs = useLogs();
15
+ const resourceByName = useResourceByName();
18
16
  const arms = useResourceNames(partID, 'arm');
19
17
  const cameras = useResourceNames(partID, 'camera');
20
18
  const grippers = useResourceNames(partID, 'gripper');
21
19
  const gantries = useResourceNames(partID, 'gantry');
22
- const logs = useLogs();
23
20
  const { refreshRates } = useMachineSettings();
24
21
  const armClients = $derived(arms.current.map((arm) => createResourceClient(ArmClient, partID, () => arm.name)));
25
22
  const gripperClients = $derived(grippers.current.map((gripper) => createResourceClient(GripperClient, partID, () => gripper.name)));
26
23
  const cameraClients = $derived(cameras.current.map((camera) => createResourceClient(CameraClient, partID, () => camera.name)));
27
24
  const gantryClients = $derived(gantries.current.map((gantry) => createResourceClient(GantryClient, partID, () => gantry.name)));
28
- const clients = $derived([...armClients, ...gripperClients, ...cameraClients, ...gantryClients].filter((client) => {
29
- return frames.current.some((frame) => frame.name === client.current?.name);
30
- }));
31
25
  const options = $derived.by(() => {
32
26
  const interval = refreshRates.get(RefreshRates.poses);
33
- const results = [];
34
- for (const client of clients) {
35
- const options = queryOptions({
36
- enabled: interval !== RefetchRates.OFF && client.current !== undefined,
37
- refetchInterval: interval === RefetchRates.MANUAL ? false : interval,
38
- queryKey: ['getGeometries', 'partID', partID(), client.current?.name],
39
- queryFn: async () => {
40
- if (!client.current) {
41
- throw new Error('No client');
42
- }
43
- logs.add(`Fetching geometries for ${client.current.name}...`);
44
- const geometries = await client.current.getGeometries();
45
- return { name: client.current.name, geometries };
46
- },
47
- });
48
- results.push(options);
27
+ return {
28
+ enabled: refreshRates.get(RefreshRates.poses) !== RefetchRates.OFF,
29
+ refetchInterval: interval === RefetchRates.MANUAL ? false : interval,
30
+ };
31
+ });
32
+ const armQueries = $derived(armClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
33
+ const gripperQueries = $derived(gripperClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
34
+ const cameraQueries = $derived(cameraClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
35
+ const gantryQueries = $derived(gantryClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
36
+ $effect(() => {
37
+ for (const [name, query] of queries) {
38
+ if (query.isFetching) {
39
+ logs.add(`Fetching geometries for ${name}...`);
40
+ }
41
+ else if (query.error) {
42
+ logs.add(`Error fetching geometries from ${name}: ${query.error.message}`, 'error');
43
+ }
49
44
  }
50
- return results;
51
45
  });
52
46
  const { updateUUIDs } = usePersistentUUIDs();
53
- const queries = fromStore(createQueries({ queries: toStore(() => options) }));
54
- const errors = $derived(queries.current.map((query) => query.error).filter((error) => error !== null));
47
+ const queries = $derived([...armQueries, ...gripperQueries, ...cameraQueries, ...gantryQueries]);
55
48
  const geometries = $derived.by(() => {
56
49
  const results = [];
57
- for (const query of queries.current) {
58
- if (!query.data)
50
+ for (const [name, query] of queries) {
51
+ if (!name || !query.data) {
59
52
  continue;
60
- for (const geometry of query.data.geometries) {
61
- const resourceName = resourceNames.current.find((item) => item.name === query.data.name);
62
- const worldObject = new WorldObject(geometry.label ? geometry.label : `${query.data.name} geometry`, undefined, query.data.name, geometry, resourceName
53
+ }
54
+ for (const geometry of query.data) {
55
+ const resourceName = resourceByName.current[name];
56
+ const worldObject = new WorldObject(geometry.label ? geometry.label : `${name} geometry`, undefined, name, geometry, resourceName
63
57
  ? {
64
58
  color: new Color(resourceColors[resourceName.subtype]),
65
59
  }
@@ -74,8 +68,10 @@ export const provideGeometries = (partID) => {
74
68
  get current() {
75
69
  return geometries;
76
70
  },
77
- get errors() {
78
- return errors;
71
+ refetch() {
72
+ for (const [, query] of queries) {
73
+ query.refetch();
74
+ }
79
75
  },
80
76
  });
81
77
  };
@@ -1,7 +1,7 @@
1
1
  import { WorldObject, type PointsGeometry } from '../WorldObject.svelte';
2
2
  interface Context {
3
3
  current: WorldObject<PointsGeometry>[];
4
- errors: Error[];
4
+ refetch: () => void;
5
5
  }
6
6
  export declare const providePointclouds: (partID: () => string) => void;
7
7
  export declare const usePointClouds: () => Context;
@@ -1,8 +1,6 @@
1
- import { createQueries, queryOptions } from '@tanstack/svelte-query';
2
1
  import { CameraClient } from '@viamrobotics/sdk';
3
2
  import { setContext, getContext } from 'svelte';
4
- import { fromStore, toStore } from 'svelte/store';
5
- import { createResourceClient, useResourceNames } from '@viamrobotics/svelte-sdk';
3
+ import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
6
4
  import { parsePcdInWorker } from '../loaders/pcd';
7
5
  import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
8
6
  import { WorldObject } from '../WorldObject.svelte';
@@ -15,54 +13,85 @@ export const providePointclouds = (partID) => {
15
13
  const { refreshRates, disabledCameras } = useMachineSettings();
16
14
  const cameras = useResourceNames(partID, 'camera');
17
15
  const clients = $derived(cameras.current.map((camera) => createResourceClient(CameraClient, partID, () => camera.name)));
18
- const options = $derived.by(() => {
19
- const interval = refreshRates.get(RefreshRates.pointclouds);
20
- const results = [];
21
- for (const cameraClient of clients) {
22
- const name = cameraClient.current?.name ?? '';
23
- const options = queryOptions({
24
- enabled: interval !== RefetchRates.OFF &&
25
- cameraClient.current !== undefined &&
26
- disabledCameras.get(name) !== true,
27
- refetchInterval: interval === RefetchRates.MANUAL ? false : interval,
28
- queryKey: ['getPointCloud', 'partID', partID(), name],
29
- queryFn: async () => {
30
- if (!cameraClient.current) {
31
- throw new Error('No camera client');
32
- }
33
- logs.add(`Fetching pointcloud for ${cameraClient.current.name}`);
34
- const response = await cameraClient.current.getPointCloud();
35
- if (!response)
36
- return null;
37
- const { positions, colors } = await parsePcdInWorker(new Uint8Array(response));
38
- return new WorldObject(`${name}:pointcloud`, undefined, name, { center: undefined, geometryType: { case: 'points', value: positions } }, colors ? { colors } : undefined);
39
- },
40
- });
41
- results.push(options);
16
+ const propQueries = $derived(clients.map((client) => [
17
+ client.current?.name,
18
+ createResourceQuery(client, 'getProperties', {
19
+ staleTime: Infinity,
20
+ refetchOnMount: false,
21
+ refetchInterval: false,
22
+ }),
23
+ ]));
24
+ const fetchedPropQueries = propQueries.every(([, query]) => query.isPending === false);
25
+ const interval = $derived(refreshRates.get(RefreshRates.pointclouds));
26
+ const enabledClients = $derived(clients.filter((client) => fetchedPropQueries &&
27
+ client.current?.name &&
28
+ interval !== RefetchRates.OFF &&
29
+ disabledCameras.get(client.current?.name) !== true));
30
+ /**
31
+ * Some machines have a lot of cameras, so before enabling all of them
32
+ * we'll first check pointcloud support.
33
+ *
34
+ * We'll disable cameras that don't support pointclouds,
35
+ * but still allow users to manually enable if they want to.
36
+ */
37
+ $effect(() => {
38
+ for (const [name, query] of propQueries) {
39
+ if (name && query.data?.supportsPcd === false) {
40
+ if (disabledCameras.get(name) === undefined) {
41
+ disabledCameras.set(name, true);
42
+ }
43
+ }
42
44
  }
43
- return results;
44
45
  });
46
+ const queries = $derived(enabledClients.map((client) => [
47
+ client.current?.name,
48
+ createResourceQuery(client, 'getPointCloud', () => ({ refetchInterval: interval })),
49
+ ]));
45
50
  const { updateUUIDs } = usePersistentUUIDs();
46
- const queries = fromStore(createQueries({
47
- queries: toStore(() => options),
48
- combine: (results) => {
49
- const data = results
50
- .flatMap((result) => result.data)
51
- .filter((data) => data !== null && data !== undefined);
52
- const errors = results.flatMap((result) => result.error).filter((error) => error !== null);
53
- updateUUIDs(data);
54
- return {
55
- data,
56
- errors,
57
- };
58
- },
59
- }));
51
+ let current = $state.raw([]);
52
+ $effect(() => {
53
+ for (const [name, query] of queries) {
54
+ if (query.isFetching) {
55
+ logs.add(`Fetching pointcloud for ${name}...`);
56
+ }
57
+ else if (query.error) {
58
+ logs.add(`Error fetching pointcloud from ${name}: ${query.error.message}`, 'error');
59
+ }
60
+ }
61
+ });
62
+ $effect(() => {
63
+ const binaries = [];
64
+ for (const [name, query] of queries) {
65
+ const { data } = query;
66
+ if (name && data) {
67
+ binaries.push([name, data]);
68
+ }
69
+ }
70
+ Promise.allSettled(binaries.map(async ([name, uint8array]) => {
71
+ const { positions, colors } = await parsePcdInWorker(new Uint8Array(uint8array));
72
+ return new WorldObject(`${name}:pointcloud`, undefined, name, { center: undefined, geometryType: { case: 'points', value: positions } }, colors ? { colors } : undefined);
73
+ })).then((results) => {
74
+ const worldObjects = [];
75
+ for (const result of results) {
76
+ if (result.status === 'fulfilled') {
77
+ worldObjects.push(result.value);
78
+ }
79
+ else if (result.status === 'rejected') {
80
+ logs.add(result.reason, 'error');
81
+ }
82
+ }
83
+ updateUUIDs(worldObjects);
84
+ current = worldObjects;
85
+ });
86
+ });
60
87
  setContext(key, {
61
88
  get current() {
62
- return queries.current.data;
89
+ return current;
63
90
  },
64
- get errors() {
65
- return queries.current.errors;
91
+ refetch() {
92
+ for (const [, query] of queries) {
93
+ query.refetch();
94
+ }
66
95
  },
67
96
  });
68
97
  };
@@ -1,9 +1,7 @@
1
- import { createResourceClient, useResourceNames } from '@viamrobotics/svelte-sdk';
1
+ import { createResourceClient, createResourceQuery } from '@viamrobotics/svelte-sdk';
2
2
  import { usePartID } from './usePartID.svelte';
3
- import { MotionClient } from '@viamrobotics/sdk';
4
- import { createQuery, queryOptions } from '@tanstack/svelte-query';
3
+ import { MotionClient, Transform } from '@viamrobotics/sdk';
5
4
  import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
6
- import { fromStore, toStore } from 'svelte/store';
7
5
  import { useMotionClient } from './useMotionClient.svelte';
8
6
  import { useEnvironment } from './useEnvironment.svelte';
9
7
  import { observe } from '@threlte/core';
@@ -11,43 +9,40 @@ import { untrack } from 'svelte';
11
9
  import { useFrames } from './useFrames.svelte';
12
10
  import { RefetchRates } from '../components/RefreshRate.svelte';
13
11
  import { useLogs } from './useLogs.svelte';
12
+ import { useResourceByName } from './useResourceByName.svelte';
13
+ import { useRefetchPoses } from './useRefetchPoses';
14
14
  export const usePose = (name, parent) => {
15
15
  const logs = useLogs();
16
16
  const { refreshRates } = useMachineSettings();
17
17
  const partID = usePartID();
18
18
  const motionClient = useMotionClient();
19
- const resources = useResourceNames(() => partID.current);
20
- const resource = $derived(resources.current.find((resource) => resource.name === name()));
21
- const parentResource = $derived(resources.current.find((resource) => resource.name === parent()));
19
+ const currentName = $derived(name());
20
+ const currentParent = $derived(parent());
21
+ const resourceByName = useResourceByName();
22
+ const { addQueryToRefetch } = useRefetchPoses();
23
+ const resource = $derived(resourceByName.current[currentName]);
24
+ const parentResource = $derived(currentParent ? resourceByName.current[currentParent] : undefined);
22
25
  const environment = useEnvironment();
23
26
  const frames = useFrames();
24
27
  const client = createResourceClient(MotionClient, () => partID.current, () => motionClient.current ?? '');
25
28
  const interval = $derived(refreshRates.get(RefreshRates.poses));
26
- const options = $derived(queryOptions({
27
- enabled: interval !== RefetchRates.OFF &&
28
- client.current !== undefined &&
29
- environment.current.viewerMode === 'monitor',
29
+ const resolvedParent = $derived(parentResource?.subtype === 'arm' ? `${parent()}_origin` : parent());
30
+ const query = createResourceQuery(client, 'getPose', () => [currentName, resolvedParent ?? 'world', []], () => ({
31
+ enabled: interval !== RefetchRates.OFF,
30
32
  refetchInterval: interval === RefetchRates.MANUAL ? false : interval,
31
- queryKey: ['getPose', 'partID', partID.current, client.current?.name, name(), parent()],
32
- queryFn: async () => {
33
- if (!client.current) {
34
- throw new Error('No client');
35
- }
36
- logs.add(`Fetching pose for ${name()}...`);
37
- const resolvedParent = parentResource?.subtype === 'arm' ? `${parent()}_origin` : parent();
38
- const pose = await client.current.getPose(name(), resolvedParent ?? 'world', []);
39
- return pose;
40
- },
41
33
  }));
42
- const query = fromStore(createQuery(toStore(() => options)));
43
- observe.pre(() => [environment.current.viewerMode, frames.current], () => {
44
- if (environment.current.viewerMode === 'monitor') {
45
- untrack(() => query.current).refetch();
34
+ $effect(() => addQueryToRefetch(query));
35
+ $effect(() => {
36
+ if (query.isFetching) {
37
+ logs.add(`Fetching pose for ${currentName}...`);
38
+ }
39
+ else if (query.error) {
40
+ logs.add(`Error fetching pose for ${currentName}: ${query.error.message}`, 'error');
46
41
  }
47
42
  });
48
- $effect(() => {
49
- if (query.current.error) {
50
- logs.add(query.current.error.message, 'error');
43
+ observe.pre(() => [environment.current.viewerMode, frames.current], () => {
44
+ if (environment.current.viewerMode === 'monitor') {
45
+ untrack(() => query.refetch());
51
46
  }
52
47
  });
53
48
  return {
@@ -59,7 +54,7 @@ export const usePose = (name, parent) => {
59
54
  if (resource?.subtype === 'arm') {
60
55
  return;
61
56
  }
62
- return query.current.data?.pose;
57
+ return query.data?.pose;
63
58
  },
64
59
  };
65
60
  };
@@ -0,0 +1,8 @@
1
+ type RefetchFn = () => void;
2
+ export declare const useRefetchPoses: () => {
3
+ addQueryToRefetch: (query: {
4
+ refetch: RefetchFn;
5
+ }) => () => boolean;
6
+ refetchPoses: () => void;
7
+ };
8
+ export {};
@@ -0,0 +1,16 @@
1
+ const queries = new Set();
2
+ const addQueryToRefetch = (query) => {
3
+ queries.add(query);
4
+ return () => queries.delete(query);
5
+ };
6
+ const refetchPoses = () => {
7
+ for (const query of queries) {
8
+ query.refetch();
9
+ }
10
+ };
11
+ export const useRefetchPoses = () => {
12
+ return {
13
+ addQueryToRefetch,
14
+ refetchPoses,
15
+ };
16
+ };
@@ -1,6 +1,6 @@
1
1
  import type { ResourceName } from '@viamrobotics/sdk';
2
2
  interface Context {
3
- current: Record<string, ResourceName>;
3
+ current: Record<string, ResourceName | undefined>;
4
4
  }
5
5
  export declare const provideResourceByName: (partID: () => string) => void;
6
6
  export declare const useResourceByName: () => Context;
@@ -39,7 +39,7 @@ const createWorldState = (partID, resourceName) => {
39
39
  let pendingEvents = [];
40
40
  let flushScheduled = false;
41
41
  const listUUIDs = createResourceQuery(client, 'listUUIDs');
42
- const getTransforms = $derived(listUUIDs.current.data?.map((uuid) => {
42
+ const getTransforms = $derived(listUUIDs.data?.map((uuid) => {
43
43
  return createResourceQuery(client, 'getTransform', () => [uuid], () => ({ refetchInterval: false }));
44
44
  }));
45
45
  const changeStream = createResourceStream(client, 'streamTransformChanges', {
@@ -99,10 +99,9 @@ const createWorldState = (partID, resourceName) => {
99
99
  return;
100
100
  if (initialized)
101
101
  return;
102
- const queries = getTransforms.map((query) => query.current);
103
- if (queries.some((query) => query?.isLoading))
102
+ if (getTransforms.some((query) => query?.isLoading))
104
103
  return;
105
- const data = queries
104
+ const data = getTransforms
106
105
  .flatMap((query) => query?.data ?? [])
107
106
  .filter((transform) => transform !== undefined);
108
107
  if (data.length === 0)
@@ -124,9 +123,9 @@ const createWorldState = (partID, resourceName) => {
124
123
  };
125
124
  });
126
125
  $effect.pre(() => {
127
- if (changeStream.current?.data === undefined)
126
+ if (changeStream?.data === undefined)
128
127
  return;
129
- const events = changeStream.current.data.filter((event) => event.transform !== undefined);
128
+ const events = changeStream.data.filter((event) => event.transform !== undefined);
130
129
  if (events.length === 0)
131
130
  return;
132
131
  worker.postMessage({ type: 'change', events });
@@ -142,10 +141,10 @@ const createWorldState = (partID, resourceName) => {
142
141
  return worldObjectsList;
143
142
  },
144
143
  get listUUIDs() {
145
- return listUUIDs.current;
144
+ return listUUIDs;
146
145
  },
147
146
  get getTransforms() {
148
- return getTransforms?.map((query) => query.current);
147
+ return getTransforms;
149
148
  },
150
149
  };
151
150
  };
package/dist/lib.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // Components
2
+ // NOTE: These components should be pure and not use any hooks if you add a new component to export here
3
+ // ensure you write a corresponding unit test to assert the component works in absence of parent providers in /src/lib/__tests__/PureComponents.svelte.spec.ts
2
4
  export { default as Geometry } from './components/Geometry.svelte';
3
5
  export { default as AxesHelper } from './components/AxesHelper.svelte';
4
6
  // Classes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -22,8 +22,6 @@
22
22
  "@sveltejs/vite-plugin-svelte": "6.1.4",
23
23
  "@tailwindcss/forms": "0.5.10",
24
24
  "@tailwindcss/vite": "4.1.13",
25
- "@tanstack/svelte-query": "5.87.1",
26
- "@tanstack/svelte-query-devtools": "5.87.3",
27
25
  "@testing-library/jest-dom": "6.8.0",
28
26
  "@testing-library/svelte": "5.2.8",
29
27
  "@thi.ng/paths": "5.2.21",
@@ -37,9 +35,10 @@
37
35
  "@typescript-eslint/eslint-plugin": "8.42.0",
38
36
  "@typescript-eslint/parser": "8.42.0",
39
37
  "@viamrobotics/prime-core": "0.1.5",
40
- "@viamrobotics/sdk": "0.55.0",
41
- "@viamrobotics/svelte-sdk": "0.7.1",
38
+ "@viamrobotics/sdk": "0.56.0",
39
+ "@viamrobotics/svelte-sdk": "1.0.1",
42
40
  "@vitejs/plugin-basic-ssl": "2.1.0",
41
+ "@vitest/coverage-v8": "^3.2.4",
43
42
  "@zag-js/svelte": "1.22.1",
44
43
  "@zag-js/tree-view": "1.22.1",
45
44
  "camera-controls": "3.1.0",
@@ -74,7 +73,6 @@
74
73
  },
75
74
  "peerDependencies": {
76
75
  "@dimforge/rapier3d-compat": ">=0.17",
77
- "@tanstack/svelte-query": ">=5",
78
76
  "@threlte/core": ">=8",
79
77
  "@threlte/extras": ">=9",
80
78
  "@threlte/rapier": ">=3",
@@ -118,6 +116,9 @@
118
116
  "!dist/**/*.test.*",
119
117
  "!dist/**/*.spec.*"
120
118
  ],
119
+ "dependencies": {
120
+ "@tanstack/svelte-query-devtools": "^6.0.2"
121
+ },
121
122
  "scripts": {
122
123
  "dev": "tsx server/check-bun && bun run server/server.ts",
123
124
  "build": "vite build && npm run prepack",
@@ -129,6 +130,7 @@
129
130
  "test:unit": "vitest",
130
131
  "test:client": "go test ./client/... -count=1",
131
132
  "test": "pnpm test:unit -- --run",
133
+ "test:coverage": "npx vitest run --coverage",
132
134
  "test:e2e": "playwright test",
133
135
  "model-pipeline:run": "node scripts/model-pipeline.js",
134
136
  "release": "changeset publish"