@viamrobotics/motion-tools 1.13.1 → 1.15.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 (136) hide show
  1. package/dist/FrameConfigUpdater.svelte.d.ts +2 -2
  2. package/dist/HoverUpdater.svelte.d.ts +1 -1
  3. package/dist/attribute.js +11 -3
  4. package/dist/buffer.d.ts +56 -7
  5. package/dist/buffer.js +70 -12
  6. package/dist/color.d.ts +1 -1
  7. package/dist/color.js +2 -2
  8. package/dist/components/App.svelte +25 -21
  9. package/dist/components/App.svelte.d.ts +1 -1
  10. package/dist/components/BatchedArrows.svelte +5 -3
  11. package/dist/components/Camera.svelte +1 -0
  12. package/dist/components/CameraControls.svelte +5 -3
  13. package/dist/components/Entities/Arrows/ArrowGroups.svelte +6 -3
  14. package/dist/components/Entities/Arrows/Arrows.svelte +6 -3
  15. package/dist/components/Entities/Entities.svelte +9 -7
  16. package/dist/components/Entities/Frame.svelte +48 -48
  17. package/dist/components/Entities/Frame.svelte.d.ts +3 -2
  18. package/dist/components/Entities/GLTF.svelte +8 -5
  19. package/dist/components/Entities/GLTF.svelte.d.ts +2 -2
  20. package/dist/components/Entities/Geometry.svelte +45 -173
  21. package/dist/components/Entities/Geometry.svelte.d.ts +5 -14
  22. package/dist/components/Entities/Line.svelte +69 -19
  23. package/dist/components/Entities/Line.svelte.d.ts +1 -1
  24. package/dist/components/Entities/LineDots.svelte +1 -1
  25. package/dist/components/Entities/LineGeometry.svelte +1 -1
  26. package/dist/components/Entities/Mesh.svelte +133 -0
  27. package/dist/components/Entities/Mesh.svelte.d.ts +4 -0
  28. package/dist/components/Entities/Points.svelte +9 -6
  29. package/dist/components/Entities/Points.svelte.d.ts +2 -2
  30. package/dist/components/Entities/Pose.svelte +4 -3
  31. package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +1 -1
  32. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +2 -2
  33. package/dist/components/FileDrop/FileDrop.svelte +10 -6
  34. package/dist/components/FileDrop/file-dropper.d.ts +1 -1
  35. package/dist/components/FileDrop/pcd-dropper.js +1 -1
  36. package/dist/components/FileDrop/ply-dropper.js +1 -1
  37. package/dist/components/FileDrop/snapshot-dropper.js +1 -1
  38. package/dist/components/Focus.svelte +4 -2
  39. package/dist/components/KeyboardControls.svelte +4 -2
  40. package/dist/components/Lasso/Debug.svelte +5 -2
  41. package/dist/components/Lasso/Lasso.svelte +9 -6
  42. package/dist/components/Lasso/Tool.svelte +10 -7
  43. package/dist/components/MeasureTool/MeasurePoint.svelte +2 -1
  44. package/dist/components/MeasureTool/MeasurePoint.svelte.d.ts +1 -1
  45. package/dist/components/MeasureTool/MeasureTool.svelte +7 -5
  46. package/dist/components/PCD.svelte +4 -3
  47. package/dist/components/PointerMissBox.svelte +3 -2
  48. package/dist/components/Scene.svelte +12 -9
  49. package/dist/components/SceneProviders.svelte +20 -18
  50. package/dist/components/Selected.svelte +7 -3
  51. package/dist/components/Snapshot.svelte +8 -5
  52. package/dist/components/StaticGeometries.svelte +10 -7
  53. package/dist/components/hover/HoveredEntities.svelte +2 -1
  54. package/dist/components/hover/HoveredEntity.svelte +2 -1
  55. package/dist/components/hover/HoveredEntityTooltip.svelte +1 -0
  56. package/dist/components/hover/LinkedHoveredEntity.svelte +7 -5
  57. package/dist/components/overlay/AddRelationship.svelte +4 -2
  58. package/dist/components/overlay/Details.svelte +21 -19
  59. package/dist/components/overlay/FloatingPanel.svelte +40 -3
  60. package/dist/components/overlay/FloatingPanel.svelte.d.ts +1 -0
  61. package/dist/components/overlay/LiveUpdatesBanner.svelte +1 -0
  62. package/dist/components/overlay/Logs.svelte +4 -2
  63. package/dist/components/overlay/Popover.svelte +3 -2
  64. package/dist/components/overlay/RefreshRate.svelte +4 -2
  65. package/dist/components/overlay/dashboard/Button.svelte +2 -1
  66. package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
  67. package/dist/components/overlay/dashboard/Dashboard.svelte +3 -1
  68. package/dist/components/overlay/left-pane/AddFrames.svelte +4 -2
  69. package/dist/components/overlay/left-pane/Drawer.svelte +3 -2
  70. package/dist/components/overlay/left-pane/Tree.svelte +6 -12
  71. package/dist/components/overlay/left-pane/TreeContainer.svelte +33 -50
  72. package/dist/components/overlay/left-pane/TreeContainer.svelte.d.ts +1 -1
  73. package/dist/components/overlay/left-pane/buildTree.js +15 -0
  74. package/dist/components/overlay/settings/Settings.svelte +37 -10
  75. package/dist/components/overlay/settings/Tabs.svelte +2 -1
  76. package/dist/components/overlay/widgets/ArmPositions.svelte +3 -2
  77. package/dist/components/overlay/widgets/Camera.svelte +6 -5
  78. package/dist/components/weblab/WeblabActive.svelte +2 -1
  79. package/dist/components/xr/ArmTeleop.svelte +7 -6
  80. package/dist/components/xr/BentPlaneGeometry.svelte +3 -2
  81. package/dist/components/xr/CameraFeed.svelte +2 -0
  82. package/dist/components/xr/Controllers.svelte +5 -3
  83. package/dist/components/xr/Draggable.svelte +4 -3
  84. package/dist/components/xr/HandCollider.svelte +2 -1
  85. package/dist/components/xr/JointLimitsWidget.svelte +1 -0
  86. package/dist/components/xr/OriginMarker.svelte +2 -1
  87. package/dist/components/xr/PointDistance.svelte +3 -2
  88. package/dist/components/xr/XR.svelte +8 -6
  89. package/dist/components/xr/XRConfigPanel.svelte +4 -3
  90. package/dist/components/xr/XRControllerSettings.svelte +2 -1
  91. package/dist/components/xr/XRToast.svelte +4 -3
  92. package/dist/ecs/traits.d.ts +3 -20
  93. package/dist/ecs/traits.js +34 -7
  94. package/dist/ecs/useQuery.svelte.js +1 -1
  95. package/dist/frame.js +1 -1
  96. package/dist/hooks/use3DModels.svelte.js +4 -6
  97. package/dist/hooks/useConfigFrames.svelte.js +3 -3
  98. package/dist/hooks/useDrawAPI.svelte.js +9 -9
  99. package/dist/hooks/useFramelessComponents.svelte.js +1 -1
  100. package/dist/hooks/useFrames.svelte.js +18 -19
  101. package/dist/hooks/useGeometries.svelte.js +66 -43
  102. package/dist/hooks/useMouseRaycaster.svelte.d.ts +1 -1
  103. package/dist/hooks/useMouseRaycaster.svelte.js +1 -1
  104. package/dist/hooks/usePartConfig.svelte.d.ts +1 -1
  105. package/dist/hooks/usePartConfig.svelte.js +3 -3
  106. package/dist/hooks/usePointcloudObjects.svelte.js +108 -63
  107. package/dist/hooks/usePointclouds.svelte.js +53 -33
  108. package/dist/hooks/usePose.svelte.js +7 -7
  109. package/dist/hooks/useSelection.svelte.d.ts +1 -1
  110. package/dist/hooks/useWeblabs.svelte.d.ts +1 -0
  111. package/dist/hooks/useWeblabs.svelte.js +15 -3
  112. package/dist/hooks/useWorldState.svelte.js +31 -31
  113. package/dist/metadata.d.ts +22 -0
  114. package/dist/metadata.js +66 -0
  115. package/dist/plugins/bvh.svelte.js +2 -2
  116. package/dist/snapshot.d.ts +22 -2
  117. package/dist/snapshot.js +67 -25
  118. package/dist/three/BatchedArrow.d.ts +1 -1
  119. package/dist/three/BatchedArrow.js +1 -1
  120. package/dist/three/InstancedArrows/InstancedArrows.d.ts +1 -1
  121. package/dist/three/InstancedArrows/InstancedArrows.js +3 -3
  122. package/dist/three/InstancedArrows/box.js +1 -1
  123. package/dist/three/InstancedArrows/geometry.js +1 -1
  124. package/dist/three/InstancedArrows/raycast.d.ts +1 -1
  125. package/dist/three/InstancedArrows/raycast.js +1 -1
  126. package/dist/three/OBBHelper.d.ts +3 -2
  127. package/dist/three/OBBHelper.js +17 -5
  128. package/dist/three/OrientationVector.js +1 -1
  129. package/dist/transform.js +1 -1
  130. package/package.json +3 -2
  131. package/dist/WorldObject.svelte.d.ts +0 -27
  132. package/dist/WorldObject.svelte.js +0 -127
  133. package/dist/hooks/__tests__/fixtures/ResizableTestWrapper.svelte +0 -41
  134. package/dist/hooks/__tests__/fixtures/ResizableTestWrapper.svelte.d.ts +0 -6
  135. package/dist/hooks/useResizable.svelte.d.ts +0 -12
  136. package/dist/hooks/useResizable.svelte.js +0 -46
@@ -1,5 +1,5 @@
1
+ import { createQuery, $internal as internal } from 'koota';
1
2
  import { untrack } from 'svelte';
2
- import { $internal as internal, createQuery } from 'koota';
3
3
  import { useWorld } from './useWorld';
4
4
  export function useQuery(...parameters) {
5
5
  const world = useWorld();
package/dist/frame.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // TODO: replace with types exported from the sdk when created
2
2
  import { UuidTool } from 'uuid-tool';
3
- import { createPoseFromFrame } from './transform';
4
3
  import { createGeometryFromFrame } from './geometry';
4
+ import { createPoseFromFrame } from './transform';
5
5
  export const createFrame = (geometry) => {
6
6
  return {
7
7
  parent: 'world',
@@ -1,10 +1,10 @@
1
+ import { isInstanceOf } from '@threlte/core';
1
2
  import { ArmClient } from '@viamrobotics/sdk';
2
3
  import { createResourceClient, useResourceNames } from '@viamrobotics/svelte-sdk';
3
4
  import { getContext, setContext } from 'svelte';
4
- import { useSettings } from './useSettings.svelte';
5
- import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
6
5
  import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
7
- import { isInstanceOf } from '@threlte/core';
6
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
7
+ import { useSettings } from './useSettings.svelte';
8
8
  const gltfLoader = new GLTFLoader();
9
9
  const dracoLoader = new DRACOLoader();
10
10
  dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
@@ -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
  }
@@ -1,8 +1,8 @@
1
- import { useEnvironment } from './useEnvironment.svelte';
2
- import { usePartConfig } from './usePartConfig.svelte';
3
- import { createTransformFromFrame } from '../frame';
4
1
  import { Transform } from '@viamrobotics/sdk';
5
2
  import { getContext, setContext } from 'svelte';
3
+ import { createTransformFromFrame } from '../frame';
4
+ import { useEnvironment } from './useEnvironment.svelte';
5
+ import { usePartConfig } from './usePartConfig.svelte';
6
6
  const key = Symbol('config-frames-context');
7
7
  export const provideConfigFrames = () => {
8
8
  const environment = useEnvironment();
@@ -1,19 +1,19 @@
1
+ import { useThrelte } from '@threlte/core';
2
+ import {} from 'koota';
1
3
  import { getContext, setContext } from 'svelte';
2
4
  import { Color, Vector3, Vector4 } from 'three';
3
5
  import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
4
- import { UuidTool } from 'uuid-tool';
5
6
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
7
+ import { UuidTool } from 'uuid-tool';
8
+ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
9
+ import { STRIDE } from '../buffer';
10
+ import { traits, useWorld } from '../ecs';
11
+ import { createBox, createCapsule, createSphere } from '../geometry';
12
+ import { parsePlyInput } from '../ply';
6
13
  import { createPose, createPoseFromFrame } from '../transform';
7
14
  import { useCameraControls } from './useControls.svelte';
8
- import { useWorld, traits } from '../ecs';
9
- import { useThrelte } from '@threlte/core';
10
- import {} from 'koota';
11
- import { parsePlyInput } from '../ply';
12
- import { useLogs } from './useLogs.svelte';
13
- import { createBox, createCapsule, createSphere } from '../geometry';
14
15
  import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
15
- import { createBufferGeometry, updateBufferGeometry } from '../attribute';
16
- import { STRIDE } from '../buffer';
16
+ import { useLogs } from './useLogs.svelte';
17
17
  const colorUtil = new Color();
18
18
  const bufferTypes = {
19
19
  DRAW_POINTS: 0,
@@ -1,6 +1,6 @@
1
1
  import { getContext, setContext } from 'svelte';
2
- import { usePartConfig } from './usePartConfig.svelte';
3
2
  import { useFrames } from './useFrames.svelte';
3
+ import { usePartConfig } from './usePartConfig.svelte';
4
4
  const key = Symbol('frameless-components-context');
5
5
  export const provideFramelessComponents = () => {
6
6
  const partConfig = usePartConfig();
@@ -1,14 +1,15 @@
1
- import { getContext, setContext, untrack } from 'svelte';
2
1
  import { MachineConnectionEvent, Transform } from '@viamrobotics/sdk';
3
- import { useRobotClient, createRobotQuery, useMachineStatus, useConnectionStatus, } from '@viamrobotics/svelte-sdk';
2
+ import { createRobotQuery, useConnectionStatus, useMachineStatus, useRobotClient, } from '@viamrobotics/svelte-sdk';
4
3
  import {} from 'koota';
5
- import { useLogs } from './useLogs.svelte';
4
+ import { getContext, setContext, untrack } from 'svelte';
6
5
  import { resourceNameToColor } from '../color';
7
- import { useEnvironment } from './useEnvironment.svelte';
8
- import { createPose } from '../transform';
9
- import { useResourceByName } from './useResourceByName.svelte';
10
6
  import { traits, useWorld } from '../ecs';
7
+ import { updateGeometryTrait } from '../ecs/traits';
8
+ import { createPose } from '../transform';
11
9
  import { useConfigFrames } from './useConfigFrames.svelte';
10
+ import { useEnvironment } from './useEnvironment.svelte';
11
+ import { useLogs } from './useLogs.svelte';
12
+ import { useResourceByName } from './useResourceByName.svelte';
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
  }
@@ -1,16 +1,17 @@
1
1
  import { ArmClient, CameraClient, GantryClient, GripperClient } from '@viamrobotics/sdk';
2
- import { untrack, setContext, getContext } from 'svelte';
3
- import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
4
2
  import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
5
- import { useLogs } from './useLogs.svelte';
6
- import { resourceColors } from '../color';
3
+ import {} from 'koota';
4
+ import { getContext, setContext, untrack } from 'svelte';
7
5
  import { Color } from 'three';
8
- import { useResourceByName } from './useResourceByName.svelte';
6
+ import { resourceColors } from '../color';
7
+ import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
9
8
  import { traits, useWorld } from '../ecs';
10
- import {} from 'koota';
9
+ import { updateGeometryTrait } from '../ecs/traits';
11
10
  import { createPose } from '../transform';
12
- import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
13
11
  import { useEnvironment } from './useEnvironment.svelte';
12
+ import { useLogs } from './useLogs.svelte';
13
+ import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
14
+ import { useResourceByName } from './useResourceByName.svelte';
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
  });
@@ -1,4 +1,4 @@
1
- import { Raycaster, type Intersection } from 'three';
1
+ import { type Intersection, Raycaster } from 'three';
2
2
  type EventNames = 'click' | 'move' | 'pointerenter' | 'pointerleave';
3
3
  interface RaycastEvent<T extends EventNames> {
4
4
  type: T;
@@ -1,5 +1,5 @@
1
- import { Vector2, Raycaster, EventDispatcher } from 'three';
2
1
  import { useThrelte } from '@threlte/core';
2
+ import { EventDispatcher, Raycaster, Vector2 } from 'three';
3
3
  const pointerDown = new Vector2();
4
4
  const pointerUp = new Vector2();
5
5
  const pointerMove = new Vector2();
@@ -1,5 +1,5 @@
1
+ import { Pose, Struct } from '@viamrobotics/sdk';
1
2
  import { type Frame } from '../frame';
2
- import { Struct, Pose } from '@viamrobotics/sdk';
3
3
  export interface PartConfig {
4
4
  components: {
5
5
  name: string;
@@ -1,8 +1,8 @@
1
- import { createFrame } from '../frame';
2
- import { createPoseFromFrame } from '../transform';
3
- import { Struct, Pose } from '@viamrobotics/sdk';
1
+ import { Pose, Struct } from '@viamrobotics/sdk';
4
2
  import { createAppMutation, createAppQuery } from '@viamrobotics/svelte-sdk';
5
3
  import { getContext, setContext } from 'svelte';
4
+ import { createFrame } from '../frame';
5
+ import { createPoseFromFrame } from '../transform';
6
6
  const key = Symbol('part-config-context');
7
7
  export const providePartConfig = (partID, params) => {
8
8
  const props = $derived(params());
@@ -1,14 +1,15 @@
1
1
  import { VisionClient } from '@viamrobotics/sdk';
2
2
  import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
- import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
4
- import { useLogs } from './useLogs.svelte';
5
- import { parsePcdInWorker } from '../lib';
6
3
  import { getContext, setContext, untrack } from 'svelte';
7
- import { traits, useWorld } from '../ecs';
8
4
  import { createBufferGeometry, updateBufferGeometry } from '../attribute';
9
- import { useEnvironment } from './useEnvironment.svelte';
10
5
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
6
+ import { traits, useWorld } from '../ecs';
7
+ import { updateGeometryTrait } from '../ecs/traits';
8
+ import { parsePcdInWorker } from '../lib';
11
9
  import { createPose } from '../transform';
10
+ import { useEnvironment } from './useEnvironment.svelte';
11
+ import { useLogs } from './useLogs.svelte';
12
+ import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
12
13
  const key = Symbol('pointcloud-object-context');
13
14
  export const providePointcloudObjects = (partID) => {
14
15
  const world = useWorld();
@@ -57,8 +58,8 @@ export const providePointcloudObjects = (partID) => {
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
  });