@viamrobotics/motion-tools 1.30.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 (76) 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 +10 -8
  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 +24 -38
  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/useConfigFrames.svelte.js +1 -1
  39. package/dist/hooks/useEnvironment.svelte.d.ts +1 -1
  40. package/dist/hooks/useLinked.svelte.js +7 -8
  41. package/dist/hooks/useMouseRaycaster.svelte.d.ts +4 -3
  42. package/dist/hooks/useMouseRaycaster.svelte.js +1 -0
  43. package/dist/hooks/useSettings.svelte.d.ts +1 -1
  44. package/dist/lib.d.ts +1 -0
  45. package/dist/lib.js +1 -0
  46. package/dist/loaders/pcd/worker.inline.d.ts +1 -1
  47. package/dist/loaders/pcd/worker.inline.js +1 -1
  48. package/dist/loaders/pcd/worker.js +3 -1
  49. package/dist/plugins/Focus/Focus.svelte +45 -0
  50. package/dist/plugins/Focus/Focus.svelte.d.ts +3 -0
  51. package/dist/plugins/Focus/FocusBox.svelte +75 -0
  52. package/dist/plugins/Focus/FocusBox.svelte.d.ts +3 -0
  53. package/dist/plugins/Focus/provideFocus.svelte.d.ts +1 -0
  54. package/dist/plugins/Focus/provideFocus.svelte.js +61 -0
  55. package/dist/{components → plugins}/MeasureTool/MeasureTool.svelte +6 -8
  56. package/dist/plugins/Selection/Ellipse.svelte +21 -16
  57. package/dist/plugins/Selection/Lasso.svelte +21 -16
  58. package/dist/plugins/Selection/SelectionTool.svelte +10 -3
  59. package/dist/plugins/Selection/relations.d.ts +6 -0
  60. package/dist/plugins/Selection/relations.js +7 -0
  61. package/dist/plugins/Selection/traits.d.ts +0 -5
  62. package/dist/plugins/Selection/traits.js +1 -6
  63. package/dist/plugins/index.d.ts +3 -0
  64. package/dist/plugins/index.js +3 -0
  65. package/dist/snapshot.d.ts +14 -0
  66. package/dist/snapshot.js +23 -0
  67. package/dist/three/arrow.d.ts +2 -0
  68. package/dist/three/arrow.js +3 -1
  69. package/package.json +17 -5
  70. package/dist/components/Focus.svelte +0 -46
  71. package/dist/components/Focus.svelte.d.ts +0 -7
  72. package/dist/hooks/useSelection.svelte.d.ts +0 -33
  73. package/dist/hooks/useSelection.svelte.js +0 -94
  74. /package/dist/{components → plugins}/MeasureTool/MeasurePoint.svelte +0 -0
  75. /package/dist/{components → plugins}/MeasureTool/MeasurePoint.svelte.d.ts +0 -0
  76. /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)
@@ -43,14 +44,15 @@
43
44
  )
44
45
  const isFragmentComponentWithVariables = $derived(
45
46
  name.current &&
46
- Object.keys(partConfig.componentNameToFragmentInfo[name.current]?.variables ?? {}).length > 0
47
+ Object.keys(partConfig.componentNameToFragmentInfo?.[name.current]?.variables ?? {}).length >
48
+ 0
47
49
  )
48
50
 
49
- // Mesh sets name={entity} on its inner mesh, so useSelectedObject3d resolves
51
+ // Mesh sets name={entity} on its inner mesh, so getObjectByName resolves
50
52
  // to that mesh — not the parent Frame Group we actually want to drive. Walk
51
53
  // up to the Group so translate/rotate/scale apply to the whole frame, not
52
54
  // the geometry inside it.
53
- const ref = $derived(selectedObject3d.current?.parent ?? selectedObject3d.current)
55
+ const ref = $derived(object3d?.parent ?? object3d)
54
56
 
55
57
  const activeMode = $derived.by<'translate' | 'rotate' | 'scale' | undefined>(() => {
56
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)
@@ -111,7 +102,8 @@
111
102
  const isGeometry = $derived(!!geometriesAPI.current)
112
103
  const isFragmentComponentWithVariables = $derived(
113
104
  name.current &&
114
- Object.keys(partConfig.componentNameToFragmentInfo[name.current]?.variables ?? {}).length > 0
105
+ Object.keys(partConfig.componentNameToFragmentInfo?.[name.current]?.variables ?? {}).length >
106
+ 0
115
107
  )
116
108
  const showEditFrameOptions = $derived(
117
109
  isFrameNode && partConfig.hasEditPermissions && !isFragmentComponentWithVariables
@@ -128,8 +120,7 @@
128
120
  })
129
121
 
130
122
  const geometryTypes = ['none', 'box', 'sphere', 'capsule'] as const
131
- // Writable derived: re-derives from the trait, but TabGroup's bind:selectedIndex
132
- // can write a transient override that lasts until the trait re-derives.
123
+
133
124
  let geometryTabIndex = $derived(geometryTypes.indexOf(geometryType))
134
125
 
135
126
  $effect(() => {
@@ -155,6 +146,10 @@
155
146
 
156
147
  const detailConfigUpdater = new FrameConfigUpdater(partConfig.updateFrame, partConfig.deleteFrame)
157
148
 
149
+ const stopKeyboardPropagation = (event: KeyboardEvent) => {
150
+ event.stopPropagation()
151
+ }
152
+
158
153
  const handlePositionChange = (event: PointChangeEvent) => {
159
154
  if (event.detail.origin !== 'internal' || !entity) return
160
155
  const next = event.detail.value as PointValue3dObject
@@ -311,13 +306,21 @@
311
306
  {/snippet}
312
307
 
313
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`. -->
314
311
  <div
315
312
  id="details-panel"
316
- 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}
317
319
  use:draggable={{
318
320
  bounds: 'body',
319
321
  handle: dragElement,
320
322
  }}
323
+ {...rest}
321
324
  >
322
325
  <div
323
326
  class="flex cursor-move items-center justify-between gap-2 pb-2"
@@ -767,25 +770,8 @@
767
770
 
768
771
  {@render details?.({ entity })}
769
772
 
770
- <h3 class="text-subtle-2 pt-3 pb-2">Actions</h3>
771
-
772
- {#if focusedEntity.current}
773
- <Button
774
- class="w-full"
775
- icon="arrow-left"
776
- variant="dark"
777
- onclick={() => focusedEntity.set()}
778
- >
779
- Exit object view
780
- </Button>
781
- {:else}
782
- <Button
783
- class="w-full"
784
- icon="image-filter-center-focus"
785
- onclick={() => focusedEntity.set(entity)}
786
- >
787
- Enter object view
788
- </Button>
773
+ {#if showRelationshipOptions || (showEditFrameOptions && environment.current.isStandalone)}
774
+ <h3 class="text-subtle-2 pt-3 pb-2">Actions</h3>
789
775
  {/if}
790
776
 
791
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;