@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
93 lines (92 loc) • 3.92 kB
TypeScript
/**
* Deterministic 2D force layout for the terminal graph view.
*
* The web viewer leans on 3d-force-graph; in the terminal we want something
* dependency-free, fast enough for a few hundred nodes, and — above all —
* deterministic, so the same store draws the same map every time the view
* opens. All jitter is derived from node ids via `seeded()`; nothing here
* calls Math.random.
*
* The store's topology is a star (project → findings/tasks/fragments/refs),
* so the simulation adds a gentle pull toward each node's home project. That
* keeps clusters readable instead of letting repulsion smear everything into
* one ring.
*/
import type { NodeKind, RawLink } from "../../graph-core/types.js";
export interface LayoutNode {
id: string;
kind: NodeKind;
/** Owning project name, for the cluster pull. Projects own themselves. */
project?: string;
/** Visual size from `sizeForNode`; drives repulsion mass. */
size: number;
}
export interface Point {
x: number;
y: number;
}
/** Ideal edge length in world units. Everything else is scaled off this. */
export declare const IDEAL_DISTANCE = 12;
/**
* A terminal canvas is far wider than it is tall. A layout that settles into a
* circle therefore strands most of the screen, so the simulation is told the
* canvas aspect and lays the graph out as a matching ellipse: the project ring
* is stretched horizontally and vertical drift is damped by the same factor.
*/
export declare const DEFAULT_ASPECT = 2.4;
export declare function normalizeAspect(aspect: number | undefined): number;
/** Project ids in stable order plus, for every node, the project(s) it hangs off. */
export declare function homes(nodes: LayoutNode[], links: RawLink[]): {
projects: LayoutNode[];
homeOf: Map<string, string[]>;
};
/**
* Starting positions: projects on a ring, leaves in a jittered blob around
* their home project, multi-home fragments at the centroid of their homes,
* orphans on an outer ring.
*/
export declare function seedPositions(nodes: LayoutNode[], links: RawLink[], aspect?: number): Map<string, Point>;
export declare class ForceSim {
readonly positions: Map<string, Point>;
private readonly ids;
private readonly index;
private readonly mass;
private readonly anchors;
private readonly isProject;
private readonly links;
private temperature;
private readonly startTemperature;
private readonly aspect;
/** How far apart projects push each other; scaled to the clusters they carry. */
private readonly projectCutoff;
/**
* The ring radius each project should sit at, measured in aspect-corrected
* space. Seeding a packed ring is not enough on its own: mutual repulsion
* between many projects expands it again, which is how a forty-project store
* ended up sprawling to nearly twice its seeded size. A radial spring holds
* each project at the radius the packing chose.
*/
private readonly targetRadius;
constructor(nodes: LayoutNode[], links: RawLink[], seed?: Map<string, Point>, aspect?: number);
/** 1 at the start, 0 when settled; mirrors d3's `alpha` for hosts that animate. */
get alpha(): number;
get settled(): boolean;
/**
* Reuse a previous layout's positions for nodes that survived a data
* refresh, and reheat only mildly so the map shifts instead of scrambling.
*/
warmStart(previous: Map<string, Point>): void;
/** Reheat to the initial temperature (e.g. the `r` relayout key). */
reheat(): void;
tick(count?: number): void;
/** Run until settled or the tick budget is spent. */
settle(maxTicks?: number): void;
private step;
}
/** Axis-aligned bounds of a position set; null when empty. */
export declare function bounds(positions: Iterable<Point>): {
minX: number;
minY: number;
maxX: number;
maxY: number;
} | null;