@viamrobotics/test-widgets 0.1.9 → 0.1.11

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.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Button, Label, Switch, ToggleButtons } from '@viamrobotics/prime-core'
2
+ import { Button, Label, Select, Switch, ToggleButtons } from '@viamrobotics/prime-core'
3
3
  import { CameraClient } from '@viamrobotics/sdk'
4
4
  import { createResourceClient, createResourceQuery } from '@viamrobotics/svelte-sdk'
5
5
 
@@ -14,6 +14,7 @@
14
14
 
15
15
  import PCDWidget from '../pcd/pcd-widget.svelte'
16
16
  import ExportScreenshot from './export-screenshot.svelte'
17
+ import { getSourceNames } from './get-source-names'
17
18
  import LiveOrPollingVideo from './live-or-polling-video.svelte'
18
19
  import PictureInPictureButton from './picture-in-picture-button.svelte'
19
20
 
@@ -29,7 +30,17 @@
29
30
  () => resourceName,
30
31
  'camera'
31
32
  )
33
+ let isPlaying = $state(false)
34
+ $effect(() => {
35
+ // Reset the play gate whenever the camera identity changes so a swapped
36
+ // resource doesn't auto-start using the previous instance's state.
37
+ void resourceName
38
+ void partID
39
+ isPlaying = false
40
+ })
32
41
  let isShowingPointcloud = $state(false)
42
+ let selectedSource = $state('')
43
+ let sourceNames = $state<string[]>([])
33
44
 
34
45
  const { addImageToDataset } = useAddImageToDataset()
35
46
  const setIsShowingPointcloud = (event: CustomEvent<boolean>) => {
@@ -42,10 +53,25 @@
42
53
  () => resourceName
43
54
  )
44
55
 
45
- const imageQuery = createResourceQuery(client, 'getImages', () => ({
46
- enabled: refetchInterval.current !== RefetchIntervals.LIVE,
47
- refetchInterval: refetchInterval.current,
48
- }))
56
+ const imageQuery = createResourceQuery(
57
+ client,
58
+ 'getImages',
59
+ () => (selectedSource ? ([[selectedSource]] as [string[]]) : ([] as [])),
60
+ () => ({
61
+ enabled: isPlaying && refetchInterval.current !== RefetchIntervals.LIVE,
62
+ refetchInterval: refetchInterval.current,
63
+ })
64
+ )
65
+
66
+ $effect(() => {
67
+ if (sourceNames.length === 0 && imageQuery.data?.images) {
68
+ const names = getSourceNames(imageQuery.data.images)
69
+ if (names.length > 0) {
70
+ sourceNames = names
71
+ selectedSource = names[0]!
72
+ }
73
+ }
74
+ })
49
75
 
50
76
  const pointcloudQuery = createResourceQuery(client, 'getPointCloud', () => ({
51
77
  enabled: isShowingPointcloud,
@@ -59,15 +85,25 @@
59
85
  // A separate, disabled query for exporting a screenshot
60
86
  // since this button should work regardless of refetch
61
87
  // interval and not affect the image feed.
62
- const exportScreenshotQuery = createResourceQuery(client, 'getImages', {
63
- enabled: false,
64
- refetchInterval: false,
65
- })
88
+ const exportScreenshotQuery = createResourceQuery(
89
+ client,
90
+ 'getImages',
91
+ () => (selectedSource ? ([[selectedSource]] as [string[]]) : ([] as [])),
92
+ {
93
+ enabled: false,
94
+ refetchInterval: false,
95
+ }
96
+ )
66
97
 
67
98
  // Firefox has native support for picture-in-picture and does not need
68
99
  // to be requested separately. Don't show a "toggle pip" button in Firefox.
69
100
  const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
70
101
 
102
+ const onSourceSelect = (event: Event) => {
103
+ const { value } = event.target as HTMLSelectElement
104
+ selectedSource = value
105
+ }
106
+
71
107
  let mousePostionTooltip = $state<'On' | 'Off'>('Off')
72
108
  const setMousePostionTooltip = (event: CustomEvent<string>) => {
73
109
  mousePostionTooltip = event.detail as 'On' | 'Off'
@@ -78,81 +114,113 @@
78
114
 
79
115
  <ConnectionStatus {partID}>
80
116
  {#snippet connected()}
81
- <div class="flex gap-2 p-4 pb-3">
82
- <RefetchController
83
- {refetchInterval}
84
- allowLive
85
- queries={[imageQuery, pointcloudQuery]}
86
- />
87
- </div>
88
- <div class="flex h-full w-full gap-4 p-4">
89
- <div class="grow">
90
- <LiveOrPollingVideo
91
- {partID}
92
- {resourceName}
93
- showResolutionOptions
94
- isLive={refetchInterval.current === RefetchIntervals.LIVE}
95
- data={imageQuery.data}
96
- error={imageQuery.error}
97
- isLoading={imageQuery.isLoading}
98
- refetch={imageQuery.refetch}
99
- showMousePositionTooltip={mousePostionTooltip === 'On'}
117
+ {#if isPlaying}
118
+ <div class="flex gap-4 p-4 pb-3">
119
+ <RefetchController
120
+ {refetchInterval}
121
+ allowLive
122
+ queries={[imageQuery, pointcloudQuery]}
100
123
  />
101
- </div>
102
- <div class="flex flex-col items-start gap-2">
103
- <ExportScreenshot
104
- name={client.current?.name ?? ''}
105
- getImage={exportScreenshotQuery.refetch}
106
- />
107
- {#if addImageToDataset}
108
- <Button
109
- icon="layers-triple-outline"
110
- onclick={async () => {
111
- let imgData
112
- if (refetchInterval.current === RefetchIntervals.LIVE) {
113
- const { isSuccess, data } = await imageQuery.refetch()
114
- if (!isSuccess) return
115
- imgData = data
116
- } else {
117
- imgData = imageQuery.data
118
- }
119
-
120
- const image = imgData?.images?.[0]?.image
121
- const mimeType = imgData?.images?.[0]?.mimeType
122
-
123
- if (image) {
124
- addImageToDataset({
125
- binaryData: new Uint8Array(image),
126
- partID,
127
- componentType: 'camera',
128
- componentName: resourceName,
129
- methodName: 'captureAllFromCamera',
130
- mimeType: mimeType ?? 'image/png',
131
- dataRequestTimes: [new Date(), new Date()],
132
- })
133
- }
134
- }}
135
- >
136
- Add current image to dataset
137
- </Button>
124
+ {#if sourceNames.length > 0 && refetchInterval.current !== RefetchIntervals.LIVE}
125
+ <Label position="left">
126
+ Source
127
+ <Select
128
+ value={selectedSource}
129
+ on:change={onSourceSelect}
130
+ slot="input"
131
+ >
132
+ {#each sourceNames as name (name)}
133
+ <option value={name}>{name}</option>
134
+ {/each}
135
+ </Select>
136
+ </Label>
138
137
  {/if}
139
- {#if !isFirefox}
140
- <PictureInPictureButton
138
+ </div>
139
+ {/if}
140
+ <div class="flex h-full w-full gap-4 p-4">
141
+ <div class="grow">
142
+ {#if isPlaying}
143
+ <LiveOrPollingVideo
144
+ {partID}
141
145
  {resourceName}
142
- rate={refetchInterval.current}
146
+ showResolutionOptions
147
+ isLive={refetchInterval.current === RefetchIntervals.LIVE}
148
+ data={imageQuery.data}
149
+ error={imageQuery.error}
150
+ isLoading={imageQuery.isLoading}
151
+ refetch={imageQuery.refetch}
152
+ showMousePositionTooltip={mousePostionTooltip === 'On'}
143
153
  />
154
+ {:else}
155
+ <div class="bg-medium flex h-64 w-80 items-center justify-center">
156
+ <Button
157
+ icon="play-circle-outline"
158
+ variant="dark"
159
+ onclick={() => {
160
+ isPlaying = true
161
+ }}
162
+ >
163
+ Start feed
164
+ </Button>
165
+ </div>
144
166
  {/if}
145
- <Label>
146
- Mouse Position Tooltip
147
-
148
- <ToggleButtons
149
- slot="input"
150
- options={['On', 'Off']}
151
- selected={mousePostionTooltip}
152
- on:input={setMousePostionTooltip}
153
- />
154
- </Label>
155
167
  </div>
168
+ {#if isPlaying}
169
+ <div class="flex flex-col items-start gap-2">
170
+ <ExportScreenshot
171
+ name={client.current?.name ?? ''}
172
+ getImage={exportScreenshotQuery.refetch}
173
+ />
174
+ {#if addImageToDataset}
175
+ <Button
176
+ icon="layers-triple-outline"
177
+ onclick={async () => {
178
+ let imgData
179
+ if (refetchInterval.current === RefetchIntervals.LIVE) {
180
+ const { isSuccess, data } = await imageQuery.refetch()
181
+ if (!isSuccess) return
182
+ imgData = data
183
+ } else {
184
+ imgData = imageQuery.data
185
+ }
186
+
187
+ const image = imgData?.images?.[0]?.image
188
+ const mimeType = imgData?.images?.[0]?.mimeType
189
+
190
+ if (image) {
191
+ addImageToDataset({
192
+ binaryData: new Uint8Array(image),
193
+ partID,
194
+ componentType: 'camera',
195
+ componentName: resourceName,
196
+ methodName: 'captureAllFromCamera',
197
+ mimeType: mimeType ?? 'image/png',
198
+ dataRequestTimes: [new Date(), new Date()],
199
+ })
200
+ }
201
+ }}
202
+ >
203
+ Add current image to dataset
204
+ </Button>
205
+ {/if}
206
+ {#if !isFirefox}
207
+ <PictureInPictureButton
208
+ {resourceName}
209
+ rate={refetchInterval.current}
210
+ />
211
+ {/if}
212
+ <Label>
213
+ Mouse Position Tooltip
214
+
215
+ <ToggleButtons
216
+ slot="input"
217
+ options={['On', 'Off']}
218
+ selected={mousePostionTooltip}
219
+ on:input={setMousePostionTooltip}
220
+ />
221
+ </Label>
222
+ </div>
223
+ {/if}
156
224
  </div>
157
225
 
158
226
  <section
@@ -0,0 +1,4 @@
1
+ /** Extract unique, non-empty source names from a GetImages response. */
2
+ export declare const getSourceNames: (images: {
3
+ sourceName: string;
4
+ }[]) => string[];
@@ -0,0 +1,4 @@
1
+ /** Extract unique, non-empty source names from a GetImages response. */
2
+ export const getSourceNames = (images) => {
3
+ return [...new Set(images.map((img) => img.sourceName).filter(Boolean))];
4
+ };
@@ -0,0 +1,3 @@
1
+ export declare const useSlowRequest: (isFetching: () => boolean) => {
2
+ readonly isSlow: boolean;
3
+ };
@@ -0,0 +1,22 @@
1
+ const SLOW_REQUEST_THRESHOLD_MS = 5000;
2
+ export const useSlowRequest = (isFetching) => {
3
+ let isSlow = $state(false);
4
+ $effect(() => {
5
+ if (!isFetching()) {
6
+ isSlow = false;
7
+ return;
8
+ }
9
+ isSlow = false;
10
+ const timeout = setTimeout(() => {
11
+ isSlow = true;
12
+ }, SLOW_REQUEST_THRESHOLD_MS);
13
+ return () => {
14
+ clearTimeout(timeout);
15
+ };
16
+ });
17
+ return {
18
+ get isSlow() {
19
+ return isSlow;
20
+ },
21
+ };
22
+ };
@@ -15,6 +15,7 @@
15
15
 
16
16
  import Image from './image.svelte'
17
17
  import ObjectPointClouds from './object-point-clouds.svelte'
18
+ import { useSlowRequest } from './use-slow-request.svelte.js'
18
19
 
19
20
  const { addImageToDataset } = useAddImageToDataset()
20
21
 
@@ -106,10 +107,16 @@
106
107
  : (Math.max(getObjectPointCloudsRefetchInterval.current, 1000 / 20) as number | false),
107
108
  }))
108
109
 
110
+ const captureAllSlow = useSlowRequest(() => captureAllQuery.isFetching)
111
+ const propertiesSlow = useSlowRequest(() => propertiesQuery.isFetching)
112
+ const objectPointCloudsSlow = useSlowRequest(() => getObjectPointCloudsQuery.isFetching)
113
+
109
114
  const onCameraSelect = (event: Event) => {
110
115
  const { value } = event.target as HTMLSelectElement
111
116
  cameraName = value
112
117
  }
118
+
119
+ const detectionsSlow = $derived(captureAllSlow.isSlow || propertiesSlow.isSlow)
113
120
  </script>
114
121
 
115
122
  <ConnectionStatus {partID}>
@@ -123,6 +130,12 @@
123
130
  on:change={onCameraSelect}
124
131
  slot="input"
125
132
  >
133
+ <option
134
+ value=""
135
+ selected={cameraName === ''}
136
+ >
137
+ Default camera
138
+ </option>
126
139
  {#each cameras.current as { name } (name)}
127
140
  <option
128
141
  selected={cameraName === name}
@@ -183,10 +196,19 @@
183
196
  {/if}
184
197
  </div>
185
198
 
199
+ {#if detectionsSlow}
200
+ <p class="text-subtle-2 text-xs italic">This request is taking a long time to complete.</p>
201
+ {/if}
202
+
186
203
  <Queries
187
204
  queries={[propertiesQuery, captureAllQuery]}
188
205
  contentCx="p-4 h-14"
189
206
  >
207
+ {#if detectionsSlow}
208
+ <p class="text-subtle-2 text-xs italic">
209
+ This request is taking a long time to complete.
210
+ </p>
211
+ {/if}
190
212
  {#if captureAllQuery.data}
191
213
  <Image
192
214
  data={captureAllQuery.data}
@@ -213,11 +235,20 @@
213
235
  />
214
236
  </div>
215
237
 
238
+ {#if showObjectPointClouds && objectPointCloudsSlow}
239
+ <p class="text-subtle-2 text-xs italic">This request is taking a long time to complete.</p>
240
+ {/if}
241
+
216
242
  {#if showObjectPointClouds}
217
243
  <Queries
218
244
  queries={[getObjectPointCloudsQuery]}
219
245
  contentCx="p-4 h-14"
220
246
  >
247
+ {#if objectPointCloudsSlow.isSlow}
248
+ <p class="text-subtle-2 text-xs italic">
249
+ This request is taking a long time to complete.
250
+ </p>
251
+ {/if}
221
252
  {#if getObjectPointCloudsQuery.data !== undefined}
222
253
  <ObjectPointClouds objects={getObjectPointCloudsQuery.data} />
223
254
  {/if}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/test-widgets",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": [
@@ -41,71 +41,71 @@
41
41
  "threlte-uikit": ">=2"
42
42
  },
43
43
  "devDependencies": {
44
- "@changesets/cli": "^2.30.0",
44
+ "@changesets/cli": "^2.31.0",
45
45
  "@eslint/compat": "^2.0.5",
46
46
  "@eslint/js": "^10.0.1",
47
47
  "@fontsource-variable/public-sans": "^5.2.7",
48
48
  "@fontsource-variable/roboto-mono": "^5.2.8",
49
49
  "@fontsource-variable/space-grotesk": "^5.2.10",
50
50
  "@playwright/test": "^1.59.1",
51
- "@sentry/svelte": "^10.48.0",
51
+ "@sentry/svelte": "^10.50.0",
52
52
  "@sveltejs/adapter-static": "^3.0.10",
53
- "@sveltejs/kit": "^2.57.1",
53
+ "@sveltejs/kit": "^2.58.0",
54
54
  "@sveltejs/package": "^2.5.7",
55
55
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
56
- "@tailwindcss/postcss": "^4.2.2",
57
- "@tailwindcss/vite": "^4.2.2",
58
- "@tanstack/svelte-query": "^6.1.16",
59
- "@tanstack/svelte-query-devtools": "^6.1.16",
56
+ "@tailwindcss/postcss": "^4.2.4",
57
+ "@tailwindcss/vite": "^4.2.4",
58
+ "@tanstack/svelte-query": "^6.1.24",
59
+ "@tanstack/svelte-query-devtools": "^6.1.24",
60
60
  "@testing-library/dom": "^10.4.1",
61
61
  "@testing-library/jest-dom": "^6.9.1",
62
62
  "@testing-library/svelte": "^5.3.1",
63
63
  "@testing-library/user-event": "^14.6.1",
64
64
  "@threlte/core": "^8.5.9",
65
- "@threlte/extras": "^9.14.6",
66
- "@threlte/test": "^2.0.2",
65
+ "@threlte/extras": "^9.15.0",
66
+ "@threlte/test": "^2.1.0",
67
67
  "@types/lodash-es": "^4.17.12",
68
- "@types/node": "^25",
68
+ "@types/node": "^25.6.0",
69
69
  "@types/three": "^0.183.1",
70
- "@viamrobotics/motion-tools": "^1.18.1",
71
- "@viamrobotics/prime-core": "^0.1.16",
70
+ "@viamrobotics/motion-tools": "^1.19.1",
71
+ "@viamrobotics/prime-core": "^0.1.19",
72
72
  "@viamrobotics/prime-editor": "^0.0.2",
73
73
  "@viamrobotics/sdk": "^0.68.2",
74
- "@viamrobotics/svelte-sdk": "^1.1.6",
74
+ "@viamrobotics/svelte-sdk": "^1.2.1",
75
75
  "@viamrobotics/tailwind-config": "^1.0.0",
76
76
  "@viamrobotics/three": "^0.0.9",
77
- "@vitest/browser": "^4.1.4",
78
- "@vitest/browser-playwright": "^4.1.4",
79
- "@vitest/coverage-v8": "^4.1.4",
80
- "eslint": "^10.2.0",
77
+ "@vitest/browser": "^4.1.5",
78
+ "@vitest/browser-playwright": "^4.1.5",
79
+ "@vitest/coverage-v8": "^4.1.5",
80
+ "eslint": "^10.2.1",
81
81
  "eslint-config-prettier": "^10.1.8",
82
- "eslint-plugin-perfectionist": "^5.8.0",
83
- "eslint-plugin-svelte": "^3.17.0",
82
+ "eslint-plugin-perfectionist": "^5.9.0",
83
+ "eslint-plugin-svelte": "^3.17.1",
84
84
  "eslint-plugin-unicorn": "^64.0.0",
85
85
  "globals": "^17.5.0",
86
86
  "lodash-es": "^4.18.1",
87
- "maplibre-gl": "^5.23.0",
87
+ "maplibre-gl": "^5.24.0",
88
88
  "mock-match-media": "^1.0.3",
89
89
  "playwright": "^1.59.1",
90
90
  "prettier": "^3.8.3",
91
91
  "prettier-plugin-svelte": "^3.5.1",
92
- "prettier-plugin-tailwindcss": "^0.7.2",
92
+ "prettier-plugin-tailwindcss": "^0.7.4",
93
93
  "publint": "^0.3.18",
94
94
  "runed": "^0.37.1",
95
- "svelte": "^5.55.4",
95
+ "svelte": "^5.55.5",
96
96
  "svelte-check": "^4.4.6",
97
97
  "svelte-splitpanes": "^8.0.12",
98
98
  "tailwind-merge": "^3.5.0",
99
- "tailwindcss": "^4.2.2",
99
+ "tailwindcss": "^4.2.4",
100
100
  "three": "^0.183.2",
101
101
  "threlte-uikit": "^2.0.0",
102
- "type-fest": "^5.5.0",
103
- "typescript": "^6.0.2",
104
- "typescript-eslint": "^8.58.2",
105
- "vite": "^8.0.8",
102
+ "type-fest": "^5.6.0",
103
+ "typescript": "^6.0.3",
104
+ "typescript-eslint": "^8.59.1",
105
+ "vite": "^8.0.10",
106
106
  "vite-plugin-devtools-json": "^1.0.0",
107
107
  "vite-plugin-glsl": "^1.6.0",
108
- "vitest": "^4.1.4",
108
+ "vitest": "^4.1.5",
109
109
  "vitest-browser-svelte": "^2.1.1"
110
110
  },
111
111
  "engines": {