Skip to content

Commit 7eb8177

Browse files
authored
Merge pull request #35 from viamrobotics/claude/add-camera-source-dropdown
feat: add source dropdown to camera widget (#32)
2 parents cc73ea0 + 0568d73 commit 7eb8177

3 files changed

Lines changed: 95 additions & 10 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { getSourceNames } from '../get-source-names'
4+
5+
describe('getSourceNames', () => {
6+
it('returns unique source names from images', () => {
7+
const images = [{ sourceName: 'color' }, { sourceName: 'depth' }, { sourceName: 'color' }]
8+
expect(getSourceNames(images)).toEqual(['color', 'depth'])
9+
})
10+
11+
it('filters out empty source names', () => {
12+
const images = [{ sourceName: '' }, { sourceName: 'color' }, { sourceName: '' }]
13+
expect(getSourceNames(images)).toEqual(['color'])
14+
})
15+
16+
it('returns an empty array when there are no images', () => {
17+
expect(getSourceNames([])).toEqual([])
18+
})
19+
20+
it('returns an empty array when all source names are empty', () => {
21+
const images = [{ sourceName: '' }, { sourceName: '' }]
22+
expect(getSourceNames(images)).toEqual([])
23+
})
24+
25+
it('preserves the order of first occurrence', () => {
26+
const images = [
27+
{ sourceName: 'depth' },
28+
{ sourceName: 'color' },
29+
{ sourceName: 'depth' },
30+
{ sourceName: 'ir' },
31+
]
32+
expect(getSourceNames(images)).toEqual(['depth', 'color', 'ir'])
33+
})
34+
35+
it('handles a single image', () => {
36+
const images = [{ sourceName: 'color' }]
37+
expect(getSourceNames(images)).toEqual(['color'])
38+
})
39+
})

src/lib/components/widgets/camera/camera.svelte

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { Button, Label, Switch, ToggleButtons } from '@viamrobotics/prime-core'
2+
import { Button, Label, Select, Switch, ToggleButtons } from '@viamrobotics/prime-core'
33
import { CameraClient } from '@viamrobotics/sdk'
44
import { createResourceClient, createResourceQuery } from '@viamrobotics/svelte-sdk'
55
@@ -14,6 +14,7 @@
1414
1515
import PCDWidget from '../pcd/pcd-widget.svelte'
1616
import ExportScreenshot from './export-screenshot.svelte'
17+
import { getSourceNames } from './get-source-names'
1718
import LiveOrPollingVideo from './live-or-polling-video.svelte'
1819
import PictureInPictureButton from './picture-in-picture-button.svelte'
1920
@@ -30,6 +31,8 @@
3031
'camera'
3132
)
3233
let isShowingPointcloud = $state(false)
34+
let selectedSource = $state('')
35+
let sourceNames = $state<string[]>([])
3336
3437
const { addImageToDataset } = useAddImageToDataset()
3538
const setIsShowingPointcloud = (event: CustomEvent<boolean>) => {
@@ -42,10 +45,25 @@
4245
() => resourceName
4346
)
4447
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+
})
4967
5068
const pointcloudQuery = createResourceQuery(client, 'getPointCloud', () => ({
5169
enabled: isShowingPointcloud,
@@ -59,15 +77,25 @@
5977
// A separate, disabled query for exporting a screenshot
6078
// since this button should work regardless of refetch
6179
// 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+
)
6689
6790
// Firefox has native support for picture-in-picture and does not need
6891
// to be requested separately. Don't show a "toggle pip" button in Firefox.
6992
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
7093
94+
const onSourceSelect = (event: Event) => {
95+
const { value } = event.target as HTMLSelectElement
96+
selectedSource = value
97+
}
98+
7199
let mousePostionTooltip = $state<'On' | 'Off'>('Off')
72100
const setMousePostionTooltip = (event: CustomEvent<string>) => {
73101
mousePostionTooltip = event.detail as 'On' | 'Off'
@@ -78,12 +106,26 @@
78106

79107
<ConnectionStatus {partID}>
80108
{#snippet connected()}
81-
<div class="flex gap-2 p-4 pb-3">
109+
<div class="flex gap-4 p-4 pb-3">
82110
<RefetchController
83111
{refetchInterval}
84112
allowLive
85113
queries={[imageQuery, pointcloudQuery]}
86114
/>
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}
87129
</div>
88130
<div class="flex h-full w-full gap-4 p-4">
89131
<div class="grow">
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** Extract unique, non-empty source names from a GetImages response. */
2+
export const getSourceNames = (images: { sourceName: string }[]): string[] => {
3+
return [...new Set(images.map((img) => img.sourceName).filter(Boolean))]
4+
}

0 commit comments

Comments
 (0)