@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,9 +1,18 @@
1
1
  import { useCursor } from '@threlte/extras';
2
- import { Vector2 } from 'three';
2
+ import { MathUtils, Matrix4, Quaternion, Vector2 } from 'three';
3
3
  import { traits, useTrait } from '../../../ecs';
4
4
  import { useFocusedEntity, useSelectedEntity } from '../../../hooks/useSelection.svelte';
5
5
  import { updateHoverInfo } from '../../../HoverUpdater.svelte';
6
- import { createPose, matrixToPose, poseToMatrix } from '../../../transform';
6
+ import { OrientationVector } from '../../../three/OrientationVector';
7
+ const tempHoverMatrix = new Matrix4();
8
+ const hoverQuat = new Quaternion();
9
+ const hoverOv = new OrientationVector();
10
+ const infoToLocalMatrix = (info, out) => {
11
+ hoverOv.set(info.oX, info.oY, info.oZ, MathUtils.degToRad(info.theta));
12
+ hoverOv.toQuaternion(hoverQuat);
13
+ out.makeRotationFromQuaternion(hoverQuat);
14
+ out.setPosition(info.x, info.y, info.z);
15
+ };
7
16
  export const useEntityEvents = (entity) => {
8
17
  const down = new Vector2();
9
18
  const selectedEntity = useSelectedEntity();
@@ -19,15 +28,18 @@ export const useEntityEvents = (entity) => {
19
28
  if (currentEntity && !currentEntity.has(traits.Hovered)) {
20
29
  const hoverInfo = updateHoverInfo(currentEntity, event);
21
30
  if (hoverInfo) {
22
- currentEntity.add(traits.InstancedPose({
31
+ infoToLocalMatrix(hoverInfo, tempHoverMatrix);
32
+ const worldMatrix = currentEntity.get(traits.WorldMatrix);
33
+ const composed = new Matrix4();
34
+ if (worldMatrix) {
35
+ composed.copy(worldMatrix).multiply(tempHoverMatrix);
36
+ }
37
+ else {
38
+ composed.copy(tempHoverMatrix);
39
+ }
40
+ currentEntity.add(traits.InstancedMatrix({
41
+ matrix: composed,
23
42
  index: hoverInfo.index,
24
- x: hoverInfo.x,
25
- y: hoverInfo.y,
26
- z: hoverInfo.z,
27
- oX: hoverInfo.oX,
28
- oY: hoverInfo.oY,
29
- oZ: hoverInfo.oZ,
30
- theta: hoverInfo.theta,
31
43
  }));
32
44
  }
33
45
  currentEntity.add(traits.Hovered);
@@ -40,34 +52,21 @@ export const useEntityEvents = (entity) => {
40
52
  const currentEntity = entity();
41
53
  if (currentEntity?.has(traits.Hovered)) {
42
54
  const hoverInfo = updateHoverInfo(currentEntity, event);
43
- const hoverPose = createPose(hoverInfo
44
- ? {
45
- x: hoverInfo.x,
46
- y: hoverInfo.y,
47
- z: hoverInfo.z,
48
- oX: 0,
49
- oY: 0,
50
- oZ: 1,
51
- theta: 0,
52
- }
53
- : undefined);
54
- const worldPose = currentEntity.get(traits.WorldPose) ?? createPose();
55
- const hoverPoseMatrix = poseToMatrix(hoverPose);
56
- const worldPoseMatrix = poseToMatrix(worldPose);
57
- const resultMatrix = worldPoseMatrix.multiply(hoverPoseMatrix);
58
- const resultPose = matrixToPose(resultMatrix);
59
- if (hoverInfo) {
60
- currentEntity.set(traits.InstancedPose, {
61
- index: hoverInfo.index,
62
- x: resultPose.x,
63
- y: resultPose.y,
64
- z: resultPose.z,
65
- oX: resultPose.oX,
66
- oY: resultPose.oY,
67
- oZ: resultPose.oZ,
68
- theta: resultPose.theta,
69
- });
55
+ if (!hoverInfo)
56
+ return;
57
+ infoToLocalMatrix(hoverInfo, tempHoverMatrix);
58
+ const instanced = currentEntity.get(traits.InstancedMatrix);
59
+ if (!instanced)
60
+ return;
61
+ const worldMatrix = currentEntity.get(traits.WorldMatrix);
62
+ if (worldMatrix) {
63
+ instanced.matrix.copy(worldMatrix).multiply(tempHoverMatrix);
64
+ }
65
+ else {
66
+ instanced.matrix.copy(tempHoverMatrix);
70
67
  }
68
+ instanced.index = hoverInfo.index;
69
+ currentEntity.changed(traits.InstancedMatrix);
71
70
  }
72
71
  };
73
72
  const onpointerleave = (event) => {
@@ -77,8 +76,8 @@ export const useEntityEvents = (entity) => {
77
76
  if (currentEntity?.has(traits.Hovered)) {
78
77
  currentEntity.remove(traits.Hovered);
79
78
  }
80
- if (currentEntity?.has(traits.InstancedPose)) {
81
- currentEntity.remove(traits.InstancedPose);
79
+ if (currentEntity?.has(traits.InstancedMatrix)) {
80
+ currentEntity.remove(traits.InstancedMatrix);
82
81
  }
83
82
  };
84
83
  const ondblclick = (event) => {
@@ -109,8 +108,8 @@ export const useEntityEvents = (entity) => {
109
108
  if (currentEntity?.has(traits.Hovered)) {
110
109
  currentEntity.remove(traits.Hovered);
111
110
  }
112
- if (currentEntity?.has(traits.InstancedPose)) {
113
- currentEntity.remove(traits.InstancedPose);
111
+ if (currentEntity?.has(traits.InstancedMatrix)) {
112
+ currentEntity.remove(traits.InstancedMatrix);
114
113
  }
115
114
  }
116
115
  });
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
3
 
4
- import { provideHierarchy } from '../ecs'
4
+ import { provideHierarchy, provideWorldMatrix } from '../ecs'
5
5
  import { provide3DModels } from '../hooks/use3DModels.svelte'
6
6
  import { provideArmClient } from '../hooks/useArmClient.svelte'
7
7
  import { provideArmKinematics } from '../hooks/useArmKinematics.svelte'
@@ -43,6 +43,7 @@
43
43
  provideLogs()
44
44
 
45
45
  provideHierarchy()
46
+ provideWorldMatrix()
46
47
  provideOrigin()
47
48
  provideDrawAPI()
48
49
  provideRelationships()
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { TransformControls } from '@threlte/extras'
3
- import { Quaternion, Vector3 } from 'three'
3
+ import { Matrix4, Quaternion, Vector3 } from 'three'
4
4
 
5
5
  import type { FrameEditSession } from '../editing/FrameEditSession'
6
6
 
@@ -11,9 +11,11 @@
11
11
  import { useSelectedEntity, useSelectedObject3d } from '../hooks/useSelection.svelte'
12
12
  import { useSettings } from '../hooks/useSettings.svelte'
13
13
  import {
14
- composeEditedPoseForRenderedPose,
15
14
  createPose,
15
+ matrixToPose,
16
+ poseToMatrix,
16
17
  quaternionToPose,
18
+ solveEditedMatrix,
17
19
  vector3ToPose,
18
20
  } from '../transform'
19
21
 
@@ -27,8 +29,8 @@
27
29
  const mode = $derived(settings.current.transformMode)
28
30
  const entity = $derived(selectedEntity.current)
29
31
  const transformable = useTrait(() => entity, traits.Transformable)
30
- const networkPose = useTrait(() => entity, traits.Pose)
31
- const livePose = useTrait(() => entity, traits.LivePose)
32
+ const configMatrix = useTrait(() => entity, traits.Matrix)
33
+ const liveMatrix = useTrait(() => entity, traits.LiveMatrix)
32
34
  const box = useTrait(() => entity, traits.Box)
33
35
  const sphere = useTrait(() => entity, traits.Sphere)
34
36
  const capsule = useTrait(() => entity, traits.Capsule)
@@ -42,10 +44,12 @@
42
44
  // the geometry inside it.
43
45
  const ref = $derived(selectedObject3d.current?.parent ?? selectedObject3d.current)
44
46
 
45
- const activeMode = $derived.by(() => {
46
- if (mode === 'none' || !transformable.current) return undefined
47
+ const activeMode = $derived.by<'translate' | 'rotate' | 'scale' | undefined>(() => {
48
+ if (mode === 'none' || !transformable.current) return
49
+
47
50
  // Scale only does anything for primitive geometries the gizmo can size.
48
- if (mode === 'scale' && !hasScalableGeometry) return undefined
51
+ if (mode === 'scale' && !hasScalableGeometry) return
52
+
49
53
  return mode
50
54
  })
51
55
  const isSphereScale = $derived(activeMode === 'scale' && sphere.current !== undefined)
@@ -54,6 +58,9 @@
54
58
  const quaternion = new Quaternion()
55
59
  const vector3 = new Vector3()
56
60
  const refPose = createPose()
61
+ const tempRefMatrix = new Matrix4()
62
+ const tempEditedMatrix = new Matrix4()
63
+ const tempPose = createPose()
57
64
 
58
65
  let session: FrameEditSession | undefined
59
66
  let scaleStart:
@@ -93,7 +100,9 @@
93
100
  if (entity?.has(traits.FramesAPI)) {
94
101
  session = sessions.begin([entity])
95
102
  }
103
+
96
104
  captureScaleStart()
105
+
97
106
  environment.current.viewerMode = 'edit'
98
107
  transformControls.setActive(true)
99
108
  }
@@ -109,15 +118,17 @@
109
118
  if (isFrameEntity) {
110
119
  stageFrameTransform()
111
120
  } else {
112
- const pose = entity.get(traits.Pose)
113
- if (pose) {
121
+ const matrix = entity.get(traits.Matrix)
122
+ if (matrix) {
123
+ matrixToPose(matrix, tempPose)
114
124
  if (activeMode === 'translate') {
115
- vector3ToPose(ref.getWorldPosition(vector3), pose)
125
+ vector3ToPose(ref.getWorldPosition(vector3), tempPose)
116
126
  } else {
117
- quaternionToPose(ref.getWorldQuaternion(quaternion), pose)
127
+ quaternionToPose(ref.getWorldQuaternion(quaternion), tempPose)
118
128
  ref.quaternion.copy(quaternion)
119
129
  }
120
- entity.set(traits.Pose, pose)
130
+ poseToMatrix(tempPose, matrix)
131
+ entity.changed(traits.Matrix)
121
132
  }
122
133
  }
123
134
  } else {
@@ -127,11 +138,14 @@
127
138
  captureScaleStart()
128
139
  }
129
140
 
141
+ // Clamp at 0 — the gizmo can produce negative scale factors when
142
+ // dragged past the origin, which would yield negative dimensions
143
+ // and a degenerate OBB.
130
144
  if (scaleStart?.type === 'box') {
131
145
  const next = {
132
- x: scaleStart.x * ref.scale.x,
133
- y: scaleStart.y * ref.scale.y,
134
- z: scaleStart.z * ref.scale.z,
146
+ x: Math.max(0, scaleStart.x * ref.scale.x),
147
+ y: Math.max(0, scaleStart.y * ref.scale.y),
148
+ z: Math.max(0, scaleStart.z * ref.scale.z),
135
149
  }
136
150
  if (isFrameEntity) {
137
151
  session?.stageGeometry(entity, { type: 'box', ...next })
@@ -139,14 +153,17 @@
139
153
  entity.set(traits.Box, next)
140
154
  }
141
155
  } else if (scaleStart?.type === 'sphere') {
142
- const next = { r: scaleStart.r * ref.scale.x }
156
+ const next = { r: Math.max(0, scaleStart.r * ref.scale.x) }
143
157
  if (isFrameEntity) {
144
158
  session?.stageGeometry(entity, { type: 'sphere', ...next })
145
159
  } else {
146
160
  entity.set(traits.Sphere, next)
147
161
  }
148
162
  } else if (scaleStart?.type === 'capsule') {
149
- const next = { r: scaleStart.r * ref.scale.x, l: scaleStart.l * ref.scale.y }
163
+ const next = {
164
+ r: Math.max(0, scaleStart.r * ref.scale.x),
165
+ l: Math.max(0, scaleStart.l * ref.scale.y),
166
+ }
150
167
  if (isFrameEntity) {
151
168
  session?.stageGeometry(entity, { type: 'capsule', ...next })
152
169
  } else {
@@ -165,29 +182,31 @@
165
182
  transformControls.setActive(false)
166
183
  }
167
184
 
168
- // Pose.svelte renders frame entities through the live blend
169
- // render = M(live) × M(network)⁻¹ × M(edited)
170
- // so for the user's drag to render where they pulled the gizmo to, EditedPose
171
- // must satisfy
172
- // M(edited) = M(network) × M(live)⁻¹ × M(ref)
173
- // where M(ref) is the gizmo-driven group's parent-relative matrix in mm.
174
- // When live ≈ network (no kinematic offset), this collapses to M(edited) =
175
- // M(ref) — the same as the naive writeback. When they diverge (e.g. an arm
176
- // whose joints have moved away from its config pose), this composition is
177
- // what keeps the rendering anchored to the user's pointer instead of
178
- // shearing through the live × baseline⁻¹ offset.
185
+ /**
186
+ * Frame.svelte renders frame entities by blending M(live) × M(config)⁻¹ × M(edited)
187
+ * so for the user's drag to render where they pulled the gizmo to,
188
+ * EditedMatrix must satisfy M(edited) = M(config) × M(live)⁻¹ × M(ref)
189
+ * where M(ref) is the gizmo-driven group's parent-relative matrix in mm.
190
+ *
191
+ * When live ≈ config (no kinematic offset), this collapses to
192
+ * M(edited) = M(ref) — the same as the naive writeback. When they diverge
193
+ * (e.g. an arm whose joints have moved away from its config pose), this
194
+ * composition is what keeps the rendering anchored to the user's pointer
195
+ * instead of shearing through the live × baseline⁻¹ offset.
196
+ */
197
+
179
198
  const stageFrameTransform = () => {
180
199
  if (!ref || !entity) return
181
200
 
182
201
  vector3ToPose(ref.position, refPose)
183
202
  quaternionToPose(ref.quaternion, refPose)
184
203
 
185
- const live = livePose.current
186
- const network = networkPose.current
204
+ const live = liveMatrix.current
205
+ const config = configMatrix.current
187
206
 
188
- if (!live || !network) {
189
- // No live pose available — Pose.svelte's blend short-circuits to
190
- // editedPose, so naive writeback is correct.
207
+ if (!live || !config) {
208
+ // No live matrix available — Frame.svelte's blend short-circuits to
209
+ // editedMatrix, so naive writeback is correct.
191
210
  if (activeMode === 'translate') {
192
211
  session?.stagePose(entity, {
193
212
  x: refPose.x,
@@ -205,7 +224,11 @@
205
224
  return
206
225
  }
207
226
 
208
- session?.stagePose(entity, composeEditedPoseForRenderedPose(network, live, refPose))
227
+ poseToMatrix(refPose, tempRefMatrix)
228
+
229
+ solveEditedMatrix(config, live, tempRefMatrix, tempEditedMatrix)
230
+ matrixToPose(tempEditedMatrix, tempPose)
231
+ session?.stagePose(entity, { ...tempPose })
209
232
  }
210
233
  </script>
211
234
 
@@ -29,7 +29,7 @@
29
29
  keys.onKeys('=', () => {
30
30
  const entity = world.spawn(
31
31
  traits.Name(`custom geometry ${++index}`),
32
- traits.Pose,
32
+ traits.Matrix,
33
33
  traits.Box({ x: 100, y: 100, z: 100 }),
34
34
  traits.Removable,
35
35
  traits.Transformable
@@ -1,6 +1,11 @@
1
1
  <script lang="ts">
2
+ import { MathUtils, Quaternion, Vector3 } from 'three'
3
+
4
+ import type { HoverInfo } from '../../HoverUpdater.svelte'
5
+
2
6
  import { traits, useTrait } from '../../ecs'
3
7
  import { useFocusedEntity, useSelectedEntity } from '../../hooks/useSelection.svelte'
8
+ import { OrientationVector } from '../../three/OrientationVector'
4
9
 
5
10
  import HoveredEntityTooltip from './HoveredEntityTooltip.svelte'
6
11
 
@@ -8,9 +13,34 @@
8
13
  const focusedEntity = useFocusedEntity()
9
14
 
10
15
  const displayEntity = $derived(selectedEntity.current ?? focusedEntity.current)
11
- const hoverInfo = useTrait(() => displayEntity, traits.InstancedPose)
16
+ const instancedMatrix = useTrait(() => displayEntity, traits.InstancedMatrix)
17
+
18
+ // Pool: InstancedMatrix's `Matrix4` is in metres (matches Three.js).
19
+ // Decompose for the tooltip's display, which expects metres for position
20
+ // and OV+theta for orientation.
21
+ const translation = new Vector3()
22
+ const quaternion = new Quaternion()
23
+ const scaleVec = new Vector3()
24
+ const ov = new OrientationVector()
25
+
26
+ const hoverInfo = $derived.by((): HoverInfo | undefined => {
27
+ const data = instancedMatrix.current
28
+ if (!data) return undefined
29
+ data.matrix.decompose(translation, quaternion, scaleVec)
30
+ ov.setFromQuaternion(quaternion)
31
+ return {
32
+ index: data.index,
33
+ x: translation.x,
34
+ y: translation.y,
35
+ z: translation.z,
36
+ oX: ov.x,
37
+ oY: ov.y,
38
+ oZ: ov.z,
39
+ theta: MathUtils.radToDeg(ov.th),
40
+ }
41
+ })
12
42
  </script>
13
43
 
14
- {#if hoverInfo.current}
15
- <HoveredEntityTooltip hoverInfo={hoverInfo.current} />
44
+ {#if hoverInfo}
45
+ <HoveredEntityTooltip {hoverInfo} />
16
46
  {/if}
@@ -3,8 +3,7 @@
3
3
 
4
4
  import { compileExpression } from 'filtrex'
5
5
 
6
- import { relations, traits } from '../../ecs'
7
- import { useTrait } from '../../ecs'
6
+ import { relations, traits, useTrait } from '../../ecs'
8
7
  import { SubEntityLinkType } from '../../ecs/relations'
9
8
  import { useSelectedEntity } from '../../hooks/useSelection.svelte'
10
9
  import { useFocusedEntity } from '../../hooks/useSelection.svelte'
@@ -22,7 +21,7 @@
22
21
  const focusedEntity = useFocusedEntity()
23
22
  const displayEntity = $derived(selectedEntity.current ?? focusedEntity.current)
24
23
 
25
- const displayedHoverInfo = useTrait(() => displayEntity, traits.InstancedPose)
24
+ const displayedHoverInfo = useTrait(() => displayEntity, traits.InstancedMatrix)
26
25
 
27
26
  let hoverInfo = $state.raw<HoverInfo | null>(null)
28
27