- Deleted various texture files related to the Raspberry Pi camera model. - Introduced a new JSON file to store model metadata including IDs, names, and URLs. - Implemented authentication and session management using Supabase. - Created a header component for navigation and user session display. - Added protected routes to restrict access to certain pages. - Developed a private layout for authenticated users. - Enhanced the home page to dynamically load models from the new JSON file. - Updated the login page to handle user authentication. - Created a model uploader for users to upload 3D models. - Improved overall styling and layout for better user experience.
167 lines
5.9 KiB
TypeScript
167 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
|
|
import { useGLTF, useAnimations, Environment, Loader, OrbitControls, Bounds } from "@react-three/drei";
|
|
import { Canvas } from "@react-three/fiber";
|
|
import { createXRStore, XR } from "@react-three/xr";
|
|
import { Suspense, useEffect, useRef, useState } from "react";
|
|
import { RotateCw, Play, Pause, Film, Shrink, Expand } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { useNavigate, useParams } from 'react-router';
|
|
|
|
function Model({ url, onAnimationsLoaded, isAR }: {
|
|
url: string;
|
|
onAnimationsLoaded: (animations: string[], actions: Record<string, any>) => void;
|
|
isAR: boolean;
|
|
}) {
|
|
const { scene, animations } = useGLTF(url);
|
|
const { actions } = useAnimations(animations, scene);
|
|
|
|
useEffect(() => {
|
|
if (actions) {
|
|
onAnimationsLoaded(animations.map(anim => anim.name), actions);
|
|
}
|
|
|
|
if (isAR) {
|
|
scene.scale.set(0.01, 0.01, 0.01);
|
|
scene.position.set(10, 0, 10);
|
|
} else {
|
|
scene.scale.set(1, 1, 1);
|
|
scene.position.set(0, 0, 0);
|
|
}
|
|
}, [actions, animations, onAnimationsLoaded, isAR]);
|
|
|
|
return (
|
|
<Bounds fit margin={.9}>
|
|
<primitive object={scene} />
|
|
</Bounds>
|
|
);
|
|
}
|
|
|
|
const store = createXRStore();
|
|
|
|
export default function Previewer() {
|
|
const { modelId } = useParams<{ modelId: string }>();
|
|
const controlsRef = useRef<OrbitControlsImpl>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [animations, setAnimations] = useState<string[]>([]);
|
|
const [actions, setActions] = useState<Record<string, any>>({});
|
|
const [currentAnimation, setCurrentAnimation] = useState<string | null>(null);
|
|
const [isAR, setIsAR] = useState(false);
|
|
const [modelUrl, setModelUrl] = useState<string | null>(null);
|
|
|
|
const navigate = useNavigate();
|
|
|
|
console.log("ID del modelo:", modelId);
|
|
|
|
useEffect(() => {
|
|
fetch(`${import.meta.env.BASE_URL}models.json`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const model = data.find((m: { id: string }) => m.id === modelId);
|
|
if (model) setModelUrl(model.url);
|
|
});
|
|
}, [modelId]);
|
|
|
|
const playAnimation = (name: string) => {
|
|
if (actions[name]) {
|
|
Object.values(actions).forEach(action => action.stop());
|
|
actions[name].play();
|
|
setCurrentAnimation(name);
|
|
}
|
|
};
|
|
|
|
const pauseAnimation = () => {
|
|
if (currentAnimation && actions[currentAnimation]) {
|
|
actions[currentAnimation].stop();
|
|
setCurrentAnimation(null);
|
|
}
|
|
};
|
|
|
|
if (!modelUrl) return <div className="p-10 text-center">Cargando modelo...</div>;
|
|
|
|
return (
|
|
<div ref={containerRef} 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">
|
|
<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();
|
|
} else {
|
|
playAnimation(anim);
|
|
}
|
|
}}
|
|
>
|
|
{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>
|
|
</div>
|
|
<div className="absolute bottom-4 right-4 z-10 flex gap-4">
|
|
<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={() => navigate("/")}
|
|
>
|
|
<RotateCw className="w-5 h-5" />
|
|
Regresar al menú
|
|
</Button>
|
|
<Button
|
|
className="bg-gradient-to-r from-gray-700 to-gray-900 w-40 text-white px-5 py-3 rounded-lg shadow-lg transition-all duration-300 hover:from-gray-600 hover:to-gray-800 active:scale-95 flex items-center gap-2 justify-center"
|
|
onClick={() => {
|
|
store.enterAR();
|
|
setIsAR(!isAR);
|
|
}}
|
|
>
|
|
{isAR ? <Shrink className="w-5 h-5" /> : <Expand className="w-5 h-5" />}
|
|
{isAR ? "Salir de AR" : "Ver en AR"}
|
|
</Button>
|
|
</div>
|
|
<div className="w-full h-full">
|
|
<Suspense fallback={<Loader />}>
|
|
<Canvas className="w-full h-full">
|
|
<XR store={store}>
|
|
<Environment preset="city" />
|
|
<OrbitControls ref={controlsRef} minPolarAngle={Math.PI / 4} maxPolarAngle={Math.PI / 1.5} />
|
|
<Model
|
|
url={modelUrl}
|
|
onAnimationsLoaded={(animNames, actionMap) => {
|
|
setAnimations(animNames);
|
|
setActions(actionMap);
|
|
}}
|
|
isAR={isAR}
|
|
/>
|
|
</XR>
|
|
</Canvas>
|
|
</Suspense>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|