Sistema de etiquetas y posicionamiento
This commit is contained in:
@@ -2,7 +2,6 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Label } from '../components/ui/label';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { NavLink } from 'react-router';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -15,9 +14,11 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { useNavigate, useNavigation } from 'react-router';
|
||||
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<div className="w-full grow bg-gradient-to-br from-gray-100/50 to-gray-300/15 shadow-xl shadow-gray-400/10 flex flex-col items-center justify-center p-5 gap-20 rounded-lg backdrop-blur-sm">
|
||||
<Card className="w-fit">
|
||||
@@ -59,11 +60,11 @@ function App() {
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction className="font-display">Confirmar</AlertDialogAction>
|
||||
<AlertDialogAction className="font-display" onClick={() => navigate('/login')}>Confirmar</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Button className="font-display px-6 py-3 text-lg font-bold text-white bg-gradient-to-b from-blue-500 to-blue-700 rounded-lg shadow-lg shadow-blue-500/50 transform transition-all duration-300 hover:shadow-xl hover:scale-105 active:translate-y-1 active:shadow-md cursor-pointer">
|
||||
<Button className="font-display px-6 py-3 text-lg font-bold text-white bg-gradient-to-b from-blue-500 to-blue-700 rounded-lg shadow-lg shadow-blue-500/50 transform transition-all duration-300 hover:shadow-xl hover:scale-105 active:translate-y-1 active:shadow-md cursor-pointer" onClick={() => navigate('/previewer')}>
|
||||
Presentar
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useNavigate } from 'react-router';
|
||||
|
||||
function Login() {
|
||||
let navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="w-full grow bg-gradient-to-br from-gray-100 to-gray-300/20 shadow-xl shadow-gray-400/10 flex flex-col items-center justify-center p-5 gap-20 rounded-lg backdrop-blur-lg">
|
||||
<Card className="w-full max-w-md p-6 bg-white rounded-xl shadow-lg">
|
||||
@@ -29,7 +30,7 @@ function Login() {
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4">
|
||||
<Button
|
||||
className="w-full font-bold text-lg py-2 bg-gradient-to-r from-blue-500 to-blue-700 text-white rounded-lg shadow-md transition-all duration-300 hover:scale-105 hover:shadow-lg active:shadow-sm" onClick={() => {
|
||||
className="font-display px-6 py-3 text-lg font-bold text-white bg-gradient-to-b from-blue-500 to-blue-700 rounded-lg shadow-lg shadow-blue-500/50 transform transition-all duration-300 hover:shadow-xl hover:scale-105 active:translate-y-1 active:shadow-md cursor-pointer" onClick={() => {
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
|
||||
19
src/pages/NotFound.tsx
Normal file
19
src/pages/NotFound.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NavLink } from 'react-router';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center text-white">
|
||||
<div className="w-full max-w-lg bg-gradient-to-br from-gray-100 to-gray-300/20 shadow-2xl shadow-gray-400/20 flex flex-col items-center justify-center p-10 gap-8 rounded-2xl backdrop-blur-lg border border-gray-200/30">
|
||||
<h1 className="text-6xl font-extrabold text-red-500 drop-shadow-lg font-display">404</h1>
|
||||
<p className="text-lg text-gray-700 text-center">La página que estás buscando no existe.</p>
|
||||
|
||||
<NavLink
|
||||
to="/"
|
||||
className="px-6 py-3 text-lg font-semibold text-white bg-blue-600 rounded-lg shadow-lg shadow-blue-600/30 transition-transform duration-200 hover:scale-105 hover:bg-blue-700"
|
||||
>
|
||||
Regresar
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user