Skip to content

Commit 080f591

Browse files
committed
add components for rendering stls
1 parent 76f15c7 commit 080f591

2 files changed

Lines changed: 311 additions & 0 deletions

File tree

src/components/STLPileViewer.tsx

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { Canvas } from "@react-three/fiber";
2+
import { OrbitControls, Environment } from "@react-three/drei";
3+
import { Physics, RigidBody } from "@react-three/rapier";
4+
import { Suspense, useEffect, useState } from "react";
5+
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
6+
import * as THREE from "three";
7+
8+
interface STLModelConfig {
9+
url: string;
10+
position?: [number, number, number];
11+
scale?: number;
12+
quantity?: number;
13+
}
14+
15+
interface PhysicsSTLProps {
16+
config: STLModelConfig;
17+
delay: number;
18+
color: string;
19+
}
20+
21+
// Generate a random pastel color
22+
function generateRandomColor(): string {
23+
const hue = Math.random() * 360;
24+
const saturation = 60 + Math.random() * 20; // 60-80%
25+
const lightness = 65 + Math.random() * 15; // 65-80%
26+
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
27+
}
28+
29+
function PhysicsSTL({ config, delay, color }: PhysicsSTLProps) {
30+
const [geometry, setGeometry] = useState<THREE.BufferGeometry | null>(null);
31+
const [isVisible, setIsVisible] = useState(false);
32+
33+
useEffect(() => {
34+
const loader = new STLLoader();
35+
loader.load(
36+
config.url,
37+
(loadedGeometry) => {
38+
loadedGeometry.computeBoundingBox();
39+
setGeometry(loadedGeometry);
40+
},
41+
undefined,
42+
(error) => {
43+
console.error("Error loading STL:", error);
44+
},
45+
);
46+
}, [config.url]);
47+
48+
useEffect(() => {
49+
const timer = setTimeout(() => {
50+
setIsVisible(true);
51+
}, delay);
52+
return () => clearTimeout(timer);
53+
}, [delay]);
54+
55+
if (!geometry || !isVisible) return null;
56+
57+
const position = config.position || [
58+
(Math.random() - 0.5) * 0.3,
59+
5,
60+
(Math.random() - 0.5) * 0.3,
61+
];
62+
63+
const scale = config.scale || 1;
64+
65+
return (
66+
<RigidBody
67+
position={position}
68+
colliders="cuboid"
69+
restitution={0.2}
70+
friction={0.3}
71+
linearDamping={0.5}
72+
angularDamping={0.5}
73+
canSleep={true}
74+
>
75+
<mesh geometry={geometry} scale={scale} castShadow receiveShadow>
76+
<meshStandardMaterial
77+
color={color}
78+
roughness={0.35}
79+
metalness={0.0}
80+
envMapIntensity={0.6}
81+
/>
82+
</mesh>
83+
</RigidBody>
84+
);
85+
}
86+
87+
function Ground() {
88+
return (
89+
<RigidBody type="fixed" colliders="cuboid" friction={0.4}>
90+
<mesh position={[0, -0.6, 0]} receiveShadow>
91+
<boxGeometry args={[10, 0.5, 10]} />
92+
<meshStandardMaterial color="#b2b2b2ff" roughness={0.9} />
93+
</mesh>
94+
</RigidBody>
95+
);
96+
}
97+
98+
export interface Props {
99+
models: STLModelConfig[];
100+
height?: string;
101+
backgroundColor?: string;
102+
enableReset?: boolean;
103+
dropDelay?: number;
104+
className?: string;
105+
}
106+
107+
export default function STLPileViewer({
108+
models,
109+
height = "600px",
110+
backgroundColor = "rgba(0,0,0,0)",
111+
dropDelay = 200,
112+
className,
113+
}: Props) {
114+
// Expand models based on quantity and randomize order
115+
const expandedModels = models.flatMap((model) => {
116+
const quantity = model.quantity || 1;
117+
return Array.from({ length: quantity }, () => ({
118+
...model,
119+
color: generateRandomColor(),
120+
}));
121+
});
122+
123+
// Shuffle the expanded models array
124+
const shuffledModels = [...expandedModels].sort(() => Math.random() - 0.5);
125+
126+
return (
127+
<div style={{ position: "relative" }}>
128+
<div
129+
className={className}
130+
style={{
131+
width: "100%",
132+
maxWidth: "1200px",
133+
height,
134+
margin: "2rem auto",
135+
borderRadius: "8px",
136+
overflow: "hidden",
137+
}}
138+
>
139+
<Canvas
140+
camera={{ position: [0, 3, 4], fov: 50 }}
141+
shadows
142+
style={{ background: backgroundColor }}
143+
>
144+
{/* <ambientLight intensity={0.1} castShadow/> */}
145+
<directionalLight
146+
castShadow
147+
position={[10, 10, 10]}
148+
intensity={0.02}
149+
shadow-mapSize={[1024, 1024]}
150+
/>
151+
<directionalLight position={[-10, -10, -10]} intensity={0.4} />
152+
<Environment preset="city" />
153+
154+
<Suspense fallback={null}>
155+
<Physics gravity={[0, -9.81, 0]}>
156+
<Ground />
157+
{shuffledModels.map((model, index) => (
158+
<PhysicsSTL
159+
config={model}
160+
delay={index * dropDelay}
161+
color={model.color}
162+
/>
163+
))}
164+
</Physics>
165+
</Suspense>
166+
167+
<OrbitControls
168+
enableDamping
169+
dampingFactor={0.05}
170+
autoRotate={false}
171+
enablePan={true}
172+
enableZoom={false}
173+
minDistance={3}
174+
maxDistance={15}
175+
target={[0, 0, 0]}
176+
/>
177+
</Canvas>
178+
</div>
179+
</div>
180+
);
181+
}

src/components/STLViewer.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { Canvas } from "@react-three/fiber";
2+
import { OrbitControls, Center, Environment } from "@react-three/drei";
3+
import { Suspense, useEffect, useState } from "react";
4+
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
5+
import * as THREE from "three";
6+
7+
interface STLModelProps {
8+
url: string;
9+
rotationX?: number;
10+
rotationY?: number;
11+
rotationZ?: number;
12+
}
13+
14+
function STLModel({
15+
url,
16+
rotationX = 0,
17+
rotationY = 0,
18+
rotationZ = 0,
19+
}: STLModelProps) {
20+
const [geometry, setGeometry] = useState<THREE.BufferGeometry | null>(null);
21+
22+
useEffect(() => {
23+
const loader = new STLLoader();
24+
loader.load(
25+
url,
26+
(loadedGeometry) => {
27+
loadedGeometry.computeBoundingBox();
28+
setGeometry(loadedGeometry);
29+
},
30+
undefined,
31+
(error) => {
32+
console.error("Error loading STL:", error);
33+
},
34+
);
35+
}, [url]);
36+
37+
if (!geometry) return null;
38+
39+
return (
40+
<Center>
41+
<mesh
42+
geometry={geometry}
43+
rotation={[rotationX, rotationY, rotationZ]}
44+
scale={1.5}
45+
castShadow
46+
receiveShadow
47+
>
48+
<meshStandardMaterial
49+
color={"#a9a9a9ff"}
50+
roughness={0.35}
51+
metalness={0.0}
52+
envMapIntensity={0.3}
53+
/>
54+
</mesh>
55+
</Center>
56+
);
57+
}
58+
59+
export interface Props {
60+
modelPath: string;
61+
height?: string;
62+
cameraDistance?: number;
63+
cameraHeight?: number;
64+
autoRotate?: boolean;
65+
backgroundColor?: string;
66+
rotationX?: number;
67+
rotationY?: number;
68+
rotationZ?: number;
69+
className?: string;
70+
}
71+
72+
export default function STLViewer({
73+
modelPath,
74+
height = "400px",
75+
cameraDistance = 2,
76+
cameraHeight = 45,
77+
autoRotate = true,
78+
backgroundColor = "rgba(0,0,0,0)",
79+
rotationX = 0,
80+
rotationY = 0,
81+
rotationZ = 0,
82+
className,
83+
}: Props) {
84+
return (
85+
<div
86+
className={className}
87+
style={{
88+
width: "100%",
89+
maxWidth: "800px",
90+
height,
91+
margin: "2rem auto",
92+
borderRadius: "8px",
93+
overflow: "hidden",
94+
}}
95+
>
96+
<Canvas
97+
camera={{ position: [20, cameraHeight, cameraDistance], fov: 50 }}
98+
shadows
99+
style={{ background: backgroundColor }}
100+
>
101+
{/* <ambientLight intensity={4} /> */}
102+
<directionalLight
103+
castShadow
104+
position={[20, cameraHeight, cameraDistance]}
105+
intensity={1}
106+
/>
107+
{/* <directionalLight position={[-1, -1, -1]} intensity={0.3} /> */}
108+
<Environment preset="city" />
109+
110+
<Suspense fallback={null}>
111+
<STLModel
112+
url={modelPath}
113+
rotationX={rotationX}
114+
rotationY={rotationY}
115+
rotationZ={rotationZ}
116+
/>
117+
</Suspense>
118+
119+
<OrbitControls
120+
enableDamping
121+
dampingFactor={0.05}
122+
autoRotate={autoRotate}
123+
autoRotateSpeed={2.0}
124+
enablePan={false}
125+
enableZoom={false}
126+
/>
127+
</Canvas>
128+
</div>
129+
);
130+
}

0 commit comments

Comments
 (0)