(function () { let busy = false; let dojoMode = 'splat'; let lastScene = null; let dojoScope = 'mine'; let dojoSearch = ''; // library search query (matches dojo name) let improvedByAya = false; // current prompt came from tiny-aya → button reads "reroll" let improving = false; // a tiny-aya suggestion call is in flight let _communityCache = null; // canonical + contributed dojos fetched from the dataset let _lastDojo = null; // { scene, cfg } of the just-generated dojo (for "Save to dataset") // Community starters — tapping one opens the create view prefilled, so generating // produces a real flux + splat scene (which then lands under "Mine"). const COMMUNITY_SEED = [ { name: 'Classic dojo', mode: 'splat', settings: { room_size: 6.5, room_height: 3.2 }, prompt: 'A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.' }, { name: 'Mountain dojo', mode: 'splat', settings: { room_size: 7.5, room_height: 4 }, prompt: 'An outdoor mountain dojo courtyard at sunrise, stone tiles, wooden gates, misty trees, full environment visible, high detail, no people, no text.' }, { name: 'Neon dojo', mode: 'splat', settings: { room_size: 6.5, room_height: 3.2 }, prompt: 'A futuristic neon martial arts training room, glossy floor, holographic wall panels, dramatic rim light, full room visible, no people, no text.' } ]; function dojoEls() { return { library: document.getElementById('kimodo-dojo-library'), create: document.getElementById('kimodo-dojo-create'), scope: document.getElementById('kimodo-dojo-scope'), add: document.getElementById('kimodo-dojo-add'), search: document.getElementById('kimodo-dojo-search'), back: document.getElementById('kimodo-dojo-back'), grid: document.getElementById('kimodo-dojo-grid'), emptyMsg: document.getElementById('kimodo-dojo-empty'), improve: document.getElementById('kimodo-dojo-improve'), save: document.getElementById('kimodo-dojo-save'), erase: document.getElementById('kimodo-dojo-erase'), prompt: document.getElementById('kimodo-dojo-prompt'), mode: document.getElementById('kimodo-dojo-mode'), roomSize: document.getElementById('kimodo-dojo-room-size'), roomSizeValue: document.getElementById('kimodo-dojo-room-size-value'), roomHeight: document.getElementById('kimodo-dojo-room-height'), roomHeightValue: document.getElementById('kimodo-dojo-room-height-value'), skybox: document.getElementById('kimodo-dojo-skybox'), seed: document.getElementById('kimodo-dojo-seed'), steps: document.getElementById('kimodo-dojo-steps'), generate: document.getElementById('kimodo-dojo-generate'), clear: document.getElementById('kimodo-dojo-clear'), status: document.getElementById('kimodo-dojo-status'), stage: document.getElementById('kimodo-dojo-stage'), iso: document.getElementById('kimodo-dojo-iso'), isoImg: document.getElementById('kimodo-dojo-iso-img') }; } function setStatus(text) { const els = dojoEls(); if (els.status) els.status.textContent = text; } function formatSeconds(value) { const seconds = Math.max(0, Math.round(Number(value) || 0)); const minutes = Math.floor(seconds / 60); const rest = seconds % 60; if (minutes <= 0) return rest + 's'; return minutes + 'm ' + String(rest).padStart(2, '0') + 's'; } function stageLabel(stage) { if (/^flux/.test(stage || '')) return 'Flux'; if (/^splat/.test(stage || '')) return 'TripoSplat'; if (/^panorama/.test(stage || '')) return 'Panorama'; if (/^viewer/.test(stage || '')) return 'Viewer'; return 'Dojo'; } function bestProgressText(stage) { if (!stage) return ''; const progress = Array.isArray(stage.progress) ? stage.progress : []; for (let i = progress.length - 1; i >= 0; i -= 1) { if (!progress[i]) continue; const item = progress[i]; const bits = []; if (item.desc) bits.push(item.desc); if (Number.isFinite(Number(item.progress))) bits.push(Math.round(Number(item.progress) * 100) + '%'); if (item.index != null && item.length != null) bits.push(item.index + '/' + item.length + (item.unit ? ' ' + item.unit : '')); if (bits.length) return bits.join(' - '); } if (stage.progress_desc) return stage.progress_desc; return ''; } function progressFraction(stage) { const progress = Array.isArray(stage && stage.progress) ? stage.progress : []; for (let i = progress.length - 1; i >= 0; i -= 1) { const item = progress[i] || {}; const direct = Number(item.progress); if (Number.isFinite(direct) && direct > 0 && direct <= 1) return direct; const index = Number(item.index); const length = Number(item.length); if (Number.isFinite(index) && Number.isFinite(length) && index > 0 && length > 0) return Math.min(1, index / length); } return null; } function setTiming(stage) { const els = dojoEls(); if (!els.time || !stage) return; const parts = []; const fraction = progressFraction(stage); const estimatedRemaining = stage.eta == null && fraction && stage.elapsed ? (Number(stage.elapsed) * (1 - fraction)) / fraction : null; if (stage.elapsed != null) parts.push('elapsed ' + formatSeconds(stage.elapsed)); if (stage.eta != null) parts.push('remaining ' + formatSeconds(stage.eta)); else if (estimatedRemaining != null) parts.push('remaining ~' + formatSeconds(estimatedRemaining)); if (stage.rank != null && stage.rank > 0) parts.push('queue #' + stage.rank); if (stage.queue_size != null && stage.queue_size > 0) parts.push(stage.queue_size + ' waiting'); if (stage.job_status) parts.push(String(stage.job_status).toLowerCase()); els.time.textContent = parts.length ? stageLabel(stage.stage) + ': ' + parts.join(' - ') : ''; if (els.detail) els.detail.textContent = bestProgressText(stage); } function hiddenInput(id) { return inputInside(id); } function writeHidden(id, value) { const input = hiddenInput(id); if (!input) return false; input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return true; } function readHidden(id) { const input = hiddenInput(id); return input ? input.value || '' : ''; } function roomSettings() { const els = dojoEls(); return { room_size: Math.max(1, Number(els.roomSize && els.roomSize.value) || 6.5), room_height: Math.max(1, Number(els.roomHeight && els.roomHeight.value) || 3.2), use_panorama_skybox: !!(els.skybox && els.skybox.checked) }; } function sendScene(scene) { const frame = previewFrame(); if (!scene) return; const payload = { ...scene, ...roomSettings(), render_mode: dojoMode }; payload.use_panorama_skybox = !!(dojoEls().skybox && dojoEls().skybox.checked); if (!payload.image && payload.panorama_image) payload.image = payload.panorama_image; if (payload.render_mode === 'panorama' && !payload.image && payload.source_image) payload.image = payload.source_image; if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-scene', scene: payload, image: payload.image }, '*'); } var CHAR_HEIGHT_M = 1.7; // character ≈ SMPLX_HEIGHT — room size is shown relative to it function updateRoomLabels() { const els = dojoEls(); const settings = roomSettings(); if (els.roomSizeValue) els.roomSizeValue.textContent = settings.room_size.toFixed(1); if (els.roomHeightValue) els.roomHeightValue.textContent = settings.room_height.toFixed(1); const ratio = document.getElementById('kimodo-dojo-room-ratio'); if (ratio) ratio.textContent = (settings.room_size / CHAR_HEIGHT_M).toFixed(1); } function sendRoomSettings() { const frame = previewFrame(); const settings = roomSettings(); if (lastScene) Object.assign(lastScene, settings); if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-room-settings', settings }, '*'); } // ---- Generation stage: iso image loads in, then a 3D-conversion scan ---- function stageReset() { const els = dojoEls(); if (els.stage) els.stage.hidden = true; if (els.iso) els.iso.classList.remove('generating', 'has-img', 'converting'); if (els.isoImg) els.isoImg.removeAttribute('src'); if (els.save) els.save.hidden = true; _lastDojo = null; } function stageGenerating() { // iso not ready yet → shimmer placeholder const els = dojoEls(); if (els.stage) els.stage.hidden = false; if (els.iso) { els.iso.classList.add('generating'); els.iso.classList.remove('has-img', 'converting'); } } function stageIsoReady(src) { // iso image arrived → fade/deblur it in const els = dojoEls(); if (!src || !els.iso || !els.isoImg) return; if (els.stage) els.stage.hidden = false; els.iso.classList.remove('generating'); if (els.isoImg.getAttribute('src') !== src) { els.isoImg.onload = () => els.iso.classList.add('has-img'); els.isoImg.src = src; } if (els.isoImg.complete && els.isoImg.naturalWidth) els.iso.classList.add('has-img'); } function stageConverting(on) { // 3D-conversion scan overlay const els = dojoEls(); if (els.iso) els.iso.classList.toggle('converting', !!on); } function setBusy(next) { busy = !!next; const els = dojoEls(); if (els.generate) { els.generate.disabled = busy; els.generate.innerHTML = busy ? ' Generating…' : 'Generate Scene'; } if (els.improve) { els.improve.disabled = busy || improving; if (busy) { els.improve.classList.add('loading'); els.improve.innerHTML = ' generating…'; } else { els.improve.classList.remove('loading'); updateImproveBtn(); } } } function waitForResult(previousValue, onStage) { return new Promise((resolve, reject) => { const started = Date.now(); let lastValue = previousValue || ''; const timer = window.setInterval(() => { const value = readHidden('dojo-result'); if (value && value !== lastValue) { lastValue = value; try { const msg = JSON.parse(value); if (onStage) onStage(msg); if (msg.done || msg.source === 'dit360' || msg.source === 'default' || msg.source === 'none' || msg.error) { window.clearInterval(timer); resolve(value); } } catch (e) { window.clearInterval(timer); reject(e); } } if (Date.now() - started > 600000) { window.clearInterval(timer); reject(new Error('Scene generation timed out.')); } }, 300); }); } function currentConfig() { const els = dojoEls(); return { prompt: (els.prompt && els.prompt.value || '').trim(), seed: Number(els.seed && els.seed.value), steps: Number(els.steps && els.steps.value), mode: dojoMode, ...roomSettings() }; } // ---------- Saved dojos (localStorage) ---------- function loadMyDojos() { try { return JSON.parse(localStorage.getItem('kimodoMyDojos') || '[]'); } catch (e) { return []; } } function saveMyDojos(list) { let arr = (list || []).slice(0, 16); // Scene payloads carry data-URL splats (can be MBs); drop the oldest until it fits. while (arr.length) { try { localStorage.setItem('kimodoMyDojos', JSON.stringify(arr)); return true; } catch (e) { arr = arr.slice(0, arr.length - 1); } } try { localStorage.setItem('kimodoMyDojos', '[]'); } catch (e) {} return false; } function dojoName(prompt) { const head = (prompt || '').replace(/[.,].*$/, '').trim().replace(/^(a|an|the)\s+/i, ''); const name = head.split(/\s+/).slice(0, 4).join(' '); return (name || 'Dojo').slice(0, 30); } // After a dojo is saved to the dataset, add a LIGHT (URL-backed) entry to My dojos — // no heavy data-URL splat in localStorage, durable across reloads. function addMineFromSaved(entry) { const iso = communityUrl(entry.iso); const mineEntry = { id: entry.id, name: entry.name, prompt: entry.prompt, mode: 'splat', settings: entry.settings, thumb: iso, contributor: entry.contributor, saved: true, ts: Date.now(), scene: { ok: true, source: 'klein-triposplat', render_mode: 'splat', image: iso, isometric_image: iso, source_image: iso, splat: communityUrl(entry.splat), room_size: entry.settings && entry.settings.room_size, room_height: entry.settings && entry.settings.room_height } }; const arr = loadMyDojos().filter((x) => x.id !== entry.id); arr.unshift(mineEntry); saveMyDojos(arr); } function dojoAnonId() { const a = inputInside('anonymous-client-id'); return a ? (a.value || '') : ''; } // Publish the just-generated dojo to the dataset (tagged as the contributor) and // add it to My dojos. function saveDojoToDataset() { if (!_lastDojo || !_lastDojo.scene) { setStatus('Generate a dojo first.'); return; } const sc = _lastDojo.scene, cfg = _lastDojo.cfg || {}; const iso = sc.isometric_image || sc.source_image, splat = sc.splat; if (!iso || !splat) { setStatus('Nothing to save (no splat).'); return; } const els = dojoEls(); const trigger = document.querySelector('#dojo-save-btn button, #dojo-save-btn'); if (!trigger || !hiddenInput('dojo-save-payload')) { setStatus('Save backend missing.'); return; } if (els.save) { els.save.disabled = true; els.save.innerHTML = ' saving…'; } setStatus('Saving to the dataset…'); writeHidden('dojo-save-result', ''); writeHidden('dojo-save-payload', JSON.stringify({ name: dojoName(cfg.prompt || (els.prompt && els.prompt.value) || 'Dojo'), prompt: cfg.prompt || '', settings: { room_size: cfg.room_size, room_height: cfg.room_height }, iso: iso, splat: splat, anon_id: dojoAnonId() })); const started = Date.now(); const timer = window.setInterval(() => { const val = readHidden('dojo-save-result'); if (val) { window.clearInterval(timer); finish(val); } else if (Date.now() - started > 150000) { window.clearInterval(timer); finish('{"ok":false,"error":"timed out"}'); } }, 400); window.setTimeout(() => trigger.click(), 40); function finish(val) { if (els.save) { els.save.disabled = false; els.save.innerHTML = '☁ Save to my dojos'; } try { const r = JSON.parse(val); if (r.ok && r.entry) { addMineFromSaved(r.entry); _communityCache = null; // contributed dojo now shows in Community if (els.save) els.save.hidden = true; setStatus('Saved to My dojos ✓ — shared to Community as ' + (r.entry.contributor || 'you') + '.'); } else { setStatus('Save failed: ' + (r.error || 'unknown')); } } catch (e) { setStatus('Save failed.'); } } } // ---------- View + library ---------- function showView(view) { const els = dojoEls(); if (els.library) els.library.hidden = (view !== 'library'); if (els.create) els.create.hidden = (view !== 'create'); if (view === 'library') renderLibrary(); } function syncModeButtons() { const els = dojoEls(); if (!els.mode) return; els.mode.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.mode === dojoMode)); } function applySettings(settings) { const els = dojoEls(); if (!settings) return; if (els.roomSize && settings.room_size) els.roomSize.value = settings.room_size; if (els.roomHeight && settings.room_height) els.roomHeight.value = settings.room_height; if (els.skybox) els.skybox.checked = !!settings.use_panorama_skybox; if (els.seed && settings.seed != null) els.seed.value = settings.seed; if (els.steps && settings.steps != null) els.steps.value = settings.steps; updateRoomLabels(); } function openCreate(seed) { showView('create'); stageReset(); const els = dojoEls(); improvedByAya = false; if (seed) { if (els.prompt) { els.prompt.value = seed.prompt || ''; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); } dojoMode = seed.mode || 'splat'; syncModeButtons(); applySettings(seed.settings); setStatus('Loaded "' + (seed.name || 'starter') + '". Tweak and Generate.'); } else { if (els.prompt) { els.prompt.value = ''; } // blank by default — placeholder "a random dojo" setStatus('Describe a dojo, or let tiny-aya decide 🎲'); } updateImproveBtn(); } function mineCard(entry) { const card = document.createElement('div'); card.className = 'kimodo-dojo-card'; card.title = entry.name || 'dojo'; if (entry.thumb) { const img = document.createElement('img'); img.className = 'thumb'; img.src = entry.thumb; img.alt = entry.name || 'dojo'; card.appendChild(img); } else { const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '🏯'; card.appendChild(ph); } const del = document.createElement('button'); del.className = 'del'; del.type = 'button'; del.textContent = '✕'; del.title = 'Delete'; del.onclick = (e) => { e.stopPropagation(); if (!window.confirm('Delete this dojo?')) return; saveMyDojos(loadMyDojos().filter((x) => x.id !== entry.id)); renderLibrary(); }; card.appendChild(del); const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = entry.name || 'Dojo'; card.appendChild(nm); card.onclick = () => applySavedDojo(entry); return card; } function communityCard(seed) { const card = document.createElement('div'); card.className = 'kimodo-dojo-card'; card.title = seed.name; const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '🏯'; card.appendChild(ph); const tag = document.createElement('span'); tag.className = 'badge'; tag.textContent = 'STARTER'; card.appendChild(tag); const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = seed.name; card.appendChild(nm); card.onclick = () => openCreate(seed); return card; } // Canonical community dojos are hosted in the dataset (viewer/dojos/community.json), // each with a real flux iso thumbnail + a splat that applies instantly. function communityUrl(path) { return (typeof DATASET_BASE !== 'undefined' && DATASET_BASE ? DATASET_BASE : '') + '/' + path; } function loadCommunity() { if (_communityCache) return Promise.resolve(_communityCache); const grab = (f) => fetch(communityUrl(f), { cache: 'no-store' }).then((r) => (r.ok ? r.json() : null)).catch(() => null); return Promise.all([grab('viewer/dojos/community.json'), grab('viewer/dojos/contributed.json')]).then(([canon, contrib]) => { const list = [].concat(Array.isArray(canon) ? canon : [], Array.isArray(contrib) ? contrib : []); if (list.length) { _communityCache = list; return list; } return null; }); } function communityRealCard(d) { const card = document.createElement('div'); card.className = 'kimodo-dojo-card'; card.title = d.name + (d.contributor ? ' · by ' + d.contributor : ''); const img = document.createElement('img'); img.className = 'thumb'; img.src = communityUrl(d.iso); img.alt = d.name; card.appendChild(img); const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = d.name; card.appendChild(nm); if (d.contributor) { const by = document.createElement('div'); by.className = 'by'; by.textContent = 'by ' + d.contributor; card.appendChild(by); } card.onclick = () => applyCommunityDojo(d); return card; } function applyCommunityDojo(d) { const iso = communityUrl(d.iso); const scene = { ok: true, source: 'klein-triposplat', render_mode: 'splat', image: iso, isometric_image: iso, source_image: iso, splat: communityUrl(d.splat), room_size: d.settings && d.settings.room_size, room_height: d.settings && d.settings.room_height }; dojoMode = 'splat'; syncModeButtons(); applySettings(d.settings); lastScene = scene; sendScene(scene); setStatus('Applying "' + d.name + '"…'); } // Clear the applied dojo scene → back to the empty grid floor. function clearDojo() { const frame = previewFrame(); if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-clear' }, '*'); lastScene = null; setStatus('Dojo cleared — back to the grid floor.'); } function clearCard() { const card = document.createElement('div'); card.className = 'kimodo-dojo-card kimodo-dojo-clearcard'; card.title = 'Clear the dojo — back to the grid floor'; const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '▦'; card.appendChild(ph); const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = 'No dojo'; card.appendChild(nm); card.onclick = clearDojo; return card; } // Anchored header (mirrors the kata library): Add button is Mine-only, search is // Community-only. Match a dojo by name against the search query. function dojoMatches(name) { const q = (dojoSearch || '').trim().toLowerCase(); return !q || String(name || '').toLowerCase().includes(q); } function renderLibrary() { const els = dojoEls(); if (els.scope) els.scope.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.scope === dojoScope)); if (els.add) els.add.style.display = (dojoScope === 'mine') ? '' : 'none'; if (els.search) els.search.style.display = (dojoScope === 'community') ? '' : 'none'; if (!els.grid) return; els.grid.innerHTML = ''; if (dojoScope === 'mine') { els.grid.appendChild(clearCard()); // first card: clear + back to the grid floor const arr = loadMyDojos().filter((e) => dojoMatches(e.name)); arr.forEach((e) => els.grid.appendChild(mineCard(e))); if (els.emptyMsg) els.emptyMsg.textContent = arr.length ? '' : 'No dojos yet — tap + Add dojo to create one.'; return; } // Community: canonical hosted dojos (instant apply), else the tap-to-generate starters. if (els.emptyMsg) els.emptyMsg.textContent = 'Loading community dojos…'; loadCommunity().then((list) => { if (dojoScope !== 'community' || !els.grid) return; els.grid.innerHTML = ''; els.grid.appendChild(clearCard()); const real = (list || []).filter((d) => dojoMatches(d.name)); if (list && list.length) real.forEach((d) => els.grid.appendChild(communityRealCard(d))); else COMMUNITY_SEED.filter((s) => dojoMatches(s.name)).forEach((s) => els.grid.appendChild(communityCard(s))); if (els.emptyMsg) els.emptyMsg.textContent = (list && list.length && !real.length) ? 'No dojos match.' : ''; }); } function applySavedDojo(entry) { const scene = entry.scene; if (!scene) { openCreate(entry); return; } // recipe-only fallback → re-generate dojoMode = scene.render_mode || (scene.splat || scene.splat_url ? 'splat' : 'panorama'); syncModeButtons(); applySettings(entry.settings); lastScene = scene; sendScene(scene); setStatus('Applied "' + (entry.name || 'dojo') + '".'); } // ---------- tiny-aya prompt suggestion ---------- // Improve-button label tracks state: blank prompt → "let tiny-aya decide", // tiny-aya already wrote one → "reroll", otherwise → "improve". function updateImproveBtn() { const els = dojoEls(); if (!els.improve || improving) return; const has = !!(els.prompt && els.prompt.value.trim()); els.improve.textContent = improvedByAya ? 'Reroll with tiny-aya 🎲' : (has ? '✦ Improve with tiny-aya' : 'let tiny-aya decide 🎲'); } function improvePrompt() { const els = dojoEls(); const payloadInput = hiddenInput('dojo-suggest-payload'); const trigger = document.querySelector('#dojo-suggest-btn button, #dojo-suggest-btn'); if (!payloadInput || !trigger) { setStatus('Suggest backend missing.'); return; } const words = (els.prompt && els.prompt.value || '').trim(); improving = true; if (els.improve) { els.improve.disabled = true; els.improve.textContent = '🎲 thinking…'; } setStatus(words ? 'Rerolling your prompt with tiny-aya…' : 'Letting tiny-aya invent a dojo…'); writeHidden('dojo-suggest-result', ''); writeHidden('dojo-suggest-payload', JSON.stringify({ words: words })); const started = Date.now(); const timer = window.setInterval(() => { const val = readHidden('dojo-suggest-result'); if (val) { window.clearInterval(timer); finishImprove(val); } else if (Date.now() - started > 150000) { window.clearInterval(timer); finishImprove('{"ok":false,"error":"timed out"}'); } }, 300); window.setTimeout(() => trigger.click(), 40); function finishImprove(val) { improving = false; if (els.improve) els.improve.disabled = busy; try { const r = JSON.parse(val); if (r.ok && r.prompt) { if (els.prompt) { els.prompt.value = r.prompt; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); } improvedByAya = true; // set AFTER the input event (which resets it) so the button reads "reroll" setStatus('Prompt ready ✦ — tweak it or tap Generate.'); } else { setStatus('tiny-aya failed: ' + (r.error || 'unknown')); } } catch (e) { setStatus('tiny-aya failed.'); } updateImproveBtn(); } } // ---------- Generate ---------- async function runGenerate() { if (busy) return; const cfg = currentConfig(); if (!cfg.prompt) { setStatus('Describe a dojo first, or tap “let tiny-aya decide 🎲”.'); return; } const payloadInput = hiddenInput('dojo-payload'); const trigger = document.querySelector('#dojo-btn button, #dojo-btn'); if (!payloadInput || !trigger) { setStatus('Dojo backend controls are missing.'); return; } setBusy(true); stageReset(); stageGenerating(); setStatus('Generating the dojo image…'); writeHidden('dojo-result', ''); writeHidden('dojo-payload', JSON.stringify(cfg)); try { trigger.click(); const value = await waitForResult('', (stage) => { const st = stage.stage || ''; const iso = stage.isometric_image || stage.source_image; if (st === 'flux-iso-start' || st === 'flux-iso-progress') { stageGenerating(); setStatus(stage.status || 'Generating the dojo image…'); } else if (st === 'flux-iso-done') { stageIsoReady(iso); setStatus(stage.status || 'Image ready — converting to 3D…'); } else if (st === 'splat-start' || st === 'splat-progress') { if (iso) stageIsoReady(iso); stageConverting(true); setStatus(bestProgressText(stage) || stage.status || 'Converting to 3D…'); } else if (st === 'splat-done') { stageConverting(false); setStatus(stage.status || 'Loading the scene into the viewer…'); } else if (st === 'fallback') { stageConverting(false); setStatus(stage.warning || stage.error || 'Fallback scene generated.'); } }); const scene = JSON.parse(value); if (scene && (scene.image || scene.splat || scene.splat_url)) { Object.assign(scene, roomSettings()); lastScene = scene; sendScene(scene); // A real splat scene can be saved to the dataset + My dojos (tap the Save button). if (scene.source === 'klein-triposplat' && scene.splat) { _lastDojo = { scene: scene, cfg: cfg }; const saveBtn = dojoEls().save; if (saveBtn) saveBtn.hidden = false; setStatus('Generated ✓ — tap “☁ Save to my dojos” to keep it.'); } else if (scene.source === 'klein-triposplat' || scene.source === 'dit360') { setStatus(scene.info || 'Scene applied.'); } else { setStatus((scene.warning || 'Using default scene.') + ' Default scene applied.'); } } else { setStatus((scene && scene.error) || 'Scene generation failed.'); } } catch (e) { console.error('dojo scene generation failed', e); setStatus('Scene generation failed: ' + e.message); } finally { setBusy(false); } } function initDojo() { const drawer = document.getElementById('kimodo-dojo-drawer'); const els = dojoEls(); if (!drawer || drawer.dataset.ready === '1') { renderLibrary(); return; } drawer.dataset.ready = '1'; updateRoomLabels(); showView('library'); if (els.scope) els.scope.addEventListener('click', (e) => { const b = e.target.closest('.kimodo-cz-segbtn[data-scope]'); if (!b) return; dojoScope = b.dataset.scope || 'mine'; renderLibrary(); }); if (els.add) els.add.addEventListener('click', () => openCreate(null)); if (els.search) els.search.addEventListener('input', () => { dojoSearch = els.search.value || ''; renderLibrary(); }); if (els.back) els.back.addEventListener('click', () => showView('library')); if (els.improve) els.improve.addEventListener('click', () => { if (!busy && !improving) improvePrompt(); }); if (els.save) els.save.addEventListener('click', () => { if (!els.save.disabled) saveDojoToDataset(); }); if (els.erase) els.erase.addEventListener('click', () => { if (els.prompt) { els.prompt.value = ''; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); els.prompt.focus(); } setStatus('Prompt cleared — let tiny-aya decide 🎲'); }); // Manual edits drop the "reroll" state (a fresh prompt → "improve"/"decide"). if (els.prompt) els.prompt.addEventListener('input', () => { if (improving) return; improvedByAya = false; updateImproveBtn(); }); if (els.mode) els.mode.addEventListener('click', (e) => { const button = e.target.closest('.kimodo-cz-segbtn[data-mode]'); if (!button || busy) return; dojoMode = button.dataset.mode || 'splat'; syncModeButtons(); setStatus(dojoMode === 'panorama' ? 'Panorama mode selected.' : 'Splat mode selected.'); if (lastScene) sendScene(lastScene); }); ['roomSize', 'roomHeight', 'skybox'].forEach((key) => { const input = els[key]; if (!input) return; input.addEventListener('input', () => { updateRoomLabels(); sendRoomSettings(); }); input.addEventListener('change', () => { updateRoomLabels(); if (lastScene) sendScene(lastScene); }); }); if (els.generate) els.generate.addEventListener('click', runGenerate); } window.addEventListener('kimodo-drawer-open', (e) => { if (e.detail === 'kimodo-dojo-drawer') initDojo(); }); window.addEventListener('message', (event) => { const msg = event.data || {}; if (msg.kind === 'dojo-status' && msg.text) { if (/loaded in viewer/i.test(msg.text)) stageConverting(false); setStatus(msg.text); } }); })();