Spaces:
Running
Running
<script lang="ts"> | |
import { onMount, onDestroy } from "svelte"; | |
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 normalToggleA: HTMLInputElement; | |
let normalToggleB: HTMLInputElement; | |
let wireframeToggleA: HTMLInputElement; | |
let wireframeToggleB: HTMLInputElement; | |
let loadingBarFillA: HTMLDivElement; | |
let loadingBarFillB: HTMLDivElement; | |
let voteOverlay: boolean = false; | |
let voteOverlayA: HTMLDivElement; | |
let voteOverlayB: HTMLDivElement; | |
let statusMessage: string = "Loading..."; | |
let errorMessage: string = ""; | |
let data: Data; | |
async function fetchScenes() { | |
statusMessage = "Loading..."; | |
errorMessage = ""; | |
try { | |
const url = "/api/fetchScenes"; | |
const response = await fetch(url, { | |
method: "GET", | |
headers: { | |
"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 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) { | |
console.log(error); | |
errorMessage = "Failed to load scenes."; | |
} | |
overlayA.style.display = "none"; | |
overlayB.style.display = "none"; | |
} | |
async function vote(option: "A" | "B") { | |
voteOverlay = true; | |
voteOverlayA.classList.add("show"); | |
voteOverlayB.classList.add("show"); | |
const payload = { | |
input: data.input, | |
better: option == "A" ? data.model1 : data.model2, | |
worse: option == "A" ? data.model2 : data.model1, | |
}; | |
const url = `/api/vote`; | |
const startTime = Date.now(); | |
try { | |
const response = await fetch(url, { | |
method: "POST", | |
headers: { | |
"Cache-Control": "no-cache", | |
"Content-Type": "application/json", | |
}, | |
body: JSON.stringify(payload), | |
}); | |
const elapsedTime = Date.now() - startTime; | |
const remainingTime = Math.max(1200 - elapsedTime, 0); | |
if (response.ok) { | |
const result = await response.json(); | |
console.log(result); | |
setTimeout(() => { | |
voteOverlayA.classList.remove("show"); | |
voteOverlayB.classList.remove("show"); | |
voteOverlay = false; | |
loadScenes(); | |
}, remainingTime); | |
} else { | |
if (response.status === 401) { | |
statusMessage = "Unauthorized. Redirecting to login..."; | |
await new Promise((resolve) => setTimeout(resolve, 1000)); | |
window.location.href = "/api/authorize"; | |
} else { | |
errorMessage = "Failed to process vote."; | |
} | |
} | |
} catch (error) { | |
errorMessage = "Failed to process vote."; | |
statusMessage = ""; | |
} | |
} | |
function skip() { | |
loadScenes(); | |
} | |
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> | |
<div bind:this={voteOverlayA} class="vote-overlay">{data.model1}</div> | |
<canvas bind:this={canvasA} class="viewer-canvas" id="canvas1"> </canvas> | |
<div class="stats"> | |
{#if viewerA} | |
<p>vertex count: {viewerA.vertexCount}</p> | |
{/if} | |
</div> | |
{#if viewerA && !viewerA.topoOnly} | |
<div class="mode-toggle"> | |
<label> | |
<input | |
type="radio" | |
name="modeA" | |
value="default" | |
checked | |
bind:this={normalToggleA} | |
on:change={() => setRenderMode(viewerA, "default")} | |
/> | |
<Cube class="mode-toggle-icon" /> | |
</label> | |
<label> | |
<input | |
type="radio" | |
name="modeA" | |
value="wireframe" | |
bind:this={wireframeToggleA} | |
on:change={() => setRenderMode(viewerA, "wireframe")} | |
/> | |
<WatsonHealth3DPrintMesh class="mode-toggle-icon" /> | |
</label> | |
</div> | |
{/if} | |
</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> | |
<div bind:this={voteOverlayB} class="vote-overlay">{data.model2}</div> | |
<canvas bind:this={canvasB} class="viewer-canvas" id="canvas2"></canvas> | |
<div class="stats"> | |
{#if viewerB} | |
<p>vertex count: {viewerB.vertexCount}</p> | |
{/if} | |
</div> | |
{#if viewerB && !viewerB.topoOnly} | |
<div class="mode-toggle"> | |
<label> | |
<input | |
type="radio" | |
name="modeB" | |
value="default" | |
checked | |
bind:this={normalToggleB} | |
on:change={() => setRenderMode(viewerB, "default")} | |
/> | |
<Cube class="mode-toggle-icon" /> | |
</label> | |
<label> | |
<input | |
type="radio" | |
name="modeB" | |
value="wireframe" | |
bind:this={wireframeToggleB} | |
on:change={() => setRenderMode(viewerB, "wireframe")} | |
/> | |
<WatsonHealth3DPrintMesh class="mode-toggle-icon" /> | |
</label> | |
</div> | |
{/if} | |
</div> | |
</div> | |
{#if !voteOverlay} | |
<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={() => skip()}>Skip</button> | |
</div> | |
{/if} | |
{/if} | |