Link only the stars that are drawn
The jump-link graph linked the whole catalogue: 3.7 million links at 8 pc, 7.4-7.8 s in the worker and a 443-515 ms frame on the main thread when they landed, and most of them between stars that were neither drawn nor clickable. A graph request now carries the star field's drawn stars, and the worker links only those, over an index of its own with cells as wide as the range. The scene asks again once a new drawn set has held still for 250 ms. The renderer is handed the graph's bounding sphere instead of computing it: three.js walked every vertex on the main thread in the first frame that drew a new graph, 48-55 ms at 8 pc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
@@ -11,6 +11,7 @@ import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
||||
import { StarRecord } from '../../shared/models/star.model';
|
||||
import { NavigationStore } from '../../shared/state/navigation.store';
|
||||
import { GalaxySystemSceneComponent } from './galaxy-system-scene.component';
|
||||
import { JumpLinkRenderer } from './jump-link-renderer';
|
||||
import { StarFieldRenderer } from './star-field-renderer';
|
||||
|
||||
// jsdom does not implement ResizeObserver; the component only uses it to react to real
|
||||
@@ -266,6 +267,85 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
||||
refocus.mockRestore();
|
||||
});
|
||||
|
||||
describe('the jump-link graph', () => {
|
||||
type LinkScene = {
|
||||
routing: { links(rangePc: number, drawn: Uint32Array): Promise<Float32Array>; route(): Promise<never>; dispose(): void };
|
||||
display: { update(change: (display: { jumpLinks: boolean }) => unknown): void };
|
||||
jumpRangePc: { set(rangePc: number): void };
|
||||
routeResult: { set(value: unknown): void };
|
||||
controls: { target: THREE.Vector3 };
|
||||
starField: { drawnStars: Uint32Array; drawn: Uint32Array };
|
||||
};
|
||||
/** Real time, since the rebuild waits on a real timer for the range and the drawn stars to settle. */
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
function linkScene(links: LinkScene['routing']['links']): LinkScene {
|
||||
const component = fixture.componentInstance as unknown as LinkScene;
|
||||
component.routing = { links, route: () => new Promise<never>(() => undefined), dispose: () => undefined };
|
||||
component.display.update((display) => ({ ...display, jumpLinks: true }));
|
||||
TestBed.tick();
|
||||
return component;
|
||||
}
|
||||
|
||||
/** Makes the next refocus choose a different set: the field is told it draws one star, then the view moves. */
|
||||
async function changeDrawnStars(component: LinkScene, targetX: number): Promise<void> {
|
||||
component.starField.drawn = Uint32Array.of(0);
|
||||
component.controls.target.set(targetX, 0, 0);
|
||||
await advanceFrames(engine, 0.3);
|
||||
}
|
||||
|
||||
it('links the stars being drawn, and asks again once a new set of them holds still', async () => {
|
||||
const links = vi.fn((_rangePc: number, _drawn: Uint32Array) => Promise.resolve(new Float32Array(0)));
|
||||
const component = linkScene(links);
|
||||
await settle();
|
||||
expect(links).toHaveBeenCalledTimes(1);
|
||||
expect(links.mock.calls[0]).toEqual([3, component.starField.drawnStars]);
|
||||
|
||||
await changeDrawnStars(component, 40);
|
||||
expect(links).toHaveBeenCalledTimes(1);
|
||||
await settle();
|
||||
expect(links).toHaveBeenCalledTimes(2);
|
||||
expect(links.mock.calls[1][1]).toBe(component.starField.drawnStars);
|
||||
expect(links.mock.calls[1][1]).not.toBe(links.mock.calls[0][1]);
|
||||
|
||||
// A route re-chooses the drawn stars around its pins, and here they come out the same: no new graph.
|
||||
component.routeResult.set({ stars: [{ id: SUN.id, name: 'Sol' }], totalPc: 0, neededRangePc: null });
|
||||
await advanceFrames(engine, 0.3);
|
||||
await settle();
|
||||
expect(links).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('draws a late graph for the range still asked for, and not one for a range left behind', async () => {
|
||||
const answers: Array<(segments: Float32Array) => void> = [];
|
||||
const setSegments = vi.spyOn(JumpLinkRenderer.prototype, 'setSegments');
|
||||
const component = linkScene(() => new Promise<Float32Array>((resolve) => answers.push(resolve)));
|
||||
await settle();
|
||||
await changeDrawnStars(component, 40);
|
||||
await settle();
|
||||
expect(answers).toHaveLength(2);
|
||||
|
||||
// For stars no longer drawn, but at the range still asked for: newer than what is on screen.
|
||||
const olderSet = new Float32Array(6);
|
||||
answers[0](olderSet);
|
||||
await flushAsync();
|
||||
expect(setSegments).toHaveBeenLastCalledWith(olderSet);
|
||||
|
||||
component.jumpRangePc.set(5);
|
||||
TestBed.tick();
|
||||
await settle();
|
||||
expect(answers).toHaveLength(3);
|
||||
answers[1](new Float32Array(12));
|
||||
await flushAsync();
|
||||
expect(setSegments).toHaveBeenLastCalledWith(olderSet);
|
||||
|
||||
const current = new Float32Array(18);
|
||||
answers[2](current);
|
||||
await flushAsync();
|
||||
expect(setSegments).toHaveBeenLastCalledWith(current);
|
||||
setSegments.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the answer to the latest route asked for, whatever order the answers arrive in', async () => {
|
||||
type Answer = { route: { stars: number[]; totalPc: number; longestHopPc: number } | null; neededRangePc: number | null };
|
||||
const answers: Array<(answer: Answer) => void> = [];
|
||||
|
||||
@@ -71,7 +71,10 @@ const LABEL_MIN_SEPARATION_NDC = 0.12;
|
||||
const LABEL_EDGE_NDC = 0.7;
|
||||
/** How far right of its point a label's text reaches, in aspect-scaled NDC (~135px at 1440). */
|
||||
const LABEL_REACH_NDC = 0.3;
|
||||
/** How long the range control has to be still before the graph is rebuilt at its value. */
|
||||
/**
|
||||
* How long the range control, and the set of drawn stars, have to be still before the graph is
|
||||
* rebuilt for them: a drag emits per pixel, and a flight re-chooses the drawn stars every few passes.
|
||||
*/
|
||||
const JUMP_LINK_REBUILD_DELAY_MS = 250;
|
||||
|
||||
/**
|
||||
@@ -355,8 +358,9 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
);
|
||||
});
|
||||
private readonly routeQuery = signal('');
|
||||
/** The range the drawn graph was last built at, so a redraw is skipped when nothing moved. */
|
||||
/** The range and the stars the drawn graph was last asked for, so a rebuild is skipped when neither moved. */
|
||||
private drawnJumpRangePc: number | null = null;
|
||||
private linkedStars: Uint32Array | null = null;
|
||||
private jumpLinkRebuild?: ReturnType<typeof setTimeout>;
|
||||
/** The current system's neighbours, resolved on arrival: id, name, distance and bearing. */
|
||||
private neighbours: readonly { star: StarRecord; distancePc: number; direction: THREE.Vector3 }[] = [];
|
||||
@@ -420,14 +424,11 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
});
|
||||
effect(() => this.applyDisplay(this.display()));
|
||||
effect(() => this.applyProjection(this.display().plan));
|
||||
// Reads both signals, so flipping the layer on and dragging the range each land here. The
|
||||
// rebuild is a quarter-second of walking the catalogue, and the range control emits per
|
||||
// pixel dragged, so it waits for the hand to settle rather than running once per pixel.
|
||||
// Reads both signals, so flipping the layer on and dragging the range each land here.
|
||||
effect(() => {
|
||||
this.jumpRangePc();
|
||||
this.display().jumpLinks;
|
||||
clearTimeout(this.jumpLinkRebuild);
|
||||
this.jumpLinkRebuild = setTimeout(() => this.refreshJumpLinks(), JUMP_LINK_REBUILD_DELAY_MS);
|
||||
this.scheduleJumpLinks();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -807,6 +808,8 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
this.starField.refocus({ centre, pinned });
|
||||
this.starFieldFocus = centre.clone();
|
||||
this.starFieldPins = pins;
|
||||
// The graph links the drawn stars, so a new set wants a new graph once it stops changing.
|
||||
this.scheduleJumpLinks();
|
||||
}
|
||||
|
||||
private updateLabels(camera: SceneCamera): void {
|
||||
@@ -1449,14 +1452,23 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/** Rebuilds the graph once the range and the drawn stars have held still. */
|
||||
private scheduleJumpLinks(): void {
|
||||
clearTimeout(this.jumpLinkRebuild);
|
||||
this.jumpLinkRebuild = setTimeout(() => this.refreshJumpLinks(), JUMP_LINK_REBUILD_DELAY_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the drawn graph, which is the expensive half: every star's neighbours, once, and 3.7
|
||||
* million links at 8 pc, so it is built in the worker. Only when the layer is on and the range
|
||||
* has actually moved — the control emits per pixel dragged — and only the graph for the range
|
||||
* last asked for is drawn, in whatever order the answers arrive.
|
||||
* Rebuilds the drawn graph: the links between the stars the field is drawing, so what is linked
|
||||
* is what can be seen and clicked. Hundreds of thousands of links at 8 pc, so it is built in the
|
||||
* worker, and only when the layer is on and the range or the drawn stars have actually changed.
|
||||
*
|
||||
* An answer is drawn if it is for the range last asked for, even when the drawn stars have moved
|
||||
* on since: the client answers in the order it was asked, so it is never older than the graph on
|
||||
* screen, and holding out for the latest set would draw nothing while the view keeps moving.
|
||||
*/
|
||||
private refreshJumpLinks(): void {
|
||||
if (!this.jumpLinks || !this.routing) {
|
||||
if (!this.jumpLinks || !this.routing || !this.starField) {
|
||||
return;
|
||||
}
|
||||
const rangePc = this.jumpRangePc();
|
||||
@@ -1464,24 +1476,28 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
if (this.drawnJumpRangePc !== null) {
|
||||
this.jumpLinks.setSegments(new Float32Array(0));
|
||||
this.drawnJumpRangePc = null;
|
||||
this.linkedStars = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.drawnJumpRangePc === rangePc) {
|
||||
const drawn = this.starField.drawnStars;
|
||||
if (this.drawnJumpRangePc === rangePc && this.linkedStars === drawn) {
|
||||
return;
|
||||
}
|
||||
this.drawnJumpRangePc = rangePc;
|
||||
void this.routing.links(rangePc).then(
|
||||
this.linkedStars = drawn;
|
||||
void this.routing.links(rangePc, drawn).then(
|
||||
(segments) => {
|
||||
if (this.drawnJumpRangePc === rangePc) {
|
||||
this.jumpLinks?.setSegments(segments);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// Replaced by a newer range, or failed. Either way this range is not drawn, and must not be
|
||||
// remembered as if it were, or asking for it again would be skipped.
|
||||
if (this.drawnJumpRangePc === rangePc) {
|
||||
// Replaced by a newer request, or failed. Either way this graph is not drawn, and must not
|
||||
// be remembered as if it were, or asking for it again would be skipped.
|
||||
if (this.drawnJumpRangePc === rangePc && this.linkedStars === drawn) {
|
||||
this.drawnJumpRangePc = null;
|
||||
this.linkedStars = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -53,6 +53,10 @@ export class JumpLinkRenderer {
|
||||
/** The graph, as vertex pairs: six floats a link, one end then the other. See `jumpLinkSegments`. */
|
||||
setSegments(vertices: Float32Array): void {
|
||||
this.replaceGeometry(this.links, vertices);
|
||||
// Given rather than left for the renderer to compute: it wants a bounding sphere to sort by and,
|
||||
// finding none, walks every vertex on the main thread in the first frame that draws the graph.
|
||||
// The graph is never culled, and it sorts by its centre, where the catalogue is centred too.
|
||||
this.links.geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), Infinity);
|
||||
}
|
||||
|
||||
/** The chain to draw over the graph, departure first. Fewer than two stars draws nothing. */
|
||||
|
||||
@@ -18,6 +18,8 @@ const STARS: StarRecord[] = Array.from({ length: 6 }, (_, i) => ({
|
||||
}));
|
||||
const POSITIONS = Float32Array.from(STARS.flatMap((star) => [star.x, star.y, star.z]));
|
||||
const index = new StarNeighbourhood(STARS);
|
||||
/** Every star drawn. */
|
||||
const ALL = Uint32Array.from(STARS.keys());
|
||||
|
||||
/** Flushes settled promises and their handlers. */
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
@@ -25,11 +27,13 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
/** A worker that records what it is sent and answers only when told to. */
|
||||
class FakeWorker {
|
||||
readonly sent: Array<RoutingRequest | { kind: 'catalogue' }> = [];
|
||||
readonly transferred: ArrayBufferLike[] = [];
|
||||
private readonly listeners: Record<string, Array<(event: { data?: unknown }) => void>> = {};
|
||||
terminated = false;
|
||||
|
||||
postMessage(message: RoutingRequest | { kind: 'catalogue' }): void {
|
||||
postMessage(message: RoutingRequest | { kind: 'catalogue' }, transfer: Transferable[] = []): void {
|
||||
this.sent.push(message);
|
||||
this.transferred.push(...(transfer as ArrayBufferLike[]));
|
||||
}
|
||||
|
||||
addEventListener(type: string, listener: (event: { data?: unknown }) => void): void {
|
||||
@@ -86,7 +90,15 @@ describe('RoutingClient without a worker', () => {
|
||||
it('answers the graph as segments', async () => {
|
||||
const client = new RoutingClient(STARS, POSITIONS, index);
|
||||
|
||||
expect(Array.from(await client.links(1.5))).toEqual(Array.from(jumpLinkSegments(index, 1.5)));
|
||||
expect(Array.from(await client.links(1.5, ALL))).toEqual(Array.from(jumpLinkSegments(index, 1.5)));
|
||||
client.dispose();
|
||||
});
|
||||
|
||||
it('links only the stars it is told are drawn', async () => {
|
||||
const client = new RoutingClient(STARS, POSITIONS, index);
|
||||
|
||||
// Stars at x = 0, 1 and 3: only the first two are within 1.5 pc of each other.
|
||||
expect(Array.from(await client.links(1.5, Uint32Array.of(0, 1, 3)))).toEqual([0, 0, 0, 1, 0, 0]);
|
||||
client.dispose();
|
||||
});
|
||||
});
|
||||
@@ -95,8 +107,8 @@ describe('RoutingClient with a worker', () => {
|
||||
it('sends the catalogue first, then one request at a time', () => {
|
||||
const { client, worker } = clientWithFake();
|
||||
|
||||
void client.links(8);
|
||||
void client.links(3);
|
||||
void client.links(8, ALL);
|
||||
void client.links(3, ALL);
|
||||
|
||||
expect(worker.sent[0].kind).toBe('catalogue');
|
||||
expect(worker.requests).toHaveLength(1);
|
||||
@@ -107,9 +119,9 @@ describe('RoutingClient with a worker', () => {
|
||||
// range slider used to queue another, and a route asked for after them waited behind them all.
|
||||
it('replaces a waiting graph with the newer one before it is ever built, and sends a route ahead of it', async () => {
|
||||
const { client, worker } = clientWithFake();
|
||||
const first = client.links(5);
|
||||
const superseded = client.links(6).catch((error: unknown) => error);
|
||||
const latest = client.links(8);
|
||||
const first = client.links(5, ALL);
|
||||
const superseded = client.links(6, ALL).catch((error: unknown) => error);
|
||||
const latest = client.links(8, ALL);
|
||||
const route = client.route(100, 104, 1.5, 8);
|
||||
|
||||
const building = worker.requests[0];
|
||||
@@ -132,23 +144,42 @@ describe('RoutingClient with a worker', () => {
|
||||
client.dispose();
|
||||
});
|
||||
|
||||
it('shares the answer to a question already on its way rather than asking it twice', async () => {
|
||||
it('shares the answer to a route already on its way rather than asking it twice', async () => {
|
||||
const { client, worker } = clientWithFake();
|
||||
const once = client.links(7.5);
|
||||
const again = client.links(7.5);
|
||||
const once = client.route(100, 104, 1.5, 8);
|
||||
const again = client.route(100, 104, 1.5, 8);
|
||||
|
||||
expect(worker.requests).toHaveLength(1);
|
||||
worker.answer({ kind: 'route', requestId: worker.requests[0].requestId, route: null, neededRangePc: 4 });
|
||||
|
||||
expect(await again).toEqual(await once);
|
||||
expect(worker.requests).toHaveLength(1);
|
||||
client.dispose();
|
||||
});
|
||||
|
||||
it('builds a graph for each set of drawn stars asked about, and never gives the list away', async () => {
|
||||
const { client, worker } = clientWithFake();
|
||||
const near = Uint32Array.of(0, 1, 2);
|
||||
const far = Uint32Array.of(3, 4, 5);
|
||||
void client.links(3, near);
|
||||
const second = client.links(3, far);
|
||||
|
||||
worker.answer({ kind: 'links', requestId: worker.requests[0].requestId, segments: new Float32Array(6) });
|
||||
await flush();
|
||||
|
||||
expect(await again).toBe(await once);
|
||||
expect(worker.requests).toHaveLength(1);
|
||||
expect(worker.requests.map((request) => request.kind === 'links' && Array.from(request.drawn))).toEqual([[0, 1, 2], [3, 4, 5]]);
|
||||
worker.answer({ kind: 'links', requestId: worker.requests[1].requestId, segments: new Float32Array(12) });
|
||||
await expect(second).resolves.toHaveLength(12);
|
||||
// The star field goes on drawing and picking from these lists, so they are copied, not moved.
|
||||
expect(worker.transferred).not.toContain(near.buffer);
|
||||
expect(worker.transferred).not.toContain(far.buffer);
|
||||
client.dispose();
|
||||
});
|
||||
|
||||
it('rejects a request the worker failed on, and goes on to the next', async () => {
|
||||
const { client, worker } = clientWithFake();
|
||||
const failing = client.route(100, 104, 1.5, 8).catch((error: unknown) => error);
|
||||
const next = client.links(3);
|
||||
const next = client.links(3, ALL);
|
||||
|
||||
worker.answer({ kind: 'failed', requestId: worker.requests[0].requestId, message: 'out of memory' });
|
||||
|
||||
@@ -163,12 +194,12 @@ describe('RoutingClient with a worker', () => {
|
||||
it('answers in place what a worker that failed to load left outstanding, and everything after', async () => {
|
||||
const { client, worker } = clientWithFake();
|
||||
const route = client.route(100, 104, 1.5, 8);
|
||||
const graph = client.links(1.5);
|
||||
const graph = client.links(1.5, Uint32Array.of(0, 1, 3));
|
||||
|
||||
worker.fail();
|
||||
|
||||
await expect(route).resolves.toEqual({ route: routeBetween(index, 100, 104, 1.5), neededRangePc: null });
|
||||
expect(Array.from(await graph)).toEqual(Array.from(jumpLinkSegments(index, 1.5)));
|
||||
expect(Array.from(await graph)).toEqual([0, 0, 0, 1, 0, 0]);
|
||||
await expect(client.route(100, 105, 1.5, 8)).resolves.toMatchObject({ route: null });
|
||||
expect(worker.terminated).toBe(true);
|
||||
client.dispose();
|
||||
|
||||
@@ -18,7 +18,11 @@ export class SupersededRequest extends Error {
|
||||
/** A request made and not yet answered: what was asked, and the promise whoever asked is holding. */
|
||||
interface Outstanding {
|
||||
readonly request: RoutingRequest;
|
||||
/** The question without its id, so the same question asked twice can be recognised. */
|
||||
/**
|
||||
* A route question without its id, so the same route asked for twice can be recognised. Empty for
|
||||
* a graph: the scene never asks for the same graph twice, and spelling out 70 000 drawn stars to
|
||||
* compare costs more than the comparison could save.
|
||||
*/
|
||||
readonly question: string;
|
||||
readonly promise: Promise<RoutingResponse>;
|
||||
readonly resolve: (response: RoutingResponse) => void;
|
||||
@@ -34,7 +38,7 @@ function outstanding(request: RoutingRequest): Outstanding {
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { requestId, ...question } = request;
|
||||
return { request, question: JSON.stringify(question), promise, resolve, reject };
|
||||
return { request, question: request.kind === 'route' ? JSON.stringify(question) : '', promise, resolve, reject };
|
||||
}
|
||||
|
||||
/** The routing worker, where this environment has one. */
|
||||
@@ -50,7 +54,7 @@ function startRoutingWorker(): Worker | undefined {
|
||||
* at 8 pc is seconds of work. So requests are held here and sent one by one, and while one is out,
|
||||
* only the latest of each kind waits behind it — a newer graph replaces an older one before it is
|
||||
* ever built, and the older promise is rejected with {@link SupersededRequest}. Routes go ahead of
|
||||
* graphs, being quick and asked for by a click. The same question asked again while it is still
|
||||
* graphs, being quick and asked for by a click. The same route asked for again while it is still
|
||||
* outstanding shares the answer rather than being worked out twice.
|
||||
*
|
||||
* Where there is no worker — the unit tests' DOM has none, and a worker can fail to load or crash —
|
||||
@@ -89,9 +93,12 @@ export class RoutingClient {
|
||||
);
|
||||
}
|
||||
|
||||
/** Vertex pairs for every link within `rangePc`, three floats to an end. */
|
||||
links(rangePc: number): Promise<Float32Array> {
|
||||
return this.ask({ kind: 'links', requestId: this.nextRequestId++, rangePc }).then((response) =>
|
||||
/**
|
||||
* Vertex pairs for every link within `rangePc` between two of the `drawn` stars (catalogue
|
||||
* indices), three floats to an end.
|
||||
*/
|
||||
links(rangePc: number, drawn: Uint32Array): Promise<Float32Array> {
|
||||
return this.ask({ kind: 'links', requestId: this.nextRequestId++, rangePc, drawn }).then((response) =>
|
||||
response.kind === 'links' ? response.segments : new Float32Array(0)
|
||||
);
|
||||
}
|
||||
@@ -109,7 +116,7 @@ export class RoutingClient {
|
||||
return new Promise((resolve) => resolve(answerRouting(this.localIndex, request)));
|
||||
}
|
||||
const asked = outstanding(request);
|
||||
const same = [this.inFlight, this.waiting[request.kind]].find((other) => other?.question === asked.question);
|
||||
const same = asked.question ? [this.inFlight, this.waiting[request.kind]].find((other) => other?.question === asked.question) : undefined;
|
||||
if (same) {
|
||||
return same.promise;
|
||||
}
|
||||
@@ -129,6 +136,8 @@ export class RoutingClient {
|
||||
}
|
||||
delete this.waiting[next.request.kind];
|
||||
this.inFlight = next;
|
||||
// Cloned, never transferred: a graph's `drawn` is the star field's own list, still drawn and
|
||||
// picked from, and answered in place from should the worker die.
|
||||
this.worker.postMessage(next.request);
|
||||
}
|
||||
|
||||
|
||||
@@ -306,6 +306,14 @@ export class StarFieldRenderer {
|
||||
return this.drawn.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The catalogue indices being drawn. Replaced by a refocus that changes them, never changed in
|
||||
* place, so the same array means the same stars.
|
||||
*/
|
||||
get drawnStars(): Uint32Array {
|
||||
return this.drawn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the drawn stars again for where the view now is, and rewrites the instance buffers
|
||||
* with them. See {@link selectDrawnStars}.
|
||||
|
||||
Reference in New Issue
Block a user