@viamrobotics/test-widgets 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +33 -0
  2. package/dist/components/index.d.ts +2 -0
  3. package/dist/components/index.js +2 -0
  4. package/dist/components/navigation-map/components/obstacle.svelte +2 -1
  5. package/dist/components/three/capsule-geometry.d.ts +10 -0
  6. package/dist/components/three/capsule-geometry.js +19 -0
  7. package/dist/components/three/index.d.ts +1 -0
  8. package/dist/components/three/index.js +1 -0
  9. package/dist/components/widgets/do-command/do-command.svelte +21 -7
  10. package/dist/components/widgets/do-command/do-command.svelte.d.ts +8 -1
  11. package/dist/components/widgets/motion/frame-select.svelte +29 -0
  12. package/dist/components/widgets/motion/frame-select.svelte.d.ts +9 -0
  13. package/dist/components/widgets/motion/frame-system-config.d.ts +37 -0
  14. package/dist/components/widgets/motion/frame-system-config.js +42 -0
  15. package/dist/components/widgets/motion/motion.svelte +65 -0
  16. package/dist/components/widgets/motion/motion.svelte.d.ts +7 -0
  17. package/dist/components/widgets/motion/move-widget.svelte +65 -0
  18. package/dist/components/widgets/motion/move-widget.svelte.d.ts +11 -0
  19. package/dist/components/widgets/motion/move.svelte +119 -0
  20. package/dist/components/widgets/motion/move.svelte.d.ts +16 -0
  21. package/dist/components/widgets/motion/parse-move-args.d.ts +24 -0
  22. package/dist/components/widgets/motion/parse-move-args.js +23 -0
  23. package/dist/components/widgets/motion/parse-pasted-pose.d.ts +10 -0
  24. package/dist/components/widgets/motion/parse-pasted-pose.js +30 -0
  25. package/dist/components/widgets/motion/pose-input.svelte +154 -0
  26. package/dist/components/widgets/motion/pose-input.svelte.d.ts +8 -0
  27. package/dist/components/widgets/navigation/obstacles/meshes.svelte +1 -2
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.js +5 -4
  30. package/dist/is-known-resource.d.ts +3 -0
  31. package/dist/is-known-resource.js +5 -0
  32. package/dist/registry.d.ts +28 -0
  33. package/dist/{resource-widget.js → registry.js} +12 -26
  34. package/dist/resource-widget-types.d.ts +21 -0
  35. package/dist/resource-widget-types.js +1 -0
  36. package/dist/show-resource-widget.d.ts +3 -0
  37. package/dist/show-resource-widget.js +10 -0
  38. package/package.json +21 -4
package/README.md CHANGED
@@ -4,6 +4,39 @@ A library of Svelte components for interacting with Viam-powered machines. Each
4
4
 
5
5
  Also includes reusable building blocks for visualizations, such as maps (MapLibre), SLAM mapping, 3D point clouds (Three.js), etc.
6
6
 
7
+ ## Entry points
8
+
9
+ There is one root entry plus a registry entry point. Which you reach for depends on whether you name widgets **statically** or resolve them **dynamically** at runtime.
10
+
11
+ ### Static use
12
+
13
+ Import the widget components you need and render them:
14
+
15
+ ```ts
16
+ import { ArmWidget, CameraWidget } from '@viamrobotics/test-widgets'
17
+ ```
18
+
19
+ ### Dynamic use
20
+
21
+ If you resolve widgets at runtime from a resource (for example, a control panel that lists every API of every resource on a scanned machine), import the registry:
22
+
23
+ ```ts
24
+ import { apiWidgetsForResource, widgetForResource } from '@viamrobotics/test-widgets/registry'
25
+ ```
26
+
27
+ - **`@viamrobotics/test-widgets/registry`** — the lookups `apiWidgetsForResource(resource)`, `widgetForResource(resource)`, and `availableAPIWidgets()`, which resolve any resource, component or service. The registry references every widget, so importing it pulls **all** widgets and their optional peers into your build. Keeping these lookups out of the root is what lets the root stay tree-shakeable.
28
+
29
+ ### Optional peer dependencies
30
+
31
+ A few widgets depend on optional peers. Install a peer only if you render a widget (or import an entry point) that needs it; otherwise it stays out of your build.
32
+
33
+ | Optional peer | Required by |
34
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `maplibre-gl` | `MovementSensorWidget` (a **component** — plots GPS position on a map), `NavigationServiceWidget`, and the `maplibre` / `navigation-map` building blocks |
36
+ | `@viamrobotics/three` | `NavigationServiceWidget` (navigation-map 3D geometry) |
37
+
38
+ So a consumer that names widgets statically installs a peer only for the widgets it renders: `maplibre-gl` only if it renders `MovementSensorWidget` or `NavigationServiceWidget`, and `@viamrobotics/three` only for `NavigationServiceWidget`. Importing `/registry` references every widget, so it needs both.
39
+
7
40
  ## Playground
8
41
 
9
42
  The playground (`pnpm dev`) can be used to develop the test-cards against prod robots with prod modules.
@@ -42,6 +42,8 @@ export { default as GripperIsMovingWidget } from './widgets/gripper/is-moving-wi
42
42
  export { default as GripperOpenWidget } from './widgets/gripper/open.svelte';
43
43
  export { default as InputControllerWidget } from './widgets/input-controller/input-controller.svelte';
44
44
  export { default as MLModelServiceWidget } from './widgets/ml-model-service/ml-model-service.svelte';
45
+ export { default as MotionServiceWidget } from './widgets/motion/motion.svelte';
46
+ export { default as MotionMoveWidget } from './widgets/motion/move-widget.svelte';
45
47
  export { default as MotorGoForWidget } from './widgets/motor/go-for-view.svelte';
46
48
  export { default as MotorGoToWidget } from './widgets/motor/go-to-view.svelte';
47
49
  export { default as MotorWidget } from './widgets/motor/motor.svelte';
@@ -41,6 +41,8 @@ export { default as GripperIsMovingWidget } from './widgets/gripper/is-moving-wi
41
41
  export { default as GripperOpenWidget } from './widgets/gripper/open.svelte';
42
42
  export { default as InputControllerWidget } from './widgets/input-controller/input-controller.svelte';
43
43
  export { default as MLModelServiceWidget } from './widgets/ml-model-service/ml-model-service.svelte';
44
+ export { default as MotionServiceWidget } from './widgets/motion/motion.svelte';
45
+ export { default as MotionMoveWidget } from './widgets/motion/move-widget.svelte';
44
46
  export { default as MotorGoForWidget } from './widgets/motor/go-for-view.svelte';
45
47
  export { default as MotorGoToWidget } from './widgets/motor/go-to-view.svelte';
46
48
  export { default as MotorWidget } from './widgets/motor/motor.svelte';
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { T } from '@threlte/core'
3
- import { AxesHelper, CapsuleGeometry } from '@viamrobotics/motion-tools/lib'
4
3
  import { theme } from '@viamrobotics/prime-core/theme'
5
4
  import {
6
5
  LngLat,
@@ -11,6 +10,8 @@
11
10
  import { fromStore } from 'svelte/store'
12
11
  import { type BufferGeometry, Vector2 } from 'three'
13
12
 
13
+ import { AxesHelper, CapsuleGeometry } from '../../three'
14
+
14
15
  import type { Obstacle } from '../types'
15
16
 
16
17
  import { useMapLibre, useMapLibreEvent } from '../../maplibre'
@@ -0,0 +1,10 @@
1
+ import { LatheGeometry } from 'three';
2
+ /**
3
+ * An alternate definition of a THREE.CapsuleGeometry: the length
4
+ * represents the entire length of the capsule, including the rounded ends,
5
+ * rather than just the midsection, which is the default THREE.CapsuleGeometry definition.
6
+ */
7
+ export declare class CapsuleGeometry extends LatheGeometry {
8
+ type: string;
9
+ constructor(r?: number, l?: number, capSegments?: number, radialSegments?: number);
10
+ }
@@ -0,0 +1,19 @@
1
+ import { LatheGeometry, Path } from 'three';
2
+ /**
3
+ * An alternate definition of a THREE.CapsuleGeometry: the length
4
+ * represents the entire length of the capsule, including the rounded ends,
5
+ * rather than just the midsection, which is the default THREE.CapsuleGeometry definition.
6
+ */
7
+ export class CapsuleGeometry extends LatheGeometry {
8
+ type = 'CapsuleGeometry';
9
+ constructor(r = 1, l = 1, capSegments = 4, radialSegments = 8) {
10
+ const radius = Math.max(0.0001, r);
11
+ const length = Math.max(0.0001, l);
12
+ const path = new Path();
13
+ const midsectionLength = length - 2 * radius;
14
+ path.absarc(0, -midsectionLength / 2, radius, Math.PI * 1.5, 0);
15
+ path.absarc(0, midsectionLength / 2, radius, 0, Math.PI * 0.5);
16
+ super(path.getPoints(capSegments), radialSegments);
17
+ this.rotateX(-Math.PI / 2);
18
+ }
19
+ }
@@ -1 +1,2 @@
1
1
  export { default as AxesHelper } from './axes-helper.svelte';
2
+ export { CapsuleGeometry } from './capsule-geometry.ts';
@@ -1 +1,2 @@
1
1
  export { default as AxesHelper } from './axes-helper.svelte';
2
+ export { CapsuleGeometry } from "./capsule-geometry.js";
@@ -19,9 +19,16 @@
19
19
  resource: ResourceName
20
20
  /** Rendered above the input/output editor row. */
21
21
  header?: Snippet<[{ input: string; setInput: (value: string) => void }]>
22
+ /**
23
+ * Optional bindable editor value. Use `bind:input` (including function
24
+ * bindings) to control it from a parent. When unbound, falls back to the
25
+ * widget's persisted local state — assignments still update locally so the
26
+ * editor cannot freeze.
27
+ */
28
+ input?: string
22
29
  }
23
30
 
24
- const { partID, resource, header }: Props = $props()
31
+ let { partID, resource, header, input = $bindable() }: Props = $props()
25
32
 
26
33
  const client = createDoCommandClient(
27
34
  () => resource,
@@ -37,19 +44,26 @@
37
44
 
38
45
  const uid = $props.id()
39
46
 
40
- const input = $derived(new PersistedState(`${partID}/${getResourceKey(resource)}`, '{\n}'))
47
+ const persisted = $derived(new PersistedState(`${partID}/${getResourceKey(resource)}`, '{\n}'))
48
+
49
+ const displayInput = $derived(input ?? persisted.current ?? '{\n}')
41
50
 
42
51
  let output = $state('')
43
52
 
53
+ const updateInput = (value: string) => {
54
+ input = value
55
+ persisted.current = value
56
+ }
57
+
44
58
  const setInput = (value: string) => {
45
- input.current = value
59
+ updateInput(value)
46
60
  }
47
61
 
48
62
  const execute = async () => {
49
63
  try {
50
64
  lastErr = null
51
65
  output = ''
52
- const parsedInput = Struct.fromJsonString(input.current ?? '{}')
66
+ const parsedInput = Struct.fromJsonString(displayInput)
53
67
  const data = await doCommandMutation.mutateAsync([parsedInput])
54
68
  output = JSON.stringify(data, null, 2)
55
69
  lastErr = null
@@ -62,7 +76,7 @@
62
76
  {#if isSupported}
63
77
  {#if header}
64
78
  <div class="border-b">
65
- {@render header({ input: input.current ?? '{}', setInput })}
79
+ {@render header({ input: displayInput, setInput })}
66
80
  </div>
67
81
  {/if}
68
82
  <div class="flex flex-row items-center justify-between">
@@ -71,9 +85,9 @@
71
85
  <CodeEditor
72
86
  label="input"
73
87
  language="json"
74
- value={input.current ?? '{}'}
88
+ value={displayInput}
75
89
  onChange={(nextInput: string) => {
76
- input.current = nextInput
90
+ updateInput(nextInput)
77
91
  }}
78
92
  class="h-56 overflow-y-auto"
79
93
  errorMessageID={lastErr ? uid : undefined}
@@ -8,7 +8,14 @@ interface Props {
8
8
  input: string;
9
9
  setInput: (value: string) => void;
10
10
  }]>;
11
+ /**
12
+ * Optional bindable editor value. Use `bind:input` (including function
13
+ * bindings) to control it from a parent. When unbound, falls back to the
14
+ * widget's persisted local state — assignments still update locally so the
15
+ * editor cannot freeze.
16
+ */
17
+ input?: string;
11
18
  }
12
- declare const DoCommand: import("svelte").Component<Props, {}, "">;
19
+ declare const DoCommand: import("svelte").Component<Props, {}, "input">;
13
20
  type DoCommand = ReturnType<typeof DoCommand>;
14
21
  export default DoCommand;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import { Label, Select } from '@viamrobotics/prime-core'
3
+
4
+ interface Props {
5
+ value: string
6
+ options: string[]
7
+ label: string
8
+ onChange: (value: string) => void
9
+ }
10
+
11
+ const { value, options, label, onChange }: Props = $props()
12
+ </script>
13
+
14
+ <Label>
15
+ {label}
16
+
17
+ <Select
18
+ slot="input"
19
+ {value}
20
+ on:change={(event) => {
21
+ onChange((event.target as HTMLSelectElement).value)
22
+ }}
23
+ >
24
+ <option value="">Select a frame…</option>
25
+ {#each options as name (name)}
26
+ <option value={name}>{name}</option>
27
+ {/each}
28
+ </Select>
29
+ </Label>
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ value: string;
3
+ options: string[];
4
+ label: string;
5
+ onChange: (value: string) => void;
6
+ }
7
+ declare const FrameSelect: import("svelte").Component<Props, {}, "">;
8
+ type FrameSelect = ReturnType<typeof FrameSelect>;
9
+ export default FrameSelect;
@@ -0,0 +1,37 @@
1
+ /** The subset of a `FrameSystemConfig` entry these helpers read. */
2
+ export interface FrameConfigEntry {
3
+ frame?: {
4
+ referenceFrame?: string;
5
+ poseInObserverFrame?: {
6
+ referenceFrame?: string;
7
+ };
8
+ };
9
+ }
10
+ /**
11
+ * The components the motion service can `Move` are exactly the frames in the
12
+ * machine's frame system config. `Move` builds a kinematic chain to a frame, so
13
+ * anything with a configured frame is a valid target.
14
+ *
15
+ * @param config - The machine's frame system config, or `undefined` while loading.
16
+ * @returns The non-empty reference frame names, sorted alphabetically.
17
+ */
18
+ export declare const movableFrameNames: (config: FrameConfigEntry[] | undefined) => string[];
19
+ /**
20
+ * The parent frame a frame is attached to in the machine's frame system. A
21
+ * frame's parent is the reference frame of the pose that positions it (its
22
+ * observer frame), so `getPose` and `Move` can be expressed relative to it.
23
+ *
24
+ * @param config - The machine's frame system config, or `undefined` while loading.
25
+ * @param frameName - The frame whose parent to look up.
26
+ * @returns The parent frame name, or `'world'` when there is no configured parent.
27
+ */
28
+ export declare const parentFrame: (config: FrameConfigEntry[] | undefined, frameName: string) => string;
29
+ /**
30
+ * The reference frames a destination pose can be expressed in: the root
31
+ * `'world'` frame first, then every frame in the machine's frame system
32
+ * (already sorted alphabetically by `movableFrameNames`).
33
+ *
34
+ * @param config - The machine's frame system config, or `undefined` while loading.
35
+ * @returns The selectable reference frame names, always led by `'world'`.
36
+ */
37
+ export declare const referenceFrameNames: (config: FrameConfigEntry[] | undefined) => string[];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The components the motion service can `Move` are exactly the frames in the
3
+ * machine's frame system config. `Move` builds a kinematic chain to a frame, so
4
+ * anything with a configured frame is a valid target.
5
+ *
6
+ * @param config - The machine's frame system config, or `undefined` while loading.
7
+ * @returns The non-empty reference frame names, sorted alphabetically.
8
+ */
9
+ export const movableFrameNames = (config) => {
10
+ if (!config) {
11
+ return [];
12
+ }
13
+ return config
14
+ .map((entry) => entry.frame?.referenceFrame ?? '')
15
+ .filter((name) => name !== '')
16
+ .toSorted((a, b) => a.localeCompare(b));
17
+ };
18
+ /**
19
+ * The parent frame a frame is attached to in the machine's frame system. A
20
+ * frame's parent is the reference frame of the pose that positions it (its
21
+ * observer frame), so `getPose` and `Move` can be expressed relative to it.
22
+ *
23
+ * @param config - The machine's frame system config, or `undefined` while loading.
24
+ * @param frameName - The frame whose parent to look up.
25
+ * @returns The parent frame name, or `'world'` when there is no configured parent.
26
+ */
27
+ export const parentFrame = (config, frameName) => {
28
+ const entry = config?.find((item) => item.frame?.referenceFrame === frameName);
29
+ return entry?.frame?.poseInObserverFrame?.referenceFrame || 'world';
30
+ };
31
+ /**
32
+ * The reference frames a destination pose can be expressed in: the root
33
+ * `'world'` frame first, then every frame in the machine's frame system
34
+ * (already sorted alphabetically by `movableFrameNames`).
35
+ *
36
+ * @param config - The machine's frame system config, or `undefined` while loading.
37
+ * @returns The selectable reference frame names, always led by `'world'`.
38
+ */
39
+ export const referenceFrameNames = (config) => {
40
+ const frames = movableFrameNames(config).filter((name) => name !== 'world');
41
+ return ['world', ...frames];
42
+ };
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { createRobotQuery, useRobotClient } from '@viamrobotics/svelte-sdk'
3
+
4
+ import ApiSection from '../../api-section.svelte'
5
+ import ConnectionStatus from '../../connection-status.svelte'
6
+
7
+ import FrameSelect from './frame-select.svelte'
8
+ import { movableFrameNames, parentFrame, referenceFrameNames } from './frame-system-config'
9
+ import MoveWidget from './move-widget.svelte'
10
+
11
+ interface Props {
12
+ partID: string
13
+ resourceName: string
14
+ }
15
+
16
+ const { partID, resourceName }: Props = $props()
17
+
18
+ const robotClient = useRobotClient(() => partID)
19
+ const frameSystem = createRobotQuery(robotClient, 'frameSystemConfig', () => ({
20
+ refetchInterval: 5000,
21
+ }))
22
+
23
+ const frameNames = $derived(movableFrameNames(frameSystem.data))
24
+ const destinationOptions = $derived(referenceFrameNames(frameSystem.data))
25
+
26
+ let selectedFrame = $state<string>()
27
+ const frameName = $derived(selectedFrame ?? frameNames[0] ?? '')
28
+
29
+ let selectedDestination = $state<string>()
30
+ const destination = $derived(selectedDestination ?? parentFrame(frameSystem.data, frameName))
31
+ </script>
32
+
33
+ <ConnectionStatus {partID}>
34
+ {#snippet connected()}
35
+ <ApiSection
36
+ title="Move"
37
+ api="rdk:service:motion"
38
+ >
39
+ <div class="flex min-w-0 flex-col gap-4">
40
+ <FrameSelect
41
+ label="Component"
42
+ value={frameName}
43
+ options={frameNames}
44
+ onChange={(value) => {
45
+ selectedFrame = value
46
+ }}
47
+ />
48
+ <FrameSelect
49
+ label="Destination frame"
50
+ value={destination}
51
+ options={destinationOptions}
52
+ onChange={(value) => {
53
+ selectedDestination = value
54
+ }}
55
+ />
56
+ <MoveWidget
57
+ {partID}
58
+ {resourceName}
59
+ {frameName}
60
+ {destination}
61
+ />
62
+ </div>
63
+ </ApiSection>
64
+ {/snippet}
65
+ </ConnectionStatus>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ partID: string;
3
+ resourceName: string;
4
+ }
5
+ declare const Motion: import("svelte").Component<Props, {}, "">;
6
+ type Motion = ReturnType<typeof Motion>;
7
+ export default Motion;
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { MotionClient, type RobotClient } from '@viamrobotics/sdk'
3
+ import {
4
+ createResourceClient,
5
+ createResourceMutation,
6
+ createRobotQuery,
7
+ useRobotClient,
8
+ } from '@viamrobotics/svelte-sdk'
9
+
10
+ import Move from './move.svelte'
11
+ import { type MoveInput, parseMoveArgs } from './parse-move-args'
12
+
13
+ interface Props {
14
+ partID: string
15
+ resourceName: string
16
+ /** The frame to move — a frame from the machine's frame system. */
17
+ frameName: string
18
+ /** The reference frame the destination pose is expressed in. */
19
+ destination: string
20
+ }
21
+
22
+ const { partID, resourceName, frameName, destination }: Props = $props()
23
+
24
+ const robotClient = useRobotClient(() => partID)
25
+ const client = createResourceClient(
26
+ MotionClient,
27
+ () => partID,
28
+ () => resourceName
29
+ )
30
+
31
+ const move = createResourceMutation(client, 'move')
32
+
33
+ // Pre-fill the editor with the frame's current pose in the destination frame.
34
+ const poseArgs = $derived<Parameters<RobotClient['getPose']>>([frameName, destination, []])
35
+ const poseQuery = createRobotQuery(
36
+ robotClient,
37
+ 'getPose',
38
+ () => poseArgs,
39
+ () => ({ enabled: frameName !== '' })
40
+ )
41
+
42
+ const currentPose = $derived(poseQuery.data?.pose)
43
+ const poseError = $derived(poseQuery.error instanceof Error ? poseQuery.error : null)
44
+
45
+ let parseError = $state<Error>()
46
+
47
+ const executeMove = (input: MoveInput) => {
48
+ try {
49
+ parseError = undefined
50
+ move.mutate(parseMoveArgs(frameName, input), {})
51
+ } catch (error) {
52
+ parseError = error instanceof Error ? error : new Error(String(error))
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <Move
58
+ {frameName}
59
+ {destination}
60
+ {currentPose}
61
+ isPending={move.isPending}
62
+ lastError={parseError ?? move.error ?? poseError}
63
+ storageKey={`${partID}/${resourceName}/motion-move`}
64
+ onExecute={executeMove}
65
+ />
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ partID: string;
3
+ resourceName: string;
4
+ /** The frame to move — a frame from the machine's frame system. */
5
+ frameName: string;
6
+ /** The reference frame the destination pose is expressed in. */
7
+ destination: string;
8
+ }
9
+ declare const MoveWidget: import("svelte").Component<Props, {}, "">;
10
+ type MoveWidget = ReturnType<typeof MoveWidget>;
11
+ export default MoveWidget;
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import type { Pose } from '@viamrobotics/sdk'
3
+
4
+ import { Button, Progress } from '@viamrobotics/prime-core'
5
+ import { CodeEditor } from '@viamrobotics/prime-core/code-editor'
6
+ import { PersistedState } from 'runed'
7
+
8
+ import ErrorDisplay from '../../error.svelte'
9
+
10
+ import type { MoveInput } from './parse-move-args'
11
+
12
+ import PoseInput from './pose-input.svelte'
13
+
14
+ interface Props {
15
+ /** The frame being moved. Gates execution and re-seeds the editor on change. */
16
+ frameName: string
17
+ /** The reference frame the destination pose is expressed in. */
18
+ destination: string
19
+ currentPose?: Pose
20
+ isPending: boolean
21
+ lastError: Error | null
22
+ storageKey: string
23
+ onExecute: (input: MoveInput) => void
24
+ }
25
+
26
+ const {
27
+ frameName,
28
+ destination,
29
+ currentPose,
30
+ isPending,
31
+ lastError,
32
+ storageKey,
33
+ onExecute,
34
+ }: Props = $props()
35
+
36
+ const zeroPose: Pose = { x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 }
37
+
38
+ const editKey = $derived(JSON.stringify([frameName, destination]))
39
+ let edit = $state<{ key: string; pose: Pose }>()
40
+ const isEdited = $derived(edit?.key === editKey)
41
+ const pose = $derived(isEdited && edit ? edit.pose : (currentPose ?? zeroPose))
42
+
43
+ const worldState = $derived(new PersistedState(`${storageKey}/world-state`, ''))
44
+ const constraints = $derived(new PersistedState(`${storageKey}/constraints`, ''))
45
+
46
+ const disabled = $derived(frameName === '' || isPending)
47
+
48
+ const execute = () => {
49
+ onExecute({
50
+ referenceFrame: destination,
51
+ pose,
52
+ worldStateJson: worldState.current,
53
+ constraintsJson: constraints.current,
54
+ })
55
+ }
56
+ </script>
57
+
58
+ <div class="flex min-w-0 flex-col gap-4">
59
+ <PoseInput
60
+ {pose}
61
+ onPoseChange={(next) => {
62
+ edit = { key: editKey, pose: next }
63
+ }}
64
+ />
65
+
66
+ <div class="flex flex-col gap-1">
67
+ <span class="text-xs font-medium">
68
+ World state <abbr class="text-subtle-2">(optional JSON)</abbr>
69
+ </span>
70
+ <CodeEditor
71
+ label="World state"
72
+ language="json"
73
+ value={worldState.current}
74
+ onChange={(next: string) => {
75
+ worldState.current = next
76
+ }}
77
+ class="h-32 overflow-y-auto"
78
+ />
79
+ </div>
80
+
81
+ <div class="flex flex-col gap-1">
82
+ <span class="text-xs font-medium">
83
+ Constraints <abbr class="text-subtle-2">(optional JSON)</abbr>
84
+ </span>
85
+ <CodeEditor
86
+ label="Constraints"
87
+ language="json"
88
+ value={constraints.current}
89
+ onChange={(next: string) => {
90
+ constraints.current = next
91
+ }}
92
+ class="h-32 overflow-y-auto"
93
+ />
94
+ </div>
95
+
96
+ <div class="flex items-center gap-2">
97
+ <Button
98
+ class="w-fit"
99
+ icon="play-circle-outline"
100
+ variant="dark"
101
+ {disabled}
102
+ onclick={execute}
103
+ >
104
+ Execute
105
+ </Button>
106
+ {#if isPending}
107
+ <Progress
108
+ size="medium"
109
+ variant="dark"
110
+ />
111
+ {/if}
112
+ </div>
113
+
114
+ <p class="text-subtle-2 text-xs">
115
+ Move blocks until the motion completes. Stop the component itself to interrupt.
116
+ </p>
117
+
118
+ <ErrorDisplay {lastError} />
119
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { Pose } from '@viamrobotics/sdk';
2
+ import type { MoveInput } from './parse-move-args';
3
+ interface Props {
4
+ /** The frame being moved. Gates execution and re-seeds the editor on change. */
5
+ frameName: string;
6
+ /** The reference frame the destination pose is expressed in. */
7
+ destination: string;
8
+ currentPose?: Pose;
9
+ isPending: boolean;
10
+ lastError: Error | null;
11
+ storageKey: string;
12
+ onExecute: (input: MoveInput) => void;
13
+ }
14
+ declare const Move: import("svelte").Component<Props, {}, "">;
15
+ type Move = ReturnType<typeof Move>;
16
+ export default Move;
@@ -0,0 +1,24 @@
1
+ import { Constraints, type Pose, type PoseInFrame, WorldState } from '@viamrobotics/sdk';
2
+ /** User-entered inputs for a motion `Move` call. Pose is always mm + degrees. */
3
+ export interface MoveInput {
4
+ referenceFrame: string;
5
+ pose: Pose;
6
+ /** Optional `WorldState` as proto JSON. Empty/whitespace means "omit". */
7
+ worldStateJson: string;
8
+ /** Optional `Constraints` as proto JSON. Empty/whitespace means "omit". */
9
+ constraintsJson: string;
10
+ }
11
+ /** Positional arguments for `MotionClient.move`, ready to spread into `mutate`. */
12
+ export type MoveArgs = [PoseInFrame, string, WorldState | undefined, Constraints | undefined];
13
+ /**
14
+ * Builds the positional arguments for `MotionClient.move` from form inputs.
15
+ *
16
+ * Empty or whitespace-only JSON fields are omitted (passed as `undefined`).
17
+ * Non-empty JSON is parsed with the generated message classes and throws on
18
+ * invalid input — callers should catch and surface the error.
19
+ *
20
+ * @param frameName - The frame to move.
21
+ * @param input - The pose, reference frame, and optional world-state/constraints JSON.
22
+ * @returns The `[destination, frameName, worldState?, constraints?]` tuple.
23
+ */
24
+ export declare const parseMoveArgs: (frameName: string, input: MoveInput) => MoveArgs;
@@ -0,0 +1,23 @@
1
+ import { Constraints, WorldState } from '@viamrobotics/sdk';
2
+ /**
3
+ * Builds the positional arguments for `MotionClient.move` from form inputs.
4
+ *
5
+ * Empty or whitespace-only JSON fields are omitted (passed as `undefined`).
6
+ * Non-empty JSON is parsed with the generated message classes and throws on
7
+ * invalid input — callers should catch and surface the error.
8
+ *
9
+ * @param frameName - The frame to move.
10
+ * @param input - The pose, reference frame, and optional world-state/constraints JSON.
11
+ * @returns The `[destination, frameName, worldState?, constraints?]` tuple.
12
+ */
13
+ export const parseMoveArgs = (frameName, input) => {
14
+ const destination = {
15
+ referenceFrame: input.referenceFrame,
16
+ pose: input.pose,
17
+ };
18
+ const worldState = input.worldStateJson.trim() === '' ? undefined : WorldState.fromJsonString(input.worldStateJson);
19
+ const constraints = input.constraintsJson.trim() === ''
20
+ ? undefined
21
+ : Constraints.fromJsonString(input.constraintsJson);
22
+ return [destination, frameName, worldState, constraints];
23
+ };
@@ -0,0 +1,10 @@
1
+ import type { Pose } from '@viamrobotics/sdk';
2
+ /**
3
+ * Parses a clipboard string as a pose. Accepts a JSON object with numeric
4
+ * `x`, `y`, `z`, `oX`, `oY`, `oZ`, and `theta` fields — the same shape the arm
5
+ * `MoveToPosition` widget copies, so poses can be pasted between the two.
6
+ *
7
+ * @param data - The pasted string.
8
+ * @returns The parsed pose, or `undefined` if it is not a valid pose.
9
+ */
10
+ export declare const parsePastedPose: (data: string) => Pose | undefined;
@@ -0,0 +1,30 @@
1
+ const poseKeys = ['x', 'y', 'z', 'oX', 'oY', 'oZ', 'theta'];
2
+ const isPose = (value) => {
3
+ if (typeof value !== 'object' || value === null) {
4
+ return false;
5
+ }
6
+ const record = value;
7
+ return poseKeys.every((key) => typeof record[key] === 'number');
8
+ };
9
+ /**
10
+ * Parses a clipboard string as a pose. Accepts a JSON object with numeric
11
+ * `x`, `y`, `z`, `oX`, `oY`, `oZ`, and `theta` fields — the same shape the arm
12
+ * `MoveToPosition` widget copies, so poses can be pasted between the two.
13
+ *
14
+ * @param data - The pasted string.
15
+ * @returns The parsed pose, or `undefined` if it is not a valid pose.
16
+ */
17
+ export const parsePastedPose = (data) => {
18
+ let parsed;
19
+ try {
20
+ parsed = JSON.parse(data);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ if (!isPose(parsed)) {
26
+ return undefined;
27
+ }
28
+ const { x, y, z, oX, oY, oZ, theta } = parsed;
29
+ return { x, y, z, oX, oY, oZ, theta };
30
+ };
@@ -0,0 +1,154 @@
1
+ <script
2
+ lang="ts"
3
+ module
4
+ >
5
+ import type { Pose } from '@viamrobotics/sdk'
6
+
7
+ const poseLabelsList: [keyof Pose, string][] = [
8
+ ['x', 'X'],
9
+ ['y', 'Y'],
10
+ ['z', 'Z'],
11
+ ['oX', 'OX'],
12
+ ['oY', 'OY'],
13
+ ['oZ', 'OZ'],
14
+ ['theta', 'θ'],
15
+ ]
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import { Button, Icon, NumericInput, Tooltip } from '@viamrobotics/prime-core'
20
+
21
+ import AngleUnitToggle from '../../angle-unit-toggle.svelte'
22
+ import CopyButton from '../../copy-button.svelte'
23
+ import PasteButton from '../../paste-button.svelte'
24
+ import Table from '../../table.svelte'
25
+ import { numberValueFromEvent } from '../../../event-handlers'
26
+ import { degreesToRadians, formatNumeric, radiansToDegrees } from '../../../format'
27
+
28
+ import { parsePastedPose } from './parse-pasted-pose'
29
+
30
+ interface Props {
31
+ pose: Pose
32
+ onPoseChange: (pose: Pose) => void
33
+ }
34
+
35
+ const { pose, onPoseChange }: Props = $props()
36
+
37
+ let useRadians = $state(false)
38
+
39
+ const displayPose = $derived({
40
+ ...pose,
41
+ theta: useRadians ? degreesToRadians(pose.theta) : pose.theta,
42
+ })
43
+
44
+ const copyData = $derived(JSON.stringify(pose))
45
+
46
+ const handlePaste = (data: string): boolean => {
47
+ const parsed = parsePastedPose(data)
48
+ if (!parsed) {
49
+ return false
50
+ }
51
+ onPoseChange(parsed)
52
+ return true
53
+ }
54
+
55
+ const handleValueChange = (key: keyof Pose, inputValue: number) => {
56
+ const nextValue = key === 'theta' && useRadians ? radiansToDegrees(inputValue) : inputValue
57
+ onPoseChange({ ...pose, [key]: nextValue })
58
+ }
59
+
60
+ const zero = () => {
61
+ onPoseChange({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 })
62
+ }
63
+
64
+ const poseUnits = $derived({
65
+ x: 'mm',
66
+ y: 'mm',
67
+ z: 'mm',
68
+ oX: '',
69
+ oY: '',
70
+ oZ: '',
71
+ theta: useRadians ? 'rad' : 'deg',
72
+ })
73
+ </script>
74
+
75
+ <div class="flex min-w-0 flex-col gap-4">
76
+ <div class="flex items-center justify-between">
77
+ <span class="flex flex-row items-center gap-1 text-sm">
78
+ Destination pose
79
+ <Tooltip>
80
+ <Icon
81
+ name="information-outline"
82
+ cx="text-gray-6"
83
+ />
84
+
85
+ <span slot="description">
86
+ The target pose expressed in the selected reference frame. Translations are in
87
+ millimeters.
88
+ </span>
89
+ </Tooltip>
90
+ </span>
91
+ <div class="flex gap-1">
92
+ <AngleUnitToggle
93
+ {useRadians}
94
+ onToggle={() => {
95
+ useRadians = !useRadians
96
+ }}
97
+ />
98
+ <CopyButton data={copyData} />
99
+ <PasteButton onPaste={handlePaste} />
100
+ </div>
101
+ </div>
102
+
103
+ <Table>
104
+ <thead>
105
+ <tr>
106
+ <th>Pose</th>
107
+ <th>Value</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ {#each poseLabelsList as labelList (labelList)}
112
+ {@const [key, label] = labelList}
113
+ {@const value = Number.parseFloat(formatNumeric(displayPose[key]))}
114
+ <tr>
115
+ <th>
116
+ <span class="relative inline-flex justify-center">
117
+ {label}
118
+ <abbr class="text-subtle-2 absolute left-full ml-1">{poseUnits[key]}</abbr>
119
+ </span>
120
+ </th>
121
+ <th>
122
+ <NumericInput
123
+ cx="max-w-[76px]"
124
+ {value}
125
+ on:change={(event) => {
126
+ handleValueChange(key, numberValueFromEvent(event) ?? 0)
127
+ }}
128
+ />
129
+ </th>
130
+ </tr>
131
+ {/each}
132
+ </tbody>
133
+ </Table>
134
+
135
+ <div class="flex flex-col gap-2">
136
+ <span class="flex flex-row gap-2">
137
+ <h4 class="text-xs font-semibold">Quick set</h4>
138
+ <Tooltip>
139
+ <Icon
140
+ name="information-outline"
141
+ cx="text-gray-6"
142
+ />
143
+
144
+ <span slot="description"> Will update the pose values but will not execute </span>
145
+ </Tooltip>
146
+ </span>
147
+ <Button
148
+ class="w-fit"
149
+ onclick={zero}
150
+ >
151
+ Zero
152
+ </Button>
153
+ </div>
154
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { Pose } from '@viamrobotics/sdk';
2
+ interface Props {
3
+ pose: Pose;
4
+ onPoseChange: (pose: Pose) => void;
5
+ }
6
+ declare const PoseInput: import("svelte").Component<Props, {}, "">;
7
+ type PoseInput = ReturnType<typeof PoseInput>;
8
+ export default PoseInput;
@@ -2,10 +2,9 @@
2
2
  import type { GeoGeometry } from '@viamrobotics/sdk'
3
3
 
4
4
  import { T } from '@threlte/core'
5
- import { CapsuleGeometry } from '@viamrobotics/motion-tools/lib'
6
5
  import { type BufferGeometry, MathUtils } from 'three'
7
6
 
8
- import { AxesHelper } from '../../../three'
7
+ import { AxesHelper, CapsuleGeometry } from '../../../three'
9
8
 
10
9
  import { getColor } from './color'
11
10
 
package/dist/index.d.ts CHANGED
@@ -3,8 +3,10 @@ export { apiDocsHref } from './api-docs-href';
3
3
  export { clientForResource } from './client-map';
4
4
  export * from './components';
5
5
  export { getResourceAPI } from './get-resource-api';
6
+ export { isKnownResource,
7
+ /** @deprecated use `isKnownResource` instead. Will be deleted in the next release */
8
+ isKnownResource as hasWidget, } from './is-known-resource';
6
9
  export { providePip, usePip } from './pip/context.svelte';
7
10
  export { ResourceTriplets } from './resource-triplet';
8
- export { apiWidgetsForResource, availableAPIWidgets, isKnownResource,
9
- /** @deprecated use `isKnownResource` instead. Will be deleted in the next release */
10
- isKnownResource as hasWidget, type ResourceAPIWidget, showResourceWidget, widgetForResource, } from './resource-widget';
11
+ export type { ResourceAPIWidget, ResourceWidget, ResourceWidgetProps, } from './resource-widget-types';
12
+ export { showResourceWidget } from './show-resource-widget';
package/dist/index.js CHANGED
@@ -3,9 +3,10 @@ export { apiDocsHref } from './api-docs-href';
3
3
  export { clientForResource } from './client-map';
4
4
  export * from './components';
5
5
  export { getResourceAPI } from './get-resource-api';
6
- export { providePip, usePip } from './pip/context.svelte';
7
- export { ResourceTriplets } from './resource-triplet';
8
- export { apiWidgetsForResource, availableAPIWidgets, isKnownResource,
6
+ export { isKnownResource,
9
7
  // TODO: Delete `hasWidget`
10
8
  /** @deprecated use `isKnownResource` instead. Will be deleted in the next release */
11
- isKnownResource as hasWidget, showResourceWidget, widgetForResource, } from './resource-widget';
9
+ isKnownResource as hasWidget, } from './is-known-resource';
10
+ export { providePip, usePip } from './pip/context.svelte';
11
+ export { ResourceTriplets } from './resource-triplet';
12
+ export { showResourceWidget } from './show-resource-widget';
@@ -0,0 +1,3 @@
1
+ import type { ResourceName } from '@viamrobotics/sdk';
2
+ /** Whether a resource's API is a recognized Viam resource triplet. */
3
+ export declare const isKnownResource: (resource: ResourceName) => boolean;
@@ -0,0 +1,5 @@
1
+ import { getResourceAPI } from "./get-resource-api.js";
2
+ import { ResourceTriplets } from "./resource-triplet.js";
3
+ const knownResources = new Set(Object.values(ResourceTriplets));
4
+ /** Whether a resource's API is a recognized Viam resource triplet. */
5
+ export const isKnownResource = (resource) => knownResources.has(getResourceAPI(resource));
@@ -0,0 +1,28 @@
1
+ import type { ResourceName } from '@viamrobotics/sdk';
2
+ import type { ResourceAPIWidget, ResourceWidget } from './resource-widget-types.ts';
3
+ import { type ResourceTriplet } from './resource-triplet.ts';
4
+ /**
5
+ * Returns a resource's individual API widgets. Each entry carries a stable `id`, a
6
+ * display `label`, and the `widgets` to render with `{ partID, resourceName }`.
7
+ *
8
+ * Returns `[]` for a resource with a card but no standalone API widgets, and for
9
+ * unrecognized resources.
10
+ *
11
+ * @example
12
+ * apiWidgetsForResource(gripperResourceName)
13
+ * // [{ id: 'open-grab', label: 'Open / Grab', widgets: [GripperOpenWidget, GripperGrabWidget] }, ...]
14
+ */
15
+ export declare const apiWidgetsForResource: (resource: ResourceName) => ResourceAPIWidget[];
16
+ /**
17
+ * Returns every resource triplet that has a test card, mapped to its API widgets.
18
+ * Use this to enumerate the full catalog, e.g. a menu spanning every resource type;
19
+ * for a single resource, prefer `apiWidgetsForResource`.
20
+ *
21
+ * @example
22
+ * availableAPIWidgets()[ResourceTriplets.Gripper]
23
+ * // [{ id: 'open-grab', label: 'Open / Grab', widgets: [GripperOpenWidget, GripperGrabWidget] }, ...]
24
+ */
25
+ export declare const availableAPIWidgets: () => Partial<Record<ResourceTriplet, ResourceAPIWidget[]>>;
26
+ /** Returns the full composite test card for a resource, or `undefined` if none exists. */
27
+ export declare const widgetForResource: (resource: ResourceName) => ResourceWidget | undefined;
28
+ export type { ResourceAPIWidget, ResourceWidget, ResourceWidgetProps, } from './resource-widget-types.ts';
@@ -1,6 +1,11 @@
1
- import { ArmGetJointPositionsWidget, ArmIsMovingWidget, ArmMoveToJointPositionsWidget, ArmMoveToPositionWidget, ArmQuickMoveWidget, ArmWidget, AudioInputGetPropertiesWidget, AudioInputWidget, AudioOutputGetPropertiesWidget, AudioOutputWidget, BaseIsMovingWidget, BaseMoveStraightWidget, BaseQuickMoveWidget, BaseSetPowerWidget, BaseSetVelocityWidget, BaseSpinWidget, BaseWidget, BoardWidget, ButtonWidget, CameraWidget, DiscoveryWidget, EncoderGetPositionWidget, EncoderWidget, GantryGetPositionWidget, GantryHomeWidget, GantryIsMovingWidget, GantryMoveToPositionWidget, GantryQuickMoveWidget, GantryWidget, GripperGrabWidget, GripperIsHoldingSomethingWidget, GripperIsMovingWidget, GripperOpenWidget, GripperWidget, InputControllerWidget, MLModelServiceWidget, MotorGoForWidget, MotorGoToWidget, MotorIsMovingWidget, MotorQuickMoveWidget, MotorSetPowerWidget, MotorSetRPMWidget, MotorWidget, MovementSensorGetAccuracyWidget, MovementSensorGetCompassHeadingWidget, MovementSensorGetOrientationWidget, MovementSensorGetPositionWidget, MovementSensorWidget, NavigationServiceWidget, PowerSensorGetCurrentWidget, PowerSensorGetPowerWidget, PowerSensorGetVoltageWidget, PowerSensorWidget, SensorWidget, ServoIsMovingWidget, ServoMoveWidget, ServoQuickMoveWidget, ServoWidget, SlamGetPositionWidget, SlamWidget, SwitchWidget, VisionServiceWidget, } from "./components/index.js";
1
+ import { ArmGetJointPositionsWidget, ArmIsMovingWidget, ArmMoveToJointPositionsWidget, ArmMoveToPositionWidget, ArmQuickMoveWidget, ArmWidget, AudioInputGetPropertiesWidget, AudioInputWidget, AudioOutputGetPropertiesWidget, AudioOutputWidget, BaseIsMovingWidget, BaseMoveStraightWidget, BaseQuickMoveWidget, BaseSetPowerWidget, BaseSetVelocityWidget, BaseSpinWidget, BaseWidget, BoardWidget, ButtonWidget, CameraWidget, DiscoveryWidget, EncoderGetPositionWidget, EncoderWidget, GantryGetPositionWidget, GantryHomeWidget, GantryIsMovingWidget, GantryMoveToPositionWidget, GantryQuickMoveWidget, GantryWidget, GripperGrabWidget, GripperIsHoldingSomethingWidget, GripperIsMovingWidget, GripperOpenWidget, GripperWidget, InputControllerWidget, MLModelServiceWidget, MotionServiceWidget, MotorGoForWidget, MotorGoToWidget, MotorIsMovingWidget, MotorQuickMoveWidget, MotorSetPowerWidget, MotorSetRPMWidget, MotorWidget, MovementSensorGetAccuracyWidget, MovementSensorGetCompassHeadingWidget, MovementSensorGetOrientationWidget, MovementSensorGetPositionWidget, MovementSensorWidget, NavigationServiceWidget, PowerSensorGetCurrentWidget, PowerSensorGetPowerWidget, PowerSensorGetVoltageWidget, PowerSensorWidget, SensorWidget, ServoIsMovingWidget, ServoMoveWidget, ServoQuickMoveWidget, ServoWidget, SlamGetPositionWidget, SlamWidget, SwitchWidget, VisionServiceWidget, } from "./components/index.js";
2
2
  import { getResourceAPI } from "./get-resource-api.js";
3
3
  import { ResourceTriplets } from "./resource-triplet.js";
4
+ /**
5
+ * Maps a resource's API triplet to its composite test card and individual API widgets.
6
+ * Referencing this pulls every widget (and its optional peers) into the build, which is
7
+ * why the lookups below live behind the `/registry` entry point rather than the root.
8
+ */
4
9
  const resourceWidgetRegistry = {
5
10
  // components
6
11
  [ResourceTriplets.Arm]: {
@@ -127,6 +132,7 @@ const resourceWidgetRegistry = {
127
132
  // services
128
133
  [ResourceTriplets.Discovery]: { widget: DiscoveryWidget, apis: [] },
129
134
  [ResourceTriplets.MLModel]: { widget: MLModelServiceWidget, apis: [] },
135
+ [ResourceTriplets.Motion]: { widget: MotionServiceWidget, apis: [] },
130
136
  [ResourceTriplets.Navigation]: { widget: NavigationServiceWidget, apis: [] },
131
137
  [ResourceTriplets.Slam]: {
132
138
  widget: SlamWidget,
@@ -145,12 +151,7 @@ const resourceWidgetRegistry = {
145
151
  * apiWidgetsForResource(gripperResourceName)
146
152
  * // [{ id: 'open-grab', label: 'Open / Grab', widgets: [GripperOpenWidget, GripperGrabWidget] }, ...]
147
153
  */
148
- export const apiWidgetsForResource = (resource) => {
149
- const api = getResourceAPI(resource);
150
- return api in resourceWidgetRegistry
151
- ? resourceWidgetRegistry[api].apis
152
- : [];
153
- };
154
+ export const apiWidgetsForResource = (resource) => resourceWidgetRegistry[getResourceAPI(resource)]?.apis ?? [];
154
155
  /**
155
156
  * Returns every resource triplet that has a test card, mapped to its API widgets.
156
157
  * Use this to enumerate the full catalog, e.g. a menu spanning every resource type;
@@ -162,26 +163,11 @@ export const apiWidgetsForResource = (resource) => {
162
163
  */
163
164
  export const availableAPIWidgets = () => {
164
165
  const result = {};
165
- for (const triplet of Object.keys(resourceWidgetRegistry)) {
166
- result[triplet] = resourceWidgetRegistry[triplet].apis;
166
+ for (const [api, entry] of Object.entries(resourceWidgetRegistry)) {
167
+ if (entry)
168
+ result[api] = entry.apis;
167
169
  }
168
170
  return result;
169
171
  };
170
172
  /** Returns the full composite test card for a resource, or `undefined` if none exists. */
171
- export const widgetForResource = (resource) => {
172
- const api = getResourceAPI(resource);
173
- return api in resourceWidgetRegistry
174
- ? resourceWidgetRegistry[api].widget
175
- : undefined;
176
- };
177
- const knownResources = new Set(Object.values(ResourceTriplets));
178
- /** Whether a resource's API is a recognized Viam resource triplet. */
179
- export const isKnownResource = (resource) => knownResources.has(getResourceAPI(resource));
180
- const hiddenResources = new Set([
181
- ResourceTriplets.DataManager,
182
- ResourceTriplets.Motion,
183
- ResourceTriplets.Sensors,
184
- ResourceTriplets.Shell,
185
- ]);
186
- /** Whether the control view should surface a card for this resource. */
187
- export const showResourceWidget = (resource) => resource.namespace !== 'rdk-internal' && !hiddenResources.has(getResourceAPI(resource));
173
+ export const widgetForResource = (resource) => resourceWidgetRegistry[getResourceAPI(resource)]?.widget;
@@ -0,0 +1,21 @@
1
+ import type { Component } from 'svelte';
2
+ /** Every resource widget shares this prop contract and is self-contained. */
3
+ export interface ResourceWidgetProps {
4
+ partID: string;
5
+ resourceName: string;
6
+ }
7
+ export type ResourceWidget = Component<ResourceWidgetProps>;
8
+ /** One of a resource's individual API widgets (e.g. a menu entry); renders one or more self-contained widgets. */
9
+ export interface ResourceAPIWidget {
10
+ /** Stable identifier, safe to persist. Never rename. e.g. `'move-to-joint-positions'`. */
11
+ id: string;
12
+ /** Human-readable menu label. e.g. `'MoveToJointPositions'` or `'Quick move'`. */
13
+ label: string;
14
+ /** The self-contained widget(s) this entry renders, each with `{ partID, resourceName }`. */
15
+ widgets: ResourceWidget[];
16
+ }
17
+ /** A registry entry: a resource's composite card plus its individual API widgets. */
18
+ export interface ResourceWidgetEntry {
19
+ widget: ResourceWidget;
20
+ apis: ResourceAPIWidget[];
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { ResourceName } from '@viamrobotics/sdk';
2
+ /** Whether the control view should surface a card for this resource. */
3
+ export declare const showResourceWidget: (resource: ResourceName) => boolean;
@@ -0,0 +1,10 @@
1
+ import { getResourceAPI } from "./get-resource-api.js";
2
+ import { ResourceTriplets } from "./resource-triplet.js";
3
+ const hiddenResources = new Set([
4
+ ResourceTriplets.DataManager,
5
+ ResourceTriplets.Motion,
6
+ ResourceTriplets.Sensors,
7
+ ResourceTriplets.Shell,
8
+ ]);
9
+ /** Whether the control view should surface a card for this resource. */
10
+ export const showResourceWidget = (resource) => resource.namespace !== 'rdk-internal' && !hiddenResources.has(getResourceAPI(resource));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/test-widgets",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": [
@@ -17,6 +17,10 @@
17
17
  ".": {
18
18
  "types": "./dist/index.d.ts",
19
19
  "svelte": "./dist/index.js"
20
+ },
21
+ "./registry": {
22
+ "types": "./dist/registry.d.ts",
23
+ "svelte": "./dist/registry.js"
20
24
  }
21
25
  },
22
26
  "publishConfig": {
@@ -33,13 +37,26 @@
33
37
  "@viamrobotics/prime-core": ">=0.1",
34
38
  "@viamrobotics/sdk": ">=0.68",
35
39
  "@viamrobotics/svelte-sdk": ">=1.1",
40
+ "@viamrobotics/three": ">=0.0.9",
36
41
  "lodash-es": ">=4",
42
+ "maplibre-gl": ">=5",
37
43
  "runed": ">=0.28",
38
44
  "svelte": ">=5",
39
45
  "three": ">=0.178",
40
46
  "threlte-uikit": ">=2"
41
47
  },
48
+ "peerDependenciesMeta": {
49
+ "@viamrobotics/three": {
50
+ "optional": true
51
+ },
52
+ "maplibre-gl": {
53
+ "optional": true
54
+ }
55
+ },
42
56
  "devDependencies": {
57
+ "@ag-grid-community/client-side-row-model": "^32.3.9",
58
+ "@ag-grid-community/core": "^32.3.9",
59
+ "@ag-grid-community/styles": "^32.3.9",
43
60
  "@changesets/cli": "^2.31.0",
44
61
  "@eslint/compat": "^2.0.5",
45
62
  "@eslint/js": "^10.0.1",
@@ -66,7 +83,6 @@
66
83
  "@types/lodash-es": "^4.17.12",
67
84
  "@types/node": "^25.6.0",
68
85
  "@types/three": "^0.183.1",
69
- "@viamrobotics/motion-tools": "^1.19.1",
70
86
  "@viamrobotics/prime-core": "^0.1.22",
71
87
  "@viamrobotics/sdk": "^0.69.0",
72
88
  "@viamrobotics/svelte-sdk": "^1.2.1",
@@ -75,6 +91,7 @@
75
91
  "@vitest/browser": "^4.1.8",
76
92
  "@vitest/browser-playwright": "^4.1.5",
77
93
  "@vitest/coverage-v8": "^4.1.5",
94
+ "@zag-js/dialog": "^1.37.0",
78
95
  "eslint": "^10.2.1",
79
96
  "eslint-config-prettier": "^10.1.8",
80
97
  "eslint-plugin-perfectionist": "^5.9.0",
@@ -118,8 +135,8 @@
118
135
  "preview": "vite preview",
119
136
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings",
120
137
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
121
- "lint": "eslint .",
122
- "lint:fix": "eslint . --fix",
138
+ "lint": "eslint . --concurrency auto",
139
+ "lint:fix": "eslint . --fix --concurrency auto",
123
140
  "format": "prettier --check .",
124
141
  "format:fix": "prettier --write .",
125
142
  "test:unit": "vitest",