your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { Canvas } from '@threlte/core';
3 import { CineonToneMapping } from 'three';
4 import type { ContentComponentProps } from '../types';
5 import Model3DScene from './Model3DScene.svelte';
6 import { getDidContext } from '$lib/website/context';
7 import { getBlobURL } from '$lib/atproto';
8 import type { Did } from '@atcute/lexicons';
9 import { onMount } from 'svelte';
10
11 let { item }: ContentComponentProps = $props();
12
13 let isHovering = $state(false);
14 let objectUrl = $state<string | undefined>(undefined);
15 let isLoading = $state(false);
16
17 const did = getDidContext();
18
19 // Fetch blob from PDS and create object URL (like VideoCard does)
20 onMount(async () => {
21 if (item.cardData.modelBlob?.$type === 'blob') {
22 isLoading = true;
23 try {
24 const pdsUrl = await getBlobURL({ did: did as Did, blob: item.cardData.modelBlob });
25 const response = await fetch(pdsUrl);
26 if (!response.ok) throw new Error(response.statusText);
27 const blob = await response.blob();
28 objectUrl = URL.createObjectURL(blob);
29 } catch (e) {
30 console.error('Failed to load 3D model:', e);
31 } finally {
32 isLoading = false;
33 }
34 }
35 });
36
37 // Get the model URL from various sources
38 let modelUrl = $derived.by(() => {
39 // Local file (during editing before save)
40 if (item.cardData.modelFile?.objectUrl) {
41 return item.cardData.modelFile.objectUrl;
42 }
43
44 // Uploaded blob (after save) - use fetched object URL
45 if (item.cardData.modelBlob?.$type === 'blob') {
46 return objectUrl;
47 }
48
49 return undefined;
50 });
51
52 let modelType = $derived(item.cardData.modelFile?.type || item.cardData.modelType || 'gltf') as
53 | 'gltf'
54 | 'stl'
55 | 'fbx';
56</script>
57
58<div
59 class="absolute inset-0 h-full w-full"
60 role="img"
61 aria-label="3D model viewer"
62 onpointerenter={() => (isHovering = true)}
63 onpointerleave={() => (isHovering = false)}
64>
65 {#if modelUrl}
66 <Canvas toneMapping={CineonToneMapping}>
67 <Model3DScene path={modelUrl} hover={isHovering} {modelType} />
68 </Canvas>
69 {:else if isLoading}
70 <div class="flex h-full items-center justify-center text-sm opacity-50">Loading model...</div>
71 {:else}
72 <div class="flex h-full items-center justify-center text-sm opacity-50">No model loaded</div>
73 {/if}
74</div>