preparing for a conceptual refactoring...
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Custom recursive tree layout — Left to Right.
|
||||
*
|
||||
* Each node is centred vertically relative to its children's block.
|
||||
* Siblings are packed tightly; different root trees are separated by ROOT_GAP.
|
||||
* Lambda handlers are placed in a compact grid below the main tree.
|
||||
*
|
||||
* This avoids Dagre's global ranking which puts all siblings in a single
|
||||
* long tower regardless of the number of nodes.
|
||||
*/
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────────────
|
||||
const RANK_GAP = 60 // horizontal gap between a node's right edge and its children's left edge
|
||||
const SIBLING_GAP = 12 // vertical gap between siblings that belong to the same parent
|
||||
const ROOT_GAP = 48 // vertical gap between independent subtrees (different roots)
|
||||
const MARGIN_X = 60
|
||||
const MARGIN_Y = 60
|
||||
|
||||
const ABSTRACT_W = 160
|
||||
const ABSTRACT_H = 50
|
||||
const CONCRETE_W = 260
|
||||
const LAMBDA_W = 230
|
||||
const LAMBDA_H = 80
|
||||
|
||||
// ── Node sizing ──────────────────────────────────────────────────────────────
|
||||
function nodeWidth(node) {
|
||||
return node.data?.isAbstract ? ABSTRACT_W : CONCRETE_W
|
||||
}
|
||||
|
||||
function nodeHeight(node) {
|
||||
if (node.data?.isAbstract) return ABSTRACT_H
|
||||
const routes = node.data?.routes?.length || 1
|
||||
const hasMw = (node.data?.middleware?.length || 0) > 0
|
||||
// header(44) + divider(9) + routes*22 + [mw row 22] + padding(20)
|
||||
return 44 + 9 + routes * 22 + (hasMw ? 22 : 0) + 20
|
||||
}
|
||||
|
||||
// ── Subtree height (recursive) ────────────────────────────────────────────────
|
||||
function subtreeHeight(nodeId, childrenMap, nodeById) {
|
||||
const kids = childrenMap.get(nodeId) || []
|
||||
const selfH = nodeHeight(nodeById[nodeId])
|
||||
if (!kids.length) return selfH
|
||||
|
||||
const kidsH = kids.reduce((acc, id, i) => {
|
||||
return acc + subtreeHeight(id, childrenMap, nodeById) + (i > 0 ? SIBLING_GAP : 0)
|
||||
}, 0)
|
||||
|
||||
return Math.max(selfH, kidsH)
|
||||
}
|
||||
|
||||
// ── Place a node and its subtree ──────────────────────────────────────────────
|
||||
function placeNode(nodeId, x, y, childrenMap, nodeById, positions) {
|
||||
const node = nodeById[nodeId]
|
||||
const kids = childrenMap.get(nodeId) || []
|
||||
const selfH = nodeHeight(node)
|
||||
const selfW = nodeWidth(node)
|
||||
const totalH = subtreeHeight(nodeId, childrenMap, nodeById)
|
||||
|
||||
// Centre this node within the height its subtree occupies
|
||||
positions.set(nodeId, { x, y: y + (totalH - selfH) / 2 })
|
||||
|
||||
if (kids.length) {
|
||||
const childX = x + selfW + RANK_GAP
|
||||
let childY = y
|
||||
kids.forEach(kidId => {
|
||||
const kidH = subtreeHeight(kidId, childrenMap, nodeById)
|
||||
placeNode(kidId, childX, childY, childrenMap, nodeById, positions)
|
||||
childY += kidH + SIBLING_GAP
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main export ───────────────────────────────────────────────────────────────
|
||||
export function layoutGraph(nodes, edges) {
|
||||
// Separate lambdas from class-based handlers
|
||||
const lambdas = nodes.filter(n => n.data?.isLambda)
|
||||
const regulars = nodes.filter(n => !n.data?.isLambda)
|
||||
|
||||
const nodeById = Object.fromEntries(regulars.map(n => [n.id, n]))
|
||||
const childrenMap = new Map() // parentId → [childId, ...]
|
||||
const parentSet = new Set() // ids that have a parent
|
||||
|
||||
edges.forEach(e => {
|
||||
if (e.edgeType !== 'extends') return
|
||||
if (!childrenMap.has(e.source)) childrenMap.set(e.source, [])
|
||||
childrenMap.get(e.source).push(e.target)
|
||||
parentSet.add(e.target)
|
||||
})
|
||||
|
||||
// Roots = regular nodes with no incoming extends edge
|
||||
const roots = regulars.filter(n => !parentSet.has(n.id))
|
||||
|
||||
// Layout each root subtree
|
||||
const positions = new Map()
|
||||
let curY = MARGIN_Y
|
||||
|
||||
roots.forEach(root => {
|
||||
const treeH = subtreeHeight(root.id, childrenMap, nodeById)
|
||||
placeNode(root.id, MARGIN_X, curY, childrenMap, nodeById, positions)
|
||||
curY += treeH + ROOT_GAP
|
||||
})
|
||||
|
||||
// Apply positions
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
let minY = Infinity
|
||||
|
||||
const layoutedNodes = regulars.map(node => {
|
||||
const pos = positions.get(node.id) || { x: MARGIN_X, y: MARGIN_Y }
|
||||
const r = pos.x + nodeWidth(node)
|
||||
const b = pos.y + nodeHeight(node)
|
||||
if (r > maxX) maxX = r
|
||||
if (b > maxY) maxY = b
|
||||
if (pos.y < minY) minY = pos.y
|
||||
return { ...node, position: pos }
|
||||
})
|
||||
|
||||
// ── Lambda grid — centred below the main tree ────────────────────────────
|
||||
if (lambdas.length) {
|
||||
const treeWidth = maxX - MARGIN_X
|
||||
const cols = Math.min(4, Math.max(2, Math.ceil(Math.sqrt(lambdas.length))))
|
||||
const colWidth = LAMBDA_W + 30
|
||||
const rowHeight = LAMBDA_H + 16
|
||||
const gridWidth = cols * colWidth - 30
|
||||
const gridStartX = MARGIN_X + Math.max(0, (treeWidth - gridWidth) / 2)
|
||||
const gridStartY = maxY + 80
|
||||
|
||||
lambdas.forEach((node, i) => {
|
||||
layoutedNodes.push({
|
||||
...node,
|
||||
position: {
|
||||
x: gridStartX + (i % cols) * colWidth,
|
||||
y: gridStartY + Math.floor(i / cols) * rowHeight,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ── Edges ────────────────────────────────────────────────────────────────
|
||||
const layoutedEdges = edges
|
||||
.filter(e => e.edgeType === 'extends')
|
||||
.map((edge, idx) => ({
|
||||
...edge,
|
||||
id: edge.id || `ext-${idx}`,
|
||||
type: 'bezier',
|
||||
pathOptions: { curvature: 0.35 },
|
||||
style: { stroke: '#2e3347', strokeWidth: 1.5 },
|
||||
markerEnd: { type: 'arrowclosed', width: 11, height: 11, color: '#2e3347' },
|
||||
animated: false,
|
||||
}))
|
||||
|
||||
return { nodes: layoutedNodes, edges: layoutedEdges }
|
||||
}
|
||||
Reference in New Issue
Block a user