Spaces:
Configuration error
Configuration error
File size: 7,188 Bytes
9f21d0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | // The shared orbit primitive's bookkeeping.
//
// Real GeometryInstance objects — a plain Cesium class that constructs fine in
// the node env. What is faked is the viewer: a clock whose ticks this test drives
// by hand, which is the only way to observe the coalescing window and the
// settled() promise deterministically.
//
// What is not covered is the build itself. Driving a Primitive through its
// creation states calls into Cesium's renderer, which needs a real WebGL context,
// so the cases here stop at the point a build starts and pick up again on the
// paths that never reach one. That still leaves the scheduling, the coalescing,
// the empty-batch teardown and settled().
import { ArcType, Cartesian3, Color, ColorGeometryInstanceAttribute, GeometryInstance, JulianDate, PolylineColorAppearance, PolylineGeometry, SceneMode } from "@cesium/engine";
import type { Viewer } from "@cesium/widgets";
import { describe, expect, test } from "vitest";
import { PolylineBatch } from "./PolylineBatch";
function orbitGeometry(id: string): GeometryInstance {
return new GeometryInstance({
geometry: new PolylineGeometry({
positions: Cartesian3.fromDegreesArray([0, 0, 10, 10]),
width: 2,
arcType: ArcType.NONE,
vertexFormat: PolylineColorAppearance.VERTEX_FORMAT,
}),
attributes: { color: ColorGeometryInstanceAttribute.fromColor(new Color(1, 1, 1, 0.15)) },
id,
});
}
/** A viewer with a hand-cranked clock and a primitive collection that records. */
function fakeViewer() {
const listeners = new Set<() => void>();
const primitives = {
added: [] as unknown[],
removed: [] as unknown[],
add(primitive: unknown) {
this.added.push(primitive);
},
remove(primitive: unknown) {
this.removed.push(primitive);
return true;
},
};
let renders = 0;
const viewer = {
clock: {
currentTime: JulianDate.fromIso8601("2026-01-01T00:00:00Z"),
onTick: {
addEventListener(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
},
},
},
scene: {
mode: SceneMode.SCENE3D,
primitives,
frameState: {},
requestRender() {
renders += 1;
},
},
};
return {
viewer: viewer as unknown as Viewer,
primitives,
get renders() {
return renders;
},
get listenerCount() {
return listeners.size;
},
tick(times = 1) {
for (let i = 0; i < times; i += 1) {
[...listeners].forEach((listener) => listener());
}
},
};
}
// Mirrors PolylineBatch's own window; the callback fires on the tick after it.
const COALESCE_TICKS = 30;
const PAST_WINDOW = COALESCE_TICKS + 2;
describe("PolylineBatch", () => {
test("nothing added, nothing pending, settled resolves at once", async () => {
const { viewer } = fakeViewer();
const batch = new PolylineBatch(viewer);
expect(batch.pending).toBe(false);
await expect(batch.settled()).resolves.toBeUndefined();
});
test("an add is pending, and nothing reaches the scene before the window closes", () => {
const host = fakeViewer();
const batch = new PolylineBatch(host.viewer);
batch.add(orbitGeometry("ISS"));
expect(batch.pending).toBe(true);
// Ticking short of the window must not start the build.
host.tick(COALESCE_TICKS - 1);
expect(host.primitives.added).toHaveLength(0);
expect(batch.pending).toBe(true);
});
test("many adds in one window cost one rebuild", () => {
const host = fakeViewer();
const batch = new PolylineBatch(host.viewer);
const before = host.listenerCount;
for (let i = 0; i < 50; i += 1) {
batch.add(orbitGeometry(`SAT-${i}`));
}
// One scheduled rebuild, not fifty: the second add sees `#scheduled` and
// returns rather than registering another tick callback.
expect(host.listenerCount).toBe(before + 1);
});
test("removing the last geometry clears the batch and settles", async () => {
const host = fakeViewer();
const batch = new PolylineBatch(host.viewer);
const geometry = orbitGeometry("ISS");
batch.add(geometry);
batch.remove(geometry);
expect(batch.pending).toBe(true);
const settled = batch.settled();
host.tick(PAST_WINDOW);
await expect(settled).resolves.toBeUndefined();
expect(batch.pending).toBe(false);
});
test("the empty rebuild stops its own tick callback", () => {
const host = fakeViewer();
const batch = new PolylineBatch(host.viewer);
// The permanent inertial-frame updater registered by the constructor.
const permanent = host.listenerCount;
const geometry = orbitGeometry("ISS");
batch.add(geometry);
batch.remove(geometry);
host.tick(PAST_WINDOW);
// The rebuild callback used to `return` out of the empty branch without
// removing itself, leaving a listener requesting a render every 30 ticks for
// the rest of the session — and another one on every subsequent empty pass.
expect(host.listenerCount).toBe(permanent);
});
test("settled() called when idle does not wait for a tick", async () => {
const host = fakeViewer();
const batch = new PolylineBatch(host.viewer);
const geometry = orbitGeometry("ISS");
batch.add(geometry);
batch.remove(geometry);
host.tick(PAST_WINDOW);
await batch.settled();
// Resolves without anything having to tick again.
await expect(batch.settled()).resolves.toBeUndefined();
});
test("replace swaps a member's geometry in place and schedules one rebuild", () => {
const host = fakeViewer();
const batch = new PolylineBatch(host.viewer, "fixed");
const first = orbitGeometry("ISS");
const second = orbitGeometry("ISS");
batch.add(first);
expect(batch.replace(first, second)).toBe(true);
// A swap is not a membership change: the batch is still one geometry long.
expect(batch.size).toBe(1);
expect(batch.pending).toBe(true);
});
test("replace refuses a geometry that is no longer a member", () => {
const host = fakeViewer();
const batch = new PolylineBatch(host.viewer, "fixed");
const stale = orbitGeometry("ISS");
// The satellite disabled its track between the refresh being scheduled and
// it running: the caller has to be told, or the batch grows a line for a
// component nobody is drawing.
expect(batch.replace(stale, orbitGeometry("ISS"))).toBe(false);
expect(batch.size).toBe(0);
});
test("only the inertial batch installs a periodic re-orientation", () => {
const host = fakeViewer();
const before = host.listenerCount;
// The inertial batch keeps a permanent listener to spin its model matrix.
const inertial = new PolylineBatch(host.viewer, "inertial");
expect(inertial.size).toBe(0);
expect(host.listenerCount).toBe(before + 1);
// An Earth-relative one is already in the frame it is drawn in, so there is
// no matrix for a listener to maintain.
const fixed = new PolylineBatch(host.viewer, "fixed");
expect(fixed.size).toBe(0);
expect(host.listenerCount).toBe(before + 1);
});
});
|