@viamrobotics/motion-tools 1.18.1 → 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.
- 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/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 +64 -42
- package/dist/ecs/traits.d.ts +12 -2
- package/dist/ecs/traits.js +19 -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
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
|
-
|
|
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
|
-
|
|
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,33 @@ export const updateTransform = (entity, { poseInObserverFrame, physicalObject, m
|
|
|
96
89
|
entity.remove(traits.Center);
|
|
97
90
|
}
|
|
98
91
|
}
|
|
99
|
-
|
|
100
|
-
|
|
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 (
|
|
105
|
+
if (metadata.invisible)
|
|
105
106
|
entity.add(traits.Invisible);
|
|
106
107
|
else
|
|
107
108
|
entity.remove(traits.Invisible);
|
|
108
|
-
const { colors, opacities } =
|
|
109
|
+
const { colors, opacities } = metadata;
|
|
109
110
|
if (colors) {
|
|
110
|
-
if (
|
|
111
|
-
updateColors(entity,
|
|
111
|
+
if (pointCloud) {
|
|
112
|
+
updateColors(entity, metadata);
|
|
112
113
|
}
|
|
113
114
|
else {
|
|
114
|
-
|
|
115
|
+
setColorTraits(entity, colors);
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
|
-
|
|
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);
|
|
118
|
+
entity.set(traits.Opacity, asOpacity(opacities, DEFAULT_OPACITY));
|
|
124
119
|
};
|
|
125
120
|
export const updateDrawing = (world, entities, drawing, api, options = { removable: true }) => {
|
|
126
121
|
const { poseInObserverFrame, physicalObject, metadata } = drawing;
|
|
@@ -137,9 +132,7 @@ export const updateDrawing = (world, entities, drawing, api, options = { removab
|
|
|
137
132
|
if (!world.has(entity))
|
|
138
133
|
return entities;
|
|
139
134
|
entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
|
|
140
|
-
|
|
141
|
-
if (parent && parent !== 'world')
|
|
142
|
-
entity.set(traits.Parent, parent);
|
|
135
|
+
traits.setParentTrait(entity, poseInObserverFrame?.referenceFrame);
|
|
143
136
|
if (metadata?.showAxesHelper)
|
|
144
137
|
entity.add(traits.ShowAxesHelper);
|
|
145
138
|
if (!metadata?.showAxesHelper)
|
|
@@ -181,15 +174,30 @@ const applyShape = (entity, { physicalObject, metadata }) => {
|
|
|
181
174
|
}
|
|
182
175
|
case 'points': {
|
|
183
176
|
const positions = asFloat32Array(geometryType.value.positions, inMeters);
|
|
177
|
+
const total = metadata?.chunks?.total;
|
|
184
178
|
const center = physicalObject?.center;
|
|
185
179
|
if (center)
|
|
186
180
|
entity.add(traits.Center(center));
|
|
187
181
|
addColorTraits(entity, colors ?? DEFAULT_POINTS_COLORS);
|
|
188
182
|
entity.add(traits.PointSize(geometryType.value.pointSize ?? DEFAULT_POINT_SIZE));
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
const vertexColors = isVertexColors(colors) ? colors : undefined;
|
|
184
|
+
const pointsMetadata = {
|
|
185
|
+
colors: vertexColors,
|
|
191
186
|
colorFormat: metadata?.colorFormat ?? ColorFormat.UNSPECIFIED,
|
|
192
|
-
|
|
187
|
+
opacities: metadata?.opacities,
|
|
188
|
+
};
|
|
189
|
+
if (total !== undefined && total > 0) {
|
|
190
|
+
const allocMetadata = {
|
|
191
|
+
...pointsMetadata,
|
|
192
|
+
colors: vertexColors ? new Uint8Array(0) : undefined,
|
|
193
|
+
};
|
|
194
|
+
const geometry = preAllocateBufferGeometry(total, STRIDE.POSITIONS, allocMetadata);
|
|
195
|
+
writeBufferGeometryRange(geometry, positions, 0, pointsMetadata);
|
|
196
|
+
entity.add(traits.BufferGeometry(geometry));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
entity.add(traits.BufferGeometry(createBufferGeometry(positions, pointsMetadata)));
|
|
200
|
+
}
|
|
193
201
|
entity.add(traits.Points);
|
|
194
202
|
break;
|
|
195
203
|
}
|
|
@@ -237,7 +245,6 @@ const applyShape = (entity, { physicalObject, metadata }) => {
|
|
|
237
245
|
};
|
|
238
246
|
const drawModel = (world, { referenceFrame, poseInObserverFrame, physicalObject, metadata }, api, { removable = true }) => {
|
|
239
247
|
const entities = [];
|
|
240
|
-
const parent = poseInObserverFrame?.referenceFrame;
|
|
241
248
|
const geometryType = physicalObject?.geometryType;
|
|
242
249
|
if (geometryType?.case !== 'model')
|
|
243
250
|
return entities;
|
|
@@ -245,9 +252,8 @@ const drawModel = (world, { referenceFrame, poseInObserverFrame, physicalObject,
|
|
|
245
252
|
traits.Name(referenceFrame),
|
|
246
253
|
traits.Pose(createPose(poseInObserverFrame?.pose)),
|
|
247
254
|
api,
|
|
255
|
+
...traits.getParentTrait(poseInObserverFrame?.referenceFrame),
|
|
248
256
|
];
|
|
249
|
-
if (parent && parent !== 'world')
|
|
250
|
-
baseTraits.push(traits.Parent(parent));
|
|
251
257
|
if (removable)
|
|
252
258
|
baseTraits.push(traits.Removable);
|
|
253
259
|
if (metadata?.invisible)
|
|
@@ -292,10 +298,19 @@ const parsePointCloud = (world, entity, pointCloud, metadata) => {
|
|
|
292
298
|
let vertexColors = pointcloud.colors;
|
|
293
299
|
if (colors && colors.length > 0)
|
|
294
300
|
vertexColors = parseColors(colors, numPoints);
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
301
|
+
const total = metadata.chunks?.total;
|
|
302
|
+
const chunkMetadata = { colors: vertexColors ?? undefined, colorFormat };
|
|
303
|
+
let geometry;
|
|
304
|
+
if (total !== undefined && total > numPoints) {
|
|
305
|
+
geometry = preAllocateBufferGeometry(total, STRIDE.POSITIONS, {
|
|
306
|
+
...chunkMetadata,
|
|
307
|
+
colors: vertexColors ? new Uint8Array(0) : undefined,
|
|
308
|
+
});
|
|
309
|
+
writeBufferGeometryRange(geometry, pointcloud.positions, 0, chunkMetadata);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
geometry = createBufferGeometry(pointcloud.positions, chunkMetadata);
|
|
313
|
+
}
|
|
299
314
|
entity.add(traits.BufferGeometry(geometry));
|
|
300
315
|
entity.add(traits.Points);
|
|
301
316
|
});
|
|
@@ -408,7 +423,7 @@ const updateShape = (entity, { physicalObject, metadata }) => {
|
|
|
408
423
|
}
|
|
409
424
|
}
|
|
410
425
|
};
|
|
411
|
-
const addColorTraits = (entity, colors) => {
|
|
426
|
+
export const addColorTraits = (entity, colors) => {
|
|
412
427
|
if (isVertexColors(colors)) {
|
|
413
428
|
entity.add(traits.Colors(colors));
|
|
414
429
|
}
|
|
@@ -416,13 +431,20 @@ const addColorTraits = (entity, colors) => {
|
|
|
416
431
|
entity.add(traits.Color(asRGB(colors, rgb)));
|
|
417
432
|
}
|
|
418
433
|
};
|
|
419
|
-
const setColorTraits = (entity, colors) => {
|
|
434
|
+
export const setColorTraits = (entity, colors) => {
|
|
420
435
|
if (isVertexColors(colors)) {
|
|
421
|
-
entity.
|
|
436
|
+
if (entity.has(traits.Colors))
|
|
437
|
+
entity.set(traits.Colors, colors);
|
|
438
|
+
else
|
|
439
|
+
entity.add(traits.Colors(colors));
|
|
422
440
|
entity.remove(traits.Color);
|
|
423
441
|
}
|
|
424
442
|
else {
|
|
425
|
-
|
|
443
|
+
const color = asRGB(colors, rgb);
|
|
444
|
+
if (entity.has(traits.Color))
|
|
445
|
+
entity.set(traits.Color, color);
|
|
446
|
+
else
|
|
447
|
+
entity.add(traits.Color(color));
|
|
426
448
|
entity.remove(traits.Colors);
|
|
427
449
|
}
|
|
428
450
|
};
|
package/dist/ecs/traits.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/ecs/traits.js
CHANGED
|
@@ -124,9 +124,14 @@ export const DotColors = trait(() => new Uint8Array());
|
|
|
124
124
|
*/
|
|
125
125
|
export const DotSize = trait(() => 10);
|
|
126
126
|
export const ReferenceFrame = trait(() => true);
|
|
127
|
+
/**
|
|
128
|
+
* Tracks chunk loading progress for progressively-loaded entities.
|
|
129
|
+
* `loaded` is the number of elements received so far; `total` is the target.
|
|
130
|
+
*/
|
|
131
|
+
export const ChunkProgress = trait({ loaded: 0, total: 0 });
|
|
127
132
|
export const SelectToolInteractionLayer = trait(() => true);
|
|
128
133
|
/**
|
|
129
|
-
* This entity can be
|
|
134
|
+
* This entity can be safely removed from the scene by the user
|
|
130
135
|
*/
|
|
131
136
|
export const Removable = trait(() => true);
|
|
132
137
|
export const Geometry = (geometry) => {
|
|
@@ -144,6 +149,19 @@ export const Geometry = (geometry) => {
|
|
|
144
149
|
}
|
|
145
150
|
return ReferenceFrame;
|
|
146
151
|
};
|
|
152
|
+
export const getParentTrait = (parent) => !parent || parent === 'world' ? [] : [Parent(parent)];
|
|
153
|
+
export const setParentTrait = (entity, parent) => {
|
|
154
|
+
if (!parent || parent === 'world') {
|
|
155
|
+
entity.remove(Parent);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (entity.has(Parent)) {
|
|
159
|
+
entity.set(Parent, parent);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
entity.add(Parent(parent));
|
|
163
|
+
}
|
|
164
|
+
};
|
|
147
165
|
export const updateGeometryTrait = (entity, geometry) => {
|
|
148
166
|
if (!geometry) {
|
|
149
167
|
entity.remove(Box, Capsule, Sphere, BufferGeometry);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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([
|
|
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
|
|
47
|
+
if (log?.level === 'error') {
|
|
48
48
|
errors.splice(errors.indexOf(log), 1);
|
|
49
49
|
}
|
|
50
|
-
else if (log
|
|
51
|
-
warnings.splice(
|
|
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;
|
|
@@ -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
|
};
|