@viamrobotics/motion-tools 1.19.0 → 1.21.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 (33) hide show
  1. package/dist/buf/draw/v1/metadata_pb.d.ts +39 -0
  2. package/dist/buf/draw/v1/metadata_pb.js +55 -0
  3. package/dist/buf/draw/v1/service_connect.d.ts +34 -1
  4. package/dist/buf/draw/v1/service_connect.js +34 -1
  5. package/dist/buf/draw/v1/service_pb.d.ts +136 -0
  6. package/dist/buf/draw/v1/service_pb.js +201 -0
  7. package/dist/components/Entities/Arrows/ArrowGroups.svelte +1 -0
  8. package/dist/components/Entities/Arrows/Arrows.svelte +1 -1
  9. package/dist/components/Entities/Entities.svelte +1 -0
  10. package/dist/components/Entities/Points.svelte +23 -23
  11. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +18 -1
  12. package/dist/components/FileDrop/FileDrop.svelte +8 -1
  13. package/dist/components/PCD.svelte +9 -1
  14. package/dist/components/PCD.svelte.d.ts +2 -0
  15. package/dist/components/SceneProviders.svelte +2 -0
  16. package/dist/components/Snapshot.svelte +12 -7
  17. package/dist/components/overlay/AddRelationship.svelte +25 -3
  18. package/dist/components/overlay/Details.svelte +293 -227
  19. package/dist/draw.d.ts +22 -9
  20. package/dist/draw.js +75 -46
  21. package/dist/ecs/relations.js +1 -1
  22. package/dist/ecs/traits.d.ts +2 -0
  23. package/dist/ecs/traits.js +63 -0
  24. package/dist/hooks/useDrawService.svelte.d.ts +2 -0
  25. package/dist/hooks/useDrawService.svelte.js +139 -20
  26. package/dist/hooks/useRelationships.svelte.d.ts +12 -0
  27. package/dist/hooks/useRelationships.svelte.js +78 -0
  28. package/dist/hooks/useWorldState.svelte.js +10 -4
  29. package/dist/metadata.d.ts +7 -3
  30. package/dist/metadata.js +26 -2
  31. package/dist/snapshot.d.ts +6 -1
  32. package/dist/snapshot.js +10 -5
  33. package/package.json +5 -2
@@ -2,13 +2,21 @@
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()
14
+
15
+ ThemeUtils.setGlobalDefaultTheme({
16
+ ...ThemeUtils.presets.light,
17
+ baseBackgroundColor: '#fbfbfc',
18
+ baseShadowColor: 'transparent',
19
+ })
12
20
  </script>
13
21
 
14
22
  <script lang="ts">
@@ -17,14 +25,30 @@
17
25
 
18
26
  import { draggable } from '@neodrag/svelte'
19
27
  import { isInstanceOf, useTask } from '@threlte/core'
20
- import { Button, Icon, Input, Select, Tooltip } from '@viamrobotics/prime-core'
28
+ import { Button, Icon, Tooltip } from '@viamrobotics/prime-core'
21
29
  import { Check, Copy } from 'lucide-svelte'
30
+ import {
31
+ List,
32
+ type ListChangeEvent,
33
+ Point,
34
+ type PointChangeEvent,
35
+ type PointValue3dObject,
36
+ type PointValue4dObject,
37
+ RotationEuler,
38
+ type RotationEulerChangeEvent,
39
+ type RotationEulerValueObject,
40
+ Slider,
41
+ type SliderChangeEvent,
42
+ TabGroup,
43
+ TabPage,
44
+ } from 'svelte-tweakpane-ui'
22
45
 
23
46
  import AddRelationship from './AddRelationship.svelte'
24
47
  import { relations, traits, useTrait, useWorld } from '../../ecs'
25
48
  import { FrameConfigUpdater } from '../../FrameConfigUpdater.svelte'
26
49
  import { useConfigFrames } from '../../hooks/useConfigFrames.svelte'
27
50
  import { useCameraControls } from '../../hooks/useControls.svelte'
51
+ import { useDrawService } from '../../hooks/useDrawService.svelte'
28
52
  import { useEnvironment } from '../../hooks/useEnvironment.svelte'
29
53
  import { useLinkedEntities } from '../../hooks/useLinked.svelte'
30
54
  import { usePartConfig } from '../../hooks/usePartConfig.svelte'
@@ -44,6 +68,7 @@
44
68
  const { details }: Props = $props()
45
69
 
46
70
  const world = useWorld()
71
+ const drawService = useDrawService()
47
72
  const controls = useCameraControls()
48
73
  const resourceByName = useResourceByName()
49
74
  const configFrames = useConfigFrames()
@@ -83,12 +108,111 @@
83
108
  return 'none'
84
109
  })
85
110
 
111
+ const geometryTypes = ['none', 'box', 'sphere', 'capsule'] as const
112
+ // Writable derived: re-derives from the trait, but TabGroup's bind:selectedIndex
113
+ // can write a transient override that lasts until the trait re-derives.
114
+ let geometryTabIndex = $derived(geometryTypes.indexOf(geometryType))
115
+
116
+ $effect(() => {
117
+ // setGeometryType guards against no-ops, so this is safe to fire on every
118
+ // tab-index change (whether user-initiated or trait-derived).
119
+ setGeometryType(geometryTypes[geometryTabIndex])
120
+ })
121
+
86
122
  let copied = $state(false)
87
123
 
88
124
  let dragElement = $state.raw<HTMLElement>()
89
125
 
126
+ const eulerValue = $derived.by<RotationEulerValueObject>(() => {
127
+ if (!localPose.current) return { x: 0, y: 0, z: 0 }
128
+ ov.set(
129
+ localPose.current.oX,
130
+ localPose.current.oY,
131
+ localPose.current.oZ,
132
+ MathUtils.degToRad(localPose.current.theta)
133
+ )
134
+ ov.toEuler(euler)
135
+ return {
136
+ x: MathUtils.radToDeg(euler.x),
137
+ y: MathUtils.radToDeg(euler.y),
138
+ z: MathUtils.radToDeg(euler.z),
139
+ }
140
+ })
141
+
142
+ const formatTwoDecimals = (value: number) => value.toFixed(2)
143
+
90
144
  const detailConfigUpdater = new FrameConfigUpdater(partConfig.updateFrame, partConfig.deleteFrame)
91
145
 
146
+ const handlePositionChange = (event: PointChangeEvent) => {
147
+ if (event.detail.origin !== 'internal' || !entity) return
148
+ const next = event.detail.value as PointValue3dObject
149
+ detailConfigUpdater.updateLocalPosition(entity, next)
150
+ }
151
+
152
+ const handleOrientationOVChange = (event: PointChangeEvent) => {
153
+ if (event.detail.origin !== 'internal' || !entity) return
154
+ const next = event.detail.value as PointValue4dObject
155
+ detailConfigUpdater.updateLocalOrientation(entity, {
156
+ oX: next.x,
157
+ oY: next.y,
158
+ oZ: next.z,
159
+ theta: next.w,
160
+ })
161
+ }
162
+
163
+ const handleOrientationEulerChange = (event: RotationEulerChangeEvent) => {
164
+ if (event.detail.origin !== 'internal' || !entity) return
165
+ const next = event.detail.value as RotationEulerValueObject
166
+ euler.set(
167
+ MathUtils.degToRad(next.x),
168
+ MathUtils.degToRad(next.y),
169
+ MathUtils.degToRad(next.z),
170
+ 'ZYX'
171
+ )
172
+ quaternion.setFromEuler(euler)
173
+ ov.setFromQuaternion(quaternion)
174
+ detailConfigUpdater.updateLocalOrientation(entity, {
175
+ oX: ov.x,
176
+ oY: ov.y,
177
+ oZ: ov.z,
178
+ theta: MathUtils.radToDeg(ov.th),
179
+ })
180
+ }
181
+
182
+ const handleBoxChange = (event: PointChangeEvent) => {
183
+ if (event.detail.origin !== 'internal' || !entity) return
184
+ const next = event.detail.value as PointValue3dObject
185
+ detailConfigUpdater.updateGeometry(entity, {
186
+ type: 'box',
187
+ x: next.x,
188
+ y: next.y,
189
+ z: next.z,
190
+ })
191
+ }
192
+
193
+ const handleSphereRChange = (event: SliderChangeEvent) => {
194
+ if (event.detail.origin !== 'internal' || !entity) return
195
+ detailConfigUpdater.updateGeometry(entity, { type: 'sphere', r: event.detail.value })
196
+ }
197
+
198
+ const handleCapsuleRChange = (event: SliderChangeEvent) => {
199
+ if (event.detail.origin !== 'internal' || !entity) return
200
+ detailConfigUpdater.updateGeometry(entity, { type: 'capsule', r: event.detail.value })
201
+ }
202
+
203
+ const handleCapsuleLChange = (event: SliderChangeEvent) => {
204
+ if (event.detail.origin !== 'internal' || !entity) return
205
+ detailConfigUpdater.updateGeometry(entity, { type: 'capsule', l: event.detail.value })
206
+ }
207
+
208
+ const handleParentChange = (event: ListChangeEvent) => {
209
+ if (event.detail.origin !== 'internal' || !entity) return
210
+ const value = event.detail.value as string
211
+ if (value === parent.current) return
212
+ traits.setParentTrait(entity, value)
213
+ detailConfigUpdater.setFrameParent(entity, value)
214
+ }
215
+
92
216
  const setGeometryType = (type: 'none' | 'box' | 'sphere' | 'capsule') => {
93
217
  if (type === geometryType) {
94
218
  return
@@ -171,19 +295,6 @@
171
295
  2
172
296
  )
173
297
  }
174
-
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
- }
187
298
  </script>
188
299
 
189
300
  {#snippet ImmutableField({
@@ -207,55 +318,7 @@
207
318
  </div>
208
319
  {/snippet}
209
320
 
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
321
  {#if entity}
256
- {@const ParentFrame = showEditFrameOptions ? DropDownField : ImmutableField}
257
- {@const ScalarAttribute = showEditFrameOptions ? MutableField : ImmutableField}
258
-
259
322
  <div
260
323
  id="details-panel"
261
324
  class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 {showEditFrameOptions
@@ -398,18 +461,22 @@
398
461
 
399
462
  <div>
400
463
  <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>
464
+ {#if showEditFrameOptions}
465
+ <div aria-label="mutable parent frame">
466
+ <List
467
+ options={configFrames.getParentFrameOptions(name.current ?? '') ?? []}
468
+ value={parent.current ?? 'world'}
469
+ on:change={handleParentChange}
470
+ />
471
+ </div>
472
+ {:else}
473
+ <div class="mt-0.5 flex gap-3">
474
+ {@render ImmutableField({
475
+ ariaLabel: 'parent frame name',
476
+ value: parent.current ?? 'world',
477
+ })}
478
+ </div>
479
+ {/if}
413
480
  </div>
414
481
 
415
482
  {#if localPose.current}
@@ -417,159 +484,162 @@
417
484
  <strong class="font-semibold">local position</strong>
418
485
  <span class="text-subtle-2">(mm)</span>
419
486
 
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>
487
+ {#if showEditFrameOptions}
488
+ <div aria-label="mutable local position">
489
+ <Point
490
+ value={{
491
+ x: localPose.current.x,
492
+ y: localPose.current.y,
493
+ z: localPose.current.z,
494
+ }}
495
+ format={formatTwoDecimals}
496
+ on:change={handlePositionChange}
497
+ />
498
+ </div>
499
+ {:else}
500
+ <div class="mt-0.5 flex gap-3">
501
+ {@render ImmutableField({
502
+ label: 'x',
503
+ ariaLabel: 'local position x coordinate',
504
+ value: localPose.current.x,
505
+ })}
506
+ {@render ImmutableField({
507
+ label: 'y',
508
+ ariaLabel: 'local position y coordinate',
509
+ value: localPose.current.y,
510
+ })}
511
+ {@render ImmutableField({
512
+ label: 'z',
513
+ ariaLabel: 'local position z coordinate',
514
+ value: localPose.current.z,
515
+ })}
516
+ </div>
517
+ {/if}
449
518
  </div>
450
519
 
451
520
  <div>
452
521
  <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>
522
+
523
+ {#if showEditFrameOptions}
524
+ <div aria-label="mutable local orientation">
525
+ <TabGroup>
526
+ <TabPage title="OV (deg)">
527
+ <Point
528
+ value={{
529
+ x: localPose.current.oX,
530
+ y: localPose.current.oY,
531
+ z: localPose.current.oZ,
532
+ w: localPose.current.theta,
533
+ }}
534
+ format={formatTwoDecimals}
535
+ on:change={handleOrientationOVChange}
536
+ />
537
+ </TabPage>
538
+ <TabPage title="Euler">
539
+ <RotationEuler
540
+ value={eulerValue}
541
+ unit="deg"
542
+ on:change={handleOrientationEulerChange}
543
+ />
544
+ </TabPage>
545
+ </TabGroup>
546
+ </div>
547
+ {:else}
548
+ <div class="mt-0.5 flex gap-3">
549
+ {@render ImmutableField({
550
+ label: 'x',
551
+ ariaLabel: 'local orientation x coordinate',
552
+ value: localPose.current.oX,
553
+ })}
554
+ {@render ImmutableField({
555
+ label: 'y',
556
+ ariaLabel: 'local orientation y coordinate',
557
+ value: localPose.current.oY,
558
+ })}
559
+ {@render ImmutableField({
560
+ label: 'z',
561
+ ariaLabel: 'local orientation z coordinate',
562
+ value: localPose.current.oZ,
563
+ })}
564
+ {@render ImmutableField({
565
+ label: 'th',
566
+ ariaLabel: 'local orientation theta degrees',
567
+ value: localPose.current.theta,
568
+ })}
569
+ </div>
570
+ {/if}
494
571
  </div>
495
572
  {/if}
496
573
 
497
574
  {#if showEditFrameOptions}
498
575
  <div>
499
576
  <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>
577
+ <span class="text-subtle-2">(mm)</span>
578
+ <div aria-label="mutable geometry">
579
+ <TabGroup bind:selectedIndex={geometryTabIndex}>
580
+ <TabPage title="None" />
581
+ <TabPage title="Box" />
582
+ <TabPage title="Sphere" />
583
+ <TabPage title="Capsule" />
584
+ </TabGroup>
529
585
  </div>
586
+ {#if geometryTabIndex === 1 && box.current}
587
+ <div aria-label="mutable box dimensions">
588
+ <Point
589
+ value={{
590
+ x: box.current.x,
591
+ y: box.current.y,
592
+ z: box.current.z,
593
+ }}
594
+ format={formatTwoDecimals}
595
+ on:change={handleBoxChange}
596
+ />
597
+ </div>
598
+ {:else if geometryTabIndex === 2 && sphere.current}
599
+ <div aria-label="mutable sphere dimensions">
600
+ <Slider
601
+ label="r"
602
+ value={sphere.current.r}
603
+ format={formatTwoDecimals}
604
+ on:change={handleSphereRChange}
605
+ />
606
+ </div>
607
+ {:else if geometryTabIndex === 3 && capsule.current}
608
+ <div aria-label="mutable capsule dimensions">
609
+ <Slider
610
+ label="r"
611
+ value={capsule.current.r}
612
+ format={formatTwoDecimals}
613
+ on:change={handleCapsuleRChange}
614
+ />
615
+ <Slider
616
+ label="l"
617
+ value={capsule.current.l}
618
+ format={formatTwoDecimals}
619
+ on:change={handleCapsuleLChange}
620
+ />
621
+ </div>
622
+ {/if}
530
623
  </div>
531
- {/if}
532
-
533
- {#if box.current}
624
+ {:else if box.current}
534
625
  <div>
535
- <strong class="font-semibold"> dimensions </strong>
626
+ <strong class="font-semibold">dimensions</strong>
536
627
  <span class="text-subtle-2">(box) (mm)</span>
537
628
  <div class="mt-0.5 flex items-center gap-2">
538
- {@render ScalarAttribute({
629
+ {@render ImmutableField({
539
630
  label: 'x',
540
631
  ariaLabel: 'box dimensions x value input',
541
632
  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
633
  })}
550
- {@render ScalarAttribute({
634
+ {@render ImmutableField({
551
635
  label: 'y',
552
636
  ariaLabel: 'box dimensions y value input',
553
637
  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
638
  })}
562
- {@render ScalarAttribute({
639
+ {@render ImmutableField({
563
640
  label: 'z',
564
641
  ariaLabel: 'box dimensions z value input',
565
642
  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
643
  })}
574
644
  </div>
575
645
  </div>
@@ -578,29 +648,15 @@
578
648
  <strong class="font-semibold">dimensions</strong>
579
649
  <span class="text-subtle-2">(capsule) (mm)</span>
580
650
  <div class="mt-0.5 flex items-center gap-2">
581
- {@render ScalarAttribute({
651
+ {@render ImmutableField({
582
652
  label: 'r',
583
653
  ariaLabel: 'capsule dimensions radius value input',
584
654
  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
655
  })}
593
- {@render ScalarAttribute({
656
+ {@render ImmutableField({
594
657
  label: 'l',
595
658
  ariaLabel: 'capsule dimensions length value input',
596
659
  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
660
  })}
605
661
  </div>
606
662
  </div>
@@ -608,17 +664,10 @@
608
664
  <div>
609
665
  <strong class="font-semibold">dimensions (sphere)</strong>
610
666
  <div class="flex items-center gap-2">
611
- {@render ScalarAttribute({
667
+ {@render ImmutableField({
612
668
  label: 'r',
613
669
  ariaLabel: 'sphere dimensions radius value',
614
670
  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
671
  })}
623
672
  </div>
624
673
  </div>
@@ -653,7 +702,13 @@
653
702
  name="trash-can-outline"
654
703
  class="h-6 cursor-pointer px-2 py-1 text-xs text-red-500"
655
704
  onclick={() => {
656
- entity.remove(relations.SubEntityLink(linkedEntity))
705
+ const sourceUuid = entity.get(traits.UUID)
706
+ const targetUuid = linkedEntity.get(traits.UUID)
707
+ if (sourceUuid && targetUuid) {
708
+ void drawService.deleteRelationship(sourceUuid, targetUuid)
709
+ } else {
710
+ entity.remove(relations.SubEntityLink(linkedEntity))
711
+ }
657
712
  }}
658
713
  />
659
714
  </div>
@@ -700,3 +755,14 @@
700
755
  {/if}
701
756
  </div>
702
757
  {/if}
758
+
759
+ <style>
760
+ :global(.tp-tabv_i) {
761
+ display: none;
762
+ }
763
+
764
+ :global(.tp-lblv),
765
+ :global(.tp-tbpv_c) {
766
+ padding-left: 0 !important;
767
+ }
768
+ </style>