@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.
- package/dist/client-map.d.ts +1 -1
- package/dist/client-map.js +1 -1
- package/dist/components/copy-button.svelte +10 -10
- package/dist/components/mutation-section.svelte +1 -1
- package/dist/components/paste-button.svelte +38 -0
- package/dist/components/paste-button.svelte.d.ts +7 -0
- package/dist/components/refetch-interval-store.svelte.d.ts +1 -1
- package/dist/components/widgets/arm/arm.svelte +7 -3
- package/dist/components/widgets/arm/joint-position-limits.d.ts +11 -0
- package/dist/components/widgets/arm/joint-position-limits.js +26 -0
- package/dist/components/widgets/arm/move-to-joint-positions-widget.svelte +9 -4
- package/dist/components/widgets/arm/move-to-joint-positions.svelte +86 -17
- package/dist/components/widgets/arm/move-to-joint-positions.svelte.d.ts +2 -0
- package/dist/components/widgets/arm/move-to-position.svelte +11 -3
- package/dist/components/widgets/camera/camera.svelte +54 -13
- package/dist/components/widgets/camera/decode-viam-depth.d.ts +37 -0
- package/dist/components/widgets/camera/decode-viam-depth.js +114 -0
- package/dist/components/widgets/camera/export-screenshot.svelte +33 -12
- package/dist/components/widgets/camera/export-screenshot.svelte.d.ts +1 -0
- package/dist/components/widgets/camera/get-xmp-json-from-image.d.ts +3 -0
- package/dist/components/widgets/camera/get-xmp-json-from-image.js +134 -0
- package/dist/components/widgets/camera/live-or-polling-video.svelte +33 -6
- package/dist/components/widgets/camera/live-or-polling-video.svelte.d.ts +1 -0
- package/dist/components/widgets/camera/pick-image-for-source.d.ts +7 -0
- package/dist/components/widgets/camera/pick-image-for-source.js +13 -0
- package/dist/components/widgets/camera/three-sixty-camera-view.svelte +69 -0
- package/dist/components/widgets/camera/three-sixty-camera-view.svelte.d.ts +8 -0
- package/dist/components/widgets/do-command/do-command.svelte +2 -1
- package/dist/components/widgets/gripper/gripper.svelte +5 -0
- package/dist/components/widgets/gripper/is-holding-something.svelte +57 -0
- package/dist/components/widgets/gripper/is-holding-something.svelte.d.ts +7 -0
- package/dist/get-resource-api.d.ts +2 -0
- package/dist/get-resource-api.js +1 -0
- package/dist/get-resource-key.d.ts +2 -0
- package/dist/get-resource-key.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/resource.d.ts +0 -2
- package/dist/resource.js +1 -2
- 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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
const matchingImage = pickImageForSource(image.data?.images, sourceName)
|
|
50
|
+
if (!matchingImage?.image) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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,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
|
-
|
|
219
|
+
const matchingImage = pickImageForSource(data?.images, sourceName)
|
|
220
|
+
if (!matchingImage?.image) {
|
|
215
221
|
return
|
|
216
222
|
}
|
|
217
223
|
|
|
218
|
-
const
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
247
|
+
return () => {
|
|
248
|
+
cancelled = true
|
|
249
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
|
250
|
+
}
|
|
224
251
|
})
|
|
225
252
|
|
|
226
253
|
let contentRect = $state.raw<DOMRect>()
|
|
@@ -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
|
|
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 @@
|
|
|
1
|
+
export const getResourceAPI = ({ namespace, type, subtype }) => `${namespace}:${type}:${subtype}`;
|
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 {
|
|
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 {
|
|
6
|
+
export { hasWidget, showResourceWidget, widgetForResource } from './resource';
|
package/dist/resource.d.ts
CHANGED
|
@@ -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
|