@viamrobotics/motion-tools 1.2.3 → 1.3.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.
@@ -20,6 +20,7 @@
20
20
  import { provideFramelessComponents } from '../hooks/useFramelessComponents.svelte'
21
21
  import { provideResourceByName } from '../hooks/useResourceByName.svelte'
22
22
  import { provide3DModels } from '../hooks/use3DModels.svelte'
23
+ import { providePointcloudObjects } from '../hooks/usePointcloudObjects.svelte'
23
24
 
24
25
  interface Props {
25
26
  cameraPose?: CameraPose
@@ -44,6 +45,7 @@
44
45
  provideGeometries(() => partID.current)
45
46
  provide3DModels(() => partID.current)
46
47
  providePointclouds(() => partID.current)
48
+ providePointcloudObjects(() => partID.current)
47
49
  provideArmClient(() => partID.current)
48
50
  provideWorldStates()
49
51
  provideFramelessComponents()
@@ -16,8 +16,9 @@
16
16
  const { invalidate } = useThrelte()
17
17
  const partID = usePartID()
18
18
  const cameras = useResourceNames(() => partID.current, 'camera')
19
+ const visionServices = useResourceNames(() => partID.current, 'vision')
19
20
  const settings = useSettings()
20
- const { disabledCameras } = useMachineSettings()
21
+ const { disabledCameras, disabledVisionServicesObjectPointclouds } = useMachineSettings()
21
22
  const geometries = useGeometries()
22
23
  const pointclouds = usePointClouds()
23
24
  const { refetchPoses } = useRefetchPoses()
@@ -56,8 +57,9 @@
56
57
  pointclouds.refetch()
57
58
  }}
58
59
  />
59
- <div>
60
- <div>Enabled pointcloud cameras</div>
60
+
61
+ <div class="mt-4">
62
+ <h3 class="text-sm"><strong>Enabled pointcloud cameras</strong></h3>
61
63
  {#each cameras.current as camera (camera)}
62
64
  <div class="flex items-center justify-between gap-4 py-2">
63
65
  {camera.name}
@@ -73,6 +75,23 @@
73
75
  {/each}
74
76
  </div>
75
77
 
78
+ <div class="mt-4">
79
+ <h3 class="text-sm"><strong>Enabled vision services</strong></h3>
80
+ {#each visionServices.current as visionService (visionService)}
81
+ <div class="flex items-center justify-between gap-4 py-2">
82
+ {visionService.name}
83
+ <Switch
84
+ on={disabledVisionServicesObjectPointclouds.get(visionService.name) !== true}
85
+ on:change={(event) => {
86
+ disabledVisionServicesObjectPointclouds.set(visionService.name, !event.detail)
87
+ }}
88
+ />
89
+ </div>
90
+ {:else}
91
+ No vision services detected
92
+ {/each}
93
+ </div>
94
+
76
95
  <h3 class="pt-2 text-sm"><strong>Pointclouds</strong></h3>
77
96
  <div class="flex flex-col gap-2.5">
78
97
  <label class="flex items-center justify-between gap-2">
@@ -28,7 +28,10 @@
28
28
 
29
29
  const partID = usePartID()
30
30
  const selectedEntity = useSelectedEntity()
31
- const resizable = useResizable(() => 'treeview')
31
+ const resizable = useResizable(
32
+ () => 'treeview',
33
+ () => ({ width: 240, height: window.innerHeight - 20 })
34
+ )
32
35
  const environment = useEnvironment()
33
36
  const partConfig = usePartConfig()
34
37
  const world = useWorld()
@@ -0,0 +1 @@
1
+ export declare const typeSafeObjectFromEntries: <const T extends ReadonlyArray<readonly [PropertyKey, unknown]>>(entries: T) => { [K in T[number] as K[0]]: K[1]; };
@@ -0,0 +1,3 @@
1
+ export const typeSafeObjectFromEntries = (entries) => {
2
+ return Object.fromEntries(entries);
3
+ };
@@ -6,6 +6,7 @@ export declare const RefreshRates: {
6
6
  type Context = {
7
7
  refreshRates: SvelteMap<string, number>;
8
8
  disabledCameras: SvelteMap<string, boolean>;
9
+ disabledVisionServicesObjectPointclouds: SvelteMap<string, boolean>;
9
10
  };
10
11
  export declare const provideMachineSettings: () => void;
11
12
  export declare const useMachineSettings: () => Context;
@@ -4,6 +4,7 @@ import { SvelteMap } from 'svelte/reactivity';
4
4
  const key = Symbol('polling-rate-context');
5
5
  const refreshRatesKey = 'polling-rate';
6
6
  const disabledCamerasKey = 'disabled-cameras';
7
+ const disabledVisionServicesObjectPointcloudsKey = 'disabled-vision-services-object-pointcloud';
7
8
  export const RefreshRates = {
8
9
  poses: 'poses',
9
10
  pointclouds: 'pointclouds',
@@ -21,12 +22,16 @@ export const provideMachineSettings = () => {
21
22
  [RefreshRates.pointclouds, -1],
22
23
  ]);
23
24
  const disabledCameras = new SvelteMap();
25
+ const disabledVisionServicesObjectPointclouds = new SvelteMap();
24
26
  get(refreshRatesKey).then((entries) => {
25
27
  setFromEntries(refreshRates, entries);
26
28
  });
27
29
  get(disabledCamerasKey).then((entries) => {
28
30
  setFromEntries(disabledCameras, entries);
29
31
  });
32
+ get(disabledVisionServicesObjectPointcloudsKey).then((entries) => {
33
+ setFromEntries(disabledVisionServicesObjectPointclouds, entries);
34
+ });
30
35
  $effect(() => {
31
36
  set(refreshRatesKey, [...refreshRates.entries()]);
32
37
  });
@@ -40,6 +45,9 @@ export const provideMachineSettings = () => {
40
45
  get disabledCameras() {
41
46
  return disabledCameras;
42
47
  },
48
+ get disabledVisionServicesObjectPointclouds() {
49
+ return disabledVisionServicesObjectPointclouds;
50
+ },
43
51
  });
44
52
  };
45
53
  export const useMachineSettings = () => {
@@ -0,0 +1,6 @@
1
+ interface Context {
2
+ refetch: () => void;
3
+ }
4
+ export declare const providePointcloudObjects: (partID: () => string) => void;
5
+ export declare const usePointcloudObjects: () => Context;
6
+ export {};
@@ -0,0 +1,174 @@
1
+ import { GeometriesInFrame, PointCloudObject, VisionClient } from '@viamrobotics/sdk';
2
+ import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
+ import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
4
+ import { useLogs } from './useLogs.svelte';
5
+ import { parsePcdInWorker } from '../lib';
6
+ import { getContext, setContext } from 'svelte';
7
+ import { traits, useWorld } from '../ecs';
8
+ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
9
+ import { useEnvironment } from './useEnvironment.svelte';
10
+ import { RefetchRates } from '../components/RefreshRate.svelte';
11
+ import { createPose } from '../transform';
12
+ const key = Symbol('pointcloud-object-context');
13
+ export const providePointcloudObjects = (partID) => {
14
+ const world = useWorld();
15
+ const environment = useEnvironment();
16
+ const { refreshRates, disabledVisionServicesObjectPointclouds } = useMachineSettings();
17
+ const services = useResourceNames(partID, 'vision');
18
+ const clients = $derived(services.current.map((service) => createResourceClient(VisionClient, partID, () => service.name)));
19
+ const propQueries = $derived(clients.map((client) => [
20
+ client.current?.name,
21
+ createResourceQuery(client, 'getProperties', {
22
+ staleTime: Infinity,
23
+ refetchOnMount: false,
24
+ refetchInterval: false,
25
+ }),
26
+ ]));
27
+ const fetchedPropQueries = $derived(propQueries.every(([, query]) => query.isPending === false));
28
+ const enabledClients = $derived.by(() => {
29
+ const results = [];
30
+ for (const client of clients) {
31
+ if (environment.current.viewerMode === 'monitor' &&
32
+ fetchedPropQueries &&
33
+ client.current?.name &&
34
+ interval !== RefetchRates.OFF) {
35
+ results.push(client);
36
+ }
37
+ }
38
+ return results;
39
+ });
40
+ /**
41
+ * Some machines have a lot of vision services, so before enabling all of them
42
+ * we'll first check pointcloud object support.
43
+ *
44
+ * We'll disable cameras that don't support pointclouds,
45
+ * but still allow users to manually enable if they want to.
46
+ */
47
+ $effect(() => {
48
+ for (const [name, query] of propQueries) {
49
+ if (name && query.data?.objectPointCloudsSupported === false) {
50
+ if (disabledVisionServicesObjectPointclouds.get(name) === undefined) {
51
+ disabledVisionServicesObjectPointclouds.set(name, true);
52
+ }
53
+ }
54
+ }
55
+ });
56
+ const logs = useLogs();
57
+ const interval = $derived(refreshRates.get(RefreshRates.pointclouds));
58
+ const options = $derived({
59
+ enabled: interval !== -1,
60
+ refetchInterval: (interval === 0 ? false : interval),
61
+ });
62
+ const queries = $derived(enabledClients.map((client) => [
63
+ client.current.name,
64
+ createResourceQuery(client, 'getObjectPointClouds', [''], () => options),
65
+ ]));
66
+ $effect(() => {
67
+ for (const [name, query] of queries) {
68
+ if (query.isFetching) {
69
+ logs.add(`Fetching pointcloud for ${name}...`);
70
+ }
71
+ else if (query.error) {
72
+ logs.add(`Error fetching pointcloud from ${name}: ${query.error.message}`, 'error');
73
+ }
74
+ }
75
+ });
76
+ let pcResults = $state.raw([]);
77
+ $effect(() => {
78
+ const responses = [];
79
+ for (const [name, query] of queries) {
80
+ const { data } = query;
81
+ if (name && data) {
82
+ responses.push([name, data]);
83
+ }
84
+ }
85
+ Promise.allSettled(responses.map(async ([name, pointcloudObjects]) => {
86
+ const pointclouds = await Promise.all(pointcloudObjects.map((value) => parsePcdInWorker(new Uint8Array(value.pointCloud))));
87
+ return {
88
+ name,
89
+ pointclouds,
90
+ geometries: pointcloudObjects.map((value) => value.geometries),
91
+ };
92
+ })).then((results) => {
93
+ const fulfilledResults = [];
94
+ for (const result of results) {
95
+ if (result.status === 'fulfilled') {
96
+ fulfilledResults.push(result.value);
97
+ }
98
+ else if (result.status === 'rejected') {
99
+ logs.add(result.reason, 'error');
100
+ }
101
+ }
102
+ pcResults = fulfilledResults;
103
+ });
104
+ });
105
+ const entities = new Map();
106
+ $effect(() => {
107
+ const active = {};
108
+ for (const { name, pointclouds, geometries } of pcResults) {
109
+ for (const [pointcloudIndex, pointcloud] of pointclouds.entries()) {
110
+ const poincloudLabel = `${name} pointcloud ${pointcloudIndex + 1}`;
111
+ const existing = entities.get(poincloudLabel);
112
+ active[poincloudLabel] = true;
113
+ if (existing) {
114
+ const geometry = existing.get(traits.BufferGeometry);
115
+ if (geometry) {
116
+ updateBufferGeometry(geometry, pointcloud.positions, pointcloud.colors);
117
+ }
118
+ }
119
+ else {
120
+ const geometry = createBufferGeometry(pointcloud.positions, pointcloud.colors);
121
+ const entity = world.spawn(traits.Name(poincloudLabel), traits.BufferGeometry(geometry), traits.Points);
122
+ entities.set(poincloudLabel, entity);
123
+ }
124
+ if (geometries) {
125
+ for (const geometriesInFrame of geometries) {
126
+ if (geometriesInFrame) {
127
+ for (const [geometryIndex, geometry] of geometriesInFrame.geometries.entries()) {
128
+ const geometryLabel = `${name} pointcloud ${pointcloudIndex} geometry ${geometryIndex + 1}`;
129
+ const pose = createPose(geometry.center);
130
+ active[geometryLabel] = true;
131
+ const existing = entities.get(geometryLabel);
132
+ if (existing) {
133
+ existing.set(traits.Pose, pose);
134
+ }
135
+ else {
136
+ const entityTraits = [
137
+ traits.Name(geometryLabel),
138
+ traits.Pose(pose),
139
+ traits.GeometriesAPI,
140
+ traits.Geometry(geometry),
141
+ traits.Opacity(0.2),
142
+ traits.Color({ r: 0, g: 1, b: 0 }),
143
+ ];
144
+ if (geometriesInFrame.referenceFrame) {
145
+ entityTraits.push(traits.Parent(geometriesInFrame.referenceFrame));
146
+ }
147
+ const entity = world.spawn(...entityTraits);
148
+ entities.set(geometryLabel, entity);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ // Clean up old entities
157
+ for (const [label, entity] of entities) {
158
+ if (!active[label]) {
159
+ entity.destroy();
160
+ entities.delete(label);
161
+ }
162
+ }
163
+ });
164
+ setContext(key, {
165
+ refetch() {
166
+ for (const [, query] of queries) {
167
+ query.refetch();
168
+ }
169
+ },
170
+ });
171
+ };
172
+ export const usePointcloudObjects = () => {
173
+ return getContext(key);
174
+ };
@@ -8,5 +8,5 @@ interface Context {
8
8
  observe: (target: HTMLElement) => void;
9
9
  }
10
10
  export declare const MIN_DIMENSIONS: Dimensions;
11
- export declare const useResizable: (name: () => string) => Context;
11
+ export declare const useResizable: (name: () => string, defaultDimensions?: () => Dimensions) => Context;
12
12
  export {};
@@ -1,8 +1,8 @@
1
1
  import { get, set } from 'idb-keyval';
2
2
  export const MIN_DIMENSIONS = { width: 240, height: 320 };
3
- export const useResizable = (name) => {
3
+ export const useResizable = (name, defaultDimensions) => {
4
4
  const key = $derived(`${name()}-resizable`);
5
- let dimensions = $state.raw(MIN_DIMENSIONS);
5
+ let dimensions = $derived(defaultDimensions?.() ?? MIN_DIMENSIONS);
6
6
  let loaded = $state(false);
7
7
  let observer;
8
8
  $effect(() => {
@@ -7,7 +7,7 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
7
7
  */
8
8
  export const createArrowGeometry = () => {
9
9
  const length = 0.1;
10
- const headLength = length * 0.2;
10
+ const headLength = length * 0.3;
11
11
  const headWidth = headLength * 0.3;
12
12
  const tailLength = length - headLength;
13
13
  const tailWidth = 0.001;
@@ -15,7 +15,7 @@ export const createArrowGeometry = () => {
15
15
  const tailGeometry = new BoxGeometry(tailWidth, tailLength, tailWidth);
16
16
  tailGeometry.translate(0, tailLength * 0.5, 0);
17
17
  // Head: cone centered at origin spanning [-h/2, +h/2] in y
18
- const radialSegments = 5;
18
+ const radialSegments = 3;
19
19
  const headGeo = new ConeGeometry(headWidth * 0.5, headLength, radialSegments, 1, false);
20
20
  // Place its center at y = shaftLength + headLength/2 so tip lands at y = shaftLength + headLength
21
21
  headGeo.translate(0, tailLength + headLength * 0.5, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",