your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { T, useTask, useThrelte } from '@threlte/core';
3 import { GLTF, OrbitControls } from '@threlte/extras';
4 import type { ThrelteGltf } from '@threlte/extras';
5 import { onMount } from 'svelte';
6 import {
7 Box3,
8 Group,
9 Vector3,
10 BufferGeometry,
11 Mesh,
12 MeshStandardMaterial,
13 type Object3D
14 } from 'three';
15 import { STLLoader } from 'three/addons/loaders/STLLoader.js';
16 import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
17
18 let {
19 path,
20 hover = false,
21 modelType = 'gltf'
22 }: {
23 path: string;
24 hover?: boolean;
25 modelType?: 'gltf' | 'stl' | 'fbx';
26 } = $props();
27
28 let rotation = $state(0);
29 let group: Group | undefined = $state();
30 let stlMesh: Mesh | undefined = $state();
31 let stlLoaded = $state(false);
32 let fbxGroup: Group | undefined = $state();
33 let fbxLoaded = $state(false);
34
35 const { start, stop } = useTask((delta: number) => {
36 rotation += delta * 0.5;
37 });
38
39 $effect(() => {
40 if (hover) {
41 start();
42 } else {
43 stop();
44 }
45 });
46
47 const { renderer } = useThrelte();
48
49 onMount(() => {
50 renderer.toneMappingExposure = 0.7;
51 });
52
53 // Load STL file
54 $effect(() => {
55 if (modelType === 'stl' && path) {
56 stlLoaded = false;
57 const loader = new STLLoader();
58 loader.load(
59 path,
60 (geometry: BufferGeometry) => {
61 // Center and scale the geometry
62 geometry.computeBoundingBox();
63 const box = geometry.boundingBox;
64 if (box) {
65 const size = new Vector3();
66 box.getSize(size);
67 const center = new Vector3();
68 box.getCenter(center);
69
70 const maxSize = Math.max(size.x, size.y, size.z);
71 const scale = 1.2 / maxSize;
72
73 geometry.translate(-center.x, -center.y, -center.z);
74 geometry.scale(scale, scale, scale);
75 }
76
77 // Create mesh with a nice material
78 const material = new MeshStandardMaterial({
79 color: 0x808080,
80 metalness: 0.3,
81 roughness: 0.6
82 });
83
84 stlMesh = new Mesh(geometry, material);
85 stlLoaded = true;
86 },
87 undefined,
88 (error) => {
89 console.error('Error loading STL:', error);
90 }
91 );
92 }
93 });
94
95 // Load FBX file
96 $effect(() => {
97 if (modelType === 'fbx' && path) {
98 fbxLoaded = false;
99 const loader = new FBXLoader();
100 loader.load(
101 path,
102 (object: Group) => {
103 // Center and scale the model
104 const box = new Box3().setFromObject(object);
105 const size = box.getSize(new Vector3());
106 const center = box.getCenter(new Vector3());
107
108 const maxSize = Math.max(size.x, size.y, size.z);
109 const scale = 1.2 / maxSize;
110
111 object.scale.set(scale, scale, scale);
112 object.position.set(-center.x * scale, -center.y * scale, -center.z * scale);
113
114 fbxGroup = object;
115 fbxLoaded = true;
116 },
117 undefined,
118 (error) => {
119 console.error('Error loading FBX:', error);
120 }
121 );
122 }
123 });
124
125 function handleGltfLoad(gltf: ThrelteGltf) {
126 if (!group) return;
127
128 const box = new Box3().setFromObject(gltf.scene as Object3D);
129 const size = box.getSize(new Vector3());
130 const center = box.getCenter(new Vector3());
131
132 let maxSize = Math.max(size.x, size.y, size.z);
133 let scale = 1.2 / maxSize;
134
135 group.scale.set(scale, scale, scale);
136 group.position.set(-center.x * scale, -center.y * scale, -center.z * scale);
137 }
138</script>
139
140<T.PerspectiveCamera makeDefault position={[0, 0, 2.5]} fov={50} near={0.1} far={100}>
141 <OrbitControls enableZoom={false} enablePan={false} />
142</T.PerspectiveCamera>
143
144<T.DirectionalLight args={[0xffffff, 2]} position={[-1, 1, 1]} />
145<T.AmbientLight args={[0xffffff, 0.7]} />
146
147<T.Group rotation={[0.3, rotation + 0.5, 0]}>
148 {#if modelType === 'stl'}
149 {#if stlLoaded && stlMesh}
150 <T is={stlMesh} />
151 {/if}
152 {:else if modelType === 'fbx'}
153 {#if fbxLoaded && fbxGroup}
154 <T is={fbxGroup} />
155 {/if}
156 {:else}
157 <T.Group bind:ref={group}>
158 <GLTF url={path} onload={handleGltfLoad} />
159 </T.Group>
160 {/if}
161</T.Group>