Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
- [x] Create benchmarking scripts to compare algorithm variants
- [x] Expand CI to include coverage gating and bundle size checks

- ## Milestone 0.4.0 – Procedural Worlds & Game Systems (Planned)
## Milestone 0.4.0 – Procedural Worlds & Game Systems (Planned)
- Procedural generators:
- [x] Wave Function Collapse tile solver with options + example
- [x] Cellular automata cave/organic generator utilities
Expand Down Expand Up @@ -82,36 +82,36 @@
- [x] Boyer–Moore fast substring search
- [x] Suffix array construction utilities
- [x] Longest common subsequence (LCS) enhancements and diff helpers
- **Data pipelines & utilities**
**Data pipelines & utilities**
- [x] Flatten/unflatten helpers for nested structures
- [x] Pagination utilities for client-side paging
- [x] Advanced diff tooling (tree diff, selective patches)
- **Visual & simulation tools**
**Visual & simulation tools**
- [x] Color manipulation helpers (RGB/HSL conversion, blending)
- [x] Force-directed graph layout
- [x] Marching squares contour extraction
- [x] Marching cubes isosurface generation
- **Graph algorithms**
**Graph algorithms**
- [x] Minimum spanning tree (Kruskal)
- [ ] Strongly connected components (Tarjan/Kosaraju)
- [ ] Maximum flow (Dinic preferred; Edmonds–Karp fallback)
- **Spatial & collision expansion**
**Spatial & collision expansion**
- [ ] Octree partitioning for 3D space
- [ ] Circle collision helpers
- [ ] Raycasting utilities
- [ ] Bounding volume hierarchy (BVH) builder
- **Data structures**
**Data structures**
- [ ] Binary heap priority queue
- [ ] Disjoint set union (union-find)
- [ ] Bloom filter probabilistic membership
- [ ] Skip list sorted structure
- [ ] Segment tree range query helper
- **Compression & encoding**
**Compression & encoding**
- [ ] Run-length encoding (RLE)
- [ ] Huffman coding utilities
- [ ] LZ77 dictionary compression helper
- [ ] Base64 encode/decode utilities
- **Geometric & numeric utilities**
**Geometric & numeric utilities**
- [ ] Closest pair of points solver for geometry toolkit

### LLM‑Optimised Additions (Priority Rationale)
Expand Down
21 changes: 21 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export const examples: {
readonly graphDFS: 'examples/graph.ts';
readonly topologicalSort: 'examples/graph.ts';
readonly computeMinimumSpanningTree: 'examples/kruskal.ts';
readonly computeStronglyConnectedComponents: 'examples/scc.ts';
};
readonly geometry: {
readonly convexHull: 'examples/geometry.ts';
Expand Down Expand Up @@ -2898,6 +2899,26 @@ export interface MinimumSpanningTree {
*/
export function computeMinimumSpanningTree(options: KruskalOptions): MinimumSpanningTree;

/**
* Strongly connected components result payload.
* Import: graph/scc.ts
*/
export interface SCCResult {
components: string[][];
}

/**
* Computes strongly connected components via Tarjan's algorithm.
* Import: graph/scc.ts
*/
export function computeStronglyConnectedComponents(graph: Graph): SCCResult;

/**
* Builds a condensation DAG from SCCs where nodes are component indices.
* Import: graph/scc.ts
*/
export function buildCondensationGraph(graph: Graph, components: string[][]): Graph;

// ============================================================================
// 📐 GEOMETRY & VISUALS
// ============================================================================
Expand Down
17 changes: 17 additions & 0 deletions examples/scc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { buildCondensationGraph, computeStronglyConnectedComponents } from '../src/index.js';

const graph = {
A: [{ node: 'B' }],
B: [{ node: 'C' }, { node: 'E' }],
C: [{ node: 'A' }, { node: 'D' }],
D: [{ node: 'E' }],
E: [{ node: 'F' }],
F: [{ node: 'D' }],
};

const { components } = computeStronglyConnectedComponents(graph);
console.log('SCCs:', components);

const dag = buildCondensationGraph(graph, components);
console.log('Condensation DAG:', dag);

90 changes: 90 additions & 0 deletions src/graph/scc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Graph } from '../types.js';

export interface SCCResult {
components: string[][];
}

/**
* Computes strongly connected components using Tarjan's algorithm.
* Use for: condensation graphs, cycle detection, program analysis.
*/
export function computeStronglyConnectedComponents(graph: Graph): SCCResult {
const indexByNode = new Map<string, number>();
const lowlinkByNode = new Map<string, number>();
const onStack = new Set<string>();
const stack: string[] = [];
const components: string[][] = [];
let index = 0;

function strongConnect(v: string): void {
indexByNode.set(v, index);
lowlinkByNode.set(v, index);
index += 1;
stack.push(v);
onStack.add(v);

const edges = graph[v] ?? [];
for (const { node: w } of edges) {
if (!indexByNode.has(w)) {
strongConnect(w);
lowlinkByNode.set(v, Math.min(lowlinkByNode.get(v)!, lowlinkByNode.get(w)!));
} else if (onStack.has(w)) {
lowlinkByNode.set(v, Math.min(lowlinkByNode.get(v)!, indexByNode.get(w)!));
}
}

if (lowlinkByNode.get(v) === indexByNode.get(v)) {
const component: string[] = [];
// Pop nodes until we close the current component
// Using an explicit loop with break to avoid recursion overhead.
for (;;) {
const w = stack.pop();
if (w === undefined) {
break;
}
onStack.delete(w);
component.push(w);
if (w === v) {
break;
}
}
components.push(component);
}
}

for (const v of Object.keys(graph)) {
if (!indexByNode.has(v)) {
strongConnect(v);
}
}

return { components };
}

/**
* Builds a condensation DAG from SCC components.
* Nodes become component indices, edges preserved between components.
*/
export function buildCondensationGraph(graph: Graph, components: string[][]): Graph {
const compIndex = new Map<string, number>();
components.forEach((comp, idx) => {
for (const node of comp) compIndex.set(node, idx);
});
const dag: Graph = {};
for (let i = 0; i < components.length; i += 1) {
dag[String(i)] = [];
}
for (const [v, edges] of Object.entries(graph)) {
const from = compIndex.get(v)!;
for (const { node: w } of edges ?? []) {
const to = compIndex.get(w)!;
if (from !== to) {
const list = dag[String(from)];
if (!list.some((e) => e.node === String(to))) {
list.push({ node: String(to) });
}
}
}
}
return dag;
}
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export const examples = {
graphDFS: 'examples/graph.ts',
topologicalSort: 'examples/graph.ts',
computeMinimumSpanningTree: 'examples/kruskal.ts',
computeStronglyConnectedComponents: 'examples/scc.ts',
},
geometry: {
convexHull: 'examples/geometry.ts',
Expand Down Expand Up @@ -1003,6 +1004,13 @@ export { computeMinimumSpanningTree } from './graph/kruskal.js';

export type { KruskalOptions, MinimumSpanningTree, WeightedEdge } from './graph/kruskal.js';

/**
* Strongly connected components and condensation graph helpers.
*/
export { computeStronglyConnectedComponents, buildCondensationGraph } from './graph/scc.js';

export type { SCCResult } from './graph/scc.js';

// ============================================================================
// 📐 GEOMETRY UTILITIES
// ============================================================================
Expand Down
32 changes: 32 additions & 0 deletions tests/scc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';

import { buildCondensationGraph, computeStronglyConnectedComponents } from '../src/index.js';

describe('strongly connected components', () => {
it('finds SCCs in a directed graph and builds condensation DAG', () => {
const graph = {
A: [{ node: 'B' }],
B: [{ node: 'C' }, { node: 'E' }],
C: [{ node: 'A' }, { node: 'D' }],
D: [{ node: 'E' }],
E: [{ node: 'F' }],
F: [{ node: 'D' }],
};

const { components } = computeStronglyConnectedComponents(graph);
// Expect two cyclic components: {A,B,C} and {D,E,F}
const sorted = components.map((c) => c.sort()).sort((a, b) => a[0].localeCompare(b[0]));
expect(sorted).toEqual([
['A', 'B', 'C'],
['D', 'E', 'F'],
]);

const dag = buildCondensationGraph(graph, components);
// There should be one edge from comp(ABC) -> comp(DEF)
const edgesOutOf0 = dag['0']?.map((e) => e.node) ?? [];
const edgesOutOf1 = dag['1']?.map((e) => e.node) ?? [];
const totalEdges = edgesOutOf0.length + edgesOutOf1.length;
expect(totalEdges).toBe(1);
});
});