import { startViewer, stopViewer, recenter, setJoystick } from './viewer.js';
const STAGES = [
['frames', 'Klatki z wideo'],
['colmap', 'Pozycje kamer (COLMAP)'],
['reconstruct', 'Rekonstrukcja 3D (GenRecon)'],
['glb', 'Składanie sceny (GLB)'],
];
const $ = (id) => document.getElementById(id);
let poll = null;
function show(view) {
for (const v of document.querySelectorAll('.view')) v.classList.remove('active');
$(view).classList.add('active');
}
function buildStages() {
$('stages').innerHTML = STAGES
.map(([k, label]) => `
${label}`).join('');
}
function updateStages(current, status) {
const order = STAGES.map((s) => s[0]);
const idx = order.indexOf(current);
document.querySelectorAll('#stages li').forEach((li, i) => {
li.classList.toggle('done', status === 'done' || i < idx);
li.classList.toggle('active', status !== 'done' && i === idx);
});
}
async function refresh(id) {
let st;
try { st = await (await fetch(`/api/jobs/${id}`)).json(); } catch { return; }
$('bar-fill').style.width = (st.progress || 0) + '%';
$('progress-msg').textContent = st.message || st.status || '';
$('progress-msg').classList.toggle('error', st.status === 'error');
updateStages(st.stage, st.status);
try {
const log = await (await fetch(`/api/jobs/${id}/log`)).text();
const el = $('log'); el.textContent = log; el.scrollTop = el.scrollHeight;
} catch { /* no log yet */ }
if (st.status === 'done') {
clearInterval(poll); poll = null;
openViewer(`/api/jobs/${id}/scene.glb`);
} else if (st.status === 'error') {
clearInterval(poll); poll = null;
$('progress-msg').textContent = '⚠ ' + (st.error || 'błąd rekonstrukcji');
}
}
async function upload(file) {
if (!file) return;
buildStages();
$('bar-fill').style.width = '0'; $('progress-msg').textContent = 'Wysyłanie filmu…'; $('log').textContent = '';
show('view-progress');
const fd = new FormData(); fd.append('video', file);
let res;
try { res = await (await fetch('/api/jobs', { method: 'POST', body: fd })).json(); }
catch (e) { $('progress-msg').classList.add('error'); $('progress-msg').textContent = 'Nie udało się wysłać: ' + e; return; }
if (res.error) { $('progress-msg').classList.add('error'); $('progress-msg').textContent = res.error; return; }
poll = setInterval(() => refresh(res.id), 1500);
refresh(res.id);
}
function openViewer(url) {
show('view-viewer');
document.body.classList.toggle('touch', matchMedia('(pointer: coarse)').matches);
startViewer(url).catch((e) => {
show('view-upload');
alert('Nie udało się załadować sceny 3D: ' + e);
});
}
function backToUpload() {
if (poll) { clearInterval(poll); poll = null; }
stopViewer();
show('view-upload');
}
// --- mobile joystick ---
function wireJoystick() {
const stick = $('joystick'), knob = $('joystick-knob');
let active = null, cx = 0, cy = 0, R = 45;
const set = (dx, dy) => {
const len = Math.hypot(dx, dy) || 1, cl = Math.min(len, R);
dx = dx / len * cl; dy = dy / len * cl;
knob.style.transform = `translate(${dx}px,${dy}px)`;
setJoystick(dx / R, -dy / R); // up = forward
};
stick.addEventListener('pointerdown', (e) => {
active = e.pointerId; const r = stick.getBoundingClientRect();
cx = r.left + r.width / 2; cy = r.top + r.height / 2;
stick.setPointerCapture(e.pointerId); set(e.clientX - cx, e.clientY - cy);
});
stick.addEventListener('pointermove', (e) => { if (e.pointerId === active) set(e.clientX - cx, e.clientY - cy); });
const end = (e) => { if (e.pointerId === active) { active = null; knob.style.transform = ''; setJoystick(0, 0); } };
stick.addEventListener('pointerup', end);
stick.addEventListener('pointercancel', end);
}
function init() {
const drop = $('drop'), input = $('file-input');
input.addEventListener('change', () => upload(input.files[0]));
['dragenter', 'dragover'].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.add('drag'); }));
['dragleave', 'drop'].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.remove('drag'); }));
drop.addEventListener('drop', (e) => upload(e.dataTransfer.files[0]));
$('btn-demo').addEventListener('click', () => openViewer('./demo.glb'));
$('btn-cancel').addEventListener('click', backToUpload);
$('btn-new').addEventListener('click', backToUpload);
$('btn-recenter').addEventListener('click', recenter);
wireJoystick();
if ('serviceWorker' in navigator) navigator.serviceWorker.register('./sw.js').catch(() => {});
// Deep-link: /?job= resumes (or opens) a specific reconstruction, so a
// finished scene has a shareable URL and progress survives a reload.
const jobId = new URLSearchParams(location.search).get('job');
if (jobId) {
buildStages();
show('view-progress');
poll = setInterval(() => refresh(jobId), 1500);
refresh(jobId);
}
}
init();