|
<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"; |
|
import { getConfig } from "./utils/getConfig"; |
|
|
|
interface Data { |
|
input: string; |
|
input_path: string; |
|
model1: string; |
|
model1_path: string; |
|
model1_displayName: string; |
|
model2: string; |
|
model2_path: string; |
|
model2_displayName: 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 token = localStorage.getItem("access_token"); |
|
const response = await fetch(url, { |
|
method: "GET", |
|
headers: { |
|
"Cache-Control": "no-cache", |
|
Authorization: `Bearer ${token}`, |
|
}, |
|
}); |
|
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() { |
|
viewerA?.dispose(); |
|
viewerB?.dispose(); |
|
|
|
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}`; |
|
|
|
const config1 = await getConfig(data.model1); |
|
const config2 = await getConfig(data.model2); |
|
|
|
data.model1_displayName = config1.DisplayName || data.model1; |
|
data.model2_displayName = config2.DisplayName || data.model2; |
|
|
|
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 token = localStorage.getItem("access_token"); |
|
if (!token) { |
|
window.location.href = "/api/authorize"; |
|
return; |
|
} |
|
|
|
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", |
|
Authorization: `Bearer ${token}`, |
|
}, |
|
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); |
|
} |
|
}); |
|
|
|
$: if (data && data.input_path) { |
|
console.log("Input path changed:", data.input_path); |
|
} |
|
</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_displayName}</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_displayName}</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} |
|
|