Answer the review: budget in drawn pixels, re-ask when the budget moves, sort only the band it runs out in

- The budget counted CSS pixels; lines are drawn in device pixels, so a screen scaled to 150% or
  200% drew 1.5-2x the calibrated line. It now counts the canvas's drawn pixels.
- A graph was re-asked only when the drawn stars changed, so with a star budget covering the whole
  catalogue, or a resize, its budget and centre stayed wherever the layer was turned on. A view that
  chose its stars again now asks, and a graph is rebuilt when the stars, the range or the budget
  changed (the budget by more than half the margin, or its centre by more than 5 pc).
- From inside a system the budget was worked out in astronomical units about the system's origin.
  Graphs are now asked for in parsec space only; the flight back out asks.
- Comparing budgets let a request re-asked with a slightly different one supersede its twin, and the
  twin's rejection cleared the state of the request that replaced it. A rejection now clears it only
  for the latest request.
- The worker sorted every link to keep a few thousand, 2.2x an unbudgeted build. It now bands links
  by distance, keeps every band before the one the budget runs out in, and sorts only that one:
  142-168 ms on the real catalogue against 233-388 ms, 103 ms unbudgeted, returning early when all fit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
2026-09-17 19:03:11 +02:00
co-authored by Claude Opus 5
parent bd5e9d4b0d
commit 539a0f0a3e
4 changed files with 219 additions and 35 deletions
+55 -22
View File
@@ -222,11 +222,10 @@ export interface LinkBudget {
}
/**
* Keys pack a link's nearer-end distance, in thousandths of a parsec, above its index, so one
* numeric sort of plain doubles orders the links nearest first: room for four million links and
* two thousand kiloparsecs, inside a double's exact integers.
* How many distance bands a budgeted graph is split into to find where its budget runs out, so that
* only the links in that one band are sorted rather than all of them.
*/
const LINK_INDEX_SPAN = 2 ** 22;
const DISTANCE_BANDS = 4096;
/**
* Every link within `rangePc` between two of the stars `index` holds, each pair once, as vertex
@@ -260,31 +259,65 @@ export function jumpLinkSegments(index: StarNeighbourhood, rangePc: number, budg
return vertices.slice(0, length);
}
// Each link's nearer end's distance from the centre, and its length.
const { centre } = budget;
const count = length / 6;
const keys = new Float64Array(count);
const nearness = new Float32Array(count);
const lengths = new Float32Array(count);
let totalPc = 0;
let farthest = 0;
for (let link = 0; link < count; link++) {
const at = link * 6;
const nearer = Math.min(
Math.hypot(vertices[at] - centre.x, vertices[at + 1] - centre.y, vertices[at + 2] - centre.z),
Math.hypot(vertices[at + 3] - centre.x, vertices[at + 4] - centre.y, vertices[at + 5] - centre.z)
);
keys[link] = Math.floor(nearer * 1000) * LINK_INDEX_SPAN + link;
const ax = vertices[at] - centre.x;
const ay = vertices[at + 1] - centre.y;
const az = vertices[at + 2] - centre.z;
const bx = vertices[at + 3] - centre.x;
const by = vertices[at + 4] - centre.y;
const bz = vertices[at + 5] - centre.z;
nearness[link] = Math.sqrt(Math.min(ax * ax + ay * ay + az * az, bx * bx + by * by + bz * bz));
lengths[link] = Math.hypot(bx - ax, by - ay, bz - az);
totalPc += lengths[link];
farthest = Math.max(farthest, nearness[link]);
}
if (totalPc <= budget.lengthPc) {
return vertices.slice(0, length);
}
keys.sort();
const kept = new Float32Array(length);
let keptLength = 0;
let totalPc = 0;
for (const key of keys) {
const at = (key % LINK_INDEX_SPAN) * 6;
const linkPc = Math.hypot(vertices[at + 3] - vertices[at], vertices[at + 4] - vertices[at + 1], vertices[at + 5] - vertices[at + 2]);
if (totalPc + linkPc > budget.lengthPc) {
// Nearest first, without sorting them all: every link in the bands before the one where the budget
// runs out fits, and only that band's links are sorted to see how many of them do. Sorting all
// 730 000 links at 30 pc from the Sun to keep 4 400 doubled the time a graph took in the worker.
const bands = new Uint16Array(count);
const bandLengths = new Float64Array(DISTANCE_BANDS);
const bandsPerPc = farthest > 0 ? DISTANCE_BANDS / farthest : 0;
for (let link = 0; link < count; link++) {
bands[link] = Math.min(DISTANCE_BANDS - 1, Math.floor(nearness[link] * bandsPerPc));
bandLengths[bands[link]] += lengths[link];
}
let lastBand = 0;
let keptPc = 0;
while (keptPc + bandLengths[lastBand] <= budget.lengthPc) {
keptPc += bandLengths[lastBand++];
}
const keptLinks: number[] = [];
const boundary: number[] = [];
for (let link = 0; link < count; link++) {
const band = bands[link];
if (band < lastBand) {
keptLinks.push(link);
} else if (band === lastBand) {
boundary.push(link);
}
}
boundary.sort((a, b) => nearness[a] - nearness[b] || a - b);
for (const link of boundary) {
if (keptPc + lengths[link] > budget.lengthPc) {
break;
}
totalPc += linkPc;
kept.set(vertices.subarray(at, at + 6), keptLength);
keptLength += 6;
keptPc += lengths[link];
keptLinks.push(link);
}
return kept.slice(0, keptLength);
const kept = new Float32Array(keptLinks.length * 6);
keptLinks.forEach((link, at) => kept.set(vertices.subarray(link * 6, link * 6 + 6), at * 6));
return kept;
}