Files
IdeA/frontend/src/features/git/graphLayout.ts
Blomios 307ae71857 feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
2026-06-06 01:27:01 +02:00

174 lines
6.1 KiB
TypeScript

/**
* Pure git-graph lane layout (L8 / git-graph view).
*
* `computeGraphRows` turns a newest-first commit list into render-ready rows that
* a renderer can draw as a GitKraken-style graph: **continuous coloured lanes**
* with curved connectors at branch points (forks) and merges.
*
* ## Model
*
* We keep an ordered list of "active lanes": `lanes[i]` is the hash of the commit
* we expect next on lane `i` (or `null` when the lane is free). A lane index is a
* stable column; we never re-index existing lanes (a freed lane is reused in
* place), so a branch that doesn't bend draws a **straight vertical line**.
*
* Each row is split into two halves around the commit node (drawn at mid-height):
* - `incoming` links go from the **top** boundary to the node's mid level,
* - `outgoing` links go from the node's mid level to the **bottom** boundary.
*
* Straight links (`fromLane === toLane`) are pass-through lane lines; bent links
* are the fork/merge connectors. Because lane x-positions are identical across
* rows, a row's `outgoing` meets the next row's `incoming` at the shared
* boundary, yielding unbroken lanes top-to-bottom.
*/
import type { GraphCommit } from "@/domain";
// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
/** A line segment within one half of a row. */
export interface GraphLink {
/** Lane index at the start of the segment. */
fromLane: number;
/** Lane index at the end of the segment. */
toLane: number;
/** Stroke colour. */
color: string;
}
export interface GraphRow {
commit: GraphCommit;
/** Lane (column) of this commit's node. */
lane: number;
/** Node colour (its lane's colour). */
color: string;
/** Links in the top half: top boundary → node mid. */
incoming: GraphLink[];
/** Links in the bottom half: node mid → bottom boundary. */
outgoing: GraphLink[];
/** Number of lane columns occupied around this row (for SVG width). */
laneCount: number;
}
// ---------------------------------------------------------------------------
// Colour palette (dark-theme-friendly)
// ---------------------------------------------------------------------------
const LANE_COLORS: readonly string[] = [
"#4f9cf9", // blue
"#a78bfa", // violet
"#34d399", // emerald
"#fbbf24", // amber
"#f87171", // rose
"#38bdf8", // sky
"#fb923c", // orange
"#c084fc", // purple
"#f472b6", // pink
"#2dd4bf", // teal
];
/** Stable colour for a lane index. */
export function laneColor(laneIndex: number): string {
return LANE_COLORS[laneIndex % LANE_COLORS.length];
}
// ---------------------------------------------------------------------------
// Core algorithm
// ---------------------------------------------------------------------------
/** Index of the first free (null) lane, or -1. */
function firstFree(lanes: (string | null)[]): number {
return lanes.indexOf(null);
}
/** Places `hash` on a lane: an existing free slot if any, else a new column. */
function allocLane(lanes: (string | null)[], hash: string): number {
const free = firstFree(lanes);
if (free !== -1) {
lanes[free] = hash;
return free;
}
lanes.push(hash);
return lanes.length - 1;
}
/**
* Assigns lanes + links to each commit.
*
* @param commits Commits ordered **newest-first** (as `git log`).
*/
export function computeGraphRows(commits: GraphCommit[]): GraphRow[] {
// lanes[i] = hash expected next on lane i (null = free).
const lanes: (string | null)[] = [];
const rows: GraphRow[] = [];
for (const commit of commits) {
const { hash, parents } = commit;
const top = lanes.slice();
// ── Resolve the commit's lane ────────────────────────────────────────
let commitLane = lanes.indexOf(hash);
if (commitLane === -1) {
// A tip nobody pointed at yet: open a lane for it.
commitLane = allocLane(lanes, hash);
}
// Other lanes also expecting this hash converge into the node and end.
for (let i = 0; i < top.length; i++) {
if (i !== commitLane && top[i] === hash) lanes[i] = null;
}
// ── Route parents ────────────────────────────────────────────────────
const parentLanes: number[] = [];
if (parents.length === 0) {
lanes[commitLane] = null; // root: lane ends here.
} else {
lanes[commitLane] = parents[0]; // first parent keeps the lane.
parentLanes.push(commitLane);
for (let k = 1; k < parents.length; k++) {
const ph = parents[k];
const existing = lanes.indexOf(ph);
parentLanes.push(existing !== -1 ? existing : allocLane(lanes, ph));
}
}
const bottom = lanes.slice();
// ── Top half: every active top lane → node mid ───────────────────────
const incoming: GraphLink[] = [];
for (let i = 0; i < top.length; i++) {
if (top[i] === null) continue;
if (i === commitLane || top[i] === hash) {
// Into the node (straight if i === commitLane, a converging curve else).
incoming.push({ fromLane: i, toLane: commitLane, color: laneColor(i) });
} else {
incoming.push({ fromLane: i, toLane: i, color: laneColor(i) }); // pass-through
}
}
// ── Bottom half: node mid → every active bottom lane ─────────────────
const outgoing: GraphLink[] = [];
for (let j = 0; j < bottom.length; j++) {
if (bottom[j] === null) continue;
if (parentLanes.includes(j)) {
outgoing.push({ fromLane: commitLane, toLane: j, color: laneColor(j) }); // node → parent
} else {
outgoing.push({ fromLane: j, toLane: j, color: laneColor(j) }); // pass-through
}
}
rows.push({
commit,
lane: commitLane,
color: laneColor(commitLane),
incoming,
outgoing,
laneCount: Math.max(top.length, bottom.length, commitLane + 1),
});
}
return rows;
}