Spaces:
Running
Running
File size: 7,494 Bytes
9d3c32a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
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);
// Set initial camera position for more zoomed-in view
// Wait for the viewer to be fully initialized before adjusting camera
setTimeout(() => {
if (viewer.camera) {
// Move camera closer to the robot for a more zoomed-in initial view
viewer.camera.position.set(0.5, 0.3, 0.5);
viewer.camera.lookAt(0, 0.2, 0); // Look at center of robot
// Update controls target if available
if (viewer.controls) {
viewer.controls.target.set(0, 0.2, 0);
viewer.controls.update();
}
// Trigger a redraw
if (viewer.redraw) {
viewer.redraw();
}
}
}, 100);
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 successful loading and set initial zoom
const onLoadSuccess = () => {
// Set more zoomed-in camera position after model loads
setTimeout(() => {
if (viewer.camera && viewer.robot) {
// Position camera closer for better initial view
viewer.camera.position.set(0.4, 0.25, 0.4);
viewer.camera.lookAt(0, 0.15, 0);
if (viewer.controls) {
viewer.controls.target.set(0, 0.15, 0);
viewer.controls.update();
}
if (viewer.redraw) {
viewer.redraw();
}
}
}, 50);
};
// 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);
viewer.addEventListener("urdf-processed", onLoadSuccess);
// Return cleanup function
return () => {
viewer.removeEventListener("error", onLoadError);
viewer.removeEventListener("urdf-processed", onLoadSuccess);
};
}
/**
* 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
// }
// });
}
|