jpcad/SketchWidget.py

633 lines
22 KiB
Python

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(),
}
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.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))
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 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_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)
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'
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
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()
sw.set_hover_snap_point(None)
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)
sw.invalidate()
def do_right_click(self, sw, x, y):
sw.window.set_mode('')