@viamrobotics/motion-tools 1.13.1 → 1.14.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.
@@ -5,12 +5,17 @@
5
5
  </script>
6
6
 
7
7
  <script lang="ts">
8
- import Frame from './Frame.svelte'
9
8
  import type { Snippet } from 'svelte'
9
+ import { T, useThrelte } from '@threlte/core'
10
+ import { meshBounds, Portal, PortalTarget } from '@threlte/extras'
10
11
  import type { Entity } from 'koota'
11
12
  import { traits, useTrait } from '../../ecs'
12
13
  import LineDots from './LineDots.svelte'
13
14
  import { darkenColor } from '../../color'
15
+ import { useEntityEvents } from './hooks/useEntityEvents.svelte'
16
+ import { Line2, LineMaterial } from 'three/examples/jsm/Addons.js'
17
+ import LineGeometry from './LineGeometry.svelte'
18
+ import { poseToObject3d } from '../../transform'
14
19
 
15
20
  interface Props {
16
21
  entity: Entity
@@ -19,25 +24,67 @@
19
24
 
20
25
  let { entity, children }: Props = $props()
21
26
 
22
- const linePositions = useTrait(() => entity, traits.LinePositions)
27
+ const { invalidate } = useThrelte()
28
+ const name = useTrait(() => entity, traits.Name)
29
+ const parent = useTrait(() => entity, traits.Parent)
30
+ const pose = useTrait(() => entity, traits.Pose)
23
31
  const color = useTrait(() => entity, traits.Color)
24
32
  const pointSize = useTrait(() => entity, traits.PointSize)
33
+ const linePositions = useTrait(() => entity, traits.LinePositions)
34
+ const lineWidth = useTrait(() => entity, traits.LineWidth)
35
+ const opacity = useTrait(() => entity, traits.Opacity)
36
+ const materialProps = useTrait(() => entity, traits.Material)
37
+ const renderOrder = useTrait(() => entity, traits.RenderOrder)
38
+
39
+ const events = useEntityEvents(() => entity)
40
+
41
+ const currentOpacity = $derived(opacity.current ?? 0.7)
25
42
 
26
- const resolvedColor = $derived(
27
- colorUtil
28
- .setRGB(color.current?.r ?? 1, color.current?.g ?? 0, color.current?.b ?? 0)
29
- .getHexString()
30
- )
43
+ const mesh = new Line2()
44
+
45
+ $effect.pre(() => {
46
+ if (pose.current) {
47
+ poseToObject3d(pose.current, mesh)
48
+ invalidate()
49
+ }
50
+ })
31
51
  </script>
32
52
 
33
- <Frame {entity}>
53
+ <Portal id={parent.current}>
54
+ <T
55
+ is={mesh}
56
+ name={entity}
57
+ userData.name={name}
58
+ raycast={meshBounds}
59
+ renderOrder={renderOrder.current}
60
+ {...events}
61
+ >
62
+ <LineGeometry positions={linePositions.current} />
63
+ <T
64
+ is={LineMaterial}
65
+ color={[color.current?.r ?? 1, color.current?.g ?? 0, color.current?.b ?? 0]}
66
+ transparent={currentOpacity < 1}
67
+ depthWrite={currentOpacity === 1}
68
+ opacity={currentOpacity}
69
+ width={lineWidth.current ? lineWidth.current * 0.001 : 0.5}
70
+ depthTest={materialProps.current?.depthTest ?? true}
71
+ />
72
+ </T>
73
+
74
+ {#if linePositions.current && pointSize.current}
75
+ <LineDots
76
+ color={darkenColor(
77
+ colorUtil.setRGB(color.current?.r ?? 1, color.current?.g ?? 0, color.current?.b ?? 0),
78
+ 10
79
+ )}
80
+ positions={linePositions.current}
81
+ scale={pointSize.current * 0.001}
82
+ />
83
+ {/if}
84
+
85
+ {#if name.current}
86
+ <PortalTarget id={name.current} />
87
+ {/if}
88
+
34
89
  {@render children?.()}
35
- </Frame>
36
-
37
- {#if linePositions.current && pointSize.current}
38
- <LineDots
39
- color={darkenColor(resolvedColor, 10)}
40
- positions={linePositions.current}
41
- scale={pointSize.current * 0.001}
42
- />
43
- {/if}
90
+ </Portal>
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ import { T, useThrelte, type Props as ThrelteProps } from '@threlte/core'
3
+ import { type Snippet } from 'svelte'
4
+ import { BufferGeometry, Color, DoubleSide, FrontSide, Mesh } from 'three'
5
+ import { CapsuleGeometry } from '../../three/CapsuleGeometry'
6
+ import { colors, darkenColor } from '../../color'
7
+ import AxesHelper from '../AxesHelper.svelte'
8
+ import type { Entity } from 'koota'
9
+ import { traits, useTrait } from '../../ecs'
10
+ import { poseToObject3d } from '../../transform'
11
+ import type { Pose } from '@viamrobotics/sdk'
12
+
13
+ interface Props extends ThrelteProps<Mesh> {
14
+ entity: Entity
15
+ color?: string
16
+ center?: Pose
17
+ children?: Snippet
18
+ }
19
+
20
+ let { entity, color: overrideColor, center, children, ...rest }: Props = $props()
21
+
22
+ const colorUtil = new Color()
23
+
24
+ const { invalidate } = useThrelte()
25
+ const name = useTrait(() => entity, traits.Name)
26
+ const entityColor = useTrait(() => entity, traits.Color)
27
+ const opacity = useTrait(() => entity, traits.Opacity)
28
+ const box = useTrait(() => entity, traits.Box)
29
+ const capsule = useTrait(() => entity, traits.Capsule)
30
+ const sphere = useTrait(() => entity, traits.Sphere)
31
+ const bufferGeometry = useTrait(() => entity, traits.BufferGeometry)
32
+ const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
33
+ const materialProps = useTrait(() => entity, traits.Material)
34
+ const renderOrder = useTrait(() => entity, traits.RenderOrder)
35
+
36
+ const color = $derived.by(() => {
37
+ if (overrideColor) {
38
+ return overrideColor
39
+ }
40
+
41
+ if (entityColor.current) {
42
+ return colorUtil.setRGB(entityColor.current.r, entityColor.current.g, entityColor.current.b)
43
+ }
44
+
45
+ return colors.default
46
+ })
47
+
48
+ const mesh = new Mesh()
49
+
50
+ $effect.pre(() => {
51
+ if (center) {
52
+ poseToObject3d(center, mesh)
53
+ invalidate()
54
+ }
55
+ })
56
+
57
+ let geo = $state.raw<BufferGeometry>()
58
+
59
+ const oncreate = (bufferGeometry: BufferGeometry) => {
60
+ geo = bufferGeometry
61
+ }
62
+ </script>
63
+
64
+ <T
65
+ is={mesh}
66
+ name={entity}
67
+ userData.name={name}
68
+ renderOrder={renderOrder.current}
69
+ {...rest}
70
+ >
71
+ {#if box.current}
72
+ {@const { x, y, z } = box.current ?? { x: 0, y: 0, z: 0 }}
73
+ <T.BoxGeometry
74
+ args={[x * 0.001, y * 0.001, z * 0.001]}
75
+ {oncreate}
76
+ />
77
+ {:else if sphere.current}
78
+ {@const { r } = sphere.current ?? { r: 0 }}
79
+ <T.SphereGeometry
80
+ args={[r * 0.001]}
81
+ {oncreate}
82
+ />
83
+ {:else if capsule.current}
84
+ {@const { r, l } = capsule.current ?? { r: 0, l: 0 }}
85
+ <T
86
+ is={CapsuleGeometry}
87
+ args={[r * 0.001, l * 0.001]}
88
+ {oncreate}
89
+ />
90
+ {:else if bufferGeometry.current}
91
+ <T
92
+ is={bufferGeometry.current}
93
+ {oncreate}
94
+ />
95
+ {/if}
96
+
97
+ {@const currentOpacity = opacity.current ?? 0.7}
98
+ <T.MeshToonMaterial
99
+ {color}
100
+ side={bufferGeometry.current ? DoubleSide : FrontSide}
101
+ transparent={currentOpacity < 1}
102
+ depthWrite={currentOpacity === 1}
103
+ opacity={currentOpacity}
104
+ depthTest={materialProps.current?.depthTest ?? true}
105
+ />
106
+
107
+ <!--
108
+ TODO(mp) currently some bufferGeometries are coming in empty,
109
+ this is a quick fix but this should be handled upstream
110
+ -->
111
+ {#if geo && geo.getAttribute('position').array.length > 0}
112
+ <T.LineSegments
113
+ raycast={() => null}
114
+ bvh={{ enabled: false }}
115
+ >
116
+ <T.EdgesGeometry args={[geo, 0]} />
117
+ <T.LineBasicMaterial color={darkenColor(color, 10)} />
118
+ </T.LineSegments>
119
+ {/if}
120
+
121
+ {@render children?.()}
122
+ </T>
123
+
124
+ {#if showAxesHelper.current}
125
+ <AxesHelper
126
+ name={entity}
127
+ width={3}
128
+ length={0.1}
129
+ />
130
+ {/if}
@@ -0,0 +1,4 @@
1
+ import { Mesh } from 'three';
2
+ declare const Mesh: any;
3
+ type Mesh = ReturnType<typeof Mesh>;
4
+ export default Mesh;
@@ -46,6 +46,8 @@
46
46
  selectedObject3d.current?.getWorldPosition(object.position)
47
47
  selectedObject3d.current?.getWorldQuaternion(object.quaternion)
48
48
  obbHelper.setFromObject(object)
49
+ } else {
50
+ obbHelper.setFromObject(object)
49
51
  }
50
52
 
51
53
  invalidate()
@@ -1,4 +1,7 @@
1
1
  import { traits } from '../../../ecs';
2
+ function sortNodes(nodes) {
3
+ nodes.sort((a, b) => a.entity.get(traits.Name)?.localeCompare(b.entity.get(traits.Name) ?? '') ?? 0);
4
+ }
2
5
  /**
3
6
  * Creates a tree representing parent child / relationships from a set of frames.
4
7
  */
@@ -29,5 +32,17 @@ export const buildTreeNodes = (entities) => {
29
32
  }
30
33
  }
31
34
  }
35
+ for (const node of rootNodes) {
36
+ if (!node.children)
37
+ continue;
38
+ sortNodes(node.children);
39
+ }
40
+ for (const node of childNodes) {
41
+ if (!node.children)
42
+ continue;
43
+ sortNodes(node.children);
44
+ }
45
+ sortNodes(rootNodes);
46
+ sortNodes(childNodes);
32
47
  return { rootNodes, nodeMap };
33
48
  };
@@ -1,4 +1,5 @@
1
1
  import type { GLTF as ThreeGltf } from 'three/examples/jsm/loaders/GLTFLoader.js';
2
+ import { type Entity } from 'koota';
2
3
  import { BufferGeometry as ThreeBufferGeometry } from 'three';
3
4
  import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
4
5
  export declare const Name: import("koota").Trait<() => string>;
@@ -178,22 +179,4 @@ export declare const Geometry: (geometry: ViamGeometry) => import("koota").Trait
178
179
  }>, Partial<{
179
180
  r: number;
180
181
  }>] | [import("koota").Trait<() => ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>>, ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>];
181
- export declare const updateGeometry: (geometry: ViamGeometry) => (ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap> | import("koota").Trait<() => ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>>)[] | ({
182
- x: number;
183
- y: number;
184
- z: number;
185
- } | import("koota").Trait<{
186
- x: number;
187
- y: number;
188
- z: number;
189
- }>)[] | ({
190
- r: number;
191
- l: number;
192
- } | import("koota").Trait<{
193
- l: number;
194
- r: number;
195
- }>)[] | ({
196
- r: number;
197
- } | import("koota").Trait<{
198
- r: number;
199
- }>)[];
182
+ export declare const updateGeometryTrait: (entity: Entity, geometry?: ViamGeometry) => void;
@@ -126,18 +126,45 @@ export const Geometry = (geometry) => {
126
126
  }
127
127
  return ReferenceFrame;
128
128
  };
129
- export const updateGeometry = (geometry) => {
129
+ export const updateGeometryTrait = (entity, geometry) => {
130
+ if (!geometry) {
131
+ entity.remove(Box, Capsule, Sphere, BufferGeometry);
132
+ return;
133
+ }
130
134
  if (geometry.geometryType.case === 'box') {
131
- return [Box, createBox(geometry.geometryType.value)];
135
+ if (entity.has(Box)) {
136
+ entity.set(Box, createBox(geometry.geometryType.value));
137
+ }
138
+ else {
139
+ entity.remove(Capsule, Sphere, BufferGeometry);
140
+ entity.add(Box(createBox(geometry.geometryType.value)));
141
+ }
132
142
  }
133
143
  else if (geometry.geometryType.case === 'capsule') {
134
- return [Capsule, createCapsule(geometry.geometryType.value)];
144
+ if (entity.has(Capsule)) {
145
+ entity.set(Capsule, createCapsule(geometry.geometryType.value));
146
+ }
147
+ else {
148
+ entity.remove(Box, Sphere, BufferGeometry);
149
+ entity.add(Capsule(createCapsule(geometry.geometryType.value)));
150
+ }
135
151
  }
136
152
  else if (geometry.geometryType.case === 'sphere') {
137
- return [Sphere, createSphere(geometry.geometryType.value)];
153
+ if (entity.has(Sphere)) {
154
+ entity.set(Sphere, createSphere(geometry.geometryType.value));
155
+ }
156
+ else {
157
+ entity.remove(Box, Capsule, BufferGeometry);
158
+ entity.add(Sphere(createSphere(geometry.geometryType.value)));
159
+ }
138
160
  }
139
161
  else if (geometry.geometryType.case === 'mesh') {
140
- return [BufferGeometry, parsePlyInput(geometry.geometryType.value.mesh)];
162
+ if (entity.has(BufferGeometry)) {
163
+ entity.set(BufferGeometry, parsePlyInput(geometry.geometryType.value.mesh));
164
+ }
165
+ else {
166
+ entity.remove(Box, Sphere, Capsule);
167
+ entity.add(BufferGeometry(parsePlyInput(geometry.geometryType.value.mesh)));
168
+ }
141
169
  }
142
- return [];
143
170
  };
@@ -57,9 +57,7 @@ export const provide3DModels = (partID) => {
57
57
  }
58
58
  };
59
59
  $effect(() => {
60
- const shouldFetchModels = settings.current.isLoaded &&
61
- (settings.current.renderArmModels === 'model' ||
62
- settings.current.renderArmModels === 'colliders+model');
60
+ const shouldFetchModels = settings.current.isLoaded && settings.current.renderArmModels.includes('model');
63
61
  if (shouldFetchModels) {
64
62
  fetch3DModels();
65
63
  }
@@ -9,6 +9,7 @@ import { createPose } from '../transform';
9
9
  import { useResourceByName } from './useResourceByName.svelte';
10
10
  import { traits, useWorld } from '../ecs';
11
11
  import { useConfigFrames } from './useConfigFrames.svelte';
12
+ import { updateGeometryTrait } from '../ecs/traits';
12
13
  const key = Symbol('frames-context');
13
14
  export const provideFrames = (partID) => {
14
15
  const configFrames = useConfigFrames();
@@ -69,15 +70,17 @@ export const provideFrames = (partID) => {
69
70
  }
70
71
  });
71
72
  $effect.pre(() => {
72
- if (current.length === 0)
73
- return;
74
73
  const currentResourcesByName = resourceByName.current;
74
+ const currentPartID = partID();
75
75
  // We only want to update whenever "current" or "resourceByName.current" changes
76
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
77
+ current.length;
76
78
  untrack(() => {
77
79
  const active = {};
78
80
  for (const frame of current) {
79
81
  const name = frame.referenceFrame;
80
- active[name] = true;
82
+ const entityKey = `${currentPartID}:${name}`;
83
+ active[entityKey] = true;
81
84
  const parent = frame.poseInObserverFrame?.referenceFrame;
82
85
  const pose = createPose(frame.poseInObserverFrame?.pose);
83
86
  const center = frame.physicalObject?.center
@@ -85,7 +88,7 @@ export const provideFrames = (partID) => {
85
88
  : undefined;
86
89
  const resourceName = currentResourcesByName[frame.referenceFrame];
87
90
  const color = resourceNameToColor(resourceName);
88
- const existing = entities.get(name);
91
+ const existing = entities.get(entityKey);
89
92
  if (existing) {
90
93
  if (!parent || parent === 'world') {
91
94
  existing.remove(traits.Parent);
@@ -102,11 +105,7 @@ export const provideFrames = (partID) => {
102
105
  if (center) {
103
106
  existing.set(traits.Center, center);
104
107
  }
105
- existing.remove(traits.Box, traits.Sphere, traits.BufferGeometry, traits.Capsule);
106
- if (frame.physicalObject) {
107
- const geometry = traits.Geometry(frame.physicalObject);
108
- existing.add(geometry);
109
- }
108
+ updateGeometryTrait(existing, frame.physicalObject);
110
109
  existing.set(traits.EditedPose, pose);
111
110
  continue;
112
111
  }
@@ -130,13 +129,13 @@ export const provideFrames = (partID) => {
130
129
  entityTraits.push(traits.Geometry(frame.physicalObject));
131
130
  }
132
131
  const entity = world.spawn(...entityTraits);
133
- entities.set(name, entity);
132
+ entities.set(entityKey, entity);
134
133
  }
135
134
  // Clean up non-active entities
136
- for (const [name, entity] of entities) {
137
- if (!active[name]) {
135
+ for (const [entityKey, entity] of entities) {
136
+ if (!active[entityKey]) {
138
137
  entity?.destroy();
139
- entities.delete(name);
138
+ entities.delete(entityKey);
140
139
  continue;
141
140
  }
142
141
  }
@@ -11,6 +11,7 @@ import {} from 'koota';
11
11
  import { createPose } from '../transform';
12
12
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
13
13
  import { useEnvironment } from './useEnvironment.svelte';
14
+ import { updateGeometryTrait } from '../ecs/traits';
14
15
  const key = Symbol('geometries-context');
15
16
  const colorUtil = new Color();
16
17
  export const provideGeometries = (partID) => {
@@ -39,6 +40,7 @@ export const provideGeometries = (partID) => {
39
40
  const gripperQueries = $derived(gripperClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
40
41
  const cameraQueries = $derived(cameraClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
41
42
  const gantryQueries = $derived(gantryClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
43
+ const queries = $derived([...armQueries, ...gripperQueries, ...cameraQueries, ...gantryQueries]);
42
44
  $effect(() => {
43
45
  if (interval === RefetchRates.FPS_30 || interval === RefetchRates.FPS_60) {
44
46
  return logs.add(`Fetching geometries every ${interval}ms...`);
@@ -56,49 +58,70 @@ export const provideGeometries = (partID) => {
56
58
  });
57
59
  }
58
60
  });
59
- const queries = $derived([...armQueries, ...gripperQueries, ...cameraQueries, ...gantryQueries]);
60
61
  const entities = new Map();
62
+ const queryEntityKeys = new Map();
61
63
  $effect(() => {
62
- const active = {};
64
+ const activeQueryKeys = new Set();
65
+ const currentPartID = partID();
63
66
  for (const [name, query] of queries) {
64
- untrack(() => {
65
- $effect(() => {
66
- if (name && query.data) {
67
- let index = 0;
68
- for (const geometry of query.data) {
69
- index += 1;
70
- const resourceName = resources.current[name];
71
- const label = geometry.label || `${name} geometry ${index}`;
72
- active[`${name}:${label}`] = true;
73
- const pose = createPose(geometry.center);
74
- const subtype = resourceName?.subtype;
75
- const existing = entities.get(`${name}:${label}`);
76
- if (existing) {
77
- existing.set(traits.Pose, pose);
78
- continue;
79
- }
80
- const entityTraits = [
81
- traits.Parent(name),
82
- traits.Name(label),
83
- traits.Pose(pose),
84
- traits.GeometriesAPI,
85
- traits.Geometry(geometry),
86
- ];
87
- if (subtype) {
88
- entityTraits.push(traits.Color(subtype ? colorUtil.set(resourceColors[subtype]) : undefined));
89
- }
90
- const entity = world.spawn(...entityTraits);
91
- entities.set(`${name}:${label}`, entity);
67
+ if (!name) {
68
+ continue;
69
+ }
70
+ const queryKey = `${currentPartID}:${name}`;
71
+ activeQueryKeys.add(queryKey);
72
+ $effect(() => {
73
+ const nextKeys = new Set();
74
+ const resourceName = resources.current[name];
75
+ const subtype = resourceName?.subtype;
76
+ if (query.data) {
77
+ let index = 0;
78
+ for (const geometry of query.data) {
79
+ index += 1;
80
+ const label = geometry.label || `${name} geometry ${index}`;
81
+ const entityKey = `${currentPartID}:${name}:${label}`;
82
+ nextKeys.add(entityKey);
83
+ const center = createPose(geometry.center);
84
+ const existing = entities.get(entityKey);
85
+ if (existing) {
86
+ existing.set(traits.Center, center);
87
+ updateGeometryTrait(existing, geometry);
88
+ continue;
89
+ }
90
+ const entityTraits = [
91
+ traits.Parent(name),
92
+ traits.Name(label),
93
+ traits.Center(center),
94
+ traits.GeometriesAPI,
95
+ traits.Geometry(geometry),
96
+ ];
97
+ if (subtype) {
98
+ entityTraits.push(traits.Color(subtype ? colorUtil.set(resourceColors[subtype]) : undefined));
92
99
  }
100
+ const entity = world.spawn(...entityTraits);
101
+ entities.set(entityKey, entity);
93
102
  }
94
- });
103
+ }
104
+ const prevKeys = queryEntityKeys.get(queryKey) ?? new Set();
105
+ // Remove entities no longer present for this specific query
106
+ for (const key of prevKeys) {
107
+ if (!nextKeys.has(key)) {
108
+ entities.get(key)?.destroy();
109
+ entities.delete(key);
110
+ }
111
+ }
112
+ queryEntityKeys.set(queryKey, nextKeys);
95
113
  });
96
114
  }
97
- // Clean up non-active entities
98
- for (const [label, entity] of entities) {
99
- if (!active[label]) {
100
- entity?.destroy();
101
- entities.delete(label);
115
+ // Clean up owners whose queries disappeared entirely
116
+ for (const [queryKey, keys] of queryEntityKeys) {
117
+ if (!activeQueryKeys.has(queryKey)) {
118
+ for (const key of keys) {
119
+ const entity = entities.get(key);
120
+ if (entity && world.has(entity))
121
+ entity.destroy();
122
+ entities.delete(key);
123
+ }
124
+ queryEntityKeys.delete(queryKey);
102
125
  }
103
126
  }
104
127
  });