Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thick-paws-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@viamrobotics/motion-tools": patch
---

Update Treeview to use entity relations
18 changes: 9 additions & 9 deletions src/lib/components/overlay/left-pane/Tree.svelte
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
<script lang="ts">
import type { Entity } from 'koota'

import { normalizeProps, useMachine } from '@zag-js/svelte'
import * as tree from '@zag-js/tree-view'
import { VirtualList } from 'svelte-virtuallists'
import { SvelteSet } from 'svelte/reactivity'

import { traits } from '$lib/ecs'
import { relations, traits } from '$lib/ecs'
import { useSelectedEntity } from '$lib/hooks/useSelection.svelte'

import type { TreeNode as TreeNodeType } from './buildTree'
import type { TreeNode as TreeNodeType } from './useTree.svelte'

import TreeNode from './TreeNode.svelte'

const selected = useSelectedEntity()

interface Props {
rootNode: TreeNodeType
nodeMap: Record<string, TreeNodeType | undefined>
dragElement?: HTMLElement
onSelectionChange?: (event: tree.SelectionChangeDetails) => void
}

let { rootNode, nodeMap, onSelectionChange, dragElement = $bindable() }: Props = $props()
let { rootNode, onSelectionChange, dragElement = $bindable() }: Props = $props()

const collection = $derived(
tree.collection<TreeNodeType>({
Expand All @@ -34,11 +35,10 @@
const expandedValues = new SvelteSet<string>()

$effect(() => {
let name = selected.current?.get(traits.Name)
let node = nodeMap[name ?? '']
while (node) {
expandedValues.add(`${node.entity}`)
node = node.parent
let entity: Entity | undefined = selected.current
while (entity) {
expandedValues.add(`${entity}`)
entity = entity.targetFor(relations.ChildOf)
}
})

Expand Down
19 changes: 4 additions & 15 deletions src/lib/components/overlay/left-pane/TreeContainer.svelte
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
<script lang="ts">
import { type Entity, IsExcluded } from 'koota'

import { traits, useQuery, useWorld } from '$lib/ecs'
import { useFrames } from '$lib/hooks/useFrames.svelte'
import { traits, useWorld } from '$lib/ecs'
import { useSelectedEntity } from '$lib/hooks/useSelection.svelte'

import FloatingPanel from '../FloatingPanel.svelte'
import { buildTreeNodes, type TreeNode } from './buildTree'
import Tree from './Tree.svelte'
import { provideTreeExpandedContext } from './useExpanded.svelte'
import { type TreeNode, useTree } from './useTree.svelte'

provideTreeExpandedContext()

const selectedEntity = useSelectedEntity()

const frames = useFrames()
const world = useWorld()

const worldEntity = world.spawn(IsExcluded, traits.Name('World'))

const allEntities = useQuery(traits.Name)

const { rootNodes, nodeMap } = $derived.by(() => {
// This ensures the tree rebuilds when frame parent relationships change
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
frames.current
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removes the dependency of frames for rebuilding the tree. Tree rebuilds should now be exactly based on entity changes. More performant + fewer edge case bugs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vijayvuyyuru FYI since you encountered this bug

return buildTreeNodes(allEntities.current)
})
const tree = useTree()

const rootNode = $derived<TreeNode>({
entity: worldEntity,
children: rootNodes,
children: tree.current,
})
</script>

Expand All @@ -44,7 +34,6 @@
>
<Tree
{rootNode}
{nodeMap}
onSelectionChange={(event) => {
const value = event.selectedValue[0]

Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/overlay/left-pane/TreeNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { traits, useTrait } from '$lib/ecs'

import type { TreeNode } from './buildTree'
import type { TreeNode } from './useTree.svelte'

import Self from './TreeNode.svelte'

Expand Down
68 changes: 0 additions & 68 deletions src/lib/components/overlay/left-pane/buildTree.ts

This file was deleted.

80 changes: 80 additions & 0 deletions src/lib/components/overlay/left-pane/useTree.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { type Entity, Not, type World } from 'koota'
import { createSubscriber } from 'svelte/reactivity'

import { relations, traits, useWorld } from '$lib/ecs'

export interface TreeNode {
entity: Entity
children?: TreeNode[]
}

const compareByName = (a: Entity, b: Entity): number =>
(a.get(traits.Name) ?? '').localeCompare(b.get(traits.Name) ?? '')

const buildTree = (world: World): TreeNode[] => {
const walk = (entity: Entity): TreeNode => {
const node: TreeNode = { entity }

const children = world.query(relations.ChildOf(entity)).toSorted(compareByName)
if (children.length > 0) {
node.children = children.map((child) => walk(child))
}

return node
}

const rootEntities: Entity[] = []
for (const entity of world.query(traits.Name, Not(traits.Orphan))) {
if (entity.targetFor(relations.ChildOf)) continue
rootEntities.push(entity)
}
rootEntities.sort(compareByName)

return rootEntities.map((entity) => walk(entity))
}

/**
* Reactive top-down tree built from `ChildOf` relations. Rebuilds when any
* named entity is added, removed, renamed, or gains/loses a `ChildOf` or
* `Orphan` edge. Orphans are hidden from the tree — they reappear once
* `provideHierarchy` resolves them to a real `ChildOf` parent.
*/
export const useTree = (): { readonly current: TreeNode[] } => {
const world = useWorld()

let cached: TreeNode[] | undefined
let dirty = true

const subscribe = createSubscriber((update) => {
const invalidate = () => {
dirty = true
update()
}

const unsubs = [
world.onAdd(traits.Name, invalidate),
world.onRemove(traits.Name, invalidate),
world.onChange(traits.Name, invalidate),
world.onAdd(relations.ChildOf, invalidate),
world.onChange(relations.ChildOf, invalidate),
world.onRemove(relations.ChildOf, invalidate),
world.onAdd(traits.Orphan, invalidate),
world.onRemove(traits.Orphan, invalidate),
]

return () => {
for (const unsub of unsubs) unsub()
}
})

return {
get current() {
subscribe()
if (dirty || !cached) {
cached = buildTree(world)
dirty = false
}
return cached
},
}
}
Loading