// JavaScript logic for FormaAI TRELLIS 3D Web UI document.addEventListener("DOMContentLoaded", () => { // DOM Elements const uploadZone = document.getElementById("uploadZone"); const fileInput = document.getElementById("fileInput"); const uploadContent = document.getElementById("uploadContent"); const previewContainer = document.getElementById("previewContainer"); const imagePreview = document.getElementById("imagePreview"); const btnResetImage = document.getElementById("btnResetImage"); const generationForm = document.getElementById("generationForm"); const sessionIdInput = document.getElementById("sessionId"); const seedInput = document.getElementById("seed"); const randomizeSeedCheckbox = document.getElementById("randomizeSeed"); const btnGenerate = document.getElementById("btnGenerate"); const tabPreview = document.getElementById("tabPreview"); const tab3dViewer = document.getElementById("tab3dViewer"); const contentPreview = document.getElementById("contentPreview"); const content3dViewer = document.getElementById("content3dViewer"); const videoPlaceholder = document.getElementById("videoPlaceholder"); const videoContainer = document.getElementById("videoContainer"); const previewVideo = document.getElementById("previewVideo"); const glbViewer = document.getElementById("glbViewer"); const exportControls = document.getElementById("exportControls"); const btnExtractGlb = document.getElementById("btnExtractGlb"); const linkDownloadGlb = document.getElementById("linkDownloadGlb"); const linkDownloadPly = document.getElementById("linkDownloadPly"); const historyGallery = document.getElementById("historyGallery"); const galleryEmpty = document.getElementById("galleryEmpty"); const btnRefreshHistory = document.getElementById("btnRefreshHistory"); // Accordions const accordions = document.querySelectorAll(".accordion-title"); accordions.forEach(acc => { acc.addEventListener("click", () => { acc.parentElement.classList.toggle("open"); }); }); // Seed Randomize behavior randomizeSeedCheckbox.addEventListener("change", (e) => { seedInput.disabled = e.target.checked; }); // Tabs switching tabPreview.addEventListener("click", () => { tabPreview.classList.add("active"); tab3dViewer.classList.remove("active"); contentPreview.style.display = "block"; content3dViewer.style.display = "none"; }); tab3dViewer.addEventListener("click", () => { tab3dViewer.classList.add("active"); tabPreview.classList.remove("active"); content3dViewer.style.display = "block"; contentPreview.style.display = "none"; }); // Upload & Preprocess logic uploadZone.addEventListener("click", (e) => { if (e.target !== btnResetImage && !btnResetImage.contains(e.target)) { fileInput.click(); } }); uploadZone.addEventListener("dragover", (e) => { e.preventDefault(); uploadZone.classList.add("dragover"); }); uploadZone.addEventListener("dragleave", () => { uploadZone.classList.remove("dragover"); }); uploadZone.addEventListener("drop", (e) => { e.preventDefault(); uploadZone.classList.remove("dragover"); const files = e.dataTransfer.files; if (files.length > 0) { handleImageUpload(files[0]); } }); fileInput.addEventListener("change", (e) => { const files = e.target.files; if (files.length > 0) { handleImageUpload(files[0]); } }); btnResetImage.addEventListener("click", (e) => { e.stopPropagation(); resetImageUpload(); }); function showToast(message, type = "info") { const toastContainer = document.getElementById("toastContainer"); const toast = document.createElement("div"); toast.className = `toast ${type}`; let icon = "fa-info-circle"; if (type === "success") icon = "fa-check-circle"; if (type === "warning") icon = "fa-exclamation-triangle"; if (type === "error") icon = "fa-exclamation-circle"; toast.innerHTML = ` ${message} `; toastContainer.appendChild(toast); setTimeout(() => { toast.style.opacity = "0"; toast.style.transform = "translateX(100%)"; setTimeout(() => toast.remove(), 300); }, 4000); } function resetImageUpload() { fileInput.value = ""; previewContainer.style.display = "none"; imagePreview.src = ""; uploadContent.style.display = "flex"; sessionIdInput.value = ""; btnGenerate.disabled = true; } async function handleImageUpload(file) { // Show loading state uploadContent.innerHTML = `

Обработка изображения...

Удаление фона и кадрирование

`; const formData = new FormData(); formData.append("file", file); try { const response = await fetch("/api/preprocess", { method: "POST", body: formData }); const data = await response.json(); if (!response.ok) throw new Error(data.detail || "Error during preprocessing"); // Show preview of processed image imagePreview.src = data.processedUrl; uploadContent.style.display = "none"; previewContainer.style.display = "block"; // Set session id sessionIdInput.value = data.id; btnGenerate.disabled = false; showToast("Изображение обработано успешно!", "success"); } catch (error) { console.error(error); showToast("Ошибка при обработке изображения: " + error.message, "error"); resetImageUpload(); uploadContent.innerHTML = `

Перетащите изображение сюда

или нажмите для выбора файла

Поддержка PNG, JPG (будет автоматически удален фон) `; } } // Generation Submit generationForm.addEventListener("submit", async (e) => { e.preventDefault(); const formData = new FormData(generationForm); // UI loading state const btnText = btnGenerate.querySelector(".btn-text"); const spinner = btnGenerate.querySelector(".spinner"); btnGenerate.disabled = true; btnText.style.display = "none"; spinner.style.display = "inline-flex"; // Reset output elements videoPlaceholder.style.display = "flex"; videoPlaceholder.innerHTML = `

Идет 3D генерация...

Это может занять от 15 до 45 секунд в зависимости от загрузки GPU

`; videoContainer.style.display = "none"; tab3dViewer.disabled = true; exportControls.style.display = "none"; showToast("Запущена генерация 3D модели...", "info"); try { const response = await fetch("/api/generate", { method: "POST", body: formData }); const data = await response.json(); if (!response.ok) throw new Error(data.detail || "Error during generation"); // Show video output videoPlaceholder.style.display = "none"; videoContainer.style.display = "block"; previewVideo.src = data.videoUrl; previewVideo.play(); // Setup download ply linkDownloadPly.href = data.plyUrl; linkDownloadPly.style.display = "inline-flex"; // Enable GLB extraction controls exportControls.style.display = "block"; btnExtractGlb.disabled = false; const btnExtractText = btnExtractGlb.querySelector(".btn-text"); const btnExtractSpinner = btnExtractGlb.querySelector(".spinner"); btnExtractText.style.display = "inline-flex"; btnExtractSpinner.style.display = "none"; // Disable GLB download button until extracted linkDownloadGlb.classList.add("disabled"); linkDownloadGlb.href = "#"; showToast("Генерация завершена успешно!", "success"); loadHistoryGallery(); } catch (error) { console.error(error); showToast("Ошибка при генерации 3D: " + error.message, "error"); videoPlaceholder.innerHTML = `

Ошибка генерации

${error.message}

`; } finally { btnText.style.display = "inline-flex"; spinner.style.display = "none"; btnGenerate.disabled = false; } }); // GLB Extraction btnExtractGlb.addEventListener("click", async () => { const id = sessionIdInput.value; const meshSimplify = document.getElementById("meshSimplify").value; const textureSize = document.getElementById("textureSize").value; const formData = new FormData(); formData.append("id", id); formData.append("mesh_simplify", meshSimplify); formData.append("texture_size", textureSize); // Loading state btnExtractGlb.disabled = true; const btnText = btnExtractGlb.querySelector(".btn-text"); const spinner = btnExtractGlb.querySelector(".spinner"); btnText.style.display = "none"; spinner.style.display = "inline-flex"; showToast("Запущено извлечение полигональной сетки и запекание текстур...", "info"); try { const response = await fetch("/api/extract_glb", { method: "POST", body: formData }); const data = await response.json(); if (!response.ok) throw new Error(data.detail || "Error during GLB extraction"); // Set GLB viewer source glbViewer.src = data.glbUrl; // Enable 3D Tab and switch tab3dViewer.disabled = false; tab3dViewer.click(); // Switch to 3D tab // Enable GLB download link linkDownloadGlb.classList.remove("disabled"); linkDownloadGlb.href = data.glbUrl; showToast("GLB модель успешно извлечена!", "success"); loadHistoryGallery(); } catch (error) { console.error(error); showToast("Ошибка при извлечении GLB: " + error.message, "error"); } finally { btnText.style.display = "inline-flex"; spinner.style.display = "none"; btnExtractGlb.disabled = false; } }); // Load History list async function loadHistoryGallery() { try { const response = await fetch("/api/history"); const history = await response.json(); // Clear gallery // Keep empty state element, but hide/show appropriately const cards = historyGallery.querySelectorAll(".history-card"); cards.forEach(card => card.remove()); if (history.length === 0) { galleryEmpty.style.display = "flex"; return; } galleryEmpty.style.display = "none"; history.forEach(item => { const card = document.createElement("div"); card.className = "history-card"; card.id = `card-${item.id}`; card.innerHTML = `
Seed: ${item.seed}
Генерация #${item.id.slice(0, 8)}
Шаги: ${item.ss_sampling_steps}/${item.slat_sampling_steps}
`; // Bind delete event const btnDelete = card.querySelector(".card-delete"); btnDelete.addEventListener("click", (e) => { e.stopPropagation(); deleteHistoryItem(item.id); }); // Bind load event const btnLoad = card.querySelector(".btn-load-model"); btnLoad.addEventListener("click", () => { loadModelToWorkspace(item); }); historyGallery.appendChild(card); }); } catch (error) { console.error("Error loading history:", error); } } async function deleteHistoryItem(id) { if (!confirm("Вы уверены, что хотите удалить эту генерацию?")) return; const formData = new FormData(); formData.append("id", id); try { const response = await fetch("/api/delete", { method: "POST", body: formData }); if (response.ok) { showToast("Генерация удалена", "info"); document.getElementById(`card-${id}`).remove(); // Check if empty if (historyGallery.querySelectorAll(".history-card").length === 0) { galleryEmpty.style.display = "flex"; } } else { showToast("Не удалось удалить элемент", "error"); } } catch (error) { showToast("Ошибка при удалении: " + error.message, "error"); } } function loadModelToWorkspace(item) { // Load image to preview imagePreview.src = item.processedUrl; uploadContent.style.display = "none"; previewContainer.style.display = "block"; sessionIdInput.value = item.id; btnGenerate.disabled = false; // Show video preview videoPlaceholder.style.display = "none"; videoContainer.style.display = "block"; previewVideo.src = item.videoUrl; previewVideo.play(); // Configure downloads linkDownloadPly.href = item.plyUrl; linkDownloadPly.style.display = "inline-flex"; exportControls.style.display = "block"; btnExtractGlb.disabled = false; // Load GLB if already extracted if (item.glbUrl) { glbViewer.src = item.glbUrl; tab3dViewer.disabled = false; tab3dViewer.click(); // Switch to 3D tab linkDownloadGlb.classList.remove("disabled"); linkDownloadGlb.href = item.glbUrl; } else { // Clean glb viewer source glbViewer.removeAttribute("src"); tab3dViewer.disabled = true; tabPreview.click(); // Go back to video preview linkDownloadGlb.classList.add("disabled"); linkDownloadGlb.href = "#"; } showToast(`Загружена генерация #${item.id.slice(0, 8)}`, "info"); } btnRefreshHistory.addEventListener("click", loadHistoryGallery); // Initial history load loadHistoryGallery(); });