@viamrobotics/motion-tools 0.19.2 → 1.0.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 (135) hide show
  1. package/README.md +56 -26
  2. package/dist/FrameConfigUpdater.svelte.d.ts +11 -17
  3. package/dist/FrameConfigUpdater.svelte.js +109 -109
  4. package/dist/WorldObject.svelte.js +2 -15
  5. package/dist/common/v1/common_pb.d.ts +950 -0
  6. package/dist/common/v1/common_pb.js +1399 -0
  7. package/dist/components/App.svelte +37 -21
  8. package/dist/components/App.svelte.d.ts +1 -0
  9. package/dist/components/BatchedArrows.svelte +102 -0
  10. package/dist/components/BatchedArrows.svelte.d.ts +3 -0
  11. package/dist/components/CameraControls.svelte +2 -3
  12. package/dist/components/Details.svelte +364 -365
  13. package/dist/components/Entities.svelte +73 -0
  14. package/dist/components/{WorldObjects.svelte.d.ts → Entities.svelte.d.ts} +3 -3
  15. package/dist/components/FileDrop.svelte +9 -23
  16. package/dist/components/Focus.svelte +2 -3
  17. package/dist/components/Frame.svelte +41 -22
  18. package/dist/components/Frame.svelte.d.ts +4 -6
  19. package/dist/components/GLTF.svelte +36 -0
  20. package/dist/components/GLTF.svelte.d.ts +11 -0
  21. package/dist/components/Geometry2.svelte +201 -0
  22. package/dist/components/Geometry2.svelte.d.ts +18 -0
  23. package/dist/components/KeyboardControls.svelte +3 -3
  24. package/dist/components/Line.svelte +10 -13
  25. package/dist/components/Line.svelte.d.ts +2 -2
  26. package/dist/components/LiveUpdatesBanner.svelte +51 -15
  27. package/dist/components/MeasureTool.svelte +4 -5
  28. package/dist/components/Pointcloud.svelte +27 -14
  29. package/dist/components/Pointcloud.svelte.d.ts +2 -2
  30. package/dist/components/PointerMissBox.svelte +3 -3
  31. package/dist/components/Pose.svelte +31 -6
  32. package/dist/components/Pose.svelte.d.ts +2 -2
  33. package/dist/components/Scene.svelte +7 -6
  34. package/dist/components/SceneProviders.svelte +0 -6
  35. package/dist/components/Selected.svelte +22 -16
  36. package/dist/components/StaticGeometries.svelte +51 -27
  37. package/dist/components/Tree/Tree.svelte +28 -22
  38. package/dist/components/Tree/Tree.svelte.d.ts +2 -3
  39. package/dist/components/Tree/TreeContainer.svelte +72 -40
  40. package/dist/components/Tree/Widgets.svelte +2 -5
  41. package/dist/components/Tree/buildTree.d.ts +3 -6
  42. package/dist/components/Tree/buildTree.js +19 -39
  43. package/dist/components/__tests__/__fixtures__/entity.d.ts +2 -0
  44. package/dist/components/__tests__/__fixtures__/entity.js +20 -0
  45. package/dist/components/__tests__/__fixtures__/resource.d.ts +17 -0
  46. package/dist/components/__tests__/__fixtures__/resource.js +13 -0
  47. package/dist/components/dashboard/Dashboard.svelte +5 -3
  48. package/dist/components/dashboard/Dashboard.svelte.d.ts +7 -2
  49. package/dist/components/widgets/ArmPositions.svelte +19 -7
  50. package/dist/draw/v1/drawing_pb.d.ts +341 -0
  51. package/dist/draw/v1/drawing_pb.js +417 -0
  52. package/dist/draw/v1/metadata_pb.d.ts +23 -0
  53. package/dist/draw/v1/metadata_pb.js +39 -0
  54. package/dist/draw/v1/scene_pb.d.ts +230 -0
  55. package/dist/draw/v1/scene_pb.js +298 -0
  56. package/dist/draw/v1/snapshot_pb.d.ts +42 -0
  57. package/dist/draw/v1/snapshot_pb.js +61 -0
  58. package/dist/draw/v1/transforms_pb.d.ts +23 -0
  59. package/dist/draw/v1/transforms_pb.js +39 -0
  60. package/dist/ecs/index.d.ts +4 -0
  61. package/dist/ecs/index.js +4 -0
  62. package/dist/ecs/traits.d.ts +128 -0
  63. package/dist/ecs/traits.js +81 -0
  64. package/dist/ecs/useQuery.svelte.d.ts +4 -0
  65. package/dist/ecs/useQuery.svelte.js +49 -0
  66. package/dist/ecs/useTrait.svelte.d.ts +19 -0
  67. package/dist/ecs/useTrait.svelte.js +40 -0
  68. package/dist/ecs/useWorld.d.ts +4 -0
  69. package/dist/ecs/useWorld.js +10 -0
  70. package/dist/geometry.js +6 -6
  71. package/dist/hooks/__tests__/fixtures/ResizableTestWrapper.svelte +41 -0
  72. package/dist/hooks/__tests__/fixtures/ResizableTestWrapper.svelte.d.ts +6 -0
  73. package/dist/hooks/use3DModels.svelte.js +6 -4
  74. package/dist/hooks/useDrawAPI.svelte.d.ts +0 -10
  75. package/dist/hooks/useDrawAPI.svelte.js +143 -267
  76. package/dist/hooks/useFramelessComponents.svelte.js +1 -1
  77. package/dist/hooks/useFrames.svelte.d.ts +6 -2
  78. package/dist/hooks/useFrames.svelte.js +123 -19
  79. package/dist/hooks/useGeometries.svelte.d.ts +0 -2
  80. package/dist/hooks/useGeometries.svelte.js +49 -25
  81. package/dist/hooks/useObjectEvents.svelte.d.ts +3 -2
  82. package/dist/hooks/useObjectEvents.svelte.js +11 -7
  83. package/dist/hooks/usePartConfig.svelte.d.ts +1 -1
  84. package/dist/hooks/usePartConfig.svelte.js +2 -1
  85. package/dist/hooks/usePointclouds.svelte.d.ts +0 -2
  86. package/dist/hooks/usePointclouds.svelte.js +52 -21
  87. package/dist/hooks/usePose.svelte.js +15 -7
  88. package/dist/hooks/useResizable.svelte.d.ts +12 -0
  89. package/dist/hooks/useResizable.svelte.js +45 -0
  90. package/dist/hooks/useResourceByName.svelte.js +8 -5
  91. package/dist/hooks/useSelection.svelte.d.ts +13 -23
  92. package/dist/hooks/useSelection.svelte.js +45 -65
  93. package/dist/hooks/useVisibility.svelte.d.ts +2 -1
  94. package/dist/hooks/useWeblabs.svelte.d.ts +0 -1
  95. package/dist/hooks/useWeblabs.svelte.js +0 -1
  96. package/dist/hooks/useWorldState.svelte.d.ts +9 -0
  97. package/dist/hooks/useWorldState.svelte.js +158 -107
  98. package/dist/lib.d.ts +1 -0
  99. package/dist/lib.js +2 -0
  100. package/dist/three/BatchedArrow.d.ts +2 -3
  101. package/dist/three/BatchedArrow.js +3 -11
  102. package/dist/three/CapsuleGeometry.d.ts +1 -1
  103. package/dist/three/CapsuleGeometry.js +3 -1
  104. package/dist/transform.js +0 -15
  105. package/package.json +12 -7
  106. package/dist/components/WorldObject.svelte +0 -28
  107. package/dist/components/WorldObject.svelte.d.ts +0 -11
  108. package/dist/components/WorldObjects.svelte +0 -159
  109. package/dist/components/WorldState.svelte +0 -92
  110. package/dist/components/WorldState.svelte.d.ts +0 -7
  111. package/dist/components/__tests__/__fixtures__/worldObject.svelte.d.ts +0 -2
  112. package/dist/components/__tests__/__fixtures__/worldObject.svelte.js +0 -35
  113. package/dist/components/portal/Portal.svelte +0 -25
  114. package/dist/components/portal/Portal.svelte.d.ts +0 -8
  115. package/dist/components/portal/PortalTarget.svelte +0 -18
  116. package/dist/components/portal/PortalTarget.svelte.d.ts +0 -6
  117. package/dist/components/portal/index.d.ts +0 -2
  118. package/dist/components/portal/index.js +0 -2
  119. package/dist/components/portal/usePortalContext.svelte.d.ts +0 -5
  120. package/dist/components/portal/usePortalContext.svelte.js +0 -5
  121. package/dist/hooks/useArrows.svelte.d.ts +0 -3
  122. package/dist/hooks/useArrows.svelte.js +0 -9
  123. package/dist/hooks/useDraggable.svelte.d.ts +0 -10
  124. package/dist/hooks/useDraggable.svelte.js +0 -36
  125. package/dist/hooks/useObjects.svelte.d.ts +0 -7
  126. package/dist/hooks/useObjects.svelte.js +0 -35
  127. package/dist/hooks/usePersistentUUIDs.svelte.d.ts +0 -5
  128. package/dist/hooks/usePersistentUUIDs.svelte.js +0 -13
  129. package/dist/hooks/useResourceByName.svelte.d.ts +0 -7
  130. package/dist/hooks/useStaticGeometries.svelte.d.ts +0 -9
  131. package/dist/hooks/useStaticGeometries.svelte.js +0 -47
  132. package/dist/workers/worldStateWorker.d.ts +0 -1
  133. package/dist/workers/worldStateWorker.js +0 -114
  134. package/dist/world-state-messages.d.ts +0 -23
  135. package/dist/world-state-messages.js +0 -1
@@ -3,7 +3,7 @@
3
3
  lang="ts"
4
4
  >
5
5
  import { OrientationVector } from '../three/OrientationVector'
6
- import { Quaternion, Vector3, MathUtils } from 'three'
6
+ import { Quaternion, Vector3, MathUtils, type Vector2Like } from 'three'
7
7
 
8
8
  const vec3 = new Vector3()
9
9
  const quaternion = new Quaternion()
@@ -11,68 +11,81 @@
11
11
  </script>
12
12
 
13
13
  <script lang="ts">
14
+ import { draggable } from '@neodrag/svelte'
14
15
  import { Check, Copy } from 'lucide-svelte'
15
16
  import { useTask } from '@threlte/core'
16
17
  import { Button, Icon, Select, Input } from '@viamrobotics/prime-core'
17
18
  import {
18
- useSelectedObject,
19
- useFocusedObject,
20
- useFocused,
19
+ useSelectedEntity,
20
+ useFocusedEntity,
21
21
  useFocusedObject3d,
22
22
  useSelectedObject3d,
23
23
  } from '../hooks/useSelection.svelte'
24
- import { useDraggable } from '../hooks/useDraggable.svelte'
25
- import WeblabActive from './weblab/WeblabActive.svelte'
26
24
  import { useFrames } from '../hooks/useFrames.svelte'
27
25
  import { usePartConfig } from '../hooks/usePartConfig.svelte'
28
26
  import { FrameConfigUpdater } from '../FrameConfigUpdater.svelte'
29
- import { useWeblabs } from '../hooks/useWeblabs.svelte'
30
- import { WEBLABS_EXPERIMENTS } from '../hooks/useWeblabs.svelte'
31
27
  import { useEnvironment } from '../hooks/useEnvironment.svelte'
28
+ import { traits, useTrait } from '../ecs'
29
+ import { useResourceByName } from '../hooks/useResourceByName.svelte'
30
+ import { PersistedState } from 'runed'
32
31
 
33
32
  const { ...rest } = $props()
34
33
 
35
- const focused = useFocused()
36
- const focusedObject = useFocusedObject()
37
- const focusedObject3d = useFocusedObject3d()
34
+ const dragPosition = new PersistedState<Vector2Like | undefined>(
35
+ 'details-drag-position',
36
+ undefined
37
+ )
38
+
39
+ const resourceByName = useResourceByName()
38
40
  const frames = useFrames()
39
41
  const partConfig = usePartConfig()
40
- const selectedObject = useSelectedObject()
42
+ const selectedEntity = useSelectedEntity()
41
43
  const selectedObject3d = useSelectedObject3d()
42
- const weblab = useWeblabs()
43
44
  const environment = useEnvironment()
44
- const object = $derived(focusedObject.current ?? selectedObject.current)
45
+ const focusedEntity = useFocusedEntity()
46
+ const focusedObject3d = useFocusedObject3d()
47
+ const entity = $derived(focusedEntity.current ?? selectedEntity.current)
45
48
  const object3d = $derived(focusedObject3d.current ?? selectedObject3d.current)
46
49
  const worldPosition = $state({ x: 0, y: 0, z: 0 })
47
50
  const worldOrientation = $state({ x: 0, y: 0, z: 1, th: 0 })
48
- let geometryType = $derived.by(
49
- () =>
50
- (object?.geometry?.geometryType.case as 'none' | 'box' | 'sphere' | 'capsule' | undefined) ??
51
- 'none'
52
- )
53
51
 
54
- const localPose = $derived(object?.localEditedPose)
55
- const referenceFrame = $derived(object?.referenceFrame ?? 'world')
56
- const referenceFrameOptions = $derived(frames.getParentFrameOptions(object?.name ?? ''))
57
- const isFrameNode = $derived(
58
- frames.current.find((frame) => frame.name === object?.name) !== undefined
59
- )
52
+ const name = useTrait(() => entity, traits.Name)
53
+ const parent = useTrait(() => entity, traits.Parent)
54
+ const localPose = useTrait(() => entity, traits.EditedPose)
55
+ const box = useTrait(() => entity, traits.Box)
56
+ const sphere = useTrait(() => entity, traits.Sphere)
57
+ const capsule = useTrait(() => entity, traits.Capsule)
58
+
59
+ const framesAPI = useTrait(() => entity, traits.FramesAPI)
60
+ const isFrameNode = $derived(!!framesAPI.current)
61
+
60
62
  const showEditFrameOptions = $derived(isFrameNode && partConfig.hasEditPermissions)
63
+
64
+ const resourceName = $derived(name.current ? resourceByName.current[name.current] : undefined)
65
+
66
+ let geometryType = $derived.by<'box' | 'sphere' | 'capsule' | 'none'>(() => {
67
+ if (box.current) return 'box'
68
+ if (sphere.current) return 'sphere'
69
+ if (capsule.current) return 'capsule'
70
+ return 'none'
71
+ })
72
+
61
73
  let copied = $state(false)
62
74
 
63
- const draggable = useDraggable('details')
75
+ let dragElement = $state.raw<HTMLElement>()
64
76
 
65
- const detailConfigUpdater = new FrameConfigUpdater(
66
- () => object,
67
- partConfig.updateFrame,
68
- partConfig.deleteFrame,
69
- () => referenceFrame
70
- )
77
+ const detailConfigUpdater = new FrameConfigUpdater(partConfig.updateFrame, partConfig.deleteFrame)
71
78
 
72
79
  const setGeometryType = (type: 'none' | 'box' | 'sphere' | 'capsule') => {
73
- if (type === geometryType) return
80
+ if (type === geometryType) {
81
+ return
82
+ }
83
+
74
84
  geometryType = type
75
- detailConfigUpdater.setGeometryType(type)
85
+
86
+ if (entity) {
87
+ detailConfigUpdater.setGeometryType(entity, type)
88
+ }
76
89
  }
77
90
 
78
91
  const { start, stop } = useTask(
@@ -109,45 +122,43 @@
109
122
  })
110
123
 
111
124
  const getCopyClipboardText = () => {
112
- if (weblab.isActive(WEBLABS_EXPERIMENTS.MOTION_TOOLS_EDIT_FRAME)) {
113
- return JSON.stringify(
114
- {
115
- worldPosition: worldPosition,
116
- worldOrientation: worldOrientation,
117
- localPosition: {
118
- x: localPose?.x,
119
- y: localPose?.y,
120
- z: localPose?.z,
121
- },
122
- localOrientation: {
123
- x: localPose?.oX,
124
- y: localPose?.oY,
125
- z: localPose?.oZ,
126
- th: localPose?.theta,
127
- },
128
- geometry: {
129
- type: geometryType,
130
- value: object?.geometry?.geometryType.value,
131
- },
132
- parentFrame: referenceFrame,
125
+ return JSON.stringify(
126
+ {
127
+ worldPosition: worldPosition,
128
+ worldOrientation: worldOrientation,
129
+ localPosition: {
130
+ x: localPose.current?.x,
131
+ y: localPose.current?.y,
132
+ z: localPose.current?.z,
133
133
  },
134
- null,
135
- 2
136
- )
137
- } else {
138
- return JSON.stringify(
139
- {
140
- worldPosition: worldPosition,
141
- worldOrientation: worldOrientation,
142
- geometry: {
143
- type: geometryType,
144
- value: object?.geometry?.geometryType.value,
145
- },
134
+ localOrientation: {
135
+ x: localPose.current?.oX,
136
+ y: localPose.current?.oY,
137
+ z: localPose.current?.oZ,
138
+ th: localPose.current?.theta,
146
139
  },
147
- null,
148
- 2
149
- )
150
- }
140
+ geometry: {
141
+ type: geometryType,
142
+ value: box.current ?? capsule.current ?? sphere.current,
143
+ },
144
+ parentFrame: parent.current ?? 'world',
145
+ },
146
+ null,
147
+ 2
148
+ )
149
+ }
150
+
151
+ const isIntermediateInput = (input: string) => {
152
+ if (input === '0') return false
153
+
154
+ return (
155
+ input.startsWith('0') ||
156
+ input.startsWith('.') ||
157
+ input.startsWith('-0') ||
158
+ input.startsWith('-.') ||
159
+ (input.includes('.') && input.endsWith('0')) ||
160
+ input.endsWith('.')
161
+ )
151
162
  }
152
163
  </script>
153
164
 
@@ -157,7 +168,7 @@
157
168
  ariaLabel,
158
169
  }: {
159
170
  label?: string
160
- value: string
171
+ value?: number | string
161
172
  ariaLabel: string
162
173
  })}
163
174
  <div>
@@ -168,7 +179,7 @@
168
179
  {label}
169
180
  </span>
170
181
 
171
- {value}
182
+ {typeof value === 'number' ? value.toFixed(2) : (value ?? '-')}
172
183
  </div>
173
184
  {/snippet}
174
185
 
@@ -179,16 +190,14 @@
179
190
  onInput,
180
191
  }: {
181
192
  label: string
182
- value: string
193
+ value?: number
183
194
  ariaLabel: string
184
195
  onInput: (value: string) => void
185
196
  })}
186
197
  <div class="flex items-center gap-1">
187
198
  <span class="text-subtle-2">{label}</span>
188
199
  <Input
189
- type="number"
190
200
  aria-label={`mutable ${ariaLabel}`}
191
- class="max-w-24 min-w-0 flex-1 rounded border px-1 py-0.5 text-xs"
192
201
  {value}
193
202
  on:input={(event) => onInput((event.target as HTMLInputElement).value)}
194
203
  />
@@ -219,23 +228,31 @@
219
228
  </Select>
220
229
  {/snippet}
221
230
 
222
- {#if object}
231
+ {#if entity}
232
+ {@const ParentFrame = showEditFrameOptions ? DropDownField : ImmutableField}
233
+ {@const ScalarAttribute = showEditFrameOptions ? MutableField : ImmutableField}
234
+
223
235
  <div
224
- class="border-medium bg-extralight absolute top-0 right-0 z-1000 m-2 {showEditFrameOptions
236
+ class="border-medium bg-extralight absolute top-0 right-0 z-10 m-2 {showEditFrameOptions
225
237
  ? 'w-80'
226
238
  : 'w-60'} border p-2 text-xs"
227
- style:transform="translate({draggable.current.x}px, {draggable.current.y}px)"
239
+ use:draggable={{
240
+ bounds: 'body',
241
+ handle: dragElement,
242
+ defaultPosition: dragPosition.current,
243
+ onDragEnd(data) {
244
+ dragPosition.current = { x: data.offsetX, y: data.offsetY }
245
+ },
246
+ }}
228
247
  {...rest}
229
248
  >
230
249
  <div class="flex items-center justify-between gap-2 pb-2">
231
250
  <div class="flex items-center gap-1">
232
- <button
233
- onmousedown={draggable.onDragStart}
234
- onmouseup={draggable.onDragEnd}
235
- >
251
+ <button bind:this={dragElement}>
236
252
  <Icon name="drag" />
237
253
  </button>
238
- {object.name}
254
+ <strong>{name.current}</strong>
255
+ <span class="text-subtle-2">{resourceName?.subtype}</span>
239
256
  </div>
240
257
  </div>
241
258
 
@@ -263,302 +280,284 @@
263
280
  </h3>
264
281
 
265
282
  <div class="flex flex-col gap-2.5">
266
- {#if worldPosition}
267
- <div>
268
- <strong class="font-semibold">world position</strong>
269
- <span class="text-subtle-2">(m)</span>
270
-
271
- <div class="flex gap-3">
272
- <div>
273
- <span class="text-subtle-2">x</span>
274
- {worldPosition.x.toFixed(2)}
275
- </div>
276
- <div>
277
- <span class="text-subtle-2">y</span>
278
- {worldPosition.y.toFixed(2)}
279
- </div>
280
- <div>
281
- <span class="text-subtle-2">z</span>
282
- {worldPosition.z.toFixed(2)}
283
- </div>
283
+ <div>
284
+ <strong class="font-semibold">world position</strong>
285
+ <span class="text-subtle-2">(mm)</span>
286
+
287
+ <div class="flex gap-3">
288
+ <div>
289
+ <span class="text-subtle-2">x</span>
290
+ {(worldPosition.x * 1000).toFixed(2)}
291
+ </div>
292
+ <div>
293
+ <span class="text-subtle-2">y</span>
294
+ {(worldPosition.y * 1000).toFixed(2)}
295
+ </div>
296
+ <div>
297
+ <span class="text-subtle-2">z</span>
298
+ {(worldPosition.z * 1000).toFixed(2)}
284
299
  </div>
285
300
  </div>
286
- {/if}
301
+ </div>
287
302
 
288
- {#if worldOrientation}
289
- <div>
290
- <strong class="font-semibold">world orientation</strong>
291
- <span class="text-subtle-2">(deg)</span>
292
- <div class="flex gap-3">
293
- <div>
294
- <span class="text-subtle-2">x</span>
295
- {worldOrientation.x.toFixed(2)}
296
- </div>
297
- <div>
298
- <span class="text-subtle-2">y</span>
299
- {worldOrientation.y.toFixed(2)}
300
- </div>
301
- <div>
302
- <span class="text-subtle-2">z</span>
303
- {worldOrientation.z.toFixed(2)}
304
- </div>
305
- <div>
306
- <span class="text-subtle-2">th</span>
307
- {MathUtils.radToDeg(worldOrientation.th).toFixed(2)}
308
- </div>
303
+ <div>
304
+ <strong class="font-semibold">world orientation</strong>
305
+ <span class="text-subtle-2">(deg)</span>
306
+ <div class="flex gap-3">
307
+ <div>
308
+ <span class="text-subtle-2">x</span>
309
+ {worldOrientation.x.toFixed(2)}
310
+ </div>
311
+ <div>
312
+ <span class="text-subtle-2">y</span>
313
+ {worldOrientation.y.toFixed(2)}
314
+ </div>
315
+ <div>
316
+ <span class="text-subtle-2">z</span>
317
+ {worldOrientation.z.toFixed(2)}
318
+ </div>
319
+ <div>
320
+ <span class="text-subtle-2">th</span>
321
+ {MathUtils.radToDeg(worldOrientation.th).toFixed(2)}
309
322
  </div>
310
323
  </div>
311
- {/if}
324
+ </div>
312
325
 
313
- <WeblabActive experiment={WEBLABS_EXPERIMENTS.MOTION_TOOLS_EDIT_FRAME}>
314
- {@const ParentFrame = showEditFrameOptions ? DropDownField : ImmutableField}
326
+ <div>
327
+ <strong class="font-semibold">parent frame</strong>
328
+ <div class="mt-0.5 flex gap-3">
329
+ {@render ParentFrame({
330
+ ariaLabel: 'parent frame name',
331
+ value: parent.current ?? 'world',
332
+ options: frames.getParentFrameOptions(name.current ?? ''),
333
+ onChange: (value) => {
334
+ detailConfigUpdater.setFrameParent(entity, value)
335
+ },
336
+ })}
337
+ </div>
338
+ </div>
315
339
 
340
+ {#if localPose.current}
316
341
  <div>
317
- <strong class="font-semibold">parent frame</strong>
318
- <div class="flex gap-3">
319
- {@render ParentFrame({
320
- ariaLabel: 'parent frame name',
321
- value: referenceFrame,
322
- options: referenceFrameOptions,
323
- onChange: (value) => detailConfigUpdater.setFrameParent(value),
342
+ <strong class="font-semibold">local position</strong>
343
+ <span class="text-subtle-2">(mm)</span>
344
+
345
+ <div class="mt-0.5 flex gap-3">
346
+ {@render ScalarAttribute({
347
+ label: 'x',
348
+ ariaLabel: 'local position x coordinate',
349
+ value: localPose.current.x,
350
+ onInput: (value) => {
351
+ if (isIntermediateInput(value)) return
352
+ detailConfigUpdater.updateLocalPosition(entity, { x: Number.parseFloat(value) })
353
+ },
354
+ })}
355
+ {@render ScalarAttribute({
356
+ label: 'y',
357
+ ariaLabel: 'local position y coordinate',
358
+ value: localPose.current.y,
359
+ onInput: (value) => {
360
+ if (isIntermediateInput(value)) return
361
+ detailConfigUpdater.updateLocalPosition(entity, { y: Number.parseFloat(value) })
362
+ },
363
+ })}
364
+ {@render ScalarAttribute({
365
+ label: 'z',
366
+ ariaLabel: 'local position z coordinate',
367
+ value: localPose.current.z,
368
+ onInput: (value) => {
369
+ if (isIntermediateInput(value)) return
370
+ detailConfigUpdater.updateLocalPosition(entity, { z: Number.parseFloat(value) })
371
+ },
324
372
  })}
325
373
  </div>
326
374
  </div>
327
375
 
328
- {#if localPose}
329
- {@const PoseAttribute = showEditFrameOptions ? MutableField : ImmutableField}
330
- <div>
331
- <strong class="font-semibold">local position</strong>
332
- <span class="text-subtle-2">(m)</span>
333
-
334
- <div class="flex gap-3">
335
- {@render PoseAttribute({
336
- label: 'x',
337
- ariaLabel: 'local position x coordinate',
338
- value: localPose.x.toFixed(2),
339
- onInput: (value) =>
340
- detailConfigUpdater.updateLocalPosition({ x: parseFloat(value) }),
341
- })}
342
- {@render PoseAttribute({
343
- label: 'y',
344
- ariaLabel: 'local position y coordinate',
345
- value: localPose.y.toFixed(2),
346
- onInput: (value) =>
347
- detailConfigUpdater.updateLocalPosition({ y: parseFloat(value) }),
348
- })}
349
- {@render PoseAttribute({
350
- label: 'z',
351
- ariaLabel: 'local position z coordinate',
352
- value: localPose.z.toFixed(2),
353
- onInput: (value) =>
354
- detailConfigUpdater.updateLocalPosition({ z: parseFloat(value) }),
355
- })}
356
- </div>
376
+ <div>
377
+ <strong class="font-semibold">local orientation</strong>
378
+ <span class="text-subtle-2">(deg)</span>
379
+ <div class="flex {showEditFrameOptions ? 'gap-2' : 'gap-3'} mt-0.5">
380
+ {@render ScalarAttribute({
381
+ label: 'x',
382
+ ariaLabel: 'local orientation x coordinate',
383
+ value: localPose.current?.oX,
384
+ onInput: (value) => {
385
+ if (isIntermediateInput(value)) return
386
+ detailConfigUpdater.updateLocalOrientation(entity, { oX: Number.parseFloat(value) })
387
+ },
388
+ })}
389
+ {@render ScalarAttribute({
390
+ label: 'y',
391
+ ariaLabel: 'local orientation y coordinate',
392
+ value: localPose.current?.oY,
393
+ onInput: (value) => {
394
+ if (isIntermediateInput(value)) return
395
+ detailConfigUpdater.updateLocalOrientation(entity, { oY: Number.parseFloat(value) })
396
+ },
397
+ })}
398
+ {@render ScalarAttribute({
399
+ label: 'z',
400
+ ariaLabel: 'local orientation z coordinate',
401
+ value: localPose.current?.oZ,
402
+ onInput: (value) => {
403
+ if (isIntermediateInput(value)) return
404
+ detailConfigUpdater.updateLocalOrientation(entity, { oZ: Number.parseFloat(value) })
405
+ },
406
+ })}
407
+ {@render ScalarAttribute({
408
+ label: 'th',
409
+ ariaLabel: 'local orientation theta degrees',
410
+ value: localPose.current?.theta,
411
+ onInput: (value) => {
412
+ if (isIntermediateInput(value)) return
413
+ detailConfigUpdater.updateLocalOrientation(entity, {
414
+ theta: Number.parseFloat(value),
415
+ })
416
+ },
417
+ })}
357
418
  </div>
419
+ </div>
420
+ {/if}
358
421
 
359
- <div>
360
- <strong class="font-semibold">local orientation</strong>
361
- <span class="text-subtle-2">(deg)</span>
362
- <div class="flex {showEditFrameOptions ? 'gap-2' : 'gap-3'}">
363
- {@render PoseAttribute({
364
- label: 'x',
365
- ariaLabel: 'local orientation x coordinate',
366
- value: localPose.oX.toFixed(2),
367
- onInput: (value) =>
368
- detailConfigUpdater.updateLocalOrientation({ oX: parseFloat(value) }),
369
- })}
370
- {@render PoseAttribute({
371
- label: 'y',
372
- ariaLabel: 'local orientation y coordinate',
373
- value: localPose.oY.toFixed(2),
374
- onInput: (value) =>
375
- detailConfigUpdater.updateLocalOrientation({ oY: parseFloat(value) }),
376
- })}
377
- {@render PoseAttribute({
378
- label: 'z',
379
- ariaLabel: 'local orientation z coordinate',
380
- value: localPose.oZ.toFixed(2),
381
- onInput: (value) =>
382
- detailConfigUpdater.updateLocalOrientation({ oZ: parseFloat(value) }),
383
- })}
384
- {@render PoseAttribute({
385
- label: 'th',
386
- ariaLabel: 'local orientation theta degrees',
387
- value: localPose.theta.toFixed(2),
388
- onInput: (value) =>
389
- detailConfigUpdater.updateLocalOrientation({ theta: parseFloat(value) }),
390
- })}
391
- </div>
422
+ {#if showEditFrameOptions}
423
+ <div>
424
+ <strong class="font-semibold">geometry</strong>
425
+ <div class="mt-0.5 grid grid-cols-4 gap-1">
426
+ <Button
427
+ variant={geometryType === 'none' ? 'dark' : 'primary'}
428
+ class="h-6 px-2 py-1 text-xs"
429
+ onclick={() => setGeometryType('none')}
430
+ >
431
+ None
432
+ </Button>
433
+ <Button
434
+ variant={geometryType === 'box' ? 'dark' : 'primary'}
435
+ class="h-6 px-2 py-1 text-xs"
436
+ onclick={() => setGeometryType('box')}
437
+ >
438
+ Box
439
+ </Button>
440
+ <Button
441
+ variant={geometryType === 'sphere' ? 'dark' : 'primary'}
442
+ class="h-6 px-2 py-1 text-xs"
443
+ onclick={() => setGeometryType('sphere')}
444
+ >
445
+ Sphere
446
+ </Button>
447
+ <Button
448
+ variant={geometryType === 'capsule' ? 'dark' : 'primary'}
449
+ class="h-6 px-2 py-1 text-xs"
450
+ onclick={() => setGeometryType('capsule')}
451
+ >
452
+ Capsule
453
+ </Button>
392
454
  </div>
393
- {/if}
455
+ </div>
456
+ {/if}
394
457
 
395
- {#if showEditFrameOptions}
396
- <div>
397
- <strong class="font-semibold">geometry</strong>
398
- <div class="grid grid-cols-4 gap-1">
399
- <Button
400
- variant={geometryType === 'none' ? 'dark' : 'primary'}
401
- class="h-6 px-2 py-1 text-xs"
402
- onclick={() => setGeometryType('none')}>None</Button
403
- >
404
- <Button
405
- variant={geometryType === 'box' ? 'dark' : 'primary'}
406
- class="h-6 px-2 py-1 text-xs"
407
- onclick={() => setGeometryType('box')}>Box</Button
408
- >
409
- <Button
410
- variant={geometryType === 'sphere' ? 'dark' : 'primary'}
411
- class="h-6 px-2 py-1 text-xs"
412
- onclick={() => setGeometryType('sphere')}>Sphere</Button
413
- >
414
- <Button
415
- variant={geometryType === 'capsule' ? 'dark' : 'primary'}
416
- class="h-6 px-2 py-1 text-xs"
417
- onclick={() => setGeometryType('capsule')}>Capsule</Button
418
- >
419
- </div>
458
+ {#if box.current}
459
+ <div>
460
+ <strong class="font-semibold"> dimensions </strong>
461
+ <span class="text-subtle-2">(box) (mm)</span>
462
+ <div class="mt-0.5 flex items-center gap-2">
463
+ {@render ScalarAttribute({
464
+ label: 'x',
465
+ ariaLabel: 'box dimensions x value input',
466
+ value: box.current.x,
467
+ onInput: (value) => {
468
+ if (isIntermediateInput(value)) return
469
+ detailConfigUpdater.updateGeometry(entity, {
470
+ type: 'box',
471
+ x: Number.parseFloat(value),
472
+ })
473
+ },
474
+ })}
475
+ {@render ScalarAttribute({
476
+ label: 'y',
477
+ ariaLabel: 'box dimensions y value input',
478
+ value: box.current.y,
479
+ onInput: (value) => {
480
+ if (isIntermediateInput(value)) return
481
+ detailConfigUpdater.updateGeometry(entity, {
482
+ type: 'box',
483
+ y: Number.parseFloat(value),
484
+ })
485
+ },
486
+ })}
487
+ {@render ScalarAttribute({
488
+ label: 'z',
489
+ ariaLabel: 'box dimensions z value input',
490
+ value: box.current.z,
491
+ onInput: (value) => {
492
+ if (isIntermediateInput(value)) return
493
+ detailConfigUpdater.updateGeometry(entity, {
494
+ type: 'box',
495
+ z: Number.parseFloat(value),
496
+ })
497
+ },
498
+ })}
420
499
  </div>
421
- {/if}
422
- {#if geometryType !== 'none'}
423
- {@const GeometryAttribute = showEditFrameOptions ? MutableField : ImmutableField}
424
- {#if geometryType === 'box'}
425
- {@const { dimsMm } = object?.geometry?.geometryType.value as {
426
- dimsMm: { x: number; y: number; z: number }
427
- }}
428
- <div>
429
- <strong class="font-semibold">dimensions (box)</strong>
430
- <div class="flex items-center gap-2">
431
- {@render GeometryAttribute({
432
- label: 'x',
433
- ariaLabel: 'box dimensions x value input',
434
- value: dimsMm?.x ? dimsMm.x.toFixed(2) : '-',
435
- onInput: (value) =>
436
- detailConfigUpdater.updateGeometry({ type: 'box', x: parseFloat(value) }),
437
- })}
438
- {@render GeometryAttribute({
439
- label: 'y',
440
- ariaLabel: 'box dimensions y value input',
441
- value: dimsMm?.y ? dimsMm.y.toFixed(2) : '-',
442
- onInput: (value) =>
443
- detailConfigUpdater.updateGeometry({ type: 'box', y: parseFloat(value) }),
444
- })}
445
- {@render GeometryAttribute({
446
- label: 'z',
447
- ariaLabel: 'box dimensions z value input',
448
- value: dimsMm?.z ? dimsMm.z.toFixed(2) : '-',
449
- onInput: (value) =>
450
- detailConfigUpdater.updateGeometry({ type: 'box', z: parseFloat(value) }),
451
- })}
452
- </div>
453
- </div>
454
- {/if}
455
- {#if geometryType === 'capsule'}
456
- {@const { radiusMm, lengthMm } = object?.geometry?.geometryType.value as {
457
- radiusMm: number
458
- lengthMm: number
459
- }}
460
- <div>
461
- <strong class="font-semibold">dimensions (capsule)</strong>
462
- <div class="flex items-center gap-2">
463
- {@render GeometryAttribute({
464
- label: 'r',
465
- ariaLabel: 'capsule dimensions radius value input',
466
- value: radiusMm ? radiusMm.toFixed(2) : '-',
467
- onInput: (value) =>
468
- detailConfigUpdater.updateGeometry({ type: 'capsule', r: parseFloat(value) }),
469
- })}
470
- {@render GeometryAttribute({
471
- label: 'l',
472
- ariaLabel: 'capsule dimensions length value input',
473
- value: lengthMm ? lengthMm.toFixed(2) : '-',
474
- onInput: (value) =>
475
- detailConfigUpdater.updateGeometry({ type: 'capsule', l: parseFloat(value) }),
476
- })}
477
- </div>
478
- </div>
479
- {/if}
480
- {#if geometryType === 'sphere'}
481
- {@const { radiusMm } = object?.geometry?.geometryType.value as { radiusMm: number }}
482
- <div>
483
- <strong class="font-semibold">dimensions (sphere)</strong>
484
- <div class="flex items-center gap-2">
485
- {@render GeometryAttribute({
486
- label: 'r',
487
- ariaLabel: 'sphere dimensions radius value',
488
- value: radiusMm ? radiusMm.toFixed(2) : '-',
489
- onInput: (value) =>
490
- detailConfigUpdater.updateGeometry({ type: 'sphere', r: parseFloat(value) }),
491
- })}
492
- </div>
493
- </div>
494
- {/if}
495
- {/if}
496
- </WeblabActive>
497
-
498
- <WeblabActive
499
- experiment={WEBLABS_EXPERIMENTS.MOTION_TOOLS_EDIT_FRAME}
500
- renderIfActive={false}
501
- >
502
- {#if object.geometry}
503
- {#if object.geometry.geometryType.case === 'box'}
504
- {@const { dimsMm } = object.geometry.geometryType.value}
505
- <div>
506
- <strong class="font-semibold">dimensions (box)</strong>
507
- <div class="flex gap-3">
508
- <div>
509
- <span class="text-subtle-2">x</span>
510
- {dimsMm?.x ? dimsMm.x.toFixed(2) : '-'}
511
- </div>
512
- <div>
513
- <span class="text-subtle-2">y</span>
514
- {dimsMm?.y ? dimsMm.y.toFixed(2) : '-'}
515
- </div>
516
- <div>
517
- <span class="text-subtle-2">z</span>
518
- {dimsMm?.z ? dimsMm.z.toFixed(2) : '-'}
519
- </div>
520
- </div>
521
- </div>
522
- {:else if object.geometry.geometryType.case === 'capsule'}
523
- {@const { value } = object.geometry.geometryType}
524
- <div>
525
- <strong class="font-semibold">dimensions (capsule)</strong>
526
- <div class="flex gap-3">
527
- <div>
528
- <span class="text-subtle-2">r</span>
529
- {value.radiusMm ? value.radiusMm.toFixed(2) : '-'}
530
- </div>
531
- <div>
532
- <span class="text-subtle-2">l</span>
533
- {value.lengthMm ? value.lengthMm.toFixed(2) : '-'}
534
- </div>
535
- </div>
536
- </div>
537
- {:else if object.geometry.geometryType.case === 'sphere'}
538
- <div class="flex justify-between">
539
- <div>
540
- <strong class="font-semibold">dimensions (sphere)</strong>
541
- <div class="flex gap-3">
542
- <div>
543
- <span class="text-subtle-2">r</span>
544
- {object.geometry.geometryType.value.radiusMm.toFixed(2)}
545
- </div>
546
- </div>
547
- </div>
548
- </div>
549
- {/if}
550
- {/if}
551
- </WeblabActive>
500
+ </div>
501
+ {:else if capsule.current}
502
+ <div>
503
+ <strong class="font-semibold">dimensions</strong>
504
+ <span class="text-subtle-2">(capsule) (mm)</span>
505
+ <div class="mt-0.5 flex items-center gap-2">
506
+ {@render ScalarAttribute({
507
+ label: 'r',
508
+ ariaLabel: 'capsule dimensions radius value input',
509
+ value: capsule.current.r,
510
+ onInput: (value) => {
511
+ if (isIntermediateInput(value)) return
512
+ detailConfigUpdater.updateGeometry(entity, {
513
+ type: 'capsule',
514
+ r: Number.parseFloat(value),
515
+ })
516
+ },
517
+ })}
518
+ {@render ScalarAttribute({
519
+ label: 'l',
520
+ ariaLabel: 'capsule dimensions length value input',
521
+ value: capsule.current.l,
522
+ onInput: (value) => {
523
+ if (isIntermediateInput(value)) return
524
+ detailConfigUpdater.updateGeometry(entity, {
525
+ type: 'capsule',
526
+ l: Number.parseFloat(value),
527
+ })
528
+ },
529
+ })}
530
+ </div>
531
+ </div>
532
+ {:else if sphere.current}
533
+ <div>
534
+ <strong class="font-semibold">dimensions (sphere)</strong>
535
+ <div class="flex items-center gap-2">
536
+ {@render ScalarAttribute({
537
+ label: 'r',
538
+ ariaLabel: 'sphere dimensions radius value',
539
+ value: sphere.current.r,
540
+ onInput: (value) => {
541
+ if (isIntermediateInput(value)) return
542
+ detailConfigUpdater.updateGeometry(entity, {
543
+ type: 'sphere',
544
+ r: Number.parseFloat(value),
545
+ })
546
+ },
547
+ })}
548
+ </div>
549
+ </div>
550
+ {/if}
552
551
  </div>
553
552
 
554
553
  <h3 class="text-subtle-2 pt-3 pb-2">Actions</h3>
555
554
 
556
- {#if focused.current}
555
+ {#if focusedEntity.current}
557
556
  <Button
558
557
  class="w-full"
559
558
  icon="arrow-left"
560
559
  variant="dark"
561
- onclick={() => focused.set()}
560
+ onclick={() => focusedEntity.set()}
562
561
  >
563
562
  Exit object view
564
563
  </Button>
@@ -566,20 +565,20 @@
566
565
  <Button
567
566
  class="w-full"
568
567
  icon="image-filter-center-focus"
569
- onclick={() => focused.set(object.uuid)}
568
+ onclick={() => focusedEntity.set(entity)}
570
569
  >
571
570
  Enter object view
572
571
  </Button>
573
572
  {/if}
574
573
 
575
- <WeblabActive experiment={WEBLABS_EXPERIMENTS.MOTION_TOOLS_EDIT_FRAME}>
576
- {#if showEditFrameOptions && environment.current.isStandalone}
577
- <Button
578
- variant="danger"
579
- class="mt-2 w-full"
580
- onclick={() => detailConfigUpdater.deleteFrame()}>Delete frame</Button
581
- >
582
- {/if}
583
- </WeblabActive>
574
+ {#if showEditFrameOptions && environment.current.isStandalone}
575
+ <Button
576
+ variant="danger"
577
+ class="mt-2 w-full"
578
+ onclick={() => detailConfigUpdater.deleteFrame(entity)}
579
+ >
580
+ Delete frame
581
+ </Button>
582
+ {/if}
584
583
  </div>
585
584
  {/if}