Draw2d in Python

Sun, Jun 20, 2004 with tags python , code , opengl , hacks

About two years ago, I wrote a [simple 2d drawing program][http://patrick.wagstrom.net/projects/academic/cs411/prog1/] for an undergraduate computer graphics class. I got bored today and somehow decided that I need to play around with Python, pygame, and OpenGL some more, so I decided to redo it. It ended up being a lot easier than I thought it would be, and the most difficult part was remembering what all the commands to the input file meant. You can [grab the code here][http://patrick.wagstrom.net/projects/personal/misc/pygame/draw2d.py]. Or, to make my blog more interesting, I’ve pasted it here:

# draw2d.py - a simple engine for drawing 2d shapes on an OpenGL canvas
#
# Copyright (c) 2004 Patrick Wagstrom
#
# This is a rewrite of a program I originally wrote an undergraduate computer
# science course in the Spring of 2002.  Basically this reads in a file and
# uses that information to build an image.

from OpenGL.GL import *
from OpenGL.GLU import *
import pygame
import Numeric
import sys
from pygame.locals import *

class DrawObject:
    def __init__(self):
        self.color = [0.0, 0.0, 0.0, 1.0]
        self.translate = [0.0, 0.0, 0.0]
        self.rotate = [0.0, 0.0, 0.0, 1.0]
        self.scale = [1.0, 1.0, 1.0]
        self.type = None
        self.filled = False
        self.pts = []

    def draw(self):
        """Draws the object.

        This could probably made faster if self.type was not a string
        """
        glColor(self.color)
        glTranslate(self.translate[0], self.translate[1], self.translate[2])
        glRotate(self.rotate[0], self.rotate[1], self.rotate[2], self.rotate[3])
        glScale(self.scale[0], self.scale[1], self.scale[2])
        if self.type == "RECT":
            if self.filled:
                glBegin(GL_POLYGON);
            else:
                glBegin(GL_LINE_LOOP);
            glVertex(self.pts[0], self.pts[1])
            glVertex(self.pts[2], self.pts[1])
            glVertex(self.pts[2], self.pts[3])
            glVertex(self.pts[0], self.pts[3])
            glEnd()
        elif self.type == "CIRC":
            if self.filled:
                glBegin(GL_POLYGON)
            else:
                glBegin(GL_LINE_LOOP)
            circleSections = 100;
            for x in xrange(circleSections):
                angle = 2 * Numeric.pi * x / circleSections
                glVertex2f(self.pts[0]+Numeric.cos(angle)*self.pts[2],
                           self.pts[1]+Numeric.sin(angle)*self.pts[2])
            glEnd()
        elif self.type == "PLINE":
            glBegin(GL_LINE_STRIP)
            for x in xrange(0,len(self.pts),2):
                glVertex(self.pts[x], self.pts[x+1])
            glEnd()
        elif self.type == "POLY":
            if self.filled:
                glBegin(GL_POLYGON)
            else:
                glBegin(GL_LINE_LOOP)
            for x in xrange(0, len(self.pts),2):
                glVertex(self.pts[x], self.pts[x+1])
            glEnd()

    def __repr__(self):
        return "<DrawObject:%s color=%r translate=%r rotate=%r pts=%r>" % (self.type, self.color, self.translate, self.rotate, self.pts)




def load(filename):
    """Reads in a file.

    Here are the valid commands for the file:

    # Add a rectangle:
    ar <ll-corner-x> <ll-corner-y> <ur-corner-x> <ur-corner-y>

    # Add a circle:
    ac <center-x> <center-y> <radius>

    # Add a polyline:
    al <num-vertices> <v1-x> <v1-y> <v2-x> <v2-y> ...

    # Add a polygon:
    ap <num-vertices> <v1-x> <v1-y> <v2-x> <v2-y> ...

    # Set color:
    sc <R> <G> <B>

    # Set fill mode:
    sf <0.0|1.0>

    # Set translation:
    st <trans-x> <trans-y>

    # Set rotation:
    sr <deg-angle>

    # Set scale:
    ss <scale-x> <scale-y>

    The translations carry over until the next object but are NOT cumulative.  Thus
    doing a "st 1.0 1.0" twice in a row does NOT do the same as "st 2.0 2.0".

    @type  filename: string
    @param filename: name of the file to load

    @rtype: list
    @return: list of DrawObject
    """

    f = open(filename)
    currentColor = [0.0, 0.0, 0.0, 1.0]
    currentTranslate = [0.0, 0.0, 0.0]
    currentFilled = False
    currentRotate = [0.0, 0.0, 0.0, 1.0]
    currentScale = [1.0, 1.0, 1.0]
    retArr = []
    for thisline in f.xreadlines():
        cmd = thisline.split()
        if len(cmd) != 0 and thisline[0] != "#":
            o = None
            args = [float(x) for x in cmd[1:]]
            cmd = cmd[0].lower()
            if cmd == "sc":
                currentColor[0:3] = args[0:3]
            elif cmd == "st":
                currentTranslate[0:2] = args[0:2]
            elif cmd == "sr":
                currentRotate[0] = args[0]
            elif cmd == "sf":
                if args[0] > 0.5:
                    currentFilled = True
                else:
                    currentFilled = False
            elif cmd == "ss":
                currentScale[0:2] = args[0:2]
            elif cmd == "ar":
                o = DrawObject()
                o.type = "RECT"
            elif cmd == "ac":
                o = DrawObject()
                o.type = "CIRC"
            elif cmd == "al":
                o = DrawObject()
                o.type = "PLINE"
                args = args[len(args)%2:]
            elif cmd == "ap":
                o = DrawObject()
                o.type = "POLY"
                args = args[len(args)%2:]
            if o:
                for x in xrange(len(currentColor)):
                    o.color[x] = currentColor[x]
                for x in xrange(len(currentTranslate)):
                    o.translate[x] = currentTranslate[x]
                o.filled = currentFilled
                for x in xrange(len(currentRotate)):
                    o.rotate[x] = currentRotate[x]
                o.pts = args
                for x in xrange(len(currentScale)):
                    o.scale[x] = currentScale[x]
                retArr.append(o)
    f.close()
    return retArr


def draw(objList):
    """Draws all the objects in the list

    @type  objList: list
    @param objList: list of objects to draw
    """
    glClear(GL_COLOR_BUFFER_BIT)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-10.0, 10.0, -10.0, 10.0, -1.0, 1.0)
    glMatrixMode(GL_MODELVIEW)

    for mem in objList:
        glPushMatrix()
        mem.draw()
        glPopMatrix()

    glFlush()

def resize((width, height)):
    """Handles a resize of the display

    @type  width: int
    @param width: new width of display
    @type  height: int
    @param heignt: int
    """
    if height == 0:
        height = 1
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-10.0, 10.0, -10.0, 10.0, -1.0, 1.0)
    glViewport(0, 0, width, height)

def init():
    """Initializes the OpenGL display
    """
    glShadeModel(GL_SMOOTH)
    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClearDepth(1.0)
    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LEQUAL)
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
    glLineWidth(2.0)

def main(infile):
    """Runs the main program

    The program is quit by hitting ESCAPE or closing the window

    @type infile: string
    @param infile: name of file to load
    """
    pygame.init()
    pygame.display.set_mode((500,500), OPENGL|DOUBLEBUF)
    pygame.display.set_caption(sys.argv[0])
    resize((500,500))
    init()
    objects = load(infile)

    frames = 0
    ticks = pygame.time.get_ticks()
    while 1:
        event = pygame.event.poll()
        if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
            break;
        draw(objects)
        pygame.display.flip()
        frames = frames + 1

    print "fps:  %d" % ((frames * 1000) / (pygame.time.get_ticks() - ticks))


if __name__ == "__main__":
    main(sys.argv[1])