player/src/engine/GlbLibrary.js
МИН 87444ee2c8 Initial public release: Rublox Player v1.0
Open-source web player for Rublox games, dual-licensed under
AGPL-3.0 + Commercial.

Highlights:
- Babylon.js 7 + React 18 + Vite 5 stack
- Self-contained engine (~46k lines): BlockManager, ModelManager,
  PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD
  gamemodes
- Configurable backend via VITE_API_BASE and friends — works against
  staging (dev-api.rublox.pro) out of the box
- Standalone mode (VITE_STANDALONE=true) loads a bundled sample game
  for first-run without any backend
- Full docs: README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG
- Lint + format scaffolding (ESLint + Prettier + EditorConfig)
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Removed before public release:
- frontend_deploy.py (contained production SSH credentials)
- ~27 admin endpoints (kept in private repo)
- Hard-coded internal URLs and IPs
- All previous git history (clean repo init)
2026-05-27 23:04:04 +03:00

134 lines
4.5 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 МБ на модель.
*/
export const MAX_GLB = 6;
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();
}
}