studio/src/editor/engine/voxelModelCodec.js
МИН 31adbf151b Initial public release: Студия Рублокса v1.0
Open-source веб-студия для создания игр Рублокса, двойная лицензия
AGPL-3.0 + Коммерческая.

Главное:
- Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16
- Самодостаточный движок ~28к строк (66 файлов): BlockManager,
  TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController,
  ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов
- Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco)
- Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn)
- Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt)
- 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.)
- Конфигурируемый бэкенд через VITE_API_BASE — работает со staging
  (dev-api.rublox.pro) без настройки
- Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка
- Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING,
  SECURITY, CHANGELOG
- ESLint + Prettier + EditorConfig
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Перед публикацией:
- Все импорты из minecraftia заменены на локальные
- Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env
- Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо)
- AdminKubikonModeration не публикуется (модерация — в team.rublox.pro)
- 93 МБ ассетов public/kubikon-assets вынесены в .gitignore
  (раздаются через release artifact)
2026-05-27 23:41:10 +03:00

137 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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:<id>" или цвет "#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,
});
}