For any shape with a dimension smaller than 2*epsilon, there can be multiple potential points to select from. In practice this resulted in e.g. the top right corner being highlighted when the cursor was placed below the bottom right corner.
210 lines
6.5 KiB
Python
210 lines
6.5 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
try:
|
|
from PyQt5.QtGui import *
|
|
from PyQt5.QtCore import *
|
|
except ImportError:
|
|
from PyQt4.QtGui import *
|
|
from PyQt4.QtCore import *
|
|
|
|
from libs.utils import distance
|
|
import sys
|
|
|
|
DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128)
|
|
DEFAULT_FILL_COLOR = QColor(255, 0, 0, 128)
|
|
DEFAULT_SELECT_LINE_COLOR = QColor(255, 255, 255)
|
|
DEFAULT_SELECT_FILL_COLOR = QColor(0, 128, 255, 155)
|
|
DEFAULT_VERTEX_FILL_COLOR = QColor(0, 255, 0, 255)
|
|
DEFAULT_HVERTEX_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
|
|
select_line_color = DEFAULT_SELECT_LINE_COLOR
|
|
select_fill_color = DEFAULT_SELECT_FILL_COLOR
|
|
vertex_fill_color = DEFAULT_VERTEX_FILL_COLOR
|
|
h_vertex_fill_color = DEFAULT_HVERTEX_FILL_COLOR
|
|
point_type = P_ROUND
|
|
point_size = 16
|
|
scale = 1.0
|
|
label_font_size = 8
|
|
|
|
def __init__(self, label=None, line_color=None, difficult=False, paint_label=False):
|
|
self.label = label
|
|
self.points = []
|
|
self.fill = False
|
|
self.selected = False
|
|
self.difficult = difficult
|
|
self.paint_label = paint_label
|
|
|
|
self._highlight_index = None
|
|
self._highlight_mode = self.NEAR_VERTEX
|
|
self._highlight_settings = {
|
|
self.NEAR_VERTEX: (4, self.P_ROUND),
|
|
self.MOVE_VERTEX: (1.5, self.P_SQUARE),
|
|
}
|
|
|
|
self._closed = False
|
|
|
|
if line_color is not None:
|
|
# Override the class line_color attribute
|
|
# with an object attribute. Currently this
|
|
# is used for drawing the pending line a different color.
|
|
self.line_color = line_color
|
|
|
|
def close(self):
|
|
self._closed = True
|
|
|
|
def reach_max_points(self):
|
|
if len(self.points) >= 4:
|
|
return True
|
|
return False
|
|
|
|
def add_point(self, point):
|
|
if not self.reach_max_points():
|
|
self.points.append(point)
|
|
|
|
def pop_point(self):
|
|
if self.points:
|
|
return self.points.pop()
|
|
return None
|
|
|
|
def is_closed(self):
|
|
return self._closed
|
|
|
|
def set_open(self):
|
|
self._closed = False
|
|
|
|
def paint(self, painter):
|
|
if self.points:
|
|
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)
|
|
|
|
line_path = QPainterPath()
|
|
vertex_path = QPainterPath()
|
|
|
|
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(vertex_path, 0)
|
|
|
|
for i, p in enumerate(self.points):
|
|
line_path.lineTo(p)
|
|
self.draw_vertex(vertex_path, i)
|
|
if self.is_closed():
|
|
line_path.lineTo(self.points[0])
|
|
|
|
painter.drawPath(line_path)
|
|
painter.drawPath(vertex_path)
|
|
painter.fillPath(vertex_path, self.vertex_fill_color)
|
|
|
|
# Draw text at the top-left
|
|
if self.paint_label:
|
|
min_x = sys.maxsize
|
|
min_y = sys.maxsize
|
|
min_y_label = int(1.25 * self.label_font_size)
|
|
for point in self.points:
|
|
min_x = min(min_x, point.x())
|
|
min_y = min(min_y, point.y())
|
|
if min_x != sys.maxsize and min_y != sys.maxsize:
|
|
font = QFont()
|
|
font.setPointSize(self.label_font_size)
|
|
font.setBold(True)
|
|
painter.setFont(font)
|
|
if self.label is None:
|
|
self.label = ""
|
|
if min_y < min_y_label:
|
|
min_y += min_y_label
|
|
painter.drawText(int(min_x), int(min_y), self.label)
|
|
|
|
if self.fill:
|
|
color = self.select_fill_color if self.selected else self.fill_color
|
|
painter.fillPath(line_path, color)
|
|
|
|
def draw_vertex(self, path, i):
|
|
d = self.point_size / self.scale
|
|
shape = self.point_type
|
|
point = self.points[i]
|
|
if i == self._highlight_index:
|
|
size, shape = self._highlight_settings[self._highlight_mode]
|
|
d *= size
|
|
if self._highlight_index is not None:
|
|
self.vertex_fill_color = self.h_vertex_fill_color
|
|
else:
|
|
self.vertex_fill_color = Shape.vertex_fill_color
|
|
if shape == self.P_SQUARE:
|
|
path.addRect(point.x() - d / 2, point.y() - d / 2, d, d)
|
|
elif shape == self.P_ROUND:
|
|
path.addEllipse(point, d / 2.0, d / 2.0)
|
|
else:
|
|
assert False, "unsupported vertex shape"
|
|
|
|
def nearest_vertex(self, point, epsilon):
|
|
index = None
|
|
for i, p in enumerate(self.points):
|
|
dist = distance(p - point)
|
|
if dist <= epsilon:
|
|
index = i
|
|
epsilon = dist
|
|
return index
|
|
|
|
def contains_point(self, point):
|
|
return self.make_path().contains(point)
|
|
|
|
def make_path(self):
|
|
path = QPainterPath(self.points[0])
|
|
for p in self.points[1:]:
|
|
path.lineTo(p)
|
|
return path
|
|
|
|
def bounding_rect(self):
|
|
return self.make_path().boundingRect()
|
|
|
|
def move_by(self, offset):
|
|
self.points = [p + offset for p in self.points]
|
|
|
|
def move_vertex_by(self, i, offset):
|
|
self.points[i] = self.points[i] + offset
|
|
|
|
def highlight_vertex(self, i, action):
|
|
self._highlight_index = i
|
|
self._highlight_mode = action
|
|
|
|
def highlight_clear(self):
|
|
self._highlight_index = None
|
|
|
|
def copy(self):
|
|
shape = Shape("%s" % self.label)
|
|
shape.points = [p for p in self.points]
|
|
shape.fill = self.fill
|
|
shape.selected = self.selected
|
|
shape._closed = self._closed
|
|
if self.line_color != Shape.line_color:
|
|
shape.line_color = self.line_color
|
|
if self.fill_color != Shape.fill_color:
|
|
shape.fill_color = self.fill_color
|
|
shape.difficult = self.difficult
|
|
return shape
|
|
|
|
def __len__(self):
|
|
return len(self.points)
|
|
|
|
def __getitem__(self, key):
|
|
return self.points[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
self.points[key] = value
|