preparing for a conceptual refactoring...

This commit is contained in:
Relism
2026-03-28 14:11:12 +01:00
parent 7b996b552b
commit 2edd68b0aa
60 changed files with 3432 additions and 518 deletions
@@ -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 }
}