Files

121 lines
7.0 KiB
JavaScript

/* Presentation transport only. Aego's Go runtime owns the preview world,
* prepared textures, lighting, materials, CPU particles and GPU resources. */
'use strict';
(() => {
const byID = id => document.getElementById(id);
let latest, loaded, loading, queued = false, published = '', configured = '', sequence = 0;
let stopped = false, lastStatus = {}, transportError = '';
let rateOverride = null;
const mode = byID('gpu-mode'), canvas = byID('gpu-preview'), status = byID('gpu-status');
const ru = () => document.documentElement.lang === 'ru';
const message = (text, error = false) => { status.textContent = text; status.classList.toggle('error', error); };
const fail = error => { transportError = error.message || String(error); message(transportError, true); canvas.classList.add('gpu-failed'); };
function options() {
return {mode: mode.value, frame: latest?.frame || '', effectTemplate:latest?.effectTemplate || '',
rateOverride:latest?.effectTemplate ? rateOverride : null, flipX: byID('gpu-flip-x').checked,
flipY: byID('gpu-flip-y').checked, bloom: byID('gpu-bloom').checked,
shadows: byID('gpu-shadows').checked, lightX: Number(byID('gpu-light').value), lightY: -.65,
intensity: 2.2, emission: Number(byID('gpu-emission').value), amount: Number(byID('gpu-amount').value),
rate: Number(byID('gpu-rate').value), speed: 30, lifetime: 1.4};
}
function accept(raw) {
const result = JSON.parse(raw);
if (result.error) throw new Error(result.error);
if (result.status) lastStatus = result.status;
return result;
}
async function load() {
if (loaded) return loaded;
if (loading) return loading;
loading = (async () => {
message(ru() ? 'Загрузка GPU renderer…' : 'Loading GPU renderer…');
const response = await fetch(window.AegoStudioRelease?.previewWasmURL || 'preview.wasm');
if (!response.ok) throw new Error(`GPU WASM HTTP ${response.status}`);
const go = new Go();
let instance;
try { ({instance} = await WebAssembly.instantiateStreaming(response.clone(), go.importObject)); }
catch { ({instance} = await WebAssembly.instantiate(await response.arrayBuffer(), go.importObject)); }
go.run(instance).catch(error => { stopped = true; fail(error); });
const api = window.aegoArtPreview;
if (!api) throw new Error('GPU WASM bridge did not initialize');
accept(api.start());
loaded = api;
return api;
})();
try { return await loading; } catch (error) { loading = null; throw error; }
}
async function synchronize() {
queued = false;
if (!latest || mode.value === 'pixels' || stopped) return;
try {
const preview = await load();
if (!latest || mode.value === 'pixels' || stopped) return;
const key = `${latest.documentKey}:${latest.revision}`;
if (published !== key && !latest.gesture) {
const snapshot = latest.api.snapshotBytes();
if (snapshot.error) throw new Error(snapshot.error);
if (snapshot.handle !== latest.documentKey || snapshot.revision !== latest.revision) { schedule(); return; }
accept(preview.publish(++sequence, snapshot.revision, snapshot.bytes));
published = key; configured = '';
}
const config = JSON.stringify(options());
if (config !== configured) { accept(preview.configure(config)); configured = config; }
// A ready renderer may still contain the previous document. A failed
// publish/configure remains visible until this exact state is accepted.
if (published === key && configured === config) transportError = '';
poll();
} catch (error) { fail(error); }
}
function schedule() { if (!queued) { queued = true; requestAnimationFrame(synchronize); } }
function poll() {
if (!loaded || mode.value === 'pixels' || stopped) return;
try {
accept(loaded.status());
if (transportError) { message(transportError, true); canvas.classList.add('gpu-failed'); return; }
if (latest && published !== `${latest.documentKey}:${latest.revision}`) {
message(ru() ? 'Обновление предпросмотра…' : 'Updating preview…');
return;
}
if (latest?.effectTemplate && rateOverride === null && Number.isFinite(lastStatus.particleRate)) byID('gpu-rate').value = lastStatus.particleRate;
const failed = lastStatus.state === 'context-lost' || lastStatus.state === 'error';
canvas.classList.toggle('gpu-failed', failed);
if (lastStatus.state === 'context-lost') message(ru() ? 'Контекст WebGL потерян; ожидается восстановление.' : 'WebGL context lost; waiting for restoration.', true);
else if (lastStatus.error) message(lastStatus.error, true);
else {status.title = lastStatus.renderer || ''; message(`WebGL2 · ${ru() ? 'Готово' : 'Ready'}${mode.value === 'particles' ? ` · ${lastStatus.particlePreset || 'ParticleSystem2D'} · ${lastStatus.particles || 0} ${ru() ? 'частиц' : 'particles'}` : ''}`);}
} catch (error) { fail(error); }
}
function select() {
const enabled = mode.value !== 'pixels';
byID('preview').classList.toggle('hidden', enabled);
canvas.classList.toggle('hidden', !enabled);
byID('gpu-controls').classList.toggle('hidden', !enabled);
status.classList.toggle('hidden', !enabled);
byID('gpu-restart').hidden = mode.value !== 'particles';
document.body.classList.toggle('has-gpu-preview', enabled);
if (enabled) {configured = ''; schedule();}
else if (loaded) {const config = {...options(), mode:lastStatus.mode || 'sprite', paused:true}; try {accept(loaded.configure(JSON.stringify(config)));} catch (error) {message(error.message,true);} }
}
mode.addEventListener('change', select);
byID('gpu-rate').addEventListener('input', () => { rateOverride = Number(byID('gpu-rate').value); });
byID('gpu-controls').addEventListener('input', schedule);
byID('gpu-restart').addEventListener('click', async () => {
if (mode.value !== 'particles' || stopped) return;
try { await synchronize(); if (loaded) { accept(loaded.restartParticles()); poll(); } }
catch (error) { fail(error); }
});
canvas.addEventListener('webglcontextlost', () => { canvas.classList.add('gpu-failed'); message(ru() ? 'Контекст WebGL потерян.' : 'WebGL context lost.', true); });
canvas.addEventListener('webglcontextrestored', () => { configured = ''; schedule(); });
window.addEventListener('pagehide', event => { if (!event.persisted) { stopped = true; loaded?.close(); } });
setInterval(poll, 400);
window.AegoGPUPreview = Object.freeze({
update(value) {
if (latest?.documentKey !== value.documentKey || latest?.effectTemplate !== value.effectTemplate) {
rateOverride = null; byID('gpu-rate').value = 16;
}
latest = value; if (mode.value !== 'pixels') schedule();
},
select(value) { if (![...mode.options].some(o => o.value === value)) return false; mode.value = value; select(); return true; },
status() { return {...lastStatus}; }
});
})();