@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.
@@ -12,6 +12,7 @@ export type Geometries = Geometry['geometryType'] | PointsGeometry | LinesGeomet
12
12
  export type Metadata = {
13
13
  colors?: Float32Array;
14
14
  color?: ColorRepresentation;
15
+ opacity?: number;
15
16
  gltf?: {
16
17
  scene: Object3D;
17
18
  };
@@ -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 = { ...transform.metadata?.fields };
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'
@@ -1,6 +1,6 @@
1
- import { type Object3D } from 'three';
2
1
  import type { Snippet } from 'svelte';
3
2
  import type { WorldObject } from '../WorldObject.svelte';
3
+ import { type Object3D } from 'three';
4
4
  interface Props {
5
5
  uuid: string;
6
6
  name: string;
@@ -109,7 +109,7 @@
109
109
  {color}
110
110
  side={geometry.case === 'mesh' ? DoubleSide : FrontSide}
111
111
  transparent
112
- opacity={0.7}
112
+ opacity={metadata.opacity ?? 0.7}
113
113
  />
114
114
 
115
115
  {#if geo}
@@ -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
- console.log('worldState', worldState);
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.current as { name, objects } (name)}
72
- <WorldState {objects} />
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
- objects: WorldObject[]
9
+ worldObjects: WorldObject[]
10
10
  }
11
11
 
12
- let { objects }: Props = $props()
12
+ let { worldObjects }: Props = $props()
13
13
  </script>
14
14
 
15
- {#each objects as object (object.uuid)}
15
+ {#each worldObjects as object (object.uuid)}
16
16
  <Portal id={object.referenceFrame}>
17
17
  <Frame
18
18
  uuid={object.uuid}
@@ -1,6 +1,6 @@
1
1
  import { WorldObject } from '../WorldObject.svelte';
2
2
  interface Props {
3
- objects: WorldObject[];
3
+ worldObjects: WorldObject[];
4
4
  }
5
5
  declare const WorldState: import("svelte").Component<Props, {}, "">;
6
6
  type WorldState = ReturnType<typeof WorldState>;
@@ -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 addPCD = async (buffer) => {
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
- let geometry;
64
- const existingMesh = meshes.find((mesh) => mesh.name === data.label);
65
- if (existingMesh) {
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 addNurbs = (data, color) => {
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 addPoses = async (reader) => {
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 addPoints = async (reader) => {
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 addLine = async (reader) => {
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 addGLTF = async (buffer) => {
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 addPoints(reader);
326
+ return drawPoints(reader);
315
327
  }
316
328
  else if (type === 1) {
317
- return addPoses(reader);
329
+ return drawPoses(reader);
318
330
  }
319
331
  else if (type === 2) {
320
- return addLine(reader);
332
+ return drawLine(reader);
321
333
  }
322
334
  else if (type === 3) {
323
- return addPCD(reader.buffer);
335
+ return drawPCD(reader.buffer);
324
336
  }
325
337
  else {
326
- return addGLTF(reader.buffer);
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 addNurbs(data, data.Color);
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 TransformChangeEvent } from '@viamrobotics/sdk';
2
- import { WorldObject } from '../WorldObject.svelte';
3
- export declare const useWorldStates: () => {
4
- readonly names: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").ResourceName>[];
5
- readonly current: {
6
- readonly name: string;
7
- readonly objects: WorldObject<import("../WorldObject.svelte").Geometries>[];
8
- readonly listUUIDs: import("@tanstack/svelte-query").QueryObserverResult<string[]>;
9
- readonly getTransforms: import("@tanstack/svelte-query").QueryObserverResult<import("@viamrobotics/sdk").TransformWithUUID>[] | undefined;
10
- 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>;
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
- export declare const useWorldState: (partID: () => string, resourceName: () => string) => {
33
+ declare const createWorldState: (partID: () => string, resourceName: () => string) => {
14
34
  readonly name: string;
15
- readonly objects: WorldObject<import("../WorldObject.svelte").Geometries>[];
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<import("@viamrobotics/sdk").TransformWithUUID>[] | undefined;
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 { toPath, getInUnsafe, mutInUnsafe } from '@thi.ng/paths';
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, WorldObject } from '../WorldObject.svelte';
3
+ import { fromTransform } from '../WorldObject.svelte';
5
4
  import { usePartID } from './usePartID.svelte';
6
- export const useWorldStates = () => {
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 }) => useWorldState(() => partID.current, () => name)));
10
- return {
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 useWorldState = (partID, resourceName) => {
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
- const worldObjects = $state({});
28
- const initializeCurrent = (objects) => {
29
- for (const object of objects) {
30
- worldObjects[object.uuid] = object;
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
- $effect(() => {
35
- if (!getTransforms) {
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
- if (initialized) {
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
- const objects = [];
46
- for (const transform of queries.flatMap((query) => query.data) ?? []) {
47
- if (transform === undefined) {
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
- switch (event.changeType) {
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
- for (const event of changeStream.current?.data ?? []) {
77
- void processChangeEvent(event);
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 objects() {
85
- return Object.values(worldObjects);
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.10.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.86.0",
26
- "@tanstack/svelte-query-devtools": "5.86.0",
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.50.0",
41
- "@viamrobotics/svelte-sdk": "0.6.0",
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 ./... -count=1",
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",