1// What stands, and why: the rules of `world/visibility.ts` over a small world.2//3// This is the seam's half of the count three surfaces have to agree on, so4// the tests are written as the sentences the rules are: a hidden collection5// takes its pins off the map, a search takes everything it does not name, a6// highlight conjoins across collections and unites within one, a held cell7// narrows the same way — and the selected and the searched are spared by both8// of the last two, because the card open about a feature must not lose it.910import test from "node:test";11import { strict as assert } from "node:assert";12import { EMPTY_SCENE } from "../scene/read.ts";13import type { Scene } from "../scene/read.ts";14import { WorldModel, project } from "../world/model.ts";15import { Visibility } from "../world/visibility.ts";16import type { Collection, TileGrid, WorldPayload } from "../data/payload.ts";17import { LocationTable } from "../data/atlasloc.ts";1819const grid: TileGrid = { sourceZoom: 5, firstTile: 0, tileSize: 256, size: 8192 };2021/** Two point collections, one area collection with two areas side by side. */22function payload(): WorldPayload {23 // GeoJSON pairs are [lng, lat], in the volume's own world space: a square24 // anchored at its north-west corner and running east and south.25 const square = (lng: number, lat: number, size: number) => [[26 [lng, lat], [lng + size, lat], [lng + size, lat - size], [lng, lat - size], [lng, lat],27 ]];28 const collections: Collection[] = [29 { id: 1, title: "Shrines", kind: "point", visible: true },30 { id: 2, title: "Chests", kind: "point", visible: true },31 {32 id: 3, title: "Regions", kind: "area", visible: true,33 features: [34 { id: 100, title: "West", geometry: [{ type: "Polygon", coordinates: square(0, 90, 90) }] },35 { id: 101, title: "East", geometry: [{ type: "Polygon", coordinates: square(90, 90, 90) }] },36 ],37 },38 {39 id: 4, title: "Districts", kind: "area", visible: true,40 features: [41 { id: 200, title: "North", geometry: [{ type: "Polygon", coordinates: square(0, 90, 180) }] },42 ],43 },44 ];45 return { lenses: [], collections };46}4748/** Positions are lat/lng in the world's own space; these are far apart. */49const points = [50 { id: 1, owner: 0, lat: 40, lng: 10, member: 0, shard: 0, title: "Alpha" },51 { id: 2, owner: 0, lat: 40, lng: 120, member: 0, shard: 0, title: "Beta" },52 { id: 3, owner: 1, lat: 10, lng: 10, member: 0, shard: 1, title: "Gamma" },53 { id: 4, owner: 1, lat: 10, lng: 120, member: 0, shard: 2, title: "Alpha Two" },54];5556function table(): LocationTable {57 const encoder = new TextEncoder();58 const n = points.length;59 const titles = points.map((point) => encoder.encode(point.title));60 const total = titles.reduce((held, run) => held + run.length, 0);61 const buffer = new ArrayBuffer(20 + 26 * n + total);62 const bytes = new Uint8Array(buffer);63 bytes.set(encoder.encode("ATLASLOC"), 0);64 const header = new DataView(buffer);65 header.setUint16(8, 3, true);66 header.setUint32(10, n, true);67 const id = new Int32Array(buffer, 16, n);68 const lat = new Float32Array(buffer, 16 + 4 * n, n);69 const lng = new Float32Array(buffer, 16 + 8 * n, n);70 const shard = new Int32Array(buffer, 16 + 16 * n, n);71 const offsets = new Uint32Array(buffer, 16 + 20 * n, n + 1);72 const owner = new Uint16Array(buffer, 20 + 24 * n, n);73 let at = 0;74 points.forEach((point, i) => {75 id[i] = point.id; lat[i] = point.lat; lng[i] = point.lng;76 shard[i] = point.shard; owner[i] = point.owner; offsets[i] = at;77 bytes.set(titles[i] ?? new Uint8Array(), 20 + 26 * n + at);78 at += (titles[i] ?? new Uint8Array()).length;79 });80 offsets[n] = at;81 return LocationTable.over(buffer);82}8384const model = new WorldModel("w", payload(), grid, table());8586function scene(over: Partial<Scene> = {}): Scene {87 return { ...EMPTY_SCENE, ...over };88}8990test("with nothing filtered, everything stands", () => {91 const standing = new Visibility(model, scene(), 0, null);92 assert.equal(standing.eligible, 4);93 assert.equal(standing.drawn, 4 + 3, "pins and shapes are one count");94 assert.equal(standing.listable, 7);95});9697test("a hidden collection takes its own off the map and out of the count", () => {98 const standing = new Visibility(model, scene({ hidden: new Set(["1"]) }), 0, null);99 assert.equal(standing.eligible, 2);100 assert.deepEqual([...standing.standing()].map((point) => point.title), ["Gamma", "Alpha Two"]);101102 const shapes = new Visibility(model, scene({ hidden: new Set(["3"]) }), 0, null);103 assert.equal(shapes.shapesShown.length, 1, "a hidden area collection is not drawn");104});105106test("a search leaves what it names, and promotes it", () => {107 const standing = new Visibility(model, scene({ search: "alpha" }), 0, null);108 assert.deepEqual([...standing.standing()].map((point) => point.title), ["Alpha", "Alpha Two"]);109 assert.equal(standing.at(0).promoted, true);110 assert.equal(standing.at(1).promoted, false, "a filtered pin is not promoted");111});112113test("a split world offers one layer at a time", () => {114 // Shard 1 admits the unsharded pins and its own; shard 2's belongs115 // elsewhere in the world rather than being merely filtered out.116 const standing = new Visibility(model, scene(), 1, null);117 assert.deepEqual([...standing.standing()].map((point) => point.title),118 ["Alpha", "Beta", "Gamma"]);119});120121test("highlights conjoin across collections and unite within one", () => {122 // West holds Alpha and Gamma; North holds all four. Highlighting West alone123 // leaves the two inside it.124 const west = new Visibility(model, scene({ highlighted: new Set(["100"]) }), 0, null);125 assert.deepEqual([...west.standing()].map((point) => point.title), ["Alpha", "Gamma"]);126127 // West and East together are alternatives within one collection: their128 // union stands.129 const both = new Visibility(model, scene({ highlighted: new Set(["100", "101"]) }), 0, null);130 assert.equal(both.eligible, 4);131132 // West and North are two collections, so they are conditions: the pins133 // inside both. North contains everything, so the answer is West's.134 const across = new Visibility(model, scene({ highlighted: new Set(["100", "200"]) }), 0, null);135 assert.deepEqual([...across.standing()].map((point) => point.title), ["Alpha", "Gamma"]);136 assert.equal(across.focusedPins, 2);137});138139test("the selected and the searched are spared a cull, and nothing else is", () => {140 const spared = new Visibility(141 model, scene({ highlighted: new Set(["100"]), selected: "2" }), 0, null);142 assert.ok([...spared.standing()].some((point) => point.title === "Beta"),143 "the card open about a feature does not lose it");144145 const searched = new Visibility(146 model, scene({ highlighted: new Set(["100"]), search: "beta" }), 0, null);147 assert.deepEqual([...searched.standing()].map((point) => point.title), ["Beta"]);148});149150test("a held cell narrows exactly the way a highlight does", () => {151 const west = project(grid, 0, 0)[0];152 const middle = project(grid, 0, 90)[0];153 const inCell = (at: readonly [number, number]) => at[0] >= west && at[0] < middle;154 const standing = new Visibility(model, scene({ gridCell: "9q" }), 0, inCell);155 assert.deepEqual([...standing.standing()].map((point) => point.title), ["Alpha", "Gamma"]);156 assert.equal(standing.priorityPins, 2, "what the cell holds is what it counts");157 assert.equal(standing.at(0).promoted, true, "a pin in the held cell is promoted");158});159160test("a pin's place in the crowd is stable and rarity-first", () => {161 const shrines = model.points.filter((point) => point.collection.id === 1);162 const chests = model.points.filter((point) => point.collection.id === 2);163 // Equal-sized collections rank equally by rarity, so the tie-break is the164 // id's own hash — and it is the same on every run.165 assert.equal(shrines.length, 2);166 assert.equal(chests.length, 2);167 const again = new WorldModel("w", payload(), grid, table());168 assert.deepEqual(again.points.map((point) => point.priority),169 model.points.map((point) => point.priority));170});