@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
package/dist/client-map.d.ts
CHANGED
|
@@ -30,5 +30,5 @@ export declare const clientMap: {
|
|
|
30
30
|
readonly 'rdk:service:world_state_store': typeof WorldStateStoreClient;
|
|
31
31
|
readonly 'rdk:service:video': typeof VideoClient;
|
|
32
32
|
};
|
|
33
|
-
export declare const clientForResource: (resource: ResourceName) => typeof ArmClient | typeof
|
|
33
|
+
export declare const clientForResource: (resource: ResourceName) => typeof ArmClient | typeof AudioInClient | typeof AudioOutClient | typeof BaseClient | typeof BoardClient | typeof ButtonClient | typeof CameraClient | typeof EncoderClient | typeof GantryClient | typeof GenericComponentClient | typeof GripperClient | typeof InputControllerClient | typeof MotorClient | typeof MovementSensorClient | typeof PoseTrackerClient | typeof PowerSensorClient | typeof SensorClient | typeof ServoClient | typeof SwitchClient | typeof DataManagerClient | typeof DiscoveryClient | typeof GenericServiceClient | typeof MLModelClient | typeof MotionClient | typeof NavigationClient | typeof SlamClient | typeof VisionClient | typeof WorldStateStoreClient | typeof VideoClient | undefined;
|
|
34
34
|
export declare const supportsDoCommand: (resource: ResourceName) => boolean;
|
package/dist/client-map.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ArmClient, AudioInClient, AudioOutClient, BaseClient, BoardClient, ButtonClient, CameraClient, DataManagerClient, DiscoveryClient, EncoderClient, GantryClient, GenericComponentClient, GenericServiceClient, GripperClient, InputControllerClient, MLModelClient, MotionClient, MotorClient, MovementSensorClient, NavigationClient, PoseTrackerClient, PowerSensorClient, SensorClient, ServoClient, SlamClient, SwitchClient, VideoClient, VisionClient, WorldStateStoreClient, } from '@viamrobotics/sdk';
|
|
2
|
-
import { getResourceAPI } from "./resource.js";
|
|
2
|
+
import { getResourceAPI } from "./get-resource-api.js";
|
|
3
3
|
export const clientMap = {
|
|
4
4
|
'rdk:component:arm': ArmClient,
|
|
5
5
|
'rdk:component:audio_in': AudioInClient,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { IconButton, Tooltip } from '@viamrobotics/prime-core'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
data: string
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
}
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
size="xs"
|
|
28
|
+
<Tooltip>
|
|
29
|
+
<IconButton
|
|
30
|
+
onclick={handleCopyClick}
|
|
31
|
+
aria-label={ariaLabel}
|
|
32
|
+
class="text-gray-6 hover:border-medium hover:bg-medium active:bg-gray-2 justify-items-end p-0.5"
|
|
33
|
+
icon={showCopySuccess ? 'check' : 'content-copy'}
|
|
34
|
+
label="Copy to clipboard"
|
|
36
35
|
/>
|
|
37
|
-
</
|
|
36
|
+
<span slot="description">{ariaLabel}</span>
|
|
37
|
+
</Tooltip>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { IconButton, Tooltip } from '@viamrobotics/prime-core'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
onPaste: (data: string) => boolean
|
|
6
|
+
ariaLabel?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { onPaste, ariaLabel = 'Paste from clipboard' }: Props = $props()
|
|
10
|
+
|
|
11
|
+
let isPasteSuccessful = $state<boolean | null>(null)
|
|
12
|
+
|
|
13
|
+
const handlePasteClick = async (event: Event) => {
|
|
14
|
+
event.stopPropagation()
|
|
15
|
+
event.preventDefault()
|
|
16
|
+
const data = await globalThis.navigator.clipboard.readText()
|
|
17
|
+
isPasteSuccessful = onPaste(data)
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
isPasteSuccessful = null
|
|
20
|
+
}, 750)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const iconName = $derived(
|
|
24
|
+
// eslint-disable-next-line unicorn/no-nested-ternary
|
|
25
|
+
isPasteSuccessful === null ? 'content-paste' : isPasteSuccessful ? 'check' : 'close'
|
|
26
|
+
)
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<Tooltip>
|
|
30
|
+
<IconButton
|
|
31
|
+
onclick={handlePasteClick}
|
|
32
|
+
aria-label={ariaLabel}
|
|
33
|
+
class="text-gray-6 hover:border-medium hover:bg-medium active:bg-gray-2 justify-items-end p-0.5"
|
|
34
|
+
icon={iconName}
|
|
35
|
+
label="Paste from clipboard"
|
|
36
|
+
/>
|
|
37
|
+
<span slot="description">{ariaLabel}</span>
|
|
38
|
+
</Tooltip>
|
|
@@ -14,5 +14,5 @@ export declare const RefetchIntervals: {
|
|
|
14
14
|
};
|
|
15
15
|
export type RefetchRate = ValueOf<typeof RefetchRates>;
|
|
16
16
|
export type RefetchInterval = ValueOf<typeof RefetchIntervals>;
|
|
17
|
-
export declare const RefetchRateToInterval: ["Live" | "Refresh every second" | "Refresh every 5 seconds" | "Manual refresh", false | 33.3333 |
|
|
17
|
+
export declare const RefetchRateToInterval: ["Live" | "Refresh every second" | "Refresh every 5 seconds" | "Manual refresh", false | 1000 | 33.3333 | 5000][];
|
|
18
18
|
export declare const createRefetchIntervalStore: (partID: () => string, resourceName: () => string, scope: string, defaultInterval?: RefetchInterval) => PersistedState<RefetchInterval>;
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
import ApiSection from '../../api-section.svelte'
|
|
12
12
|
import ConnectionStatus from '../../connection-status.svelte'
|
|
13
13
|
import IsMoving from '../../is-moving.svelte'
|
|
14
|
+
import Queries from '../../queries.svelte'
|
|
14
15
|
import Query from '../../query.svelte'
|
|
15
16
|
import StopButton from '../../stop-button.svelte'
|
|
16
17
|
|
|
17
18
|
import GetJointPositions from './get-joint-positions.svelte'
|
|
19
|
+
import { getJointPositionLimits, type KinematicsJSON } from './joint-position-limits'
|
|
18
20
|
import MoveToJointPositions from './move-to-joint-positions.svelte'
|
|
19
21
|
import MoveToPosition from './move-to-position.svelte'
|
|
20
22
|
import QuickMove from './quick-move.svelte'
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
const options = { refetchInterval: 500 }
|
|
36
38
|
const jointPositionsQuery = createResourceQuery(client, 'getJointPositions', options)
|
|
37
39
|
const endPositionQuery = createResourceQuery(client, 'getEndPosition', options)
|
|
40
|
+
const kinematicsQuery = createResourceQuery(client, 'getKinematics', options)
|
|
38
41
|
|
|
39
42
|
const moveToJointPosMutation = createResourceMutation(client, 'moveToJointPositions')
|
|
40
43
|
const quickMoveToJointPosMutation = createResourceMutation(client, 'moveToJointPositions')
|
|
@@ -72,15 +75,16 @@
|
|
|
72
75
|
</Query>
|
|
73
76
|
</ApiSection>
|
|
74
77
|
<ApiSection title="MoveToJointPositions">
|
|
75
|
-
<
|
|
76
|
-
{#if jointPositionsQuery.data}
|
|
78
|
+
<Queries queries={[jointPositionsQuery, kinematicsQuery]}>
|
|
79
|
+
{#if jointPositionsQuery.data && kinematicsQuery.data}
|
|
77
80
|
<MoveToJointPositions
|
|
78
81
|
positions={jointPositionsQuery.data.values}
|
|
79
82
|
{moveToJointPositions}
|
|
80
83
|
lastError={moveToJointPosMutation.error}
|
|
84
|
+
jointLimitsDegrees={getJointPositionLimits(kinematicsQuery.data as KinematicsJSON)}
|
|
81
85
|
/>
|
|
82
86
|
{/if}
|
|
83
|
-
</
|
|
87
|
+
</Queries>
|
|
84
88
|
</ApiSection>
|
|
85
89
|
<ApiSection title="MoveToPosition">
|
|
86
90
|
<Query query={endPositionQuery}>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type JointLimit = {
|
|
2
|
+
minDegrees: number;
|
|
3
|
+
maxDegrees: number;
|
|
4
|
+
};
|
|
5
|
+
export type OutOfBoundsSide = 'below-min' | 'above-max';
|
|
6
|
+
export interface KinematicsJSON {
|
|
7
|
+
kinematic_param_type: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const isOutsideJointLimit: (value: number, min: number, max: number) => boolean;
|
|
10
|
+
export declare const getOutOfBoundsSide: (value: number, min: number, max: number) => OutOfBoundsSide | null;
|
|
11
|
+
export declare const getJointPositionLimits: (kinematics: KinematicsJSON) => JointLimit[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const isOutsideJointLimit = (value, min, max) => value < min || value > max;
|
|
2
|
+
export const getOutOfBoundsSide = (value, min, max) => {
|
|
3
|
+
if (value < min) {
|
|
4
|
+
return 'below-min';
|
|
5
|
+
}
|
|
6
|
+
if (value > max) {
|
|
7
|
+
return 'above-max';
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
};
|
|
11
|
+
export const getJointPositionLimits = (kinematics) => {
|
|
12
|
+
if (kinematics.kinematic_param_type === 'SVA') {
|
|
13
|
+
return parseSVAKinematics(kinematics);
|
|
14
|
+
}
|
|
15
|
+
return [];
|
|
16
|
+
};
|
|
17
|
+
const parseSVAKinematics = (kinematics) => {
|
|
18
|
+
const joints = kinematics.joints;
|
|
19
|
+
const limits = joints.map((joint) => {
|
|
20
|
+
return {
|
|
21
|
+
minDegrees: joint.min,
|
|
22
|
+
maxDegrees: joint.max,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
return limits;
|
|
26
|
+
};
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
createResourceQuery,
|
|
7
7
|
} from '@viamrobotics/svelte-sdk'
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import Queries from '../../queries.svelte'
|
|
10
10
|
|
|
11
|
+
import { getJointPositionLimits, type KinematicsJSON } from './joint-position-limits'
|
|
11
12
|
import MoveToJointPositions from './move-to-joint-positions.svelte'
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
@@ -26,6 +27,9 @@
|
|
|
26
27
|
const jointPositionsQuery = createResourceQuery(client, 'getJointPositions', {
|
|
27
28
|
refetchInterval: 500,
|
|
28
29
|
})
|
|
30
|
+
const kinematicsQuery = createResourceQuery(client, 'getKinematics', {
|
|
31
|
+
refetchInterval: 500,
|
|
32
|
+
})
|
|
29
33
|
|
|
30
34
|
const moveToJointPosMutation = createResourceMutation(client, 'moveToJointPositions')
|
|
31
35
|
|
|
@@ -34,12 +38,13 @@
|
|
|
34
38
|
}
|
|
35
39
|
</script>
|
|
36
40
|
|
|
37
|
-
<
|
|
38
|
-
{#if jointPositionsQuery.data}
|
|
41
|
+
<Queries queries={[jointPositionsQuery, kinematicsQuery]}>
|
|
42
|
+
{#if jointPositionsQuery.data && kinematicsQuery.data}
|
|
39
43
|
<MoveToJointPositions
|
|
40
44
|
positions={jointPositionsQuery.data.values}
|
|
41
45
|
{moveToJointPositions}
|
|
42
46
|
lastError={moveToJointPosMutation.error}
|
|
47
|
+
jointLimitsDegrees={getJointPositionLimits(kinematicsQuery.data as KinematicsJSON)}
|
|
43
48
|
/>
|
|
44
49
|
{/if}
|
|
45
|
-
</
|
|
50
|
+
</Queries>
|
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
import AngleUnitToggle from '../../angle-unit-toggle.svelte'
|
|
5
5
|
import CopyButton from '../../copy-button.svelte'
|
|
6
6
|
import ErrorDisplay from '../../error.svelte'
|
|
7
|
+
import PasteButton from '../../paste-button.svelte'
|
|
7
8
|
import Table from '../../table.svelte'
|
|
8
9
|
import { numberValueFromEvent } from '../../../event-handlers'
|
|
9
10
|
import { degreesToRadians, formatNumeric, radiansToDegrees } from '../../../format'
|
|
10
11
|
|
|
12
|
+
import { getOutOfBoundsSide, isOutsideJointLimit, type JointLimit } from './joint-position-limits'
|
|
13
|
+
|
|
11
14
|
interface Props {
|
|
12
15
|
positions: number[]
|
|
13
16
|
moveToJointPositions: (jointPositions: number[]) => void
|
|
14
17
|
lastError: Error | null
|
|
18
|
+
jointLimitsDegrees: JointLimit[]
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
const { positions, moveToJointPositions, lastError }: Props = $props()
|
|
21
|
+
const { positions, moveToJointPositions, lastError, jointLimitsDegrees }: Props = $props()
|
|
18
22
|
|
|
19
23
|
// svelte-ignore state_referenced_locally
|
|
20
24
|
let desiredPositions = $state([...positions])
|
|
@@ -28,23 +32,77 @@
|
|
|
28
32
|
desiredPositions = [...positions]
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
const displayPositions = $derived(
|
|
32
|
-
desiredPositions.map((pos) => (useRadians ? degreesToRadians(pos) : pos))
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
const copyData = $derived(`[${displayPositions.join(', ')}]`)
|
|
36
|
-
|
|
37
35
|
const handleJointInputChange = (index: number, inputValue: number) => {
|
|
38
36
|
// default is degrees, so if user has toggle to radians, convert back to degrees before setting
|
|
39
37
|
// (we only convert to radians when displaying)
|
|
40
38
|
desiredPositions[index] = useRadians ? radiansToDegrees(inputValue) : inputValue
|
|
41
39
|
}
|
|
40
|
+
|
|
41
|
+
const handlePaste = (data: string): boolean => {
|
|
42
|
+
try {
|
|
43
|
+
desiredPositions = JSON.parse(data) as number[]
|
|
44
|
+
} catch {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
const degreesToDisplayAngle = (degrees: number) => {
|
|
50
|
+
return useRadians ? degreesToRadians(degrees) : degrees
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const outOfBoundsMessage = (displayValue: number, limit: JointLimit) => {
|
|
54
|
+
const min = degreesToDisplayAngle(limit.minDegrees)
|
|
55
|
+
const max = degreesToDisplayAngle(limit.maxDegrees)
|
|
56
|
+
const unit = useRadians ? 'rad' : 'deg'
|
|
57
|
+
const side = getOutOfBoundsSide(displayValue, min, max)
|
|
58
|
+
|
|
59
|
+
if (side === 'below-min') {
|
|
60
|
+
return `Min value is ${formatNumeric(min)} ${unit}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (side === 'above-max') {
|
|
64
|
+
return `Max value is ${formatNumeric(max)} ${unit}`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const displayPositions = $derived(desiredPositions.map((pos) => degreesToDisplayAngle(pos)))
|
|
69
|
+
|
|
70
|
+
const copyData = $derived(`[${displayPositions.join(', ')}]`)
|
|
71
|
+
|
|
72
|
+
const outOfBoundsByIndex = $derived(
|
|
73
|
+
displayPositions.map((displayValue, index) => {
|
|
74
|
+
const limit = jointLimitsDegrees[index]
|
|
75
|
+
if (!limit) {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
return isOutsideJointLimit(
|
|
79
|
+
displayValue,
|
|
80
|
+
degreesToDisplayAngle(limit.minDegrees),
|
|
81
|
+
degreesToDisplayAngle(limit.maxDegrees)
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const hasOutOfBoundsPositions = $derived(outOfBoundsByIndex.some(Boolean))
|
|
42
87
|
</script>
|
|
43
88
|
|
|
44
89
|
<div class="flex min-w-0 flex-col gap-4">
|
|
45
90
|
<!-- Controls Header -->
|
|
46
91
|
<div class="flex items-center justify-between">
|
|
47
|
-
<span class="text-sm">
|
|
92
|
+
<span class="flex flex-row items-center gap-1 text-sm">
|
|
93
|
+
Joint Positions
|
|
94
|
+
<Tooltip>
|
|
95
|
+
<Icon
|
|
96
|
+
name="information-outline"
|
|
97
|
+
cx="text-gray-6"
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
<span slot="description">
|
|
101
|
+
Joint position limits are based solely on the arm kinematics and do not take into account
|
|
102
|
+
motion service limit overrides.
|
|
103
|
+
</span>
|
|
104
|
+
</Tooltip>
|
|
105
|
+
</span>
|
|
48
106
|
<div class="flex gap-1">
|
|
49
107
|
<AngleUnitToggle
|
|
50
108
|
{useRadians}
|
|
@@ -53,6 +111,7 @@
|
|
|
53
111
|
}}
|
|
54
112
|
/>
|
|
55
113
|
<CopyButton data={copyData} />
|
|
114
|
+
<PasteButton onPaste={handlePaste} />
|
|
56
115
|
</div>
|
|
57
116
|
</div>
|
|
58
117
|
|
|
@@ -65,18 +124,27 @@
|
|
|
65
124
|
</thead>
|
|
66
125
|
<tbody>
|
|
67
126
|
{#each { length: positions.length }, index}
|
|
127
|
+
{@const limit = jointLimitsDegrees[index]}
|
|
68
128
|
{@const value = Number.parseFloat(formatNumeric(displayPositions[index]))}
|
|
129
|
+
{@const outOfBounds = outOfBoundsByIndex[index]}
|
|
69
130
|
<tr>
|
|
70
|
-
<th>
|
|
131
|
+
<th>{index}</th>
|
|
71
132
|
<th>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
133
|
+
<div class="flex flex-col items-center gap-0.5 py-0.5">
|
|
134
|
+
<NumericInput
|
|
135
|
+
cx="max-w-[76px]"
|
|
136
|
+
{value}
|
|
137
|
+
on:change={(event) => {
|
|
138
|
+
const inputValue = numberValueFromEvent(event) ?? 0
|
|
139
|
+
handleJointInputChange(index, inputValue)
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
{#if limit && outOfBounds}
|
|
143
|
+
<p class="text-danger-dark text-center text-[10px] leading-snug whitespace-normal">
|
|
144
|
+
{outOfBoundsMessage(displayPositions[index], limit)}
|
|
145
|
+
</p>
|
|
146
|
+
{/if}
|
|
147
|
+
</div>
|
|
80
148
|
</th>
|
|
81
149
|
</tr>
|
|
82
150
|
{/each}
|
|
@@ -104,6 +172,7 @@
|
|
|
104
172
|
</div>
|
|
105
173
|
<Button
|
|
106
174
|
class="mt-auto w-fit"
|
|
175
|
+
disabled={hasOutOfBoundsPositions}
|
|
107
176
|
icon="play-circle-outline"
|
|
108
177
|
variant="dark"
|
|
109
178
|
onclick={() => moveToJointPositions(desiredPositions)}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { type JointLimit } from './joint-position-limits';
|
|
1
2
|
interface Props {
|
|
2
3
|
positions: number[];
|
|
3
4
|
moveToJointPositions: (jointPositions: number[]) => void;
|
|
4
5
|
lastError: Error | null;
|
|
6
|
+
jointLimitsDegrees: JointLimit[];
|
|
5
7
|
}
|
|
6
8
|
declare const MoveToJointPositions: import("svelte").Component<Props, {}, "">;
|
|
7
9
|
type MoveToJointPositions = ReturnType<typeof MoveToJointPositions>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import AngleUnitToggle from '../../angle-unit-toggle.svelte'
|
|
7
7
|
import CopyButton from '../../copy-button.svelte'
|
|
8
8
|
import ErrorDisplay from '../../error.svelte'
|
|
9
|
+
import PasteButton from '../../paste-button.svelte'
|
|
9
10
|
import Table from '../../table.svelte'
|
|
10
11
|
import { numberValueFromEvent } from '../../../event-handlers'
|
|
11
12
|
import { degreesToRadians, formatNumeric, radiansToDegrees } from '../../../format'
|
|
@@ -48,9 +49,15 @@
|
|
|
48
49
|
theta: useRadians ? degreesToRadians(desiredPosition.theta) : desiredPosition.theta,
|
|
49
50
|
})
|
|
50
51
|
|
|
51
|
-
const copyData = $derived(
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const copyData = $derived(JSON.stringify(displayPosition))
|
|
53
|
+
const handlePaste = (data: string): boolean => {
|
|
54
|
+
try {
|
|
55
|
+
desiredPosition = JSON.parse(data) as Pose
|
|
56
|
+
} catch {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
54
61
|
|
|
55
62
|
const handleAngleInputChange = (key: keyof Pose, inputValue: number) => {
|
|
56
63
|
if (key === 'theta') {
|
|
@@ -84,6 +91,7 @@
|
|
|
84
91
|
}}
|
|
85
92
|
/>
|
|
86
93
|
<CopyButton data={copyData} />
|
|
94
|
+
<PasteButton onPaste={handlePaste} />
|
|
87
95
|
</div>
|
|
88
96
|
</div>
|
|
89
97
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { Canvas } from '@threlte/core'
|
|
2
3
|
import { Button, Label, Select, Switch, ToggleButtons } from '@viamrobotics/prime-core'
|
|
3
4
|
import { CameraClient } from '@viamrobotics/sdk'
|
|
4
5
|
import { createResourceClient, createResourceQuery } from '@viamrobotics/svelte-sdk'
|
|
@@ -15,8 +16,11 @@
|
|
|
15
16
|
import PCDWidget from '../pcd/pcd-widget.svelte'
|
|
16
17
|
import ExportScreenshot from './export-screenshot.svelte'
|
|
17
18
|
import { getSourceNames } from './get-source-names'
|
|
19
|
+
import { getXmpJsonFromImageBytes, type XmpJson } from './get-xmp-json-from-image'
|
|
18
20
|
import LiveOrPollingVideo from './live-or-polling-video.svelte'
|
|
21
|
+
import { pickImageForSource } from './pick-image-for-source'
|
|
19
22
|
import PictureInPictureButton from './picture-in-picture-button.svelte'
|
|
23
|
+
import ThreeSixtyCameraView from './three-sixty-camera-view.svelte'
|
|
20
24
|
|
|
21
25
|
interface Props {
|
|
22
26
|
partID: string
|
|
@@ -41,6 +45,7 @@
|
|
|
41
45
|
let isShowingPointcloud = $state(false)
|
|
42
46
|
let selectedSource = $state('')
|
|
43
47
|
let sourceNames = $state<string[]>([])
|
|
48
|
+
let displayAs360 = $state(false)
|
|
44
49
|
|
|
45
50
|
const { addImageToDataset } = useAddImageToDataset()
|
|
46
51
|
const setIsShowingPointcloud = (event: CustomEvent<boolean>) => {
|
|
@@ -73,6 +78,20 @@
|
|
|
73
78
|
}
|
|
74
79
|
})
|
|
75
80
|
|
|
81
|
+
const xmpJson = $derived.by((): XmpJson | null => {
|
|
82
|
+
const imageRecord = imageQuery.data?.images?.[0]
|
|
83
|
+
const image = imageRecord?.image
|
|
84
|
+
if (!image) {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return getXmpJsonFromImageBytes(new Uint8Array(image), imageRecord.mimeType)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const is360EnabledImage = $derived.by((): boolean => {
|
|
92
|
+
return xmpJson?.['viam:is360'] === 'true'
|
|
93
|
+
})
|
|
94
|
+
|
|
76
95
|
const pointcloudQuery = createResourceQuery(client, 'getPointCloud', () => ({
|
|
77
96
|
enabled: isShowingPointcloud,
|
|
78
97
|
refetchInterval:
|
|
@@ -135,22 +154,42 @@
|
|
|
135
154
|
</Select>
|
|
136
155
|
</Label>
|
|
137
156
|
{/if}
|
|
157
|
+
{#if is360EnabledImage}
|
|
158
|
+
<Label>
|
|
159
|
+
Display as 360°
|
|
160
|
+
<ToggleButtons
|
|
161
|
+
slot="input"
|
|
162
|
+
options={['On', 'Off']}
|
|
163
|
+
selected={displayAs360 ? 'On' : 'Off'}
|
|
164
|
+
on:input={(event) => {
|
|
165
|
+
displayAs360 = event.detail === 'On'
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
</Label>
|
|
169
|
+
{/if}
|
|
138
170
|
</div>
|
|
139
171
|
{/if}
|
|
140
172
|
<div class="flex h-full w-full gap-4 p-4">
|
|
141
173
|
<div class="grow">
|
|
142
174
|
{#if isPlaying}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
{#if displayAs360}
|
|
176
|
+
<Canvas>
|
|
177
|
+
<ThreeSixtyCameraView data={imageQuery.data} />
|
|
178
|
+
</Canvas>
|
|
179
|
+
{:else}
|
|
180
|
+
<LiveOrPollingVideo
|
|
181
|
+
{partID}
|
|
182
|
+
{resourceName}
|
|
183
|
+
showResolutionOptions
|
|
184
|
+
isLive={refetchInterval.current === RefetchIntervals.LIVE}
|
|
185
|
+
data={imageQuery.data}
|
|
186
|
+
error={imageQuery.error}
|
|
187
|
+
isLoading={imageQuery.isLoading}
|
|
188
|
+
refetch={imageQuery.refetch}
|
|
189
|
+
showMousePositionTooltip={mousePostionTooltip === 'On'}
|
|
190
|
+
sourceName={selectedSource}
|
|
191
|
+
/>
|
|
192
|
+
{/if}
|
|
154
193
|
{:else}
|
|
155
194
|
<div class="bg-medium flex h-64 w-80 items-center justify-center">
|
|
156
195
|
<Button
|
|
@@ -169,6 +208,7 @@
|
|
|
169
208
|
<div class="flex flex-col items-start gap-2">
|
|
170
209
|
<ExportScreenshot
|
|
171
210
|
name={client.current?.name ?? ''}
|
|
211
|
+
sourceName={selectedSource}
|
|
172
212
|
getImage={exportScreenshotQuery.refetch}
|
|
173
213
|
/>
|
|
174
214
|
{#if addImageToDataset}
|
|
@@ -184,8 +224,9 @@
|
|
|
184
224
|
imgData = imageQuery.data
|
|
185
225
|
}
|
|
186
226
|
|
|
187
|
-
const
|
|
188
|
-
const
|
|
227
|
+
const matchingImage = pickImageForSource(imgData?.images, selectedSource)
|
|
228
|
+
const image = matchingImage?.image
|
|
229
|
+
const mimeType = matchingImage?.mimeType
|
|
189
230
|
|
|
190
231
|
if (image) {
|
|
191
232
|
addImageToDataset({
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** MIME type used by Viam's custom 16-bit depth format. */
|
|
2
|
+
export declare const VIAM_DEPTH_MIME_TYPE = "image/vnd.viam.dep";
|
|
3
|
+
/**
|
|
4
|
+
* Decode the Viam depth format (`image/vnd.viam.dep`) and return a colorized
|
|
5
|
+
* visualization (warm = near, cool = far), matching the hue sweep that RDK's
|
|
6
|
+
* `DepthMap.ToPrettyPicture` produces. Pixels with depth 0 are rendered as
|
|
7
|
+
* opaque black to match RDK. The wire format is:
|
|
8
|
+
*
|
|
9
|
+
* - bytes 0–8: magic `DEPTHMAP` (big-endian)
|
|
10
|
+
* - bytes 8–16: width (big-endian uint64)
|
|
11
|
+
* - bytes 16–24: height (big-endian uint64)
|
|
12
|
+
* - bytes 24–end: depth values as big-endian uint16, row-major
|
|
13
|
+
*
|
|
14
|
+
* The browser cannot decode this MIME type natively, so depth frames must be
|
|
15
|
+
* rendered to a canvas ourselves. References:
|
|
16
|
+
*
|
|
17
|
+
* - Reader: https://github.com/viamrobotics/rdk/blob/main/rimage/depth_map_raw.go (readDepthMapViam)
|
|
18
|
+
* - Format registration: https://github.com/viamrobotics/rdk/blob/main/rimage/image_file.go
|
|
19
|
+
* - Visualization (`DepthMap.ToPrettyPicture`):
|
|
20
|
+
* https://github.com/viamrobotics/rdk/blob/main/rimage/depth_map.go
|
|
21
|
+
* - Python equivalent (`ViamImage.bytes_to_depth_array`):
|
|
22
|
+
* https://github.com/viamrobotics/viam-python-sdk/blob/main/src/viam/media/video.py
|
|
23
|
+
*
|
|
24
|
+
* Returns undefined if the buffer is too short to be a valid depth image.
|
|
25
|
+
*/
|
|
26
|
+
export declare const decodeViamDepth: (bytes: Uint8Array) => {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
pixels: Uint8ClampedArray;
|
|
30
|
+
} | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Decode the Viam depth format and return a PNG `Blob` suitable for display or export,
|
|
33
|
+
* using the same colorization as `decodeViamDepth`.
|
|
34
|
+
*
|
|
35
|
+
* Returns `undefined` if the buffer is too short or a canvas context is unavailable.
|
|
36
|
+
*/
|
|
37
|
+
export declare const getBlobForViamDepth: (bytes: Uint8Array) => Promise<Blob | undefined>;
|