Commit 0cd29256 authored by sumpfralle's avatar sumpfralle

moved OpenGL code to OpenGLTools

moved more matrix functions to pycam.Geometry.Matrix
moved pycam.Gui.ode_objects to pycam.Physics.ode_physics


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@321 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent d2f12762
...@@ -28,7 +28,7 @@ class CylindricalCutter(BaseCutter): ...@@ -28,7 +28,7 @@ class CylindricalCutter(BaseCutter):
def get_shape(self, format="ODE"): def get_shape(self, format="ODE"):
if format == "ODE": if format == "ODE":
import ode import ode
import pycam.Gui.ode_objects import pycam.Physics.ode_physics
""" We don't handle the the "additional_distance" perfectly, since """ We don't handle the the "additional_distance" perfectly, since
the "right" shape would be a cylinder with a small flat cap that the "right" shape would be a cylinder with a small flat cap that
grows to the full expanded radius through a partial sphere. The grows to the full expanded radius through a partial sphere. The
...@@ -70,7 +70,7 @@ class CylindricalCutter(BaseCutter): ...@@ -70,7 +70,7 @@ class CylindricalCutter(BaseCutter):
rot_matrix_box = (cosinus, sinus, 0.0, -sinus, cosinus, 0.0, 0.0, 0.0, 1.0) rot_matrix_box = (cosinus, sinus, 0.0, -sinus, cosinus, 0.0, 0.0, 0.0, 1.0)
geom_connect_transform = ode.GeomTransform(geom.space) geom_connect_transform = ode.GeomTransform(geom.space)
geom_connect_transform.setBody(geom.getBody()) geom_connect_transform.setBody(geom.getBody())
geom_connect = pycam.Gui.ode_objects.get_parallelepiped_geom( geom_connect = pycam.Physics.ode_physics.get_parallelepiped_geom(
(Point(-hypotenuse / 2.0, radius, -diff_z / 2.0), Point(hypotenuse / 2.0, radius, diff_z / 2.0), (Point(-hypotenuse / 2.0, radius, -diff_z / 2.0), Point(hypotenuse / 2.0, radius, diff_z / 2.0),
Point(hypotenuse / 2.0, -radius, diff_z / 2.0), Point(-hypotenuse / 2.0, -radius, -diff_z / 2.0)), Point(hypotenuse / 2.0, -radius, diff_z / 2.0), Point(-hypotenuse / 2.0, -radius, -diff_z / 2.0)),
(Point(-hypotenuse / 2.0, radius, self.height - diff_z / 2.0), Point(hypotenuse / 2.0, radius, self.height + diff_z / 2.0), (Point(-hypotenuse / 2.0, radius, self.height - diff_z / 2.0), Point(hypotenuse / 2.0, radius, self.height + diff_z / 2.0),
......
...@@ -28,7 +28,7 @@ class SphericalCutter(BaseCutter): ...@@ -28,7 +28,7 @@ class SphericalCutter(BaseCutter):
def get_shape(self, format="ODE"): def get_shape(self, format="ODE"):
if format == "ODE": if format == "ODE":
import ode import ode
import pycam.Gui.ode_objects import pycam.Physics.ode_physics
additional_distance = self.get_required_distance() additional_distance = self.get_required_distance()
radius = self.radius + additional_distance radius = self.radius + additional_distance
center_height = 0.5 * self.height + radius - additional_distance center_height = 0.5 * self.height + radius - additional_distance
...@@ -61,7 +61,7 @@ class SphericalCutter(BaseCutter): ...@@ -61,7 +61,7 @@ class SphericalCutter(BaseCutter):
rot_matrix_box = (cosinus, sinus, 0.0, -sinus, cosinus, 0.0, 0.0, 0.0, 1.0) rot_matrix_box = (cosinus, sinus, 0.0, -sinus, cosinus, 0.0, 0.0, 0.0, 1.0)
geom_connect_transform = ode.GeomTransform(geom.space) geom_connect_transform = ode.GeomTransform(geom.space)
geom_connect_transform.setBody(geom.getBody()) geom_connect_transform.setBody(geom.getBody())
geom_connect = pycam.Gui.ode_objects.get_parallelepiped_geom( geom_connect = pycam.Physics.ode_physics.get_parallelepiped_geom(
(Point(-hypotenuse / 2.0, radius, -diff_z / 2.0), Point(hypotenuse / 2.0, radius, diff_z / 2.0), (Point(-hypotenuse / 2.0, radius, -diff_z / 2.0), Point(hypotenuse / 2.0, radius, diff_z / 2.0),
Point(hypotenuse / 2.0, -radius, diff_z / 2.0), Point(-hypotenuse / 2.0, -radius, -diff_z / 2.0)), Point(hypotenuse / 2.0, -radius, diff_z / 2.0), Point(-hypotenuse / 2.0, -radius, -diff_z / 2.0)),
(Point(-hypotenuse / 2.0, radius, self.height - diff_z / 2.0), Point(hypotenuse / 2.0, radius, self.height + diff_z / 2.0), (Point(-hypotenuse / 2.0, radius, self.height - diff_z / 2.0), Point(hypotenuse / 2.0, radius, self.height + diff_z / 2.0),
...@@ -77,7 +77,7 @@ class SphericalCutter(BaseCutter): ...@@ -77,7 +77,7 @@ class SphericalCutter(BaseCutter):
# rotate cylinder vector # rotate cylinder vector
cyl_original_vector = (0, 0, hypotenuse_3d) cyl_original_vector = (0, 0, hypotenuse_3d)
cyl_destination_vector = (diff_x, diff_y, diff_z) cyl_destination_vector = (diff_x, diff_y, diff_z)
geom_cyl.setRotation(Matrix.get_rotation_matrix(cyl_original_vector, cyl_destination_vector)) geom_cyl.setRotation(Matrix.get_rotation_matrix_from_to(cyl_original_vector, cyl_destination_vector))
# the rotation is around the center - thus we ignore negative diff values # the rotation is around the center - thus we ignore negative diff values
geom_cyl.setPosition((abs(diff_x / 2.0), abs(diff_y / 2.0), radius - additional_distance)) geom_cyl.setPosition((abs(diff_x / 2.0), abs(diff_y / 2.0), radius - additional_distance))
geom_cyl_transform.setGeom(geom_cyl) geom_cyl_transform.setGeom(geom_cyl)
......
...@@ -6,9 +6,27 @@ from pycam.Geometry.Point import Point ...@@ -6,9 +6,27 @@ from pycam.Geometry.Point import Point
import math import math
def get_dot_product(a, b): def get_dot_product(a, b):
""" calculate the dot product of two 3d vectors
@type a: tuple(float) | list(float)
@value a: the first vector to be multiplied
@type b: tuple(float) | list(float)
@value b: the second vector to be multiplied
@rtype: float
@return: the dot product is (a0*b0 + a1*b1 + a2*b2)
"""
return sum(map(lambda l1, l2: l1 * l2, a, b)) return sum(map(lambda l1, l2: l1 * l2, a, b))
def get_cross_product(a, b): def get_cross_product(a, b):
""" calculate the cross product of two 3d vectors
@type a: tuple(float) | list(float) | pycam.Geometry.Point
@value a: the first vector to be multiplied
@type b: tuple(float) | list(float) | pycam.Geometry.Point
@value b: the second vector to be multiplied
@rtype: tuple(float)
@return: the cross product is a 3d vector
"""
if isinstance(a, Point): if isinstance(a, Point):
a = (a.x, a.y, a.z) a = (a.x, a.y, a.z)
if isinstance(b, Point): if isinstance(b, Point):
...@@ -18,9 +36,36 @@ def get_cross_product(a, b): ...@@ -18,9 +36,36 @@ def get_cross_product(a, b):
a[0] * b[1] - a[1] * b[0]) a[0] * b[1] - a[1] * b[0])
def get_length(vector): def get_length(vector):
""" calculate the lengt of a 3d vector
@type vector: tuple(float) | list(float)
@value vector: the given 3d vector
@rtype: float
@return: the length of a vector is the square root of the dot product
of the vector with itself
"""
return math.sqrt(get_dot_product(vector, vector)) return math.sqrt(get_dot_product(vector, vector))
def get_rotation_matrix(v_orig, v_dest): def get_rotation_matrix_from_to(v_orig, v_dest):
""" calculate the rotation matrix used to transform one vector into another
The result is useful for modifying the rotation matrix of a 3d object.
See the "extend_shape" code in each of the cutter classes (for ODE).
The simplest example is the following with the original vector pointing
along the x axis, while the destination vectors goes along the y axis:
get_rotation_matrix((1, 0, 0), (0, 1, 0))
Basically this describes a rotation around the z axis by 90 degrees.
The resulting 3x3 matrix (tuple of 9 floats) can be multiplied with any
other vector to rotate it in the same way around the z axis.
@type v_orig: tuple(float) | list(float) | pycam.Geometry.Point
@value v_orig: the original 3d vector
@type v_dest: tuple(float) | list(float) | pycam.Geometry.Point
@value v_dest: the destination 3d vector
@rtype: tuple(float)
@return: the tuple of 9 floats represents a 3x3 matrix, that can be
multiplied with any vector to rotate it in the same way, as you would
rotate v_orig to the position of v_dest
"""
if isinstance(v_orig, Point): if isinstance(v_orig, Point):
v_orig = (v_orig.x, v_orig.y, v_orig.z) v_orig = (v_orig.x, v_orig.y, v_orig.z)
if isinstance(v_dest, Point): if isinstance(v_dest, Point):
...@@ -50,3 +95,41 @@ def get_rotation_matrix(v_orig, v_dest): ...@@ -50,3 +95,41 @@ def get_rotation_matrix(v_orig, v_dest):
t * rot_axis.y * rot_axis.z + s * rot_axis.x, t * rot_axis.y * rot_axis.z + s * rot_axis.x,
t * rot_axis.z * rot_axis.z + c) t * rot_axis.z * rot_axis.z + c)
def get_rotation_matrix_axis_angle(rot_axis, rot_angle):
""" calculate rotation matrix for a normalized "rot_axis" vector and an angle
see http://mathworld.wolfram.com/RotationMatrix.html
@type rot_axis: tuple(float)
@value rot_axis: the vector describes the rotation axis. Its length should
be 1.0 (normalized).
@type rot_angle: float
@value rot_angle: rotation angle (radiant)
@rtype: tuple(float)
@return: the roation
"""
sin = math.sin(rot_angle)
cos = math.cos(rot_angle)
return ((cos + rot_axis[0]*rot_axis[0]*(1-cos),
rot_axis[0]*rot_axis[1]*(1-cos) - rot_axis[2]*sin,
rot_axis[0]*rot_axis[2]*(1-cos) + rot_axis[1]*sin),
(rot_axis[1]*rot_axis[0]*(1-cos) + rot_axis[2]*sin,
cos + rot_axis[1]*rot_axis[1]*(1-cos),
rot_axis[1]*rot_axis[2]*(1-cos) - rot_axis[0]*sin),
(rot_axis[2]*rot_axis[0]*(1-cos) - rot_axis[1]*sin,
rot_axis[2]*rot_axis[1]*(1-cos) + rot_axis[0]*sin,
cos + rot_axis[2]*rot_axis[2]*(1-cos)))
def multiply_vector_matrix(v, m):
""" Multiply a 3d vector with a 3x3 matrix. The result is a 3d vector.
@type v: tuple(float) | list(float)
@value v: a 3d vector as tuple or list containing three floats
@type m: tuple(tuple(float)) | list(list(float))
@value m: a 3x3 list/tuple of floats
@rtype: tuple(float)
@return: a tuple of 3 floats as the matrix product
"""
return (v[0] * m[0][0] + v[1] * m[0][1] + v[2] * m[0][2],
v[0] * m[1][0] + v[1] * m[1][1] + v[2] * m[1][2],
v[0] * m[2][0] + v[1] * m[2][1] + v[2] * m[2][2])
from pycam.Geometry.Point import Point from pycam.Geometry.Point import Point
import pycam.Geometry.Matrix as Matrix
import OpenGL.GL as GL import OpenGL.GL as GL
import OpenGL.GLU as GLU import OpenGL.GLU as GLU
import OpenGL.GLUT as GLUT import OpenGL.GLUT as GLUT
import gtk
import pango
import math import math
import time
# buttons for rotating, moving and zooming the model view window
BUTTON_ROTATE = gtk.gdk.BUTTON1_MASK
BUTTON_MOVE = gtk.gdk.BUTTON2_MASK
BUTTON_ZOOM = gtk.gdk.BUTTON3_MASK
# the length of the distance vector does not matter - it will be normalized and multiplied later anyway # the length of the distance vector does not matter - it will be normalized and multiplied later anyway
VIEWS = { VIEWS = {
...@@ -15,26 +24,6 @@ VIEWS = { ...@@ -15,26 +24,6 @@ VIEWS = {
"back": {"distance": (0.0, 1.0, 0.0), "center": (0.0, 0.0, 0.0), "up": (0.0, 0.0, 1.0), "znear": 0.1, "zfar": 1000.0, "fovy": 30.0}, "back": {"distance": (0.0, 1.0, 0.0), "center": (0.0, 0.0, 0.0), "up": (0.0, 0.0, 1.0), "znear": 0.1, "zfar": 1000.0, "fovy": 30.0},
} }
def rotate(orig, rot_axis, sin, cos):
""" rotation of an original vector around a normalized "rot_axis" vector
see http://mathworld.wolfram.com/RotationMatrix.html
@type orig: tuple(float)
@value orig: the vector to be rotated
@type rot_axis: tuple(float)
@value rot_axis: the vector describes the rotation axis
@type sin: float
@value sin: sinus of the rotation angle
@type cos: float
@value cos: cosinus of the rotation angle
@rtype: tuple(float)
"""
rot_matrix = ((cos + rot_axis[0]*rot_axis[0]*(1-cos), rot_axis[0]*rot_axis[1]*(1-cos) - rot_axis[2]*sin, rot_axis[0]*rot_axis[2]*(1-cos) + rot_axis[1]*sin),
(rot_axis[1]*rot_axis[0]*(1-cos) + rot_axis[2]*sin, cos + rot_axis[1]*rot_axis[1]*(1-cos), rot_axis[1]*rot_axis[2]*(1-cos) - rot_axis[0]*sin),
(rot_axis[2]*rot_axis[0]*(1-cos) - rot_axis[1]*sin, rot_axis[2]*rot_axis[1]*(1-cos) + rot_axis[0]*sin, cos + rot_axis[2]*rot_axis[2]*(1-cos)))
return (orig[0]*rot_matrix[0][0] + orig[1]*rot_matrix[0][1] + orig[2]*rot_matrix[0][2],
orig[0]*rot_matrix[1][0] + orig[1]*rot_matrix[1][1] + orig[2]*rot_matrix[1][2],
orig[0]*rot_matrix[2][0] + orig[1]*rot_matrix[2][1] + orig[2]*rot_matrix[2][2])
class Camera: class Camera:
...@@ -125,19 +114,16 @@ class Camera: ...@@ -125,19 +114,16 @@ class Camera:
xdiff = -xdiff xdiff = -xdiff
rot_x_angle = rot_x_factor * math.pi * ydiff / height rot_x_angle = rot_x_factor * math.pi * ydiff / height
rot_y_angle = rot_y_factor * math.pi * xdiff / width rot_y_angle = rot_y_factor * math.pi * xdiff / width
# calculate sinus / cosinus
rot_x_sin = math.sin(rot_x_angle)
rot_x_cos = math.cos(rot_x_angle)
rot_y_sin = math.sin(rot_y_angle)
rot_y_cos = math.cos(rot_y_angle)
# rotate around the "up" vector with the y-axis rotation # rotate around the "up" vector with the y-axis rotation
original_distance = self.view["distance"] original_distance = self.view["distance"]
original_up = self.view["up"] original_up = self.view["up"]
new_distance = rotate(original_distance, factors_y, rot_y_sin, rot_y_cos) y_rot_matrix = Matrix.get_rotation_matrix_axis_angle(factors_y, rot_y_angle)
new_up = rotate(original_up, factors_y, rot_y_sin, rot_y_cos) new_distance = Matrix.multiply_vector_matrix(original_distance, y_rot_matrix)
new_up = Matrix.multiply_vector_matrix(original_up, y_rot_matrix)
# rotate around the cross vector with the x-axis rotation # rotate around the cross vector with the x-axis rotation
new_distance = rotate(new_distance, factors_x, rot_x_sin, rot_x_cos) x_rot_matrix = Matrix.get_rotation_matrix_axis_angle(factors_x, rot_x_angle)
new_up = rotate(new_up, factors_x, rot_x_sin, rot_x_cos) new_distance = Matrix.multiply_vector_matrix(new_distance, x_rot_matrix)
new_up = Matrix.multiply_vector_matrix(new_up, x_rot_matrix)
self.view["distance"] = new_distance self.view["distance"] = new_distance
self.view["up"] = new_up self.view["up"] = new_up
...@@ -177,23 +163,316 @@ class Camera: ...@@ -177,23 +163,316 @@ class Camera:
return (factors_x, factors_y) return (factors_x, factors_y)
class ModelViewWindowGL:
def __init__(self, gui, settings, notify_destroy=None, accel_group=None):
# assume, that initialization will fail
self.gui = gui
self.window = self.gui.get_object("view3dwindow")
if not accel_group is None:
self.window.add_accel_group(accel_group)
self.initialized = False
self.busy = False
self.settings = settings
self.is_visible = False
# check if the 3D view is available
try:
import gtk.gtkgl
self.enabled = True
except ImportError:
show_error_dialog(self.window, "Failed to initialize the interactive 3D model view."
+ "\nPlease install 'python-gtkglext1' to enable it.")
self.enabled = False
return
self.mouse = {"start_pos": None, "button": None, "timestamp": 0}
self.notify_destroy_func = notify_destroy
self.window.connect("delete-event", self.destroy)
self.window.set_default_size(560, 400)
self._position = self.gui.get_object("ProjectWindow").get_position()
self._position = (self._position[0] + 100, self._position[1] + 100)
self.container = self.gui.get_object("view3dbox")
self.gui.get_object("Reset View").connect("clicked", self.rotate_view, VIEWS["reset"])
self.gui.get_object("Left View").connect("clicked", self.rotate_view, VIEWS["left"])
self.gui.get_object("Right View").connect("clicked", self.rotate_view, VIEWS["right"])
self.gui.get_object("Front View").connect("clicked", self.rotate_view, VIEWS["front"])
self.gui.get_object("Back View").connect("clicked", self.rotate_view, VIEWS["back"])
self.gui.get_object("Top View").connect("clicked", self.rotate_view, VIEWS["top"])
self.gui.get_object("Bottom View").connect("clicked", self.rotate_view, VIEWS["bottom"])
# key binding
self.window.connect("key-press-event", self.key_handler)
# OpenGL stuff
glconfig = gtk.gdkgl.Config(mode=gtk.gdkgl.MODE_RGB|gtk.gdkgl.MODE_DEPTH|gtk.gdkgl.MODE_DOUBLE)
self.area = gtk.gtkgl.DrawingArea(glconfig)
# first run; might also be important when doing other fancy gtk/gdk stuff
self.area.connect_after('realize', self.paint)
# called when a part of the screen is uncovered
self.area.connect('expose-event', self.paint)
# resize window
self.area.connect('configure-event', self._resize_window)
# catch mouse events
self.area.set_events(gtk.gdk.MOUSE | gtk.gdk.BUTTON_PRESS_MASK)
self.area.connect("button-press-event", self.mouse_handler)
self.area.connect('motion-notify-event', self.mouse_handler)
self.area.show()
self.container.add(self.area)
self.camera = Camera(self.settings, lambda: (self.area.allocation.width, self.area.allocation.height))
# color the dimension value according to the axes
# for "y" axis: 100% green is too bright on light background - we reduce it a bit
for color, names in (
(pango.AttrForeground(65535, 0, 0, 0, 100), ("model_dim_x_label", "model_dim_x")),
(pango.AttrForeground(0, 50000, 0, 0, 100), ("model_dim_y_label", "model_dim_y")),
(pango.AttrForeground(0, 0, 65535, 0, 100), ("model_dim_z_label", "model_dim_z"))):
attributes = pango.AttrList()
attributes.insert(color)
for name in names:
self.gui.get_object(name).set_attributes(attributes)
# show the window
self.container.show()
self.show()
def show(self):
self.is_visible = True
self.window.move(*self._position)
self.window.show()
def hide(self):
self.is_visible = False
self._position = self.window.get_position()
self.window.hide()
def key_handler(self, widget=None, event=None):
if event is None:
return
try:
keyval = getattr(event, "keyval")
get_state = getattr(event, "get_state")
except AttributeError:
return
if not (0 <= keyval <= 255):
# e.g. "shift" key
return
if chr(keyval) in ('l', 'm', 's'):
if (chr(keyval) == 'l'):
key = "view_light"
elif (chr(keyval) == 'm'):
key = "view_polygon"
elif (chr(keyval) == 's'):
key = "view_shadow"
else:
key = None
# toggle setting
self.settings.set(key, not self.settings.get(key))
# re-init gl settings
self.glsetup()
self.paint()
else:
#print "Key pressed: %s (%s)" % (chr(keyval), get_state())
pass
def check_busy(func):
def check_busy_wrapper(self, *args, **kwargs):
if not self.enabled or self.busy:
return
self.busy = True
result = func(self, *args, **kwargs)
self.busy = False
return result
return check_busy_wrapper
def gtkgl_refresh(func):
def gtkgl_refresh_wrapper(self, *args, **kwargs):
prev_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE)
GL.glMatrixMode(GL.GL_MODELVIEW)
# clear the background with the configured color
bg_col = self.settings.get("color_background")
GL.glClearColor(bg_col[0], bg_col[1], bg_col[2], 0.0)
GL.glClear(GL.GL_COLOR_BUFFER_BIT|GL.GL_DEPTH_BUFFER_BIT)
result = func(self, *args, **kwargs)
self.camera.position_camera()
self._paint_raw()
GL.glMatrixMode(prev_mode)
GL.glFlush()
self.area.get_gl_drawable().swap_buffers()
return result
return gtkgl_refresh_wrapper
def glsetup(self):
GLUT.glutInit()
if self.settings.get("view_shadow"):
GL.glShadeModel(GL.GL_FLAT)
else:
GL.glShadeModel(GL.GL_SMOOTH)
bg_col = self.settings.get("color_background")
GL.glClearColor(bg_col[0], bg_col[1], bg_col[2], 0.0)
GL.glClearDepth(1.)
GL.glEnable(GL.GL_DEPTH_TEST)
GL.glDepthFunc(GL.GL_LEQUAL)
GL.glDepthMask(GL.GL_TRUE)
GL.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST)
GL.glMatrixMode(GL.GL_MODELVIEW)
#GL.glMaterial(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT, (0.1, 0.1, 0.1, 1.0))
GL.glMaterial(GL.GL_FRONT_AND_BACK, GL.GL_SPECULAR, (0.1, 0.1, 0.1, 1.0))
#GL.glMaterial(GL.GL_FRONT_AND_BACK, GL.GL_SHININESS, (0.5))
if self.settings.get("view_polygon"):
GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL)
else:
GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE)
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glLoadIdentity()
GL.glMatrixMode(GL.GL_PROJECTION)
GL.glLoadIdentity()
GL.glViewport(0, 0, self.area.allocation.width, self.area.allocation.height)
# lightning
GL.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, (0.3, 0.3, 0.3, 3.)) # Setup The Ambient Light
GL.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, (1., 1., 1., .0)) # Setup The Diffuse Light
GL.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, (.3, .3, .3, 1.0)) # Setup The SpecularLight
GL.glEnable(GL.GL_LIGHT0)
# Enable Light One
if self.settings.get("view_light"):
GL.glEnable(GL.GL_LIGHTING)
else:
GL.glDisable(GL.GL_LIGHTING)
GL.glEnable(GL.GL_NORMALIZE)
GL.glColorMaterial(GL.GL_FRONT_AND_BACK,GL.GL_AMBIENT_AND_DIFFUSE)
#GL.glColorMaterial(GL.GL_FRONT_AND_BACK,GL.GL_SPECULAR)
#GL.glColorMaterial(GL.GL_FRONT_AND_BACK,GL.GL_EMISSION)
GL.glEnable(GL.GL_COLOR_MATERIAL)
def destroy(self, widget=None, data=None):
if self.notify_destroy_func:
self.notify_destroy_func()
# don't close the window
return True
def gtkgl_functionwrapper(function):
def gtkgl_functionwrapper_function(self, *args, **kwords):
gldrawable=self.area.get_gl_drawable()
if not gldrawable:
return
glcontext=self.area.get_gl_context()
if not gldrawable.gl_begin(glcontext):
return
if not self.initialized:
self.glsetup()
self.initialized = True
result = function(self, *args, **kwords)
gldrawable.gl_end()
return result
return gtkgl_functionwrapper_function
@check_busy
@gtkgl_functionwrapper
def mouse_handler(self, widget, event):
last_timestamp = self.mouse["timestamp"]
x, y, state = event.x, event.y, event.state
if self.mouse["button"] is None:
if (state == BUTTON_ZOOM) or (state == BUTTON_ROTATE) or (state == BUTTON_MOVE):
self.mouse["button"] = state
self.mouse["start_pos"] = [x, y]
self.area.set_events(gtk.gdk.MOUSE | gtk.gdk.BUTTON_PRESS_MASK)
else:
# not more than 25 frames per second (enough for decent visualization)
if time.time() - last_timestamp < 0.04:
return
# a button was pressed before
if state == self.mouse["button"] == BUTTON_ZOOM:
# the start button is still active: update the view
start_x, start_y = self.mouse["start_pos"]
self.mouse["start_pos"] = [x, y]
# move the mouse from lower left to top right corner for scale up
scale = 1 - 0.01 * ((x - start_x) + (start_y - y))
# do some sanity checks, scale no more than
# 1:100 on any given click+drag
if scale < 0.01:
scale = 0.01
elif scale > 100:
scale = 100
self.camera.scale_distance(scale)
self._paint_ignore_busy()
elif (state == self.mouse["button"] == BUTTON_MOVE) or (state == self.mouse["button"] == BUTTON_ROTATE):
start_x, start_y = self.mouse["start_pos"]
self.mouse["start_pos"] = [x, y]
if (state == BUTTON_MOVE):
# determine the biggest dimension (x/y/z) for moving the screen's center in relation to this value
obj_dim = []
obj_dim.append(self.settings.get("maxx") - self.settings.get("minx"))
obj_dim.append(self.settings.get("maxy") - self.settings.get("miny"))
obj_dim.append(self.settings.get("maxz") - self.settings.get("minz"))
max_dim = max(max(obj_dim[0], obj_dim[1]), obj_dim[2])
self.camera.move_camera_by_screen(x - start_x, y - start_y, max_dim)
else:
# BUTTON_ROTATE
# update the camera position according to the mouse movement
self.camera.rotate_camera_by_screen(start_x, start_y, x, y)
self._paint_ignore_busy()
else:
# button was released
self.mouse["button"] = None
self._paint_ignore_busy()
self.mouse["timestamp"] = time.time()
@check_busy
@gtkgl_functionwrapper
@gtkgl_refresh
def rotate_view(self, widget=None, view=None):
self.camera.set_view(view)
def reset_view(self):
self.rotate_view(None, None)
@check_busy
@gtkgl_functionwrapper
@gtkgl_refresh
def _resize_window(self, widget, data=None):
GL.glViewport(0, 0, self.area.allocation.width, self.area.allocation.height)
@check_busy
@gtkgl_functionwrapper
@gtkgl_refresh
def paint(self, widget=None, data=None):
# the decorators take core for redraw
pass
@gtkgl_functionwrapper
@gtkgl_refresh
def _paint_ignore_busy(self, widget=None):
pass
def _paint_raw(self, widget=None):
# draw the model
draw_complete_model_view(self.settings)
# update the dimension display
s = self.settings
dimension_bar = self.gui.get_object("view3ddimension")
if s.get("show_dimensions"):
for name, size in (
("model_dim_x", s.get("maxx") - s.get("minx")),
("model_dim_y", s.get("maxy") - s.get("miny")),
("model_dim_z", s.get("maxz") - s.get("minz"))):
self.gui.get_object(name).set_text("%.3f %s" % (size, s.get("unit")))
dimension_bar.show()
else:
dimension_bar.hide()
def keep_gl_mode(func): def keep_gl_mode(func):
def wrapper(*args, **kwargs): def keep_gl_mode_wrapper(*args, **kwargs):
prev_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE) prev_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE)
func(*args, **kwargs) result = func(*args, **kwargs)
GL.glMatrixMode(prev_mode) GL.glMatrixMode(prev_mode)
return wrapper return result
return keep_gl_mode_wrapper
def keep_matrix(func): def keep_matrix(func):
def wrapper(*args, **kwargs): def keep_matrix_wrapper(*args, **kwargs):
pushed_matrix_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE) pushed_matrix_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE)
GL.glPushMatrix() GL.glPushMatrix()
func(*args, **kwargs) result = func(*args, **kwargs)
final_matrix_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE) final_matrix_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE)
GL.glMatrixMode(pushed_matrix_mode) GL.glMatrixMode(pushed_matrix_mode)
GL.glPopMatrix() GL.glPopMatrix()
GL.glMatrixMode(final_matrix_mode) GL.glMatrixMode(final_matrix_mode)
return wrapper return result
return keep_matrix_wrapper
@keep_matrix @keep_matrix
def draw_string(x, y, z, p, s, scale=.01): def draw_string(x, y, z, p, s, scale=.01):
......
...@@ -13,14 +13,8 @@ import pycam.Geometry.utils as utils ...@@ -13,14 +13,8 @@ import pycam.Geometry.utils as utils
# this requires ODE - we import it later, if necessary # this requires ODE - we import it later, if necessary
#import pycam.Simulation.ODEBlocks #import pycam.Simulation.ODEBlocks
import pycam.Gui.OpenGLTools as ogl_tools import pycam.Gui.OpenGLTools as ogl_tools
import pycam.Gui.ode_objects as ode_objects import pycam.Physics.ode_physics
import OpenGL.GL as GL
import OpenGL.GLU as GLU
import OpenGL.GLUT as GLUT
# gtk.gtkgl is imported in the constructor of "GLView" below
#import gtk.gtkgl
import gtk import gtk
import pango
import ConfigParser import ConfigParser
import math import math
import time import time
...@@ -41,10 +35,6 @@ FILTER_MODEL = ("STL models", "*.stl") ...@@ -41,10 +35,6 @@ FILTER_MODEL = ("STL models", "*.stl")
FILTER_CONFIG = ("Config files", "*.conf") FILTER_CONFIG = ("Config files", "*.conf")
FILTER_EMC_TOOL = ("EMC tool files", "*.tbl") FILTER_EMC_TOOL = ("EMC tool files", "*.tbl")
BUTTON_ROTATE = gtk.gdk.BUTTON1_MASK
BUTTON_MOVE = gtk.gdk.BUTTON2_MASK
BUTTON_ZOOM = gtk.gdk.BUTTON3_MASK
COLORS = { COLORS = {
"color_background": (0.0, 0.0, 0.0), "color_background": (0.0, 0.0, 0.0),
"color_model": (0.5, 0.5, 1.0), "color_model": (0.5, 0.5, 1.0),
...@@ -65,13 +55,13 @@ PREFERENCES_DEFAULTS = { ...@@ -65,13 +55,13 @@ PREFERENCES_DEFAULTS = {
"show_bounding_box": True, "show_bounding_box": True,
"show_toolpath": True, "show_toolpath": True,
"show_drill_progress": False, "show_drill_progress": False,
"color_background": COLORS["color_background"], "color_background": (0.0, 0.0, 0.0),
"color_model": COLORS["color_model"], "color_model": (0.5, 0.5, 1.0),
"color_bounding_box": COLORS["color_bounding_box"], "color_bounding_box": (0.3, 0.3, 0.3),
"color_cutter": COLORS["color_cutter"], "color_cutter": (1.0, 0.2, 0.2),
"color_toolpath_cut": COLORS["color_toolpath_cut"], "color_toolpath_cut": (1.0, 0.5, 0.5),
"color_toolpath_return": COLORS["color_toolpath_return"], "color_toolpath_return": (0.5, 1.0, 0.5),
"color_material": COLORS["color_material"], "color_material": (1.0, 0.5, 0.0),
"view_light": True, "view_light": True,
"view_shadow": True, "view_shadow": True,
"view_polygon": True, "view_polygon": True,
...@@ -102,299 +92,6 @@ def show_error_dialog(window, message): ...@@ -102,299 +92,6 @@ def show_error_dialog(window, message):
warn_window.destroy() warn_window.destroy()
class GLView:
def __init__(self, gui, settings, notify_destroy=None, accel_group=None):
# assume, that initialization will fail
self.gui = gui
self.window = self.gui.get_object("view3dwindow")
if not accel_group is None:
self.window.add_accel_group(accel_group)
self.initialized = False
self.busy = False
self.settings = settings
self.is_visible = False
# check if the 3D view is available
try:
import gtk.gtkgl
self.enabled = True
except ImportError:
show_error_dialog(self.window, "Failed to initialize the interactive 3D model view."
+ "\nPlease install 'python-gtkglext1' to enable it.")
self.enabled = False
return
self.mouse = {"start_pos": None, "button": None, "timestamp": 0}
self.notify_destroy_func = notify_destroy
self.window.connect("delete-event", self.destroy)
self.window.set_default_size(560, 400)
self._position = self.gui.get_object("ProjectWindow").get_position()
self._position = (self._position[0] + 100, self._position[1] + 100)
self.container = self.gui.get_object("view3dbox")
self.gui.get_object("Reset View").connect("clicked", self.rotate_view, ogl_tools.VIEWS["reset"])
self.gui.get_object("Left View").connect("clicked", self.rotate_view, ogl_tools.VIEWS["left"])
self.gui.get_object("Right View").connect("clicked", self.rotate_view, ogl_tools.VIEWS["right"])
self.gui.get_object("Front View").connect("clicked", self.rotate_view, ogl_tools.VIEWS["front"])
self.gui.get_object("Back View").connect("clicked", self.rotate_view, ogl_tools.VIEWS["back"])
self.gui.get_object("Top View").connect("clicked", self.rotate_view, ogl_tools.VIEWS["top"])
self.gui.get_object("Bottom View").connect("clicked", self.rotate_view, ogl_tools.VIEWS["bottom"])
# key binding
self.window.connect("key-press-event", self.key_handler)
# OpenGL stuff
glconfig = gtk.gdkgl.Config(mode=gtk.gdkgl.MODE_RGB|gtk.gdkgl.MODE_DEPTH|gtk.gdkgl.MODE_DOUBLE)
self.area = gtk.gtkgl.DrawingArea(glconfig)
# first run; might also be important when doing other fancy gtk/gdk stuff
self.area.connect_after('realize', self.paint)
# called when a part of the screen is uncovered
self.area.connect('expose-event', self.paint)
# resize window
self.area.connect('configure-event', self._resize_window)
# catch mouse events
self.area.set_events(gtk.gdk.MOUSE | gtk.gdk.BUTTON_PRESS_MASK)
self.area.connect("button-press-event", self.mouse_handler)
self.area.connect('motion-notify-event', self.mouse_handler)
self.area.show()
self.container.add(self.area)
self.camera = ogl_tools.Camera(self.settings, lambda: (self.area.allocation.width, self.area.allocation.height))
# color the dimension value according to the axes
# for "y" axis: 100% green is too bright on light background - we reduce it a bit
for color, names in (
(pango.AttrForeground(65535, 0, 0, 0, 100), ("model_dim_x_label", "model_dim_x")),
(pango.AttrForeground(0, 50000, 0, 0, 100), ("model_dim_y_label", "model_dim_y")),
(pango.AttrForeground(0, 0, 65535, 0, 100), ("model_dim_z_label", "model_dim_z"))):
attributes = pango.AttrList()
attributes.insert(color)
for name in names:
self.gui.get_object(name).set_attributes(attributes)
# show the window
self.container.show()
self.show()
def show(self):
self.is_visible = True
self.window.move(*self._position)
self.window.show()
def hide(self):
self.is_visible = False
self._position = self.window.get_position()
self.window.hide()
def key_handler(self, widget=None, event=None):
if event is None:
return
try:
keyval = getattr(event, "keyval")
get_state = getattr(event, "get_state")
except AttributeError:
return
if not (0 <= keyval <= 255):
# e.g. "shift" key
return
if chr(keyval) in ('l', 'm', 's'):
if (chr(keyval) == 'l'):
key = "view_light"
elif (chr(keyval) == 'm'):
key = "view_polygon"
elif (chr(keyval) == 's'):
key = "view_shadow"
else:
key = None
# toggle setting
self.settings.set(key, not self.settings.get(key))
# re-init gl settings
self.glsetup()
self.paint()
else:
#print "Key pressed: %s (%s)" % (chr(keyval), get_state())
pass
def check_busy(func):
def busy_wrapper(self, *args, **kwargs):
if not self.enabled or self.busy:
return
self.busy = True
func(self, *args, **kwargs)
self.busy = False
return busy_wrapper
def gtkgl_refresh(func):
def refresh_wrapper(self, *args, **kwargs):
prev_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE)
GL.glMatrixMode(GL.GL_MODELVIEW)
# clear the background with the configured color
bg_col = self.settings.get("color_background")
GL.glClearColor(bg_col[0], bg_col[1], bg_col[2], 0.0)
GL.glClear(GL.GL_COLOR_BUFFER_BIT|GL.GL_DEPTH_BUFFER_BIT)
result = func(self, *args, **kwargs)
self.camera.position_camera()
self._paint_raw()
GL.glMatrixMode(prev_mode)
GL.glFlush()
self.area.get_gl_drawable().swap_buffers()
return result
return refresh_wrapper
def glsetup(self):
GLUT.glutInit()
if self.settings.get("view_shadow"):
GL.glShadeModel(GL.GL_FLAT)
else:
GL.glShadeModel(GL.GL_SMOOTH)
bg_col = self.settings.get("color_background")
GL.glClearColor(bg_col[0], bg_col[1], bg_col[2], 0.0)
GL.glClearDepth(1.)
GL.glEnable(GL.GL_DEPTH_TEST)
GL.glDepthFunc(GL.GL_LEQUAL)
GL.glDepthMask(GL.GL_TRUE)
GL.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST)
GL.glMatrixMode(GL.GL_MODELVIEW)
#GL.glMaterial(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT, (0.1, 0.1, 0.1, 1.0))
GL.glMaterial(GL.GL_FRONT_AND_BACK, GL.GL_SPECULAR, (0.1, 0.1, 0.1, 1.0))
#GL.glMaterial(GL.GL_FRONT_AND_BACK, GL.GL_SHININESS, (0.5))
if self.settings.get("view_polygon"):
GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL)
else:
GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE)
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glLoadIdentity()
GL.glMatrixMode(GL.GL_PROJECTION)
GL.glLoadIdentity()
GL.glViewport(0, 0, self.area.allocation.width, self.area.allocation.height)
# lightning
GL.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, (0.3, 0.3, 0.3, 3.)) # Setup The Ambient Light
GL.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, (1., 1., 1., .0)) # Setup The Diffuse Light
GL.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, (.3, .3, .3, 1.0)) # Setup The SpecularLight
GL.glEnable(GL.GL_LIGHT0)
# Enable Light One
if self.settings.get("view_light"):
GL.glEnable(GL.GL_LIGHTING)
else:
GL.glDisable(GL.GL_LIGHTING)
GL.glEnable(GL.GL_NORMALIZE)
GL.glColorMaterial(GL.GL_FRONT_AND_BACK,GL.GL_AMBIENT_AND_DIFFUSE)
#GL.glColorMaterial(GL.GL_FRONT_AND_BACK,GL.GL_SPECULAR)
#GL.glColorMaterial(GL.GL_FRONT_AND_BACK,GL.GL_EMISSION)
GL.glEnable(GL.GL_COLOR_MATERIAL)
def destroy(self, widget=None, data=None):
if self.notify_destroy_func:
self.notify_destroy_func()
# don't close the window
return True
def gtkgl_functionwrapper(function):
def decorated(self, *args, **kwords):
gldrawable=self.area.get_gl_drawable()
if not gldrawable:
return
glcontext=self.area.get_gl_context()
if not gldrawable.gl_begin(glcontext):
return
if not self.initialized:
self.glsetup()
self.initialized = True
result = function(self, *args, **kwords)
gldrawable.gl_end()
return result
return decorated # TODO: make this a well behaved decorator (keeping name, docstring etc)
def keyboard_handler(self, widget, event):
print "KEY:", event
@check_busy
@gtkgl_functionwrapper
def mouse_handler(self, widget, event):
last_timestamp = self.mouse["timestamp"]
x, y, state = event.x, event.y, event.state
if self.mouse["button"] is None:
if (state == BUTTON_ZOOM) or (state == BUTTON_ROTATE) or (state == BUTTON_MOVE):
self.mouse["button"] = state
self.mouse["start_pos"] = [x, y]
self.area.set_events(gtk.gdk.MOUSE | gtk.gdk.BUTTON_PRESS_MASK)
else:
# not more than 25 frames per second (enough for decent visualization)
if time.time() - last_timestamp < 0.04:
return
# a button was pressed before
if state == self.mouse["button"] == BUTTON_ZOOM:
# the start button is still active: update the view
start_x, start_y = self.mouse["start_pos"]
self.mouse["start_pos"] = [x, y]
# move the mouse from lower left to top right corner for scale up
scale = 1 - 0.01 * ((x - start_x) + (start_y - y))
# do some sanity checks, scale no more than
# 1:100 on any given click+drag
if scale < 0.01:
scale = 0.01
elif scale > 100:
scale = 100
self.camera.scale_distance(scale)
self._paint_ignore_busy()
elif (state == self.mouse["button"] == BUTTON_MOVE) or (state == self.mouse["button"] == BUTTON_ROTATE):
start_x, start_y = self.mouse["start_pos"]
self.mouse["start_pos"] = [x, y]
if (state == BUTTON_MOVE):
# determine the biggest dimension (x/y/z) for moving the screen's center in relation to this value
obj_dim = []
obj_dim.append(self.settings.get("maxx") - self.settings.get("minx"))
obj_dim.append(self.settings.get("maxy") - self.settings.get("miny"))
obj_dim.append(self.settings.get("maxz") - self.settings.get("minz"))
max_dim = max(max(obj_dim[0], obj_dim[1]), obj_dim[2])
self.camera.move_camera_by_screen(x - start_x, y - start_y, max_dim)
else:
# BUTTON_ROTATE
# update the camera position according to the mouse movement
self.camera.rotate_camera_by_screen(start_x, start_y, x, y)
self._paint_ignore_busy()
else:
# button was released
self.mouse["button"] = None
self._paint_ignore_busy()
self.mouse["timestamp"] = time.time()
@check_busy
@gtkgl_functionwrapper
@gtkgl_refresh
def rotate_view(self, widget=None, view=None):
self.camera.set_view(view)
def reset_view(self):
self.rotate_view(None, None)
@check_busy
@gtkgl_functionwrapper
@gtkgl_refresh
def _resize_window(self, widget, data=None):
GL.glViewport(0, 0, self.area.allocation.width, self.area.allocation.height)
@check_busy
@gtkgl_functionwrapper
@gtkgl_refresh
def paint(self, widget=None, data=None):
# the decorators take core for redraw
pass
@gtkgl_functionwrapper
@gtkgl_refresh
def _paint_ignore_busy(self, widget=None):
pass
def _paint_raw(self, widget=None):
# draw the model
ogl_tools.draw_complete_model_view(self.settings)
# update the dimension display
s = self.settings
dimension_bar = self.gui.get_object("view3ddimension")
if s.get("show_dimensions"):
for name, size in (
("model_dim_x", s.get("maxx") - s.get("minx")),
("model_dim_y", s.get("maxy") - s.get("miny")),
("model_dim_z", s.get("maxz") - s.get("minz"))):
self.gui.get_object(name).set_text("%.3f %s" % (size, s.get("unit")))
dimension_bar.show()
else:
dimension_bar.hide()
class ProjectGui: class ProjectGui:
BOUNDARY_MODES = { BOUNDARY_MODES = {
...@@ -551,7 +248,7 @@ class ProjectGui: ...@@ -551,7 +248,7 @@ class ProjectGui:
obj.connect("color-set", self.update_view) obj.connect("color-set", self.update_view)
# set the availability of ODE # set the availability of ODE
enable_ode_control = self.gui.get_object("SettingEnableODE") enable_ode_control = self.gui.get_object("SettingEnableODE")
if ode_objects.is_ode_available(): if pycam.Physics.ode_physics.is_ode_available():
self.settings.add_item("enable_ode", enable_ode_control.get_active, enable_ode_control.set_active) self.settings.add_item("enable_ode", enable_ode_control.get_active, enable_ode_control.set_active)
else: else:
enable_ode_control.set_sensitive(False) enable_ode_control.set_sensitive(False)
...@@ -716,7 +413,7 @@ class ProjectGui: ...@@ -716,7 +413,7 @@ class ProjectGui:
def get_physics(self, cutter): def get_physics(self, cutter):
if self.settings.get("enable_ode"): if self.settings.get("enable_ode"):
self._physics_cache = ode_objects.generate_physics(self.model, self._physics_cache = pycam.Physics.ode_physics.generate_physics(self.model,
cutter, self._physics_cache) cutter, self._physics_cache)
else: else:
self._physics_cache = None self._physics_cache = None
...@@ -963,7 +660,7 @@ class ProjectGui: ...@@ -963,7 +660,7 @@ class ProjectGui:
elif new_state: elif new_state:
if self.view3d is None: if self.view3d is None:
# do the gl initialization # do the gl initialization
self.view3d = GLView(self.gui, self.settings, self.view3d = ogl_tools.ModelViewWindowGL(self.gui, self.settings,
notify_destroy=self.toggle_3d_view, notify_destroy=self.toggle_3d_view,
accel_group=self._accel_group) accel_group=self._accel_group)
if self.model and self.view3d.enabled: if self.model and self.view3d.enabled:
...@@ -1635,7 +1332,7 @@ class ProjectGui: ...@@ -1635,7 +1332,7 @@ class ProjectGui:
self.gui.get_object("toolpath_up").set_sensitive((not new_index is None) and (new_index > 0)) self.gui.get_object("toolpath_up").set_sensitive((not new_index is None) and (new_index > 0))
self.gui.get_object("toolpath_delete").set_sensitive(not new_index is None) self.gui.get_object("toolpath_delete").set_sensitive(not new_index is None)
self.gui.get_object("toolpath_down").set_sensitive((not new_index is None) and (new_index + 1 < len(self.toolpath))) self.gui.get_object("toolpath_down").set_sensitive((not new_index is None) and (new_index + 1 < len(self.toolpath)))
self.gui.get_object("toolpath_simulate").set_sensitive((not new_index is None) and ode_objects.is_ode_available()) self.gui.get_object("toolpath_simulate").set_sensitive((not new_index is None) and pycam.Physics.ode_physics.is_ode_available())
@gui_activity_guard @gui_activity_guard
def save_task_settings_file(self, widget=None, filename=None): def save_task_settings_file(self, widget=None, filename=None):
......
# Tkinter is used for "EmergencyDialog" below - but we will try to import it carefully # Tkinter is used for "EmergencyDialog" below - but we will try to import it carefully
#import Tkinter #import Tkinter
# "ode" is imported later, if required
#import ode_objects
import random import random
import sys import sys
import os import os
......
#!/usr/bin/python #!/usr/bin/python
from pycam.Gui.ode_objects import override_ode_availability from pycam.Physics.ode_physics import override_ode_availability
import pycam.Gui.common as GuiCommon import pycam.Gui.common as GuiCommon
from optparse import OptionParser from optparse import OptionParser
import sys import sys
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment