@viamrobotics/motion-tools 1.33.0 → 1.33.2

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.
Files changed (60) hide show
  1. package/dist/components/Entities/Entities.svelte +18 -25
  2. package/dist/components/Entities/Entities.svelte.d.ts +2 -17
  3. package/dist/components/Entities/Label.svelte +79 -13
  4. package/dist/components/Entities/Label.svelte.d.ts +2 -1
  5. package/dist/components/Entities/Labels.svelte +36 -0
  6. package/dist/components/Entities/Labels.svelte.d.ts +3 -0
  7. package/dist/components/Entities/LineDots.svelte +8 -3
  8. package/dist/components/Entities/labelLayout/applyTeleports.d.ts +9 -0
  9. package/dist/components/Entities/labelLayout/applyTeleports.js +39 -0
  10. package/dist/components/Entities/labelLayout/buildNeighborhood.d.ts +8 -0
  11. package/dist/components/Entities/labelLayout/buildNeighborhood.js +26 -0
  12. package/dist/components/Entities/labelLayout/cameraHash.d.ts +8 -0
  13. package/dist/components/Entities/labelLayout/cameraHash.js +25 -0
  14. package/dist/components/Entities/labelLayout/cost.d.ts +44 -0
  15. package/dist/components/Entities/labelLayout/cost.js +126 -0
  16. package/dist/components/Entities/labelLayout/createLabelLayout.d.ts +27 -0
  17. package/dist/components/Entities/labelLayout/createLabelLayout.js +194 -0
  18. package/dist/components/Entities/labelLayout/geometry.d.ts +20 -0
  19. package/dist/components/Entities/labelLayout/geometry.js +151 -0
  20. package/dist/components/Entities/labelLayout/labelStore.svelte.d.ts +17 -0
  21. package/dist/components/Entities/labelLayout/labelStore.svelte.js +28 -0
  22. package/dist/components/Entities/labelLayout/measure.d.ts +13 -0
  23. package/dist/components/Entities/labelLayout/measure.js +42 -0
  24. package/dist/components/Entities/labelLayout/slots.d.ts +11 -0
  25. package/dist/components/Entities/labelLayout/slots.js +47 -0
  26. package/dist/components/Entities/labelLayout/solve.d.ts +11 -0
  27. package/dist/components/Entities/labelLayout/solve.js +93 -0
  28. package/dist/components/Entities/labelLayout/spatialHash.d.ts +15 -0
  29. package/dist/components/Entities/labelLayout/spatialHash.js +53 -0
  30. package/dist/components/Entities/labelLayout/types.d.ts +105 -0
  31. package/dist/components/Entities/labelLayout/types.js +19 -0
  32. package/dist/components/Entities/labelLayout/writeBack.d.ts +20 -0
  33. package/dist/components/Entities/labelLayout/writeBack.js +51 -0
  34. package/dist/components/Scene.svelte +2 -1
  35. package/dist/components/SelectedTransformControls.svelte +65 -47
  36. package/dist/components/overlay/Details.svelte +210 -226
  37. package/dist/components/overlay/Details.svelte.d.ts +1 -1
  38. package/dist/components/overlay/Popover.svelte +6 -4
  39. package/dist/components/overlay/Popover.svelte.d.ts +6 -2
  40. package/dist/components/overlay/dashboard/Button.svelte +7 -2
  41. package/dist/components/overlay/dashboard/Button.svelte.d.ts +2 -1
  42. package/dist/components/overlay/details/AxesHelperDetails.svelte +32 -0
  43. package/dist/components/overlay/details/AxesHelperDetails.svelte.d.ts +7 -0
  44. package/dist/components/overlay/details/ColorDetails.svelte +35 -0
  45. package/dist/components/overlay/details/ColorDetails.svelte.d.ts +7 -0
  46. package/dist/components/overlay/details/GeometryDetails.svelte +104 -0
  47. package/dist/components/overlay/details/GeometryDetails.svelte.d.ts +7 -0
  48. package/dist/components/overlay/details/LineDetails/LineDetails.svelte +196 -0
  49. package/dist/components/overlay/details/LineDetails/LineDetails.svelte.d.ts +7 -0
  50. package/dist/components/overlay/details/LineDetails/linePositions.d.ts +3 -0
  51. package/dist/components/overlay/details/LineDetails/linePositions.js +30 -0
  52. package/dist/components/overlay/details/OpacityDetails.svelte +44 -0
  53. package/dist/components/overlay/details/OpacityDetails.svelte.d.ts +7 -0
  54. package/dist/components/overlay/details/PoseDetails.svelte +189 -0
  55. package/dist/components/overlay/details/PoseDetails.svelte.d.ts +14 -0
  56. package/dist/ecs/traits.d.ts +1 -1
  57. package/dist/ecs/traits.js +1 -1
  58. package/dist/hooks/usePartConfig.svelte.js +8 -6
  59. package/dist/hooks/useWorldState.svelte.js +94 -69
  60. package/package.json +4 -2
@@ -71,7 +71,7 @@ export declare const Invisible: import("koota").Trait<() => boolean>;
71
71
  * `details-extensions` portal target (e.g. gizmo plugin entities) opt in by
72
72
  * adding this trait.
73
73
  */
74
- export declare const CustomDetails: import("koota").Trait<() => boolean>;
74
+ export declare const CustomDetails: import("koota").TagTrait;
75
75
  /**
76
76
  * True when the entity itself, or any of its parents up the `ChildOf`
77
77
  * chain, has `Invisible`. Maintained by `provideInheritedInvisible`;
@@ -68,7 +68,7 @@ export const Invisible = trait(() => true);
68
68
  * `details-extensions` portal target (e.g. gizmo plugin entities) opt in by
69
69
  * adding this trait.
70
70
  */
71
- export const CustomDetails = trait(() => true);
71
+ export const CustomDetails = trait();
72
72
  /**
73
73
  * True when the entity itself, or any of its parents up the `ChildOf`
74
74
  * chain, has `Invisible`. Maintained by `provideInheritedInvisible`;
@@ -105,12 +105,14 @@ export const providePartConfig = (partID, params) => {
105
105
  y: pose.y ?? currentPose.y,
106
106
  z: pose.z ?? currentPose.z,
107
107
  };
108
- component.frame.orientation.type = 'ov_degrees';
109
- component.frame.orientation.value = {
110
- x: pose.oX ?? currentPose.oX,
111
- y: pose.oY ?? currentPose.oY,
112
- z: pose.oZ ?? currentPose.oZ,
113
- th: pose.theta ?? currentPose.theta,
108
+ component.frame.orientation = {
109
+ type: 'ov_degrees',
110
+ value: {
111
+ x: pose.oX ?? currentPose.oX,
112
+ y: pose.oY ?? currentPose.oY,
113
+ z: pose.oZ ?? currentPose.oZ,
114
+ th: pose.theta ?? currentPose.theta,
115
+ },
114
116
  };
115
117
  if (geometry) {
116
118
  if (geometry.type === 'none') {
@@ -1,11 +1,11 @@
1
1
  import { useThrelte } from '@threlte/core';
2
2
  import { Struct, TransformChangeType, WorldStateStoreClient, } from '@viamrobotics/sdk';
3
- import { createResourceClient, createResourceQuery, createResourceStream, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
+ import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
4
4
  import { Matrix4 } from 'three';
5
5
  import { asFloat32Array, inMeters } from '../buffer';
6
6
  import { createChunkLoader } from '../chunking';
7
7
  import { drawTransform, updateMetadata } from '../draw';
8
- import { traits, useWorld } from '../ecs';
8
+ import { hierarchy, traits, useWorld } from '../ecs';
9
9
  import { isPointCloud } from '../geometry';
10
10
  import { metadataFromStruct } from '../metadata';
11
11
  import { createPose, poseToMatrix } from '../transform';
@@ -27,6 +27,10 @@ export const provideWorldStates = () => {
27
27
  };
28
28
  });
29
29
  };
30
+ // FieldMask paths are proto field names; spec-compliant backends emit
31
+ // snake_case (`pose_in_observer_frame`) while some emit camelCase. Normalize
32
+ // to camelCase so matching against the message's accessors is casing-agnostic.
33
+ const snakeToCamel = (path) => path.replaceAll(/_([a-z])/g, (_, char) => char.toUpperCase());
30
34
  const decodeBase64 = (encoded) => {
31
35
  const binary = atob(encoded);
32
36
  const bytes = new Uint8Array(binary.length);
@@ -87,6 +91,11 @@ const createWorldState = (client) => {
87
91
  const world = useWorld();
88
92
  const relationships = useRelationships();
89
93
  const entities = new Map();
94
+ // UUIDs the stream has removed; guards against a stale initial snapshot or a
95
+ // self-heal fetch re-creating an entity the server has already deleted.
96
+ const removedUUIDs = new Set();
97
+ // UUIDs with an in-flight self-heal `getTransform`, to dedupe concurrent fetches.
98
+ const pendingSpawns = new Set();
90
99
  const chunkLoader = createChunkLoader({
91
100
  world,
92
101
  invalidate,
@@ -105,7 +114,7 @@ const createWorldState = (client) => {
105
114
  },
106
115
  });
107
116
  const spawnEntity = (transform) => {
108
- if (entities.has(transform.uuidString)) {
117
+ if (entities.has(transform.uuidString) || removedUUIDs.has(transform.uuidString)) {
109
118
  return;
110
119
  }
111
120
  const spawned = drawTransform(world, transform, traits.WorldStateStoreAPI, { removable: false });
@@ -118,6 +127,7 @@ const createWorldState = (client) => {
118
127
  invalidate();
119
128
  };
120
129
  const destroyEntity = (uuid) => {
130
+ removedUUIDs.add(uuid);
121
131
  const entity = entities.get(uuid);
122
132
  if (!entity)
123
133
  return;
@@ -126,29 +136,54 @@ const createWorldState = (client) => {
126
136
  }
127
137
  entities.delete(uuid);
128
138
  };
139
+ // Spawn an entity whose UPDATE delta arrived before the initial snapshot
140
+ // created it. The delta carries only changed fields, so fetch the full
141
+ // transform; skip if it was removed or already spawned meanwhile.
142
+ const spawnFromServer = async (uuid) => {
143
+ if (entities.has(uuid) || removedUUIDs.has(uuid) || pendingSpawns.has(uuid))
144
+ return;
145
+ pendingSpawns.add(uuid);
146
+ try {
147
+ const transform = await client.current?.getTransform(uuid);
148
+ if (transform && !removedUUIDs.has(uuid)) {
149
+ spawnEntity(transform);
150
+ invalidate();
151
+ }
152
+ }
153
+ catch (error) {
154
+ console.error('World state self-heal failed for', uuid, error);
155
+ }
156
+ finally {
157
+ pendingSpawns.delete(uuid);
158
+ }
159
+ };
129
160
  const updateEntity = (transform, changes) => {
130
161
  const entity = entities.get(transform.uuidString);
131
- if (!entity)
162
+ if (!entity) {
163
+ void spawnFromServer(transform.uuidString);
132
164
  return;
165
+ }
133
166
  let metadataDirty = false;
134
- for (const path of changes) {
135
- if (typeof path === 'string') {
136
- if (path.startsWith('poseInObserverFrame.pose')) {
137
- const matrix = entity.get(traits.Matrix);
138
- if (matrix) {
139
- poseToMatrix(createPose(transform.poseInObserverFrame?.pose), matrix);
140
- entity.changed(traits.Matrix);
141
- }
142
- else {
143
- entity.add(traits.Matrix(poseToMatrix(createPose(transform.poseInObserverFrame?.pose), new Matrix4())));
144
- }
145
- }
146
- else if (path.startsWith('physicalObject') && transform.physicalObject) {
147
- traits.updateGeometryTrait(entity, transform.physicalObject);
167
+ for (const rawPath of changes) {
168
+ if (typeof rawPath !== 'string')
169
+ continue;
170
+ const path = snakeToCamel(rawPath);
171
+ if (path.startsWith('poseInObserverFrame')) {
172
+ const matrix = entity.get(traits.Matrix);
173
+ if (matrix) {
174
+ poseToMatrix(createPose(transform.poseInObserverFrame?.pose), matrix);
175
+ entity.changed(traits.Matrix);
148
176
  }
149
- else if (path.startsWith('metadata')) {
150
- metadataDirty = true;
177
+ else {
178
+ entity.add(traits.Matrix(poseToMatrix(createPose(transform.poseInObserverFrame?.pose), new Matrix4())));
151
179
  }
180
+ hierarchy.setParent(entity, transform.poseInObserverFrame?.referenceFrame);
181
+ }
182
+ else if (path.startsWith('physicalObject') && transform.physicalObject) {
183
+ traits.updateGeometryTrait(entity, transform.physicalObject);
184
+ }
185
+ else if (path.startsWith('metadata')) {
186
+ metadataDirty = true;
152
187
  }
153
188
  }
154
189
  if (metadataDirty) {
@@ -161,17 +196,16 @@ const createWorldState = (client) => {
161
196
  };
162
197
  let initialized = false;
163
198
  let flushScheduled = false;
199
+ let rafId = 0;
164
200
  let pendingEvents = [];
165
201
  const listUUIDs = createResourceQuery(client, 'listUUIDs');
166
202
  const getTransformQueries = $derived(listUUIDs.data?.map((uuid) => {
167
203
  return createResourceQuery(client, 'getTransform', () => [uuid], () => ({ refetchInterval: false }));
168
204
  }));
169
- const changeStream = createResourceStream(client, 'streamTransformChanges', {
170
- refetchMode: 'replace',
171
- });
172
205
  const applyEvents = (events) => {
173
206
  for (const event of events) {
174
207
  if (event.changeType === TransformChangeType.ADDED) {
208
+ removedUUIDs.delete(event.transform.uuidString);
175
209
  spawnEntity(event.transform);
176
210
  }
177
211
  else if (event.changeType === TransformChangeType.REMOVED) {
@@ -190,11 +224,12 @@ const createWorldState = (client) => {
190
224
  if (flushScheduled)
191
225
  return;
192
226
  flushScheduled = true;
193
- requestAnimationFrame(() => {
194
- const toApply = pendingEvents;
195
- applyEvents(toApply);
227
+ rafId = requestAnimationFrame(() => {
228
+ rafId = 0;
196
229
  flushScheduled = false;
230
+ const toApply = pendingEvents;
197
231
  pendingEvents = [];
232
+ applyEvents(toApply);
198
233
  });
199
234
  };
200
235
  $effect(() => {
@@ -213,55 +248,45 @@ const createWorldState = (client) => {
213
248
  invalidate();
214
249
  initialized = true;
215
250
  });
216
- $effect(() => {
217
- if (changeStream?.data === undefined)
251
+ /**
252
+ * Consumes the `streamTransformChanges` server stream directly.
253
+ * Transform changes are write-once into the ECS world, so we drain
254
+ * each event into `pendingEvents` (cleared every flush) and never
255
+ * retain history. Mirrors `useDrawService`'s stream consumption.
256
+ */
257
+ const consumeChanges = async (signal) => {
258
+ const activeClient = client.current;
259
+ if (!activeClient)
218
260
  return;
219
- const eventsByUUID = new Map();
220
- for (const event of changeStream.data) {
221
- if (!event.transform) {
222
- continue;
223
- }
224
- const uuid = event.transform.uuidString;
225
- const existing = eventsByUUID.get(uuid);
226
- if (!existing) {
227
- eventsByUUID.set(uuid, event);
228
- continue;
229
- }
230
- switch (event.changeType) {
231
- case TransformChangeType.REMOVED: {
232
- eventsByUUID.set(uuid, event);
233
- break;
234
- }
235
- case TransformChangeType.ADDED: {
236
- if (existing.changeType !== TransformChangeType.REMOVED) {
237
- eventsByUUID.set(uuid, event);
238
- }
239
- break;
240
- }
241
- case TransformChangeType.UPDATED: {
242
- // merge with existing updated event
243
- if (existing.changeType === TransformChangeType.UPDATED) {
244
- existing.updatedFields ??= { paths: [] };
245
- const paths = event.updatedFields?.paths ?? [];
246
- for (const path of paths) {
247
- if (existing.updatedFields.paths.includes(path)) {
248
- continue;
249
- }
250
- existing.updatedFields.paths.push(path);
251
- }
252
- existing.transform = event.transform;
253
- }
254
- else {
255
- eventsByUUID.set(uuid, event);
256
- }
261
+ try {
262
+ for await (const event of activeClient.streamTransformChanges(undefined, { signal })) {
263
+ if (signal.aborted)
257
264
  break;
258
- }
265
+ if (!event.transform)
266
+ continue;
267
+ pendingEvents.push(event);
268
+ scheduleFlush();
269
+ }
270
+ }
271
+ catch (error) {
272
+ if (!signal.aborted) {
273
+ console.error('World state transform stream error:', error);
259
274
  }
260
275
  }
261
- pendingEvents.push(...eventsByUUID.values());
262
- scheduleFlush();
276
+ };
277
+ $effect(() => {
278
+ if (!client.current)
279
+ return;
280
+ const controller = new AbortController();
281
+ void consumeChanges(controller.signal);
282
+ return () => {
283
+ controller.abort();
284
+ };
263
285
  });
264
286
  return () => {
287
+ if (rafId)
288
+ cancelAnimationFrame(rafId);
289
+ pendingEvents = [];
265
290
  chunkLoader.dispose();
266
291
  for (const [, entity] of entities) {
267
292
  if (world.has(entity)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.33.0",
3
+ "version": "1.33.2",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -33,6 +33,7 @@
33
33
  "@threlte/rapier": "3.4.1",
34
34
  "@threlte/xr": "1.6.0",
35
35
  "@types/bun": "1.2.21",
36
+ "@types/d3-force": "^3.0.10",
36
37
  "@types/earcut": "^3.0.0",
37
38
  "@types/lodash-es": "4.17.12",
38
39
  "@types/node": "^25.6.0",
@@ -84,7 +85,7 @@
84
85
  "vite-plugin-devtools-json": "1.0.0",
85
86
  "vite-plugin-glsl": "^1.5.5",
86
87
  "vite-plugin-mkcert": "1.17.9",
87
- "vitest": "3.2.4"
88
+ "vitest": "3.2.6"
88
89
  },
89
90
  "peerDependencies": {
90
91
  "@ag-grid-community/client-side-row-model": ">=32.3.0",
@@ -163,6 +164,7 @@
163
164
  "dependencies": {
164
165
  "@bufbuild/protobuf": "1.10.1",
165
166
  "@neodrag/svelte": "^2.3.3",
167
+ "d3-force": "^3.0.0",
166
168
  "filtrex": "^3.1.0",
167
169
  "koota": "0.6.5",
168
170
  "lodash-es": "4.18.1",