@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/draw.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Vector3, Vector4 } from 'three';
2
2
  import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
3
- import { createBufferGeometry, updateBufferGeometry } from './attribute';
3
+ import { createBufferGeometry, preAllocateBufferGeometry, updateBufferGeometry, writeBufferGeometryRange, } from './attribute';
4
4
  import { asFloat32Array, asOpacity, asRGB, inMeters, isSingleColor, isVertexColors, STRIDE, } from './buffer';
5
5
  import { traits } from './ecs';
6
6
  import { parsePcdInWorker } from './loaders/pcd';
@@ -38,9 +38,7 @@ export const drawTransform = (world, { referenceFrame, poseInObserverFrame, phys
38
38
  }
39
39
  if (options.removable)
40
40
  entityTraits.push(traits.Removable);
41
- const parent = poseInObserverFrame?.referenceFrame;
42
- if (parent && parent !== 'world')
43
- entityTraits.push(traits.Parent(parent));
41
+ entityTraits.push(...traits.getParentTrait(poseInObserverFrame?.referenceFrame));
44
42
  const parsedMetadata = metadataFromStruct(metadata?.fields);
45
43
  if (parsedMetadata.showAxesHelper)
46
44
  entityTraits.push(traits.ShowAxesHelper);
@@ -68,10 +66,7 @@ export const drawDrawing = (world, drawing, api, options = { removable: true })
68
66
  const { referenceFrame, poseInObserverFrame, physicalObject, metadata } = drawing;
69
67
  if (physicalObject?.geometryType?.case === 'model')
70
68
  return drawModel(world, drawing, api, options);
71
- const entity = world.spawn(traits.Name(referenceFrame), traits.Pose(createPose(poseInObserverFrame?.pose)), api);
72
- const parent = poseInObserverFrame?.referenceFrame;
73
- if (parent && parent !== 'world')
74
- entity.add(traits.Parent(parent));
69
+ const entity = world.spawn(traits.Name(referenceFrame), traits.Pose(createPose(poseInObserverFrame?.pose)), api, ...traits.getParentTrait(poseInObserverFrame?.referenceFrame));
75
70
  if (options.removable)
76
71
  entity.add(traits.Removable);
77
72
  if (metadata?.showAxesHelper)
@@ -83,9 +78,7 @@ export const drawDrawing = (world, drawing, api, options = { removable: true })
83
78
  };
84
79
  export const updateTransform = (entity, { poseInObserverFrame, physicalObject, metadata }, options = { removable: true }) => {
85
80
  entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
86
- const parent = poseInObserverFrame?.referenceFrame;
87
- if (parent && parent !== 'world')
88
- entity.set(traits.Parent, parent);
81
+ traits.setParentTrait(entity, poseInObserverFrame?.referenceFrame);
89
82
  if (physicalObject) {
90
83
  traits.updateGeometryTrait(entity, physicalObject);
91
84
  const center = physicalObject.center;
@@ -96,31 +89,32 @@ export const updateTransform = (entity, { poseInObserverFrame, physicalObject, m
96
89
  entity.remove(traits.Center);
97
90
  }
98
91
  }
99
- const parsedMetadata = metadataFromStruct(metadata?.fields);
100
- if (parsedMetadata.showAxesHelper)
92
+ updateMetadata(entity, metadataFromStruct(metadata?.fields), {
93
+ pointCloud: isPointCloud(physicalObject?.geometryType),
94
+ });
95
+ if (options.removable)
96
+ entity.add(traits.Removable);
97
+ if (!options.removable)
98
+ entity.remove(traits.Removable);
99
+ };
100
+ export const updateMetadata = (entity, metadata, { pointCloud = false } = {}) => {
101
+ if (metadata.showAxesHelper)
101
102
  entity.add(traits.ShowAxesHelper);
102
103
  else
103
104
  entity.remove(traits.ShowAxesHelper);
104
- if (parsedMetadata.invisible)
105
+ if (metadata.invisible)
105
106
  entity.add(traits.Invisible);
106
107
  else
107
108
  entity.remove(traits.Invisible);
108
- const { colors, opacities } = parsedMetadata;
109
+ const { colors, opacities } = metadata;
109
110
  if (colors) {
110
- if (isPointCloud(physicalObject?.geometryType)) {
111
- updateColors(entity, parsedMetadata);
112
- }
113
- else {
114
- addColorTraits(entity, colors);
111
+ if (pointCloud) {
112
+ updatePointCloudColors(entity, metadata);
115
113
  }
114
+ // Always set color traits so any subsequent async work can read them
115
+ setColorTraits(entity, colors);
116
116
  }
117
- const opacity = asOpacity(opacities, DEFAULT_OPACITY);
118
- if (opacity < 1)
119
- entity.add(traits.Opacity(opacity));
120
- if (options.removable)
121
- entity.add(traits.Removable);
122
- if (!options.removable)
123
- entity.remove(traits.Removable);
117
+ entity.set(traits.Opacity, asOpacity(opacities, DEFAULT_OPACITY));
124
118
  };
125
119
  export const updateDrawing = (world, entities, drawing, api, options = { removable: true }) => {
126
120
  const { poseInObserverFrame, physicalObject, metadata } = drawing;
@@ -137,9 +131,7 @@ export const updateDrawing = (world, entities, drawing, api, options = { removab
137
131
  if (!world.has(entity))
138
132
  return entities;
139
133
  entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
140
- const parent = poseInObserverFrame?.referenceFrame;
141
- if (parent && parent !== 'world')
142
- entity.set(traits.Parent, parent);
134
+ traits.setParentTrait(entity, poseInObserverFrame?.referenceFrame);
143
135
  if (metadata?.showAxesHelper)
144
136
  entity.add(traits.ShowAxesHelper);
145
137
  if (!metadata?.showAxesHelper)
@@ -181,15 +173,30 @@ const applyShape = (entity, { physicalObject, metadata }) => {
181
173
  }
182
174
  case 'points': {
183
175
  const positions = asFloat32Array(geometryType.value.positions, inMeters);
176
+ const total = metadata?.chunks?.total;
184
177
  const center = physicalObject?.center;
185
178
  if (center)
186
179
  entity.add(traits.Center(center));
187
180
  addColorTraits(entity, colors ?? DEFAULT_POINTS_COLORS);
188
181
  entity.add(traits.PointSize(geometryType.value.pointSize ?? DEFAULT_POINT_SIZE));
189
- entity.add(traits.BufferGeometry(createBufferGeometry(positions, {
190
- colors: isVertexColors(colors) ? colors : undefined,
182
+ const vertexColors = isVertexColors(colors) ? colors : undefined;
183
+ const pointsMetadata = {
184
+ colors: vertexColors,
191
185
  colorFormat: metadata?.colorFormat ?? ColorFormat.UNSPECIFIED,
192
- })));
186
+ opacities: metadata?.opacities,
187
+ };
188
+ if (total !== undefined && total > 0) {
189
+ const allocMetadata = {
190
+ ...pointsMetadata,
191
+ colors: vertexColors ? new Uint8Array(0) : undefined,
192
+ };
193
+ const geometry = preAllocateBufferGeometry(total, STRIDE.POSITIONS, allocMetadata);
194
+ writeBufferGeometryRange(geometry, positions, 0, pointsMetadata);
195
+ entity.add(traits.BufferGeometry(geometry));
196
+ }
197
+ else {
198
+ entity.add(traits.BufferGeometry(createBufferGeometry(positions, pointsMetadata)));
199
+ }
193
200
  entity.add(traits.Points);
194
201
  break;
195
202
  }
@@ -237,7 +244,6 @@ const applyShape = (entity, { physicalObject, metadata }) => {
237
244
  };
238
245
  const drawModel = (world, { referenceFrame, poseInObserverFrame, physicalObject, metadata }, api, { removable = true }) => {
239
246
  const entities = [];
240
- const parent = poseInObserverFrame?.referenceFrame;
241
247
  const geometryType = physicalObject?.geometryType;
242
248
  if (geometryType?.case !== 'model')
243
249
  return entities;
@@ -245,9 +251,8 @@ const drawModel = (world, { referenceFrame, poseInObserverFrame, physicalObject,
245
251
  traits.Name(referenceFrame),
246
252
  traits.Pose(createPose(poseInObserverFrame?.pose)),
247
253
  api,
254
+ ...traits.getParentTrait(poseInObserverFrame?.referenceFrame),
248
255
  ];
249
- if (parent && parent !== 'world')
250
- baseTraits.push(traits.Parent(parent));
251
256
  if (removable)
252
257
  baseTraits.push(traits.Removable);
253
258
  if (metadata?.invisible)
@@ -292,15 +297,24 @@ const parsePointCloud = (world, entity, pointCloud, metadata) => {
292
297
  let vertexColors = pointcloud.colors;
293
298
  if (colors && colors.length > 0)
294
299
  vertexColors = parseColors(colors, numPoints);
295
- const geometry = createBufferGeometry(pointcloud.positions, {
296
- colors: vertexColors ?? undefined,
297
- colorFormat,
298
- });
300
+ const total = metadata.chunks?.total;
301
+ const chunkMetadata = { colors: vertexColors ?? undefined, colorFormat };
302
+ let geometry;
303
+ if (total !== undefined && total > numPoints) {
304
+ geometry = preAllocateBufferGeometry(total, STRIDE.POSITIONS, {
305
+ ...chunkMetadata,
306
+ colors: vertexColors ? new Uint8Array(0) : undefined,
307
+ });
308
+ writeBufferGeometryRange(geometry, pointcloud.positions, 0, chunkMetadata);
309
+ }
310
+ else {
311
+ geometry = createBufferGeometry(pointcloud.positions, chunkMetadata);
312
+ }
299
313
  entity.add(traits.BufferGeometry(geometry));
300
314
  entity.add(traits.Points);
301
315
  });
302
316
  };
303
- const updateColors = (entity, metadata) => {
317
+ const updatePointCloudColors = (entity, metadata) => {
304
318
  const buffer = entity.get(traits.BufferGeometry);
305
319
  if (!buffer) {
306
320
  if (metadata.colors)
@@ -408,7 +422,7 @@ const updateShape = (entity, { physicalObject, metadata }) => {
408
422
  }
409
423
  }
410
424
  };
411
- const addColorTraits = (entity, colors) => {
425
+ export const addColorTraits = (entity, colors) => {
412
426
  if (isVertexColors(colors)) {
413
427
  entity.add(traits.Colors(colors));
414
428
  }
@@ -416,13 +430,20 @@ const addColorTraits = (entity, colors) => {
416
430
  entity.add(traits.Color(asRGB(colors, rgb)));
417
431
  }
418
432
  };
419
- const setColorTraits = (entity, colors) => {
433
+ export const setColorTraits = (entity, colors) => {
420
434
  if (isVertexColors(colors)) {
421
- entity.set(traits.Colors, colors);
435
+ if (entity.has(traits.Colors))
436
+ entity.set(traits.Colors, colors);
437
+ else
438
+ entity.add(traits.Colors(colors));
422
439
  entity.remove(traits.Color);
423
440
  }
424
441
  else {
425
- entity.set(traits.Color, asRGB(colors, rgb));
442
+ const color = asRGB(colors, rgb);
443
+ if (entity.has(traits.Color))
444
+ entity.set(traits.Color, color);
445
+ else
446
+ entity.add(traits.Color(color));
426
447
  entity.remove(traits.Colors);
427
448
  }
428
449
  };
@@ -1,6 +1,6 @@
1
1
  import type { GLTF as ThreeGltf } from 'three/examples/jsm/loaders/GLTFLoader.js';
2
2
  import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
3
- import { type Entity } from 'koota';
3
+ import { type ConfigurableTrait, type Entity } from 'koota';
4
4
  import { BufferGeometry as ThreeBufferGeometry } from 'three';
5
5
  export declare const Name: import("koota").Trait<() => string>;
6
6
  export declare const Parent: import("koota").Trait<() => string>;
@@ -170,13 +170,21 @@ export declare const DotColors: import("koota").Trait<() => Uint8Array<ArrayBuff
170
170
  */
171
171
  export declare const DotSize: import("koota").Trait<() => number>;
172
172
  export declare const ReferenceFrame: import("koota").Trait<() => boolean>;
173
+ /**
174
+ * Tracks chunk loading progress for progressively-loaded entities.
175
+ * `loaded` is the number of elements received so far; `total` is the target.
176
+ */
177
+ export declare const ChunkProgress: import("koota").Trait<{
178
+ loaded: number;
179
+ total: number;
180
+ }>;
173
181
  /**
174
182
  * Interaction layers for entities
175
183
  */
176
184
  export type InteractionLayerValue = 'selectTool';
177
185
  export declare const SelectToolInteractionLayer: import("koota").Trait<() => boolean>;
178
186
  /**
179
- * This entity can be safetly removed from the scene by the user
187
+ * This entity can be safely removed from the scene by the user
180
188
  */
181
189
  export declare const Removable: import("koota").Trait<() => boolean>;
182
190
  export declare const Geometry: (geometry: ViamGeometry) => import("koota").Trait<() => boolean> | [import("koota").Trait<{
@@ -198,4 +206,6 @@ export declare const Geometry: (geometry: ViamGeometry) => import("koota").Trait
198
206
  }>, Partial<{
199
207
  r: number;
200
208
  }>] | [import("koota").Trait<() => ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>>, ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>];
209
+ export declare const getParentTrait: (parent: string | undefined) => ConfigurableTrait[];
210
+ export declare const setParentTrait: (entity: Entity, parent: string | undefined) => void;
201
211
  export declare const updateGeometryTrait: (entity: Entity, geometry?: ViamGeometry) => void;
@@ -1,7 +1,10 @@
1
1
  import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
2
2
  import { trait } from 'koota';
3
3
  import { BufferGeometry as ThreeBufferGeometry } from 'three';
4
+ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
5
+ import { ColorFormat } from '../buf/draw/v1/metadata_pb';
4
6
  import { createBox, createCapsule, createSphere } from '../geometry';
7
+ import { parsePcdInWorker } from '../loaders/pcd';
5
8
  import { parsePlyInput } from '../ply';
6
9
  export const Name = trait(() => '');
7
10
  export const Parent = trait(() => 'world');
@@ -124,9 +127,14 @@ export const DotColors = trait(() => new Uint8Array());
124
127
  */
125
128
  export const DotSize = trait(() => 10);
126
129
  export const ReferenceFrame = trait(() => true);
130
+ /**
131
+ * Tracks chunk loading progress for progressively-loaded entities.
132
+ * `loaded` is the number of elements received so far; `total` is the target.
133
+ */
134
+ export const ChunkProgress = trait({ loaded: 0, total: 0 });
127
135
  export const SelectToolInteractionLayer = trait(() => true);
128
136
  /**
129
- * This entity can be safetly removed from the scene by the user
137
+ * This entity can be safely removed from the scene by the user
130
138
  */
131
139
  export const Removable = trait(() => true);
132
140
  export const Geometry = (geometry) => {
@@ -144,6 +152,19 @@ export const Geometry = (geometry) => {
144
152
  }
145
153
  return ReferenceFrame;
146
154
  };
155
+ export const getParentTrait = (parent) => !parent || parent === 'world' ? [] : [Parent(parent)];
156
+ export const setParentTrait = (entity, parent) => {
157
+ if (!parent || parent === 'world') {
158
+ entity.remove(Parent);
159
+ return;
160
+ }
161
+ if (entity.has(Parent)) {
162
+ entity.set(Parent, parent);
163
+ }
164
+ else {
165
+ entity.add(Parent(parent));
166
+ }
167
+ };
147
168
  export const updateGeometryTrait = (entity, geometry) => {
148
169
  if (!geometry) {
149
170
  entity.remove(Box, Capsule, Sphere, BufferGeometry);
@@ -185,4 +206,62 @@ export const updateGeometryTrait = (entity, geometry) => {
185
206
  entity.add(BufferGeometry(parsePlyInput(geometry.geometryType.value.mesh)));
186
207
  }
187
208
  }
209
+ else if (geometry.geometryType.case === 'pointcloud') {
210
+ updatePointCloud(entity, geometry.geometryType.value.pointCloud);
211
+ }
212
+ };
213
+ const updatePointCloud = (entity, pointCloud) => {
214
+ parsePcdInWorker(new Uint8Array(pointCloud))
215
+ .then((parsed) => {
216
+ if (!entity.isAlive())
217
+ return;
218
+ const buffer = entity.get(BufferGeometry);
219
+ let colors = parsed.colors;
220
+ if (buffer) {
221
+ // Reapply single color trait if the point count changed
222
+ if (parsed.colors === undefined) {
223
+ const color = entity.get(Color);
224
+ if (color) {
225
+ const newCount = parsed.positions.length / 3;
226
+ colors = new Uint8Array(newCount * 3);
227
+ const r = Math.round(color.r * 255);
228
+ const g = Math.round(color.g * 255);
229
+ const b = Math.round(color.b * 255);
230
+ for (let i = 0; i < newCount; i++) {
231
+ colors[i * 3] = r;
232
+ colors[i * 3 + 1] = g;
233
+ colors[i * 3 + 2] = b;
234
+ }
235
+ }
236
+ }
237
+ // When the point count changes, attributes must be reallocated.
238
+ const oldCount = buffer.getAttribute('position').count;
239
+ const newCount = parsed.positions.length / 3;
240
+ if (oldCount === newCount) {
241
+ updateBufferGeometry(buffer, parsed.positions, {
242
+ colors,
243
+ colorFormat: ColorFormat.RGB,
244
+ });
245
+ }
246
+ else {
247
+ const fresh = createBufferGeometry(parsed.positions, {
248
+ colors,
249
+ colorFormat: ColorFormat.RGB,
250
+ });
251
+ buffer.dispose();
252
+ entity.set(BufferGeometry, fresh);
253
+ }
254
+ return;
255
+ }
256
+ entity.remove(Box, Capsule, Sphere);
257
+ entity.add(BufferGeometry(createBufferGeometry(parsed.positions, {
258
+ colors: parsed.colors,
259
+ colorFormat: ColorFormat.RGB,
260
+ })));
261
+ if (!entity.has(Points))
262
+ entity.add(Points);
263
+ })
264
+ .catch((error) => {
265
+ console.error('Failed to update pointcloud buffer geometry:', error);
266
+ });
188
267
  };
@@ -127,9 +127,7 @@ export const provideDrawAPI = () => {
127
127
  const existing = entities.get(name);
128
128
  if (existing) {
129
129
  existing.set(traits.Pose, pose);
130
- if (parent && parent !== 'world') {
131
- existing.set(traits.Parent, parent);
132
- }
130
+ traits.setParentTrait(existing, parent);
133
131
  continue;
134
132
  }
135
133
  const geometryTrait = () => {
@@ -144,10 +142,7 @@ export const provideDrawAPI = () => {
144
142
  }
145
143
  return traits.ReferenceFrame;
146
144
  };
147
- const entityTraits = [];
148
- if (parent && parent !== 'world') {
149
- entityTraits.push(traits.Parent(parent));
150
- }
145
+ const entityTraits = [...traits.getParentTrait(parent)];
151
146
  if (frame.geometry) {
152
147
  entityTraits.push(geometryTrait());
153
148
  }
@@ -181,11 +176,15 @@ export const provideDrawAPI = () => {
181
176
  }
182
177
  return traits.ReferenceFrame;
183
178
  };
184
- const entityTraits = [];
185
- if (parent && parent !== 'world') {
186
- entityTraits.push(traits.Parent(parent));
187
- }
188
- entityTraits.push(traits.Name(data.label ?? ++geometryIndex), traits.Pose(pose), traits.Color(colorUtil.set(color)), geometryTrait(), traits.DrawAPI, traits.Removable);
179
+ const entityTraits = [
180
+ traits.Name(data.label ?? ++geometryIndex),
181
+ ...traits.getParentTrait(parent),
182
+ traits.Pose(pose),
183
+ traits.Color(colorUtil.set(color)),
184
+ geometryTrait(),
185
+ traits.DrawAPI,
186
+ traits.Removable,
187
+ ];
189
188
  const entity = world.spawn(...entityTraits);
190
189
  entities.set(name, entity);
191
190
  };
@@ -2,17 +2,18 @@ import { MachineConnectionEvent, Transform } from '@viamrobotics/sdk';
2
2
  import { createRobotQuery, useConnectionStatus, useMachineStatus, useRobotClient, } from '@viamrobotics/svelte-sdk';
3
3
  import {} from 'koota';
4
4
  import { getContext, setContext, untrack } from 'svelte';
5
- import { resourceNameToColor } from '../color';
5
+ import { resourceNameToColor, subtypeToColor } from '../color';
6
6
  import { traits, useWorld } from '../ecs';
7
- import { updateGeometryTrait } from '../ecs/traits';
8
7
  import { createPose } from '../transform';
9
8
  import { useConfigFrames } from './useConfigFrames.svelte';
10
9
  import { useEnvironment } from './useEnvironment.svelte';
11
10
  import { useLogs } from './useLogs.svelte';
11
+ import { usePartConfig } from './usePartConfig.svelte';
12
12
  import { useResourceByName } from './useResourceByName.svelte';
13
13
  const key = Symbol('frames-context');
14
14
  export const provideFrames = (partID) => {
15
15
  const configFrames = useConfigFrames();
16
+ const partConfig = usePartConfig();
16
17
  const environment = useEnvironment();
17
18
  const world = useWorld();
18
19
  const resourceByName = useResourceByName();
@@ -20,6 +21,7 @@ export const provideFrames = (partID) => {
20
21
  const connectionStatus = useConnectionStatus(partID);
21
22
  const machineStatus = useMachineStatus(partID);
22
23
  const logs = useLogs();
24
+ const pendingSaveKey = $derived(`viam-pending-save-revision:${partID()}`);
23
25
  let didRecentlyEdit = $state(false);
24
26
  const isEditMode = $derived(environment.current.viewerMode === 'edit');
25
27
  const query = createRobotQuery(client, 'frameSystemConfig', () => ({
@@ -37,14 +39,19 @@ export const provideFrames = (partID) => {
37
39
  });
38
40
  const frames = $derived.by(() => {
39
41
  const frames = {};
40
- for (const { frame } of query.data ?? []) {
41
- if (frame === undefined) {
42
- continue;
42
+ if (!partConfig.hasPendingSave) {
43
+ for (const { frame } of query.data ?? []) {
44
+ if (frame === undefined) {
45
+ continue;
46
+ }
47
+ frames[frame.referenceFrame] = frame;
43
48
  }
44
- frames[frame.referenceFrame] = frame;
45
49
  }
46
- // Let config frames take priority if the user has made edits
47
- if (didRecentlyEdit || connectionStatus.current === MachineConnectionEvent.DISCONNECTED) {
50
+ // Let config frames take priority if the user has made edits,
51
+ // has a pending save, or is disconnected
52
+ if (didRecentlyEdit ||
53
+ partConfig.hasPendingSave ||
54
+ connectionStatus.current === MachineConnectionEvent.DISCONNECTED) {
48
55
  const mergedFrames = {
49
56
  ...frames,
50
57
  ...configFrames.current,
@@ -71,6 +78,45 @@ export const provideFrames = (partID) => {
71
78
  untrack(() => query.refetch());
72
79
  }
73
80
  });
81
+ $effect(() => {
82
+ const key = pendingSaveKey;
83
+ const storedRevision = sessionStorage.getItem(key);
84
+ if (!storedRevision) {
85
+ return;
86
+ }
87
+ if (!revision) {
88
+ if (!partConfig.hasPendingSave) {
89
+ partConfig.setPendingSave();
90
+ }
91
+ return;
92
+ }
93
+ if (revision === storedRevision) {
94
+ if (!partConfig.hasPendingSave) {
95
+ partConfig.setPendingSave();
96
+ }
97
+ return;
98
+ }
99
+ sessionStorage.removeItem(key);
100
+ partConfig.clearPendingSave();
101
+ didRecentlyEdit = true;
102
+ });
103
+ $effect(() => {
104
+ if (partConfig.hasPendingSave && revision) {
105
+ sessionStorage.setItem(pendingSaveKey, revision);
106
+ }
107
+ });
108
+ const componentSubtypeByName = $derived.by(() => {
109
+ const result = {};
110
+ for (const { name, api } of partConfig.current.components ?? []) {
111
+ if (api) {
112
+ const subtype = api.split(':').at(-1);
113
+ if (subtype) {
114
+ result[name] = subtype;
115
+ }
116
+ }
117
+ }
118
+ return result;
119
+ });
74
120
  $effect(() => {
75
121
  if (isEditMode) {
76
122
  didRecentlyEdit = true;
@@ -79,6 +125,7 @@ export const provideFrames = (partID) => {
79
125
  $effect.pre(() => {
80
126
  const currentResourcesByName = resourceByName.current;
81
127
  const currentPartID = partID();
128
+ const currentComponentSubtypeByName = componentSubtypeByName;
82
129
  // We only want to update whenever "current" or "resourceByName.current" changes
83
130
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
84
131
  current.length;
@@ -94,25 +141,18 @@ export const provideFrames = (partID) => {
94
141
  ? createPose(frame.physicalObject.center)
95
142
  : undefined;
96
143
  const resourceName = currentResourcesByName[frame.referenceFrame];
97
- const color = resourceNameToColor(resourceName);
144
+ const color = resourceNameToColor(resourceName) ??
145
+ subtypeToColor(currentComponentSubtypeByName[frame.referenceFrame]);
98
146
  const existing = entities.get(entityKey);
99
147
  if (existing) {
100
- if (!parent || parent === 'world') {
101
- existing.remove(traits.Parent);
102
- }
103
- else if (parent && existing.has(traits.Parent)) {
104
- existing.set(traits.Parent, parent);
105
- }
106
- else {
107
- existing.add(traits.Parent(parent));
108
- }
148
+ traits.setParentTrait(existing, parent);
109
149
  if (color) {
110
150
  existing.set(traits.Color, color);
111
151
  }
112
152
  if (center) {
113
153
  existing.set(traits.Center, center);
114
154
  }
115
- updateGeometryTrait(existing, frame.physicalObject);
155
+ traits.updateGeometryTrait(existing, frame.physicalObject);
116
156
  existing.set(traits.EditedPose, pose);
117
157
  continue;
118
158
  }
@@ -122,10 +162,8 @@ export const provideFrames = (partID) => {
122
162
  traits.EditedPose(pose),
123
163
  traits.FramesAPI,
124
164
  traits.ShowAxesHelper,
165
+ ...traits.getParentTrait(parent),
125
166
  ];
126
- if (parent && parent !== 'world') {
127
- entityTraits.push(traits.Parent(parent));
128
- }
129
167
  if (color) {
130
168
  entityTraits.push(traits.Color(color));
131
169
  }
@@ -1,4 +1,4 @@
1
- import { ArmClient, CameraClient, GantryClient, GripperClient } from '@viamrobotics/sdk';
1
+ import { ArmClient, BaseClient, CameraClient, GantryClient, GripperClient } from '@viamrobotics/sdk';
2
2
  import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
3
  import {} from 'koota';
4
4
  import { getContext, setContext, untrack } from 'svelte';
@@ -20,12 +20,14 @@ export const provideGeometries = (partID) => {
20
20
  const world = useWorld();
21
21
  const logs = useLogs();
22
22
  const arms = useResourceNames(partID, 'arm');
23
+ const bases = useResourceNames(partID, 'base');
23
24
  const cameras = useResourceNames(partID, 'camera');
24
25
  const grippers = useResourceNames(partID, 'gripper');
25
26
  const gantries = useResourceNames(partID, 'gantry');
26
27
  const settings = useSettings();
27
28
  const { refreshRates } = $derived(settings.current);
28
29
  const armClients = $derived(arms.current.map((arm) => createResourceClient(ArmClient, partID, () => arm.name)));
30
+ const baseClients = $derived(bases.current.map((base) => createResourceClient(BaseClient, partID, () => base.name)));
29
31
  const gripperClients = $derived(grippers.current.map((gripper) => createResourceClient(GripperClient, partID, () => gripper.name)));
30
32
  const cameraClients = $derived(cameras.current.map((camera) => createResourceClient(CameraClient, partID, () => camera.name)));
31
33
  const gantryClients = $derived(gantries.current.map((gantry) => createResourceClient(GantryClient, partID, () => gantry.name)));
@@ -35,10 +37,17 @@ export const provideGeometries = (partID) => {
35
37
  refetchInterval: interval === RefetchRates.MANUAL ? false : interval,
36
38
  });
37
39
  const armQueries = $derived(armClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
40
+ const baseQueries = $derived(baseClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
38
41
  const gripperQueries = $derived(gripperClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
39
42
  const cameraQueries = $derived(cameraClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
40
43
  const gantryQueries = $derived(gantryClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
41
- const queries = $derived([...armQueries, ...gripperQueries, ...cameraQueries, ...gantryQueries]);
44
+ const queries = $derived([
45
+ ...armQueries,
46
+ ...baseQueries,
47
+ ...gripperQueries,
48
+ ...cameraQueries,
49
+ ...gantryQueries,
50
+ ]);
42
51
  $effect(() => {
43
52
  if (interval === RefetchRates.FPS_30 || interval === RefetchRates.FPS_60) {
44
53
  return logs.add(`Fetching geometries every ${interval}ms...`);
@@ -44,11 +44,11 @@ export const provideLogs = () => {
44
44
  }
45
45
  if (logs.length > 200) {
46
46
  const log = logs.pop();
47
- if (log && level === 'error') {
47
+ if (log?.level === 'error') {
48
48
  errors.splice(errors.indexOf(log), 1);
49
49
  }
50
- else if (log && level === 'warn') {
51
- warnings.splice(errors.indexOf(log), 1);
50
+ else if (log?.level === 'warn') {
51
+ warnings.splice(warnings.indexOf(log), 1);
52
52
  }
53
53
  }
54
54
  });
@@ -3,6 +3,7 @@ import { type Frame } from '../frame';
3
3
  export interface PartConfig {
4
4
  components: {
5
5
  name: string;
6
+ api?: string;
6
7
  frame?: Frame;
7
8
  }[];
8
9
  fragment_mods?: {
@@ -13,6 +14,7 @@ export interface PartConfig {
13
14
  interface PartConfigContext {
14
15
  current: PartConfig;
15
16
  isDirty: boolean;
17
+ hasPendingSave: boolean;
16
18
  hasEditPermissions: boolean;
17
19
  componentNameToFragmentId: Record<string, string>;
18
20
  updateFrame: (componentName: string, referenceFrame: string, pose: Pose, geometry?: Frame['geometry']) => void;
@@ -20,6 +22,8 @@ interface PartConfigContext {
20
22
  createFrame: (componentName: string) => void;
21
23
  save: () => void;
22
24
  discardChanges: () => void;
25
+ clearPendingSave: () => void;
26
+ setPendingSave: () => void;
23
27
  }
24
28
  export declare const providePartConfig: (partID: () => string, params: () => AppEmbeddedPartConfigProps | undefined) => void;
25
29
  export declare const usePartConfig: () => PartConfigContext;