import React, { useEffect, useRef, useState, useMemo } from "react"; import { Play, Pause } from "lucide-react"; import { cn } from "@/lib/utils"; import { useTheme } from "@/hooks/useTheme"; import URDFManipulator from "urdf-loader/src/urdf-manipulator-element.js"; import { useUrdf } from "@/hooks/useUrdf"; import { animateHexapodRobot, animateRobot, cassieWalkingConfig, } from "@/lib/urdfAnimationHelpers"; import { createUrdfViewer, setupMeshLoader, setupJointHighlighting, setupModelLoading, } from "@/lib/urdfViewerHelpers"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { ModeToggle } from "./ModeToggle"; // Register the URDFManipulator as a custom element if it hasn't been already if (typeof window !== "undefined" && !customElements.get("urdf-viewer")) { customElements.define("urdf-viewer", URDFManipulator); } // Extend the interface for the URDF viewer element to include background property interface URDFViewerElement extends HTMLElement { background?: string; setJointValue?: (jointName: string, value: number) => void; } interface URDFViewerProps { hasAnimation?: boolean; headerHeight?: number; } const URDFViewer: React.FC = ({ hasAnimation = false, headerHeight = 0, }) => { const { theme } = useTheme(); const isDarkMode = theme === "dark"; const containerRef = useRef(null); const [highlightedJoint, setHighlightedJoint] = useState(null); const { registerUrdfProcessor, alternativeUrdfModels, isDefaultModel, currentAnimationConfig, } = useUrdf(); // Add state for animation control const [isAnimating, setIsAnimating] = useState(isDefaultModel); const [showAnimationControl, setShowAnimationControl] = useState( isDefaultModel || hasAnimation ); const cleanupAnimationRef = useRef<(() => void) | null>(null); const viewerRef = useRef(null); const hasInitializedRef = useRef(false); // Add state for custom URDF path const [customUrdfPath, setCustomUrdfPath] = useState(null); const [urlModifierFunc, setUrlModifierFunc] = useState< ((url: string) => string) | null >(null); const packageRef = useRef(""); // Implement UrdfProcessor interface for drag and drop const urdfProcessor = useMemo( () => ({ loadUrdf: (urdfPath: string) => { setCustomUrdfPath(urdfPath); }, setUrlModifierFunc: (func: (url: string) => string) => { setUrlModifierFunc(() => func); }, getPackage: () => { return packageRef.current; }, }), [] ); // Register the URDF processor with the global drag and drop context useEffect(() => { registerUrdfProcessor(urdfProcessor); }, [registerUrdfProcessor, urdfProcessor]); // Effect to update animation control visibility based on props useEffect(() => { // Show animation controls for default model or when hasAnimation is true setShowAnimationControl(isDefaultModel || hasAnimation); }, [isDefaultModel, hasAnimation]); // Toggle animation function const toggleAnimation = () => { setIsAnimating((prev) => !prev); }; // Main effect to create and setup the viewer only once useEffect(() => { if (!containerRef.current) return; // Create and configure the URDF viewer element const viewer = createUrdfViewer(containerRef.current, isDarkMode); viewerRef.current = viewer; // Store reference to the viewer // Setup mesh loading function setupMeshLoader(viewer, urlModifierFunc); // Determine which URDF to load const urdfPath = isDefaultModel ? "/urdf/T12/urdf/T12.URDF" : customUrdfPath || ""; // Setup model loading if a path is available let cleanupModelLoading = () => {}; if (urdfPath) { cleanupModelLoading = setupModelLoading( viewer, urdfPath, packageRef.current, setCustomUrdfPath, alternativeUrdfModels ); } // Setup joint highlighting const cleanupJointHighlighting = setupJointHighlighting( viewer, setHighlightedJoint ); // Setup animation event handler for the default model or when hasAnimation is true const onModelProcessed = () => { hasInitializedRef.current = true; if (isAnimating && "setJointValue" in viewer) { // Clear any existing animation if (cleanupAnimationRef.current) { cleanupAnimationRef.current(); cleanupAnimationRef.current = null; } // Use the appropriate animation based on whether it's the default model or hasAnimation if (isDefaultModel) { // Start the hexapod animation when it's the default model cleanupAnimationRef.current = animateHexapodRobot( viewer as import("@/lib/urdfAnimationHelpers").URDFViewerElement ); } else if (hasAnimation) { // Use the custom animation config from the context if available, otherwise fall back to cassieWalkingConfig const animationConfig = currentAnimationConfig || cassieWalkingConfig; // Start the animation using the selected configuration cleanupAnimationRef.current = animateRobot( viewer as import("@/lib/urdfAnimationHelpers").URDFViewerElement, animationConfig ); } } }; viewer.addEventListener("urdf-processed", onModelProcessed); // Return cleanup function return () => { if (cleanupAnimationRef.current) { cleanupAnimationRef.current(); cleanupAnimationRef.current = null; } hasInitializedRef.current = false; cleanupJointHighlighting(); cleanupModelLoading(); viewer.removeEventListener("urdf-processed", onModelProcessed); }; }, [ isDefaultModel, customUrdfPath, urlModifierFunc, hasAnimation, currentAnimationConfig, ]); // Separate effect to handle theme changes without recreating the viewer useEffect(() => { if (!viewerRef.current) return; // Update only the visual aspects based on theme if (viewerRef.current.background !== undefined) { if (isDarkMode) { viewerRef.current.background = "#1f2937"; // Dark background } else { viewerRef.current.background = "#e0e7ff"; // Light background } } }, [isDarkMode]); // Effect to handle animation toggling after initial load useEffect(() => { if (!viewerRef.current || !hasInitializedRef.current) return; // Only manage animation if viewer has setJointValue (required for animation) if (!("setJointValue" in viewerRef.current)) return; if (isAnimating) { if (!cleanupAnimationRef.current) { // Only start animation if it's not already running if (isDefaultModel) { cleanupAnimationRef.current = animateHexapodRobot( viewerRef.current as import("@/lib/urdfAnimationHelpers").URDFViewerElement ); } else if (hasAnimation) { // Use the custom animation config from the context if available, otherwise fall back to cassieWalkingConfig const animationConfig = currentAnimationConfig; // Start animation using the selected configuration cleanupAnimationRef.current = animateRobot( viewerRef.current as import("@/lib/urdfAnimationHelpers").URDFViewerElement, animationConfig ); } } } else { if (cleanupAnimationRef.current) { // Just cancel the animation frame without resetting anything cleanupAnimationRef.current(); cleanupAnimationRef.current = null; } } }, [isAnimating, isDefaultModel, hasAnimation, currentAnimationConfig]); return (
{/* Control buttons container in top right, with vertical padding for header */}
{/* ModeToggle button */} {/* Animation control button with icon - show for both default model and when animation is available */} {showAnimationControl && (

{isAnimating ? "Stop Animation" : "Start Animation"}

)}
{/* Joint highlight indicator */} {highlightedJoint && (
Joint: {highlightedJoint}
)}
); }; export default URDFViewer;