class BaseCanvas(QWidget): """The subclass can draw a blank canvas more easier.""" def __init__(self, parent: QWidget): """Set the parameters for drawing.""" super(BaseCanvas, self).__init__(parent) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.setFocusPolicy(Qt.StrongFocus) self.painter = QPainter() # Origin coordinate. self.ox = self.width() / 2 self.oy = self.height() / 2 # Canvas zoom rate. self.rate = 2. self.zoom = 2. * self.rate # Joint size. self.joint_size = 3 # Canvas line width. self.link_width = 3 self.path_width = 3 # Font size. self.font_size = 15 # Show point mark or dimension. self.show_point_mark = True self.show_dimension = True # Path track. self.Path = _Path() # Path solving. self.target_path = {} self.show_target_path = False # Background self.background = QImage() self.background_opacity = 1. self.background_scale = 1 self.background_offset = QPointF(0, 0) def paintEvent(self, event): """Using a QPainter under 'self', so just change QPen or QBrush before painting. """ self.painter.begin(self) self.painter.fillRect(event.rect(), QBrush(Qt.white)) # Translation self.painter.translate(self.ox, self.oy) # Background if not self.background.isNull(): rect = self.background.rect() self.painter.setOpacity(self.background_opacity) img_origin: QPointF = self.background_offset * self.zoom self.painter.drawImage( QRectF(img_origin, QSizeF( rect.width() * self.background_scale * self.zoom, rect.height() * self.background_scale * self.zoom )), self.background, QRectF(rect) ) self.painter.setOpacity(1) # Show frame. pen = QPen(Qt.blue) pen.setWidth(1) self.painter.setPen(pen) self.painter.setFont(QFont("Arial", self.font_size)) # Draw origin lines. pen.setColor(Qt.gray) self.painter.setPen(pen) x_l = -self.ox x_r = self.width() - self.ox self.painter.drawLine(QPointF(x_l, 0), QPointF(x_r, 0)) y_t = self.height() - self.oy y_b = -self.oy self.painter.drawLine(QPointF(0, y_b), QPointF(0, y_t)) def indexing(v): """Draw tick.""" return int(v / self.zoom - v / self.zoom % 5) for x in range(indexing(x_l), indexing(x_r) + 1, 5): self.painter.drawLine( QPointF(x, 0) * self.zoom, QPointF(x * self.zoom, -10 if x % 10 == 0 else -5) ) for y in range(indexing(y_b), indexing(y_t) + 1, 5): self.painter.drawLine( QPointF(0, y) * self.zoom, QPointF(10 if y % 10 == 0 else 5, y * self.zoom) ) # Please to call the "end" method when ending paint event. # self.painter.end() def drawPoint( self, i: int, cx, cy, fix: bool, color: QColor ): """Draw a joint.""" pen = QPen(color) pen.setWidth(2) self.painter.setPen(pen) x = cx * self.zoom y = cy * -self.zoom if fix: bottom = y + 20 width = 10 # Draw a triangle below. self.painter.drawPolygon( QPointF(x, y), QPointF(x - width, bottom), QPointF(x + width, bottom) ) r = self.joint_size * 2 else: r = self.joint_size self.painter.drawEllipse(QPointF(x, y), r, r) if not self.show_point_mark: return pen.setColor(Qt.darkGray) pen.setWidth(2) self.painter.setPen(pen) text = f"[{i}]" if type(i) == str else f"[Point{i}]" if self.show_dimension: text += f":({cx:.02f}, {cy:.02f})" self.painter.drawText(QPointF(x, y) + QPointF(6, -6), text) def drawTargetPath(self): """Draw solving path.""" pen = QPen() pen.setWidth(self.path_width) for i, name in enumerate(sorted(self.target_path)): path = self.target_path[name] road, dot, brush = target_path_style(i) pen.setColor(road) self.painter.setPen(pen) self.painter.setBrush(brush) if len(path) == 1: x, y = path[0] p = QPointF(x, -y) * self.zoom self.painter.drawText(p + QPointF(6, -6), name) pen.setColor(dot) self.painter.setPen(pen) self.painter.drawEllipse(p, self.joint_size, self.joint_size) else: painter_path = QPainterPath() for j, (x, y) in enumerate(path): p = QPointF(x, -y) * self.zoom self.painter.drawEllipse(p, self.joint_size, self.joint_size) if j == 0: self.painter.drawText(p + QPointF(6, -6), name) painter_path.moveTo(p) else: x2, y2 = path[j - 1] self.__draw_arrow(x, -y, x2, -y2, zoom=True) painter_path.lineTo(p) pen.setColor(road) self.painter.setPen(pen) self.painter.drawPath(painter_path) for x, y in path: pen.setColor(dot) self.painter.setPen(pen) p = QPointF(x, -y) * self.zoom self.painter.drawEllipse(p, self.joint_size, self.joint_size) self.painter.setBrush(Qt.NoBrush) def __draw_arrow( self, x1: float, y1: float, x2: float, y2: float, *, zoom: bool = False, text: str = '' ): """Front point -> Back point""" if zoom: x1 *= self.zoom y1 *= self.zoom x2 *= self.zoom y2 *= self.zoom a = atan2(y2 - y1, x2 - x1) x1 = (x1 + x2) / 2 - 7.5 * cos(a) y1 = (y1 + y2) / 2 - 7.5 * sin(a) first_point = QPointF(x1, y1) self.painter.drawLine(first_point, QPointF( x1 + 15 * cos(a + radians(20)), y1 + 15 * sin(a + radians(20)) )) self.painter.drawLine(first_point, QPointF( x1 + 15 * cos(a - radians(20)), y1 + 15 * sin(a - radians(20)) )) if not text: return # Font font = self.painter.font() font_copy = QFont(font) font.setBold(True) font.setPointSize(font.pointSize() + 8) self.painter.setFont(font) # Color pen = self.painter.pen() color = pen.color() pen.setColor(color.darker()) self.painter.setPen(pen) self.painter.drawText(first_point, text) pen.setColor(color) self.painter.setPen(pen) self.painter.setFont(font_copy) def drawCurve(self, path: Sequence[Tuple[float, float]]): """Draw path as curve.""" if len(set(path)) <= 2: return painter_path = QPainterPath() error = False for i, (x, y) in enumerate(path): if isnan(x): error = True self.painter.drawPath(painter_path) painter_path = QPainterPath() else: x *= self.zoom y *= -self.zoom if i == 0: painter_path.moveTo(x, y) self.painter.drawEllipse(QPointF(x, y), self.joint_size, self.joint_size) continue if error: painter_path.moveTo(x, y) error = False else: painter_path.lineTo(x, y) self.painter.drawPath(painter_path) def drawDot(self, path: Sequence[Tuple[float, float]]): """Draw path as dots.""" if len(set(path)) <= 2: return for x, y in path: if isnan(x): continue self.painter.drawPoint(QPointF(x, -y) * self.zoom) def solutionPolygon( self, func: str, args: Sequence[str], target: str, pos: Union[Tuple[VPoint, ...], Dict[int, Tuple[float, float]]] ) -> Tuple[List[QPointF], QColor]: """Get solution polygon.""" if func == 'PLLP': color = QColor(121, 171, 252) params = [args[0], args[-1]] elif func == 'PLAP': color = QColor(249, 84, 216) params = [args[0]] else: if func == 'PLPP': color = QColor(94, 255, 185) else: # PXY color = QColor(249, 175, 27) params = [args[0]] params.append(target) tmp_list = [] for name in params: try: index = int(name.replace('P', '')) except ValueError: continue else: x, y = pos[index] tmp_list.append(QPointF(x, -y) * self.zoom) return tmp_list, color def drawSolution( self, func: str, args: Sequence[str], target: str, pos: Union[Tuple[VPoint, ...], Dict[int, Tuple[float, float]]] ): """Draw the solution triangle.""" points, color = self.solutionPolygon(func, args, target, pos) color.setAlpha(150) pen = QPen(color) pen.setWidth(self.joint_size) self.painter.setPen(pen) def draw_arrow(index: int, text: str): """Draw arrow.""" self.__draw_arrow( points[-1].x(), points[-1].y(), points[index].x(), points[index].y(), text=text ) draw_arrow(0, args[1]) if func == 'PLLP': draw_arrow(1, args[2]) color.setAlpha(30) self.painter.setBrush(QBrush(color)) self.painter.drawPolygon(QPolygonF(points)) self.painter.setBrush(Qt.NoBrush)
def to_graph( g: Graph, width: int, engine: Union[str, Pos], node_mode: bool, show_label: bool, monochrome: bool, *, except_node: Optional[int] = None ) -> QIcon: """Draw a generalized chain graph.""" pos: Pos = engine_picker(g, engine, node_mode) if not pos: pixmap = QPixmap(width, width) pixmap.fill(Qt.transparent) return QIcon(pixmap) width_bound = -float('inf') for x, y in pos.values(): if abs(x) > width_bound: width_bound = x if abs(y) > width_bound: width_bound = y width_bound *= 2.5 image = QImage( QSize(int(width_bound), int(width_bound)), QImage.Format_ARGB32_Premultiplied ) image.fill(Qt.transparent) painter = QPainter(image) painter.translate(image.width() / 2, image.height() / 2) pen = QPen() r = int(width_bound / 50) pen.setWidth(r) painter.setPen(pen) _font.setPixelSize(r * 6) painter.setFont(_font) # Draw edges. if node_mode: for l1, l2 in g.edges: if except_node in {l1, l2}: pen.setColor(Qt.gray) else: pen.setColor(Qt.black) painter.setPen(pen) painter.drawLine( QPointF(pos[l1][0], -pos[l1][1]), QPointF(pos[l2][0], -pos[l2][1]) ) else: if monochrome: color = QColor(Qt.darkGray) else: color = QColor(226, 219, 190) color.setAlpha(150) painter.setBrush(QBrush(color)) for link in g.nodes: if link == except_node: pen.setColor(Qt.gray) else: pen.setColor(Qt.black) painter.setPen(pen) painter.drawPolygon(*convex_hull([ (pos[n][0], -pos[n][1]) for n, edge in edges_view(g) if link in edge ], as_qpoint=True)) # Draw nodes. for k, (x, y) in pos.items(): if node_mode: color = color_num(len(list(g.neighbors(k))) - 1) if k == except_node: color.setAlpha(150) else: if monochrome: color = Qt.black elif except_node in dict(edges_view(g))[k]: color = color_qt('Green') else: color = color_qt('Blue') pen.setColor(color) painter.setPen(pen) painter.setBrush(QBrush(color)) point = QPointF(x, -y) painter.drawEllipse(point, r, r) if show_label: pen.setColor(Qt.darkMagenta) painter.setPen(pen) painter.drawText(point, str(k)) painter.end() return QIcon(QPixmap.fromImage(image).scaledToWidth(width))
class BaseCanvas(QWidget): """The subclass can draw a blank canvas more easier.""" def __init__(self, parent=None): super(BaseCanvas, self).__init__(parent) self.setSizePolicy(QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding )) #Origin coordinate. self.ox = self.width()/2 self.oy = self.height()/2 #Canvas zoom rate. self.rate = 2 self.zoom = 2 * self.rate #Canvas line width. self.linkWidth = 3 self.pathWidth = 3 #Font size. self.fontSize = 10 #Show point mark or dimension. self.showPointMark = True self.showDimension = True #Path track. self.Path = Path() #Path solving. self.targetPath = {} self.showTargetPath = False def paintEvent(self, event): """Using a QPainter under 'self', so just change QPen or QBrush before painting. """ self.painter = QPainter() self.painter.begin(self) self.painter.fillRect(event.rect(), QBrush(Qt.white)) self.painter.translate(self.ox, self.oy) #Draw origin lines. pen = QPen(Qt.gray) pen.setWidth(1) self.painter.setPen(pen) x_l = -self.ox x_r = self.width()-self.ox self.painter.drawLine(QPointF(x_l, 0), QPointF(x_r, 0)) y_t = self.height()-self.oy y_b = -self.oy self.painter.drawLine(QPointF(0, y_b), QPointF(0, y_t)) #Draw tick. Indexing = lambda v: int(v/self.zoom - (v/self.zoom)%5) for x in range(Indexing(x_l), Indexing(x_r)+1, 5): self.painter.drawLine( QPointF(x*self.zoom, 0), QPointF(x*self.zoom, -10 if x%10==0 else -5) ) for y in range(Indexing(y_b), Indexing(y_t)+1, 5): self.painter.drawLine( QPointF(0, y*self.zoom), QPointF(10 if y%10==0 else 5, y*self.zoom) ) """Please to call the "end" method when ending paint event. self.painter.end() """ def drawFrame(self): """Draw a outer frame.""" positive_x = self.width() - self.ox positive_y = -self.oy negative_x = -self.ox negative_y = self.height() - self.oy self.painter.drawLine( QPointF(negative_x, positive_y), QPointF(positive_x, positive_y) ) self.painter.drawLine( QPointF(negative_x, negative_y), QPointF(positive_x, negative_y) ) self.painter.drawLine( QPointF(negative_x, positive_y), QPointF(negative_x, negative_y) ) self.painter.drawLine( QPointF(positive_x, positive_y), QPointF(positive_x, negative_y) ) def drawPoint(self, i: int, cx, cy, fix: bool, color: QColor ): """Draw a joint.""" pen = QPen(color) pen.setWidth(2) self.painter.setPen(pen) x = cx*self.zoom y = cy*-self.zoom if fix: bottom = y + 20 width = 10 #Draw a triangle below. self.painter.drawPolygon( QPointF(x, y), QPointF(x - width, bottom), QPointF(x + width, bottom) ) self.painter.drawEllipse(QPointF(x, y), width, width) else: self.painter.drawEllipse(QPointF(x, y), 5, 5) if not self.showPointMark: return pen.setColor(Qt.darkGray) pen.setWidth(2) self.painter.setPen(pen) self.painter.setFont(QFont("Arial", self.fontSize)) text = "[{}]".format(i) if type(i)==str else "[Point{}]".format(i) if self.showDimension: text += ":({:.02f}, {:.02f})".format(cx, cy) self.painter.drawText(QPointF(x+6, y-6), text) def drawTargetPath(self): """Draw solving path.""" pen = QPen() pen.setWidth(self.pathWidth) for i, name in enumerate(sorted(self.targetPath)): path = self.targetPath[name] Pen, Dot, Brush = colorPath(i) pen.setColor(Pen) self.painter.setPen(pen) self.painter.setBrush(Brush) if len(path)>1: pointPath = QPainterPath() for i, (x, y) in enumerate(path): x *= self.zoom y *= -self.zoom self.painter.drawEllipse(QPointF(x, y), 4, 4) if i==0: self.painter.drawText(QPointF(x+6, y-6), name) pointPath.moveTo(x, y) else: x2, y2 = path[i-1] self.drawArrow(x, y, x2*self.zoom, y2*-self.zoom) pointPath.lineTo(QPointF(x, y)) self.painter.drawPath(pointPath) for x, y in path: pen.setColor(Dot) self.painter.setPen(pen) self.painter.drawEllipse( QPointF(x*self.zoom, y*-self.zoom), 3, 3 ) elif len(path)==1: x = path[0][0]*self.zoom y = path[0][1]*-self.zoom self.painter.drawText(QPointF(x+6, y-6), name) pen.setColor(Dot) self.painter.setPen(pen) self.painter.drawEllipse(QPointF(x, y), 3, 3) self.painter.setBrush(Qt.NoBrush) def drawArrow(self, x1: float, y1: float, x2: float, y2: float): """Front point -> Back point""" a = atan2(y2 - y1, x2 - x1) x1 = (x1 + x2) / 2 - 7.5*cos(a) y1 = (y1 + y2) / 2 - 7.5*sin(a) self.painter.drawLine( QPointF(x1, y1), QPointF(x1 + 15*cos(a + radians(20)), y1 + 15*sin(a + radians(20))) ) self.painter.drawLine( QPointF(x1, y1), QPointF(x1 + 15*cos(a - radians(20)), y1 + 15*sin(a - radians(20))) ) def drawCurve(self, path): pointPath = QPainterPath() for i, (x, y) in enumerate(path): if isnan(x): continue else: if i==0: pointPath.moveTo(x*self.zoom, y*-self.zoom) else: pointPath.lineTo(QPointF(x*self.zoom, y*-self.zoom)) self.painter.drawPath(pointPath) def drawDot(self, path): for x, y in path: if isnan(x): continue else: self.painter.drawPoint(QPointF(x*self.zoom, y*-self.zoom))
class BaseCanvas(QWidget): """The subclass can draw a blank canvas more easier.""" def __init__(self, parent=None): super(BaseCanvas, self).__init__(parent) self.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) #Origin coordinate. self.ox = self.width() / 2 self.oy = self.height() / 2 #Canvas zoom rate. self.rate = 2 self.zoom = 2 * self.rate #Joint size. self.jointsize = 5 #Canvas line width. self.linkWidth = 3 self.pathWidth = 3 #Font size. self.fontSize = 15 #Show point mark or dimension. self.showPointMark = True self.showDimension = True #Path track. self.Path = Path() #Path solving. self.targetPath = {} self.showTargetPath = False def paintEvent(self, event): """Using a QPainter under 'self', so just change QPen or QBrush before painting. """ self.painter = QPainter() self.painter.begin(self) self.painter.fillRect(event.rect(), QBrush(Qt.white)) self.painter.translate(self.ox, self.oy) #Draw origin lines. pen = QPen(Qt.gray) pen.setWidth(1) self.painter.setPen(pen) x_l = -self.ox x_r = self.width() - self.ox self.painter.drawLine(QPointF(x_l, 0), QPointF(x_r, 0)) y_t = self.height() - self.oy y_b = -self.oy self.painter.drawLine(QPointF(0, y_b), QPointF(0, y_t)) #Draw tick. Indexing = lambda v: int(v / self.zoom - (v / self.zoom) % 5) for x in range(Indexing(x_l), Indexing(x_r) + 1, 5): self.painter.drawLine( QPointF(x * self.zoom, 0), QPointF(x * self.zoom, -10 if x % 10 == 0 else -5)) for y in range(Indexing(y_b), Indexing(y_t) + 1, 5): self.painter.drawLine( QPointF(0, y * self.zoom), QPointF(10 if y % 10 == 0 else 5, y * self.zoom)) """Please to call the "end" method when ending paint event. self.painter.end() """ def __drawPoint(self, i: int, cx, cy, fix: bool, color: QColor): """Draw a joint.""" pen = QPen(color) pen.setWidth(2) self.painter.setPen(pen) x = cx * self.zoom y = cy * -self.zoom if fix: bottom = y + 20 width = 10 #Draw a triangle below. self.painter.drawPolygon(QPointF(x, y), QPointF(x - width, bottom), QPointF(x + width, bottom)) self.painter.drawEllipse(QPointF(x, y), width, width) else: self.painter.drawEllipse(QPointF(x, y), self.jointsize, self.jointsize) if not self.showPointMark: return pen.setColor(Qt.darkGray) pen.setWidth(2) self.painter.setPen(pen) self.painter.setFont(QFont("Arial", self.fontSize)) text = "[{}]".format(i) if type(i) == str else "[Point{}]".format(i) if self.showDimension: text += ":({:.02f}, {:.02f})".format(cx, cy) self.painter.drawText(QPointF(x + 6, y - 6), text) def __drawTargetPath(self): """Draw solving path.""" pen = QPen() pen.setWidth(self.pathWidth) for i, name in enumerate(sorted(self.targetPath)): path = self.targetPath[name] Pen, Dot, Brush = colorPath(i) pen.setColor(Pen) self.painter.setPen(pen) self.painter.setBrush(Brush) if len(path) > 1: pointPath = QPainterPath() for j, (x, y) in enumerate(path): x *= self.zoom y *= -self.zoom self.painter.drawEllipse(QPointF(x, y), RADIUS, RADIUS) if j == 0: self.painter.drawText(QPointF(x + 6, y - 6), name) pointPath.moveTo(x, y) else: x2, y2 = path[j - 1] self.__drawArrow(x, y, x2 * self.zoom, y2 * -self.zoom) pointPath.lineTo(QPointF(x, y)) self.painter.drawPath(pointPath) for x, y in path: pen.setColor(Dot) self.painter.setPen(pen) self.painter.drawEllipse( QPointF(x * self.zoom, y * -self.zoom), RADIUS, RADIUS) elif len(path) == 1: x = path[0][0] * self.zoom y = path[0][1] * -self.zoom self.painter.drawText(QPointF(x + 6, y - 6), name) pen.setColor(Dot) self.painter.setPen(pen) self.painter.drawEllipse(QPointF(x, y), RADIUS, RADIUS) self.painter.setBrush(Qt.NoBrush) def __drawArrow(self, x1: float, y1: float, x2: float, y2: float): """Front point -> Back point""" a = atan2(y2 - y1, x2 - x1) x1 = (x1 + x2) / 2 - 7.5 * cos(a) y1 = (y1 + y2) / 2 - 7.5 * sin(a) self.painter.drawLine( QPointF(x1, y1), QPointF(x1 + 15 * cos(a + radians(20)), y1 + 15 * sin(a + radians(20)))) self.painter.drawLine( QPointF(x1, y1), QPointF(x1 + 15 * cos(a - radians(20)), y1 + 15 * sin(a - radians(20)))) def __drawCurve(self, path: Sequence[Tuple[float, float]]): """Draw path as curve.""" pointPath = QPainterPath() error = False for i, (x, y) in enumerate(path): if isnan(x): error = True self.painter.drawPath(pointPath) pointPath = QPainterPath() else: x *= self.zoom y *= -self.zoom if i == 0: pointPath.moveTo(x, y) self.painter.drawEllipse(QPointF(x, y), RADIUS, RADIUS) continue if error: pointPath.moveTo(x, y) error = False else: pointPath.lineTo(x, y) self.painter.drawPath(pointPath) def __drawDot(self, path: Sequence[Tuple[float, float]]): """Draw path as dots.""" for x, y in path: if isnan(x): continue self.painter.drawPoint(QPointF(x * self.zoom, y * -self.zoom)) def __drawSolution(self, func: str, args: Tuple[str], target: str, pos: TypeVar('Coords', Tuple[VPoint], Dict[int, Tuple[float, float]])): """Draw the solution triangle.""" if func == 'PLLP': color = QColor(121, 171, 252) params = [args[0], args[-1]] elif func == 'PLAP': color = QColor(249, 84, 216) params = [args[0]] elif func == 'PLPP': color = QColor(94, 255, 185) params = [args[0]] params.append(target) pen = QPen() pen.setColor(color) pen.setWidth(RADIUS) self.painter.setPen(pen) def drawArrow(n: int) -> bool: """Draw arrow and return True if done.""" try: x, y = pos[int(params[-1].replace('P', ''))] x2, y2 = pos[int(params[n].replace('P', ''))] except ValueError: return False else: self.__drawArrow(x * self.zoom, y * -self.zoom, x2 * self.zoom, y2 * -self.zoom) return True if not drawArrow(0): return if func == 'PLLP': if not drawArrow(1): return color.setAlpha(30) self.painter.setBrush(QBrush(color)) qpoints = [] for name in params: x, y = pos[int(name.replace('P', ''))] qpoints.append(QPointF(x * self.zoom, y * -self.zoom)) self.painter.drawPolygon(*qpoints) self.painter.setBrush(Qt.NoBrush)