@viamrobotics/motion-tools 0.14.2 → 0.14.3

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.
@@ -4,7 +4,6 @@
4
4
  import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools'
5
5
  import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
6
6
  import type { Struct } from '@viamrobotics/sdk'
7
-
8
7
  import Scene from './Scene.svelte'
9
8
  import TreeContainer from './Tree/TreeContainer.svelte'
10
9
  import Details from './Details.svelte'
@@ -16,7 +15,7 @@
16
15
  import { domPortal } from '../portal'
17
16
  import { provideSettings } from '../hooks/useSettings.svelte'
18
17
  import FileDrop from './FileDrop.svelte'
19
- import WeblabProvider from './weblab/WeblabProvider.svelte'
18
+ import { provideWeblabs } from '../hooks/useWeblabs.svelte'
20
19
  import { providePartConfig } from '../hooks/usePartConfig.svelte'
21
20
  import { useViamClient } from '@viamrobotics/svelte-sdk'
22
21
  import LiveUpdatesBanner from './LiveUpdatesBanner.svelte'
@@ -54,6 +53,7 @@
54
53
 
55
54
  createPartIDContext(() => partID)
56
55
 
56
+ provideWeblabs()
57
57
  provideToast()
58
58
 
59
59
  let root = $state.raw<HTMLElement>()
@@ -83,41 +83,39 @@
83
83
  <SvelteQueryDevtools initialIsOpen />
84
84
  {/if}
85
85
 
86
- <WeblabProvider>
87
- <div
88
- class="relative h-full w-full overflow-hidden"
89
- bind:this={root}
90
- >
91
- <Canvas renderMode="always">
92
- <World>
93
- <SceneProviders>
94
- {#snippet children({ focus })}
95
- <Scene>
96
- {@render appChildren?.()}
97
- </Scene>
98
-
99
- <XR {@attach domPortal(root)} />
100
-
101
- <Dashboard {@attach domPortal(root)} />
102
- <Details {@attach domPortal(root)} />
103
- {#if environment.current.isStandalone}
104
- <LiveUpdatesBanner {@attach domPortal(root)} />
105
- {/if}
106
-
107
- {#if !focus}
108
- <TreeContainer {@attach domPortal(root)} />
109
- {/if}
110
-
111
- {#if !focus && settings.current.enableArmPositionsWidget}
112
- <ArmPositions {@attach domPortal(root)} />
113
- {/if}
114
-
115
- <FileDrop {@attach domPortal(root)} />
116
- {/snippet}
117
- </SceneProviders>
118
- </World>
119
- </Canvas>
120
-
121
- <ToastContainer />
122
- </div>
123
- </WeblabProvider>
86
+ <div
87
+ class="relative h-full w-full overflow-hidden"
88
+ bind:this={root}
89
+ >
90
+ <Canvas renderMode="always">
91
+ <World>
92
+ <SceneProviders>
93
+ {#snippet children({ focus })}
94
+ <Scene>
95
+ {@render appChildren?.()}
96
+ </Scene>
97
+
98
+ <XR {@attach domPortal(root)} />
99
+
100
+ <Dashboard {@attach domPortal(root)} />
101
+ <Details {@attach domPortal(root)} />
102
+ {#if environment.current.isStandalone}
103
+ <LiveUpdatesBanner {@attach domPortal(root)} />
104
+ {/if}
105
+
106
+ {#if !focus}
107
+ <TreeContainer {@attach domPortal(root)} />
108
+ {/if}
109
+
110
+ {#if !focus && settings.current.enableArmPositionsWidget}
111
+ <ArmPositions {@attach domPortal(root)} />
112
+ {/if}
113
+
114
+ <FileDrop {@attach domPortal(root)} />
115
+ {/snippet}
116
+ </SceneProviders>
117
+ </World>
118
+ </Canvas>
119
+
120
+ <ToastContainer />
121
+ </div>
@@ -26,6 +26,7 @@
26
26
  import { useFrames } from '../hooks/useFrames.svelte'
27
27
  import { usePartConfig } from '../hooks/usePartConfig.svelte'
28
28
  import { DetailConfigUpdater } from '../Detail.svelte'
29
+ import { useWeblabs } from '../hooks/useWeblabs.svelte'
29
30
 
30
31
  const { ...rest } = $props()
31
32
 
@@ -36,6 +37,7 @@
36
37
  const partConfig = usePartConfig()
37
38
  const selectedObject = useSelectedObject()
38
39
  const selectedObject3d = useSelectedObject3d()
40
+ const weblab = useWeblabs()
39
41
 
40
42
  const object = $derived(focusedObject.current ?? selectedObject.current)
41
43
  const object3d = $derived(focusedObject3d.current ?? selectedObject3d.current)
@@ -99,6 +101,48 @@
99
101
  stop()
100
102
  }
101
103
  })
104
+
105
+ const getCopyClipboardText = () => {
106
+ if (weblab.isActive('MOTION_TOOLS_EDIT_FRAME')) {
107
+ return JSON.stringify(
108
+ {
109
+ worldPosition: worldPosition,
110
+ worldOrientation: worldOrientation,
111
+ localPosition: {
112
+ x: localPose?.x,
113
+ y: localPose?.y,
114
+ z: localPose?.z,
115
+ },
116
+ localOrientation: {
117
+ x: localPose?.oX,
118
+ y: localPose?.oY,
119
+ z: localPose?.oZ,
120
+ th: localPose?.theta,
121
+ },
122
+ geometry: {
123
+ type: geometryType,
124
+ value: object?.geometry?.geometryType.value,
125
+ },
126
+ parentFrame: referenceFrame,
127
+ },
128
+ null,
129
+ 2
130
+ )
131
+ } else {
132
+ return JSON.stringify(
133
+ {
134
+ worldPosition: worldPosition,
135
+ worldOrientation: worldOrientation,
136
+ geometry: {
137
+ type: geometryType,
138
+ value: object?.geometry?.geometryType.value,
139
+ },
140
+ },
141
+ null,
142
+ 2
143
+ )
144
+ }
145
+ }
102
146
  </script>
103
147
 
104
148
  {#snippet ImmutableField({
@@ -199,7 +243,7 @@
199
243
 
200
244
  <button
201
245
  onclick={async () => {
202
- navigator.clipboard.writeText(JSON.stringify($state.snapshot(object)))
246
+ navigator.clipboard.writeText(getCopyClipboardText())
203
247
  copied = true
204
248
  setTimeout(() => (copied = false), 1000)
205
249
  }}
@@ -430,7 +474,7 @@
430
474
  <div class="flex items-center gap-2">
431
475
  {@render GeometryAttribute({
432
476
  label: 'r',
433
- ariaLabel: 'sphere dimensions radius value input',
477
+ ariaLabel: 'sphere dimensions radius value',
434
478
  value: radiusMm ? radiusMm.toFixed(2) : '-',
435
479
  onInput: (value) =>
436
480
  detailConfigUpdater.updateGeometry({ type: 'sphere', r: parseFloat(value) }),
@@ -523,7 +567,7 @@
523
567
  <Button
524
568
  variant="danger"
525
569
  class="mt-2 w-full"
526
- onclick={() => detailConfigUpdater.deleteFrame()}>Delete Frame</Button
570
+ onclick={() => detailConfigUpdater.deleteFrame()}>Delete frame</Button
527
571
  >
528
572
  {/if}
529
573
  </WeblabActive>
@@ -15,7 +15,7 @@
15
15
  class="flex flex-col items-start rounded border-l-4 border-yellow-600 bg-yellow-50 px-4 py-2"
16
16
  >
17
17
  <p>
18
- <strong>Live Updates Paused</strong>
18
+ <strong>Live updates paused</strong>
19
19
  </p>
20
20
 
21
21
  <p class="text-sm">
@@ -9,7 +9,7 @@
9
9
  </script>
10
10
 
11
11
  <Drawer name="Add frames">
12
- <div class="flex h-64 w-60 flex-col gap-2 overflow-auto p-3">
12
+ <div class="flex max-h-64 flex-col gap-2 overflow-auto p-3">
13
13
  {#if framelessComponents.current.length > 0}
14
14
  <ul class="space-y-1">
15
15
  {#each framelessComponents.current as component (component)}
@@ -7,9 +7,10 @@
7
7
  name: string
8
8
  defaultOpen?: boolean
9
9
  children: Snippet
10
+ titleAlert?: Snippet
10
11
  }
11
12
 
12
- let { name, children, defaultOpen = false }: Props = $props()
13
+ let { name, children, titleAlert, defaultOpen = false }: Props = $props()
13
14
 
14
15
  const expanded = $derived(new PersistedState(`${name}-expanded`, defaultOpen))
15
16
  </script>
@@ -24,9 +25,10 @@
24
25
  label="unfold more icon"
25
26
  variant="ghost"
26
27
  cx="size-6"
27
- on:click={() => (expanded.current = !expanded.current)}
28
+ onclick={() => (expanded.current = !expanded.current)}
28
29
  />
29
30
  {name}
31
+ {@render titleAlert?.()}
30
32
  </h3>
31
33
  </button>
32
34
 
@@ -3,6 +3,7 @@ interface Props {
3
3
  name: string;
4
4
  defaultOpen?: boolean;
5
5
  children: Snippet;
6
+ titleAlert?: Snippet;
6
7
  }
7
8
  declare const Drawer: import("svelte").Component<Props, {}, "">;
8
9
  type Drawer = ReturnType<typeof Drawer>;
@@ -1,14 +1,56 @@
1
1
  <script lang="ts">
2
+ import { useFrames } from '../../hooks/useFrames.svelte'
3
+ import { useGeometries } from '../../hooks/useGeometries.svelte'
2
4
  import { useLogs } from '../../hooks/useLogs.svelte'
5
+ import { usePointClouds } from '../../hooks/usePointclouds.svelte'
3
6
  import Drawer from './Drawer.svelte'
4
7
 
8
+ const frames = useFrames()
9
+ const geometries = useGeometries()
10
+ const pointclouds = usePointClouds()
5
11
  const logs = useLogs()
6
- const truncated = $derived(logs.current.slice(0, 100).reverse())
12
+
13
+ $effect(() => {
14
+ if (frames.error) {
15
+ const message = `Frames: ${frames.error.message}`
16
+ logs.add(message, 'error')
17
+ }
18
+ })
19
+
20
+ $effect(() => {
21
+ if (geometries.errors.length > 0) {
22
+ for (const error of geometries.errors) {
23
+ logs.add(`Geometries: ${error.message}`, 'error')
24
+ }
25
+ }
26
+ })
27
+
28
+ $effect(() => {
29
+ if (pointclouds.errors.length > 0) {
30
+ for (const error of pointclouds.errors) {
31
+ logs.add(`Pointclouds: ${error.message}`, 'error')
32
+ }
33
+ }
34
+ })
7
35
  </script>
8
36
 
9
37
  <Drawer name="Logs">
10
- <div class="flex h-64 w-60 flex-col gap-2 overflow-auto p-3">
11
- {#each truncated as log (log.uuid)}
38
+ {#snippet titleAlert()}
39
+ {#if logs.warnings.length > 0}
40
+ <span class="mr-1 rounded bg-yellow-700 px-1 py-0.5 text-xs text-white">
41
+ {logs.warnings.length}
42
+ </span>
43
+ {/if}
44
+
45
+ {#if logs.errors.length > 0}
46
+ <span class="mr-1 rounded bg-red-700 px-1 py-0.5 text-xs text-white">
47
+ {logs.errors.length}
48
+ </span>
49
+ {/if}
50
+ {/snippet}
51
+
52
+ <div class="flex h-64 flex-col gap-2 overflow-auto p-3">
53
+ {#each logs.current as log (log.uuid)}
12
54
  <div>
13
55
  <div class="flex flex-wrap items-center gap-1.5">
14
56
  <div
@@ -23,7 +65,14 @@
23
65
  ></div>
24
66
  <div class="text-subtle-2">{log.timestamp}</div>
25
67
  </div>
26
- <div>{log.message}</div>
68
+ <div>
69
+ {#if log.count > 1}
70
+ <span class="mr-1 rounded bg-green-700 px-1 py-0.5 text-xs text-white">
71
+ {log.count}
72
+ </span>
73
+ {/if}
74
+ {log.message}
75
+ </div>
27
76
  </div>
28
77
  {:else}
29
78
  No logs
@@ -13,10 +13,12 @@
13
13
  import Widgets from './Widgets.svelte'
14
14
  import AddFrames from './AddFrames.svelte'
15
15
  import { useEnvironment } from '../../hooks/useEnvironment.svelte'
16
+ import { usePartID } from '../../hooks/usePartID.svelte'
16
17
  const { ...rest } = $props()
17
18
 
18
19
  provideTreeExpandedContext()
19
20
 
21
+ const partID = usePartID()
20
22
  const selected = useSelected()
21
23
  const objects = useObjects()
22
24
  const draggable = useDraggable('treeview')
@@ -40,7 +42,7 @@
40
42
  </script>
41
43
 
42
44
  <div
43
- class="bg-extralight border-medium absolute top-0 left-0 z-1000 m-2 overflow-y-auto border text-xs"
45
+ class="bg-extralight border-medium absolute top-0 left-0 z-1000 m-2 w-60 overflow-y-auto border text-xs"
44
46
  style:transform="translate({draggable.current.x}px, {draggable.current.y}px)"
45
47
  {...rest}
46
48
  >
@@ -56,9 +58,10 @@
56
58
  />
57
59
  {/key}
58
60
 
59
- {#if environment.current.isStandalone}
61
+ {#if environment.current.isStandalone && partID.current}
60
62
  <AddFrames />
61
63
  {/if}
64
+
62
65
  <Logs />
63
66
  <Settings />
64
67
  <Widgets />
@@ -9,13 +9,13 @@
9
9
  }
10
10
  let { experiment, children, renderIfActive = true }: Props = $props()
11
11
 
12
- const { weblab } = useWeblabs()
12
+ const weblabs = useWeblabs()
13
13
 
14
14
  $effect.pre(() => {
15
- weblab.load([experiment])
15
+ weblabs.load([experiment])
16
16
  })
17
17
  </script>
18
18
 
19
- {#if weblab.isActive(experiment) === renderIfActive}
19
+ {#if weblabs.isActive(experiment) === renderIfActive}
20
20
  {@render children()}
21
21
  {/if}
@@ -1,13 +1,13 @@
1
1
  import { getContext, setContext, untrack } from 'svelte';
2
2
  import { useRobotClient, createRobotQuery, useMachineStatus, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
3
  import { WorldObject } from '../WorldObject.svelte';
4
- import { observe } from '@threlte/core';
5
4
  import { useLogs } from './useLogs.svelte';
6
5
  import { resourceColors } from '../color';
7
6
  import { usePartConfig } from './usePartConfig.svelte';
8
7
  import { Color } from 'three';
9
8
  import { useEnvironment } from './useEnvironment.svelte';
10
9
  import { createPoseFromFrame } from '../transform';
10
+ import { observe } from '@threlte/core';
11
11
  const key = Symbol('frames-context');
12
12
  export const provideFrames = (partID) => {
13
13
  const resourceNames = useResourceNames(partID);
@@ -22,7 +22,7 @@ export const provideFrames = (partID) => {
22
22
  untrack(() => query.current).refetch();
23
23
  logs.add('Fetching frames...');
24
24
  });
25
- observe.pre(() => [partConfig.isDirty], () => {
25
+ $effect.pre(() => {
26
26
  if (partConfig.isDirty) {
27
27
  environment.current.viewerMode = 'edit';
28
28
  }
@@ -1,6 +1,7 @@
1
1
  import { WorldObject } from '../WorldObject.svelte';
2
2
  interface Context {
3
3
  current: WorldObject[];
4
+ errors: Error[];
4
5
  }
5
6
  export declare const provideGeometries: (partID: () => string) => void;
6
7
  export declare const useGeometries: () => Context;
@@ -1,4 +1,4 @@
1
- import { ArmClient, CameraClient, Geometry, GripperClient } from '@viamrobotics/sdk';
1
+ import { ArmClient, CameraClient, GantryClient, Geometry, GripperClient } from '@viamrobotics/sdk';
2
2
  import { createQueries, queryOptions } from '@tanstack/svelte-query';
3
3
  import { createResourceClient, useResourceNames } from '@viamrobotics/svelte-sdk';
4
4
  import { setContext, getContext } from 'svelte';
@@ -9,18 +9,24 @@ import { usePersistentUUIDs } from './usePersistentUUIDs.svelte';
9
9
  import { useLogs } from './useLogs.svelte';
10
10
  import { resourceColors } from '../color';
11
11
  import { Color } from 'three';
12
+ import { useFrames } from './useFrames.svelte';
12
13
  const key = Symbol('geometries-context');
13
14
  export const provideGeometries = (partID) => {
15
+ const frames = useFrames();
14
16
  const resourceNames = useResourceNames(partID);
15
17
  const arms = useResourceNames(partID, 'arm');
16
18
  const cameras = useResourceNames(partID, 'camera');
17
19
  const grippers = useResourceNames(partID, 'gripper');
20
+ const gantries = useResourceNames(partID, 'gantry');
18
21
  const logs = useLogs();
19
22
  const { refreshRates } = useMachineSettings();
20
23
  const armClients = $derived(arms.current.map((arm) => createResourceClient(ArmClient, partID, () => arm.name)));
21
24
  const gripperClients = $derived(grippers.current.map((gripper) => createResourceClient(GripperClient, partID, () => gripper.name)));
22
25
  const cameraClients = $derived(cameras.current.map((camera) => createResourceClient(CameraClient, partID, () => camera.name)));
23
- const clients = $derived([...armClients, ...gripperClients, ...cameraClients]);
26
+ const gantryClients = $derived(gantries.current.map((gantry) => createResourceClient(GantryClient, partID, () => gantry.name)));
27
+ const clients = $derived([...armClients, ...gripperClients, ...cameraClients, ...gantryClients].filter((client) => {
28
+ return frames.current.some((frame) => frame.name === client.current?.name);
29
+ }));
24
30
  const options = $derived.by(() => {
25
31
  const interval = refreshRates.get(RefreshRates.poses);
26
32
  const results = [];
@@ -44,6 +50,7 @@ export const provideGeometries = (partID) => {
44
50
  });
45
51
  const { updateUUIDs } = usePersistentUUIDs();
46
52
  const queries = fromStore(createQueries({ queries: toStore(() => options) }));
53
+ const errors = $derived(queries.current.map((query) => query.error).filter((error) => error !== null));
47
54
  const geometries = $derived.by(() => {
48
55
  const results = [];
49
56
  for (const query of queries.current) {
@@ -66,6 +73,9 @@ export const provideGeometries = (partID) => {
66
73
  get current() {
67
74
  return geometries;
68
75
  },
76
+ get errors() {
77
+ return errors;
78
+ },
69
79
  });
70
80
  };
71
81
  export const useGeometries = () => {
@@ -2,11 +2,14 @@ type Level = 'info' | 'warn' | 'error';
2
2
  interface Log {
3
3
  uuid: string;
4
4
  message: string;
5
+ count: number;
5
6
  level: Level;
6
7
  timestamp: string;
7
8
  }
8
9
  interface Context {
9
10
  current: Log[];
11
+ errors: Log[];
12
+ warnings: Log[];
10
13
  add(message: string, level?: Level): void;
11
14
  }
12
15
  export declare const provideLogs: () => void;
@@ -1,8 +1,10 @@
1
- import { getContext, setContext } from 'svelte';
1
+ import { getContext, setContext, untrack } from 'svelte';
2
2
  import { MathUtils } from 'three';
3
3
  const key = Symbol('logs-context');
4
4
  export const provideLogs = () => {
5
5
  const logs = $state([]);
6
+ const warnings = $state([]);
7
+ const errors = $state([]);
6
8
  const intl = new Intl.DateTimeFormat('en-US', {
7
9
  dateStyle: 'short',
8
10
  timeStyle: 'short',
@@ -11,16 +13,45 @@ export const provideLogs = () => {
11
13
  get current() {
12
14
  return logs;
13
15
  },
16
+ get errors() {
17
+ return errors;
18
+ },
19
+ get warnings() {
20
+ return warnings;
21
+ },
14
22
  add(message, level = 'info') {
15
- logs.push({
16
- message,
17
- level,
18
- uuid: MathUtils.generateUUID(),
19
- timestamp: intl.format(Date.now()),
23
+ untrack(() => {
24
+ const timestamp = intl.format(Date.now());
25
+ const match = logs.find((log) => log.message === message && log.level === level && log.timestamp === timestamp);
26
+ if (match) {
27
+ match.count += 1;
28
+ }
29
+ else {
30
+ const log = {
31
+ timestamp,
32
+ message,
33
+ count: 1,
34
+ level,
35
+ uuid: MathUtils.generateUUID(),
36
+ };
37
+ logs.unshift(log);
38
+ if (level === 'error') {
39
+ errors.unshift(log);
40
+ }
41
+ else if (level === 'warn') {
42
+ warnings.unshift(log);
43
+ }
44
+ }
45
+ if (logs.length > 200) {
46
+ const log = logs.pop();
47
+ if (log && level === 'error') {
48
+ errors.splice(errors.indexOf(log), 1);
49
+ }
50
+ else if (log && level === 'warn') {
51
+ warnings.splice(errors.indexOf(log), 1);
52
+ }
53
+ }
20
54
  });
21
- if (logs.length > 1000) {
22
- logs.shift();
23
- }
24
55
  },
25
56
  });
26
57
  };
@@ -257,32 +257,29 @@ export class StandalonePartConfig {
257
257
  const fragmentRequests = [];
258
258
  if (configJson.fragments) {
259
259
  for (const fragmentId of configJson.fragments) {
260
- fragmentRequests.push(standalonePartConfigProps.viamClient()?.appClient.getFragment(fragmentId));
260
+ //TODO: right now the json could be just a list of strings or an object with an id prop
261
+ const fragId = typeof fragmentId === 'string' ? fragmentId : fragmentId.id;
262
+ fragmentRequests.push(standalonePartConfigProps.viamClient()?.appClient.getFragment(fragId));
261
263
  }
262
- try {
263
- const fragementResponses = await Promise.all(fragmentRequests);
264
- for (const fragmentResponse of fragementResponses) {
265
- const fragmentId = fragmentResponse?.id;
266
- if (!fragmentId) {
267
- continue;
268
- }
269
- const components = fragmentResponse?.fragment?.fields['components'].kind;
270
- if (components?.case === 'listValue') {
271
- for (const component of components.value.values) {
272
- if (component.kind.case === 'structValue') {
273
- const componentName = component.kind.value.fields['name'].kind;
274
- if (componentName.case === 'stringValue') {
275
- componentNameToFragmentId[componentName.value] = fragmentId;
276
- }
264
+ const fragementResponses = await Promise.all(fragmentRequests);
265
+ for (const fragmentResponse of fragementResponses) {
266
+ const fragmentId = fragmentResponse?.id;
267
+ if (!fragmentId) {
268
+ continue;
269
+ }
270
+ const components = fragmentResponse?.fragment?.fields['components'].kind;
271
+ if (components?.case === 'listValue') {
272
+ for (const component of components.value.values) {
273
+ if (component.kind.case === 'structValue') {
274
+ const componentName = component.kind.value.fields['name'].kind;
275
+ if (componentName.case === 'stringValue') {
276
+ componentNameToFragmentId[componentName.value] = fragmentId;
277
277
  }
278
278
  }
279
279
  }
280
280
  }
281
- this._componentNameToFragmentId = componentNameToFragmentId;
282
- }
283
- catch {
284
- /* Do nothing */
285
281
  }
282
+ this._componentNameToFragmentId = componentNameToFragmentId;
286
283
  }
287
284
  };
288
285
  initLocalConfig();
@@ -1,6 +1,7 @@
1
1
  import { WorldObject, type PointsGeometry } from '../WorldObject.svelte';
2
2
  interface Context {
3
3
  current: WorldObject<PointsGeometry>[];
4
+ errors: Error[];
4
5
  }
5
6
  export declare const providePointclouds: (partID: () => string) => void;
6
7
  export declare const usePointClouds: () => Context;
@@ -48,9 +48,11 @@ export const providePointclouds = (partID) => {
48
48
  const data = results
49
49
  .flatMap((result) => result.data)
50
50
  .filter((data) => data !== null && data !== undefined);
51
+ const errors = results.flatMap((result) => result.error).filter((error) => error !== null);
51
52
  updateUUIDs(data);
52
53
  return {
53
54
  data,
55
+ errors,
54
56
  };
55
57
  },
56
58
  }));
@@ -58,6 +60,9 @@ export const providePointclouds = (partID) => {
58
60
  get current() {
59
61
  return queries.current.data;
60
62
  },
63
+ get errors() {
64
+ return queries.current.errors;
65
+ },
61
66
  });
62
67
  };
63
68
  export const usePointClouds = () => {
@@ -1,12 +1,9 @@
1
+ export declare const WEBLABS_CONTEXT_KEY: unique symbol;
1
2
  interface Context {
2
- weblab: Weblab;
3
+ load: (experiments: string[]) => void;
4
+ isActive(experiment: string): boolean;
3
5
  }
6
+ export declare const createWeblabs: () => Context;
4
7
  export declare const provideWeblabs: () => void;
5
8
  export declare const useWeblabs: () => Context;
6
- export declare class Weblab {
7
- private activeExperiments;
8
- constructor();
9
- isActive(experiment: string): boolean;
10
- load(experiments: string[]): void;
11
- }
12
9
  export {};
@@ -1,25 +1,50 @@
1
1
  import { getContext, setContext } from 'svelte';
2
- const key = Symbol('weblabs-context');
3
- export const provideWeblabs = () => {
4
- const weblab = $state(new Weblab());
5
- setContext(key, { weblab });
6
- };
7
- export const useWeblabs = () => {
8
- return getContext(key);
9
- };
10
- export class Weblab {
11
- activeExperiments;
12
- constructor() {
13
- this.activeExperiments = new Set();
2
+ import { SvelteSet } from 'svelte/reactivity';
3
+ export const WEBLABS_CONTEXT_KEY = Symbol('weblabs-context');
4
+ const getCookie = (name) => {
5
+ const value = `; ${document.cookie}`;
6
+ const parts = value.split(`; ${name}=`);
7
+ if (parts.length === 2) {
8
+ return parts.pop()?.split(';').shift() || null;
14
9
  }
15
- isActive(experiment) {
16
- return this.activeExperiments.has(experiment);
10
+ return null;
11
+ };
12
+ const addCookie = (name, value, days, path = '/') => {
13
+ let expires = '';
14
+ if (days !== undefined) {
15
+ const date = new Date();
16
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); // days in milliseconds
17
+ expires = '; expires=' + date.toUTCString();
17
18
  }
18
- load(experiments) {
19
+ document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}${expires}; path=${path}`;
20
+ };
21
+ const getCookieExperiments = () => {
22
+ return getCookie('weblab_experiments')?.split(',') ?? [];
23
+ };
24
+ export const createWeblabs = () => {
25
+ const activeExperiments = new SvelteSet();
26
+ const load = (experiments) => {
27
+ const cookieExperiments = getCookieExperiments();
19
28
  for (const experiment of experiments) {
20
- if (document.cookie.includes(experiment)) {
21
- this.activeExperiments.add(experiment);
29
+ if (cookieExperiments.includes(experiment)) {
30
+ activeExperiments.add(experiment);
22
31
  }
23
32
  }
33
+ };
34
+ return {
35
+ load,
36
+ isActive: (experiment) => {
37
+ return activeExperiments.has(experiment);
38
+ },
39
+ };
40
+ };
41
+ export const provideWeblabs = () => {
42
+ const urlExperiment = new URLSearchParams(window.location.search).get('experiment');
43
+ if (urlExperiment) {
44
+ addCookie('weblab_experiments', [...getCookieExperiments(), urlExperiment].join(','));
24
45
  }
25
- }
46
+ setContext(WEBLABS_CONTEXT_KEY, createWeblabs());
47
+ };
48
+ export const useWeblabs = () => {
49
+ return getContext(WEBLABS_CONTEXT_KEY);
50
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "@typescript-eslint/parser": "8.42.0",
39
39
  "@viamrobotics/prime-core": "0.1.5",
40
40
  "@viamrobotics/sdk": "0.52.0",
41
- "@viamrobotics/svelte-sdk": "0.6.1",
41
+ "@viamrobotics/svelte-sdk": "0.7.1",
42
42
  "@vitejs/plugin-basic-ssl": "2.1.0",
43
43
  "@zag-js/svelte": "1.22.1",
44
44
  "@zag-js/tree-view": "1.22.1",
@@ -1,8 +0,0 @@
1
- <script lang="ts">
2
- import { provideWeblabs } from '../../hooks/useWeblabs.svelte'
3
-
4
- let { children } = $props()
5
- provideWeblabs()
6
- </script>
7
-
8
- {@render children()}
@@ -1,5 +0,0 @@
1
- declare const WeblabProvider: import("svelte").Component<{
2
- children: any;
3
- }, {}, "">;
4
- type WeblabProvider = ReturnType<typeof WeblabProvider>;
5
- export default WeblabProvider;