/** * 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/.jpg * И они подхватываются как обычные статические картинки. */ import { BabylonScene } from '../editor/engine/BabylonScene'; import { TEMPLATES } from './templates'; const SCREENSHOT_W = 640; const SCREENSHOT_H = 360; /** * Сгенерировать скриншот одного шаблона. Возвращает Promise. */ 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; }