@viamrobotics/motion-tools 1.3.5 → 1.5.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.
@@ -1,3 +1,3 @@
1
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;
2
+ export declare const createBufferGeometry: (positions: Float32Array, colors?: Uint8Array | null) => BufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>;
3
+ export declare const updateBufferGeometry: (geometry: BufferGeometry, positions: Float32Array, colors?: Uint8Array | null) => void;
package/dist/attribute.js CHANGED
@@ -3,7 +3,7 @@ export const createBufferGeometry = (positions, colors) => {
3
3
  const geometry = new BufferGeometry();
4
4
  geometry.setAttribute('position', new BufferAttribute(positions, 3));
5
5
  if (colors) {
6
- geometry.setAttribute('color', new BufferAttribute(colors, 3));
6
+ geometry.setAttribute('color', new BufferAttribute(colors, 3, true));
7
7
  }
8
8
  return geometry;
9
9
  };
@@ -24,7 +24,7 @@ export const updateBufferGeometry = (geometry, positions, colors) => {
24
24
  colorAttr.needsUpdate = true;
25
25
  }
26
26
  else {
27
- geometry.setAttribute('color', new BufferAttribute(colors, 3));
27
+ geometry.setAttribute('color', new BufferAttribute(colors, 3, true));
28
28
  }
29
29
  }
30
30
  };
@@ -665,6 +665,14 @@ export declare class GetKinematicsResponse extends Message<GetKinematicsResponse
665
665
  * @generated from field: bytes kinematics_data = 2;
666
666
  */
667
667
  kinematicsData: Uint8Array<ArrayBuffer>;
668
+ /**
669
+ * Map of URDF mesh file paths to mesh data
670
+ *
671
+ * @generated from field: map<string, viam.common.v1.Mesh> meshes_by_urdf_filepath = 3;
672
+ */
673
+ meshesByUrdfFilepath: {
674
+ [key: string]: Mesh;
675
+ };
668
676
  constructor(data?: PartialMessage<GetKinematicsResponse>);
669
677
  static readonly runtime: typeof proto3;
670
678
  static readonly typeName = "viam.common.v1.GetKinematicsResponse";
@@ -959,6 +959,12 @@ export class GetKinematicsResponse extends Message {
959
959
  * @generated from field: bytes kinematics_data = 2;
960
960
  */
961
961
  kinematicsData = new Uint8Array(0);
962
+ /**
963
+ * Map of URDF mesh file paths to mesh data
964
+ *
965
+ * @generated from field: map<string, viam.common.v1.Mesh> meshes_by_urdf_filepath = 3;
966
+ */
967
+ meshesByUrdfFilepath = {};
962
968
  constructor(data) {
963
969
  super();
964
970
  proto3.util.initPartial(data, this);
@@ -968,6 +974,7 @@ export class GetKinematicsResponse extends Message {
968
974
  static fields = proto3.util.newFieldList(() => [
969
975
  { no: 1, name: "format", kind: "enum", T: proto3.getEnumType(KinematicsFileFormat) },
970
976
  { no: 2, name: "kinematics_data", kind: "scalar", T: 12 /* ScalarType.BYTES */ },
977
+ { no: 3, name: "meshes_by_urdf_filepath", kind: "map", K: 9 /* ScalarType.STRING */, V: { kind: "message", T: Mesh } },
971
978
  ]);
972
979
  static fromBinary(bytes, options) {
973
980
  return new GetKinematicsResponse().fromBinary(bytes, options);
@@ -3,7 +3,7 @@
3
3
  lang="ts"
4
4
  >
5
5
  import { OrientationVector } from '../three/OrientationVector'
6
- import { Quaternion, Vector3, MathUtils } from 'three'
6
+ import { Quaternion, Vector3, MathUtils, BufferAttribute } from 'three'
7
7
 
8
8
  const vec3 = new Vector3()
9
9
  const quaternion = new Quaternion()
@@ -13,7 +13,7 @@
13
13
  <script lang="ts">
14
14
  import { draggable } from '@neodrag/svelte'
15
15
  import { Check, Copy } from 'lucide-svelte'
16
- import { useTask } from '@threlte/core'
16
+ import { useTask, isInstanceOf } from '@threlte/core'
17
17
  import { Button, Icon, Select, Input, Tooltip } from '@viamrobotics/prime-core'
18
18
  import {
19
19
  useSelectedEntity,
@@ -596,6 +596,19 @@
596
596
  </div>
597
597
  </div>
598
598
  {/if}
599
+
600
+ {#if isInstanceOf(object3d, 'Points')}
601
+ <div>
602
+ <strong class="font-semibold">points</strong>
603
+ {@render ImmutableField({
604
+ label: 'count',
605
+ ariaLabel: 'points count',
606
+ value: new Intl.NumberFormat().format(
607
+ (object3d.geometry.getAttribute('position') as BufferAttribute).array.length / 3
608
+ ),
609
+ })}
610
+ </div>
611
+ {/if}
599
612
  </div>
600
613
 
601
614
  <h3 class="text-subtle-2 pt-3 pb-2">Actions</h3>
@@ -25,7 +25,6 @@
25
25
  label="unfold more icon"
26
26
  variant="ghost"
27
27
  cx="size-6"
28
- onclick={() => (expanded.current = !expanded.current)}
29
28
  />
30
29
  {name}
31
30
  {@render titleAlert?.()}
@@ -10,6 +10,7 @@
10
10
  import { useEnvironment } from '../../hooks/useEnvironment.svelte'
11
11
  import { usePartID } from '../../hooks/usePartID.svelte'
12
12
  import { usePartConfig } from '../../hooks/usePartConfig.svelte'
13
+ import { useFrames } from '../../hooks/useFrames.svelte'
13
14
  import { traits, useQuery, useWorld } from '../../ecs'
14
15
  import { IsExcluded, type Entity } from 'koota'
15
16
  import { buildTreeNodes, type TreeNode } from './buildTree'
@@ -30,13 +31,19 @@
30
31
  )
31
32
  const environment = useEnvironment()
32
33
  const partConfig = usePartConfig()
34
+ const frames = useFrames()
33
35
  const world = useWorld()
34
36
 
35
37
  const worldEntity = world.spawn(IsExcluded, traits.Name('World'))
36
38
 
37
39
  const allEntities = useQuery(traits.Name)
38
40
 
39
- const { rootNodes, nodeMap } = $derived(buildTreeNodes(allEntities.current))
41
+ const { rootNodes, nodeMap } = $derived.by(() => {
42
+ // This ensures the tree rebuilds when frame parent relationships change
43
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
44
+ frames.current
45
+ return buildTreeNodes(allEntities.current)
46
+ })
40
47
 
41
48
  const rootNode = $derived<TreeNode>({
42
49
  entity: worldEntity,
@@ -71,6 +71,11 @@ class BinaryReader {
71
71
  this.offsetBytes += 4;
72
72
  return v;
73
73
  }
74
+ readU32() {
75
+ const v = this.view.getUint32(this.offsetBytes, this.littleEndian);
76
+ this.offsetBytes += 4;
77
+ return v;
78
+ }
74
79
  /**
75
80
  * Get a Float32Array VIEW into the underlying buffer (no copy) and advance.
76
81
  * Requires current offset to be 4-byte aligned (it will be, if you only readF32 so far).
@@ -222,24 +227,32 @@ export const provideDrawAPI = () => {
222
227
  label += String.fromCharCode(reader.read());
223
228
  }
224
229
  // Read counts
225
- const nPoints = reader.read();
226
- const nColors = reader.read();
230
+ const nPoints = reader.readU32();
231
+ const nColors = reader.readU32();
227
232
  // Read default color
228
- const r = reader.read();
229
- const g = reader.read();
230
- const b = reader.read();
233
+ let r = reader.read();
234
+ let g = reader.read();
235
+ let b = reader.read();
231
236
  const nPointsElements = nPoints * 3;
232
237
  const positions = reader.readF32Array(nPointsElements);
233
238
  const nColorsElements = nColors * 3;
234
- const rawColors = reader.readF32Array(nColorsElements);
235
- const colors = new Float32Array(nPointsElements);
236
- colors.set(rawColors);
237
- // Cover the gap for any points not colored
238
- for (let i = nColors; i < nPoints; i++) {
239
- const offset = i * 3;
240
- colors[offset] = r;
241
- colors[offset + 1] = g;
242
- colors[offset + 2] = b;
239
+ const rawColors = reader.readU8Array(nColorsElements);
240
+ let colors = null;
241
+ if (nColors > 1) {
242
+ colors = new Uint8Array(nPointsElements);
243
+ colors.set(rawColors);
244
+ // Cover the gap for any points not colored
245
+ for (let i = nColors; i < nPoints; i++) {
246
+ const offset = i * 3;
247
+ colors[offset] = Math.round(r * 255);
248
+ colors[offset + 1] = Math.round(g * 255);
249
+ colors[offset + 2] = Math.round(b * 255);
250
+ }
251
+ }
252
+ else if (nColors === 1) {
253
+ r = rawColors[0] / 255;
254
+ g = rawColors[1] / 255;
255
+ b = rawColors[2] / 255;
243
256
  }
244
257
  const entities = world.query(traits.DrawAPI);
245
258
  const entity = entities.find((entity) => entity.get(traits.Name) === label);
@@ -370,7 +383,8 @@ export const provideDrawAPI = () => {
370
383
  }
371
384
  else if (type === bufferTypes.DRAW_GLTF) {
372
385
  operation = 'DrawGLTF';
373
- drawGLTF(reader.buffer);
386
+ // GLTF payload starts after the 20-byte header (16 bytes UUID + 4 bytes type)
387
+ drawGLTF(reader.buffer.slice(20));
374
388
  }
375
389
  else {
376
390
  throw new Error('Invalid buffer');
@@ -22,11 +22,6 @@ export const provideFrames = (partID) => {
22
22
  }));
23
23
  const revision = $derived(machineStatus.current?.config?.revision);
24
24
  const partConfig = usePartConfig();
25
- $effect(() => {
26
- if (revision) {
27
- untrack(() => query.refetch());
28
- }
29
- });
30
25
  $effect(() => {
31
26
  if (query.isFetching) {
32
27
  logs.add('Fetching frames...');
@@ -118,15 +113,24 @@ export const provideFrames = (partID) => {
118
113
  const current = $derived(Object.values(frames));
119
114
  const entities = new Map();
120
115
  $effect.pre(() => {
121
- for (const [name, machineFrame] of Object.entries(machineFrames)) {
122
- if (machineFrame === undefined) {
123
- continue;
124
- }
125
- const existing = entities.get(name);
126
- if (existing) {
127
- const pose = createPose(machineFrame.transform.poseInObserverFrame?.pose);
128
- existing.set(traits.Pose, pose);
129
- }
116
+ if (revision) {
117
+ untrack(async () => {
118
+ await query.refetch();
119
+ for (const [name, machineFrame] of Object.entries(machineFrames)) {
120
+ if (machineFrame === undefined) {
121
+ continue;
122
+ }
123
+ const existing = entities.get(name);
124
+ if (existing) {
125
+ const pose = createPose(machineFrame.transform.poseInObserverFrame?.pose);
126
+ existing.set(traits.Pose, pose);
127
+ if (environment.current.viewerMode === 'monitor') {
128
+ // if we are in monitor mode, we want the network pose to overwrite any leftover edited poses
129
+ existing.set(traits.EditedPose, pose);
130
+ }
131
+ }
132
+ }
133
+ });
130
134
  }
131
135
  });
132
136
  $effect.pre(() => {
@@ -10,7 +10,7 @@ import { RefetchRates } from '../components/RefreshRate.svelte';
10
10
  import { useLogs } from './useLogs.svelte';
11
11
  import { useResourceByName } from './useResourceByName.svelte';
12
12
  import { useRefetchPoses } from './useRefetchPoses';
13
- const origingFrameComponentTypes = ['arm', 'gantry', 'gripper'];
13
+ const origingFrameComponentTypes = ['arm', 'gantry', 'gripper', 'base'];
14
14
  export const usePose = (name, parent) => {
15
15
  const environment = useEnvironment();
16
16
  const logs = useLogs();
@@ -7,6 +7,8 @@ import { createPose } from '../transform';
7
7
  import { useThrelte } from '@threlte/core';
8
8
  import { createBox, createCapsule, createSphere } from '../geometry';
9
9
  import { parsePlyInput } from '../ply';
10
+ import { parsePcdInWorker } from '../loaders/pcd';
11
+ import { createBufferGeometry } from '../attribute';
10
12
  export const provideWorldStates = () => {
11
13
  const partID = usePartID();
12
14
  const resourceNames = useResourceNames(() => partID.current, 'world_state_store');
@@ -45,7 +47,22 @@ const createWorldState = (client) => {
45
47
  entityTraits.push(traits.VertexColors(metadata.colors));
46
48
  }
47
49
  if (transform.physicalObject) {
48
- entityTraits.push(traits.Geometry(transform.physicalObject));
50
+ if (transform.physicalObject.geometryType.case === 'pointcloud') {
51
+ parsePcdInWorker(new Uint8Array(transform.physicalObject.geometryType.value.pointCloud)).then((pointcloud) => {
52
+ // pcds are a special case since they have to be loaded in a worker and the trait will be added to the existing entity
53
+ const entity = entities.get(transform.uuidString);
54
+ if (!entity) {
55
+ console.error('Entity not found to add pointcloud trait to', transform.uuidString);
56
+ return;
57
+ }
58
+ const geometry = createBufferGeometry(pointcloud.positions, pointcloud.colors);
59
+ entity.add(traits.BufferGeometry(geometry));
60
+ entity.add(traits.Points);
61
+ });
62
+ }
63
+ else {
64
+ entityTraits.push(traits.Geometry(transform.physicalObject));
65
+ }
49
66
  }
50
67
  if (metadata.shape === 'line' && metadata.points) {
51
68
  const { points } = metadata;
@@ -1,7 +1,7 @@
1
1
  export interface SuccessMessage {
2
2
  id: number;
3
3
  positions: Float32Array<ArrayBuffer>;
4
- colors: Float32Array<ArrayBuffer> | null;
4
+ colors: Uint8Array<ArrayBuffer> | null;
5
5
  }
6
6
  export type Message = SuccessMessage | {
7
7
  id: number;
@@ -15,7 +15,13 @@ self.onmessage = async (event) => {
15
15
  */
16
16
  const positions = pcd.geometry.attributes.position?.array ??
17
17
  new Float32Array(0);
18
- const colors = pcd.geometry.attributes.color?.array ?? null;
18
+ const colorsFloat = pcd.geometry.attributes.color?.array ?? null;
19
+ const colors = colorsFloat ? new Uint8Array(colorsFloat.length) : null;
20
+ if (colors) {
21
+ for (let i = 0, l = colorsFloat.length; i < l; i++) {
22
+ colors[i] = Math.round(colorsFloat[i] * 255);
23
+ }
24
+ }
19
25
  postMessage({ positions, colors, id }, colors ? [positions.buffer, colors.buffer] : [positions.buffer]);
20
26
  }
21
27
  else {
package/dist/snapshot.js CHANGED
@@ -192,9 +192,7 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
192
192
  for (let i = 0, l = positions.length; i < l; i += 1) {
193
193
  positions[i] *= 0.001;
194
194
  }
195
- const colors = drawing.metadata?.colors
196
- ? rgbaBytesToFloat32(drawing.metadata.colors)
197
- : undefined;
195
+ const colors = drawing.metadata?.colors;
198
196
  const geometry = createBufferGeometry(positions, colors);
199
197
  entityTraits.push(traits.BufferGeometry(geometry));
200
198
  if (geometryType.value.pointSize) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.3.5",
3
+ "version": "1.5.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -129,7 +129,7 @@
129
129
  "@neodrag/svelte": "^2.3.3",
130
130
  "@tanstack/svelte-query-devtools": "^6.0.2",
131
131
  "koota": "^0.5.3",
132
- "lodash-es": "4.17.21",
132
+ "lodash-es": "4.17.23",
133
133
  "uuid-tool": "^2.0.3"
134
134
  },
135
135
  "scripts": {