@viamrobotics/motion-tools 1.4.0 → 1.7.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.
package/README.md CHANGED
@@ -17,21 +17,28 @@ make setup
17
17
 
18
18
  This single command will:
19
19
 
20
- 1. Install and configure **nvm** (Node Version Manager)
21
- 2. Install the latest **Node.js LTS** version via nvm
22
- 3. Install **pnpm** package manager
23
- 4. Install **bun** runtime
20
+ 1. Install **fnm** (Fast Node Manager) and **Node.js 22**
21
+ 2. Install **pnpm** package manager
22
+ 3. Install **bun** runtime
23
+ 4. Install **Go** and **buf** (for protobuf generation)
24
24
  5. Install all project dependencies
25
+ 6. Generate protobuf code
26
+
27
+ After setup completes, add the shell configuration it prints to your shell config file (`~/.zshrc` or `~/.bashrc`), then restart your terminal.
25
28
 
26
29
  #### Manual setup
27
30
 
28
31
  If the above does not work for you, or if you prefer to install dependencies manually:
29
32
 
30
- 1. [Install nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
31
- 2. Install Node.js LTS: `nvm install --lts && nvm use --lts`
32
- 3. [Install pnpm](https://pnpm.io/installation)
33
- 4. [Install bun](https://bun.sh/docs/installation)
34
- 5. Install dependencies: `pnpm i`
33
+ 1. [Install fnm](https://github.com/Schniz/fnm#installation): `curl -fsSL https://fnm.vercel.app/install | bash`
34
+ 2. Install Node.js: `fnm install 22 && fnm use 22`
35
+ 3. [Install pnpm](https://pnpm.io/installation): `curl -fsSL https://get.pnpm.io/install.sh | sh -`
36
+ 4. [Install bun](https://bun.sh/docs/installation): `curl -fsSL https://bun.sh/install | bash`
37
+ 5. [Install Go](https://go.dev/doc/install)
38
+ 6. [Install buf](https://buf.build/docs/installation): download from GitHub releases
39
+ 7. Install Go tools: `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest`
40
+ 8. Install dependencies: `pnpm install`
41
+ 9. Generate protobufs: `make proto`
35
42
 
36
43
  ### Env files for machine configs
37
44
 
@@ -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);
@@ -26,6 +26,7 @@
26
26
  provideDrawConnectionConfig,
27
27
  type DrawConnectionConfig,
28
28
  } from '../hooks/useDrawConnectionConfig.svelte'
29
+ import Camera from './widgets/Camera.svelte'
29
30
 
30
31
  interface LocalConfigProps {
31
32
  getLocalPartConfig: () => Struct
@@ -64,6 +65,8 @@
64
65
  const settings = provideSettings()
65
66
  const environment = provideEnvironment()
66
67
 
68
+ const currentRobotCameraWidgets = $derived(settings.current.openCameraWidgets[partID] || [])
69
+
67
70
  $effect(() => {
68
71
  settings.current.enableKeybindings = enableKeybindings
69
72
  })
@@ -138,6 +141,15 @@
138
141
  <ArmPositions {@attach domPortal(root)} />
139
142
  {/if}
140
143
 
144
+ {#if !focus}
145
+ {#each currentRobotCameraWidgets as cameraName (cameraName)}
146
+ <Camera
147
+ name={cameraName}
148
+ {@attach domPortal(root)}
149
+ />
150
+ {/each}
151
+ {/if}
152
+
141
153
  <FileDrop {@attach domPortal(root)} />
142
154
  {/snippet}
143
155
  </SceneProviders>
@@ -3,12 +3,10 @@
3
3
  import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
4
4
  import { Color, Group, type Object3D } from 'three'
5
5
  import Geometry from './Geometry2.svelte'
6
- import { useWeblabs } from '../hooks/useWeblabs.svelte'
7
6
  import { useSelectedEntity } from '../hooks/useSelection.svelte'
8
7
  import { useSettings } from '../hooks/useSettings.svelte'
9
8
  import { use3DModels } from '../hooks/use3DModels.svelte'
10
9
  import { colors, darkenColor, resourceColors } from '../color'
11
- import { WEBLABS_EXPERIMENTS } from '../hooks/useWeblabs.svelte'
12
10
  import type { Entity } from 'koota'
13
11
  import { traits, useTrait } from '../ecs'
14
12
  import type { Pose } from '@viamrobotics/sdk'
@@ -31,7 +29,6 @@
31
29
  const componentModels = use3DModels()
32
30
  const selectedEntity = useSelectedEntity()
33
31
  const resourceByName = useResourceByName()
34
- const weblabs = useWeblabs()
35
32
 
36
33
  const name = useTrait(() => entity, traits.Name)
37
34
  const parent = useTrait(() => entity, traits.Parent)
@@ -57,10 +54,6 @@
57
54
  })
58
55
 
59
56
  const model = $derived.by(() => {
60
- if (!weblabs.isActive(WEBLABS_EXPERIMENTS.MOTION_TOOLS_RENDER_ARM_MODELS)) {
61
- return
62
- }
63
-
64
57
  if (!name.current) {
65
58
  return
66
59
  }
@@ -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?.()}
@@ -6,8 +6,6 @@
6
6
  import { useResourceNames } from '@viamrobotics/svelte-sdk'
7
7
  import { usePartID } from '../../hooks/usePartID.svelte'
8
8
  import { RefreshRates, useMachineSettings } from '../../hooks/useMachineSettings.svelte'
9
- import WeblabActive from '../weblab/WeblabActive.svelte'
10
- import { WEBLABS_EXPERIMENTS } from '../../hooks/useWeblabs.svelte'
11
9
  import { useGeometries } from '../../hooks/useGeometries.svelte'
12
10
  import { usePointClouds } from '../../hooks/usePointclouds.svelte'
13
11
  import { useThrelte } from '@threlte/core'
@@ -195,26 +193,24 @@
195
193
  <label class="flex items-center justify-between gap-2">
196
194
  Render stats <Switch bind:on={settings.current.renderStats} />
197
195
  </label>
198
- <WeblabActive experiment={WEBLABS_EXPERIMENTS.MOTION_TOOLS_RENDER_ARM_MODELS}>
199
- <label class="flex items-center justify-between gap-2">
200
- Render Arm Models
201
- <Select
202
- value={settings.current.renderArmModels}
203
- onchange={(event: InputEvent) => {
204
- if (event.target instanceof HTMLSelectElement) {
205
- settings.current.renderArmModels = event.target.value as
206
- | 'colliders'
207
- | 'colliders+model'
208
- | 'model'
209
- }
210
- }}
211
- >
212
- <option value="colliders">Colliders</option>
213
- <option value="colliders+model">Colliders + Model</option>
214
- <option value="model">Model</option>
215
- </Select>
216
- </label>
217
- </WeblabActive>
196
+ <label class="flex items-center justify-between gap-2">
197
+ Render Arm Models
198
+ <Select
199
+ value={settings.current.renderArmModels}
200
+ onchange={(event: InputEvent) => {
201
+ if (event.target instanceof HTMLSelectElement) {
202
+ settings.current.renderArmModels = event.target.value as
203
+ | 'colliders'
204
+ | 'colliders+model'
205
+ | 'model'
206
+ }
207
+ }}
208
+ >
209
+ <option value="colliders">Colliders</option>
210
+ <option value="colliders+model">Colliders + Model</option>
211
+ <option value="model">Model</option>
212
+ </Select>
213
+ </label>
218
214
  </div>
219
215
  </div>
220
216
  </Drawer>
@@ -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,
@@ -2,8 +2,20 @@
2
2
  import { Switch } from '@viamrobotics/prime-core'
3
3
  import Drawer from './Drawer.svelte'
4
4
  import { useSettings } from '../../hooks/useSettings.svelte'
5
+ import { useResourceByName } from '../../hooks/useResourceByName.svelte'
6
+ import { usePartID } from '../../hooks/usePartID.svelte'
5
7
 
6
8
  const settings = useSettings()
9
+ const resourceByName = useResourceByName()
10
+ const partID = usePartID()
11
+
12
+ const cameras = $derived(
13
+ Object.values(resourceByName.current).filter((resource) => resource?.subtype === 'camera')
14
+ )
15
+
16
+ const currentRobotCameraWidgets = $derived(
17
+ settings.current.openCameraWidgets[partID.current] || []
18
+ )
7
19
  </script>
8
20
 
9
21
  <Drawer name="Widgets">
@@ -17,5 +29,37 @@
17
29
  }}
18
30
  />
19
31
  </div>
32
+
33
+ <div class="mt-4">
34
+ <h3 class="text-sm"><strong>Camera Widgets</strong></h3>
35
+ {#each cameras as camera (camera?.name)}
36
+ {#if camera}
37
+ {@const isOpen = currentRobotCameraWidgets.includes(camera.name)}
38
+ <div class="flex items-center justify-between gap-4 py-2">
39
+ {camera.name}
40
+ <Switch
41
+ on={isOpen}
42
+ on:change={(event) => {
43
+ if (event.detail) {
44
+ settings.current.openCameraWidgets = {
45
+ ...settings.current.openCameraWidgets,
46
+ [partID.current]: [...currentRobotCameraWidgets, camera.name],
47
+ }
48
+ } else {
49
+ settings.current.openCameraWidgets = {
50
+ ...settings.current.openCameraWidgets,
51
+ [partID.current]: currentRobotCameraWidgets.filter(
52
+ (widget) => widget !== camera.name
53
+ ),
54
+ }
55
+ }
56
+ }}
57
+ />
58
+ </div>
59
+ {/if}
60
+ {:else}
61
+ <div class="py-2">No cameras detected</div>
62
+ {/each}
63
+ </div>
20
64
  </div>
21
65
  </Drawer>
@@ -1,18 +1,3 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const Widgets: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type Widgets = InstanceType<typeof Widgets>;
1
+ declare const Widgets: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Widgets = ReturnType<typeof Widgets>;
18
3
  export default Widgets;
@@ -0,0 +1,195 @@
1
+ <script lang="ts">
2
+ import { draggable } from '@neodrag/svelte'
3
+ import { Icon, Select } from '@viamrobotics/prime-core'
4
+ import { CameraStream, useRobotClient } from '@viamrobotics/svelte-sdk'
5
+ import { StreamClient } from '@viamrobotics/sdk'
6
+ import { useSettings } from '../../hooks/useSettings.svelte'
7
+ import { usePartID } from '../../hooks/usePartID.svelte'
8
+ import { useEnvironment } from '../../hooks/useEnvironment.svelte'
9
+
10
+ interface Resolution {
11
+ width: number
12
+ height: number
13
+ }
14
+
15
+ const { name, ...rest } = $props<{ name: string }>()
16
+
17
+ const settings = useSettings()
18
+ const partID = usePartID()
19
+ const client = useRobotClient(() => partID.current)
20
+ const environment = useEnvironment()
21
+
22
+ let dragElement = $state.raw<HTMLElement>()
23
+ let aspectRatio = $state.raw<number | undefined>(undefined)
24
+ let fps = $state(0)
25
+ let resolutions = $state<Resolution[]>([])
26
+ let currentResolution = $state<string>('')
27
+ let isLoading = $state(true)
28
+ let error = $state<string | undefined>(undefined)
29
+
30
+ let fpsInterval: ReturnType<typeof setInterval> | undefined
31
+ let fpsCounterActive = false
32
+
33
+ // Cleanup on destroy
34
+ $effect(() => {
35
+ return () => {
36
+ if (fpsInterval) clearInterval(fpsInterval)
37
+ fpsCounterActive = false
38
+ }
39
+ })
40
+
41
+ const onMediaLoad = (e: Event) => {
42
+ const target = e.target as HTMLVideoElement
43
+
44
+ // Update aspect ratio
45
+ if (target.videoWidth && target.videoHeight) {
46
+ aspectRatio = target.videoWidth / target.videoHeight
47
+ }
48
+
49
+ // Start FPS counter
50
+ if ('requestVideoFrameCallback' in target) {
51
+ if (fpsInterval) clearInterval(fpsInterval)
52
+ fpsCounterActive = false
53
+
54
+ let frameCount = 0
55
+ fpsCounterActive = true
56
+
57
+ const onFrame = () => {
58
+ if (!fpsCounterActive) return
59
+ frameCount++
60
+ target.requestVideoFrameCallback(onFrame)
61
+ }
62
+ target.requestVideoFrameCallback(onFrame)
63
+
64
+ // Update FPS state every 500ms
65
+ fpsInterval = setInterval(() => {
66
+ // FPS = frames / 0.5s = frames * 2
67
+ fps = frameCount * 2
68
+ frameCount = 0
69
+ }, 500)
70
+ }
71
+ }
72
+
73
+ // Create a single StreamClient instance per robot client
74
+ let streamClient = $derived(client.current ? new StreamClient(client.current) : undefined)
75
+
76
+ $effect(() => {
77
+ if (streamClient) {
78
+ isLoading = true
79
+ error = undefined
80
+ streamClient
81
+ .getOptions(name)
82
+ .then((options) => {
83
+ resolutions = options.map((opt) => ({ width: opt.width, height: opt.height }))
84
+ isLoading = false
85
+ })
86
+ .catch((e) => {
87
+ error = e instanceof Error ? e.message : 'Failed to get stream options'
88
+ isLoading = false
89
+ })
90
+ }
91
+ })
92
+
93
+ const handleResolutionChange = async (e: Event) => {
94
+ const target = e.target as HTMLSelectElement
95
+ if (!target.value || !streamClient) return
96
+
97
+ const [w, h] = target.value.split('x').map(Number)
98
+ if (isNaN(w) || isNaN(h)) return
99
+
100
+ try {
101
+ await streamClient.setOptions(name, w, h)
102
+ error = undefined
103
+ } catch (err) {
104
+ error = err instanceof Error ? err.message : 'Failed to set resolution'
105
+ }
106
+ }
107
+ </script>
108
+
109
+ <div
110
+ class="bg-extralight border-medium absolute top-0 left-0 z-1000 m-2 flex resize-x flex-col overflow-hidden border text-xs"
111
+ style:width="320px"
112
+ style:height="auto !important"
113
+ use:draggable={{
114
+ bounds: 'body',
115
+ handle: dragElement,
116
+ }}
117
+ {...rest}
118
+ >
119
+ <div class="flex h-full min-w-0 flex-col">
120
+ <div class="flex w-full shrink-0 items-center justify-between">
121
+ <div class="border-medium flex w-full items-center gap-1 border-b p-2">
122
+ <button bind:this={dragElement}>
123
+ <Icon name="drag" />
124
+ </button>
125
+ <h3 class="min-w-0 truncate">{name}</h3>
126
+ <div class="flex-1"></div>
127
+
128
+ {#if isLoading}
129
+ <span class="text-subtle mr-2">Loading...</span>
130
+ {:else if resolutions.length > 0}
131
+ <div class="mr-2 w-32">
132
+ <Select
133
+ bind:value={currentResolution}
134
+ onchange={handleResolutionChange}
135
+ >
136
+ <option value="">Default</option>
137
+ {#each resolutions as res (`${res.width}x${res.height}`)}
138
+ <option value={`${res.width}x${res.height}`}>{res.width}x{res.height}</option>
139
+ {/each}
140
+ </Select>
141
+ </div>
142
+ {/if}
143
+
144
+ <button
145
+ aria-label="close"
146
+ class="hover:text-default"
147
+ onclick={() => {
148
+ const widgets = settings.current.openCameraWidgets[partID.current] || []
149
+ settings.current.openCameraWidgets = {
150
+ ...settings.current.openCameraWidgets,
151
+ [partID.current]: widgets.filter((widget) => widget !== name),
152
+ }
153
+ }}
154
+ >
155
+ <Icon
156
+ name="close"
157
+ size="xs"
158
+ />
159
+ </button>
160
+ </div>
161
+ </div>
162
+
163
+ <div
164
+ class="relative min-h-0 w-full flex-1 overflow-hidden bg-black [&_img]:h-full [&_img]:w-full [&_img]:object-fill [&_video]:h-full [&_video]:w-full [&_video]:object-fill"
165
+ style:aspect-ratio={aspectRatio}
166
+ >
167
+ {#key environment.current.viewerMode === 'monitor'}
168
+ <CameraStream
169
+ {name}
170
+ partID={partID.current}
171
+ onloadedmetadata={onMediaLoad}
172
+ onload={onMediaLoad}
173
+ />
174
+ {/key}
175
+
176
+ <!-- FPS Pill -->
177
+ {#if fps > 0}
178
+ <div
179
+ class="absolute bottom-2 left-2 z-10 rounded-[3px] bg-black/30 px-1 py-0.5 text-right font-mono text-xs text-white"
180
+ >
181
+ {fps.toFixed(1)}fps
182
+ </div>
183
+ {/if}
184
+
185
+ <!-- Error display -->
186
+ {#if error}
187
+ <div
188
+ class="absolute inset-0 flex items-center justify-center bg-black/50 p-2 text-center text-white"
189
+ >
190
+ {error}
191
+ </div>
192
+ {/if}
193
+ </div>
194
+ </div>
195
+ </div>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ name: string;
3
+ };
4
+ declare const Camera: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type Camera = ReturnType<typeof Camera>;
6
+ export default Camera;
@@ -1,7 +1,6 @@
1
1
  import { ArmClient } from '@viamrobotics/sdk';
2
2
  import { createResourceClient, useResourceNames } from '@viamrobotics/svelte-sdk';
3
3
  import { getContext, setContext } from 'svelte';
4
- import { useWeblabs, WEBLABS_EXPERIMENTS } from './useWeblabs.svelte';
5
4
  import { useSettings } from './useSettings.svelte';
6
5
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
7
6
  import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
@@ -11,7 +10,6 @@ dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5
11
10
  gltfLoader.setDRACOLoader(dracoLoader);
12
11
  const key = Symbol('3d-models-context');
13
12
  export const provide3DModels = (partID) => {
14
- const weblabs = useWeblabs();
15
13
  const settings = useSettings();
16
14
  let current = $state.raw({});
17
15
  const arms = useResourceNames(partID, 'arm');
@@ -52,7 +50,7 @@ export const provide3DModels = (partID) => {
52
50
  const shouldFetchModels = settings.current.isLoaded &&
53
51
  (settings.current.renderArmModels === 'model' ||
54
52
  settings.current.renderArmModels === 'colliders+model');
55
- if (weblabs.isActive(WEBLABS_EXPERIMENTS.MOTION_TOOLS_RENDER_ARM_MODELS) && shouldFetchModels) {
53
+ if (shouldFetchModels) {
56
54
  fetch3DModels();
57
55
  }
58
56
  });
@@ -383,7 +383,8 @@ export const provideDrawAPI = () => {
383
383
  }
384
384
  else if (type === bufferTypes.DRAW_GLTF) {
385
385
  operation = 'DrawGLTF';
386
- drawGLTF(reader.buffer);
386
+ // GLTF payload starts after the 20-byte header (16 bytes UUID + 4 bytes type)
387
+ drawGLTF(reader.buffer.slice(20));
387
388
  }
388
389
  else {
389
390
  throw new Error('Invalid buffer');
@@ -18,6 +18,7 @@ export interface Settings {
18
18
  enableQueryDevtools: boolean;
19
19
  enableXR: boolean;
20
20
  enableArmPositionsWidget: boolean;
21
+ openCameraWidgets: Record<string, string[]>;
21
22
  renderStats: boolean;
22
23
  renderArmModels: 'colliders' | 'colliders+model' | 'model';
23
24
  }
@@ -21,6 +21,7 @@ const defaults = () => ({
21
21
  enableQueryDevtools: false,
22
22
  enableXR: false,
23
23
  enableArmPositionsWidget: false,
24
+ openCameraWidgets: {},
24
25
  renderStats: false,
25
26
  renderArmModels: 'colliders+model',
26
27
  });
@@ -1,6 +1,4 @@
1
- export declare const WEBLABS_EXPERIMENTS: {
2
- readonly MOTION_TOOLS_RENDER_ARM_MODELS: "MOTION_TOOLS_RENDER_ARM_MODELS";
3
- };
1
+ export declare const WEBLABS_EXPERIMENTS: {};
4
2
  export declare const WEBLABS_CONTEXT_KEY: unique symbol;
5
3
  interface Context {
6
4
  load: (experiments: string[]) => void;
@@ -1,8 +1,6 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  import { SvelteSet } from 'svelte/reactivity';
3
- export const WEBLABS_EXPERIMENTS = {
4
- MOTION_TOOLS_RENDER_ARM_MODELS: 'MOTION_TOOLS_RENDER_ARM_MODELS',
5
- };
3
+ export const WEBLABS_EXPERIMENTS = {};
6
4
  export const WEBLABS_CONTEXT_KEY = Symbol('weblabs-context');
7
5
  const getCookie = (name) => {
8
6
  const value = `; ${document.cookie}`;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.4.0",
3
+ "version": "1.7.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": {