import sys import math import gtk import gtk.gtkgl from OpenGL.GL import * from PointRef import PointRef from shapes import * from constraints import * from Sketch import * class SketchWidget(object): 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.modes = { 'select': SelectMode(), 'line': LineMode(), 'circle': CircleMode(), 'connect': ConnectMode(), 'horizontal': HorizontalMode(), 'vertical': VerticalMode(), 'xdim': XDimMode(), 'ydim': YDimMode(), } self.mode_name = 'select' self.mode = self.modes[self.mode_name] self.snap_ptrefs = {} # keyed on PointRef self.cursors = { 'arrow': gtk.gdk.Cursor(gtk.gdk.ARROW), 'crosshair': gtk.gdk.Cursor(gtk.gdk.CROSSHAIR), } self.cursor = 'arrow' self.hover_snap_ptref = None self.solved = False # 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.constraint_ip_color = (0.1, 1.0, 0.5, 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.solve() 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)) self.mode.draw(self) 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: self.mode.do_left_click(self, 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: self.mode.do_right_click(self, 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: self.mode.do_motion(self, 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 drawStippleConnect(self, pt1, pt2): self.drawHandle(*pt1) self.drawHandle(*pt2) glPushAttrib(GL_LINE_BIT) glEnable(GL_LINE_STIPPLE) glLineStipple(3, 0x5555) glBegin(GL_LINES) glVertex(*pt1) glVertex(*pt2) glEnd() glPopAttrib() def drawHorizontal(self, con): if con.shape1 == con.shape2 and isinstance(con.shape1, Line): l = 15.0 glPushAttrib(GL_LINE_BIT) glEnable(GL_LINE_STIPPLE) glLineStipple(3, 0x5555) 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() else: pt1 = self.ptToScreenPt(con.shape1.getPt(con.pt1)) pt2 = self.ptToScreenPt(con.shape2.getPt(con.pt2)) self.drawStippleConnect(pt1, pt2) def drawVertical(self, con): if con.shape1 == con.shape2 and isinstance(con.shape1, Line): l = 15.0 glPushAttrib(GL_LINE_BIT) glEnable(GL_LINE_STIPPLE) glLineStipple(3, 0x5555) 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() else: pt1 = self.ptToScreenPt(con.shape1.getPt(con.pt1)) pt2 = self.ptToScreenPt(con.shape2.getPt(con.pt2)) self.drawStippleConnect(pt1, pt2) def set_mode(self, mode_name): if mode_name not in self.modes: sys.stderr.write(__name__ + ':set_mode(): Unknown mode %s\n' % mode_name) elif mode_name != self.mode_name: self.mode.end_mode(self) self.mode_name = mode_name self.mode = self.modes[mode_name] self.cancel_drawing_shape() c = self.mode.get_cursor() if c in self.cursors and c != self.cursor: self.cursor = c self.widget.window.set_cursor(self.cursors[c]) self.mode.start_mode(self) def cancel_drawing_shape(self): if self.drawingShape is not None: self.drawingShape = None self.queue_redraw() 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 = {} self.invalidate() 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, x, y): sp = self.get_closest_snap_ptref(x, y) self.set_hover_snap_point(sp) def set_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() def invalidate(self): self.solved = False self.solve() def solve(self): if not self.solved: result = self.sketch.solve() self.solved = True valid = False text = '%d/%d' % (self.sketch.num_constraints, self.sketch.num_variables) if result == SOLVED: valid = True elif result == NO_SOLUTION: text = 'No Solution' elif result == OVERCONSTRAINED: text += ' - Overconstrained' elif result == UNDERCONSTRAINED: text += ' - Underconstrained' else: sys.stderr.write(__name__ + ':solve(): Unknown Sketch.solve() result\n') self.window.update_sketch_status(valid, text) self.update_snap_ptrefs() self.queue_redraw() class Mode(object): def do_left_click(self, sw, x, y): pass def do_right_click(self, sw, x, y): pass def do_motion(self, sw, x, y): pass def start_mode(self, sw): pass def end_mode(self, sw): pass def get_cursor(self): return 'arrow' def draw(self, sw): pass class SelectMode(Mode): pass class LineMode(Mode): def do_left_click(self, sw, x, y): click_pt = sw.screenPtToPt((x, sw.size[1] - y)) start = click_pt start_pt_ref = None if sw.hover_snap_ptref is not None: start = sw.hover_snap_ptref.getPt() start_pt_ref = sw.hover_snap_ptref if sw.drawingShape is not None: # end a currently drawing line # add snap points for newly drawn line sw.add_snap_ptrefs_from_shape(sw.drawingShape) start = sw.drawingShape.getPt(1) # start at last snap point prev_line = sw.drawingShape if sw.hover_snap_ptref is not None: c = Connect(sw.hover_snap_ptref.shape, sw.hover_snap_ptref.ptNum, sw.drawingShape, 1) sw.drawingConstraints['c2'] = c pt = sw.hover_snap_ptref.getPt() sw.drawingShape.setPt(1, pt) # don't do a horizontal/vertical when snapping sw.drawingConstraints['hv'] = None sw.merge_in_drawing_shape() start = prev_line.getPt(1) start_pt_ref = PointRef(prev_line, 1) # begin a new line sw.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, sw.drawingShape, 0) sw.drawingConstraints['c1'] = c sw.queue_redraw() def do_right_click(self, sw, x, y): if sw.drawingShape is not None: sw.cancel_drawing_shape() sw.queue_redraw() else: sw.window.set_mode('') def do_motion(self, sw, x, y): sw.update_hover_snap_point(x, y) if sw.drawingShape is not None: this_pt = sw.screenPtToPt((x, sw.size[1] - y)) if sw.hover_snap_ptref is not None: this_pt = sw.hover_snap_ptref.getPt() sw.drawingConstraints['hv'] = None else: start = sw.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, sw.snap_angle) hv_snap_dist = sw.screenDistToDist(sw.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 sw.drawingConstraints and isinstance(sw.drawingConstraints['hv'], Horizontal)): c = Horizontal( sw.drawingShape, 0, sw.drawingShape, 1) sw.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 sw.drawingConstraints and isinstance(sw.drawingConstraints['hv'], Vertical)): c = Vertical(sw.drawingShape, 0, sw.drawingShape, 1) sw.drawingConstraints['hv'] = c else: sw.drawingConstraints['hv'] = None sw.drawingShape.setPt(1, this_pt) sw.queue_redraw() def get_cursor(self): return 'crosshair' class CircleMode(Mode): def do_left_click(self, sw, x, y): pt = sw.screenPtToPt((x, sw.size[1] - y)) if sw.drawingShape is None: hsp = sw.hover_snap_ptref if hsp is not None: pt = hsp.getPt() sw.drawingShape = Circle(pt[0], pt[1], 0) if hsp is not None: c = Connect(hsp.shape, hsp.ptNum, sw.drawingShape, 0) sw.drawingConstraints['c'] = c sw.set_hover_snap_point(None) else: sw.add_snap_ptrefs_from_shape(sw.drawingShape) sw.merge_in_drawing_shape() sw.queue_redraw() def do_right_click(self, sw, x, y): if sw.drawingShape is not None: sw.cancel_drawing_shape() sw.queue_redraw() else: sw.window.set_mode('') def do_motion(self, sw, x, y): if sw.drawingShape is not None: pt = sw.screenPtToPt((x, sw.size[1] - y)) r = sw.dist_bw(sw.drawingShape.getPt(0), pt) sw.drawingShape.setRadius(r) sw.queue_redraw() else: sw.update_hover_snap_point(x, y) def get_cursor(self): return 'crosshair' class ConnectMode(Mode): def start_mode(self, sw): self.first_ptref = None def get_cursor(self): return 'crosshair' def do_motion(self, sw, x, y): sw.update_hover_snap_point(x, y) def do_left_click(self, sw, x, y): if sw.hover_snap_ptref is not None: if self.first_ptref is None: self.first_ptref = sw.hover_snap_ptref else: c = Connect(self.first_ptref.shape, self.first_ptref.ptNum, sw.hover_snap_ptref.shape, sw.hover_snap_ptref.ptNum) sw.sketch.constraints.append(c) self.first_ptref = None sw.invalidate() def do_right_click(self, sw, x, y): if self.first_ptref is not None: self.first_ptref = None else: sw.window.set_mode('') def draw(self, sw): if self.first_ptref is not None: glColor(*sw.constraint_ip_color) s, p = self.first_ptref.shape, self.first_ptref.ptNum sw.drawConstraint(Connect(s, p, s, p)) class HorizontalMode(ConnectMode): def do_left_click(self, sw, x, y): if sw.hover_snap_ptref is not None: if self.first_ptref is None: self.first_ptref = sw.hover_snap_ptref else: c = Horizontal(self.first_ptref.shape, self.first_ptref.ptNum, sw.hover_snap_ptref.shape, sw.hover_snap_ptref.ptNum) sw.sketch.constraints.append(c) self.first_ptref = None sw.invalidate() class VerticalMode(ConnectMode): def do_left_click(self, sw, x, y): if sw.hover_snap_ptref is not None: if self.first_ptref is None: self.first_ptref = sw.hover_snap_ptref else: c = Vertical(self.first_ptref.shape, self.first_ptref.ptNum, sw.hover_snap_ptref.shape, sw.hover_snap_ptref.ptNum) sw.sketch.constraints.append(c) self.first_ptref = None sw.invalidate() class DimMode(Mode): def start_mode(self, sw): self.ptrefs = [] def get_cursor(self): return 'crosshair' def do_motion(self, sw, x, y): if len(self.ptrefs) < 2: sw.update_hover_snap_point(x, y) def do_left_click(self, sw, x, y): if len(self.ptrefs) < 2: if sw.hover_snap_ptref is not None: self.ptrefs.append(sw.hover_snap_ptref) sw.set_hover_snap_point(None) else: c = self.get_constraint() sw.sketch.constraints.append(c) sw.invalidate() self.ptrefs = [] def do_right_click(self, sw, x, y): if len(self.ptrefs) > 0: self.ptrefs = [] else: sw.window.set_mode('') class XDimMode(DimMode): def get_constraint(self): if (self.ptrefs[0].shape.getPt(self.ptrefs[0].ptNum)[0] < self.ptrefs[1].shape.getPt(self.ptrefs[1].ptNum)[0]): p1, p2 = self.ptrefs else: p2, p1 = self.ptrefs dist = (p2.shape.getPt(p2.ptNum)[0] - p1.shape.getPt(p1.ptNum)[0]) return XDistance(p1.shape, p1.ptNum, p2.shape, p2.ptNum, dist) class YDimMode(DimMode): def get_constraint(self): if (self.ptrefs[0].shape.getPt(self.ptrefs[0].ptNum)[1] < self.ptrefs[1].shape.getPt(self.ptrefs[1].ptNum)[1]): p1, p2 = self.ptrefs else: p2, p1 = self.ptrefs dist = (p2.shape.getPt(p2.ptNum)[1] - p1.shape.getPt(p1.ptNum)[1]) return YDistance(p1.shape, p1.ptNum, p2.shape, p2.ptNum, dist)