// ════════════════════════════════════════════ // REPLACE the existing make3D() function with this one. // Also replace pollCloud() with the updated version below. // ════════════════════════════════════════════ async function make3D() { if (!conceptUrl && !conceptB64) { toast('No concept image'); return; } let btn = G('btnMake3D'); btn.disabled = true; btn.textContent = '⏳ Submitting to Forge...'; showStatus('Sending to ' + (ENGINE_NAMES[curEngine]||curEngine) + '...'); let fd = new FormData(); if (curEngine === 'trellis2') fd.append('action', 'generate_trellis'); else if (curEngine === 'hunyuan3d') fd.append('action', 'generate_hunyuan'); else if (curEngine === 'tripo') { fd.append('action', 'generate_tripo'); fd.append('mode', 'image'); } else if (curEngine === 'meshy') { fd.append('action', 'generate_meshy'); fd.append('mode', 'image'); } if (conceptB64) { let resp = await fetch(conceptB64); let blob = await resp.blob(); fd.append('image', blob, 'concept.png'); } else if (conceptUrl) { fd.append('image_url', conceptUrl); } fd.append('prompt', conceptPrompt || ''); fd.append('poly_count', G('polySlider').value); fd.append('texture_size', G('texSize').value); try { let resp = await fetch('3d-api.php', {method:'POST', body: fd}); let text = await resp.text(); let data; try { data = JSON.parse(text); } catch(e) { console.error('3d-api.php returned non-JSON:', text.substring(0, 500)); hideStatus(); btn.disabled=false; btn.textContent='✅ Approve & Generate 3D Model'; toast('❌ Server error — check console'); return; } // ── LOCAL ENGINES: got job_id → poll ── if (data.success && data.job_id) { toast('✅ Job submitted! Rendering on Mac...'); btn.textContent = '⏳ Rendering 3D...'; pollLocalJob(data.job_id, data.prompt || conceptPrompt, btn); return; } // ── CLOUD ENGINES: got task_id → poll cloud ── if (data.success && data.task_id) { toast('⏳ Cloud generation started — polling...'); btn.disabled = false; btn.textContent = '✅ Approve & Generate 3D Model'; pollCloud(data.engine, data.task_id); backToStep1(); return; } // ── DIRECT RESULT (shouldn't happen now but fallback) ── if (data.success) { hideStatus(); btn.disabled = false; btn.textContent = '✅ Approve & Generate 3D Model'; toast('✅ 3D model generated!'); let url = data.local_path || data.download_url; if (url) loadModel(url, data); switchTab('viewer'); backToStep1(); return; } // ── ERROR ── hideStatus(); btn.disabled = false; btn.textContent = '✅ Approve & Generate 3D Model'; toast('❌ ' + (data.error || 'Generation failed')); console.error('3D gen error:', data); } catch(e) { hideStatus(); btn.disabled=false; btn.textContent='✅ Approve & Generate 3D Model'; toast('❌ Connection failed — is the Forge engine online?'); console.error('Fetch error:', e); } } // ════════════════════════════════════════════ // POLL LOCAL JOB (TRELLIS / Hunyuan on Mac) // Polls every 5s for up to 30 min // ════════════════════════════════════════════ async function pollLocalJob(jobId, prompt, btn) { showStatus('Rendering 3D model on Mac... (this takes 3-8 min)'); for (let i = 0; i < 360; i++) { // 360 × 5s = 30 min max await new Promise(r => setTimeout(r, 5000)); try { let url = '3d-api.php?action=check_local_job&job_id=' + encodeURIComponent(jobId); if (prompt) url += '&prompt=' + encodeURIComponent(prompt); let resp = await fetch(url); let data = await resp.json(); if (data.status === 'complete' && data.result) { hideStatus(); btn.disabled = false; btn.textContent = '✅ Approve & Generate 3D Model'; toast('✅ 3D model generated!'); let dlUrl = data.result.download_url || data.result.local_path || ''; if (dlUrl) loadModel(dlUrl, data.result); switchTab('viewer'); backToStep1(); return; } if (data.status === 'failed') { hideStatus(); btn.disabled = false; btn.textContent = '✅ Approve & Generate 3D Model'; toast('❌ ' + (data.error || 'Generation failed')); return; } // Still processing — update timer let elapsed = (i + 1) * 5; let mins = Math.floor(elapsed / 60); let secs = elapsed % 60; G('statusText').textContent = 'Rendering 3D model... ' + mins + 'm ' + secs + 's'; } catch(e) { // Network glitch — keep polling, don't give up console.warn('Poll error (will retry):', e.message); } } hideStatus(); btn.disabled = false; btn.textContent = '✅ Approve & Generate 3D Model'; toast('⏰ Timed out after 30 min — check Gallery later'); } // ════════════════════════════════════════════ // POLL CLOUD (Meshy / Tripo — unchanged logic) // ════════════════════════════════════════════ async function pollCloud(engine, taskId) { showStatus('Cloud processing...'); let checkAction = engine === 'meshy' ? 'check_meshy' : 'check_tripo'; for (let i = 0; i < 60; i++) { await new Promise(r => setTimeout(r, 5000)); try { let resp = await fetch('3d-api.php?action=' + checkAction + '&task_id=' + taskId); let data = await resp.json(); let status = engine === 'meshy' ? (data.data?.status||'') : (data.data?.data?.status||''); if (status === 'SUCCEEDED' || status === 'success') { hideStatus(); let url = engine === 'meshy' ? (data.data?.model_urls?.glb||'') : (data.data?.data?.model?.glb?.url||''); if (url) { loadModel(url, {engine}); switchTab('viewer'); } toast('✅ Done!'); return; } else if (status === 'FAILED' || status === 'failed') { hideStatus(); toast('❌ Cloud generation failed'); return; } G('statusText').textContent = 'Cloud processing... (' + (i*5) + 's)'; } catch(e) {} } hideStatus(); toast('⏰ Timed out — check Gallery later'); }