@viamrobotics/motion-tools 1.13.0 → 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.
Files changed (102) hide show
  1. package/dist/FrameConfigUpdater.svelte.js +1 -1
  2. package/dist/attribute.js +10 -2
  3. package/dist/buf/draw/v1/service_connect.d.ts +14 -25
  4. package/dist/buf/draw/v1/service_connect.js +14 -25
  5. package/dist/buf/draw/v1/service_pb.d.ts +61 -76
  6. package/dist/buf/draw/v1/service_pb.js +58 -111
  7. package/dist/buffer.d.ts +56 -7
  8. package/dist/buffer.js +70 -12
  9. package/dist/color.js +3 -3
  10. package/dist/components/App.svelte +1 -5
  11. package/dist/components/Camera.svelte +1 -7
  12. package/dist/components/Camera.svelte.d.ts +0 -1
  13. package/dist/components/CameraControls.svelte +2 -2
  14. package/dist/components/{Arrows → Entities/Arrows}/ArrowGroups.svelte +3 -3
  15. package/dist/components/{Arrows → Entities/Arrows}/Arrows.svelte +6 -6
  16. package/dist/components/{Arrows → Entities/Arrows}/Arrows.svelte.d.ts +1 -1
  17. package/dist/components/{Entities.svelte → Entities/Entities.svelte} +7 -3
  18. package/dist/components/Entities/Frame.svelte +86 -0
  19. package/dist/components/{Frame.svelte.d.ts → Entities/Frame.svelte.d.ts} +1 -0
  20. package/dist/components/{GLTF.svelte → Entities/GLTF.svelte} +5 -5
  21. package/dist/components/Entities/Geometry.svelte +75 -0
  22. package/dist/components/Entities/Geometry.svelte.d.ts +10 -0
  23. package/dist/components/{Label.svelte → Entities/Label.svelte} +1 -1
  24. package/dist/components/Entities/Line.svelte +90 -0
  25. package/dist/components/Entities/Mesh.svelte +130 -0
  26. package/dist/components/Entities/Mesh.svelte.d.ts +4 -0
  27. package/dist/components/{Points.svelte → Entities/Points.svelte} +5 -5
  28. package/dist/components/{Pose.svelte → Entities/Pose.svelte} +3 -3
  29. package/dist/{hooks/useObjectEvents.svelte.d.ts → components/Entities/hooks/useEntityEvents.svelte.d.ts} +1 -1
  30. package/dist/{hooks/useObjectEvents.svelte.js → components/Entities/hooks/useEntityEvents.svelte.js} +6 -6
  31. package/dist/components/FileDrop/file-names.js +6 -3
  32. package/dist/components/FileDrop/snapshot-dropper.js +8 -4
  33. package/dist/components/FileDrop/useFileDrop.svelte.js +9 -6
  34. package/dist/components/Lasso/Lasso.svelte +4 -4
  35. package/dist/components/PCD.svelte +14 -6
  36. package/dist/components/PCD.svelte.d.ts +2 -0
  37. package/dist/components/Scene.svelte +1 -3
  38. package/dist/components/Selected.svelte +2 -0
  39. package/dist/components/StaticGeometries.svelte +1 -1
  40. package/dist/components/overlay/AddRelationship.svelte +1 -1
  41. package/dist/components/overlay/LiveUpdatesBanner.svelte +4 -6
  42. package/dist/components/overlay/left-pane/buildTree.js +15 -0
  43. package/dist/components/overlay/settings/Settings.svelte +11 -13
  44. package/dist/components/overlay/widgets/Camera.svelte +11 -9
  45. package/dist/components/xr/ArmTeleop.svelte +33 -33
  46. package/dist/components/xr/CameraFeed.svelte +21 -23
  47. package/dist/components/xr/JointLimitsWidget.svelte +19 -5
  48. package/dist/components/xr/XRConfigPanel.svelte +17 -16
  49. package/dist/components/xr/XRControllerSettings.svelte +5 -4
  50. package/dist/components/xr/XRToast.svelte +11 -6
  51. package/dist/ecs/relations.d.ts +1 -0
  52. package/dist/ecs/relations.js +1 -0
  53. package/dist/ecs/traits.d.ts +2 -19
  54. package/dist/ecs/traits.js +33 -6
  55. package/dist/ecs/useQuery.svelte.js +3 -3
  56. package/dist/format.js +1 -1
  57. package/dist/hooks/use3DModels.svelte.js +36 -38
  58. package/dist/hooks/useConfigFrames.svelte.js +2 -7
  59. package/dist/hooks/useDrawAPI.svelte.js +1 -1
  60. package/dist/hooks/useFramelessComponents.svelte.js +1 -1
  61. package/dist/hooks/useFrames.svelte.js +62 -56
  62. package/dist/hooks/useGeometries.svelte.js +59 -36
  63. package/dist/hooks/useLinked.svelte.js +5 -4
  64. package/dist/hooks/usePartConfig.svelte.js +16 -17
  65. package/dist/hooks/usePointcloudObjects.svelte.js +107 -62
  66. package/dist/hooks/usePointclouds.svelte.js +50 -32
  67. package/dist/hooks/usePose.svelte.js +3 -7
  68. package/dist/hooks/useResizable.svelte.js +4 -3
  69. package/dist/hooks/useSettings.svelte.js +2 -2
  70. package/dist/hooks/useWeblabs.svelte.js +3 -2
  71. package/dist/hooks/useWorldState.svelte.js +31 -28
  72. package/dist/loaders/pcd/index.js +2 -1
  73. package/dist/loaders/pcd/worker.inline.d.ts +1 -1
  74. package/dist/loaders/pcd/worker.inline.js +1 -1
  75. package/dist/loaders/pcd/worker.js +1 -1
  76. package/dist/metadata.d.ts +22 -0
  77. package/dist/metadata.js +66 -0
  78. package/dist/snapshot.d.ts +20 -0
  79. package/dist/snapshot.js +72 -28
  80. package/dist/three/InstancedArrows/InstancedArrows.js +1 -1
  81. package/dist/three/InstancedArrows/box.js +1 -1
  82. package/dist/three/InstancedArrows/raycast.js +12 -12
  83. package/dist/three/OBBHelper.d.ts +3 -2
  84. package/dist/three/OBBHelper.js +17 -5
  85. package/package.json +19 -11
  86. package/dist/WorldObject.svelte.d.ts +0 -27
  87. package/dist/WorldObject.svelte.js +0 -114
  88. package/dist/components/Frame.svelte +0 -89
  89. package/dist/components/Geometry.svelte +0 -211
  90. package/dist/components/Geometry.svelte.d.ts +0 -19
  91. package/dist/components/Line.svelte +0 -43
  92. /package/dist/components/{Arrows → Entities/Arrows}/ArrowGroups.svelte.d.ts +0 -0
  93. /package/dist/components/{Entities.svelte.d.ts → Entities/Entities.svelte.d.ts} +0 -0
  94. /package/dist/components/{GLTF.svelte.d.ts → Entities/GLTF.svelte.d.ts} +0 -0
  95. /package/dist/components/{Label.svelte.d.ts → Entities/Label.svelte.d.ts} +0 -0
  96. /package/dist/components/{Line.svelte.d.ts → Entities/Line.svelte.d.ts} +0 -0
  97. /package/dist/components/{LineDots.svelte → Entities/LineDots.svelte} +0 -0
  98. /package/dist/components/{LineDots.svelte.d.ts → Entities/LineDots.svelte.d.ts} +0 -0
  99. /package/dist/components/{LineGeometry.svelte → Entities/LineGeometry.svelte} +0 -0
  100. /package/dist/components/{LineGeometry.svelte.d.ts → Entities/LineGeometry.svelte.d.ts} +0 -0
  101. /package/dist/components/{Points.svelte.d.ts → Entities/Points.svelte.d.ts} +0 -0
  102. /package/dist/components/{Pose.svelte.d.ts → Entities/Pose.svelte.d.ts} +0 -0
@@ -39,7 +39,7 @@ const tryParse = (json) => {
39
39
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
40
  const lowercaseKeys = (obj) => {
41
41
  if (Array.isArray(obj)) {
42
- return obj.map(lowercaseKeys);
42
+ return obj.map((item) => lowercaseKeys(item));
43
43
  }
44
44
  else if (obj && typeof obj === 'object' && obj.constructor === Object) {
45
45
  return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k.toLowerCase(), lowercaseKeys(v)]));
@@ -12,7 +12,7 @@ export const provideFramelessComponents = () => {
12
12
  .map((component) => component.name) ?? [];
13
13
  const fragmentComponentsWithNoFrame = [];
14
14
  for (const fragmentComponentName of Object.keys(partConfig.componentNameToFragmentId)) {
15
- if (frames.current.find((frame) => frame.referenceFrame === fragmentComponentName)) {
15
+ if (frames.current.some((frame) => frame.referenceFrame === fragmentComponentName)) {
16
16
  continue;
17
17
  }
18
18
  fragmentComponentsWithNoFrame.push(fragmentComponentName);
@@ -1,6 +1,7 @@
1
1
  import { getContext, setContext, untrack } from 'svelte';
2
2
  import { MachineConnectionEvent, Transform } from '@viamrobotics/sdk';
3
3
  import { useRobotClient, createRobotQuery, useMachineStatus, useConnectionStatus, } from '@viamrobotics/svelte-sdk';
4
+ import {} from 'koota';
4
5
  import { useLogs } from './useLogs.svelte';
5
6
  import { resourceNameToColor } from '../color';
6
7
  import { useEnvironment } from './useEnvironment.svelte';
@@ -8,6 +9,7 @@ import { createPose } from '../transform';
8
9
  import { useResourceByName } from './useResourceByName.svelte';
9
10
  import { traits, useWorld } from '../ecs';
10
11
  import { useConfigFrames } from './useConfigFrames.svelte';
12
+ import { updateGeometryTrait } from '../ecs/traits';
11
13
  const key = Symbol('frames-context');
12
14
  export const provideFrames = (partID) => {
13
15
  const configFrames = useConfigFrames();
@@ -68,72 +70,76 @@ export const provideFrames = (partID) => {
68
70
  }
69
71
  });
70
72
  $effect.pre(() => {
71
- for (const frame of current) {
72
- if (frame === undefined) {
73
- continue;
74
- }
75
- const name = frame.referenceFrame;
76
- const parent = frame.poseInObserverFrame?.referenceFrame;
77
- const pose = createPose(frame.poseInObserverFrame?.pose);
78
- const center = frame.physicalObject?.center
79
- ? createPose(frame.physicalObject.center)
80
- : undefined;
81
- const resourceName = resourceByName.current[frame.referenceFrame];
82
- const color = resourceNameToColor(resourceName);
83
- const existing = entities.get(name);
84
- if (existing) {
85
- if (!parent || parent === 'world') {
86
- existing.remove(traits.Parent);
73
+ const currentResourcesByName = resourceByName.current;
74
+ const currentPartID = partID();
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;
78
+ untrack(() => {
79
+ const active = {};
80
+ for (const frame of current) {
81
+ const name = frame.referenceFrame;
82
+ const entityKey = `${currentPartID}:${name}`;
83
+ active[entityKey] = true;
84
+ const parent = frame.poseInObserverFrame?.referenceFrame;
85
+ const pose = createPose(frame.poseInObserverFrame?.pose);
86
+ const center = frame.physicalObject?.center
87
+ ? createPose(frame.physicalObject.center)
88
+ : undefined;
89
+ const resourceName = currentResourcesByName[frame.referenceFrame];
90
+ const color = resourceNameToColor(resourceName);
91
+ const existing = entities.get(entityKey);
92
+ if (existing) {
93
+ if (!parent || parent === 'world') {
94
+ existing.remove(traits.Parent);
95
+ }
96
+ else if (parent && existing.has(traits.Parent)) {
97
+ existing.set(traits.Parent, parent);
98
+ }
99
+ else {
100
+ existing.add(traits.Parent(parent));
101
+ }
102
+ if (color) {
103
+ existing.set(traits.Color, color);
104
+ }
105
+ if (center) {
106
+ existing.set(traits.Center, center);
107
+ }
108
+ updateGeometryTrait(existing, frame.physicalObject);
109
+ existing.set(traits.EditedPose, pose);
110
+ continue;
87
111
  }
88
- else if (parent && existing.has(traits.Parent)) {
89
- existing.set(traits.Parent, parent);
90
- }
91
- else {
92
- existing.add(traits.Parent(parent));
112
+ const entityTraits = [
113
+ traits.Name(name),
114
+ traits.Pose(pose),
115
+ traits.EditedPose(pose),
116
+ traits.FramesAPI,
117
+ traits.ShowAxesHelper,
118
+ ];
119
+ if (parent && parent !== 'world') {
120
+ entityTraits.push(traits.Parent(parent));
93
121
  }
94
122
  if (color) {
95
- existing.set(traits.Color, color);
123
+ entityTraits.push(traits.Color(color));
96
124
  }
97
125
  if (center) {
98
- existing.set(traits.Center, center);
126
+ entityTraits.push(traits.Center(center));
99
127
  }
100
- existing.remove(traits.Box, traits.Sphere, traits.BufferGeometry, traits.Capsule);
101
128
  if (frame.physicalObject) {
102
- const geometry = traits.Geometry(frame.physicalObject);
103
- existing.add(geometry);
129
+ entityTraits.push(traits.Geometry(frame.physicalObject));
104
130
  }
105
- existing.set(traits.EditedPose, pose);
106
- continue;
107
- }
108
- const entityTraits = [
109
- traits.Name(name),
110
- traits.Pose(pose),
111
- traits.EditedPose(pose),
112
- traits.FramesAPI,
113
- traits.ShowAxesHelper,
114
- ];
115
- if (parent && parent !== 'world') {
116
- entityTraits.push(traits.Parent(parent));
131
+ const entity = world.spawn(...entityTraits);
132
+ entities.set(entityKey, entity);
117
133
  }
118
- if (color) {
119
- entityTraits.push(traits.Color(color));
120
- }
121
- if (center) {
122
- entityTraits.push(traits.Center(center));
123
- }
124
- if (frame.physicalObject) {
125
- entityTraits.push(traits.Geometry(frame.physicalObject));
126
- }
127
- const entity = world.spawn(...entityTraits);
128
- entities.set(name, entity);
129
- }
130
- // Clean up non-active entities
131
- for (const [name, entity] of entities) {
132
- if (!frames[name]) {
133
- entity?.destroy();
134
- entities.delete(name);
134
+ // Clean up non-active entities
135
+ for (const [entityKey, entity] of entities) {
136
+ if (!active[entityKey]) {
137
+ entity?.destroy();
138
+ entities.delete(entityKey);
139
+ continue;
140
+ }
135
141
  }
136
- }
142
+ });
137
143
  });
138
144
  setContext(key, {
139
145
  get current() {
@@ -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 ? 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
  });
@@ -18,11 +18,12 @@ export const provideLinkedEntities = () => {
18
18
  linkedEntities = linkedEntities.filter((e) => e !== target);
19
19
  }
20
20
  });
21
+ const unsub = () => {
22
+ unsubAdd();
23
+ unsubRemove();
24
+ };
21
25
  $effect(() => {
22
- return () => {
23
- unsubAdd();
24
- unsubRemove();
25
- };
26
+ return unsub;
26
27
  });
27
28
  setContext(linkedKey, {
28
29
  get current() {
@@ -79,16 +79,16 @@ export const providePartConfig = (partID, params) => {
79
79
  const existingFrameIndex = fragmentMod.mods.findLastIndex(
80
80
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
81
  (mod) => mod?.['$set']?.[modSetPath] !== undefined);
82
- if (existingFrameIndex !== -1) {
82
+ if (existingFrameIndex === -1) {
83
+ fragmentMod.mods.push(frame);
84
+ }
85
+ else {
83
86
  const existingGeometry = fragmentMod.mods[existingFrameIndex]['$set']?.[modSetPath].geometry;
84
87
  if (existingGeometry && !frameGeometry) {
85
88
  frame['$set'][modSetPath].geometry = existingGeometry;
86
89
  }
87
90
  fragmentMod.mods[existingFrameIndex] = frame;
88
91
  }
89
- else {
90
- fragmentMod.mods.push(frame);
91
- }
92
92
  config.set(newConfig);
93
93
  };
94
94
  const updatePartFrame = (componentName, referenceFrame, pose, geometry) => {
@@ -127,11 +127,10 @@ export const providePartConfig = (partID, params) => {
127
127
  const deletePartFrame = (componentName) => {
128
128
  const newConfig = getCurrent();
129
129
  const component = newConfig?.components?.find(({ name }) => name === componentName);
130
- if (!component) {
131
- return;
130
+ if (component) {
131
+ delete component.frame;
132
+ config.set(newConfig);
132
133
  }
133
- delete component.frame;
134
- config.set(newConfig);
135
134
  };
136
135
  const deleteFragmentFrame = (fragmentId, componentName) => {
137
136
  const newConfig = getCurrent();
@@ -168,29 +167,29 @@ export const providePartConfig = (partID, params) => {
168
167
  },
169
168
  updateFrame: (componentName, referenceFrame, framePosition, frameGeometry) => {
170
169
  const fragmentId = config.componentNameToFragmentId[componentName];
171
- if (fragmentId !== undefined) {
172
- updateFragmentFrame(fragmentId, componentName, referenceFrame, framePosition, frameGeometry);
170
+ if (fragmentId === undefined) {
171
+ updatePartFrame(componentName, referenceFrame, framePosition, frameGeometry);
173
172
  }
174
173
  else {
175
- updatePartFrame(componentName, referenceFrame, framePosition, frameGeometry);
174
+ updateFragmentFrame(fragmentId, componentName, referenceFrame, framePosition, frameGeometry);
176
175
  }
177
176
  },
178
177
  deleteFrame: (componentName) => {
179
178
  const fragmentId = config.componentNameToFragmentId[componentName];
180
- if (fragmentId !== undefined) {
181
- deleteFragmentFrame(fragmentId, componentName);
179
+ if (fragmentId === undefined) {
180
+ deletePartFrame(componentName);
182
181
  }
183
182
  else {
184
- deletePartFrame(componentName);
183
+ deleteFragmentFrame(fragmentId, componentName);
185
184
  }
186
185
  },
187
186
  createFrame: (componentName) => {
188
187
  const fragmentId = config.componentNameToFragmentId[componentName];
189
- if (fragmentId !== undefined) {
190
- createFragmentFrame(fragmentId, componentName);
188
+ if (fragmentId === undefined) {
189
+ createPartFrame(componentName);
191
190
  }
192
191
  else {
193
- createPartFrame(componentName);
192
+ createFragmentFrame(fragmentId, componentName);
194
193
  }
195
194
  },
196
195
  save: () => config.save?.(),
@@ -9,6 +9,7 @@ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
9
9
  import { useEnvironment } from './useEnvironment.svelte';
10
10
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
11
11
  import { createPose } from '../transform';
12
+ import { updateGeometryTrait } from '../ecs/traits';
12
13
  const key = Symbol('pointcloud-object-context');
13
14
  export const providePointcloudObjects = (partID) => {
14
15
  const world = useWorld();
@@ -47,18 +48,18 @@ export const providePointcloudObjects = (partID) => {
47
48
  */
48
49
  $effect(() => {
49
50
  for (const [name, query] of propQueries) {
50
- if (name && query.data?.objectPointCloudsSupported === false) {
51
- if (disabledVisionServices.get(name) === undefined) {
52
- disabledVisionServices.set(name, true);
53
- }
51
+ if (name &&
52
+ query.data?.objectPointCloudsSupported === false &&
53
+ disabledVisionServices.get(name) === undefined) {
54
+ disabledVisionServices.set(name, true);
54
55
  }
55
56
  }
56
57
  });
57
58
  const logs = useLogs();
58
59
  const interval = $derived(refreshRates.get(RefreshRates.pointclouds));
59
60
  const options = $derived({
60
- enabled: interval !== -1,
61
- refetchInterval: (interval === 0 ? false : interval),
61
+ enabled: interval !== RefetchRates.OFF,
62
+ refetchInterval: (interval === RefetchRates.MANUAL ? false : interval),
62
63
  });
63
64
  const queries = $derived(enabledClients.map((client) => [
64
65
  client.current.name,
@@ -79,73 +80,117 @@ export const providePointcloudObjects = (partID) => {
79
80
  }
80
81
  });
81
82
  const entities = new Map();
83
+ const queryEntityKeys = new Map();
84
+ const destroyEntity = (key) => {
85
+ const entity = entities.get(key);
86
+ if (entity) {
87
+ if (world.has(entity))
88
+ entity.destroy();
89
+ entities.delete(key);
90
+ }
91
+ };
82
92
  $effect(() => {
83
- const active = {};
93
+ const currentPartID = partID();
94
+ const activeQueryKeys = new Set();
84
95
  for (const [name, query] of queries) {
85
- untrack(() => {
86
- $effect(() => {
87
- const { data } = query;
88
- if (!data || data.length === 0)
89
- return;
90
- let index = 0;
91
- for (const { geometries: geometriesInFrame, pointCloud } of data) {
92
- if (pointCloud.length > 0) {
93
- parsePcdInWorker(pointCloud).then(({ positions, colors }) => {
94
- const poincloudLabel = `${name} pointcloud ${index + 1}`;
95
- const existing = entities.get(poincloudLabel);
96
- if (existing) {
97
- const geometry = existing.get(traits.BufferGeometry);
98
- if (geometry) {
99
- updateBufferGeometry(geometry, positions, colors);
100
- }
101
- }
102
- else {
103
- const geometry = createBufferGeometry(positions, colors);
104
- const entity = world.spawn(traits.Name(poincloudLabel), traits.BufferGeometry(geometry), traits.Points);
105
- entities.set(poincloudLabel, entity);
106
- }
107
- });
96
+ const queryKey = `${currentPartID}:${name}`;
97
+ activeQueryKeys.add(queryKey);
98
+ $effect(() => {
99
+ const { data } = query;
100
+ let disposed = false;
101
+ const nextKeys = new Set();
102
+ const reconcileRemovedKeys = () => {
103
+ const prevKeys = queryEntityKeys.get(queryKey) ?? new Set();
104
+ for (const key of prevKeys) {
105
+ if (!nextKeys.has(key)) {
106
+ destroyEntity(key);
108
107
  }
109
- if (geometriesInFrame) {
110
- let geometryIndex = 0;
111
- for (const geometry of geometriesInFrame.geometries) {
112
- const geometryLabel = `${name} pointcloud ${index} geometry ${geometryIndex + 1}`;
113
- const pose = createPose(geometry.center);
114
- active[geometryLabel] = true;
115
- const existing = entities.get(geometryLabel);
116
- if (existing) {
117
- existing.set(traits.Pose, pose);
108
+ }
109
+ queryEntityKeys.set(queryKey, new Set(nextKeys));
110
+ };
111
+ if (!data || data.length === 0) {
112
+ reconcileRemovedKeys();
113
+ return () => {
114
+ disposed = true;
115
+ };
116
+ }
117
+ let index = 0;
118
+ for (const { geometries: geometriesInFrame, pointCloud } of data) {
119
+ if (pointCloud.length > 0) {
120
+ const pointcloudLabel = `${name} pointcloud ${index + 1}`;
121
+ nextKeys.add(pointcloudLabel);
122
+ parsePcdInWorker(pointCloud)
123
+ .then(({ positions, colors }) => {
124
+ if (disposed) {
125
+ return;
126
+ }
127
+ if (!nextKeys.has(pointcloudLabel)) {
128
+ return;
129
+ }
130
+ const existing = entities.get(pointcloudLabel);
131
+ if (existing) {
132
+ const geometry = existing.get(traits.BufferGeometry);
133
+ if (geometry) {
134
+ updateBufferGeometry(geometry, positions, colors);
118
135
  }
119
- else {
120
- const entityTraits = [
121
- traits.Name(geometryLabel),
122
- traits.Pose(pose),
123
- traits.GeometriesAPI,
124
- traits.Geometry(geometry),
125
- traits.Opacity(0.2),
126
- traits.Color({ r: 0, g: 1, b: 0 }),
127
- ];
128
- if (geometriesInFrame.referenceFrame) {
129
- entityTraits.push(traits.Parent(geometriesInFrame.referenceFrame));
130
- }
131
- const entity = world.spawn(...entityTraits);
132
- entities.set(geometryLabel, entity);
136
+ }
137
+ else {
138
+ const geometry = createBufferGeometry(positions, colors);
139
+ const entity = world.spawn(traits.Name(pointcloudLabel), traits.BufferGeometry(geometry), traits.Points);
140
+ entities.set(pointcloudLabel, entity);
141
+ }
142
+ })
143
+ .catch((error) => {
144
+ if (disposed) {
145
+ return;
146
+ }
147
+ logs.add(error?.reason ?? error?.message ?? 'Failed to parse pointcloud', 'error');
148
+ });
149
+ }
150
+ if (geometriesInFrame) {
151
+ let geometryIndex = 0;
152
+ for (const geometry of geometriesInFrame.geometries) {
153
+ const geometryLabel = `${name} pointcloud ${index + 1} geometry ${geometryIndex + 1}`;
154
+ nextKeys.add(geometryLabel);
155
+ const center = createPose(geometry.center);
156
+ const existing = entities.get(geometryLabel);
157
+ if (existing) {
158
+ existing.set(traits.Center, center);
159
+ updateGeometryTrait(existing, geometry);
160
+ }
161
+ else {
162
+ const entityTraits = [
163
+ traits.Name(geometryLabel),
164
+ traits.Center(center),
165
+ traits.GeometriesAPI,
166
+ traits.Geometry(geometry),
167
+ traits.Opacity(0.2),
168
+ traits.Color({ r: 0, g: 1, b: 0 }),
169
+ ];
170
+ if (geometriesInFrame.referenceFrame) {
171
+ entityTraits.push(traits.Parent(geometriesInFrame.referenceFrame));
133
172
  }
134
- geometryIndex += 1;
173
+ const entity = world.spawn(...entityTraits);
174
+ entities.set(geometryLabel, entity);
135
175
  }
176
+ geometryIndex += 1;
136
177
  }
137
- index += 1;
138
178
  }
139
- });
179
+ index += 1;
180
+ }
181
+ reconcileRemovedKeys();
182
+ return () => {
183
+ disposed = true;
184
+ };
140
185
  });
141
186
  }
142
- // Clean up old entities
143
- for (const [label, entity] of entities) {
144
- if (!active[label]) {
145
- if (world.has(entity)) {
146
- entity.destroy();
187
+ // cleanup queries that disappeared entirely
188
+ for (const [queryKey, keys] of queryEntityKeys) {
189
+ if (!activeQueryKeys.has(queryKey)) {
190
+ for (const key of keys) {
191
+ destroyEntity(key);
147
192
  }
148
- entities.delete(label);
193
+ queryEntityKeys.delete(queryKey);
149
194
  }
150
195
  }
151
196
  });