Sistema de etiquetas y posicionamiento

This commit is contained in:
Your Name
2025-03-04 16:27:17 -06:00
parent 2616ea478a
commit d15c91ad73
14 changed files with 701 additions and 24 deletions

View File

@@ -1,36 +1,194 @@
import { Canvas } from "@react-three/fiber";
import { useGLTF, useAnimations, Environment, Loader } from "@react-three/drei";
import { Suspense, useEffect } from "react";
"use client";
function Model({ url }: { url: string }) {
import type { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
import { Canvas } from "@react-three/fiber";
import { useGLTF, useAnimations, Environment, Loader, OrbitControls, Html } from "@react-three/drei";
import { Suspense, useEffect, useRef, useState } from "react";
import { RotateCw, Play, Pause, Film, ListTree } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
// Model component
function Model({ url, position, rotation, scale, onAnimationsLoaded, onNodesLoaded }: {
url: string;
position: [number, number, number];
rotation: number;
scale: number;
onAnimationsLoaded: (animations: string[], actions: Record<string, any>) => void;
onNodesLoaded: (nodes: string[], nodePositions: Record<string, [number, number, number]>) => void;
}) {
const { scene, animations } = useGLTF(url);
const { actions } = useAnimations(animations, scene);
useEffect(() => {
if (actions && animations.length > 0) {
actions[animations[0].name]?.play(); // Play the first animation if available
if (actions) {
onAnimationsLoaded(animations.map(anim => anim.name), actions);
}
}, [actions, animations]);
return <primitive object={scene} scale={0.01} position={[0, -2, 0]} rotation={[0, -3 * Math.PI / 5, 0]} />;
if (scene.children) {
const nodeList = scene.children.map(node => node.name);
const nodePositions = scene.children.reduce((acc, node) => {
acc[node.name] = [node.position.x, node.position.y, node.position.z];
return acc;
}, {} as Record<string, [number, number, number]>);
onNodesLoaded(nodeList, nodePositions);
}
}, [actions, animations, onAnimationsLoaded, onNodesLoaded]);
return (
<>
<primitive object={scene} scale={scale} position={position} rotation={[0, rotation, 0]} />
{scene.children.map((node, index) => (
<Html key={index} position={[node.position.x, node.position.y + 0.5, node.position.z]}>
<div className="bg-blue-500 text-white px-2 py-1 rounded-md text-xs shadow-md">
{node.name}
</div>
</Html>
))}
</>
);
}
// Preload the model
useGLTF.preload("/3d/motor_de_combustion/scene.gltf");
// Preload the model for smoother loading
useGLTF.preload("/models/sample.glb");
const InitialParams = {
position: [0, -2, 0] as [number, number, number],
rotation: -Math.PI / 2,
scale: 0.01,
};
// Main 3D Viewer
export default function Previewer({ modelUrl }: { modelUrl: string }) {
const controlsRef = useRef<OrbitControlsImpl>(null);
const [animations, setAnimations] = useState<string[]>([]);
const [actions, setActions] = useState<Record<string, any>>({});
const [currentAnimation, setCurrentAnimation] = useState<string | null>(null);
const [nodes, setNodes] = useState<string[]>([]);
const [nodePositions, setNodePositions] = useState<Record<string, [number, number, number]>>({});
const resetView = () => {
if (controlsRef.current) {
controlsRef.current.reset();
}
};
const playAnimation = (name: string) => {
if (actions[name]) {
Object.values(actions).forEach(action => action.stop()); // Stop other animations
actions[name].play();
setCurrentAnimation(name);
}
};
const pauseAnimation = () => {
if (currentAnimation && actions[currentAnimation]) {
actions[currentAnimation].stop();
setCurrentAnimation(null);
}
};
return (
<div className="grow w-full bg-white/10 shadow-xl shadow-gray-300/25 rounded-lg p-6 backdrop-blur-lg">
<div className="w-full h-100 grow">
<div className="relative w-full grow bg-white/10 shadow-xl shadow-gray-300/25 rounded-lg p-6 backdrop-blur-lg h-100">
<div className="z-10 absolute top-4 w-100 flex wrap gap-5">
<Popover>
<PopoverTrigger className="flex items-center gap-2 bg-gradient-to-r from-blue-600 to-blue-800 text-white px-4 py-2 rounded-lg shadow-lg transition-all duration-300 hover:from-blue-500 hover:to-blue-700">
<Film className="w-5 h-5" />
<span>Animaciones</span>
</PopoverTrigger>
<PopoverContent className="p-4 bg-blue-900 text-white w-64 my-5 mx-15">
<ScrollArea className="max-h-60">
<div className="flex flex-col gap-2">
{animations.length > 0 ? (
animations.map((anim) => (
<Button
key={anim}
className="w-full flex justify-between italic"
onClick={() => {
if (currentAnimation === anim) {
pauseAnimation(); // Pause if it's already playing
} else {
playAnimation(anim); // Play if it's not currently playing
}
}}
>
{anim}
{currentAnimation === anim ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
</Button>
))
) : (
<p className="text-gray-500 text-center">Este modelo no tiene animaciones.</p>
)}
</div>
</ScrollArea>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger className="flex items-center gap-2 bg-gradient-to-r from-blue-100 to-blue-50 text-blue-500 px-4 py-2 rounded-lg shadow-lg transition-all duration-300 hover:from-blue-100 hover:to-blue-200">
<ListTree />
Partes del modelo
</PopoverTrigger>
<PopoverContent className="p-4 bg-blue-50 text-white w-64 my-5 mx-15">
<ScrollArea className="max-h-60">
<div className="flex flex-col gap-2">
{nodes.length > 0 ? (
nodes.map((node, index) => (
<div key={index} className="bg-blue-900/10 p-2 rounded-lg text-center text-blue-700">
{node}
</div>
))
) : (
<p className="text-blue-500 text-center">Este modelo no tiene partes.</p>
)}
</div>
</ScrollArea>
</PopoverContent>
</Popover>
</div>
{/* Reset Camera Button */}
<div className="absolute bottom-4 right-4 z-10">
<Button
className="bg-gradient-to-r from-red-600 to-red-800 w-40 text-white px-5 py-3 rounded-lg shadow-lg transition-all duration-300 hover:from-red-500 hover:to-red-700 active:scale-95 flex items-center gap-2 justify-center"
onClick={resetView}
>
<RotateCw className="w-5 h-5" />
Reset Camera
</Button>
</div>
<div className="w-full h-full">
<Suspense fallback={<Loader />}>
<Canvas
camera={{ position: [0, 0, 12], fov: 20 }}>
<Canvas camera={{ position: [0, 0, 12], fov: 20 }}>
<Environment preset="city" />
<Model url={modelUrl} />
<OrbitControls ref={controlsRef} />
<Model
url={modelUrl}
position={InitialParams.position}
rotation={InitialParams.rotation}
scale={InitialParams.scale}
onAnimationsLoaded={(animNames, actionMap) => {
setAnimations(animNames);
setActions(actionMap);
}}
onNodesLoaded={(nodeList, nodePos) => {
setNodes(nodeList);
setNodePositions(nodePos);
}}
/>
</Canvas>
</Suspense>
</div>
</div>
);
}