@viamrobotics/motion-tools 1.15.4 → 1.15.6

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 (30) hide show
  1. package/dist/components/Entities/Arrows/Arrows.svelte +6 -3
  2. package/dist/components/Entities/Frame.svelte +5 -1
  3. package/dist/components/Entities/GLTF.svelte +2 -0
  4. package/dist/components/Entities/Geometry.svelte +20 -17
  5. package/dist/components/Entities/Line.svelte +2 -0
  6. package/dist/components/Entities/Points.svelte +2 -0
  7. package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +0 -1
  8. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +3 -8
  9. package/dist/components/KeyboardControls.svelte +6 -4
  10. package/dist/components/SceneProviders.svelte +0 -2
  11. package/dist/components/overlay/FloatingPanel.svelte +1 -1
  12. package/dist/components/overlay/Logs.svelte +95 -28
  13. package/dist/components/overlay/left-pane/Tree.svelte +17 -147
  14. package/dist/components/overlay/left-pane/Tree.svelte.d.ts +3 -3
  15. package/dist/components/overlay/left-pane/TreeNode.svelte +169 -0
  16. package/dist/components/overlay/left-pane/TreeNode.svelte.d.ts +10 -0
  17. package/dist/components/xr/OriginMarker.svelte +4 -20
  18. package/dist/components/xr/XRConfigPanel.svelte +1 -7
  19. package/dist/components/xr/useAnchors.svelte.d.ts +8 -1
  20. package/dist/components/xr/useAnchors.svelte.js +27 -27
  21. package/dist/ecs/traits.d.ts +1 -0
  22. package/dist/ecs/traits.js +1 -0
  23. package/dist/hooks/useFramelessComponents.svelte.js +3 -3
  24. package/dist/hooks/useFrames.svelte.js +19 -4
  25. package/dist/hooks/useGeometries.svelte.js +11 -1
  26. package/dist/hooks/usePointcloudObjects.svelte.js +8 -0
  27. package/dist/hooks/usePointclouds.svelte.js +8 -0
  28. package/package.json +3 -3
  29. package/dist/hooks/useVisibility.svelte.d.ts +0 -6
  30. package/dist/hooks/useVisibility.svelte.js +0 -10
@@ -7,7 +7,7 @@
7
7
  import type { InstancedArrows } from '../../../three/InstancedArrows/InstancedArrows'
8
8
 
9
9
  import { useEntityEvents } from '../hooks/useEntityEvents.svelte'
10
- import { traits } from '../../../ecs'
10
+ import { traits, useTrait } from '../../../ecs'
11
11
  import { useFocusedEntity, useSelectedEntity } from '../../../hooks/useSelection.svelte'
12
12
  import { meshBoundsRaycast, raycast } from '../../../three/InstancedArrows/raycast'
13
13
 
@@ -18,6 +18,9 @@
18
18
 
19
19
  let { entity, arrows }: Props = $props()
20
20
 
21
+ const parent = useTrait(() => entity, traits.Parent)
22
+ const invisible = useTrait(() => entity, traits.Invisible)
23
+
21
24
  const events = useEntityEvents(() => entity)
22
25
  const selectedEntity = useSelectedEntity()
23
26
  const focusedEntity = useFocusedEntity()
@@ -32,7 +35,7 @@
32
35
  })
33
36
  </script>
34
37
 
35
- <Portal id={entity.get(traits.Parent)}>
38
+ <Portal id={parent.current}>
36
39
  <T
37
40
  is={arrows}
38
41
  name={entity}
@@ -43,7 +46,7 @@
43
46
  is={arrows.headMesh}
44
47
  bvh={{ enabled: false }}
45
48
  raycast={() => null}
46
- visible={events.visible}
49
+ visible={invisible.current}
47
50
  />
48
51
  <T
49
52
  is={arrows.shaftMesh}
@@ -42,6 +42,7 @@ Renders a Viam Frame object
42
42
  const entityColor = useTrait(() => entity, traits.Color)
43
43
  const entityPose = useTrait(() => entity, traits.Pose)
44
44
  const center = useTrait(() => entity, traits.Center)
45
+ const invisible = useTrait(() => entity, traits.Invisible)
45
46
 
46
47
  const events = useEntityEvents(() => entity)
47
48
 
@@ -72,7 +73,10 @@ Renders a Viam Frame object
72
73
  </script>
73
74
 
74
75
  <Portal id={parent.current}>
75
- <T is={group}>
76
+ <T
77
+ is={group}
78
+ visible={invisible.current !== true}
79
+ >
76
80
  <Mesh
77
81
  {entity}
78
82
  {color}
@@ -38,6 +38,7 @@
38
38
  const pose = useTrait(() => entity, traits.Pose)
39
39
  const gltfTrait = useTrait(() => entity, traits.GLTF)
40
40
  const scale = useTrait(() => entity, traits.Scale)
41
+ const invisible = useTrait(() => entity, traits.Invisible)
41
42
  const events = useEntityEvents(() => entity)
42
43
 
43
44
  const animationName = $derived(gltfTrait.current?.animationName)
@@ -88,6 +89,7 @@
88
89
  is={$gltf.scene as Object3D}
89
90
  scale={[scale.current?.x ?? 1, scale.current?.y ?? 1, scale.current?.z ?? 1]}
90
91
  name={entity}
92
+ visible={invisible.current !== true}
91
93
  {...events}
92
94
  {...rest}
93
95
  >
@@ -33,6 +33,7 @@ Renders a Viam Geometry object
33
33
  const name = useTrait(() => entity, traits.Name)
34
34
  const parent = useTrait(() => entity, traits.Parent)
35
35
  const center = useTrait(() => entity, traits.Center)
36
+ const invisible = useTrait(() => entity, traits.Invisible)
36
37
 
37
38
  const model = $derived.by(() => {
38
39
  if (!settings.current.renderArmModels.includes('model')) {
@@ -62,21 +63,23 @@ Renders a Viam Geometry object
62
63
  </script>
63
64
 
64
65
  <Portal id={parent.current}>
65
- {#if model}
66
- <T
67
- is={model}
68
- name={entity}
69
- {...events}
70
- />
71
- {/if}
72
-
73
- {#if settings.current.renderArmModels.includes('colliders') || !model}
74
- <Mesh
75
- {entity}
76
- center={center.current}
77
- {...events}
78
- >
79
- {@render children?.()}
80
- </Mesh>
81
- {/if}
66
+ <T.Group visible={invisible.current !== true}>
67
+ {#if model}
68
+ <T
69
+ is={model}
70
+ name={entity}
71
+ {...events}
72
+ />
73
+ {/if}
74
+
75
+ {#if settings.current.renderArmModels.includes('colliders') || !model}
76
+ <Mesh
77
+ {entity}
78
+ center={center.current}
79
+ {...events}
80
+ >
81
+ {@render children?.()}
82
+ </Mesh>
83
+ {/if}
84
+ </T.Group>
82
85
  </Portal>
@@ -38,6 +38,7 @@
38
38
  const opacity = useTrait(() => entity, traits.Opacity)
39
39
  const materialProps = useTrait(() => entity, traits.Material)
40
40
  const renderOrder = useTrait(() => entity, traits.RenderOrder)
41
+ const invisible = useTrait(() => entity, traits.Invisible)
41
42
 
42
43
  const events = useEntityEvents(() => entity)
43
44
 
@@ -60,6 +61,7 @@
60
61
  userData.name={name}
61
62
  raycast={meshBounds}
62
63
  renderOrder={renderOrder.current}
64
+ visible={invisible.current !== true}
63
65
  {...events}
64
66
  >
65
67
  <LineGeometry positions={linePositions.current} />
@@ -28,6 +28,7 @@
28
28
  const color = useTrait(() => entity, traits.Color)
29
29
  const opacity = useTrait(() => entity, traits.Opacity)
30
30
  const entityPointSize = useTrait(() => entity, traits.PointSize)
31
+ const invisible = useTrait(() => entity, traits.Invisible)
31
32
 
32
33
  const pointSize = $derived(
33
34
  entityPointSize.current ? entityPointSize.current * 0.001 : settings.current.pointSize
@@ -126,6 +127,7 @@
126
127
  is={points}
127
128
  name={entity}
128
129
  bvh={{ maxDepth: 40, maxLeafSize: 20 }}
130
+ visible={invisible.current !== true}
129
131
  {...events}
130
132
  >
131
133
  <T is={geometry.current} />
@@ -1,7 +1,6 @@
1
1
  import type { Entity } from 'koota';
2
2
  import { type IntersectionEvent } from '@threlte/extras';
3
3
  export declare const useEntityEvents: (entity: () => Entity | undefined) => {
4
- readonly visible: boolean;
5
4
  onpointerenter: (event: IntersectionEvent<MouseEvent>) => void;
6
5
  onpointermove: (event: IntersectionEvent<MouseEvent>) => void;
7
6
  onpointerleave: (event: IntersectionEvent<MouseEvent>) => void;
@@ -1,18 +1,15 @@
1
1
  import { useCursor } from '@threlte/extras';
2
2
  import { Vector2 } from 'three';
3
- import { traits } from '../../../ecs';
3
+ import { traits, useTrait } from '../../../ecs';
4
4
  import { useFocusedEntity, useSelectedEntity } from '../../../hooks/useSelection.svelte';
5
- import { useVisibility } from '../../../hooks/useVisibility.svelte';
6
5
  import { updateHoverInfo } from '../../../HoverUpdater.svelte';
7
6
  import { createPose, matrixToPose, poseToMatrix } from '../../../transform';
8
7
  export const useEntityEvents = (entity) => {
9
8
  const down = new Vector2();
10
9
  const selectedEntity = useSelectedEntity();
11
10
  const focusedEntity = useFocusedEntity();
12
- const visibility = useVisibility();
13
11
  const cursor = useCursor();
14
12
  const currentEntity = $derived(entity());
15
- const visible = $derived(currentEntity ? (visibility.get(currentEntity) ?? true) : true);
16
13
  const onpointerenter = (event) => {
17
14
  event.stopPropagation();
18
15
  cursor.onPointerEnter();
@@ -90,15 +87,13 @@ export const useEntityEvents = (entity) => {
90
87
  selectedEntity.set(currentEntity, event.instanceId ?? event.batchId);
91
88
  }
92
89
  };
90
+ const invisible = useTrait(entity, traits.Invisible);
93
91
  $effect(() => {
94
- if (!visible) {
92
+ if (invisible.current) {
95
93
  cursor.onPointerLeave();
96
94
  }
97
95
  });
98
96
  return {
99
- get visible() {
100
- return visible;
101
- },
102
97
  onpointerenter,
103
98
  onpointermove,
104
99
  onpointerleave,
@@ -5,9 +5,9 @@
5
5
  import { PressedKeys } from 'runed'
6
6
  import { MathUtils, Vector3 } from 'three'
7
7
 
8
+ import { traits } from '../ecs'
8
9
  import { useFocusedEntity, useSelectedEntity } from '../hooks/useSelection.svelte'
9
10
  import { useSettings } from '../hooks/useSettings.svelte'
10
- import { useVisibility } from '../hooks/useVisibility.svelte'
11
11
 
12
12
  interface Props {
13
13
  cameraControls: CameraControlsRef
@@ -21,7 +21,6 @@
21
21
  const entity = $derived(focusedEntity.current ?? selectedEntity.current)
22
22
 
23
23
  const settings = useSettings()
24
- const visibility = useVisibility()
25
24
 
26
25
  const keys = new PressedKeys()
27
26
  const meta = $derived(keys.has('meta'))
@@ -181,9 +180,12 @@
181
180
 
182
181
  event.stopImmediatePropagation()
183
182
 
184
- const visible = visibility.get(entity) ?? true
183
+ if (entity.has(traits.Invisible)) {
184
+ entity.remove(traits.Invisible)
185
+ } else {
186
+ entity.add(traits.Invisible)
187
+ }
185
188
 
186
- visibility.set(entity, !visible)
187
189
  return
188
190
  }
189
191
  }
@@ -22,7 +22,6 @@
22
22
  import { providePointclouds } from '../hooks/usePointclouds.svelte'
23
23
  import { provideResourceByName } from '../hooks/useResourceByName.svelte'
24
24
  import { provideSelection } from '../hooks/useSelection.svelte'
25
- import { provideVisibility } from '../hooks/useVisibility.svelte'
26
25
  import { provideWorldStates } from '../hooks/useWorldState.svelte'
27
26
 
28
27
  import { provideOrigin } from './xr/useOrigin.svelte'
@@ -38,7 +37,6 @@
38
37
 
39
38
  provideCameraControls(() => cameraPose)
40
39
  provideTransformControls()
41
- provideVisibility()
42
40
  provideMachineSettings()
43
41
  provideLogs()
44
42
 
@@ -64,7 +64,7 @@
64
64
  >
65
65
  <div
66
66
  {...api.getHeaderProps()}
67
- class="border-medium flex justify-between border-b p-2"
67
+ class="border-medium flex items-center justify-between border-b p-2"
68
68
  >
69
69
  <h3
70
70
  {...api.getTitleProps()}
@@ -10,6 +10,12 @@
10
10
  const logs = useLogs()
11
11
 
12
12
  const isOpen = new PersistedState('logs-is-open', false)
13
+
14
+ let levels = new PersistedState('logs-selected-levels', {
15
+ info: true,
16
+ warn: true,
17
+ error: true,
18
+ })
13
19
  </script>
14
20
 
15
21
  <Portal id="dashboard">
@@ -44,34 +50,95 @@
44
50
  title="Logs"
45
51
  bind:isOpen={isOpen.current}
46
52
  defaultSize={{ width: 240, height: 315 }}
53
+ resizable
47
54
  >
48
- <div class="flex h-70 flex-col gap-2 overflow-auto p-3 text-xs">
49
- {#each logs.current as log (log.uuid)}
50
- <div>
51
- <div class="flex flex-wrap items-center gap-1.5">
52
- <div
53
- class={[
54
- 'h-2 w-2 rounded-full',
55
- {
56
- 'bg-danger-dark': log.level === 'error',
57
- 'bg-amber-300': log.level === 'warn',
58
- 'bg-blue-400': log.level === 'info',
59
- },
60
- ]}
61
- ></div>
62
- <div class="text-subtle-2">{log.timestamp}</div>
63
- </div>
64
- <div>
65
- {#if log.count > 1}
66
- <span class="mr-1 rounded bg-green-700 px-1 py-0.5 text-xs text-white">
67
- {log.count}
68
- </span>
69
- {/if}
70
- {log.message}
71
- </div>
72
- </div>
73
- {:else}
74
- No logs
75
- {/each}
55
+ <div class="flex h-full flex-col">
56
+ <div class="flex gap-1 px-3 py-2">
57
+ <button
58
+ type="button"
59
+ class={[
60
+ 'chip border px-2',
61
+ {
62
+ 'border-danger-dark bg-danger-dark text-white hover:border-red-700 hover:bg-red-700':
63
+ levels.current.error,
64
+ 'bg-light hover:bg-ghost-light hover:border-light border-light text-subtle-1':
65
+ !levels.current.error,
66
+ },
67
+ ]}
68
+ onclick={() => {
69
+ levels.current.error = !levels.current.error
70
+ }}
71
+ >
72
+ error
73
+ </button>
74
+
75
+ <button
76
+ type="button"
77
+ class={[
78
+ 'chip border',
79
+ {
80
+ 'border-amber-400 bg-amber-400 text-white hover:border-amber-500 hover:bg-amber-500':
81
+ levels.current.warn,
82
+ 'bg-light hover:bg-ghost-light hover:border-light border-light text-subtle-1':
83
+ !levels.current.warn,
84
+ },
85
+ ]}
86
+ onclick={() => {
87
+ levels.current.warn = !levels.current.warn
88
+ }}
89
+ >
90
+ warn
91
+ </button>
92
+
93
+ <button
94
+ type="button"
95
+ class={[
96
+ 'chip border',
97
+ {
98
+ 'border-blue-400 bg-blue-400 text-white hover:border-blue-500 hover:bg-blue-500':
99
+ levels.current.info,
100
+ 'bg-light hover:bg-ghost-light hover:border-light border-light text-subtle-1':
101
+ !levels.current.info,
102
+ },
103
+ ]}
104
+ onclick={() => {
105
+ levels.current.info = !levels.current.info
106
+ }}
107
+ >
108
+ info
109
+ </button>
110
+ </div>
111
+
112
+ <div class="flex flex-col gap-2 overflow-auto px-3 pb-3 text-xs">
113
+ {#each logs.current as log (log.uuid)}
114
+ {#if levels.current[log.level]}
115
+ <div>
116
+ <div class="flex flex-wrap items-center gap-1.5">
117
+ <div
118
+ class={[
119
+ 'h-2 w-2 rounded-full',
120
+ {
121
+ 'bg-danger-dark': log.level === 'error',
122
+ 'bg-amber-300': log.level === 'warn',
123
+ 'bg-blue-400': log.level === 'info',
124
+ },
125
+ ]}
126
+ ></div>
127
+ <div class="text-subtle-2">{log.timestamp}</div>
128
+ </div>
129
+ <div>
130
+ {#if log.count > 1}
131
+ <span class="mr-1 rounded bg-green-700 px-1 py-0.5 text-xs text-white">
132
+ {log.count}
133
+ </span>
134
+ {/if}
135
+ {log.message}
136
+ </div>
137
+ </div>
138
+ {/if}
139
+ {:else}
140
+ No logs
141
+ {/each}
142
+ </div>
76
143
  </div>
77
144
  </FloatingPanel>
@@ -1,22 +1,21 @@
1
1
  <script lang="ts">
2
2
  import { normalizeProps, useMachine } from '@zag-js/svelte'
3
3
  import * as tree from '@zag-js/tree-view'
4
- import { ChevronRight, Eye, EyeOff } from 'lucide-svelte'
5
4
  import { VirtualList } from 'svelte-virtuallists'
6
5
  import { SvelteSet } from 'svelte/reactivity'
7
6
 
8
7
  import { traits } from '../../../ecs'
9
8
  import { useSelectedEntity } from '../../../hooks/useSelection.svelte'
10
- import { useVisibility } from '../../../hooks/useVisibility.svelte'
11
9
 
12
- import type { TreeNode } from './buildTree'
10
+ import type { TreeNode as TreeNodeType } from './buildTree'
11
+
12
+ import TreeNode from './TreeNode.svelte'
13
13
 
14
14
  const selected = useSelectedEntity()
15
- const visibility = useVisibility()
16
15
 
17
16
  interface Props {
18
- rootNode: TreeNode
19
- nodeMap: Record<string, TreeNode | undefined>
17
+ rootNode: TreeNodeType
18
+ nodeMap: Record<string, TreeNodeType | undefined>
20
19
  dragElement?: HTMLElement
21
20
  onSelectionChange?: (event: tree.SelectionChangeDetails) => void
22
21
  }
@@ -24,7 +23,7 @@
24
23
  let { rootNode, nodeMap, onSelectionChange, dragElement = $bindable() }: Props = $props()
25
24
 
26
25
  const collection = $derived(
27
- tree.collection<TreeNode>({
26
+ tree.collection<TreeNodeType>({
28
27
  nodeToValue: (node) => `${node.entity}`,
29
28
  nodeToString: (node) => node.entity.get(traits.Name) ?? '',
30
29
  rootNode,
@@ -74,108 +73,6 @@
74
73
  })
75
74
  </script>
76
75
 
77
- {#snippet treeNode({
78
- node,
79
- indexPath,
80
- api,
81
- }: {
82
- node: TreeNode
83
- indexPath: number[]
84
- api: tree.Api
85
- })}
86
- {@const nodeProps = { indexPath, node }}
87
- {@const nodeState = api.getNodeState(nodeProps)}
88
- {@const isVisible = visibility.get(node.entity) ?? true}
89
- {@const { selected } = nodeState}
90
-
91
- {#if nodeState.isBranch}
92
- {@const { expanded } = nodeState}
93
- {@const { children = [] } = node}
94
- <div
95
- {...api.getBranchProps(nodeProps)}
96
- class={[
97
- 'w-full',
98
- {
99
- 'text-disabled': !isVisible,
100
- 'bg-medium': selected,
101
- sticky: true,
102
- },
103
- ]}
104
- >
105
- <div {...api.getBranchControlProps(nodeProps)}>
106
- <span
107
- {...api.getBranchIndicatorProps(nodeProps)}
108
- class={{ 'rotate-90': expanded }}
109
- >
110
- <ChevronRight size={14} />
111
- </span>
112
- <span
113
- class="flex items-center overflow-hidden text-ellipsis"
114
- {...api.getBranchTextProps(nodeProps)}
115
- >
116
- {node.entity.get(traits.Name)}
117
- </span>
118
-
119
- <button
120
- class="text-gray-6"
121
- onclick={(event) => {
122
- event.stopPropagation()
123
- visibility.set(node.entity, !isVisible)
124
- }}
125
- >
126
- {#if isVisible}
127
- <Eye size={14} />
128
- {:else}
129
- <EyeOff size={14} />
130
- {/if}
131
- </button>
132
- </div>
133
- <div {...api.getBranchContentProps(nodeProps)}>
134
- <div {...api.getBranchIndentGuideProps(nodeProps)}></div>
135
-
136
- {#if children.length > 200}
137
- <VirtualList
138
- class="w-full"
139
- style="height:{Math.min(8, Math.max(children.length, 5)) * 32}px;"
140
- items={children}
141
- >
142
- {#snippet vl_slot({ index, item })}
143
- {@render treeNode({ node: item, indexPath: [...indexPath, Number(index)], api })}
144
- {/snippet}
145
- </VirtualList>
146
- {:else}
147
- {#each children as node, index (node.entity)}
148
- {@render treeNode({ node, indexPath: [...indexPath, index], api })}
149
- {/each}
150
- {/if}
151
- </div>
152
- </div>
153
- {:else}
154
- <div
155
- class={{ 'flex justify-between': true, 'text-disabled': !isVisible, 'bg-medium': selected }}
156
- {...api.getItemProps(nodeProps)}
157
- >
158
- <span class="flex items-center gap-1.5 overflow-hidden text-nowrap text-ellipsis">
159
- {node.entity.get(traits.Name)}
160
- </span>
161
-
162
- <button
163
- class="text-gray-6"
164
- onclick={(event) => {
165
- event.stopPropagation()
166
- visibility.set(node.entity, !isVisible)
167
- }}
168
- >
169
- {#if isVisible}
170
- <Eye size={14} />
171
- {:else}
172
- <EyeOff size={14} />
173
- {/if}
174
- </button>
175
- </div>
176
- {/if}
177
- {/snippet}
178
-
179
76
  <div
180
77
  {...api.getRootProps()}
181
78
  class="h-full overflow-auto text-xs"
@@ -188,49 +85,22 @@
188
85
  class="w-full"
189
86
  items={rootChildren}
190
87
  >
191
- {#snippet vl_slot({ index, item })}
192
- {@render treeNode({ node: item, indexPath: [Number(index)], api })}
88
+ {#snippet vl_slot({ index, item: node })}
89
+ <TreeNode
90
+ {node}
91
+ indexPath={[Number(index)]}
92
+ {api}
93
+ />
193
94
  {/snippet}
194
95
  </VirtualList>
195
96
  {:else}
196
97
  {#each rootChildren as node, index (node.entity)}
197
- {@render treeNode({ node, indexPath: [Number(index)], api })}
98
+ <TreeNode
99
+ {node}
100
+ indexPath={[Number(index)]}
101
+ {api}
102
+ />
198
103
  {/each}
199
104
  {/if}
200
105
  </div>
201
106
  </div>
202
-
203
- <style>
204
- :global(:root) {
205
- [data-scope='tree-view'][data-part='item'],
206
- [data-scope='tree-view'][data-part='branch-control'] {
207
- user-select: none;
208
- --padding-inline: 16px;
209
- padding-inline-start: calc(var(--depth) * var(--padding-inline));
210
- padding-inline-end: var(--padding-inline);
211
- display: flex;
212
- align-items: center;
213
- gap: 8px;
214
- min-height: 32px;
215
- }
216
-
217
- [data-scope='tree-view'][data-part='item-text'],
218
- [data-scope='tree-view'][data-part='branch-text'] {
219
- flex: 1;
220
- }
221
-
222
- [data-scope='tree-view'][data-part='branch-content'] {
223
- position: relative;
224
- isolation: isolate;
225
- }
226
-
227
- [data-scope='tree-view'][data-part='branch-indent-guide'] {
228
- position: absolute;
229
- content: '';
230
- border-left: 1px solid #eee;
231
- height: 100%;
232
- translate: calc(var(--depth) * 1.25rem);
233
- z-index: 1;
234
- }
235
- }
236
- </style>
@@ -1,8 +1,8 @@
1
1
  import * as tree from '@zag-js/tree-view';
2
- import type { TreeNode } from './buildTree';
2
+ import type { TreeNode as TreeNodeType } from './buildTree';
3
3
  interface Props {
4
- rootNode: TreeNode;
5
- nodeMap: Record<string, TreeNode | undefined>;
4
+ rootNode: TreeNodeType;
5
+ nodeMap: Record<string, TreeNodeType | undefined>;
6
6
  dragElement?: HTMLElement;
7
7
  onSelectionChange?: (event: tree.SelectionChangeDetails) => void;
8
8
  }
@@ -0,0 +1,169 @@
1
+ <script lang="ts">
2
+ import type { Api } from '@zag-js/tree-view'
3
+
4
+ import { ChevronRight, Eye, EyeOff } from 'lucide-svelte'
5
+ import { VirtualList } from 'svelte-virtuallists'
6
+
7
+ import { traits, useTrait } from '../../../ecs'
8
+
9
+ import type { TreeNode } from './buildTree'
10
+
11
+ import Self from './TreeNode.svelte'
12
+
13
+ interface Props {
14
+ node: TreeNode
15
+ indexPath: number[]
16
+ api: Api
17
+ }
18
+
19
+ let { node, indexPath, api }: Props = $props()
20
+
21
+ const name = useTrait(() => node.entity, traits.Name)
22
+ const invisible = useTrait(() => node.entity, traits.Invisible)
23
+
24
+ const nodeProps = $derived({ indexPath, node })
25
+ const nodeState = $derived(api.getNodeState(nodeProps))
26
+ </script>
27
+
28
+ {#if nodeState.isBranch}
29
+ {@const { expanded } = nodeState}
30
+ {@const { children = [] } = node}
31
+ <div
32
+ {...api.getBranchProps(nodeProps)}
33
+ class={[
34
+ 'w-full',
35
+ {
36
+ 'text-disabled': invisible.current,
37
+ 'bg-medium': nodeState.selected,
38
+ sticky: true,
39
+ },
40
+ ]}
41
+ >
42
+ <div {...api.getBranchControlProps(nodeProps)}>
43
+ <span
44
+ {...api.getBranchIndicatorProps(nodeProps)}
45
+ class={{ 'rotate-90': expanded }}
46
+ >
47
+ <ChevronRight size={14} />
48
+ </span>
49
+ <span
50
+ class="flex items-center overflow-hidden text-ellipsis"
51
+ {...api.getBranchTextProps(nodeProps)}
52
+ >
53
+ {name.current}
54
+ </span>
55
+
56
+ <button
57
+ class="text-gray-6"
58
+ onclick={(event) => {
59
+ event.stopPropagation()
60
+
61
+ if (node.entity.has(traits.Invisible)) {
62
+ node.entity.remove(traits.Invisible)
63
+ } else {
64
+ node.entity.add(traits.Invisible)
65
+ }
66
+ }}
67
+ >
68
+ {#if invisible.current}
69
+ <EyeOff size={14} />
70
+ {:else}
71
+ <Eye size={14} />
72
+ {/if}
73
+ </button>
74
+ </div>
75
+ <div {...api.getBranchContentProps(nodeProps)}>
76
+ <div {...api.getBranchIndentGuideProps(nodeProps)}></div>
77
+
78
+ {#if children.length > 200}
79
+ <VirtualList
80
+ class="w-full"
81
+ style="height:{Math.min(8, Math.max(children.length, 5)) * 32}px;"
82
+ items={children}
83
+ >
84
+ {#snippet vl_slot({ index, item: node })}
85
+ <Self
86
+ {node}
87
+ indexPath={[...indexPath, Number(index)]}
88
+ {api}
89
+ />
90
+ {/snippet}
91
+ </VirtualList>
92
+ {:else}
93
+ {#each children as node, index (node.entity)}
94
+ <Self
95
+ {node}
96
+ indexPath={[...indexPath, Number(index)]}
97
+ {api}
98
+ />
99
+ {/each}
100
+ {/if}
101
+ </div>
102
+ </div>
103
+ {:else}
104
+ <div
105
+ class={{
106
+ 'flex justify-between': true,
107
+ 'text-disabled': invisible.current,
108
+ 'bg-medium': nodeState.selected,
109
+ }}
110
+ {...api.getItemProps(nodeProps)}
111
+ >
112
+ <span class="flex items-center gap-1.5 overflow-hidden text-nowrap text-ellipsis">
113
+ {node.entity.get(traits.Name)}
114
+ </span>
115
+
116
+ <button
117
+ class="text-gray-6"
118
+ onclick={(event) => {
119
+ event.stopPropagation()
120
+ if (node.entity.has(traits.Invisible)) {
121
+ node.entity.remove(traits.Invisible)
122
+ } else {
123
+ node.entity.add(traits.Invisible)
124
+ }
125
+ }}
126
+ >
127
+ {#if invisible.current}
128
+ <EyeOff size={14} />
129
+ {:else}
130
+ <Eye size={14} />
131
+ {/if}
132
+ </button>
133
+ </div>
134
+ {/if}
135
+
136
+ <style>
137
+ :global(:root) {
138
+ [data-scope='tree-view'][data-part='item'],
139
+ [data-scope='tree-view'][data-part='branch-control'] {
140
+ user-select: none;
141
+ --padding-inline: 16px;
142
+ padding-inline-start: calc(var(--depth) * var(--padding-inline));
143
+ padding-inline-end: var(--padding-inline);
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 8px;
147
+ min-height: 32px;
148
+ }
149
+
150
+ [data-scope='tree-view'][data-part='item-text'],
151
+ [data-scope='tree-view'][data-part='branch-text'] {
152
+ flex: 1;
153
+ }
154
+
155
+ [data-scope='tree-view'][data-part='branch-content'] {
156
+ position: relative;
157
+ isolation: isolate;
158
+ }
159
+
160
+ [data-scope='tree-view'][data-part='branch-indent-guide'] {
161
+ position: absolute;
162
+ content: '';
163
+ border-left: 1px solid #eee;
164
+ height: 100%;
165
+ translate: calc(var(--depth) * 1.25rem);
166
+ z-index: 1;
167
+ }
168
+ }
169
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Api } from '@zag-js/tree-view';
2
+ import type { TreeNode } from './buildTree';
3
+ interface Props {
4
+ node: TreeNode;
5
+ indexPath: number[];
6
+ api: Api;
7
+ }
8
+ declare const TreeNode: import("svelte").Component<Props, {}, "">;
9
+ type TreeNode = ReturnType<typeof TreeNode>;
10
+ export default TreeNode;
@@ -51,7 +51,7 @@
51
51
  })
52
52
  leftPad.trigger.on('up', () => (dragging = false))
53
53
 
54
- const dragTask = useTask(
54
+ useTask(
55
55
  () => {
56
56
  if (!$left || !rigidBody) return
57
57
 
@@ -62,11 +62,11 @@
62
62
  rigidBody.setNextKinematicTranslation({ x: position.x, y: position.y, z: position.z })
63
63
  },
64
64
  {
65
- autoStart: false,
65
+ running: () => dragging,
66
66
  }
67
67
  )
68
68
 
69
- const rotateTask = useTask(
69
+ useTask(
70
70
  () => {
71
71
  if (!$right || !rigidBody) return
72
72
 
@@ -80,24 +80,8 @@
80
80
 
81
81
  rigidBody.setNextKinematicRotation(quaternion.setFromEuler(euler))
82
82
  },
83
- { autoStart: false }
83
+ { running: () => rotating }
84
84
  )
85
-
86
- $effect.pre(() => {
87
- if (dragging) {
88
- dragTask.start()
89
- } else {
90
- dragTask.stop()
91
- }
92
- })
93
-
94
- $effect.pre(() => {
95
- if (rotating) {
96
- rotateTask.start()
97
- } else {
98
- rotateTask.stop()
99
- }
100
- })
101
85
  </script>
102
86
 
103
87
  <T
@@ -18,13 +18,7 @@
18
18
  const settings = useSettings()
19
19
  const armClient = useArmClient()
20
20
  const partID = usePartID()
21
-
22
- let resources: ReturnType<typeof useResourceNames> | undefined
23
- try {
24
- resources = useResourceNames(() => partID.current)
25
- } catch (error) {
26
- console.warn('Failed to get resources, robot may not be connected yet:', error)
27
- }
21
+ let resources = useResourceNames(() => partID.current)
28
22
 
29
23
  // Get available arms and grippers
30
24
  const armNames = $derived(armClient.names || [])
@@ -1,2 +1,9 @@
1
+ import { type Object3D, type Quaternion, type Vector3 } from 'three';
2
+ interface Context {
3
+ createAnchor: (position: Vector3, orientation: Quaternion) => Promise<XRAnchor> | undefined;
4
+ bindAnchorObject: (anchor: XRAnchor, object: Object3D) => void;
5
+ unbindAnchorObject: (anchor: XRAnchor) => void;
6
+ }
1
7
  export declare const provideAnchors: () => void;
2
- export declare const useAnchors: () => void;
8
+ export declare const useAnchors: () => Context;
9
+ export {};
@@ -1,28 +1,32 @@
1
- import { useTask, useThrelte, watch } from '@threlte/core';
1
+ import { useTask, useThrelte } from '@threlte/core';
2
2
  import { useXR } from '@threlte/xr';
3
3
  import { getContext, setContext } from 'svelte';
4
- import { Matrix4 } from 'three';
4
+ import { fromStore } from 'svelte/store';
5
+ import {} from 'three';
5
6
  const key = Symbol('anchors-context');
6
7
  export const provideAnchors = () => {
7
- const matrix4 = new Matrix4();
8
8
  const { renderer } = useThrelte();
9
- const { isPresenting } = useXR();
9
+ const { isPresenting: isPresentingStore } = useXR();
10
+ const isPresenting = fromStore(isPresentingStore);
10
11
  const map = new WeakMap();
11
- let space = renderer.xr.getReferenceSpace();
12
- const createAnchor = (position, orientation) => {
13
- space ??= renderer.xr.getReferenceSpace();
14
- if (space === null)
12
+ const createAnchor = (position, quaternion) => {
13
+ const space = renderer.xr.getReferenceSpace();
14
+ const frame = renderer.xr.getFrame();
15
+ if (!space || !frame)
15
16
  return;
16
- const pose = new XRRigidTransform(position, orientation);
17
- return renderer.xr.getFrame().createAnchor?.(pose, space);
17
+ const pose = new XRRigidTransform({ x: position.x, y: position.y, z: position.z }, { x: quaternion.x, y: quaternion.y, z: quaternion.z, w: quaternion.w });
18
+ return frame.createAnchor?.(pose, space);
18
19
  };
19
- const { start, stop } = useTask(() => {
20
- space ??= renderer.xr.getReferenceSpace();
21
- if (!space) {
22
- return;
23
- }
20
+ const bindAnchorObject = (anchor, object) => {
21
+ map.set(anchor, object);
22
+ };
23
+ const unbindAnchorObject = (anchor) => {
24
+ map.delete(anchor);
25
+ };
26
+ useTask(() => {
27
+ const space = renderer.xr.getReferenceSpace();
24
28
  const frame = renderer.xr.getFrame();
25
- if (!frame.trackedAnchors) {
29
+ if (!space || !frame?.trackedAnchors) {
26
30
  return;
27
31
  }
28
32
  for (const anchor of frame.trackedAnchors) {
@@ -34,22 +38,18 @@ export const provideAnchors = () => {
34
38
  if (!anchorPose) {
35
39
  continue;
36
40
  }
37
- matrix4.fromArray(anchorPose.transform.matrix);
38
- object3d.applyMatrix4(matrix4);
39
- }
40
- });
41
- watch(isPresenting, ($isPresenting) => {
42
- if ($isPresenting) {
43
- start();
44
- }
45
- else {
46
- stop();
41
+ object3d.matrixAutoUpdate = false;
42
+ object3d.matrix.fromArray(anchorPose.transform.matrix);
47
43
  }
44
+ }, {
45
+ running: () => isPresenting.current,
48
46
  });
49
47
  setContext(key, {
50
48
  createAnchor,
49
+ bindAnchorObject,
50
+ unbindAnchorObject,
51
51
  });
52
52
  };
53
53
  export const useAnchors = () => {
54
- getContext(key);
54
+ return getContext(key);
55
55
  };
@@ -51,6 +51,7 @@ export declare const WorldPose: import("koota").Trait<{
51
51
  theta: number;
52
52
  }>;
53
53
  export declare const Hovered: import("koota").Trait<() => boolean>;
54
+ export declare const Invisible: import("koota").Trait<() => boolean>;
54
55
  /**
55
56
  * Represents that an entity is composed of many instances, so that the treeview and
56
57
  * details panel may display all instances
@@ -28,6 +28,7 @@ export const WorldPose = trait({
28
28
  theta: 0,
29
29
  });
30
30
  export const Hovered = trait(() => true);
31
+ export const Invisible = trait(() => true);
31
32
  /**
32
33
  * Represents that an entity is composed of many instances, so that the treeview and
33
34
  * details panel may display all instances
@@ -10,14 +10,14 @@ export const provideFramelessComponents = () => {
10
10
  const partComponentsWIthNoFrame = components
11
11
  ?.filter((component) => component.frame === undefined)
12
12
  .map((component) => component.name) ?? [];
13
- const fragmentComponentsWithNoFrame = [];
13
+ const fragmentComponentsWithNoFrame = new Set(partComponentsWIthNoFrame);
14
14
  for (const fragmentComponentName of Object.keys(partConfig.componentNameToFragmentId)) {
15
15
  if (frames.current.some((frame) => frame.referenceFrame === fragmentComponentName)) {
16
16
  continue;
17
17
  }
18
- fragmentComponentsWithNoFrame.push(fragmentComponentName);
18
+ fragmentComponentsWithNoFrame.add(fragmentComponentName);
19
19
  }
20
- return [...partComponentsWIthNoFrame, ...fragmentComponentsWithNoFrame];
20
+ return [...fragmentComponentsWithNoFrame];
21
21
  });
22
22
  setContext(key, {
23
23
  get current() {
@@ -20,8 +20,10 @@ export const provideFrames = (partID) => {
20
20
  const connectionStatus = useConnectionStatus(partID);
21
21
  const machineStatus = useMachineStatus(partID);
22
22
  const logs = useLogs();
23
+ let didRecentlyEdit = $state(false);
23
24
  const isEditMode = $derived(environment.current.viewerMode === 'edit');
24
25
  const query = createRobotQuery(client, 'frameSystemConfig', () => ({
26
+ refetchOnWindowFocus: false,
25
27
  enabled: partID() !== '' && !isEditMode,
26
28
  }));
27
29
  const revision = $derived(machineStatus.current?.config?.revision);
@@ -41,7 +43,8 @@ export const provideFrames = (partID) => {
41
43
  }
42
44
  frames[frame.referenceFrame] = frame;
43
45
  }
44
- if (isEditMode || connectionStatus.current === MachineConnectionEvent.DISCONNECTED) {
46
+ // Let config frames take priority if the user has made edits
47
+ if (didRecentlyEdit || connectionStatus.current === MachineConnectionEvent.DISCONNECTED) {
45
48
  const mergedFrames = {
46
49
  ...frames,
47
50
  ...configFrames.current,
@@ -56,9 +59,8 @@ export const provideFrames = (partID) => {
56
59
  return mergedFrames;
57
60
  }
58
61
  /**
59
- * If we're not in edit mode and we have a robot connection,
62
+ * If we haven't edited and we have a robot connection,
60
63
  * we only use frames reported by the machine
61
- *
62
64
  */
63
65
  return frames;
64
66
  });
@@ -69,6 +71,11 @@ export const provideFrames = (partID) => {
69
71
  untrack(() => query.refetch());
70
72
  }
71
73
  });
74
+ $effect(() => {
75
+ if (isEditMode) {
76
+ didRecentlyEdit = true;
77
+ }
78
+ });
72
79
  $effect.pre(() => {
73
80
  const currentResourcesByName = resourceByName.current;
74
81
  const currentPartID = partID();
@@ -136,11 +143,19 @@ export const provideFrames = (partID) => {
136
143
  if (!active[entityKey]) {
137
144
  entity?.destroy();
138
145
  entities.delete(entityKey);
139
- continue;
140
146
  }
141
147
  }
142
148
  });
143
149
  });
150
+ // Clear all entities on unmount
151
+ $effect(() => {
152
+ return () => {
153
+ for (const [, entity] of entities) {
154
+ entity?.destroy();
155
+ }
156
+ entities.clear();
157
+ };
158
+ });
144
159
  setContext(key, {
145
160
  get current() {
146
161
  return current;
@@ -117,14 +117,24 @@ export const provideGeometries = (partID) => {
117
117
  if (!activeQueryKeys.has(queryKey)) {
118
118
  for (const key of keys) {
119
119
  const entity = entities.get(key);
120
- if (entity && world.has(entity))
120
+ if (entity && world.has(entity)) {
121
121
  entity.destroy();
122
+ }
122
123
  entities.delete(key);
123
124
  }
124
125
  queryEntityKeys.delete(queryKey);
125
126
  }
126
127
  }
127
128
  });
129
+ // Clear all entities on unmount
130
+ $effect(() => {
131
+ return () => {
132
+ for (const [, entity] of entities) {
133
+ entity?.destroy();
134
+ }
135
+ entities.clear();
136
+ };
137
+ });
128
138
  setContext(key, {
129
139
  refetch() {
130
140
  for (const [, query] of queries) {
@@ -194,6 +194,14 @@ export const providePointcloudObjects = (partID) => {
194
194
  }
195
195
  }
196
196
  });
197
+ $effect(() => {
198
+ return () => {
199
+ for (const [, entity] of entities) {
200
+ entity.destroy();
201
+ }
202
+ entities.clear();
203
+ };
204
+ });
197
205
  setContext(key, {
198
206
  refetch() {
199
207
  for (const [, query] of queries) {
@@ -133,6 +133,14 @@ export const providePointclouds = (partID) => {
133
133
  }
134
134
  }
135
135
  });
136
+ $effect(() => {
137
+ return () => {
138
+ for (const [, entity] of entities) {
139
+ entity.destroy();
140
+ }
141
+ entities.clear();
142
+ };
143
+ });
136
144
  setContext(key, {
137
145
  refetch() {
138
146
  for (const [, query] of queries) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.15.4",
3
+ "version": "1.15.6",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -38,7 +38,6 @@
38
38
  "@viamrobotics/prime-core": "0.1.5",
39
39
  "@viamrobotics/sdk": "0.58.0",
40
40
  "@viamrobotics/svelte-sdk": "1.0.1",
41
- "@vitejs/plugin-basic-ssl": "2.1.0",
42
41
  "@vitest/coverage-v8": "^3.2.4",
43
42
  "@zag-js/collapsible": "1.22.1",
44
43
  "@zag-js/floating-panel": "1.22.1",
@@ -63,7 +62,7 @@
63
62
  "prettier-plugin-tailwindcss": "0.6.14",
64
63
  "publint": "0.3.12",
65
64
  "runed": "0.31.1",
66
- "svelte": "5.53.13",
65
+ "svelte": "5.55.0",
67
66
  "svelte-check": "4.4.5",
68
67
  "svelte-virtuallists": "1.4.2",
69
68
  "tailwindcss": "4.1.13",
@@ -145,6 +144,7 @@
145
144
  },
146
145
  "scripts": {
147
146
  "dev": "tsx server/check-bun && bun run server/server.ts",
147
+ "dev:https": "vite dev -- --https",
148
148
  "build": "vite build && npm run prepack",
149
149
  "build:workers": "node scripts/build-workers.js",
150
150
  "preview": "vite preview",
@@ -1,6 +0,0 @@
1
- import type { Entity } from 'koota';
2
- import { SvelteMap } from 'svelte/reactivity';
3
- type Context = SvelteMap<Entity, boolean>;
4
- export declare const provideVisibility: () => void;
5
- export declare const useVisibility: () => Context;
6
- export {};
@@ -1,10 +0,0 @@
1
- import { getContext, setContext } from 'svelte';
2
- import { SvelteMap } from 'svelte/reactivity';
3
- const key = Symbol('object-visibility-context');
4
- export const provideVisibility = () => {
5
- const map = new SvelteMap();
6
- setContext(key, map);
7
- };
8
- export const useVisibility = () => {
9
- return getContext(key);
10
- };