| "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 = [ |
| |
| { id: 'B3', label: 'E-WASTE\nS. ELEKTRONIK', color: '#dc2626', angle: 0 }, |
| { id: 'Organik', label: 'BAHAN KOMPOS\nSISA MAKANAN', color: '#16a34a', angle: Math.PI / 2 }, |
| { id: 'Residu', label: 'RESIDU\nSULIT DIDAUR', color: '#475569', angle: Math.PI }, |
| { id: 'Anorganik', label: 'ANORGANIK\nBERNILAI JUAL', color: '#eab308', angle: -Math.PI / 2 }, |
| ]; |
|
|
| function HollowBin({ position, color, rotationY, label, items = [] }: { position: [number, number, number], color: string, rotationY: number, label: string, items?: string[] }) { |
| const t = 0.06; |
| const w = 2.0; |
| const h = 2.5; |
| 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) { |
| |
| 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 { |
| |
| 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); |
| |
| if (!next[key].includes(capitalized)) { |
| next[key] = [...next[key], capitalized]; |
| |
| 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> |
| ); |
| } |
|
|