85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
"use client"
|
|
|
|
/**
|
|
* GLB Placement Preview
|
|
* Shows ghost preview of model before placement
|
|
*/
|
|
|
|
import { useRef, useState } from "react"
|
|
import { useFrame, useThree } from "@react-three/fiber"
|
|
import { useGLTF } from "@react-three/drei"
|
|
import * as THREE from "three"
|
|
import { usePlannerStore } from "@/lib/store/planner-store"
|
|
|
|
export function GLBPlacementPreview() {
|
|
const { draggedProduct, snapSettings } = usePlannerStore()
|
|
const { camera, raycaster, pointer } = useThree()
|
|
const meshRef = useRef<THREE.Group>(null)
|
|
const [position, setPosition] = useState<THREE.Vector3>(new THREE.Vector3(0, 0, 0))
|
|
|
|
// Load model if dragging
|
|
const modelUrl = draggedProduct?.glbFile?.url || draggedProduct?.glbFile
|
|
const gltf = modelUrl ? useGLTF(modelUrl) : null
|
|
|
|
// Update position based on mouse
|
|
useFrame(() => {
|
|
if (!draggedProduct || !meshRef.current) return
|
|
|
|
// Raycast to find position on ground
|
|
raycaster.setFromCamera(pointer, camera)
|
|
|
|
// Create ground plane for raycasting
|
|
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
const intersectionPoint = new THREE.Vector3()
|
|
|
|
raycaster.ray.intersectPlane(groundPlane, intersectionPoint)
|
|
|
|
// Apply snapping if enabled
|
|
if (snapSettings.enabled) {
|
|
const gridSize = snapSettings.gridSize
|
|
intersectionPoint.x = Math.round(intersectionPoint.x / gridSize) * gridSize
|
|
intersectionPoint.z = Math.round(intersectionPoint.z / gridSize) * gridSize
|
|
}
|
|
|
|
setPosition(intersectionPoint)
|
|
meshRef.current.position.copy(intersectionPoint)
|
|
|
|
// Animate bobbing effect
|
|
const time = Date.now() * 0.001
|
|
meshRef.current.position.y = Math.sin(time * 2) * 0.05 + 0.1
|
|
})
|
|
|
|
if (!draggedProduct || !gltf) return null
|
|
|
|
return (
|
|
<group ref={meshRef} position={position}>
|
|
<primitive
|
|
object={gltf.scene.clone()}
|
|
scale={[1, 1, 1]}
|
|
/>
|
|
|
|
{/* Ghost material overlay */}
|
|
<mesh position={[0, 0, 0]}>
|
|
<boxGeometry args={[
|
|
(draggedProduct.dimensions?.width || 60) / 100,
|
|
(draggedProduct.dimensions?.height || 80) / 100,
|
|
(draggedProduct.dimensions?.depth || 60) / 100
|
|
]} />
|
|
<meshStandardMaterial
|
|
color="#00ff00"
|
|
transparent
|
|
opacity={0.3}
|
|
wireframe
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Placement indicator */}
|
|
<mesh position={[0, -0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
<ringGeometry args={[0.3, 0.35, 32]} />
|
|
<meshBasicMaterial color="#00ff00" transparent opacity={0.8} />
|
|
</mesh>
|
|
</group>
|
|
)
|
|
}
|
|
|