1
0
Fork 0
tsgames/test/common/navigation/bresenham.test.ts

166 lines
6.4 KiB
TypeScript

import { describe, it, expect } from "bun:test";
import { bresenham } from "@common/navigation/bresenham";
// Helpers
const pts = (coords: [number, number][]) =>
coords.map(([x, y]) => ({ x, y }));
function is4Connected(points: { x: number; y: number }[]): boolean {
for (let i = 1; i < points.length; i++) {
const dx = Math.abs(points[i].x - points[i - 1].x);
const dy = Math.abs(points[i].y - points[i - 1].y);
if (dx + dy !== 1) return false;
}
return true;
}
function is8Connected(points: { x: number; y: number }[]): boolean {
for (let i = 1; i < points.length; i++) {
const dx = Math.abs(points[i].x - points[i - 1].x);
const dy = Math.abs(points[i].y - points[i - 1].y);
if (dx > 1 || dy > 1) return false;
}
return true;
}
describe("bresenham", () => {
describe("single point", () => {
it("returns one point when start === end", () => {
expect(bresenham(3, 3, 3, 3)).toEqual(pts([[3, 3]]));
});
});
describe("axis-aligned lines", () => {
it("horizontal right", () => {
expect(bresenham(0, 0, 3, 0)).toEqual(pts([[0, 0], [1, 0], [2, 0], [3, 0]]));
});
it("horizontal left", () => {
expect(bresenham(3, 0, 0, 0)).toEqual(pts([[3, 0], [2, 0], [1, 0], [0, 0]]));
});
it("vertical down", () => {
expect(bresenham(0, 0, 0, 3)).toEqual(pts([[0, 0], [0, 1], [0, 2], [0, 3]]));
});
it("vertical up", () => {
expect(bresenham(0, 3, 0, 0)).toEqual(pts([[0, 3], [0, 2], [0, 1], [0, 0]]));
});
});
describe("diagonal lines", () => {
it("perfect diagonal — directions=4 splits into axis steps", () => {
const result = bresenham(0, 0, 2, 2);
expect(result[0]).toEqual({ x: 0, y: 0 });
expect(result[result.length - 1]).toEqual({ x: 2, y: 2 });
expect(is4Connected(result)).toBe(true);
// dx+dy+1 = 2+2+1 = 5 points
expect(result.length).toBe(5);
});
it("perfect diagonal — directions=8 emits single diagonal steps", () => {
const result = bresenham(0, 0, 2, 2, { directions: 8 });
expect(result).toEqual(pts([[0, 0], [1, 1], [2, 2]]));
});
it("diagonal all quadrants produce correct endpoints", () => {
const cases: [[number, number, number, number]] = [
[0, 0, 3, 3],
[0, 0, -3, 3],
[0, 0, 3, -3],
[0, 0, -3, -3],
] as any;
for (const [fx, fy, tx, ty] of cases) {
const r = bresenham(fx, fy, tx, ty, { directions: 8 });
expect(r[0]).toEqual({ x: fx, y: fy });
expect(r[r.length - 1]).toEqual({ x: tx, y: ty });
expect(is8Connected(r)).toBe(true);
}
});
});
describe("connectivity guarantees", () => {
it("directions=4 (default) is always 4-connected", () => {
// steep slope
expect(is4Connected(bresenham(0, 0, 3, 7))).toBe(true);
// shallow slope
expect(is4Connected(bresenham(0, 0, 7, 3))).toBe(true);
// negative direction
expect(is4Connected(bresenham(5, 5, -2, 1))).toBe(true);
});
it("directions=8 is always 8-connected", () => {
expect(is8Connected(bresenham(0, 0, 3, 7, { directions: 8 }))).toBe(true);
expect(is8Connected(bresenham(0, 0, 7, 3, { directions: 8 }))).toBe(true);
expect(is8Connected(bresenham(5, 5, -2, 1, { directions: 8 }))).toBe(true);
});
it("directions=4 output length is dx+dy+1", () => {
const r = bresenham(0, 0, 4, 3);
expect(r.length).toBe(4 + 3 + 1);
});
it("directions=8 output length is max(dx,dy)+1", () => {
const r = bresenham(0, 0, 4, 3, { directions: 8 });
expect(r.length).toBe(Math.max(4, 3) + 1);
});
it("start and end are always first and last points", () => {
const r = bresenham(1, 2, 5, 8);
expect(r[0]).toEqual({ x: 1, y: 2 });
expect(r[r.length - 1]).toEqual({ x: 5, y: 8 });
});
});
describe("clipping", () => {
it("returns empty array when segment is entirely outside bounds", () => {
expect(bresenham(10, 10, 20, 20, { minX: 0, maxX: 5, minY: 0, maxY: 5 })).toEqual([]);
});
it("clips start when it lies outside bounds", () => {
const r = bresenham(-5, 0, 5, 0, { minX: 0, maxX: 10, minY: 0, maxY: 10 });
expect(r[0].x).toBeGreaterThanOrEqual(0);
expect(r[r.length - 1]).toEqual({ x: 5, y: 0 });
});
it("clips end when it lies outside bounds", () => {
const r = bresenham(0, 0, 15, 0, { minX: 0, maxX: 10, minY: 0, maxY: 10 });
expect(r[0]).toEqual({ x: 0, y: 0 });
expect(r[r.length - 1].x).toBeLessThanOrEqual(10);
});
it("all returned points are within bounds", () => {
const bounds = { minX: 1, maxX: 8, minY: 1, maxY: 8 };
const r = bresenham(0, 0, 10, 10, bounds);
for (const p of r) {
expect(p.x).toBeGreaterThanOrEqual(bounds.minX);
expect(p.x).toBeLessThanOrEqual(bounds.maxX);
expect(p.y).toBeGreaterThanOrEqual(bounds.minY);
expect(p.y).toBeLessThanOrEqual(bounds.maxY);
}
});
it("segment touching only a corner of bounds returns at least one point", () => {
// line passes through (0,0) exactly, bounds include only (0,0)
const r = bresenham(-2, -2, 2, 2, { minX: 0, maxX: 0, minY: 0, maxY: 0 });
expect(r.length).toBeGreaterThan(0);
});
it("clipping preserves 4-connectivity within bounds", () => {
const r = bresenham(-3, -3, 10, 10, { minX: 0, maxX: 6, minY: 0, maxY: 6 });
expect(is4Connected(r)).toBe(true);
});
it("clipping preserves 8-connectivity within bounds", () => {
const r = bresenham(-3, -3, 10, 10, { minX: 0, maxX: 6, minY: 0, maxY: 6, directions: 8 });
expect(is8Connected(r)).toBe(true);
});
});
describe("non-integer inputs", () => {
it("rounds float inputs to nearest integer", () => {
expect(bresenham(0.4, 0.4, 2.6, 0.4)).toEqual(pts([[0, 0], [1, 0], [2, 0], [3, 0]]));
});
});
});