/** * voxelModelCodec — кодек воксельных моделей пользователя. * * ПРОБЛЕМА которую решает: * Старый формат v2 хранил каждый воксель как * {"x":2,"y":0,"z":24,"c":"#7c5430"} ≈ 36 байт/воксель. * Модель в 7000+ вокселей = 250+ КБ JSON. Парсинг, передача по сети * и постановка в сцену — медленные. Редактор грузил модель >10 сек. * * ФОРМАТ v3 (компактный): * { * "version": 3, * "size": 48, * "palette": ["#7c5430", "a06a3a", "t:grass", ...], // уникальные материалы * "data": [coordIdx, palIdx, coordIdx, palIdx, ...] // плоский массив чисел * } * - coordIdx = x + y*size + z*size*size — одно число вместо трёх полей. * - palIdx — индекс в palette. Цвета/текстуры дедуплицированы. * - Цвет в палитре: строка "#rrggbb" или "rrggbb" (# опционально). * Текстура: строка с префиксом "t:" — например "t:grass". * ≈ 7-9 байт/воксель — в 4-5 раз компактнее v2. * * СОВМЕСТИМОСТЬ: * decodeVoxelModel() читает И v3, И v2 (старые модели), И v1 (только цвет). * encodeVoxelModel() всегда пишет v3. * Внутреннее представление — массив { x, y, z, c?, t? } (как было в v2), * чтобы остальной код (рендер, физика) не переписывать. */ /** Нормализовать hex-цвет к виду "#rrggbb" (lowercase, с #). */ function normHex(hex) { if (typeof hex !== 'string') return '#ffffff'; let h = hex.trim().toLowerCase(); if (h[0] === '#') h = h.slice(1); if (!/^[0-9a-f]{6}$/.test(h)) return '#ffffff'; return '#' + h; } /** * Декодировать model_data (строка JSON или объект) в нормализованную форму. * @returns {{ size:number, voxels:Array<{x,y,z,c?,t?}> } | null} */ export function decodeVoxelModel(modelData) { let data; try { data = typeof modelData === 'string' ? JSON.parse(modelData) : modelData; } catch (e) { return null; } if (!data || typeof data !== 'object') return null; const size = (typeof data.size === 'number' && data.size > 0) ? (data.size | 0) : 32; // --- Формат v3: палитра + плоский массив --- if (data.version === 3 && Array.isArray(data.palette) && Array.isArray(data.data)) { const palette = data.palette; const flat = data.data; const s2 = size * size; const voxels = []; for (let i = 0; i + 1 < flat.length; i += 2) { const coord = flat[i] | 0; const palIdx = flat[i + 1] | 0; const mat = palette[palIdx]; if (mat == null) continue; const x = coord % size; const y = ((coord / size) | 0) % size; const z = (coord / s2) | 0; if (typeof mat === 'string' && mat.startsWith('t:')) { voxels.push({ x, y, z, t: mat.slice(2) }); } else { voxels.push({ x, y, z, c: normHex(mat) }); } } return { size, voxels }; } // --- Формат v2 / v1: список объектов {x,y,z,c|t} --- if (Array.isArray(data.voxels)) { const voxels = []; for (const v of data.voxels) { if (!v || typeof v.x !== 'number') continue; const x = v.x | 0, y = v.y | 0, z = v.z | 0; if (v.t) voxels.push({ x, y, z, t: v.t }); else voxels.push({ x, y, z, c: normHex(v.c || '#ffffff') }); } return { size, voxels }; } return null; } /** * Закодировать модель в компактный формат v3. * @param {number} size — размер сетки (8/16/32/48/64/128). * @param {Array<{x,y,z,c?,t?}>} voxels — список вокселей. * @returns {string} JSON-строка формата v3. */ export function encodeVoxelModel(size, voxels) { const sz = (typeof size === 'number' && size > 0) ? (size | 0) : 32; const s2 = sz * sz; const palette = []; const palMap = new Map(); // материал-строка → индекс палитры const flat = []; const palIndexOf = (matStr) => { let idx = palMap.get(matStr); if (idx === undefined) { idx = palette.length; palette.push(matStr); palMap.set(matStr, idx); } return idx; }; for (const v of voxels) { if (!v || typeof v.x !== 'number') continue; const x = v.x | 0, y = v.y | 0, z = v.z | 0; if (x < 0 || y < 0 || z < 0 || x >= sz || y >= sz || z >= sz) continue; const coord = x + y * sz + z * s2; // Материал: текстура "t:" или цвет "#rrggbb" (# убираем — экономия). let matStr; if (v.t) { matStr = 't:' + v.t; } else { matStr = normHex(v.c || '#ffffff').slice(1); // без # — короче } flat.push(coord, palIndexOf(matStr)); } return JSON.stringify({ version: 3, size: sz, palette, data: flat, }); }