218 lines
9.5 KiB
JavaScript
218 lines
9.5 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { mapToLocal, MapTopology, COL_STEP } from "../shared/hex.js";
|
|
import {
|
|
DISTORTION,
|
|
EDGE_SEGMENTS,
|
|
CORNERS,
|
|
periodUnits,
|
|
vertexOffset,
|
|
distortPoint,
|
|
distortedCorners,
|
|
distortedBoundary,
|
|
edgePoint,
|
|
bentPath,
|
|
latticeKey,
|
|
vertexKey,
|
|
} from "../client/js/map_view/distortion.js";
|
|
|
|
export class DistortionTest extends TestCase {
|
|
async test_is_deterministic_for_a_seed() {
|
|
const first = distortedCorners(1234, { x: 40, y: 40 }, 0);
|
|
const second = distortedCorners(1234, { x: 40, y: 40 }, 0);
|
|
this.assertEqual(first, second, "the same seed warps the same way");
|
|
const other = distortedCorners(5678, { x: 40, y: 40 }, 0);
|
|
this.assertNotEqual(first, other, "a different seed gives a different shape");
|
|
}
|
|
|
|
async test_offsets_stay_within_the_amplitude() {
|
|
for (let xi = -50; xi <= 50; xi += 7) {
|
|
for (let yi = -50; yi <= 50; yi += 5) {
|
|
const offset = vertexOffset(99, xi, yi, 0);
|
|
this.assertTrue(Math.abs(offset.x) <= DISTORTION.x, `x offset ${offset.x} too large`);
|
|
this.assertTrue(Math.abs(offset.y) <= DISTORTION.y, `y offset ${offset.y} too large`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async test_shared_corners_move_together_so_the_mesh_stays_watertight() {
|
|
// Tile (0,0)'s right corner is the same lattice vertex as tile (1,0)'s
|
|
// top-left corner: (31.5, 0). If the two tiles displaced it differently the
|
|
// map would tear along their shared edge.
|
|
const a = distortedCorners(2024, mapToLocal(0, 0), 0);
|
|
const b = distortedCorners(2024, mapToLocal(1, 0), 0);
|
|
this.assertEqual(a[0], b[2], "the shared corner is displaced identically");
|
|
|
|
// Same for the lower shared vertex (15.75, 28): (0,0)'s bottom-right and
|
|
// (1,0)'s left corner.
|
|
this.assertEqual(a[5], b[3], "the second shared corner is displaced identically");
|
|
}
|
|
|
|
async test_cylindrical_wrap_is_seamless() {
|
|
const topology = new MapTopology({ x: 40, y: 30 }, true);
|
|
const period = periodUnits(topology);
|
|
this.assertEqual(period, topology.width * 3, "one period is 3*width lattice columns");
|
|
const x = 31.5;
|
|
const wrap = COL_STEP * topology.width;
|
|
const near = distortPoint(555, x, -28, period);
|
|
const wrapped = distortPoint(555, x + wrap, -28, period);
|
|
this.assertApprox(wrapped.x - near.x, wrap, 1e-9);
|
|
this.assertApprox(wrapped.y, near.y, 1e-9);
|
|
this.assertEqual(periodUnits(new MapTopology({ x: 4, y: 4 }, false)), 0, "flat maps do not fold");
|
|
}
|
|
|
|
async test_negative_lattice_coordinates_are_folded_consistently() {
|
|
const period = 60;
|
|
const wrap = period * (63 / 4);
|
|
const x = -10;
|
|
const before = distortPoint(12, x, -10, period);
|
|
const after = distortPoint(12, x + wrap, -10, period);
|
|
this.assertApprox(after.x - before.x, wrap, 1e-9);
|
|
this.assertApprox(after.y, before.y, 1e-9);
|
|
}
|
|
|
|
async test_wrapped_edges_are_identical() {
|
|
// An edge and its copy one wrap period over are the same border. Both must
|
|
// bend the same way, or the map tears along the cylindrical seam.
|
|
const topology = new MapTopology({ x: 40, y: 30 }, true);
|
|
const period = periodUnits(topology);
|
|
const wrap = COL_STEP * topology.width;
|
|
const a = { x: 31.5, y: 0 };
|
|
const b = { x: 15.75, y: -28 };
|
|
for (let j = 0; j <= 8; j++) {
|
|
const t = j / 8;
|
|
const point = edgePoint(555, a, b, t, period);
|
|
const copy = edgePoint(555, { x: a.x + wrap, y: a.y }, { x: b.x + wrap, y: b.y }, t, period);
|
|
this.assertApprox(copy.x - point.x, wrap, 1e-9, "seam edge x");
|
|
this.assertApprox(copy.y, point.y, 1e-9, "seam edge y");
|
|
}
|
|
}
|
|
|
|
async test_edges_are_subdivided_and_meet_their_corners() {
|
|
const centre = mapToLocal(3, -2);
|
|
const boundary = distortedBoundary(77, centre, 0);
|
|
this.assertSize(boundary, CORNERS.length * EDGE_SEGMENTS, "every edge is split");
|
|
// Each edge's first point is its starting corner, displaced exactly like
|
|
// the corner function, so adjacent edges still join.
|
|
for (let i = 0; i < CORNERS.length; i++) {
|
|
const point = boundary[i * EDGE_SEGMENTS];
|
|
const corner = distortPoint(77, centre.x + CORNERS[i].x, centre.y + CORNERS[i].y, 0);
|
|
this.assertApprox(point.x, corner.x, 1e-9, "corner moved");
|
|
this.assertApprox(point.y, corner.y, 1e-9, "corner moved");
|
|
this.assertApprox(point.idealX, centre.x + CORNERS[i].x, 1e-9, "ideal corner");
|
|
this.assertApprox(point.idealY, centre.y + CORNERS[i].y, 1e-9, "ideal corner");
|
|
this.assertEqual(point.t, 0);
|
|
}
|
|
}
|
|
|
|
async test_wobbled_edge_is_shared_by_both_neighbours() {
|
|
// Edge (31.5, 0) -> (15.75, -28) of tile (0,0) is the same border as edge
|
|
// (15.75, -28) -> (31.5, 0) of its neighbour (1,-1). Traversed in opposite
|
|
// directions, both must produce the identical set of points.
|
|
const a0 = { x: 31.5, y: 0 };
|
|
const a1 = { x: 15.75, y: -28 };
|
|
const fromA = [];
|
|
const fromB = [];
|
|
for (let j = 0; j <= EDGE_SEGMENTS; j++) {
|
|
fromA.push(edgePoint(4242, a0, a1, j / EDGE_SEGMENTS, 0));
|
|
fromB.push(edgePoint(4242, a1, a0, j / EDGE_SEGMENTS, 0));
|
|
}
|
|
const sort = (list) =>
|
|
list.slice().sort((p, q) => (p.x !== q.x ? p.x - q.x : p.y - q.y));
|
|
const left = sort(fromA);
|
|
const right = sort(fromB);
|
|
this.assertSize(left, right.length);
|
|
for (let i = 0; i < left.length; i++) {
|
|
this.assertApprox(left[i].x, right[i].x, 1e-9, "shared border point");
|
|
this.assertApprox(left[i].y, right[i].y, 1e-9, "shared border point");
|
|
}
|
|
}
|
|
|
|
async test_edge_wobble_vanishes_at_the_corners() {
|
|
const a = { x: 31.5, y: 0 };
|
|
const b = { x: 15.75, y: -28 };
|
|
this.assertEqual(edgePoint(9, a, b, 0, 0), distortPoint(9, a.x, a.y, 0));
|
|
this.assertEqual(edgePoint(9, a, b, 1, 0), distortPoint(9, b.x, b.y, 0));
|
|
}
|
|
|
|
async test_shared_corners_share_a_lattice_key() {
|
|
// Tile (0,0)'s right corner and tile (1,0)'s top-left corner are the same
|
|
// lattice vertex, so border chaining can stitch them with one integer key.
|
|
const a = distortedCorners(1, mapToLocal(0, 0), 0);
|
|
const b = distortedCorners(1, mapToLocal(1, 0), 0);
|
|
this.assertEqual(latticeKey(a[0].x, a[0].y), latticeKey(b[2].x, b[2].y));
|
|
this.assertNotEqual(latticeKey(a[0].x, a[0].y), latticeKey(a[1].x, a[1].y));
|
|
}
|
|
|
|
async test_bent_path_still_starts_and_ends_where_asked() {
|
|
const from = { x: 0, y: 0 };
|
|
const to = { x: 30, y: 12 };
|
|
this.assertEqual(bentPath(5, from, to, 1, 0), from);
|
|
this.assertEqual(bentPath(5, from, to, 1, 1), to);
|
|
const middle = bentPath(5, from, to, 1, 0.5);
|
|
this.assertApprox(middle.x, 15, DISTORTION.road, "the bend stays small");
|
|
}
|
|
|
|
async test_a_straight_edge_is_the_line_between_its_warped_corners() {
|
|
// `wavy` false drops the saw-tooth but keeps the shared corners, so open
|
|
// water reads as a regular hexagon while still tiling with its neighbours.
|
|
const a = { x: 31.5, y: 0 };
|
|
const b = { x: 15.75, y: -28 };
|
|
const ca = distortPoint(9, a.x, a.y, 0);
|
|
const cb = distortPoint(9, b.x, b.y, 0);
|
|
for (let j = 0; j <= 8; j++) {
|
|
const t = j / 8;
|
|
const point = edgePoint(9, a, b, t, 0, false);
|
|
this.assertApprox(point.x, ca.x + (cb.x - ca.x) * t, 1e-9, "straight edge x");
|
|
this.assertApprox(point.y, ca.y + (cb.y - ca.y) * t, 1e-9, "straight edge y");
|
|
}
|
|
}
|
|
|
|
async test_boundary_honours_the_per_edge_waviness() {
|
|
const centre = mapToLocal(2, 1);
|
|
const wavy = [false, true, false, true, false, true];
|
|
const boundary = distortedBoundary(31, centre, 0, EDGE_SEGMENTS, wavy);
|
|
this.assertSize(boundary, CORNERS.length * EDGE_SEGMENTS, "every edge is still split");
|
|
for (let i = 0; i < CORNERS.length; i++) {
|
|
if (wavy[i]) continue;
|
|
const a = { x: centre.x + CORNERS[i].x, y: centre.y + CORNERS[i].y };
|
|
const next = CORNERS[(i + 1) % CORNERS.length];
|
|
const b = { x: centre.x + next.x, y: centre.y + next.y };
|
|
const ca = distortPoint(31, a.x, a.y, 0);
|
|
const cb = distortPoint(31, b.x, b.y, 0);
|
|
for (let j = 0; j < EDGE_SEGMENTS; j++) {
|
|
const point = boundary[i * EDGE_SEGMENTS + j];
|
|
const t = j / EDGE_SEGMENTS;
|
|
this.assertApprox(point.x, ca.x + (cb.x - ca.x) * t, 1e-9, "straight edge x");
|
|
this.assertApprox(point.y, ca.y + (cb.y - ca.y) * t, 1e-9, "straight edge y");
|
|
}
|
|
}
|
|
}
|
|
|
|
async test_an_empty_warp_set_leaves_the_ideal_hexagon() {
|
|
// Open water: no vertex is warped and no edge is wavy, so the boundary is
|
|
// the plain regular hexagon.
|
|
const centre = mapToLocal(3, -1);
|
|
const boundary = distortedBoundary(31, centre, 0, EDGE_SEGMENTS, null, new Set());
|
|
for (let i = 0; i < CORNERS.length; i++) {
|
|
const ideal = { x: centre.x + CORNERS[i].x, y: centre.y + CORNERS[i].y };
|
|
const point = boundary[i * EDGE_SEGMENTS];
|
|
this.assertApprox(point.x, ideal.x, 1e-9, "the corner is unwarped");
|
|
this.assertApprox(point.y, ideal.y, 1e-9, "the corner is unwarped");
|
|
}
|
|
}
|
|
|
|
async test_the_warp_set_selects_which_vertices_move() {
|
|
const a = { x: 31.5, y: 0 };
|
|
const b = { x: 15.75, y: -28 };
|
|
const key = vertexKey(a.x, a.y, 0);
|
|
const onlyA = new Set([key]);
|
|
const start = edgePoint(9, a, b, 0, 0, false, onlyA);
|
|
const end = edgePoint(9, a, b, 1, 0, false, onlyA);
|
|
const movedA = distortPoint(9, a.x, a.y, 0);
|
|
this.assertApprox(start.x, movedA.x, 1e-9, "a listed vertex is warped");
|
|
this.assertApprox(start.y, movedA.y, 1e-9, "a listed vertex is warped");
|
|
this.assertApprox(end.x, b.x, 1e-9, "an unlisted vertex stays put");
|
|
this.assertApprox(end.y, b.y, 1e-9, "an unlisted vertex stays put");
|
|
}
|
|
}
|