From 336c388fadb5fc684e1a04e929d6faf8f485775b Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 7 Oct 2025 23:18:54 +0900 Subject: [PATCH] feat: add bresenham line helper --- README.md | 2 +- ROADMAP.md | 2 +- docs/index.d.ts | 8 ++++++ examples/bresenham.ts | 4 +++ src/geometry/bresenham.ts | 55 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 8 ++++++ tests/bresenham.test.ts | 48 ++++++++++++++++++++++++++++++++++ 7 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 examples/bresenham.ts create mode 100644 src/geometry/bresenham.ts create mode 100644 tests/bresenham.test.ts diff --git a/README.md b/README.md index cbed200..0ee5dfe 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ CDN usage: | Search & text | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` | | Data & diff pipelines | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` | | Graph algorithms | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` | -| Visual & geometry | `convexHull`, `lineIntersection`, `pointInPolygon`, `easing`, `quadraticBezier`, `cubicBezier` | `geometry/*.ts`, `visual/*.ts` | `examples/geometry.ts`, `examples/visual.ts` | +| Visual & geometry | `convexHull`, `lineIntersection`, `pointInPolygon`, `bresenhamLine`, `easing`, `quadraticBezier`, `cubicBezier` | `geometry/*.ts`, `visual/*.ts` | `examples/geometry.ts`, `examples/bresenham.ts`, `examples/visual.ts` | ## Scripts ```bash diff --git a/ROADMAP.md b/ROADMAP.md index 815e494..a92467f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -49,7 +49,7 @@ - [x] Object pool helper for reusable entities - [x] Weighted random selector (alias method) - [x] Fisher–Yates shuffle implementation - - [ ] Bresenham line / raster traversal helpers + - [x] Bresenham line / raster traversal helpers - Real-time systems: - [ ] 2D camera system (smooth follow, dead zones, screen shake) - [ ] Particle system with configurable emitters diff --git a/docs/index.d.ts b/docs/index.d.ts index 63a02ff..79ca7f2 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -1118,6 +1118,14 @@ export function lineIntersection( */ export function pointInPolygon(point: Point, polygon: Point[]): boolean; +/** + * Bresenham line rasterisation. + * Use for: grid traversal, tile picking, pixel plotting. + * Performance: O(max(|dx|, |dy|)). + * Import: geometry/bresenham.ts + */ +export function bresenhamLine(start: Point, end: Point): Point[]; + /** * Common easing curves for animation. * Use for: UI transitions, motion design, data viz. diff --git a/examples/bresenham.ts b/examples/bresenham.ts new file mode 100644 index 0000000..e75f66b --- /dev/null +++ b/examples/bresenham.ts @@ -0,0 +1,4 @@ +import { bresenhamLine } from '../src/index.js'; + +const cells = bresenhamLine({ x: 2, y: 3 }, { x: 10, y: 7 }); +console.log(cells); diff --git a/src/geometry/bresenham.ts b/src/geometry/bresenham.ts new file mode 100644 index 0000000..41011c5 --- /dev/null +++ b/src/geometry/bresenham.ts @@ -0,0 +1,55 @@ +import type { Point } from '../types.js'; + +function assertPoint(point: Point, name: string): void { + if (typeof point?.x !== 'number' || Number.isNaN(point.x) || !Number.isFinite(point.x)) { + throw new Error(`${name}.x must be a finite number.`); + } + if (typeof point?.y !== 'number' || Number.isNaN(point.y) || !Number.isFinite(point.y)) { + throw new Error(`${name}.y must be a finite number.`); + } +} + +function round(value: number): number { + return Math.round(value); +} + +/** + * Generates integer raster coordinates using Bresenham's line algorithm. + * Useful for: tile picking, grid ray tracing, and discrete drawing operations. + */ +export function bresenhamLine(start: Point, end: Point): Point[] { + assertPoint(start, 'start'); + assertPoint(end, 'end'); + + let x0 = round(start.x); + let y0 = round(start.y); + const x1 = round(end.x); + const y1 = round(end.y); + + const points: Point[] = []; + + const dx = Math.abs(x1 - x0); + const sx = x0 < x1 ? 1 : -1; + const dy = -Math.abs(y1 - y0); + const sy = y0 < y1 ? 1 : -1; + let error = dx + dy; + + points.push({ x: x0, y: y0 }); + while (x0 !== x1 || y0 !== y1) { + const e2 = 2 * error; + if (e2 >= dy) { + error += dy; + x0 += sx; + } + if (e2 <= dx) { + error += dx; + y0 += sy; + } + points.push({ x: x0, y: y0 }); + } + + return points; +} + +/** @internal */ +export const __internals = { assertPoint, round }; diff --git a/src/index.ts b/src/index.ts index da8ea9f..e8bc0ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -110,6 +110,7 @@ export const examples = { convexHull: 'examples/geometry.ts', lineIntersection: 'examples/geometry.ts', pointInPolygon: 'examples/geometry.ts', + bresenhamLine: 'examples/bresenham.ts', }, visual: { easing: 'examples/visual.ts', @@ -547,6 +548,13 @@ export { lineIntersection } from './geometry/lineIntersection.js'; */ export { pointInPolygon } from './geometry/pointInPolygon.js'; +/** + * Bresenham rasterisation for grid-based line traversal. + * + * Example file: examples/bresenham.ts + */ +export { bresenhamLine } from './geometry/bresenham.js'; + // ============================================================================ // 🎨 VISUAL & ANIMATION // ============================================================================ diff --git a/tests/bresenham.test.ts b/tests/bresenham.test.ts new file mode 100644 index 0000000..7dd7243 --- /dev/null +++ b/tests/bresenham.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; + +import { bresenhamLine } from '../src/index.js'; + +describe('bresenhamLine', () => { + it('handles horizontal lines', () => { + const points = bresenhamLine({ x: 0, y: 2 }, { x: 4, y: 2 }); + expect(points).toEqual([ + { x: 0, y: 2 }, + { x: 1, y: 2 }, + { x: 2, y: 2 }, + { x: 3, y: 2 }, + { x: 4, y: 2 }, + ]); + }); + + it('handles steep lines', () => { + const points = bresenhamLine({ x: 1, y: 1 }, { x: 3, y: 6 }); + expect(points).toEqual([ + { x: 1, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + { x: 2, y: 4 }, + { x: 3, y: 5 }, + { x: 3, y: 6 }, + ]); + }); + + it('rounds floating inputs', () => { + const points = bresenhamLine({ x: 0.4, y: 0.6 }, { x: 2.6, y: 2.4 }); + expect(points).toEqual([ + { x: 0, y: 1 }, + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 2 }, + ]); + }); + + it('handles identical points', () => { + const points = bresenhamLine({ x: 1.2, y: 1.2 }, { x: 1.4, y: 1.4 }); + expect(points).toEqual([{ x: 1, y: 1 }]); + }); + + it('throws on invalid points', () => { + expect(() => bresenhamLine({ x: Number.NaN, y: 0 }, { x: 1, y: 1 })).toThrow(/start.x/); + expect(() => bresenhamLine({ x: 0, y: 0 }, { x: 1, y: Number.POSITIVE_INFINITY })).toThrow(/end.y/); + }); +});