@viamrobotics/motion-tools 1.19.1 → 1.22.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 (53) hide show
  1. package/dist/FrameConfigUpdater.svelte.d.ts +0 -1
  2. package/dist/FrameConfigUpdater.svelte.js +6 -24
  3. package/dist/buf/draw/v1/metadata_pb.d.ts +39 -0
  4. package/dist/buf/draw/v1/metadata_pb.js +55 -0
  5. package/dist/buf/draw/v1/service_connect.d.ts +34 -1
  6. package/dist/buf/draw/v1/service_connect.js +34 -1
  7. package/dist/buf/draw/v1/service_pb.d.ts +136 -0
  8. package/dist/buf/draw/v1/service_pb.js +201 -0
  9. package/dist/components/Entities/Arrows/ArrowGroups.svelte +1 -0
  10. package/dist/components/Entities/Arrows/Arrows.svelte +1 -1
  11. package/dist/components/Entities/Points.svelte +23 -23
  12. package/dist/components/Entities/Pose.svelte +18 -13
  13. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +18 -1
  14. package/dist/components/FileDrop/FileDrop.svelte +8 -1
  15. package/dist/components/FileDrop/useFileDrop.svelte.js +16 -2
  16. package/dist/components/PCD.svelte +9 -1
  17. package/dist/components/PCD.svelte.d.ts +2 -0
  18. package/dist/components/PointerMissBox.svelte +1 -1
  19. package/dist/components/Scene.svelte +2 -0
  20. package/dist/components/SceneProviders.svelte +4 -0
  21. package/dist/components/SelectedTransformControls.svelte +227 -0
  22. package/dist/components/SelectedTransformControls.svelte.d.ts +3 -0
  23. package/dist/components/Snapshot.svelte +12 -7
  24. package/dist/components/StaticGeometries.svelte +3 -56
  25. package/dist/components/overlay/AddRelationship.svelte +25 -3
  26. package/dist/components/overlay/Details.svelte +290 -229
  27. package/dist/components/overlay/dashboard/Button.svelte +4 -2
  28. package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
  29. package/dist/components/overlay/dashboard/Dashboard.svelte +43 -33
  30. package/dist/draw.d.ts +22 -9
  31. package/dist/draw.js +71 -41
  32. package/dist/ecs/relations.js +1 -1
  33. package/dist/ecs/traits.d.ts +17 -0
  34. package/dist/ecs/traits.js +9 -0
  35. package/dist/editing/FrameEditSession.d.ts +37 -0
  36. package/dist/editing/FrameEditSession.js +178 -0
  37. package/dist/hooks/useDrawService.svelte.d.ts +2 -0
  38. package/dist/hooks/useDrawService.svelte.js +139 -20
  39. package/dist/hooks/useFrameEditSession.svelte.d.ts +15 -0
  40. package/dist/hooks/useFrameEditSession.svelte.js +36 -0
  41. package/dist/hooks/useFrames.svelte.js +37 -2
  42. package/dist/hooks/usePartConfig.svelte.js +10 -0
  43. package/dist/hooks/useRelationships.svelte.d.ts +12 -0
  44. package/dist/hooks/useRelationships.svelte.js +78 -0
  45. package/dist/hooks/useSettings.svelte.d.ts +1 -2
  46. package/dist/hooks/useSettings.svelte.js +1 -2
  47. package/dist/hooks/useWorldState.svelte.js +10 -4
  48. package/dist/metadata.d.ts +7 -3
  49. package/dist/metadata.js +26 -2
  50. package/dist/snapshot.d.ts +6 -1
  51. package/dist/snapshot.js +10 -5
  52. package/dist/transform.js +13 -0
  53. package/package.json +7 -4
@@ -2,13 +2,15 @@
2
2
  module
3
3
  lang="ts"
4
4
  >
5
- import { BufferAttribute, MathUtils, Quaternion, Vector3 } from 'three'
5
+ import { ThemeUtils } from 'svelte-tweakpane-ui'
6
+ import { BufferAttribute, Euler, MathUtils, Quaternion, Vector3 } from 'three'
6
7
 
7
8
  import { OrientationVector } from '../../three/OrientationVector'
8
9
 
9
10
  const vec3 = new Vector3()
10
11
  const quaternion = new Quaternion()
11
12
  const ov = new OrientationVector()
13
+ const euler = new Euler()
12
14
  </script>
13
15
 
14
16
  <script lang="ts">
@@ -17,14 +19,30 @@
17
19
 
18
20
  import { draggable } from '@neodrag/svelte'
19
21
  import { isInstanceOf, useTask } from '@threlte/core'
20
- import { Button, Icon, Input, Select, Tooltip } from '@viamrobotics/prime-core'
22
+ import { Button, Icon, Tooltip } from '@viamrobotics/prime-core'
21
23
  import { Check, Copy } from 'lucide-svelte'
24
+ import {
25
+ List,
26
+ type ListChangeEvent,
27
+ Point,
28
+ type PointChangeEvent,
29
+ type PointValue3dObject,
30
+ type PointValue4dObject,
31
+ RotationEuler,
32
+ type RotationEulerChangeEvent,
33
+ type RotationEulerValueObject,
34
+ Slider,
35
+ type SliderChangeEvent,
36
+ TabGroup,
37
+ TabPage,
38
+ } from 'svelte-tweakpane-ui'
22
39
 
23
40
  import AddRelationship from './AddRelationship.svelte'
24
41
  import { relations, traits, useTrait, useWorld } from '../../ecs'
25
42
  import { FrameConfigUpdater } from '../../FrameConfigUpdater.svelte'
26
43
  import { useConfigFrames } from '../../hooks/useConfigFrames.svelte'
27
44
  import { useCameraControls } from '../../hooks/useControls.svelte'
45
+ import { useDrawService } from '../../hooks/useDrawService.svelte'
28
46
  import { useEnvironment } from '../../hooks/useEnvironment.svelte'
29
47
  import { useLinkedEntities } from '../../hooks/useLinked.svelte'
30
48
  import { usePartConfig } from '../../hooks/usePartConfig.svelte'
@@ -44,6 +62,7 @@
44
62
  const { details }: Props = $props()
45
63
 
46
64
  const world = useWorld()
65
+ const drawService = useDrawService()
47
66
  const controls = useCameraControls()
48
67
  const resourceByName = useResourceByName()
49
68
  const configFrames = useConfigFrames()
@@ -83,12 +102,109 @@
83
102
  return 'none'
84
103
  })
85
104
 
105
+ const geometryTypes = ['none', 'box', 'sphere', 'capsule'] as const
106
+ // Writable derived: re-derives from the trait, but TabGroup's bind:selectedIndex
107
+ // can write a transient override that lasts until the trait re-derives.
108
+ let geometryTabIndex = $derived(geometryTypes.indexOf(geometryType))
109
+
110
+ $effect(() => {
111
+ // setGeometryType guards against no-ops, so this is safe to fire on every
112
+ // tab-index change (whether user-initiated or trait-derived).
113
+ setGeometryType(geometryTypes[geometryTabIndex])
114
+ })
115
+
86
116
  let copied = $state(false)
87
117
 
88
118
  let dragElement = $state.raw<HTMLElement>()
89
119
 
120
+ const eulerValue = $derived.by<RotationEulerValueObject>(() => {
121
+ if (!localPose.current) return { x: 0, y: 0, z: 0 }
122
+ ov.set(
123
+ localPose.current.oX,
124
+ localPose.current.oY,
125
+ localPose.current.oZ,
126
+ MathUtils.degToRad(localPose.current.theta)
127
+ )
128
+ ov.toEuler(euler)
129
+ return {
130
+ x: MathUtils.radToDeg(euler.x),
131
+ y: MathUtils.radToDeg(euler.y),
132
+ z: MathUtils.radToDeg(euler.z),
133
+ }
134
+ })
135
+
90
136
  const detailConfigUpdater = new FrameConfigUpdater(partConfig.updateFrame, partConfig.deleteFrame)
91
137
 
138
+ const handlePositionChange = (event: PointChangeEvent) => {
139
+ if (event.detail.origin !== 'internal' || !entity) return
140
+ const next = event.detail.value as PointValue3dObject
141
+ detailConfigUpdater.updateLocalPosition(entity, next)
142
+ }
143
+
144
+ const handleOrientationOVChange = (event: PointChangeEvent) => {
145
+ if (event.detail.origin !== 'internal' || !entity) return
146
+ const next = event.detail.value as PointValue4dObject
147
+ detailConfigUpdater.updateLocalOrientation(entity, {
148
+ oX: next.x,
149
+ oY: next.y,
150
+ oZ: next.z,
151
+ theta: next.w,
152
+ })
153
+ }
154
+
155
+ const handleOrientationEulerChange = (event: RotationEulerChangeEvent) => {
156
+ if (event.detail.origin !== 'internal' || !entity) return
157
+ const next = event.detail.value as RotationEulerValueObject
158
+ euler.set(
159
+ MathUtils.degToRad(next.x),
160
+ MathUtils.degToRad(next.y),
161
+ MathUtils.degToRad(next.z),
162
+ 'ZYX'
163
+ )
164
+ quaternion.setFromEuler(euler)
165
+ ov.setFromQuaternion(quaternion)
166
+ detailConfigUpdater.updateLocalOrientation(entity, {
167
+ oX: ov.x,
168
+ oY: ov.y,
169
+ oZ: ov.z,
170
+ theta: MathUtils.radToDeg(ov.th),
171
+ })
172
+ }
173
+
174
+ const handleBoxChange = (event: PointChangeEvent) => {
175
+ if (event.detail.origin !== 'internal' || !entity) return
176
+ const next = event.detail.value as PointValue3dObject
177
+ detailConfigUpdater.updateGeometry(entity, {
178
+ type: 'box',
179
+ x: next.x,
180
+ y: next.y,
181
+ z: next.z,
182
+ })
183
+ }
184
+
185
+ const handleSphereRChange = (event: SliderChangeEvent) => {
186
+ if (event.detail.origin !== 'internal' || !entity) return
187
+ detailConfigUpdater.updateGeometry(entity, { type: 'sphere', r: event.detail.value })
188
+ }
189
+
190
+ const handleCapsuleRChange = (event: SliderChangeEvent) => {
191
+ if (event.detail.origin !== 'internal' || !entity) return
192
+ detailConfigUpdater.updateGeometry(entity, { type: 'capsule', r: event.detail.value })
193
+ }
194
+
195
+ const handleCapsuleLChange = (event: SliderChangeEvent) => {
196
+ if (event.detail.origin !== 'internal' || !entity) return
197
+ detailConfigUpdater.updateGeometry(entity, { type: 'capsule', l: event.detail.value })
198
+ }
199
+
200
+ const handleParentChange = (event: ListChangeEvent) => {
201
+ if (event.detail.origin !== 'internal' || !entity) return
202
+ const value = event.detail.value as string
203
+ if (value === parent.current) return
204
+ traits.setParentTrait(entity, value)
205
+ detailConfigUpdater.setFrameParent(entity, value)
206
+ }
207
+
92
208
  const setGeometryType = (type: 'none' | 'box' | 'sphere' | 'capsule') => {
93
209
  if (type === geometryType) {
94
210
  return
@@ -172,18 +288,11 @@
172
288
  )
173
289
  }
174
290
 
175
- const isIntermediateInput = (input: string) => {
176
- if (input === '0') return false
177
-
178
- return (
179
- input.startsWith('0') ||
180
- input.startsWith('.') ||
181
- input.startsWith('-0') ||
182
- input.startsWith('-.') ||
183
- (input.includes('.') && input.endsWith('0')) ||
184
- input.endsWith('.')
185
- )
186
- }
291
+ ThemeUtils.setGlobalDefaultTheme({
292
+ ...ThemeUtils.presets.light,
293
+ baseBackgroundColor: '#fbfbfc',
294
+ baseShadowColor: 'transparent',
295
+ })
187
296
  </script>
188
297
 
189
298
  {#snippet ImmutableField({
@@ -207,60 +316,10 @@
207
316
  </div>
208
317
  {/snippet}
209
318
 
210
- {#snippet MutableField({
211
- label,
212
- value,
213
- ariaLabel,
214
- onInput,
215
- }: {
216
- label: string
217
- value?: number
218
- ariaLabel: string
219
- onInput: (value: string) => void
220
- })}
221
- <div class="flex items-center gap-1">
222
- <span class="text-subtle-2">{label}</span>
223
- <Input
224
- aria-label={`mutable ${ariaLabel}`}
225
- {value}
226
- on:input={(event) => onInput((event.target as HTMLInputElement).value)}
227
- />
228
- </div>
229
- {/snippet}
230
-
231
- {#snippet DropDownField({
232
- value,
233
- ariaLabel,
234
- options,
235
- onChange,
236
- }: {
237
- value: string
238
- ariaLabel: string
239
- options: string[]
240
- onChange: (value: string) => void
241
- })}
242
- <Select
243
- aria-label={`dropdown ${ariaLabel}`}
244
- {value}
245
- onchange={(event: InputEvent) => {
246
- onChange((event.target as HTMLSelectElement).value)
247
- }}
248
- >
249
- {#each options as option (option)}
250
- <option value={option}>{option}</option>
251
- {/each}
252
- </Select>
253
- {/snippet}
254
-
255
319
  {#if entity}
256
- {@const ParentFrame = showEditFrameOptions ? DropDownField : ImmutableField}
257
- {@const ScalarAttribute = showEditFrameOptions ? MutableField : ImmutableField}
258
-
259
320
  <div
260
321
  id="details-panel"
261
- class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 {showEditFrameOptions
262
- ? 'w-80'
263
- : 'w-60'} border p-2 text-xs dark:text-black"
322
+ class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 w-70 border p-2 text-xs dark:text-black"
264
323
  use:draggable={{
265
324
  bounds: 'body',
266
325
  handle: dragElement,
@@ -398,18 +457,22 @@
398
457
 
399
458
  <div>
400
459
  <strong class="font-semibold">parent frame</strong>
401
- <div class="mt-0.5 flex gap-3">
402
- {@render ParentFrame({
403
- ariaLabel: 'parent frame name',
404
- value: parent.current ?? 'world',
405
- options: configFrames.getParentFrameOptions(name.current ?? ''),
406
- onChange: (value) => {
407
- if (value === parent.current) return
408
- traits.setParentTrait(entity, value)
409
- detailConfigUpdater.setFrameParent(entity, value)
410
- },
411
- })}
412
- </div>
460
+ {#if showEditFrameOptions}
461
+ <div aria-label="mutable parent frame">
462
+ <List
463
+ options={configFrames.getParentFrameOptions(name.current ?? '') ?? []}
464
+ value={parent.current ?? 'world'}
465
+ on:change={handleParentChange}
466
+ />
467
+ </div>
468
+ {:else}
469
+ <div class="mt-0.5 flex gap-3">
470
+ {@render ImmutableField({
471
+ ariaLabel: 'parent frame name',
472
+ value: parent.current ?? 'world',
473
+ })}
474
+ </div>
475
+ {/if}
413
476
  </div>
414
477
 
415
478
  {#if localPose.current}
@@ -417,159 +480,161 @@
417
480
  <strong class="font-semibold">local position</strong>
418
481
  <span class="text-subtle-2">(mm)</span>
419
482
 
420
- <div class="mt-0.5 flex gap-3">
421
- {@render ScalarAttribute({
422
- label: 'x',
423
- ariaLabel: 'local position x coordinate',
424
- value: localPose.current.x,
425
- onInput: (value) => {
426
- if (isIntermediateInput(value)) return
427
- detailConfigUpdater.updateLocalPosition(entity, { x: Number.parseFloat(value) })
428
- },
429
- })}
430
- {@render ScalarAttribute({
431
- label: 'y',
432
- ariaLabel: 'local position y coordinate',
433
- value: localPose.current.y,
434
- onInput: (value) => {
435
- if (isIntermediateInput(value)) return
436
- detailConfigUpdater.updateLocalPosition(entity, { y: Number.parseFloat(value) })
437
- },
438
- })}
439
- {@render ScalarAttribute({
440
- label: 'z',
441
- ariaLabel: 'local position z coordinate',
442
- value: localPose.current.z,
443
- onInput: (value) => {
444
- if (isIntermediateInput(value)) return
445
- detailConfigUpdater.updateLocalPosition(entity, { z: Number.parseFloat(value) })
446
- },
447
- })}
448
- </div>
483
+ {#if showEditFrameOptions}
484
+ <div aria-label="mutable local position">
485
+ <Point
486
+ value={{
487
+ x: localPose.current.x,
488
+ y: localPose.current.y,
489
+ z: localPose.current.z,
490
+ }}
491
+ on:change={handlePositionChange}
492
+ />
493
+ </div>
494
+ {:else}
495
+ <div class="mt-0.5 flex gap-3">
496
+ {@render ImmutableField({
497
+ label: 'x',
498
+ ariaLabel: 'local position x coordinate',
499
+ value: localPose.current.x,
500
+ })}
501
+ {@render ImmutableField({
502
+ label: 'y',
503
+ ariaLabel: 'local position y coordinate',
504
+ value: localPose.current.y,
505
+ })}
506
+ {@render ImmutableField({
507
+ label: 'z',
508
+ ariaLabel: 'local position z coordinate',
509
+ value: localPose.current.z,
510
+ })}
511
+ </div>
512
+ {/if}
449
513
  </div>
450
514
 
451
515
  <div>
452
516
  <strong class="font-semibold">local orientation</strong>
453
- <span class="text-subtle-2">(deg)</span>
454
- <div class="flex {showEditFrameOptions ? 'gap-2' : 'gap-3'} mt-0.5">
455
- {@render ScalarAttribute({
456
- label: 'x',
457
- ariaLabel: 'local orientation x coordinate',
458
- value: localPose.current?.oX,
459
- onInput: (value) => {
460
- if (isIntermediateInput(value)) return
461
- detailConfigUpdater.updateLocalOrientation(entity, { oX: Number.parseFloat(value) })
462
- },
463
- })}
464
- {@render ScalarAttribute({
465
- label: 'y',
466
- ariaLabel: 'local orientation y coordinate',
467
- value: localPose.current?.oY,
468
- onInput: (value) => {
469
- if (isIntermediateInput(value)) return
470
- detailConfigUpdater.updateLocalOrientation(entity, { oY: Number.parseFloat(value) })
471
- },
472
- })}
473
- {@render ScalarAttribute({
474
- label: 'z',
475
- ariaLabel: 'local orientation z coordinate',
476
- value: localPose.current?.oZ,
477
- onInput: (value) => {
478
- if (isIntermediateInput(value)) return
479
- detailConfigUpdater.updateLocalOrientation(entity, { oZ: Number.parseFloat(value) })
480
- },
481
- })}
482
- {@render ScalarAttribute({
483
- label: 'th',
484
- ariaLabel: 'local orientation theta degrees',
485
- value: localPose.current?.theta,
486
- onInput: (value) => {
487
- if (isIntermediateInput(value)) return
488
- detailConfigUpdater.updateLocalOrientation(entity, {
489
- theta: Number.parseFloat(value),
490
- })
491
- },
492
- })}
493
- </div>
517
+
518
+ {#if showEditFrameOptions}
519
+ <div aria-label="mutable local orientation">
520
+ <TabGroup>
521
+ <TabPage title="OV (deg)">
522
+ <Point
523
+ value={{
524
+ x: localPose.current.oX,
525
+ y: localPose.current.oY,
526
+ z: localPose.current.oZ,
527
+ w: localPose.current.theta,
528
+ }}
529
+ on:change={handleOrientationOVChange}
530
+ />
531
+ </TabPage>
532
+ <TabPage title="Euler">
533
+ <RotationEuler
534
+ value={eulerValue}
535
+ unit="deg"
536
+ on:change={handleOrientationEulerChange}
537
+ />
538
+ </TabPage>
539
+ </TabGroup>
540
+ </div>
541
+ {:else}
542
+ <div class="mt-0.5 flex gap-3">
543
+ {@render ImmutableField({
544
+ label: 'x',
545
+ ariaLabel: 'local orientation x coordinate',
546
+ value: localPose.current.oX,
547
+ })}
548
+ {@render ImmutableField({
549
+ label: 'y',
550
+ ariaLabel: 'local orientation y coordinate',
551
+ value: localPose.current.oY,
552
+ })}
553
+ {@render ImmutableField({
554
+ label: 'z',
555
+ ariaLabel: 'local orientation z coordinate',
556
+ value: localPose.current.oZ,
557
+ })}
558
+ {@render ImmutableField({
559
+ label: 'th',
560
+ ariaLabel: 'local orientation theta degrees',
561
+ value: localPose.current.theta,
562
+ })}
563
+ </div>
564
+ {/if}
494
565
  </div>
495
566
  {/if}
496
567
 
497
568
  {#if showEditFrameOptions}
498
569
  <div>
499
570
  <strong class="font-semibold">geometry</strong>
500
- <div class="mt-0.5 grid grid-cols-4 gap-1">
501
- <Button
502
- variant={geometryType === 'none' ? 'dark' : 'primary'}
503
- class="h-6 px-2 py-1 text-xs"
504
- onclick={() => setGeometryType('none')}
505
- >
506
- None
507
- </Button>
508
- <Button
509
- variant={geometryType === 'box' ? 'dark' : 'primary'}
510
- class="h-6 px-2 py-1 text-xs"
511
- onclick={() => setGeometryType('box')}
512
- >
513
- Box
514
- </Button>
515
- <Button
516
- variant={geometryType === 'sphere' ? 'dark' : 'primary'}
517
- class="h-6 px-2 py-1 text-xs"
518
- onclick={() => setGeometryType('sphere')}
519
- >
520
- Sphere
521
- </Button>
522
- <Button
523
- variant={geometryType === 'capsule' ? 'dark' : 'primary'}
524
- class="h-6 px-2 py-1 text-xs"
525
- onclick={() => setGeometryType('capsule')}
526
- >
527
- Capsule
528
- </Button>
571
+ <span class="text-subtle-2">(mm)</span>
572
+ <div aria-label="mutable geometry">
573
+ <TabGroup bind:selectedIndex={geometryTabIndex}>
574
+ <TabPage title="None" />
575
+ <TabPage title="Box">
576
+ {#if box.current}
577
+ <div aria-label="mutable box dimensions">
578
+ <Point
579
+ value={{
580
+ x: box.current.x,
581
+ y: box.current.y,
582
+ z: box.current.z,
583
+ }}
584
+ on:change={handleBoxChange}
585
+ />
586
+ </div>
587
+ {/if}
588
+ </TabPage>
589
+ <TabPage title="Sphere">
590
+ {#if sphere.current}
591
+ <div aria-label="mutable sphere dimensions">
592
+ <Slider
593
+ label="r"
594
+ value={sphere.current.r}
595
+ on:change={handleSphereRChange}
596
+ />
597
+ </div>
598
+ {/if}
599
+ </TabPage>
600
+ <TabPage title="Capsule">
601
+ {#if capsule.current}
602
+ <div aria-label="mutable capsule dimensions">
603
+ <Slider
604
+ label="r"
605
+ value={capsule.current.r}
606
+ on:change={handleCapsuleRChange}
607
+ />
608
+ <Slider
609
+ label="l"
610
+ value={capsule.current.l}
611
+ on:change={handleCapsuleLChange}
612
+ />
613
+ </div>
614
+ {/if}
615
+ </TabPage>
616
+ </TabGroup>
529
617
  </div>
530
618
  </div>
531
- {/if}
532
-
533
- {#if box.current}
619
+ {:else if box.current}
534
620
  <div>
535
- <strong class="font-semibold"> dimensions </strong>
621
+ <strong class="font-semibold">dimensions</strong>
536
622
  <span class="text-subtle-2">(box) (mm)</span>
537
623
  <div class="mt-0.5 flex items-center gap-2">
538
- {@render ScalarAttribute({
624
+ {@render ImmutableField({
539
625
  label: 'x',
540
626
  ariaLabel: 'box dimensions x value input',
541
627
  value: box.current.x,
542
- onInput: (value) => {
543
- if (isIntermediateInput(value)) return
544
- detailConfigUpdater.updateGeometry(entity, {
545
- type: 'box',
546
- x: Number.parseFloat(value),
547
- })
548
- },
549
628
  })}
550
- {@render ScalarAttribute({
629
+ {@render ImmutableField({
551
630
  label: 'y',
552
631
  ariaLabel: 'box dimensions y value input',
553
632
  value: box.current.y,
554
- onInput: (value) => {
555
- if (isIntermediateInput(value)) return
556
- detailConfigUpdater.updateGeometry(entity, {
557
- type: 'box',
558
- y: Number.parseFloat(value),
559
- })
560
- },
561
633
  })}
562
- {@render ScalarAttribute({
634
+ {@render ImmutableField({
563
635
  label: 'z',
564
636
  ariaLabel: 'box dimensions z value input',
565
637
  value: box.current.z,
566
- onInput: (value) => {
567
- if (isIntermediateInput(value)) return
568
- detailConfigUpdater.updateGeometry(entity, {
569
- type: 'box',
570
- z: Number.parseFloat(value),
571
- })
572
- },
573
638
  })}
574
639
  </div>
575
640
  </div>
@@ -578,29 +643,15 @@
578
643
  <strong class="font-semibold">dimensions</strong>
579
644
  <span class="text-subtle-2">(capsule) (mm)</span>
580
645
  <div class="mt-0.5 flex items-center gap-2">
581
- {@render ScalarAttribute({
646
+ {@render ImmutableField({
582
647
  label: 'r',
583
648
  ariaLabel: 'capsule dimensions radius value input',
584
649
  value: capsule.current.r,
585
- onInput: (value) => {
586
- if (isIntermediateInput(value)) return
587
- detailConfigUpdater.updateGeometry(entity, {
588
- type: 'capsule',
589
- r: Number.parseFloat(value),
590
- })
591
- },
592
650
  })}
593
- {@render ScalarAttribute({
651
+ {@render ImmutableField({
594
652
  label: 'l',
595
653
  ariaLabel: 'capsule dimensions length value input',
596
654
  value: capsule.current.l,
597
- onInput: (value) => {
598
- if (isIntermediateInput(value)) return
599
- detailConfigUpdater.updateGeometry(entity, {
600
- type: 'capsule',
601
- l: Number.parseFloat(value),
602
- })
603
- },
604
655
  })}
605
656
  </div>
606
657
  </div>
@@ -608,17 +659,10 @@
608
659
  <div>
609
660
  <strong class="font-semibold">dimensions (sphere)</strong>
610
661
  <div class="flex items-center gap-2">
611
- {@render ScalarAttribute({
662
+ {@render ImmutableField({
612
663
  label: 'r',
613
664
  ariaLabel: 'sphere dimensions radius value',
614
665
  value: sphere.current.r,
615
- onInput: (value) => {
616
- if (isIntermediateInput(value)) return
617
- detailConfigUpdater.updateGeometry(entity, {
618
- type: 'sphere',
619
- r: Number.parseFloat(value),
620
- })
621
- },
622
666
  })}
623
667
  </div>
624
668
  </div>
@@ -653,7 +697,13 @@
653
697
  name="trash-can-outline"
654
698
  class="h-6 cursor-pointer px-2 py-1 text-xs text-red-500"
655
699
  onclick={() => {
656
- entity.remove(relations.SubEntityLink(linkedEntity))
700
+ const sourceUuid = entity.get(traits.UUID)
701
+ const targetUuid = linkedEntity.get(traits.UUID)
702
+ if (sourceUuid && targetUuid) {
703
+ void drawService.deleteRelationship(sourceUuid, targetUuid)
704
+ } else {
705
+ entity.remove(relations.SubEntityLink(linkedEntity))
706
+ }
657
707
  }}
658
708
  />
659
709
  </div>
@@ -700,3 +750,14 @@
700
750
  {/if}
701
751
  </div>
702
752
  {/if}
753
+
754
+ <style>
755
+ :global(.tp-tabv_i) {
756
+ display: none;
757
+ }
758
+
759
+ :global(.tp-lblv),
760
+ :global(.tp-tbpv_c) {
761
+ padding-left: 0 !important;
762
+ }
763
+ </style>
@@ -2,10 +2,10 @@
2
2
  import type { ClassValue, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements'
3
3
 
4
4
  import { Icon, type IconName, Tooltip } from '@viamrobotics/prime-core'
5
- import { Ruler } from 'lucide-svelte'
5
+ import { MousePointer2, Ruler } from 'lucide-svelte'
6
6
 
7
7
  interface Props extends HTMLButtonAttributes {
8
- icon: IconName | 'ruler'
8
+ icon: IconName | 'ruler' | 'mouse-pointer'
9
9
  active?: boolean
10
10
  description: string
11
11
  hotkey?: string
@@ -48,6 +48,8 @@
48
48
  >
49
49
  {#if icon === 'ruler'}
50
50
  <Ruler size="16" />
51
+ {:else if icon === 'mouse-pointer'}
52
+ <MousePointer2 size="16" />
51
53
  {:else}
52
54
  <Icon name={icon} />
53
55
  {/if}
@@ -1,7 +1,7 @@
1
1
  import type { ClassValue, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements';
2
2
  import { type IconName } from '@viamrobotics/prime-core';
3
3
  interface Props extends HTMLButtonAttributes {
4
- icon: IconName | 'ruler';
4
+ icon: IconName | 'ruler' | 'mouse-pointer';
5
5
  active?: boolean;
6
6
  description: string;
7
7
  hotkey?: string;