@viamrobotics/motion-tools 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -18,3 +18,25 @@ Open the machine config page (bottom right) and enter in connection details to v
18
18
  - remote IP access
19
19
  - ortho points are messed up size-wise
20
20
  - geometries parented to parent
21
+ - end effector pose visualized
22
+ - poses of all frames
23
+ - bounding boxes should include just the thing and not children
24
+ - configure frames from here
25
+ - color pallet for resource to color
26
+
27
+ ## Env files
28
+
29
+ To add a list of connection configs in an `.env.local` file, use the following format:
30
+
31
+ ```
32
+ VITE_CONFIGS='
33
+ {
34
+ "fleet-rover-01": {
35
+ "host": "fleet-rover-01-main.ve4ba7w5qr.viam.cloud",
36
+ "partId": "myPartID",
37
+ "apiKeyId": "myApiKeyId",
38
+ "apiKeyValue": "MyApiKeyValue",
39
+ "signalingAddress": "https://app.viam.com:443"
40
+ }
41
+ }
42
+ ```
@@ -13,6 +13,7 @@
13
13
  import type { Snippet } from 'svelte'
14
14
  import { provideObjects } from '../hooks/useObjects.svelte'
15
15
  import { provideMotionClient } from '../hooks/useMotionClient.svelte'
16
+ import { provideLogs } from '../hooks/useLogs.svelte'
16
17
 
17
18
  interface Props {
18
19
  children: Snippet<[{ focus: boolean }]>
@@ -25,6 +26,7 @@
25
26
  provideTransformControls()
26
27
  provideVisibility()
27
28
  provideRefreshRates()
29
+ provideLogs()
28
30
 
29
31
  provideStaticGeometries()
30
32
  provideShapes()
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { PersistedState } from 'runed'
3
+ import { Icon } from '@viamrobotics/prime-core'
4
+ import type { Snippet } from 'svelte'
5
+
6
+ interface Props {
7
+ name: string
8
+ children: Snippet
9
+ }
10
+
11
+ let { name, children }: Props = $props()
12
+
13
+ const expanded = $derived(new PersistedState(`${name}-expanded`, false))
14
+ </script>
15
+
16
+ <button
17
+ class="border-medium w-full border-t p-2 text-left"
18
+ onclick={() => (expanded.current = !expanded.current)}
19
+ >
20
+ <h3 class="text-default flex items-center gap-1.5">
21
+ <Icon
22
+ name={expanded.current ? 'unfold-more-horizontal' : 'unfold-less-horizontal'}
23
+ label="unfold more icon"
24
+ variant="ghost"
25
+ cx="size-6"
26
+ on:click={() => (expanded.current = !expanded.current)}
27
+ />
28
+ {name}
29
+ </h3>
30
+ </button>
31
+
32
+ {#if expanded.current}
33
+ <div class="border-medium border-t">
34
+ {@render children()}
35
+ </div>
36
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ name: string;
4
+ children: Snippet;
5
+ }
6
+ declare const Drawer: import("svelte").Component<Props, {}, "">;
7
+ type Drawer = ReturnType<typeof Drawer>;
8
+ export default Drawer;
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import { useLogs } from '../../hooks/useLogs.svelte'
3
+ import Drawer from './Drawer.svelte'
4
+
5
+ const logs = useLogs()
6
+ const truncated = $derived(logs.current.slice(0, 100).reverse())
7
+ </script>
8
+
9
+ <Drawer name="Logs">
10
+ <div class="flex h-[250px] w-[240px] flex-col gap-2 overflow-auto p-3">
11
+ {#each truncated as log (log.uuid)}
12
+ <div>
13
+ <div class="flex flex-wrap items-center gap-1.5">
14
+ <div
15
+ class={[
16
+ 'h-2 w-2 rounded-full',
17
+ {
18
+ 'bg-danger-dark': log.level === 'error',
19
+ 'bg-amber-300': log.level === 'warn',
20
+ 'bg-blue-400': log.level === 'info',
21
+ },
22
+ ]}
23
+ ></div>
24
+ <div class="text-subtle-2">{log.timestamp}</div>
25
+ </div>
26
+ <div>{log.message}</div>
27
+ </div>
28
+ {:else}
29
+ No logs
30
+ {/each}
31
+ </div>
32
+ </Drawer>
@@ -0,0 +1,3 @@
1
+ declare const Logs: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Logs = ReturnType<typeof Logs>;
3
+ export default Logs;
@@ -1,32 +1,14 @@
1
1
  <script lang="ts">
2
- import { PersistedState } from 'runed'
3
- import { Icon, Select } from '@viamrobotics/prime-core'
2
+ import { Select } from '@viamrobotics/prime-core'
4
3
  import RefreshRate from '../RefreshRate.svelte'
5
4
  import { useMotionClient } from '../../hooks/useMotionClient.svelte'
6
-
7
- const showSettings = new PersistedState('show-settings', false)
5
+ import Drawer from './Drawer.svelte'
8
6
 
9
7
  const motionClient = useMotionClient()
10
8
  </script>
11
9
 
12
- <button
13
- class="border-medium w-full border-t p-2 text-left"
14
- onclick={() => (showSettings.current = !showSettings.current)}
15
- >
16
- <h3 class="text-default flex items-center gap-1.5">
17
- <Icon
18
- name={showSettings.current ? 'unfold-more-horizontal' : 'unfold-less-horizontal'}
19
- label="unfold more icon"
20
- variant="ghost"
21
- cx="size-6"
22
- on:click={() => (showSettings.current = !showSettings.current)}
23
- />
24
- Settings
25
- </h3>
26
- </button>
27
-
28
- {#if showSettings.current}
29
- <div class="border-medium flex flex-col gap-2 border-t p-3">
10
+ <Drawer name="Settings">
11
+ <div class="flex flex-col gap-2 p-3">
30
12
  <RefreshRate name="Frames">
31
13
  <option value="0">Do not fetch</option>
32
14
  <option value="1">Fetch on reconfigure</option>
@@ -51,4 +33,4 @@
51
33
  </Select>
52
34
  </label>
53
35
  </div>
54
- {/if}
36
+ </Drawer>
@@ -10,6 +10,7 @@
10
10
  import { isEqual } from 'lodash-es'
11
11
  import { useObjects } from '../../hooks/useObjects.svelte'
12
12
  import Settings from './Settings.svelte'
13
+ import Logs from './Logs.svelte'
13
14
 
14
15
  const showTreeview = new PersistedState('show-treeview', true)
15
16
 
@@ -65,6 +66,7 @@
65
66
  />
66
67
  {/key}
67
68
 
69
+ <Logs />
68
70
  <Settings />
69
71
  </div>
70
72
  {/if}
@@ -7,6 +7,9 @@ interface ConnectionConfig {
7
7
  }
8
8
  interface Context {
9
9
  current: ConnectionConfig[];
10
+ add: () => void;
11
+ remove: (index: number) => void;
12
+ isEnvConfig: (config: ConnectionConfig) => boolean;
10
13
  }
11
14
  export declare const provideConnectionConfigs: () => void;
12
15
  export declare const useConnectionConfigs: () => Context;
@@ -1,7 +1,8 @@
1
1
  import { get, set } from 'idb-keyval';
2
2
  import { PersistedState } from 'runed';
3
3
  import { getContext, setContext } from 'svelte';
4
- import { envDialConfigs } from '../../routes/lib/configs';
4
+ import { envConfigs } from '../../routes/lib/configs';
5
+ import { isEqual } from 'lodash-es';
5
6
  const key = Symbol('connection-config-context');
6
7
  const activeConfig = new PersistedState('active-connection-config', 0);
7
8
  export const provideConnectionConfigs = () => {
@@ -14,11 +15,29 @@ export const provideConnectionConfigs = () => {
14
15
  $effect(() => {
15
16
  set('connection-configs', $state.snapshot(connectionConfigs));
16
17
  });
17
- const envConfigs = Object.values(envDialConfigs);
18
+ const merged = $derived([...envConfigs, ...connectionConfigs]);
19
+ const add = () => {
20
+ connectionConfigs.push({
21
+ host: '',
22
+ partId: '',
23
+ apiKeyId: '',
24
+ apiKeyValue: '',
25
+ signalingAddress: '',
26
+ });
27
+ };
28
+ const remove = (index) => {
29
+ connectionConfigs.splice(index - envConfigs.length, 1);
30
+ };
31
+ const isEnvConfig = (config) => {
32
+ return envConfigs.some((value) => isEqual(config, value));
33
+ };
18
34
  setContext(key, {
19
35
  get current() {
20
- return [...envConfigs, ...connectionConfigs];
36
+ return merged;
21
37
  },
38
+ add,
39
+ remove,
40
+ isEnvConfig,
22
41
  });
23
42
  };
24
43
  export const useConnectionConfigs = () => {
@@ -3,12 +3,14 @@ import { useRobotClient, createRobotQuery, useMachineStatus } from '@viamrobotic
3
3
  import { WorldObject } from '../WorldObject';
4
4
  import { useRefreshRates } from './useRefreshRates.svelte';
5
5
  import { observe } from '@threlte/core';
6
+ import { useLogs } from './useLogs.svelte';
6
7
  const key = Symbol('frames-context');
7
8
  export const provideFrames = (partID) => {
8
9
  const refreshRates = useRefreshRates();
9
10
  if (!refreshRates.has('Frames')) {
10
11
  refreshRates.set('Frames', 1);
11
12
  }
13
+ const logs = useLogs();
12
14
  const client = useRobotClient(partID);
13
15
  const query = createRobotQuery(client, 'frameSystemConfig');
14
16
  const machineStatus = useMachineStatus(partID);
@@ -17,6 +19,7 @@ export const provideFrames = (partID) => {
17
19
  observe.pre(() => [revision], () => {
18
20
  if (shouldFetch) {
19
21
  untrack(() => query.current).refetch();
22
+ logs.add('Fetching frames...');
20
23
  }
21
24
  });
22
25
  const current = $derived.by(() => {
@@ -6,8 +6,10 @@ import { fromStore, toStore } from 'svelte/store';
6
6
  import { useRefreshRates } from './useRefreshRates.svelte';
7
7
  import { WorldObject } from '../WorldObject';
8
8
  import { usePersistentUUIDs } from './usePersistentUUIDs.svelte';
9
+ import { useLogs } from './useLogs.svelte';
9
10
  const key = Symbol('geometries-context');
10
11
  export const provideGeometries = (partID) => {
12
+ const logs = useLogs();
11
13
  const refreshRates = useRefreshRates();
12
14
  const arms = useResourceNames(partID, 'arm');
13
15
  const cameras = useResourceNames(partID, 'camera');
@@ -28,6 +30,7 @@ export const provideGeometries = (partID) => {
28
30
  if (!client.current) {
29
31
  throw new Error('No client');
30
32
  }
33
+ logs.add(`Fetching geometries for ${client.current.name}...`);
31
34
  const geometries = await client.current.getGeometries();
32
35
  return { name: client.current.name, geometries };
33
36
  },
@@ -0,0 +1,14 @@
1
+ type Level = 'info' | 'warn' | 'error';
2
+ interface Log {
3
+ uuid: string;
4
+ message: string;
5
+ level: Level;
6
+ timestamp: string;
7
+ }
8
+ interface Context {
9
+ current: Log[];
10
+ add(message: string, level?: Level): void;
11
+ }
12
+ export declare const provideLogs: () => void;
13
+ export declare const useLogs: () => Context;
14
+ export {};
@@ -0,0 +1,29 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { MathUtils } from 'three';
3
+ const key = Symbol('logs-context');
4
+ export const provideLogs = () => {
5
+ const logs = $state([]);
6
+ const intl = new Intl.DateTimeFormat('en-US', {
7
+ dateStyle: 'short',
8
+ timeStyle: 'short',
9
+ });
10
+ setContext(key, {
11
+ get current() {
12
+ return logs;
13
+ },
14
+ add(message, level = 'info') {
15
+ logs.push({
16
+ message,
17
+ level,
18
+ uuid: MathUtils.generateUUID(),
19
+ timestamp: intl.format(Date.now()),
20
+ });
21
+ if (logs.length > 1000) {
22
+ logs.shift();
23
+ }
24
+ },
25
+ });
26
+ };
27
+ export const useLogs = () => {
28
+ return getContext(key);
29
+ };
@@ -7,8 +7,10 @@ import { parsePCD } from '../loaders/pcd';
7
7
  import { useRefreshRates } from './useRefreshRates.svelte';
8
8
  import { WorldObject } from '../WorldObject';
9
9
  import { usePersistentUUIDs } from './usePersistentUUIDs.svelte';
10
+ import { useLogs } from './useLogs.svelte';
10
11
  const key = Symbol('pointcloud-context');
11
12
  export const providePointclouds = (partID) => {
13
+ const logs = useLogs();
12
14
  const refreshRates = useRefreshRates();
13
15
  const cameras = useResourceNames(partID, 'camera');
14
16
  if (!refreshRates.has('Pointclouds')) {
@@ -18,6 +20,7 @@ export const providePointclouds = (partID) => {
18
20
  const options = $derived(clients.map((cameraClient) => {
19
21
  const name = cameraClient.current?.name ?? '';
20
22
  const interval = refreshRates.get('Pointclouds');
23
+ console.log(interval);
21
24
  return queryOptions({
22
25
  enabled: interval !== -1 && cameraClient.current !== undefined,
23
26
  refetchInterval: interval === 0 ? false : interval,
@@ -26,6 +29,7 @@ export const providePointclouds = (partID) => {
26
29
  if (!cameraClient.current) {
27
30
  throw new Error('No camera client');
28
31
  }
32
+ logs.add(`Fetching pointcloud for ${cameraClient.current.name}`);
29
33
  const response = await cameraClient.current.getPointCloud();
30
34
  if (!response)
31
35
  return null;
@@ -21,7 +21,6 @@ export const provideShapes = () => {
21
21
  let poseIndex = 0;
22
22
  let reconnectDelay = 200;
23
23
  const maxReconnectDelay = 5_000;
24
- const { BACKEND_IP, BUN_SERVER_PORT } = globalThis;
25
24
  let ws;
26
25
  const points = $state([]);
27
26
  const meshes = $state([]);
@@ -168,6 +167,7 @@ export const provideShapes = () => {
168
167
  models.push(new WorldObject(gltf.scene.name, undefined, undefined, undefined, { gltf }));
169
168
  URL.revokeObjectURL(url);
170
169
  };
170
+ const { BACKEND_IP, BUN_SERVER_PORT } = globalThis;
171
171
  const scheduleReconnect = () => {
172
172
  setTimeout(() => {
173
173
  reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay);
@@ -227,12 +227,14 @@ export const provideShapes = () => {
227
227
  }
228
228
  };
229
229
  const connect = () => {
230
- const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
231
- ws = new WebSocket(`${protocol}://${BACKEND_IP}:${BUN_SERVER_PORT}/ws`);
232
- ws.onclose = onClose;
233
- ws.onerror = onError;
234
- ws.onopen = onOpen;
235
- ws.onmessage = onMessage;
230
+ if (BACKEND_IP && BUN_SERVER_PORT) {
231
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
232
+ ws = new WebSocket(`${protocol}://${BACKEND_IP}:${BUN_SERVER_PORT}/ws`);
233
+ ws.onclose = onClose;
234
+ ws.onerror = onError;
235
+ ws.onopen = onOpen;
236
+ ws.onmessage = onMessage;
237
+ }
236
238
  };
237
239
  connect();
238
240
  setContext(key, {
@@ -0,0 +1,6 @@
1
+ interface Context {
2
+ anchors: XRAnchor[];
3
+ }
4
+ export declare const provideAnchors: () => void;
5
+ export declare const useAnchors: () => Context;
6
+ export {};
@@ -1 +1,12 @@
1
- "use strict";
1
+ import { getContext, setContext } from 'svelte';
2
+ const key = Symbol('anchors-context');
3
+ export const provideAnchors = () => {
4
+ setContext(key, {
5
+ get anchors() {
6
+ return [];
7
+ },
8
+ });
9
+ };
10
+ export const useAnchors = () => {
11
+ return getContext(key);
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,20 +9,20 @@
9
9
  "@ag-grid-community/core": "^32.3.5",
10
10
  "@ag-grid-community/styles": "^32.3.5",
11
11
  "@changesets/cli": "^2.29.4",
12
- "@dimforge/rapier3d-compat": "^0.17.0",
12
+ "@dimforge/rapier3d-compat": "^0.17.1",
13
13
  "@eslint/compat": "^1.2.9",
14
14
  "@eslint/js": "^9.27.0",
15
15
  "@playwright/test": "^1.52.0",
16
16
  "@skeletonlabs/skeleton": "3.1.3",
17
- "@skeletonlabs/skeleton-svelte": "1.2.2",
17
+ "@skeletonlabs/skeleton-svelte": "1.2.3",
18
18
  "@sveltejs/adapter-static": "^3.0.8",
19
19
  "@sveltejs/kit": "^2.21.1",
20
20
  "@sveltejs/package": "^2.3.11",
21
21
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
22
22
  "@tailwindcss/forms": "^0.5.10",
23
23
  "@tailwindcss/vite": "^4.1.7",
24
- "@tanstack/svelte-query": "^5.76.2",
25
- "@tanstack/svelte-query-devtools": "^5.76.2",
24
+ "@tanstack/svelte-query": "^5.77.2",
25
+ "@tanstack/svelte-query-devtools": "^5.77.2",
26
26
  "@testing-library/jest-dom": "^6.6.3",
27
27
  "@testing-library/svelte": "^5.2.8",
28
28
  "@threlte/core": "^8.0.4",
@@ -39,13 +39,13 @@
39
39
  "@viamrobotics/svelte-sdk": "0.1.4",
40
40
  "@viamrobotics/three": "^0.0.9",
41
41
  "@vitejs/plugin-basic-ssl": "^2.0.0",
42
- "@zag-js/svelte": "1.12.4",
43
- "@zag-js/tree-view": "1.12.4",
42
+ "@zag-js/svelte": "1.13.1",
43
+ "@zag-js/tree-view": "1.13.1",
44
44
  "camera-controls": "^2.10.1",
45
45
  "eslint": "^9.27.0",
46
46
  "eslint-config-prettier": "^10.1.5",
47
47
  "eslint-plugin-svelte": "^3.9.0",
48
- "globals": "^16.1.0",
48
+ "globals": "^16.2.0",
49
49
  "idb-keyval": "^6.2.2",
50
50
  "jsdom": "^26.1.0",
51
51
  "lodash-es": "^4.17.21",
@@ -55,7 +55,7 @@
55
55
  "prettier-plugin-tailwindcss": "^0.6.11",
56
56
  "publint": "^0.3.12",
57
57
  "runed": "^0.28.0",
58
- "svelte": "5.33.0",
58
+ "svelte": "5.33.4",
59
59
  "svelte-check": "^4.2.1",
60
60
  "svelte-virtuallists": "^1.4.2",
61
61
  "tailwindcss": "^4.1.7",