@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.
- package/README.md +33 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/navigation-map/components/obstacle.svelte +2 -1
- package/dist/components/three/capsule-geometry.d.ts +10 -0
- package/dist/components/three/capsule-geometry.js +19 -0
- package/dist/components/three/index.d.ts +1 -0
- package/dist/components/three/index.js +1 -0
- package/dist/components/widgets/do-command/do-command.svelte +21 -7
- package/dist/components/widgets/do-command/do-command.svelte.d.ts +8 -1
- package/dist/components/widgets/motion/frame-select.svelte +29 -0
- package/dist/components/widgets/motion/frame-select.svelte.d.ts +9 -0
- package/dist/components/widgets/motion/frame-system-config.d.ts +37 -0
- package/dist/components/widgets/motion/frame-system-config.js +42 -0
- package/dist/components/widgets/motion/motion.svelte +65 -0
- package/dist/components/widgets/motion/motion.svelte.d.ts +7 -0
- package/dist/components/widgets/motion/move-widget.svelte +65 -0
- package/dist/components/widgets/motion/move-widget.svelte.d.ts +11 -0
- package/dist/components/widgets/motion/move.svelte +119 -0
- package/dist/components/widgets/motion/move.svelte.d.ts +16 -0
- package/dist/components/widgets/motion/parse-move-args.d.ts +24 -0
- package/dist/components/widgets/motion/parse-move-args.js +23 -0
- package/dist/components/widgets/motion/parse-pasted-pose.d.ts +10 -0
- package/dist/components/widgets/motion/parse-pasted-pose.js +30 -0
- package/dist/components/widgets/motion/pose-input.svelte +154 -0
- package/dist/components/widgets/motion/pose-input.svelte.d.ts +8 -0
- package/dist/components/widgets/navigation/obstacles/meshes.svelte +1 -2
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -4
- package/dist/is-known-resource.d.ts +3 -0
- package/dist/is-known-resource.js +5 -0
- package/dist/registry.d.ts +28 -0
- package/dist/{resource-widget.js → registry.js} +12 -26
- package/dist/resource-widget-types.d.ts +21 -0
- package/dist/resource-widget-types.js +1 -0
- package/dist/show-resource-widget.d.ts +3 -0
- package/dist/show-resource-widget.js +10 -0
- 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';
|
package/dist/components/index.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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={
|
|
88
|
+
value={displayInput}
|
|
75
89
|
onChange={(nextInput: string) => {
|
|
76
|
-
|
|
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,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 {
|
|
9
|
-
|
|
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 {
|
|
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,
|
|
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,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
|
|
166
|
-
|
|
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,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.
|
|
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",
|