feat: replace BFS layout with dagre for professional graph layout
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import dagre from 'dagre'
|
||||||
|
|
||||||
interface NodeInfo {
|
interface NodeInfo {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
@@ -8,72 +10,42 @@ interface EdgeInfo {
|
|||||||
target: string
|
target: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const H_GAP = 300
|
const NODE_W = 180
|
||||||
const V_GAP = 140
|
const NODE_H = 60
|
||||||
const PAD = 60
|
|
||||||
|
|
||||||
export function computePositions(
|
export function computePositions(
|
||||||
nodes: NodeInfo[],
|
nodes: NodeInfo[],
|
||||||
edges: EdgeInfo[],
|
edges: EdgeInfo[],
|
||||||
startScene: string,
|
_startScene: string,
|
||||||
): Map<string, { x: number; y: number }> {
|
): Map<string, { x: number; y: number }> {
|
||||||
const positions = new Map<string, { x: number; y: number }>()
|
const g = new dagre.graphlib.Graph()
|
||||||
|
g.setGraph({
|
||||||
const adj = new Map<string, string[]>()
|
rankdir: 'LR',
|
||||||
for (const n of nodes) adj.set(n.id, [])
|
nodesep: 50,
|
||||||
for (const e of edges) {
|
ranksep: 240,
|
||||||
const list = adj.get(e.source)
|
marginx: 60,
|
||||||
if (list) list.push(e.target)
|
marginy: 60,
|
||||||
}
|
})
|
||||||
|
g.setDefaultEdgeLabel(() => ({}))
|
||||||
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) {
|
for (const n of nodes) {
|
||||||
if (!level.has(n.id)) {
|
g.setNode(n.id, { width: NODE_W, height: NODE_H })
|
||||||
maxLevel++
|
}
|
||||||
level.set(n.id, maxLevel)
|
|
||||||
|
const nodeIds = new Set(nodes.map((n) => n.id))
|
||||||
|
for (const e of edges) {
|
||||||
|
if (nodeIds.has(e.source) && nodeIds.has(e.target)) {
|
||||||
|
g.setEdge(e.source, e.target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const byLevel = new Map<number, string[]>()
|
dagre.layout(g)
|
||||||
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])
|
const positions = new Map<string, { x: number; y: number }>()
|
||||||
|
for (const n of nodes) {
|
||||||
for (const [lv, ids] of levels) {
|
const node = g.node(n.id)
|
||||||
ids.sort()
|
if (node) {
|
||||||
const count = ids.length
|
positions.set(n.id, { x: node.x - NODE_W / 2, y: node.y - NODE_H / 2 })
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -8,9 +8,11 @@
|
|||||||
"name": "moviegame",
|
"name": "moviegame",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/dagre": "^0.7.54",
|
||||||
"@vue-flow/background": "^1.3.2",
|
"@vue-flow/background": "^1.3.2",
|
||||||
"@vue-flow/controls": "^1.1.3",
|
"@vue-flow/controls": "^1.1.3",
|
||||||
"@vue-flow/core": "^1.48.2",
|
"@vue-flow/core": "^1.48.2",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"dexie": "^4.4.3",
|
"dexie": "^4.4.3",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
@@ -762,6 +764,11 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/dagre": {
|
||||||
|
"version": "0.7.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.54.tgz",
|
||||||
|
"integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ=="
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -1130,6 +1137,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dagre": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
|
||||||
|
"dependencies": {
|
||||||
|
"graphlib": "^2.1.8",
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/de-indent": {
|
"node_modules/de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
@@ -1209,6 +1225,14 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/graphlib": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/he": {
|
"node_modules/he": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
@@ -1218,6 +1242,11 @@
|
|||||||
"he": "bin/he"
|
"he": "bin/he"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/dagre": "^0.7.54",
|
||||||
"@vue-flow/background": "^1.3.2",
|
"@vue-flow/background": "^1.3.2",
|
||||||
"@vue-flow/controls": "^1.1.3",
|
"@vue-flow/controls": "^1.1.3",
|
||||||
"@vue-flow/core": "^1.48.2",
|
"@vue-flow/core": "^1.48.2",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"dexie": "^4.4.3",
|
"dexie": "^4.4.3",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user