Files
website/src/lib/components/models/ModelViewer.svelte
T

150 lines
3.8 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import Icon from '@iconify/svelte';
import type { ModelScene } from '#lib/models/model-scene';
let { modelUrl, modelName }: { modelUrl: string; modelName: string } = $props();
let container = $state<HTMLDivElement>();
let modelScene = $state<ModelScene | null>(null);
let status = $state<'loading' | 'ready' | 'error' | 'unsupported'>('loading');
let autoRotate = $state(true);
let wireframe = $state(false);
let brightness = $state(0.6);
onMount(() => {
let disposed = false;
let resizeObserver: ResizeObserver | undefined;
import('#lib/models/model-scene')
.then(({ ModelScene }) => {
if (disposed || !container) return;
modelScene = new ModelScene(container);
resizeObserver = new ResizeObserver(() => modelScene?.resize());
resizeObserver.observe(container);
})
.catch(() => (status = 'unsupported'));
return () => {
disposed = true;
resizeObserver?.disconnect();
modelScene?.dispose();
};
});
$effect(() => {
if (!modelScene) return;
status = 'loading';
wireframe = false;
modelScene
.load(modelUrl)
.then(() => (status = 'ready'))
.catch(() => (status = 'error'));
});
$effect(() => modelScene?.setAutoRotate(autoRotate));
$effect(() => modelScene?.setWireframe(wireframe));
$effect(() => modelScene?.setBrightness(brightness));
</script>
<div class="viewer">
<div class="toolbar">
<span class="model-name">{modelName}</span>
<div class="controls">
<button class:active={autoRotate} title="Auto rotate" onclick={() => (autoRotate = !autoRotate)}>
<Icon icon="mdi:rotate-3d-variant" width="16" />
</button>
<button class:active={wireframe} title="Wireframe" onclick={() => (wireframe = !wireframe)}>
<Icon icon="mdi:cube-outline" width="16" />
</button>
<button title="Darker" onclick={() => (brightness = Math.max(0, brightness - 0.2))}>
<Icon icon="mdi:brightness-5" width="16" />
</button>
<button title="Brighter" onclick={() => (brightness = Math.min(3, brightness + 0.2))}>
<Icon icon="mdi:brightness-7" width="16" />
</button>
<button title="Reset camera" onclick={() => modelScene?.resetCamera()}>
<Icon icon="mdi:camera-retake" width="16" />
</button>
</div>
</div>
<div class="canvas-area" bind:this={container} tabindex="0" role="application" aria-label="3D view of {modelName}">
{#if status !== 'ready'}
<div class="status muted">
{#if status === 'loading'}
loading model...
{:else if status === 'error'}
failed to load model
{:else}
WebGL is not available in this browser
{/if}
</div>
{/if}
</div>
<div class="hints muted">drag to rotate · scroll to zoom · arrow keys after clicking</div>
</div>
<style>
.viewer {
display: flex;
flex-direction: column;
border: 1px solid color-mix(in srgb, var(--foreground) 22%, transparent);
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.3rem 0.6rem;
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 22%, transparent);
}
.model-name {
color: var(--accent);
}
.controls {
display: flex;
gap: 0.25rem;
}
.controls button {
display: grid;
place-items: center;
width: 28px;
height: 28px;
background: none;
border: 1px solid transparent;
}
.controls button:hover {
border-color: color-mix(in srgb, var(--foreground) 30%, transparent);
}
.controls button.active {
color: var(--accent);
}
.canvas-area {
position: relative;
height: 380px;
background: radial-gradient(circle, var(--lighter-background), var(--dark-background));
}
.canvas-area :global(canvas) {
display: block;
}
.status {
position: absolute;
inset: 0;
display: grid;
place-items: center;
}
.hints {
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
}
</style>