small-projects/inverse-kinimatics/robot.py

238 lines
8.5 KiB
Python
Raw Normal View History

2025-04-11 01:15:38 +00:00
import pygame
import sys
import math
# -------------------------------
# Pygame and Global Setup
# -------------------------------
pygame.init()
screen_width, screen_height = 1000, 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Mars Rover Suspension Simulation")
clock = pygame.time.Clock()
# -------------------------------
# Colors and Constants
# -------------------------------
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
DARKGRAY = (50, 50, 50)
GREEN = (80, 200, 80)
BLUE = (100, 100, 255)
# Ground control point spacing and scrolling speed.
SEGMENT_SPACING = 1 # Horizontal spacing between control points
SCROLL_SPEED = 1 # Pixels per frame
# -------------------------------
# Ground: Dynamic "Plot"
# -------------------------------
# We keep a list of control points in screen coordinates.
ground_points = []
# Initialize control points so the ground covers a bit more than the screen width.
num_points = screen_width // SEGMENT_SPACING + 3
start_x = -SEGMENT_SPACING
for i in range(num_points):
# For the initial ground, set a default height (for example, 450)
ground_points.append([start_x + i * SEGMENT_SPACING, 450])
def update_ground(mouse_y):
"""
Update ground control points:
- Shift all points left by SCROLL_SPEED.
- If the leftmost point is off-screen, remove it.
- Append a new point at the right end with x determined by
last point + SEGMENT_SPACING and y given by current mouse_y.
"""
# Shift every point to the left.
for pt in ground_points:
pt[0] -= SCROLL_SPEED
# Remove points that have moved off-screen.
if ground_points[0][0] < -SEGMENT_SPACING:
ground_points.pop(0)
# Add a new control point at the right if needed.
if ground_points[-1][0] < screen_width + SEGMENT_SPACING:
new_x = ground_points[-1][0] + SEGMENT_SPACING
# Use the mouse's y (clamped to a reasonable range) for the new control point.
new_y = max(300, min(mouse_y, screen_height - 50))
ground_points.append([new_x, new_y])
def get_ground_y(x):
"""
Given an x coordinate (in screen space), interpolate the ground's
y value using the control points.
"""
# Find the segment that contains x.
for i in range(len(ground_points) - 1):
x0, y0 = ground_points[i]
x1, y1 = ground_points[i+1]
if x0 <= x <= x1:
t = (x - x0) / (x1 - x0)
return y0 * (1 - t) + y1 * t
# Fallback: if x is beyond our control points.
return ground_points[-1][1]
def draw_ground(surface):
"""Draw the ground as a polyline connecting all control points,
then fill below."""
pts = [(pt[0], pt[1]) for pt in ground_points if -SEGMENT_SPACING <= pt[0] <= screen_width + SEGMENT_SPACING]
if len(pts) >= 2:
pygame.draw.lines(surface, GREEN, False, pts, 4)
# Fill ground below the line.
fill_points = pts.copy()
fill_points.append((pts[-1][0], screen_height))
fill_points.append((pts[0][0], screen_height))
pygame.draw.polygon(surface, (200,255,200), fill_points)
# -------------------------------
# Rover Suspension Simulation
# -------------------------------
# The rover is fixed horizontally at the center.
rover_x = screen_width // 2
# Wheel horizontal offsets relative to rover center.
wheel_offsets = { 'left': -50, 'center': 0, 'right': 50 }
wheel_radius = 20
# Suspension parameters (vertical springs).
L_rest = 60 # Resting (ideal) suspension length.
k = 0.08 # Spring constant.
damping = 0.1 # Damping coefficient.
# For each wheel, we simulate the vertical position of the chassis attachment (the top of the suspension).
# We store for each wheel its current attachment y and vertical velocity.
suspension = {
'left': 450 - L_rest,
'center': 450 - L_rest,
'right': 450 - L_rest
}
suspension_vel = {'left': 0, 'center': 0, 'right': 0}
def update_suspension():
"""
Update suspension for each wheel.
For a given wheel, the desired chassis attachment y is:
ground_y(wheel_x) - L_rest
We then update the current attachment using a springdamper simulation.
"""
for key, offset in wheel_offsets.items():
# World x coordinate for the wheel.
x_pos = rover_x + offset
# The ground contact y at the wheel's x.
ground_contact = get_ground_y(x_pos)
desired_attach = ground_contact - L_rest
# Current state.
attach_y = suspension[key]
vel = suspension_vel[key]
# Compute spring force.
force = k * (desired_attach - attach_y)
# Acceleration (assume mass = 1 for simplicity).
acc = force - damping * vel
# Update velocity and position.
vel += acc
attach_y += vel
suspension[key] = attach_y
suspension_vel[key] = vel
def draw_spring(surface, start, end, coils=6, amplitude=8):
"""
Draws a zigzag spring between start and end.
"""
sx, sy = start
ex, ey = end
total_dist = math.hypot(ex - sx, ey - sy)
# Define number of segments.
num_points = coils * 2 + 1
points = []
for i in range(num_points):
t = i / (num_points - 1)
# Linear interpolation for y.
x = sx + t * (ex - sx)
y = sy + t * (ey - sy)
if i != 0 and i != num_points - 1:
offset = amplitude if i % 2 == 0 else -amplitude
# Offset perpendicular to the line (for vertical line, horizontal offset; here assume nearly vertical)
x += offset
points.append((x, y))
pygame.draw.lines(surface, DARKGRAY, False, points, 3)
def draw_rover(surface):
"""
Draw the rover.
- Wheels: circles drawn at their contact points on the ground.
- Suspension: springs from wheel contact to chassis attachment.
- Chassis: drawn as a rectangle whose top edge connects the left and right attachments.
"""
# Determine wheel positions.
wheels = {}
for key, offset in wheel_offsets.items():
world_x = rover_x + offset
world_y = get_ground_y(world_x)
wheels[key] = (world_x, world_y)
# Draw wheel.
pygame.draw.circle(surface, DARKGRAY, (int(world_x), int(world_y)), wheel_radius)
pygame.draw.circle(surface, BLACK, (int(world_x), int(world_y)), wheel_radius, 2)
# Update suspension so that each wheel has its chassis attachment position.
update_suspension()
attachments = {}
for key, offset in wheel_offsets.items():
attach_x = rover_x + offset
attach_y = suspension[key]
attachments[key] = (attach_x, attach_y)
# Draw spring from wheel contact to attachment.
draw_spring(surface, wheels[key], attachments[key], coils=8, amplitude=6)
# Draw the attachment point.
pygame.draw.circle(surface, BLACK, (int(attach_x), int(attach_y)), 4)
# Draw chassis.
# For simplicity, use the left and right attachment points to define the top edge.
left_attach = attachments['left']
right_attach = attachments['right']
# Compute the chassis center as the average of the three attachments.
center_attach = (
(attachments['left'][0] + attachments['center'][0] + attachments['right'][0]) / 3,
(attachments['left'][1] + attachments['center'][1] + attachments['right'][1]) / 3,
)
# Chassis properties.
chassis_height = 20
# The top edge will be the line from left_attach to right_attach.
# For a rough rectangle, offset the top edge downward by chassis_height to form the body.
top_edge = [left_attach, right_attach]
bottom_edge = [(left_attach[0], left_attach[1] + chassis_height),
(right_attach[0], right_attach[1] + chassis_height)]
body_points = [left_attach, right_attach, bottom_edge[1], bottom_edge[0]]
pygame.draw.polygon(surface, BLUE, body_points)
pygame.draw.polygon(surface, BLACK, body_points, 2)
# -------------------------------
# Main Simulation Loop
# -------------------------------
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# The mouse vertical position controls the next ground segment's height.
mouse_x, mouse_y = pygame.mouse.get_pos()
# Update ground control points.
update_ground(mouse_y)
# Clear the screen.
screen.fill(WHITE)
# Draw the ground.
draw_ground(screen)
# Draw the rover (suspension, wheels, chassis).
draw_rover(screen)
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()