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,
|