studio/src/editor/engine/GlbLibrary.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

136 lines
4.8 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.

/**
* GlbLibrary — библиотека импортированных пользователем 3D-моделей .glb
* (Фаза 5.8).
*
* Зачем: автор может загрузить свою glTF/GLB-модель и ставить её в сцену
* как обычную модель — через палитру или scene.spawn('glb:<id>').
*
* Как хранится: каждая модель — { id, name, dataUrl }, dataUrl — base64
* GLB. Сериализуется в scene.glbModels, едет в БД с проектом (своего
* файлового хранилища у Рублокс-веба нет — как звуки/картинки).
*
* Ограничения (GLB в base64 сильно раздувает JSON проекта):
* MAX_GLB — не больше 6 моделей на проект;
* MAX_BYTES — не больше 4 МБ на модель.
*/
// Phase 6.7: лимиты увеличены для эталонных игр (Tower of Hell, Магазин, Tycoon).
// 20 моделей × 4 МБ = до 80 МБ на проект -- крупные игры с GLB-ассетами влезают.
export const MAX_GLB = 20;
export const MAX_GLB_BYTES = 4 * 1024 * 1024;
let _glbIdSeq = 1;
export class GlbLibrary {
constructor() {
// Map id → { id, name, dataUrl }
this.models = new Map();
}
/** Все модели массивом — для UI-панели и палитры. */
list() {
return [...this.models.values()];
}
get(id) {
return this.models.get(id) || null;
}
/** dataURL по id — base64 GLB для SceneLoader. */
getDataUrl(id) {
const m = this.models.get(id);
return m ? m.dataUrl : null;
}
count() {
return this.models.size;
}
/**
* Загрузить .glb из File (input type=file).
* Возвращает Promise<{ ok, id?, error? }>.
*/
addFromFile(file) {
return new Promise((resolve) => {
if (this.models.size >= MAX_GLB) {
resolve({ ok: false, error: `Лимит ${MAX_GLB} моделей на проект` });
return;
}
const name = (file && file.name) || '';
if (!/\.(glb|gltf)$/i.test(name)) {
resolve({ ok: false, error: 'Нужен файл .glb или .gltf' });
return;
}
if (file.size > MAX_GLB_BYTES) {
resolve({
ok: false,
error: `Модель слишком большая (макс ${Math.round(MAX_GLB_BYTES / 1024 / 1024)} МБ)`,
});
return;
}
const reader = new FileReader();
reader.onerror = () => resolve({ ok: false, error: 'Не удалось прочитать файл' });
reader.onload = () => {
const dataUrl = reader.result;
if (typeof dataUrl !== 'string') {
resolve({ ok: false, error: 'Битый файл' });
return;
}
if (dataUrl.length > MAX_GLB_BYTES * 1.4) {
resolve({ ok: false, error: 'Модель слишком большая' });
return;
}
const cleanName = name.replace(/\.[^.]+$/, '');
const id = this.add(cleanName, dataUrl);
resolve({ ok: true, id });
};
reader.readAsDataURL(file);
});
}
/** Добавить модель с готовым dataUrl. Возвращает id. */
add(name, dataUrl) {
const id = `glb_${_glbIdSeq++}`;
this.models.set(id, { id, name: name || 'модель', dataUrl });
return id;
}
rename(id, name) {
const m = this.models.get(id);
if (m) m.name = name || m.name;
}
remove(id) {
this.models.delete(id);
}
// ============ STATE ============
serialize() {
return this.list().map(m => ({
id: m.id, name: m.name, dataUrl: m.dataUrl,
}));
}
load(data) {
this.models.clear();
if (!Array.isArray(data)) return;
let maxNum = 0;
for (const m of data) {
if (!m || typeof m.id !== 'string' || typeof m.dataUrl !== 'string') continue;
this.models.set(m.id, {
id: m.id,
name: typeof m.name === 'string' ? m.name : 'модель',
dataUrl: m.dataUrl,
});
const mt = /^glb_(\d+)$/.exec(m.id);
if (mt) maxNum = Math.max(maxNum, Number(mt[1]));
}
if (maxNum >= _glbIdSeq) _glbIdSeq = maxNum + 1;
}
dispose() {
this.models.clear();
}
}