leLab / src /components /UrdfViewer.tsx
jurmy24's picture
fix urdf viewer
f177fa3
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<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);
// 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<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]);
// 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 (
<div
className={cn(
"w-full h-full transition-all duration-300 ease-in-out relative",
"bg-gradient-to-br from-gray-900 to-gray-800"
)}
>
<div ref={containerRef} className="w-full h-full" />
{/* 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>
)}
{/* WebSocket connection status */}
{isDefaultModel && (
<div className="absolute top-4 right-4 z-10">
<div
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono ${
isWebSocketConnected
? "bg-green-900/70 text-green-300"
: "bg-red-900/70 text-red-300"
}`}
>
<div
className={`w-2 h-2 rounded-full ${
isWebSocketConnected ? "bg-green-400" : "bg-red-400"
}`}
/>
{isWebSocketConnected ? "Live Robot Data" : "Disconnected"}
</div>
</div>
)}
</div>
);
};
export default UrdfViewer;