@viamrobotics/motion-tools 1.26.1 → 1.27.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 (58) hide show
  1. package/dist/FrameConfigUpdater.svelte.js +42 -29
  2. package/dist/assert.d.ts +13 -0
  3. package/dist/assert.js +20 -0
  4. package/dist/buf/common/v1/common_pb.d.ts +19 -0
  5. package/dist/buf/common/v1/common_pb.js +32 -0
  6. package/dist/components/BatchedArrows.svelte +43 -45
  7. package/dist/components/Entities/Arrows/Arrows.svelte +35 -29
  8. package/dist/components/Entities/Entities.svelte +3 -8
  9. package/dist/components/Entities/Frame.svelte +31 -32
  10. package/dist/components/Entities/Frame.svelte.d.ts +0 -2
  11. package/dist/components/Entities/GLTF.svelte +27 -36
  12. package/dist/components/Entities/Geometry.svelte +35 -24
  13. package/dist/components/Entities/Line.svelte +37 -43
  14. package/dist/components/Entities/Mesh.svelte +12 -18
  15. package/dist/components/Entities/Points.svelte +25 -28
  16. package/dist/components/Entities/Pose.svelte +17 -24
  17. package/dist/components/Entities/Pose.svelte.d.ts +1 -4
  18. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +40 -41
  19. package/dist/components/Scene.svelte +7 -1
  20. package/dist/components/SceneProviders.svelte +2 -1
  21. package/dist/components/SelectedTransformControls.svelte +57 -34
  22. package/dist/components/StaticGeometries.svelte +1 -1
  23. package/dist/components/hover/HoveredEntity.svelte +33 -3
  24. package/dist/components/hover/LinkedHoveredEntity.svelte +2 -3
  25. package/dist/components/overlay/Details.svelte +72 -94
  26. package/dist/components/overlay/__tests__/__fixtures__/entity.js +14 -17
  27. package/dist/components/overlay/left-pane/Tree.svelte +9 -9
  28. package/dist/components/overlay/left-pane/Tree.svelte.d.ts +1 -2
  29. package/dist/components/overlay/left-pane/TreeContainer.svelte +4 -15
  30. package/dist/components/overlay/left-pane/TreeNode.svelte +1 -1
  31. package/dist/components/overlay/left-pane/TreeNode.svelte.d.ts +1 -1
  32. package/dist/components/overlay/left-pane/useTree.svelte.d.ts +14 -0
  33. package/dist/components/overlay/left-pane/useTree.svelte.js +63 -0
  34. package/dist/draw.js +24 -9
  35. package/dist/ecs/index.d.ts +1 -0
  36. package/dist/ecs/index.js +1 -0
  37. package/dist/ecs/provideWorldMatrix.svelte.d.ts +8 -0
  38. package/dist/ecs/provideWorldMatrix.svelte.js +13 -0
  39. package/dist/ecs/traits.d.ts +41 -50
  40. package/dist/ecs/traits.js +57 -29
  41. package/dist/ecs/useTrait.svelte.d.ts +1 -6
  42. package/dist/ecs/useTrait.svelte.js +21 -13
  43. package/dist/ecs/worldMatrix.d.ts +10 -0
  44. package/dist/ecs/worldMatrix.js +138 -0
  45. package/dist/editing/FrameEditSession.js +31 -18
  46. package/dist/hooks/use3DModels.svelte.js +1 -1
  47. package/dist/hooks/useConfigFrames.svelte.js +12 -0
  48. package/dist/hooks/useDrawAPI.svelte.js +14 -6
  49. package/dist/hooks/useDrawService.svelte.js +4 -7
  50. package/dist/hooks/useFrames.svelte.js +23 -11
  51. package/dist/hooks/useGeometries.svelte.js +11 -3
  52. package/dist/hooks/usePartConfig.svelte.js +43 -6
  53. package/dist/hooks/useWorldState.svelte.js +10 -2
  54. package/dist/plugins/bvh.svelte.js +37 -26
  55. package/dist/transform.js +55 -21
  56. package/package.json +3 -3
  57. package/dist/components/overlay/left-pane/buildTree.d.ts +0 -13
  58. package/dist/components/overlay/left-pane/buildTree.js +0 -48
@@ -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
 
@@ -3,22 +3,22 @@
3
3
  lang="ts"
4
4
  >
5
5
  import { ThemeUtils } from 'svelte-tweakpane-ui'
6
- import { BufferAttribute, Euler, MathUtils, Quaternion, Vector3 } from 'three'
6
+ import { BufferAttribute, Euler, MathUtils, Quaternion } from 'three'
7
7
 
8
8
  import { OrientationVector } from '../../three/OrientationVector'
9
9
 
10
- const vec3 = new Vector3()
11
10
  const quaternion = new Quaternion()
12
11
  const ov = new OrientationVector()
13
12
  const euler = new Euler()
14
13
  </script>
15
14
 
16
15
  <script lang="ts">
16
+ import type { Pose } from '@viamrobotics/sdk'
17
17
  import type { Entity } from 'koota'
18
18
  import type { Snippet } from 'svelte'
19
19
 
20
20
  import { draggable } from '@neodrag/svelte'
21
- import { isInstanceOf, useTask, useThrelte } from '@threlte/core'
21
+ import { isInstanceOf, useThrelte } from '@threlte/core'
22
22
  import { Button, Icon, Tooltip } from '@viamrobotics/prime-core'
23
23
  import { Check, Copy } from 'lucide-svelte'
24
24
  import {
@@ -53,7 +53,7 @@
53
53
  useSelectedEntity,
54
54
  useSelectedObject3d,
55
55
  } from '../../hooks/useSelection.svelte'
56
- import { createPose } from '../../transform'
56
+ import { createPose, matrixToPose } from '../../transform'
57
57
 
58
58
  interface Props {
59
59
  details?: Snippet<[{ entity: Entity }]>
@@ -73,14 +73,17 @@
73
73
  const environment = useEnvironment()
74
74
  const focusedEntity = useFocusedEntity()
75
75
  const focusedObject3d = useFocusedObject3d()
76
+ const linkedEntities = useLinkedEntities()
77
+
76
78
  const entity = $derived(focusedEntity.current ?? selectedEntity.current)
77
79
  const object3d = $derived(focusedObject3d.current ?? selectedObject3d.current)
78
- const worldPosition = $state({ x: 0, y: 0, z: 0 })
79
- const worldOrientation = $state({ x: 0, y: 0, z: 1, th: 0 })
80
- const linkedEntities = useLinkedEntities()
80
+
81
81
  const name = useTrait(() => entity, traits.Name)
82
82
  const parent = useParentName(() => entity)
83
- const localPose = useTrait(() => entity, traits.EditedPose)
83
+ const matrix = useTrait(() => entity, traits.Matrix)
84
+ const editedMatrix = useTrait(() => entity, traits.EditedMatrix)
85
+ const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
86
+ const center = useTrait(() => entity, traits.Center)
84
87
  const box = useTrait(() => entity, traits.Box)
85
88
  const sphere = useTrait(() => entity, traits.Sphere)
86
89
  const capsule = useTrait(() => entity, traits.Capsule)
@@ -88,14 +91,27 @@
88
91
  const points = useTrait(() => entity, traits.Points)
89
92
  const arrows = useTrait(() => entity, traits.Arrows)
90
93
  const opacity = useTrait(() => entity, traits.Opacity)
91
-
92
94
  const framesAPI = useTrait(() => entity, traits.FramesAPI)
93
- const isFrameNode = $derived(!!framesAPI.current)
95
+ const geometriesAPI = useTrait(() => entity, traits.GeometriesAPI)
96
+
97
+ const localPose = $derived.by<Pose | undefined>(() => {
98
+ const source = editedMatrix.current ?? matrix.current
99
+ if (source) return matrixToPose(source, createPose())
100
+ if (center.current) return createPose(center.current)
101
+ return undefined
102
+ })
103
+ const worldPose = $derived.by<Pose | undefined>(() => {
104
+ if (!worldMatrix.current) return
105
+
106
+ return matrixToPose(worldMatrix.current, createPose())
107
+ })
94
108
 
109
+ const isFrameNode = $derived(!!framesAPI.current)
110
+ const isGeometry = $derived(!!geometriesAPI.current)
95
111
  const showEditFrameOptions = $derived(isFrameNode && partConfig.hasEditPermissions)
96
112
  const showRelationshipOptions = $derived(points.current || arrows.current)
97
-
98
113
  const resourceName = $derived(name.current ? resourceByName.current[name.current] : undefined)
114
+ const displayType = $derived(isFrameNode ? resourceName?.subtype : isGeometry ? 'geometry' : '')
99
115
 
100
116
  let geometryType = $derived.by<'box' | 'sphere' | 'capsule' | 'none'>(() => {
101
117
  if (box.current) return 'box'
@@ -120,13 +136,8 @@
120
136
  let dragElement = $state.raw<HTMLElement>()
121
137
 
122
138
  const eulerValue = $derived.by<RotationEulerValueObject>(() => {
123
- if (!localPose.current) return { x: 0, y: 0, z: 0 }
124
- ov.set(
125
- localPose.current.oX,
126
- localPose.current.oY,
127
- localPose.current.oZ,
128
- MathUtils.degToRad(localPose.current.theta)
129
- )
139
+ if (!localPose) return { x: 0, y: 0, z: 0 }
140
+ ov.set(localPose.oX, localPose.oY, localPose.oZ, MathUtils.degToRad(localPose.theta))
130
141
  ov.toEuler(euler)
131
142
  return {
132
143
  x: MathUtils.radToDeg(euler.x),
@@ -236,65 +247,28 @@
236
247
  }
237
248
  }
238
249
 
239
- useTask(
240
- () => {
241
- object3d?.getWorldPosition(vec3)
242
- if (!vec3.equals(worldPosition)) {
243
- worldPosition.x = vec3.x
244
- worldPosition.y = vec3.y
245
- worldPosition.z = vec3.z
246
- }
247
-
248
- object3d?.getWorldQuaternion(quaternion)
249
- ov.setFromQuaternion(quaternion)
250
-
251
- if (!ov.equals(worldOrientation)) {
252
- worldOrientation.x = ov.x
253
- worldOrientation.y = ov.y
254
- worldOrientation.z = ov.z
255
- worldOrientation.th = ov.th
256
- }
257
- },
258
- {
259
- autoInvalidate: false,
260
- running: () => object3d !== undefined,
261
- }
262
- )
263
-
264
- $effect(() => {
265
- if (entity) {
266
- const worldPose = createPose({
267
- x: worldPosition.x,
268
- y: worldPosition.y,
269
- z: worldPosition.z,
270
- oX: worldOrientation.x,
271
- oY: worldOrientation.y,
272
- oZ: worldOrientation.z,
273
- theta: MathUtils.radToDeg(worldOrientation.th),
274
- })
275
- if (entity.has(traits.WorldPose)) {
276
- entity.set(traits.WorldPose, worldPose)
277
- } else {
278
- entity.add(traits.WorldPose(worldPose))
279
- }
280
- }
281
- })
282
-
283
250
  const getCopyClipboardText = () => {
284
251
  return JSON.stringify(
285
252
  {
286
- worldPosition: worldPosition,
287
- worldOrientation: worldOrientation,
253
+ worldPosition: worldPose ? { x: worldPose.x, y: worldPose.y, z: worldPose.z } : null,
254
+ worldOrientation: worldPose
255
+ ? {
256
+ x: worldPose.oX,
257
+ y: worldPose.oY,
258
+ z: worldPose.oZ,
259
+ th: MathUtils.degToRad(worldPose.theta),
260
+ }
261
+ : null,
288
262
  localPosition: {
289
- x: localPose.current?.x,
290
- y: localPose.current?.y,
291
- z: localPose.current?.z,
263
+ x: localPose?.x,
264
+ y: localPose?.y,
265
+ z: localPose?.z,
292
266
  },
293
267
  localOrientation: {
294
- x: localPose.current?.oX,
295
- y: localPose.current?.oY,
296
- z: localPose.current?.oZ,
297
- th: localPose.current?.theta,
268
+ x: localPose?.oX,
269
+ y: localPose?.oY,
270
+ z: localPose?.oZ,
271
+ th: localPose?.theta,
298
272
  },
299
273
  geometry: {
300
274
  type: geometryType,
@@ -350,7 +324,7 @@
350
324
  >
351
325
  <div class="flex w-[90%] items-center gap-1">
352
326
  <strong class="overflow-hidden text-nowrap text-ellipsis">{name.current}</strong>
353
- <span class="text-subtle-2">{resourceName?.subtype}</span>
327
+ <span class="text-subtle-2">{displayType}</span>
354
328
  </div>
355
329
 
356
330
  {#if object3d}
@@ -438,15 +412,15 @@
438
412
  <div class="flex gap-3">
439
413
  <div>
440
414
  <span class="text-subtle-2">x</span>
441
- {(worldPosition.x * 1000).toFixed(2)}
415
+ {(worldPose?.x ?? 0).toFixed(2)}
442
416
  </div>
443
417
  <div>
444
418
  <span class="text-subtle-2">y</span>
445
- {(worldPosition.y * 1000).toFixed(2)}
419
+ {(worldPose?.y ?? 0).toFixed(2)}
446
420
  </div>
447
421
  <div>
448
422
  <span class="text-subtle-2">z</span>
449
- {(worldPosition.z * 1000).toFixed(2)}
423
+ {(worldPose?.z ?? 0).toFixed(2)}
450
424
  </div>
451
425
  </div>
452
426
  </div>
@@ -457,19 +431,19 @@
457
431
  <div class="flex gap-3">
458
432
  <div>
459
433
  <span class="text-subtle-2">x</span>
460
- {worldOrientation.x.toFixed(2)}
434
+ {(worldPose?.oX ?? 0).toFixed(2)}
461
435
  </div>
462
436
  <div>
463
437
  <span class="text-subtle-2">y</span>
464
- {worldOrientation.y.toFixed(2)}
438
+ {(worldPose?.oY ?? 0).toFixed(2)}
465
439
  </div>
466
440
  <div>
467
441
  <span class="text-subtle-2">z</span>
468
- {worldOrientation.z.toFixed(2)}
442
+ {(worldPose?.oZ ?? 0).toFixed(2)}
469
443
  </div>
470
444
  <div>
471
445
  <span class="text-subtle-2">th</span>
472
- {MathUtils.radToDeg(worldOrientation.th).toFixed(2)}
446
+ {(worldPose?.theta ?? 0).toFixed(2)}
473
447
  </div>
474
448
  </div>
475
449
  </div>
@@ -505,7 +479,7 @@
505
479
  {/if}
506
480
  </div>
507
481
 
508
- {#if localPose.current}
482
+ {#if localPose}
509
483
  <div>
510
484
  <strong class="font-semibold">local position</strong>
511
485
  <span class="text-subtle-2">(mm)</span>
@@ -514,9 +488,9 @@
514
488
  <div aria-label="mutable local position">
515
489
  <Point
516
490
  value={{
517
- x: localPose.current.x,
518
- y: localPose.current.y,
519
- z: localPose.current.z,
491
+ x: localPose.x,
492
+ y: localPose.y,
493
+ z: localPose.z,
520
494
  }}
521
495
  on:change={handlePositionChange}
522
496
  />
@@ -526,17 +500,17 @@
526
500
  {@render ImmutableField({
527
501
  label: 'x',
528
502
  ariaLabel: 'local position x coordinate',
529
- value: localPose.current.x,
503
+ value: localPose.x,
530
504
  })}
531
505
  {@render ImmutableField({
532
506
  label: 'y',
533
507
  ariaLabel: 'local position y coordinate',
534
- value: localPose.current.y,
508
+ value: localPose.y,
535
509
  })}
536
510
  {@render ImmutableField({
537
511
  label: 'z',
538
512
  ariaLabel: 'local position z coordinate',
539
- value: localPose.current.z,
513
+ value: localPose.z,
540
514
  })}
541
515
  </div>
542
516
  {/if}
@@ -551,10 +525,10 @@
551
525
  <TabPage title="OV (deg)">
552
526
  <Point
553
527
  value={{
554
- x: localPose.current.oX,
555
- y: localPose.current.oY,
556
- z: localPose.current.oZ,
557
- w: localPose.current.theta,
528
+ x: localPose.oX,
529
+ y: localPose.oY,
530
+ z: localPose.oZ,
531
+ w: localPose.theta,
558
532
  }}
559
533
  on:change={handleOrientationOVChange}
560
534
  />
@@ -573,22 +547,22 @@
573
547
  {@render ImmutableField({
574
548
  label: 'x',
575
549
  ariaLabel: 'local orientation x coordinate',
576
- value: localPose.current.oX,
550
+ value: localPose.oX,
577
551
  })}
578
552
  {@render ImmutableField({
579
553
  label: 'y',
580
554
  ariaLabel: 'local orientation y coordinate',
581
- value: localPose.current.oY,
555
+ value: localPose.oY,
582
556
  })}
583
557
  {@render ImmutableField({
584
558
  label: 'z',
585
559
  ariaLabel: 'local orientation z coordinate',
586
- value: localPose.current.oZ,
560
+ value: localPose.oZ,
587
561
  })}
588
562
  {@render ImmutableField({
589
563
  label: 'th',
590
564
  ariaLabel: 'local orientation theta degrees',
591
- value: localPose.current.theta,
565
+ value: localPose.theta,
592
566
  })}
593
567
  </div>
594
568
  {/if}
@@ -611,6 +585,7 @@
611
585
  y: box.current.y,
612
586
  z: box.current.z,
613
587
  }}
588
+ min={0}
614
589
  on:change={handleBoxChange}
615
590
  />
616
591
  </div>
@@ -622,6 +597,7 @@
622
597
  <Slider
623
598
  label="r"
624
599
  value={sphere.current.r}
600
+ min={0}
625
601
  on:change={handleSphereRChange}
626
602
  />
627
603
  </div>
@@ -633,11 +609,13 @@
633
609
  <Slider
634
610
  label="r"
635
611
  value={capsule.current.r}
612
+ min={0}
636
613
  on:change={handleCapsuleRChange}
637
614
  />
638
615
  <Slider
639
616
  label="l"
640
617
  value={capsule.current.l}
618
+ min={0}
641
619
  on:change={handleCapsuleLChange}
642
620
  />
643
621
  </div>
@@ -1,20 +1,17 @@
1
+ import { Matrix4 } from 'three';
1
2
  import { hierarchy, traits } from '../../../../ecs';
3
+ import { createPose, poseToMatrix } from '../../../../transform';
4
+ // OV must be a unit vector — (0.6, 0.8, 0) magnitude 1 — so the matrix
5
+ // round-trip in Details.svelte returns the same components.
6
+ const buildMatrix = () => poseToMatrix(createPose({
7
+ x: 10,
8
+ y: 20,
9
+ z: 30,
10
+ oX: 0.6,
11
+ oY: 0.8,
12
+ oZ: 0,
13
+ theta: 0.4,
14
+ }), new Matrix4());
2
15
  export const createEntityFixture = (world) => {
3
- return world.spawn(...hierarchy.parentTraits('parent_frame'), traits.Name('Test Object'), traits.Pose({
4
- x: 10,
5
- y: 20,
6
- z: 30,
7
- oX: 0.1,
8
- oY: 0.2,
9
- oZ: 0.3,
10
- theta: 0.4,
11
- }), traits.EditedPose({
12
- x: 10,
13
- y: 20,
14
- z: 30,
15
- oX: 0.1,
16
- oY: 0.2,
17
- oZ: 0.3,
18
- theta: 0.4,
19
- }), traits.Box({ x: 0.01, y: 0.02, z: 0.03 }));
16
+ return world.spawn(...hierarchy.parentTraits('parent_frame'), traits.Name('Test Object'), traits.Matrix(buildMatrix()), traits.EditedMatrix(buildMatrix()), traits.Box({ x: 0.01, y: 0.02, z: 0.03 }));
20
17
  };