@viamrobotics/motion-tools 1.26.1 → 1.26.2

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 (51) hide show
  1. package/dist/FrameConfigUpdater.svelte.js +42 -29
  2. package/dist/buf/common/v1/common_pb.d.ts +19 -0
  3. package/dist/buf/common/v1/common_pb.js +32 -0
  4. package/dist/components/BatchedArrows.svelte +31 -15
  5. package/dist/components/Entities/Entities.svelte +3 -8
  6. package/dist/components/Entities/Frame.svelte +25 -9
  7. package/dist/components/Entities/Frame.svelte.d.ts +0 -2
  8. package/dist/components/Entities/GLTF.svelte +5 -4
  9. package/dist/components/Entities/Line.svelte +5 -4
  10. package/dist/components/Entities/Mesh.svelte +12 -18
  11. package/dist/components/Entities/Points.svelte +5 -4
  12. package/dist/components/Entities/Pose.svelte +17 -24
  13. package/dist/components/Entities/Pose.svelte.d.ts +1 -4
  14. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +40 -41
  15. package/dist/components/SceneProviders.svelte +2 -1
  16. package/dist/components/SelectedTransformControls.svelte +57 -34
  17. package/dist/components/StaticGeometries.svelte +1 -1
  18. package/dist/components/hover/HoveredEntity.svelte +33 -3
  19. package/dist/components/hover/LinkedHoveredEntity.svelte +2 -3
  20. package/dist/components/overlay/Details.svelte +72 -94
  21. package/dist/components/overlay/__tests__/__fixtures__/entity.js +14 -17
  22. package/dist/components/overlay/left-pane/Tree.svelte +9 -9
  23. package/dist/components/overlay/left-pane/Tree.svelte.d.ts +1 -2
  24. package/dist/components/overlay/left-pane/TreeContainer.svelte +4 -15
  25. package/dist/components/overlay/left-pane/TreeNode.svelte +1 -1
  26. package/dist/components/overlay/left-pane/TreeNode.svelte.d.ts +1 -1
  27. package/dist/components/overlay/left-pane/useTree.svelte.d.ts +14 -0
  28. package/dist/components/overlay/left-pane/useTree.svelte.js +63 -0
  29. package/dist/draw.js +21 -7
  30. package/dist/ecs/index.d.ts +1 -0
  31. package/dist/ecs/index.js +1 -0
  32. package/dist/ecs/provideWorldMatrix.svelte.d.ts +8 -0
  33. package/dist/ecs/provideWorldMatrix.svelte.js +13 -0
  34. package/dist/ecs/traits.d.ts +41 -45
  35. package/dist/ecs/traits.js +57 -28
  36. package/dist/ecs/useTrait.svelte.d.ts +1 -6
  37. package/dist/ecs/useTrait.svelte.js +21 -13
  38. package/dist/ecs/worldMatrix.d.ts +10 -0
  39. package/dist/ecs/worldMatrix.js +148 -0
  40. package/dist/editing/FrameEditSession.js +31 -18
  41. package/dist/hooks/use3DModels.svelte.js +1 -1
  42. package/dist/hooks/useConfigFrames.svelte.js +12 -0
  43. package/dist/hooks/useDrawAPI.svelte.js +14 -6
  44. package/dist/hooks/useFrames.svelte.js +23 -11
  45. package/dist/hooks/useGeometries.svelte.js +4 -2
  46. package/dist/hooks/usePartConfig.svelte.js +38 -3
  47. package/dist/hooks/useWorldState.svelte.js +10 -2
  48. package/dist/transform.js +55 -21
  49. package/package.json +3 -3
  50. package/dist/components/overlay/left-pane/buildTree.d.ts +0 -13
  51. package/dist/components/overlay/left-pane/buildTree.js +0 -48
@@ -1,7 +1,7 @@
1
1
  import { useThrelte } from '@threlte/core';
2
2
  import {} from 'koota';
3
3
  import { getContext, setContext } from 'svelte';
4
- import { Color, Vector3, Vector4 } from 'three';
4
+ import { Color, Matrix4, Vector3, Vector4 } from 'three';
5
5
  import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
6
6
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
7
7
  import { UuidTool } from 'uuid-tool';
@@ -11,7 +11,7 @@ import { asRGB, STRIDE } from '../buffer';
11
11
  import { hierarchy, traits, useWorld } from '../ecs';
12
12
  import { createBox, createCapsule, createSphere } from '../geometry';
13
13
  import { parsePlyInput } from '../ply';
14
- import { createPose, createPoseFromFrame } from '../transform';
14
+ import { createPose, createPoseFromFrame, poseToMatrix } from '../transform';
15
15
  import { useCameraControls } from './useControls.svelte';
16
16
  import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
17
17
  import { useLogs } from './useLogs.svelte';
@@ -126,7 +126,11 @@ export const provideDrawAPI = () => {
126
126
  const parent = frame.parent;
127
127
  const existing = entities.get(name);
128
128
  if (existing) {
129
- existing.set(traits.Pose, pose);
129
+ const matrix = existing.get(traits.Matrix);
130
+ if (matrix) {
131
+ poseToMatrix(pose, matrix);
132
+ existing.changed(traits.Matrix);
133
+ }
130
134
  hierarchy.setParent(existing, parent);
131
135
  continue;
132
136
  }
@@ -146,7 +150,7 @@ export const provideDrawAPI = () => {
146
150
  if (frame.geometry) {
147
151
  entityTraits.push(geometryTrait());
148
152
  }
149
- entityTraits.push(traits.Name(name), traits.Pose(pose), traits.DrawAPI, traits.ReferenceFrame, traits.Removable, traits.ShowAxesHelper);
153
+ entityTraits.push(traits.Name(name), traits.Matrix(poseToMatrix(pose, new Matrix4())), traits.DrawAPI, traits.ReferenceFrame, traits.Removable, traits.ShowAxesHelper);
150
154
  const entity = world.spawn(...entityTraits);
151
155
  entities.set(name, entity);
152
156
  }
@@ -157,7 +161,11 @@ export const provideDrawAPI = () => {
157
161
  const pose = createPose(data.center);
158
162
  const existing = entities.get(name);
159
163
  if (existing) {
160
- existing.set(traits.Pose, pose);
164
+ const matrix = existing.get(traits.Matrix);
165
+ if (matrix) {
166
+ poseToMatrix(pose, matrix);
167
+ existing.changed(traits.Matrix);
168
+ }
161
169
  return;
162
170
  }
163
171
  const geometryTrait = () => {
@@ -179,7 +187,7 @@ export const provideDrawAPI = () => {
179
187
  const entityTraits = [
180
188
  traits.Name(data.label ?? ++geometryIndex),
181
189
  ...hierarchy.parentTraits(parent),
182
- traits.Pose(pose),
190
+ traits.Matrix(poseToMatrix(pose, new Matrix4())),
183
191
  traits.Color(colorUtil.set(color)),
184
192
  geometryTrait(),
185
193
  traits.DrawAPI,
@@ -2,9 +2,10 @@ import { MachineConnectionEvent, Transform } from '@viamrobotics/sdk';
2
2
  import { createRobotQuery, useConnectionStatus, useMachineStatus, useRobotClient, } from '@viamrobotics/svelte-sdk';
3
3
  import {} from 'koota';
4
4
  import { getContext, setContext, untrack } from 'svelte';
5
+ import { Matrix4 } from 'three';
5
6
  import { resourceNameToColor, subtypeToColor } from '../color';
6
7
  import { hierarchy, traits, useWorld } from '../ecs';
7
- import { createPose } from '../transform';
8
+ import { createPose, isPoseEqual, poseToMatrix } from '../transform';
8
9
  import { useConfigFrames } from './useConfigFrames.svelte';
9
10
  import { useEnvironment } from './useEnvironment.svelte';
10
11
  import { useFrameEditSession } from './useFrameEditSession.svelte';
@@ -171,34 +172,45 @@ export const provideFrames = (partID) => {
171
172
  }
172
173
  hierarchy.setParent(existing, parent);
173
174
  if (color) {
174
- existing.set(traits.Color, color);
175
+ const cur = existing.get(traits.Color);
176
+ if (!cur || cur.r !== color.r || cur.g !== color.g || cur.b !== color.b) {
177
+ existing.set(traits.Color, color);
178
+ }
175
179
  }
176
- if (center) {
180
+ if (center && !isPoseEqual(existing.get(traits.Center), center)) {
177
181
  existing.set(traits.Center, center);
178
182
  }
179
183
  traits.updateGeometryTrait(existing, frame.physicalObject);
180
184
  if (!isEditMode && !partConfig.hasPendingSave) {
181
- existing.set(traits.Pose, pose);
185
+ const baseline = existing.get(traits.Matrix);
186
+ if (baseline) {
187
+ poseToMatrix(pose, baseline);
188
+ existing.changed(traits.Matrix);
189
+ }
182
190
  }
183
- if (!existing.has(traits.LivePose)) {
184
- existing.add(traits.LivePose(pose));
191
+ if (!existing.has(traits.LiveMatrix)) {
192
+ existing.add(traits.LiveMatrix(poseToMatrix(pose, new Matrix4())));
185
193
  }
186
- // Skip the EditedPose overwrite while in edit mode. The merged
194
+ // Skip the EditedMatrix overwrite while in edit mode. The merged
187
195
  // `frames` source can differ from query.data once didRecentlyEdit
188
196
  // flips (fragment overrides, round-trip drift), and writing those
189
197
  // values would shift entities whose parents the user is portaling
190
198
  // into — the gizmo's drag target moves underneath it. Once we're
191
199
  // back in monitor mode, the next sync resumes the overwrite.
192
200
  if (!isEditMode) {
193
- existing.set(traits.EditedPose, pose);
201
+ const edited = existing.get(traits.EditedMatrix);
202
+ if (edited) {
203
+ poseToMatrix(pose, edited);
204
+ existing.changed(traits.EditedMatrix);
205
+ }
194
206
  }
195
207
  continue;
196
208
  }
197
209
  const entityTraits = [
198
210
  traits.Name(name),
199
- traits.Pose(pose),
200
- traits.EditedPose(pose),
201
- traits.LivePose(pose),
211
+ traits.Matrix(poseToMatrix(pose, new Matrix4())),
212
+ traits.EditedMatrix(poseToMatrix(pose, new Matrix4())),
213
+ traits.LiveMatrix(poseToMatrix(pose, new Matrix4())),
202
214
  traits.FramesAPI,
203
215
  traits.Transformable,
204
216
  traits.ShowAxesHelper,
@@ -7,7 +7,7 @@ import { resourceColors } from '../color';
7
7
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
8
8
  import { hierarchy, traits, useWorld } from '../ecs';
9
9
  import { updateGeometryTrait } from '../ecs/traits';
10
- import { createPose } from '../transform';
10
+ import { createPose, isPoseEqual } from '../transform';
11
11
  import { useEnvironment } from './useEnvironment.svelte';
12
12
  import { useLogs } from './useLogs.svelte';
13
13
  import { useResourceByName } from './useResourceByName.svelte';
@@ -91,7 +91,9 @@ export const provideGeometries = (partID) => {
91
91
  const existing = entities.get(entityKey);
92
92
  if (existing) {
93
93
  hierarchy.setParent(existing, name);
94
- existing.set(traits.Center, center);
94
+ if (!isPoseEqual(existing.get(traits.Center), center)) {
95
+ existing.set(traits.Center, center);
96
+ }
95
97
  updateGeometryTrait(existing, geometry);
96
98
  continue;
97
99
  }
@@ -204,9 +204,40 @@ export const usePartConfig = () => {
204
204
  return getContext(key);
205
205
  };
206
206
  const useEmbeddedPartConfig = (props) => {
207
+ let hasPendingSave = $state(false);
208
+ let prevIsDirty = false;
209
+ let cleanSnapshot;
210
+ const snapshot = (current) => {
211
+ const json = current?.toJson?.();
212
+ return json === undefined ? undefined : JSON.stringify(json);
213
+ };
214
+ /**
215
+ * The host app owns saving, and we aren't notified directly. Set hasPendingSave
216
+ * to watch isDirty: true -> false transitions, representing a save.
217
+ *
218
+ * `useFrames` clears the flag on the next `revision` change
219
+ * once the server reports the new framesystem.
220
+ */
221
+ $effect.pre(() => {
222
+ const dirty = props.isDirty;
223
+ const current = props.current;
224
+ if (prevIsDirty && !dirty) {
225
+ const next = snapshot(current);
226
+ if (next !== undefined && cleanSnapshot !== undefined && next !== cleanSnapshot) {
227
+ hasPendingSave = true;
228
+ }
229
+ cleanSnapshot = next;
230
+ }
231
+ else if (!prevIsDirty && !dirty) {
232
+ cleanSnapshot = snapshot(current);
233
+ }
234
+ prevIsDirty = dirty;
235
+ });
207
236
  return {
208
237
  hasEditPermissions: true,
209
- hasPendingSave: false,
238
+ get hasPendingSave() {
239
+ return hasPendingSave;
240
+ },
210
241
  get isDirty() {
211
242
  return props.isDirty;
212
243
  },
@@ -220,8 +251,12 @@ const useEmbeddedPartConfig = (props) => {
220
251
  const struct = Struct.fromJson(config);
221
252
  return props.setLocalPartConfig(struct);
222
253
  },
223
- clearPendingSave() { },
224
- setPendingSave() { },
254
+ clearPendingSave() {
255
+ hasPendingSave = false;
256
+ },
257
+ setPendingSave() {
258
+ hasPendingSave = true;
259
+ },
225
260
  };
226
261
  };
227
262
  const useStandalonePartConfig = (partID) => {
@@ -1,13 +1,14 @@
1
1
  import { useThrelte } from '@threlte/core';
2
2
  import { Struct, TransformChangeType, WorldStateStoreClient, } from '@viamrobotics/sdk';
3
3
  import { createResourceClient, createResourceQuery, createResourceStream, useResourceNames, } from '@viamrobotics/svelte-sdk';
4
+ import { Matrix4 } from 'three';
4
5
  import { asFloat32Array, inMeters } from '../buffer';
5
6
  import { createChunkLoader } from '../chunking';
6
7
  import { drawTransform, updateMetadata } from '../draw';
7
8
  import { traits, useWorld } from '../ecs';
8
9
  import { isPointCloud } from '../geometry';
9
10
  import { metadataFromStruct } from '../metadata';
10
- import { createPose } from '../transform';
11
+ import { createPose, poseToMatrix } from '../transform';
11
12
  import { usePartID } from './usePartID.svelte';
12
13
  import { useRelationships } from './useRelationships.svelte';
13
14
  export const provideWorldStates = () => {
@@ -133,7 +134,14 @@ const createWorldState = (client) => {
133
134
  for (const path of changes) {
134
135
  if (typeof path === 'string') {
135
136
  if (path.startsWith('poseInObserverFrame.pose')) {
136
- entity.set(traits.Pose, transform.poseInObserverFrame?.pose ?? createPose());
137
+ const matrix = entity.get(traits.Matrix);
138
+ if (matrix) {
139
+ poseToMatrix(createPose(transform.poseInObserverFrame?.pose), matrix);
140
+ entity.changed(traits.Matrix);
141
+ }
142
+ else {
143
+ entity.add(traits.Matrix(poseToMatrix(createPose(transform.poseInObserverFrame?.pose), new Matrix4())));
144
+ }
137
145
  }
138
146
  else if (path.startsWith('physicalObject') && transform.physicalObject) {
139
147
  traits.updateGeometryTrait(entity, transform.physicalObject);
package/dist/transform.js CHANGED
@@ -5,6 +5,20 @@ const euler = new Euler();
5
5
  const ov = new OrientationVector();
6
6
  const translation = new Vector3();
7
7
  const scale = new Vector3();
8
+ const matA = new Matrix4();
9
+ export const isPoseEqual = (a, b) => {
10
+ if (a === b)
11
+ return true;
12
+ if (!a || !b)
13
+ return false;
14
+ return (a.x === b.x &&
15
+ a.y === b.y &&
16
+ a.z === b.z &&
17
+ a.oX === b.oX &&
18
+ a.oY === b.oY &&
19
+ a.oZ === b.oZ &&
20
+ a.theta === b.theta);
21
+ };
8
22
  export const createPose = (pose) => {
9
23
  // We should only default to the 0,0,1,0 orientation vector if the entire vector component is missing
10
24
  const oZ = pose?.oX === undefined && pose?.oY === undefined && pose?.oZ === undefined ? 1 : (pose?.oZ ?? 0);
@@ -84,20 +98,31 @@ export const poseToDirection = (pose) => {
84
98
  ov.set(pose.oX, pose.oY, pose.oZ, MathUtils.degToRad(pose.theta));
85
99
  return new Vector3(ov.x, ov.y, ov.z);
86
100
  };
87
- export const poseToMatrix = (pose) => {
101
+ export const isFinitePose = (pose) => Number.isFinite(pose.x) &&
102
+ Number.isFinite(pose.y) &&
103
+ Number.isFinite(pose.z) &&
104
+ Number.isFinite(pose.oX) &&
105
+ Number.isFinite(pose.oY) &&
106
+ Number.isFinite(pose.oZ) &&
107
+ Number.isFinite(pose.theta);
108
+ /**
109
+ * Build a TRS `Matrix4` (m) from a `Pose` (mm), writing into `matrix`.
110
+ */
111
+ export const poseToMatrix = (pose, matrix) => {
88
112
  ov.set(pose.oX, pose.oY, pose.oZ, MathUtils.degToRad(pose.theta));
89
113
  ov.toQuaternion(quaternion);
90
- const matrix = new Matrix4();
91
114
  matrix.makeRotationFromQuaternion(quaternion);
92
- matrix.setPosition(pose.x, pose.y, pose.z);
115
+ matrix.setPosition(pose.x * 0.001, pose.y * 0.001, pose.z * 0.001);
93
116
  return matrix;
94
117
  };
95
- export const matrixToPose = (matrix) => {
96
- const pose = createPose();
118
+ /**
119
+ * Decompose a `Matrix4` (m) into a `Pose` (mm), writing into `pose`.
120
+ */
121
+ export const matrixToPose = (matrix, pose) => {
97
122
  matrix.decompose(translation, quaternion, scale);
98
- pose.x = translation.x;
99
- pose.y = translation.y;
100
- pose.z = translation.z;
123
+ pose.x = translation.x * 1000;
124
+ pose.y = translation.y * 1000;
125
+ pose.z = translation.z * 1000;
101
126
  ov.setFromQuaternion(quaternion);
102
127
  pose.oX = ov.x;
103
128
  pose.oY = ov.y;
@@ -105,16 +130,25 @@ export const matrixToPose = (matrix) => {
105
130
  pose.theta = MathUtils.radToDeg(ov.th);
106
131
  return pose;
107
132
  };
108
- export const composeRenderedPose = (livePose, baselinePose, editedPose) => matrixToPose(poseToMatrix(livePose)
109
- .multiply(poseToMatrix(baselinePose).invert())
110
- .multiply(poseToMatrix(editedPose)));
111
- export const composeEditedPoseForRenderedPose = (baselinePose, livePose, renderedPose) => matrixToPose(poseToMatrix(baselinePose)
112
- .multiply(poseToMatrix(livePose).invert())
113
- .multiply(poseToMatrix(renderedPose)));
114
- export const isFinitePose = (pose) => Number.isFinite(pose.x) &&
115
- Number.isFinite(pose.y) &&
116
- Number.isFinite(pose.z) &&
117
- Number.isFinite(pose.oX) &&
118
- Number.isFinite(pose.oY) &&
119
- Number.isFinite(pose.oZ) &&
120
- Number.isFinite(pose.theta);
133
+ /**
134
+ * Compose the entity's local-to-parent transform: writes
135
+ * `live × baseline⁻¹ × edited` into `out`. Mirrors the formula
136
+ * `Frame.svelte` uses to blend live kinematics with user-staged edits;
137
+ * `worldMatrix.ts` premultiplies the result by the parent's `WorldMatrix`.
138
+ */
139
+ export const composeLocalMatrix = (live, baseline, edited, out) => {
140
+ matA.copy(baseline).invert();
141
+ out.copy(live).multiply(matA).multiply(edited);
142
+ return out;
143
+ };
144
+ /**
145
+ * Inverse of `composeLocalMatrix` for the transform controls path:
146
+ * writes `baseline × live⁻¹ × target` into `out`. Solves for the
147
+ * `EditedMatrix` that, blended through `composeLocalMatrix`, renders
148
+ * to `target`.
149
+ */
150
+ export const solveEditedMatrix = (baseline, live, target, out) => {
151
+ matA.copy(live).invert();
152
+ out.copy(baseline).multiply(matA).multiply(target);
153
+ return out;
154
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.26.1",
3
+ "version": "1.26.2",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -25,8 +25,8 @@
25
25
  "@testing-library/jest-dom": "6.8.0",
26
26
  "@testing-library/svelte": "5.2.8",
27
27
  "@testing-library/user-event": "^14.6.1",
28
- "@threlte/core": "8.5.13",
29
- "@threlte/extras": "9.15.2",
28
+ "@threlte/core": "8.5.14",
29
+ "@threlte/extras": "9.17.0",
30
30
  "@threlte/rapier": "3.4.1",
31
31
  "@threlte/xr": "1.6.0",
32
32
  "@types/bun": "1.2.21",
@@ -1,13 +0,0 @@
1
- import type { Entity, QueryResult, Trait } from 'koota';
2
- export interface TreeNode {
3
- entity: Entity;
4
- parent?: TreeNode;
5
- children?: TreeNode[];
6
- }
7
- /**
8
- * Creates a tree representing parent child / relationships from a set of frames.
9
- */
10
- export declare const buildTreeNodes: (entities: QueryResult<[Trait]>) => {
11
- rootNodes: TreeNode[];
12
- nodeMap: Record<string, TreeNode | undefined>;
13
- };
@@ -1,48 +0,0 @@
1
- import { hierarchy, traits } from '../../../ecs';
2
- function sortNodes(nodes) {
3
- nodes.sort((a, b) => a.entity.get(traits.Name)?.localeCompare(b.entity.get(traits.Name) ?? '') ?? 0);
4
- }
5
- /**
6
- * Creates a tree representing parent child / relationships from a set of frames.
7
- */
8
- export const buildTreeNodes = (entities) => {
9
- const nodeMap = {};
10
- const rootNodes = [];
11
- const childNodes = [];
12
- for (const entity of entities) {
13
- const parent = hierarchy.getParentName(entity);
14
- const name = entity.get(traits.Name) ?? '';
15
- const node = { entity };
16
- nodeMap[name] = node;
17
- if (!parent || parent === 'world') {
18
- rootNodes.push(node);
19
- }
20
- else {
21
- childNodes.push(node);
22
- }
23
- }
24
- for (const node of childNodes) {
25
- const parent = hierarchy.getParentName(node.entity);
26
- if (parent) {
27
- const parentNode = nodeMap[parent];
28
- node.parent = parentNode;
29
- if (parentNode) {
30
- parentNode.children ??= [];
31
- parentNode.children?.push(node);
32
- }
33
- }
34
- }
35
- for (const node of rootNodes) {
36
- if (!node.children)
37
- continue;
38
- sortNodes(node.children);
39
- }
40
- for (const node of childNodes) {
41
- if (!node.children)
42
- continue;
43
- sortNodes(node.children);
44
- }
45
- sortNodes(rootNodes);
46
- sortNodes(childNodes);
47
- return { rootNodes, nodeMap };
48
- };