@viamrobotics/motion-tools 1.12.2 → 1.13.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.
Files changed (29) hide show
  1. package/dist/assets/ferndale_studio_11_1k.hdr +0 -0
  2. package/dist/components/App.svelte +11 -25
  3. package/dist/components/App.svelte.d.ts +3 -3
  4. package/dist/components/Geometry.svelte +42 -42
  5. package/dist/components/Scene.svelte +4 -1
  6. package/dist/components/SceneProviders.svelte +2 -0
  7. package/dist/components/Selected.svelte +9 -9
  8. package/dist/components/StaticGeometries.svelte +5 -2
  9. package/dist/components/overlay/Details.svelte +3 -3
  10. package/dist/components/overlay/FloatingPanel.svelte +3 -0
  11. package/dist/components/overlay/FloatingPanel.svelte.d.ts +1 -0
  12. package/dist/components/overlay/LiveUpdatesBanner.svelte +3 -3
  13. package/dist/components/overlay/Logs.svelte +75 -0
  14. package/dist/components/overlay/__tests__/__fixtures__/resource.d.ts +10 -1
  15. package/dist/components/overlay/__tests__/__fixtures__/resource.js +1 -0
  16. package/dist/components/overlay/dashboard/Dashboard.svelte +1 -1
  17. package/dist/components/overlay/left-pane/TreeContainer.svelte +0 -3
  18. package/dist/hooks/use3DModels.svelte.js +10 -0
  19. package/dist/hooks/useConfigFrames.svelte.d.ts +9 -0
  20. package/dist/hooks/useConfigFrames.svelte.js +92 -0
  21. package/dist/hooks/useFramelessComponents.svelte.js +2 -2
  22. package/dist/hooks/useFrames.svelte.d.ts +1 -6
  23. package/dist/hooks/useFrames.svelte.js +39 -145
  24. package/dist/hooks/usePartConfig.svelte.d.ts +10 -19
  25. package/dist/hooks/usePartConfig.svelte.js +138 -165
  26. package/dist/hooks/useVisibility.svelte.js +0 -12
  27. package/package.json +1 -1
  28. package/dist/components/overlay/left-pane/Logs.svelte +0 -52
  29. /package/dist/components/overlay/{left-pane/Logs.svelte.d.ts → Logs.svelte.d.ts} +0 -0
@@ -1,25 +1,19 @@
1
1
  import { createFrame } from '../frame';
2
2
  import { createPoseFromFrame } from '../transform';
3
3
  import { Struct, Pose } from '@viamrobotics/sdk';
4
+ import { createAppMutation, createAppQuery } from '@viamrobotics/svelte-sdk';
4
5
  import { getContext, setContext } from 'svelte';
5
6
  const key = Symbol('part-config-context');
6
- export const providePartConfig = (params) => {
7
- const { appEmbeddedPartConfigProps, standalonePartConfigProps } = params();
8
- let _localPartConfig;
9
- if (appEmbeddedPartConfigProps) {
10
- _localPartConfig = new AppEmbeddedPartConfig(appEmbeddedPartConfigProps);
11
- }
12
- else if (standalonePartConfigProps) {
13
- _localPartConfig = new StandalonePartConfig(standalonePartConfigProps);
14
- }
15
- else {
16
- throw new Error('No part config provided');
17
- }
7
+ export const providePartConfig = (partID, params) => {
8
+ const props = $derived(params());
9
+ const config = $derived(props ? useEmbeddedPartConfig(props) : useStandalonePartConfig(partID));
10
+ const getCurrent = () => {
11
+ return (config.current.toJson?.() ?? { components: [] });
12
+ };
13
+ const current = $derived(getCurrent());
18
14
  const createFragmentFrame = (fragmentId, componentName) => {
19
- const newConfig = _localPartConfig.getLocalPartConfig().toJson();
20
- if (newConfig.fragment_mods === undefined) {
21
- newConfig.fragment_mods = [];
22
- }
15
+ const newConfig = getCurrent();
16
+ newConfig.fragment_mods ??= [];
23
17
  let fragmentMod = newConfig.fragment_mods.find((mod) => mod.fragment_id === fragmentId);
24
18
  if (fragmentMod === undefined) {
25
19
  fragmentMod = {
@@ -35,23 +29,19 @@ export const providePartConfig = (params) => {
35
29
  },
36
30
  };
37
31
  fragmentMod.mods.push(frame);
38
- const configStruct = Struct.fromJson(newConfig);
39
- _localPartConfig.setLocalPartConfig(configStruct);
32
+ config.set(newConfig);
40
33
  };
41
34
  const createPartFrame = (componentName) => {
42
- const newConfig = _localPartConfig.getLocalPartConfig().toJson();
35
+ const newConfig = getCurrent();
43
36
  const component = newConfig?.components?.find((comp) => comp.name === componentName);
44
37
  if (component) {
45
38
  component.frame = createFrame();
46
39
  }
47
- const configStruct = Struct.fromJson(newConfig);
48
- _localPartConfig.setLocalPartConfig(configStruct);
40
+ config.set(newConfig);
49
41
  };
50
42
  const updateFragmentFrame = (fragmentId, componentName, referenceFrame, framePosition, frameGeometry) => {
51
- const newConfig = _localPartConfig.getLocalPartConfig().toJson();
52
- if (newConfig.fragment_mods === undefined) {
53
- newConfig.fragment_mods = [];
54
- }
43
+ const newConfig = getCurrent();
44
+ newConfig.fragment_mods ??= [];
55
45
  let fragmentMod = newConfig.fragment_mods.find((mod) => mod.fragment_id === fragmentId);
56
46
  if (fragmentMod === undefined) {
57
47
  fragmentMod = {
@@ -99,11 +89,12 @@ export const providePartConfig = (params) => {
99
89
  else {
100
90
  fragmentMod.mods.push(frame);
101
91
  }
102
- _localPartConfig.setLocalPartConfig(Struct.fromJson(newConfig));
92
+ config.set(newConfig);
103
93
  };
104
94
  const updatePartFrame = (componentName, referenceFrame, pose, geometry) => {
105
- const newConfig = _localPartConfig.getLocalPartConfig().toJson();
106
- const component = newConfig?.components?.find((comp) => comp.name === componentName);
95
+ const newConfig = getCurrent();
96
+ const component = newConfig.components?.find(({ name }) => name === componentName);
97
+ console.log('hi', newConfig, componentName);
107
98
  if (!component) {
108
99
  return;
109
100
  }
@@ -131,24 +122,20 @@ export const providePartConfig = (params) => {
131
122
  }
132
123
  }
133
124
  }
134
- const configStruct = Struct.fromJson(newConfig);
135
- _localPartConfig.setLocalPartConfig(configStruct);
125
+ config.set(newConfig);
136
126
  };
137
127
  const deletePartFrame = (componentName) => {
138
- const newConfig = _localPartConfig.getLocalPartConfig().toJson();
139
- const component = newConfig?.components?.find((comp) => comp.name === componentName);
128
+ const newConfig = getCurrent();
129
+ const component = newConfig?.components?.find(({ name }) => name === componentName);
140
130
  if (!component) {
141
131
  return;
142
132
  }
143
133
  delete component.frame;
144
- const configStruct = Struct.fromJson(newConfig);
145
- _localPartConfig.setLocalPartConfig(configStruct);
134
+ config.set(newConfig);
146
135
  };
147
136
  const deleteFragmentFrame = (fragmentId, componentName) => {
148
- const newConfig = _localPartConfig.getLocalPartConfig().toJson();
149
- if (newConfig.fragment_mods === undefined) {
150
- newConfig.fragment_mods = [];
151
- }
137
+ const newConfig = getCurrent();
138
+ newConfig.fragment_mods ??= [];
152
139
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
140
  let fragmentMod = newConfig.fragment_mods.find((mod) => mod.fragment_id === fragmentId);
154
141
  if (fragmentMod === undefined) {
@@ -164,12 +151,23 @@ export const providePartConfig = (params) => {
164
151
  [modUnSetPath]: '',
165
152
  },
166
153
  });
167
- const configStruct = Struct.fromJson(newConfig);
168
- _localPartConfig.setLocalPartConfig(configStruct);
154
+ config.set(newConfig);
169
155
  };
170
156
  setContext(key, {
157
+ get current() {
158
+ return current;
159
+ },
160
+ get componentNameToFragmentId() {
161
+ return config.componentNameToFragmentId;
162
+ },
163
+ get isDirty() {
164
+ return config.isDirty;
165
+ },
166
+ get hasEditPermissions() {
167
+ return config.hasEditPermissions;
168
+ },
171
169
  updateFrame: (componentName, referenceFrame, framePosition, frameGeometry) => {
172
- const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
170
+ const fragmentId = config.componentNameToFragmentId[componentName];
173
171
  if (fragmentId !== undefined) {
174
172
  updateFragmentFrame(fragmentId, componentName, referenceFrame, framePosition, frameGeometry);
175
173
  }
@@ -178,7 +176,7 @@ export const providePartConfig = (params) => {
178
176
  }
179
177
  },
180
178
  deleteFrame: (componentName) => {
181
- const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
179
+ const fragmentId = config.componentNameToFragmentId[componentName];
182
180
  if (fragmentId !== undefined) {
183
181
  deleteFragmentFrame(fragmentId, componentName);
184
182
  }
@@ -187,7 +185,7 @@ export const providePartConfig = (params) => {
187
185
  }
188
186
  },
189
187
  createFrame: (componentName) => {
190
- const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
188
+ const fragmentId = config.componentNameToFragmentId[componentName];
191
189
  if (fragmentId !== undefined) {
192
190
  createFragmentFrame(fragmentId, componentName);
193
191
  }
@@ -195,137 +193,112 @@ export const providePartConfig = (params) => {
195
193
  createPartFrame(componentName);
196
194
  }
197
195
  },
198
- saveLocalPartConfig: () => {
199
- _localPartConfig.saveLocalPartConfig?.();
200
- },
201
- resetLocalPartConfig: () => {
202
- _localPartConfig.resetLocalPartConfig?.();
196
+ save: () => config.save?.(),
197
+ discardChanges: () => config.discardChanges?.(),
198
+ });
199
+ };
200
+ export const usePartConfig = () => {
201
+ return getContext(key);
202
+ };
203
+ const useEmbeddedPartConfig = (props) => {
204
+ return {
205
+ hasEditPermissions: true,
206
+ get isDirty() {
207
+ return props.isDirty;
203
208
  },
204
- get localPartConfig() {
205
- return _localPartConfig.getLocalPartConfig();
209
+ get current() {
210
+ return props.current ?? new Struct();
206
211
  },
207
212
  get componentNameToFragmentId() {
208
- return _localPartConfig.componentNameToFragmentId();
209
- },
210
- get isDirty() {
211
- return _localPartConfig.isDirty();
213
+ return props.componentToFragId;
212
214
  },
213
- get hasEditPermissions() {
214
- return _localPartConfig.hasEditPermissions();
215
+ set(config) {
216
+ const struct = Struct.fromJson(config);
217
+ return props.setLocalPartConfig(struct);
215
218
  },
216
- });
217
- };
218
- export const usePartConfig = () => {
219
- return getContext(key);
219
+ };
220
220
  };
221
- class AppEmbeddedPartConfig {
222
- _appEmbeddedPartConfigProps;
223
- constructor(appEmbeddedPartConfigProps) {
224
- this._appEmbeddedPartConfigProps = appEmbeddedPartConfigProps;
225
- }
226
- isDirty() {
227
- return this._appEmbeddedPartConfigProps.isDirty();
228
- }
229
- getLocalPartConfig() {
230
- return this._appEmbeddedPartConfigProps.getLocalPartConfig();
231
- }
232
- setLocalPartConfig(config) {
233
- return this._appEmbeddedPartConfigProps.setLocalPartConfig(config);
234
- }
235
- componentNameToFragmentId() {
236
- return this._appEmbeddedPartConfigProps.getComponentToFragId();
237
- }
238
- hasEditPermissions() {
239
- return true;
240
- }
241
- }
242
- class StandalonePartConfig {
243
- _standalonePartConfigProps;
244
- _isDirty = $state(false);
245
- _hasEditPermissions = $state(false);
246
- _networkPartConfig = $state();
247
- _localPartConfig = $state();
248
- _partName = $state();
249
- _componentNameToFragmentId = $state();
250
- constructor(standalonePartConfigProps) {
251
- this._standalonePartConfigProps = standalonePartConfigProps;
252
- $effect.pre(() => {
253
- const initLocalConfig = async () => {
254
- const partResponse = await standalonePartConfigProps
255
- .viamClient()
256
- ?.appClient.getRobotPart(standalonePartConfigProps.partID());
257
- if (JSON.parse(partResponse?.configJson ?? 'null') === null) {
258
- // no config returned here indicates this api key has no permission to update config
259
- return;
260
- }
261
- this._hasEditPermissions = true;
262
- const configJson = JSON.parse(partResponse?.configJson ?? '{}');
263
- this._networkPartConfig = Struct.fromJson(configJson);
264
- this._localPartConfig = Struct.fromJson(configJson);
265
- this._partName = partResponse?.part?.name;
266
- const componentNameToFragmentId = {};
267
- const fragmentRequests = [];
268
- if (configJson.fragments) {
269
- for (const fragmentId of configJson.fragments) {
270
- //TODO: right now the json could be just a list of strings or an object with an id prop
271
- const fragId = typeof fragmentId === 'string' ? fragmentId : fragmentId.id;
272
- fragmentRequests.push(standalonePartConfigProps.viamClient()?.appClient.getFragment(fragId));
273
- }
274
- const fragementResponses = await Promise.all(fragmentRequests);
275
- for (const fragmentResponse of fragementResponses) {
276
- const fragmentId = fragmentResponse?.id;
277
- if (!fragmentId) {
278
- continue;
279
- }
280
- const components = fragmentResponse?.fragment?.fields['components']?.kind;
281
- if (components?.case === 'listValue') {
282
- for (const component of components.value.values) {
283
- if (component.kind.case === 'structValue') {
284
- const componentName = component.kind.value.fields['name'].kind;
285
- if (componentName.case === 'stringValue') {
286
- componentNameToFragmentId[componentName.value] = fragmentId;
287
- }
288
- }
289
- }
221
+ const useStandalonePartConfig = (partID) => {
222
+ const partQuery = createAppQuery('getRobotPart', () => [partID()], {
223
+ refetchInterval: false,
224
+ });
225
+ const partName = $derived(partQuery.data?.part?.name);
226
+ const configJSON = $derived.by(() => {
227
+ if (!partQuery.data?.configJson) {
228
+ return undefined;
229
+ }
230
+ try {
231
+ return JSON.parse(partQuery.data.configJson);
232
+ }
233
+ catch {
234
+ return undefined;
235
+ }
236
+ });
237
+ let networkPartConfig = $derived(configJSON ? Struct.fromJson(configJSON) : undefined);
238
+ let current = $state.raw();
239
+ let isDirty = $state(false);
240
+ const hasEditPermissions = $derived(networkPartConfig !== undefined);
241
+ const fragmentQueries = $derived((configJSON?.fragments ?? []).map((fragmentId) => {
242
+ const id = typeof fragmentId === 'string' ? fragmentId : fragmentId.id;
243
+ return createAppQuery('getFragment', () => [id], { refetchInterval: false });
244
+ }));
245
+ const componentNameToFragmentId = $derived.by(() => {
246
+ const results = {};
247
+ for (const query of fragmentQueries) {
248
+ if (!query.data) {
249
+ continue;
250
+ }
251
+ const fragmentId = query.data.id;
252
+ const components = query.data?.fragment?.fields['components']?.kind;
253
+ if (components?.case === 'listValue') {
254
+ for (const component of components.value.values) {
255
+ if (component.kind.case === 'structValue') {
256
+ const componentName = component.kind.value.fields['name']?.kind;
257
+ if (componentName.case === 'stringValue') {
258
+ results[componentName.value] = fragmentId;
290
259
  }
291
260
  }
292
- this._componentNameToFragmentId = componentNameToFragmentId;
293
261
  }
294
- };
295
- initLocalConfig();
296
- });
297
- }
298
- getLocalPartConfig() {
299
- return this._localPartConfig ?? new Struct();
300
- }
301
- setLocalPartConfig(config) {
302
- this._localPartConfig = config;
303
- this._isDirty = true;
304
- }
305
- isDirty() {
306
- return this._isDirty;
307
- }
308
- hasEditPermissions() {
309
- return this._hasEditPermissions;
310
- }
311
- componentNameToFragmentId() {
312
- return this._componentNameToFragmentId ?? {};
313
- }
314
- async saveLocalPartConfig() {
315
- if (!this._localPartConfig || !this._partName) {
316
- return;
262
+ }
317
263
  }
318
- this._networkPartConfig = this._localPartConfig;
319
- await this._standalonePartConfigProps
320
- .viamClient()
321
- ?.appClient.updateRobotPart(this._standalonePartConfigProps.partID(), this._partName, this._localPartConfig);
322
- this._isDirty = false;
323
- }
324
- async resetLocalPartConfig() {
325
- if (!this._networkPartConfig) {
264
+ return results;
265
+ });
266
+ $effect.pre(() => {
267
+ if (!networkPartConfig) {
268
+ // no config returned here indicates this api key has no permission to update config
326
269
  return;
327
270
  }
328
- this._localPartConfig = this._networkPartConfig;
329
- this._isDirty = false;
330
- }
331
- }
271
+ current = networkPartConfig;
272
+ });
273
+ const updateRobotPartMutation = createAppMutation('updateRobotPart');
274
+ return {
275
+ get current() {
276
+ return current ?? new Struct();
277
+ },
278
+ get isDirty() {
279
+ return isDirty;
280
+ },
281
+ get hasEditPermissions() {
282
+ return hasEditPermissions;
283
+ },
284
+ get componentNameToFragmentId() {
285
+ return componentNameToFragmentId;
286
+ },
287
+ set(config) {
288
+ current = Struct.fromJson(config);
289
+ isDirty = true;
290
+ },
291
+ async save() {
292
+ if (!current || !partName) {
293
+ return;
294
+ }
295
+ networkPartConfig = current;
296
+ await updateRobotPartMutation.mutateAsync([partID(), partName, current]);
297
+ isDirty = false;
298
+ },
299
+ discardChanges() {
300
+ current = networkPartConfig;
301
+ isDirty = false;
302
+ },
303
+ };
304
+ };
@@ -1,20 +1,8 @@
1
- import { get, set } from 'idb-keyval';
2
1
  import { getContext, setContext } from 'svelte';
3
2
  import { SvelteMap } from 'svelte/reactivity';
4
3
  const key = Symbol('object-visibility-context');
5
- const idbKey = 'object-visibility';
6
4
  export const provideVisibility = () => {
7
5
  const map = new SvelteMap();
8
- get(idbKey).then((entries) => {
9
- if (entries) {
10
- for (const [key, value] of entries) {
11
- map.set(key, value);
12
- }
13
- }
14
- });
15
- $effect(() => {
16
- set(idbKey, [...map.entries()]);
17
- });
18
6
  setContext(key, map);
19
7
  };
20
8
  export const useVisibility = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.12.2",
3
+ "version": "1.13.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,52 +0,0 @@
1
- <script lang="ts">
2
- import { useLogs } from '../../../hooks/useLogs.svelte'
3
- import Drawer from './Drawer.svelte'
4
-
5
- const logs = useLogs()
6
- </script>
7
-
8
- <Drawer name="Logs">
9
- {#snippet titleAlert()}
10
- {#if logs.warnings.length > 0}
11
- <span class="mr-1 rounded bg-yellow-700 px-1 py-0.5 text-xs text-white">
12
- {logs.warnings.length}
13
- </span>
14
- {/if}
15
-
16
- {#if logs.errors.length > 0}
17
- <span class="mr-1 rounded bg-red-700 px-1 py-0.5 text-xs text-white">
18
- {logs.errors.length}
19
- </span>
20
- {/if}
21
- {/snippet}
22
-
23
- <div class="flex h-64 flex-col gap-2 overflow-auto p-3">
24
- {#each logs.current as log (log.uuid)}
25
- <div>
26
- <div class="flex flex-wrap items-center gap-1.5">
27
- <div
28
- class={[
29
- 'h-2 w-2 rounded-full',
30
- {
31
- 'bg-danger-dark': log.level === 'error',
32
- 'bg-amber-300': log.level === 'warn',
33
- 'bg-blue-400': log.level === 'info',
34
- },
35
- ]}
36
- ></div>
37
- <div class="text-subtle-2">{log.timestamp}</div>
38
- </div>
39
- <div>
40
- {#if log.count > 1}
41
- <span class="mr-1 rounded bg-green-700 px-1 py-0.5 text-xs text-white">
42
- {log.count}
43
- </span>
44
- {/if}
45
- {log.message}
46
- </div>
47
- </div>
48
- {:else}
49
- No logs
50
- {/each}
51
- </div>
52
- </Drawer>