@viamrobotics/test-widgets 0.3.1 → 0.5.0

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.
Files changed (40) hide show
  1. package/dist/client-map.d.ts +1 -1
  2. package/dist/client-map.js +1 -1
  3. package/dist/components/copy-button.svelte +10 -10
  4. package/dist/components/mutation-section.svelte +1 -1
  5. package/dist/components/paste-button.svelte +38 -0
  6. package/dist/components/paste-button.svelte.d.ts +7 -0
  7. package/dist/components/refetch-interval-store.svelte.d.ts +1 -1
  8. package/dist/components/widgets/arm/arm.svelte +7 -3
  9. package/dist/components/widgets/arm/joint-position-limits.d.ts +11 -0
  10. package/dist/components/widgets/arm/joint-position-limits.js +26 -0
  11. package/dist/components/widgets/arm/move-to-joint-positions-widget.svelte +9 -4
  12. package/dist/components/widgets/arm/move-to-joint-positions.svelte +86 -17
  13. package/dist/components/widgets/arm/move-to-joint-positions.svelte.d.ts +2 -0
  14. package/dist/components/widgets/arm/move-to-position.svelte +11 -3
  15. package/dist/components/widgets/camera/camera.svelte +54 -13
  16. package/dist/components/widgets/camera/decode-viam-depth.d.ts +37 -0
  17. package/dist/components/widgets/camera/decode-viam-depth.js +114 -0
  18. package/dist/components/widgets/camera/export-screenshot.svelte +33 -12
  19. package/dist/components/widgets/camera/export-screenshot.svelte.d.ts +1 -0
  20. package/dist/components/widgets/camera/get-xmp-json-from-image.d.ts +3 -0
  21. package/dist/components/widgets/camera/get-xmp-json-from-image.js +134 -0
  22. package/dist/components/widgets/camera/live-or-polling-video.svelte +33 -6
  23. package/dist/components/widgets/camera/live-or-polling-video.svelte.d.ts +1 -0
  24. package/dist/components/widgets/camera/pick-image-for-source.d.ts +7 -0
  25. package/dist/components/widgets/camera/pick-image-for-source.js +13 -0
  26. package/dist/components/widgets/camera/three-sixty-camera-view.svelte +69 -0
  27. package/dist/components/widgets/camera/three-sixty-camera-view.svelte.d.ts +8 -0
  28. package/dist/components/widgets/do-command/do-command.svelte +2 -1
  29. package/dist/components/widgets/gripper/gripper.svelte +5 -0
  30. package/dist/components/widgets/gripper/is-holding-something.svelte +57 -0
  31. package/dist/components/widgets/gripper/is-holding-something.svelte.d.ts +7 -0
  32. package/dist/get-resource-api.d.ts +2 -0
  33. package/dist/get-resource-api.js +1 -0
  34. package/dist/get-resource-key.d.ts +2 -0
  35. package/dist/get-resource-key.js +2 -0
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.js +2 -1
  38. package/dist/resource.d.ts +0 -2
  39. package/dist/resource.js +1 -2
  40. package/package.json +7 -5
@@ -0,0 +1,114 @@
1
+ /** MIME type used by Viam's custom 16-bit depth format. */
2
+ export const VIAM_DEPTH_MIME_TYPE = 'image/vnd.viam.dep';
3
+ const HEADER_BYTES = 24;
4
+ // Hue sweep used by RDK's ToPrettyPicture: near = warm (~30°), far = cool (~230°).
5
+ const HUE_NEAR = 30;
6
+ const HUE_FAR = 230;
7
+ const hueToRgb = (hue) => {
8
+ const sector = hue / 60;
9
+ const x = Math.round((1 - Math.abs((sector % 2) - 1)) * 255);
10
+ if (sector < 1)
11
+ return [255, x, 0];
12
+ if (sector < 2)
13
+ return [x, 255, 0];
14
+ if (sector < 3)
15
+ return [0, 255, x];
16
+ if (sector < 4)
17
+ return [0, x, 255];
18
+ if (sector < 5)
19
+ return [x, 0, 255];
20
+ return [255, 0, x];
21
+ };
22
+ /**
23
+ * Decode the Viam depth format (`image/vnd.viam.dep`) and return a colorized
24
+ * visualization (warm = near, cool = far), matching the hue sweep that RDK's
25
+ * `DepthMap.ToPrettyPicture` produces. Pixels with depth 0 are rendered as
26
+ * opaque black to match RDK. The wire format is:
27
+ *
28
+ * - bytes 0–8: magic `DEPTHMAP` (big-endian)
29
+ * - bytes 8–16: width (big-endian uint64)
30
+ * - bytes 16–24: height (big-endian uint64)
31
+ * - bytes 24–end: depth values as big-endian uint16, row-major
32
+ *
33
+ * The browser cannot decode this MIME type natively, so depth frames must be
34
+ * rendered to a canvas ourselves. References:
35
+ *
36
+ * - Reader: https://github.com/viamrobotics/rdk/blob/main/rimage/depth_map_raw.go (readDepthMapViam)
37
+ * - Format registration: https://github.com/viamrobotics/rdk/blob/main/rimage/image_file.go
38
+ * - Visualization (`DepthMap.ToPrettyPicture`):
39
+ * https://github.com/viamrobotics/rdk/blob/main/rimage/depth_map.go
40
+ * - Python equivalent (`ViamImage.bytes_to_depth_array`):
41
+ * https://github.com/viamrobotics/viam-python-sdk/blob/main/src/viam/media/video.py
42
+ *
43
+ * Returns undefined if the buffer is too short to be a valid depth image.
44
+ */
45
+ export const decodeViamDepth = (bytes) => {
46
+ if (bytes.length < HEADER_BYTES) {
47
+ return undefined;
48
+ }
49
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
50
+ // Verify the 8-byte "DEPTHMAP" magic (0x4445505448_4d4150).
51
+ const magic = view.getBigUint64(0, false);
52
+ if (magic !== 0x44455054484d4150n) {
53
+ return undefined;
54
+ }
55
+ const width = Number(view.getBigUint64(8, false));
56
+ const height = Number(view.getBigUint64(16, false));
57
+ const pixelCount = width * height;
58
+ if (bytes.length < HEADER_BYTES + pixelCount * 2) {
59
+ return undefined;
60
+ }
61
+ let min = Number.POSITIVE_INFINITY;
62
+ let max = 0;
63
+ for (let i = 0; i < pixelCount; i++) {
64
+ const depth = view.getUint16(HEADER_BYTES + i * 2, false);
65
+ if (depth === 0)
66
+ continue;
67
+ if (depth < min)
68
+ min = depth;
69
+ if (depth > max)
70
+ max = depth;
71
+ }
72
+ const span = Number.isFinite(min) ? Math.max(1, max - min) : 1;
73
+ const base = Number.isFinite(min) ? min : 0;
74
+ const hueSpan = HUE_FAR - HUE_NEAR;
75
+ const pixels = new Uint8ClampedArray(pixelCount * 4);
76
+ for (let i = 0; i < pixelCount; i++) {
77
+ const depth = view.getUint16(HEADER_BYTES + i * 2, false);
78
+ const idx = i * 4;
79
+ pixels[idx + 3] = 255;
80
+ if (depth === 0) {
81
+ // Unmeasured pixel — leave RGB at 0 (black) to match RDK.
82
+ continue;
83
+ }
84
+ const ratio = (depth - base) / span;
85
+ const [r, g, b] = hueToRgb(HUE_NEAR + ratio * hueSpan);
86
+ pixels[idx] = r;
87
+ pixels[idx + 1] = g;
88
+ pixels[idx + 2] = b;
89
+ }
90
+ return { width, height, pixels };
91
+ };
92
+ /**
93
+ * Decode the Viam depth format and return a PNG `Blob` suitable for display or export,
94
+ * using the same colorization as `decodeViamDepth`.
95
+ *
96
+ * Returns `undefined` if the buffer is too short or a canvas context is unavailable.
97
+ */
98
+ export const getBlobForViamDepth = (bytes) => {
99
+ const decoded = decodeViamDepth(bytes);
100
+ if (!decoded)
101
+ return Promise.resolve(undefined);
102
+ const offscreen = document.createElement('canvas');
103
+ offscreen.width = decoded.width;
104
+ offscreen.height = decoded.height;
105
+ const ctx = offscreen.getContext('2d');
106
+ if (!ctx)
107
+ return Promise.resolve(undefined);
108
+ const imageData = ctx.createImageData(decoded.width, decoded.height);
109
+ imageData.data.set(decoded.pixels);
110
+ ctx.putImageData(imageData, 0, 0);
111
+ return new Promise((resolve) => {
112
+ offscreen.toBlob((blob) => resolve(blob ?? undefined), 'image/png');
113
+ });
114
+ };
@@ -6,12 +6,16 @@
6
6
 
7
7
  import ErrorDisplay from '../../error.svelte'
8
8
 
9
+ import { getBlobForViamDepth, VIAM_DEPTH_MIME_TYPE } from './decode-viam-depth'
10
+ import { pickImageForSource } from './pick-image-for-source'
11
+
9
12
  interface Props {
10
13
  name: string
14
+ sourceName?: string
11
15
  getImage: () => Promise<QueryObserverResult<Awaited<ReturnType<CameraClient['getImages']>>>>
12
16
  }
13
17
 
14
- const { name, getImage }: Props = $props()
18
+ const { name, sourceName = '', getImage }: Props = $props()
15
19
 
16
20
  let lastError = $state<Error>()
17
21
 
@@ -35,7 +39,6 @@
35
39
  }
36
40
 
37
41
  const handleExport = async () => {
38
- const exportFilename = `${name}-${getDateString()}.jpeg`
39
42
  const image = await getImage()
40
43
  if (image.error) {
41
44
  lastError = image.error
@@ -43,18 +46,36 @@
43
46
  }
44
47
 
45
48
  lastError = undefined
46
- if (image.data?.images?.[0]?.image) {
47
- const imageBlob = new Blob([new Uint8Array(image.data.images[0].image)], {
48
- type: image.data.images[0].mimeType || 'image/jpeg',
49
- })
49
+ const matchingImage = pickImageForSource(image.data?.images, sourceName)
50
+ if (!matchingImage?.image) {
51
+ return
52
+ }
50
53
 
51
- const link = document.createElement('a')
52
- const dataUrl = URL.createObjectURL(imageBlob)
53
- link.href = dataUrl
54
- link.download = exportFilename
55
- link.click()
56
- URL.revokeObjectURL(dataUrl)
54
+ const bytes = new Uint8Array(matchingImage.image)
55
+ let blob: Blob
56
+ let ext: string
57
+
58
+ if (matchingImage.mimeType === VIAM_DEPTH_MIME_TYPE) {
59
+ // Decode depth frames to a viewable PNG so the exported file matches the live feed.
60
+ const depthBlob = await getBlobForViamDepth(bytes)
61
+ if (!depthBlob) {
62
+ lastError = new Error('Failed to decode depth image')
63
+ return
64
+ }
65
+ blob = depthBlob
66
+ ext = 'png'
67
+ } else {
68
+ blob = new Blob([bytes], { type: matchingImage.mimeType || 'image/jpeg' })
69
+ ext = 'jpeg'
57
70
  }
71
+
72
+ const exportFilename = `${name}-${getDateString()}.${ext}`
73
+ const link = document.createElement('a')
74
+ const dataUrl = URL.createObjectURL(blob)
75
+ link.href = dataUrl
76
+ link.download = exportFilename
77
+ link.click()
78
+ URL.revokeObjectURL(dataUrl)
58
79
  }
59
80
  </script>
60
81
 
@@ -2,6 +2,7 @@ import type { QueryObserverResult } from '@tanstack/svelte-query';
2
2
  import type { CameraClient } from '@viamrobotics/sdk';
3
3
  interface Props {
4
4
  name: string;
5
+ sourceName?: string;
5
6
  getImage: () => Promise<QueryObserverResult<Awaited<ReturnType<CameraClient['getImages']>>>>;
6
7
  }
7
8
  declare const ExportScreenshot: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,3 @@
1
+ export type XmpJson = Record<string, unknown>;
2
+ /** Extract XMP metadata from image bytes and return it as a plain object. */
3
+ export declare const getXmpJsonFromImageBytes: (image: Uint8Array, mimeType?: string) => XmpJson | null;
@@ -0,0 +1,134 @@
1
+ const JPEG_SOI = 0xffd8;
2
+ const JPEG_MARKER_PREFIX = 0xff;
3
+ const JPEG_APP1_MARKER = 0xe1;
4
+ const JPEG_EOI_MARKER = 0xd9;
5
+ const XMP_IDENTIFIER = 'http://ns.adobe.com/xap/1.0/\0';
6
+ /** Extract XMP metadata from image bytes and return it as a plain object. */
7
+ export const getXmpJsonFromImageBytes = (image, mimeType) => {
8
+ if (mimeType?.includes('png')) {
9
+ return getXmpJsonFromPng(image);
10
+ }
11
+ return getXmpJsonFromJpeg(image);
12
+ };
13
+ const getXmpJsonFromJpeg = (image) => {
14
+ if (image.length < 4 || readUint16(image, 0) !== JPEG_SOI) {
15
+ return null;
16
+ }
17
+ const xmpXml = readXmpXmlFromJpeg(image);
18
+ if (!xmpXml) {
19
+ return null;
20
+ }
21
+ return xmpXmlToJson(xmpXml);
22
+ };
23
+ const readXmpXmlFromJpeg = (image) => {
24
+ const identifier = new TextEncoder().encode(XMP_IDENTIFIER);
25
+ let offset = 2;
26
+ while (offset + 4 < image.length) {
27
+ if (image[offset] !== JPEG_MARKER_PREFIX) {
28
+ break;
29
+ }
30
+ const marker = image[offset + 1];
31
+ if (marker === undefined) {
32
+ break;
33
+ }
34
+ if (marker === JPEG_EOI_MARKER) {
35
+ break;
36
+ }
37
+ const segmentLength = readUint16(image, offset + 2);
38
+ if (segmentLength < 2 || offset + 2 + segmentLength > image.length) {
39
+ break;
40
+ }
41
+ if (marker === JPEG_APP1_MARKER) {
42
+ const segmentData = image.subarray(offset + 4, offset + 2 + segmentLength);
43
+ if (startsWith(segmentData, identifier)) {
44
+ const xmpBytes = segmentData.subarray(identifier.length);
45
+ return new TextDecoder('utf-8').decode(xmpBytes);
46
+ }
47
+ }
48
+ offset += 2 + segmentLength;
49
+ }
50
+ return null;
51
+ };
52
+ const getXmpJsonFromPng = (image) => {
53
+ const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
54
+ if (image.length < signature.length || !startsWith(image, new Uint8Array(signature))) {
55
+ return null;
56
+ }
57
+ let offset = signature.length;
58
+ while (offset + 12 <= image.length) {
59
+ const chunkLength = readUint32(image, offset);
60
+ const chunkType = new TextDecoder('ascii').decode(image.subarray(offset + 4, offset + 8));
61
+ if (chunkType === 'iTXt') {
62
+ const chunkData = image.subarray(offset + 8, offset + 8 + chunkLength);
63
+ const xmpXml = readXmpXmlFromPngITXtChunk(chunkData);
64
+ if (xmpXml) {
65
+ return xmpXmlToJson(xmpXml);
66
+ }
67
+ }
68
+ offset += 12 + chunkLength;
69
+ }
70
+ return null;
71
+ };
72
+ const readXmpXmlFromPngITXtChunk = (chunkData) => {
73
+ let index = 0;
74
+ const readNullTerminated = () => {
75
+ const start = index;
76
+ while (index < chunkData.length && chunkData[index] !== 0) {
77
+ index += 1;
78
+ }
79
+ if (index >= chunkData.length) {
80
+ return null;
81
+ }
82
+ const value = new TextDecoder('utf-8').decode(chunkData.subarray(start, index));
83
+ index += 1;
84
+ return value;
85
+ };
86
+ const keyword = readNullTerminated();
87
+ if (keyword !== 'XML:com.adobe.xmp') {
88
+ return null;
89
+ }
90
+ readNullTerminated(); // compression flag
91
+ readNullTerminated(); // compression method
92
+ readNullTerminated(); // language tag
93
+ readNullTerminated(); // translated keyword
94
+ if (index >= chunkData.length) {
95
+ return null;
96
+ }
97
+ return new TextDecoder('utf-8').decode(chunkData.subarray(index));
98
+ };
99
+ const xmpXmlToJson = (xmpXml) => {
100
+ const doc = new DOMParser().parseFromString(xmpXml, 'application/xml');
101
+ if (doc.querySelector('parsererror')) {
102
+ return null;
103
+ }
104
+ const json = {};
105
+ for (const element of doc.querySelectorAll('*')) {
106
+ for (const attribute of element.attributes) {
107
+ if (attribute.localName === 'about' || attribute.name === 'rdf:about') {
108
+ continue;
109
+ }
110
+ json[attribute.name] = attribute.value;
111
+ }
112
+ if (element.childElementCount === 0 && element.textContent?.trim()) {
113
+ const key = element.prefix ? `${element.prefix}:${element.localName}` : element.localName;
114
+ json[key] = element.textContent.trim();
115
+ }
116
+ }
117
+ return Object.keys(json).length > 0 ? json : null;
118
+ };
119
+ const readUint16 = (bytes, offset) => (bytes[offset] << 8) | bytes[offset + 1];
120
+ const readUint32 = (bytes, offset) => (bytes[offset] << 24) |
121
+ (bytes[offset + 1] << 16) |
122
+ (bytes[offset + 2] << 8) |
123
+ bytes[offset + 3];
124
+ const startsWith = (bytes, prefix) => {
125
+ if (bytes.length < prefix.length) {
126
+ return false;
127
+ }
128
+ for (let index = 0; index < prefix.length; index += 1) {
129
+ if (bytes[index] !== prefix[index]) {
130
+ return false;
131
+ }
132
+ }
133
+ return true;
134
+ };
@@ -15,6 +15,9 @@
15
15
  import { formatNumeric } from '../../../format'
16
16
  import { useMeasureFps } from '../../../fps.svelte'
17
17
 
18
+ import { getBlobForViamDepth, VIAM_DEPTH_MIME_TYPE } from './decode-viam-depth'
19
+ import { pickImageForSource } from './pick-image-for-source'
20
+
18
21
  interface Props {
19
22
  resourceName: string
20
23
  partID: string
@@ -25,6 +28,7 @@
25
28
  isLoading: boolean
26
29
  videoClass?: string
27
30
  showMousePositionTooltip?: boolean
31
+ sourceName?: string
28
32
  refetch: () => Promise<unknown>
29
33
  }
30
34
 
@@ -38,6 +42,7 @@
38
42
  isLoading,
39
43
  videoClass = '',
40
44
  showMousePositionTooltip = false,
45
+ sourceName = '',
41
46
  refetch,
42
47
  }: Props = $props()
43
48
 
@@ -211,16 +216,38 @@
211
216
  })
212
217
 
213
218
  $effect(() => {
214
- if (!data?.images?.[0]?.image) {
219
+ const matchingImage = pickImageForSource(data?.images, sourceName)
220
+ if (!matchingImage?.image) {
215
221
  return
216
222
  }
217
223
 
218
- const imageBlob = new Blob([new Uint8Array(data.images[0].image)], {
219
- type: data.images[0].mimeType || 'image/jpeg',
220
- })
224
+ const bytes = new Uint8Array(matchingImage.image)
225
+ let cancelled = false
226
+ let objectUrl: string | undefined
227
+
228
+ const render = async () => {
229
+ let blob: Blob | undefined
230
+ if (matchingImage.mimeType === VIAM_DEPTH_MIME_TYPE) {
231
+ blob = await getBlobForViamDepth(bytes)
232
+ if (!blob) {
233
+ lastError = new Error('Failed to decode depth frame: truncated or corrupt buffer')
234
+ return
235
+ }
236
+ } else {
237
+ blob = new Blob([bytes], { type: matchingImage.mimeType || 'image/jpeg' })
238
+ }
239
+ if (cancelled) return
240
+ lastError = undefined
241
+ objectUrl = URL.createObjectURL(blob)
242
+ img.src = objectUrl
243
+ }
244
+
245
+ void render()
221
246
 
222
- img.src = URL.createObjectURL(imageBlob)
223
- return () => URL.revokeObjectURL(img.src)
247
+ return () => {
248
+ cancelled = true
249
+ if (objectUrl) URL.revokeObjectURL(objectUrl)
250
+ }
224
251
  })
225
252
 
226
253
  let contentRect = $state.raw<DOMRect>()
@@ -10,6 +10,7 @@ interface Props {
10
10
  isLoading: boolean;
11
11
  videoClass?: string;
12
12
  showMousePositionTooltip?: boolean;
13
+ sourceName?: string;
13
14
  refetch: () => Promise<unknown>;
14
15
  }
15
16
  declare const LiveOrPollingVideo: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pick the image whose `sourceName` matches the given source, falling back to
3
+ * the first image.
4
+ */
5
+ export declare const pickImageForSource: <T extends {
6
+ sourceName: string;
7
+ }>(images: T[] | undefined, sourceName: string) => T | undefined;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Pick the image whose `sourceName` matches the given source, falling back to
3
+ * the first image.
4
+ */
5
+ export const pickImageForSource = (images, sourceName) => {
6
+ if (!images || images.length === 0) {
7
+ return undefined;
8
+ }
9
+ if (!sourceName) {
10
+ return images[0];
11
+ }
12
+ return images.find((img) => img.sourceName === sourceName) ?? images[0];
13
+ };
@@ -0,0 +1,69 @@
1
+ <script lang="ts">
2
+ import type { QueryObserverResult } from '@tanstack/svelte-query'
3
+ import type { OrbitControls as OrbitControlsType } from 'three/examples/jsm/controls/OrbitControls.js'
4
+
5
+ import { T } from '@threlte/core'
6
+ import { OrbitControls, useTexture } from '@threlte/extras'
7
+ import { CameraClient } from '@viamrobotics/sdk'
8
+ import { BackSide } from 'three'
9
+
10
+ const {
11
+ data,
12
+ }: { data: QueryObserverResult<Awaited<ReturnType<CameraClient['getImages']>>>['data'] } =
13
+ $props()
14
+
15
+ let imageUrl = $state.raw('')
16
+ let controlsRef = $state<OrbitControlsType>()
17
+
18
+ $effect(() => {
19
+ const imageRecord = data?.images?.[0]
20
+ const image = imageRecord?.image
21
+ if (!image) {
22
+ imageUrl = ''
23
+ return
24
+ }
25
+
26
+ const imageBytes = new Uint8Array(image)
27
+ const imageBlob = new Blob([imageBytes], {
28
+ type: imageRecord.mimeType || 'image/jpeg',
29
+ })
30
+ const url = URL.createObjectURL(imageBlob)
31
+ imageUrl = url
32
+
33
+ return () => {
34
+ URL.revokeObjectURL(url)
35
+ }
36
+ })
37
+
38
+ const texture = $derived.by(() => (imageUrl ? useTexture(imageUrl) : null))
39
+ </script>
40
+
41
+ <T.PerspectiveCamera
42
+ makeDefault
43
+ position={[0, 0, 0.1]}
44
+ fov={75}
45
+ >
46
+ <OrbitControls
47
+ bind:ref={controlsRef}
48
+ enableZoom={true}
49
+ enablePan={false}
50
+ />
51
+ </T.PerspectiveCamera>
52
+
53
+ {#if $texture}
54
+ {#await texture then map}
55
+ <T.Mesh scale={[-1, 1, 1]}>
56
+ <T.SphereGeometry args={[500, 60, 40]} />
57
+ <T.MeshBasicMaterial
58
+ {map}
59
+ side={BackSide}
60
+ />
61
+ </T.Mesh>
62
+ {/await}
63
+ {/if}
64
+
65
+ <T.AmbientLight intensity={0.5} />
66
+ <T.DirectionalLight
67
+ position={[5, 5, 5]}
68
+ intensity={1}
69
+ />
@@ -0,0 +1,8 @@
1
+ import type { QueryObserverResult } from '@tanstack/svelte-query';
2
+ import { CameraClient } from '@viamrobotics/sdk';
3
+ type $$ComponentProps = {
4
+ data: QueryObserverResult<Awaited<ReturnType<CameraClient['getImages']>>>['data'];
5
+ };
6
+ declare const ThreeSixtyCameraView: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type ThreeSixtyCameraView = ReturnType<typeof ThreeSixtyCameraView>;
8
+ export default ThreeSixtyCameraView;
@@ -6,7 +6,8 @@
6
6
  import { PersistedState } from 'runed'
7
7
 
8
8
  import { supportsDoCommand } from '../../../client-map'
9
- import { getResourceAPI, getResourceKey } from '../../../resource'
9
+ import { getResourceAPI } from '../../../get-resource-api'
10
+ import { getResourceKey } from '../../../get-resource-key'
10
11
 
11
12
  import ErrorDisplay from '../../error.svelte'
12
13
  import { createDoCommandClient } from './create-do-command-client.svelte'
@@ -8,6 +8,7 @@
8
8
  import StopButton from '../../stop-button.svelte'
9
9
 
10
10
  import Grab from './grab.svelte'
11
+ import IsHoldingSomething from './is-holding-something.svelte'
11
12
  import Open from './open.svelte'
12
13
 
13
14
  interface Props {
@@ -63,6 +64,10 @@
63
64
  {partID}
64
65
  {resourceName}
65
66
  />
67
+ <IsHoldingSomething
68
+ {partID}
69
+ {resourceName}
70
+ />
66
71
  </div>
67
72
  </div>
68
73
  {/snippet}
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import { Pill } from '@viamrobotics/prime-core'
3
+ import { GripperClient } from '@viamrobotics/sdk'
4
+ import { createResourceClient, createResourceQuery } from '@viamrobotics/svelte-sdk'
5
+
6
+ import ApiSection from '../../api-section.svelte'
7
+ import Query from '../../query.svelte'
8
+ import StatusPill from '../../status-pill.svelte'
9
+
10
+ import ClosedGripperSvg from './closed-gripper-svg.svelte'
11
+ import OpenGripperSvg from './open-gripper-svg.svelte'
12
+
13
+ interface Props {
14
+ partID: string
15
+ resourceName: string
16
+ }
17
+
18
+ const { partID, resourceName }: Props = $props()
19
+
20
+ const client = createResourceClient(
21
+ GripperClient,
22
+ () => partID,
23
+ () => resourceName
24
+ )
25
+
26
+ const query = createResourceQuery(client, 'isHoldingSomething', {
27
+ refetchInterval: 500,
28
+ })
29
+ </script>
30
+
31
+ <ApiSection
32
+ title="IsHoldingSomething"
33
+ bottomText="Updates automatically"
34
+ class="grow"
35
+ >
36
+ <Query
37
+ {query}
38
+ contentCx="h-5"
39
+ >
40
+ <div class="flex items-center gap-2">
41
+ {#if query.data !== undefined}
42
+ {#if query.data}
43
+ <ClosedGripperSvg />
44
+ {:else}
45
+ <OpenGripperSvg />
46
+ {/if}
47
+ <StatusPill
48
+ isActive={query.data}
49
+ activeText="Holding"
50
+ inactiveText="Empty"
51
+ />
52
+ {:else}
53
+ <Pill value="Loading" />
54
+ {/if}
55
+ </div>
56
+ </Query>
57
+ </ApiSection>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ partID: string;
3
+ resourceName: string;
4
+ }
5
+ declare const IsHoldingSomething: import("svelte").Component<Props, {}, "">;
6
+ type IsHoldingSomething = ReturnType<typeof IsHoldingSomething>;
7
+ export default IsHoldingSomething;
@@ -0,0 +1,2 @@
1
+ import type { ResourceName } from '@viamrobotics/sdk';
2
+ export declare const getResourceAPI: ({ namespace, type, subtype }: ResourceName) => string;
@@ -0,0 +1 @@
1
+ export const getResourceAPI = ({ namespace, type, subtype }) => `${namespace}:${type}:${subtype}`;
@@ -0,0 +1,2 @@
1
+ import type { ResourceName } from '@viamrobotics/sdk';
2
+ export declare const getResourceKey: (name: ResourceName) => string;
@@ -0,0 +1,2 @@
1
+ import { getResourceAPI } from "./get-resource-api.js";
2
+ export const getResourceKey = (name) => `${getResourceAPI(name)}/${name.name}`;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createAddImageToDatasetContext, type ImageData, useAddImageToDataset, } from './add-image-to-dataset';
2
2
  export { clientForResource } from './client-map';
3
3
  export * from './components';
4
+ export { getResourceAPI } from './get-resource-api';
4
5
  export { providePip, usePip } from './pip/context.svelte';
5
- export { getResourceAPI, hasWidget, showResourceWidget, widgetForResource } from './resource';
6
+ export { hasWidget, showResourceWidget, widgetForResource } from './resource';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createAddImageToDatasetContext, useAddImageToDataset, } from './add-image-to-dataset';
2
2
  export { clientForResource } from './client-map';
3
3
  export * from './components';
4
+ export { getResourceAPI } from './get-resource-api';
4
5
  export { providePip, usePip } from './pip/context.svelte';
5
- export { getResourceAPI, hasWidget, showResourceWidget, widgetForResource } from './resource';
6
+ export { hasWidget, showResourceWidget, widgetForResource } from './resource';
@@ -16,8 +16,6 @@ export declare const ResourceStatusText: {
16
16
  1: string;
17
17
  4: string;
18
18
  };
19
- export declare const getResourceAPI: ({ namespace, type, subtype }: ResourceName) => string;
20
- export declare const getResourceKey: (name: ResourceName) => string;
21
19
  export declare const sortResourceNames: (names: ResourceName[]) => ResourceName[];
22
20
  export declare const hasWidget: (resource: ResourceName) => boolean;
23
21
  export declare const widgetForResource: (resource: ResourceName) => Component<ResourceWidget> | undefined;
package/dist/resource.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { robotApi } from '@viamrobotics/sdk';
2
2
  import { clientMap } from "./client-map.js";
3
3
  import { ArmWidget, AudioInputWidget, AudioOutputWidget, BaseWidget, BoardWidget, ButtonWidget, CameraWidget, DiscoveryWidget, EncoderWidget, GantryWidget, GripperWidget, InputControllerWidget, MLModelServiceWidget, MotorWidget, MovementSensorWidget, NavigationServiceWidget, PowerSensorWidget, SensorWidget, ServoWidget, SlamWidget, SwitchWidget, VisionServiceWidget, } from './components';
4
+ import { getResourceAPI } from "./get-resource-api.js";
4
5
  export const ResourceStatusText = {
5
6
  [robotApi.ResourceStatus_State.UNSPECIFIED]: 'unspecified',
6
7
  [robotApi.ResourceStatus_State.READY]: 'ready',
@@ -63,8 +64,6 @@ const resourceMap =
63
64
  'rdk:service:world_state_store': [clientMap['rdk:service:world_state_store'], undefined, true],
64
65
  'rdk:service:video': [clientMap['rdk:service:video'], undefined, true],
65
66
  };
66
- export const getResourceAPI = ({ namespace, type, subtype }) => `${namespace}:${type}:${subtype}`;
67
- export const getResourceKey = (name) => `${getResourceAPI(name)}/${name.name}`;
68
67
  // sorts resource names by local/remote -> type -> name (alphabetical) to produce a list like
69
68
  // component a
70
69
  // component z