mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
351 lines
14 KiB
JavaScript
351 lines
14 KiB
JavaScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
FLAT_VIEW,
|
|
PLANET_VIEW,
|
|
DEFAULT_ORBIT_DIST,
|
|
LAYOUT_SCALE_FLOOR,
|
|
PLANET_KIND_ME,
|
|
PLANET_KIND_IFACE_ON,
|
|
PLANET_KIND_PEER,
|
|
assignPlanetHomes,
|
|
clampOrbit,
|
|
clipLineToPositiveW,
|
|
computeLayoutScale,
|
|
layoutToSphere,
|
|
sphereToLayout,
|
|
orbitEye,
|
|
planetLodZoom,
|
|
pickPlanetNode,
|
|
pointerToLayout,
|
|
placePlanetCenters,
|
|
projectPlanetScene,
|
|
raySphere,
|
|
stabilizeLayoutScale,
|
|
normalizeVisualiserViewMode,
|
|
} from "@/js/networkVisualiserPlanet.js";
|
|
import { EDGE_STRIDE, NODE_STRIDE as DRAW_STRIDE } from "@/js/networkVisualiserWebGL.js";
|
|
|
|
function packNode(arr, i, x, y, size = 24, a = 1) {
|
|
const o = i * DRAW_STRIDE;
|
|
arr[o] = x;
|
|
arr[o + 1] = y;
|
|
arr[o + 2] = size;
|
|
arr[o + 6] = a;
|
|
}
|
|
|
|
describe("networkVisualiserPlanet", () => {
|
|
it("normalizes view mode to flat or planet", () => {
|
|
expect(normalizeVisualiserViewMode("planet")).toBe(PLANET_VIEW);
|
|
expect(normalizeVisualiserViewMode("flat")).toBe(FLAT_VIEW);
|
|
expect(normalizeVisualiserViewMode("nope")).toBe(FLAT_VIEW);
|
|
expect(normalizeVisualiserViewMode(null)).toBe(FLAT_VIEW);
|
|
});
|
|
|
|
it("maps the origin to the front of the globe and inverts", () => {
|
|
const p = layoutToSphere(0, 0, 400);
|
|
expect(p.x).toBeCloseTo(0, 5);
|
|
expect(p.y).toBeCloseTo(0, 5);
|
|
expect(p.z).toBeCloseTo(1, 5);
|
|
const back = layoutToSphere(400, 0, 400);
|
|
expect(back.z).toBeCloseTo(-1, 5);
|
|
const round = sphereToLayout(p.x, p.y, p.z, 400);
|
|
expect(round.x).toBeCloseTo(0, 5);
|
|
expect(round.y).toBeCloseTo(0, 5);
|
|
const q = layoutToSphere(120, -40, 400);
|
|
const inv = sphereToLayout(q.x, q.y, q.z, 400);
|
|
expect(inv.x).toBeCloseTo(120, 4);
|
|
expect(inv.y).toBeCloseTo(-40, 4);
|
|
});
|
|
|
|
it("keeps layout scale at least the floor and grows with spread", () => {
|
|
expect(computeLayoutScale(new Float32Array(DRAW_STRIDE), DRAW_STRIDE)).toBe(LAYOUT_SCALE_FLOOR);
|
|
const nodes = new Float32Array(DRAW_STRIDE * 2);
|
|
nodes[0] = 0;
|
|
nodes[1] = 0;
|
|
nodes[DRAW_STRIDE] = 1000;
|
|
nodes[DRAW_STRIDE + 1] = 0;
|
|
expect(computeLayoutScale(nodes, DRAW_STRIDE)).toBeGreaterThan(LAYOUT_SCALE_FLOOR);
|
|
expect(1000 / computeLayoutScale(nodes, DRAW_STRIDE)).toBeLessThan(1);
|
|
});
|
|
|
|
it("keeps layout points on the front-to-back hemisphere", () => {
|
|
const scale = 400;
|
|
const overshoot = layoutToSphere(scale * 2.5, 0, scale);
|
|
expect(overshoot.phi).toBeLessThanOrEqual(Math.PI);
|
|
expect(overshoot.z).toBeCloseTo(-1, 5);
|
|
const atRim = layoutToSphere(scale, 0, scale);
|
|
expect(atRim.z).toBeCloseTo(-1, 5);
|
|
const mid = layoutToSphere(scale * 0.5, 0, scale);
|
|
expect(mid.phi).toBeCloseTo(Math.PI * 0.5, 5);
|
|
expect(mid.z).toBeCloseTo(0, 5);
|
|
});
|
|
|
|
it("clamps orbit pitch and distance", () => {
|
|
const c = clampOrbit(1, 9, 0.2);
|
|
expect(c.pitch).toBeLessThan(1.2);
|
|
expect(c.dist).toBeGreaterThan(2.5);
|
|
const far = clampOrbit(0, 0, 99);
|
|
expect(far.dist).toBeLessThan(17);
|
|
const bad = clampOrbit(Number.NaN, Number.NaN, Number.NaN);
|
|
expect(Number.isFinite(bad.yaw)).toBe(true);
|
|
expect(Number.isFinite(bad.pitch)).toBe(true);
|
|
expect(Number.isFinite(bad.dist)).toBe(true);
|
|
});
|
|
|
|
it("places the default eye in front of the origin", () => {
|
|
const eye = orbitEye(0, 0, DEFAULT_ORBIT_DIST);
|
|
expect(eye.x).toBeCloseTo(0, 5);
|
|
expect(eye.y).toBeCloseTo(0, 5);
|
|
expect(eye.z).toBeCloseTo(DEFAULT_ORBIT_DIST, 5);
|
|
});
|
|
|
|
it("hits the unit sphere from the default camera", () => {
|
|
const eye = orbitEye(0, 0, DEFAULT_ORBIT_DIST);
|
|
const hit = raySphere(eye, { x: 0, y: 0, z: -1 }, 1);
|
|
expect(hit).not.toBeNull();
|
|
expect(hit.z).toBeCloseTo(1, 4);
|
|
});
|
|
|
|
it("maps a center click back to the layout origin", () => {
|
|
const eye = orbitEye(0, 0, DEFAULT_ORBIT_DIST);
|
|
const got = pointerToLayout(400, 300, 800, 600, eye, 400);
|
|
expect(got).not.toBeNull();
|
|
expect(Math.hypot(got.x, got.y)).toBeLessThan(8);
|
|
});
|
|
|
|
it("places interface planets on a ring with the first on +X", () => {
|
|
const one = placePlanetCenters(1);
|
|
expect(one).toHaveLength(1);
|
|
expect(one[0].cx).toBeGreaterThan(1);
|
|
expect(Math.abs(one[0].cz)).toBeLessThan(0.2);
|
|
const two = placePlanetCenters(2);
|
|
expect(two).toHaveLength(2);
|
|
expect(Math.hypot(two[0].cx - two[1].cx, two[0].cz - two[1].cz)).toBeGreaterThan(1);
|
|
});
|
|
|
|
it("homes peers on the nearest interface planet", () => {
|
|
const nodes = new Float32Array(DRAW_STRIDE * 4);
|
|
packNode(nodes, 0, 0, 0, 32);
|
|
packNode(nodes, 1, 200, 0, 24);
|
|
packNode(nodes, 2, -200, 0, 24);
|
|
packNode(nodes, 3, 230, 20, 22);
|
|
const kinds = [PLANET_KIND_ME, PLANET_KIND_IFACE_ON, PLANET_KIND_IFACE_ON, PLANET_KIND_PEER];
|
|
const got = assignPlanetHomes(4, kinds, ["me", "eth0", "wifi", "peer"], nodes);
|
|
expect(got.fallback).toBe(false);
|
|
expect(got.ifaceIndices).toEqual([1, 2]);
|
|
expect(got.home[0]).toBe(-1);
|
|
expect(got.home[1]).toBe(0);
|
|
expect(got.home[2]).toBe(1);
|
|
expect(got.home[3]).toBe(0);
|
|
});
|
|
|
|
it("projects me as a hub and each interface as its own globe", () => {
|
|
const nodes = new Float32Array(DRAW_STRIDE * 3);
|
|
packNode(nodes, 0, 0, 0, 32);
|
|
packNode(nodes, 1, 200, 0, 24);
|
|
packNode(nodes, 2, 240, 30, 22);
|
|
const out = projectPlanetScene({
|
|
nodes,
|
|
edges: new Float32Array(0),
|
|
width: 800,
|
|
height: 600,
|
|
yaw: 0,
|
|
pitch: 0,
|
|
dist: DEFAULT_ORBIT_DIST,
|
|
dark: true,
|
|
idByIndex: ["me", "eth0", "peer"],
|
|
kindByIndex: [PLANET_KIND_ME, PLANET_KIND_IFACE_ON, PLANET_KIND_PEER],
|
|
});
|
|
expect(out.planets).toHaveLength(1);
|
|
expect(out.nodes.length).toBeGreaterThanOrEqual(DRAW_STRIDE * 3);
|
|
expect(out.pick.some((p) => p.id === "me")).toBe(true);
|
|
expect(out.pick.some((p) => p.id === "eth0")).toBe(true);
|
|
expect(out.projected[1].kind).toBe(PLANET_KIND_IFACE_ON);
|
|
expect(
|
|
pickPlanetNode(out.pick, out.pick.find((p) => p.id === "me").sx, out.pick.find((p) => p.id === "me").sy, 24)
|
|
).toBe("me");
|
|
expect(pickPlanetNode(out.pick, -400, -400, 8)).toBeNull();
|
|
});
|
|
|
|
it("builds one globe per interface", () => {
|
|
const nodes = new Float32Array(DRAW_STRIDE * 3);
|
|
packNode(nodes, 0, 0, 0, 32);
|
|
packNode(nodes, 1, 200, 0, 24);
|
|
packNode(nodes, 2, -180, 40, 24);
|
|
const out = projectPlanetScene({
|
|
nodes,
|
|
edges: new Float32Array(0),
|
|
width: 800,
|
|
height: 600,
|
|
yaw: 0.4,
|
|
pitch: 0.2,
|
|
dist: DEFAULT_ORBIT_DIST,
|
|
dark: true,
|
|
idByIndex: ["me", "eth0", "wifi"],
|
|
kindByIndex: [PLANET_KIND_ME, PLANET_KIND_IFACE_ON, PLANET_KIND_IFACE_ON],
|
|
});
|
|
expect(out.planets).toHaveLength(2);
|
|
expect(out.planets[0].id).toBe("eth0");
|
|
expect(out.planets[1].id).toBe("wifi");
|
|
expect(
|
|
Math.hypot(out.planets[0].cx - out.planets[1].cx, out.planets[0].cz - out.planets[1].cz)
|
|
).toBeGreaterThan(1);
|
|
});
|
|
|
|
it("marks a far-side peer as back-facing on its interface globe", () => {
|
|
const nodes = new Float32Array(DRAW_STRIDE * 3);
|
|
packNode(nodes, 0, 0, 0, 32);
|
|
packNode(nodes, 1, 200, 0, 24);
|
|
packNode(nodes, 2, 200 + 400, 0, 22);
|
|
const out = projectPlanetScene({
|
|
nodes,
|
|
edges: new Float32Array(0),
|
|
width: 800,
|
|
height: 600,
|
|
yaw: 0,
|
|
pitch: 0,
|
|
dist: DEFAULT_ORBIT_DIST,
|
|
dark: true,
|
|
idByIndex: ["me", "eth0", "peer"],
|
|
kindByIndex: [PLANET_KIND_ME, PLANET_KIND_IFACE_ON, PLANET_KIND_PEER],
|
|
});
|
|
expect(out.projected[2].front).toBe(false);
|
|
expect(out.pick.some((p) => p.id === "peer")).toBe(false);
|
|
expect(out.pick.some((p) => p.id === "eth0")).toBe(true);
|
|
});
|
|
|
|
it("maps a click on an interface globe back near that interface layout", () => {
|
|
const nodes = new Float32Array(DRAW_STRIDE * 2);
|
|
packNode(nodes, 0, 0, 0, 32);
|
|
packNode(nodes, 1, 200, 0, 24);
|
|
const out = projectPlanetScene({
|
|
nodes,
|
|
edges: new Float32Array(0),
|
|
width: 800,
|
|
height: 600,
|
|
yaw: 0,
|
|
pitch: 0,
|
|
dist: DEFAULT_ORBIT_DIST,
|
|
dark: true,
|
|
idByIndex: ["me", "eth0"],
|
|
kindByIndex: [PLANET_KIND_ME, PLANET_KIND_IFACE_ON],
|
|
});
|
|
const eye = orbitEye(0, 0, DEFAULT_ORBIT_DIST);
|
|
const pl = out.planets[0];
|
|
const eth = out.pick.find((p) => p.id === "eth0");
|
|
expect(eth).toBeTruthy();
|
|
const got = pointerToLayout(eth.sx, eth.sy, 800, 600, eye, 400, out.planets);
|
|
expect(got).not.toBeNull();
|
|
expect(got.planetIndex).toBe(0);
|
|
expect(Math.hypot(got.x - pl.originX, got.y - pl.originY)).toBeLessThan(pl.layoutScale);
|
|
});
|
|
|
|
it("maps closer orbit to a higher LOD zoom", () => {
|
|
expect(planetLodZoom(DEFAULT_ORBIT_DIST)).toBeCloseTo(1, 5);
|
|
expect(planetLodZoom(DEFAULT_ORBIT_DIST * 2)).toBeCloseTo(0.5, 5);
|
|
});
|
|
|
|
it("clips clip-space segments that cross the camera plane", () => {
|
|
const kept = clipLineToPositiveW({ x: 0, y: 0, w: 2 }, { x: 4, y: 0, w: 2 }, 0.1);
|
|
expect(kept.x1).toBe(0);
|
|
expect(kept.x2).toBe(4);
|
|
const cut = clipLineToPositiveW({ x: 0, y: 0, w: 2 }, { x: 10, y: 0, w: -2 }, 0.1);
|
|
expect(cut).not.toBeNull();
|
|
expect(cut.w2).toBeCloseTo(0.1, 5);
|
|
expect(cut.x2).toBeGreaterThan(0);
|
|
expect(clipLineToPositiveW({ x: 0, y: 0, w: -1 }, { x: 1, y: 0, w: -2 }, 0.1)).toBeNull();
|
|
});
|
|
|
|
it("holds layout scale across small cluster size changes", () => {
|
|
expect(stabilizeLayoutScale(280, 260)).toBe(260);
|
|
expect(stabilizeLayoutScale(400, 260)).toBe(400);
|
|
expect(stabilizeLayoutScale(200, null)).toBe(200);
|
|
});
|
|
|
|
it("keeps a peer on its previous interface across a nearby boundary", () => {
|
|
const nodes = new Float32Array(DRAW_STRIDE * 4);
|
|
packNode(nodes, 0, 0, 0, 32);
|
|
packNode(nodes, 1, 200, 0, 24);
|
|
packNode(nodes, 2, -200, 0, 24);
|
|
packNode(nodes, 3, -10, 0, 22);
|
|
const kinds = [PLANET_KIND_ME, PLANET_KIND_IFACE_ON, PLANET_KIND_IFACE_ON, PLANET_KIND_PEER];
|
|
const ids = ["me", "eth0", "wifi", "peer"];
|
|
const fresh = assignPlanetHomes(4, kinds, ids, nodes);
|
|
expect(fresh.home[3]).toBe(1);
|
|
const stuck = assignPlanetHomes(4, kinds, ids, nodes, { peer: "eth0" });
|
|
expect(stuck.home[3]).toBe(0);
|
|
expect(stuck.homeById.peer).toBe("eth0");
|
|
});
|
|
|
|
it("omits cross-planet graph edges and keeps same-planet ones", () => {
|
|
const nodes = new Float32Array(DRAW_STRIDE * 5);
|
|
packNode(nodes, 0, 0, 0, 32);
|
|
packNode(nodes, 1, 200, 0, 24);
|
|
packNode(nodes, 2, -200, 0, 24);
|
|
packNode(nodes, 3, 230, 10, 22);
|
|
packNode(nodes, 4, -230, 10, 22);
|
|
const ids = ["me", "eth0", "wifi", "a", "b"];
|
|
const kinds = [PLANET_KIND_ME, PLANET_KIND_IFACE_ON, PLANET_KIND_IFACE_ON, PLANET_KIND_PEER, PLANET_KIND_PEER];
|
|
const base = {
|
|
nodes,
|
|
width: 800,
|
|
height: 600,
|
|
yaw: 0,
|
|
pitch: 0,
|
|
dist: DEFAULT_ORBIT_DIST,
|
|
dark: true,
|
|
idByIndex: ids,
|
|
kindByIndex: kinds,
|
|
};
|
|
const none = projectPlanetScene({ ...base, edges: new Float32Array(0) });
|
|
const cross = new Float32Array(EDGE_STRIDE);
|
|
cross[0] = 230;
|
|
cross[1] = 10;
|
|
cross[2] = -230;
|
|
cross[3] = 10;
|
|
cross[7] = 1;
|
|
const withCross = projectPlanetScene({ ...base, edges: cross });
|
|
expect(withCross.edges.length).toBe(none.edges.length);
|
|
const same = new Float32Array(EDGE_STRIDE);
|
|
same[0] = 200;
|
|
same[1] = 0;
|
|
same[2] = 230;
|
|
same[3] = 10;
|
|
same[7] = 1;
|
|
const withSame = projectPlanetScene({ ...base, edges: same });
|
|
expect(withSame.edges.length).toBe(none.edges.length + EDGE_STRIDE);
|
|
});
|
|
|
|
it("keeps projected edges finite when the camera is close", () => {
|
|
const nodes = new Float32Array(DRAW_STRIDE * 5);
|
|
packNode(nodes, 0, 0, 0, 32);
|
|
packNode(nodes, 1, 200, 0, 24);
|
|
packNode(nodes, 2, -200, 0, 24);
|
|
packNode(nodes, 3, 0, 200, 24);
|
|
packNode(nodes, 4, 0, -200, 24);
|
|
const out = projectPlanetScene({
|
|
nodes,
|
|
edges: new Float32Array(0),
|
|
width: 800,
|
|
height: 600,
|
|
yaw: 0.4,
|
|
pitch: 0.3,
|
|
dist: 2.6,
|
|
dark: true,
|
|
idByIndex: ["me", "a", "b", "c", "d"],
|
|
kindByIndex: [
|
|
PLANET_KIND_ME,
|
|
PLANET_KIND_IFACE_ON,
|
|
PLANET_KIND_IFACE_ON,
|
|
PLANET_KIND_IFACE_ON,
|
|
PLANET_KIND_IFACE_ON,
|
|
],
|
|
});
|
|
expect(out.orbit.dist).toBeGreaterThan(2.6);
|
|
const limit = 1.6 * Math.hypot(800, 600);
|
|
for (let i = 0; i < out.edges.length; i++) {
|
|
expect(Number.isFinite(out.edges[i])).toBe(true);
|
|
expect(Math.abs(out.edges[i])).toBeLessThan(limit);
|
|
}
|
|
});
|
|
});
|