@viamrobotics/motion-tools 1.26.2 → 1.27.1

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,13 @@
1
+ export declare class AssertionError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ /**
5
+ * Assert that a value is defined.
6
+ *
7
+ * @example
8
+ * const stringify = (value: number | undefined): number => {
9
+ * assertExists(value)
10
+ * return `${value}` // TS now knows that value is of type `number`
11
+ * }
12
+ */
13
+ export declare const assertExists: <T>(value: T, message: string) => asserts value is NonNullable<T>;
package/dist/assert.js ADDED
@@ -0,0 +1,20 @@
1
+ export class AssertionError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'AssertionError';
5
+ }
6
+ }
7
+ /**
8
+ * Assert that a value is defined.
9
+ *
10
+ * @example
11
+ * const stringify = (value: number | undefined): number => {
12
+ * assertExists(value)
13
+ * return `${value}` // TS now knows that value is of type `number`
14
+ * }
15
+ */
16
+ export const assertExists = (value, message) => {
17
+ if (value === null || value === undefined) {
18
+ throw new AssertionError(message);
19
+ }
20
+ };
@@ -8,6 +8,7 @@
8
8
  import { PortalTarget } from '@threlte/extras'
9
9
  import { useXR } from '@threlte/xr'
10
10
  import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
11
+ import { ThemeUtils } from 'svelte-tweakpane-ui'
11
12
 
12
13
  import type { CameraPose } from '../hooks/useControls.svelte'
13
14
 
@@ -90,10 +91,6 @@
90
91
  const currentRobotCameraWidgets = $derived(settings.current.openCameraWidgets[partID] || [])
91
92
  const { isPresenting } = useXR()
92
93
 
93
- $effect(() => {
94
- environment.current.inputBindingsEnabled = inputBindingsEnabled
95
- })
96
-
97
94
  createPartIDContext(() => partID)
98
95
  provideDrawConnectionConfig(() => drawConnectionConfig)
99
96
  provideWeblabs()
@@ -106,9 +103,18 @@
106
103
  () => localConfigProps
107
104
  )
108
105
 
109
- $effect.pre(() => {
106
+ $effect(() => {
107
+ environment.current.inputBindingsEnabled = inputBindingsEnabled
110
108
  environment.current.isStandalone = !localConfigProps
111
109
  })
110
+
111
+ $effect(() => {
112
+ ThemeUtils.setGlobalDefaultTheme({
113
+ ...ThemeUtils.presets.light,
114
+ baseBackgroundColor: '#fbfbfc',
115
+ baseShadowColor: 'transparent',
116
+ })
117
+ })
112
118
  </script>
113
119
 
114
120
  {#if settings.current.enableQueryDevtools}
@@ -2,17 +2,13 @@
2
2
  import type { Entity } from 'koota'
3
3
 
4
4
  import { T } from '@threlte/core'
5
- import { Portal } from '@threlte/extras'
6
5
  import { Color, Quaternion, Vector3 } from 'three'
7
6
 
8
- import { hierarchy, traits, useWorld } from '../ecs'
7
+ import { traits, useWorld } from '../ecs'
9
8
  import { BatchedArrow } from '../three/BatchedArrow'
10
9
  import { OrientationVector } from '../three/OrientationVector'
11
10
 
12
- const arrowBatchMap = $state<Record<string, BatchedArrow>>({
13
- world: new BatchedArrow(),
14
- })
15
- const batchEntries = $derived(Object.entries(arrowBatchMap))
11
+ const batched = new BatchedArrow()
16
12
 
17
13
  const world = useWorld()
18
14
 
@@ -24,24 +20,19 @@
24
20
  const tempOv = new OrientationVector()
25
21
 
26
22
  /**
27
- * Decompose the matrix directly into the arrow's direction
28
- * (OV components from the rotation) and origin (translation)
23
+ * Decompose the entity's `WorldMatrix` directly into the arrow's world
24
+ * origin (translation) and direction (OV components from the rotation).
29
25
  */
30
26
  const decompose = (entity: Entity): boolean => {
31
- const matrix = entity.get(traits.Matrix)
32
- if (!matrix) return false
33
- matrix.decompose(origin, tempQuat, tempScale)
27
+ const worldMatrix = entity.get(traits.WorldMatrix)
28
+ if (!worldMatrix) return false
29
+ worldMatrix.decompose(origin, tempQuat, tempScale)
34
30
  tempOv.setFromQuaternion(tempQuat)
35
31
  direction.set(tempOv.x, tempOv.y, tempOv.z)
36
32
  return true
37
33
  }
38
34
 
39
35
  const onAdd = (entity: Entity) => {
40
- const parent = hierarchy.getParentName(entity) ?? 'world'
41
-
42
- arrowBatchMap[parent] ??= new BatchedArrow()
43
- const batched = arrowBatchMap[parent]
44
-
45
36
  const colorRGB = entity.get(traits.Color)
46
37
 
47
38
  if (!decompose(entity)) {
@@ -58,63 +49,54 @@
58
49
  entity.add(traits.Instance({ instanceID, meshID: batched.mesh.id }))
59
50
  }
60
51
 
61
- const onMatrixChange = (entity: Entity) => {
52
+ const onWorldMatrixChange = (entity: Entity) => {
62
53
  if (!entity.has(traits.Arrow)) return
63
54
 
64
- const parent = hierarchy.getParentName(entity) ?? 'world'
65
- const batch = arrowBatchMap[parent]
66
55
  const instanceID = entity.get(traits.Instance)?.instanceID
67
56
 
68
57
  if (instanceID && instanceID !== -1 && decompose(entity)) {
69
- batch?.updateArrow(instanceID, direction, origin)
58
+ batched.updateArrow(instanceID, direction, origin)
70
59
  }
71
60
  }
72
61
 
73
62
  const onColorChange = (entity: Entity) => {
74
63
  if (!entity.has(traits.Arrow)) return
75
64
 
76
- const parent = hierarchy.getParentName(entity) ?? 'world'
77
- const batch = arrowBatchMap[parent]
78
65
  const instanceID = entity.get(traits.Instance)?.instanceID
79
66
  const colorRGB = entity.get(traits.Color)
80
67
 
81
68
  if (instanceID && instanceID !== -1 && colorRGB) {
82
69
  color.set(colorRGB.r, colorRGB.g, colorRGB.b)
83
- batch.mesh.setColorAt(instanceID, color)
70
+ batched.mesh.setColorAt(instanceID, color)
84
71
  }
85
72
  }
86
73
 
87
74
  const onInstanceRemove = (entity: Entity) => {
88
75
  const instance = entity.get(traits.Instance)
89
-
90
- for (const [, batch] of batchEntries) {
91
- if (batch.mesh.id === instance?.meshID) {
92
- batch.removeArrow(instance.instanceID)
93
- }
76
+ if (instance && instance.meshID === batched.mesh.id) {
77
+ batched.removeArrow(instance.instanceID)
94
78
  }
95
79
  }
96
80
 
97
81
  $effect(() => {
98
82
  const unsubAdd = world.onAdd(traits.Arrow, onAdd)
99
83
  const unsubRemove = world.onRemove(traits.Instance, onInstanceRemove)
100
- const unsubMatrixChange = world.onChange(traits.Matrix, onMatrixChange)
84
+ const unsubMatrixAdd = world.onAdd(traits.WorldMatrix, onWorldMatrixChange)
85
+ const unsubMatrixChange = world.onChange(traits.WorldMatrix, onWorldMatrixChange)
101
86
  const unsubColorChange = world.onChange(traits.Color, onColorChange)
102
87
 
103
88
  return () => {
104
89
  unsubAdd()
105
90
  unsubRemove()
91
+ unsubMatrixAdd()
106
92
  unsubMatrixChange()
107
93
  unsubColorChange()
108
94
  }
109
95
  })
110
96
  </script>
111
97
 
112
- {#each batchEntries as [parent, batch] (parent)}
113
- <Portal id={parent}>
114
- <T
115
- is={batch.mesh}
116
- dispose={false}
117
- bvh={{ enabled: false }}
118
- />
119
- </Portal>
120
- {/each}
98
+ <T
99
+ is={batched.mesh}
100
+ dispose={false}
101
+ bvh={{ enabled: false }}
102
+ />
@@ -1,14 +1,13 @@
1
1
  <script lang="ts">
2
2
  import type { Entity } from 'koota'
3
3
 
4
- import { T } from '@threlte/core'
5
- import { Portal } from '@threlte/extras'
4
+ import { T, useThrelte } from '@threlte/core'
6
5
 
7
6
  import type { InstancedArrows } from '../../../three/InstancedArrows/InstancedArrows'
8
7
 
9
8
  import AxesHelper from '../../AxesHelper.svelte'
10
9
  import { useEntityEvents } from '../hooks/useEntityEvents.svelte'
11
- import { traits, useParentName, useTrait } from '../../../ecs'
10
+ import { traits, useTrait } from '../../../ecs'
12
11
  import { useFocusedEntity, useSelectedEntity } from '../../../hooks/useSelection.svelte'
13
12
  import { meshBoundsRaycast, raycast } from '../../../three/InstancedArrows/raycast'
14
13
 
@@ -19,7 +18,8 @@
19
18
 
20
19
  let { entity, arrows }: Props = $props()
21
20
 
22
- const parent = useParentName(() => entity)
21
+ const { invalidate } = useThrelte()
22
+ const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
23
23
  const invisible = useTrait(() => entity, traits.Invisible)
24
24
  const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
25
25
 
@@ -35,32 +35,38 @@
35
35
  }
36
36
  return meshBoundsRaycast
37
37
  })
38
+
39
+ $effect.pre(() => {
40
+ arrows.matrixAutoUpdate = false
41
+ if (!worldMatrix.current) return
42
+ arrows.matrix.copy(worldMatrix.current)
43
+ arrows.updateMatrixWorld()
44
+ invalidate()
45
+ })
38
46
  </script>
39
47
 
40
- <Portal id={parent.current}>
48
+ <T
49
+ is={arrows}
50
+ name={entity}
51
+ {...events}
52
+ raycast={raycastFunction}
53
+ visible={invisible.current !== true}
54
+ >
41
55
  <T
42
- is={arrows}
43
- name={entity}
44
- {...events}
45
- raycast={raycastFunction}
46
- visible={invisible.current !== true}
47
- >
48
- <T
49
- is={arrows.headMesh}
50
- bvh={{ enabled: false }}
51
- raycast={() => null}
52
- />
53
- <T
54
- is={arrows.shaftMesh}
55
- bvh={{ enabled: false }}
56
- raycast={() => null}
56
+ is={arrows.headMesh}
57
+ bvh={{ enabled: false }}
58
+ raycast={() => null}
59
+ />
60
+ <T
61
+ is={arrows.shaftMesh}
62
+ bvh={{ enabled: false }}
63
+ raycast={() => null}
64
+ />
65
+ {#if showAxesHelper.current}
66
+ <AxesHelper
67
+ name={entity}
68
+ width={3}
69
+ length={0.1}
57
70
  />
58
- {#if showAxesHelper.current}
59
- <AxesHelper
60
- name={entity}
61
- width={3}
62
- length={0.1}
63
- />
64
- {/if}
65
- </T>
66
- </Portal>
71
+ {/if}
72
+ </T>
@@ -14,14 +14,12 @@ Renders a Viam Frame object
14
14
  import type { Snippet } from 'svelte'
15
15
 
16
16
  import { T, useThrelte } from '@threlte/core'
17
- import { Portal, PortalTarget } from '@threlte/extras'
18
17
  import { Group, type Object3D } from 'three'
19
18
 
20
19
  import { asColor } from '../../buffer'
21
20
  import { colors, resourceColors } from '../../color'
22
- import { traits, useParentName, useTrait } from '../../ecs'
21
+ import { traits, useTrait } from '../../ecs'
23
22
  import { useResourceByName } from '../../hooks/useResourceByName.svelte'
24
- import { composeLocalMatrix } from '../../transform'
25
23
 
26
24
  import { useEntityEvents } from './hooks/useEntityEvents.svelte'
27
25
  import Mesh from './Mesh.svelte'
@@ -37,12 +35,9 @@ Renders a Viam Frame object
37
35
  const resourceByName = useResourceByName()
38
36
 
39
37
  const name = useTrait(() => entity, traits.Name)
40
- const parent = useParentName(() => entity)
41
38
  const entityColors = useTrait(() => entity, traits.Colors)
42
39
  const entityColor = useTrait(() => entity, traits.Color)
43
- const matrix = useTrait(() => entity, traits.Matrix)
44
- const editedMatrix = useTrait(() => entity, traits.EditedMatrix)
45
- const liveMatrix = useTrait(() => entity, traits.LiveMatrix)
40
+ const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
46
41
  const center = useTrait(() => entity, traits.Center)
47
42
  const invisible = useTrait(() => entity, traits.Invisible)
48
43
 
@@ -71,15 +66,9 @@ Renders a Viam Frame object
71
66
  group.matrixAutoUpdate = false
72
67
 
73
68
  $effect.pre(() => {
74
- if (liveMatrix.current && matrix.current && editedMatrix.current) {
75
- composeLocalMatrix(liveMatrix.current, matrix.current, editedMatrix.current, group.matrix)
76
- } else if (editedMatrix.current) {
77
- group.matrix.copy(editedMatrix.current)
78
- } else if (matrix.current) {
79
- group.matrix.copy(matrix.current)
80
- } else {
81
- return
82
- }
69
+ if (!worldMatrix.current) return
70
+
71
+ group.matrix.copy(worldMatrix.current)
83
72
 
84
73
  /**
85
74
  * Keep position/quaternion/scale in sync with matrix so TransformControls
@@ -94,22 +83,16 @@ Renders a Viam Frame object
94
83
  })
95
84
  </script>
96
85
 
97
- <Portal id={parent.current}>
98
- <T
99
- is={group}
100
- visible={invisible.current !== true}
101
- >
102
- <Mesh
103
- {entity}
104
- {color}
105
- {...events}
106
- center={center.current}
107
- />
108
-
109
- {#if name.current}
110
- <PortalTarget id={name.current} />
111
- {/if}
112
-
113
- {@render children?.({ ref: group })}
114
- </T>
115
- </Portal>
86
+ <T
87
+ is={group}
88
+ visible={invisible.current !== true}
89
+ >
90
+ <Mesh
91
+ {entity}
92
+ {color}
93
+ {...events}
94
+ center={center.current}
95
+ />
96
+
97
+ {@render children?.({ ref: group })}
98
+ </T>
@@ -16,10 +16,10 @@
16
16
  import type { Snippet } from 'svelte'
17
17
 
18
18
  import { T, type Props as ThrelteProps } from '@threlte/core'
19
- import { Portal, PortalTarget, type ThrelteGltf, useGltfAnimations } from '@threlte/extras'
19
+ import { type ThrelteGltf, useGltfAnimations } from '@threlte/extras'
20
20
  import { Group, type Object3D } from 'three'
21
21
 
22
- import { traits, useParentName, useTrait } from '../../ecs'
22
+ import { traits, useTrait } from '../../ecs'
23
23
 
24
24
  import AxesHelper from '../AxesHelper.svelte'
25
25
  import { useEntityEvents } from './hooks/useEntityEvents.svelte'
@@ -33,11 +33,8 @@
33
33
 
34
34
  const { gltf, actions } = useGltfAnimations()
35
35
 
36
- const name = useTrait(() => entity, traits.Name)
37
- const parent = useParentName(() => entity)
38
- const matrix = useTrait(() => entity, traits.Matrix)
36
+ const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
39
37
  const gltfTrait = useTrait(() => entity, traits.GLTF)
40
- const scale = useTrait(() => entity, traits.Scale)
41
38
  const invisible = useTrait(() => entity, traits.Invisible)
42
39
  const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
43
40
  const events = useEntityEvents(() => entity)
@@ -48,8 +45,8 @@
48
45
  group.matrixAutoUpdate = false
49
46
 
50
47
  $effect.pre(() => {
51
- if (matrix.current) {
52
- group.matrix.copy(matrix.current)
48
+ if (worldMatrix.current) {
49
+ group.matrix.copy(worldMatrix.current)
53
50
  group.updateMatrixWorld()
54
51
  }
55
52
  })
@@ -85,30 +82,23 @@
85
82
  })
86
83
  </script>
87
84
 
88
- <Portal id={parent.current}>
89
- <T is={group}>
90
- {#if showAxesHelper.current}
91
- <AxesHelper
92
- name={entity}
93
- width={3}
94
- length={0.1}
95
- />
96
- {/if}
97
- {#if $gltf}
98
- <T
99
- is={$gltf.scene as Object3D}
100
- scale={[scale.current?.x ?? 1, scale.current?.y ?? 1, scale.current?.z ?? 1]}
101
- name={entity}
102
- visible={invisible.current !== true}
103
- {...events}
104
- {...rest}
105
- >
106
- {@render children?.()}
107
-
108
- {#if name.current}
109
- <PortalTarget id={name.current} />
110
- {/if}
111
- </T>
112
- {/if}
113
- </T>
114
- </Portal>
85
+ <T is={group}>
86
+ {#if showAxesHelper.current}
87
+ <AxesHelper
88
+ name={entity}
89
+ width={3}
90
+ length={0.1}
91
+ />
92
+ {/if}
93
+ {#if $gltf}
94
+ <T
95
+ is={$gltf.scene as Object3D}
96
+ name={entity}
97
+ visible={invisible.current !== true}
98
+ {...events}
99
+ {...rest}
100
+ >
101
+ {@render children?.()}
102
+ </T>
103
+ {/if}
104
+ </T>
@@ -8,9 +8,9 @@ Renders a Viam Geometry object
8
8
  import type { Snippet } from 'svelte'
9
9
 
10
10
  import { T, useThrelte } from '@threlte/core'
11
- import { Portal } from '@threlte/extras'
11
+ import { Group } from 'three'
12
12
 
13
- import { traits, useParentName, useTrait } from '../../ecs'
13
+ import { traits, useTrait } from '../../ecs'
14
14
  import { use3DModels } from '../../hooks/use3DModels.svelte'
15
15
  import { useSettings } from '../../hooks/useSettings.svelte'
16
16
  import { poseToObject3d } from '../../transform'
@@ -31,7 +31,7 @@ Renders a Viam Geometry object
31
31
  const models = use3DModels()
32
32
 
33
33
  const name = useTrait(() => entity, traits.Name)
34
- const parent = useParentName(() => entity)
34
+ const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
35
35
  const center = useTrait(() => entity, traits.Center)
36
36
  const invisible = useTrait(() => entity, traits.Invisible)
37
37
 
@@ -52,6 +52,16 @@ Renders a Viam Geometry object
52
52
  return models.current[componentName]?.[id]?.clone() ?? undefined
53
53
  })
54
54
 
55
+ const group = new Group()
56
+ group.matrixAutoUpdate = false
57
+
58
+ $effect.pre(() => {
59
+ if (!worldMatrix.current) return
60
+ group.matrix.copy(worldMatrix.current)
61
+ group.updateMatrixWorld()
62
+ invalidate()
63
+ })
64
+
55
65
  $effect.pre(() => {
56
66
  if (model && center.current) {
57
67
  poseToObject3d(center.current, model)
@@ -62,24 +72,25 @@ Renders a Viam Geometry object
62
72
  const events = useEntityEvents(() => entity)
63
73
  </script>
64
74
 
65
- <Portal id={parent.current}>
66
- <T.Group visible={invisible.current !== true}>
67
- {#if model}
68
- <T
69
- is={model}
70
- name={entity}
71
- {...events}
72
- />
73
- {/if}
74
-
75
- {#if settings.current.renderArmModels.includes('colliders') || !model}
76
- <Mesh
77
- {entity}
78
- center={center.current}
79
- {...events}
80
- >
81
- {@render children?.()}
82
- </Mesh>
83
- {/if}
84
- </T.Group>
85
- </Portal>
75
+ <T
76
+ is={group}
77
+ visible={invisible.current !== true}
78
+ >
79
+ {#if model}
80
+ <T
81
+ is={model}
82
+ name={entity}
83
+ {...events}
84
+ />
85
+ {/if}
86
+
87
+ {#if settings.current.renderArmModels.includes('colliders') || !model}
88
+ <Mesh
89
+ {entity}
90
+ center={center.current}
91
+ {...events}
92
+ >
93
+ {@render children?.()}
94
+ </Mesh>
95
+ {/if}
96
+ </T>
@@ -3,11 +3,11 @@
3
3
  import type { Snippet } from 'svelte'
4
4
 
5
5
  import { T, useThrelte } from '@threlte/core'
6
- import { meshBounds, Portal, PortalTarget } from '@threlte/extras'
6
+ import { meshBounds } from '@threlte/extras'
7
7
  import { Line2, LineMaterial } from 'three/examples/jsm/Addons.js'
8
8
 
9
9
  import { isVertexColors, STRIDE } from '../../buffer'
10
- import { traits, useParentName, useTrait } from '../../ecs'
10
+ import { traits, useTrait } from '../../ecs'
11
11
 
12
12
  import AxesHelper from '../AxesHelper.svelte'
13
13
  import { useEntityEvents } from './hooks/useEntityEvents.svelte'
@@ -23,8 +23,7 @@
23
23
 
24
24
  const { invalidate } = useThrelte()
25
25
  const name = useTrait(() => entity, traits.Name)
26
- const parent = useParentName(() => entity)
27
- const matrix = useTrait(() => entity, traits.Matrix)
26
+ const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
28
27
  const color = useTrait(() => entity, traits.Color)
29
28
  const colors = useTrait(() => entity, traits.Colors)
30
29
  const dotColors = useTrait(() => entity, traits.DotColors)
@@ -65,47 +64,45 @@
65
64
  mesh.matrixAutoUpdate = false
66
65
 
67
66
  $effect.pre(() => {
68
- if (matrix.current) {
69
- mesh.matrix.copy(matrix.current)
67
+ if (worldMatrix.current) {
68
+ mesh.matrix.copy(worldMatrix.current)
70
69
  mesh.updateMatrixWorld()
71
70
  invalidate()
72
71
  }
73
72
  })
74
73
  </script>
75
74
 
76
- <Portal id={parent.current}>
75
+ <T
76
+ is={mesh}
77
+ name={entity}
78
+ userData.name={name}
79
+ raycast={meshBounds}
80
+ renderOrder={renderOrder.current}
81
+ visible={invisible.current !== true}
82
+ {...events}
83
+ >
84
+ <LineGeometry
85
+ positions={linePositions.current}
86
+ colors={lineColors}
87
+ />
77
88
  <T
78
- is={mesh}
79
- name={entity}
80
- userData.name={name}
81
- raycast={meshBounds}
82
- renderOrder={renderOrder.current}
83
- visible={invisible.current !== true}
84
- {...events}
85
- >
86
- <LineGeometry
87
- positions={linePositions.current}
88
- colors={lineColors}
89
- />
90
- <T
91
- is={LineMaterial}
92
- color={hasVertexColors ? [1, 1, 1] : lineColor}
93
- vertexColors={hasVertexColors}
94
- transparent={currentOpacity < 1}
95
- depthWrite={currentOpacity === 1}
96
- opacity={currentOpacity}
97
- worldUnits={!screenSpace.current}
98
- linewidth={(lineWidth.current ?? 5) * (screenSpace.current ? 1 : 0.001)}
99
- depthTest={materialProps.current?.depthTest ?? true}
89
+ is={LineMaterial}
90
+ color={hasVertexColors ? [1, 1, 1] : lineColor}
91
+ vertexColors={hasVertexColors}
92
+ transparent={currentOpacity < 1}
93
+ depthWrite={currentOpacity === 1}
94
+ opacity={currentOpacity}
95
+ worldUnits={!screenSpace.current}
96
+ linewidth={(lineWidth.current ?? 5) * (screenSpace.current ? 1 : 0.001)}
97
+ depthTest={materialProps.current?.depthTest ?? true}
98
+ />
99
+ {#if showAxesHelper.current}
100
+ <AxesHelper
101
+ name={entity}
102
+ width={3}
103
+ length={0.1}
100
104
  />
101
- {#if showAxesHelper.current}
102
- <AxesHelper
103
- name={entity}
104
- width={3}
105
- length={0.1}
106
- />
107
- {/if}
108
- </T>
105
+ {/if}
109
106
 
110
107
  {#if linePositions.current && dotSize.current}
111
108
  <LineDots
@@ -116,9 +113,5 @@
116
113
  />
117
114
  {/if}
118
115
 
119
- {#if name.current}
120
- <PortalTarget id={name.current} />
121
- {/if}
122
-
123
116
  {@render children?.()}
124
- </Portal>
117
+ </T>
@@ -3,11 +3,10 @@
3
3
  import type { Snippet } from 'svelte'
4
4
 
5
5
  import { T, useTask, useThrelte } from '@threlte/core'
6
- import { Portal } from '@threlte/extras'
7
6
  import { OrthographicCamera, Points, PointsMaterial } from 'three'
8
7
 
9
8
  import { asColor, isSingleColor } from '../../buffer'
10
- import { traits, useParentName, useTrait } from '../../ecs'
9
+ import { traits, useTrait } from '../../ecs'
11
10
  import { useSettings } from '../../hooks/useSettings.svelte'
12
11
 
13
12
  import AxesHelper from '../AxesHelper.svelte'
@@ -23,8 +22,7 @@
23
22
  const { camera } = useThrelte()
24
23
  const settings = useSettings()
25
24
 
26
- const parent = useParentName(() => entity)
27
- const matrix = useTrait(() => entity, traits.Matrix)
25
+ const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
28
26
  const geometry = useTrait(() => entity, traits.BufferGeometry)
29
27
  const entityColor = useTrait(() => entity, traits.Color)
30
28
  const colors = useTrait(() => entity, traits.Colors)
@@ -98,8 +96,8 @@
98
96
  })
99
97
 
100
98
  $effect.pre(() => {
101
- if (matrix.current) {
102
- points.matrix.copy(matrix.current)
99
+ if (worldMatrix.current) {
100
+ points.matrix.copy(worldMatrix.current)
103
101
  points.updateMatrixWorld()
104
102
  }
105
103
  })
@@ -126,25 +124,23 @@
126
124
  </script>
127
125
 
128
126
  {#if geometry.current}
129
- <Portal id={parent.current}>
130
- <T
131
- is={points}
132
- name={entity}
133
- bvh={{ maxDepth: 40, maxLeafSize: 20 }}
134
- visible={invisible.current !== true}
135
- renderOrder={renderOrder.current}
136
- {...events}
137
- >
138
- <T is={geometry.current} />
139
- <T is={material} />
140
- {#if showAxesHelper.current}
141
- <AxesHelper
142
- name={entity}
143
- width={3}
144
- length={0.1}
145
- />
146
- {/if}
147
- {@render children?.()}
148
- </T>
149
- </Portal>
127
+ <T
128
+ is={points}
129
+ name={entity}
130
+ bvh={{ maxDepth: 40, maxLeafSize: 20 }}
131
+ visible={invisible.current !== true}
132
+ renderOrder={renderOrder.current}
133
+ {...events}
134
+ >
135
+ <T is={geometry.current} />
136
+ <T is={material} />
137
+ {#if showAxesHelper.current}
138
+ <AxesHelper
139
+ name={entity}
140
+ width={3}
141
+ length={0.1}
142
+ />
143
+ {/if}
144
+ {@render children?.()}
145
+ </T>
150
146
  {/if}
@@ -51,7 +51,13 @@
51
51
  enabled.set(settings.current.interactionMode === 'navigate')
52
52
  })
53
53
 
54
- bvh(raycaster, () => ({ helper: false }))
54
+ const bvhEnabled = $derived(
55
+ settings.current.renderSubEntityHoverDetail ||
56
+ settings.current.interactionMode === 'measure' ||
57
+ settings.current.interactionMode === 'select'
58
+ )
59
+
60
+ bvh(raycaster, () => ({ helper: false, enabled: bvhEnabled }))
55
61
 
56
62
  const focusedObject = $derived(focusedObject3d.current)
57
63
 
@@ -2,7 +2,6 @@
2
2
  module
3
3
  lang="ts"
4
4
  >
5
- import { ThemeUtils } from 'svelte-tweakpane-ui'
6
5
  import { BufferAttribute, Euler, MathUtils, Quaternion } from 'three'
7
6
 
8
7
  import { OrientationVector } from '../../three/OrientationVector'
@@ -280,12 +279,6 @@
280
279
  2
281
280
  )
282
281
  }
283
-
284
- ThemeUtils.setGlobalDefaultTheme({
285
- ...ThemeUtils.presets.light,
286
- baseBackgroundColor: '#fbfbfc',
287
- baseShadowColor: 'transparent',
288
- })
289
282
  </script>
290
283
 
291
284
  {#snippet ImmutableField({
package/dist/draw.js CHANGED
@@ -305,8 +305,9 @@ const drawModel = (world, model, api, { removable = true }) => {
305
305
  relations.ChildOf(root),
306
306
  api,
307
307
  ];
308
- if (scale)
309
- subEntityTraits.push(traits.Scale(scale));
308
+ if (scale) {
309
+ subEntityTraits.push(traits.Matrix(new Matrix4().makeScale(scale.x ?? 1, scale.y ?? 1, scale.z ?? 1)));
310
+ }
310
311
  if (metadata?.invisible)
311
312
  subEntityTraits.push(traits.Invisible);
312
313
  if (metadata?.showAxesHelper)
@@ -143,11 +143,6 @@ export declare const GLTF: import("koota").Trait<() => {
143
143
  };
144
144
  animationName: string;
145
145
  }>;
146
- export declare const Scale: import("koota").Trait<{
147
- x: number;
148
- y: number;
149
- z: number;
150
- }>;
151
146
  export declare const FramesAPI: import("koota").Trait<() => boolean>;
152
147
  export declare const GeometriesAPI: import("koota").Trait<() => boolean>;
153
148
  export declare const DrawAPI: import("koota").Trait<() => boolean>;
@@ -120,7 +120,6 @@ export const GLTF = trait(() => ({
120
120
  source: { url: '' },
121
121
  animationName: '',
122
122
  }));
123
- export const Scale = trait({ x: 1, y: 1, z: 1 });
124
123
  export const FramesAPI = trait(() => true);
125
124
  export const GeometriesAPI = trait(() => true);
126
125
  export const DrawAPI = trait(() => true);
@@ -231,7 +230,9 @@ export const updateGeometryTrait = (entity, geometry) => {
231
230
  }
232
231
  else if (geometry.geometryType.case === 'mesh') {
233
232
  if (entity.has(BufferGeometry)) {
233
+ const old = entity.get(BufferGeometry);
234
234
  entity.set(BufferGeometry, parsePlyInput(geometry.geometryType.value.mesh));
235
+ old?.dispose();
235
236
  }
236
237
  else {
237
238
  entity.remove(Box, Sphere, Capsule);
@@ -1,8 +1,8 @@
1
1
  import { type World } from 'koota';
2
2
  /**
3
3
  * Wire up listeners that maintain `WorldMatrix` reactively. Subscribes to
4
- * add/change/remove on `Matrix`, `EditedMatrix`, `LiveMatrix`, `Scale`, and
5
- * `ChildOf`; enqueues affected entities and flushes on the next microtask.
4
+ * add/change/remove on `Matrix`, `EditedMatrix`, `LiveMatrix`, and `ChildOf`;
5
+ * enqueues affected entities and flushes on the next microtask.
6
6
  *
7
7
  * Returns an unsubscribe function. Plain function (not a rune hook) so tests
8
8
  * can drive the lifecycle without mounting Svelte.
@@ -1,9 +1,8 @@
1
1
  import {} from 'koota';
2
- import { Matrix4, Vector3 } from 'three';
2
+ import { Matrix4 } from 'three';
3
3
  import { composeLocalMatrix } from '../transform';
4
4
  import { ChildOf } from './relations';
5
- import { EditedMatrix, LiveMatrix, Matrix, Scale, WorldMatrix } from './traits';
6
- const scaleVec3 = new Vector3();
5
+ import { EditedMatrix, LiveMatrix, Matrix, WorldMatrix } from './traits';
7
6
  /**
8
7
  * Compute the entity's local-to-parent transform into `out`. Mirrors the
9
8
  * blend used by `Frame.svelte` so `WorldMatrix` agrees with the displayed
@@ -52,10 +51,6 @@ const recomputeWorldMatrix = (world, entity, cache) => {
52
51
  const hasLocal = toLocalMatrix(entity, out);
53
52
  if (!hasLocal)
54
53
  out.identity();
55
- const scale = entity.get(Scale);
56
- if (scale) {
57
- out.scale(scaleVec3.copy(scale));
58
- }
59
54
  const parent = entity.targetFor(ChildOf);
60
55
  if (parent && parent.isAlive()) {
61
56
  const parentWorld = recomputeWorldMatrix(world, parent, cache);
@@ -97,8 +92,8 @@ const flushDirty = (world, dirty) => {
97
92
  };
98
93
  /**
99
94
  * Wire up listeners that maintain `WorldMatrix` reactively. Subscribes to
100
- * add/change/remove on `Matrix`, `EditedMatrix`, `LiveMatrix`, `Scale`, and
101
- * `ChildOf`; enqueues affected entities and flushes on the next microtask.
95
+ * add/change/remove on `Matrix`, `EditedMatrix`, `LiveMatrix`, and `ChildOf`;
96
+ * enqueues affected entities and flushes on the next microtask.
102
97
  *
103
98
  * Returns an unsubscribe function. Plain function (not a rune hook) so tests
104
99
  * can drive the lifecycle without mounting Svelte.
@@ -122,8 +117,6 @@ export const installWorldMatrixListeners = (world) => {
122
117
  enqueue(entity);
123
118
  for (const entity of world.query(LiveMatrix))
124
119
  enqueue(entity);
125
- for (const entity of world.query(Scale))
126
- enqueue(entity);
127
120
  const unsubs = [
128
121
  world.onAdd(Matrix, enqueue),
129
122
  world.onChange(Matrix, enqueue),
@@ -134,9 +127,6 @@ export const installWorldMatrixListeners = (world) => {
134
127
  world.onAdd(LiveMatrix, enqueue),
135
128
  world.onChange(LiveMatrix, enqueue),
136
129
  world.onRemove(LiveMatrix, enqueue),
137
- world.onAdd(Scale, enqueue),
138
- world.onChange(Scale, enqueue),
139
- world.onRemove(Scale, enqueue),
140
130
  world.onAdd(ChildOf, enqueue),
141
131
  world.onChange(ChildOf, enqueue),
142
132
  world.onRemove(ChildOf, enqueue),
@@ -10,7 +10,7 @@ import { DrawService } from '../buf/draw/v1/service_connect';
10
10
  import { CreateRelationshipRequest, DeleteRelationshipRequest, EntityChangeType, StreamEntityChangesResponse, } from '../buf/draw/v1/service_pb';
11
11
  import { asFloat32Array, inMeters, STRIDE } from '../buffer';
12
12
  import { drawDrawing, drawTransform, updateDrawing, updateModel, updateTransform, uuidStringToBytes, } from '../draw';
13
- import { traits, useWorld } from '../ecs';
13
+ import { hierarchy, traits, useWorld } from '../ecs';
14
14
  import { useCameraControls } from './useControls.svelte';
15
15
  import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
16
16
  import { useRelationships } from './useRelationships.svelte';
@@ -50,8 +50,7 @@ export function provideDrawService() {
50
50
  const entity = drawingEntities.get(uuidStr);
51
51
  if (!entity)
52
52
  return;
53
- if (world.has(entity))
54
- entity.destroy();
53
+ hierarchy.destroyEntityTree(world, entity);
55
54
  drawingEntities.delete(uuidStr);
56
55
  };
57
56
  const processEvent = (event) => {
@@ -334,13 +333,11 @@ export function provideDrawService() {
334
333
  connectionStatus = ConnectionStatus.DISCONNECTED;
335
334
  activeClient = undefined;
336
335
  for (const entity of transformEntities.values()) {
337
- if (world.has(entity))
338
- entity.destroy();
336
+ hierarchy.destroyEntityTree(world, entity);
339
337
  }
340
338
  transformEntities.clear();
341
339
  for (const entity of drawingEntities.values()) {
342
- if (world.has(entity))
343
- entity.destroy();
340
+ hierarchy.destroyEntityTree(world, entity);
344
341
  }
345
342
  drawingEntities.clear();
346
343
  relationships.clear();
@@ -1,19 +1,20 @@
1
- import { ArmClient, BaseClient, CameraClient, GantryClient, GripperClient } from '@viamrobotics/sdk';
1
+ import { ArmClient, BaseClient, CameraClient, GantryClient, GenericComponentClient, GripperClient, } from '@viamrobotics/sdk';
2
2
  import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
3
  import {} from 'koota';
4
4
  import { getContext, setContext, untrack } from 'svelte';
5
- import { Color } from 'three';
5
+ import { Color, Matrix4 } from 'three';
6
6
  import { resourceColors } from '../color';
7
7
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
8
8
  import { hierarchy, traits, useWorld } from '../ecs';
9
9
  import { updateGeometryTrait } from '../ecs/traits';
10
- import { createPose, isPoseEqual } from '../transform';
10
+ import { createPose, poseToMatrix } from '../transform';
11
11
  import { useEnvironment } from './useEnvironment.svelte';
12
12
  import { useLogs } from './useLogs.svelte';
13
13
  import { useResourceByName } from './useResourceByName.svelte';
14
14
  import { RefreshRates, useSettings } from './useSettings.svelte';
15
15
  const key = Symbol('geometries-context');
16
16
  const colorUtil = new Color();
17
+ const tempMatrix = new Matrix4();
17
18
  export const provideGeometries = (partID) => {
18
19
  const environment = useEnvironment();
19
20
  const resources = useResourceByName();
@@ -24,6 +25,7 @@ export const provideGeometries = (partID) => {
24
25
  const cameras = useResourceNames(partID, 'camera');
25
26
  const grippers = useResourceNames(partID, 'gripper');
26
27
  const gantries = useResourceNames(partID, 'gantry');
28
+ const generics = useResourceNames(partID, 'generic');
27
29
  const settings = useSettings();
28
30
  const { refreshRates } = $derived(settings.current);
29
31
  const armClients = $derived(arms.current.map((arm) => createResourceClient(ArmClient, partID, () => arm.name)));
@@ -31,6 +33,9 @@ export const provideGeometries = (partID) => {
31
33
  const gripperClients = $derived(grippers.current.map((gripper) => createResourceClient(GripperClient, partID, () => gripper.name)));
32
34
  const cameraClients = $derived(cameras.current.map((camera) => createResourceClient(CameraClient, partID, () => camera.name)));
33
35
  const gantryClients = $derived(gantries.current.map((gantry) => createResourceClient(GantryClient, partID, () => gantry.name)));
36
+ const genericClients = $derived(generics.current
37
+ .filter((generic) => generic.type === 'component')
38
+ .map((generic) => createResourceClient(GenericComponentClient, partID, () => generic.name)));
34
39
  const interval = $derived(refreshRates[RefreshRates.poses]);
35
40
  const options = $derived({
36
41
  enabled: interval !== RefetchRates.OFF && environment.current.viewerMode === 'monitor',
@@ -41,12 +46,14 @@ export const provideGeometries = (partID) => {
41
46
  const gripperQueries = $derived(gripperClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
42
47
  const cameraQueries = $derived(cameraClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
43
48
  const gantryQueries = $derived(gantryClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
49
+ const genericQueries = $derived(genericClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
44
50
  const queries = $derived([
45
51
  ...armQueries,
46
52
  ...baseQueries,
47
53
  ...gripperQueries,
48
54
  ...cameraQueries,
49
55
  ...gantryQueries,
56
+ ...genericQueries,
50
57
  ]);
51
58
  $effect(() => {
52
59
  if (interval === RefetchRates.FPS_30 || interval === RefetchRates.FPS_60) {
@@ -91,8 +98,11 @@ export const provideGeometries = (partID) => {
91
98
  const existing = entities.get(entityKey);
92
99
  if (existing) {
93
100
  hierarchy.setParent(existing, name);
94
- if (!isPoseEqual(existing.get(traits.Center), center)) {
95
- existing.set(traits.Center, center);
101
+ poseToMatrix(center, tempMatrix);
102
+ const matrix = existing.get(traits.Matrix);
103
+ if (matrix && !matrix.equals(tempMatrix)) {
104
+ matrix.copy(tempMatrix);
105
+ existing.changed(traits.Matrix);
96
106
  }
97
107
  updateGeometryTrait(existing, geometry);
98
108
  continue;
@@ -100,7 +110,7 @@ export const provideGeometries = (partID) => {
100
110
  const entityTraits = [
101
111
  ...hierarchy.parentTraits(name),
102
112
  traits.Name(label),
103
- traits.Center(center),
113
+ traits.Matrix(poseToMatrix(center, new Matrix4())),
104
114
  traits.GeometriesAPI,
105
115
  traits.Geometry(geometry),
106
116
  ];
@@ -8,7 +8,7 @@ export const providePartConfig = (partID, params) => {
8
8
  const props = $derived(params());
9
9
  const config = $derived(props ? useEmbeddedPartConfig(props) : useStandalonePartConfig(partID));
10
10
  const getCurrent = () => {
11
- return (config.current.toJson?.() ?? { components: [] });
11
+ return (config.current?.toJson?.() ?? { components: [] });
12
12
  };
13
13
  const current = $derived(getCurrent());
14
14
  const createFragmentFrame = (fragmentId, componentName) => {
@@ -313,10 +313,12 @@ const useStandalonePartConfig = (partID) => {
313
313
  const id = partID();
314
314
  if (lastPartID !== undefined && lastPartID !== id) {
315
315
  // Part changed: drop any in-memory edits/pending-save state from the
316
- // previous part. `current` is left for the existing sync below to
317
- // repopulate once the new part's networkPartConfig arrives.
316
+ // previous part, and clear `current` so consumers don't keep
317
+ // rendering the old config's frames while the new part loads
318
+ // (offline parts may never load, leaving the old frames forever).
318
319
  isDirty = false;
319
320
  hasPendingSave = false;
321
+ current = undefined;
320
322
  }
321
323
  lastPartID = id;
322
324
  if (!networkPartConfig || isDirty) {
@@ -17,11 +17,14 @@ export const bvh = (raycaster, options) => {
17
17
  injectPlugin('bvh', (args) => {
18
18
  const { props } = $derived(args);
19
19
  const opts = $derived(props.bvh ? { ...bvhOptions, ...props.bvh } : bvhOptions);
20
+ let computed = false;
21
+ let helper;
20
22
  $effect(() => {
21
23
  const { ref } = args;
22
- if (opts.enabled === false) {
24
+ if (computed)
25
+ return;
26
+ if (opts.enabled === false)
23
27
  return;
24
- }
25
28
  if (isInstanceOf(ref, 'Points') &&
26
29
  /**
27
30
  * This check is necessary, there are some strange cases where points are coming in from PCDs without any position data
@@ -31,28 +34,12 @@ export const bvh = (raycaster, options) => {
31
34
  ref.geometry.disposeBoundsTree = disposeBoundsTree;
32
35
  ref.raycast = acceleratedRaycast;
33
36
  computeBoundsTree.call(ref.geometry, { type: PointsBVH, ...opts });
34
- const helper = opts.helper ? new BVHHelper(ref) : undefined;
35
- if (helper)
36
- ref.add(helper);
37
- return () => {
38
- ref.raycast = Points.prototype.raycast;
39
- if (helper)
40
- ref.remove(helper);
41
- };
42
37
  }
43
38
  else if (isInstanceOf(ref, 'BatchedMesh')) {
44
39
  /* @ts-expect-error Some sort of ambient type is conflicing here, likely from @threlte/extras */
45
40
  ref.geometry.computeBoundsTree = computeBatchedBoundsTree;
46
41
  ref.geometry.disposeBoundsTree = disposeBatchedBoundsTree;
47
42
  ref.raycast = acceleratedRaycast;
48
- const helper = opts.helper ? new BVHHelper(ref) : undefined;
49
- if (helper)
50
- ref.add(helper);
51
- return () => {
52
- ref.raycast = BatchedMesh.prototype.raycast;
53
- if (helper)
54
- ref.remove(helper);
55
- };
56
43
  }
57
44
  else if (isInstanceOf(ref, 'Mesh') &&
58
45
  /**
@@ -64,15 +51,39 @@ export const bvh = (raycaster, options) => {
64
51
  ref.geometry.disposeBoundsTree = disposeBoundsTree;
65
52
  ref.raycast = acceleratedRaycast;
66
53
  computeBoundsTree.call(ref.geometry, opts);
67
- const helper = opts.helper ? new BVHHelper(ref) : undefined;
68
- if (helper)
69
- ref.add(helper);
70
- return () => {
71
- ref.raycast = Mesh.prototype.raycast;
72
- if (helper)
73
- ref.remove(helper);
74
- };
75
54
  }
55
+ else {
56
+ return;
57
+ }
58
+ if (opts.helper) {
59
+ helper = new BVHHelper(ref);
60
+ ref.add(helper);
61
+ }
62
+ computed = true;
63
+ });
64
+ $effect(() => {
65
+ const { ref } = args;
66
+ return () => {
67
+ if (!computed)
68
+ return;
69
+ if (isInstanceOf(ref, 'Points')) {
70
+ ref.geometry.disposeBoundsTree?.();
71
+ ref.raycast = Points.prototype.raycast;
72
+ }
73
+ else if (isInstanceOf(ref, 'BatchedMesh')) {
74
+ ref.geometry.disposeBoundsTree?.();
75
+ ref.raycast = BatchedMesh.prototype.raycast;
76
+ }
77
+ else if (isInstanceOf(ref, 'Mesh')) {
78
+ ref.geometry.disposeBoundsTree?.();
79
+ ref.raycast = Mesh.prototype.raycast;
80
+ }
81
+ if (helper) {
82
+ ref.remove(helper);
83
+ helper = undefined;
84
+ }
85
+ computed = false;
86
+ };
76
87
  });
77
88
  });
78
89
  };
@@ -39,9 +39,12 @@ const expandBoxByTransformedBox = (box, childBox, matrix) => {
39
39
  };
40
40
  export class OBBHelper extends LineSegments2 {
41
41
  constructor(color = 0x000000, linewidth = 2) {
42
- const edges = new EdgesGeometry(new BoxGeometry());
42
+ const boxGeometry = new BoxGeometry();
43
+ const edges = new EdgesGeometry(boxGeometry);
43
44
  const geometry = new LineSegmentsGeometry();
44
45
  geometry.setPositions(edges.getAttribute('position').array);
46
+ edges.dispose();
47
+ boxGeometry.dispose();
45
48
  const material = new LineMaterial({
46
49
  color,
47
50
  linewidth,
@@ -20,6 +20,8 @@ export const createArrowGeometry = () => {
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);
22
22
  const merged = mergeGeometries([tailGeometry, headGeo], true);
23
+ tailGeometry.dispose();
24
+ headGeo.dispose();
23
25
  merged.computeVertexNormals();
24
26
  merged.computeBoundingBox();
25
27
  merged.computeBoundingSphere();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.26.2",
3
+ "version": "1.27.1",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -64,7 +64,7 @@
64
64
  "prettier-plugin-tailwindcss": "0.6.14",
65
65
  "publint": "0.3.12",
66
66
  "runed": "0.31.1",
67
- "svelte": "5.55.0",
67
+ "svelte": "5.55.7",
68
68
  "svelte-check": "4.4.5",
69
69
  "svelte-tweakpane-ui": "^1.5.16",
70
70
  "svelte-virtuallists": "1.4.2",