فهرست منبع

Add working prototype of vertex moving

Mostly working, vertex highlighting needs a bit of tweaking. The code
could use a refactoring as well.

Also change the way vertices are drawn (fill color).

Remove some duplicated code.
Michael Pitidis 13 سال پیش
والد
کامیت
12347de72b
3فایلهای تغییر یافته به همراه117 افزوده شده و 58 حذف شده
  1. 46 31
      canvas.py
  2. 5 0
      lib.py
  3. 66 27
      shape.py

+ 46 - 31
canvas.py

@@ -1,11 +1,10 @@
 
-from math import sqrt
-
 from PyQt4.QtGui import *
 from PyQt4.QtCore import *
 from PyQt4.QtOpenGL import *
 
 from shape import Shape
+from lib import distance
 
 # TODO:
 # - [maybe] Find optimal epsilon value.
@@ -47,6 +46,7 @@ class Canvas(QWidget):
         self._hideBackround = False
         self.hideBackround = False
         self.highlightedShape = None
+        self._nearest = None
         self._painter = QPainter()
         self._cursor = CURSOR_DEFAULT
         # Menus:
@@ -93,11 +93,11 @@ class Canvas(QWidget):
                     pos = self.current[0]
                     color = self.current.line_color
                     self.overrideCursor(CURSOR_POINT)
-                    self.current.highlightStart = True
+                    self.current.highlightVertex(0, Shape.NEAR_VERTEX)
                 self.line[1] = pos
                 self.line.line_color = color
                 self.repaint()
-                self.current.highlightStart = False
+                self.current.highlightClear()
             return
 
         # Polygon copy moving.
@@ -112,11 +112,15 @@ class Canvas(QWidget):
             return
 
         # Polygon moving.
-        if Qt.LeftButton & ev.buttons() and self.selectedShape and self.prevPoint:
-            self.overrideCursor(CURSOR_MOVE)
-            self.boundedMoveShape(self.selectedShape, pos)
-            self.shapeMoved.emit()
-            self.repaint()
+        if Qt.LeftButton & ev.buttons():
+            if self._nearest and self.prevPoint:
+                self.boundedMoveVertex(pos)
+                self.repaint()
+            elif self.selectedShape and self.prevPoint:
+                self.overrideCursor(CURSOR_MOVE)
+                self.boundedMoveShape(self.selectedShape, pos)
+                self.shapeMoved.emit()
+                self.repaint()
             return
 
         # Just hovering over the canvas:
@@ -124,17 +128,29 @@ class Canvas(QWidget):
         self.setToolTip("Image")
         previous = self.highlightedShape
         for shape in reversed(self.shapes):
-            if shape.containsPoint(pos) and self.isVisible(shape):
-                self.setToolTip("Object '%s'" % shape.label)
-                self.highlightedShape = shape
-                self.overrideCursor(CURSOR_GRAB)
-                break
+            if self.isVisible(shape):
+                v = shape.nearestVertex(pos, self.epsilon)
+                if v is not None:
+                    self._nearest = (v, shape)
+                    shape.highlightVertex(v, shape.MOVE_VERTEX)
+                    self.highlightedShape = shape
+                    self.overrideCursor(CURSOR_POINT)
+                    self.setStatusTip("Click & drag to move point")
+                    break
+                if shape.containsPoint(pos):
+                    self.setToolTip("Object '%s'" % shape.label)
+                    self.highlightedShape = shape
+                    self.overrideCursor(CURSOR_GRAB)
+                    break
         else:
+            if self.highlightedShape:
+                self.highlightedShape.highlightClear()
             self.highlightedShape = None
+            self._nearest = None
 
         if previous != self.highlightedShape:
             # Try to minimise repaints.
-            self.repaint()
+            self.update()
 
     def mousePressEvent(self, ev):
         pos = self.transformPos(ev.posF())
@@ -161,7 +177,6 @@ class Canvas(QWidget):
             self.repaint()
 
     def mouseReleaseEvent(self, ev):
-        pos = self.transformPos(ev.posF())
         if ev.button() == Qt.RightButton:
             menu = self.menus[bool(self.selectedShapeCopy)]
             self.restoreCursor()
@@ -208,7 +223,8 @@ class Canvas(QWidget):
             # Replace the last point with the starting point.
             # We have to do this because the mousePressEvent handler
             # adds that point before this handler is called!
-            self.current[-1] = self.current[0]
+            self.current.popPoint()
+            self.current.addPoint(self.current[0])
             self.finalise(ev)
 
     def selectShape(self, shape):
@@ -222,6 +238,10 @@ class Canvas(QWidget):
     def selectShapePoint(self, point):
         """Select the first shape created which contains this point."""
         self.deSelectShape()
+        if self._nearest:
+            i, s = self._nearest
+            s.highlightVertex(i, s.MOVE_VERTEX)
+            return
         for shape in reversed(self.shapes):
             if self.isVisible(shape) and shape.containsPoint(point):
                 shape.selected = True
@@ -239,6 +259,13 @@ class Canvas(QWidget):
         y2 = (rect.y() + rect.height()) - point.y()
         self.offsets = QPointF(x1, y1), QPointF(x2, y2)
 
+    def boundedMoveVertex(self, pos):
+        i, shape = self._nearest
+        point = shape[i]
+        if self.outOfPixmap(pos):
+            pos = self.intersectionPoint(point, pos)
+        shape.moveVertexBy(i, pos - point)
+
     def boundedMoveShape(self, shape, pos):
         if self.outOfPixmap(pos):
             return # No need to move
@@ -276,9 +303,10 @@ class Canvas(QWidget):
     def copySelectedShape(self):
         if self.selectedShape:
             shape = self.selectedShape.copy()
+            self.deSelectShape()
             self.shapes.append(shape)
+            shape.selected = True
             self.selectedShape = shape
-            self.deSelectShape()
             return shape
 
     def paintEvent(self, event):
@@ -441,16 +469,6 @@ class Canvas(QWidget):
         self.current = None
         self.repaint()
 
-    def copySelectedShape(self):
-        if self.selectedShape:
-            newShape=self.selectedShape.copy()
-            self.shapes.append(newShape)
-            self.deSelectShape()
-            self.shapes[-1].selected=True
-            self.selectedShape=self.shapes[-1]
-            self.repaint()
-            return self.selectedShape
-
     def setShapeVisible(self, shape, value):
         self.visible[shape] = value
         self.repaint()
@@ -471,6 +489,3 @@ class Canvas(QWidget):
 def pp(p):
     return '%.2f, %.2f' % (p.x(), p.y())
 
-def distance(p):
-    return sqrt(p.x() * p.x() + p.y() * p.y())
-

+ 5 - 0
lib.py

@@ -1,4 +1,6 @@
 
+from math import sqrt
+
 from PyQt4.QtGui import *
 from PyQt4.QtCore import *
 
@@ -51,3 +53,6 @@ class struct(object):
     def __init__(self, **kwargs):
         self.__dict__.update(kwargs)
 
+def distance(p):
+    return sqrt(p.x() * p.x() + p.y() * p.y())
+

+ 66 - 27
shape.py

@@ -4,25 +4,29 @@
 from PyQt4.QtGui import *
 from PyQt4.QtCore import *
 
-# FIXME:
-# - Add support for highlighting vertices.
+from lib import distance
 
 # TODO:
 # - [opt] Store paths instead of creating new ones at each paint.
 
 DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128)
 DEFAULT_FILL_COLOR = QColor(255, 0, 0, 128)
-DEFAULT_SELECT_COLOR = QColor(255, 255, 255)
+DEFAULT_SELECT_LINE_COLOR = QColor(255, 255, 255)
+DEFAULT_SELECT_FILL_COLOR = QColor(0, 128, 255, 155)
+DEFAULT_VERTEX_FILL_COLOR = QColor(255, 0, 0)
 
 class Shape(object):
     P_SQUARE, P_ROUND = range(2)
 
+    MOVE_VERTEX, NEAR_VERTEX = range(2)
+
     ## The following class variables influence the drawing
     ## of _all_ shape objects.
     line_color = DEFAULT_LINE_COLOR
     fill_color = DEFAULT_FILL_COLOR
-    sel_fill_color=QColor(0, 128, 255, 155)
-    select_color = DEFAULT_SELECT_COLOR
+    select_line_color = DEFAULT_SELECT_LINE_COLOR
+    select_fill_color = DEFAULT_SELECT_FILL_COLOR
+    vertex_fill_color = DEFAULT_VERTEX_FILL_COLOR
     point_type = P_ROUND
     point_size = 8
     scale = 1.0
@@ -32,7 +36,16 @@ class Shape(object):
         self.points = []
         self.fill = False
         self.selected = False
-        self.highlightStart = False
+
+        self._highlightIndex = None
+        self._highlightMode = self.NEAR_VERTEX
+        self._highlightSettings = {
+            self.NEAR_VERTEX: (4, self.P_ROUND),
+            self.MOVE_VERTEX: (2, self.P_SQUARE),
+            }
+
+        self._closed = False
+
         if line_color is not None:
             # Override the class line_color attribute
             # with an object attribute. Currently this
@@ -40,6 +53,9 @@ class Shape(object):
             self.line_color = line_color
 
     def addPoint(self, point):
+        if self.points and point == self.points[0]:
+            self._closed = True
+            return
         self.points.append(point)
 
     def popPoint(self):
@@ -48,11 +64,12 @@ class Shape(object):
         return None
 
     def isClosed(self):
-        return len(self.points) > 1 and self[0] == self[-1]
+        return self._closed
 
     def paint(self, painter):
         if self.points:
-            pen = QPen(self.select_color if self.selected else self.line_color)
+            color = self.select_line_color if self.selected else self.line_color
+            pen = QPen(color)
             # Try using integer sizes for smoother drawing(?)
             pen.setWidth(max(1, int(round(2.0 / self.scale))))
             painter.setPen(pen)
@@ -60,32 +77,44 @@ class Shape(object):
             line_path = QPainterPath()
             vrtx_path = QPainterPath()
 
-            line_path.moveTo(QPointF(self.points[0]))
-            self.drawVertex(vrtx_path, self.points[0],
-                    highlight=self.highlightStart)
+            line_path.moveTo(self.points[0])
+            # Uncommenting the following line will draw 2 paths
+            # for the 1st vertex, and make it non-filled, which
+            # may be desirable.
+            #self.drawVertex(vrtx_path, 0)
+
+            for i, p in enumerate(self.points):
+                line_path.lineTo(p)
+                self.drawVertex(vrtx_path, i)
+            if self.isClosed():
+                line_path.lineTo(self.points[0])
 
-            for p in self.points[1:]:
-                line_path.lineTo(QPointF(p))
-                # Skip last element, otherwise its vertex is not filled.
-                if p != self.points[0]:
-                    self.drawVertex(vrtx_path, p)
             painter.drawPath(line_path)
-            painter.fillPath(vrtx_path, self.line_color)
+            painter.drawPath(vrtx_path)
+            painter.fillPath(vrtx_path, self.vertex_fill_color)
             if self.fill:
-                if self.selected:
-                    fillColor=self.sel_fill_color
-                else:
-                    fillColor=self.fill_color
-                painter.fillPath(line_path,fillColor)
+                color = self.select_fill_color if self.selected else self.fill_color
+                painter.fillPath(line_path, color)
 
-    def drawVertex(self, path, point, highlight=False):
+    def drawVertex(self, path, i):
         d = self.point_size / self.scale
-        if highlight:
-            d *= 4
-        if self.point_type == self.P_SQUARE:
+        shape = self.point_type
+        point = self.points[i]
+        if i == self._highlightIndex:
+            size, shape = self._highlightSettings[self._highlightMode]
+            d *= size
+        if shape == self.P_SQUARE:
             path.addRect(point.x() - d/2, point.y() - d/2, d, d)
-        else:
+        elif shape == self.P_ROUND:
             path.addEllipse(point, d/2.0, d/2.0)
+        else:
+            assert False, "unsupported vertex shape"
+
+    def nearestVertex(self, point, epsilon):
+        for i, p in enumerate(self.points):
+            if distance(p - point) <= epsilon:
+                return i
+        return None
 
     def containsPoint(self, point):
         return self.makePath().contains(point)
@@ -102,6 +131,16 @@ class Shape(object):
     def moveBy(self, offset):
         self.points = [p + offset for p in self.points]
 
+    def moveVertexBy(self, i, offset):
+        self.points[i] = self.points[i] + offset
+
+    def highlightVertex(self, i, action):
+        self._highlightIndex = i
+        self._highlightMode = action
+
+    def highlightClear(self):
+        self._highlightIndex = None
+
     def copy(self):
         shape = Shape("Copy of %s" % self.label )
         shape.points= [p for p in self.points]