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:
@@ -168,6 +168,30 @@ function linksDrawn(segments: Float32Array, points: readonly StarPoint[]): strin
|
||||
return links;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a budget should keep, worked out the slow way: every link sorted by how near its nearer end
|
||||
* is to the centre, then taken until one does not fit. Lengths and distances as the float32 buffer
|
||||
* holds them.
|
||||
*/
|
||||
function nearestFirst(points: readonly StarPoint[], rangePc: number, centre: { x: number; y: number; z: number }, lengthPc: number): string[] {
|
||||
const all = jumpLinkSegments(index([...points]), rangePc);
|
||||
const links = Array.from({ length: all.length / 6 }, (_, link) => {
|
||||
const v = Array.from(all.subarray(link * 6, link * 6 + 6));
|
||||
const nearer = Math.fround(Math.sqrt(Math.min((v[0] - centre.x) ** 2 + (v[1] - centre.y) ** 2 + (v[2] - centre.z) ** 2, (v[3] - centre.x) ** 2 + (v[4] - centre.y) ** 2 + (v[5] - centre.z) ** 2)));
|
||||
return { link, nearer, length: Math.fround(Math.hypot(v[3] - v[0], v[4] - v[1], v[5] - v[2])), key: linksDrawn(all.subarray(link * 6, link * 6 + 6), points)[0] };
|
||||
}).sort((a, b) => a.nearer - b.nearer || a.link - b.link);
|
||||
const kept: string[] = [];
|
||||
let total = 0;
|
||||
for (const { length, key } of links) {
|
||||
if (total + length > lengthPc) {
|
||||
break;
|
||||
}
|
||||
total += length;
|
||||
kept.push(key);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
/** Stars a parsec apart along x, as points, for reading a segment buffer back. */
|
||||
function chainPoints(count: number): StarPoint[] {
|
||||
return Array.from({ length: count }, (_, i) => ({ id: i, x: i, y: 0, z: 0 }));
|
||||
@@ -202,6 +226,37 @@ describe('jumpLinkSegments', () => {
|
||||
expect(segments.buffer.byteLength).toBe(segments.byteLength);
|
||||
});
|
||||
|
||||
it('keeps exactly the links a full nearest-first sort would, without sorting them all', () => {
|
||||
let seed = 7;
|
||||
const random = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648) * 40 - 20;
|
||||
const points: StarPoint[] = Array.from({ length: 600 }, (_, id) => ({ id, x: random(), y: random(), z: random() }));
|
||||
const centre = { x: 3, y: -2, z: 1 };
|
||||
|
||||
for (const lengthPc of [0, 5, 60, 900, 4000, 1e9]) {
|
||||
expect(linksDrawn(jumpLinkSegments(index(points), 4, { centre, lengthPc }), points).sort()).toEqual(nearestFirst(points, 4, centre, lengthPc).sort());
|
||||
}
|
||||
});
|
||||
|
||||
it('sorts the distance band the budget runs out in, and stops at the first link there that does not fit', () => {
|
||||
// One pair 4 kpc out makes each band about a parsec deep, so dozens of short links near the
|
||||
// centre share the band the budget ends in, in whatever order the grid walks them.
|
||||
let seed = 3;
|
||||
const random = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648;
|
||||
const points: StarPoint[] = [{ id: 0, x: 4000, y: 0, z: 0 }, { id: 1, x: 4000.03, y: 0, z: 0 }];
|
||||
for (let pair = 0; pair < 40; pair++) {
|
||||
const r = 0.05 + random() * 0.9;
|
||||
const theta = random() * Math.PI * 2;
|
||||
const x = r * Math.cos(theta);
|
||||
const y = r * Math.sin(theta);
|
||||
points.push({ id: 2 + pair * 2, x, y, z: 0 }, { id: 3 + pair * 2, x, y, z: 0.005 + random() * 0.04 });
|
||||
}
|
||||
const centre = { x: 0, y: 0, z: 0 };
|
||||
|
||||
for (const lengthPc of [0.1, 0.3, 0.5]) {
|
||||
expect(linksDrawn(jumpLinkSegments(index(points), 0.05, { centre, lengthPc }), points).sort()).toEqual(nearestFirst(points, 0.05, centre, lengthPc).sort());
|
||||
}
|
||||
});
|
||||
|
||||
it('counts the budget in parsecs of link, not in links', () => {
|
||||
// Stars at 0, 1 and 3: a 2 pc link nearest the centre, then a 1 pc one. Two and a half parsecs
|
||||
// hold the first and not both, though two links would fit a count of two and a half.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user