瀏覽代碼

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.QtGui import *
 from PyQt4.QtCore import *
 from PyQt4.QtCore import *
 from PyQt4.QtOpenGL import *
 from PyQt4.QtOpenGL import *
 
 
 from shape import Shape
 from shape import Shape
+from lib import distance
 
 
 # TODO:
 # TODO:
 # - [maybe] Find optimal epsilon value.
 # - [maybe] Find optimal epsilon value.
@@ -47,6 +46,7 @@ class Canvas(QWidget):
         self._hideBackround = False
         self._hideBackround = False
         self.hideBackround = False
         self.hideBackround = False
         self.highlightedShape = None
         self.highlightedShape = None
+        self._nearest = None
         self._painter = QPainter()
         self._painter = QPainter()
         self._cursor = CURSOR_DEFAULT
         self._cursor = CURSOR_DEFAULT
         # Menus:
         # Menus:
@@ -93,11 +93,11 @@ class Canvas(QWidget):
                     pos = self.current[0]
                     pos = self.current[0]
                     color = self.current.line_color
                     color = self.current.line_color
                     self.overrideCursor(CURSOR_POINT)
                     self.overrideCursor(CURSOR_POINT)
-                    self.current.highlightStart = True
+                    self.current.highlightVertex(0, Shape.NEAR_VERTEX)
                 self.line[1] = pos
                 self.line[1] = pos
                 self.line.line_color = color
                 self.line.line_color = color
                 self.repaint()
                 self.repaint()
-                self.current.highlightStart = False
+                self.current.highlightClear()
             return
             return
 
 
         # Polygon copy moving.
         # Polygon copy moving.
@@ -112,11 +112,15 @@ class Canvas(QWidget):
             return
             return
 
 
         # Polygon moving.
         # 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
             return
 
 
         # Just hovering over the canvas:
         # Just hovering over the canvas:
@@ -124,17 +128,29 @@ class Canvas(QWidget):
         self.setToolTip("Image")
         self.setToolTip("Image")
         previous = self.highlightedShape
         previous = self.highlightedShape
         for shape in reversed(self.shapes):
         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:
         else:
+            if self.highlightedShape:
+                self.highlightedShape.highlightClear()
             self.highlightedShape = None
             self.highlightedShape = None
+            self._nearest = None
 
 
         if previous != self.highlightedShape:
         if previous != self.highlightedShape:
             # Try to minimise repaints.
             # Try to minimise repaints.
-            self.repaint()
+            self.update()
 
 
     def mousePressEvent(self, ev):
     def mousePressEvent(self, ev):
         pos = self.transformPos(ev.posF())
         pos = self.transformPos(ev.posF())
@@ -161,7 +177,6 @@ class Canvas(QWidget):
             self.repaint()
             self.repaint()
 
 
     def mouseReleaseEvent(self, ev):
     def mouseReleaseEvent(self, ev):
-        pos = self.transformPos(ev.posF())
         if ev.button() == Qt.RightButton:
         if ev.button() == Qt.RightButton:
             menu = self.menus[bool(self.selectedShapeCopy)]
             menu = self.menus[bool(self.selectedShapeCopy)]
             self.restoreCursor()
             self.restoreCursor()
@@ -208,7 +223,8 @@ class Canvas(QWidget):
             # Replace the last point with the starting point.
             # Replace the last point with the starting point.
             # We have to do this because the mousePressEvent handler
             # We have to do this because the mousePressEvent handler
             # adds that point before this handler is called!
             # 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)
             self.finalise(ev)
 
 
     def selectShape(self, shape):
     def selectShape(self, shape):
@@ -222,6 +238,10 @@ class Canvas(QWidget):
     def selectShapePoint(self, point):
     def selectShapePoint(self, point):
         """Select the first shape created which contains this point."""
         """Select the first shape created which contains this point."""
         self.deSelectShape()
         self.deSelectShape()
+        if self._nearest:
+            i, s = self._nearest
+            s.highlightVertex(i, s.MOVE_VERTEX)
+            return
         for shape in reversed(self.shapes):
         for shape in reversed(self.shapes):
             if self.isVisible(shape) and shape.containsPoint(point):
             if self.isVisible(shape) and shape.containsPoint(point):
                 shape.selected = True
                 shape.selected = True
@@ -239,6 +259,13 @@ class Canvas(QWidget):
         y2 = (rect.y() + rect.height()) - point.y()
         y2 = (rect.y() + rect.height()) - point.y()
         self.offsets = QPointF(x1, y1), QPointF(x2, y2)
         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):
     def boundedMoveShape(self, shape, pos):
         if self.outOfPixmap(pos):
         if self.outOfPixmap(pos):
             return # No need to move
             return # No need to move
@@ -276,9 +303,10 @@ class Canvas(QWidget):
     def copySelectedShape(self):
     def copySelectedShape(self):
         if self.selectedShape:
         if self.selectedShape:
             shape = self.selectedShape.copy()
             shape = self.selectedShape.copy()
+            self.deSelectShape()
             self.shapes.append(shape)
             self.shapes.append(shape)
+            shape.selected = True
             self.selectedShape = shape
             self.selectedShape = shape
-            self.deSelectShape()
             return shape
             return shape
 
 
     def paintEvent(self, event):
     def paintEvent(self, event):
@@ -441,16 +469,6 @@ class Canvas(QWidget):
         self.current = None
         self.current = None
         self.repaint()
         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):
     def setShapeVisible(self, shape, value):
         self.visible[shape] = value
         self.visible[shape] = value
         self.repaint()
         self.repaint()
@@ -471,6 +489,3 @@ class Canvas(QWidget):
 def pp(p):
 def pp(p):
     return '%.2f, %.2f' % (p.x(), p.y())
     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.QtGui import *
 from PyQt4.QtCore import *
 from PyQt4.QtCore import *
 
 
@@ -51,3 +53,6 @@ class struct(object):
     def __init__(self, **kwargs):
     def __init__(self, **kwargs):
         self.__dict__.update(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.QtGui import *
 from PyQt4.QtCore import *
 from PyQt4.QtCore import *
 
 
-# FIXME:
-# - Add support for highlighting vertices.
+from lib import distance
 
 
 # TODO:
 # TODO:
 # - [opt] Store paths instead of creating new ones at each paint.
 # - [opt] Store paths instead of creating new ones at each paint.
 
 
 DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128)
 DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128)
 DEFAULT_FILL_COLOR = QColor(255, 0, 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):
 class Shape(object):
     P_SQUARE, P_ROUND = range(2)
     P_SQUARE, P_ROUND = range(2)
 
 
+    MOVE_VERTEX, NEAR_VERTEX = range(2)
+
     ## The following class variables influence the drawing
     ## The following class variables influence the drawing
     ## of _all_ shape objects.
     ## of _all_ shape objects.
     line_color = DEFAULT_LINE_COLOR
     line_color = DEFAULT_LINE_COLOR
     fill_color = DEFAULT_FILL_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_type = P_ROUND
     point_size = 8
     point_size = 8
     scale = 1.0
     scale = 1.0
@@ -32,7 +36,16 @@ class Shape(object):
         self.points = []
         self.points = []
         self.fill = False
         self.fill = False
         self.selected = 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:
         if line_color is not None:
             # Override the class line_color attribute
             # Override the class line_color attribute
             # with an object attribute. Currently this
             # with an object attribute. Currently this
@@ -40,6 +53,9 @@ class Shape(object):
             self.line_color = line_color
             self.line_color = line_color
 
 
     def addPoint(self, point):
     def addPoint(self, point):
+        if self.points and point == self.points[0]:
+            self._closed = True
+            return
         self.points.append(point)
         self.points.append(point)
 
 
     def popPoint(self):
     def popPoint(self):
@@ -48,11 +64,12 @@ class Shape(object):
         return None
         return None
 
 
     def isClosed(self):
     def isClosed(self):
-        return len(self.points) > 1 and self[0] == self[-1]
+        return self._closed
 
 
     def paint(self, painter):
     def paint(self, painter):
         if self.points:
         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(?)
             # Try using integer sizes for smoother drawing(?)
             pen.setWidth(max(1, int(round(2.0 / self.scale))))
             pen.setWidth(max(1, int(round(2.0 / self.scale))))
             painter.setPen(pen)
             painter.setPen(pen)
@@ -60,32 +77,44 @@ class Shape(object):
             line_path = QPainterPath()
             line_path = QPainterPath()
             vrtx_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.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.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
         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)
             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)
             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):
     def containsPoint(self, point):
         return self.makePath().contains(point)
         return self.makePath().contains(point)
@@ -102,6 +131,16 @@ class Shape(object):
     def moveBy(self, offset):
     def moveBy(self, offset):
         self.points = [p + offset for p in self.points]
         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):
     def copy(self):
         shape = Shape("Copy of %s" % self.label )
         shape = Shape("Copy of %s" % self.label )
         shape.points= [p for p in self.points]
         shape.points= [p for p in self.points]