# Filename: A09b_2D_vector_sandbox.py
# Written by: James D. Miller
import sys, os
import pygame
# PyGame Constants
from pygame.locals import *
from pygame.color import THECOLORS
# PyGame gui
from pgu import gui
# Import the vector class from a local module (in this same directory)
from vec2d_jdm import Vec2D
#=====================================================================
# Classes
#=====================================================================
class Vectors_Add():
def __init__(self, vectors_2d_m=None, theColor=THECOLORS["green"]):
self.color = theColor
self.selectable = False
self.radius_m = 0.10
self.radius_px = int(round(env.px_from_m( self.radius_m)))
if vectors_2d_m == None:
self.vectors_2d_m = v_sb.vectors
else:
self.vectors_2d_m = vectors_2d_m
self.total_2d_m = Vec2D(0,0)
self.total_history_2d_m = []
self.total_history_tuples_px = []
self.needing_reset = False
def update(self, mode="reset"):
self.total_2d_m = Vec2D(0,0)
for eachvector_2d_m in self.vectors_2d_m:
if mode == "add":
# Change the base vector to the running total.
eachvector_2d_m.base_2d_m = self.total_2d_m
self.total_2d_m += eachvector_2d_m
else:
eachvector_2d_m.base_2d_m = Vec2D(0,0)
# Put the current total vector into a FIFO.
if (v_sb.tail_time_s > v_sb.tail_timelimit_s) and v_sb.enable_tails:
self.total_history_2d_m.append( self.total_2d_m)
# Displace the oldest part of the FIFO; pop it off.
if len(self.total_history_2d_m) > 700:
self.total_history_2d_m.pop(0)
v_sb.tail_time_s = 0
def draw_circle_vector(self, vec_A_2d_m, vec_B_2d_m, color=None, small_circle=False):
if color==None:
color = self.color
# Draw line segment
line_points = [env.ConvertWorldToScreen(vec_A_2d_m), env.ConvertWorldToScreen(vec_B_2d_m)]
pygame.draw.aaline(game_window.surface, color, line_points[0], line_points[1], True)
# Draw circle.
self.radius_px = int(round(env.px_from_m( self.radius_m)))
if small_circle:
radius_px = int(round(self.radius_px/2.0))
else:
radius_px = self.radius_px
if radius_px < 1:
radius_px = 1
pygame.draw.circle(game_window.surface, color, env.ConvertWorldToScreen(vec_B_2d_m), radius_px, 1)
def draw(self):
if v_sb.display_total:
self.update("add")
# Draw the total vector.
Visual_Vec2D( self.total_2d_m, theColor=self.color).draw()
self.needing_reset = True
# Draw the total vector's history.
if v_sb.enable_tails:
if v_sb.lines_not_points:
if len(self.total_history_2d_m) >= 2:
self.total_history_tuples_px = []
for point_2d_m in self.total_history_2d_m:
self.total_history_tuples_px.append( env.ConvertWorldToScreen( point_2d_m))
#pygame.draw.lines(game_window.surface, self.color, False, self.total_history_tuples_px, 1)
pygame.draw.aalines(game_window.surface, self.color, False, self.total_history_tuples_px, True)
else:
for point_2d_m in self.total_history_2d_m:
pygame.draw.circle(game_window.surface, self.color, env.ConvertWorldToScreen( point_2d_m), 1, 1)
else:
if self.needing_reset:
# Reset these so all are drawn from the origin.
self.update("reset")
self.needing_reset = False
class Visual_Vec2D( Vec2D):
def __init__(self, x_m_or_Vec2D, y_m=None, theColor=THECOLORS["yellow"], rotation_rate_dps=0):
if isinstance( x_m_or_Vec2D, Vec2D):
x_m = x_m_or_Vec2D.x
y_m = x_m_or_Vec2D.y
else:
x_m = x_m_or_Vec2D
y_m = y_m
Vec2D.__init__(self, x_m, y_m)
self.rotation_rate_dps = rotation_rate_dps
self.radius_m = 0.10
self.color = theColor
self.selectable = True
self.selected = False
# Projection target
self.projection_target_2d_m = None
# Base vector (to displace the drawing of the vector from the origin)
self.base_2d_m = Vec2D(0.0,0.0)
# Arrowhead definition.
self.sf_x = 0.3
self.sf_y = 0.1
self.arrowhead_vertices_2d_m =[Vec2D( -1.00 * self.sf_x, -0.50 * self.sf_y),
Vec2D( -1.00 * self.sf_x, 0.50 * self.sf_y),
Vec2D( 0.00 * self.sf_x, 0.00 * self.sf_y)]
self.rt_vertices_2d_px = []
self.update()
def update(self):
# Rotate this vector a wee bit during each time step.
if (not env.freeze) and (not self.selected):
self.rotated(self.rotation_rate_dps * env.dt_s, sameVector=True)
self.angle_deg = self.get_angle()
# Rotate and translate the arrow head and convert for screen display.
self.rotate_and_translate_vertices( self.arrowhead_vertices_2d_m, self.angle_deg)
def rotate_and_translate_vertices(self, vertices_2d_m, angle_deg):
# Put modified vectors in a new list.
self.rt_vertices_2d_px = []
for vertex_2d_m in vertices_2d_m:
# Rotated and translated (add to the shaft).
rt_vertex_2d_m = vertex_2d_m.rotated( angle_deg) + self + self.base_2d_m
# Convert for screen display.
rt_vertex_2d_px = env.ConvertWorldToScreen( rt_vertex_2d_m)
self.rt_vertices_2d_px.append( rt_vertex_2d_px)
def draw_selection_circle(self):
# Draw circle.
radius_px = int(round(env.px_from_m( self.radius_m)))
if radius_px < 1:
radius_px = 1
pygame.draw.circle(game_window.surface, self.color, env.ConvertWorldToScreen(self), radius_px, 1)
def draw_vector(self):
# Draw main body of the arrow: line from base point to end point.
line_points = [env.ConvertWorldToScreen(self.base_2d_m), env.ConvertWorldToScreen(self + self.base_2d_m)]
pygame.draw.aaline(game_window.surface, self.color, line_points[0], line_points[1], True)
#pygame.draw.line(game_window.surface, self.color, line_points[0], line_points[1], 2)
# Draw the arrowhead
if self.length_squared() > 0:
if self.base_2d_m.equal(Vec2D(0.0,0.0)):
arrow_head_line_thickness = 0
else:
arrow_head_line_thickness = 1
pygame.draw.polygon(game_window.surface, self.color, self.rt_vertices_2d_px, arrow_head_line_thickness)
# Draw a selection circle if there is a non-zero base vector.
if self.base_2d_m.not_equal( Vec2D(0,0)):
self.draw_selection_circle()
def draw(self):
self.update()
# Main vector
self.draw_vector()
if (self.selected and v_sb.enable_components):
# Its two components
Visual_Vec2D( self.x, 0, self.color).draw_vector()
Visual_Vec2D( 0, self.y, self.color).draw_vector()
# The normal
if self.length_squared() > 0:
normal_2d_m = self.normal() # This returns a Vec2D object.
Visual_Vec2D( normal_2d_m, theColor=THECOLORS["red"]).draw_vector()
Visual_Vec2D( normal_2d_m.rotate90(), theColor=THECOLORS["red"]).draw_vector()
Visual_Vec2D( self.projection_onto( self.projection_target_2d_m), theColor=self.color).draw_vector()
class VectorSandbox:
def __init__(self, walls_dic):
self.vectors = []
self.walls = walls_dic
self.selected_vector = None
self.total_vector_2d_m = None
self.display_total = False
self.enable_components = True
self.enable_tails = False
self.lines_not_points = False
self.tail_time_s = 0
self.tail_timelimit_s = 1/30.0
def draw(self):
#{"L_m":0.0, "R_m":10.0, "B_m":0.0, "T_m":10.0}
# Define endpoints for each axis.
x_pos_2d_px = env.ConvertWorldToScreen( Vec2D( self.walls['R_m'], 0.0 ))
x_neg_2d_px = env.ConvertWorldToScreen( Vec2D( -self.walls['R_m'], 0.0 ))
y_pos_2d_px = env.ConvertWorldToScreen( Vec2D( 0.0, self.walls['T_m']))
y_neg_2d_px = env.ConvertWorldToScreen( Vec2D( 0.0, -self.walls['T_m']))
# Draw the two axes.
pygame.draw.line(game_window.surface, THECOLORS["orangered1"], x_pos_2d_px, x_neg_2d_px, 1)
pygame.draw.line(game_window.surface, THECOLORS["orangered1"], y_pos_2d_px, y_neg_2d_px, 1)
def checkForVectorAtCursorPosition(self, x_px_or_tuple, y_px = None):
if y_px == None:
x_px = x_px_or_tuple[0]
y_px = x_px_or_tuple[1]
else:
x_px = x_px_or_tuple
y_px = y_px
test_position_2d_m = env.ConvertScreenToWorld(Vec2D(x_px, y_px))
for vector in self.vectors:
if vector.selectable:
vector_difference_2d_m = test_position_2d_m - vector
# Use squared lengths for speed (avoid square root)
mag_of_difference_m2 = vector_difference_2d_m.length_squared()
if mag_of_difference_m2 < vector.radius_m**2:
vector.selected = True
return vector
return None
def updateSelectedVector(self):
if (self.selected_vector == None):
if env.buttonIsStillDown:
self.selected_vector = self.checkForVectorAtCursorPosition(env.cursor_location_px)
else:
if not env.buttonIsStillDown:
# Deselect the vector and bomb out of here.
self.selected_vector.selected = False
self.selected_vector = None
return None
else:
# Drag the vector to follow the cursor.
cursor_pos_2d_m = env.ConvertScreenToWorld(Vec2D(env.cursor_location_px))
self.selected_vector.x, self.selected_vector.y = cursor_pos_2d_m.x, cursor_pos_2d_m.y
class Environment:
def __init__(self, screenSize_px, length_x_m):
self.screenSize_px = Vec2D(screenSize_px)
self.viewOffset_px = Vec2D(-self.screenSize_px.x/2,-self.screenSize_px.y/2)
self.viewCenter_px = Vec2D(0,0)
self.viewZoom = 1
self.viewZoom_rate = 0.01
self.key_b = 'U'
self.key_n = 'U'
self.key_m = 'U'
self.key_h = 'U'
self.px_to_m = length_x_m/float(self.screenSize_px.x)
self.m_to_px = (float(self.screenSize_px.x)/length_x_m)
self.inhibit_screen_clears = False
# Keyboard/mouse state
self.cursor_location_px = (0,0) # x_px, y_px
self.mouse_button = 1 # 1, 2, or 3
self.buttonIsStillDown = False
self.selected_vector = None
self.dt_s = 0
self.freeze = False
# Convert from meters to pixels
def px_from_m(self, dx_m):
return dx_m * self.m_to_px * self.viewZoom
# Convert from pixels to meters
# Note: still floating values here)
def m_from_px(self, dx_px):
return float(dx_px) * self.px_to_m / self.viewZoom
def control_zoom_and_view(self):
if self.key_h == "D":
self.viewZoom += self.viewZoom_rate * self.viewZoom
if self.key_n == "D":
self.viewZoom -= self.viewZoom_rate * self.viewZoom
def ConvertScreenToWorld(self, point_2d_px):
#self.viewOffset_px = self.viewCenter_px
x_m = ( point_2d_px.x + self.viewOffset_px.x) / (self.m_to_px * self.viewZoom)
y_m = (self.screenSize_px.y - point_2d_px.y + self.viewOffset_px.y) / (self.m_to_px * self.viewZoom)
return Vec2D( x_m, y_m)
def ConvertWorldToScreen(self, point_2d_m):
"""
Convert from world to screen coordinates (pixels).
In the class instance, we store a zoom factor, an offset indicating where
the view extents start at, and the screen size (in pixels).
"""
# self.viewOffset = self.viewCenter - self.screenSize_px/2
#self.viewOffset = self.viewCenter_px
x_px = (point_2d_m.x * self.m_to_px * self.viewZoom) - self.viewOffset_px.x
y_px = (point_2d_m.y * self.m_to_px * self.viewZoom) - self.viewOffset_px.y
y_px = self.screenSize_px.y - y_px
# Return a tuple of integers.
return Vec2D(x_px, y_px, "int").tuple()
def get_user_input(self):
# Get all the events since the last call to get().
for event in pygame.event.get():
if (event.type == pygame.QUIT):
sys.exit()
elif (event.type == pygame.KEYDOWN):
if (event.key == K_ESCAPE):
sys.exit()
elif (event.key==K_KP1):
return "1p"
elif (event.key==K_KP2):
return "2p"
elif (event.key==K_KP3):
return "3p"
elif (event.key==K_1):
return 1
elif (event.key==K_2):
return 2
elif (event.key==K_3):
return 3
elif (event.key==K_4):
return 4
elif (event.key==K_5):
return 5
elif (event.key==K_6):
return 6
elif (event.key==K_7):
return 7
elif (event.key==K_8):
return 8
elif (event.key==K_9):
return 9
elif (event.key==K_0):
return 0
# Sandbox control toggles.
elif (event.key==K_c):
v_sb.enable_components = not v_sb.enable_components
elif (event.key==K_t):
v_sb.enable_tails = not v_sb.enable_tails
elif (event.key==K_f):
env.freeze = not env.freeze
elif (event.key==K_a):
v_sb.display_total = not v_sb.display_total
elif (event.key==K_l):
v_sb.lines_not_points = not v_sb.lines_not_points
# Zoom keys.
elif (event.key==K_b):
self.key_b = 'D'
elif (event.key==K_n):
self.key_n = 'D'
elif (event.key==K_m):
self.key_m = 'D'
elif (event.key==K_h):
self.key_h = 'D'
else:
return "nothing set up for this key"
elif (event.type == pygame.KEYUP):
# Zoom keys
if (event.key==K_b):
self.key_b = 'U'
elif (event.key==K_n):
self.key_n = 'U'
elif (event.key==K_m):
self.key_m = 'U'
elif (event.key==K_h):
self.key_h = 'U'
elif event.type == pygame.MOUSEBUTTONDOWN:
self.buttonIsStillDown = True
(button1, button2, button3) = pygame.mouse.get_pressed()
if button1:
self.mouse_button = 1
elif button2:
self.mouse_button = 2
elif button3:
self.mouse_button = 3
else:
self.mouse_button = 0
elif event.type == pygame.MOUSEBUTTONUP:
self.buttonIsStillDown = False
self.mouse_button = 0
if self.buttonIsStillDown:
self.cursor_location_px = (mouseX, mouseY) = pygame.mouse.get_pos()
class GameWindow:
def __init__(self, screen_tuple_px, title):
self.width_px = screen_tuple_px[0]
self.height_px = screen_tuple_px[1]
# The initial World position vector of the Upper Right corner of the screen.
# Yes, that's right y_px = 0 for UR.
self.UR_2d_m = env.ConvertScreenToWorld(Vec2D(self.width_px, 0))
# Create a reference to the display surface object. This is a pygame "surface".
# Screen dimensions in pixels (tuple)
self.surface = pygame.display.set_mode(screen_tuple_px)
self.update_caption(title)
self.surface.fill(THECOLORS["black"])
pygame.display.update()
def update_caption(self, title):
pygame.display.set_caption( title)
self.caption = title
def update(self):
pygame.display.update()
def clear(self):
# Useful for shifting between the various demos.
self.surface.fill(THECOLORS["black"])
pygame.display.update()
#===========================================================
# Functions
#===========================================================
def make_some_vectors(resetmode):
game_window.update_caption("Vector Sandbox V.1: Demo #" + str(resetmode))
env.inhibit_screen_clears = False
env.always_render = False
if resetmode == 1:
v_1 = Visual_Vec2D( 0.0, 4.0, THECOLORS["white"], rotation_rate_dps=-20)
v_sb.vectors.append( v_1)
v_2 = Visual_Vec2D( 0.0, 1.0, THECOLORS["yellow"], rotation_rate_dps=180)
v_sb.vectors.append( v_2)
v_1.projection_target_2d_m = v_2
v_2.projection_target_2d_m = v_1
v_sb.total_vector_2d_m = Vectors_Add()
elif resetmode == 2:
v_1 = Visual_Vec2D( 0.0, 3.0, THECOLORS["white"], rotation_rate_dps=-20)
v_sb.vectors.append( v_1)
v_2 = Visual_Vec2D( 0.0, 2.0, THECOLORS["yellow"], rotation_rate_dps=-40)
v_sb.vectors.append( v_2)
v_1.projection_target_2d_m = v_2
v_2.projection_target_2d_m = v_1
v_3 = Visual_Vec2D( 0.0, 1.0, THECOLORS["tan"], rotation_rate_dps=-60)
v_sb.vectors.append( v_3)
v_3.projection_target_2d_m = v_1
v_sb.total_vector_2d_m = Vectors_Add()
elif resetmode == 3:
for j in range(1,11):
temp = Visual_Vec2D( 0.0, 3, THECOLORS["white"], rotation_rate_dps=-(10*j))
v_sb.vectors.append( temp)
v_sb.vectors[j-1].projection_target_2d_m = v_sb.vectors[0]
v_sb.total_vector_2d_m = Vectors_Add()
elif resetmode == 4:
for j in range(1,11):
temp = Visual_Vec2D( 0.0, 0.4*j, THECOLORS["white"], rotation_rate_dps=-(10*j))
v_sb.vectors.append( temp)
v_sb.vectors[j-1].projection_target_2d_m = v_sb.vectors[0]
v_sb.total_vector_2d_m = Vectors_Add()
elif resetmode == 5:
for j in range(1,11):
temp = Visual_Vec2D( 0.0, 3-0.2*j, THECOLORS["white"], rotation_rate_dps=-(10*j))
v_sb.vectors.append( temp)
v_sb.vectors[j-1].projection_target_2d_m = v_sb.vectors[0]
v_sb.total_vector_2d_m = Vectors_Add()
elif resetmode == 6:
for j in range(1,41):
temp = Visual_Vec2D( 0.0, 0.4*j, THECOLORS["white"], rotation_rate_dps=-(10*j))
v_sb.vectors.append( temp)
v_sb.vectors[j-1].projection_target_2d_m = v_sb.vectors[0]
v_sb.total_vector_2d_m = Vectors_Add()
elif resetmode == 7:
for j in range(1,141):
temp = Visual_Vec2D( 0.0, 0.4*j, THECOLORS["white"], rotation_rate_dps=-(10*j))
v_sb.vectors.append( temp)
v_sb.vectors[j-1].projection_target_2d_m = v_sb.vectors[0]
v_sb.total_vector_2d_m = Vectors_Add()
else:
print "Nothing set up for this key."
def display_number(numeric_value, font_object, mode='FPS'):
if mode=='FPS':
# Small background rectangle for FPS text
pygame.draw.rect(game_window.surface, THECOLORS["white"], pygame.Rect(10, 10, 35, 20))
# The text
fps_string = "%.0f" % numeric_value
txt_surface = font_object.render(fps_string, True, THECOLORS["black"])
game_window.surface.blit(txt_surface, [18, 11])
elif mode=='gameTimer':
# The text
fps_string = "%.2f" % numeric_value
txt_surface = font_object.render(fps_string, True, THECOLORS["white"])
game_window.surface.blit(txt_surface, [600, 11])
#============================================================
# Main procedural script.
#============================================================
def main():
# A few globals.
global env, game_window, v_sb
pygame.init()
myclock = pygame.time.Clock()
window_dimensions_px = (800, 700) #window_width_px, window_height_px
# Create the first user/client and the methods for moving between the screen and the world.
env = Environment(window_dimensions_px, 10.0) # 10m in along the x axis.
game_window = GameWindow(window_dimensions_px, 'Vector Sandbox V.1')
# Define the Left, Right, Bottom, and Top boundaries of the game window.
v_sb = VectorSandbox({"L_m":0.0, "R_m":game_window.UR_2d_m.x, "B_m":0.0, "T_m":game_window.UR_2d_m.y})
# Add some vectors to the table.
demo_mode = 1
make_some_vectors( demo_mode)
# Font object for rendering text onto display surface.
fnt_FPS = pygame.font.SysFont("Arial", 14)
fnt_gameTimer = pygame.font.SysFont("Arial", 60)
# Limit the framerate, but let it float below this limit.
framerate_limit = 480.0 # 480
while True:
env.dt_s = float(myclock.tick( framerate_limit) * 1e-3)
# Listen to the user: establish the state of the keyboard and mouse and determine if a new demo is called for.
resetmode = env.get_user_input()
# Reset the game based on local user control.
if resetmode in ["1p","2p","3p",1,2,3,4,5,6,7,8,9,0]:
demo_mode = resetmode
print resetmode
# Delete all the objects in the sandbox. Cleaning out these list reference to these objects effectively
# deletes the objects.
v_sb.total_vector_2d_m = None
v_sb.vectors = []
# Now just black out the screen.
game_window.clear()
# Reinitialize the demo.
make_some_vectors( demo_mode)
# Control vector selection and mouse related movement.
v_sb.updateSelectedVector()
# Control the zoom
env.control_zoom_and_view()
# Erase the blackboard.
if not env.inhibit_screen_clears:
game_window.surface.fill((0,0,0))
# Display FPS text.
display_number(1/env.dt_s, fnt_FPS, mode='FPS')
# Draw axis for the vector sandbox.
v_sb.draw()
# Draw vectors.
if v_sb.total_vector_2d_m != None:
v_sb.total_vector_2d_m.draw()
for eachvector in v_sb.vectors:
eachvector.draw()
pygame.display.flip()
v_sb.tail_time_s += env.dt_s
#============================================================
# Run the main program.
#============================================================
main()