@viamrobotics/motion-tools 0.11.2 → 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 +38 -4
- package/dist/components/App.svelte +8 -0
- package/dist/components/FileDrop.svelte +118 -0
- package/dist/components/FileDrop.svelte.d.ts +3 -0
- package/dist/components/MeasureTool.svelte +13 -3
- package/dist/hooks/useDrawAPI.svelte.d.ts +1 -0
- package/dist/hooks/useDrawAPI.svelte.js +3 -0
- package/dist/hooks/usePose.svelte.js +1 -1
- package/dist/hooks/usePoses.svelte.js +1 -1
- package/dist/hooks/useWorldState.svelte.js +2 -3
- package/dist/loaders/pcd/index.js +22 -11
- package/dist/loaders/pcd/worker.d.ts +2 -0
- package/dist/loaders/pcd/worker.js +5 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,10 +4,44 @@
|
|
|
4
4
|
|
|
5
5
|
### Getting started
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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>
|
|
@@ -76,15 +76,24 @@
|
|
|
76
76
|
|
|
77
77
|
{#if enabled}
|
|
78
78
|
{#if intersection}
|
|
79
|
-
<DotSprite
|
|
79
|
+
<DotSprite
|
|
80
|
+
position={intersection?.point.toArray()}
|
|
81
|
+
opacity={0.5}
|
|
82
|
+
/>
|
|
80
83
|
{/if}
|
|
81
84
|
|
|
82
85
|
{#if p1}
|
|
83
|
-
<DotSprite
|
|
86
|
+
<DotSprite
|
|
87
|
+
position={p1.toArray()}
|
|
88
|
+
opacity={0.5}
|
|
89
|
+
/>
|
|
84
90
|
{/if}
|
|
85
91
|
|
|
86
92
|
{#if p2}
|
|
87
|
-
<DotSprite
|
|
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[];
|
|
@@ -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
|
|
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
|
-
|
|
2
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
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,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
|
+
"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.
|
|
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",
|