fpAI-komposin / fe /components /Simulation3D.tsx
orang-sukses
Initial commit: Upload seluruh project FE, BE, dan Model
08181d8
Raw
History Blame Contribute Delete
9.87 kB
"use client";
import React, { useRef, useState, useEffect } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Environment, ContactShadows, RoundedBox, Text } from '@react-three/drei';
import * as THREE from 'three';
const categories = [
// Bins layout: Red(+Z), Green(+X), Grey(-Z), Yellow(-X)
{ id: 'B3', label: 'E-WASTE\nS. ELEKTRONIK', color: '#dc2626', angle: 0 }, // Merah
{ id: 'Organik', label: 'BAHAN KOMPOS\nSISA MAKANAN', color: '#16a34a', angle: Math.PI / 2 }, // Hijau
{ id: 'Residu', label: 'RESIDU\nSULIT DIDAUR', color: '#475569', angle: Math.PI }, // Abu-abu
{ id: 'Anorganik', label: 'ANORGANIK\nBERNILAI JUAL', color: '#eab308', angle: -Math.PI / 2 },// Kuning (gabungan plastik, kaleng, kertas, dus)
];
function HollowBin({ position, color, rotationY, label, items = [] }: { position: [number, number, number], color: string, rotationY: number, label: string, items?: string[] }) {
const t = 0.06; // thickness
const w = 2.0; // much wider bins
const h = 2.5; // taller bins
const d = 2.0;
return (
<group position={position} rotation={[0, rotationY, 0]}>
{/* Bottom face at y=0 */}
<mesh position={[0, t/2, 0]} castShadow receiveShadow>
<boxGeometry args={[w, t, d]}/>
<meshStandardMaterial color={color} roughness={0.6} />
</mesh>
{/* Front */}
<group position={[0, h/2, d/2 - t/2]}>
<mesh castShadow receiveShadow>
<boxGeometry args={[w, h, t]}/>
<meshStandardMaterial color={color} roughness={0.6} />
</mesh>
{/* Label on the front */}
<Text
position={[0, h/2 - 0.2, t/2 + 0.01]}
fontSize={0.2}
color="white"
anchorX="center"
anchorY="top"
textAlign="center"
fontWeight="bold"
outlineWidth={0.01}
outlineColor="#000000"
outlineOpacity={0.5}
>
{label}
</Text>
{/* Render Items with Boxes */}
{items.map((item, i) => (
<group key={i} position={[0, h/2 - 0.85 - (i * 0.28), t/2 + 0.01]}>
<RoundedBox args={[1.6, 0.22, 0.02]} radius={0.05}>
<meshBasicMaterial color="#000000" opacity={0.4} transparent />
</RoundedBox>
<Text
position={[0, 0, 0.015]}
fontSize={0.12}
color="white"
anchorX="center"
anchorY="middle"
fontWeight="bold"
>
{i + 1}. {item}
</Text>
</group>
))}
</group>
{/* Back */}
<mesh position={[0, h/2, -d/2 + t/2]} castShadow receiveShadow>
<boxGeometry args={[w, h, t]}/>
<meshStandardMaterial color={color} roughness={0.6} />
</mesh>
{/* Left */}
<mesh position={[w/2 - t/2, h/2, 0]} castShadow receiveShadow>
<boxGeometry args={[t, h, d]}/>
<meshStandardMaterial color={color} roughness={0.6} />
</mesh>
{/* Right */}
<mesh position={[-w/2 + t/2, h/2, 0]} castShadow receiveShadow>
<boxGeometry args={[t, h, d]}/>
<meshStandardMaterial color={color} roughness={0.6} />
</mesh>
</group>
);
}
function HardwarePlatform({ activeCategory }: { activeCategory: string }) {
const platformGroup = useRef<THREE.Group>(null);
const targetCategory = categories.find(c => activeCategory?.trim().toLowerCase() === c.id.toLowerCase());
const isMixed = activeCategory?.trim().toLowerCase() === 'campuran';
const tiltAngle = Math.PI / 3.5;
let targetRotX = 0;
let targetRotZ = 0;
if (targetCategory && !isMixed) {
if (targetCategory.id === 'B3') targetRotX = tiltAngle;
else if (targetCategory.id === 'Residu') targetRotX = -tiltAngle;
else if (targetCategory.id === 'Organik') targetRotZ = -tiltAngle;
else if (targetCategory.id === 'Anorganik') targetRotZ = tiltAngle;
}
useFrame((state, delta) => {
if (platformGroup.current) {
if (isMixed) {
// Shake logic for rejected waste (menggeleng/menolak)
platformGroup.current.rotation.y = Math.sin(state.clock.elapsedTime * 15) * 0.15;
platformGroup.current.rotation.x += (0 - platformGroup.current.rotation.x) * 0.1;
platformGroup.current.rotation.z += (0 - platformGroup.current.rotation.z) * 0.1;
} else {
// Use lerp for smooth 4-directional tilt
platformGroup.current.rotation.y += (0 - platformGroup.current.rotation.y) * 0.1;
platformGroup.current.rotation.x += (targetRotX - platformGroup.current.rotation.x) * 0.1;
platformGroup.current.rotation.z += (targetRotZ - platformGroup.current.rotation.z) * 0.1;
}
}
});
return (
<group position={[0, 3.25, 0]}>
{/* Stationary Base */}
<mesh position={[0, 0.1, 0]} castShadow>
<boxGeometry args={[0.4, 0.2, 0.4]}/>
<meshStandardMaterial color="#111"/>
</mesh>
{/* Multi-axis Tilting Platform */}
<group ref={platformGroup} position={[0, 0.2, 0]}>
{/* Ball Joint Visual */}
<mesh position={[0, 0.05, 0]} castShadow>
<sphereGeometry args={[0.15, 16, 16]}/>
<meshStandardMaterial color="#222"/>
</mesh>
{/* Flat White Rounded Square Platform */}
<RoundedBox args={[2.4, 0.08, 2.4]} radius={0.03} position={[0, 0.22, 0]} castShadow receiveShadow>
<meshStandardMaterial color="#f8fafc" roughness={0.2} metalness={0.1} />
</RoundedBox>
{/* Debug Text on the platform */}
{activeCategory && (
<Text
position={[0, 0.27, 0]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={0.2}
color={isMixed ? "#dc2626" : "black"}
textAlign="center"
fontWeight="bold"
>
{isMixed ? 'CAMPURAN!\n(Pisahkan Dulu)' : `${activeCategory} ${targetCategory ? '(Match!)' : '(No Match!)'}`}
</Text>
)}
</group>
</group>
);
}
function SmartBinHardware({ activeCategory, history }: { activeCategory: string, history: Record<string, string[]> }) {
return (
<group position={[0, -1.2, 0]}>
{/* 4 Colored Bins */}
{categories.map((cat, idx) => {
const radius = 2.6; // Push bins further outward to fit the larger bins
const px = Math.sin(cat.angle) * radius;
const pz = Math.cos(cat.angle) * radius;
const uniqueItems = history[cat.id] || [];
return (
<HollowBin
key={idx}
position={[px, 0, pz]} // Bins rest at ground level (y=0)
color={cat.color}
rotationY={cat.angle}
label={cat.label}
items={uniqueItems}
/>
);
})}
{/* Central Stand Pillar */}
<mesh position={[0, 1.8, 0]} castShadow receiveShadow>
<boxGeometry args={[0.35, 2.9, 0.35]}/>
<meshStandardMaterial color="#27272a"/>
</mesh>
{/* 4 Diagonal Tripod Legs */}
{[Math.PI/4, 3*Math.PI/4, -Math.PI/4, -3*Math.PI/4].map((rot, i) => (
<group key={i} rotation={[0, rot, 0]}>
{/* Legs touch ground at y=0 and lean onto the pillar */}
<mesh position={[0, 1.0, 0.85]} rotation={[-Math.PI / 6, 0, 0]} castShadow>
<boxGeometry args={[0.25, 2.4, 0.25]}/>
<meshStandardMaterial color="#27272a"/>
</mesh>
</group>
))}
{/* Tilting Sorting Platform */}
<HardwarePlatform activeCategory={activeCategory} />
</group>
);
}
export default function Simulation3D({ activeCategory, predictions = [] }: { activeCategory: string, predictions?: any[] }) {
const [history, setHistory] = useState<Record<string, string[]>>({
'B3': [],
'Organik': [],
'Residu': [],
'Anorganik': []
});
useEffect(() => {
if (predictions && predictions.length > 0) {
setHistory(prev => {
const next = { ...prev };
let changed = false;
predictions.forEach(p => {
const cat = p.category;
const item = p.class_name?.replace(/_/g, ' ');
if (cat && item) {
const key = Object.keys(next).find(k => k.toLowerCase() === cat.toLowerCase());
if (key) {
const capitalized = item.charAt(0).toUpperCase() + item.slice(1);
// Tambahkan jika belum ada di dalam daftar yang sudah ada
if (!next[key].includes(capitalized)) {
next[key] = [...next[key], capitalized];
// Batasi maksimal 5 item terakhir
if (next[key].length > 5) {
next[key] = next[key].slice(-5);
}
changed = true;
}
}
}
});
return changed ? next : prev;
});
}
}, [predictions]);
return (
<Canvas shadows camera={{ position: [7, 10, 11], fov: 45 }} className="w-full h-full outline-none cursor-grab active:cursor-grabbing">
<ambientLight intensity={0.5} />
<spotLight position={[5, 18, 8]} angle={0.5} penumbra={1} intensity={2.5} castShadow shadow-mapSize={1024} />
<pointLight position={[-8, 8, -8]} intensity={0.6} color="#ffffff" />
<SmartBinHardware activeCategory={activeCategory} history={history} />
<ContactShadows position={[0, -1.2, 0]} opacity={0.6} scale={20} blur={2.5} far={4} color="#000000" />
<Environment preset="city" />
<OrbitControls
enableZoom={true}
enablePan={false}
maxPolarAngle={Math.PI / 2 + 0.1}
minPolarAngle={0.1}
autoRotate={!activeCategory}
autoRotateSpeed={0.5}
makeDefault
/>
</Canvas>
);
}