Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2.2 KiB
Crate: crates/graph (notescraft-graph, layer L2). SPEC sections: 6 item 5 and 12 (graph budget). Allowed new dependencies: thiserror only. Pure std, no egui, no rayon unless it is already a workspace dependency (it is not: do not add it).
Implement a deterministic force-directed layout:
Graph { node_count: usize, edges: Vec<(usize, usize)> }withGraph::new(node_count, edges) -> Result<Graph>(validates edge indices, drops self loops and duplicates, caps node_count at 200_000).LayoutConfig { iterations, repulsion, attraction, gravity, cooling, area, seed, pinned: Vec<(usize, [f32;2])> }withDefault, andlayout(&Graph, &LayoutConfig) -> Vec<[f32; 2]>. Fruchterman-Reingold with a uniform-grid spatial hash so repulsion only considers nearby nodes (cutoff radius 2k), gravity toward the centre, linear cooling. Initial positions from a seeded xorshift64 RNG (no rand crate): same seed gives bit-identical output on every platform (use only +,-,*,/,sqrt on f32 in a fixed order; no fused ops, no HashMap iteration order dependence). Pinned nodes never move. NaN/inf can never be produced (clamp).- Incremental use:
Layout::new(&Graph, &LayoutConfig)withstep(&mut self) -> f32(returns remaining energy) andpositions(), so a UI can animate;layout()is just run-to-completion.Layout::pin,unpin,set_position. local_graph(&Graph, center: usize, depth: usize) -> (Graph, Vec<usize>)returning the induced subgraph withindepthhops (depth 1..=3 typical, any depth capped) plus the mapping to original ids.- Helpers for the renderer:
degrees(&Graph) -> Vec<u32>,components(&Graph) -> Vec<usize>(connected component id per node, iterative, no recursion),orphans(&Graph).
Tests: determinism (two runs identical, different seeds differ), no NaN on degenerate inputs (0 nodes, 1 node, all nodes connected to one hub, duplicate edges, disconnected components), pinned nodes stay, edge validation errors, local_graph depth correctness, components. Performance test (marked #[ignore] but you must run it once with cargo test -p notescraft-graph --release -- --ignored): 5000 nodes / 15000 random edges (deterministic generator) lays out in under 2 seconds in release; report the measured time.