1
0
Fork 0

Add the explicit breaker option instead of .next(true) protocol.

This commit is contained in:
Pabloader 2026-05-04 14:52:18 +00:00
parent 96d85d3164
commit 3bc89af23b
1 changed files with 23 additions and 38 deletions

View File

@ -1,6 +1,6 @@
import { clamp } from "@common/utils"; import { clamp } from "@common/utils";
export interface Point { interface Point {
x: number; x: number;
y: number; y: number;
} }
@ -21,16 +21,27 @@ interface BresenhamOptions {
export interface BresenhamLineOptions extends BresenhamOptions {} export interface BresenhamLineOptions extends BresenhamOptions {}
export interface BresenhamCircleOptions<T = boolean | 'fov'> extends BresenhamOptions { interface BresenhamCircleBaseOptions<T extends boolean | 'fov'> extends BresenhamOptions {
/** /**
* - `false` (default) outline only. * - `false` (default) outline only.
* - `true` filled disc (span-from-outline scanline). * - `true` filled disc (span-from-outline scanline).
* - `'fov'` ray-casting field of view; the generator accepts `true` via * - `'fov'` ray-casting field of view.
* `.next(true)` to signal an obstacle and skip the rest of that ray.
*/ */
fill?: T; fill?: T;
} }
type BresenhamCircleFillOptions = BresenhamCircleBaseOptions<boolean>;
interface BresenhamCircleFovOptions extends BresenhamCircleBaseOptions<'fov'> {
/**
* A callback that is called for each point on the ray casted from the start
* point to the current point. If the callback returns `true`, the ray is
* terminated and the generator skips to the next ray.
*/
breaker?: (x: number, y: number) => boolean;
}
type BresenhamCircleOptions = BresenhamCircleFillOptions | BresenhamCircleFovOptions;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Cohen-Sutherland outcodes // Cohen-Sutherland outcodes
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -247,30 +258,16 @@ export const bresenhamLine = (
* *
* ## FOV mode (`fill: 'fov'`) ray-casting field of view * ## FOV mode (`fill: 'fov'`) ray-casting field of view
* *
* For each outline point, casts an 8-connected Bresenham ray from the centre * For each outline point, casts an 4-connected Bresenham ray from the centre
* to that point and yields every cell along the ray. The generator uses the * to that point and yields every cell along the ray. You can pass `breaker`
* **send protocol**: the caller can pass `true` to `.next(true)` after * to signal an obstacle the rest of that ray is then
* 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 * 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). * is not yielded (obstacle is hidden).
*
* ```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 * **Bounds:** The outline is always generated without clipping so that rays
* extend to the full circle perimeter. The individual rays are clipped to * extend to the full circle perimeter. The individual rays are clipped to
* the supplied bounds, so only in-bounds cells are yielded. * 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 * ## Clipping
@ -284,25 +281,13 @@ export const bresenhamLine = (
* yields only the centre point. * yields only the centre point.
* @param options - Optional `fill` flag and clip bounds. * @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( export function* bresenhamCircleGen(
x: number, x: number,
y: number, y: number,
r: number, r: number,
options?: BresenhamCircleOptions, options?: BresenhamCircleOptions,
): Generator<Point, void, boolean | void> { ): Generator<Point, void, void> {
if (options?.fill === 'fov') { if (options?.fill === 'fov') {
const lineBounds: BresenhamLineOptions = { const lineBounds: BresenhamLineOptions = {
directions: options.directions, directions: options.directions,
@ -313,8 +298,8 @@ export function* bresenhamCircleGen(
}; };
for (const { x: ox, y: oy } of bresenhamCircleGen(x, y, r, { fill: false })) { for (const { x: ox, y: oy } of bresenhamCircleGen(x, y, r, { fill: false })) {
for (const linePoint of bresenhamLineGen(x, y, ox, oy, lineBounds)) { for (const linePoint of bresenhamLineGen(x, y, ox, oy, lineBounds)) {
const skipLine = yield linePoint; if (options?.breaker?.(linePoint.x, linePoint.y)) break;
if (skipLine) break; yield linePoint;
} }
} }
return; return;
@ -403,5 +388,5 @@ export const bresenhamCircle = (
x: number, x: number,
y: number, y: number,
r: number, r: number,
options?: BresenhamCircleOptions<boolean>, options?: BresenhamCircleFillOptions,
): Point[] => Array.from(bresenhamCircleGen(x, y, r, options)); ): Point[] => Array.from(bresenhamCircleGen(x, y, r, options));