| import { |
| LoadingManager, |
| Object3D, |
| PerspectiveCamera, |
| Vector3, |
| Color, |
| AmbientLight, |
| DirectionalLight, |
| Scene, |
| } from "three"; |
| import { toast } from "@/components/ui/sonner"; |
| import { loadMeshFile } from "./meshLoaders"; |
|
|
| |
| export interface URDFViewerElement extends HTMLElement { |
| setJointValue: (joint: string, value: number) => void; |
| loadMeshFunc?: ( |
| path: string, |
| manager: LoadingManager, |
| done: (result: Object3D | null, err?: Error) => void |
| ) => void; |
|
|
| |
| camera: PerspectiveCamera; |
| controls: { |
| target: Vector3; |
| update: () => void; |
| }; |
| robot: Object3D; |
| redraw: () => void; |
| up: string; |
| scene: Scene; |
| } |
|
|
| |
| |
| |
| export function createUrdfViewer( |
| container: HTMLDivElement, |
| isDarkMode: boolean |
| ): URDFViewerElement { |
| |
| container.innerHTML = ""; |
|
|
| |
| const viewer = document.createElement("urdf-viewer") as URDFViewerElement; |
| viewer.classList.add("w-full", "h-full"); |
|
|
| |
| container.appendChild(viewer); |
|
|
| |
| viewer.setAttribute("up", "Z"); |
| setViewerColor(viewer, isDarkMode ? "#2c2b3a" : "#eff4ff"); |
| viewer.setAttribute("highlight-color", isDarkMode ? "#df6dd4" : "#b05ffe"); |
| viewer.setAttribute("auto-redraw", "true"); |
| |
|
|
| |
| const ambientLight = new AmbientLight(0xd6d6d6, 1); |
| viewer.scene.add(ambientLight); |
|
|
| |
| const directionalLight = new DirectionalLight(0xffffff, 0.8); |
| directionalLight.position.set(5, 30, 5); |
| directionalLight.castShadow = true; |
| viewer.scene.add(directionalLight); |
|
|
| |
| |
|
|
| return 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 |
| ) => { |
| |
| const modifiedPath = urlModifierFunc ? urlModifierFunc(path) : path; |
|
|
| |
| try { |
| loadMeshFile(modifiedPath, manager, (result, err) => { |
| if (err) { |
| console.warn(`Error loading mesh ${modifiedPath}:`, err); |
| |
| done(null); |
| } else { |
| done(result); |
| } |
| }); |
| } catch (err) { |
| console.error(`Exception loading mesh ${modifiedPath}:`, err); |
| done(null, err as Error); |
| } |
| }; |
| } |
| } |
|
|
| |
| |
| |
| 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); |
| }; |
|
|
| |
| viewer.addEventListener("joint-mouseover", onJointMouseover); |
| viewer.addEventListener("joint-mouseout", onJointMouseout); |
|
|
| |
| return () => { |
| viewer.removeEventListener("joint-mouseover", onJointMouseover); |
| viewer.removeEventListener("joint-mouseout", onJointMouseout); |
| }; |
| } |
|
|
| |
| |
| |
| export function setupModelLoading( |
| viewer: URDFViewerElement, |
| urdfPath: string, |
| packagePath: string, |
| setCustomUrdfPath: (path: string) => void, |
| alternativeRobotModels: string[] = [] |
| ): () => void { |
| |
| const loadPath = |
| urdfPath.startsWith("blob:") && !urdfPath.includes("#.") |
| ? urdfPath + "#.urdf" |
| : urdfPath; |
|
|
| |
| viewer.setAttribute("urdf", loadPath); |
| viewer.setAttribute("package", packagePath); |
|
|
| |
| const onLoadError = () => { |
| |
| |
| |
| |
|
|
| |
| 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); |
| |
|
|
| |
| return () => { |
| viewer.removeEventListener("error", onLoadError); |
| }; |
| } |
|
|
| |
| |
| |
| export function setViewerColor(viewer: URDFViewerElement, color: string): void { |
| |
| |
|
|
| |
| const container = viewer.parentElement; |
| if (container) { |
| container.style.backgroundColor = color; |
| } |
| } |
|
|
| |
| |
| |
| export function updateViewerTheme( |
| viewer: URDFViewerElement, |
| isDarkMode: boolean |
| ): void { |
| |
| setViewerColor(viewer, isDarkMode ? "#2c2b3a" : "#eff4ff"); |
| viewer.setAttribute("highlight-color", isDarkMode ? "#df6dd4" : "#b05ffe"); |
|
|
| |
| |
| |
| |
| |
| |
| } |
|
|