@viamrobotics/motion-tools 1.18.1 → 1.19.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/attribute.d.ts +2 -0
- package/dist/attribute.js +47 -1
- package/dist/buf/draw/v1/metadata_pb.d.ts +39 -0
- package/dist/buf/draw/v1/metadata_pb.js +55 -0
- package/dist/chunking.d.ts +26 -0
- package/dist/chunking.js +59 -0
- package/dist/color.d.ts +1 -0
- package/dist/color.js +11 -3
- package/dist/components/Entities/Arrows/Arrows.svelte +1 -1
- package/dist/components/Entities/Entities.svelte +1 -0
- package/dist/components/Entities/Mesh.svelte +21 -5
- package/dist/components/Entities/Pose.svelte +3 -1
- package/dist/components/PCD.svelte +6 -2
- package/dist/components/Scene.svelte +0 -1
- package/dist/components/overlay/Details.svelte +2 -0
- package/dist/components/overlay/left-pane/TreeNode.svelte +65 -36
- package/dist/draw.d.ts +6 -0
- package/dist/draw.js +66 -45
- package/dist/ecs/traits.d.ts +12 -2
- package/dist/ecs/traits.js +80 -1
- package/dist/hooks/useDrawAPI.svelte.js +11 -12
- package/dist/hooks/useFrames.svelte.js +60 -22
- package/dist/hooks/useGeometries.svelte.js +11 -2
- package/dist/hooks/useLogs.svelte.js +3 -3
- package/dist/hooks/usePartConfig.svelte.d.ts +4 -0
- package/dist/hooks/usePartConfig.svelte.js +29 -9
- package/dist/hooks/usePointcloudObjects.svelte.js +2 -5
- package/dist/hooks/useWorldState.svelte.js +90 -17
- package/dist/metadata.js +13 -1
- package/package.json +3 -3
|
@@ -94,7 +94,6 @@ export const providePartConfig = (partID, params) => {
|
|
|
94
94
|
const updatePartFrame = (componentName, referenceFrame, pose, geometry) => {
|
|
95
95
|
const newConfig = getCurrent();
|
|
96
96
|
const component = newConfig.components?.find(({ name }) => name === componentName);
|
|
97
|
-
console.log('hi', newConfig, componentName);
|
|
98
97
|
if (!component) {
|
|
99
98
|
return;
|
|
100
99
|
}
|
|
@@ -162,6 +161,9 @@ export const providePartConfig = (partID, params) => {
|
|
|
162
161
|
get isDirty() {
|
|
163
162
|
return config.isDirty;
|
|
164
163
|
},
|
|
164
|
+
get hasPendingSave() {
|
|
165
|
+
return config.hasPendingSave;
|
|
166
|
+
},
|
|
165
167
|
get hasEditPermissions() {
|
|
166
168
|
return config.hasEditPermissions;
|
|
167
169
|
},
|
|
@@ -194,6 +196,8 @@ export const providePartConfig = (partID, params) => {
|
|
|
194
196
|
},
|
|
195
197
|
save: () => config.save?.(),
|
|
196
198
|
discardChanges: () => config.discardChanges?.(),
|
|
199
|
+
clearPendingSave: () => config.clearPendingSave(),
|
|
200
|
+
setPendingSave: () => config.setPendingSave(),
|
|
197
201
|
});
|
|
198
202
|
};
|
|
199
203
|
export const usePartConfig = () => {
|
|
@@ -202,6 +206,7 @@ export const usePartConfig = () => {
|
|
|
202
206
|
const useEmbeddedPartConfig = (props) => {
|
|
203
207
|
return {
|
|
204
208
|
hasEditPermissions: true,
|
|
209
|
+
hasPendingSave: false,
|
|
205
210
|
get isDirty() {
|
|
206
211
|
return props.isDirty;
|
|
207
212
|
},
|
|
@@ -215,6 +220,8 @@ const useEmbeddedPartConfig = (props) => {
|
|
|
215
220
|
const struct = Struct.fromJson(config);
|
|
216
221
|
return props.setLocalPartConfig(struct);
|
|
217
222
|
},
|
|
223
|
+
clearPendingSave() { },
|
|
224
|
+
setPendingSave() { },
|
|
218
225
|
};
|
|
219
226
|
};
|
|
220
227
|
const useStandalonePartConfig = (partID) => {
|
|
@@ -222,21 +229,25 @@ const useStandalonePartConfig = (partID) => {
|
|
|
222
229
|
refetchInterval: false,
|
|
223
230
|
});
|
|
224
231
|
const partName = $derived(partQuery.data?.part?.name);
|
|
232
|
+
// Use part.robotConfig (the stored Struct config) as the authoritative source.
|
|
233
|
+
// configJson is the compiled running config from the robot daemon and may be empty
|
|
234
|
+
// even when the stored config exists and the API key has edit permissions.
|
|
235
|
+
let networkPartConfig = $derived(partQuery.data?.part?.robotConfig);
|
|
236
|
+
let current = $state.raw();
|
|
237
|
+
let isDirty = $state(false);
|
|
238
|
+
let hasPendingSave = $state(false);
|
|
239
|
+
const hasEditPermissions = $derived(networkPartConfig !== undefined);
|
|
225
240
|
const configJSON = $derived.by(() => {
|
|
226
|
-
if (!
|
|
241
|
+
if (!networkPartConfig) {
|
|
227
242
|
return undefined;
|
|
228
243
|
}
|
|
229
244
|
try {
|
|
230
|
-
return
|
|
245
|
+
return networkPartConfig.toJson();
|
|
231
246
|
}
|
|
232
247
|
catch {
|
|
233
248
|
return undefined;
|
|
234
249
|
}
|
|
235
250
|
});
|
|
236
|
-
let networkPartConfig = $derived(configJSON ? Struct.fromJson(configJSON) : undefined);
|
|
237
|
-
let current = $state.raw();
|
|
238
|
-
let isDirty = $state(false);
|
|
239
|
-
const hasEditPermissions = $derived(networkPartConfig !== undefined);
|
|
240
251
|
const fragmentQueries = $derived((configJSON?.fragments ?? []).map((fragmentId) => {
|
|
241
252
|
const id = typeof fragmentId === 'string' ? fragmentId : fragmentId.id;
|
|
242
253
|
return createAppQuery('getFragment', () => [id], { refetchInterval: false });
|
|
@@ -263,8 +274,7 @@ const useStandalonePartConfig = (partID) => {
|
|
|
263
274
|
return results;
|
|
264
275
|
});
|
|
265
276
|
$effect.pre(() => {
|
|
266
|
-
if (!networkPartConfig) {
|
|
267
|
-
// no config returned here indicates this api key has no permission to update config
|
|
277
|
+
if (!networkPartConfig || isDirty) {
|
|
268
278
|
return;
|
|
269
279
|
}
|
|
270
280
|
current = networkPartConfig;
|
|
@@ -277,6 +287,9 @@ const useStandalonePartConfig = (partID) => {
|
|
|
277
287
|
get isDirty() {
|
|
278
288
|
return isDirty;
|
|
279
289
|
},
|
|
290
|
+
get hasPendingSave() {
|
|
291
|
+
return hasPendingSave;
|
|
292
|
+
},
|
|
280
293
|
get hasEditPermissions() {
|
|
281
294
|
return hasEditPermissions;
|
|
282
295
|
},
|
|
@@ -294,10 +307,17 @@ const useStandalonePartConfig = (partID) => {
|
|
|
294
307
|
networkPartConfig = current;
|
|
295
308
|
await updateRobotPartMutation.mutateAsync([partID(), partName, current]);
|
|
296
309
|
isDirty = false;
|
|
310
|
+
hasPendingSave = true;
|
|
297
311
|
},
|
|
298
312
|
discardChanges() {
|
|
299
313
|
current = networkPartConfig;
|
|
300
314
|
isDirty = false;
|
|
301
315
|
},
|
|
316
|
+
clearPendingSave() {
|
|
317
|
+
hasPendingSave = false;
|
|
318
|
+
},
|
|
319
|
+
setPendingSave() {
|
|
320
|
+
hasPendingSave = true;
|
|
321
|
+
},
|
|
302
322
|
};
|
|
303
323
|
};
|
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
3
|
+
"version": "1.19.1",
|
|
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",
|