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; | |