/**
* Формат воксельной модели (JSON): размер, плотные данные base64,
* слоты палитры. Модель — данные: генераторы/импорт .vox/ручная правка
* дают один и тот же формат; валидация — числовая, без рендера.
*
* Слой Y растёт вверх; порядок вокселей в данных: x, затем y, затем z
* (idx = x + sx*(y + sy*z) — как в VoxelGrid).
*/
import { VoxelGrid } from '../voxel/grid';
export interface VoxelModel {
/** Размер по осям [sx, sy, sz], каждая > 0. */
size: [number, number, number];
/** Слоты палитры: слот-индекс сцены → hex; значения 1..255 — воксели ссылаются на ключи. */
palette: Record<string, string>;
/** base64 плотных данных: 1 байт на воксель, 0 = пусто. */
data: string;
}
const MAX_DIM = 1024; // предел по оси — защита от мусора и OOM
const MAX_VOXELS = 32 * 1024 * 1024; // ~32 МБ данных
/** Кодирует сетку в модель: палитра берётся из карты слот→hex. */
export function encodeModel(grid: VoxelGrid, palette: Record<string, string>): VoxelModel {
const bytes = new Uint8Array(grid.sx * grid.sy * grid.sz);
for (let i = 0; i < bytes.length; i++) bytes[i] = grid.data[i];
return {
size: [grid.sx, grid.sy, grid.sz],
palette: { ...palette },
data: bytesToBase64(bytes),
};
}
/** Декодирует модель в сетку. Бросает на невалидных данных (см. validateModel). */
export function decodeModel(model: VoxelModel): VoxelGrid {
assertModelValid(model);
const [sx, sy, sz] = model.size;
const bytes = base64ToBytes(model.data);
if (bytes.length !== sx * sy * sz) {
throw new Error(`decodeModel: данных ${bytes.length} байт, а нужно ${sx * sy * sz}`);
}
const grid = new VoxelGrid(sx, sy, sz);
for (let i = 0; i < bytes.length; i++) grid.data[i] = bytes[i];
return grid;
}
/**
* Числовая валидация модели без декодирования. Возвращает список проблем;
* пустой список = модель валидна. Проверяет: размер, объём, hex-слоты,
* что данные — валидный base64 нужной длины, что байты ссылаются на слоты.
*/
export function validateModel(model: unknown): string[] {
const problems: string[] = [];
const m = model as Partial<VoxelModel> | null;
if (!m || typeof m !== 'object') return ['модель — не объект'];
if (!Array.isArray(m.size) || m.size.length !== 3 ||
m.size.some((n) => !Number.isInteger(n) || n <= 0 || n > MAX_DIM)) {
problems.push(`size должен быть [sx,sy,sz] с целыми 1..${MAX_DIM}`);
return problems; // без размера дальше проверять нечего
}
const [sx, sy, sz] = m.size;
const volume = sx * sy * sz;
if (volume > MAX_VOXELS) problems.push(`объём ${volume} > ${MAX_VOXELS}`);
if (!m.palette || typeof m.palette !== 'object') {
problems.push('palette должен быть объектом слот→hex');
} else {
for (const [slot, hex] of Object.entries(m.palette)) {
const idx = Number(slot);
if (!Number.isInteger(idx) || idx < 1 || idx > 255) {
problems.push(`слот «${slot}» вне 1..255`);
}
if (typeof hex !== 'string' || !/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(hex)) {
problems.push(`слот ${slot}: hex «${hex}» не вида #rgb/#rrggbb`);
}
}
}
const bytes = safeBase64(m.data);
if (!bytes) {
problems.push('data — не валидный base64');
return problems;
}
if (bytes.length !== volume) {
problems.push(`данных ${bytes.length} байт, а size требует ${volume}`);
return problems;
}
// Ссылки на слоты проверяем только при целостной палитре-объекте
const palette = m.palette;
if (palette && typeof palette === 'object' && !Array.isArray(palette)) {
for (const b of bytes) {
if (b !== 0 && !(b in palette)) {
problems.push(`воксель со слотом ${b} отсутствует в палитре`);
break;
}
}
}
return problems;
}
/** Кидает первую проблему валидации (удобно в decode). */
export function assertModelValid(model: unknown): void {
const problems = validateModel(model);
if (problems.length > 0) throw new Error(`модель невалидна: ${problems[0]}`);
}
function bytesToBase64(bytes: Uint8Array): string {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
function base64ToBytes(b64: string): Uint8Array {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
function safeBase64(s: unknown): Uint8Array | null {
if (typeof s !== 'string') return null;
try { return base64ToBytes(s); } catch { return null; }
}