@viamrobotics/test-widgets 0.5.0 → 0.6.1
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.
- package/dist/components/widgets/camera/camera.svelte +46 -11
- package/dist/components/widgets/camera/get-pano-coverage-from-xmp.d.ts +31 -0
- package/dist/components/widgets/camera/get-pano-coverage-from-xmp.js +56 -0
- package/dist/components/widgets/camera/live-three-sixty-camera-view.svelte +61 -0
- package/dist/components/widgets/camera/live-three-sixty-camera-view.svelte.d.ts +9 -0
- package/dist/components/widgets/camera/pano-sphere-scene.svelte +54 -0
- package/dist/components/widgets/camera/pano-sphere-scene.svelte.d.ts +10 -0
- package/dist/components/widgets/camera/three-sixty-camera-view.svelte +33 -43
- package/dist/components/widgets/camera/three-sixty-camera-view.svelte.d.ts +5 -3
- package/dist/components/widgets/do-command/do-command.svelte +14 -1
- package/dist/components/widgets/do-command/do-command.svelte.d.ts +6 -0
- package/package.json +3 -3
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
import PCDWidget from '../pcd/pcd-widget.svelte'
|
|
17
17
|
import ExportScreenshot from './export-screenshot.svelte'
|
|
18
|
+
import { getPanoCoverageFromXmp } from './get-pano-coverage-from-xmp'
|
|
18
19
|
import { getSourceNames } from './get-source-names'
|
|
19
20
|
import { getXmpJsonFromImageBytes, type XmpJson } from './get-xmp-json-from-image'
|
|
20
21
|
import LiveOrPollingVideo from './live-or-polling-video.svelte'
|
|
22
|
+
import LiveThreeSixtyCameraView from './live-three-sixty-camera-view.svelte'
|
|
21
23
|
import { pickImageForSource } from './pick-image-for-source'
|
|
22
24
|
import PictureInPictureButton from './picture-in-picture-button.svelte'
|
|
23
25
|
import ThreeSixtyCameraView from './three-sixty-camera-view.svelte'
|
|
@@ -58,16 +60,30 @@
|
|
|
58
60
|
() => resourceName
|
|
59
61
|
)
|
|
60
62
|
|
|
63
|
+
const isLive = $derived(refetchInterval.current === RefetchIntervals.LIVE)
|
|
64
|
+
|
|
61
65
|
const imageQuery = createResourceQuery(
|
|
62
66
|
client,
|
|
63
67
|
'getImages',
|
|
64
68
|
() => (selectedSource ? ([[selectedSource]] as [string[]]) : ([] as [])),
|
|
65
69
|
() => ({
|
|
66
|
-
enabled: isPlaying &&
|
|
70
|
+
enabled: isPlaying && !isLive,
|
|
67
71
|
refetchInterval: refetchInterval.current,
|
|
68
72
|
})
|
|
69
73
|
)
|
|
70
74
|
|
|
75
|
+
// The live WebRTC stream carries no XMP, so when live we fetch a single frame
|
|
76
|
+
// to learn the GPano crop geometry needed to render the stream on the right band.
|
|
77
|
+
const liveCoverageProbe = createResourceQuery(
|
|
78
|
+
client,
|
|
79
|
+
'getImages',
|
|
80
|
+
() => (selectedSource ? ([[selectedSource]] as [string[]]) : ([] as [])),
|
|
81
|
+
() => ({
|
|
82
|
+
enabled: isPlaying && isLive,
|
|
83
|
+
refetchInterval: false,
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
|
|
71
87
|
$effect(() => {
|
|
72
88
|
if (sourceNames.length === 0 && imageQuery.data?.images) {
|
|
73
89
|
const names = getSourceNames(imageQuery.data.images)
|
|
@@ -79,7 +95,7 @@
|
|
|
79
95
|
})
|
|
80
96
|
|
|
81
97
|
const xmpJson = $derived.by((): XmpJson | null => {
|
|
82
|
-
const imageRecord = imageQuery.data?.images?.[0]
|
|
98
|
+
const imageRecord = isLive ? liveCoverageProbe.data?.images?.[0] : imageQuery.data?.images?.[0]
|
|
83
99
|
const image = imageRecord?.image
|
|
84
100
|
if (!image) {
|
|
85
101
|
return null
|
|
@@ -88,9 +104,14 @@
|
|
|
88
104
|
return getXmpJsonFromImageBytes(new Uint8Array(image), imageRecord.mimeType)
|
|
89
105
|
})
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
// Covers both the full-sphere `viam:equirectangular` tag and GPano
|
|
108
|
+
// (Google Photo Sphere) cropped-area metadata. Non-null means the image
|
|
109
|
+
// can be rendered on a viewing sphere.
|
|
110
|
+
const panoCoverage = $derived.by(() => getPanoCoverageFromXmp(xmpJson))
|
|
111
|
+
// A GPano frame is a partial band, not a full 360° sphere.
|
|
112
|
+
const panoToggleLabel = $derived(
|
|
113
|
+
panoCoverage?.kind === 'gpano' ? 'Display as panorama' : 'Display as 360°'
|
|
114
|
+
)
|
|
94
115
|
|
|
95
116
|
const pointcloudQuery = createResourceQuery(client, 'getPointCloud', () => ({
|
|
96
117
|
enabled: isShowingPointcloud,
|
|
@@ -154,9 +175,9 @@
|
|
|
154
175
|
</Select>
|
|
155
176
|
</Label>
|
|
156
177
|
{/if}
|
|
157
|
-
{#if
|
|
178
|
+
{#if panoCoverage}
|
|
158
179
|
<Label>
|
|
159
|
-
|
|
180
|
+
{panoToggleLabel}
|
|
160
181
|
<ToggleButtons
|
|
161
182
|
slot="input"
|
|
162
183
|
options={['On', 'Off']}
|
|
@@ -173,15 +194,29 @@
|
|
|
173
194
|
<div class="grow">
|
|
174
195
|
{#if isPlaying}
|
|
175
196
|
{#if displayAs360}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
197
|
+
{#if isLive}
|
|
198
|
+
<!-- renderMode="always" keeps the live VideoTexture advancing each frame -->
|
|
199
|
+
<Canvas renderMode="always">
|
|
200
|
+
<LiveThreeSixtyCameraView
|
|
201
|
+
{partID}
|
|
202
|
+
{resourceName}
|
|
203
|
+
coverage={panoCoverage}
|
|
204
|
+
/>
|
|
205
|
+
</Canvas>
|
|
206
|
+
{:else}
|
|
207
|
+
<Canvas>
|
|
208
|
+
<ThreeSixtyCameraView
|
|
209
|
+
data={imageQuery.data}
|
|
210
|
+
coverage={panoCoverage}
|
|
211
|
+
/>
|
|
212
|
+
</Canvas>
|
|
213
|
+
{/if}
|
|
179
214
|
{:else}
|
|
180
215
|
<LiveOrPollingVideo
|
|
181
216
|
{partID}
|
|
182
217
|
{resourceName}
|
|
183
218
|
showResolutionOptions
|
|
184
|
-
isLive
|
|
219
|
+
{isLive}
|
|
185
220
|
data={imageQuery.data}
|
|
186
221
|
error={imageQuery.error}
|
|
187
222
|
isLoading={imageQuery.isLoading}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { XmpJson } from './get-xmp-json-from-image';
|
|
2
|
+
/**
|
|
3
|
+
* Angular extent of a (partial) sphere, in Three.js `SphereGeometry` terms. The
|
|
4
|
+
* `kind` discriminator records which metadata form produced it so the UI can pick
|
|
5
|
+
* a matching label; the renderer only consumes the angles.
|
|
6
|
+
*/
|
|
7
|
+
export interface PanoCoverage {
|
|
8
|
+
kind: 'equirectangular' | 'gpano';
|
|
9
|
+
/** Azimuth (longitude) start, radians. */
|
|
10
|
+
phiStart: number;
|
|
11
|
+
/** Azimuth span, radians (full sphere = 2π). */
|
|
12
|
+
phiLength: number;
|
|
13
|
+
/** Polar start from the +Y north pole, radians. */
|
|
14
|
+
thetaStart: number;
|
|
15
|
+
/** Polar span, radians (full sphere = π). */
|
|
16
|
+
thetaLength: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve how an image's pixels map onto a viewing sphere from its XMP metadata.
|
|
20
|
+
*
|
|
21
|
+
* - `viam:equirectangular="true"` → a full 360°×180° sphere.
|
|
22
|
+
* - `GPano:ProjectionType="equirectangular"` → the cropped sub-rectangle described
|
|
23
|
+
* by the GPano `FullPano*`/`CroppedArea*` pixel fields, mapped to a partial sphere
|
|
24
|
+
* (e.g. an equatorial band with empty poles). Malformed/zero fields fall back to
|
|
25
|
+
* a full sphere rather than producing NaN geometry.
|
|
26
|
+
*
|
|
27
|
+
* @param xmp - Parsed XMP attributes, or `null` when the image has none.
|
|
28
|
+
* @returns The sphere coverage, or `null` if the image is not a sphere-displayable
|
|
29
|
+
* equirectangular panorama.
|
|
30
|
+
*/
|
|
31
|
+
export declare const getPanoCoverageFromXmp: (xmp: XmpJson | null) => PanoCoverage | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const TAU = Math.PI * 2;
|
|
2
|
+
const FULL_ANGLES = {
|
|
3
|
+
phiStart: 0,
|
|
4
|
+
phiLength: TAU,
|
|
5
|
+
thetaStart: 0,
|
|
6
|
+
thetaLength: Math.PI,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Resolve how an image's pixels map onto a viewing sphere from its XMP metadata.
|
|
10
|
+
*
|
|
11
|
+
* - `viam:equirectangular="true"` → a full 360°×180° sphere.
|
|
12
|
+
* - `GPano:ProjectionType="equirectangular"` → the cropped sub-rectangle described
|
|
13
|
+
* by the GPano `FullPano*`/`CroppedArea*` pixel fields, mapped to a partial sphere
|
|
14
|
+
* (e.g. an equatorial band with empty poles). Malformed/zero fields fall back to
|
|
15
|
+
* a full sphere rather than producing NaN geometry.
|
|
16
|
+
*
|
|
17
|
+
* @param xmp - Parsed XMP attributes, or `null` when the image has none.
|
|
18
|
+
* @returns The sphere coverage, or `null` if the image is not a sphere-displayable
|
|
19
|
+
* equirectangular panorama.
|
|
20
|
+
*/
|
|
21
|
+
export const getPanoCoverageFromXmp = (xmp) => {
|
|
22
|
+
if (!xmp) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (xmp['viam:equirectangular'] === 'true') {
|
|
26
|
+
return { kind: 'equirectangular', ...FULL_ANGLES };
|
|
27
|
+
}
|
|
28
|
+
if (xmp['GPano:ProjectionType'] !== 'equirectangular') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const num = (key) => Number(xmp[key]);
|
|
32
|
+
const fullWidth = num('GPano:FullPanoWidthPixels');
|
|
33
|
+
const fullHeight = num('GPano:FullPanoHeightPixels');
|
|
34
|
+
// Malformed/zero dims → full sphere rather than NaN geometry.
|
|
35
|
+
if (!(fullWidth > 0) || !(fullHeight > 0)) {
|
|
36
|
+
return { kind: 'gpano', ...FULL_ANGLES };
|
|
37
|
+
}
|
|
38
|
+
const croppedWidth = num('GPano:CroppedAreaImageWidthPixels');
|
|
39
|
+
const croppedHeight = num('GPano:CroppedAreaImageHeightPixels');
|
|
40
|
+
// Malformed/zero cropped dims → full sphere rather than zero-area geometry.
|
|
41
|
+
if (!(croppedWidth > 0) || !(croppedHeight > 0)) {
|
|
42
|
+
return { kind: 'gpano', ...FULL_ANGLES };
|
|
43
|
+
}
|
|
44
|
+
const coverage = {
|
|
45
|
+
kind: 'gpano',
|
|
46
|
+
phiStart: TAU * (num('GPano:CroppedAreaLeftPixels') / fullWidth),
|
|
47
|
+
phiLength: TAU * (croppedWidth / fullWidth),
|
|
48
|
+
thetaStart: Math.PI * (num('GPano:CroppedAreaTopPixels') / fullHeight),
|
|
49
|
+
thetaLength: Math.PI * (croppedHeight / fullHeight),
|
|
50
|
+
};
|
|
51
|
+
// Any non-finite cropped field → full sphere fallback.
|
|
52
|
+
const angles = [coverage.phiStart, coverage.phiLength, coverage.thetaStart, coverage.thetaLength];
|
|
53
|
+
return angles.every((element) => Number.isFinite(element))
|
|
54
|
+
? coverage
|
|
55
|
+
: { kind: 'gpano', ...FULL_ANGLES };
|
|
56
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useThrelte } from '@threlte/core'
|
|
3
|
+
import { createStreamClient } from '@viamrobotics/svelte-sdk'
|
|
4
|
+
import { VideoTexture } from 'three'
|
|
5
|
+
|
|
6
|
+
import type { PanoCoverage } from './get-pano-coverage-from-xmp'
|
|
7
|
+
|
|
8
|
+
import PanoSphereScene from './pano-sphere-scene.svelte'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
partID: string
|
|
12
|
+
resourceName: string
|
|
13
|
+
coverage?: PanoCoverage | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { partID, resourceName, coverage = null }: Props = $props()
|
|
17
|
+
|
|
18
|
+
const stream = createStreamClient(
|
|
19
|
+
() => partID,
|
|
20
|
+
() => resourceName
|
|
21
|
+
)
|
|
22
|
+
const { renderer } = useThrelte()
|
|
23
|
+
|
|
24
|
+
// A muted, inline, autoplaying <video> backs the VideoTexture. Mirrors the
|
|
25
|
+
// element setup in live-or-polling-video.svelte; created up front like the
|
|
26
|
+
// other camera widgets create their <img>/<canvas> elements.
|
|
27
|
+
const video = document.createElement('video')
|
|
28
|
+
video.muted = true
|
|
29
|
+
video.autoplay = true
|
|
30
|
+
video.playsInline = true
|
|
31
|
+
|
|
32
|
+
let map = $state.raw<VideoTexture>()
|
|
33
|
+
|
|
34
|
+
$effect(() => {
|
|
35
|
+
const mediaStream = stream.mediaStream
|
|
36
|
+
if (!mediaStream) {
|
|
37
|
+
video.srcObject = null
|
|
38
|
+
map = undefined
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
video.srcObject = mediaStream
|
|
43
|
+
void video.play()
|
|
44
|
+
|
|
45
|
+
const texture = new VideoTexture(video)
|
|
46
|
+
texture.colorSpace = renderer.outputColorSpace
|
|
47
|
+
map = texture
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
texture.dispose()
|
|
51
|
+
video.pause()
|
|
52
|
+
video.srcObject = null
|
|
53
|
+
map = undefined
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<PanoSphereScene
|
|
59
|
+
{coverage}
|
|
60
|
+
{map}
|
|
61
|
+
/>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PanoCoverage } from './get-pano-coverage-from-xmp';
|
|
2
|
+
interface Props {
|
|
3
|
+
partID: string;
|
|
4
|
+
resourceName: string;
|
|
5
|
+
coverage?: PanoCoverage | null;
|
|
6
|
+
}
|
|
7
|
+
declare const LiveThreeSixtyCameraView: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type LiveThreeSixtyCameraView = ReturnType<typeof LiveThreeSixtyCameraView>;
|
|
9
|
+
export default LiveThreeSixtyCameraView;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { T } from '@threlte/core'
|
|
3
|
+
import { OrbitControls } from '@threlte/extras'
|
|
4
|
+
import { BackSide, type Texture } from 'three'
|
|
5
|
+
|
|
6
|
+
import type { PanoCoverage } from './get-pano-coverage-from-xmp'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
coverage?: PanoCoverage | null
|
|
10
|
+
/** Texture to paint on the inside of the sphere (image frame or live video). */
|
|
11
|
+
map?: Texture
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { coverage = null, map }: Props = $props()
|
|
15
|
+
|
|
16
|
+
// Default to a full sphere so a missing coverage renders the original 360° view.
|
|
17
|
+
// `kind` is irrelevant to geometry; only the angles are consumed below.
|
|
18
|
+
const sphere = $derived(
|
|
19
|
+
coverage ?? { phiStart: 0, phiLength: Math.PI * 2, thetaStart: 0, thetaLength: Math.PI }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// A band that covers less than the full pole-to-pole sweep is a horizontal-only
|
|
23
|
+
// view: fitting the camera's vertical FOV to the band removes the empty sphere
|
|
24
|
+
// above and below it, and locking the vertical tilt keeps the user from swinging
|
|
25
|
+
// toward those empty caps. A full sphere keeps the original free 75° view.
|
|
26
|
+
const isPartialBand = $derived(sphere.thetaLength < Math.PI - 1e-3)
|
|
27
|
+
const verticalCenter = $derived(sphere.thetaStart + sphere.thetaLength / 2)
|
|
28
|
+
const fov = $derived(isPartialBand ? (sphere.thetaLength * 180) / Math.PI : 75)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<T.PerspectiveCamera
|
|
32
|
+
makeDefault
|
|
33
|
+
position={[0, 0, 0.1]}
|
|
34
|
+
{fov}
|
|
35
|
+
>
|
|
36
|
+
<OrbitControls
|
|
37
|
+
enableZoom={true}
|
|
38
|
+
enablePan={false}
|
|
39
|
+
minPolarAngle={isPartialBand ? verticalCenter : 0}
|
|
40
|
+
maxPolarAngle={isPartialBand ? verticalCenter : Math.PI}
|
|
41
|
+
/>
|
|
42
|
+
</T.PerspectiveCamera>
|
|
43
|
+
|
|
44
|
+
{#if map}
|
|
45
|
+
<T.Mesh scale={[-1, 1, 1]}>
|
|
46
|
+
<T.SphereGeometry
|
|
47
|
+
args={[500, 60, 40, sphere.phiStart, sphere.phiLength, sphere.thetaStart, sphere.thetaLength]}
|
|
48
|
+
/>
|
|
49
|
+
<T.MeshBasicMaterial
|
|
50
|
+
{map}
|
|
51
|
+
side={BackSide}
|
|
52
|
+
/>
|
|
53
|
+
</T.Mesh>
|
|
54
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Texture } from 'three';
|
|
2
|
+
import type { PanoCoverage } from './get-pano-coverage-from-xmp';
|
|
3
|
+
interface Props {
|
|
4
|
+
coverage?: PanoCoverage | null;
|
|
5
|
+
/** Texture to paint on the inside of the sphere (image frame or live video). */
|
|
6
|
+
map?: Texture;
|
|
7
|
+
}
|
|
8
|
+
declare const PanoSphereScene: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type PanoSphereScene = ReturnType<typeof PanoSphereScene>;
|
|
10
|
+
export default PanoSphereScene;
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { QueryObserverResult } from '@tanstack/svelte-query'
|
|
3
|
-
import type {
|
|
3
|
+
import type { Texture } from 'three'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { OrbitControls, useTexture } from '@threlte/extras'
|
|
5
|
+
import { useTexture } from '@threlte/extras'
|
|
7
6
|
import { CameraClient } from '@viamrobotics/sdk'
|
|
8
|
-
import { BackSide } from 'three'
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
import type { PanoCoverage } from './get-pano-coverage-from-xmp'
|
|
9
|
+
|
|
10
|
+
import PanoSphereScene from './pano-sphere-scene.svelte'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
data: QueryObserverResult<Awaited<ReturnType<CameraClient['getImages']>>>['data']
|
|
14
|
+
coverage?: PanoCoverage | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { data, coverage = null }: Props = $props()
|
|
14
18
|
|
|
15
19
|
let imageUrl = $state.raw('')
|
|
16
|
-
let controlsRef = $state<OrbitControlsType>()
|
|
17
20
|
|
|
18
21
|
$effect(() => {
|
|
19
22
|
const imageRecord = data?.images?.[0]
|
|
@@ -23,47 +26,34 @@
|
|
|
23
26
|
return
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
const
|
|
27
|
-
const imageBlob = new Blob([imageBytes], {
|
|
29
|
+
const imageBlob = new Blob([new Uint8Array(image)], {
|
|
28
30
|
type: imageRecord.mimeType || 'image/jpeg',
|
|
29
31
|
})
|
|
30
32
|
const url = URL.createObjectURL(imageBlob)
|
|
31
33
|
imageUrl = url
|
|
32
34
|
|
|
33
|
-
return () =>
|
|
34
|
-
URL.revokeObjectURL(url)
|
|
35
|
-
}
|
|
35
|
+
return () => URL.revokeObjectURL(url)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
enablePan={false}
|
|
50
|
-
/>
|
|
51
|
-
</T.PerspectiveCamera>
|
|
38
|
+
// `useTexture` returns a store that resolves to the decoded texture (with the
|
|
39
|
+
// renderer's color space already applied). Mirror its value into reactive state so
|
|
40
|
+
// the sphere scene can render the camera even before the first frame loads, and so
|
|
41
|
+
// `map` clears when the source goes away.
|
|
42
|
+
const textureStore = $derived(imageUrl ? useTexture(imageUrl) : undefined)
|
|
43
|
+
let map = $state.raw<Texture>()
|
|
44
|
+
$effect(() => {
|
|
45
|
+
if (!textureStore) {
|
|
46
|
+
map = undefined
|
|
47
|
+
return
|
|
48
|
+
}
|
|
52
49
|
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{map}
|
|
59
|
-
side={BackSide}
|
|
60
|
-
/>
|
|
61
|
-
</T.Mesh>
|
|
62
|
-
{/await}
|
|
63
|
-
{/if}
|
|
50
|
+
return textureStore.subscribe((value) => {
|
|
51
|
+
map = value ?? undefined
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
</script>
|
|
64
55
|
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
intensity={1}
|
|
56
|
+
<PanoSphereScene
|
|
57
|
+
{coverage}
|
|
58
|
+
{map}
|
|
69
59
|
/>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { QueryObserverResult } from '@tanstack/svelte-query';
|
|
2
2
|
import { CameraClient } from '@viamrobotics/sdk';
|
|
3
|
-
type
|
|
3
|
+
import type { PanoCoverage } from './get-pano-coverage-from-xmp';
|
|
4
|
+
interface Props {
|
|
4
5
|
data: QueryObserverResult<Awaited<ReturnType<CameraClient['getImages']>>>['data'];
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
coverage?: PanoCoverage | null;
|
|
7
|
+
}
|
|
8
|
+
declare const ThreeSixtyCameraView: import("svelte").Component<Props, {}, "">;
|
|
7
9
|
type ThreeSixtyCameraView = ReturnType<typeof ThreeSixtyCameraView>;
|
|
8
10
|
export default ThreeSixtyCameraView;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
2
4
|
import { Button, Progress } from '@viamrobotics/prime-core'
|
|
3
5
|
import { CodeEditor } from '@viamrobotics/prime-core/code-editor'
|
|
4
6
|
import { type ResourceName, Struct } from '@viamrobotics/sdk'
|
|
@@ -15,9 +17,11 @@
|
|
|
15
17
|
interface Props {
|
|
16
18
|
partID: string
|
|
17
19
|
resource: ResourceName
|
|
20
|
+
/** Rendered above the input/output editor row. */
|
|
21
|
+
header?: Snippet<[{ input: string; setInput: (value: string) => void }]>
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
const { partID, resource }: Props = $props()
|
|
24
|
+
const { partID, resource, header }: Props = $props()
|
|
21
25
|
|
|
22
26
|
const client = createDoCommandClient(
|
|
23
27
|
() => resource,
|
|
@@ -37,6 +41,10 @@
|
|
|
37
41
|
|
|
38
42
|
let output = $state('')
|
|
39
43
|
|
|
44
|
+
const setInput = (value: string) => {
|
|
45
|
+
input.current = value
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
const execute = async () => {
|
|
41
49
|
try {
|
|
42
50
|
lastErr = null
|
|
@@ -52,6 +60,11 @@
|
|
|
52
60
|
</script>
|
|
53
61
|
|
|
54
62
|
{#if isSupported}
|
|
63
|
+
{#if header}
|
|
64
|
+
<div class="border-b">
|
|
65
|
+
{@render header({ input: input.current ?? '{}', setInput })}
|
|
66
|
+
</div>
|
|
67
|
+
{/if}
|
|
55
68
|
<div class="flex flex-row items-center justify-between">
|
|
56
69
|
<div class="flex w-[45%] flex-col gap-2 border-r py-2">
|
|
57
70
|
<span class="text-gray-9 px-4 text-sm font-medium">Input</span>
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import { type ResourceName } from '@viamrobotics/sdk';
|
|
2
3
|
interface Props {
|
|
3
4
|
partID: string;
|
|
4
5
|
resource: ResourceName;
|
|
6
|
+
/** Rendered above the input/output editor row. */
|
|
7
|
+
header?: Snippet<[{
|
|
8
|
+
input: string;
|
|
9
|
+
setInput: (value: string) => void;
|
|
10
|
+
}]>;
|
|
5
11
|
}
|
|
6
12
|
declare const DoCommand: import("svelte").Component<Props, {}, "">;
|
|
7
13
|
type DoCommand = ReturnType<typeof DoCommand>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viamrobotics/test-widgets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@playwright/test": "^1.59.1",
|
|
50
50
|
"@sentry/svelte": "^10.50.0",
|
|
51
51
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
52
|
-
"@sveltejs/kit": "^2.
|
|
52
|
+
"@sveltejs/kit": "^2.60.1",
|
|
53
53
|
"@sveltejs/package": "^2.5.7",
|
|
54
54
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
55
55
|
"@tailwindcss/postcss": "^4.2.4",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@viamrobotics/svelte-sdk": "^1.2.1",
|
|
73
73
|
"@viamrobotics/tailwind-config": "^1.0.0",
|
|
74
74
|
"@viamrobotics/three": "^0.0.9",
|
|
75
|
-
"@vitest/browser": "^4.1.
|
|
75
|
+
"@vitest/browser": "^4.1.6",
|
|
76
76
|
"@vitest/browser-playwright": "^4.1.5",
|
|
77
77
|
"@vitest/coverage-v8": "^4.1.5",
|
|
78
78
|
"eslint": "^10.2.1",
|