app
Circuit Trace Generators
Public Made by Adomby adom
Two dependency-free, seeded circuit-background generators: a seamless torus tile with ambient or progressive reveal, and the v9 continuous field with obstacles, scroll reveal, and five ambient systems.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
const DEFAULTS = Object.freeze({
seed: "nitrogen",
width: 900,
pitch: 7.895,
featureScale: 1,
chunkRows: 96,
density: 1,
fill: 0.668,
routeLength: 0.78,
horizontalFlow: 0.82,
flowAngle: 0,
swirl: 0.28,
swirlScale: 0.8,
diagonalFlow: 0.58,
jogLength: 0.72,
channelAttraction: 0.78,
longFlow: 0.18,
edgeDensity: 0.22,
maxRouteBudget: 86,
routeGuardRows: 0,
});
const DIRECTIONS = Object.freeze([[1, 0], [0, 1], [-1, 0], [0, -1]]);
function clamp(value, minimum, maximum) {
return Math.max(minimum, Math.min(maximum, value));
}
function hashSeed(value) {
let hash = 2166136261;
for (const character of String(value)) {
hash ^= character.charCodeAt(0);
hash = Math.imul(hash, 16777619);
}
return hash >>> 0;
}
function mix32(value) {
value = Math.imul(value ^ (value >>> 16), 0x7feb352d);
value = Math.imul(value ^ (value >>> 15), 0x846ca68b);
return (value ^ (value >>> 16)) >>> 0;
}
function coordinateHash(seed, x, y, salt = 0) {
let value = seed ^ Math.imul(x | 0, 0x9e3779b1);
value ^= Math.imul(y | 0, 0x85ebca77);
value ^= Math.imul(salt | 0, 0xc2b2ae3d);
return mix32(value);
}
function unitHash(seed, x, y, salt = 0) {
return coordinateHash(seed, x, y, salt) / 4294967296;
}
function mulberry32(seed) {
return function random() {
let value = (seed += 0x6d2b79f5);
value = Math.imul(value ^ (value >>> 15), value | 1);
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
};
}
function integer(random, minimum, maximum) {
if (maximum <= minimum) return minimum;
return Math.floor(random() * (maximum - minimum + 1)) + minimum;
}
function weighted(random, entries) {
let total = 0;
for (const [, weight] of entries) total += weight;
let roll = random() * total;
for (const [value, weight] of entries) {
roll -= weight;
if (roll <= 0) return value;
}
return entries.at(-1)?.[0];
}
function shuffle(random, values) {
for (let index = values.length - 1; index > 0; index -= 1) {
const swapIndex = integer(random, 0, index);
[values[index], values[swapIndex]] = [values[swapIndex], values[index]];
}
return values;
}
function diagonalness(vector) {
return 1 - Math.abs(Math.abs(vector[0]) - Math.abs(vector[1]));
}
function closestDirection(vector) {
let result = 0;
let bestScore = -Infinity;
for (let index = 0; index < DIRECTIONS.length; index += 1) {
const score = DIRECTIONS[index][0] * vector[0] + DIRECTIONS[index][1] * vector[1];
if (score > bestScore) {
bestScore = score;
result = index;
}
}
return result;
}
function simplify(path) {
if (path.length < 3) return path.slice();
const result = [path[0]];
for (let index = 1; index < path.length - 1; index += 1) {
const previous = result.at(-1);
const current = path[index];
const next = path[index + 1];
const ax = Math.sign(current[0] - previous[0]);
const ay = Math.sign(current[1] - previous[1]);
const bx = Math.sign(next[0] - current[0]);
const by = Math.sign(next[1] - current[1]);
if (ax !== bx || ay !== by) result.push(current);
}
result.push(path.at(-1));
return result;
}
function chamfer(path) {
if (path.length < 3) return path.slice();
const result = [path[0]];
for (let index = 1; index < path.length - 1; index += 1) {
const previous = path[index - 1];
const current = path[index];
const next = path[index + 1];
const ax = current[0] - previous[0];
const ay = current[1] - previous[1];
const bx = next[0] - current[0];
const by = next[1] - current[1];
const incomingLength = Math.hypot(ax, ay);
const outgoingLength = Math.hypot(bx, by);
if (ax * bx + ay * by !== 0 || (ax === bx && ay === by)) {
result.push(current);
continue;
}
const inset = Math.min(0.38, incomingLength * 0.38, outgoingLength * 0.38);
result.push(
[current[0] - ax / incomingLength * inset, current[1] - ay / incomingLength * inset],
[current[0] + bx / outgoingLength * inset, current[1] + by / outgoingLength * inset],
);
}
result.push(path.at(-1));
return result;
}
function pathLength(points) {
let length = 0;
for (let index = 1; index < points.length; index += 1) {
length += Math.hypot(
points[index][0] - points[index - 1][0],
points[index][1] - points[index - 1][1],
);
}
return length;
}
function normalizeOptions(options) {
const config = { ...DEFAULTS, ...options };
const featureScale = Math.max(0.25, Number(config.featureScale) || 1);
const pitch = Math.max(1, Number(config.pitch) * featureScale || DEFAULTS.pitch);
const requestedWidth = Math.max(240, Number(config.width) || DEFAULTS.width);
const columns = Math.max(30, Math.floor(requestedWidth / pitch));
const chunkRows = Math.max(24, Math.round(Number(config.chunkRows) || DEFAULTS.chunkRows));
const maxRouteBudget = Math.max(
12,
Math.round((Number(config.maxRouteBudget) || DEFAULTS.maxRouteBudget)
* clamp(Number(config.routeLength) || DEFAULTS.routeLength, 0.2, 2)),
);
const routeGuardRows = Math.max(
maxRouteBudget + 8,
Math.round(Number(config.routeGuardRows) || 0),
);
return {
...config,
width: (columns - 1) * pitch,
pitch,
columns,
chunkRows,
maxRouteBudget,
routeGuardRows,
fill: clamp(Number(config.fill) || DEFAULTS.fill, 0.05, 0.9),
density: clamp(Number(config.density) || DEFAULTS.density, 0.05, 1.5),
horizontalFlow: clamp(Number(config.horizontalFlow) || 0, 0, 1),
swirl: clamp(Number(config.swirl) || 0, 0, 1.5),
swirlScale: clamp(Number(config.swirlScale) || DEFAULTS.swirlScale, 0.2, 3),
diagonalFlow: clamp(Number(config.diagonalFlow) || 0, 0, 1),
jogLength: clamp(Number(config.jogLength) || 0, 0.15, 1.5),
channelAttraction: clamp(Number(config.channelAttraction) || 0, 0, 2),
longFlow: clamp(Number(config.longFlow) || 0, 0, 1),
edgeDensity: clamp(Number(config.edgeDensity) || 0, 0, 1),
flowAngle: Number(config.flowAngle) || 0,
};
}
/**
* Creates a deterministic, append-only field of non-crossing circuit traces.
* Geometry uses absolute world coordinates. ensureThrough() is synchronous and
* always processes the same fixed-height chunks, regardless of call cadence.
*/
export function createStreamingCircuitField(options = {}) {
let config;
let seedHash;
let occupied;
let rowOccupancy;
let flowVortexCache;
let retained;
let callbacks;
let nextChunkId;
let nextTraceSequence;
let generatedThroughRow;
let totalPublished;
let totalPruned;
let totalOccupiedCells;
let prunedOccupiedCells;
let prunedOccupancyRows;
let prunedFlowVortices;
let generationCacheMinimumRow;
let rejectedRoutes;
let destroyed = false;
function initialize(nextOptions) {
config = normalizeOptions(nextOptions);
seedHash = hashSeed(config.seed);
occupied = new Set();
rowOccupancy = new Map();
flowVortexCache = new Map();
retained = [];
callbacks = callbacks || new Set();
nextChunkId = 0;
nextTraceSequence = 0;
generatedThroughRow = 0;
totalPublished = 0;
totalPruned = 0;
totalOccupiedCells = 0;
prunedOccupiedCells = 0;
prunedOccupancyRows = 0;
prunedFlowVortices = 0;
generationCacheMinimumRow = 1;
rejectedRoutes = 0;
}
function assertLive() {
if (destroyed) throw new Error("Streaming circuit field has been destroyed");
}
function cellKey(x, y) {
return y * config.columns + x;
}
function isBlocked(x, y) {
return occupied.has(cellKey(x, y));
}
function flowVortex(latticeX, latticeY, spacing) {
const key = latticeX + "," + latticeY;
let vortex = flowVortexCache.get(key);
if (vortex) return vortex;
vortex = {
x: (latticeX + 0.15 + unitHash(seedHash, latticeX, latticeY, 1) * 0.7) * spacing,
y: (latticeY + 0.15 + unitHash(seedHash, latticeX, latticeY, 2) * 0.7) * spacing,
radius: spacing * (0.7 + unitHash(seedHash, latticeX, latticeY, 3) * 0.55),
spin: unitHash(seedHash, latticeX, latticeY, 4) < 0.5 ? -1 : 1,
strength: 0.72 + unitHash(seedHash, latticeX, latticeY, 5) * 0.56,
};
flowVortexCache.set(key, vortex);
return vortex;
}
function flowVector(x, y, angleDegrees) {
const radians = angleDegrees * Math.PI / 180;
let vx = Math.cos(radians);
let vy = Math.sin(radians);
const spacing = Math.max(24, Math.round(72 * config.swirlScale));
const gridX = Math.floor(x / spacing);
const gridY = Math.floor(y / spacing);
// A hashed lattice gives the infinite field stable vortices without using
// viewport dimensions or the current generated height.
for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
for (let offsetX = -1; offsetX <= 1; offsetX += 1) {
const latticeX = gridX + offsetX;
const latticeY = gridY + offsetY;
const vortex = flowVortex(latticeX, latticeY, spacing);
const dx = x - vortex.x;
const dy = y - vortex.y;
const distance = Math.hypot(dx, dy);
if (distance < 0.0001) continue;
const falloff = Math.exp(
-(distance * distance) / (2 * vortex.radius * vortex.radius),
);
const influence = config.swirl * vortex.strength * falloff * 0.34;
vx += (-dy / distance) * vortex.spin * influence;
vy += (dx / distance) * vortex.spin * influence;
}
}
const magnitude = Math.hypot(vx, vy) || 1;
return [vx / magnitude, vy / magnitude];
}
function runLength(random, perpendicular, vector) {
let result;
if (perpendicular) {
result = random() < 0.22 ? integer(random, 4, 7) : integer(random, 1, 4);
const scale = config.jogLength + (1 - config.jogLength) * diagonalness(vector);
return Math.max(1, Math.round(result * scale));
}
const roll = random();
if (roll < 0.14) result = integer(random, 16, 28);
else if (roll < 0.52) result = integer(random, 9, 16);
else result = integer(random, 4, 10);
const scale = 1 - diagonalness(vector) * config.diagonalFlow * 0.9;
return Math.max(1, Math.round(result * scale));
}
function traceBudget(random) {
const ceiling = config.maxRouteBudget;
const roll = random();
let result;
if (roll < 0.2) result = integer(random, Math.min(42, ceiling), ceiling);
else if (roll < 0.65) result = integer(random, Math.min(24, ceiling), Math.min(50, ceiling));
else result = integer(random, Math.min(12, ceiling), Math.min(28, ceiling));
return Math.max(6, result);
}
function lookAhead(x, y, direction, local, minimumRow, maximumRow) {
const [dx, dy] = DIRECTIONS[direction];
let open = 0;
for (let step = 1; step <= 5; step += 1) {
const nextX = x + dx * step;
const nextY = y + dy * step;
if (
nextX < 1
|| nextX >= config.columns - 1
|| nextY < minimumRow
|| nextY > maximumRow
) break;
const key = cellKey(nextX, nextY);
if (occupied.has(key) || local.has(key)) break;
open += 1;
}
return open;
}
function growOne(
start,
initialDirection,
limit,
local,
random,
angleDegrees,
travelSign,
minimumRow,
maximumRow,
) {
const points = [start];
let [x, y] = start;
let direction = initialDirection;
const firstVector = flowVector(x, y, angleDegrees);
let targetVector = [firstVector[0] * travelSign, firstVector[1] * travelSign];
let remaining = runLength(random, false, targetVector);
while (points.length <= limit) {
const candidates = [];
const primaryDirection = closestDirection(targetVector);
for (const candidate of [direction, (direction + 1) % 4, (direction + 3) % 4]) {
const [dx, dy] = DIRECTIONS[candidate];
const nextX = x + dx;
const nextY = y + dy;
if (
nextX < 1
|| nextX >= config.columns - 1
|| nextY < minimumRow
|| nextY > maximumRow
) continue;
const key = cellKey(nextX, nextY);
if (occupied.has(key) || local.has(key)) continue;
const open = lookAhead(x, y, candidate, local, minimumRow, maximumRow);
if (!open) continue;
const straight = candidate === direction;
let weight = straight ? (remaining > 0 ? 15 : 1.6) : (remaining > 0 ? 0.12 : 4.2);
const alignment = dx * targetVector[0] + dy * targetVector[1];
if (alignment < -0.16) continue;
weight *= 0.45 + open * 0.34;
let neighbors = 0;
if (candidate % 2 === 0) {
weight *= 0.45 + Math.max(0, alignment) * 2.2;
if (nextY > minimumRow && isBlocked(nextX, nextY - 1)) neighbors += 1;
if (nextY < maximumRow && isBlocked(nextX, nextY + 1)) neighbors += 1;
} else {
if (nextX > 1 && isBlocked(nextX - 1, nextY)) neighbors += 1;
if (nextX < config.columns - 2 && isBlocked(nextX + 1, nextY)) neighbors += 1;
}
weight *= 1 + neighbors * config.channelAttraction;
candidates.push([candidate, weight]);
}
if (!candidates.length) break;
const nextDirection = weighted(random, candidates);
if (nextDirection !== direction) {
direction = nextDirection;
remaining = runLength(
random,
direction % 2 !== primaryDirection % 2,
targetVector,
);
} else {
remaining -= 1;
}
x += DIRECTIONS[direction][0];
y += DIRECTIONS[direction][1];
const key = cellKey(x, y);
local.add(key);
points.push([x, y]);
const sampled = flowVector(x, y, angleDegrees);
targetVector = [sampled[0] * travelSign, sampled[1] * travelSign];
}
return points;
}
function commitTrace(cells, ownerChunk, destination) {
if (cells.length < 6) {
rejectedRoutes += 1;
return false;
}
for (const [x, y] of cells) {
const key = cellKey(x, y);
if (occupied.has(key)) {
rejectedRoutes += 1;
return false;
}
}
for (const [x, y] of cells) {
occupied.add(cellKey(x, y));
rowOccupancy.set(y, (rowOccupancy.get(y) || 0) + 1);
}
totalOccupiedCells += cells.length;
// Simplify first, then cut only a short diagonal across each reserved
// corner. This keeps the visible path inside its occupied grid corridor.
const shaped = chamfer(simplify(cells));
const points = shaped.map(([x, y]) => [x * config.pitch, y * config.pitch]);
const yValues = points.map((point) => point[1]);
const trace = Object.freeze({
id: `${seedHash.toString(36)}-${nextTraceSequence.toString(36)}`,
chunkId: ownerChunk,
ownerChunk,
points,
length: pathLength(points),
minY: Math.min(...yValues),
maxY: Math.max(...yValues),
viaStart: true,
viaEnd: true,
});
nextTraceSequence += 1;
destination.push(trace);
retained.push(trace);
totalPublished += 1;
return true;
}
function makeInteriorTrace(start, ownerChunk, destination, random, minimumRow, maximumRow) {
const startKey = cellKey(start[0], start[1]);
if (occupied.has(startKey)) return false;
const followsPrimary = random() < config.horizontalFlow;
const angleDegrees = config.flowAngle + (followsPrimary ? 0 : 90);
const startVector = flowVector(start[0], start[1], angleDegrees);
const direction = closestDirection(startVector);
const budget = traceBudget(random);
const forwardBudget = Math.max(3, Math.floor(budget * (0.42 + random() * 0.16)));
const local = new Set([startKey]);
const forward = growOne(
start,
direction,
forwardBudget,
local,
random,
angleDegrees,
1,
minimumRow,
maximumRow,
);
const backward = growOne(
start,
(direction + 2) % 4,
budget - forwardBudget,
local,
random,
angleDegrees,
-1,
minimumRow,
maximumRow,
);
return commitTrace(backward.slice(1).reverse().concat(forward), ownerChunk, destination);
}
function makeEdgeTrace(
start,
direction,
ownerChunk,
destination,
random,
minimumRow,
maximumRow,
routeOptions = {},
) {
const startKey = cellKey(start[0], start[1]);
if (occupied.has(startKey)) return false;
const local = new Set([startKey]);
const budget = routeOptions.budget ?? traceBudget(random);
const angleDegrees = routeOptions.angleDegrees
?? config.flowAngle + (random() < config.horizontalFlow ? 0 : 90);
const sampled = flowVector(start[0], start[1], angleDegrees);
const inward = DIRECTIONS[direction][0] * sampled[0] + DIRECTIONS[direction][1] * sampled[1];
const cells = growOne(
start,
direction,
budget,
local,
random,
angleDegrees,
inward >= 0 ? 1 : -1,
minimumRow,
maximumRow,
);
if (cells.length < (routeOptions.minimumLength ?? 6)) {
rejectedRoutes += 1;
return false;
}
return commitTrace(cells, ownerChunk, destination);
}
function occupiedInRange(startRow, endRow) {
let result = 0;
for (let row = startRow; row <= endRow; row += 1) {
result += rowOccupancy.get(row) || 0;
}
return result;
}
// Future route probes reject every row below this monotonically increasing
// guard, so neither occupancy structure can observe discarded rows again.
function pruneGenerationCaches(minimumRouteRow) {
generationCacheMinimumRow = minimumRouteRow;
for (const key of occupied) {
const row = Math.floor(key / config.columns);
if (row < minimumRouteRow) {
occupied.delete(key);
prunedOccupiedCells += 1;
}
}
for (const row of rowOccupancy.keys()) {
if (row < minimumRouteRow) {
rowOccupancy.delete(row);
prunedOccupancyRows += 1;
}
}
// flowVector samples gridY - 1 through gridY + 1, making lower lattice
// rows unreachable once the route guard advances past them.
const spacing = Math.max(24, Math.round(72 * config.swirlScale));
const minimumLatticeRow = Math.floor(minimumRouteRow / spacing) - 1;
for (const key of flowVortexCache.keys()) {
const latticeRow = Number(key.slice(key.indexOf(",") + 1));
if (latticeRow < minimumLatticeRow) {
flowVortexCache.delete(key);
prunedFlowVortices += 1;
}
}
}
function generateChunk(chunkId) {
const chunkStart = chunkId * config.chunkRows + 1;
const chunkEnd = chunkStart + config.chunkRows - 1;
const minimumRouteRow = Math.max(1, chunkStart - config.routeGuardRows);
const maximumRouteRow = chunkEnd + config.routeGuardRows;
const random = mulberry32(hashSeed(
`${config.seed}:${config.columns}:chunk:${chunkId}`,
));
const appended = [];
const angleRadians = config.flowAngle * Math.PI / 180;
const absoluteX = Math.abs(Math.cos(angleRadians));
const absoluteY = Math.abs(Math.sin(angleRadians));
const primaryHorizontal = absoluteX / (absoluteX + absoluteY || 1);
const horizontalWeight = config.horizontalFlow * primaryHorizontal
+ (1 - config.horizontalFlow) * (1 - primaryHorizontal);
// Carriers are admitted only from the true left and right boundaries.
// Chunk top and bottom are never treated as edges, which avoids seams.
const carriers = [];
for (let row = chunkStart; row <= chunkEnd; row += 1) {
carriers.push([[1, row], 0], [[config.columns - 2, row], 2]);
}
shuffle(random, carriers);
const carrierTarget = Math.round(config.chunkRows * config.longFlow * horizontalWeight);
let carrierCount = 0;
for (const [start, direction] of carriers) {
if (carrierCount >= carrierTarget) break;
const minimumBudget = Math.max(12, Math.round(config.columns * 0.58));
const maximumBudget = Math.min(
config.maxRouteBudget,
Math.max(minimumBudget, Math.round(config.columns * 1.02)),
);
const budget = integer(random, Math.min(minimumBudget, maximumBudget), maximumBudget);
if (makeEdgeTrace(
start,
direction,
chunkId,
appended,
random,
minimumRouteRow,
maximumRouteRow,
{
budget,
angleDegrees: config.flowAngle,
minimumLength: Math.max(6, Math.round(config.columns * 0.3)),
},
)) carrierCount += 1;
}
const edgeSeeds = shuffle(random, carriers.slice());
const edgeTarget = Math.round(config.chunkRows * 2 * config.edgeDensity);
let edgeCount = 0;
for (const [start, direction] of edgeSeeds) {
if (edgeCount >= edgeTarget) break;
if (makeEdgeTrace(
start,
direction,
chunkId,
appended,
random,
minimumRouteRow,
maximumRouteRow,
)) edgeCount += 1;
}
const seeds = [];
for (let row = chunkStart; row <= chunkEnd; row += 1) {
for (let column = 1; column < config.columns - 1; column += 1) {
seeds.push([column, row]);
}
}
shuffle(random, seeds);
const targetFill = Math.min(0.82, Math.max(0.12, config.fill * config.density));
const targetOccupied = Math.round(
targetFill * config.chunkRows * (config.columns - 2),
);
let chunkOccupied = occupiedInRange(chunkStart, chunkEnd);
for (const seed of seeds) {
if (chunkOccupied >= targetOccupied) break;
if (isBlocked(seed[0], seed[1])) continue;
if (makeInteriorTrace(
seed,
chunkId,
appended,
random,
minimumRouteRow,
maximumRouteRow,
)) {
chunkOccupied = occupiedInRange(chunkStart, chunkEnd);
}
}
generatedThroughRow = chunkEnd;
nextChunkId = chunkId + 1;
const nextMinimumRouteRow = Math.max(
1,
nextChunkId * config.chunkRows + 1 - config.routeGuardRows,
);
pruneGenerationCaches(nextMinimumRouteRow);
if (appended.length) {
const immutableBatch = Object.freeze(appended.slice());
for (const callback of callbacks) callback(immutableBatch, api);
}
return appended;
}
const api = {
ensureThrough(worldY) {
assertLive();
const target = Math.max(0, Number(worldY) || 0);
const appended = [];
while (generatedThroughRow * config.pitch < target) {
appended.push(...generateChunk(nextChunkId));
}
return appended;
},
onAppend(callback) {
assertLive();
if (typeof callback !== "function") throw new TypeError("callback must be a function");
callbacks.add(callback);
return () => callbacks.delete(callback);
},
pruneBefore(worldY) {
assertLive();
const boundary = Number(worldY) || 0;
const removed = [];
retained = retained.filter((trace) => {
if (trace.maxY >= boundary) return true;
removed.push(trace.id);
return false;
});
totalPruned += removed.length;
return removed;
},
reset(nextOptions = {}) {
assertLive();
initialize({ ...config, ...nextOptions });
return api;
},
destroy() {
if (destroyed) return;
destroyed = true;
callbacks.clear();
retained = [];
},
get width() {
return config.width;
},
get height() {
return generatedThroughRow * config.pitch;
},
get generatedThrough() {
return generatedThroughRow * config.pitch;
},
get traces() {
return retained;
},
get retainedTraces() {
return retained;
},
get stats() {
const generatedCells = Math.max(1, generatedThroughRow * config.columns);
return Object.freeze({
chunks: nextChunkId,
columns: config.columns,
chunkRows: config.chunkRows,
pitch: config.pitch,
width: config.width,
generatedThrough: generatedThroughRow * config.pitch,
occupiedCells: totalOccupiedCells,
occupancyFill: totalOccupiedCells / generatedCells,
occupiedCellsRetained: occupied.size,
occupiedCellsPruned: prunedOccupiedCells,
occupancyRowsRetained: rowOccupancy.size,
occupancyRowsPruned: prunedOccupancyRows,
flowVorticesRetained: flowVortexCache.size,
flowVorticesPruned: prunedFlowVortices,
generationCacheMinimumRow,
totalPublished,
retainedTraces: retained.length,
totalPruned,
rejectedRoutes,
routeGuardRows: config.routeGuardRows,
occupancyRetainedForSafety: true,
});
},
};
initialize(options);
return api;
}