Большой консолидирующий коммит после поднятия studio.rublox.pro (28 мая 2026). Содержит изменения которые делались в процессе подготовки прод-окружения: Фиксы импортов после выноса из minecraftia: - Массовая замена путей ../../components → ../components (40+ файлов в src/community/, src/admin-preview/) - Замена ../KubikonEditor/ → ../editor/, ../KubikonStudio/ → ../community/, ../AdminPreview/ → ../admin-preview/ - API.js скопирован из минки целиком (было 8 экспортов, стало 312) - Добавлены PLAYER_URL, MyButton_1, недостающие компоненты - Заменены require() на статические ES-imports в BabylonScene, PrimitiveManager, GameRuntime (Vite не поддерживает CJS require) Структура ассетов: - public/kubikon-templates/ → public/assets/kubikon-templates/ - public/kubikon-learn/ → public/assets/kubikon-learn/ - (код искал в /assets/, файлы лежали без /assets/) Навигация роутов внутри студии: - /kubikon-studio/docs → /docs (90+ навигационных вызовов sed-replaced) - /kubikon-editor/X → /edit/X, /kubikon/play/X → /play/X, /kubikon/gd/X → /gd/X UI: - Новый компонент StudioHeader (61px, как в минке) + копия favicon - WithHeader wrapper в App.jsx для всех страниц кроме fullscreen-редактора/плеера - SSO ticket-flow в AuthContext (auto-redeem #ticket= при загрузке) - Тёмная тема карточек игр в ВИКИ (фон #1c2231 вместо #fff, картинка впритык) Документация: - docs/ONBOARDING.md — путь нового контрибьютора от нуля до PR - docs/TUTORIAL_ADD_SCRIPT_API.md — как добавить game.* API - API_USAGE.md — список эндпоинтов backend - README в подпапках engine/, engine/terrain/, engine/voxel/, engine/robloxterrain/, engine/types/ .gitignore: - public/wiki/ исключён (73МБ PNG, будут на CDN отдельной задачей) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
5.0 KiB
JavaScript
122 lines
5.0 KiB
JavaScript
/**
|
||
* templateScreenshots.js — генерирует PNG-скриншоты шаблонов offscreen.
|
||
*
|
||
* Запускается dev-кнопкой «Перегенерировать превью» в KubikonStudio.
|
||
* Для каждого шаблона:
|
||
* 1) создаёт оффскрин Babylon engine + scene
|
||
* 2) грузит state шаблона через scene.loadFromState
|
||
* 3) ждёт следующего кадра (модели подгружаются async)
|
||
* 4) делает canvas.toDataURL('image/jpeg')
|
||
* 5) триггерит скачивание файла
|
||
*
|
||
* Полученные PNG-файлы пользователь кладёт в
|
||
* public/assets/kubikon-templates/<id>.jpg
|
||
* И они подхватываются как обычные статические картинки.
|
||
*/
|
||
|
||
import { BabylonScene } from '../editor/engine/BabylonScene';
|
||
import { TEMPLATES } from './templates';
|
||
|
||
const SCREENSHOT_W = 640;
|
||
const SCREENSHOT_H = 360;
|
||
|
||
/**
|
||
* Сгенерировать скриншот одного шаблона. Возвращает Promise<dataUrl>.
|
||
*/
|
||
async function captureOne(template) {
|
||
// Временный canvas, добавляем в DOM (Babylon engine требует видимый canvas
|
||
// для GL-контекста — но мы скрываем визуально через position:absolute + opacity:0).
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = SCREENSHOT_W;
|
||
canvas.height = SCREENSHOT_H;
|
||
canvas.style.position = 'fixed';
|
||
canvas.style.left = '-9999px';
|
||
canvas.style.top = '0';
|
||
canvas.style.width = `${SCREENSHOT_W}px`;
|
||
canvas.style.height = `${SCREENSHOT_H}px`;
|
||
canvas.style.opacity = '0';
|
||
canvas.style.pointerEvents = 'none';
|
||
canvas.tabIndex = -1;
|
||
document.body.appendChild(canvas);
|
||
|
||
let scene = null;
|
||
try {
|
||
scene = new BabylonScene(canvas);
|
||
scene.init();
|
||
|
||
// Стандартная камера для красивого ракурса (3/4 сверху)
|
||
if (scene.camera) {
|
||
scene.camera.position.set(60, 60, 60);
|
||
// Нацеливаем камеру в (0, 5, 0) через yaw/pitch.
|
||
// UniversalCamera использует rotation.y = yaw, rotation.x = pitch.
|
||
const tx = 0, ty = 5, tz = 0;
|
||
const dx = tx - scene.camera.position.x;
|
||
const dy = ty - scene.camera.position.y;
|
||
const dz = tz - scene.camera.position.z;
|
||
scene.camera.rotation.y = Math.atan2(dx, dz);
|
||
const horiz = Math.sqrt(dx * dx + dz * dz);
|
||
scene.camera.rotation.x = -Math.atan2(dy, horiz);
|
||
scene.camera.rotation.z = 0;
|
||
}
|
||
|
||
const state = template.build();
|
||
await scene.loadFromState(state);
|
||
|
||
// Прогреваем 3 кадра и ждём — модели подгружаются async, материалы
|
||
// компилятся в первом render-loop.
|
||
for (let i = 0; i < 8; i++) {
|
||
scene.scene.render();
|
||
await new Promise(res => requestAnimationFrame(res));
|
||
}
|
||
|
||
// Дополнительная пауза 500мс для GLB-моделей
|
||
await new Promise(res => setTimeout(res, 500));
|
||
for (let i = 0; i < 4; i++) {
|
||
scene.scene.render();
|
||
await new Promise(res => requestAnimationFrame(res));
|
||
}
|
||
|
||
// Снимок
|
||
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
|
||
return dataUrl;
|
||
} finally {
|
||
// Чистим
|
||
try { scene?.dispose?.(); } catch (e) { /* ignore */ }
|
||
try { document.body.removeChild(canvas); } catch (e) { /* ignore */ }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Триггер скачивания PNG-файла в браузере.
|
||
*/
|
||
function downloadDataUrl(dataUrl, filename) {
|
||
const a = document.createElement('a');
|
||
a.href = dataUrl;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
}
|
||
|
||
/**
|
||
* Сгенерировать скриншоты ВСЕХ шаблонов и скачать их по одному.
|
||
* Возвращает Promise который резолвится массивом dataUrl'ов.
|
||
*
|
||
* @param {(progress:{current:number,total:number,id:string}) => void} onProgress
|
||
*/
|
||
export async function generateAllTemplateScreenshots(onProgress) {
|
||
const results = [];
|
||
for (let i = 0; i < TEMPLATES.length; i++) {
|
||
const tpl = TEMPLATES[i];
|
||
if (onProgress) onProgress({ current: i + 1, total: TEMPLATES.length, id: tpl.id });
|
||
// eslint-disable-next-line no-await-in-loop
|
||
const dataUrl = await captureOne(tpl);
|
||
downloadDataUrl(dataUrl, `${tpl.id}.jpg`);
|
||
results.push({ id: tpl.id, dataUrl });
|
||
// Небольшая пауза между генерациями чтобы браузер не тормозил
|
||
// eslint-disable-next-line no-await-in-loop
|
||
await new Promise(res => setTimeout(res, 200));
|
||
}
|
||
return results;
|
||
}
|