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]])); }); }); });