Python CAD Tutorial 10 - Extending the GTK interface


View/Download Code

tut10-interface.png

Figure 1: extended gui

We have created some buttons in glade and connected the events so we can switch between the object types ie circle polygon and closed polygons.

Create a class for managing the treeview, this widget is very flexible and can be difficult to work with so abstract the details away. This will be used for showing the layers we have in the project and the objects under each layer.

import os
import sys
from gi.repository import Gtk, GdkX11, Gdk


class objectList:
    treeview = None
    treemodel = None

    selected = 'workspace'

    def __init__(self, workspace, treeview):
	self.treeview = treeview
	self.workspace = workspace

	# connect the treeview events, capture row selection and row click
	self.treeview.connect('row-activated', self.selection)
	self.treeview.connect('button_press_event', self.show_menu)

	#add a tree store model so everything has a hierarchy
	self.treemodel = Gtk.TreeStore(str)
	self.treeview.set_model(self.treemodel)

	# append colmun headers to the treeview and cell renderer
	column = Gtk.TreeViewColumn("Objects")
	self.treeview.append_column(column)

	cell = Gtk.CellRendererText()
	column.pack_start(cell, False)
	column.add_attribute(cell, "text", 0)

	# populate the objects from the layers
	self.populate()
	self.menu()

    def populate(self):
	self.treemodel.clear()
	for layer in self.workspace.keys():
	    tree_iter = self.append(layer)
	    for part in self.workspace[layer]:
		self.append(part.name, tree_iter)

    def menu(self):
	self.layer_menu = Gtk.Menu()

	self.layer_menu_item = Gtk.MenuItem("Display")
	#self.layer_menu_item.connect("activate", self.toggle_value_handler, 'show')
	self.layer_menu.append(self.layer_menu_item)

	self.layer_menu_item = Gtk.MenuItem("Colour")
	self.layer_menu.append(self.layer_menu_item)

    def show_menu(self, tv, event):
	if event.button == 2:
	    model = tv.get_model()
	    treeiter = model.get_iter(treepath)
	    self.selected = model.get_value(treeiter, 0)

	if event.button == 3:
	    self.layer_menu.show_all()
	    self.layer_menu.popup(None, None, None, None, 1, 0)

    def append(self, name, parent=None):
	myiter = self.treemodel.insert_after(parent, None)
	self.treemodel.set_value(myiter, 0, name)
	return myiter

    def selection(self, tv, treepath, tvcolumn):
	model = tv.get_model()
	treeiter = model.get_iter(treepath)
	self.selected = model.get_value(treeiter, 0)

The workspace class below is a management class. It will contain various methods for working with the visible objects. It will also manage our layers. For now we will add simple functions, for example 'append' (which will add objects to a layer).In the main mycad.py file, you can see that our new polygon is contained and updated through the shape class. Later on we will attach this to our GUI so we can dynamically add primitives as we need them.

class objectList:
    treeview = None
    treemodel = None

    selected = 'workspace'

    def __init__(self, workspace, treeview):
	self.treeview = treeview
	self.workspace = workspace

	# connect the treeview events, capture row selection and row click
	self.treeview.connect('row-activated', self.selection)
	self.treeview.connect('button_press_event', self.show_menu)

	#add a tree store model so everything has a hierarchy
	self.treemodel = Gtk.TreeStore(str)
	self.treeview.set_model(self.treemodel)

	# append colmun headers to the treeview and cell renderer
	column = Gtk.TreeViewColumn("Objects")
	self.treeview.append_column(column)

	cell = Gtk.CellRendererText()
	column.pack_start(cell, False)
	column.add_attribute(cell, "text", 0)

	# populate the objects from the layers
	self.populate()
	self.menu()

    def populate(self):
	self.treemodel.clear()
	for layer in self.workspace.keys():
	    tree_iter = self.append(layer)
	    for part in self.workspace[layer]:
		self.append(part.name, tree_iter)

    def menu(self):
	self.layer_menu = Gtk.Menu()

	self.layer_menu_item = Gtk.MenuItem("Display")
	#self.layer_menu_item.connect("activate", self.toggle_value_handler, 'show')
	self.layer_menu.append(self.layer_menu_item)

	self.layer_menu_item = Gtk.MenuItem("Colour")
	self.layer_menu.append(self.layer_menu_item)

    def show_menu(self, tv, event):
	if event.button == 2:
	    model = tv.get_model()
	    treeiter = model.get_iter(treepath)
	    self.selected = model.get_value(treeiter, 0)

	if event.button == 3:
	    self.layer_menu.show_all()
	    self.layer_menu.popup(None, None, None, None, 1, 0)

    def append(self, name, parent=None):
	myiter = self.treemodel.insert_after(parent, None)
	self.treemodel.set_value(myiter, 0, name)
	return myiter

    def selection(self, tv, treepath, tvcolumn):
	model = tv.get_model()
	treeiter = model.get_iter(treepath)
	self.selected = model.get_value(treeiter, 0)

Python CAD Tutorial 09 - Workspace class


View/Download Code

tut09-interface.png

Figure 1: workspace class

The workspace class below is a management class. It will contain various methods for working with the visible objects. It will also manage our layers. For now we will add simple functions, for example 'append' (which will add objects to a layer).In the main mycad.py file, you can see that our new polygon is contained and updated through the shape class. Later on we will attach this to our GUI so we can dynamically add primitives as we need them.

from cad.point import createpoint
from OpenGL.GL import *
from OpenGL.arrays import vbo
from numpy import array


#lets create a dict object we can name our layers from here
class workspace(dict):
    selected_shape = 'workspace'
    selected_part = None

    def reset(self):
	for k in self.keys():
	    del(self[k])
	self['workspace'] = shape.createshape()

    def create(self, item, name=None):
	if name is not None:
	    self.selected_part = name
	if self.selected_part is not None:
	    if not self.get(self.selected_part):
		self[self.selected_part] = shape.createshape()
	    self[self.selected_part].append(item)
	    self.selected_shape = self[self.selected_part].count()

    def append(self, item, name=None):
	self[self.selected_part].primitives[self.selected_shape].append(item)

    def objects_iter(self):
	for layer in self.keys():
	    for shape in self[layer]:
		yield shape

    def set(self, name, value):
	for layer in self.values():
	    for item in layer.primitives:
		item.display_color = value
		self.color = value

    #handle drawing our shapes here
    def draw(self):
	for item in self.values():
	    item.draw()

Python CAD Tutorial 08 - Drawing a polygon & add generic shape class


View/Download Code

tut08-interface.png

Figure 1: Polygon

We created a grid in the previous tutorial. Now let's add a polygon class and a shape object to contain our primitives.The polygon class is basically an extension of the line class. It works in the same way, however we have added an append method which keeps adding points to the polygon on each click. The draw method has then been adjusted to loop over and draw the polygon from the point list.

from point import createpoint


class createpolygon:
    name = 'polygon'
    points = []
    closed = True

    def __init__(self, point):
	self.points = []
	self.points.append(createpoint(point))
	self.color = 0, 1, 0
	self.display_color = 0, 1, 0
	self.newPoint = None

    def append(self, point):
	self.points.append(createpoint(point))

    def glvertex(self):
	glVertex3f(self.p1.x, self.p1.y, self.p1.z)

    #vertexs to draw a line use with glbegin(GL_LINES)
    def glvertexlist(self):
	glVertex3f(self.p1.x, self.p1.y, self.p1.z)
	glVertex3f(self.p2.x, self.p2.y, self.p2.z)

    def get_points(self):
	for p in self.points:
	    yield (p)

    def draw(self):
	glColor3f(*self.display_color)
	if len(self.points) > 1:
	    glBegin(GL_LINES)
	    for n in xrange(0, len(self.points) - 1):
		glVertex3f(self.points[n].x, self.points[n].y, self.points[n].z)
		glVertex3f(self.points[n + 1].x, self.points[n + 1].y, self.points[n + 1].z)
	    if self.closed is True:
		glVertex3f(self.points[0].x, self.points[0].y, self.points[0].z)
		glVertex3f(self.points[-1].x, self.points[-1].y, self.points[-1].z)
	    glEnd()

	for item in self.points:
	    item.draw()

The shape class below is basically just a container object for storing our various primitives like lines, circles and polygons. This class has methods to loop over and add the primitives store,. This will be useful later on for changing properties on mass.In the main mycad.py file you can see that our new polygon is contained and updated through the shape class. Later on we will attach this to our GUI so we can dynamically add primitives as we need them.

from point import createpoint


class createshape:
    location = createpoint((0, 0, 0))
    primitives = []

    def __init__(self, name=None):
	self.primitives = []

    def append(self, item):
	self.primitives.append(item)

    def __getitem__(self, n):
	return self.primitives[n]

    #temporary to get last primitive added to the scene
    def update_last(self):
	return self.primitives[-1]

    def count(self):
	return len(self.primitives) - 1

    def draw(self):
	for item in self.primitives:
	    item.draw()

Python CAD Tutorial 07 - Drawing a grid


View/Download Code

tut07-interface.png

Figure 1: Grid

We created a plane in the previous tutorial. Now we will inherit that class to create a grid, at a later stage we will add snap to grid functionality.The new grid class will take three parameters: a centre point, a spacing value and a size value. We will set the size value to be the size of the viewport and will set a default spacing between the lines.The new methods for the class are below:

  • The gridspacing method shifts the lines by the amount specified in the spacing parameter until the maximum size has been reached.
  • The draw method draws lines to make up the grid. On every tenth line it changes the colour to a darker line.

Future tutorials will deal with snap to grid functionality and auto calculating based on the zoom level, but for now we have something to work with.

import math

from point import createpoint
from plane import createplanesimple


class creategrid:
    display_color = (0.6, 0.6, 0.6)

    large_grid = 10
    small_grid = 2.5

    def __init__(self, p1, spacing=1.5, size=65):
	self.plane = createplanesimple(p1, size * 2)
	self.p1 = createpoint(p1)
	self.normal = createpoint((0, 1, 0))
	self.size = size
	self.small_grid = spacing
	self.large_grid = spacing * 10

    def grid_spacing(self):
	#work out how many times the grid units fit inside our grid size and make it a whole number
	size =  math.ceil(self.size / self.small_grid) * self.small_grid
	#adjust x by size so we can draw the lines
	x = self.p1.x - size
	#loop from start until our lines are greater than our max size
	while x < (self.p1.x + size):
	    x += self.small_grid
	    yield x

    def draw(self):
	self.plane.draw()
	glColor3f(*self.display_color)
	glBegin(GL_LINES)

	for item in self.grid_spacing():
	    #coordinate modulus large_grid (returns 0 if there is no remaineder), so lets draw a different colour line
	    if (item % self.large_grid) == 0:
		glColor3f(0.4, 0.4, 0.4)
	    else:
		glColor3f(*self.display_color)

	    glVertex3f(item, self.p1.y - self.size, self.p1.z)
	    glVertex3f(item, self.p1.y + self.size, self.p1.z)

	    glVertex3f(self.p1.x - self.size, item, self.p1.z)
	    glVertex3f(self.p1.x + self.size, item, self.p1.z)
	glEnd()

Python CAD Tutorial 06 - Drawing planes in 3D space


View/Download Code

tut06-interface.png

Figure 1: Planes

We will now add a plane class, this is another fundamental building block. It will also be used for various collision detections at a later stage.We have created two classes below:

  • The first class will be used for the grid in the next tutorial. A plane does not normally have a limit of size but in this we are limiting it for display purposes.'createplanesimple' takes a point in space and a size. It will then calculate the plane by appending the size to the x and y coridinates, z will remain fixed.
  • The second class is a more accurate way of representing a plane. Usually we only need to store a point in space and a direction, so by adding the coordinates together we can get a central point and direction this will be added at a later date.
from point import createpoint

class createplanesimple:
    def __init__(self, p1, plane_size=10):
	size = plane_size / 2
	self.p1 = createpoint((p1[0] + size, p1[1]+size, p1[2]))
	self.p2 = createpoint((p1[0] - size, p1[1]+size, p1[2]))
	self.p3 = createpoint((p1[0] + size, p1[1]-size, p1[2]))
	self.p4 = createpoint((p1[0] - size, p1[1]-size, p1[2]))
	self.normal = createpoint((0, 1, 0))

    def draw(self):
	glBegin(GL_TRIANGLE_STRIP)
	glColor3f(0.9, 0.9, 0.9)
	glNormal3f(self.normal.x, self.normal.y, self.normal.z)
	glVertex3f(self.p1.x, self.p1.y, self.p1.z)
	glVertex3f(self.p2.x, self.p2.y, self.p2.z)
	glVertex3f(self.p3.x, self.p3.y, self.p3.z)

	glVertex3f(self.p2.x, self.p2.y, self.p2.z)
	glVertex3f(self.p3.x, self.p3.y, self.p3.z)
	glVertex3f(self.p4.x, self.p4.y, self.p4.z)
	glEnd()
	self.p1.draw((0, 0, 1))
	self.p2.draw((0, 0, 1))
	self.p3.draw((0, 0, 1))
	self.p4.draw((0, 0, 1))


class createplane:
    def __init__(self, p1, p2, p3):
	self.p1 = createpoint(p1)
	self.p2 = createpoint(p2)
	self.p3 = createpoint(p3)
	self.normal = createpoint((0, 1, 0))


    def draw(self):
	glBegin(GL_TRIANGLE_STRIP)
	glColor3f(0.8, 0.8, 0.8)
	glNormal3f(self.normal.x, self.normal.y, self.normal.z)
	glVertex3f(self.p1.x, self.p1.y, self.p1.z)
	glVertex3f(self.p2.x, self.p2.y, self.p2.z)
	glVertex3f(self.p3.x, self.p3.y, self.p3.z)
	glEnd()
	self.p1.draw((1, 0, 0))
	self.p2.draw((0, 1, 0))
	self.p3.draw((0, 0, 1))

Python CAD Tutorial 05 - Drawing lines in 3D space


View/Download Code

tut05-interface.png

Figure 1: Lines

In this tutorial we will create a new line class. Initially this will be setup to create an unlimited number of lines, drawn on every second click of the mouse.Our new class will be similar to the point class, but will contain two points to store the start and stop coordinates of the lines. We will also adjust the draw method to draw lines instead of points.

from point import createpoint


class createline:
    p1 = p2 = None
    color = 0, 1, 0
    display_color = (0, 1, 0)

    def __init__(self, point1, point2):
	""" create the start and stop points and colour of the line"""
	self.p1 = createpoint(point1)
	self.p2 = createpoint(point2)

    def glvertex(self):
	""" Opengl vertex useful so we can dont have to glbegin and glend for each point"""
	glVertex3f(self.p1.x, self.p1.y, self.p1.z)
	glVertex3f(self.p2.x, self.p2.y, self.p2.z)

    def get_points(self):
	""" return the start and end point of the line could be used in a drawing loop"""
	for p in (self.p1, self.p2):
	    yield (p)

    def draw(self):
	""" lets draw the line baase on the cordinates, also draw the points for now """
	glColor3f(*self.display_color)

	glBegin(GL_LINES)
	glVertex3f(self.p1.x, self.p1.y, self.p1.z)
	glVertex3f(self.p2.x, self.p2.y, self.p2.z)
	glEnd()

	self.p1.draw()
	self.p2.draw()

Python CAD Tutorial 04 - Mouse coordinates in 3D space


View/Download Code

tut04-interface.png

Figure 1: Mouse coordinates

We have a camera and something displayed on the screen, now let's start taking input from the mouse. We will create a new mouse handling class which will allow us to store a series of clicks. We can also use the stored positions for previews at a later date.The new class contains a few simple methods:

  • append() to add a new click
  • count() to return the number of stored clicks
  • getclickposition() returns the last clicked position
  • getpoints() returns a list of the stored locations, and resets the list
  • clear() resets the stored coordinates.
class mouse_state:
    """ store mouse clicks so we know where to draw and what to create """
    button1 = 0
    button2 = 0
    button3 = 0
    cordinates = []
    x = y = 0

    def append(self, x, y, button=1):
	""" store a new mouse click """
	self.x = x
	self.y = y
	self.cordinates.append((x, y,))

    def get_click_position(self):
	""" return the position of the last click """
	return self.x, self.y

    def get_points(self):
	""" return all stored points we may want to store lots of points when drawing a line for example"""
	if len(self.cordinates) != 2:
	    return None
	result = self.cordinates
	self.cordinates = []
	return result

    def count(self):
	"""return number of stored points """
	return len(self.cordinates)

    def clear(self):
	self.x = self.y = 0
	self.cordinates = []

Let's add a new method to our camera class, which will convert mouse clicks in 2 dimensional space into 3 dimensional coordinates so we can position something on the screen.The 'getclickpoint' method below takes a tuple containing the x and y coordinates from the mouse. It then converts the y position because in opengl '0' is at the bottom, but drawing area widget has '0' at the top. Finally it uses gluUnProject to convert from 2d space to 3d space.

def get_click_point(self, pos):
    """ convert 2d click in the viewport to 3d point in space"""
    viewport = glGetIntegerv(GL_VIEWPORT)
    modelview = glGetDoublev(GL_MODELVIEW_MATRIX)
    projection = glGetDoublev(GL_PROJECTION_MATRIX)

    #convert screen ccordinate to opengl cordinates, this means modifying the y axes only
    x, y = pos[0], self.viewport[1] - pos[1]

    #use unproject to calculate the point and store the resutl in a point object
    return createpoint(gluUnProject(x, y, 0.20, modelview, projection, viewport), (250, 0, 0))

Let's handle the mouse click events in the drawing area; we will store the 2d x and y coordinates of a mouse click in our new mousestateclass. Now we adjust the ondraw method to draw a point at the location we clicked on the page, which gives us visual feedback that everything is working correctly.

def mouse_click(self, widget, event):
    self.mouse.append(event.x, event.y, event.button)
    self.test_point = self.camera.get_click_point(self.mouse.get_click_position())
    self.on_draw()

def on_draw(self, *args):
    """ Test code to make sure we can draw a pixel successfully,
    also test we can position our new camera class to lookat the pixel"""
    #lets not look directly at the point we are drawing, demonstrating we can lookat points in space
    self.camera.lookat.x = 20
    self.camera.lookat.y = 20
    self.camera.lookat.z = -20

    #recalculate our camera based on the new settings
    self.camera.update()

    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClear(GL_COLOR_BUFFER_BIT)
    self.glwrap.draw_start()

    #place a point we can lookat that will be positioned in our field of view.
    self.test_point.draw()

    self.glwrap.draw_finish()

Python CAD Tutorial 03 - Setup the camera for viewing our scene


View/Download Code


[[View/Download Code][https://code.launchpad.net/~oly/fabricad/tut03]

web.images.create(web.template.pathimage + '/cad/tut03-interface.png', title='Camera View')

In this tutorial we will deal with setting up the camera to view our workspace. The camera can be positioned and adjusted to change what is visible in the application. Look at the diagram below for the required parameters. The diagram uses the same name as the variables in the code, so you know what values to adjust to change the scene. Most should be self explanatory, for example 'camera location' and 'where we are looking'. The near and far planes set the size of the visible area, we can only see objects positioned inside this space. The field of view is the viewing angle and will adjust the size of the farplane accordingly, also changing how much is visible. The only other parameters you need are the viewport width and height, which is basically the size of the screen or window. In our case, it's the size of the drawingarea widget.

web.images.create(web.template.pathimage + '/cad/camera-view.svg', title='Camera View')

The code for this is mainly oriented around the two functions called 'gluPerspective' and 'gluLookAt'.

'gluLookAt' is relatively straight forward; it takes 3 x,y,z values: the position of the camera, the point we wish to look at and an up vector. In this case we are using the y axis as the up vector.

'gluPerspective' is where we set what is visible in the world. We pass in our near and far planes and field of view to set the view frustrum area. We need to call the update method whenever something changes like the cameras position.


web.pre.create(loadpartial(os.path.abspath(self.pathabsolute + '../examples/tut03/cad/camera.py'), 0)) web.page.section(web.pre.render()) web.page.section(web.googleplus.render())

tut03-interface.png

Figure 1: Setup the camera

In this tutorial we will deal with setting up the camera to view our workspace. The camera can be positioned and adjusted to change what is visible in the application. Look at the diagram below for the required parameters. The diagram uses the same name as the variables in the code, so you know what values to adjust to change the scene. Most should be self explanatory, for example 'camera location' and 'where we are looking'. The near and far planes set the size of the visible area, we can only see objects positioned inside this space. The field of view is the viewing angle and will adjust the size of the farplane accordingly, also changing how much is visible. The only other parameters you need are the viewport width and height, which is basically the size of the screen or window. In our case, it's the size of the drawingarea widget.

camera-view.svg

The code for this is mainly oriented around the two functions called 'gluPerspective' and 'gluLookAt'. 'gluLookAt' is relatively straight forward; it takes 3 x,y,z values: the position of the camera, the point we wish to look at and an up vector. In this case we are using the y axis as the up vector. 'gluPerspective' is where we set what is visible in the world. We pass in our near and far planes and field of view to set the view frustrum area. We need to call the update method whenever something changes like the cameras position.

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL import GLX
from OpenGL import GL
from ctypes import *
from point import createpoint


class createcamera:
    #your visible area in 2d space, cordinates of screen or window view space
    viewport = 400, 400
    viewport_aspect = viewport[0] / viewport[1]

    #how much of the world can we see, how wide is our sight
    field_of_view = 110

    #camera parameters position look direction rotation
    lookat = createpoint((0.0, 0.0, 100.0))
    location = createpoint((0.0, 0.0, 0.0))
    camera_rotation = 0.0

    #viewing depth of the camera, also know as the frustrum near and far planes
    near_plane = 10.0
    far_plane = 100.0

    #view frustrum / camera visible area properties,
    def __init__(self, width, height):
	self.viewport = width, height
	self.viewport_aspect = self.viewport[0] / self.viewport[1]

    def draw(self):
	pass

    #the window has been resized so recalculate display
    #also used in initial setup of screen
    def update(self):
	"""call this when ever the screen is updated,
	calculates what's visible and where the camera exists in space.
	also set what we are loking at in the world"""
	glViewport(0, 0, self.viewport[0], self.viewport[1])
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity()

	#setup camera parameters field of view, size of camera (ie the view window size)
	#set the frustrum parameters
	gluPerspective(self.field_of_view,
		       1.0 * self.viewport[0] / self.viewport[1],
		       self.near_plane, self.far_plane)

	#position the camera and look at something
	gluLookAt(self.location.x, self.location.y, self.location.z,
		  self.lookat.x, self.lookat.y, self.lookat.z,
		  0.0, 1.0, 0.0)

	glMatrixMode(GL_MODELVIEW)
	glLoadIdentity()

Python CAD Tutorial 02 - Draw points in 3d Space


View/Download Code

tut02-interface.png

Figure 1: Point in 3D space

In this tutorial we will create a 3D point class and position and draw it to the screen.

This will be a base class for storing positions in space, we will also implement code to display the points in this class. For now we will hard code a single point in the camera view. We do not have a working camera class yet, so this will be fixed with a position so we know its working.We will store the x, y and z positions and colour for the point along with the display size. The important methods here are the 'init' and the 'draw' method, the others are helper methods which we will use later on.The helper methods include:

  • The eq method will be usefull for testing if two points share the same location in space.
  • str will format the point into a string, which is usefull for debugging.
  • getposition will return a numeric version of the point as a tuple.
class createpoint:
    x = y = z = 0.0
    display_color = (0, 0, 1)

    def __init__(self, p, c=(0, 1, 0)):
	""" Position in 3d space as a tuple or list, and colour in tuple or list format"""
	self.point_size = 5
	self.color = c
	self.display_color = c
	self.x, self.y, self.z = p

    def get_position(self):
	""" Return the cordinates as a tuple"""
	return self.x, self.y, self.z

    def glvertex(self):
	""" Opengl vertex useful so we can dont have to glbegin and glend for each point"""
	glVertex3f(self.x, self.y, self.z)

    def __getitem__(self, index):
	""" Get a cordinate handy for use in for loops where we want to calculate things"""
	return (self.x, self.y, self.z)[index]

    def __str__(self):
	""" Print point cordinates useful for debugging"""
	return '(%s, %s, %s)' % (str(self.x), str(self.y), str(self.z))

    def __eq__(self, point):
	""" Equality test so we can test if points occupy same space"""
	return self.x == point.x and self.y == point.y and self.z == point.z

    def draw(self, c=(0, 1, 0)):
	""" Set the size of the point and render"""
	glPointSize(self.point_size)
	glBegin(GL_POINTS)
	glColor3f(self.color[0], self.color[1], self.color[2])
	glVertex3f(self.x, self.y, self.z)
	glEnd()

Update the draw method to test out new code works.

def on_draw(self, *args):
    """ Test code to make sure we can draw a pixel successfully can play with the parameters here"""
    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClear(GL_COLOR_BUFFER_BIT)
    self.glwrap.draw_start()
    test_point = point.createpoint((0,0,0.5))
    test_point.draw()

    self.glwrap.draw_finish()

Python CAD Tutorial 01 - Initial Application


View/Code Download

tut01-interface.png

Figure 1: Point in 3D space

I have decided to write a series of tutorials in the hope we will see some good CAD applications appear on linux. In my experience, the current options are very unstable or difficult to use. By writing this program as a series of tutorials, I hope it will encourage contribution to the project or inspire you to develop your own CAD solution. This will hopefully help anyone who would like to write this style of 3D modelling application. These tutorials will use python, opengl and gtk3 for development. I will not be focusing on performance in this application, but focus more on readability and a clean layout.

Tools used in development of this application are Glade Interface Designer and Geany IDE, developed on ubuntu 13.04. Both opengl and gtk are cross platform so these examples may work on other platforms and distributions, but have not been tested on anything other than ubuntu 13.04. ../../../images/cad/glade-icon.png ../../../images/cad/geany.jpeg

The initial GUI has been designed in Glade and has the following layout: a standard menu and toolbar at the top of the window, then from left to right we have a toolbar like in gimp, in the centre we will display our designs and on the right we will have an object list. At the bottom we will embed an interactive console and a status bar for future expansion.

The glade interface can be found in the code repository above to open up and adjust. If anyone wants to design a nicer GUI, I would be interested to see what you come up with. cad/glade-interface-01.png', title='Glade Interface' [[..../../images/cad/glade-icon.png]]

This initial program will contain just enough code to display the glade interface and display an opengl triangle on the screen to prove everything is setup and working and give us a base to build from.

To get started you will need to install the libraries below from synaptics or by running the command in terminal.

We then create a configure method which grabs the window X id and sets the size of the viewport (in this case the width and height of the drawing area widget). We then create a drawing start and finish method to call before and after we do any drawing to the screen.

The last test method is just a test to make sure every thing is setup and functioning correctly and displays the standard OpenGL coloured triangle in the new window.

sudo apt-get install python-gi python-opengl python-gobject python-xlib

This first class separates the OpenGL and xlib code out from the gtk interface. We could reuse the code in another application. We have to use the c library for some xlib functions because the python implementation is limited - it's only used to grab the current xdisplay for OpenGL rendering.

For some of this code you will need to look at the OpenGL documentations to understand some of the parameters. In the initialise method we setup the OpenGL display parameters we would like to turn on, for example alpha channels for transparency and disable double buffering. We also create a glx context for our OpenGL scene to be drawn to.

class gtkgl:
    """ These method do not seem to exist in python x11 library lets exploit the c methods """
    xlib = cdll.LoadLibrary('libX11.so')
    xlib.XOpenDisplay.argtypes = [c_char_p]
    xlib.XOpenDisplay.restype = POINTER(struct__XDisplay)
    xdisplay = xlib.XOpenDisplay("")
    display = Xlib.display.Display()
    attrs = []

    xwindow_id = None
    width = height = 200

    def __init__(self):
	""" Lets setup are opengl settings and create the context for our window """

	self.add_attribute(GLX.GLX_RGBA, True)
	self.add_attribute(GLX.GLX_RED_SIZE, 1)
	self.add_attribute(GLX.GLX_GREEN_SIZE, 1)
	self.add_attribute(GLX.GLX_BLUE_SIZE, 1)
	self.add_attribute(GLX.GLX_DOUBLEBUFFER, 0)

	xvinfo = GLX.glXChooseVisual(self.xdisplay, self.display.get_default_screen(), self.get_attributes())
	configs = GLX.glXChooseFBConfig(self.xdisplay, 0, None, byref(c_int()))
	self.context = GLX.glXCreateContext(self.xdisplay, xvinfo, None, True)

    def add_attribute(self, setting, value):
	"""a simple method to nicely add opengl parameters"""
	self.attrs.append(setting)
	self.attrs.append(value)

    def get_attributes(self):
	""" return our parameters in the expected structure"""
	attrs = self.attrs + [0, 0]
	return (c_int * len(attrs))(*attrs)

    def configure(self, wid):
	""" setup up a opengl viewport for the current window, grab the xwindow id and store for later usage"""
	self.xwindow_id = GdkX11.X11Window.get_xid(wid)
	if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
	    print ('configure failed running glXMakeCurrent')
	glViewport(0, 0, self.width, self.height)

    def draw_start(self):
	"""make cairo context current for drawing"""
	if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
	    print ('draw failed running glXMakeCurrent')

    def draw_finish(self):
	"""swap buffer when we have finished drawing"""
	GLX.glXSwapBuffers(self.xdisplay, self.xwindow_id)

    def test(self):
	"""Test method to draw something so we can make sure opengl is working and we can see something"""
	self.draw_start()

	glClearColor(0.0, 0.0, 0.0, 0.0)
	glClear(GL_COLOR_BUFFER_BIT)
	glBegin(GL_TRIANGLES)
	glIndexi(0)
	glColor3f(1.0, 0.0, 0.0)
	glVertex2i(0, 1)
	glIndexi(0)
	glColor3f(0.0, 1.0, 0.0)
	glVertex2i(-1, -1)
	glIndexi(0)
	glColor3f(0.0, 0.0, 1.0)
	glVertex2i(1, -1)
	glEnd()

	self.draw_finish()

This class will deal with the GUI interaction between GTK and OpenGL; we will capture mouse events, menu events and button events here and use them to adjust the OpenGL application.

The initialisation method will load in the interface from glade and grab the widgets we are interested in, at this stage we are only interested in the window and the drawing area widgets. We want to grab the close and draw events for these widgets and set a few parameters against the drawing widget.

Once we have the drawing area widget we create two methods: configure and draw. The first is called when setting up the drawing area or if we resize it or similar happens we may need to modify the drawing area size. The second method is called whenever the display needs to be updated; we can call this manually or it will get called if another window covers it up to redraw the OpenGL scene.

class gui:
   """cad drawing application."""
   glwrap = gtkgl()

   def __init__(self):
       """ Initialise the GTK interface grab the GTK widgets, connect the events we are interested in receiving"""
       xml = Gtk.Builder()
       xml.add_from_file('interface.glade')
       self.window = xml.get_object('window')
       self.window.connect('delete_event', Gtk.main_quit)
       self.window.connect('destroy', lambda quit: Gtk.main_quit())
       self.window.set_reallocate_redraws(True)

       self.drawing_area = xml.get_object('drawingarea')
       self.drawing_area.set_double_buffered(False)
       self.drawing_area.set_size_request(self.glwrap.width, self.glwrap.height)
       self.drawing_area.connect('configure_event', self.on_configure_event)
       self.drawing_area.connect('draw', self.on_draw)
       self.drawing_area.connect('realize', self.on_draw)

       self.window.show_all()

   def on_configure_event(self, widget, event):
       self.glwrap.configure(widget.get_window())
       return True

   def on_draw(self, *args):
       self.glwrap.test()

   def quit(self, *args):
       Gtk.main_quit()