Spaces:
Running
Running
import { | |
LoadingManager, | |
Object3D, | |
PerspectiveCamera, | |
Vector3, | |
Color, | |
AmbientLight, | |
DirectionalLight, | |
Scene, | |
} from "three"; | |
import { toast } from "@/components/ui/sonner"; | |
import { loadMeshFile } from "./meshLoaders"; | |
// Define the interface for the URDF viewer element | |
export interface URDFViewerElement extends HTMLElement { | |
setJointValue: (joint: string, value: number) => void; | |
loadMeshFunc?: ( | |
path: string, | |
manager: LoadingManager, | |
done: (result: Object3D | null, err?: Error) => void | |
) => void; | |
// Extended properties for camera fitting | |
camera: PerspectiveCamera; | |
controls: { | |
target: Vector3; | |
update: () => void; | |
}; | |
robot: Object3D; | |
redraw: () => void; | |
up: string; | |
scene: Scene; | |
} | |
/** | |
* Creates and configures a URDF viewer element | |
*/ | |
export function createUrdfViewer( | |
container: HTMLDivElement, | |
isDarkMode: boolean | |
): URDFViewerElement { | |
// Clear any existing content | |
container.innerHTML = ""; | |
// Create the urdf-viewer element | |
const viewer = document.createElement("urdf-viewer") as URDFViewerElement; | |
viewer.classList.add("w-full", "h-full"); | |
// Add the element to the container | |
container.appendChild(viewer); | |
// Set initial viewer properties | |
viewer.setAttribute("up", "Z"); | |
setViewerColor(viewer, isDarkMode ? "#2c2b3a" : "#eff4ff"); | |
viewer.setAttribute("highlight-color", isDarkMode ? "#df6dd4" : "#b05ffe"); | |
viewer.setAttribute("auto-redraw", "true"); | |
// viewer.setAttribute("display-shadow", ""); // Enable shadows | |
// Add ambient light to the scene | |
const ambientLight = new AmbientLight(0xd6d6d6, 1); // Increased intensity to 0.4 | |
viewer.scene.add(ambientLight); | |
// Add directional light for better shadows and depth | |
const directionalLight = new DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(5, 30, 5); | |
directionalLight.castShadow = true; | |
viewer.scene.add(directionalLight); | |
// Camera position is no longer adjusted automatically to prevent auto-zooming. | |
// The user can control the view with the mouse. | |
return viewer; | |
} | |
/** | |
* Setup mesh loading function for URDF viewer | |
*/ | |
export function setupMeshLoader( | |
viewer: URDFViewerElement, | |
urlModifierFunc: ((url: string) => string) | null | |
): void { | |
if ("loadMeshFunc" in viewer) { | |
viewer.loadMeshFunc = ( | |
path: string, | |
manager: LoadingManager, | |
done: (result: Object3D | null, err?: Error) => void | |
) => { | |
// Apply URL modifier if available (for custom uploads) | |
const modifiedPath = urlModifierFunc ? urlModifierFunc(path) : path; | |
// If loading fails, log the error but continue | |
try { | |
loadMeshFile(modifiedPath, manager, (result, err) => { | |
if (err) { | |
console.warn(`Error loading mesh ${modifiedPath}:`, err); | |
// Try to continue with other meshes | |
done(null); | |
} else { | |
done(result); | |
} | |
}); | |
} catch (err) { | |
console.error(`Exception loading mesh ${modifiedPath}:`, err); | |
done(null, err as Error); | |
} | |
}; | |
} | |
} | |
/** | |
* Setup event handlers for joint highlighting | |
*/ | |
export function setupJointHighlighting( | |
viewer: URDFViewerElement, | |
setHighlightedJoint: (joint: string | null) => void | |
): () => void { | |
const onJointMouseover = (e: Event) => { | |
const customEvent = e as CustomEvent; | |
setHighlightedJoint(customEvent.detail); | |
}; | |
const onJointMouseout = () => { | |
setHighlightedJoint(null); | |
}; | |
// Add event listeners | |
viewer.addEventListener("joint-mouseover", onJointMouseover); | |
viewer.addEventListener("joint-mouseout", onJointMouseout); | |
// Return cleanup function | |
return () => { | |
viewer.removeEventListener("joint-mouseover", onJointMouseover); | |
viewer.removeEventListener("joint-mouseout", onJointMouseout); | |
}; | |
} | |
/** | |
* Setup model loading and error handling | |
*/ | |
export function setupModelLoading( | |
viewer: URDFViewerElement, | |
urdfPath: string, | |
packagePath: string, | |
setCustomUrdfPath: (path: string) => void, | |
alternativeRobotModels: string[] = [] // Add parameter for alternative models | |
): () => void { | |
// Add XML content type hint for blob URLs | |
const loadPath = | |
urdfPath.startsWith("blob:") && !urdfPath.includes("#.") | |
? urdfPath + "#.urdf" // Add extension hint if it's a blob URL | |
: urdfPath; | |
// Set the URDF path | |
viewer.setAttribute("urdf", loadPath); | |
viewer.setAttribute("package", packagePath); | |
// Handle error loading | |
const onLoadError = () => { | |
// toast.error("Failed to load model", { | |
// description: "There was an error loading the URDF model.", | |
// duration: 3000, | |
// }); | |
// Use the provided alternativeRobotModels instead of the global window object | |
if (alternativeRobotModels.length > 0) { | |
const nextModel = alternativeRobotModels[0]; | |
if (nextModel) { | |
setCustomUrdfPath(nextModel); | |
toast.info("Trying alternative model...", { | |
description: `First model failed to load. Trying ${ | |
nextModel.split("/").pop() || "alternative model" | |
}`, | |
duration: 2000, | |
}); | |
} | |
} | |
}; | |
viewer.addEventListener("error", onLoadError); | |
// The 'urdf-processed' event that handled auto-zooming has been removed. | |
// Return cleanup function | |
return () => { | |
viewer.removeEventListener("error", onLoadError); | |
}; | |
} | |
/** | |
* Sets the background color of the URDF viewer | |
*/ | |
export function setViewerColor(viewer: URDFViewerElement, color: string): void { | |
// Set the ambient color for the scene | |
// viewer.setAttribute("ambient-color", color); | |
// Set the background color on the viewer's parent container | |
const container = viewer.parentElement; | |
if (container) { | |
container.style.backgroundColor = color; | |
} | |
} | |
/** | |
* Updates the viewer's colors based on the current theme | |
*/ | |
export function updateViewerTheme( | |
viewer: URDFViewerElement, | |
isDarkMode: boolean | |
): void { | |
// Update the ambient color | |
setViewerColor(viewer, isDarkMode ? "#2c2b3a" : "#eff4ff"); | |
viewer.setAttribute("highlight-color", isDarkMode ? "#df6dd4" : "#b05ffe"); | |
// // Update the ambient light intensity based on theme | |
// viewer.scene.traverse((object) => { | |
// if (object instanceof AmbientLight) { | |
// object.intensity = isDarkMode ? 0.4 : 0.6; // Brighter in light mode | |
// } | |
// }); | |
} | |