@viamrobotics/test-widgets 0.1.8 → 0.1.10

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
 
@@ -30,6 +31,8 @@
30
31
  'camera'
31
32
  )
32
33
  let isShowingPointcloud = $state(false)
34
+ let selectedSource = $state('')
35
+ let sourceNames = $state<string[]>([])
33
36
 
34
37
  const { addImageToDataset } = useAddImageToDataset()
35
38
  const setIsShowingPointcloud = (event: CustomEvent<boolean>) => {
@@ -42,10 +45,25 @@
42
45
  () => resourceName
43
46
  )
44
47
 
45
- const imageQuery = createResourceQuery(client, 'getImages', () => ({
46
- enabled: refetchInterval.current !== RefetchIntervals.LIVE,
47
- refetchInterval: refetchInterval.current,
48
- }))
48
+ const imageQuery = createResourceQuery(
49
+ client,
50
+ 'getImages',
51
+ () => (selectedSource ? ([[selectedSource]] as [string[]]) : ([] as [])),
52
+ () => ({
53
+ enabled: refetchInterval.current !== RefetchIntervals.LIVE,
54
+ refetchInterval: refetchInterval.current,
55
+ })
56
+ )
57
+
58
+ $effect(() => {
59
+ if (sourceNames.length === 0 && imageQuery.data?.images) {
60
+ const names = getSourceNames(imageQuery.data.images)
61
+ if (names.length > 0) {
62
+ sourceNames = names
63
+ selectedSource = names[0]!
64
+ }
65
+ }
66
+ })
49
67
 
50
68
  const pointcloudQuery = createResourceQuery(client, 'getPointCloud', () => ({
51
69
  enabled: isShowingPointcloud,
@@ -59,15 +77,25 @@
59
77
  // A separate, disabled query for exporting a screenshot
60
78
  // since this button should work regardless of refetch
61
79
  // interval and not affect the image feed.
62
- const exportScreenshotQuery = createResourceQuery(client, 'getImages', {
63
- enabled: false,
64
- refetchInterval: false,
65
- })
80
+ const exportScreenshotQuery = createResourceQuery(
81
+ client,
82
+ 'getImages',
83
+ () => (selectedSource ? ([[selectedSource]] as [string[]]) : ([] as [])),
84
+ {
85
+ enabled: false,
86
+ refetchInterval: false,
87
+ }
88
+ )
66
89
 
67
90
  // Firefox has native support for picture-in-picture and does not need
68
91
  // to be requested separately. Don't show a "toggle pip" button in Firefox.
69
92
  const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
70
93
 
94
+ const onSourceSelect = (event: Event) => {
95
+ const { value } = event.target as HTMLSelectElement
96
+ selectedSource = value
97
+ }
98
+
71
99
  let mousePostionTooltip = $state<'On' | 'Off'>('Off')
72
100
  const setMousePostionTooltip = (event: CustomEvent<string>) => {
73
101
  mousePostionTooltip = event.detail as 'On' | 'Off'
@@ -78,12 +106,26 @@
78
106
 
79
107
  <ConnectionStatus {partID}>
80
108
  {#snippet connected()}
81
- <div class="flex gap-2 p-4 pb-3">
109
+ <div class="flex gap-4 p-4 pb-3">
82
110
  <RefetchController
83
111
  {refetchInterval}
84
112
  allowLive
85
113
  queries={[imageQuery, pointcloudQuery]}
86
114
  />
115
+ {#if sourceNames.length > 0 && refetchInterval.current !== RefetchIntervals.LIVE}
116
+ <Label position="left">
117
+ Source
118
+ <Select
119
+ value={selectedSource}
120
+ on:change={onSourceSelect}
121
+ slot="input"
122
+ >
123
+ {#each sourceNames as name (name)}
124
+ <option value={name}>{name}</option>
125
+ {/each}
126
+ </Select>
127
+ </Label>
128
+ {/if}
87
129
  </div>
88
130
  <div class="flex h-full w-full gap-4 p-4">
89
131
  <div class="grow">
@@ -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
+ };
@@ -1,8 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { useThrelte } from '@threlte/core'
3
- import { useInteractivity } from '@threlte/extras'
4
- import { onMount } from 'svelte'
5
- import { Vector2 } from 'three'
6
3
  import { Container, Text } from 'threlte-uikit'
7
4
 
8
5
  import { lightTextColors } from './color'
@@ -17,19 +14,6 @@
17
14
 
18
15
  const { detection, factor = 0 }: Props = $props()
19
16
 
20
- const normalizedDeviceCoordsToPixel = (
21
- ndcCoords: Vector2,
22
- canvasWidth: number,
23
- canvasHeight: number
24
- ) => {
25
- const screenX = (ndcCoords.x + 1) / 2
26
- const screenY = (ndcCoords.y + 1) / 2
27
- const pixelX = Math.round(screenX * canvasWidth)
28
- const pixelY = Math.round((1 - screenY) * canvasHeight)
29
- return new Vector2(pixelX, pixelY)
30
- }
31
-
32
- const { pointer } = useInteractivity()
33
17
  const context = useDetections()
34
18
 
35
19
  const factoredxMin = $derived(Number(detection.xMin) * factor)
@@ -37,46 +21,32 @@
37
21
  const factoredyMin = $derived(Number(detection.yMin) * factor)
38
22
  const factoredyMax = $derived(Number(detection.yMax) * factor)
39
23
 
40
- /**
41
- * Check preexisting mouse coords for hover state
42
- */
43
- $effect.pre(() => {
44
- const pixelCoords = normalizedDeviceCoordsToPixel($pointer, $size.width, $size.height)
45
-
46
- if (
47
- factoredxMin < pixelCoords.x &&
48
- factoredxMax > pixelCoords.x &&
49
- factoredyMin < pixelCoords.y &&
50
- factoredyMax > pixelCoords.y
51
- ) {
52
- context.hovered.add(detection.id)
53
- } else {
54
- context.hovered.delete(detection.id)
55
- }
56
- })
57
-
58
- const hovering = $derived(context.hovered.has(detection.id))
24
+ let hovering = $state(false)
59
25
 
60
26
  // These are ballparking, but seem good enough for nearly all cases
61
27
  const isDetectionNearTop = $derived(factoredyMin < 30)
62
28
  const isDetectionNearRight = $derived(factoredxMin > $size.width * 0.66)
63
-
64
- onMount(() => {
65
- return () => {
66
- context.hovered.delete(detection.id)
67
- }
68
- })
69
29
  </script>
70
30
 
71
31
  <Container
72
32
  positionType="absolute"
73
33
  positionLeft={factoredxMin}
74
34
  positionTop={factoredyMin}
75
- borderColor={hovering ? detection.color : '#aaa'}
35
+ borderColor="#aaa"
36
+ hover={{
37
+ borderColor: detection.color,
38
+ }}
76
39
  borderWidth={2}
77
40
  width={factoredxMax - factoredxMin}
78
41
  height={factoredyMax - factoredyMin}
79
- onpointerleave={() => context.hovered.delete(detection.id)}
42
+ onpointerenter={() => {
43
+ context.hovered.add(detection.id)
44
+ hovering = true
45
+ }}
46
+ onpointerleave={() => {
47
+ context.hovered.delete(detection.id)
48
+ hovering = false
49
+ }}
80
50
  >
81
51
  {#if hovering}
82
52
  <Text
@@ -16,7 +16,7 @@
16
16
  interface Props {
17
17
  data?: {
18
18
  detections: Detection[]
19
- image: ({ image: Uint8Array } & Exclude<cameraApi.Image, 'image'>) | undefined
19
+ image: ({ image: Uint8Array } & Omit<cameraApi.Image, 'image'>) | undefined
20
20
  classifications: Classification[]
21
21
  }
22
22
  detectionsSupported: boolean
@@ -62,7 +62,9 @@
62
62
  provideFontFamilies({
63
63
  publicSans: {
64
64
  // This must remain `/static/*` while the frontend directory still exist
65
+ light: '/static/fonts/public-sans.json',
65
66
  medium: '/static/fonts/public-sans.json',
67
+ 'extra-light': '/static/fonts/public-sans.json',
66
68
  },
67
69
  })
68
70
 
@@ -108,7 +110,7 @@
108
110
  </div>
109
111
  {:else}
110
112
  <div
111
- class="relative max-h-full min-h-0 max-w-full"
113
+ class="border-light relative max-h-full min-h-0 max-w-full border"
112
114
  style:width={`${size.width.toString()}px`}
113
115
  style:height={`${size.height.toString()}px`}
114
116
  >
@@ -117,11 +119,13 @@
117
119
  >
118
120
  {fps.current.toFixed(1)}fps
119
121
  </div>
122
+ <img
123
+ class="absolute top-0 left-0"
124
+ alt=""
125
+ {src}
126
+ />
120
127
  <Canvas>
121
- <Scene
122
- {src}
123
- factor={size.factor}
124
- />
128
+ <Scene factor={size.factor} />
125
129
  </Canvas>
126
130
  </div>
127
131
  <div
@@ -4,7 +4,7 @@ interface Props {
4
4
  detections: Detection[];
5
5
  image: ({
6
6
  image: Uint8Array;
7
- } & Exclude<cameraApi.Image, 'image'>) | undefined;
7
+ } & Omit<cameraApi.Image, 'image'>) | undefined;
8
8
  classifications: Classification[];
9
9
  };
10
10
  detectionsSupported: boolean;
@@ -1,17 +1,16 @@
1
1
  <script lang="ts">
2
2
  import { T } from '@threlte/core'
3
3
  import { interactivity } from '@threlte/extras'
4
- import { Fullscreen, Image } from 'threlte-uikit'
4
+ import { Fullscreen } from 'threlte-uikit'
5
5
 
6
6
  import { useDetections } from './context.svelte'
7
7
  import Detection from './detection.svelte'
8
8
 
9
9
  interface Props {
10
- src: string
11
10
  factor?: number
12
11
  }
13
12
 
14
- const { src, factor = 0 }: Props = $props()
13
+ const { factor = 0 }: Props = $props()
15
14
 
16
15
  const context = useDetections()
17
16
 
@@ -22,24 +21,7 @@
22
21
  makeDefault
23
22
  position={[0, 0, 1]}
24
23
  >
25
- <Fullscreen
26
- positionType="relative"
27
- width="100%"
28
- height="100%"
29
- >
30
- <Image
31
- positionType="absolute"
32
- width="100%"
33
- height="100%"
34
- {src}
35
- />
36
- </Fullscreen>
37
-
38
- <Fullscreen
39
- positionType="relative"
40
- width="100%"
41
- height="100%"
42
- >
24
+ <Fullscreen>
43
25
  {#each context.current as detection (detection.id)}
44
26
  <Detection
45
27
  {detection}
@@ -1,5 +1,4 @@
1
1
  interface Props {
2
- src: string;
3
2
  factor?: number;
4
3
  }
5
4
  declare const Scene: import("svelte").Component<Props, {}, "">;
@@ -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}>
@@ -183,10 +190,19 @@
183
190
  {/if}
184
191
  </div>
185
192
 
193
+ {#if detectionsSlow}
194
+ <p class="text-subtle-2 text-xs italic">This request is taking a long time to complete.</p>
195
+ {/if}
196
+
186
197
  <Queries
187
198
  queries={[propertiesQuery, captureAllQuery]}
188
199
  contentCx="p-4 h-14"
189
200
  >
201
+ {#if detectionsSlow}
202
+ <p class="text-subtle-2 text-xs italic">
203
+ This request is taking a long time to complete.
204
+ </p>
205
+ {/if}
190
206
  {#if captureAllQuery.data}
191
207
  <Image
192
208
  data={captureAllQuery.data}
@@ -213,11 +229,20 @@
213
229
  />
214
230
  </div>
215
231
 
232
+ {#if showObjectPointClouds && objectPointCloudsSlow}
233
+ <p class="text-subtle-2 text-xs italic">This request is taking a long time to complete.</p>
234
+ {/if}
235
+
216
236
  {#if showObjectPointClouds}
217
237
  <Queries
218
238
  queries={[getObjectPointCloudsQuery]}
219
239
  contentCx="p-4 h-14"
220
240
  >
241
+ {#if objectPointCloudsSlow.isSlow}
242
+ <p class="text-subtle-2 text-xs italic">
243
+ This request is taking a long time to complete.
244
+ </p>
245
+ {/if}
221
246
  {#if getObjectPointCloudsQuery.data !== undefined}
222
247
  <ObjectPointClouds objects={getObjectPointCloudsQuery.data} />
223
248
  {/if}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/test-widgets",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": [