@viamrobotics/motion-tools 1.31.0 → 1.32.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 (62) hide show
  1. package/dist/components/App.svelte +51 -46
  2. package/dist/components/App.svelte.d.ts +1 -1
  3. package/dist/components/Entities/Arrows/Arrows.svelte +4 -7
  4. package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +0 -1
  5. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +30 -16
  6. package/dist/components/InputBindings.svelte +0 -43
  7. package/dist/components/KeyboardBindings.svelte +38 -0
  8. package/dist/components/KeyboardBindings.svelte.d.ts +18 -0
  9. package/dist/components/PointerMissBox.svelte +6 -3
  10. package/dist/components/Scene.svelte +34 -45
  11. package/dist/components/SceneProviders.svelte +2 -4
  12. package/dist/components/SceneProviders.svelte.d.ts +1 -3
  13. package/dist/components/Selected.svelte +20 -27
  14. package/dist/components/SelectedTransformControls.svelte +8 -7
  15. package/dist/components/StaticGeometries.svelte +3 -5
  16. package/dist/components/hover/HoveredEntities.svelte +15 -14
  17. package/dist/components/hover/HoveredEntities.svelte.d.ts +17 -2
  18. package/dist/components/hover/HoveredEntity.svelte +8 -5
  19. package/dist/components/hover/HoveredEntity.svelte.d.ts +5 -1
  20. package/dist/components/hover/LinkedHoveredEntity.svelte +7 -11
  21. package/dist/components/hover/LinkedHoveredEntity.svelte.d.ts +1 -0
  22. package/dist/components/overlay/Details.svelte +22 -37
  23. package/dist/components/overlay/Details.svelte.d.ts +3 -1
  24. package/dist/components/overlay/controls/Controls.svelte +0 -2
  25. package/dist/components/overlay/dashboard/Button.svelte +5 -3
  26. package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
  27. package/dist/components/overlay/left-pane/Tree.svelte +13 -10
  28. package/dist/components/overlay/left-pane/TreeContainer.svelte +9 -4
  29. package/dist/components/overlay/left-pane/TreeNode.svelte +6 -4
  30. package/dist/draw.d.ts +1 -0
  31. package/dist/draw.js +1 -1
  32. package/dist/ecs/index.d.ts +1 -0
  33. package/dist/ecs/index.js +1 -0
  34. package/dist/ecs/traits.d.ts +22 -5
  35. package/dist/ecs/traits.js +33 -4
  36. package/dist/ecs/useTag.svelte.d.ts +5 -0
  37. package/dist/ecs/useTag.svelte.js +43 -0
  38. package/dist/hooks/useEnvironment.svelte.d.ts +1 -1
  39. package/dist/hooks/useLinked.svelte.js +7 -8
  40. package/dist/hooks/useMouseRaycaster.svelte.d.ts +4 -3
  41. package/dist/hooks/useMouseRaycaster.svelte.js +1 -0
  42. package/dist/hooks/useSettings.svelte.d.ts +1 -1
  43. package/dist/plugins/Focus/Focus.svelte +45 -0
  44. package/dist/plugins/Focus/Focus.svelte.d.ts +3 -0
  45. package/dist/plugins/Focus/FocusBox.svelte +75 -0
  46. package/dist/plugins/Focus/FocusBox.svelte.d.ts +3 -0
  47. package/dist/plugins/Focus/provideFocus.svelte.d.ts +1 -0
  48. package/dist/plugins/Focus/provideFocus.svelte.js +61 -0
  49. package/dist/{components → plugins}/MeasureTool/MeasureTool.svelte +6 -8
  50. package/dist/plugins/Selection/SelectionTool.svelte +10 -3
  51. package/dist/plugins/index.d.ts +2 -0
  52. package/dist/plugins/index.js +2 -0
  53. package/dist/three/arrow.d.ts +2 -0
  54. package/dist/three/arrow.js +3 -1
  55. package/package.json +16 -4
  56. package/dist/components/Focus.svelte +0 -46
  57. package/dist/components/Focus.svelte.d.ts +0 -7
  58. package/dist/hooks/useSelection.svelte.d.ts +0 -33
  59. package/dist/hooks/useSelection.svelte.js +0 -94
  60. /package/dist/{components → plugins}/MeasureTool/MeasurePoint.svelte +0 -0
  61. /package/dist/{components → plugins}/MeasureTool/MeasurePoint.svelte.d.ts +0 -0
  62. /package/dist/{components → plugins}/MeasureTool/MeasureTool.svelte.d.ts +0 -0
@@ -1,15 +1,15 @@
1
1
  <script lang="ts">
2
+ import { useThrelte } from '@threlte/core'
2
3
  import { TransformControls } from '@threlte/extras'
3
4
  import { Matrix4, Quaternion, Vector3 } from 'three'
4
5
 
5
6
  import type { FrameEditSession } from '../editing/FrameEditSession'
6
7
 
7
- import { relations, traits, useTrait } from '../ecs'
8
+ import { relations, traits, useQuery, useTrait } from '../ecs'
8
9
  import { useTransformControls } from '../hooks/useControls.svelte'
9
10
  import { useEnvironment } from '../hooks/useEnvironment.svelte'
10
11
  import { useFrameEditSession } from '../hooks/useFrameEditSession.svelte'
11
12
  import { usePartConfig } from '../hooks/usePartConfig.svelte'
12
- import { useSelectedEntity, useSelectedObject3d } from '../hooks/useSelection.svelte'
13
13
  import { useSettings } from '../hooks/useSettings.svelte'
14
14
  import {
15
15
  createPose,
@@ -20,16 +20,17 @@
20
20
  vector3ToPose,
21
21
  } from '../transform'
22
22
 
23
+ const { scene } = useThrelte()
23
24
  const settings = useSettings()
24
25
  const environment = useEnvironment()
25
26
  const partConfig = usePartConfig()
26
27
  const transformControls = useTransformControls()
27
- const selectedEntity = useSelectedEntity()
28
- const selectedObject3d = useSelectedObject3d()
29
28
  const sessions = useFrameEditSession()
29
+ const selected = useQuery(traits.Selected)
30
30
 
31
31
  const mode = $derived(settings.current.transformMode)
32
- const entity = $derived(selectedEntity.current)
32
+ const entity = $derived(selected.current[0])
33
+ const object3d = $derived(scene.getObjectByName(entity as unknown as string))
33
34
  const transformable = useTrait(() => entity, traits.Transformable)
34
35
  const invisible = useTrait(() => entity, traits.InheritedInvisible)
35
36
  const configMatrix = useTrait(() => entity, traits.Matrix)
@@ -47,11 +48,11 @@
47
48
  0
48
49
  )
49
50
 
50
- // Mesh sets name={entity} on its inner mesh, so useSelectedObject3d resolves
51
+ // Mesh sets name={entity} on its inner mesh, so getObjectByName resolves
51
52
  // to that mesh — not the parent Frame Group we actually want to drive. Walk
52
53
  // up to the Group so translate/rotate/scale apply to the whole frame, not
53
54
  // the geometry inside it.
54
- const ref = $derived(selectedObject3d.current?.parent ?? selectedObject3d.current)
55
+ const ref = $derived(object3d?.parent ?? object3d)
55
56
 
56
57
  const activeMode = $derived.by<'translate' | 'rotate' | 'scale' | undefined>(() => {
57
58
  if (mode === 'none' || !transformable.current) return
@@ -11,17 +11,16 @@
11
11
  import { PressedKeys } from 'runed'
12
12
  import { SvelteSet } from 'svelte/reactivity'
13
13
 
14
- import { traits, useWorld } from '../ecs'
15
- import { useSelectedEntity } from '../hooks/useSelection.svelte'
14
+ import { traits, useQuery, useWorld } from '../ecs'
16
15
 
17
16
  import Frame from './Entities/Frame.svelte'
18
17
 
19
18
  const world = useWorld()
20
- const selectedEntity = useSelectedEntity()
19
+ const selected = useQuery(traits.Selected)
21
20
 
22
21
  const entities = new SvelteSet<Entity>()
23
22
  const selectedCustomGeometry = $derived(
24
- [...entities].find((entity) => entity === selectedEntity.current)
23
+ [...entities].find((entity) => entity === selected.current[0])
25
24
  )
26
25
 
27
26
  const keys = new PressedKeys()
@@ -43,7 +42,6 @@
43
42
  const entity = selectedCustomGeometry
44
43
  entity.destroy()
45
44
  entities.delete(entity)
46
- selectedEntity.set()
47
45
  }
48
46
  })
49
47
  </script>
@@ -1,24 +1,25 @@
1
1
  <script lang="ts">
2
- import { traits, useTrait } from '../../ecs'
2
+ import { traits, useQuery, useTrait } from '../../ecs'
3
3
  import { useLinkedEntities } from '../../hooks/useLinked.svelte'
4
- import { useSelectedEntity } from '../../hooks/useSelection.svelte'
5
- import { useFocusedEntity } from '../../hooks/useSelection.svelte'
6
4
 
7
5
  import HoveredEntity from './HoveredEntity.svelte'
8
6
  import LinkedHoveredEntity from './LinkedHoveredEntity.svelte'
9
7
 
10
- const selectedEntity = useSelectedEntity()
11
- const focusedEntity = useFocusedEntity()
12
8
  const linkedEntities = useLinkedEntities()
13
-
14
- const displayEntity = $derived(selectedEntity.current ?? focusedEntity.current)
15
- const isHovered = useTrait(() => displayEntity, traits.Hovered)
9
+ const selected = useQuery(traits.Selected)
16
10
  </script>
17
11
 
18
- {#if isHovered}
19
- <HoveredEntity />
12
+ {#each selected.current as entity (entity)}
13
+ {@const isHovered = useTrait(() => entity, traits.Hovered)}
14
+
15
+ {#if isHovered}
16
+ <HoveredEntity {entity} />
20
17
 
21
- {#each linkedEntities.current as entity (entity)}
22
- <LinkedHoveredEntity linkedEntity={entity} />
23
- {/each}
24
- {/if}
18
+ {#each linkedEntities.current as linkedEntity (linkedEntity)}
19
+ <LinkedHoveredEntity
20
+ {linkedEntity}
21
+ {entity}
22
+ />
23
+ {/each}
24
+ {/if}
25
+ {/each}
@@ -1,3 +1,18 @@
1
- declare const HoveredEntities: import("svelte").Component<Record<string, never>, {}, "">;
2
- type HoveredEntities = ReturnType<typeof HoveredEntities>;
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const HoveredEntities: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type HoveredEntities = InstanceType<typeof HoveredEntities>;
3
18
  export default HoveredEntities;
@@ -1,19 +1,22 @@
1
1
  <script lang="ts">
2
+ import type { Entity } from 'koota'
3
+
2
4
  import { MathUtils, Quaternion, Vector3 } from 'three'
3
5
 
4
6
  import type { HoverInfo } from '../../HoverUpdater.svelte'
5
7
 
6
8
  import { traits, useTrait } from '../../ecs'
7
- import { useFocusedEntity, useSelectedEntity } from '../../hooks/useSelection.svelte'
8
9
  import { OrientationVector } from '../../three/OrientationVector'
9
10
 
10
11
  import HoveredEntityTooltip from './HoveredEntityTooltip.svelte'
11
12
 
12
- const selectedEntity = useSelectedEntity()
13
- const focusedEntity = useFocusedEntity()
13
+ interface Props {
14
+ entity: Entity
15
+ }
16
+
17
+ let { entity }: Props = $props()
14
18
 
15
- const displayEntity = $derived(selectedEntity.current ?? focusedEntity.current)
16
- const instancedMatrix = useTrait(() => displayEntity, traits.InstancedMatrix)
19
+ const instancedMatrix = useTrait(() => entity, traits.InstancedMatrix)
17
20
 
18
21
  // Pool: InstancedMatrix's `Matrix4` is in metres (matches Three.js).
19
22
  // Decompose for the tooltip's display, which expects metres for position
@@ -1,3 +1,7 @@
1
- declare const HoveredEntity: import("svelte").Component<Record<string, never>, {}, "">;
1
+ import type { Entity } from 'koota';
2
+ interface Props {
3
+ entity: Entity;
4
+ }
5
+ declare const HoveredEntity: import("svelte").Component<Props, {}, "">;
2
6
  type HoveredEntity = ReturnType<typeof HoveredEntity>;
3
7
  export default HoveredEntity;
@@ -5,36 +5,32 @@
5
5
 
6
6
  import { relations, traits, useTrait } from '../../ecs'
7
7
  import { SubEntityLinkType } from '../../ecs/relations'
8
- import { useSelectedEntity } from '../../hooks/useSelection.svelte'
9
- import { useFocusedEntity } from '../../hooks/useSelection.svelte'
10
8
  import { getLinkedHoverInfo, type HoverInfo } from '../../HoverUpdater.svelte'
11
9
 
12
10
  import HoveredEntityTooltip from './HoveredEntityTooltip.svelte'
13
11
 
14
12
  interface Props {
15
13
  linkedEntity: Entity
14
+ entity: Entity
16
15
  }
17
16
 
18
- let { linkedEntity }: Props = $props()
17
+ let { linkedEntity, entity }: Props = $props()
19
18
 
20
- const selectedEntity = useSelectedEntity()
21
- const focusedEntity = useFocusedEntity()
22
- const displayEntity = $derived(selectedEntity.current ?? focusedEntity.current)
23
-
24
- const displayedHoverInfo = useTrait(() => displayEntity, traits.InstancedMatrix)
19
+ const displayedHoverInfo = useTrait(() => entity, traits.InstancedMatrix)
25
20
 
26
21
  let hoverInfo = $state.raw<HoverInfo | null>(null)
27
22
 
28
23
  $effect(() => {
29
- if (displayEntity && displayedHoverInfo.current) {
30
- const linkType = displayEntity?.get(relations.SubEntityLink(linkedEntity))?.type
24
+ if (entity && displayedHoverInfo.current) {
25
+ const linkType = entity?.get(relations.SubEntityLink(linkedEntity))?.type
31
26
  if (linkType !== SubEntityLinkType.HoverLink) {
32
27
  return
33
28
  }
29
+
34
30
  // Index mapping is a formula with the variable 'index' in it.
35
31
  // Supported operations: https://github.com/cshaa/filtrex#expressions
36
32
  const indexMapping =
37
- displayEntity?.get(relations.SubEntityLink(linkedEntity))?.indexMapping ?? 'index'
33
+ entity?.get(relations.SubEntityLink(linkedEntity))?.indexMapping ?? 'index'
38
34
  const evaluate = compileExpression(indexMapping)
39
35
  const resolvedIndex = evaluate({ index: displayedHoverInfo.current.index })
40
36
  const linkedHoverInfo = getLinkedHoverInfo(resolvedIndex, linkedEntity)
@@ -1,6 +1,7 @@
1
1
  import type { Entity } from 'koota';
2
2
  interface Props {
3
3
  linkedEntity: Entity;
4
+ entity: Entity;
4
5
  }
5
6
  declare const LinkedHoveredEntity: import("svelte").Component<Props, {}, "">;
6
7
  type LinkedHoveredEntity = ReturnType<typeof LinkedHoveredEntity>;
@@ -15,6 +15,7 @@
15
15
  import type { Pose } from '@viamrobotics/sdk'
16
16
  import type { Entity } from 'koota'
17
17
  import type { Snippet } from 'svelte'
18
+ import type { HTMLAttributes } from 'svelte/elements'
18
19
 
19
20
  import { draggable } from '@neodrag/svelte'
20
21
  import { isInstanceOf, useThrelte } from '@threlte/core'
@@ -46,38 +47,28 @@
46
47
  import { usePartConfig } from '../../hooks/usePartConfig.svelte'
47
48
  import { usePartID } from '../../hooks/usePartID.svelte'
48
49
  import { useResourceByName } from '../../hooks/useResourceByName.svelte'
49
- import {
50
- useFocusedEntity,
51
- useFocusedObject3d,
52
- useSelectedEntity,
53
- useSelectedObject3d,
54
- } from '../../hooks/useSelection.svelte'
55
50
  import { useSettings } from '../../hooks/useSettings.svelte'
56
51
  import { createPose, matrixToPose } from '../../transform'
57
52
 
58
- interface Props {
53
+ interface Props extends HTMLAttributes<HTMLDivElement> {
54
+ entity: Entity
59
55
  details?: Snippet<[{ entity: Entity }]>
60
56
  }
61
57
 
62
- const { details }: Props = $props()
58
+ const { entity, details, ...rest }: Props = $props()
63
59
 
64
60
  const world = useWorld()
65
- const { invalidate } = useThrelte()
61
+ const { scene, invalidate } = useThrelte()
66
62
  const controls = useCameraControls()
67
63
  const resourceByName = useResourceByName()
68
64
  const configFrames = useConfigFrames()
69
65
  const partConfig = usePartConfig()
70
66
  const partID = usePartID()
71
67
  const settings = useSettings()
72
- const selectedEntity = useSelectedEntity()
73
- const selectedObject3d = useSelectedObject3d()
74
68
  const environment = useEnvironment()
75
- const focusedEntity = useFocusedEntity()
76
- const focusedObject3d = useFocusedObject3d()
77
69
  const linkedEntities = useLinkedEntities()
78
70
 
79
- const entity = $derived(focusedEntity.current ?? selectedEntity.current)
80
- const object3d = $derived(focusedObject3d.current ?? selectedObject3d.current)
71
+ const object3d = $derived(scene.getObjectByName(entity as unknown as string))
81
72
 
82
73
  const name = useTrait(() => entity, traits.Name)
83
74
  const parent = useParentName(() => entity)
@@ -129,8 +120,7 @@
129
120
  })
130
121
 
131
122
  const geometryTypes = ['none', 'box', 'sphere', 'capsule'] as const
132
- // Writable derived: re-derives from the trait, but TabGroup's bind:selectedIndex
133
- // can write a transient override that lasts until the trait re-derives.
123
+
134
124
  let geometryTabIndex = $derived(geometryTypes.indexOf(geometryType))
135
125
 
136
126
  $effect(() => {
@@ -156,6 +146,10 @@
156
146
 
157
147
  const detailConfigUpdater = new FrameConfigUpdater(partConfig.updateFrame, partConfig.deleteFrame)
158
148
 
149
+ const stopKeyboardPropagation = (event: KeyboardEvent) => {
150
+ event.stopPropagation()
151
+ }
152
+
159
153
  const handlePositionChange = (event: PointChangeEvent) => {
160
154
  if (event.detail.origin !== 'internal' || !entity) return
161
155
  const next = event.detail.value as PointValue3dObject
@@ -312,13 +306,21 @@
312
306
  {/snippet}
313
307
 
314
308
  {#if entity}
309
+ <!-- tabindex makes the whole panel focusable so a click anywhere in it (not
310
+ just the inputs) raises it via `focus-within:z-5`. -->
315
311
  <div
316
312
  id="details-panel"
317
- class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 w-70 border p-2 text-xs"
313
+ class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 w-70 border p-2 text-xs focus-within:z-5"
314
+ role="region"
315
+ aria-label="Details panel"
316
+ tabindex="-1"
317
+ onkeydown={stopKeyboardPropagation}
318
+ onkeyup={stopKeyboardPropagation}
318
319
  use:draggable={{
319
320
  bounds: 'body',
320
321
  handle: dragElement,
321
322
  }}
323
+ {...rest}
322
324
  >
323
325
  <div
324
326
  class="flex cursor-move items-center justify-between gap-2 pb-2"
@@ -768,25 +770,8 @@
768
770
 
769
771
  {@render details?.({ entity })}
770
772
 
771
- <h3 class="text-subtle-2 pt-3 pb-2">Actions</h3>
772
-
773
- {#if focusedEntity.current}
774
- <Button
775
- class="w-full"
776
- icon="arrow-left"
777
- variant="dark"
778
- onclick={() => focusedEntity.set()}
779
- >
780
- Exit object view
781
- </Button>
782
- {:else}
783
- <Button
784
- class="w-full"
785
- icon="image-filter-center-focus"
786
- onclick={() => focusedEntity.set(entity)}
787
- >
788
- Enter object view
789
- </Button>
773
+ {#if showRelationshipOptions || (showEditFrameOptions && environment.current.isStandalone)}
774
+ <h3 class="text-subtle-2 pt-3 pb-2">Actions</h3>
790
775
  {/if}
791
776
 
792
777
  {#if showRelationshipOptions}
@@ -1,6 +1,8 @@
1
1
  import type { Entity } from 'koota';
2
2
  import type { Snippet } from 'svelte';
3
- interface Props {
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ interface Props extends HTMLAttributes<HTMLDivElement> {
5
+ entity: Entity;
4
6
  details?: Snippet<[{
5
7
  entity: Entity;
6
8
  }]>;
@@ -16,7 +16,6 @@
16
16
 
17
17
  <fieldset class="flex flex-col">
18
18
  <Button
19
- active
20
19
  class="rounded-b-none"
21
20
  icon="camera-outline"
22
21
  description="Reset camera"
@@ -26,7 +25,6 @@
26
25
  }}
27
26
  />
28
27
  <Button
29
- active
30
28
  class="-my-0.5 rounded-t-none"
31
29
  icon={isOrthographic ? 'grid-orthographic' : 'grid-perspective'}
32
30
  description={isOrthographic ? 'Switch to perspective view' : 'Switch to orthographic view'}
@@ -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 { MousePointer2, Ruler } from 'lucide-svelte'
5
+ import { Focus, MousePointer2, Ruler } from 'lucide-svelte'
6
6
 
7
7
  interface Props extends HTMLButtonAttributes {
8
- icon: IconName | 'ruler' | 'mouse-pointer'
8
+ icon: IconName | 'ruler' | 'mouse-pointer' | 'focus'
9
9
  active?: boolean
10
10
  description: string
11
11
  hotkey?: string
@@ -33,7 +33,7 @@
33
33
  <label
34
34
  class={[
35
35
  className,
36
- 'relative block rounded-md border',
36
+ 'relative block rounded-md border active:z-4 active:border-[#666] active:bg-[#666] active:text-white',
37
37
  active ? 'z-4 border-[#666] bg-[#666] text-white' : 'border-gray-5 text-gray-8 bg-white',
38
38
  ]}
39
39
  aria-describedby={tooltipID}
@@ -50,6 +50,8 @@
50
50
  <Ruler size="16" />
51
51
  {:else if icon === 'mouse-pointer'}
52
52
  <MousePointer2 size="16" />
53
+ {:else if icon === 'focus'}
54
+ <Focus size="16" />
53
55
  {:else}
54
56
  <Icon name={icon} />
55
57
  {/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' | 'mouse-pointer';
4
+ icon: IconName | 'ruler' | 'mouse-pointer' | 'focus';
5
5
  active?: boolean;
6
6
  description: string;
7
7
  hotkey?: string;
@@ -6,15 +6,12 @@
6
6
  import { VirtualList } from 'svelte-virtuallists'
7
7
  import { SvelteSet } from 'svelte/reactivity'
8
8
 
9
- import { relations, traits } from '../../../ecs'
10
- import { useSelectedEntity } from '../../../hooks/useSelection.svelte'
9
+ import { relations, traits, useQuery } from '../../../ecs'
11
10
 
12
11
  import type { TreeNode as TreeNodeType } from './useTree.svelte'
13
12
 
14
13
  import TreeNode from './TreeNode.svelte'
15
14
 
16
- const selected = useSelectedEntity()
17
-
18
15
  interface Props {
19
16
  rootNode: TreeNodeType
20
17
  dragElement?: HTMLElement
@@ -31,14 +28,18 @@
31
28
  })
32
29
  )
33
30
 
34
- const selectedValue = $derived(selected.current ? [`${selected.current}`] : [])
31
+ const selected = useQuery(traits.Selected)
32
+
33
+ const selectedValue = $derived(selected.current.map((entity) => `${entity}`))
35
34
  const expandedValues = new SvelteSet<string>()
36
35
 
37
36
  $effect(() => {
38
- let entity: Entity | undefined = selected.current
39
- while (entity) {
40
- expandedValues.add(`${entity}`)
41
- entity = entity.targetFor(relations.ChildOf)
37
+ for (const entity of selected.current) {
38
+ let ancestor: Entity | undefined = entity.targetFor(relations.ChildOf)
39
+ while (ancestor) {
40
+ expandedValues.add(`${ancestor}`)
41
+ ancestor = ancestor.targetFor(relations.ChildOf)
42
+ }
42
43
  }
43
44
  })
44
45
 
@@ -46,6 +47,8 @@
46
47
  const service = useMachine(tree.machine, () => ({
47
48
  id,
48
49
  collection,
50
+ selectionMode: 'multiple' as const,
51
+ expandOnClick: false,
49
52
  selectedValue,
50
53
  expandedValue: [...expandedValues],
51
54
  onSelectionChange(details) {
@@ -64,7 +67,7 @@
64
67
 
65
68
  $effect(() => {
66
69
  const element = document.querySelector(
67
- `[data-scope="tree-view"][data-value="${selected.current}"]`
70
+ `[data-scope="tree-view"][data-value="${selected.current.at(-1)}"]`
68
71
  )
69
72
 
70
73
  requestAnimationFrame(() => {
@@ -2,7 +2,6 @@
2
2
  import { type Entity, IsExcluded } from 'koota'
3
3
 
4
4
  import { traits, useWorld } from '../../../ecs'
5
- import { useSelectedEntity } from '../../../hooks/useSelection.svelte'
6
5
 
7
6
  import FloatingPanel from '../FloatingPanel.svelte'
8
7
  import Tree from './Tree.svelte'
@@ -11,7 +10,6 @@
11
10
 
12
11
  provideTreeExpandedContext()
13
12
 
14
- const selectedEntity = useSelectedEntity()
15
13
  const world = useWorld()
16
14
 
17
15
  const worldEntity = world.spawn(IsExcluded, traits.Name('World'))
@@ -35,9 +33,16 @@
35
33
  <Tree
36
34
  {rootNode}
37
35
  onSelectionChange={(event) => {
38
- const value = event.selectedValue[0]
36
+ const next = new Set(event.selectedValue.map(Number))
39
37
 
40
- selectedEntity.set(value ? (Number(value) as Entity) : undefined)
38
+ for (const entity of world.query(traits.Selected)) {
39
+ if (!next.has(entity as number)) entity.remove(traits.Selected)
40
+ }
41
+
42
+ for (const id of next) {
43
+ const entity = id as Entity
44
+ if (!entity.has(traits.Selected)) entity.add(traits.Selected)
45
+ }
41
46
  }}
42
47
  />
43
48
  </FloatingPanel>
@@ -63,12 +63,14 @@
63
63
  ]}
64
64
  >
65
65
  <div {...api.getBranchControlProps(nodeProps)}>
66
- <span
67
- {...api.getBranchIndicatorProps(nodeProps)}
68
- class={{ 'rotate-90': expanded }}
66
+ <button
67
+ type="button"
68
+ aria-label={expanded ? 'Collapse' : 'Expand'}
69
+ {...api.getBranchTriggerProps(nodeProps)}
70
+ class={['flex items-center', { 'rotate-90': expanded }]}
69
71
  >
70
72
  <ChevronRight size={14} />
71
- </span>
73
+ </button>
72
74
  <span
73
75
  class="flex items-center overflow-hidden text-ellipsis"
74
76
  {...api.getBranchTextProps(nodeProps)}
package/dist/draw.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { Transform as TransformProto } from './buf/common/v1/common_pb';
4
4
  import type { Drawing } from './buf/draw/v1/drawing_pb';
5
5
  import type { Relationship } from './metadata';
6
6
  import { type Metadata } from './metadata';
7
+ export declare const DEFAULT_LINE_WIDTH = 5;
7
8
  export type Transform = TransformWithUUID | TransformProto;
8
9
  export declare const uuidBytesToString: (bytes: Uint8Array | undefined) => string | undefined;
9
10
  export declare const uuidStringToBytes: (uuid: string) => Uint8Array<ArrayBuffer>;
package/dist/draw.js CHANGED
@@ -11,7 +11,7 @@ import { ColorFormat } from './buf/draw/v1/metadata_pb';
11
11
  import { isPointCloud } from './geometry';
12
12
  const vec3 = new Vector3();
13
13
  const rgb = { r: 0, g: 0, b: 0 };
14
- const DEFAULT_LINE_WIDTH = 5;
14
+ export const DEFAULT_LINE_WIDTH = 5;
15
15
  const DEFAULT_POINT_SIZE = 10;
16
16
  const DEFAULT_NURBS_DEGREE = 3;
17
17
  const DEFAULT_NURBS_WEIGHT = 1;
@@ -1,6 +1,7 @@
1
1
  export { provideWorld, useWorld } from './useWorld';
2
2
  export { useQuery } from './useQuery.svelte';
3
3
  export { useTrait } from './useTrait.svelte';
4
+ export { useTag } from './useTag.svelte';
4
5
  export { useTarget } from './useTarget.svelte';
5
6
  export { useParentName } from './useParentName.svelte';
6
7
  export { provideHierarchy } from './provideHierarchy.svelte';
package/dist/ecs/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { provideWorld, useWorld } from './useWorld';
2
2
  export { useQuery } from './useQuery.svelte';
3
3
  export { useTrait } from './useTrait.svelte';
4
+ export { useTag } from './useTag.svelte';
4
5
  export { useTarget } from './useTarget.svelte';
5
6
  export { useParentName } from './useParentName.svelte';
6
7
  export { provideHierarchy } from './provideHierarchy.svelte';
@@ -1,5 +1,5 @@
1
1
  import type { GLTF as ThreeGltf } from 'three/examples/jsm/loaders/GLTFLoader.js';
2
- import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
2
+ import { type Pose, Geometry as ViamGeometry } from '@viamrobotics/sdk';
3
3
  import { type Entity } from 'koota';
4
4
  import { Matrix4, BufferGeometry as ThreeBufferGeometry } from 'three';
5
5
  export declare const Name: import("koota").Trait<() => string>;
@@ -65,6 +65,13 @@ export declare const InstancedMatrix: import("koota").Trait<() => {
65
65
  }>;
66
66
  export declare const Hovered: import("koota").Trait<() => boolean>;
67
67
  export declare const Invisible: import("koota").Trait<() => boolean>;
68
+ /**
69
+ * Suppresses the default frame-style world/local pose and parent-frame blocks
70
+ * in the details panel. Entities that render their own pose UI via the
71
+ * `details-extensions` portal target (e.g. gizmo plugin entities) opt in by
72
+ * adding this trait.
73
+ */
74
+ export declare const CustomDetails: import("koota").Trait<() => boolean>;
68
75
  /**
69
76
  * True when the entity itself, or any of its parents up the `ChildOf`
70
77
  * chain, has `Invisible`. Maintained by `provideInheritedInvisible`;
@@ -76,11 +83,14 @@ export declare const InheritedInvisible: import("koota").Trait<() => boolean>;
76
83
  * Represents that an entity is composed of many instances, so that the treeview and
77
84
  * details panel may display all instances
78
85
  */
79
- export declare const Instanced: import("koota").Trait<() => boolean>;
86
+ export declare const InstanceId: import("koota").Trait<() => number>;
80
87
  export declare const Instance: import("koota").Trait<{
81
88
  meshID: number;
82
89
  instanceID: number;
83
90
  }>;
91
+ export declare const Instances: import("koota").Trait<{
92
+ count: number;
93
+ }>;
84
94
  export declare const RenderOrder: import("koota").Trait<() => number>;
85
95
  export declare const Opacity: import("koota").Trait<() => number>;
86
96
  /**
@@ -108,9 +118,6 @@ export declare const Colors: import("koota").Trait<() => Uint8Array<ArrayBufferL
108
118
  * Per-vertex opacity values packed as uint8 (0-255).
109
119
  */
110
120
  export declare const Opacities: import("koota").Trait<() => Uint8Array<ArrayBuffer>>;
111
- export declare const Instances: import("koota").Trait<{
112
- count: number;
113
- }>;
114
121
  export declare const Arrows: import("koota").Trait<{
115
122
  headAtPose: boolean;
116
123
  }>;
@@ -205,6 +212,10 @@ export declare const ChunkProgress: import("koota").Trait<{
205
212
  */
206
213
  export type InteractionLayerValue = 'selectTool';
207
214
  export declare const SelectToolInteractionLayer: import("koota").Trait<() => boolean>;
215
+ /**
216
+ * This entity is selected by the user
217
+ */
218
+ export declare const Selected: import("koota").TagTrait;
208
219
  /**
209
220
  * This entity can be safely removed from the scene by the user
210
221
  */
@@ -229,3 +240,9 @@ export declare const Geometry: (geometry: ViamGeometry) => import("koota").Trait
229
240
  r: number;
230
241
  }>] | [import("koota").Trait<() => ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>>, ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>];
231
242
  export declare const updateGeometryTrait: (entity: Entity, geometry?: ViamGeometry) => void;
243
+ /**
244
+ * Patches an entity's `Matrix` trait in-place via the `Pose` round-trip
245
+ * (`matrixToPose` → merge → `poseToMatrix`), then signals `entity.changed(Matrix)`.
246
+ * No-ops silently if the entity has no `Matrix` trait.
247
+ */
248
+ export declare const writeMatrix: (entity: Entity, patch: Partial<Pose>) => void;