player/src/engine/AssetManager.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

149 lines
5.6 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.

/**
* AssetManager — библиотека пользовательских картинок проекта.
*
* Зачем: даёт автору игры загрузить свои PNG/JPG и использовать их
* - как текстуру на гранях примитива (поле `textureAsset` в данных примитива);
* - как картинку в image-GUI (поле `imageAsset` у GUI-элемента).
*
* Как хранится: каждый ассет — { id, name, dataUrl }, где dataUrl —
* base64-PNG. Библиотека сериализуется в scene.assets и едет в БД вместе
* с проектом (своего файлового хранилища у Рублокс-веба пока нет, поэтому
* картинки встроены в JSON — так же как аватары пользователей в нативе).
*
* Ограничения (чтобы JSON проекта не раздувался и не ломал автосейв):
* MAX_ASSETS — не больше 24 картинок на проект;
* MAX_SIDE — картинка ужимается до 512px по большей стороне;
* приводится к PNG через canvas — единый формат, отсекает EXIF и т.п.
*/
export const MAX_ASSETS = 24;
export const MAX_SIDE = 512;
let _idSeq = 1;
export class AssetManager {
constructor() {
// Map id → { id, name, dataUrl }
this.assets = new Map();
}
/** Все ассеты массивом — для UI-панели. */
list() {
return [...this.assets.values()];
}
get(id) {
return this.assets.get(id) || null;
}
/** dataURL по id — то, что отдаём в Babylon Texture / <img src>. */
getDataUrl(id) {
const a = this.assets.get(id);
return a ? a.dataUrl : null;
}
count() {
return this.assets.size;
}
/**
* Загрузить картинку из File (input type=file).
* Возвращает Promise<{ ok, id?, error? }>.
* Картинка нормализуется: ужимается до MAX_SIDE, конвертится в PNG.
*/
addFromFile(file) {
return new Promise((resolve) => {
if (this.assets.size >= MAX_ASSETS) {
resolve({ ok: false, error: `Лимит ${MAX_ASSETS} картинок на проект` });
return;
}
if (!file || !/^image\//.test(file.type)) {
resolve({ ok: false, error: 'Это не картинка' });
return;
}
const reader = new FileReader();
reader.onerror = () => resolve({ ok: false, error: 'Не удалось прочитать файл' });
reader.onload = () => {
const img = new Image();
img.onerror = () => resolve({ ok: false, error: 'Битая картинка' });
img.onload = () => {
try {
const dataUrl = AssetManager._normalize(img);
const name = (file.name || 'картинка').replace(/\.[^.]+$/, '');
const id = this.add(name, dataUrl);
resolve({ ok: true, id });
} catch (e) {
resolve({ ok: false, error: 'Ошибка обработки картинки' });
}
};
img.src = reader.result;
};
reader.readAsDataURL(file);
});
}
/** Добавить ассет с готовым dataUrl. Возвращает id. */
add(name, dataUrl) {
const id = `asset_${_idSeq++}`;
this.assets.set(id, { id, name: name || 'картинка', dataUrl });
return id;
}
rename(id, name) {
const a = this.assets.get(id);
if (a) a.name = name || a.name;
}
remove(id) {
this.assets.delete(id);
}
/** Нормализация: ужать до MAX_SIDE, перерисовать в canvas → PNG dataURL. */
static _normalize(img) {
let w = img.naturalWidth || img.width;
let h = img.naturalHeight || img.height;
if (w <= 0 || h <= 0) throw new Error('zero size');
if (w > MAX_SIDE || h > MAX_SIDE) {
const k = MAX_SIDE / Math.max(w, h);
w = Math.round(w * k);
h = Math.round(h * k);
}
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
return canvas.toDataURL('image/png');
}
// ============ STATE ============
serialize() {
return this.list().map(a => ({
id: a.id, name: a.name, dataUrl: a.dataUrl,
}));
}
load(data) {
this.assets.clear();
if (!Array.isArray(data)) return;
let maxNum = 0;
for (const a of data) {
if (!a || typeof a.id !== 'string' || typeof a.dataUrl !== 'string') continue;
this.assets.set(a.id, {
id: a.id,
name: typeof a.name === 'string' ? a.name : 'картинка',
dataUrl: a.dataUrl,
});
const m = /^asset_(\d+)$/.exec(a.id);
if (m) maxNum = Math.max(maxNum, Number(m[1]));
}
// Чтобы новые id не конфликтовали с загруженными.
if (maxNum >= _idSeq) _idSeq = maxNum + 1;
}
dispose() {
this.assets.clear();
}
}