@viamrobotics/test-widgets 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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/component-name-select.svelte +29 -0
  12. package/dist/components/widgets/motion/component-name-select.svelte.d.ts +9 -0
  13. package/dist/components/widgets/motion/frame-system-config.d.ts +28 -0
  14. package/dist/components/widgets/motion/frame-system-config.js +30 -0
  15. package/dist/components/widgets/motion/motion.svelte +27 -0
  16. package/dist/components/widgets/motion/motion.svelte.d.ts +7 -0
  17. package/dist/components/widgets/motion/move-widget.svelte +84 -0
  18. package/dist/components/widgets/motion/move-widget.svelte.d.ts +7 -0
  19. package/dist/components/widgets/motion/move.svelte +127 -0
  20. package/dist/components/widgets/motion/move.svelte.d.ts +14 -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-in-frame-input.svelte +168 -0
  26. package/dist/components/widgets/motion/pose-in-frame-input.svelte.d.ts +10 -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} +15 -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 = 'Component', 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 component…</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 ComponentNameSelect: import("svelte").Component<Props, {}, "">;
8
+ type ComponentNameSelect = ReturnType<typeof ComponentNameSelect>;
9
+ export default ComponentNameSelect;
@@ -0,0 +1,28 @@
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 component is attached to in the machine's frame system.
21
+ * A 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 componentName - The component (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, componentName: string) => string;
@@ -0,0 +1,30 @@
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 component is attached to in the machine's frame system.
20
+ * A 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 componentName - The component (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, componentName) => {
28
+ const entry = config?.find((item) => item.frame?.referenceFrame === componentName);
29
+ return entry?.frame?.poseInObserverFrame?.referenceFrame || 'world';
30
+ };
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import ApiSection from '../../api-section.svelte'
3
+ import ConnectionStatus from '../../connection-status.svelte'
4
+
5
+ import MoveWidget from './move-widget.svelte'
6
+
7
+ interface Props {
8
+ partID: string
9
+ resourceName: string
10
+ }
11
+
12
+ const { partID, resourceName }: Props = $props()
13
+ </script>
14
+
15
+ <ConnectionStatus {partID}>
16
+ {#snippet connected()}
17
+ <ApiSection
18
+ title="Move"
19
+ api="rdk:service:motion"
20
+ >
21
+ <MoveWidget
22
+ {partID}
23
+ {resourceName}
24
+ />
25
+ </ApiSection>
26
+ {/snippet}
27
+ </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,84 @@
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 ComponentNameSelect from './component-name-select.svelte'
11
+ import { movableFrameNames, parentFrame } from './frame-system-config'
12
+ import Move from './move.svelte'
13
+ import { type MoveInput, parseMoveArgs } from './parse-move-args'
14
+
15
+ interface Props {
16
+ partID: string
17
+ resourceName: string
18
+ }
19
+
20
+ const { partID, resourceName }: Props = $props()
21
+
22
+ const robotClient = useRobotClient(() => partID)
23
+ const client = createResourceClient(
24
+ MotionClient,
25
+ () => partID,
26
+ () => resourceName
27
+ )
28
+
29
+ const move = createResourceMutation(client, 'move')
30
+ const frameSystem = createRobotQuery(robotClient, 'frameSystemConfig', () => ({
31
+ refetchInterval: 5000,
32
+ }))
33
+ const frameNames = $derived(movableFrameNames(frameSystem.data))
34
+
35
+ let selectedName = $state<string>()
36
+ const componentName = $derived(selectedName ?? frameNames[0] ?? '')
37
+
38
+ const destinationFrame = $derived(parentFrame(frameSystem.data, componentName))
39
+ const poseArgs = $derived<Parameters<RobotClient['getPose']>>([
40
+ componentName,
41
+ destinationFrame,
42
+ [],
43
+ ])
44
+
45
+ const poseQuery = createRobotQuery(
46
+ robotClient,
47
+ 'getPose',
48
+ () => poseArgs,
49
+ () => ({ enabled: componentName !== '' })
50
+ )
51
+
52
+ const currentPose = $derived(poseQuery.data?.pose)
53
+ const poseError = $derived(poseQuery.error instanceof Error ? poseQuery.error : null)
54
+
55
+ let parseError = $state<Error>()
56
+
57
+ const executeMove = (input: MoveInput) => {
58
+ try {
59
+ parseError = undefined
60
+ move.mutate(parseMoveArgs(componentName, input), {})
61
+ } catch (error) {
62
+ parseError = error instanceof Error ? error : new Error(String(error))
63
+ }
64
+ }
65
+ </script>
66
+
67
+ <div class="flex flex-col gap-4">
68
+ <ComponentNameSelect
69
+ value={componentName}
70
+ options={frameNames}
71
+ onChange={(value) => {
72
+ selectedName = value
73
+ }}
74
+ />
75
+ <Move
76
+ {componentName}
77
+ {currentPose}
78
+ currentReferenceFrame={destinationFrame}
79
+ isPending={move.isPending}
80
+ lastError={parseError ?? move.error ?? poseError}
81
+ storageKey={`${partID}/${resourceName}/motion-move`}
82
+ onExecute={executeMove}
83
+ />
84
+ </div>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ partID: string;
3
+ resourceName: string;
4
+ }
5
+ declare const MoveWidget: import("svelte").Component<Props, {}, "">;
6
+ type MoveWidget = ReturnType<typeof MoveWidget>;
7
+ export default MoveWidget;
@@ -0,0 +1,127 @@
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 PoseInFrameInput from './pose-in-frame-input.svelte'
13
+
14
+ interface Props {
15
+ componentName: string
16
+ currentPose?: Pose
17
+ currentReferenceFrame?: string
18
+ isPending: boolean
19
+ lastError: Error | null
20
+ storageKey: string
21
+ onExecute: (input: MoveInput) => void
22
+ }
23
+
24
+ const {
25
+ componentName,
26
+ currentPose,
27
+ currentReferenceFrame,
28
+ isPending,
29
+ lastError,
30
+ storageKey,
31
+ onExecute,
32
+ }: Props = $props()
33
+
34
+ const zeroPose: Pose = { x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 }
35
+
36
+ let edit = $state<{ component: string; referenceFrame: string; pose: Pose }>()
37
+ const isEdited = $derived(edit?.component === componentName)
38
+ const referenceFrame = $derived(
39
+ isEdited && edit ? edit.referenceFrame : (currentReferenceFrame ?? 'world')
40
+ )
41
+
42
+ const pose = $derived(isEdited && edit ? edit.pose : (currentPose ?? zeroPose))
43
+
44
+ // PersistedState is created inside $derived so Svelte can track the storageKey
45
+ // dependency. The localStorage read on construction is benign and idempotent.
46
+ // storageKey is stable for the lifetime of this component.
47
+ const worldState = $derived(new PersistedState(`${storageKey}/world-state`, ''))
48
+ const constraints = $derived(new PersistedState(`${storageKey}/constraints`, ''))
49
+
50
+ const disabled = $derived(componentName === '' || isPending)
51
+
52
+ const execute = () => {
53
+ onExecute({
54
+ referenceFrame,
55
+ pose,
56
+ worldStateJson: worldState.current,
57
+ constraintsJson: constraints.current,
58
+ })
59
+ }
60
+ </script>
61
+
62
+ <div class="flex min-w-0 flex-col gap-4">
63
+ <PoseInFrameInput
64
+ {referenceFrame}
65
+ {pose}
66
+ onReferenceFrameChange={(frame) => {
67
+ edit = { component: componentName, referenceFrame: frame, pose }
68
+ }}
69
+ onPoseChange={(next) => {
70
+ edit = { component: componentName, referenceFrame, pose: next }
71
+ }}
72
+ />
73
+
74
+ <div class="flex flex-col gap-1">
75
+ <span class="text-xs font-medium">
76
+ World state <abbr class="text-subtle-2">(optional JSON)</abbr>
77
+ </span>
78
+ <CodeEditor
79
+ label="World state"
80
+ language="json"
81
+ value={worldState.current}
82
+ onChange={(next: string) => {
83
+ worldState.current = next
84
+ }}
85
+ class="h-32 overflow-y-auto"
86
+ />
87
+ </div>
88
+
89
+ <div class="flex flex-col gap-1">
90
+ <span class="text-xs font-medium">
91
+ Constraints <abbr class="text-subtle-2">(optional JSON)</abbr>
92
+ </span>
93
+ <CodeEditor
94
+ label="Constraints"
95
+ language="json"
96
+ value={constraints.current}
97
+ onChange={(next: string) => {
98
+ constraints.current = next
99
+ }}
100
+ class="h-32 overflow-y-auto"
101
+ />
102
+ </div>
103
+
104
+ <div class="flex items-center gap-2">
105
+ <Button
106
+ class="w-fit"
107
+ icon="play-circle-outline"
108
+ variant="dark"
109
+ {disabled}
110
+ onclick={execute}
111
+ >
112
+ Execute
113
+ </Button>
114
+ {#if isPending}
115
+ <Progress
116
+ size="medium"
117
+ variant="dark"
118
+ />
119
+ {/if}
120
+ </div>
121
+
122
+ <p class="text-subtle-2 text-xs">
123
+ Move blocks until the motion completes. Stop the component itself to interrupt.
124
+ </p>
125
+
126
+ <ErrorDisplay {lastError} />
127
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { Pose } from '@viamrobotics/sdk';
2
+ import type { MoveInput } from './parse-move-args';
3
+ interface Props {
4
+ componentName: string;
5
+ currentPose?: Pose;
6
+ currentReferenceFrame?: string;
7
+ isPending: boolean;
8
+ lastError: Error | null;
9
+ storageKey: string;
10
+ onExecute: (input: MoveInput) => void;
11
+ }
12
+ declare const Move: import("svelte").Component<Props, {}, "">;
13
+ type Move = ReturnType<typeof Move>;
14
+ 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 componentName - Name of the component to move.
21
+ * @param input - The pose, reference frame, and optional world-state/constraints JSON.
22
+ * @returns The `[destination, componentName, worldState?, constraints?]` tuple.
23
+ */
24
+ export declare const parseMoveArgs: (componentName: 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 componentName - Name of the component to move.
10
+ * @param input - The pose, reference frame, and optional world-state/constraints JSON.
11
+ * @returns The `[destination, componentName, worldState?, constraints?]` tuple.
12
+ */
13
+ export const parseMoveArgs = (componentName, 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, componentName, 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,168 @@
1
+ <script
2
+ lang="ts"
3
+ module
4
+ >
5
+ import type { Pose } from '@viamrobotics/sdk'
6
+
7
+ const poseLabelsList = Object.entries({
8
+ x: 'X',
9
+ y: 'Y',
10
+ z: 'Z',
11
+ oX: 'OX',
12
+ oY: 'OY',
13
+ oZ: 'OZ',
14
+ theta: 'θ',
15
+ }) as [keyof Pose, string][]
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import { Button, Icon, Input, Label, 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
+ referenceFrame: string
32
+ pose: Pose
33
+ onReferenceFrameChange: (frame: string) => void
34
+ onPoseChange: (pose: Pose) => void
35
+ }
36
+
37
+ const { referenceFrame, pose, onReferenceFrameChange, onPoseChange }: Props = $props()
38
+
39
+ let useRadians = $state(false)
40
+
41
+ const displayPose = $derived({
42
+ ...pose,
43
+ theta: useRadians ? degreesToRadians(pose.theta) : pose.theta,
44
+ })
45
+
46
+ const copyData = $derived(JSON.stringify(pose))
47
+
48
+ const handlePaste = (data: string): boolean => {
49
+ const parsed = parsePastedPose(data)
50
+ if (!parsed) {
51
+ return false
52
+ }
53
+ onPoseChange(parsed)
54
+ return true
55
+ }
56
+
57
+ const handleValueChange = (key: keyof Pose, inputValue: number) => {
58
+ const nextValue = key === 'theta' && useRadians ? radiansToDegrees(inputValue) : inputValue
59
+ onPoseChange({ ...pose, [key]: nextValue })
60
+ }
61
+
62
+ const zero = () => {
63
+ onPoseChange({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 })
64
+ }
65
+
66
+ const poseUnits = $derived({
67
+ x: 'mm',
68
+ y: 'mm',
69
+ z: 'mm',
70
+ oX: '',
71
+ oY: '',
72
+ oZ: '',
73
+ theta: useRadians ? 'rad' : 'deg',
74
+ })
75
+ </script>
76
+
77
+ <div class="flex min-w-0 flex-col gap-4">
78
+ <div class="flex items-center justify-between">
79
+ <span class="flex flex-row items-center gap-1 text-sm">
80
+ Destination pose
81
+ <Tooltip>
82
+ <Icon
83
+ name="information-outline"
84
+ cx="text-gray-6"
85
+ />
86
+
87
+ <span slot="description">
88
+ The target pose expressed in the given reference frame. Translations are in millimeters.
89
+ </span>
90
+ </Tooltip>
91
+ </span>
92
+ <div class="flex gap-1">
93
+ <AngleUnitToggle
94
+ {useRadians}
95
+ onToggle={() => {
96
+ useRadians = !useRadians
97
+ }}
98
+ />
99
+ <CopyButton data={copyData} />
100
+ <PasteButton onPaste={handlePaste} />
101
+ </div>
102
+ </div>
103
+
104
+ <Label>
105
+ Reference frame
106
+
107
+ <Input
108
+ slot="input"
109
+ value={referenceFrame}
110
+ placeholder="world"
111
+ on:input={(event) => {
112
+ onReferenceFrameChange((event.target as HTMLInputElement).value)
113
+ }}
114
+ />
115
+ </Label>
116
+
117
+ <Table>
118
+ <thead>
119
+ <tr>
120
+ <th>Pose</th>
121
+ <th>Value</th>
122
+ </tr>
123
+ </thead>
124
+ <tbody>
125
+ {#each poseLabelsList as labelList (labelList)}
126
+ {@const [key, label] = labelList}
127
+ {@const value = Number.parseFloat(formatNumeric(displayPose[key]))}
128
+ <tr>
129
+ <th>
130
+ <span class="relative inline-flex justify-center">
131
+ {label}
132
+ <abbr class="text-subtle-2 absolute left-full ml-1">{poseUnits[key]}</abbr>
133
+ </span>
134
+ </th>
135
+ <th>
136
+ <NumericInput
137
+ cx="max-w-[76px]"
138
+ {value}
139
+ on:change={(event) => {
140
+ handleValueChange(key, numberValueFromEvent(event) ?? 0)
141
+ }}
142
+ />
143
+ </th>
144
+ </tr>
145
+ {/each}
146
+ </tbody>
147
+ </Table>
148
+
149
+ <div class="flex flex-col gap-2">
150
+ <span class="flex flex-row gap-2">
151
+ <h4 class="text-xs font-semibold">Quick set</h4>
152
+ <Tooltip>
153
+ <Icon
154
+ name="information-outline"
155
+ cx="text-gray-6"
156
+ />
157
+
158
+ <span slot="description"> Will update the pose values but will not execute </span>
159
+ </Tooltip>
160
+ </span>
161
+ <Button
162
+ class="w-fit"
163
+ onclick={zero}
164
+ >
165
+ Zero
166
+ </Button>
167
+ </div>
168
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Pose } from '@viamrobotics/sdk';
2
+ interface Props {
3
+ referenceFrame: string;
4
+ pose: Pose;
5
+ onReferenceFrameChange: (frame: string) => void;
6
+ onPoseChange: (pose: Pose) => void;
7
+ }
8
+ declare const PoseInFrameInput: import("svelte").Component<Props, {}, "">;
9
+ type PoseInFrameInput = ReturnType<typeof PoseInFrameInput>;
10
+ export default PoseInFrameInput;
@@ -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, MotionMoveWidget, 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,10 @@ const resourceWidgetRegistry = {
127
132
  // services
128
133
  [ResourceTriplets.Discovery]: { widget: DiscoveryWidget, apis: [] },
129
134
  [ResourceTriplets.MLModel]: { widget: MLModelServiceWidget, apis: [] },
135
+ [ResourceTriplets.Motion]: {
136
+ widget: MotionServiceWidget,
137
+ apis: [{ id: 'move', label: 'Move', widgets: [MotionMoveWidget] }],
138
+ },
130
139
  [ResourceTriplets.Navigation]: { widget: NavigationServiceWidget, apis: [] },
131
140
  [ResourceTriplets.Slam]: {
132
141
  widget: SlamWidget,
@@ -145,12 +154,7 @@ const resourceWidgetRegistry = {
145
154
  * apiWidgetsForResource(gripperResourceName)
146
155
  * // [{ id: 'open-grab', label: 'Open / Grab', widgets: [GripperOpenWidget, GripperGrabWidget] }, ...]
147
156
  */
148
- export const apiWidgetsForResource = (resource) => {
149
- const api = getResourceAPI(resource);
150
- return api in resourceWidgetRegistry
151
- ? resourceWidgetRegistry[api].apis
152
- : [];
153
- };
157
+ export const apiWidgetsForResource = (resource) => resourceWidgetRegistry[getResourceAPI(resource)]?.apis ?? [];
154
158
  /**
155
159
  * Returns every resource triplet that has a test card, mapped to its API widgets.
156
160
  * Use this to enumerate the full catalog, e.g. a menu spanning every resource type;
@@ -162,26 +166,11 @@ export const apiWidgetsForResource = (resource) => {
162
166
  */
163
167
  export const availableAPIWidgets = () => {
164
168
  const result = {};
165
- for (const triplet of Object.keys(resourceWidgetRegistry)) {
166
- result[triplet] = resourceWidgetRegistry[triplet].apis;
169
+ for (const [api, entry] of Object.entries(resourceWidgetRegistry)) {
170
+ if (entry)
171
+ result[api] = entry.apis;
167
172
  }
168
173
  return result;
169
174
  };
170
175
  /** 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));
176
+ 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.0",
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",