407 lines
14 KiB
Python
407 lines
14 KiB
Python
|
|
import math
|
|
import gtk
|
|
import gtk.gtkgl
|
|
|
|
from OpenGL.GL import *
|
|
|
|
from shapes import *
|
|
from constraints import *
|
|
|
|
class SketchWidget:
|
|
def __init__(self, sketch, window):
|
|
self.sketch = sketch
|
|
self.window = window
|
|
self.view_center = (0, 0)
|
|
self.view_width = 4.0
|
|
self.size = (1, 1)
|
|
self.panning = False
|
|
self.drawingShape = None
|
|
self.drawingConstraints = []
|
|
self.mode = ''
|
|
|
|
# Configuration parameters
|
|
self.line_width = 1.5
|
|
self.axis_width = 2.0
|
|
self.axis_length = 50
|
|
self.zoom_factor = 1.2
|
|
self.background_color = (0.0, 0.05, 0.1, 1.0)
|
|
self.line_color = (0.1, 0.6, 1.0, 1.0)
|
|
self.axis_color = (1.0, 0.0, 0.0, 1.0)
|
|
self.constraint_color = (0.8, 1.0, 0.0, 1.0)
|
|
self.snap_angle = 10
|
|
self.snap_dist = 10
|
|
|
|
try:
|
|
# try double-buffered
|
|
self.glconfig = gtk.gdkgl.Config(
|
|
mode = (gtk.gdkgl.MODE_RGB |
|
|
gtk.gdkgl.MODE_DOUBLE |
|
|
gtk.gdkgl.MODE_DEPTH))
|
|
except gtk.gdkgl.NoMatches:
|
|
# try single-buffered
|
|
self.glconfig = gtk.gdkgl.Config(
|
|
mode = (gtk.gdkgl.MODE_RGB |
|
|
gtk.gdkgl.MODE_DEPTH))
|
|
|
|
self.widget = gtk.gtkgl.DrawingArea(self.glconfig)
|
|
self.widget.set_flags(gtk.CAN_FOCUS)
|
|
|
|
self.widget.connect_after('realize', self.init)
|
|
self.widget.connect('configure_event', self.reshape)
|
|
self.widget.connect('expose_event', self.draw)
|
|
self.widget.connect('button-press-event', self.button_press_event)
|
|
self.widget.connect('button-release-event', self.button_release_event)
|
|
self.widget.connect('motion-notify-event', self.motion_event)
|
|
self.widget.connect('scroll-event', self.scroll_event)
|
|
|
|
self.widget.add_events(
|
|
gtk.gdk.POINTER_MOTION_MASK
|
|
| gtk.gdk.BUTTON_PRESS_MASK
|
|
| gtk.gdk.BUTTON_RELEASE_MASK
|
|
| gtk.gdk.SCROLL_MASK)
|
|
|
|
def init(self, glarea):
|
|
# get GLContext and GLDrawable
|
|
glcontext = glarea.get_gl_context()
|
|
gldrawable = glarea.get_gl_drawable()
|
|
|
|
# GL calls
|
|
if not gldrawable.gl_begin(glcontext): return
|
|
|
|
# glEnable(GL_BLEND)
|
|
# glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
# glEnable(GL_POLYGON_SMOOTH)
|
|
# glEnable(GL_LINE_SMOOTH)
|
|
|
|
glClearColor(*self.background_color)
|
|
|
|
gldrawable.gl_end()
|
|
|
|
def reshape(self, glarea, event):
|
|
# get GLContext and GLDrawable
|
|
glcontext = glarea.get_gl_context()
|
|
gldrawable = glarea.get_gl_drawable()
|
|
|
|
# GL calls
|
|
if not gldrawable.gl_begin(glcontext): return
|
|
|
|
x, y, width, height = glarea.get_allocation()
|
|
self.size = (float(width), float(height))
|
|
|
|
glViewport(0, 0, width, height)
|
|
glMatrixMode(GL_PROJECTION)
|
|
glLoadIdentity()
|
|
glOrtho(0, width - 1, 0, height - 1, 1.0, -1.0)
|
|
|
|
glMatrixMode(GL_MODELVIEW)
|
|
glLoadIdentity()
|
|
|
|
gldrawable.gl_end()
|
|
|
|
return True
|
|
|
|
def draw(self, glarea, event):
|
|
# get GLContext and GLDrawable
|
|
glcontext = glarea.get_gl_context()
|
|
gldrawable = glarea.get_gl_drawable()
|
|
|
|
# GL calls
|
|
if not gldrawable.gl_begin(glcontext): return
|
|
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
|
|
|
self.drawAxes()
|
|
|
|
self.drawShapes()
|
|
self.drawConstraints()
|
|
|
|
if self.drawingShape is not None:
|
|
self.drawShape(self.drawingShape)
|
|
for c in self.drawingConstraints:
|
|
self.drawConstraint(c)
|
|
|
|
if gldrawable.is_double_buffered():
|
|
gldrawable.swap_buffers()
|
|
else:
|
|
glFlush()
|
|
|
|
gldrawable.gl_end()
|
|
|
|
return True
|
|
|
|
def ptToScreenPt(self, pt):
|
|
return ((pt[0] - self.view_center[0]) / self.view_width * self.size[0]
|
|
+ self.size[0] / 2,
|
|
(pt[1] - self.view_center[1]) / self.view_width * self.size[0]
|
|
+ self.size[1] / 2)
|
|
|
|
def distToScreenDist(self, dist):
|
|
return dist / self.view_width * self.size[0]
|
|
|
|
def screenPtToPt(self, pt):
|
|
return ((pt[0] - self.size[0] / 2) / self.size[0] * self.view_width
|
|
+ self.view_center[0],
|
|
(pt[1] - self.size[1] / 2) / self.size[0] * self.view_width
|
|
+ self.view_center[1])
|
|
|
|
def screenDistToDist(self, dist):
|
|
return dist / self.size[0] * self.view_width
|
|
|
|
def drawLine(self, shape):
|
|
glColor(*self.line_color)
|
|
pt0 = self.ptToScreenPt(shape.getPt(0))
|
|
pt1 = self.ptToScreenPt(shape.getPt(1))
|
|
self.drawFilledLine(pt0[0], pt0[1], pt1[0], pt1[1], self.line_width)
|
|
|
|
def drawCircle(self, shape):
|
|
glColor(*self.line_color)
|
|
center = self.ptToScreenPt(shape.getCenter())
|
|
rad = self.distToScreenDist(shape.getRadius())
|
|
if rad < 30:
|
|
steps = 12
|
|
elif rad < 50:
|
|
steps = 16
|
|
elif rad < 100:
|
|
steps = 24
|
|
elif rad < 200:
|
|
steps = 36
|
|
elif rad < 400:
|
|
steps = 48
|
|
else:
|
|
steps = 64
|
|
step = 2 * math.pi / steps
|
|
for i in range(steps + 1):
|
|
angle = i * step
|
|
next_angle = (i + 1) * step
|
|
x0 = center[0] + rad * math.cos(angle)
|
|
y0 = center[1] + rad * math.sin(angle)
|
|
x1 = center[0] + rad * math.cos(next_angle)
|
|
y1 = center[1] + rad * math.sin(next_angle)
|
|
self.drawFilledLineUncapped(x0, y0, x1, y1, self.line_width)
|
|
self.drawFilledCircle(x0, y0, self.line_width, 8)
|
|
|
|
def drawFilledLine(self, x0, y0, x1, y1, size):
|
|
self.drawFilledLineUncapped(x0, y0, x1, y1, size)
|
|
self.drawFilledCircle(x0, y0, size, 16)
|
|
self.drawFilledCircle(x1, y1, size, 16)
|
|
|
|
def drawFilledLineUncapped(self, x0, y0, x1, y1, size):
|
|
glBegin(GL_QUADS)
|
|
angle = math.atan2(y1 - y0, x1 - x0)
|
|
ninety = math.pi / 2
|
|
x_left = size * math.cos(angle + ninety)
|
|
y_left = size * math.sin(angle + ninety)
|
|
x_right = size * math.cos(angle - ninety)
|
|
y_right = size * math.sin(angle - ninety)
|
|
glVertex(x0 + x_left, y0 + y_left)
|
|
glVertex(x0 + x_right, y0 + y_right)
|
|
glVertex(x1 + x_right, y1 + y_right)
|
|
glVertex(x1 + x_left, y1 + y_left)
|
|
glEnd()
|
|
|
|
def drawFilledCircle(self, x, y, radius, steps):
|
|
glBegin(GL_TRIANGLE_FAN)
|
|
glVertex(x, y)
|
|
for i in range(steps + 1):
|
|
angle = i * 2 * math.pi / steps
|
|
glVertex(x + radius * math.cos(angle), y + radius * math.sin(angle))
|
|
glEnd()
|
|
|
|
def drawAxes(self):
|
|
glColor(*self.axis_color)
|
|
cx, cy = self.ptToScreenPt((0, 0))
|
|
self.drawFilledLine(cx - self.axis_length, cy,
|
|
cx + self.axis_length, cy, self.axis_width)
|
|
self.drawFilledLine(cx, cy - self.axis_length,
|
|
cx, cy + self.axis_length, self.axis_width)
|
|
|
|
def button_press_event(self, widget, event, data = None):
|
|
if event.button == 1:
|
|
if self.mode == 'line':
|
|
self.do_line_left_click(event.x, event.y)
|
|
elif self.mode == 'circle':
|
|
self.do_circle_left_click(event.x, event.y)
|
|
elif event.button == 2:
|
|
self.panning = True
|
|
self.panning_start = (event.x, self.size[1] - event.y)
|
|
elif event.button == 3:
|
|
if self.mode == 'line':
|
|
self.do_line_right_click(event.x, event.y)
|
|
elif self.mode == 'circle':
|
|
self.do_circle_right_click(event.x, event.y)
|
|
|
|
def button_release_event(self, widget, event, data = None):
|
|
if event.button == 2:
|
|
self.panning = False
|
|
|
|
def motion_event(self, widget, event, data = None):
|
|
if self.panning:
|
|
start_pt = self.screenPtToPt(self.panning_start)
|
|
this_pt = self.screenPtToPt((event.x, self.size[1] - event.y))
|
|
self.view_center = (self.view_center[0] - this_pt[0] + start_pt[0],
|
|
self.view_center[1] - this_pt[1] + start_pt[1])
|
|
self.panning_start = (event.x, self.size[1] - event.y)
|
|
self.queue_redraw()
|
|
elif self.drawingShape is not None:
|
|
if self.mode == 'line':
|
|
self.do_line_motion(event.x, event.y)
|
|
|
|
def scroll_event(self, widget, event, data = None):
|
|
if event.direction == gtk.gdk.SCROLL_UP:
|
|
zoom_pt = self.screenPtToPt((event.x,
|
|
self.size[1] - event.y))
|
|
off_x = zoom_pt[0] - self.view_center[0]
|
|
off_y = zoom_pt[1] - self.view_center[1]
|
|
self.view_center = (zoom_pt[0] - off_x / self.zoom_factor,
|
|
zoom_pt[1] - off_y / self.zoom_factor)
|
|
self.view_width /= self.zoom_factor
|
|
self.queue_redraw()
|
|
elif event.direction == gtk.gdk.SCROLL_DOWN:
|
|
zoom_pt = self.screenPtToPt((event.x,
|
|
self.size[1] - event.y))
|
|
off_x = zoom_pt[0] - self.view_center[0]
|
|
off_y = zoom_pt[1] - self.view_center[1]
|
|
self.view_center = (zoom_pt[0] - off_x * self.zoom_factor,
|
|
zoom_pt[1] - off_y * self.zoom_factor)
|
|
self.view_width *= self.zoom_factor
|
|
self.queue_redraw()
|
|
|
|
def queue_redraw(self):
|
|
self.widget.queue_draw_area(0, 0, *map(int, self.size))
|
|
|
|
def drawShapes(self):
|
|
for shape in self.sketch:
|
|
self.drawShape(shape)
|
|
|
|
def drawShape(self, shape):
|
|
if isinstance(shape, Line):
|
|
self.drawLine(shape)
|
|
elif isinstance(shape, Circle):
|
|
self.drawCircle(shape)
|
|
|
|
def drawConstraints(self):
|
|
for c in self.sketch.constraints:
|
|
self.drawConstraint(c)
|
|
|
|
def drawConstraint(self, c):
|
|
if isinstance(c, Connect):
|
|
self.drawConnect(c)
|
|
elif isinstance(c, Horizontal):
|
|
self.drawHorizontal(c)
|
|
elif isinstance(c, Vertical):
|
|
self.drawVertical(c)
|
|
|
|
def drawConnect(self, con):
|
|
glColor(*self.constraint_color)
|
|
pt = self.ptToScreenPt(con.shape1.getPt(con.pt1))
|
|
self.drawHandle(pt[0], pt[1])
|
|
|
|
def drawHandle(self, x, y):
|
|
glColor(*self.constraint_color)
|
|
self.drawFilledCircle(x, y, self.line_width, 4)
|
|
s = self.line_width * 2
|
|
glBegin(GL_LINE_LOOP)
|
|
glVertex(x + s, y + s)
|
|
glVertex(x - s, y + s)
|
|
glVertex(x - s, y - s)
|
|
glVertex(x + s, y - s)
|
|
glEnd()
|
|
|
|
def drawHorizontal(self, con):
|
|
glColor(*self.constraint_color)
|
|
l = 15.0
|
|
glPushAttrib(GL_LINE_BIT)
|
|
glEnable(GL_LINE_STIPPLE)
|
|
glLineStipple(3, 0x5555)
|
|
if con.shape1 == con.shape2 and isinstance(con.shape1, Line):
|
|
pt = self.ptToScreenPt(con.shape1.getPt(2))
|
|
glBegin(GL_LINES)
|
|
glVertex(pt[0] - l/2, pt[1] + self.line_width * 2)
|
|
glVertex(pt[0] + l/2, pt[1] + self.line_width * 2)
|
|
glVertex(pt[0] - l/2, pt[1] - self.line_width * 2)
|
|
glVertex(pt[0] + l/2, pt[1] - self.line_width * 2)
|
|
glEnd()
|
|
glPopAttrib()
|
|
|
|
def drawVertical(self, con):
|
|
glColor(*self.constraint_color)
|
|
l = 15.0
|
|
glPushAttrib(GL_LINE_BIT)
|
|
glEnable(GL_LINE_STIPPLE)
|
|
glLineStipple(3, 0x5555)
|
|
if con.shape1 == con.shape2 and isinstance(con.shape1, Line):
|
|
pt = self.ptToScreenPt(con.shape1.getPt(2))
|
|
glBegin(GL_LINES)
|
|
glVertex(pt[0] - self.line_width * 2, pt[1] + l/2)
|
|
glVertex(pt[0] - self.line_width * 2, pt[1] - l/2)
|
|
glVertex(pt[0] + self.line_width * 2, pt[1] + l/2)
|
|
glVertex(pt[0] + self.line_width * 2, pt[1] - l/2)
|
|
glEnd()
|
|
glPopAttrib()
|
|
|
|
def set_mode(self, mode):
|
|
if mode != self.mode:
|
|
if self.mode == 'line':
|
|
self.drawingShape = None
|
|
self.queue_redraw()
|
|
elif self.mode == 'circle':
|
|
self.drawingShape = None
|
|
self.queue_redraw()
|
|
self.mode = mode
|
|
|
|
def do_line_left_click(self, x, y):
|
|
start = self.screenPtToPt((x, self.size[1] - y))
|
|
if self.drawingShape is not None:
|
|
self.sketch.shapes.append(self.drawingShape)
|
|
for c in self.drawingConstraints:
|
|
self.sketch.constraints.append(c)
|
|
self.drawingConstraints = []
|
|
start = self.drawingShape.getPt(1) # start at last snap point
|
|
self.drawingShape = Line(start[0], start[1], start[0], start[1])
|
|
self.queue_redraw()
|
|
|
|
def do_line_right_click(self, x, y):
|
|
if self.drawingShape is not None:
|
|
# cancel line currently being drawn
|
|
self.drawingShape = None
|
|
self.queue_redraw()
|
|
|
|
def do_line_motion(self, x, y):
|
|
this_pt = self.screenPtToPt((x, self.size[1] - y))
|
|
start = self.drawingShape.getPt(0)
|
|
angle = math.atan2(this_pt[1] - start[1], this_pt[0] - start[0])
|
|
angle *= 180.0 / math.pi
|
|
def within(a, b, d): return abs(a - b) < d
|
|
def snaps_to(q): return within(angle, q, self.snap_angle)
|
|
snap_dist = self.screenDistToDist(self.snap_dist)
|
|
if ((snaps_to(-180) or snaps_to(180) or snaps_to(0))
|
|
and within(start[1], this_pt[1], snap_dist)):
|
|
this_pt = (this_pt[0], start[1])
|
|
if not (len(self.drawingConstraints) > 0
|
|
and isinstance(self.drawingConstraints[0], Horizontal)):
|
|
c = Horizontal(self.drawingShape, 0, self.drawingShape, 1)
|
|
self.drawingConstraints = [c]
|
|
elif ((snaps_to(-90) or snaps_to(90))
|
|
and within(start[0], this_pt[0], snap_dist)):
|
|
this_pt = (start[0], this_pt[1])
|
|
if not (len(self.drawingConstraints) > 0
|
|
and isinstance(self.drawingConstraints[0], Vertical)):
|
|
c = Vertical(self.drawingShape, 0, self.drawingShape, 1)
|
|
self.drawingConstraints = [c]
|
|
else:
|
|
self.drawingConstraints = []
|
|
self.drawingShape.setPt(1, this_pt)
|
|
self.queue_redraw()
|
|
|
|
def do_circle_left_click(self, x, y):
|
|
pt = self.screenPtToPt((x, self.size[1] - y))
|
|
if self.drawingShape is None:
|
|
self.drawingShape = Circle(pt[0], pt[1], 0)
|
|
|
|
def do_circle_right_click(self, x, y):
|
|
if self.drawingShape is not None:
|
|
# cancel circle currently being drawn
|
|
self.drawingShape = None
|
|
self.queue_redraw()
|