Spaces:
Running
Running
refactor: rename URDF to Urdf
Browse files- .dockerignore +1 -1
- Dockerfile +9 -9
- README.md +1 -1
- viewer/README.md +1 -1
- viewer/index.html +1 -1
- viewer/public/urdf/SO_5DOF_ARM100_05d/urdf/SO_5DOF_ARM100_05d.urdf +1 -1
- viewer/src/components/UrdfSelectionModalContainer.tsx +1 -1
- viewer/src/components/layout/Layout.tsx +3 -3
- viewer/src/components/ui/UrdfSelectionModal.tsx +2 -2
- viewer/src/contexts/DragAndDropContext.tsx +2 -2
- viewer/src/contexts/UrdfContext.tsx +56 -56
- viewer/src/hooks/useUrdf.ts +1 -1
- viewer/src/hooks/useUrdfData.ts +3 -3
- viewer/src/hooks/useUrdfParser.ts +13 -13
- viewer/src/lib/UrdfDragAndDrop.ts +47 -45
- viewer/src/lib/types.ts +4 -4
- viewer/src/lib/urdfAnimationHelpers.ts +6 -6
- viewer/src/lib/urdfViewerHelpers.ts +12 -12
- viewer/supabase/functions/README.md +4 -3
- viewer/supabase/functions/urdf-parser/index.ts +10 -10
.dockerignore
CHANGED
|
@@ -47,7 +47,7 @@ Thumbs.db
|
|
| 47 |
Dockerfile
|
| 48 |
.dockerignore
|
| 49 |
|
| 50 |
-
#
|
| 51 |
*.STL
|
| 52 |
*.stl
|
| 53 |
|
|
|
|
| 47 |
Dockerfile
|
| 48 |
.dockerignore
|
| 49 |
|
| 50 |
+
# Urdf files and STLs (if you don't need them in the image)
|
| 51 |
*.STL
|
| 52 |
*.stl
|
| 53 |
|
Dockerfile
CHANGED
|
@@ -22,15 +22,15 @@ RUN npm install
|
|
| 22 |
# Copy the entire viewer directory
|
| 23 |
COPY --chown=user viewer/ .
|
| 24 |
|
| 25 |
-
# Debug: List all files in the src/components directory
|
| 26 |
-
RUN echo "=== Listing src/components directory ===" && \
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
|
| 35 |
# Ensure proper file permissions
|
| 36 |
RUN chmod -R 755 .
|
|
|
|
| 22 |
# Copy the entire viewer directory
|
| 23 |
COPY --chown=user viewer/ .
|
| 24 |
|
| 25 |
+
# # Debug: List all files in the src/components directory
|
| 26 |
+
# RUN echo "=== Listing src/components directory ===" && \
|
| 27 |
+
# ls -la src/components && \
|
| 28 |
+
# echo "=== Listing UrdfViewer file specifically ===" && \
|
| 29 |
+
# ls -la src/components/UrdfViewer* && \
|
| 30 |
+
# echo "=== Current working directory ===" && \
|
| 31 |
+
# pwd && \
|
| 32 |
+
# echo "=== Directory structure ===" && \
|
| 33 |
+
# find . -type f -name "*.tsx" | sort
|
| 34 |
|
| 35 |
# Ensure proper file permissions
|
| 36 |
RUN chmod -R 755 .
|
README.md
CHANGED
|
@@ -6,7 +6,7 @@ colorTo: indigo
|
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
-
short_description: Upload a
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
+
short_description: Upload a Urdf folder to view and interact with your robot.
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
viewer/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
**URL**: https://urdf-web-visualizer.lovable.app/
|
| 4 |
|
|
|
|
| 1 |
+
# Urdf Web Visualizer
|
| 2 |
|
| 3 |
**URL**: https://urdf-web-visualizer.lovable.app/
|
| 4 |
|
viewer/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>placeholder</title>
|
| 7 |
-
<meta name="description" content="A simple tool to visualize
|
| 8 |
<meta name="author" content="Victor Oldensand" />
|
| 9 |
<meta property="og:image" content="/og-image.png" />
|
| 10 |
</head>
|
|
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>placeholder</title>
|
| 7 |
+
<meta name="description" content="A simple tool to visualize Urdf files." />
|
| 8 |
<meta name="author" content="Victor Oldensand" />
|
| 9 |
<meta property="og:image" content="/og-image.png" />
|
| 10 |
</head>
|
viewer/public/urdf/SO_5DOF_ARM100_05d/urdf/SO_5DOF_ARM100_05d.urdf
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
-
<!-- This
|
| 3 |
Commit Version: 1.6.0-1-g15f4949 Build Version: 1.6.7594.29634
|
| 4 |
For more information, please see http://wiki.ros.org/sw_urdf_exporter -->
|
| 5 |
<robot
|
|
|
|
| 1 |
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<!-- This Urdf was automatically created by SolidWorks to Urdf Exporter! Originally created by Stephen Brawner ([email protected])
|
| 3 |
Commit Version: 1.6.0-1-g15f4949 Build Version: 1.6.7594.29634
|
| 4 |
For more information, please see http://wiki.ros.org/sw_urdf_exporter -->
|
| 5 |
<robot
|
viewer/src/components/UrdfSelectionModalContainer.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useUrdf } from "@/hooks/useUrdf";
|
|
| 2 |
import { UrdfSelectionModal } from "@/components/ui/UrdfSelectionModal";
|
| 3 |
|
| 4 |
/**
|
| 5 |
-
* Container component that manages the
|
| 6 |
* This is meant to be placed in the application layout to ensure the modal
|
| 7 |
* is accessible throughout the application without nesting issues.
|
| 8 |
*/
|
|
|
|
| 2 |
import { UrdfSelectionModal } from "@/components/ui/UrdfSelectionModal";
|
| 3 |
|
| 4 |
/**
|
| 5 |
+
* Container component that manages the Urdf selection modal.
|
| 6 |
* This is meant to be placed in the application layout to ensure the modal
|
| 7 |
* is accessible throughout the application without nesting issues.
|
| 8 |
*/
|
viewer/src/components/layout/Layout.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import React, { useState, useEffect } from "react";
|
| 2 |
-
import
|
| 3 |
import { Sidebar, SidebarBody } from "@/components/ui/sidebar";
|
| 4 |
import { LayoutDashboard, PanelLeft, RotateCcw } from "lucide-react";
|
| 5 |
import { useUrdf } from "@/hooks/useUrdf";
|
|
@@ -42,7 +42,7 @@ const Layout: React.FC = () => {
|
|
| 42 |
<div className="absolute inset-0 overflow-hidden">
|
| 43 |
{/* Main content that fills the container */}
|
| 44 |
<div className="w-full h-full">
|
| 45 |
-
<
|
| 46 |
</div>
|
| 47 |
</div>
|
| 48 |
);
|
|
@@ -52,7 +52,7 @@ const Layout: React.FC = () => {
|
|
| 52 |
<div className="relative w-full h-full overflow-hidden">
|
| 53 |
{/* Main content that fills the container */}
|
| 54 |
<div className="w-full h-full">
|
| 55 |
-
<
|
| 56 |
</div>
|
| 57 |
{/* Sidebar positioned above with z-index, with top padding for header */}
|
| 58 |
<div
|
|
|
|
| 1 |
import React, { useState, useEffect } from "react";
|
| 2 |
+
import UrdfViewer from "@/components/UrdfViewer";
|
| 3 |
import { Sidebar, SidebarBody } from "@/components/ui/sidebar";
|
| 4 |
import { LayoutDashboard, PanelLeft, RotateCcw } from "lucide-react";
|
| 5 |
import { useUrdf } from "@/hooks/useUrdf";
|
|
|
|
| 42 |
<div className="absolute inset-0 overflow-hidden">
|
| 43 |
{/* Main content that fills the container */}
|
| 44 |
<div className="w-full h-full">
|
| 45 |
+
<UrdfViewer />
|
| 46 |
</div>
|
| 47 |
</div>
|
| 48 |
);
|
|
|
|
| 52 |
<div className="relative w-full h-full overflow-hidden">
|
| 53 |
{/* Main content that fills the container */}
|
| 54 |
<div className="w-full h-full">
|
| 55 |
+
<UrdfViewer />
|
| 56 |
</div>
|
| 57 |
{/* Sidebar positioned above with z-index, with top padding for header */}
|
| 58 |
<div
|
viewer/src/components/ui/UrdfSelectionModal.tsx
CHANGED
|
@@ -34,9 +34,9 @@ export function UrdfSelectionModal({
|
|
| 34 |
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
| 35 |
<DialogContent className="sm:max-w-[600px]">
|
| 36 |
<DialogHeader>
|
| 37 |
-
<DialogTitle>Select
|
| 38 |
<DialogDescription>
|
| 39 |
-
Multiple
|
| 40 |
visualize.
|
| 41 |
</DialogDescription>
|
| 42 |
</DialogHeader>
|
|
|
|
| 34 |
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
| 35 |
<DialogContent className="sm:max-w-[600px]">
|
| 36 |
<DialogHeader>
|
| 37 |
+
<DialogTitle>Select Urdf Model</DialogTitle>
|
| 38 |
<DialogDescription>
|
| 39 |
+
Multiple Urdf files were found. Please select the model you want to
|
| 40 |
visualize.
|
| 41 |
</DialogDescription>
|
| 42 |
</DialogHeader>
|
viewer/src/contexts/DragAndDropContext.tsx
CHANGED
|
@@ -28,7 +28,7 @@ export const DragAndDropProvider: React.FC<DragAndDropProviderProps> = ({
|
|
| 28 |
}) => {
|
| 29 |
const [isDragging, setIsDragging] = useState(false);
|
| 30 |
|
| 31 |
-
// Get the
|
| 32 |
const { urdfProcessor, processUrdfFiles } = useUrdf();
|
| 33 |
|
| 34 |
const handleDragOver = (e: DragEvent) => {
|
|
@@ -111,7 +111,7 @@ export const DragAndDropProvider: React.FC<DragAndDropProviderProps> = ({
|
|
| 111 |
{isDragging && (
|
| 112 |
<div className="fixed inset-0 bg-primary/10 pointer-events-none z-50 flex items-center justify-center">
|
| 113 |
<div className="bg-background p-8 rounded-lg shadow-lg text-center">
|
| 114 |
-
<div className="text-3xl font-bold mb-4">Drop
|
| 115 |
<p className="text-muted-foreground">
|
| 116 |
Release to upload your robot model
|
| 117 |
</p>
|
|
|
|
| 28 |
}) => {
|
| 29 |
const [isDragging, setIsDragging] = useState(false);
|
| 30 |
|
| 31 |
+
// Get the Urdf context
|
| 32 |
const { urdfProcessor, processUrdfFiles } = useUrdf();
|
| 33 |
|
| 34 |
const handleDragOver = (e: DragEvent) => {
|
|
|
|
| 111 |
{isDragging && (
|
| 112 |
<div className="fixed inset-0 bg-primary/10 pointer-events-none z-50 flex items-center justify-center">
|
| 113 |
<div className="bg-background p-8 rounded-lg shadow-lg text-center">
|
| 114 |
+
<div className="text-3xl font-bold mb-4">Drop Urdf Files Here</div>
|
| 115 |
<p className="text-muted-foreground">
|
| 116 |
Release to upload your robot model
|
| 117 |
</p>
|
viewer/src/contexts/UrdfContext.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import { UrdfData, UrdfFileModel } from "@/lib/types";
|
|
| 13 |
import { useDefaultRobotData } from "@/hooks/useDefaultRobotData";
|
| 14 |
import { RobotAnimationConfig } from "@/lib/types";
|
| 15 |
|
| 16 |
-
// Define the result interface for
|
| 17 |
interface UrdfDetectionResult {
|
| 18 |
hasUrdf: boolean;
|
| 19 |
modelName?: string;
|
|
@@ -52,7 +52,7 @@ export type UrdfContextType = {
|
|
| 52 |
// These properties are kept for backward compatibility but are considered
|
| 53 |
// implementation details and should not be used directly in components.
|
| 54 |
// TODO: Remove these next three once the time is right
|
| 55 |
-
parsedRobotData: UrdfData | null; // Data from parsed
|
| 56 |
customModelName: string;
|
| 57 |
customModelDescription: string;
|
| 58 |
};
|
|
@@ -68,7 +68,7 @@ interface UrdfProviderProps {
|
|
| 68 |
}
|
| 69 |
|
| 70 |
export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
| 71 |
-
// State for
|
| 72 |
const [urdfProcessor, setUrdfProcessor] = useState<UrdfProcessor | null>(
|
| 73 |
null
|
| 74 |
);
|
|
@@ -81,7 +81,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 81 |
[]
|
| 82 |
);
|
| 83 |
|
| 84 |
-
// State for the
|
| 85 |
const [isSelectionModalOpen, setIsSelectionModalOpen] = useState(false);
|
| 86 |
const [urdfModelOptions, setUrdfModelOptions] = useState<UrdfFileModel[]>([]);
|
| 87 |
|
|
@@ -103,34 +103,34 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 103 |
// Compute the current robot data based on model state
|
| 104 |
const currentRobotData = isDefaultModel ? defaultRobotData : parsedRobotData;
|
| 105 |
|
| 106 |
-
// Fetch the default
|
| 107 |
useEffect(() => {
|
| 108 |
// Only fetch if we don't have content and we're using the default model
|
| 109 |
if (isDefaultModel && !urdfContent) {
|
| 110 |
const fetchDefaultUrdf = async () => {
|
| 111 |
try {
|
| 112 |
-
// Path to the default T12
|
| 113 |
const defaultUrdfPath =
|
| 114 |
-
"/urdf/SO_5DOF_ARM100_05d/urdf/SO_5DOF_ARM100_05d.
|
| 115 |
|
| 116 |
-
// Fetch the
|
| 117 |
const response = await fetch(defaultUrdfPath);
|
| 118 |
|
| 119 |
if (!response.ok) {
|
| 120 |
throw new Error(
|
| 121 |
-
`Failed to fetch default
|
| 122 |
);
|
| 123 |
}
|
| 124 |
|
| 125 |
const defaultUrdfContent = await response.text();
|
| 126 |
console.log(
|
| 127 |
-
`📄 Default
|
| 128 |
);
|
| 129 |
|
| 130 |
-
// Set the
|
| 131 |
setUrdfContent(defaultUrdfContent);
|
| 132 |
} catch (error) {
|
| 133 |
-
console.error("❌ Error loading default
|
| 134 |
}
|
| 135 |
};
|
| 136 |
|
|
@@ -170,7 +170,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 170 |
});
|
| 171 |
}, []);
|
| 172 |
|
| 173 |
-
// Register a callback for
|
| 174 |
const onUrdfDetected = useCallback(
|
| 175 |
(callback: (result: UrdfDetectionResult) => void) => {
|
| 176 |
urdfCallbacksRef.current.push(callback);
|
|
@@ -184,7 +184,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 184 |
[]
|
| 185 |
);
|
| 186 |
|
| 187 |
-
// Register a
|
| 188 |
const registerUrdfProcessor = useCallback((processor: UrdfProcessor) => {
|
| 189 |
setUrdfProcessor(processor);
|
| 190 |
}, []);
|
|
@@ -192,11 +192,11 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 192 |
// Internal function to notify callbacks and update central state
|
| 193 |
const notifyUrdfCallbacks = useCallback(
|
| 194 |
(result: UrdfDetectionResult) => {
|
| 195 |
-
console.log("📣 Notifying
|
| 196 |
|
| 197 |
// Update our internal state based on the result
|
| 198 |
if (result.hasUrdf) {
|
| 199 |
-
// Always ensure we set isDefaultModel to false when we have a
|
| 200 |
setIsDefaultModel(false);
|
| 201 |
|
| 202 |
if (result.parsedData) {
|
|
@@ -240,7 +240,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 240 |
setParsedRobotData(minimalData);
|
| 241 |
}
|
| 242 |
} else {
|
| 243 |
-
// If no
|
| 244 |
resetToDefaultModel();
|
| 245 |
}
|
| 246 |
|
|
@@ -250,7 +250,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 250 |
[resetToDefaultModel]
|
| 251 |
);
|
| 252 |
|
| 253 |
-
// Helper function to process the selected
|
| 254 |
const processSelectedUrdf = useCallback(
|
| 255 |
async (model: UrdfFileModel) => {
|
| 256 |
if (!urdfProcessor) return;
|
|
@@ -267,12 +267,12 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 267 |
.filter((item) => item !== null);
|
| 268 |
|
| 269 |
if (files.length === 0) {
|
| 270 |
-
console.error("❌ Could not find file for selected
|
| 271 |
return;
|
| 272 |
}
|
| 273 |
|
| 274 |
-
// Show a toast notification that we're parsing the
|
| 275 |
-
const parsingToast = toast.loading("Analyzing
|
| 276 |
description: "Extracting robot information",
|
| 277 |
duration: 10000, // Long duration since we'll dismiss it manually
|
| 278 |
});
|
|
@@ -295,23 +295,23 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 295 |
}
|
| 296 |
);
|
| 297 |
|
| 298 |
-
// Read the
|
| 299 |
const urdfContent = await readUrdfFileContent(file);
|
| 300 |
|
| 301 |
console.log(
|
| 302 |
-
`📏
|
| 303 |
);
|
| 304 |
|
| 305 |
-
// Store the
|
| 306 |
setUrdfContent(urdfContent);
|
| 307 |
|
| 308 |
-
// Parse the
|
| 309 |
const parseResult = await parseUrdf(urdfContent);
|
| 310 |
|
| 311 |
// Dismiss the toast
|
| 312 |
toast.dismiss(parsingToast);
|
| 313 |
|
| 314 |
-
// Always set isDefaultModel to false when processing a custom
|
| 315 |
setIsDefaultModel(false);
|
| 316 |
|
| 317 |
if (parseResult) {
|
|
@@ -345,7 +345,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 345 |
// Update the state with enhanced parsed data
|
| 346 |
setParsedRobotData(enhancedParsedData);
|
| 347 |
|
| 348 |
-
toast.success("
|
| 349 |
description: `Model: ${modelDisplayName}`,
|
| 350 |
duration: 3000,
|
| 351 |
});
|
|
@@ -373,7 +373,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 373 |
setCustomModelDescription(minimalData.description);
|
| 374 |
setParsedRobotData(minimalData);
|
| 375 |
|
| 376 |
-
toast.warning("
|
| 377 |
description: "Could not fully analyze the robot structure",
|
| 378 |
duration: 3000,
|
| 379 |
});
|
|
@@ -387,9 +387,9 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 387 |
}
|
| 388 |
} catch (error) {
|
| 389 |
// Error case
|
| 390 |
-
console.error("❌ Error processing selected
|
| 391 |
toast.dismiss(parsingToast);
|
| 392 |
-
toast.error("Error analyzing
|
| 393 |
description: `Error: ${
|
| 394 |
error instanceof Error ? error.message : String(error)
|
| 395 |
}`,
|
|
@@ -403,11 +403,11 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 403 |
[urdfBlobUrls, urdfProcessor, parseUrdf, notifyUrdfCallbacks]
|
| 404 |
);
|
| 405 |
|
| 406 |
-
// Function to handle selecting a
|
| 407 |
const selectUrdfModel = useCallback(
|
| 408 |
(model: UrdfFileModel) => {
|
| 409 |
if (!urdfProcessor) {
|
| 410 |
-
console.error("❌ No
|
| 411 |
return;
|
| 412 |
}
|
| 413 |
|
|
@@ -425,7 +425,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 425 |
?.replace(/\.urdf$/i, "") ||
|
| 426 |
"Unknown";
|
| 427 |
|
| 428 |
-
// Load the selected
|
| 429 |
urdfProcessor.loadUrdf(model.blobUrl);
|
| 430 |
|
| 431 |
// Update our state immediately even before parsing
|
|
@@ -451,7 +451,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 451 |
[urdfProcessor, notifyUrdfCallbacks, processSelectedUrdf]
|
| 452 |
);
|
| 453 |
|
| 454 |
-
// Process
|
| 455 |
const processUrdfFiles = useCallback(
|
| 456 |
async (files: Record<string, File>, availableModels: string[]) => {
|
| 457 |
// Clear previous blob URLs to prevent memory leaks
|
|
@@ -461,10 +461,10 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 461 |
setUrdfModelOptions([]);
|
| 462 |
|
| 463 |
try {
|
| 464 |
-
// Check if we have any
|
| 465 |
if (availableModels.length > 0 && urdfProcessor) {
|
| 466 |
console.log(
|
| 467 |
-
`🤖 Found ${availableModels.length}
|
| 468 |
availableModels
|
| 469 |
);
|
| 470 |
|
|
@@ -495,7 +495,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 495 |
|
| 496 |
// If there's only one model, use it directly
|
| 497 |
if (availableModels.length === 1) {
|
| 498 |
-
// Extract model name from the
|
| 499 |
const fileName = availableModels[0].split("/").pop() || "";
|
| 500 |
const modelName = fileName.replace(/\.urdf$/i, "");
|
| 501 |
console.log(`📄 Using model: ${modelName} (${fileName})`);
|
|
@@ -503,21 +503,21 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 503 |
// Use the blob URL instead of the file path
|
| 504 |
const blobUrl = newUrdfBlobUrls[availableModels[0]];
|
| 505 |
if (blobUrl) {
|
| 506 |
-
console.log(`🔗 Using blob URL for
|
| 507 |
urdfProcessor.loadUrdf(blobUrl);
|
| 508 |
|
| 509 |
// Immediately update model state
|
| 510 |
setIsDefaultModel(false);
|
| 511 |
setCustomModelName(modelName);
|
| 512 |
|
| 513 |
-
// Process the
|
| 514 |
if (files[availableModels[0]]) {
|
| 515 |
console.log(
|
| 516 |
-
"📄 Reading
|
| 517 |
);
|
| 518 |
|
| 519 |
-
// Show a toast notification that we're parsing the
|
| 520 |
-
const parsingToast = toast.loading("Analyzing
|
| 521 |
description: "Extracting robot information",
|
| 522 |
duration: 10000, // Long duration since we'll dismiss it manually
|
| 523 |
});
|
|
@@ -528,10 +528,10 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 528 |
);
|
| 529 |
|
| 530 |
console.log(
|
| 531 |
-
`📏
|
| 532 |
);
|
| 533 |
|
| 534 |
-
// Store the
|
| 535 |
setUrdfContent(urdfContent);
|
| 536 |
|
| 537 |
// Call the parseUrdf function from the hook
|
|
@@ -541,7 +541,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 541 |
toast.dismiss(parsingToast);
|
| 542 |
|
| 543 |
if (parseResult) {
|
| 544 |
-
toast.success("
|
| 545 |
description: `Model: ${modelName}`,
|
| 546 |
duration: 3000,
|
| 547 |
});
|
|
@@ -590,7 +590,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 590 |
setParsedRobotData(minimalData);
|
| 591 |
|
| 592 |
toast.warning(
|
| 593 |
-
"
|
| 594 |
{
|
| 595 |
description:
|
| 596 |
"Could not fully analyze the robot structure",
|
|
@@ -606,9 +606,9 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 606 |
});
|
| 607 |
}
|
| 608 |
} catch (parseError) {
|
| 609 |
-
console.error("❌ Error parsing
|
| 610 |
toast.dismiss(parsingToast);
|
| 611 |
-
toast.error("Error analyzing
|
| 612 |
description: `Error: ${
|
| 613 |
parseError instanceof Error
|
| 614 |
? parseError.message
|
|
@@ -625,7 +625,7 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 625 |
}
|
| 626 |
} else {
|
| 627 |
console.error(
|
| 628 |
-
"❌ Could not find file for
|
| 629 |
availableModels[0]
|
| 630 |
);
|
| 631 |
console.log("📦 Available files:", Object.keys(files));
|
|
@@ -653,13 +653,13 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 653 |
});
|
| 654 |
}
|
| 655 |
} else {
|
| 656 |
-
// Multiple
|
| 657 |
console.log(
|
| 658 |
-
"📋 Multiple
|
| 659 |
);
|
| 660 |
setIsSelectionModalOpen(true);
|
| 661 |
|
| 662 |
-
// Notify that
|
| 663 |
notifyUrdfCallbacks({
|
| 664 |
hasUrdf: true,
|
| 665 |
modelName: "Multiple models available",
|
|
@@ -667,20 +667,20 @@ export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
|
| 667 |
}
|
| 668 |
} else {
|
| 669 |
console.warn(
|
| 670 |
-
"❌ No
|
| 671 |
);
|
| 672 |
notifyUrdfCallbacks({ hasUrdf: false, parsedData: null });
|
| 673 |
|
| 674 |
-
// Reset to default model when no
|
| 675 |
resetToDefaultModel();
|
| 676 |
|
| 677 |
-
toast.error("No
|
| 678 |
description: "Please upload a folder containing a .urdf file.",
|
| 679 |
duration: 3000,
|
| 680 |
});
|
| 681 |
}
|
| 682 |
} catch (error) {
|
| 683 |
-
console.error("❌ Error processing
|
| 684 |
toast.error("Error processing files", {
|
| 685 |
description: `Error: ${
|
| 686 |
error instanceof Error ? error.message : String(error)
|
|
|
|
| 13 |
import { useDefaultRobotData } from "@/hooks/useDefaultRobotData";
|
| 14 |
import { RobotAnimationConfig } from "@/lib/types";
|
| 15 |
|
| 16 |
+
// Define the result interface for Urdf detection
|
| 17 |
interface UrdfDetectionResult {
|
| 18 |
hasUrdf: boolean;
|
| 19 |
modelName?: string;
|
|
|
|
| 52 |
// These properties are kept for backward compatibility but are considered
|
| 53 |
// implementation details and should not be used directly in components.
|
| 54 |
// TODO: Remove these next three once the time is right
|
| 55 |
+
parsedRobotData: UrdfData | null; // Data from parsed Urdf
|
| 56 |
customModelName: string;
|
| 57 |
customModelDescription: string;
|
| 58 |
};
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
export const UrdfProvider: React.FC<UrdfProviderProps> = ({ children }) => {
|
| 71 |
+
// State for Urdf processor
|
| 72 |
const [urdfProcessor, setUrdfProcessor] = useState<UrdfProcessor | null>(
|
| 73 |
null
|
| 74 |
);
|
|
|
|
| 81 |
[]
|
| 82 |
);
|
| 83 |
|
| 84 |
+
// State for the Urdf selection modal
|
| 85 |
const [isSelectionModalOpen, setIsSelectionModalOpen] = useState(false);
|
| 86 |
const [urdfModelOptions, setUrdfModelOptions] = useState<UrdfFileModel[]>([]);
|
| 87 |
|
|
|
|
| 103 |
// Compute the current robot data based on model state
|
| 104 |
const currentRobotData = isDefaultModel ? defaultRobotData : parsedRobotData;
|
| 105 |
|
| 106 |
+
// Fetch the default Urdf content when the component mounts
|
| 107 |
useEffect(() => {
|
| 108 |
// Only fetch if we don't have content and we're using the default model
|
| 109 |
if (isDefaultModel && !urdfContent) {
|
| 110 |
const fetchDefaultUrdf = async () => {
|
| 111 |
try {
|
| 112 |
+
// Path to the default T12 Urdf file
|
| 113 |
const defaultUrdfPath =
|
| 114 |
+
"/urdf/SO_5DOF_ARM100_05d/urdf/SO_5DOF_ARM100_05d.Urdf";
|
| 115 |
|
| 116 |
+
// Fetch the Urdf content
|
| 117 |
const response = await fetch(defaultUrdfPath);
|
| 118 |
|
| 119 |
if (!response.ok) {
|
| 120 |
throw new Error(
|
| 121 |
+
`Failed to fetch default Urdf: ${response.statusText}`
|
| 122 |
);
|
| 123 |
}
|
| 124 |
|
| 125 |
const defaultUrdfContent = await response.text();
|
| 126 |
console.log(
|
| 127 |
+
`📄 Default Urdf content loaded, length: ${defaultUrdfContent.length} characters`
|
| 128 |
);
|
| 129 |
|
| 130 |
+
// Set the Urdf content in state
|
| 131 |
setUrdfContent(defaultUrdfContent);
|
| 132 |
} catch (error) {
|
| 133 |
+
console.error("❌ Error loading default Urdf content:", error);
|
| 134 |
}
|
| 135 |
};
|
| 136 |
|
|
|
|
| 170 |
});
|
| 171 |
}, []);
|
| 172 |
|
| 173 |
+
// Register a callback for Urdf detection
|
| 174 |
const onUrdfDetected = useCallback(
|
| 175 |
(callback: (result: UrdfDetectionResult) => void) => {
|
| 176 |
urdfCallbacksRef.current.push(callback);
|
|
|
|
| 184 |
[]
|
| 185 |
);
|
| 186 |
|
| 187 |
+
// Register a Urdf processor
|
| 188 |
const registerUrdfProcessor = useCallback((processor: UrdfProcessor) => {
|
| 189 |
setUrdfProcessor(processor);
|
| 190 |
}, []);
|
|
|
|
| 192 |
// Internal function to notify callbacks and update central state
|
| 193 |
const notifyUrdfCallbacks = useCallback(
|
| 194 |
(result: UrdfDetectionResult) => {
|
| 195 |
+
console.log("📣 Notifying Urdf callbacks with result:", result);
|
| 196 |
|
| 197 |
// Update our internal state based on the result
|
| 198 |
if (result.hasUrdf) {
|
| 199 |
+
// Always ensure we set isDefaultModel to false when we have a Urdf
|
| 200 |
setIsDefaultModel(false);
|
| 201 |
|
| 202 |
if (result.parsedData) {
|
|
|
|
| 240 |
setParsedRobotData(minimalData);
|
| 241 |
}
|
| 242 |
} else {
|
| 243 |
+
// If no Urdf, reset to default
|
| 244 |
resetToDefaultModel();
|
| 245 |
}
|
| 246 |
|
|
|
|
| 250 |
[resetToDefaultModel]
|
| 251 |
);
|
| 252 |
|
| 253 |
+
// Helper function to process the selected Urdf model
|
| 254 |
const processSelectedUrdf = useCallback(
|
| 255 |
async (model: UrdfFileModel) => {
|
| 256 |
if (!urdfProcessor) return;
|
|
|
|
| 267 |
.filter((item) => item !== null);
|
| 268 |
|
| 269 |
if (files.length === 0) {
|
| 270 |
+
console.error("❌ Could not find file for selected Urdf model");
|
| 271 |
return;
|
| 272 |
}
|
| 273 |
|
| 274 |
+
// Show a toast notification that we're parsing the Urdf
|
| 275 |
+
const parsingToast = toast.loading("Analyzing Urdf model...", {
|
| 276 |
description: "Extracting robot information",
|
| 277 |
duration: 10000, // Long duration since we'll dismiss it manually
|
| 278 |
});
|
|
|
|
| 295 |
}
|
| 296 |
);
|
| 297 |
|
| 298 |
+
// Read the Urdf content
|
| 299 |
const urdfContent = await readUrdfFileContent(file);
|
| 300 |
|
| 301 |
console.log(
|
| 302 |
+
`📏 Urdf content read, length: ${urdfContent.length} characters`
|
| 303 |
);
|
| 304 |
|
| 305 |
+
// Store the Urdf content in state
|
| 306 |
setUrdfContent(urdfContent);
|
| 307 |
|
| 308 |
+
// Parse the Urdf
|
| 309 |
const parseResult = await parseUrdf(urdfContent);
|
| 310 |
|
| 311 |
// Dismiss the toast
|
| 312 |
toast.dismiss(parsingToast);
|
| 313 |
|
| 314 |
+
// Always set isDefaultModel to false when processing a custom Urdf
|
| 315 |
setIsDefaultModel(false);
|
| 316 |
|
| 317 |
if (parseResult) {
|
|
|
|
| 345 |
// Update the state with enhanced parsed data
|
| 346 |
setParsedRobotData(enhancedParsedData);
|
| 347 |
|
| 348 |
+
toast.success("Urdf model loaded successfully", {
|
| 349 |
description: `Model: ${modelDisplayName}`,
|
| 350 |
duration: 3000,
|
| 351 |
});
|
|
|
|
| 373 |
setCustomModelDescription(minimalData.description);
|
| 374 |
setParsedRobotData(minimalData);
|
| 375 |
|
| 376 |
+
toast.warning("Urdf model loaded with limited information", {
|
| 377 |
description: "Could not fully analyze the robot structure",
|
| 378 |
duration: 3000,
|
| 379 |
});
|
|
|
|
| 387 |
}
|
| 388 |
} catch (error) {
|
| 389 |
// Error case
|
| 390 |
+
console.error("❌ Error processing selected Urdf:", error);
|
| 391 |
toast.dismiss(parsingToast);
|
| 392 |
+
toast.error("Error analyzing Urdf", {
|
| 393 |
description: `Error: ${
|
| 394 |
error instanceof Error ? error.message : String(error)
|
| 395 |
}`,
|
|
|
|
| 403 |
[urdfBlobUrls, urdfProcessor, parseUrdf, notifyUrdfCallbacks]
|
| 404 |
);
|
| 405 |
|
| 406 |
+
// Function to handle selecting a Urdf model from the modal
|
| 407 |
const selectUrdfModel = useCallback(
|
| 408 |
(model: UrdfFileModel) => {
|
| 409 |
if (!urdfProcessor) {
|
| 410 |
+
console.error("❌ No Urdf processor available");
|
| 411 |
return;
|
| 412 |
}
|
| 413 |
|
|
|
|
| 425 |
?.replace(/\.urdf$/i, "") ||
|
| 426 |
"Unknown";
|
| 427 |
|
| 428 |
+
// Load the selected Urdf model
|
| 429 |
urdfProcessor.loadUrdf(model.blobUrl);
|
| 430 |
|
| 431 |
// Update our state immediately even before parsing
|
|
|
|
| 451 |
[urdfProcessor, notifyUrdfCallbacks, processSelectedUrdf]
|
| 452 |
);
|
| 453 |
|
| 454 |
+
// Process Urdf files - moved from DragAndDropContext
|
| 455 |
const processUrdfFiles = useCallback(
|
| 456 |
async (files: Record<string, File>, availableModels: string[]) => {
|
| 457 |
// Clear previous blob URLs to prevent memory leaks
|
|
|
|
| 461 |
setUrdfModelOptions([]);
|
| 462 |
|
| 463 |
try {
|
| 464 |
+
// Check if we have any Urdf files
|
| 465 |
if (availableModels.length > 0 && urdfProcessor) {
|
| 466 |
console.log(
|
| 467 |
+
`🤖 Found ${availableModels.length} Urdf models:`,
|
| 468 |
availableModels
|
| 469 |
);
|
| 470 |
|
|
|
|
| 495 |
|
| 496 |
// If there's only one model, use it directly
|
| 497 |
if (availableModels.length === 1) {
|
| 498 |
+
// Extract model name from the Urdf file
|
| 499 |
const fileName = availableModels[0].split("/").pop() || "";
|
| 500 |
const modelName = fileName.replace(/\.urdf$/i, "");
|
| 501 |
console.log(`📄 Using model: ${modelName} (${fileName})`);
|
|
|
|
| 503 |
// Use the blob URL instead of the file path
|
| 504 |
const blobUrl = newUrdfBlobUrls[availableModels[0]];
|
| 505 |
if (blobUrl) {
|
| 506 |
+
console.log(`🔗 Using blob URL for Urdf: ${blobUrl}`);
|
| 507 |
urdfProcessor.loadUrdf(blobUrl);
|
| 508 |
|
| 509 |
// Immediately update model state
|
| 510 |
setIsDefaultModel(false);
|
| 511 |
setCustomModelName(modelName);
|
| 512 |
|
| 513 |
+
// Process the Urdf file for parsing
|
| 514 |
if (files[availableModels[0]]) {
|
| 515 |
console.log(
|
| 516 |
+
"📄 Reading Urdf content for edge function parsing..."
|
| 517 |
);
|
| 518 |
|
| 519 |
+
// Show a toast notification that we're parsing the Urdf
|
| 520 |
+
const parsingToast = toast.loading("Analyzing Urdf model...", {
|
| 521 |
description: "Extracting robot information",
|
| 522 |
duration: 10000, // Long duration since we'll dismiss it manually
|
| 523 |
});
|
|
|
|
| 528 |
);
|
| 529 |
|
| 530 |
console.log(
|
| 531 |
+
`📏 Urdf content read, length: ${urdfContent.length} characters`
|
| 532 |
);
|
| 533 |
|
| 534 |
+
// Store the Urdf content in state
|
| 535 |
setUrdfContent(urdfContent);
|
| 536 |
|
| 537 |
// Call the parseUrdf function from the hook
|
|
|
|
| 541 |
toast.dismiss(parsingToast);
|
| 542 |
|
| 543 |
if (parseResult) {
|
| 544 |
+
toast.success("Urdf model analyzed successfully", {
|
| 545 |
description: `Model: ${modelName}`,
|
| 546 |
duration: 3000,
|
| 547 |
});
|
|
|
|
| 590 |
setParsedRobotData(minimalData);
|
| 591 |
|
| 592 |
toast.warning(
|
| 593 |
+
"Urdf model loaded with limited information",
|
| 594 |
{
|
| 595 |
description:
|
| 596 |
"Could not fully analyze the robot structure",
|
|
|
|
| 606 |
});
|
| 607 |
}
|
| 608 |
} catch (parseError) {
|
| 609 |
+
console.error("❌ Error parsing Urdf:", parseError);
|
| 610 |
toast.dismiss(parsingToast);
|
| 611 |
+
toast.error("Error analyzing Urdf", {
|
| 612 |
description: `Error: ${
|
| 613 |
parseError instanceof Error
|
| 614 |
? parseError.message
|
|
|
|
| 625 |
}
|
| 626 |
} else {
|
| 627 |
console.error(
|
| 628 |
+
"❌ Could not find file for Urdf model:",
|
| 629 |
availableModels[0]
|
| 630 |
);
|
| 631 |
console.log("📦 Available files:", Object.keys(files));
|
|
|
|
| 653 |
});
|
| 654 |
}
|
| 655 |
} else {
|
| 656 |
+
// Multiple Urdf files found, show selection modal
|
| 657 |
console.log(
|
| 658 |
+
"📋 Multiple Urdf files found, showing selection modal"
|
| 659 |
);
|
| 660 |
setIsSelectionModalOpen(true);
|
| 661 |
|
| 662 |
+
// Notify that Urdf files are available but selection is needed
|
| 663 |
notifyUrdfCallbacks({
|
| 664 |
hasUrdf: true,
|
| 665 |
modelName: "Multiple models available",
|
|
|
|
| 667 |
}
|
| 668 |
} else {
|
| 669 |
console.warn(
|
| 670 |
+
"❌ No Urdf models found in dropped files or no processor available"
|
| 671 |
);
|
| 672 |
notifyUrdfCallbacks({ hasUrdf: false, parsedData: null });
|
| 673 |
|
| 674 |
+
// Reset to default model when no Urdf files are found
|
| 675 |
resetToDefaultModel();
|
| 676 |
|
| 677 |
+
toast.error("No Urdf file found", {
|
| 678 |
description: "Please upload a folder containing a .urdf file.",
|
| 679 |
duration: 3000,
|
| 680 |
});
|
| 681 |
}
|
| 682 |
} catch (error) {
|
| 683 |
+
console.error("❌ Error processing Urdf files:", error);
|
| 684 |
toast.error("Error processing files", {
|
| 685 |
description: `Error: ${
|
| 686 |
error instanceof Error ? error.message : String(error)
|
viewer/src/hooks/useUrdf.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { UrdfContextType, UrdfContext } from "@/contexts/UrdfContext";
|
| 2 |
import { useContext } from "react";
|
| 3 |
|
| 4 |
-
// Custom hook to use the
|
| 5 |
export const useUrdf = (): UrdfContextType => {
|
| 6 |
const context = useContext(UrdfContext);
|
| 7 |
if (context === undefined) {
|
|
|
|
| 1 |
import { UrdfContextType, UrdfContext } from "@/contexts/UrdfContext";
|
| 2 |
import { useContext } from "react";
|
| 3 |
|
| 4 |
+
// Custom hook to use the Urdf context
|
| 5 |
export const useUrdf = (): UrdfContextType => {
|
| 6 |
const context = useContext(UrdfContext);
|
| 7 |
if (context === undefined) {
|
viewer/src/hooks/useUrdfData.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
|
|
| 2 |
import { getUrdfs, getCategories, getUrdfById } from "@/api/urdfApi";
|
| 3 |
import { ContentItem, Category } from "@/lib/types";
|
| 4 |
|
| 5 |
-
// Hook to fetch all
|
| 6 |
export const useUrdfs = () => {
|
| 7 |
return useQuery({
|
| 8 |
queryKey: ["urdfs"],
|
|
@@ -19,7 +19,7 @@ export const useCategories = () => {
|
|
| 19 |
});
|
| 20 |
};
|
| 21 |
|
| 22 |
-
// Hook to fetch a specific
|
| 23 |
export const useUrdf = (id: string) => {
|
| 24 |
return useQuery({
|
| 25 |
queryKey: ["urdf", id],
|
|
@@ -28,7 +28,7 @@ export const useUrdf = (id: string) => {
|
|
| 28 |
});
|
| 29 |
};
|
| 30 |
|
| 31 |
-
// Hook to get
|
| 32 |
export const useUrdfsByCategory = (categoryId: string) => {
|
| 33 |
const urdfsQuery = useUrdfs();
|
| 34 |
|
|
|
|
| 2 |
import { getUrdfs, getCategories, getUrdfById } from "@/api/urdfApi";
|
| 3 |
import { ContentItem, Category } from "@/lib/types";
|
| 4 |
|
| 5 |
+
// Hook to fetch all Urdfs
|
| 6 |
export const useUrdfs = () => {
|
| 7 |
return useQuery({
|
| 8 |
queryKey: ["urdfs"],
|
|
|
|
| 19 |
});
|
| 20 |
};
|
| 21 |
|
| 22 |
+
// Hook to fetch a specific Urdf by ID
|
| 23 |
export const useUrdf = (id: string) => {
|
| 24 |
return useQuery({
|
| 25 |
queryKey: ["urdf", id],
|
|
|
|
| 28 |
});
|
| 29 |
};
|
| 30 |
|
| 31 |
+
// Hook to get Urdfs filtered by category
|
| 32 |
export const useUrdfsByCategory = (categoryId: string) => {
|
| 33 |
const urdfsQuery = useUrdfs();
|
| 34 |
|
viewer/src/hooks/useUrdfParser.ts
CHANGED
|
@@ -10,16 +10,16 @@ export const useUrdfParser = () => {
|
|
| 10 |
|
| 11 |
const parseUrdf = async (urdfContent: string) => {
|
| 12 |
const requestId = `urdf-${Date.now()}`; // Generate unique ID for tracking this request
|
| 13 |
-
console.log(`[${requestId}] 🚀
|
| 14 |
console.log(
|
| 15 |
`[${requestId}] 🔍 Content preview:`,
|
| 16 |
urdfContent?.substring(0, 100) + "..."
|
| 17 |
);
|
| 18 |
|
| 19 |
-
// Check if the
|
| 20 |
const MAX_SIZE = 4 * 1024 * 1024; // 4MB limit
|
| 21 |
if (urdfContent.length > MAX_SIZE) {
|
| 22 |
-
const errorMessage = `
|
| 23 |
urdfContent.length /
|
| 24 |
1024 /
|
| 25 |
1024
|
|
@@ -41,7 +41,7 @@ export const useUrdfParser = () => {
|
|
| 41 |
try {
|
| 42 |
// Make sure the urdfContent is a string
|
| 43 |
if (typeof urdfContent !== "string") {
|
| 44 |
-
throw new Error("
|
| 45 |
}
|
| 46 |
|
| 47 |
console.log(
|
|
@@ -62,15 +62,15 @@ export const useUrdfParser = () => {
|
|
| 62 |
if (error.message.includes("non-2xx status code")) {
|
| 63 |
// Server error from edge function
|
| 64 |
const serverErrorMsg =
|
| 65 |
-
"The
|
| 66 |
console.error(`[${requestId}] 🔥 Server error in edge function`);
|
| 67 |
|
| 68 |
// Check if we got fallback data despite the error
|
| 69 |
if (rawResponse && rawResponse.fallback) {
|
| 70 |
console.log(`[${requestId}] 🛟 Using fallback data from server`);
|
| 71 |
-
toast.warning("Reduced
|
| 72 |
description:
|
| 73 |
-
"Limited information extracted from your
|
| 74 |
duration: 5000,
|
| 75 |
});
|
| 76 |
|
|
@@ -79,7 +79,7 @@ export const useUrdfParser = () => {
|
|
| 79 |
return rawResponse.fallback;
|
| 80 |
}
|
| 81 |
|
| 82 |
-
toast.error("
|
| 83 |
description: serverErrorMsg,
|
| 84 |
duration: 5000,
|
| 85 |
});
|
|
@@ -88,7 +88,7 @@ export const useUrdfParser = () => {
|
|
| 88 |
} else if (error.message.includes("timeout")) {
|
| 89 |
// Timeout error
|
| 90 |
const timeoutMsg =
|
| 91 |
-
"The
|
| 92 |
console.error(`[${requestId}] ⏱️ Edge function timed out`);
|
| 93 |
|
| 94 |
toast.error("Processing Timeout", {
|
|
@@ -101,7 +101,7 @@ export const useUrdfParser = () => {
|
|
| 101 |
// Generic error
|
| 102 |
setError(error.message);
|
| 103 |
|
| 104 |
-
toast.error("Error Parsing
|
| 105 |
description: error.message,
|
| 106 |
duration: 5000,
|
| 107 |
});
|
|
@@ -175,7 +175,7 @@ export const useUrdfParser = () => {
|
|
| 175 |
other: jointTypes.other,
|
| 176 |
},
|
| 177 |
links: transformedLinks,
|
| 178 |
-
// Default materials since we don't have this info from
|
| 179 |
materials: [{ name: "Metal", percentage: 100 }],
|
| 180 |
};
|
| 181 |
|
|
@@ -190,7 +190,7 @@ export const useUrdfParser = () => {
|
|
| 190 |
const endTime = performance.now();
|
| 191 |
|
| 192 |
console.error(
|
| 193 |
-
`[${requestId}] ❌ Error parsing
|
| 194 |
endTime - startTime
|
| 195 |
).toFixed(2)}ms:`,
|
| 196 |
err
|
|
@@ -206,7 +206,7 @@ export const useUrdfParser = () => {
|
|
| 206 |
} finally {
|
| 207 |
setIsLoading(false);
|
| 208 |
console.log(
|
| 209 |
-
`[${requestId}] 🏁
|
| 210 |
);
|
| 211 |
}
|
| 212 |
};
|
|
|
|
| 10 |
|
| 11 |
const parseUrdf = async (urdfContent: string) => {
|
| 12 |
const requestId = `urdf-${Date.now()}`; // Generate unique ID for tracking this request
|
| 13 |
+
console.log(`[${requestId}] 🚀 Urdf Parser: Starting parse request`);
|
| 14 |
console.log(
|
| 15 |
`[${requestId}] 🔍 Content preview:`,
|
| 16 |
urdfContent?.substring(0, 100) + "..."
|
| 17 |
);
|
| 18 |
|
| 19 |
+
// Check if the Urdf content is too large
|
| 20 |
const MAX_SIZE = 4 * 1024 * 1024; // 4MB limit
|
| 21 |
if (urdfContent.length > MAX_SIZE) {
|
| 22 |
+
const errorMessage = `Urdf content too large (${(
|
| 23 |
urdfContent.length /
|
| 24 |
1024 /
|
| 25 |
1024
|
|
|
|
| 41 |
try {
|
| 42 |
// Make sure the urdfContent is a string
|
| 43 |
if (typeof urdfContent !== "string") {
|
| 44 |
+
throw new Error("Urdf content must be a string");
|
| 45 |
}
|
| 46 |
|
| 47 |
console.log(
|
|
|
|
| 62 |
if (error.message.includes("non-2xx status code")) {
|
| 63 |
// Server error from edge function
|
| 64 |
const serverErrorMsg =
|
| 65 |
+
"The Urdf parser encountered a server error. It might be due to the complexity of your Urdf file or temporary server issues.";
|
| 66 |
console.error(`[${requestId}] 🔥 Server error in edge function`);
|
| 67 |
|
| 68 |
// Check if we got fallback data despite the error
|
| 69 |
if (rawResponse && rawResponse.fallback) {
|
| 70 |
console.log(`[${requestId}] 🛟 Using fallback data from server`);
|
| 71 |
+
toast.warning("Reduced Urdf Data", {
|
| 72 |
description:
|
| 73 |
+
"Limited information extracted from your Urdf file due to parsing limitations.",
|
| 74 |
duration: 5000,
|
| 75 |
});
|
| 76 |
|
|
|
|
| 79 |
return rawResponse.fallback;
|
| 80 |
}
|
| 81 |
|
| 82 |
+
toast.error("Urdf Parser Error", {
|
| 83 |
description: serverErrorMsg,
|
| 84 |
duration: 5000,
|
| 85 |
});
|
|
|
|
| 88 |
} else if (error.message.includes("timeout")) {
|
| 89 |
// Timeout error
|
| 90 |
const timeoutMsg =
|
| 91 |
+
"The Urdf parser timed out. Your file might be too complex to process within the allowed time.";
|
| 92 |
console.error(`[${requestId}] ⏱️ Edge function timed out`);
|
| 93 |
|
| 94 |
toast.error("Processing Timeout", {
|
|
|
|
| 101 |
// Generic error
|
| 102 |
setError(error.message);
|
| 103 |
|
| 104 |
+
toast.error("Error Parsing Urdf", {
|
| 105 |
description: error.message,
|
| 106 |
duration: 5000,
|
| 107 |
});
|
|
|
|
| 175 |
other: jointTypes.other,
|
| 176 |
},
|
| 177 |
links: transformedLinks,
|
| 178 |
+
// Default materials since we don't have this info from Urdf
|
| 179 |
materials: [{ name: "Metal", percentage: 100 }],
|
| 180 |
};
|
| 181 |
|
|
|
|
| 190 |
const endTime = performance.now();
|
| 191 |
|
| 192 |
console.error(
|
| 193 |
+
`[${requestId}] ❌ Error parsing Urdf after ${(
|
| 194 |
endTime - startTime
|
| 195 |
).toFixed(2)}ms:`,
|
| 196 |
err
|
|
|
|
| 206 |
} finally {
|
| 207 |
setIsLoading(false);
|
| 208 |
console.log(
|
| 209 |
+
`[${requestId}] 🏁 Urdf parsing request completed, isLoading set to false`
|
| 210 |
);
|
| 211 |
}
|
| 212 |
};
|
viewer/src/lib/UrdfDragAndDrop.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
-
|
| 2 |
/**
|
| 3 |
-
*
|
| 4 |
*
|
| 5 |
-
* This file provides functionality for handling drag and drop of
|
| 6 |
* It converts the dropped files into accessible blobs for visualization.
|
| 7 |
*/
|
| 8 |
|
|
@@ -136,7 +135,7 @@ export function cleanFilePath(path: string): string {
|
|
| 136 |
}
|
| 137 |
|
| 138 |
/**
|
| 139 |
-
* Interface representing the structure of an
|
| 140 |
*/
|
| 141 |
export interface UrdfProcessor {
|
| 142 |
loadUrdf: (path: string) => void;
|
|
@@ -148,8 +147,8 @@ export interface UrdfProcessor {
|
|
| 148 |
const packageRef = { current: "" };
|
| 149 |
|
| 150 |
/**
|
| 151 |
-
* Reads the content of a
|
| 152 |
-
* @param file The
|
| 153 |
* @returns A promise that resolves with the content of the file as a string
|
| 154 |
*/
|
| 155 |
export function readUrdfFileContent(file: File): Promise<string> {
|
|
@@ -159,10 +158,10 @@ export function readUrdfFileContent(file: File): Promise<string> {
|
|
| 159 |
if (event.target && event.target.result) {
|
| 160 |
resolve(event.target.result as string);
|
| 161 |
} else {
|
| 162 |
-
reject(new Error("Failed to read
|
| 163 |
}
|
| 164 |
};
|
| 165 |
-
reader.onerror = () => reject(new Error("Error reading
|
| 166 |
reader.readAsText(file);
|
| 167 |
});
|
| 168 |
}
|
|
@@ -170,7 +169,7 @@ export function readUrdfFileContent(file: File): Promise<string> {
|
|
| 170 |
/**
|
| 171 |
* Downloads a zip file from a URL and extracts its contents
|
| 172 |
* @param zipUrl URL of the zip file to download
|
| 173 |
-
* @param urdfProcessor The
|
| 174 |
* @returns A promise that resolves with the extraction results
|
| 175 |
*/
|
| 176 |
export async function downloadAndExtractZip(
|
|
@@ -182,58 +181,62 @@ export async function downloadAndExtractZip(
|
|
| 182 |
blobUrls: Record<string, string>;
|
| 183 |
}> {
|
| 184 |
console.log("🔄 Downloading zip file from:", zipUrl);
|
| 185 |
-
|
| 186 |
try {
|
| 187 |
// Download the zip file
|
| 188 |
const response = await fetch(zipUrl);
|
| 189 |
if (!response.ok) {
|
| 190 |
throw new Error(`Failed to download zip: ${response.statusText}`);
|
| 191 |
}
|
| 192 |
-
|
| 193 |
const zipBlob = await response.blob();
|
| 194 |
-
|
| 195 |
// Load JSZip dynamically since it's much easier to work with than manual Blob handling
|
| 196 |
// We use dynamic import to avoid adding a dependency
|
| 197 |
-
const JSZip = (await import(
|
| 198 |
const zip = new JSZip();
|
| 199 |
-
|
| 200 |
// Load the zip content
|
| 201 |
const contents = await zip.loadAsync(zipBlob);
|
| 202 |
-
|
| 203 |
// Convert zip contents to files
|
| 204 |
const files: Record<string, File> = {};
|
| 205 |
const filePromises: Promise<void>[] = [];
|
| 206 |
-
|
| 207 |
// Process each file in the zip
|
| 208 |
contents.forEach((relativePath, zipEntry) => {
|
| 209 |
if (!zipEntry.dir) {
|
| 210 |
-
const promise = zipEntry.async(
|
| 211 |
// Create a file with the proper name and path
|
| 212 |
-
const path =
|
| 213 |
-
files[path] = new File(
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
});
|
| 217 |
filePromises.push(promise);
|
| 218 |
}
|
| 219 |
});
|
| 220 |
-
|
| 221 |
// Wait for all files to be processed
|
| 222 |
await Promise.all(filePromises);
|
| 223 |
-
|
| 224 |
// Get all file paths and clean them
|
| 225 |
const fileNames = Object.keys(files).map((n) => cleanFilePath(n));
|
| 226 |
-
|
| 227 |
-
// Filter all files ending in
|
| 228 |
const availableModels = fileNames.filter((n) => /urdf$/i.test(n));
|
| 229 |
-
|
| 230 |
-
// Create blob URLs for
|
| 231 |
const blobUrls: Record<string, string> = {};
|
| 232 |
availableModels.forEach((path) => {
|
| 233 |
blobUrls[path] = URL.createObjectURL(files[path]);
|
| 234 |
});
|
| 235 |
-
|
| 236 |
-
// Extract the package base path from the first
|
| 237 |
let packageBasePath = "";
|
| 238 |
if (availableModels.length > 0) {
|
| 239 |
// Extract the main directory path (e.g., '/cassie_description/')
|
|
@@ -243,51 +246,51 @@ export async function downloadAndExtractZip(
|
|
| 243 |
packageBasePath = packageMatch[1];
|
| 244 |
}
|
| 245 |
}
|
| 246 |
-
|
| 247 |
// Store the package path for future reference
|
| 248 |
const packagePathRef = packageBasePath;
|
| 249 |
urdfProcessor.setUrlModifierFunc((url) => {
|
| 250 |
// Find the matching file given the requested URL
|
| 251 |
-
|
| 252 |
// Store package reference for future use
|
| 253 |
if (packagePathRef) {
|
| 254 |
packageRef.current = packagePathRef;
|
| 255 |
}
|
| 256 |
-
|
| 257 |
// Simple approach: just find the first file that matches the end of the URL
|
| 258 |
const cleaned = cleanFilePath(url);
|
| 259 |
-
|
| 260 |
// Get the filename from the URL
|
| 261 |
const urlFilename = cleaned.split("/").pop() || "";
|
| 262 |
-
|
| 263 |
// Find the first file that ends with this filename
|
| 264 |
let fileName = fileNames.find((name) => name.endsWith(urlFilename));
|
| 265 |
-
|
| 266 |
// If no match found, just take the first file with a similar extension
|
| 267 |
if (!fileName && urlFilename.includes(".")) {
|
| 268 |
const extension = "." + urlFilename.split(".").pop();
|
| 269 |
fileName = fileNames.find((name) => name.endsWith(extension));
|
| 270 |
}
|
| 271 |
-
|
| 272 |
if (fileName !== undefined && fileName !== null) {
|
| 273 |
// Extract file extension for content type
|
| 274 |
const fileExtension = fileName.split(".").pop()?.toLowerCase() || "";
|
| 275 |
-
|
| 276 |
// Create blob URL with extension in the searchParams to help with format detection
|
| 277 |
const blob = new Blob([files[fileName]], {
|
| 278 |
type: getMimeType(fileExtension),
|
| 279 |
});
|
| 280 |
const blobUrl = URL.createObjectURL(blob) + "#." + fileExtension;
|
| 281 |
-
|
| 282 |
// Don't revoke immediately, wait for the mesh to be loaded
|
| 283 |
setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
|
| 284 |
return blobUrl;
|
| 285 |
}
|
| 286 |
-
|
| 287 |
console.warn(`No matching file found for: ${url}`);
|
| 288 |
return url;
|
| 289 |
});
|
| 290 |
-
|
| 291 |
return {
|
| 292 |
files,
|
| 293 |
availableModels,
|
|
@@ -300,7 +303,7 @@ export async function downloadAndExtractZip(
|
|
| 300 |
}
|
| 301 |
|
| 302 |
/**
|
| 303 |
-
* Processes dropped files and returns information about available
|
| 304 |
*/
|
| 305 |
export async function processDroppedFiles(
|
| 306 |
dataTransfer: DataTransfer,
|
|
@@ -319,16 +322,16 @@ export async function processDroppedFiles(
|
|
| 319 |
// Get all file paths and clean them
|
| 320 |
const fileNames = Object.keys(files).map((n) => cleanFilePath(n));
|
| 321 |
|
| 322 |
-
// Filter all files ending in
|
| 323 |
const availableModels = fileNames.filter((n) => /urdf$/i.test(n));
|
| 324 |
|
| 325 |
-
// Create blob URLs for
|
| 326 |
const blobUrls: Record<string, string> = {};
|
| 327 |
availableModels.forEach((path) => {
|
| 328 |
blobUrls[path] = URL.createObjectURL(files[path]);
|
| 329 |
});
|
| 330 |
|
| 331 |
-
// Extract the package base path from the first
|
| 332 |
let packageBasePath = "";
|
| 333 |
if (availableModels.length > 0) {
|
| 334 |
// Extract the main directory path (e.g., '/cassie_description/')
|
|
@@ -410,4 +413,3 @@ function getMimeType(extension: string): string {
|
|
| 410 |
return "application/octet-stream";
|
| 411 |
}
|
| 412 |
}
|
| 413 |
-
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
+
* Urdf Drag and Drop Utility
|
| 3 |
*
|
| 4 |
+
* This file provides functionality for handling drag and drop of Urdf folders.
|
| 5 |
* It converts the dropped files into accessible blobs for visualization.
|
| 6 |
*/
|
| 7 |
|
|
|
|
| 135 |
}
|
| 136 |
|
| 137 |
/**
|
| 138 |
+
* Interface representing the structure of an Urdf processor
|
| 139 |
*/
|
| 140 |
export interface UrdfProcessor {
|
| 141 |
loadUrdf: (path: string) => void;
|
|
|
|
| 147 |
const packageRef = { current: "" };
|
| 148 |
|
| 149 |
/**
|
| 150 |
+
* Reads the content of a Urdf file
|
| 151 |
+
* @param file The Urdf file object
|
| 152 |
* @returns A promise that resolves with the content of the file as a string
|
| 153 |
*/
|
| 154 |
export function readUrdfFileContent(file: File): Promise<string> {
|
|
|
|
| 158 |
if (event.target && event.target.result) {
|
| 159 |
resolve(event.target.result as string);
|
| 160 |
} else {
|
| 161 |
+
reject(new Error("Failed to read Urdf file content"));
|
| 162 |
}
|
| 163 |
};
|
| 164 |
+
reader.onerror = () => reject(new Error("Error reading Urdf file"));
|
| 165 |
reader.readAsText(file);
|
| 166 |
});
|
| 167 |
}
|
|
|
|
| 169 |
/**
|
| 170 |
* Downloads a zip file from a URL and extracts its contents
|
| 171 |
* @param zipUrl URL of the zip file to download
|
| 172 |
+
* @param urdfProcessor The Urdf processor to use for loading
|
| 173 |
* @returns A promise that resolves with the extraction results
|
| 174 |
*/
|
| 175 |
export async function downloadAndExtractZip(
|
|
|
|
| 181 |
blobUrls: Record<string, string>;
|
| 182 |
}> {
|
| 183 |
console.log("🔄 Downloading zip file from:", zipUrl);
|
| 184 |
+
|
| 185 |
try {
|
| 186 |
// Download the zip file
|
| 187 |
const response = await fetch(zipUrl);
|
| 188 |
if (!response.ok) {
|
| 189 |
throw new Error(`Failed to download zip: ${response.statusText}`);
|
| 190 |
}
|
| 191 |
+
|
| 192 |
const zipBlob = await response.blob();
|
| 193 |
+
|
| 194 |
// Load JSZip dynamically since it's much easier to work with than manual Blob handling
|
| 195 |
// We use dynamic import to avoid adding a dependency
|
| 196 |
+
const JSZip = (await import("jszip")).default;
|
| 197 |
const zip = new JSZip();
|
| 198 |
+
|
| 199 |
// Load the zip content
|
| 200 |
const contents = await zip.loadAsync(zipBlob);
|
| 201 |
+
|
| 202 |
// Convert zip contents to files
|
| 203 |
const files: Record<string, File> = {};
|
| 204 |
const filePromises: Promise<void>[] = [];
|
| 205 |
+
|
| 206 |
// Process each file in the zip
|
| 207 |
contents.forEach((relativePath, zipEntry) => {
|
| 208 |
if (!zipEntry.dir) {
|
| 209 |
+
const promise = zipEntry.async("blob").then((blob) => {
|
| 210 |
// Create a file with the proper name and path
|
| 211 |
+
const path = "/" + relativePath;
|
| 212 |
+
files[path] = new File(
|
| 213 |
+
[blob],
|
| 214 |
+
relativePath.split("/").pop() || "unknown",
|
| 215 |
+
{
|
| 216 |
+
type: getMimeType(relativePath.split(".").pop() || ""),
|
| 217 |
+
}
|
| 218 |
+
);
|
| 219 |
});
|
| 220 |
filePromises.push(promise);
|
| 221 |
}
|
| 222 |
});
|
| 223 |
+
|
| 224 |
// Wait for all files to be processed
|
| 225 |
await Promise.all(filePromises);
|
| 226 |
+
|
| 227 |
// Get all file paths and clean them
|
| 228 |
const fileNames = Object.keys(files).map((n) => cleanFilePath(n));
|
| 229 |
+
|
| 230 |
+
// Filter all files ending in Urdf
|
| 231 |
const availableModels = fileNames.filter((n) => /urdf$/i.test(n));
|
| 232 |
+
|
| 233 |
+
// Create blob URLs for Urdf files
|
| 234 |
const blobUrls: Record<string, string> = {};
|
| 235 |
availableModels.forEach((path) => {
|
| 236 |
blobUrls[path] = URL.createObjectURL(files[path]);
|
| 237 |
});
|
| 238 |
+
|
| 239 |
+
// Extract the package base path from the first Urdf model for reference
|
| 240 |
let packageBasePath = "";
|
| 241 |
if (availableModels.length > 0) {
|
| 242 |
// Extract the main directory path (e.g., '/cassie_description/')
|
|
|
|
| 246 |
packageBasePath = packageMatch[1];
|
| 247 |
}
|
| 248 |
}
|
| 249 |
+
|
| 250 |
// Store the package path for future reference
|
| 251 |
const packagePathRef = packageBasePath;
|
| 252 |
urdfProcessor.setUrlModifierFunc((url) => {
|
| 253 |
// Find the matching file given the requested URL
|
| 254 |
+
|
| 255 |
// Store package reference for future use
|
| 256 |
if (packagePathRef) {
|
| 257 |
packageRef.current = packagePathRef;
|
| 258 |
}
|
| 259 |
+
|
| 260 |
// Simple approach: just find the first file that matches the end of the URL
|
| 261 |
const cleaned = cleanFilePath(url);
|
| 262 |
+
|
| 263 |
// Get the filename from the URL
|
| 264 |
const urlFilename = cleaned.split("/").pop() || "";
|
| 265 |
+
|
| 266 |
// Find the first file that ends with this filename
|
| 267 |
let fileName = fileNames.find((name) => name.endsWith(urlFilename));
|
| 268 |
+
|
| 269 |
// If no match found, just take the first file with a similar extension
|
| 270 |
if (!fileName && urlFilename.includes(".")) {
|
| 271 |
const extension = "." + urlFilename.split(".").pop();
|
| 272 |
fileName = fileNames.find((name) => name.endsWith(extension));
|
| 273 |
}
|
| 274 |
+
|
| 275 |
if (fileName !== undefined && fileName !== null) {
|
| 276 |
// Extract file extension for content type
|
| 277 |
const fileExtension = fileName.split(".").pop()?.toLowerCase() || "";
|
| 278 |
+
|
| 279 |
// Create blob URL with extension in the searchParams to help with format detection
|
| 280 |
const blob = new Blob([files[fileName]], {
|
| 281 |
type: getMimeType(fileExtension),
|
| 282 |
});
|
| 283 |
const blobUrl = URL.createObjectURL(blob) + "#." + fileExtension;
|
| 284 |
+
|
| 285 |
// Don't revoke immediately, wait for the mesh to be loaded
|
| 286 |
setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
|
| 287 |
return blobUrl;
|
| 288 |
}
|
| 289 |
+
|
| 290 |
console.warn(`No matching file found for: ${url}`);
|
| 291 |
return url;
|
| 292 |
});
|
| 293 |
+
|
| 294 |
return {
|
| 295 |
files,
|
| 296 |
availableModels,
|
|
|
|
| 303 |
}
|
| 304 |
|
| 305 |
/**
|
| 306 |
+
* Processes dropped files and returns information about available Urdf models
|
| 307 |
*/
|
| 308 |
export async function processDroppedFiles(
|
| 309 |
dataTransfer: DataTransfer,
|
|
|
|
| 322 |
// Get all file paths and clean them
|
| 323 |
const fileNames = Object.keys(files).map((n) => cleanFilePath(n));
|
| 324 |
|
| 325 |
+
// Filter all files ending in Urdf
|
| 326 |
const availableModels = fileNames.filter((n) => /urdf$/i.test(n));
|
| 327 |
|
| 328 |
+
// Create blob URLs for Urdf files
|
| 329 |
const blobUrls: Record<string, string> = {};
|
| 330 |
availableModels.forEach((path) => {
|
| 331 |
blobUrls[path] = URL.createObjectURL(files[path]);
|
| 332 |
});
|
| 333 |
|
| 334 |
+
// Extract the package base path from the first Urdf model for reference
|
| 335 |
let packageBasePath = "";
|
| 336 |
if (availableModels.length > 0) {
|
| 337 |
// Extract the main directory path (e.g., '/cassie_description/')
|
|
|
|
| 413 |
return "application/octet-stream";
|
| 414 |
}
|
| 415 |
}
|
|
|
viewer/src/lib/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
/**
|
| 2 |
-
* Shared type definitions for
|
| 3 |
*/
|
| 4 |
|
| 5 |
export interface UrdfData {
|
|
@@ -25,11 +25,11 @@ export interface UrdfData {
|
|
| 25 |
}
|
| 26 |
|
| 27 |
/**
|
| 28 |
-
* Interface representing a
|
| 29 |
*/
|
| 30 |
export interface UrdfFileModel {
|
| 31 |
/**
|
| 32 |
-
* Path to the
|
| 33 |
*/
|
| 34 |
path: string;
|
| 35 |
|
|
@@ -48,7 +48,7 @@ export interface UrdfFileModel {
|
|
| 48 |
* Joint animation configuration interface
|
| 49 |
*/
|
| 50 |
export interface JointAnimationConfig {
|
| 51 |
-
/** Joint name in the
|
| 52 |
name: string;
|
| 53 |
/** Animation type (sine, linear, etc.) */
|
| 54 |
type: "sine" | "linear" | "constant";
|
|
|
|
| 1 |
/**
|
| 2 |
+
* Shared type definitions for Urdf parsing from supabase edge function
|
| 3 |
*/
|
| 4 |
|
| 5 |
export interface UrdfData {
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
/**
|
| 28 |
+
* Interface representing a Urdf file model
|
| 29 |
*/
|
| 30 |
export interface UrdfFileModel {
|
| 31 |
/**
|
| 32 |
+
* Path to the Urdf file
|
| 33 |
*/
|
| 34 |
path: string;
|
| 35 |
|
|
|
|
| 48 |
* Joint animation configuration interface
|
| 49 |
*/
|
| 50 |
export interface JointAnimationConfig {
|
| 51 |
+
/** Joint name in the Urdf */
|
| 52 |
name: string;
|
| 53 |
/** Animation type (sine, linear, etc.) */
|
| 54 |
type: "sine" | "linear" | "constant";
|
viewer/src/lib/urdfAnimationHelpers.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
import { MathUtils } from "three";
|
| 2 |
import { RobotAnimationConfig } from "@/lib/types";
|
| 3 |
|
| 4 |
-
// Define the interface for the
|
| 5 |
-
export interface
|
| 6 |
setJointValue: (joint: string, value: number) => void;
|
| 7 |
}
|
| 8 |
|
| 9 |
/**
|
| 10 |
* Generalized animation function for any robot
|
| 11 |
-
* @param viewer The
|
| 12 |
* @param config Configuration for the robot's joint animations
|
| 13 |
* @returns A cleanup function to cancel the animation
|
| 14 |
*/
|
| 15 |
export function animateRobot(
|
| 16 |
-
viewer:
|
| 17 |
config: RobotAnimationConfig
|
| 18 |
): () => void {
|
| 19 |
let animationFrameId: number | null = null;
|
|
@@ -92,10 +92,10 @@ export function animateRobot(
|
|
| 92 |
|
| 93 |
/**
|
| 94 |
* Animates a hexapod robot (like T12) with walking motion
|
| 95 |
-
* @param viewer The
|
| 96 |
* @returns A cleanup function to cancel the animation
|
| 97 |
*/
|
| 98 |
-
export function animateHexapodRobot(viewer:
|
| 99 |
let animationFrameId: number | null = null;
|
| 100 |
let isRunning = true;
|
| 101 |
|
|
|
|
| 1 |
import { MathUtils } from "three";
|
| 2 |
import { RobotAnimationConfig } from "@/lib/types";
|
| 3 |
|
| 4 |
+
// Define the interface for the Urdf viewer element
|
| 5 |
+
export interface UrdfViewerElement extends HTMLElement {
|
| 6 |
setJointValue: (joint: string, value: number) => void;
|
| 7 |
}
|
| 8 |
|
| 9 |
/**
|
| 10 |
* Generalized animation function for any robot
|
| 11 |
+
* @param viewer The Urdf viewer element
|
| 12 |
* @param config Configuration for the robot's joint animations
|
| 13 |
* @returns A cleanup function to cancel the animation
|
| 14 |
*/
|
| 15 |
export function animateRobot(
|
| 16 |
+
viewer: UrdfViewerElement,
|
| 17 |
config: RobotAnimationConfig
|
| 18 |
): () => void {
|
| 19 |
let animationFrameId: number | null = null;
|
|
|
|
| 92 |
|
| 93 |
/**
|
| 94 |
* Animates a hexapod robot (like T12) with walking motion
|
| 95 |
+
* @param viewer The Urdf viewer element
|
| 96 |
* @returns A cleanup function to cancel the animation
|
| 97 |
*/
|
| 98 |
+
export function animateHexapodRobot(viewer: UrdfViewerElement): () => void {
|
| 99 |
let animationFrameId: number | null = null;
|
| 100 |
let isRunning = true;
|
| 101 |
|
viewer/src/lib/urdfViewerHelpers.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
import { LoadingManager, Object3D } from "three";
|
| 2 |
import { toast } from "sonner";
|
| 3 |
import { loadMeshFile } from "./meshLoaders";
|
| 4 |
-
import {
|
| 5 |
|
| 6 |
-
// Extended
|
| 7 |
-
export interface
|
| 8 |
loadMeshFunc: (
|
| 9 |
path: string,
|
| 10 |
manager: LoadingManager,
|
|
@@ -13,19 +13,19 @@ export interface ExtendedURDFViewerElement extends URDFViewerElement {
|
|
| 13 |
}
|
| 14 |
|
| 15 |
/**
|
| 16 |
-
* Creates and configures a
|
| 17 |
*/
|
| 18 |
export function createUrdfViewer(
|
| 19 |
container: HTMLDivElement,
|
| 20 |
isDarkMode: boolean
|
| 21 |
-
):
|
| 22 |
// Clear any existing content
|
| 23 |
container.innerHTML = "";
|
| 24 |
|
| 25 |
// Create the urdf-viewer element
|
| 26 |
const viewer = document.createElement(
|
| 27 |
"urdf-viewer"
|
| 28 |
-
) as
|
| 29 |
viewer.classList.add("w-full", "h-full");
|
| 30 |
|
| 31 |
// Add the element to the container
|
|
@@ -42,10 +42,10 @@ export function createUrdfViewer(
|
|
| 42 |
}
|
| 43 |
|
| 44 |
/**
|
| 45 |
-
* Setup mesh loading function for
|
| 46 |
*/
|
| 47 |
export function setupMeshLoader(
|
| 48 |
-
viewer:
|
| 49 |
urlModifierFunc: ((url: string) => string) | null
|
| 50 |
): void {
|
| 51 |
if ("loadMeshFunc" in viewer) {
|
|
@@ -80,7 +80,7 @@ export function setupMeshLoader(
|
|
| 80 |
* Setup event handlers for joint highlighting
|
| 81 |
*/
|
| 82 |
export function setupJointHighlighting(
|
| 83 |
-
viewer:
|
| 84 |
setHighlightedJoint: (joint: string | null) => void
|
| 85 |
): () => void {
|
| 86 |
const onJointMouseover = (e: Event) => {
|
|
@@ -107,7 +107,7 @@ export function setupJointHighlighting(
|
|
| 107 |
* Setup model loading and error handling
|
| 108 |
*/
|
| 109 |
export function setupModelLoading(
|
| 110 |
-
viewer:
|
| 111 |
urdfPath: string,
|
| 112 |
packagePath: string,
|
| 113 |
setCustomUrdfPath: (path: string) => void,
|
|
@@ -119,14 +119,14 @@ export function setupModelLoading(
|
|
| 119 |
? urdfPath + "#.urdf" // Add extension hint if it's a blob URL
|
| 120 |
: urdfPath;
|
| 121 |
|
| 122 |
-
// Set the
|
| 123 |
viewer.setAttribute("urdf", loadPath);
|
| 124 |
viewer.setAttribute("package", packagePath);
|
| 125 |
|
| 126 |
// Handle error loading
|
| 127 |
const onLoadError = () => {
|
| 128 |
toast.error("Failed to load model", {
|
| 129 |
-
description: "There was an error loading the
|
| 130 |
duration: 3000,
|
| 131 |
});
|
| 132 |
|
|
|
|
| 1 |
import { LoadingManager, Object3D } from "three";
|
| 2 |
import { toast } from "sonner";
|
| 3 |
import { loadMeshFile } from "./meshLoaders";
|
| 4 |
+
import { UrdfViewerElement } from "./urdfAnimationHelpers";
|
| 5 |
|
| 6 |
+
// Extended Urdf Viewer Element with mesh loading capability
|
| 7 |
+
export interface ExtendedUrdfViewerElement extends UrdfViewerElement {
|
| 8 |
loadMeshFunc: (
|
| 9 |
path: string,
|
| 10 |
manager: LoadingManager,
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
/**
|
| 16 |
+
* Creates and configures a Urdf viewer element
|
| 17 |
*/
|
| 18 |
export function createUrdfViewer(
|
| 19 |
container: HTMLDivElement,
|
| 20 |
isDarkMode: boolean
|
| 21 |
+
): ExtendedUrdfViewerElement {
|
| 22 |
// Clear any existing content
|
| 23 |
container.innerHTML = "";
|
| 24 |
|
| 25 |
// Create the urdf-viewer element
|
| 26 |
const viewer = document.createElement(
|
| 27 |
"urdf-viewer"
|
| 28 |
+
) as ExtendedUrdfViewerElement;
|
| 29 |
viewer.classList.add("w-full", "h-full");
|
| 30 |
|
| 31 |
// Add the element to the container
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
/**
|
| 45 |
+
* Setup mesh loading function for Urdf viewer
|
| 46 |
*/
|
| 47 |
export function setupMeshLoader(
|
| 48 |
+
viewer: ExtendedUrdfViewerElement,
|
| 49 |
urlModifierFunc: ((url: string) => string) | null
|
| 50 |
): void {
|
| 51 |
if ("loadMeshFunc" in viewer) {
|
|
|
|
| 80 |
* Setup event handlers for joint highlighting
|
| 81 |
*/
|
| 82 |
export function setupJointHighlighting(
|
| 83 |
+
viewer: UrdfViewerElement,
|
| 84 |
setHighlightedJoint: (joint: string | null) => void
|
| 85 |
): () => void {
|
| 86 |
const onJointMouseover = (e: Event) => {
|
|
|
|
| 107 |
* Setup model loading and error handling
|
| 108 |
*/
|
| 109 |
export function setupModelLoading(
|
| 110 |
+
viewer: UrdfViewerElement,
|
| 111 |
urdfPath: string,
|
| 112 |
packagePath: string,
|
| 113 |
setCustomUrdfPath: (path: string) => void,
|
|
|
|
| 119 |
? urdfPath + "#.urdf" // Add extension hint if it's a blob URL
|
| 120 |
: urdfPath;
|
| 121 |
|
| 122 |
+
// Set the Urdf path
|
| 123 |
viewer.setAttribute("urdf", loadPath);
|
| 124 |
viewer.setAttribute("package", packagePath);
|
| 125 |
|
| 126 |
// Handle error loading
|
| 127 |
const onLoadError = () => {
|
| 128 |
toast.error("Failed to load model", {
|
| 129 |
+
description: "There was an error loading the Urdf model.",
|
| 130 |
duration: 3000,
|
| 131 |
});
|
| 132 |
|
viewer/supabase/functions/README.md
CHANGED
|
@@ -1,23 +1,24 @@
|
|
| 1 |
-
|
| 2 |
# Supabase Edge Functions
|
| 3 |
|
| 4 |
This directory contains Edge Functions that can be deployed to Supabase.
|
| 5 |
|
| 6 |
## Available Functions
|
| 7 |
|
| 8 |
-
- `urdf-parser`: Extracts metadata from
|
| 9 |
-
- `create-animation`: Generates animations for
|
| 10 |
|
| 11 |
## Deployment Instructions
|
| 12 |
|
| 13 |
### Prerequisites
|
| 14 |
|
| 15 |
1. Install Supabase CLI:
|
|
|
|
| 16 |
```
|
| 17 |
npm install -g supabase
|
| 18 |
```
|
| 19 |
|
| 20 |
2. Login to Supabase:
|
|
|
|
| 21 |
```
|
| 22 |
npx supabase login
|
| 23 |
```
|
|
|
|
|
|
|
| 1 |
# Supabase Edge Functions
|
| 2 |
|
| 3 |
This directory contains Edge Functions that can be deployed to Supabase.
|
| 4 |
|
| 5 |
## Available Functions
|
| 6 |
|
| 7 |
+
- `urdf-parser`: Extracts metadata from Urdf files using OpenAI
|
| 8 |
+
- `create-animation`: Generates animations for Urdf models based on user instructions
|
| 9 |
|
| 10 |
## Deployment Instructions
|
| 11 |
|
| 12 |
### Prerequisites
|
| 13 |
|
| 14 |
1. Install Supabase CLI:
|
| 15 |
+
|
| 16 |
```
|
| 17 |
npm install -g supabase
|
| 18 |
```
|
| 19 |
|
| 20 |
2. Login to Supabase:
|
| 21 |
+
|
| 22 |
```
|
| 23 |
npx supabase login
|
| 24 |
```
|
viewer/supabase/functions/urdf-parser/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { XMLParser } from "npm:[email protected]";
|
| 2 |
import { serve } from "https://deno.land/[email protected]/http/server.ts";
|
| 3 |
|
| 4 |
-
interface
|
| 5 |
name: string;
|
| 6 |
mass: number;
|
| 7 |
inertia: {
|
|
@@ -16,7 +16,7 @@ interface URDFLink {
|
|
| 16 |
has_collision: boolean;
|
| 17 |
}
|
| 18 |
|
| 19 |
-
interface
|
| 20 |
name: string;
|
| 21 |
type: string;
|
| 22 |
parent: string;
|
|
@@ -50,10 +50,10 @@ serve(async (req) => {
|
|
| 50 |
const { urdfContent } = await req.json();
|
| 51 |
|
| 52 |
if (!urdfContent) {
|
| 53 |
-
throw new Error("No
|
| 54 |
}
|
| 55 |
|
| 56 |
-
// Parse
|
| 57 |
const parser = new XMLParser({
|
| 58 |
ignoreAttributes: false,
|
| 59 |
attributeNamePrefix: "",
|
|
@@ -63,15 +63,15 @@ serve(async (req) => {
|
|
| 63 |
const doc = parser.parse(urdfContent);
|
| 64 |
|
| 65 |
if (!doc || !doc.robot) {
|
| 66 |
-
throw new Error("No robot element found in
|
| 67 |
}
|
| 68 |
|
| 69 |
// Extract robot data
|
| 70 |
const robotElement = doc.robot;
|
| 71 |
const name = robotElement.name || "unnamed_robot";
|
| 72 |
const version = robotElement.version;
|
| 73 |
-
const links:
|
| 74 |
-
const joints:
|
| 75 |
let total_mass = 0;
|
| 76 |
|
| 77 |
// Parse links
|
|
@@ -85,7 +85,7 @@ serve(async (req) => {
|
|
| 85 |
const mass = inertial?.mass?.value || 0;
|
| 86 |
total_mass += mass;
|
| 87 |
|
| 88 |
-
const linkData:
|
| 89 |
name: linkName,
|
| 90 |
mass,
|
| 91 |
inertia: {
|
|
@@ -123,7 +123,7 @@ serve(async (req) => {
|
|
| 123 |
? robotElement.joint
|
| 124 |
: [robotElement.joint];
|
| 125 |
jointElements.forEach((joint: any) => {
|
| 126 |
-
const jointData:
|
| 127 |
name: joint.name || "",
|
| 128 |
type: joint.type || "",
|
| 129 |
parent: joint.parent?.link || "",
|
|
@@ -164,7 +164,7 @@ serve(async (req) => {
|
|
| 164 |
},
|
| 165 |
});
|
| 166 |
} catch (error) {
|
| 167 |
-
console.error("Error parsing
|
| 168 |
return new Response(
|
| 169 |
JSON.stringify({
|
| 170 |
success: false,
|
|
|
|
| 1 |
import { XMLParser } from "npm:[email protected]";
|
| 2 |
import { serve } from "https://deno.land/[email protected]/http/server.ts";
|
| 3 |
|
| 4 |
+
interface UrdfLink {
|
| 5 |
name: string;
|
| 6 |
mass: number;
|
| 7 |
inertia: {
|
|
|
|
| 16 |
has_collision: boolean;
|
| 17 |
}
|
| 18 |
|
| 19 |
+
interface UrdfJoint {
|
| 20 |
name: string;
|
| 21 |
type: string;
|
| 22 |
parent: string;
|
|
|
|
| 50 |
const { urdfContent } = await req.json();
|
| 51 |
|
| 52 |
if (!urdfContent) {
|
| 53 |
+
throw new Error("No Urdf content provided");
|
| 54 |
}
|
| 55 |
|
| 56 |
+
// Parse Urdf XML using fast-xml-parser
|
| 57 |
const parser = new XMLParser({
|
| 58 |
ignoreAttributes: false,
|
| 59 |
attributeNamePrefix: "",
|
|
|
|
| 63 |
const doc = parser.parse(urdfContent);
|
| 64 |
|
| 65 |
if (!doc || !doc.robot) {
|
| 66 |
+
throw new Error("No robot element found in Urdf");
|
| 67 |
}
|
| 68 |
|
| 69 |
// Extract robot data
|
| 70 |
const robotElement = doc.robot;
|
| 71 |
const name = robotElement.name || "unnamed_robot";
|
| 72 |
const version = robotElement.version;
|
| 73 |
+
const links: UrdfLink[] = [];
|
| 74 |
+
const joints: UrdfJoint[] = [];
|
| 75 |
let total_mass = 0;
|
| 76 |
|
| 77 |
// Parse links
|
|
|
|
| 85 |
const mass = inertial?.mass?.value || 0;
|
| 86 |
total_mass += mass;
|
| 87 |
|
| 88 |
+
const linkData: UrdfLink = {
|
| 89 |
name: linkName,
|
| 90 |
mass,
|
| 91 |
inertia: {
|
|
|
|
| 123 |
? robotElement.joint
|
| 124 |
: [robotElement.joint];
|
| 125 |
jointElements.forEach((joint: any) => {
|
| 126 |
+
const jointData: UrdfJoint = {
|
| 127 |
name: joint.name || "",
|
| 128 |
type: joint.type || "",
|
| 129 |
parent: joint.parent?.link || "",
|
|
|
|
| 164 |
},
|
| 165 |
});
|
| 166 |
} catch (error) {
|
| 167 |
+
console.error("Error parsing Urdf:", error);
|
| 168 |
return new Response(
|
| 169 |
JSON.stringify({
|
| 170 |
success: false,
|