@viamrobotics/motion-tools 1.3.4 → 1.4.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
  };
@@ -3,7 +3,7 @@
3
3
  lang="ts"
4
4
  >
5
5
  import { OrientationVector } from '../three/OrientationVector'
6
- import { Quaternion, Vector3, MathUtils, type Vector2Like } 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,
@@ -25,15 +25,13 @@
25
25
  import { usePartConfig } from '../hooks/usePartConfig.svelte'
26
26
  import { FrameConfigUpdater } from '../FrameConfigUpdater.svelte'
27
27
  import { useEnvironment } from '../hooks/useEnvironment.svelte'
28
- import { traits, useTrait } from '../ecs'
28
+ import { traits, useTrait, useWorld } from '../ecs'
29
29
  import { useResourceByName } from '../hooks/useResourceByName.svelte'
30
- import { PersistedState } from 'runed'
31
30
  import { useCameraControls } from '../hooks/useControls.svelte'
32
31
 
33
32
  const { ...rest } = $props()
34
33
 
35
- const dragPosition = new PersistedState<Vector2Like>('details-drag-position', { x: 0, y: 0 })
36
-
34
+ const world = useWorld()
37
35
  const controls = useCameraControls()
38
36
  const resourceByName = useResourceByName()
39
37
  const frames = useFrames()
@@ -54,6 +52,7 @@
54
52
  const box = useTrait(() => entity, traits.Box)
55
53
  const sphere = useTrait(() => entity, traits.Sphere)
56
54
  const capsule = useTrait(() => entity, traits.Capsule)
55
+ const removable = useTrait(() => entity, traits.Removable)
57
56
 
58
57
  const framesAPI = useTrait(() => entity, traits.FramesAPI)
59
58
  const isFrameNode = $derived(!!framesAPI.current)
@@ -232,21 +231,18 @@
232
231
  {@const ScalarAttribute = showEditFrameOptions ? MutableField : ImmutableField}
233
232
 
234
233
  <div
234
+ id="details-panel"
235
235
  class="border-medium bg-extralight absolute top-0 right-0 z-10 m-2 {showEditFrameOptions
236
236
  ? 'w-80'
237
237
  : 'w-60'} border p-2 text-xs"
238
238
  use:draggable={{
239
239
  bounds: 'body',
240
240
  handle: dragElement,
241
- defaultPosition: dragPosition.current,
242
- onDragEnd(data) {
243
- dragPosition.current = { x: data.offsetX, y: data.offsetY }
244
- },
245
241
  }}
246
242
  {...rest}
247
243
  >
248
244
  <div class="flex items-center justify-between gap-2 pb-2">
249
- <div class="flex w-[90%] items-center gap-1">
245
+ <div class="flex w-[80%] items-center gap-1">
250
246
  <button bind:this={dragElement}>
251
247
  <Icon name="drag" />
252
248
  </button>
@@ -286,6 +282,26 @@
286
282
  <p slot="description">Zoom to object</p>
287
283
  </Tooltip>
288
284
  {/if}
285
+
286
+ {#if removable.current}
287
+ <Tooltip
288
+ let:tooltipID
289
+ location="bottom"
290
+ >
291
+ <button
292
+ class="text-subtle-2"
293
+ aria-describedby={tooltipID}
294
+ onclick={() => {
295
+ if (world.has(entity)) {
296
+ entity.destroy()
297
+ }
298
+ }}
299
+ >
300
+ <Icon name="trash-can-outline" />
301
+ </button>
302
+ <p slot="description">Remove from scene</p>
303
+ </Tooltip>
304
+ {/if}
289
305
  </div>
290
306
 
291
307
  <div class="border-medium -mx-2 w-[100%+0.5rem] border-b"></div>
@@ -580,6 +596,19 @@
580
596
  </div>
581
597
  </div>
582
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}
583
612
  </div>
584
613
 
585
614
  <h3 class="text-subtle-2 pt-3 pb-2">Actions</h3>
@@ -42,7 +42,8 @@
42
42
  traits.Name(result.name),
43
43
  traits.BufferGeometry(geometry),
44
44
  traits.Points,
45
- traits.DroppedFile
45
+ traits.DroppedFile,
46
+ traits.Removable
46
47
  )
47
48
  break
48
49
  }
@@ -50,7 +51,8 @@
50
51
  world.spawn(
51
52
  traits.Name(result.name),
52
53
  traits.BufferGeometry(result.ply),
53
- traits.DroppedFile
54
+ traits.DroppedFile,
55
+ traits.Removable
54
56
  )
55
57
  break
56
58
  }
@@ -185,7 +185,7 @@
185
185
  opacity={opacity.current ?? 0.7}
186
186
  />
187
187
 
188
- {#if geo}
188
+ {#if geo && renderMode.includes('colliders')}
189
189
  <T.LineSegments
190
190
  raycast={() => null}
191
191
  bvh={{ enabled: false }}
@@ -39,7 +39,8 @@
39
39
  const entity = world.spawn(
40
40
  traits.Name(`custom geometry ${++index}`),
41
41
  traits.Pose,
42
- traits.Box({ x: 0.1, y: 0.1, z: 0.1 })
42
+ traits.Box({ x: 0.1, y: 0.1, z: 0.1 }),
43
+ traits.Removable
43
44
  )
44
45
 
45
46
  entities.add(entity)
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import type { Vector2Like } from 'three'
3
2
  import { draggable } from '@neodrag/svelte'
4
3
  import Tree from './Tree.svelte'
5
4
  import { useSelectedEntity } from '../../hooks/useSelection.svelte'
@@ -11,16 +10,13 @@
11
10
  import { useEnvironment } from '../../hooks/useEnvironment.svelte'
12
11
  import { usePartID } from '../../hooks/usePartID.svelte'
13
12
  import { usePartConfig } from '../../hooks/usePartConfig.svelte'
14
- import { traits, useWorld } from '../../ecs'
13
+ import { traits, useQuery, useWorld } from '../../ecs'
15
14
  import { IsExcluded, type Entity } from 'koota'
16
15
  import { buildTreeNodes, type TreeNode } from './buildTree'
17
16
  import { MIN_DIMENSIONS, useResizable } from '../../hooks/useResizable.svelte'
18
- import { PersistedState } from 'runed'
19
17
 
20
18
  const { ...rest } = $props()
21
19
 
22
- const dragPosition = new PersistedState<Vector2Like | undefined>('tree-drag-position', undefined)
23
-
24
20
  provideTreeExpandedContext()
25
21
 
26
22
  let container = $state.raw<HTMLDivElement>()
@@ -38,32 +34,13 @@
38
34
 
39
35
  const worldEntity = world.spawn(IsExcluded, traits.Name('World'))
40
36
 
41
- let children = $state.raw<TreeNode[]>([])
42
- let nodeMap = $state.raw<Record<string, TreeNode | undefined>>({})
43
-
44
- let pending = false
45
- const flush = () => {
46
- if (pending) return
47
- pending = true
48
-
49
- window.setTimeout(() => {
50
- const results = buildTreeNodes(world.query(traits.Name))
51
- children = results.rootNodes
52
- nodeMap = results.nodeMap
53
- pending = false
54
- })
55
- }
37
+ const allEntities = useQuery(traits.Name)
56
38
 
57
- world.onAdd(traits.Name, flush)
58
- world.onAdd(traits.Parent, flush)
59
- world.onRemove(traits.Name, flush)
60
- world.onRemove(traits.Parent, flush)
61
- world.onChange(traits.Name, flush)
62
- world.onChange(traits.Parent, flush)
39
+ const { rootNodes, nodeMap } = $derived(buildTreeNodes(allEntities.current))
63
40
 
64
41
  const rootNode = $derived<TreeNode>({
65
42
  entity: worldEntity,
66
- children,
43
+ children: rootNodes,
67
44
  })
68
45
 
69
46
  $effect(() => {
@@ -83,10 +60,6 @@
83
60
  use:draggable={{
84
61
  bounds: 'body',
85
62
  handle: dragElement,
86
- defaultPosition: dragPosition.current,
87
- onDragEnd(data) {
88
- dragPosition.current = { x: Math.max(data.offsetX, 0), y: Math.max(data.offsetY, 0) }
89
- },
90
63
  }}
91
64
  {...rest}
92
65
  >
@@ -1,19 +1,12 @@
1
1
  <script lang="ts">
2
- import type { Vector2Like } from 'three'
3
2
  import { draggable } from '@neodrag/svelte'
4
3
  import { formatNumeric } from '../../format'
5
4
  import Table from '../shared/Table.svelte'
6
5
  import { useArmClient } from '../../hooks/useArmClient.svelte'
7
6
  import { Icon, Label, Select } from '@viamrobotics/prime-core'
8
- import { PersistedState } from 'runed'
9
7
 
10
8
  const { ...rest } = $props()
11
9
 
12
- const dragPosition = new PersistedState<Vector2Like | undefined>(
13
- 'details-drag-position',
14
- undefined
15
- )
16
-
17
10
  let dragElement = $state.raw<HTMLElement>()
18
11
 
19
12
  const armClient = useArmClient()
@@ -28,10 +21,6 @@
28
21
  use:draggable={{
29
22
  bounds: 'body',
30
23
  handle: dragElement,
31
- defaultPosition: dragPosition.current,
32
- onDragEnd(data) {
33
- dragPosition.current = { x: data.offsetX, y: data.offsetY }
34
- },
35
24
  }}
36
25
  {...rest}
37
26
  >
@@ -126,6 +126,10 @@ export declare const PointSize: import("koota").Trait<() => number>;
126
126
  */
127
127
  export declare const LineWidth: import("koota").Trait<() => number>;
128
128
  export declare const ReferenceFrame: import("koota").TagTrait;
129
+ /**
130
+ * This entity can be safetly removed from the scene by the user
131
+ */
132
+ export declare const Removable: import("koota").TagTrait;
129
133
  export declare const Geometry: (geometry: ViamGeometry) => import("koota").TagTrait | [import("koota").Trait<{
130
134
  x: number;
131
135
  y: number;
@@ -78,6 +78,10 @@ export const PointSize = trait(() => 10);
78
78
  */
79
79
  export const LineWidth = trait(() => 5);
80
80
  export const ReferenceFrame = trait();
81
+ /**
82
+ * This entity can be safetly removed from the scene by the user
83
+ */
84
+ export const Removable = trait();
81
85
  export const Geometry = (geometry) => {
82
86
  if (geometry.geometryType.case === 'box') {
83
87
  return Box(createBox(geometry.geometryType.value));
@@ -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).
@@ -144,7 +149,7 @@ export const provideDrawAPI = () => {
144
149
  if (frame.geometry) {
145
150
  entityTraits.push(geometryTrait());
146
151
  }
147
- entityTraits.push(traits.Name(name), traits.Pose(pose), traits.DrawAPI, traits.ReferenceFrame);
152
+ entityTraits.push(traits.Name(name), traits.Pose(pose), traits.DrawAPI, traits.ReferenceFrame, traits.Removable);
148
153
  const entity = world.spawn(...entityTraits);
149
154
  entities.set(name, entity);
150
155
  }
@@ -178,7 +183,7 @@ export const provideDrawAPI = () => {
178
183
  if (parent && parent !== 'world') {
179
184
  entityTraits.push(traits.Parent(parent));
180
185
  }
181
- entityTraits.push(traits.Name(data.label ?? ++geometryIndex), traits.Pose(pose), traits.Color(colorUtil.set(color)), geometryTrait(), traits.DrawAPI);
186
+ entityTraits.push(traits.Name(data.label ?? ++geometryIndex), traits.Pose(pose), traits.Color(colorUtil.set(color)), geometryTrait(), traits.DrawAPI, traits.Removable);
182
187
  const entity = world.spawn(...entityTraits);
183
188
  entities.set(name, entity);
184
189
  };
@@ -201,7 +206,7 @@ export const provideDrawAPI = () => {
201
206
  existing.set(traits.LinePositions, points);
202
207
  return;
203
208
  }
204
- const entity = world.spawn(traits.Name(name), traits.Color(colorUtil.set(color)), traits.LinePositions(points), traits.DrawAPI);
209
+ const entity = world.spawn(traits.Name(name), traits.Color(colorUtil.set(color)), traits.LinePositions(points), traits.DrawAPI, traits.Removable);
205
210
  entities.set(name, entity);
206
211
  };
207
212
  const vec3 = new Vector3();
@@ -211,7 +216,7 @@ export const provideDrawAPI = () => {
211
216
  const nColors = reader.read();
212
217
  const arrowHeadAtPose = reader.read();
213
218
  const entities = [];
214
- const entity = world.spawn(traits.Name(`Arrow group ${++poseIndex}`), traits.Positions(reader.readF32Array(nPoints * STRIDE.ARROWS)), traits.Colors(reader.readU8Array(nColors * STRIDE.COLORS_RGB)), traits.Arrows({ headAtPose: arrowHeadAtPose === 1 }), traits.DrawAPI);
219
+ const entity = world.spawn(traits.Name(`Arrow group ${++poseIndex}`), traits.Positions(reader.readF32Array(nPoints * STRIDE.ARROWS)), traits.Colors(reader.readU8Array(nColors * STRIDE.COLORS_RGB)), traits.Arrows({ headAtPose: arrowHeadAtPose === 1 }), traits.DrawAPI, traits.Removable);
215
220
  entities.push(entity);
216
221
  };
217
222
  const drawPoints = async (reader) => {
@@ -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);
@@ -251,7 +264,7 @@ export const provideDrawAPI = () => {
251
264
  }
252
265
  }
253
266
  const geometry = createBufferGeometry(positions, colors);
254
- world.spawn(traits.Name(label), traits.Color(colorUtil.set(r, g, b)), traits.BufferGeometry(geometry), traits.Points, traits.DrawAPI);
267
+ world.spawn(traits.Name(label), traits.Color(colorUtil.set(r, g, b)), traits.BufferGeometry(geometry), traits.Points, traits.DrawAPI, traits.Removable);
255
268
  };
256
269
  const drawLine = async (reader) => {
257
270
  // Read label length
@@ -279,7 +292,7 @@ export const provideDrawAPI = () => {
279
292
  points[i + 1] = reader.read();
280
293
  points[i + 2] = reader.read();
281
294
  }
282
- world.spawn(traits.Name(label), traits.Color({ r, g, b }), traits.LinePositions(points), traits.PointColor({ r: dotR, g: dotG, b: dotB }), traits.DrawAPI);
295
+ world.spawn(traits.Name(label), traits.Color({ r, g, b }), traits.LinePositions(points), traits.PointColor({ r: dotR, g: dotG, b: dotB }), traits.DrawAPI, traits.Removable);
283
296
  };
284
297
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
285
298
  const drawGeometries = (geometries, colors, parent) => {
@@ -293,14 +306,16 @@ export const provideDrawAPI = () => {
293
306
  const blob = new Blob([buffer], { type: 'model/gltf-binary' });
294
307
  const url = URL.createObjectURL(blob);
295
308
  const gltf = await loader.loadAsync(url);
296
- world.spawn(traits.Name(gltf.scene.name), traits.GLTF({ source: { gltf }, animationName: '' }), traits.DrawAPI);
309
+ world.spawn(traits.Name(gltf.scene.name), traits.GLTF({ source: { gltf }, animationName: '' }), traits.DrawAPI, traits.Removable);
297
310
  URL.revokeObjectURL(url);
298
311
  };
299
312
  const remove = (names) => {
300
313
  for (const name of names) {
301
314
  for (const entity of world.query(traits.DrawAPI)) {
302
315
  if (entity.get(traits.Name) === name) {
303
- entity.destroy();
316
+ if (world.has(entity)) {
317
+ entity.destroy();
318
+ }
304
319
  entities.delete(name);
305
320
  }
306
321
  }
@@ -308,7 +323,9 @@ export const provideDrawAPI = () => {
308
323
  };
309
324
  const removeAll = () => {
310
325
  for (const entity of world.query(traits.DrawAPI)) {
311
- entity.destroy();
326
+ if (world.has(entity)) {
327
+ entity.destroy();
328
+ }
312
329
  }
313
330
  entities.clear();
314
331
  geometryIndex = 0;
@@ -1,6 +1,6 @@
1
1
  export declare const ENVIRONMENT_CONTEXT_KEY: unique symbol;
2
2
  interface Environemnt {
3
- viewerMode: 'edit' | 'monitor';
3
+ viewerMode: 'edit' | 'monitor' | 'focus';
4
4
  isStandalone: boolean;
5
5
  }
6
6
  interface Context {
@@ -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(() => {
@@ -161,7 +161,9 @@ export const providePointcloudObjects = (partID) => {
161
161
  // Clean up old entities
162
162
  for (const [label, entity] of entities) {
163
163
  if (!active[label]) {
164
- entity.destroy();
164
+ if (world.has(entity)) {
165
+ entity.destroy();
166
+ }
165
167
  entities.delete(label);
166
168
  }
167
169
  }
@@ -117,7 +117,9 @@ export const providePointclouds = (partID) => {
117
117
  // Clean up old entities
118
118
  for (const [name, entity] of entities) {
119
119
  if (!queryMap[name]?.data) {
120
- entity.destroy();
120
+ if (world.has(entity)) {
121
+ entity.destroy();
122
+ }
121
123
  entities.delete(name);
122
124
  }
123
125
  }
@@ -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();
@@ -1,13 +1,15 @@
1
- import { isInstanceOf, useThrelte } from '@threlte/core';
2
- import { getContext, setContext } from 'svelte';
1
+ import { useThrelte } from '@threlte/core';
2
+ import { getContext, setContext, untrack } from 'svelte';
3
3
  import { Object3D } from 'three';
4
4
  import { traits, useWorld } from '../ecs';
5
+ import { useEnvironment } from './useEnvironment.svelte';
5
6
  const selectedKey = Symbol('selected-frame-context');
6
7
  const focusedKey = Symbol('focused-frame-context');
7
8
  const focusedObject3dKey = Symbol('focused-object-3d-context');
8
9
  export const provideSelection = () => {
9
10
  const world = useWorld();
10
11
  const { scene } = useThrelte();
12
+ const environment = useEnvironment();
11
13
  let selected = $state.raw();
12
14
  let selectedInstance = $state();
13
15
  let focused = $state.raw();
@@ -46,18 +48,15 @@ export const provideSelection = () => {
46
48
  },
47
49
  };
48
50
  setContext(focusedKey, focusedEntityContext);
49
- const focusedObject3d = $derived.by(() => {
50
- const name = focused?.get(traits.Name);
51
- if (!name) {
52
- return;
51
+ const focusedObject3d = $derived(focused ? scene.getObjectByName(focused)?.clone() : undefined);
52
+ $effect(() => {
53
+ const previousMode = untrack(() => environment.current.viewerMode);
54
+ if (focusedObject3d) {
55
+ environment.current.viewerMode = 'focus';
56
+ return () => {
57
+ environment.current.viewerMode = previousMode;
58
+ };
53
59
  }
54
- const object = scene.getObjectByName(name)?.clone();
55
- object?.traverse((child) => {
56
- if (isInstanceOf(child, 'LineSegments')) {
57
- child.raycast = () => null;
58
- }
59
- });
60
- return object;
61
60
  });
62
61
  setContext(focusedObject3dKey, {
63
62
  get current() {
@@ -71,7 +71,9 @@ const createWorldState = (client) => {
71
71
  const entity = entities.get(uuid);
72
72
  if (!entity)
73
73
  return;
74
- entity.destroy();
74
+ if (world.has(entity)) {
75
+ entity.destroy();
76
+ }
75
77
  entities.delete(uuid);
76
78
  };
77
79
  const updateEntity = (transform, changes) => {
@@ -202,7 +204,9 @@ const createWorldState = (client) => {
202
204
  });
203
205
  return () => {
204
206
  for (const [, entity] of entities) {
205
- entity.destroy();
207
+ if (world.has(entity)) {
208
+ entity.destroy();
209
+ }
206
210
  }
207
211
  };
208
212
  };
@@ -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
@@ -83,6 +83,7 @@ const spawnTransformEntity = (world, transform) => {
83
83
  traits.Geometry(transform.physicalObject ?? Geometry.fromJson({})),
84
84
  traits.Center(transform.physicalObject?.center),
85
85
  traits.SnapshotAPI,
86
+ traits.Removable,
86
87
  ];
87
88
  const poseInFrame = transform.poseInObserverFrame;
88
89
  entityTraits.push(traits.Pose(poseInFrame?.pose));
@@ -117,7 +118,7 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
117
118
  if (colors) {
118
119
  entityTraits.push(traits.Colors(colors));
119
120
  }
120
- const entity = world.spawn(...entityTraits, traits.SnapshotAPI, traits.Arrows({ headAtPose: true }), traits.Instances({ count: poses.length / STRIDE.ARROWS }));
121
+ const entity = world.spawn(...entityTraits, traits.Arrows({ headAtPose: true }), traits.Instances({ count: poses.length / STRIDE.ARROWS }), traits.SnapshotAPI, traits.Removable);
121
122
  entities.push(entity);
122
123
  }
123
124
  else if (geometryType?.case === 'model') {
@@ -129,7 +130,7 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
129
130
  if (parent) {
130
131
  rootEntityTraits.push(traits.Parent(parent));
131
132
  }
132
- const rootEntity = world.spawn(...rootEntityTraits, traits.SnapshotAPI);
133
+ const rootEntity = world.spawn(...rootEntityTraits, traits.SnapshotAPI, traits.Removable);
133
134
  entities.push(rootEntity);
134
135
  let i = 1;
135
136
  for (const asset of geometryType.value.assets) {
@@ -152,7 +153,7 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
152
153
  animationName: geometryType.value.animationName ?? '',
153
154
  }));
154
155
  }
155
- const entity = world.spawn(...entityTraits, traits.SnapshotAPI);
156
+ const entity = world.spawn(...entityTraits, traits.SnapshotAPI, traits.Removable);
156
157
  entities.push(entity);
157
158
  }
158
159
  }
@@ -191,9 +192,7 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
191
192
  for (let i = 0, l = positions.length; i < l; i += 1) {
192
193
  positions[i] *= 0.001;
193
194
  }
194
- const colors = drawing.metadata?.colors
195
- ? rgbaBytesToFloat32(drawing.metadata.colors)
196
- : undefined;
195
+ const colors = drawing.metadata?.colors;
197
196
  const geometry = createBufferGeometry(positions, colors);
198
197
  entityTraits.push(traits.BufferGeometry(geometry));
199
198
  if (geometryType.value.pointSize) {
@@ -227,7 +226,7 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
227
226
  }
228
227
  entityTraits.push(traits.LinePositions(points));
229
228
  }
230
- const entity = world.spawn(...entityTraits, traits.SnapshotAPI);
229
+ const entity = world.spawn(...entityTraits, traits.SnapshotAPI, traits.Removable);
231
230
  entities.push(entity);
232
231
  }
233
232
  return entities;
@@ -1,6 +1,6 @@
1
1
  import { RawShaderMaterial, FrontSide, Group, InstancedBufferAttribute, DynamicDrawUsage, Mesh, BufferGeometry, InstancedInterleavedBuffer, InterleavedBufferAttribute, Material, Color, Vector3, Box3, } from 'three';
2
- import vertexShader from './vertex.glsl?raw';
3
- import fragmentShader from './fragment.glsl?raw';
2
+ import vertexShader from './vertex.glsl';
3
+ import fragmentShader from './fragment.glsl';
4
4
  import { createHeadGeometry, createShaftGeometry, toInstanced } from './geometry';
5
5
  import { computeBoundingBox } from './box';
6
6
  const defaults = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "@skeletonlabs/skeleton": "3.2.0",
18
18
  "@skeletonlabs/skeleton-svelte": "1.5.1",
19
19
  "@sveltejs/adapter-static": "3.0.9",
20
- "@sveltejs/kit": "2.37.0",
20
+ "@sveltejs/kit": "2.49.5",
21
21
  "@sveltejs/package": "2.5.0",
22
22
  "@sveltejs/vite-plugin-svelte": "6.1.4",
23
23
  "@tailwindcss/forms": "0.5.10",
@@ -68,10 +68,14 @@
68
68
  "typescript-eslint": "8.42.0",
69
69
  "vite": "7.1.11",
70
70
  "vite-plugin-devtools-json": "1.0.0",
71
+ "vite-plugin-glsl": "^1.5.5",
71
72
  "vite-plugin-mkcert": "1.17.8",
72
73
  "vitest": "3.2.4"
73
74
  },
74
75
  "peerDependencies": {
76
+ "@ag-grid-community/client-side-row-model": ">=32.3.0",
77
+ "@ag-grid-community/core": ">=32.3.0",
78
+ "@ag-grid-community/styles": ">=32.3.0",
75
79
  "@dimforge/rapier3d-compat": ">=0.17",
76
80
  "@threlte/core": ">=8",
77
81
  "@threlte/extras": ">=9",
@@ -81,6 +85,7 @@
81
85
  "@viamrobotics/sdk": ">=0.38",
82
86
  "@viamrobotics/svelte-sdk": ">=0.1",
83
87
  "@zag-js/collapsible": ">=1",
88
+ "@zag-js/dialog": ">=1.31",
84
89
  "@zag-js/floating-panel": ">=1",
85
90
  "@zag-js/svelte": ">=1",
86
91
  "@zag-js/tree-view": ">=1",
@@ -89,11 +94,7 @@
89
94
  "lucide-svelte": ">=0.511",
90
95
  "runed": ">=0.28",
91
96
  "svelte": ">=5",
92
- "svelte-virtuallists": ">=1",
93
- "@ag-grid-community/client-side-row-model": ">=32.3.0",
94
- "@ag-grid-community/core": ">=32.3.0",
95
- "@ag-grid-community/styles": ">=32.3.0",
96
- "@zag-js/dialog": ">=1.31"
97
+ "svelte-virtuallists": ">=1"
97
98
  },
98
99
  "engines": {
99
100
  "node": ">=22.12.0"