Circle drawing
This commit is contained in:
parent
f955dc6d05
commit
85a264eed3
|
|
@ -5,7 +5,7 @@ export interface Point {
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BresenhamOptions {
|
interface BresenhamOptions {
|
||||||
minX?: number;
|
minX?: number;
|
||||||
maxX?: number;
|
maxX?: number;
|
||||||
minY?: number;
|
minY?: number;
|
||||||
|
|
@ -19,6 +19,18 @@ export interface BresenhamOptions {
|
||||||
directions?: 4 | 8;
|
directions?: 4 | 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BresenhamLineOptions extends BresenhamOptions {}
|
||||||
|
|
||||||
|
export interface BresenhamCircleOptions<T = boolean | 'fov'> extends BresenhamOptions {
|
||||||
|
/**
|
||||||
|
* - `false` (default) — outline only.
|
||||||
|
* - `true` — filled disc (span-from-outline scanline).
|
||||||
|
* - `'fov'` — ray-casting field of view; the generator accepts `true` via
|
||||||
|
* `.next(true)` to signal an obstacle and skip the rest of that ray.
|
||||||
|
*/
|
||||||
|
fill?: T;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Cohen-Sutherland outcodes
|
// Cohen-Sutherland outcodes
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -111,13 +123,13 @@ function cohenSutherland(
|
||||||
* @returns Ordered array of integer grid points. Empty when the segment lies entirely
|
* @returns Ordered array of integer grid points. Empty when the segment lies entirely
|
||||||
* outside the supplied bounds.
|
* outside the supplied bounds.
|
||||||
*/
|
*/
|
||||||
export function bresenham(
|
export function* bresenhamLineGen(
|
||||||
fromX: number,
|
fromX: number,
|
||||||
fromY: number,
|
fromY: number,
|
||||||
toX: number,
|
toX: number,
|
||||||
toY: number,
|
toY: number,
|
||||||
options?: BresenhamOptions,
|
options?: BresenhamLineOptions,
|
||||||
): Point[] {
|
): Generator<Point, void, void> {
|
||||||
// Round inputs to integers — the algorithm is defined on integer grids.
|
// Round inputs to integers — the algorithm is defined on integer grids.
|
||||||
let x0 = Math.round(fromX), y0 = Math.round(fromY);
|
let x0 = Math.round(fromX), y0 = Math.round(fromY);
|
||||||
let x1 = Math.round(toX), y1 = Math.round(toY);
|
let x1 = Math.round(toX), y1 = Math.round(toY);
|
||||||
|
|
@ -130,7 +142,7 @@ export function bresenham(
|
||||||
const maxY = Math.ceil(options.maxY ?? Infinity);
|
const maxY = Math.ceil(options.maxY ?? Infinity);
|
||||||
|
|
||||||
const clipped = cohenSutherland(x0, y0, x1, y1, minX, maxX, minY, maxY);
|
const clipped = cohenSutherland(x0, y0, x1, y1, minX, maxX, minY, maxY);
|
||||||
if (!clipped) return [];
|
if (!clipped) return;
|
||||||
|
|
||||||
// Re-snap clipped floats to the nearest integer cell that is inside bounds.
|
// Re-snap clipped floats to the nearest integer cell that is inside bounds.
|
||||||
x0 = Math.round(clipped[0]); y0 = Math.round(clipped[1]);
|
x0 = Math.round(clipped[0]); y0 = Math.round(clipped[1]);
|
||||||
|
|
@ -144,7 +156,6 @@ export function bresenham(
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bresenham walk ---
|
// --- Bresenham walk ---
|
||||||
const points: Point[] = [];
|
|
||||||
const use8 = options?.directions === 8;
|
const use8 = options?.directions === 8;
|
||||||
|
|
||||||
const dx = Math.abs(x1 - x0);
|
const dx = Math.abs(x1 - x0);
|
||||||
|
|
@ -156,18 +167,18 @@ export function bresenham(
|
||||||
|
|
||||||
if (dx === 0 && dy === 0) {
|
if (dx === 0 && dy === 0) {
|
||||||
// Single point.
|
// Single point.
|
||||||
points.push({ x, y });
|
yield { x, y };
|
||||||
return points;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dx >= dy) {
|
if (dx >= dy) {
|
||||||
// X is the driving axis.
|
// X is the driving axis.
|
||||||
let err = 2 * dy - dx;
|
let err = 2 * dy - dx;
|
||||||
for (let i = 0; i <= dx; i++) {
|
for (let i = 0; i <= dx; i++) {
|
||||||
points.push({ x, y });
|
yield { x, y };
|
||||||
if (i === dx) break;
|
if (i === dx) break;
|
||||||
if (err >= 0) {
|
if (err >= 0) {
|
||||||
if (!use8) points.push({ x, y: y + sy }); // 4-connected: emit y step before x
|
if (!use8) yield { x, y: y + sy }; // 4-connected: emit y step before x
|
||||||
y += sy;
|
y += sy;
|
||||||
err -= 2 * dx;
|
err -= 2 * dx;
|
||||||
}
|
}
|
||||||
|
|
@ -178,10 +189,10 @@ export function bresenham(
|
||||||
// Y is the driving axis.
|
// Y is the driving axis.
|
||||||
let err = 2 * dx - dy;
|
let err = 2 * dx - dy;
|
||||||
for (let i = 0; i <= dy; i++) {
|
for (let i = 0; i <= dy; i++) {
|
||||||
points.push({ x, y });
|
yield { x, y };
|
||||||
if (i === dy) break;
|
if (i === dy) break;
|
||||||
if (err >= 0) {
|
if (err >= 0) {
|
||||||
if (!use8) points.push({ x: x + sx, y }); // 4-connected: emit x step before y
|
if (!use8) yield { x: x + sx, y }; // 4-connected: emit x step before y
|
||||||
x += sx;
|
x += sx;
|
||||||
err -= 2 * dy;
|
err -= 2 * dy;
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +200,208 @@ export function bresenham(
|
||||||
y += sy;
|
y += sy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return points;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array-returning wrapper for {@link bresenhamLineGen}.
|
||||||
|
* @see bresenhamLineGen for full parameter and algorithm documentation.
|
||||||
|
*/
|
||||||
|
export const bresenhamLine = (
|
||||||
|
fromX: number,
|
||||||
|
fromY: number,
|
||||||
|
toX: number,
|
||||||
|
toY: number,
|
||||||
|
options?: BresenhamLineOptions,
|
||||||
|
): Point[] => Array.from(bresenhamLineGen(fromX, fromY, toX, toY, options));
|
||||||
|
|
||||||
|
/////////
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yields integer grid points forming a circle outline or filled disc centred
|
||||||
|
* on `(x, y)` with radius `r`.
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* ## Outline mode (`fill: false`, default) — midpoint circle algorithm
|
||||||
|
*
|
||||||
|
* Tracks one point `(ox, oy)` in the first octant (`0 ≤ ox ≤ oy`) and uses
|
||||||
|
* 8-fold symmetry to emit up to 8 points per step. A decision variable `d`
|
||||||
|
* (initialised to `1 − r`) tracks which side of the true circle boundary the
|
||||||
|
* next midpoint falls on:
|
||||||
|
*
|
||||||
|
* - `d < 0` → midpoint is inside → keep `oy`, update `d += 2·ox + 3`
|
||||||
|
* - `d ≥ 0` → midpoint is outside → `oy--`, update `d += 2·(ox − oy) + 5`
|
||||||
|
*
|
||||||
|
* `ox` increments every step; the loop runs while `ox ≤ oy`.
|
||||||
|
*
|
||||||
|
* Duplicate-point guards:
|
||||||
|
* - The `−ox` reflections are skipped when `ox === 0` (axis crossings).
|
||||||
|
* - The swapped octant block is skipped when `ox === oy` (45° diagonal).
|
||||||
|
*
|
||||||
|
* ## Fill mode (`fill: true`) — span-from-outline scanline
|
||||||
|
*
|
||||||
|
* Runs the same midpoint algorithm to collect the horizontal half-width
|
||||||
|
* (`spanRight`) for every row offset `|dy|`, then fills each row from
|
||||||
|
* `cx − spanRight[|dy|]` to `cx + spanRight[|dy|]` inclusive. The fill
|
||||||
|
* boundary is therefore **pixel-for-pixel identical** to the outline.
|
||||||
|
*
|
||||||
|
* ## FOV mode (`fill: 'fov'`) — ray-casting field of view
|
||||||
|
*
|
||||||
|
* For each outline point, casts an 8-connected Bresenham ray from the centre
|
||||||
|
* to that point and yields every cell along the ray. The generator uses the
|
||||||
|
* **send protocol**: the caller can pass `true` to `.next(true)` after
|
||||||
|
* receiving a cell to signal an obstacle — the rest of that ray is then
|
||||||
|
* skipped and the next outline point's ray begins. The obstacle cell itself
|
||||||
|
* is always yielded (it is visible; only the cells behind it are blocked).
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const fov = bresenhamCircleGen(cx, cy, r, { fill: 'fov' });
|
||||||
|
* let result = fov.next();
|
||||||
|
* while (!result.done) {
|
||||||
|
* const blocked = isWall(result.value);
|
||||||
|
* result = fov.next(blocked); // pass true to stop this ray
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* **Bounds:** The outline is always generated without clipping so that rays
|
||||||
|
* extend to the full circle perimeter. The individual rays are clipped to
|
||||||
|
* the supplied bounds, so only in-bounds cells are yielded.
|
||||||
|
*
|
||||||
|
* **Coverage:** One ray is cast per outline point (`4r` rays total). At large
|
||||||
|
* radii, the angular gap between adjacent rays may leave interior cells
|
||||||
|
* uncovered. For precise FOV at large radii, prefer a shadowcasting algorithm.
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* ## Clipping
|
||||||
|
*
|
||||||
|
* Points outside `[minX, maxX] × [minY, maxY]` are silently skipped.
|
||||||
|
* Bounds are floored/ceiled to integer cells.
|
||||||
|
*
|
||||||
|
* @param x - Centre x (rounded to nearest integer).
|
||||||
|
* @param y - Centre y (rounded to nearest integer).
|
||||||
|
* @param r - Radius in grid cells (clamped to `≥ 0`, rounded). Radius 0
|
||||||
|
* yields only the centre point.
|
||||||
|
* @param options - Optional `fill` flag and clip bounds.
|
||||||
|
*/
|
||||||
|
export function bresenhamCircleGen(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
r: number,
|
||||||
|
options?: BresenhamCircleOptions<boolean>,
|
||||||
|
): Generator<Point, void, void>;
|
||||||
|
export function bresenhamCircleGen(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
r: number,
|
||||||
|
options: BresenhamCircleOptions<'fov'>,
|
||||||
|
): Generator<Point, void, boolean | void>;
|
||||||
|
|
||||||
|
export function* bresenhamCircleGen(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
r: number,
|
||||||
|
options?: BresenhamCircleOptions,
|
||||||
|
): Generator<Point, void, boolean | void> {
|
||||||
|
if (options?.fill === 'fov') {
|
||||||
|
const lineBounds: BresenhamLineOptions = {
|
||||||
|
directions: options.directions,
|
||||||
|
minX: options.minX,
|
||||||
|
maxX: options.maxX,
|
||||||
|
minY: options.minY,
|
||||||
|
maxY: options.maxY,
|
||||||
|
};
|
||||||
|
for (const { x: ox, y: oy } of bresenhamCircleGen(x, y, r, { fill: false })) {
|
||||||
|
for (const linePoint of bresenhamLineGen(x, y, ox, oy, lineBounds)) {
|
||||||
|
const skipLine = yield linePoint;
|
||||||
|
if (skipLine) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cx = Math.round(x);
|
||||||
|
const cy = Math.round(y);
|
||||||
|
const radius = Math.max(0, Math.round(r));
|
||||||
|
|
||||||
|
const minX = options?.minX !== undefined ? Math.floor(options.minX) : -Infinity;
|
||||||
|
const maxX = options?.maxX !== undefined ? Math.ceil(options.maxX) : Infinity;
|
||||||
|
const minY = options?.minY !== undefined ? Math.floor(options.minY) : -Infinity;
|
||||||
|
const maxY = options?.maxY !== undefined ? Math.ceil(options.maxY) : Infinity;
|
||||||
|
const fill = options?.fill === true;
|
||||||
|
|
||||||
|
const inBounds = (px: number, py: number) =>
|
||||||
|
px >= minX && px <= maxX && py >= minY && py <= maxY;
|
||||||
|
|
||||||
|
if (radius === 0) {
|
||||||
|
if (inBounds(cx, cy)) yield { x: cx, y: cy };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fill) {
|
||||||
|
// Span-from-outline: use the midpoint algorithm to find the exact
|
||||||
|
// horizontal extent for each row, then fill between those extents.
|
||||||
|
// Guarantees the fill boundary matches the outline pixel-for-pixel.
|
||||||
|
const spanRight = new Int32Array(radius + 1); // index = |dy|, value = max ox
|
||||||
|
let ox = 0, oy = radius, d = 1 - radius;
|
||||||
|
while (ox <= oy) {
|
||||||
|
// oy is the half-width for rows ±ox; ox is the half-width for rows ±oy.
|
||||||
|
if (spanRight[oy] < ox) spanRight[oy] = ox;
|
||||||
|
if (spanRight[ox] < oy) spanRight[ox] = oy;
|
||||||
|
if (d < 0) { d += 2 * ox + 3; }
|
||||||
|
else { d += 2 * (ox - oy) + 5; oy--; }
|
||||||
|
ox++;
|
||||||
|
}
|
||||||
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
|
const dxMax = spanRight[Math.abs(dy)];
|
||||||
|
for (let dx = -dxMax; dx <= dxMax; dx++) {
|
||||||
|
const px = cx + dx, py = cy + dy;
|
||||||
|
if (inBounds(px, py)) yield { x: px, y: py };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Midpoint circle algorithm (Bresenham) for outline.
|
||||||
|
// Guards prevent duplicate points at axis crossings (ox === 0) and on the
|
||||||
|
// 45° diagonal (ox === oy) where naïve 8-fold expansion collapses octants.
|
||||||
|
let ox = 0;
|
||||||
|
let oy = radius;
|
||||||
|
let d = 1 - radius;
|
||||||
|
|
||||||
|
while (ox <= oy) {
|
||||||
|
// Primary octant and its y-reflection (top / bottom).
|
||||||
|
if (inBounds(cx + ox, cy + oy)) yield { x: cx + ox, y: cy + oy };
|
||||||
|
if (ox > 0 && inBounds(cx - ox, cy + oy)) yield { x: cx - ox, y: cy + oy };
|
||||||
|
if (inBounds(cx + ox, cy - oy)) yield { x: cx + ox, y: cy - oy };
|
||||||
|
if (ox > 0 && inBounds(cx - ox, cy - oy)) yield { x: cx - ox, y: cy - oy };
|
||||||
|
|
||||||
|
// Swapped octant (left / right), skipped on the diagonal where it
|
||||||
|
// would duplicate the primary octant points.
|
||||||
|
if (ox < oy) {
|
||||||
|
if (inBounds(cx + oy, cy + ox)) yield { x: cx + oy, y: cy + ox };
|
||||||
|
if (inBounds(cx - oy, cy + ox)) yield { x: cx - oy, y: cy + ox };
|
||||||
|
if (ox > 0 && inBounds(cx + oy, cy - ox)) yield { x: cx + oy, y: cy - ox };
|
||||||
|
if (ox > 0 && inBounds(cx - oy, cy - ox)) yield { x: cx - oy, y: cy - ox };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d < 0) {
|
||||||
|
d += 2 * ox + 3;
|
||||||
|
} else {
|
||||||
|
d += 2 * (ox - oy) + 5;
|
||||||
|
oy--;
|
||||||
|
}
|
||||||
|
ox++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array-returning wrapper for {@link bresenhamCircleGen}.
|
||||||
|
* @see bresenhamCircleGen for full parameter and algorithm documentation.
|
||||||
|
*/
|
||||||
|
export const bresenhamCircle = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
r: number,
|
||||||
|
options?: BresenhamCircleOptions<boolean>,
|
||||||
|
): Point[] => Array.from(bresenhamCircleGen(x, y, r, options));
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
import { TextDisplay } from "@common/display/text";
|
import { createCanvas } from "@common/display/canvas";
|
||||||
import { Inventory } from "@common/rpg/components/inventory";
|
import { bresenhamCircleGen } from "@common/navigation/bresenham";
|
||||||
import { Position } from "@common/rpg/components/position";
|
|
||||||
import { World } from "@common/rpg/core/world";
|
const S = 20;
|
||||||
import { TextDisplaySystem } from "@common/rpg/systems/render/text";
|
|
||||||
|
|
||||||
console.log("Hello, world 1");
|
|
||||||
export default async function main() {
|
export default async function main() {
|
||||||
const world = new World();
|
const canvas = createCanvas(S, S);
|
||||||
const e = world.createEntity();
|
|
||||||
e.add(new Position(1, 1));
|
const ctx = canvas.getContext("2d");
|
||||||
e.add(new Inventory());
|
if (!ctx) return;
|
||||||
console.log("Hello, world!");
|
|
||||||
const display = new TextDisplay();
|
ctx.fillStyle = 'black';
|
||||||
new TextDisplaySystem(display);
|
for (const fill of [true, false, 'fov'] as const) {
|
||||||
console.log("Hello, world 2");
|
let i = 0;
|
||||||
|
|
||||||
|
console.time(`fill=${fill}`);
|
||||||
|
// @ts-ignore
|
||||||
|
for (const { x, y } of bresenhamCircleGen(S / 2, S / 2, S * 0.4, { fill })) {
|
||||||
|
i++;
|
||||||
|
ctx.fillRect(x, y, 1, 1);
|
||||||
|
}
|
||||||
|
console.timeEnd(`fill=${fill}`);
|
||||||
|
console.log(`i=${i}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect } from "bun:test";
|
||||||
import { bresenham } from "@common/navigation/bresenham";
|
import { bresenhamLine } from "@common/navigation/bresenham";
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
const pts = (coords: [number, number][]) =>
|
const pts = (coords: [number, number][]) =>
|
||||||
|
|
@ -26,31 +26,31 @@ function is8Connected(points: { x: number; y: number }[]): boolean {
|
||||||
describe("bresenham", () => {
|
describe("bresenham", () => {
|
||||||
describe("single point", () => {
|
describe("single point", () => {
|
||||||
it("returns one point when start === end", () => {
|
it("returns one point when start === end", () => {
|
||||||
expect(bresenham(3, 3, 3, 3)).toEqual(pts([[3, 3]]));
|
expect(bresenhamLine(3, 3, 3, 3)).toEqual(pts([[3, 3]]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("axis-aligned lines", () => {
|
describe("axis-aligned lines", () => {
|
||||||
it("horizontal right", () => {
|
it("horizontal right", () => {
|
||||||
expect(bresenham(0, 0, 3, 0)).toEqual(pts([[0, 0], [1, 0], [2, 0], [3, 0]]));
|
expect(bresenhamLine(0, 0, 3, 0)).toEqual(pts([[0, 0], [1, 0], [2, 0], [3, 0]]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("horizontal left", () => {
|
it("horizontal left", () => {
|
||||||
expect(bresenham(3, 0, 0, 0)).toEqual(pts([[3, 0], [2, 0], [1, 0], [0, 0]]));
|
expect(bresenhamLine(3, 0, 0, 0)).toEqual(pts([[3, 0], [2, 0], [1, 0], [0, 0]]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("vertical down", () => {
|
it("vertical down", () => {
|
||||||
expect(bresenham(0, 0, 0, 3)).toEqual(pts([[0, 0], [0, 1], [0, 2], [0, 3]]));
|
expect(bresenhamLine(0, 0, 0, 3)).toEqual(pts([[0, 0], [0, 1], [0, 2], [0, 3]]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("vertical up", () => {
|
it("vertical up", () => {
|
||||||
expect(bresenham(0, 3, 0, 0)).toEqual(pts([[0, 3], [0, 2], [0, 1], [0, 0]]));
|
expect(bresenhamLine(0, 3, 0, 0)).toEqual(pts([[0, 3], [0, 2], [0, 1], [0, 0]]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("diagonal lines", () => {
|
describe("diagonal lines", () => {
|
||||||
it("perfect diagonal — directions=4 splits into axis steps", () => {
|
it("perfect diagonal — directions=4 splits into axis steps", () => {
|
||||||
const result = bresenham(0, 0, 2, 2);
|
const result = bresenhamLine(0, 0, 2, 2);
|
||||||
expect(result[0]).toEqual({ x: 0, y: 0 });
|
expect(result[0]).toEqual({ x: 0, y: 0 });
|
||||||
expect(result[result.length - 1]).toEqual({ x: 2, y: 2 });
|
expect(result[result.length - 1]).toEqual({ x: 2, y: 2 });
|
||||||
expect(is4Connected(result)).toBe(true);
|
expect(is4Connected(result)).toBe(true);
|
||||||
|
|
@ -59,7 +59,7 @@ describe("bresenham", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("perfect diagonal — directions=8 emits single diagonal steps", () => {
|
it("perfect diagonal — directions=8 emits single diagonal steps", () => {
|
||||||
const result = bresenham(0, 0, 2, 2, { directions: 8 });
|
const result = bresenhamLine(0, 0, 2, 2, { directions: 8 });
|
||||||
expect(result).toEqual(pts([[0, 0], [1, 1], [2, 2]]));
|
expect(result).toEqual(pts([[0, 0], [1, 1], [2, 2]]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ describe("bresenham", () => {
|
||||||
[0, 0, -3, -3],
|
[0, 0, -3, -3],
|
||||||
] as any;
|
] as any;
|
||||||
for (const [fx, fy, tx, ty] of cases) {
|
for (const [fx, fy, tx, ty] of cases) {
|
||||||
const r = bresenham(fx, fy, tx, ty, { directions: 8 });
|
const r = bresenhamLine(fx, fy, tx, ty, { directions: 8 });
|
||||||
expect(r[0]).toEqual({ x: fx, y: fy });
|
expect(r[0]).toEqual({ x: fx, y: fy });
|
||||||
expect(r[r.length - 1]).toEqual({ x: tx, y: ty });
|
expect(r[r.length - 1]).toEqual({ x: tx, y: ty });
|
||||||
expect(is8Connected(r)).toBe(true);
|
expect(is8Connected(r)).toBe(true);
|
||||||
|
|
@ -82,31 +82,31 @@ describe("bresenham", () => {
|
||||||
describe("connectivity guarantees", () => {
|
describe("connectivity guarantees", () => {
|
||||||
it("directions=4 (default) is always 4-connected", () => {
|
it("directions=4 (default) is always 4-connected", () => {
|
||||||
// steep slope
|
// steep slope
|
||||||
expect(is4Connected(bresenham(0, 0, 3, 7))).toBe(true);
|
expect(is4Connected(bresenhamLine(0, 0, 3, 7))).toBe(true);
|
||||||
// shallow slope
|
// shallow slope
|
||||||
expect(is4Connected(bresenham(0, 0, 7, 3))).toBe(true);
|
expect(is4Connected(bresenhamLine(0, 0, 7, 3))).toBe(true);
|
||||||
// negative direction
|
// negative direction
|
||||||
expect(is4Connected(bresenham(5, 5, -2, 1))).toBe(true);
|
expect(is4Connected(bresenhamLine(5, 5, -2, 1))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("directions=8 is always 8-connected", () => {
|
it("directions=8 is always 8-connected", () => {
|
||||||
expect(is8Connected(bresenham(0, 0, 3, 7, { directions: 8 }))).toBe(true);
|
expect(is8Connected(bresenhamLine(0, 0, 3, 7, { directions: 8 }))).toBe(true);
|
||||||
expect(is8Connected(bresenham(0, 0, 7, 3, { directions: 8 }))).toBe(true);
|
expect(is8Connected(bresenhamLine(0, 0, 7, 3, { directions: 8 }))).toBe(true);
|
||||||
expect(is8Connected(bresenham(5, 5, -2, 1, { directions: 8 }))).toBe(true);
|
expect(is8Connected(bresenhamLine(5, 5, -2, 1, { directions: 8 }))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("directions=4 output length is dx+dy+1", () => {
|
it("directions=4 output length is dx+dy+1", () => {
|
||||||
const r = bresenham(0, 0, 4, 3);
|
const r = bresenhamLine(0, 0, 4, 3);
|
||||||
expect(r.length).toBe(4 + 3 + 1);
|
expect(r.length).toBe(4 + 3 + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("directions=8 output length is max(dx,dy)+1", () => {
|
it("directions=8 output length is max(dx,dy)+1", () => {
|
||||||
const r = bresenham(0, 0, 4, 3, { directions: 8 });
|
const r = bresenhamLine(0, 0, 4, 3, { directions: 8 });
|
||||||
expect(r.length).toBe(Math.max(4, 3) + 1);
|
expect(r.length).toBe(Math.max(4, 3) + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("start and end are always first and last points", () => {
|
it("start and end are always first and last points", () => {
|
||||||
const r = bresenham(1, 2, 5, 8);
|
const r = bresenhamLine(1, 2, 5, 8);
|
||||||
expect(r[0]).toEqual({ x: 1, y: 2 });
|
expect(r[0]).toEqual({ x: 1, y: 2 });
|
||||||
expect(r[r.length - 1]).toEqual({ x: 5, y: 8 });
|
expect(r[r.length - 1]).toEqual({ x: 5, y: 8 });
|
||||||
});
|
});
|
||||||
|
|
@ -114,24 +114,24 @@ describe("bresenham", () => {
|
||||||
|
|
||||||
describe("clipping", () => {
|
describe("clipping", () => {
|
||||||
it("returns empty array when segment is entirely outside bounds", () => {
|
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([]);
|
expect(bresenhamLine(10, 10, 20, 20, { minX: 0, maxX: 5, minY: 0, maxY: 5 })).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clips start when it lies outside bounds", () => {
|
it("clips start when it lies outside bounds", () => {
|
||||||
const r = bresenham(-5, 0, 5, 0, { minX: 0, maxX: 10, minY: 0, maxY: 10 });
|
const r = bresenhamLine(-5, 0, 5, 0, { minX: 0, maxX: 10, minY: 0, maxY: 10 });
|
||||||
expect(r[0].x).toBeGreaterThanOrEqual(0);
|
expect(r[0].x).toBeGreaterThanOrEqual(0);
|
||||||
expect(r[r.length - 1]).toEqual({ x: 5, y: 0 });
|
expect(r[r.length - 1]).toEqual({ x: 5, y: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clips end when it lies outside bounds", () => {
|
it("clips end when it lies outside bounds", () => {
|
||||||
const r = bresenham(0, 0, 15, 0, { minX: 0, maxX: 10, minY: 0, maxY: 10 });
|
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[0]).toEqual({ x: 0, y: 0 });
|
||||||
expect(r[r.length - 1].x).toBeLessThanOrEqual(10);
|
expect(r[r.length - 1].x).toBeLessThanOrEqual(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("all returned points are within bounds", () => {
|
it("all returned points are within bounds", () => {
|
||||||
const bounds = { minX: 1, maxX: 8, minY: 1, maxY: 8 };
|
const bounds = { minX: 1, maxX: 8, minY: 1, maxY: 8 };
|
||||||
const r = bresenham(0, 0, 10, 10, bounds);
|
const r = bresenhamLine(0, 0, 10, 10, bounds);
|
||||||
for (const p of r) {
|
for (const p of r) {
|
||||||
expect(p.x).toBeGreaterThanOrEqual(bounds.minX);
|
expect(p.x).toBeGreaterThanOrEqual(bounds.minX);
|
||||||
expect(p.x).toBeLessThanOrEqual(bounds.maxX);
|
expect(p.x).toBeLessThanOrEqual(bounds.maxX);
|
||||||
|
|
@ -142,24 +142,24 @@ describe("bresenham", () => {
|
||||||
|
|
||||||
it("segment touching only a corner of bounds returns at least one point", () => {
|
it("segment touching only a corner of bounds returns at least one point", () => {
|
||||||
// line passes through (0,0) exactly, bounds include only (0,0)
|
// 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 });
|
const r = bresenhamLine(-2, -2, 2, 2, { minX: 0, maxX: 0, minY: 0, maxY: 0 });
|
||||||
expect(r.length).toBeGreaterThan(0);
|
expect(r.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clipping preserves 4-connectivity within bounds", () => {
|
it("clipping preserves 4-connectivity within bounds", () => {
|
||||||
const r = bresenham(-3, -3, 10, 10, { minX: 0, maxX: 6, minY: 0, maxY: 6 });
|
const r = bresenhamLine(-3, -3, 10, 10, { minX: 0, maxX: 6, minY: 0, maxY: 6 });
|
||||||
expect(is4Connected(r)).toBe(true);
|
expect(is4Connected(r)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clipping preserves 8-connectivity within bounds", () => {
|
it("clipping preserves 8-connectivity within bounds", () => {
|
||||||
const r = bresenham(-3, -3, 10, 10, { minX: 0, maxX: 6, minY: 0, maxY: 6, directions: 8 });
|
const r = bresenhamLine(-3, -3, 10, 10, { minX: 0, maxX: 6, minY: 0, maxY: 6, directions: 8 });
|
||||||
expect(is8Connected(r)).toBe(true);
|
expect(is8Connected(r)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("non-integer inputs", () => {
|
describe("non-integer inputs", () => {
|
||||||
it("rounds float inputs to nearest integer", () => {
|
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]]));
|
expect(bresenhamLine(0.4, 0.4, 2.6, 0.4)).toEqual(pts([[0, 0], [1, 0], [2, 0], [3, 0]]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue