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();