Primitivas

This commit is contained in:
2025-03-13 10:19:04 -06:00
parent a3c77fd1f3
commit 6dce45e88e
4 changed files with 149 additions and 111 deletions

View File

@@ -1,41 +1,33 @@
import { useNavigate } from "react-router";
import { Canvas } from "@react-three/fiber";
import { Environment, useGLTF } from "@react-three/drei";
import { Canvas, } from "@react-three/fiber";
import { Bounds, Environment, useGLTF } from "@react-three/drei";
import { Button } from "@/components/ui/button";
import { useRef } from "react";
import { Group } from "three";
const models = [
{
name: "Motor de Combustión",
url: "./4d/motor_de_combustion/scene.gltf",
route: "./previewer",
name: "Raspberry Pi Cam V2.1",
url: "./3d/raspberry_pi_cam_v2.1/scene.gltf",
route: "./previewer2",
},
{
name: "Raspberry Pi Cam V2.1",
url: "./4d/raspberry_pi_cam_v2.1/scene.gltf",
route: "./previewer2",
name: "Motor de Combustión",
url: "./3d/motor_de_combustion/scene.gltf",
route: "./previewer",
},
];
function ModelPreview({ url }: { url: string }) {
function CenteredModel({ url }: { url: string }) {
const group = useRef<Group>(null);
const { scene } = useGLTF(url);
// Define heuristic parameters for best perspective
const parameters: Record<string, { position: [number, number, number]; rotation: [number, number, number]; scale: number }> = {
"./4d/motor_de_combustion/scene.gltf": {
position: [0, -1.5, 0],
rotation: [0.2, Math.PI / 4, 0],
scale: 0.01,
},
"./4d/raspberry_pi_cam_v2.1/scene.gltf": {
position: [0, -0.5, 0],
rotation: [0.1, Math.PI / 6, 0],
scale: 0.5,
},
};
const { position, rotation, scale } = parameters[url] || { position: [0, 0, 0], rotation: [0, 0, 0], scale: 0.02 };
return <primitive object={scene} position={position} rotation={rotation} scale={scale} />;
return <Bounds fit margin={0.9}>
<group ref={group}>
<primitive object={scene} />
</group>
</Bounds>
}
export default function Home() {
@@ -50,18 +42,16 @@ export default function Home() {
key={index}
className="bg-white p-6 rounded-2xl shadow-lg flex flex-col items-center border border-white/20 transition-transform hover:scale-105 hover:shadow-xl"
>
{/* 3D Preview */}
<div className="w-64 h-64 bg-white rounded-xl overflow-hidden border border-white/20 shadow-md">
<Canvas camera={{ position: [0, 1, 3], fov: 30 }}>
<Canvas >
<Environment preset="city" />
<ModelPreview url={model.url} />
{/* Your rotating model */}
<CenteredModel url={model.url} />
</Canvas>
</div>
{/* Model Name */}
<h2 className="text-2xl font-semibold mt-6">{model.name}</h2>
{/* Navigation Button */}
<Button
className="mt-6 bg-gradient-to-r from-blue-600 to-purple-600 px-6 py-3 rounded-lg shadow-lg hover:from-blue-500 hover:to-purple-500 transition-all font-display"
onClick={() => navigate(model.route)}
@@ -76,4 +66,4 @@ export default function Home() {
}
// Preload models for performance
models.forEach(model => useGLTF.preload(model.url));
models.forEach(({ url }) => useGLTF.preload(url));

View File

@@ -2,25 +2,23 @@
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 { useGLTF, useAnimations, Environment, Loader, OrbitControls, Bounds } from "@react-three/drei";
import { Suspense, useEffect, useRef, useState } from "react";
import { RotateCw, Play, Pause, Film, ListTree, Shrink, Expand } from "lucide-react";
import { Play, Pause, Film, Expand, } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { createXRStore, XR, XRControllerModel, XRHandModel } from "@react-three/xr";
import { ScrollArea } from "@/components/ui/scroll-area";
const store = createXRStore();
// Model component
function Model({ url, position, rotation, scale, onAnimationsLoaded, onNodesLoaded }: {
function Model({ url, 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;
}) {
@@ -45,26 +43,15 @@ function Model({ url, position, rotation, scale, onAnimationsLoaded, onNodesLoad
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>
))}
<Bounds fit margin={.9}>
<primitive object={scene} />
</Bounds>
</>
);
}
// Preload the model
// useGLTF.preload("/3d/motor_de_combustion/scene.gltf");
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);
@@ -73,19 +60,12 @@ export default function Previewer({ modelUrl }: { modelUrl: string }) {
const [actions, setActions] = useState<Record<string, any>>({});
const [currentAnimation, setCurrentAnimation] = useState<string | null>(null);
const [nodes, setNodes] = useState<string[]>([]);
const [_, setNodes] = useState<string[]>([]);
const [nodePositions, setNodePositions] = useState<Record<string, [number, number, number]>>({});
const [isFullscreen, setIsFullscreen] = useState(false);
nodePositions;
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
@@ -101,24 +81,6 @@ export default function Previewer({ modelUrl }: { modelUrl: string }) {
}
};
// Handle Fullscreen Mode
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
containerRef.current?.requestFullscreen().then(() => {
setIsFullscreen(true);
}).catch(err => {
console.error("Error entering fullscreen:", err);
});
} else {
document.exitFullscreen().then(() => {
setIsFullscreen(false);
}).catch(err => {
console.error("Error exiting fullscreen:", err);
});
}
};
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">
@@ -156,7 +118,7 @@ export default function Previewer({ modelUrl }: { modelUrl: string }) {
</PopoverContent>
</Popover>
<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
@@ -176,48 +138,46 @@ export default function Previewer({ modelUrl }: { modelUrl: string }) {
</div>
</ScrollArea>
</PopoverContent>
</Popover>
</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={resetView}
>
<RotateCw className="w-5 h-5" />
Centrar cámara
</Button>
{/* Botón de pantalla completa */}
<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={toggleFullscreen} >
{isFullscreen ? <Shrink className="w-5 h-5" /> : <Expand className="w-5 h-5" />}
{isFullscreen ? "Salir" : "Pantalla Completa"}
onClick={() => store.enterAR()}>
<Expand className="w-5 h-5" />
Entrar en AR
</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.enterVR()}>
<Expand className="w-5 h-5" />
Entrar en VR
</Button>
</div>
<div className="w-full h-full">
<Suspense fallback={<Loader />}>
<Canvas camera={{ position: [0, 0, 12], fov: 20 }}>
<Environment preset="city" />
<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 className="w-full h-full">
<XR store={store}>
<Environment preset="city" />
<OrbitControls ref={controlsRef} />
<Model
url={modelUrl}
onAnimationsLoaded={(animNames, actionMap) => {
setAnimations(animNames);
setActions(actionMap);
}}
onNodesLoaded={(nodeList, nodePos) => {
setNodes(nodeList);
setNodePositions(nodePos);
}}
/>
</XR>
</Canvas>
</Suspense>
</div>
</div>
</div >
);
}