Newer
Older
rpg / v2 / packages / engine / src / voxel / grid.ts
/**
 * Воксельная сетка — чистая математика, без зависимостей (урок v1 №3).
 *
 * Значение вокселя — индекс цвета в палитре сцены (1..255); 0 — пустота.
 * Сетка фиксированного размера, координаты — целые мировые единицы (1 = размер
 * вокселя). Никакой проекции тут нет: проекция — на границе рендера (урок №1).
 */

export interface VoxelPalette {
    /** индекс → цвет в hex (#rrggbb); индекс 0 (пустота) не используется */
    colors: Record<number, string>;
}

export class VoxelGrid {
    readonly sx: number;
    readonly sy: number;
    readonly sz: number;
    /** плоский буфер: index = x + sx*(y + sy*z); 0 — пусто, иначе цвет 1..255 */
    readonly data: Uint8Array;

    constructor(sx: number, sy: number, sz: number) {
        if (sx <= 0 || sy <= 0 || sz <= 0) {
            throw new Error(`VoxelGrid: размеры должны быть положительными, дано ${sx}×${sy}×${sz}`);
        }
        this.sx = sx;
        this.sy = sy;
        this.sz = sz;
        this.data = new Uint8Array(sx * sy * sz);
    }

    idx(x: number, y: number, z: number): number {
        return x + this.sx * (y + this.sy * z);
    }

    /** Внутри ли координата сетки. */
    inside(x: number, y: number, z: number): boolean {
        return x >= 0 && y >= 0 && z >= 0 && x < this.sx && y < this.sy && z < this.sz;
    }

    /** Цвет вокселя (0 — пусто). Вне сетки всегда 0. */
    get(x: number, y: number, z: number): number {
        if (!this.inside(x, y, z)) return 0;
        return this.data[this.idx(x, y, z)];
    }

    /** Поставить воксель (0 — стереть). Игнорирует выход за границы. */
    set(x: number, y: number, z: number, color: number): void {
        if (!this.inside(x, y, z)) return;
        if (color < 0 || color > 255) throw new Error(`VoxelGrid.set: цвет ${color} вне 0..255`);
        this.data[this.idx(x, y, z)] = color;
    }

    isEmpty(x: number, y: number, z: number): boolean {
        return this.get(x, y, z) === 0;
    }

    /** Число непустых вокселей (для тестов и статистики). */
    count(): number {
        let n = 0;
        for (let i = 0; i < this.data.length; i++) if (this.data[i] !== 0) n++;
        return n;
    }
}

/** Парсинг hex-цвета (#rgb или #rrggbb) в [r,g,b] 0..1. */
export function hexToRgb(hex: string): [number, number, number] {
    const h = hex.replace('#', '');
    const s = h.length === 3 ? h.split('').map((c) => c + c).join('') : h;
    const n = parseInt(s, 16);
    if (Number.isNaN(n) || s.length !== 6) throw new Error(`hexToRgb: плохой цвет "${hex}"`);
    return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}