@viamrobotics/motion-tools 0.10.0 → 0.11.1
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/dist/WorldObject.svelte.d.ts +1 -0
- package/dist/WorldObject.svelte.js +44 -2
- package/dist/components/Frame.svelte +1 -2
- package/dist/components/Frame.svelte.d.ts +1 -1
- package/dist/components/Geometry.svelte +1 -1
- package/dist/components/SceneProviders.svelte +2 -1
- package/dist/components/Tree/buildTree.d.ts +2 -4
- package/dist/components/Tree/buildTree.js +2 -4
- package/dist/components/WorldObjects.svelte +2 -2
- package/dist/components/WorldState.svelte +3 -3
- package/dist/components/WorldState.svelte.d.ts +1 -1
- package/dist/hooks/useDrawAPI.svelte.js +28 -16
- package/dist/hooks/useWorldState.svelte.d.ts +54 -15
- package/dist/hooks/useWorldState.svelte.js +111 -57
- package/dist/workers/worldStateWorker.d.ts +1 -0
- package/dist/workers/worldStateWorker.js +109 -0
- package/dist/world-state-messages.d.ts +23 -0
- package/dist/world-state-messages.js +1 -0
- package/package.json +6 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BatchedMesh, Box3, MathUtils, Object3D, Vector3 } from 'three';
|
|
1
|
+
import { BatchedMesh, Box3, Color, MathUtils, Object3D, Vector3, } from 'three';
|
|
2
2
|
import { createPose } from './transform';
|
|
3
3
|
export class WorldObject {
|
|
4
4
|
uuid;
|
|
@@ -18,8 +18,50 @@ export class WorldObject {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
const unwrapValue = (value) => {
|
|
22
|
+
if (!value?.kind)
|
|
23
|
+
return value;
|
|
24
|
+
switch (value.kind.case) {
|
|
25
|
+
case 'numberValue':
|
|
26
|
+
case 'stringValue':
|
|
27
|
+
case 'boolValue':
|
|
28
|
+
return value.kind.value;
|
|
29
|
+
case 'structValue': {
|
|
30
|
+
// Recursively unwrap nested struct
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const [key, val] of Object.entries(value.kind.value.fields || {})) {
|
|
33
|
+
result[key] = unwrapValue(val);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
case 'listValue':
|
|
38
|
+
return value.kind.value.values?.map(unwrapValue) || [];
|
|
39
|
+
case 'nullValue':
|
|
40
|
+
return null;
|
|
41
|
+
default:
|
|
42
|
+
return value.kind.value;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const parseMetadata = (metadata) => {
|
|
46
|
+
let json = {};
|
|
47
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
48
|
+
const unwrappedValue = unwrapValue(v);
|
|
49
|
+
// TODO: Remove special case and add better handling for metadata
|
|
50
|
+
if (k === 'color' && unwrappedValue && typeof unwrappedValue === 'object') {
|
|
51
|
+
const { r, g, b } = unwrappedValue;
|
|
52
|
+
json[k] = new Color().setRGB(r / 255, g / 255, b / 255);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
json = { ...json, [k]: unwrappedValue };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return json;
|
|
59
|
+
};
|
|
21
60
|
export const fromTransform = (transform) => {
|
|
22
|
-
const metadata =
|
|
61
|
+
const metadata = transform.metadata
|
|
62
|
+
? parseMetadata(transform.metadata.fields)
|
|
63
|
+
: {};
|
|
23
64
|
const worldObject = new WorldObject(transform.referenceFrame, transform.poseInObserverFrame?.pose, transform.poseInObserverFrame?.referenceFrame, transform.physicalObject?.geometryType, metadata);
|
|
65
|
+
worldObject.uuid = transform.uuidString;
|
|
24
66
|
return worldObject;
|
|
25
67
|
};
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script module>
|
|
2
|
-
import { Color, type Object3D } from 'three'
|
|
3
|
-
|
|
4
2
|
const colorUtil = new Color()
|
|
5
3
|
</script>
|
|
6
4
|
|
|
@@ -8,6 +6,7 @@
|
|
|
8
6
|
import type { Snippet } from 'svelte'
|
|
9
7
|
import type { WorldObject } from '../WorldObject.svelte'
|
|
10
8
|
import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
|
|
9
|
+
import { Color, type Object3D } from 'three'
|
|
11
10
|
import Geometry from './Geometry.svelte'
|
|
12
11
|
import { useSelected } from '../hooks/useSelection.svelte'
|
|
13
12
|
import { colors, darkenColor } from '../color'
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { provideMotionClient } from '../hooks/useMotionClient.svelte'
|
|
15
15
|
import { provideLogs } from '../hooks/useLogs.svelte'
|
|
16
16
|
import { provideOrigin } from './xr/useOrigin.svelte'
|
|
17
|
-
|
|
17
|
+
import { provideWorldStates } from '../hooks/useWorldState.svelte'
|
|
18
18
|
interface Props {
|
|
19
19
|
children: Snippet<[{ focus: boolean }]>
|
|
20
20
|
}
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
providePointclouds(() => partID.current)
|
|
38
38
|
provideMotionClient(() => partID.current)
|
|
39
39
|
provideObjects()
|
|
40
|
+
provideWorldStates()
|
|
40
41
|
|
|
41
42
|
const { focus } = provideSelection()
|
|
42
43
|
</script>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { useWorldStates } from '../../hooks/useWorldState.svelte';
|
|
1
2
|
import type { WorldObject } from '../../WorldObject.svelte';
|
|
2
3
|
export interface TreeNode {
|
|
3
4
|
id: string;
|
|
@@ -8,7 +9,4 @@ export interface TreeNode {
|
|
|
8
9
|
/**
|
|
9
10
|
* Creates a tree representing parent child / relationships from a set of frames.
|
|
10
11
|
*/
|
|
11
|
-
export declare const buildTreeNodes: (objects: WorldObject[], worldStates:
|
|
12
|
-
name: string;
|
|
13
|
-
objects: WorldObject[];
|
|
14
|
-
}[]) => TreeNode[];
|
|
12
|
+
export declare const buildTreeNodes: (objects: WorldObject[], worldStates: ReturnType<typeof useWorldStates>["current"]) => TreeNode[];
|
|
@@ -25,15 +25,14 @@ export const buildTreeNodes = (objects, worldStates) => {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
for (const worldState of worldStates) {
|
|
28
|
+
for (const worldState of Object.values(worldStates)) {
|
|
29
29
|
const node = {
|
|
30
30
|
name: worldState.name,
|
|
31
31
|
id: worldState.name,
|
|
32
32
|
children: [],
|
|
33
33
|
href: `/world-state/${worldState.name}`,
|
|
34
34
|
};
|
|
35
|
-
|
|
36
|
-
for (const object of worldState.objects) {
|
|
35
|
+
for (const object of worldState.worldObjects) {
|
|
37
36
|
const child = {
|
|
38
37
|
name: object.name,
|
|
39
38
|
id: object.uuid,
|
|
@@ -42,7 +41,6 @@ export const buildTreeNodes = (objects, worldStates) => {
|
|
|
42
41
|
};
|
|
43
42
|
nodeMap.set(object.name, child);
|
|
44
43
|
node.children?.push(child);
|
|
45
|
-
console.log('child', child);
|
|
46
44
|
}
|
|
47
45
|
nodeMap.set(worldState.name, node);
|
|
48
46
|
rootNodes.push(node);
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
</Portal>
|
|
69
69
|
{/each}
|
|
70
70
|
|
|
71
|
-
{#each worldStates.
|
|
72
|
-
<WorldState {
|
|
71
|
+
{#each worldStates.names as { name } (name)}
|
|
72
|
+
<WorldState worldObjects={worldStates.current[name].worldObjects} />
|
|
73
73
|
{/each}
|
|
74
74
|
|
|
75
75
|
{#each points.current as object (object.uuid)}
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
import { WorldObject } from '../WorldObject.svelte'
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
|
-
|
|
9
|
+
worldObjects: WorldObject[]
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
let {
|
|
12
|
+
let { worldObjects }: Props = $props()
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
|
-
{#each
|
|
15
|
+
{#each worldObjects as object (object.uuid)}
|
|
16
16
|
<Portal id={object.referenceFrame}>
|
|
17
17
|
<Frame
|
|
18
18
|
uuid={object.uuid}
|
|
@@ -51,7 +51,7 @@ export const provideDrawAPI = () => {
|
|
|
51
51
|
const origin = new Vector3();
|
|
52
52
|
const vec3 = new Vector3();
|
|
53
53
|
const loader = new GLTFLoader();
|
|
54
|
-
const
|
|
54
|
+
const drawPCD = async (buffer) => {
|
|
55
55
|
const { positions, colors } = await parsePcdInWorker(new Uint8Array(buffer));
|
|
56
56
|
points.push(new WorldObject(`points ${++pointsIndex}`, undefined, undefined, {
|
|
57
57
|
case: 'points',
|
|
@@ -60,12 +60,12 @@ export const provideDrawAPI = () => {
|
|
|
60
60
|
};
|
|
61
61
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
62
|
const drawGeometry = (data, color, parent) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
existingMesh.pose = data.center;
|
|
63
|
+
const result = meshes.find((mesh) => mesh.name === data.label);
|
|
64
|
+
if (result) {
|
|
65
|
+
result.pose = data.center;
|
|
67
66
|
return;
|
|
68
67
|
}
|
|
68
|
+
let geometry;
|
|
69
69
|
if ('mesh' in data) {
|
|
70
70
|
geometry = {
|
|
71
71
|
case: 'mesh',
|
|
@@ -90,14 +90,18 @@ export const provideDrawAPI = () => {
|
|
|
90
90
|
meshes.push(object);
|
|
91
91
|
};
|
|
92
92
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
-
const
|
|
93
|
+
const drawNurbs = (data, color) => {
|
|
94
|
+
const index = nurbs.findIndex(({ name }) => name === data.name);
|
|
95
|
+
if (index !== -1) {
|
|
96
|
+
nurbs.splice(index, 1);
|
|
97
|
+
}
|
|
94
98
|
const controlPoints = data.ControlPts.map((point) => new Vector4(point.x / 1000, point.y / 1000, point.z / 1000));
|
|
95
99
|
const curve = new NURBSCurve(data.Degree, data.Knots, controlPoints);
|
|
96
100
|
const object = new WorldObject(data.name, data.pose, data.parent, { case: 'line', value: new Float32Array() }, { color, points: curve.getPoints(200) });
|
|
97
101
|
nurbs.push(object);
|
|
98
102
|
};
|
|
99
103
|
const batchedArrow = new BatchedArrow();
|
|
100
|
-
const
|
|
104
|
+
const drawPoses = async (reader) => {
|
|
101
105
|
// Read counts
|
|
102
106
|
const nPoints = reader.read();
|
|
103
107
|
const nColors = reader.read();
|
|
@@ -133,13 +137,17 @@ export const provideDrawAPI = () => {
|
|
|
133
137
|
}));
|
|
134
138
|
}
|
|
135
139
|
};
|
|
136
|
-
const
|
|
140
|
+
const drawPoints = async (reader) => {
|
|
137
141
|
// Read label length
|
|
138
142
|
const labelLen = reader.read();
|
|
139
143
|
let label = '';
|
|
140
144
|
for (let i = 0; i < labelLen; i++) {
|
|
141
145
|
label += String.fromCharCode(reader.read());
|
|
142
146
|
}
|
|
147
|
+
const index = points.findIndex(({ name }) => name === label);
|
|
148
|
+
if (index !== -1) {
|
|
149
|
+
points.splice(index, 1);
|
|
150
|
+
}
|
|
143
151
|
// Read counts
|
|
144
152
|
const nPoints = reader.read();
|
|
145
153
|
const nColors = reader.read();
|
|
@@ -184,13 +192,17 @@ export const provideDrawAPI = () => {
|
|
|
184
192
|
value: positions,
|
|
185
193
|
}, metadata));
|
|
186
194
|
};
|
|
187
|
-
const
|
|
195
|
+
const drawLine = async (reader) => {
|
|
188
196
|
// Read label length
|
|
189
197
|
const labelLen = reader.read();
|
|
190
198
|
let label = '';
|
|
191
199
|
for (let i = 0; i < labelLen; i++) {
|
|
192
200
|
label += String.fromCharCode(reader.read());
|
|
193
201
|
}
|
|
202
|
+
const index = lines.findIndex(({ name }) => name === label);
|
|
203
|
+
if (index !== -1) {
|
|
204
|
+
lines.splice(index, 1);
|
|
205
|
+
}
|
|
194
206
|
// Read counts
|
|
195
207
|
const nPoints = reader.read();
|
|
196
208
|
// Read default color
|
|
@@ -226,7 +238,7 @@ export const provideDrawAPI = () => {
|
|
|
226
238
|
i += 1;
|
|
227
239
|
}
|
|
228
240
|
};
|
|
229
|
-
const
|
|
241
|
+
const drawGLTF = async (buffer) => {
|
|
230
242
|
const blob = new Blob([buffer], { type: 'model/gltf-binary' });
|
|
231
243
|
const url = URL.createObjectURL(blob);
|
|
232
244
|
const gltf = await loader.loadAsync(url);
|
|
@@ -311,19 +323,19 @@ export const provideDrawAPI = () => {
|
|
|
311
323
|
const reader = await new Float32Reader().init(event.data);
|
|
312
324
|
const type = reader.read();
|
|
313
325
|
if (type === 0) {
|
|
314
|
-
return
|
|
326
|
+
return drawPoints(reader);
|
|
315
327
|
}
|
|
316
328
|
else if (type === 1) {
|
|
317
|
-
return
|
|
329
|
+
return drawPoses(reader);
|
|
318
330
|
}
|
|
319
331
|
else if (type === 2) {
|
|
320
|
-
return
|
|
332
|
+
return drawLine(reader);
|
|
321
333
|
}
|
|
322
334
|
else if (type === 3) {
|
|
323
|
-
return
|
|
335
|
+
return drawPCD(reader.buffer);
|
|
324
336
|
}
|
|
325
337
|
else {
|
|
326
|
-
return
|
|
338
|
+
return drawGLTF(reader.buffer);
|
|
327
339
|
}
|
|
328
340
|
}
|
|
329
341
|
const data = tryParse(event.data);
|
|
@@ -343,7 +355,7 @@ export const provideDrawAPI = () => {
|
|
|
343
355
|
return drawGeometry(data.geometry, data.color);
|
|
344
356
|
}
|
|
345
357
|
if ('Knots' in data) {
|
|
346
|
-
return
|
|
358
|
+
return drawNurbs(data, data.Color);
|
|
347
359
|
}
|
|
348
360
|
if ('remove' in data) {
|
|
349
361
|
return remove(data.names);
|
|
@@ -1,19 +1,58 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { type TransformWithUUID, ResourceName } from '@viamrobotics/sdk';
|
|
2
|
+
interface Context {
|
|
3
|
+
names: ResourceName[];
|
|
4
|
+
current: Record<string, ReturnType<typeof createWorldState>>;
|
|
5
|
+
}
|
|
6
|
+
export declare const provideWorldStates: () => void;
|
|
7
|
+
export declare const useWorldStates: () => Context;
|
|
8
|
+
export declare const useWorldState: (resourceName: () => string) => {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly transforms: TransformWithUUID[];
|
|
11
|
+
readonly worldObjects: import("../WorldObject.svelte").WorldObject<{
|
|
12
|
+
case: undefined;
|
|
13
|
+
value?: undefined;
|
|
14
|
+
} | {
|
|
15
|
+
case: "sphere";
|
|
16
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Sphere>;
|
|
17
|
+
} | {
|
|
18
|
+
case: "box";
|
|
19
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").RectangularPrism>;
|
|
20
|
+
} | {
|
|
21
|
+
case: "capsule";
|
|
22
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Capsule>;
|
|
23
|
+
} | {
|
|
24
|
+
case: "mesh";
|
|
25
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Mesh>;
|
|
26
|
+
} | {
|
|
27
|
+
case: "pointcloud";
|
|
28
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").PointCloud>;
|
|
29
|
+
}>[];
|
|
30
|
+
readonly listUUIDs: import("@tanstack/svelte-query").QueryObserverResult<string[]>;
|
|
31
|
+
readonly getTransforms: import("@tanstack/svelte-query").QueryObserverResult<TransformWithUUID>[] | undefined;
|
|
12
32
|
};
|
|
13
|
-
|
|
33
|
+
declare const createWorldState: (partID: () => string, resourceName: () => string) => {
|
|
14
34
|
readonly name: string;
|
|
15
|
-
readonly
|
|
35
|
+
readonly transforms: TransformWithUUID[];
|
|
36
|
+
readonly worldObjects: import("../WorldObject.svelte").WorldObject<{
|
|
37
|
+
case: undefined;
|
|
38
|
+
value?: undefined;
|
|
39
|
+
} | {
|
|
40
|
+
case: "sphere";
|
|
41
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Sphere>;
|
|
42
|
+
} | {
|
|
43
|
+
case: "box";
|
|
44
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").RectangularPrism>;
|
|
45
|
+
} | {
|
|
46
|
+
case: "capsule";
|
|
47
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Capsule>;
|
|
48
|
+
} | {
|
|
49
|
+
case: "mesh";
|
|
50
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Mesh>;
|
|
51
|
+
} | {
|
|
52
|
+
case: "pointcloud";
|
|
53
|
+
value: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").PointCloud>;
|
|
54
|
+
}>[];
|
|
16
55
|
readonly listUUIDs: import("@tanstack/svelte-query").QueryObserverResult<string[]>;
|
|
17
|
-
readonly getTransforms: import("@tanstack/svelte-query").QueryObserverResult<
|
|
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>;
|
|
56
|
+
readonly getTransforms: import("@tanstack/svelte-query").QueryObserverResult<TransformWithUUID>[] | undefined;
|
|
19
57
|
};
|
|
58
|
+
export {};
|
|
@@ -1,88 +1,145 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { WorldStateStoreClient, TransformChangeType, } from '@viamrobotics/sdk';
|
|
1
|
+
import { WorldStateStoreClient, TransformChangeType, ResourceName, } from '@viamrobotics/sdk';
|
|
3
2
|
import { createResourceClient, createResourceQuery, createResourceStream, useResourceNames, } from '@viamrobotics/svelte-sdk';
|
|
4
|
-
import { fromTransform
|
|
3
|
+
import { fromTransform } from '../WorldObject.svelte';
|
|
5
4
|
import { usePartID } from './usePartID.svelte';
|
|
6
|
-
|
|
5
|
+
import { setInUnsafe } from '@thi.ng/paths';
|
|
6
|
+
import { getContext, setContext } from 'svelte';
|
|
7
|
+
const key = Symbol('world-state-context');
|
|
8
|
+
const worker = new Worker(new URL('../workers/worldStateWorker.ts', import.meta.url), {
|
|
9
|
+
type: 'module',
|
|
10
|
+
});
|
|
11
|
+
export const provideWorldStates = () => {
|
|
7
12
|
const partID = usePartID();
|
|
8
13
|
const resourceNames = useResourceNames(() => partID.current, 'world_state_store');
|
|
9
|
-
const current = $derived.by(() => resourceNames.current.map(({ name }) =>
|
|
10
|
-
|
|
14
|
+
const current = $derived.by(() => Object.fromEntries(resourceNames.current.map(({ name }) => [
|
|
15
|
+
name,
|
|
16
|
+
createWorldState(() => partID.current, () => name),
|
|
17
|
+
])));
|
|
18
|
+
setContext(key, {
|
|
11
19
|
get names() {
|
|
12
20
|
return resourceNames.current;
|
|
13
21
|
},
|
|
14
22
|
get current() {
|
|
15
23
|
return current;
|
|
16
24
|
},
|
|
17
|
-
};
|
|
25
|
+
});
|
|
18
26
|
};
|
|
19
|
-
export const
|
|
27
|
+
export const useWorldStates = () => {
|
|
28
|
+
return getContext(key);
|
|
29
|
+
};
|
|
30
|
+
export const useWorldState = (resourceName) => {
|
|
31
|
+
return useWorldStates().current[resourceName()];
|
|
32
|
+
};
|
|
33
|
+
const createWorldState = (partID, resourceName) => {
|
|
20
34
|
const client = createResourceClient(WorldStateStoreClient, partID, resourceName);
|
|
21
|
-
let initialized = false;
|
|
35
|
+
let initialized = $state(false);
|
|
36
|
+
let transforms = $state.raw({});
|
|
37
|
+
const transformsList = $derived.by(() => Object.values(transforms));
|
|
38
|
+
const worldObjectsList = $derived.by(() => transformsList.map(fromTransform));
|
|
39
|
+
let pendingEvents = [];
|
|
40
|
+
let flushScheduled = false;
|
|
22
41
|
const listUUIDs = createResourceQuery(client, 'listUUIDs');
|
|
23
42
|
const getTransforms = $derived(listUUIDs.current.data?.map((uuid) => {
|
|
24
43
|
return createResourceQuery(client, 'getTransform', () => [uuid], () => ({ refetchInterval: false }));
|
|
25
44
|
}));
|
|
26
|
-
const changeStream = createResourceStream(client, 'streamTransformChanges'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
const changeStream = createResourceStream(client, 'streamTransformChanges', {
|
|
46
|
+
refetchMode: 'replace',
|
|
47
|
+
});
|
|
48
|
+
const initialize = (initial) => {
|
|
49
|
+
const next = { ...transforms };
|
|
50
|
+
for (const transform of initial) {
|
|
51
|
+
next[transform.uuidString] = transform;
|
|
31
52
|
}
|
|
53
|
+
transforms = next;
|
|
32
54
|
initialized = true;
|
|
33
55
|
};
|
|
34
|
-
|
|
35
|
-
if (
|
|
56
|
+
const applyEvents = (events) => {
|
|
57
|
+
if (events.length === 0)
|
|
36
58
|
return;
|
|
59
|
+
const next = { ...transforms };
|
|
60
|
+
for (const event of events) {
|
|
61
|
+
switch (event.type) {
|
|
62
|
+
case TransformChangeType.ADDED:
|
|
63
|
+
next[event.uuidString] = event.transform;
|
|
64
|
+
break;
|
|
65
|
+
case TransformChangeType.REMOVED:
|
|
66
|
+
delete next[event.uuidString];
|
|
67
|
+
break;
|
|
68
|
+
case TransformChangeType.UPDATED: {
|
|
69
|
+
if (event.changes.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
let toUpdate = next[event.uuidString];
|
|
72
|
+
if (!toUpdate)
|
|
73
|
+
continue;
|
|
74
|
+
for (const [path, value] of event.changes) {
|
|
75
|
+
toUpdate = setInUnsafe(toUpdate, path, value);
|
|
76
|
+
}
|
|
77
|
+
next[event.uuidString] = toUpdate;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
37
81
|
}
|
|
38
|
-
|
|
82
|
+
transforms = next;
|
|
83
|
+
};
|
|
84
|
+
const scheduleFlush = () => {
|
|
85
|
+
if (flushScheduled)
|
|
86
|
+
return;
|
|
87
|
+
flushScheduled = true;
|
|
88
|
+
requestAnimationFrame(() => {
|
|
89
|
+
const toApply = pendingEvents;
|
|
90
|
+
if (toApply.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
applyEvents(toApply);
|
|
93
|
+
flushScheduled = false;
|
|
94
|
+
pendingEvents = [];
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
$effect(() => {
|
|
98
|
+
if (!getTransforms)
|
|
99
|
+
return;
|
|
100
|
+
if (initialized)
|
|
39
101
|
return;
|
|
40
|
-
}
|
|
41
102
|
const queries = getTransforms.map((query) => query.current);
|
|
42
|
-
if (queries.some((query) => query?.isLoading))
|
|
103
|
+
if (queries.some((query) => query?.isLoading))
|
|
43
104
|
return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
objects.push(fromTransform(transform));
|
|
51
|
-
}
|
|
52
|
-
initializeCurrent(objects);
|
|
53
|
-
});
|
|
54
|
-
const processChangeEvent = async (event) => {
|
|
55
|
-
if (event.transform === undefined) {
|
|
105
|
+
const data = queries
|
|
106
|
+
.flatMap((query) => query?.data ?? [])
|
|
107
|
+
.filter((transform) => transform !== undefined);
|
|
108
|
+
if (data.length === 0)
|
|
56
109
|
return;
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
};
|
|
110
|
+
initialize(data);
|
|
111
|
+
});
|
|
75
112
|
$effect(() => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
worker.onmessage = (e) => {
|
|
114
|
+
if (e.data.type !== 'process')
|
|
115
|
+
return;
|
|
116
|
+
const { events } = e.data ?? { events: [] };
|
|
117
|
+
if (events.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
pendingEvents.push(...events);
|
|
120
|
+
scheduleFlush();
|
|
121
|
+
};
|
|
122
|
+
return () => {
|
|
123
|
+
worker.terminate();
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
$effect.pre(() => {
|
|
127
|
+
if (changeStream.current?.data === undefined)
|
|
128
|
+
return;
|
|
129
|
+
const events = changeStream.current.data.filter((event) => event.transform !== undefined);
|
|
130
|
+
if (events.length === 0)
|
|
131
|
+
return;
|
|
132
|
+
worker.postMessage({ type: 'change', events });
|
|
79
133
|
});
|
|
80
134
|
return {
|
|
81
135
|
get name() {
|
|
82
136
|
return resourceName();
|
|
83
137
|
},
|
|
84
|
-
get
|
|
85
|
-
return
|
|
138
|
+
get transforms() {
|
|
139
|
+
return transformsList;
|
|
140
|
+
},
|
|
141
|
+
get worldObjects() {
|
|
142
|
+
return worldObjectsList;
|
|
86
143
|
},
|
|
87
144
|
get listUUIDs() {
|
|
88
145
|
return listUUIDs.current;
|
|
@@ -90,8 +147,5 @@ export const useWorldState = (partID, resourceName) => {
|
|
|
90
147
|
get getTransforms() {
|
|
91
148
|
return getTransforms?.map((query) => query.current);
|
|
92
149
|
},
|
|
93
|
-
get changeStream() {
|
|
94
|
-
return changeStream.current;
|
|
95
|
-
},
|
|
96
150
|
};
|
|
97
151
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { getInUnsafe, toPath } from '@thi.ng/paths';
|
|
2
|
+
import { TransformChangeType, } from '@viamrobotics/sdk';
|
|
3
|
+
const createEntry = (event) => {
|
|
4
|
+
if (!event.transform)
|
|
5
|
+
return undefined;
|
|
6
|
+
switch (event.changeType) {
|
|
7
|
+
case TransformChangeType.ADDED:
|
|
8
|
+
return {
|
|
9
|
+
type: event.changeType,
|
|
10
|
+
uuidString: event.transform.uuidString,
|
|
11
|
+
transform: event.transform,
|
|
12
|
+
};
|
|
13
|
+
case TransformChangeType.REMOVED:
|
|
14
|
+
return {
|
|
15
|
+
type: event.changeType,
|
|
16
|
+
uuidString: event.transform.uuidString,
|
|
17
|
+
};
|
|
18
|
+
case TransformChangeType.UPDATED: {
|
|
19
|
+
const changes = {};
|
|
20
|
+
const paths = toPath(event.updatedFields?.paths ?? []);
|
|
21
|
+
for (const path of paths) {
|
|
22
|
+
changes[path.toString()] = getInUnsafe(event.transform, path);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
type: event.changeType,
|
|
26
|
+
uuidString: event.transform.uuidString,
|
|
27
|
+
transform: event.transform,
|
|
28
|
+
changes,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
self.onmessage = (e) => {
|
|
34
|
+
const { events } = e.data;
|
|
35
|
+
if (events.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const eventsByUUID = new Map();
|
|
38
|
+
for (const event of events) {
|
|
39
|
+
const entry = createEntry(event);
|
|
40
|
+
if (!entry)
|
|
41
|
+
continue;
|
|
42
|
+
const uuid = entry.uuidString;
|
|
43
|
+
const existing = eventsByUUID.get(uuid);
|
|
44
|
+
if (!existing) {
|
|
45
|
+
eventsByUUID.set(uuid, entry);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
switch (entry.type) {
|
|
49
|
+
case TransformChangeType.REMOVED:
|
|
50
|
+
eventsByUUID.set(uuid, entry);
|
|
51
|
+
break;
|
|
52
|
+
case TransformChangeType.ADDED:
|
|
53
|
+
if (existing.type !== TransformChangeType.REMOVED)
|
|
54
|
+
eventsByUUID.set(uuid, entry);
|
|
55
|
+
break;
|
|
56
|
+
case TransformChangeType.UPDATED:
|
|
57
|
+
// merge with existing updated event
|
|
58
|
+
if (existing.type === TransformChangeType.UPDATED) {
|
|
59
|
+
const paths = toPath(event.updatedFields?.paths ?? []);
|
|
60
|
+
if (paths.length === 0)
|
|
61
|
+
continue;
|
|
62
|
+
for (const path of paths) {
|
|
63
|
+
if (!existing.changes)
|
|
64
|
+
existing.changes = {};
|
|
65
|
+
existing.changes[path.toString()] = getInUnsafe(entry.transform, path);
|
|
66
|
+
}
|
|
67
|
+
existing.transform = event.transform;
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const processedEvents = [];
|
|
73
|
+
for (const entry of eventsByUUID.values()) {
|
|
74
|
+
switch (entry.type) {
|
|
75
|
+
case TransformChangeType.ADDED:
|
|
76
|
+
if (!entry.transform)
|
|
77
|
+
continue;
|
|
78
|
+
processedEvents.push({
|
|
79
|
+
type: TransformChangeType.ADDED,
|
|
80
|
+
uuidString: entry.uuidString,
|
|
81
|
+
transform: entry.transform,
|
|
82
|
+
});
|
|
83
|
+
break;
|
|
84
|
+
case TransformChangeType.REMOVED:
|
|
85
|
+
processedEvents.push({
|
|
86
|
+
type: TransformChangeType.REMOVED,
|
|
87
|
+
uuidString: entry.uuidString,
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
case TransformChangeType.UPDATED: {
|
|
91
|
+
const changes = Object.entries(entry.changes ?? {});
|
|
92
|
+
if (changes.length === 0)
|
|
93
|
+
continue;
|
|
94
|
+
processedEvents.push({
|
|
95
|
+
type: TransformChangeType.UPDATED,
|
|
96
|
+
uuidString: entry.uuidString,
|
|
97
|
+
changes,
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const message = {
|
|
104
|
+
type: 'process',
|
|
105
|
+
events: processedEvents,
|
|
106
|
+
};
|
|
107
|
+
self.postMessage(message);
|
|
108
|
+
};
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TransformChangeEvent, TransformChangeType, TransformWithUUID } from '@viamrobotics/sdk';
|
|
2
|
+
export type ChangeMessage = {
|
|
3
|
+
type: 'change';
|
|
4
|
+
events: TransformChangeEvent[];
|
|
5
|
+
};
|
|
6
|
+
export type AddedEvent = {
|
|
7
|
+
type: TransformChangeType.ADDED;
|
|
8
|
+
uuidString: string;
|
|
9
|
+
transform: TransformWithUUID;
|
|
10
|
+
};
|
|
11
|
+
export type RemovedEvent = {
|
|
12
|
+
type: TransformChangeType.REMOVED;
|
|
13
|
+
uuidString: string;
|
|
14
|
+
};
|
|
15
|
+
export type UpdatedEvent = {
|
|
16
|
+
type: TransformChangeType.UPDATED;
|
|
17
|
+
uuidString: string;
|
|
18
|
+
changes: [path: string, value: unknown][];
|
|
19
|
+
};
|
|
20
|
+
export type ProcessMessage = {
|
|
21
|
+
type: 'process';
|
|
22
|
+
events: (AddedEvent | RemovedEvent | UpdatedEvent)[];
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viamrobotics/motion-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Motion visualization with Viam",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -22,8 +22,8 @@
|
|
|
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.
|
|
26
|
-
"@tanstack/svelte-query-devtools": "5.
|
|
25
|
+
"@tanstack/svelte-query": "5.87.1",
|
|
26
|
+
"@tanstack/svelte-query-devtools": "5.87.3",
|
|
27
27
|
"@testing-library/jest-dom": "6.8.0",
|
|
28
28
|
"@testing-library/svelte": "5.2.8",
|
|
29
29
|
"@thi.ng/paths": "5.2.21",
|
|
@@ -37,8 +37,8 @@
|
|
|
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.
|
|
41
|
-
"@viamrobotics/svelte-sdk": "0.6.
|
|
40
|
+
"@viamrobotics/sdk": "0.51.0",
|
|
41
|
+
"@viamrobotics/svelte-sdk": "0.6.1",
|
|
42
42
|
"@vitejs/plugin-basic-ssl": "2.1.0",
|
|
43
43
|
"@zag-js/svelte": "1.22.1",
|
|
44
44
|
"@zag-js/tree-view": "1.22.1",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"format": "prettier --write .",
|
|
124
124
|
"lint": "prettier --check . && eslint .",
|
|
125
125
|
"test:unit": "vitest",
|
|
126
|
-
"test:client": "go test
|
|
126
|
+
"test:client": "go test ./client/... -count=1",
|
|
127
127
|
"test": "pnpm test:unit -- --run",
|
|
128
128
|
"test:e2e": "playwright test",
|
|
129
129
|
"model-pipeline:run": "node scripts/model-pipeline.js",
|