def __getAttributePosition(self, attrType): attr_x_pos = 0 if attrType.match(EAttribute.kTypeOutput): attr_x_pos = self.__titleRect.width() - self.__attrRect.width() rect = self.__attrRect.translated( QPointF(attr_x_pos, self.__out_attr_step)) point = QPointF((rect.topRight() + rect.bottomRight()) / 2) point.setX(point.x() + self.pen().width() * 2) self.__out_attr_step += self.__attrRect.width() + self.pen().width( ) return [rect, point] rect = self.__attrRect.translated( QPointF(attr_x_pos, self.__in_attr_step)) point = QPointF((rect.topLeft() + rect.bottomLeft()) / 2) point.setX(point.x() - self.pen().width() * 2) self.__in_attr_step += self.__attrRect.width() + self.pen().width() return [rect, point]
def paint(self, painter, option, widget=None): pt = QPointF(-self.WIDTH / 2, -self.HEIGHT / 2) + self.position rect = QRectF(pt.x(), pt.y(), self.WIDTH, self.HEIGHT) if self.isIn: painter.setPen(QPen(Qt.transparent, 1)) painter.setBrush(QBrush(Qt.lightGray, Qt.SolidPattern)) else: painter.setPen(QPen(Qt.transparent, 1)) painter.setBrush(QBrush(Qt.transparent, Qt.SolidPattern)) painter.drawRect(rect) painter.drawPixmap(pt.x(), pt.y(), self.pixmap)
def drawPipeline(self, qp): """Called by paintEvent, it draws figures and link between them. Parameters ---------- qp : QtGui.QPainter Performs low-level painting """ # If self.levels is empty, indeed, it does not make sense to draw # something. if self.levels is None: return # define Rep position because they change whan resising main windows width = self.size().width() height = self.size().height() if len(self.levels) != 0: total_height = (len(self.levels) - 1) * ( STAGE_SIZE_Y + ROUTER_SIZE_Y) self.zoom = height / total_height else: self.zoom = 1 # center figures on screen last_point = QPointF(width / 2, -GAP_Y / 2 * self.zoom) for level in self.levels: total_x = len(level) * STAGE_SIZE_X * self.zoom + ( len(level) - 1 * STAGE_GAP_X * self.zoom) last_point.setX(width / 2 - total_x / 2) last_point.setY(last_point.y() + GAP_Y * self.zoom) for figure in level: figure.setCenter(QPointF(last_point)) last_point.setX(last_point.x() + STAGE_GAP_X * self.zoom) # Start to paint size = self.size() lines = list() last_level_pt = list() for level in self.levels: current_level_pt = list() for figure in level: figure.draw(qp, zoom=self.zoom) connexion_pt = QPointF(figure.center.x(), figure.center.y() - figure.size_y / 2 * self.zoom) current_level_pt.append(QPointF(connexion_pt.x(), connexion_pt.y() + figure.size_y * self.zoom)) # Link to previous level connexion point(s) for point in last_level_pt: lines.append(QLineF(point, connexion_pt)) # Keep points for next level last_level_pt = list(current_level_pt) for line in lines: qp.setPen(QtGui.QPen(self.blue_cta, 1, QtCore.Qt.SolidLine)) qp.drawLine(line)
def paint(self, painter, option, widget=None): myPen = self.pen() myPen.setColor(self.myColor) painter.setPen(myPen) painter.setBrush(self.myColor) controlPoints = [] endPt = self.endItem.getLinkPointForParameter(self.endIndex) startPt = self.startItem.getLinkPointForOutput(self.startIndex) if isinstance(self.startItem.element, Algorithm): if self.startIndex != -1: controlPoints.append(self.startItem.pos() + startPt) controlPoints.append(self.startItem.pos() + startPt + QPointF(ModelerGraphicItem.BOX_WIDTH / 2, 0)) controlPoints.append(self.endItem.pos() + endPt - QPointF(ModelerGraphicItem.BOX_WIDTH / 2, 0)) controlPoints.append(self.endItem.pos() + endPt) pt = QPointF(self.startItem.pos() + startPt + QPointF(-3, -3)) painter.drawEllipse(pt.x(), pt.y(), 6, 6) pt = QPointF(self.endItem.pos() + endPt + QPointF(-3, -3)) painter.drawEllipse(pt.x(), pt.y(), 6, 6) else: # Case where there is a dependency on an algorithm not # on an output controlPoints.append(self.startItem.pos() + startPt) controlPoints.append(self.startItem.pos() + startPt + QPointF(ModelerGraphicItem.BOX_WIDTH / 2, 0)) controlPoints.append(self.endItem.pos() + endPt - QPointF(ModelerGraphicItem.BOX_WIDTH / 2, 0)) controlPoints.append(self.endItem.pos() + endPt) else: controlPoints.append(self.startItem.pos()) controlPoints.append(self.startItem.pos() + QPointF(ModelerGraphicItem.BOX_WIDTH / 2, 0)) controlPoints.append(self.endItem.pos() + endPt - QPointF(ModelerGraphicItem.BOX_WIDTH / 2, 0)) controlPoints.append(self.endItem.pos() + endPt) pt = QPointF(self.endItem.pos() + endPt + QPointF(-3, -3)) painter.drawEllipse(pt.x(), pt.y(), 6, 6) path = QPainterPath() path.moveTo(controlPoints[0]) path.cubicTo(*controlPoints[1:]) painter.strokePath(path, painter.pen()) self.setPath(path)
def drawArrow(self, paint, x1, y1, x2, y2): m = paint.worldMatrix() paint.translate(x1, y1) pi = 3.1415926 if abs(x2 - x1) > 0: alpha = math.atan(abs(y2 - y1) / abs(x2 - x1)) * 180 / pi else: alpha = 90 if y2 > y1: if x2 > x1: paint.rotate(alpha) else: paint.rotate(180 - alpha) else: if x2 > x1: paint.rotate(-alpha) else: paint.rotate(alpha - 180) endcoord = math.sqrt((x2 - x1)**2 + (y2 - y1)**2) - self.size / 2 p1 = QPointF(endcoord, 0) paint.drawLine(0, 0, p1.x(), 0) coord = math.sqrt(9**2 - 6**2) p2 = QPointF(endcoord - coord, 6) p3 = QPointF(endcoord - coord, -6) path = QPainterPath() path.moveTo(p1) path.lineTo(p2) path.lineTo(p3) path.lineTo(p1) paint.fillPath(path, paint.pen().color()) paint.setWorldMatrix(m)
def draw(self, qpainter, zoom=1): """Draw this figure Parameters ---------- qpainter: PySide.QtGui.QPainter """ size_x = self.size_x * zoom size_y = self.size_y * zoom pensize = 3 qpainter.setPen( QtGui.QPen(PipelineDrawer.blue_cta, pensize, QtCore.Qt.SolidLine)) text_pos = QPointF(self.center) text_pos.setX(text_pos.x() - size_x / 2 + 2) text_pos.setY(text_pos.y() + pensize) qpainter.drawText(text_pos, str(self.nb_job_done)) pt = QPointF(self.center) pt.setX(5) pos = self.name.find("$$thread_number$$") if pos != -1: name = self.name[0:pos] else: name = self.name qpainter.drawText(pt, name) if self.running == True: qpainter.setPen( QtGui.QPen(PipelineDrawer.mygreen, 3, QtCore.Qt.SolidLine)) else: qpainter.setPen( QtGui.QPen(PipelineDrawer.blue_cta, 3, QtCore.Qt.SolidLine)) x1 = self.center.x() - (size_x / 2) y1 = self.center.y() - (size_y / 2) qpainter.drawRoundedRect(x1, y1, size_x, size_y, 12.0, 12.0)
def distToLine(pt, p1, p2): """ Compute the distance from the point `pt` to the line segment [p1,p2] """ u = p2-p1 lu = u.x()*u.x() + u.y()*u.y() pmax = QPointF(max(abs(p1.x()), abs(p2.x())), max(abs(p1.y()), abs(p2.y()))) if lu / (pmax.x()*pmax.x() + pmax.y()*pmax.y()) < 1e-10: diff = u - p1 return sqrt(diff.x()*diff.x() + diff.y()*diff.y()) dp = pt-p1 proj = (u.x()*dp.x() + u.y()*dp.y()) if proj >= 0 and proj <= lu: return abs(dp.x()*u.y() - u.x()*dp.y())/sqrt(lu) elif proj < 0: return dp.x()*dp.x() + dp.y()*dp.y() else: return dist(pt, p2)
def movePoints(self, image_name, pt_ids): if image_name == self.image_name: data = self.current_data dm = self.data_manager points = self.points cells = self.cells for pt_id in pt_ids: pos = data[pt_id] pos = QPointF(pos.x() / self.min_scale, pos.y() / self.min_scale) points[pt_id].setPos(pos) for cid in dm.cell_points[pt_id]: cell = cells.get(cid, None) if cell is not None and cell.isVisible(): cell.setGeometry()
def updateSmooth(self): if self.m_smooth: if self.m_cursor.x() != self.m_smooth_x or self.m_cursor.y() != self.m_smooth_y: if abs(self.m_cursor.x() - self.m_smooth_x) <= 0.001: self.m_smooth_x = self.m_cursor.x() return elif abs(self.m_cursor.y() - self.m_smooth_y) <= 0.001: self.m_smooth_y = self.m_cursor.y() return new_x = (self.m_smooth_x + self.m_cursor.x() * 3) / 4 new_y = (self.m_smooth_y + self.m_cursor.y() * 3) / 4 pos = QPointF(new_x, new_y) self.m_cursor.setPos(pos) self.m_lineH.setY(pos.y()) self.m_lineV.setX(pos.x()) xp = pos.x() / (self.p_size.x() + self.p_size.width()) yp = pos.y() / (self.p_size.y() + self.p_size.height()) self.sendMIDI(xp, yp) self.emit(SIGNAL("cursorMoved(double, double)"), xp, yp)
def draw(self, qpainter, zoom=1): """Draw this figure Parameters ---------- qpainter: PySide.QtGui.QPainter """ size_x = self.size_x * zoom size_y = self.size_y * zoom pensize = 3 qpainter.setPen( QtGui.QPen(QtCore.Qt.black, pensize, QtCore.Qt.SolidLine)) qpainter.drawEllipse(self.center, size_x / 2, size_y / 2) text_pos = QPointF(self.center) text_pos.setX(text_pos.x() - size_x / 2 + 10) text_pos.setY(text_pos.y() + pensize) qpainter.drawText(text_pos, str(self.queue_size)) qpainter.setPen( QtGui.QPen(PipelineDrawer.blue_cta, 3, QtCore.Qt.SolidLine))
def path_link_disabled(basepath): """ Return a QPainterPath 'styled' to indicate a 'disabled' link. A disabled link is displayed with a single disconnection symbol in the middle (--||--) Parameters ---------- basepath : QPainterPath The base path (a simple curve spine). Returns ------- path : QPainterPath A 'styled' link path """ segmentlen = basepath.length() px = 5 if segmentlen < 10: return QPainterPath(basepath) t = (px / 2) / segmentlen p1, _ = qpainterpath_simple_split(basepath, 0.50 - t) _, p2 = qpainterpath_simple_split(basepath, 0.50 + t) angle = -basepath.angleAtPercent(0.5) + 90 angler = math.radians(angle) normal = QPointF(math.cos(angler), math.sin(angler)) end1 = p1.currentPosition() start2 = QPointF(p2.elementAt(0).x, p2.elementAt(0).y) p1.moveTo(start2.x(), start2.y()) p1.addPath(p2) def QPainterPath_addLine(path, line): path.moveTo(line.p1()) path.lineTo(line.p2()) QPainterPath_addLine(p1, QLineF(end1 - normal * 3, end1 + normal * 3)) QPainterPath_addLine(p1, QLineF(start2 - normal * 3, start2 + normal * 3)) return p1
def paint(self, painter, option, widget): arc_rect = 10 connector_length = 5 painter.setPen(self.pen) path = QtGui.QPainterPath() if self.source.x() == self.dest.x(): path.moveTo(self.source.x(), self.source.y()) path.lineTo(self.dest.x(), self.dest.y()) painter.drawPath(path) else: #Define points starting from source point1 = QPointF(self.source.x(), self.source.y()) point2 = QPointF(point1.x(), point1.y() - connector_length) point3 = QPointF(point2.x() + arc_rect, point2.y() - arc_rect) #Define points starting from dest point4 = QPointF(self.dest.x(), self.dest.y()) point5 = QPointF(point4.x(),point3.y() - arc_rect) point6 = QPointF(point5.x() - arc_rect, point5.y() + arc_rect) start_angle_arc1 = 180 span_angle_arc1 = 90 start_angle_arc2 = 90 span_angle_arc2 = -90 # If the dest is at the left of the source, then we # need to reverse some values if self.source.x() > self.dest.x(): point5 = QPointF(point4.x(), point4.y() + connector_length) point6 = QPointF(point5.x() + arc_rect, point5.y() + arc_rect) point3 = QPointF(self.source.x() - arc_rect, point6.y()) point2 = QPointF(self.source.x(), point3.y() + arc_rect) span_angle_arc1 = 90 path.moveTo(point1) path.lineTo(point2) path.arcTo(QRectF(point2, point3), start_angle_arc1, span_angle_arc1) path.lineTo(point6) path.arcTo(QRectF(point6, point5), start_angle_arc2, span_angle_arc2) path.lineTo(point4) painter.drawPath(path)
def paint(self, painter, option, widget): arc_rect = 10 connector_length = 5 painter.setPen(self.pen) path = QtGui.QPainterPath() if self.source.x() == self.dest.x(): path.moveTo(self.source.x(), self.source.y()) path.lineTo(self.dest.x(), self.dest.y()) painter.drawPath(path) else: #Define points starting from source point1 = QPointF(self.source.x(), self.source.y()) point2 = QPointF(point1.x(), point1.y() - connector_length) point3 = QPointF(point2.x() + arc_rect, point2.y() - arc_rect) #Define points starting from dest point4 = QPointF(self.dest.x(), self.dest.y()) point5 = QPointF(point4.x(), point3.y() - arc_rect) point6 = QPointF(point5.x() - arc_rect, point5.y() + arc_rect) start_angle_arc1 = 180 span_angle_arc1 = 90 start_angle_arc2 = 90 span_angle_arc2 = -90 # If the dest is at the left of the source, then we # need to reverse some values if self.source.x() > self.dest.x(): point5 = QPointF(point4.x(), point4.y() + connector_length) point6 = QPointF(point5.x() + arc_rect, point5.y() + arc_rect) point3 = QPointF(self.source.x() - arc_rect, point6.y()) point2 = QPointF(self.source.x(), point3.y() + arc_rect) span_angle_arc1 = 90 path.moveTo(point1) path.lineTo(point2) path.arcTo(QRectF(point2, point3), start_angle_arc1, span_angle_arc1) path.lineTo(point6) path.arcTo(QRectF(point6, point5), start_angle_arc2, span_angle_arc2) path.lineTo(point4) painter.drawPath(path)
def drawArrow(self, paint, x1, y1, x2, y2): m = paint.worldMatrix() paint.translate(x1,y1) pi = 3.1415926 if abs(x2 - x1) > 0: alpha = math.atan(abs(y2-y1)/abs(x2-x1)) * 180 / pi else: alpha = 90 if y2 > y1: if x2 > x1: paint.rotate(alpha) else: paint.rotate(180-alpha) else: if x2 > x1: paint.rotate(-alpha) else: paint.rotate(alpha-180) endcoord = math.sqrt((x2-x1)**2 + (y2-y1)**2) p1 = QPointF(endcoord , 0) paint.drawLine(0, 0, p1.x(), 0) paint.setWorldMatrix(m)
def paint(self, painter, rect, widget_size, draw_highlight): GraphXYView.paint(self, painter, rect, widget_size, draw_highlight) smooth = 2.5 # this is invert smoothing, increase it to sharpen graph if not self.model(): return option = self.viewOptions() background = option.palette.base() foreground = QPen(option.palette.color(QPalette.Foreground)) # Handle step by step size increment step_width = floor(widget_size.width() / RESIZE_INCREMENT) * RESIZE_INCREMENT step_height = floor(widget_size.height() / RESIZE_INCREMENT) * RESIZE_INCREMENT if self.model().rowCount(self.rootIndex()) != 0: value_max = self.getValueMaxAll() else: value_max = 0 # If there is no data yet, or the max is at 0, use a max of 10 to dispaly an axe if self.fetcher.fragment.type == 'LoadStream': if value_max < 100: value_max = 100 else: if value_max == 0: value_max = 10 if len(self.data) != 0: xmin = self.getValueMin(0) xmax = self.getValueMax(0) else: return if xmin == xmax: return # avoid division by 0 painter.save() # Offset to cerrectly center the graph after the resizing due to the step by step increment painter.translate((widget_size.width() - step_width) / 2.0, (widget_size.height() - step_height) / 2.0) painter.setPen(foreground) # Draw lines fm = QFontMetrics(painter.font()) margin_size = fm.width("0") if self.fetcher.fragment.type == 'TrafficStream': x_axe_off = fm.width(unicode(value_max * 2000)) else: x_axe_off = fm.width(unicode(value_max * 200)) # Order them by max mean value means = {} for col in xrange(1, len(self.data[0])): means[col] = self.getMeanValue(col) def mean_sorter(x, y): return cmp(y[1], x[1]) means_sorted = means.items() means_sorted.sort(mean_sorter) def slope_by_index(points, index, sens): if index >= len(points): return 0 if index == 0: return points[index].y() + (points[index+1].y()-points[index].y())/smooth if index == (len(points) - 1): return points[index].y() - (points[index].y()-points[index-1].y())/smooth vpoints = [ points[index-1].y(), points[index].y(), points[index+1].y()] if points[index].y() == max(vpoints): return points[index].y() if points[index].y() == min(vpoints): return points[index].y() if sens == 0: return points[index].y() + (points[index+1].y()-points[index].y())/smooth #return points[index].y() + (points[index+1].y() - points[index-1].y()) / (points[index+1].x()-points[index-1].x()) * (points[index+1].x() - points[index].x()) / smooth else: return points[index].y() - (points[index].y()-points[index-1].y())/smooth #return points[index].y() + (points[index+1].y() - points[index-1].y()) / (points[index+1].x()-points[index-1].x()) * (points[index-1].x() - points[index].x()) / smooth colors = odict() for _col in means_sorted: col = _col[0] path = QPainterPath() path.setFillRule(Qt.WindingFill) last_point = None height_max = 0.0 width_max = step_width - 2 * margin_size - x_axe_off height_max = (step_height * (1.0 - TITLE_AREA)) - 2 * margin_size points = [] for row in xrange(len(self.data)): index = self.model().index(row, col, self.rootIndex()) value = index.data().toInt()[0] x_value = int(self.data[row][0]) #print "x=", x_value, "y=", value if value >= 0.0: height = height_max * value/value_max x = (float(x_value - xmin) / float(xmax - xmin)) * width_max point = QPointF(x + margin_size + x_axe_off, height_max - height + margin_size) points.append(point) for index, point in enumerate(points): # draw simple point painter.setBrush(QBrush(QColor(self.colours[col]).dark(200))) painter.drawEllipse(QRectF(point.x() - 2, point.y() - 2, 4, 4)) # init drawing if index == 0: path.moveTo(point) continue px = points[index-1].x() + (points[index].x() - points[index-1].x()) / smooth py = slope_by_index(points, index-1, 0) c1 = QPointF(px, py) px = points[index].x() - ( points[index].x() - points[index-1].x() ) / smooth py = slope_by_index(points, index, 1) c2 = QPointF(px, py) path.cubicTo(c1, c2, point) last_point = points[len(points)-1] if last_point: txt = self.model().headerData(col, Qt.Horizontal).toString() txt_width = fm.width(txt) txt_height = fm.height() colors[txt] = QColor(self.colours[col]) path.lineTo(QPointF(x + margin_size + x_axe_off, height_max + margin_size)) path.lineTo(QPointF(margin_size + x_axe_off, height_max + margin_size)) color = QColor(self.colours[col]) color.setAlpha(200) ## Create the gradient effect grad = QLinearGradient(QPointF(0.0, 0.0), QPointF(0.0, height_max)) grad.setColorAt(1.0, color.dark(150)) grad.setColorAt(0.95, color) grad.setColorAt(0.05, color) grad.setColorAt(0.0, Qt.white) painter.setBrush(QBrush(grad)) painter.drawPath(path) # Graduations nbr_grad = xmax - xmin dgrad = 1 while nbr_grad > 10: nbr_grad = floor(nbr_grad / 10) dgrad = dgrad * 10 if nbr_grad <= 2: dgrad = dgrad / 10 nbr_grad = 10 # Prevent for infinite loops. if dgrad < 1: dgrad = 1 dx = (float(dgrad) / float(xmax-xmin)) * width_max text_dy = fm.height() i = 0 while (i * dgrad) <= xmax - xmin: if self.fetcher.fragment.type != 'TrafficStream' or (self.fetcher.fragment.type == 'TrafficStream' and i % 4 == 0): grad_width = fm.width("0") painter.drawLine(margin_size + x_axe_off + (i*dx), height_max + margin_size, margin_size + x_axe_off + (i*dx), height_max + margin_size + grad_width) text = unicode(int(dgrad * i)) # Legend drawing: painter.translate(margin_size + x_axe_off + (i*dx), height_max + margin_size + 2*grad_width) painter.rotate(-45.0) int_time = (i * dgrad) + xmin text = QString('%ds' % (xmax - int_time)) txt_width = fm.width(text) painter.drawText(QRect(-txt_width, -dx, txt_width, 2*dx), Qt.AlignRight|Qt.AlignVCenter, text) painter.rotate(45.0) painter.translate(-(margin_size + x_axe_off + (i*dx)), -(height_max + margin_size + 2*grad_width)) i = i + 1 interval = 0 height = height_max + margin_size + txt_width * sin(45) + 30 for k, v in colors.iteritems(): painter.setPen(v) legendRect = QRect(interval, height, 10, 10) painter.drawRect(legendRect) painter.fillRect(legendRect, QBrush(v)) painter.setPen(foreground) txt_width = fm.width(k) txt_height = fm.height() painter.drawText(QRect(interval + 10, height, txt_width, txt_height), Qt.AlignRight|Qt.AlignVCenter, k) interval += 20 + txt_width painter.translate((step_width - widget_size.width()) / 2.0, (step_height - widget_size.height()) / 2.0) painter.restore()
class BrushingModel(QObject): brushSizeChanged = pyqtSignal(int) brushColorChanged = pyqtSignal(QColor) brushStrokeAvailable = pyqtSignal(QPointF, object) drawnNumberChanged = pyqtSignal(int) minBrushSize = 1 maxBrushSize = 61 defaultBrushSize = 3 defaultDrawnNumber = 1 defaultColor = Qt.white erasingColor = Qt.black erasingNumber = 100 def __init__(self, parent=None): QObject.__init__(self, parent=parent) self.sliceRect = None self.bb = QRect() #bounding box enclosing the drawing self.brushSize = self.defaultBrushSize self.drawColor = self.defaultColor self._temp_color = None self._temp_number = None self.drawnNumber = self.defaultDrawnNumber self.pos = None self.erasing = False self._hasMoved = False self.drawOnto = None #an empty scene, where we add all drawn line segments #a QGraphicsLineItem, and which we can use to then #render to an image self.scene = QGraphicsScene() def toggleErase(self): self.erasing = not (self.erasing) if self.erasing: self.setErasing() else: self.disableErasing() def setErasing(self): self.erasing = True self._temp_color = self.drawColor self._temp_number = self.drawnNumber self.setBrushColor(self.erasingColor) self.brushColorChanged.emit(self.erasingColor) self.setDrawnNumber(self.erasingNumber) def disableErasing(self): self.erasing = False self.setBrushColor(self._temp_color) self.brushColorChanged.emit(self.drawColor) self.setDrawnNumber(self._temp_number) def setBrushSize(self, size): self.brushSize = size self.brushSizeChanged.emit(self.brushSize) def setDrawnNumber(self, num): self.drawnNumber = num self.drawnNumberChanged.emit(num) def getBrushSize(self): return self.brushSize def brushSmaller(self): b = self.brushSize if b > self.minBrushSize: self.setBrushSize(b - 1) def brushBigger(self): b = self.brushSize if self.brushSize < self.maxBrushSize: self.setBrushSize(b + 1) def setBrushColor(self, color): self.drawColor = color self.brushColorChanged.emit(self.drawColor) def beginDrawing(self, pos, sliceRect): ''' pos -- QPointF-like ''' self.sliceRect = sliceRect self.scene.clear() self.bb = QRect() self.pos = QPointF(pos.x(), pos.y()) self._hasMoved = False def endDrawing(self, pos): has_moved = self._hasMoved # _hasMoved will change after calling moveTo if has_moved: self.moveTo(pos) else: assert (self.pos == pos) self.moveTo(QPointF(pos.x() + 0.0001, pos.y() + 0.0001)) # move a little tempi = QImage(QSize(self.bb.width(), self.bb.height()), QImage.Format_ARGB32_Premultiplied) #TODO: format tempi.fill(0) painter = QPainter(tempi) self.scene.render(painter, target=QRectF(), source=QRectF( QPointF(self.bb.x(), self.bb.y()), QSizeF(self.bb.width(), self.bb.height()))) painter.end() ndarr = qimage2ndarray.rgb_view(tempi)[:, :, 0] labels = numpy.where(ndarr > 0, numpy.uint8(self.drawnNumber), numpy.uint8(0)) labels = labels.swapaxes(0, 1) assert labels.shape[0] == self.bb.width() assert labels.shape[1] == self.bb.height() ## ## ensure that at least one pixel is label when the brush size is 1 ## ## this happens when the user just clicked without moving ## in that case the lineitem will be so tiny, that it won't be rendered ## into a single pixel by the code above if not has_moved and self.brushSize <= 1 and numpy.count_nonzero( labels) == 0: labels[labels.shape[0] // 2, labels.shape[1] // 2] = self.drawnNumber self.brushStrokeAvailable.emit(QPointF(self.bb.x(), self.bb.y()), labels) def dumpDraw(self, pos): res = self.endDrawing(pos) self.beginDrawing(pos, self.sliceRect) return res def moveTo(self, pos): #data coordinates oldX, oldY = self.pos.x(), self.pos.y() x, y = pos.x(), pos.y() line = QGraphicsLineItem(oldX, oldY, x, y) line.setPen( QPen(QBrush(Qt.white), self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) self.scene.addItem(line) self._hasMoved = True #update bounding Box if not self.bb.isValid(): self.bb = QRect(QPoint(oldX, oldY), QSize(1, 1)) #grow bounding box self.bb.setLeft(min(self.bb.left(), max(0, x - self.brushSize / 2 - 1))) self.bb.setRight( max(self.bb.right(), min(self.sliceRect[0] - 1, x + self.brushSize / 2 + 1))) self.bb.setTop(min(self.bb.top(), max(0, y - self.brushSize / 2 - 1))) self.bb.setBottom( max(self.bb.bottom(), min(self.sliceRect[1] - 1, y + self.brushSize / 2 + 1))) #update/move position self.pos = pos
def mouseReleaseEvent(self, event): position = QPointF(event.scenePos()) self.clicked.emit((position.x(), position.y()))
class TracepointWaveScene(QtGui.QGraphicsScene): '''QGraphicsView for wave''' def __init__(self, type_, values, duration): '''CTOR @param type_: string, must be in supported types @param values: list of values of variable @param duration: integer, holds stepwidth of wave ''' QtGui.QGraphicsScene.__init__(self) self.supportedTypes = ["bool", "int", "float", "double"] self.vSpace = 10 self.values = [] self.curPos = QPointF(0, 2) self.type = type_ self.width = duration self.valFont = QtGui.QFont("Arial", 7) self.valFontColor = QtGui.QColor() self.valFontColor.setGreen(100) self.setSceneRect(0, 0, self.width, 15) for v in values: self.appendValue(v, duration) def getSupportedTypes(self): ''' Returns supported waveform types @return list[string] ''' return self.supportedTypes def getType(self): ''' @return string identifying type of TracepointWaveScene''' return self.type def appendValue(self, value, duration): ''' Append value to wave @param value: value to add @param duration: integer, defines duration(length) of value in wave ''' drawEdge = len(self.values) > 0 and self.values[len(self.values) - 1] != value if drawEdge: self.__drawEdge() self.values.append(value) self.__drawLine(value, duration, drawEdge or len(self.values) == 1) self.setSceneRect(0, 0, self.width, 15) def __drawEdge(self): ''' Draws an edge depending on the type of the waveform. ''' if self.type == "bool": self.addItem(QtGui.QGraphicsLineItem(QLineF(self.curPos, QPointF(self.curPos.x(), self.curPos.y() + self.vSpace)))) elif self.type in self.supportedTypes: self.addItem(QtGui.QGraphicsLineItem(QLineF(self.curPos, QPointF(self.curPos.x() + 2, self.curPos.y() + self.vSpace)))) self.addItem(QtGui.QGraphicsLineItem(QLineF(QPointF(self.curPos.x() + 2, self.curPos.y()), QPointF(self.curPos.x(), self.curPos.y() + self.vSpace)))) def __drawLine(self, value, duration, printvalue=True): ''' Draws a line depending on the type of the waveform. @param value: value to add to wave @param duration: integer, defines duration(length) of value in wave @param printvalue: bool, add values to waveform (for value-type waveforms only) ''' self.width = self.width + duration tmp = self.curPos self.curPos = QPointF(self.curPos.x() + duration, self.curPos.y()) if self.type == "bool": if value: self.addItem(QtGui.QGraphicsLineItem(QLineF(tmp, self.curPos))) else: self.addItem(QtGui.QGraphicsLineItem(tmp.x(), tmp.y() + self.vSpace, self.curPos.x(), self.curPos.y() + self.vSpace)) elif self.type in self.supportedTypes: if printvalue: text = QtGui.QGraphicsTextItem(str(value)) text.setFont(self.valFont) text.setDefaultTextColor(self.valFontColor) text.setPos(QPointF(tmp.x() + 4, tmp.y() - 5)) self.addItem(text) self.addItem(QtGui.QGraphicsLineItem(QLineF(tmp, self.curPos))) self.addItem(QtGui.QGraphicsLineItem(tmp.x(), tmp.y() + self.vSpace, self.curPos.x(), self.curPos.y() + self.vSpace))
class ImageView2D(QGraphicsView): focusChanged = pyqtSignal() """ Shows a ImageScene2D to the user and allows for interactive scrolling, panning, zooming etc. """ @property def sliceShape(self): """ (width, height) of the scene. Specifying the shape is necessary to allow for correct scrollbars """ return self._sliceShape @sliceShape.setter def sliceShape(self, s): self._sliceShape = s self.scene().dataShape = s self._crossHairCursor.dataShape = s self._sliceIntersectionMarker.dataShape = s @property def hud(self): return self._hud @hud.setter def hud(self, hud): """ Sets up a heads up display at the upper left corner of the view hud -- a QWidget """ self._hud = hud self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self._hud) self.layout().addStretch() scene = self.scene() hud.zoomToFitButtonClicked.connect(self.fitImage) hud.resetZoomButtonClicked.connect(self.doScaleTo) hud.rotLeftButtonClicked.connect(scene._onRotateLeft) hud.rotRightButtonClicked.connect(scene._onRotateRight) hud.swapAxesButtonClicked.connect(scene._onSwapAxes) scene.axesChanged.connect(hud.setAxes) def __init__(self, parent, imagescene2d): """ Constructs a view upon a ImageScene2D imagescene2d -- a ImgeScene2D instance """ QGraphicsView.__init__(self, parent) self.setScene(imagescene2d) self.mousePos = QPointF(0, 0) # FIXME: These int members shadow QWidget.x() and QWidget.y(), which can lead to confusion when debugging... self.x, self.y = (0, 0) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._isRubberBandZoom = False self._cursorBackup = None #these attributes are exposed as public properties above self._sliceShape = None #2D shape of this view's shown image self._slices = None #number of slices that are stacked self._hud = None self._crossHairCursor = None self._sliceIntersectionMarker = None self._ticker = QTimer(self) self._ticker.timeout.connect(self._tickerEvent) # # Setup the Viewport for fast painting # #With these flags turned on we could handle the drawing of the #white background ourselves thus removing the flicker #when scrolling fast through the slices #self.viewport().setAttribute(Qt.WA_OpaquePaintEvent) #self.viewport().setAttribute(Qt.WA_NoSystemBackground) #self.viewport().setAttribute(Qt.WA_PaintOnScreen) #self.viewport().setAutoFillBackground(False) self.setViewportUpdateMode(QGraphicsView.MinimalViewportUpdate) #as rescaling images is slow if done in software, #we use Qt's built-in background caching mode so that the cached #image need only be blitted on the screen when we only move #the cursor self.setCacheMode(QGraphicsView.CacheBackground) self.setRenderHint(QPainter.Antialiasing, False) self._crossHairCursor = CrossHairCursor(self.scene()) self._crossHairCursor.setZValue(99) self._sliceIntersectionMarker = SliceIntersectionMarker(self.scene()) self._sliceIntersectionMarker.setZValue(100) self._sliceIntersectionMarker.setVisibility(True) #FIXME: this should be private, but is currently used from # within the image scene renderer self.tempImageItems = [] self._zoomFactor = 1.0 #for panning self._lastPanPoint = QPoint() self._dragMode = False self._deltaPan = QPointF(0, 0) #FIXME: Is there are more elegant way to handle this? self.setMouseTracking(True) # invisible cursor to enable custom cursor self._hiddenCursor = QCursor(Qt.BlankCursor) # For screen recording BlankCursor doesn't work #self.hiddenCursor = QCursor(Qt.ArrowCursor) def _cleanUp(self): self._ticker.stop() del self._ticker def setZoomFactor(self, zoom): if self._hud is not None: self._hud.zoomLevelIndicator.updateLevel(zoom) self._zoomFactor = zoom def indicateSlicingPositionSettled(self, settled): self.scene().indicateSlicingPositionSettled(settled) def viewportRect(self): """ Return a QRectF giving the part of the scene currently displayed in this widget's viewport in the scene's coordinates """ r = self.mapToScene(self.viewport().geometry()).boundingRect() return r def mapScene2Data(self, pos): return self.scene().scene2data.map(pos) def mapMouseCoordinates2Data(self, pos): return self.mapScene2Data(self.mapToScene(pos)) def _panning(self): hBar = self.horizontalScrollBar() vBar = self.verticalScrollBar() vBar.setValue(vBar.value() - self._deltaPan.y()) if self.isRightToLeft(): hBar.setValue(hBar.value() + self._deltaPan.x()) else: hBar.setValue(hBar.value() - self._deltaPan.x()) def _deaccelerate(self, speed, a=1, maxVal=64): x = self._qBound(-maxVal, speed.x(), maxVal) y = self._qBound(-maxVal, speed.y(), maxVal) ax, ay = self._setdeaccelerateAxAy(speed.x(), speed.y(), a) if x > 0: x = max(0.0, x - a * ax) elif x < 0: x = min(0.0, x + a * ax) if y > 0: y = max(0.0, y - a * ay) elif y < 0: y = min(0.0, y + a * ay) return QPointF(x, y) def _qBound(self, minVal, current, maxVal): """PyQt4 does not wrap the qBound function from Qt's global namespace This is equivalent.""" return max(min(current, maxVal), minVal) def _setdeaccelerateAxAy(self, x, y, a): x = abs(x) y = abs(y) if x > y: if y > 0: ax = int(x / y) if ax != 0: return ax, 1 else: return x / a, 1 if y > x: if x > 0: ay = int(y / x) if ay != 0: return 1, ay else: return 1, y / a return 1, 1 def _tickerEvent(self): if self._deltaPan.x() == 0.0 and self._deltaPan.y( ) == 0.0 or self._dragMode == True: self._ticker.stop() else: self._deltaPan = self._deaccelerate(self._deltaPan) self._panning() def zoomOut(self): self.doScale(0.9) def zoomIn(self): self.doScale(1.1) def fitImage(self): self.fitInView(self.sceneRect(), Qt.KeepAspectRatio) width, height = self.size().width() / self.sceneRect().width( ), self.height() / self.sceneRect().height() self.setZoomFactor(min(width, height)) def centerImage(self): self.centerOn(self.sceneRect().width() / 2 + self.sceneRect().x(), self.sceneRect().height() / 2 + self.sceneRect().y()) def toggleHud(self): if self._hud is not None: self._hud.setVisible(not self._hud.isVisible()) def setHudVisible(self, visible): if self._hud is not None: self._hud.setVisible(visible) def hudVisible(self): return self._hud.isVisible() def focusInEvent(self, event): self.setStyleSheet( ".QFrame {border: 2px solid white; border-radius: 4px;}") self.focusChanged.emit() def focusOutEvent(self, event): self.setStyleSheet(".QFrame {}") def changeViewPort(self, qRectf): self.fitInView(qRectf, mode=Qt.KeepAspectRatio) width, height = self.size().width() / qRectf.width(), self.height( ) / qRectf.height() self.setZoomFactor(min(width, height)) def doScale(self, factor): self.setZoomFactor(self._zoomFactor * factor) self.scale(factor, factor) def doScaleTo(self, zoom=1): factor = (1 / self._zoomFactor) * zoom self.setZoomFactor(zoom) self.scale(factor, factor)
class Particle(QGraphicsItem): def __init__(self, parent=None): super(Particle, self).__init__(parent) self.effect = QGraphicsBlurEffect() self.blur_radius = random.uniform(0.8, 1.6) self.effect.setBlurRadius(self.blur_radius) self.setGraphicsEffect(self.effect) self.height = random.uniform(1, 6) self.width = random.uniform(1, 6) self.depth = 1 self.setZValue(self.depth) self.newPos = QPointF() self.animation_timer = QTimer() self.animation_timer.setInterval(1000 / 25) self.animation_timer.timeout.connect(self.advance) self.animation_timer.start() self.speed = 1 self.next_pos = QPointF(0, 0) self.animated = True self.max_speed = 3.0 self.color = QColor(0, 0, 0, 50) # self.change_position_timer = QTimer() # self.change_position_timer.setInterval(5000) # self.change_position_timer.timeout.connect(self.calculate_next_pos) def animate(self, bool): self.animated = bool def calculate_forces(self): # Sum up all forces pushing this item away. xvel = 0.0 yvel = 0.0 line = QtCore.QLineF(self.next_pos, self.pos()) dx = line.dx() dy = line.dy() l = math.sqrt(math.pow(dx, 2) + math.pow(dy, 2)) # l = 2.0 * (dx * dx + dy * dy) if l > 0: xvel -= (dx * self.speed) / l yvel -= (dy * self.speed) / l if l < 10: self.calculate_next_pos() scene_rect = self.scene().sceneRect() self.newPos = self.pos() + QtCore.QPointF(xvel, yvel) self.newPos.setX( min(max(self.newPos.x(), scene_rect.left() + 10), scene_rect.right() - 10)) self.newPos.setY( min(max(self.newPos.y(), scene_rect.top() + 10), scene_rect.bottom() - 10)) def calculate_next_pos(self): particle_x = random.uniform(-self.scene().sceneRect().width() / 2, self.scene().sceneRect().width() / 2) particle_y = random.uniform(-self.scene().sceneRect().height() / 2, self.scene().sceneRect().height() / 2) self.next_pos = QPointF(particle_x, particle_y) self.blur_radius = random.uniform(0.8, 1.6) self.effect.setBlurRadius(self.blur_radius) self.setGraphicsEffect(self.effect) self.height = random.uniform(1, 6) self.width = random.uniform(1, 6) self.depth = random.uniform(0, 6) self.speed *= random.uniform(0.1, 2.0) self.speed %= self.max_speed # print self.speed def advance(self): if self.animated: self.calculate_forces() if self.newPos == self.pos(): return False self.setPos(self.newPos) return True else: return False def paint(self, painter, options, widget=None): # painter.drawRect(self.boundingRect()) painter.setBrush(self.color) painter.setPen(self.color) painter.drawEllipse(-self.width / 2, -self.height / 2, self.width, self.height) def boundingRect(self): return QRectF(-self.width / 2, -self.height / 2, self.width, self.height) def reduce_speed(self, factor=0.9): if factor < 1 and factor > 0: self.speed *= factor def increase_speed(self, factor=1.1): if factor > 1: self.speed *= factor def set_color(self, color): self.color = color def instant_pos_change(self): particle_x = random.uniform(-self.scene().sceneRect().width() / 2, self.scene().sceneRect().width() / 2) particle_y = random.uniform(-self.scene().sceneRect().height() / 2, self.scene().sceneRect().height() / 2) self.setPos(QPointF(particle_x, particle_y)) self.next_pos = QPointF(particle_x, particle_y)
class EEdge(QGraphicsObject): def __init__(self, head, tail, uuid): QGraphicsObject.__init__(self) if not issubclass(head.__class__, dict) and not isinstance(tail.__class__, dict): raise AttributeError self.setZValue(0.0) self.__kId = uuid self.__head = head self.__tail = tail if head[ENode.kGuiAttributeType].match(EAttribute.kTypeInput): self.__head = tail self.__tail = head self.__head[ENode.kGuiAttributeParent].onMove.connect(self.update) self.__tail[ENode.kGuiAttributeParent].onMove.connect(self.update) self.__headPoint = QPointF(0.0, 0.0) self.__tailPoint = QPointF(0.0, 0.0) self.__pen = QPen(QColor(43, 43, 43), 2, Qt.SolidLine) self.update() @property def Id(self): return self.__kId @property def Head(self): return self.__head @property def Tail(self): return self.__tail def pen(self): return self.__pen def setPen(self, pen): if not isinstance(pen, QPen): raise AttributeError self.__pen = pen def update(self): QGraphicsObject.prepareGeometryChange(self) self.__headPoint = self.mapFromItem(self.__head[ENode.kGuiAttributeParent], self.__head[ENode.kGuiAttributePlug]) self.__tailPoint = self.mapFromItem(self.__tail[ENode.kGuiAttributeParent], self.__tail[ENode.kGuiAttributePlug]) self.__headOffsetLine = QLineF(self.__headPoint, QPointF(self.__headPoint.x() + 15, self.__headPoint.y())) self.__tailOffsetLine = QLineF(self.__tailPoint, QPointF(self.__tailPoint.x() - 15, self.__tailPoint.y())) line = QLineF(self.__headPoint, self.__tailPoint) self.__line = line def boundingRect(self): extra = (self.pen().width() * 64) / 2 return QRectF(self.__line.p1(), QSizeF(self.__line.p2().x() - self.__line.p1().x(), self.__line.p2().y() - self.__line.p1().y())).normalized().adjusted(-extra, -extra, extra, extra) def shape(self): return QGraphicsObject.shape(self) def drawPath(self, startPoint, endPoint): path = QPainterPath() one = (QPointF(endPoint.x(), startPoint.y()) + startPoint) / 2 two = (QPointF(startPoint.x(), endPoint.y()) + endPoint) / 2 path.moveTo(startPoint) angle = math.pi / 2 bLine1 = QLineF() bLine1.setP1(startPoint) if startPoint.x() > endPoint.x(): dist = startPoint.x() - endPoint.x() one = (bLine1.p1() + QPointF(math.sin(angle) * dist, math.cos(angle) * dist)) bLine1.setP1(endPoint) two = (bLine1.p1() + QPointF(math.sin(angle) * dist, math.cos(angle) * dist)) path.cubicTo(one, two, endPoint) return path, QLineF(one, two) def paint(self, painter, option, widget=None): painter.setPen(self.pen()) headCenter = self.mapFromItem(self.__head[ENode.kGuiAttributeParent], self.__head[ENode.kGuiAttributeParent].boundingRect().center()) tailCenter = self.mapFromItem(self.__tail[ENode.kGuiAttributeParent], self.__tail[ENode.kGuiAttributeParent].boundingRect().center()) centerPoint = QLineF(headCenter, tailCenter).pointAt(0.5) centerPoint.setX(self.__headOffsetLine.p2().x()) lineFromHead = QLineF(self.__headOffsetLine.p2(), centerPoint) centerPoint.setX(self.__tailOffsetLine.p2().x()) lineFromTail = QLineF(self.__tailOffsetLine.p2(), centerPoint) painter.drawPath(self.drawPath(self.__headOffsetLine.p1(), self.__tailOffsetLine.p1())[0])
class BrushingModel(QObject): brushSizeChanged = pyqtSignal(int) brushColorChanged = pyqtSignal(QColor) brushStrokeAvailable = pyqtSignal(QPointF, object) drawnNumberChanged = pyqtSignal(int) minBrushSize = 1 maxBrushSize = 61 defaultBrushSize = 3 defaultDrawnNumber = 1 defaultColor = Qt.white erasingColor = Qt.black erasingNumber = 100 def __init__(self): QObject.__init__(self) self.sliceRect = None self.bb = QRect() #bounding box enclosing the drawing self.brushSize = self.defaultBrushSize self.drawColor = self.defaultColor self._temp_color = None self._temp_number = None self.drawnNumber = self.defaultDrawnNumber self.pos = None self.erasing = False self.drawOnto = None #an empty scene, where we add all drawn line segments #a QGraphicsLineItem, and which we can use to then #render to an image self.scene = QGraphicsScene() def toggleErase(self): self.erasing = not(self.erasing) if self.erasing: self.setErasing() else: self.disableErasing() def setErasing(self): self.erasing = True self._temp_color = self.drawColor self._temp_number = self.drawnNumber self.setBrushColor(self.erasingColor) self.brushColorChanged.emit(self.erasingColor) self.setDrawnNumber(self.erasingNumber) def disableErasing(self): self.erasing = False self.setBrushColor(self._temp_color) self.brushColorChanged.emit(self.drawColor) self.setDrawnNumber(self._temp_number) def setBrushSize(self, size): self.brushSize = size self.brushSizeChanged.emit(self.brushSize) def setDrawnNumber(self, num): print "Setting Drawnnumer", num self.drawnNumber = num self.drawnNumberChanged.emit(num) def getBrushSize(self): return self.brushSize def brushSmaller(self): b = self.brushSize if b > self.minBrushSize: self.setBrushSize(b-1) def brushBigger(self): b = self.brushSize if self.brushSize < self.maxBrushSize: self.setBrushSize(b+1) def setBrushColor(self, color): self.drawColor = color self.brushColorChanged.emit(self.drawColor) def beginDrawing(self, pos, sliceRect): self.sliceRect = sliceRect self.scene.clear() self.bb = QRect() self.pos = QPointF(pos.x()+0.0001, pos.y()+0.0001) line = self.moveTo(pos) return line def endDrawing(self, pos): self.moveTo(pos) tempi = QImage(QSize(self.bb.width(), self.bb.height()), QImage.Format_ARGB32_Premultiplied) #TODO: format tempi.fill(0) painter = QPainter(tempi) self.scene.render(painter, target=QRectF(), source=QRectF(QPointF(self.bb.x(), self.bb.y()), QSizeF(self.bb.width(), self.bb.height()))) painter.end() ndarr = qimage2ndarray.rgb_view(tempi)[:,:,0] labels = numpy.where(ndarr>0,numpy.uint8(self.drawnNumber),numpy.uint8(0)) labels = labels.swapaxes(0,1) assert labels.shape[0] == self.bb.width() assert labels.shape[1] == self.bb.height() self.brushStrokeAvailable.emit(QPointF(self.bb.x(), self.bb.y()), labels) def dumpDraw(self, pos): res = self.endDrawing(pos) self.beginDrawing(pos, self.sliceRect) return res def moveTo(self, pos): oldX, oldY = self.pos.x(), self.pos.y() x,y = pos.x(), pos.y() #print "BrushingModel.moveTo(pos=%r)" % (pos) line = QGraphicsLineItem(oldX, oldY, x, y) line.setPen(QPen( QBrush(Qt.white), self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) self.scene.addItem(line) #update bounding Box if not self.bb.isValid(): self.bb = QRect(QPoint(x,y), QSize(1,1)) #grow bounding box self.bb.setLeft( min(self.bb.left(), max(0, x-self.brushSize/2-1) ) ) self.bb.setRight( max(self.bb.right(), min(self.sliceRect[0]-1, x+self.brushSize/2+1) ) ) self.bb.setTop( min(self.bb.top(), max(0, y-self.brushSize/2-1) ) ) self.bb.setBottom(max(self.bb.bottom(), min(self.sliceRect[1]-1, y+self.brushSize/2+1) ) ) #update/move position self.pos = pos
class BrushingModel(QObject): brushSizeChanged = pyqtSignal(int) brushColorChanged = pyqtSignal(QColor) brushStrokeAvailable = pyqtSignal(QPointF, object) drawnNumberChanged = pyqtSignal(int) minBrushSize = 1 maxBrushSize = 61 defaultBrushSize = 3 defaultDrawnNumber = 1 defaultColor = Qt.white erasingColor = Qt.black erasingNumber = 100 def __init__(self, parent=None): QObject.__init__(self, parent=parent) self.sliceRect = None self.bb = QRect() #bounding box enclosing the drawing self.brushSize = self.defaultBrushSize self.drawColor = self.defaultColor self._temp_color = None self._temp_number = None self.drawnNumber = self.defaultDrawnNumber self.pos = None self.erasing = False self._hasMoved = False self.drawOnto = None #an empty scene, where we add all drawn line segments #a QGraphicsLineItem, and which we can use to then #render to an image self.scene = QGraphicsScene() def toggleErase(self): self.erasing = not (self.erasing) if self.erasing: self.setErasing() else: self.disableErasing() def setErasing(self): self.erasing = True self._temp_color = self.drawColor self._temp_number = self.drawnNumber self.setBrushColor(self.erasingColor) self.brushColorChanged.emit(self.erasingColor) self.setDrawnNumber(self.erasingNumber) def disableErasing(self): self.erasing = False self.setBrushColor(self._temp_color) self.brushColorChanged.emit(self.drawColor) self.setDrawnNumber(self._temp_number) def setBrushSize(self, size): self.brushSize = size self.brushSizeChanged.emit(self.brushSize) def setDrawnNumber(self, num): self.drawnNumber = num self.drawnNumberChanged.emit(num) def getBrushSize(self): return self.brushSize def brushSmaller(self): b = self.brushSize if b > self.minBrushSize: self.setBrushSize(b - 1) def brushBigger(self): b = self.brushSize if self.brushSize < self.maxBrushSize: self.setBrushSize(b + 1) def setBrushColor(self, color): self.drawColor = color self.brushColorChanged.emit(self.drawColor) def beginDrawing(self, pos, sliceRect): ''' pos -- QPointF-like ''' self.sliceRect = sliceRect self.scene.clear() self.bb = QRect() self.pos = QPointF(pos.x(), pos.y()) self._hasMoved = False def endDrawing(self, pos): has_moved = self._hasMoved # _hasMoved will change after calling moveTo if has_moved: self.moveTo(pos) else: assert (self.pos == pos) self.moveTo(QPointF(pos.x() + 0.0001, pos.y() + 0.0001)) # move a little # Qt seems to use strange rules for determining which pixels to set when rendering a brush stroke to a QImage. # We seem to get better results if we do the following: # 1) Slightly offset the source window because apparently there is a small shift in the data # 2) Render the scene to an image that is MUCH larger than the scene resolution (4x by 4x) # 3) Downsample each 4x4 patch from the large image back to a single pixel in the final image, # applying some threshold to determine if the final pixel is on or off. tempi = QImage(QSize(4 * self.bb.width(), 4 * self.bb.height()), QImage.Format_ARGB32_Premultiplied) #TODO: format tempi.fill(0) painter = QPainter(tempi) # Offset the source window. At first I thought the right offset was 0.5, because # that would seem to make sure points are rounded to pixel CENTERS, but # experimentation indicates that 0.25 is slightly better for some reason... source_rect = QRectF(QPointF(self.bb.x() + 0.25, self.bb.y() + 0.25), QSizeF(self.bb.width(), self.bb.height())) target_rect = QRectF(QPointF(0, 0), QSizeF(4 * self.bb.width(), 4 * self.bb.height())) self.scene.render(painter, target=target_rect, source=source_rect) painter.end() # Now downsample: convert each 4x4 patch into a single pixel by summing and dividing ndarr = qimage2ndarray.rgb_view(tempi)[:, :, 0].astype(int) ndarr = ndarr.reshape((ndarr.shape[0], ) + (ndarr.shape[1] // 4, ) + (4, )) ndarr = ndarr.sum(axis=-1) ndarr = ndarr.transpose() ndarr = ndarr.reshape((ndarr.shape[0], ) + (ndarr.shape[1] // 4, ) + (4, )) ndarr = ndarr.sum(axis=-1) ndarr = ndarr.transpose() ndarr //= 4 * 4 downsample_threshold = (7. / 16) * 255 labels = numpy.where(ndarr >= downsample_threshold, numpy.uint8(self.drawnNumber), numpy.uint8(0)) labels = labels.swapaxes(0, 1) assert labels.shape[0] == self.bb.width() assert labels.shape[1] == self.bb.height() ## ## ensure that at least one pixel is label when the brush size is 1 ## ## this happens when the user just clicked without moving ## in that case the lineitem will be so tiny, that it won't be rendered ## into a single pixel by the code above if not has_moved and self.brushSize <= 1 and numpy.count_nonzero( labels) == 0: labels[labels.shape[0] // 2, labels.shape[1] // 2] = self.drawnNumber self.brushStrokeAvailable.emit(QPointF(self.bb.x(), self.bb.y()), labels) def dumpDraw(self, pos): res = self.endDrawing(pos) self.beginDrawing(pos, self.sliceRect) return res def moveTo(self, pos): #data coordinates oldX, oldY = self.pos.x(), self.pos.y() x, y = pos.x(), pos.y() line = QGraphicsLineItem(oldX, oldY, x, y) line.setPen( QPen(QBrush(Qt.white), self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) self.scene.addItem(line) self._hasMoved = True #update bounding Box if not self.bb.isValid(): self.bb = QRect(QPoint(oldX, oldY), QSize(1, 1)) #grow bounding box self.bb.setLeft( min(self.bb.left(), max(0, x - self.brushSize // 2 - 1))) self.bb.setRight( max(self.bb.right(), min(self.sliceRect[0] - 1, x + self.brushSize // 2 + 1))) self.bb.setTop(min(self.bb.top(), max(0, y - self.brushSize // 2 - 1))) self.bb.setBottom( max(self.bb.bottom(), min(self.sliceRect[1] - 1, y + self.brushSize // 2 + 1))) #update/move position self.pos = pos
class EEdge(QGraphicsObject): def __init__(self, head, tail, uuid, arrowed=False): QGraphicsObject.__init__(self) self.__arrowed = arrowed if not issubclass(head.__class__, dict) and not isinstance(tail.__class__, dict): raise AttributeError self.setZValue(0.0) self.__kId = uuid self.__head = head self.__tail = tail self.__path = QPainterPath() self.__headPoint = QPointF(0.0, 0.0) self.__tailPoint = QPointF(0.0, 0.0) self.__head[ENode.kGuiAttributeParent].onMove.connect(self.update) self.__tail[ENode.kGuiAttributeParent].onMove.connect(self.update) self.__pen = QPen(QColor(43, 43, 43), 2, Qt.SolidLine) self.update() @property def Id(self): return self.__kId @property def Line(self): return QLineF(self.__headPoint, self.__tailPoint) @property def Head(self): return self.__head @Head.setter def Head(self, newHead): self.__head = newHead @property def Tail(self): return self.__tail @Tail.setter def Tail(self, newTail): self.__tail = newTail def pen(self): return self.__pen def setPen(self, pen): if not isinstance(pen, QPen): raise AttributeError self.__pen = pen def update(self): QGraphicsObject.prepareGeometryChange(self) self.__headPoint = self.mapFromItem(self.__head[ENode.kGuiAttributeParent], self.__head[ENode.kGuiAttributePlug]) self.__tailPoint = self.mapFromItem(self.__tail[ENode.kGuiAttributeParent], self.__tail[ENode.kGuiAttributePlug]) self.__headOffsetLine = QLineF(self.__headPoint, QPointF(self.__headPoint.x() + 15, self.__headPoint.y())) self.__tailOffsetLine = QLineF(self.__tailPoint, QPointF(self.__tailPoint.x() - 15, self.__tailPoint.y())) line = QLineF(self.__headPoint, self.__tailPoint) self.__line = line def boundingRect(self): extra = (self.pen().width() * 64) / 2 return QRectF(self.__line.p1(), QSizeF(self.__line.p2().x() - self.__line.p1().x(), self.__line.p2().y() - self.__line.p1().y())).normalized().adjusted(-extra, -extra, extra, extra) def shape(self): if self.__arrowed: return QGraphicsObject.shape(self) return QPainterPath(self.__path) def getIntersectPoint(self, polygon, point1, point2): p1 = polygon[0] + point1 intersectPoint = QPointF() for i in polygon: p2 = i + point2 polyLine = QLineF(p1, p2) intersectType = polyLine.intersect(QLineF(point1, point2), intersectPoint) if intersectType == QLineF.BoundedIntersection: break p1 = p2 return intersectPoint def getArrow(self, line): Pi = math.pi TwoPi = 2.0 * Pi arrowSize = 14 if line.length() > 0: angle = math.acos(line.dx() / line.length()) if line.dy() >= 0: angle = TwoPi - angle sourceArrowP1 = line.p1() + QPointF(math.sin(angle + Pi / 3) * arrowSize, math.cos(angle + Pi / 3) * arrowSize) sourceArrowP2 = line.p1() + QPointF(math.sin(angle + Pi - Pi / 3) * arrowSize, math.cos(angle + Pi - Pi / 3) * arrowSize) destinationArrowP1 = line.p2() + QPointF(math.sin(angle - Pi / 3) * arrowSize, math.cos(angle - Pi / 3) * arrowSize) destinationArrowP2 = line.p2() + QPointF(math.sin(angle - Pi + Pi / 3) * arrowSize, math.cos(angle - Pi + Pi / 3) * arrowSize) arrows = [QPolygonF([line.p1(), sourceArrowP1, sourceArrowP2]), QPolygonF([line.p2(), destinationArrowP1, destinationArrowP2])] return arrows[0] return QPolygonF() def drawPath(self, startPoint, endPoint): path = QPainterPath() one = (QPointF(endPoint.x(), startPoint.y()) + startPoint) / 2 two = (QPointF(startPoint.x(), endPoint.y()) + endPoint) / 2 path.moveTo(startPoint) if startPoint.x() > endPoint.x(): dist = (startPoint.x() - endPoint.x()) * 2 tLine = QLineF((dist / 2), 0.0, -(dist / 2), 0.0).translated(QLineF(startPoint, endPoint).pointAt(0.5)) one = tLine.p1() two = tLine.p2() path.cubicTo(one, two, endPoint) self.__path = path return path, QLineF(one, two) def paint(self, painter, option, widget=None): painter.setPen(self.pen()) if not self.__arrowed: headCenter = self.mapFromItem(self.__head[ENode.kGuiAttributeParent], self.__head[ENode.kGuiAttributeParent].boundingRect().center()) tailCenter = self.mapFromItem(self.__tail[ENode.kGuiAttributeParent], self.__tail[ENode.kGuiAttributeParent].boundingRect().center()) centerPoint = QLineF(headCenter, tailCenter).pointAt(0.5) centerPoint.setX(self.__headOffsetLine.p2().x()) centerPoint.setX(self.__tailOffsetLine.p2().x()) painter.drawPath(self.drawPath(self.__headOffsetLine.p1(), self.__tailOffsetLine.p1())[0]) else: painter.drawLine(self.__line) painter.setPen(Qt.NoPen) painter.setBrush(QColor(43, 43, 43)) headCutPoint = self.getIntersectPoint(self.__head[ENode.kGuiAttributeParent].Polygon, self.__line.p1(), self.__line.p2()) tailCutPoint = self.getIntersectPoint(self.__tail[ENode.kGuiAttributeParent].Polygon, self.__line.p2(), self.__line.p1()) painter.drawPolygon(self.getArrow(QLineF(headCutPoint, tailCutPoint)))
class TracepointWaveScene(QtGui.QGraphicsScene): '''QGraphicsView for wave''' def __init__(self, type_, values, duration): '''CTOR @param type_: string, must be in supported types @param values: list of values of variable @param duration: integer, holds stepwidth of wave ''' QtGui.QGraphicsScene.__init__(self) self.supportedTypes = ["bool", "int", "float", "double"] self.vSpace = 10 self.values = [] self.curPos = QPointF(0, 2) self.type = type_ self.width = duration self.valFont = QtGui.QFont("Arial", 7) self.valFontColor = QtGui.QColor() self.valFontColor.setGreen(100) self.setSceneRect(0, 0, self.width, 15) for v in values: self.appendValue(v, duration) def getSupportedTypes(self): ''' Returns supported waveform types @return list[string] ''' return self.supportedTypes def getType(self): ''' @return string identifying type of TracepointWaveScene''' return self.type def appendValue(self, value, duration): ''' Append value to wave @param value: value to add @param duration: integer, defines duration(length) of value in wave ''' drawEdge = len( self.values) > 0 and self.values[len(self.values) - 1] != value if drawEdge: self.__drawEdge() self.values.append(value) self.__drawLine(value, duration, drawEdge or len(self.values) == 1) self.setSceneRect(0, 0, self.width, 15) def __drawEdge(self): ''' Draws an edge depending on the type of the waveform. ''' if self.type == "bool": self.addItem( QtGui.QGraphicsLineItem( QLineF( self.curPos, QPointF(self.curPos.x(), self.curPos.y() + self.vSpace)))) elif self.type in self.supportedTypes: self.addItem( QtGui.QGraphicsLineItem( QLineF( self.curPos, QPointF(self.curPos.x() + 2, self.curPos.y() + self.vSpace)))) self.addItem( QtGui.QGraphicsLineItem( QLineF( QPointF(self.curPos.x() + 2, self.curPos.y()), QPointF(self.curPos.x(), self.curPos.y() + self.vSpace)))) def __drawLine(self, value, duration, printvalue=True): ''' Draws a line depending on the type of the waveform. @param value: value to add to wave @param duration: integer, defines duration(length) of value in wave @param printvalue: bool, add values to waveform (for value-type waveforms only) ''' self.width = self.width + duration tmp = self.curPos self.curPos = QPointF(self.curPos.x() + duration, self.curPos.y()) if self.type == "bool": if value: self.addItem(QtGui.QGraphicsLineItem(QLineF(tmp, self.curPos))) else: self.addItem( QtGui.QGraphicsLineItem(tmp.x(), tmp.y() + self.vSpace, self.curPos.x(), self.curPos.y() + self.vSpace)) elif self.type in self.supportedTypes: if printvalue: text = QtGui.QGraphicsTextItem(str(value)) text.setFont(self.valFont) text.setDefaultTextColor(self.valFontColor) text.setPos(QPointF(tmp.x() + 4, tmp.y() - 5)) self.addItem(text) self.addItem(QtGui.QGraphicsLineItem(QLineF(tmp, self.curPos))) self.addItem( QtGui.QGraphicsLineItem(tmp.x(), tmp.y() + self.vSpace, self.curPos.x(), self.curPos.y() + self.vSpace))
def drawCorner(self, painter, position, cornerType, maxRadius=None): #logging.debug(self.__class__.__name__ +": drawCorner() "+ self.cornerTypeString(cornerType)) thickness = self.CONNECTION_THICKNESS * self.zoomFactor() halfthick = thickness / 2 cornerRoundness = halfthick ** 0.5 cornerOffset = halfthick * (cornerRoundness) innerCorner = halfthick * (cornerRoundness - 1) outerCorner = halfthick * (cornerRoundness + 1) innerWidth = halfthick * (cornerRoundness - 1) radius = halfthick * (cornerRoundness + 1) if maxRadius: maxRadius = max(maxRadius, thickness) radius = min(radius, maxRadius) if cornerType == self.CornerType.TOP_RIGHT: startAngle = 0 outerCorner = QPointF(position.x() + halfthick - 2 * radius, position.y() - halfthick) innerCorner = QPointF(outerCorner.x(), outerCorner.y() + (thickness)) center = QPointF(outerCorner.x() + radius, outerCorner.y() + radius) outerRect = QRectF(outerCorner, QSizeF(2 * radius, 2 * radius)) innerRect = QRectF(innerCorner, QSizeF((2 * radius - thickness), (2 * radius - thickness))) outerStart = QPointF(outerCorner.x() + 2 * radius, outerCorner.y() + (radius + halfthick)) innerStart = QPointF(outerCorner.x() + (radius - halfthick), outerCorner.y()) elif cornerType == self.CornerType.TOP_LEFT: startAngle = 90 outerCorner = QPointF(position.x() - halfthick, position.y() - halfthick) innerCorner = QPointF(outerCorner.x() + (thickness), outerCorner.y() + (thickness)) center = QPointF(outerCorner.x() + radius, outerCorner.y() + radius) outerRect = QRectF(outerCorner, QSizeF(2 * radius, 2 * radius)) innerRect = QRectF(innerCorner, QSizeF((2 * radius - thickness), (2 * radius - thickness))) outerStart = QPointF(outerCorner.x() + (radius + halfthick), outerCorner.y()) innerStart = QPointF(outerCorner.x(), outerCorner.y() + (radius + halfthick)) elif cornerType == self.CornerType.BOTTOM_LEFT: startAngle = 180 outerCorner = QPointF(position.x() - halfthick, position.y() + halfthick - 2 * radius) innerCorner = QPointF(outerCorner.x() + (thickness), outerCorner.y()) center = QPointF(outerCorner.x() + radius, outerCorner.y() + radius) outerRect = QRectF(outerCorner, QSizeF(2 * radius, 2 * radius)) innerRect = QRectF(innerCorner, QSizeF((2 * radius - thickness), (2 * radius - thickness))) outerStart = QPointF(outerCorner.x(), outerCorner.y() + (radius - halfthick)) innerStart = QPointF(outerCorner.x() + (radius + halfthick), outerCorner.y() + (2 * radius)) elif cornerType == self.CornerType.BOTTOM_RIGHT: startAngle = 270 outerCorner = QPointF(position.x() + halfthick - 2 * radius, position.y() + halfthick - 2 * radius) innerCorner = QPointF(outerCorner.x(), outerCorner.y()) center = QPointF(outerCorner.x() + radius, outerCorner.y() + radius) outerRect = QRectF(outerCorner, QSizeF(2 * radius, 2 * radius)) innerRect = QRectF(innerCorner, QSizeF((2 * radius - thickness), (2 * radius - thickness))) outerStart = QPointF(outerCorner.x() + (radius - halfthick), outerCorner.y() + 2 * radius) innerStart = QPointF(outerCorner.x() + 2 * radius, outerCorner.y() + (radius - halfthick)) else: # No defined corner, so nothing to draw. #print "PointToPointConnection.drawCorner() - No valid corner, aborting..." return if painter.redirected(painter.device()): # e.q. QPixmap.grabWidget() painter.setBrush(self.FILL_COLOR1) else: brush = QRadialGradient(center, radius) if radius >= thickness: brush.setColorAt((radius - thickness) / radius, self.FILL_COLOR1) # inner border brush.setColorAt((radius - halfthick + 1) / radius, self.FILL_COLOR2) # center of line else: # If zoom is too small use single color brush.setColorAt(0, self.FILL_COLOR1) brush.setColorAt(1, self.FILL_COLOR1) # outer border painter.setBrush(brush) path = QPainterPath() path.moveTo(outerStart) path.arcTo(outerRect, startAngle, 90) path.lineTo(innerStart) path.arcTo(innerRect, startAngle + 90, - 90) path.closeSubpath() #painter.setPen(Qt.NoPen) painter.drawPath(path)
def getXover(self, phg, strandtype, fromHelix,\ fromIndex, toHelix, toIndex): """ Draws a line from the center of the fromBase (pA) to the top or bottom of that same base, depending on its direction (qA), then a quad curve to the top or bottom of the toBase (qB), and finally to the center of the toBase (pB). """ # if we need to speed this up, we could keep track if pA changed? pA = QPointF(*fromHelix.baseLocation(strandtype,\ fromIndex,\ center=True)) pA = phg.mapFromItem(fromHelix, pA) pB = QPointF(*toHelix.baseLocation(strandtype,\ toIndex,\ center=True)) pB = phg.mapFromItem(toHelix, pB) yA = yB = self._baseWidth / 2 if fromHelix.vhelix().directionOfStrandIs5to3(strandtype): orientA = HandleOrient.LeftUp yA = -yA else: orientA = HandleOrient.RightDown if toHelix.vhelix().directionOfStrandIs5to3(strandtype): orientB = HandleOrient.RightUp yB = -yB else: orientB = HandleOrient.LeftDown # Determine start and end points of quad curve qA = QPointF(pA.x(), pA.y() + yA) qB = QPointF(pB.x(), pB.y() + yB) # Determine control point of quad curve c1 = QPointF() # case 1: same strand if fromHelix.number() == toHelix.number(): if pA.x() < pB.x(): # draw only from left if orientA == HandleOrient.LeftUp or \ orientA == HandleOrient.RightUp: dx = abs(pB.x() - pA.x()) c1.setX(0.5 * (pA.x() + pB.x())) c1.setY(pA.y() - self.yScale * dx) # end if # end if # case 2: same parity elif fromHelix.evenParity() == toHelix.evenParity(): dy = abs(pB.y() - pA.y()) c1.setX(pA.x() + self.xScale * dy) c1.setY(0.5 * (pA.y() + pB.y())) # case 3: default else: if orientA == HandleOrient.LeftUp: c1.setX(pA.x() - self.xScale * abs(pB.y() - pA.y())) else: c1.setX(pA.x() + self.xScale * abs(pB.y() - pA.y())) c1.setY(0.5 * (pA.y() + pB.y())) # Construct painter path painterpath = QPainterPath() painterpath.moveTo(pA) painterpath.lineTo(qA) painterpath.quadTo(c1, qB) painterpath.lineTo(pB) return painterpath
class TreeNode(object): """store data for a class and manage hierarchy""" def __init__(self, classid,name,rect=None,color=None,pos=None,accu = 1.0,parent=None): super(TreeNode, self).__init__() self.classid = classid self.name = name self.parent = parent self.accu = accu self.rect = QRectF(*rect) if rect else rect self.color = QColor(color) if color else color self.pos = QPointF(*pos) if pos else pos self.children = [] if parent: parent.children.append(self) # def __init__(self,jsonobj): # self.__init__(jsonobj['id'],jsonobj['name'],jsonobj['rect'],jsonobj['color'],jsonobj['pos']) # for child in jsonobj['children']: # childItem = TreeNode(child) # self.children.append(childItem) # childItem.parent = self def isLeaf(self): if self.children: return False else: return True def isRoot(self): if self.parent: return False else: return True def toJSON(self): jsonObj = {} jsonObj['id']= self.classid jsonObj['name'] = self.name jsonObj['rect'] = [self.rect.left(),self.rect.top(),self.rect.width(),self.rect.height()] if self.rect else self.rect jsonObj['color'] = str(self.color.name()) if self.color else self.color jsonObj['pos'] = [self.pos.x(),self.pos.y()] if isinstance(self.pos,QPointF) else self.pos jsonObj['accu'] = self.accu jsonObj['children'] = [] for child in self.children: jsonObj['children'].append(child.toJSON()) return jsonObj def __str__(self): tempstr = "%s %s\n" % (self.classid,self.name) for child in self.children: tempstr+= ' '+str(child) return tempstr def findNode(self,id): if self.classid == id: return self for child in self.children: result = child.findNode(id) if result: return result return None def toplogicalDistance(self,id1,id2): if id1==id2: return 0 else: node1 = self.findNode(id1) node2 = self.findNode(id2) if node1 and node2: if node1.parent == node2.parent: return 1 else: return 2 return 0 def matrixDistance(self,id1,id2): if id1==id2: return 0 else: node1 = self.findNode(id1) node2 = self.findNode(id2) if node1 and node2: pos1 = node1.pos + node1.parent.pos pos2 = node2.pos + node2.parent.pos return QLineF(pos1,pos2).length() return 2000.0 def transactionPossibility(self,id1,id2): node1 = self.findNode(id1) node2 = self.findNode(id2) if node1 and node2: return node1.accu * node2.accu else: return 1.0
def save(self, filename): growth_num = Result.growth_num fdata = StringIO() invert_pts, invert_cells = self.data.save(f=fdata) f = open(filename, 'w') w = csv.writer(f, delimiter=',') w.writerow(["TRKR_VERSION", Result.CURRENT_VERSION]) w.writerow(["Growth computation parameters"]) hf = self.header_fields for h in self.header_order: w.writerow([h] + hf[h][0](self)) w.writerow([]) w.writerow(["Growth per image"]) w.writerow(Result.fields) wall_shift = self.fields_num["wall"]-1 for img_id in range(len(self.images)): img = self.images[img_id] w.writerow([img]) cells = self.cells[img_id] cells_area = self.cells_area[img_id] walls = self.walls[img_id] data = self.data[img] cells_shape = data.cells rows = [] for c in sorted(cells.keys()): # Get the center of mass of the cell cell = [data[p] for p in cells_shape[c] if p in data] center = QPointF(0, 0) area = 0.0 u1 = cell[-1] for i in range(len(cell)): u2 = cell[i] loc_area = u1.x() * u2.y() - u1.y() * u2.x() center += (u1 + u2)*loc_area area += loc_area u1 = u2 center /= area row = ["", "Cell %d" % invert_cells[c], cells_area[c], cells[c][growth_num["kmax"]], cells[c][growth_num["kmin"]], cells[c][growth_num["theta"]] * 180 / pi, cells[c][growth_num["phi"]], center.x(), center.y()] rows.append(row) lr = len(rows) for i, ws in enumerate(sorted(walls.keys())): wll = ["", "Wall %d-%d" % (invert_pts[ws[0]], invert_pts[ws[1]]), walls[ws]] if i >= lr: rows.append([""] * wall_shift) rows[i] += wll w.writerows(rows) w.writerow(["Actual cell shapes"]) w.writerow(["Image", "Cell", "Begin/End", "Shape [x y]"]) for img_id in range(len(self.images)): img = self.images[img_id] w.writerow([img]) cells_shapes = self.cells_shapes[img_id] rows = [] for c in sorted(cells_shapes.keys()): sh = cells_shapes[c] row1 = ["", "Cell %d" % invert_cells[c], "Begin"] + list(sh[0].flatten()) row2 = ["", "Cell %d" % invert_cells[c], "End"] + list(sh[1].flatten()) rows.append(row1) rows.append(row2) w.writerows(rows) w.writerow([]) w.writerow(["Data"]) f.write(fdata.getvalue()) f.close() fdata.close()
class BrushingModel(QObject): brushSizeChanged = pyqtSignal(int) brushColorChanged = pyqtSignal(QColor) brushStrokeAvailable = pyqtSignal(QPointF, object) drawnNumberChanged = pyqtSignal(int) minBrushSize = 1 maxBrushSize = 61 defaultBrushSize = 3 defaultDrawnNumber = 1 defaultColor = Qt.white erasingColor = Qt.black erasingNumber = 100 def __init__(self, parent=None): QObject.__init__(self, parent=parent) self.sliceRect = None self.bb = QRect() #bounding box enclosing the drawing self.brushSize = self.defaultBrushSize self.drawColor = self.defaultColor self._temp_color = None self._temp_number = None self.drawnNumber = self.defaultDrawnNumber self.pos = None self.erasing = False self._hasMoved = False self.drawOnto = None #an empty scene, where we add all drawn line segments #a QGraphicsLineItem, and which we can use to then #render to an image self.scene = QGraphicsScene() def toggleErase(self): self.erasing = not(self.erasing) if self.erasing: self.setErasing() else: self.disableErasing() def setErasing(self): self.erasing = True self._temp_color = self.drawColor self._temp_number = self.drawnNumber self.setBrushColor(self.erasingColor) self.brushColorChanged.emit(self.erasingColor) self.setDrawnNumber(self.erasingNumber) def disableErasing(self): self.erasing = False self.setBrushColor(self._temp_color) self.brushColorChanged.emit(self.drawColor) self.setDrawnNumber(self._temp_number) def setBrushSize(self, size): self.brushSize = size self.brushSizeChanged.emit(self.brushSize) def setDrawnNumber(self, num): self.drawnNumber = num self.drawnNumberChanged.emit(num) def getBrushSize(self): return self.brushSize def brushSmaller(self): b = self.brushSize if b > self.minBrushSize: self.setBrushSize(b-1) def brushBigger(self): b = self.brushSize if self.brushSize < self.maxBrushSize: self.setBrushSize(b+1) def setBrushColor(self, color): self.drawColor = color self.brushColorChanged.emit(self.drawColor) def beginDrawing(self, pos, sliceRect): ''' pos -- QPointF-like ''' self.sliceRect = sliceRect self.scene.clear() self.bb = QRect() self.pos = QPointF(pos.x(), pos.y()) self._hasMoved = False def endDrawing(self, pos): has_moved = self._hasMoved # _hasMoved will change after calling moveTo if has_moved: self.moveTo(pos) else: assert(self.pos == pos) self.moveTo(QPointF(pos.x()+0.0001, pos.y()+0.0001)) # move a little # Qt seems to use strange rules for determining which pixels to set when rendering a brush stroke to a QImage. # We seem to get better results if we do the following: # 1) Slightly offset the source window because apparently there is a small shift in the data # 2) Render the scene to an image that is MUCH larger than the scene resolution (4x by 4x) # 3) Downsample each 4x4 patch from the large image back to a single pixel in the final image, # applying some threshold to determine if the final pixel is on or off. tempi = QImage(QSize(4*self.bb.width(), 4*self.bb.height()), QImage.Format_ARGB32_Premultiplied) #TODO: format tempi.fill(0) painter = QPainter(tempi) # Offset the source window. At first I thought the right offset was 0.5, because # that would seem to make sure points are rounded to pixel CENTERS, but # experimentation indicates that 0.25 is slightly better for some reason... source_rect = QRectF( QPointF(self.bb.x()+0.25, self.bb.y()+0.25), QSizeF(self.bb.width(), self.bb.height()) ) target_rect = QRectF( QPointF(0,0), QSizeF(4*self.bb.width(), 4*self.bb.height()) ) self.scene.render(painter, target=target_rect, source=source_rect) painter.end() # Now downsample: convert each 4x4 patch into a single pixel by summing and dividing ndarr = qimage2ndarray.rgb_view(tempi)[:,:,0].astype(int) ndarr = ndarr.reshape( (ndarr.shape[0],) + (ndarr.shape[1]//4,) + (4,) ) ndarr = ndarr.sum(axis=-1) ndarr = ndarr.transpose() ndarr = ndarr.reshape( (ndarr.shape[0],) + (ndarr.shape[1]//4,) + (4,) ) ndarr = ndarr.sum(axis=-1) ndarr = ndarr.transpose() ndarr //= 4*4 downsample_threshold = (7./16)*255 labels = numpy.where(ndarr>=downsample_threshold, numpy.uint8(self.drawnNumber), numpy.uint8(0)) labels = labels.swapaxes(0,1) assert labels.shape[0] == self.bb.width() assert labels.shape[1] == self.bb.height() ## ## ensure that at least one pixel is label when the brush size is 1 ## ## this happens when the user just clicked without moving ## in that case the lineitem will be so tiny, that it won't be rendered ## into a single pixel by the code above if not has_moved and self.brushSize <= 1 and numpy.count_nonzero(labels) == 0: labels[labels.shape[0]//2, labels.shape[1]//2] = self.drawnNumber self.brushStrokeAvailable.emit(QPointF(self.bb.x(), self.bb.y()), labels) def dumpDraw(self, pos): res = self.endDrawing(pos) self.beginDrawing(pos, self.sliceRect) return res def moveTo(self, pos): #data coordinates oldX, oldY = self.pos.x(), self.pos.y() x,y = pos.x(), pos.y() line = QGraphicsLineItem(oldX, oldY, x, y) line.setPen(QPen( QBrush(Qt.white), self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) self.scene.addItem(line) self._hasMoved = True #update bounding Box if not self.bb.isValid(): self.bb = QRect(QPoint(oldX,oldY), QSize(1,1)) #grow bounding box self.bb.setLeft( min(self.bb.left(), max(0, x-self.brushSize//2-1) ) ) self.bb.setRight( max(self.bb.right(), min(self.sliceRect[0]-1, x+self.brushSize//2+1) ) ) self.bb.setTop( min(self.bb.top(), max(0, y-self.brushSize//2-1) ) ) self.bb.setBottom(max(self.bb.bottom(), min(self.sliceRect[1]-1, y+self.brushSize//2+1) ) ) #update/move position self.pos = pos
def paint(self, painter, _1, _2): """ Main-Method of the Pointer-Class <br> calculates/renders/draws the Lines of the Arrow """ if self.fromView.collidesWithItem(self.toView): return # antialiasing makes things look nicer :) painter.setRenderHint(QPainter.Antialiasing) self.toView.x() pM1 = QPointF(self.fromView.x() + self.fromView.size().width() / 2, self.fromView.y() + self.fromView.size().height() / 2) pM2 = QPointF(self.toView.x() + self.toView.size().width() / 2, self.toView.y() + self.toView.size().height() / 2) deltaX = pM2.x() - pM1.x() deltaY = pM2.y() - pM1.y() if deltaX == 0: deltaX = 0.01 if deltaY == 0: deltaY = 0.01 if deltaX >= 0: if deltaY >= 0: # rechts unten if deltaX / deltaY >= self.fromView.size().width( ) / self.fromView.size().height(): # Start von rechter Seite pStart = QPointF( pM1.x() + self.fromView.size().width() / 2, pM1.y() + (self.fromView.size().width() / 2) * (deltaY / deltaX)) else: # Start von unterer Seite pStart = QPointF( pM1.x() + (self.fromView.size().height() / 2) * (deltaX / deltaY), pM1.y() + self.fromView.size().height() / 2) if deltaX / deltaY >= self.toView.size().width( ) / self.toView.size().height(): # Ende bei linker Seite pEnd = QPointF( pM2.x() - self.toView.size().width() / 2, pM2.y() - (self.toView.size().width() / 2) * (deltaY / deltaX)) else: # Ende bei oberer Seite pEnd = QPointF( pM2.x() - (self.toView.size().height() / 2) * (deltaX / deltaY), pM2.y() - self.toView.size().height() / 2) else: # rechts oben if deltaX / deltaY * -1 >= self.fromView.size().width( ) / self.fromView.size().height(): # Start von rechter Seite pStart = QPointF( pM1.x() + self.fromView.size().width() / 2, pM1.y() + (self.fromView.size().width() / 2) * (deltaY / deltaX)) else: # Start von oberer Seite pStart = QPointF( pM1.x() - (self.fromView.size().height() / 2) * (deltaX / deltaY), pM1.y() - self.fromView.size().height() / 2) if deltaX / deltaY * -1 >= self.toView.size().width( ) / self.toView.size().height(): # Ende bei linker Seite pEnd = QPointF( pM2.x() - self.toView.size().width() / 2, pM2.y() - (self.toView.size().width() / 2) * (deltaY / deltaX)) else: # Ende bei unterer Seite pEnd = QPointF( pM2.x() + (self.toView.size().height() / 2) * (deltaX / deltaY), pM2.y() + self.toView.size().height() / 2) else: if deltaY >= 0: # links unten if deltaX / deltaY * -1 >= self.fromView.size().width( ) / self.fromView.size().height(): # Start von linker Seite pStart = QPointF( pM1.x() - self.fromView.size().width() / 2, pM1.y() - (self.fromView.size().width() / 2) * (deltaY / deltaX)) else: # Start von unterer Seite pStart = QPointF( pM1.x() + (self.fromView.size().height() / 2) * (deltaX / deltaY), pM1.y() + self.fromView.size().height() / 2) if deltaX / deltaY * -1 >= self.toView.size().width( ) / self.toView.size().height(): # Ende bei rechten Seite pEnd = QPointF( pM2.x() + self.toView.size().width() / 2, pM2.y() + (self.toView.size().width() / 2) * (deltaY / deltaX)) else: # Ende bei oberer Seite pEnd = QPointF( pM2.x() - (self.toView.size().height() / 2) * (deltaX / deltaY), pM2.y() - self.toView.size().height() / 2) else: # links oben if deltaX / deltaY >= self.fromView.size().width( ) / self.fromView.size().height(): # Start von linker Seite pStart = QPointF( pM1.x() - self.fromView.size().width() / 2, pM1.y() - (self.fromView.size().width() / 2) * (deltaY / deltaX)) else: # Start von oberer Seite pStart = QPointF( pM1.x() - (self.fromView.size().height() / 2) * (deltaX / deltaY), pM1.y() - self.fromView.size().height() / 2) if deltaX / deltaY >= self.toView.size().width( ) / self.toView.size().height(): # Ende bei rechter Seite pEnd = QPointF( pM2.x() + self.toView.size().width() / 2, pM2.y() + (self.toView.size().width() / 2) * (deltaY / deltaX)) else: # Ende bei unterer Seite pEnd = QPointF( pM2.x() + (self.toView.size().height() / 2) * (deltaX / deltaY), pM2.y() + self.toView.size().height() / 2) self.setLine(QLineF(pEnd, pStart)) if self.line().length() != 0: angle = math.acos(self.line().dx() / self.line().length()) if self.line().dy() >= 0: angle = math.pi * 2 - angle arrowP1 = self.line().p1() + QPointF( math.sin(angle + math.pi / 2.5) * self.arrowSize, math.cos(angle + math.pi / 2.5) * self.arrowSize) arrowP2 = self.line().p1() + QPointF( math.sin(angle + math.pi - math.pi / 2.5) * self.arrowSize, math.cos(angle + math.pi - math.pi / 2.5) * self.arrowSize) self.arrowhead.clear() self.arrowhead.append(self.line().p1()) self.arrowhead.append(arrowP1) self.arrowhead.append(arrowP2) painter.setBrush(QBrush(self.bgcolor)) painter.drawLine(self.line()) painter.drawPolygon(self.arrowhead)
def drawCorner(self, painter, position, cornerType, maxRadius=None): #logging.debug(self.__class__.__name__ +": drawCorner() "+ self.cornerTypeString(cornerType)) thickness = self.CONNECTION_THICKNESS * self.zoomFactor() halfthick = thickness / 2 cornerRoundness = halfthick**0.5 cornerOffset = halfthick * (cornerRoundness) innerCorner = halfthick * (cornerRoundness - 1) outerCorner = halfthick * (cornerRoundness + 1) innerWidth = halfthick * (cornerRoundness - 1) radius = halfthick * (cornerRoundness + 1) if maxRadius: maxRadius = max(maxRadius, thickness) radius = min(radius, maxRadius) if cornerType == self.CornerType.TOP_RIGHT: startAngle = 0 outerCorner = QPointF(position.x() + halfthick - 2 * radius, position.y() - halfthick) innerCorner = QPointF(outerCorner.x(), outerCorner.y() + (thickness)) center = QPointF(outerCorner.x() + radius, outerCorner.y() + radius) outerRect = QRectF(outerCorner, QSizeF(2 * radius, 2 * radius)) innerRect = QRectF( innerCorner, QSizeF((2 * radius - thickness), (2 * radius - thickness))) outerStart = QPointF(outerCorner.x() + 2 * radius, outerCorner.y() + (radius + halfthick)) innerStart = QPointF(outerCorner.x() + (radius - halfthick), outerCorner.y()) elif cornerType == self.CornerType.TOP_LEFT: startAngle = 90 outerCorner = QPointF(position.x() - halfthick, position.y() - halfthick) innerCorner = QPointF(outerCorner.x() + (thickness), outerCorner.y() + (thickness)) center = QPointF(outerCorner.x() + radius, outerCorner.y() + radius) outerRect = QRectF(outerCorner, QSizeF(2 * radius, 2 * radius)) innerRect = QRectF( innerCorner, QSizeF((2 * radius - thickness), (2 * radius - thickness))) outerStart = QPointF(outerCorner.x() + (radius + halfthick), outerCorner.y()) innerStart = QPointF(outerCorner.x(), outerCorner.y() + (radius + halfthick)) elif cornerType == self.CornerType.BOTTOM_LEFT: startAngle = 180 outerCorner = QPointF(position.x() - halfthick, position.y() + halfthick - 2 * radius) innerCorner = QPointF(outerCorner.x() + (thickness), outerCorner.y()) center = QPointF(outerCorner.x() + radius, outerCorner.y() + radius) outerRect = QRectF(outerCorner, QSizeF(2 * radius, 2 * radius)) innerRect = QRectF( innerCorner, QSizeF((2 * radius - thickness), (2 * radius - thickness))) outerStart = QPointF(outerCorner.x(), outerCorner.y() + (radius - halfthick)) innerStart = QPointF(outerCorner.x() + (radius + halfthick), outerCorner.y() + (2 * radius)) elif cornerType == self.CornerType.BOTTOM_RIGHT: startAngle = 270 outerCorner = QPointF(position.x() + halfthick - 2 * radius, position.y() + halfthick - 2 * radius) innerCorner = QPointF(outerCorner.x(), outerCorner.y()) center = QPointF(outerCorner.x() + radius, outerCorner.y() + radius) outerRect = QRectF(outerCorner, QSizeF(2 * radius, 2 * radius)) innerRect = QRectF( innerCorner, QSizeF((2 * radius - thickness), (2 * radius - thickness))) outerStart = QPointF(outerCorner.x() + (radius - halfthick), outerCorner.y() + 2 * radius) innerStart = QPointF(outerCorner.x() + 2 * radius, outerCorner.y() + (radius - halfthick)) else: # No defined corner, so nothing to draw. #print "PointToPointConnection.drawCorner() - No valid corner, aborting..." return if painter.redirected(painter.device()): # e.q. QPixmap.grabWidget() painter.setBrush(self.FILL_COLOR1) else: brush = QRadialGradient(center, radius) if radius >= thickness: brush.setColorAt((radius - thickness) / radius, self.FILL_COLOR1) # inner border brush.setColorAt((radius - halfthick + 1) / radius, self.FILL_COLOR2) # center of line else: # If zoom is too small use single color brush.setColorAt(0, self.FILL_COLOR1) brush.setColorAt(1, self.FILL_COLOR1) # outer border painter.setBrush(brush) path = QPainterPath() path.moveTo(outerStart) path.arcTo(outerRect, startAngle, 90) path.lineTo(innerStart) path.arcTo(innerRect, startAngle + 90, -90) path.closeSubpath() #painter.setPen(Qt.NoPen) painter.drawPath(path)
def paint(self, painter, _1, _2): """ Main-Method of the Pointer-Class <br> calculates/renders/draws the Lines of the Arrow """ if self.fromView.collidesWithItem(self.toView): return # antialiasing makes things look nicer :) painter.setRenderHint(QPainter.Antialiasing) self.toView.x() pM1 = QPointF(self.fromView.x() + self.fromView.size().width() / 2, self.fromView.y() + self.fromView.size().height() / 2) pM2 = QPointF(self.toView.x() + self.toView.size().width() / 2, self.toView.y() + self.toView.size().height() / 2) deltaX = pM2.x() - pM1.x() deltaY = pM2.y() - pM1.y() if deltaX == 0: deltaX = 0.01 if deltaY == 0: deltaY = 0.01 if deltaX >= 0: if deltaY >= 0: # rechts unten if deltaX / deltaY >= self.fromView.size().width() / self.fromView.size().height(): # Start von rechter Seite pStart = QPointF(pM1.x() + self.fromView.size().width() / 2, pM1.y() + (self.fromView.size().width() / 2) * (deltaY / deltaX)) else: # Start von unterer Seite pStart = QPointF(pM1.x() + (self.fromView.size().height() / 2) * (deltaX / deltaY), pM1.y() + self.fromView.size().height() / 2) if deltaX / deltaY >= self.toView.size().width() / self.toView.size().height(): # Ende bei linker Seite pEnd = QPointF(pM2.x() - self.toView.size().width() / 2, pM2.y() - (self.toView.size().width() / 2) * (deltaY / deltaX)) else: # Ende bei oberer Seite pEnd = QPointF(pM2.x() - (self.toView.size().height() / 2) * (deltaX / deltaY), pM2.y() - self.toView.size().height() / 2) else: # rechts oben if deltaX / deltaY * -1 >= self.fromView.size().width() / self.fromView.size().height(): # Start von rechter Seite pStart = QPointF(pM1.x() + self.fromView.size().width() / 2, pM1.y() + (self.fromView.size().width() / 2) * (deltaY / deltaX)) else: # Start von oberer Seite pStart = QPointF(pM1.x() - (self.fromView.size().height() / 2) * (deltaX / deltaY), pM1.y() - self.fromView.size().height() / 2) if deltaX / deltaY * -1 >= self.toView.size().width() / self.toView.size().height(): # Ende bei linker Seite pEnd = QPointF(pM2.x() - self.toView.size().width() / 2, pM2.y() - (self.toView.size().width() / 2) * (deltaY / deltaX)) else: # Ende bei unterer Seite pEnd = QPointF(pM2.x() + (self.toView.size().height() / 2) * (deltaX / deltaY), pM2.y() + self.toView.size().height() / 2) else: if deltaY >= 0: # links unten if deltaX / deltaY * -1 >= self.fromView.size().width() / self.fromView.size().height(): # Start von linker Seite pStart = QPointF(pM1.x() - self.fromView.size().width() / 2, pM1.y() - (self.fromView.size().width() / 2) * (deltaY / deltaX)) else: # Start von unterer Seite pStart = QPointF(pM1.x() + (self.fromView.size().height() / 2) * (deltaX / deltaY), pM1.y() + self.fromView.size().height() / 2) if deltaX / deltaY * -1 >= self.toView.size().width() / self.toView.size().height(): # Ende bei rechten Seite pEnd = QPointF(pM2.x() + self.toView.size().width() / 2, pM2.y() + (self.toView.size().width() / 2) * (deltaY / deltaX)) else: # Ende bei oberer Seite pEnd = QPointF(pM2.x() - (self.toView.size().height() / 2) * (deltaX / deltaY), pM2.y() - self.toView.size().height() / 2) else: # links oben if deltaX / deltaY >= self.fromView.size().width() / self.fromView.size().height(): # Start von linker Seite pStart = QPointF(pM1.x() - self.fromView.size().width() / 2, pM1.y() - (self.fromView.size().width() / 2) * (deltaY / deltaX)) else: # Start von oberer Seite pStart = QPointF(pM1.x() - (self.fromView.size().height() / 2) * (deltaX / deltaY), pM1.y() - self.fromView.size().height() / 2) if deltaX / deltaY >= self.toView.size().width() / self.toView.size().height(): # Ende bei rechter Seite pEnd = QPointF(pM2.x() + self.toView.size().width() / 2, pM2.y() + (self.toView.size().width() / 2) * (deltaY / deltaX)) else: # Ende bei unterer Seite pEnd = QPointF(pM2.x() + (self.toView.size().height() / 2) * (deltaX / deltaY), pM2.y() + self.toView.size().height() / 2) self.setLine(QLineF(pEnd, pStart)) if self.line().length() != 0: angle = math.acos(self.line().dx() / self.line().length()) if self.line().dy() >= 0: angle = math.pi * 2 - angle arrowP1 = self.line().p1() + QPointF(math.sin(angle + math.pi / 2.5) * self.arrowSize, math.cos(angle + math.pi / 2.5) * self.arrowSize) arrowP2 = self.line().p1() + QPointF(math.sin(angle + math.pi - math.pi / 2.5) * self.arrowSize, math.cos(angle + math.pi - math.pi / 2.5) * self.arrowSize) self.arrowhead.clear() self.arrowhead.append(self.line().p1()) self.arrowhead.append(arrowP1) self.arrowhead.append(arrowP2) painter.setBrush(QBrush(self.bgcolor)) painter.drawLine(self.line()) painter.drawPolygon(self.arrowhead)
class DrawManager(QObject): """ DEPRECATED. Will be replaced with BrushingModel, BrushingControler, BrushStroke. """ brushSizeChanged = pyqtSignal(int) brushColorChanged = pyqtSignal(QColor) minBrushSize = 1 maxBrushSize = 61 defaultBrushSize = 3 defaultDrawnNumber = 1 defaultColor = Qt.white erasingColor = Qt.black def __init__(self): QObject.__init__(self) self.shape = None self.bb = QRect() #bounding box enclosing the drawing self.brushSize = self.defaultBrushSize self.drawColor = self.defaultColor self.drawnNumber = self.defaultDrawnNumber self.penVis = QPen(self.drawColor, self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) self.penDraw = QPen(self.drawColor, self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) self.pos = None self.erasing = False #on which layer do we want to draw when self.drawingEnabled? self.drawOnto = None #an empty scene, where we add all drawn line segments #a QGraphicsLineItem, and which we can use to then #render to an image self.scene = QGraphicsScene() def growBoundingBox(self): self.bb.setLeft( max(0, self.bb.left()-self.brushSize-1)) self.bb.setTop( max(0, self.bb.top()-self.brushSize-1 )) self.bb.setRight( min(self.shape[0], self.bb.right()+self.brushSize+1)) self.bb.setBottom(min(self.shape[1], self.bb.bottom()+self.brushSize+1)) def toggleErase(self): self.erasing = not(self.erasing) def setErasing(self): self.erasing = True self.brushColorChanged.emit(self.erasingColor) def disableErasing(self): self.erasing = False self.brushColorChanged.emit(self.drawColor) def setBrushSize(self, size): self.brushSize = size self.penVis.setWidth(size) self.penDraw.setWidth(size) self.brushSizeChanged.emit(self.brushSize) def setDrawnNumber(self, num): self.drawnNumber = num self.drawnNumberChanged.emit(num) def getBrushSize(self): return self.brushSize def brushSmaller(self): b = self.brushSize if b > self.minBrushSize: self.setBrushSize(b-1) def brushBigger(self): b = self.brushSize if self.brushSize < self.maxBrushSize: self.setBrushSize(b+1) def setBrushColor(self, color): self.drawColor = color self.penVis.setColor(color) self.emit.brushColorChanged(self.drawColor) def beginDrawing(self, pos, shape): self.shape = shape self.bb = QRectF(0, 0, self.shape[0], self.shape[1]) self.scene.clear() if self.erasing: self.penVis.setColor(self.erasingColor) else: self.penVis.setColor(self.drawColor) self.pos = QPointF(pos.x()+0.0001, pos.y()+0.0001) line = self.moveTo(pos) return line def endDrawing(self, pos): self.moveTo(pos) self.growBoundingBox() tempi = QImage(QSize(self.bb.width(), self.bb.height()), QImage.Format_ARGB32_Premultiplied) #TODO: format tempi.fill(0) painter = QPainter(tempi) self.scene.render(painter, QRectF(QPointF(0,0), self.bb.size()), self.bb) return (self.bb.left(), self.bb.top(), tempi) #TODO: hackish, probably return a class ?? def dumpDraw(self, pos): res = self.endDrawing(pos) self.beginDrawing(pos, self.shape) return res def moveTo(self, pos): lineVis = QGraphicsLineItem(self.pos.x(), self.pos.y(), pos.x(), pos.y()) lineVis.setPen(self.penVis) line = QGraphicsLineItem(self.pos.x(), self.pos.y(), pos.x(), pos.y()) line.setPen(self.penDraw) self.scene.addItem(line) self.pos = pos x = pos.x() y = pos.y() #update bounding Box : if x > self.bb.right(): self.bb.setRight(x) if x < self.bb.left(): self.bb.setLeft(x) if y > self.bb.bottom(): self.bb.setBottom(y) if y < self.bb.top(): self.bb.setTop(y) return lineVis
def paint(self, painter, option, widget): if not self.source or not self.dest: return # Draw the line itself. line = QLineF(self.sourcePoint, self.destPoint) if line.length() == 0.0: return palette = QPalette() self.setZValue(self.state) if self.state == 3: pen = QPen(Qt.red, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) if self.state == 2: pen = QPen(Qt.red, 2, Qt.DashLine, Qt.RoundCap, Qt.RoundJoin) elif self.state == 1: pen = QPen(palette.color(QPalette.Disabled, QPalette.WindowText), 0, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) elif self.state == 0: pen = QPen() pen.setBrush(QBrush(Qt.NoBrush)) painter.setPen(pen) painter.drawLine(line) angle = math.acos(line.dx() / line.length()) if line.dy() >= 0: angle = Edge.TwoPi - angle # draw arrowheads if self.state == 2 or self.state == 3: # Draw the arrows if there's enough room. sourceArrowP1 = self.sourcePoint + QPointF( math.sin(angle + Edge.Pi / 3) * self.arrowSize, math.cos(angle + Edge.Pi / 3) * self.arrowSize) sourceArrowP2 = self.sourcePoint + QPointF( math.sin(angle + Edge.Pi - Edge.Pi / 3) * self.arrowSize, math.cos(angle + Edge.Pi - Edge.Pi / 3) * self.arrowSize) destArrowP1 = self.destPoint + QPointF( math.sin(angle - Edge.Pi / 3) * self.arrowSize, math.cos(angle - Edge.Pi / 3) * self.arrowSize) destArrowP2 = self.destPoint + QPointF( math.sin(angle - Edge.Pi + Edge.Pi / 3) * self.arrowSize, math.cos(angle - Edge.Pi + Edge.Pi / 3) * self.arrowSize) painter.setPen( QPen(Qt.red, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.setBrush(QBrush(Qt.red, Qt.SolidPattern)) painter.drawPolygon( QPolygonF([line.p1(), sourceArrowP1, sourceArrowP2])) #painter.drawPolygon(QPolygonF([line.p2(), destArrowP1, destArrowP2])) if self.state > 0 and self.source > self.dest: point = QPointF((self.sourcePoint.x() + self.destPoint.x()) / 2, (self.sourcePoint.y() + self.destPoint.y()) / 2) point = QPointF(point.x() + math.sin(angle) * 16, point.y() + math.cos(angle) * 16) painter.drawText(point, self.text)
class Edge(QGraphicsItem): Pi = math.pi TwoPi = 2.0 * Pi Type = QGraphicsItem.UserType + 2 def __init__(self, sourceNode, destNode, state=1, text=""): super(Edge, self).__init__() self.arrowSize = 15.0 self.sourcePoint = QPointF() self.destPoint = QPointF() self.setAcceptedMouseButtons(Qt.NoButton) self.source = sourceNode self.dest = destNode self.source.addEdge(self) self.dest.addEdge(self) self.adjust() self.state = state self.text = text line = QLineF(sourceNode.pos(), destNode.pos()) self.weight = line.length() def __len__(self): line = QLineF(self.sourceNode().pos(), self.destNode().pos()) return line.length() def type(self): return Edge.Type def sourceNode(self): return self.source def setSourceNode(self, node): self.source = node self.adjust() def destNode(self): return self.dest def setDestNode(self, node): self.dest = node self.adjust() def adjust(self): if not self.source or not self.dest: return line = QLineF(self.mapFromItem(self.source, 0, 0), self.mapFromItem(self.dest, 0, 0)) length = line.length() self.prepareGeometryChange() if length > 20.0: edgeOffset = QPointF((line.dx() * 10) / length, (line.dy() * 10) / length) self.sourcePoint = line.p1() + edgeOffset self.destPoint = line.p2() - edgeOffset else: self.sourcePoint = line.p1() self.destPoint = line.p1() def setVisible(self, value=True): if value: self.state = 1 else: self.state = 0 def setActive(self, value=True): if value: self.state = 3 else: self.state = 1 def setTempActive(self, value=True): if value: self.state = 2 else: self.state = 1 def boundingRect(self): if not self.source or not self.dest: return QRectF() penWidth = 1.0 extra = (penWidth + self.arrowSize) / 2.0 + 100 return QRectF( self.sourcePoint, QSizeF(self.destPoint.x() - self.sourcePoint.x(), self.destPoint.y() - self.sourcePoint.y())).normalized().adjusted( -extra, -extra, extra, extra) def paint(self, painter, option, widget): if not self.source or not self.dest: return # Draw the line itself. line = QLineF(self.sourcePoint, self.destPoint) if line.length() == 0.0: return palette = QPalette() self.setZValue(self.state) if self.state == 3: pen = QPen(Qt.red, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) if self.state == 2: pen = QPen(Qt.red, 2, Qt.DashLine, Qt.RoundCap, Qt.RoundJoin) elif self.state == 1: pen = QPen(palette.color(QPalette.Disabled, QPalette.WindowText), 0, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) elif self.state == 0: pen = QPen() pen.setBrush(QBrush(Qt.NoBrush)) painter.setPen(pen) painter.drawLine(line) angle = math.acos(line.dx() / line.length()) if line.dy() >= 0: angle = Edge.TwoPi - angle # draw arrowheads if self.state == 2 or self.state == 3: # Draw the arrows if there's enough room. sourceArrowP1 = self.sourcePoint + QPointF( math.sin(angle + Edge.Pi / 3) * self.arrowSize, math.cos(angle + Edge.Pi / 3) * self.arrowSize) sourceArrowP2 = self.sourcePoint + QPointF( math.sin(angle + Edge.Pi - Edge.Pi / 3) * self.arrowSize, math.cos(angle + Edge.Pi - Edge.Pi / 3) * self.arrowSize) destArrowP1 = self.destPoint + QPointF( math.sin(angle - Edge.Pi / 3) * self.arrowSize, math.cos(angle - Edge.Pi / 3) * self.arrowSize) destArrowP2 = self.destPoint + QPointF( math.sin(angle - Edge.Pi + Edge.Pi / 3) * self.arrowSize, math.cos(angle - Edge.Pi + Edge.Pi / 3) * self.arrowSize) painter.setPen( QPen(Qt.red, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.setBrush(QBrush(Qt.red, Qt.SolidPattern)) painter.drawPolygon( QPolygonF([line.p1(), sourceArrowP1, sourceArrowP2])) #painter.drawPolygon(QPolygonF([line.p2(), destArrowP1, destArrowP2])) if self.state > 0 and self.source > self.dest: point = QPointF((self.sourcePoint.x() + self.destPoint.x()) / 2, (self.sourcePoint.y() + self.destPoint.y()) / 2) point = QPointF(point.x() + math.sin(angle) * 16, point.y() + math.cos(angle) * 16) painter.drawText(point, self.text)
class BrushingModel(QObject): brushSizeChanged = pyqtSignal(int) brushColorChanged = pyqtSignal(QColor) brushStrokeAvailable = pyqtSignal(QPointF, object) drawnNumberChanged = pyqtSignal(int) minBrushSize = 1 maxBrushSize = 61 defaultBrushSize = 3 defaultDrawnNumber = 1 defaultColor = Qt.white erasingColor = Qt.black erasingNumber = 100 def __init__(self, parent=None): QObject.__init__(self, parent=parent) self.sliceRect = None self.bb = QRect() #bounding box enclosing the drawing self.brushSize = self.defaultBrushSize self.drawColor = self.defaultColor self._temp_color = None self._temp_number = None self.drawnNumber = self.defaultDrawnNumber self.pos = None self.erasing = False self._hasMoved = False self.drawOnto = None #an empty scene, where we add all drawn line segments #a QGraphicsLineItem, and which we can use to then #render to an image self.scene = QGraphicsScene() def toggleErase(self): self.erasing = not(self.erasing) if self.erasing: self.setErasing() else: self.disableErasing() def setErasing(self): self.erasing = True self._temp_color = self.drawColor self._temp_number = self.drawnNumber self.setBrushColor(self.erasingColor) self.brushColorChanged.emit(self.erasingColor) self.setDrawnNumber(self.erasingNumber) def disableErasing(self): self.erasing = False self.setBrushColor(self._temp_color) self.brushColorChanged.emit(self.drawColor) self.setDrawnNumber(self._temp_number) def setBrushSize(self, size): self.brushSize = size self.brushSizeChanged.emit(self.brushSize) def setDrawnNumber(self, num): self.drawnNumber = num self.drawnNumberChanged.emit(num) def getBrushSize(self): return self.brushSize def brushSmaller(self): b = self.brushSize if b > self.minBrushSize: self.setBrushSize(b-1) def brushBigger(self): b = self.brushSize if self.brushSize < self.maxBrushSize: self.setBrushSize(b+1) def setBrushColor(self, color): self.drawColor = color self.brushColorChanged.emit(self.drawColor) def beginDrawing(self, pos, sliceRect): ''' pos -- QPointF-like ''' self.sliceRect = sliceRect self.scene.clear() self.bb = QRect() self.pos = QPointF(pos.x(), pos.y()) self._hasMoved = False def endDrawing(self, pos): has_moved = self._hasMoved # _hasMoved will change after calling moveTo if has_moved: self.moveTo(pos) else: assert(self.pos == pos) self.moveTo(QPointF(pos.x()+0.0001, pos.y()+0.0001)) # move a little tempi = QImage(QSize(self.bb.width(), self.bb.height()), QImage.Format_ARGB32_Premultiplied) #TODO: format tempi.fill(0) painter = QPainter(tempi) self.scene.render(painter, target=QRectF(), source=QRectF(QPointF(self.bb.x(), self.bb.y()), QSizeF(self.bb.width(), self.bb.height()))) painter.end() ndarr = qimage2ndarray.rgb_view(tempi)[:,:,0] labels = numpy.where(ndarr>0,numpy.uint8(self.drawnNumber),numpy.uint8(0)) labels = labels.swapaxes(0,1) assert labels.shape[0] == self.bb.width() assert labels.shape[1] == self.bb.height() ## ## ensure that at least one pixel is label when the brush size is 1 ## ## this happens when the user just clicked without moving ## in that case the lineitem will be so tiny, that it won't be rendered ## into a single pixel by the code above if not has_moved and self.brushSize <= 1 and numpy.count_nonzero(labels) == 0: labels[labels.shape[0]//2, labels.shape[1]//2] = self.drawnNumber self.brushStrokeAvailable.emit(QPointF(self.bb.x(), self.bb.y()), labels) def dumpDraw(self, pos): res = self.endDrawing(pos) self.beginDrawing(pos, self.sliceRect) return res def moveTo(self, pos): #data coordinates oldX, oldY = self.pos.x(), self.pos.y() x,y = pos.x(), pos.y() line = QGraphicsLineItem(oldX, oldY, x, y) line.setPen(QPen( QBrush(Qt.white), self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) self.scene.addItem(line) self._hasMoved = True #update bounding Box if not self.bb.isValid(): self.bb = QRect(QPoint(oldX,oldY), QSize(1,1)) #grow bounding box self.bb.setLeft( min(self.bb.left(), max(0, x-self.brushSize/2-1) ) ) self.bb.setRight( max(self.bb.right(), min(self.sliceRect[0]-1, x+self.brushSize/2+1) ) ) self.bb.setTop( min(self.bb.top(), max(0, y-self.brushSize/2-1) ) ) self.bb.setBottom(max(self.bb.bottom(), min(self.sliceRect[1]-1, y+self.brushSize/2+1) ) ) #update/move position self.pos = pos
class ImageView2D(QGraphicsView): focusChanged = pyqtSignal() """ Shows a ImageScene2D to the user and allows for interactive scrolling, panning, zooming etc. """ @property def sliceShape(self): """ (width, height) of the scene. Specifying the shape is necessary to allow for correct scrollbars """ return self._sliceShape @sliceShape.setter def sliceShape(self, s): self._sliceShape = s self.scene().dataShape = s self._crossHairCursor.dataShape = s self._sliceIntersectionMarker.dataShape = s @property def hud(self): return self._hud @hud.setter def hud(self, hud): """ Sets up a heads up display at the upper left corner of the view hud -- a QWidget """ self._hud = hud self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0,0,0,0) self.layout().addWidget(self._hud) self.layout().addStretch() scene = self.scene() hud.zoomToFitButtonClicked.connect(self.fitImage) hud.resetZoomButtonClicked.connect(self.doScaleTo) hud.rotLeftButtonClicked.connect(scene._onRotateLeft) hud.rotRightButtonClicked.connect(scene._onRotateRight) hud.swapAxesButtonClicked.connect(scene._onSwapAxes) scene.axesChanged.connect(hud.setAxes) def __init__(self, parent, imagescene2d): """ Constructs a view upon a ImageScene2D imagescene2d -- a ImgeScene2D instance """ QGraphicsView.__init__(self, parent) self.setScene(imagescene2d) self.mousePos = QPointF(0,0) # FIXME: These int members shadow QWidget.x() and QWidget.y(), which can lead to confusion when debugging... self.x, self.y = (0,0) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._isRubberBandZoom = False self._cursorBackup = None #these attributes are exposed as public properties above self._sliceShape = None #2D shape of this view's shown image self._slices = None #number of slices that are stacked self._hud = None self._crossHairCursor = None self._sliceIntersectionMarker = None self._ticker = QTimer(self) self._ticker.timeout.connect(self._tickerEvent) # # Setup the Viewport for fast painting # #With these flags turned on we could handle the drawing of the #white background ourselves thus removing the flicker #when scrolling fast through the slices #self.viewport().setAttribute(Qt.WA_OpaquePaintEvent) #self.viewport().setAttribute(Qt.WA_NoSystemBackground) #self.viewport().setAttribute(Qt.WA_PaintOnScreen) #self.viewport().setAutoFillBackground(False) self.setViewportUpdateMode(QGraphicsView.MinimalViewportUpdate) #as rescaling images is slow if done in software, #we use Qt's built-in background caching mode so that the cached #image need only be blitted on the screen when we only move #the cursor self.setCacheMode(QGraphicsView.CacheBackground) self.setRenderHint(QPainter.Antialiasing, False) self._crossHairCursor = CrossHairCursor(self.scene()) self._crossHairCursor.setZValue(99) self._sliceIntersectionMarker = SliceIntersectionMarker(self.scene()) self._sliceIntersectionMarker.setZValue(100) self._sliceIntersectionMarker.setVisibility(True) #FIXME: this should be private, but is currently used from # within the image scene renderer self.tempImageItems = [] self._zoomFactor = 1.0 #for panning self._lastPanPoint = QPoint() self._dragMode = False self._deltaPan = QPointF(0,0) #FIXME: Is there are more elegant way to handle this? self.setMouseTracking(True) # invisible cursor to enable custom cursor self._hiddenCursor = QCursor(Qt.BlankCursor) # For screen recording BlankCursor doesn't work #self.hiddenCursor = QCursor(Qt.ArrowCursor) def _cleanUp(self): self._ticker.stop() del self._ticker def setZoomFactor(self,zoom): if self._hud is not None: self._hud.zoomLevelIndicator.updateLevel(zoom) self._zoomFactor = zoom def indicateSlicingPositionSettled(self, settled): self.scene().indicateSlicingPositionSettled(settled) def viewportRect(self): """ Return a QRectF giving the part of the scene currently displayed in this widget's viewport in the scene's coordinates """ r = self.mapToScene(self.viewport().geometry()).boundingRect() return r def mapScene2Data(self, pos): return self.scene().scene2data.map(pos) def mapMouseCoordinates2Data(self, pos): return self.mapScene2Data(self.mapToScene(pos)) def _panning(self): hBar = self.horizontalScrollBar() vBar = self.verticalScrollBar() vBar.setValue(vBar.value() - self._deltaPan.y()) if self.isRightToLeft(): hBar.setValue(hBar.value() + self._deltaPan.x()) else: hBar.setValue(hBar.value() - self._deltaPan.x()) def _deaccelerate(self, speed, a=1, maxVal=64): x = self._qBound(-maxVal, speed.x(), maxVal) y = self._qBound(-maxVal, speed.y(), maxVal) ax ,ay = self._setdeaccelerateAxAy(speed.x(), speed.y(), a) if x > 0: x = max(0.0, x - a*ax) elif x < 0: x = min(0.0, x + a*ax) if y > 0: y = max(0.0, y - a*ay) elif y < 0: y = min(0.0, y + a*ay) return QPointF(x, y) def _qBound(self, minVal, current, maxVal): """PyQt4 does not wrap the qBound function from Qt's global namespace This is equivalent.""" return max(min(current, maxVal), minVal) def _setdeaccelerateAxAy(self, x, y, a): x = abs(x) y = abs(y) if x > y: if y > 0: ax = int(x / y) if ax != 0: return ax, 1 else: return x/a, 1 if y > x: if x > 0: ay = int(y/x) if ay != 0: return 1, ay else: return 1, y/a return 1, 1 def _tickerEvent(self): if self._deltaPan.x() == 0.0 and self._deltaPan.y() == 0.0 or self._dragMode == True: self._ticker.stop() else: self._deltaPan = self._deaccelerate(self._deltaPan) self._panning() def zoomOut(self): self.doScale(0.9) def zoomIn(self): self.doScale(1.1) def fitImage(self): self.fitInView(self.sceneRect(), Qt.KeepAspectRatio) width, height = self.size().width() / self.sceneRect().width(), self.height() / self.sceneRect().height() self.setZoomFactor(min(width, height)) def centerImage(self): self.centerOn(self.sceneRect().width()/2 + self.sceneRect().x(), self.sceneRect().height()/2 + self.sceneRect().y()) def toggleHud(self): if self._hud is not None: self._hud.setVisible(not self._hud.isVisible()) def setHudVisible(self, visible): if self._hud is not None: self._hud.setVisible(visible) def hudVisible(self): return self._hud.isVisible() def focusInEvent(self, event): self.setStyleSheet(".QFrame {border: 2px solid white; border-radius: 4px;}") self.focusChanged.emit() def focusOutEvent(self, event): self.setStyleSheet(".QFrame {}") def changeViewPort(self,qRectf): self.fitInView(qRectf,mode = Qt.KeepAspectRatio) width, height = self.size().width() / qRectf.width(), self.height() / qRectf.height() self.setZoomFactor(min(width, height)) def doScale(self, factor): self.setZoomFactor(self._zoomFactor * factor) self.scale(factor, factor) def doScaleTo(self, zoom=1): factor = ( 1 / self._zoomFactor ) * zoom self.setZoomFactor(zoom) self.scale(factor, factor)
class QgsMapToolAnnotation(QgsMapTool): def __init__(self, canvas): self.mCanvas = canvas QgsMapTool.__init__(self, self.mCanvas) self.mCurrentMoveAction = QgsAnnotationItem.NoAction self.mLastMousePosition = QPointF(0, 0) self.mCursor = QCursor(Qt.ArrowCursor) def createItem(self, e): return None def createItemEditor(self, item): if (item == None): return None item._class_ = QgsTextAnnotationItem if isinstance(item, QgsTextAnnotationItem): return QgsTextAnnotationDialog(item) return None def canvasReleaseEvent(self, e): self.mCurrentMoveAction = QgsAnnotationItem.NoAction self.mCanvas.setCursor(self.mCursor) def canvasPressEvent(self, e): sItem = self.selectedItem() if (sItem != None): self.mCurrentMoveAction = sItem.moveActionForPosition(e.posF()) if (self.mCurrentMoveAction != QgsAnnotationItem.NoAction): return if (sItem == None or self.mCurrentMoveAction == QgsAnnotationItem.NoAction): self.mCanvas.scene().clearSelection() existingItem = self.itemAtPos(e.posF()) if (existingItem != None): existingItem.setSelected(True) else: self.createItem(e) def keyPressEvent(self, e): if (e.key() == Qt.Key_T and e.modifiers() == Qt.ControlModifier): self.toggleTextItemVisibilities() sItem = self.selectedItem() if (sItem != None): if (e.key() == Qt.Key_Backspace or e.key() == Qt.Key_Delete): neutralCursor = QCursor( sItem.cursorShapeForAction(QgsAnnotationItem.NoAction)) self.mCanvas.scene().removeItem(sItem) self.mCanvas.setCursor(neutralCursor) #// Override default shortcut management in MapCanvas e.ignore() def canvasMoveEvent(self, e): sItem = self.selectedItem() if (sItem != None and (e.buttons() & Qt.LeftButton)): if (self.mCurrentMoveAction == QgsAnnotationItem.MoveMapPosition): sItem.setMapPosition(self.toMapCoordinates(e.pos())) sItem.update() elif (self.mCurrentMoveAction == QgsAnnotationItem.MoveFramePosition): if (sItem.mapPositionFixed()): sItem.setOffsetFromReferencePoint( sItem.offsetFromReferencePoint() + (e.posF() - self.mLastMousePosition)) else: newCanvasPos = sItem.pos() + (e.posF() - self.mLastMousePosition) sItem.setMapPosition( self.toMapCoordinates(newCanvasPos.toPoint())) sItem.update() elif (self.mCurrentMoveAction != QgsAnnotationItem.NoAction): size = sItem.frameSize() xmin = sItem.offsetFromReferencePoint().x() ymin = sItem.offsetFromReferencePoint().y() xmax = xmin + size.width() ymax = ymin + size.height() if (self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameRight or self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameRightDown or self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameRightUp): xmax += e.posF().x() - self.mLastMousePosition.x() if (self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameLeft or self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameLeftDown or self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameLeftUp): xmin += e.posF().x() - self.mLastMousePosition.x() if (self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameUp or self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameLeftUp or self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameRightUp): ymin += e.posF().y() - self.mLastMousePosition.y() if (self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameDown or self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameLeftDown or self.mCurrentMoveAction == QgsAnnotationItem.ResizeFrameRightDown): ymax += e.posF().y() - self.mLastMousePosition.y() #//switch min / max if necessary tmp = 0.0 if (xmax < xmin): tmp = xmax xmax = xmin xmin = tmp if (ymax < ymin): tmp = ymax ymax = ymin ymin = tmp sItem.setOffsetFromReferencePoint(QPointF(xmin, ymin)) sItem.setFrameSize(QSizeF(xmax - xmin, ymax - ymin)) sItem.update() elif (sItem != None): moveAction = sItem.moveActionForPosition(e.posF()) self.mCanvas.setCursor( QCursor(sItem.cursorShapeForAction(moveAction))) self.mLastMousePosition = e.posF() def canvasDoubleClickEvent(self, e): item = self.itemAtPos(e.posF()) if (item == None): return itemEditor = self.createItemEditor(item) if (itemEditor != None): itemEditor.exec_() def itemAtPos(self, pos): graphicItems = self.mCanvas.items(pos.toPoint()) for annotationItem in graphicItems: annotationItem._class_ = QgsAnnotationItem if (isinstance(annotationItem, QgsAnnotationItem)): return annotationItem return None def selectedItem(self): gItemList = self.mCanvas.scene().selectedItems() for aItem in gItemList: aItem._class_ = QgsAnnotationItem if (isinstance(aItem, QgsAnnotationItem)): return aItem return None def annotationItems(self): annotationItemList = [] itemList = self.mCanvas.scene().items() for aItem in itemList: aItem._class_ = QgsAnnotationItem if (isinstance(aItem, QgsAnnotationItem)): self.annotationItemList.append(aItem) return annotationItemList def toggleTextItemVisibilities(self): itemList = self.annotationItems() for textItem in itemList: textItem._class_ = QgsTextAnnotationItem if (isinstance(textItem, QgsTextAnnotationItem)): textItem.setVisible(not textItem.isVisible())
def layout(self, scene, nodes, center=None, padX=None, padY=None, direction=None, animationGroup=None): """ Lays out the nodes for this scene based on a block layering algorithm. :param scene | <XNodeScene> nodes | [<XNode>, ..] center | <QPointF> || None padX | <int> || None padY | <int> || None direction | <Qt.Direction> animationGroup | <QAnimationGroup> || None :return {<XNode>: <QRectF>, ..} | new rects per affected node """ nodes = filter(lambda x: x is not None and x.isVisible(), nodes) # make sure we have at least 1 node, otherwise, it is already laid out if not nodes or len(nodes) == 1: return {} # calculate the default padding based on the scene if padX == None: if direction == Qt.Vertical: padX = 2 * scene.cellWidth() else: padX = 4 * scene.cellWidth() if padY == None: if direction == Qt.Vertical: padY = 4 * scene.cellHeight() else: padY = 2 * scene.cellWidth() # step 1: create a mapping of the connections connection_map = self.connectionMap(scene, nodes) # step 2: organize the nodes into layers based on their connection chain layers = self.generateLayers(scene, nodes, connection_map) layers = list(reversed(layers)) # step 3: calculate the total dimensions for the layout bounds = QRectF() # step 3.1: compare the nodes together that have common connections layer_widths = [] layer_heights = [] node_heights = {} node_widths = {} for layer_index, layer in enumerate(layers): layer_w = 0 layer_h = 0 layer_node_w = [] layer_node_h = [] self.organizeLayer(layer, connection_map) for node in layer: rect = node.rect() layer_node_w.append(rect.width()) layer_node_h.append(rect.height()) if direction == Qt.Vertical: layer_w += rect.width() layer_h = max(rect.height(), layer_h) else: layer_w = max(rect.width(), layer_w) layer_h += rect.height() # update the bounding area if direction == Qt.Vertical: layer_w += padX * 1 - len(layer) bounds.setWidth(max(layer_w, bounds.width())) bounds.setHeight(bounds.height() + layer_h) else: layer_h += padY * 1 - len(layer) bounds.setWidth(bounds.width() + layer_w) bounds.setHeight(max(layer_h, bounds.height())) node_widths[layer_index] = layer_node_w node_heights[layer_index] = layer_node_h layer_widths.append(layer_w) layer_heights.append(layer_h) if not center: center = scene.sceneRect().center() w = bounds.width() h = bounds.height() bounds.setX(center.x() - bounds.width() / 2.0) bounds.setY(center.y() - bounds.height() / 2.0) bounds.setWidth(w) bounds.setHeight(h) # step 4: assign positions for each node by layer processed_nodes = {} layer_grps = [(i, layer) for i, layer in enumerate(layers)] layer_grps.sort(key=lambda x: len(x[1])) for layer_index, layer in reversed(layer_grps): layer_width = layer_widths[layer_index] layer_height = layer_heights[layer_index] # determine the starting point for this layer if direction == Qt.Vertical: offset = layer_index * padY + sum(layer_heights[:layer_index]) point = QPointF(bounds.x(), offset + bounds.y()) else: offset = layer_index * padX + sum(layer_widths[:layer_index]) point = QPointF(offset + bounds.x(), bounds.y()) # assign node positions based on existing connections for node_index, node in enumerate(layer): max_, min_ = (None, None) inputs, outputs = connection_map[node] for connected_node in inputs + outputs: if not connected_node in processed_nodes: continue npos = processed_nodes[connected_node] nrect = connected_node.rect() rect = QRectF(npos.x(), npos.y(), nrect.width(), nrect.height()) if direction == Qt.Vertical: if min_ is None: min_ = rect.left() min_ = min(rect.left(), min_) max_ = max(rect.right(), max_) else: if min_ is None: min_ = rect.top() min_ = min(rect.top(), min_) max_ = max(rect.bottom(), max_) if direction == Qt.Vertical: off_x = 0 off_y = (layer_height - node.rect().height()) / 2.0 start_x = (bounds.width() - layer_width) start_y = 0 else: off_x = (layer_width - node.rect().width()) / 2.0 off_y = 0 start_x = 0 start_y = (bounds.height() - layer_height) # align against existing nodes if not None in (min_, max): if direction == Qt.Vertical: off_x = (max_ - min_) / 2.0 - node.rect().width() / 2.0 point_x = min_ + off_x point_y = point.y() + off_y else: off_y = (max_ - min_) / 2.0 - node.rect().height() / 2.0 point_x = point.x() + off_x point_y = min_ + off_y # otherwise, align based on its position in the layer else: if direction == Qt.Vertical: off_x = sum(node_widths[layer_index][:node_index]) off_x += node_index * padX off_x += start_x point_x = point.x() + off_x point_y = point.y() + off_y else: off_y = sum(node_heights[layer_index][:node_index]) off_y += node_index * padY off_y += start_y point_x = point.x() + off_x point_y = point.y() + off_y if not animationGroup: node.setPos(point_x, point_y) else: anim = XNodeAnimation(node, 'setPos') anim.setStartValue(node.pos()) anim.setEndValue(QPointF(point_x, point_y)) animationGroup.addAnimation(anim) processed_nodes[node] = QPointF(point_x, point_y) if self._testing: QApplication.processEvents() time.sleep(1) return processed_nodes
def render(self): """ Main-Method of the Pointer-Class <br> calculates/renders/draws the Lines of the Arrow """ points = QPolygonF() self.toView.x() pM1 = QPointF(self.fromView.x() + self.fromView.size().width()/2, self.fromView.y() + self.fromView.size().height()/2) pM2 = QPointF(self.toView.x() + self.toView.size().width()/2, self.toView.y() + self.toView.size().height()/2) deltaX = pM2.x()-pM1.x() deltaY = pM2.y()-pM1.y() if deltaX == 0: deltaX = 0.01 if deltaY == 0: deltaY = 0.01 if deltaX >= 0: if deltaY >= 0: # rechts unten if deltaX/deltaY >= self.fromView.size().width()/self.fromView.size().height(): # Start von rechter Seite pStart = QPointF(pM1.x() + self.fromView.size().width()/2, pM1.y() + (self.fromView.size().width()/2)*(deltaY/deltaX)) else: # Start von unterer Seite pStart = QPointF(pM1.x() + (self.fromView.size().height()/2)*(deltaX/deltaY), pM1.y() + self.fromView.size().height()/2) if deltaX/deltaY >= self.toView.size().width()/self.toView.size().height(): # Ende bei linker Seite pEnd = QPointF(pM2.x() - self.toView.size().width()/2, pM2.y() - (self.toView.size().width()/2)*(deltaY/deltaX)) else: # Ende bei oberer Seite pEnd = QPointF(pM2.x() - (self.toView.size().height()/2)*(deltaX/deltaY), pM2.y() - self.toView.size().height()/2) else: # rechts oben if deltaX/deltaY*-1 >= self.fromView.size().width()/self.fromView.size().height(): # Start von rechter Seite pStart = QPointF(pM1.x() + self.fromView.size().width()/2, pM1.y() + (self.fromView.size().width()/2)*(deltaY/deltaX)) else: # Start von oberer Seite pStart = QPointF(pM1.x() - (self.fromView.size().height()/2)*(deltaX/deltaY), pM1.y() - self.fromView.size().height()/2) if deltaX/deltaY*-1 >= self.toView.size().width()/self.toView.size().height(): # Ende bei linker Seite pEnd = QPointF(pM2.x() - self.toView.size().width()/2, pM2.y() - (self.toView.size().width()/2)*(deltaY/deltaX)) else: # Ende bei unterer Seite pEnd = QPointF(pM2.x() + (self.toView.size().height()/2)*(deltaX/deltaY), pM2.y() + self.toView.size().height()/2) else: if deltaY >= 0: # links unten if deltaX/deltaY*-1 >= self.fromView.size().width()/self.fromView.size().height(): # Start von linker Seite pStart = QPointF(pM1.x() - self.fromView.size().width()/2, pM1.y() - (self.fromView.size().width()/2)*(deltaY/deltaX)) else: # Start von unterer Seite pStart = QPointF(pM1.x() + (self.fromView.size().height()/2)*(deltaX/deltaY), pM1.y() + self.fromView.size().height()/2) if deltaX/deltaY*-1 >= self.toView.size().width()/self.toView.size().height(): # Ende bei rechten Seite pEnd = QPointF(pM2.x() + self.toView.size().width()/2, pM2.y() + (self.toView.size().width()/2)*(deltaY/deltaX)) else: # Ende bei oberer Seite pEnd = QPointF(pM2.x() - (self.toView.size().height()/2)*(deltaX/deltaY), pM2.y() - self.toView.size().height()/2) else: # links oben if deltaX/deltaY >= self.fromView.size().width()/self.fromView.size().height(): # Start von linker Seite pStart = QPointF(pM1.x() - self.fromView.size().width()/2, pM1.y() - (self.fromView.size().width()/2)*(deltaY/deltaX)) else: # Start von oberer Seite pStart = QPointF(pM1.x() - (self.fromView.size().height()/2)*(deltaX/deltaY), pM1.y() - self.fromView.size().height()/2) if deltaX/deltaY >= self.toView.size().width()/self.toView.size().height(): # Ende bei rechter Seite pEnd = QPointF(pM2.x() + self.toView.size().width()/2, pM2.y() + (self.toView.size().width()/2)*(deltaY/deltaX)) else: # Ende bei unterer Seite pEnd = QPointF(pM2.x() + (self.toView.size().height()/2)*(deltaX/deltaY), pM2.y() + self.toView.size().height()/2) #pStart = QPointF(self.fromView.x() + self.fromView.size().width(), # self.fromView.y() + self.fromView.size().height()/2) #pEnd = QPointF(self.toView.x(), self.toView.y() + self.toView.size().height()/2) p1 = pStart p2 = QPointF(pEnd.x()-(pEnd.x()-pStart.x())/7, pEnd.y()-(pEnd.y()-pStart.y())/7) p3 = QPointF(p2.x()-(p2.y()-p1.y())/20, p2.y()+(p2.x()-p1.x())/20) p4 = pEnd p5 = QPointF(p2.x()+(p2.y()-p1.y())/20, p2.y()-(p2.x()-p1.x())/20) p6 = p2 points.append(p1) points.append(p2) points.append(p3) points.append(p4) points.append(p5) points.append(p6) self.setPolygon(points)
class DataPlot(Qwt.QwtPlot): mouseCoordinatesChanged = Signal(QPointF) colors = [Qt.red, Qt.blue, Qt.magenta, Qt.cyan, Qt.green] dataNumValuesSaved = 1000 dataNumValuesPloted = 1000 def __init__(self, *args): super(DataPlot, self).__init__(*args) self.setCanvasBackground(Qt.white) self.insertLegend(Qwt.QwtLegend(), Qwt.QwtPlot.BottomLegend) self.curves = {} self.pauseFlag = False self.dataOffsetX = 0 self.canvasOffsetX = 0 self.canvasOffsetY = 0 self.lastCanvasX = 0 self.lastCanvasY = 0 self.pressedCanvasY = 0 self.redrawOnEachUpdate = False self.redrawOnFullUpdate = True self.redrawTimerInterval = None self.redrawManually = False self.oscilloscopeNextDataPosition = 0 self.oscilloscopeMode = False self.lastClickCoordinates = None markerAxisY = Qwt.QwtPlotMarker() markerAxisY.setLabelAlignment(Qt.AlignRight | Qt.AlignTop) markerAxisY.setLineStyle(Qwt.QwtPlotMarker.HLine) markerAxisY.setYValue(0.0) markerAxisY.attach(self) #self.setAxisTitle(Qwt.QwtPlot.xBottom, "Time") #self.setAxisTitle(Qwt.QwtPlot.yLeft, "Value") self.picker = Qwt.QwtPlotPicker( Qwt.QwtPlot.xBottom, Qwt.QwtPlot.yLeft, Qwt.QwtPicker.PolygonSelection, Qwt.QwtPlotPicker.PolygonRubberBand, Qwt.QwtPicker.AlwaysOn, self.canvas() ) self.picker.setRubberBandPen(QPen(self.colors[-1])) self.picker.setTrackerPen(QPen(self.colors[-1])) # Initialize data self.timeAxis = arange(self.dataNumValuesPloted) self.canvasDisplayHeight = 1000 self.canvasDisplayWidth = self.canvas().width() self.dataOffsetX = self.dataNumValuesSaved - len(self.timeAxis) self.redraw() self.moveCanvas(0, 0) self.canvas().setMouseTracking(True) self.canvas().installEventFilter(self) # init and start redraw timer self.timerRedraw = QTimer(self) self.timerRedraw.timeout.connect(self.redraw) if self.redrawTimerInterval: self.timerRedraw.start(self.redrawTimerInterval) def eventFilter(self, _, event): if event.type() == QEvent.MouseButtonRelease: x = self.invTransform(Qwt.QwtPlot.xBottom, event.pos().x()) y = self.invTransform(Qwt.QwtPlot.yLeft, event.pos().y()) self.lastClickCoordinates = QPointF(x, y) elif event.type() == QEvent.MouseMove: x = self.invTransform(Qwt.QwtPlot.xBottom, event.pos().x()) y = self.invTransform(Qwt.QwtPlot.yLeft, event.pos().y()) coords = QPointF(x, y) if self.picker.isActive() and self.lastClickCoordinates is not None: toolTip = 'origin x: %.5f, y: %.5f' % (self.lastClickCoordinates.x(), self.lastClickCoordinates.y()) delta = coords - self.lastClickCoordinates toolTip += '\ndelta x: %.5f, y: %.5f\nlength: %.5f' % (delta.x(), delta.y(), math.sqrt(delta.x() ** 2 + delta.y() ** 2)) else: toolTip = 'buttons\nleft: measure\nmiddle: move\nright: zoom x/y\nwheel: zoom y' self.setToolTip(toolTip) self.mouseCoordinatesChanged.emit(coords) return False def setRedrawInterval(self, interval): self.redrawTimerInterval = interval if self.redrawTimerInterval: self.redrawOnEachUpdate = False self.redrawOnFullUpdate = False self.timerRedraw.start(self.redrawTimerInterval) def resizeEvent(self, event): super(DataPlot, self).resizeEvent(event) self.rescale() def getCurves(self): return self.curves def addCurve(self, curveId, curveName): curveId = str(curveId) if self.curves.get(curveId): return curveObject = Qwt.QwtPlotCurve(curveName) curveObject.attach(self) curveObject.setPen(QPen(self.colors[len(self.curves.keys()) % len(self.colors)])) self.curves[curveId] = { 'name': curveName, 'data': zeros(self.dataNumValuesSaved), 'object': curveObject, } def removeCurve(self, curveId): curveId = str(curveId) if curveId in self.curves: self.curves[curveId]['object'].hide() self.curves[curveId]['object'].attach(None) del self.curves[curveId]['object'] del self.curves[curveId] def removeAllCurves(self): for curveId in self.curves.keys(): self.removeCurve(curveId) self.clear() @Slot(str, float) def updateValue(self, curveId, value): curveId = str(curveId) # update data plot if (not self.pauseFlag) and curveId in self.curves: if self.oscilloscopeMode: self.curves[curveId]['data'][self.oscilloscopeNextDataPosition] = float(value) # only advance the oscilloscopeNextDataPosition for the first curve in the dict if self.curves.keys()[0] == curveId: self.oscilloscopeNextDataPosition = (self.oscilloscopeNextDataPosition + 1) % len(self.timeAxis) else: self.curves[curveId]['data'] = concatenate((self.curves[curveId]['data'][1:], self.curves[curveId]['data'][:1]), 1) self.curves[curveId]['data'][-1] = float(value) if not self.redrawManually: if self.redrawOnEachUpdate or (self.redrawOnFullUpdate and self.curves.keys()[0] == curveId): self.redraw() @Slot(bool) def togglePause(self, enabled): self.pauseFlag = enabled @Slot(bool) def toggleOscilloscopeMode(self, enabled): self.oscilloscopeMode = enabled def hasCurve(self, curveId): curveId = str(curveId) return curveId in self.curves def redraw(self): for curveId in self.curves.keys(): self.curves[curveId]['object'].setData(self.timeAxis, self.curves[curveId]['data'][self.dataOffsetX : self.dataOffsetX + len(self.timeAxis)]) #self.curves[curveId]['object'].setStyle(Qwt.QwtPlotCurve.CurveStyle(3)) self.replot() def rescale(self): yNumTicks = self.height() / 40 yLowerLimit = self.canvasOffsetY - (self.canvasDisplayHeight / 2) yUpperLimit = self.canvasOffsetY + (self.canvasDisplayHeight / 2) # calculate a fitting step size for nice, round tick labels, depending on the displayed value area yDelta = yUpperLimit - yLowerLimit exponent = int(math.log10(yDelta)) presicion = -(exponent - 2) yStepSize = round(yDelta / yNumTicks, presicion) self.setAxisScale(Qwt.QwtPlot.yLeft, yLowerLimit, yUpperLimit, yStepSize) self.setAxisScale(Qwt.QwtPlot.xBottom, 0, len(self.timeAxis)) self.redraw() def rescaleAxisX(self, deltaX): newLen = len(self.timeAxis) + deltaX newLen = max(10, min(newLen, self.dataNumValuesSaved)) self.timeAxis = arange(newLen) self.dataOffsetX = max(0, min(self.dataOffsetX, self.dataNumValuesSaved - len(self.timeAxis))) self.rescale() def scaleAxisY(self, maxValue): self.canvasDisplayHeight = maxValue self.rescale() def moveCanvas(self, deltaX, deltaY): self.dataOffsetX += deltaX * len(self.timeAxis) / float(self.canvas().width()) self.dataOffsetX = max(0, min(self.dataOffsetX, self.dataNumValuesSaved - len(self.timeAxis))) self.canvasOffsetX += deltaX * self.canvasDisplayWidth / self.canvas().width() self.canvasOffsetY += deltaY * self.canvasDisplayHeight / self.canvas().height() self.rescale() def mousePressEvent(self, event): self.lastCanvasX = event.x() - self.canvas().x() self.lastCanvasY = event.y() - self.canvas().y() self.pressedCanvasY = event.y() - self.canvas().y() def mouseMoveEvent(self, event): canvasX = event.x() - self.canvas().x() canvasY = event.y() - self.canvas().y() if event.buttons() & Qt.MiddleButton: # middle button moves the canvas deltaX = self.lastCanvasX - canvasX deltaY = canvasY - self.lastCanvasY self.moveCanvas(deltaX, deltaY) elif event.buttons() & Qt.RightButton: # right button zooms zoomFactor = max(-0.6, min(0.6, (self.lastCanvasY - canvasY) / 20.0 / 2.0)) deltaY = (self.canvas().height() / 2.0) - self.pressedCanvasY self.moveCanvas(0, zoomFactor * deltaY * 1.0225) self.scaleAxisY(max(0.005, self.canvasDisplayHeight - (zoomFactor * self.canvasDisplayHeight))) self.rescaleAxisX(self.lastCanvasX - canvasX) self.lastCanvasX = canvasX self.lastCanvasY = canvasY def wheelEvent(self, event): # mouse wheel zooms the y-axis canvasY = event.y() - self.canvas().y() zoomFactor = max(-0.6, min(0.6, (event.delta() / 120) / 6.0)) deltaY = (self.canvas().height() / 2.0) - canvasY self.moveCanvas(0, zoomFactor * deltaY * 1.0225) self.scaleAxisY(max(0.0005, self.canvasDisplayHeight - zoomFactor * self.canvasDisplayHeight))
class ImageView2D(QGraphicsView): """ Shows a ImageScene2D to the user and allows for interactive scrolling, panning, zooming etc. It intercepts all meaningful events on the widget for further interpretation (e.g. the view does not know which slicing axis it represents in the 3D slice viewer setting). """ #notifies about the relative change in the slicing position #that is requested changeSliceDelta = pyqtSignal(int) drawing = pyqtSignal(QPointF) beginDraw = pyqtSignal(QPointF, object) endDraw = pyqtSignal(QPointF) erasingToggled = pyqtSignal(bool) #notifies that the mouse has moved to 2D coordinate x,y mouseMoved = pyqtSignal(int, int) #notifies that the user has double clicked on the 2D coordinate x,y mouseDoubleClicked = pyqtSignal(int, int) drawUpdateInterval = 300 #ms @property def shape(self): """ (width, height) of the scene. Specifying the shape is necessary to allow for correct scrollbars """ return self._shape @shape.setter def shape(self, s): self._shape = s self.scene().shape = s self._crossHairCursor.shape = (s[1], s[0]) self._sliceIntersectionMarker.shape = (s[1], s[0]) #FIXME unused? @property def name(self): return self._name @name.setter def name(self, n): self._name = n @property def hud(self): return self._hud @hud.setter def hud(self, hud): """ Sets up a heads up display at the upper left corner of the view hud -- a QWidget """ self._hud = hud self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0,0,0,0) self.layout().addWidget(self._hud) self.layout().addStretch() @property def drawingEnabled(self): return self._drawingEnabled @drawingEnabled.setter def drawingEnabled(self, enable): self._drawingEnabled = enable def __init__(self, imagescene2d, useGL=False): """ Constructs a view upon a ImageScene2D imagescene2d -- a ImgeScene2D instance useGL -- wether to enable OpenGL rendering """ QGraphicsView.__init__(self) self._useGL = useGL self.setScene(imagescene2d) #these attributes are exposed as public properties above self._shape = None #2D shape of this view's shown image self._slices = None #number of slices that are stacked self._name = '' self._hud = None self._drawingEnabled = False self._crossHairCursor = None self._sliceIntersectionMarker = None # # Setup the Viewport for fast painting # if self._useGL: self.openglWidget = QGLWidget(self) self.setViewport(self.openglWidget) #we clear the background ourselves self.viewport().setAutoFillBackground(False) #QGraphicsView cannot use partial updates when using #an OpenGL widget as a viewport #http://doc.qt.nokia.com/qq/qq26-openglcanvas.html self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) else: #Unfortunately, setting these flags has no effect when #Cache background is turned on. #With these flags turned on we could handle the drawing of the #white background ourselves thus removing the flicker #when scrolling fast through the slices #self.viewport().setAttribute(Qt.WA_OpaquePaintEvent) #self.viewport().setAttribute(Qt.WA_NoSystemBackground) #self.viewport().setAttribute(Qt.WA_PaintOnScreen) #self.viewport().setAutoFillBackground(False) self.setViewportUpdateMode(QGraphicsView.MinimalViewportUpdate) #as rescaling images is slow if done in software, #we use Qt's built-in background caching mode so that the cached #image need only be blitted on the screen when we only move #the cursor self.setCacheMode(QGraphicsView.CacheBackground) self.setRenderHint(QPainter.Antialiasing, False) #Intitialize the scene if self._useGL: self.scene().activateOpenGL( self.openglWidget ) self._crossHairCursor = CrossHairCursor(self.scene()) self._crossHairCursor.setZValue(99) self._sliceIntersectionMarker = SliceIntersectionMarker() self._sliceIntersectionMarker.setZValue(100) self.scene().addItem(self._sliceIntersectionMarker) #FIXME: Use a QAction here so that we do not have to synchronize #between this initial state and the toggle button's initial state self._sliceIntersectionMarker.setVisibility(True) #FIXME: this should be private, but is currently used from # within the image scene renderer self.tempImageItems = [] self._isDrawing = False self._zoomFactor = 1.0 #for panning self._lastPanPoint = QPoint() self._dragMode = False self._deltaPan = QPointF(0,0) #Unfortunately, setting the style like this make the scroll bars look #really crappy... #self.setStyleSheet("QWidget:!focus { border: 2px solid " + self._axisColor[self._axis].name() +"; border-radius: 4px; }\ # QWidget:focus { border: 2px solid white; border-radius: 4px; }") #FIXME: Is there are more elegant way to handle this? self.setMouseTracking(True) self._ticker = QTimer(self) self._ticker.timeout.connect(self._tickerEvent) #label updates while drawing, needed for interactive segmentation self._drawTimer = QTimer(self) self._drawTimer.timeout.connect(self.notifyDrawing) # invisible cursor to enable custom cursor self._hiddenCursor = QCursor(Qt.BlankCursor) # For screen recording BlankCursor doesn't work #self.hiddenCursor = QCursor(Qt.ArrowCursor) self._tempErase = False def swapAxes(self): """ Displays this image as if the x and y axes were swapped. """ #FIXME: This is needed for the current arrangements of the three # 3D slice views. Can this be made more elegant self.rotate(90.0) self.scale(1.0,-1.0) def _cleanUp(self): self._ticker.stop() self._drawTimer.stop() del self._drawTimer del self._ticker def viewportRect(self): """ Return a QRectF giving the part of the scene currently displayed in this widget's viewport in the scene's coordinates """ return self.mapToScene(self.viewport().geometry()).boundingRect() def saveSlice(self, filename): """Legacy code.""" #print "Saving in ", filename, "slice #", self.sliceNumber, "axis", self._axis result_image = QImage(self.scene().image.size(), self.scene().image.format()) p = QPainter(result_image) for patchNr in range(self.patchAccessor.patchCount): bounds = self.patchAccessor.getPatchBounds(patchNr) if self.openglWidget is None: p.drawImage(0, 0, self.scene().image) else: p.drawImage(bounds[0], bounds[2], self.imagePatches[patchNr]) p.end() #horrible way to transpose an image. but it works. transform = QTransform() transform.rotate(90) result_image = result_image.mirrored() result_image = result_image.transformed(transform) result_image.save(QString(filename)) def notifyDrawing(self): print "ImageView2D.notifyDrawing" #FIXME: resurrect self.drawing.emit(self.mousePos) def beginDrawing(self, pos): self.mousePos = pos self._isDrawing = True self.beginDraw.emit(pos, self.shape) def endDrawing(self, pos): InteractionLogger.log("%f: endDrawing()" % (time.clock())) self._drawTimer.stop() self._isDrawing = False self.endDraw.emit(pos) def wheelEvent(self, event): keys = QApplication.keyboardModifiers() k_alt = (keys == Qt.AltModifier) k_ctrl = (keys == Qt.ControlModifier) self.mousePos = self.mapToScene(event.pos()) grviewCenter = self.mapToScene(self.viewport().rect().center()) if event.delta() > 0: if k_alt: self.changeSlice(10) elif k_ctrl: scaleFactor = 1.1 self.doScale(scaleFactor) else: self.changeSlice(1) else: if k_alt: self.changeSlice(-10) elif k_ctrl: scaleFactor = 0.9 self.doScale(scaleFactor) else: self.changeSlice(-1) if k_ctrl: mousePosAfterScale = self.mapToScene(event.pos()) offset = self.mousePos - mousePosAfterScale newGrviewCenter = grviewCenter + offset self.centerOn(newGrviewCenter) self.mouseMoveEvent(event) def mousePressEvent(self, event): if event.button() == Qt.MidButton: self.setCursor(QCursor(Qt.SizeAllCursor)) self._lastPanPoint = event.pos() self._crossHairCursor.setVisible(False) self._dragMode = True if self._ticker.isActive(): self._deltaPan = QPointF(0, 0) if event.buttons() == Qt.RightButton: #make sure that we have the cursor at the correct position #before we call the context menu self.mouseMoveEvent(event) self.customContextMenuRequested.emit(event.pos()) return if not self.drawingEnabled: print "ImageView2D.mousePressEvent: drawing is not enabled" return if event.buttons() == Qt.LeftButton: #don't draw if flicker the view if self._ticker.isActive(): return if QApplication.keyboardModifiers() == Qt.ShiftModifier: self.erasingToggled.emit(True) self._tempErase = True mousePos = self.mapToScene(event.pos()) self.beginDrawing(mousePos) def mouseMoveEvent(self,event): if self._dragMode == True: #the mouse was moved because the user wants to change #the viewport self._deltaPan = QPointF(event.pos() - self._lastPanPoint) self._panning() self._lastPanPoint = event.pos() return if self._ticker.isActive(): #the view is still scrolling #do nothing until it comes to a complete stop return self.mousePos = mousePos = self.mapToScene(event.pos()) x = self.x = mousePos.x() y = self.y = mousePos.y() self.mouseMoved.emit(x,y) if self._isDrawing: self.drawing.emit(mousePos) def mouseReleaseEvent(self, event): if event.button() == Qt.MidButton: self.setCursor(QCursor()) releasePoint = event.pos() self._lastPanPoint = releasePoint self._dragMode = False self._ticker.start(20) if self._isDrawing: mousePos = self.mapToScene(event.pos()) self.endDrawing(mousePos) if self._tempErase: self.erasingToggled.emit(False) self._tempErase = False def _panning(self): hBar = self.horizontalScrollBar() vBar = self.verticalScrollBar() vBar.setValue(vBar.value() - self._deltaPan.y()) if self.isRightToLeft(): hBar.setValue(hBar.value() + self._deltaPan.x()) else: hBar.setValue(hBar.value() - self._deltaPan.x()) def _deaccelerate(self, speed, a=1, maxVal=64): x = self._qBound(-maxVal, speed.x(), maxVal) y = self._qBound(-maxVal, speed.y(), maxVal) ax ,ay = self._setdeaccelerateAxAy(speed.x(), speed.y(), a) if x > 0: x = max(0.0, x - a*ax) elif x < 0: x = min(0.0, x + a*ax) if y > 0: y = max(0.0, y - a*ay) elif y < 0: y = min(0.0, y + a*ay) return QPointF(x, y) def _qBound(self, minVal, current, maxVal): """PyQt4 does not wrap the qBound function from Qt's global namespace This is equivalent.""" return max(min(current, maxVal), minVal) def _setdeaccelerateAxAy(self, x, y, a): x = abs(x) y = abs(y) if x > y: if y > 0: ax = int(x / y) if ax != 0: return ax, 1 else: return x/a, 1 if y > x: if x > 0: ay = int(y/x) if ay != 0: return 1, ay else: return 1, y/a return 1, 1 def _tickerEvent(self): if self._deltaPan.x() == 0.0 and self._deltaPan.y() == 0.0 or self._dragMode == True: self._ticker.stop() else: self._deltaPan = self._deaccelerate(self._deltaPan) self._panning() def mouseDoubleClickEvent(self, event): mousePos = self.mapToScene(event.pos()) self.mouseDoubleClicked.emit(mousePos.x(), mousePos.y()) def changeSlice(self, delta): if self._isDrawing: self.endDrawing(self.mousePos) self._isDrawing = True #FIXME: #self._drawManager.beginDrawing(self.mousePos, self.self.shape2D) self.changeSliceDelta.emit(delta) #FIXME resurrect #InteractionLogger.log("%f: changeSliceDelta(axis, num) %d, %d" % (time.clock(), self._axis, delta)) def zoomOut(self): self.doScale(0.9) def zoomIn(self): self.doScale(1.1) def changeViewPort(self,qRectf): self.fitInView(qRectf,mode = Qt.KeepAspectRatio) def doScale(self, factor): self._zoomFactor = self._zoomFactor * factor InteractionLogger.log("%f: zoomFactor(factor) %f" % (time.clock(), self._zoomFactor)) self.scale(factor, factor)
class Node(QGraphicsItem): Type = QGraphicsItem.UserType + 1 def __init__(self, graphWidget, text=""): super(Node, self).__init__() self.graph = graphWidget self.edgeList = [] self.newPos = QPointF() self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.setZValue(1) self.text = text self.active = False self.b = 15 def type(self): return Node.Type def addEdge(self, edge): self.edgeList.append(edge) edge.adjust() def setActive(self, value=True): self.active = value def edges(self): return self.edgeList def calculateForces(self): if not self.scene() or self.scene().mouseGrabberItem() is self: self.newPos = self.pos() return # Sum up all forces pushing this item away. xvel = 0.0 yvel = 0.0 for item in self.scene().items(): if not isinstance(item, Node): continue line = QLineF(self.mapFromItem(item, 0, 0), QPointF(0, 0)) dx = line.dx() dy = line.dy() l = 2.0 * (dx * dx + dy * dy) if l > 0: xvel += (dx * 150.0) / l yvel += (dy * 150.0) / l # Now subtract all forces pulling items together. #weight = (len(self.edgeList) + 1) * 100.0 for edge in self.edgeList: if edge.sourceNode() is self: pos = self.mapFromItem(edge.destNode(), 0, 0) else: weight = edge.weight * 6 pos = self.mapFromItem(edge.sourceNode(), 0, 0) xvel += pos.x() / weight yvel += pos.y() / weight if qAbs(xvel) < 0.1 and qAbs(yvel) < 0.1: xvel = yvel = 0.0 sceneRect = self.scene().sceneRect() self.newPos = self.pos() + QPointF(xvel, yvel) self.newPos.setX( min(max(self.newPos.x(), sceneRect.left() + 10), sceneRect.right() - 10)) self.newPos.setY( min(max(self.newPos.y(), sceneRect.top() + 10), sceneRect.bottom() - 10)) def advance(self): if self.newPos == self.pos(): return False self.setPos(self.newPos) return True def boundingRect(self): adjust = 2.0 return QRectF(-self.b - adjust, -self.b - adjust, 2 * self.b + 3 + adjust, 2 * self.b + 3 + adjust) def shape(self): path = QPainterPath() path.addEllipse(-self.b, -self.b, 2 * self.b, 2 * self.b) return path def paint(self, painter, option, widget): self.setZValue(5) palette = QPalette() painter.setPen(Qt.NoPen) painter.setBrush(Qt.darkGray) gradient = QRadialGradient(-3, -3, 15) if option.state & QStyle.State_Sunken or self.active: gradient.setCenter(3, 3) gradient.setFocalPoint(3, 3) gradient.setColorAt( 1, palette.color(QPalette.Active, QPalette.Button)) gradient.setColorAt( 0, palette.color(QPalette.Active, QPalette.Button)) pen = QPen(palette.color(QPalette.Active, QPalette.ButtonText), 2) else: gradient.setColorAt( 1, palette.color(QPalette.Disabled, QPalette.Button)) gradient.setColorAt( 0, palette.color(QPalette.Disabled, QPalette.Button)) pen = QPen(palette.color(QPalette.Disabled, QPalette.ButtonText), 0) painter.setBrush(QBrush(gradient)) painter.setPen(pen) painter.drawEllipse(-self.b, -self.b, 2 * self.b, 2 * self.b) painter.drawText(self.boundingRect(), Qt.AlignCenter, self.text) def itemChange(self, change, value): if change == QGraphicsItem.ItemPositionHasChanged: for edge in self.edgeList: edge.adjust() self.graph.itemMoved() return super(Node, self).itemChange(change, value) def mousePressEvent(self, event): self.update() super(Node, self).mousePressEvent(event) def mouseReleaseEvent(self, event): self.update() super(Node, self).mouseReleaseEvent(event)
def __select(self, pos, bhighlight=True, select_type=None): """ Select an object based on computed distance. The x and y values are normalized first to ensure the same units for distance in x and y coordinates. The x axis is in samples while the y axis is in BPM. :param pos: :param bhighlight: :param select_type: Select specific type of curve (annotation) :type pos: QPointF :type select_type: str :return: """ # found, distance, point = None, 100, -1 debug = False if debug: print 'SELECT' dmax = self._distance found_curve = None found_point = None point_dist = np.infty line_dist = np.infty note_dist = np.infty ellipse_dist = np.infty caliper_dist = np.infty point_i = -1 bellipse = False bcaliper = False # the normalization have to be adjusted to aspect ratio of the plot size = self.__plot.size_plot_area() r_adjust = size.width() / float(size.height()) xmin = self.__plot.viewXMinSample() xmax = self.__plot.viewXMaxSample() rx = (xmax - xmin) / r_adjust # print xmin, xmax, (xmax - xmin) ymin = self.__plot.viewYMinSample() ymax = self.__plot.viewYMaxSample() ry = abs(ymax - ymin) # print ymin, ymax, (ymax - ymin) # normalize, aby mely x a y stejne jednotky (puvodne x je ve vzorcich a y v bpm) pos_clicked = pos pos = QPointF((pos.x() - xmin) / rx, (pos.y() - ymin) / ry) # pos = QPointF((pos.x() - xmin), (pos.y() - ymin) ) self.__unselect() # create local copy and add caliper (if visible) d_ann = self.__plot.ann_get().copy() if self.__plot.caliper_is_visible(): d_ann[self.__plot.caliper.id] = self.__plot.caliper for key, curve in d_ann.iteritems(): t = curve.get_curve_type() if select_type is not None: if t != select_type: continue x_from = (curve.x_from - xmin) / rx x_to = (curve.x_to - xmin) / rx y1 = y2 = None if curve.yval1 is not None: y1 = (curve.yval1 - ymin) / ry if curve.yval2 is not None: y2 = (curve.yval2 - ymin) / ry # x_from = curve.x_from - xmin # x_to = curve.x_to - xmin # y1 = curve.yval1 - ymin # y2 = curve.yval2 - ymin # print rx, ry # print pos, x_from, x_to, y1, y2 # if debug: # print 'normalize x a y:', x_from, x_to, y1, y2 if t == EnumAnnType.basal: line_dist = abs(pos.y() - y1) if debug: print 'basal (line dist):', line_dist elif t == EnumAnnType.baseline or t == EnumAnnType.recovery or t == EnumAnnType.no_recovery or \ t == EnumAnnType.excessive_ua or t == EnumAnnType.acceleration or t == EnumAnnType.deceleration or \ t == EnumAnnType.uterine_contraction: d0 = abs(x_from - pos.x()) d1 = abs(x_to - pos.x()) # print d0, d1 if d0 < d1: point_i = 0 point_dist = d0 else: point_i = 1 point_dist = d1 line_dist = abs(pos.y() - y1) if debug: print 'baseline etc (d0,d1,line dist):', d0, d1, line_dist elif t == EnumAnnType.ellipsenote: d = compute_dist_ellipse(x_from, x_to, y1, y2, pos) ellipse_dist = min(d) point_i = d.index(ellipse_dist) point_i = -1 if point_i == 0 else point_i bx = x_from <= pos.x() <= x_to by = y1 <= pos.y() <= y2 bellipse = bx and by if debug: print 'ellipse etc (ellipse_dist, bellipse):', ellipse_dist, bellipse elif t == EnumAnnType.note or t == EnumAnnType.evaluation_note: note_dist = abs(pos.x() - x_from) if debug: print 'note etc (note_dist):', note_dist elif isinstance(curve, PyQwtPlotFloatingBaseline): baseline_x, baseline_y = curve.get_points() # potrebuji normalizovat baseline_x = [(t - xmin) / rx for t in baseline_x] baseline_y = [(t - ymin) / ry for t in baseline_y] d = [distance_to_point(pos, x, y) for x, y in zip(baseline_x, baseline_y)] point_dist = min(d) point_i = d.index(point_dist) line_dist = point_dist if debug: print 'floating baseline (distances, point , dist):', d, point_i, point_dist elif isinstance(curve, Caliper): d = compute_dist_caliper(x_from, x_to, y1, y2, pos) caliper_dist = min(d) point_i = d.index(caliper_dist) point_i = -1 if point_i == 0 else point_i bx = x_from <= pos.x() <= x_to by = y1 <= pos.y() <= y2 bcaliper = bx and by # if clicked inside box and all distances are small if debug: print 'caliper etc (caliper_dist, point_i, bcaliper, d):', caliper_dist, point_i, bcaliper, d else: continue # return 0 if point_dist < dmax and line_dist < dmax: # if a point/line is close found_curve = curve found_point = point_i dmax = min(point_dist, line_dist) elif line_dist <= dmax and x_from < pos.x() < x_to: # if a line is close found_curve = curve found_point = -1 dmax = line_dist elif note_dist < dmax: # if a note is close found_curve = curve found_point = -1 dmax = note_dist elif ellipse_dist < dmax: # if an ellipse is close found_curve = curve found_point = point_i dmax = ellipse_dist elif caliper_dist < dmax: found_curve = curve found_point = point_i dmax = caliper_dist elif bellipse and found_curve is None: # if not another curve has been selected found_curve = curve found_point = -1 elif bcaliper and found_curve is None: # if not another curve has been selected found_curve = curve found_point = -1 if found_curve is not None: self.__selected_curve = found_curve self.__selected_point = found_point self.__highlight(bhighlight) self.__point_clicked = pos_clicked return True return False
def save(self, filename): growth_num = Result.growth_num fdata = StringIO() invert_pts, invert_cells = self.data.save(f=fdata) f = open(filename, 'w') w = csv.writer(f, delimiter=',') w.writerow(["TRKR_VERSION", Result.CURRENT_VERSION]) w.writerow(["Growth computation parameters"]) hf = self.header_fields for h in self.header_order: w.writerow([h] + hf[h][0](self)) w.writerow([]) w.writerow(["Growth per image"]) w.writerow(Result.fields) wall_shift = self.fields_num["wall"] - 1 for img_id in range(len(self.images)): img = self.images[img_id] w.writerow([img]) cells = self.cells[img_id] cells_area = self.cells_area[img_id] walls = self.walls[img_id] data = self.data[img] cells_shape = data.cells rows = [] for c in sorted(cells.keys()): # Get the center of mass of the cell cell = [data[p] for p in cells_shape[c] if p in data] center = QPointF(0, 0) area = 0.0 u1 = cell[-1] for i in range(len(cell)): u2 = cell[i] loc_area = u1.x() * u2.y() - u1.y() * u2.x() center += (u1 + u2) * loc_area area += loc_area u1 = u2 center /= area row = [ "", "Cell %d" % invert_cells[c], cells_area[c], cells[c][growth_num["kmax"]], cells[c][growth_num["kmin"]], cells[c][growth_num["theta"]] * 180 / pi, cells[c][growth_num["phi"]], center.x(), center.y() ] rows.append(row) lr = len(rows) for i, ws in enumerate(sorted(walls.keys())): wll = [ "", "Wall %d-%d" % (invert_pts[ws[0]], invert_pts[ws[1]]), walls[ws] ] if i >= lr: rows.append([""] * wall_shift) rows[i] += wll w.writerows(rows) w.writerow(["Actual cell shapes"]) w.writerow(["Image", "Cell", "Begin/End", "Shape [x y]"]) for img_id in range(len(self.images)): img = self.images[img_id] w.writerow([img]) cells_shapes = self.cells_shapes[img_id] rows = [] for c in sorted(cells_shapes.keys()): sh = cells_shapes[c] row1 = ["", "Cell %d" % invert_cells[c], "Begin"] + list( sh[0].flatten()) row2 = ["", "Cell %d" % invert_cells[c], "End"] + list( sh[1].flatten()) rows.append(row1) rows.append(row2) w.writerows(rows) w.writerow([]) w.writerow(["Data"]) f.write(fdata.getvalue()) f.close() fdata.close()
class XNodeConnection( QGraphicsPathItem ): """ Defines the base graphics item class that is used to draw a connection between two nodes. """ def __init__( self, scene ): self._visible = True super(XNodeConnection, self).__init__() # define custom properties self._textItem = None self._polygons = [] self._style = XConnectionStyle.Linear self._padding = 20 self._squashThreshold = 2 * scene.cellWidth() self._showDirectionArrow = False self._highlightPen = QPen(QColor('yellow')) self._disabledPen = QPen(QColor(100, 100, 100)) self._disableWithLayer = False self._enabled = True self._dirty = True self._customData = {} self._layer = None self._font = QApplication.instance().font() self._text = '' self._inputNode = None self._inputFixedY = None self._inputFixedX = None self._inputPoint = QPointF() self._inputLocation = XConnectionLocation.Left self._autoCalculateInputLocation = False self._showInputArrow = False self._outputNode = None self._outputFixedX = None self._outputFixedY = None self._outputPoint = QPointF() self._outputLocation = XConnectionLocation.Right self._autoCalculateOutputLocation = False self._showOutputArrow = False # set standard properties self.setFlags( self.ItemIsSelectable ) self.setZValue(-1) self.setPen( QColor('white') ) self.setLayer( scene.currentLayer() ) def autoCalculateInputLocation( self ): """ :remarks Returns whether or not to auto calculate the input location based on the proximity to the output node or point. :return <bool> """ return self._autoCalculateInputLocation def autoCalculateOutputLocation( self ): """ :remarks Returns whether or not to auto calculate the input location based on the proximity to the output node or point. :return <bool> """ return self._autoCalculateOutputLocation def connectSignals( self, node ): """ :remarks Connects to signals of the inputed node, if the node is a valid XNode type. :param node <XNode> || None :return <bool> success """ from projexui.widgets.xnodewidget.xnode import XNode # make sure we're connecting to a valid node if ( not isinstance(node, XNode) ): return False node.dispatch.geometryChanged.connect( self.setDirty ) node.dispatch.visibilityChanged.connect(self.setDirty) node.dispatch.removed.connect( self.forceRemove ) return True def controlPoints( self ): """ :remarks Generates the control points for this path :return <list> [ <tuple> ( <float> x, <float> y), .. ] """ # calculate the positions outputPoint = self.outputPoint() inputPoint = self.inputPoint() points = [] x0 = outputPoint.x() y0 = outputPoint.y() xN = inputPoint.x() yN = inputPoint.y() xC = (x0 + xN) / 2.0 yC = (y0 + yN) / 2.0 points.append((x0, y0)) oloc = self.outputLocation() iloc = self.inputLocation() left = XConnectionLocation.Left right = XConnectionLocation.Right bot = XConnectionLocation.Bottom top = XConnectionLocation.Top # create a right-to-left if ( (oloc & right) and (iloc & left) ): if ( xN < (x0 + self.squashThreshold()) ): points.append((x0+self.padding(), y0)) points.append((x0+self.padding(), yC)) points.append((xN-self.padding(), yC)) points.append((xN-self.padding(), yN)) else: points.append((xC, y0)) points.append((xC, yN)) # create a left-to-right elif ( (oloc & left) and (iloc & right) ): if ( (x0 - self.squashThreshold()) < xN ): points.append((x0-self.padding(), y0)) points.append((x0-self.padding(), yC)) points.append((xN+self.padding(), yC)) points.append((xN+self.padding(), yN)) else: points.append((xC, y0)) points.append((xC, yN)) # create a bottom-to-top elif ( (oloc & bot) and (iloc & top) ): if ( yN < (y0 + self.squashThreshold()) ): points.append((x0, y0+self.padding())) points.append((xC, y0+self.padding())) points.append((xC, yN-self.padding())) points.append((xN, yN-self.padding())) else: points.append((x0, yC)) points.append((xN, yC)) # create a top-to-bottom elif ( (oloc & top) and (iloc & bot) ): if ( (y0 - self.squashThreshold()) < yN ): points.append((x0, y0-self.padding())) points.append((xC, y0-self.padding())) points.append((xC, yN+self.padding())) points.append((xN, yN+self.padding())) else: points.append((x0, yC)) points.append((xN, yC)) # create a left-to-left elif ( (oloc & left) and (iloc & left) ): xMin = min(x0-self.padding(), xN-self.padding()) points.append((xMin, y0)) points.append((xMin, yN)) # create a right-to-right elif ( (oloc & right) and (iloc & right) ): xMax = max(x0+self.padding(), xN+self.padding()) points.append((xMax, y0)) points.append((xMax, yN)) # create a bottom-to-bottom elif ( (oloc & top) and (iloc & top) ): yMin = min(y0-self.padding(), yN-self.padding()) points.append((x0, yMin)) points.append((xN, yMin)) # create a bottom-to-bottom elif ( (oloc & bot) and (iloc & bot) ): yMax = max(y0+self.padding(), yN+self.padding()) points.append((x0, yMax)) points.append((xN, yMax)) # create a bottom-to-left or left-to-bottom elif ( ((oloc & bot) and (iloc & left)) or ((oloc & left) and (iloc & bot)) ): points.append((x0, yN)) # create a bottom-to-right or right-to-bottom elif ( ((oloc & bot) and (iloc & right)) or ((oloc & right) and (iloc & bot)) ): points.append((x0, y0)) # create a top-to-left or left-to-top elif ( ((oloc & top) and (iloc & left)) or ((oloc & left) and (iloc & top)) ): points.append((xN, yN)) # create a top-to-right or right-to-top elif ( ((oloc & top) and (iloc & right)) or ((oloc & right) and (iloc & top)) ): points.append((xN, y0)) points.append((xN, yN)) return points def customData( self, key, default = None ): """ Returns custom defined data that can be tracked per connection. :param key <str> :param default <variant> :return <variant> """ return self._customData.get(str(key), default) def direction( self ): """ Returns the output-to-input direction as a tuple of the output \ and input locations. :return (<XConnectionLocation> output, <XConnectionLocation> input) """ return (self.outputLocation(), self.inputLocation()) def disabledPen( self ): """ Returns the pen that should be used when rendering a disabled \ connection. :return <QPen> """ return self._disabledPen def disableWithLayer( self ): """ Returns whether or not this connection's enabled state should be \ affected by its layer. :return <bool> """ return self._disableWithLayer def disconnectSignals( self, node ): """ Disconnects from signals of the inputed node, if the node is a \ valid XNode type. :param node <XNode> || None :return <bool> success """ from projexui.widgets.xnodewidget.xnode import XNode # make sure we're disconnecting from a valid node if ( not isinstance(node, XNode) ): return False node.dispatch.geometryChanged.disconnect( self.setDirty ) node.dispatch.removed.disconnect( self.forceRemove ) return True def forceRemove( self ): """ Removes the object from the scene by queuing it up for removal. """ scene = self.scene() if ( scene ): scene.forceRemove(self) def font( self ): """ Returns the font for this connection. :return <QFont> """ return self._font def hasCustomData( self, key ): """ Returns whether or not there is the given key in the custom data. :param key | <str> :return <bool> """ return str(key) in self._customData def highlightPen( self ): """ Return the highlight pen for this connection. :return <QPen> """ return self._highlightPen def inputLocation( self ): """ Returns the input location for this connection. :return <XConnectionLocation> """ if ( not self.autoCalculateInputLocation() ): return self._inputLocation # auto calculate directions based on the scene if ( self._outputNode ): outputRect = self._outputNode.sceneRect() else: y = self._outputPoint.y() outputRect = QRectF( self._outputPoint.x(), y, 0, 0 ) if ( self._inputNode ): inputRect = self._inputNode.sceneRect() else: y = self._inputPoint.y() inputRect = QRectF( self._inputPoint.x(), y, 0, 0 ) # use the input location as potential places where it can be iloc = self._inputLocation left = XConnectionLocation.Left right = XConnectionLocation.Right top = XConnectionLocation.Top bot = XConnectionLocation.Bottom if ( self._inputNode == self._outputNode ): if ( iloc & right ): return right elif ( iloc & left ): return left elif ( iloc & top ): return top else: return bot elif ( (iloc & left) and outputRect.right() < inputRect.left() ): return left elif ( (iloc & right) and inputRect.right() < outputRect.left() ): return right elif ( (iloc & top) and outputRect.bottom() < inputRect.top() ): return top elif ( (iloc & bot) ): return bot elif ( (iloc & left) ): return left elif ( (iloc & right) ): return right elif ( (iloc & top) ): return top else: return left def inputNode( self ): """ Returns the input node that is connected to this connection. :return <XNode> """ return self._inputNode def inputFixedX( self ): """ Returns the fixed X value for the input option :return <float> || None """ return self._inputFixedX def inputFixedY( self ): """ Returns the fixed Y value for the input option. :return <float> || None """ return self._inputFixedY def inputPoint( self ): """ Returns a scene space point that the connection \ will draw to as its input target. If the connection \ has a node defined, then it will calculate the input \ point based on the position of the node, factoring in \ preference for input location and fixed information. \ If there is no node connected, then the point defined \ using the setInputPoint method will be used. :return <QPointF> """ node = self.inputNode() # return the set point if ( not node ): return self._inputPoint # otherwise, calculate the point based on location and fixed info ilocation = self.inputLocation() ifixedx = self.inputFixedX() ifixedy = self.inputFixedY() return node.positionAt( ilocation, ifixedx, ifixedy ) def isDirection( self, outputLocation, inputLocation ): """ Checks to see if the output and input locations match the settings \ for this item. :param outputLocation | <XConnectionLocation> :param inputLocation | <XConnectionLocation> :return <bool> """ return (self.isOutputLocation(outputLocation) and self.isInputLocation(inputLocation)) def isDirty( self ): """ Returns whether or not this path object is dirty and needs to \ be rebuilt. :return <bool> """ return self._dirty def isEnabled( self ): """ Returns whether or not this connection is enabled. :sa disableWithLayer :return <bool> """ if self._disableWithLayer and self._layer: lenabled = self._layer.isEnabled() elif self._inputNode and self._outputNode: lenabled = self._inputNode.isEnabled() and self._outputNode.isEnabled() else: lenabled = True return self._enabled and lenabled def isInputLocation( self, location ): """ Returns whether or not the inputed location value matches the \ given input location. :param location | <XConnectionLocation> :return <bool> """ return (self.inputLocation() & location) != 0 def isOutputLocation( self, location ): """ Returns whether or not the inputed location value matches the \ given output location. :param location | <XConnectionLocation> :return <bool> """ return (self.outputLocation() & location) != 0 def isStyle( self, style ): """ Return whether or not the connection is set to a particular style. :param style | <XConnectionStyle> :return <bool> """ return (self._style & style) != 0 def isVisible( self ): """ Returns whether or not this connection is visible. If either node it is connected to is hidden, then it should be as well. :return <bool> """ in_node = self.inputNode() out_node = self.outputNode() if ( in_node and not in_node.isVisible() ): return False if ( out_node and not out_node.isVisible() ): return False return self._visible def layer( self ): """ Returns the layer that this node is assigned to. :return <XNodeLayer> || None """ return self._layer def mappedPolygon( self, polygon, path = None, percent = 0.5 ): """ Maps the inputed polygon to the inputed path \ used when drawing items along the path. If no \ specific path is supplied, then this object's own \ path will be used. It will rotate and move the \ polygon according to the inputed percentage. :param polygon <QPolygonF> :param path <QPainterPath> :param percent <float> :return <QPolygonF> mapped_poly """ translatePerc = percent anglePerc = percent # we don't want to allow the angle percentage greater than 0.85 # or less than 0.05 or we won't get a good rotation angle if ( 0.95 <= anglePerc ): anglePerc = 0.98 elif ( anglePerc <= 0.05 ): anglePerc = 0.05 if ( not path ): path = self.path() if ( not (path and path.length()) ): return QPolygonF() # transform the polygon to the path point = path.pointAtPercent(translatePerc) angle = path.angleAtPercent(anglePerc) # rotate about the 0 axis transform = QTransform().rotate(-angle) polygon = transform.map(polygon) # move to the translation point transform = QTransform().translate(point.x(), point.y()) # create the rotated polygon mapped_poly = transform.map(polygon) self._polygons.append(mapped_poly) return mapped_poly def mousePressEvent( self, event ): """ Overloads the mouse press event to handle special cases and \ bypass when the scene is in view mode. :param event <QMousePressEvent> """ # ignore events when the scene is in view mode scene = self.scene() if ( scene and scene.inViewMode() ): event.ignore() return # block the selection signals if ( scene ): scene.blockSelectionSignals(True) # clear the selection if ( not (self.isSelected() or event.modifiers() == Qt.ControlModifier) ): for item in scene.selectedItems(): if ( item != self ): item.setSelected(False) # try to start the connection super(XNodeConnection, self).mousePressEvent(event) def mouseMoveEvent( self, event ): """ Overloads the mouse move event to ignore the event when \ the scene is in view mode. :param event <QMouseMoveEvent> """ # ignore events when the scene is in view mode scene = self.scene() if ( scene and (scene.inViewMode() or scene.isConnecting()) ): event.ignore() return # call the base method super(XNodeConnection, self).mouseMoveEvent(event) def mouseReleaseEvent( self, event ): """ Overloads the mouse release event to ignore the event when the \ scene is in view mode, and release the selection block signal. :param event <QMouseReleaseEvent> """ # ignore events when the scene is in view mode scene = self.scene() if ( scene and (scene.inViewMode() or scene.isConnecting()) ): event.ignore() return # emit the scene's connection menu requested signal if # the button was a right mouse button if ( event.button() == Qt.RightButton and scene ): scene.emitConnectionMenuRequested(self) event.accept() else: super(XNodeConnection, self).mouseReleaseEvent(event) # unblock the selection signals if ( scene ): scene.blockSelectionSignals(False) def opacity( self ): """ Returns the opacity amount for this connection. :return <float> """ in_node = self.inputNode() out_node = self.outputNode() if ( in_node and out_node and \ (in_node.isIsolateHidden() or out_node.isIsolateHidden()) ): return 0.1 opacity = super(XNodeConnection, self).opacity() layer = self.layer() if ( layer ): return layer.opacity() * opacity return opacity def outputLocation( self ): """ Returns the location for the output source position. :return <XConnectionLocation> """ if ( not self.autoCalculateOutputLocation() ): return self._outputLocation # auto calculate directions based on the scene if ( self._outputNode ): outputRect = self._outputNode.sceneRect() else: y = self._outputPoint.y() outputRect = QRectF( self._outputPoint.x(), y, 0, 0 ) if ( self._inputNode ): inputRect = self._inputNode.sceneRect() else: y = self._inputPoint.y() inputRect = QRectF( self._inputPoint.x(), y, 0, 0 ) oloc = self._outputLocation left = XConnectionLocation.Left right = XConnectionLocation.Right top = XConnectionLocation.Top bot = XConnectionLocation.Bottom if ( self._inputNode == self._outputNode ): if ( oloc & right ): return right elif ( oloc & left ): return left elif ( oloc & top ): return top else: return bot elif ( (oloc & right) and outputRect.right() < inputRect.left() ): return right elif ( (oloc & left) and inputRect.right() < outputRect.left() ): return left elif ( (oloc & bot) and outputRect.bottom() < inputRect.top() ): return bot elif ( (oloc & top) ): return top elif ( (oloc & right) ): return right elif ( (oloc & left) ): return left elif ( (oloc & bot) ): return bot else: return right def outputNode( self ): """ Returns the output source node that this connection is currently \ connected to. :return <XNode> || None """ return self._outputNode def outputFixedX( self ): """ Returns the fixed X position for the output component of this \ connection. :return <float> || None """ return self._outputFixedX def outputFixedY( self ): """ Returns the fixed Y position for the output component of this \ connection. :return <float> || None """ return self._outputFixedY def outputPoint( self ): """ Returns a scene space point that the connection \ will draw to as its output source. If the connection \ has a node defined, then it will calculate the output \ point based on the position of the node, factoring in \ preference for output location and fixed positions. If \ there is no node connected, then the point defined using \ the setOutputPoint method will be used. :return <QPointF> """ node = self.outputNode() # return the set point if ( not node ): return self._outputPoint # otherwise, calculate the point based on location and fixed positions olocation = self.outputLocation() ofixedx = self.outputFixedX() ofixedy = self.outputFixedY() return node.positionAt( olocation, ofixedx, ofixedy ) def padding( self ): """ Returns the amount of padding to be used when drawing a connection \ that will be drawn backwards. :return <float> """ return self._padding def paint( self, painter, option, widget ): """ Overloads the paint method from QGraphicsPathItem to \ handle custom drawing of the path using this items \ pens and polygons. :param painter <QPainter> :param option <QGraphicsItemStyleOption> :param widget <QWidget> """ # following the arguments required by Qt # pylint: disable-msg=W0613 painter.setOpacity(self.opacity()) # show the connection selected if ( not self.isEnabled() ): pen = QPen(self.disabledPen()) elif ( self.isSelected() ): pen = QPen(self.highlightPen()) else: pen = QPen(self.pen()) if ( self._textItem ): self._textItem.setOpacity(self.opacity()) self._textItem.setDefaultTextColor(pen.color().darker(110)) # rebuild first if necessary if ( self.isDirty() ): self.setPath(self.rebuild()) # store the initial hint hint = painter.renderHints() painter.setRenderHint( painter.Antialiasing ) pen.setWidthF(1.25) painter.setPen(pen) painter.drawPath(self.path()) # redraw the polys to force-fill them for poly in self._polygons: if ( not poly.isClosed() ): continue painter.setBrush(pen.color()) painter.drawPolygon(poly) # restore the render hints painter.setRenderHints(hint) def prepareToRemove( self ): """ Handles any code that needs to run to cleanup the connection \ before it gets removed from the scene. :return <bool> success """ # disconnect the signals from the input and output nodes for node in (self._outputNode, self._inputNode): self.disconnectSignals(node) # clear the pointers to the nodes self._inputNode = None self._outputNode = None return True def rebuild( self ): """ Rebuilds the path for this connection based on the given connection \ style parameters that have been set. :return <QPainterPath> """ # create the path path = self.rebuildPath() self._polygons = self.rebuildPolygons(path) if ( self._textItem ): point = path.pointAtPercent(0.5) metrics = QFontMetrics(self._textItem.font()) point.setY(point.y() - metrics.height() / 2.0) self._textItem.setPos(point) # create the path for the item for poly in self._polygons: path.addPolygon(poly) # unmark as dirty self.setDirty(False) return path def rebuildPath( self ): """ Rebuilds the path for the given style options based on the currently \ set parameters. :return <QPainterPath> """ # rebuild linear style if ( self.isStyle( XConnectionStyle.Linear ) ): return self.rebuildLinear() # rebuild block style elif ( self.isStyle( XConnectionStyle.Block ) ): return self.rebuildBlock() # rebuild smooth style elif ( self.isStyle( XConnectionStyle.Smooth ) ): return self.rebuildSmooth() # otherwise, we have an invalid style, or a style # defined by a subclass else: return QPainterPath() def rebuildPolygons( self, path ): """ Rebuilds the polygons that will be used on this path. :param path | <QPainterPath> :return <list> [ <QPolygonF>, .. ] """ output = [] # create the input arrow if ( self.showInputArrow() ): a = QPointF(-10, -3) b = QPointF(0, 0) c = QPointF(-10, 3) mpoly = self.mappedPolygon(QPolygonF([a, b, c, a]), path, 1.0 ) output.append( mpoly ) # create the direction arrow if ( self.showDirectionArrow() ): a = QPointF(-5, -3) b = QPointF(5, 0) c = QPointF(-5, 3) mpoly = self.mappedPolygon(QPolygonF([a, b, c, a]), path, 0.5 ) output.append( mpoly ) # create the output arrow if ( self.showOutputArrow() ): a = QPointF(10, -3) b = QPointF(0, 0) c = QPointF(10, 3) mpoly = self.mappedPolygon(QPolygonF([a, b, c, a]), path, 0 ) output.append( mpoly ) return output def rebuildLinear( self ): """ Rebuilds a linear path from the output to input points. :return <QPainterPath> """ points = self.controlPoints() x0, y0 = points[0] xN, yN = points[-1] # create a simple line between the output and # input points path = QPainterPath() path.moveTo(x0, y0) path.lineTo(xN, yN) return path def rebuildBlock( self ): """ Rebuilds a blocked path from the output to input points. :return <QPainterPath> """ # collect the control points points = self.controlPoints() # create the path path = QPainterPath() for i, point in enumerate(points): if ( not i ): path.moveTo( point[0], point[1] ) else: path.lineTo( point[0], point[1] ) return path def rebuildSmooth( self ): """ Rebuilds a smooth path based on the inputed points and set \ parameters for this item. :return <QPainterPath> """ # collect the control points points = self.controlPoints() # create the path path = QPainterPath() if ( len(points) == 3 ): x0, y0 = points[0] x1, y1 = points[1] xN, yN = points[2] path.moveTo(x0, y0) path.quadTo(x1, y1, xN, yN) elif ( len(points) == 4 ): x0, y0 = points[0] x1, y1 = points[1] x2, y2 = points[2] xN, yN = points[3] path.moveTo(x0, y0) path.cubicTo(x1, y1, x2, y2, xN, yN) elif ( len(points) == 6 ): x0, y0 = points[0] x1, y1 = points[1] x2, y2 = points[2] x3, y3 = points[3] x4, y4 = points[4] xN, yN = points[5] xC = (x2+x3) / 2.0 yC = (y2+y3) / 2.0 path.moveTo(x0, y0) path.cubicTo(x1, y1, x2, y2, xC, yC) path.cubicTo(x3, y3, x4, y4, xN, yN) else: x0, y0 = points[0] xN, yN = points[-1] path.moveTo(x0, y0) path.lineTo(xN, yN) return path def refreshVisible( self ): """ Refreshes whether or not this node should be visible based on its current visible state. """ super(XNodeConnection, self).setVisible(self.isVisible()) def setAutoCalculateInputLocation( self, state = True ): """ Sets whether or not to auto calculate the input location based on \ the proximity to the output node or point. :param state | <bool> """ self._autoCalculateInputLocation = state self.setDirty() def setAutoCalculateOutputLocation( self, state = True ): """ Sets whether or not to auto calculate the input location based on \ the proximity to the output node or point. :param state | <bool> """ self._autoCalculateOutputLocation = state self.setDirty() def setCustomData( self, key, value ): """ Stores the inputed value as custom data on this connection for \ the given key. :param key | <str> :param value | <variant> """ self._customData[str(key)] = value def setDirection( self, outputLocation, inputLocation ): """ Sets the output-to-input direction by setting both the locations \ at the same time. :param outputLocation | <XConnectionLocation> :param inputLocation | <XConnectionLocation> """ self.setOutputLocation(outputLocation) self.setInputLocation(inputLocation) def setDirty( self, state = True ): """ Flags the connection as being dirty and needing a rebuild. :param state | <bool> """ self._dirty = state # set if this connection should be visible if ( self._inputNode and self._outputNode ): vis = self._inputNode.isVisible() and self._outputNode.isVisible() self.setVisible(vis) def setDisabledPen( self, pen ): """ Sets the disabled pen that will be used when rendering a connection \ in a disabled state. :param pen | <QPen> """ self._disabledPen = QPen(pen) def setDisableWithLayer( self, state ): """ Sets whether or not this connection's layer's current state should \ affect its enabled state. :param state | <bool> """ self._disableWithLayer = state self.setDirty() def setEnabled( self, state ): """ Sets whether or not this connection is enabled or not. :param state | <bool> """ self._enabled = state def setFont( self, font ): """ Sets the font for this connection to the inputed font. :param font | <QFont> """ self._font = font def setHighlightPen( self, pen ): """ Sets the pen to be used when highlighting a selected connection. :param pen | <QPen> || <QColor> """ self._highlightPen = QPen(pen) def setInputLocation( self, location ): """ Sets the input location for where this connection should point to. :param location | <XConnectionLocation> """ self._inputLocation = location self.setDirty() def setInputNode( self, node ): """ Sets the node that will be recieving this connection as an input. :param node | <XNode> """ # if the node already matches the current input node, ignore if ( self._inputNode == node ): return # disconnect from the existing node self.disconnectSignals( self._inputNode ) # store the node self._inputNode = node # connect to the new node self.connectSignals( self._inputNode ) # force the rebuilding of the path self.setPath(self.rebuild()) def setInputFixedX( self, x ): """ Sets the fixed x position for the input component of this connection. :param x | <float> || None """ self._inputFixedX = x self.setDirty() def setInputFixedY( self, y ): """ Sets the fixed y position for the input component of this connection. :param y | <float> || None """ self._inputFixedY = y self.setDirty() def setInputPoint( self, point ): """ Sets the scene level input point position to draw the connection to. \ This is used mainly by the scene when drawing a user connection - \ it will only be used when there is no connected input node. :param point | <QPointF> """ self._inputPoint = point self.setPath(self.rebuild()) def setLayer( self, layer ): """ Sets the layer that this node is associated with to the given layer. :param layer | <XNodeLayer> || None :return <bool> changed """ if ( layer == self._layer ): return False self._layer = layer self.syncLayerData() return True def setOutputLocation( self, location ): """ Sets the location for the output part of the connection to generate \ from. :param location | <XConnectionLocation> """ self._outputLocation = location self.setDirty() def setOutputNode( self, node ): """ Sets the node that will be generating the output information for \ this connection. :param node | <XNode> """ # if the output node matches the current, ignore if ( node == self._outputNode ): return # disconnect from an existing node self.disconnectSignals( self._outputNode ) # set the current node self._outputNode = node self.connectSignals( self._outputNode ) # force the rebuilding of the path self.setPath( self.rebuild() ) def setOutputFixedX( self, x ): """ Sets the fixed x position for the output component of this connection. :param x | <float> || None """ self._outputFixedX = x self.setDirty() def setOutputFixedY( self, y ): """ Sets the fixed y position for the output component of this connection. :param y | <float> || None """ self._outputFixedY = y self.setDirty() def setOutputPoint( self, point ): """ Sets the scene space point for where this connection should draw \ its output from. This value will only be used if no output \ node is defined. :param point | <QPointF> """ self._outputPoint = point self.setPath( self.rebuild() ) def setPadding( self, padding ): """ Sets the padding amount that will be used when drawing a connection \ whose points will overlap. :param padding | <float> """ self._padding = padding self.setDirty() def setShowDirectionArrow( self, state = True ): """ Marks whether or not an arrow in the center of the path should be \ drawn, showing the direction that the connection is flowing in. :param state | <bool> """ self._showDirectionArrow = state self.setDirty() def setShowInputArrow( self, state = True ): """ :remarks Marks whether or not an arrow should be shown pointing at the input node. :param state <bool> """ self._showInputArrow = state self.setDirty() def setShowOutputArrow( self, state = True ): """ :remarks Marks whether or not an arrow should be shown pointing at the output node. :param state <bool> """ self._showOutputArrow = state self.setDirty() def setSquashThreshold( self, amount ): """ :remarks Sets the threshold limit of when the connection should start 'squashing', calculated based on the distance between the input and output points when rebuilding. :param amount <float> """ self._squashThreshold = amount self.setDirty() def setStyle( self, style ): """ :remarks Sets the style of the connection that will be used. :param style <XConnectionStyle> """ self._style = style self.setDirty() self.update() def setVisible( self, state ): """ Sets whether or not this connection's local visibility should be on. :param state | ,bool> """ self._visible = state super(XNodeConnection, self).setVisible(self.isVisible()) def setZValue( self, value ): """ Sets the z value for this connection, also updating the text item to match the value if one is defined. :param value | <int> """ super(XNodeConnection, self).setZValue(value) if ( self._textItem ): self._textItem.setZValue(value) def showDirectionArrow( self ): """ :remarks Return whether or not the direction arrow is visible for this connection. :return <bool> """ return self._showDirectionArrow def showInputArrow( self ): """ :remarks Return whether or not the input arrow is visible for this connection. :return <bool> """ return self._showInputArrow def showOutputArrow( self ): """ :remarks Return whether or not the output arrow is visible for this connection. :return <bool> """ return self._showOutputArrow def squashThreshold( self ): """ :remarks Returns the sqash threshold for when the line should be squashed based on the input and output points becoming too close together. :return <float> """ return self._squashThreshold def setText( self, text ): """ Sets the text for this connection to the inputed text. :param text | <str> """ self._text = text if ( text ): if ( self._textItem is None ): self._textItem = QGraphicsTextItem() self._textItem.setParentItem(self) self._textItem.setPlainText(text) elif ( self._textItem ): self.scene().removeItem(self._textItem) self._textItem = None def style( self ): """ :remarks Returns the style of the connection that is being drawn. :return style <XConnectionStyle> """ return self._style def syncLayerData( self, layerData = None ): """ Syncs the layer information for this item from the given layer data. :param layerData | <dict> """ if ( not self._layer ): return if ( not layerData ): layerData = self._layer.layerData() self.setVisible( layerData.get('visible', True) ) if ( layerData.get('current') ): # set the default parameters self.setFlags( self.ItemIsSelectable ) self.setAcceptHoverEvents(True) self.setZValue(99) else: # set the default parameters self.setFlags( self.GraphicsItemFlags(0) ) self.setAcceptHoverEvents(True) self.setZValue(layerData.get('zValue', 0)-1) def text( self ): """ Returns the text for this connection. :return <str> """ return self._text
class EEdge(QGraphicsObject): def __init__(self, head, tail, uuid): QGraphicsObject.__init__(self) if not issubclass(head.__class__, dict) and not isinstance( tail.__class__, dict): raise AttributeError self.setZValue(0.0) self.__kId = uuid self.__head = head self.__tail = tail if head[ENode.kGuiAttributeType].match(EAttribute.kTypeInput): self.__head = tail self.__tail = head self.__head[ENode.kGuiAttributeParent].onMove.connect(self.update) self.__tail[ENode.kGuiAttributeParent].onMove.connect(self.update) self.__headPoint = QPointF(0.0, 0.0) self.__tailPoint = QPointF(0.0, 0.0) self.__pen = QPen(QColor(43, 43, 43), 2, Qt.SolidLine) self.update() @property def Id(self): return self.__kId @property def Head(self): return self.__head @property def Tail(self): return self.__tail def pen(self): return self.__pen def setPen(self, pen): if not isinstance(pen, QPen): raise AttributeError self.__pen = pen def update(self): QGraphicsObject.prepareGeometryChange(self) self.__headPoint = self.mapFromItem( self.__head[ENode.kGuiAttributeParent], self.__head[ENode.kGuiAttributePlug]) self.__tailPoint = self.mapFromItem( self.__tail[ENode.kGuiAttributeParent], self.__tail[ENode.kGuiAttributePlug]) self.__headOffsetLine = QLineF( self.__headPoint, QPointF(self.__headPoint.x() + 15, self.__headPoint.y())) self.__tailOffsetLine = QLineF( self.__tailPoint, QPointF(self.__tailPoint.x() - 15, self.__tailPoint.y())) line = QLineF(self.__headPoint, self.__tailPoint) self.__line = line def boundingRect(self): extra = (self.pen().width() * 64) / 2 return QRectF( self.__line.p1(), QSizeF(self.__line.p2().x() - self.__line.p1().x(), self.__line.p2().y() - self.__line.p1().y())).normalized().adjusted( -extra, -extra, extra, extra) def shape(self): return QGraphicsObject.shape(self) def drawPath(self, startPoint, endPoint): path = QPainterPath() one = (QPointF(endPoint.x(), startPoint.y()) + startPoint) / 2 two = (QPointF(startPoint.x(), endPoint.y()) + endPoint) / 2 path.moveTo(startPoint) angle = math.pi / 2 bLine1 = QLineF() bLine1.setP1(startPoint) if startPoint.x() > endPoint.x(): dist = startPoint.x() - endPoint.x() one = (bLine1.p1() + QPointF(math.sin(angle) * dist, math.cos(angle) * dist)) bLine1.setP1(endPoint) two = (bLine1.p1() + QPointF(math.sin(angle) * dist, math.cos(angle) * dist)) path.cubicTo(one, two, endPoint) return path, QLineF(one, two) def paint(self, painter, option, widget=None): painter.setPen(self.pen()) headCenter = self.mapFromItem( self.__head[ENode.kGuiAttributeParent], self.__head[ENode.kGuiAttributeParent].boundingRect().center()) tailCenter = self.mapFromItem( self.__tail[ENode.kGuiAttributeParent], self.__tail[ENode.kGuiAttributeParent].boundingRect().center()) centerPoint = QLineF(headCenter, tailCenter).pointAt(0.5) centerPoint.setX(self.__headOffsetLine.p2().x()) lineFromHead = QLineF(self.__headOffsetLine.p2(), centerPoint) centerPoint.setX(self.__tailOffsetLine.p2().x()) lineFromTail = QLineF(self.__tailOffsetLine.p2(), centerPoint) painter.drawPath( self.drawPath(self.__headOffsetLine.p1(), self.__tailOffsetLine.p1())[0])