def select_area(self, area: Optional[Area]): self.area = area if area is None: return if "total_boundings" in area.extra: min_x, max_x, min_y, max_y = [ area.extra["total_boundings"][k] for k in ["x1", "x2", "y1", "y2"] ] else: min_x, min_y = math.inf, math.inf max_x, max_y = -math.inf, -math.inf for node in area.nodes: if node.location is None: continue min_x = min(min_x, node.location.x) min_y = min(min_y, node.location.y) max_x = max(max_x, node.location.x) max_y = max(max_y, node.location.y) image_path = self.game.data_path.joinpath( "assets", "maps", f"{area.map_name}.png") if self.game is not None else None if image_path is not None and image_path.exists(): self._background_image = QtGui.QImage(os.fspath(image_path)) min_x = area.extra.get("map_min_x", 0) min_y = area.extra.get("map_min_y", 0) max_x = self._background_image.width() - area.extra.get( "map_max_x", 0) max_y = self._background_image.height() - area.extra.get( "map_max_y", 0) self.image_bounds = BoundsInt(min_x, min_y, max_x, max_y) self.world_bounds = BoundsFloat(min_x, min_y, max_x, max_y) else: self.update_world_bounds() self.area_bounds = BoundsFloat( min_x=min_x, min_y=min_y, max_x=max_x, max_y=max_y, ) self.area_size = QSizeF( max(max_x - min_x, 1), max(max_y - min_y, 1), ) self.update()
def ScalePicture(self): rect = QRectF(self.graphicsItem.pos(), QSizeF( self.pixMap.size())) unity = self.graphicsView.transform().mapRect(QRectF(0, 0, 1, 1)) width = unity.width() height = unity.height() if width <= 0 or height <= 0: return self.graphicsView.scale(1 / width, 1 / height) viewRect = self.graphicsView.viewport().rect() sceneRect = self.graphicsView.transform().mapRect(rect) if sceneRect.width() <= 0 or sceneRect.height() <= 0: return x_ratio = viewRect.width() / sceneRect.width() y_ratio = viewRect.height() / sceneRect.height() x_ratio = y_ratio = min(x_ratio, y_ratio) self.graphicsView.scale(x_ratio, y_ratio) # if self.readImg.isStripModel: # height2 = self.pixMap.size().height() / 2 # height3 = self.graphicsView.size().height()/2 # height3 = height3/x_ratio # p = self.graphicsItem.pos() # self.graphicsItem.setPos(p.x(), p.y()+height2-height3) self.graphicsView.centerOn(rect.center()) for _ in range(abs(self.scaleCnt)): if self.scaleCnt > 0: self.graphicsView.scale(1.1, 1.1) else: self.graphicsView.scale(1/1.1, 1/1.1)
def CreateSelectionRect(self) -> QRectF: cursorScene = self.mapToScene(self._currentCursorPosition.toPoint()) if self._state is State.SELECTING: selectionRect = QRectF(0, 0, abs(self._boxSelectionRectAnchor.x() - cursorScene.x()), abs(self._boxSelectionRectAnchor.y() - cursorScene.y())) selectionRect.moveCenter((cursorScene + self._boxSelectionRectAnchor) / 2.0) else: selectionRect = QRectF(cursorScene, QSizeF()) return selectionRect
def _update_scale_variables(self): self.border_x = self.rect().width() * 0.05 self.border_y = self.rect().height() * 0.05 canvas_width = max(self.rect().width() - self.border_x * 2, 1) canvas_height = max(self.rect().height() - self.border_y * 2, 1) self.scale = min(canvas_width / self.area_size.width(), canvas_height / self.area_size.height()) self.canvas_size = QSizeF(canvas_width, canvas_width)
def __init__(self): super().__init__() scene = QGraphicsScene() self.setScene(scene) self.setRenderHint(QPainter.Antialiasing) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setMouseTracking(True) self._offset = QPointF(0, 0) self._zoom = 0 self.backgroundColor = QColor(40, 40, 40) self.gridSpacing = QSizeF(60, 60) self.gridThickness = 1 self.gridColor = QColor(100, 100, 100) self.gridZoomThreshold = -1.5 self.showGrid = True self.selectionBoxStrokeColor = QColor(52, 222, 235) self.selectionBoxFillColor = QColor(52, 222, 235, 50) self.selectionBoxThickness = 2 self._editing = True self._hoveredItems: List[ChipItem] = [] self._selectedItems: List[ChipItem] = [] self._sceneItems: Set[ChipItem] = set() self._boxSelectionRectAnchor = QPointF() self._currentCursorPosition = QPointF() self.selectionBox = self.scene().addRect(QRectF(), QPen(self.selectionBoxStrokeColor, self.selectionBoxThickness), QBrush(self.selectionBoxFillColor)) self.selectionBox.setVisible(False) self._state = State.IDLE self.UpdateView()
class ChipSceneViewer(QGraphicsView): selectionChanged = Signal(list) def __init__(self): super().__init__() scene = QGraphicsScene() self.setScene(scene) self.setRenderHint(QPainter.Antialiasing) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setMouseTracking(True) self._offset = QPointF(0, 0) self._zoom = 0 self.backgroundColor = QColor(40, 40, 40) self.gridSpacing = QSizeF(60, 60) self.gridThickness = 1 self.gridColor = QColor(100, 100, 100) self.gridZoomThreshold = -1.5 self.showGrid = True self.selectionBoxStrokeColor = QColor(52, 222, 235) self.selectionBoxFillColor = QColor(52, 222, 235, 50) self.selectionBoxThickness = 2 self._editing = True self._hoveredItems: List[ChipItem] = [] self._selectedItems: List[ChipItem] = [] self._sceneItems: Set[ChipItem] = set() self._boxSelectionRectAnchor = QPointF() self._currentCursorPosition = QPointF() self.selectionBox = self.scene().addRect(QRectF(), QPen(self.selectionBoxStrokeColor, self.selectionBoxThickness), QBrush(self.selectionBoxFillColor)) self.selectionBox.setVisible(False) self._state = State.IDLE self.UpdateView() def SetEditing(self, editing: bool): self._editing = editing self.showGrid = editing for item in self._sceneItems: item.SetEditDisplay(editing) if not editing: self.DeselectAll() def GetSelectedItems(self): return self._selectedItems def AddItem(self, item: ChipItem): if item not in self._sceneItems: self._sceneItems.add(item) item.Move(QPointF()) item.onRemoved.connect(self.RemoveItem) self.scene().addItem(item.GraphicsObject()) item.SetEditDisplay(self._editing) return item def RemoveItem(self, item: ChipItem): self.DeselectItem(item) if item in self._hoveredItems: self._hoveredItems.remove(item) if item in self._sceneItems: self._sceneItems.remove(item) self.scene().removeItem(item.GraphicsObject()) def GetItems(self): return self._sceneItems def RemoveAll(self): self._hoveredItems.clear() self._selectedItems.clear() for item in self._sceneItems.copy(): self.RemoveItem(item) def SelectItem(self, item: ChipItem): if item not in self._selectedItems: item.SetSelected(True) self._selectedItems.append(item) def ToggleSelectItem(self, item: ChipItem): if item in self._selectedItems: self.DeselectItem(item) else: self.SelectItem(item) def DeselectItem(self, item: ChipItem): if item in self._selectedItems: item.SetSelected(False) self._selectedItems.remove(item) def DeselectAll(self): for item in self._selectedItems.copy(): self.DeselectItem(item) def Recenter(self): itemsRect = QRectF() for item in self._sceneItems: itemsRect = itemsRect.united(item.GraphicsObject().boundingRect().translated(item.GraphicsObject().pos())) self._offset = itemsRect.center() self.UpdateView() def CenterItem(self, item: ChipItem): QApplication.processEvents() sceneCenter = self.mapToScene(self.rect().center()) currentCenter = item.GraphicsObject().sceneBoundingRect().center() delta = sceneCenter - currentCenter item.Move(delta) def UpdateView(self): matrix = QTransform() matrix.scale(2 ** self._zoom, 2 ** self._zoom) self.setTransform(matrix) self.setSceneRect(QRectF(self._offset.x(), self._offset.y(), 10, 10)) self.UpdateSelectionBox() self.UpdateHoveredItems() @staticmethod def GetSelectionMode(): if QApplication.keyboardModifiers() == Qt.ShiftModifier: return SelectionMode.MODIFY return SelectionMode.NORMAL def CreateSelectionRect(self) -> QRectF: cursorScene = self.mapToScene(self._currentCursorPosition.toPoint()) if self._state is State.SELECTING: selectionRect = QRectF(0, 0, abs(self._boxSelectionRectAnchor.x() - cursorScene.x()), abs(self._boxSelectionRectAnchor.y() - cursorScene.y())) selectionRect.moveCenter((cursorScene + self._boxSelectionRectAnchor) / 2.0) else: selectionRect = QRectF(cursorScene, QSizeF()) return selectionRect def UpdateHoveredItems(self): if self._state is State.PANNING: return if not self._editing or self._state is State.MOVING: hoveredChipItems = [] else: # What are we hovering over in the scene hoveredGraphicsItems = [item for item in self.scene().items(self.selectionBox.sceneBoundingRect())] hoveredChipItems = [item for item in self._sceneItems if item.GraphicsObject() in hoveredGraphicsItems] # Make sure we maintain the found order hoveredChipItems.sort(key=lambda x: hoveredGraphicsItems.index(x.GraphicsObject())) if self._state is not State.SELECTING: hoveredChipItems = hoveredChipItems[:1] for item in hoveredChipItems: if item not in self._hoveredItems and item.CanSelect(): item.SetHovered(True) for item in self._hoveredItems: if item not in hoveredChipItems: item.SetHovered(False) self._hoveredItems = hoveredChipItems def UpdateSelectionBox(self): if self._state == State.SELECTING: self.selectionBox.setVisible(True) self.selectionBox.prepareGeometryChange() else: self.selectionBox.setVisible(False) self.selectionBox.setRect(self.CreateSelectionRect()) def drawBackground(self, painter: QPainter, rect: QRectF): currentColor = self.backgroundBrush().color() if currentColor != self.backgroundColor: self.setBackgroundBrush(QBrush(self.backgroundColor)) super().drawBackground(painter, rect) if self._zoom <= self.gridZoomThreshold or not self.showGrid: return painter.setPen(QPen(self.gridColor, self.gridThickness)) lines = [] if self.gridSpacing.width() > 0: xStart = rect.left() - rect.left() % self.gridSpacing.width() while xStart <= rect.right(): line = QLineF(xStart, rect.bottom(), xStart, rect.top()) lines.append(line) xStart = xStart + self.gridSpacing.width() if self.gridSpacing.height() > 0: yStart = rect.top() - rect.top() % self.gridSpacing.height() while yStart <= rect.bottom(): line = QLineF(rect.left(), yStart, rect.right(), yStart) lines.append(line) yStart = yStart + self.gridSpacing.height() painter.drawLines(lines) def wheelEvent(self, event: QWheelEvent): numSteps = float(event.angleDelta().y()) / 1000 oldWorldPos = self.mapToScene(self._currentCursorPosition.toPoint()) self._zoom = min(self._zoom + numSteps, 0) self.UpdateView() newWorldPos = self.mapToScene(self._currentCursorPosition.toPoint()) delta = newWorldPos - oldWorldPos self._offset -= delta self.UpdateView() def mousePressEvent(self, event): if self._state != State.IDLE: return if event.button() == Qt.RightButton: self._state = State.PANNING elif event.button() == Qt.LeftButton and self._editing: if len(self._hoveredItems) == 0: self._state = State.SELECTING self._boxSelectionRectAnchor = self.mapToScene(self._currentCursorPosition.toPoint()) else: hoveredItem = self._hoveredItems[0] if self.GetSelectionMode() is SelectionMode.MODIFY: self.ToggleSelectItem(hoveredItem) self.selectionChanged.emit(self._selectedItems) else: if hoveredItem not in self._selectedItems: self.DeselectAll() self.SelectItem(hoveredItem) self.selectionChanged.emit(self._selectedItems) if hoveredItem.CanMove(self.mapToScene(self._currentCursorPosition.toPoint())): self._state = State.MOVING self.UpdateView() super().mousePressEvent(event) def mouseMoveEvent(self, event: QMouseEvent): # Update position movement newCursorPosition = event.localPos() if self._currentCursorPosition is None: deltaScene = QPointF() else: deltaScene = (self.mapToScene(newCursorPosition.toPoint()) - self.mapToScene(self._currentCursorPosition.toPoint())) self._currentCursorPosition = newCursorPosition if self._state is State.PANNING: self._offset -= deltaScene self.UpdateView() elif self._state is State.MOVING: for selectedItem in self._selectedItems: selectedItem.Move(deltaScene) self.UpdateView() super().mouseMoveEvent(event) def mouseReleaseEvent(self, event: QMouseEvent): super().mouseReleaseEvent(event) if self._state == State.PANNING: if event.button() == Qt.RightButton: self._state = State.IDLE elif self._state == State.SELECTING: if event.button() == Qt.LeftButton: self._state = State.IDLE if self.GetSelectionMode() is SelectionMode.MODIFY: for item in self._hoveredItems: self.ToggleSelectItem(item) else: self.DeselectAll() for item in self._hoveredItems: self.SelectItem(item) self.selectionChanged.emit(self._selectedItems) elif self._state == State.MOVING: if event.button() == Qt.LeftButton: self._state = State.IDLE self.UpdateView() super().mouseReleaseEvent(event) def keyReleaseEvent(self, event: QKeyEvent): if event.key() == Qt.Key.Key_Delete: self.DeleteSelected() if event.key() == Qt.Key.Key_D and event.modifiers() == Qt.Modifier.CTRL: self.DuplicateSelected() super().keyReleaseEvent(event) def DeleteSelected(self): for item in self.GetSelectedItems().copy(): if item.CanDelete(): item.RequestDelete() def DuplicateSelected(self): newItems = [] for item in self.GetSelectedItems(): if item.CanDuplicate(): newItem = item.Duplicate() newItem.Move(QPointF(50, 50)) self.AddItem(newItem) newItems.append(newItem) if newItems: self.DeselectAll() [self.SelectItem(item) for item in newItems] self.selectionChanged.emit(self.GetSelectedItems())
class DataEditorCanvas(QtWidgets.QWidget): game: Optional[RandovaniaGame] = None world: Optional[World] = None area: Optional[Area] = None highlighted_node: Optional[Node] = None _background_image: Optional[QtGui.QImage] = None world_bounds: BoundsFloat area_bounds: BoundsFloat area_size: QSizeF image_bounds: BoundsInt edit_mode: bool = True scale: float border_x: float = 75 border_y: float = 75 canvas_size: QSizeF _next_node_location: NodeLocation = NodeLocation(0, 0, 0) CreateNodeRequest = Signal(NodeLocation) MoveNodeRequest = Signal(Node, NodeLocation) SelectNodeRequest = Signal(Node) SelectAreaRequest = Signal(Area) SelectConnectionsRequest = Signal(Node) ReplaceConnectionsRequest = Signal(Node, Requirement) CreateDockRequest = Signal(NodeLocation, Area) MoveNodeToAreaRequest = Signal(Node, Area) state: Optional[State] = None visible_nodes: Optional[set[Node]] = None def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self._show_all_connections_action = QtGui.QAction( "Show all node connections", self) self._show_all_connections_action.setCheckable(True) self._show_all_connections_action.setChecked(False) self._show_all_connections_action.triggered.connect(self.update) self._create_node_action = QtGui.QAction("Create node here", self) self._create_node_action.triggered.connect(self._on_create_node) self._move_node_action = QtGui.QAction("Move selected node here", self) self._move_node_action.triggered.connect(self._on_move_node) def _on_create_node(self): self.CreateNodeRequest.emit(self._next_node_location) def _on_move_node(self): self.MoveNodeRequest.emit(self.highlighted_node, self._next_node_location) def set_edit_mode(self, value: bool): self.edit_mode = value def select_game(self, game: RandovaniaGame): self.game = game def select_world(self, world: World): self.world = world image_path = self.game.data_path.joinpath( "assets", "maps", f"{world.name}.png") if self.game is not None else None if image_path is not None and image_path.exists(): self._background_image = QtGui.QImage(os.fspath(image_path)) self.image_bounds = BoundsInt( min_x=world.extra.get("map_min_x", 0), min_y=world.extra.get("map_min_y", 0), max_x=self._background_image.width() - world.extra.get("map_max_x", 0), max_y=self._background_image.height() - world.extra.get("map_max_y", 0), ) else: self._background_image = None self.update_world_bounds() self.update() def update_world_bounds(self): if self.world is None: return min_x, min_y = math.inf, math.inf max_x, max_y = -math.inf, -math.inf for area in self.world.areas: total_boundings = area.extra.get("total_boundings") if total_boundings is None: continue min_x = min(min_x, total_boundings["x1"], total_boundings["x2"]) max_x = max(max_x, total_boundings["x1"], total_boundings["x2"]) min_y = min(min_y, total_boundings["y1"], total_boundings["y2"]) max_y = max(max_y, total_boundings["y1"], total_boundings["y2"]) self.world_bounds = BoundsFloat( min_x=min_x, min_y=min_y, max_x=max_x, max_y=max_y, ) def get_image_point(self, x: float, y: float): bounds = self.image_bounds return QPointF(bounds.min_x + (bounds.max_x - bounds.min_x) * x, bounds.min_y + (bounds.max_y - bounds.min_y) * y) def select_area(self, area: Optional[Area]): self.area = area if area is None: return if "total_boundings" in area.extra: min_x, max_x, min_y, max_y = [ area.extra["total_boundings"][k] for k in ["x1", "x2", "y1", "y2"] ] else: min_x, min_y = math.inf, math.inf max_x, max_y = -math.inf, -math.inf for node in area.nodes: if node.location is None: continue min_x = min(min_x, node.location.x) min_y = min(min_y, node.location.y) max_x = max(max_x, node.location.x) max_y = max(max_y, node.location.y) image_path = self.game.data_path.joinpath( "assets", "maps", f"{area.map_name}.png") if self.game is not None else None if image_path is not None and image_path.exists(): self._background_image = QtGui.QImage(os.fspath(image_path)) min_x = area.extra.get("map_min_x", 0) min_y = area.extra.get("map_min_y", 0) max_x = self._background_image.width() - area.extra.get( "map_max_x", 0) max_y = self._background_image.height() - area.extra.get( "map_max_y", 0) self.image_bounds = BoundsInt(min_x, min_y, max_x, max_y) self.world_bounds = BoundsFloat(min_x, min_y, max_x, max_y) else: self.update_world_bounds() self.area_bounds = BoundsFloat( min_x=min_x, min_y=min_y, max_x=max_x, max_y=max_y, ) self.area_size = QSizeF( max(max_x - min_x, 1), max(max_y - min_y, 1), ) self.update() def highlight_node(self, node: Node): self.highlighted_node = node self.update() def set_state(self, state: Optional[State]): self.state = state self.highlighted_node = state.node self.update() def set_visible_nodes(self, visible_nodes: Optional[set[Node]]): self.visible_nodes = visible_nodes self.update() def is_node_visible(self, node: Node) -> bool: return self.visible_nodes is None or node in self.visible_nodes def is_connection_visible(self, requirement: Requirement) -> bool: return self.state is None or requirement.satisfied( self.state.resources, self.state.energy, self.state.resource_database) def _update_scale_variables(self): self.border_x = self.rect().width() * 0.05 self.border_y = self.rect().height() * 0.05 canvas_width = max(self.rect().width() - self.border_x * 2, 1) canvas_height = max(self.rect().height() - self.border_y * 2, 1) self.scale = min(canvas_width / self.area_size.width(), canvas_height / self.area_size.height()) self.canvas_size = QSizeF(canvas_width, canvas_width) def _nodes_at_position(self, qt_local_position: QPointF): return [ node for node in self.area.nodes if node.location is not None and (self.game_loc_to_qt_local(node.location) - qt_local_position).manhattanLength() < 10 ] def _other_areas_at_position(self, qt_local_position: QPointF): result = [] for area in self.world.areas: if "total_boundings" not in area.extra or area == self.area: continue bounds = BoundsFloat.from_bounds(area.extra["total_boundings"]) tl = self.game_loc_to_qt_local([bounds.min_x, bounds.min_y]) br = self.game_loc_to_qt_local([bounds.max_x, bounds.max_y]) rect = QRectF(tl, br) if rect.contains(qt_local_position): result.append(area) return result def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None: local_pos = QPointF(self.mapFromGlobal(event.globalPos())) local_pos -= self.get_area_canvas_offset() nodes_at_mouse = self._nodes_at_position(local_pos) if nodes_at_mouse: if len(nodes_at_mouse) == 1: self.SelectNodeRequest.emit(nodes_at_mouse[0]) return areas_at_mouse = self._other_areas_at_position(local_pos) if areas_at_mouse: if len(areas_at_mouse) == 1: self.SelectAreaRequest.emit(areas_at_mouse[0]) return def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None: local_pos = QPointF(self.mapFromGlobal(event.globalPos())) local_pos -= self.get_area_canvas_offset() self._next_node_location = self.qt_local_to_game_loc(local_pos) menu = QtWidgets.QMenu(self) if self.state is None: menu.addAction(self._show_all_connections_action) if self.edit_mode: menu.addAction(self._create_node_action) menu.addAction(self._move_node_action) self._move_node_action.setEnabled( self.highlighted_node is not None) if self.highlighted_node is not None: self._move_node_action.setText( f"Move {self.highlighted_node.name} here") # Areas Menu menu.addSeparator() areas_at_mouse = self._other_areas_at_position(local_pos) for area in areas_at_mouse: sub_menu = QtWidgets.QMenu(f"Area: {area.name}", self) sub_menu.addAction("View area").triggered.connect( functools.partial(self.SelectAreaRequest.emit, area)) if self.edit_mode: sub_menu.addAction( "Create dock here to this area").triggered.connect( functools.partial(self.CreateDockRequest.emit, self._next_node_location, area)) menu.addMenu(sub_menu) if not areas_at_mouse: sub_menu = QtGui.QAction("No areas here", self) sub_menu.setEnabled(False) menu.addAction(sub_menu) # Nodes Menu menu.addSeparator() nodes_at_mouse = self._nodes_at_position(local_pos) if self.highlighted_node in nodes_at_mouse: nodes_at_mouse.remove(self.highlighted_node) for node in nodes_at_mouse: if len(nodes_at_mouse) == 1: menu.addAction(node.name).setEnabled(False) sub_menu = menu else: sub_menu = QtWidgets.QMenu(node.name, self) sub_menu.addAction("Highlight this").triggered.connect( functools.partial(self.SelectNodeRequest.emit, node)) view_connections = sub_menu.addAction("View connections to this") view_connections.setEnabled( (self.edit_mode and self.highlighted_node != node) or (node in self.area.connections.get(self.highlighted_node, {}))) view_connections.triggered.connect( functools.partial(self.SelectConnectionsRequest.emit, node)) if self.edit_mode: sub_menu.addSeparator() sub_menu.addAction( "Replace connection with Trivial").triggered.connect( functools.partial( self.ReplaceConnectionsRequest.emit, node, Requirement.trivial(), )) sub_menu.addAction("Remove connection").triggered.connect( functools.partial( self.ReplaceConnectionsRequest.emit, node, Requirement.impossible(), )) if areas_at_mouse: move_menu = QtWidgets.QMenu("Move to...", self) for area in areas_at_mouse: move_menu.addAction(area.name).triggered.connect( functools.partial( self.MoveNodeToAreaRequest.emit, node, area, )) sub_menu.addMenu(move_menu) if sub_menu != menu: menu.addMenu(sub_menu) if not nodes_at_mouse: sub_menu = QtGui.QAction("No other nodes here", self) sub_menu.setEnabled(False) menu.addAction(sub_menu) # Done menu.exec_(event.globalPos()) def game_loc_to_qt_local(self, pos: Union[NodeLocation, list[float]]) -> QPointF: if isinstance(pos, NodeLocation): x = pos.x y = pos.y else: x, y = pos[0], pos[1] return QPointF(self.scale * (x - self.area_bounds.min_x), self.scale * (self.area_bounds.max_y - y)) def qt_local_to_game_loc(self, pos: QPointF) -> NodeLocation: return NodeLocation((pos.x() / self.scale) + self.area_bounds.min_x, self.area_bounds.max_y - (pos.y() / self.scale), 0) def get_area_canvas_offset(self): return QPointF( (self.width() - self.area_size.width() * self.scale) / 2, (self.height() - self.area_size.height() * self.scale) / 2, ) def paintEvent(self, event: QtGui.QPaintEvent) -> None: if self.world is None or self.area is None: return self._update_scale_variables() painter = QtGui.QPainter(self) painter.setPen(QtGui.Qt.white) painter.setFont(QtGui.QFont("Arial", 10)) # Center what we're drawing painter.translate(self.get_area_canvas_offset()) if self._background_image is not None: scaled_border_x = 8 * self.border_x / self.scale scaled_border_y = 8 * self.border_y / self.scale wbounds = self.world_bounds abounds = self.area_bounds # Calculate the top-left corner and bottom-right of the background image percent_x_start = (abounds.min_x - wbounds.min_x - scaled_border_x ) / (wbounds.max_x - wbounds.min_x) percent_x_end = (abounds.max_x - wbounds.min_x + scaled_border_x) / (wbounds.max_x - wbounds.min_x) percent_y_start = 1 - (abounds.max_y - wbounds.min_y + scaled_border_y) / (wbounds.max_y - wbounds.min_y) percent_y_end = 1 - (abounds.min_y - wbounds.min_y - scaled_border_y) / (wbounds.max_y - wbounds.min_y) painter.drawImage( QRectF(-scaled_border_x * self.scale, -scaled_border_y * self.scale, (scaled_border_x * 2 + self.area_size.width()) * self.scale, (scaled_border_y * 2 + self.area_size.height()) * self.scale), self._background_image, QRectF(self.get_image_point(percent_x_start, percent_y_start), self.get_image_point(percent_x_end, percent_y_end))) area = self.area if "polygon" in area.extra: points = [ self.game_loc_to_qt_local(p) for p in area.extra["polygon"] ] painter.drawPolygon(points, QtGui.Qt.FillRule.OddEvenFill) def draw_connections_from(source_node: Node): if source_node.location is None: return for target_node, requirement in area.connections[ source_node].items(): if target_node.location is None: continue if not self.is_connection_visible(requirement): painter.setPen(QtGui.Qt.darkGray) elif source_node == self.highlighted_node or self.state is not None: painter.setPen(QtGui.Qt.white) else: painter.setPen(QtGui.Qt.gray) source = self.game_loc_to_qt_local(source_node.location) target = self.game_loc_to_qt_local(target_node.location) line = QtCore.QLineF(source, target) line_len = line.length() if line_len == 0: continue end_point = line.pointAt(1 - 7 / line_len) line.setPoints(end_point, line.pointAt(5 / line_len)) painter.drawLine(line) line_angle = line.angle() line.setAngle(line_angle + 30) tri_point_1 = line.pointAt(15 / line_len) line.setAngle(line_angle - 30) tri_point_2 = line.pointAt(15 / line_len) arrow = QtGui.QPolygonF([end_point, tri_point_1, tri_point_2]) painter.drawPolygon(arrow) brush = painter.brush() brush.setStyle(QtGui.Qt.BrushStyle.SolidPattern) painter.setBrush(brush) if self._show_all_connections_action.isChecked( ) or self.visible_nodes is not None: for node in area.nodes: if node != self.highlighted_node: draw_connections_from(node) if self.highlighted_node is not None and self.highlighted_node in area.nodes: draw_connections_from(self.highlighted_node) painter.setPen(QtGui.Qt.white) for node in area.nodes: if node.location is None: continue if self.is_node_visible(node): brush.setColor(_color_for_node.get(type(node), QtGui.Qt.yellow)) else: brush.setColor(QtGui.Qt.darkGray) painter.setBrush(brush) p = self.game_loc_to_qt_local(node.location) if self.highlighted_node == node: painter.drawEllipse(p, 7, 7) painter.drawEllipse(p, 5, 5) centered_text(painter, p + QPointF(0, 15), node.name)