import type { LayoutEdge } from '../utils/dagre-layout.js'; export type XY = { x: number; y: number; }; export type Size = { w: number; h: number; }; /** Tunables for the ER layout passes, kept together so they're auditable in one place. */ export declare const ER_LAYOUT: { /** A table is a "hub" (pulled to a rank extreme) at or above this connection degree. */ readonly HUB_DEGREE_THRESHOLD: 5; /** Vertical gap between a repositioned hub and the node band it sits beside. */ readonly HUB_BAND_GAP: 80; /** Clearance between two boxes after overlap resolution. Must exceed a crow's-foot's * outward reach (~18px: apex at +12, "one" bar at +18) so a marker on one table's border * can't intrude on a neighbour sitting alongside it (GH-565). */ readonly OVERLAP_MARGIN: 28; /** Max relaxation passes for overlap resolution (enough for cascading dense overlaps). */ readonly OVERLAP_MAX_PASSES: 40; /** Gap a repositioned leaf leaves below/above its parent. Generous so the short vertical * connector between a stacked pair is clearly visible, not a barely-there stub (GH-565). */ readonly LEAF_GAP: 72; }; /** * Derive the dagre `nodesep`/`ranksep` from the graph itself rather than tuned factors. * * nodesep IS the width of the vertical channel a hub's many connectors thread through, so * size it to what must physically fit: the busiest channel sits beside the highest-degree * hub, whose D edges split ~half per side, needing ceil(D/2) lanes at `laneGap` px each. * Floor at a readable minimum and cap so one giant hub can't explode the board (GH-430). * ranksep grows with the entity count (more tables → more ranks whose cross-routes need * room), floored/capped likewise. */ export declare function deriveErSpacing(nodeIds: string[], edges: LayoutEdge[], laneGap?: number): { nodesep: number; ranksep: number; }; /** * Push high-degree hubs to the TOP/BOTTOM edges of the layout (rank extremes), not the * crowded middle (GH-430). dagre centres a hub among its many neighbours; this relocates * each hub to just beyond the node band and recentres it on the x-centroid of its * neighbours, so its many edges stay short and uncrossed. Mutates `positions` in place. */ export declare function repositionHubs(hubIds: string[], edges: LayoutEdge[], positions: Map, sizeOf: (id: string) => Size): void; /** * Pull each LEAF (degree-1 table) next to its single parent (GH-565). dagre orders within * a rank by barycenter over ALL edges, which drags a leaf with one weak edge to the far * end of its rank (CaseComment ended ~1800px from Case; the worst real case ~8400px). A * leaf has no other edge to cross, so moving it beside its parent only shortens its own * connector — it never adds a crossing. Place it directly below the parent (or above when * the parent sits at the bottom of the band) aligned in x; resolveBoxOverlaps then nudges * any collision the move creates. Hubs are skipped (already placed at rank extremes). * Mutates `positions` in place. */ export declare function repositionLeaves(edges: LayoutEdge[], hubIds: Set, positions: Map, sizeOf: (id: string) => Size): void; /** * Post-layout collision resolution: the passes above can leave table boxes overlapping. * Iteratively push each overlapping pair apart along the axis of least penetration (plus a * margin) until none overlap, so every table owns a clear cell and no connector/crow's-foot * lands inside a neighbour (GH-563/564). Mutates `positions` in place. */ export declare function resolveBoxOverlaps(ids: string[], positions: Map, sizeOf: (id: string) => Size, margin?: 28, maxPasses?: 40): void; /** * Run the ER layout adaptation passes in order on dagre's raw positions: hubs to the rank * extremes, leaves beside their parents, then overlap relaxation. `allIds` is every table * id; `sizeOf` returns each table's painted box size. Mutates `positions` in place. */ export declare function applyErLayoutPasses(allIds: string[], edges: LayoutEdge[], positions: Map, sizeOf: (id: string) => Size): void;