Spaces:
Running
Running
import React, { useEffect, useRef, useState, useMemo } from "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 { | |
createUrdfViewer, | |
setupMeshLoader, | |
setupJointHighlighting, | |
setupModelLoading, | |
} from "@/lib/urdfViewerHelpers"; | |
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; | |
} | |
const URDFViewer: React.FC = () => { | |
const { theme } = useTheme(); | |
const isDarkMode = theme === "dark"; | |
const containerRef = useRef<HTMLDivElement>(null); | |
const [highlightedJoint, setHighlightedJoint] = useState<string | null>(null); | |
const { registerUrdfProcessor, alternativeUrdfModels, isDefaultModel } = | |
useUrdf(); | |
// Add state for animation control | |
useState<boolean>(isDefaultModel); | |
const cleanupAnimationRef = useRef<(() => void) | null>(null); | |
const viewerRef = useRef<URDFViewerElement | null>(null); | |
const hasInitializedRef = useRef<boolean>(false); | |
// Add state for custom URDF path | |
const [customUrdfPath, setCustomUrdfPath] = useState<string | null>(null); | |
const [urlModifierFunc, setUrlModifierFunc] = useState< | |
((url: string) => string) | null | |
>(null); | |
const packageRef = useRef<string>(""); | |
// 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]); | |
// 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/SO_5DOF_ARM100_05d/urdf/SO_5DOF_ARM100_05d.urdf" | |
// : customUrdfPath || ""; | |
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 ("setJointValue" in viewer) { | |
// Clear any existing animation | |
if (cleanupAnimationRef.current) { | |
cleanupAnimationRef.current(); | |
cleanupAnimationRef.current = null; | |
} | |
} | |
}; | |
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]); | |
// 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]); | |
return ( | |
<div | |
className={cn( | |
"w-full h-full transition-all duration-300 ease-in-out relative", | |
isDarkMode | |
? "bg-gradient-to-br from-gray-900 to-gray-800" | |
: "bg-gradient-to-br from-blue-50 to-indigo-50" | |
)} | |
> | |
<div ref={containerRef} className="w-full h-full" /> | |
{/* Control buttons container in top right */} | |
<div className="absolute top-4 right-4 flex items-center space-x-2 z-10"> | |
{/* ModeToggle button */} | |
<ModeToggle /> | |
</div> | |
{/* Joint highlight indicator */} | |
{highlightedJoint && ( | |
<div className="absolute bottom-4 right-4 bg-black/70 text-white px-3 py-2 rounded-md text-sm font-mono z-10"> | |
Joint: {highlightedJoint} | |
</div> | |
)} | |
</div> | |
); | |
}; | |
export default URDFViewer; | |