@viamrobotics/test-widgets 0.11.2 → 0.12.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.
@@ -1,6 +1,4 @@
1
1
  <script lang="ts">
2
- import type { Pose } from '@viamrobotics/sdk'
3
-
4
2
  import { ArmClient } from '@viamrobotics/sdk'
5
3
  import {
6
4
  createResourceClient,
@@ -17,7 +15,7 @@
17
15
  import GetJointPositions from './get-joint-positions.svelte'
18
16
  import { getJointPositionLimits, type KinematicsJSON } from './joint-position-limits'
19
17
  import MoveToJointPositions from './move-to-joint-positions.svelte'
20
- import MoveToPosition from './move-to-position.svelte'
18
+ import MoveToPositionControl from './move-to-position-control.svelte'
21
19
 
22
20
  interface Props {
23
21
  partID: string
@@ -34,22 +32,16 @@
34
32
 
35
33
  const options = { refetchInterval: 500 }
36
34
  const jointPositionsQuery = createResourceQuery(client, 'getJointPositions', options)
37
- const endPositionQuery = createResourceQuery(client, 'getEndPosition', options)
38
35
  const kinematicsQuery = createResourceQuery(client, 'getKinematics', options)
39
36
  const isMovingQuery = createResourceQuery(client, 'isMoving', options)
40
37
 
41
38
  const moveToJointPosMutation = createResourceMutation(client, 'moveToJointPositions')
42
39
  const stopMutation = createResourceMutation(client, 'stop')
43
- const moveToPosMutation = createResourceMutation(client, 'moveToPosition')
44
40
 
45
41
  const moveToJointPositions = (jointPositionsList: number[]) => {
46
42
  moveToJointPosMutation.mutate([jointPositionsList], {})
47
43
  }
48
44
 
49
- const moveToPosition = (position: Pose) => {
50
- moveToPosMutation.mutate([position], {})
51
- }
52
-
53
45
  const jointLimitsDegrees = $derived(
54
46
  kinematicsQuery.data ? getJointPositionLimits(kinematicsQuery.data as KinematicsJSON) : []
55
47
  )
@@ -94,15 +86,10 @@
94
86
  title="MoveToPosition"
95
87
  api="rdk:component:arm"
96
88
  >
97
- <Query query={endPositionQuery}>
98
- {#if endPositionQuery.data}
99
- <MoveToPosition
100
- endPosition={endPositionQuery.data}
101
- {moveToPosition}
102
- lastError={moveToPosMutation.error}
103
- />
104
- {/if}
105
- </Query>
89
+ <MoveToPositionControl
90
+ {partID}
91
+ {resourceName}
92
+ />
106
93
  </ApiSection>
107
94
  </div>
108
95
 
@@ -0,0 +1,19 @@
1
+ /** How the arm MoveToPosition widget executes a move. */
2
+ export type MoveControlMode = 'motion' | 'direct';
3
+ /**
4
+ * The mode the widget starts in: motion planning whenever the machine has a
5
+ * motion service, direct arm control only when it has none.
6
+ */
7
+ export declare const defaultMoveControlMode: (motionServiceNames: string[]) => MoveControlMode;
8
+ /**
9
+ * The motion service a `Move` call targets: `builtin` when present, otherwise
10
+ * the first discovered service.
11
+ *
12
+ * @returns The service name, or `undefined` when the machine has no motion service.
13
+ */
14
+ export declare const moveMotionServiceName: (motionServiceNames: string[]) => string | undefined;
15
+ /**
16
+ * The selectable motion services for the service select: `builtin` first when
17
+ * present, the rest alphabetical.
18
+ */
19
+ export declare const motionServiceOptions: (motionServiceNames: string[]) => string[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The mode the widget starts in: motion planning whenever the machine has a
3
+ * motion service, direct arm control only when it has none.
4
+ */
5
+ export const defaultMoveControlMode = (motionServiceNames) => motionServiceNames.length > 0 ? 'motion' : 'direct';
6
+ /**
7
+ * The motion service a `Move` call targets: `builtin` when present, otherwise
8
+ * the first discovered service.
9
+ *
10
+ * @returns The service name, or `undefined` when the machine has no motion service.
11
+ */
12
+ export const moveMotionServiceName = (motionServiceNames) => motionServiceNames.includes('builtin') ? 'builtin' : motionServiceNames[0];
13
+ /**
14
+ * The selectable motion services for the service select: `builtin` first when
15
+ * present, the rest alphabetical.
16
+ */
17
+ export const motionServiceOptions = (motionServiceNames) => {
18
+ const others = motionServiceNames
19
+ .filter((name) => name !== 'builtin')
20
+ .toSorted((a, b) => a.localeCompare(b));
21
+ return motionServiceNames.includes('builtin') ? ['builtin', ...others] : others;
22
+ };
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Icon, Tooltip } from '@viamrobotics/prime-core'
2
+ import { Icon, ToggleButtons, Tooltip } from '@viamrobotics/prime-core'
3
3
 
4
4
  import AngleUnitToggle from '../../angle-unit-toggle.svelte'
5
5
  import CopyButton from '../../copy-button.svelte'
@@ -11,7 +11,9 @@
11
11
  import { type JointLimit } from './joint-position-limits'
12
12
  import JointPositionQuickMove from './joint-position-quick-move.svelte'
13
13
 
14
- type ControlMode = 'jointPositions' | 'quickMove'
14
+ type ControlMode = 'Joint Positions' | 'Quick Move'
15
+
16
+ const CONTROL_MODES: ControlMode[] = ['Joint Positions', 'Quick Move']
15
17
 
16
18
  interface Props {
17
19
  positions: number[]
@@ -32,9 +34,9 @@
32
34
  // svelte-ignore state_referenced_locally
33
35
  let desiredPositions = $state([...positions]) // in degrees
34
36
  let useRadians = $state(false)
35
- let controlMode = $state<ControlMode>('jointPositions')
37
+ let controlMode = $state<ControlMode>('Joint Positions')
36
38
 
37
- const isQuickMoveMode = $derived(controlMode === 'quickMove')
39
+ const isQuickMoveMode = $derived(controlMode === 'Quick Move')
38
40
 
39
41
  const getJointMin = (index: number): number => jointLimitsDegrees[index]?.minDegrees ?? -180
40
42
  const getJointMax = (index: number): number => jointLimitsDegrees[index]?.maxDegrees ?? 180
@@ -44,8 +46,11 @@
44
46
  const displayPositions = $derived(desiredPositions.map((degrees) => toDisplayAngle(degrees)))
45
47
  const copyData = $derived(`[${displayPositions.map((v) => formatNumeric(v)).join(', ')}]`)
46
48
 
47
- const toggleMode = () => {
48
- controlMode = isQuickMoveMode ? 'jointPositions' : 'quickMove'
49
+ const handleModeChange = ({ detail }: CustomEvent<string>) => {
50
+ const nextMode = CONTROL_MODES.find((mode) => mode === detail)
51
+ if (nextMode) {
52
+ controlMode = nextMode
53
+ }
49
54
  }
50
55
 
51
56
  const handlePaste = (data: string): boolean => {
@@ -63,48 +68,35 @@
63
68
  </script>
64
69
 
65
70
  <div class="flex min-w-0 flex-col gap-4">
66
- <div class="flex items-center justify-between">
67
- <span class="flex flex-row items-center gap-1 text-sm">
68
- Joint Positions
69
- <Tooltip>
70
- <Icon
71
- name="information-outline"
72
- cx="text-gray-6"
73
- />
74
- <span slot="description">
75
- Joint position limits are based solely on the arm kinematics and do not take into account
76
- motion service limit overrides.
77
- </span>
78
- </Tooltip>
79
- </span>
71
+ <div class="flex flex-wrap items-center justify-between gap-2">
72
+ <ToggleButtons
73
+ role="group"
74
+ aria-label="Control mode"
75
+ options={CONTROL_MODES}
76
+ selected={controlMode}
77
+ on:input={handleModeChange}
78
+ />
80
79
  <div class="flex items-center gap-1">
81
- <div class="flex gap-1">
82
- {#if !isQuickMoveMode}
83
- <CopyButton data={copyData} />
84
- <PasteButton onPaste={handlePaste} />
85
- {/if}
86
- <AngleUnitToggle
87
- {useRadians}
88
- onToggle={() => {
89
- useRadians = !useRadians
90
- }}
91
- />
92
- </div>
93
- <Tooltip>
94
- <button
95
- onclick={toggleMode}
96
- aria-label={isQuickMoveMode ? 'Exit quick move mode' : 'Enter quick move mode'}
97
- class={[
98
- 'hover:border-medium hover:bg-medium active:bg-gray-2 rounded p-0.5',
99
- isQuickMoveMode ? 'text-amber-600' : 'text-gray-6',
100
- ]}
101
- >
102
- <Icon name="lightning-bolt-outline" />
103
- </button>
104
- <span slot="description">
105
- {isQuickMoveMode ? 'Exit quick move mode' : 'Enter quick move mode'}
106
- </span>
107
- </Tooltip>
80
+ {#if !isQuickMoveMode}
81
+ <Tooltip>
82
+ <Icon
83
+ name="information-outline"
84
+ cx="text-gray-6"
85
+ />
86
+ <span slot="description">
87
+ Joint position limits are based solely on the arm kinematics and do not take into
88
+ account motion service limit overrides.
89
+ </span>
90
+ </Tooltip>
91
+ <CopyButton data={copyData} />
92
+ <PasteButton onPaste={handlePaste} />
93
+ {/if}
94
+ <AngleUnitToggle
95
+ {useRadians}
96
+ onToggle={() => {
97
+ useRadians = !useRadians
98
+ }}
99
+ />
108
100
  </div>
109
101
  </div>
110
102
 
@@ -0,0 +1,164 @@
1
+ <script lang="ts">
2
+ import { Banner, Icon, Label, Select, ToggleButtons, Tooltip } from '@viamrobotics/prime-core'
3
+ import { ArmClient, MotionClient, type Pose, type RobotClient } from '@viamrobotics/sdk'
4
+ import {
5
+ createResourceClient,
6
+ createResourceMutation,
7
+ createResourceQuery,
8
+ createRobotQuery,
9
+ useResourceStatuses,
10
+ useRobotClient,
11
+ } from '@viamrobotics/svelte-sdk'
12
+
13
+ import Query from '../../query.svelte'
14
+
15
+ import {
16
+ defaultMoveControlMode,
17
+ motionServiceOptions,
18
+ type MoveControlMode,
19
+ moveMotionServiceName,
20
+ } from './move-control-mode'
21
+ import MoveToPosition from './move-to-position.svelte'
22
+
23
+ interface Props {
24
+ partID: string
25
+ resourceName: string
26
+ }
27
+
28
+ const { partID, resourceName }: Props = $props()
29
+
30
+ const motionServices = useResourceStatuses(() => partID, 'motion')
31
+ const motionServiceNames = $derived(
32
+ motionServices.current
33
+ .map((service) => service.name?.name)
34
+ .filter((name): name is string => name !== undefined)
35
+ )
36
+ const hasMotionService = $derived(motionServiceNames.length > 0)
37
+
38
+ let userChoice = $state<MoveControlMode>()
39
+ const mode = $derived(userChoice ?? defaultMoveControlMode(motionServiceNames))
40
+
41
+ let userServiceChoice = $state<string>()
42
+ const activeMotionServiceName = $derived(
43
+ userServiceChoice ?? moveMotionServiceName(motionServiceNames)
44
+ )
45
+ const serviceOptions = $derived(motionServiceOptions(motionServiceNames))
46
+ const showServiceSelect = $derived(mode === 'motion' && serviceOptions.length > 1)
47
+
48
+ const robotClient = useRobotClient(() => partID)
49
+ const armClient = createResourceClient(
50
+ ArmClient,
51
+ () => partID,
52
+ () => resourceName
53
+ )
54
+ const motionClient = createResourceClient(
55
+ MotionClient,
56
+ () => partID,
57
+ () => activeMotionServiceName ?? ''
58
+ )
59
+
60
+ // Pre-fill the editor with the arm's current pose, in the frame the active mode sends to.
61
+ const poseArgs = $derived<Parameters<RobotClient['getPose']>>([resourceName, 'world', []])
62
+ const poseQuery = createRobotQuery(
63
+ robotClient,
64
+ 'getPose',
65
+ () => poseArgs,
66
+ () => ({ enabled: mode === 'motion' })
67
+ )
68
+ const endPositionQuery = createResourceQuery(armClient, 'getEndPosition', () => ({
69
+ refetchInterval: 500,
70
+ enabled: mode === 'direct',
71
+ }))
72
+
73
+ const activeQuery = $derived(mode === 'motion' ? poseQuery : endPositionQuery)
74
+ const endPosition = $derived(mode === 'motion' ? poseQuery.data?.pose : endPositionQuery.data)
75
+
76
+ const moveMutation = createResourceMutation(motionClient, 'move')
77
+ const moveToPosMutation = createResourceMutation(armClient, 'moveToPosition')
78
+ const lastError = $derived(mode === 'motion' ? moveMutation.error : moveToPosMutation.error)
79
+
80
+ const description = $derived(
81
+ mode === 'motion'
82
+ ? 'Pose is in the world frame, as required by the motion service.'
83
+ : 'Pose is with respect to the arm origin and does not take into account the motion service or frame system.'
84
+ )
85
+
86
+ const handleModeInput = (event: CustomEvent<string>) => {
87
+ userChoice = event.detail === 'Motion service' ? 'motion' : 'direct'
88
+ }
89
+
90
+ const handleServiceChange = (event: Event) => {
91
+ if (event.target instanceof HTMLSelectElement) {
92
+ userServiceChoice = event.target.value
93
+ }
94
+ }
95
+
96
+ const moveToPosition = (position: Pose) => {
97
+ if (mode === 'motion') {
98
+ moveMutation.mutate([{ referenceFrame: 'world', pose: position }, resourceName], {})
99
+ } else {
100
+ moveToPosMutation.mutate([position], {})
101
+ }
102
+ }
103
+ </script>
104
+
105
+ <div class="flex flex-col gap-4">
106
+ {#if hasMotionService}
107
+ <Label position="top">
108
+ <span class="flex items-center gap-1 text-xs">
109
+ Control mode
110
+ <Tooltip>
111
+ <Icon
112
+ name="information-outline"
113
+ size="sm"
114
+ />
115
+ <span slot="description">
116
+ Using a motion service will include motion planning and obstacle avoidance. Direct arm
117
+ control will move the arm without any planning and regardless of obstacles.
118
+ </span>
119
+ </Tooltip>
120
+ </span>
121
+
122
+ <ToggleButtons
123
+ slot="input"
124
+ options={['Motion service', 'Arm']}
125
+ selected={mode === 'motion' ? 'Motion service' : 'Arm'}
126
+ on:input={handleModeInput}
127
+ />
128
+ </Label>
129
+ {/if}
130
+ {#if showServiceSelect}
131
+ <Label>
132
+ Motion service name
133
+
134
+ <Select
135
+ slot="input"
136
+ value={activeMotionServiceName ?? ''}
137
+ on:change={handleServiceChange}
138
+ >
139
+ {#each serviceOptions as name (name)}
140
+ <option value={name}>{name}</option>
141
+ {/each}
142
+ </Select>
143
+ </Label>
144
+ {/if}
145
+ <Banner variant={mode === 'motion' ? 'info' : 'danger'}>
146
+ {#snippet subtitle()}
147
+ {#if mode === 'motion'}
148
+ Movement goes through motion planning and attempts to avoid obstacles.
149
+ {:else}
150
+ The arm will not avoid obstacles when moving. Use with caution.
151
+ {/if}
152
+ {/snippet}
153
+ </Banner>
154
+ <Query query={activeQuery}>
155
+ {#if endPosition}
156
+ <MoveToPosition
157
+ {endPosition}
158
+ {moveToPosition}
159
+ {lastError}
160
+ {description}
161
+ />
162
+ {/if}
163
+ </Query>
164
+ </div>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ partID: string;
3
+ resourceName: string;
4
+ }
5
+ declare const MoveToPositionControl: import("svelte").Component<Props, {}, "">;
6
+ type MoveToPositionControl = ReturnType<typeof MoveToPositionControl>;
7
+ export default MoveToPositionControl;
@@ -1,14 +1,5 @@
1
1
  <script lang="ts">
2
- import { ArmClient, type Pose } from '@viamrobotics/sdk'
3
- import {
4
- createResourceClient,
5
- createResourceMutation,
6
- createResourceQuery,
7
- } from '@viamrobotics/svelte-sdk'
8
-
9
- import Query from '../../query.svelte'
10
-
11
- import MoveToPosition from './move-to-position.svelte'
2
+ import MoveToPositionControl from './move-to-position-control.svelte'
12
3
 
13
4
  interface Props {
14
5
  partID: string
@@ -16,28 +7,9 @@
16
7
  }
17
8
 
18
9
  const { partID, resourceName }: Props = $props()
19
-
20
- const client = createResourceClient(
21
- ArmClient,
22
- () => partID,
23
- () => resourceName
24
- )
25
-
26
- const endPositionQuery = createResourceQuery(client, 'getEndPosition', {
27
- refetchInterval: 500,
28
- })
29
- const moveToPosMutation = createResourceMutation(client, 'moveToPosition')
30
- const moveToPosition = (position: Pose) => {
31
- moveToPosMutation.mutate([position], {})
32
- }
33
10
  </script>
34
11
 
35
- <Query query={endPositionQuery}>
36
- {#if endPositionQuery.data}
37
- <MoveToPosition
38
- endPosition={endPositionQuery.data}
39
- {moveToPosition}
40
- lastError={moveToPosMutation.error}
41
- />
42
- {/if}
43
- </Query>
12
+ <MoveToPositionControl
13
+ {partID}
14
+ {resourceName}
15
+ />
@@ -10,9 +10,15 @@
10
10
  endPosition: Pose
11
11
  moveToPosition: (position: Pose) => void
12
12
  lastError: Error | null
13
+ description?: string
13
14
  }
14
15
 
15
- const { endPosition, moveToPosition, lastError }: Props = $props()
16
+ const {
17
+ endPosition,
18
+ moveToPosition,
19
+ lastError,
20
+ description = 'Pose is with respect to the arm origin and does not take into account the motion service or frame system.',
21
+ }: Props = $props()
16
22
 
17
23
  // svelte-ignore state_referenced_locally
18
24
  let desiredPosition = $state({ ...endPosition })
@@ -41,7 +47,7 @@
41
47
  desiredPosition = next
42
48
  }}
43
49
  title="Pose Values"
44
- description="Pose is with respect to the arm origin and does not take into account the motion service or frame system."
50
+ {description}
45
51
  />
46
52
 
47
53
  <div class="mb-2 flex flex-col gap-2">
@@ -3,6 +3,7 @@ interface Props {
3
3
  endPosition: Pose;
4
4
  moveToPosition: (position: Pose) => void;
5
5
  lastError: Error | null;
6
+ description?: string;
6
7
  }
7
8
  declare const MoveToPosition: import("svelte").Component<Props, {}, "">;
8
9
  type MoveToPosition = ReturnType<typeof MoveToPosition>;
@@ -1,7 +1,6 @@
1
1
  import { MLModelClient, ResourceName } from '@viamrobotics/sdk';
2
+ import { type ResourceClientContext } from '@viamrobotics/svelte-sdk';
2
3
  import { clientMap } from '../../../client-map';
3
4
  type DoCommandable = InstanceType<Exclude<(typeof clientMap)[keyof typeof clientMap], typeof MLModelClient>>;
4
- export declare const createDoCommandClient: (resource: () => ResourceName, partID: () => string, resourceName: () => string) => {
5
- current: DoCommandable | undefined;
6
- };
5
+ export declare const createDoCommandClient: (resource: () => ResourceName, partID: () => string, resourceName: () => string) => ResourceClientContext<DoCommandable>;
7
6
  export {};
@@ -1,5 +1,5 @@
1
1
  import { MachineConnectionEvent, MLModelClient, ResourceName } from '@viamrobotics/sdk';
2
- import { useConnectionStatus, useRobotClient } from '@viamrobotics/svelte-sdk';
2
+ import { useConnectionStatus, useRobotClient, } from '@viamrobotics/svelte-sdk';
3
3
  import { clientForResource, clientMap } from '../../../client-map';
4
4
  export const createDoCommandClient = (resource, partID, resourceName) => {
5
5
  const robotClient = useRobotClient(partID);
@@ -22,5 +22,11 @@ export const createDoCommandClient = (resource, partID, resourceName) => {
22
22
  get current() {
23
23
  return resourceClient;
24
24
  },
25
+ get partID() {
26
+ return partID();
27
+ },
28
+ get name() {
29
+ return resourceName();
30
+ },
25
31
  };
26
32
  };
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { useResourceNames } from '@viamrobotics/svelte-sdk'
2
+ import { useResourceStatuses } from '@viamrobotics/svelte-sdk'
3
3
 
4
4
  import DoCommand from './do-command.svelte'
5
5
 
@@ -10,9 +10,9 @@
10
10
 
11
11
  const { partID, resourceName }: Props = $props()
12
12
 
13
- const resourceNames = useResourceNames(() => partID)
13
+ const statuses = useResourceStatuses(() => partID)
14
14
  const resource = $derived(
15
- resourceNames.current.find((candidate) => candidate.name === resourceName)
15
+ statuses.current.find((status) => status.name?.name === resourceName)?.name
16
16
  )
17
17
  </script>
18
18
 
@@ -24,7 +24,7 @@
24
24
  const lng = $derived(safeReadCoordinate(coordinate?.longitude))
25
25
  </script>
26
26
 
27
- <div class="relative h-[300px] w-full lg:h-auto lg:w-1/2">
27
+ <div class="relative h-[300px] w-full @4xl:h-auto @4xl:w-1/2">
28
28
  <MapLibre
29
29
  zoom={15}
30
30
  options={{ attributionControl: { compact: false } }}
@@ -103,150 +103,152 @@
103
103
  query={propertiesQuery}
104
104
  contentCx="p-4 h-14"
105
105
  >
106
- <div class="flex flex-wrap text-xs lg:flex-nowrap">
107
- <div class="flex w-full flex-col gap-5 py-4 pr-6 pl-4 lg:w-1/4">
108
- {#if propertiesQuery.data?.positionSupported}
109
- <div class="flex flex-col gap-2">
110
- <SectionTitle
111
- title="GetPosition"
112
- api={MS_API}
113
- />
114
- <Query
115
- query={positionQuery}
116
- contentCx="h-6"
117
- >
118
- {#if positionQuery.data !== undefined}
119
- <Position data={positionQuery.data} />
120
- {/if}
121
- </Query>
122
- </div>
123
- {/if}
106
+ <div class="@container">
107
+ <div class="flex flex-wrap text-xs @4xl:flex-nowrap">
108
+ <div class="flex w-full flex-col gap-5 py-4 pr-6 pl-4 @4xl:w-1/4">
109
+ {#if propertiesQuery.data?.positionSupported}
110
+ <div class="flex flex-col gap-2">
111
+ <SectionTitle
112
+ title="GetPosition"
113
+ api={MS_API}
114
+ />
115
+ <Query
116
+ query={positionQuery}
117
+ contentCx="h-6"
118
+ >
119
+ {#if positionQuery.data !== undefined}
120
+ <Position data={positionQuery.data} />
121
+ {/if}
122
+ </Query>
123
+ </div>
124
+ {/if}
124
125
 
125
- {#if propertiesQuery.data?.orientationSupported}
126
- <div class="flex flex-col gap-2">
127
- <SectionTitle
128
- title="GetOrientation"
129
- api={MS_API}
130
- >
131
- {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal">(º)</span
132
- >{/snippet}
133
- </SectionTitle>
134
- <Query
135
- query={orientationQuery}
136
- contentCx="h-6"
137
- >
138
- {#if orientationQuery.data !== undefined}
139
- <Orientation data={orientationQuery.data} />
140
- {/if}
141
- </Query>
142
- </div>
143
- {/if}
126
+ {#if propertiesQuery.data?.orientationSupported}
127
+ <div class="flex flex-col gap-2">
128
+ <SectionTitle
129
+ title="GetOrientation"
130
+ api={MS_API}
131
+ >
132
+ {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal">(º)</span
133
+ >{/snippet}
134
+ </SectionTitle>
135
+ <Query
136
+ query={orientationQuery}
137
+ contentCx="h-6"
138
+ >
139
+ {#if orientationQuery.data !== undefined}
140
+ <Orientation data={orientationQuery.data} />
141
+ {/if}
142
+ </Query>
143
+ </div>
144
+ {/if}
144
145
 
145
- {#if propertiesQuery.data?.compassHeadingSupported}
146
- <div class="flex flex-col gap-2">
147
- <SectionTitle
148
- title="GetCompassHeading"
149
- api={MS_API}
150
- >
151
- {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal">(º)</span
152
- >{/snippet}
153
- </SectionTitle>
154
- <Query
155
- query={compassHeadingQuery}
156
- contentCx="h-6"
157
- >
158
- {#if compassHeadingQuery.data !== undefined}
159
- <CompassHeading data={compassHeadingQuery.data} />
160
- {/if}
161
- </Query>
162
- </div>
163
- {/if}
164
- </div>
146
+ {#if propertiesQuery.data?.compassHeadingSupported}
147
+ <div class="flex flex-col gap-2">
148
+ <SectionTitle
149
+ title="GetCompassHeading"
150
+ api={MS_API}
151
+ >
152
+ {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal">(º)</span
153
+ >{/snippet}
154
+ </SectionTitle>
155
+ <Query
156
+ query={compassHeadingQuery}
157
+ contentCx="h-6"
158
+ >
159
+ {#if compassHeadingQuery.data !== undefined}
160
+ <CompassHeading data={compassHeadingQuery.data} />
161
+ {/if}
162
+ </Query>
163
+ </div>
164
+ {/if}
165
+ </div>
165
166
 
166
- <div class="flex w-full flex-col gap-5 p-4 lg:w-1/4">
167
- {#if propertiesQuery.data?.angularVelocitySupported}
168
- <div class="flex flex-col gap-2">
169
- <SectionTitle
170
- title="GetAngularVelocity"
171
- api={MS_API}
172
- >
173
- {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal">(º/s)</span
174
- >{/snippet}
175
- </SectionTitle>
176
- <Query
177
- query={angularVelocityQuery}
178
- contentCx="h-6"
179
- >
180
- {#if angularVelocityQuery.data !== undefined}
181
- <Vector3 data={angularVelocityQuery.data} />
182
- {/if}
183
- </Query>
184
- </div>
185
- {/if}
167
+ <div class="flex w-full flex-col gap-5 p-4 @4xl:w-1/4">
168
+ {#if propertiesQuery.data?.angularVelocitySupported}
169
+ <div class="flex flex-col gap-2">
170
+ <SectionTitle
171
+ title="GetAngularVelocity"
172
+ api={MS_API}
173
+ >
174
+ {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal">(º/s)</span
175
+ >{/snippet}
176
+ </SectionTitle>
177
+ <Query
178
+ query={angularVelocityQuery}
179
+ contentCx="h-6"
180
+ >
181
+ {#if angularVelocityQuery.data !== undefined}
182
+ <Vector3 data={angularVelocityQuery.data} />
183
+ {/if}
184
+ </Query>
185
+ </div>
186
+ {/if}
186
187
 
187
- {#if propertiesQuery.data?.linearVelocitySupported}
188
- <div class="flex flex-col gap-2">
189
- <SectionTitle
190
- title="GetLinearVelocity"
191
- api={MS_API}
192
- >
193
- {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal">(m/s)</span
194
- >{/snippet}
195
- </SectionTitle>
196
- <Query
197
- query={linearVelocityQuery}
198
- contentCx="h-6"
199
- >
200
- {#if linearVelocityQuery.data !== undefined}
201
- <Vector3 data={linearVelocityQuery.data} />
202
- {/if}
203
- </Query>
204
- </div>
205
- {/if}
188
+ {#if propertiesQuery.data?.linearVelocitySupported}
189
+ <div class="flex flex-col gap-2">
190
+ <SectionTitle
191
+ title="GetLinearVelocity"
192
+ api={MS_API}
193
+ >
194
+ {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal">(m/s)</span
195
+ >{/snippet}
196
+ </SectionTitle>
197
+ <Query
198
+ query={linearVelocityQuery}
199
+ contentCx="h-6"
200
+ >
201
+ {#if linearVelocityQuery.data !== undefined}
202
+ <Vector3 data={linearVelocityQuery.data} />
203
+ {/if}
204
+ </Query>
205
+ </div>
206
+ {/if}
207
+
208
+ {#if propertiesQuery.data?.linearAccelerationSupported}
209
+ <div class="flex flex-col gap-2">
210
+ <SectionTitle
211
+ title="GetLinearAcceleration"
212
+ api={MS_API}
213
+ >
214
+ {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal"
215
+ >(m/s<sup>2</sup>)</span
216
+ >{/snippet}
217
+ </SectionTitle>
218
+ <Query
219
+ query={linearAccelerationQuery}
220
+ contentCx="h-6"
221
+ >
222
+ {#if linearAccelerationQuery.data !== undefined}
223
+ <Vector3 data={linearAccelerationQuery.data} />
224
+ {/if}
225
+ </Query>
226
+ </div>
227
+ {/if}
206
228
 
207
- {#if propertiesQuery.data?.linearAccelerationSupported}
208
229
  <div class="flex flex-col gap-2">
209
230
  <SectionTitle
210
- title="GetLinearAcceleration"
231
+ title="GetAccuracy"
211
232
  api={MS_API}
212
- >
213
- {#snippet suffix()}<span class="text-subtle-2 text-xs font-normal"
214
- >(m/s<sup>2</sup>)</span
215
- >{/snippet}
216
- </SectionTitle>
233
+ />
217
234
  <Query
218
- query={linearAccelerationQuery}
235
+ query={accuracyQuery}
219
236
  contentCx="h-6"
220
237
  >
221
- {#if linearAccelerationQuery.data !== undefined}
222
- <Vector3 data={linearAccelerationQuery.data} />
238
+ {#if accuracyQuery.data !== undefined}
239
+ <Accuracy data={accuracyQuery.data} />
223
240
  {/if}
224
241
  </Query>
225
242
  </div>
226
- {/if}
243
+ </div>
227
244
 
228
- <div class="flex flex-col gap-2">
229
- <SectionTitle
230
- title="GetAccuracy"
231
- api={MS_API}
245
+ {#if propertiesQuery.data?.positionSupported}
246
+ <Map
247
+ coordinate={positionQuery.data?.coordinate}
248
+ rotation={orientationQuery.data?.oZ}
232
249
  />
233
- <Query
234
- query={accuracyQuery}
235
- contentCx="h-6"
236
- >
237
- {#if accuracyQuery.data !== undefined}
238
- <Accuracy data={accuracyQuery.data} />
239
- {/if}
240
- </Query>
241
- </div>
250
+ {/if}
242
251
  </div>
243
-
244
- {#if propertiesQuery.data?.positionSupported}
245
- <Map
246
- coordinate={positionQuery.data?.coordinate}
247
- rotation={orientationQuery.data?.oZ}
248
- />
249
- {/if}
250
252
  </div>
251
253
  </Query>
252
254
 
@@ -134,7 +134,7 @@
134
134
 
135
135
  <ConnectionStatus {partID}>
136
136
  {#snippet connected()}
137
- <div class="flex w-full justify-between p-4">
137
+ <div class="flex w-full flex-wrap justify-between gap-2 p-4">
138
138
  <RefetchController
139
139
  {refetchInterval}
140
140
  queries={[locationQuery, obstaclesQuery, waypointsQuery]}
@@ -152,85 +152,89 @@
152
152
  </div>
153
153
  </div>
154
154
 
155
- <div class="relative h-120 w-full items-stretch p-2 sm:flex">
156
- <div class="w-full max-w-62.5 p-2">
157
- <TabsBar variant="secondary">
158
- <Tab
159
- title="Obstacles"
160
- selected={tab.current === 'obstacles'}
161
- selectTab={() => (tab.current = 'obstacles')}
162
- />
163
- <Tab
164
- title="Waypoints"
165
- selected={tab.current === 'waypoints'}
166
- selectTab={() => (tab.current = 'waypoints')}
167
- />
168
- </TabsBar>
169
-
170
- <ul class="h-[calc(100%-32px)] overflow-auto">
171
- {#if tab.current === 'obstacles'}
172
- <ObstaclesLegend
173
- {map}
174
- {hovered}
175
- {obstacles}
176
- onEnter={setHovered}
177
- onLeave={setHovered}
155
+ <div class="@container">
156
+ <div class="relative flex w-full flex-col items-stretch p-2 @2xl:h-120 @2xl:flex-row">
157
+ <div class="w-full p-2 @2xl:max-w-62.5">
158
+ <TabsBar variant="secondary">
159
+ <Tab
160
+ title="Obstacles"
161
+ selected={tab.current === 'obstacles'}
162
+ selectTab={() => (tab.current = 'obstacles')}
178
163
  />
179
- {:else if tab.current === 'waypoints'}
180
- <WaypointsLegend
181
- {map}
182
- {hovered}
183
- {waypoints}
184
- onEnter={setHovered}
185
- onLeave={setHovered}
186
- onRemove={async (id) => {
187
- await removeWaypointMutation.mutateAsync([id])
188
- await waypointsQuery.refetch()
189
- }}
164
+ <Tab
165
+ title="Waypoints"
166
+ selected={tab.current === 'waypoints'}
167
+ selectTab={() => (tab.current = 'waypoints')}
190
168
  />
191
- {/if}
192
- </ul>
193
- </div>
169
+ </TabsBar>
170
+
171
+ <ul class="max-h-60 overflow-auto @2xl:h-[calc(100%-32px)] @2xl:max-h-none">
172
+ {#if tab.current === 'obstacles'}
173
+ <ObstaclesLegend
174
+ {map}
175
+ {hovered}
176
+ {obstacles}
177
+ onEnter={setHovered}
178
+ onLeave={setHovered}
179
+ />
180
+ {:else if tab.current === 'waypoints'}
181
+ <WaypointsLegend
182
+ {map}
183
+ {hovered}
184
+ {waypoints}
185
+ onEnter={setHovered}
186
+ onLeave={setHovered}
187
+ onRemove={async (id) => {
188
+ await removeWaypointMutation.mutateAsync([id])
189
+ await waypointsQuery.refetch()
190
+ }}
191
+ />
192
+ {/if}
193
+ </ul>
194
+ </div>
194
195
 
195
- <MapLibre
196
- class="relative grow"
197
- maxPitch={view === '3D' ? 60 : 0}
198
- mapProvider={MapProviders.googleMaps}
199
- mapProviderKey={import.meta.env.VITE_GOOGLE_MAPS_API_KEY}
200
- bind:map
201
- >
202
- <NavigationControls showZoom={false} />
196
+ <div class="relative h-80 @2xl:h-auto @2xl:grow">
197
+ <MapLibre
198
+ class="relative"
199
+ maxPitch={view === '3D' ? 60 : 0}
200
+ mapProvider={MapProviders.googleMaps}
201
+ mapProviderKey={import.meta.env.VITE_GOOGLE_MAPS_API_KEY}
202
+ bind:map
203
+ >
204
+ <NavigationControls showZoom={false} />
205
+
206
+ <div class="absolute top-5 right-14 z-10 flex items-center gap-2">
207
+ <ToggleButtons
208
+ options={['2D', '3D']}
209
+ selected={view}
210
+ on:input={handleViewSelect}
211
+ />
212
+ <SatelliteControls />
213
+ <CenterControls />
214
+ </div>
215
+
216
+ {#if locationQuery.data}
217
+ <DirectionalMarker position={locationQuery.data} />
218
+ {/if}
219
+
220
+ <Waypoints
221
+ {waypoints}
222
+ {addWayPoint}
223
+ tab={tab.current ?? 'obstacles'}
224
+ {hovered}
225
+ />
203
226
 
204
- <div class="absolute top-5 right-14 z-10 flex items-center gap-2">
205
- <ToggleButtons
206
- options={['2D', '3D']}
207
- selected={view}
208
- on:input={handleViewSelect}
209
- />
210
- <SatelliteControls />
211
- <CenterControls />
227
+ {#snippet layer()}
228
+ <Obstacles
229
+ {obstacles}
230
+ {hovered}
231
+ {setHovered}
232
+ {view}
233
+ />
234
+ {/snippet}
235
+ </MapLibre>
212
236
  </div>
213
-
214
- {#if locationQuery.data}
215
- <DirectionalMarker position={locationQuery.data} />
216
- {/if}
217
-
218
- <Waypoints
219
- {waypoints}
220
- {addWayPoint}
221
- tab={tab.current ?? 'obstacles'}
222
- {hovered}
223
- />
224
-
225
- {#snippet layer()}
226
- <Obstacles
227
- {obstacles}
228
- {hovered}
229
- {setHovered}
230
- {view}
231
- />
232
- {/snippet}
233
- </MapLibre>
237
+ </div>
234
238
  </div>
235
239
  {/snippet}
236
240
  </ConnectionStatus>
@@ -49,7 +49,7 @@
49
49
  End position
50
50
  <abbr class="text-disabled">(m)</abbr>
51
51
  </h4>
52
- <div class="flex flex-row gap-2 px-3 py-2.5">
52
+ <div class="flex flex-wrap gap-2 px-3 py-2.5">
53
53
  <Label>
54
54
  X
55
55
 
@@ -191,78 +191,86 @@
191
191
  >.
192
192
  </div>
193
193
  {:else}
194
- <div class="flex divide-x">
195
- <div class="divide-y">
196
- <div class="m-4">
197
- <RefetchController
198
- {refetchInterval}
199
- queries={[positionQuery, pointCloudMapQuery]}
194
+ <div class="@container">
195
+ <div class="flex flex-col @2xl:flex-row @2xl:divide-x">
196
+ <div class="divide-y">
197
+ <div class="m-4">
198
+ <RefetchController
199
+ {refetchInterval}
200
+ queries={[positionQuery, pointCloudMapQuery]}
201
+ />
202
+ </div>
203
+
204
+ <ApiSection
205
+ title="GetPosition"
206
+ api="rdk:service:slam"
207
+ >
208
+ <Queries
209
+ queries={[propertiesQuery, positionQuery]}
210
+ contentCx="h-6"
211
+ >
212
+ {#if positionQuery.data !== undefined}
213
+ <Position position={positionQuery.data} />
214
+ {/if}
215
+ </Queries>
216
+ </ApiSection>
217
+
218
+ <ApiSection title="Motion">
219
+ <Label>
220
+ Base name
221
+
222
+ <Input
223
+ slot="input"
224
+ bind:value={baseName}
225
+ />
226
+ </Label>
227
+ <Label>
228
+ Motion name
229
+
230
+ <Input
231
+ slot="input"
232
+ placeholder="builtin"
233
+ bind:value={motionName}
234
+ />
235
+ </Label>
236
+ </ApiSection>
237
+
238
+ <MoveOnMap
239
+ {destination}
240
+ {updateDestination}
241
+ {moveOnMap}
242
+ {stopPlan}
243
+ lastError={moveOnMapMutation.error ?? stopPlanMutation.error}
200
244
  />
201
245
  </div>
202
246
 
203
- <ApiSection
204
- title="GetPosition"
205
- api="rdk:service:slam"
206
- >
247
+ <div class="flex w-full">
207
248
  <Queries
208
- queries={[propertiesQuery, positionQuery]}
209
- contentCx="h-6"
249
+ queries={[propertiesQuery, positionQuery, pointCloudMapQuery]}
250
+ contentCx="p-4 h-auto"
210
251
  >
211
- {#if positionQuery.data !== undefined}
212
- <Position position={positionQuery.data} />
252
+ {#if positionQuery.data?.pose !== undefined && pointCloudMapQuery.data !== undefined}
253
+ <div class="h-80 w-full @2xl:h-full">
254
+ <SlamMap2D
255
+ pointcloud={pointCloudMapQuery.data}
256
+ basePose={{
257
+ // Position is returned in millimeters, but the map uses meters
258
+ x: positionQuery.data.pose.x / 1000,
259
+ y: positionQuery.data.pose.y / 1000,
260
+ theta: positionQuery.data.pose.theta,
261
+ }}
262
+ {motionPath}
263
+ destination={destination
264
+ ? new Vector2(destination.x, destination.y)
265
+ : undefined}
266
+ helpers={true}
267
+ onClick={handleClick}
268
+ />
269
+ </div>
213
270
  {/if}
214
271
  </Queries>
215
- </ApiSection>
216
-
217
- <ApiSection title="Motion">
218
- <Label>
219
- Base name
220
-
221
- <Input
222
- slot="input"
223
- bind:value={baseName}
224
- />
225
- </Label>
226
- <Label>
227
- Motion name
228
-
229
- <Input
230
- slot="input"
231
- placeholder="builtin"
232
- bind:value={motionName}
233
- />
234
- </Label>
235
- </ApiSection>
236
-
237
- <MoveOnMap
238
- {destination}
239
- {updateDestination}
240
- {moveOnMap}
241
- {stopPlan}
242
- lastError={moveOnMapMutation.error ?? stopPlanMutation.error}
243
- />
272
+ </div>
244
273
  </div>
245
-
246
- <Queries
247
- queries={[propertiesQuery, positionQuery, pointCloudMapQuery]}
248
- contentCx="p-4 h-auto"
249
- >
250
- {#if positionQuery.data?.pose !== undefined && pointCloudMapQuery.data !== undefined}
251
- <SlamMap2D
252
- pointcloud={pointCloudMapQuery.data}
253
- basePose={{
254
- // Position is returned in millimeters, but the map uses meters
255
- x: positionQuery.data.pose.x / 1000,
256
- y: positionQuery.data.pose.y / 1000,
257
- theta: positionQuery.data.pose.theta,
258
- }}
259
- {motionPath}
260
- destination={destination ? new Vector2(destination.x, destination.y) : undefined}
261
- helpers={true}
262
- onClick={handleClick}
263
- />
264
- {/if}
265
- </Queries>
266
274
  </div>
267
275
  {/if}
268
276
  </Query>
@@ -4,7 +4,7 @@
4
4
  import {
5
5
  createResourceClient,
6
6
  createResourceQuery,
7
- useResourceNames,
7
+ useResourceStatuses,
8
8
  } from '@viamrobotics/svelte-sdk'
9
9
 
10
10
  import { useAddImageToDataset } from '../../../add-image-to-dataset'
@@ -46,12 +46,18 @@
46
46
  let showObjectPointClouds = $state(false)
47
47
  let isRemote = $state(false)
48
48
 
49
- const cameras = useResourceNames(() => partID, 'camera')
49
+ const cameraStatuses = useResourceStatuses(() => partID, 'camera')
50
+
51
+ const cameras = $derived(
52
+ cameraStatuses.current
53
+ .map((status) => status.name?.name)
54
+ .filter((name): name is string => name !== undefined)
55
+ )
50
56
 
51
57
  $effect.pre(() => {
52
- if (cameras.current.length > 0 && !initialFetchComplete) {
58
+ if (cameras.length > 0 && !initialFetchComplete) {
53
59
  initialFetchComplete = true
54
- cameraName = cameras.current[0]?.name ?? ''
60
+ cameraName = cameras[0] ?? ''
55
61
  }
56
62
  })
57
63
 
@@ -136,7 +142,7 @@
136
142
  >
137
143
  Default camera
138
144
  </option>
139
- {#each cameras.current as { name } (name)}
145
+ {#each cameras as name (name)}
140
146
  <option
141
147
  selected={cameraName === name}
142
148
  value={name}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/test-widgets",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "wireit": {
@@ -249,7 +249,7 @@
249
249
  "@types/three": "^0.183.1",
250
250
  "@viamrobotics/prime-core": "^0.1.22",
251
251
  "@viamrobotics/sdk": "^0.69.0",
252
- "@viamrobotics/svelte-sdk": "^1.2.1",
252
+ "@viamrobotics/svelte-sdk": "^1.5.1",
253
253
  "@viamrobotics/tailwind-config": "^1.2.0",
254
254
  "@vitest/browser": "^4.1.10",
255
255
  "@vitest/browser-playwright": "^4.1.5",