166 lines
6.5 KiB
TypeScript
166 lines
6.5 KiB
TypeScript
import { describe, it, expect } from "bun:test";
|
|
import { bresenhamLine } 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(bresenhamLine(3, 3, 3, 3)).toEqual(pts([[3, 3]]));
|
|
});
|
|
});
|
|
|
|
describe("axis-aligned lines", () => {
|
|
it("horizontal right", () => {
|
|
expect(bresenhamLine(0, 0, 3, 0)).toEqual(pts([[0, 0], [1, 0], [2, 0], [3, 0]]));
|
|
});
|
|
|
|
it("horizontal left", () => {
|
|
expect(bresenhamLine(3, 0, 0, 0)).toEqual(pts([[3, 0], [2, 0], [1, 0], [0, 0]]));
|
|
});
|
|
|
|
it("vertical down", () => {
|
|
expect(bresenhamLine(0, 0, 0, 3)).toEqual(pts([[0, 0], [0, 1], [0, 2], [0, 3]]));
|
|
});
|
|
|
|
it("vertical up", () => {
|
|
expect(bresenhamLine(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 = bresenhamLine(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 = bresenhamLine(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 = bresenhamLine(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(bresenhamLine(0, 0, 3, 7))).toBe(true);
|
|
// shallow slope
|
|
expect(is4Connected(bresenhamLine(0, 0, 7, 3))).toBe(true);
|
|
// negative direction
|
|
expect(is4Connected(bresenhamLine(5, 5, -2, 1))).toBe(true);
|
|
});
|
|
|
|
it("directions=8 is always 8-connected", () => {
|
|
expect(is8Connected(bresenhamLine(0, 0, 3, 7, { directions: 8 }))).toBe(true);
|
|
expect(is8Connected(bresenhamLine(0, 0, 7, 3, { directions: 8 }))).toBe(true);
|
|
expect(is8Connected(bresenhamLine(5, 5, -2, 1, { directions: 8 }))).toBe(true);
|
|
});
|
|
|
|
it("directions=4 output length is dx+dy+1", () => {
|
|
const r = bresenhamLine(0, 0, 4, 3);
|
|
expect(r.length).toBe(4 + 3 + 1);
|
|
});
|
|
|
|
it("directions=8 output length is max(dx,dy)+1", () => {
|
|
const r = bresenhamLine(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 = bresenhamLine(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(bresenhamLine(10, 10, 20, 20, { minX: 0, maxX: 5, minY: 0, maxY: 5 })).toEqual([]);
|
|
});
|
|
|
|
it("clips start when it lies outside bounds", () => {
|
|
const r = bresenhamLine(-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 = bresenhamLine(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 = bresenhamLine(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 = bresenhamLine(-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 = bresenhamLine(-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 = bresenhamLine(-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(bresenhamLine(0.4, 0.4, 2.6, 0.4)).toEqual(pts([[0, 0], [1, 0], [2, 0], [3, 0]]));
|
|
});
|
|
});
|
|
});
|