@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,16 +1,13 @@
1
1
  import { CameraClient } from '@viamrobotics/sdk';
2
- import { setContext, getContext, untrack } from 'svelte';
3
2
  import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
4
- import { parsePcdInWorker } from '../loaders/pcd';
5
- import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
6
- import { useLogs } from './useLogs.svelte';
3
+ import { getContext, setContext, untrack } from 'svelte';
4
+ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
7
5
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
8
6
  import { traits, useWorld } from '../ecs';
7
+ import { parsePcdInWorker } from '../loaders/pcd';
9
8
  import { useEnvironment } from './useEnvironment.svelte';
10
- import { createBufferGeometry, updateBufferGeometry } from '../attribute';
11
- const typeSafeObjectFromEntries = (entries) => {
12
- return Object.fromEntries(entries);
13
- };
9
+ import { useLogs } from './useLogs.svelte';
10
+ import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
14
11
  const key = Symbol('pointcloud-context');
15
12
  export const providePointclouds = (partID) => {
16
13
  const environment = useEnvironment();
@@ -60,7 +57,6 @@ export const providePointclouds = (partID) => {
60
57
  refetchInterval: interval === RefetchRates.MANUAL ? false : interval,
61
58
  });
62
59
  const queries = $derived(enabledClients.map((client) => [client.current.name, createResourceQuery(client, 'getPointCloud', () => options)]));
63
- const queryMap = $derived(typeSafeObjectFromEntries(queries));
64
60
  $effect(() => {
65
61
  for (const [name, query] of queries) {
66
62
  untrack(() => {
@@ -77,39 +73,63 @@ export const providePointclouds = (partID) => {
77
73
  });
78
74
  const entities = new Map();
79
75
  $effect(() => {
76
+ const currentPartID = partID();
77
+ const activeQueryKeys = new Set();
80
78
  for (const [name, query] of queries) {
81
- untrack(() => {
82
- $effect(() => {
83
- const { data } = query;
84
- if (!data || data.length === 0)
79
+ const queryKey = `${currentPartID}:${name}`;
80
+ activeQueryKeys.add(queryKey);
81
+ $effect(() => {
82
+ const { data } = query;
83
+ let disposed = false;
84
+ const destroyEntity = () => {
85
+ const entity = entities.get(queryKey);
86
+ if (entity) {
87
+ if (world.has(entity))
88
+ entity.destroy();
89
+ entities.delete(queryKey);
90
+ }
91
+ };
92
+ if (!data || data.length === 0) {
93
+ destroyEntity();
94
+ return () => {
95
+ disposed = true;
96
+ };
97
+ }
98
+ parsePcdInWorker(data)
99
+ .then(({ positions, colors }) => {
100
+ if (disposed) {
85
101
  return;
86
- parsePcdInWorker(data)
87
- .then(({ positions, colors }) => {
88
- const existing = entities.get(name);
89
- if (existing) {
90
- const geometry = existing.get(traits.BufferGeometry);
91
- if (geometry) {
92
- updateBufferGeometry(geometry, positions, colors);
93
- return;
94
- }
102
+ }
103
+ const existing = entities.get(queryKey);
104
+ if (existing) {
105
+ const geometry = existing.get(traits.BufferGeometry);
106
+ if (geometry) {
107
+ updateBufferGeometry(geometry, positions, colors);
108
+ return;
95
109
  }
96
- const geometry = createBufferGeometry(positions, colors);
97
- const entity = world.spawn(traits.Parent(name), traits.Name(`${name} pointcloud`), traits.BufferGeometry(geometry), traits.Points);
98
- entities.set(name, entity);
99
- })
100
- .catch((error) => {
101
- logs.add(error.reason, 'error');
102
- });
110
+ }
111
+ const geometry = createBufferGeometry(positions, colors);
112
+ const entity = world.spawn(traits.Parent(name), traits.Name(`${name} pointcloud`), traits.BufferGeometry(geometry), traits.Points);
113
+ entities.set(queryKey, entity);
114
+ })
115
+ .catch((error) => {
116
+ if (disposed) {
117
+ return;
118
+ }
119
+ logs.add(error?.reason ?? error?.message ?? 'Failed to parse pointcloud', 'error');
103
120
  });
121
+ return () => {
122
+ disposed = true;
123
+ };
104
124
  });
105
125
  }
106
- // Clean up old entities
107
- for (const [name, entity] of entities) {
108
- if (!queryMap[name]?.data) {
126
+ // clean up queries that disappeared entirely
127
+ for (const [queryKey, entity] of entities) {
128
+ if (!activeQueryKeys.has(queryKey)) {
109
129
  if (world.has(entity)) {
110
130
  entity.destroy();
111
131
  }
112
- entities.delete(name);
132
+ entities.delete(queryKey);
113
133
  }
114
134
  }
115
135
  });
@@ -1,15 +1,15 @@
1
- import { createRobotQuery, useRobotClient } from '@viamrobotics/svelte-sdk';
2
- import { usePartID } from './usePartID.svelte';
3
- import { commonApi, Pose } from '@viamrobotics/sdk';
4
- import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
5
- import { useEnvironment } from './useEnvironment.svelte';
6
1
  import { observe } from '@threlte/core';
2
+ import { commonApi, Pose } from '@viamrobotics/sdk';
3
+ import { createRobotQuery, useRobotClient } from '@viamrobotics/svelte-sdk';
7
4
  import { untrack } from 'svelte';
8
- import { useFrames } from './useFrames.svelte';
9
5
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
6
+ import { useEnvironment } from './useEnvironment.svelte';
7
+ import { useFrames } from './useFrames.svelte';
10
8
  import { useLogs } from './useLogs.svelte';
11
- import { useResourceByName } from './useResourceByName.svelte';
9
+ import { RefreshRates, useMachineSettings } from './useMachineSettings.svelte';
10
+ import { usePartID } from './usePartID.svelte';
12
11
  import { useRefetchPoses } from './useRefetchPoses';
12
+ import { useResourceByName } from './useResourceByName.svelte';
13
13
  const originFrameComponentTypes = new Set(['arm', 'gantry', 'gripper', 'base']);
14
14
  export const usePose = (name, parent) => {
15
15
  const environment = useEnvironment();
@@ -1,5 +1,5 @@
1
- import { Object3D } from 'three';
2
1
  import type { Entity } from 'koota';
2
+ import { Object3D } from 'three';
3
3
  interface SelectedEntityContext {
4
4
  readonly current: Entity | undefined;
5
5
  readonly instance: number | undefined;
@@ -3,6 +3,7 @@ export declare const WEBLABS_CONTEXT_KEY: unique symbol;
3
3
  interface Context {
4
4
  load: (experiments: string[]) => void;
5
5
  isActive(experiment: string): boolean;
6
+ toggle(experiment: string): void;
6
7
  }
7
8
  export declare const createWeblabs: () => Context;
8
9
  export declare const provideWeblabs: () => void;
@@ -37,11 +37,22 @@ export const createWeblabs = () => {
37
37
  }
38
38
  }
39
39
  };
40
+ const toggle = (experiment) => {
41
+ const cookieExperiments = new Set(getCookieExperiments());
42
+ if (activeExperiments.has(experiment)) {
43
+ activeExperiments.delete(experiment);
44
+ cookieExperiments.delete(experiment);
45
+ }
46
+ else {
47
+ activeExperiments.add(experiment);
48
+ cookieExperiments.add(experiment);
49
+ }
50
+ addCookie('weblab_experiments', [...cookieExperiments].join(','));
51
+ };
40
52
  return {
41
53
  load,
42
- isActive: (experiment) => {
43
- return activeExperiments.has(experiment);
44
- },
54
+ isActive: (experiment) => activeExperiments.has(experiment),
55
+ toggle,
45
56
  };
46
57
  };
47
58
  export const provideWeblabs = () => {
@@ -58,6 +69,7 @@ export const useWeblabs = () => {
58
69
  return {
59
70
  load: () => { },
60
71
  isActive: () => false,
72
+ toggle: () => { },
61
73
  };
62
74
  }
63
75
  return context;
@@ -1,14 +1,17 @@
1
- import { WorldStateStoreClient, TransformChangeType, } from '@viamrobotics/sdk';
1
+ import { useThrelte } from '@threlte/core';
2
+ import { TransformChangeType, WorldStateStoreClient, } from '@viamrobotics/sdk';
2
3
  import { createResourceClient, createResourceQuery, createResourceStream, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
- import { parseMetadata } from '../WorldObject.svelte';
4
- import { usePartID } from './usePartID.svelte';
4
+ import { Color } from 'three';
5
+ import { createBufferGeometry } from '../attribute';
6
+ import { asColor, asOpacity, isPerVertexColors, STRIDE } from '../buffer';
5
7
  import { traits, useWorld } from '../ecs';
6
- import { createPose } from '../transform';
7
- import { useThrelte } from '@threlte/core';
8
8
  import { createBox, createCapsule, createSphere } from '../geometry';
9
- import { parsePlyInput } from '../ply';
10
9
  import { parsePcdInWorker } from '../loaders/pcd';
11
- import { createBufferGeometry } from '../attribute';
10
+ import { parseMetadata } from '../metadata';
11
+ import { parsePlyInput } from '../ply';
12
+ import { createPose } from '../transform';
13
+ import { usePartID } from './usePartID.svelte';
14
+ const colorUtil = new Color();
12
15
  export const provideWorldStates = () => {
13
16
  const partID = usePartID();
14
17
  const resourceNames = useResourceNames(() => partID.current, 'world_state_store');
@@ -40,46 +43,43 @@ const createWorldState = (client) => {
40
43
  if (parent && parent !== 'world') {
41
44
  entityTraits.push(traits.Parent(parent));
42
45
  }
43
- if (metadata.color) {
44
- entityTraits.push(traits.Color(metadata.color));
45
- }
46
- if (metadata.colors) {
47
- entityTraits.push(traits.VertexColors(metadata.colors));
48
- }
49
46
  if (transform.physicalObject) {
50
47
  if (transform.physicalObject.geometryType.case === 'pointcloud') {
48
+ const metadataColors = metadata.colors;
51
49
  parsePcdInWorker(new Uint8Array(transform.physicalObject.geometryType.value.pointCloud)).then((pointcloud) => {
52
- // pcds are a special case since they have to be loaded in a worker and the trait will be added to the existing entity
53
50
  const entity = entities.get(transform.uuidString);
54
51
  if (!entity) {
55
52
  console.error('Entity not found to add pointcloud trait to', transform.uuidString);
56
53
  return;
57
54
  }
58
- const geometry = createBufferGeometry(pointcloud.positions, pointcloud.colors);
55
+ const numPoints = pointcloud.positions.length / STRIDE.POSITIONS;
56
+ const vertexColors = metadataColors && isPerVertexColors(metadataColors, numPoints)
57
+ ? metadataColors
58
+ : pointcloud.colors;
59
+ const geometry = createBufferGeometry(pointcloud.positions, vertexColors);
59
60
  entity.add(traits.BufferGeometry(geometry));
60
61
  entity.add(traits.Points);
62
+ if (metadataColors && !isPerVertexColors(metadataColors, numPoints)) {
63
+ asColor(metadataColors, colorUtil);
64
+ entity.add(traits.Color({ r: colorUtil.r, g: colorUtil.g, b: colorUtil.b }));
65
+ if (metadataColors.length % STRIDE.COLORS_RGBA === 0) {
66
+ entity.add(traits.Opacity(asOpacity(metadataColors)));
67
+ }
68
+ }
69
+ invalidate();
61
70
  });
62
71
  }
63
72
  else {
73
+ if (metadata.colors) {
74
+ asColor(metadata.colors, colorUtil);
75
+ entityTraits.push(traits.Color({ r: colorUtil.r, g: colorUtil.g, b: colorUtil.b }));
76
+ if (metadata.colors.length % STRIDE.COLORS_RGBA === 0) {
77
+ entityTraits.push(traits.Opacity(asOpacity(metadata.colors)));
78
+ }
79
+ }
64
80
  entityTraits.push(traits.Geometry(transform.physicalObject));
65
81
  }
66
82
  }
67
- if (metadata.shape === 'line' && metadata.points) {
68
- const { points } = metadata;
69
- const positions = new Float32Array(points.length * 3);
70
- for (let i = 0, j = 0, l = points.length * 3; i < l; i += 3, j += 1) {
71
- positions[i + 0] = points[j].x;
72
- positions[i + 1] = points[j].y;
73
- positions[i + 2] = points[j].z;
74
- }
75
- entityTraits.push(traits.LinePositions(positions), traits.PointColor(metadata.lineDotColor));
76
- }
77
- if (metadata.gltf) {
78
- entityTraits.push(traits.GLTF({ source: { gltf: metadata.gltf }, animationName: '' }));
79
- }
80
- if (metadata.shape === 'arrow') {
81
- entityTraits.push(traits.Arrow);
82
- }
83
83
  entityTraits.push(traits.Name(transform.referenceFrame), traits.Pose(pose), traits.ShowAxesHelper, traits.WorldStateStoreAPI);
84
84
  const entity = world.spawn(...entityTraits);
85
85
  entities.set(transform.uuidString, entity);
@@ -0,0 +1,22 @@
1
+ import type { PlainMessage, Struct } from '@viamrobotics/sdk';
2
+ /**
3
+ * Metadata for a Viam `Transform`.
4
+ *
5
+ * Per the API this can be a struct of any data, so we type this version for
6
+ * fields we use and how we expect them to be defined.
7
+ */
8
+ export type Metadata = {
9
+ colors?: Uint8Array<ArrayBuffer>;
10
+ };
11
+ /** Type guard that checks whether a string is a recognised {@link Metadata} field name. */
12
+ export declare const isMetadataKey: (key: string) => key is keyof Metadata;
13
+ /**
14
+ * Extracts typed {@link Metadata} from a proto `Struct` fields map.
15
+ *
16
+ * The `colors` field is expected as a base64-encoded string (the only way to
17
+ * represent binary data in a `google.protobuf.Value`), which is decoded into
18
+ * a `Uint8Array`.
19
+ *
20
+ * Unknown keys are silently ignored.
21
+ */
22
+ export declare const parseMetadata: (fields?: PlainMessage<Struct>["fields"]) => Metadata;
@@ -0,0 +1,66 @@
1
+ /** Type guard that checks whether a string is a recognised {@link Metadata} field name. */
2
+ export const isMetadataKey = (key) => {
3
+ return key === 'colors';
4
+ };
5
+ /**
6
+ * Extracts typed {@link Metadata} from a proto `Struct` fields map.
7
+ *
8
+ * The `colors` field is expected as a base64-encoded string (the only way to
9
+ * represent binary data in a `google.protobuf.Value`), which is decoded into
10
+ * a `Uint8Array`.
11
+ *
12
+ * Unknown keys are silently ignored.
13
+ */
14
+ export const parseMetadata = (fields = {}) => {
15
+ const json = {};
16
+ for (const [k, v] of Object.entries(fields)) {
17
+ if (!isMetadataKey(k))
18
+ continue;
19
+ const unwrappedValue = unwrapValue(v);
20
+ switch (k) {
21
+ case 'colors': {
22
+ if (typeof unwrappedValue === 'string') {
23
+ const binary = atob(unwrappedValue);
24
+ const colorBytes = new Uint8Array(binary.length);
25
+ for (let i = 0; i < binary.length; i++) {
26
+ colorBytes[i] = binary.charCodeAt(i);
27
+ }
28
+ json.colors = colorBytes;
29
+ }
30
+ break;
31
+ }
32
+ }
33
+ }
34
+ return json;
35
+ };
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const unwrapValue = (value) => {
38
+ if (!value?.kind)
39
+ return value;
40
+ switch (value.kind.case) {
41
+ case 'numberValue':
42
+ case 'stringValue':
43
+ case 'boolValue': {
44
+ return value.kind.value;
45
+ }
46
+ case 'structValue': {
47
+ const result = {};
48
+ for (const [key, val] of Object.entries(value.kind.value.fields || {})) {
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ result[key] = unwrapValue(val);
51
+ }
52
+ return result;
53
+ }
54
+ case 'listValue': {
55
+ return (value.kind.value.values?.map(
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ (v) => unwrapValue(v)) || []);
58
+ }
59
+ case 'nullValue': {
60
+ return null;
61
+ }
62
+ default: {
63
+ return value.kind.value;
64
+ }
65
+ }
66
+ };
@@ -1,6 +1,6 @@
1
1
  import { injectPlugin, isInstanceOf } from '@threlte/core';
2
- import { BatchedMesh, Points, Mesh } from 'three';
3
- import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, computeBatchedBoundsTree, disposeBatchedBoundsTree, PointsBVH, SAH, BVHHelper, } from 'three-mesh-bvh';
2
+ import { BatchedMesh, Mesh, Points } from 'three';
3
+ import { acceleratedRaycast, BVHHelper, computeBatchedBoundsTree, computeBoundsTree, disposeBatchedBoundsTree, disposeBoundsTree, PointsBVH, SAH, } from 'three-mesh-bvh';
4
4
  export const bvh = (raycaster, options) => {
5
5
  const bvhOptions = $derived({
6
6
  strategy: SAH,
@@ -1,7 +1,27 @@
1
- import type { World, Entity } from 'koota';
1
+ import type { Entity, World } from 'koota';
2
2
  import type { Snapshot } from './buf/draw/v1/snapshot_pb';
3
- import { type SceneMetadata } from './buf/draw/v1/scene_pb';
4
3
  import type { Settings } from './hooks/useSettings.svelte';
4
+ import { type SceneMetadata } from './buf/draw/v1/scene_pb';
5
+ /**
6
+ * Merges scene-level metadata (grid, camera, point/line settings) into the
7
+ * current viewer settings. Millimetre values from the proto are converted
8
+ * to metres.
9
+ */
5
10
  export declare const applySceneMetadata: (settings: Settings, metadata: SceneMetadata) => Settings;
11
+ /**
12
+ * Spawns ECS entities for every transform and drawing in a {@link Snapshot}.
13
+ *
14
+ * Each transform produces one entity with Name, Pose, Parent, Geometry, and
15
+ * optional Color/Opacity traits. Each drawing produces one or more entities
16
+ * depending on the geometry type (arrows, points, line, nurbs, model, or
17
+ * simple shapes like box/sphere/capsule).
18
+ *
19
+ * @returns The spawned entities — pass them to {@link destroyEntities} to
20
+ * clean up before loading a new snapshot.
21
+ */
6
22
  export declare const spawnSnapshotEntities: (world: World, snapshot: Snapshot) => Entity[];
23
+ /**
24
+ * Destroys a list of entities that are still alive in the given world.
25
+ * Silently skips entities that have already been removed.
26
+ */
7
27
  export declare const destroyEntities: (world: World, entities: Entity[]) => void;
package/dist/snapshot.js CHANGED
@@ -1,14 +1,20 @@
1
- import { Vector3, Vector4 } from 'three';
1
+ import { Geometry } from '@viamrobotics/sdk';
2
+ import { Color, Vector3, Vector4 } from 'three';
2
3
  import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
3
- import { RenderArmModels } from './buf/draw/v1/scene_pb';
4
4
  import {} from './buf/draw/v1/drawing_pb';
5
+ import { RenderArmModels } from './buf/draw/v1/scene_pb';
5
6
  import { traits } from './ecs';
6
- import { Geometry } from '@viamrobotics/sdk';
7
- import { parseMetadata } from './WorldObject.svelte';
8
- import { rgbaBytesToFloat32, rgbaToHex } from './color';
9
- import { asFloat32Array, STRIDE } from './buffer';
7
+ import { parseMetadata } from './metadata';
10
8
  import { createBufferGeometry } from './attribute';
9
+ import { asColor, asFloat32Array, asOpacity, isPerVertexColors, STRIDE } from './buffer';
10
+ import { rgbaToHex } from './color';
11
11
  const vec3 = new Vector3();
12
+ const colorUtil = new Color();
13
+ /**
14
+ * Merges scene-level metadata (grid, camera, point/line settings) into the
15
+ * current viewer settings. Millimetre values from the proto are converted
16
+ * to metres.
17
+ */
12
18
  export const applySceneMetadata = (settings, metadata) => {
13
19
  const next = { ...settings };
14
20
  if (metadata.grid !== undefined) {
@@ -46,6 +52,17 @@ export const applySceneMetadata = (settings, metadata) => {
46
52
  }
47
53
  return next;
48
54
  };
55
+ /**
56
+ * Spawns ECS entities for every transform and drawing in a {@link Snapshot}.
57
+ *
58
+ * Each transform produces one entity with Name, Pose, Parent, Geometry, and
59
+ * optional Color/Opacity traits. Each drawing produces one or more entities
60
+ * depending on the geometry type (arrows, points, line, nurbs, model, or
61
+ * simple shapes like box/sphere/capsule).
62
+ *
63
+ * @returns The spawned entities — pass them to {@link destroyEntities} to
64
+ * clean up before loading a new snapshot.
65
+ */
49
66
  export const spawnSnapshotEntities = (world, snapshot) => {
50
67
  const entities = [];
51
68
  for (const transform of snapshot.transforms) {
@@ -59,6 +76,10 @@ export const spawnSnapshotEntities = (world, snapshot) => {
59
76
  }
60
77
  return entities;
61
78
  };
79
+ /**
80
+ * Destroys a list of entities that are still alive in the given world.
81
+ * Silently skips entities that have already been removed.
82
+ */
62
83
  export const destroyEntities = (world, entities) => {
63
84
  for (const entity of entities) {
64
85
  if (world.has(entity)) {
@@ -92,12 +113,8 @@ const spawnTransformEntity = (world, transform) => {
92
113
  entityTraits.push(traits.Pose(poseInFrame?.pose), traits.Parent(poseInFrame?.referenceFrame));
93
114
  if (transform.metadata) {
94
115
  const metadata = parseMetadata(transform.metadata.fields);
95
- if (metadata.color) {
96
- entityTraits.push(traits.Color(metadata.color));
97
- }
98
- if (metadata.opacity !== undefined) {
99
- entityTraits.push(traits.Opacity(metadata.opacity));
100
- }
116
+ if (metadata.colors)
117
+ addColorTraits(entityTraits, metadata.colors);
101
118
  }
102
119
  return world.spawn(...entityTraits);
103
120
  };
@@ -167,15 +184,6 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
167
184
  if (parent && parent !== 'world') {
168
185
  entityTraits.push(traits.Parent);
169
186
  }
170
- if (drawing.metadata?.colors) {
171
- const colors = rgbaBytesToFloat32(drawing.metadata.colors);
172
- if (colors.length === 4) {
173
- entityTraits.push(traits.Color({ r: colors[0], g: colors[1], b: colors[2] }), traits.Opacity(colors[3]));
174
- }
175
- else {
176
- entityTraits.push(traits.VertexColors(colors));
177
- }
178
- }
179
187
  if (drawing.physicalObject?.center) {
180
188
  entityTraits.push(traits.Center(drawing.physicalObject.center));
181
189
  }
@@ -184,9 +192,20 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
184
192
  for (let i = 0, l = positions.length; i < l; i += 1) {
185
193
  positions[i] *= 0.001;
186
194
  }
187
- entityTraits.push(traits.LinePositions(positions), traits.LineWidth(geometryType.value.lineWidth), traits.PointSize(geometryType.value.pointSize));
188
- if (geometryType.value.pointSize) {
189
- entityTraits.push(traits.PointSize(geometryType.value.pointSize * 0.001));
195
+ entityTraits.push(traits.LinePositions(positions), traits.LineWidth(geometryType.value.lineWidth), traits.PointSize((geometryType.value.pointSize ?? 0) * 0.001));
196
+ // Lines pack exactly 2 colors: [lineColor, pointColor]
197
+ const colors = drawing.metadata?.colors;
198
+ if (colors && colors.length >= STRIDE.COLORS_RGB) {
199
+ const stride = colors.length % STRIDE.COLORS_RGBA === 0 ? STRIDE.COLORS_RGBA : STRIDE.COLORS_RGB;
200
+ asColor(colors, colorUtil, 0);
201
+ entityTraits.push(traits.Color({ r: colorUtil.r, g: colorUtil.g, b: colorUtil.b }));
202
+ if (colors.length >= stride * 2) {
203
+ asColor(colors, colorUtil, stride);
204
+ entityTraits.push(traits.PointColor({ r: colorUtil.r, g: colorUtil.g, b: colorUtil.b }));
205
+ if (stride === STRIDE.COLORS_RGBA) {
206
+ entityTraits.push(traits.Opacity(asOpacity(colors, 1, 3)));
207
+ }
208
+ }
190
209
  }
191
210
  }
192
211
  else if (geometryType?.case === 'points') {
@@ -195,8 +214,13 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
195
214
  positions[i] *= 0.001;
196
215
  }
197
216
  const colors = drawing.metadata?.colors;
198
- const geometry = createBufferGeometry(positions, colors);
217
+ const numPoints = positions.length / STRIDE.POSITIONS;
218
+ const vertexColors = colors && isPerVertexColors(colors, numPoints) ? colors : undefined;
219
+ const geometry = createBufferGeometry(positions, vertexColors);
199
220
  entityTraits.push(traits.BufferGeometry(geometry));
221
+ if (colors && !vertexColors) {
222
+ addColorTraits(entityTraits, colors);
223
+ }
200
224
  if (geometryType.value.pointSize) {
201
225
  entityTraits.push(traits.PointSize(geometryType.value.pointSize * 0.001));
202
226
  }
@@ -227,9 +251,27 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
227
251
  points[i + 2] = vec3.z;
228
252
  }
229
253
  entityTraits.push(traits.LinePositions(points));
254
+ const colors = drawing.metadata?.colors;
255
+ if (colors) {
256
+ addColorTraits(entityTraits, colors);
257
+ }
258
+ }
259
+ else {
260
+ // Box, sphere, capsule, and other geometry shapes with a single color
261
+ const colors = drawing.metadata?.colors;
262
+ if (colors) {
263
+ addColorTraits(entityTraits, colors);
264
+ }
230
265
  }
231
266
  const entity = world.spawn(...entityTraits, traits.SnapshotAPI, traits.Removable);
232
267
  entities.push(entity);
233
268
  }
234
269
  return entities;
235
270
  };
271
+ const addColorTraits = (entityTraits, bytes) => {
272
+ asColor(bytes, colorUtil);
273
+ entityTraits.push(traits.Color(colorUtil));
274
+ const isRgba = bytes.length % STRIDE.COLORS_RGBA === 0;
275
+ if (isRgba)
276
+ entityTraits.push(traits.Opacity(asOpacity(bytes)));
277
+ };
@@ -1,5 +1,5 @@
1
- import { BatchedMesh, Vector3, Color } from 'three';
2
1
  import type { OBB } from 'three/addons/math/OBB.js';
2
+ import { BatchedMesh, Color, Vector3 } from 'three';
3
3
  export declare class BatchedArrow {
4
4
  mesh: BatchedMesh;
5
5
  _geometryId: number;
@@ -1,4 +1,4 @@
1
- import { BatchedMesh, MeshBasicMaterial, Object3D, Vector3, Color, Box3 } from 'three';
1
+ import { BatchedMesh, Box3, Color, MeshBasicMaterial, Object3D, Vector3 } from 'three';
2
2
  import { createArrowGeometry } from './arrow';
3
3
  const black = new Color('black');
4
4
  const axis = new Vector3();
@@ -1,4 +1,4 @@
1
- import { Group, Mesh, BufferGeometry, InstancedInterleavedBuffer, type ColorRepresentation, Vector3, Box3 } from 'three';
1
+ import { Box3, BufferGeometry, type ColorRepresentation, Group, InstancedInterleavedBuffer, Mesh, Vector3 } from 'three';
2
2
  export declare class InstancedArrows extends Group {
3
3
  isInstancedArrows: boolean;
4
4
  count: number;
@@ -1,8 +1,8 @@
1
- import { RawShaderMaterial, FrontSide, Group, InstancedBufferAttribute, DynamicDrawUsage, Mesh, BufferGeometry, InstancedInterleavedBuffer, InterleavedBufferAttribute, Material, Color, Vector3, Box3, } from 'three';
2
- import vertexShader from './vertex.glsl';
1
+ import { Box3, BufferGeometry, Color, DynamicDrawUsage, FrontSide, Group, InstancedBufferAttribute, InstancedInterleavedBuffer, InterleavedBufferAttribute, Material, Mesh, RawShaderMaterial, Vector3, } from 'three';
2
+ import { computeBoundingBox } from './box';
3
3
  import fragmentShader from './fragment.glsl';
4
4
  import { createHeadGeometry, createShaftGeometry, toInstanced } from './geometry';
5
- import { computeBoundingBox } from './box';
5
+ import vertexShader from './vertex.glsl';
6
6
  const defaults = {
7
7
  LENGTH: 0.1,
8
8
  HEAD_LENGTH: 0.02,
@@ -1,4 +1,4 @@
1
- import { RawShaderMaterial, Box3, BufferGeometry } from 'three';
1
+ import { Box3, BufferGeometry, RawShaderMaterial } from 'three';
2
2
  const bounds = new Box3();
3
3
  export function computeBoundingBox(geometry) {
4
4
  const src = this.poses.array;
@@ -1,4 +1,4 @@
1
- import { BufferGeometry, BufferAttribute, InstancedBufferGeometry } from 'three';
1
+ import { BufferAttribute, BufferGeometry, InstancedBufferGeometry } from 'three';
2
2
  export const createShaftGeometry = () => {
3
3
  // Triangular prism aligned to +Y, base at y=0, top at y=1.
4
4
  // No caps, 6 verts, 6 side triangles.
@@ -1,4 +1,4 @@
1
- import { Raycaster, type Intersection } from 'three';
1
+ import { type Intersection, Raycaster } from 'three';
2
2
  import type { InstancedArrows } from './InstancedArrows';
3
3
  export declare function meshBoundsRaycast(this: InstancedArrows, raycaster: Raycaster, intersects: Intersection[]): void;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { Ray, Matrix4, Raycaster, Vector3, Box3, RawShaderMaterial } from 'three';
1
+ import { Box3, Matrix4, RawShaderMaterial, Ray, Raycaster, Vector3 } from 'three';
2
2
  const vec3 = new Vector3();
3
3
  const inverseMatrix = new Matrix4();
4
4
  const localRay = new Ray();
@@ -1,5 +1,6 @@
1
- import { LineSegments, Vector3, Object3D } from 'three';
2
- export declare class OBBHelper extends LineSegments {
1
+ import { Object3D, Vector3 } from 'three';
2
+ import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js';
3
+ export declare class OBBHelper extends LineSegments2 {
3
4
  constructor(color?: number, linewidth?: number);
4
5
  setFromOBB(obb: {
5
6
  center: Vector3;