@viamrobotics/motion-tools 1.2.2 → 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.
@@ -0,0 +1,3 @@
1
+ import { BufferGeometry } from 'three';
2
+ export declare const createBufferGeometry: (positions: Float32Array, colors?: Float32Array | null) => BufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>;
3
+ export declare const updateBufferGeometry: (geometry: BufferGeometry, positions: Float32Array, colors?: Float32Array | null) => void;
@@ -0,0 +1,30 @@
1
+ import { BufferGeometry, BufferAttribute } from 'three';
2
+ export const createBufferGeometry = (positions, colors) => {
3
+ const geometry = new BufferGeometry();
4
+ geometry.setAttribute('position', new BufferAttribute(positions, 3));
5
+ if (colors) {
6
+ geometry.setAttribute('color', new BufferAttribute(colors, 3));
7
+ }
8
+ return geometry;
9
+ };
10
+ export const updateBufferGeometry = (geometry, positions, colors) => {
11
+ const positionAttr = geometry.getAttribute('position');
12
+ if (positionAttr && positionAttr.array.length >= positions.length) {
13
+ positionAttr.array.set(positions, 0);
14
+ geometry.setDrawRange(0, positions.length);
15
+ positionAttr.needsUpdate = true;
16
+ }
17
+ else {
18
+ geometry.setAttribute('position', new BufferAttribute(positions, 3));
19
+ }
20
+ if (colors) {
21
+ const colorAttr = geometry.getAttribute('color');
22
+ if (colorAttr && colorAttr.array.length >= colors.length) {
23
+ colorAttr.array.set(colors, 0);
24
+ colorAttr.needsUpdate = true;
25
+ }
26
+ else {
27
+ geometry.setAttribute('color', new BufferAttribute(colors, 3));
28
+ }
29
+ }
30
+ };
@@ -37,10 +37,11 @@
37
37
  Not(traits.FramesAPI),
38
38
  Not(traits.GeometriesAPI),
39
39
  Not(traits.WorldStateStoreAPI),
40
+ Not(traits.Points),
40
41
  Or(traits.Box, traits.Capsule, traits.Sphere, traits.BufferGeometry, traits.ReferenceFrame)
41
42
  )
42
43
 
43
- const points = useQuery(traits.PointsPositions)
44
+ const points = useQuery(traits.Points)
44
45
  const lines = useQuery(traits.LinePositions)
45
46
  const gltfs = useQuery(traits.GLTF)
46
47
  </script>
@@ -7,6 +7,7 @@
7
7
  import { traits } from '../../ecs'
8
8
  import { spawnSnapshotEntities } from '../../snapshot'
9
9
  import { useCameraControls } from '../../hooks/useControls.svelte'
10
+ import { createBufferGeometry } from '../../attribute'
10
11
 
11
12
  const props: HTMLAttributes<HTMLDivElement> = $props()
12
13
 
@@ -34,21 +35,25 @@
34
35
 
35
36
  break
36
37
  }
37
- case 'pcd':
38
+ case 'pcd': {
39
+ const geometry = createBufferGeometry(result.pcd.positions, result.pcd.colors)
40
+
38
41
  world.spawn(
39
42
  traits.Name(result.name),
40
- traits.PointsPositions(result.pcd.positions),
41
- result.pcd.colors ? traits.VertexColors(result.pcd.colors) : traits.Color,
43
+ traits.BufferGeometry(geometry),
44
+ traits.Points,
42
45
  traits.DroppedFile
43
46
  )
44
47
  break
45
- case 'ply':
48
+ }
49
+ case 'ply': {
46
50
  world.spawn(
47
51
  traits.Name(result.name),
48
52
  traits.BufferGeometry(result.ply),
49
53
  traits.DroppedFile
50
54
  )
51
55
  break
56
+ }
52
57
  }
53
58
 
54
59
  toast({ message: `${result.name} loaded.`, variant: ToastVariant.Success })
@@ -1,11 +1,5 @@
1
1
  <script lang="ts">
2
- import {
3
- Points,
4
- BufferAttribute,
5
- BufferGeometry,
6
- PointsMaterial,
7
- OrthographicCamera,
8
- } from 'three'
2
+ import { Points, PointsMaterial, OrthographicCamera } from 'three'
9
3
  import { T, useTask, useThrelte } from '@threlte/core'
10
4
  import { Portal } from '@threlte/extras'
11
5
  import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
@@ -28,9 +22,8 @@
28
22
  const name = useTrait(() => entity, traits.Name)
29
23
  const parent = useTrait(() => entity, traits.Parent)
30
24
  const pose = useTrait(() => entity, traits.Pose)
31
- const positions = useTrait(() => entity, traits.PointsPositions)
25
+ const geometry = useTrait(() => entity, traits.BufferGeometry)
32
26
  const color = useTrait(() => entity, traits.Color)
33
- const colors = useTrait(() => entity, traits.VertexColors)
34
27
  const opacity = useTrait(() => entity, traits.Opacity)
35
28
  const entityPointSize = useTrait(() => entity, traits.PointSize)
36
29
 
@@ -40,8 +33,7 @@
40
33
  const orthographic = $derived(settings.current.cameraMode === 'orthographic')
41
34
 
42
35
  const points = new Points()
43
- const geometry = new BufferGeometry()
44
- const material = new PointsMaterial()
36
+ const material = points.material as PointsMaterial
45
37
  material.toneMapped = false
46
38
 
47
39
  $effect.pre(() => {
@@ -49,7 +41,7 @@
49
41
  })
50
42
 
51
43
  $effect.pre(() => {
52
- if (colors.current) {
44
+ if (geometry.current?.getAttribute('color')) {
53
45
  material.color.set(0xffffff)
54
46
  } else if (color.current) {
55
47
  material.color.setRGB(color.current.r, color.current.g, color.current.b)
@@ -74,25 +66,18 @@
74
66
  })
75
67
 
76
68
  $effect.pre(() => {
77
- if (positions.current) {
78
- geometry.setAttribute('position', new BufferAttribute(positions.current, 3))
79
- }
80
- })
69
+ const colors = geometry.current?.getAttribute('color')
70
+ const positions = geometry.current?.getAttribute('position')
81
71
 
82
- $effect.pre(() => {
83
- material.vertexColors = colors.current !== undefined
72
+ material.vertexColors = colors !== undefined
84
73
 
85
- if (colors.current && positions.current) {
86
- const vertexColors = colors.current
87
- const hasAlphaChannel = positions.current.length / vertexColors.length === 0.75
88
- const itemSize = hasAlphaChannel ? 4 : 3
89
- geometry.setAttribute('color', new BufferAttribute(vertexColors, itemSize))
90
- geometry.attributes.color.needsUpdate = true
74
+ if (colors && positions) {
75
+ const hasAlphaChannel = positions.array.length / colors.array.length === 0.75
91
76
 
92
77
  let transparent = false
93
78
  if (hasAlphaChannel) {
94
- for (let i = 3, l = vertexColors.length; i < l; i += 4) {
95
- if (vertexColors[i] < 1) {
79
+ for (let i = 3, l = colors.array.length; i < l; i += 4) {
80
+ if (colors.array[i] < 1) {
96
81
  transparent = true
97
82
  break
98
83
  }
@@ -133,15 +118,17 @@
133
118
  })
134
119
  </script>
135
120
 
136
- <Portal id={parent.current}>
137
- <T
138
- is={points}
139
- name={name.current}
140
- {...events}
141
- bvh={{ maxDepth: 40, maxLeafTris: 20 }}
142
- >
143
- <T is={geometry} />
144
- <T is={material} />
145
- {@render children?.()}
146
- </T>
147
- </Portal>
121
+ {#if geometry.current}
122
+ <Portal id={parent.current}>
123
+ <T
124
+ is={points}
125
+ name={name.current}
126
+ {...events}
127
+ bvh={{ maxDepth: 40, maxLeafTris: 20 }}
128
+ >
129
+ <T is={geometry.current} />
130
+ <T is={material} />
131
+ {@render children?.()}
132
+ </T>
133
+ </Portal>
134
+ {/if}
@@ -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()
@@ -42,6 +45,7 @@
42
45
  const flush = () => {
43
46
  if (pending) return
44
47
  pending = true
48
+
45
49
  window.setTimeout(() => {
46
50
  const results = buildTreeNodes(world.query(traits.Name))
47
51
  children = results.rootNodes
@@ -45,6 +45,10 @@ export declare const Color: import("koota").Trait<{
45
45
  b: number;
46
46
  }>;
47
47
  export declare const Arrow: import("koota").TagTrait;
48
+ /**
49
+ * Render entity as points
50
+ */
51
+ export declare const Points: import("koota").TagTrait;
48
52
  /**
49
53
  * A box, in mm
50
54
  */
@@ -73,7 +77,6 @@ export declare const PointColor: import("koota").Trait<{
73
77
  }>;
74
78
  /** format [x, y, z, ...] */
75
79
  export declare const LinePositions: import("koota").Trait<() => Float32Array<ArrayBuffer>>;
76
- export declare const PointsPositions: import("koota").Trait<() => Float32Array<ArrayBuffer>>;
77
80
  export declare const BufferGeometry: import("koota").Trait<() => ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>>;
78
81
  /** format [r, g, b, ...] */
79
82
  export declare const VertexColors: import("koota").Trait<() => Float32Array<ArrayBuffer>>;
@@ -19,6 +19,10 @@ export const Opacity = trait(() => 1);
19
19
  */
20
20
  export const Color = trait({ r: 0, g: 0, b: 0 });
21
21
  export const Arrow = trait();
22
+ /**
23
+ * Render entity as points
24
+ */
25
+ export const Points = trait();
22
26
  /**
23
27
  * A box, in mm
24
28
  */
@@ -34,7 +38,6 @@ export const Sphere = trait({ r: 200 });
34
38
  export const PointColor = trait({ r: 0, g: 0, b: 0 });
35
39
  /** format [x, y, z, ...] */
36
40
  export const LinePositions = trait(() => new Float32Array());
37
- export const PointsPositions = trait(() => new Float32Array());
38
41
  export const BufferGeometry = trait(() => new ThreeBufferGeometry());
39
42
  /** format [r, g, b, ...] */
40
43
  export const VertexColors = trait(() => new Float32Array());
@@ -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
+ };
@@ -2,7 +2,6 @@ import { getContext, setContext } from 'svelte';
2
2
  import { Color, Vector3, Vector4 } from 'three';
3
3
  import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
4
4
  import { UuidTool } from 'uuid-tool';
5
- import { parsePcdInWorker } from '../loaders/pcd';
6
5
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
7
6
  import { createPose, createPoseFromFrame } from '../transform';
8
7
  import { useCameraControls } from './useControls.svelte';
@@ -13,6 +12,7 @@ import { parsePlyInput } from '../ply';
13
12
  import { useLogs } from './useLogs.svelte';
14
13
  import { createBox, createCapsule, createSphere } from '../geometry';
15
14
  import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
15
+ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
16
16
  const colorUtil = new Color();
17
17
  const bufferTypes = {
18
18
  DRAW_POINTS: 0,
@@ -74,7 +74,6 @@ export const provideDrawAPI = () => {
74
74
  const drawConnectionConfig = useDrawConnectionConfig();
75
75
  const backendIP = $derived(drawConnectionConfig.current?.backendIP);
76
76
  const websocketPort = $derived(drawConnectionConfig.current?.websocketPort);
77
- let pointsIndex = 0;
78
77
  let geometryIndex = 0;
79
78
  let poseIndex = 0;
80
79
  let reconnectDelay = 200;
@@ -126,13 +125,6 @@ export const provideDrawAPI = () => {
126
125
  entities.set(name, entity);
127
126
  }
128
127
  };
129
- const drawPCD = async (buffer) => {
130
- const { positions, colors } = await parsePcdInWorker(new Uint8Array(buffer));
131
- const entity = world.spawn(traits.Name(`Points ${++pointsIndex}`), traits.PointsPositions(positions), traits.DrawAPI);
132
- if (colors) {
133
- entity.add(traits.VertexColors(colors));
134
- }
135
- };
136
128
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
129
  const drawGeometry = (data, color, parent) => {
138
130
  const name = data.label ?? `geometry ${++geometryIndex}`;
@@ -225,9 +217,6 @@ export const provideDrawAPI = () => {
225
217
  for (let i = 0; i < labelLen; i++) {
226
218
  label += String.fromCharCode(reader.read());
227
219
  }
228
- const entities = world.query(traits.DrawAPI);
229
- const entity = entities.find((entity) => entity.get(traits.Name) === label);
230
- entity?.destroy();
231
220
  // Read counts
232
221
  const nPoints = reader.read();
233
222
  const nColors = reader.read();
@@ -250,7 +239,17 @@ export const provideDrawAPI = () => {
250
239
  colors[offset + 1] = g;
251
240
  colors[offset + 2] = b;
252
241
  }
253
- world.spawn(traits.Name(label), traits.Color(colorUtil.set(r, g, b)), traits.PointsPositions(positions), traits.VertexColors(colors), traits.DrawAPI);
242
+ const entities = world.query(traits.DrawAPI);
243
+ const entity = entities.find((entity) => entity.get(traits.Name) === label);
244
+ if (entity) {
245
+ const geometry = entity.get(traits.BufferGeometry);
246
+ if (geometry) {
247
+ updateBufferGeometry(geometry, positions, colors);
248
+ return;
249
+ }
250
+ }
251
+ const geometry = createBufferGeometry(positions, colors);
252
+ world.spawn(traits.Name(label), traits.Color(colorUtil.set(r, g, b)), traits.BufferGeometry(geometry), traits.Points, traits.DrawAPI);
254
253
  };
255
254
  const drawLine = async (reader) => {
256
255
  // Read label length
@@ -310,7 +309,6 @@ export const provideDrawAPI = () => {
310
309
  entity.destroy();
311
310
  }
312
311
  entities.clear();
313
- pointsIndex = 0;
314
312
  geometryIndex = 0;
315
313
  poseIndex = 0;
316
314
  };
@@ -364,10 +362,6 @@ export const provideDrawAPI = () => {
364
362
  operation = 'DrawLine';
365
363
  drawLine(reader);
366
364
  }
367
- else if (type === bufferTypes.DRAW_PCD) {
368
- operation = 'DrawPCD';
369
- drawPCD(reader.buffer);
370
- }
371
365
  else if (type === bufferTypes.DRAW_GLTF) {
372
366
  operation = 'DrawGLTF';
373
367
  drawGLTF(reader.buffer);
@@ -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
+ };
@@ -1,5 +1,5 @@
1
1
  import { CameraClient } from '@viamrobotics/sdk';
2
- import { setContext, getContext, untrack } from 'svelte';
2
+ import { setContext, getContext } from 'svelte';
3
3
  import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
4
4
  import { parsePcdInWorker } from '../loaders/pcd';
5
5
  import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
@@ -7,6 +7,7 @@ import { useLogs } from './useLogs.svelte';
7
7
  import { RefetchRates } from '../components/RefreshRate.svelte';
8
8
  import { traits, useWorld } from '../ecs';
9
9
  import { useEnvironment } from './useEnvironment.svelte';
10
+ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
10
11
  const typeSafeObjectFromEntries = (entries) => {
11
12
  return Object.fromEntries(entries);
12
13
  };
@@ -26,12 +27,13 @@ export const providePointclouds = (partID) => {
26
27
  refetchInterval: false,
27
28
  }),
28
29
  ]));
29
- const fetchedPropQueries = propQueries.every(([, query]) => query.isPending === false);
30
+ const fetchedPropQueries = $derived(propQueries.every(([, query]) => query.isPending === false));
30
31
  const interval = $derived(refreshRates.get(RefreshRates.pointclouds));
31
32
  const enabledClients = $derived.by(() => {
32
33
  const results = [];
33
34
  for (const client of clients) {
34
- if (fetchedPropQueries &&
35
+ if (environment.current.viewerMode === 'monitor' &&
36
+ fetchedPropQueries &&
35
37
  client.current?.name &&
36
38
  interval !== RefetchRates.OFF &&
37
39
  disabledCameras.get(client.current?.name) !== true) {
@@ -57,7 +59,6 @@ export const providePointclouds = (partID) => {
57
59
  }
58
60
  });
59
61
  const options = $derived({
60
- enabled: environment.current.viewerMode === 'edit',
61
62
  refetchInterval: interval,
62
63
  });
63
64
  const queries = $derived(enabledClients.map((client) => [client.current.name, createResourceQuery(client, 'getPointCloud', () => options)]));
@@ -72,7 +73,7 @@ export const providePointclouds = (partID) => {
72
73
  }
73
74
  }
74
75
  });
75
- const pcObjects = $state([]);
76
+ let pcObjects = $state.raw([]);
76
77
  $effect(() => {
77
78
  const binaries = [];
78
79
  for (const [name, query] of queries) {
@@ -85,14 +86,16 @@ export const providePointclouds = (partID) => {
85
86
  const { positions, colors } = await parsePcdInWorker(new Uint8Array(uint8array));
86
87
  return { name, positions, colors };
87
88
  })).then((results) => {
89
+ const fulfilledResults = [];
88
90
  for (const result of results) {
89
91
  if (result.status === 'fulfilled') {
90
- untrack(() => pcObjects.push(result.value));
92
+ fulfilledResults.push(result.value);
91
93
  }
92
94
  else if (result.status === 'rejected') {
93
95
  logs.add(result.reason, 'error');
94
96
  }
95
97
  }
98
+ pcObjects = fulfilledResults;
96
99
  });
97
100
  });
98
101
  const entities = new Map();
@@ -101,13 +104,14 @@ export const providePointclouds = (partID) => {
101
104
  for (const { name, positions, colors } of pcObjects) {
102
105
  const existing = entities.get(name);
103
106
  if (existing) {
104
- existing.set(traits.PointsPositions, positions);
105
- if (colors) {
106
- existing.set(traits.VertexColors, colors);
107
+ const geometry = existing.get(traits.BufferGeometry);
108
+ if (geometry) {
109
+ updateBufferGeometry(geometry, positions, colors);
110
+ continue;
107
111
  }
108
- continue;
109
112
  }
110
- const entity = world.spawn(traits.Parent(name), traits.Name(`${name} pointcloud`), traits.PointsPositions(positions), colors ? traits.VertexColors(colors) : traits.Color);
113
+ const geometry = createBufferGeometry(positions, colors);
114
+ const entity = world.spawn(traits.Parent(name), traits.Name(`${name} pointcloud`), traits.BufferGeometry(geometry), traits.Points);
111
115
  entities.set(name, entity);
112
116
  }
113
117
  // Clean up old entities
@@ -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(() => {
package/dist/snapshot.js CHANGED
@@ -8,6 +8,7 @@ import { parseMetadata } from './WorldObject.svelte';
8
8
  import { rgbaBytesToFloat32, rgbaToHex } from './color';
9
9
  import { asFloat32Array, STRIDE } from './buffer';
10
10
  import { createPose } from './transform';
11
+ import { createBufferGeometry } from './attribute';
11
12
  const vec3 = new Vector3();
12
13
  const origin = new Vector3();
13
14
  const direction = new Vector3();
@@ -222,10 +223,15 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
222
223
  for (let i = 0, l = positions.length; i < l; i += 1) {
223
224
  positions[i] *= 0.001;
224
225
  }
225
- entityTraits.push(traits.PointsPositions(positions));
226
+ const colors = drawing.metadata?.colors
227
+ ? rgbaBytesToFloat32(drawing.metadata.colors)
228
+ : undefined;
229
+ const geometry = createBufferGeometry(positions, colors);
230
+ entityTraits.push(traits.BufferGeometry(geometry));
226
231
  if (geometryType.value.pointSize) {
227
232
  entityTraits.push(traits.PointSize(geometryType.value.pointSize * 0.001));
228
233
  }
234
+ entityTraits.push(traits.Points);
229
235
  }
230
236
  else if (geometryType?.case === 'nurbs') {
231
237
  const { degree = 3, knots: knotsBuffer, weights: weightsBuffer, controlPoints: controlPointsBuffer, } = geometryType.value;
@@ -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.2",
3
+ "version": "1.3.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",