@viamrobotics/motion-tools 0.11.3 → 0.11.4

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
@@ -4,10 +4,44 @@
4
4
 
5
5
  ### Getting started
6
6
 
7
- 1. [Install pnpm](https://pnpm.io/installation)
8
- 2. [Install bun](https://bun.sh/docs/installation)
9
- 3. Install dependencies: `pnpm i`
10
- 4. Run local app server: `pnpm dev`
7
+ #### Quick Setup (Recommended)
8
+
9
+ The easiest way to get started is using our automated setup script:
10
+
11
+ ```bash
12
+ make setup
13
+ ```
14
+
15
+ This single command will:
16
+
17
+ 1. Install and configure **nvm** (Node Version Manager)
18
+ 2. Install the latest **Node.js LTS** version via nvm
19
+ 3. Install **pnpm** package manager
20
+ 4. Install **bun** runtime
21
+ 5. Install all project dependencies
22
+
23
+ After setup completes, start the development server:
24
+
25
+ ```bash
26
+ make up
27
+ ```
28
+
29
+ #### Manual Setup
30
+
31
+ If you prefer to install dependencies manually:
32
+
33
+ 1. [Install nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
34
+ 2. Install Node.js LTS: `nvm install --lts && nvm use --lts`
35
+ 3. [Install pnpm](https://pnpm.io/installation)
36
+ 4. [Install bun](https://bun.sh/docs/installation)
37
+ 5. Install dependencies: `pnpm i`
38
+ 6. Run local app server: `pnpm dev`
39
+
40
+ #### Available Make Commands
41
+
42
+ - `make setup` - Complete development environment setup
43
+ - `make up` - Start the development server
44
+ - `make help` - Show available commands
11
45
 
12
46
  ### Running the visualizer
13
47
 
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte'
3
3
  import { Canvas } from '@threlte/core'
4
4
  import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools'
5
+ import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
5
6
 
6
7
  import Scene from './Scene.svelte'
7
8
  import TreeContainer from './Tree/TreeContainer.svelte'
@@ -13,6 +14,7 @@
13
14
  import Dashboard from './dashboard/Dashboard.svelte'
14
15
  import { domPortal } from '../portal'
15
16
  import { provideSettings } from '../hooks/useSettings.svelte'
17
+ import FileDrop from './FileDrop.svelte'
16
18
 
17
19
  interface Props {
18
20
  partID?: string
@@ -30,6 +32,8 @@
30
32
 
31
33
  createPartIDContext(() => partID)
32
34
 
35
+ provideToast()
36
+
33
37
  let root = $state.raw<HTMLElement>()
34
38
  </script>
35
39
 
@@ -57,8 +61,12 @@
57
61
  {#if !focus}
58
62
  <TreeContainer {@attach domPortal(root)} />
59
63
  {/if}
64
+
65
+ <FileDrop {@attach domPortal(root)} />
60
66
  {/snippet}
61
67
  </SceneProviders>
62
68
  </World>
63
69
  </Canvas>
70
+
71
+ <ToastContainer />
64
72
  </div>
@@ -0,0 +1,118 @@
1
+ <script lang="ts">
2
+ import { useDrawAPI } from '../hooks/useDrawAPI.svelte'
3
+ import { parsePcdInWorker, WorldObject } from '../lib'
4
+ import { useToast, ToastVariant } from '@viamrobotics/prime-core'
5
+
6
+ let { ...rest } = $props()
7
+
8
+ const { addPoints } = useDrawAPI()
9
+
10
+ type DropStates = 'inactive' | 'hovering' | 'loading'
11
+
12
+ let dropState = $state<DropStates>('inactive')
13
+
14
+ // prevent default to allow drop
15
+ const ondragenter = (event: DragEvent) => {
16
+ event.preventDefault()
17
+ dropState = 'hovering'
18
+ }
19
+
20
+ // prevent default to allow drop
21
+ const ondragover = (event: DragEvent) => {
22
+ event.preventDefault()
23
+ }
24
+
25
+ const ondragleave = (event: DragEvent) => {
26
+ // only deactivate if really leaving the window
27
+ if (event.relatedTarget === null) {
28
+ dropState = 'inactive'
29
+ }
30
+ }
31
+
32
+ const toast = useToast()
33
+
34
+ const ondrop = (event: DragEvent) => {
35
+ event.preventDefault()
36
+
37
+ if (event.dataTransfer === null) {
38
+ return
39
+ }
40
+
41
+ let completed = 0
42
+
43
+ const { files } = event.dataTransfer
44
+
45
+ for (const file of files) {
46
+ const ext = file.name.split('.').at(-1)
47
+
48
+ if (ext !== '.pcd') {
49
+ toast({
50
+ message: `.${ext} is not a supported file type.`,
51
+ variant: ToastVariant.Danger,
52
+ })
53
+
54
+ continue
55
+ }
56
+
57
+ const reader = new FileReader()
58
+
59
+ reader.addEventListener('loadend', () => {
60
+ completed += 1
61
+
62
+ if (completed === files.length) {
63
+ dropState = 'inactive'
64
+ }
65
+ })
66
+
67
+ reader.addEventListener('error', () => {
68
+ toast({ message: `${file.name} failed to load.`, variant: ToastVariant.Danger })
69
+ })
70
+
71
+ reader.addEventListener('load', async (event) => {
72
+ const arrayBuffer = event.target?.result
73
+
74
+ if (!arrayBuffer || typeof arrayBuffer === 'string') {
75
+ return
76
+ }
77
+
78
+ const result = await parsePcdInWorker(new Uint8Array(arrayBuffer))
79
+
80
+ addPoints(
81
+ new WorldObject(
82
+ file.name,
83
+ undefined,
84
+ undefined,
85
+ {
86
+ case: 'points',
87
+ value: result.positions,
88
+ },
89
+ result.colors ? { colors: result.colors } : undefined
90
+ )
91
+ )
92
+ toast({ message: `Loaded ${file.name}`, variant: ToastVariant.Success })
93
+ })
94
+
95
+ reader.readAsArrayBuffer(file)
96
+
97
+ dropState = 'loading'
98
+ }
99
+ }
100
+ </script>
101
+
102
+ <svelte:window
103
+ {ondragenter}
104
+ {ondragleave}
105
+ {ondragover}
106
+ />
107
+
108
+ <div
109
+ class={{
110
+ 'fixed inset-0 z-9999 ': true,
111
+ 'pointer-events-none': dropState === 'inactive',
112
+ 'bg-black/10': dropState !== 'inactive',
113
+ }}
114
+ role="region"
115
+ aria-label="File drop zone"
116
+ {ondrop}
117
+ {...rest}
118
+ ></div>
@@ -0,0 +1,3 @@
1
+ declare const FileDrop: import("svelte").Component<Record<string, any>, {}, "">;
2
+ type FileDrop = ReturnType<typeof FileDrop>;
3
+ export default FileDrop;
@@ -76,15 +76,24 @@
76
76
 
77
77
  {#if enabled}
78
78
  {#if intersection}
79
- <DotSprite position={intersection?.point.toArray()} />
79
+ <DotSprite
80
+ position={intersection?.point.toArray()}
81
+ opacity={0.5}
82
+ />
80
83
  {/if}
81
84
 
82
85
  {#if p1}
83
- <DotSprite position={p1.toArray()} />
86
+ <DotSprite
87
+ position={p1.toArray()}
88
+ opacity={0.5}
89
+ />
84
90
  {/if}
85
91
 
86
92
  {#if p2}
87
- <DotSprite position={p2.toArray()} />
93
+ <DotSprite
94
+ position={p2.toArray()}
95
+ opacity={0.5}
96
+ />
88
97
  {/if}
89
98
 
90
99
  {#if p1 && p2}
@@ -98,6 +107,7 @@
98
107
  width={2.5}
99
108
  depthTest={false}
100
109
  color="black"
110
+ opacity={0.5}
101
111
  attenuate={false}
102
112
  transparent
103
113
  />
@@ -3,6 +3,7 @@ import { BatchedArrow } from '../three/BatchedArrow';
3
3
  import { WorldObject, type PointsGeometry } from '../WorldObject.svelte';
4
4
  type ConnectionStatus = 'connecting' | 'open' | 'closed';
5
5
  interface Context {
6
+ addPoints(worldObject: WorldObject<PointsGeometry>): void;
6
7
  points: WorldObject<PointsGeometry>[];
7
8
  lines: WorldObject[];
8
9
  meshes: WorldObject[];
@@ -379,6 +379,9 @@ export const provideDrawAPI = () => {
379
379
  get points() {
380
380
  return points;
381
381
  },
382
+ addPoints(worldObject) {
383
+ points.push(worldObject);
384
+ },
382
385
  get lines() {
383
386
  return lines;
384
387
  },
@@ -28,7 +28,7 @@ export const usePose = (name, parent) => {
28
28
  if (!client.current || !resource) {
29
29
  throw new Error('No client');
30
30
  }
31
- const pose = await client.current.getPose(resource, parent() ?? 'world', []);
31
+ const pose = await client.current.getPose(resource.name, parent() ?? 'world', []);
32
32
  return pose;
33
33
  },
34
34
  }));
@@ -26,7 +26,7 @@ export const providePoses = (partID) => {
26
26
  throw new Error('No client');
27
27
  }
28
28
  const promises = components.map((component) => {
29
- return client.getPose(component, 'world', []);
29
+ return client.getPose(component.name, 'world', []);
30
30
  });
31
31
  const results = await Promise.allSettled(promises);
32
32
  return results
@@ -4,10 +4,9 @@ import { fromTransform } from '../WorldObject.svelte';
4
4
  import { usePartID } from './usePartID.svelte';
5
5
  import { setInUnsafe } from '@thi.ng/paths';
6
6
  import { getContext, setContext } from 'svelte';
7
+ import WorldStateWorker from '../workers/worldStateWorker?worker';
7
8
  const key = Symbol('world-state-context');
8
- const worker = new Worker(new URL('../workers/worldStateWorker.ts', import.meta.url), {
9
- type: 'module',
10
- });
9
+ const worker = new WorldStateWorker();
11
10
  export const provideWorldStates = () => {
12
11
  const partID = usePartID();
13
12
  const resourceNames = useResourceNames(() => partID.current, 'world_state_store');
@@ -1,14 +1,25 @@
1
- const worker = new Worker(new URL('./worker', import.meta.url), { type: 'module' });
2
- export const parsePcdInWorker = async (data) => {
1
+ import PCDWorker from './worker?worker';
2
+ const worker = new PCDWorker();
3
+ let requestId = 0;
4
+ const pending = new Map();
5
+ worker.addEventListener('message', (event) => {
6
+ const { id, ...rest } = event.data;
7
+ const promise = pending.get(id);
8
+ if (!promise) {
9
+ return;
10
+ }
11
+ pending.delete(id);
12
+ if ('error' in rest) {
13
+ promise.reject(rest.error);
14
+ }
15
+ else {
16
+ promise.resolve(rest);
17
+ }
18
+ });
19
+ export const parsePcdInWorker = (data) => {
3
20
  return new Promise((resolve, reject) => {
4
- const onMessage = (event) => {
5
- worker.removeEventListener('message', onMessage);
6
- if ('error' in event.data) {
7
- return reject(event.data.error);
8
- }
9
- resolve(event.data);
10
- };
11
- worker.addEventListener('message', onMessage);
12
- worker.postMessage({ data }, [data.buffer]);
21
+ const id = ++requestId;
22
+ pending.set(id, { resolve, reject });
23
+ worker.postMessage({ id, data }, [data.buffer]);
13
24
  });
14
25
  };
@@ -1,7 +1,9 @@
1
1
  export interface SuccessMessage {
2
+ id: number;
2
3
  positions: Float32Array<ArrayBuffer>;
3
4
  colors: Float32Array | null;
4
5
  }
5
6
  export type Message = SuccessMessage | {
7
+ id: number;
6
8
  error: string;
7
9
  };
@@ -1,10 +1,9 @@
1
- // worker.js
2
1
  import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader.js';
3
2
  const loader = new PCDLoader();
4
3
  self.onmessage = async (event) => {
5
- const { data } = event.data;
4
+ const { data, id } = event.data;
6
5
  if (!(data instanceof Uint8Array)) {
7
- postMessage({ error: 'Invalid data format' });
6
+ postMessage({ id, error: 'Invalid data format' });
8
7
  return;
9
8
  }
10
9
  try {
@@ -12,13 +11,13 @@ self.onmessage = async (event) => {
12
11
  if (pcd.geometry) {
13
12
  const positions = pcd.geometry.attributes.position.array;
14
13
  const colors = pcd.geometry.attributes.color?.array ?? null;
15
- postMessage({ positions, colors }, colors ? [positions.buffer, colors.buffer] : [positions.buffer]);
14
+ postMessage({ positions, colors, id }, colors ? [positions.buffer, colors.buffer] : [positions.buffer]);
16
15
  }
17
16
  else {
18
- postMessage({ error: 'Failed to extract geometry' });
17
+ postMessage({ id, error: 'Failed to extract geometry' });
19
18
  }
20
19
  }
21
20
  catch (error) {
22
- postMessage({ error: error.message });
21
+ postMessage({ id, error: error.message });
23
22
  }
24
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "@typescript-eslint/eslint-plugin": "8.42.0",
38
38
  "@typescript-eslint/parser": "8.42.0",
39
39
  "@viamrobotics/prime-core": "0.1.5",
40
- "@viamrobotics/sdk": "0.51.0",
40
+ "@viamrobotics/sdk": "0.52.0",
41
41
  "@viamrobotics/svelte-sdk": "0.6.1",
42
42
  "@vitejs/plugin-basic-ssl": "2.1.0",
43
43
  "@zag-js/svelte": "1.22.1",