import math import gtk import gtk.gtkgl from OpenGL.GL import * from PointRef import PointRef 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 = '' self.snap_ptrefs = {} # keyed on PointRef self.cursors = { 'arrow': gtk.gdk.Cursor(gtk.gdk.ARROW), 'crosshair': gtk.gdk.Cursor(gtk.gdk.CROSSHAIR), } self.hover_snap_ptref = None # 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.hover_color = (1.0, 1.0, 1.0, 1.0) self.constraint_color = (0.8, 1.0, 0.0, 1.0) self.snap_angle = 10 self.hv_snap_dist = 10 self.snap_dist = 6 self.snap_dist2 = self.snap_dist * self.snap_dist 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() self.update_snap_ptrefs() 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: glColor(*self.line_color) self.drawShape(self.drawingShape) glColor(*self.constraint_color) for c in self.drawingConstraints: self.drawConstraint(self.drawingConstraints[c]) if self.hover_snap_ptref is not None: glColor(*self.hover_color) s, p = self.hover_snap_ptref.shape, self.hover_snap_ptref.ptNum self.drawConnect(Connect(s, p, s, p)) 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): 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): 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 self.update_snap_ptrefs() 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() else: if self.mode == 'line': self.do_line_motion(event.x, event.y) elif self.mode == 'circle': self.do_circle_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.update_snap_ptrefs() 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.update_snap_ptrefs() self.queue_redraw() def queue_redraw(self): self.widget.queue_draw_area(0, 0, *map(int, self.size)) def drawShapes(self): glColor(*self.line_color) 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): glColor(*self.constraint_color) 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): pt = self.ptToScreenPt(con.shape1.getPt(con.pt1)) self.drawHandle(pt[0], pt[1]) def drawHandle(self, x, y): 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): 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): 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() if self.widget.window is not None: if mode in ('line', 'circle'): self.widget.window.set_cursor(self.cursors['crosshair']) else: self.widget.window.set_cursor(self.cursors['arrow']) self.mode = mode self.cancel_drawing_shape() def do_line_left_click(self, x, y): click_pt = self.screenPtToPt((x, self.size[1] - y)) start = click_pt start_pt_ref = None if self.hover_snap_ptref is not None: start = self.hover_snap_ptref.getPt() start_pt_ref = self.hover_snap_ptref if self.drawingShape is not None: # end a currently drawing line # add snap points for newly drawn line self.add_snap_ptrefs_from_shape(self.drawingShape) start = self.drawingShape.getPt(1) # start at last snap point prev_line = self.drawingShape if self.hover_snap_ptref is not None: c = Connect(self.hover_snap_ptref.shape, self.hover_snap_ptref.ptNum, self.drawingShape, 1) self.drawingConstraints['c2'] = c pt = self.hover_snap_ptref.getPt() self.drawingShape.setPt(1, pt) # don't do a horizontal/vertical when snapping self.drawingConstraints['hv'] = None self.merge_in_drawing_shape() start = prev_line.getPt(1) start_pt_ref = PointRef(prev_line, 1) # begin a new line self.drawingShape = Line( start[0], start[1], click_pt[0], click_pt[1]) if start_pt_ref is not None: c = Connect(start_pt_ref.shape, start_pt_ref.ptNum, self.drawingShape, 0) self.drawingConstraints['c1'] = c self.queue_redraw() def do_line_right_click(self, x, y): if self.drawingShape is not None: self.cancel_drawing_shape() self.queue_redraw() def do_line_motion(self, x, y): snap_ptref = self.get_closest_snap_ptref(x, y) self.update_hover_snap_point(snap_ptref) if self.drawingShape is not None: this_pt = self.screenPtToPt((x, self.size[1] - y)) if snap_ptref is not None: this_pt = snap_ptref.getPt() self.drawingConstraints['hv'] = None else: 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) hv_snap_dist = self.screenDistToDist(self.hv_snap_dist) if ((snaps_to(-180) or snaps_to(180) or snaps_to(0)) and within(start[1], this_pt[1], hv_snap_dist)): this_pt = (this_pt[0], start[1]) if not ('hv' in self.drawingConstraints and isinstance(self.drawingConstraints['hv'], Horizontal)): c = Horizontal( self.drawingShape, 0, self.drawingShape, 1) self.drawingConstraints['hv'] = c elif ((snaps_to(-90) or snaps_to(90)) and within(start[0], this_pt[0], hv_snap_dist)): this_pt = (start[0], this_pt[1]) if not ('hv' in self.drawingConstraints and isinstance(self.drawingConstraints['hv'], Vertical)): c = Vertical(self.drawingShape, 0, self.drawingShape, 1) self.drawingConstraints['hv'] = c else: self.drawingConstraints['hv'] = None 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: hsp = self.hover_snap_ptref if hsp is not None: pt = hsp.getPt() self.drawingShape = Circle(pt[0], pt[1], 0) if hsp is not None: c = Connect(hsp.shape, hsp.ptNum, self.drawingShape, 0) self.drawingConstraints['c'] = c else: self.add_snap_ptrefs_from_shape(self.drawingShape) self.merge_in_drawing_shape() self.queue_redraw() def do_circle_right_click(self, x, y): if self.drawingShape is not None: self.cancel_drawing_shape() self.queue_redraw() def do_circle_motion(self, x, y): if self.drawingShape is not None: pt = self.screenPtToPt((x, self.size[1] - y)) r = self.dist_bw(self.drawingShape.getPt(0), pt) self.drawingShape.setRadius(r) self.queue_redraw() self.update_hover_snap_point(None) else: sp = self.get_closest_snap_ptref(x, y) self.update_hover_snap_point(sp) def cancel_drawing_shape(self): self.drawingShape = None self.drawingConstraints = {} self.hover_snap_ptref = None def merge_in_drawing_shape(self): self.sketch.shapes.append(self.drawingShape) for c in self.drawingConstraints: if self.drawingConstraints[c] is not None: self.sketch.constraints.append(self.drawingConstraints[c]) self.drawingShape = None self.drawingConstraints = {} def dist_bw(self, pt1, pt2): x = pt2[0] - pt1[0] y = pt2[1] - pt1[1] return math.sqrt(x * x + y * y) def dist2_bw(self, pt1, pt2): x = pt2[0] - pt1[0] y = pt2[1] - pt1[1] return x * x + y * y def update_snap_ptrefs(self): self.snap_ptrefs = {} for s in self.sketch.shapes: self.add_snap_ptrefs_from_shape(s) def add_snap_ptrefs_from_shape(self, shape): for i in range(shape.nPts()): pt = self.ptToScreenPt(shape.getPt(i)) self.snap_ptrefs[PointRef(shape, i)] = pt def get_closest_snap_ptref(self, x, y): closest_ptref = None closest_dist = self.snap_dist2 * 2 for p in self.snap_ptrefs: screen_pt = self.snap_ptrefs[p] dist = self.dist2_bw((x, self.size[1] - y), screen_pt) if dist < closest_dist: closest_dist = dist closest_ptref = p if closest_dist <= self.snap_dist2: return closest_ptref return None def get_snap_ptrefs_within_range(self, x, y): ptrefs = [] for p in self.snap_ptrefs: screen_pt = self.snap_ptrefs[p] dist = self.dist2_bw((x, self.size[1] - y), screen_pt) if dist <= self.snap_dist2: ptrefs.append(p) return ptrefs def update_hover_snap_point(self, sp): if sp is not None: if sp != self.hover_snap_ptref: self.hover_snap_ptref = sp self.queue_redraw() else: if self.hover_snap_ptref is not None: self.hover_snap_ptref = None self.queue_redraw()