feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
173
frontend/src/features/git/graphLayout.ts
Normal file
173
frontend/src/features/git/graphLayout.ts
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user