@viamrobotics/motion-tools 0.14.12 → 0.15.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,5 +1,5 @@
1
1
  import type { Geometry, PlainMessage, Pose, Struct, TransformWithUUID } from '@viamrobotics/sdk';
2
- import { BatchedMesh, Color, Object3D, Vector3 } from 'three';
2
+ import { BatchedMesh, Color, Object3D, Vector3, type BufferGeometry } from 'three';
3
3
  import type { ValueOf } from 'type-fest';
4
4
  import type { OBB } from 'three/addons/math/OBB.js';
5
5
  export type PointsGeometry = {
@@ -16,7 +16,14 @@ export type LinesGeometry = {
16
16
  value: Float32Array;
17
17
  };
18
18
  };
19
- export type Geometries = Geometry | PointsGeometry | LinesGeometry;
19
+ export type ThreeBufferGeometry = {
20
+ center: undefined;
21
+ geometryType: {
22
+ case: 'bufferGeometry';
23
+ value: BufferGeometry;
24
+ };
25
+ };
26
+ export type Geometries = Geometry | PointsGeometry | LinesGeometry | ThreeBufferGeometry;
20
27
  export declare const SupportedShapes: {
21
28
  readonly points: "points";
22
29
  readonly line: "line";
package/dist/color.js CHANGED
@@ -102,8 +102,8 @@ export const isRGB = (color) => {
102
102
  };
103
103
  export const parseRGB = (color, defaultColor = { r: 0, g: 0, b: 0 }) => {
104
104
  if (!isRGB(color))
105
- return new Color().setRGB(defaultColor.r, defaultColor.g, defaultColor.b);
106
- return new Color().setRGB(color.r, color.g, color.b);
105
+ return new Color().setRGB(defaultColor.r > 1 ? defaultColor.r / 255 : defaultColor.r, defaultColor.g > 1 ? defaultColor.g / 255 : defaultColor.g, defaultColor.b > 1 ? defaultColor.b / 255 : defaultColor.b);
106
+ return new Color().setRGB(color.r > 1 ? color.r / 255 : color.r, color.g > 1 ? color.g / 255 : color.g, color.b > 1 ? color.b / 255 : color.b);
107
107
  };
108
108
  export const parseOpacity = (opacity, defaultOpacity = 1) => {
109
109
  if (!isNumber(opacity))
@@ -28,6 +28,7 @@
28
28
  import { FrameConfigUpdater } from '../FrameConfigUpdater.svelte'
29
29
  import { useWeblabs } from '../hooks/useWeblabs.svelte'
30
30
  import { WEBLABS_EXPERIMENTS } from '../hooks/useWeblabs.svelte'
31
+ import { useEnvironment } from '../hooks/useEnvironment.svelte'
31
32
  const { ...rest } = $props()
32
33
 
33
34
  const focused = useFocused()
@@ -38,7 +39,7 @@
38
39
  const selectedObject = useSelectedObject()
39
40
  const selectedObject3d = useSelectedObject3d()
40
41
  const weblab = useWeblabs()
41
-
42
+ const environment = useEnvironment()
42
43
  const object = $derived(focusedObject.current ?? selectedObject.current)
43
44
  const object3d = $derived(focusedObject3d.current ?? selectedObject3d.current)
44
45
  const worldPosition = $state({ x: 0, y: 0, z: 0 })
@@ -564,7 +565,7 @@
564
565
  {/if}
565
566
 
566
567
  <WeblabActive experiment={WEBLABS_EXPERIMENTS.MOTION_TOOLS_EDIT_FRAME}>
567
- {#if showEditFrameOptions}
568
+ {#if showEditFrameOptions && environment.current.isStandalone}
568
569
  <Button
569
570
  variant="danger"
570
571
  class="mt-2 w-full"
@@ -2,10 +2,11 @@
2
2
  import { useDrawAPI } from '../hooks/useDrawAPI.svelte'
3
3
  import { parsePcdInWorker, WorldObject } from '../lib'
4
4
  import { useToast, ToastVariant } from '@viamrobotics/prime-core'
5
+ import { PLYLoader } from 'three/examples/jsm/Addons.js'
5
6
 
6
7
  let { ...rest } = $props()
7
8
 
8
- const { addPoints } = useDrawAPI()
9
+ const { addPoints, addMesh } = useDrawAPI()
9
10
 
10
11
  type DropStates = 'inactive' | 'hovering' | 'loading'
11
12
 
@@ -31,6 +32,12 @@
31
32
 
32
33
  const toast = useToast()
33
34
 
35
+ const extensions = {
36
+ PCD: 'pcd',
37
+ PLY: 'ply',
38
+ }
39
+ const supportedFiles = [extensions.PCD, extensions.PLY]
40
+
34
41
  const ondrop = (event: DragEvent) => {
35
42
  event.preventDefault()
36
43
 
@@ -45,9 +52,18 @@
45
52
  for (const file of files) {
46
53
  const ext = file.name.split('.').at(-1)
47
54
 
48
- if (ext !== '.pcd') {
55
+ if (!ext) {
56
+ toast({
57
+ message: `Could not determine file extension.`,
58
+ variant: ToastVariant.Danger,
59
+ })
60
+
61
+ continue
62
+ }
63
+
64
+ if (!supportedFiles.includes(ext)) {
49
65
  toast({
50
- message: `.${ext} is not a supported file type.`,
66
+ message: `Only ${supportedFiles.map((file) => `.${file}`).join(', ')} files are supported.`,
51
67
  variant: ToastVariant.Danger,
52
68
  })
53
69
 
@@ -65,34 +81,55 @@
65
81
  })
66
82
 
67
83
  reader.addEventListener('error', () => {
68
- toast({ message: `${file.name} failed to load.`, variant: ToastVariant.Danger })
84
+ toast({
85
+ message: `${file.name} failed to load.`,
86
+ variant: ToastVariant.Danger,
87
+ })
69
88
  })
70
89
 
71
90
  reader.addEventListener('load', async (event) => {
72
91
  const arrayBuffer = event.target?.result
73
92
 
74
93
  if (!arrayBuffer || typeof arrayBuffer === 'string') {
94
+ toast({
95
+ message: `${file.name} failed to load.`,
96
+ variant: ToastVariant.Danger,
97
+ })
98
+
75
99
  return
76
100
  }
77
101
 
78
- const result = await parsePcdInWorker(new Uint8Array(arrayBuffer))
79
-
80
- addPoints(
81
- new WorldObject(
82
- file.name,
83
- undefined,
84
- undefined,
85
- {
86
- center: undefined,
87
- geometryType: {
88
- case: 'points',
89
- value: result.positions,
102
+ if (ext === extensions.PCD) {
103
+ const result = await parsePcdInWorker(new Uint8Array(arrayBuffer))
104
+
105
+ addPoints(
106
+ new WorldObject(
107
+ file.name,
108
+ undefined,
109
+ undefined,
110
+ {
111
+ center: undefined,
112
+ geometryType: {
113
+ case: 'points',
114
+ value: result.positions,
115
+ },
90
116
  },
91
- },
92
- result.colors ? { colors: result.colors } : undefined
117
+ result.colors ? { colors: result.colors } : undefined
118
+ )
93
119
  )
94
- )
95
- toast({ message: `Loaded ${file.name}`, variant: ToastVariant.Success })
120
+
121
+ toast({ message: `Loaded ${file.name}`, variant: ToastVariant.Success })
122
+ } else if (ext === extensions.PLY) {
123
+ const result = new PLYLoader().parse(arrayBuffer)
124
+ const worldObject = new WorldObject(file.name, undefined, undefined, {
125
+ center: undefined,
126
+ geometryType: { case: 'bufferGeometry', value: result },
127
+ })
128
+
129
+ addMesh(worldObject)
130
+
131
+ toast({ message: `Loaded ${file.name}`, variant: ToastVariant.Success })
132
+ }
96
133
  })
97
134
 
98
135
  reader.readAsArrayBuffer(file)
@@ -103,7 +103,12 @@
103
103
  {uuid}
104
104
  bvh={{ enabled: false }}
105
105
  >
106
- {#if geometry.geometryType.case === 'mesh'}
106
+ {#if geometry.geometryType.case === 'bufferGeometry'}
107
+ <T
108
+ is={geometry.geometryType.value}
109
+ {oncreate}
110
+ />
111
+ {:else if geometry.geometryType.case === 'mesh'}
107
112
  {@const mesh = geometry.geometryType.value.mesh}
108
113
  {@const meshGeometry = parsePlyInput(mesh)}
109
114
  <T
@@ -39,8 +39,11 @@ export const buildTreeNodes = (objects, worldStates) => {
39
39
  children: [],
40
40
  href: `/world-state/${worldState.name}/${object.name}`,
41
41
  };
42
+ const parentNode = object.referenceFrame && nodeMap.has(object.referenceFrame)
43
+ ? nodeMap.get(object.referenceFrame)
44
+ : node;
42
45
  nodeMap.set(object.name, child);
43
- node.children?.push(child);
46
+ parentNode.children?.push(child);
44
47
  }
45
48
  nodeMap.set(worldState.name, node);
46
49
  rootNodes.push(node);
@@ -6,8 +6,9 @@
6
6
  import Portal from './portal/Portal.svelte'
7
7
  import PortalTarget from './portal/PortalTarget.svelte'
8
8
  import { WorldObject } from '../WorldObject.svelte'
9
- import { useArrows } from '../hooks/useArrows.svelte'
10
9
  import { poseToDirection } from '../transform'
10
+ import { BatchedArrow } from '../three/BatchedArrow'
11
+ import { T } from '@threlte/core'
11
12
 
12
13
  interface Props {
13
14
  worldObjects: WorldObject[]
@@ -15,38 +16,36 @@
15
16
 
16
17
  let { worldObjects }: Props = $props()
17
18
 
18
- const batchedArrow = useArrows()
19
19
  const currentArrows: Record<string, { id: number; arrow: WorldObject }> = {}
20
+ const arrowBatches = $state<Record<string, BatchedArrow>>({})
20
21
 
21
22
  const arrows = $derived(worldObjects.filter((object) => object.metadata?.shape === 'arrow'))
22
23
  const objects = $derived(worldObjects.filter((object) => object.metadata?.shape !== 'arrow'))
23
24
 
24
25
  const getArrows = () => ({ ...currentArrows })
25
- const getArrow = (uuid: string) => currentArrows[uuid]
26
- const removeArrow = (uuid: string) => delete currentArrows[uuid]
26
+ const getArrow = (referenceFrame: string, uuid: string) =>
27
+ currentArrows[`${referenceFrame}:${uuid}`]
28
+
29
+ const removeArrow = (referenceFrame: string, uuid: string) => {
30
+ delete currentArrows[`${referenceFrame}:${uuid}`]
31
+ }
32
+
27
33
  const setArrow = (arrow: WorldObject) => {
28
- const currentArrow = getArrow(arrow.uuid)
34
+ const referenceFrame = arrow.referenceFrame ?? 'world'
35
+ const currentArrow = getArrow(referenceFrame, arrow.uuid)
29
36
  const color = arrow.metadata?.color ?? new Color('yellow')
30
- if (currentArrow) {
31
- batchedArrow.updateArrow(
32
- currentArrow.id,
33
- poseToDirection(arrow.pose),
34
- new Vector3(arrow.pose.x, arrow.pose.y, arrow.pose.z),
35
- 0.1,
36
- color,
37
- true
38
- )
37
+ const direction = poseToDirection(arrow.pose)
38
+ const position = new Vector3(arrow.pose.x, arrow.pose.y, arrow.pose.z)
39
+
40
+ arrowBatches[referenceFrame] ??= new BatchedArrow()
41
+ const batchedArrow = arrowBatches[referenceFrame]
39
42
 
40
- currentArrows[arrow.uuid] = { id: currentArrow.id, arrow }
43
+ if (currentArrow) {
44
+ batchedArrow.updateArrow(currentArrow.id, direction, position, 0.1, color, true)
45
+ currentArrows[`${referenceFrame}:${arrow.uuid}`] = { id: currentArrow.id, arrow }
41
46
  } else {
42
- const id = batchedArrow.addArrow(
43
- poseToDirection(arrow.pose),
44
- new Vector3(arrow.pose.x, arrow.pose.y, arrow.pose.z),
45
- 0.1,
46
- color,
47
- true
48
- )
49
- currentArrows[arrow.uuid] = { id, arrow }
47
+ const id = batchedArrow.addArrow(direction, position, 0.1, color, true)
48
+ currentArrows[`${referenceFrame}:${arrow.uuid}`] = { id, arrow }
50
49
  }
51
50
  }
52
51
 
@@ -58,12 +57,24 @@
58
57
  })
59
58
 
60
59
  Object.values(toRemove).forEach(({ id, arrow }) => {
61
- batchedArrow.removeArrow(id)
62
- removeArrow(arrow.uuid)
60
+ const referenceFrame = arrow.referenceFrame ?? 'world'
61
+ arrowBatches[referenceFrame].removeArrow(id)
62
+ removeArrow(referenceFrame, arrow.uuid)
63
63
  })
64
64
  })
65
65
  </script>
66
66
 
67
+ {#each Object.entries(arrowBatches) as [referenceFrame, batch] (referenceFrame)}
68
+ <Portal id={referenceFrame}>
69
+ <T
70
+ name={batch.object3d.name}
71
+ is={batch.object3d}
72
+ dispose={false}
73
+ bvh={{ enabled: false }}
74
+ />
75
+ </Portal>
76
+ {/each}
77
+
67
78
  {#each objects as object (object.uuid)}
68
79
  <Portal id={object.referenceFrame}>
69
80
  <Frame
@@ -2,7 +2,6 @@ import { Vector3 } from 'three';
2
2
  import { WorldObject, type PointsGeometry } from '../WorldObject.svelte';
3
3
  type ConnectionStatus = 'connecting' | 'open' | 'closed';
4
4
  interface Context {
5
- addPoints(worldObject: WorldObject<PointsGeometry>): void;
6
5
  points: WorldObject<PointsGeometry>[];
7
6
  frames: WorldObject[];
8
7
  lines: WorldObject[];
@@ -16,6 +15,8 @@ interface Context {
16
15
  lookAt: Vector3;
17
16
  animate: boolean;
18
17
  } | undefined;
18
+ addPoints(worldObject: WorldObject<PointsGeometry>): void;
19
+ addMesh(worldObject: WorldObject): void;
19
20
  clearCamera: () => void;
20
21
  }
21
22
  export declare const provideDrawAPI: () => void;
@@ -112,13 +112,7 @@ export const provideDrawAPI = () => {
112
112
  result.pose = data.center;
113
113
  return;
114
114
  }
115
- const geometry = {
116
- label: data.label,
117
- center: undefined,
118
- geometryType: {
119
- case: undefined,
120
- },
121
- };
115
+ const geometry = createGeometry();
122
116
  if ('mesh' in data) {
123
117
  geometry.geometryType.case = 'mesh';
124
118
  geometry.geometryType.value = data.mesh;
@@ -443,9 +437,6 @@ export const provideDrawAPI = () => {
443
437
  get points() {
444
438
  return points;
445
439
  },
446
- addPoints(worldObject) {
447
- points.push(worldObject);
448
- },
449
440
  get lines() {
450
441
  return lines;
451
442
  },
@@ -467,6 +458,12 @@ export const provideDrawAPI = () => {
467
458
  get camera() {
468
459
  return camera;
469
460
  },
461
+ addPoints(worldObject) {
462
+ points.push(worldObject);
463
+ },
464
+ addMesh(worldObject) {
465
+ meshes.push(worldObject);
466
+ },
470
467
  clearCamera: () => {
471
468
  camera = undefined;
472
469
  },
@@ -1,3 +1,4 @@
1
+ export declare const ENVIRONMENT_CONTEXT_KEY: unique symbol;
1
2
  interface Environemnt {
2
3
  viewerMode: 'edit' | 'monitor';
3
4
  isStandalone: boolean;
@@ -5,6 +6,7 @@ interface Environemnt {
5
6
  interface Context {
6
7
  current: Environemnt;
7
8
  }
9
+ export declare const createEnvironment: () => Context;
8
10
  export declare const provideEnvironment: () => Context;
9
11
  export declare const useEnvironment: () => Context;
10
12
  export {};
@@ -1,19 +1,23 @@
1
1
  import { getContext, setContext } from 'svelte';
2
- const key = Symbol('environment');
2
+ export const ENVIRONMENT_CONTEXT_KEY = Symbol('environment');
3
3
  const defaults = () => ({
4
4
  viewerMode: 'monitor',
5
5
  isStandalone: true,
6
6
  });
7
- export const provideEnvironment = () => {
7
+ export const createEnvironment = () => {
8
8
  const environment = $state(defaults());
9
9
  const context = {
10
10
  get current() {
11
11
  return environment;
12
12
  },
13
13
  };
14
- setContext(key, context);
14
+ return context;
15
+ };
16
+ export const provideEnvironment = () => {
17
+ const context = createEnvironment();
18
+ setContext(ENVIRONMENT_CONTEXT_KEY, context);
15
19
  return context;
16
20
  };
17
21
  export const useEnvironment = () => {
18
- return getContext(key);
22
+ return getContext(ENVIRONMENT_CONTEXT_KEY);
19
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.14.12",
3
+ "version": "0.15.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",