@viamrobotics/motion-tools 1.33.0 → 1.33.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 (60) hide show
  1. package/dist/components/Entities/Entities.svelte +18 -25
  2. package/dist/components/Entities/Entities.svelte.d.ts +2 -17
  3. package/dist/components/Entities/Label.svelte +79 -13
  4. package/dist/components/Entities/Label.svelte.d.ts +2 -1
  5. package/dist/components/Entities/Labels.svelte +36 -0
  6. package/dist/components/Entities/Labels.svelte.d.ts +3 -0
  7. package/dist/components/Entities/LineDots.svelte +8 -3
  8. package/dist/components/Entities/labelLayout/applyTeleports.d.ts +9 -0
  9. package/dist/components/Entities/labelLayout/applyTeleports.js +39 -0
  10. package/dist/components/Entities/labelLayout/buildNeighborhood.d.ts +8 -0
  11. package/dist/components/Entities/labelLayout/buildNeighborhood.js +26 -0
  12. package/dist/components/Entities/labelLayout/cameraHash.d.ts +8 -0
  13. package/dist/components/Entities/labelLayout/cameraHash.js +25 -0
  14. package/dist/components/Entities/labelLayout/cost.d.ts +44 -0
  15. package/dist/components/Entities/labelLayout/cost.js +126 -0
  16. package/dist/components/Entities/labelLayout/createLabelLayout.d.ts +27 -0
  17. package/dist/components/Entities/labelLayout/createLabelLayout.js +194 -0
  18. package/dist/components/Entities/labelLayout/geometry.d.ts +20 -0
  19. package/dist/components/Entities/labelLayout/geometry.js +151 -0
  20. package/dist/components/Entities/labelLayout/labelStore.svelte.d.ts +17 -0
  21. package/dist/components/Entities/labelLayout/labelStore.svelte.js +28 -0
  22. package/dist/components/Entities/labelLayout/measure.d.ts +13 -0
  23. package/dist/components/Entities/labelLayout/measure.js +42 -0
  24. package/dist/components/Entities/labelLayout/slots.d.ts +11 -0
  25. package/dist/components/Entities/labelLayout/slots.js +47 -0
  26. package/dist/components/Entities/labelLayout/solve.d.ts +11 -0
  27. package/dist/components/Entities/labelLayout/solve.js +93 -0
  28. package/dist/components/Entities/labelLayout/spatialHash.d.ts +15 -0
  29. package/dist/components/Entities/labelLayout/spatialHash.js +53 -0
  30. package/dist/components/Entities/labelLayout/types.d.ts +105 -0
  31. package/dist/components/Entities/labelLayout/types.js +19 -0
  32. package/dist/components/Entities/labelLayout/writeBack.d.ts +20 -0
  33. package/dist/components/Entities/labelLayout/writeBack.js +51 -0
  34. package/dist/components/Scene.svelte +2 -1
  35. package/dist/components/SelectedTransformControls.svelte +65 -47
  36. package/dist/components/overlay/Details.svelte +210 -226
  37. package/dist/components/overlay/Details.svelte.d.ts +1 -1
  38. package/dist/components/overlay/Popover.svelte +6 -4
  39. package/dist/components/overlay/Popover.svelte.d.ts +6 -2
  40. package/dist/components/overlay/dashboard/Button.svelte +7 -2
  41. package/dist/components/overlay/dashboard/Button.svelte.d.ts +2 -1
  42. package/dist/components/overlay/details/AxesHelperDetails.svelte +32 -0
  43. package/dist/components/overlay/details/AxesHelperDetails.svelte.d.ts +7 -0
  44. package/dist/components/overlay/details/ColorDetails.svelte +35 -0
  45. package/dist/components/overlay/details/ColorDetails.svelte.d.ts +7 -0
  46. package/dist/components/overlay/details/GeometryDetails.svelte +104 -0
  47. package/dist/components/overlay/details/GeometryDetails.svelte.d.ts +7 -0
  48. package/dist/components/overlay/details/LineDetails/LineDetails.svelte +196 -0
  49. package/dist/components/overlay/details/LineDetails/LineDetails.svelte.d.ts +7 -0
  50. package/dist/components/overlay/details/LineDetails/linePositions.d.ts +3 -0
  51. package/dist/components/overlay/details/LineDetails/linePositions.js +30 -0
  52. package/dist/components/overlay/details/OpacityDetails.svelte +44 -0
  53. package/dist/components/overlay/details/OpacityDetails.svelte.d.ts +7 -0
  54. package/dist/components/overlay/details/PoseDetails.svelte +189 -0
  55. package/dist/components/overlay/details/PoseDetails.svelte.d.ts +14 -0
  56. package/dist/ecs/traits.d.ts +1 -1
  57. package/dist/ecs/traits.js +1 -1
  58. package/dist/hooks/usePartConfig.svelte.js +8 -6
  59. package/dist/hooks/useWorldState.svelte.js +94 -69
  60. package/package.json +4 -2
@@ -13,13 +13,14 @@
13
13
 
14
14
  <script lang="ts">
15
15
  import type { Pose } from '@viamrobotics/sdk'
16
- import type { Entity } from 'koota'
17
16
  import type { Snippet } from 'svelte'
18
17
  import type { HTMLAttributes } from 'svelte/elements'
19
18
 
20
19
  import { draggable } from '@neodrag/svelte'
21
20
  import { isInstanceOf, useThrelte } from '@threlte/core'
21
+ import { PortalTarget } from '@threlte/extras'
22
22
  import { Button, Icon, Tooltip } from '@viamrobotics/prime-core'
23
+ import { type Entity } from 'koota'
23
24
  import { Check, Copy } from 'lucide-svelte'
24
25
  import {
25
26
  List,
@@ -38,7 +39,9 @@
38
39
  } from 'svelte-tweakpane-ui'
39
40
 
40
41
  import AddRelationship from './AddRelationship.svelte'
41
- import { hierarchy, relations, traits, useParentName, useTrait, useWorld } from '../../ecs'
42
+ import AxesHelperDetails from './details/AxesHelperDetails.svelte'
43
+ import OpacityDetails from './details/OpacityDetails.svelte'
44
+ import { hierarchy, relations, traits, useParentName, useTag, useTrait, useWorld } from '../../ecs'
42
45
  import { FrameConfigUpdater } from '../../FrameConfigUpdater.svelte'
43
46
  import { useConfigFrames } from '../../hooks/useConfigFrames.svelte'
44
47
  import { useCameraControls } from '../../hooks/useControls.svelte'
@@ -58,7 +61,7 @@
58
61
  const { entity, details, ...rest }: Props = $props()
59
62
 
60
63
  const world = useWorld()
61
- const { scene, invalidate } = useThrelte()
64
+ const { scene } = useThrelte()
62
65
  const controls = useCameraControls()
63
66
  const resourceByName = useResourceByName()
64
67
  const configFrames = useConfigFrames()
@@ -82,9 +85,9 @@
82
85
  const removable = useTrait(() => entity, traits.Removable)
83
86
  const points = useTrait(() => entity, traits.Points)
84
87
  const arrows = useTrait(() => entity, traits.Arrows)
85
- const opacity = useTrait(() => entity, traits.Opacity)
86
88
  const framesAPI = useTrait(() => entity, traits.FramesAPI)
87
89
  const geometriesAPI = useTrait(() => entity, traits.GeometriesAPI)
90
+ const customDetails = useTag(() => entity, traits.CustomDetails)
88
91
 
89
92
  const localPose = $derived.by<Pose | undefined>(() => {
90
93
  const source = editedMatrix.current ?? matrix.current
@@ -112,7 +115,7 @@
112
115
  const resourceName = $derived(name.current ? resourceByName.current[name.current] : undefined)
113
116
  const displayType = $derived(isFrameNode ? resourceName?.subtype : isGeometry ? 'geometry' : '')
114
117
 
115
- let geometryType = $derived.by<'box' | 'sphere' | 'capsule' | 'none'>(() => {
118
+ const geometryType = $derived.by(() => {
116
119
  if (box.current) return 'box'
117
120
  if (sphere.current) return 'sphere'
118
121
  if (capsule.current) return 'capsule'
@@ -124,13 +127,22 @@
124
127
  let geometryTabIndex = $derived(geometryTypes.indexOf(geometryType))
125
128
 
126
129
  $effect(() => {
127
- // setGeometryType guards against no-ops, so this is safe to fire on every
128
- // tab-index change (whether user-initiated or trait-derived).
129
- setGeometryType(geometryTypes[geometryTabIndex])
130
+ const nextType = geometryTypes[geometryTabIndex]
131
+
132
+ /**
133
+ * geometryTabIndex is derived from the entity's geometry traits, so on
134
+ * selection (or any trait-driven recompute) nextType already equals
135
+ * geometryType — firing then would call updateFrame, dirtying the part
136
+ * config and resetting the geometry to default dimensions. Only a user
137
+ * tab pick sets geometryTabIndex ahead of the trait, so guard on the two
138
+ * differing to fire solely for user-initiated changes.
139
+ */
140
+ if (nextType === geometryType) return
141
+
142
+ detailConfigUpdater.setGeometryType(entity, nextType)
130
143
  })
131
144
 
132
145
  let copied = $state(false)
133
-
134
146
  let dragElement = $state.raw<HTMLElement>()
135
147
 
136
148
  const eulerValue = $derived.by<RotationEulerValueObject>(() => {
@@ -212,23 +224,6 @@
212
224
  detailConfigUpdater.updateGeometry(entity, { type: 'capsule', l: event.detail.value })
213
225
  }
214
226
 
215
- const opacityValue = $derived(opacity.current ?? 1)
216
-
217
- const handleOpacityChange = (event: SliderChangeEvent) => {
218
- if (event.detail.origin !== 'internal' || !entity) return
219
- const next = event.detail.value
220
- // No trait === fully opaque, so drop the trait when the user returns to 1
221
- // instead of leaving an Opacity(1) entry on the entity.
222
- if (next >= 1) {
223
- entity.remove(traits.Opacity)
224
- } else if (entity.has(traits.Opacity)) {
225
- entity.set(traits.Opacity, next)
226
- } else {
227
- entity.add(traits.Opacity(next))
228
- }
229
- invalidate()
230
- }
231
-
232
227
  const handleParentChange = (event: ListChangeEvent) => {
233
228
  if (event.detail.origin !== 'internal' || !entity) return
234
229
  const value = event.detail.value as string
@@ -237,18 +232,6 @@
237
232
  detailConfigUpdater.setFrameParent(entity, value)
238
233
  }
239
234
 
240
- const setGeometryType = (type: 'none' | 'box' | 'sphere' | 'capsule') => {
241
- if (type === geometryType) {
242
- return
243
- }
244
-
245
- geometryType = type
246
-
247
- if (entity) {
248
- detailConfigUpdater.setGeometryType(entity, type)
249
- }
250
- }
251
-
252
235
  const getCopyClipboardText = () => {
253
236
  return JSON.stringify(
254
237
  {
@@ -327,6 +310,7 @@
327
310
  bind:this={dragElement}
328
311
  >
329
312
  <div class="flex w-[90%] items-center gap-1">
313
+ <PortalTarget id="details-header-icon" />
330
314
  <strong class="overflow-hidden text-nowrap text-ellipsis">{name.current}</strong>
331
315
  <span class="text-subtle-2">{displayType}</span>
332
316
  </div>
@@ -411,6 +395,32 @@
411
395
  <p slot="description">Remove from scene</p>
412
396
  </Tooltip>
413
397
  {/if}
398
+
399
+ <Tooltip
400
+ let:tooltipID
401
+ location="bottom"
402
+ >
403
+ <button
404
+ class="text-subtle-2"
405
+ aria-describedby={tooltipID}
406
+ onclick={async () => {
407
+ try {
408
+ await navigator.clipboard.writeText(getCopyClipboardText())
409
+ } catch {
410
+ // clipboard unavailable (non-secure context or permission denied)
411
+ }
412
+ copied = true
413
+ setTimeout(() => (copied = false), 1000)
414
+ }}
415
+ >
416
+ {#if copied}
417
+ <Check size={14} />
418
+ {:else}
419
+ <Copy size={14} />
420
+ {/if}
421
+ </button>
422
+ <p slot="description">Copy details to clipboard</p>
423
+ </Tooltip>
414
424
  </div>
415
425
 
416
426
  <div class="border-medium -mx-2 w-[100%+0.5rem] border-b"></div>
@@ -425,75 +435,57 @@
425
435
  </p>
426
436
  {/if}
427
437
 
428
- <h3
429
- class="text-subtle-2 flex justify-between py-2"
430
- data-testid="details-header"
431
- >
432
- Details
433
-
434
- <button
435
- onclick={async () => {
436
- navigator.clipboard.writeText(getCopyClipboardText())
437
- copied = true
438
- setTimeout(() => (copied = false), 1000)
439
- }}
440
- >
441
- {#if copied}
442
- <Check size={14} />
443
- {:else}
444
- <Copy size={14} />
445
- {/if}
446
- </button>
447
- </h3>
438
+ <h3 class="text-subtle-2 pt-3 pb-2">Details</h3>
448
439
 
449
440
  <div class="flex flex-col gap-2.5">
450
- <div>
451
- <strong class="font-semibold">world position</strong>
452
- <span class="text-subtle-2">(mm)</span>
441
+ {#if !customDetails.current}
442
+ <div>
443
+ <strong class="font-semibold">world position</strong>
444
+ <span class="text-subtle-2">(mm)</span>
453
445
 
454
- <div class="flex gap-3">
455
- <div>
456
- <span class="text-subtle-2">x</span>
457
- {(worldPose?.x ?? 0).toFixed(2)}
458
- </div>
459
- <div>
460
- <span class="text-subtle-2">y</span>
461
- {(worldPose?.y ?? 0).toFixed(2)}
462
- </div>
463
- <div>
464
- <span class="text-subtle-2">z</span>
465
- {(worldPose?.z ?? 0).toFixed(2)}
446
+ <div class="flex gap-3">
447
+ <div>
448
+ <span class="text-subtle-2">x</span>
449
+ {(worldPose?.x ?? 0).toFixed(2)}
450
+ </div>
451
+ <div>
452
+ <span class="text-subtle-2">y</span>
453
+ {(worldPose?.y ?? 0).toFixed(2)}
454
+ </div>
455
+ <div>
456
+ <span class="text-subtle-2">z</span>
457
+ {(worldPose?.z ?? 0).toFixed(2)}
458
+ </div>
466
459
  </div>
467
460
  </div>
468
- </div>
469
461
 
470
- <div>
471
- <strong class="font-semibold">world orientation</strong>
472
- <span class="text-subtle-2">(deg)</span>
473
- <div class="flex gap-3">
474
- <div>
475
- <span class="text-subtle-2">x</span>
476
- {(worldPose?.oX ?? 0).toFixed(2)}
477
- </div>
478
- <div>
479
- <span class="text-subtle-2">y</span>
480
- {(worldPose?.oY ?? 0).toFixed(2)}
481
- </div>
482
- <div>
483
- <span class="text-subtle-2">z</span>
484
- {(worldPose?.oZ ?? 0).toFixed(2)}
485
- </div>
486
- <div>
487
- <span class="text-subtle-2">th</span>
488
- {(worldPose?.theta ?? 0).toFixed(2)}
462
+ <div>
463
+ <strong class="font-semibold">world orientation</strong>
464
+ <span class="text-subtle-2">(deg)</span>
465
+ <div class="flex gap-3">
466
+ <div>
467
+ <span class="text-subtle-2">x</span>
468
+ {(worldPose?.oX ?? 0).toFixed(2)}
469
+ </div>
470
+ <div>
471
+ <span class="text-subtle-2">y</span>
472
+ {(worldPose?.oY ?? 0).toFixed(2)}
473
+ </div>
474
+ <div>
475
+ <span class="text-subtle-2">z</span>
476
+ {(worldPose?.oZ ?? 0).toFixed(2)}
477
+ </div>
478
+ <div>
479
+ <span class="text-subtle-2">th</span>
480
+ {(worldPose?.theta ?? 0).toFixed(2)}
481
+ </div>
489
482
  </div>
490
483
  </div>
491
- </div>
492
484
 
493
- <div>
494
- <strong class="font-semibold">parent frame</strong>
495
- {#if showEditFrameOptions}
496
- <!--
485
+ <div>
486
+ <strong class="font-semibold">parent frame</strong>
487
+ {#if showEditFrameOptions}
488
+ <!--
497
489
  Remount on entity change. svelte-tweakpane-ui's List runs
498
490
  `listBlade.value = value` on the still-mounted blade before its
499
491
  `options` prop has propagated, so the new entity's parent name
@@ -502,113 +494,114 @@
502
494
  event that handleParentChange interprets as a user pick — silently
503
495
  reparenting the clicked frame.
504
496
  -->
505
- {#key entity}
506
- <div aria-label="mutable parent frame">
507
- <List
508
- options={configFrames.getParentFrameOptions(name.current ?? '') ?? []}
509
- value={parent.current ?? 'world'}
510
- on:change={handleParentChange}
511
- />
512
- </div>
513
- {/key}
514
- {:else}
515
- <div class="mt-0.5 flex gap-3">
516
- {@render ImmutableField({
517
- ariaLabel: 'parent frame name',
518
- value: parent.current ?? 'world',
519
- })}
520
- </div>
521
- {/if}
522
- </div>
523
-
524
- {#if localPose}
525
- <div>
526
- <strong class="font-semibold">local position</strong>
527
- <span class="text-subtle-2">(mm)</span>
528
-
529
- {#if showEditFrameOptions}
530
- <div aria-label="mutable local position">
531
- <Point
532
- value={{
533
- x: localPose.x,
534
- y: localPose.y,
535
- z: localPose.z,
536
- }}
537
- on:change={handlePositionChange}
538
- />
539
- </div>
497
+ {#key entity}
498
+ <div aria-label="mutable parent frame">
499
+ <List
500
+ options={configFrames.getParentFrameOptions(name.current ?? '') ?? []}
501
+ value={parent.current ?? 'world'}
502
+ on:change={handleParentChange}
503
+ />
504
+ </div>
505
+ {/key}
540
506
  {:else}
541
507
  <div class="mt-0.5 flex gap-3">
542
508
  {@render ImmutableField({
543
- label: 'x',
544
- ariaLabel: 'local position x coordinate',
545
- value: localPose.x,
546
- })}
547
- {@render ImmutableField({
548
- label: 'y',
549
- ariaLabel: 'local position y coordinate',
550
- value: localPose.y,
551
- })}
552
- {@render ImmutableField({
553
- label: 'z',
554
- ariaLabel: 'local position z coordinate',
555
- value: localPose.z,
509
+ ariaLabel: 'parent frame name',
510
+ value: parent.current ?? 'world',
556
511
  })}
557
512
  </div>
558
513
  {/if}
559
514
  </div>
560
515
 
561
- <div>
562
- <strong class="font-semibold">local orientation</strong>
516
+ {#if localPose}
517
+ <div>
518
+ <strong class="font-semibold">local position</strong>
519
+ <span class="text-subtle-2">(mm)</span>
520
+
521
+ {#if showEditFrameOptions}
522
+ <div aria-label="mutable local position">
523
+ <Point
524
+ value={{
525
+ x: localPose.x,
526
+ y: localPose.y,
527
+ z: localPose.z,
528
+ }}
529
+ on:change={handlePositionChange}
530
+ />
531
+ </div>
532
+ {:else}
533
+ <div class="mt-0.5 flex gap-3">
534
+ {@render ImmutableField({
535
+ label: 'x',
536
+ ariaLabel: 'local position x coordinate',
537
+ value: localPose.x,
538
+ })}
539
+ {@render ImmutableField({
540
+ label: 'y',
541
+ ariaLabel: 'local position y coordinate',
542
+ value: localPose.y,
543
+ })}
544
+ {@render ImmutableField({
545
+ label: 'z',
546
+ ariaLabel: 'local position z coordinate',
547
+ value: localPose.z,
548
+ })}
549
+ </div>
550
+ {/if}
551
+ </div>
563
552
 
564
- {#if showEditFrameOptions}
565
- <div aria-label="mutable local orientation">
566
- <TabGroup>
567
- <TabPage title="OV (deg)">
568
- <Point
569
- value={{
570
- x: localPose.oX,
571
- y: localPose.oY,
572
- z: localPose.oZ,
573
- w: localPose.theta,
574
- }}
575
- on:change={handleOrientationOVChange}
576
- />
577
- </TabPage>
578
- <TabPage title="Euler">
579
- <RotationEuler
580
- value={eulerValue}
581
- unit="deg"
582
- on:change={handleOrientationEulerChange}
583
- />
584
- </TabPage>
585
- </TabGroup>
586
- </div>
587
- {:else}
588
- <div class="mt-0.5 flex gap-3">
589
- {@render ImmutableField({
590
- label: 'x',
591
- ariaLabel: 'local orientation x coordinate',
592
- value: localPose.oX,
593
- })}
594
- {@render ImmutableField({
595
- label: 'y',
596
- ariaLabel: 'local orientation y coordinate',
597
- value: localPose.oY,
598
- })}
599
- {@render ImmutableField({
600
- label: 'z',
601
- ariaLabel: 'local orientation z coordinate',
602
- value: localPose.oZ,
603
- })}
604
- {@render ImmutableField({
605
- label: 'th',
606
- ariaLabel: 'local orientation theta degrees',
607
- value: localPose.theta,
608
- })}
609
- </div>
610
- {/if}
611
- </div>
553
+ <div>
554
+ <strong class="font-semibold">local orientation</strong>
555
+
556
+ {#if showEditFrameOptions}
557
+ <div aria-label="mutable local orientation">
558
+ <TabGroup>
559
+ <TabPage title="OV (deg)">
560
+ <Point
561
+ value={{
562
+ x: localPose.oX,
563
+ y: localPose.oY,
564
+ z: localPose.oZ,
565
+ w: localPose.theta,
566
+ }}
567
+ on:change={handleOrientationOVChange}
568
+ />
569
+ </TabPage>
570
+ <TabPage title="Euler">
571
+ <RotationEuler
572
+ value={eulerValue}
573
+ unit="deg"
574
+ on:change={handleOrientationEulerChange}
575
+ />
576
+ </TabPage>
577
+ </TabGroup>
578
+ </div>
579
+ {:else}
580
+ <div class="mt-0.5 flex gap-3">
581
+ {@render ImmutableField({
582
+ label: 'x',
583
+ ariaLabel: 'local orientation x coordinate',
584
+ value: localPose.oX,
585
+ })}
586
+ {@render ImmutableField({
587
+ label: 'y',
588
+ ariaLabel: 'local orientation y coordinate',
589
+ value: localPose.oY,
590
+ })}
591
+ {@render ImmutableField({
592
+ label: 'z',
593
+ ariaLabel: 'local orientation z coordinate',
594
+ value: localPose.oZ,
595
+ })}
596
+ {@render ImmutableField({
597
+ label: 'th',
598
+ ariaLabel: 'local orientation theta degrees',
599
+ value: localPose.theta,
600
+ })}
601
+ </div>
602
+ {/if}
603
+ </div>
604
+ {/if}
612
605
  {/if}
613
606
 
614
607
  {#if showEditFrameOptions}
@@ -718,20 +711,6 @@
718
711
  </div>
719
712
  {/if}
720
713
 
721
- <div>
722
- <strong class="font-semibold">opacity</strong>
723
- <div aria-label="mutable opacity">
724
- <Slider
725
- value={opacityValue}
726
- min={0}
727
- max={1}
728
- step={0.01}
729
- format={(v) => v.toFixed(2)}
730
- on:change={handleOpacityChange}
731
- />
732
- </div>
733
- </div>
734
-
735
714
  {#if isInstanceOf(object3d, 'Points')}
736
715
  <div>
737
716
  <strong class="font-semibold">points</strong>
@@ -744,27 +723,32 @@
744
723
  })}
745
724
  </div>
746
725
  {/if}
726
+
727
+ <PortalTarget id="details-extensions" />
728
+
729
+ {#if !customDetails.current}
730
+ <OpacityDetails {entity} />
731
+ <AxesHelperDetails {entity} />
732
+ {/if}
747
733
  </div>
748
734
 
749
735
  {#if linkedEntities.current.length > 0}
750
736
  <h3 class="text-subtle-2 pt-3 pb-2">Relationships</h3>
751
737
 
752
- <div>
753
- <div class="mt-0.5 flex flex-col gap-1">
754
- <strong class="font-semibold">Linked entities</strong>
755
- {#each linkedEntities.current as linkedEntity (linkedEntity)}
756
- {@const linkedEntityName = linkedEntity.get(traits.Name)}
757
- {@const linkType = entity.get(relations.SubEntityLink(linkedEntity))?.type}
758
- <div class="flex items-center gap-1">
759
- <span class="text-primary">{linkedEntityName} ({linkType})</span>
760
- <Icon
761
- name="trash-can-outline"
762
- class="h-6 cursor-pointer px-2 py-1 text-xs text-red-500"
763
- onclick={() => entity.remove(relations.SubEntityLink(linkedEntity))}
764
- />
765
- </div>
766
- {/each}
767
- </div>
738
+ <div class="mt-0.5 flex flex-col gap-1">
739
+ <strong class="font-semibold">Linked entities</strong>
740
+ {#each linkedEntities.current as linkedEntity (linkedEntity)}
741
+ {@const linkedEntityName = linkedEntity.get(traits.Name)}
742
+ {@const linkType = entity.get(relations.SubEntityLink(linkedEntity))?.type}
743
+ <div class="flex items-center gap-1">
744
+ <span class="text-primary">{linkedEntityName} ({linkType})</span>
745
+ <Icon
746
+ name="trash-can-outline"
747
+ class="h-6 cursor-pointer px-2 py-1 text-xs text-red-500"
748
+ onclick={() => entity.remove(relations.SubEntityLink(linkedEntity))}
749
+ />
750
+ </div>
751
+ {/each}
768
752
  </div>
769
753
  {/if}
770
754
 
@@ -1,6 +1,6 @@
1
- import type { Entity } from 'koota';
2
1
  import type { Snippet } from 'svelte';
3
2
  import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Entity } from 'koota';
4
4
  interface Props extends HTMLAttributes<HTMLDivElement> {
5
5
  entity: Entity;
6
6
  details?: Snippet<[{
@@ -6,8 +6,8 @@
6
6
  import { normalizeProps, portal, useMachine } from '@zag-js/svelte'
7
7
 
8
8
  interface Props {
9
- trigger: Snippet<[HTMLButtonAttributes]>
10
- children: Snippet
9
+ trigger: Snippet<[HTMLButtonAttributes, { isOpen: boolean }]>
10
+ children: Snippet<[{ close: () => void }]>
11
11
  }
12
12
 
13
13
  let { children, trigger }: Props = $props()
@@ -15,15 +15,17 @@
15
15
  const id = $props.id()
16
16
  const service = useMachine(popover.machine, { id })
17
17
  const api = $derived(popover.connect(service, normalizeProps))
18
+
19
+ const close = () => api.setOpen(false)
18
20
  </script>
19
21
 
20
- {@render trigger(api.getTriggerProps())}
22
+ {@render trigger(api.getTriggerProps(), { isOpen: api.open })}
21
23
 
22
24
  <div
23
25
  use:portal={{ disabled: !api.portalled }}
24
26
  {...api.getPositionerProps()}
25
27
  >
26
28
  <div {...api.getContentProps()}>
27
- {@render children()}
29
+ {@render children({ close })}
28
30
  </div>
29
31
  </div>
@@ -1,8 +1,12 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLButtonAttributes } from 'svelte/elements';
3
3
  interface Props {
4
- trigger: Snippet<[HTMLButtonAttributes]>;
5
- children: Snippet;
4
+ trigger: Snippet<[HTMLButtonAttributes, {
5
+ isOpen: boolean;
6
+ }]>;
7
+ children: Snippet<[{
8
+ close: () => void;
9
+ }]>;
6
10
  }
7
11
  declare const Popover: import("svelte").Component<Props, {}, "">;
8
12
  type Popover = ReturnType<typeof Popover>;
@@ -2,15 +2,16 @@
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 { Focus, MousePointer2, Ruler } from 'lucide-svelte'
5
+ import { Focus, MousePointer2, Ruler, Shapes } from 'lucide-svelte'
6
6
 
7
7
  interface Props extends HTMLButtonAttributes {
8
- icon: IconName | 'ruler' | 'mouse-pointer' | 'focus'
8
+ icon: IconName | 'ruler' | 'mouse-pointer' | 'shapes' | 'focus'
9
9
  active?: boolean
10
10
  description: string
11
11
  hotkey?: string
12
12
  class?: ClassValue | null | undefined
13
13
  tooltipLocation?: 'bottom' | 'right' | 'left' | 'top'
14
+ disableTooltip?: boolean
14
15
  onclick?: MouseEventHandler<HTMLButtonElement> | null | undefined
15
16
  }
16
17
 
@@ -21,6 +22,7 @@
21
22
  hotkey = '',
22
23
  class: className = '',
23
24
  tooltipLocation,
25
+ disableTooltip = false,
24
26
  onclick,
25
27
  ...rest
26
28
  }: Props = $props()
@@ -29,6 +31,7 @@
29
31
  <Tooltip
30
32
  let:tooltipID
31
33
  location={tooltipLocation ?? 'bottom'}
34
+ state={disableTooltip ? 'invisible' : undefined}
32
35
  >
33
36
  <label
34
37
  class={[
@@ -50,6 +53,8 @@
50
53
  <Ruler size="16" />
51
54
  {:else if icon === 'mouse-pointer'}
52
55
  <MousePointer2 size="16" />
56
+ {:else if icon === 'shapes'}
57
+ <Shapes size="16" />
53
58
  {:else if icon === 'focus'}
54
59
  <Focus size="16" />
55
60
  {:else}
@@ -1,12 +1,13 @@
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' | 'mouse-pointer' | 'focus';
4
+ icon: IconName | 'ruler' | 'mouse-pointer' | 'shapes' | 'focus';
5
5
  active?: boolean;
6
6
  description: string;
7
7
  hotkey?: string;
8
8
  class?: ClassValue | null | undefined;
9
9
  tooltipLocation?: 'bottom' | 'right' | 'left' | 'top';
10
+ disableTooltip?: boolean;
10
11
  onclick?: MouseEventHandler<HTMLButtonElement> | null | undefined;
11
12
  }
12
13
  declare const Button: import("svelte").Component<Props, {}, "">;