diff --git a/editor/components/SceneGraph.vue b/editor/components/SceneGraph.vue index 60bb9f1..4991ee5 100644 --- a/editor/components/SceneGraph.vue +++ b/editor/components/SceneGraph.vue @@ -7,6 +7,7 @@ import '@vue-flow/core/dist/style.css' import '@vue-flow/controls/dist/style.css' import '@vue-flow/core/dist/theme-default.css' import type { Connection } from '@vue-flow/core' +import { computePositions } from '../composables/useLayout' const props = defineProps<{ sceneNodes: { id: string; label: string }[] @@ -25,20 +26,24 @@ const edges = ref([]) const { onNodeClick, onConnect, fitView } = useVueFlow() function makeNodes() { - return props.sceneNodes.map((n, i) => ({ - id: n.id, - type: 'default', - position: { x: (i % 4) * 220 + 50, y: Math.floor(i / 4) * 120 + 50 }, - data: { label: n.id === props.startScene ? `\u25b6 ${n.label}` : n.label }, - style: n.id === props.startScene - ? { background: '#1b5e20', color: '#fff', borderColor: '#388e3c' } - : n.id === props.selectedNodeId - ? { background: '#1565c0', color: '#fff', borderColor: '#1976d2' } - : {}, - sourcePosition: 'right' as const, - targetPosition: 'left' as const, - connectable: true, - })) + 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, + type: 'default', + position: pos, + data: { label: n.id === props.startScene ? `\u25b6 ${n.label}` : n.label }, + style: n.id === props.startScene + ? { background: '#1b5e20', color: '#fff', borderColor: '#388e3c' } + : n.id === props.selectedNodeId + ? { background: '#1565c0', color: '#fff', borderColor: '#1976d2' } + : {}, + sourcePosition: 'right' as const, + targetPosition: 'left' as const, + connectable: true, + } + }) } function makeEdges() { diff --git a/editor/composables/useLayout.ts b/editor/composables/useLayout.ts new file mode 100644 index 0000000..878e0b5 --- /dev/null +++ b/editor/composables/useLayout.ts @@ -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 { + const positions = new Map() + + const adj = new Map() + 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() + const visited = new Set() + 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() + 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 +}