/**
* Воксельная сетка — чистая математика, без зависимостей (урок 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];
}