Spaces:
Running
Running
| import { MathUtils } from "three"; | |
| import { RobotAnimationConfig } from "@/lib/types"; | |
| // Define the interface for the Urdf viewer element | |
| export interface UrdfViewerElement extends HTMLElement { | |
| setJointValue: (joint: string, value: number) => void; | |
| } | |
| /** | |
| * Generalized animation function for any robot | |
| * @param viewer The Urdf viewer element | |
| * @param config Configuration for the robot's joint animations | |
| * @returns A cleanup function to cancel the animation | |
| */ | |
| export function animateRobot( | |
| viewer: UrdfViewerElement, | |
| config: RobotAnimationConfig | |
| ): () => void { | |
| let animationFrameId: number | null = null; | |
| let isRunning = true; | |
| const speedMultiplier = config.speedMultiplier || 1; | |
| const animate = () => { | |
| if (!isRunning) return; | |
| const time = Date.now() / 300; // Base time unit | |
| try { | |
| // Process each joint configuration | |
| for (const joint of config.joints) { | |
| // Calculate the animation ratio (0-1) based on the animation type | |
| let ratio = 0; | |
| const adjustedTime = | |
| time * joint.speed * speedMultiplier + joint.offset; | |
| switch (joint.type) { | |
| case "sine": | |
| // Sine wave oscillation mapped to 0-1 | |
| ratio = (Math.sin(adjustedTime) + 1) / 2; | |
| break; | |
| case "linear": | |
| // Saw tooth pattern (0 to 1 repeated) | |
| ratio = (adjustedTime % (2 * Math.PI)) / (2 * Math.PI); | |
| break; | |
| case "constant": | |
| // Constant value (using max) | |
| ratio = 1; | |
| break; | |
| default: | |
| // Use custom easing if provided | |
| if (joint.customEasing) { | |
| ratio = joint.customEasing(adjustedTime); | |
| } | |
| } | |
| // Calculate the joint value based on min/max and the ratio | |
| let value = MathUtils.lerp(joint.min, joint.max, ratio); | |
| // Convert from degrees to radians if specified | |
| if (joint.isDegrees) { | |
| value = (value * Math.PI) / 180; | |
| } | |
| // Set the joint value, catching errors for non-existent joints | |
| try { | |
| viewer.setJointValue(joint.name, value); | |
| } catch (e) { | |
| // Silently ignore if the joint doesn't exist | |
| } | |
| } | |
| } catch (err) { | |
| console.error("Error in robot animation:", err); | |
| } | |
| // Continue the animation loop | |
| animationFrameId = requestAnimationFrame(animate); | |
| }; | |
| // Start the animation | |
| animationFrameId = requestAnimationFrame(animate); | |
| // Return cleanup function | |
| return () => { | |
| isRunning = false; | |
| if (animationFrameId) { | |
| cancelAnimationFrame(animationFrameId); | |
| animationFrameId = null; | |
| } | |
| }; | |
| } | |
| /** | |
| * Animates a hexapod robot (like T12) with walking motion | |
| * @param viewer The Urdf viewer element | |
| * @returns A cleanup function to cancel the animation | |
| */ | |
| export function animateHexapodRobot(viewer: UrdfViewerElement): () => void { | |
| let animationFrameId: number | null = null; | |
| let isRunning = true; | |
| const animate = () => { | |
| // Don't continue animation if we've been told to stop | |
| if (!isRunning) return; | |
| // Animate the legs (for T12 robot) | |
| const time = Date.now() / 3e2; | |
| try { | |
| for (let i = 1; i <= 6; i++) { | |
| const offset = (i * Math.PI) / 3; | |
| const ratio = Math.max(0, Math.sin(time + offset)); | |
| // For a hexapod robot like T12 | |
| if (typeof viewer.setJointValue === "function") { | |
| // Hip joints | |
| viewer.setJointValue( | |
| `HP${i}`, | |
| (MathUtils.lerp(30, 0, ratio) * Math.PI) / 180 | |
| ); | |
| // Knee joints | |
| viewer.setJointValue( | |
| `KP${i}`, | |
| (MathUtils.lerp(90, 150, ratio) * Math.PI) / 180 | |
| ); | |
| // Ankle joints | |
| viewer.setJointValue( | |
| `AP${i}`, | |
| (MathUtils.lerp(-30, -60, ratio) * Math.PI) / 180 | |
| ); | |
| // Check if these joints exist before setting values | |
| try { | |
| // Tire/Contact joints | |
| viewer.setJointValue(`TC${i}A`, MathUtils.lerp(0, 0.065, ratio)); | |
| viewer.setJointValue(`TC${i}B`, MathUtils.lerp(0, 0.065, ratio)); | |
| // Wheel rotation | |
| viewer.setJointValue(`W${i}`, performance.now() * 0.001); | |
| } catch (e) { | |
| // Silently ignore if those joints don't exist | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| console.error("Error in animation:", err); | |
| } | |
| // Continue the animation loop | |
| animationFrameId = requestAnimationFrame(animate); | |
| }; | |
| // Start the animation | |
| animationFrameId = requestAnimationFrame(animate); | |
| // Return cleanup function | |
| return () => { | |
| // Mark animation as stopped but DO NOT reset joint positions | |
| isRunning = false; | |
| if (animationFrameId) { | |
| cancelAnimationFrame(animationFrameId); | |
| animationFrameId = null; | |
| } | |
| }; | |
| } | |
| // Example: Walking animation for Cassie robot | |
| export const cassieWalkingConfig: RobotAnimationConfig = { | |
| speedMultiplier: 0.5, // Adjust overall speed | |
| joints: [ | |
| // Left leg | |
| { | |
| name: "hip_abduction_left", | |
| type: "sine", | |
| min: -0.1, // Small side-to-side movement | |
| max: 0.1, | |
| speed: 1, | |
| offset: 0, | |
| isDegrees: false, // Already in radians | |
| }, | |
| { | |
| name: "hip_rotation_left", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.2, | |
| max: 0.2, | |
| speed: 1, | |
| offset: Math.PI / 2, // 90 degrees out of phase | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "hip_flexion_left", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.3, | |
| max: 0.6, | |
| speed: 1, | |
| offset: 0, | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "knee_joint_left", // Assuming this joint exists | |
| type: "sine", | |
| min: 0.2, | |
| max: 1.4, | |
| speed: 1, | |
| offset: Math.PI / 2, // 90 degrees phase shifted from hip | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "ankle_joint_left", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.4, | |
| max: 0.1, | |
| speed: 1, | |
| offset: Math.PI, // 180 degrees out of phase with hip | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "toe_joint_left", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.2, | |
| max: 0.2, | |
| speed: 1, | |
| offset: Math.PI * 1.5, // 270 degrees phase | |
| isDegrees: false, | |
| }, | |
| // Right leg (with appropriate phase shift to alternate with left leg) | |
| { | |
| name: "hip_abduction_right", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.1, | |
| max: 0.1, | |
| speed: 1, | |
| offset: Math.PI, // 180 degrees out of phase with left side | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "hip_rotation_right", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.2, | |
| max: 0.2, | |
| speed: 1, | |
| offset: Math.PI + Math.PI / 2, // 180 + 90 degrees phase | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "hip_flexion_right", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.3, | |
| max: 0.6, | |
| speed: 1, | |
| offset: Math.PI, // 180 degrees out of phase with left hip | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "knee_joint_right", // Assuming this joint exists | |
| type: "sine", | |
| min: 0.2, | |
| max: 1.4, | |
| speed: 1, | |
| offset: Math.PI + Math.PI / 2, // 180 + 90 degrees phase | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "ankle_joint_right", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.4, | |
| max: 0.1, | |
| speed: 1, | |
| offset: 0, // 180 + 180 degrees = 360 = 0 | |
| isDegrees: false, | |
| }, | |
| { | |
| name: "toe_joint_right", // Assuming this joint exists | |
| type: "sine", | |
| min: -0.2, | |
| max: 0.2, | |
| speed: 1, | |
| offset: Math.PI / 2, // 180 + 270 = 450 degrees = 90 degrees | |
| isDegrees: false, | |
| }, | |
| ], | |
| }; | |