120 lines
3.0 KiB
Vue
120 lines
3.0 KiB
Vue
<script setup lang="ts">
|
|
import { watch, onMounted, nextTick } from 'vue'
|
|
import { VueFlow, useVueFlow } from '@vue-flow/core'
|
|
import { Background } from '@vue-flow/background'
|
|
import { Controls } from '@vue-flow/controls'
|
|
import '@vue-flow/core/dist/style.css'
|
|
import '@vue-flow/controls/dist/style.css'
|
|
import '@vue-flow/core/dist/theme-default.css'
|
|
import type { Node, Edge, Connection } from '@vue-flow/core'
|
|
|
|
const props = defineProps<{
|
|
sceneNodes: { id: string; label: string }[]
|
|
sceneEdges: { id: string; source: string; target: string; label?: string }[]
|
|
startScene: string
|
|
selectedNodeId: string | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
selectNode: [id: string]
|
|
addNode: []
|
|
addEdge: [source: string, target: string]
|
|
}>()
|
|
|
|
const { nodes, edges, onNodeClick, onConnect, fitView } = useVueFlow()
|
|
|
|
function buildNodes() {
|
|
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 ? `▶ ${n.label}` : n.label },
|
|
style: n.id === props.startScene
|
|
? 'background:#1b5e20; color:#fff; border-color:#388e3c'
|
|
: n.id === props.selectedNodeId
|
|
? 'background:#1565c0; color:#fff; border-color:#1976d2'
|
|
: '',
|
|
sourcePosition: 'right' as const,
|
|
targetPosition: 'left' as const,
|
|
connectable: true,
|
|
}))
|
|
}
|
|
|
|
function buildEdges() {
|
|
return props.sceneEdges.map((e) => ({
|
|
id: e.id,
|
|
source: e.source,
|
|
target: e.target,
|
|
label: e.label || '',
|
|
animated: true,
|
|
style: { stroke: '#4fc3f7' },
|
|
labelStyle: { fill: '#aaa', fontWeight: 400, fontSize: 11 },
|
|
labelBgStyle: { fill: 'rgba(0,0,0,0.7)' },
|
|
}))
|
|
}
|
|
|
|
let lastNodesLen = 0
|
|
let lastEdgesLen = 0
|
|
|
|
async function structuralRebuild() {
|
|
edges.value = []
|
|
await nextTick()
|
|
nodes.value = buildNodes()
|
|
await nextTick()
|
|
edges.value = buildEdges()
|
|
lastNodesLen = props.sceneNodes.length
|
|
lastEdgesLen = props.sceneEdges.length
|
|
}
|
|
|
|
function styleOnlyRebuild() {
|
|
nodes.value = buildNodes()
|
|
edges.value = buildEdges()
|
|
}
|
|
|
|
watch(() => [props.sceneNodes, props.sceneEdges, props.startScene, props.selectedNodeId], () => {
|
|
const nodesLen = props.sceneNodes.length
|
|
const edgesLen = props.sceneEdges.length
|
|
if (nodesLen !== lastNodesLen || edgesLen !== lastEdgesLen) {
|
|
structuralRebuild()
|
|
} else {
|
|
styleOnlyRebuild()
|
|
}
|
|
})
|
|
|
|
onNodeClick((event) => {
|
|
emit('selectNode', event.node.id)
|
|
})
|
|
|
|
onConnect((connection: Connection) => {
|
|
if (connection.source && connection.target) {
|
|
emit('addEdge', connection.source, connection.target)
|
|
}
|
|
})
|
|
|
|
onMounted(() => {
|
|
setTimeout(() => fitView({ padding: 0.2 }), 100)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="scene-graph">
|
|
<VueFlow
|
|
:nodes="nodes"
|
|
:edges="edges"
|
|
:min-zoom="0.2"
|
|
:max-zoom="2"
|
|
>
|
|
<Background :gap="20" />
|
|
<Controls />
|
|
</VueFlow>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.scene-graph {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: #0d0d1a;
|
|
}
|
|
</style>
|