@viamrobotics/motion-tools 1.18.0 → 1.19.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.
@@ -5,7 +5,6 @@ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
5
5
  import { ColorFormat } from '../buf/draw/v1/metadata_pb';
6
6
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
7
7
  import { traits, useWorld } from '../ecs';
8
- import { updateGeometryTrait } from '../ecs/traits';
9
8
  import { parsePcdInWorker } from '../lib';
10
9
  import { createPose } from '../transform';
11
10
  import { useEnvironment } from './useEnvironment.svelte';
@@ -162,20 +161,18 @@ export const providePointcloudObjects = (partID) => {
162
161
  const existing = entities.get(geometryLabel);
163
162
  if (existing) {
164
163
  existing.set(traits.Center, center);
165
- updateGeometryTrait(existing, geometry);
164
+ traits.updateGeometryTrait(existing, geometry);
166
165
  }
167
166
  else {
168
167
  const entityTraits = [
169
168
  traits.Name(geometryLabel),
169
+ ...traits.getParentTrait(geometriesInFrame.referenceFrame),
170
170
  traits.Center(center),
171
171
  traits.GeometriesAPI,
172
172
  traits.Geometry(geometry),
173
173
  traits.Opacity(0.2),
174
174
  traits.Color({ r: 0, g: 1, b: 0 }),
175
175
  ];
176
- if (geometriesInFrame.referenceFrame) {
177
- entityTraits.push(traits.Parent(geometriesInFrame.referenceFrame));
178
- }
179
176
  const entity = world.spawn(...entityTraits);
180
177
  entities.set(geometryLabel, entity);
181
178
  }
@@ -1,11 +1,12 @@
1
1
  import { useThrelte } from '@threlte/core';
2
- import { TransformChangeType, WorldStateStoreClient, } from '@viamrobotics/sdk';
2
+ import { Struct, TransformChangeType, WorldStateStoreClient, } from '@viamrobotics/sdk';
3
3
  import { createResourceClient, createResourceQuery, createResourceStream, useResourceNames, } from '@viamrobotics/svelte-sdk';
4
- import { drawTransform } from '../draw';
4
+ import { asFloat32Array, inMeters } from '../buffer';
5
+ import { createChunkLoader } from '../chunking';
6
+ import { drawTransform, updateMetadata } from '../draw';
5
7
  import { traits, useWorld } from '../ecs';
6
- import { createBox, createCapsule, createSphere } from '../geometry';
7
8
  import { isPointCloud } from '../geometry';
8
- import { parsePlyInput } from '../ply';
9
+ import { metadataFromStruct } from '../metadata';
9
10
  import { createPose } from '../transform';
10
11
  import { usePartID } from './usePartID.svelte';
11
12
  export const provideWorldStates = () => {
@@ -24,16 +25,90 @@ export const provideWorldStates = () => {
24
25
  };
25
26
  });
26
27
  };
28
+ const decodeBase64 = (encoded) => {
29
+ const binary = atob(encoded);
30
+ const bytes = new Uint8Array(binary.length);
31
+ for (let i = 0; i < binary.length; i++) {
32
+ bytes[i] = binary.charCodeAt(i);
33
+ }
34
+ return bytes;
35
+ };
36
+ /**
37
+ * Unpacks a `get_entity_chunk` DoCommand response into the shape the shared
38
+ * chunk loader expects. The world-state store sends binary buffers as base64
39
+ * strings inside a JSON `Struct`, which is why this adapter exists.
40
+ *
41
+ * Request:
42
+ * { "command": "get_entity_chunk", "uuid": "<uuid-string>", "start": <element-offset> }
43
+ *
44
+ * Response:
45
+ * {
46
+ * "entity": {
47
+ * "metadata": {
48
+ * "colors": "<base64 Uint8Array>" (optional),
49
+ * "opacities": "<base64 Uint8Array>" (optional)
50
+ * },
51
+ * "physical_object": {
52
+ * "points": { "positions": "<base64 Float32Array>" }
53
+ * }
54
+ * },
55
+ * "start": <number>,
56
+ * "done": <boolean>
57
+ * }
58
+ */
59
+ const decodeWorldStateChunk = (response, fallbackStart) => {
60
+ const fields = response;
61
+ const done = fields['done'] === true;
62
+ const start = typeof fields['start'] === 'number' ? fields['start'] : fallbackStart;
63
+ const chunkEntity = fields['entity'];
64
+ if (!chunkEntity)
65
+ return null;
66
+ const physicalObject = chunkEntity['physical_object'];
67
+ const points = physicalObject?.['points'];
68
+ const encodedPositions = points?.['positions'];
69
+ if (typeof encodedPositions !== 'string' || encodedPositions.length === 0)
70
+ return null;
71
+ const positions = asFloat32Array(decodeBase64(encodedPositions), inMeters);
72
+ const metadata = chunkEntity['metadata'];
73
+ const encodedColors = metadata?.['colors'];
74
+ const colors = typeof encodedColors === 'string' && encodedColors.length > 0
75
+ ? decodeBase64(encodedColors)
76
+ : undefined;
77
+ const encodedOpacities = metadata?.['opacities'];
78
+ const opacities = typeof encodedOpacities === 'string' && encodedOpacities.length > 0
79
+ ? decodeBase64(encodedOpacities)
80
+ : undefined;
81
+ return { start, positions, colors, opacities, done };
82
+ };
27
83
  const createWorldState = (client) => {
28
84
  const { invalidate } = useThrelte();
29
85
  const world = useWorld();
30
86
  const entities = new Map();
87
+ const chunkLoader = createChunkLoader({
88
+ world,
89
+ invalidate,
90
+ fetchChunk: async (uuid, start, signal) => {
91
+ const activeClient = client.current;
92
+ if (!activeClient)
93
+ return null;
94
+ const response = await activeClient.doCommand(Struct.fromJson({
95
+ command: 'get_entity_chunk',
96
+ uuid,
97
+ start,
98
+ }));
99
+ if (signal.aborted)
100
+ return null;
101
+ return decodeWorldStateChunk(response, start);
102
+ },
103
+ });
31
104
  const spawnEntity = (transform) => {
32
105
  if (entities.has(transform.uuidString)) {
33
106
  return;
34
107
  }
35
108
  const entity = drawTransform(world, transform, traits.WorldStateStoreAPI, { removable: false });
36
109
  entities.set(transform.uuidString, entity);
110
+ const parsedMetadata = metadataFromStruct(transform.metadata?.fields);
111
+ chunkLoader.start(transform.uuidString, entity, parsedMetadata);
37
112
  if (isPointCloud(transform.physicalObject?.geometryType))
38
113
  invalidate();
39
114
  };
@@ -50,28 +125,25 @@ const createWorldState = (client) => {
50
125
  const entity = entities.get(transform.uuidString);
51
126
  if (!entity)
52
127
  return;
128
+ let metadataDirty = false;
53
129
  for (const path of changes) {
54
130
  if (typeof path === 'string') {
55
131
  if (path.startsWith('poseInObserverFrame.pose')) {
56
132
  entity.set(traits.Pose, transform.poseInObserverFrame?.pose ?? createPose());
57
133
  }
58
134
  else if (path.startsWith('physicalObject') && transform.physicalObject) {
59
- const { geometryType } = transform.physicalObject;
60
- if (geometryType.case === 'box') {
61
- entity.set(traits.Box, createBox(geometryType.value));
62
- }
63
- else if (geometryType.case === 'capsule') {
64
- entity.set(traits.Capsule, createCapsule(geometryType.value));
65
- }
66
- else if (geometryType.case === 'sphere') {
67
- entity.set(traits.Sphere, createSphere(geometryType.value));
68
- }
69
- else if (geometryType.case === 'mesh') {
70
- entity.set(traits.BufferGeometry, parsePlyInput(geometryType.value.mesh));
71
- }
135
+ traits.updateGeometryTrait(entity, transform.physicalObject);
136
+ }
137
+ else if (path.startsWith('metadata')) {
138
+ metadataDirty = true;
72
139
  }
73
140
  }
74
141
  }
142
+ if (metadataDirty) {
143
+ updateMetadata(entity, metadataFromStruct(transform.metadata?.fields), {
144
+ pointCloud: isPointCloud(transform.physicalObject?.geometryType),
145
+ });
146
+ }
75
147
  };
76
148
  let initialized = false;
77
149
  let flushScheduled = false;
@@ -176,6 +248,7 @@ const createWorldState = (client) => {
176
248
  scheduleFlush();
177
249
  });
178
250
  return () => {
251
+ chunkLoader.dispose();
179
252
  for (const [, entity] of entities) {
180
253
  if (world.has(entity)) {
181
254
  entity.destroy();
package/dist/metadata.js CHANGED
@@ -5,7 +5,8 @@ export const isMetadataField = (key) => {
5
5
  key === 'color_format' ||
6
6
  key === 'opacities' ||
7
7
  key === 'show_axes_helper' ||
8
- key === 'invisible');
8
+ key === 'invisible' ||
9
+ key === 'chunks');
9
10
  };
10
11
  /**
11
12
  * Extracts typed {@link Metadata} from a proto `Struct` fields map.
@@ -65,6 +66,17 @@ export const metadataFromStruct = (fields = {}) => {
65
66
  }
66
67
  break;
67
68
  }
69
+ case 'chunks': {
70
+ if (typeof unwrappedValue === 'object' && unwrappedValue !== null) {
71
+ const obj = unwrappedValue;
72
+ json.chunks = {
73
+ chunkSize: typeof obj['chunk_size'] === 'number' ? obj['chunk_size'] : 0,
74
+ total: typeof obj['total'] === 'number' ? obj['total'] : 0,
75
+ stride: typeof obj['stride'] === 'number' ? obj['stride'] : 0,
76
+ };
77
+ }
78
+ break;
79
+ }
68
80
  }
69
81
  }
70
82
  return json;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -165,8 +165,8 @@
165
165
  "test:draw": "go test ./draw/... -count=1",
166
166
  "test": "pnpm test:unit -- --run",
167
167
  "test:coverage": "npx vitest run --coverage",
168
- "test:e2e": "playwright test",
169
- "test:e2e-ui": "playwright test --ui",
168
+ "test:e2e": "./e2e/setup.sh && playwright test",
169
+ "test:e2e-ui": "./e2e/setup.sh && playwright test --ui",
170
170
  "vet:draw": "go vet ./draw/...",
171
171
  "vet:client": "go vet ./client/...",
172
172
  "vet": "pnpm vet:draw && pnpm vet:client",