import React, { useEffect, useRef, useState, useMemo, useCallback, } from "react"; import { cn } from "@/lib/utils"; import URDFManipulator from "urdf-loader/src/urdf-manipulator-element.js"; import { useUrdf } from "@/hooks/useUrdf"; import { useRealTimeJoints } from "@/hooks/useRealTimeJoints"; import { createUrdfViewer, setupMeshLoader, setupJointHighlighting, setupModelLoading, URDFViewerElement, } from "@/lib/urdfViewerHelpers"; // 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); } import * as THREE from "three"; // 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 containerRef = useRef(null); const [highlightedJoint, setHighlightedJoint] = useState(null); const { registerUrdfProcessor, alternativeUrdfModels, isDefaultModel } = useUrdf(); // Add state for animation control useState(isDefaultModel); const cleanupAnimationRef = useRef<(() => void) | null>(null); const viewerRef = useRef(null); const hasInitializedRef = useRef(false); // Real-time joint updates via WebSocket const { isConnected: isWebSocketConnected } = useRealTimeJoints({ viewerRef, enabled: isDefaultModel, // Only enable WebSocket for default model }); // 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]); // Create URL modifier function for default model const defaultUrlModifier = useCallback((url: string) => { console.log(`🔗 defaultUrlModifier called with: ${url}`); // Handle various package:// URL formats for the default SO-101 model if (url.startsWith("package://so_arm_description/meshes/")) { const modifiedUrl = url.replace( "package://so_arm_description/meshes/", "/so-101-urdf/meshes/" ); console.log(`🔗 Modified URL (package): ${modifiedUrl}`); return modifiedUrl; } // Handle case where package path might be partially resolved if (url.includes("so_arm_description/meshes/")) { const modifiedUrl = url.replace( /.*so_arm_description\/meshes\//, "/so-101-urdf/meshes/" ); console.log(`🔗 Modified URL (partial): ${modifiedUrl}`); return modifiedUrl; } // Handle the specific problematic path pattern we're seeing in logs if (url.includes("/so-101-urdf/so_arm_description/meshes/")) { const modifiedUrl = url.replace( "/so-101-urdf/so_arm_description/meshes/", "/so-101-urdf/meshes/" ); console.log(`🔗 Modified URL (problematic path): ${modifiedUrl}`); return modifiedUrl; } // Handle relative paths that might need mesh folder prefix if ( url.endsWith(".stl") && !url.startsWith("/") && !url.startsWith("http") ) { const modifiedUrl = `/so-101-urdf/meshes/${url}`; console.log(`🔗 Modified URL (relative): ${modifiedUrl}`); return modifiedUrl; } console.log(`🔗 Unmodified URL: ${url}`); return url; }, []); // 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, true); viewerRef.current = viewer; // Store reference to the viewer // Setup mesh loading function with appropriate URL modifier const activeUrlModifier = isDefaultModel ? defaultUrlModifier : urlModifierFunc; setupMeshLoader(viewer, activeUrlModifier); // Determine which URDF to load - fixed path to match the actual available file const urdfPath = isDefaultModel ? "/so-101-urdf/urdf/so101_new_calib.urdf" : customUrdfPath || ""; // Set the package path for the default model if (isDefaultModel) { packageRef.current = "/"; // Set to root so we can handle full path resolution in URL modifier } // 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 ); // Function to fit the robot to the camera view const fitRobotToView = (viewer: URDFViewerElement) => { if (!viewer || !viewer.robot) { console.log( "[RobotViewer] Cannot fit to view: No viewer or robot available" ); return; } try { // Create a bounding box for the robot const boundingBox = new THREE.Box3().setFromObject(viewer.robot); // Calculate the center of the bounding box const center = new THREE.Vector3(); boundingBox.getCenter(center); // Calculate the size of the bounding box const size = new THREE.Vector3(); boundingBox.getSize(size); // Get the maximum dimension to ensure the entire robot is visible const maxDim = Math.max(size.x, size.y, size.z); // Position camera to see the center of the model viewer.camera.position.copy(center); // Move the camera back to see the entire robot // Use the model's up direction to determine which axis to move along const upVector = new THREE.Vector3(); if (viewer.up === "+Z" || viewer.up === "Z") { upVector.set(1, 1, 1); // Move back in a diagonal } else if (viewer.up === "+Y" || viewer.up === "Y") { upVector.set(1, 1, 1); // Move back in a diagonal } else { upVector.set(1, 1, 1); // Default direction } // Normalize the vector and multiply by the size upVector.normalize().multiplyScalar(maxDim * 1.3); viewer.camera.position.add(upVector); // Make the camera look at the center of the model viewer.controls.target.copy(center); // Update controls and mark for redraw viewer.controls.update(); viewer.redraw(); console.log("[RobotViewer] Robot auto-fitted to view"); } catch (error) { console.error("[RobotViewer] Error fitting robot to view:", error); } }; // Add event listener for when the robot is loaded to auto-fit to view const onRobotLoad = () => { fitRobotToView(viewer); }; // 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; } } // Auto-fit the robot to view when the model is processed onRobotLoad(); }; 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, defaultUrlModifier, alternativeUrdfModels, ]); return (
{/* Joint highlight indicator */} {highlightedJoint && (
Joint: {highlightedJoint}
)} {/* WebSocket connection status */} {isDefaultModel && (
{isWebSocketConnected ? "Live Robot Data" : "Disconnected"}
)}
); }; export default UrdfViewer;