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)
137 lines
5.4 KiB
JavaScript
137 lines
5.4 KiB
JavaScript
/**
|
||
* 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,
|
||
});
|
||
}
|