feat: BFS-based left-to-right tree layout for scene graph
This commit is contained in:
@@ -7,6 +7,7 @@ import '@vue-flow/core/dist/style.css'
|
|||||||
import '@vue-flow/controls/dist/style.css'
|
import '@vue-flow/controls/dist/style.css'
|
||||||
import '@vue-flow/core/dist/theme-default.css'
|
import '@vue-flow/core/dist/theme-default.css'
|
||||||
import type { Connection } from '@vue-flow/core'
|
import type { Connection } from '@vue-flow/core'
|
||||||
|
import { computePositions } from '../composables/useLayout'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
sceneNodes: { id: string; label: string }[]
|
sceneNodes: { id: string; label: string }[]
|
||||||
@@ -25,10 +26,13 @@ const edges = ref<any[]>([])
|
|||||||
const { onNodeClick, onConnect, fitView } = useVueFlow()
|
const { onNodeClick, onConnect, fitView } = useVueFlow()
|
||||||
|
|
||||||
function makeNodes() {
|
function makeNodes() {
|
||||||
return props.sceneNodes.map((n, i) => ({
|
const positions = computePositions(props.sceneNodes, props.sceneEdges, props.startScene)
|
||||||
|
return props.sceneNodes.map((n) => {
|
||||||
|
const pos = positions.get(n.id) ?? { x: 0, y: 0 }
|
||||||
|
return {
|
||||||
id: n.id,
|
id: n.id,
|
||||||
type: 'default',
|
type: 'default',
|
||||||
position: { x: (i % 4) * 220 + 50, y: Math.floor(i / 4) * 120 + 50 },
|
position: pos,
|
||||||
data: { label: n.id === props.startScene ? `\u25b6 ${n.label}` : n.label },
|
data: { label: n.id === props.startScene ? `\u25b6 ${n.label}` : n.label },
|
||||||
style: n.id === props.startScene
|
style: n.id === props.startScene
|
||||||
? { background: '#1b5e20', color: '#fff', borderColor: '#388e3c' }
|
? { background: '#1b5e20', color: '#fff', borderColor: '#388e3c' }
|
||||||
@@ -38,7 +42,8 @@ function makeNodes() {
|
|||||||
sourcePosition: 'right' as const,
|
sourcePosition: 'right' as const,
|
||||||
targetPosition: 'left' as const,
|
targetPosition: 'left' as const,
|
||||||
connectable: true,
|
connectable: true,
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeEdges() {
|
function makeEdges() {
|
||||||
|
|||||||
81
editor/composables/useLayout.ts
Normal file
81
editor/composables/useLayout.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
interface NodeInfo {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EdgeInfo {
|
||||||
|
source: string
|
||||||
|
target: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const H_GAP = 300
|
||||||
|
const V_GAP = 140
|
||||||
|
const PAD = 60
|
||||||
|
|
||||||
|
export function computePositions(
|
||||||
|
nodes: NodeInfo[],
|
||||||
|
edges: EdgeInfo[],
|
||||||
|
startScene: string,
|
||||||
|
): Map<string, { x: number; y: number }> {
|
||||||
|
const positions = new Map<string, { x: number; y: number }>()
|
||||||
|
|
||||||
|
const adj = new Map<string, string[]>()
|
||||||
|
for (const n of nodes) adj.set(n.id, [])
|
||||||
|
for (const e of edges) {
|
||||||
|
const list = adj.get(e.source)
|
||||||
|
if (list) list.push(e.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = new Map<string, number>()
|
||||||
|
const visited = new Set<string>()
|
||||||
|
const queue: string[] = []
|
||||||
|
|
||||||
|
if (startScene && adj.has(startScene)) {
|
||||||
|
level.set(startScene, 0)
|
||||||
|
visited.add(startScene)
|
||||||
|
queue.push(startScene)
|
||||||
|
}
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const id = queue.shift()!
|
||||||
|
const cur = level.get(id)!
|
||||||
|
for (const t of adj.get(id)!) {
|
||||||
|
if (!visited.has(t)) {
|
||||||
|
visited.add(t)
|
||||||
|
level.set(t, cur + 1)
|
||||||
|
queue.push(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxLevel = -1
|
||||||
|
for (const l of level.values()) maxLevel = Math.max(maxLevel, l)
|
||||||
|
|
||||||
|
for (const n of nodes) {
|
||||||
|
if (!level.has(n.id)) {
|
||||||
|
maxLevel++
|
||||||
|
level.set(n.id, maxLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const byLevel = new Map<number, string[]>()
|
||||||
|
for (const [id, lv] of level) {
|
||||||
|
const arr = byLevel.get(lv) || []
|
||||||
|
arr.push(id)
|
||||||
|
byLevel.set(lv, arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const levels = [...byLevel.entries()].sort((a, b) => a[0] - b[0])
|
||||||
|
|
||||||
|
for (const [lv, ids] of levels) {
|
||||||
|
ids.sort()
|
||||||
|
const count = ids.length
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = lv * H_GAP + PAD
|
||||||
|
const y = i * V_GAP + PAD
|
||||||
|
positions.set(ids[i], { x, y })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return positions
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user