|
<script lang="ts"> |
|
import { onMount, onDestroy } from "svelte"; |
|
import { v4 as uuidv4 } from "uuid"; |
|
import type { IViewer } from "./viewers/IViewer"; |
|
import { createViewer } from "./viewers/ViewerFactory"; |
|
import { Cube, WatsonHealth3DPrintMesh } from "carbon-icons-svelte"; |
|
|
|
interface Data { |
|
input: string; |
|
input_path: string; |
|
model1: string; |
|
model1_path: string; |
|
model2: string; |
|
model2_path: string; |
|
} |
|
|
|
let viewerA: IViewer; |
|
let viewerB: IViewer; |
|
let canvasA: HTMLCanvasElement; |
|
let canvasB: HTMLCanvasElement; |
|
let containerA: HTMLDivElement; |
|
let containerB: HTMLDivElement; |
|
let overlayA: HTMLDivElement; |
|
let overlayB: HTMLDivElement; |
|
let loadingBarFillA: HTMLDivElement; |
|
let loadingBarFillB: HTMLDivElement; |
|
let statusMessage: string = "Loading..."; |
|
let errorMessage: string = ""; |
|
let data: Data; |
|
|
|
function getUsername() { |
|
let storedUsername = sessionStorage.getItem("username"); |
|
if (!storedUsername) { |
|
storedUsername = uuidv4(); |
|
sessionStorage.setItem("username", storedUsername); |
|
} |
|
return storedUsername; |
|
} |
|
|
|
async function fetchScenes() { |
|
statusMessage = "Loading..."; |
|
errorMessage = ""; |
|
|
|
try { |
|
const username = getUsername(); |
|
console.log(`Fetching with username: ${username}`); |
|
const url = `https://dylanebert-3d-arena-backend.hf.space/pair?username=${username}`; |
|
const response = await fetch(url, { |
|
method: "GET", |
|
headers: { |
|
Authorization: "Bearer " + import.meta.env.VITE_HF_TOKEN, |
|
"Cache-Control": "no-cache", |
|
}, |
|
}); |
|
const result = await response.json(); |
|
if (result.input) { |
|
data = result; |
|
statusMessage = ""; |
|
return true; |
|
} else { |
|
statusMessage = "Voting complete."; |
|
return false; |
|
} |
|
} catch (error) { |
|
errorMessage = "Failed to fetch pair."; |
|
statusMessage = ""; |
|
return false; |
|
} |
|
} |
|
|
|
async function loadScenes() { |
|
const scrollPosition = window.scrollY; |
|
const success = await fetchScenes(); |
|
if (!success) return; |
|
|
|
overlayA.style.display = "flex"; |
|
overlayB.style.display = "flex"; |
|
|
|
const baseUrl = "https://huggingface.co/datasets/dylanebert/3d-arena/resolve/main/"; |
|
const model1_path = `${baseUrl}${data.model1_path}`; |
|
const model2_path = `${baseUrl}${data.model2_path}`; |
|
|
|
try { |
|
[viewerA, viewerB] = await Promise.all([ |
|
createViewer(model1_path, canvasA, (progress) => { |
|
loadingBarFillA.style.width = `${progress * 100}%`; |
|
}), |
|
createViewer(model2_path, canvasB, (progress) => { |
|
loadingBarFillB.style.width = `${progress * 100}%`; |
|
}), |
|
]); |
|
|
|
window.addEventListener("resize", handleResize); |
|
handleResize(); |
|
} catch (error) { |
|
errorMessage = "Failed to load scenes."; |
|
} |
|
|
|
overlayA.style.display = "none"; |
|
overlayB.style.display = "none"; |
|
window.scrollTo(0, scrollPosition); |
|
} |
|
|
|
async function vote(option: "A" | "B") { |
|
statusMessage = "Processing vote..."; |
|
errorMessage = ""; |
|
|
|
const payload = { |
|
username: getUsername(), |
|
input: data.input, |
|
better: option == "A" ? data.model1 : data.model2, |
|
worse: option == "A" ? data.model2 : data.model1, |
|
}; |
|
const url = `https://dylanebert-3d-arena-backend.hf.space/vote`; |
|
|
|
try { |
|
const response = await fetch(url, { |
|
method: "POST", |
|
headers: { |
|
Authorization: "Bearer " + import.meta.env.VITE_HF_TOKEN, |
|
"Cache-Control": "no-cache", |
|
"Content-Type": "application/json", |
|
}, |
|
body: JSON.stringify(payload), |
|
}); |
|
|
|
if (response.ok) { |
|
const result = await response.json(); |
|
console.log(result); |
|
loadScenes(); |
|
} else { |
|
errorMessage = "Failed to process vote."; |
|
} |
|
} catch (error) { |
|
errorMessage = "Failed to process vote."; |
|
statusMessage = ""; |
|
} |
|
} |
|
|
|
function handleResize() { |
|
requestAnimationFrame(() => { |
|
if (canvasA && containerA) { |
|
canvasA.width = containerA.clientWidth; |
|
canvasA.height = containerA.clientHeight; |
|
} |
|
if (canvasB && containerB) { |
|
canvasB.width = containerB.clientWidth; |
|
canvasB.height = containerB.clientHeight; |
|
} |
|
}); |
|
} |
|
|
|
function setRenderMode(viewer: IViewer, mode: string) { |
|
viewer.setRenderMode(mode); |
|
} |
|
|
|
onMount(loadScenes); |
|
|
|
onDestroy(() => { |
|
viewerA?.dispose(); |
|
viewerB?.dispose(); |
|
if (typeof window !== "undefined") { |
|
window.removeEventListener("resize", handleResize); |
|
} |
|
}); |
|
</script> |
|
|
|
{#if errorMessage} |
|
<p class="center-title muted" style="color: red;">{errorMessage}</p> |
|
{:else if statusMessage} |
|
<p class="center-title muted">{statusMessage}</p> |
|
{:else} |
|
<div class="vote-input"> |
|
<img |
|
src={`https://huggingface.co/datasets/dylanebert/3d-arena/resolve/main/${data.input_path}`} |
|
class="input-image" |
|
alt="Input" |
|
/> |
|
</div> |
|
<h2 class="center-title">Which is better?</h2> |
|
<p class="center-subtitle">Use mouse/touch to change the view.</p> |
|
<div class="voting-container"> |
|
<div bind:this={containerA} class="canvas-container"> |
|
<div bind:this={overlayA} class="loading-overlay"> |
|
<div class="loading-bar"> |
|
<div bind:this={loadingBarFillA} class="loading-bar-fill" /> |
|
</div> |
|
</div> |
|
<canvas bind:this={canvasA} class="viewer-canvas" id="canvas1"> </canvas> |
|
<div class="stats"> |
|
{#if viewerA} |
|
<p>vertex count: {viewerA.vertexCount}</p> |
|
{/if} |
|
</div> |
|
<div class="mode-toggle"> |
|
<label> |
|
<input |
|
type="radio" |
|
name="modeA" |
|
value="default" |
|
checked |
|
on:change={() => setRenderMode(viewerA, "default")} |
|
/> |
|
<Cube class="mode-toggle-icon" /> |
|
</label> |
|
<label> |
|
<input |
|
type="radio" |
|
name="modeA" |
|
value="wireframe" |
|
on:change={() => setRenderMode(viewerA, "wireframe")} |
|
/> |
|
<WatsonHealth3DPrintMesh class="mode-toggle-icon" /> |
|
</label> |
|
</div> |
|
</div> |
|
<div bind:this={containerB} class="canvas-container"> |
|
<div bind:this={overlayB} class="loading-overlay"> |
|
<div class="loading-bar"> |
|
<div bind:this={loadingBarFillB} class="loading-bar-fill" /> |
|
</div> |
|
</div> |
|
<canvas bind:this={canvasB} class="viewer-canvas" id="canvas2"></canvas> |
|
<div class="stats"> |
|
{#if viewerB} |
|
<p>vertex count: {viewerB.vertexCount}</p> |
|
{/if} |
|
</div> |
|
<div class="mode-toggle"> |
|
<label> |
|
<input |
|
type="radio" |
|
name="modeB" |
|
value="default" |
|
checked |
|
on:change={() => setRenderMode(viewerB, "default")} |
|
/> |
|
<Cube class="mode-toggle-icon" /> |
|
</label> |
|
<label> |
|
<input |
|
type="radio" |
|
name="modeB" |
|
value="wireframe" |
|
on:change={() => setRenderMode(viewerB, "wireframe")} |
|
/> |
|
<WatsonHealth3DPrintMesh class="mode-toggle-icon" /> |
|
</label> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="vote-buttons-container"> |
|
<button class="vote-button" on:click={() => vote("A")}>A is Better</button> |
|
<button class="vote-button" on:click={() => vote("B")}>B is Better</button> |
|
</div> |
|
<div class="skip-container"> |
|
<button class="vote-button" on:click={() => loadScenes()}>Skip</button> |
|
</div> |
|
{/if} |
|
|